env.info( '*** MOOSE GITHUB Commit Hash ID: 2025-10-13T18:52:26+02:00-3a95e75c2d0e79365990fbb694373fb0c83eee67 ***' )

-- Automatic dynamic loading of development files, if they exists.
-- Try to load Moose as individual script files from <DcsInstallDir\Script\Moose
-- which should be a Junction link to the MOOSE repository subfolder "Moose Development\Moose".
-- This method is used by Moose developers and not mission builders.
if not MOOSE_DEVELOPMENT_FOLDER then
	MOOSE_DEVELOPMENT_FOLDER='Scripts'
end

ModuleLoader = MOOSE_DEVELOPMENT_FOLDER..'/Moose/Modules.lua'

if io then
  local f=io.open(ModuleLoader,"r")
  if f~=nil then
    io.close(f)

    env.info( '*** MOOSE DYNAMIC INCLUDE START *** ' )

    local base = _G

    __Moose = {}

    __Moose.Include = function( IncludeFile )
      if not __Moose.Includes[ IncludeFile ] then
        __Moose.Includes[IncludeFile] = IncludeFile
        local f = assert( base.loadfile( IncludeFile ) )
        if f == nil then
          error ("Moose: Could not load Moose file " .. IncludeFile )
        else
          env.info( "Moose: " .. IncludeFile .. " dynamically loaded." )
          return f()
        end
      end
    end

    __Moose.Includes = {}

    __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Modules.lua' )
    BASE:TraceOnOff( true )
    env.info( '*** MOOSE INCLUDE END *** ' )

    -- Skip the static part of this file completly
    do return end
  end
else
  env.info( '*** MOOSE DYNAMIC INCLUDE NOT POSSIBLE (Desanitize io to use it) *** ' )
end

-- Individual Moose files are not found. Use the static code below.
env.info( '*** MOOSE STATIC INCLUDE START *** ' )

--- **Utilities** Enumerators.
-- 
-- An enumerator is a variable that holds a constant value. Enumerators are very useful because they make the code easier to read and to change in general.
-- 
-- For example, instead of using the same value at multiple different places in your code, you should use a variable set to that value.
-- If, for whatever reason, the value needs to be changed, you only have to change the variable once and do not have to search through you code and reset
-- every value by hand.
-- 
-- Another big advantage is that the LDT intellisense "knows" the enumerators. So you can use the autocompletion feature and do not have to keep all the
-- values in your head or look them up in the docs. 
-- 
-- DCS itself provides a lot of enumerators for various things. See [Enumerators](https://wiki.hoggitworld.com/view/Category:Enumerators) on Hoggit.
-- 
-- Other Moose classes also have enumerators. For example, the AIRBASE class has enumerators for airbase names.
-- 
-- @module Utilities.Enums
-- @image MOOSE.JPG

--- [DCS Enum world](https://wiki.hoggitworld.com/view/DCS_enum_world)
-- @type ENUMS

--- Because ENUMS are just better practice.
-- 
--  The ENUMS class adds some handy variables, which help you to make your code better and more general.
--
-- @field #ENUMS
ENUMS = {}

--- Suppress the error box
env.setErrorMessageBoxEnabled( false )

--- Rules of Engagement.
-- @type ENUMS.ROE
-- @field #number WeaponFree [AIR] AI will engage any enemy group it detects. Target prioritization is based based on the threat of the target.
-- @field #number OpenFireWeaponFree [AIR] AI will engage any enemy group it detects, but will prioritize targets specified in the groups tasking.
-- @field #number OpenFire [AIR, GROUND, NAVAL] AI will engage only targets specified in its taskings.
-- @field #number ReturnFire [AIR, GROUND, NAVAL] AI will only engage threats that shoot first.
-- @field #number WeaponHold [AIR, GROUND, NAVAL] AI will hold fire under all circumstances.
ENUMS.ROE = {
  WeaponFree=0,
  OpenFireWeaponFree=1,
  OpenFire=2,
  ReturnFire=3,
  WeaponHold=4,
  }

--- Reaction On Threat.
-- @type ENUMS.ROT
-- @field #number NoReaction No defensive actions will take place to counter threats.
-- @field #number PassiveDefense AI will use jammers and other countermeasures in an attempt to defeat the threat. AI will not attempt a maneuver to defeat a threat.
-- @field #number EvadeFire AI will react by performing defensive maneuvers against incoming threats. AI will also use passive defense.
-- @field #number BypassAndEscape AI will attempt to avoid enemy threat zones all together. This includes attempting to fly above or around threats.
-- @field #number AllowAbortMission If a threat is deemed severe enough the AI will abort its mission and return to base.
ENUMS.ROT = {
  NoReaction=0,
  PassiveDefense=1,
  EvadeFire=2,
  BypassAndEscape=3,
  AllowAbortMission=4,
}

--- Alarm state.
-- @type ENUMS.AlarmState
-- @field #number Auto AI will automatically switch alarm states based on the presence of threats. The AI kind of cheats in this regard.
-- @field #number Green Group is not combat ready. Sensors are stowed if possible.
-- @field #number Red Group is combat ready and actively searching for targets. Some groups like infantry will not move in this state.
ENUMS.AlarmState = {
  Auto=0,
  Green=1,
  Red=2,
}

--- Weapon types. See the [Weapon Flag](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) enumerotor on hoggit wiki.
-- @type ENUMS.WeaponFlag
ENUMS.WeaponFlag={
  -- Bombs
  LGB                  =          2,
  TvGB                 =          4,
  SNSGB                =          8,
  HEBomb               =         16,
  Penetrator           =         32,
  NapalmBomb           =         64,
  FAEBomb              =        128,
  ClusterBomb          =        256,
  Dispencer            =        512,
  CandleBomb           =       1024,
  ParachuteBomb        = 2147483648,
  -- Rockets
  LightRocket          =       2048,
  MarkerRocket         =       4096,
  CandleRocket         =       8192,
  HeavyRocket          =      16384,
  -- Air-To-Surface Missiles
  AntiRadarMissile     =      32768,
  AntiShipMissile      =      65536,
  AntiTankMissile      =     131072,
  FireAndForgetASM     =     262144,
  LaserASM             =     524288,
  TeleASM              =    1048576,
  CruiseMissile        =    2097152,
  AntiRadarMissile2    = 1073741824,
  -- Air-To-Air Missiles
  SRAM                 =    4194304,
  MRAAM                =    8388608,
  LRAAM                =   16777216,
  IR_AAM               =   33554432,
  SAR_AAM              =   67108864,
  AR_AAM               =  134217728,
  --- Guns
  GunPod               =  268435456,
  BuiltInCannon        =  536870912,
  ---
  -- Combinations
  --
  -- Bombs
  GuidedBomb           =         14, -- (LGB + TvGB + SNSGB)
  AnyUnguidedBomb      = 2147485680, -- (HeBomb + Penetrator + NapalmBomb + FAEBomb + ClusterBomb + Dispencer + CandleBomb + ParachuteBomb)
  AnyBomb              = 2147485694, -- (GuidedBomb + AnyUnguidedBomb)
  --- Rockets
  AnyRocket            =      30720, -- LightRocket + MarkerRocket + CandleRocket + HeavyRocket
  --- Air-To-Surface Missiles
  GuidedASM            =    1572864, -- (LaserASM + TeleASM)
  TacticalASM          =    1835008, -- (GuidedASM + FireAndForgetASM)
  AnyASM               =    4161536, -- (AntiRadarMissile + AntiShipMissile + AntiTankMissile + FireAndForgetASM + GuidedASM + CruiseMissile)
  AnyASM2              = 1077903360, -- 4161536+1073741824,
  --- Air-To-Air Missiles
  AnyAAM               =  264241152, -- IR_AAM + SAR_AAM + AR_AAM + SRAAM + MRAAM + LRAAM
  AnyAutonomousMissile =   36012032, -- IR_AAM + AntiRadarMissile + AntiShipMissile + FireAndForgetASM + CruiseMissile
  AnyMissile           =  268402688, -- AnyASM + AnyAAM
  --- Guns
  Cannons              =  805306368, -- GUN_POD + BuiltInCannon
  --- Torpedo
  Torpedo              = 4294967296,
  ---
  -- Even More Genral
  Auto                 = 3221225470, -- Any Weapon (AnyBomb + AnyRocket + AnyMissile + Cannons)
  AutoDCS              = 1073741822, -- Something if often see
  AnyAG                = 2956984318, -- Any Air-To-Ground Weapon
  AnyAA                =  264241152, -- Any Air-To-Air Weapon
  AnyUnguided          = 2952822768, -- Any Unguided Weapon
  AnyGuided            =  268402702, -- Any Guided Weapon
}

--- Weapon types by category. See the [Weapon Flag](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) enumerator on hoggit wiki.
-- @type ENUMS.WeaponType
-- @field #table Bomb Bombs.
-- @field #table Rocket Rocket.
-- @field #table Gun Guns.
-- @field #table Missile Missiles.
-- @field #table AAM Air-to-Air missiles.
-- @field #table Torpedo Torpedos.
-- @field #table Any Combinations.
ENUMS.WeaponType={}
ENUMS.WeaponType.Bomb={
  -- Bombs
  LGB                  =          2,
  TvGB                 =          4,
  SNSGB                =          8,
  HEBomb               =         16,
  Penetrator           =         32,
  NapalmBomb           =         64,
  FAEBomb              =        128,
  ClusterBomb          =        256,
  Dispencer            =        512,
  CandleBomb           =       1024,
  ParachuteBomb        = 2147483648,
  -- Combinations
  GuidedBomb           =         14, -- (LGB + TvGB + SNSGB)
  AnyUnguidedBomb      = 2147485680, -- (HeBomb + Penetrator + NapalmBomb + FAEBomb + ClusterBomb + Dispencer + CandleBomb + ParachuteBomb)
  AnyBomb              = 2147485694, -- (GuidedBomb + AnyUnguidedBomb)
}
ENUMS.WeaponType.Rocket={
  -- Rockets
  LightRocket          =       2048,
  MarkerRocket         =       4096,
  CandleRocket         =       8192,
  HeavyRocket          =      16384,
  -- Combinations
  AnyRocket            =      30720, -- LightRocket + MarkerRocket + CandleRocket + HeavyRocket
}
ENUMS.WeaponType.Gun={
  -- Guns
  GunPod               =  268435456,
  BuiltInCannon        =  536870912,
  -- Combinations
  Cannons              =  805306368, -- GUN_POD + BuiltInCannon
}
ENUMS.WeaponType.Missile={
  -- Missiles
  AntiRadarMissile     =      32768,
  AntiShipMissile      =      65536,
  AntiTankMissile      =     131072,
  FireAndForgetASM     =     262144,
  LaserASM             =     524288,
  TeleASM              =    1048576,
  CruiseMissile        =    2097152,
  AntiRadarMissile2    = 1073741824,
  -- Combinations
  GuidedASM            =    1572864, -- (LaserASM + TeleASM)
  TacticalASM          =    1835008, -- (GuidedASM + FireAndForgetASM)
  AnyASM               =    4161536, -- (AntiRadarMissile + AntiShipMissile + AntiTankMissile + FireAndForgetASM + GuidedASM + CruiseMissile)
  AnyASM2              = 1077903360, -- 4161536+1073741824,
  AnyAutonomousMissile =   36012032, -- IR_AAM + AntiRadarMissile + AntiShipMissile + FireAndForgetASM + CruiseMissile
  AnyMissile           =  268402688, -- AnyASM + AnyAAM
}
ENUMS.WeaponType.AAM={
  -- Air-To-Air Missiles
  SRAM                 =    4194304,
  MRAAM                =    8388608,
  LRAAM                =   16777216,
  IR_AAM               =   33554432,
  SAR_AAM              =   67108864,
  AR_AAM               =  134217728,
  -- Combinations
  AnyAAM               =  264241152, -- IR_AAM + SAR_AAM + AR_AAM + SRAAM + MRAAM + LRAAM
}
ENUMS.WeaponType.Torpedo={
  -- Torpedo
  Torpedo              = 4294967296,
}
ENUMS.WeaponType.Any={
  -- General combinations
  Weapon               = 3221225470, -- Any Weapon (AnyBomb + AnyRocket + AnyMissile + Cannons)
  AG                   = 2956984318, -- Any Air-To-Ground Weapon
  AA                   =  264241152, -- Any Air-To-Air Weapon
  Unguided             = 2952822768, -- Any Unguided Weapon
  Guided               =  268402702, -- Any Guided Weapon
}


--- Mission tasks.
-- @type ENUMS.MissionTask
-- @field #string NOTHING No special task. Group can perform the minimal tasks: Orbit, Refuelling, Follow and Aerobatics.
-- @field #string AFAC Forward Air Controller Air. Can perform the tasks: Attack Group, Attack Unit, FAC assign group, Bombing, Attack Map Object.
-- @field #string ANTISHIPSTRIKE Naval ops. Can perform the tasks: Attack Group, Attack Unit.
-- @field #string AWACS AWACS.
-- @field #string CAP Combat Air Patrol.
-- @field #string CAS Close Air Support.
-- @field #string ESCORT Escort another group.
-- @field #string FIGHTERSWEEP Fighter sweep.
-- @field #string GROUNDATTACK Ground attack.
-- @field #string GROUNDESCORT Ground escort another group.
-- @field #string INTERCEPT Intercept.
-- @field #string PINPOINTSTRIKE Pinpoint strike.
-- @field #string RECONNAISSANCE Reconnaissance mission.
-- @field #string REFUELING Refueling mission.
-- @field #string RUNWAYATTACK Attack the runway of an airdrome.
-- @field #string SEAD Suppression of Enemy Air Defenses.
-- @field #string TRANSPORT Troop transport.
ENUMS.MissionTask={
  NOTHING="Nothing",
  AFAC="AFAC",
  ANTISHIPSTRIKE="Antiship Strike",
  AWACS="AWACS",
  CAP="CAP",
  CAS="CAS",
  ESCORT="Escort",
  GROUNDESCORT="Ground escort",
  FIGHTERSWEEP="Fighter Sweep",
  GROUNDATTACK="Ground Attack",
  INTERCEPT="Intercept",
  PINPOINTSTRIKE="Pinpoint Strike",
  RECONNAISSANCE="Reconnaissance",
  REFUELING="Refueling",
  RUNWAYATTACK="Runway Attack",
  SEAD="SEAD",
  TRANSPORT="Transport",
}

--- Formations (new). See the [Formations](https://wiki.hoggitworld.com/view/DCS_enum_formation) on hoggit wiki.
-- @type ENUMS.Formation
ENUMS.Formation={}
ENUMS.Formation.FixedWing={}
ENUMS.Formation.FixedWing.LineAbreast={}
ENUMS.Formation.FixedWing.LineAbreast.Close = 65537
ENUMS.Formation.FixedWing.LineAbreast.Open  = 65538
ENUMS.Formation.FixedWing.LineAbreast.Group = 65539
ENUMS.Formation.FixedWing.Trail={}
ENUMS.Formation.FixedWing.Trail.Close = 131073
ENUMS.Formation.FixedWing.Trail.Open  = 131074
ENUMS.Formation.FixedWing.Trail.Group = 131075
ENUMS.Formation.FixedWing.Wedge={}
ENUMS.Formation.FixedWing.Wedge.Close = 196609
ENUMS.Formation.FixedWing.Wedge.Open  = 196610
ENUMS.Formation.FixedWing.Wedge.Group = 196611
ENUMS.Formation.FixedWing.EchelonRight={}
ENUMS.Formation.FixedWing.EchelonRight.Close = 262145
ENUMS.Formation.FixedWing.EchelonRight.Open  = 262146
ENUMS.Formation.FixedWing.EchelonRight.Group = 262147
ENUMS.Formation.FixedWing.EchelonLeft={}
ENUMS.Formation.FixedWing.EchelonLeft.Close = 327681
ENUMS.Formation.FixedWing.EchelonLeft.Open  = 327682
ENUMS.Formation.FixedWing.EchelonLeft.Group = 327683
ENUMS.Formation.FixedWing.FingerFour={}
ENUMS.Formation.FixedWing.FingerFour.Close = 393217
ENUMS.Formation.FixedWing.FingerFour.Open  = 393218
ENUMS.Formation.FixedWing.FingerFour.Group = 393219
ENUMS.Formation.FixedWing.Spread={}
ENUMS.Formation.FixedWing.Spread.Close = 458753
ENUMS.Formation.FixedWing.Spread.Open  = 458754
ENUMS.Formation.FixedWing.Spread.Group = 458755
ENUMS.Formation.FixedWing.BomberElement={}
ENUMS.Formation.FixedWing.BomberElement.Close = 786433
ENUMS.Formation.FixedWing.BomberElement.Open  = 786434
ENUMS.Formation.FixedWing.BomberElement.Group = 786435
ENUMS.Formation.FixedWing.BomberElementHeight={}
ENUMS.Formation.FixedWing.BomberElementHeight.Close = 851968
ENUMS.Formation.FixedWing.FighterVic={}
ENUMS.Formation.FixedWing.FighterVic.Close = 917505
ENUMS.Formation.FixedWing.FighterVic.Open  = 917506
ENUMS.Formation.RotaryWing={}
ENUMS.Formation.RotaryWing.Column={}
ENUMS.Formation.RotaryWing.Column.D70=720896
ENUMS.Formation.RotaryWing.Wedge={}
ENUMS.Formation.RotaryWing.Wedge.D70=8
ENUMS.Formation.RotaryWing.FrontRight={}
ENUMS.Formation.RotaryWing.FrontRight.D300=655361
ENUMS.Formation.RotaryWing.FrontRight.D600=655362
ENUMS.Formation.RotaryWing.FrontLeft={}
ENUMS.Formation.RotaryWing.FrontLeft.D300=655617
ENUMS.Formation.RotaryWing.FrontLeft.D600=655618
ENUMS.Formation.RotaryWing.EchelonRight={}
ENUMS.Formation.RotaryWing.EchelonRight.D70 =589825
ENUMS.Formation.RotaryWing.EchelonRight.D300=589826
ENUMS.Formation.RotaryWing.EchelonRight.D600=589827
ENUMS.Formation.RotaryWing.EchelonLeft={}
ENUMS.Formation.RotaryWing.EchelonLeft.D70 =590081
ENUMS.Formation.RotaryWing.EchelonLeft.D300=590082
ENUMS.Formation.RotaryWing.EchelonLeft.D600=590083
ENUMS.Formation.Vehicle={}
ENUMS.Formation.Vehicle.Vee="Vee"
ENUMS.Formation.Vehicle.EchelonRight="EchelonR"
ENUMS.Formation.Vehicle.OffRoad="Off Road"
ENUMS.Formation.Vehicle.Rank="Rank"
ENUMS.Formation.Vehicle.EchelonLeft="EchelonL"
ENUMS.Formation.Vehicle.OnRoad="On Road"
ENUMS.Formation.Vehicle.Cone="Cone"
ENUMS.Formation.Vehicle.Diamond="Diamond"

--- Formations (old). The old format is a simplified version of the new formation enums, which allow more sophisticated settings.
-- See the [Formations](https://wiki.hoggitworld.com/view/DCS_enum_formation) on hoggit wiki.
-- @type ENUMS.FormationOld
ENUMS.FormationOld={}
ENUMS.FormationOld.FixedWing={}
ENUMS.FormationOld.FixedWing.LineAbreast=1
ENUMS.FormationOld.FixedWing.Trail=2
ENUMS.FormationOld.FixedWing.Wedge=3
ENUMS.FormationOld.FixedWing.EchelonRight=4
ENUMS.FormationOld.FixedWing.EchelonLeft=5
ENUMS.FormationOld.FixedWing.FingerFour=6
ENUMS.FormationOld.FixedWing.SpreadFour=7
ENUMS.FormationOld.FixedWing.BomberElement=12
ENUMS.FormationOld.FixedWing.BomberElementHeight=13
ENUMS.FormationOld.FixedWing.FighterVic=14
ENUMS.FormationOld.RotaryWing={}
ENUMS.FormationOld.RotaryWing.Wedge=8
ENUMS.FormationOld.RotaryWing.Echelon=9
ENUMS.FormationOld.RotaryWing.Front=10
ENUMS.FormationOld.RotaryWing.Column=11


--- Morse Code. See the [Wikipedia](https://en.wikipedia.org/wiki/Morse_code).
-- 
-- * Short pulse "*"
-- * Long pulse "-"
-- 
-- Pulses are separated by a blank character " ".
-- 
-- @type ENUMS.Morse
ENUMS.Morse={}
ENUMS.Morse.A="* -"
ENUMS.Morse.B="- * * *"
ENUMS.Morse.C="- * - *"
ENUMS.Morse.D="- * *"
ENUMS.Morse.E="*"
ENUMS.Morse.F="* * - *"
ENUMS.Morse.G="- - *"
ENUMS.Morse.H="* * * *"
ENUMS.Morse.I="* *"
ENUMS.Morse.J="* - - -"
ENUMS.Morse.K="- * -"
ENUMS.Morse.L="* - * *"
ENUMS.Morse.M="- -"
ENUMS.Morse.N="- *"
ENUMS.Morse.O="- - -"
ENUMS.Morse.P="* - - *"
ENUMS.Morse.Q="- - * -"
ENUMS.Morse.R="* - *"
ENUMS.Morse.S="* * *"
ENUMS.Morse.T="-"
ENUMS.Morse.U="* * -"
ENUMS.Morse.V="* * * -"
ENUMS.Morse.W="* - -"
ENUMS.Morse.X="- * * -"
ENUMS.Morse.Y="- * - -"
ENUMS.Morse.Z="- - * *"
ENUMS.Morse.N1="* - - - -"
ENUMS.Morse.N2="* * - - -"
ENUMS.Morse.N3="* * * - -"
ENUMS.Morse.N4="* * * * -"
ENUMS.Morse.N5="* * * * *"
ENUMS.Morse.N6="- * * * *"
ENUMS.Morse.N7="- - * * *"
ENUMS.Morse.N8="- - - * *"
ENUMS.Morse.N9="- - - - *"
ENUMS.Morse.N0="- - - - -"
ENUMS.Morse[" "]=" "

--- ISO (639-1) 2-letter Language Codes. See the [Wikipedia](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
-- 
-- @type ENUMS.ISOLang
ENUMS.ISOLang =
{
  Arabic    = 'AR',
  Chinese   = 'ZH',
  English   = 'EN',
  French    = 'FR',
  German    = 'DE',
  Russian   = 'RU',
  Spanish   = 'ES',
  Japanese  = 'JA',
  Italian   = 'IT',
}

--- Phonetic Alphabet (NATO). See the [Wikipedia](https://en.wikipedia.org/wiki/NATO_phonetic_alphabet).
-- 
-- @type ENUMS.Phonetic
ENUMS.Phonetic =
{
  A = 'Alpha',
  B = 'Bravo',
  C = 'Charlie',
  D = 'Delta',
  E = 'Echo',
  F = 'Foxtrot',
  G = 'Golf',
  H = 'Hotel',
  I = 'India',
  J = 'Juliett',
  K = 'Kilo',
  L = 'Lima',
  M = 'Mike',
  N = 'November',
  O = 'Oscar',
  P = 'Papa',
  Q = 'Quebec',
  R = 'Romeo',
  S = 'Sierra',
  T = 'Tango',
  U = 'Uniform',
  V = 'Victor',
  W = 'Whiskey',
  X = 'Xray',
  Y = 'Yankee',
  Z = 'Zulu',
}

--- Reporting Names (NATO). See the [Wikipedia](https://en.wikipedia.org/wiki/List_of_NATO_reporting_names_for_fighter_aircraft).
-- DCS known aircraft types
-- 
-- @type ENUMS.ReportingName
ENUMS.ReportingName =
{
  NATO = {
    -- Fighters
    Dragon = "JF-17", -- China, correctly Fierce Dragon, Thunder for PAC
    Fagot = "MiG-15",
    Farmer = "MiG-19", -- Shenyang J-6 and Mikoyan-Gurevich MiG-19
    Felon = "Su-57",
    Fencer = "Su-24",
    Fishbed = "MiG-21",
    Fitter = "Su-17", -- Sukhoi Su-7 and Su-17/Su-20/Su-22
    Flogger = "MiG-23",  --and MiG-27
    Flogger_D = "MiG-27",  --and MiG-23
    Flagon = "Su-15",
    Foxbat = "MiG-25",
    Fulcrum = "MiG-29",
    Foxhound = "MiG-31",
    Flanker = "Su-27", -- Sukhoi Su-27/Su-30/Su-33/Su-35/Su-37 and Shenyang J-11/J-15/J-16
    Flanker_C = "Su-30",
    Flanker_E = "Su-35",
    Flanker_F = "Su-37",
    Flanker_L = "J-11A",
    Firebird = "J-10",
    Sea_Flanker = "Su-33",
    Fullback = "Su-34", -- also Su-32
    Frogfoot = "Su-25",
    Tomcat = "F-14", -- Iran
    Mirage = "Mirage", -- various non-NATO
    Codling = "Yak-40",
    Maya = "L-39",
    -- Fighters US/NATO
    Warthog = "A-10",
    --Mosquito = "A-20",
    Skyhawk = "A-4E",
    Viggen = "AJS37",
    Harrier_B = "AV8BNA",
    Harrier = "AV-8B",
    Spirit = "B-2",
    Aviojet = "C-101",
    Nighthawk = "F-117A",
    Eagle = "F-15C",
    Mudhen = "F-15E",
    Viper = "F-16",
    Phantom = "F-4E",
    Tiger = "F-5", -- was thinkg to name this MiG-25 ;)
    Sabre = "F-86",
    Hornet = "A-18", -- avoiding the slash
    Hawk = "Hawk",
    Albatros = "L-39",
    Goshawk = "T-45",
    Starfighter = "F-104",
    Tornado = "Tornado",
    -- Transport / Bomber / Others
    Atlas = "A400",
    Lancer = "B1-B",
    Stratofortress = "B-52H",
    Hercules = "C-130",
    Super_Hercules = "Hercules",
    Globemaster = "C-17",
    Greyhound = "C-2A",
    Galaxy = "C-5",
    Hawkeye = "E-2D",
    Sentry = "E-3A",
    Stratotanker = "KC-135",
    Gasstation = "KC-135MPRS",
    Extender = "KC-10",
    Orion = "P-3C",
    Viking = "S-3B",
    Osprey = "V-22",
    -- Bomber Rus
    Badger = "H6-J",
    Bear_J = "Tu-142", -- also Tu-95
    Bear = "Tu-95", -- also Tu-142
    Blinder = "Tu-22",
    Blackjack = "Tu-160",
    -- AIC / Transport / Other
    Clank = "An-30",
    Curl = "An-26",
    Candid = "IL-76",
    Midas = "IL-78",
    Mainstay = "A-50",
    Mainring = "KJ-2000", -- A-50 China
    Yak = "Yak-52",
    -- Helos
    Helix = "Ka-27",
    Shark = "Ka-50",
    Hind = "Mi-24",
    Halo = "Mi-26",
    Hip = "Mi-8",
    Havoc = "Mi-28",
    Gazelle = "SA342",
    -- Helos US
    Huey = "UH-1H",
    Cobra = "AH-1",
    Apache = "AH-64",
    Chinook = "CH-47",
    Sea_Stallion = "CH-53",
    Kiowa = "OH-58",
    Seahawk = "SH-60",
    Blackhawk = "UH-60",
    Sea_King = "S-61",
    -- Drones
    UCAV = "WingLoong",
    Reaper = "MQ-9",
    Predator = "MQ-1A",
  }
}

--- Enums for Link16 transmit power
-- @type ENUMS.Link16Power
ENUMS.Link16Power = {
  none = 0,
  low = 1,
  medium = 2,
  high = 3,
}


--- Enums for the STORAGE class for stores - which need to be in ""
-- @type ENUMS.Storage
-- @type ENUMS.Storage.weapons
-- @type ENUMS.Storage.weapons.missiles
-- @type ENUMS.Storage.weapons.bombs
-- @type ENUMS.Storage.weapons.nurs
-- @type ENUMS.Storage.weapons.containers
-- @type ENUMS.Storage.weapons.droptanks
-- @type ENUMS.Storage.weapons.adapters
-- @type ENUMS.Storage.weapons.torpedoes
-- @type ENUMS.Storage.weapons.Gazelle
-- @type ENUMS.Storage.weapons.CH47
-- @type ENUMS.Storage.weapons.OH58
-- @type ENUMS.Storage.weapons.UH1H
-- @type ENUMS.Storage.weapons.AH64D
ENUMS.Storage = {
  weapons = {
    missiles = {}, -- Missiles
    bombs = {}, -- Bombs
    nurs = {}, --  Rockets and unguided
    containers = {}, -- Containers
    droptanks = {}, -- Droptanks
    adapters = {}, -- Adapter
    torpedoes = {}, -- Torpedoes
    Gazelle = {}, -- Gazelle specifics
    CH47 = {}, -- Chinook specifics
    OH58 = {}, -- Kiowa specifics
    UH1H = {}, -- Huey specifics
    AH64D = {}, -- Huey specifics
    UH60L = {}, -- Blackhawk specifics
  }
}

ENUMS.Storage.weapons.nurs.SNEB_TYPE253_F1B = "weapons.nurs.SNEB_TYPE253_F1B"
ENUMS.Storage.weapons.missiles.P_24T = "weapons.missiles.P_24T"
ENUMS.Storage.weapons.bombs.BLU_3B_OLD = "weapons.bombs.BLU-3B_OLD"
ENUMS.Storage.weapons.missiles.AGM_154 = "weapons.missiles.AGM_154"
ENUMS.Storage.weapons.nurs.HYDRA_70_M151_M433 = "weapons.nurs.HYDRA_70_M151_M433"
ENUMS.Storage.weapons.bombs.SAM_Avenger_M1097_Skid_7090lb = "weapons.bombs.SAM Avenger M1097 Skid [7090lb]"
ENUMS.Storage.weapons.bombs.British_GP_250LB_Bomb_Mk5 = "weapons.bombs.British_GP_250LB_Bomb_Mk5"
ENUMS.Storage.weapons.containers.OV10_SMOKE = "weapons.containers.{OV10_SMOKE}"
ENUMS.Storage.weapons.bombs.BLU_4B_OLD = "weapons.bombs.BLU-4B_OLD"
ENUMS.Storage.weapons.bombs.FAB_500M54 = "weapons.bombs.FAB-500M54"
ENUMS.Storage.weapons.bombs.GBU_38 = "weapons.bombs.GBU_38"
ENUMS.Storage.weapons.containers.F_15E_AXQ_14_DATALINK = "weapons.containers.F-15E_AXQ-14_DATALINK"
ENUMS.Storage.weapons.bombs.BEER_BOMB = "weapons.bombs.BEER_BOMB"
ENUMS.Storage.weapons.bombs.P_50T = "weapons.bombs.P-50T"
ENUMS.Storage.weapons.nurs.C_8CM_GN = "weapons.nurs.C_8CM_GN"
ENUMS.Storage.weapons.bombs.FAB_500SL = "weapons.bombs.FAB-500SL"
ENUMS.Storage.weapons.bombs.KAB_1500Kr = "weapons.bombs.KAB_1500Kr"
ENUMS.Storage.weapons.bombs.two50_2 = "weapons.bombs.250-2"
ENUMS.Storage.weapons.droptanks.Spitfire_tank_1 = "weapons.droptanks.Spitfire_tank_1"
ENUMS.Storage.weapons.missiles.AGM_65G = "weapons.missiles.AGM_65G"
ENUMS.Storage.weapons.missiles.AGM_65A = "weapons.missiles.AGM_65A"
ENUMS.Storage.weapons.containers.Hercules_JATO = "weapons.containers.Hercules_JATO"
ENUMS.Storage.weapons.nurs.HYDRA_70_M259 = "weapons.nurs.HYDRA_70_M259"
ENUMS.Storage.weapons.missiles.AGM_84E = "weapons.missiles.AGM_84E"
ENUMS.Storage.weapons.bombs.AN_M30A1 = "weapons.bombs.AN_M30A1"
ENUMS.Storage.weapons.nurs.C_25 = "weapons.nurs.C_25"
ENUMS.Storage.weapons.containers.AV8BNA_ALQ164 = "weapons.containers.AV8BNA_ALQ164"
ENUMS.Storage.weapons.containers.lav_25 = "weapons.containers.lav-25"
ENUMS.Storage.weapons.missiles.P_60 = "weapons.missiles.P_60"
ENUMS.Storage.weapons.bombs.FAB_1500 = "weapons.bombs.FAB_1500"
ENUMS.Storage.weapons.droptanks.FuelTank_350L = "weapons.droptanks.FuelTank_350L"
ENUMS.Storage.weapons.bombs.AAA_Vulcan_M163_Skid_21577lb = "weapons.bombs.AAA Vulcan M163 Skid [21577lb]"
ENUMS.Storage.weapons.missiles.Kormoran = "weapons.missiles.Kormoran"
ENUMS.Storage.weapons.droptanks.HB_F14_EXT_DROPTANK_EMPTY = "weapons.droptanks.HB_F14_EXT_DROPTANK_EMPTY"
ENUMS.Storage.weapons.droptanks.FuelTank_150L = "weapons.droptanks.FuelTank_150L"
ENUMS.Storage.weapons.missiles.Rb_15F_for_A_I = "weapons.missiles.Rb 15F (for A.I.)"
ENUMS.Storage.weapons.missiles.RB75T = "weapons.missiles.RB75T"
ENUMS.Storage.weapons.missiles.Vikhr_M = "weapons.missiles.Vikhr_M"
ENUMS.Storage.weapons.nurs.FFAR_M156_WP = "weapons.nurs.FFAR M156 WP"
ENUMS.Storage.weapons.nurs.British_HE_60LBSAPNo2_3INCHNo1 = "weapons.nurs.British_HE_60LBSAPNo2_3INCHNo1"
ENUMS.Storage.weapons.missiles.DWS39_MJ2 = "weapons.missiles.DWS39_MJ2"
ENUMS.Storage.weapons.bombs.HEBOMBD = "weapons.bombs.HEBOMBD"
ENUMS.Storage.weapons.missiles.CATM_9M = "weapons.missiles.CATM_9M"
ENUMS.Storage.weapons.bombs.Mk_81 = "weapons.bombs.Mk_81"
ENUMS.Storage.weapons.droptanks.Drop_Tank_300_Liter = "weapons.droptanks.Drop_Tank_300_Liter"
ENUMS.Storage.weapons.containers.HMMWV_M1025 = "weapons.containers.HMMWV_M1025"
ENUMS.Storage.weapons.bombs.SAM_CHAPARRAL_Air_21624lb = "weapons.bombs.SAM CHAPARRAL Air [21624lb]"
ENUMS.Storage.weapons.missiles.AGM_154A = "weapons.missiles.AGM_154A"
ENUMS.Storage.weapons.bombs.Mk_84AIR_TP = "weapons.bombs.Mk_84AIR_TP"
ENUMS.Storage.weapons.bombs.GBU_31_V_3B = "weapons.bombs.GBU_31_V_3B"
ENUMS.Storage.weapons.nurs.C_8CM_WH = "weapons.nurs.C_8CM_WH"
ENUMS.Storage.weapons.missiles.Matra_Super_530D = "weapons.missiles.Matra Super 530D"
ENUMS.Storage.weapons.nurs.ARF8M3TPSM = "weapons.nurs.ARF8M3TPSM"
ENUMS.Storage.weapons.missiles.TGM_65H = "weapons.missiles.TGM_65H"
ENUMS.Storage.weapons.nurs.M8rocket = "weapons.nurs.M8rocket"
ENUMS.Storage.weapons.bombs.GBU_27 = "weapons.bombs.GBU_27"
ENUMS.Storage.weapons.missiles.AGR_20A = "weapons.missiles.AGR_20A"
ENUMS.Storage.weapons.missiles.LS_6_250 = "weapons.missiles.LS-6-250"
ENUMS.Storage.weapons.droptanks.M2KC_RPL_522_EMPTY = "weapons.droptanks.M2KC_RPL_522_EMPTY"
ENUMS.Storage.weapons.droptanks.M2KC_02_RPL541 = "weapons.droptanks.M2KC_02_RPL541"
ENUMS.Storage.weapons.missiles.AGM_45 = "weapons.missiles.AGM_45"
ENUMS.Storage.weapons.missiles.AGM_84A = "weapons.missiles.AGM_84A"
ENUMS.Storage.weapons.bombs.APC_BTR_80_Air_23936lb = "weapons.bombs.APC BTR-80 Air [23936lb]"
ENUMS.Storage.weapons.missiles.P_33E = "weapons.missiles.P_33E"
ENUMS.Storage.weapons.missiles.Ataka_9M120 = "weapons.missiles.Ataka_9M120"
ENUMS.Storage.weapons.bombs.MK76 = "weapons.bombs.MK76"
ENUMS.Storage.weapons.bombs.AB_250_2_SD_2 = "weapons.bombs.AB_250_2_SD_2"
ENUMS.Storage.weapons.missiles.Rb_05A = "weapons.missiles.Rb 05A"
ENUMS.Storage.weapons.bombs.ART_GVOZDIKA_34720lb = "weapons.bombs.ART GVOZDIKA [34720lb]"
ENUMS.Storage.weapons.bombs.Generic_Crate_20000lb = "weapons.bombs.Generic Crate [20000lb]"
ENUMS.Storage.weapons.bombs.FAB_100SV = "weapons.bombs.FAB_100SV"
ENUMS.Storage.weapons.bombs.BetAB_500 = "weapons.bombs.BetAB_500"
ENUMS.Storage.weapons.droptanks.M2KC_02_RPL541_EMPTY = "weapons.droptanks.M2KC_02_RPL541_EMPTY"
ENUMS.Storage.weapons.droptanks.PTB600_MIG15 = "weapons.droptanks.PTB600_MIG15"
ENUMS.Storage.weapons.missiles.Rb_24J = "weapons.missiles.Rb 24J"
ENUMS.Storage.weapons.nurs.C_8CM_BU = "weapons.nurs.C_8CM_BU"
ENUMS.Storage.weapons.nurs.SNEB_TYPE259E_F1B = "weapons.nurs.SNEB_TYPE259E_F1B"
ENUMS.Storage.weapons.nurs.WGr21 = "weapons.nurs.WGr21"
ENUMS.Storage.weapons.bombs.SAMP250HD = "weapons.bombs.SAMP250HD"
ENUMS.Storage.weapons.containers.alq_184long = "weapons.containers.alq-184long"
ENUMS.Storage.weapons.nurs.SNEB_TYPE259E_H1 = "weapons.nurs.SNEB_TYPE259E_H1"
ENUMS.Storage.weapons.bombs.British_SAP_250LB_Bomb_Mk5 = "weapons.bombs.British_SAP_250LB_Bomb_Mk5"
ENUMS.Storage.weapons.bombs.Transport_UAZ_469_Air_3747lb = "weapons.bombs.Transport UAZ-469 Air [3747lb]"
ENUMS.Storage.weapons.bombs.Mk_83CT = "weapons.bombs.Mk_83CT"
ENUMS.Storage.weapons.missiles.AIM_7P = "weapons.missiles.AIM-7P"
ENUMS.Storage.weapons.missiles.AT_6 = "weapons.missiles.AT_6"
ENUMS.Storage.weapons.nurs.SNEB_TYPE254_H1_GREEN = "weapons.nurs.SNEB_TYPE254_H1_GREEN"
ENUMS.Storage.weapons.nurs.SNEB_TYPE250_F1B = "weapons.nurs.SNEB_TYPE250_F1B"
ENUMS.Storage.weapons.containers.U22A = "weapons.containers.U22A"
ENUMS.Storage.weapons.bombs.British_GP_250LB_Bomb_Mk1 = "weapons.bombs.British_GP_250LB_Bomb_Mk1"
ENUMS.Storage.weapons.bombs.CBU_105 = "weapons.bombs.CBU_105"
ENUMS.Storage.weapons.droptanks.FW_190_Fuel_Tank = "weapons.droptanks.FW-190_Fuel-Tank"
ENUMS.Storage.weapons.missiles.X_58 = "weapons.missiles.X_58"
ENUMS.Storage.weapons.missiles.BK90_MJ1_MJ2 = "weapons.missiles.BK90_MJ1_MJ2"
ENUMS.Storage.weapons.missiles.TGM_65D = "weapons.missiles.TGM_65D"
ENUMS.Storage.weapons.containers.BRD_4_250 = "weapons.containers.BRD-4-250"
ENUMS.Storage.weapons.missiles.P_73 = "weapons.missiles.P_73"
ENUMS.Storage.weapons.bombs.AN_M66 = "weapons.bombs.AN_M66"
ENUMS.Storage.weapons.bombs.APC_LAV_25_Air_22520lb = "weapons.bombs.APC LAV-25 Air [22520lb]"
ENUMS.Storage.weapons.missiles.AIM_7MH = "weapons.missiles.AIM-7MH"
ENUMS.Storage.weapons.containers.MB339_TravelPod = "weapons.containers.MB339_TravelPod"
ENUMS.Storage.weapons.bombs.GBU_12 = "weapons.bombs.GBU_12"
ENUMS.Storage.weapons.bombs.SC_250_T3_J = "weapons.bombs.SC_250_T3_J"
ENUMS.Storage.weapons.missiles.KD_20 = "weapons.missiles.KD-20"
ENUMS.Storage.weapons.missiles.AGM_86C = "weapons.missiles.AGM_86C"
ENUMS.Storage.weapons.missiles.X_35 = "weapons.missiles.X_35"
ENUMS.Storage.weapons.bombs.MK106 = "weapons.bombs.MK106"
ENUMS.Storage.weapons.bombs.BETAB_500S = "weapons.bombs.BETAB-500S"
ENUMS.Storage.weapons.nurs.C_5 = "weapons.nurs.C_5"
ENUMS.Storage.weapons.nurs.S_24B = "weapons.nurs.S-24B"
ENUMS.Storage.weapons.bombs.British_MC_500LB_Bomb_Mk2 = "weapons.bombs.British_MC_500LB_Bomb_Mk2"
ENUMS.Storage.weapons.containers.ANAWW_13 = "weapons.containers.ANAWW_13"
ENUMS.Storage.weapons.droptanks.droptank_108_gal = "weapons.droptanks.droptank_108_gal"
ENUMS.Storage.weapons.droptanks.DFT_300_GAL_A4E_LR = "weapons.droptanks.DFT_300_GAL_A4E_LR"
ENUMS.Storage.weapons.bombs.CBU_87 = "weapons.bombs.CBU_87"
ENUMS.Storage.weapons.missiles.GAR_8 = "weapons.missiles.GAR-8"
ENUMS.Storage.weapons.bombs.BELOUGA = "weapons.bombs.BELOUGA"
ENUMS.Storage.weapons.containers.EclairM_33 = "weapons.containers.{EclairM_33}"
ENUMS.Storage.weapons.bombs.ART_2S9_NONA_Air_19140lb = "weapons.bombs.ART 2S9 NONA Air [19140lb]"
ENUMS.Storage.weapons.bombs.BR_250 = "weapons.bombs.BR_250"
ENUMS.Storage.weapons.bombs.IAB_500 = "weapons.bombs.IAB-500"
ENUMS.Storage.weapons.containers.AN_ASQ_228 = "weapons.containers.AN_ASQ_228"
ENUMS.Storage.weapons.missiles.P_27P = "weapons.missiles.P_27P"
ENUMS.Storage.weapons.bombs.SD_250_Stg = "weapons.bombs.SD_250_Stg"
ENUMS.Storage.weapons.missiles.R_530F_IR = "weapons.missiles.R_530F_IR"
ENUMS.Storage.weapons.bombs.British_SAP_500LB_Bomb_Mk5 = "weapons.bombs.British_SAP_500LB_Bomb_Mk5"
ENUMS.Storage.weapons.bombs.FAB_250M54 = "weapons.bombs.FAB-250M54"
ENUMS.Storage.weapons.containers.M2KC_AAF = "weapons.containers.{M2KC_AAF}"
ENUMS.Storage.weapons.missiles.CM_802AKG_AI = "weapons.missiles.CM-802AKG_AI"
ENUMS.Storage.weapons.bombs.CBU_103 = "weapons.bombs.CBU_103"
ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_RED = "weapons.containers.{US_M10_SMOKE_TANK_RED}"
ENUMS.Storage.weapons.missiles.X_29T = "weapons.missiles.X_29T"
ENUMS.Storage.weapons.bombs.HEMTT_TFFT_34400lb = "weapons.bombs.HEMTT TFFT [34400lb]"
ENUMS.Storage.weapons.missiles.C_701IR = "weapons.missiles.C-701IR"
ENUMS.Storage.weapons.containers.fullCargoSeats = "weapons.containers.fullCargoSeats"
ENUMS.Storage.weapons.bombs.GBU_15_V_31_B = "weapons.bombs.GBU_15_V_31_B"
ENUMS.Storage.weapons.bombs.APC_M1043_HMMWV_Armament_Air_7023lb = "weapons.bombs.APC M1043 HMMWV Armament Air [7023lb]"
ENUMS.Storage.weapons.missiles.PL_5EII = "weapons.missiles.PL-5EII"
ENUMS.Storage.weapons.bombs.SC_250_T1_L2 = "weapons.bombs.SC_250_T1_L2"
ENUMS.Storage.weapons.torpedoes.mk46torp_name = "weapons.torpedoes.mk46torp_name"
ENUMS.Storage.weapons.containers.F_15E_AAQ_33_XR_ATP_SE = "weapons.containers.F-15E_AAQ-33_XR_ATP-SE"
ENUMS.Storage.weapons.missiles.AIM_7 = "weapons.missiles.AIM_7"
ENUMS.Storage.weapons.missiles.AGM_122 = "weapons.missiles.AGM_122"
ENUMS.Storage.weapons.bombs.HEBOMB = "weapons.bombs.HEBOMB"
ENUMS.Storage.weapons.bombs.CBU_97 = "weapons.bombs.CBU_97"
ENUMS.Storage.weapons.bombs.MK_81SE = "weapons.bombs.MK-81SE"
ENUMS.Storage.weapons.nurs.Zuni_127 = "weapons.nurs.Zuni_127"
ENUMS.Storage.weapons.containers.M2KC_AGF = "weapons.containers.{M2KC_AGF}"
ENUMS.Storage.weapons.droptanks.Hercules_ExtFuelTank = "weapons.droptanks.Hercules_ExtFuelTank"
ENUMS.Storage.weapons.containers.SMOKE_WHITE = "weapons.containers.{SMOKE_WHITE}"
ENUMS.Storage.weapons.droptanks.droptank_150_gal = "weapons.droptanks.droptank_150_gal"
ENUMS.Storage.weapons.nurs.HYDRA_70_WTU1B = "weapons.nurs.HYDRA_70_WTU1B"
ENUMS.Storage.weapons.missiles.GB_6_SFW = "weapons.missiles.GB-6-SFW"
ENUMS.Storage.weapons.missiles.KD_63 = "weapons.missiles.KD-63"
ENUMS.Storage.weapons.bombs.GBU_28 = "weapons.bombs.GBU_28"
ENUMS.Storage.weapons.nurs.C_8CM_YE = "weapons.nurs.C_8CM_YE"
ENUMS.Storage.weapons.droptanks.HB_F14_EXT_DROPTANK = "weapons.droptanks.HB_F14_EXT_DROPTANK"
ENUMS.Storage.weapons.missiles.Super_530F = "weapons.missiles.Super_530F"
ENUMS.Storage.weapons.missiles.Ataka_9M220 = "weapons.missiles.Ataka_9M220"
ENUMS.Storage.weapons.bombs.BDU_33 = "weapons.bombs.BDU_33"
ENUMS.Storage.weapons.bombs.British_GP_250LB_Bomb_Mk4 = "weapons.bombs.British_GP_250LB_Bomb_Mk4"
ENUMS.Storage.weapons.missiles.TOW = "weapons.missiles.TOW"
ENUMS.Storage.weapons.bombs.ATGM_M1045_HMMWV_TOW_Air_7183lb = "weapons.bombs.ATGM M1045 HMMWV TOW Air [7183lb]"
ENUMS.Storage.weapons.missiles.X_25MR = "weapons.missiles.X_25MR"
ENUMS.Storage.weapons.droptanks.fueltank230 = "weapons.droptanks.fueltank230"
ENUMS.Storage.weapons.droptanks.PTB_490C_MIG21 = "weapons.droptanks.PTB-490C-MIG21"
ENUMS.Storage.weapons.bombs.M1025_HMMWV_Air_6160lb = "weapons.bombs.M1025 HMMWV Air [6160lb]"
ENUMS.Storage.weapons.nurs.SNEB_TYPE254_F1B_GREEN = "weapons.nurs.SNEB_TYPE254_F1B_GREEN"
ENUMS.Storage.weapons.missiles.R_550 = "weapons.missiles.R_550"
ENUMS.Storage.weapons.bombs.KAB_1500LG = "weapons.bombs.KAB_1500LG"
ENUMS.Storage.weapons.missiles.AGM_84D = "weapons.missiles.AGM_84D"
ENUMS.Storage.weapons.missiles.YJ_83K = "weapons.missiles.YJ-83K"
ENUMS.Storage.weapons.missiles.AIM_54C_Mk47 = "weapons.missiles.AIM_54C_Mk47"
ENUMS.Storage.weapons.missiles.BRM_1_90MM = "weapons.missiles.BRM-1_90MM"
ENUMS.Storage.weapons.missiles.Ataka_9M120F = "weapons.missiles.Ataka_9M120F"
ENUMS.Storage.weapons.droptanks.Eleven00L_Tank = "weapons.droptanks.1100L Tank"
ENUMS.Storage.weapons.bombs.BAP_100 = "weapons.bombs.BAP_100"
ENUMS.Storage.weapons.adapters.lau_88 = "weapons.adapters.lau-88"
ENUMS.Storage.weapons.missiles.P_40T = "weapons.missiles.P_40T"
ENUMS.Storage.weapons.missiles.GB_6 = "weapons.missiles.GB-6"
ENUMS.Storage.weapons.bombs.FAB_250M54TU = "weapons.bombs.FAB-250M54TU"
ENUMS.Storage.weapons.missiles.DWS39_MJ1 = "weapons.missiles.DWS39_MJ1"
ENUMS.Storage.weapons.missiles.CM_802AKG = "weapons.missiles.CM-802AKG"
ENUMS.Storage.weapons.bombs.FAB_250 = "weapons.bombs.FAB_250"
ENUMS.Storage.weapons.missiles.C_802AK = "weapons.missiles.C_802AK"
ENUMS.Storage.weapons.bombs.SD_500_A = "weapons.bombs.SD_500_A"
ENUMS.Storage.weapons.bombs.GBU_32_V_2B = "weapons.bombs.GBU_32_V_2B"
ENUMS.Storage.weapons.containers.marder = "weapons.containers.marder"
ENUMS.Storage.weapons.missiles.ADM_141B = "weapons.missiles.ADM_141B"
ENUMS.Storage.weapons.bombs.ROCKEYE = "weapons.bombs.ROCKEYE"
ENUMS.Storage.weapons.missiles.BK90_MJ1 = "weapons.missiles.BK90_MJ1"
ENUMS.Storage.weapons.containers.BTR_80 = "weapons.containers.BTR-80"
ENUMS.Storage.weapons.bombs.SAM_ROLAND_ADS_34720lb = "weapons.bombs.SAM ROLAND ADS [34720lb]"
ENUMS.Storage.weapons.containers.wmd7 = "weapons.containers.wmd7"
ENUMS.Storage.weapons.missiles.C_701T = "weapons.missiles.C-701T"
ENUMS.Storage.weapons.missiles.AIM_7E_2 = "weapons.missiles.AIM-7E-2"
ENUMS.Storage.weapons.nurs.HVAR = "weapons.nurs.HVAR"
ENUMS.Storage.weapons.containers.HMMWV_M1043 = "weapons.containers.HMMWV_M1043"
ENUMS.Storage.weapons.droptanks.PTB_800_MIG21 = "weapons.droptanks.PTB-800-MIG21"
ENUMS.Storage.weapons.missiles.AGM_114 = "weapons.missiles.AGM_114"
ENUMS.Storage.weapons.bombs.APC_M1126_Stryker_ICV_29542lb = "weapons.bombs.APC M1126 Stryker ICV [29542lb]"
ENUMS.Storage.weapons.bombs.APC_M113_Air_21624lb = "weapons.bombs.APC M113 Air [21624lb]"
ENUMS.Storage.weapons.bombs.M_117 = "weapons.bombs.M_117"
ENUMS.Storage.weapons.missiles.AGM_65D = "weapons.missiles.AGM_65D"
ENUMS.Storage.weapons.droptanks.MB339_TT320_L = "weapons.droptanks.MB339_TT320_L"
ENUMS.Storage.weapons.missiles.AGM_86 = "weapons.missiles.AGM_86"
ENUMS.Storage.weapons.bombs.BDU_45LGB = "weapons.bombs.BDU_45LGB"
ENUMS.Storage.weapons.missiles.AGM_65H = "weapons.missiles.AGM_65H"
ENUMS.Storage.weapons.nurs.RS_82 = "weapons.nurs.RS-82"
ENUMS.Storage.weapons.nurs.SNEB_TYPE252_F1B = "weapons.nurs.SNEB_TYPE252_F1B"
ENUMS.Storage.weapons.bombs.BL_755 = "weapons.bombs.BL_755"
ENUMS.Storage.weapons.containers.F_15E_AAQ_28_LITENING = "weapons.containers.F-15E_AAQ-28_LITENING"
ENUMS.Storage.weapons.nurs.SNEB_TYPE256_F1B = "weapons.nurs.SNEB_TYPE256_F1B"
ENUMS.Storage.weapons.missiles.AGM_84H = "weapons.missiles.AGM_84H"
ENUMS.Storage.weapons.missiles.AIM_54 = "weapons.missiles.AIM_54"
ENUMS.Storage.weapons.missiles.X_31A = "weapons.missiles.X_31A"
ENUMS.Storage.weapons.bombs.KAB_500Kr = "weapons.bombs.KAB_500Kr"
ENUMS.Storage.weapons.containers.SPS_141_100 = "weapons.containers.SPS-141-100"
ENUMS.Storage.weapons.missiles.BK90_MJ2 = "weapons.missiles.BK90_MJ2"
ENUMS.Storage.weapons.missiles.Super_530D = "weapons.missiles.Super_530D"
ENUMS.Storage.weapons.bombs.CBU_52B = "weapons.bombs.CBU_52B"
ENUMS.Storage.weapons.droptanks.PTB_450 = "weapons.droptanks.PTB-450"
ENUMS.Storage.weapons.bombs.IFV_MCV_80_34720lb = "weapons.bombs.IFV MCV-80 [34720lb]"
ENUMS.Storage.weapons.containers.Two_c9 = "weapons.containers.2-c9"
ENUMS.Storage.weapons.missiles.AIM_9JULI = "weapons.missiles.AIM-9JULI"
ENUMS.Storage.weapons.droptanks.MB339_TT500_R = "weapons.droptanks.MB339_TT500_R"
ENUMS.Storage.weapons.nurs.C_8CM = "weapons.nurs.C_8CM"
ENUMS.Storage.weapons.containers.BARAX = "weapons.containers.BARAX"
ENUMS.Storage.weapons.missiles.P_40R = "weapons.missiles.P_40R"
ENUMS.Storage.weapons.missiles.YJ_12 = "weapons.missiles.YJ-12"
ENUMS.Storage.weapons.missiles.CM_802AKG = "weapons.missiles.CM_802AKG"
ENUMS.Storage.weapons.nurs.SNEB_TYPE254_H1_YELLOW = "weapons.nurs.SNEB_TYPE254_H1_YELLOW"
ENUMS.Storage.weapons.bombs.Durandal = "weapons.bombs.Durandal"
ENUMS.Storage.weapons.droptanks.i16_eft = "weapons.droptanks.i16_eft"
ENUMS.Storage.weapons.droptanks.AV8BNA_AERO1D_EMPTY = "weapons.droptanks.AV8BNA_AERO1D_EMPTY"
ENUMS.Storage.weapons.containers.Hercules_Battle_Station_TGP = "weapons.containers.Hercules_Battle_Station_TGP"
ENUMS.Storage.weapons.nurs.C_8CM_VT = "weapons.nurs.C_8CM_VT"
ENUMS.Storage.weapons.missiles.PL_12 = "weapons.missiles.PL-12"
ENUMS.Storage.weapons.missiles.R_3R = "weapons.missiles.R-3R"
ENUMS.Storage.weapons.bombs.GBU_54_V_1B = "weapons.bombs.GBU_54_V_1B"
ENUMS.Storage.weapons.droptanks.MB339_TT320_R = "weapons.droptanks.MB339_TT320_R"
ENUMS.Storage.weapons.bombs.RN_24 = "weapons.bombs.RN-24"
ENUMS.Storage.weapons.containers.Twoc6m = "weapons.containers.2c6m"
ENUMS.Storage.weapons.bombs.ARV_BRDM_2_Air_12320lb = "weapons.bombs.ARV BRDM-2 Air [12320lb]"
ENUMS.Storage.weapons.bombs.ARV_BRDM_2_Skid_12210lb = "weapons.bombs.ARV BRDM-2 Skid [12210lb]"
ENUMS.Storage.weapons.nurs.SNEB_TYPE251_F1B = "weapons.nurs.SNEB_TYPE251_F1B"
ENUMS.Storage.weapons.missiles.X_41 = "weapons.missiles.X_41"
ENUMS.Storage.weapons.containers.MIG21_SMOKE_WHITE = "weapons.containers.{MIG21_SMOKE_WHITE}"
ENUMS.Storage.weapons.bombs.MK_82AIR = "weapons.bombs.MK_82AIR"
ENUMS.Storage.weapons.missiles.R_530F_EM = "weapons.missiles.R_530F_EM"
ENUMS.Storage.weapons.bombs.SAMP400LD = "weapons.bombs.SAMP400LD"
ENUMS.Storage.weapons.bombs.FAB_50 = "weapons.bombs.FAB_50"
ENUMS.Storage.weapons.bombs.AB_250_2_SD_10A = "weapons.bombs.AB_250_2_SD_10A"
ENUMS.Storage.weapons.missiles.ADM_141A = "weapons.missiles.ADM_141A"
ENUMS.Storage.weapons.containers.KBpod = "weapons.containers.KBpod"
ENUMS.Storage.weapons.bombs.British_GP_500LB_Bomb_Mk4 = "weapons.bombs.British_GP_500LB_Bomb_Mk4"
ENUMS.Storage.weapons.missiles.AGM_65E = "weapons.missiles.AGM_65E"
ENUMS.Storage.weapons.containers.sa342_dipole_antenna = "weapons.containers.sa342_dipole_antenna"
ENUMS.Storage.weapons.bombs.OFAB_100_Jupiter = "weapons.bombs.OFAB-100 Jupiter"
ENUMS.Storage.weapons.nurs.SNEB_TYPE257_F1B = "weapons.nurs.SNEB_TYPE257_F1B"
ENUMS.Storage.weapons.missiles.Rb_04E_for_A_I = "weapons.missiles.Rb 04E (for A.I.)"
ENUMS.Storage.weapons.bombs.AN_M66A2 = "weapons.bombs.AN-M66A2"
ENUMS.Storage.weapons.missiles.P_27T = "weapons.missiles.P_27T"
ENUMS.Storage.weapons.droptanks.LNS_VIG_XTANK = "weapons.droptanks.LNS_VIG_XTANK"
ENUMS.Storage.weapons.missiles.R_55 = "weapons.missiles.R-55"
ENUMS.Storage.weapons.torpedoes.YU_6 = "weapons.torpedoes.YU-6"
ENUMS.Storage.weapons.bombs.British_MC_250LB_Bomb_Mk2 = "weapons.bombs.British_MC_250LB_Bomb_Mk2"
ENUMS.Storage.weapons.droptanks.PTB_120_F86F35 = "weapons.droptanks.PTB_120_F86F35"
ENUMS.Storage.weapons.missiles.PL_8B = "weapons.missiles.PL-8B"
ENUMS.Storage.weapons.droptanks.F_15E_Drop_Tank_Empty = "weapons.droptanks.F-15E_Drop_Tank_Empty"
ENUMS.Storage.weapons.nurs.British_HE_60LBFNo1_3INCHNo1 = "weapons.nurs.British_HE_60LBFNo1_3INCHNo1"
ENUMS.Storage.weapons.missiles.P_77 = "weapons.missiles.P_77"
ENUMS.Storage.weapons.torpedoes.LTF_5B = "weapons.torpedoes.LTF_5B"
ENUMS.Storage.weapons.missiles.R_3S = "weapons.missiles.R-3S"
ENUMS.Storage.weapons.nurs.SNEB_TYPE253_H1 = "weapons.nurs.SNEB_TYPE253_H1"
ENUMS.Storage.weapons.missiles.PL_8A = "weapons.missiles.PL-8A"
ENUMS.Storage.weapons.bombs.APC_BTR_82A_Skid_24888lb = "weapons.bombs.APC BTR-82A Skid [24888lb]"
ENUMS.Storage.weapons.containers.Sborka = "weapons.containers.Sborka"
ENUMS.Storage.weapons.missiles.AGM_65L = "weapons.missiles.AGM_65L"
ENUMS.Storage.weapons.missiles.X_28 = "weapons.missiles.X_28"
ENUMS.Storage.weapons.missiles.TGM_65G = "weapons.missiles.TGM_65G"
ENUMS.Storage.weapons.nurs.SNEB_TYPE257_H1 = "weapons.nurs.SNEB_TYPE257_H1"
ENUMS.Storage.weapons.missiles.RB75B = "weapons.missiles.RB75B"
ENUMS.Storage.weapons.missiles.X_25ML = "weapons.missiles.X_25ML"
ENUMS.Storage.weapons.droptanks.FPU_8A = "weapons.droptanks.FPU_8A"
ENUMS.Storage.weapons.bombs.BLG66 = "weapons.bombs.BLG66"
ENUMS.Storage.weapons.nurs.C_8CM_RD = "weapons.nurs.C_8CM_RD"
ENUMS.Storage.weapons.containers.EclairM_06 = "weapons.containers.{EclairM_06}"
ENUMS.Storage.weapons.bombs.RBK_500AO = "weapons.bombs.RBK_500AO"
ENUMS.Storage.weapons.missiles.AIM_9P = "weapons.missiles.AIM-9P"
ENUMS.Storage.weapons.bombs.British_GP_500LB_Bomb_Mk4_Short = "weapons.bombs.British_GP_500LB_Bomb_Mk4_Short"
ENUMS.Storage.weapons.containers.MB339_Vinten = "weapons.containers.MB339_Vinten"
ENUMS.Storage.weapons.missiles.Rb_15F = "weapons.missiles.Rb 15F"
ENUMS.Storage.weapons.nurs.ARAKM70BHE = "weapons.nurs.ARAKM70BHE"
ENUMS.Storage.weapons.bombs.AAA_Vulcan_M163_Air_21666lb = "weapons.bombs.AAA Vulcan M163 Air [21666lb]"
ENUMS.Storage.weapons.missiles.X_29L = "weapons.missiles.X_29L"
ENUMS.Storage.weapons.containers.F14_LANTIRN_TP = "weapons.containers.{F14-LANTIRN-TP}"
ENUMS.Storage.weapons.bombs.FAB_250_M62 = "weapons.bombs.FAB-250-M62"
ENUMS.Storage.weapons.missiles.AIM_120C = "weapons.missiles.AIM_120C"
ENUMS.Storage.weapons.bombs.EWR_SBORKA_Air_21624lb = "weapons.bombs.EWR SBORKA Air [21624lb]"
ENUMS.Storage.weapons.bombs.SAMP250LD = "weapons.bombs.SAMP250LD"
ENUMS.Storage.weapons.droptanks.Spitfire_slipper_tank = "weapons.droptanks.Spitfire_slipper_tank"
ENUMS.Storage.weapons.missiles.LS_6_500 = "weapons.missiles.LS-6-500"
ENUMS.Storage.weapons.bombs.GBU_31_V_4B = "weapons.bombs.GBU_31_V_4B"
ENUMS.Storage.weapons.droptanks.PTB400_MIG15 = "weapons.droptanks.PTB400_MIG15"
ENUMS.Storage.weapons.containers.m_113 = "weapons.containers.m-113"
ENUMS.Storage.weapons.bombs.SPG_M1128_Stryker_MGS_33036lb = "weapons.bombs.SPG M1128 Stryker MGS [33036lb]"
ENUMS.Storage.weapons.missiles.AIM_9L = "weapons.missiles.AIM-9L"
ENUMS.Storage.weapons.missiles.AIM_9X = "weapons.missiles.AIM_9X"
ENUMS.Storage.weapons.nurs.C_8 = "weapons.nurs.C_8"
ENUMS.Storage.weapons.bombs.SAM_CHAPARRAL_Skid_21516lb = "weapons.bombs.SAM CHAPARRAL Skid [21516lb]"
ENUMS.Storage.weapons.missiles.P_27TE = "weapons.missiles.P_27TE"
ENUMS.Storage.weapons.bombs.ODAB_500PM = "weapons.bombs.ODAB-500PM"
ENUMS.Storage.weapons.bombs.MK77mod1_WPN = "weapons.bombs.MK77mod1-WPN"
ENUMS.Storage.weapons.droptanks.PTB400_MIG19 = "weapons.droptanks.PTB400_MIG19"
ENUMS.Storage.weapons.torpedoes.Mark_46 = "weapons.torpedoes.Mark_46"
ENUMS.Storage.weapons.containers.rightSeat = "weapons.containers.rightSeat"
ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_ORANGE = "weapons.containers.{US_M10_SMOKE_TANK_ORANGE}"
ENUMS.Storage.weapons.bombs.SAB_100MN = "weapons.bombs.SAB_100MN"
ENUMS.Storage.weapons.nurs.FFAR_Mk5_HEAT = "weapons.nurs.FFAR Mk5 HEAT"
ENUMS.Storage.weapons.bombs.IFV_TPZ_FUCH_33440lb = "weapons.bombs.IFV TPZ FUCH [33440lb]"
ENUMS.Storage.weapons.bombs.IFV_M2A2_Bradley_34720lb = "weapons.bombs.IFV M2A2 Bradley [34720lb]"
ENUMS.Storage.weapons.bombs.MK77mod0_WPN = "weapons.bombs.MK77mod0-WPN"
ENUMS.Storage.weapons.containers.ASO_2 = "weapons.containers.ASO-2"
ENUMS.Storage.weapons.bombs.Mk_84AIR_GP = "weapons.bombs.Mk_84AIR_GP"
ENUMS.Storage.weapons.nurs.S_24A = "weapons.nurs.S-24A"
ENUMS.Storage.weapons.bombs.RBK_250_275_AO_1SCH = "weapons.bombs.RBK_250_275_AO_1SCH"
ENUMS.Storage.weapons.bombs.Transport_Tigr_Skid_15730lb = "weapons.bombs.Transport Tigr Skid [15730lb]"
ENUMS.Storage.weapons.missiles.AIM_7F = "weapons.missiles.AIM-7F"
ENUMS.Storage.weapons.bombs.CBU_99 = "weapons.bombs.CBU_99"
ENUMS.Storage.weapons.bombs.LUU_2B = "weapons.bombs.LUU_2B"
ENUMS.Storage.weapons.bombs.FAB_500TA = "weapons.bombs.FAB-500TA"
ENUMS.Storage.weapons.missiles.AGR_20_M282 = "weapons.missiles.AGR_20_M282"
ENUMS.Storage.weapons.droptanks.MB339_FT330 = "weapons.droptanks.MB339_FT330"
ENUMS.Storage.weapons.bombs.SAMP125LD = "weapons.bombs.SAMP125LD"
ENUMS.Storage.weapons.missiles.X_25MP = "weapons.missiles.X_25MP"
ENUMS.Storage.weapons.nurs.SNEB_TYPE252_H1 = "weapons.nurs.SNEB_TYPE252_H1"
ENUMS.Storage.weapons.missiles.AGM_65F = "weapons.missiles.AGM_65F"
ENUMS.Storage.weapons.missiles.AIM_9P5 = "weapons.missiles.AIM-9P5"
ENUMS.Storage.weapons.bombs.Transport_Tigr_Air_15900lb = "weapons.bombs.Transport Tigr Air [15900lb]"
ENUMS.Storage.weapons.nurs.SNEB_TYPE254_H1_RED = "weapons.nurs.SNEB_TYPE254_H1_RED"
ENUMS.Storage.weapons.nurs.FFAR_Mk1_HE = "weapons.nurs.FFAR Mk1 HE"
ENUMS.Storage.weapons.nurs.SPRD_99 = "weapons.nurs.SPRD-99"
ENUMS.Storage.weapons.bombs.BIN_200 = "weapons.bombs.BIN_200"
ENUMS.Storage.weapons.bombs.BLU_4B_GROUP = "weapons.bombs.BLU_4B_GROUP"
ENUMS.Storage.weapons.bombs.GBU_24 = "weapons.bombs.GBU_24"
ENUMS.Storage.weapons.missiles.Rb_04E = "weapons.missiles.Rb 04E"
ENUMS.Storage.weapons.missiles.Rb_74 = "weapons.missiles.Rb 74"
ENUMS.Storage.weapons.containers.leftSeat = "weapons.containers.leftSeat"
ENUMS.Storage.weapons.bombs.LS_6_100 = "weapons.bombs.LS-6-100"
ENUMS.Storage.weapons.bombs.Transport_URAL_375_14815lb = "weapons.bombs.Transport URAL-375 [14815lb]"
ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_GREEN = "weapons.containers.{US_M10_SMOKE_TANK_GREEN}"
ENUMS.Storage.weapons.missiles.X_22 = "weapons.missiles.X_22"
ENUMS.Storage.weapons.containers.FAS = "weapons.containers.FAS"
ENUMS.Storage.weapons.nurs.S_25_O = "weapons.nurs.S-25-O"
ENUMS.Storage.weapons.droptanks.para = "weapons.droptanks.para"
ENUMS.Storage.weapons.droptanks.F_15E_Drop_Tank = "weapons.droptanks.F-15E_Drop_Tank"
ENUMS.Storage.weapons.droptanks.M2KC_08_RPL541_EMPTY = "weapons.droptanks.M2KC_08_RPL541_EMPTY"
ENUMS.Storage.weapons.missiles.X_31P = "weapons.missiles.X_31P"
ENUMS.Storage.weapons.bombs.RBK_500U = "weapons.bombs.RBK_500U"
ENUMS.Storage.weapons.missiles.AIM_54A_Mk47 = "weapons.missiles.AIM_54A_Mk47"
ENUMS.Storage.weapons.droptanks.oiltank = "weapons.droptanks.oiltank"
ENUMS.Storage.weapons.missiles.AGM_154B = "weapons.missiles.AGM_154B"
ENUMS.Storage.weapons.containers.MB339_SMOKE_POD = "weapons.containers.MB339_SMOKE-POD"
ENUMS.Storage.weapons.containers.ECM_POD_L_175V = "weapons.containers.{ECM_POD_L_175V}"
ENUMS.Storage.weapons.droptanks.PTB_580G_F1 = "weapons.droptanks.PTB_580G_F1"
ENUMS.Storage.weapons.containers.EclairM_15 = "weapons.containers.{EclairM_15}"
ENUMS.Storage.weapons.containers.F_15E_AAQ_13_LANTIRN = "weapons.containers.F-15E_AAQ-13_LANTIRN"
ENUMS.Storage.weapons.droptanks.Eight00L_Tank_Empty = "weapons.droptanks.800L Tank Empty"
ENUMS.Storage.weapons.containers.One6c_hts_pod = "weapons.containers.16c_hts_pod"
ENUMS.Storage.weapons.bombs.AN_M81 = "weapons.bombs.AN-M81"
ENUMS.Storage.weapons.droptanks.Mosquito_Drop_Tank_100gal = "weapons.droptanks.Mosquito_Drop_Tank_100gal"
ENUMS.Storage.weapons.droptanks.Mosquito_Drop_Tank_50gal = "weapons.droptanks.Mosquito_Drop_Tank_50gal"
ENUMS.Storage.weapons.droptanks.DFT_150_GAL_A4E = "weapons.droptanks.DFT_150_GAL_A4E"
ENUMS.Storage.weapons.missiles.AIM_9 = "weapons.missiles.AIM_9"
ENUMS.Storage.weapons.bombs.IFV_BTR_D_Air_18040lb = "weapons.bombs.IFV BTR-D Air [18040lb]"
ENUMS.Storage.weapons.containers.EclairM_42 = "weapons.containers.{EclairM_42}"
ENUMS.Storage.weapons.bombs.KAB_1500T = "weapons.bombs.KAB_1500T"
ENUMS.Storage.weapons.droptanks.PTB_490_MIG21 = "weapons.droptanks.PTB-490-MIG21"
ENUMS.Storage.weapons.droptanks.PTB_200_F86F35 = "weapons.droptanks.PTB_200_F86F35"
ENUMS.Storage.weapons.droptanks.PTB760_MIG19 = "weapons.droptanks.PTB760_MIG19"
ENUMS.Storage.weapons.bombs.GBU_43_B_MOAB = "weapons.bombs.GBU-43/B(MOAB)"
ENUMS.Storage.weapons.torpedoes.G7A_T1 = "weapons.torpedoes.G7A_T1"
ENUMS.Storage.weapons.bombs.IFV_BMD_1_Air_18040lb = "weapons.bombs.IFV BMD-1 Air [18040lb]"
ENUMS.Storage.weapons.bombs.SAM_LINEBACKER_34720lb = "weapons.bombs.SAM LINEBACKER [34720lb]"
ENUMS.Storage.weapons.containers.ais_pod_t50_r = "weapons.containers.ais-pod-t50_r"
ENUMS.Storage.weapons.containers.CE2_SMOKE_WHITE = "weapons.containers.{CE2_SMOKE_WHITE}"
ENUMS.Storage.weapons.droptanks.fuel_tank_230 = "weapons.droptanks.fuel_tank_230"
ENUMS.Storage.weapons.droptanks.M2KC_RPL_522 = "weapons.droptanks.M2KC_RPL_522"
ENUMS.Storage.weapons.missiles.AGM_130 = "weapons.missiles.AGM_130"
ENUMS.Storage.weapons.droptanks.Eight00L_Tank = "weapons.droptanks.800L Tank"
ENUMS.Storage.weapons.bombs.IFV_BTR_D_Skid_17930lb = "weapons.bombs.IFV BTR-D Skid [17930lb]"
ENUMS.Storage.weapons.containers.bmp_1 = "weapons.containers.bmp-1"
ENUMS.Storage.weapons.bombs.GBU_31 = "weapons.bombs.GBU_31"
ENUMS.Storage.weapons.containers.aaq_28LEFT_litening = "weapons.containers.aaq-28LEFT litening"
ENUMS.Storage.weapons.missiles.Kh_66_Grom = "weapons.missiles.Kh-66_Grom"
ENUMS.Storage.weapons.containers.MIG21_SMOKE_RED = "weapons.containers.{MIG21_SMOKE_RED}"
ENUMS.Storage.weapons.containers.U22 = "weapons.containers.U22"
ENUMS.Storage.weapons.bombs.IFV_BMD_1_Skid_17930lb = "weapons.bombs.IFV BMD-1 Skid [17930lb]"
ENUMS.Storage.weapons.droptanks.Bidon = "weapons.droptanks.Bidon"
ENUMS.Storage.weapons.bombs.GBU_31_V_2B = "weapons.bombs.GBU_31_V_2B"
ENUMS.Storage.weapons.bombs.Mk_82Y = "weapons.bombs.Mk_82Y"
ENUMS.Storage.weapons.containers.pl5eii = "weapons.containers.pl5eii"
ENUMS.Storage.weapons.bombs.RBK_500U_OAB_2_5RT = "weapons.bombs.RBK_500U_OAB_2_5RT"
ENUMS.Storage.weapons.bombs.British_GP_500LB_Bomb_Mk5 = "weapons.bombs.British_GP_500LB_Bomb_Mk5"
ENUMS.Storage.weapons.containers.Eclair = "weapons.containers.{Eclair}"
ENUMS.Storage.weapons.nurs.S5MO_HEFRAG_FFAR = "weapons.nurs.S5MO_HEFRAG_FFAR"
ENUMS.Storage.weapons.bombs.BETAB_500M = "weapons.bombs.BETAB-500M"
ENUMS.Storage.weapons.bombs.Transport_M818_16000lb = "weapons.bombs.Transport M818 [16000lb]"
ENUMS.Storage.weapons.bombs.British_MC_250LB_Bomb_Mk1 = "weapons.bombs.British_MC_250LB_Bomb_Mk1"
ENUMS.Storage.weapons.nurs.SNEB_TYPE251_H1 = "weapons.nurs.SNEB_TYPE251_H1"
ENUMS.Storage.weapons.bombs.TYPE_200A = "weapons.bombs.TYPE-200A"
ENUMS.Storage.weapons.nurs.HYDRA_70_M151 = "weapons.nurs.HYDRA_70_M151"
ENUMS.Storage.weapons.bombs.IFV_BMP_3_32912lb = "weapons.bombs.IFV BMP-3 [32912lb]"
ENUMS.Storage.weapons.bombs.APC_MTLB_Air_26400lb = "weapons.bombs.APC MTLB Air [26400lb]"
ENUMS.Storage.weapons.nurs.HYDRA_70_M229 = "weapons.nurs.HYDRA_70_M229"
ENUMS.Storage.weapons.bombs.BDU_45 = "weapons.bombs.BDU_45"
ENUMS.Storage.weapons.bombs.OFAB_100_120TU = "weapons.bombs.OFAB-100-120TU"
ENUMS.Storage.weapons.missiles.AIM_9J = "weapons.missiles.AIM-9J"
ENUMS.Storage.weapons.nurs.ARF8M3API = "weapons.nurs.ARF8M3API"
ENUMS.Storage.weapons.bombs.BetAB_500ShP = "weapons.bombs.BetAB_500ShP"
ENUMS.Storage.weapons.nurs.C_8OFP2 = "weapons.nurs.C_8OFP2"
ENUMS.Storage.weapons.bombs.GBU_10 = "weapons.bombs.GBU_10"
ENUMS.Storage.weapons.bombs.APC_MTLB_Skid_26290lb = "weapons.bombs.APC MTLB Skid [26290lb]"
ENUMS.Storage.weapons.nurs.SNEB_TYPE254_F1B_RED = "weapons.nurs.SNEB_TYPE254_F1B_RED"
ENUMS.Storage.weapons.missiles.X_65 = "weapons.missiles.X_65"
ENUMS.Storage.weapons.missiles.R_550_M1 = "weapons.missiles.R_550_M1"
ENUMS.Storage.weapons.missiles.AGM_65K = "weapons.missiles.AGM_65K"
ENUMS.Storage.weapons.nurs.SNEB_TYPE254_F1B_YELLOW = "weapons.nurs.SNEB_TYPE254_F1B_YELLOW"
ENUMS.Storage.weapons.missiles.AGM_88 = "weapons.missiles.AGM_88"
ENUMS.Storage.weapons.nurs.C_8OM = "weapons.nurs.C_8OM"
ENUMS.Storage.weapons.bombs.SAM_ROLAND_LN_34720b = "weapons.bombs.SAM ROLAND LN [34720b]"
ENUMS.Storage.weapons.missiles.AIM_120 = "weapons.missiles.AIM_120"
ENUMS.Storage.weapons.missiles.HOT3_MBDA = "weapons.missiles.HOT3_MBDA"
ENUMS.Storage.weapons.missiles.R_13M = "weapons.missiles.R-13M"
ENUMS.Storage.weapons.missiles.AIM_54C_Mk60 = "weapons.missiles.AIM_54C_Mk60"
ENUMS.Storage.weapons.bombs.AAA_GEPARD_34720lb = "weapons.bombs.AAA GEPARD [34720lb]"
ENUMS.Storage.weapons.missiles.R_13M1 = "weapons.missiles.R-13M1"
ENUMS.Storage.weapons.bombs.APC_Cobra_Air_10912lb = "weapons.bombs.APC Cobra Air [10912lb]"
ENUMS.Storage.weapons.bombs.RBK_250 = "weapons.bombs.RBK_250"
ENUMS.Storage.weapons.bombs.SC_500_J = "weapons.bombs.SC_500_J"
ENUMS.Storage.weapons.missiles.AGM_114K = "weapons.missiles.AGM_114K"
ENUMS.Storage.weapons.missiles.ALARM = "weapons.missiles.ALARM"
ENUMS.Storage.weapons.bombs.Mk_83 = "weapons.bombs.Mk_83"
ENUMS.Storage.weapons.missiles.AGM_65B = "weapons.missiles.AGM_65B"
ENUMS.Storage.weapons.bombs.MK_82SNAKEYE = "weapons.bombs.MK_82SNAKEYE"
ENUMS.Storage.weapons.nurs.HYDRA_70_MK1 = "weapons.nurs.HYDRA_70_MK1"
ENUMS.Storage.weapons.bombs.BLG66_BELOUGA = "weapons.bombs.BLG66_BELOUGA"
ENUMS.Storage.weapons.containers.EclairM_51 = "weapons.containers.{EclairM_51}"
ENUMS.Storage.weapons.missiles.AIM_54A_Mk60 = "weapons.missiles.AIM_54A_Mk60"
ENUMS.Storage.weapons.droptanks.DFT_300_GAL_A4E = "weapons.droptanks.DFT_300_GAL_A4E"
ENUMS.Storage.weapons.bombs.ATGM_M1134_Stryker_30337lb = "weapons.bombs.ATGM M1134 Stryker [30337lb]"
ENUMS.Storage.weapons.bombs.BAT_120 = "weapons.bombs.BAT-120"
ENUMS.Storage.weapons.missiles.DWS39_MJ1_MJ2 = "weapons.missiles.DWS39_MJ1_MJ2"
ENUMS.Storage.weapons.containers.SPRD = "weapons.containers.SPRD"
ENUMS.Storage.weapons.bombs.BR_500 = "weapons.bombs.BR_500"
ENUMS.Storage.weapons.bombs.British_GP_500LB_Bomb_Mk1 = "weapons.bombs.British_GP_500LB_Bomb_Mk1"
ENUMS.Storage.weapons.bombs.BDU_50HD = "weapons.bombs.BDU_50HD"
ENUMS.Storage.weapons.missiles.RS2US = "weapons.missiles.RS2US"
ENUMS.Storage.weapons.bombs.IFV_BMP_2_25168lb = "weapons.bombs.IFV BMP-2 [25168lb]"
ENUMS.Storage.weapons.bombs.SAMP400HD = "weapons.bombs.SAMP400HD"
ENUMS.Storage.weapons.containers.Hercules_Battle_Station = "weapons.containers.Hercules_Battle_Station"
ENUMS.Storage.weapons.bombs.AN_M64 = "weapons.bombs.AN_M64"
ENUMS.Storage.weapons.containers.rearCargoSeats = "weapons.containers.rearCargoSeats"
ENUMS.Storage.weapons.bombs.Mk_82 = "weapons.bombs.Mk_82"
ENUMS.Storage.weapons.missiles.AKD_10 = "weapons.missiles.AKD-10"
ENUMS.Storage.weapons.bombs.BDU_50LGB = "weapons.bombs.BDU_50LGB"
ENUMS.Storage.weapons.missiles.SD_10 = "weapons.missiles.SD-10"
ENUMS.Storage.weapons.containers.IRDeflector = "weapons.containers.IRDeflector"
ENUMS.Storage.weapons.bombs.FAB_500 = "weapons.bombs.FAB_500"
ENUMS.Storage.weapons.bombs.KAB_500 = "weapons.bombs.KAB_500"
ENUMS.Storage.weapons.nurs.S_5M = "weapons.nurs.S-5M"
ENUMS.Storage.weapons.missiles.MICA_R = "weapons.missiles.MICA_R"
ENUMS.Storage.weapons.missiles.X_59M = "weapons.missiles.X_59M"
ENUMS.Storage.weapons.nurs.UG_90MM = "weapons.nurs.UG_90MM"
ENUMS.Storage.weapons.bombs.LYSBOMB = "weapons.bombs.LYSBOMB"
ENUMS.Storage.weapons.nurs.R4M = "weapons.nurs.R4M"
ENUMS.Storage.weapons.containers.dlpod_akg = "weapons.containers.dlpod_akg"
ENUMS.Storage.weapons.missiles.LD_10 = "weapons.missiles.LD-10"
ENUMS.Storage.weapons.bombs.SC_50 = "weapons.bombs.SC_50"
ENUMS.Storage.weapons.nurs.HYDRA_70_MK5 = "weapons.nurs.HYDRA_70_MK5"
ENUMS.Storage.weapons.bombs.FAB_100M = "weapons.bombs.FAB_100M"
ENUMS.Storage.weapons.missiles.Rb_24 = "weapons.missiles.Rb 24"
ENUMS.Storage.weapons.bombs.BDU_45B = "weapons.bombs.BDU_45B"
ENUMS.Storage.weapons.missiles.GB_6_HE = "weapons.missiles.GB-6-HE"
ENUMS.Storage.weapons.missiles.KD_63B = "weapons.missiles.KD-63B"
ENUMS.Storage.weapons.missiles.P_27PE = "weapons.missiles.P_27PE"
ENUMS.Storage.weapons.droptanks.PTB300_MIG15 = "weapons.droptanks.PTB300_MIG15"
ENUMS.Storage.weapons.bombs.Two50_3 = "weapons.bombs.250-3"
ENUMS.Storage.weapons.bombs.SC_500_L2 = "weapons.bombs.SC_500_L2"
ENUMS.Storage.weapons.containers.HMMWV_M1045 = "weapons.containers.HMMWV_M1045"
ENUMS.Storage.weapons.bombs.FAB_500M54TU = "weapons.bombs.FAB-500M54TU"
ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_YELLOW = "weapons.containers.{US_M10_SMOKE_TANK_YELLOW}"
ENUMS.Storage.weapons.containers.EclairM_60 = "weapons.containers.{EclairM_60}"
ENUMS.Storage.weapons.bombs.SAB_250_200 = "weapons.bombs.SAB_250_200"
ENUMS.Storage.weapons.bombs.FAB_100 = "weapons.bombs.FAB_100"
ENUMS.Storage.weapons.bombs.KAB_500S = "weapons.bombs.KAB_500S"
ENUMS.Storage.weapons.missiles.AGM_45A = "weapons.missiles.AGM_45A"
ENUMS.Storage.weapons.missiles.Kh25MP_PRGS1VP = "weapons.missiles.Kh25MP_PRGS1VP"
ENUMS.Storage.weapons.nurs.S5M1_HEFRAG_FFAR = "weapons.nurs.S5M1_HEFRAG_FFAR"
ENUMS.Storage.weapons.containers.kg600 = "weapons.containers.kg600"
ENUMS.Storage.weapons.bombs.AN_M65 = "weapons.bombs.AN_M65"
ENUMS.Storage.weapons.bombs.AN_M57 = "weapons.bombs.AN_M57"
ENUMS.Storage.weapons.bombs.BLU_3B_GROUP = "weapons.bombs.BLU_3B_GROUP"
ENUMS.Storage.weapons.bombs.BAP_100 = "weapons.bombs.BAP-100"
ENUMS.Storage.weapons.containers.HEMTT = "weapons.containers.HEMTT"
ENUMS.Storage.weapons.bombs.British_MC_500LB_Bomb_Mk1_Short = "weapons.bombs.British_MC_500LB_Bomb_Mk1_Short"
ENUMS.Storage.weapons.nurs.ARAKM70BAP = "weapons.nurs.ARAKM70BAP"
ENUMS.Storage.weapons.missiles.AGM_119 = "weapons.missiles.AGM_119"
ENUMS.Storage.weapons.missiles.MMagicII = "weapons.missiles.MMagicII"
ENUMS.Storage.weapons.bombs.AB_500_1_SD_10A = "weapons.bombs.AB_500_1_SD_10A"
ENUMS.Storage.weapons.nurs.HYDRA_70_M282 = "weapons.nurs.HYDRA_70_M282"
ENUMS.Storage.weapons.droptanks.DFT_400_GAL_A4E = "weapons.droptanks.DFT_400_GAL_A4E"
ENUMS.Storage.weapons.nurs.HYDRA_70_M257 = "weapons.nurs.HYDRA_70_M257"
ENUMS.Storage.weapons.droptanks.AV8BNA_AERO1D = "weapons.droptanks.AV8BNA_AERO1D"
ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_BLUE = "weapons.containers.{US_M10_SMOKE_TANK_BLUE}"
ENUMS.Storage.weapons.nurs.ARF8M3HEI = "weapons.nurs.ARF8M3HEI"
ENUMS.Storage.weapons.bombs.RN_28 = "weapons.bombs.RN-28"
ENUMS.Storage.weapons.bombs.Squad_30_x_Soldier_7950lb = "weapons.bombs.Squad 30 x Soldier [7950lb]"
ENUMS.Storage.weapons.containers.uaz_469 = "weapons.containers.uaz-469"
ENUMS.Storage.weapons.containers.Otokar_Cobra = "weapons.containers.Otokar_Cobra"
ENUMS.Storage.weapons.bombs.APC_BTR_82A_Air_24998lb = "weapons.bombs.APC BTR-82A Air [24998lb]"
ENUMS.Storage.weapons.nurs.HYDRA_70_M274 = "weapons.nurs.HYDRA_70_M274"
ENUMS.Storage.weapons.missiles.P_24R = "weapons.missiles.P_24R"
ENUMS.Storage.weapons.nurs.HYDRA_70_MK61 = "weapons.nurs.HYDRA_70_MK61"
ENUMS.Storage.weapons.missiles.Igla_1E = "weapons.missiles.Igla_1E"
ENUMS.Storage.weapons.missiles.C_802AK = "weapons.missiles.C-802AK"
ENUMS.Storage.weapons.nurs.C_24 = "weapons.nurs.C_24"
ENUMS.Storage.weapons.droptanks.M2KC_08_RPL541 = "weapons.droptanks.M2KC_08_RPL541"
ENUMS.Storage.weapons.nurs.C_13 = "weapons.nurs.C_13"
ENUMS.Storage.weapons.droptanks.droptank_110_gal = "weapons.droptanks.droptank_110_gal"
ENUMS.Storage.weapons.bombs.Mk_84 = "weapons.bombs.Mk_84"
ENUMS.Storage.weapons.missiles.Sea_Eagle = "weapons.missiles.Sea_Eagle"
ENUMS.Storage.weapons.droptanks.PTB_1200_F1 = "weapons.droptanks.PTB_1200_F1"
ENUMS.Storage.weapons.nurs.SNEB_TYPE256_H1 = "weapons.nurs.SNEB_TYPE256_H1"
ENUMS.Storage.weapons.containers.MATRA_PHIMAT = "weapons.containers.MATRA-PHIMAT"
ENUMS.Storage.weapons.containers.smoke_pod = "weapons.containers.smoke_pod"
ENUMS.Storage.weapons.containers.F_15E_AAQ_14_LANTIRN = "weapons.containers.F-15E_AAQ-14_LANTIRN"
ENUMS.Storage.weapons.containers.EclairM_24 = "weapons.containers.{EclairM_24}"
ENUMS.Storage.weapons.bombs.GBU_16 = "weapons.bombs.GBU_16"
ENUMS.Storage.weapons.nurs.HYDRA_70_M156 = "weapons.nurs.HYDRA_70_M156"
ENUMS.Storage.weapons.missiles.R_60 = "weapons.missiles.R-60"
ENUMS.Storage.weapons.containers.zsu_23_4 = "weapons.containers.zsu-23-4"
ENUMS.Storage.weapons.missiles.RB75 = "weapons.missiles.RB75"
ENUMS.Storage.weapons.missiles.Mistral = "weapons.missiles.Mistral"
ENUMS.Storage.weapons.droptanks.MB339_TT500_L = "weapons.droptanks.MB339_TT500_L"
ENUMS.Storage.weapons.bombs.SAM_SA_13_STRELA_21624lb = "weapons.bombs.SAM SA-13 STRELA [21624lb]"
ENUMS.Storage.weapons.bombs.SAM_Avenger_M1097_Air_7200lb = "weapons.bombs.SAM Avenger M1097 Air [7200lb]"
ENUMS.Storage.weapons.droptanks.Eleven00L_Tank_Empty = "weapons.droptanks.1100L Tank Empty"
ENUMS.Storage.weapons.bombs.AN_M88 = "weapons.bombs.AN-M88"
ENUMS.Storage.weapons.missiles.S_25L = "weapons.missiles.S_25L"
ENUMS.Storage.weapons.nurs.British_AP_25LBNo1_3INCHNo1 = "weapons.nurs.British_AP_25LBNo1_3INCHNo1"
ENUMS.Storage.weapons.bombs.BDU_50LD = "weapons.bombs.BDU_50LD"
ENUMS.Storage.weapons.bombs.AGM_62 = "weapons.bombs.AGM_62"
ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_WHITE = "weapons.containers.{US_M10_SMOKE_TANK_WHITE}"
ENUMS.Storage.weapons.missiles.MICA_T = "weapons.missiles.MICA_T"
ENUMS.Storage.weapons.containers.HVAR_rocket = "weapons.containers.HVAR_rocket"
-- 2025
ENUMS.Storage.weapons.containers.LANTIRN = "weapons.containers.LANTIRN"
ENUMS.Storage.weapons.missiles.AGM_78B = "weapons.missiles.AGM_78B"
ENUMS.Storage.weapons.containers.uh_60l_pilot = "weapons.containers.uh-60l_pilot"
ENUMS.Storage.weapons.missiles.AIM_92E = "weapons.missiles.AIM-92E"
ENUMS.Storage.weapons.missiles.KD_63B = "weapons.missiles.KD_63B"
ENUMS.Storage.weapons.bombs.Type_200A = "weapons.bombs.Type_200A"
ENUMS.Storage.weapons.missiles.HB_AIM_7E_2 = "weapons.missiles.HB-AIM-7E-2"
ENUMS.Storage.weapons.containers.Spear = "weapons.containers.Spear"
ENUMS.Storage.weapons.missiles.LS_6 = "weapons.missiles.LS_6"
ENUMS.Storage.weapons.containers.HB_ALE_40_0_120 = "weapons.containers.HB_ALE_40_0_120"
ENUMS.Storage.weapons.containers.Fantasm = "weapons.containers.Fantasm"
ENUMS.Storage.weapons.nurs.FFAR_Mk61 = "weapons.nurs.FFAR_Mk61"
ENUMS.Storage.weapons.bombs.HB_F4E_GBU15V1 = "weapons.bombs.HB_F4E_GBU15V1"
ENUMS.Storage.weapons.containers.HB_F14_EXT_AN_APQ_167 = "weapons.containers.HB_F14_EXT_AN_APQ-167"
ENUMS.Storage.weapons.nurs.LWL_RP = "weapons.nurs.LWL_RP"
ENUMS.Storage.weapons.bombs.AGM_62_I = "weapons.bombs.AGM_62_I"
ENUMS.Storage.weapons.containers.ETHER = "weapons.containers.ETHER"
ENUMS.Storage.weapons.containers.TANGAZH = "weapons.containers.TANGAZH"
ENUMS.Storage.weapons.bombs.LYSBOMB_11086 = "weapons.bombs.LYSBOMB 11086"
ENUMS.Storage.weapons.containers.Stub_Wing = "weapons.containers.Stub_Wing"
ENUMS.Storage.weapons.missiles.AIM_9E = "weapons.missiles.AIM-9E"
ENUMS.Storage.weapons.missiles.C_701T = "weapons.missiles.C_701T"
ENUMS.Storage.weapons.bombs.BAP_100 = "weapons.bombs.BAP_100"
ENUMS.Storage.weapons.missiles.CM_802AKG = "weapons.missiles.CM-802AKG"
ENUMS.Storage.weapons.missiles.CM_400AKG = "weapons.missiles.CM-400AKG"
ENUMS.Storage.weapons.missiles.C_802AK = "weapons.missiles.C_802AK"
ENUMS.Storage.weapons.missiles.KD_63 = "weapons.missiles.KD_63"
ENUMS.Storage.weapons.containers.HB_ORD_Pave_Spike_Fast = "weapons.containers.HB_ORD_Pave_Spike_Fast"
ENUMS.Storage.weapons.missiles.SPIKE_ER2 = "weapons.missiles.SPIKE_ER2"
ENUMS.Storage.weapons.containers.KINGAL = "weapons.containers.KINGAL"
ENUMS.Storage.weapons.containers.LANTIRN_F14_TARGET = "weapons.containers.LANTIRN-F14-TARGET"
ENUMS.Storage.weapons.containers.SPS_141 = "weapons.containers.SPS-141"
ENUMS.Storage.weapons.bombs.BLU_3B_GROUP = "weapons.bombs.BLU-3B_GROUP"
ENUMS.Storage.weapons.containers.HB_ALE_40_30_0 = "weapons.containers.HB_ALE_40_30_0"
ENUMS.Storage.weapons.droptanks.HB_HIGH_PERFORMANCE_CENTERLINE_600_GAL = "weapons.droptanks.HB_HIGH_PERFORMANCE_CENTERLINE_600_GAL"
ENUMS.Storage.weapons.containers.ALQ_184 = "weapons.containers.ALQ-184"
ENUMS.Storage.weapons.missiles.AGM_45B = "weapons.missiles.AGM_45B"
ENUMS.Storage.weapons.bombs.BLU_3_GROUP = "weapons.bombs.BLU-3_GROUP"
ENUMS.Storage.weapons.missiles.SPIKE_ER = "weapons.missiles.SPIKE_ER"
ENUMS.Storage.weapons.nurs.ARAKM70BAPPX = "weapons.nurs.ARAKM70BAPPX"
ENUMS.Storage.weapons.bombs.LYSBOMB_11088 = "weapons.bombs.LYSBOMB 11088"
ENUMS.Storage.weapons.bombs.LYSBOMB_11087 = "weapons.bombs.LYSBOMB 11087"
ENUMS.Storage.weapons.missiles.KD_20 = "weapons.missiles.KD_20"
ENUMS.Storage.weapons.droptanks.HB_F_4E_EXT_WingTank = "weapons.droptanks.HB_F-4E_EXT_WingTank"
ENUMS.Storage.weapons.missiles.Rb_04 = "weapons.missiles.Rb_04"
ENUMS.Storage.weapons.containers.AAQ_33 = "weapons.containers.AAQ-33"
ENUMS.Storage.weapons.droptanks.HB_F_4E_EXT_Center_Fuel_Tank_EMPTY = "weapons.droptanks.HB_F-4E_EXT_Center_Fuel_Tank_EMPTY"
ENUMS.Storage.weapons.droptanks.HB_F_4E_EXT_WingTank_R_EMPTY = "weapons.droptanks.HB_F-4E_EXT_WingTank_R_EMPTY"
ENUMS.Storage.weapons.droptanks.HB_F_4E_EXT_WingTank_EMPTY = "weapons.droptanks.HB_F-4E_EXT_WingTank_EMPTY"
ENUMS.Storage.weapons.containers.uh_60l_copilot = "weapons.containers.uh-60l_copilot"
ENUMS.Storage.weapons.droptanks.JAYHAWK_80gal_Fuel_Tankv2 = "weapons.droptanks.JAYHAWK_80gal_Fuel_Tankv2"
ENUMS.Storage.weapons.containers.supply_m134 = "weapons.containers.supply_m134"
ENUMS.Storage.weapons.containers.Seahawk_Pylon = "weapons.containers.Seahawk_Pylon"
ENUMS.Storage.weapons.nurs.LWL_MPP = "weapons.nurs.LWL_MPP"
ENUMS.Storage.weapons.nurs.S_5KP = "weapons.nurs.S_5KP"
ENUMS.Storage.weapons.missiles.AIM_92J = "weapons.missiles.AIM-92J"
ENUMS.Storage.weapons.missiles.HB_AIM_7E = "weapons.missiles.HB-AIM-7E"
ENUMS.Storage.weapons.containers.ALQ_131 = "weapons.containers.ALQ-131"
ENUMS.Storage.weapons.containers.HB_F14_EXT_TARPS = "weapons.containers.HB_F14_EXT_TARPS"
ENUMS.Storage.weapons.containers.MH60_SOAR = "weapons.containers.MH60_SOAR"
ENUMS.Storage.weapons.missiles.YJ_83 = "weapons.missiles.YJ-83"
ENUMS.Storage.weapons.bombs.GBU_8_B = "weapons.bombs.GBU_8_B"
ENUMS.Storage.weapons.containers.HB_F14_EXT_ECA = "weapons.containers.HB_F14_EXT_ECA"
ENUMS.Storage.weapons.bombs.BAP_100 = "weapons.bombs.BAP-100"
ENUMS.Storage.weapons.nurs.M261_MPSM_Rocket = "weapons.nurs.M261_MPSM_Rocket"
ENUMS.Storage.weapons.droptanks.SEAHAWK_120_Fuel_Tank = "weapons.droptanks.SEAHAWK_120_Fuel_Tank"
ENUMS.Storage.weapons.containers.SHPIL = "weapons.containers.SHPIL"
ENUMS.Storage.weapons.bombs.GBU_39 = "weapons.bombs.GBU_39"
ENUMS.Storage.weapons.nurs.S_5M = "weapons.nurs.S_5M"
ENUMS.Storage.weapons.containers.HB_ALE_40_15_90 = "weapons.containers.HB_ALE_40_15_90"
ENUMS.Storage.weapons.missiles.AIM_7E = "weapons.missiles.AIM-7E"
ENUMS.Storage.weapons.missiles.AIM_9P3 = "weapons.missiles.AIM-9P3"
ENUMS.Storage.weapons.missiles.AGM_12B = "weapons.missiles.AGM_12B"
ENUMS.Storage.weapons.missiles.CM_802AKG = "weapons.missiles.CM_802AKG"
ENUMS.Storage.weapons.droptanks.JAYHAWK_120_Fuel_Dual_Tank = "weapons.droptanks.JAYHAWK_120_Fuel_Dual_Tank"
ENUMS.Storage.weapons.droptanks.HB_F_4E_EXT_Center_Fuel_Tank = "weapons.droptanks.HB_F-4E_EXT_Center_Fuel_Tank"
ENUMS.Storage.weapons.containers.PAVETACK = "weapons.containers.PAVETACK"
ENUMS.Storage.weapons.missiles.LS_6_500 = "weapons.missiles.LS_6_500"
ENUMS.Storage.weapons.bombs.LYSBOMB_11089 = "weapons.bombs.LYSBOMB 11089"
ENUMS.Storage.weapons.bombs.BLU_4B_GROUP = "weapons.bombs.BLU-4B_GROUP"
ENUMS.Storage.weapons.containers.ah_64d_radar = "weapons.containers.ah-64d_radar"
ENUMS.Storage.weapons.containers.F_18_LDT_POD = "weapons.containers.F-18-LDT-POD"
ENUMS.Storage.weapons.containers.HB_ALE_40_30_60 = "weapons.containers.HB_ALE_40_30_60"
ENUMS.Storage.weapons.bombs.LS_6_100 = "weapons.bombs.LS_6_100"
ENUMS.Storage.weapons.droptanks.HB_F_4E_EXT_WingTank_R = "weapons.droptanks.HB_F-4E_EXT_WingTank_R"
ENUMS.Storage.weapons.containers.SORBCIJA_R = "weapons.containers.SORBCIJA_R"
ENUMS.Storage.weapons.missiles.CATM_65K = "weapons.missiles.CATM_65K"
ENUMS.Storage.weapons.containers.HB_ORD_Pave_Spike = "weapons.containers.HB_ORD_Pave_Spike"
ENUMS.Storage.weapons.containers.RobbieTank1 = "weapons.containers.RobbieTank1"
ENUMS.Storage.weapons.containers.SKY_SHADOW = "weapons.containers.SKY_SHADOW"
ENUMS.Storage.weapons.containers.SORBCIJA_L = "weapons.containers.SORBCIJA_L"
ENUMS.Storage.weapons.containers.Pavehawk = "weapons.containers.Pavehawk"
ENUMS.Storage.weapons.bombs.BLG66_EG = "weapons.bombs.BLG66_EG"
ENUMS.Storage.weapons.missiles.AGM_12C_ED = "weapons.missiles.AGM_12C_ED"
ENUMS.Storage.weapons.missiles.AIM_92C = "weapons.missiles.AIM-92C"
ENUMS.Storage.weapons.containers.MPS_410 = "weapons.containers.MPS-410"
ENUMS.Storage.weapons.missiles.HJ_12 = "weapons.missiles.HJ-12"
ENUMS.Storage.weapons.containers.AAQ_28_LITENING = "weapons.containers.AAQ-28_LITENING"
ENUMS.Storage.weapons.containers.F_18_FLIR_POD = "weapons.containers.F-18-FLIR-POD"
ENUMS.Storage.weapons.bombs.BLU_3B_GROUP = "weapons.bombs.BLU_3B_GROUP"
ENUMS.Storage.weapons.containers.UH60L_Jayhawk = "weapons.containers.UH60L_Jayhawk"
ENUMS.Storage.weapons.containers.BOZ_100 = "weapons.containers.BOZ-100"
ENUMS.Storage.weapons.missiles.AGM_78A = "weapons.missiles.AGM_78A"
ENUMS.Storage.weapons.missiles.LAU_61_APKWS_M282 = "weapons.missiles.LAU_61_APKWS_M282"
ENUMS.Storage.weapons.bombs.BAP_100 = "weapons.bombs.BAP-100"
ENUMS.Storage.weapons.missiles.CM_802AKG = "weapons.missiles.CM-802AKG"
ENUMS.Storage.weapons.bombs.BLU_3B_GROUP = "weapons.bombs.BLU_3B_GROUP"
ENUMS.Storage.weapons.bombs.BLU_4B_GROUP = "weapons.bombs.BLU-4B_GROUP"
ENUMS.Storage.weapons.nurs.S_5M = "weapons.nurs.S_5M"
ENUMS.Storage.weapons.missiles.AGM_12A = "weapons.missiles.AGM_12A"
ENUMS.Storage.weapons.droptanks.JAYHAWK_120_Fuel_Tank = "weapons.droptanks.JAYHAWK_120_Fuel_Tank"
ENUMS.Storage.weapons.bombs.GBU_15_V_1_B = "weapons.bombs.GBU_15_V_1_B"
ENUMS.Storage.weapons.missiles.HYDRA_70_M151_APKWS = {4,4,8,292}
ENUMS.Storage.weapons.missiles.HYDRA_70_M282_APKWS = {4,4,8,293}
-- dupes with typos
ENUMS.Storage.weapons.bombs.BAP100 = "weapons.bombs.BAP_100"
ENUMS.Storage.weapons.bombs.BLU3B_GROUP = "weapons.bombs.BLU-3B_GROUP"
ENUMS.Storage.weapons.missiles.CM_802AKG = "weapons.missiles.CM_802AKG"
ENUMS.Storage.weapons.bombs.BLU_4B_GROUP = "weapons.bombs.BLU_4B_GROUP"
ENUMS.Storage.weapons.nurs.S5M = "weapons.nurs.S-5M"
-- Gazelle
ENUMS.Storage.weapons.Gazelle.HMP400_100RDS = {4,15,46,1771}
ENUMS.Storage.weapons.Gazelle.HMP400_200RDS = {4,15,46,1770}
ENUMS.Storage.weapons.Gazelle.HMP400_400RDS = {4,15,46,1769}
ENUMS.Storage.weapons.Gazelle.GIAT_M261_AP = {4,15,46,1768}
ENUMS.Storage.weapons.Gazelle.GIAT_M261_SAPHEI = {4,15,46,1767}
ENUMS.Storage.weapons.Gazelle.GIAT_M261_HE = {4,15,46,1766}
ENUMS.Storage.weapons.Gazelle.GIAT_M261_HEAP = {4,15,46,1765}
ENUMS.Storage.weapons.Gazelle.GIAT_M261_APHE = {4,15,46,1764}
ENUMS.Storage.weapons.Gazelle.GAZELLE_IR_DEFLECTOR = {4,15,47,680}
ENUMS.Storage.weapons.Gazelle.GAZELLE_FAS_SANDFILTER = {4,15,47,679}
-- Chinook (changed)
ENUMS.Storage.weapons.CH47.CH47_PORT_M60D = {4,15,46,2489}
ENUMS.Storage.weapons.CH47.CH47_STBD_M60D = {4,15,46,2488}
ENUMS.Storage.weapons.CH47.CH47_AFT_M60D = {4,15,46,2490}
ENUMS.Storage.weapons.CH47.CH47_PORT_M134D = {4,15,46,2494}
ENUMS.Storage.weapons.CH47.CH47_STBD_M134D = {4,15,46,2495}
ENUMS.Storage.weapons.CH47.CH47_AFT_M3M = {4,15,46,2496} --
ENUMS.Storage.weapons.CH47.CH47_PORT_M240H = {4,15,46,2492}
ENUMS.Storage.weapons.CH47.CH47_STBD_M240H = {4,15,46,2491}
ENUMS.Storage.weapons.CH47.CH47_AFT_M240H = {4,15,46,2493}
-- Huey
ENUMS.Storage.weapons.UH1H.M134_MiniGun_Right = {4,15,46,161}
ENUMS.Storage.weapons.UH1H.M134_MiniGun_Left = {4,15,46,160}
ENUMS.Storage.weapons.UH1H.M134_MiniGun_Right_Door  =  {4,15,46,175}
ENUMS.Storage.weapons.UH1H.M60_MG_Right_Door  =  {4,15,46,177}
ENUMS.Storage.weapons.UH1H.M134_MiniGun_Left_Door  =  {4,15,46,174}
ENUMS.Storage.weapons.UH1H.M60_MG_Left_Door  =  {4,15,46,176}
-- UH-60L
ENUMS.Storage.weapons.UH60L.M151_HYDRA = {4, 7, 33, 147} -- 2.75" Hydra, UnGd Rkts M151, HE
ENUMS.Storage.weapons.UH60L.M156_HYDRA = {4, 7, 33, 148} -- 2.75" Hydra, UnGd Rkts M156, Wht Phos
ENUMS.Storage.weapons.UH60L.M229_HYDRA = {4, 7, 33, 148} -- 2.75" Hydra, UnGd Rkts M229, HE
ENUMS.Storage.weapons.UH60L.M257_HYDRA = {4, 7, 33, 151} -- 2.75" Hydra, UnGd Rkts M257, Para Illum
ENUMS.Storage.weapons.UH60L.M259_HYDRA = {4, 7, 33, 151} -- 2.75" Hydra, UnGd Rkts M259, Smoke Marker
ENUMS.Storage.weapons.UH60L.M274_HYDRA = {4, 7, 33, 150} -- 2.75" Hydra, UnGd Rkts M274, Practice Smk
ENUMS.Storage.weapons.UH60L.M134_DOOR_GUN = {4, 15, 46, 3031}
ENUMS.Storage.weapons.UH60L.M3M = {4, 15, 46, 2496}
ENUMS.Storage.weapons.UH60L.M3M_DOOR_GUN = {4, 15, 46, 3032}
ENUMS.Storage.weapons.UH60L.M60_DOOR_GUN = {4, 15, 46, 3033}
ENUMS.Storage.weapons.UH60L.FUEL_TANK_200 = {1,3,43,3023}
ENUMS.Storage.weapons.UH60L.FUEL_TANK_230 = {1,3,43,3024}
ENUMS.Storage.weapons.UH60L.FUEL_TANK_450 = {1,3,43,3025}
ENUMS.Storage.weapons.UH60L.FUEL_TANK_DUAL_AUX = {1,3,43,3026}
ENUMS.Storage.weapons.UH60L.CARGO_SEAT_REAR_ROW = {1,3,43,3030}
ENUMS.Storage.weapons.UH60L.CARGO_SEAT_THREE_ROWS = {1,3,43,3029}
ENUMS.Storage.weapons.UH60L.EMPTY_GUNNER_SEAT_1 = {1,3,43,3027}
ENUMS.Storage.weapons.UH60L.EMPTY_GUNNER_SEAT_2 = {1,3,43,3028}

-- Kiowa
ENUMS.Storage.weapons.OH58.FIM92  =  {4,4,7,449}
ENUMS.Storage.weapons.OH58.MG_M3P100  =  {4,15,46,2611}
ENUMS.Storage.weapons.OH58.MG_M3P200  =  {4,15,46,2610}
ENUMS.Storage.weapons.OH58.MG_M3P300  =  {4,15,46,2609}
ENUMS.Storage.weapons.OH58.MG_M3P400  =  {4,15,46,2608}
ENUMS.Storage.weapons.OH58.MG_M3P500  =  {4,15,46,2607}
ENUMS.Storage.weapons.OH58.Smk_Grenade_Blue  =  {4,5,9,488}
ENUMS.Storage.weapons.OH58.Smk_Grenade_Green  =  {4,5,9,489}
ENUMS.Storage.weapons.OH58.Smk_Grenade_Red  =  {4,5,9,487}
ENUMS.Storage.weapons.OH58.Smk_Grenade_Violet  =  {4,5,9,490}
ENUMS.Storage.weapons.OH58.Smk_Grenade_White  =  {4,5,9,492}
ENUMS.Storage.weapons.OH58.Smk_Grenade_Yellow  =  {4,5,9,491}
-- Apache
ENUMS.Storage.weapons.AH64D.AN_APG78 = {4,15,44,2114}
ENUMS.Storage.weapons.AH64D.Internal_Aux_FuelTank = {1,3,43,1700}
-- Other - but cannot set or track those. Harrier stuff?
ENUMS.Storage.weapons.droptanks.FuelTank_610gal = {1,3,43,10}
ENUMS.Storage.weapons.droptanks.FuelTank_370gal = {1,3,43,11}
ENUMS.Storage.weapons.containers.AV8BNA_GAU_12_AP_M79 = {4,15,46,824}
ENUMS.Storage.weapons.containers.AV8BNA_GAU_12_HE_M792 = {4,15,46,825}
ENUMS.Storage.weapons.containers.AV8BNA_GAU_12_SAPHEI_T = {4,15,46,300}

---
-- @type ENUMS.FARPType
-- @field #string FARP
-- @field #string INVISIBLE
-- @field #string HELIPADSINGLE
-- @field #string PADSINGLE
ENUMS.FARPType = {
  FARP = "FARP",
  INVISIBLE = "INVISIBLE",
  HELIPADSINGLE = "HELIPADSINGLE",
  PADSINGLE = "PADSINGLE",
}


---
-- @type ENUMS.FARPObjectTypeNamesAndShape
-- @field #string FARP
-- @field #string INVISIBLE
-- @field #string HELIPADSINGLE
-- @field #string PADSINGLE
ENUMS.FARPObjectTypeNamesAndShape ={
  [ENUMS.FARPType.FARP] = { TypeName="FARP", ShapeName="FARPS"},
  [ENUMS.FARPType.INVISIBLE] = { TypeName="Invisible FARP", ShapeName="invisiblefarp"},
  [ENUMS.FARPType.HELIPADSINGLE] = { TypeName="SINGLE_HELIPAD", ShapeName="FARP"},
  [ENUMS.FARPType.PADSINGLE] = { TypeName="FARP_SINGLE_01", ShapeName="FARP_SINGLE_01"},
}

--- **Utilities** - Derived utilities taken from the MIST framework, added helpers from the MOOSE community.
--
-- ### Authors:
--
--   * Grimes : Design & Programming of the MIST framework.
--
-- ### Contributions:
--
--   * FlightControl : Rework to OO framework.
--   * And many more
--
-- @module Utilities.Utils
-- @image MOOSE.JPG

--- Smoke color enum `trigger.smokeColor`.
-- @type SMOKECOLOR
-- @field #number Green Green smoke (0)
-- @field #number Red Red smoke (1)
-- @field #number White White smoke (2)
-- @field #number Orange Orange smoke (3)
-- @field #number Blue Blue smoke (4)

SMOKECOLOR = trigger.smokeColor -- #SMOKECOLOR

--- Flare colur enum `trigger.flareColor`.
-- @type FLARECOLOR
-- @field #number Green (0)
-- @field #number Red Red flare (1)
-- @field #number White White flare (2)
-- @field #number Yellow Yellow flare (3)

FLARECOLOR = trigger.flareColor -- #FLARECOLOR

--- Big smoke preset enum.
-- @type BIGSMOKEPRESET
-- @field #number SmallSmokeAndFire Small moke and fire (1)
-- @field #number MediumSmokeAndFire Medium smoke and fire (2)
-- @field #number LargeSmokeAndFire Large smoke and fire (3)
-- @field #number HugeSmokeAndFire Huge smoke and fire (4)
-- @field #number SmallSmoke Small smoke (5)
-- @field #number MediumSmoke Medium smoke (6)
-- @field #number LargeSmoke Large smoke (7)
-- @field #number HugeSmoke Huge smoke (8)
BIGSMOKEPRESET = {
  SmallSmokeAndFire=1,
  MediumSmokeAndFire=2,
  LargeSmokeAndFire=3,
  HugeSmokeAndFire=4,
  SmallSmoke=5,
  MediumSmoke=6,
  LargeSmoke=7,
  HugeSmoke=8,
}

--- DCS map as returned by `env.mission.theatre`.
-- @type DCSMAP
-- @field #string Caucasus Caucasus map.
-- @field #string Normandy Normandy map.
-- @field #string NTTR Nevada Test and Training Range map.
-- @field #string PersianGulf Persian Gulf map.
-- @field #string TheChannel The Channel map.
-- @field #string Syria Syria map.
-- @field #string MarianaIslands Mariana Islands map.
-- @field #string Falklands South Atlantic map.
-- @field #string Sinai Sinai map.
-- @field #string Kola Kola map.
-- @field #string Afghanistan Afghanistan map
-- @field #string Iraq Iraq map
-- @field #string GermanyCW Germany Cold War map
DCSMAP = {
  Caucasus="Caucasus",
  NTTR="Nevada",
  Normandy="Normandy",
  PersianGulf="PersianGulf",
  TheChannel="TheChannel",
  Syria="Syria",
  MarianaIslands="MarianaIslands",
  Falklands="Falklands",
  Sinai="SinaiMap",
  Kola="Kola",
  Afghanistan="Afghanistan",
  Iraq="Iraq",
  GermanyCW="GermanyCW",
}


--- See [DCS_enum_callsigns](https://wiki.hoggitworld.com/view/DCS_enum_callsigns)
-- @type CALLSIGN
CALLSIGN={
  -- Aircraft
  Aircraft={
    Enfield=1,
    Springfield=2,
    Uzi=3,
    Colt=4,
    Dodge=5,
    Ford=6,
    Chevy=7,
    Pontiac=8,
    -- A-10A or A-10C
    Hawg=9,
    Boar=10,
    Pig=11,
    Tusk=12,
  },
  -- AWACS
  AWACS={
    Overlord=1,
    Magic=2,
    Wizard=3,
    Focus=4,
    Darkstar=5,
  },
  -- Tanker
  Tanker={
    Texaco=1,
    Arco=2,
    Shell=3,
    Navy_One=4,
    Mauler=5,
    Bloodhound=6,
    },
  -- JTAC
  JTAC={
    Axeman=1,
    Darknight=2,
    Warrior=3,
    Pointer=4,
    Eyeball=5,
    Moonbeam=6,
    Whiplash=7,
    Finger=8,
    Pinpoint=9,
    Ferret=10,
    Shaba=11,
    Playboy=12,
    Hammer=13,
    Jaguar=14,
    Deathstar=15,
    Anvil=16,
    Firefly=17,
    Mantis=18,
    Badger=19,
  },
  -- FARP
  FARP={
    London=1,
    Dallas=2,
    Paris=3,
    Moscow=4,
    Berlin=5,
    Rome=6,
    Madrid=7,
    Warsaw=8,
    Dublin=9,
    Perth=10,
  },
  F16={
    Viper=9,
    Venom=10,
    Lobo=11,
    Cowboy=12,
    Python=13,
    Rattler=14,
    Panther=15,
    Wolf=16,
    Weasel=17,
    Wild=18,
    Ninja=19,
    Jedi=20,
  },
  F18={
    Hornet=9,
    Squid=10,
    Ragin=11,
    Roman=12,
    Sting=13,
    Jury=14,
    Jokey=15,
    Ram=16,
    Hawk=17,
    Devil=18,
    Check=19,
    Snake=20,
  },
  F15E={
    Dude=9,
    Thud=10,
    Gunny=11,
    Trek=12,
    Sniper=13,
    Sled=14,
    Best=15,
    Jazz=16,
    Rage=17,
    Tahoe=18,
  },
  B1B={
    Bone=9,
    Dark=10,
    Vader=11
  },
  B52={
    Buff=9,
    Dump=10,
    Kenworth=11,
  },
  TransportAircraft={
    Heavy=9,
    Trash=10,
    Cargo=11,
    Ascot=12,
  },
  AH64={
    Army_Air = 9,
    Apache = 10,
    Crow = 11,
    Sioux = 12,
    Gatling = 13,
    Gunslinger = 14,
    Hammerhead = 15,
    Bootleg = 16,
    Palehorse = 17,
    Carnivor = 18,
    Saber = 19,
  },
  Kiowa = {
    Anvil = 1,
    Azrael = 2,
    BamBam = 3,
    Blackjack = 4,
    Bootleg = 5,
    BurninStogie = 6,
    Chaos = 7,
    CrazyHorse = 8,
    Crusader = 9,
    Darkhorse = 10,
    Eagle = 11,
    Lighthorse = 12,
    Mustang = 13,
    Outcast = 14,
    Palehorse = 15,
    Pegasus = 16,
    Pistol = 17,
    Roughneck = 18,
    Saber = 19,
    Shamus = 20,
    Spur = 21,
    Stetson = 22,
    Wrath = 23,
  },
  
} --#CALLSIGN

--- Utilities static class.
-- @type UTILS
-- @field #number _MarkID Marker index counter. Running number when marker is added.
UTILS = {
  _MarkID = 1
}

--- Function to infer instance of an object
--
-- ### Examples:
--
--    * UTILS.IsInstanceOf( 'some text', 'string' ) will return true
--    * UTILS.IsInstanceOf( some_function, 'function' ) will return true
--    * UTILS.IsInstanceOf( 10, 'number' ) will return true
--    * UTILS.IsInstanceOf( false, 'boolean' ) will return true
--    * UTILS.IsInstanceOf( nil, 'nil' ) will return true
--
--    * UTILS.IsInstanceOf( ZONE:New( 'some zone', ZONE ) will return true
--    * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'ZONE' ) will return true
--    * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'zone' ) will return true
--    * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'BASE' ) will return true
--
--    * UTILS.IsInstanceOf( ZONE:New( 'some zone', 'GROUP' ) will return false
--
--
-- @param object is the object to be evaluated
-- @param className is the name of the class to evaluate (can be either a string or a Moose class)
-- @return #boolean
UTILS.IsInstanceOf = function( object, className )
  -- Is className NOT a string ?
  if type( className ) ~= 'string' then

    -- Is className a Moose class ?
    if type( className ) == 'table' and className.IsInstanceOf ~= nil then

      -- Get the name of the Moose class as a string
      className = className.ClassName

    -- className is neither a string nor a Moose class, throw an error
    else

      -- I'm not sure if this should take advantage of MOOSE logging function, or throw an error for pcall
      local err_str = 'className parameter should be a string; parameter received: '..type( className )
      return false
      -- error( err_str )

    end
  end

  -- Is the object a Moose class instance ?
  if type( object ) == 'table' and object.IsInstanceOf ~= nil then

    -- Use the IsInstanceOf method of the BASE class
    return object:IsInstanceOf( className )
  else

    -- If the object is not an instance of a Moose class, evaluate against lua basic data types
    local basicDataTypes = { 'string', 'number', 'function', 'boolean', 'nil', 'table' }
    for _, basicDataType in ipairs( basicDataTypes ) do
      if className == basicDataType then
        return type( object ) == basicDataType
      end
    end
  end

  -- Check failed
  return false
end


--- Deep copy a table. See http://lua-users.org/wiki/CopyTable
-- @param #table object The input table.
-- @return #table Copy of the input table.
UTILS.DeepCopy = function(object)

  local lookup_table = {}

  -- Copy function.
  local function _copy(object)
    if type(object) ~= "table" then
      return object
    elseif lookup_table[object] then
      return lookup_table[object]
    end

    local new_table = {}

    lookup_table[object] = new_table

    for index, value in pairs(object) do
      new_table[_copy(index)] = _copy(value)
    end

    return setmetatable(new_table, getmetatable(object))
  end

  local objectreturn = _copy(object)

  return objectreturn
end


--- Serialize a given table.
-- @param #table tbl Input table.
-- @return #string Table as a string.
UTILS.OneLineSerialize = function( tbl )  -- serialization of a table all on a single line, no comments, made to replace old get_table_string function

  local lookup_table = {}

  local function _Serialize( tbl )

    if type(tbl) == 'table' then --function only works for tables!

      if lookup_table[tbl] then
        return lookup_table[object]
      end

      local tbl_str = {}

      lookup_table[tbl] = tbl_str

      tbl_str[#tbl_str + 1] = '{'

      for ind,val in pairs(tbl) do -- serialize its fields
        local ind_str = {}
        if type(ind) == "number" then
          ind_str[#ind_str + 1] = '['
          ind_str[#ind_str + 1] = tostring(ind)
          ind_str[#ind_str + 1] = ']='
        else --must be a string
          ind_str[#ind_str + 1] = '['
          ind_str[#ind_str + 1] = UTILS.BasicSerialize(ind)
          ind_str[#ind_str + 1] = ']='
        end

        local val_str = {}
        if ((type(val) == 'number') or (type(val) == 'boolean')) then
          val_str[#val_str + 1] = tostring(val)
          val_str[#val_str + 1] = ','
          tbl_str[#tbl_str + 1] = table.concat(ind_str)
          tbl_str[#tbl_str + 1] = table.concat(val_str)
      elseif type(val) == 'string' then
          val_str[#val_str + 1] = UTILS.BasicSerialize(val)
          val_str[#val_str + 1] = ','
          tbl_str[#tbl_str + 1] = table.concat(ind_str)
          tbl_str[#tbl_str + 1] = table.concat(val_str)
        elseif type(val) == 'nil' then -- won't ever happen, right?
          val_str[#val_str + 1] = 'nil,'
          tbl_str[#tbl_str + 1] = table.concat(ind_str)
          tbl_str[#tbl_str + 1] = table.concat(val_str)
        elseif type(val) == 'table' then
          if ind == "__index" then
          --  tbl_str[#tbl_str + 1] = "__index"
          --  tbl_str[#tbl_str + 1] = ','   --I think this is right, I just added it
          else

            val_str[#val_str + 1] = _Serialize(val)
            val_str[#val_str + 1] = ','   --I think this is right, I just added it
            tbl_str[#tbl_str + 1] = table.concat(ind_str)
            tbl_str[#tbl_str + 1] = table.concat(val_str)
          end
        elseif type(val) == 'function' then
          tbl_str[#tbl_str + 1] = "f() " .. tostring(ind)
          tbl_str[#tbl_str + 1] = ','   --I think this is right, I just added it
        else
          env.info('unable to serialize value type ' .. UTILS.BasicSerialize(type(val)) .. ' at index ' .. tostring(ind))
          env.info( debug.traceback() )
        end

      end
      tbl_str[#tbl_str + 1] = '}'
      return table.concat(tbl_str)
    else
      return tostring(tbl)
    end
  end

  local objectreturn = _Serialize(tbl)
  return objectreturn
end

--- Serialize a table to a single line string.
-- @param #table tbl table to serialize.
-- @return #string string containing serialized table.
function UTILS._OneLineSerialize(tbl)

  if type(tbl) == 'table' then --function only works for tables!

    local tbl_str = {}

    tbl_str[#tbl_str + 1] = '{ '

    for ind,val in pairs(tbl) do -- serialize its fields
      if type(ind) == "number" then
        tbl_str[#tbl_str + 1] = '['
        tbl_str[#tbl_str + 1] = tostring(ind)
        tbl_str[#tbl_str + 1] = '] = '
      else --must be a string
        tbl_str[#tbl_str + 1] = '['
        tbl_str[#tbl_str + 1] = UTILS.BasicSerialize(ind)
        tbl_str[#tbl_str + 1] = '] = '
      end

      if ((type(val) == 'number') or (type(val) == 'boolean')) then
        tbl_str[#tbl_str + 1] = tostring(val)
        tbl_str[#tbl_str + 1] = ', '
      elseif type(val) == 'string' then
        tbl_str[#tbl_str + 1] = UTILS.BasicSerialize(val)
        tbl_str[#tbl_str + 1] = ', '
      elseif type(val) == 'nil' then -- won't ever happen, right?
        tbl_str[#tbl_str + 1] = 'nil, '
      elseif type(val) == 'table' then
        --tbl_str[#tbl_str + 1] = UTILS.TableShow(tbl,loc,indent,tableshow_tbls)
        --tbl_str[#tbl_str + 1] = ', '   --I think this is right, I just added it
      else
        --log:warn('Unable to serialize value type $1 at index $2', mist.utils.basicSerialize(type(val)), tostring(ind))
      end

    end

      tbl_str[#tbl_str + 1] = '}'
      return table.concat(tbl_str)
    else
      return  UTILS.BasicSerialize(tbl)
  end
end

--- Basic serialize (porting in Slmod's "safestring" basic serialize).
-- @param #string s Table to serialize.
UTILS.BasicSerialize = function(s)
  if s == nil then
    return "\"\""
  else
    if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'userdata') ) then
      return tostring(s)
    elseif type(s) == "table" then
      return UTILS._OneLineSerialize(s)
    elseif type(s) == 'string' then
      s = string.format('(%s)', s)
      return s
    end
  end
end

--- Counts the number of elements in a table.
-- @param #table T Table to count
-- @return #number Number of elements in the table
function UTILS.TableLength(T)
  local count = 0
  for _ in pairs(T or {}) do count = count + 1 end
  return count
end

--- Print a table to log in a nice format
-- @param #table table The table to print
-- @param #number indent Number of indents
-- @param #boolean noprint Don't log but return text
-- @return #string text Text created on the fly of the log output
function UTILS.PrintTableToLog(table, indent, noprint)
  local text = "\n"
  if not table or type(table) ~= "table" then
    env.warning("No table passed!")
    return nil
  end
  if not indent then indent = 0 end
  for k, v in pairs(table) do
    if string.find(k," ") then k='"'..k..'"'end
    if type(v) == "table" and UTILS.TableLength(v) > 0 then
      if not noprint then
        env.info(string.rep("  ", indent) .. tostring(k) .. " = {")
      end
      text = text ..string.rep("  ", indent) .. tostring(k) .. " = {\n"
      text = text .. tostring(UTILS.PrintTableToLog(v, indent + 1), noprint).."\n"
      if not noprint then
        env.info(string.rep("  ", indent) .. "},")
      end
      text = text .. string.rep("  ", indent) .. "},\n"
    elseif type(v) == "function" then
    else
      local value
      if tostring(v) == "true" or tostring(v) == "false" or tonumber(v) ~= nil then
        value=v
      else
        value = '"'..tostring(v)..'"'
      end
      if not noprint then
        env.info(string.rep("  ", indent) .. tostring(k) .. " = " .. tostring(value)..",\n")
      end
      text = text .. string.rep("  ", indent) .. tostring(k) .. " = " .. tostring(value)..",\n"
    end
  end
  return text
end

--- Returns table in a easy readable string representation.
-- @param tbl table to show
-- @param loc
-- @param indent
-- @param tableshow_tbls
-- @return Human readable string representation of given table.
function UTILS.TableShow(tbl, loc, indent, tableshow_tbls)
  tableshow_tbls = tableshow_tbls or {} --create table of tables
  loc = loc or ""
  indent = indent or ""
  if type(tbl) == 'table' then --function only works for tables!
    tableshow_tbls[tbl] = loc

    local tbl_str = {}

    tbl_str[#tbl_str + 1] = indent .. '{\n'

    for ind,val in pairs(tbl) do -- serialize its fields
      if type(ind) == "number" then
        tbl_str[#tbl_str + 1] = indent
        tbl_str[#tbl_str + 1] = loc .. '['
        tbl_str[#tbl_str + 1] = tostring(ind)
        tbl_str[#tbl_str + 1] = '] = '
      else
        tbl_str[#tbl_str + 1] = indent
        tbl_str[#tbl_str + 1] = loc .. '['
        tbl_str[#tbl_str + 1] = UTILS.BasicSerialize(ind)
        tbl_str[#tbl_str + 1] = '] = '
      end

      if ((type(val) == 'number') or (type(val) == 'boolean')) then
        tbl_str[#tbl_str + 1] = tostring(val)
        tbl_str[#tbl_str + 1] = ',\n'
      elseif type(val) == 'string' then
        tbl_str[#tbl_str + 1] = UTILS.BasicSerialize(val)
        tbl_str[#tbl_str + 1] = ',\n'
      elseif type(val) == 'nil' then -- won't ever happen, right?
        tbl_str[#tbl_str + 1] = 'nil,\n'
      elseif type(val) == 'table' then
        if tableshow_tbls[val] then
          tbl_str[#tbl_str + 1] = tostring(val) .. ' already defined: ' .. tableshow_tbls[val] .. ',\n'
        else
          tableshow_tbls[val] = loc ..  '[' .. UTILS.BasicSerialize(ind) .. ']'
          tbl_str[#tbl_str + 1] = tostring(val) .. ' '
          tbl_str[#tbl_str + 1] = UTILS.TableShow(val, loc .. '[' .. UTILS.BasicSerialize(ind).. ']', indent .. '    ', tableshow_tbls)
          tbl_str[#tbl_str + 1] = ',\n'
        end
      elseif type(val) == 'function' then
        if debug and debug.getinfo then
          local fcnname = tostring(val)
          local info = debug.getinfo(val, "S")
          if info.what == "C" then
            tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', C function') .. ',\n'
          else
            if (string.sub(info.source, 1, 2) == [[./]]) then
              tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')' .. info.source) ..',\n'
            else
              tbl_str[#tbl_str + 1] = string.format('%q', fcnname .. ', defined in (' .. info.linedefined .. '-' .. info.lastlinedefined .. ')') ..',\n'
            end
          end

        else
          tbl_str[#tbl_str + 1] = 'a function,\n'
        end
      else
        tbl_str[#tbl_str + 1] = 'unable to serialize value type ' .. UTILS.BasicSerialize(type(val)) .. ' at index ' .. tostring(ind)
      end
    end

    tbl_str[#tbl_str + 1] = indent .. '}'
    return table.concat(tbl_str)
  end
end

--- Dumps the global table _G.
-- This dumps the global table _G to a file in the DCS\Logs directory.
-- This function requires you to disable script sanitization in $DCS_ROOT\Scripts\MissionScripting.lua to access lfs and io libraries.
-- @param #string fname File name.
function UTILS.Gdump(fname)
  if lfs and io then

    local fdir = lfs.writedir() .. [[Logs\]] .. fname

    local f = io.open(fdir, 'w')

    f:write(UTILS.TableShow(_G))

    f:close()

    env.info(string.format('Wrote debug data to $1', fdir))
  else
    env.error("WARNING: lfs and/or io not de-sanitized - cannot dump _G!")
  end
end

--- Executes the given string.
-- borrowed from Slmod
-- @param #string s string containing LUA code.
-- @return #boolean `true` if successfully executed, `false` otherwise.
function UTILS.DoString(s)
  local f, err = loadstring(s)
  if f then
    return true, f()
  else
    return false, err
  end
end


UTILS.ToDegree = function(angle)
  return angle*180/math.pi
end

UTILS.ToRadian = function(angle)
  return angle*math.pi/180
end

UTILS.MetersToNM = function(meters)
  return meters/1852
end

UTILS.KiloMetersToNM = function(kilometers)
  return kilometers/1852*1000
end

UTILS.MetersToSM = function(meters)
  return meters/1609.34
end

UTILS.KiloMetersToSM = function(kilometers)
  return kilometers/1609.34*1000
end

UTILS.MetersToFeet = function(meters)
  return meters/0.3048
end

UTILS.KiloMetersToFeet = function(kilometers)
  return kilometers/0.3048*1000
end

UTILS.NMToMeters = function(NM)
  return NM*1852
end

UTILS.NMToKiloMeters = function(NM)
  return NM*1852/1000
end

UTILS.FeetToMeters = function(feet)
  return feet*0.3048
end

UTILS.KnotsToKmph = function(knots)
  return knots * 1.852
end

UTILS.KmphToKnots = function(knots)
  return knots / 1.852
end

UTILS.KmphToMps = function( kmph )
  return kmph / 3.6
end

UTILS.MpsToKmph = function( mps )
  return mps * 3.6
end

UTILS.MiphToMps = function( miph )
  return miph * 0.44704
end

--- Convert meters per second to miles per hour.
-- @param #number mps Speed in m/s.
-- @return #number Speed in miles per hour.
UTILS.MpsToMiph = function( mps )
  return mps / 0.44704
end

--- Convert meters per second to knots.
-- @param #number mps Speed in m/s.
-- @return #number Speed in knots.
UTILS.MpsToKnots = function( mps )
  return mps * 1.94384 --3600 / 1852
end

--- Convert knots to meters per second.
-- @param #number knots Speed in knots.
-- @return #number Speed in m/s.
UTILS.KnotsToMps = function( knots )
  if type(knots) == "number" then
    return knots / 1.94384 --* 1852 / 3600
  else
   return 0
  end
end

--- Convert temperature from Celsius to Fahrenheit.
-- @param #number Celcius Temperature in degrees Celsius.
-- @return #number Temperature in degrees Fahrenheit.
UTILS.CelsiusToFahrenheit = function( Celcius )
  return Celcius * 9/5 + 32
end

--- Convert pressure from hecto Pascal (hPa) to inches of mercury (inHg).
-- @param #number hPa Pressure in hPa.
-- @return #number Pressure in inHg.
UTILS.hPa2inHg = function( hPa )
  return hPa * 0.0295299830714
end

--- Convert indicated airspeed (IAS) to true airspeed (TAS) for a given altitude above main sea level.
-- The conversion is based on the approximation that TAS is ~2% higher than IAS with every 1000 ft altitude above sea level.
-- @param #number ias Indicated air speed in any unit (m/s, km/h, knots, ...)
-- @param #number altitude Altitude above main sea level in meters.
-- @param #number oatcorr (Optional) Outside air temperature correction factor. Default 0.017.
-- @return #number True airspeed in the same unit the IAS has been given.
UTILS.IasToTas = function( ias, altitude, oatcorr )
  oatcorr=oatcorr or 0.017
  local tas=ias + (ias * oatcorr * UTILS.MetersToFeet(altitude) / 1000)
  return tas
end

--- Convert true airspeed (TAS) to indicated airspeed (IAS) for a given altitude above main sea level.
-- The conversion is based on the approximation that TAS is ~2% higher than IAS with every 1000 ft altitude above sea level.
-- @param #number tas True air speed in any unit (m/s, km/h, knots, ...)
-- @param #number altitude Altitude above main sea level in meters.
-- @param #number oatcorr (Optional) Outside air temperature correction factor. Default 0.017.
-- @return #number Indicated airspeed in the same unit the TAS has been given.
UTILS.TasToIas = function( tas, altitude, oatcorr )
  oatcorr=oatcorr or 0.017
  local ias=tas/(1+oatcorr*UTILS.MetersToFeet(altitude)/1000)
  return ias
end


--- Convert knots to altitude corrected KIAS, e.g. for tankers.
-- @param #number knots Speed in knots.
-- @param #number altitude Altitude in feet
-- @return #number Corrected KIAS
UTILS.KnotsToAltKIAS = function( knots, altitude )
  return (knots * 0.018 * (altitude / 1000)) + knots
end

--- Convert pressure from hecto Pascal (hPa) to millimeters of mercury (mmHg).
-- @param #number hPa Pressure in hPa.
-- @return #number Pressure in mmHg.
UTILS.hPa2mmHg = function( hPa )
  return hPa * 0.7500615613030
end

--- Convert kilo gramms (kg) to pounds (lbs).
-- @param #number kg Mass in kg.
-- @return #number Mass in lbs.
UTILS.kg2lbs = function( kg )
  return kg * 2.20462
end

--[[acc:
in DM: decimal point of minutes.
In DMS: decimal point of seconds.
position after the decimal of the least significant digit:
So:
42.32 - acc of 2.
]]
UTILS.tostringLL = function( lat, lon, acc, DMS)

  local latHemi, lonHemi
  if lat > 0 then
    latHemi = 'N'
  else
    latHemi = 'S'
  end

  if lon > 0 then
    lonHemi = 'E'
  else
    lonHemi = 'W'
  end

  lat = math.abs(lat)
  lon = math.abs(lon)

  local latDeg = math.floor(lat)
  local latMin = (lat - latDeg)*60

  local lonDeg = math.floor(lon)
  local lonMin = (lon - lonDeg)*60

  if DMS then  -- degrees, minutes, and seconds.
    local oldLatMin = latMin
    latMin = math.floor(latMin)
    local latSec = UTILS.Round((oldLatMin - latMin)*60, acc)

    local oldLonMin = lonMin
    lonMin = math.floor(lonMin)
    local lonSec = UTILS.Round((oldLonMin - lonMin)*60, acc)

    if latSec == 60 then
      latSec = 0
      latMin = latMin + 1
    end

    if lonSec == 60 then
      lonSec = 0
      lonMin = lonMin + 1
    end

    local secFrmtStr -- create the formatting string for the seconds place
    secFrmtStr = '%02d'
    if acc <= 0 then  -- no decimal place.
      secFrmtStr = '%02d'
    else
      local width = 3 + acc  -- 01.310 - that's a width of 6, for example. Acc is limited to 2 for DMS!
      secFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
    end

    -- 024° 23' 12"N or 024° 23' 12.03"N
    return string.format('%03d°', latDeg)..string.format('%02d', latMin)..'\''..string.format(secFrmtStr, latSec)..'"'..latHemi..' '
        .. string.format('%03d°', lonDeg)..string.format('%02d', lonMin)..'\''..string.format(secFrmtStr, lonSec)..'"'..lonHemi

  else  -- degrees, decimal minutes.
    latMin = UTILS.Round(latMin, acc)
    lonMin = UTILS.Round(lonMin, acc)

    if latMin == 60 then
      latMin = 0
      latDeg = latDeg + 1
    end

    if lonMin == 60 then
      lonMin = 0
      lonDeg = lonDeg + 1
    end

    local minFrmtStr -- create the formatting string for the minutes place
    if acc <= 0 then  -- no decimal place.
      minFrmtStr = '%02d'
    else
      local width = 3 + acc  -- 01.310 - that's a width of 6, for example.
      minFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
    end

    -- 024 23'N or 024 23.123'N
    return string.format('%03d°', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. '   '
        .. string.format('%03d°', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi

  end
end

--[[acc:
in DM: decimal point of minutes.
In DMS: decimal point of seconds.
position after the decimal of the least significant digit:
So:
42.32 - acc of 2.
]]
UTILS.tostringLLM2KData = function( lat, lon, acc)

  local latHemi, lonHemi
  if lat > 0 then
    latHemi = 'N'
  else
    latHemi = 'S'
  end

  if lon > 0 then
    lonHemi = 'E'
  else
    lonHemi = 'W'
  end

  lat = math.abs(lat)
  lon = math.abs(lon)

  local latDeg = math.floor(lat)
  local latMin = (lat - latDeg)*60

  local lonDeg = math.floor(lon)
  local lonMin = (lon - lonDeg)*60

  -- degrees, decimal minutes.
  latMin = UTILS.Round(latMin, acc)
  lonMin = UTILS.Round(lonMin, acc)

  if latMin == 60 then
    latMin = 0
    latDeg = latDeg + 1
  end

  if lonMin == 60 then
    lonMin = 0
    lonDeg = lonDeg + 1
  end

  local minFrmtStr -- create the formatting string for the minutes place
  if acc <= 0 then  -- no decimal place.
    minFrmtStr = '%02d'
  else
    local width = 3 + acc  -- 01.310 - that's a width of 6, for example.
    minFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
  end

  -- 024 23'N or 024 23.123'N
  return latHemi..string.format('%02d:', latDeg) .. string.format(minFrmtStr, latMin), lonHemi..string.format('%02d:', lonDeg) .. string.format(minFrmtStr, lonMin)

end

-- acc- the accuracy of each easting/northing.  0, 1, 2, 3, 4, or 5.
UTILS.tostringMGRS = function(MGRS, acc) --R2.1

  if acc <= 0 then
    return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph
  else

    if acc > 5 then acc = 5 end

    -- Test if Easting/Northing have less than 4 digits.
    --MGRS.Easting=123    -- should be 00123
    --MGRS.Northing=5432  -- should be 05432

    -- Truncate rather than round MGRS grid!
    local Easting=tostring(MGRS.Easting)
    local Northing=tostring(MGRS.Northing)

    -- Count number of missing digits. Easting/Northing should have 5 digits. However, it is passed as a number. Therefore, any leading zeros would not be displayed by lua.
    local nE=5-string.len(Easting)
    local nN=5-string.len(Northing)

    -- Get leading zeros (if any).
    for i=1,nE do Easting="0"..Easting end
    for i=1,nN do Northing="0"..Northing end

    -- Return MGRS string.
    return string.format("%s %s %s %s", MGRS.UTMZone, MGRS.MGRSDigraph, string.sub(Easting, 1, acc), string.sub(Northing, 1, acc))
  end

end


--- From http://lua-users.org/wiki/SimpleRound
-- use negative idp for rounding ahead of decimal place, positive for rounding after decimal place
function UTILS.Round( num, idp )
  local mult = 10 ^ ( idp or 0 )
  return math.floor( num * mult + 0.5 ) / mult
end

--- Porting in Slmod's dostring - execute a string as LUA code with error handling.
-- @param #string s The code as string to be executed
-- @return #boolean success If true, code was successfully executed, else false
-- @return #string Outcome Code outcome if successful or error string if not successful
function UTILS.DoString( s )
  local f, err = loadstring( s )
  if f then
    return true, f()
  else
    return false, err
  end
end

--- Here is a customized version of pairs, which I called spairs because it iterates over the table in a sorted order.
-- @param #table t The table
-- @param #string order (Optional) The sorting function
-- @return #string key The index key
-- @return #string value The value at the indexed key
-- @usage
--            for key,value in UTILS.spairs(mytable) do
--                -- your code here
--            end
function UTILS.spairs( t, order )
    -- collect the keys
    local keys = {}
    for k in pairs(t) do keys[#keys+1] = k end

    -- if order function given, sort by it by passing the table and keys a, b,
    -- otherwise just sort the keys
    if order then
        table.sort(keys, function(a,b) return order(t, a, b) end)
    else
        table.sort(keys)
    end

    -- return the iterator function
    local i = 0
    return function()
        i = i + 1
        if keys[i] then
            return keys[i], t[keys[i]]
        end
    end
end


--- Here is a customized version of pairs, which I called kpairs because it iterates over the table in a sorted order, based on a function that will determine the keys as reference first.
-- @param #table t The table
-- @param #string getkey The function to determine the keys for sorting
-- @param #string order (Optional) The sorting function itself
-- @return #string key The index key
-- @return #string value The value at the indexed key
-- @usage
--            for key,value in UTILS.kpairs(mytable, getkeyfunc) do
--                -- your code here
--            end
function UTILS.kpairs( t, getkey, order )
    -- collect the keys
    local keys = {}
    local keyso = {}
    for k, o in pairs(t) do keys[#keys+1] = k keyso[#keyso+1] = getkey( o ) end

    -- if order function given, sort by it by passing the table and keys a, b,
    -- otherwise just sort the keys
    if order then
        table.sort(keys, function(a,b) return order(t, a, b) end)
    else
        table.sort(keys)
    end

    -- return the iterator function
    local i = 0
    return function()
        i = i + 1
        if keys[i] then
            return keyso[i], t[keys[i]]
        end
    end
end

--- Here is a customized version of pairs, which I called rpairs because it iterates over the table in a random order.
-- @param #table t The table
-- @return #string key The index key
-- @return #string value The value at the indexed key
-- @usage
--            for key,value in UTILS.rpairs(mytable) do
--                -- your code here
--            end
function UTILS.rpairs( t )
    -- collect the keys

    local keys = {}
    for k in pairs(t) do keys[#keys+1] = k end

    local random = {}
    local j = #keys
    for i = 1, j do
      local k = math.random( 1, #keys )
      random[i] = keys[k]
      table.remove( keys, k )
    end

    -- return the iterator function
    local i = 0
    return function()
        i = i + 1
        if random[i] then
            return random[i], t[random[i]]
        end
    end
end

-- get a new mark ID for markings
function UTILS.GetMarkID()

  UTILS._MarkID = UTILS._MarkID + 1
  return UTILS._MarkID

end

--- Remove an object (marker, circle, arrow, text, quad, ...) on the F10 map.
-- @param #number MarkID Unique ID of the object.
-- @param #number Delay (Optional) Delay in seconds before the mark is removed.
function UTILS.RemoveMark(MarkID, Delay)
  if Delay and Delay>0 then
    TIMER:New(UTILS.RemoveMark, MarkID):Start(Delay)
  else
    if MarkID then
      trigger.action.removeMark(MarkID)
    end
  end
end


-- Test if a Vec2 is in a radius of another Vec2
function UTILS.IsInRadius( InVec2, Vec2, Radius )

  local InRadius = ( ( InVec2.x - Vec2.x ) ^2 + ( InVec2.y - Vec2.y ) ^2 ) ^ 0.5 <= Radius

  return InRadius
end

-- Test if a Vec3 is in the sphere of another Vec3
function UTILS.IsInSphere( InVec3, Vec3, Radius )

  local InSphere = ( ( InVec3.x - Vec3.x ) ^2 + ( InVec3.y - Vec3.y ) ^2 + ( InVec3.z - Vec3.z ) ^2 ) ^ 0.5 <= Radius

  return InSphere
end

--- Beaufort scale: returns Beaufort number and wind description as a function of wind speed in m/s.
-- @param #number speed Wind speed in m/s.
-- @return #number Beaufort number.
-- @return #string Beauford wind description.
function UTILS.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

--- Split string at separators. C.f. [split-string-in-lua](http://stackoverflow.com/questions/1426954/split-string-in-lua).
-- @param #string str Sting to split.
-- @param #string sep Separator for split.
-- @return #table Split text.
function UTILS.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

--- Get a table of all characters in a string.
-- @param #string str Sting.
-- @return #table Individual characters.
function UTILS.GetCharacters(str)

  local chars={}

  for i=1,#str do
    local c=str:sub(i,i)
    table.insert(chars, c)
  end

  return chars
end

--- Convert time in seconds to hours, minutes and seconds.
-- @param #number seconds Time in seconds, e.g. from timer.getAbsTime() function.
-- @param #boolean short (Optional) If true, use short output, i.e. (HH:)MM:SS without day.
-- @return #string Time in format Hours:Minutes:Seconds+Days (HH:MM:SS+D).
function UTILS.SecondsToClock(seconds, short)

  -- Nil check.
  if seconds==nil then
    return nil
  end

  -- Seconds
  local seconds = tonumber(seconds) or 0

  -- Seconds of this day.
  local _seconds=seconds%(60*60*24)

  if seconds<0 then
    return nil
  else
    local hours = string.format("%02.f", math.floor(_seconds/3600))
    local mins  = string.format("%02.f", math.floor(_seconds/60 - (hours*60)))
    local secs  = string.format("%02.f", math.floor(_seconds - hours*3600 - mins *60))
    local days  = string.format("%d", seconds/(60*60*24))
    local clock=hours..":"..mins..":"..secs.."+"..days
    if short then
      if hours=="00" then
        --clock=mins..":"..secs
        clock=hours..":"..mins..":"..secs
      else
        clock=hours..":"..mins..":"..secs
      end
    end
    return clock
  end
end

--- Seconds of today.
-- @return #number Seconds passed since last midnight.
function UTILS.SecondsOfToday()

    -- Time in seconds.
    local time=timer.getAbsTime()

    -- Short format without days since mission start.
    local clock=UTILS.SecondsToClock(time, true)

    -- Time is now the seconds passed since last midnight.
    return UTILS.ClockToSeconds(clock)
end

--- Cound seconds until next midnight.
-- @return #number Seconds to midnight.
function UTILS.SecondsToMidnight()
  return 24*60*60-UTILS.SecondsOfToday()
end

--- Convert clock time from hours, minutes and seconds to seconds.
-- @param #string clock String of clock time. E.g., "06:12:35" or "5:1:30+1". Format is (H)H:(M)M:((S)S)(+D) H=Hours, M=Minutes, S=Seconds, D=Days.
-- @return #number Seconds. Corresponds to what you cet from timer.getAbsTime() function.
function UTILS.ClockToSeconds(clock)

  -- Nil check.
  if clock==nil then
    return nil
  end

  -- Seconds init.
  local seconds=0

  -- Split additional days.
  local dsplit=UTILS.Split(clock, "+")

  -- Convert days to seconds.
  if #dsplit>1 then
    seconds=seconds+tonumber(dsplit[2])*60*60*24
  end

  -- Split hours, minutes, seconds
  local tsplit=UTILS.Split(dsplit[1], ":")

  -- Get time in seconds
  local i=1
  for _,time in ipairs(tsplit) do
    if i==1 then
      -- Hours
      seconds=seconds+tonumber(time)*60*60
    elseif i==2 then
      -- Minutes
      seconds=seconds+tonumber(time)*60
    elseif i==3 then
      -- Seconds
      seconds=seconds+tonumber(time)
    end
    i=i+1
  end

  return seconds
end

--- Display clock and mission time on screen as a message to all.
-- @param #number duration Duration in seconds how long the time is displayed. Default is 5 seconds.
function UTILS.DisplayMissionTime(duration)
  duration=duration or 5
  local Tnow=timer.getAbsTime()
  local mission_time=Tnow-timer.getTime0()
  local mission_time_minutes=mission_time/60
  local mission_time_seconds=mission_time%60
  local local_time=UTILS.SecondsToClock(Tnow)
  local text=string.format("Time: %s - %02d:%02d", local_time, mission_time_minutes, mission_time_seconds)
  MESSAGE:New(text, duration):ToAll()
end

--- Replace illegal characters [<>|/?*:\\] in a string.
-- @param #string Text Input text.
-- @param #string ReplaceBy Replace illegal characters by this character or string. Default underscore "_".
-- @return #string The input text with illegal chars replaced.
function UTILS.ReplaceIllegalCharacters(Text, ReplaceBy)
  ReplaceBy=ReplaceBy or "_"
  local text=Text:gsub("[<>|/?*:\\]", ReplaceBy)
  return text
end

--- Generate a Gaussian pseudo-random number.
-- @param #number x0 Expectation value of distribution.
-- @param #number sigma (Optional) Standard deviation. Default 10.
-- @param #number xmin (Optional) Lower cut-off value.
-- @param #number xmax (Optional) Upper cut-off value.
-- @param #number imax (Optional) Max number of tries to get a value between xmin and xmax (if specified). Default 100.
-- @return #number Gaussian random number.
function UTILS.RandomGaussian(x0, sigma, xmin, xmax, imax)

  -- Standard deviation. Default 10 if not given.
  sigma=sigma or 10

  -- Max attempts.
  imax=imax or 100

  local r
  local gotit=false
  local i=0
  while not gotit do

    -- Uniform numbers in [0,1). We need two.
    local x1=math.random()
    local x2=math.random()

    -- Transform to Gaussian exp(-(x-x0)°/(2*sigma°).
    r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2) + x0

    i=i+1
    if (r>=xmin and r<=xmax) or i>imax then
      gotit=true
    end
  end

  return r
end

--- Randomize a value by a certain amount.
-- @param #number value The value which should be randomized
-- @param #number fac Randomization factor.
-- @param #number lower (Optional) Lower limit of the returned value.
-- @param #number upper (Optional) Upper limit of the returned value.
-- @return #number Randomized value.
-- @usage UTILS.Randomize(100, 0.1) returns a value between 90 and 110, i.e. a plus/minus ten percent variation.
-- @usage UTILS.Randomize(100, 0.5, nil, 120) returns a value between 50 and 120, i.e. a plus/minus fivty percent variation with upper bound 120.
function UTILS.Randomize(value, fac, lower, upper)
  local min
  if lower then
    min=math.max(value-value*fac, lower)
  else
    min=value-value*fac
  end
  local max
  if upper then
    max=math.min(value+value*fac, upper)
  else
    max=value+value*fac
  end

  local r=math.random(min, max)

  return r
end

--- Calculate the [dot product](https://en.wikipedia.org/wiki/Dot_product) of two vectors. The result is a number.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return #number Scalar product of the two vectors a*b.
function UTILS.VecDot(a, b)
  return a.x*b.x + a.y*b.y + a.z*b.z
end

--- Calculate the [dot product](https://en.wikipedia.org/wiki/Dot_product) of two 2D vectors. The result is a number.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param DCS#Vec2 b Vector in 2D with x, y components.
-- @return #number Scalar product of the two vectors a*b.
function UTILS.Vec2Dot(a, b)
  return a.x*b.x + a.y*b.y
end


--- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 3D vector.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @return #number Norm of the vector.
function UTILS.VecNorm(a)
  return math.sqrt(UTILS.VecDot(a, a))
end

--- Calculate the [euclidean norm](https://en.wikipedia.org/wiki/Euclidean_distance) (length) of a 2D vector.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @return #number Norm of the vector.
function UTILS.Vec2Norm(a)
  return math.sqrt(UTILS.Vec2Dot(a, a))
end

--- Calculate the distance between two 2D vectors.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param DCS#Vec2 b Vector in 2D with x, y components.
-- @return #number Distance between the vectors.
function UTILS.VecDist2D(a, b)

  local d = math.huge

  if (not a) or (not b) then return d end

  local c={x=b.x-a.x, y=b.y-a.y}

  d=math.sqrt(c.x*c.x+c.y*c.y)

  return d
end


--- Calculate the distance between two 3D vectors.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return #number Distance between the vectors.
function UTILS.VecDist3D(a, b)


  local d = math.huge

  if (not a) or (not b) then return d end

  local c={x=b.x-a.x, y=b.y-a.y, z=b.z-a.z}

  d=math.sqrt(UTILS.VecDot(c, c))

  return d
end

--- Calculate the [cross product](https://en.wikipedia.org/wiki/Cross_product) of two 3D vectors. The result is a 3D vector.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return DCS#Vec3 Vector
function UTILS.VecCross(a, b)
  return {x=a.y*b.z - a.z*b.y, y=a.z*b.x - a.x*b.z, z=a.x*b.y - a.y*b.x}
end

--- Calculate the difference between two 3D vectors by substracting the x,y,z components from each other.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return DCS#Vec3 Vector c=a-b with c(i)=a(i)-b(i), i=x,y,z.
function UTILS.VecSubstract(a, b)
  return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z}
end

--- Substract is not a word, don't want to rename the original function because it's been around since forever
function UTILS.VecSubtract(a, b)
  return UTILS.VecSubstract(a, b)
end

--- Calculate the difference between two 2D vectors by substracting the x,y components from each other.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param DCS#Vec2 b Vector in 2D with x, y components.
-- @return DCS#Vec2 Vector c=a-b with c(i)=a(i)-b(i), i=x,y.
function UTILS.Vec2Substract(a, b)
  return {x=a.x-b.x, y=a.y-b.y}
end

--- Substract is not a word, don't want to rename the original function because it's been around since forever
function UTILS.Vec2Subtract(a, b)
  return UTILS.Vec2Substract(a, b)
end

--- Calculate the total vector of two 3D vectors by adding the x,y,z components of each other.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return DCS#Vec3 Vector c=a+b with c(i)=a(i)+b(i), i=x,y,z.
function UTILS.VecAdd(a, b)
  return {x=a.x+b.x, y=a.y+b.y, z=a.z+b.z}
end

--- Calculate the total vector of two 2D vectors by adding the x,y components of each other.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param DCS#Vec2 b Vector in 2D with x, y components.
-- @return DCS#Vec2 Vector c=a+b with c(i)=a(i)+b(i), i=x,y.
function UTILS.Vec2Add(a, b)
  return {x=a.x+b.x, y=a.y+b.y}
end

--- Calculate the angle between two 3D vectors.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return #number Angle alpha between and b in degrees. alpha=acos(a*b)/(|a||b|), (* denotes the dot product).
function UTILS.VecAngle(a, b)

  local cosalpha=UTILS.VecDot(a,b)/(UTILS.VecNorm(a)*UTILS.VecNorm(b))

  local alpha=0
  if cosalpha>=0.9999999999 then  --acos(1) is not defined.
    alpha=0
  elseif cosalpha<=-0.999999999 then --acos(-1) is not defined.
    alpha=math.pi
  else
    alpha=math.acos(cosalpha)
  end

  return math.deg(alpha)
end

--- Calculate "heading" of a 3D vector in the X-Z plane.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @return #number Heading in degrees in [0,360).
function UTILS.VecHdg(a)
  local h=math.deg(math.atan2(a.z, a.x))
  if h<0 then
    h=h+360
  end
  return h
end

--- Calculate "heading" of a 2D vector in the X-Y plane.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @return #number Heading in degrees in [0,360).
function UTILS.Vec2Hdg(a)
  local h=math.deg(math.atan2(a.y, a.x))
  if h<0 then
    h=h+360
  end
  return h
end

--- Calculate the difference between two "heading", i.e. angles in [0,360) deg.
-- @param #number h1 Heading one.
-- @param #number h2 Heading two.
-- @return #number Heading difference in degrees.
function UTILS.HdgDiff(h1, h2)

  -- Angle in rad.
  local alpha= math.rad(tonumber(h1))
  local beta = math.rad(tonumber(h2))

  -- Runway vector.
  local v1={x=math.cos(alpha), y=0, z=math.sin(alpha)}
  local v2={x=math.cos(beta),  y=0, z=math.sin(beta)}

  local delta=UTILS.VecAngle(v1, v2)

  return math.abs(delta)
end

--- Returns the heading from one vec2/vec3 to another vec2/vec3.
-- @param DCS#Vec3 a From Vec2 or Vec3.
-- @param DCS#Vec3 b To Vec2 or Vec3.
-- @return #number Heading in degrees.
function UTILS.HdgTo(a, b)
  local dz=(b.z or b.y) - (a.z or a.y)
  local dx=b.x-a.x
  local heading=math.deg(math.atan2(dz, dx))
  if heading < 0 then
    heading = 360 + heading
  end
  return heading
end


--- Translate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param #number distance The distance to translate.
-- @param #number angle Rotation angle in degrees.
-- @return DCS#Vec3 Vector rotated in the (x,z) plane.
function UTILS.VecTranslate(a, distance, angle)

  local SX = a.x
  local SY = a.z
  local Radians=math.rad(angle or 0)
  local TX=distance*math.cos(Radians)+SX
  local TY=distance*math.sin(Radians)+SY

  return {x=TX, y=a.y, z=TY}
end

--- Translate 2D vector in the 2D (x,z) plane.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param #number distance The distance to translate.
-- @param #number angle Rotation angle in degrees.
-- @return DCS#Vec2 Translated vector.
function UTILS.Vec2Translate(a, distance, angle)

  local SX = a.x
  local SY = a.y
  local Radians=math.rad(angle or 0)
  local TX=distance*math.cos(Radians)+SX
  local TY=distance*math.sin(Radians)+SY

  return {x=TX, y=TY}
end

--- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param #number angle Rotation angle in degrees.
-- @return DCS#Vec3 Vector rotated in the (x,z) plane.
function UTILS.Rotate2D(a, angle)

  local phi=math.rad(angle)

  local x=a.z
  local y=a.x

  local Z=x*math.cos(phi)-y*math.sin(phi)
  local X=x*math.sin(phi)+y*math.cos(phi)
  local Y=a.y

  local A={x=X, y=Y, z=Z}

  return A
end

--- Rotate 2D vector in the 2D (x,z) plane.
-- @param DCS#Vec2 a Vector in 2D with x, y components.
-- @param #number angle Rotation angle in degrees.
-- @return DCS#Vec2 Vector rotated in the (x,y) plane.
function UTILS.Vec2Rotate2D(a, angle)

  local phi=math.rad(angle)

  local x=a.x
  local y=a.y

  local X=x*math.cos(phi)-y*math.sin(phi)
  local Y=x*math.sin(phi)+y*math.cos(phi)

  local A={x=X, y=Y}

  return A
end


--- Converts a TACAN Channel/Mode couple into a frequency in Hz.
-- @param #number TACANChannel The TACAN channel, i.e. the 10 in "10X".
-- @param #string TACANMode The TACAN mode, i.e. the "X" in "10X".
-- @return #number Frequency in Hz or #nil if parameters are invalid.
function UTILS.TACANToFrequency(TACANChannel, TACANMode)

  if type(TACANChannel) ~= "number" then
    return nil -- error in arguments
  end
  if TACANMode ~= "X" and TACANMode ~= "Y" then
    return nil -- error in arguments
  end

-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137.
-- I have no idea what it does but it seems to work
  local A = 1151 -- 'X', channel >= 64
  local B = 64   -- channel >= 64

  if TACANChannel < 64 then
    B = 1
  end

  if TACANMode == 'Y' then
    A = 1025
    if TACANChannel < 64 then
      A = 1088
    end
  else -- 'X'
    if TACANChannel < 64 then
      A = 962
    end
  end

  return (A + TACANChannel - B) * 1000000
end


--- Returns the DCS map/theatre as optained by `env.mission.theatre`.
-- @return #string DCS map name.
function UTILS.GetDCSMap()
  return env.mission.theatre
end

--- Returns the mission date. This is the date the mission **started**.
-- @return #string Mission date in yyyy/mm/dd format.
-- @return #number The year anno domini.
-- @return #number The month.
-- @return #number The day.
function UTILS.GetDCSMissionDate()
  local year=tostring(env.mission.date.Year)
  local month=tostring(env.mission.date.Month)
  local day=tostring(env.mission.date.Day)
  return string.format("%s/%s/%s", year, month, day), tonumber(year), tonumber(month), tonumber(day)
end

--- Returns the day of the mission.
-- @param #number Time (Optional) Abs. time in seconds. Default now, i.e. the value return from timer.getAbsTime().
-- @return #number Day of the mission. Mission starts on day 0.
function UTILS.GetMissionDay(Time)

  Time=Time or timer.getAbsTime()

  local clock=UTILS.SecondsToClock(Time, false)

  local x=tonumber(UTILS.Split(clock, "+")[2])

  return x
end

--- Returns the current day of the year of the mission.
-- @param #number Time (Optional) Abs. time in seconds. Default now, i.e. the value return from timer.getAbsTime().
-- @return #number Current day of year of the mission. For example, January 1st returns 0, January 2nd returns 1 etc.
function UTILS.GetMissionDayOfYear(Time)

  local Date, Year, Month, Day=UTILS.GetDCSMissionDate()

  local d=UTILS.GetMissionDay(Time)

  return UTILS.GetDayOfYear(Year, Month, Day)+d

end

--- Returns the magnetic declination of the map.
-- Returned values for the current maps are:
--
-- * Caucasus +6 (East), year ~ 2011
-- * NTTR +12 (East), year ~ 2011
-- * Normandy -10 (West), year ~ 1944
-- * Persian Gulf +2 (East), year ~ 2011
-- * The Cannel Map -10 (West)
-- * Syria +5 (East)
-- * Mariana Islands +2 (East)
-- * Falklands +12 (East) - note there's a LOT of deviation across the map, as we're closer to the South Pole
-- * Sinai +4.8 (East)
-- * Kola +15 (East) - note there is a lot of deviation across the map (-1° to +24°), as we are close to the North pole
-- * Afghanistan +3 (East) - actually +3.6 (NW) to +2.3 (SE)
-- * Iraq +4.4 (East)
-- * Germany Cold War +0.1 (East) - near Fulda
-- @param #string map (Optional) Map for which the declination is returned. Default is from `env.mission.theatre`.
-- @return #number Declination in degrees.
function UTILS.GetMagneticDeclination(map)

  -- Map.
  map=map or UTILS.GetDCSMap()

  local declination=0
  if map==DCSMAP.Caucasus then
    declination=6
  elseif map==DCSMAP.NTTR then
    declination=12
  elseif map==DCSMAP.Normandy then
    declination=-10
  elseif map==DCSMAP.PersianGulf then
    declination=2
  elseif map==DCSMAP.TheChannel then
    declination=-10
  elseif map==DCSMAP.Syria then
    declination=5
  elseif map==DCSMAP.MarianaIslands then
    declination=2
  elseif map==DCSMAP.Falklands then
    declination=12
  elseif map==DCSMAP.Sinai then
    declination=4.8
  elseif map==DCSMAP.Kola then
    declination=15
  elseif map==DCSMAP.Afghanistan then
    declination=3
  elseif map==DCSMAP.Iraq then
    declination=4.4
  elseif map==DCSMAP.GermanyCW then
    declination=0.1
  else
    declination=0
  end

  return declination
end

--- Checks if a file exists or not. This requires **io** to be desanitized.
-- @param #string file File that should be checked.
-- @return #boolean True if the file exists, false if the file does not exist or nil if the io module is not available and the check could not be performed.
function UTILS.FileExists(file)
  if io then
    local f=io.open(file, "r")
    if f~=nil then
      io.close(f)
      return true
    else
      return false
    end
  else
    return nil
  end
end

--- Checks the current memory usage collectgarbage("count"). Info is printed to the DCS log file. Time stamp is the current mission runtime.
-- @param #boolean output If true, print to DCS log file.
-- @return #number Memory usage in kByte.
function UTILS.CheckMemory(output)
  local time=timer.getTime()
  local clock=UTILS.SecondsToClock(time)
  local mem=collectgarbage("count")
  if output then
    env.info(string.format("T=%s  Memory usage %d kByte = %.2f MByte", clock, mem, mem/1024))
  end
  return mem
end


--- Get the coalition name from its numerical ID, e.g. coalition.side.RED.
-- @param #number Coalition The coalition ID.
-- @return #string The coalition name, i.e. "Neutral", "Red" or "Blue" (or "Unknown").
function UTILS.GetCoalitionName(Coalition)

  if Coalition then
    if Coalition==coalition.side.BLUE then
      return "Blue"
    elseif Coalition==coalition.side.RED then
      return "Red"
    elseif Coalition==coalition.side.NEUTRAL then
      return "Neutral"
    else
      return "Unknown"
    end
  else
    return "Unknown"
  end

end

--- Get the enemy coalition for a given coalition.
-- @param #number Coalition The coalition ID.
-- @param #boolean Neutral Include neutral as enemy.
-- @return #table Enemy coalition table.
function UTILS.GetCoalitionEnemy(Coalition, Neutral)

  local Coalitions={}
  if Coalition then
    if Coalition==coalition.side.RED then
      Coalitions={coalition.side.BLUE}
    elseif Coalition==coalition.side.BLUE then
      Coalitions={coalition.side.RED}
    elseif Coalition==coalition.side.NEUTRAL then
      Coalitions={coalition.side.RED, coalition.side.BLUE}
    end
  end

  if Neutral then
    table.insert(Coalitions, coalition.side.NEUTRAL)
  end

  return Coalitions
end

--- Get the modulation name from its numerical value.
-- @param #number Modulation The modulation enumerator number. Can be either 0 or 1.
-- @return #string The modulation name, i.e. "AM"=0 or "FM"=1. Anything else will return "Unknown".
function UTILS.GetModulationName(Modulation)

  if Modulation then
    if Modulation==0  then
      return "AM"
    elseif Modulation==1  then
      return "FM"
    else
      return "Unknown"
    end
  else
    return "Unknown"
  end

end

--- Get the NATO reporting name of a unit type name
-- @param #number Typename The type name.
-- @return #string The Reporting name or "Bogey".
function UTILS.GetReportingName(Typename)

  local typename = string.lower(Typename)
  
  -- special cases - Shark and Manstay have "A-50" in the name
  if string.find(typename,"ka-50",1,true) then
    return "Shark"
  elseif string.find(typename,"a-50",1,true) then
    return "Mainstay"
  end

  for name, value in pairs(ENUMS.ReportingName.NATO) do
    local svalue = string.lower(value)
    if string.find(typename,svalue,1,true) then
      return name
    end
  end

  return "Bogey"
end

--- Get the callsign name from its enumerator value
-- @param #number Callsign The enumerator callsign.
-- @return #string The callsign name or "Ghostrider".
function UTILS.GetCallsignName(Callsign)

  for name, value in pairs(CALLSIGN.Aircraft) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.AWACS) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.JTAC) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.Tanker) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.B1B) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.B52) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.F15E) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.F16) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.F18) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.FARP) do
    if value==Callsign then
      return name
    end
  end

  for name, value in pairs(CALLSIGN.TransportAircraft) do
    if value==Callsign then
      return name
    end
  end
  
  for name, value in pairs(CALLSIGN.AH64) do
    if value==Callsign then
      return name
    end
  end
  
  for name, value in pairs(CALLSIGN.Kiowa) do
    if value==Callsign then
      return name
    end
  end
  
  return "Ghostrider"
end

--- Get the time difference between GMT and local time.
-- @return #number Local time difference in hours compared to GMT. E.g. Dubai is GMT+4 ==> +4 is returned.
function UTILS.GMTToLocalTimeDifference()

  local theatre=UTILS.GetDCSMap()

  if theatre==DCSMAP.Caucasus then
    return 4   -- Caucasus UTC+4 hours
  elseif theatre==DCSMAP.PersianGulf then
    return 4   -- Abu Dhabi UTC+4 hours
  elseif theatre==DCSMAP.NTTR then
    return -8  -- Las Vegas UTC-8 hours
  elseif theatre==DCSMAP.Normandy then
    return 0   -- Calais UTC+1 hour
  elseif theatre==DCSMAP.TheChannel then
    return 2   -- This map currently needs +2
  elseif theatre==DCSMAP.Syria then
    return 3   -- Damascus is UTC+3 hours
  elseif theatre==DCSMAP.MarianaIslands then
    return 10  -- Guam is UTC+10 hours.
  elseif theatre==DCSMAP.Falklands then
    return -3  -- Fireland is UTC-3 hours.
  elseif theatre==DCSMAP.Sinai then
    return 2   -- Currently map is +2 but should be +3 (DCS bug?)
  elseif theatre==DCSMAP.Kola then
    return 3   -- Currently map is +2 but should be +3 (DCS bug?)
  elseif theatre==DCSMAP.Afghanistan then
    return 4.5   -- UTC +4:30
  elseif theatre==DCSMAP.Iraq then
    return 3.0   -- UTC +3
  elseif theatre==DCSMAP.GermanyCW then
    return 1.0   -- UTC +1 Central European Time (not summer time)  
  else
    BASE:E(string.format("ERROR: Unknown Map %s in UTILS.GMTToLocal function. Returning 0", tostring(theatre)))
    return 0
  end

end


--- Get the day of the year. Counting starts on 1st of January.
-- @param #number Year The year.
-- @param #number Month The month.
-- @param #number Day The day.
-- @return #number The day of the year.
function UTILS.GetDayOfYear(Year, Month, Day)

  local floor = math.floor

   local n1 = floor(275 * Month / 9)
   local n2 = floor((Month + 9) / 12)
   local n3 = (1 + floor((Year - 4 * floor(Year / 4) + 2) / 3))

   return n1 - (n2 * n3) + Day - 30
end

--- Get sunrise or sun set of a specific day of the year at a specific location.
-- @param #number DayOfYear The day of the year.
-- @param #number Latitude Latitude.
-- @param #number Longitude Longitude.
-- @param #boolean Rising If true, calc sun rise, or sun set otherwise.
-- @param #number Tlocal Local time offset in hours. E.g. +4 for a location which has GMT+4.
-- @return #number Sun rise/set in seconds of the day.
function UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, Rising, Tlocal)

  -- Defaults
  local zenith=90.83
  local latitude=Latitude
  local longitude=Longitude
  local rising=Rising
  local n=DayOfYear
  Tlocal=Tlocal or 0


  -- Short cuts.
  local rad = math.rad
  local deg = math.deg
  local floor = math.floor
  local frac = function(n) return n - floor(n) end
  local cos = function(d) return math.cos(rad(d)) end
  local acos = function(d) return deg(math.acos(d)) end
  local sin = function(d) return math.sin(rad(d)) end
  local asin = function(d) return deg(math.asin(d)) end
  local tan = function(d) return math.tan(rad(d)) end
  local atan = function(d) return deg(math.atan(d)) end

  local function fit_into_range(val, min, max)
     local range = max - min
     local count
     if val < min then
        count = floor((min - val) / range) + 1
        return val + count * range
     elseif val >= max then
        count = floor((val - max) / range) + 1
        return val - count * range
     else
        return val
     end
  end

   -- Convert the longitude to hour value and calculate an approximate time
   local lng_hour = longitude / 15

   local t
   if rising then -- Rising time is desired
      t = n + ((6 - lng_hour) / 24)
   else -- Setting time is desired
      t = n + ((18 - lng_hour) / 24)
   end

   -- Calculate the Sun's mean anomaly
   local M = (0.9856 * t) - 3.289

   -- Calculate the Sun's true longitude
   local L = fit_into_range(M + (1.916 * sin(M)) + (0.020 * sin(2 * M)) + 282.634, 0, 360)

   -- Calculate the Sun's right ascension
   local RA = fit_into_range(atan(0.91764 * tan(L)), 0, 360)

   -- Right ascension value needs to be in the same quadrant as L
   local Lquadrant  = floor(L / 90) * 90
   local RAquadrant = floor(RA / 90) * 90
   RA = RA + Lquadrant - RAquadrant

   -- Right ascension value needs to be converted into hours
   RA = RA / 15

   -- Calculate the Sun's declination
   local sinDec = 0.39782 * sin(L)
   local cosDec = cos(asin(sinDec))

   -- Calculate the Sun's local hour angle
   local cosH = (cos(zenith) - (sinDec * sin(latitude))) / (cosDec * cos(latitude))

   if rising and cosH > 1 then
      return "N/R" -- The sun never rises on this location on the specified date
   elseif cosH < -1 then
      return "N/S" -- The sun never sets on this location on the specified date
   end

   -- Finish calculating H and convert into hours
   local H
   if rising then
      H = 360 - acos(cosH)
   else
      H = acos(cosH)
   end
   H = H / 15

   -- Calculate local mean time of rising/setting
   local T = H + RA - (0.06571 * t) - 6.622

   -- Adjust back to UTC
   local UT = fit_into_range(T - lng_hour +Tlocal, 0, 24)

   return floor(UT)*60*60+frac(UT)*60*60--+Tlocal*60*60
 end

--- Get sun rise of a specific day of the year at a specific location.
-- @param #number Day Day of the year.
-- @param #number Month Month of the year.
-- @param #number Year Year.
-- @param #number Latitude Latitude.
-- @param #number Longitude Longitude.
-- @param #boolean Rising If true, calc sun rise, or sun set otherwise.
-- @param #number Tlocal Local time offset in hours. E.g. +4 for a location which has GMT+4. Default 0.
-- @return #number Sun rise in seconds of the day.
function UTILS.GetSunrise(Day, Month, Year, Latitude, Longitude, Tlocal)

  local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day)

  return UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tlocal)
end

--- Get sun set of a specific day of the year at a specific location.
-- @param #number Day Day of the year.
-- @param #number Month Month of the year.
-- @param #number Year Year.
-- @param #number Latitude Latitude.
-- @param #number Longitude Longitude.
-- @param #boolean Rising If true, calc sun rise, or sun set otherwise.
-- @param #number Tlocal Local time offset in hours. E.g. +4 for a location which has GMT+4. Default 0.
-- @return #number Sun rise in seconds of the day.
function UTILS.GetSunset(Day, Month, Year, Latitude, Longitude, Tlocal)

  local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day)

  return UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tlocal)
end

--- Get OS time. Needs os to be desanitized!
-- @return #number Os time in seconds.
function UTILS.GetOSTime()
  if os then
    local ts = 0
    local t = os.date("*t")
    local s = t.sec
    local m = t.min * 60
    local h = t.hour * 3600
    ts = s+m+h
    return ts
  else
    return nil
  end
end

--- Shuffle a table accoring to Fisher Yeates algorithm
--@param #table t Table to be shuffled.
--@return #table Shuffled table.
function UTILS.ShuffleTable(t)
  if t == nil or type(t) ~= "table" then
    BASE:I("Error in ShuffleTable: Missing or wrong type of Argument")
    return
  end
  math.random()
  math.random()
  math.random()
  local TempTable = {}
  for i = 1, #t do
    local r = math.random(1,#t)
    TempTable[i] = t[r]
    table.remove(t,r)
  end
  return TempTable
end

--- Get a random element of a table.
--@param #table t Table.
--@param #boolean replace If `true`, the drawn element is replaced, i.e. not deleted.
--@return #number Table element.
function UTILS.GetRandomTableElement(t, replace)

  if t == nil or type(t) ~= "table" then
    BASE:I("Error in ShuffleTable: Missing or wrong type of Argument")
    return
  end

  math.random()
  math.random()
  math.random()

  local r=math.random(#t)

  local element=t[r]

  if not replace then
    table.remove(t, r)
  end

  return element
end

--- (Helicopter) Check if one loading door is open.
--@param #string unit_name Unit name to be checked
--@return #boolean Outcome - true if a (loading door) is open, false if not, nil if none exists.
function UTILS.IsLoadingDoorOpen( unit_name )

  local unit = Unit.getByName(unit_name)

  if unit ~= nil then
      local type_name = unit:getTypeName()
      BASE:T("TypeName = ".. type_name)

      if type_name == "Mi-8MT" and (unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1 or unit:getDrawArgumentValue(250) < 0) then
          BASE:T(unit_name .. " Cargo doors are open or cargo door not present")
          return true
      end

      if type_name == "Mi-24P" and (unit:getDrawArgumentValue(38) == 1 or unit:getDrawArgumentValue(86) == 1) then
          BASE:T(unit_name .. " a side door is open")
          return true
      end

      if type_name == "UH-1H" and (unit:getDrawArgumentValue(43) == 1 or unit:getDrawArgumentValue(44) == 1) then
          BASE:T(unit_name .. " a side door is open ")
          return true
      end

      if string.find(type_name, "SA342" ) and (unit:getDrawArgumentValue(34) == 1) then
          BASE:T(unit_name .. " front door(s) are open or doors removed")
          return true
      end

      if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1215) == 1 and unit:getDrawArgumentValue(1216) == 1) then
          BASE:T(unit_name .. " rear doors are open")
          return true
      end

      if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1220) == 1 or unit:getDrawArgumentValue(1221) == 1) then
          BASE:T(unit_name .. " para doors are open")
          return true
      end

      if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1217) == 1) then
          BASE:T(unit_name .. " side door is open")
          return true
      end

      if type_name == "Bell-47" then -- bell aint got no doors so always ready to load injured soldiers
          BASE:T(unit_name .. " door is open")
          return true
      end

      if type_name == "UH-60L" and (unit:getDrawArgumentValue(401) == 1 or unit:getDrawArgumentValue(402) == 1) then
          BASE:T(unit_name .. " cargo door is open")
          return true
      end

      if type_name ==  "UH-60L" and (unit:getDrawArgumentValue(38) > 0 or unit:getDrawArgumentValue(400) == 1 ) then
          BASE:T(unit_name .. " front door(s) are open")
          return true
      end

      if type_name == "UH-60L_DAP" and (unit:getDrawArgumentValue(401) == 1 or unit:getDrawArgumentValue(402) == 1) then
          BASE:T(unit_name .. " cargo door is open")
          return true
      end

      if type_name ==  "UH-60L_DAP" and (unit:getDrawArgumentValue(38) > 0 or unit:getDrawArgumentValue(400) == 1 ) then
          BASE:T(unit_name .. " front door(s) are open")
          return true
      end

      if type_name == "AH-64D_BLK_II" then
         BASE:T(unit_name .. " front door(s) are open")
         return true -- no doors on this one ;)
      end

      if type_name == "Bronco-OV-10A" then
         BASE:T(unit_name .. " front door(s) are open")
         return true -- no doors on this one ;)
      end

      if type_name == "MH-60R" and (unit:getDrawArgumentValue(403) > 0 or unit:getDrawArgumentValue(403) == -1) then
        BASE:T(unit_name .. " cargo door is open")
        return true
      end

      if type_name == "OH58D" then
        BASE:T(unit_name .. " front door(s) are open")
        return true -- no doors on this one ;)
      end

      if type_name == "CH-47Fbl1" and (unit:getDrawArgumentValue(86) > 0.5) then
        BASE:T(unit_name .. " rear cargo door is open")
        return true
      end
      
      -- ground
      local UnitDescriptor = unit:getDesc()
      local IsGroundResult = (UnitDescriptor.category == Unit.Category.GROUND_UNIT)
      
      return IsGroundResult

  end -- nil

  return nil
end

--- Function to generate valid FM frequencies in mHz for radio beacons (FM).
-- @return #table Table of frequencies.
function UTILS.GenerateFMFrequencies()
    local FreeFMFrequencies = {}
    for _first = 3, 7 do
        for _second = 0, 5 do
            for _third = 0, 9 do
                local _frequency = ((100 * _first) + (10 * _second) + _third) * 100000 --extra 0 because we didnt bother with 4th digit
                table.insert(FreeFMFrequencies, _frequency)
            end
        end
    end
    return FreeFMFrequencies
end

--- Function to generate valid VHF frequencies in kHz for radio beacons (FM).
-- @return #table VHFrequencies
function UTILS.GenerateVHFrequencies()
  
  -- known and sorted map-wise NDBs in kHz
  local _skipFrequencies = {
  214,243,264,273,274,288,291.5,295,297.5,
  300.5,304,305,307,309.5,310,311,312,312.5,316,317,
  320,323,324,325,326,328,329,330,332,335,336,337,
  340,342,343,346,348,351,352,353,358,
  360,363,364,365,368,372.5,373,374,
  380,381,384,385,387,389,391,395,396,399,
  403,404,410,412,414,418,420,423,
  430,432,435,440,445,
  450,455,462,470,485,490,
  507,515,520,525,528,540,550,560,563,570,577,580,595,
  602,625,641,662,670,680,682,690,
  705,720,722,730,735,740,745,750,770,795,
  822,830,862,866,
  905,907,920,935,942,950,995,
  1000,1025,1030,1050,1065,1116,1175,1182,1210,1215
  }

  local FreeVHFFrequencies = {}

    -- first range
  local _start = 200000
  while _start < 400000 do

      -- skip existing NDB frequencies#
      local _found = false
      for _, value in pairs(_skipFrequencies) do
          if value * 1000 == _start then
              _found = true
              break
          end
      end
      if _found == false then
          table.insert(FreeVHFFrequencies, _start)
      end
       _start = _start + 10000
  end

   -- second range
  _start = 400000
  while _start < 850000 do
       -- skip existing NDB frequencies
      local _found = false
      for _, value in pairs(_skipFrequencies) do
          if value * 1000 == _start then
              _found = true
              break
          end
      end
      if _found == false then
          table.insert(FreeVHFFrequencies, _start)
      end
      _start = _start + 10000
  end

  -- third range
  _start = 850000
  while _start <= 999000 do -- adjusted for Gazelle
      -- skip existing NDB frequencies
      local _found = false
      for _, value in pairs(_skipFrequencies) do
          if value * 1000 == _start then
              _found = true
              break
          end
      end
      if _found == false then
          table.insert(FreeVHFFrequencies, _start)
      end
       _start = _start + 50000
  end

  return FreeVHFFrequencies
end

--- Function to generate valid UHF Frequencies in mHz (AM). Can be between 220 and 399 mHz. 243 is auto-excluded.
-- @param Start (Optional) Avoid frequencies between Start and End in mHz, e.g. 244
-- @param End (Optional) Avoid frequencies between Start and End in mHz, e.g. 320
-- @return #table UHF Frequencies
function UTILS.GenerateUHFrequencies(Start,End)

    local FreeUHFFrequencies = {}
    local _start = 220000000

    if not Start then
      while _start < 399000000 do
      if _start ~= 243000000 then
        table.insert(FreeUHFFrequencies, _start)
      end
          _start = _start + 500000
      end
    else
      local myend = End*1000000 or 399000000
      local mystart = Start*1000000 or 220000000

      while _start < 399000000 do
      if _start ~= 243000000 and (_start < mystart or _start > myend) then
        print(_start)
        table.insert(FreeUHFFrequencies, _start)
      end
          _start = _start + 500000
      end

    end


    return FreeUHFFrequencies
end

--- Function to generate valid laser codes for JTAC.
-- @return #table Laser Codes.
function UTILS.GenerateLaserCodes()
    local jtacGeneratedLaserCodes = {}

    -- helper function
    local function ContainsDigit(_number, _numberToFind)
      local _thisNumber = _number
      local _thisDigit = 0
      while _thisNumber ~= 0 do
          _thisDigit = _thisNumber % 10
          _thisNumber = math.floor(_thisNumber / 10)
          if _thisDigit == _numberToFind then
              return true
          end
      end
      return false
    end

    -- generate list of laser codes
    local _code = 1111
    local _count = 1
    while _code < 1777 and _count < 30 do
        while true do
           _code = _code + 1
            if not ContainsDigit(_code, 8)
                    and not ContainsDigit(_code, 9)
                    and not ContainsDigit(_code, 0) then
                table.insert(jtacGeneratedLaserCodes, _code)
                break
            end
        end
        _count = _count + 1
    end
    return jtacGeneratedLaserCodes
end

--- Ensure the passed object is a table.
-- @param #table Object The object that should be a table.
-- @param #boolean ReturnNil If `true`, return `#nil` if `Object` is nil. Otherwise an empty table `{}` is returned.
-- @return #table The object that now certainly *is* a table.
function UTILS.EnsureTable(Object, ReturnNil)

  if Object then
    if type(Object)~="table" then
      Object={Object}
    end
  else
    if ReturnNil then
      return nil
    else
      Object={}
    end

  end

  return Object
end

--- Function to save an object to a file
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file. Existing file will be overwritten.
-- @param #string Data The data structure to save. This will be e.g. a string of text lines with an \\n at the end of each line.
-- @return #boolean outcome True if saving is possible, else false.
function UTILS.SaveToFile(Path,Filename,Data)
  -- Thanks to @FunkyFranky
  -- Check io module is available.
  if not io then
    BASE:E("ERROR: io not desanitized. Can't save current file.")
    return false
  end

  -- Check default path.
  if Path==nil and not lfs then
    BASE:E("WARNING: lfs not desanitized. File will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
  end

  -- Set path or default.
  local path = nil
  if lfs then
    path=Path or lfs.writedir()
  end

  -- Set file name.
  local filename=Filename
  if path~=nil then
    filename=path.."\\"..filename
  end

  -- write
  local f = assert(io.open(filename, "wb"))
  f:write(Data)
  f:close()
  return true
end

--- Function to load an object from a file.
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return #boolean outcome True if reading is possible and successful, else false.
-- @return #table data The data read from the filesystem (table of lines of text). Each line is one single #string!
function UTILS.LoadFromFile(Path,Filename)
  -- Thanks to @FunkyFranky
  -- Check io module is available.
  if not io then
    BASE:E("ERROR: io not desanitized. Can't save current state.")
    return false
  end

  -- Check default path.
  if Path==nil and not lfs then
    BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
  end

  -- Set path or default.
  local path = nil
  if lfs then
    path=Path or lfs.writedir()
  end

  -- Set file name.
  local filename=Filename
  if path~=nil then
    filename=path.."\\"..filename
  end

  -- Check if file exists.
  local exists=UTILS.CheckFileExists(Path,Filename)
  if not exists then
    BASE:I(string.format("ERROR: File %s does not exist!",filename))
    return false
  end

  -- read
  local file=assert(io.open(filename, "rb"))
  local loadeddata = {}
  for line in file:lines() do
      loadeddata[#loadeddata+1] = line
  end
  file:close()
  return true, loadeddata
end

--- Function to check if a file exists.
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return #boolean outcome True if reading is possible, else false.
function UTILS.CheckFileExists(Path,Filename)
  -- Thanks to @FunkyFranky
  -- Function that check if a file exists.
  local function _fileexists(name)
     local f=io.open(name,"r")
     if f~=nil then
      io.close(f)
      return true
    else
      return false
    end
  end

  -- Check io module is available.
  if not io then
    BASE:E("ERROR: io not desanitized.")
    return false
  end

  -- Check default path.
  if Path==nil and not lfs then
    BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
  end

  -- Set path or default.
  local path = nil
  if lfs then
    path=Path or lfs.writedir()
  end

  -- Set file name.
  local filename=Filename
  if path~=nil then
    filename=path.."\\"..filename
  end

  -- Check if file exists.
  local exists=_fileexists(filename)
  if not exists then
    BASE:E(string.format("ERROR: File %s does not exist!",filename))
    return false
  else
    return true
  end
end

--- Function to obtain a table of typenames from the group given with the number of units of the same type in the group.
-- @param Wrapper.Group#GROUP Group The group to list
-- @return #table Table of typnames and typename counts, e.g. `{["KAMAZ Truck"]=3,["ATZ-5"]=1}`
function UTILS.GetCountPerTypeName(Group)
  local units = Group:GetUnits()
  local TypeNameTable = {}
  for _,_unt in pairs (units) do
    local unit = _unt -- Wrapper.Unit#UNIT
    local typen = unit:GetTypeName()
    if not TypeNameTable[typen] then
      TypeNameTable[typen] = 1
    else
      TypeNameTable[typen] = TypeNameTable[typen] + 1
    end
  end
  return TypeNameTable
end

--- Function to save the state of a list of groups found by name
-- @param #table List Table of strings with groupnames
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @param #boolean Structured Append the data with a list of typenames in the group plus their count.
-- @return #boolean outcome True if saving is successful, else false.
-- @usage
-- We will go through the list and find the corresponding group and save the current group size (0 when dead).
-- These groups are supposed to be put on the map in the ME and have *not* moved (e.g. stationary SAM sites).
-- Position is still saved for your usage.
-- The idea is to reduce the number of units when reloading the data again to restart the saved mission.
-- The data will be a simple comma separated list of groupname and size, with one header line.
function UTILS.SaveStationaryListOfGroups(List,Path,Filename,Structured)
  local filename = Filename or "StateListofGroups"
  local data = "--Save Stationary List of Groups: "..Filename .."\n"
  for _,_group in pairs (List) do
    local group = GROUP:FindByName(_group) -- Wrapper.Group#GROUP
    if group and group:IsAlive() then
      local units = group:CountAliveUnits()
      local position = group:GetVec3()
      if Structured then
        local structure = UTILS.GetCountPerTypeName(group)
        local strucdata =  ""
        for typen,anzahl in pairs (structure) do
          strucdata = strucdata .. typen .. "=="..anzahl..";"
        end
        data = string.format("%s%s,%d,%d,%d,%d,%s\n",data,_group,units,position.x,position.y,position.z,strucdata)
      else
        data = string.format("%s%s,%d,%d,%d,%d\n",data,_group,units,position.x,position.y,position.z)
      end
    else
      data = string.format("%s%s,0,0,0,0\n",data,_group)
    end
  end
  -- save the data
  local outcome = UTILS.SaveToFile(Path,Filename,data)
  return outcome
end

--- Function to save the state of a set of Wrapper.Group#GROUP objects.
-- @param Core.Set#SET_BASE Set of objects to save
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @param #boolean Structured Append the data with a list of typenames in the group plus their count.
-- @return #boolean outcome True if saving is successful, else false.
-- @usage
-- We will go through the set and find the corresponding group and save the current group size and current position.
-- The idea is to respawn the groups **spawned during an earlier run of the mission** at the given location and reduce
-- the number of units in the group when reloading the data again to restart the saved mission. Note that *dead* groups
-- cannot be covered with this.
-- **Note** Do NOT use dashes or hashes in group template names (-,#)!
-- The data will be a simple comma separated list of groupname and size, with one header line.
-- The current task/waypoint/etc cannot be restored.
function UTILS.SaveSetOfGroups(Set,Path,Filename,Structured)
  local filename = Filename or "SetOfGroups"
  local data = "--Save SET of groups: "..Filename .."\n"
  local List = Set:GetSetObjects()
  for _,_group in pairs (List) do
    local group = _group -- Wrapper.Group#GROUP
    if group and group:IsAlive() then
      local name = group:GetName()
      local template = string.gsub(name,"-(.+)$","")
      if string.find(name,"AID") then
        template = string.gsub(name,"(.AID.%d+$","")
      end
      if string.find(template,"#") then
       template = string.gsub(name,"#(%d+)$","")
      end
      local units = group:CountAliveUnits()
      local position = group:GetVec3()
      if Structured then
        local structure = UTILS.GetCountPerTypeName(group)
        local strucdata =  ""
        for typen,anzahl in pairs (structure) do
          strucdata = strucdata .. typen .. "=="..anzahl..";"
        end
        data = string.format("%s%s,%s,%d,%d,%d,%d,%s\n",data,name,template,units,position.x,position.y,position.z,strucdata)
      else
        data = string.format("%s%s,%s,%d,%d,%d,%d\n",data,name,template,units,position.x,position.y,position.z)
      end
    end
  end
  -- save the data
  local outcome = UTILS.SaveToFile(Path,Filename,data)
  return outcome
end

--- Function to save the state of a set of Wrapper.Static#STATIC objects.
-- @param Core.Set#SET_BASE Set of objects to save
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return #boolean outcome True if saving is successful, else false.
-- @usage
-- We will go through the set and find the corresponding static and save the current name and postion when alive.
-- The data will be a simple comma separated list of name and state etc, with one header line.
function UTILS.SaveSetOfStatics(Set,Path,Filename)
  local filename = Filename or "SetOfStatics"
  local data = "--Save SET of statics: "..Filename .."\n"
  local List = Set:GetSetObjects()
  for _,_group in pairs (List) do
    local group = _group -- Wrapper.Static#STATIC
    if group and group:IsAlive() then
      local name = group:GetName()
      local position = group:GetVec3()
      data = string.format("%s%s,%d,%d,%d\n",data,name,position.x,position.y,position.z)
    end
  end
  -- save the data
  local outcome = UTILS.SaveToFile(Path,Filename,data)
  return outcome
end

--- Function to save the state of a list of statics found by name
-- @param #table List Table of strings with statics names
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return #boolean outcome True if saving is successful, else false.
-- @usage
-- We will go through the list and find the corresponding static and save the current alive state as 1 (0 when dead).
-- Position is saved for your usage. **Note** this works on UNIT-name level.
-- The idea is to reduce the number of units when reloading the data again to restart the saved mission.
-- The data will be a simple comma separated list of name and state etc, with one header line.
function UTILS.SaveStationaryListOfStatics(List,Path,Filename)
  local filename = Filename or "StateListofStatics"
  local data = "--Save Stationary List of Statics: "..Filename .."\n"
  for _,_group in pairs (List) do
    local group = STATIC:FindByName(_group,false) -- Wrapper.Static#STATIC
    if group and group:IsAlive() then
      local position = group:GetVec3()
      data = string.format("%s%s,1,%d,%d,%d\n",data,_group,position.x,position.y,position.z)
    else
      data = string.format("%s%s,0,0,0,0\n",data,_group)
    end
  end
  -- save the data
  local outcome = UTILS.SaveToFile(Path,Filename,data)
  return outcome
end

--- Load back a stationary list of groups from file.
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @param #boolean Reduce If false, existing loaded groups will not be reduced to fit the saved number.
-- @param #boolean Structured (Optional, needs Reduce = true) If true, and the data has been saved as structure before, remove the correct unit types as per the saved list.
-- @param #boolean Cinematic (Optional, needs Structured = true) If true, place a fire/smoke effect on the dead static position.
-- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke.
-- @param #number Density (Optional for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5.
-- @return #table Table of data objects (tables) containing groupname, coordinate and group object. Returns nil when file cannot be read.
-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )`
function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce,Structured,Cinematic,Effect,Density)

  local fires = {}

  local function Smokers(name,coord,effect,density)
    local eff = math.random(8)
    if type(effect) == "number" then eff = effect end
    coord:BigSmokeAndFire(eff,density,name)
    table.insert(fires,name)
  end

  local function Cruncher(group,typename,anzahl)
    local units = group:GetUnits()
    local reduced = 0
    for _,_unit in pairs (units) do
      local typo = _unit:GetTypeName()
      if typename == typo then
        if Cinematic then
          local coordinate = _unit:GetCoordinate()
          local name = _unit:GetName()
          Smokers(name,coordinate,Effect,Density)
        end
        _unit:Destroy(false)
        reduced = reduced + 1
        if reduced == anzahl then break end
      end
    end
  end

  local reduce = true
  if Reduce == false then reduce = false end
  local filename = Filename or "StateListofGroups"
  local datatable = {}
  if UTILS.CheckFileExists(Path,filename) then
    local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename)
    -- remove header
    table.remove(loadeddata, 1)
    for _id,_entry in pairs (loadeddata) do
      local dataset = UTILS.Split(_entry,",")
      -- groupname,units,position.x,position.y,position.z
      local groupname = dataset[1]
      local size = tonumber(dataset[2])
      local posx = tonumber(dataset[3])
      local posy = tonumber(dataset[4])
      local posz = tonumber(dataset[5])
      local structure = dataset[6]
      --BASE:I({structure})
      local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz})
      local data = { groupname=groupname, size=size, coordinate=coordinate, group=GROUP:FindByName(groupname) }
      if reduce then
        local actualgroup = GROUP:FindByName(groupname)
        if actualgroup and actualgroup:IsAlive() and actualgroup:CountAliveUnits() > size then
          if Structured and structure then
            --BASE:I("Reducing group structure!")
            local loadedstructure = {}
            local strcset = UTILS.Split(structure,";")
            for _,_data in pairs(strcset) do
              local datasplit = UTILS.Split(_data,"==")
              loadedstructure[datasplit[1]] = tonumber(datasplit[2])
            end
            --BASE:I({loadedstructure})
            local originalstructure = UTILS.GetCountPerTypeName(actualgroup)
            --BASE:I({originalstructure})
            for _name,_number in pairs(originalstructure) do
              local loadednumber = 0
              if loadedstructure[_name] then
                loadednumber = loadedstructure[_name]
              end
              local reduce = false
              if loadednumber < _number then reduce = true end

              --BASE:I(string.format("Looking at: %s | Original number: %d | Loaded number: %d | Reduce: %s",_name,_number,loadednumber,tostring(reduce)))

              if reduce then
                Cruncher(actualgroup,_name,_number-loadednumber)
              end

            end
          else
            local reduction = actualgroup:CountAliveUnits() - size
            --BASE:I("Reducing groupsize by ".. reduction .. " units!")
            -- reduce existing group
            local units = actualgroup:GetUnits()
            local units2 = UTILS.ShuffleTable(units) -- randomize table
            for i=1,reduction do
              units2[i]:Destroy(false)
            end
          end
        end
      end
      table.insert(datatable,data)
    end
  else
    return nil
  end
  return datatable,fires
end

--- Load back a SET of groups from file.
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @param #boolean Spawn If set to false, do not re-spawn the groups loaded in location and reduce to size.
-- @param #boolean Structured (Optional, needs Spawn=true)If true, and the data has been saved as structure before, remove the correct unit types as per the saved list.
-- @param #boolean Cinematic (Optional, needs Structured=true) If true, place a fire/smoke effect on the dead static position.
-- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke.
-- @param #number Density (Optional for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5.
-- @return Core.Set#SET_GROUP Set of GROUP objects.
-- Returns nil when file cannot be read. Returns a table of data entries if Spawn is false: `{ groupname=groupname, size=size, coordinate=coordinate, template=template }`
-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )`
function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,Density)

  local fires = {}
  local usedtemplates = {}
  local spawn = true
  if Spawn == false then spawn = false end
  local filename = Filename or "SetOfGroups"
  local setdata = SET_GROUP:New()
  local datatable = {}

  local function Smokers(name,coord,effect,density)
    local eff = math.random(8)
    if type(effect) == "number" then eff = effect end
    coord:BigSmokeAndFire(eff,density,name)
    table.insert(fires,name)
  end

  local function Cruncher(group,typename,anzahl)
    local units = group:GetUnits()
    local reduced = 0
    for _,_unit in pairs (units) do
      local typo = _unit:GetTypeName()
      if typename == typo then
        if Cinematic then
          local coordinate = _unit:GetCoordinate()
          local name = _unit:GetName()
          Smokers(name,coordinate,Effect,Density)
        end
        _unit:Destroy(false)
        reduced = reduced + 1
        if reduced == anzahl then break end
      end
    end
  end

  local function PostSpawn(args)
    local spwndgrp = args[1]
    local size = args[2]
    local structure = args[3]

    setdata:AddObject(spwndgrp)
    local actualsize = spwndgrp:CountAliveUnits()
    if actualsize > size then
      if Structured and structure then

        local loadedstructure = {}
        local strcset = UTILS.Split(structure,";")
        for _,_data in pairs(strcset) do
          local datasplit = UTILS.Split(_data,"==")
          loadedstructure[datasplit[1]] = tonumber(datasplit[2])
        end

        local originalstructure = UTILS.GetCountPerTypeName(spwndgrp)

        for _name,_number in pairs(originalstructure) do
          local loadednumber = 0
          if loadedstructure[_name] then
            loadednumber = loadedstructure[_name]
          end
          local reduce = false
          if loadednumber < _number then reduce = true end

          if reduce then
            Cruncher(spwndgrp,_name,_number-loadednumber)
          end

        end
      else
        local reduction = actualsize-size
        -- reduce existing group
        local units = spwndgrp:GetUnits()
        local units2 = UTILS.ShuffleTable(units) -- randomize table
        for i=1,reduction do
          units2[i]:Destroy(false)
        end
      end
    end
  end

  local function MultiUse(Data)
    local template = Data.template
    if template and usedtemplates[template] and usedtemplates[template].used and usedtemplates[template].used > 1 then
      -- multispawn
      if not usedtemplates[template].done then
        local spwnd = 0
        local spawngrp = SPAWN:New(template)
        spawngrp:InitLimit(0,usedtemplates[template].used)
        for _,_entry in pairs(usedtemplates[template].data) do
          spwnd = spwnd + 1
          local sgrp=spawngrp:SpawnFromCoordinate(_entry.coordinate,spwnd)
          BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure})
        end
        usedtemplates[template].done = true
      end
      return true
    else
      return false
    end
  end

  --BASE:I("Spawn = "..tostring(spawn))
  if UTILS.CheckFileExists(Path,filename) then
    local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename)
    -- remove header
    table.remove(loadeddata, 1)
    for _id,_entry in pairs (loadeddata) do
      local dataset = UTILS.Split(_entry,",")
      -- groupname,template,units,position.x,position.y,position.z
      local groupname = dataset[1]
      local template = dataset[2]
      local size = tonumber(dataset[3])
      local posx = tonumber(dataset[4])
      local posy = tonumber(dataset[5])
      local posz = tonumber(dataset[6])
      local structure = dataset[7]
      local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz})
      local group=nil
      if size > 0 then
        local data = { groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure }
        table.insert(datatable,data)
        if usedtemplates[template] then
          usedtemplates[template].used = usedtemplates[template].used + 1
          table.insert(usedtemplates[template].data,data)
        else
          usedtemplates[template] = {
              data = {},
              used = 1,
              done = false,
            }
          table.insert(usedtemplates[template].data,data)
        end
      end
    end
    for _id,_entry in pairs (datatable) do
      if spawn and not MultiUse(_entry) and _entry.size > 0 then
        local group = SPAWN:New(_entry.template)
        local sgrp=group:SpawnFromCoordinate(_entry.coordinate)
        BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure})
      end
    end
  else
    return nil
  end
  if spawn then
    return setdata,fires
  else
   return datatable
  end
end

--- Load back a SET of statics from file.
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return Core.Set#SET_STATIC Set SET_STATIC containing the static objects.
function UTILS.LoadSetOfStatics(Path,Filename)
  local filename = Filename or "SetOfStatics"
  local datatable = SET_STATIC:New()
  if UTILS.CheckFileExists(Path,filename) then
    local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename)
    -- remove header
    table.remove(loadeddata, 1)
    for _id,_entry in pairs (loadeddata) do
      local dataset = UTILS.Split(_entry,",")
      local staticname = dataset[1]
      local StaticObject = STATIC:FindByName(staticname,false)
      if StaticObject then
        datatable:AddObject(StaticObject)
      end
    end
  else
    return nil
  end
  return datatable
end

--- Load back a stationary list of statics from file.
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @param #boolean Reduce If false, do not destroy the units with size=0.
-- @param #boolean Dead (Optional, needs Reduce = true) If Dead is true, re-spawn the dead object as dead and do not just delete it.
-- @param #boolean Cinematic (Optional, needs Dead = true) If true, place a fire/smoke effect on the dead static position.
-- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke.
-- @param #number Density (Optional for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5.
-- @return #table Table of data objects (tables) containing staticname, size (0=dead else 1), coordinate and the static object. Dead objects will have coordinate points `{x=0,y=0,z=0}`
-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )`
-- Returns nil when file cannot be read.
function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce,Dead,Cinematic,Effect,Density)
  local fires = {}
  local reduce = true
  if Reduce == false then reduce = false end
  local filename = Filename or "StateListofStatics"
  local datatable = {}
  if UTILS.CheckFileExists(Path,filename) then
    local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename)
    -- remove header
    table.remove(loadeddata, 1)
    for _id,_entry in pairs (loadeddata) do
      local dataset = UTILS.Split(_entry,",")
      -- staticname,units(1/0),position.x,position.y,position.z)
      local staticname = dataset[1]
      local size = tonumber(dataset[2])
      local posx = tonumber(dataset[3])
      local posy = tonumber(dataset[4])
      local posz = tonumber(dataset[5])
      local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz})
      local data = { staticname=staticname, size=size, coordinate=coordinate, static=STATIC:FindByName(staticname,false) }
      table.insert(datatable,data)
      if size==0 and reduce then
        local static = STATIC:FindByName(staticname,false)
        if static then
          if Dead then
            local deadobject = SPAWNSTATIC:NewFromStatic(staticname,static:GetCountry())
            deadobject:InitDead(true)
            local heading = static:GetHeading()
            local coord = static:GetCoordinate()
            static:Destroy(false)
            deadobject:SpawnFromCoordinate(coord,heading,staticname)
            if Cinematic then
              local effect = math.random(8)
              if type(Effect) == "number" then
                effect = Effect
              end
              coord:BigSmokeAndFire(effect,Density,staticname)
              table.insert(fires,staticname)
            end
          else
            static:Destroy(false)
          end
        end
      end
    end
  else
    return nil
  end
  return datatable,fires
end

--- Heading Degrees (0-360) to Cardinal
-- @param #number Heading The heading
-- @return #string Cardinal, e.g. "NORTH"
function UTILS.BearingToCardinal(Heading)
  if     Heading >= 0   and Heading <= 22  then return "North"
    elseif Heading >= 23  and Heading <= 66  then return "North-East"
    elseif Heading >= 67  and Heading <= 101 then return "East"
    elseif Heading >= 102 and Heading <= 146 then return "South-East"
    elseif Heading >= 147 and Heading <= 201 then return "South"
    elseif Heading >= 202 and Heading <= 246 then return "South-West"
    elseif Heading >= 247 and Heading <= 291 then return "West"
    elseif Heading >= 292 and Heading <= 338 then return "North-West"
    elseif Heading >= 339 then return "North"
  end
end

--- Create a BRAA NATO call string BRAA between two GROUP objects
-- @param Wrapper.Group#GROUP FromGrp GROUP object
-- @param Wrapper.Group#GROUP ToGrp GROUP object
-- @return #string Formatted BRAA NATO call
function UTILS.ToStringBRAANATO(FromGrp,ToGrp)
  local BRAANATO = "Merged."
  local GroupNumber = ToGrp:GetSize()
  local GroupWords = "Singleton"
  if GroupNumber == 2 then GroupWords = "Two-Ship"
    elseif GroupNumber >= 3 then GroupWords = "Heavy"
  end
  local grpLeadUnit = ToGrp:GetUnit(1)
  local tgtCoord = grpLeadUnit:GetCoordinate()
  local currentCoord = FromGrp:GetCoordinate()
  local hdg = UTILS.Round(ToGrp:GetHeading()/100,1)*100
  local bearing = UTILS.Round(currentCoord:HeadingTo(tgtCoord),0)
  local rangeMetres = tgtCoord:Get2DDistance(currentCoord)
  local rangeNM = UTILS.Round( UTILS.MetersToNM(rangeMetres), 0)
  local aspect = tgtCoord:ToStringAspect(currentCoord)
  local alt = UTILS.Round(UTILS.MetersToFeet(grpLeadUnit:GetAltitude())/1000,0)--*1000
  local track = UTILS.BearingToCardinal(hdg)
  if rangeNM > 3 then
      if aspect == "" then
        BRAANATO = string.format("%s, BRA, %03d, %d miles, Angels %d, Track %s",GroupWords,bearing, rangeNM, alt, track)
      else
        BRAANATO = string.format("%s, BRAA, %03d, %d miles, Angels %d, %s, Track %s",GroupWords, bearing, rangeNM, alt, aspect, track)
      end
  end
  return BRAANATO
end

--- Check if an object is contained in a table.
-- @param #table Table The table.
-- @param #table Object The object to check.
-- @param #string Key (Optional) Key to check. By default, the object itself is checked.
-- @return #boolean Returns `true` if object is in table.
function UTILS.IsInTable(Table, Object, Key)

  for key, object in pairs(Table) do
    if Key then
      if Object[Key]==object[Key] then
        return true
      end
    else
      if object==Object then
        return true
      end
    end
  end

  return false
end

--- Check if any object of multiple given objects is contained in a table.
-- @param #table Table The table.
-- @param #table Objects The objects to check.
-- @param #string Key (Optional) Key to check.
-- @return #boolean Returns `true` if object is in table.
function UTILS.IsAnyInTable(Table, Objects, Key)

  for _,Object in pairs(UTILS.EnsureTable(Objects)) do

    for key, object in pairs(Table) do
      if Key then
        if Object[Key]==object[Key] then
          return true
        end
      else
        if object==Object then
          return true
        end
      end
    end

  end

  return false
end

--- Helper function to plot a racetrack on the F10 Map - curtesy of Buur.
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Altitude Altitude in feet
-- @param #number Speed Speed in knots
-- @param #number Heading Heading in degrees
-- @param #number Leg Leg in NM
-- @param #number Coalition Coalition side, e.g. coaltion.side.RED or coaltion.side.BLUE
-- @param #table Color Color of the line in RGB, e.g. {1,0,0} for red
-- @param #number Alpha Transparency factor, between 0.1 and 1
-- @param #number LineType Line type to be used, line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
-- @param #boolean ReadOnly
function UTILS.PlotRacetrack(Coordinate, Altitude, Speed, Heading, Leg, Coalition, Color, Alpha, LineType, ReadOnly)
    local fix_coordinate = Coordinate
    local altitude = Altitude
    local speed = Speed or 350
    local heading = Heading or 270
    local leg_distance = Leg or 10

    local coalition = Coalition or -1
    local color = Color or {1,0,0}
    local alpha = Alpha or 1
    local lineType = LineType or 1


    speed = UTILS.IasToTas(speed, UTILS.FeetToMeters(altitude), oatcorr)

    local turn_radius = 0.0211 * speed -3.01

    local point_two = fix_coordinate:Translate(UTILS.NMToMeters(leg_distance), heading, true, false)
    local point_three = point_two:Translate(UTILS.NMToMeters(turn_radius)*2, heading - 90, true, false)
    local point_four = fix_coordinate:Translate(UTILS.NMToMeters(turn_radius)*2, heading - 90, true, false)
    local circle_center_fix_four = point_two:Translate(UTILS.NMToMeters(turn_radius), heading - 90, true, false)
    local circle_center_two_three = fix_coordinate:Translate(UTILS.NMToMeters(turn_radius), heading - 90, true, false)


    fix_coordinate:LineToAll(point_two, coalition, color, alpha, lineType)
    point_four:LineToAll(point_three, coalition, color, alpha, lineType)
    circle_center_fix_four:CircleToAll(UTILS.NMToMeters(turn_radius), coalition, color, alpha, nil, 0, lineType)--, ReadOnly, Text)
    circle_center_two_three:CircleToAll(UTILS.NMToMeters(turn_radius), coalition, color, alpha, nil, 0, lineType)--, ReadOnly, Text)

end

--- Get the current time in a "nice" format like 21:01:15
-- @return #string Returns string with the current time
function UTILS.TimeNow()
    return UTILS.SecondsToClock(timer.getAbsTime(), false, false)
end


--- Given 2 "nice" time string, returns the difference between the two in seconds
-- @param #string start_time Time string like "07:15:22"
-- @param #string end_time Time string like "08:11:27"
-- @return #number Seconds between start_time and end_time
function UTILS.TimeDifferenceInSeconds(start_time, end_time)
    return UTILS.ClockToSeconds(end_time) - UTILS.ClockToSeconds(start_time)
end

--- Check if the current time is later than time_string.
-- @param #string start_time Time string like "07:15:22"
-- @return #boolean True if later, False if before
function UTILS.TimeLaterThan(time_string)
    if timer.getAbsTime() > UTILS.ClockToSeconds(time_string) then
        return true
    end
    return false
end

--- Check if the current time is before time_string.
-- @param #string start_time Time string like "07:15:22"
-- @return #boolean False if later, True if before
function UTILS.TimeBefore(time_string)
    if timer.getAbsTime() < UTILS.ClockToSeconds(time_string) then
        return true
    end
    return false
end


--- Combines two time strings to give you a new time. For example "15:16:32" and "02:06:24" would return "17:22:56"
-- @param #string time_string_01 Time string like "07:15:22"
-- @param #string time_string_02 Time string like "08:11:27"
-- @return #string Result of the two time string combined
function UTILS.CombineTimeStrings(time_string_01, time_string_02)
    local hours1, minutes1, seconds1 = time_string_01:match("(%d+):(%d+):(%d+)")
    local hours2, minutes2, seconds2 = time_string_02:match("(%d+):(%d+):(%d+)")
    local total_seconds = tonumber(seconds1) + tonumber(seconds2) + tonumber(minutes1) * 60 + tonumber(minutes2) * 60 + tonumber(hours1) * 3600 + tonumber(hours2) * 3600

    total_seconds = total_seconds % (24 * 3600)
    if total_seconds < 0 then
        total_seconds = total_seconds + 24 * 3600
    end

    local hours = math.floor(total_seconds / 3600)
    total_seconds = total_seconds - hours * 3600
    local minutes = math.floor(total_seconds / 60)
    local seconds = total_seconds % 60

    return string.format("%02d:%02d:%02d", hours, minutes, seconds)
end


--- Subtracts two time string to give you a new time. For example "15:16:32" and "02:06:24" would return "13:10:08"
-- @param #string time_string_01 Time string like "07:15:22"
-- @param #string time_string_02 Time string like "08:11:27"
-- @return #string Result of the two time string subtracted
function UTILS.SubtractTimeStrings(time_string_01, time_string_02)
    local hours1, minutes1, seconds1 = time_string_01:match("(%d+):(%d+):(%d+)")
    local hours2, minutes2, seconds2 = time_string_02:match("(%d+):(%d+):(%d+)")
    local total_seconds = tonumber(seconds1) - tonumber(seconds2) + tonumber(minutes1) * 60 - tonumber(minutes2) * 60 + tonumber(hours1) * 3600 - tonumber(hours2) * 3600

    total_seconds = total_seconds % (24 * 3600)
    if total_seconds < 0 then
        total_seconds = total_seconds + 24 * 3600
    end

    local hours = math.floor(total_seconds / 3600)
    total_seconds = total_seconds - hours * 3600
    local minutes = math.floor(total_seconds / 60)
    local seconds = total_seconds % 60

    return string.format("%02d:%02d:%02d", hours, minutes, seconds)
end

--- Checks if the current time is in between start_time and end_time
-- @param #string time_string_01 Time string like "07:15:22"
-- @param #string time_string_02 Time string like "08:11:27"
-- @return #boolean True if it is, False if it's not
function UTILS.TimeBetween(start_time, end_time)
    return UTILS.TimeLaterThan(start_time) and UTILS.TimeBefore(end_time)
end

--- Easy to read one line to roll the dice on something. 1% is very unlikely to happen, 99% is very likely to happen
-- @param #number chance (optional) Percentage chance you want something to happen. Defaults to a random number if not given
-- @return #boolean True if the dice roll was within the given percentage chance of happening
function UTILS.PercentageChance(chance)
    chance = chance or math.random(0, 100)
    chance = UTILS.Clamp(chance, 0, 100)
    local percentage = math.random(0, 100)
    if percentage < chance then
        return true
    end
    return false
end

--- Easy to read one liner to clamp a value
-- @param #number value Input value
-- @param #number min Minimal value that should be respected
-- @param #number max Maximal value that should be respected
-- @return #number Clamped value
function UTILS.Clamp(value, min, max)
    if value < min then value = min end
    if value > max then value = max end

    return value
end

--- Clamp an angle so that it's always between 0 and 360 while still being correct
-- @param #number value Input value
-- @return #number Clamped value
function UTILS.ClampAngle(value)
    if value > 360 then return value - 360 end
    if value < 0 then return value + 360 end
    return value
end

--- Remap an input to a new value in a given range. For example:
--- UTILS.RemapValue(20, 10, 30, 0, 200) would return 100
--- 20 is 50% between 10 and 30
--- 50% between 0 and 200 is 100
-- @param #number value Input value
-- @param #number old_min Min value to remap from
-- @param #number old_max Max value to remap from
-- @param #number new_min Min value to remap to
-- @param #number new_max Max value to remap to
-- @return #number Remapped value
function UTILS.RemapValue(value, old_min, old_max, new_min, new_max)
    new_min = new_min or 0
    new_max = new_max or 100

    local old_range = old_max - old_min
    local new_range = new_max - new_min
    local percentage = (value - old_min) / old_range
    return (new_range * percentage) + new_min
end

--- Given a triangle made out of 3 vector 2s, return a vec2 that is a random number in this triangle
-- @param DCS#Vec2 pt1 Min value to remap from
-- @param DCS#Vec2 pt2 Max value to remap from
-- @param DCS#Vec2 pt3 Max value to remap from
-- @return DCS#Vec2 Random point in triangle
function UTILS.RandomPointInTriangle(pt1, pt2, pt3)
    local pt = {math.random(), math.random()}
    table.sort(pt)
    local s = pt[1]
    local t = pt[2] - pt[1]
    local u = 1 - pt[2]

    return {x = s * pt1.x + t * pt2.x + u * pt3.x,
            y = s * pt1.y + t * pt2.y + u * pt3.y}
end

--- Checks if a given angle (heading) is between 2 other angles. Min and max have to be given in clockwise order For example:
--- UTILS.AngleBetween(350, 270, 15) would return True
--- UTILS.AngleBetween(22, 95, 20) would return False
-- @param #number angle Min value to remap from
-- @param #number min Max value to remap from
-- @param #number max Max value to remap from
-- @return #boolean 
function UTILS.AngleBetween(angle, min, max)
    angle = (360 + (angle % 360)) % 360
    min = (360 + min % 360) % 360
    max = (360 + max % 360) % 360

    if min < max then return min <= angle and angle <= max end
    return min <= angle or angle <= max
end

--- Easy to read one liner to write a JSON file. Everything in @data should be serializable
--- json.lua exists in the DCS install Scripts folder
-- @param #table data table to write
-- @param #string file_path File path
function UTILS.WriteJSON(data, file_path)
    package.path  = package.path ..  ";.\\Scripts\\?.lua"
    local JSON = require("json")
    local pretty_json_text = JSON:encode_pretty(data)
    local write_file = io.open(file_path, "w")
    write_file:write(pretty_json_text)
    write_file:close()
end

--- Easy to read one liner to read a JSON file.
--- json.lua exists in the DCS install Scripts folder
-- @param #string file_path File path
-- @return #table
function UTILS.ReadJSON(file_path)
    package.path  = package.path ..  ";.\\Scripts\\?.lua"
    local JSON = require("json")
    local read_file = io.open(file_path, "r")
    local contents = read_file:read( "*a" )
    io.close(read_file)
    return JSON:decode(contents)
end

--- Get the properties names and values of properties set up on a Zone in the Mission Editor.
--- This doesn't work for any zones created in MOOSE
-- @param #string zone_name Name of the zone as set up in the Mission Editor
-- @return #table with all the properties on a zone
function UTILS.GetZoneProperties(zone_name)
    local return_table = {}
    for _, zone in pairs(env.mission.triggers.zones) do
        if zone["name"] == zone_name then
            if table.length(zone["properties"]) > 0 then
                for _, property in pairs(zone["properties"]) do
                    return_table[property["key"]] = property["value"]
                end
              return return_table
            else
                BASE:I(string.format("%s doesn't have any properties", zone_name))
                return {}
            end
        end
    end
end

--- Rotates a point around another point with a given angle. Useful if you're loading in groups or
--- statics but you want to rotate them all as a collection. You can get the center point of everything
--- and then rotate all the positions of every object around this center point.
-- @param DCS#Vec2 point Point that you want to rotate
-- @param DCS#Vec2 pivot Pivot point of the rotation
-- @param #number angle How many degrees the point should be rotated
-- @return DCS#Vec2 Rotated point
function UTILS.RotatePointAroundPivot(point, pivot, angle)
    local radians = math.rad(angle)

    local x = point.x - pivot.x
    local y = point.y - pivot.y

    local rotated_x = x * math.cos(radians) - y * math.sin(radians)
    local rotatex_y = x * math.sin(radians) + y * math.cos(radians)

    local original_x = rotated_x + pivot.x
    local original_y = rotatex_y + pivot.y

    return { x = original_x, y = original_y }
end

--- Makes a string semi-unique by attaching a random number between 0 and 1 million to it
-- @param #string base String you want to unique-fy
-- @return #string Unique string
function UTILS.UniqueName(base)
    base = base or ""
    local ran = tostring(math.random(0, 1000000))

    if base == "" then
        return ran
    end
    return base .. "_" .. ran
end

--- Check if a string starts with something
-- @param #string str String to check
-- @param #string value
-- @return #boolean True if str starts with value
function string.startswith(str, value)
   return string.sub(str,1,string.len(value)) == value
end


--- Check if a string ends with something
-- @param #string str String to check
-- @param #string value
-- @return #boolean True if str ends with value
function string.endswith(str, value)
    return value == "" or str:sub(-#value) == value
end

--- Splits a string on a separator. For example:
--- string.split("hello_dcs_world", "-") would return {"hello", "dcs", "world"}
-- @param #string input String to split
-- @param #string separator What to split on
-- @return #table individual strings
function string.split(input, separator)
    local parts = {}
    for part in input:gmatch("[^" .. separator .. "]+") do
        table.insert(parts, part)
    end
    return parts
end


--- Checks if a string contains a substring. Easier to remember for Python people :)
--- string.split("hello_dcs_world", "-") would return {"hello", "dcs", "world"}
-- @param #string str
-- @param #string value
-- @return #boolean True if str contains value
function string.contains(str, value)
    return string.match(str, value)
end


--- Moves an object from one table to another
-- @param #table obj object to move
-- @param #table from_table table to move from
-- @param #table to_table table to move to
function table.move_object(obj, from_table, to_table)
    local index
    for i, v in pairs(from_table) do
        if v == obj then
            index = i
        end
    end

    if index then
        local moved = table.remove(from_table, index)
        table.insert_unique(to_table, moved)
    end
end

--- Given tbl is a indexed table ({"hello", "dcs", "world"}), checks if element exists in the table.
--- The table can be made up out of complex tables or values as well
-- @param #table tbl
-- @param #string element
-- @return #boolean True if tbl contains element
function table.contains(tbl, element)
    if element == nil or tbl == nil then return false end

    local index = 1
    while tbl[index] do
        if tbl[index] == element then
            return true
        end
        index = index + 1
    end
    return false
end

--- Checks if a table contains a specific key.
-- @param #table tbl Table to check
-- @param #string key Key to look for
-- @return #boolean True if tbl contains key
function table.contains_key(tbl, key)
    if tbl[key] ~= nil then return true else return false end
end

--- Inserts a unique element into a table.
-- @param #table tbl Table to insert into
-- @param #string element Element to insert
function table.insert_unique(tbl, element)
    if element == nil or tbl == nil then return end

    if not table.contains(tbl, element) then
        table.insert(tbl, element)
    end
end

--- Removes an element from a table by its value.
-- @param #table tbl Table to remove from
-- @param #string element Element to remove
function table.remove_by_value(tbl, element)
    local indices_to_remove = {}
    local index = 1
    for _, value in pairs(tbl) do
        if value == element then
            table.insert(indices_to_remove, index)
        end
        index = index + 1
    end

    for _, idx in pairs(indices_to_remove) do
        table.remove(tbl, idx)
    end
end

--- Removes an element from a table by its key.
-- @param #table table Table to remove from
-- @param #string key Key of the element to remove
-- @return #string Removed element
function table.remove_key(table, key)
    local element = table[key]
    table[key] = nil
    return element
end

--- Finds the index of an element in a table.
-- @param #table table Table to search
-- @param #string element Element to find
-- @return #number Index of the element, or nil if not found
function table.index_of(table, element)
    for i, v in ipairs(table) do
        if v == element then
            return i
        end
    end
    return nil
end

--- Counts the number of elements in a table.
-- @param #table T Table to count
-- @return #number Number of elements in the table
function table.length(T)
  local count = 0
  for _ in pairs(T) do count = count + 1 end
  return count
end

--- Slices a table between two indices, much like Python's my_list[2:-1]
-- @param #table tbl Table to slice
-- @param #number first Starting index
-- @param #number last Ending index
-- @return #table Sliced table
function table.slice(tbl, first, last)
  local sliced = {}
  local start = first or 1
  local stop = last or table.length(tbl)
  local count = 1

  for key, value in pairs(tbl) do
      if count >= start and count <= stop then
          sliced[key] = value
      end
      count = count + 1
  end

  return sliced
end

--- Counts the number of occurrences of a value in a table.
-- @param #table tbl Table to search
-- @param #string value Value to count
-- @return #number Number of occurrences of the value
function table.count_value(tbl, value)
    local count = 0
    for _, item in pairs(tbl) do
        if item == value then count = count + 1 end
    end
    return count
end

--- Add 2 table together, t2 gets added to t1
-- @param #table t1 First table
-- @param #table t2 Second table
-- @return #table Combined table
function table.combine(t1, t2)
    if t1 == nil and t2 == nil then
        BASE:E("Both tables were empty!")
    end

    if t1 == nil then return t2 end
    if t2 == nil then return t1 end
    for i=1,#t2 do
        t1[#t1+1] = t2[i]
    end
    return t1
end

--- Merges two tables into one. If a key exists in both t1 and t2, the value of t1 with be overwritten by the value of t2
-- @param #table t1 First table
-- @param #table t2 Second table
-- @return #table Merged table
function table.merge(t1, t2)
    for k, v in pairs(t2) do
        if (type(v) == "table") and (type(t1[k] or false) == "table") then
            table.merge(t1[k], t2[k])
        else
            t1[k] = v
        end
    end
    return t1
end

--- Adds an item to the end of a table.
-- @param #table tbl Table to add to
-- @param #string item Item to add
function table.add(tbl, item)
    tbl[#tbl + 1] = item
end

--- Shuffles the elements of a table.
-- @param #table tbl Table to shuffle
-- @return #table Shuffled table
function table.shuffle(tbl)
    local new_table = {}
    for _, value in ipairs(tbl) do
        local pos = math.random(1, #new_table +1)
        table.insert(new_table, pos, value)
    end
    return new_table
end

--- Finds a key-value pair in a table.
-- @param #table tbl Table to search
-- @param #string key Key to find
-- @param #string value Value to find
-- @return #table Table containing the key-value pair, or nil if not found
function table.find_key_value_pair(tbl, key, value)
    for k, v in pairs(tbl) do
        if type(v) == "table" then
            local result = table.find_key_value_pair(v, key, value)
            if result ~= nil then
                return result
            end
        elseif k == key and v == value then
            return tbl
        end
    end
    return nil
end

--- Convert a decimal to octal
-- @param #number Number the number to convert
-- @return #number Octal
function UTILS.DecimalToOctal(Number)
  if Number < 8 then return Number end
  local number = tonumber(Number)
  local octal = ""
  local n=1
  while number > 7 do
    local number1 = number%8
    octal = string.format("%d",number1)..octal
    local number2 = math.abs(number/8)
    if number2 < 8 then
      octal = string.format("%d",number2)..octal
    end
    number = number2
    n=n+1
  end
  return tonumber(octal)
end

--- Convert an octal to decimal
-- @param #number Number the number to convert
-- @return #number Decimal
function UTILS.OctalToDecimal(Number)
  return tonumber(Number,8)
end


--- HexToRGBA
-- @param hex_string table
-- @return #table R, G, B, A
function UTILS.HexToRGBA(hex_string)
    local hexNumber = tonumber(string.sub(hex_string, 3), 16) -- convert the string to a number
    -- extract RGBA components
    local alpha = hexNumber % 256
    hexNumber = (hexNumber - alpha) / 256
    local blue = hexNumber % 256
    hexNumber = (hexNumber - blue) / 256
    local green = hexNumber % 256
    hexNumber = (hexNumber - green) / 256
    local red = hexNumber % 256

    return {R = red, G = green, B = blue, A = alpha}
end


--- Function to save the position of a set of #OPSGROUP (ARMYGROUP) objects.
-- @param Core.Set#SET_OPSGROUP Set of ops objects to save
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @param #boolean Structured Append the data with a list of typenames in the group plus their count.
-- @return #boolean outcome True if saving is successful, else false.
function UTILS.SaveSetOfOpsGroups(Set,Path,Filename,Structured)
  local filename = Filename or "SetOfGroups"
  local data = "--Save SET of groups: (name,legion,template,alttemplate,units,position.x,position.y,position.z,strucdata) "..Filename .."\n"
  local List = Set:GetSetObjects()
  for _,_group in pairs (List) do
    local group = _group:GetGroup() -- Wrapper.Group#GROUP
    if group and group:IsAlive() then
      local name = group:GetName()
      local template = string.gsub(name,"(.AID.%d+$","")
      if string.find(template,"#") then
       template = string.gsub(name,"#(%d+)$","")
      end
      local alttemplate = _group.templatename or "none"
      local legiono = _group.legion -- Ops.Legion#LEGION
      local legion = "none"
      if legiono and type(legiono) == "table" and legiono.ClassName then
        legion = legiono:GetName()
        local asset = legiono:GetAssetByName(name) -- Functional.Warehouse#WAREHOUSE.Assetitem
        alttemplate=asset.templatename
      end
      local units = group:CountAliveUnits()
      local position = group:GetVec3()
      if Structured then
        local structure = UTILS.GetCountPerTypeName(group)
        local strucdata =  ""
        for typen,anzahl in pairs (structure) do
          strucdata = strucdata .. typen .. "=="..anzahl..";"
        end
        data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d,%s\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z,strucdata)
      else
        data = string.format("%s%s,%s,%s,%s,%d,%d,%d,%d\n",data,name,legion,template,alttemplate,units,position.x,position.y,position.z)
      end
    end
  end
  -- save the data
  local outcome = UTILS.SaveToFile(Path,Filename,data)
  return outcome
end

--- Load back a #OPSGROUP (ARMYGROUP) data from file for use with @{Ops.Brigade#BRIGADE.LoadBackAssetInPosition}()
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return #table Returns a table of data entries: `{ groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure, legion=legion, alttemplate=alttemplate }`
-- Returns nil when the file cannot be read.
function UTILS.LoadSetOfOpsGroups(Path,Filename)

  local filename = Filename or "SetOfGroups"
  local datatable = {}

  if UTILS.CheckFileExists(Path,filename) then
    local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename)
    -- remove header
    table.remove(loadeddata, 1)
    for _id,_entry in pairs (loadeddata) do
      local dataset = UTILS.Split(_entry,",")
      -- 1name,2legion,3template,4alttemplate,5units,6position.x,7position.y,8position.z,9strucdata
      local groupname = dataset[1]
      local legion = dataset[2]
      local template = dataset[3]
      local alttemplate = dataset[4]
      local size = tonumber(dataset[5])
      local posx = tonumber(dataset[6])
      local posy = tonumber(dataset[7])
      local posz = tonumber(dataset[8])
      local structure = dataset[9]
      local coordinate = COORDINATE:NewFromVec3({x=posx, y=posy, z=posz})
      if size > 0 then
        local data = { groupname=groupname, size=size, coordinate=coordinate, template=template, structure=structure, legion=legion, alttemplate=alttemplate }
        table.insert(datatable,data)
      end
    end
  else
    return nil
  end

  return datatable
end

--- Get the clock position from a relative heading
-- @param #number refHdg The heading of the reference object (such as a Wrapper.UNIT) in 0-360
-- @param #number tgtHdg The absolute heading from the reference object to the target object/point in 0-360
-- @return #string text Text in clock heading such as "4 O'CLOCK"
-- @usage Display the range and clock distance of a BTR in relation to REAPER 1-1's heading:
--
--          myUnit = UNIT:FindByName( "REAPER 1-1" )
--          myTarget = GROUP:FindByName( "BTR-1" )
--
--          coordUnit = myUnit:GetCoordinate()
--          coordTarget = myTarget:GetCoordinate()
--
--          hdgUnit = myUnit:GetHeading()
--          hdgTarget = coordUnit:HeadingTo( coordTarget )
--          distTarget = coordUnit:Get3DDistance( coordTarget )
--
--          clockString = UTILS.ClockHeadingString( hdgUnit, hdgTarget )
--
--          -- Will show this message to REAPER 1-1 in-game: Contact BTR at 3 o'clock for 1134m!
--          MESSAGE:New("Contact BTR at " .. clockString .. " for " .. distTarget  .. "m!):ToUnit( myUnit )
function UTILS.ClockHeadingString(refHdg,tgtHdg)
    local relativeAngle = tgtHdg - refHdg
    if relativeAngle < 0 then
        relativeAngle = relativeAngle + 360
    end
    local clockPos = math.ceil((relativeAngle % 360) / 30)
    return clockPos.." o'clock"
end

--- Get a NATO abbreviated MGRS text for SRS use, optionally with prosody slow tag
-- @param #string Text The input string, e.g. "MGRS 4Q FJ 12345 67890"
-- @param #boolean Slow Optional - add slow tags
-- @return #string Output for (Slow) spelling in SRS TTS e.g. "MGRS;<prosody rate="slow">4;Quebec;Foxtrot;Juliett;1;2;3;4;5;6;7;8;niner;zero;</prosody>"
function UTILS.MGRSStringToSRSFriendly(Text,Slow)
    local Text = string.gsub(Text,"MGRS ","")
    Text = string.gsub(Text,"%s+","")
    Text = string.gsub(Text,"([%a%d])","%1;") -- "0;5;1;"
    Text = string.gsub(Text,"A","Alpha")
    Text = string.gsub(Text,"B","Bravo")
    Text = string.gsub(Text,"C","Charlie")
    Text = string.gsub(Text,"D","Delta")
    Text = string.gsub(Text,"E","Echo")
    Text = string.gsub(Text,"F","Foxtrot")
    Text = string.gsub(Text,"G","Golf")
    Text = string.gsub(Text,"H","Hotel")
    Text = string.gsub(Text,"I","India")
    Text = string.gsub(Text,"J","Juliett")
    Text = string.gsub(Text,"K","Kilo")
    Text = string.gsub(Text,"L","Lima")
    Text = string.gsub(Text,"M","Mike")
    Text = string.gsub(Text,"N","November")
    Text = string.gsub(Text,"O","Oscar")
    Text = string.gsub(Text,"P","Papa")
    Text = string.gsub(Text,"Q","Quebec")
    Text = string.gsub(Text,"R","Romeo")
    Text = string.gsub(Text,"S","Sierra")
    Text = string.gsub(Text,"T","Tango")
    Text = string.gsub(Text,"U","Uniform")
    Text = string.gsub(Text,"V","Victor")
    Text = string.gsub(Text,"W","Whiskey")
    Text = string.gsub(Text,"X","Xray")
    Text = string.gsub(Text,"Y","Yankee")
    Text = string.gsub(Text,"Z","Zulu")
    Text = string.gsub(Text,"0","zero")
    Text = string.gsub(Text,"9","niner")
    if Slow then
      Text = '<prosody rate="slow">'..Text..'</prosody>'
    end
    Text = "MGRS;"..Text
    return Text
end


--- Read csv file and convert it to a lua table.
-- The csv must have a header specifing the names of the columns. The column names are used as table keys.
-- @param #string filename File name including full path on local disk.
-- @return #table The table filled with data from the csv file.
function UTILS.ReadCSV(filename)

  if not UTILS.FileExists(filename) then  
    env.error("File does not exist")
    return nil  
  end
  
  --- Function that load data from a file.
  local function _loadfile( filename )
    local f = io.open( filename, "rb" )
    if f then
      local data = f:read( "*all" )
      f:close()
      return data
    else
      BASE:E(string.format( "WARNING: Could read data from file %s!", tostring( filename ) ) )
      return nil
    end
  end
  
  -- Load asset data from file.
  local data = _loadfile( filename )
  
  local lines=UTILS.Split(data, "\n" )
  
  -- Remove carriage returns from end of lines
  for _,line in pairs(lines) do
    line=string.gsub(line, "[\n\r]","")
  end
  
  local sep=";"
  
  local columns=UTILS.Split(lines[1], sep)

  -- Remove header line.
  table.remove(lines, 1)
  
  local csvdata={}  
  for i, line in pairs(lines) do
    line=string.gsub(line, "[\n\r]","")
  
    local row={}
    for j, value in pairs(UTILS.Split(line, sep)) do
    
      local key=string.gsub(columns[j], "[\n\r]","")
      row[key]=value    
    end
    table.insert(csvdata, row)
  
  end

  return csvdata
end

--- Seed the LCG random number generator.
-- @param #number seed Seed value. Default is a random number using math.random()
function UTILS.LCGRandomSeed(seed)
  UTILS.lcg = {
    seed = seed or math.random(1, 2^32 - 1),
    a = 1664525,
    c = 1013904223,
    m = 2^32
  }
end

--- Return a pseudo-random number using the LCG algorithm.
-- @return #number Random number between 0 and 1.
function UTILS.LCGRandom()
  if UTILS.lcg == nil then
    UTILS.LCGRandomSeed()
  end
  UTILS.lcg.seed = (UTILS.lcg.a * UTILS.lcg.seed + UTILS.lcg.c) % UTILS.lcg.m
  return UTILS.lcg.seed / UTILS.lcg.m
end

--- Spawns a new FARP of a defined type and coalition and functional statics (fuel depot, ammo storage, tent, windsock) around that FARP to make it operational.
-- Adds vehicles from template if given. Fills the FARP warehouse with liquids and known materiels.
-- References: [DCS Forum Topic](https://forum.dcs.world/topic/282989-farp-equipment-to-run-it)
-- @param #string Name Name of this FARP installation. Must be unique. 
-- @param Core.Point#COORDINATE Coordinate Where to spawn the FARP.
-- @param #string FARPType Type of FARP, can be one of the known types ENUMS.FARPType.FARP, ENUMS.FARPType.INVISIBLE, ENUMS.FARPType.HELIPADSINGLE, ENUMS.FARPType.PADSINGLE. Defaults to ENUMS.FARPType.FARP.
-- @param #number Coalition Coalition of this FARP, i.e. coalition.side.BLUE or coalition.side.RED, defaults to coalition.side.BLUE.
-- @param #number Country Country of this FARP, defaults to country.id.USA (blue) or country.id.RUSSIA (red).
-- @param #number CallSign Callsign of the FARP ATC, defaults to CALLSIGN.FARP.Berlin.
-- @param #number Frequency Frequency of the FARP ATC Radio, defaults to 127.5 (MHz).
-- @param #number Modulation Modulation of the FARP ATC Radio, defaults to radio.modulation.AM.
-- @param #number ADF ADF Beacon (FM) Frequency in KHz, e.g. 428. If not nil, creates an VHF/FM ADF Beacon for this FARP. Requires a sound called "beacon.ogg" to be in the mission (trigger "sound to" ...)
-- @param #number SpawnRadius Radius of the FARP, i.e. where the FARP objects will be placed in meters, not more than 150m away. Defaults to 100.
-- @param #string VehicleTemplate, template name for additional vehicles. Can be nil for no additional vehicles.
-- @param #number Liquids Tons of fuel to be added initially to the FARP. Defaults to 10 (tons). Set to 0 for no fill.
-- @param #number Equipment Number of equipment items per known item to be added initially to the FARP. Defaults to 10 (items). Set to 0 for no fill.
-- @param #number Airframes Number of helicopter airframes per known type in Ops.CSAR#CSAR.AircraftType to be added initially to the FARP. Set to 0 for no airframes.
-- @param #string F10Text Text to display on F10 map if given. Handy to post things like the ADF beacon Frequency, Callsign and ATC Frequency.
-- @param #boolean DynamicSpawns If true, allow Dynamic Spawns from this FARP.
-- @param #boolean HotStart If true and DynamicSpawns is true, allow hot starts for Dynamic Spawns from this FARP.
-- @return #list<Wrapper.Static#STATIC> Table of spawned objects and vehicle object (if given).
-- @return #string ADFBeaconName Name of the ADF beacon, to be able to remove/stop it later.
-- @return #number MarkerID ID of the F10 Text, to be able to remove it later.
function UTILS.SpawnFARPAndFunctionalStatics(Name,Coordinate,FARPType,Coalition,Country,CallSign,Frequency,Modulation,ADF,SpawnRadius,VehicleTemplate,Liquids,Equipment,Airframes,F10Text,DynamicSpawns,HotStart)
  
  -- Set Defaults
  local farplocation = Coordinate
  local farptype = FARPType or ENUMS.FARPType.FARP
  local Coalition = Coalition or coalition.side.BLUE
  local callsign = CallSign or CALLSIGN.FARP.Berlin
  local freq = Frequency or 127.5
  local mod = Modulation or radio.modulation.AM
  local radius = SpawnRadius or 100
  if radius < 0 or radius > 150 then radius = 100 end
  local liquids = Liquids or 10
  liquids = liquids * 1000 -- tons to kg
  local equip = Equipment or 10
  local airframes = Airframes or 10
  local statictypes = ENUMS.FARPObjectTypeNamesAndShape[farptype] or {TypeName="FARP", ShapeName="FARPS"}
  local STypeName = statictypes.TypeName
  local SShapeName = statictypes.ShapeName
  local Country = Country or (Coalition == coalition.side.BLUE and country.id.USA or country.id.RUSSIA)
  local ReturnObjects = {}
  
  -- Spawn FARP
  local newfarp = SPAWNSTATIC:NewFromType(STypeName,"Heliports",Country) --  "Invisible FARP" "FARP"
  newfarp:InitShape(SShapeName) -- "invisiblefarp" "FARPS"
  newfarp:InitFARP(callsign,freq,mod,DynamicSpawns,HotStart)
  local spawnedfarp = newfarp:SpawnFromCoordinate(farplocation,0,Name)
  table.insert(ReturnObjects,spawnedfarp)
  -- Spawn Objects
  local FARPStaticObjectsNato = {
    ["FUEL"] = { TypeName = "FARP Fuel Depot", ShapeName = "GSM Rus", Category = "Fortifications"},
    ["AMMO"] = { TypeName = "FARP Ammo Dump Coating", ShapeName = "SetkaKP", Category = "Fortifications"},
    ["TENT"] = { TypeName = "FARP Tent", ShapeName = "PalatkaB", Category = "Fortifications"},
    ["WINDSOCK"]  = { TypeName = "Windsock", ShapeName = "H-Windsock_RW", Category = "Fortifications"},
  }
    
  local farpobcount = 0
  for _name,_object in pairs(FARPStaticObjectsNato) do
    local objloc = farplocation:Translate(radius,farpobcount*30)
    local heading = objloc:HeadingTo(farplocation)
    local newobject = SPAWNSTATIC:NewFromType(_object.TypeName,_object.Category,Country)
    newobject:InitShape(_object.ShapeName)
    newobject:InitHeading(heading)
    newobject:SpawnFromCoordinate(objloc,farpobcount*30,_name.." - "..Name)
    table.insert(ReturnObjects,newobject)
    farpobcount = farpobcount + 1
  end
  
  -- Vehicle if any
  if VehicleTemplate and type(VehicleTemplate) == "string" then
    local vcoordinate = farplocation:Translate(radius,farpobcount*30)
    local heading = vcoordinate:HeadingTo(farplocation)
    local vehicles = SPAWN:NewWithAlias(VehicleTemplate,"FARP Vehicles - "..Name)
    vehicles:InitGroupHeading(heading)
    vehicles:InitCountry(Country)
    vehicles:InitCoalition(Coalition)
    vehicles:InitDelayOff()
    local spawnedvehicle = vehicles:SpawnFromCoordinate(vcoordinate)
    table.insert(ReturnObjects,spawnedvehicle)
  end
  
  local newWH = STORAGE:New(Name)
  if liquids and liquids > 0 then
    -- Storage fill-up
    newWH:SetLiquid(STORAGE.Liquid.DIESEL,liquids) -- kgs to tons
    newWH:SetLiquid(STORAGE.Liquid.GASOLINE,liquids)
    newWH:SetLiquid(STORAGE.Liquid.JETFUEL,liquids)
    newWH:SetLiquid(STORAGE.Liquid.MW50,liquids)
  end
  
  if equip and equip > 0 then
    for cat,nitem in pairs(ENUMS.Storage.weapons) do
      for name,item in pairs(nitem) do
        newWH:SetItem(item,equip)
      end
    end
  end
  
  if airframes and airframes > 0 then
    for typename in pairs (CSAR.AircraftType) do
      newWH:SetItem(typename,airframes)
    end
  end
  
  local ADFName
  if ADF and type(ADF) == "number" then
    local ADFFreq = ADF*1000 -- KHz to Hz
    local Sound =  "l10n/DEFAULT/beacon.ogg"
    local vec3 = farplocation:GetVec3()
    ADFName = Name .. " ADF "..tostring(ADF).."KHz"
    --BASE:I(string.format("Adding FARP Beacon %d KHz Name %s",ADF,ADFName))
    trigger.action.radioTransmission(Sound, vec3, 0, true, ADFFreq, 250, ADFName)
  end
  
  local MarkerID = nil
  if F10Text then
    local Color = {0,0,1}
    if Coalition == coalition.side.RED then
      Color = {1,0,0}
    elseif Coalition == coalition.side.NEUTRAL then
      Color = {0,1,0}
    end
    local Alpha = 0.75
    local coordinate = Coordinate:Translate(600,0)
    MarkerID = coordinate:TextToAll(F10Text,Coalition,Color,1,{1,1,1},Alpha,14,true)
  end
  
  return ReturnObjects, ADFName, MarkerID
end

--- Spawn a MASH at a given coordinate, optionally, add an ADF Beacon.
-- @param #string Name Unique Name of the Mash.
-- @param Core.Point#COORDINATE Coordinate Coordinate where to spawn the MASH. Can be given as a Core.Zone#ZONE object, in this case we take the center coordinate.
-- @param #number Country Country ID the MASH belongs to, e.g. country.id.USA or country.id.RUSSIA.
-- @param #number ADF (Optional) ADF Frequency in kHz (Kilohertz), if given activate an ADF Beacon at the location of the MASH.
-- @param #string Livery (Optional) The livery of the static CH-47, defaults to dark green.
-- @param #boolean DeployHelo (Optional) If true, deploy the helicopter static.
-- @param #number MASHRadio MASH Radio Frequency, defaults to 127.5.
-- @param #number MASHRadioModulation MASH Radio Modulation, defaults to radio.modulation.AM.
-- @param #number MASHCallsign Defaults to CALLSIGN.FARP.Berlin.
-- @param #table Templates (Optional) You can hand in your own template table of numbered(!) entries. Each entry consist of a relative(!) x,y position and data of a 
-- static, shape_name is optional. Also, livery_id is optional, but is applied to the helicopter static only.
-- @return #table Table of Wrapper.Static#STATIC objects that were spawned.
-- @return #string ADFName Name of the ADF Beacon to remove it later.
-- @usage
--            -- MASH Template example, this one is the built in one used in the function:
--            MASHTemplates = {
--              [1]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.000000,y=0.000000,},
--              [2]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.313533,y=8.778935,},
--              [3]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=16.303737,y=20.379671,},
--              [4]={category='Helicopters',type='CH-47Fbl1',shape_name='none',heading=0,x=-20.047735,y=-63.166179,livery_id = "us army dark green",},
--              [5]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=26.650339,y=20.066138,},
--              [6]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.432292,y=9.077099,},
--              [7]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=-3.216114,},
--              [8]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.439281,y=-3.216114,},
--              [9]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=9.155603,},
--              [10]={category='Fortifications',type='TACAN_beacon',shape_name='none',heading=0,x=-2.329847,y=-16.579903,},
--              [11]={category='Fortifications',type='FARP Fuel Depot',shape_name='GSM Rus',heading=0,x=2.222011,y=4.487030,},
--              [12]={category='Fortifications',type='APFC fuel',shape_name='M92_APFCfuel',heading=0,x=3.614927,y=0.367838,},
--              [13]={category='Fortifications',type='Camouflage03',shape_name='M92_Camouflage03',heading=0,x=21.544148,y=21.998879,},
--              [14]={category='Fortifications',type='Container_generator',shape_name='M92_Container_generator',heading=0,x=20.989192,y=37.314334,},
--              [15]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=3.988003,y=8.362333,},
--              [16]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=-3.953195,y=12.945844,},
--              [17]={category='Fortifications',type='Windsock',shape_name='H-Windsock_RW',heading=0,x=-18.944173,y=-33.042196,},
--              [18]={category='Fortifications',type='Tent04',shape_name='M92_Tent04',heading=0,x=21.220671,y=30.247529,},
--              }
--    
function UTILS.SpawnMASHStatics(Name,Coordinate,Country,ADF,Livery,DeployHelo,MASHRadio,MASHRadioModulation,MASHCallsign,Templates)
  
  -- Basic objects table
  
  local MASHTemplates = {
    [1]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.000000,y=0.000000,},
    [2]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=0.313533,y=8.778935,},
    [3]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=16.303737,y=20.379671,},
    [4]={category='Helicopters',type='CH-47Fbl1',shape_name='none',heading=0,x=-20.047735,y=-63.166179,livery_id = "us army dark green",},
    [5]={category='Infantry',type='Soldier M4',shape_name='none',heading=0,x=26.650339,y=20.066138,},
    [6]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.432292,y=9.077099,},
    [7]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=-3.216114,},
    [8]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-25.439281,y=-3.216114,},
    [9]={category='Heliports',type='FARP_SINGLE_01',shape_name='FARP_SINGLE_01',heading=0,x=-12.717421,y=9.155603,},
    [10]={category='Fortifications',type='TACAN_beacon',shape_name='none',heading=0,x=-2.329847,y=-16.579903,},
    [11]={category='Fortifications',type='FARP Fuel Depot',shape_name='GSM Rus',heading=0,x=2.222011,y=4.487030,},
    [12]={category='Fortifications',type='APFC fuel',shape_name='M92_APFCfuel',heading=0,x=3.614927,y=0.367838,},
    [13]={category='Fortifications',type='Camouflage03',shape_name='M92_Camouflage03',heading=0,x=21.544148,y=21.998879,},
    [14]={category='Fortifications',type='Container_generator',shape_name='M92_Container_generator',heading=0,x=20.989192,y=37.314334,},
    [15]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=3.988003,y=8.362333,},
    [16]={category='Fortifications',type='FireExtinguisher02',shape_name='M92_FireExtinguisher02',heading=0,x=-3.953195,y=12.945844,},
    [17]={category='Fortifications',type='Windsock',shape_name='H-Windsock_RW',heading=0,x=-18.944173,y=-33.042196,},
    [18]={category='Fortifications',type='Tent04',shape_name='M92_Tent04',heading=0,x=21.220671,y=30.247529,},
    }

  if Templates then MASHTemplates=Templates end
  
  -- locals
  local name = Name or "Florence Nightingale"
  local positionVec2
  local positionVec3
  local ReturnStatics = {} 
  local CountryID = Country or country.id.USA
  local livery = "us army dark green"
  local MASHRadio = MASHRadio or 127.5
  local MASHRadioModulation = MASHRadioModulation or radio.modulation.AM
  local MASHCallsign = MASHCallsign or CALLSIGN.FARP.Berlin
  
  -- check for coordinate or zone  
  if type(Coordinate) == "table" then
    if Coordinate:IsInstanceOf("COORDINATE") or Coordinate:IsInstanceOf("ZONE_BASE") then
      positionVec2 = Coordinate:GetVec2()
      positionVec3 = Coordinate:GetVec3()  
    end
  else
      BASE:E("Spawn MASH - no ZONE or COORDINATE handed!")
      return
  end
  
  -- position
  local BaseX = positionVec2.x
  local BaseY = positionVec2.y
   
  -- Statics
  for id,object in pairs(MASHTemplates) do
    local NewName = string.format("%s#%3d",name,id)
    local vec2 = {x=BaseX+object.x,y=BaseY+object.y}
    local Coordinate=COORDINATE:NewFromVec2(vec2)
    local static = SPAWNSTATIC:NewFromType(object.type,object.category,CountryID)
    if object.shape_name and object.shape_name ~= "none" then
      static:InitShape(object.shape_name)
    end
    if object.category == "Helicopters" and DeployHelo == true then 
      if object.livery_id ~= nil then
        livery = object.livery_id
      end
      static:InitLivery(livery)
      local newstatic = static:SpawnFromCoordinate(Coordinate,object.heading,NewName)
      table.insert(ReturnStatics,newstatic)
    elseif object.category == "Heliports" then
      static:InitFARP(MASHCallsign,MASHRadio,MASHRadioModulation,false,false)
      local newstatic = static:SpawnFromCoordinate(Coordinate,object.heading,NewName)
      table.insert(ReturnStatics,newstatic)
    elseif object.category ~= "Helicopters" and object.category ~= "Heliports" then
      local newstatic = static:SpawnFromCoordinate(Coordinate,object.heading,NewName)
      table.insert(ReturnStatics,newstatic)
    end

  end
  
  -- Beacon
  local ADFName
  if ADF and type(ADF) == "number" then
    local ADFFreq = ADF*1000 -- KHz to Hz
    local Sound =  "l10n/DEFAULT/beacon.ogg"
    ADFName = Name .. " ADF "..tostring(ADF).."KHz"
    --BASE:I(string.format("Adding MASH Beacon %d KHz Name %s",ADF,ADFName))
    trigger.action.radioTransmission(Sound, positionVec3, 0, true, ADFFreq, 250, ADFName)
  end
  
  return ReturnStatics, ADFName
end

--- Converts a Vec2 to a Vec3.
-- @param vec the 2D vector
-- @param y optional new y axis (altitude) value. If omitted it's 0.
function UTILS.Vec2toVec3(vec,y) 
  if not vec.z then
    if vec.alt and not y then
      y = vec.alt
    elseif not y then
      y = 0
    end
    return {x = vec.x, y = y, z = vec.y}
  else
    return {x = vec.x, y = vec.y, z = vec.z}  -- it was already Vec3, actually.
  end
end

--- Get the correction needed for true north in radians
-- @param gPoint The map point vec2 or vec3
-- @return number correction
function UTILS.GetNorthCorrection(gPoint)  
  local point = UTILS.DeepCopy(gPoint)
  if not point.z then --Vec2; convert to Vec3
    point.z = point.y
    point.y = 0
  end
  local lat, lon = coord.LOtoLL(point)
  local north_posit = coord.LLtoLO(lat + 1, lon)
  return math.atan2(north_posit.z - point.z, north_posit.x - point.x)
end

--- Convert time in seconds to a DHMS table `{d = days, h = hours, m = minutes, s = seconds}`
-- @param timeInSec Time in Seconds
-- @return #table Table with DHMS data
function UTILS.GetDHMS(timeInSec)
  if timeInSec and type(timeInSec) == 'number' then
    local tbl = {d = 0, h = 0, m = 0, s = 0}
    if timeInSec > 86400 then
      while timeInSec > 86400 do
        tbl.d = tbl.d + 1
        timeInSec = timeInSec - 86400
      end
    end
    if timeInSec > 3600 then
      while timeInSec > 3600 do
        tbl.h = tbl.h + 1
        timeInSec = timeInSec - 3600
      end
    end
    if timeInSec > 60 then
      while timeInSec > 60 do
        tbl.m = tbl.m + 1
        timeInSec = timeInSec - 60
      end
    end
    tbl.s = timeInSec
    return tbl
  else
    BASE:E("No number handed!")
    return
  end
end

--- Returns heading-error corrected direction in radians.
-- True-north corrected direction from point along vector vec.
-- @param vec Vec3 Starting point
-- @param point Vec2 Direction
-- @return direction corrected direction from point.
function UTILS.GetDirectionRadians(vec, point)
  local dir = math.atan2(vec.z, vec.x)
  if point then
    dir = dir + UTILS.GetNorthCorrection(point)
  end
  if dir < 0 then
    dir = dir + 2 * math.pi -- put dir in range of 0 to 2*pi
  end
  return dir
end

--- Raycasting a point in polygon. Code from http://softsurfer.com/Archive/algorithm_0103/algorithm_0103.htm
-- @param point Vec2 or Vec3 to test
-- @param #table poly Polygon Table of Vec2/3 point forming the Polygon
-- @param #number maxalt Altitude limit (optional)
-- @param #boolean outcome 
function UTILS.IsPointInPolygon(point, poly, maxalt) 
  point = UTILS.Vec2toVec3(point)
  local px = point.x
  local pz = point.z
  local cn = 0
  local newpoly = UTILS.DeepCopy(poly)

  if not maxalt or (point.y <= maxalt) then
    local polysize = #newpoly
    newpoly[#newpoly + 1] = newpoly[1]

    newpoly[1] = UTILS.Vec2toVec3(newpoly[1])

    for k = 1, polysize do
      newpoly[k+1] = UTILS.Vec2toVec3(newpoly[k+1])
      if ((newpoly[k].z <= pz) and (newpoly[k+1].z > pz)) or ((newpoly[k].z > pz) and (newpoly[k+1].z <= pz)) then
        local vt = (pz - newpoly[k].z) / (newpoly[k+1].z - newpoly[k].z)
        if (px < newpoly[k].x + vt*(newpoly[k+1].x - newpoly[k].x)) then
          cn = cn + 1
        end
      end
    end

    return cn%2 == 1
  else
    return false
  end
end

--- Vector scalar multiplication.
-- @param vec Vec3 vector to multiply
-- @param #number mult scalar multiplicator
-- @return Vec3 new vector multiplied with the given scalar
function UTILS.ScalarMult(vec, mult)
  return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult}
end

--- Utilities weather class for fog mainly.
-- @type UTILS.Weather
UTILS.Weather = {}

--- Returns the current fog thickness in meters. Returns zero if fog is not present. 
function UTILS.Weather.GetFogThickness()
  return world.weather.getFogThickness()
end

--- Sets the fog to the desired thickness in meters at sea level. 
-- @param #number Thickness Thickness in meters.
-- Any fog animation will be discarded.
-- Valid range : 100 to 5000 meters
function UTILS.Weather.SetFogThickness(Thickness)
  local value = Thickness
  if value < 100 then value = 100
  elseif value > 5000 then  value = 5000 end
  return world.weather.setFogThickness(value)
end

--- Removes the fog.
function UTILS.Weather.RemoveFog()
  return world.weather.setFogThickness(0)
end

--- Gets the maximum visibility distance of the current fog setting.
-- Returns 0 if no fog is present. 
function UTILS.Weather.GetFogVisibilityDistanceMax()
  return world.weather.getFogVisibilityDistance()
end

--- Sets the maximum visibility at sea level in meters.
-- @param #number Thickness Thickness in meters.
-- Limit: 100 to 100000 
function UTILS.Weather.SetFogVisibilityDistance(Thickness)
  local value = Thickness
  if value < 100 then value = 100
  elseif value > 100000 then  value = 100000 end
  return world.weather.setFogVisibilityDistance(value)
end

---  Uses data from the passed table to change the fog visibility and thickness over a desired timeframe. This allows for a gradual increase/decrease of fog values rather than abruptly applying the values.
-- Animation Key Format: {time, visibility, thickness}
-- @param #table AnimationKeys Table of AnimationKey tables
-- @usage
-- Time: in seconds 0 to infinity
-- Time is relative to when the function was called. Time value for each key must be larger than the previous key. If time is set to 0 then the fog will be applied to the corresponding visibility and thickness values at that key. Any time value greater than 0 will result in the current fog being inherited and changed to the first key.
-- Visibility: in meters 100 to 100000
-- Thickness: in meters 100 to 5000
-- The speed at which the visibility and thickness changes is based on the time between keys and the values that visibility and thickness are being set to.
--
-- When the function is passed an empty table {} or nil the fog animation will be discarded and whatever the current thickness and visibility are set to will remain.
-- 
-- The following will set the fog in the mission to disappear in 1 minute.
--
--            UTILS.Weather.SetFogAnimation({ {60, 0, 0} })
--
-- The following will take 1 hour to get to the first fog setting, it will maintain that fog setting for another hour, then lightly removes the fog over the 2nd and 3rd hour, the completely removes the fog after 3 hours and 3 minutes from when the function was called.
--
--            UTILS.Weather.SetFogAnimation({
--              {3600, 10000, 3000},    -- one hour to get to that fog setting
--              {7200, 10000, 3000},    -- will maintain for 2 hours
--              {10800, 20000, 2000},   -- at 3 hours visibility will have been increased while thickness decreases slightly
--              {12600, 0, 0},          -- at 3:30 after the function was called the fog will be completely removed. 
--            })
--  
function UTILS.Weather.SetFogAnimation(AnimationKeys)
  return world.weather.setFogAnimation(AnimationKeys)
end

--- The fog animation will be discarded and whatever the current thickness and visibility are set to will remain
function UTILS.Weather.StopFogAnimation()
  return world.weather.setFogAnimation({})
end

--- Find a ME created zone by its name
function UTILS.GetEnvZone(name)
    for _,v in ipairs(env.mission.triggers.zones) do
        if v.name == name then
            return v
        end
    end
end

--- net.dostring_in
function UTILS.DoStringIn(State,DoString)
  return net.dostring_in(State,DoString)
end

--- Show a picture on the screen to all
-- @param #string FileName File name of the picture
-- @param #number Duration Duration in seconds, defaults to 10
-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false
-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0
-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right
-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom
-- @param #number Size Size of the picture in percent, defaults to 100
-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size
function UTILS.ShowPictureToAll(FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)
    ClearView = ClearView or false
    StartDelay = StartDelay or 0
    HorizontalAlign = HorizontalAlign or 1
    VerticalAlign = VerticalAlign or 1
    Size = Size or 100
    SizeUnits = SizeUnits or 0

    if ClearView then ClearView = "true" else ClearView = "false" end

    net.dostring_in("mission", string.format("a_out_picture(\"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits))
end

--- Show a picture on the screen to Coalition
-- @param #number Coalition Coalition ID, can be coalition.side.BLUE, coalition.side.RED or coalition.side.NEUTRAL
-- @param #string FileName File name of the picture
-- @param #number Duration Duration in seconds, defaults to 10
-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false
-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0
-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right
-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom
-- @param #number Size Size of the picture in percent, defaults to 100
-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size
function UTILS.ShowPictureToCoalition(Coalition, FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)
    ClearView = ClearView or false
    StartDelay = StartDelay or 0
    HorizontalAlign = HorizontalAlign or 1
    VerticalAlign = VerticalAlign or 1
    Size = Size or 100
    SizeUnits = SizeUnits or 0

    if ClearView then ClearView = "true" else ClearView = "false" end

    local coalName = string.lower(UTILS.GetCoalitionName(Coalition))

    net.dostring_in("mission", string.format("a_out_picture_s(\"%s\", \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", coalName, FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits))
end

--- Show a picture on the screen to Country
-- @param #number Country Country ID, can be country.id.USA, country.id.RUSSIA, etc.
-- @param #string FileName File name of the picture
-- @param #number Duration Duration in seconds, defaults to 10
-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false
-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0
-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right
-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom
-- @param #number Size Size of the picture in percent, defaults to 100
-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size
function UTILS.ShowPictureToCountry(Country, FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)
    ClearView = ClearView or false
    StartDelay = StartDelay or 0
    HorizontalAlign = HorizontalAlign or 1
    VerticalAlign = VerticalAlign or 1
    Size = Size or 100
    SizeUnits = SizeUnits or 0

    if ClearView then ClearView = "true" else ClearView = "false" end

    net.dostring_in("mission", string.format("a_out_picture_c(%d, \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", Country, FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits))
end

--- Show a picture on the screen to Group
-- @param Wrapper.Group#GROUP Group Group to show the picture to
-- @param #string FileName File name of the picture
-- @param #number Duration Duration in seconds, defaults to 10
-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false
-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0
-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right
-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom
-- @param #number Size Size of the picture in percent, defaults to 100
-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size
function UTILS.ShowPictureToGroup(Group, FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)
    ClearView = ClearView or false
    StartDelay = StartDelay or 0
    HorizontalAlign = HorizontalAlign or 1
    VerticalAlign = VerticalAlign or 1
    Size = Size or 100
    SizeUnits = SizeUnits or 0

    if ClearView then ClearView = "true" else ClearView = "false" end

    net.dostring_in("mission", string.format("a_out_picture_g(%d, \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", Group:GetID(), FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits))
end

--- Show a picture on the screen to Unit
-- @param Wrapper.Unit#UNIT Unit Unit to show the picture to
-- @param #string FileName File name of the picture
-- @param #number Duration Duration in seconds, defaults to 10
-- @param #boolean ClearView If true, clears the view before showing the picture, defaults to false
-- @param #number StartDelay Delay in seconds before showing the picture, defaults to 0
-- @param #number HorizontalAlign Horizontal alignment of the picture, 0: Left, 1: Center, 2: Right
-- @param #number VerticalAlign Vertical alignment of the picture, 0: Top, 1: Center, 2: Bottom
-- @param #number Size Size of the picture in percent, defaults to 100
-- @param #number SizeUnits Size units, 0 for % of original picture size, and 1 for % of window size
function UTILS.ShowPictureToUnit(Unit, FilePath, Duration, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits)
    ClearView = ClearView or false
    StartDelay = StartDelay or 0
    HorizontalAlign = HorizontalAlign or 1
    VerticalAlign = VerticalAlign or 1
    Size = Size or 100
    SizeUnits = SizeUnits or 0

    if ClearView then ClearView = "true" else ClearView = "false" end

    net.dostring_in("mission", string.format("a_out_picture_u(%d, \"%s\", %d, %s, %d, \"%d\", \"%d\", %d, \"%d\")", Unit:GetID(), FilePath, Duration or 10, ClearView, StartDelay, HorizontalAlign, VerticalAlign, Size, SizeUnits))
end

--- Load a mission file. This will replace the current mission with the one given carrying along the online clients.
-- @param #string FileName Mission filename
function UTILS.LoadMission(FileName)
    net.dostring_in("mission", string.format("a_load_mission(\"%s\")", FileName))
end

--- Set the mission briefing for a coalition.
-- @param #number Coalition Briefing coalition ID, can be coalition.side.BLUE, coalition.side.RED or coalition.side.NEUTRAL
-- @param #string Text Briefing text, can contain newlines, will be converted formatted properly for DCS
-- @param #string Picture Picture file path, can be a file in the DEFAULT folder inside the .miz
function UTILS.SetMissionBriefing(Coalition, Text, Picture)
    Text = Text or ""
    Text = Text:gsub("\n", "\\n")
    Picture = Picture or ""
    local coalName = string.lower(UTILS.GetCoalitionName(Coalition))
    net.dostring_in("mission", string.format("a_set_briefing(\"%s\", \"%s\", \"%s\")", coalName, Picture, Text))
end

--- Show a helper gate at a DCS#Vec3 position
-- @param DCS#Vec3 pos The position
-- @param #number heading Heading in degrees, can be 0..359 degrees
function UTILS.ShowHelperGate(pos, heading)
    net.dostring_in("mission",string.format("a_show_helper_gate(%s, %s, %s, %f)", pos.x, pos.y, pos.z, math.rad(heading)))
end

--- Show a helper gate for a unit.
-- @param Wrapper.Unit#UNIT Unit The unit to show the gate for
-- @param #number Flag Helper gate flag
function UTILS.ShowHelperGateForUnit(Unit, Flag)
    net.dostring_in("mission",string.format("a_show_route_gates_for_unit(%d, \"%d\")", Unit:GetID(), Flag))
end

--- Set the carrier illumination mode. -2: OFF, -1: AUTO, 0: NAVIGATION, 1: AC LAUNCH, 2: AC RECOVERY
-- @param #number UnitID Carrier unit ID ( UNIT:GetID() )
-- @param #number Mode Illumination mode, can be -2: OFF, -1: AUTO, 0: NAVIGATION, 1: AC LAUNCH, 2: AC RECOVERY
function UTILS.SetCarrierIlluminationMode(UnitID, Mode)
    net.dostring_in("mission",string.format("a_set_carrier_illumination_mode(%d, %d)", UnitID, Mode))
end

--- Shell a zone, zone must ME created
-- @param #string name The name of the ME created zone
-- @param #number power Equals kg of TNT, e.g. 75
-- @param #count Number of shells simulated
function UTILS.ShellZone(name, power, count)
    local z = UTILS.GetEnvZone(name)
    if z then
        net.dostring_in("mission",string.format("a_shelling_zone(%d, %d, %d)", z.zoneId, power, count))
    end
end

--- Remove objects from a zone, zone must ME created
-- @param #string name The name of the ME created zone
-- @param #number type Type of objects to remove can be 0:all, 1: trees, 2:objects
function UTILS.RemoveObjects(name, type)
    local z = UTILS.GetEnvZone(name)
    if z then
        net.dostring_in("mission",string.format("a_remove_scene_objects(%d, %d)", z.zoneId, type))
    end
end

--- Remove scenery objects from a zone, zone must ME created
-- @param #string name The name of the ME created zone
-- @param #number level Level of removal
function UTILS.DestroyScenery(name, level)
    local z = UTILS.GetEnvZone(name)
    if z then
        net.dostring_in("mission",string.format("a_scenery_destruction_zone(%d, %d)", z.zoneId, level))
    end
end

--- Search for clear zones in a given area. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery.
-- @param DCS##Vec3 Center position vector for the search area.
-- @param #number SearchRadius Radius of the search area.
-- @param #number PosRadius Required clear radius around each position.
-- @param #number NumPositions Number of positions to find.
-- @return #table A table of DCS#Vec2 positions that are clear of map objects within the given PosRadius.
function UTILS.GetSimpleZones(Vec3, SearchRadius, PosRadius, NumPositions)
    return Disposition.getSimpleZones(Vec3, SearchRadius, PosRadius, NumPositions)
end

--- Search for clear ground spawn zones within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery.
-- @param Core.Zone#ZONE Zone to search.
-- @param #number (Optional) PosRadius Required clear radius around each position. (Default is math.min(Radius/10, 200))
-- @param #number (Optional) NumPositions Number of positions to find. (Default 50)
-- @return #table A table of DCS#Vec2 positions that are clear of map objects within the given PosRadius. nil if no clear positions are found.
function UTILS.GetClearZonePositions(Zone, PosRadius, NumPositions)
    local radius = PosRadius or math.min(Zone:GetRadius()/10, 200)
    local clearPositions = UTILS.GetSimpleZones(Zone:GetVec3(), Zone:GetRadius(), radius, NumPositions or 50)
    if clearPositions and #clearPositions > 0 then
        local validZones = {}
        for _, vec2 in pairs(clearPositions) do
            if Zone:IsVec2InZone(vec2) then
                table.insert(validZones, vec2)
            end
        end
        if #validZones > 0 then
            return validZones, radius
        end
    end
    return nil
end


--- Search for a random clear ground spawn coordinate within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery.
-- @param Core.Zone#ZONE Zone to search.
-- @param #number PosRadius (Optional) Required clear radius around each position. (Default is math.min(Radius/10, 200))
-- @param #number NumPositions (Optional) Number of positions to find. (Default 50)
-- @return Core.Point#COORDINATE A random coordinate for a clear zone. nil if no clear positions are found.
-- @return #number Assigned radius for the found zones. nil if no clear positions are found.
function UTILS.GetRandomClearZoneCoordinate(Zone, PosRadius, NumPositions)
    local clearPositions = UTILS.GetClearZonePositions(Zone, PosRadius, NumPositions)
    if clearPositions and #clearPositions > 0 then
        local randomPosition, radius = clearPositions[math.random(1, #clearPositions)]
        return COORDINATE:NewFromVec2(randomPosition), radius
    end

    return nil
end

--- Find the point on the radius of a circle closest to a point outside of the radius.
-- @param DCS#Vec2 Vec1 Simple Vec2 marking the middle of the circle.
-- @param #number Radius The radius of the circle.
-- @param DCS#Vec2 Vec2 Simple Vec2 marking the point outside of the circle.
-- @return DCS#Vec2 Vec2 point on the radius.
function UTILS.FindNearestPointOnCircle(Vec1,Radius,Vec2)
    local r = Radius
    local cx = Vec1.x or 1
    local cy = Vec1.y or 1
    local px = Vec2.x or 1
    local py = Vec2.y or 1

    -- Berechne den Vektor vom Mittelpunkt zum externen Punkt
    local dx = px - cx
    local dy = py - cy

    -- Berechne die Länge des Vektors
    local dist = math.sqrt(dx * dx + dy * dy)

    -- Wenn der Punkt im Mittelpunkt liegt, wähle einen Punkt auf der X-Achse
    if dist == 0 then
        return {x=cx + r, y=cy}
    end

    -- Normalisiere den Vektor (richtungsweise Vektor mit Länge 1)
    local norm_dx = dx / dist
    local norm_dy = dy / dist

    -- Berechne den Punkt auf dem Rand des Kreises
    local qx = cx + r * norm_dx
    local qy = cy + r * norm_dy

    local shift_factor = 1
    qx = qx + shift_factor * norm_dx
    qy = qy + shift_factor * norm_dy

    return {x=qx, y=qy}
end

--- This function uses Disposition and other fallback logic to find better ground positions for ground units.
--- NOTE: This is not a spawn randomizer.
--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table.
--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit.
-- @param #table Positions A table of DCS#Vec2 or DCS#Vec3, can be a units table from the group template.
-- @param DCS#Vec2 Anchor (Optional) DCS#Vec2 or DCS#Vec3 as anchor point to calculate offset of the units.
-- @param #number MaxRadius (Optional) Max radius to search for valid ground locations in meters. Default is double the max radius of the units.
-- @param #number Spacing (Optional) Minimum spacing between units in meters. Default is 5% of the search radius or 5 meters, whichever is larger.
function UTILS.ValidateAndRepositionGroundUnits(Positions, Anchor, MaxRadius, Spacing)
    local units = Positions
    Anchor = Anchor or UTILS.GetCenterPoint(units)
    local gPos = { x = Anchor.x, y = Anchor.z or Anchor.y }
    local maxRadius = 0
    local unitCount = 0
    for _, unit in pairs(units) do
        local pos = { x = unit.x, y = unit.z or unit.y }
        local dist = UTILS.VecDist2D(pos, gPos)
        if dist > maxRadius then
            maxRadius = dist
        end
        unitCount = unitCount + 1
    end
    maxRadius = MaxRadius or math.max(maxRadius * 2, 10)
    local spacing = Spacing or math.max(maxRadius * 0.05, 5)
    if unitCount > 0 and maxRadius > 5 then
        local spots = UTILS.GetSimpleZones(UTILS.Vec2toVec3(gPos), maxRadius, spacing, 1000)
        if spots and #spots > 0 then
            local validSpots = {}
            for _, spot in pairs(spots) do -- Disposition sometimes returns points on roads, hence this filter.
                if land.getSurfaceType(spot) == land.SurfaceType.LAND then
                    table.insert(validSpots, spot)
                end
            end
            spots = validSpots
        end

        local step = spacing
        for _, unit in pairs(units) do
            local pos = { x = unit.x, y = unit.z or unit.y }
            local isOnLand = land.getSurfaceType(pos) == land.SurfaceType.LAND
            local isValid = false
            if spots and #spots > 0 then
                local si = 1
                local sid = 0
                local closestDist = 100000000
                local closestSpot
                for _, spot in pairs(spots) do
                    local dist = UTILS.VecDist2D(pos, spot)
                    if dist < closestDist then
                        closestDist = dist
                        closestSpot = spot
                        sid = si
                    end
                    si = si + 1
                end
                if closestSpot then
                    if closestDist >= spacing then
                        pos = closestSpot
                    end
                    isValid = true
                    table.remove(spots, sid)
                end
            end

            -- Failsafe calculation
            if not isValid and not isOnLand then

                local h = UTILS.HdgTo(pos, gPos)
                local retries = 0
                while not isValid and retries < 500 do

                    local dist = UTILS.VecDist2D(pos, gPos)
                    pos = UTILS.Vec2Translate(pos, step, h)

                    local skip = false
                    for _, unit2 in pairs(units) do
                        if unit ~= unit2 then
                            local pos2 = { x = unit2.x, y = unit2.z or unit2.y }
                            local dist2 = UTILS.VecDist2D(pos, pos2)
                            if dist2 < 12 then
                                isValid = false
                                skip = true
                                break
                            end
                        end
                    end

                    if not skip and dist > step and land.getSurfaceType(pos) == land.SurfaceType.LAND then
                        isValid = true
                        break
                    elseif dist <= step then
                        break
                    end

                    retries = retries + 1
                end
            end

            if isValid then
                unit.x = pos.x
                if unit.z then
                    unit.z = pos.y
                else
                    unit.y = pos.y
                end
            end
        end
    end
end

--- This function uses Disposition and other fallback logic to find better ground positions for statics.
--- NOTE: This is not a spawn randomizer.
--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table.
--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit.
-- @param #table Positions A table of DCS#Vec2 or DCS#Vec3, can be a units table from the group template.
-- @param DCS#Vec2 Position DCS#Vec2 or DCS#Vec3 initial spawn location.
-- @param #number MaxRadius (Optional) Max radius to search for valid ground locations in meters. Default is double the max radius of the static.
-- @return DCS#Vec2 Initial Position if it's valid, else a valid spawn position. nil if no valid position found.
function UTILS.ValidateAndRepositionStatic(Country, Category, Type, Position, ShapeName, MaxRadius)
    local coord = COORDINATE:NewFromVec2(Position)
    local st = SPAWNSTATIC:NewFromType(Type, Category, Country)
    if ShapeName then
        st:InitShape(ShapeName)
    end
    local sName = "s-"..timer.getTime().."-"..math.random(1,10000)
    local tempStatic = st:SpawnFromCoordinate(coord, 0, sName)
    if tempStatic then
        local sRadius = tempStatic:GetBoundingRadius(2) or 3
        tempStatic:Destroy()
        sRadius = sRadius * 0.5
        MaxRadius = MaxRadius or math.max(sRadius * 10, 100)
        local positions = UTILS.GetSimpleZones(coord:GetVec3(), MaxRadius, sRadius, 20)
        if positions and #positions > 0 then
            local closestSpot
            local closestDist = math.huge
            for _, spot in pairs(positions) do -- Disposition sometimes returns points on roads, hence this filter.
                if land.getSurfaceType(spot) == land.SurfaceType.LAND then
                    local dist = UTILS.VecDist2D(Position, spot)
                    if dist < closestDist then
                        closestDist = dist
                        closestSpot = spot
                    end
                end
            end

            if closestSpot then
                if closestDist >= sRadius then
                    return closestSpot
                else
                    return Position
                end
            end
        end
    end

    return nil
end
--- **Utils** - Lua Profiler.
--
-- Find out how many times functions are called and how much real time it costs.
--
-- ===
--
-- ### Author: **TAW CougarNL**, *funkyfranky*
--
-- @module Utilities.Profiler
-- @image Utils_Profiler.jpg

--- PROFILER class.
-- @type PROFILER
-- @field #string ClassName Name of the class.
-- @field #table Counters Function counters.
-- @field #table dInfo Info.
-- @field #table fTime Function time.
-- @field #table fTimeTotal Total function time.
-- @field #table eventhandler Event handler to get mission end event.
-- @field #number TstartGame Game start time timer.getTime().
-- @field #number TstartOS OS real start time os.clock.
-- @field #boolean logUnknown Log unknown functions. Default is off.
-- @field #number ThreshCPS Low calls per second threshold. Only write output if function has more calls per second than this value.
-- @field #number ThreshTtot Total time threshold. Only write output if total function CPU time is more than this value.
-- @field #string fileNamePrefix Output file name prefix, e.g. "MooseProfiler".
-- @field #string fileNameSuffix Output file name prefix, e.g. "txt"

--- *The emperor counsels simplicity.* *First principles. Of each particular thing, ask: What is it in itself, in its own constitution? What is its causal nature?*
--
-- ===
--
-- ![Banner Image](..\Presentations\Utilities\PROFILER_Main.jpg)
--
-- # The PROFILER Concept
--
-- Profile your lua code. This tells you, which functions are called very often and which consume most real time.
-- With this information you can optimize the performance of your code.
--
-- # Prerequisites
--
-- The modules **os**, **io** and **lfs** need to be de-sanitized. Comment out the lines
--
--     --sanitizeModule('os')
--     --sanitizeModule('io')
--     --sanitizeModule('lfs')
--
-- in your *"DCS World OpenBeta/Scripts/MissionScripting.lua"* file.
--
-- But be aware that these changes can make you system vulnerable to attacks.
--
-- # Disclaimer
--
-- **Profiling itself is CPU expensive!** Don't use this when you want to fly or host a mission.
--
--
-- # Start
--
-- The profiler can simply be started with the @{#PROFILER.Start}(*Delay, Duration*) function
--
--     PROFILER.Start()
--
-- The optional parameter *Delay* can be used to delay the start by a certain amount of seconds and the optional parameter *Duration* can be used to
-- stop the profiler after a certain amount of seconds.
--
-- # Stop
--
-- The profiler automatically stops when the mission ends. But it can be stopped any time with the @{#PROFILER.Stop}(*Delay*) function
--
--     PROFILER.Stop()
--
-- The optional parameter *Delay* can be used to specify a delay after which the profiler is stopped.
--
-- When the profiler is stopped, the output is written to a file.
--
-- # Output
--
-- The profiler output is written to a file in your DCS home folder
--
--     X:\User\<Your User Name>\Saved Games\DCS OpenBeta\Logs
--
-- The default file name is "MooseProfiler.txt". If that file exists, the file name is "MooseProfiler-001.txt" etc.
--
-- ## Data
--
-- The data in the output file provides information on the functions that were called in the mission.
--
-- It will tell you how many times a function was called in total, how many times per second, how much time in total and the percentage of time.
--
-- If you only want output for functions that are called more than *X* times per second, you can set
--
--     PROFILER.ThreshCPS=1.5
--
-- With this setting, only functions which are called more than 1.5 times per second are displayed. The default setting is PROFILER.ThreshCPS=0.0 (no threshold).
--
-- Furthermore, you can limit the output for functions that consumed a certain amount of CPU time in total by
--
--     PROFILER.ThreshTtot=0.005
--
-- With this setting, which is also the default, only functions which in total used more than 5 milliseconds CPU time.
--
-- @field #PROFILER
PROFILER = {
  ClassName      = "PROFILER",
  Counters       = {},
  dInfo          = {},
  fTime          = {},
  fTimeTotal     = {},
  eventHandler   = {},
  logUnknown     = false,
  ThreshCPS      = 0.0,
  ThreshTtot     = 0.005,
  fileNamePrefix = "MooseProfiler",
  fileNameSuffix = "txt"
}

--- Waypoint data.
-- @type PROFILER.Data
-- @field #string func The function name.
-- @field #string src The source file.
-- @field #number line The line number
-- @field #number count Number of function calls.
-- @field #number tm Total time in seconds.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start/Stop Profiler
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Start profiler.
-- @param #number Delay Delay in seconds before profiler is stated. Default is immediately.
-- @param #number Duration Duration in (game) seconds before the profiler is stopped. Default is when mission ends.
function PROFILER.Start( Delay, Duration )

  -- Check if os, io and lfs are available.
  local go = true
  if not os then
    env.error( "ERROR: Profiler needs os to be de-sanitized!" )
    go = false
  end
  if not io then
    env.error("ERROR: Profiler needs io to be desanitized!")
    go=false
  end
  if not lfs then
    env.error("ERROR: Profiler needs lfs to be desanitized!")
    go=false
  end
  if not go then
    return
  end

  if Delay and Delay > 0 then
    BASE:ScheduleOnce( Delay, PROFILER.Start, 0, Duration )
  else

    -- Set start time.
    PROFILER.TstartGame=timer.getTime()
    PROFILER.TstartOS=os.clock()

    -- Add event handler.
    world.addEventHandler(PROFILER.eventHandler)

    -- Info in log.
    env.info( '############################   Profiler Started   ############################' )
    if Duration then
      env.info( string.format( "- Will be running for %d seconds", Duration ) )
    else
      env.info( string.format( "- Will be stopped when mission ends" ) )
    end
    env.info(string.format("- Calls per second threshold %.3f/sec", PROFILER.ThreshCPS))
    env.info(string.format("- Total function time threshold %.3f sec", PROFILER.ThreshTtot))
    env.info(string.format("- Output file \"%s\" in your DCS log file folder", PROFILER.getfilename(PROFILER.fileNameSuffix)))
    env.info(string.format("- Output file \"%s\" in CSV format", PROFILER.getfilename("csv")))
    env.info('###############################################################################')


    -- Message on screen
    local duration=Duration or 600
    trigger.action.outText("### Profiler running ###", duration)

    -- Set hook.
    debug.sethook(PROFILER.hook, "cr")

    -- Auto stop profiler.
    if Duration then
      PROFILER.Stop( Duration )
    end

  end

end

--- Stop profiler.
-- @param #number Delay Delay before stop in seconds.
function PROFILER.Stop( Delay )

  if Delay and Delay > 0 then

    BASE:ScheduleOnce( Delay, PROFILER.Stop )
  end
end

function PROFILER.Stop(Delay)

  if Delay and Delay>0 then

    BASE:ScheduleOnce(Delay, PROFILER.Stop)

  else

    -- Remove hook.
    debug.sethook()


    -- Run time game.
    local runTimeGame=timer.getTime()-PROFILER.TstartGame

    -- Run time real OS.
    local runTimeOS=os.clock()-PROFILER.TstartOS

    -- Show info.
    PROFILER.showInfo(runTimeGame, runTimeOS)

  end

end

--- Event handler.
function PROFILER.eventHandler:onEvent( event )
  if event.id == world.event.S_EVENT_MISSION_END then
    PROFILER.Stop()
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Hook
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Debug hook.
-- @param #table event Event.
function PROFILER.hook(event)

  local f=debug.getinfo(2, "f").func

  if event=='call' then

    if PROFILER.Counters[f]==nil then

      PROFILER.Counters[f]=1
      PROFILER.dInfo[f]=debug.getinfo(2,"Sn")

      if PROFILER.fTimeTotal[f]==nil then
        PROFILER.fTimeTotal[f]=0
      end

    else
      PROFILER.Counters[f] = PROFILER.Counters[f] + 1
    end

    if PROFILER.fTime[f]==nil then
      PROFILER.fTime[f]=os.clock()
    end

  elseif (event=='return') then

    if PROFILER.fTime[f]~=nil then
      PROFILER.fTimeTotal[f]=PROFILER.fTimeTotal[f]+(os.clock()-PROFILER.fTime[f])
      PROFILER.fTime[f]=nil
    end

  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Data
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get data.
-- @param #function func Function.
-- @return #string Function name.
-- @return #string Source file name.
-- @return #string Line number.
-- @return #number Function time in seconds.
function PROFILER.getData( func )

  local n=PROFILER.dInfo[func]

  if n.what=="C" then
    return n.name, "?", "?", PROFILER.fTimeTotal[func]
  end

  return n.name, n.short_src, n.linedefined, PROFILER.fTimeTotal[func]
end

--- Write text to log file.
-- @param #function f The file.
-- @param #string txt The text.
function PROFILER._flog( f, txt )
  f:write( txt .. "\r\n" )
end

--- Show table.
-- @param #table data Data table.
-- @param #function f The file.
-- @param #number runTimeGame Game run time in seconds.
function PROFILER.showTable( data, f, runTimeGame )

  -- Loop over data.
  for i=1, #data do
    local t=data[i] --#PROFILER.Data

    -- Calls per second.
    local cps=t.count/runTimeGame

    local threshCPS=cps>=PROFILER.ThreshCPS
    local threshTot=t.tm>=PROFILER.ThreshTtot

    if threshCPS and threshTot then

      -- Output
      local text=string.format("%30s: %8d calls %8.1f/sec - Time Total %8.3f sec (%.3f %%) %5.3f sec/call  %s line %s", t.func, t.count, cps, t.tm, t.tm/runTimeGame*100, t.tm/t.count, tostring(t.src), tostring(t.line))
      PROFILER._flog(f, text)

    end
  end

end

--- Print csv file.
-- @param #table data Data table.
-- @param #number runTimeGame Game run time in seconds.
function PROFILER.printCSV( data, runTimeGame )

  -- Output file.
  local file = PROFILER.getfilename( "csv" )
  local g = io.open( file, 'w' )

  -- Header.
  local text="Function,Total Calls,Calls per Sec,Total Time,Total in %,Sec per Call,Source File;Line Number,"
  g:write(text.."\r\n")

  -- Loop over data.
  for i=1, #data do
    local t=data[i] --#PROFILER.Data

    -- Calls per second.
    local cps = t.count / runTimeGame

    -- Output
    local txt=string.format("%s,%d,%.1f,%.3f,%.3f,%.3f,%s,%s,", t.func, t.count, cps, t.tm, t.tm/runTimeGame*100, t.tm/t.count, tostring(t.src), tostring(t.line))
    g:write(txt.."\r\n")

  end

  -- Close file.
  g:close()
end

--- Write info to output file.
-- @param #string ext Extension.
-- @return #string File name.
function PROFILER.getfilename(ext)

  local dir=lfs.writedir()..[[Logs\]]

  ext=ext or PROFILER.fileNameSuffix

  local file=dir..PROFILER.fileNamePrefix.."."..ext

  if not UTILS.FileExists(file) then
    return file
  end

  for i = 1, 999 do

    local file = string.format( "%s%s-%03d.%s", dir, PROFILER.fileNamePrefix, i, ext )

    if not UTILS.FileExists( file ) then
      return file
    end

  end

end

--- Write info to output file.
-- @param #number runTimeGame Game time in seconds.
-- @param #number runTimeOS OS time in seconds.
function PROFILER.showInfo( runTimeGame, runTimeOS )

  -- Output file.
  local file=PROFILER.getfilename(PROFILER.fileNameSuffix)
  local f=io.open(file, 'w')

  -- Gather data.
  local Ttot=0
  local Calls=0

  local t={}

  local tcopy=nil --#PROFILER.Data
  local tserialize=nil --#PROFILER.Data
  local tforgen=nil --#PROFILER.Data
  local tpairs=nil --#PROFILER.Data


  for func, count in pairs(PROFILER.Counters) do

    local s,src,line,tm=PROFILER.getData(func)

    if PROFILER.logUnknown==true then
      if s==nil then s="<Unknown>" end
    end

    if s~=nil then

      -- Profile data.
      local T=
      { func=s,
        src=src,
        line=line,
        count=count,
        tm=tm,
      } --#PROFILER.Data

      -- Collect special cases. Somehow, e.g. "_copy" appears multiple times so we try to gather all data.
      if s == "_copy" then
        if tcopy == nil then
          tcopy = T
        else
          tcopy.count = tcopy.count + T.count
          tcopy.tm = tcopy.tm + T.tm
        end
      elseif s == "_Serialize" then
        if tserialize == nil then
          tserialize = T
        else
          tserialize.count=tserialize.count+T.count
          tserialize.tm=tserialize.tm+T.tm
        end
      elseif s=="(for generator)" then
        if tforgen==nil then
          tforgen=T
        else
          tforgen.count=tforgen.count+T.count
          tforgen.tm=tforgen.tm+T.tm
        end
      elseif s=="pairs" then
        if tpairs==nil then
          tpairs=T
        else
          tpairs.count=tpairs.count+T.count
          tpairs.tm=tpairs.tm+T.tm
        end
      else
        table.insert( t, T )
      end

      -- Total function time.
      Ttot=Ttot+tm

      -- Total number of calls.
      Calls=Calls+count

    end

  end

  -- Add special cases.
  if tcopy then
    table.insert( t, tcopy )
  end
  if tserialize then
    table.insert(t, tserialize)
  end
  if tforgen then
    table.insert( t, tforgen )
  end
  if tpairs then
    table.insert(t, tpairs)
  end

  env.info('############################   Profiler Stopped   ############################')
  env.info(string.format("* Runtime Game     : %s = %d sec", UTILS.SecondsToClock(runTimeGame, true), runTimeGame))
  env.info(string.format("* Runtime Real     : %s = %d sec", UTILS.SecondsToClock(runTimeOS, true), runTimeOS))
  env.info(string.format("* Function time    : %s = %.1f sec (%.1f percent of runtime game)", UTILS.SecondsToClock(Ttot, true), Ttot, Ttot/runTimeGame*100))
  env.info(string.format("* Total functions  : %d", #t))
  env.info(string.format("* Total func calls : %d", Calls))
  env.info(string.format("* Writing to file  : \"%s\"", file))
  env.info(string.format("* Writing to file  : \"%s\"", PROFILER.getfilename("csv")))
  env.info("##############################################################################")

  -- Sort by total time.
  table.sort(t, function(a,b) return a.tm>b.tm end)

  -- Write data.
  PROFILER._flog(f,"")
  PROFILER._flog(f,"************************************************************************************************************************")
  PROFILER._flog(f,"************************************************************************************************************************")
  PROFILER._flog(f,"************************************************************************************************************************")
  PROFILER._flog(f,"")
  PROFILER._flog(f,"-------------------------")
  PROFILER._flog(f,"---- Profiler Report ----")
  PROFILER._flog(f,"-------------------------")
  PROFILER._flog(f,"")
  PROFILER._flog(f,string.format("* Runtime Game     : %s = %.1f sec", UTILS.SecondsToClock(runTimeGame, true), runTimeGame))
  PROFILER._flog(f,string.format("* Runtime Real     : %s = %.1f sec", UTILS.SecondsToClock(runTimeOS, true), runTimeOS))
  PROFILER._flog(f,string.format("* Function time    : %s = %.1f sec (%.1f %% of runtime game)", UTILS.SecondsToClock(Ttot, true), Ttot, Ttot/runTimeGame*100))
  PROFILER._flog(f,"")
  PROFILER._flog(f,string.format("* Total functions  = %d", #t))
  PROFILER._flog(f,string.format("* Total func calls = %d", Calls))
  PROFILER._flog(f,"")
  PROFILER._flog(f,string.format("* Calls per second threshold = %.3f/sec", PROFILER.ThreshCPS))
  PROFILER._flog(f,string.format("* Total func time threshold  = %.3f sec", PROFILER.ThreshTtot))
  PROFILER._flog(f,"")
  PROFILER._flog(f,"************************************************************************************************************************")
  PROFILER._flog(f,"")
  PROFILER.showTable(t, f, runTimeGame)

  -- Sort by number of calls.
  table.sort(t, function(a,b) return a.tm/a.count>b.tm/b.count end)

  -- Detailed data.
  PROFILER._flog(f,"")
  PROFILER._flog(f,"************************************************************************************************************************")
  PROFILER._flog(f,"")
  PROFILER._flog(f,"--------------------------------------")
  PROFILER._flog(f,"---- Data Sorted by Time per Call ----")
  PROFILER._flog(f,"--------------------------------------")
  PROFILER._flog(f,"")
  PROFILER.showTable(t, f, runTimeGame)

  -- Sort by number of calls.
  table.sort(t, function(a,b) return a.count>b.count end)

  -- Detailed data.
  PROFILER._flog(f,"")
  PROFILER._flog(f,"************************************************************************************************************************")
  PROFILER._flog(f,"")
  PROFILER._flog(f,"------------------------------------")
  PROFILER._flog(f,"---- Data Sorted by Total Calls ----")
  PROFILER._flog(f,"------------------------------------")
  PROFILER._flog(f,"")
  PROFILER.showTable(t, f, runTimeGame)

  -- Closing.
  PROFILER._flog( f, "" )
  PROFILER._flog( f, "************************************************************************************************************************" )
  PROFILER._flog( f, "************************************************************************************************************************" )
  PROFILER._flog( f, "************************************************************************************************************************" )
  -- Close file.
  f:close()

  -- Print csv file.
  PROFILER.printCSV( t, runTimeGame )
end
---  **UTILS** - Classic FiFo Stack.
--
-- ===
--
-- ## Main Features:
--
--    * Build a simple multi-purpose FiFo (First-In, First-Out) stack for generic data.
--    * [Wikipedia](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)
--
-- ===
--
-- ### Author: **applevangelist**
-- @module Utilities.FiFo
-- @image MOOSE.JPG

-- Date: April 2022

do
--- FIFO class.
-- @type FIFO
-- @field #string ClassName Name of the class.
-- @field #string lid Class id string for output to DCS log file.
-- @field #string version Version of FiFo.
-- @field #number counter Counter.
-- @field #number pointer Pointer.
-- @field #table stackbypointer Stack by pointer.
-- @field #table stackbyid Stack by ID.
-- @extends Core.Base#BASE

---
-- @type FIFO.IDEntry
-- @field #number pointer
-- @field #table data
-- @field #table uniqueID

---
-- @field #FIFO
FIFO = {
  ClassName = "FIFO",
  lid = "",
  version = "0.0.5",
  counter = 0,
  pointer = 0,
  stackbypointer = {},
  stackbyid = {}
}

--- Instantiate a new FIFO Stack.
-- @param #FIFO self
-- @return #FIFO self
function FIFO:New()
  -- Inherit everything from BASE class.
  local self=BASE:Inherit(self, BASE:New()) --#FIFO
  self.pointer = 0
  self.counter = 0
  self.stackbypointer = {}
  self.stackbyid = {}
  self.uniquecounter = 0
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", "FiFo", self.version)
  self:T(self.lid .."Created.") 
  return self
end

--- Empty FIFO Stack.
-- @param #FIFO self
-- @return #FIFO self
function FIFO:Clear()
  self:T(self.lid.."Clear")
  self.pointer = 0
  self.counter = 0
  self.stackbypointer = nil
  self.stackbyid = nil
  self.stackbypointer = {}
  self.stackbyid = {}
  self.uniquecounter = 0
  return self
end

--- FIFO Push Object to Stack.
-- @param #FIFO self
-- @param #table Object
-- @param #string UniqueID (optional) - will default to current pointer + 1. Note - if you intend to use `FIFO:GetIDStackSorted()` keep the UniqueID numerical!
-- @return #FIFO self
function FIFO:Push(Object,UniqueID)
  self:T(self.lid.."Push")
  self:T({Object,UniqueID})
  self.pointer = self.pointer + 1 
  self.counter = self.counter + 1
  local uniID = UniqueID
  if not UniqueID then
    self.uniquecounter = self.uniquecounter + 1
    uniID = self.uniquecounter
  end
  self.stackbyid[uniID] = { pointer = self.pointer, data = Object, uniqueID = uniID }
  self.stackbypointer[self.pointer] = { pointer = self.pointer, data = Object, uniqueID = uniID }
  return self
end

--- FIFO Pull Object from Stack.
-- @param #FIFO self
-- @return #table Object or nil if stack is empty
function FIFO:Pull()
  self:T(self.lid.."Pull")
  if self.counter == 0 then return nil end
  --local object = self.stackbypointer[self.pointer].data
  --self.stackbypointer[self.pointer] = nil
  local object = self.stackbypointer[1].data
  self.stackbypointer[1] = nil
  self.counter = self.counter - 1
  --self.pointer = self.pointer - 1
  self:Flatten()
  return object
end

--- FIFO Pull Object from Stack by Pointer
-- @param #FIFO self
-- @param #number Pointer
-- @return #table Object or nil if stack is empty
function FIFO:PullByPointer(Pointer)
  self:T(self.lid.."PullByPointer " .. tostring(Pointer))
  if self.counter == 0 then return nil end
  local object = self.stackbypointer[Pointer] -- #FIFO.IDEntry
  self.stackbypointer[Pointer] = nil
  if object then self.stackbyid[object.uniqueID] = nil end
  self.counter = self.counter - 1
  self:Flatten()
  if object then
    return object.data
  else
    return nil
  end
end


--- FIFO Read, not Pull, Object from Stack by Pointer
-- @param #FIFO self
-- @param #number Pointer
-- @return #table Object or nil if stack is empty or pointer does not exist
function FIFO:ReadByPointer(Pointer)
  self:T(self.lid.."ReadByPointer " .. tostring(Pointer))
  if self.counter == 0 or not Pointer or not self.stackbypointer[Pointer]  then return nil end
  local object = self.stackbypointer[Pointer] -- #FIFO.IDEntry
  if object then
    return object.data
  else
    return nil
  end
end

--- FIFO Read, not Pull, Object from Stack by UniqueID
-- @param #FIFO self
-- @param #number UniqueID
-- @return #table Object data or nil if stack is empty or ID does not exist
function FIFO:ReadByID(UniqueID)
  self:T(self.lid.."ReadByID " .. tostring(UniqueID))
  if self.counter == 0 or not UniqueID or not self.stackbyid[UniqueID]  then return nil end
  local object = self.stackbyid[UniqueID] -- #FIFO.IDEntry
  if object then
    return object.data
  else
    return nil
  end
end

--- FIFO Pull Object from Stack by UniqueID
-- @param #FIFO self
-- @param #tableUniqueID
-- @return #table Object or nil if stack is empty
function FIFO:PullByID(UniqueID)
  self:T(self.lid.."PullByID " .. tostring(UniqueID))
  if self.counter == 0 then return nil end
  local object = self.stackbyid[UniqueID] -- #FIFO.IDEntry
  --self.stackbyid[UniqueID] = nil
  if object then
    return self:PullByPointer(object.pointer)
  else
    return nil
  end
end

--- FIFO Housekeeping
-- @param #FIFO self
-- @return #FIFO self
function FIFO:Flatten()
  self:T(self.lid.."Flatten")
  -- rebuild stacks
  local pointerstack = {}
  local idstack = {}
  local counter = 0
  for _ID,_entry in pairs(self.stackbypointer) do
    counter = counter + 1
    pointerstack[counter] = { pointer = counter, data = _entry.data, uniqueID = _entry.uniqueID}
  end
  for _ID,_entry in pairs(pointerstack) do
      idstack[_entry.uniqueID] = { pointer = _entry.pointer , data = _entry.data, uniqueID = _entry.uniqueID}
  end
  self.stackbypointer = nil
  self.stackbypointer = pointerstack
  self.stackbyid = nil
  self.stackbyid = idstack
  self.counter = counter
  self.pointer = counter
  return self
end

--- FIFO Check Stack is empty
-- @param #FIFO self
-- @return #boolean empty
function FIFO:IsEmpty()
  self:T(self.lid.."IsEmpty")
  return self.counter == 0 and true or false
end

--- FIFO Get stack size
-- @param #FIFO self
-- @return #number size
function FIFO:GetSize()
  self:T(self.lid.."GetSize")
  return self.counter
end

--- FIFO Get stack size
-- @param #FIFO self
-- @return #number size
function FIFO:Count()
  self:T(self.lid.."Count")
  return self.counter
end

--- FIFO Check Stack is NOT empty
-- @param #FIFO self
-- @return #boolean notempty
function FIFO:IsNotEmpty()
  self:T(self.lid.."IsNotEmpty")
  return not self:IsEmpty()
end

--- FIFO Get the data stack by pointer
-- @param #FIFO self
-- @return #table Table of #FIFO.IDEntry entries
function FIFO:GetPointerStack()
  self:T(self.lid.."GetPointerStack")
  return self.stackbypointer
end

--- FIFO Check if a certain UniqeID exists
-- @param #FIFO self
-- @return #boolean exists
function FIFO:HasUniqueID(UniqueID)
  self:T(self.lid.."HasUniqueID")
  if self.stackbyid[UniqueID] ~= nil then
    return true
  else
    return false
  end
end

--- FIFO Get the data stack by UniqueID
-- @param #FIFO self
-- @return #table Table of #FIFO.IDEntry entries
function FIFO:GetIDStack()
  self:T(self.lid.."GetIDStack")
  return self.stackbyid
end

--- FIFO Get table of UniqueIDs sorted smallest to largest
-- @param #FIFO self
-- @return #table Table with index [1] to [n] of UniqueID entries
function FIFO:GetIDStackSorted()
  self:T(self.lid.."GetIDStackSorted")
  
  local stack = self:GetIDStack()
  local idstack = {}
  for _id,_entry in pairs(stack) do
    idstack[#idstack+1] = _id
    
    self:T({"pre",_id})
  end
  
  local function sortID(a, b)
      return a < b
  end
  
  table.sort(idstack)
 
  return idstack
end

--- FIFO Get table of data entries
-- @param #FIFO self
-- @return #table Raw table indexed [1] to [n] of object entries - might be empty!
function FIFO:GetDataTable()
  self:T(self.lid.."GetDataTable")
  local datatable = {}
  for _,_entry in pairs(self.stackbypointer) do
    datatable[#datatable+1] = _entry.data
  end
  return datatable
end

--- FIFO Get sorted table of data entries by UniqueIDs (must be numerical UniqueIDs only!)
-- @param #FIFO self
-- @return #table Table indexed [1] to [n] of sorted object entries - might be empty!
function FIFO:GetSortedDataTable()
  self:T(self.lid.."GetSortedDataTable")
  local datatable = {}
  local idtablesorted = self:GetIDStackSorted()
  for _,_entry in pairs(idtablesorted) do
    datatable[#datatable+1] = self:ReadByID(_entry)
  end
  return datatable
end

--- Iterate the FIFO and call an iterator function for the given FIFO data, providing the object for each element of the stack and optional parameters.
-- @param #FIFO self
-- @param #function IteratorFunction The function that will be called.
-- @param #table Arg (Optional) Further Arguments of the IteratorFunction.
-- @param #function Function (Optional) A function returning a #boolean true/false. Only if true, the IteratorFunction is called.
-- @param #table FunctionArguments (Optional) Function arguments.
-- @return #FIFO self
function FIFO:ForEach( IteratorFunction, Arg, Function, FunctionArguments )
  self:T(self.lid.."ForEach")

  local Set = self:GetPointerStack() or {}
  Arg = Arg or {}

  local function CoRoutine()
    local Count = 0
    for ObjectID, ObjectData in pairs( Set ) do
      local Object = ObjectData.data
        self:T( {Object} )
        if Function then
          if Function( unpack( FunctionArguments or {} ), Object ) == true then
            IteratorFunction( Object, unpack( Arg ) )
          end
        else
          IteratorFunction( Object, unpack( Arg ) )
        end
        Count = Count + 1
    end
    return true
  end

  local co = CoRoutine

  local function Schedule()

    local status, res = co()
    self:T( { status, res } )

    if status == false then
      error( res )
    end
    if res == false then
      return true -- resume next time the loop
    end

    return false
  end

  Schedule()

  return self
end
   
--- FIFO Print stacks to dcs.log
-- @param #FIFO self
-- @return #FIFO self
function FIFO:Flush()
  self:T(self.lid.."FiFo Flush")
  self:I("FIFO Flushing Stack by Pointer")
  for _id,_data in pairs (self.stackbypointer) do
    local data = _data -- #FIFO.IDEntry
    self:I(string.format("Pointer: %s | Entry: Number = %s Data = %s UniqueID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID)))
  end
  self:I("FIFO Flushing Stack by ID")
  for _id,_data in pairs (self.stackbyid) do
    local data = _data -- #FIFO.IDEntry
    self:I(string.format("ID: %s | Entry: Number = %s Data = %s UniqueID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID)))
  end
  self:I("Counter = " .. self.counter)
  self:I("Pointer = ".. self.pointer)
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- End FIFO
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- LIFO
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

do
--- **UTILS** - LiFo Stack.
--
-- **Main Features:**
--
--    * Build a simple multi-purpose LiFo (Last-In, First-Out) stack for generic data.
--
-- ===
--
-- ### Author: **applevangelist**

--- LIFO class.
-- @type LIFO
-- @field #string ClassName Name of the class.
-- @field #string lid Class id string for output to DCS log file.
-- @field #string version Version of LiFo
-- @field #number counter
-- @field #number pointer
-- @field #table stackbypointer
-- @field #table stackbyid
-- @extends Core.Base#BASE

---
-- @type LIFO.IDEntry
-- @field #number pointer
-- @field #table data
-- @field #table uniqueID

---
-- @field #LIFO
LIFO = {
  ClassName = "LIFO",
  lid = "",
  version = "0.0.5",
  counter = 0,
  pointer = 0,
  stackbypointer = {},
  stackbyid = {}
}

--- Instantiate a new LIFO Stack
-- @param #LIFO self
-- @return #LIFO self
function LIFO:New()
  -- Inherit everything from BASE class.
  local self=BASE:Inherit(self, BASE:New())
  self.pointer = 0
  self.counter = 0
  self.uniquecounter = 0
  self.stackbypointer = {}
  self.stackbyid = {}
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", "LiFo", self.version)
  self:T(self.lid .."Created.") 
  return self
end

--- Empty LIFO Stack
-- @param #LIFO self
-- @return #LIFO self
function LIFO:Clear()
  self:T(self.lid.."Clear")
  self.pointer = 0
  self.counter = 0
  self.stackbypointer = nil
  self.stackbyid = nil
  self.stackbypointer = {}
  self.stackbyid = {}
  self.uniquecounter = 0
  return self
end

--- LIFO Push Object to Stack
-- @param #LIFO self
-- @param #table Object
-- @param #string UniqueID (optional) - will default to current pointer + 1
-- @return #LIFO self
function LIFO:Push(Object,UniqueID)
  self:T(self.lid.."Push")
  self:T({Object,UniqueID})
  self.pointer = self.pointer + 1 
  self.counter = self.counter + 1
  local uniID = UniqueID
  if not UniqueID then
    self.uniquecounter = self.uniquecounter + 1
    uniID = self.uniquecounter
  end
  self.stackbyid[uniID] = { pointer = self.pointer, data = Object, uniqueID = uniID }
  self.stackbypointer[self.pointer] = { pointer = self.pointer, data = Object, uniqueID = uniID }
  return self
end

--- LIFO Pull Object from Stack
-- @param #LIFO self
-- @return #table Object or nil if stack is empty
function LIFO:Pull()
  self:T(self.lid.."Pull")
  if self.counter == 0 then return nil end
  local object = self.stackbypointer[self.pointer].data
  self.stackbypointer[self.pointer] = nil
  --local object = self.stackbypointer[1].data
  --self.stackbypointer[1] = nil
  self.counter = self.counter - 1
  self.pointer = self.pointer - 1
  self:Flatten()
  return object
end

--- LIFO Pull Object from Stack by Pointer
-- @param #LIFO self
-- @param #number Pointer
-- @return #table Object or nil if stack is empty
function LIFO:PullByPointer(Pointer)
  self:T(self.lid.."PullByPointer " .. tostring(Pointer))
  if self.counter == 0 then return nil end
  local object = self.stackbypointer[Pointer] -- #FIFO.IDEntry
  self.stackbypointer[Pointer] = nil
  if object then self.stackbyid[object.uniqueID] = nil end
  self.counter = self.counter - 1
  self:Flatten()
  if object then
    return object.data
  else
    return nil
  end
end

--- LIFO Read, not Pull, Object from Stack by Pointer
-- @param #LIFO self
-- @param #number Pointer
-- @return #table Object or nil if stack is empty or pointer does not exist
function LIFO:ReadByPointer(Pointer)
  self:T(self.lid.."ReadByPointer " .. tostring(Pointer))
  if self.counter == 0 or not Pointer or not self.stackbypointer[Pointer]  then return nil end
  local object = self.stackbypointer[Pointer] -- #LIFO.IDEntry
  if object then
    return object.data
  else
    return nil
  end
end

--- LIFO Read, not Pull, Object from Stack by UniqueID
-- @param #LIFO self
-- @param #number UniqueID
-- @return #table Object or nil if stack is empty or ID does not exist
function LIFO:ReadByID(UniqueID)
  self:T(self.lid.."ReadByID " .. tostring(UniqueID))
  if self.counter == 0 or not UniqueID or not self.stackbyid[UniqueID]  then return nil end
  local object = self.stackbyid[UniqueID] -- #LIFO.IDEntry
  if object then
    return object.data
  else
    return nil
  end
end

--- LIFO Pull Object from Stack by UniqueID
-- @param #LIFO self
-- @param #tableUniqueID
-- @return #table Object or nil if stack is empty
function LIFO:PullByID(UniqueID)
  self:T(self.lid.."PullByID " .. tostring(UniqueID))
  if self.counter == 0 then return nil end
  local object = self.stackbyid[UniqueID] -- #LIFO.IDEntry
  --self.stackbyid[UniqueID] = nil
  if object then
    return self:PullByPointer(object.pointer)
  else
    return nil
  end
end

--- LIFO Housekeeping
-- @param #LIFO self
-- @return #LIFO self
function LIFO:Flatten()
  self:T(self.lid.."Flatten")
  -- rebuild stacks
  local pointerstack = {}
  local idstack = {}
  local counter = 0
  for _ID,_entry in pairs(self.stackbypointer) do
    counter = counter + 1
    pointerstack[counter] = { pointer = counter, data = _entry.data, uniqueID = _entry.uniqueID}
  end
  for _ID,_entry in pairs(pointerstack) do
      idstack[_entry.uniqueID] = { pointer = _entry.pointer , data = _entry.data, uniqueID = _entry.uniqueID}
  end
  self.stackbypointer = nil
  self.stackbypointer = pointerstack
  self.stackbyid = nil
  self.stackbyid = idstack
  self.counter = counter
  self.pointer = counter
  return self
end

--- LIFO Check Stack is empty
-- @param #LIFO self
-- @return #boolean empty
function LIFO:IsEmpty()
  self:T(self.lid.."IsEmpty")
  return self.counter == 0 and true or false
end

--- LIFO Get stack size
-- @param #LIFO self
-- @return #number size
function LIFO:GetSize()
  self:T(self.lid.."GetSize")
  return self.counter
end

--- LIFO Get stack size
-- @param #LIFO self
-- @return #number size
function LIFO:Count()
  self:T(self.lid.."Count")
  return self.counter
end

--- LIFO Check Stack is NOT empty
-- @param #LIFO self
-- @return #boolean notempty
function LIFO:IsNotEmpty()
  self:T(self.lid.."IsNotEmpty")
  return not self:IsEmpty()
end

--- LIFO Get the data stack by pointer
-- @param #LIFO self
-- @return #table Table of #LIFO.IDEntry entries
function LIFO:GetPointerStack()
  self:T(self.lid.."GetPointerStack")
  return self.stackbypointer
end

--- LIFO Get the data stack by UniqueID
-- @param #LIFO self
-- @return #table Table of #LIFO.IDEntry entries
function LIFO:GetIDStack()
  self:T(self.lid.."GetIDStack")
  return self.stackbyid
end

--- LIFO Get table of UniqueIDs sorted smallest to largest
-- @param #LIFO self
-- @return #table Table of #LIFO.IDEntry entries
function LIFO:GetIDStackSorted()
  self:T(self.lid.."GetIDStackSorted")
  
  local stack = self:GetIDStack()
  local idstack = {}
  for _id,_entry in pairs(stack) do
    idstack[#idstack+1] = _id
    
    self:T({"pre",_id})
  end
  
  local function sortID(a, b)
      return a < b
  end
  
  table.sort(idstack)
 
  return idstack
end

--- LIFO Check if a certain UniqeID exists
-- @param #LIFO self
-- @return #boolean exists
function LIFO:HasUniqueID(UniqueID)
  self:T(self.lid.."HasUniqueID")
  return  self.stackbyid[UniqueID] and true or false
end

--- LIFO Print stacks to dcs.log
-- @param #LIFO self
-- @return #LIFO self
function LIFO:Flush()
  self:T(self.lid.."FiFo Flush")
  self:I("LIFO Flushing Stack by Pointer")
  for _id,_data in pairs (self.stackbypointer) do
    local data = _data -- #LIFO.IDEntry
    self:I(string.format("Pointer: %s | Entry: Number = %s Data = %s UniqueID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID)))
  end
  self:I("LIFO Flushing Stack by ID")
  for _id,_data in pairs (self.stackbyid) do
    local data = _data -- #LIFO.IDEntry
    self:I(string.format("ID: %s | Entry: Number = %s Data = %s UniqueID = %s",tostring(_id),tostring(data.pointer),tostring(data.data),tostring(data.uniqueID)))
  end
  self:I("Counter = " .. self.counter)
  self:I("Pointer = ".. self.pointer)
  return self
end

--- LIFO Get table of data entries
-- @param #LIFO self
-- @return #table Raw table indexed [1] to [n] of object entries - might be empty!
function LIFO:GetDataTable()
  self:T(self.lid.."GetDataTable")
  local datatable = {}
  for _,_entry in pairs(self.stackbypointer) do
    datatable[#datatable+1] = _entry.data
  end
  return datatable
end

--- LIFO Get sorted table of data entries by UniqueIDs (must be numerical UniqueIDs only!)
-- @param #LIFO self
-- @return #table Table indexed [1] to [n] of sorted object entries - might be empty!
function LIFO:GetSortedDataTable()
  self:T(self.lid.."GetSortedDataTable")
  local datatable = {}
  local idtablesorted = self:GetIDStackSorted()
  for _,_entry in pairs(idtablesorted) do
    datatable[#datatable+1] = self:ReadByID(_entry)
  end
  return datatable
end

--- Iterate the LIFO and call an iterator function for the given LIFO data, providing the object for each element of the stack and optional parameters.
-- @param #LIFO self
-- @param #function IteratorFunction The function that will be called.
-- @param #table Arg (Optional) Further Arguments of the IteratorFunction.
-- @param #function Function (Optional) A function returning a #boolean true/false. Only if true, the IteratorFunction is called.
-- @param #table FunctionArguments (Optional) Function arguments.
-- @return #LIFO self
function LIFO:ForEach( IteratorFunction, Arg, Function, FunctionArguments )
  self:T(self.lid.."ForEach")

  local Set = self:GetPointerStack() or {}
  Arg = Arg or {}

  local function CoRoutine()
    local Count = 0
    for ObjectID, ObjectData in pairs( Set ) do
      local Object = ObjectData.data
        self:T( {Object} )
        if Function then
          if Function( unpack( FunctionArguments or {} ), Object ) == true then
            IteratorFunction( Object, unpack( Arg ) )
          end
        else
          IteratorFunction( Object, unpack( Arg ) )
        end
        Count = Count + 1
    end
    return true
  end

  local co = CoRoutine

  local function Schedule()

    local status, res = co()
    self:T( { status, res } )

    if status == false then
      error( res )
    end
    if res == false then
      return true -- resume next time the loop
    end

    return false
  end

  Schedule()

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- End LIFO
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
end--- **Utilities** - Socket.
--
-- **Main Features:**
--
--    * Creates UDP Sockets
--    * Send messages to Discord
--    * Compatible with [FunkMan](https://github.com/funkyfranky/FunkMan)
--    * Compatible with [DCSServerBot](https://github.com/Special-K-s-Flightsim-Bots/DCSServerBot)
--
-- ===
--
-- ### Author: **funkyfranky**
-- @module Utilities.Socket
-- @image MOOSE.JPG


--- SOCKET class.
-- @type SOCKET
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field #table socket The socket.
-- @field #number port The port.
-- @field #string host The host.
-- @field #table json JSON.
-- @extends Core.Fsm#FSM

--- **At times I feel like a socket that remembers its tooth.** -- Saul Bellow
--
-- ===
--
-- # The SOCKET Concept
-- 
-- Create a UDP socket server. It enables you to send messages to discord servers via discord bots.
-- 
-- **Note** that you have to **de-sanitize** `require` and `package` in your `MissionScripting.lua` file, which is in your `DCS/Scripts` folder.
--
--
-- @field #SOCKET
SOCKET = {
  ClassName      = "SOCKET",
  verbose        =     0,
  lid            =   nil,
}

--- Data type. This is the keyword the socket listener uses.
-- @type SOCKET.DataType
-- @field #string TEXT Plain text.
-- @field #string BOMBRESULT Range bombing.
-- @field #string STRAFERESULT Range strafeing result.
-- @field #string LSOGRADE Airboss LSO grade.
-- @field #string TTS Text-To-Speech.
SOCKET.DataType={
  TEXT="moose_text",
  BOMBRESULT="moose_bomb_result",
  STRAFERESULT="moose_strafe_result",
  LSOGRADE="moose_lso_grade",
  TTS="moose_text2speech"
}


--- SOCKET class version.
-- @field #string version
SOCKET.version="0.3.0"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: A lot!
-- TODO: Messages as spoiler.
-- TODO: Send images?

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new SOCKET object.
-- @param #SOCKET self
-- @param #number Port UDP port. Default `10042`.
-- @param #string Host Host. Default `"127.0.0.1"`.
-- @return #SOCKET self
function SOCKET:New(Port, Host)

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) --#SOCKET
  
  package.path  = package.path..";.\\LuaSocket\\?.lua;"
  package.cpath = package.cpath..";.\\LuaSocket\\?.dll;"
  
  self.socket = require("socket")
  
  self.port=Port or 10042
  self.host=Host or "127.0.0.1"
  
  self.json=loadfile("Scripts\\JSON.lua")()
  
  self.UDPSendSocket=self.socket.udp()
  self.UDPSendSocket:settimeout(0)

  return self
end

--- Set port.
-- @param #SOCKET self
-- @param #number Port Port. Default 10042.
-- @return #SOCKET self
function SOCKET:SetPort(Port)
  self.port=Port or 10042
end

--- Set host.
-- @param #SOCKET self
-- @param #string Host Host. Default `"127.0.0.1"`.
-- @return #SOCKET self
function SOCKET:SetHost(Host)
  self.host=Host or "127.0.0.1"
end


--- Send a table.
-- @param #SOCKET self
-- @param #table Table Table to send.
-- @return #SOCKET self
function SOCKET:SendTable(Table)

  -- Add server name for DCS
  Table.server_name=BASE.ServerName or "Unknown"

  -- Encode json table.
  local json= self.json:encode(Table)
  
  -- Debug info.
  self:T("Json table:")
  self:T(json)
  
  -- Send data.
  self.socket.try(self.UDPSendSocket:sendto(json, self.host, self.port))

  return self
end

--- Send a text message.
-- @param #SOCKET self
-- @param #string Text Text message.
-- @return #SOCKET self
function SOCKET:SendText(Text)

  local message={}
  
  message.command = SOCKET.DataType.TEXT
  message.text = Text  

  self:SendTable(message)

  return self
end

--- Send a text-to-speech message.
-- @param #SOCKET self
-- @param #string Text The text message to speek.
-- @param #number Provider The TTS provider: 0=Microsoft (default), 1=Google.
-- @param #string Voice The specific voice to use, e.g. `"Microsoft David Desktop"` or "`en-US-Standard-A`". If not set, the service will choose a voice based on the other parameters such as culture and gender.
-- @param #string Culture The Culture or language code, *e.g.* `"en-US"`.
-- @param #string Gender The Gender, *i.e.* "male", "female". Default "female".
-- @param #number Volume The volume. Microsoft: [0,100] default 50, Google: [-96, 10] default 0.
-- @return #SOCKET self
function SOCKET:SendTextToSpeech(Text, Provider, Voice, Culture, Gender, Volume)

  Text=Text or "Hello World!"

  local message={}
  
  message.command = SOCKET.DataType.TTS
  message.text = Text
  message.provider=Provider
  message.voice = Voice
  message.culture = Culture
  message.gender = Gender
  message.volume = Volume

  self:SendTable(message)

  return self
end


--- **Core** - The base class within the framework.
--
-- ===
--
-- ## Features:
--
--   * The construction and inheritance of MOOSE classes.
--   * The class naming and numbering system.
--   * The class hierarchy search system.
--   * The tracing of information or objects during mission execution for debugging purposes.
--   * The subscription to DCS events for event handling in MOOSE objects.
--   * Object inspection.
--
-- ===
--
-- All classes within the MOOSE framework are derived from the BASE class.
-- Note: The BASE class is an abstract class and is not meant to be used directly.
--
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions:
--
-- ===
--
-- @module Core.Base
-- @image Core_Base.JPG

local _TraceOnOff = false -- default to no tracing
local _TraceLevel = 1
local _TraceAll = false
local _TraceClass = {}
local _TraceClassMethod = {}

local _ClassID = 0

--- Base class of everything
-- @type BASE
-- @field #string ClassName The name of the class.
-- @field #number ClassID The ID number of the class.
-- @field #string ClassNameAndID The name of the class concatenated with the ID number of the class.
-- @field Core.Scheduler#SCHEDULER Scheduler The scheduler object.

--- BASE class
--
-- # 1. BASE constructor.
--
-- Any class derived from BASE, will use the @{Core.Base#BASE.New} constructor embedded in the @{Core.Base#BASE.Inherit} method.
-- See an example at the @{Core.Base#BASE.New} method how this is done.
--
-- # 2. Trace information for debugging.
--
-- The BASE class contains trace methods to trace progress within a mission execution of a certain object.
-- These trace methods are inherited by each MOOSE class inheriting BASE, thus all objects created from
-- a class derived from BASE can use the tracing methods to trace its execution.
--
-- Any type of information can be passed to these tracing methods. See the following examples:
--
--     self:E( "Hello" )
--
-- Result in the word "Hello" in the dcs.log.
--
--     local Array = { 1, nil, "h", { "a","b" }, "x" }
--     self:E( Array )
--
-- Results with the text [1]=1,[3]="h",[4]={[1]="a",[2]="b"},[5]="x"} in the dcs.log.
--
--     local Object1 = "Object1"
--     local Object2 = 3
--     local Object3 = { Object 1, Object 2 }
--     self:E( { Object1, Object2, Object3 } )
--
-- Results with the text [1]={[1]="Object",[2]=3,[3]={[1]="Object",[2]=3}} in the dcs.log.
--
--     local SpawnObject = SPAWN:New( "Plane" )
--     local GroupObject = GROUP:FindByName( "Group" )
--     self:E( { Spawn = SpawnObject, Group = GroupObject } )
--
-- Results with the text [1]={Spawn={....),Group={...}} in the dcs.log.
--
-- Below a more detailed explanation of the different method types for tracing.
--
-- ## 2.1. Tracing methods categories.
--
-- There are basically 3 types of tracing methods available:
--
--   * @{#BASE.F}: Used to trace the entrance of a function and its given parameters. An F is indicated at column 44 in the DCS.log file.
--   * @{#BASE.T}: Used to trace further logic within a function giving optional variables or parameters. A T is indicated at column 44 in the DCS.log file.
--   * @{#BASE.E}: Used to always trace information giving optional variables or parameters. An E is indicated at column 44 in the DCS.log file.
--
-- ## 2.2 Tracing levels.
--
-- There are 3 tracing levels within MOOSE.
-- These tracing levels were defined to avoid bulks of tracing to be generated by lots of objects.
--
-- As such, the F and T methods have additional variants to trace level 2 and 3 respectively:
--
--   * @{#BASE.F2}: Trace the beginning of a function and its given parameters with tracing level 2.
--   * @{#BASE.F3}: Trace the beginning of a function and its given parameters with tracing level 3.
--   * @{#BASE.T2}: Trace further logic within a function giving optional variables or parameters with tracing level 2.
--   * @{#BASE.T3}: Trace further logic within a function giving optional variables or parameters with tracing level 3.
--
-- ## 2.3. Trace activation.
--
-- Tracing can be activated in several ways:
--
--   * Switch tracing on or off through the @{#BASE.TraceOnOff}() method.
--   * Activate all tracing through the @{#BASE.TraceAll}() method.
--   * Activate only the tracing of a certain class (name) through the @{#BASE.TraceClass}() method.
--   * Activate only the tracing of a certain method of a certain class through the @{#BASE.TraceClassMethod}() method.
--   * Activate only the tracing of a certain level through the @{#BASE.TraceLevel}() method.
--
-- ## 2.4. Check if tracing is on.
--
-- The method @{#BASE.IsTrace}() will validate if tracing is activated or not.
--
-- # 3. DCS simulator Event Handling.
--
-- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator,
-- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently.
--
-- ## 3.1. Subscribe / Unsubscribe to DCS Events.
--
-- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class.
-- So, when the DCS event occurs, the class will be notified of that event.
-- There are two methods which you use to subscribe to or unsubscribe from an event.
--
--   * @{#BASE.HandleEvent}(): Subscribe to a DCS Event.
--   * @{#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event.
--
-- ## 3.2. Event Handling of DCS Events.
--
-- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called
-- when the DCS event occurs. The Event Handling method receives an @{Core.Event#EVENTDATA} structure, which contains a lot of information
-- about the event that occurred.
--
-- Find below an example of the prototype how to write an event handling function for two units:
--
--      local Tank1 = UNIT:FindByName( "Tank A" )
--      local Tank2 = UNIT:FindByName( "Tank B" )
--
--      -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified.
--      Tank1:HandleEvent( EVENTS.Dead )
--      Tank2:HandleEvent( EVENTS.Dead )
--
--      --- This function is an Event Handling function that will be called when Tank1 is Dead.
--      -- @param Wrapper.Unit#UNIT self
--      -- @param Core.Event#EVENTDATA EventData
--      function Tank1:OnEventDead( EventData )
--
--        self:SmokeGreen()
--      end
--
--      --- This function is an Event Handling function that will be called when Tank2 is Dead.
--      -- @param Wrapper.Unit#UNIT self
--      -- @param Core.Event#EVENTDATA EventData
--      function Tank2:OnEventDead( EventData )
--
--        self:SmokeBlue()
--      end
--
-- See the @{Core.Event} module for more information about event handling.
--
-- # 4. Class identification methods.
--
-- BASE provides methods to get more information of each object:
--
--   * @{#BASE.GetClassID}(): Gets the ID (number) of the object. Each object created is assigned a number, that is incremented by one.
--   * @{#BASE.GetClassName}(): Gets the name of the object, which is the name of the class the object was instantiated from.
--   * @{#BASE.GetClassNameAndID}(): Gets the name and ID of the object.
--
-- # 5. All objects derived from BASE can have "States".
--
-- A mechanism is in place in MOOSE, that allows to let the objects administer **states**.
-- States are essentially properties of objects, which are identified by a **Key** and a **Value**.
--
-- The method @{#BASE.SetState}() can be used to set a Value with a reference Key to the object.
-- To **read or retrieve** a state Value based on a Key, use the @{#BASE.GetState} method.
--
-- These two methods provide a very handy way to keep state at long lasting processes.
-- Values can be stored within the objects, and later retrieved or changed when needed.
-- There is one other important thing to note, the @{#BASE.SetState}() and @{#BASE.GetState} methods
-- receive as the **first parameter the object for which the state needs to be set**.
-- Thus, if the state is to be set for the same object as the object for which the method is used, then provide the same
-- object name to the method.
--
-- # 6. Inheritance.
--
-- The following methods are available to implement inheritance
--
--   * @{#BASE.Inherit}: Inherits from a class.
--   * @{#BASE.GetParent}: Returns the parent object from the object it is handling, or nil if there is no parent object.
--
-- ===
--
-- @field #BASE
BASE = {
  ClassName = "BASE",
  ClassID = 0,
  Events = {},
  States = {},
  Debug = debug,
  Scheduler = nil,
  Properties = {},
}

-- @field #BASE.__
BASE.__ = {}

-- @field #BASE._
BASE._ = {
  Schedules = {}, --- Contains the Schedulers Active
}

--- BASE constructor.
--
-- This is an example how to use the BASE:New() constructor in a new class definition when inheriting from BASE.
--
--     function EVENT:New()
--       local self = BASE:Inherit( self, BASE:New() ) -- #EVENT
--       return self
--     end
--
-- @param #BASE self
-- @return #BASE
function BASE:New()
  --local self = UTILS.DeepCopy( self ) -- Create a new self instance
  local self = UTILS.DeepCopy(self)

  _ClassID = _ClassID + 1
  self.ClassID = _ClassID

  -- This is for "private" methods...
  -- When a __ is passed to a method as "self", the __index will search for the method on the public method list too!
  --  if rawget( self, "__" ) then
  -- setmetatable( self, { __index = self.__ } )
  --  end

  return self
end

--- This is the worker method to inherit from a parent class.
-- @param #BASE self
-- @param Child is the Child class that inherits.
-- @param #BASE Parent is the Parent class that the Child inherits from.
-- @return #BASE Child
function BASE:Inherit( Child, Parent )

  -- Create child.
  local Child = UTILS.DeepCopy( Child )

  if Child ~= nil then

    -- This is for "private" methods...
    -- When a __ is passed to a method as "self", the __index will search for the method on the public method list of the same object too!
    if rawget( Child, "__" ) then
      setmetatable( Child, { __index = Child.__ } )
      setmetatable( Child.__, { __index = Parent } )
    else
      setmetatable( Child, { __index = Parent } )
    end

    -- Child:_SetDestructor()
  end

  return Child
end

local function getParent( Child )
  local Parent = nil

  if Child.ClassName == 'BASE' then
    Parent = nil
  else
    if rawget( Child, "__" ) then
      Parent = getmetatable( Child.__ ).__index
    else
      Parent = getmetatable( Child ).__index
    end
  end
  return Parent
end

--- This is the worker method to retrieve the Parent class.
-- Note that the Parent class must be passed to call the parent class method.
--
--     self:GetParent(self):ParentMethod()
--
--
-- @param #BASE self
-- @param #BASE Child This is the Child class from which the Parent class needs to be retrieved.
-- @param #BASE FromClass (Optional) The class from which to get the parent.
-- @return #BASE
function BASE:GetParent( Child, FromClass )

  local Parent
  -- BASE class has no parent
  if Child.ClassName == 'BASE' then
    Parent = nil
  else

    -- self:E({FromClass = FromClass})
    -- self:E({Child = Child.ClassName})
    if FromClass then
      while (Child.ClassName ~= "BASE" and Child.ClassName ~= FromClass.ClassName) do
        Child = getParent( Child )
        -- self:E({Child.ClassName})
      end
    end
    if Child.ClassName == 'BASE' then
      Parent = nil
    else
      Parent = getParent( Child )
    end
  end
  -- self:E({Parent.ClassName})
  return Parent
end

--- This is the worker method to check if an object is an (sub)instance of a class.
--
-- ### Examples:
--
--    * ZONE:New( 'some zone' ):IsInstanceOf( ZONE ) will return true
--    * ZONE:New( 'some zone' ):IsInstanceOf( 'ZONE' ) will return true
--    * ZONE:New( 'some zone' ):IsInstanceOf( 'zone' ) will return true
--    * ZONE:New( 'some zone' ):IsInstanceOf( 'BASE' ) will return true
--
--    * ZONE:New( 'some zone' ):IsInstanceOf( 'GROUP' ) will return false
--
-- @param #BASE self
-- @param ClassName is the name of the class or the class itself to run the check against
-- @return #boolean
function BASE:IsInstanceOf( ClassName )

  -- Is className NOT a string ?
  if type( ClassName ) ~= 'string' then

    -- Is className a Moose class ?
    if type( ClassName ) == 'table' and ClassName.ClassName ~= nil then

      -- Get the name of the Moose class as a string
      ClassName = ClassName.ClassName

      -- className is neither a string nor a Moose class, throw an error
    else

      -- I'm not sure if this should take advantage of MOOSE logging function, or throw an error for pcall
      local err_str = 'className parameter should be a string; parameter received: ' .. type( ClassName )
      self:E( err_str )
      -- error( err_str )
      return false

    end
  end

  ClassName = string.upper( ClassName )

  if string.upper( self.ClassName ) == ClassName then
    return true
  end

  local Parent = getParent( self )

  while Parent do

    if string.upper( Parent.ClassName ) == ClassName then
      return true
    end

    Parent = getParent( Parent )

  end

  return false

end
--- Get the ClassName + ClassID of the class instance.
-- The ClassName + ClassID is formatted as '%s#%09d'.
-- @param #BASE self
-- @return #string The ClassName + ClassID of the class instance.
function BASE:GetClassNameAndID()
  return string.format( '%s#%09d', self.ClassName, self.ClassID )
end

--- Get the ClassName of the class instance.
-- @param #BASE self
-- @return #string The ClassName of the class instance.
function BASE:GetClassName()
  return self.ClassName
end

--- Get the ClassID of the class instance.
-- @param #BASE self
-- @return #string The ClassID of the class instance.
function BASE:GetClassID()
  return self.ClassID
end

do -- Event Handling

  --- Returns the event dispatcher
  -- @param #BASE self
  -- @return Core.Event#EVENT
  function BASE:EventDispatcher()

    return _EVENTDISPATCHER
  end

  --- Get the Class @{Core.Event} processing Priority.
  -- The Event processing Priority is a number from 1 to 10,
  -- reflecting the order of the classes subscribed to the Event to be processed.
  -- @param #BASE self
  -- @return #number The @{Core.Event} processing Priority.
  function BASE:GetEventPriority()
    return self._.EventPriority or 5
  end

  --- Set the Class @{Core.Event} processing Priority.
  -- The Event processing Priority is a number from 1 to 10,
  -- reflecting the order of the classes subscribed to the Event to be processed.
  -- @param #BASE self
  -- @param #number EventPriority The @{Core.Event} processing Priority.
  -- @return #BASE self
  function BASE:SetEventPriority( EventPriority )
    self._.EventPriority = EventPriority
  end

  --- Remove all subscribed events
  -- @param #BASE self
  -- @return #BASE
  function BASE:EventRemoveAll()

    self:EventDispatcher():RemoveAll( self )

    return self
  end

  --- Subscribe to a DCS Event.
  -- @param #BASE self
  -- @param Core.Event#EVENTS EventID Event ID.
  -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit.
  -- @return #BASE
  function BASE:HandleEvent( EventID, EventFunction )

    self:EventDispatcher():OnEventGeneric( EventFunction, self, EventID )

    return self
  end

  --- UnSubscribe to a DCS event.
  -- @param #BASE self
  -- @param Core.Event#EVENTS EventID Event ID.
  -- @return #BASE
  function BASE:UnHandleEvent( EventID )

    self:EventDispatcher():RemoveEvent( self, EventID )

    return self
  end
  
  -- Event handling function prototypes - Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  
  --- Occurs whenever any unit in a mission fires a weapon. But not any machine gun or autocannon based weapon, those are handled by EVENT.ShootingStart.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- @function [parent=#BASE] OnEventShot
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs whenever an object is hit by a weapon.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit object the fired the weapon
  -- weapon: Weapon object that hit the target
  -- target: The Object that was hit. 
  -- @function [parent=#BASE] OnEventHit
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when an aircraft takes off from an airbase, farp, or ship.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that tookoff
  -- place: Object from where the AI took-off from. Can be an Airbase Object, FARP, or Ships 
  -- @function [parent=#BASE] OnEventTakeoff
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when an aircraft lands at an airbase, farp or ship
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that has landed
  -- place: Object that the unit landed on. Can be an Airbase Object, FARP, or Ships 
  -- @function [parent=#BASE] OnEventLand
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when any aircraft crashes into the ground and is completely destroyed.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that has crashed 
  -- @function [parent=#BASE] OnEventCrash
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when a pilot ejects from an aircraft
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that has ejected 
  -- @function [parent=#BASE] OnEventEjection
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when an aircraft connects with a tanker and begins taking on fuel.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that is receiving fuel. 
  -- @function [parent=#BASE] OnEventRefueling
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when an object is dead.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that is dead. 
  -- @function [parent=#BASE] OnEventDead
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when an Event for an object is triggered.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that triggered the event. 
  -- @function [parent=#BASE] OnEvent
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when the pilot of an aircraft is killed. Can occur either if the player is alive and crashes or if a weapon kills the pilot without completely destroying the plane.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that the pilot has died in. 
  -- @function [parent=#BASE] OnEventPilotDead
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when a ground unit captures either an airbase or a farp.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that captured the base
  -- place: The airbase that was captured, can be a FARP or Airbase. When calling place:getCoalition() the faction will already be the new owning faction. 
  -- @function [parent=#BASE] OnEventBaseCaptured
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when a mission starts
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes. 
  -- @function [parent=#BASE] OnEventMissionStart
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when a mission ends
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- @function [parent=#BASE] OnEventMissionEnd
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when an aircraft is finished taking fuel.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that was receiving fuel. 
  -- @function [parent=#BASE] OnEventRefuelingStop
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when any object is spawned into the mission.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that was spawned 
  -- @function [parent=#BASE] OnEventBirth
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when any system fails on a human controlled aircraft.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that had the failure 
  -- @function [parent=#BASE] OnEventHumanFailure
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when any aircraft starts its engines.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that is starting its engines. 
  -- @function [parent=#BASE] OnEventEngineStartup
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when any aircraft shuts down its engines.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that is stopping its engines. 
  -- @function [parent=#BASE] OnEventEngineShutdown
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when any player assumes direct control of a unit. Note - not Mulitplayer safe. Use PlayerEnterAircraft.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that is being taken control of. 
  -- @function [parent=#BASE] OnEventPlayerEnterUnit
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when any player relieves control of a unit to the AI.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that the player left. 
  -- @function [parent=#BASE] OnEventPlayerLeaveUnit
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when any unit begins firing a weapon that has a high rate of fire. Most common with aircraft cannons (GAU-8), autocannons, and machine guns.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that is doing the shooting.
  -- target: The unit that is being targeted.
  -- @function [parent=#BASE] OnEventShootingStart
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when any unit stops firing its weapon. Event will always correspond with a shooting start event.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- initiator : The unit that was doing the shooting. 
  -- @function [parent=#BASE] OnEventShootingEnd
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when a new mark was added.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- MarkID: ID of the mark. 
  -- @function [parent=#BASE] OnEventMarkAdded
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when a mark was removed.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- MarkID: ID of the mark. 
  -- @function [parent=#BASE] OnEventMarkRemoved
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when a mark text was changed.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- MarkID: ID of the mark. 
  -- @function [parent=#BASE] OnEventMarkChange
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Unknown precisely what creates this event, likely tied into newer damage model. Will update this page when new information become available.
  --
  -- * initiator: The unit that had the failure.
  --
  -- @function [parent=#BASE] OnEventDetailedFailure
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when any modification to the "Score" as seen on the debrief menu would occur.
  -- There is no information on what values the score was changed to. Event is likely similar to player_comment in this regard.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- @function [parent=#BASE] OnEventScore
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs on the death of a unit. Contains more and different information. Similar to unit_lost it will occur for aircraft before the aircraft crash event occurs.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- 
  -- * initiator: The unit that killed the target
  -- * target: Target Object
  -- * weapon: Weapon Object
  --
  -- @function [parent=#BASE] OnEventKill
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when any modification to the "Score" as seen on the debrief menu would occur.
  -- There is no information on what values the score was changed to. Event is likely similar to player_comment in this regard.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- @function [parent=#BASE] OnEventScore
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when the game thinks an object is destroyed.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- 
  -- * initiator: The unit that is was destroyed.
  --
  -- @function [parent=#BASE] OnEventUnitLost
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs shortly after the landing animation of an ejected pilot touching the ground and standing up. Event does not occur if the pilot lands in the water and sub combs to Davey Jones Locker.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- 
  -- * initiator: Static object representing the ejected pilot. Place : Aircraft that the pilot ejected from.
  -- * place: may not return as a valid object if the aircraft has crashed into the ground and no longer exists.
  -- * subplace: is always 0 for unknown reasons.
  --
  -- @function [parent=#BASE] OnEventLandingAfterEjection
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Paratrooper landing.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- @function [parent=#BASE] OnEventParatrooperLanding
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Discard chair after ejection.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- @function [parent=#BASE] OnEventDiscardChairAfterEjection
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Weapon add. Fires when entering a mission per pylon with the name of the weapon (double pylons not counted, infinite wep reload not counted.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- @function [parent=#BASE] OnEventParatrooperLanding
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Trigger zone.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- @function [parent=#BASE] OnEventTriggerZone
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Landing quality mark. 
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- @function [parent=#BASE] OnEventLandingQualityMark
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- BDA.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- @function [parent=#BASE] OnEventBDA
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when a player enters a slot and takes control of an aircraft.
  -- Have a look at the class @{Core.Event#EVENT} as these are just the prototypes.
  -- **NOTE**: This is a workaround of a long standing DCS bug with the PLAYER_ENTER_UNIT event. 
  -- initiator : The unit that is being taken control of. 
  -- @function [parent=#BASE] OnEventPlayerEnterAircraft
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.
  
  --- Occurs when a player creates a dynamic cargo object from the F8 ground crew menu.
  -- *** NOTE *** this is a workarounf for DCS not creating these events as of Aug 2024.
  -- @function [parent=#BASE] OnEventNewDynamicCargo
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.
  
    --- Occurs when a player loads a dynamic cargo object with the F8 ground crew menu into a helo.
  -- *** NOTE *** this is a workarounf for DCS not creating these events as of Aug 2024.
  -- @function [parent=#BASE] OnEventDynamicCargoLoaded
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.
  
  --- Occurs when a player unloads a dynamic cargo object with the F8 ground crew menu from a helo.
  -- *** NOTE *** this is a workarounf for DCS not creating these events as of Aug 2024.
  -- @function [parent=#BASE] OnEventDynamicCargoUnloaded
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.
  
  --- Occurs when a dynamic cargo crate is removed.
  -- *** NOTE *** this is a workarounf for DCS not creating these events as of Aug 2024.
  -- @function [parent=#BASE] OnEventDynamicCargoRemoved
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.
  
end

--- Creation of a Birth Event.
-- @param #BASE self
-- @param DCS#Time EventTime The time stamp of the event.
-- @param DCS#Object Initiator The initiating object of the event.
-- @param #string IniUnitName The initiating unit name.
-- @param place
-- @param subplace
function BASE:CreateEventBirth( EventTime, Initiator, IniUnitName, place, subplace )
  self:F( { EventTime, Initiator, IniUnitName, place, subplace } )

  local Event = {
    id = world.event.S_EVENT_BIRTH,
    time = EventTime,
    initiator = Initiator,
    IniUnitName = IniUnitName,
    place = place,
    subplace = subplace,
  }

  world.onEvent( Event )
end

--- Creation of a Crash Event.
-- @param #BASE self
-- @param DCS#Time EventTime The time stamp of the event.
-- @param DCS#Object Initiator The initiating object of the event.
function BASE:CreateEventCrash( EventTime, Initiator, IniObjectCategory )
  self:F( { EventTime, Initiator } )

  local Event = {
    id = world.event.S_EVENT_CRASH,
    time = EventTime,
    initiator = Initiator,
    IniObjectCategory = IniObjectCategory,
  }

  world.onEvent( Event )
end

--- Creation of a Crash Event.
-- @param #BASE self
-- @param DCS#Time EventTime The time stamp of the event.
-- @param DCS#Object Initiator The initiating object of the event.
function BASE:CreateEventUnitLost(EventTime, Initiator)
  self:F( { EventTime, Initiator } )

  local Event = {
    id = world.event.S_EVENT_UNIT_LOST,
    time = EventTime,
    initiator = Initiator,
    }

  world.onEvent( Event )
end

--- Creation of a Dead Event.
-- @param #BASE self
-- @param DCS#Time EventTime The time stamp of the event.
-- @param DCS#Object Initiator The initiating object of the event.
function BASE:CreateEventDead( EventTime, Initiator, IniObjectCategory )
  self:F( { EventTime, Initiator, IniObjectCategory } )

  local Event = {
    id = world.event.S_EVENT_DEAD,
    time = EventTime,
    initiator = Initiator,
    IniObjectCategory = IniObjectCategory,
    }

  world.onEvent( Event )
end

--- Creation of a Remove Unit Event.
-- @param #BASE self
-- @param DCS#Time EventTime The time stamp of the event.
-- @param DCS#Object Initiator The initiating object of the event.
function BASE:CreateEventRemoveUnit( EventTime, Initiator )
  self:F( { EventTime, Initiator } )

  local Event = {
    id = EVENTS.RemoveUnit,
    time = EventTime,
    initiator = Initiator,
  }

  world.onEvent( Event )
end

--- Creation of a Takeoff Event.
-- @param #BASE self
-- @param DCS#Time EventTime The time stamp of the event.
-- @param DCS#Object Initiator The initiating object of the event.
function BASE:CreateEventTakeoff( EventTime, Initiator )
  self:F( { EventTime, Initiator } )

  local Event = {
    id = world.event.S_EVENT_TAKEOFF,
    time = EventTime,
    initiator = Initiator,
  }

  world.onEvent( Event )
end

  --- Creation of a `S_EVENT_PLAYER_ENTER_AIRCRAFT` event.
  -- @param #BASE self
  -- @param Wrapper.Unit#UNIT PlayerUnit The aircraft unit the player entered.
  function BASE:CreateEventPlayerEnterAircraft( PlayerUnit )
    self:F( { PlayerUnit } )
  
    local Event = {
      id = EVENTS.PlayerEnterAircraft,
      time = timer.getTime(),
      initiator = PlayerUnit:GetDCSObject()
      }
  
    world.onEvent(Event)
  end  
  
    --- Creation of a S_EVENT_NEW_DYNAMIC_CARGO event.
  -- @param #BASE self
  -- @param Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo the dynamic cargo object
  function BASE:CreateEventNewDynamicCargo(DynamicCargo)
    self:F({DynamicCargo})
    local Event = {
      id = EVENTS.NewDynamicCargo,
      time = timer.getTime(),
      dynamiccargo = DynamicCargo,
      initiator = DynamicCargo:GetDCSObject(),
    }
    world.onEvent( Event )
  end
  
  --- Creation of a S_EVENT_DYNAMIC_CARGO_LOADED event.
  -- @param #BASE self
  -- @param Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo the dynamic cargo object
  function BASE:CreateEventDynamicCargoLoaded(DynamicCargo)
    self:F({DynamicCargo})
    local Event = {
      id = EVENTS.DynamicCargoLoaded,
      time = timer.getTime(),
      dynamiccargo = DynamicCargo,
      initiator = DynamicCargo:GetDCSObject(),
    }
    world.onEvent( Event )
  end
  
  --- Creation of a S_EVENT_DYNAMIC_CARGO_UNLOADED event.
  -- @param #BASE self
  -- @param Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo the dynamic cargo object
  function BASE:CreateEventDynamicCargoUnloaded(DynamicCargo)
    self:F({DynamicCargo})
    local Event = {
      id = EVENTS.DynamicCargoUnloaded,
      time = timer.getTime(),
      dynamiccargo = DynamicCargo,
      initiator = DynamicCargo:GetDCSObject(),
    }
    world.onEvent( Event )
  end
  
  --- Creation of a S_EVENT_DYNAMIC_CARGO_REMOVED event.
  -- @param #BASE self
  -- @param Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo the dynamic cargo object
  function BASE:CreateEventDynamicCargoRemoved(DynamicCargo)
    self:F({DynamicCargo})
    local Event = {
      id = EVENTS.DynamicCargoRemoved,
      time = timer.getTime(),
      dynamiccargo = DynamicCargo,
      initiator = DynamicCargo:GetDCSObject(),
    }
    world.onEvent( Event )
  end
                  
--- The main event handling function... This function captures all events generated for the class.
-- @param #BASE self
-- @param DCS#Event event
function BASE:onEvent( event )

  if self then

    for EventID, EventObject in pairs( self.Events ) do
      if EventObject.EventEnabled then

        if event.id == EventObject.Event then

          if self == EventObject.Self then

            if event.initiator and event.initiator:isExist() then
              event.IniUnitName = event.initiator:getName()
            end

            if event.target and event.target:isExist() then
              event.TgtUnitName = event.target:getName()
            end

          end

        end

      end
    end
  end
end

do -- Scheduling

  --- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also.
  -- @param #BASE self
  -- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called.
  -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments.
  -- @param ... Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }.
  -- @return #string The Schedule ID of the planned schedule.
  function BASE:ScheduleOnce( Start, SchedulerFunction, ... )
  
    -- Object name.
    local ObjectName = "-"
    ObjectName = self.ClassName .. self.ClassID
    
    -- Debug info.
    self:F3( { "ScheduleOnce: ", ObjectName,  Start } )
    
    if not self.Scheduler then
      self.Scheduler = SCHEDULER:New( self )
    end
  
    -- FF this was wrong!
    --[[
    local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( 
      self, 
      SchedulerFunction,
      { ... },
      Start,
      nil,
      nil,
      nil
    )
    ]]
    
    -- NOTE: MasterObject (first parameter) needs to be nil or it will be the first argument passed to the SchedulerFunction!
    local ScheduleID = self.Scheduler:Schedule(nil, SchedulerFunction, {...}, Start)
    
    self._.Schedules[#self._.Schedules+1] = ScheduleID
  
    return self._.Schedules[#self._.Schedules]
  end

  --- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also.
  -- @param #BASE self
  -- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called.
  -- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function.
  -- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat.
  -- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped.
  -- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments.
  -- @param #table ... Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }.
  -- @return #string The Schedule ID of the planned schedule.
  function BASE:ScheduleRepeat( Start, Repeat, RandomizeFactor, Stop, SchedulerFunction, ... )
    self:F2( { Start } )
    self:T3( { ... } )

    local ObjectName = "-"
    ObjectName = self.ClassName .. self.ClassID

    self:F3( { "ScheduleRepeat: ", ObjectName, Start, Repeat, RandomizeFactor, Stop } )

    if not self.Scheduler then
      self.Scheduler = SCHEDULER:New( self )
    end
    
    -- NOTE: MasterObject (first parameter) should(!) be nil as it will be the first argument passed to the SchedulerFunction!
    local ScheduleID = self.Scheduler:Schedule(
      nil, 
      SchedulerFunction,
      { ... },
      Start,
      Repeat,
      RandomizeFactor,
      Stop,
      4
    )
    
    self._.Schedules[#self._.Schedules+1] = ScheduleID
  
    return self._.Schedules[#self._.Schedules]
  end

  --- Stops the Schedule.
  -- @param #BASE self
  -- @param #string SchedulerID (Optional) Scheduler ID to be stopped. If nil, all pending schedules are stopped.
  function BASE:ScheduleStop( SchedulerID )  
    self:F3( { "ScheduleStop:" } )

    if self.Scheduler then
      --_SCHEDULEDISPATCHER:Stop( self.Scheduler, self._.Schedules[SchedulerFunction] )
      _SCHEDULEDISPATCHER:Stop(self.Scheduler, SchedulerID)
    end
  end

end

--- Set a state or property of the Object given a Key and a Value.
-- Note that if the Object is destroyed, set to nil, or garbage collected, then the Values and Keys will also be gone.
-- @param #BASE self
-- @param Object The object that will hold the Value set by the Key.
-- @param Key The key that is used as a reference of the value. Note that the key can be a #string, but it can also be any other type!
-- @param Value The value to is stored in the object.
-- @return The Value set. 
function BASE:SetState( Object, Key, Value )

  local ClassNameAndID = Object:GetClassNameAndID()

  self.States[ClassNameAndID] = self.States[ClassNameAndID] or {}
  self.States[ClassNameAndID][Key] = Value

  return self.States[ClassNameAndID][Key]
end

--- Get a Value given a Key from the Object.
-- Note that if the Object is destroyed, set to nil, or garbage collected, then the Values and Keys will also be gone.
-- @param #BASE self
-- @param Object The object that holds the Value set by the Key.
-- @param Key The key that is used to retrieve the value. Note that the key can be a #string, but it can also be any other type!
-- @return The Value retrieved or nil if the Key was not found and thus the Value could not be retrieved.
function BASE:GetState( Object, Key )

  local ClassNameAndID = Object:GetClassNameAndID()

  if self.States[ClassNameAndID] then
    local Value = self.States[ClassNameAndID][Key] or false
    return Value
  end

  return nil
end

--- Clear the state of an object.
-- @param #BASE self
-- @param Object The object that holds the Value set by the Key.
-- @param StateName The key that is should be cleared.
function BASE:ClearState( Object, StateName )

  local ClassNameAndID = Object:GetClassNameAndID()
  if self.States[ClassNameAndID] then
    self.States[ClassNameAndID][StateName] = nil
  end
end

--- Set one property of an object.
-- @param #BASE self
-- @param Key The key that is used as a reference of the value. Note that the key can be a #string, but it can also be any other type!
-- @param Value The value that is stored. Note that the value can be a #string, but it can also be any other type!
function BASE:SetProperty(Key,Value)
  self.Properties = self.Properties or {}
  self.Properties[Key] = Value
end
            
--- Get one property of an object by the key.
-- @param #BASE self
-- @param Key The key that is used as a reference of the value. Note that the key can be a #string, but it can also be any other type!
-- @return Value The value that is stored. Note that the value can be a #string, but it can also be any other type! Nil if not found.         
function BASE:GetProperty(Key)
  self.Properties = self.Properties or {}
  return self.Properties[Key]
end

--- Get all of the properties of an object in a table.
-- @param #BASE self
-- @return #table of values, indexed by keys.
function BASE:GetProperties()
  return self.Properties
end
            
-- Trace section

-- Log a trace (only shown when trace is on)
-- TODO: Make trace function using variable parameters.

--- Set trace on.
-- @param #BASE self
-- @usage
-- -- Switch the tracing On
-- BASE:TraceOn()
function BASE:TraceOn()
  self:TraceOnOff( true )
end

--- Set trace off.
-- @param #BASE self
-- @usage
-- -- Switch the tracing Off
-- BASE:TraceOff()
function BASE:TraceOff()
  self:TraceOnOff( false )
end

--- Set trace on or off
-- Note that when trace is off, no BASE.Debug statement is performed, increasing performance!
-- When Moose is loaded statically, (as one file), tracing is switched off by default.
-- So tracing must be switched on manually in your mission if you are using Moose statically.
-- When moose is loading dynamically (for moose class development), tracing is switched on by default.
-- @param #BASE self
-- @param #boolean TraceOnOff Switch the tracing on or off.
-- @usage
--
--   -- Switch the tracing On
--   BASE:TraceOnOff( true )
--
--   -- Switch the tracing Off
--   BASE:TraceOnOff( false )
--
function BASE:TraceOnOff( TraceOnOff )
  if TraceOnOff == false then
    self:I( "Tracing in MOOSE is OFF" )
    _TraceOnOff = false
  else
    self:I( "Tracing in MOOSE is ON" )
    _TraceOnOff = true
  end
end

--- Enquires if tracing is on (for the class).
-- @param #BASE self
-- @return #boolean
function BASE:IsTrace()

  if BASE.Debug and (_TraceAll == true) or (_TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName]) then
    return true
  else
    return false
  end
end

--- Set trace level
-- @param #BASE self
-- @param #number Level
function BASE:TraceLevel( Level )
  _TraceLevel = Level or 1
  self:I( "Tracing level " .. _TraceLevel )
end

--- Trace all methods in MOOSE
-- @param #BASE self
-- @param #boolean TraceAll true = trace all methods in MOOSE.
function BASE:TraceAll( TraceAll )

  if TraceAll == false then
    _TraceAll = false
  else
    _TraceAll = true
  end

  if _TraceAll then
    self:I( "Tracing all methods in MOOSE " )
  else
    self:I( "Switched off tracing all methods in MOOSE" )
  end
end

--- Set tracing for a class
-- @param #BASE self
-- @param #string Class Class name.
function BASE:TraceClass( Class )
  _TraceClass[Class] = true
  _TraceClassMethod[Class] = {}
  self:I( "Tracing class " .. Class )
end

--- Set tracing for a specific method of  class
-- @param #BASE self
-- @param #string Class Class name.
-- @param #string Method Method.
function BASE:TraceClassMethod( Class, Method )
  if not _TraceClassMethod[Class] then
    _TraceClassMethod[Class] = {}
    _TraceClassMethod[Class].Method = {}
  end
  _TraceClassMethod[Class].Method[Method] = true
  self:I( "Tracing method " .. Method .. " of class " .. Class )
end

--- (Internal) Serialize arguments
-- @param #BASE self
-- @param #table Arguments
-- @return #string Text
function BASE:_Serialize(Arguments)
  local text = UTILS.PrintTableToLog({Arguments}, 0, true)
  text = string.gsub(text,"(\n+)","")
  text = string.gsub(text,"%(%(","%(")
  text = string.gsub(text,"%)%)","%)")
  text = string.gsub(text,"(%s+)"," ")
  return text
end

----- (Internal) Serialize arguments
---- @param #BASE self
---- @param #table Arguments
---- @return #string Text
--function BASE:_Serialize(Arguments)
--  local text=UTILS.BasicSerialize(Arguments)
--  return text
--end

--- Trace a function call. This function is private.
-- @param #BASE self
-- @param Arguments A #table or any field.
function BASE:_F( Arguments, DebugInfoCurrentParam, DebugInfoFromParam )

  if BASE.Debug and (_TraceAll == true) or (_TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName]) then

    local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or BASE.Debug.getinfo( 2, "nl" )
    local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or BASE.Debug.getinfo( 3, "l" )

    local Function = "function"
    if DebugInfoCurrent.name then
      Function = DebugInfoCurrent.name
    end

    if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then
      local LineCurrent = 0
      if DebugInfoCurrent.currentline then
        LineCurrent = DebugInfoCurrent.currentline
      end
      local LineFrom = 0
      if DebugInfoFrom then
        LineFrom = DebugInfoFrom.currentline
      end
      env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)", LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, BASE:_Serialize(Arguments) ) )
    end
  end
end

--- Trace a function call. Must be at the beginning of the function logic.
-- @param #BASE self
-- @param Arguments A #table or any field.
function BASE:F( Arguments )

  if BASE.Debug and _TraceOnOff == true then
    local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" )
    local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" )

    if _TraceLevel >= 1 then
      self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom )
    end
  end
end

--- Trace a function call level 2. Must be at the beginning of the function logic.
-- @param #BASE self
-- @param Arguments A #table or any field.
function BASE:F2( Arguments )

  if BASE.Debug and _TraceOnOff == true and _TraceLevel >= 2 then
    local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" )
    local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" )

    if _TraceLevel >= 2 then
      self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom )
    end
  end
end

--- Trace a function call level 3. Must be at the beginning of the function logic.
-- @param #BASE self
-- @param Arguments A #table or any field.
function BASE:F3( Arguments )

  if BASE.Debug and _TraceOnOff == true and _TraceLevel >= 3 then
    local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" )
    local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" )

    if _TraceLevel >= 3 then
      self:_F( Arguments, DebugInfoCurrent, DebugInfoFrom )
    end
  end
end

--- Trace a function logic.
-- @param #BASE self
-- @param Arguments A #table or any field.
function BASE:_T( Arguments, DebugInfoCurrentParam, DebugInfoFromParam )

  if BASE.Debug and (_TraceAll == true) or (_TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName]) then

    local DebugInfoCurrent = DebugInfoCurrentParam and DebugInfoCurrentParam or BASE.Debug.getinfo( 2, "nl" )
    local DebugInfoFrom = DebugInfoFromParam and DebugInfoFromParam or BASE.Debug.getinfo( 3, "l" )

    local Function = "function"
    if DebugInfoCurrent.name then
      Function = DebugInfoCurrent.name
    end

    if _TraceAll == true or _TraceClass[self.ClassName] or _TraceClassMethod[self.ClassName].Method[Function] then
      local LineCurrent = 0
      if DebugInfoCurrent.currentline then
        LineCurrent = DebugInfoCurrent.currentline
      end
      local LineFrom = 0
      if DebugInfoFrom then
        LineFrom = DebugInfoFrom.currentline
      end
      env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s", LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, BASE:_Serialize(Arguments) ) )
    end
  end
end

--- Trace a function logic level 1. Can be anywhere within the function logic.
-- @param #BASE self
-- @param Arguments A #table or any field.
function BASE:T( Arguments )

  if BASE.Debug and _TraceOnOff == true then
    local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" )
    local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" )

    if _TraceLevel >= 1 then
      self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom )
    end
  end
end

--- Trace a function logic level 2. Can be anywhere within the function logic.
-- @param #BASE self
-- @param Arguments A #table or any field.
function BASE:T2( Arguments )

  if BASE.Debug and _TraceOnOff == true and _TraceLevel >= 2 then
    local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" )
    local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" )

    if _TraceLevel >= 2 then
      self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom )
    end
  end
end

--- Trace a function logic level 3. Can be anywhere within the function logic.
-- @param #BASE self
-- @param Arguments A #table or any field.
function BASE:T3( Arguments )

  if BASE.Debug and _TraceOnOff == true and _TraceLevel >= 3 then
    local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" )
    local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" )

    if _TraceLevel >= 3 then
      self:_T( Arguments, DebugInfoCurrent, DebugInfoFrom )
    end
  end
end

--- Log an exception which will be traced always. Can be anywhere within the function logic.
-- @param #BASE self
-- @param Arguments A #table or any field.
function BASE:E( Arguments )

  if BASE.Debug then
    local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" )
    local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" )

    local Function = "function"
    if DebugInfoCurrent.name then
      Function = DebugInfoCurrent.name
    end

    local LineCurrent = DebugInfoCurrent.currentline
    local LineFrom = -1
    if DebugInfoFrom then
      LineFrom = DebugInfoFrom.currentline
    end

    env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)", LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, UTILS.BasicSerialize( Arguments ) ) )
  else
    env.info( string.format( "%1s:%30s%05d(%s)", "E", self.ClassName, self.ClassID, UTILS.BasicSerialize(Arguments) ) )
  end

end

--- Log an information which will be traced always. Can be anywhere within the function logic.
-- @param #BASE self
-- @param Arguments A #table or any field.
function BASE:I( Arguments )

  if BASE.Debug then
    local DebugInfoCurrent = BASE.Debug.getinfo( 2, "nl" )
    local DebugInfoFrom = BASE.Debug.getinfo( 3, "l" )

    local Function = "function"
    if DebugInfoCurrent.name then
      Function = DebugInfoCurrent.name
    end

    local LineCurrent = DebugInfoCurrent.currentline
    local LineFrom = -1
    if DebugInfoFrom then
      LineFrom = DebugInfoFrom.currentline
    end

    env.info( string.format( "%6d(%6d)/%1s:%30s%05d.%s(%s)", LineCurrent, LineFrom, "I", self.ClassName, self.ClassID, Function, UTILS.BasicSerialize( Arguments ) ) )
  else
    env.info( string.format( "%1s:%30s%05d(%s)", "I", self.ClassName, self.ClassID, UTILS.BasicSerialize(Arguments)) )
  end

end
--- **Core** - A* Pathfinding.
--
-- **Main Features:**
--
--    * Find path from A to B.
--    * Pre-defined as well as custom valid neighbour functions.
--    * Pre-defined as well as custom cost functions.
--    * Easy rectangular grid setup.
--
-- ===
--
-- ### Author: **funkyfranky**
-- 
-- ===
-- @module Core.Astar
-- @image CORE_Astar.png


--- ASTAR class.
-- @type ASTAR
-- @field #string ClassName Name of the class.
-- @field #boolean Debug Debug mode. Messages to all about status.
-- @field #string lid Class id string for output to DCS log file.
-- @field #table nodes Table of nodes.
-- @field #number counter Node counter.
-- @field #number Nnodes Number of nodes.
-- @field #number nvalid Number of nvalid calls.
-- @field #number nvalidcache Number of cached valid evals.
-- @field #number ncost Number of cost evaluations.
-- @field #number ncostcache Number of cached cost evals.
-- @field #ASTAR.Node startNode Start node.
-- @field #ASTAR.Node endNode End node.
-- @field Core.Point#COORDINATE startCoord Start coordinate.
-- @field Core.Point#COORDINATE endCoord End coordinate.
-- @field #function ValidNeighbourFunc Function to check if a node is valid.
-- @field #table ValidNeighbourArg Optional arguments passed to the valid neighbour function.
-- @field #function CostFunc Function to calculate the heuristic "cost" to go from one node to another.
-- @field #table CostArg Optional arguments passed to the cost function. 
-- @extends Core.Base#BASE

--- *When nothing goes right... Go left!*
--
-- ===
--
-- # The ASTAR Concept
-- 
-- Pathfinding algorithm.
-- 
-- 
-- # Start and Goal
-- 
-- The first thing we need to define is obviously the place where we want to start and where we want to go eventually.
-- 
-- ## Start
-- 
-- The start
-- 
-- ## Goal 
-- 
-- 
-- # Nodes
-- 
-- ## Rectangular Grid
-- 
-- A rectangular grid can be created using the @{#ASTAR.CreateGrid}(*ValidSurfaceTypes, BoxHY, SpaceX, deltaX, deltaY, MarkGrid*), where
-- 
-- * *ValidSurfaceTypes* is a table of valid surface types. By default all surface types are valid.
-- * *BoxXY* is the width of the grid perpendicular the the line between start and end node. Default is 40,000 meters (40 km).
-- * *SpaceX* is the additional space behind the start and end nodes. Default is 20,000 meters (20 km).
-- * *deltaX* is the grid spacing between nodes in the direction of start and end node. Default is 2,000 meters (2 km).
-- * *deltaY* is the grid spacing perpendicular to the direction of start and end node. Default is the same as *deltaX*.
-- * *MarkGrid* If set to *true*, this places marker on the F10 map on each grid node. Note that this can stall DCS if too many nodes are created. 
-- 
-- ## Valid Surfaces
-- 
-- Certain unit types can only travel on certain surfaces types, for example
-- 
-- * Naval units can only travel on water (that also excludes shallow water in DCS currently),
-- * Ground units can only traval on land.
-- 
-- By restricting the surface type in the grid construction, we also reduce the number of nodes, which makes the algorithm more efficient.
-- 
-- ## Box Width (BoxHY)
-- 
-- The box width needs to be large enough to capture all paths you want to consider.
-- 
-- ## Space in X
-- 
-- The space in X value is important if the algorithm needs to to backwards from the start node or needs to extend even further than the end node.
-- 
-- ## Grid Spacing
-- 
-- The grid spacing is an important factor as it determines the number of nodes and hence the performance of the algorithm. It should be as large as possible.
-- However, if the value is too large, the algorithm might fail to get a valid path.
-- 
-- A good estimate of the grid spacing is to set it to be smaller (~ half the size) of the smallest gap you need to path.
-- 
-- # Valid Neighbours
-- 
-- The A* algorithm needs to know if a transition from one node to another is allowed or not. By default, hopping from one node to another is always possible.
-- 
-- ## Line of Sight
-- 
-- For naval
--  
-- 
-- # Heuristic Cost
-- 
-- In order to determine the optimal path, the pathfinding algorithm needs to know, how costly it is to go from one node to another.
-- Often, this can simply be determined by the distance between two nodes. Therefore, the default cost function is set to be the 2D distance between two nodes.
-- 
-- 
-- # Calculate the Path
-- 
-- Finally, we have to calculate the path. This is done by the @{#GetPath}(*ExcludeStart, ExcludeEnd*) function. This function returns a table of nodes, which
-- describe the optimal path from the start node to the end node.
-- 
-- By default, the start and end node are include in the table that is returned.
-- 
-- Note that a valid path must not always exist. So you should check if the function returns *nil*.
-- 
-- Common reasons that a path cannot be found are:
-- 
-- * The grid is too small ==> increase grid size, e.g. *BoxHY* and/or *SpaceX* if you use a rectangular grid.  
-- * The grid spacing is too large ==> decrease *deltaX* and/or *deltaY*
-- * There simply is no valid path ==> you are screwed :(
-- 
-- 
-- # Examples
-- 
-- ## Strait of Hormuz
-- 
-- Carrier Group finds its way through the Stait of Hormuz.
-- 
-- ## 
-- 
--
--
-- @field #ASTAR
ASTAR = {
  ClassName      = "ASTAR",
  Debug          =   nil,
  lid            =   nil,
  nodes          =    {},
  counter        =     1,
  Nnodes         =     0,
  ncost          =     0,
  ncostcache     =     0,
  nvalid         =     0,
  nvalidcache    =     0,
}

--- Node data.
-- @type ASTAR.Node
-- @field #number id Node id.
-- @field Core.Point#COORDINATE coordinate Coordinate of the node.
-- @field #number surfacetype Surface type.
-- @field #table valid Cached valid/invalid nodes.
-- @field #table cost Cached cost.

--- ASTAR infinity.
-- @field #number INF
ASTAR.INF=1/0

--- ASTAR class version.
-- @field #string version
ASTAR.version="0.4.0"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Add more valid neighbour functions.
-- TODO: Write docs.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new ASTAR object.
-- @param #ASTAR self
-- @return #ASTAR self
function ASTAR:New()

  -- Inherit everything from INTEL class.
  local self=BASE:Inherit(self, BASE:New()) --#ASTAR

  self.lid="ASTAR | "

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set coordinate from where to start.
-- @param #ASTAR self
-- @param Core.Point#COORDINATE Coordinate Start coordinate.
-- @return #ASTAR self
function ASTAR:SetStartCoordinate(Coordinate)

  self.startCoord=Coordinate
  
  return self
end

--- Set coordinate where you want to go.
-- @param #ASTAR self
-- @param Core.Point#COORDINATE Coordinate end coordinate.
-- @return #ASTAR self
function ASTAR:SetEndCoordinate(Coordinate)

  self.endCoord=Coordinate
  
  return self
end

--- Create a node from a given coordinate.
-- @param #ASTAR self
-- @param Core.Point#COORDINATE Coordinate The coordinate where to create the node.
-- @return #ASTAR.Node The node.
function ASTAR:GetNodeFromCoordinate(Coordinate)

  local node={} --#ASTAR.Node
  
  node.coordinate=Coordinate
  node.surfacetype=Coordinate:GetSurfaceType()
  node.id=self.counter
  
  node.valid={}
  node.cost={}
  
  self.counter=self.counter+1
  
  return node
end


--- Add a node to the table of grid nodes.
-- @param #ASTAR self
-- @param #ASTAR.Node Node The node to be added.
-- @return #ASTAR self
function ASTAR:AddNode(Node)

  self.nodes[Node.id]=Node
  self.Nnodes=self.Nnodes+1 
    
  return self
end

--- Add a node to the table of grid nodes specifying its coordinate.
-- @param #ASTAR self
-- @param Core.Point#COORDINATE Coordinate The coordinate where the node is created.
-- @return #ASTAR.Node The node.
function ASTAR:AddNodeFromCoordinate(Coordinate)

  local node=self:GetNodeFromCoordinate(Coordinate)
  
  self:AddNode(node)
    
  return node
end

--- Check if the coordinate of a node has is at a valid surface type.
-- @param #ASTAR self
-- @param #ASTAR.Node Node The node to be added.
-- @param #table SurfaceTypes Surface types, for example `{land.SurfaceType.WATER}`. By default all surface types are valid.
-- @return #boolean If true, surface type of node is valid.
function ASTAR:CheckValidSurfaceType(Node, SurfaceTypes)

  if SurfaceTypes then
  
    if type(SurfaceTypes)~="table" then
      SurfaceTypes={SurfaceTypes}
    end
    
    for _,surface in pairs(SurfaceTypes) do
      if surface==Node.surfacetype then
        return true
      end
    end
  
    return false
    
  else
    return true
  end

end

--- Add a function to determine if a neighbour of a node is valid.
-- @param #ASTAR self
-- @param #function NeighbourFunction Function that needs to return *true* for a neighbour to be valid.
-- @param ... Condition function arguments if any.
-- @return #ASTAR self
function ASTAR:SetValidNeighbourFunction(NeighbourFunction, ...)

  self.ValidNeighbourFunc=NeighbourFunction
  
  self.ValidNeighbourArg={}
  if arg then
    self.ValidNeighbourArg=arg
  end
  
  return self
end


--- Set valid neighbours to require line of sight between two nodes.
-- @param #ASTAR self
-- @param #number CorridorWidth Width of LoS corridor in meters.
-- @return #ASTAR self
function ASTAR:SetValidNeighbourLoS(CorridorWidth)

  self:SetValidNeighbourFunction(ASTAR.LoS, CorridorWidth)

  return self
end

--- Set valid neighbours to be in a certain distance.
-- @param #ASTAR self
-- @param #number MaxDistance Max distance between nodes in meters. Default is 2000 m.
-- @return #ASTAR self
function ASTAR:SetValidNeighbourDistance(MaxDistance)

  self:SetValidNeighbourFunction(ASTAR.DistMax, MaxDistance)

  return self
end

--- Set valid neighbours to be in a certain distance.
-- @param #ASTAR self
-- @param #number MaxDistance Max distance between nodes in meters. Default is 2000 m.
-- @return #ASTAR self
function ASTAR:SetValidNeighbourRoad(MaxDistance)

  self:SetValidNeighbourFunction(ASTAR.Road, MaxDistance)

  return self
end

--- Set the function which calculates the "cost" to go from one to another node.
-- The first to arguments of this function are always the two nodes under consideration. But you can add optional arguments.
-- Very often the distance between nodes is a good measure for the cost.
-- @param #ASTAR self
-- @param #function CostFunction Function that returns the "cost".
-- @param ... Condition function arguments if any.
-- @return #ASTAR self
function ASTAR:SetCostFunction(CostFunction, ...)

  self.CostFunc=CostFunction
  
  self.CostArg={}
  if arg then
    self.CostArg=arg
  end
  
  return self
end

--- Set heuristic cost to go from one node to another to be their 2D distance.
-- @param #ASTAR self
-- @return #ASTAR self
function ASTAR:SetCostDist2D()

  self:SetCostFunction(ASTAR.Dist2D)

  return self
end

--- Set heuristic cost to go from one node to another to be their 3D distance.
-- @param #ASTAR self
-- @return #ASTAR self
function ASTAR:SetCostDist3D()

  self:SetCostFunction(ASTAR.Dist3D)

  return self
end

--- Set heuristic cost to go from one node to another to be their 3D distance.
-- @param #ASTAR self
-- @return #ASTAR self
function ASTAR:SetCostRoad()

  self:SetCostFunction(ASTAR)

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Grid functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a rectangular grid of nodes between star and end coordinate.
-- The coordinate system is oriented along the line between start and end point.
-- @param #ASTAR self
-- @param #table ValidSurfaceTypes Valid surface types. By default is all surfaces are allowed.
-- @param #number BoxHY Box "height" in meters along the y-coordinate. Default 40000 meters (40 km).
-- @param #number SpaceX Additional space in meters before start and after end coordinate. Default 10000 meters (10 km).
-- @param #number deltaX Increment in the direction of start to end coordinate in meters. Default 2000 meters.
-- @param #number deltaY Increment perpendicular to the direction of start to end coordinate in meters. Default is same as deltaX.
-- @param #boolean MarkGrid If true, create F10 map markers at grid nodes.
-- @return #ASTAR self
function ASTAR:CreateGrid(ValidSurfaceTypes, BoxHY, SpaceX, deltaX, deltaY, MarkGrid)

  -- Note that internally
  -- x coordinate is z: x-->z  Line from start to end
  -- y coordinate is x: y-->x  Perpendicular

  -- Grid length and width.
  local Dz=SpaceX or 10000
  local Dx=BoxHY and BoxHY/2 or 20000

  -- Increments.
  local dz=deltaX or 2000  
  local dx=deltaY or dz
  
  -- Heading from start to end coordinate.
  local angle=self.startCoord:HeadingTo(self.endCoord)
  
  --Distance between start and end.
  local dist=self.startCoord:Get2DDistance(self.endCoord)+2*Dz
  
  -- Origin of map. Needed to translate back to wanted position.
  local co=COORDINATE:New(0, 0, 0)
  local do1=co:Get2DDistance(self.startCoord)
  local ho1=co:HeadingTo(self.startCoord)
  
  -- Start of grid.
  local xmin=-Dx
  local zmin=-Dz
  
  -- Number of grid points.
  local nz=dist/dz+1
  local nx=2*Dx/dx+1
  
  -- Debug info.
  local text=string.format("Building grid with nx=%d ny=%d => total=%d nodes", nx, nz, nx*nz)
  self:T(self.lid..text)
  
  -- Loop over x and z coordinate to create a 2D grid.
  for i=1,nx do
  
    -- x coordinate perpendicular to z.
    local x=xmin+dx*(i-1)
  
    for j=1,nz do
    
      -- z coordinate connecting start and end.
      local z=zmin+dz*(j-1)
      
      -- Rotate 2D.
      local vec3=UTILS.Rotate2D({x=x, y=0, z=z}, angle)
      
      -- Coordinate of the node.
      local c=COORDINATE:New(vec3.z, vec3.y, vec3.x):Translate(do1, ho1, true)
        
      -- Create a node at this coordinate.
      local node=self:GetNodeFromCoordinate(c)
        
      -- Check if node has valid surface type.
      if self:CheckValidSurfaceType(node, ValidSurfaceTypes) then
          
        if MarkGrid then
          c:MarkToAll(string.format("i=%d, j=%d surface=%d", i, j, node.surfacetype))
        end
          
        -- Add node to grid.
        self:AddNode(node)
        
      end
    
    end
  end
    
  -- Debug info.
  local text=string.format("Done building grid!")
  self:T2(self.lid..text)

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Valid neighbour functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function to check if two nodes have line of sight (LoS).
-- @param #ASTAR.Node nodeA First node.
-- @param #ASTAR.Node nodeB Other node.
-- @param #number corridor (Optional) Width of corridor in meters.
-- @return #boolean If true, two nodes have LoS.
function ASTAR.LoS(nodeA, nodeB, corridor)

  local offset=1
  
  local dx=corridor and corridor/2 or nil
  local dy=dx
  
  local cA=nodeA.coordinate:GetVec3()
  local cB=nodeB.coordinate:GetVec3()
  cA.y=offset
  cB.y=offset

  local los=land.isVisible(cA, cB)
  
  if los and corridor then
  
    -- Heading from A to B.
    local heading=nodeA.coordinate:HeadingTo(nodeB.coordinate)
    
    local Ap=UTILS.VecTranslate(cA, dx, heading+90)
    local Bp=UTILS.VecTranslate(cB, dx, heading+90)

    los=land.isVisible(Ap, Bp)
    
    if los then

      local Am=UTILS.VecTranslate(cA, dx, heading-90)
      local Bm=UTILS.VecTranslate(cB, dx, heading-90)
    
      los=land.isVisible(Am, Bm)
    end
    
  end

  return los
end

--- Function to check if two nodes have a road connection.
-- @param #ASTAR.Node nodeA First node.
-- @param #ASTAR.Node nodeB Other node.
-- @return #boolean If true, two nodes are connected via a road.
function ASTAR.Road(nodeA, nodeB)

  local path=land.findPathOnRoads("roads", nodeA.coordinate.x, nodeA.coordinate.z, nodeB.coordinate.x, nodeB.coordinate.z)
  
  if path then
    return true    
  else
    return false
  end

end

--- Function to check if distance between two nodes is less than a threshold distance.
-- @param #ASTAR.Node nodeA First node.
-- @param #ASTAR.Node nodeB Other node.
-- @param #number distmax Max distance in meters. Default is 2000 m.
-- @return #boolean If true, distance between the two nodes is below threshold.
function ASTAR.DistMax(nodeA, nodeB, distmax)

  distmax=distmax or 2000

  local dist=nodeA.coordinate:Get2DDistance(nodeB.coordinate)
  
  return dist<=distmax
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Heuristic cost functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Heuristic cost is given by the 2D distance between the nodes. 
-- @param #ASTAR.Node nodeA First node.
-- @param #ASTAR.Node nodeB Other node.
-- @return #number Distance between the two nodes.
function ASTAR.Dist2D(nodeA, nodeB)
  local dist=nodeA.coordinate:Get2DDistance(nodeB)
  return dist
end

--- Heuristic cost is given by the 3D distance between the nodes. 
-- @param #ASTAR.Node nodeA First node.
-- @param #ASTAR.Node nodeB Other node.
-- @return #number Distance between the two nodes.
function ASTAR.Dist3D(nodeA, nodeB)
  local dist=nodeA.coordinate:Get3DDistance(nodeB.coordinate)
  return dist
end

--- Heuristic cost is given by the distance between the nodes on road. 
-- @param #ASTAR.Node nodeA First node.
-- @param #ASTAR.Node nodeB Other node.
-- @return #number Distance between the two nodes.
function ASTAR.DistRoad(nodeA, nodeB)

  -- Get the path.
  local path=land.findPathOnRoads("roads", nodeA.coordinate.x, nodeA.coordinate.z, nodeB.coordinate.x, nodeB.coordinate.z)
  
  if path then
  
    local dist=0
    
    for i=2,#path do
      local b=path[i] --DCS#Vec2
      local a=path[i-1] --DCS#Vec2
      
      dist=dist+UTILS.VecDist2D(a,b)
      
    end

    return dist
  end
  

  return math.huge
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Find the closest node from a given coordinate.
-- @param #ASTAR self
-- @param Core.Point#COORDINATE Coordinate.
-- @return #ASTAR.Node Cloest node to the coordinate.
-- @return #number Distance to closest node in meters.
function ASTAR:FindClosestNode(Coordinate)

  local distMin=math.huge
  local closeNode=nil
  
  for _,_node in pairs(self.nodes) do
    local node=_node --#ASTAR.Node
    
    local dist=node.coordinate:Get2DDistance(Coordinate)
    
    if dist<distMin then
      distMin=dist
      closeNode=node
    end
    
  end
    
  return closeNode, distMin
end

--- Find the start node.
-- @param #ASTAR self
-- @param #ASTAR.Node Node The node to be added to the nodes table.
-- @return #ASTAR self
function ASTAR:FindStartNode()

  local node, dist=self:FindClosestNode(self.startCoord)
  
  self.startNode=node
  
  if dist>1000 then
    self:T(self.lid.."Adding start node to node grid!")
    self:AddNode(node)
  end
    
  return self
end

--- Add a node.
-- @param #ASTAR self
-- @param #ASTAR.Node Node The node to be added to the nodes table.
-- @return #ASTAR self
function ASTAR:FindEndNode()

  local node, dist=self:FindClosestNode(self.endCoord)

  self.endNode=node
  
  if dist>1000 then
    self:T(self.lid.."Adding end node to node grid!")
    self:AddNode(node)
  end
    
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Main A* pathfinding function
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- A* pathfinding function. This seaches the path along nodes between start and end nodes/coordinates.
-- @param #ASTAR self
-- @param #boolean ExcludeStartNode If *true*, do not include start node in found path. Default is to include it.
-- @param #boolean ExcludeEndNode If *true*, do not include end node in found path. Default is to include it.
-- @return #table Table of nodes from start to finish.
function ASTAR:GetPath(ExcludeStartNode, ExcludeEndNode)

  self:FindStartNode()
  self:FindEndNode()

  local nodes=self.nodes
  local start=self.startNode
  local goal=self.endNode

  -- Sets.
  local openset   = {}
  local closedset = {}
  local came_from = {}
  local g_score   = {}
  local f_score   = {}
  
  openset[start.id]=true
  local Nopen=1
  
  -- Initial scores.
  g_score[start.id]=0
  f_score[start.id]=g_score[start.id]+self:_HeuristicCost(start, goal)
  
  -- Set start time.
  local T0=timer.getAbsTime()

  -- Debug message.
  local text=string.format("Starting A* pathfinding with %d Nodes", self.Nnodes)
  self:T(self.lid..text)
  
  local Tstart=UTILS.GetOSTime()

  -- Loop while we still have an open set.
  while Nopen > 0 do
  
    -- Get current node.
    local current=self:_LowestFscore(openset, f_score)
    
    -- Check if we are at the end node.
    if current.id==goal.id then
    
      local path=self:_UnwindPath({}, came_from, goal)
      
      if not ExcludeEndNode then
        table.insert(path, goal)
      end
      
      if ExcludeStartNode then
        table.remove(path, 1)
      end
      
      local Tstop=UTILS.GetOSTime()
      
      local dT=nil
      if Tstart and Tstop then
        dT=Tstop-Tstart
      end
      
      -- Debug message.
      local text=string.format("Found path with %d nodes (%d total)", #path, self.Nnodes)
      if dT then
        text=text..string.format(", OS Time %.6f sec", dT)
      end
      text=text..string.format(", Nvalid=%d [%d cached]", self.nvalid, self.nvalidcache)
      text=text..string.format(", Ncost=%d [%d cached]", self.ncost, self.ncostcache)
      self:T(self.lid..text)
      
      return path
    end

    -- Move Node from open to closed set.
    openset[current.id]=nil
    Nopen=Nopen-1
    closedset[current.id]=true
    
    -- Get neighbour nodes.
    local neighbors=self:_NeighbourNodes(current, nodes)
    
    -- Loop over neighbours.
    for _,neighbor in pairs(neighbors) do
    
      if self:_NotIn(closedset, neighbor.id) then
      
        local tentative_g_score=g_score[current.id]+self:_DistNodes(current, neighbor)
         
        if self:_NotIn(openset, neighbor.id) or tentative_g_score < g_score[neighbor.id] then
        
          came_from[neighbor]=current
          
          g_score[neighbor.id]=tentative_g_score
          f_score[neighbor.id]=g_score[neighbor.id]+self:_HeuristicCost(neighbor, goal)
          
          if self:_NotIn(openset, neighbor.id) then
            -- Add to open set.
            openset[neighbor.id]=true
            Nopen=Nopen+1
          end
          
        end
      end
    end
  end

  -- Debug message.
  local text=string.format("WARNING: Could NOT find valid path!")
  self:E(self.lid..text)
  MESSAGE:New(text, 60, "ASTAR"):ToAllIf(self.Debug)
  
  return nil -- no valid path
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- A* pathfinding helper functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Heuristic "cost" function to go from node A to node B. Default is the distance between the nodes.
-- @param #ASTAR self
-- @param #ASTAR.Node nodeA Node A.
-- @param #ASTAR.Node nodeB Node B.
-- @return #number "Cost" to go from node A to node B.
function ASTAR:_HeuristicCost(nodeA, nodeB)
  
  -- Counter.
  self.ncost=self.ncost+1

  -- Get chached cost if available.
  local cost=nodeA.cost[nodeB.id]
  if cost~=nil then
    self.ncostcache=self.ncostcache+1
    return cost
  end

  local cost=nil
  if self.CostFunc then
    cost=self.CostFunc(nodeA, nodeB, unpack(self.CostArg))
  else
    cost=self:_DistNodes(nodeA, nodeB)
  end
  
  nodeA.cost[nodeB.id]=cost
  nodeB.cost[nodeA.id]=cost  -- Symmetric problem. 
  
  return cost
end

--- Check if going from a node to a neighbour is possible.
-- @param #ASTAR self
-- @param #ASTAR.Node node A node.
-- @param #ASTAR.Node neighbor Neighbour node.
-- @return #boolean If true, transition between nodes is possible.
function ASTAR:_IsValidNeighbour(node, neighbor)

  -- Counter.
  self.nvalid=self.nvalid+1
  
  local valid=node.valid[neighbor.id]
  if valid~=nil then
    --env.info(string.format("Node %d has valid=%s neighbour %d", node.id, tostring(valid), neighbor.id))
    self.nvalidcache=self.nvalidcache+1
    return valid
  end

  local valid=nil
  if self.ValidNeighbourFunc then
    valid=self.ValidNeighbourFunc(node, neighbor, unpack(self.ValidNeighbourArg))  
  else
    valid=true
  end

  node.valid[neighbor.id]=valid
  neighbor.valid[node.id]=valid  -- Symmetric problem. 

  return valid
end

--- Calculate 2D distance between two nodes.
-- @param #ASTAR self
-- @param #ASTAR.Node nodeA Node A.
-- @param #ASTAR.Node nodeB Node B.
-- @return #number Distance between nodes in meters.
function ASTAR:_DistNodes(nodeA, nodeB)
  return nodeA.coordinate:Get2DDistance(nodeB.coordinate)
end

--- Function that calculates the lowest F score.
-- @param #ASTAR self
-- @param #table set The set of nodes IDs.
-- @param #number f_score F score.
-- @return #ASTAR.Node Best node.
function ASTAR:_LowestFscore(set, f_score)

  local lowest, bestNode = ASTAR.INF, nil
  
  for nid,node in pairs(set) do
  
    local score=f_score[nid]
    
    if score<lowest then
      lowest, bestNode = score, nid
    end
  end
  
  return self.nodes[bestNode]
end

--- Function to get valid neighbours of a node.
-- @param #ASTAR self
-- @param #ASTAR.Node theNode The node.
-- @param #table nodes Possible neighbours.
-- @param #table Valid neighbour nodes.
function ASTAR:_NeighbourNodes(theNode, nodes)

  local neighbors = {}
  
  for _,node in pairs(nodes) do
  
    if theNode.id~=node.id then
    
      local isvalid=self:_IsValidNeighbour(theNode, node)
    
      if isvalid then
        table.insert(neighbors, node)
      end
      
    end
    
  end
  
  return neighbors
end

--- Function to check if a node is not in a set.
-- @param #ASTAR self
-- @param #table set Set of nodes.
-- @param #ASTAR.Node theNode The node to check.
-- @return #boolean If true, the node is not in the set.
function ASTAR:_NotIn(set, theNode)
  return set[theNode]==nil
end

--- Unwind path function.
-- @param #ASTAR self
-- @param #table flat_path Flat path.
-- @param #table map Map.
-- @param #ASTAR.Node current_node The current node.
-- @return #table Unwinded path.
function ASTAR:_UnwindPath( flat_path, map, current_node )

  if map [current_node] then
    table.insert (flat_path, 1, map[current_node]) 
    return self:_UnwindPath(flat_path, map, map[current_node])
  else
    return flat_path
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- **Core** - TACAN and other beacons.
-- 
-- ===
-- 
-- ## Features:
-- 
--   * Provide beacon functionality to assist pilots.
--
-- ===
--
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/Core/Beacon)
--
-- ===
--
-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky
--
-- @module Core.Beacon
-- @image Core_Radio.JPG

--- *In order for the light to shine so brightly, the darkness must be present.* -- Francis Bacon
-- 
-- After attaching a @{#BEACON} to your @{Wrapper.Positionable#POSITIONABLE}, you need to select the right function to activate the kind of beacon you want. 
-- There are two types of BEACONs available : the (aircraft) TACAN Beacon and the general purpose Radio Beacon.
-- Note that in both case, you can set an optional parameter : the `BeaconDuration`. This can be very useful to simulate the battery time if your BEACON is
-- attach to a cargo crate, for example. 
-- 
-- ## Aircraft TACAN Beacon usage
-- 
-- This beacon only works with airborne @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}. Use @{#BEACON.ActivateTACAN}() to set the beacon parameters and start the beacon.
-- Use @{#BEACON.StopRadioBeacon}() to stop it.
-- 
-- ## General Purpose Radio Beacon usage
-- 
-- This beacon will work with any @{Wrapper.Positionable#POSITIONABLE}, but **it won't follow the @{Wrapper.Positionable#POSITIONABLE}** ! This means that you should only use it with
-- @{Wrapper.Positionable#POSITIONABLE} that don't move, or move very slowly. Use @{#BEACON.RadioBeacon}() to set the beacon parameters and start the beacon.
-- Use @{#BEACON.StopRadioBeacon}() to stop it.
-- 
-- @type BEACON
-- @field #string ClassName Name of the class "BEACON".
-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{Wrapper.Controllable#CONTROLLABLE} that will receive radio capabilities.
-- @field #number UniqueName Counter to make the unique naming work.
-- @extends Core.Base#BASE
BEACON = {
  ClassName    = "BEACON",
  Positionable = nil,
  name         = nil,
  UniqueName = 0, 
}

--- Beacon types supported by DCS. 
-- @type BEACON.Type
-- @field #number NULL
-- @field #number VOR
-- @field #number DME
-- @field #number VOR_DME
-- @field #number TACAN TACtical Air Navigation system.
-- @field #number VORTAC
-- @field #number RSBN
-- @field #number BROADCAST_STATION
-- @field #number HOMER
-- @field #number AIRPORT_HOMER
-- @field #number AIRPORT_HOMER_WITH_MARKER
-- @field #number ILS_FAR_HOMER
-- @field #number ILS_NEAR_HOMER
-- @field #number ILS_LOCALIZER
-- @field #number ILS_GLIDESLOPE
-- @field #number PRMG_LOCALIZER
-- @field #number PRMG_GLIDESLOPE
-- @field #number ICLS Same as ICLS glideslope.
-- @field #number ICLS_LOCALIZER
-- @field #number ICLS_GLIDESLOPE
-- @field #number NAUTICAL_HOMER
BEACON.Type={
  NULL                      = 0, 
  VOR                       = 1,
  DME                       = 2,
  VOR_DME                   = 3, 
  TACAN                     = 4,
  VORTAC                    = 5, 
  RSBN                      = 128,
  BROADCAST_STATION         = 1024,
  HOMER                     = 8,
  AIRPORT_HOMER             = 4104,
  AIRPORT_HOMER_WITH_MARKER = 4136,
  ILS_FAR_HOMER             = 16408,
  ILS_NEAR_HOMER            = 16424,
  ILS_LOCALIZER             = 16640,
  ILS_GLIDESLOPE            = 16896,
  PRMG_LOCALIZER            = 33024,
  PRMG_GLIDESLOPE           = 33280,
  ICLS                      = 131584, --leaving this in here but it is the same as ICLS_GLIDESLOPE
  ICLS_LOCALIZER            = 131328,
  ICLS_GLIDESLOPE           = 131584,
  NAUTICAL_HOMER            = 65536,
}

--- Beacon systems supported by DCS. https://wiki.hoggitworld.com/view/DCS_command_activateBeacon
-- @type BEACON.System
-- @field #number PAR_10 ?
-- @field #number RSBN_5 Russian VOR/DME system.
-- @field #number TACAN TACtical Air Navigation system on ground.
-- @field #number TACAN_TANKER_X TACtical Air Navigation system for tankers on X band.
-- @field #number TACAN_TANKER_Y TACtical Air Navigation system for tankers on Y band.
-- @field #number VOR Very High Frequency Omni-Directional Range
-- @field #number ILS_LOCALIZER ILS localizer
-- @field #number ILS_GLIDESLOPE ILS glide slope.
-- @field #number PRGM_LOCALIZER PRGM localizer.
-- @field #number PRGM_GLIDESLOPE PRGM glide slope.
-- @field #number BROADCAST_STATION Broadcast station.
-- @field #number VORTAC Radio-based navigational aid for aircraft pilots consisting of a co-located VHF omni-directional range (VOR) beacon and a tactical air navigation system (TACAN) beacon.
-- @field #number TACAN_AA_MODE_X TACtical Air Navigation for aircraft on X band.
-- @field #number TACAN_AA_MODE_Y TACtical Air Navigation for aircraft on Y band.
-- @field #number VORDME Radio beacon that combines a VHF omnidirectional range (VOR) with a distance measuring equipment (DME).
-- @field #number ICLS_LOCALIZER Carrier landing system.
-- @field #number ICLS_GLIDESLOPE Carrier landing system.
BEACON.System={
  PAR_10            = 1,
  RSBN_5            = 2,
  TACAN             = 3,
  TACAN_TANKER_X    = 4,
  TACAN_TANKER_Y    = 5,
  VOR               = 6,
  ILS_LOCALIZER     = 7,
  ILS_GLIDESLOPE    = 8,
  PRMG_LOCALIZER    = 9,
  PRMG_GLIDESLOPE   = 10,
  BROADCAST_STATION = 11,
  VORTAC            = 12,
  TACAN_AA_MODE_X   = 13,
  TACAN_AA_MODE_Y   = 14,
  VORDME            = 15,
  ICLS_LOCALIZER    = 16,
  ICLS_GLIDESLOPE   = 17,
}

--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc.
-- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead.
-- @param #BEACON self
-- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Wrapper.Positionable} that will receive radio capabilities.
-- @return #BEACON Beacon object or #nil if the positionable is invalid.
function BEACON:New(Positionable)

  -- Inherit BASE.
  local self=BASE:Inherit(self, BASE:New()) --#BEACON

  -- Debug.
  self:F(Positionable)

  -- Set positionable.
  if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid
    self.Positionable = Positionable
    self.name=Positionable:GetName()
    self:I(string.format("New BEACON %s", tostring(self.name)))
    return self
  end

  self:E({"The passed positionable is invalid, no BEACON created", Positionable})
  return nil
end

--- Activates a TACAN BEACON.
-- @param #BEACON self
-- @param #number Channel TACAN channel, i.e. the "10" part in "10Y".
-- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y".
-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon.
-- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available.
-- @param #number Duration How long will the beacon last in seconds. Omit for forever.
-- @return #BEACON self
-- @usage
-- -- Let's create a TACAN Beacon for a tanker
-- local myUnit = UNIT:FindByName("MyUnit") 
-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon
-- 
-- myBeacon:ActivateTACAN(20, "Y", "TEXACO", true) -- Activate the beacon
function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration)
  self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration})

  Mode=Mode or "Y"

  -- Get frequency.
  local Frequency=UTILS.TACANToFrequency(Channel, Mode)

  -- Check.
  if not Frequency then 
    self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"})
    return self
  end

  -- Beacon type.
  local Type=BEACON.Type.TACAN

  -- Beacon system.  
  local System=BEACON.System.TACAN

  -- Check if unit is an aircraft and set system accordingly.
  local AA=self.Positionable:IsAir()


  if AA then
    System=5 --NOTE: 5 is how you cat the correct tanker behaviour! --BEACON.System.TACAN_TANKER
    -- Check if "Y" mode is selected for aircraft.
    if Mode=="X" then
      --self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y!", self.Positionable})
      System=BEACON.System.TACAN_TANKER_X
    else
      System=BEACON.System.TACAN_TANKER_Y
    end
  end

  -- Attached unit.
  local UnitID=self.Positionable:GetID()

  -- Debug.
  self:I({string.format("BEACON Activating TACAN %s: Channel=%d%s, Morse=%s, Bearing=%s, Duration=%s!", tostring(self.name), Channel, Mode, Message, tostring(Bearing), tostring(Duration))})

  -- Start beacon.
  self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing)

  -- Stop scheduler.
  if Duration then
    self.Positionable:DeactivateBeacon(Duration)
  end

  return self
end

--- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system.
-- @param #BEACON self
-- @param #number Channel ICLS channel.
-- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon.
-- @param #number Duration How long will the beacon last in seconds. Omit for forever.
-- @return #BEACON self
function BEACON:ActivateICLS(Channel, Callsign, Duration)
  self:F({Channel=Channel, Callsign=Callsign, Duration=Duration})

  -- Attached unit.
  local UnitID=self.Positionable:GetID()

  -- Debug
  self:T2({"ICLS BEACON started!"})

  -- Start beacon.
  self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign)

  -- Stop scheduler
  if Duration then -- Schedule the stop of the BEACON if asked by the MD
    self.Positionable:DeactivateBeacon(Duration)
  end

  return self
end

--- Activates a LINK4 BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system.
-- @param #BEACON self
-- @param #number Frequency LINK4 FRequency in MHz, eg 336.
-- @param #string Morse The ID that is going to be coded in Morse and broadcasted by the beacon.
-- @param #number Duration How long will the beacon last in seconds. Omit for forever.
-- @return #BEACON self
function BEACON:ActivateLink4(Frequency, Morse, Duration)
  self:F({Frequency=Frequency, Morse=Morse, Duration=Duration})

  -- Attached unit.
  local UnitID=self.Positionable:GetID()

  -- Debug
  self:T2({"LINK4 BEACON started!"})

  -- Start beacon.
  self.Positionable:CommandActivateLink4(Frequency,UnitID,Morse)

  -- Stop sheduler
  if Duration then -- Schedule the stop of the BEACON if asked by the MD
    self.Positionable:CommandDeactivateLink4(Duration)
  end

  return self
end

--- DEPRECATED: Please use @{#BEACON.ActivateTACAN}() instead.
-- Activates a TACAN BEACON on an Aircraft.
-- @param #BEACON self
-- @param #number TACANChannel (the "10" part in "10Y"). Note that AA TACAN are only available on Y Channels
-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon
-- @param #boolean Bearing Can the BEACON be homed on ?
-- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever.
-- @return #BEACON self
-- @usage
-- -- Let's create a TACAN Beacon for a tanker
-- local myUnit = UNIT:FindByName("MyUnit") 
-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon
-- 
-- myBeacon:AATACAN(20, "TEXACO", true) -- Activate the beacon
function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration)
  self:F({TACANChannel, Message, Bearing, BeaconDuration})
  self:E("This method is DEPRECATED! Please use ActivateTACAN() instead.")

  local IsValid = true

  if not self.Positionable:IsAir() then
    self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft ! The BEACON is not emitting", self.Positionable})
    IsValid = false
  end

  local Frequency = self:_TACANToFrequency(TACANChannel, "Y")
  if not Frequency then 
    self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"})
    IsValid = false
  end

  -- I'm using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the bearing shows its bearing or 14 (TACAN_AA_MODE_Y) if it does not
  local System
  if Bearing then
    System = BEACON.System.TACAN_TANKER_Y
  else
    System = BEACON.System.TACAN_AA_MODE_Y
  end

  if IsValid then -- Starts the BEACON
    self:T2({"AA TACAN BEACON started !"})
    self.Positionable:SetCommand({
      id = "ActivateBeacon",
      params = {
        type = BEACON.Type.TACAN,
        system = System,
        callsign = Message,
        AA = true,
        frequency = Frequency,
        bearing = Bearing,
        modeChannel = "Y",
        }
      })

    if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD
      SCHEDULER:New(nil, 
      function()
        self:StopAATACAN()
      end, {}, BeaconDuration)
    end
  end

  return self
end

--- Stops the AA TACAN BEACON
-- @param #BEACON self
-- @return #BEACON self
function BEACON:StopAATACAN()
  self:F()
  if not self.Positionable then
    self:E({"Start the beacon first before stoping it !"})
  else
    self.Positionable:SetCommand({
      id = 'DeactivateBeacon', 
        params = { 
      } 
    })
  end
end

--- Activates a general purpose Radio Beacon
-- This uses the very generic singleton function "trigger.action.radioTransmission()" provided by DCS to broadcast a sound file on a specific frequency.
-- Although any frequency could be used, only a few DCS Modules can home on radio beacons at the time of writing, i.e. the Mi-8, Huey, Gazelle etc.
-- The following e.g. can home in on these specific frequencies : 
-- * **Mi8**
-- * R-828 -> 20-60MHz
-- * ARKUD -> 100-150MHz (canal 1 : 114166, canal 2 : 114333, canal 3 : 114583, canal 4 : 121500, canal 5 : 123100, canal 6 : 124100) AM
-- * ARK9 -> 150-1300KHz
-- * **Huey**
-- * AN/ARC-131 -> 30-76 Mhz FM
-- @param #BEACON self
-- @param #string FileName The name of the audio file
-- @param #number Frequency in MHz
-- @param #number Modulation either radio.modulation.AM or radio.modulation.FM
-- @param #number Power in W
-- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever.
-- @return #BEACON self
-- @usage
-- -- Let's create a beacon for a unit in distress.
-- -- Frequency will be 40MHz FM (home-able by a Huey's AN/ARC-131)
-- -- The beacon they use is battery-powered, and only lasts for 5 min
-- local UnitInDistress = UNIT:FindByName("Unit1")
-- local UnitBeacon = UnitInDistress:GetBeacon()
-- 
-- -- Set the beacon and start it
-- UnitBeacon:RadioBeacon("MySoundFileSOS.ogg", 40, radio.modulation.FM, 20, 5*60)
function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDuration)
  self:F({FileName, Frequency, Modulation, Power, BeaconDuration})
  local IsValid = false
  
  Modulation = Modulation or radio.modulation.AM
  
  -- Check the filename
  if type(FileName) == "string" then
    if FileName:find(".ogg") or FileName:find(".wav") then
      if not FileName:find("l10n/DEFAULT/") then
        FileName = "l10n/DEFAULT/" .. FileName
      end
      IsValid = true
    end
  end
  if not IsValid then
    self:E({"File name invalid. Maybe something wrong with the extension? ", FileName})
  end

  -- Check the Frequency
  if type(Frequency) ~= "number" and IsValid then
    self:E({"Frequency invalid. ", Frequency})
    IsValid = false
  end
  Frequency = Frequency * 1000000 -- Conversion to Hz

  -- Check the modulation
  if Modulation ~= radio.modulation.AM and Modulation ~= radio.modulation.FM and IsValid then --TODO Maybe make this future proof if ED decides to add an other modulation ?
    self:E({"Modulation is invalid. Use DCS's enum radio.modulation.", Modulation})
    IsValid = false
  end

  -- Check the Power
  if type(Power) ~= "number" and IsValid then
    self:E({"Power is invalid. ", Power})
    IsValid = false
  end
  Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that

  if IsValid then
    self:T2({"Activating Beacon on ", Frequency, Modulation})
    -- Note that this is looped. I have to give this transmission a unique name, I use the class ID
    BEACON.UniqueName = BEACON.UniqueName + 1
    self.BeaconName = "MooseBeacon"..tostring(BEACON.UniqueName)
    trigger.action.radioTransmission(FileName, self.Positionable:GetPositionVec3(), Modulation, true, Frequency, Power, self.BeaconName)

     if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD
       SCHEDULER:New( nil, 
         function()
           self:StopRadioBeacon()
         end, {}, BeaconDuration)
     end
  end
  return self 
end

--- Stops the Radio Beacon
-- @param #BEACON self
-- @return #BEACON self
function BEACON:StopRadioBeacon()
  self:F()
  -- The unique name of the transmission is the class ID
  trigger.action.stopRadioTransmission(self.BeaconName)
  return self
end

--- Converts a TACAN Channel/Mode couple into a frequency in Hz
-- @param #BEACON self
-- @param #number TACANChannel
-- @param #string TACANMode
-- @return #number Frequecy
-- @return #nil if parameters are invalid
function BEACON:_TACANToFrequency(TACANChannel, TACANMode)
  self:F3({TACANChannel, TACANMode})

  if type(TACANChannel) ~= "number" then
    if TACANMode ~= "X" and TACANMode ~= "Y" then
      return nil -- error in arguments
    end
  end

-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137.
-- I have no idea what it does but it seems to work
  local A = 1151 -- 'X', channel >= 64
  local B = 64   -- channel >= 64

  if TACANChannel < 64 then
    B = 1
  end

  if TACANMode == 'Y' then
    A = 1025
    if TACANChannel < 64 then
      A = 1088
    end
  else -- 'X'
    if TACANChannel < 64 then
      A = 962
    end
  end

  return (A + TACANChannel - B) * 1000000
end
--- **Core** - Define any or all conditions to be evaluated.
--
-- **Main Features:**
--
--    * Add arbitrary numbers of conditon functions
--    * Evaluate *any* or *all* conditions
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Core/Condition).
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ===
-- @module Core.Condition
-- @image MOOSE.JPG

--- CONDITON class.
-- @type CONDITION
-- @field #string ClassName Name of the class.
-- @field #string lid Class id string for output to DCS log file.
-- @field #string name Name of the condition.
-- @field #boolean isAny General functions are evaluated as any condition.
-- @field #boolean negateResult Negate result of evaluation.
-- @field #boolean noneResult Boolean that is returned if no condition functions at all were specified.
-- @field #table functionsGen General condition functions.
-- @field #table functionsAny Any condition functions.
-- @field #table functionsAll All condition functions.
-- @field #number functionCounter Running number to determine the unique ID of condition functions.
-- @field #boolean defaultPersist Default persistence of condition functions.
-- 
-- @extends Core.Base#BASE

--- *Better three hours too soon than a minute too late.* - William Shakespeare
--
-- ===
--
-- # The CONDITION Concept
-- 
-- 
--
-- @field #CONDITION
CONDITION = {
  ClassName       = "CONDITION",
  lid             =   nil,
  functionsGen    =    {},
  functionsAny    =    {},
  functionsAll    =    {},
  functionCounter =     0, 
  defaultPersist  = false,
}

--- Condition function.
-- @type CONDITION.Function
-- @field #number uid Unique ID of the condition function.
-- @field #string type Type of the condition function: "gen", "any", "all".
-- @field #boolean persistence If `true`, this is persistent.
-- @field #function func Callback function to check for a condition. Must return a `#boolean`.
-- @field #table arg (Optional) Arguments passed to the condition callback function if any.

--- CONDITION class version.
-- @field #string version
CONDITION.version="0.3.0"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Make FSM. No sure if really necessary.
-- DONE: Option to remove condition functions.
-- DONE: Persistence option for condition functions.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new CONDITION object.
-- @param #CONDITION self
-- @param #string Name (Optional) Name used in the logs. 
-- @return #CONDITION self
function CONDITION:New(Name)

  -- Inherit BASE.
  local self=BASE:Inherit(self, BASE:New()) --#CONDITION
  
  self.name=Name or "Condition X"
  
  self:SetNoneResult(false)
  
  self.lid=string.format("%s | ", self.name)

  return self
end

--- Set that general condition functions return `true` if `any` function returns `true`. Default is that *all* functions must return `true`.
-- @param #CONDITION self
-- @param #boolean Any If `true`, *any* condition can be true. Else *all* conditions must result `true`.
-- @return #CONDITION self
function CONDITION:SetAny(Any)
  self.isAny=Any
  return self
end

--- Negate result.
-- @param #CONDITION self
-- @param #boolean Negate If `true`, result is negated else  not.
-- @return #CONDITION self
function CONDITION:SetNegateResult(Negate)
  self.negateResult=Negate
  return self
end

--- Set whether `true` or `false` is returned, if no conditions at all were specified. By default `false` is returned.
-- @param #CONDITION self
-- @param #boolean ReturnValue Returns this boolean.
-- @return #CONDITION self
function CONDITION:SetNoneResult(ReturnValue)
  if not ReturnValue then
    self.noneResult=false
  else
    self.noneResult=true
  end
  return self
end

--- Set whether condition functions are persistent, *i.e.* are removed.
-- @param #CONDITION self
-- @param #boolean IsPersistent If `true`, condition functions are persistent.
-- @return #CONDITION self
function CONDITION:SetDefaultPersistence(IsPersistent)
  self.defaultPersist=IsPersistent
  return self
end

--- Add a function that is evaluated. It must return a `#boolean` value, *i.e.* either `true` or `false` (or `nil`).
-- @param #CONDITION self
-- @param #function Function The function to call.
-- @param ... (Optional) Parameters passed to the function (if any).
-- 
-- @usage
-- local function isAequalB(a, b)
--   return a==b
-- end
-- 
-- myCondition:AddFunction(isAequalB, a, b)
-- 
-- @return #CONDITION.Function Condition function table.
function CONDITION:AddFunction(Function, ...)

  -- Condition function.
  local condition=self:_CreateCondition(0, Function, ...)

  -- Add to table.
  table.insert(self.functionsGen, condition)

  return condition
end

--- Add a function that is evaluated. It must return a `#boolean` value, *i.e.* either `true` or `false` (or `nil`).
-- @param #CONDITION self
-- @param #function Function The function to call.
-- @param ... (Optional) Parameters passed to the function (if any).
-- @return #CONDITION.Function Condition function table.
function CONDITION:AddFunctionAny(Function, ...)

  -- Condition function.
  local condition=self:_CreateCondition(1, Function, ...)

  -- Add to table.
  table.insert(self.functionsAny, condition)

  return condition
end

--- Add a function that is evaluated. It must return a `#boolean` value, *i.e.* either `true` or `false` (or `nil`).
-- @param #CONDITION self
-- @param #function Function The function to call.
-- @param ... (Optional) Parameters passed to the function (if any).
-- @return #CONDITION.Function Condition function table.
function CONDITION:AddFunctionAll(Function, ...)

  -- Condition function.
  local condition=self:_CreateCondition(2, Function, ...)

  -- Add to table.
  table.insert(self.functionsAll, condition)

  return condition
end

--- Remove a condition function.
-- @param #CONDITION self
-- @param #CONDITION.Function ConditionFunction The condition function to be removed.
-- @return #CONDITION self
function CONDITION:RemoveFunction(ConditionFunction)

  if ConditionFunction then
  
    local data=nil
    if ConditionFunction.type==0 then
      data=self.functionsGen
    elseif ConditionFunction.type==1 then
      data=self.functionsAny
    elseif ConditionFunction.type==2 then
      data=self.functionsAll
    end
    
    if data then
      for i=#data,1,-1 do
        local cf=data[i] --#CONDITION.Function
        if cf.uid==ConditionFunction.uid then
          self:T(self.lid..string.format("Removed ConditionFunction UID=%d", cf.uid))
          table.remove(data, i)
          return self
        end
      end
    end
  
  end

  return self
end

--- Remove all non-persistant condition functions.
-- @param #CONDITION self
-- @return #CONDITION self
function CONDITION:RemoveNonPersistant()

  for i=#self.functionsGen,1,-1 do
    local cf=self.functionsGen[i] --#CONDITION.Function
    if not cf.persistence then
      table.remove(self.functionsGen, i)
    end
  end 

  for i=#self.functionsAll,1,-1 do
    local cf=self.functionsAll[i] --#CONDITION.Function
    if not cf.persistence then
      table.remove(self.functionsAll, i)
    end
  end 

  for i=#self.functionsAny,1,-1 do
    local cf=self.functionsAny[i] --#CONDITION.Function
    if not cf.persistence then
      table.remove(self.functionsAny, i)
    end
  end 

  return self
end


--- Evaluate conditon functions.
-- @param #CONDITION self
-- @param #boolean AnyTrue If `true`, evaluation return `true` if *any* condition function returns `true`. By default, *all* condition functions must return true.
-- @return #boolean Result of condition functions.
function CONDITION:Evaluate(AnyTrue)

  -- Check if at least one function was given.
  if #self.functionsAll + #self.functionsAny + #self.functionsAll == 0 then
    return self.noneResult
  end

  -- Any condition for gen.
  local evalAny=self.isAny
  if AnyTrue~=nil then
    evalAny=AnyTrue
  end
  
  local isGen=nil
  if evalAny then
    isGen=self:_EvalConditionsAny(self.functionsGen)
  else
    isGen=self:_EvalConditionsAll(self.functionsGen)
  end
  
  -- Is any?
  local isAny=self:_EvalConditionsAny(self.functionsAny)
  
  -- Is all?
  local isAll=self:_EvalConditionsAll(self.functionsAll)
  
  -- Result.
  local result=isGen and isAny and isAll
  
  -- Negate result.
  if self.negateResult then
    result=not result
  end
  
  -- Debug message.
  self:T(self.lid..string.format("Evaluate: isGen=%s, isAny=%s, isAll=%s (negate=%s) ==> result=%s", tostring(isGen), tostring(isAny), tostring(isAll), tostring(self.negateResult), tostring(result)))

  return result
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check if all given condition are true.
-- @param #CONDITION self
-- @param #table functions Functions to evaluate.
-- @return #boolean If true, all conditions were true (or functions was empty/nil). Returns false if at least one condition returned false.
function CONDITION:_EvalConditionsAll(functions)

  -- At least one condition?
  local gotone=false


  -- Any stop condition must be true.
  for _,_condition in pairs(functions or {}) do
    local condition=_condition --#CONDITION.Function

    -- At least one condition was defined.
    gotone=true

    -- Call function.
    local istrue=condition.func(unpack(condition.arg))

    -- Any false will return false.
    if not istrue then
      return false
    end

  end
  
  -- All conditions were true.
  return true
end


--- Check if any of the given conditions is true.
-- @param #CONDITION self
-- @param #table functions Functions to evaluate.
-- @return #boolean If true, at least one condition is true (or functions was emtpy/nil).
function CONDITION:_EvalConditionsAny(functions)

  -- At least one condition?
  local gotone=false

  -- Any stop condition must be true.
  for _,_condition in pairs(functions or {}) do
    local condition=_condition --#CONDITION.Function
    
    -- At least one condition was defined.
    gotone=true

    -- Call function.
    local istrue=condition.func(unpack(condition.arg))

    -- Any true will return true.
    if istrue then
      return true
    end

  end
  
  -- No condition was true.
  if gotone then
    return false
  else
    -- No functions passed.
    return true
  end
end

--- Create conditon function object.
-- @param #CONDITION self
-- @param #number Ftype Function type: 0=Gen, 1=All, 2=Any.
-- @param #function Function The function to call.
-- @param ... (Optional) Parameters passed to the function (if any).
-- @return #CONDITION.Function Condition function.
function CONDITION:_CreateCondition(Ftype, Function, ...)

  -- Increase counter.
  self.functionCounter=self.functionCounter+1

  local condition={} --#CONDITION.Function

  condition.uid=self.functionCounter
  condition.type=Ftype or 0
  condition.persistence=self.defaultPersist
  condition.func=Function
  condition.arg={}
  if arg then
    condition.arg=arg
  end
  
  return condition
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Global Condition Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Condition to check if time is greater than a given threshold time.
-- @param #number Time Time in seconds.
-- @param #boolean Absolute If `true`, abs. mission time from `timer.getAbsTime()` is checked. Default is relative mission time from `timer.getTime()`.
-- @return #boolean Returns `true` if time is greater than give the time.
function CONDITION.IsTimeGreater(Time, Absolute)

  local Tnow=nil 
  
  if Absolute then
    Tnow=timer.getAbsTime()
  else
    Tnow=timer.getTime()
  end
  
  if Tnow>Time then
    return true
  else
    return false      
  end    

  return nil
end

--- Function that returns `true` (success) with a certain probability. For example, if you specify `Probability=80` there is an 80% chance that `true` is returned.
-- Technically, a random number between 0 and 100 is created. If the given success probability is less then this number, `true` is returned.
-- @param #number Probability Success probability in percent. Default 50 %.
-- @return #boolean Returns `true` for success and `false` otherwise.
function CONDITION.IsRandomSuccess(Probability)

  Probability=Probability or 50
  
  -- Create some randomness.
  math.random()
  math.random()
  math.random()

  -- Number between 0 and 100.
  local N=math.random()*100
  
  if N<Probability then
    return true
  else
    return false
  end

end

--- Function that returns always `true`
-- @return #boolean Returns `true` unconditionally.
function CONDITION.ReturnTrue()
  return true
end

--- Function that returns always `false`
-- @return #boolean Returns `false` unconditionally.
function CONDITION.ReturnFalse()
  return false
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- **Core** - Manage user flags to interact with the mission editor trigger system and server side scripts.
--
-- ===
-- 
-- ## Features:
-- 
--   * Set or get DCS user flags within running missions.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===
-- 
-- @module Core.UserFlag
-- @image Core_Userflag.JPG
-- 

do -- UserFlag

  -- @type USERFLAG
  -- @field #string ClassName Name of the class
  -- @field #string UserFlagName Name of the flag.
  -- @extends Core.Base#BASE


  --- Management of DCS User Flags.
  -- 
  -- # 1. USERFLAG constructor
  --   
  --   * @{#USERFLAG.New}(): Creates a new USERFLAG object.
  -- 
  -- @field #USERFLAG
  USERFLAG = {
    ClassName    = "USERFLAG",
    UserFlagName = nil,
  }

  --- USERFLAG Constructor.
  -- @param #USERFLAG self
  -- @param #string UserFlagName The name of the userflag, which is a free text string.
  -- @return #USERFLAG
  function USERFLAG:New( UserFlagName ) --R2.3

    local self = BASE:Inherit( self, BASE:New() ) -- #USERFLAG

    self.UserFlagName = UserFlagName

    return self
  end

  --- Get the userflag name.
  -- @param #USERFLAG self
  -- @return #string Name of the user flag.
  function USERFLAG:GetName()
    return self.UserFlagName
  end  

  --- Set the userflag to a given Number.
  -- @param #USERFLAG self
  -- @param #number Number The number value to set the flag to.
  -- @param #number Delay Delay in seconds, before the flag is set.
  -- @return #USERFLAG The userflag instance.
  -- @usage
  --   local BlueVictory = USERFLAG:New( "VictoryBlue" )
  --   BlueVictory:Set( 100 ) -- Set the UserFlag VictoryBlue to 100.
  --   
  function USERFLAG:Set( Number, Delay ) --R2.3

    if Delay and Delay>0 then
      self:ScheduleOnce(Delay, USERFLAG.Set, self, Number)
    else
      --env.info(string.format("Setting flag \"%s\" to %d at T=%.1f", self.UserFlagName, Number, timer.getTime()))
      trigger.action.setUserFlag( self.UserFlagName, Number )
    end

    return self
  end

  --- Get the userflag Number.
  -- @param #USERFLAG self
  -- @return #number Number The number value to be checked if it is the same as the userflag.
  -- @usage
  --   local BlueVictory = USERFLAG:New( "VictoryBlue" )
  --   local BlueVictoryValue = BlueVictory:Get() -- Get the UserFlag VictoryBlue value.
  --   
  function USERFLAG:Get() --R2.3

    return trigger.misc.getUserFlag( self.UserFlagName )
  end

  --- Check if the userflag has a value of Number.
  -- @param #USERFLAG self
  -- @param #number Number The number value to be checked if it is the same as the userflag.
  -- @return #boolean true if the Number is the value of the userflag.
  -- @usage
  --   local BlueVictory = USERFLAG:New( "VictoryBlue" )
  --   if BlueVictory:Is( 1 ) then
  --     return "Blue has won"
  --   end
  function USERFLAG:Is( Number ) --R2.3

    return trigger.misc.getUserFlag( self.UserFlagName ) == Number

  end

end
--- **Core** - Provides a handy means to create messages and reports.
--
-- ===
--
-- ## Features:
--
--   * Create text blocks that are formatted.
--   * Create automatic indents.
--   * Variate the delimiters between reporting lines.
--
-- ===
--
-- ### Authors: FlightControl : Design & Programming
--
-- @module Core.Report
-- @image Core_Report.JPG

---
-- @type REPORT
-- @extends Core.Base#BASE

--- Provides a handy means to create messages and reports.
-- @field #REPORT
REPORT = {
  ClassName = "REPORT",
  Title = "",
}

--- Create a new REPORT.
-- @param #REPORT self
-- @param #string Title
-- @return #REPORT
function REPORT:New( Title )

  local self = BASE:Inherit( self, BASE:New() ) -- #REPORT

  self.Report = {}

  self:SetTitle( Title or "" )
  self:SetIndent( 3 )

  return self
end

--- Has the REPORT Text?
-- @param #REPORT self
-- @return #boolean
function REPORT:HasText() -- R2.1

  return #self.Report > 0
end

--- Set indent of a REPORT.
-- @param #REPORT self
-- @param #number Indent
-- @return #REPORT
function REPORT:SetIndent( Indent ) -- R2.1
  self.Indent = Indent
  return self
end

--- Add a new line to a REPORT.
-- @param #REPORT self
-- @param #string Text
-- @return #REPORT
function REPORT:Add( Text )
  self.Report[#self.Report + 1] = Text
  return self
end

--- Add a new line to a REPORT, but indented. A separator character can be specified to separate the reported lines visually.
-- @param #REPORT self
-- @param #string Text The report text.
-- @param #string Separator (optional) The start of each report line can begin with an optional separator character. This can be a "-", or "#", or "*". You're free to choose what you find the best.
-- @return #REPORT
function REPORT:AddIndent( Text, Separator )
  self.Report[#self.Report + 1] = ((Separator and Separator .. string.rep( " ", self.Indent - 1 )) or string.rep( " ", self.Indent )) .. Text:gsub( "\n", "\n" .. string.rep( " ", self.Indent ) )
  return self
end

--- Produces the text of the report, taking into account an optional delimiter, which is \n by default.
-- @param #REPORT self
-- @param #string Delimiter (optional) A delimiter text.
-- @return #string The report text.
function REPORT:Text( Delimiter )
  Delimiter = Delimiter or "\n"
  local ReportText = (self.Title ~= "" and self.Title .. Delimiter or self.Title) .. table.concat( self.Report, Delimiter ) or ""
  return ReportText
end

--- Sets the title of the report.
-- @param #REPORT self
-- @param #string Title The title of the report.
-- @return #REPORT
function REPORT:SetTitle( Title )
  self.Title = Title
  return self
end

--- Gets the amount of report items contained in the report.
-- @param #REPORT self
-- @return #number Returns the number of report items contained in the report. 0 is returned if no report items are contained in the report. The title is not counted for.
function REPORT:GetCount()
  return #self.Report
end
--- **Core** - Prepares and handles the execution of functions over scheduled time (intervals).
--
-- ===
-- 
-- ## Features:
--
--   * Schedule functions over time,
--   * optionally in an optional specified time interval,
--   * optionally **repeating** with a specified time repeat interval,
--   * optionally **randomizing** with a specified time interval randomization factor,
--   * optionally **stop** the repeating after a specified time interval.
--
-- ===
--
-- # Demo Missions
-- 
-- ### [SCHEDULER Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Core/Scheduler)
--
-- ===
--
-- # YouTube Channel
--
-- ### None
--
-- ===
--
-- ### Contributions:
--
--   * FlightControl : Concept & Testing
--
-- ### Authors:
--
--   * FlightControl : Design & Programming
--
-- ===
--
-- @module Core.Scheduler
-- @image Core_Scheduler.JPG

--- The SCHEDULER class
-- @type SCHEDULER
-- @field #table Schedules Table of schedules.
-- @field #table MasterObject Master object.
-- @field #boolean ShowTrace Trace info if true.
-- @extends Core.Base#BASE

--- Creates and handles schedules over time, which allow to execute code at specific time intervals with randomization.
--
-- A SCHEDULER can manage **multiple** (repeating) schedules. Each planned or executing schedule has a unique **ScheduleID**.
-- The ScheduleID is returned when the method @{#SCHEDULER.Schedule}() is called.
-- It is recommended to store the ScheduleID in a variable, as it is used in the methods @{#SCHEDULER.Start}() and @{#SCHEDULER.Stop}(),
-- which can start and stop specific repeating schedules respectively within a SCHEDULER object.
--
-- ## SCHEDULER constructor
--
-- The SCHEDULER class is quite easy to use, but note that the New constructor has variable parameters:
--
-- The @{#SCHEDULER.New}() method returns 2 variables:
--
--  1. The SCHEDULER object reference.
--  2. The first schedule planned in the SCHEDULER object.
--
-- To clarify the different appliances, lets have a look at the following examples: 
--
-- ### Construct a SCHEDULER object without a persistent schedule.
--
--   * @{#SCHEDULER.New}( nil ): Setup a new SCHEDULER object, which is persistently executed after garbage collection.
--
--     MasterObject = SCHEDULER:New()
--     SchedulerID = MasterObject:Schedule( nil, ScheduleFunction, {} )
--
-- The above example creates a new MasterObject, but does not schedule anything.
-- A separate schedule is created by using the MasterObject using the method :Schedule..., which returns a ScheduleID
--
-- ### Construct a SCHEDULER object without a volatile schedule, but volatile to the Object existence...
--
--   * @{#SCHEDULER.New}( Object ): Setup a new SCHEDULER object, which is linked to the Object. When the Object is set to nil or destroyed, the SCHEDULER object will also be destroyed and stopped after garbage collection.
--
--     ZoneObject = ZONE:New( "ZoneName" )
--     MasterObject = SCHEDULER:New( ZoneObject )
--     SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} )
--     ...
--     ZoneObject = nil
--     garbagecollect()
--
-- The above example creates a new MasterObject, but does not schedule anything, and is bound to the existence of ZoneObject, which is a ZONE.
-- A separate schedule is created by using the MasterObject using the method :Schedule()..., which returns a ScheduleID
-- Later in the logic, the ZoneObject is put to nil, and garbage is collected.
-- As a result, the MasterObject will cancel any planned schedule.
--
-- ### Construct a SCHEDULER object with a persistent schedule.
--
--   * @{#SCHEDULER.New}( nil, Function, FunctionArguments, Start, ... ): Setup a new persistent SCHEDULER object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters.
--
--     MasterObject, SchedulerID = SCHEDULER:New( nil, ScheduleFunction, {} )
--
-- The above example creates a new MasterObject, and does schedule the first schedule as part of the call.
-- Note that 2 variables are returned here: MasterObject, ScheduleID...
--
-- ### Construct a SCHEDULER object without a schedule, but volatile to the Object existence...
--
--   * @{#SCHEDULER.New}( Object, Function, FunctionArguments, Start, ... ): Setup a new SCHEDULER object, linked to Object, and start a new schedule for the Function with the defined FunctionArguments according the Start and sequent parameters.
--
--     ZoneObject = ZONE:New( "ZoneName" )
--     MasterObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} )
--     SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} )
--     ...
--     ZoneObject = nil
--     garbagecollect()
--
-- The above example creates a new MasterObject, and schedules a method call (ScheduleFunction), 
-- and is bound to the existence of ZoneObject, which is a ZONE object (ZoneObject).
-- Both a MasterObject and a SchedulerID variable are returned.
-- Later in the logic, the ZoneObject is put to nil, and garbage is collected.
-- As a result, the MasterObject will cancel the planned schedule.
--
-- ## SCHEDULER timer stopping and (re-)starting.
--
-- The SCHEDULER can be stopped and restarted with the following methods:
--
--  * @{#SCHEDULER.Start}(): (Re-)Start the schedules within the SCHEDULER object. If a CallID is provided to :Start(), only the schedule referenced by CallID will be (re-)started.
--  * @{#SCHEDULER.Stop}(): Stop the schedules within the SCHEDULER object. If a CallID is provided to :Stop(), then only the schedule referenced by CallID will be stopped.
--
--     ZoneObject = ZONE:New( "ZoneName" )
--     MasterObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} )
--     SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 10 )
--     ...
--     MasterObject:Stop( SchedulerID )
--     ...
--     MasterObject:Start( SchedulerID )
--
-- The above example creates a new MasterObject, and does schedule the first schedule as part of the call.
-- Note that 2 variables are returned here: MasterObject, ScheduleID...
-- Later in the logic, the repeating schedule with SchedulerID is stopped.
-- A bit later, the repeating schedule with SchedulerId is (re)-started.
--
-- ## Create a new schedule
--
-- With the method @{#SCHEDULER.Schedule}() a new time event can be scheduled.
-- This method is used by the :New() constructor when a new schedule is planned.
--
-- Consider the following code fragment of the SCHEDULER object creation.
--
--     ZoneObject = ZONE:New( "ZoneName" )
--     MasterObject = SCHEDULER:New( ZoneObject )
--
-- Several parameters can be specified that influence the behavior of a Schedule.
--
-- ### A single schedule, immediately executed
--
--     SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {} )
--
-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within milliseconds ...
--
-- ### A single schedule, planned over time
--
--     SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10 )
--
-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds ...
--
-- ### A schedule with a repeating time interval, planned over time
--
--     SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60 )
--
-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds,
-- and repeating 60 every seconds ...
--
-- ### A schedule with a repeating time interval, planned over time, with time interval randomization
--
--     SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5 )
--
-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds,
-- and repeating 60 seconds, with a 50% time interval randomization ...
-- So the repeating time interval will be randomized using the **0.5**,
-- and will calculate between **60 - ( 60 * 0.5 )** and **60 + ( 60 * 0.5 )** for each repeat,
-- which is in this example between **30** and **90** seconds.
--
-- ### A schedule with a repeating time interval, planned over time, with time interval randomization, and stop after a time interval
--
--     SchedulerID = MasterObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 60, 0.5, 300 )
--
-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within 10 seconds,
-- The schedule will repeat every 60 seconds.
-- So the repeating time interval will be randomized using the **0.5**,
-- and will calculate between **60 - ( 60 * 0.5 )** and **60 + ( 60 * 0.5 )** for each repeat,
-- which is in this example between **30** and **90** seconds.
-- The schedule will stop after **300** seconds.
--
-- @field #SCHEDULER
SCHEDULER = {
  ClassName = "SCHEDULER",
  Schedules = {},
  MasterObject = nil,
  ShowTrace = nil,
}

--- SCHEDULER constructor.
-- @param #SCHEDULER self
-- @param #table MasterObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference.
-- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments.
-- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }.
-- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called.
-- @param #number Repeat Specifies the interval in seconds when the scheduler will call the event function.
-- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat.
-- @param #number Stop Specifies the amount of seconds when the scheduler will be stopped.
-- @return #SCHEDULER self.
-- @return #string The ScheduleID of the planned schedule.
function SCHEDULER:New( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop )

  local self = BASE:Inherit( self, BASE:New() ) -- #SCHEDULER
  self:F2( { Start, Repeat, RandomizeFactor, Stop } )

  local ScheduleID = nil

  self.MasterObject = MasterObject
  self.ShowTrace = false

  if SchedulerFunction then
    ScheduleID = self:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, 3 )
  end

  return self, ScheduleID
end

--- Schedule a new time event. Note that the schedule will only take place if the scheduler is *started*. Even for a single schedule event, the scheduler needs to be started also.
-- @param #SCHEDULER self
-- @param #table MasterObject Specified for which Moose object the timer is setup. If a value of nil is provided, a scheduler will be setup without an object reference.
-- @param #function SchedulerFunction The event function to be called when a timer event occurs. The event function needs to accept the parameters specified in SchedulerArguments.
-- @param #table SchedulerArguments Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }.
-- @param #number Start Specifies the amount of seconds that will be waited before the scheduling is started, and the event function is called.
-- @param #number Repeat Specifies the time interval in seconds when the scheduler will call the event function.
-- @param #number RandomizeFactor Specifies a randomization factor between 0 and 1 to randomize the Repeat.
-- @param #number Stop Time interval in seconds after which the scheduler will be stopped.
-- @param #number TraceLevel Trace level [0,3]. Default 3.
-- @param Core.Fsm#FSM Fsm Finite state model.
-- @return #string The Schedule ID of the planned schedule.
function SCHEDULER:Schedule( MasterObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop, TraceLevel, Fsm )
  self:F2( { Start, Repeat, RandomizeFactor, Stop } )
  self:T3( { SchedulerArguments } )

  -- Debug info.
  local ObjectName = "-"
  if MasterObject and MasterObject.ClassName and MasterObject.ClassID then
    ObjectName = MasterObject.ClassName .. MasterObject.ClassID
  end
  self:F3( { "Schedule :", ObjectName, tostring( MasterObject ), Start, Repeat, RandomizeFactor, Stop } )

  -- Set master object.
  self.MasterObject = MasterObject

  -- Add schedule.
  local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( self,
                                                      SchedulerFunction,
                                                      SchedulerArguments,
                                                      Start,
                                                      Repeat,
                                                      RandomizeFactor,
                                                      Stop,
                                                      TraceLevel or 3,
                                                      Fsm
                                                    )

  self.Schedules[#self.Schedules + 1] = ScheduleID

  return ScheduleID
end

--- (Re-)Starts the schedules or a specific schedule if a valid ScheduleID is provided.
-- @param #SCHEDULER self
-- @param #string ScheduleID (Optional) The Schedule ID of the planned (repeating) schedule.
function SCHEDULER:Start( ScheduleID )
  self:F3( { ScheduleID } )
  self:T( string.format( "Starting scheduler ID=%s", tostring( ScheduleID ) ) )
  _SCHEDULEDISPATCHER:Start( self, ScheduleID )
end

--- Stops the schedules or a specific schedule if a valid ScheduleID is provided.
-- @param #SCHEDULER self
-- @param #string ScheduleID (Optional) The ScheduleID of the planned (repeating) schedule.
function SCHEDULER:Stop( ScheduleID )
  self:F3( { ScheduleID } )
  self:T( string.format( "Stopping scheduler ID=%s", tostring( ScheduleID ) ) )
  _SCHEDULEDISPATCHER:Stop( self, ScheduleID )
end

--- Removes a specific schedule if a valid ScheduleID is provided.
-- @param #SCHEDULER self
-- @param #string ScheduleID (optional) The ScheduleID of the planned (repeating) schedule.
function SCHEDULER:Remove( ScheduleID )
  self:F3( { ScheduleID } )
  self:T( string.format( "Removing scheduler ID=%s", tostring( ScheduleID ) ) )
  _SCHEDULEDISPATCHER:RemoveSchedule( self, ScheduleID )
end

--- Clears all pending schedules.
-- @param #SCHEDULER self
function SCHEDULER:Clear()
  self:F3()
  self:T( string.format( "Clearing scheduler" ) )
  _SCHEDULEDISPATCHER:Clear( self )
end

--- Show tracing for this scheduler.
-- @param #SCHEDULER self
function SCHEDULER:ShowTrace()
  _SCHEDULEDISPATCHER:ShowTrace( self )
end

--- No tracing for this scheduler.
-- @param #SCHEDULER self
function SCHEDULER:NoTrace()
  _SCHEDULEDISPATCHER:NoTrace( self )
end
---- **Core** - SCHEDULEDISPATCHER dispatches the different schedules.
--
-- ===
--
-- Takes care of the creation and dispatching of scheduled functions for SCHEDULER objects.
--
-- This class is tricky and needs some thorough explanation.
-- SCHEDULE classes are used to schedule functions for objects, or as persistent objects.
-- The SCHEDULEDISPATCHER class ensures that:
--
--   - Scheduled functions are planned according the SCHEDULER object parameters.
--   - Scheduled functions are repeated when requested, according the SCHEDULER object parameters.
--   - Scheduled functions are automatically removed when the schedule is finished, according the SCHEDULER object parameters.
--
-- The SCHEDULEDISPATCHER class will manage SCHEDULER object in memory during garbage collection:
--
--   - When a SCHEDULER object is not attached to another object (that is, it's first :Schedule() parameter is nil), then the SCHEDULER object is _persistent_ within memory.
--   - When a SCHEDULER object *is* attached to another object, then the SCHEDULER object is _not persistent_ within memory after a garbage collection!
--
-- The non-persistency of SCHEDULERS attached to objects is required to allow SCHEDULER objects to be garbage collected when the parent object is destroyed, or set to nil and garbage collected.
-- Even when there are pending timer scheduled functions to be executed for the SCHEDULER object,
-- these will not be executed anymore when the SCHEDULER object has been destroyed.
--
-- The SCHEDULEDISPATCHER allows multiple scheduled functions to be planned and executed for one SCHEDULER object.
-- The SCHEDULER object therefore keeps a table of "CallID's", which are returned after each planning of a new scheduled function by the SCHEDULEDISPATCHER.
-- The SCHEDULER object plans new scheduled functions through the @{Core.Scheduler#SCHEDULER.Schedule}() method.
-- The Schedule() method returns the CallID that is the reference ID for each planned schedule.
--
-- ===
--
-- ### Contributions: -
-- ### Authors: FlightControl : Design & Programming
--
-- @module Core.ScheduleDispatcher
-- @image Core_Schedule_Dispatcher.JPG

--- SCHEDULEDISPATCHER class.
-- @type SCHEDULEDISPATCHER
-- @field #string ClassName Name of the class.
-- @field #number CallID Call ID counter.
-- @field #table PersistentSchedulers Persistent schedulers.
-- @field #table ObjectSchedulers Schedulers that only exist as long as the master object exists.
-- @field #table Schedule Meta table setmetatable( {}, { __mode = "k" } ).
-- @extends Core.Base#BASE

--- The SCHEDULEDISPATCHER structure
-- @type SCHEDULEDISPATCHER
SCHEDULEDISPATCHER = {
  ClassName = "SCHEDULEDISPATCHER",
  CallID = 0,
  PersistentSchedulers = {},
  ObjectSchedulers = {},
  Schedule = nil,
}

--- Player data table holding all important parameters of each player.
-- @type SCHEDULEDISPATCHER.ScheduleData
-- @field #function Function The schedule function to be called.
-- @field #table Arguments Schedule function arguments.
-- @field #number Start Start time in seconds.
-- @field #number Repeat Repeat time interval in seconds.
-- @field #number Randomize Randomization factor [0,1].
-- @field #number Stop Stop time in seconds.
-- @field #number StartTime Time in seconds when the scheduler is created.
-- @field #number ScheduleID Schedule ID.
-- @field #function CallHandler Function to be passed to the DCS timer.scheduleFunction().
-- @field #boolean ShowTrace If true, show tracing info.

--- Create a new schedule dispatcher object.
-- @param #SCHEDULEDISPATCHER self
-- @return #SCHEDULEDISPATCHER self
function SCHEDULEDISPATCHER:New()
  local self = BASE:Inherit( self, BASE:New() )
  self:F3()
  return self
end

--- Add a Schedule to the ScheduleDispatcher.
-- The development of this method was really tidy.
-- It is constructed as such that a garbage collection is executed on the weak tables, when the Scheduler is set to nil.
-- Nothing of this code should be modified without testing it thoroughly.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
-- @param #function ScheduleFunction Scheduler function.
-- @param #table ScheduleArguments Table of arguments passed to the ScheduleFunction.
-- @param #number Start Start time in seconds.
-- @param #number Repeat Repeat interval in seconds.
-- @param #number Randomize Randomization factor [0,1].
-- @param #number Stop Stop time in seconds.
-- @param #number TraceLevel Trace level [0,3].
-- @param Core.Fsm#FSM Fsm Finite state model.
-- @return #string Call ID or nil.
function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel, Fsm )
  self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop, TraceLevel, Fsm } )

  -- Increase counter.
  self.CallID = self.CallID + 1

  -- Create ID.
  local CallID = self.CallID .. "#" .. (Scheduler.MasterObject and Scheduler.MasterObject.GetClassNameAndID and Scheduler.MasterObject:GetClassNameAndID() or "") or ""

  self:T2( string.format( "Adding schedule #%d CallID=%s", self.CallID, CallID ) )

  -- Initialize PersistentSchedulers
  self.PersistentSchedulers = self.PersistentSchedulers or {}

  -- Initialize the ObjectSchedulers array, which is a weakly coupled table.
  -- If the object used as the key is nil, then the garbage collector will remove the item from the Functions array.
  self.ObjectSchedulers = self.ObjectSchedulers or setmetatable( {}, { __mode = "v" } )

  if Scheduler.MasterObject then
    --env.info("FF Object Scheduler")
    self.ObjectSchedulers[CallID] = Scheduler
    self:F3( { CallID = CallID, ObjectScheduler = tostring( self.ObjectSchedulers[CallID] ), MasterObject = tostring( Scheduler.MasterObject ) } )
  else
    --env.info("FF Persistent Scheduler")
    self.PersistentSchedulers[CallID] = Scheduler
    self:F3( { CallID = CallID, PersistentScheduler = self.PersistentSchedulers[CallID] } )
  end

  self.Schedule = self.Schedule or setmetatable( {}, { __mode = "k" } )
  self.Schedule[Scheduler] = self.Schedule[Scheduler] or {}
  self.Schedule[Scheduler][CallID] = {} -- #SCHEDULEDISPATCHER.ScheduleData
  self.Schedule[Scheduler][CallID].Function = ScheduleFunction
  self.Schedule[Scheduler][CallID].Arguments = ScheduleArguments
  self.Schedule[Scheduler][CallID].StartTime = timer.getTime() + ( Start or 0 )
  self.Schedule[Scheduler][CallID].Start = Start + 0.001
  self.Schedule[Scheduler][CallID].Repeat = Repeat or 0
  self.Schedule[Scheduler][CallID].Randomize = Randomize or 0
  self.Schedule[Scheduler][CallID].Stop = Stop

  -- This section handles the tracing of the scheduled calls.
  -- Because these calls will be executed with a delay, we inspect the place where these scheduled calls are initiated.
  -- The Info structure contains the output of the debug.getinfo() calls, which inspects the call stack for the function name, line number and source name.
  -- The call stack has many levels, and the correct semantical function call depends on where in the code AddSchedule was "used".
  --   - Using SCHEDULER:New()
  --   - Using Schedule:AddSchedule()
  --   - Using Fsm:__Func()
  --   - Using Class:ScheduleOnce()
  --   - Using Class:ScheduleRepeat()
  --   - ...
  -- So for each of these scheduled call variations, AddSchedule is the workhorse which will schedule the call.
  -- But the correct level with the correct semantical function location will differ depending on the above scheduled call invocation forms.
  -- That's where the field TraceLevel contains optionally the level in the call stack where the call information is obtained.
  -- The TraceLevel field indicates the correct level where the semantical scheduled call was invoked within the source, ensuring that function name, line number and source name are correct.
  -- There is one quick ...
  -- The FSM class models scheduled calls using the __Func syntax. However, these functions are "tailed".
  -- There aren't defined anywhere within the source code, but rather implemented as triggers within the FSM logic, 
  -- and using the onbefore, onafter, onenter, onleave prefixes. (See the FSM for details).
  -- Therefore, in the call stack, at the TraceLevel these functions are mentioned as "tail calls", and the Info.name field will be nil as a result.
  -- To obtain the correct function name for FSM object calls, the function is mentioned in the call stack at a higher stack level.
  -- So when function name stored in Info.name is nil, then I inspect the function name within the call stack one level higher.
  -- So this little piece of code does its magic wonderfully, performance overhead is negligible, as scheduled calls don't happen that often.

  local Info = {}

  if debug then
    TraceLevel = TraceLevel or 2
    Info = debug.getinfo( TraceLevel, "nlS" )
    local name_fsm = debug.getinfo( TraceLevel - 1, "n" ).name -- #string
    if name_fsm then
      Info.name = name_fsm
    end
  end

  self:T3( self.Schedule[Scheduler][CallID] )

  --- Function passed to the DCS timer.scheduleFunction()
  self.Schedule[Scheduler][CallID].CallHandler = function( Params )

    local CallID = Params.CallID
    local Info = Params.Info or {}
    local Source = Info.source or "?"
    local Line = Info.currentline or "?"
    local Name = Info.name or "?"

    local ErrorHandler = function( errmsg )
      env.info( "Error in timer function: " .. errmsg or "" )
      if BASE.Debug ~= nil then
        env.info( BASE.Debug.traceback() )
      end
      return errmsg
    end

    -- Get object or persistent scheduler object.
    local Scheduler = self.ObjectSchedulers[CallID] -- Core.Scheduler#SCHEDULER
    if not Scheduler then
      Scheduler = self.PersistentSchedulers[CallID]
    end

    -- self:T3( { Scheduler = Scheduler } )

    if Scheduler then

      local MasterObject = tostring( Scheduler.MasterObject )

      -- Schedule object.
      local Schedule = self.Schedule[Scheduler][CallID] -- #SCHEDULEDISPATCHER.ScheduleData

      -- self:T3( { Schedule = Schedule } )

      local SchedulerObject = Scheduler.MasterObject -- Scheduler.SchedulerObject Now is this the Master or Scheduler object?
      local ShowTrace       = Scheduler.ShowTrace

      local ScheduleFunction  = Schedule.Function
      local ScheduleArguments = Schedule.Arguments or {}
      local Start             = Schedule.Start
      local Repeat            = Schedule.Repeat or 0
      local Randomize         = Schedule.Randomize or 0
      local Stop              = Schedule.Stop or 0
      local ScheduleID        = Schedule.ScheduleID

      local Prefix = (Repeat == 0) and "--->" or "+++>"

      local Status, Result
      -- self:E( { SchedulerObject = SchedulerObject } )
      if SchedulerObject then
        local function Timer()
          if ShowTrace then
            SchedulerObject:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" )
          end
          -- The master object is passed as first parameter. A few :Schedule() calls in MOOSE expect this currently. But in principle it should be removed.
          return ScheduleFunction( SchedulerObject, unpack( ScheduleArguments ) ) 
        end
        Status, Result = xpcall( Timer, ErrorHandler )
      else
        local function Timer()
          if ShowTrace then
            self:T( Prefix .. Name .. ":" .. Line .. " (" .. Source .. ")" )
          end
          return ScheduleFunction( unpack( ScheduleArguments ) )
        end
        Status, Result = xpcall( Timer, ErrorHandler )
      end

      local CurrentTime = timer.getTime()
      local StartTime = Schedule.StartTime

      -- Debug info.
      self:F3( { CallID = CallID, ScheduleID = ScheduleID, Master = MasterObject, CurrentTime = CurrentTime, StartTime = StartTime, Start = Start, Repeat = Repeat, Randomize = Randomize, Stop = Stop } )

      if Status and ((Result == nil) or (Result and Result ~= false)) then

        if Repeat ~= 0 and ((Stop == 0) or (Stop ~= 0 and CurrentTime <= StartTime + Stop)) then
          local ScheduleTime = CurrentTime + Repeat + math.random( -(Randomize * Repeat / 2), (Randomize * Repeat / 2) ) + 0.0001 -- Accuracy
          -- self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } )
          return ScheduleTime -- returns the next time the function needs to be called.
        else
          self:Stop( Scheduler, CallID )
        end

      else
        self:Stop( Scheduler, CallID )
      end
    else
      self:I( "<<<>" .. Name .. ":" .. Line .. " (" .. Source .. ")" )
    end

    return nil
  end

  self:Start( Scheduler, CallID, Info )

  return CallID
end

--- Remove schedule.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
-- @param #table CallID Call ID.
function SCHEDULEDISPATCHER:RemoveSchedule( Scheduler, CallID )
  self:F2( { Remove = CallID, Scheduler = Scheduler } )

  if CallID then
    self:Stop( Scheduler, CallID )
    self.Schedule[Scheduler][CallID] = nil
  end
end

--- Start dispatcher.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
-- @param #table CallID (Optional) Call ID.
-- @param #string Info (Optional) Debug info.
function SCHEDULEDISPATCHER:Start( Scheduler, CallID, Info )
  self:F2( { Start = CallID, Scheduler = Scheduler } )

  if CallID then

    local Schedule = self.Schedule[Scheduler][CallID] -- #SCHEDULEDISPATCHER.ScheduleData

    -- Only start when there is no ScheduleID defined!
    -- This prevents to "Start" the scheduler twice with the same CallID...
    if not Schedule.ScheduleID then

      -- Current time in seconds.
      local Tnow = timer.getTime()

      Schedule.StartTime = Tnow -- Set the StartTime field to indicate when the scheduler started.

      -- Start DCS schedule function https://wiki.hoggitworld.com/view/DCS_func_scheduleFunction
      Schedule.ScheduleID = timer.scheduleFunction( Schedule.CallHandler, { CallID = CallID, Info = Info }, Tnow + Schedule.Start )

      self:T( string.format( "Starting SCHEDULEDISPATCHER Call ID=%s ==> Schedule ID=%s", tostring( CallID ), tostring( Schedule.ScheduleID ) ) )
    end

  else

    -- Recursive.
    for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do
      self:Start( Scheduler, CallID, Info ) -- Recursive
    end

  end
end

--- Stop dispatcher.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
-- @param #string CallID (Optional) Scheduler Call ID. If nil, all pending schedules are stopped recursively.
function SCHEDULEDISPATCHER:Stop( Scheduler, CallID )
  self:F2( { Stop = CallID, Scheduler = Scheduler } )

  if CallID then

    local Schedule = self.Schedule[Scheduler][CallID] -- #SCHEDULEDISPATCHER.ScheduleData

    -- Only stop when there is a ScheduleID defined for the CallID. So, when the scheduler was stopped before, do nothing.
    if Schedule and Schedule.ScheduleID then

      self:T( string.format( "SCHEDULEDISPATCHER stopping scheduler CallID=%s, ScheduleID=%s", tostring( CallID ), tostring( Schedule.ScheduleID ) ) )

      -- Remove schedule function https://wiki.hoggitworld.com/view/DCS_func_removeFunction
      timer.removeFunction( Schedule.ScheduleID )

      Schedule.ScheduleID = nil

    else
      self:T( string.format( "Error no ScheduleID for CallID=%s", tostring( CallID ) ) )
    end

  else

    for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do
      self:Stop( Scheduler, CallID ) -- Recursive
    end

  end
end

--- Clear all schedules by stopping all dispatchers.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
function SCHEDULEDISPATCHER:Clear( Scheduler )
  self:F2( { Scheduler = Scheduler } )

  for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do
    self:Stop( Scheduler, CallID ) -- Recursive
  end
end

--- Show tracing info.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
function SCHEDULEDISPATCHER:ShowTrace( Scheduler )
  self:F2( { Scheduler = Scheduler } )
  Scheduler.ShowTrace = true
end

--- No tracing info.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object.
function SCHEDULEDISPATCHER:NoTrace( Scheduler )
  self:F2( { Scheduler = Scheduler } )
  Scheduler.ShowTrace = false
end

--- **Core** - Models DCS event dispatching using a publish-subscribe model.
--
-- ===
--
-- ## Features:
--
--   * Capture DCS events and dispatch them to the subscribed objects.
--   * Generate DCS events to the subscribed objects from within the code.
--
-- ===
--
-- # Event Handling Overview
--
-- ![Objects](..\Presentations\EVENT\Dia2.JPG)
--
-- Within a running mission, various DCS events occur. Units are dynamically created, crash, die, shoot stuff, get hit etc.
-- This module provides a mechanism to dispatch those events occurring within your running mission, to the different objects orchestrating your mission.
--
-- ![Objects](..\Presentations\EVENT\Dia3.JPG)
--
-- Objects can subscribe to different events. The Event dispatcher will publish the received DCS events to the subscribed MOOSE objects, in a specified order.
-- In this way, the subscribed MOOSE objects are kept in sync with your evolving running mission.
--
-- ## 1. Event Dispatching
--
-- ![Objects](..\Presentations\EVENT\Dia4.JPG)
--
-- The _EVENTDISPATCHER object is automatically created within MOOSE,
-- and handles the dispatching of DCS Events occurring
-- in the simulator to the subscribed objects
-- in the correct processing order.
--
-- ![Objects](..\Presentations\EVENT\Dia5.JPG)
--
-- There are 5 types/levels of objects that the _EVENTDISPATCHER services:
--
--  * _DATABASE object: The core of the MOOSE objects. Any object that is created, deleted or updated, is done in this database.
--  * SET_ derived classes: These are subsets of the global _DATABASE object (an instance of @{Core.Database#DATABASE}). These subsets are updated by the _EVENTDISPATCHER as the second priority.
--  * UNIT objects: UNIT objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed UNIT object.
--  * GROUP objects: GROUP objects can subscribe to DCS events. Each DCS event will be directly published to the subscribed GROUP object.
--  * Any other object: Various other objects can subscribe to DCS events. Each DCS event triggered will be published to each subscribed object.
--
-- ![Objects](..\Presentations\EVENT\Dia6.JPG)
--
-- For most DCS events, the above order of updating will be followed.
--
-- ![Objects](..\Presentations\EVENT\Dia7.JPG)
--
-- But for some DCS events, the publishing order is reversed. This is due to the fact that objects need to be **erased** instead of added.
--
-- # 2. Event Handling
--
-- ![Objects](..\Presentations\EVENT\Dia8.JPG)
--
-- The actual event subscribing and handling is not facilitated through the _EVENTDISPATCHER, but it is done through the @{Core.Base#BASE} class, @{Wrapper.Unit#UNIT} class and @{Wrapper.Group#GROUP} class.
-- The _EVENTDISPATCHER is a component that is quietly working in the background of MOOSE.
--
-- ![Objects](..\Presentations\EVENT\Dia9.JPG)
--
-- The BASE class provides methods to catch DCS Events. These are events that are triggered from within the DCS simulator,
-- and handled through lua scripting. MOOSE provides an encapsulation to handle these events more efficiently.
--
-- ## 2.1. Subscribe to / Unsubscribe from DCS Events.
--
-- At first, the mission designer will need to **Subscribe** to a specific DCS event for the class.
-- So, when the DCS event occurs, the class will be notified of that event.
-- There are two functions which you use to subscribe to or unsubscribe from an event.
--
--   * @{Core.Base#BASE.HandleEvent}(): Subscribe to a DCS Event.
--   * @{Core.Base#BASE.UnHandleEvent}(): Unsubscribe from a DCS Event.
--
-- Note that for a UNIT, the event will be handled **for that UNIT only**!
-- Note that for a GROUP, the event will be handled **for all the UNITs in that GROUP only**!
--
-- For all objects of other classes, the subscribed events will be handled for **all UNITs within the Mission**!
-- So if a UNIT within the mission has the subscribed event for that object,
-- then the object event handler will receive the event for that UNIT!
--
-- ## 2.2 Event Handling of DCS Events
--
-- Once the class is subscribed to the event, an **Event Handling** method on the object or class needs to be written that will be called
-- when the DCS event occurs. The Event Handling method receives an @{Core.Event#EVENTDATA} structure, which contains a lot of information
-- about the event that occurred.
--
-- Find below an example of the prototype how to write an event handling function for two units:
--
--      local Tank1 = UNIT:FindByName( "Tank A" )
--      local Tank2 = UNIT:FindByName( "Tank B" )
--
--      -- Here we subscribe to the Dead events. So, if one of these tanks dies, the Tank1 or Tank2 objects will be notified.
--      Tank1:HandleEvent( EVENTS.Dead )
--      Tank2:HandleEvent( EVENTS.Dead )
--
--      --- This function is an Event Handling function that will be called when Tank1 is Dead.
--      -- @param Wrapper.Unit#UNIT self
--      -- @param Core.Event#EVENTDATA EventData
--      function Tank1:OnEventDead( EventData )
--
--        self:SmokeGreen()
--      end
--
--      --- This function is an Event Handling function that will be called when Tank2 is Dead.
--      -- @param Wrapper.Unit#UNIT self
--      -- @param Core.Event#EVENTDATA EventData
--      function Tank2:OnEventDead( EventData )
--
--        self:SmokeBlue()
--      end
--
-- ## 2.3 Event Handling methods that are automatically called upon subscribed DCS events.
--
-- ![Objects](..\Presentations\EVENT\Dia10.JPG)
--
-- The following list outlines which EVENTS item in the structure corresponds to which Event Handling method.
-- Always ensure that your event handling methods align with the events being subscribed to, or nothing will be executed.
--
-- # 3. EVENTS type
--
-- The EVENTS structure contains names for all the different DCS events that objects can subscribe to using the
-- @{Core.Base#BASE.HandleEvent}() method.
--
-- # 4. EVENTDATA type
--
-- The @{Core.Event#EVENTDATA} structure contains all the fields that are populated with event information before
-- an Event Handler method is being called by the event dispatcher.
-- The Event Handler received the EVENTDATA object as a parameter, and can be used to investigate further the different events.
-- There are basically 4 main categories of information stored in the EVENTDATA structure:
--
--    * Initiator Unit data: Several fields documenting the initiator unit related to the event.
--    * Target Unit data: Several fields documenting the target unit related to the event.
--    * Weapon data: Certain events populate weapon information.
--    * Place data: Certain events populate place information.
--
--      --- This function is an Event Handling function that will be called when Tank1 is Dead.
--      -- EventData is an EVENTDATA structure.
--      -- We use the EventData.IniUnit to smoke the tank Green.
--      -- @param Wrapper.Unit#UNIT self
--      -- @param Core.Event#EVENTDATA EventData
--      function Tank1:OnEventDead( EventData )
--
--        EventData.IniUnit:SmokeGreen()
--      end
--
--
-- Find below an overview which events populate which information categories:
--
-- ![Objects](..\Presentations\EVENT\Dia14.JPG)
--
-- **IMPORTANT NOTE:** Some events can involve not just UNIT objects, but also STATIC objects!!!
-- In that case the initiator or target unit fields will refer to a STATIC object!
-- In case a STATIC object is involved, the documentation indicates which fields will and won't not be populated.
-- The fields **IniObjectCategory** and **TgtObjectCategory** contain the indicator which **kind of object is involved** in the event.
-- You can use the enumerator **Object.Category.UNIT** and **Object.Category.STATIC** to check on IniObjectCategory and TgtObjectCategory.
-- Example code snippet:
--
--      if Event.IniObjectCategory == Object.Category.UNIT then
--       ...
--      end
--      if Event.IniObjectCategory == Object.Category.STATIC then
--       ...
--      end
--
-- When a static object is involved in the event, the Group and Player fields won't be populated.
--
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions:
--
-- ===
--
-- @module Core.Event
-- @image Core_Event.JPG


---
-- @type EVENT
-- @field #EVENT.Events Events
-- @extends Core.Base#BASE

--- The EVENT class
-- @field #EVENT
EVENT = {
  ClassName = "EVENT",
  ClassID = 0,
  MissionEnd = false,
}

world.event.S_EVENT_NEW_CARGO = world.event.S_EVENT_MAX + 1000
world.event.S_EVENT_DELETE_CARGO = world.event.S_EVENT_MAX + 1001
world.event.S_EVENT_NEW_ZONE = world.event.S_EVENT_MAX + 1002
world.event.S_EVENT_DELETE_ZONE = world.event.S_EVENT_MAX + 1003
world.event.S_EVENT_NEW_ZONE_GOAL = world.event.S_EVENT_MAX + 1004
world.event.S_EVENT_DELETE_ZONE_GOAL = world.event.S_EVENT_MAX + 1005
world.event.S_EVENT_REMOVE_UNIT = world.event.S_EVENT_MAX + 1006
world.event.S_EVENT_PLAYER_ENTER_AIRCRAFT = world.event.S_EVENT_MAX + 1007
-- dynamic cargo
world.event.S_EVENT_NEW_DYNAMIC_CARGO = world.event.S_EVENT_MAX + 1008
world.event.S_EVENT_DYNAMIC_CARGO_LOADED = world.event.S_EVENT_MAX + 1009
world.event.S_EVENT_DYNAMIC_CARGO_UNLOADED = world.event.S_EVENT_MAX + 1010
world.event.S_EVENT_DYNAMIC_CARGO_REMOVED = world.event.S_EVENT_MAX + 1011


--- The different types of events supported by MOOSE.
-- Use this structure to subscribe to events using the @{Core.Base#BASE.HandleEvent}() method.
-- @type EVENTS
EVENTS = {
  Shot =              world.event.S_EVENT_SHOT,
  Hit =               world.event.S_EVENT_HIT,
  Takeoff =           world.event.S_EVENT_TAKEOFF,
  Land =              world.event.S_EVENT_LAND,
  Crash =             world.event.S_EVENT_CRASH,
  Ejection =          world.event.S_EVENT_EJECTION,
  Refueling =         world.event.S_EVENT_REFUELING,
  Dead =              world.event.S_EVENT_DEAD,
  PilotDead =         world.event.S_EVENT_PILOT_DEAD,
  BaseCaptured =      world.event.S_EVENT_BASE_CAPTURED,
  MissionStart =      world.event.S_EVENT_MISSION_START,
  MissionEnd =        world.event.S_EVENT_MISSION_END,
  TookControl =       world.event.S_EVENT_TOOK_CONTROL,
  RefuelingStop =     world.event.S_EVENT_REFUELING_STOP,
  Birth =             world.event.S_EVENT_BIRTH,
  HumanFailure =      world.event.S_EVENT_HUMAN_FAILURE,
  EngineStartup =     world.event.S_EVENT_ENGINE_STARTUP,
  EngineShutdown =    world.event.S_EVENT_ENGINE_SHUTDOWN,
  PlayerEnterUnit =   world.event.S_EVENT_PLAYER_ENTER_UNIT,
  PlayerLeaveUnit =   world.event.S_EVENT_PLAYER_LEAVE_UNIT,
  PlayerComment =     world.event.S_EVENT_PLAYER_COMMENT,
  ShootingStart =     world.event.S_EVENT_SHOOTING_START,
  ShootingEnd =       world.event.S_EVENT_SHOOTING_END,
  -- Added with DCS 2.5.1
  MarkAdded =         world.event.S_EVENT_MARK_ADDED,
  MarkChange =        world.event.S_EVENT_MARK_CHANGE,
  MarkRemoved =       world.event.S_EVENT_MARK_REMOVED,
  -- Moose Events
  NewCargo =          world.event.S_EVENT_NEW_CARGO,
  DeleteCargo =       world.event.S_EVENT_DELETE_CARGO,
  NewZone =           world.event.S_EVENT_NEW_ZONE,
  DeleteZone =        world.event.S_EVENT_DELETE_ZONE,
  NewZoneGoal =       world.event.S_EVENT_NEW_ZONE_GOAL,
  DeleteZoneGoal =    world.event.S_EVENT_DELETE_ZONE_GOAL,
  RemoveUnit =        world.event.S_EVENT_REMOVE_UNIT,
  PlayerEnterAircraft = world.event.S_EVENT_PLAYER_ENTER_AIRCRAFT,
  -- Added with DCS 2.5.6
  DetailedFailure           = world.event.S_EVENT_DETAILED_FAILURE or -1,  --We set this to -1 for backward compatibility to DCS 2.5.5 and earlier
  Kill                      = world.event.S_EVENT_KILL or -1,
  Score                     = world.event.S_EVENT_SCORE or -1,
  UnitLost                  = world.event.S_EVENT_UNIT_LOST or -1,
  LandingAfterEjection      = world.event.S_EVENT_LANDING_AFTER_EJECTION or -1,
  -- Added with DCS 2.7.0
  ParatrooperLanding        = world.event.S_EVENT_PARATROOPER_LENDING or -1,
  DiscardChairAfterEjection = world.event.S_EVENT_DISCARD_CHAIR_AFTER_EJECTION or -1,
  WeaponAdd                 = world.event.S_EVENT_WEAPON_ADD or -1,
  TriggerZone               = world.event.S_EVENT_TRIGGER_ZONE or -1,
  LandingQualityMark        = world.event.S_EVENT_LANDING_QUALITY_MARK or -1,
  BDA                       = world.event.S_EVENT_BDA or -1,
  -- Added with DCS 2.8.0
  AIAbortMission            = world.event.S_EVENT_AI_ABORT_MISSION or -1,
  DayNight                  = world.event.S_EVENT_DAYNIGHT or -1,
  FlightTime                = world.event.S_EVENT_FLIGHT_TIME or -1,
  SelfKillPilot             = world.event.S_EVENT_PLAYER_SELF_KILL_PILOT or -1,
  PlayerCaptureAirfield     = world.event.S_EVENT_PLAYER_CAPTURE_AIRFIELD or -1, 
  EmergencyLanding          = world.event.S_EVENT_EMERGENCY_LANDING or -1,
  UnitCreateTask            = world.event.S_EVENT_UNIT_CREATE_TASK or -1,
  UnitDeleteTask            = world.event.S_EVENT_UNIT_DELETE_TASK or -1,
  SimulationStart           = world.event.S_EVENT_SIMULATION_START or -1,
  WeaponRearm               = world.event.S_EVENT_WEAPON_REARM or -1,
  WeaponDrop                = world.event.S_EVENT_WEAPON_DROP or -1,
  -- Added with DCS 2.9.x
  --UnitTaskTimeout           = world.event.S_EVENT_UNIT_TASK_TIMEOUT or -1,
  UnitTaskComplete          = world.event.S_EVENT_UNIT_TASK_COMPLETE or -1,
  UnitTaskStage             = world.event.S_EVENT_UNIT_TASK_STAGE or -1,
  --MacSubtaskScore           = world.event.S_EVENT_MAC_SUBTASK_SCORE or -1, 
  MacExtraScore             = world.event.S_EVENT_MAC_EXTRA_SCORE or -1,
  MissionRestart            = world.event.S_EVENT_MISSION_RESTART or -1,
  MissionWinner             = world.event.S_EVENT_MISSION_WINNER or -1, 
  RunwayTakeoff             = world.event.S_EVENT_RUNWAY_TAKEOFF or -1, 
  RunwayTouch               = world.event.S_EVENT_RUNWAY_TOUCH or -1,
  MacLMSRestart             = world.event.S_EVENT_MAC_LMS_RESTART or -1,
  SimulationFreeze          = world.event.S_EVENT_SIMULATION_FREEZE or -1, 
  SimulationUnfreeze        = world.event.S_EVENT_SIMULATION_UNFREEZE or -1, 
  HumanAircraftRepairStart  = world.event.S_EVENT_HUMAN_AIRCRAFT_REPAIR_START or -1, 
  HumanAircraftRepairFinish = world.event.S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH or -1,
  -- dynamic cargo
  NewDynamicCargo           = world.event.S_EVENT_NEW_DYNAMIC_CARGO or -1,
  DynamicCargoLoaded        = world.event.S_EVENT_DYNAMIC_CARGO_LOADED or -1,
  DynamicCargoUnloaded      = world.event.S_EVENT_DYNAMIC_CARGO_UNLOADED or -1,
  DynamicCargoRemoved       = world.event.S_EVENT_DYNAMIC_CARGO_REMOVED or -1,
}


--- The Event structure
-- Note that at the beginning of each field description, there is an indication which field will be populated depending on the object type involved in the Event:
--
--   * A (Object.Category.)UNIT : A UNIT object type is involved in the Event.
--   * A (Object.Category.)STATIC : A STATIC object type is involved in the Event.
--
-- @type EVENTDATA
-- @field #number id The identifier of the event.
--
-- @field DCS#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{DCS#Unit} or @{DCS#StaticObject}.
-- @field DCS#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ).
-- @field DCS#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCS#Unit} or @{DCS#StaticObject}.
-- @field #string IniDCSUnitName (UNIT/STATIC) The initiating Unit name.
-- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Wrapper.Unit#UNIT} of the initiator Unit object.
-- @field #string IniUnitName (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName).
-- @field DCS#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}.
-- @field #string IniDCSGroupName (UNIT) The initiating Group name.
-- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Wrapper.Group#GROUP} of the initiator Group object.
-- @field #string IniGroupName UNIT) The initiating GROUP name (same as IniDCSGroupName).
-- @field #string IniPlayerName (UNIT) The name of the initiating player in case the Unit is a client or player slot.
-- @field #string IniPlayerUCID (UNIT) The UCID of the initiating player in case the Unit is a client or player slot and on a multi-player server.
-- @field DCS#coalition.side IniCoalition (UNIT) The coalition of the initiator.
-- @field DCS#Unit.Category IniCategory (UNIT) The category of the initiator.
-- @field #string IniTypeName (UNIT) The type name of the initiator.
--
-- @field DCS#Unit target (UNIT/STATIC) The target @{DCS#Unit} or @{DCS#StaticObject}.
-- @field DCS#Object.Category TgtObjectCategory (UNIT/STATIC) The target object category ( Object.Category.UNIT or Object.Category.STATIC ).
-- @field DCS#Unit TgtDCSUnit (UNIT/STATIC) The target @{DCS#Unit} or @{DCS#StaticObject}.
-- @field #string TgtDCSUnitName (UNIT/STATIC) The target Unit name.
-- @field Wrapper.Unit#UNIT TgtUnit (UNIT/STATIC) The target MOOSE wrapper @{Wrapper.Unit#UNIT} of the target Unit object.
-- @field #string TgtUnitName (UNIT/STATIC) The target UNIT name (same as TgtDCSUnitName).
-- @field DCS#Group TgtDCSGroup (UNIT) The target {DCSGroup#Group}.
-- @field #string TgtDCSGroupName (UNIT) The target Group name.
-- @field Wrapper.Group#GROUP TgtGroup (UNIT) The target MOOSE wrapper @{Wrapper.Group#GROUP} of the target Group object.
-- @field #string TgtGroupName (UNIT) The target GROUP name (same as TgtDCSGroupName).
-- @field #string TgtPlayerName (UNIT) The name of the target player in case the Unit is a client or player slot.
-- @field #string TgtPlayerUCID (UNIT) The UCID of the target player in case the Unit is a client or player slot and on a multi-player server.
-- @field DCS#coalition.side TgtCoalition (UNIT) The coalition of the target.
-- @field DCS#Unit.Category TgtCategory (UNIT) The category of the target.
-- @field #string TgtTypeName (UNIT) The type name of the target.
--
-- @field DCS#Airbase place The @{DCS#Airbase}
-- @field Wrapper.Airbase#AIRBASE Place The MOOSE airbase object.
-- @field #string PlaceName The name of the airbase.
--
-- @field DCS#Weapon weapon The weapon used during the event.
-- @field DCS#Weapon Weapon The weapon used during the event.
-- @field #string WeaponName Name of the weapon.
-- @field DCS#Unit WeaponTgtDCSUnit Target DCS unit of the weapon.
--
-- @field Cargo.Cargo#CARGO Cargo The cargo object.
-- @field #string CargoName The name of the cargo object.
--
-- @field Core.Zone#ZONE Zone The zone object.
-- @field #string ZoneName The name of the zone.
-- 
-- @field Wrapper.DynamicCargo#DYNAMICCARGO IniDynamicCargo The dynamic cargo object.
-- @field #string IniDynamicCargoName The dynamic cargo unit name.



local _EVENTMETA = {
   [world.event.S_EVENT_SHOT] = {
     Order = 1,
     Side = "I",
     Event = "OnEventShot",
     Text = "S_EVENT_SHOT"
   },
   [world.event.S_EVENT_HIT] = {
     Order = 1,
     Side = "T",
     Event = "OnEventHit",
     Text = "S_EVENT_HIT"
   },
   [world.event.S_EVENT_TAKEOFF] = {
     Order = 1,
     Side = "I",
     Event = "OnEventTakeoff",
     Text = "S_EVENT_TAKEOFF"
   },
   [world.event.S_EVENT_LAND] = {
     Order = 1,
     Side = "I",
     Event = "OnEventLand",
     Text = "S_EVENT_LAND"
   },
   [world.event.S_EVENT_CRASH] = {
     Order = -1,
     Side = "I",
     Event = "OnEventCrash",
     Text = "S_EVENT_CRASH"
   },
   [world.event.S_EVENT_EJECTION] = {
     Order = 1,
     Side = "I",
     Event = "OnEventEjection",
     Text = "S_EVENT_EJECTION"
   },
   [world.event.S_EVENT_REFUELING] = {
     Order = 1,
     Side = "I",
     Event = "OnEventRefueling",
     Text = "S_EVENT_REFUELING"
   },
   [world.event.S_EVENT_DEAD] = {
     Order = -1,
     Side = "I",
     Event = "OnEventDead",
     Text = "S_EVENT_DEAD"
   },
   [world.event.S_EVENT_PILOT_DEAD] = {
     Order = 1,
     Side = "I",
     Event = "OnEventPilotDead",
     Text = "S_EVENT_PILOT_DEAD"
   },
   [world.event.S_EVENT_BASE_CAPTURED] = {
     Order = 1,
     Side = "I",
     Event = "OnEventBaseCaptured",
     Text = "S_EVENT_BASE_CAPTURED"
   },
   [world.event.S_EVENT_MISSION_START] = {
     Order = 1,
     Side = "N",
     Event = "OnEventMissionStart",
     Text = "S_EVENT_MISSION_START"
   },
   [world.event.S_EVENT_MISSION_END] = {
     Order = 1,
     Side = "N",
     Event = "OnEventMissionEnd",
     Text = "S_EVENT_MISSION_END"
   },
   [world.event.S_EVENT_TOOK_CONTROL] = {
     Order = 1,
     Side = "N",
     Event = "OnEventTookControl",
     Text = "S_EVENT_TOOK_CONTROL"
   },
   [world.event.S_EVENT_REFUELING_STOP] = {
     Order = 1,
     Side = "I",
     Event = "OnEventRefuelingStop",
     Text = "S_EVENT_REFUELING_STOP"
   },
   [world.event.S_EVENT_BIRTH] = {
     Order = 1,
     Side = "I",
     Event = "OnEventBirth",
     Text = "S_EVENT_BIRTH"
   },
   [world.event.S_EVENT_HUMAN_FAILURE] = {
     Order = 1,
     Side = "I",
     Event = "OnEventHumanFailure",
     Text = "S_EVENT_HUMAN_FAILURE"
   },
   [world.event.S_EVENT_ENGINE_STARTUP] = {
     Order = 1,
     Side = "I",
     Event = "OnEventEngineStartup",
     Text = "S_EVENT_ENGINE_STARTUP"
   },
   [world.event.S_EVENT_ENGINE_SHUTDOWN] = {
     Order = 1,
     Side = "I",
     Event = "OnEventEngineShutdown",
     Text = "S_EVENT_ENGINE_SHUTDOWN"
   },
   [world.event.S_EVENT_PLAYER_ENTER_UNIT] = {
     Order = 1,
     Side = "I",
     Event = "OnEventPlayerEnterUnit",
     Text = "S_EVENT_PLAYER_ENTER_UNIT"
   },
   [world.event.S_EVENT_PLAYER_LEAVE_UNIT] = {
     Order = -1,
     Side = "I",
     Event = "OnEventPlayerLeaveUnit",
     Text = "S_EVENT_PLAYER_LEAVE_UNIT"
   },
   [world.event.S_EVENT_PLAYER_COMMENT] = {
     Order = 1,
     Side = "I",
     Event = "OnEventPlayerComment",
     Text = "S_EVENT_PLAYER_COMMENT"
   },
   [world.event.S_EVENT_SHOOTING_START] = {
     Order = 1,
     Side = "I",
     Event = "OnEventShootingStart",
     Text = "S_EVENT_SHOOTING_START"
   },
   [world.event.S_EVENT_SHOOTING_END] = {
     Order = 1,
     Side = "I",
     Event = "OnEventShootingEnd",
     Text = "S_EVENT_SHOOTING_END"
   },
   [world.event.S_EVENT_MARK_ADDED] = {
     Order = 1,
     Side = "I",
     Event = "OnEventMarkAdded",
     Text = "S_EVENT_MARK_ADDED"
   },
   [world.event.S_EVENT_MARK_CHANGE] = {
     Order = 1,
     Side = "I",
     Event = "OnEventMarkChange",
     Text = "S_EVENT_MARK_CHANGE"
   },
   [world.event.S_EVENT_MARK_REMOVED] = {
     Order = 1,
     Side = "I",
     Event = "OnEventMarkRemoved",
     Text = "S_EVENT_MARK_REMOVED"
   },
   [EVENTS.NewCargo] = {
     Order = 1,
     Event = "OnEventNewCargo",
     Text = "S_EVENT_NEW_CARGO"
   },
   [EVENTS.DeleteCargo] = {
     Order = 1,
     Event = "OnEventDeleteCargo",
     Text = "S_EVENT_DELETE_CARGO"
   },
   [EVENTS.NewZone] = {
     Order = 1,
     Event = "OnEventNewZone",
     Text = "S_EVENT_NEW_ZONE"
   },
   [EVENTS.DeleteZone] = {
     Order = 1,
     Event = "OnEventDeleteZone",
     Text = "S_EVENT_DELETE_ZONE"
   },
   [EVENTS.NewZoneGoal] = {
     Order = 1,
     Event = "OnEventNewZoneGoal",
     Text = "S_EVENT_NEW_ZONE_GOAL"
   },
   [EVENTS.DeleteZoneGoal] = {
     Order = 1,
     Event = "OnEventDeleteZoneGoal",
     Text = "S_EVENT_DELETE_ZONE_GOAL"
   },
   [EVENTS.RemoveUnit] = {
     Order = -1,
     Event = "OnEventRemoveUnit",
     Text = "S_EVENT_REMOVE_UNIT"
   },
   [EVENTS.PlayerEnterAircraft] = {
     Order = 1,
     Event = "OnEventPlayerEnterAircraft",
     Text = "S_EVENT_PLAYER_ENTER_AIRCRAFT"
   },
   -- Added with DCS 2.5.6
   [EVENTS.DetailedFailure] = {
     Order = 1,
     Event = "OnEventDetailedFailure",
     Text = "S_EVENT_DETAILED_FAILURE"
   },
   [EVENTS.Kill] = {
     Order = 1,
     Event = "OnEventKill",
     Text = "S_EVENT_KILL"
   },
   [EVENTS.Score] = {
     Order = 1,
     Event = "OnEventScore",
     Text = "S_EVENT_SCORE"
   },
   [EVENTS.UnitLost] = {
     Order = 1,
     Event = "OnEventUnitLost",
     Text = "S_EVENT_UNIT_LOST"
   },
   [EVENTS.LandingAfterEjection] = {
     Order = 1,
     Event = "OnEventLandingAfterEjection",
     Text = "S_EVENT_LANDING_AFTER_EJECTION"
   },
   -- Added with DCS 2.7.0
   [EVENTS.ParatrooperLanding] = {
     Order = 1,
     Event = "OnEventParatrooperLanding",
     Text = "S_EVENT_PARATROOPER_LENDING"
   },
   [EVENTS.DiscardChairAfterEjection] = {
     Order = 1,
     Event = "OnEventDiscardChairAfterEjection",
     Text = "S_EVENT_DISCARD_CHAIR_AFTER_EJECTION"
   },
   [EVENTS.WeaponAdd] = {
     Order = 1,
     Event = "OnEventWeaponAdd",
     Text = "S_EVENT_WEAPON_ADD"
   },
   [EVENTS.TriggerZone] = {
     Order = 1,
     Event = "OnEventTriggerZone",
     Text = "S_EVENT_TRIGGER_ZONE"
   },
   [EVENTS.LandingQualityMark] = {
     Order = 1,
     Event = "OnEventLandingQualityMark",
     Text = "S_EVENT_LANDING_QUALITYMARK"
   },
   [EVENTS.BDA] = {
     Order = 1,
     Event = "OnEventBDA",
     Text = "S_EVENT_BDA"
   },
   -- Added with DCS 2.8
   [EVENTS.AIAbortMission] = {
     Order = 1,
     Side = "I",
     Event = "OnEventAIAbortMission",
     Text = "S_EVENT_AI_ABORT_MISSION"
   },
   [EVENTS.DayNight] = {
     Order = 1,
     Event = "OnEventDayNight",
     Text = "S_EVENT_DAYNIGHT"
   },
   [EVENTS.FlightTime] = {
     Order = 1,
     Event = "OnEventFlightTime",
     Text = "S_EVENT_FLIGHT_TIME"
   },
   [EVENTS.SelfKillPilot] = {
     Order = 1,
     Side = "I",
     Event = "OnEventSelfKillPilot",
     Text = "S_EVENT_PLAYER_SELF_KILL_PILOT"
   },
   [EVENTS.PlayerCaptureAirfield] = {
     Order = 1,
     Event = "OnEventPlayerCaptureAirfield",
     Text = "S_EVENT_PLAYER_CAPTURE_AIRFIELD"
   },
   [EVENTS.EmergencyLanding] = {
     Order = 1,
     Side = "I",
     Event = "OnEventEmergencyLanding",
     Text = "S_EVENT_EMERGENCY_LANDING"
   },
   [EVENTS.UnitCreateTask] = {
     Order = 1,
     Event = "OnEventUnitCreateTask",
     Text = "S_EVENT_UNIT_CREATE_TASK"
   },
   [EVENTS.UnitDeleteTask] = {
     Order = 1,
     Event = "OnEventUnitDeleteTask",
     Text = "S_EVENT_UNIT_DELETE_TASK"
   },
   [EVENTS.SimulationStart] = {
     Order = 1,
     Event = "OnEventSimulationStart",
     Text = "S_EVENT_SIMULATION_START"
   },
   [EVENTS.WeaponRearm] = {
     Order = 1,
     Side = "I",
     Event = "OnEventWeaponRearm",
     Text = "S_EVENT_WEAPON_REARM"
   },
   [EVENTS.WeaponDrop] = {
     Order = 1,
     Side = "I",
     Event = "OnEventWeaponDrop",
     Text = "S_EVENT_WEAPON_DROP"
   },
   -- DCS 2.9
  --[EVENTS.UnitTaskTimeout] = {
    -- Order = 1,
    -- Side = "I",
    -- Event = "OnEventUnitTaskTimeout",
    -- Text = "S_EVENT_UNIT_TASK_TIMEOUT "
   --},
  [EVENTS.UnitTaskStage] = {
     Order = 1,
     Side = "I",
     Event = "OnEventUnitTaskStage",
     Text = "S_EVENT_UNIT_TASK_STAGE "
   },
  --[EVENTS.MacSubtaskScore] = {
    -- Order = 1,
     --Side = "I",
     --Event = "OnEventMacSubtaskScore",
     --Text = "S_EVENT_MAC_SUBTASK_SCORE"
   --},
  [EVENTS.MacExtraScore] = {
     Order = 1,
     Side = "I",
     Event = "OnEventMacExtraScore",
     Text = "S_EVENT_MAC_EXTRA_SCOREP"
   },
  [EVENTS.MissionRestart] = {
     Order = 1,
     Side = "I",
     Event = "OnEventMissionRestart",
     Text = "S_EVENT_MISSION_RESTART"
   },
  [EVENTS.MissionWinner] = {
     Order = 1,
     Side = "I",
     Event = "OnEventMissionWinner",
     Text = "S_EVENT_MISSION_WINNER"
   },
  [EVENTS.RunwayTakeoff] = {
     Order = 1,
     Side = "I",
     Event = "OnEventRunwayTakeoff",
     Text = "S_EVENT_RUNWAY_TAKEOFF"
   },
  [EVENTS.RunwayTouch] = {
     Order = 1,
     Side = "I",
     Event = "OnEventRunwayTouch",
     Text = "S_EVENT_RUNWAY_TOUCH"
   }, 
     [EVENTS.MacLMSRestart] = {
     Order = 1,
     Side = "I",
     Event = "OnEventMacLMSRestart",
     Text = "S_EVENT_MAC_LMS_RESTART"
   }, 
     [EVENTS.SimulationFreeze] = {
     Order = 1,
     Side = "I",
     Event = "OnEventSimulationFreeze",
     Text = "S_EVENT_SIMULATION_FREEZE"
   }, 
     [EVENTS.SimulationUnfreeze] = {
     Order = 1,
     Side = "I",
     Event = "OnEventSimulationUnfreeze",
     Text = "S_EVENT_SIMULATION_UNFREEZE"
   }, 
     [EVENTS.HumanAircraftRepairStart] = {
     Order = 1,
     Side = "I",
     Event = "OnEventHumanAircraftRepairStart",
     Text = "S_EVENT_HUMAN_AIRCRAFT_REPAIR_START"
   }, 
     [EVENTS.HumanAircraftRepairFinish] = {
     Order = 1,
     Side = "I",
     Event = "OnEventHumanAircraftRepairFinish",
     Text = "S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH"
   },
   -- dynamic cargo
     [EVENTS.NewDynamicCargo] = {
     Order = 1,
     Side = "I",
     Event = "OnEventNewDynamicCargo",
     Text = "S_EVENT_NEW_DYNAMIC_CARGO"
   },
     [EVENTS.DynamicCargoLoaded] = {
     Order = 1,
     Side = "I",
     Event = "OnEventDynamicCargoLoaded",
     Text = "S_EVENT_DYNAMIC_CARGO_LOADED"
   },
     [EVENTS.DynamicCargoUnloaded] = {
     Order = 1,
     Side = "I",
     Event = "OnEventDynamicCargoUnloaded",
     Text = "S_EVENT_DYNAMIC_CARGO_UNLOADED"
   },
     [EVENTS.DynamicCargoRemoved] = {
     Order = 1,
     Side = "I",
     Event = "OnEventDynamicCargoRemoved",
     Text = "S_EVENT_DYNAMIC_CARGO_REMOVED"
   }, 
}


--- The Events structure
-- @type EVENT.Events
-- @field #number IniUnit

--- Create new event handler.
-- @param #EVENT self
-- @return #EVENT self
function EVENT:New()

  -- Inherit base.
  local self = BASE:Inherit( self, BASE:New() )

  -- Add world event handler.
  self.EventHandler = world.addEventHandler(self)

  return self
end


--- Initializes the Events structure for the event.
-- @param #EVENT self
-- @param DCS#world.event EventID Event ID.
-- @param Core.Base#BASE EventClass The class object for which events are handled.
-- @return #EVENT.Events
function EVENT:Init( EventID, EventClass )
  self:F3( { _EVENTMETA[EventID].Text, EventClass } )

  if not self.Events[EventID] then
    -- Create a WEAK table to ensure that the garbage collector is cleaning the event links when the object usage is cleaned.
    self.Events[EventID] = {}
  end

  -- Each event has a subtable of EventClasses, ordered by EventPriority.
  local EventPriority = EventClass:GetEventPriority()

  if not self.Events[EventID][EventPriority] then
    self.Events[EventID][EventPriority] = setmetatable( {}, { __mode = "k" } )
  end

  if not self.Events[EventID][EventPriority][EventClass] then
     self.Events[EventID][EventPriority][EventClass] = {}
  end

  return self.Events[EventID][EventPriority][EventClass]
end

--- Removes a subscription
-- @param #EVENT self
-- @param Core.Base#BASE EventClass The self instance of the class for which the event is.
-- @param DCS#world.event EventID Event ID.
-- @return #EVENT self
function EVENT:RemoveEvent( EventClass, EventID  )

  -- Debug info.
  self:F2( { "Removing subscription for class: ", EventClass:GetClassNameAndID() } )

  -- Get event prio.
  local EventPriority = EventClass:GetEventPriority()

  -- Events.
  self.Events = self.Events or {}
  self.Events[EventID] = self.Events[EventID] or {}
  self.Events[EventID][EventPriority] = self.Events[EventID][EventPriority] or {}

  -- Remove
  self.Events[EventID][EventPriority][EventClass] = nil

  return self
end

--- Resets subscriptions.
-- @param #EVENT self
-- @param Core.Base#BASE EventClass The self instance of the class for which the event is.
-- @param DCS#world.event EventID Event ID.
-- @return #EVENT.Events
function EVENT:Reset( EventObject ) --R2.1

  self:F( { "Resetting subscriptions for class: ", EventObject:GetClassNameAndID() } )

  local EventPriority = EventObject:GetEventPriority()

  for EventID, EventData in pairs( self.Events ) do
    if self.EventsDead then
      if self.EventsDead[EventID] then
        if self.EventsDead[EventID][EventPriority] then
          if self.EventsDead[EventID][EventPriority][EventObject] then
            self.Events[EventID][EventPriority][EventObject] = self.EventsDead[EventID][EventPriority][EventObject]
          end
        end
      end
    end
  end
end


--- Clears all event subscriptions for a @{Core.Base#BASE} derived object.
-- @param #EVENT self
-- @param Core.Base#BASE EventClass The self class object for which the events are removed.
-- @return #EVENT self
function EVENT:RemoveAll(EventClass)

  local EventClassName = EventClass:GetClassNameAndID()

  -- Get Event prio.
  local EventPriority = EventClass:GetEventPriority()

  for EventID, EventData in pairs( self.Events ) do
    self.Events[EventID][EventPriority][EventClass] = nil
  end

  return self
end



--- Create an OnDead event handler for a group
-- @param #EVENT self
-- @param #table EventTemplate
-- @param #function EventFunction The function to be called when the event occurs for the unit.
-- @param EventClass The instance of the class for which the event is.
-- @param #function OnEventFunction
-- @return #EVENT self
function EVENT:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EventID )
  self:F2( EventTemplate.name )

  for EventUnitID, EventUnit in pairs( EventTemplate.units ) do
    self:OnEventForUnit( EventUnit.name, EventFunction, EventClass, EventID )
  end
  return self
end

--- Set a new listener for an `S_EVENT_X` event independent from a unit or a weapon.
-- @param #EVENT self
-- @param #function EventFunction The function to be called when the event occurs for the unit.
-- @param Core.Base#BASE EventClass The self instance of the class for which the event is captured. When the event happens, the event process will be called in this class provided.
-- @param EventID
-- @return #EVENT
function EVENT:OnEventGeneric( EventFunction, EventClass, EventID )
  self:F2( { EventID, EventClass, EventFunction } )

  local EventData = self:Init( EventID, EventClass )
  EventData.EventFunction = EventFunction

  return self
end


--- Set a new listener for an `S_EVENT_X` event for a UNIT.
-- @param #EVENT self
-- @param #string UnitName The name of the UNIT.
-- @param #function EventFunction The function to be called when the event occurs for the GROUP.
-- @param Core.Base#BASE EventClass The self instance of the class for which the event is.
-- @param EventID
-- @return #EVENT self
function EVENT:OnEventForUnit( UnitName, EventFunction, EventClass, EventID )
  self:F2( UnitName )

  local EventData = self:Init( EventID, EventClass )
  EventData.EventUnit = true
  EventData.EventFunction = EventFunction
  return self
end

--- Set a new listener for an S_EVENT_X event for a GROUP.
-- @param #EVENT self
-- @param #string GroupName The name of the GROUP.
-- @param #function EventFunction The function to be called when the event occurs for the GROUP.
-- @param Core.Base#BASE EventClass The self instance of the class for which the event is.
-- @param #number EventID Event ID.
-- @param ... Optional arguments passed to the event function.
-- @return #EVENT self
function EVENT:OnEventForGroup( GroupName, EventFunction, EventClass, EventID, ... )

  local Event = self:Init( EventID, EventClass )
  Event.EventGroup = true
  Event.EventFunction = EventFunction
  Event.Params = arg
  return self
end

do -- OnBirth

  --- Create an OnBirth event handler for a group
  -- @param #EVENT self
  -- @param Wrapper.Group#GROUP EventGroup
  -- @param #function EventFunction The function to be called when the event occurs for the unit.
  -- @param EventClass The self instance of the class for which the event is.
  -- @return #EVENT self
  function EVENT:OnBirthForTemplate( EventTemplate, EventFunction, EventClass )
    self:F2( EventTemplate.name )

    self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Birth )

    return self
  end

end

do -- OnCrash

  --- Create an OnCrash event handler for a group
  -- @param #EVENT self
  -- @param Wrapper.Group#GROUP EventGroup
  -- @param #function EventFunction The function to be called when the event occurs for the unit.
  -- @param EventClass The self instance of the class for which the event is.
  -- @return #EVENT
  function EVENT:OnCrashForTemplate( EventTemplate, EventFunction, EventClass )
    self:F2( EventTemplate.name )

    self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Crash )

    return self
  end

end

do -- OnDead

  --- Create an OnDead event handler for a group
  -- @param #EVENT self
  -- @param Wrapper.Group#GROUP EventGroup The GROUP object.
  -- @param #function EventFunction The function to be called when the event occurs for the unit.
  -- @param #table EventClass The self instance of the class for which the event is.
  -- @return #EVENT self
  function EVENT:OnDeadForTemplate( EventTemplate, EventFunction, EventClass )
    self:F2( EventTemplate.name )

    self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Dead )

    return self
  end

end


do -- OnLand

  --- Create an OnLand event handler for a group
  -- @param #EVENT self
  -- @param #table EventTemplate
  -- @param #function EventFunction The function to be called when the event occurs for the unit.
  -- @param #table EventClass The self instance of the class for which the event is.
  -- @return #EVENT self
  function EVENT:OnLandForTemplate( EventTemplate, EventFunction, EventClass )
    self:F2( EventTemplate.name )

    self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Land )

    return self
  end

end

do -- OnTakeOff

  --- Create an OnTakeOff event handler for a group
  -- @param #EVENT self
  -- @param #table EventTemplate Template table.
  -- @param #function EventFunction The function to be called when the event occurs for the unit.
  -- @param #table EventClass The self instance of the class for which the event is.
  -- @return #EVENT self
  function EVENT:OnTakeOffForTemplate( EventTemplate, EventFunction, EventClass )
    self:F2( EventTemplate.name )

    self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.Takeoff )

    return self
  end

end

do -- OnEngineShutDown

  --- Create an OnDead event handler for a group
  -- @param #EVENT self
  -- @param #table EventTemplate
  -- @param #function EventFunction The function to be called when the event occurs for the unit.
  -- @param EventClass The self instance of the class for which the event is.
  -- @return #EVENT
  function EVENT:OnEngineShutDownForTemplate( EventTemplate, EventFunction, EventClass )
    self:F2( EventTemplate.name )

    self:OnEventForTemplate( EventTemplate, EventFunction, EventClass, EVENTS.EngineShutdown )

    return self
  end

end

do -- Event Creation

  --- Creation of a New Cargo Event.
  -- @param #EVENT self
  -- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created.
  function EVENT:CreateEventNewCargo( Cargo )
    self:F( { Cargo } )

    local Event = {
      id = EVENTS.NewCargo,
      time = timer.getTime(),
      cargo = Cargo,
      }

    world.onEvent( Event )
  end

  --- Creation of a Cargo Deletion Event.
  -- @param #EVENT self
  -- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created.
  function EVENT:CreateEventDeleteCargo( Cargo )
    self:F( { Cargo } )

    local Event = {
      id = EVENTS.DeleteCargo,
      time = timer.getTime(),
      cargo = Cargo,
      }

    world.onEvent( Event )
  end

  --- Creation of a New Zone Event.
  -- @param #EVENT self
  -- @param Core.Zone#ZONE_BASE Zone The Zone created.
  function EVENT:CreateEventNewZone( Zone )
    self:F( { Zone } )

    local Event = {
      id = EVENTS.NewZone,
      time = timer.getTime(),
      zone = Zone,
      }

    world.onEvent( Event )
  end

  --- Creation of a Zone Deletion Event.
  -- @param #EVENT self
  -- @param Core.Zone#ZONE_BASE Zone The Zone created.
  function EVENT:CreateEventDeleteZone( Zone )
    self:F( { Zone } )

    local Event = {
      id = EVENTS.DeleteZone,
      time = timer.getTime(),
      zone = Zone,
      }

    world.onEvent( Event )
  end

  --- Creation of a New ZoneGoal Event.
  -- @param #EVENT self
  -- @param Functional.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal created.
  function EVENT:CreateEventNewZoneGoal( ZoneGoal )
    self:F( { ZoneGoal } )

    local Event = {
      id = EVENTS.NewZoneGoal,
      time = timer.getTime(),
      ZoneGoal = ZoneGoal,
      }

    world.onEvent( Event )
  end


  --- Creation of a ZoneGoal Deletion Event.
  -- @param #EVENT self
  -- @param Functional.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal created.
  function EVENT:CreateEventDeleteZoneGoal( ZoneGoal )
    self:F( { ZoneGoal } )

    local Event = {
      id = EVENTS.DeleteZoneGoal,
      time = timer.getTime(),
      ZoneGoal = ZoneGoal,
      }

    world.onEvent( Event )
  end


  --- Creation of a S_EVENT_PLAYER_ENTER_UNIT Event.
  -- @param #EVENT self
  -- @param Wrapper.Unit#UNIT PlayerUnit.
  function EVENT:CreateEventPlayerEnterUnit( PlayerUnit )
    self:F( { PlayerUnit } )

    local Event = {
      id = EVENTS.PlayerEnterUnit,
      time = timer.getTime(),
      initiator = PlayerUnit:GetDCSObject()
      }

    world.onEvent( Event )
  end

  --- Creation of a S_EVENT_PLAYER_ENTER_AIRCRAFT event.
  -- @param #EVENT self
  -- @param Wrapper.Unit#UNIT PlayerUnit The aircraft unit the player entered.
  function EVENT:CreateEventPlayerEnterAircraft( PlayerUnit )
    self:F( { PlayerUnit } )

    local Event = {
      id = EVENTS.PlayerEnterAircraft,
      time = timer.getTime(),
      initiator = PlayerUnit:GetDCSObject()
      }

    world.onEvent( Event )
  end
  
  --- Creation of a S_EVENT_NEW_DYNAMIC_CARGO event.
  -- @param #EVENT self
  -- @param Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo the dynamic cargo object
  function EVENT:CreateEventNewDynamicCargo(DynamicCargo)
    self:F({DynamicCargo})
    local Event = {
      id = EVENTS.NewDynamicCargo,
      time = timer.getTime(),
      dynamiccargo = DynamicCargo,
      initiator = DynamicCargo:GetDCSObject(),
    }
    world.onEvent( Event )
  end
  
  --- Creation of a S_EVENT_DYNAMIC_CARGO_LOADED event.
  -- @param #EVENT self
  -- @param Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo the dynamic cargo object
  function EVENT:CreateEventDynamicCargoLoaded(DynamicCargo)
    self:F({DynamicCargo})
    local Event = {
      id = EVENTS.DynamicCargoLoaded,
      time = timer.getTime(),
      dynamiccargo = DynamicCargo,
      initiator = DynamicCargo:GetDCSObject(),
    }
    world.onEvent( Event )
  end
  
  --- Creation of a S_EVENT_DYNAMIC_CARGO_UNLOADED event.
  -- @param #EVENT self
  -- @param Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo the dynamic cargo object
  function EVENT:CreateEventDynamicCargoUnloaded(DynamicCargo)
    self:F({DynamicCargo})
    local Event = {
      id = EVENTS.DynamicCargoUnloaded,
      time = timer.getTime(),
      dynamiccargo = DynamicCargo,
      initiator = DynamicCargo:GetDCSObject(),
    }
    world.onEvent( Event )
  end
  
  --- Creation of a S_EVENT_DYNAMIC_CARGO_REMOVED event.
  -- @param #EVENT self
  -- @param Wrapper.DynamicCargo#DYNAMICCARGO DynamicCargo the dynamic cargo object
  function EVENT:CreateEventDynamicCargoRemoved(DynamicCargo)
    self:F({DynamicCargo})
    local Event = {
      id = EVENTS.DynamicCargoRemoved,
      time = timer.getTime(),
      dynamiccargo = DynamicCargo,
      initiator = DynamicCargo:GetDCSObject(),
    }
    world.onEvent( Event )
  end
  
end

--- Main event function.
-- @param #EVENT self
-- @param #EVENTDATA Event Event data table.
function EVENT:onEvent( Event )

  --- Function to handle errors.
  local ErrorHandler = function( errmsg )
    env.info( "Error in SCHEDULER function:" .. errmsg )
    if BASE.Debug ~= nil then
      env.info( debug.traceback() )
    end
    return errmsg
  end


  -- Get event meta data.
  local EventMeta = _EVENTMETA[Event.id]

  -- Check if this is a known event?
  if EventMeta then

    if self and self.Events and self.Events[Event.id] and self.MissionEnd==false and (Event.initiator~=nil or (Event.initiator==nil and Event.id~=EVENTS.PlayerLeaveUnit)) then

      -- Check if mission has ended.
      if Event.id and Event.id == EVENTS.MissionEnd then
        self.MissionEnd = true
      end

      if Event.initiator then

        Event.IniObjectCategory = Object.getCategory(Event.initiator)
        
        if Event.IniObjectCategory == Object.Category.STATIC then
          ---
          -- Static
          ---          
          if Event.id==31 then
            -- Event.initiator is a Static object representing the pilot. But getName() errors due to DCS bug.
            Event.IniDCSUnit = Event.initiator
            local ID=Event.initiator.id_
            Event.IniDCSUnitName = string.format("Ejected Pilot ID %s", tostring(ID))
            Event.IniUnitName = Event.IniDCSUnitName
            Event.IniCoalition = 0
            Event.IniCategory  = 0
            Event.IniTypeName = "Ejected Pilot"
         elseif Event.id == 33 then -- ejection seat discarded
            Event.IniDCSUnit = Event.initiator
            local ID=Event.initiator.id_
            Event.IniDCSUnitName = string.format("Ejection Seat ID %s", tostring(ID))
            Event.IniUnitName = Event.IniDCSUnitName
            Event.IniCoalition = 0
            Event.IniCategory  = 0
            Event.IniTypeName = "Ejection Seat"
          else
            Event.IniDCSUnit = Event.initiator
            Event.IniDCSUnitName = Event.IniDCSUnit:getName()
            Event.IniUnitName = Event.IniDCSUnitName
            Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false )
            Event.IniCoalition = Event.IniDCSUnit:getCoalition()
            Event.IniCategory = Event.IniDCSUnit:getDesc().category
            Event.IniTypeName = Event.IniDCSUnit:getTypeName()
          end
          
          -- Dead events of units can be delayed and the initiator changed to a static.
          -- Take care of that.
          local Unit=UNIT:FindByName(Event.IniDCSUnitName)
          if Unit then
            Event.IniObjectCategory = Object.Category.UNIT
          end       

        elseif Event.IniObjectCategory == Object.Category.UNIT then
          ---
          -- Unit
          ---        
          Event.IniDCSUnit = Event.initiator
          Event.IniDCSUnitName = Event.IniDCSUnit:getName()
          Event.IniUnitName = Event.IniDCSUnitName
          Event.IniDCSGroup = Event.IniDCSUnit:getGroup()
          Event.IniUnit = UNIT:FindByName( Event.IniDCSUnitName )
                  
          if not Event.IniUnit then
            -- Unit can be a CLIENT. Most likely this will be the case ...
            Event.IniUnit = CLIENT:FindByName( Event.IniDCSUnitName, '', true )
          end
          
          Event.IniDCSGroupName = Event.IniUnit and Event.IniUnit.GroupName or ""
          Event.IniGroupName=Event.IniDCSGroupName --At least set the group name because group might not exist any more
          if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then
            Event.IniDCSGroupName = Event.IniDCSGroup:getName()
            Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName )
            Event.IniGroupName = Event.IniDCSGroupName
          end
          
          Event.IniPlayerName = Event.IniDCSUnit:getPlayerName()
          if Event.IniPlayerName then
            -- get UUCID
            local PID = NET.GetPlayerIDByName(nil,Event.IniPlayerName)
            if PID then
              Event.IniPlayerUCID = net.get_player_info(tonumber(PID), 'ucid')
              --env.info("Event.IniPlayerUCID="..tostring(Event.IniPlayerUCID),false)
            end
          end
          Event.IniCoalition = Event.IniDCSUnit:getCoalition()
          Event.IniTypeName = Event.IniDCSUnit:getTypeName()
          Event.IniCategory = Event.IniDCSUnit:getDesc().category  

        elseif Event.IniObjectCategory == Object.Category.CARGO then
          ---
          -- Cargo
          ---
          Event.IniDCSUnit = Event.initiator
          Event.IniDCSUnitName = Event.IniDCSUnit:getName()
          Event.IniUnitName = Event.IniDCSUnitName
          if string.match(Event.IniUnitName,".+|%d%d:%d%d|PKG%d+") then
            Event.IniDynamicCargo = DYNAMICCARGO:FindByName(Event.IniUnitName)
            Event.IniDynamicCargoName = Event.IniUnitName
            Event.IniPlayerName = string.match(Event.IniUnitName,"^(.+)|%d%d:%d%d|PKG%d+")
          else
            Event.IniUnit = CARGO:FindByName( Event.IniDCSUnitName )
          end
          Event.IniCoalition = Event.IniDCSUnit:getCoalition()
          Event.IniCategory = Event.IniDCSUnit:getDesc().category
          Event.IniTypeName = Event.IniDCSUnit:getTypeName()

        elseif Event.IniObjectCategory == Object.Category.SCENERY then
          ---
          -- Scenery
          ---          
          Event.IniDCSUnit = Event.initiator
          Event.IniDCSUnitName = ( Event.IniDCSUnit and Event.IniDCSUnit.getName ) and Event.IniDCSUnit:getName() or "Scenery no name "..math.random(1,20000)
          Event.IniUnitName = Event.IniDCSUnitName
          Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator )
          Event.IniCategory =  (Event.IniDCSUnit and Event.IniDCSUnit.getDesc ) and Event.IniDCSUnit:getDesc().category
          Event.IniTypeName = (Event.initiator and Event.initiator.isExist          
          and Event.initiator:isExist() and Event.IniDCSUnit and Event.IniDCSUnit.getTypeName) and Event.IniDCSUnit:getTypeName() or "SCENERY"

        elseif Event.IniObjectCategory == Object.Category.BASE then
          ---
          -- Base Object
          ---
          Event.IniDCSUnit = Event.initiator
          Event.IniDCSUnitName = Event.IniDCSUnit:getName()
          Event.IniUnitName = Event.IniDCSUnitName
          Event.IniUnit = AIRBASE:FindByName(Event.IniDCSUnitName)
          Event.IniCoalition = Event.IniDCSUnit:getCoalition()
          Event.IniCategory = Event.IniDCSUnit:getDesc().category
          Event.IniTypeName = Event.IniDCSUnit:getTypeName()
          
          -- If the airbase does not exist in the DB, we add it (e.g. when FARPS are spawned).
          if not Event.IniUnit then
            _DATABASE:_RegisterAirbase(Event.initiator)
            Event.IniUnit = AIRBASE:FindByName(Event.IniDCSUnitName)
          end
        end
      end

      if Event.target then
      
        ---
        -- TARGET
        ---

        -- Target category.
        Event.TgtObjectCategory = Object.getCategory(Event.target)

        if Event.TgtObjectCategory == Object.Category.UNIT then
          ---
          -- UNIT
          ---
          Event.TgtDCSUnit = Event.target
          Event.TgtDCSGroup = Event.TgtDCSUnit:getGroup()
          Event.TgtDCSUnitName = Event.TgtDCSUnit:getName()
          Event.TgtUnitName = Event.TgtDCSUnitName
          Event.TgtUnit = UNIT:FindByName( Event.TgtDCSUnitName )
          Event.TgtDCSGroupName = ""
          if Event.TgtDCSGroup and Event.TgtDCSGroup:isExist() then
            Event.TgtDCSGroupName = Event.TgtDCSGroup:getName()
            Event.TgtGroup = GROUP:FindByName( Event.TgtDCSGroupName )
            Event.TgtGroupName = Event.TgtDCSGroupName
          end
          Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName()
          if Event.TgtPlayerName  then
            -- get UUCID
            local PID = NET.GetPlayerIDByName(nil,Event.TgtPlayerName)
            if PID then
              Event.TgtPlayerUCID = net.get_player_info(tonumber(PID), 'ucid')
              --env.info("Event.TgtPlayerUCID="..tostring(Event.TgtPlayerUCID),false)
            end
          end
          Event.TgtCoalition = Event.TgtDCSUnit:getCoalition()
          Event.TgtCategory = Event.TgtDCSUnit:getDesc().category
          Event.TgtTypeName = Event.TgtDCSUnit:getTypeName()

        elseif Event.TgtObjectCategory == Object.Category.STATIC then
          ---
          -- STATIC
          ---
          Event.TgtDCSUnit = Event.target
          if Event.target.isExist and Event.target:isExist() and Event.id ~= 33 then -- leave out ejected seat object, check that isExist exists (Kiowa Hellfire issue, Special K)
            Event.TgtDCSUnitName = Event.TgtDCSUnit:getName()
            -- Workaround for borked target info on cruise missiles
            if Event.TgtDCSUnitName and Event.TgtDCSUnitName ~= "" then
              Event.TgtUnitName = Event.TgtDCSUnitName
              Event.TgtUnit = STATIC:FindByName( Event.TgtDCSUnitName, false )
              Event.TgtCoalition = Event.TgtDCSUnit:getCoalition()
              Event.TgtCategory = Event.TgtDCSUnit:getDesc().category
              Event.TgtTypeName = Event.TgtDCSUnit:getTypeName()
            end
          else
            Event.TgtDCSUnitName = string.format("No target object for Event ID %s", tostring(Event.id))
            Event.TgtUnitName = Event.TgtDCSUnitName
            Event.TgtUnit = nil
            Event.TgtCoalition = 0
            Event.TgtCategory = 0
            if Event.id == 6 then
              Event.TgtTypeName = "Ejected Pilot"
              Event.TgtDCSUnitName = string.format("Ejected Pilot ID %s", tostring(Event.IniDCSUnitName))
              Event.TgtUnitName = Event.TgtDCSUnitName
            elseif Event.id == 33 then
              Event.TgtTypeName = "Ejection Seat"
              Event.TgtDCSUnitName = string.format("Ejection Seat ID %s", tostring(Event.IniDCSUnitName))
              Event.TgtUnitName = Event.TgtDCSUnitName
            else
              Event.TgtTypeName = "Static"
            end
          end

        elseif Event.TgtObjectCategory == Object.Category.SCENERY then
          ---
          -- SCENERY
          ---
          Event.TgtDCSUnit = Event.target
          Event.TgtDCSUnitName = Event.TgtDCSUnit.getName and Event.TgtDCSUnit.getName() or nil
          if Event.TgtDCSUnitName~=nil then
            Event.TgtUnitName = Event.TgtDCSUnitName
            Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target )
            Event.TgtCategory = Event.TgtDCSUnit:getDesc().category
            Event.TgtTypeName = Event.TgtDCSUnit:getTypeName()
          end
        end
      end

      -- Weapon.
      if Event.weapon and type(Event.weapon) == "table" and Event.weapon.isExist and Event.weapon:isExist() then
        Event.Weapon = Event.weapon
        Event.WeaponName = Event.weapon:isExist() and Event.weapon:getTypeName() or "Unknown Weapon"
        Event.WeaponUNIT = CLIENT:Find( Event.Weapon, '', true ) -- Sometimes, the weapon is a player unit!
        Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon.getPlayerName and Event.Weapon:getPlayerName()
        --Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName()
        Event.WeaponCoalition = Event.WeaponUNIT and Event.Weapon.getCoalition and Event.Weapon:getCoalition()
        Event.WeaponCategory = Event.WeaponUNIT and Event.Weapon.getDesc and Event.Weapon:getDesc().category
        Event.WeaponTypeName = Event.WeaponUNIT and Event.Weapon.getTypeName  and Event.Weapon:getTypeName()
        --Event.WeaponTgtDCSUnit = Event.Weapon:getTarget()
      end

      -- Place should be given for takeoff and landing events as well as base captured. It should be a DCS airbase.
      if Event.place then
        if Event.id==EVENTS.LandingAfterEjection then
          -- Place is here the UNIT of which the pilot ejected.
          --local name=Event.place:getName()  -- This returns a DCS error "Airbase doesn't exit" :(
          -- However, this is not a big thing, as the aircraft the pilot ejected from is usually long crashed before the ejected pilot touches the ground.
          --Event.Place=UNIT:Find(Event.place)
        else  
          if Event.place:isExist() and Object.getCategory(Event.place) ~= Object.Category.SCENERY then
            Event.Place=AIRBASE:Find(Event.place)
            if Event.Place then
              Event.PlaceName=Event.Place:GetName()
            end
          end
        end
      end

      --  Mark points.
      if Event.idx then
        Event.MarkID=Event.idx
        Event.MarkVec3=Event.pos
        Event.MarkCoordinate=COORDINATE:NewFromVec3(Event.pos)
        Event.MarkText=Event.text
        Event.MarkCoalition=Event.coalition
        Event.IniCoalition=Event.coalition
        Event.MarkGroupID = Event.groupID
      end

      -- Cargo object.
      if Event.cargo then
        Event.Cargo = Event.cargo
        Event.CargoName = Event.cargo.Name
      end
      
      -- Dynamic cargo Object
      if Event.dynamiccargo then
        Event.IniDynamicCargo = Event.dynamiccargo
        Event.IniDynamicCargoName = Event.IniDynamicCargo.StaticName
        if Event.IniDynamicCargo.Owner or Event.IniUnitName then
          Event.IniPlayerName = Event.IniDynamicCargo.Owner or string.match(Event.IniUnitName or "None|00:00|PKG00","^(.+)|%d%d:%d%d|PKG%d+")
        end
      end

      -- Zone object.
      if Event.zone then
        Event.Zone = Event.zone
        Event.ZoneName = Event.zone.ZoneName
      end

      -- Priority order.
      local PriorityOrder = EventMeta.Order
      local PriorityBegin = PriorityOrder == -1 and 5 or 1
      local PriorityEnd   = PriorityOrder == -1 and 1 or 5

      for EventPriority = PriorityBegin, PriorityEnd, PriorityOrder do

        if self.Events[Event.id][EventPriority] then

          -- Okay, we got the event from DCS. Now loop the SORTED self.EventSorted[] table for the received Event.id, and for each EventData registered, check if a function needs to be called.
          for EventClass, EventData in pairs( self.Events[Event.id][EventPriority] ) do

            --if Event.IniObjectCategory ~= Object.Category.STATIC then
            --  self:E( { "Evaluating: ", EventClass:GetClassNameAndID() } )
            --end

            Event.IniGroup = Event.IniGroup or GROUP:FindByName( Event.IniDCSGroupName )
            Event.TgtGroup = Event.TgtGroup or GROUP:FindByName( Event.TgtDCSGroupName )

            -- If the EventData is for a UNIT, the call directly the EventClass EventFunction for that UNIT.
            if EventData.EventUnit then

              -- So now the EventClass must be a UNIT class!!! We check if it is still "Alive".
              if EventClass:IsAlive() or
                 Event.id == EVENTS.PlayerEnterUnit or
                 Event.id == EVENTS.Crash or
                 Event.id == EVENTS.Dead or
                 Event.id == EVENTS.RemoveUnit or
                 Event.id == EVENTS.UnitLost then

                local UnitName = EventClass:GetName()

                if ( EventMeta.Side == "I" and UnitName == Event.IniDCSUnitName ) or
                   ( EventMeta.Side == "T" and UnitName == Event.TgtDCSUnitName ) then
                   
                  -- First test if a EventFunction is Set, otherwise search for the default function
                  if EventData.EventFunction then
 
                    local Result, Value = xpcall(
                      function()
                        return EventData.EventFunction( EventClass, Event )
                      end, ErrorHandler )

                  else

                    -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object.
                    local EventFunction = EventClass[ EventMeta.Event ]
                    if EventFunction and type( EventFunction ) == "function" then

                      -- Now call the default event function.
                      local Result, Value = xpcall(
                        function()
                          return EventFunction( EventClass, Event )
                        end, ErrorHandler )
                    end
                    
                  end
                end
              else
                -- The EventClass is not alive anymore, we remove it from the EventHandlers...
                self:RemoveEvent( EventClass, Event.id )
              end

            else

              --- If the EventData is for a GROUP, the call directly the EventClass EventFunction for the UNIT in that GROUP.
              if EventData.EventGroup then

                -- So now the EventClass must be a GROUP class!!! We check if it is still "Alive".
                if EventClass:IsAlive() or
                   Event.id == EVENTS.PlayerEnterUnit or
                   Event.id == EVENTS.Crash or
                   Event.id == EVENTS.Dead or
                   Event.id == EVENTS.RemoveUnit or
                   Event.id == EVENTS.UnitLost then

                  -- We can get the name of the EventClass, which is now always a GROUP object.
                  local GroupName = EventClass:GetName()

                  if ( EventMeta.Side == "I" and GroupName == Event.IniDCSGroupName ) or
                     ( EventMeta.Side == "T" and GroupName == Event.TgtDCSGroupName ) then

                    -- First test if a EventFunction is Set, otherwise search for the default function
                    if EventData.EventFunction then

                      local Result, Value = xpcall(
                        function()
                          return EventData.EventFunction( EventClass, Event, unpack( EventData.Params ) )
                        end, ErrorHandler )

                    else

                      -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object.
                      local EventFunction = EventClass[ EventMeta.Event ]
                      if EventFunction and type( EventFunction ) == "function" then

                        -- Now call the default event function.
                        local Result, Value = xpcall(
                          function()
                            return EventFunction( EventClass, Event, unpack( EventData.Params ) )
                          end, ErrorHandler )
                      end
                    end
                  end
                else
                  -- The EventClass is not alive anymore, we remove it from the EventHandlers...
                  --self:RemoveEvent( EventClass, Event.id )
                end
              else
              
                -- If the EventData is not bound to a specific unit, then call the EventClass EventFunction.
                -- Note that here the EventFunction will need to implement and determine the logic for the relevant source- or target unit, or weapon.
                if not EventData.EventUnit then

                  -- First test if a EventFunction is Set, otherwise search for the default function
                  if EventData.EventFunction then

                    -- There is an EventFunction defined, so call the EventFunction.
                    local Result, Value = xpcall(
                      function()
                        return EventData.EventFunction( EventClass, Event )
                      end, ErrorHandler )
                  else

                    -- There is no EventFunction defined, so try to find if a default OnEvent function is defined on the object.
                    local EventFunction = EventClass[ EventMeta.Event ]
                    if EventFunction and type( EventFunction ) == "function" then

                      -- Now call the default event function.
                      local Result, Value = xpcall(
                        function()
                          local Result, Value = EventFunction( EventClass, Event )
                          return Result, Value
                        end, ErrorHandler )
                        
                    end
                    
                  end

                end
              end
            end
          end
        end
      end

      -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD.
      -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call.
      -- And this is a problem because it will remove all entries from the SET_CARGOs.
      -- To prevent this from happening, the Cargo object has a flag NoDestroy.
      -- When true, the SET_CARGO won't Remove the Cargo object from the set.
      -- But we need to switch that flag off after the event handlers have been called.
      if Event.id == EVENTS.DeleteCargo then
        Event.Cargo.NoDestroy = nil
      end
    else
      self:T( { EventMeta.Text, Event } )
    end
  else
    self:E(string.format("WARNING: Could not get EVENTMETA data for event ID=%d! Is this an unknown/new DCS event?", tostring(Event.id)))
  end

  Event = nil
end

--- The EVENTHANDLER structure.
-- @type EVENTHANDLER
-- @extends Core.Base#BASE
EVENTHANDLER = {
  ClassName = "EVENTHANDLER",
  ClassID = 0,
}

--- The EVENTHANDLER constructor.
-- @param #EVENTHANDLER self
-- @return #EVENTHANDLER self
function EVENTHANDLER:New()
  self = BASE:Inherit( self, BASE:New() ) -- #EVENTHANDLER
  return self
end
--- **Core** - Manages various settings for missions, providing a menu for players to tweak settings in running missions.
--
-- ===
--
-- ## Features:
--
--   * Provide a settings menu system to the players.
--   * Provide a player settings menu and an overall mission settings menu.
--   * Mission settings provide default settings, while player settings override mission settings.
--   * Provide a menu to select between different coordinate formats for A2G coordinates.
--   * Provide a menu to select between different coordinate formats for A2A coordinates.
--   * Provide a menu to select between different message time duration options.
--   * Provide a menu to select between different metric systems.
--
-- ===
--
-- The documentation of the SETTINGS class can be found further in this document.
--
-- ===
--
-- # **AUTHORS and CONTRIBUTIONS**
--
-- ### Contributions:
--
-- ### Authors:
--
--   * **FlightControl**: Design & Programming
--
-- @module Core.Settings
-- @image Core_Settings.JPG


---
-- @type SETTINGS
-- @extends Core.Base#BASE

--- Takes care of various settings that influence the behavior of certain functionalities and classes within the MOOSE framework.
--
-- ===
--
-- The SETTINGS class takes care of various settings that influence the behavior of certain functionalities and classes within the MOOSE framework.
-- SETTINGS can work on 2 levels:
--
--   - **Default settings**: A running mission has **Default settings**.
--   - **Player settings**: For each player its own **Player settings** can be defined, overriding the **Default settings**.
--
-- So, when there isn't any **Player setting** defined for a player for a specific setting, or, the player cannot be identified, the **Default setting** will be used instead.
--
-- # 1) \_SETTINGS object
--
-- MOOSE defines by default a singleton object called **\_SETTINGS**. Use this object to modify all the **Default settings** for a running mission.
-- For each player, MOOSE will automatically allocate also a **player settings** object, and will expose a radio menu to allow the player to adapt the settings to his own preferences.
--
-- # 2) SETTINGS Menu
--
-- Settings can be adapted by the Players and by the Mission Administrator through **radio menus, which are automatically available in the mission**.
-- These menus can be found **on level F10 under "Settings"**. There are two kinds of menus generated by the system.
--
-- ## 2.1) Default settings menu
--
-- A menu is created automatically per Command Center that allows to modify the **Default** settings.
-- So, when joining a CC unit, a menu will be available that allows to change the settings parameters **FOR ALL THE PLAYERS**!
-- Note that the **Default settings** will only be used when a player has not chosen its own settings.
--
-- ## 2.2) Player settings menu
--
-- A menu is created automatically per Player Slot (group) that allows to modify the **Player** settings.
-- So, when joining a slot, a menu wil be available that allows to change the settings parameters **FOR THE PLAYER ONLY**!
-- Note that when a player has not chosen a specific setting, the **Default settings** will be used.
--
-- ## 2.3) Show or Hide the Player Setting menus
--
-- Of course, it may be required not to show any setting menus. In this case, a method is available on the **\_SETTINGS object**.
-- Use @{#SETTINGS.SetPlayerMenuOff}() to hide the player menus, and use @{#SETTINGS.SetPlayerMenuOn}() show the player menus.
-- Note that when this method is used, any player already in a slot will not have its menus visibility changed.
-- The option will only have effect when a player enters a new slot or changes a slot.
--
-- Example:
--
--      _SETTINGS:SetPlayerMenuOff() -- will disable the player menus.
--      _SETTINGS:SetPlayerMenuOn() -- will enable the player menus.
--      -- But only when a player exits and reenters the slot these settings will have effect!
--
--
-- # 3) Settings
--
-- There are different settings that are managed and applied within the MOOSE framework.
-- See below a comprehensive description of each.
--
-- ## 3.1) **A2G coordinates** display formatting
--
-- ### 3.1.1) A2G coordinates setting **types**
--
-- Will customize which display format is used to indicate A2G coordinates in text as part of the Command Center communications.
--
--   - A2G BR: [Bearing Range](https://en.wikipedia.org/wiki/Bearing_\(navigation\)).
--   - A2G MGRS: The [Military Grid Reference System](https://en.wikipedia.org/wiki/Military_Grid_Reference_System). The accuracy can also be adapted.
--   - A2G LL DMS: Latitude Longitude [Degrees Minutes Seconds](https://en.wikipedia.org/wiki/Geographic_coordinate_conversion). The accuracy can also be adapted.
--   - A2G LL DDM: Latitude Longitude [Decimal Degrees Minutes](https://en.wikipedia.org/wiki/Decimal_degrees). The accuracy can also be adapted.
--
-- ### 3.1.2) A2G coordinates setting **menu**
--
-- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot.
--
-- ### 3.1.3) A2G coordinates setting **methods**
--
-- There are different methods that can be used to change the **System settings** using the \_SETTINGS object.
--
--   - @{#SETTINGS.SetA2G_BR}(): Enable the BR display formatting by default.
--   - @{#SETTINGS.SetA2G_MGRS}(): Enable the MGRS display formatting by default. Use @{#SETTINGS.SetMGRS_Accuracy}() to adapt the accuracy of the MGRS formatting.
--   - @{#SETTINGS.SetA2G_LL_DMS}(): Enable the LL DMS display formatting by default. Use @{#SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting.
--   - @{#SETTINGS.SetA2G_LL_DDM}(): Enable the LL DDM display formatting by default. Use @{#SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting.
--
-- ### 3.1.4) A2G coordinates setting - additional notes
--
-- One additional note on BR. In a situation when a BR coordinate should be given,
-- but there isn't any player context (no player unit to reference from), the MGRS formatting will be applied!
--
-- ## 3.2) **A2A coordinates** formatting
--
-- ### 3.2.1) A2A coordinates setting **types**
--
-- Will customize which display format is used to indicate A2A coordinates in text as part of the Command Center communications.
--
--   - A2A BRAA: [Bearing Range Altitude Aspect](https://en.wikipedia.org/wiki/Bearing_\(navigation\)).
--   - A2A MGRS: The [Military Grid Reference System](https://en.wikipedia.org/wiki/Military_Grid_Reference_System). The accuracy can also be adapted.
--   - A2A LL DMS: Lattitude Longitude [Degrees Minutes Seconds](https://en.wikipedia.org/wiki/Geographic_coordinate_conversion). The accuracy can also be adapted.
--   - A2A LL DDM: Lattitude Longitude [Decimal Degrees and Minutes](https://en.wikipedia.org/wiki/Decimal_degrees). The accuracy can also be adapted.
--   - A2A BULLS: [Bullseye](http://falcon4.wikidot.com/concepts:bullseye).
--
-- ### 3.2.2) A2A coordinates setting **menu**
--
-- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot.
--
-- ### 3.2.3) A2A coordinates setting **methods**
--
-- There are different methods that can be used to change the **System settings** using the \_SETTINGS object.
--
--   - @{#SETTINGS.SetA2A_BRAA}(): Enable the BR display formatting by default.
--   - @{#SETTINGS.SetA2A_MGRS}(): Enable the MGRS display formatting by default. Use @{#SETTINGS.SetMGRS_Accuracy}() to adapt the accuracy of the MGRS formatting.
--   - @{#SETTINGS.SetA2A_LL_DMS}(): Enable the LL DMS display formatting by default. Use @{#SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting.
--   - @{#SETTINGS.SetA2A_LL_DDM}(): Enable the LL DDM display formatting by default. Use @{#SETTINGS.SetLL_Accuracy}() to adapt the accuracy of the Seconds formatting.
--   - @{#SETTINGS.SetA2A_BULLS}(): Enable the BULLSeye display formatting by default.
--
-- ### 3.2.4) A2A coordinates settings - additional notes
--
-- One additional note on BRAA. In a situation when a BRAA coordinate should be given,
-- but there isn't any player context (no player unit to reference from), the MGRS formatting will be applied!
--
-- ## 3.3) **Measurements** formatting
--
-- ### 3.3.1) Measurements setting **types**
--
-- Will customize the measurements system being used as part as part of the Command Center communications.
--
--   - **Metrics** system: Applies the [Metrics system](https://en.wikipedia.org/wiki/Metric_system) ...
--   - **Imperial** system: Applies the [Imperial system](https://en.wikipedia.org/wiki/Imperial_units) ...
--
-- ### 3.3.2) Measurements setting **menu**
--
-- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot.
--
-- ### 3.3.3) Measurements setting **methods**
--
-- There are different methods that can be used to change the **Default settings** using the \_SETTINGS object.
--
--   - @{#SETTINGS.SetMetric}(): Enable the Metric system.
--   - @{#SETTINGS.SetImperial}(): Enable the Imperial system.
--
-- ## 3.4) **Message** display times
--
-- ### 3.4.1) Message setting **types**
--
-- There are various **Message Types** that will influence the duration how long a message will appear as part of the Command Center communications.
--
--   - **Update** message: A short update message.
--   - **Information** message: Provides new information **while** executing a mission.
--   - **Briefing** message: Provides a complete briefing **before** executing a mission.
--   - **Overview report**: Provides a short report overview, the summary of the report.
--   - **Detailed report**: Provides a complete report.
--
-- ### 3.4.2) Message setting **menu**
--
-- The settings can be changed by using the **Default settings menu** on the Command Center or the **Player settings menu** on the Player Slot.
--
-- Each Message Type has specific timings that will be applied when the message is displayed.
-- The Settings Menu will provide for each Message Type a selection of proposed durations from which can be chosen.
-- So the player can choose its own amount of seconds how long a message should be displayed of a certain type.
-- Note that **Update** messages can be chosen not to be displayed at all!
--
-- ### 3.4.3) Message setting **methods**
--
-- There are different methods that can be used to change the **System settings** using the \_SETTINGS object.
--
--   - @{#SETTINGS.SetMessageTime}(): Define for a specific @{Core.Message#MESSAGE.MessageType} the duration to be displayed in seconds.
--   - @{#SETTINGS.GetMessageTime}(): Retrieves for a specific @{Core.Message#MESSAGE.MessageType} the duration to be displayed in seconds.
--
-- ## 3.5) **Era** of the battle
--
-- The threat level metric is scaled according the era of the battle. A target that is AAA, will pose a much greater threat in WWII than on modern warfare.
-- Therefore, there are 4 era that are defined within the settings:
--
--   - **WWII** era: Use for warfare with equipment during the world war II time.
--   - **Korea** era: Use for warfare with equipment during the Korea war time.
--   - **Cold War** era: Use for warfare with equipment during the cold war time.
--   - **Modern** era: Use for warfare with modern equipment in the 2000s.
--
-- There are different API defined that you can use with the _SETTINGS object to configure your mission script to work in one of the 4 era:
-- @{#SETTINGS.SetEraWWII}(), @{#SETTINGS.SetEraKorea}(), @{#SETTINGS.SetEraCold}(), @{#SETTINGS.SetEraModern}()
--
-- ===
--
-- @field #SETTINGS
SETTINGS = {
  ClassName = "SETTINGS",
  ShowPlayerMenu = true,
  MenuShort = false,
  MenuStatic = false,
}

SETTINGS.__Enum = {}

---
-- @type SETTINGS.__Enum.Era
-- @field #number WWII
-- @field #number Korea
-- @field #number Cold
-- @field #number Modern
SETTINGS.__Enum.Era = {
  WWII = 1,
  Korea = 2,
  Cold = 3,
  Modern = 4,
}

do -- SETTINGS

  --- SETTINGS constructor.
  -- @param #SETTINGS self
  -- @param #string PlayerName (Optional) Set settings for this player.
  -- @return #SETTINGS
  function SETTINGS:Set( PlayerName )

    if PlayerName == nil then
      local self = BASE:Inherit( self, BASE:New() ) -- #SETTINGS
      self:SetMetric() -- Defaults
      self:SetA2G_BR() -- Defaults
      self:SetA2A_BRAA() -- Defaults
      self:SetLL_Accuracy( 3 ) -- Defaults
      self:SetMGRS_Accuracy( 5 ) -- Defaults
      self:SetMessageTime( MESSAGE.Type.Briefing, 180 )
      self:SetMessageTime( MESSAGE.Type.Detailed, 60 )
      self:SetMessageTime( MESSAGE.Type.Information, 30 )
      self:SetMessageTime( MESSAGE.Type.Overview, 60 )
      self:SetMessageTime( MESSAGE.Type.Update, 15 )
      self:SetEraModern()
      self:SetLocale("en")
      return self
    else
      local Settings = _DATABASE:GetPlayerSettings( PlayerName )
      if not Settings then
        Settings = BASE:Inherit( self, BASE:New() ) -- #SETTINGS
        _DATABASE:SetPlayerSettings( PlayerName, Settings )
      end
      return Settings
    end
  end

  --- Set short text for menus on (*true*) or off (*false*).
  -- Short text are better suited for, e.g., VR.
  -- @param #SETTINGS self
  -- @param #boolean onoff If *true* use short menu texts. If *false* long ones (default).
  function SETTINGS:SetMenutextShort( onoff )
    _SETTINGS.MenuShort = onoff
  end

  --- Set menu to be static.
  -- @param #SETTINGS self
  -- @param #boolean onoff If *true* menu is static. If *false* menu will be updated after changes (default).
  function SETTINGS:SetMenuStatic( onoff )
    _SETTINGS.MenuStatic = onoff
  end

  --- Sets the SETTINGS metric.
  -- @param #SETTINGS self
  function SETTINGS:SetMetric()
    self.Metric = true
  end

  --- Sets the SETTINGS default text locale.
  -- @param #SETTINGS self
  -- @param #string Locale
  function SETTINGS:SetLocale(Locale)
    self.Locale = Locale or "en"
  end

  --- Gets the SETTINGS text locale.
  -- @param #SETTINGS self
  -- @return #string
  function SETTINGS:GetLocale()
    return self.Locale or _SETTINGS:GetLocale()
  end

  --- Gets if the SETTINGS is metric.
  -- @param #SETTINGS self
  -- @return #boolean true if metric.
  function SETTINGS:IsMetric()
    return (self.Metric ~= nil and self.Metric == true) or (self.Metric == nil and _SETTINGS:IsMetric())
  end

  --- Sets the SETTINGS imperial.
  -- @param #SETTINGS self
  function SETTINGS:SetImperial()
    self.Metric = false
  end

  --- Gets if the SETTINGS is imperial.
  -- @param #SETTINGS self
  -- @return #boolean true if imperial.
  function SETTINGS:IsImperial()
    return (self.Metric ~= nil and self.Metric == false) or (self.Metric == nil and _SETTINGS:IsImperial())
  end

  --- Sets the SETTINGS LL accuracy.
  -- @param #SETTINGS self
  -- @param #number LL_Accuracy
  -- @return #SETTINGS
  function SETTINGS:SetLL_Accuracy( LL_Accuracy )
    self.LL_Accuracy = LL_Accuracy
  end

  --- Gets the SETTINGS LL accuracy.
  -- @param #SETTINGS self
  -- @return #number
  function SETTINGS:GetLL_DDM_Accuracy()
    return self.LL_DDM_Accuracy or _SETTINGS:GetLL_DDM_Accuracy()
  end

  --- Sets the SETTINGS MGRS accuracy.
  -- @param #SETTINGS self
  -- @param #number MGRS_Accuracy 0 to 5
  -- @return #SETTINGS
  function SETTINGS:SetMGRS_Accuracy( MGRS_Accuracy )
    self.MGRS_Accuracy = MGRS_Accuracy
  end

  --- Gets the SETTINGS MGRS accuracy.
  -- @param #SETTINGS self
  -- @return #number
  function SETTINGS:GetMGRS_Accuracy()
    return self.MGRS_Accuracy or _SETTINGS:GetMGRS_Accuracy()
  end

  --- Sets the SETTINGS Message Display Timing of a MessageType
  -- @param #SETTINGS self
  -- @param Core.Message#MESSAGE MessageType The type of the message.
  -- @param #number MessageTime The display time duration in seconds of the MessageType.
  function SETTINGS:SetMessageTime( MessageType, MessageTime )
    self.MessageTypeTimings = self.MessageTypeTimings or {}
    self.MessageTypeTimings[MessageType] = MessageTime
  end

  --- Gets the SETTINGS Message Display Timing of a MessageType
  -- @param #SETTINGS self
  -- @param Core.Message#MESSAGE MessageType The type of the message.
  -- @return #number
  function SETTINGS:GetMessageTime( MessageType )
    return (self.MessageTypeTimings and self.MessageTypeTimings[MessageType]) or _SETTINGS:GetMessageTime( MessageType )
  end

  --- Sets A2G LL DMS
  -- @param #SETTINGS self
  -- @return #SETTINGS
  function SETTINGS:SetA2G_LL_DMS()
    self.A2GSystem = "LL DMS"
  end

  --- Sets A2G LL DDM
  -- @param #SETTINGS self
  -- @return #SETTINGS
  function SETTINGS:SetA2G_LL_DDM()
    self.A2GSystem = "LL DDM"
  end

  --- Is LL DMS
  -- @param #SETTINGS self
  -- @return #boolean true if LL DMS
  function SETTINGS:IsA2G_LL_DMS()
    return (self.A2GSystem and self.A2GSystem == "LL DMS") or (not self.A2GSystem and _SETTINGS:IsA2G_LL_DMS())
  end

  --- Is LL DDM
  -- @param #SETTINGS self
  -- @return #boolean true if LL DDM
  function SETTINGS:IsA2G_LL_DDM()
    return (self.A2GSystem and self.A2GSystem == "LL DDM") or (not self.A2GSystem and _SETTINGS:IsA2G_LL_DDM())
  end

  --- Sets A2G MGRS
  -- @param #SETTINGS self
  -- @return #SETTINGS
  function SETTINGS:SetA2G_MGRS()
    self.A2GSystem = "MGRS"
  end

  --- Is MGRS
  -- @param #SETTINGS self
  -- @return #boolean true if MGRS
  function SETTINGS:IsA2G_MGRS()
    return (self.A2GSystem and self.A2GSystem == "MGRS") or (not self.A2GSystem and _SETTINGS:IsA2G_MGRS())
  end

  --- Sets A2G BRA
  -- @param #SETTINGS self
  -- @return #SETTINGS
  function SETTINGS:SetA2G_BR()
    self.A2GSystem = "BR"
  end

  --- Is BRA
  -- @param #SETTINGS self
  -- @return #boolean true if BRA
  function SETTINGS:IsA2G_BR()
    return (self.A2GSystem and self.A2GSystem == "BR") or (not self.A2GSystem and _SETTINGS:IsA2G_BR())
  end

  --- Sets A2A BRA
  -- @param #SETTINGS self
  -- @return #SETTINGS
  function SETTINGS:SetA2A_BRAA()
    self.A2ASystem = "BRAA"
  end

  --- Is BRA
  -- @param #SETTINGS self
  -- @return #boolean true if BRA
  function SETTINGS:IsA2A_BRAA()
    return (self.A2ASystem and self.A2ASystem == "BRAA") or (not self.A2ASystem and _SETTINGS:IsA2A_BRAA())
  end

  --- Sets A2A BULLS
  -- @param #SETTINGS self
  -- @return #SETTINGS
  function SETTINGS:SetA2A_BULLS()
    self.A2ASystem = "BULLS"
  end

  --- Is BULLS
  -- @param #SETTINGS self
  -- @return #boolean true if BULLS
  function SETTINGS:IsA2A_BULLS()
    return (self.A2ASystem and self.A2ASystem == "BULLS") or (not self.A2ASystem and _SETTINGS:IsA2A_BULLS())
  end

  --- Sets A2A LL DMS
  -- @param #SETTINGS self
  -- @return #SETTINGS
  function SETTINGS:SetA2A_LL_DMS()
    self.A2ASystem = "LL DMS"
  end

  --- Sets A2A LL DDM
  -- @param #SETTINGS self
  -- @return #SETTINGS
  function SETTINGS:SetA2A_LL_DDM()
    self.A2ASystem = "LL DDM"
  end

  --- Is LL DMS
  -- @param #SETTINGS self
  -- @return #boolean true if LL DMS
  function SETTINGS:IsA2A_LL_DMS()
    return (self.A2ASystem and self.A2ASystem == "LL DMS") or (not self.A2ASystem and _SETTINGS:IsA2A_LL_DMS())
  end

  --- Is LL DDM
  -- @param #SETTINGS self
  -- @return #boolean true if LL DDM
  function SETTINGS:IsA2A_LL_DDM()
    return (self.A2ASystem and self.A2ASystem == "LL DDM") or (not self.A2ASystem and _SETTINGS:IsA2A_LL_DDM())
  end

  --- Sets A2A MGRS
  -- @param #SETTINGS self
  -- @return #SETTINGS
  function SETTINGS:SetA2A_MGRS()
    self.A2ASystem = "MGRS"
  end

  --- Is MGRS
  -- @param #SETTINGS self
  -- @return #boolean true if MGRS
  function SETTINGS:IsA2A_MGRS()
    return (self.A2ASystem and self.A2ASystem == "MGRS") or (not self.A2ASystem and _SETTINGS:IsA2A_MGRS())
  end

  --- @param #SETTINGS self
  -- @param Wrapper.Group#GROUP MenuGroup Group for which to add menus.
  -- @param #table RootMenu Root menu table
  -- @return #SETTINGS
  function SETTINGS:SetSystemMenu( MenuGroup, RootMenu )

    local MenuText = "System Settings"

    local MenuTime = timer.getTime()

    local SettingsMenu = MENU_GROUP:New( MenuGroup, MenuText, RootMenu ):SetTime( MenuTime )

    -------
    -- A2G Coordinate System
    -------

    local text = "A2G Coordinate System"
    if _SETTINGS.MenuShort then
      text = "A2G Coordinates"
    end
    local A2GCoordinateMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime )

    -- Set LL DMS
    if not self:IsA2G_LL_DMS() then
      local text = "Lat/Lon Degree Min Sec (LL DMS)"
      if _SETTINGS.MenuShort then
        text = "LL DMS"
      end
      MENU_GROUP_COMMAND:New( MenuGroup, text, A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime )
    end

    -- Set LL DDM
    if not self:IsA2G_LL_DDM() then
      local text = "Lat/Lon Degree Dec Min (LL DDM)"
      if _SETTINGS.MenuShort then
        text = "LL DDM"
      end
      MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime )
    end

    -- Set LL DMS accuracy.
    if self:IsA2G_LL_DDM() then
      local text1 = "LL DDM Accuracy 1"
      local text2 = "LL DDM Accuracy 2"
      local text3 = "LL DDM Accuracy 3"
      if _SETTINGS.MenuShort then
        text1 = "LL DDM"
      end
      MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 3", A2GCoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime )
    end

    -- Set BR.
    if not self:IsA2G_BR() then
      local text = "Bearing, Range (BR)"
      if _SETTINGS.MenuShort then
        text = "BR"
      end
      MENU_GROUP_COMMAND:New( MenuGroup, text, A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "BR" ):SetTime( MenuTime )
    end

    -- Set MGRS.
    if not self:IsA2G_MGRS() then
      local text = "Military Grid (MGRS)"
      if _SETTINGS.MenuShort then
        text = "MGRS"
      end
      MENU_GROUP_COMMAND:New( MenuGroup, text, A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime )
    end

    -- Set MGRS accuracy.
    if self:IsA2G_MGRS() then
      MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 1", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 2", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 3", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 4", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 4 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 5", A2GCoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 5 ):SetTime( MenuTime )
    end

    -------
    -- A2A Coordinate System
    -------

    local text = "A2A Coordinate System"
    if _SETTINGS.MenuShort then
      text = "A2A Coordinates"
    end
    local A2ACoordinateMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime )

    if not self:IsA2A_LL_DMS() then
      local text = "Lat/Lon Degree Min Sec (LL DMS)"
      if _SETTINGS.MenuShort then
        text = "LL DMS"
      end
      MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime )
    end

    if not self:IsA2A_LL_DDM() then
      local text = "Lat/Lon Degree Dec Min (LL DDM)"
      if _SETTINGS.MenuShort then
        text = "LL DDM"
      end
      MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime )
    end

    if self:IsA2A_LL_DDM() or self:IsA2A_LL_DMS() then
      MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 0", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 0 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 1", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 2", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "LL Accuracy 3", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime )
    end

    if not self:IsA2A_BULLS() then
      local text = "Bullseye (BULLS)"
      if _SETTINGS.MenuShort then
        text = "Bulls"
      end
      MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BULLS" ):SetTime( MenuTime )
    end

    if not self:IsA2A_BRAA() then
      local text = "Bearing Range Altitude Aspect (BRAA)"
      if _SETTINGS.MenuShort then
        text = "BRAA"
      end
      MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BRAA" ):SetTime( MenuTime )
    end

    if not self:IsA2A_MGRS() then
      local text = "Military Grid (MGRS)"
      if _SETTINGS.MenuShort then
        text = "MGRS"
      end
      MENU_GROUP_COMMAND:New( MenuGroup, text, A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime )
    end

    if self:IsA2A_MGRS() then
      MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 1", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 2", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 3", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 4", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 4 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "MGRS Accuracy 5", A2ACoordinateMenu, self.MenuMGRS_Accuracy, self, MenuGroup, RootMenu, 5 ):SetTime( MenuTime )
    end

    local text = "Measures and Weights System"
    if _SETTINGS.MenuShort then
      text = "Unit System"
    end
    local MetricsMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime )

    if self:IsMetric() then
      local text = "Imperial (Miles,Feet)"
      if _SETTINGS.MenuShort then
        text = "Imperial"
      end
      MENU_GROUP_COMMAND:New( MenuGroup, text, MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, false ):SetTime( MenuTime )
    end

    if self:IsImperial() then
      local text = "Metric (Kilometers,Meters)"
      if _SETTINGS.MenuShort then
        text = "Metric"
      end
      MENU_GROUP_COMMAND:New( MenuGroup, text, MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, true ):SetTime( MenuTime )
    end

    local text = "Messages and Reports"
    if _SETTINGS.MenuShort then
      text = "Messages & Reports"
    end
    local MessagesMenu = MENU_GROUP:New( MenuGroup, text, SettingsMenu ):SetTime( MenuTime )

    local UpdateMessagesMenu = MENU_GROUP:New( MenuGroup, "Update Messages", MessagesMenu ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "Off", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 0 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "5 seconds", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 5 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "10 seconds", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 10 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 15 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 30 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", UpdateMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Update, 60 ):SetTime( MenuTime )

    local InformationMessagesMenu = MENU_GROUP:New( MenuGroup, "Information Messages", MessagesMenu ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "5 seconds", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 5 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "10 seconds", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 10 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 15 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 30 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 60 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", InformationMessagesMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Information, 120 ):SetTime( MenuTime )

    local BriefingReportsMenu = MENU_GROUP:New( MenuGroup, "Briefing Reports", MessagesMenu ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 15 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 30 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 60 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 120 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "3 minutes", BriefingReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Briefing, 180 ):SetTime( MenuTime )

    local OverviewReportsMenu = MENU_GROUP:New( MenuGroup, "Overview Reports", MessagesMenu ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 15 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 30 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 60 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 120 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "3 minutes", OverviewReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.Overview, 180 ):SetTime( MenuTime )

    local DetailedReportsMenu = MENU_GROUP:New( MenuGroup, "Detailed Reports", MessagesMenu ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "15 seconds", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 15 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "30 seconds", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 30 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "1 minute", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 60 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "2 minutes", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 120 ):SetTime( MenuTime )
    MENU_GROUP_COMMAND:New( MenuGroup, "3 minutes", DetailedReportsMenu, self.MenuMessageTimingsSystem, self, MenuGroup, RootMenu, MESSAGE.Type.DetailedReportsMenu, 180 ):SetTime( MenuTime )

    SettingsMenu:Remove( MenuTime )

    return self
  end

  --- Sets the player menus on, so that the **Player setting menus** show up for the players.
  -- But only when a player exits and reenters the slot these settings will have effect!
  -- It is advised to use this method at the start of the mission.
  -- @param #SETTINGS self
  -- @return #SETTINGS
  -- @usage
  --   _SETTINGS:SetPlayerMenuOn() -- will enable the player menus.
  function SETTINGS:SetPlayerMenuOn()
    self.ShowPlayerMenu = true
  end

  --- Sets the player menus off, so that the **Player setting menus** won't show up for the players.
  -- But only when a player exits and reenters the slot these settings will have effect!
  -- It is advised to use this method at the start of the mission.
  -- @param #SETTINGS self
  -- @return #SETTINGS self
  -- @usage
  --   _SETTINGS:SetPlayerMenuOff() -- will disable the player menus.
  function SETTINGS:SetPlayerMenuOff()
    self.ShowPlayerMenu = false
  end

  --- Updates the menu of the player seated in the PlayerUnit.
  -- @param #SETTINGS self
  -- @param Wrapper.Client#CLIENT PlayerUnit
  -- @return #SETTINGS self
  function SETTINGS:SetPlayerMenu( PlayerUnit )

    if _SETTINGS.ShowPlayerMenu == true then

      local PlayerGroup = PlayerUnit:GetGroup()
      local PlayerName = PlayerUnit:GetPlayerName() or "None"
      --local PlayerNames = PlayerGroup:GetPlayerNames()

      local PlayerMenu = MENU_GROUP:New( PlayerGroup, 'Settings "' .. PlayerName .. '"' )

      self.PlayerMenu = PlayerMenu

      self:T( string.format( "Setting menu for player %s", tostring( PlayerName ) ) )

      local submenu = MENU_GROUP:New( PlayerGroup, "LL Accuracy", PlayerMenu )
      MENU_GROUP_COMMAND:New( PlayerGroup, "LL 0 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 0 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "LL 1 Decimal", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "LL 2 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "LL 3 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "LL 4 Decimals", submenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 )

      local submenu = MENU_GROUP:New( PlayerGroup, "MGRS Accuracy", PlayerMenu )
      MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 0", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 0 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 1", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 2", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 3", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 4", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "MRGS Accuracy 5", submenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 5 )

      ------
      -- A2G Coordinate System
      ------

      local text = "A2G Coordinate System"
      if _SETTINGS.MenuShort then
        text = "A2G Coordinates"
      end
      local A2GCoordinateMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu )

      if not self:IsA2G_LL_DMS() or _SETTINGS.MenuStatic then
        local text = "Lat/Lon Degree Min Sec (LL DMS)"
        if _SETTINGS.MenuShort then
          text = "A2G LL DMS"
        end
        MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" )
      end

      if not self:IsA2G_LL_DDM() or _SETTINGS.MenuStatic then
        local text = "Lat/Lon Degree Dec Min (LL DDM)"
        if _SETTINGS.MenuShort then
          text = "A2G LL DDM"
        end
        MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" )
      end

      if not self:IsA2G_BR() or _SETTINGS.MenuStatic then
        local text = "Bearing, Range (BR)"
        if _SETTINGS.MenuShort then
          text = "A2G BR"
        end
        MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "BR" )
      end

      if not self:IsA2G_MGRS() or _SETTINGS.MenuStatic then
        local text = "Military Grid (MGRS)"
        if _SETTINGS.MenuShort then
          text = "A2G MGRS"
        end
        MENU_GROUP_COMMAND:New( PlayerGroup, text, A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" )
      end

      ------
      -- A2A Coordinates Menu
      ------

      local text = "A2A Coordinate System"
      if _SETTINGS.MenuShort then
        text = "A2A Coordinates"
      end
      local A2ACoordinateMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu )

      if not self:IsA2A_LL_DMS() or _SETTINGS.MenuStatic then
        local text = "Lat/Lon Degree Min Sec (LL DMS)"
        if _SETTINGS.MenuShort then
          text = "A2A LL DMS"
        end
        MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" )
      end

      if not self:IsA2A_LL_DDM() or _SETTINGS.MenuStatic then
        local text = "Lat/Lon Degree Dec Min (LL DDM)"
        if _SETTINGS.MenuShort then
          text = "A2A LL DDM"
        end
        MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" )
      end

      if not self:IsA2A_BULLS() or _SETTINGS.MenuStatic then
        local text = "Bullseye (BULLS)"
        if _SETTINGS.MenuShort then
          text = "A2A BULLS"
        end
        MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BULLS" )
      end

      if not self:IsA2A_BRAA() or _SETTINGS.MenuStatic then
        local text = "Bearing Range Altitude Aspect (BRAA)"
        if _SETTINGS.MenuShort then
          text = "A2A BRAA"
        end
        MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BRAA" )
      end

      if not self:IsA2A_MGRS() or _SETTINGS.MenuStatic then
        local text = "Military Grid (MGRS)"
        if _SETTINGS.MenuShort then
          text = "A2A MGRS"
        end
        MENU_GROUP_COMMAND:New( PlayerGroup, text, A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" )
      end

      ---
      -- Unit system
      ---

      local text = "Measures and Weights System"
      if _SETTINGS.MenuShort then
        text = "Unit System"
      end
      local MetricsMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu )

      if self:IsMetric() or _SETTINGS.MenuStatic then
        local text = "Imperial (Miles,Feet)"
        if _SETTINGS.MenuShort then
          text = "Imperial"
        end
        MENU_GROUP_COMMAND:New( PlayerGroup, text, MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, false )
      end

      if self:IsImperial() or _SETTINGS.MenuStatic then
        local text = "Metric (Kilometers,Meters)"
        if _SETTINGS.MenuShort then
          text = "Metric"
        end
        MENU_GROUP_COMMAND:New( PlayerGroup, text, MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, true )
      end

      ---
      -- Messages and Reports
      ---

      local text = "Messages and Reports"
      if _SETTINGS.MenuShort then
        text = "Messages & Reports"
      end
      local MessagesMenu = MENU_GROUP:New( PlayerGroup, text, PlayerMenu )

      local UpdateMessagesMenu = MENU_GROUP:New( PlayerGroup, "Update Messages", MessagesMenu )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Updates Off", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 0 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 5 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 5 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 10 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 10 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 15 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 15 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 30 sec", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 30 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Updates 1 min", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 60 )

      local InformationMessagesMenu = MENU_GROUP:New( PlayerGroup, "Info Messages", MessagesMenu )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Info 5 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 5 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Info 10 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 10 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Info 15 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 15 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Info 30 sec", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 30 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Info 1 min", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 60 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Info 2 min", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 120 )

      local BriefingReportsMenu = MENU_GROUP:New( PlayerGroup, "Briefing Reports", MessagesMenu )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 15 sec", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 15 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 30 sec", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 30 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 1 min", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 60 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 2 min", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 120 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Brief 3 min", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 180 )

      local OverviewReportsMenu = MENU_GROUP:New( PlayerGroup, "Overview Reports", MessagesMenu )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 15 sec", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 15 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 30 sec", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 30 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 1 min", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 60 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 2 min", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 120 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Overview 3 min", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 180 )

      local DetailedReportsMenu = MENU_GROUP:New( PlayerGroup, "Detailed Reports", MessagesMenu )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 15 sec", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 15 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 30 sec", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 30 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 1 min", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 60 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 2 min", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 120 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Detailed 3 min", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 180 )

    end

    return self
  end

  --- Removes the player menu from the PlayerUnit.
  -- @param #SETTINGS self
  -- @param Wrapper.Client#CLIENT PlayerUnit
  -- @return #SETTINGS self
  function SETTINGS:RemovePlayerMenu( PlayerUnit )

    if self.PlayerMenu then
      self.PlayerMenu:Remove()
      self.PlayerMenu = nil
    end

    return self
  end

  --- @param #SETTINGS self
  function SETTINGS:A2GMenuSystem( MenuGroup, RootMenu, A2GSystem )
    self.A2GSystem = A2GSystem
    MESSAGE:New( string.format( "Settings: Default A2G coordinate system set to %s for all players!", A2GSystem ), 5 ):ToAll()
    self:SetSystemMenu( MenuGroup, RootMenu )
  end

  --- @param #SETTINGS self
  function SETTINGS:A2AMenuSystem( MenuGroup, RootMenu, A2ASystem )
    self.A2ASystem = A2ASystem
    MESSAGE:New( string.format( "Settings: Default A2A coordinate system set to %s for all players!", A2ASystem ), 5 ):ToAll()
    self:SetSystemMenu( MenuGroup, RootMenu )
  end

  --- @param #SETTINGS self
  function SETTINGS:MenuLL_DDM_Accuracy( MenuGroup, RootMenu, LL_Accuracy )
    self.LL_Accuracy = LL_Accuracy
    MESSAGE:New( string.format( "Settings: Default LL accuracy set to %s for all players!", LL_Accuracy ), 5 ):ToAll()
    self:SetSystemMenu( MenuGroup, RootMenu )
  end

  --- @param #SETTINGS self
  function SETTINGS:MenuMGRS_Accuracy( MenuGroup, RootMenu, MGRS_Accuracy )
    self.MGRS_Accuracy = MGRS_Accuracy
    MESSAGE:New( string.format( "Settings: Default MGRS accuracy set to %s for all players!", MGRS_Accuracy ), 5 ):ToAll()
    self:SetSystemMenu( MenuGroup, RootMenu )
  end

  --- @param #SETTINGS self
  function SETTINGS:MenuMWSystem( MenuGroup, RootMenu, MW )
    self.Metric = MW
    MESSAGE:New( string.format( "Settings: Default measurement format set to %s for all players!", MW and "Metric" or "Imperial" ), 5 ):ToAll()
    self:SetSystemMenu( MenuGroup, RootMenu )
  end

  --- @param #SETTINGS self
  function SETTINGS:MenuMessageTimingsSystem( MenuGroup, RootMenu, MessageType, MessageTime )
    self:SetMessageTime( MessageType, MessageTime )
    MESSAGE:New( string.format( "Settings: Default message time set for %s to %d.", MessageType, MessageTime ), 5 ):ToAll()
  end

  do
    --- @param #SETTINGS self
    function SETTINGS:MenuGroupA2GSystem( PlayerUnit, PlayerGroup, PlayerName, A2GSystem )
      --BASE:E( {PlayerUnit:GetName(), A2GSystem } )
      self.A2GSystem = A2GSystem
      MESSAGE:New( string.format( "Settings: A2G format set to %s for player %s.", A2GSystem, PlayerName ), 5 ):ToGroup( PlayerGroup )
      if _SETTINGS.MenuStatic == false then
        self:RemovePlayerMenu( PlayerUnit )
        self:SetPlayerMenu( PlayerUnit )
      end
    end

    --- @param #SETTINGS self
    function SETTINGS:MenuGroupA2ASystem( PlayerUnit, PlayerGroup, PlayerName, A2ASystem )
      self.A2ASystem = A2ASystem
      MESSAGE:New( string.format( "Settings: A2A format set to %s for player %s.", A2ASystem, PlayerName ), 5 ):ToGroup( PlayerGroup )
      if _SETTINGS.MenuStatic == false then
        self:RemovePlayerMenu( PlayerUnit )
        self:SetPlayerMenu( PlayerUnit )
      end
    end

    --- @param #SETTINGS self
    function SETTINGS:MenuGroupLL_DDM_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, LL_Accuracy )
      self.LL_Accuracy = LL_Accuracy
      MESSAGE:New( string.format( "Settings: LL format accuracy set to %d decimal places for player %s.", LL_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup )
      if _SETTINGS.MenuStatic == false then
        self:RemovePlayerMenu( PlayerUnit )
        self:SetPlayerMenu( PlayerUnit )
      end
    end

    --- @param #SETTINGS self
    function SETTINGS:MenuGroupMGRS_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, MGRS_Accuracy )
      self.MGRS_Accuracy = MGRS_Accuracy
      MESSAGE:New( string.format( "Settings: MGRS format accuracy set to %d for player %s.", MGRS_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup )
      if _SETTINGS.MenuStatic == false then
        self:RemovePlayerMenu( PlayerUnit )
        self:SetPlayerMenu( PlayerUnit )
      end
    end

    --- @param #SETTINGS self
    function SETTINGS:MenuGroupMWSystem( PlayerUnit, PlayerGroup, PlayerName, MW )
      self.Metric = MW
      MESSAGE:New( string.format( "Settings: Measurement format set to %s for player %s.", MW and "Metric" or "Imperial", PlayerName ), 5 ):ToGroup( PlayerGroup )
      if _SETTINGS.MenuStatic == false then
        self:RemovePlayerMenu( PlayerUnit )
        self:SetPlayerMenu( PlayerUnit )
      end
    end

    --- @param #SETTINGS self
    function SETTINGS:MenuGroupMessageTimingsSystem( PlayerUnit, PlayerGroup, PlayerName, MessageType, MessageTime )
      self:SetMessageTime( MessageType, MessageTime )
      MESSAGE:New( string.format( "Settings: Default message time set for %s to %d.", MessageType, MessageTime ), 5 ):ToGroup( PlayerGroup )
    end

  end

  --- Configures the era of the mission to be WWII.
  -- @param #SETTINGS self
  -- @return #SETTINGS self
  function SETTINGS:SetEraWWII()

    self.Era = SETTINGS.__Enum.Era.WWII

  end

  --- Configures the era of the mission to be Korea.
  -- @param #SETTINGS self
  -- @return #SETTINGS self
  function SETTINGS:SetEraKorea()

    self.Era = SETTINGS.__Enum.Era.Korea

  end

  --- Configures the era of the mission to be Cold war.
  -- @param #SETTINGS self
  -- @return #SETTINGS self
  function SETTINGS:SetEraCold()

    self.Era = SETTINGS.__Enum.Era.Cold

  end

  --- Configures the era of the mission to be Modern war.
  -- @param #SETTINGS self
  -- @return #SETTINGS self
  function SETTINGS:SetEraModern()

    self.Era = SETTINGS.__Enum.Era.Modern

  end

end
--- **Core** - Manage hierarchical menu structures and commands for players within a mission.
--
-- ===
--
-- ## Features:
--
--   * Setup mission sub menus.
--   * Setup mission command menus.
--   * Setup coalition sub menus.
--   * Setup coalition command menus.
--   * Setup group sub menus.
--   * Setup group command menus.
--   * Manage menu creation intelligently, avoid double menu creation.
--   * Only create or delete menus when required, and keep existing menus persistent.
--   * Update menu structures.
--   * Refresh menu structures intelligently, based on a time stamp of updates.
--     - Delete obsolete menus.
--     - Create new one where required.
--     - Don't touch the existing ones.
--   * Provide a variable amount of parameters to menus.
--   * Update the parameters and the receiving methods, without updating the menu within DCS!
--   * Provide a great performance boost in menu management.
--   * Provide a great tool to manage menus in your code.
--
-- DCS Menus can be managed using the MENU classes.
-- The advantage of using MENU classes is that it hides the complexity of dealing with menu management in more advanced scenarios where you need to
-- set menus and later remove them, and later set them again. You'll find while using use normal DCS scripting functions, that setting and removing
-- menus is not a easy feat if you have complex menu hierarchies defined.
-- Using the MOOSE menu classes, the removal and refreshing of menus are nicely being handled within these classes, and becomes much more easy.
-- On top, MOOSE implements **variable parameter** passing for command menus.
--
-- There are basically two different MENU class types that you need to use:
--
-- ### To manage **main menus**, the classes begin with **MENU_**:
--
--   * @{Core.Menu#MENU_MISSION}: Manages main menus for whole mission file.
--   * @{Core.Menu#MENU_COALITION}: Manages main menus for whole coalition.
--   * @{Core.Menu#MENU_GROUP}: Manages main menus for GROUPs.
--
-- ### To manage **command menus**, which are menus that allow the player to issue **functions**, the classes begin with **MENU_COMMAND_**:
--
--   * @{Core.Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file.
--   * @{Core.Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition.
--   * @{Core.Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs.
--
-- ===
--
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/Core/Menu)
--
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions:
--
-- ===
--
-- @module Core.Menu
-- @image Core_Menu.JPG

MENU_INDEX = {}
MENU_INDEX.MenuMission = {}
MENU_INDEX.MenuMission.Menus = {}
MENU_INDEX.Coalition = {}
MENU_INDEX.Coalition[coalition.side.BLUE] = {}
MENU_INDEX.Coalition[coalition.side.BLUE].Menus = {}
MENU_INDEX.Coalition[coalition.side.RED] = {}
MENU_INDEX.Coalition[coalition.side.RED].Menus = {}
MENU_INDEX.Group = {}

function MENU_INDEX:ParentPath( ParentMenu, MenuText )
  local Path = ParentMenu and "@" .. table.concat( ParentMenu.MenuPath or {}, "@" ) or ""
  if ParentMenu then
    if ParentMenu:IsInstanceOf( "MENU_GROUP" ) or ParentMenu:IsInstanceOf( "MENU_GROUP_COMMAND" ) then
      local GroupName = ParentMenu.Group:GetName()
      if not self.Group[GroupName].Menus[Path] then
        BASE:E( { Path = Path, GroupName = GroupName } )
        error( "Parent path not found in menu index for group menu" )
        return nil
      end
    elseif ParentMenu:IsInstanceOf( "MENU_COALITION" ) or ParentMenu:IsInstanceOf( "MENU_COALITION_COMMAND" ) then
      local Coalition = ParentMenu.Coalition
      if not self.Coalition[Coalition].Menus[Path] then
        BASE:E( { Path = Path, Coalition = Coalition } )
        error( "Parent path not found in menu index for coalition menu" )
        return nil
      end
    elseif ParentMenu:IsInstanceOf( "MENU_MISSION" ) or ParentMenu:IsInstanceOf( "MENU_MISSION_COMMAND" ) then
      if not self.MenuMission.Menus[Path] then
        BASE:E( { Path = Path } )
        error( "Parent path not found in menu index for mission menu" )
        return nil
      end
    end
  end

  Path = Path .. "@" .. MenuText
  return Path
end

function MENU_INDEX:PrepareMission()
    self.MenuMission.Menus = self.MenuMission.Menus or {}
end

function MENU_INDEX:PrepareCoalition( CoalitionSide )
    self.Coalition[CoalitionSide] = self.Coalition[CoalitionSide] or {}
    self.Coalition[CoalitionSide].Menus = self.Coalition[CoalitionSide].Menus or {}
end

---
-- @param Wrapper.Group#GROUP Group
function MENU_INDEX:PrepareGroup( Group )
  if Group and Group:IsAlive() ~= nil  then -- something was changed here!
    local GroupName = Group:GetName()
    self.Group[GroupName] = self.Group[GroupName] or {}
    self.Group[GroupName].Menus = self.Group[GroupName].Menus or {}
  end
end

function MENU_INDEX:HasMissionMenu( Path )
  return self.MenuMission.Menus[Path]
end

function MENU_INDEX:SetMissionMenu( Path, Menu )
  self.MenuMission.Menus[Path] = Menu
end

function MENU_INDEX:ClearMissionMenu( Path )
  self.MenuMission.Menus[Path] = nil
end

function MENU_INDEX:HasCoalitionMenu( Coalition, Path )
  return self.Coalition[Coalition].Menus[Path]
end

function MENU_INDEX:SetCoalitionMenu( Coalition, Path, Menu )
  self.Coalition[Coalition].Menus[Path] = Menu
end

function MENU_INDEX:ClearCoalitionMenu( Coalition, Path )
  self.Coalition[Coalition].Menus[Path] = nil
end

function MENU_INDEX:HasGroupMenu( Group, Path )
  if Group and Group:IsAlive() then
    local MenuGroupName = Group:GetName()
    if self.Group[MenuGroupName] and self.Group[MenuGroupName].Menus and self.Group[MenuGroupName].Menus[Path] then
      return self.Group[MenuGroupName].Menus[Path]
    end
  end
  return nil
end

function MENU_INDEX:SetGroupMenu( Group, Path, Menu )
  local MenuGroupName = Group:GetName()
  --Group:F({MenuGroupName=MenuGroupName,Path=Path})
  self.Group[MenuGroupName].Menus[Path] = Menu
end

function MENU_INDEX:ClearGroupMenu( Group, Path )
  local MenuGroupName = Group:GetName()
  self.Group[MenuGroupName].Menus[Path] = nil
end

function MENU_INDEX:Refresh( Group )
    for MenuID, Menu in pairs( self.MenuMission.Menus ) do
      Menu:Refresh()
    end
    for MenuID, Menu in pairs( self.Coalition[coalition.side.BLUE].Menus ) do
      Menu:Refresh()
    end
    for MenuID, Menu in pairs( self.Coalition[coalition.side.RED].Menus ) do
      Menu:Refresh()
    end
    local GroupName = Group:GetName()
    for MenuID, Menu in pairs( self.Group[GroupName].Menus ) do
      Menu:Refresh()
    end

  return self
end

do -- MENU_BASE
    ---
  -- @type MENU_BASE
  -- @extends Core.Base#BASE
  --- Defines the main MENU class where other MENU classes are derived from.
  -- This is an abstract class, so don't use it.
  -- @field #MENU_BASE
  MENU_BASE = {
    ClassName = "MENU_BASE",
    MenuPath = nil,
    MenuText = "",
    MenuParentPath = nil,
  }

  --- Constructor
  -- @param #MENU_BASE
  -- @return #MENU_BASE
  function MENU_BASE:New( MenuText, ParentMenu )

    local MenuParentPath = {}
    if ParentMenu ~= nil then
      MenuParentPath = ParentMenu.MenuPath
    end
    local self = BASE:Inherit( self, BASE:New() )

    self.MenuPath = nil
    self.MenuText = MenuText
    self.ParentMenu = ParentMenu
    self.MenuParentPath = MenuParentPath
    self.Path = ( self.ParentMenu and "@" .. table.concat( self.MenuParentPath or {}, "@" ) or "" ) .. "@" .. self.MenuText
    self.Menus = {}
    self.MenuCount = 0
    self.MenuStamp = timer.getTime()
    self.MenuRemoveParent = false

    if self.ParentMenu then
      self.ParentMenu.Menus = self.ParentMenu.Menus or {}
      self.ParentMenu.Menus[MenuText] = self
    end

    return self
  end
  
  function MENU_BASE:SetParentMenu( MenuText, Menu )
    if self.ParentMenu then
      self.ParentMenu.Menus = self.ParentMenu.Menus or {}
      self.ParentMenu.Menus[MenuText] = Menu
      self.ParentMenu.MenuCount = self.ParentMenu.MenuCount + 1
    end
  end
  function MENU_BASE:ClearParentMenu( MenuText )
    if self.ParentMenu and self.ParentMenu.Menus[MenuText] then
      self.ParentMenu.Menus[MenuText] = nil
      self.ParentMenu.MenuCount = self.ParentMenu.MenuCount - 1
      if self.ParentMenu.MenuCount == 0 then
        --self.ParentMenu:Remove()
      end
    end
  end
  --- Sets a @{Menu} to remove automatically the parent menu when the menu removed is the last child menu of that parent @{Menu}.
  -- @param #MENU_BASE self
  -- @param #boolean RemoveParent If true, the parent menu is automatically removed when this menu is the last child menu of that parent @{Menu}.
  -- @return #MENU_BASE
  function MENU_BASE:SetRemoveParent( RemoveParent )
    --self:F( { RemoveParent } )
    self.MenuRemoveParent = RemoveParent
    return self
  end

  --- Gets a @{Menu} from a parent @{Menu}
  -- @param #MENU_BASE self
  -- @param #string MenuText The text of the child menu.
  -- @return #MENU_BASE
  function MENU_BASE:GetMenu( MenuText )
    return self.Menus[MenuText]
  end

  --- Sets a menu stamp for later prevention of menu removal.
  -- @param #MENU_BASE self
  -- @param MenuStamp
  -- @return #MENU_BASE
  function MENU_BASE:SetStamp( MenuStamp )
    self.MenuStamp = MenuStamp
    return self
  end


  --- Gets a menu stamp for later prevention of menu removal.
  -- @param #MENU_BASE self
  -- @return MenuStamp
  function MENU_BASE:GetStamp()
    return timer.getTime()
  end


  --- Sets a time stamp for later prevention of menu removal.
  -- @param #MENU_BASE self
  -- @param MenuStamp
  -- @return #MENU_BASE
  function MENU_BASE:SetTime( MenuStamp )
    self.MenuStamp = MenuStamp
    return self
  end

  --- Sets a tag for later selection of menu refresh.
  -- @param #MENU_BASE self
  -- @param #string MenuTag A Tag or Key that will filter only menu items set with this key.
  -- @return #MENU_BASE
  function MENU_BASE:SetTag( MenuTag )
    self.MenuTag = MenuTag
    return self
  end

end
do 
  ---
  -- MENU_COMMAND_BASE
  -- @type MENU_COMMAND_BASE
  -- @field #function MenuCallHandler
  -- @extends Core.Menu#MENU_BASE

  --- Defines the main MENU class where other MENU COMMAND_
  -- classes are derived from, in order to set commands.
  --
  -- @field #MENU_COMMAND_BASE
  MENU_COMMAND_BASE = {
    ClassName = "MENU_COMMAND_BASE",
    CommandMenuFunction = nil,
    CommandMenuArgument = nil,
    MenuCallHandler = nil,
  }

  --- Constructor
  -- @param #MENU_COMMAND_BASE
  -- @return #MENU_COMMAND_BASE
  function MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArguments )

    local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) -- #MENU_COMMAND_BASE
    -- When a menu function goes into error, DCS displays an obscure menu message.
    -- This error handler catches the menu error and displays the full call stack.
    local ErrorHandler = function( errmsg )
      env.info( "MOOSE error in MENU COMMAND function: " .. errmsg )
      if BASE.Debug ~= nil then
        env.info( BASE.Debug.traceback() )
      end
      return errmsg
    end

    self:SetCommandMenuFunction( CommandMenuFunction )
    self:SetCommandMenuArguments( CommandMenuArguments )
    self.MenuCallHandler = function()
      local function MenuFunction()
        return self.CommandMenuFunction( unpack( self.CommandMenuArguments ) )
      end
      local Status, Result = xpcall( MenuFunction, ErrorHandler )
    end

    return self
  end

  --- This sets the new command function of a menu,
  -- so that if a menu is regenerated, or if command function changes,
  -- that the function set for the menu is loosely coupled with the menu itself!!!
  -- If the function changes, no new menu needs to be generated if the menu text is the same!!!
  -- @param #MENU_COMMAND_BASE
  -- @return #MENU_COMMAND_BASE
  function MENU_COMMAND_BASE:SetCommandMenuFunction( CommandMenuFunction )
    self.CommandMenuFunction = CommandMenuFunction
    return self
  end
  --- This sets the new command arguments of a menu,
  -- so that if a menu is regenerated, or if command arguments change,
  -- that the arguments set for the menu are loosely coupled with the menu itself!!!
  -- If the arguments change, no new menu needs to be generated if the menu text is the same!!!
  -- @param #MENU_COMMAND_BASE
  -- @return #MENU_COMMAND_BASE
  function MENU_COMMAND_BASE:SetCommandMenuArguments( CommandMenuArguments )
    self.CommandMenuArguments = CommandMenuArguments
    return self
  end
end

do 
  ---
  -- MENU_MISSION
  -- @type MENU_MISSION
  -- @extends Core.Menu#MENU_BASE
  --- Manages the main menus for a complete mission.
  --
  -- You can add menus with the @{#MENU_MISSION.New} method, which constructs a MENU_MISSION object and returns you the object reference.
  -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION.Remove}.
  -- @field #MENU_MISSION
  MENU_MISSION = {
    ClassName = "MENU_MISSION",
  }

  --- MENU_MISSION constructor. Creates a new MENU_MISSION object and creates the menu for a complete mission file.
  -- @param #MENU_MISSION self
  -- @param #string MenuText The text for the menu.
  -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the parent menu of DCS world (under F10 other).
  -- @return #MENU_MISSION
  function MENU_MISSION:New( MenuText, ParentMenu )

    MENU_INDEX:PrepareMission()
    local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
    local MissionMenu = MENU_INDEX:HasMissionMenu( Path )
    if MissionMenu then
      return MissionMenu
    else
      local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) )
      MENU_INDEX:SetMissionMenu( Path, self )

      self.MenuPath = missionCommands.addSubMenu( self.MenuText, self.MenuParentPath )
      self:SetParentMenu( self.MenuText, self )
      return self
    end

  end

  --- Refreshes a radio item for a mission
  -- @param #MENU_MISSION self
  -- @return #MENU_MISSION
  function MENU_MISSION:Refresh()
    do
      missionCommands.removeItem( self.MenuPath )
      self.MenuPath = missionCommands.addSubMenu( self.MenuText, self.MenuParentPath )
    end
    return self
  end

  --- Removes the sub menus recursively of this MENU_MISSION. Note that the main menu is kept!
  -- @param #MENU_MISSION self
  -- @return #MENU_MISSION
  function MENU_MISSION:RemoveSubMenus()

    for MenuID, Menu in pairs( self.Menus or {} ) do
      Menu:Remove()
    end

    self.Menus = nil

  end

  --- Removes the main menu and the sub menus recursively of this MENU_MISSION.
  -- @param #MENU_MISSION self
  -- @return #nil
  function MENU_MISSION:Remove( MenuStamp, MenuTag )

    MENU_INDEX:PrepareMission()
    local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
    local MissionMenu = MENU_INDEX:HasMissionMenu( Path )
    if MissionMenu == self then
      self:RemoveSubMenus()
      if not MenuStamp or self.MenuStamp ~= MenuStamp then
        if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then
          self:F( { Text = self.MenuText, Path = self.MenuPath } )
          if self.MenuPath ~= nil then
            missionCommands.removeItem( self.MenuPath )
          end
          MENU_INDEX:ClearMissionMenu( self.Path )
          self:ClearParentMenu( self.MenuText )
          return nil
        end
      end
    else
      BASE:E( { "Cannot Remove MENU_MISSION", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText } )
    end

    return self
  end

end
do -- MENU_MISSION_COMMAND

  --- @type MENU_MISSION_COMMAND
  -- @extends Core.Menu#MENU_COMMAND_BASE

  --- Manages the command menus for a complete mission, which allow players to execute functions during mission execution.
  --
  -- You can add menus with the @{#MENU_MISSION_COMMAND.New} method, which constructs a MENU_MISSION_COMMAND object and returns you the object reference.
  -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION_COMMAND.Remove}.
  --
  -- @field #MENU_MISSION_COMMAND
  MENU_MISSION_COMMAND = {
    ClassName = "MENU_MISSION_COMMAND",
  }

  --- MENU_MISSION constructor. Creates a new radio command item for a complete mission file, which can invoke a function with parameters.
  -- @param #MENU_MISSION_COMMAND self
  -- @param #string MenuText The text for the menu.
  -- @param Core.Menu#MENU_MISSION ParentMenu The parent menu.
  -- @param CommandMenuFunction A function that is called when the menu key is pressed.
  -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this.
  -- @return #MENU_MISSION_COMMAND self
  function MENU_MISSION_COMMAND:New( MenuText, ParentMenu, CommandMenuFunction, ... )

    MENU_INDEX:PrepareMission()
    local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
    local MissionMenu = MENU_INDEX:HasMissionMenu( Path )
    if MissionMenu then
      MissionMenu:SetCommandMenuFunction( CommandMenuFunction )
      MissionMenu:SetCommandMenuArguments( arg )
      return MissionMenu
    else
      local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) )
      MENU_INDEX:SetMissionMenu( Path, self )

      self.MenuPath = missionCommands.addCommand( MenuText, self.MenuParentPath, self.MenuCallHandler )
      self:SetParentMenu( self.MenuText, self )
      return self
    end
  end
  --- Refreshes a radio item for a mission
  -- @param #MENU_MISSION_COMMAND self
  -- @return #MENU_MISSION_COMMAND
  function MENU_MISSION_COMMAND:Refresh()
    do
      missionCommands.removeItem( self.MenuPath )
      missionCommands.addCommand( self.MenuText, self.MenuParentPath, self.MenuCallHandler )
    end
    return self
  end

  --- Removes a radio command item for a coalition
  -- @param #MENU_MISSION_COMMAND self
  -- @return #nil
  function MENU_MISSION_COMMAND:Remove()

    MENU_INDEX:PrepareMission()
    local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
    local MissionMenu = MENU_INDEX:HasMissionMenu( Path )
    if MissionMenu == self then
      if not MenuStamp or self.MenuStamp ~= MenuStamp then
        if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then
          self:F( { Text = self.MenuText, Path = self.MenuPath } )
          if self.MenuPath ~= nil then
            missionCommands.removeItem( self.MenuPath )
          end
          MENU_INDEX:ClearMissionMenu( self.Path )
          self:ClearParentMenu( self.MenuText )
          return nil
        end
      end
    else
      BASE:E( { "Cannot Remove MENU_MISSION_COMMAND", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText } )
    end

    return self
  end
end
do 
  --- MENU_COALITION
  -- @type MENU_COALITION
  -- @extends Core.Menu#MENU_BASE

  --- Manages the main menus for DCS.coalition.
  --
  -- You can add menus with the @{#MENU_COALITION.New} method, which constructs a MENU_COALITION object and returns you the object reference.
  -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION.Remove}.
  --
  --
  -- @usage
  --  -- This demo creates a menu structure for the planes within the red coalition.
  --  -- To test, join the planes, then look at the other radio menus (Option F10).
  --  -- Then switch planes and check if the menu is still there.
  --
  --  local Plane1 = CLIENT:FindByName( "Plane 1" )
  --  local Plane2 = CLIENT:FindByName( "Plane 2" )
  --
  --
  --  -- This would create a menu for the red coalition under the main DCS "Others" menu.
  --  local MenuCoalitionRed = MENU_COALITION:New( coalition.side.RED, "Manage Menus" )
  --
  --
  --  local function ShowStatus( StatusText, Coalition )
  --
  --    MESSAGE:New( Coalition, 15 ):ToRed()
  --    Plane1:Message( StatusText, 15 )
  --    Plane2:Message( StatusText, 15 )
  --  end
  --
  --  local MenuStatus -- Menu#MENU_COALITION
  --  local MenuStatusShow -- Menu#MENU_COALITION_COMMAND
  --
  --  local function RemoveStatusMenu()
  --    MenuStatus:Remove()
  --  end
  --
  --  local function AddStatusMenu()
  --
  --    -- This would create a menu for the red coalition under the MenuCoalitionRed menu object.
  --    MenuStatus = MENU_COALITION:New( coalition.side.RED, "Status for Planes" )
  --    MenuStatusShow = MENU_COALITION_COMMAND:New( coalition.side.RED, "Show Status", MenuStatus, ShowStatus, "Status of planes is ok!", "Message to Red Coalition" )
  --  end
  --
  --  local MenuAdd = MENU_COALITION_COMMAND:New( coalition.side.RED, "Add Status Menu", MenuCoalitionRed, AddStatusMenu )
  --  local MenuRemove = MENU_COALITION_COMMAND:New( coalition.side.RED, "Remove Status Menu", MenuCoalitionRed, RemoveStatusMenu )
  --
  --  @field #MENU_COALITION
  MENU_COALITION = {
    ClassName = "MENU_COALITION"
  }

  --- MENU_COALITION constructor. Creates a new MENU_COALITION object and creates the menu for a complete coalition.
  -- @param #MENU_COALITION self
  -- @param DCS#coalition.side Coalition The coalition owning the menu.
  -- @param #string MenuText The text for the menu.
  -- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the parent menu of DCS world (under F10 other).
  -- @return #MENU_COALITION self
  function MENU_COALITION:New( Coalition, MenuText, ParentMenu )
    MENU_INDEX:PrepareCoalition( Coalition )
    local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
    local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( Coalition, Path )
    if CoalitionMenu then
      return CoalitionMenu
    else
      local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) )
      MENU_INDEX:SetCoalitionMenu( Coalition, Path, self )

      self.Coalition = Coalition

      self.MenuPath = missionCommands.addSubMenuForCoalition( Coalition, MenuText, self.MenuParentPath )
      self:SetParentMenu( self.MenuText, self )
      return self
    end
  end
  --- Refreshes a radio item for a coalition
  -- @param #MENU_COALITION self
  -- @return #MENU_COALITION
  function MENU_COALITION:Refresh()
    do
      missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath )
      missionCommands.addSubMenuForCoalition( self.Coalition, self.MenuText, self.MenuParentPath )
    end
    return self
  end

  --- Removes the sub menus recursively of this MENU_COALITION. Note that the main menu is kept!
  -- @param #MENU_COALITION self
  -- @return #MENU_COALITION
  function MENU_COALITION:RemoveSubMenus()

    for MenuID, Menu in pairs( self.Menus or {} ) do
      Menu:Remove()
    end

    self.Menus = nil
  end

  --- Removes the main menu and the sub menus recursively of this MENU_COALITION.
  -- @param #MENU_COALITION self
  -- @return #nil
  function MENU_COALITION:Remove( MenuStamp, MenuTag )

    MENU_INDEX:PrepareCoalition( self.Coalition )
    local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
    local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( self.Coalition, Path )
    if CoalitionMenu == self then
      self:RemoveSubMenus()
      if not MenuStamp or self.MenuStamp ~= MenuStamp then
        if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then
          self:F( { Coalition = self.Coalition, Text = self.MenuText, Path = self.MenuPath } )
          if self.MenuPath ~= nil then
            missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath )
          end
          MENU_INDEX:ClearCoalitionMenu( self.Coalition, Path )
          self:ClearParentMenu( self.MenuText )
          return nil
        end
      end
    else
      BASE:E( { "Cannot Remove MENU_COALITION", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Coalition = self.Coalition } )
    end

    return self
  end
end
do 

  --- MENU_COALITION_COMMAND
  -- @type MENU_COALITION_COMMAND
  -- @extends Core.Menu#MENU_COMMAND_BASE

  --- Manages the command menus for coalitions, which allow players to execute functions during mission execution.
  --
  -- You can add menus with the @{#MENU_COALITION_COMMAND.New} method, which constructs a MENU_COALITION_COMMAND object and returns you the object reference.
  -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION_COMMAND.Remove}.
  --
  -- @field #MENU_COALITION_COMMAND
  MENU_COALITION_COMMAND = {
    ClassName = "MENU_COALITION_COMMAND"
  }

  --- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters.
  -- @param #MENU_COALITION_COMMAND self
  -- @param DCS#coalition.side Coalition The coalition owning the menu.
  -- @param #string MenuText The text for the menu.
  -- @param Core.Menu#MENU_COALITION ParentMenu The parent menu.
  -- @param CommandMenuFunction A function that is called when the menu key is pressed.
  -- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this.
  -- @return #MENU_COALITION_COMMAND
  function MENU_COALITION_COMMAND:New( Coalition, MenuText, ParentMenu, CommandMenuFunction, ... )

    MENU_INDEX:PrepareCoalition( Coalition )
    local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
    local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( Coalition, Path )
    if CoalitionMenu then
      CoalitionMenu:SetCommandMenuFunction( CommandMenuFunction )
      CoalitionMenu:SetCommandMenuArguments( arg )
      return CoalitionMenu
    else

      local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) )
      MENU_INDEX:SetCoalitionMenu( Coalition, Path, self )

      self.Coalition = Coalition
      self.MenuPath = missionCommands.addCommandForCoalition( self.Coalition, MenuText, self.MenuParentPath, self.MenuCallHandler )
      self:SetParentMenu( self.MenuText, self )
      return self
    end
  end
  --- Refreshes a radio item for a coalition
  -- @param #MENU_COALITION_COMMAND self
  -- @return #MENU_COALITION_COMMAND
  function MENU_COALITION_COMMAND:Refresh()
    do
      missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath )
      missionCommands.addCommandForCoalition( self.Coalition, self.MenuText, self.MenuParentPath, self.MenuCallHandler )
    end

    return self
  end

  --- Removes a radio command item for a coalition
  -- @param #MENU_COALITION_COMMAND self
  -- @return #nil
  function MENU_COALITION_COMMAND:Remove( MenuStamp, MenuTag )

    MENU_INDEX:PrepareCoalition( self.Coalition )
    local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
    local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( self.Coalition, Path )
    if CoalitionMenu == self then
      if not MenuStamp or self.MenuStamp ~= MenuStamp then
        if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then
          self:F( { Coalition = self.Coalition, Text = self.MenuText, Path = self.MenuPath } )
          if self.MenuPath ~= nil then
            missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath )
          end
          MENU_INDEX:ClearCoalitionMenu( self.Coalition, Path )
          self:ClearParentMenu( self.MenuText )
          return nil
        end
      end
    else
      BASE:E( { "Cannot Remove MENU_COALITION_COMMAND", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Coalition = self.Coalition } )
    end

    return self
  end
end

--- MENU_GROUP
do
  -- This local variable is used to cache the menus registered under groups.
  -- Menus don't disappear when groups for players are destroyed and restarted.
  -- So every menu for a client created must be tracked so that program logic accidentally does not create.
  -- the same menus twice during initialization logic.
  -- These menu classes are handling this logic with this variable.
  
  local _MENUGROUPS = {}
  
  ---
  -- @type MENU_GROUP
  -- @extends Core.Menu#MENU_BASE


  --- Manages the main menus for @{Wrapper.Group}s.
  --
  -- You can add menus with the @{#MENU_GROUP.New} method, which constructs a MENU_GROUP object and returns you the object reference.
  -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP.Remove}.
  --
  -- @usage
  --  -- This demo creates a menu structure for the two groups of planes.
  --  -- Each group will receive a different menu structure.
  --  -- To test, join the planes, then look at the other radio menus (Option F10).
  --  -- Then switch planes and check if the menu is still there.
  --  -- And play with the Add and Remove menu options.
  --
  --  -- Note that in multi player, this will only work after the DCS groups bug is solved.
  --
  --  local function ShowStatus( PlaneGroup, StatusText, Coalition )
  --
  --    MESSAGE:New( Coalition, 15 ):ToRed()
  --    PlaneGroup:Message( StatusText, 15 )
  --  end
  --
  --  local MenuStatus = {}
  --
  --  local function RemoveStatusMenu( MenuGroup )
  --    local MenuGroupName = MenuGroup:GetName()
  --    MenuStatus[MenuGroupName]:Remove()
  --  end
  --
  --  -- @param Wrapper.Group#GROUP MenuGroup
  --  local function AddStatusMenu( MenuGroup )
  --    local MenuGroupName = MenuGroup:GetName()
  --    -- This would create a menu for the red coalition under the MenuCoalitionRed menu object.
  --    MenuStatus[MenuGroupName] = MENU_GROUP:New( MenuGroup, "Status for Planes" )
  --    MENU_GROUP_COMMAND:New( MenuGroup, "Show Status", MenuStatus[MenuGroupName], ShowStatus, MenuGroup, "Status of planes is ok!", "Message to Red Coalition" )
  --  end
  --
  --  SCHEDULER:New( nil,
  --    function()
  --      local PlaneGroup = GROUP:FindByName( "Plane 1" )
  --      if PlaneGroup and PlaneGroup:IsAlive() then
  --        local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" )
  --        MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 1", MenuManage, AddStatusMenu, PlaneGroup )
  --        MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 1", MenuManage, RemoveStatusMenu, PlaneGroup )
  --      end
  --    end, {}, 10, 10 )
  --
  --  SCHEDULER:New( nil,
  --    function()
  --      local PlaneGroup = GROUP:FindByName( "Plane 2" )
  --      if PlaneGroup and PlaneGroup:IsAlive() then
  --        local MenuManage = MENU_GROUP:New( PlaneGroup, "Manage Menus" )
  --        MENU_GROUP_COMMAND:New( PlaneGroup, "Add Status Menu Plane 2", MenuManage, AddStatusMenu, PlaneGroup )
  --        MENU_GROUP_COMMAND:New( PlaneGroup, "Remove Status Menu Plane 2", MenuManage, RemoveStatusMenu, PlaneGroup )
  --      end
  --    end, {}, 10, 10 )
  --
  -- @field #MENU_GROUP
  MENU_GROUP = {
    ClassName = "MENU_GROUP"
  }

  --- MENU_GROUP constructor. Creates a new radio menu item for a group.
  -- @param #MENU_GROUP self
  -- @param Wrapper.Group#GROUP Group The Group owning the menu.
  -- @param #string MenuText The text for the menu.
  -- @param #table ParentMenu The parent menu.
  -- @return #MENU_GROUP self
  function MENU_GROUP:New( Group, MenuText, ParentMenu )

    MENU_INDEX:PrepareGroup( Group )
    local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
    local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path )
    if GroupMenu then
      return GroupMenu
    else
      self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) )
      MENU_INDEX:SetGroupMenu( Group, Path, self )
      self.Group = Group
      self.GroupID = Group:GetID()
      self.MenuPath = missionCommands.addSubMenuForGroup( self.GroupID, MenuText, self.MenuParentPath )

      self:SetParentMenu( self.MenuText, self )
      return self
    end

  end

  --- Refreshes a new radio item for a group and submenus
  -- @param #MENU_GROUP self
  -- @return #MENU_GROUP
  function MENU_GROUP:Refresh()
    do
      missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
      missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath )

      for MenuText, Menu in pairs( self.Menus or {} ) do
        Menu:Refresh()
      end
    end

    return self
  end

  --- Refreshes a new radio item for a group and submenus, ordering by (numerical) MenuTag
  -- @param #MENU_GROUP self
  -- @return #MENU_GROUP
  function MENU_GROUP:RefreshAndOrderByTag()

    do
      missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
      missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath )

      local MenuTable = {}
      for MenuText, Menu in pairs( self.Menus or {} ) do
        local tag = Menu.MenuTag or math.random(1,10000)
        MenuTable[#MenuTable+1] = {Tag=tag, Enty=Menu}
      end
      table.sort(MenuTable, function (k1, k2) return k1.tag < k2.tag end )
      for _, Menu in pairs( MenuTable ) do
        Menu.Entry:Refresh()
      end
    end

    return self
  end

  --- Removes the sub menus recursively of this MENU_GROUP.
  -- @param #MENU_GROUP self
  -- @param MenuStamp
  -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set.
  -- @return #MENU_GROUP self
  function MENU_GROUP:RemoveSubMenus( MenuStamp, MenuTag )
    for MenuText, Menu in pairs( self.Menus or {} ) do
      Menu:Remove( MenuStamp, MenuTag )
    end

    self.Menus = nil

  end

  --- Removes the main menu and sub menus recursively of this MENU_GROUP.
  -- @param #MENU_GROUP self
  -- @param MenuStamp
  -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set.
  -- @return #nil
  function MENU_GROUP:Remove( MenuStamp, MenuTag )
    MENU_INDEX:PrepareGroup( self.Group )
    local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
    local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path )
    if GroupMenu == self then
      self:RemoveSubMenus( MenuStamp, MenuTag )
      if not MenuStamp or self.MenuStamp ~= MenuStamp then
        if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then
          if self.MenuPath ~= nil then
            self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } )
            missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
          end
          MENU_INDEX:ClearGroupMenu( self.Group, Path )
          self:ClearParentMenu( self.MenuText )
          return nil
        end
      end
    else
      BASE:E( { "Cannot Remove MENU_GROUP", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Group = self.Group } )
      return nil
    end

    return self
  end
  
  ---
  -- @type MENU_GROUP_COMMAND
  -- @extends Core.Menu#MENU_COMMAND_BASE

  --- The @{Core.Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution.
  -- You can add menus with the @{#MENU_GROUP_COMMAND.New} method, which constructs a MENU_GROUP_COMMAND object and returns you the object reference.
  -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND.Remove}.
  --
  -- @field #MENU_GROUP_COMMAND
  MENU_GROUP_COMMAND = {
    ClassName = "MENU_GROUP_COMMAND"
  }

  --- Creates a new radio command item for a group
  -- @param #MENU_GROUP_COMMAND self
  -- @param Wrapper.Group#GROUP Group The Group owning the menu.
  -- @param MenuText The text for the menu.
  -- @param ParentMenu The parent menu.
  -- @param CommandMenuFunction A function that is called when the menu key is pressed.
  -- @param CommandMenuArgument An argument for the function.
  -- @return #MENU_GROUP_COMMAND
  function MENU_GROUP_COMMAND:New( Group, MenuText, ParentMenu, CommandMenuFunction, ... )
    MENU_INDEX:PrepareGroup( Group )
    local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
    local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path )
    if GroupMenu then
      GroupMenu:SetCommandMenuFunction( CommandMenuFunction )
      GroupMenu:SetCommandMenuArguments( arg )
      return GroupMenu
    else
      self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) )
      MENU_INDEX:SetGroupMenu( Group, Path, self )

      self.Group = Group
      self.GroupID = Group:GetID()

      self.MenuPath = missionCommands.addCommandForGroup( self.GroupID, MenuText, self.MenuParentPath, self.MenuCallHandler )

      self:SetParentMenu( self.MenuText, self )
      return self
    end
  end
  --- Refreshes a radio item for a group
  -- @param #MENU_GROUP_COMMAND self
  -- @return #MENU_GROUP_COMMAND
  function MENU_GROUP_COMMAND:Refresh()
    do
      missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
      missionCommands.addCommandForGroup( self.GroupID, self.MenuText, self.MenuParentPath, self.MenuCallHandler )
    end

    return self
  end

  --- Removes a menu structure for a group.
  -- @param #MENU_GROUP_COMMAND self
  -- @param MenuStamp
  -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set.
  -- @return #nil
  function MENU_GROUP_COMMAND:Remove( MenuStamp, MenuTag )
    MENU_INDEX:PrepareGroup( self.Group )
    local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
    local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path )
    if GroupMenu == self then
      if not MenuStamp or self.MenuStamp ~= MenuStamp then
        if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then
          if self.MenuPath ~= nil then
           self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } )
            missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
          end
          MENU_INDEX:ClearGroupMenu( self.Group, Path )
          self:ClearParentMenu( self.MenuText )
          return nil
        end
      end
    else
      BASE:E( { "Cannot Remove MENU_GROUP_COMMAND", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Group = self.Group } )
    end

    return self
  end
end
--- MENU_GROUP_DELAYED
do
  ---
  -- @type MENU_GROUP_DELAYED
  -- @extends Core.Menu#MENU_BASE


  --- The MENU_GROUP_DELAYED class manages the main menus for groups.
  -- You can add menus with the @{#MENU_GROUP.New} method, which constructs a MENU_GROUP object and returns you the object reference.
  -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP.Remove}.
  -- The creation of the menu item is delayed however, and must be created using the @{#MENU_GROUP.Set} method.
  -- This method is most of the time called after the "old" menu items have been removed from the sub menu.
  --
  --
  -- @field #MENU_GROUP_DELAYED
  MENU_GROUP_DELAYED = {
    ClassName = "MENU_GROUP_DELAYED"
  }

  --- MENU_GROUP_DELAYED constructor. Creates a new radio menu item for a group.
  -- @param #MENU_GROUP_DELAYED self
  -- @param Wrapper.Group#GROUP Group The Group owning the menu.
  -- @param #string MenuText The text for the menu.
  -- @param #table ParentMenu The parent menu.
  -- @return #MENU_GROUP_DELAYED self
  function MENU_GROUP_DELAYED:New( Group, MenuText, ParentMenu )

    MENU_INDEX:PrepareGroup( Group )
    local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
    local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path )
    if GroupMenu then
      return GroupMenu
    else
      self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) )
      MENU_INDEX:SetGroupMenu( Group, Path, self )
      self.Group = Group
      self.GroupID = Group:GetID()
      if self.MenuParentPath then
        self.MenuPath = UTILS.DeepCopy( self.MenuParentPath )
      else
        self.MenuPath = {}
      end
      table.insert( self.MenuPath, self.MenuText )

      self:SetParentMenu( self.MenuText, self )
      return self
    end

  end

  --- Refreshes a new radio item for a group and submenus
  -- @param #MENU_GROUP_DELAYED self
  -- @return #MENU_GROUP_DELAYED
  function MENU_GROUP_DELAYED:Set()
    if not self.GroupID then return end
    do
      if not self.MenuSet then
        missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath )
        self.MenuSet = true
      end

      for MenuText, Menu in pairs( self.Menus or {} ) do
        Menu:Set()
      end
    end
  end

  --- Refreshes a new radio item for a group and submenus
  -- @param #MENU_GROUP_DELAYED self
  -- @return #MENU_GROUP_DELAYED
  function MENU_GROUP_DELAYED:Refresh()
    do
      missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
      missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath )

      for MenuText, Menu in pairs( self.Menus or {} ) do
        Menu:Refresh()
      end
    end

    return self
  end

  --- Removes the sub menus recursively of this MENU_GROUP_DELAYED.
  -- @param #MENU_GROUP_DELAYED self
  -- @param MenuStamp
  -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set.
  -- @return #MENU_GROUP_DELAYED self
  function MENU_GROUP_DELAYED:RemoveSubMenus( MenuStamp, MenuTag )
    for MenuText, Menu in pairs( self.Menus or {} ) do
      Menu:Remove( MenuStamp, MenuTag )
    end

    self.Menus = nil

  end

  --- Removes the main menu and sub menus recursively of this MENU_GROUP.
  -- @param #MENU_GROUP_DELAYED self
  -- @param MenuStamp
  -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set.
  -- @return #nil
  function MENU_GROUP_DELAYED:Remove( MenuStamp, MenuTag )
    MENU_INDEX:PrepareGroup( self.Group )
    local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
    local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path )
    if GroupMenu == self then
      self:RemoveSubMenus( MenuStamp, MenuTag )
      if not MenuStamp or self.MenuStamp ~= MenuStamp then
        if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then
          if self.MenuPath ~= nil then
            self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } )
            missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
          end
          MENU_INDEX:ClearGroupMenu( self.Group, Path )
          self:ClearParentMenu( self.MenuText )
          return nil
        end
      end
    else
      BASE:E( { "Cannot Remove MENU_GROUP_DELAYED", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Group = self.Group } )
      return nil
    end

    return self
  end
  
  ---
  -- @type MENU_GROUP_COMMAND_DELAYED
  -- @extends Core.Menu#MENU_COMMAND_BASE

  --- Manages the command menus for coalitions, which allow players to execute functions during mission execution.
  --
  -- You can add menus with the @{#MENU_GROUP_COMMAND_DELAYED.New} method, which constructs a MENU_GROUP_COMMAND_DELAYED object and returns you the object reference.
  -- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND_DELAYED.Remove}.
  --
  -- @field #MENU_GROUP_COMMAND_DELAYED
  MENU_GROUP_COMMAND_DELAYED = {
    ClassName = "MENU_GROUP_COMMAND_DELAYED"
  }

  --- Creates a new radio command item for a group
  -- @param #MENU_GROUP_COMMAND_DELAYED self
  -- @param Wrapper.Group#GROUP Group The Group owning the menu.
  -- @param MenuText The text for the menu.
  -- @param ParentMenu The parent menu.
  -- @param CommandMenuFunction A function that is called when the menu key is pressed.
  -- @param CommandMenuArgument An argument for the function.
  -- @return #MENU_GROUP_COMMAND_DELAYED
  function MENU_GROUP_COMMAND_DELAYED:New( Group, MenuText, ParentMenu, CommandMenuFunction, ... )
    MENU_INDEX:PrepareGroup( Group )
    local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
    local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path )
    if GroupMenu then
      GroupMenu:SetCommandMenuFunction( CommandMenuFunction )
      GroupMenu:SetCommandMenuArguments( arg )
      return GroupMenu
    else
      self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) )
      MENU_INDEX:SetGroupMenu( Group, Path, self )

      self.Group = Group
      self.GroupID = Group:GetID()

      if self.MenuParentPath then
        self.MenuPath = UTILS.DeepCopy( self.MenuParentPath )
      else
        self.MenuPath = {}
      end
      table.insert( self.MenuPath, self.MenuText )

      self:SetParentMenu( self.MenuText, self )
      return self
    end
  end
  --- Refreshes a radio item for a group
  -- @param #MENU_GROUP_COMMAND_DELAYED self
  -- @return #MENU_GROUP_COMMAND_DELAYED
  function MENU_GROUP_COMMAND_DELAYED:Set()
    do
      if not self.MenuSet then
        self.MenuPath = missionCommands.addCommandForGroup( self.GroupID, self.MenuText, self.MenuParentPath, self.MenuCallHandler )
        self.MenuSet = true
      end
    end
  end

  --- Refreshes a radio item for a group
  -- @param #MENU_GROUP_COMMAND_DELAYED self
  -- @return #MENU_GROUP_COMMAND_DELAYED
  function MENU_GROUP_COMMAND_DELAYED:Refresh()
    do
      missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
      missionCommands.addCommandForGroup( self.GroupID, self.MenuText, self.MenuParentPath, self.MenuCallHandler )
    end

    return self
  end

  --- Removes a menu structure for a group.
  -- @param #MENU_GROUP_COMMAND_DELAYED self
  -- @param MenuStamp
  -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set.
  -- @return #nil
  function MENU_GROUP_COMMAND_DELAYED:Remove( MenuStamp, MenuTag )
    MENU_INDEX:PrepareGroup( self.Group )
    local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
    local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path )
    if GroupMenu == self then
      if not MenuStamp or self.MenuStamp ~= MenuStamp then
        if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then
          if self.MenuPath ~= nil then
            self:F( { Group = self.GroupID, Text = self.MenuText, Path = self.MenuPath } )
            missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
          end
          MENU_INDEX:ClearGroupMenu( self.Group, Path )
          self:ClearParentMenu( self.MenuText )
          return nil
        end
      end
    else
      BASE:E( { "Cannot Remove MENU_GROUP_COMMAND_DELAYED", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Group = self.Group } )
    end

    return self
  end
end
--- **Core** - Define zones within your mission of various forms, with various capabilities.
--
-- ===
--
-- ## Features:
--
--   * Create radius zones.
--   * Create trigger zones.
--   * Create polygon zones.
--   * Create moving zones around a unit.
--   * Create moving zones around a group.
--   * Provide the zone behavior. Some zones are static, while others are moveable.
--   * Enquire if a coordinate is within a zone.
--   * Smoke zones.
--   * Set a zone probability to control zone selection.
--   * Get zone coordinates.
--   * Get zone properties.
--   * Get zone bounding box.
--   * Set/get zone name.
--   * Draw zones (circular and polygon) on the F10 map.
--
--
-- There are essentially two core functions that zones accommodate:
--
--   * Test if an object is within the zone boundaries.
--   * Provide the zone behavior. Some zones are static, while others are moveable.
--
-- The object classes are using the zone classes to test the zone boundaries, which can take various forms:
--
--   * Test if completely within the zone.
--   * Test if partly within the zone (for @{Wrapper.Group#GROUP} objects).
--   * Test if not in the zone.
--   * Distance to the nearest intersecting point of the zone.
--   * Distance to the center of the zone.
--   * ...
--
-- Each of these ZONE classes have a zone name, and specific parameters defining the zone type:
--
--   * @{#ZONE_BASE}: The ZONE_BASE class defining the base for all other zone classes.
--   * @{#ZONE_RADIUS}: The ZONE_RADIUS class defined by a zone name, a location and a radius.
--   * @{#ZONE}: The ZONE class, defined by the zone name as defined within the Mission Editor.
--   * @{#ZONE_UNIT}: The ZONE_UNIT class defines by a zone around a @{Wrapper.Unit#UNIT} with a radius.
--   * @{#ZONE_GROUP}: The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius.
--   * @{#ZONE_POLYGON}: The ZONE_POLYGON class defines by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon.
--   * @{#ZONE_OVAL}: The ZONE_OVAL class is defined by a center point, major axis, minor axis, and angle.
--
-- ===
--
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/Core/Zone)
--
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions: **Applevangelist**, **FunkyFranky**, **coconutcockpit**
--
-- ===
--
-- @module Core.Zone
-- @image Core_Zones.JPG

---
-- @type ZONE_BASE
-- @field #string ZoneName Name of the zone.
-- @field #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability.
-- @field #number DrawID Unique ID of the drawn zone on the F10 map.
-- @field #table Color Table with four entries, e.g. {1, 0, 0, 0.15}. First three are RGB color code. Fourth is the transparency Alpha value.
-- @field #table FillColor Table with four entries, e.g. {1, 0, 0, 0.15}. First three are RGB color code. Fourth is the transparency Alpha value.
-- @field #number drawCoalition Draw coalition.
-- @field #number ZoneID ID of zone. Only zones defined in the ME have an ID!
-- @field #table Table of any trigger zone properties from the ME. The key is the Name of the property, and the value is the property's Value.
-- @field #number Surface Type of surface. Only determined at the center of the zone!
-- @field #number Checktime Check every Checktime seconds, used for ZONE:Trigger()
-- @field #boolean PartlyInside When called, a GROUP is considered inside as soon as any of its units enters the zone even if they are far apart.
-- @extends Core.Fsm#FSM


--- This class is an abstract BASE class for derived classes, and is not meant to be instantiated.
--
-- ## Each zone has a name:
--
--   * @{#ZONE_BASE.GetName}(): Returns the name of the zone.
--   * @{#ZONE_BASE.SetName}(): Sets the name of the zone.
--
--
-- ## Each zone implements two polymorphic functions defined in @{#ZONE_BASE}:
--
--   * @{#ZONE_BASE.IsVec2InZone}(): Returns if a 2D vector is within the zone.
--   * @{#ZONE_BASE.IsVec3InZone}(): Returns if a 3D vector is within the zone.
--   * @{#ZONE_BASE.IsPointVec2InZone}(): Returns if a 2D point vector is within the zone.
--   * @{#ZONE_BASE.IsPointVec3InZone}(): Returns if a 3D point vector is within the zone.
--
-- ## A zone has a probability factor that can be set to randomize a selection between zones:
--
--   * @{#ZONE_BASE.SetZoneProbability}(): Set the randomization probability of a zone to be selected, taking a value between 0 and 1 ( 0 = 0%, 1 = 100% )
--   * @{#ZONE_BASE.GetZoneProbability}(): Get the randomization probability of a zone to be selected, passing a value between 0 and 1 ( 0 = 0%, 1 = 100% )
--   * @{#ZONE_BASE.GetZoneMaybe}(): Get the zone taking into account the randomization probability. nil is returned if this zone is not a candidate.
--
-- ## A zone manages vectors:
--
--   * @{#ZONE_BASE.GetVec2}(): Returns the 2D vector coordinate of the zone.
--   * @{#ZONE_BASE.GetVec3}(): Returns the 3D vector coordinate of the zone.
--   * @{#ZONE_BASE.GetPointVec2}(): Returns the 2D point vector coordinate of the zone.
--   * @{#ZONE_BASE.GetPointVec3}(): Returns the 3D point vector coordinate of the zone.
--   * @{#ZONE_BASE.GetRandomVec2}(): Define a random 2D vector within the zone.
--   * @{#ZONE_BASE.GetRandomPointVec2}(): Define a random 2D point vector within the zone.
--   * @{#ZONE_BASE.GetRandomPointVec3}(): Define a random 3D point vector within the zone.
--
-- ## A zone has a bounding square:
--
--   * @{#ZONE_BASE.GetBoundingSquare}(): Get the outer most bounding square of the zone.
--
-- ## A zone can be marked:
--
--   * @{#ZONE_BASE.SmokeZone}(): Smokes the zone boundaries in a color.
--   * @{#ZONE_BASE.FlareZone}(): Flares the zone boundaries in a color.
--
-- ## A zone might have additional Properties created in the DCS Mission Editor, which can be accessed:
--
--   *@{#ZONE_BASE.GetProperty}(): Returns the Value of the zone with the given PropertyName, or nil if no matching property exists.
--   *@{#ZONE_BASE.GetAllProperties}(): Returns the zone Properties table.
--
-- @field #ZONE_BASE
ZONE_BASE = {
  ClassName = "ZONE_BASE",
  ZoneName = "",
  ZoneProbability = 1,
  DrawID=nil,
  Color={},
  ZoneID=nil,
  Properties={},
  Surface=nil,
  Checktime = 5,
}

--- The ZONE_BASE.BoundingSquare
-- @type ZONE_BASE.BoundingSquare
-- @field DCS#Distance x1 The lower x coordinate (left down)
-- @field DCS#Distance y1 The lower y coordinate (left down)
-- @field DCS#Distance x2 The higher x coordinate (right up)
-- @field DCS#Distance y2 The higher y coordinate (right up)

--- ZONE_BASE constructor
-- @param #ZONE_BASE self
-- @param #string ZoneName Name of the zone.
-- @return #ZONE_BASE self
function ZONE_BASE:New( ZoneName )
  local self = BASE:Inherit( self, FSM:New() )
  --self:F( ZoneName )

  self.ZoneName = ZoneName

  --_DATABASE:AddZone(ZoneName,self)

  return self
end

--- Returns the name of the zone.
-- @param #ZONE_BASE self
-- @return #string The name of the zone.
function ZONE_BASE:GetName()
  --self:F2()

  return self.ZoneName
end

--- Sets the name of the zone.
-- @param #ZONE_BASE self
-- @param #string ZoneName The name of the zone.
-- @return #ZONE_BASE
function ZONE_BASE:SetName( ZoneName )
  --self:F2()

  self.ZoneName = ZoneName
end

--- Returns if a Vec2 is within the zone.
-- @param #ZONE_BASE self
-- @param DCS#Vec2 Vec2 The Vec2 to test.
-- @return #boolean true if the Vec2 is within the zone.
function ZONE_BASE:IsVec2InZone( Vec2 )
 --self:F2( Vec2 )

  return false
end

--- Returns if a Vec3 is within the zone.
-- @param #ZONE_BASE self
-- @param DCS#Vec3 Vec3 The point to test.
-- @return #boolean true if the Vec3 is within the zone.
function ZONE_BASE:IsVec3InZone( Vec3 )
  if not Vec3 then return false end
  local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } )
  return InZone
end

--- Returns if a Coordinate is within the zone.
-- @param #ZONE_BASE self
-- @param Core.Point#COORDINATE Coordinate The coordinate to test.
-- @return #boolean true if the coordinate is within the zone.
function ZONE_BASE:IsCoordinateInZone( Coordinate )
  if not Coordinate then return false end
  local InZone = self:IsVec2InZone( Coordinate:GetVec2() )
  return InZone
end

--- Returns if a PointVec2 is within the zone. (Name is misleading, actually takes a #COORDINATE)
-- @param #ZONE_BASE self
-- @param Core.Point#COORDINATE Coordinate The coordinate to test.
-- @return #boolean true if the PointVec2 is within the zone.
function ZONE_BASE:IsPointVec2InZone( Coordinate )
  local InZone = self:IsVec2InZone( Coordinate:GetVec2() )
  return InZone
end

--- Returns if a PointVec3 is within the zone.
-- @param #ZONE_BASE self
-- @param Core.Point#COORDINATE PointVec3 The PointVec3 to test.
-- @return #boolean true if the PointVec3 is within the zone.
function ZONE_BASE:IsPointVec3InZone( PointVec3 )
  local InZone = self:IsPointVec2InZone( PointVec3 )
  return InZone
end

--- Returns the @{DCS#Vec2} coordinate of the zone.
-- @param #ZONE_BASE self
-- @return #nil.
function ZONE_BASE:GetVec2()
  return nil
end

--- Returns a @{Core.Point#COORDINATE} of the zone.
-- @param #ZONE_BASE self
-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located.
-- @return Core.Point#COORDINATE The COORDINATE of the zone.
function ZONE_BASE:GetPointVec2()
  --self:F2( self.ZoneName )

  local Vec2 = self:GetVec2()

  local PointVec2 = COORDINATE:NewFromVec2( Vec2 )

  --self:T2( { PointVec2 } )

  return PointVec2
end

--- Returns the @{DCS#Vec3} of the zone.
-- @param #ZONE_BASE self
-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located.
-- @return DCS#Vec3 The Vec3 of the zone.
function ZONE_BASE:GetVec3( Height )
  --self:F2( self.ZoneName )

  Height = Height or 0

  local Vec2 = self:GetVec2()

  local Vec3 = { x = Vec2.x, y = Height and Height or land.getHeight( self:GetVec2() ), z = Vec2.y }

  --self:T2( { Vec3 } )

  return Vec3
end

--- Returns a @{Core.Point#COORDINATE} of the zone.
-- @param #ZONE_BASE self
-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located.
-- @return Core.Point#COORDINATE The PointVec3 of the zone.
function ZONE_BASE:GetPointVec3( Height )
  --self:F2( self.ZoneName )

  local Vec3 = self:GetVec3( Height )

  local PointVec3 = COORDINATE:NewFromVec3( Vec3 )

  --self:T2( { PointVec3 } )

  return PointVec3
end

--- Returns a @{Core.Point#COORDINATE} of the zone.
-- @param #ZONE_BASE self
-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located.
-- @return Core.Point#COORDINATE The Coordinate of the zone.
function ZONE_BASE:GetCoordinate( Height ) --R2.1
  --self:F2(self.ZoneName)

  local Vec3 = self:GetVec3( Height )

  if self.Coordinate then

    -- Update coordinates.
    self.Coordinate.x=Vec3.x
    self.Coordinate.y=Vec3.y
    self.Coordinate.z=Vec3.z

    --env.info("FF GetCoordinate NEW for ZONE_BASE "..tostring(self.ZoneName))
  else

    -- Create a new coordinate object.
    self.Coordinate=COORDINATE:NewFromVec3(Vec3)

    --env.info("FF GetCoordinate NEW for ZONE_BASE "..tostring(self.ZoneName))
  end

  return self.Coordinate
end

--- Get 2D distance to a coordinate.
-- @param #ZONE_BASE self
-- @param Core.Point#COORDINATE Coordinate Reference coordinate. Can also be a DCS#Vec2 or DCS#Vec3 object.
-- @return #number Distance to the reference coordinate in meters.
function ZONE_BASE:Get2DDistance(Coordinate)
  local a=self:GetVec2()
  local b={}
  if Coordinate.z then
    b.x=Coordinate.x
    b.y=Coordinate.z
  else
    b.x=Coordinate.x
    b.y=Coordinate.y
  end
  local dist=UTILS.VecDist2D(a,b)
  return dist
end

--- Define a random @{DCS#Vec2} within the zone.
-- @param #ZONE_BASE self
-- @return DCS#Vec2 The Vec2 coordinates.
function ZONE_BASE:GetRandomVec2()
  return nil
end

--- Define a random @{Core.Point#COORDINATE} within the zone. Note that this is actually a @{Core.Point#COORDINATE} type object, and not a simple Vec2 table.
-- @param #ZONE_BASE self
-- @return Core.Point#COORDINATE The COORDINATE coordinates.
function ZONE_BASE:GetRandomPointVec2()
  return nil
end

--- Define a random @{Core.Point#COORDINATE} within the zone. Note that this is actually a @{Core.Point#COORDINATE} type object, and not a simple Vec3 table.
-- @param #ZONE_BASE self
-- @return Core.Point#COORDINATE The COORDINATE coordinates.
function ZONE_BASE:GetRandomPointVec3()
  return nil
end

--- Get the bounding square the zone.
-- @param #ZONE_BASE self
-- @return #nil The bounding square.
function ZONE_BASE:GetBoundingSquare()
  return nil
end

--- Get surface type of the zone.
-- @param #ZONE_BASE self
-- @return DCS#SurfaceType Type of surface.
function ZONE_BASE:GetSurfaceType()
  local coord=self:GetCoordinate()
  local surface=coord:GetSurfaceType()
  return surface
end

--- Bound the zone boundaries with a tires.
-- @param #ZONE_BASE self
function ZONE_BASE:BoundZone()
  --self:F2()
end

--- Set draw coalition of zone.
-- @param #ZONE_BASE self
-- @param #number Coalition Coalition. Default -1.
-- @return #ZONE_BASE self
function ZONE_BASE:SetDrawCoalition(Coalition)
  self.drawCoalition=Coalition or -1
  return self
end

--- Get draw coalition of zone.
-- @param #ZONE_BASE self
-- @return #number Draw coalition.
function ZONE_BASE:GetDrawCoalition()
  return self.drawCoalition or -1
end

--- Set color of zone.
-- @param #ZONE_BASE self
-- @param #table RGBcolor RGB color table. Default `{1, 0, 0}`.
-- @param #number Alpha Transparency between 0 and 1. Default 0.15.
-- @return #ZONE_BASE self
function ZONE_BASE:SetColor(RGBcolor, Alpha)

  RGBcolor=RGBcolor or {1, 0, 0}
  Alpha=Alpha or 0.15

  self.Color={}
  self.Color[1]=RGBcolor[1]
  self.Color[2]=RGBcolor[2]
  self.Color[3]=RGBcolor[3]
  self.Color[4]=Alpha

  return self
end

--- Get color table of the zone.
-- @param #ZONE_BASE self
-- @return #table Table with four entries, e.g. {1, 0, 0, 0.15}. First three are RGB color code. Fourth is the transparency Alpha value.
function ZONE_BASE:GetColor()
  return self.Color or {1, 0, 0, 0.15}
end

--- Get RGB color of zone.
-- @param #ZONE_BASE self
-- @return #table Table with three entries, e.g. {1, 0, 0}, which is the RGB color code.
function ZONE_BASE:GetColorRGB()
  local rgb={}
  local Color=self:GetColor()
  rgb[1]=Color[1]
  rgb[2]=Color[2]
  rgb[3]=Color[3]
  return rgb
end

--- Get transparency Alpha value of zone.
-- @param #ZONE_BASE self
-- @return #number Alpha value.
function ZONE_BASE:GetColorAlpha()
  local Color=self:GetColor()
  local alpha=Color[4]
  return alpha
end

--- Set fill color of zone.
-- @param #ZONE_BASE self
-- @param #table RGBcolor RGB color table. Default `{1, 0, 0}`.
-- @param #number Alpha Transparacy between 0 and 1. Default 0.15.
-- @return #ZONE_BASE self
function ZONE_BASE:SetFillColor(RGBcolor, Alpha)

  RGBcolor=RGBcolor or {1, 0, 0}
  Alpha=Alpha or 0.15

  self.FillColor={}
  self.FillColor[1]=RGBcolor[1]
  self.FillColor[2]=RGBcolor[2]
  self.FillColor[3]=RGBcolor[3]
  self.FillColor[4]=Alpha

  return self
end

--- Get fill color table of the zone.
-- @param #ZONE_BASE self
-- @return #table Table with four entries, e.g. {1, 0, 0, 0.15}. First three are RGB color code. Fourth is the transparency Alpha value.
function ZONE_BASE:GetFillColor()
  return self.FillColor or {1, 0, 0, 0.15}
end

--- Get RGB fill color of zone.
-- @param #ZONE_BASE self
-- @return #table Table with three entries, e.g. {1, 0, 0}, which is the RGB color code.
function ZONE_BASE:GetFillColorRGB()
  local rgb={}
  local FillColor=self:GetFillColor()
  rgb[1]=FillColor[1]
  rgb[2]=FillColor[2]
  rgb[3]=FillColor[3]
  return rgb
end

--- Get transparency Alpha fill value of zone.
-- @param #ZONE_BASE self
-- @return #number Alpha value.
function ZONE_BASE:GetFillColorAlpha()
  local FillColor=self:GetFillColor()
  local alpha=FillColor[4]
  return alpha
end

--- Remove the drawing of the zone from the F10 map.
-- @param #ZONE_BASE self
-- @param #number Delay (Optional) Delay before the drawing is removed.
-- @return #ZONE_BASE self
function ZONE_BASE:UndrawZone(Delay)
  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, ZONE_BASE.UndrawZone, self)
  else
    if self.DrawID then
      if type(self.DrawID) ~= "table" then
        UTILS.RemoveMark(self.DrawID)
      else -- DrawID is a table with a collections of mark ids, as used in ZONE_POLYGON
        for _, mark_id in pairs(self.DrawID) do
          UTILS.RemoveMark(mark_id)
        end
      end
    end
  end
  return self
end

--- Get ID of the zone object drawn on the F10 map.
-- The ID can be used to remove the drawn object from the F10 map view via `UTILS.RemoveMark(MarkID)`.
-- @param #ZONE_BASE self
-- @return #number Unique ID of the
function ZONE_BASE:GetDrawID()
  return self.DrawID
end


--- Smokes the zone boundaries in a color.
-- @param #ZONE_BASE self
-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color.
function ZONE_BASE:SmokeZone( SmokeColor )
  --self:F2( SmokeColor )

end

--- Set the randomization probability of a zone to be selected.
-- @param #ZONE_BASE self
-- @param #number ZoneProbability A value between 0 and 1. 0 = 0% and 1 = 100% probability.
-- @return #ZONE_BASE self
function ZONE_BASE:SetZoneProbability( ZoneProbability )
  --self:F( { self:GetName(), ZoneProbability = ZoneProbability } )

  self.ZoneProbability = ZoneProbability or 1
  return self
end

--- Get the randomization probability of a zone to be selected.
-- @param #ZONE_BASE self
-- @return #number A value between 0 and 1. 0 = 0% and 1 = 100% probability.
function ZONE_BASE:GetZoneProbability()
  --self:F2()

  return self.ZoneProbability
end

--- Get the coordinate on the radius of the zone nearest to Outsidecoordinate. Useto e.g. find an ingress point.
-- @param #ZONE_BASE self
-- @param Core.Point#COORDINATE Outsidecoordinate The coordinate outside of the zone from where to look.
-- @return Core.Point#COORDINATE CoordinateOnRadius
function ZONE_BASE:FindNearestCoordinateOnRadius(Outsidecoordinate)
  local Vec1 = self:GetVec2()
  local Radius = self:GetRadius()
  local Vec2 = Outsidecoordinate:GetVec2()
  local Point = UTILS.FindNearestPointOnCircle(Vec1,Radius,Vec2)
  local rc = COORDINATE:NewFromVec2(Point)
  return rc
end

--- Get the zone taking into account the randomization probability of a zone to be selected.
-- @param #ZONE_BASE self
-- @return #ZONE_BASE The zone is selected taking into account the randomization probability factor.
-- @return #nil The zone is not selected taking into account the randomization probability factor.
-- @usage
--
-- local ZoneArray = { ZONE:New( "Zone1" ), ZONE:New( "Zone2" ) }
--
-- -- We set a zone probability of 70% to the first zone and 30% to the second zone.
-- ZoneArray[1]:SetZoneProbability( 0.5 )
-- ZoneArray[2]:SetZoneProbability( 0.5 )
--
-- local ZoneSelected = nil
--
-- while ZoneSelected == nil do
--   for _, Zone in pairs( ZoneArray ) do
--     ZoneSelected = Zone:GetZoneMaybe()
--     if ZoneSelected ~= nil then
--       break
--     end
--   end
-- end
--
-- -- The result should be that Zone1 would be more probable selected than Zone2.
--
function ZONE_BASE:GetZoneMaybe()
  --self:F2()

  local Randomization = math.random()
  if Randomization <= self.ZoneProbability then
    return self
  else
    return nil
  end
end

--- Set the check time for ZONE:Trigger()
-- @param #ZONE_BASE self
-- @param #number seconds Check every seconds for objects entering or leaving the zone. Defaults to 5 secs.
-- @return #ZONE_BASE self
function ZONE_BASE:SetCheckTime(seconds)
  self.Checktime = seconds or 5
  return self
end

--- Start watching if the Object or Objects move into or out of a zone.
-- @param #ZONE_BASE self
-- @param Wrapper.Controllable#CONTROLLABLE Objects Object or Objects to watch, can be of type UNIT, GROUP, CLIENT, or SET\_UNIT, SET\_GROUP, SET\_CLIENT
-- @return #ZONE_BASE self
-- @usage
--            -- Create a new zone and start watching it every 5 secs for a defined GROUP entering or leaving
--            local triggerzone = ZONE:New("ZonetoWatch"):Trigger(GROUP:FindByName("Aerial-1"))
--
--            -- This FSM function will be called when the group enters the zone
--            function triggerzone:OnAfterEnteredZone(From,Event,To,Group)
--              MESSAGE:New("Group has entered zone!",15):ToAll()
--            end
--
--            -- This FSM function will be called when the group leaves the zone
--            function triggerzone:OnAfterLeftZone(From,Event,To,Group)
--              MESSAGE:New("Group has left zone!",15):ToAll()
--            end
--
--            -- Stop watching the zone after 1 hour
--           triggerzone:__TriggerStop(3600)
--            -- Call :SetPartlyInside() if you use SET_GROUP to count as inside when any of their units enters even when they are far apart.
--            -- Make sure to call :SetPartlyInside() before :Trigger()!
function ZONE_BASE:Trigger(Objects)
  --self:I("Added Zone Trigger")
  self:SetStartState("TriggerStopped")
  self:AddTransition("TriggerStopped","TriggerStart","TriggerRunning")
  self:AddTransition("*","EnteredZone","*")
  self:AddTransition("*","LeftZone","*")
  self:AddTransition("*","ZoneEmpty","*")
  self:AddTransition("*","ObjectDead","*")
  self:AddTransition("*","TriggerRunCheck","*")
  self:AddTransition("*","TriggerStop","TriggerStopped")
  self:TriggerStart()
  self.checkobjects = Objects
  self.ObjectsInZone = false
  if UTILS.IsInstanceOf(Objects,"SET_BASE") then
    self.objectset = Objects.Set
  else
    self.objectset = {Objects}
  end
  self:_TriggerCheck(true)
  self:__TriggerRunCheck(self.Checktime)
  return self

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "TriggerStop". Stops the ZONE_BASE Trigger.
  -- @function [parent=#ZONE_BASE] TriggerStop
  -- @param #ZONE_BASE self

  --- Triggers the FSM event "TriggerStop" after a delay.
  -- @function [parent=#ZONE_BASE] __TriggerStop
  -- @param #ZONE_BASE self
  -- @param #number delay Delay in seconds.

  --- On After "EnteredZone" event. An observed object has entered the zone.
  -- @function [parent=#ZONE_BASE] OnAfterEnteredZone
  -- @param #ZONE_BASE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The controllable entering the zone.

  --- On After "LeftZone" event. An observed object has left the zone.
  -- @function [parent=#ZONE_BASE] OnAfterLeftZone
  -- @param #ZONE_BASE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The controllable leaving the zone.
  
  --- On After "ObjectDead" event. An observed object has left the zone.
  -- @function [parent=#ZONE_BASE] OnAfterObjectDead
  -- @param #ZONE_BASE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The controllable which died. Might be nil.
  
  --- On After "ZoneEmpty" event. All observed objects have left the zone or are dead.
  -- @function [parent=#ZONE_BASE] OnAfterZoneEmpty
  -- @param #ZONE_BASE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
end

  --- Toggle “partly-inside” handling for this zone. To be used before :Trigger().
  -- * Default:* flag is **false** until you call the method.  
  -- * Call with no argument or with **true** → enable.  
  -- * Call with **false** → disable again (handy if it was enabled before).
  -- @param #ZONE_BASE self
  -- @return #ZONE_BASE self
  function ZONE_BASE:SetPartlyInside(state)
  self.PartlyInside = state or not ( state == false )
  return self
  end
--- (Internal) Check the assigned objects for being in/out of the zone
-- @param #ZONE_BASE self
-- @param #boolean fromstart If true, do the init of the objects
-- @return #ZONE_BASE self
function ZONE_BASE:_TriggerCheck(fromstart)
  --self:I("_TriggerCheck | FromStart = "..tostring(fromstart))
  local objectset = self.objectset or {}
  if fromstart then
    -- just earmark everyone in/out
    for _,_object in pairs(objectset) do
      local obj = _object -- Wrapper.Controllable#CONTROLLABLE
      if not obj.TriggerInZone then 
        obj.TriggerInZone = {}
        obj.TriggerZoneDeadNotification = false 
      end
      if obj and obj:IsAlive() and self:IsCoordinateInZone(obj:GetCoordinate()) then
        obj.TriggerInZone[self.ZoneName] = true
        self.ObjectsInZone = true
      else
        obj.TriggerInZone[self.ZoneName] = false
      end
      --self:I("Object "..obj:GetName().." is in zone = "..tostring(obj.TriggerInZone[self.ZoneName]))
    end
  else
    -- Check for changes
    local objcount = 0
    for _,_object in pairs(objectset) do
      local obj = _object -- Wrapper.Controllable#CONTROLLABLE
      if obj and obj:IsAlive() then
        if not obj.TriggerInZone then
          -- has not been tagged previously - wasn't in set!
          obj.TriggerInZone = {}
        end
        if not obj.TriggerInZone[self.ZoneName] then
          -- has not been tagged previously - wasn't in set!
          obj.TriggerInZone[self.ZoneName] = false
        end
        -- is obj in zone?
        local inzone
        if self.PartlyInside and obj.ClassName == "GROUP" then
            inzone = obj:IsAnyInZone(self)                     -- TRUE if any unit is inside
        else
            inzone = self:IsCoordinateInZone(obj:GetCoordinate()) -- original barycentre test
        end
        --self:I("Object "..obj:GetName().." is in zone: "..tostring(inzone))
        if inzone and obj.TriggerInZone[self.ZoneName] then
          -- just count
          objcount = objcount + 1
          self.ObjectsInZone = true
          obj.TriggerZoneDeadNotification = false
        end
        if inzone and not obj.TriggerInZone[self.ZoneName] then
          -- wasn't in zone before
          --self:I("Newly entered")
          self:__EnteredZone(0.5,obj)
          obj.TriggerInZone[self.ZoneName] = true
          objcount = objcount + 1
          self.ObjectsInZone = true
          obj.TriggerZoneDeadNotification = false
        elseif (not inzone) and obj.TriggerInZone[self.ZoneName] then
          -- has left the zone
          --self:I("Newly left")
          self:__LeftZone(0.5,obj)
          obj.TriggerInZone[self.ZoneName] = false
        else
          --self:I("Not left or not entered, or something went wrong!")
        end
      else
        -- object dead
        if not obj.TriggerZoneDeadNotification == true then
          obj.TriggerInZone = nil
          self:__ObjectDead(0.5,obj)
          obj.TriggerZoneDeadNotification = true
        end
      end
    end
    -- zone empty?
    if objcount == 0 and self.ObjectsInZone == true then
      -- zone was not but is now empty
      self.ObjectsInZone = false
      self:__ZoneEmpty(0.5)
    end
  end
  return self
end

--- (Internal) Check the assigned objects for being in/out of the zone
-- @param #ZONE_BASE self
-- @param #string From
-- @param #string Event
-- @param #string to
-- @return #ZONE_BASE self
function ZONE_BASE:onafterTriggerRunCheck(From,Event,To)
  if self:GetState() ~= "TriggerStopped" then
    self:_TriggerCheck()
    self:__TriggerRunCheck(self.Checktime)
  end
  return self
end



--- Returns the Value of the zone with the given PropertyName, or nil if no matching property exists.
-- @param #ZONE_BASE self
-- @param #string PropertyName The name of a the TriggerZone Property to be retrieved.
-- @return #string The Value of the TriggerZone Property with the given PropertyName, or nil if absent.
-- @usage
--
-- local PropertiesZone = ZONE:FindByName("Properties Zone")
-- local Property = "ExampleProperty"
-- local PropertyValue = PropertiesZone:GetProperty(Property)
--
function ZONE_BASE:GetProperty(PropertyName)
  return self.Properties[PropertyName]
end

--- Returns the zone Properties table.
-- @param #ZONE_BASE self
-- @return #table The Key:Value table of TriggerZone properties of the zone.
function ZONE_BASE:GetAllProperties()
  return self.Properties
end

--- The ZONE_RADIUS class, defined by a zone name, a location and a radius.
-- @type ZONE_RADIUS
-- @field DCS#Vec2 Vec2 The current location of the zone.
-- @field DCS#Distance Radius The radius of the zone.
-- @extends #ZONE_BASE

--- The ZONE_RADIUS class defined by a zone name, a location and a radius.
-- This class implements the inherited functions from @{#ZONE_BASE} taking into account the own zone format and properties.
--
-- ## ZONE_RADIUS constructor
--
--   * @{#ZONE_RADIUS.New}(): Constructor.
--
-- ## Manage the radius of the zone
--
--   * @{#ZONE_RADIUS.SetRadius}(): Sets the radius of the zone.
--   * @{#ZONE_RADIUS.GetRadius}(): Returns the radius of the zone.
--
-- ## Manage the location of the zone
--
--   * @{#ZONE_RADIUS.SetVec2}(): Sets the @{DCS#Vec2} of the zone.
--   * @{#ZONE_RADIUS.GetVec2}(): Returns the @{DCS#Vec2} of the zone.
--   * @{#ZONE_RADIUS.GetVec3}(): Returns the @{DCS#Vec3} of the zone, taking an additional height parameter.
--
-- ## Zone point randomization
--
-- Various functions exist to find random points within the zone.
--
--   * @{#ZONE_RADIUS.GetRandomVec2}(): Gets a random 2D point in the zone.
--   * @{#ZONE_RADIUS.GetRandomPointVec2}(): Gets a @{Core.Point#COORDINATE} object representing a random 2D point in the zone.
--   * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Core.Point#COORDINATE} object representing a random 3D point in the zone. Note that the height of the point is at landheight.
--
-- ## Draw zone
--
--   * @{#ZONE_RADIUS.DrawZone}(): Draws the zone on the F10 map.
--
-- @field #ZONE_RADIUS
ZONE_RADIUS = {
  ClassName="ZONE_RADIUS",
  }

--- Constructor of @{#ZONE_RADIUS}, taking the zone name, the zone location and a radius.
-- @param #ZONE_RADIUS self
-- @param #string ZoneName Name of the zone.
-- @param DCS#Vec2 Vec2 The location of the zone.
-- @param DCS#Distance Radius The radius of the zone.
-- @param DCS#Boolean DoNotRegisterZone Determines if the Zone should not be registered in the _Database Table. Default=false
-- @return #ZONE_RADIUS self
function ZONE_RADIUS:New( ZoneName, Vec2, Radius, DoNotRegisterZone )

  -- Inherit ZONE_BASE.
  local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS
  --self:F( { ZoneName, Vec2, Radius } )

  self.Radius = Radius
  self.Vec2 = Vec2

  if not DoNotRegisterZone then
    _EVENTDISPATCHER:CreateEventNewZone(self)
  end

  --self.Coordinate=COORDINATE:NewFromVec2(Vec2)

  return self
end

--- Update zone from a 2D vector.
-- @param #ZONE_RADIUS self
-- @param DCS#Vec2 Vec2 The location of the zone.
-- @param DCS#Distance Radius The radius of the zone.
-- @return #ZONE_RADIUS self
function ZONE_RADIUS:UpdateFromVec2(Vec2, Radius)

  -- New center of the zone.
  self.Vec2=Vec2

  if Radius then
    self.Radius=Radius
  end

  return self
end

--- Update zone from a 2D vector.
-- @param #ZONE_RADIUS self
-- @param DCS#Vec3 Vec3 The location of the zone.
-- @param DCS#Distance Radius The radius of the zone.
-- @return #ZONE_RADIUS self
function ZONE_RADIUS:UpdateFromVec3(Vec3, Radius)

  -- New center of the zone.
  self.Vec2.x=Vec3.x
  self.Vec2.y=Vec3.z

  if Radius then
    self.Radius=Radius
  end

  return self
end

--- Mark the zone with markers on the F10 map.
-- @param #ZONE_RADIUS self
-- @param #number Points (Optional) The amount of points in the circle. Default 360.
-- @return #ZONE_RADIUS self
function ZONE_RADIUS:MarkZone(Points)

  local Point = {}
  local Vec2 = self:GetVec2()

  Points = Points and Points or 360

  local Angle
  local RadialBase = math.pi*2

  for Angle = 0, 360, (360 / Points ) do

    local Radial = Angle * RadialBase / 360

    Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius()
    Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius()

    COORDINATE:NewFromVec2(Point):MarkToAll(self:GetName())

  end

end

--- Draw the zone circle on the F10 map.
-- @param #ZONE_RADIUS self
-- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All.
-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red.
-- @param #number Alpha Transparency [0,1]. Default 1.
-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value.
-- @param #number FillAlpha Transparency [0,1]. Default 0.15.
-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
-- @return #ZONE_RADIUS self
function ZONE_RADIUS:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly)

  local coordinate=self:GetCoordinate()

  local Radius=self:GetRadius()

  Color=Color or self:GetColorRGB()
  Alpha=Alpha or 1
  FillColor=FillColor or UTILS.DeepCopy(Color)
  FillAlpha=FillAlpha or self:GetColorAlpha()

  self.DrawID=coordinate:CircleToAll(Radius, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly)

  return self
end

--- Bounds the zone with tires.
-- @param #ZONE_RADIUS self
-- @param #number Points (optional) The amount of points in the circle. Default 360.
-- @param DCS#country.id CountryID The country id of the tire objects, e.g. country.id.USA for blue or country.id.RUSSIA for red.
-- @param #boolean UnBound (Optional) If true the tyres will be destroyed.
-- @return #ZONE_RADIUS self
function ZONE_RADIUS:BoundZone( Points, CountryID, UnBound )

  local Point = {}
  local Vec2 = self:GetVec2()
  local countryID = CountryID or country.id.USA
  
  Points = Points and Points or 360

  local Angle
  local RadialBase = math.pi*2

  for Angle = 0, 360, (360 / Points ) do
    local Radial = Angle * RadialBase / 360
    Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius()
    Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius()

    local CountryName = _DATABASE.COUNTRY_NAME[countryID]

    local Tire = {
        ["country"] = CountryName,
        ["category"] = "Fortifications",
        ["canCargo"] = false,
        ["shape_name"] = "H-tyre_B_WF",
        ["type"] = "Black_Tyre_WF",
        --["unitId"] = Angle + 10000,
        ["y"] = Point.y,
        ["x"] = Point.x,
        ["name"] = string.format( "%s-Tire #%0d", self:GetName(), Angle ),
        ["heading"] = 0,
    } -- end of ["group"]

    local Group = coalition.addStaticObject( countryID, Tire )
    if UnBound and UnBound == true then
      Group:destroy()
    end
  end

  return self
end

--- Smokes the zone boundaries in a color.
-- @param #ZONE_RADIUS self
-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color.
-- @param #number Points (optional) The amount of points in the circle.
-- @param #number AddHeight (optional) The height to be added for the smoke.
-- @param #number AddOffSet (optional) The angle to be added for the smoking start position.
-- @return #ZONE_RADIUS self
function ZONE_RADIUS:SmokeZone( SmokeColor, Points, AddHeight, AngleOffset )
  --self:F2( SmokeColor )

  local Point = {}
  local Vec2 = self:GetVec2()

  AddHeight = AddHeight or 0
  AngleOffset = AngleOffset or 0

  Points = Points and Points or 360

  local Angle
  local RadialBase = math.pi*2

  for Angle = 0, 360, 360 / Points do
    local Radial = ( Angle + AngleOffset ) * RadialBase / 360
    Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius()
    Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius()
    COORDINATE:New( Point.x, AddHeight, Point.y  ):Smoke( SmokeColor )
  end

  return self
end

--- Flares the zone boundaries in a color.
-- @param #ZONE_RADIUS self
-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color.
-- @param #number Points (optional) The amount of points in the circle.
-- @param DCS#Azimuth Azimuth (optional) Azimuth The azimuth of the flare.
-- @param #number AddHeight (optional) The height to be added for the smoke.
-- @return #ZONE_RADIUS self
function ZONE_RADIUS:FlareZone( FlareColor, Points, Azimuth, AddHeight )
  --self:F2( { FlareColor, Azimuth } )

  local Point = {}
  local Vec2 = self:GetVec2()

  AddHeight = AddHeight or 0

  Points = Points and Points or 360

  local Angle
  local RadialBase = math.pi*2

  for Angle = 0, 360, 360 / Points do
    local Radial = Angle * RadialBase / 360
    Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius()
    Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius()
    COORDINATE:New( Point.x, AddHeight, Point.y ):Flare( FlareColor, Azimuth )
  end

  return self
end

--- Returns the radius of the zone.
-- @param #ZONE_RADIUS self
-- @return DCS#Distance The radius of the zone.
function ZONE_RADIUS:GetRadius()
  --self:F2( self.ZoneName )

  --self:T2( { self.Radius } )

  return self.Radius
end

--- Sets the radius of the zone.
-- @param #ZONE_RADIUS self
-- @param DCS#Distance Radius The radius of the zone.
-- @return DCS#Distance The radius of the zone.
function ZONE_RADIUS:SetRadius( Radius )
  --self:F2( self.ZoneName )

  self.Radius = Radius
  --self:T2( { self.Radius } )

  return self.Radius
end

--- Returns the @{DCS#Vec2} of the zone.
-- @param #ZONE_RADIUS self
-- @return DCS#Vec2 The location of the zone.
function ZONE_RADIUS:GetVec2()
  --self:F2( self.ZoneName )

  --self:T2( { self.Vec2 } )

  return self.Vec2
end

--- Sets the @{DCS#Vec2} of the zone.
-- @param #ZONE_RADIUS self
-- @param DCS#Vec2 Vec2 The new location of the zone.
-- @return DCS#Vec2 The new location of the zone.
function ZONE_RADIUS:SetVec2( Vec2 )
  --self:F2( self.ZoneName )

  self.Vec2 = Vec2

  --self:T2( { self.Vec2 } )

  return self.Vec2
end

--- Returns the @{DCS#Vec3} of the ZONE_RADIUS.
-- @param #ZONE_RADIUS self
-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located.
-- @return DCS#Vec3 The point of the zone.
function ZONE_RADIUS:GetVec3( Height )
  --self:F2( { self.ZoneName, Height } )

  Height = Height or 0
  local Vec2 = self:GetVec2()

  local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y }

  --self:T2( { Vec3 } )

  return Vec3
end

--- Scan the zone for the presence of units of the given ObjectCategories.
-- Note that **only after** a zone has been scanned, the zone can be evaluated by:
--
--   * @{Core.Zone#ZONE_RADIUS.IsAllInZoneOfCoalition}(): Scan the presence of units in the zone of a coalition.
--   * @{Core.Zone#ZONE_RADIUS.IsAllInZoneOfOtherCoalition}(): Scan the presence of units in the zone of an other coalition.
--   * @{Core.Zone#ZONE_RADIUS.IsSomeInZoneOfCoalition}(): Scan if there is some presence of units in the zone of the given coalition.
--   * @{Core.Zone#ZONE_RADIUS.IsNoneInZoneOfCoalition}(): Scan if there isn't any presence of units in the zone of an other coalition than the given one.
--   * @{Core.Zone#ZONE_RADIUS.IsNoneInZone}(): Scan if the zone is empty.
-- @param #ZONE_RADIUS self
-- @param ObjectCategories An array of categories of the objects to find in the zone. E.g. `{Object.Category.UNIT}`
-- @param UnitCategories An array of unit categories of the objects to find in the zone. E.g. `{Unit.Category.GROUND_UNIT,Unit.Category.SHIP}`
-- @usage
--    myzone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT})
--    local IsAttacked = myzone:IsSomeInZoneOfCoalition( self.Coalition )
function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories )

  self.ScanData = {}
  self.ScanData.Coalitions = {}
  self.ScanData.Scenery = {}
  self.ScanData.SceneryTable = {}
  self.ScanData.Units = {}

  local ZoneCoord = self:GetCoordinate()
  local ZoneRadius = self:GetRadius()

  --self:F({ZoneCoord = ZoneCoord, ZoneRadius = ZoneRadius, ZoneCoordLL = ZoneCoord:ToStringLLDMS()})

  local SphereSearch = {
    id = world.VolumeType.SPHERE,
      params = {
      point = ZoneCoord:GetVec3(),
      radius = ZoneRadius,
      }
    }

  local function EvaluateZone( ZoneObject )
    --if ZoneObject:isExist() then --FF: isExist always returns false for SCENERY objects since DCS 2.2 and still in DCS 2.5
    if ZoneObject and self:IsVec3InZone(ZoneObject:getPoint()) then

      -- Get object category.
      local ObjectCategory = Object.getCategory(ZoneObject)

      if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then

        local Include = false
        if not UnitCategories then
          -- Anything found is included.
          Include = true
        else
          -- Check if found object is in specified categories.
          local CategoryDCSUnit = ZoneObject:getDesc().category

          for UnitCategoryID, UnitCategory in pairs( UnitCategories ) do
            if UnitCategory == CategoryDCSUnit then
              Include = true
              break
            end
          end

        end

        if Include then

          local CoalitionDCSUnit = ZoneObject:getCoalition()

          -- This coalition is inside the zone.
          self.ScanData.Coalitions[CoalitionDCSUnit] = true

          self.ScanData.Units[ZoneObject] = ZoneObject

          --self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } )
        end
      end

      if ObjectCategory == Object.Category.SCENERY then
        local SceneryType = ZoneObject:getTypeName()
        local SceneryName = ZoneObject:getName()
        --BASE:I("SceneryType "..SceneryType.." SceneryName "..tostring(SceneryName))
        self.ScanData.Scenery[SceneryType] = self.ScanData.Scenery[SceneryType] or {}
        self.ScanData.Scenery[SceneryType][SceneryName] = SCENERY:Register( tostring(SceneryName), ZoneObject)
        table.insert(self.ScanData.SceneryTable,self.ScanData.Scenery[SceneryType][SceneryName] )
        --self:T( { SCENERY =  self.ScanData.Scenery[SceneryType][SceneryName] } )
      end

    end

    return true
  end

  -- Search objects.
  world.searchObjects( ObjectCategories, SphereSearch, EvaluateZone )

end

--- Remove junk inside the zone using the `world.removeJunk` function.
-- @param #ZONE_RADIUS self
-- @return #number Number of deleted objects.
function ZONE_RADIUS:RemoveJunk()

  local radius=self.Radius
  local vec3=self:GetVec3()

  local volS = {
    id = world.VolumeType.SPHERE,
    params = {point = vec3, radius = radius}
  }

  local n=world.removeJunk(volS)

  return n
end

--- Get a table  of scanned units.
-- @param #ZONE_RADIUS self
-- @return #table Table of DCS units and DCS statics inside the zone.
function ZONE_RADIUS:GetScannedUnits()

  return self.ScanData.Units
end

--- Get a set of scanned units.
-- @param #ZONE_RADIUS self
-- @return Core.Set#SET_UNIT Set of units and statics inside the zone.
function ZONE_RADIUS:GetScannedSetUnit()

  local SetUnit = SET_UNIT:New()

  if self.ScanData then
    for ObjectID, UnitObject in pairs( self.ScanData.Units ) do
      local UnitObject = UnitObject -- DCS#Unit
      if UnitObject:isExist() then
        local FoundUnit = UNIT:FindByName( UnitObject:getName() )
        if FoundUnit then
          SetUnit:AddUnit( FoundUnit )
        else
          local FoundStatic = STATIC:FindByName( UnitObject:getName(), false )
          if FoundStatic then
            SetUnit:AddUnit( FoundStatic )
          end
        end
      end
    end
  end

  return SetUnit
end

--- Get a set of scanned groups.
-- @param #ZONE_RADIUS self
-- @return Core.Set#SET_GROUP Set of groups.
function ZONE_RADIUS:GetScannedSetGroup()

  self.ScanSetGroup=self.ScanSetGroup or SET_GROUP:New() --Core.Set#SET_GROUP

  self.ScanSetGroup.Set={}

  if self.ScanData then
    for ObjectID, UnitObject in pairs( self.ScanData.Units ) do
      local UnitObject = UnitObject -- DCS#Unit
      if UnitObject:isExist() then

        local FoundUnit=UNIT:FindByName(UnitObject:getName())
        if FoundUnit then
          local group=FoundUnit:GetGroup()
          self.ScanSetGroup:AddGroup(group)
        end
      end
    end
  end

  return self.ScanSetGroup
end

--- Count the number of different coalitions inside the zone.
-- @param #ZONE_RADIUS self
-- @return #number Counted coalitions.
function ZONE_RADIUS:CountScannedCoalitions()

  local Count = 0

  for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do
    Count = Count + 1
  end

  return Count
end

--- Check if a certain coalition is inside a scanned zone.
-- @param #ZONE_RADIUS self
-- @param #number Coalition The coalition id, e.g. coalition.side.BLUE.
-- @return #boolean If true, the coalition is inside the zone.
function ZONE_RADIUS:CheckScannedCoalition( Coalition )
  if Coalition then
    return self.ScanData.Coalitions[Coalition]
  end
  return nil
end

--- Get Coalitions of the units in the Zone, or Check if there are units of the given Coalition in the Zone.
-- Returns nil if there are none to two Coalitions in the zone!
-- Returns one Coalition if there are only Units of one Coalition in the Zone.
-- Returns the Coalition for the given Coalition if there are units of the Coalition in the Zone.
-- @param #ZONE_RADIUS self
-- @return #table
function ZONE_RADIUS:GetScannedCoalition( Coalition )

  if Coalition then
    return self.ScanData.Coalitions[Coalition]
  else
    local Count = 0
    local ReturnCoalition = nil

    for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do
      Count = Count + 1
      ReturnCoalition = CoalitionID
    end

    if Count ~= 1 then
      ReturnCoalition = nil
    end

    return ReturnCoalition
  end
end

--- Get scanned scenery type
-- @param #ZONE_RADIUS self
-- @return #table Table of DCS scenery type objects.
function ZONE_RADIUS:GetScannedSceneryType( SceneryType )
  return self.ScanData.Scenery[SceneryType]
end

--- Get scanned scenery table
-- @param #ZONE_RADIUS self
-- @return #table Structured object table: [type].[name].SCENERY
function ZONE_RADIUS:GetScannedScenery()
  return self.ScanData.Scenery
end

--- Get table of scanned scenery objects
-- @param #ZONE_RADIUS self
-- @return #table Table of SCENERY objects.
function ZONE_RADIUS:GetScannedSceneryObjects()
  return self.ScanData.SceneryTable
end

--- Get set of scanned scenery objects
-- @param #ZONE_RADIUS self
-- @return #table Table of Wrapper.Scenery#SCENERY scenery objects.
function ZONE_RADIUS:GetScannedSetScenery()
  local scenery = SET_SCENERY:New()
  local objects = self:GetScannedSceneryObjects()
  for _,_obj in pairs (objects) do
    scenery:AddScenery(_obj)
  end
  return scenery
end

--- Is All in Zone of Coalition?
-- Check if only the specified coalition is inside the zone and no one else.
-- @param #ZONE_RADIUS self
-- @param #number Coalition Coalition ID of the coalition which is checked to be the only one in the zone.
-- @return #boolean True, if **only** that coalition is inside the zone and no one else.
-- @usage
--    self.Zone:Scan()
--    local IsGuarded = self.Zone:IsAllInZoneOfCoalition( self.Coalition )
function ZONE_RADIUS:IsAllInZoneOfCoalition( Coalition )

  --self:E( { Coalitions = self.Coalitions, Count = self:CountScannedCoalitions() } )
  return self:CountScannedCoalitions() == 1 and self:GetScannedCoalition( Coalition ) == true
end

--- Is All in Zone of Other Coalition?
-- Check if only one coalition is inside the zone and the specified coalition is not the one.
-- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated!
-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set.
-- @param #ZONE_RADIUS self
-- @param #number Coalition Coalition ID of the coalition which is not supposed to be in the zone.
-- @return #boolean True, if and only if only one coalition is inside the zone and the specified coalition is not it.
-- @usage
--    self.Zone:Scan()
--    local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition )
function ZONE_RADIUS:IsAllInZoneOfOtherCoalition( Coalition )

  --self:E( { Coalitions = self.Coalitions, Count = self:CountScannedCoalitions() } )
  return self:CountScannedCoalitions() == 1 and self:GetScannedCoalition( Coalition ) == nil
end

--- Is Some in Zone of Coalition?
-- Check if more than one coalition is inside the zone and the specified coalition is one of them.
-- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated!
-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set.
-- @param #ZONE_RADIUS self
-- @param #number Coalition ID of the coalition which is checked to be inside the zone.
-- @return #boolean True if more than one coalition is inside the zone and the specified coalition is one of them.
-- @usage
--    self.Zone:Scan()
--    local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition )
function ZONE_RADIUS:IsSomeInZoneOfCoalition( Coalition )

  return self:CountScannedCoalitions() > 1 and self:GetScannedCoalition( Coalition ) == true
end

--- Is None in Zone of Coalition?
-- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated!
-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set.
-- @param #ZONE_RADIUS self
-- @param Coalition
-- @return #boolean
-- @usage
--    self.Zone:Scan()
--    local IsOccupied = self.Zone:IsNoneInZoneOfCoalition( self.Coalition )
function ZONE_RADIUS:IsNoneInZoneOfCoalition( Coalition )

  return self:GetScannedCoalition( Coalition ) == nil
end

--- Is None in Zone?
-- You first need to use the @{#ZONE_RADIUS.Scan} method to scan the zone before it can be evaluated!
-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set.
-- @param #ZONE_RADIUS self
-- @return #boolean
-- @usage
--    self.Zone:Scan()
--    local IsEmpty = self.Zone:IsNoneInZone()
function ZONE_RADIUS:IsNoneInZone()

  return self:CountScannedCoalitions() == 0
end

--- Searches the zone
-- @param #ZONE_RADIUS self
-- @param ObjectCategories A list of categories, which are members of Object.Category
-- @param EvaluateFunction
function ZONE_RADIUS:SearchZone( EvaluateFunction, ObjectCategories )

  local SearchZoneResult = true

  local ZoneCoord = self:GetCoordinate()
  local ZoneRadius = self:GetRadius()

  --self:F({ZoneCoord = ZoneCoord, ZoneRadius = ZoneRadius, ZoneCoordLL = ZoneCoord:ToStringLLDMS()})

  local SphereSearch = {
    id = world.VolumeType.SPHERE,
      params = {
      point = ZoneCoord:GetVec3(),
      radius = ZoneRadius,
      }
    }

  local function EvaluateZone( ZoneDCSUnit )


    local ZoneUnit = UNIT:Find( ZoneDCSUnit )

    return EvaluateFunction( ZoneUnit )
  end

  world.searchObjects( Object.Category.UNIT, SphereSearch, EvaluateZone )

end

--- Returns if a location is within the zone.
-- @param #ZONE_RADIUS self
-- @param DCS#Vec2 Vec2 The location to test.
-- @return #boolean true if the location is within the zone.
function ZONE_RADIUS:IsVec2InZone( Vec2 )
  --self:F2( Vec2 )

  if not Vec2 then return false end

  local ZoneVec2 = self:GetVec2()

  if ZoneVec2 then
    if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then
      return true
    end
  end

  return false
end

--- Returns if a point is within the zone.
-- @param #ZONE_RADIUS self
-- @param DCS#Vec3 Vec3 The point to test.
-- @return #boolean true if the point is within the zone.
function ZONE_RADIUS:IsVec3InZone( Vec3 )
  --self:F2( Vec3 )
  if not Vec3 then return false end
  local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } )

  return InZone
end

--- Search for clear ground spawn zones within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery.
-- @param #ZONE_RADIUS self
-- @param #number PosRadius Required clear radius around each position.
-- @param #number NumPositions Number of positions to find.
-- @return #table A table of DCS#Vec2 positions that are clear of map objects within the given PosRadius. nil if no clear positions are found.
function ZONE_RADIUS:GetClearZonePositions(PosRadius, NumPositions)
    return UTILS.GetClearZonePositions(self, PosRadius, NumPositions)
end


--- Search for a random clear ground spawn coordinate within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery.
-- @param #ZONE_RADIUS self
-- @param #number PosRadius (Optional) Required clear radius around each position. (Default is math.min(Radius/10, 200))
-- @param #number NumPositions (Optional) Number of positions to find. (Default 50)
-- @return Core.Point#COORDINATE A random coordinate for a clear zone. nil if no clear positions are found.
-- @return #number Assigned radius for the found zones. nil if no clear positions are found.
function ZONE_RADIUS:GetRandomClearZoneCoordinate(PosRadius, NumPositions)
    return UTILS.GetRandomClearZoneCoordinate(self, PosRadius, NumPositions)
end

--- Returns a random Vec2 location within the zone.
-- @param #ZONE_RADIUS self
-- @param #number inner (Optional) Minimal distance from the center of the zone. Default is 0.
-- @param #number outer (Optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone.
-- @param #table surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 100 times to find the right type!
-- @return DCS#Vec2 The random location within the zone.
function ZONE_RADIUS:GetRandomVec2(inner, outer, surfacetypes)

  local Vec2 = self:GetVec2()
  local _inner = inner or 0
  local _outer = outer or self:GetRadius()
  
  math.random()
  math.random()
  math.random()

  if surfacetypes and type(surfacetypes)~="table" then
    surfacetypes={surfacetypes}
  end

  local function _getpoint()
    local point = {}
    local angle = math.random() * math.pi * 2
    point.x = Vec2.x + math.cos(angle) * math.random(_inner, _outer)
    point.y = Vec2.y + math.sin(angle) * math.random(_inner, _outer)
    return point
  end

  local function _checkSurface(point)
    local stype=land.getSurfaceType(point)
    for _,sf in pairs(surfacetypes) do
      if sf==stype then
        return true
      end
    end
    return false
  end

  local point=_getpoint()

  if surfacetypes then
    local N=1 ; local Nmax=100 ; local gotit=false
    while gotit==false and N<=Nmax do
      gotit=_checkSurface(point)
      if gotit then
        --env.info(string.format("Got random coordinate with surface type %d after N=%d/%d iterations", land.getSurfaceType(point), N, Nmax))
      else
        point=_getpoint()
        N=N+1
      end
    end
  end

  return point
end

--- Returns a @{Core.Point#COORDINATE} object reflecting a random 2D location within the zone. Note that this is actually a @{Core.Point#COORDINATE} type object, and not a simple Vec2 table.
-- @param #ZONE_RADIUS self
-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0.
-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone.
-- @return Core.Point#COORDINATE The @{Core.Point#COORDINATE} object reflecting the random 3D location within the zone.
function ZONE_RADIUS:GetRandomPointVec2( inner, outer )
  --self:F( self.ZoneName, inner, outer )

  local PointVec2 = COORDINATE:NewFromVec2( self:GetRandomVec2( inner, outer ) )

  --self:T3( { PointVec2 } )

  return PointVec2
end

--- Returns Returns a random Vec3 location within the zone.
-- @param #ZONE_RADIUS self
-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0.
-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone.
-- @return DCS#Vec3 The random location within the zone.
function ZONE_RADIUS:GetRandomVec3( inner, outer )
  --self:F( self.ZoneName, inner, outer )

  local Vec2 = self:GetRandomVec2( inner, outer )

  --self:T3( { x = Vec2.x, y = self.y, z = Vec2.y } )

  return { x = Vec2.x, y = self.y, z = Vec2.y }
end


--- Returns a @{Core.Point#COORDINATE} object reflecting a random 3D location within the zone. Note that this is actually a @{Core.Point#COORDINATE} type object, and not a simple Vec3 table.
-- @param #ZONE_RADIUS self
-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0.
-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone.
-- @return Core.Point#COORDINATE The @{Core.Point#COORDINATE} object reflecting the random 3D location within the zone.
function ZONE_RADIUS:GetRandomPointVec3( inner, outer )
  --self:F( self.ZoneName, inner, outer )

  local PointVec3 = COORDINATE:NewFromVec2( self:GetRandomVec2( inner, outer ) )

  --self:T3( { PointVec3 } )

  return PointVec3
end


--- Returns a @{Core.Point#COORDINATE} object reflecting a random 3D location within the zone.
-- @param #ZONE_RADIUS self
-- @param #number inner (Optional) Minimal distance from the center of the zone in meters. Default is 0 m.
-- @param #number outer (Optional) Maximal distance from the outer edge of the zone in meters. Default is the radius of the zone.
-- @param #table surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 100 times to find the right type!
-- @return Core.Point#COORDINATE The random coordinate.
function ZONE_RADIUS:GetRandomCoordinate(inner, outer, surfacetypes)

  local vec2=self:GetRandomVec2(inner, outer, surfacetypes)

  local Coordinate = COORDINATE:NewFromVec2(vec2)

  return Coordinate
end

--- Returns a @{Core.Point#COORDINATE} object reflecting a random location within the zone where there are no **map objects** of type "Building".
-- Does not find statics you might have placed there. **Note** This might be quite CPU intensive, use with care.
-- @param #ZONE_RADIUS self
-- @param #number inner (Optional) Minimal distance from the center of the zone in meters. Default is 0m.
-- @param #number outer (Optional) Maximal distance from the outer edge of the zone in meters. Default is the radius of the zone.
-- @param #number distance (Optional) Minimum distance from any building coordinate. Defaults to 100m.
-- @param #boolean markbuildings (Optional) Place markers on found buildings (if any).
-- @param #boolean markfinal (Optional) Place marker on the final coordinate (if any).
-- @return Core.Point#COORDINATE The random coordinate or `nil` if cannot be found in 1000 iterations.
function ZONE_RADIUS:GetRandomCoordinateWithoutBuildings(inner,outer,distance,markbuildings,markfinal)

  local dist = distance or 100

  local objects = {}

  if self.ScanData and self.ScanData.Scenery then
    objects = self:GetScannedScenery()
  else
    self:Scan({Object.Category.SCENERY})
    objects = self:GetScannedScenery()
  end

  local T0 = timer.getTime()
  local T1 = timer.getTime()

  local buildings = {}
  local buildingzones = {}

  if self.ScanData and self.ScanData.BuildingCoordinates then
    buildings = self.ScanData.BuildingCoordinates
    buildingzones = self.ScanData.BuildingZones
  else
    -- build table of buildings coordinates
    for _,_object in pairs (objects) do
      for _,_scen in pairs (_object) do
         local scenery = _scen -- Wrapper.Scenery#SCENERY
         local description=scenery:GetDesc()
         if description and description.attributes and description.attributes.Buildings then
          if markbuildings then
            MARKER:New(scenery:GetCoordinate(),"Building"):ToAll()
          end
          buildings[#buildings+1] = scenery:GetCoordinate()
          local bradius = scenery:GetBoundingRadius() or dist
          local bzone = ZONE_RADIUS:New("Building-"..math.random(1,100000),scenery:GetVec2(),bradius,false)
          buildingzones[#buildingzones+1] = bzone
          --bzone:DrawZone(-1,{1,0,0},Alpha,FillColor,FillAlpha,1,ReadOnly)
         end
      end
    end
    self.ScanData.BuildingCoordinates = buildings
    self.ScanData.BuildingZones = buildingzones
  end

  -- max 1000 tries
  local rcoord = nil
  local found = true
  local iterations = 0

  for i=1,1000 do
    iterations = iterations + 1
    rcoord = self:GetRandomCoordinate(inner,outer)
    found = true
    for _,_coord in pairs (buildingzones) do
      local zone = _coord -- Core.Zone#ZONE_RADIUS
      -- keep >50m dist from buildings
      if zone:IsPointVec2InZone(rcoord) then
        found = false
        break
      end
    end
    if found then
      -- we have a winner!
      if markfinal then
        MARKER:New(rcoord,"FREE"):ToAll()
      end
      break
    end
  end

  if not found then
    -- max 1000 tries
    local rcoord = nil
    local found = true
    local iterations = 0

    for i=1,1000 do
      iterations = iterations + 1
      rcoord = self:GetRandomCoordinate(inner,outer)
      found = true
      for _,_coord in pairs (buildings) do
        local coord = _coord -- Core.Point#COORDINATE
        -- keep >50m dist from buildings
        if coord:Get3DDistance(rcoord) < dist then
          found = false
        end
      end
      if found then
        -- we have a winner!
        if markfinal then
          MARKER:New(rcoord,"FREE"):ToAll()
        end
        break
      end
    end
  end

  T1=timer.getTime()

  --self:T(string.format("Found a coordinate: %s | Iterations: %d | Time: %.3f",tostring(found),iterations,T1-T0))

  if found then return rcoord else return nil end

end

---
-- @type ZONE
-- @extends #ZONE_RADIUS


--- The ZONE class, defined by the zone name as defined within the Mission Editor.
-- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties.
--
-- ## ZONE constructor
--
--   * @{#ZONE.New}(): Constructor. This will search for a trigger zone with the name given, and will return for you a ZONE object.
--
-- ## Declare a ZONE directly in the DCS mission editor!
--
-- You can declare a ZONE using the DCS mission editor by adding a trigger zone in the mission editor.
--
-- Then during mission startup, when loading Moose.lua, this trigger zone will be detected as a ZONE declaration.
-- Within the background, a ZONE object will be created within the @{Core.Database}.
-- The ZONE name will be the trigger zone name.
--
-- So, you can search yourself for the ZONE object by using the @{#ZONE.FindByName}() method.
-- In this example, `local TriggerZone = ZONE:FindByName( "DefenseZone" )` would return the ZONE object
-- that was created at mission startup, and reference it into the `TriggerZone` local object.
--
-- Refer to mission `ZON-110` for a demonstration.
--
-- This is especially handy if you want to quickly setup a SET_ZONE...
-- So when you would declare `local SetZone = SET_ZONE:New():FilterPrefixes( "Defense" ):FilterStart()`,
-- then SetZone would contain the ZONE object `DefenseZone` as part of the zone collection,
-- without much scripting overhead!!!
--
--
-- @field #ZONE
ZONE = {
  ClassName="ZONE",
  }


--- Constructor of ZONE taking the zone name.
-- @param #ZONE self
-- @param #string ZoneName The name of the zone as defined within the mission editor.
-- @return #ZONE self
function ZONE:New( ZoneName )

  -- First try to find the zone in the DB.
  local zone=_DATABASE:FindZone(ZoneName)

  if zone then
    --env.info("FF found zone in DB")
    return zone
  end

  -- Get zone from DCS trigger function.
  local Zone = trigger.misc.getZone( ZoneName )

  -- Error!
  if not Zone then
    env.error( "ERROR: Zone " .. ZoneName .. " does not exist!" )
    return nil
  end

  -- Create a new ZONE_RADIUS.
  local self=BASE:Inherit( self, ZONE_RADIUS:New(ZoneName, {x=Zone.point.x, y=Zone.point.z}, Zone.radius, true))
  --self:F(ZoneName)

  -- Color of zone.
  self.Color={1, 0, 0, 0.15}

  -- DCS zone.
  self.Zone = Zone

  return self
end

--- Find a zone in the _DATABASE using the name of the zone.
-- @param #ZONE self
-- @param #string ZoneName The name of the zone.
-- @return #ZONE self
function ZONE:FindByName( ZoneName )

  local ZoneFound = _DATABASE:FindZone( ZoneName )
  return ZoneFound
end


---
-- @type ZONE_UNIT
-- @field Wrapper.Unit#UNIT ZoneUNIT
-- @extends Core.Zone#ZONE_RADIUS


--- # ZONE_UNIT class, extends @{#ZONE_RADIUS}
--
-- The ZONE_UNIT class defined by a zone attached to a @{Wrapper.Unit#UNIT} with a radius and optional offsets.
-- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties.
--
-- @field #ZONE_UNIT
ZONE_UNIT = {
  ClassName="ZONE_UNIT",
  }

--- Constructor to create a ZONE_UNIT instance, taking the zone name, a zone unit and a radius and optional offsets in X and Y directions.
-- @param #ZONE_UNIT self
-- @param #string ZoneName Name of the zone.
-- @param Wrapper.Unit#UNIT ZoneUNIT The unit as the center of the zone.
-- @param #number Radius The radius of the zone in meters.
-- @param #table Offset A table specifying the offset. The offset table may have the following elements:
--  dx The offset in X direction, +x is north.
--  dy The offset in Y direction, +y is east.
--  rho The distance of the zone from the unit
--  theta The azimuth of the zone relative to unit
--  relative_to_unit If true, theta is measured clockwise from unit's direction else clockwise from north. If using dx, dy setting this to true makes +x parallel to unit heading.
--  dx, dy OR rho, theta may be used, not both.
-- @return #ZONE_UNIT self
function ZONE_UNIT:New( ZoneName, ZoneUNIT, Radius, Offset)

  if Offset then
    -- check if the inputs was reasonable, either (dx, dy) or (rho, theta) can be given, else raise an exception.
    if (Offset.dx or Offset.dy) and (Offset.rho or Offset.theta) then
      error("Cannot use (dx, dy) with (rho, theta)")
    end
  end

  local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneUNIT:GetVec2(), Radius, true ) )

  if Offset then
    self.dy = Offset.dy or 0.0
    self.dx = Offset.dx or 0.0
    self.rho = Offset.rho or 0.0
    self.theta = (Offset.theta or 0.0) * math.pi / 180.0
    self.relative_to_unit = Offset.relative_to_unit or false
  end

  --self:F( { ZoneName, ZoneUNIT:GetVec2(), Radius } )

  self.ZoneUNIT = ZoneUNIT
  self.LastVec2 = ZoneUNIT:GetVec2()

  -- Zone objects are added to the _DATABASE and SET_ZONE objects.
  _EVENTDISPATCHER:CreateEventNewZone( self )

  return self
end

--- Updates the current location from a @{Wrapper.Group}.
-- @param #ZONE_UNIT self
-- @param Wrapper.Group#GROUP Group (optional) Update from this Unit, if nil, update from the UNIT this zone is based on.
-- @return self
function ZONE_UNIT:UpdateFromUnit(Unit)
  if Unit and Unit:IsAlive() then
    local vec2 = Unit:GetVec2()
    self.LastVec2 = vec2
  elseif self.ZoneUNIT and self.ZoneUNIT:IsAlive() then
    local ZoneVec2 = self.ZoneUNIT:GetVec2()
    self.LastVec2 = ZoneVec2
  end
  return self
end


--- Returns the current location of the @{Wrapper.Unit#UNIT}.
-- @param #ZONE_UNIT self
-- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Unit#UNIT}location and the offset, if any.
function ZONE_UNIT:GetVec2()
  --self:F2( self.ZoneName )

  local ZoneVec2 = self.ZoneUNIT:GetVec2()
  if ZoneVec2 then

    local heading
    if self.relative_to_unit then
        heading = ( self.ZoneUNIT:GetHeading() or 0.0 ) * math.pi / 180.0
      else
        heading = 0.0
    end

    -- update the zone position with the offsets.
    if (self.dx or self.dy) then

      -- use heading to rotate offset relative to unit using rotation matrix in 2D.
      -- see: https://en.wikipedia.org/wiki/Rotation_matrix
      ZoneVec2.x = ZoneVec2.x + self.dx * math.cos( -heading ) + self.dy * math.sin( -heading )
      ZoneVec2.y = ZoneVec2.y - self.dx * math.sin( -heading ) + self.dy * math.cos( -heading )
    end

    -- if using the polar coordinates
    if (self.rho or self.theta) then
       ZoneVec2.x = ZoneVec2.x + self.rho * math.cos( self.theta + heading )
       ZoneVec2.y = ZoneVec2.y + self.rho * math.sin( self.theta + heading )
    end

    self.LastVec2 = ZoneVec2
    return ZoneVec2
  else
    return self.LastVec2
  end

  --self:T2( { ZoneVec2 } )

  return nil
end

--- Returns a random location within the zone.
-- @param #ZONE_UNIT self
-- @return DCS#Vec2 The random location within the zone.
function ZONE_UNIT:GetRandomVec2()
  --self:F( self.ZoneName )

  local RandomVec2 = {}
  --local Vec2 = self.ZoneUNIT:GetVec2()  -- FF: This does not take care of the new offset feature!
  local Vec2 = self:GetVec2()

  if not Vec2 then
    Vec2 = self.LastVec2
  end

  local angle = math.random() * math.pi*2;
  RandomVec2.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius();
  RandomVec2.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius();

  --self:T( { RandomVec2 } )

  return RandomVec2
end

--- Returns the @{DCS#Vec3} of the ZONE_UNIT.
-- @param #ZONE_UNIT self
-- @param DCS#Distance Height The height to add to the land height where the center of the zone is located.
-- @return DCS#Vec3 The point of the zone.
function ZONE_UNIT:GetVec3( Height )
  --self:F2( self.ZoneName )

  Height = Height or 0

  local Vec2 = self:GetVec2()

  local Vec3 = { x = Vec2.x, y = land.getHeight( self:GetVec2() ) + Height, z = Vec2.y }

  --self:T2( { Vec3 } )

  return Vec3
end

---
-- @type ZONE_GROUP
-- @extends #ZONE_RADIUS


--- The ZONE_GROUP class defines by a zone around a @{Wrapper.Group#GROUP} with a radius. The current leader of the group defines the center of the zone.
-- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties.
--
-- @field #ZONE_GROUP
ZONE_GROUP = {
  ClassName="ZONE_GROUP",
  }

--- Constructor to create a ZONE_GROUP instance, taking the zone name, a zone @{Wrapper.Group#GROUP} and a radius.
-- @param #ZONE_GROUP self
-- @param #string ZoneName Name of the zone.
-- @param Wrapper.Group#GROUP ZoneGROUP The @{Wrapper.Group} as the center of the zone.
-- @param DCS#Distance Radius The radius of the zone.
-- @return #ZONE_GROUP self
function ZONE_GROUP:New( ZoneName, ZoneGROUP, Radius )
  local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneGROUP:GetVec2(), Radius, true ) )
  --self:F( { ZoneName, ZoneGROUP:GetVec2(), Radius } )

  self._.ZoneGROUP = ZoneGROUP
  self._.ZoneVec2Cache = self._.ZoneGROUP:GetVec2()

  -- Zone objects are added to the _DATABASE and SET_ZONE objects.
  _EVENTDISPATCHER:CreateEventNewZone( self )

  return self
end


--- Returns the current location of the @{Wrapper.Group}.
-- @param #ZONE_GROUP self
-- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location.
function ZONE_GROUP:GetVec2()
  --self:F( self.ZoneName )

  local ZoneVec2 = nil

  if self._.ZoneGROUP:IsAlive() then
    ZoneVec2 = self._.ZoneGROUP:GetVec2()
    self._.ZoneVec2Cache = ZoneVec2
  else
    ZoneVec2 = self._.ZoneVec2Cache
  end

  --self:T( { ZoneVec2 } )

  return ZoneVec2
end

--- Updates the current location from a @{Wrapper.Group}.
-- @param #ZONE_GROUP self
-- @param Wrapper.Group#GROUP Group (optional) Update from this Group, if nil, update from the GROUP this zone is based on.
-- @return self
function ZONE_GROUP:UpdateFromGroup(Group)
  if Group and Group:IsAlive() then
    local vec2 = Group:GetVec2()
    self.Vec2 = vec2
  elseif self._.ZoneGROUP and self._.ZoneGROUP:IsAlive() then
    local ZoneVec2 = self._.ZoneGROUP:GetVec2()
    self.Vec2 = ZoneVec2
    self._.ZoneVec2Cache = ZoneVec2
  end
  return self
end

--- Returns a random location within the zone of the @{Wrapper.Group}.
-- @param #ZONE_GROUP self
-- @return DCS#Vec2 The random location of the zone based on the @{Wrapper.Group} location.
function ZONE_GROUP:GetRandomVec2()
  --self:F( self.ZoneName )

  local Point = {}
  local Vec2 = self._.ZoneGROUP:GetVec2()

  local angle = math.random() * math.pi*2;
  Point.x = Vec2.x + math.cos( angle ) * math.random() * self:GetRadius();
  Point.y = Vec2.y + math.sin( angle ) * math.random() * self:GetRadius();

  --self:T( { Point } )

  return Point
end

--- Returns a @{Core.Point#COORDINATE} object reflecting a random 2D location within the zone. Note that this is actually a @{Core.Point#COORDINATE} type object, and not a simple Vec2 table.
-- @param #ZONE_GROUP self
-- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0.
-- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone.
-- @return Core.Point#COORDINATE The @{Core.Point#COORDINATE} object reflecting the random 3D location within the zone.
function ZONE_GROUP:GetRandomPointVec2( inner, outer )
  --self:F( self.ZoneName, inner, outer )

  local PointVec2 = COORDINATE:NewFromVec2( self:GetRandomVec2() )

  --self:T3( { PointVec2 } )

  return PointVec2
end


--- Ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Triangle.lua
--- This triangle "zone" is not really to be used on its own, it only serves as building blocks for
--- ZONE_POLYGON to accurately find a point inside a polygon; as well as getting the correct surface area of
--- a polygon.
-- @type _ZONE_TRIANGLE
-- @extends Core.Zone#ZONE_BASE

--- ## _ZONE_TRIANGLE class, extends @{#ZONE_BASE}
--
-- _ZONE_TRIANGLE class is a helper class for ZONE_POLYGON
-- This class implements the inherited functions from @{#ZONE_BASE} taking into account the own zone format and properties.
--
-- @field #_ZONE_TRIANGLE
_ZONE_TRIANGLE = {
    ClassName="ZONE_TRIANGLE",
    Points={},
    Coords={},
    CenterVec2={x=0, y=0},
    SurfaceArea=0,
    DrawID={}
}
---
-- @param #_ZONE_TRIANGLE self
-- @param DCS#Vec p1
-- @param DCS#Vec p2
-- @param DCS#Vec p3
-- @return #_ZONE_TRIANGLE self
function _ZONE_TRIANGLE:New(p1, p2, p3)
    local self = BASE:Inherit(self, ZONE_BASE:New())
    self.Points = {p1, p2, p3}

    local center_x = (p1.x + p2.x + p3.x) / 3
    local center_y = (p1.y + p2.y + p3.y) / 3
    self.CenterVec2 = {x=center_x, y=center_y}

    for _, pt in pairs({p1, p2, p3}) do
        table.add(self.Coords, COORDINATE:NewFromVec2(pt))
    end

    self.SurfaceArea = math.abs((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)) * 0.5
    
    return self
end

--- Checks if a point is contained within the triangle.
-- @param #_ZONE_TRIANGLE self
-- @param #table pt The point to check
-- @param #table points (optional) The points of the triangle, or 3 other points if you're just using the TRIANGLE class without an object of it
-- @return #boolean True if the point is contained, false otherwise
function _ZONE_TRIANGLE:ContainsPoint(pt, points)
    points = points or self.Points

    local function sign(p1, p2, p3)
        return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y)
    end

    local d1 = sign(pt, self.Points[1], self.Points[2])
    local d2 = sign(pt, self.Points[2], self.Points[3])
    local d3 = sign(pt, self.Points[3], self.Points[1])

    local has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0)
    local has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0)

    return not (has_neg and has_pos)
end

--- Returns a random Vec2 within the triangle.
-- @param #_ZONE_TRIANGLE self
-- @param #table points The points of the triangle, or 3 other points if you're just using the TRIANGLE class without an object of it
-- @return #table The random Vec2
function _ZONE_TRIANGLE:GetRandomVec2(points)
    points = points or self.Points
    local pt = {math.random(), math.random()}
    table.sort(pt)
    local s = pt[1]
    local t = pt[2] - pt[1]
    local u = 1 - pt[2]

    return {x = s * points[1].x + t * points[2].x + u * points[3].x,
            y = s * points[1].y + t * points[2].y + u * points[3].y}
end

--- Draw the triangle
-- @param #_ZONE_TRIANGLE self
-- @return #table of draw IDs
function _ZONE_TRIANGLE:Draw(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly)
    Coalition=Coalition or -1

    Color=Color or {1, 0, 0 }
    Alpha=Alpha or 1

    FillColor=FillColor or Color
    if not FillColor then UTILS.DeepCopy(Color) end
    FillAlpha=FillAlpha or Alpha
    if not FillAlpha then FillAlpha=1 end

    for i=1, #self.Coords do
        local c1 = self.Coords[i]
        local c2 = self.Coords[i % #self.Coords + 1]
        local id = c1:LineToAll(c2, Coalition, Color, Alpha, LineType, ReadOnly)
        self.DrawID[#self.DrawID+1] = id
    end
    local newID = self.Coords[1]:MarkupToAllFreeForm({self.Coords[2],self.Coords[3]},Coalition,Color,Alpha,FillColor,FillAlpha,LineType,ReadOnly)
    self.DrawID[#self.DrawID+1] = newID
    return self.DrawID
end

--- Draw the triangle
-- @param #_ZONE_TRIANGLE self
-- @return #table of draw IDs
function _ZONE_TRIANGLE:Fill(Coalition, FillColor, FillAlpha, ReadOnly)
    Coalition=Coalition or -1
    FillColor = FillColor
    FillAlpha = FillAlpha
    local newID = self.Coords[1]:MarkupToAllFreeForm({self.Coords[2],self.Coords[3]},Coalition,nil,nil,FillColor,FillAlpha,0,nil)
    self.DrawID[#self.DrawID+1] = newID
    return self.DrawID
end


---
-- @type ZONE_POLYGON_BASE
-- @field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCS#Vec2}.
-- @field #number SurfaceArea
-- @field #table DrawID
-- @field #table FillTriangles
-- @field #table _Triangles
-- @field #table Borderlines
-- @extends #ZONE_BASE


--- The ZONE_POLYGON_BASE class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon.
-- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties.
-- This class is an abstract BASE class for derived classes, and is not meant to be instantiated.
--
-- ## Zone point randomization
--
-- Various functions exist to find random points within the zone.
--
--   * @{#ZONE_POLYGON_BASE.GetRandomVec2}(): Gets a random 2D point in the zone.
--   * @{#ZONE_POLYGON_BASE.GetRandomPointVec2}(): Return a @{Core.Point#COORDINATE} object representing a random 2D point within the zone.
--   * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Core.Point#COORDINATE} object representing a random 3D point at landheight within the zone.
--
-- ## Draw zone
--
--   * @{#ZONE_POLYGON_BASE.DrawZone}(): Draws the zone on the F10 map.
--   * @{#ZONE_POLYGON_BASE.Boundary}(): Draw a frontier on the F10 map with small filled circles.
--
--
-- @field #ZONE_POLYGON_BASE
ZONE_POLYGON_BASE = {
  ClassName="ZONE_POLYGON_BASE",
  _Triangles={}, -- #table of #_ZONE_TRIANGLE
  SurfaceArea=0,
  DrawID={}, -- making a table out of the MarkID so its easier to draw an n-sided polygon, see ZONE_POLYGON_BASE:Draw()
  FillTriangles = {},
  Borderlines = {},
}

--- A 2D points array.
-- @type ZONE_POLYGON_BASE.ListVec2
-- @list <DCS#Vec2> Table of 2D vectors.

--- A 3D points array.
-- @type ZONE_POLYGON_BASE.ListVec3
-- @list <DCS#Vec3> Table of 3D vectors.

--- Constructor to create a ZONE_POLYGON_BASE instance, taking the zone name and an array of @{DCS#Vec2}, forming a polygon.
-- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected.
-- @param #ZONE_POLYGON_BASE self
-- @param #string ZoneName Name of the zone.
-- @param #ZONE_POLYGON_BASE.ListVec2 PointsArray An array of @{DCS#Vec2}, forming a polygon.
-- @return #ZONE_POLYGON_BASE self
function ZONE_POLYGON_BASE:New( ZoneName, PointsArray )

  -- Inherit ZONE_BASE.
  local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) )
  --self:F( { ZoneName, PointsArray } )

  if PointsArray then

    self._.Polygon = {}

    for i = 1, #PointsArray do
      self._.Polygon[i] = {}
      self._.Polygon[i].x = PointsArray[i].x
      self._.Polygon[i].y = PointsArray[i].y
    end

    -- triangulate the polygon so we can work with it
    self._Triangles = self:_Triangulate()
    -- set the polygon's surface area
    self.SurfaceArea = self:_CalculateSurfaceArea()

  end

  return self
end

--- Triangulates the polygon.
--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua
-- @param #ZONE_POLYGON_BASE self
-- @return #table The #_ZONE_TRIANGLE list that makes up the polygon
function ZONE_POLYGON_BASE:_Triangulate()
    local points = self._.Polygon
    local triangles = {}

    local function get_orientation(shape_points)
        local sum = 0
        for i = 1, #shape_points do
            local j = i % #shape_points + 1
            sum = sum + (shape_points[j].x - shape_points[i].x) * (shape_points[j].y + shape_points[i].y)
        end
        return sum >= 0 and "clockwise" or "counter-clockwise" -- sum >= 0, return "clockwise", else return "counter-clockwise"
    end

    local function ensure_clockwise(shape_points)
        local orientation = get_orientation(shape_points)
        if orientation == "counter-clockwise" then
            -- Reverse the order of shape_points so they're clockwise
            local reversed = {}
            for i = #shape_points, 1, -1 do
                table.insert(reversed, shape_points[i])
            end
            return reversed
        end
        return shape_points
    end

    local function is_clockwise(p1, p2, p3)
        local cross_product = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)
        return cross_product < 0
    end

    local function divide_recursively(shape_points)
        if #shape_points == 3 then
            table.insert(triangles, _ZONE_TRIANGLE:New(shape_points[1], shape_points[2], shape_points[3]))
        elseif #shape_points > 3 then  -- find an ear -> a triangle with no other points inside it
            for i, p1 in ipairs(shape_points) do
                local p2 = shape_points[(i % #shape_points) + 1]
                local p3 = shape_points[(i + 1) % #shape_points + 1]
                local triangle = _ZONE_TRIANGLE:New(p1, p2, p3)
                local is_ear = true

                if not is_clockwise(p1, p2, p3) then
                    is_ear = false
                else
                    for _, point in ipairs(shape_points) do
                        if point ~= p1 and point ~= p2 and point ~= p3 and triangle:ContainsPoint(point) then
                            is_ear = false
                            break
                        end
                    end
                end

                if is_ear then
                    -- Check if any point in the original polygon is inside the ear triangle
                    local is_valid_triangle = true
                    for _, point in ipairs(points) do
                        if point ~= p1 and point ~= p2 and point ~= p3 and triangle:ContainsPoint(point) then
                            is_valid_triangle = false
                            break
                        end
                    end
                    if is_valid_triangle then
                        table.insert(triangles, triangle)
                        local remaining_points = {}
                        for j, point in ipairs(shape_points) do
                            if point ~= p2 then
                                table.insert(remaining_points, point)
                            end
                        end
                        divide_recursively(remaining_points)
                        break
                    end
                else

                end
            end
        end
    end

    points = ensure_clockwise(points)
    divide_recursively(points)
    return triangles
end

--- Update polygon points with an array of @{DCS#Vec2}.
-- @param #ZONE_POLYGON_BASE self
-- @param #ZONE_POLYGON_BASE.ListVec2 Vec2Array An array of @{DCS#Vec2}, forming a polygon.
-- @return #ZONE_POLYGON_BASE self
function ZONE_POLYGON_BASE:UpdateFromVec2(Vec2Array)

  self._.Polygon = {}

  for i=1,#Vec2Array do
    self._.Polygon[i] = {}
    self._.Polygon[i].x=Vec2Array[i].x
    self._.Polygon[i].y=Vec2Array[i].y
  end

  -- triangulate the polygon so we can work with it
  self._Triangles = self:_Triangulate()
  -- set the polygon's surface area
  self.SurfaceArea = self:_CalculateSurfaceArea()
  return self
end

--- Update polygon points with an array of @{DCS#Vec3}.
-- @param #ZONE_POLYGON_BASE self
-- @param #ZONE_POLYGON_BASE.ListVec3 Vec2Array An array of @{DCS#Vec3}, forming a polygon.
-- @return #ZONE_POLYGON_BASE self
function ZONE_POLYGON_BASE:UpdateFromVec3(Vec3Array)

  self._.Polygon = {}

  for i=1,#Vec3Array do
    self._.Polygon[i] = {}
    self._.Polygon[i].x=Vec3Array[i].x
    self._.Polygon[i].y=Vec3Array[i].z
  end

  -- triangulate the polygon so we can work with it
  self._Triangles = self:_Triangulate()
  -- set the polygon's surface area
  self.SurfaceArea = self:_CalculateSurfaceArea()
  return self
end

--- Calculates the surface area of the polygon. The surface area is the sum of the areas of the triangles that make up the polygon.
--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua
-- @param #ZONE_POLYGON_BASE self
-- @return #number The surface area of the polygon
function ZONE_POLYGON_BASE:_CalculateSurfaceArea()
    local area = 0
    for _, triangle in pairs(self._Triangles) do
        area = area + triangle.SurfaceArea
    end
    return area
end

--- Returns the center location of the polygon.
-- @param #ZONE_POLYGON_BASE self
-- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} location.
function ZONE_POLYGON_BASE:GetVec2()
  --self:F( self.ZoneName )

  local Bounds = self:GetBoundingSquare()

  return { x = ( Bounds.x2 + Bounds.x1 ) / 2, y = ( Bounds.y2 + Bounds.y1 ) / 2 }
end

--- Get a vertex of the polygon.
-- @param #ZONE_POLYGON_BASE self
-- @param #number Index Index of the vertex. Default 1.
-- @return DCS#Vec2 Vertex of the polygon.
function ZONE_POLYGON_BASE:GetVertexVec2(Index)
  return self._.Polygon[Index or 1]
end

--- Get a vertex of the polygon.
-- @param #ZONE_POLYGON_BASE self
-- @param #number Index Index of the vertex. Default 1.
-- @return DCS#Vec3 Vertex of the polygon.
function ZONE_POLYGON_BASE:GetVertexVec3(Index)
  local vec2=self:GetVertexVec2(Index)
  if vec2 then
    local vec3={x=vec2.x, y=land.getHeight(vec2), z=vec2.y}
    return vec3
  end
  return nil
end

--- Get a vertex of the polygon.
-- @param #ZONE_POLYGON_BASE self
-- @param #number Index Index of the vertex. Default 1.
-- @return Core.Point#COORDINATE Vertex of the polygon.
function ZONE_POLYGON_BASE:GetVertexCoordinate(Index)
  local vec2=self:GetVertexVec2(Index)
  if vec2 then
    local coord=COORDINATE:NewFromVec2(vec2)
    return coord
  end
  return nil
end


--- Get a list of verticies of the polygon.
-- @param #ZONE_POLYGON_BASE self
-- @return <DCS#Vec2> List of DCS#Vec2 verticies defining the edges of the polygon.
function ZONE_POLYGON_BASE:GetVerticiesVec2()
  return self._.Polygon
end

--- Get a list of verticies of the polygon.
-- @param #ZONE_POLYGON_BASE self
-- @return #table List of DCS#Vec3 verticies defining the edges of the polygon.
function ZONE_POLYGON_BASE:GetVerticiesVec3()

  local coords={}

  for i,vec2 in ipairs(self._.Polygon) do
    local vec3={x=vec2.x, y=land.getHeight(vec2), z=vec2.y}
    table.insert(coords, vec3)
  end

  return coords
end

--- Get a list of verticies of the polygon.
-- @param #ZONE_POLYGON_BASE self
-- @return #table List of COORDINATES verticies defining the edges of the polygon.
function ZONE_POLYGON_BASE:GetVerticiesCoordinates()

  local coords={}

  for i,vec2 in ipairs(self._.Polygon) do
    local coord=COORDINATE:NewFromVec2(vec2)
    table.insert(coords, coord)
  end

  return coords
end

--- Flush polygon coordinates as a table in DCS.log.
-- @param #ZONE_POLYGON_BASE self
-- @return #ZONE_POLYGON_BASE self
function ZONE_POLYGON_BASE:Flush()
  --self:F2()

  --self:F( { Polygon = self.ZoneName, Coordinates = self._.Polygon } )

  return self
end

--- Search for clear ground spawn zones within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery.
-- @param #ZONE_POLYGON_BASE self
-- @param #number PosRadius Required clear radius around each position.
-- @param #number NumPositions Number of positions to find.
-- @return #table A table of DCS#Vec2 positions that are clear of map objects within the given PosRadius. nil if no clear positions are found.
function ZONE_POLYGON_BASE:GetClearZonePositions(PosRadius, NumPositions)
    return UTILS.GetClearZonePositions(self, PosRadius, NumPositions)
end


--- Search for a random clear ground spawn coordinate within this zone. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery.
-- @param #ZONE_POLYGON_BASE self
-- @param #number PosRadius (Optional) Required clear radius around each position. (Default is math.min(Radius/10, 200))
-- @param #number NumPositions (Optional) Number of positions to find. (Default 50)
-- @return Core.Point#COORDINATE A random coordinate for a clear zone. nil if no clear positions are found.
-- @return #number Assigned radius for the found zones. nil if no clear positions are found.
function ZONE_POLYGON_BASE:GetRandomClearZoneCoordinate(PosRadius, NumPositions)
    return UTILS.GetRandomClearZoneCoordinate(self, PosRadius, NumPositions)
end

--- Smokes the zone boundaries in a color.
-- @param #ZONE_POLYGON_BASE self
-- @param #boolean UnBound If true, the tyres will be destroyed.
-- @return #ZONE_POLYGON_BASE self
function ZONE_POLYGON_BASE:BoundZone( UnBound )

  local i
  local j
  local Segments = 10

  i = 1
  j = #self._.Polygon

  while i <= #self._.Polygon do
    --self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } )

    local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x
    local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y

    for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line.
      local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments )
      local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments )
      local Tire = {
          ["country"] = "USA",
          ["category"] = "Fortifications",
          ["canCargo"] = false,
          ["shape_name"] = "H-tyre_B_WF",
          ["type"] = "Black_Tyre_WF",
          ["y"] = PointY,
          ["x"] = PointX,
          ["name"] = string.format( "%s-Tire #%0d", self:GetName(), ((i - 1) * Segments) + Segment ),
          ["heading"] = 0,
      } -- end of ["group"]

      local Group = coalition.addStaticObject( country.id.USA, Tire )
      if UnBound and UnBound == true then
        Group:destroy()
      end

    end
    j = i
    i = i + 1
  end

  return self
end

--- Draw the zone on the F10 map.  Infinite number of points supported
--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua
-- @param #ZONE_POLYGON_BASE self
-- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All.
-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red.
-- @param #number Alpha Transparency [0,1]. Default 1.
-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. -- doesn't seem to work
-- @param #number FillAlpha Transparency [0,1]. Default 0.15.                                                 -- doesn't seem to work
-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.s
-- @return #ZONE_POLYGON_BASE self
function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, IncludeTriangles)

  
  if self._.Polygon and #self._.Polygon >= 3 then
    Coalition = Coalition or self:GetDrawCoalition()
  
    -- Set draw coalition.
    self:SetDrawCoalition(Coalition)
  
    Color = Color or self:GetColorRGB()
    Alpha = Alpha or self:GetColorAlpha()
  
    FillColor = FillColor or self:GetFillColorRGB()
    FillAlpha = FillAlpha or self:GetFillColorAlpha()
  
    if FillColor then
      self:ReFill(FillColor,FillAlpha)  
    end
    
    if Color then
        self:ReDrawBorderline(Color,Alpha,LineType)
    end
  end


  if false then
    local coords = self:GetVerticiesCoordinates()

    local coord=coords[1] --Core.Point#COORDINATE

    table.remove(coords, 1)

    coord:MarkupToAllFreeForm(coords, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, "Drew Polygon")

    if true then
      return
    end
  end

  return self
end

--- Change/Re-fill a Polygon Zone
-- @param #ZONE_POLYGON_BASE self
-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red.
-- @param #number Alpha Transparency [0,1]. Default 1.
-- @return #ZONE_POLYGON_BASE self
function ZONE_POLYGON_BASE:ReFill(Color,Alpha)
  local color = Color or self:GetFillColorRGB() or {1,0,0}
  local alpha = Alpha or self:GetFillColorAlpha() or 1
  local coalition = self:GetDrawCoalition() or -1
  -- undraw if already filled
  if #self.FillTriangles > 0 then
    for _, triangle in pairs(self._Triangles) do
      triangle:UndrawZone()
    end
    -- remove mark IDs
    for _,_value in pairs(self.FillTriangles) do
      table.remove_by_value(self.DrawID, _value)
    end
    self.FillTriangles = nil
    self.FillTriangles = {}
  end
  -- refill
  for _,triangle in pairs(self._Triangles) do
      local draw_ids = triangle:Fill(coalition,color,alpha,nil)
      self.FillTriangles = draw_ids
      table.combine(self.DrawID, draw_ids)
  end
  return self
end

--- Change/Re-draw the border of a Polygon Zone
-- @param #ZONE_POLYGON_BASE self
-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red.
-- @param #number Alpha Transparency [0,1]. Default 1.
-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
-- @return #ZONE_POLYGON_BASE
function ZONE_POLYGON_BASE:ReDrawBorderline(Color, Alpha, LineType)
  local color = Color or self:GetFillColorRGB() or {1,0,0}
  local alpha = Alpha or self:GetFillColorAlpha() or 1
  local coalition = self:GetDrawCoalition() or -1
  local linetype = LineType or 1
    -- undraw if already drawn
  if #self.Borderlines > 0 then
    for _, MarkID in pairs(self.Borderlines) do
      trigger.action.removeMark(MarkID)
    end
    -- remove mark IDs
    for _,_value in pairs(self.Borderlines) do
      table.remove_by_value(self.DrawID, _value)
    end
    self.Borderlines = nil
    self.Borderlines = {}
  end
  -- Redraw border
  local coords = self:GetVerticiesCoordinates()
  for i = 1, #coords do
      local c1 = coords[i]
      local c2 = coords[i % #coords + 1]
      local newID = c1:LineToAll(c2, coalition, color, alpha, linetype, nil)
      self.DrawID[#self.DrawID+1]=newID
      self.Borderlines[#self.Borderlines+1] = newID
  end
  return self
end

--- Get the surface area of this polygon
-- @param #ZONE_POLYGON_BASE self
-- @return #number Surface area
function ZONE_POLYGON_BASE:GetSurfaceArea()
  return self.SurfaceArea
end



--- Get the smallest radius encompassing all points of the polygon zone.
-- @param #ZONE_POLYGON_BASE self
-- @return #number Radius of the zone in meters.
function ZONE_POLYGON_BASE:GetRadius()

  local center=self:GetVec2()

  local radius=0

  for _,_vec2 in pairs(self._.Polygon) do
    local vec2=_vec2 --DCS#Vec2

    local r=UTILS.VecDist2D(center, vec2)

    if r>radius then
      radius=r
    end

  end

  return radius
end

--- Get the smallest circular zone encompassing all points of the polygon zone.
-- @param #ZONE_POLYGON_BASE self
-- @param #string ZoneName (Optional) Name of the zone. Default is the name of the polygon zone.
-- @param #boolean DoNotRegisterZone (Optional) If `true`, zone is not registered.
-- @return #ZONE_RADIUS The circular zone.
function ZONE_POLYGON_BASE:GetZoneRadius(ZoneName, DoNotRegisterZone)

  local center=self:GetVec2()

  local radius=self:GetRadius()

  local zone=ZONE_RADIUS:New(ZoneName or self.ZoneName, center, radius, DoNotRegisterZone)

  return zone
end


--- Get the smallest rectangular zone encompassing all points points of the polygon zone.
-- @param #ZONE_POLYGON_BASE self
-- @param #string ZoneName (Optional) Name of the zone. Default is the name of the polygon zone.
-- @param #boolean DoNotRegisterZone (Optional) If `true`, zone is not registered.
-- @return #ZONE_POLYGON The rectangular zone.
function ZONE_POLYGON_BASE:GetZoneQuad(ZoneName, DoNotRegisterZone)

  local vec1, vec3=self:GetBoundingVec2()

  local vec2={x=vec1.x, y=vec3.y}
  local vec4={x=vec3.x, y=vec1.y}

  local zone=ZONE_POLYGON_BASE:New(ZoneName or self.ZoneName, {vec1, vec2, vec3, vec4})

  return zone
end

--- Remove junk inside the zone. Due to DCS limitations, this works only for rectangular zones. So we get the smallest rectangular zone encompassing all points points of the polygon zone.
-- @param #ZONE_POLYGON_BASE self
-- @param #number Height Height of the box in meters. Default 1000.
-- @return #number Number of removed objects.
function ZONE_POLYGON_BASE:RemoveJunk(Height)

  Height=Height or 1000

  local vec2SW, vec2NE=self:GetBoundingVec2()

  local vec3SW={x=vec2SW.x, y=-Height, z=vec2SW.y} --DCS#Vec3
  local vec3NE={x=vec2NE.x, y= Height, z=vec2NE.y} --DCS#Vec3

  --local coord1=COORDINATE:NewFromVec3(vec3SW):MarkToAll("SW")
  --local coord1=COORDINATE:NewFromVec3(vec3NE):MarkToAll("NE")

  local volume = {
    id = world.VolumeType.BOX,
    params = {
      min=vec3SW,
      max=vec3SW
    }
  }

  local n=world.removeJunk(volume)

  return n
end

--- Smokes the zone boundaries in a color.
-- @param #ZONE_POLYGON_BASE self
-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color.
-- @param #number Segments (Optional) Number of segments within boundary line. Default 10.
-- @return #ZONE_POLYGON_BASE self
function ZONE_POLYGON_BASE:SmokeZone( SmokeColor, Segments )
  --self:F2( SmokeColor )

  Segments=Segments or 10

  local i=1
  local j=#self._.Polygon

  while i <= #self._.Polygon do
    --self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } )

    local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x
    local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y

    for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line.
      local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments )
      local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments )
      COORDINATE:New( PointX, 0, PointY ):Smoke( SmokeColor )
    end
    j = i
    i = i + 1
  end

  return self
end

--- Flare the zone boundaries in a color.
-- @param #ZONE_POLYGON_BASE self
-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color.
-- @param #number Segments (Optional) Number of segments within boundary line. Default 10.
-- @param DCS#Azimuth Azimuth (optional) Azimuth The azimuth of the flare.
-- @param #number AddHeight (optional) The height to be added for the smoke.
-- @return #ZONE_POLYGON_BASE self
function ZONE_POLYGON_BASE:FlareZone( FlareColor, Segments, Azimuth, AddHeight )
  --self:F2(FlareColor)

  Segments=Segments or 10

  AddHeight = AddHeight or 0

  local i=1
  local j=#self._.Polygon

  while i <= #self._.Polygon do
    --self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } )

    local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x
    local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y

    for Segment = 0, Segments do -- We divide each line in 5 segments and smoke a point on the line.
      local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments )
      local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments )
      COORDINATE:New( PointX, AddHeight, PointY ):Flare(FlareColor, Azimuth)
    end
    j = i
    i = i + 1
  end

  return self
end

--- Returns if a location is within the zone.
-- Source learned and taken from: https://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
-- @param #ZONE_POLYGON_BASE self
-- @param DCS#Vec2 Vec2 The location to test.
-- @return #boolean true if the location is within the zone.
function ZONE_POLYGON_BASE:IsVec2InZone( Vec2 )
  --self:F2( Vec2 )
  if not Vec2 then return false end
  local Next
  local Prev
  local InPolygon = false

  Next = 1
  Prev = #self._.Polygon

  while Next <= #self._.Polygon do
    --self:T( { Next, Prev, self._.Polygon[Next], self._.Polygon[Prev] } )
    if ( ( ( self._.Polygon[Next].y > Vec2.y ) ~= ( self._.Polygon[Prev].y > Vec2.y ) ) and
         ( Vec2.x < ( self._.Polygon[Prev].x - self._.Polygon[Next].x ) * ( Vec2.y - self._.Polygon[Next].y ) / ( self._.Polygon[Prev].y - self._.Polygon[Next].y ) + self._.Polygon[Next].x )
       ) then
       InPolygon = not InPolygon
    end
    --self:T2( { InPolygon = InPolygon } )
    Prev = Next
    Next = Next + 1
  end

  --self:T( { InPolygon = InPolygon } )
  return InPolygon
end

--- Returns if a point is within the zone.
-- @param #ZONE_POLYGON_BASE self
-- @param DCS#Vec3 Vec3 The point to test.
-- @return #boolean true if the point is within the zone.
function ZONE_POLYGON_BASE:IsVec3InZone( Vec3 )
  --self:F2( Vec3 )

  if not Vec3 then return false end

  local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } )

  return InZone
end

--- Define a random @{DCS#Vec2} within the zone.
--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Polygon.lua
-- @param #ZONE_POLYGON_BASE self
-- @return DCS#Vec2 The Vec2 coordinate.
function ZONE_POLYGON_BASE:GetRandomVec2()
    -- make sure we assign weights to the triangles based on their surface area, otherwise
    -- we'll be more likely to generate random points in smaller triangles
    
    math.random()
    math.random()
    math.random()
    
    local weights = {}
    for _, triangle in pairs(self._Triangles) do
        weights[triangle] = triangle.SurfaceArea / self.SurfaceArea
    end

    local random_weight = math.random()
    local accumulated_weight = 0
    for triangle, weight in pairs(weights) do
        accumulated_weight = accumulated_weight + weight
        if accumulated_weight >= random_weight then
            return triangle:GetRandomVec2()
        end
    end
end

--- Return a @{Core.Point#COORDINATE} object representing a random 2D point at landheight within the zone. Note that this is actually a @{Core.Point#COORDINATE} type object, and not a simple Vec2 table.
-- @param #ZONE_POLYGON_BASE self
-- @return @{Core.Point#COORDINATE}
function ZONE_POLYGON_BASE:GetRandomPointVec2()
  --self:F2()

  local PointVec2 = COORDINATE:NewFromVec2( self:GetRandomVec2() )

  --self:T2( PointVec2 )

  return PointVec2
end

--- Return a @{Core.Point#COORDINATE} object representing a random 3D point at landheight within the zone. Note that this is actually a @{Core.Point#COORDINATE} type object, and not a simple Vec3 table.
-- @param #ZONE_POLYGON_BASE self
-- @return @{Core.Point#COORDINATE}
function ZONE_POLYGON_BASE:GetRandomPointVec3()
  --self:F2()

  local PointVec3 = COORDINATE:NewFromVec2( self:GetRandomVec2() )

  --self:T2( PointVec3 )

  return PointVec3
end


--- Return a @{Core.Point#COORDINATE} object representing a random 3D point at landheight within the zone.
-- @param #ZONE_POLYGON_BASE self
-- @return Core.Point#COORDINATE
function ZONE_POLYGON_BASE:GetRandomCoordinate()
  --self:F2()

  local Coordinate = COORDINATE:NewFromVec2( self:GetRandomVec2() )

  --self:T2( Coordinate )

  return Coordinate
end


--- Get the bounding square the zone.
-- @param #ZONE_POLYGON_BASE self
-- @return #ZONE_POLYGON_BASE.BoundingSquare The bounding square.
function ZONE_POLYGON_BASE:GetBoundingSquare()

  local x1 = self._.Polygon[1].x
  local y1 = self._.Polygon[1].y
  local x2 = self._.Polygon[1].x
  local y2 = self._.Polygon[1].y

  for i = 2, #self._.Polygon do
    --self:T2( { self._.Polygon[i], x1, y1, x2, y2 } )
    x1 = ( x1 > self._.Polygon[i].x ) and self._.Polygon[i].x or x1
    x2 = ( x2 < self._.Polygon[i].x ) and self._.Polygon[i].x or x2
    y1 = ( y1 > self._.Polygon[i].y ) and self._.Polygon[i].y or y1
    y2 = ( y2 < self._.Polygon[i].y ) and self._.Polygon[i].y or y2

  end

  return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 }
end

--- Get the bounding 2D vectors of the polygon.
-- @param #ZONE_POLYGON_BASE self
-- @return DCS#Vec2 Coordinates of western-southern-lower vertex of the box.
-- @return DCS#Vec2 Coordinates of eastern-northern-upper vertex of the box.
function ZONE_POLYGON_BASE:GetBoundingVec2()

  local x1 = self._.Polygon[1].x
  local y1 = self._.Polygon[1].y
  local x2 = self._.Polygon[1].x
  local y2 = self._.Polygon[1].y

  for i = 2, #self._.Polygon do
    --self:T2( { self._.Polygon[i], x1, y1, x2, y2 } )
    x1 = ( x1 > self._.Polygon[i].x ) and self._.Polygon[i].x or x1
    x2 = ( x2 < self._.Polygon[i].x ) and self._.Polygon[i].x or x2
    y1 = ( y1 > self._.Polygon[i].y ) and self._.Polygon[i].y or y1
    y2 = ( y2 < self._.Polygon[i].y ) and self._.Polygon[i].y or y2

  end

  local vec1={x=x1, y=y1}
  local vec2={x=x2, y=y2}

  return vec1, vec2
end

--- Draw a frontier on the F10 map with small filled circles.
-- @param #ZONE_POLYGON_BASE self
-- @param #number Coalition (Optional) Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1= All.
-- @param #table Color (Optional) RGB color table {r, g, b}, e.g. {1, 0, 0} for red. Default {1, 1, 1}= White.
-- @param #number Radius (Optional) Radius of the circles in meters. Default 1000.
-- @param #number Alpha (Optional) Alpha transparency [0,1]. Default 1.
-- @param #number Segments (Optional) Number of segments within boundary line. Default 10.
-- @param #boolean Closed (Optional) Link the last point with the first one to obtain a closed boundary. Default false
-- @return #ZONE_POLYGON_BASE self
function ZONE_POLYGON_BASE:Boundary(Coalition, Color, Radius, Alpha, Segments, Closed)
    Coalition = Coalition or -1
    Color = Color or {1, 1, 1}
    Radius = Radius or 1000
    Alpha = Alpha or 1
    Segments = Segments or 10
    Closed = Closed or false
    local Limit
    local i = 1
    local j = #self._.Polygon
    if (Closed) then
        Limit = #self._.Polygon + 1
    else
        Limit = #self._.Polygon
    end
    while i <= #self._.Polygon do
        --self:T( { i, j, self._.Polygon[i], self._.Polygon[j] } )
        if j ~= Limit then
            local DeltaX = self._.Polygon[j].x - self._.Polygon[i].x
            local DeltaY = self._.Polygon[j].y - self._.Polygon[i].y
            for Segment = 0, Segments do
                local PointX = self._.Polygon[i].x + ( Segment * DeltaX / Segments )
                local PointY = self._.Polygon[i].y + ( Segment * DeltaY / Segments )
                --ZONE_RADIUS:New( "Zone", {x = PointX, y = PointY}, Radius ):DrawZone(Coalition, Color, 1, Color, Alpha, nil, true)
            end
        end
        j = i
        i = i + 1
    end
    return self
end

do -- Zone_Polygon


---
-- @type ZONE_POLYGON
-- @extends #ZONE_POLYGON_BASE
-- @extends #ZONE_BASE


--- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon, OR by drawings made with the Draw tool
--- in the Mission Editor
-- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties.
--
-- ## Declare a ZONE_POLYGON directly in the DCS mission editor!
--
-- You can declare a ZONE_POLYGON using the DCS mission editor by adding the #ZONE_POLYGON tag in the group name.
--
-- So, imagine you have a group declared in the mission editor, with group name `DefenseZone#ZONE_POLYGON`.
-- Then during mission startup, when loading Moose.lua, this group will be detected as a ZONE_POLYGON declaration.
-- Within the background, a ZONE_POLYGON object will be created within the @{Core.Database} using the properties of the group.
-- The ZONE_POLYGON name will be the group name without the #ZONE_POLYGON tag.
--
-- So, you can search yourself for the ZONE_POLYGON by using the @{#ZONE_POLYGON.FindByName}() method.
-- In this example, `local PolygonZone = ZONE_POLYGON:FindByName( "DefenseZone" )` would return the ZONE_POLYGON object
-- that was created at mission startup, and reference it into the `PolygonZone` local object.
--
-- Mission `ZON-510` shows a demonstration of this feature or method.
--
-- This is especially handy if you want to quickly setup a SET_ZONE...
-- So when you would declare `local SetZone = SET_ZONE:New():FilterPrefixes( "Defense" ):FilterStart()`,
-- then SetZone would contain the ZONE_POLYGON object `DefenseZone` as part of the zone collection,
-- without much scripting overhead!
--
-- This class now also supports drawings made with the Draw tool in the Mission Editor. Any drawing made with Line > Segments > Closed, Polygon > Rect or Polygon > Free can be
-- made into a ZONE_POLYGON.
--
-- This class has been updated to use a accurate way of generating random points inside the polygon without having to use trial and error guesses.
-- You can also get the surface area of the polygon now, handy if you want measure which coalition has the largest captured area, for example.
--
-- @field #ZONE_POLYGON
ZONE_POLYGON = {
  ClassName="ZONE_POLYGON",
  }

--- Constructor to create a ZONE_POLYGON instance, taking the zone name and the @{Wrapper.Group#GROUP} defined within the Mission Editor.
-- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected by ZONE_POLYGON.
-- @param #ZONE_POLYGON self
-- @param #string ZoneName Name of the zone.
-- @param Wrapper.Group#GROUP ZoneGroup The GROUP waypoints as defined within the Mission Editor define the polygon shape.
-- @return #ZONE_POLYGON self
function ZONE_POLYGON:New( ZoneName, ZoneGroup )

  local GroupPoints = ZoneGroup:GetTaskRoute()

  local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( ZoneName, GroupPoints ) )
  --self:F( { ZoneName, ZoneGroup, self._.Polygon } )

  -- Zone objects are added to the _DATABASE and SET_ZONE objects.
  _EVENTDISPATCHER:CreateEventNewZone( self )

  return self
end

--- Constructor to create a ZONE_POLYGON instance, taking the zone name and an array of DCS#Vec2, forming a polygon.
-- @param #ZONE_POLYGON self
-- @param #string ZoneName Name of the zone.
-- @param  #ZONE_POLYGON_BASE.ListVec2 PointsArray An array of @{DCS#Vec2}, forming a polygon.
-- @return #ZONE_POLYGON self
function ZONE_POLYGON:NewFromPointsArray( ZoneName, PointsArray )

  local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) )
  --self:F( { ZoneName, self._.Polygon } )

  -- Zone objects are added to the _DATABASE and SET_ZONE objects.
  _EVENTDISPATCHER:CreateEventNewZone( self )

  return self
end

--- Constructor to create a ZONE_POLYGON instance, taking the zone name and the **name** of the @{Wrapper.Group#GROUP} defined within the Mission Editor.
-- The @{Wrapper.Group#GROUP} waypoints define the polygon corners. The first and the last point are automatically connected by ZONE_POLYGON.
-- @param #ZONE_POLYGON self
-- @param #string GroupName The group name of the GROUP defining the waypoints within the Mission Editor to define the polygon shape.
-- @return #ZONE_POLYGON self
function ZONE_POLYGON:NewFromGroupName( GroupName )

  local ZoneGroup = GROUP:FindByName( GroupName )

  local GroupPoints = ZoneGroup:GetTaskRoute()

  local self = BASE:Inherit( self, ZONE_POLYGON_BASE:New( GroupName, GroupPoints ) )
  --self:F( { GroupName, ZoneGroup, self._.Polygon } )

  -- Zone objects are added to the _DATABASE and SET_ZONE objects.
  _EVENTDISPATCHER:CreateEventNewZone( self )

  return self
end

--- Constructor to create a ZONE_POLYGON instance, taking the name of a drawing made with the draw tool in the Mission Editor.
-- @param #ZONE_POLYGON self
-- @param #string DrawingName The name of the drawing in the Mission Editor
-- @return #ZONE_POLYGON self
function ZONE_POLYGON:NewFromDrawing(DrawingName)
    local points = {}
    for _, layer in pairs(env.mission.drawings.layers) do
        for _, object in pairs(layer["objects"]) do
            if object["name"] == DrawingName then
                if (object["primitiveType"] == "Line" and object["closed"] == true) or (object["polygonMode"] == "free") then
                    -- points for the drawings are saved in local space, so add the object's map x and y coordinates to get
                    -- world space points we can use
                    for _, point in UTILS.spairs(object["points"]) do
                        -- check if we want to skip adding a point
                        local skip = false
                        local p = {x = object["mapX"] + point["x"],
                                   y = object["mapY"] + point["y"] }

                        -- Check if the same coordinates already exist in the list, skip if they do
                        -- This can happen when drawing a Polygon in Free mode, DCS adds points on
                        -- top of each other that are in the `mission` file, but not visible in the
                        -- Mission Editor
                        for _, pt in pairs(points) do
                            if pt.x == p.x and pt.y == p.y then
                                skip = true
                            end
                        end

                        -- if it's a unique point, add it
                        if not skip then
                            table.add(points, p)
                        end
                    end
                elseif object["polygonMode"] == "rect" then
                    -- the points for a rect are saved as local coordinates with an angle. To get the world space points from this
                    -- we need to rotate the points around the center of the rects by an angle. UTILS.RotatePointAroundPivot was
                    -- committed in an earlier commit
                    local angle = object["angle"]
                    local half_width  = object["width"] / 2
                    local half_height = object["height"] / 2

                    local center = { x = object["mapX"], y = object["mapY"] }
                    local p1 = UTILS.RotatePointAroundPivot({ x = center.x - half_height, y = center.y + half_width }, center, angle)
                    local p2 = UTILS.RotatePointAroundPivot({ x = center.x + half_height, y = center.y + half_width }, center, angle)
                    local p3 = UTILS.RotatePointAroundPivot({ x = center.x + half_height, y = center.y - half_width }, center, angle)
                    local p4 = UTILS.RotatePointAroundPivot({ x = center.x - half_height, y = center.y - half_width }, center, angle)

                    points = {p1, p2, p3, p4}
                else
                    -- bring the Arrow code over from Shape/Polygon
                    -- something else that might be added in the future
                end
            end
        end
    end
    local self = BASE:Inherit(self, ZONE_POLYGON_BASE:New(DrawingName, points))
    _EVENTDISPATCHER:CreateEventNewZone(self)
    return self
end


--- Find a polygon zone in the _DATABASE using the name of the polygon zone.
-- @param #ZONE_POLYGON self
-- @param #string ZoneName The name of the polygon zone.
-- @return #ZONE_POLYGON self
function ZONE_POLYGON:FindByName( ZoneName )

  local ZoneFound = _DATABASE:FindZone( ZoneName )
  return ZoneFound
end

--- Scan the zone for the presence of units of the given ObjectCategories. Does **not** scan for scenery at the moment.
-- Note that **only after** a zone has been scanned, the zone can be evaluated by:
--
--   * @{Core.Zone#ZONE_POLYGON.IsAllInZoneOfCoalition}(): Scan the presence of units in the zone of a coalition.
--   * @{Core.Zone#ZONE_POLYGON.IsAllInZoneOfOtherCoalition}(): Scan the presence of units in the zone of an other coalition.
--   * @{Core.Zone#ZONE_POLYGON.IsSomeInZoneOfCoalition}(): Scan if there is some presence of units in the zone of the given coalition.
--   * @{Core.Zone#ZONE_POLYGON.IsNoneInZoneOfCoalition}(): Scan if there isn't any presence of units in the zone of an other coalition than the given one.
--   * @{Core.Zone#ZONE_POLYGON.IsNoneInZone}(): Scan if the zone is empty.
-- @param #ZONE_POLYGON self
-- @param ObjectCategories An array of categories of the objects to find in the zone. E.g. `{Object.Category.UNIT}`
-- @param UnitCategories An array of unit categories of the objects to find in the zone. E.g. `{Unit.Category.GROUND_UNIT,Unit.Category.SHIP}`
-- @usage
--    myzone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT})
--    local IsAttacked = myzone:IsSomeInZoneOfCoalition( self.Coalition )
function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories )

  self.ScanData = {}
  self.ScanData.Coalitions = {}
  self.ScanData.Scenery = {}
  self.ScanData.SceneryTable = {}
  self.ScanData.Units = {}

  local vectors = self:GetBoundingSquare()

  local ZoneRadius = UTILS.VecDist2D({x=vectors.x1, y=vectors.y1}, {x=vectors.x2, y=vectors.y2})/2
--  self:I("Scan Radius:" ..ZoneRadius)
  local CenterVec3 = self:GetCoordinate():GetVec3()

 --[[ this a bit shaky in functionality it seems
  local VolumeBox = {
   id = world.VolumeType.BOX,
   params = {
     min = minVec3,
     max = maxVec3
   }
  }
  --]]

  local SphereSearch = {
  id = world.VolumeType.SPHERE,
    params = {
    point = CenterVec3,
    radius = ZoneRadius,
    }
  }

  local function EvaluateZone( ZoneObject )

    if ZoneObject and self:IsVec3InZone(ZoneObject:getPoint()) then

      local ObjectCategory = Object.getCategory(ZoneObject)

      if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then

        local Include = false
        if not UnitCategories then
          -- Anything found is included.
          Include = true
        else
          -- Check if found object is in specified categories.
          local CategoryDCSUnit = ZoneObject:getDesc().category

          for UnitCategoryID, UnitCategory in pairs( UnitCategories ) do
            if UnitCategory == CategoryDCSUnit then
              Include = true
              break
            end
          end

        end

        if Include then

          local CoalitionDCSUnit = ZoneObject:getCoalition()

          -- This coalition is inside the zone.
          self.ScanData.Coalitions[CoalitionDCSUnit] = true

          self.ScanData.Units[ZoneObject] = ZoneObject

          --self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } )
        end
      end

      -- trying with box search
      if ObjectCategory == Object.Category.SCENERY then
        local SceneryType = ZoneObject:getTypeName()
        local SceneryName = ZoneObject:getName()
        self.ScanData.Scenery[SceneryType] = self.ScanData.Scenery[SceneryType] or {}
        self.ScanData.Scenery[SceneryType][SceneryName] = SCENERY:Register( SceneryName, ZoneObject )
        table.insert(self.ScanData.SceneryTable,self.ScanData.Scenery[SceneryType][SceneryName])
        --self:T( { SCENERY =  self.ScanData.Scenery[SceneryType][SceneryName] } )
      end

    end

    return true
  end

  -- Search objects.
  local inzoneunits = SET_UNIT:New():FilterZones({self}):FilterOnce()
  local inzonestatics = SET_STATIC:New():FilterZones({self}):FilterOnce()

  inzoneunits:ForEach(
    function(unit)
      local Unit = unit --Wrapper.Unit#UNIT
      local DCS = Unit:GetDCSObject()
      EvaluateZone(DCS)
    end
  )

  inzonestatics:ForEach(
    function(static)
      local Static = static --Wrapper.Static#STATIC
      local DCS = Static:GetDCSObject()
      EvaluateZone(DCS)
    end
  )

  local searchscenery = false
  for _,_type in pairs(ObjectCategories) do
    if _type == Object.Category.SCENERY then
      searchscenery = true
    end
  end

  if searchscenery then
    -- Search objects.
    world.searchObjects({Object.Category.SCENERY}, SphereSearch, EvaluateZone )
  end

end

--- Count the number of different coalitions inside the zone.
-- @param #ZONE_POLYGON self
-- @return #table Table of DCS units and DCS statics inside the zone.
function ZONE_POLYGON:GetScannedUnits()
  return self.ScanData.Units
end

--- Get a set of scanned units.
-- @param #ZONE_POLYGON self
-- @return Core.Set#SET_UNIT Set of units and statics inside the zone.
function ZONE_POLYGON:GetScannedSetUnit()

  local SetUnit = SET_UNIT:New()

  if self.ScanData then
    for ObjectID, UnitObject in pairs( self.ScanData.Units ) do
      local UnitObject = UnitObject -- DCS#Unit
      if UnitObject:isExist() then
        local FoundUnit = UNIT:FindByName( UnitObject:getName() )
        if FoundUnit then
          SetUnit:AddUnit( FoundUnit )
        else
          local FoundStatic = STATIC:FindByName( UnitObject:getName() )
          if FoundStatic then
            SetUnit:AddUnit( FoundStatic )
          end
        end
      end
    end
  end

  return SetUnit
end

--- Get a set of scanned units.
-- @param #ZONE_POLYGON self
-- @return Core.Set#SET_GROUP Set of groups.
function ZONE_POLYGON:GetScannedSetGroup()

  self.ScanSetGroup=self.ScanSetGroup or SET_GROUP:New() --Core.Set#SET_GROUP

  self.ScanSetGroup.Set={}

  if self.ScanData then
    for ObjectID, UnitObject in pairs( self.ScanData.Units ) do
      local UnitObject = UnitObject -- DCS#Unit
      if UnitObject:isExist() then

        local FoundUnit=UNIT:FindByName(UnitObject:getName())
        if FoundUnit then
          local group=FoundUnit:GetGroup()
          self.ScanSetGroup:AddGroup(group)
        end
      end
    end
  end

  return self.ScanSetGroup
end

--- Count the number of different coalitions inside the zone.
-- @param #ZONE_POLYGON self
-- @return #number Counted coalitions.
function ZONE_POLYGON:CountScannedCoalitions()

  local Count = 0

  for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do
    Count = Count + 1
  end

  return Count
end

--- Check if a certain coalition is inside a scanned zone.
-- @param #ZONE_POLYGON self
-- @param #number Coalition The coalition id, e.g. coalition.side.BLUE.
-- @return #boolean If true, the coalition is inside the zone.
function ZONE_POLYGON:CheckScannedCoalition( Coalition )
  if Coalition then
    return self.ScanData.Coalitions[Coalition]
  end
  return nil
end

--- Get Coalitions of the units in the Zone, or Check if there are units of the given Coalition in the Zone.
-- Returns nil if there are none to two Coalitions in the zone!
-- Returns one Coalition if there are only Units of one Coalition in the Zone.
-- Returns the Coalition for the given Coalition if there are units of the Coalition in the Zone.
-- @param #ZONE_POLYGON self
-- @return #table
function ZONE_POLYGON:GetScannedCoalition( Coalition )

  if Coalition then
    return self.ScanData.Coalitions[Coalition]
  else
    local Count = 0
    local ReturnCoalition = nil

    for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do
      Count = Count + 1
      ReturnCoalition = CoalitionID
    end

    if Count ~= 1 then
      ReturnCoalition = nil
    end

    return ReturnCoalition
  end
end

--- Get scanned scenery types
-- @param #ZONE_POLYGON self
-- @return #table Table of DCS scenery type objects.
function ZONE_POLYGON:GetScannedSceneryType( SceneryType )
  return self.ScanData.Scenery[SceneryType]
end

--- Get scanned scenery table
-- @param #ZONE_POLYGON self
-- @return #table Table of Wrapper.Scenery#SCENERY scenery objects.
function ZONE_POLYGON:GetScannedSceneryObjects()
  return self.ScanData.SceneryTable
end

--- Get scanned scenery table
-- @param #ZONE_POLYGON self
-- @return #table Structured table of [type].[name].Wrapper.Scenery#SCENERY scenery objects.
function ZONE_POLYGON:GetScannedScenery()
  return self.ScanData.Scenery
end

--- Get scanned set of scenery objects
-- @param #ZONE_POLYGON self
-- @return #table Table of Wrapper.Scenery#SCENERY scenery objects.
function ZONE_POLYGON:GetScannedSetScenery()
  local scenery = SET_SCENERY:New()
  local objects = self:GetScannedSceneryObjects()
  for _,_obj in pairs (objects) do
    scenery:AddScenery(_obj)
  end
  return scenery
end

--- Is All in Zone of Coalition?
-- Check if only the specified coalition is inside the zone and noone else.
-- @param #ZONE_POLYGON self
-- @param #number Coalition Coalition ID of the coalition which is checked to be the only one in the zone.
-- @return #boolean True, if **only** that coalition is inside the zone and no one else.
-- @usage
--    self.Zone:Scan()
--    local IsGuarded = self.Zone:IsAllInZoneOfCoalition( self.Coalition )
function ZONE_POLYGON:IsAllInZoneOfCoalition( Coalition )
  return self:CountScannedCoalitions() == 1 and self:GetScannedCoalition( Coalition ) == true
end

--- Is All in Zone of Other Coalition?
-- Check if only one coalition is inside the zone and the specified coalition is not the one.
-- You first need to use the @{#ZONE_POLYGON.Scan} method to scan the zone before it can be evaluated!
-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set.
-- @param #ZONE_POLYGON self
-- @param #number Coalition Coalition ID of the coalition which is not supposed to be in the zone.
-- @return #boolean True, if and only if only one coalition is inside the zone and the specified coalition is not it.
-- @usage
--    self.Zone:Scan()
--    local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition )
function ZONE_POLYGON:IsAllInZoneOfOtherCoalition( Coalition )
  return self:CountScannedCoalitions() == 1 and self:GetScannedCoalition( Coalition ) == nil
end

--- Is Some in Zone of Coalition?
-- Check if more than one coalition is inside the zone and the specified coalition is one of them.
-- You first need to use the @{#ZONE_POLYGON.Scan} method to scan the zone before it can be evaluated!
-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set.
-- @param #ZONE_POLYGON self
-- @param #number Coalition ID of the coalition which is checked to be inside the zone.
-- @return #boolean True if more than one coalition is inside the zone and the specified coalition is one of them.
-- @usage
--    self.Zone:Scan()
--    local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition )
function ZONE_POLYGON:IsSomeInZoneOfCoalition( Coalition )
  return self:CountScannedCoalitions() > 1 and self:GetScannedCoalition( Coalition ) == true
end

--- Is None in Zone of Coalition?
-- You first need to use the @{#ZONE_POLYGON.Scan} method to scan the zone before it can be evaluated!
-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set.
-- @param #ZONE_POLYGON self
-- @param Coalition
-- @return #boolean
-- @usage
--    self.Zone:Scan()
--    local IsOccupied = self.Zone:IsNoneInZoneOfCoalition( self.Coalition )
function ZONE_POLYGON:IsNoneInZoneOfCoalition( Coalition )
  return self:GetScannedCoalition( Coalition ) == nil
end

--- Is None in Zone?
-- You first need to use the @{#ZONE_POLYGON.Scan} method to scan the zone before it can be evaluated!
-- Note that once a zone has been scanned, multiple evaluations can be done on the scan result set.
-- @param #ZONE_POLYGON self
-- @return #boolean
-- @usage
--    self.Zone:Scan()
--    local IsEmpty = self.Zone:IsNoneInZone()
function ZONE_POLYGON:IsNoneInZone()
  return self:CountScannedCoalitions() == 0
end

end

do -- ZONE_ELASTIC

  ---
  -- @type ZONE_ELASTIC
  -- @field #table points Points in 2D.
  -- @field #table setGroups Set of GROUPs.
  -- @field #table setOpsGroups Set of OPSGROUPS.
  -- @field #table setUnits Set of UNITs.
  -- @field #number updateID Scheduler ID for updating.
  -- @extends #ZONE_POLYGON_BASE

  --- The ZONE_ELASTIC class defines a dynamic polygon zone, where only the convex hull is used.
  --
  -- @field #ZONE_ELASTIC
  ZONE_ELASTIC = {
    ClassName="ZONE_ELASTIC",
    points={},
    setGroups={}
    }

  --- Constructor to create a ZONE_ELASTIC instance.
  -- @param #ZONE_ELASTIC self
  -- @param #string ZoneName Name of the zone.
  -- @param DCS#Vec2 Points (Optional) Fixed points.
  -- @return #ZONE_ELASTIC self
  function ZONE_ELASTIC:New(ZoneName, Points)

    local self=BASE:Inherit(self, ZONE_POLYGON_BASE:New(ZoneName, Points)) --#ZONE_ELASTIC

    -- Zone objects are added to the _DATABASE and SET_ZONE objects.
    _EVENTDISPATCHER:CreateEventNewZone( self )

    if Points then
      self.points=Points
    end

    return self
  end

  --- Add a vertex (point) to the polygon.
  -- @param #ZONE_ELASTIC self
  -- @param DCS#Vec2 Vec2 Point in 2D (with x and y coordinates).
  -- @return #ZONE_ELASTIC self
  function ZONE_ELASTIC:AddVertex2D(Vec2)

    -- Add vec2 to points.
    table.insert(self.points, Vec2)

    return self
  end
  
  --- Remove a vertex (point) from the polygon.
  -- @param #ZONE_ELASTIC self
  -- @param DCS#Vec2 Vec2 Point in 2D (with x and y coordinates).
  -- @return #ZONE_ELASTIC self
  function ZONE_ELASTIC:RemoveVertex2D(Vec2)
    
    local found = false
    local findex = 0
    for _id,_vec2 in pairs(self.points) do
      if _vec2.x == Vec2.x and _vec2.y == Vec2.y then
        found = true
        findex = _id
        break
      end
    end
    
    if found == true and findex >  0 then
      table.remove(self.points,findex)
    end

    return self
  end
    
  --- Remove a vertex (point) from the polygon.
  -- @param #ZONE_ELASTIC self
  -- @param DCS#Vec3 Vec3 Point in 3D (with x, y and z coordinates). Only the x and z coordinates are used.
  -- @return #ZONE_ELASTIC self
  function ZONE_ELASTIC:RemoveVertex3D(Vec3)
    return self:RemoveVertex2D({x=Vec3.x, y=Vec3.z})
  end

  --- Add a vertex (point) to the polygon.
  -- @param #ZONE_ELASTIC self
  -- @param DCS#Vec3 Vec3 Point in 3D (with x, y and z coordinates). Only the x and z coordinates are used.
  -- @return #ZONE_ELASTIC self
  function ZONE_ELASTIC:AddVertex3D(Vec3)

    -- Add vec2 from vec3 to points.
    table.insert(self.points, {x=Vec3.x, y=Vec3.z})

    return self
  end


  --- Add a set of groups. Positions of the group will be considered as polygon vertices when contructing the convex hull.
  -- @param #ZONE_ELASTIC self
  -- @param Core.Set#SET_GROUP GroupSet Set of groups.
  -- @return #ZONE_ELASTIC self
  function ZONE_ELASTIC:AddSetGroup(GroupSet)

    -- Add set to table.
    table.insert(self.setGroups, GroupSet)

    return self
  end


  --- Update the convex hull of the polygon.
  -- This uses the [Graham scan](https://en.wikipedia.org/wiki/Graham_scan).
  -- @param #ZONE_ELASTIC self
  -- @param #number Delay Delay in seconds before the zone is updated. Default 0.
  -- @param #boolean Draw Draw the zone. Default `nil`.
  -- @return #ZONE_ELASTIC self
  function ZONE_ELASTIC:Update(Delay, Draw)

    -- Debug info.
    --self:T(string.format("Updating ZONE_ELASTIC %s", tostring(self.ZoneName)))
    
    -- Copy all points.
    local points=UTILS.DeepCopy(self.points or {})

    if self.setGroups then
      for _,_setGroup in pairs(self.setGroups) do
        local setGroup=_setGroup --Core.Set#SET_GROUP
        for _,_group in pairs(setGroup.Set) do
          local group=_group --Wrapper.Group#GROUP
          if group and group:IsAlive() then
            table.insert(points, group:GetVec2())
          end
        end
      end
    end

    -- Update polygon verticies from points.
    self._.Polygon=self:_ConvexHull(points)
          
    self._Triangles = self:_Triangulate()
    self.SurfaceArea = self:_CalculateSurfaceArea()

    if Draw~=false then
      if self.DrawID or Draw==true then
        self:UndrawZone()
        self:DrawZone()
      end
    end

    return self
  end

  --- Start the updating scheduler.
  -- @param #ZONE_ELASTIC self
  -- @param #number Tstart Time in seconds before the updating starts.
  -- @param #number dT Time interval in seconds between updates. Default 60 sec.
  -- @param #number Tstop Time in seconds after which the updating stops. Default `nil`.
  -- @param #boolean Draw Draw the zone. Default `nil`.
  -- @return #ZONE_ELASTIC self
  function ZONE_ELASTIC:StartUpdate(Tstart, dT, Tstop, Draw)

    self.updateID=self:ScheduleRepeat(Tstart, dT, 0, Tstop, ZONE_ELASTIC.Update, self, 0, Draw)

    return self
  end

  --- Stop the updating scheduler.
  -- @param #ZONE_ELASTIC self
  -- @param #number Delay Delay in seconds before the scheduler will be stopped. Default 0.
  -- @return #ZONE_ELASTIC self
  function ZONE_ELASTIC:StopUpdate(Delay)

    if Delay and Delay>0 then
      self:ScheduleOnce(Delay, ZONE_ELASTIC.StopUpdate, self)
    else

      if self.updateID then

        self:ScheduleStop(self.updateID)

        self.updateID=nil

      end

    end

    return self
  end


  --- Create a convex hull.
  -- @param #ZONE_ELASTIC self
  -- @param #table pl Points
  -- @return #table Points
  function ZONE_ELASTIC:_ConvexHull(pl)

    if #pl == 0 then
      return {}
    end

    table.sort(pl, function(left,right)
      return left.x < right.x
    end)

    local h = {}

    -- Function: ccw > 0 if three points make a counter-clockwise turn, clockwise if ccw < 0, and collinear if ccw = 0.
    local function ccw(a,b,c)
      return (b.x - a.x) * (c.y - a.y) > (b.y - a.y) * (c.x - a.x)
    end

    -- lower hull
    for i,pt in pairs(pl) do
      while #h >= 2 and not ccw(h[#h-1], h[#h], pt) do
        table.remove(h,#h)
      end
      table.insert(h,pt)
    end

    -- upper hull
    local t = #h + 1
    for i=#pl, 1, -1 do
      local pt = pl[i]
      while #h >= t and not ccw(h[#h-1], h[#h], pt) do
        table.remove(h, #h)
      end
      table.insert(h, pt)
    end

    table.remove(h, #h)

    return h
  end

end



--- ZONE_OVAL created from a center point, major axis, minor axis, and angle.
-- Ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua
-- @type ZONE_OVAL
-- @extends Core.Zone#ZONE_BASE

--- ## ZONE_OVAL class, extends @{#ZONE_BASE}
--
-- The ZONE_OVAL class is defined by a center point, major axis, minor axis, and angle.
-- This class implements the inherited functions from @{#ZONE_BASE} taking into account the own zone format and properties.
--
-- @field #ZONE_OVAL
ZONE_OVAL = {
    ClassName = "OVAL",
    ZoneName="",
    MajorAxis = nil,
    MinorAxis = nil,
    Angle = 0,
    DrawPoly = nil -- let's just use a ZONE_POLYGON to draw the ZONE_OVAL on the map
}

--- Creates a new ZONE_OVAL from a center point, major axis, minor axis, and angle.
--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua
-- @param #ZONE_OVAL self
-- @param #string name Name of the zone.
-- @param #table vec2 The center point of the oval
-- @param #number major_axis The major axis of the oval
-- @param #number minor_axis The minor axis of the oval
-- @param #number angle The angle of the oval
-- @return #ZONE_OVAL The new oval
function ZONE_OVAL:New(name, vec2, major_axis, minor_axis, angle)

    self = BASE:Inherit(self, ZONE_BASE:New())

    self.ZoneName = name
    self.CenterVec2 = vec2
    self.MajorAxis = major_axis
    self.MinorAxis = minor_axis
    self.Angle = angle or 0

    _DATABASE:AddZone(name, self)

    return self
end

--- Constructor to create a ZONE_OVAL instance, taking the name of a drawing made with the draw tool in the Mission Editor.
--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua
-- @param #ZONE_OVAL self
-- @param #string DrawingName The name of the drawing in the Mission Editor
-- @return #ZONE_OVAL self
function ZONE_OVAL:NewFromDrawing(DrawingName)
    self = BASE:Inherit(self, ZONE_BASE:New(DrawingName))
    for _, layer in pairs(env.mission.drawings.layers) do
        for _, object in pairs(layer["objects"]) do
            if string.find(object["name"], DrawingName, 1, true) then
                if object["polygonMode"] == "oval" then
                    self.CenterVec2 = { x = object["mapX"], y = object["mapY"] }
                    self.MajorAxis = object["r1"]
                    self.MinorAxis = object["r2"]
                    self.Angle = object["angle"]

                end
            end
        end
    end

    _DATABASE:AddZone(DrawingName, self)

    return self
end

--- Gets the major axis of the oval.
-- @param #ZONE_OVAL self
-- @return #number The major axis of the oval
function ZONE_OVAL:GetMajorAxis()
    return self.MajorAxis
end

--- Gets the minor axis of the oval.
-- @param #ZONE_OVAL self
-- @return #number The minor axis of the oval
function ZONE_OVAL:GetMinorAxis()
    return self.MinorAxis
end

--- Gets the angle of the oval.
-- @param #ZONE_OVAL self
-- @return #number The angle of the oval
function ZONE_OVAL:GetAngle()
    return self.Angle
end

--- Returns a the center point of the oval
-- @param #ZONE_OVAL self
-- @return #table The center Vec2
function ZONE_OVAL:GetVec2()
    return self.CenterVec2
end

--- Checks if a point is contained within the oval.
-- @param #ZONE_OVAL self
-- @param #table point The point to check
-- @return #boolean True if the point is contained, false otherwise
function ZONE_OVAL:IsVec2InZone(vec2)
    local cos, sin = math.cos, math.sin
    local dx = vec2.x - self.CenterVec2.x
    local dy = vec2.y - self.CenterVec2.y
    local rx = dx * cos(self.Angle) + dy * sin(self.Angle)
    local ry = -dx * sin(self.Angle) + dy * cos(self.Angle)
    return rx * rx / (self.MajorAxis * self.MajorAxis) + ry * ry / (self.MinorAxis * self.MinorAxis) <= 1
end

--- Calculates the bounding box of the oval. The bounding box is the smallest rectangle that contains the oval.
-- @param #ZONE_OVAL self
-- @return #table The bounding box of the oval
function ZONE_OVAL:GetBoundingSquare()
    local min_x = self.CenterVec2.x - self.MajorAxis
    local min_y = self.CenterVec2.y - self.MinorAxis
    local max_x = self.CenterVec2.x + self.MajorAxis
    local max_y = self.CenterVec2.y + self.MinorAxis

    return {
        {x=min_x, y=min_x}, {x=max_x, y=min_y}, {x=max_x, y=max_y}, {x=min_x, y=max_y}
    }
end

--- Find points on the edge of the oval
-- @param #ZONE_OVAL self
-- @param #number num_points How many points should be found. More = smoother shape
-- @return #table Points on he edge
function ZONE_OVAL:PointsOnEdge(num_points)
    num_points = num_points or 40
    local points = {}
    local dtheta = 2 * math.pi / num_points

    for i = 0, num_points - 1 do
        local theta = i * dtheta
        local x = self.CenterVec2.x + self.MajorAxis * math.cos(theta) * math.cos(self.Angle) - self.MinorAxis * math.sin(theta) * math.sin(self.Angle)
        local y = self.CenterVec2.y + self.MajorAxis * math.cos(theta) * math.sin(self.Angle) + self.MinorAxis * math.sin(theta) * math.cos(self.Angle)
        table.insert(points, {x = x, y = y})
    end

    return points
end

--- Returns a random Vec2 within the oval.
-- @param #ZONE_OVAL self
-- @return #table The random Vec2
function ZONE_OVAL:GetRandomVec2()
    local theta = math.rad(self.Angle)

    local random_point = math.sqrt(math.random())  --> uniformly
    --local random_point = math.random()           --> more clumped around center
    local phi = math.random() * 2 * math.pi
    local x_c = random_point * math.cos(phi)
    local y_c = random_point * math.sin(phi)
    local x_e = x_c * self.MajorAxis
    local y_e = y_c * self.MinorAxis
    local rx = (x_e * math.cos(theta) - y_e * math.sin(theta)) + self.CenterVec2.x
    local ry = (x_e * math.sin(theta) + y_e * math.cos(theta)) + self.CenterVec2.y

    return {x=rx, y=ry}
end

--- Define a random @{Core.Point#COORDINATE} within the zone. Note that this is actually a @{Core.Point#COORDINATE} type object, and not a simple Vec2 table.
-- @param #ZONE_OVAL self
-- @return Core.Point#COORDINATE The COORDINATE coordinates.
function ZONE_OVAL:GetRandomPointVec2()
    return COORDINATE:NewFromVec2(self:GetRandomVec2())
end

--- Define a random @{Core.Point#COORDINATE} within the zone. Note that this is actually a @{Core.Point#COORDINATE} type object, and not a simple Vec3 table.
-- @param #ZONE_OVAL self
-- @return Core.Point#COORDINATE The COORDINATE coordinates.
function ZONE_OVAL:GetRandomPointVec3()
    return COORDINATE:NewFromVec3(self:GetRandomVec2())
end

--- Draw the zone on the F10 map.
--- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua
-- @param #ZONE_OVAL self
-- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All.
-- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red.
-- @param #number Alpha Transparency [0,1]. Default 1.
-- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value. -- doesn't seem to work
-- @param #number FillAlpha Transparency [0,1]. Default 0.15.                                                 -- doesn't seem to work
-- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
-- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
-- @return #ZONE_OVAL self
function ZONE_OVAL:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType)
    Coalition = Coalition or self:GetDrawCoalition()

    -- Set draw coalition.
    self:SetDrawCoalition(Coalition)

    Color = Color or self:GetColorRGB()
    Alpha = Alpha or 1

    -- Set color.
    self:SetColor(Color, Alpha)

    FillColor = FillColor or self:GetFillColorRGB()
    if not FillColor then
        UTILS.DeepCopy(Color)
    end
    FillAlpha = FillAlpha or self:GetFillColorAlpha()
    if not FillAlpha then
        FillAlpha = 0.15
    end

    LineType = LineType or 1

    -- Set fill color -----------> has fill color worked in recent versions of DCS?
    -- doing something like
    --
    -- trigger.action.markupToAll(7, -1, 501, p.Coords[1]:GetVec3(), p.Coords[2]:GetVec3(),p.Coords[3]:GetVec3(),p.Coords[4]:GetVec3(),{1,0,0, 1}, {1,0,0, 1}, 4, false, Text or "")
    --
    -- doesn't seem to fill in the shape for an n-sided polygon
    self:SetFillColor(FillColor, FillAlpha)

    self.DrawPoly = ZONE_POLYGON:NewFromPointsArray(self.ZoneName, self:PointsOnEdge(80))
    self.DrawPoly:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType)
end

--- Remove drawing from F10 map
-- @param #ZONE_OVAL self
function ZONE_OVAL:UndrawZone()
    if self.DrawPoly then
        self.DrawPoly:UndrawZone()
    end
end

do -- ZONE_AIRBASE

  ---
  -- @type ZONE_AIRBASE
  -- @field #boolean isShip If `true`, airbase is a ship.
  -- @field #boolean isHelipad If `true`, airbase is a helipad.
  -- @field #boolean isAirdrome If `true`, airbase is an airdrome.
  -- @extends #ZONE_RADIUS


  --- The ZONE_AIRBASE class defines by a zone around a @{Wrapper.Airbase#AIRBASE} with a radius.
  -- This class implements the inherited functions from @{#ZONE_RADIUS} taking into account the own zone format and properties.
  --
  -- @field #ZONE_AIRBASE
  ZONE_AIRBASE = {
    ClassName="ZONE_AIRBASE",
    }



  --- Constructor to create a ZONE_AIRBASE instance, taking the zone name, a zone @{Wrapper.Airbase#AIRBASE} and a radius.
  -- @param #ZONE_AIRBASE self
  -- @param #string AirbaseName Name of the airbase.
  -- @param DCS#Distance Radius (Optional)The radius of the zone in meters. Default 4000 meters.
  -- @return #ZONE_AIRBASE self
  function ZONE_AIRBASE:New( AirbaseName, Radius )

    Radius=Radius or 4000

    local Airbase = AIRBASE:FindByName( AirbaseName )

    local self = BASE:Inherit( self, ZONE_RADIUS:New( AirbaseName, Airbase:GetVec2(), Radius, true ) )

    self._.ZoneAirbase = Airbase
    self._.ZoneVec2Cache = self._.ZoneAirbase:GetVec2()

    if Airbase:IsShip() then
      self.isShip=true
      self.isHelipad=false
      self.isAirdrome=false
    elseif Airbase:IsHelipad() then
      self.isShip=false
      self.isHelipad=true
      self.isAirdrome=false
    elseif Airbase:IsAirdrome() then
      self.isShip=false
      self.isHelipad=false
      self.isAirdrome=true
    end

    -- Zone objects are added to the _DATABASE and SET_ZONE objects.
    _EVENTDISPATCHER:CreateEventNewZone( self )

    return self
  end

  --- Get the airbase as part of the ZONE_AIRBASE object.
  -- @param #ZONE_AIRBASE self
  -- @return Wrapper.Airbase#AIRBASE The airbase.
  function ZONE_AIRBASE:GetAirbase()
    return self._.ZoneAirbase
  end

  --- Returns the current location of the AIRBASE.
  -- @param #ZONE_AIRBASE self
  -- @return DCS#Vec2 The location of the zone based on the AIRBASE location.
  function ZONE_AIRBASE:GetVec2()
    --self:F( self.ZoneName )

    local ZoneVec2 = nil

    if self._.ZoneAirbase:IsAlive() then
      ZoneVec2 = self._.ZoneAirbase:GetVec2()
      self._.ZoneVec2Cache = ZoneVec2
    else
      ZoneVec2 = self._.ZoneVec2Cache
    end

    --self:T( { ZoneVec2 } )

    return ZoneVec2
  end

  --- Returns a @{Core.Point#COORDINATE} object reflecting a random 2D location within the zone. Note that this is actually a @{Core.Point#COORDINATE} type object, and not a simple Vec2 table.
  -- @param #ZONE_AIRBASE self
  -- @param #number inner (optional) Minimal distance from the center of the zone. Default is 0.
  -- @param #number outer (optional) Maximal distance from the outer edge of the zone. Default is the radius of the zone.
  -- @return Core.Point#COORDINATE The @{Core.Point#COORDINATE} object reflecting the random 3D location within the zone.
  function ZONE_AIRBASE:GetRandomPointVec2( inner, outer )
    --self:F( self.ZoneName, inner, outer )

    local PointVec2 = COORDINATE:NewFromVec2( self:GetRandomVec2() )

    --self:T3( { PointVec2 } )

    return PointVec2
  end

end
--- **Core** - The ZONE_DETECTION class, defined by a zone name, a detection object and a radius.
-- @module Core.Zone_Detection
-- @image MOOSE.JPG

---
-- @type ZONE_DETECTION
-- @field DCS#Vec2 Vec2 The current location of the zone.
-- @field DCS#Distance Radius The radius of the zone.
-- @extends #ZONE_BASE

--- The ZONE_DETECTION class defined by a zone name, a location and a radius.
-- This class implements the inherited functions from Core.Zone#ZONE_BASE taking into account the own zone format and properties.
-- 
-- ## ZONE_DETECTION constructor
-- 
--   * @{#ZONE_DETECTION.New}(): Constructor.
-- 
-- @field #ZONE_DETECTION
ZONE_DETECTION = {
  ClassName="ZONE_DETECTION",
  }

--- Constructor of @{#ZONE_DETECTION}, taking the zone name, the zone location and a radius.
-- @param #ZONE_DETECTION self
-- @param #string ZoneName Name of the zone.
-- @param Functional.Detection#DETECTION_BASE Detection The detection object defining the locations of the central detections.
-- @param DCS#Distance Radius The radius around the detections defining the combined zone.
-- @return #ZONE_DETECTION self
function ZONE_DETECTION:New( ZoneName, Detection, Radius )
  local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_DETECTION
  self:F( { ZoneName, Detection, Radius } )

  self.Detection = Detection
  self.Radius = Radius

  return self
end

--- Bounds the zone with tires.
-- @param #ZONE_DETECTION self
-- @param #number Points (optional) The amount of points in the circle. Default 360.
-- @param DCS#country.id CountryID The country id of the tire objects, e.g. country.id.USA for blue or country.id.RUSSIA for red.
-- @param #boolean UnBound (Optional) If true the tyres will be destroyed.
-- @return #ZONE_DETECTION self
function ZONE_DETECTION:BoundZone( Points, CountryID, UnBound )

  local Point = {}
  local Vec2 = self:GetVec2()

  Points = Points and Points or 360

  local Angle
  local RadialBase = math.pi*2

  for Angle = 0, 360, (360 / Points ) do
    local Radial = Angle * RadialBase / 360
    Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius()
    Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius()

    local CountryName = _DATABASE.COUNTRY_NAME[CountryID]

    local Tire = {
        ["country"] = CountryName, 
        ["category"] = "Fortifications",
        ["canCargo"] = false,
        ["shape_name"] = "H-tyre_B_WF",
        ["type"] = "Black_Tyre_WF",
        --["unitId"] = Angle + 10000,
        ["y"] = Point.y,
        ["x"] = Point.x,
        ["name"] = string.format( "%s-Tire #%0d", self:GetName(), Angle ),
        ["heading"] = 0,
    } -- end of ["group"]

    local Group = coalition.addStaticObject( CountryID, Tire )
    if UnBound and UnBound == true then
      Group:destroy()
    end
  end

  return self
end


--- Smokes the zone boundaries in a color.
-- @param #ZONE_DETECTION self
-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color.
-- @param #number Points (optional) The amount of points in the circle.
-- @param #number AddHeight (optional) The height to be added for the smoke.
-- @param #number AddOffSet (optional) The angle to be added for the smoking start position.
-- @return #ZONE_DETECTION self
function ZONE_DETECTION:SmokeZone( SmokeColor, Points, AddHeight, AngleOffset )
  self:F2( SmokeColor )

  local Point = {}
  local Vec2 = self:GetVec2()
  
  AddHeight = AddHeight or 0
  AngleOffset = AngleOffset or 0

  Points = Points and Points or 360

  local Angle
  local RadialBase = math.pi*2
  
  for Angle = 0, 360, 360 / Points do
    local Radial = ( Angle + AngleOffset ) * RadialBase / 360
    Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius()
    Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius()
    COORDINATE:New( Point.x, AddHeight, Point.y):Smoke( SmokeColor )
  end

  return self
end


--- Flares the zone boundaries in a color.
-- @param #ZONE_DETECTION self
-- @param Utilities.Utils#FLARECOLOR FlareColor The flare color.
-- @param #number Points (optional) The amount of points in the circle.
-- @param DCS#Azimuth Azimuth (optional) Azimuth The azimuth of the flare.
-- @param #number AddHeight (optional) The height to be added for the smoke.
-- @return #ZONE_DETECTION self
function ZONE_DETECTION:FlareZone( FlareColor, Points, Azimuth, AddHeight )
  self:F2( { FlareColor, Azimuth } )

  local Point = {}
  local Vec2 = self:GetVec2()
  
  AddHeight = AddHeight or 0
  
  Points = Points and Points or 360

  local Angle
  local RadialBase = math.pi*2
  
  for Angle = 0, 360, 360 / Points do
    local Radial = Angle * RadialBase / 360
    Point.x = Vec2.x + math.cos( Radial ) * self:GetRadius()
    Point.y = Vec2.y + math.sin( Radial ) * self:GetRadius()
    COORDINATE:New( Point.x, AddHeight, Point.y ):Flare( FlareColor, Azimuth )
  end

  return self
end

--- Returns the radius around the detected locations defining the combine zone.
-- @param #ZONE_DETECTION self
-- @return DCS#Distance The radius.
function ZONE_DETECTION:GetRadius()
  self:F2( self.ZoneName )

  self:T2( { self.Radius } )

  return self.Radius
end

--- Sets the radius around the detected locations defining the combine zone.
-- @param #ZONE_DETECTION self
-- @param DCS#Distance Radius The radius.
-- @return #ZONE_DETECTION self
function ZONE_DETECTION:SetRadius( Radius )
  self:F2( self.ZoneName )

  self.Radius = Radius
  self:T2( { self.Radius } )

  return self.Radius
end



--- Returns if a location is within the zone.
-- @param #ZONE_DETECTION self
-- @param DCS#Vec2 Vec2 The location to test.
-- @return #boolean true if the location is within the zone.
function ZONE_DETECTION:IsVec2InZone( Vec2 )
  self:F2( Vec2 )

  local Coordinates = self.Detection:GetDetectedItemCoordinates() -- This returns a list of coordinates that define the (central) locations of the detections.
  
  for CoordinateID, Coordinate in pairs( Coordinates ) do    
    local ZoneVec2 = Coordinate:GetVec2()    
    if ZoneVec2 then
      if (( Vec2.x - ZoneVec2.x )^2 + ( Vec2.y - ZoneVec2.y ) ^2 ) ^ 0.5 <= self:GetRadius() then
        return true
      end
    end
  end
  
  return false
end

--- Returns if a point is within the zone.
-- @param #ZONE_DETECTION self
-- @param DCS#Vec3 Vec3 The point to test.
-- @return #boolean true if the point is within the zone.
function ZONE_DETECTION:IsVec3InZone( Vec3 )
  self:F2( Vec3 )

  local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } )

  return InZone
end
--- **Core** - Manages several databases containing templates, mission objects, and mission information.
--
-- ===
--
-- ## Features:
--
--   * During mission startup, scan the mission environment, and create / instantiate intelligently the different objects as defined within the mission.
--   * Manage database of DCS Group templates (as modelled using the mission editor).
--     - Group templates.
--     - Unit templates.
--     - Statics templates.
--   * Manage database of @{Wrapper.Group#GROUP} objects alive in the mission.
--   * Manage database of @{Wrapper.Unit#UNIT} objects alive in the mission.
--   * Manage database of @{Wrapper.Static#STATIC} objects alive in the mission.
--   * Manage database of players.
--   * Manage database of client slots defined using the mission editor.
--   * Manage database of airbases on the map, and from FARPs and ships as defined using the mission editor.
--   * Manage database of countries.
--   * Manage database of zone names.
--   * Manage database of hits to units and statics.
--   * Manage database of destroys of units and statics.
--   * Manage database of @{Core.Zone#ZONE_BASE} objects.
--   * Manage database of @{Wrapper.DynamicCargo#DYNAMICCARGO} objects alive in the mission.
--
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions: **funkyfranky**
--
-- ===
--
-- @module Core.Database
-- @image Core_Database.JPG

---
-- @type DATABASE
-- @field #string ClassName Name of the class.
-- @field #table Templates Templates: Units, Groups, Statics, ClientsByName, ClientsByID.
-- @field #table CLIENTS Clients.
-- @field #table STORAGES DCS warehouse storages.
-- @field #table STNS Used Link16 octal numbers for F16/15/18/AWACS planes.
-- @field #table SADL Used Link16 octal numbers for A10/C-II planes.
-- @field #table DYNAMICCARGO Dynamic Cargo objects.
-- @extends Core.Base#BASE

--- Contains collections of wrapper objects defined within MOOSE that reflect objects within the simulator.
--
-- Mission designers can use the DATABASE class to refer to:
--
--  * STATICS
--  * UNITS
--  * GROUPS
--  * CLIENTS
--  * AIRBASES
--  * PLAYERSJOINED
--  * PLAYERS
--  * CARGOS
--  * STORAGES (DCS warehouses)
--  * DYNAMICCARGO
--
-- On top, for internal MOOSE administration purposes, the DATABASE administers the Unit and Group TEMPLATES as defined within the Mission Editor.
--
-- The singleton object **_DATABASE** is automatically created by MOOSE, that administers all objects within the mission.
-- Moose refers to **_DATABASE** within the framework extensively, but you can also refer to the _DATABASE object within your missions if required.
--
-- @field #DATABASE
DATABASE = {
  ClassName = "DATABASE",
  Templates = {
    Units = {},
    Groups = {},
    Statics = {},
    ClientsByName = {},
    ClientsByID = {},
  },
  UNITS = {},
  UNITS_Index = {},
  STATICS = {},
  GROUPS = {},
  PLAYERS = {},
  PLAYERSJOINED = {},
  PLAYERUNITS = {},
  CLIENTS = {},
  CARGOS = {},
  AIRBASES = {},
  COUNTRY_ID = {},
  COUNTRY_NAME = {},
  NavPoints = {},
  PLAYERSETTINGS = {},
  ZONENAMES = {},
  HITS = {},
  DESTROYS = {},
  ZONES = {},
  ZONES_GOAL = {},
  WAREHOUSES = {},
  FLIGHTGROUPS = {},
  FLIGHTCONTROLS = {},
  OPSZONES = {},
  PATHLINES = {},
  STORAGES = {},
  STNS={},
  SADL={},
  DYNAMICCARGO={},
}

local _DATABASECoalition =
  {
    [1] = "Red",
    [2] = "Blue",
    [3] = "Neutral",
  }

local _DATABASECategory =
  {
    ["plane"] = Unit.Category.AIRPLANE,
    ["helicopter"] = Unit.Category.HELICOPTER,
    ["vehicle"] = Unit.Category.GROUND_UNIT,
    ["ship"] = Unit.Category.SHIP,
    ["static"] = Unit.Category.STRUCTURE,
  }


--- Creates a new DATABASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names.
-- @param #DATABASE self
-- @return #DATABASE
-- @usage
-- -- Define a new DATABASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE.
-- DBObject = DATABASE:New()
function DATABASE:New()

  -- Inherits from BASE
  local self = BASE:Inherit( self, BASE:New() ) -- #DATABASE

  self:SetEventPriority( 1 )

  self:HandleEvent( EVENTS.Birth, self._EventOnBirth )
  -- DCS 2.9 fixed CA event for players -- TODO: reset unit when leaving
  self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit )
  self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
  self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash )
  self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash )
  self:HandleEvent( EVENTS.UnitLost, self._EventOnDeadOrCrash )  -- DCS 2.7.1 for Aerial units no dead event ATM
  self:HandleEvent( EVENTS.Hit, self.AccountHits )
  self:HandleEvent( EVENTS.NewCargo )
  self:HandleEvent( EVENTS.DeleteCargo )
  self:HandleEvent( EVENTS.NewZone )
  self:HandleEvent( EVENTS.DeleteZone )
  --self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) -- This is not working anymore!, handling this through the birth event.
  self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit )
  -- DCS 2.9.7 Moose own dynamic cargo events
  self:HandleEvent( EVENTS.DynamicCargoRemoved, self._EventOnDynamicCargoRemoved)

  self:_RegisterTemplates()
  self:_RegisterGroupsAndUnits()
  self:_RegisterClients()
  self:_RegisterStatics()
  --self:_RegisterPlayers()
  --self:_RegisterAirbases()

  self.UNITS_Position = 0

  return self
end

--- Finds a Unit based on the Unit Name.
-- @param #DATABASE self
-- @param #string UnitName
-- @return Wrapper.Unit#UNIT The found Unit.
function DATABASE:FindUnit( UnitName )

  local UnitFound = self.UNITS[UnitName]
  return UnitFound
end


--- Adds a Unit based on the Unit Name in the DATABASE.
-- @param #DATABASE self
-- @param #string DCSUnitName Unit name.
-- @param #boolean force
-- @return Wrapper.Unit#UNIT The added unit.
function DATABASE:AddUnit( DCSUnitName, force )
  
  local DCSunitName = DCSUnitName
  
  if type(DCSunitName) == "number" then DCSunitName = string.format("%d",DCSUnitName) end
  
  if not self.UNITS[DCSunitName] or force == true then
    -- Debug info.
    self:T( { "Add UNIT:", DCSunitName } )

    -- Register unit
    self.UNITS[DCSunitName]=UNIT:Register(DCSunitName)
  end

  return self.UNITS[DCSunitName]
end


--- Deletes a Unit from the DATABASE based on the Unit Name.
-- @param #DATABASE self
function DATABASE:DeleteUnit( DCSUnitName )
  self:T("DeleteUnit "..tostring(DCSUnitName))
  self.UNITS[DCSUnitName] = nil
end

--- Adds a Static based on the Static Name in the DATABASE.
-- @param #DATABASE self
-- @param #string DCSStaticName Name of the static.
-- @return Wrapper.Static#STATIC The static object.
function DATABASE:AddStatic( DCSStaticName )

  if not self.STATICS[DCSStaticName] then
    self.STATICS[DCSStaticName] = STATIC:Register( DCSStaticName )
  end

  return self.STATICS[DCSStaticName]
end


--- Deletes a Static from the DATABASE based on the Static Name.
-- @param #DATABASE self
function DATABASE:DeleteStatic( DCSStaticName )
  self.STATICS[DCSStaticName] = nil
end

--- Finds a STATIC based on the Static Name.
-- @param #DATABASE self
-- @param #string StaticName Name of the static object.
-- @return Wrapper.Static#STATIC The found STATIC.
function DATABASE:FindStatic( StaticName )
  local StaticFound = self.STATICS[StaticName]
  return StaticFound
end

--- Add a DynamicCargo to the database.
-- @param #DATABASE self
-- @param #string Name Name of the dynamic cargo.
-- @return Wrapper.DynamicCargo#DYNAMICCARGO The dynamic cargo object.
function DATABASE:AddDynamicCargo( Name )
  if not self.DYNAMICCARGO[Name] then
    self.DYNAMICCARGO[Name] = DYNAMICCARGO:Register(Name)
  end
  return self.DYNAMICCARGO[Name]
end

--- Finds a DYNAMICCARGO based on the Dynamic Cargo Name.
-- @param #DATABASE self
-- @param #string DynamicCargoName
-- @return Wrapper.DynamicCargo#DYNAMICCARGO The found DYNAMICCARGO.
function DATABASE:FindDynamicCargo( DynamicCargoName )
  local StaticFound = self.DYNAMICCARGO[DynamicCargoName]
  return StaticFound
end

--- Deletes a DYNAMICCARGO from the DATABASE based on the Dynamic Cargo Name.
-- @param #DATABASE self
function DATABASE:DeleteDynamicCargo( DynamicCargoName )
  self.DYNAMICCARGO[DynamicCargoName] = nil
  return self
end

--- Adds a Airbase based on the Airbase Name in the DATABASE.
-- @param #DATABASE self
-- @param #string AirbaseName The name of the airbase.
-- @return Wrapper.Airbase#AIRBASE Airbase object.
function DATABASE:AddAirbase( AirbaseName )

  if not self.AIRBASES[AirbaseName] then
    self.AIRBASES[AirbaseName] = AIRBASE:Register( AirbaseName )
  end

  return self.AIRBASES[AirbaseName]
end


--- Deletes a Airbase from the DATABASE based on the Airbase Name.
-- @param #DATABASE self
-- @param #string AirbaseName The name of the airbase
function DATABASE:DeleteAirbase( AirbaseName )

  self.AIRBASES[AirbaseName] = nil
end

--- Finds an AIRBASE based on the AirbaseName.
-- @param #DATABASE self
-- @param #string AirbaseName
-- @return Wrapper.Airbase#AIRBASE The found AIRBASE.
function DATABASE:FindAirbase( AirbaseName )

  local AirbaseFound = self.AIRBASES[AirbaseName]
  return AirbaseFound
end



--- Adds a STORAGE (DCS warehouse wrapper) based on the Airbase Name to the DATABASE.
-- @param #DATABASE self
-- @param #string AirbaseName The name of the airbase.
-- @return Wrapper.Storage#STORAGE Storage object.
function DATABASE:AddStorage( AirbaseName )

  if not self.STORAGES[AirbaseName] then
    self.STORAGES[AirbaseName] = STORAGE:New( AirbaseName )
  end

  return self.STORAGES[AirbaseName]
end


--- Deletes a STORAGE from the DATABASE based on the name of the associated airbase.
-- @param #DATABASE self
-- @param #string AirbaseName The name of the airbase.
function DATABASE:DeleteStorage( AirbaseName )
  self.STORAGES[AirbaseName] = nil
end


--- Finds an STORAGE based on the name of the associated airbase.
-- @param #DATABASE self
-- @param #string AirbaseName Name of the airbase.
-- @return Wrapper.Storage#STORAGE The found STORAGE.
function DATABASE:FindStorage( AirbaseName )
  local storage = self.STORAGES[AirbaseName]
  return storage
end

do -- Zones and Pathlines

  --- Finds a @{Core.Zone} based on the zone name.
  -- @param #DATABASE self
  -- @param #string ZoneName The name of the zone.
  -- @return Core.Zone#ZONE_BASE The found ZONE.
  function DATABASE:FindZone( ZoneName )

    local ZoneFound = self.ZONES[ZoneName]
    return ZoneFound
  end

  --- Adds a @{Core.Zone} based on the zone name in the DATABASE.
  -- @param #DATABASE self
  -- @param #string ZoneName The name of the zone.
  -- @param Core.Zone#ZONE_BASE Zone The zone.
  function DATABASE:AddZone( ZoneName, Zone )

    if not self.ZONES[ZoneName] then
      self.ZONES[ZoneName] = Zone
    end
  end

  --- Deletes a @{Core.Zone} from the DATABASE based on the zone name.
  -- @param #DATABASE self
  -- @param #string ZoneName The name of the zone.
  function DATABASE:DeleteZone( ZoneName )

    self.ZONES[ZoneName] = nil
  end


  --- Adds a @{Core.Pathline} based on its name in the DATABASE.
  -- @param #DATABASE self
  -- @param #string PathlineName The name of the pathline
  -- @param Core.Pathline#PATHLINE Pathline The pathline.
  function DATABASE:AddPathline( PathlineName, Pathline )

    if not self.PATHLINES[PathlineName] then
      self.PATHLINES[PathlineName]=Pathline
    end
  end

  --- Finds a @{Core.Pathline} by its name.
  -- @param #DATABASE self
  -- @param #string PathlineName The name of the Pathline.
  -- @return Core.Pathline#PATHLINE The found PATHLINE.
  function DATABASE:FindPathline( PathlineName )

    local pathline = self.PATHLINES[PathlineName]

    return pathline
  end


  --- Deletes a @{Core.Pathline} from the DATABASE based on its name.
  -- @param #DATABASE self
  -- @param #string PathlineName The name of the PATHLINE.
  function DATABASE:DeletePathline( PathlineName )

    self.PATHLINES[PathlineName]=nil

    return self
  end

  --- Private method that registers new ZONE_BASE derived objects within the DATABASE Object.
  -- @param #DATABASE self
  -- @return #DATABASE self
  function DATABASE:_RegisterZones()

    for ZoneID, ZoneData in pairs(env.mission.triggers.zones) do
      local ZoneName = ZoneData.name

      -- Color
      local color=ZoneData.color or {1, 0, 0, 0.15}

      -- Create new Zone
      local Zone=nil   --Core.Zone#ZONE_BASE

      if ZoneData.type==0 then

        ---
        -- Circular zone
        ---

        self:I(string.format("Register ZONE: %s (Circular)", ZoneName))

        Zone=ZONE:New(ZoneName)

      else

        ---
        -- Quad-point zone
        ---

        self:I(string.format("Register ZONE: %s (Polygon, Quad)", ZoneName))

        Zone=ZONE_POLYGON:NewFromPointsArray(ZoneName, ZoneData.verticies)

        --for i,vec2 in pairs(ZoneData.verticies) do
        --  local coord=COORDINATE:NewFromVec2(vec2)
        --  coord:MarkToAll(string.format("%s Point %d", ZoneName, i))
        --end

      end

      if Zone then

        -- Store color of zone.
        Zone.Color=color

        -- Store zone ID.
        Zone.ZoneID=ZoneData.zoneId

        -- Store zone properties (if any)
        local ZoneProperties = ZoneData.properties or nil
        Zone.Properties = {}
        if ZoneName and ZoneProperties then
          for _,ZoneProp in ipairs(ZoneProperties) do
            if ZoneProp.key then
              Zone.Properties[ZoneProp.key] = ZoneProp.value
            end
          end
        end

        -- Store in DB.
        self.ZONENAMES[ZoneName] = ZoneName

        -- Add zone.
        self:AddZone(ZoneName, Zone)

      end

    end

    -- Polygon zones defined by late activated groups.
    for ZoneGroupName, ZoneGroup in pairs( self.GROUPS ) do
      if ZoneGroupName:match("#ZONE_POLYGON") then

        local ZoneName1 = ZoneGroupName:match("(.*)#ZONE_POLYGON")
        local ZoneName2 = ZoneGroupName:match(".*#ZONE_POLYGON(.*)")
        local ZoneName = ZoneName1 .. ( ZoneName2 or "" )

        -- Debug output
        self:I(string.format("Register ZONE: %s (Polygon)", ZoneName))

        -- Create a new polygon zone.
        local Zone_Polygon = ZONE_POLYGON:New( ZoneName, ZoneGroup )

        -- Set color.
        Zone_Polygon:SetColor({1, 0, 0}, 0.15)

        -- Store name in DB.
        self.ZONENAMES[ZoneName] = ZoneName

        -- Add zone to DB.
        self:AddZone( ZoneName, Zone_Polygon )
      end

    end

    -- Drawings as zones
    if env.mission.drawings and env.mission.drawings.layers then

      -- Loop over layers.
      for layerID, layerData in pairs(env.mission.drawings.layers or {}) do
        
        -- Loop over objects in layers.
        for objectID, objectData in pairs(layerData.objects or {}) do
          
          -- Check for polygon which has at least 4 points (we would need 3 but the origin seems to be there twice)
          if objectData.polygonMode and (objectData.polygonMode=="free") and objectData.points and #objectData.points>=4 then

            ---
            -- Drawing: Polygon free
            ---

            -- Name of the zone.
            local ZoneName=objectData.name or "Unknown free Polygon Drawing"

            -- Reference point. All other points need to be translated by this.
            local vec2={x=objectData.mapX, y=objectData.mapY}

            -- Debug stuff.
            --local vec3={x=objectData.mapX, y=0, z=objectData.mapY}
            --local coord=COORDINATE:NewFromVec2(vec2):MarkToAll("MapX, MapY")
            --trigger.action.markToAll(id,  "mapXY", vec3)

            -- Copy points array.
            local points=UTILS.DeepCopy(objectData.points)

            -- Translate points.
            for i,_point in pairs(points) do
              local point=_point --DCS#Vec2
              points[i]=UTILS.Vec2Add(point, vec2)
            end

            -- Remove last point.
            table.remove(points, #points)

            -- Debug output
            self:I(string.format("Register ZONE: %s (Polygon (free) drawing with %d vertices)", ZoneName, #points))

            -- Create new polygon zone.
            local Zone=ZONE_POLYGON:NewFromPointsArray(ZoneName, points)
            
            --Zone.DrawID = objectID
            
            -- Set color.
            Zone:SetColor({1, 0, 0}, 0.15)
            Zone:SetFillColor({1, 0, 0}, 0.15)
            
            if objectData.colorString then 
              -- eg colorString = 0xff0000ff
              local color = string.gsub(objectData.colorString,"^0x","")
              local r = tonumber(string.sub(color,1,2),16)/255
              local g = tonumber(string.sub(color,3,4),16)/255
              local b = tonumber(string.sub(color,5,6),16)/255
              local a = tonumber(string.sub(color,7,8),16)/255
              Zone:SetColor({r, g, b}, a)
            end
            if objectData.fillColorString then 
              -- eg fillColorString = 0xff00004b
              local color = string.gsub(objectData.colorString,"^0x","")
              local r = tonumber(string.sub(color,1,2),16)/255
              local g = tonumber(string.sub(color,3,4),16)/255
              local b = tonumber(string.sub(color,5,6),16)/255
              local a = tonumber(string.sub(color,7,8),16)/255
              Zone:SetFillColor({r, g, b}, a)
            end
            
            -- Store in DB.
            self.ZONENAMES[ZoneName] = ZoneName

            -- Add zone.
            self:AddZone(ZoneName, Zone)

          -- Check for polygon which has at least 4 points (we would need 3 but the origin seems to be there twice)
          elseif objectData.polygonMode and objectData.polygonMode=="rect" then

            ---
            -- Drawing: Polygon rect
            ---

            -- Name of the zone.
            local ZoneName=objectData.name or "Unknown rect Polygon Drawing"

            -- Reference point (center of the rectangle).
            local vec2={x=objectData.mapX, y=objectData.mapY}

            -- For a rectangular polygon drawing, we have the width (y) and height (x).
            local w=objectData.width
            local h=objectData.height
            local rotation = UTILS.ToRadian(objectData.angle or 0)

            local sinRot = math.sin(rotation)
            local cosRot = math.cos(rotation)
            local dx = h / 2
            local dy = w / 2

            local points = {
                { x = -dx * cosRot - (-dy * sinRot) + vec2.x, y = -dx * sinRot + (-dy * cosRot) + vec2.y },
                { x = dx * cosRot - (-dy * sinRot) + vec2.x, y = dx * sinRot + (-dy * cosRot) + vec2.y },
                { x = dx * cosRot - (dy * sinRot) + vec2.x, y = dx * sinRot + (dy * cosRot) + vec2.y },
                { x = -dx * cosRot - (dy * sinRot) + vec2.x, y = -dx * sinRot + (dy * cosRot) + vec2.y },
            }

            --local coord=COORDINATE:NewFromVec2(vec2):MarkToAll("MapX, MapY")

            -- Debug output
            self:I(string.format("Register ZONE: %s (Polygon (rect) drawing with %d vertices)", ZoneName, #points))

            -- Create new polygon zone.
            local Zone=ZONE_POLYGON:NewFromPointsArray(ZoneName, points)

            -- Set color.
            Zone:SetColor({1, 0, 0}, 0.15)
            
           if objectData.colorString then 
              -- eg colorString = 0xff0000ff
              local color = string.gsub(objectData.colorString,"^0x","")
              local r = tonumber(string.sub(color,1,2),16)/255
              local g = tonumber(string.sub(color,3,4),16)/255
              local b = tonumber(string.sub(color,5,6),16)/255
              local a = tonumber(string.sub(color,7,8),16)/255
              Zone:SetColor({r, g, b}, a)
            end
            if objectData.fillColorString then 
              -- eg fillColorString = 0xff00004b
              local color = string.gsub(objectData.colorString,"^0x","")
              local r = tonumber(string.sub(color,1,2),16)/255
              local g = tonumber(string.sub(color,3,4),16)/255
              local b = tonumber(string.sub(color,5,6),16)/255
              local a = tonumber(string.sub(color,7,8),16)/255
              Zone:SetFillColor({r, g, b}, a)
            end
            
            -- Store in DB.
            self.ZONENAMES[ZoneName] = ZoneName

            -- Add zone.
            self:AddZone(ZoneName, Zone)

          elseif objectData.lineMode and (objectData.lineMode=="segments" or objectData.lineMode=="segment" or objectData.lineMode=="free") and objectData.points and #objectData.points>=2 then

            ---
            -- Drawing: Line (segments, segment or free)
            ---

           -- Name of the zone.
            local Name=objectData.name or "Unknown Line Drawing"

            -- Reference point. All other points need to be translated by this.
            local vec2={x=objectData.mapX, y=objectData.mapY}

            -- Copy points array.
            local points=UTILS.DeepCopy(objectData.points)

            -- Translate points.
            for i,_point in pairs(points) do
              local point=_point --DCS#Vec2
              points[i]=UTILS.Vec2Add(point, vec2)
            end

            -- Debug output
            self:I(string.format("Register PATHLINE: %s (Line drawing with %d points)", Name, #points))

            -- Create new polygon zone.
            local Pathline=PATHLINE:NewFromVec2Array(Name, points)

            -- Set color.
            --Zone:SetColor({1, 0, 0}, 0.15)

            -- Add zone.
            self:AddPathline(Name,Pathline)

          end

        end

      end

    end


  end

end -- zone

do -- Zone_Goal

  --- Finds a @{Core.Zone} based on the zone name.
  -- @param #DATABASE self
  -- @param #string ZoneName The name of the zone.
  -- @return Core.Zone#ZONE_BASE The found ZONE.
  function DATABASE:FindZoneGoal( ZoneName )

    local ZoneFound = self.ZONES_GOAL[ZoneName]
    return ZoneFound
  end

  --- Adds a @{Core.Zone} based on the zone name in the DATABASE.
  -- @param #DATABASE self
  -- @param #string ZoneName The name of the zone.
  -- @param Core.Zone#ZONE_BASE Zone The zone.
  function DATABASE:AddZoneGoal( ZoneName, Zone )

    if not self.ZONES_GOAL[ZoneName] then
      self.ZONES_GOAL[ZoneName] = Zone
    end
  end


  --- Deletes a @{Core.Zone} from the DATABASE based on the zone name.
  -- @param #DATABASE self
  -- @param #string ZoneName The name of the zone.
  function DATABASE:DeleteZoneGoal( ZoneName )

    self.ZONES_GOAL[ZoneName] = nil
  end

end -- Zone_Goal

do -- OpsZone

  --- Finds a @{Ops.OpsZone#OPSZONE} based on the zone name.
  -- @param #DATABASE self
  -- @param #string ZoneName The name of the zone.
  -- @return Ops.OpsZone#OPSZONE The found OPSZONE.
  function DATABASE:FindOpsZone( ZoneName )

    local ZoneFound = self.OPSZONES[ZoneName]

    return ZoneFound
  end

  --- Adds a @{Ops.OpsZone#OPSZONE} based on the zone name in the DATABASE.
  -- @param #DATABASE self
  -- @param Ops.OpsZone#OPSZONE OpsZone The zone.
  function DATABASE:AddOpsZone( OpsZone )

    if OpsZone then

      local ZoneName=OpsZone:GetName()

      if not self.OPSZONES[ZoneName] then
        self.OPSZONES[ZoneName] = OpsZone
      end

    end
  end


  --- Deletes a @{Ops.OpsZone#OPSZONE} from the DATABASE based on the zone name.
  -- @param #DATABASE self
  -- @param #string ZoneName The name of the zone.
  function DATABASE:DeleteOpsZone( ZoneName )
    self.OPSZONES[ZoneName] = nil
  end

end -- OpsZone

do -- cargo

  --- Adds a Cargo based on the Cargo Name in the DATABASE.
  -- @param #DATABASE self
  -- @param #string CargoName The name of the airbase
  function DATABASE:AddCargo( Cargo )

    if not self.CARGOS[Cargo.Name] then
      self.CARGOS[Cargo.Name] = Cargo
    end
  end


  --- Deletes a Cargo from the DATABASE based on the Cargo Name.
  -- @param #DATABASE self
  -- @param #string CargoName The name of the airbase
  function DATABASE:DeleteCargo( CargoName )

    self.CARGOS[CargoName] = nil
  end

  --- Finds an CARGO based on the CargoName.
  -- @param #DATABASE self
  -- @param #string CargoName
  -- @return Cargo.Cargo#CARGO The found CARGO.
  function DATABASE:FindCargo( CargoName )

    local CargoFound = self.CARGOS[CargoName]
    return CargoFound
  end

  --- Checks if the Template name has a #CARGO tag.
  -- If yes, the group is a cargo.
  -- @param #DATABASE self
  -- @param #string TemplateName
  -- @return #boolean
  function DATABASE:IsCargo( TemplateName )

    TemplateName = env.getValueDictByKey( TemplateName )

    local Cargo = TemplateName:match( "#(CARGO)" )

    return Cargo and Cargo == "CARGO"
  end

  --- Private method that registers new Static Templates within the DATABASE Object.
  -- @param #DATABASE self
  -- @return #DATABASE self
  function DATABASE:_RegisterCargos()

    local Groups = UTILS.DeepCopy( self.GROUPS ) -- This is a very important statement. CARGO_GROUP:New creates a new _DATABASE.GROUP entry, which will confuse the loop. I searched 4 hours on this to find the bug!

    for CargoGroupName, CargoGroup in pairs( Groups ) do
      if self:IsCargo( CargoGroupName ) then
        local CargoInfo = CargoGroupName:match("#CARGO(.*)")
        local CargoParam = CargoInfo and CargoInfo:match( "%((.*)%)")
        local CargoName1 = CargoGroupName:match("(.*)#CARGO%(.*%)")
        local CargoName2 = CargoGroupName:match(".*#CARGO%(.*%)(.*)")
        local CargoName = CargoName1 .. ( CargoName2 or "" )
        local Type = CargoParam and CargoParam:match( "T=([%a%d ]+),?")
        local Name = CargoParam and CargoParam:match( "N=([%a%d]+),?") or CargoName
        local LoadRadius = CargoParam and tonumber( CargoParam:match( "RR=([%a%d]+),?") )
        local NearRadius = CargoParam and tonumber( CargoParam:match( "NR=([%a%d]+),?") )

        self:I({"Register CargoGroup:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius})
        CARGO_GROUP:New( CargoGroup, Type, Name, LoadRadius, NearRadius )
      end
    end

    for CargoStaticName, CargoStatic in pairs( self.STATICS ) do
      if self:IsCargo( CargoStaticName ) then
        local CargoInfo = CargoStaticName:match("#CARGO(.*)")
        local CargoParam = CargoInfo and CargoInfo:match( "%((.*)%)")
        local CargoName = CargoStaticName:match("(.*)#CARGO")
        local Type = CargoParam and CargoParam:match( "T=([%a%d ]+),?")
        local Category = CargoParam and CargoParam:match( "C=([%a%d ]+),?")
        local Name = CargoParam and CargoParam:match( "N=([%a%d]+),?") or CargoName
        local LoadRadius = CargoParam and tonumber( CargoParam:match( "RR=([%a%d]+),?") )
        local NearRadius = CargoParam and tonumber( CargoParam:match( "NR=([%a%d]+),?") )

        if Category == "SLING" then
          self:I({"Register CargoSlingload:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius})
          CARGO_SLINGLOAD:New( CargoStatic, Type, Name, LoadRadius, NearRadius )
        else
          if Category == "CRATE" then
            self:I({"Register CargoCrate:",Type=Type,Name=Name,LoadRadius=LoadRadius,NearRadius=NearRadius})
            CARGO_CRATE:New( CargoStatic, Type, Name, LoadRadius, NearRadius )
          end
        end
      end
    end

  end

end -- cargo

--- Finds a CLIENT based on the ClientName.
-- @param #DATABASE self
-- @param #string ClientName - Note this is the UNIT name of the client!
-- @return Wrapper.Client#CLIENT The found CLIENT.
function DATABASE:FindClient( ClientName )

  local ClientFound = self.CLIENTS[ClientName]
  return ClientFound
end


--- Adds a CLIENT based on the ClientName in the DATABASE.
-- @param #DATABASE self
-- @param #string ClientName Name of the Client unit.
-- @param #boolean Force (optional) Force registration of client.
-- @return Wrapper.Client#CLIENT The client object.
function DATABASE:AddClient( ClientName, Force )
  
  local DCSUnitName = ClientName
  
  if type(DCSUnitName) == "number" then DCSUnitName = string.format("%d",ClientName) end
  
  if not self.CLIENTS[DCSUnitName] or Force == true then
    self.CLIENTS[DCSUnitName] = CLIENT:Register( DCSUnitName )
  end

  return self.CLIENTS[DCSUnitName]
end


--- Finds a GROUP based on the GroupName.
-- @param #DATABASE self
-- @param #string GroupName
-- @return Wrapper.Group#GROUP The found GROUP.
function DATABASE:FindGroup( GroupName )

  if type(GroupName) ~= "string" or GroupName == "" then return end

  local GroupFound = self.GROUPS[GroupName]
  
  if GroupFound == nil and GroupName ~= nil and self.Templates.Groups[GroupName] == nil then
    -- see if the group exists in the API, maybe a dynamic slot
    self:_RegisterDynamicGroup(GroupName)
    return self.GROUPS[GroupName]
  end
  
  return GroupFound
end


--- Adds a GROUP based on the GroupName in the DATABASE.
-- @param #DATABASE self
-- @param #string GroupName
-- @param #boolean force
-- @return Wrapper.Group#GROUP The Group
function DATABASE:AddGroup( GroupName, force )

  if not self.GROUPS[GroupName] or force == true then
    self:T( { "Add GROUP:", GroupName } )
    self.GROUPS[GroupName] = GROUP:Register( GroupName )
  end

  return self.GROUPS[GroupName]
end

--- Adds a player based on the Player Name in the DATABASE.
-- @param #DATABASE self
function DATABASE:AddPlayer( UnitName, PlayerName )
  
  if type(UnitName) == "number" then UnitName = string.format("%d",UnitName) end
  
  if PlayerName then
    self:I( { "Add player for unit:", UnitName, PlayerName } )
    self.PLAYERS[PlayerName] = UnitName
    self.PLAYERUNITS[PlayerName] = self:FindUnit( UnitName )
    self.PLAYERSJOINED[PlayerName] = PlayerName
  end
  
end

--- Get a PlayerName by UnitName from PLAYERS in DATABASE.
-- @param #DATABASE self
-- @return #string PlayerName
-- @return Wrapper.Unit#UNIT PlayerUnit
function DATABASE:_FindPlayerNameByUnitName(UnitName)
  if UnitName then
    for playername,unitname in pairs(self.PLAYERS) do
      if unitname == UnitName and self.PLAYERUNITS[playername] and self.PLAYERUNITS[playername]:IsAlive() then
        return playername, self.PLAYERUNITS[playername]
      end
    end
  end
  return nil
end

--- Deletes a player from the DATABASE based on the Player Name.
-- @param #DATABASE self
function DATABASE:DeletePlayer( UnitName, PlayerName )

  if PlayerName then
    self:T( { "Clean player:", PlayerName } )
    self.PLAYERS[PlayerName] = nil
    self.PLAYERUNITS[PlayerName] = nil
  end
end

--- Get the player table from the DATABASE.
-- The player table contains all unit names with the key the name of the player (PlayerName).
-- @param #DATABASE self
-- @usage
--   local Players = _DATABASE:GetPlayers()
--   for PlayerName, UnitName in pairs( Players ) do
--     ..
--   end
function DATABASE:GetPlayers()
  return self.PLAYERS
end


--- Get the player table from the DATABASE, which contains all UNIT objects.
-- The player table contains all UNIT objects of the player with the key the name of the player (PlayerName).
-- @param #DATABASE self
-- @usage
--   local PlayerUnits = _DATABASE:GetPlayerUnits()
--   for PlayerName, PlayerUnit in pairs( PlayerUnits ) do
--     ..
--   end
function DATABASE:GetPlayerUnits()
  return self.PLAYERUNITS
end


--- Get the player table from the DATABASE which have joined in the mission historically.
-- The player table contains all UNIT objects with the key the name of the player (PlayerName).
-- @param #DATABASE self
-- @usage
--   local PlayersJoined = _DATABASE:GetPlayersJoined()
--   for PlayerName, PlayerUnit in pairs( PlayersJoined ) do
--     ..
--   end
function DATABASE:GetPlayersJoined()
  return self.PLAYERSJOINED
end


--- Instantiate new Groups within the DCSRTE.
-- This method expects EXACTLY the same structure as a structure within the ME, and needs 2 additional fields defined:
-- SpawnCountryID, SpawnCategoryID
-- This method is used by the SPAWN class.
-- @param #DATABASE self
-- @param #table SpawnTemplate Template of the group to spawn.
-- @return Wrapper.Group#GROUP Spawned group.
function DATABASE:Spawn( SpawnTemplate )
  self:F( SpawnTemplate.name )

  self:T( { SpawnTemplate.SpawnCountryID, SpawnTemplate.SpawnCategoryID } )

  -- Copy the spawn variables of the template in temporary storage, nullify, and restore the spawn variables.
  local SpawnCoalitionID = SpawnTemplate.CoalitionID
  local SpawnCountryID = SpawnTemplate.CountryID
  local SpawnCategoryID = SpawnTemplate.CategoryID

  -- Nullify
  SpawnTemplate.CoalitionID = nil
  SpawnTemplate.CountryID = nil
  SpawnTemplate.CategoryID = nil

  self:_RegisterGroupTemplate( SpawnTemplate, SpawnCoalitionID, SpawnCategoryID, SpawnCountryID, SpawnTemplate.name  )

  self:T3( SpawnTemplate )
  coalition.addGroup( SpawnCountryID, SpawnCategoryID, SpawnTemplate )

  -- Restore
  SpawnTemplate.CoalitionID = SpawnCoalitionID
  SpawnTemplate.CountryID = SpawnCountryID
  SpawnTemplate.CategoryID = SpawnCategoryID

  -- Ensure that for the spawned group and its units, there are GROUP and UNIT objects created in the DATABASE.
  local SpawnGroup = self:AddGroup( SpawnTemplate.name )
  for UnitID, UnitData in pairs( SpawnTemplate.units ) do
    self:AddUnit( UnitData.name )
  end

  return SpawnGroup
end

--- Set a status to a Group within the Database, this to check crossing events for example.
-- @param #DATABASE self
-- @param #string GroupName Group name.
-- @param #string Status Status.
function DATABASE:SetStatusGroup( GroupName, Status )
  self:F2( Status )

  self.Templates.Groups[GroupName].Status = Status
end

--- Get a status to a Group within the Database, this to check crossing events for example.
-- @param #DATABASE self
-- @param #string GroupName Group name.
-- @return #string Status or an empty string "".
function DATABASE:GetStatusGroup( GroupName )
  self:F2( GroupName )

  if self.Templates.Groups[GroupName] then
    return self.Templates.Groups[GroupName].Status
  else
    return ""
  end
end

--- Private method that registers new Group Templates within the DATABASE Object.
-- @param #DATABASE self
-- @param #table GroupTemplate
-- @param DCS#coalition.side CoalitionSide The coalition.side of the object.
-- @param DCS#Object.Category CategoryID The Object.category of the object.
-- @param DCS#country.id CountryID the country ID of the object.
-- @param #string GroupName (Optional) The name of the group. Default is `GroupTemplate.name`.
-- @return #DATABASE self
function DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, CategoryID, CountryID, GroupName )

  local GroupTemplateName = GroupName or env.getValueDictByKey( GroupTemplate.name )

  if not self.Templates.Groups[GroupTemplateName] then
    self.Templates.Groups[GroupTemplateName] = {}
    self.Templates.Groups[GroupTemplateName].Status = nil
  end

  -- Delete the spans from the route, it is not needed and takes memory.
  if GroupTemplate.route and GroupTemplate.route.spans then
    GroupTemplate.route.spans = nil
  end

  GroupTemplate.CategoryID = CategoryID
  GroupTemplate.CoalitionID = CoalitionSide
  GroupTemplate.CountryID = CountryID

  self.Templates.Groups[GroupTemplateName].GroupName = GroupTemplateName
  self.Templates.Groups[GroupTemplateName].Template = GroupTemplate
  self.Templates.Groups[GroupTemplateName].groupId = GroupTemplate.groupId
  self.Templates.Groups[GroupTemplateName].UnitCount = #GroupTemplate.units
  self.Templates.Groups[GroupTemplateName].Units = GroupTemplate.units
  self.Templates.Groups[GroupTemplateName].CategoryID = CategoryID
  self.Templates.Groups[GroupTemplateName].CoalitionID = CoalitionSide
  self.Templates.Groups[GroupTemplateName].CountryID = CountryID
  
  local UnitNames = {}

  for unit_num, UnitTemplate in pairs( GroupTemplate.units ) do

    UnitTemplate.name = env.getValueDictByKey(UnitTemplate.name)

    self.Templates.Units[UnitTemplate.name] = {}
    self.Templates.Units[UnitTemplate.name].UnitName = UnitTemplate.name
    self.Templates.Units[UnitTemplate.name].Template = UnitTemplate
    self.Templates.Units[UnitTemplate.name].GroupName = GroupTemplateName
    self.Templates.Units[UnitTemplate.name].GroupTemplate = GroupTemplate
    self.Templates.Units[UnitTemplate.name].GroupId = GroupTemplate.groupId
    self.Templates.Units[UnitTemplate.name].CategoryID = CategoryID
    self.Templates.Units[UnitTemplate.name].CoalitionID = CoalitionSide
    self.Templates.Units[UnitTemplate.name].CountryID = CountryID

    if UnitTemplate.skill and (UnitTemplate.skill == "Client" or UnitTemplate.skill == "Player") then
      self.Templates.ClientsByName[UnitTemplate.name] = UnitTemplate
      self.Templates.ClientsByName[UnitTemplate.name].CategoryID = CategoryID
      self.Templates.ClientsByName[UnitTemplate.name].CoalitionID = CoalitionSide
      self.Templates.ClientsByName[UnitTemplate.name].CountryID = CountryID
      self.Templates.ClientsByID[UnitTemplate.unitId] = UnitTemplate
    end
    
    if UnitTemplate.AddPropAircraft then
      if UnitTemplate.AddPropAircraft.STN_L16 then
        local stn = UTILS.OctalToDecimal(UnitTemplate.AddPropAircraft.STN_L16)
        if stn == nil or stn < 1 then
          self:E("WARNING: Invalid STN "..tostring(UnitTemplate.AddPropAircraft.STN_L16).." for ".. UnitTemplate.name)
        else
          self.STNS[stn] = UnitTemplate.name
          self:T("Register STN "..tostring(UnitTemplate.AddPropAircraft.STN_L16).." for ".. UnitTemplate.name)
        end
      end
      if UnitTemplate.AddPropAircraft.SADL_TN then
        local sadl = UTILS.OctalToDecimal(UnitTemplate.AddPropAircraft.SADL_TN)
        if sadl == nil or sadl < 1 then
          self:E("WARNING: Invalid SADL "..tostring(UnitTemplate.AddPropAircraft.SADL_TN).." for ".. UnitTemplate.name)
        else
          self.SADL[sadl] = UnitTemplate.name
          self:T("Register SADL "..tostring(UnitTemplate.AddPropAircraft.SADL_TN).." for ".. UnitTemplate.name)
        end
      end  
    end

    UnitNames[#UnitNames+1] = self.Templates.Units[UnitTemplate.name].UnitName
  end
    
  -- Debug info.
  self:T( { Group     = self.Templates.Groups[GroupTemplateName].GroupName,
            Coalition = self.Templates.Groups[GroupTemplateName].CoalitionID,
            Category  = self.Templates.Groups[GroupTemplateName].CategoryID,
            Country   = self.Templates.Groups[GroupTemplateName].CountryID,
            Units     = UnitNames
          }
        )
end

--- Get next (consecutive) free STN as octal number.
-- @param #DATABASE self
-- @param #number octal Starting octal.
-- @param #string unitname Name of the associated unit.
-- @return #number Octal
function DATABASE:GetNextSTN(octal,unitname)
  local first = UTILS.OctalToDecimal(octal) or 0
  if self.STNS[first] == unitname then return octal end
  local nextoctal = 77777
  local found = false
  if 32767-first < 10 then
    first = 0
  end
  for i=first+1,32767 do
    if self.STNS[i] == nil then
      found = true
      nextoctal = UTILS.DecimalToOctal(i)
      self.STNS[i] = unitname
      self:T("Register STN "..tostring(nextoctal).." for ".. unitname)
      break
    end
  end
  if not found then
    self:E(string.format("WARNING: No next free STN past %05d found!",octal))
    -- cleanup
    local NewSTNS = {}
    for _id,_name in pairs(self.STNS) do
      if self.UNITS[_name] ~= nil then
        NewSTNS[_id] = _name
      end
    end
    self.STNS = nil
    self.STNS = NewSTNS
  end
  return nextoctal 
end

--- Get next (consecutive) free SADL as octal number.
-- @param #DATABASE self
-- @param #number octal Starting octal.
-- @param #string unitname Name of the associated unit.
-- @return #number Octal
function DATABASE:GetNextSADL(octal,unitname)
  local first = UTILS.OctalToDecimal(octal) or 0
  if self.SADL[first] == unitname then return octal end
  local nextoctal = 7777
  local found = false
  if 4095-first < 10 then
    first = 0
  end
  for i=first+1,4095 do
    if self.STNS[i] == nil then
      found = true
      nextoctal = UTILS.DecimalToOctal(i)
      self.SADL[i] = unitname
      self:T("Register SADL "..tostring(nextoctal).." for ".. unitname)
      break
    end
  end
  if not found then
    self:E(string.format("WARNING: No next free SADL past %04d found!",octal))
    -- cleanup
    local NewSTNS = {}
    for _id,_name in pairs(self.SADL) do
      if self.UNITS[_name] ~= nil then
        NewSTNS[_id] = _name
      end
    end
    self.SADL = nil
    self.SADL = NewSTNS
  end
  return nextoctal 
end

--- Get group template.
-- @param #DATABASE self
-- @param #string GroupName Group name.
-- @return #table Group template table.
function DATABASE:GetGroupTemplate( GroupName )
  local GroupTemplate=nil
  if self.Templates.Groups[GroupName] then
    GroupTemplate = self.Templates.Groups[GroupName].Template
    GroupTemplate.SpawnCoalitionID = self.Templates.Groups[GroupName].CoalitionID
    GroupTemplate.SpawnCategoryID = self.Templates.Groups[GroupName].CategoryID
    GroupTemplate.SpawnCountryID = self.Templates.Groups[GroupName].CountryID
  end
  return GroupTemplate
end

--- Private method that registers new Static Templates within the DATABASE Object.
-- @param #DATABASE self
-- @param #table StaticTemplate Template table.
-- @param #number CoalitionID Coalition ID.
-- @param #number CategoryID Category ID.
-- @param #number CountryID Country ID.
-- @return #DATABASE self
function DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID )

  local StaticTemplate = UTILS.DeepCopy( StaticTemplate )

  local StaticTemplateGroupName = env.getValueDictByKey(StaticTemplate.name)

  local StaticTemplateName=StaticTemplate.units[1].name

  self.Templates.Statics[StaticTemplateName] = self.Templates.Statics[StaticTemplateName] or {}

  StaticTemplate.CategoryID = CategoryID
  StaticTemplate.CoalitionID = CoalitionID
  StaticTemplate.CountryID = CountryID

  self.Templates.Statics[StaticTemplateName].StaticName = StaticTemplateGroupName
  self.Templates.Statics[StaticTemplateName].GroupTemplate = StaticTemplate
  self.Templates.Statics[StaticTemplateName].UnitTemplate = StaticTemplate.units[1]
  self.Templates.Statics[StaticTemplateName].CategoryID = CategoryID
  self.Templates.Statics[StaticTemplateName].CoalitionID = CoalitionID
  self.Templates.Statics[StaticTemplateName].CountryID = CountryID

  -- Debug info.
  self:T( { Static = self.Templates.Statics[StaticTemplateName].StaticName,
            Coalition = self.Templates.Statics[StaticTemplateName].CoalitionID,
            Category = self.Templates.Statics[StaticTemplateName].CategoryID,
            Country = self.Templates.Statics[StaticTemplateName].CountryID
          }
        )

  self:AddStatic( StaticTemplateName )

  return self
end

--- Get a generic static cargo group template from scratch for dynamic cargo spawns register. Does not register the template!
-- @param #DATABASE self
-- @param #string Name Name of the static.
-- @param #string Typename Typename of the static. Defaults to "container_cargo".
-- @param #number Mass Mass of the static. Defaults to 0.
-- @param #number Coalition Coalition of the static. Defaults to coalition.side.BLUE.
-- @param #number Country Country of the static. Defaults to country.id.GERMANY.
-- @return #table Static template table.
function DATABASE:_GetGenericStaticCargoGroupTemplate(Name,Typename,Mass,Coalition,Country)
  local StaticTemplate = {}
  StaticTemplate.name = Name or "None"
  StaticTemplate.units = { [1] = { 
    name = Name, 
    resourcePayload = {
      ["weapons"] = {},
      ["aircrafts"] = {},
      ["gasoline"] = 0,
      ["diesel"] = 0,
      ["methanol_mixture"] = 0,
      ["jet_fuel"] = 0,   
    },
    ["mass"] = Mass or 0,
    ["category"] = "Cargos",
    ["canCargo"] = true,
    ["type"] = Typename or "container_cargo",
    ["rate"] = 100,
    ["y"] = 0,
    ["x"] = 0,
    ["heading"] = 0,
  }}
  StaticTemplate.CategoryID = "static"
  StaticTemplate.CoalitionID = Coalition or coalition.side.BLUE
  StaticTemplate.CountryID = Country or country.id.GERMANY
  --UTILS.PrintTableToLog(StaticTemplate)
  return StaticTemplate
end

--- Get static group template.
-- @param #DATABASE self
-- @param #string StaticName Name of the static.
-- @return #table Static template table.
function DATABASE:GetStaticGroupTemplate( StaticName )
  if self.Templates.Statics[StaticName] then
    local StaticTemplate = self.Templates.Statics[StaticName].GroupTemplate
    return StaticTemplate, self.Templates.Statics[StaticName].CoalitionID, self.Templates.Statics[StaticName].CategoryID, self.Templates.Statics[StaticName].CountryID
  else
    self:E("ERROR: Static group template does NOT exist for static "..tostring(StaticName))
    return nil
  end
end

--- Get static unit template.
-- @param #DATABASE self
-- @param #string StaticName Name of the static.
-- @return #table Static template table.
function DATABASE:GetStaticUnitTemplate( StaticName )
  if self.Templates.Statics[StaticName] then
    local UnitTemplate = self.Templates.Statics[StaticName].UnitTemplate
    return UnitTemplate, self.Templates.Statics[StaticName].CoalitionID, self.Templates.Statics[StaticName].CategoryID, self.Templates.Statics[StaticName].CountryID
  else
    self:E("ERROR: Static unit template does NOT exist for static "..tostring(StaticName))
    return nil
  end
end

--- Get group name from unit name.
-- @param #DATABASE self
-- @param #string UnitName Name of the unit.
-- @return #string Group name.
function DATABASE:GetGroupNameFromUnitName( UnitName )
  if self.Templates.Units[UnitName] then
    return self.Templates.Units[UnitName].GroupName
  else
    self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName))
    return nil
  end
end

--- Get group template from unit name.
-- @param #DATABASE self
-- @param #string UnitName Name of the unit.
-- @return #table Group template.
function DATABASE:GetGroupTemplateFromUnitName( UnitName )
  if self.Templates.Units[UnitName] then
    return self.Templates.Units[UnitName].GroupTemplate
  else
    self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName))
    return nil
  end
end

--- Get group template from unit name.
-- @param #DATABASE self
-- @param #string UnitName Name of the unit.
-- @return #table Group template.
function DATABASE:GetUnitTemplateFromUnitName( UnitName )
  if self.Templates.Units[UnitName] then
    return self.Templates.Units[UnitName]
  else
    self:E("ERROR: Unit template does not exist for unit "..tostring(UnitName))
    return nil
  end
end


--- Get coalition ID from client name.
-- @param #DATABASE self
-- @param #string ClientName Name of the Client.
-- @return #number Coalition ID.
function DATABASE:GetCoalitionFromClientTemplate( ClientName )
  if self.Templates.ClientsByName[ClientName] then  
    return self.Templates.ClientsByName[ClientName].CoalitionID
  end
  self:T("WARNING: Template does not exist for client "..tostring(ClientName))
  return nil
end

--- Get category ID from client name.
-- @param #DATABASE self
-- @param #string ClientName Name of the Client.
-- @return #number Category ID.
function DATABASE:GetCategoryFromClientTemplate( ClientName )
  if self.Templates.ClientsByName[ClientName] then  
    return self.Templates.ClientsByName[ClientName].CategoryID
  end
  self:T("WARNING: Template does not exist for client "..tostring(ClientName))
  return nil
end

--- Get country ID from client name.
-- @param #DATABASE self
-- @param #string ClientName Name of the Client.
-- @return #number Country ID.
function DATABASE:GetCountryFromClientTemplate( ClientName )
  if self.Templates.ClientsByName[ClientName] then  
    return self.Templates.ClientsByName[ClientName].CountryID
  end
  self:T("WARNING: Template does not exist for client "..tostring(ClientName))
  return nil  
end

--- Airbase

--- Get coalition ID from airbase name.
-- @param #DATABASE self
-- @param #string AirbaseName Name of the airbase.
-- @return #number Coalition ID.
function DATABASE:GetCoalitionFromAirbase( AirbaseName )
  return self.AIRBASES[AirbaseName]:GetCoalition()
end

--- Get category from airbase name.
-- @param #DATABASE self
-- @param #string AirbaseName Name of the airbase.
-- @return #number Category.
function DATABASE:GetCategoryFromAirbase( AirbaseName )
  return self.AIRBASES[AirbaseName]:GetAirbaseCategory()
end



--- Private method that registers all alive players in the mission.
-- @param #DATABASE self
-- @return #DATABASE self
function DATABASE:_RegisterPlayers()

  local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ), AlivePlayersNeutral = coalition.getPlayers( coalition.side.NEUTRAL ) }
  for CoalitionId, CoalitionData in pairs( CoalitionsData ) do
    for UnitId, UnitData in pairs( CoalitionData ) do
      self:T3( { "UnitData:", UnitData } )
      if UnitData and UnitData:isExist() then
        local UnitName = UnitData:getName()
        local PlayerName = UnitData:getPlayerName()
        if not self.PLAYERS[PlayerName] then
          self:I( { "Add player for unit:", UnitName, PlayerName } )
          self:AddPlayer( UnitName, PlayerName )
        end
      end
    end
  end

  return self
end

--- Private method that registers a single dynamic slot Group and Units within in the mission.
-- @param #DATABASE self
-- @return #DATABASE self
function DATABASE:_RegisterDynamicGroup(Groupname)
  local DCSGroup = Group.getByName(Groupname)
  if DCSGroup and DCSGroup:isExist() then
  
    -- Group name.
    local DCSGroupName = DCSGroup:getName()
  
    -- Add group.
    self:I(string.format("Register Group: %s", tostring(DCSGroupName)))
    self:AddGroup( DCSGroupName, true )
  
    -- Loop over units in group.
    for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do
  
      -- Get unit name.
      local DCSUnitName = DCSUnit:getName()
  
      -- Add unit.
      self:I(string.format("Register Unit: %s", tostring(DCSUnitName)))
      self:AddUnit( tostring(DCSUnitName), true )
  
    end
  else
    self:E({"Group does not exist: ", DCSGroup})
  end
  return self
end

--- Private method that registers all Groups and Units within in the mission.
-- @param #DATABASE self
-- @return #DATABASE self
function DATABASE:_RegisterGroupsAndUnits()

  local CoalitionsData = { GroupsRed = coalition.getGroups( coalition.side.RED ), GroupsBlue = coalition.getGroups( coalition.side.BLUE ),  GroupsNeutral = coalition.getGroups( coalition.side.NEUTRAL ) }

  for CoalitionId, CoalitionData in pairs( CoalitionsData ) do

    for DCSGroupId, DCSGroup in pairs( CoalitionData ) do

      if DCSGroup:isExist() then

        -- Group name.
        local DCSGroupName = DCSGroup:getName()

        -- Add group.
        self:I(string.format("Register Group: %s", tostring(DCSGroupName)))
        self:AddGroup( DCSGroupName )

        -- Loop over units in group.
        for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do

          -- Get unit name.
          local DCSUnitName = DCSUnit:getName()

          -- Add unit.
          self:I(string.format("Register Unit: %s", tostring(DCSUnitName)))
          self:AddUnit( DCSUnitName )

        end
      else
        self:E({"Group does not exist: ", DCSGroup})
      end

    end
  end

  return self
end

--- Private method that registers all Units of skill Client or Player within in the mission.
-- @param #DATABASE self
-- @return #DATABASE self
function DATABASE:_RegisterClients()

  for ClientName, ClientTemplate in pairs( self.Templates.ClientsByName ) do
    self:I(string.format("Register Client: %s", tostring(ClientName)))
    local client=self:AddClient( ClientName )
    client.SpawnCoord=COORDINATE:New(ClientTemplate.x, ClientTemplate.alt, ClientTemplate.y)
  end

  return self
end

--- Private method that registeres all static objects.
-- @param #DATABASE self
function DATABASE:_RegisterStatics()

  local CoalitionsData={GroupsRed=coalition.getStaticObjects(coalition.side.RED), GroupsBlue=coalition.getStaticObjects(coalition.side.BLUE), GroupsNeutral=coalition.getStaticObjects(coalition.side.NEUTRAL)}

  for CoalitionId, CoalitionData in pairs( CoalitionsData ) do
    for DCSStaticId, DCSStatic in pairs( CoalitionData ) do

      if DCSStatic:isExist() then
        local DCSStaticName = DCSStatic:getName()

        self:I(string.format("Register Static: %s", tostring(DCSStaticName)))
        self:AddStatic( DCSStaticName )
      else
        self:E( { "Static does not exist: ",  DCSStatic } )
      end
    end
  end

  return self
end

--- Register all world airbases.
-- @param #DATABASE self
-- @return #DATABASE self
function DATABASE:_RegisterAirbases()

 for DCSAirbaseId, DCSAirbase in pairs(world.getAirbases()) do

    self:_RegisterAirbase(DCSAirbase)

  end

  return self
end

--- Register a DCS airbase.
-- @param #DATABASE self
-- @param DCS#Airbase airbase Airbase.
-- @return #DATABASE self
function DATABASE:_RegisterAirbase(airbase)
  
  local IsSyria = UTILS.GetDCSMap() == "Syria" and true or false
  local countHSyria = 0
  
  if airbase then

    -- Get the airbase name.
    local DCSAirbaseName = airbase:getName()
    
    -- DCS 2.9.8.1107 added 143 helipads all named H with the same object ID ..
    if IsSyria and DCSAirbaseName == "H" and countHSyria > 0 then
      --[[
      local p = airbase:getPosition().p
      local mgrs = COORDINATE:New(p.x,p.z,p.y):ToStringMGRS()
      self:I("Airbase on Syria map named H @ "..mgrs)
      countHSyria = countHSyria + 1
      if countHSyria > 1 then return self end
      --]]
      return self
    elseif IsSyria and DCSAirbaseName == "H" and countHSyria == 0 then
      countHSyria = countHSyria + 1
    end
    
    -- This gave the incorrect value to be inserted into the airdromeID for DCS 2.5.6. Is fixed now.
    local airbaseID=airbase:getID()

    -- Add and register airbase.
    local airbase=self:AddAirbase( DCSAirbaseName )

    -- Unique ID.
    local airbaseUID=airbase:GetID(true)
    
    local typename = airbase:GetTypeName()
    
    local category = airbase.category
    
    if category == Airbase.Category.SHIP and typename == "FARP_SINGLE_01" then
      category = Airbase.Category.HELIPAD
    end
    
    -- Debug output.
    local text=string.format("Register %s: %s (UID=%d), Runways=%d, Parking=%d [", AIRBASE.CategoryName[category], tostring(DCSAirbaseName), airbaseUID, #airbase.runways, airbase.NparkingTotal)
    for _,terminalType in pairs(AIRBASE.TerminalType) do
      if airbase.NparkingTerminal and airbase.NparkingTerminal[terminalType] then
        text=text..string.format("%d=%d ", terminalType, airbase.NparkingTerminal[terminalType])
      end
    end
    text=text.."]"
    self:I(text)

  end

  return self
end


--- Events

--- Handles the OnBirth event for the alive units set.
-- @param #DATABASE self
-- @param Core.Event#EVENTDATA Event
function DATABASE:_EventOnBirth( Event )
  self:T( { Event } )

  if Event.IniDCSUnit then

    if Event.IniObjectCategory == Object.Category.STATIC then

      -- Add static object to DB.
      self:AddStatic( Event.IniDCSUnitName )
    
    elseif Event.IniObjectCategory == Object.Category.CARGO and string.match(Event.IniUnitName,".+|%d%d:%d%d|PKG%d+") then

      -- Add dynamic cargo object to DB
      
      local cargo = self:AddDynamicCargo(Event.IniDCSUnitName)
      
      self:I(string.format("Adding dynamic cargo %s", tostring(Event.IniDCSUnitName)))
      
      self:CreateEventNewDynamicCargo( cargo )
        
    else

      if Event.IniObjectCategory == Object.Category.UNIT then

        -- Add unit and group to DB.
        self:AddUnit( Event.IniDCSUnitName )
        self:AddGroup( Event.IniDCSGroupName )

        -- A unit can also be an airbase (e.g. ships).
        local DCSAirbase = Airbase.getByName(Event.IniDCSUnitName)
        if DCSAirbase then
          -- Add airbase if it was spawned later in the mission.
          self:I(string.format("Adding airbase %s", tostring(Event.IniDCSUnitName)))
          self:AddAirbase(Event.IniDCSUnitName)
        end

      end
    end

    if Event.IniObjectCategory == Object.Category.UNIT then
      
      Event.IniGroup = self:FindGroup( Event.IniDCSGroupName )
      Event.IniUnit = self:FindUnit( Event.IniDCSUnitName )

      -- Client
      local client=self.CLIENTS[Event.IniDCSUnitName] --Wrapper.Client#CLIENT

      if client then
        -- TODO: create event ClientAlive
      end

      -- Get player name.
      local PlayerName = Event.IniUnit:GetPlayerName()

      if PlayerName then

        -- Debug info.
        self:I(string.format("Player '%s' joined unit '%s' (%s) of group '%s'", tostring(PlayerName), tostring(Event.IniDCSUnitName), tostring(Event.IniTypeName), tostring(Event.IniDCSGroupName)))
              
        -- Add client in case it does not exist already.
        if client == nil or (client and client:CountPlayers() == 0) then
          client=self:AddClient(Event.IniDCSUnitName, true)
        end

        -- Add player.
        client:AddPlayer(PlayerName)

        -- Add player.
        if not self.PLAYERS[PlayerName] then
          self:AddPlayer( Event.IniUnitName, PlayerName )
        end
        
        local function SetPlayerSettings(self,PlayerName,IniUnit)
          -- Player settings.
          local Settings = SETTINGS:Set( PlayerName )
          --Settings:SetPlayerMenu(Event.IniUnit)
          Settings:SetPlayerMenu(IniUnit)
          -- Create an event.
          self:CreateEventPlayerEnterAircraft(IniUnit)
          --self:CreateEventPlayerEnterAircraft(Event.IniUnit)
        end
        
        self:ScheduleOnce(1,SetPlayerSettings,self,PlayerName,Event.IniUnit)
        
      end

    end

  end

end


--- Handles the OnDead or OnCrash event for alive units set.
-- @param #DATABASE self
-- @param Core.Event#EVENTDATA Event
function DATABASE:_EventOnDeadOrCrash( Event )
  if Event.IniDCSUnit then

    local name=Event.IniDCSUnitName

    if Event.IniObjectCategory == 3 then

      ---
      -- STATICS 
      ---

      if self.STATICS[Event.IniDCSUnitName] then
        self:DeleteStatic( Event.IniDCSUnitName )
      end

      ---
      -- Maybe a UNIT?
      ---
 
      -- Delete unit.
      if self.UNITS[Event.IniDCSUnitName] then
        self:T("STATIC Event for UNIT "..tostring(Event.IniDCSUnitName))
        local DCSUnit = _DATABASE:FindUnit( Event.IniDCSUnitName )
        self:T({DCSUnit})
        if DCSUnit then
          --self:I("Creating DEAD Event for UNIT "..tostring(Event.IniDCSUnitName))
          --DCSUnit:Destroy(true)
          return
        end
      end

    else

      if Event.IniObjectCategory == 1 then

        ---
        -- UNITS
        ---

        -- Delete unit.
        if self.UNITS[Event.IniDCSUnitName] then
          self:ScheduleOnce(1,self.DeleteUnit,self,Event.IniDCSUnitName)
          --self:DeleteUnit(Event.IniDCSUnitName)
        end

        -- Remove client players.
        local client=self.CLIENTS[name] --Wrapper.Client#CLIENT

        if client then
          client:RemovePlayers()
        end

      end
    end

    -- Add airbase if it was spawned later in the mission.
    local airbase=self.AIRBASES[Event.IniDCSUnitName] --Wrapper.Airbase#AIRBASE
    if airbase and (airbase:IsHelipad() or airbase:IsShip()) then
      self:DeleteAirbase(Event.IniDCSUnitName)
    end

  end

  -- Account destroys.
  self:AccountDestroys( Event )
end


--- Handles the OnPlayerEnterUnit event to fill the active players table for CA units (with the unit filter applied).
-- @param #DATABASE self
-- @param Core.Event#EVENTDATA Event
function DATABASE:_EventOnPlayerEnterUnit( Event )
  self:F2( { Event } )

  if Event.IniDCSUnit then
    -- Player entering a CA slot
    if Event.IniObjectCategory == 1 and Event.IniGroup and Event.IniGroup:IsGround() then
        
      local IsPlayer = Event.IniDCSUnit:getPlayerName()
      if IsPlayer then

        -- Debug info.
        self:I(string.format("Player '%s' joined GROUND unit '%s' of group '%s'", tostring(Event.IniPlayerName), tostring(Event.IniDCSUnitName), tostring(Event.IniDCSGroupName)))
        
        local client= self.CLIENTS[Event.IniDCSUnitName] --Wrapper.Client#CLIENT
        
        -- Add client in case it does not exist already.
        if not client then
          client=self:AddClient(Event.IniDCSUnitName)
        end
              
        -- Add player.
        client:AddPlayer(Event.IniPlayerName)

        -- Add player.
        if not self.PLAYERS[Event.IniPlayerName] then
          self:AddPlayer( Event.IniUnitName, Event.IniPlayerName )
        end

        -- Player settings.
        local Settings = SETTINGS:Set( Event.IniPlayerName )
        Settings:SetPlayerMenu(Event.IniUnit)

      end

    end
  end
end

--- Handles the OnDynamicCargoRemoved event to clean the active dynamic cargo table.
-- @param #DATABASE self
-- @param Core.Event#EVENTDATA Event
function DATABASE:_EventOnDynamicCargoRemoved( Event )
  self:T( { Event } )
  if Event.IniDynamicCargoName then
    self:DeleteDynamicCargo(Event.IniDynamicCargoName)
  end
end

--- Handles the OnPlayerLeaveUnit event to clean the active players table.
-- @param #DATABASE self
-- @param Core.Event#EVENTDATA Event
function DATABASE:_EventOnPlayerLeaveUnit( Event )
  self:F2( { Event } )
  
  local function FindPlayerName(UnitName)
    local playername = nil
    for _name,_unitname in pairs(self.PLAYERS) do
      if _unitname == UnitName then
        playername = _name
        break
      end
    end
    return playername
  end
  
  if Event.IniUnit then

    if Event.IniObjectCategory == 1 then

      -- Try to get the player name. This can be buggy for multicrew aircraft!
      local PlayerName = Event.IniPlayerName or Event.IniUnit:GetPlayerName() or FindPlayerName(Event.IniUnitName)
          
      if PlayerName then

        -- Debug info.
        self:I(string.format("Player '%s' left unit %s", tostring(PlayerName), tostring(Event.IniUnitName)))

        -- Remove player menu.
        local Settings = SETTINGS:Set( PlayerName )
        Settings:RemovePlayerMenu(Event.IniUnit)

        -- Delete player.
        self:DeletePlayer(Event.IniUnit, PlayerName)

        -- Client stuff.
        local client=self.CLIENTS[Event.IniDCSUnitName] --Wrapper.Client#CLIENT
        if client then
          client:RemovePlayer(PlayerName)
          --self.PLAYERSETTINGS[PlayerName] = nil
        end

      end
    end
  end
end

--- Iterators

--- Iterate the DATABASE and call an iterator function for the given set, providing the Object for each element within the set and optional parameters.
-- @param #DATABASE self
-- @param #function IteratorFunction The function that will be called when there is an alive player in the database.
-- @return #DATABASE self
function DATABASE:ForEach( IteratorFunction, FinalizeFunction, arg, Set )
  self:F2( arg )

  local function CoRoutine()
    local Count = 0
    for ObjectID, Object in pairs( Set ) do
        self:T2( Object )
        IteratorFunction( Object, unpack( arg ) )
        Count = Count + 1
--        if Count % 100 == 0 then
--          coroutine.yield( false )
--        end
    end
    return true
  end

--  local co = coroutine.create( CoRoutine )
  local co = CoRoutine

  local function Schedule()

--    local status, res = coroutine.resume( co )
    local status, res = co()
    self:T3( { status, res } )

    if status == false then
      error( res )
    end
    if res == false then
      return true -- resume next time the loop
    end
    if FinalizeFunction then
      FinalizeFunction( unpack( arg ) )
    end
    return false
  end

  --local Scheduler = SCHEDULER:New( self, Schedule, {}, 0.001, 0.001, 0 )
  Schedule()

  return self
end


--- Iterate the DATABASE and call an iterator function for each **alive** STATIC, providing the STATIC and optional parameters.
-- @param #DATABASE self
-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a STATIC parameter.
-- @return #DATABASE self
function DATABASE:ForEachStatic( IteratorFunction, FinalizeFunction, ... )  --R2.1
  self:F2( arg )

  self:ForEach( IteratorFunction, FinalizeFunction, arg, self.STATICS )

  return self
end


--- Iterate the DATABASE and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters.
-- @param #DATABASE self
-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a UNIT parameter.
-- @return #DATABASE self
function DATABASE:ForEachUnit( IteratorFunction, FinalizeFunction, ... )
  self:F2( arg )

  self:ForEach( IteratorFunction, FinalizeFunction, arg, self.UNITS )

  return self
end


--- Iterate the DATABASE and call an iterator function for each **alive** GROUP, providing the GROUP and optional parameters.
-- @param #DATABASE self
-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a GROUP parameter.
-- @return #DATABASE self
function DATABASE:ForEachGroup( IteratorFunction, FinalizeFunction, ... )
  self:F2( arg )

  self:ForEach( IteratorFunction, FinalizeFunction, arg, self.GROUPS )

  return self
end


--- Iterate the DATABASE and call an iterator function for each **ALIVE** player, providing the player name and optional parameters.
-- @param #DATABASE self
-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept the player name.
-- @return #DATABASE self
function DATABASE:ForEachPlayer( IteratorFunction, FinalizeFunction, ... )
  self:F2( arg )

  self:ForEach( IteratorFunction, FinalizeFunction, arg, self.PLAYERS )

  return self
end


--- Iterate the DATABASE and call an iterator function for each player who has joined the mission, providing the Unit of the player and optional parameters.
-- @param #DATABASE self
-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a UNIT parameter.
-- @return #DATABASE self
function DATABASE:ForEachPlayerJoined( IteratorFunction, FinalizeFunction, ... )
  self:F2( arg )

  self:ForEach( IteratorFunction, FinalizeFunction, arg, self.PLAYERSJOINED )

  return self
end

--- Iterate the DATABASE and call an iterator function for each **ALIVE** player UNIT, providing the player UNIT and optional parameters.
-- @param #DATABASE self
-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept the player name.
-- @return #DATABASE self
function DATABASE:ForEachPlayerUnit( IteratorFunction, FinalizeFunction, ... )
  self:F2( arg )

  self:ForEach( IteratorFunction, FinalizeFunction, arg, self.PLAYERUNITS )

  return self
end


--- Iterate the DATABASE and call an iterator function for each CLIENT, providing the CLIENT to the function and optional parameters.
-- @param #DATABASE self
-- @param #function IteratorFunction The function that will be called object in the database. The function needs to accept a CLIENT parameter.
-- @return #DATABASE self
function DATABASE:ForEachClient( IteratorFunction, FinalizeFunction, ... )
  self:F2( arg )

  self:ForEach( IteratorFunction, FinalizeFunction, arg, self.CLIENTS )

  return self
end

--- Iterate the DATABASE and call an iterator function for each CARGO, providing the CARGO object to the function and optional parameters.
-- @param #DATABASE self
-- @param #function IteratorFunction The function that will be called for each object in the database. The function needs to accept a CLIENT parameter.
-- @return #DATABASE self
function DATABASE:ForEachCargo( IteratorFunction, FinalizeFunction, ... )
  self:F2( arg )

  self:ForEach( IteratorFunction, FinalizeFunction, arg, self.CARGOS )

  return self
end


--- Handles the OnEventNewCargo event.
-- @param #DATABASE self
-- @param Core.Event#EVENTDATA EventData
function DATABASE:OnEventNewCargo( EventData )
  self:F2( { EventData } )

  if EventData.Cargo then
    self:AddCargo( EventData.Cargo )
  end
end


--- Handles the OnEventDeleteCargo.
-- @param #DATABASE self
-- @param Core.Event#EVENTDATA EventData
function DATABASE:OnEventDeleteCargo( EventData )
  self:F2( { EventData } )

  if EventData.Cargo then
    self:DeleteCargo( EventData.Cargo.Name )
  end
end


--- Handles the OnEventNewZone event.
-- @param #DATABASE self
-- @param Core.Event#EVENTDATA EventData
function DATABASE:OnEventNewZone( EventData )
  self:F2( { EventData } )

  if EventData.Zone then
    self:AddZone( EventData.Zone.ZoneName, EventData.Zone )
  end
end


--- Handles the OnEventDeleteZone.
-- @param #DATABASE self
-- @param Core.Event#EVENTDATA EventData
function DATABASE:OnEventDeleteZone( EventData )
  self:F2( { EventData } )

  if EventData.Zone then
    self:DeleteZone( EventData.Zone.ZoneName )
  end
end



--- Gets the player settings
-- @param #DATABASE self
-- @param #string PlayerName
-- @return Core.Settings#SETTINGS
function DATABASE:GetPlayerSettings( PlayerName )
  self:F2( { PlayerName } )
  return self.PLAYERSETTINGS[PlayerName]
end


--- Sets the player settings
-- @param #DATABASE self
-- @param #string PlayerName
-- @param Core.Settings#SETTINGS Settings
-- @return Core.Settings#SETTINGS
function DATABASE:SetPlayerSettings( PlayerName, Settings )
  self:F2( { PlayerName, Settings } )
  self.PLAYERSETTINGS[PlayerName] = Settings
end

--- Add an OPS group (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) to the data base.
-- @param #DATABASE self
-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group added to the DB.
function DATABASE:AddOpsGroup(opsgroup)
  --env.info("Adding OPSGROUP "..tostring(opsgroup.groupname))
  self.FLIGHTGROUPS[opsgroup.groupname]=opsgroup
end

--- Get an OPS group (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) from the data base.
-- @param #DATABASE self
-- @param #string groupname Group name of the group. Can also be passed as GROUP object.
-- @return Ops.OpsGroup#OPSGROUP OPS group object.
function DATABASE:GetOpsGroup(groupname)

  -- Get group and group name.
  if type(groupname)=="string" then
  else
    groupname=groupname:GetName()
  end

  --env.info("Getting OPSGROUP "..tostring(groupname))
  return self.FLIGHTGROUPS[groupname]
end

--- Find an OPSGROUP (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) in the data base.
-- @param #DATABASE self
-- @param #string groupname Group name of the group. Can also be passed as GROUP object.
-- @return Ops.OpsGroup#OPSGROUP OPS group object.
function DATABASE:FindOpsGroup(groupname)

  -- Get group and group name.
  if type(groupname)=="string" then
  else
    groupname=groupname:GetName()
  end

  --env.info("Getting OPSGROUP "..tostring(groupname))
  return self.FLIGHTGROUPS[groupname]
end

--- Find an OPSGROUP (FLIGHTGROUP, ARMYGROUP, NAVYGROUP) in the data base for a given unit.
-- @param #DATABASE self
-- @param #string unitname Unit name. Can also be passed as UNIT object.
-- @return Ops.OpsGroup#OPSGROUP OPS group object.
function DATABASE:FindOpsGroupFromUnit(unitname)

  local unit=nil --Wrapper.Unit#UNIT
  local groupname

  -- Get group and group name.
  if type(unitname)=="string" then
    unit=UNIT:FindByName(unitname)
  else
    unit=unitname
  end

  if unit then
    groupname=unit:GetGroup():GetName()
  end

  if groupname then
    return self.FLIGHTGROUPS[groupname]
  else
    return nil
  end
end

--- Add a flight control to the data base.
-- @param #DATABASE self
-- @param OPS.FlightControl#FLIGHTCONTROL flightcontrol
function DATABASE:AddFlightControl(flightcontrol)
  self:F2( { flightcontrol } )
  self.FLIGHTCONTROLS[flightcontrol.airbasename]=flightcontrol
end

--- Get a flight control object from the data base.
-- @param #DATABASE self
-- @param #string airbasename Name of the associated airbase.
-- @return OPS.FlightControl#FLIGHTCONTROL The FLIGHTCONTROL object.s
function DATABASE:GetFlightControl(airbasename)
  return self.FLIGHTCONTROLS[airbasename]
end

-- @param #DATABASE self
function DATABASE:_RegisterTemplates()
  self:F2()

  self.Navpoints = {}
  self.UNITS = {}
  --Build self.Navpoints
  for CoalitionName, coa_data in pairs(env.mission.coalition) do
    self:T({CoalitionName=CoalitionName})

    if (CoalitionName == 'red' or CoalitionName == 'blue' or CoalitionName == 'neutrals') and type(coa_data) == 'table' then
      --self.Units[coa_name] = {}

      local CoalitionSide = coalition.side[string.upper(CoalitionName)]
      if CoalitionName=="red" then
        CoalitionSide=coalition.side.RED
      elseif CoalitionName=="blue" then
        CoalitionSide=coalition.side.BLUE
      else
        CoalitionSide=coalition.side.NEUTRAL
      end

      -- build nav points DB
      self.Navpoints[CoalitionName] = {}
      if coa_data.nav_points then --navpoints
        for nav_ind, nav_data in pairs(coa_data.nav_points) do

          if type(nav_data) == 'table' then
            self.Navpoints[CoalitionName][nav_ind] = UTILS.DeepCopy(nav_data)

            self.Navpoints[CoalitionName][nav_ind]['name'] = nav_data.callsignStr  -- name is a little bit more self-explanatory.
            self.Navpoints[CoalitionName][nav_ind]['point'] = {}  -- point is used by SSE, support it.
            self.Navpoints[CoalitionName][nav_ind]['point']['x'] = nav_data.x
            self.Navpoints[CoalitionName][nav_ind]['point']['y'] = 0
            self.Navpoints[CoalitionName][nav_ind]['point']['z'] = nav_data.y
          end
        end
      end

      -------------------------------------------------
      if coa_data.country then --there is a country table
        for cntry_id, cntry_data in pairs(coa_data.country) do

          local CountryName = string.upper(cntry_data.name)
          local CountryID = cntry_data.id

          self.COUNTRY_ID[CountryName] = CountryID
          self.COUNTRY_NAME[CountryID] = CountryName

          --self.Units[coa_name][countryName] = {}
          --self.Units[coa_name][countryName]["countryId"] = cntry_data.id

          if type(cntry_data) == 'table' then  --just making sure

            for obj_type_name, obj_type_data in pairs(cntry_data) do

              if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then --should be an unncessary check

                local CategoryName = obj_type_name

                if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then  --there's a group!

                  --self.Units[coa_name][countryName][category] = {}

                  for group_num, Template in pairs(obj_type_data.group) do

                    if obj_type_name ~= "static" and Template and Template.units and type(Template.units) == 'table' then  --making sure again- this is a valid group
                      
                      self:_RegisterGroupTemplate(Template, CoalitionSide, _DATABASECategory[string.lower(CategoryName)], CountryID)

                    else

                      self:_RegisterStaticTemplate(Template, CoalitionSide, _DATABASECategory[string.lower(CategoryName)], CountryID)

                    end --if GroupTemplate and GroupTemplate.units then
                  end --for group_num, GroupTemplate in pairs(obj_type_data.group) do
                end --if ((type(obj_type_data) == 'table') and obj_type_data.group and (type(obj_type_data.group) == 'table') and (#obj_type_data.group > 0)) then
              end --if obj_type_name == "helicopter" or obj_type_name == "ship" or obj_type_name == "plane" or obj_type_name == "vehicle" or obj_type_name == "static" then
          end --for obj_type_name, obj_type_data in pairs(cntry_data) do
          end --if type(cntry_data) == 'table' then
      end --for cntry_id, cntry_data in pairs(coa_data.country) do
      end --if coa_data.country then --there is a country table
    end --if coa_name == 'red' or coa_name == 'blue' and type(coa_data) == 'table' then
  end --for coa_name, coa_data in pairs(mission.coalition) do

  return self
end

  --- Account the Hits of the Players.
  -- @param #DATABASE self
  -- @param Core.Event#EVENTDATA Event
  function DATABASE:AccountHits( Event )
    self:F( { Event } )

    if Event.IniPlayerName ~= nil then -- It is a player that is hitting something
      self:T( "Hitting Something" )

      -- What is he hitting?
      if Event.TgtCategory then

        -- A target got hit
        self.HITS[Event.TgtUnitName] = self.HITS[Event.TgtUnitName] or {}
        local Hit = self.HITS[Event.TgtUnitName]

        Hit.Players = Hit.Players or {}
        Hit.Players[Event.IniPlayerName] = true
      end
    end

    -- It is a weapon initiated by a player, that is hitting something
    -- This seems to occur only with scenery and static objects.
    if Event.WeaponPlayerName ~= nil then
        self:T( "Hitting Scenery" )

      -- What is he hitting?
      if Event.TgtCategory then

        if Event.WeaponCoalition then -- A coalition object was hit, probably a static.
          -- A target got hit
          self.HITS[Event.TgtUnitName] = self.HITS[Event.TgtUnitName] or {}
          local Hit = self.HITS[Event.TgtUnitName]

          Hit.Players = Hit.Players or {}
          Hit.Players[Event.WeaponPlayerName] = true
        else -- A scenery object was hit.
        end
      end
    end
  end

  --- Account the destroys.
  -- @param #DATABASE self
  -- @param Core.Event#EVENTDATA Event
  function DATABASE:AccountDestroys( Event )
    self:F( { Event } )

    local TargetUnit = nil
    local TargetGroup = nil
    local TargetUnitName = ""
    local TargetGroupName = ""
    local TargetPlayerName = ""
    local TargetCoalition = nil
    local TargetCategory = nil
    local TargetType = nil
    local TargetUnitCoalition = nil
    local TargetUnitCategory = nil
    local TargetUnitType = nil

    if Event.IniDCSUnit then

      TargetUnit = Event.IniUnit
      TargetUnitName = Event.IniDCSUnitName
      TargetGroup = Event.IniDCSGroup
      TargetGroupName = Event.IniDCSGroupName
      TargetPlayerName = Event.IniPlayerName

      TargetCoalition = Event.IniCoalition
      TargetCategory = Event.IniCategory
      TargetType = Event.IniTypeName

      TargetUnitType = TargetType

      self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } )
    end

    local Destroyed = false

    -- What is the player destroying?
    if self.HITS[Event.IniUnitName] then -- Was there a hit for this unit for this player before registered???
      self.DESTROYS[Event.IniUnitName] = self.DESTROYS[Event.IniUnitName] or {}
      self.DESTROYS[Event.IniUnitName] = true
    end
  end
--- **Core** - Define collections of objects to perform bulk actions and logically group objects.
--
-- ===
--
-- ## Features:
--
--   * Dynamically maintain collections of objects.
--   * Manually modify the collection, by adding or removing objects.
--   * Collections of different types.
--   * Validate the presence of objects in the collection.
--   * Perform bulk actions on collection.
--
-- ===
--
-- Group objects or data of the same type into a collection, which is either:
--
--   * Manually managed using the **:Add...()** or **:Remove...()** methods. The initial SET can be filtered with the **@{#SET_BASE.FilterOnce}()** method.
--   * Dynamically updated when new objects are created or objects are destroyed using the **@{#SET_BASE.FilterStart}()** method.
--
-- Various types of SET_ classes are available:
--
--   * @{#SET_GROUP}: Defines a collection of @{Wrapper.Group}s filtered by filter criteria.
--   * @{#SET_UNIT}: Defines a collection of @{Wrapper.Unit}s filtered by filter criteria.
--   * @{#SET_STATIC}: Defines a collection of @{Wrapper.Static}s filtered by filter criteria.
--   * @{#SET_CLIENT}: Defines a collection of @{Wrapper.Client}s filtered by filter criteria.
--   * @{#SET_AIRBASE}: Defines a collection of @{Wrapper.Airbase}s filtered by filter criteria.
--   * @{#SET_CARGO}: Defines a collection of @{Cargo.Cargo}s filtered by filter criteria.
--   * @{#SET_ZONE}: Defines a collection of @{Core.Zone}s filtered by filter criteria.
--   * @{#SET_SCENERY}: Defines a collection of @{Wrapper.Scenery}s added via a filtered @{#SET_ZONE}.
--   * @{#SET_DYNAMICCARGO}: Defines a collection of @{Wrapper.DynamicCargo}s filtered by filter criteria.
--
-- These classes are derived from @{#SET_BASE}, which contains the main methods to manage the collections.
--
-- A multitude of other methods are available in the individual set classes that allow to:
--
--   * Validate the presence of objects in the SET.
--   * Trigger events when objects in the SET change a zone presence.
--
-- ## Notes on `FilterPrefixes()`:  
-- 
-- This filter always looks for a **partial match** somewhere in the given field. LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. 
-- Have a read through the following to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching).  
-- For example, setting a filter like so `FilterPrefixes("Huey")` is perfectly all right, whilst `FilterPrefixes("UH-1H Al-Assad")` might not be due to the minus signs. A quick fix here is to use a dot (.) 
-- in place of the special character, or escape it with a percentage sign (%), i.e. either `FilterPrefixes("UH.1H Al.Assad")` or `FilterPrefixes("UH%-1H Al%-Assad")` will give you the expected results.
--
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions: **funkyfranky**, **applevangelist**
--
-- ===
--
-- @module Core.Set
-- @image Core_Sets.JPG

do -- SET_BASE
  
  ---
  -- @type SET_BASE
  -- @field #table Filter Table of filters.
  -- @field #table Set Table of objects.
  -- @field #table Index Table of indices.
  -- @field #table List Unused table.
  -- @field Core.Scheduler#SCHEDULER CallScheduler
  -- @field #SET_BASE.Filters Filter Filters
  -- @extends Core.Base#BASE

  --- The @{Core.Set#SET_BASE} class defines the core functions that define a collection of objects.
  -- A SET provides iterators to iterate the SET, but will **temporarily** yield the ForEach iterator loop at defined **"intervals"** to the mail simulator loop.
  -- In this way, large loops can be done while not blocking the simulator main processing loop.
  -- The default **"yield interval"** is after 10 objects processed.
  -- The default **"time interval"** is after 0.001 seconds.
  --
  -- ## Add or remove objects from the SET
  --
  -- Some key core functions are @{Core.Set#SET_BASE.Add} and @{Core.Set#SET_BASE.Remove} to add or remove objects from the SET in your logic.
  --
  -- ## Define the SET iterator **"yield interval"** and the **"time interval"**
  --
  -- Modify the iterator intervals with the @{Core.Set#SET_BASE.SetIteratorIntervals} method.
  -- You can set the **"yield interval"**, and the **"time interval"**. (See above).
  --
  -- @field #SET_BASE SET_BASE
  SET_BASE = {
    ClassName = "SET_BASE",
    Filter = {},
    Set = {},
    List = {},
    Index = {},
    Database = nil,
    CallScheduler = nil,
    
  }

  --- Filters
  -- @type SET_BASE.Filters
  -- @field #table Coalition Coalitions
  -- @field #table Prefix Prefixes.

  --- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names.
  -- @param #SET_BASE self
  -- @return #SET_BASE
  -- @usage
  -- -- Define a new SET_BASE Object. This DBObject will contain a reference to all Group and Unit Templates defined within the ME and the DCSRTE.
  -- DBObject = SET_BASE:New()
  function SET_BASE:New( Database )

    -- Inherits from BASE
    local self = BASE:Inherit( self, FSM:New() ) -- Core.Set#SET_BASE

    self.Database = Database

    self:SetStartState( "Started" )

    --- Added Handler OnAfter for SET_BASE
    -- @function [parent=#SET_BASE] OnAfterAdded
    -- @param #SET_BASE self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param #string ObjectName The name of the object.
    -- @param Object The object.

    self:AddTransition( "*", "Added", "*" )

    --- Removed Handler OnAfter for SET_BASE
    -- @function [parent=#SET_BASE] OnAfterRemoved
    -- @param #SET_BASE self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param #string ObjectName The name of the object.
    -- @param Object The object.

    self:AddTransition( "*", "Removed", "*" )

    self.YieldInterval = 10
    self.TimeInterval = 0.001

    self.Set = {}
    self.Index = {}

    self.CallScheduler = SCHEDULER:New( self )

    self:SetEventPriority( 2 )

    return self
  end
  
  --- [Internal] Add a functional filter
  -- @param #SET_BASE self
  -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a CONTROLLABLE object as first argument.
  -- @param ... Condition function arguments, if any.
  -- @return #boolean If true, at least one condition is true
  function SET_BASE:FilterFunction(ConditionFunction, ...)
  
    local condition={}
    condition.func=ConditionFunction
    condition.arg={}
    
    if arg then
      condition.arg=arg
    end
    
    if not self.Filter.Functions then self.Filter.Functions = {} end
    table.insert(self.Filter.Functions, condition)
    
    return self
  end
    
  --- [Internal] Check if the condition functions returns true.
  -- @param #SET_BASE self
  -- @param Wrapper.Controllable#CONTROLLABLE Object The object to filter for
  -- @return #boolean If true, if **all** conditions are true
  function SET_BASE:_EvalFilterFunctions(Object) 
    -- All conditions must be true.
    for _,_condition in pairs(self.Filter.Functions or {}) do
      local condition=_condition
      -- Call function.
      if condition.func(Object,unpack(condition.arg)) == false then
        return false
      end
    end  
    -- No condition was false.
    return true
  end
  
  --- Clear the Objects in the Set.
  -- @param #SET_BASE self
  -- @param #boolean TriggerEvent If `true`, an event remove is triggered for each group that is removed from the set.
  -- @return #SET_BASE self
  function SET_BASE:Clear(TriggerEvent)

    for Name, Object in pairs( self.Set ) do
      self:Remove( Name, not TriggerEvent )
    end

    return self
  end

  --- Finds an @{Core.Base#BASE} object based on the object Name.
  -- @param #SET_BASE self
  -- @param #string ObjectName
  -- @return Core.Base#BASE The Object found.
  function SET_BASE:_Find( ObjectName )

    local ObjectFound = self.Set[ObjectName]
    return ObjectFound
  end

  --- Gets the Set.
  -- @param #SET_BASE self
  -- @return #SET_BASE self
  function SET_BASE:GetSet()
    --self:F2()

    return self.Set or {}
  end

  --- Gets a list of the Names of the Objects in the Set.
  -- @param #SET_BASE self
  -- @return #table Table of names.
  function SET_BASE:GetSetNames() -- R2.3
    --self:F2()

    local Names = {}

    for Name, Object in pairs( self.Set ) do
      table.insert( Names, Name )
    end

    return Names
  end

  --- Returns a table of the Objects in the Set.
  -- @param #SET_BASE self
  -- @return #table Table of objects.
  function SET_BASE:GetSetObjects() -- R2.3
    --self:F2()

    local Objects = {}

    for Name, Object in pairs( self.Set ) do
      table.insert( Objects, Object )
    end

    return Objects
  end

  --- Removes a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name.
  -- @param #SET_BASE self
  -- @param #string ObjectName
  -- @param #boolean NoTriggerEvent (Optional) When `true`, the :Remove() method will not trigger a **Removed** event.
  function SET_BASE:Remove( ObjectName, NoTriggerEvent )
    --self:F2( { ObjectName = ObjectName } )
    
    local TriggerEvent = true
    if NoTriggerEvent then 
      TriggerEvent = false
    else
      TriggerEvent = true
    end
    
    local Object = self.Set[ObjectName]

    if Object then
    
      for Index, Key in ipairs( self.Index ) do
        if Key == ObjectName then
          table.remove( self.Index, Index )
          self.Set[ObjectName] = nil
          break
        end
      end
      
      -- When NoTriggerEvent is true, then no Removed event will be triggered.
      if TriggerEvent then
        self:Removed( ObjectName, Object )
      end
    end
  end

  --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using a given ObjectName as the index.
  -- @param #SET_BASE self
  -- @param #string ObjectName The name of the object.
  -- @param Core.Base#BASE Object The object itself.
  -- @return Core.Base#BASE The added BASE Object.
  function SET_BASE:Add( ObjectName, Object )
  
    -- Debug info.
    --self:T2( { ObjectName = ObjectName, Object = Object } )
    
    -- Error ahndling
    if not ObjectName or ObjectName == "" then
      self:E("SET_BASE:Add - Invalid ObjectName handed")
      self:E({ObjectName=ObjectName, Object=Object})
      return self
    end
    
    -- Ensure that the existing element is removed from the Set before a new one is inserted to the Set
    if self.Set[ObjectName] then
      self:Remove( ObjectName, true )
    end

    -- Add object to set.
    self.Set[ObjectName] = Object

    -- Add Object name to Index.
    table.insert( self.Index, ObjectName )

    -- Trigger Added event.
    self:Added( ObjectName, Object )
    
    return self
  end

  --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using the Object Name as the index.
  -- @param #SET_BASE self
  -- @param Wrapper.Object#OBJECT Object
  -- @return Core.Base#BASE The added BASE Object.
  function SET_BASE:AddObject( Object )
    --self:F2( Object.ObjectName )

    --self:T( Object.UnitName )
    --self:T( Object.ObjectName )
    self:Add( Object.ObjectName, Object )

  end

  --- Sort the set by name.
  -- @param #SET_BASE self
  -- @return Core.Base#BASE The added BASE Object.
  function SET_BASE:SortByName()
  
    local function sort(a, b)
      return a<b
    end
  
    table.sort(self.Index)
  
  end

  --- Add a SET to this set.
  -- @param #SET_BASE self
  -- @param Core.Set#SET_BASE SetToAdd Set to add.
  -- @return #SET_BASE self
  function SET_BASE:AddSet(SetToAdd)
    
    if not SetToAdd then return self end
    
    for _,ObjectB in pairs(SetToAdd.Set) do
      self:AddObject(ObjectB)
    end

    return self
  end


  --- Get the *union* of two sets.
  -- @param #SET_BASE self
  -- @param Core.Set#SET_BASE SetB Set *B*.
  -- @return Core.Set#SET_BASE The union set, i.e. contains objects that are in set *A* **or** in set *B*.
  function SET_BASE:GetSetUnion( SetB )

    local union = SET_BASE:New()

    for _, ObjectA in pairs( self.Set ) do
      union:AddObject( ObjectA )
    end

    for _, ObjectB in pairs( SetB.Set ) do
      union:AddObject( ObjectB )
    end

    return union
  end

  --- Get the *intersection* of this set, called *A*, and another set.
  -- @param #SET_BASE self
  -- @param Core.Set#SET_BASE SetB Set other set, called *B*.
  -- @return Core.Set#SET_BASE A set of objects that is included in set *A* **and** in set *B*.
  function SET_BASE:GetSetIntersection(SetB)

    local intersection=SET_BASE:New()

    local union=self:GetSetUnion(SetB)

    for _,Object in pairs(union.Set) do
      if self:IsIncludeObject(Object) and SetB:IsIncludeObject(Object) then
        intersection:AddObject(Object)
      end
    end

    return intersection
  end

  --- Get the *complement* of two sets.
  -- @param #SET_BASE self
  -- @param Core.Set#SET_BASE SetB Set other set, called *B*.
  -- @return Core.Set#SET_BASE The set of objects that are in set *B* but **not** in this set *A*.
  function SET_BASE:GetSetComplement( SetB )

    local complement = self:GetSetUnion(SetB)
    local intersection = self:GetSetIntersection(SetB)

    for _,Object in pairs(intersection.Set) do
        complement:Remove(Object.ObjectName,true)
    end

    return complement
  end

  --- Compare two sets.
  -- @param #SET_BASE self
  -- @param Core.Set#SET_BASE SetA First set.
  -- @param Core.Set#SET_BASE SetB Set to be merged into first set.
  -- @return Core.Set#SET_BASE The set of objects that are included in SetA and SetB.
  function SET_BASE:CompareSets( SetA, SetB )

    for _, ObjectB in pairs( SetB.Set ) do
      if SetA:IsIncludeObject( ObjectB ) then
        SetA:Add( ObjectB )
      end
    end

    return SetA
  end

  --- Gets a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name.
  -- @param #SET_BASE self
  -- @param #string ObjectName
  -- @return Core.Base#BASE
  function SET_BASE:Get( ObjectName )
    --self:F( ObjectName )

    local Object = self.Set[ObjectName]

    --self:T3( { ObjectName, Object } )
    return Object
  end

  --- Gets the first object from the @{Core.Set#SET_BASE} and derived classes.
  -- @param #SET_BASE self
  -- @return Core.Base#BASE
  function SET_BASE:GetFirst()
    local ObjectName = self.Index[1]
    local FirstObject = self.Set[ObjectName]
    --self:T3( { FirstObject } )
    return FirstObject
  end

  --- Gets the last object from the @{Core.Set#SET_BASE} and derived classes.
  -- @param #SET_BASE self
  -- @return Core.Base#BASE
  function SET_BASE:GetLast()
    local tablemax = table.maxn(self.Index)
    local ObjectName = self.Index[tablemax]
    local LastObject = self.Set[ObjectName]
    --self:T3( { LastObject } )
    return LastObject
  end

  --- Gets a random object from the @{Core.Set#SET_BASE} and derived classes.
  -- @param #SET_BASE self
  -- @return Core.Base#BASE or nil if none found or the SET is empty!
  function SET_BASE:GetRandom()
    local tablemax = 0
    for _,_ind in pairs(self.Index) do
      tablemax = tablemax + 1
    end
    --local tablemax = table.maxn(self.Index)
    local RandomItem = self.Set[self.Index[math.random(1,tablemax)]]
    --self:T3( { RandomItem } )
    return RandomItem
  end
  
  --- Gets a random object from the @{Core.Set#SET_BASE} and derived classes. A bit slower than @{#SET_BASE.GetRandom}() but tries to ensure you get an object back if the SET is not empty. 
  -- @param #SET_BASE self
  -- @return Core.Base#BASE or nil if  the SET is empty!
  function SET_BASE:GetRandomSurely()
    local tablemax = 0
    local sorted = {}
    for _,_obj in pairs(self.Set) do
      tablemax = tablemax + 1
      sorted[tablemax] = _obj
    end
    --local tablemax = table.maxn(self.Index)
    --local RandomItem = self.Set[self.Index[math.random(1,tablemax)]]
    local RandomItem = sorted[math.random(1,tablemax)]
    --self:T3( { RandomItem } )
    return RandomItem
  end

  --- Retrieves the amount of objects in the @{Core.Set#SET_BASE} and derived classes.
  -- @param #SET_BASE self
  -- @return #number Count
  function SET_BASE:Count()
    return self.Index and table.maxn(self.Index) or 0
  end

  --- Copies the Filter criteria from a given Set (for rebuilding a new Set based on an existing Set).
  -- @param #SET_BASE self
  -- @param #SET_BASE BaseSet
  -- @return #SET_BASE
  function SET_BASE:SetDatabase( BaseSet )

    -- Copy the filter criteria of the BaseSet
    local OtherFilter = UTILS.DeepCopy( BaseSet.Filter )
    self.Filter = OtherFilter

    -- Now base the new Set on the BaseSet
    self.Database = BaseSet:GetSet()
    return self
  end

  --- Define the SET iterator **"limit"**.
  -- @param #SET_BASE self
  -- @param #number Limit Defines how many objects are evaluated of the set as part of the Some iterators. The default is 1.
  -- @return #SET_BASE self
  function SET_BASE:SetSomeIteratorLimit( Limit )

    self.SomeIteratorLimit = Limit or 1

    return self
  end

  --- Get the SET iterator **"limit"**.
  -- @param #SET_BASE self
  -- @return #number Defines how many objects are evaluated of the set as part of the Some iterators.
  function SET_BASE:GetSomeIteratorLimit()

    return self.SomeIteratorLimit or self:Count()
  end
  
  --- Get max threat level of all objects in the SET.
  -- @param #SET_BASE self
  -- @return #number Max threat level found.
  function SET_BASE:GetThreatLevelMax()
    local ThreatMax = 0
    for _,_unit in pairs(self.Set or {}) do
      local unit = _unit -- Wrapper.Unit#UNIT
      local threat = unit.GetThreatLevel and unit:GetThreatLevel() or 0
      if threat > ThreatMax then
        ThreatMax = threat
      end
    end
    return ThreatMax
  end

  --- Filters for the defined collection.
  -- @param #SET_BASE self
  -- @return #SET_BASE self
  function SET_BASE:FilterOnce()
  
    --self:Clear()

    for ObjectName, Object in pairs( self.Database ) do

      if self:IsIncludeObject( Object ) then
        self:Add( ObjectName, Object )
      else
        self:Remove(ObjectName, true)
      end
    end

    return self
  end
  
  --- Clear all filters. You still need to apply :FilterOnce()
  -- @param #SET_BASE self
  -- @return #SET_BASE self
  function SET_BASE:FilterClear()
  
    for key,value in pairs(self.Filter) do
      self.Filter[key]={}
    end

    return self
  end  

  --- Starts the filtering for the defined collection.
  -- @param #SET_BASE self
  -- @return #SET_BASE self
  function SET_BASE:_FilterStart()

    for ObjectName, Object in pairs( self.Database ) do

      if self:IsIncludeObject( Object ) then
        self:Add( ObjectName, Object )
      end
    end

    -- Follow alive players and clients
    -- self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit )
    -- self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit )

    return self
  end

  --- Starts the filtering of the Dead events for the collection.
  -- @param #SET_BASE self
  -- @return #SET_BASE self
  function SET_BASE:FilterDeads() -- R2.1 allow deads to be filtered to automatically handle deads in the collection.

    self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )

    return self
  end

  --- Starts the filtering of the Crash events for the collection.
  -- @param #SET_BASE self
  -- @return #SET_BASE self
  function SET_BASE:FilterCrashes() -- R2.1 allow crashes to be filtered to automatically handle crashes in the collection.

    self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash )

    return self
  end

  --- Stops the filtering for the defined collection.
  -- @param #SET_BASE self
  -- @return #SET_BASE self
  function SET_BASE:FilterStop()

    self:UnHandleEvent( EVENTS.Birth )
    self:UnHandleEvent( EVENTS.Dead )
    self:UnHandleEvent( EVENTS.Crash )

    return self
  end

  --- Iterate the SET_BASE while identifying the nearest object in the set from a @{Core.Point#COORDINATE}.
  -- @param #SET_BASE self
  -- @param Core.Point#COORDINATE Coordinate A @{Core.Point#COORDINATE} object (but **not** a simple DCS#Vec2!) from where to evaluate the closest object in the set.
  -- @return Core.Base#BASE The closest object.
  -- @usage
  --          myset:FindNearestObjectFromPointVec2( ZONE:New("Test Zone"):GetCoordinate() )
  function SET_BASE:FindNearestObjectFromPointVec2( Coordinate )
    --self:F2( Coordinate )

    local NearestObject = nil
    local ClosestDistance = nil

    for ObjectID, ObjectData in pairs( self.Set ) do
      if NearestObject == nil then
        NearestObject = ObjectData
        ClosestDistance = Coordinate:DistanceFromPointVec2( ObjectData:GetCoordinate() )
      else
        local Distance = Coordinate:DistanceFromPointVec2( ObjectData:GetCoordinate() )
        if Distance < ClosestDistance then
          NearestObject = ObjectData
          ClosestDistance = Distance
        end
      end
    end

    return NearestObject
  end

  --- Events

  --- Handles the OnBirth event for the Set.
  -- @param #SET_BASE self
  -- @param Core.Event#EVENTDATA Event
  function SET_BASE:_EventOnBirth( Event )
    --self:F3( { Event } )

    if Event.IniDCSUnit then
      local ObjectName, Object = self:AddInDatabase( Event )
      --self:T3( ObjectName, Object )
      if Object and self:IsIncludeObject( Object ) then
        self:Add( ObjectName, Object )
        -- self:_EventOnPlayerEnterUnit( Event )
      end
    end
  end

  --- Handles the OnDead or OnCrash event for alive units set.
  -- @param #SET_BASE self
  -- @param Core.Event#EVENTDATA Event
  function SET_BASE:_EventOnDeadOrCrash( Event )
    --self:F( { Event } )

    if Event.IniDCSUnit then
      local ObjectName, Object = self:FindInDatabase( Event )
      if ObjectName then
        self:Remove( ObjectName )
      end
    end
  end

  --- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied).
  -- @param #SET_BASE self
  -- @param Core.Event#EVENTDATA Event
  -- function SET_BASE:_EventOnPlayerEnterUnit( Event )
  --  --self:F3( { Event } )
  --
  --  if Event.IniDCSUnit then
  --    local ObjectName, Object = self:AddInDatabase( Event )
  --    self:T3( ObjectName, Object )
  --    if self:IsIncludeObject( Object ) then
  --      self:Add( ObjectName, Object )
  --      --self:_EventOnPlayerEnterUnit( Event )
  --    end
  --  end
  -- end

  --- Handles the OnPlayerLeaveUnit event to clean the active players table.
  -- @param #SET_BASE self
  -- @param Core.Event#EVENTDATA Event
  -- function SET_BASE:_EventOnPlayerLeaveUnit( Event )
  --  --self:F3( { Event } )
  --
  --  local ObjectName = Event.IniDCSUnit
  --  if Event.IniDCSUnit then
  --    if Event.IniDCSGroup then
  --      local GroupUnits = Event.IniDCSGroup:getUnits()
  --      local PlayerCount = 0
  --      for _, DCSUnit in pairs( GroupUnits ) do
  --        if DCSUnit ~= Event.IniDCSUnit then
  --          if DCSUnit:getPlayerName() ~= nil then
  --            PlayerCount = PlayerCount + 1
  --          end
  --        end
  --      end
  --      self:E(PlayerCount)
  --      if PlayerCount == 0 then
  --        self:Remove( Event.IniDCSGroupName )
  --      end
  --    end
  --  end
  -- end

  -- Iterators

  --- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters.
  -- @param #SET_BASE self
  -- @param #function IteratorFunction The function that will be called.
  -- @param #table arg Arguments of the IteratorFunction.
  -- @param #SET_BASE Set (Optional) The set to use. Default self:GetSet().
  -- @param #function Function (Optional) A function returning a #boolean true/false. Only if true, the IteratorFunction is called.
  -- @param #table FunctionArguments (Optional) Function arguments.
  -- @return #SET_BASE self
  function SET_BASE:ForEach( IteratorFunction, arg, Set, Function, FunctionArguments )
    --self:F3( arg )

    Set = Set or self:GetSet()
    arg = arg or {}

    local function CoRoutine()
      local Count = 0
      for ObjectID, ObjectData in pairs( Set ) do
        local Object = ObjectData
        --self:T3( Object )
        if Function then
          if Function( unpack( FunctionArguments or {} ), Object ) == true then
            IteratorFunction( Object, unpack( arg ) )
          end
        else
          IteratorFunction( Object, unpack( arg ) )
        end
        Count = Count + 1
        --        if Count % self.YieldInterval == 0 then
        --          coroutine.yield( false )
        --        end
      end
      return true
    end

    --  local co = coroutine.create( CoRoutine )
    local co = CoRoutine

    local function Schedule()

      --    local status, res = coroutine.resume( co )
      local status, res = co()
      --self:T3( { status, res } )

      if status == false then
        error( res )
      end
      if res == false then
        return true -- resume next time the loop
      end

      return false
    end

    -- self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 )
    Schedule()

    return self
  end

  --- Iterate the SET_BASE and derived classes and call an iterator function for the given SET_BASE, providing the Object for each element within the set and optional parameters.
  -- @param #SET_BASE self
  -- @param #function IteratorFunction The function that will be called.
  -- @return #SET_BASE self
  function SET_BASE:ForSome( IteratorFunction, arg, Set, Function, FunctionArguments )
    --self:F3( arg )

    Set = Set or self:GetSet()
    arg = arg or {}

    local Limit = self:GetSomeIteratorLimit()

    local function CoRoutine()
      local Count = 0
      for ObjectID, ObjectData in pairs( Set ) do
        local Object = ObjectData
        --self:T3( Object )
        if Function then
          if Function( unpack( FunctionArguments ), Object ) == true then
            IteratorFunction( Object, unpack( arg ) )
          end
        else
          IteratorFunction( Object, unpack( arg ) )
        end
        Count = Count + 1
        if Count >= Limit then
          break
        end
        --        if Count % self.YieldInterval == 0 then
        --          coroutine.yield( false )
        --        end
      end
      return true
    end

    --  local co = coroutine.create( CoRoutine )
    local co = CoRoutine

    local function Schedule()

      --    local status, res = coroutine.resume( co )
      local status, res = co()
      --self:T3( { status, res } )

      if status == false then
        error( res )
      end
      if res == false then
        return true -- resume next time the loop
      end

      return false
    end

    -- self.CallScheduler:Schedule( self, Schedule, {}, self.TimeInterval, self.TimeInterval, 0 )
    Schedule()

    return self
  end


  ----- Iterate the SET_BASE and call an iterator function for each **alive** unit, providing the Unit and optional parameters.
  -- @param #SET_BASE self
  -- @param #function IteratorFunction The function that will be called when there is an alive unit in the SET_BASE. The function needs to accept a UNIT parameter.
  ---- @return #SET_BASE self
  -- function SET_BASE:ForEachDCSUnitAlive( IteratorFunction, ... )
  --  --self:F3( arg )
  --
  --  self:ForEach( IteratorFunction, arg, self.DCSUnitsAlive )
  --
  --  return self
  -- end
  --
  ----- Iterate the SET_BASE and call an iterator function for each **alive** player, providing the Unit of the player and optional parameters.
  -- @param #SET_BASE self
  -- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a UNIT parameter.
  ---- @return #SET_BASE self
  -- function SET_BASE:ForEachPlayer( IteratorFunction, ... )
  --  --self:F3( arg )
  --
  --  self:ForEach( IteratorFunction, arg, self.PlayersAlive )
  --
  --  return self
  -- end
  --
  --
  ----- Iterate the SET_BASE and call an iterator function for each client, providing the Client to the function and optional parameters.
  -- @param #SET_BASE self
  -- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_BASE. The function needs to accept a CLIENT parameter.
  ---- @return #SET_BASE self
  -- function SET_BASE:ForEachClient( IteratorFunction, ... )
  --  --self:F3( arg )
  --
  --  self:ForEach( IteratorFunction, arg, self.Clients )
  --
  --  return self
  -- end

  --- Decides whether to include the Object.
  -- @param #SET_BASE self
  -- @param #table Object
  -- @return #SET_BASE self
  function SET_BASE:IsIncludeObject( Object )
    --self:F3( Object )

    return true
  end

  --- Decides whether an object is in the SET
  -- @param #SET_BASE self
  -- @param #table Object
  -- @return #boolean `true` if object is in set and `false` otherwise.
  function SET_BASE:IsInSet( Object )
    --self:F3( Object )
    local outcome = false
    local name = Object:GetName()
    --self:I("SET_BASE: Objectname = "..name)
    self:ForEach(
      function(object)
        --self:I("SET_BASE: In set objectname = "..object:GetName())
        if object:GetName() == name then
          outcome = true
        end
      end
    )
    return outcome
  end
  
  --- Decides whether an object is **not** in the SET
  -- @param #SET_BASE self
  -- @param #table Object
  -- @return #SET_BASE self
  function SET_BASE:IsNotInSet( Object )
    --self:F3( Object )
    return not self:IsInSet(Object)
  end

  --- Gets a string with all the object names.
  -- @param #SET_BASE self
  -- @return #string A string with the names of the objects.
  function SET_BASE:GetObjectNames()
    --self:F3()

    local ObjectNames = ""
    for ObjectName, Object in pairs( self.Set ) do
      ObjectNames = ObjectNames .. ObjectName .. ", "
    end

    return ObjectNames
  end

  --- Flushes the current SET_BASE contents in the log ... (for debugging reasons).
  -- @param #SET_BASE self
  -- @param Core.Base#BASE MasterObject (Optional) The master object as a reference.
  -- @return #string A string with the names of the objects.
  function SET_BASE:Flush( MasterObject )
    --self:F3()

    local ObjectNames = ""
    for ObjectName, Object in pairs( self.Set ) do
      ObjectNames = ObjectNames .. ObjectName .. ", "
    end
    --self:F( { MasterObject = MasterObject and MasterObject:GetClassNameAndID(), "Objects in Set:", ObjectNames } )

    return ObjectNames
  end
  
    --- Get a *new* set table that only contains alive objects.
  -- @param #SET_BASE self
  -- @return #table Set table of alive objects.
  function SET_BASE:GetAliveSet()
    --self:F2()

    local AliveSet = {}
    -- Clean the Set before returning with only the alive Objects.
    for ObjectName, Object in pairs( self.Set ) do
      if Object then
        if Object:IsAlive() then
          AliveSet[#AliveSet+1] = Object
        end
      end
    end

    return AliveSet or {}
  end
  
end

do 

  -- SET_GROUP
  
  ---
  -- @type SET_GROUP #SET_GROUP
  -- @field Core.Timer#TIMER ZoneTimer
  -- @field #number ZoneTimerInterval
  -- @extends Core.Set#SET_BASE

  --- Mission designers can use the @{Core.Set#SET_GROUP} class to build sets of groups belonging to certain:
  --
  --  * Coalitions
  --  * Categories
  --  * Countries
  --  * Starting with certain prefix strings.
  --
  -- ## SET_GROUP constructor
  --
  -- Create a new SET_GROUP object with the @{#SET_GROUP.New} method:
  --
  --    * @{#SET_GROUP.New}: Creates a new SET_GROUP object.
  --
  -- ## Add or Remove GROUP(s) from SET_GROUP
  --
  -- GROUPS can be added and removed using the @{Core.Set#SET_GROUP.AddGroupsByName} and @{Core.Set#SET_GROUP.RemoveGroupsByName} respectively.
  -- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_GROUP.
  --
  -- ## SET_GROUP filter criteria
  --
  -- You can set filter criteria to define the set of groups within the SET_GROUP.
  -- Filter criteria are defined by:
  --
  --    * @{#SET_GROUP.FilterCoalitions}: Builds the SET_GROUP with the groups belonging to the coalition(s).
  --    * @{#SET_GROUP.FilterCategories}: Builds the SET_GROUP with the groups belonging to the category(ies).
  --    * @{#SET_GROUP.FilterCountries}: Builds the SET_GROUP with the groups belonging to the country(ies).
  --    * @{#SET_GROUP.FilterPrefixes}: Builds the SET_GROUP with the groups *containing* the given string in the group name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. 
  -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching)
  --    * @{#SET_GROUP.FilterActive}: Builds the SET_GROUP with the groups that are only active. Groups that are inactive (late activation) won't be included in the set!
  --
  -- For the Category Filter, extra methods have been added:
  --
  --    * @{#SET_GROUP.FilterCategoryAirplane}: Builds the SET_GROUP from airplanes.
  --    * @{#SET_GROUP.FilterCategoryHelicopter}: Builds the SET_GROUP from helicopters.
  --    * @{#SET_GROUP.FilterCategoryGround}: Builds the SET_GROUP from ground vehicles or infantry.
  --    * @{#SET_GROUP.FilterCategoryShip}: Builds the SET_GROUP from ships.
  --    * @{#SET_GROUP.FilterCategoryStructure}: Builds the SET_GROUP from structures.
  --    * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Core.Zone#ZONE}.
  --    * @{#SET_GROUP.FilterFunction}: Builds the SET_GROUP with a custom condition.
  --
  -- Once the filter criteria have been set for the SET_GROUP, you can start filtering using:
  --
  --    * @{#SET_GROUP.FilterStart}: Starts the filtering of the groups within the SET_GROUP and add or remove GROUP objects **dynamically**.
  --    * @{#SET_GROUP.FilterOnce}: Filters of the groups **once**.
  --
  -- ## SET_GROUP iterators
  --
  -- Once the filters have been defined and the SET_GROUP has been built, you can iterate the SET_GROUP with the available iterator methods.
  -- The iterator methods will walk the SET_GROUP set, and call for each element within the set a function that you provide.
  -- The following iterator methods are currently available within the SET_GROUP:
  --
  --   * @{#SET_GROUP.ForEachGroup}: Calls a function for each alive group it finds within the SET_GROUP.
  --   * @{#SET_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Core.Zone}, providing the GROUP and optional parameters to the called function.
  --   * @{#SET_GROUP.ForEachGroupPartlyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Core.Zone}, providing the GROUP and optional parameters to the called function.
  --   * @{#SET_GROUP.ForEachGroupNotInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Core.Zone}, providing the GROUP and optional parameters to the called function.
  --
  --
  -- ## SET_GROUP trigger events on the GROUP objects.
  --
  -- The SET is derived from the FSM class, which provides extra capabilities to track the contents of the GROUP objects in the SET_GROUP.
  --
  -- ### When a GROUP object crashes or is dead, the SET_GROUP will trigger a **Dead** event.
  --
  -- You can handle the event using the OnBefore and OnAfter event handlers.
  -- The event handlers need to have the parameters From, Event, To, GroupObject.
  -- The GroupObject is the GROUP object that is dead and within the SET_GROUP, and is passed as a parameter to the event handler.
  -- See the following example:
  --
  --        -- Create the SetCarrier SET_GROUP collection.
  --
  --        local SetHelicopter = SET_GROUP:New():FilterPrefixes( "Helicopter" ):FilterStart()
  --
  --        -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset.
  --
  --        function SetHelicopter:OnAfterDead( From, Event, To, GroupObject )
  --          --self:F( { GroupObject = GroupObject:GetName() } )
  --        end
  --
  -- While this is a good example, there is a catch.
  -- Imagine you want to execute the code above, the the self would need to be from the object declared outside (above) the OnAfterDead method.
  -- So, the self would need to contain another object. Fortunately, this can be done, but you must use then the **`.`** notation for the method.
  -- See the modified example:
  --
  --        -- Now we have a constructor of the class AI_CARGO_DISPATCHER, that receives the SetHelicopter as a parameter.
  --        -- Within that constructor, we want to set an enclosed event handler OnAfterDead for SetHelicopter.
  --        -- But within the OnAfterDead method, we want to refer to the self variable of the AI_CARGO_DISPATCHER.
  --
  --        function AI_CARGO_DISPATCHER:New( SetCarrier, SetCargo, SetDeployZones )
  --
  --          local self = BASE:Inherit( self, FSM:New() ) -- #AI_CARGO_DISPATCHER
  --
  --          -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset.
  --          -- Note the "." notation, and the explicit declaration of SetHelicopter, which would be using the ":" notation the implicit self variable declaration.
  --
  --          function SetHelicopter.OnAfterDead( SetHelicopter, From, Event, To, GroupObject )
  --            SetHelicopter:F( { GroupObject = GroupObject:GetName() } )
  --            self.PickupCargo[GroupObject] = nil  -- So here I clear the PickupCargo table entry of the self object AI_CARGO_DISPATCHER.
  --            self.CarrierHome[GroupObject] = nil
  --          end
  --
  --        end
  --
  -- ===
  -- @field #SET_GROUP SET_GROUP
  SET_GROUP = {
    ClassName = "SET_GROUP",
    Filter = {
      Coalitions = nil,
      Categories = nil,
      Countries = nil,
      GroupPrefixes = nil,
      Zones = nil,
      Functions = nil,
      Alive = nil,
    },
    FilterMeta = {
      Coalitions = {
        red = coalition.side.RED,
        blue = coalition.side.BLUE,
        neutral = coalition.side.NEUTRAL,
      },
      Categories = {
        plane = Group.Category.AIRPLANE,
        helicopter = Group.Category.HELICOPTER,
        ground = Group.Category.GROUND, -- R2.2
        ship = Group.Category.SHIP,
        structure = Group.Category.STRUCTURE,
      },
    },
  }

  --- Creates a new SET_GROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names.
  -- @param #SET_GROUP self
  -- @return #SET_GROUP
  -- @usage
  -- -- Define a new SET_GROUP Object. This DBObject will contain a reference to all alive GROUPS.
  -- DBObject = SET_GROUP:New()
  function SET_GROUP:New()

    -- Inherits from BASE
    local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.GROUPS ) ) -- #SET_GROUP

    self:FilterActive( false )

    return self
       
    --- Filter the set once
    -- @function [parent=#SET_GROUP] FilterOnce
    -- @param #SET_GROUP self
    -- @return #SET_GROUP self
    
    
  end
  
  --- Get a *new* set table that only contains alive groups.
  -- @param #SET_GROUP self
  -- @return #table Set of alive groups.
  function SET_GROUP:GetAliveSet()
    --self:F2()

    --local AliveSet = SET_GROUP:New()
    local AliveSet = {}
    -- Clean the Set before returning with only the alive Groups.
    for GroupName, GroupObject in pairs( self.Set ) do
      local GroupObject = GroupObject -- Wrapper.Group#GROUP
      if GroupObject then
        if GroupObject:IsAlive() then
          AliveSet[GroupName] = GroupObject
        end
      end
    end

    return AliveSet or {}
  end

  --- Returns a report of of unit types.
  -- @param #SET_GROUP self
  -- @return Core.Report#REPORT A report of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found.
  function SET_GROUP:GetUnitTypeNames()
    --self:F2()

    local MT = {} -- Message Text
    local UnitTypes = {}

    local ReportUnitTypes = REPORT:New()

    for GroupID, GroupData in pairs( self:GetSet() ) do
      local Units = GroupData:GetUnits()
      for UnitID, UnitData in pairs( Units ) do
        if UnitData:IsAlive() then
          local UnitType = UnitData:GetTypeName()

          if not UnitTypes[UnitType] then
            UnitTypes[UnitType] = 1
          else
            UnitTypes[UnitType] = UnitTypes[UnitType] + 1
          end
        end
      end
    end

    for UnitTypeID, UnitType in pairs( UnitTypes ) do
      ReportUnitTypes:Add( UnitType .. " of " .. UnitTypeID )
    end

    return ReportUnitTypes
  end

  --- Add a GROUP to SET_GROUP.
  -- Note that for each unit in the group that is set, a default cargo bay limit is initialized.
  -- @param Core.Set#SET_GROUP self
  -- @param Wrapper.Group#GROUP group The group which should be added to the set.
  -- @param #boolean DontSetCargoBayLimit If true, do not attempt to auto-add the cargo bay limit per unit in this group.
  -- @return Core.Set#SET_GROUP self
  function SET_GROUP:AddGroup( group, DontSetCargoBayLimit )

    self:Add( group:GetName(), group )
    
    if not DontSetCargoBayLimit then
      -- I set the default cargo bay weight limit each time a new group is added to the set.
      -- TODO Why is this here in the first place?
      for UnitID, UnitData in pairs( group:GetUnits() or {} ) do
        if UnitData and UnitData:IsAlive() then
          UnitData:SetCargoBayWeightLimit()
        end
      end
    end
    
    return self
  end

  --- Add GROUP(s) to SET_GROUP.
  -- @param Core.Set#SET_GROUP self
  -- @param #string AddGroupNames A single name or an array of GROUP names.
  -- @return Core.Set#SET_GROUP self
  function SET_GROUP:AddGroupsByName( AddGroupNames )

    local AddGroupNamesArray = (type( AddGroupNames ) == "table") and AddGroupNames or { AddGroupNames }

    for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do
      self:Add( AddGroupName, GROUP:FindByName( AddGroupName ) )
    end

    return self
  end

  --- Remove GROUP(s) from SET_GROUP.
  -- @param Core.Set#SET_GROUP self
  -- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names.
  -- @return Core.Set#SET_GROUP self
  function SET_GROUP:RemoveGroupsByName( RemoveGroupNames )

    local RemoveGroupNamesArray = (type( RemoveGroupNames ) == "table") and RemoveGroupNames or { RemoveGroupNames }

    for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do
      self:Remove( RemoveGroupName )
    end

    return self
  end

  --- Finds a Group based on the Group Name.
  -- @param #SET_GROUP self
  -- @param #string GroupName
  -- @return Wrapper.Group#GROUP The found Group.
  function SET_GROUP:FindGroup( GroupName )

    local GroupFound = self.Set[GroupName]
    return GroupFound
  end

  --- Iterate the SET_GROUP while identifying the nearest object from a @{Core.Point#COORDINATE}.
  -- @param #SET_GROUP self
  -- @param Core.Point#COORDINATE Coordinate A @{Core.Point#COORDINATE} object from where to evaluate the closest object in the set.
  -- @return Wrapper.Group#GROUP The closest group.
  function SET_GROUP:FindNearestGroupFromPointVec2( Coordinate )
    --self:F2( Coordinate )

    local NearestGroup = nil -- Wrapper.Group#GROUP
    local ClosestDistance = nil
    
    local Set = self:GetAliveSet()
    
    for ObjectID, ObjectData in pairs( Set ) do
      if NearestGroup == nil then
        NearestGroup = ObjectData
        ClosestDistance = Coordinate:DistanceFromPointVec2( ObjectData:GetCoordinate() )
      else
        local Distance = Coordinate:DistanceFromPointVec2( ObjectData:GetCoordinate() )
        if Distance < ClosestDistance then
          NearestGroup = ObjectData
          ClosestDistance = Distance
        end
      end
    end

    return NearestGroup
  end

  --- Builds a set of groups in zones.
  -- @param #SET_GROUP self
  -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE
  -- @param #boolean Clear If `true`, clear any previously defined filters.
  -- @return #SET_GROUP self
  function SET_GROUP:FilterZones( Zones, Clear )
  
    if Clear or not self.Filter.Zones then
      self.Filter.Zones = {}
    end
    
    local zones = {}
    if Zones.ClassName and Zones.ClassName == "SET_ZONE" then
      zones = Zones.Set
    elseif type( Zones ) ~= "table" or (type( Zones ) == "table" and Zones.ClassName) then
      self:E( "***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!" )
      return self
    else
      zones = Zones
    end
    
    for _, Zone in pairs( zones ) do
      local zonename = Zone:GetName()
      self.Filter.Zones[zonename] = Zone
    end
    
    return self
  end
  
  --- [User] Add a custom condition function.
  -- @function [parent=#SET_GROUP] FilterFunction
  -- @param #SET_GROUP self
  -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a GROUP object as first argument.
  -- @param ... Condition function arguments if any.
  -- @return #SET_GROUP self
  -- @usage
  --          -- Image you want to exclude a specific GROUP from a SET:
  --          local groundset = SET_GROUP:New():FilterCoalitions("blue"):FilterCategoryGround():FilterFunction(
  --          -- The function needs to take a GROUP object as first - and in this case, only - argument.
  --          function(grp)
  --              local isinclude = true
  --              if grp:GetName() == "Exclude Me" then isinclude = false end
  --              return isinclude
  --          end
  --          ):FilterOnce()
  --          BASE:I(groundset:Flush())

  
  --- Builds a set of groups of coalitions.
  -- Possible current coalitions are red, blue and neutral.
  -- @param #SET_GROUP self
  -- @param #string Coalitions Can take the following values: "red", "blue", "neutral".
  -- @param #boolean Clear If `true`, clear any previously defined filters.
  -- @return #SET_GROUP self
  function SET_GROUP:FilterCoalitions( Coalitions, Clear )
  
    if Clear or (not self.Filter.Coalitions) then
      self.Filter.Coalitions = {}
    end
    
    -- Ensure table.
    Coalitions = UTILS.EnsureTable(Coalitions, false)
    
    for CoalitionID, Coalition in pairs( Coalitions ) do
      self.Filter.Coalitions[Coalition] = Coalition
    end
    
    return self
  end

  --- Builds a set of groups out of categories.
  -- Possible current categories are plane, helicopter, ground, ship.
  -- @param #SET_GROUP self
  -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship".
  -- @param #boolean Clear If `true`, clear any previously defined filters.
  -- @return #SET_GROUP self
  function SET_GROUP:FilterCategories( Categories, Clear )
  
    if Clear or not self.Filter.Categories then
      self.Filter.Categories = {}
    end
    
    if type( Categories ) ~= "table" then
      Categories = { Categories }
    end
    
    for CategoryID, Category in pairs( Categories ) do
      self.Filter.Categories[Category] = Category
    end
    
    return self
  end

  --- Builds a set of groups out of ground category.
  -- @param #SET_GROUP self
  -- @return #SET_GROUP self
  function SET_GROUP:FilterCategoryGround()
    self:FilterCategories( "ground" )
    return self
  end

  --- Builds a set of groups out of airplane category.
  -- @param #SET_GROUP self
  -- @return #SET_GROUP self
  function SET_GROUP:FilterCategoryAirplane()
    self:FilterCategories( "plane" )
    return self
  end

  --- Builds a set of groups out of helicopter category.
  -- @param #SET_GROUP self
  -- @return #SET_GROUP self
  function SET_GROUP:FilterCategoryHelicopter()
    self:FilterCategories( "helicopter" )
    return self
  end

  --- Builds a set of groups out of ship category.
  -- @param #SET_GROUP self
  -- @return #SET_GROUP self
  function SET_GROUP:FilterCategoryShip()
    self:FilterCategories( "ship" )
    return self
  end

  --- Builds a set of groups out of structure category.
  -- @param #SET_GROUP self
  -- @return #SET_GROUP self
  function SET_GROUP:FilterCategoryStructure()
    self:FilterCategories( "structure" )
    return self
  end

  --- Builds a set of groups of defined countries.
  -- Possible current countries are those known within DCS world.
  -- @param #SET_GROUP self
  -- @param #string Countries Can take those country strings known within DCS world.
  -- @return #SET_GROUP self
  function SET_GROUP:FilterCountries( Countries )
    if not self.Filter.Countries then
      self.Filter.Countries = {}
    end
    if type( Countries ) ~= "table" then
      Countries = { Countries }
    end
    for CountryID, Country in pairs( Countries ) do
      self.Filter.Countries[Country] = Country
    end
    return self
  end

  --- Builds a set of groups that contain the given string in their group name.
  -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all groups that **contain** the string.
  -- @param #SET_GROUP self
  -- @param #string Prefixes The string pattern(s) that needs to be contained in the group name. Can also be passed as a `#table` of strings.
  -- @return #SET_GROUP self
  function SET_GROUP:FilterPrefixes( Prefixes )
    if not self.Filter.GroupPrefixes then
      self.Filter.GroupPrefixes = {}
    end
    if type( Prefixes ) ~= "table" then
      Prefixes = { Prefixes }
    end
    for PrefixID, Prefix in pairs( Prefixes ) do
      self.Filter.GroupPrefixes[Prefix] = Prefix
    end
    return self
  end
  
  --- [Internal] Private function for use of continous zone filter
  -- @param #SET_GROUP self
  -- @return #SET_GROUP self
  function SET_GROUP:_ContinousZoneFilter()
    
    local Database = _DATABASE.GROUPS
    
    for ObjectName, Object in pairs( Database ) do
      if self:IsIncludeObject( Object ) and self:IsNotInSet(Object) then
        self:Add( ObjectName, Object )
      elseif (not self:IsIncludeObject( Object )) and self:IsInSet(Object) then
        self:Remove(ObjectName)
      end
    end
    
    return self
    
  end

  --- Builds a set of groups that are active, ie in the mission but not yet activated (false) or actived (true).
  -- Only the groups that are active will be included within the set.
  -- @param #SET_GROUP self
  -- @param #boolean Active (Optional) Include only active groups to the set.
  -- Include inactive groups if you provide false.
  -- @return #SET_GROUP self
  -- @usage
  --
  -- -- Include only active groups to the set.
  -- GroupSet = SET_GROUP:New():FilterActive():FilterStart()
  --
  -- -- Include only active groups to the set of the blue coalition, and filter one time.
  -- GroupSet = SET_GROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce()
  --
  -- -- Include only active groups to the set of the blue coalition, and filter one time.
  -- -- Later, reset to include back inactive groups to the set.
  -- GroupSet = SET_GROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce()
  -- ... logic ...
  -- GroupSet = SET_GROUP:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce()
  --
  function SET_GROUP:FilterActive( Active )
    Active = Active or not (Active == false)
    self.Filter.Active = Active
    return self
  end
  
  --- Build a set of groups that are alive.
  -- @param #SET_GROUP self
  -- @return #SET_GROUP self
  function SET_GROUP:FilterAlive()
    self.Filter.Alive = true
    return self
  end

  --- Starts the filtering.
  -- @param #SET_GROUP self
  -- @return #SET_GROUP self
  function SET_GROUP:FilterStart()

    if _DATABASE then
      self:_FilterStart()
      self:HandleEvent( EVENTS.Birth, self._EventOnBirth )
      self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.UnitLost, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnDeadOrCrash )
      if self.Filter.Zones then
        self.ZoneTimer = TIMER:New(self._ContinousZoneFilter,self)
        local timing = self.ZoneTimerInterval or 30
        self.ZoneTimer:Start(timing,timing)
      end
    end

    return self
  end
  
  --- Set filter timer interval for FilterZones if using active filtering with FilterStart().
  -- @param #SET_GROUP self
  -- @param #number Seconds Seconds between check intervals, defaults to 30. **Caution** - do not be too agressive with timing! Groups are usually not moving fast enough
  -- to warrant a check of below 10 seconds.
  -- @return #SET_GROUP self
  function SET_GROUP:FilterZoneTimer(Seconds)
    self.ZoneTimerInterval = Seconds or 30
    return self
  end
  
  --- Stops the filtering.
  -- @param #SET_GROUP self
  -- @return #SET_GROUP self
  function SET_GROUP:FilterStop()

    if _DATABASE then
      
      self:UnHandleEvent(EVENTS.Birth)
      self:UnHandleEvent(EVENTS.Dead)
      self:UnHandleEvent(EVENTS.Crash)
      self:UnHandleEvent(EVENTS.RemoveUnit)
      self:UnHandleEvent(EVENTS.UnitLost)
      
      if self.Filter.Zones and self.ZoneTimer and self.ZoneTimer:IsRunning() then
        self.ZoneTimer:Stop()
      end
    end

    return self
  end


  --- Handles the OnDead or OnCrash event for alive groups set.
  -- Note: The GROUP object in the SET_GROUP collection will only be removed if the last unit is destroyed of the GROUP.
  -- @param #SET_GROUP self
  -- @param Core.Event#EVENTDATA Event
  function SET_GROUP:_EventOnDeadOrCrash( Event )
    --self:F( { Event } )

    if Event.IniDCSUnit then
      local ObjectName, Object = self:FindInDatabase( Event )
      if ObjectName then
        local size = 1
        if Event.IniDCSGroup then
         size = Event.IniDCSGroup:getSize()
        elseif Event.IniDCSGroupName then
          local grp = Group.getByName(Event.IniDCSGroupName)
          if grp then
            size = grp:getSize()
          end
        elseif Object:IsAlive() then
          size = Object:CountAliveUnits()
        end
        if size == 1 then -- Only remove if the last unit of the group was destroyed.
          self:Remove( ObjectName )
        end
      end
    end
  end

  --- Handles the Database to check on an event (birth) that the Object was added in the Database.
  -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event!
  -- @param #SET_GROUP self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the GROUP
  -- @return #table The GROUP
  function SET_GROUP:AddInDatabase( Event )
    --self:F3( { Event } )

    if Event.IniObjectCategory == Object.Category.UNIT then
      if not self.Database[Event.IniDCSGroupName] then
        self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName )
        --self:T(3( self.Database[Event.IniDCSGroupName] )
      end
    end

    return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName]
  end

  --- Handles the Database to check on any event that Object exists in the Database.
  -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
  -- @param #SET_GROUP self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the GROUP
  -- @return #table The GROUP
  function SET_GROUP:FindInDatabase( Event )
    --self:F3( { Event } )

    return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName]
  end

  --- Iterate the SET_GROUP and call an iterator function for each GROUP object, providing the GROUP and optional parameters.
  -- @param #SET_GROUP self
  -- @param #function IteratorFunction The function that will be called for all GROUP in the SET_GROUP. The function needs to accept a GROUP parameter.
  -- @return #SET_GROUP self
  function SET_GROUP:ForEachGroup( IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet() )

    return self
  end

  --- Iterate the SET_GROUP and call an iterator function for some GROUP objects, providing the GROUP and optional parameters.
  -- @param #SET_GROUP self
  -- @param #function IteratorFunction The function that will be called for some GROUP in the SET_GROUP. The function needs to accept a GROUP parameter.
  -- @return #SET_GROUP self
  function SET_GROUP:ForSomeGroup( IteratorFunction, ... )
    --self:F2( arg )

    self:ForSome( IteratorFunction, arg, self:GetSet() )

    return self
  end

  --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP object, providing the GROUP and optional parameters.
  -- @param #SET_GROUP self
  -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter.
  -- @return #SET_GROUP self
  function SET_GROUP:ForEachGroupAlive( IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetAliveSet() )

    return self
  end

  --- Iterate the SET_GROUP and call an iterator function for some **alive** GROUP objects, providing the GROUP and optional parameters.
  -- @param #SET_GROUP self
  -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter.
  -- @return #SET_GROUP self
  function SET_GROUP:ForSomeGroupAlive( IteratorFunction, ... )
    --self:F2( arg )

    self:ForSome( IteratorFunction, arg, self:GetAliveSet() )

    return self
  end

  --- Activate late activated groups.
  -- @param #SET_GROUP self
  -- @param #number Delay Delay in seconds.
  -- @return #SET_GROUP self
  function SET_GROUP:Activate(Delay)
    local Set = self:GetSet()
    for GroupID, GroupData in pairs(Set) do -- For each GROUP in SET_GROUP
      local group=GroupData --Wrapper.Group#GROUP
      if group and group:IsAlive()==false then
        group:Activate(Delay)
      end
    end
    return self
  end


  --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{Core.Zone}, providing the GROUP and optional parameters to the called function.
  -- @param #SET_GROUP self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter.
  -- @return #SET_GROUP self
  function SET_GROUP:ForEachGroupCompletelyInZone( ZoneObject, IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet(),
      -- @param Core.Zone#ZONE_BASE ZoneObject
      -- @param Wrapper.Group#GROUP GroupObject
      function( ZoneObject, GroupObject )
        if GroupObject:IsCompletelyInZone( ZoneObject ) then
          return true
        else
          return false
        end
      end, { ZoneObject } )

    return self
  end

  --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence partly in a @{Core.Zone}, providing the GROUP and optional parameters to the called function.
  -- @param #SET_GROUP self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter.
  -- @return #SET_GROUP self
  function SET_GROUP:ForEachGroupPartlyInZone( ZoneObject, IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet(),
      -- @param Core.Zone#ZONE_BASE ZoneObject
      -- @param Wrapper.Group#GROUP GroupObject
      function( ZoneObject, GroupObject )
        if GroupObject:IsPartlyInZone( ZoneObject ) then
          return true
        else
          return false
        end
      end, { ZoneObject } )

    return self
  end

  --- Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence not in a @{Core.Zone}, providing the GROUP and optional parameters to the called function.
  -- @param #SET_GROUP self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter.
  -- @return #SET_GROUP self
  function SET_GROUP:ForEachGroupNotInZone( ZoneObject, IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet(),
      -- @param Core.Zone#ZONE_BASE ZoneObject
      -- @param Wrapper.Group#GROUP GroupObject
      function( ZoneObject, GroupObject )
        if GroupObject:IsNotInZone( ZoneObject ) then
          return true
        else
          return false
        end
      end, { ZoneObject } )

    return self
  end

  --- Iterate the SET_GROUP and return true if all the @{Wrapper.Group#GROUP} are completely in the @{Core.Zone#ZONE}
  -- @param #SET_GROUP self
  -- @param Core.Zone#ZONE Zone The Zone to be tested for.
  -- @return #boolean true if all the @{Wrapper.Group#GROUP} are completely in the @{Core.Zone#ZONE}, false otherwise
  -- @usage
  -- local MyZone = ZONE:New("Zone1")
  -- local MySetGroup = SET_GROUP:New()
  -- MySetGroup:AddGroupsByName({"Group1", "Group2"})
  --
  -- if MySetGroup:AllCompletelyInZone(MyZone) then
  --   MESSAGE:New("All the SET's GROUP are in zone !", 10):ToAll()
  -- else
  --   MESSAGE:New("Some or all SET's GROUP are outside zone !", 10):ToAll()
  -- end
  function SET_GROUP:AllCompletelyInZone( Zone )
    --self:F2( Zone )
    local Set = self:GetSet()
    for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP
      if not GroupData:IsCompletelyInZone( Zone ) then
        return false
      end
    end
    return true
  end

  --- Iterate the SET_GROUP and call an iterator function for each alive GROUP that has any unit in the @{Core.Zone}, providing the GROUP and optional parameters to the called function.
  -- @param #SET_GROUP self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @param #function IteratorFunction The function that will be called when there is an alive GROUP in the SET_GROUP. The function needs to accept a GROUP parameter.
  -- @return #SET_GROUP self
  function SET_GROUP:ForEachGroupAnyInZone( ZoneObject, IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet(),
      -- @param Core.Zone#ZONE_BASE ZoneObject
      -- @param Wrapper.Group#GROUP GroupObject
      function( ZoneObject, GroupObject )
        if GroupObject:IsAnyInZone( ZoneObject ) then
          return true
        else
          return false
        end
      end, { ZoneObject } )

    return self
  end

  --- Iterate the SET_GROUP and return true if at least one of the @{Wrapper.Group#GROUP} is completely inside the @{Core.Zone#ZONE}
  -- @param #SET_GROUP self
  -- @param Core.Zone#ZONE Zone The Zone to be tested for.
  -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is completely inside the @{Core.Zone#ZONE}, false otherwise.
  -- @usage
  -- local MyZone = ZONE:New("Zone1")
  -- local MySetGroup = SET_GROUP:New()
  -- MySetGroup:AddGroupsByName({"Group1", "Group2"})
  --
  -- if MySetGroup:AnyCompletelyInZone(MyZone) then
  --   MESSAGE:New("At least one GROUP is completely in zone !", 10):ToAll()
  -- else
  --   MESSAGE:New("No GROUP is completely in zone !", 10):ToAll()
  -- end
  function SET_GROUP:AnyCompletelyInZone( Zone )
    --self:F2( Zone )
    local Set = self:GetSet()
    for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP
      if GroupData:IsCompletelyInZone( Zone ) then
        return true
      end
    end
    return false
  end

  --- Iterate the SET_GROUP and return true if at least one @{#UNIT} of one @{Wrapper.Group#GROUP} of the @{#SET_GROUP} is in @{Core.Zone}
  -- @param #SET_GROUP self
  -- @param Core.Zone#ZONE Zone The Zone to be tested for.
  -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completely inside the @{Core.Zone#ZONE}, false otherwise.
  -- @usage
  -- local MyZone = ZONE:New("Zone1")
  -- local MySetGroup = SET_GROUP:New()
  -- MySetGroup:AddGroupsByName({"Group1", "Group2"})
  --
  -- if MySetGroup:AnyPartlyInZone(MyZone) then
  --   MESSAGE:New("At least one GROUP has at least one UNIT in zone !", 10):ToAll()
  -- else
  --   MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll()
  -- end
  function SET_GROUP:AnyInZone( Zone )
    --self:F2( Zone )
    local Set = self:GetSet()
    for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP
      if GroupData:IsPartlyInZone( Zone ) or GroupData:IsCompletelyInZone( Zone ) then
        return true
      end
    end
    return false
  end

  --- Iterate the SET_GROUP and return true if at least one @{Wrapper.Group#GROUP} of the @{#SET_GROUP} is partly in @{Core.Zone}.
  -- Will return false if a @{Wrapper.Group#GROUP} is fully in the @{Core.Zone}
  -- @param #SET_GROUP self
  -- @param Core.Zone#ZONE Zone The Zone to be tested for.
  -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completely inside the @{Core.Zone#ZONE}, false otherwise.
  -- @usage
  -- local MyZone = ZONE:New("Zone1")
  -- local MySetGroup = SET_GROUP:New()
  -- MySetGroup:AddGroupsByName({"Group1", "Group2"})
  --
  -- if MySetGroup:AnyPartlyInZone(MyZone) then
  --   MESSAGE:New("At least one GROUP is partially in the zone, but none are fully in it !", 10):ToAll()
  -- else
  --   MESSAGE:New("No GROUP are in zone, or one (or more) GROUP is completely in it !", 10):ToAll()
  -- end
  function SET_GROUP:AnyPartlyInZone( Zone )
    --self:F2( Zone )
    local IsPartlyInZone = false
    local Set = self:GetSet()
    for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP
      if GroupData:IsCompletelyInZone( Zone ) then
        return false
      elseif GroupData:IsPartlyInZone( Zone ) then
        IsPartlyInZone = true -- at least one GROUP is partly in zone
      end
    end

    if IsPartlyInZone then
      return true
    else
      return false
    end
  end

  --- Iterate the SET_GROUP and return true if no @{Wrapper.Group#GROUP} of the @{#SET_GROUP} is in @{Core.Zone}
  -- This could also be achieved with `not SET_GROUP:AnyPartlyInZone(Zone)`, but it's easier for the
  -- mission designer to add a dedicated method
  -- @param #SET_GROUP self
  -- @param Core.Zone#ZONE Zone The Zone to be tested for.
  -- @return #boolean true if no @{Wrapper.Group#GROUP} is inside the @{Core.Zone#ZONE} in any way, false otherwise.
  -- @usage
  -- local MyZone = ZONE:New("Zone1")
  -- local MySetGroup = SET_GROUP:New()
  -- MySetGroup:AddGroupsByName({"Group1", "Group2"})
  --
  -- if MySetGroup:NoneInZone(MyZone) then
  --   MESSAGE:New("No GROUP is completely in zone !", 10):ToAll()
  -- else
  --   MESSAGE:New("No UNIT of any GROUP is in zone !", 10):ToAll()
  -- end
  function SET_GROUP:NoneInZone( Zone )
    --self:F2( Zone )
    local Set = self:GetSet()
    for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP
      if not GroupData:IsNotInZone( Zone ) then -- If the GROUP is in Zone in any way
        return false
      end
    end
    return true
  end

  --- Iterate the SET_GROUP and count how many GROUPs are completely in the Zone
  -- That could easily be done with SET_GROUP:ForEachGroupCompletelyInZone(), but this function
  -- provides an easy to use shortcut...
  -- @param #SET_GROUP self
  -- @param Core.Zone#ZONE Zone The Zone to be tested for.
  -- @return #number the number of GROUPs completely in the Zone
  -- @usage
  -- local MyZone = ZONE:New("Zone1")
  -- local MySetGroup = SET_GROUP:New()
  -- MySetGroup:AddGroupsByName({"Group1", "Group2"})
  --
  -- MESSAGE:New("There are " .. MySetGroup:CountInZone(MyZone) .. " GROUPs in the Zone !", 10):ToAll()
  function SET_GROUP:CountInZone( Zone )
    --self:F2( Zone )
    local Count = 0
    local Set = self:GetSet()
    for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP
      if GroupData:IsCompletelyInZone( Zone ) then
        Count = Count + 1
      end
    end
    return Count
  end

  --- Iterate the SET_GROUP and count how many UNITs are completely in the Zone
  -- @param #SET_GROUP self
  -- @param Core.Zone#ZONE Zone The Zone to be tested for.
  -- @return #number the number of GROUPs completely in the Zone
  -- @usage
  -- local MyZone = ZONE:New("Zone1")
  -- local MySetGroup = SET_GROUP:New()
  -- MySetGroup:AddGroupsByName({"Group1", "Group2"})
  --
  -- MESSAGE:New("There are " .. MySetGroup:CountUnitInZone(MyZone) .. " UNITs in the Zone !", 10):ToAll()
  function SET_GROUP:CountUnitInZone( Zone )
    --self:F2( Zone )
    local Count = 0
    local Set = self:GetSet()
    for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP
      Count = Count + GroupData:CountInZone( Zone )
    end
    return Count
  end

  --- Iterate the SET_GROUP and count how many GROUPs and UNITs are alive.
  -- @param #SET_GROUP self
  -- @return #number The number of GROUPs alive.
  -- @return #number The number of UNITs alive.
  function SET_GROUP:CountAlive()
    local CountG = 0
    local CountU = 0

    local Set = self:GetSet()

    for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP
      if GroupData and GroupData:IsAlive() then

        CountG = CountG + 1

        -- Count Units.
        for _, _unit in pairs( GroupData:GetUnits() ) do
          local unit = _unit -- Wrapper.Unit#UNIT
          if unit and unit:IsAlive() then
            CountU = CountU + 1
          end
        end
      end

    end

    return CountG, CountU
  end

  ----- Iterate the SET_GROUP and call an iterator function for each **alive** player, providing the Group of the player and optional parameters.
  -- @param #SET_GROUP self
  -- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a GROUP parameter.
  ---- @return #SET_GROUP self
  -- function SET_GROUP:ForEachPlayer( IteratorFunction, ... )
  --  --self:F2( arg )
  --
  --  self:ForEach( IteratorFunction, arg, self.PlayersAlive )
  --
  --  return self
  -- end
  --
  --
  ----- Iterate the SET_GROUP and call an iterator function for each client, providing the Client to the function and optional parameters.
  -- @param #SET_GROUP self
  -- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_GROUP. The function needs to accept a CLIENT parameter.
  ---- @return #SET_GROUP self
  -- function SET_GROUP:ForEachClient( IteratorFunction, ... )
  --  --self:F2( arg )
  --
  --  self:ForEach( IteratorFunction, arg, self.Clients )
  --
  --  return self
  -- end

  ---
  -- @param #SET_GROUP self
  -- @param Wrapper.Group#GROUP MGroup The group that is checked for inclusion.
  -- @return #SET_GROUP self
  function SET_GROUP:IsIncludeObject( MGroup )
    --self:F2( MGroup )
    local MGroupInclude = true
    
    if self.Filter.Alive == true then
      local MGroupAlive = false
      --self:F( { Active = self.Filter.Active } )
      if MGroup and MGroup:IsAlive() then
        MGroupAlive = true
      end
      MGroupInclude = MGroupInclude and MGroupAlive
    end
    
    if self.Filter.Active ~= nil then
      local MGroupActive = false
      --self:F( { Active = self.Filter.Active } )
      if self.Filter.Active == false or (self.Filter.Active == true and MGroup:IsActive() == true) then
        MGroupActive = true
      end
      MGroupInclude = MGroupInclude and MGroupActive
    end

    if self.Filter.Coalitions and MGroupInclude then
      local MGroupCoalition = false
      for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do
        --self:T3( { "Coalition:", MGroup:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } )
        if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MGroup:GetCoalition() then
          MGroupCoalition = true
        end
      end
      MGroupInclude = MGroupInclude and MGroupCoalition
    end

    if self.Filter.Categories and MGroupInclude then
      local MGroupCategory = false
      for CategoryID, CategoryName in pairs( self.Filter.Categories ) do
        --self:I( { "Category:", MGroup:GetCategory(), self.FilterMeta.Categories[CategoryName], CategoryName } )
        if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MGroup:GetCategory() then
          MGroupCategory = true
        end
      end
      MGroupInclude = MGroupInclude and MGroupCategory
      --self:I("Is Included: "..tostring(MGroupInclude))
    end

    if self.Filter.Countries and MGroupInclude then
      local MGroupCountry = false
      for CountryID, CountryName in pairs( self.Filter.Countries ) do
        --self:T3( { "Country:", MGroup:GetCountry(), CountryName } )
        if country.id[CountryName] == MGroup:GetCountry() then
          MGroupCountry = true
        end
      end
      MGroupInclude = MGroupInclude and MGroupCountry
    end

    if self.Filter.GroupPrefixes and MGroupInclude then
      local MGroupPrefix = false
      for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do
        --self:I( { "Prefix:", MGroup:GetName(), GroupPrefix } )
        if string.find(MGroup:GetName(), string.gsub(GroupPrefix,"-","%%-"),1) then
          MGroupPrefix = true
        end
      end
      MGroupInclude = MGroupInclude and MGroupPrefix
      --self:I("Is Included: "..tostring(MGroupInclude))
    end
    
    if self.Filter.Zones and MGroupInclude then
      local MGroupZone = false
      for ZoneName, Zone in pairs( self.Filter.Zones ) do
        --self:T( "Zone:", ZoneName )
        if MGroup:IsInZone(Zone) then
          MGroupZone = true
        end
      end
      MGroupInclude = MGroupInclude and MGroupZone
    end
    
    if self.Filter.Functions and MGroupInclude then
      local MGroupFunc = false
      MGroupFunc = self:_EvalFilterFunctions(MGroup)
      MGroupInclude = MGroupInclude and MGroupFunc
    end
     
    --self:I( MGroupInclude )
    return MGroupInclude
  end

  --- Get the closest group of the set with respect to a given reference coordinate. Optionally, only groups of given coalitions are considered in the search.
  -- @param #SET_GROUP self
  -- @param Core.Point#COORDINATE Coordinate Reference Coordinate from which the closest group is determined.
  -- @param #table Coalitions (Optional) Table of coalition #number entries to filter for.
  -- @return Wrapper.Group#GROUP The closest group (if any).
  -- @return #number Distance in meters to the closest group.
  function SET_GROUP:GetClosestGroup(Coordinate, Coalitions)
  
    local Set = self:GetSet()
    
    local dmin=math.huge
    local gmin=nil
    
    for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP
      local group=GroupData --Wrapper.Group#GROUP
      
      if group and group:IsAlive() and (Coalitions==nil or UTILS.IsAnyInTable(Coalitions, group:GetCoalition())) then
      
        local coord=group:GetCoordinate()
        
        local d
        
        if coord ~= nil then
          -- Distance between ref. coordinate and group coordinate.
          d=UTILS.VecDist3D(Coordinate, coord)
        
          if d<dmin then
            dmin=d
            gmin=group
          end
        end
      end
    
    end
    
    return gmin, dmin
  end

  --- Iterate the SET_GROUP and set for each unit the default cargo bay weight limit.
  -- Because within a group, the type of carriers can differ, each cargo bay weight limit is set on @{Wrapper.Unit} level.
  -- @param #SET_GROUP self
  -- @usage
  -- -- Set the default cargo bay weight limits of the carrier units.
  -- local MySetGroup = SET_GROUP:New()
  -- MySetGroup:SetCargoBayWeightLimit()
  function SET_GROUP:SetCargoBayWeightLimit()
    local Set = self:GetSet()
    for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP
      for UnitName, UnitData in pairs( GroupData:GetUnits() ) do
        -- local UnitData = UnitData -- Wrapper.Unit#UNIT
        UnitData:SetCargoBayWeightLimit()
      end
    end
  end

end

do -- SET_UNIT
  
  ---
  -- @type SET_UNIT SET\_UNIT
  -- @field Core.Timer#TIMER ZoneTimer
  -- @field #number ZoneTimerInterval
  -- @extends Core.Set#SET_BASE

  --- Mission designers can use the SET_UNIT class to build sets of units belonging to certain:
  --
  --  * Coalitions
  --  * Categories
  --  * Countries
  --  * Unit types
  --  * Starting with certain prefix strings.
  --
  -- ## 1) SET_UNIT constructor
  --
  -- Create a new SET_UNIT object with the @{#SET_UNIT.New} method:
  --
  --    * @{#SET_UNIT.New}: Creates a new SET_UNIT object.
  --
  -- ## 2) Add or Remove UNIT(s) from SET_UNIT
  --
  -- UNITs can be added and removed using the @{Core.Set#SET_UNIT.AddUnitsByName} and @{Core.Set#SET_UNIT.RemoveUnitsByName} respectively.
  -- These methods take a single UNIT name or an array of UNIT names to be added or removed from SET_UNIT.
  --
  -- ## 3) SET_UNIT filter criteria
  --
  -- You can set filter criteria to define the set of units within the SET_UNIT.
  -- Filter criteria are defined by:
  --
  --    * @{#SET_UNIT.FilterCoalitions}: Builds the SET_UNIT with the units belonging to the coalition(s).
  --    * @{#SET_UNIT.FilterCategories}: Builds the SET_UNIT with the units belonging to the category(ies).
  --    * @{#SET_UNIT.FilterTypes}: Builds the SET_UNIT with the units belonging to the unit type(s).
  --    * @{#SET_UNIT.FilterCountries}: Builds the SET_UNIT with the units belonging to the country(ies).
  --    * @{#SET_UNIT.FilterPrefixes}: Builds the SET_UNIT with the units sharing the same string(s) in their name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. 
  -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching)
  --    * @{#SET_UNIT.FilterActive}: Builds the SET_UNIT with the units that are only active. Units that are inactive (late activation) won't be included in the set!
  --    * @{#SET_UNIT.FilterZones}: Builds the SET_UNIT with the units within a @{Core.Zone#ZONE}.
  --    * @{#SET_UNIT.FilterFunction}: Builds the SET_UNIT with a custom condition.
  --    
  -- Once the filter criteria have been set for the SET_UNIT, you can start filtering using:
  --
  --   * @{#SET_UNIT.FilterStart}: Starts the filtering of the units **dynamically**.
  --   * @{#SET_UNIT.FilterOnce}: Filters of the units **once**.
  --
  -- ## 4) SET_UNIT iterators
  --
  -- Once the filters have been defined and the SET_UNIT has been built, you can iterate the SET_UNIT with the available iterator methods.
  -- The iterator methods will walk the SET_UNIT set, and call for each element within the set a function that you provide.
  -- The following iterator methods are currently available within the SET_UNIT:
  --
  --   * @{#SET_UNIT.ForEachUnit}: Calls a function for each alive unit it finds within the SET_UNIT.
  --   * @{#SET_UNIT.ForEachUnitInZone}: Iterate the SET_UNIT and call an iterator function for each **alive** UNIT object presence completely in a @{Core.Zone}, providing the UNIT object and optional parameters to the called function.
  --   * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate the SET_UNIT and call an iterator function for each **alive** UNIT object presence not in a @{Core.Zone}, providing the UNIT object and optional parameters to the called function.
  --   * @{#SET_UNIT:ForEachUnitPerThreatLevel}: Iterate the SET_UNIT **sorted *per Threat Level** and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters
  --
  -- ## 5) SET_UNIT atomic methods
  --
  -- Various methods exist for a SET_UNIT to perform actions or calculations and retrieve results from the SET_UNIT:
  --
  --   * @{#SET_UNIT.GetTypeNames}(): Retrieve the type names of the @{Wrapper.Unit}s in the SET, delimited by a comma.
  --
  -- ## 6) SET_UNIT trigger events on the UNIT objects.
  --
  -- The SET is derived from the FSM class, which provides extra capabilities to track the contents of the UNIT objects in the SET_UNIT.
  --
  -- ### 6.1) When a UNIT object crashes or is dead, the SET_UNIT will trigger a **Dead** event.
  --
  -- You can handle the event using the OnBefore and OnAfter event handlers.
  -- The event handlers need to have the parameters From, Event, To, GroupObject.
  -- The GroupObject is the UNIT object that is dead and within the SET_UNIT, and is passed as a parameter to the event handler.
  -- See the following example:
  --
  --        -- Create the SetCarrier SET_UNIT collection.
  --
  --        local SetHelicopter = SET_UNIT:New():FilterPrefixes( "Helicopter" ):FilterStart()
  --
  --        -- Put a Dead event handler on SetCarrier, to ensure that when a carrier unit is destroyed, that all internal parameters are reset.
  --
  --        function SetHelicopter:OnAfterDead( From, Event, To, UnitObject )
  --          --self:F( { UnitObject = UnitObject:GetName() } )
  --        end
  --
  -- While this is a good example, there is a catch.
  -- Imagine you want to execute the code above, the the self would need to be from the object declared outside (above) the OnAfterDead method.
  -- So, the self would need to contain another object. Fortunately, this can be done, but you must use then the **`.`** notation for the method.
  -- See the modified example:
  --
  --        -- Now we have a constructor of the class AI_CARGO_DISPATCHER, that receives the SetHelicopter as a parameter.
  --        -- Within that constructor, we want to set an enclosed event handler OnAfterDead for SetHelicopter.
  --        -- But within the OnAfterDead method, we want to refer to the self variable of the AI_CARGO_DISPATCHER.
  --
  --        function ACLASS:New( SetCarrier, SetCargo, SetDeployZones )
  --
  --          local self = BASE:Inherit( self, FSM:New() ) -- #AI_CARGO_DISPATCHER
  --
  --          -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset.
  --          -- Note the "." notation, and the explicit declaration of SetHelicopter, which would be using the ":" notation the implicit self variable declaration.
  --
  --          function SetHelicopter.OnAfterDead( SetHelicopter, From, Event, To, UnitObject )
  --            SetHelicopter:F( { UnitObject = UnitObject:GetName() } )
  --            self.array[UnitObject] = nil  -- So here I clear the array table entry of the self object ACLASS.
  --          end
  --
  --        end
  -- ===
  -- @field #SET_UNIT SET_UNIT
  SET_UNIT = {
    ClassName = "SET_UNIT",
    Units = {},
    Filter = {
      Coalitions = nil,
      Categories = nil,
      Types = nil,
      Countries = nil,
      UnitPrefixes = nil,
      Zones = nil,
      Functions = nil,
    },
    FilterMeta = {
      Coalitions = {
        red = coalition.side.RED,
        blue = coalition.side.BLUE,
        neutral = coalition.side.NEUTRAL,
      },
      Categories = {
        plane = Unit.Category.AIRPLANE,
        helicopter = Unit.Category.HELICOPTER,
        ground = Unit.Category.GROUND_UNIT,
        ship = Unit.Category.SHIP,
        structure = Unit.Category.STRUCTURE,
      },
    },
  }

  --- Get the first unit from the set.
  -- @function [parent=#SET_UNIT] GetFirst
  -- @param #SET_UNIT self
  -- @return Wrapper.Unit#UNIT The UNIT object.

  --- Creates a new SET_UNIT object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names.
  -- @param #SET_UNIT self
  -- @return #SET_UNIT
  -- @usage
  -- -- Define a new SET_UNIT Object. This DBObject will contain a reference to all alive Units.
  -- DBObject = SET_UNIT:New()
  function SET_UNIT:New()

    -- Inherits from BASE
    local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.UNITS ) ) -- #SET_UNIT

    self:FilterActive( false )
    
    --- Count Alive Units
    -- @function [parent=#SET_UNIT] CountAlive
    -- @param #SET_UNIT self
    -- @return #SET_UNIT self
    
    return self
  end

  --- Add UNIT(s) to SET_UNIT.
  -- @param #SET_UNIT self
  -- @param Wrapper.Unit#UNIT Unit A single UNIT.
  -- @return #SET_UNIT self
  function SET_UNIT:AddUnit( Unit )
    --self:F2( Unit:GetName() )

    self:Add( Unit:GetName(), Unit )
    
    if Unit:IsInstanceOf("UNIT") then
      -- Set the default cargo bay limit each time a new unit is added to the set.
      Unit:SetCargoBayWeightLimit()
    end
    
    return self
  end

  --- Add UNIT(s) to SET_UNIT.
  -- @param #SET_UNIT self
  -- @param #string AddUnitNames A single name or an array of UNIT names.
  -- @return #SET_UNIT self
  function SET_UNIT:AddUnitsByName( AddUnitNames )

    local AddUnitNamesArray = (type( AddUnitNames ) == "table") and AddUnitNames or { AddUnitNames }

    --self:T( AddUnitNamesArray )
    for AddUnitID, AddUnitName in pairs( AddUnitNamesArray ) do
      self:Add( AddUnitName, UNIT:FindByName( AddUnitName ) )
    end

    return self
  end

  --- Remove UNIT(s) from SET_UNIT.
  -- @param Core.Set#SET_UNIT self
  -- @param #table RemoveUnitNames A single name or an array of UNIT names.
  -- @return Core.Set#SET_UNIT self
  function SET_UNIT:RemoveUnitsByName( RemoveUnitNames )

    local RemoveUnitNamesArray = (type( RemoveUnitNames ) == "table") and RemoveUnitNames or { RemoveUnitNames }

    for RemoveUnitID, RemoveUnitName in pairs( RemoveUnitNamesArray ) do
      self:Remove( RemoveUnitName )
    end

    return self
  end

  --- Finds a Unit based on the Unit Name.
  -- @param #SET_UNIT self
  -- @param #string UnitName
  -- @return Wrapper.Unit#UNIT The found Unit.
  function SET_UNIT:FindUnit( UnitName )

    local UnitFound = self.Set[UnitName]
    return UnitFound
  end

  --- Builds a set of units of coalitions.
  -- Possible current coalitions are red, blue and neutral.
  -- @param #SET_UNIT self
  -- @param #string Coalitions Can take the following values: "red", "blue", "neutral".
  -- @return #SET_UNIT self
  function SET_UNIT:FilterCoalitions( Coalitions )

    self.Filter.Coalitions = {}
    if type( Coalitions ) ~= "table" then
      Coalitions = { Coalitions }
    end
    
    for CoalitionID, Coalition in pairs( Coalitions ) do
      self.Filter.Coalitions[Coalition] = Coalition
    end
    
    return self
  end

  --- Builds a set of units out of categories.
  -- Possible current categories are plane, helicopter, ground, ship.
  -- @param #SET_UNIT self
  -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship".
  -- @return #SET_UNIT self
  function SET_UNIT:FilterCategories( Categories )
    if not self.Filter.Categories then
      self.Filter.Categories = {}
    end
    if type( Categories ) ~= "table" then
      Categories = { Categories }
    end
    for CategoryID, Category in pairs( Categories ) do
      self.Filter.Categories[Category] = Category
    end
    return self
  end

  --- Builds a set of units of defined unit types.
  -- Possible current types are those types known within DCS world.
  -- @param #SET_UNIT self
  -- @param #string Types Can take those type strings known within DCS world.
  -- @return #SET_UNIT self
  function SET_UNIT:FilterTypes( Types )
    if not self.Filter.Types then
      self.Filter.Types = {}
    end
    if type( Types ) ~= "table" then
      Types = { Types }
    end
    for TypeID, Type in pairs( Types ) do
      self.Filter.Types[Type] = Type
    end
    return self
  end

  --- Builds a set of units of defined countries.
  -- Possible current countries are those known within DCS world.
  -- @param #SET_UNIT self
  -- @param #string Countries Can take those country strings known within DCS world.
  -- @return #SET_UNIT self
  function SET_UNIT:FilterCountries( Countries )
    if not self.Filter.Countries then
      self.Filter.Countries = {}
    end
    if type( Countries ) ~= "table" then
      Countries = { Countries }
    end
    for CountryID, Country in pairs( Countries ) do
      self.Filter.Countries[Country] = Country
    end
    return self
  end

  --- Builds a set of UNITs that contain a given string in their unit name.
  -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all units that **contain** the string. 
  -- @param #SET_UNIT self
  -- @param #string Prefixes The string pattern(s) that needs to be contained in the unit name. Can also be passed as a `#table` of strings.
  -- @return #SET_UNIT self
  function SET_UNIT:FilterPrefixes( Prefixes )
    if not self.Filter.UnitPrefixes then
      self.Filter.UnitPrefixes = {}
    end
    if type( Prefixes ) ~= "table" then
      Prefixes = { Prefixes }
    end
    for PrefixID, Prefix in pairs( Prefixes ) do
      self.Filter.UnitPrefixes[Prefix] = Prefix
    end
    return self
  end
  
  --- Builds a set of units in zones.
  -- @param #SET_UNIT self
  -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE
  -- @return #SET_UNIT self
  function SET_UNIT:FilterZones( Zones )
    if not self.Filter.Zones then
      self.Filter.Zones = {}
    end
    local zones = {}
    if Zones.ClassName and Zones.ClassName == "SET_ZONE" then
      zones = Zones.Set
    elseif type( Zones ) ~= "table" or (type( Zones ) == "table" and Zones.ClassName ) then
      self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!")
      return self     
    else
      zones = Zones
    end
    for _,Zone in pairs( zones ) do
      local zonename = Zone:GetName()
      self.Filter.Zones[zonename] = Zone
    end
    return self
  end
  
  --- Builds a set of units that are only active.
  -- Only the units that are active will be included within the set.
  -- @param #SET_UNIT self
  -- @param #boolean Active (Optional) Include only active units to the set.
  -- Include inactive units if you provide false.
  -- @return #SET_UNIT self
  -- @usage
  --
  -- -- Include only active units to the set.
  -- UnitSet = SET_UNIT:New():FilterActive():FilterStart()
  --
  -- -- Include only active units to the set of the blue coalition, and filter one time.
  -- UnitSet = SET_UNIT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce()
  --
  -- -- Include only active units to the set of the blue coalition, and filter one time.
  -- -- Later, reset to include back inactive units to the set.
  -- UnitSet = SET_UNIT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce()
  -- ... logic ...
  -- UnitSet = SET_UNIT:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce()
  --
  function SET_UNIT:FilterActive( Active )
    Active = Active or not (Active == false)
    self.Filter.Active = Active
    return self
  end
  
  --- Builds a set of units which exist and are alive.
  -- @param #SET_UNIT self
  -- @return #SET_UNIT self
  function SET_UNIT:FilterAlive()
    self:FilterFunction(
      function(unit)
        if unit and unit:IsExist() and unit:IsAlive() then
          return true
        else
          return false
        end
      end
    )
    return self
  end
  
  --- Builds a set of units which belong to groups with certain **group names**.
  -- @param #SET_UNIT self
  -- @param #string Prefixes The (partial) group names to look for. Can be a single string or a table of strings.
  -- @return #SET_UNIT self
  function SET_UNIT:FilterGroupPrefixes(Prefixes)
    if type(Prefixes) == "string" then
      Prefixes = {Prefixes}
    end
    self:FilterFunction(
      function(unit,prefixes)
        local outcome = false
        if unit then
          local grp = unit:GetGroup()
          local gname = grp ~= nil and grp:GetName() or "none"
          for _,_fix in pairs(prefixes or {}) do
            if string.find(gname,_fix) then
              outcome = true
              break
            end
          end
        else
          return false
        end
        return outcome
      end, Prefixes
    )
    return self
  end

  --- Builds a set of units having a radar of give types.
  -- All the units having a radar of a given type will be included within the set.
  -- @param #SET_UNIT self
  -- @param #table RadarTypes The radar types.
  -- @return #SET_UNIT self
  function SET_UNIT:FilterHasRadar( RadarTypes )

    self.Filter.RadarTypes = self.Filter.RadarTypes or {}
    if type( RadarTypes ) ~= "table" then
      RadarTypes = { RadarTypes }
    end
    for RadarTypeID, RadarType in pairs( RadarTypes ) do
      self.Filter.RadarTypes[RadarType] = RadarType
    end
    return self
  end

  --- Builds a set of SEADable units.
  -- @param #SET_UNIT self
  -- @return #SET_UNIT self
  function SET_UNIT:FilterHasSEAD()

    self.Filter.SEAD = true
    return self
  end

  --- Iterate the SET_UNIT and count how many UNITs are alive.
  -- @param #SET_UNIT self
  -- @return #number The number of UNITs alive.
  function SET_UNIT:CountAlive()

    local Set = self:GetSet()

    local CountU = 0
    for UnitID, UnitData in pairs( Set ) do -- For each GROUP in SET_GROUP
      if UnitData and UnitData:IsAlive() then
        CountU = CountU + 1
      end

    end

    return CountU
  end
  
  --- Gets the alive set.
  -- @param #SET_UNIT self
  -- @return #table Table of alive UNIT objects
  -- @return #SET_UNIT AliveSet 
  function SET_UNIT:GetAliveSet()

    local AliveSet = SET_UNIT:New()

    -- Clean the Set before returning with only the alive Groups.
    for GroupName, GroupObject in pairs(self.Set) do       
      if GroupObject and GroupObject:IsAlive() then      
        AliveSet[GroupName] = GroupObject
      end
    end

    return AliveSet.Set or {}, AliveSet
  end
  
  --- [Internal] Private function for use of continous zone filter
  -- @param #SET_UNIT self
  -- @return #SET_UNIT self
  function SET_UNIT:_ContinousZoneFilter()
    
    local Database = _DATABASE.UNITS
    
    for ObjectName, Object in pairs( Database ) do
      if self:IsIncludeObject( Object ) and self:IsNotInSet(Object) then
        self:Add( ObjectName, Object )
      elseif (not self:IsIncludeObject( Object )) and self:IsInSet(Object) then
        self:Remove(ObjectName)
      end
    end
    
    return self
    
  end
  
  --- Set filter timer interval for FilterZones if using active filtering with FilterStart().
  -- @param #SET_UNIT self
  -- @param #number Seconds Seconds between check intervals, defaults to 30. **Caution** - do not be too agressive with timing! Groups are usually not moving fast enough
  -- to warrant a check of below 10 seconds.
  -- @return #SET_UNIT self
  function SET_UNIT:FilterZoneTimer(Seconds)
    self.ZoneTimerInterval = Seconds or 30
    return self
  end
  
  --- Stops the filtering.
  -- @param #SET_UNIT self
  -- @return #SET_UNIT self
  function SET_UNIT:FilterStop()

    if _DATABASE then
      
      self:UnHandleEvent(EVENTS.Birth)
      self:UnHandleEvent(EVENTS.Dead)
      self:UnHandleEvent(EVENTS.Crash)
      self:UnHandleEvent(EVENTS.RemoveUnit)
      
      if self.Filter.Zones and self.ZoneTimer and self.ZoneTimer:IsRunning() then
        self.ZoneTimer:Stop()
      end
    end

    return self
  end
  
  --- Starts the filtering.
  -- @param #SET_UNIT self
  -- @return #SET_UNIT self
  function SET_UNIT:FilterStart()

    if _DATABASE then
      self:_FilterStart()
      self:HandleEvent( EVENTS.Birth, self._EventOnBirth )
      self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.UnitLost, self._EventOnDeadOrCrash )
      if self.Filter.Zones then
        self.ZoneTimer = TIMER:New(self._ContinousZoneFilter,self)
        local timing = self.ZoneTimerInterval or 30
        self.ZoneTimer:Start(timing,timing)
      end
    end

    return self
  end

  --- [User] Add a custom condition function.
  -- @function [parent=#SET_UNIT] FilterFunction
  -- @param #SET_UNIT self
  -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a UNIT object as first argument.
  -- @param ... Condition function arguments if any.
  -- @return #SET_UNIT self
  -- @usage
  --          -- Image you want to exclude a specific UNIT from a SET:
  --          local groundset = SET_UNIT:New():FilterCoalitions("blue"):FilterCategories("ground"):FilterFunction(
  --          -- The function needs to take a UNIT object as first - and in this case, only - argument.
  --          function(unit)
  --              local isinclude = true
  --              if unit:GetName() == "Exclude Me" then isinclude = false end
  --              return isinclude
  --          end
  --          ):FilterOnce()
  --          BASE:I(groundset:Flush())


  --- Handles the Database to check on an event (birth) that the Object was added in the Database.
  -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event!
  -- @param #SET_UNIT self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the UNIT
  -- @return #table The UNIT
  function SET_UNIT:AddInDatabase( Event )
    --self:F3( { Event } )

    if Event.IniObjectCategory == Object.Category.UNIT then
      if not self.Database[Event.IniDCSUnitName] then
        self.Database[Event.IniDCSUnitName] = UNIT:Register( Event.IniDCSUnitName )
        --self:T3( self.Database[Event.IniDCSUnitName] )
      end
    end

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Handles the Database to check on any event that Object exists in the Database.
  -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
  -- @param #SET_UNIT self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the UNIT
  -- @return #table The UNIT
  function SET_UNIT:FindInDatabase( Event )
    --self:F2( { Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event } )

    return Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName]
  end

  do -- Is Zone methods

    --- Check if minimal one element of the SET_UNIT is in the Zone.
    -- @param #SET_UNIT self
    -- @param Core.Zone#ZONE ZoneTest The Zone to be tested for.
    -- @return #boolean
    function SET_UNIT:IsPartiallyInZone( ZoneTest )

      local IsPartiallyInZone = false

      local function EvaluateZone( ZoneUnit )

        local ZoneUnitName = ZoneUnit:GetName()
        --self:F( { ZoneUnitName = ZoneUnitName } )
        if self:FindUnit( ZoneUnitName ) then
          IsPartiallyInZone = true
          --self:F( { Found = true } )
          return false
        end

        return true
      end

      ZoneTest:SearchZone( EvaluateZone )

      return IsPartiallyInZone
    end

    --- Check if no element of the SET_UNIT is in the Zone.
    -- @param #SET_UNIT self
    -- @param Core.Zone#ZONE Zone The Zone to be tested for.
    -- @return #boolean
    function SET_UNIT:IsNotInZone( Zone )

      local IsNotInZone = true

      local function EvaluateZone( ZoneUnit )

        local ZoneUnitName = ZoneUnit:GetName()
        if self:FindUnit( ZoneUnitName ) then
          IsNotInZone = false
          return false
        end

        return true
      end

      Zone:SearchZone( EvaluateZone )

      return IsNotInZone
    end

  end


  --- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters.
  -- @param #SET_UNIT self
  -- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter.
  -- @return #SET_UNIT self
  function SET_UNIT:ForEachUnit( IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet() )

    return self
  end

  --- Get the SET of the SET_UNIT **sorted per Threat Level**.
  --
  -- @param #SET_UNIT self
  -- @param #number FromThreatLevel The TreatLevel to start the evaluation **From** (this must be a value between 0 and 10).
  -- @param #number ToThreatLevel The TreatLevel to stop the evaluation **To** (this must be a value between 0 and 10).
  -- @return #SET_UNIT self
  function SET_UNIT:GetSetPerThreatLevel( FromThreatLevel, ToThreatLevel )
    --self:F2( arg )

    local ThreatLevelSet = {}

    if self:Count() ~= 0 then
      for UnitName, UnitObject in pairs( self.Set ) do
        local Unit = UnitObject -- Wrapper.Unit#UNIT

        local ThreatLevel = Unit:GetThreatLevel()
        ThreatLevelSet[ThreatLevel] = ThreatLevelSet[ThreatLevel] or {}
        ThreatLevelSet[ThreatLevel].Set = ThreatLevelSet[ThreatLevel].Set or {}
        ThreatLevelSet[ThreatLevel].Set[UnitName] = UnitObject
        --self:F( { ThreatLevel = ThreatLevel, ThreatLevelSet = ThreatLevelSet[ThreatLevel].Set } )
      end

      local OrderedPerThreatLevelSet = {}

      local ThreatLevelIncrement = FromThreatLevel <= ToThreatLevel and 1 or -1

      for ThreatLevel = FromThreatLevel, ToThreatLevel, ThreatLevelIncrement do
        --self:F( { ThreatLevel = ThreatLevel } )
        local ThreatLevelItem = ThreatLevelSet[ThreatLevel]
        if ThreatLevelItem then
          for UnitName, UnitObject in pairs( ThreatLevelItem.Set ) do
            table.insert( OrderedPerThreatLevelSet, UnitObject )
          end
        end
      end

      return OrderedPerThreatLevelSet
    end

  end


  --- Iterate the SET_UNIT **sorted *per Threat Level** and call an iterator function for each **alive** UNIT, providing the UNIT and optional parameters.
  --
  -- @param #SET_UNIT self
  -- @param #number FromThreatLevel The TreatLevel to start the evaluation **From** (this must be a value between 0 and 10).
  -- @param #number ToThreatLevel The TreatLevel to stop the evaluation **To** (this must be a value between 0 and 10).
  -- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter.
  -- @return #SET_UNIT self
  -- @usage
  --
  --     UnitSet:ForEachUnitPerThreatLevel( 10, 0,
  --       -- @param Wrapper.Unit#UNIT UnitObject The UNIT object in the UnitSet, that will be passed to the local function for evaluation.
  --       function( UnitObject )
  --         .. logic ..
  --       end
  --     )
  --
  function SET_UNIT:ForEachUnitPerThreatLevel( FromThreatLevel, ToThreatLevel, IteratorFunction, ... ) -- R2.1 Threat Level implementation
    --self:F2( arg )

    local ThreatLevelSet = {}

    if self:Count() ~= 0 then
      for UnitName, UnitObject in pairs( self.Set ) do
        local Unit = UnitObject -- Wrapper.Unit#UNIT

        local ThreatLevel = Unit:GetThreatLevel()
        ThreatLevelSet[ThreatLevel] = ThreatLevelSet[ThreatLevel] or {}
        ThreatLevelSet[ThreatLevel].Set = ThreatLevelSet[ThreatLevel].Set or {}
        ThreatLevelSet[ThreatLevel].Set[UnitName] = UnitObject
        --self:F( { ThreatLevel = ThreatLevel, ThreatLevelSet = ThreatLevelSet[ThreatLevel].Set } )
      end

      local ThreatLevelIncrement = FromThreatLevel <= ToThreatLevel and 1 or -1

      for ThreatLevel = FromThreatLevel, ToThreatLevel, ThreatLevelIncrement do
        --self:F( { ThreatLevel = ThreatLevel } )
        local ThreatLevelItem = ThreatLevelSet[ThreatLevel]
        if ThreatLevelItem then
          self:ForEach( IteratorFunction, arg, ThreatLevelItem.Set )
        end
      end
    end

    return self
  end

  --- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence completely in a @{Core.Zone}, providing the UNIT and optional parameters to the called function.
  -- @param #SET_UNIT self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter.
  -- @return #SET_UNIT self
  function SET_UNIT:ForEachUnitCompletelyInZone( ZoneObject, IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet(),
      -- @param Core.Zone#ZONE_BASE ZoneObject
      -- @param Wrapper.Unit#UNIT UnitObject
      function( ZoneObject, UnitObject )
        if UnitObject:IsInZone( ZoneObject ) then
          return true
        else
          return false
        end
      end, { ZoneObject } )

    return self
  end

  --- Iterate the SET_UNIT and call an iterator function for each **alive** UNIT presence not in a @{Core.Zone}, providing the UNIT and optional parameters to the called function.
  -- @param #SET_UNIT self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @param #function IteratorFunction The function that will be called when there is an alive UNIT in the SET_UNIT. The function needs to accept a UNIT parameter.
  -- @return #SET_UNIT self
  function SET_UNIT:ForEachUnitNotInZone( ZoneObject, IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet(),
      -- @param Core.Zone#ZONE_BASE ZoneObject
      -- @param Wrapper.Unit#UNIT UnitObject
      function( ZoneObject, UnitObject )
        if UnitObject:IsNotInZone( ZoneObject ) then
          return true
        else
          return false
        end
      end, { ZoneObject } )

    return self
  end

  --- Returns map of unit types.
  -- @param #SET_UNIT self
  -- @return #map<#string,#number> A map of the unit types found. The key is the UnitTypeName and the value is the amount of unit types found.
  function SET_UNIT:GetUnitTypes()
    --self:F2()

    local MT = {} -- Message Text
    local UnitTypes = {}

    for UnitID, UnitData in pairs( self:GetSet() ) do
      local TextUnit = UnitData -- Wrapper.Unit#UNIT
      if TextUnit:IsAlive() then
        local UnitType = TextUnit:GetTypeName()

        if not UnitTypes[UnitType] then
          UnitTypes[UnitType] = 1
        else
          UnitTypes[UnitType] = UnitTypes[UnitType] + 1
        end
      end
    end

    for UnitTypeID, UnitType in pairs( UnitTypes ) do
      MT[#MT + 1] = UnitType .. " of " .. UnitTypeID
    end

    return UnitTypes
  end

  --- Returns a comma separated string of the unit types with a count in the  @{Core.Set}.
  -- @param #SET_UNIT self
  -- @return #string The unit types string
  function SET_UNIT:GetUnitTypesText()
    --self:F2()

    local MT = {} -- Message Text
    local UnitTypes = self:GetUnitTypes()

    for UnitTypeID, UnitType in pairs( UnitTypes ) do
      MT[#MT + 1] = UnitType .. " of " .. UnitTypeID
    end

    return table.concat( MT, ", " )
  end

  --- Returns map of unit threat levels.
  -- @param #SET_UNIT self
  -- @return #table.
  function SET_UNIT:GetUnitThreatLevels()
    --self:F2()

    local UnitThreatLevels = {}

    for UnitID, UnitData in pairs( self:GetSet() ) do
      local ThreatUnit = UnitData -- Wrapper.Unit#UNIT
      if ThreatUnit:IsAlive() then
        local UnitThreatLevel, UnitThreatLevelText = ThreatUnit:GetThreatLevel()
        local ThreatUnitName = ThreatUnit:GetName()

        UnitThreatLevels[UnitThreatLevel] = UnitThreatLevels[UnitThreatLevel] or {}
        UnitThreatLevels[UnitThreatLevel].UnitThreatLevelText = UnitThreatLevelText
        UnitThreatLevels[UnitThreatLevel].Units = UnitThreatLevels[UnitThreatLevel].Units or {}
        UnitThreatLevels[UnitThreatLevel].Units[ThreatUnitName] = ThreatUnit
      end
    end

    return UnitThreatLevels
  end

  --- Calculate the maximum A2G threat level of the SET_UNIT.
  -- @param #SET_UNIT self
  -- @return #number The maximum threat level
  function SET_UNIT:CalculateThreatLevelA2G()

    local MaxThreatLevelA2G = 0
    local MaxThreatText = ""
    for UnitName, UnitData in pairs( self:GetSet() ) do
      local ThreatUnit = UnitData -- Wrapper.Unit#UNIT
      local ThreatLevelA2G, ThreatText = ThreatUnit:GetThreatLevel()
      if ThreatLevelA2G > MaxThreatLevelA2G then
        MaxThreatLevelA2G = ThreatLevelA2G
        MaxThreatText = ThreatText
      end
    end

    --self:F( { MaxThreatLevelA2G = MaxThreatLevelA2G, MaxThreatText = MaxThreatText } )
    return MaxThreatLevelA2G, MaxThreatText

  end

  --- Get the center coordinate of the SET_UNIT.
  -- @param #SET_UNIT self
  -- @return Core.Point#COORDINATE The center coordinate of all the units in the set, including heading in degrees and speed in mps in case of moving units.
  function SET_UNIT:GetCoordinate()
    
    local function GetSetVec3(units)
      -- Init.
      local x=0 
      local y=0 
      local z=0 
      local n=0
      -- Loop over all units.
      for _,unit in pairs(units) do
        local vec3=nil --DCS#Vec3
        if unit and unit:IsAlive() then
          vec3 = unit:GetVec3()
        end
        if vec3 then
          -- Sum up posits.
          x=x+vec3.x
          y=y+vec3.y
          z=z+vec3.z
          -- Increase counter.
          n=n+1
        end
      end
      if n>0 then
        -- Average.
        local Vec3={x=x/n, y=y/n, z=z/n} --DCS#Vec3
        return Vec3
      end
      return nil
    end
    
    local Coordinate = nil
    local Vec3 = GetSetVec3(self.Set)
    if Vec3 then
      Coordinate = COORDINATE:NewFromVec3(Vec3)
    end

    if Coordinate then
      local heading = self:GetHeading() or 0
      local velocity = self:GetVelocity() or 0
      Coordinate:SetHeading( heading )
      Coordinate:SetVelocity( velocity )
      --self:T(UTILS.PrintTableToLog(Coordinate))
    end

    return Coordinate
  end

  --- Get the maximum velocity of the SET_UNIT.
  -- @param #SET_UNIT self
  -- @return #number The speed in mps in case of moving units.
  function SET_UNIT:GetVelocity()

    local Coordinate = self:GetFirst():GetCoordinate()

    local MaxVelocity = 0

    for UnitName, UnitData in pairs( self:GetSet() ) do

      local Unit = UnitData -- Wrapper.Unit#UNIT
      local Coordinate = Unit:GetCoordinate()

      local Velocity = Coordinate:GetVelocity()
      if Velocity ~= 0 then
        MaxVelocity = (MaxVelocity < Velocity) and Velocity or MaxVelocity
      end
    end

    --self:F( { MaxVelocity = MaxVelocity } )
    return MaxVelocity

  end

  --- Get the average heading of the SET_UNIT.
  -- @param #SET_UNIT self
  -- @return #number Heading Heading in degrees and speed in mps in case of moving units.
  function SET_UNIT:GetHeading()

    local HeadingSet = nil
    local MovingCount = 0

    for UnitName, UnitData in pairs( self:GetSet() ) do

      local Unit = UnitData -- Wrapper.Unit#UNIT
      local Coordinate = Unit:GetCoordinate()

      local Velocity = Coordinate:GetVelocity()
      if Velocity ~= 0 then
        local Heading = Coordinate:GetHeading()
        if HeadingSet == nil then
          HeadingSet = Heading
        else
          local HeadingDiff = (HeadingSet - Heading + 180 + 360) % 360 - 180
          HeadingDiff = math.abs( HeadingDiff )
          if HeadingDiff > 5 then
            HeadingSet = nil
            break
          end
        end
      end
    end

    return HeadingSet

  end

  --- Returns if the @{Core.Set} has targets having a radar (of a given type).
  -- @param #SET_UNIT self
  -- @param DCS#Unit.RadarType RadarType
  -- @return #number The amount of radars in the Set with the given type
  function SET_UNIT:HasRadar( RadarType )
    --self:F2( RadarType )

    local RadarCount = 0
    for UnitID, UnitData in pairs( self:GetSet() ) do
      local UnitSensorTest = UnitData -- Wrapper.Unit#UNIT
      local HasSensors
      if RadarType then
        HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR, RadarType )
      else
        HasSensors = UnitSensorTest:HasSensors( Unit.SensorType.RADAR )
      end
      --self:T3( HasSensors )
      if HasSensors then
        RadarCount = RadarCount + 1
      end
    end

    return RadarCount
  end

  --- Returns if the @{Core.Set} has targets that can be SEADed.
  -- @param #SET_UNIT self
  -- @return #number The amount of SEADable units in the Set
  function SET_UNIT:HasSEAD()
    --self:F2()

    local SEADCount = 0
    for UnitID, UnitData in pairs( self:GetSet() ) do
      local UnitSEAD = UnitData -- Wrapper.Unit#UNIT
      if UnitSEAD:IsAlive() then
        local UnitSEADAttributes = UnitSEAD:GetDesc().attributes

        local HasSEAD = UnitSEAD:HasSEAD()

        --self:T3( HasSEAD )
        if HasSEAD then
          SEADCount = SEADCount + 1
        end
      end
    end

    return SEADCount
  end

  --- Returns if the @{Core.Set} has ground targets.
  -- @param #SET_UNIT self
  -- @return #number The amount of ground targets in the Set.
  function SET_UNIT:HasGroundUnits()
    --self:F2()

    local GroundUnitCount = 0
    for UnitID, UnitData in pairs( self:GetSet() ) do
      local UnitTest = UnitData -- Wrapper.Unit#UNIT
      if UnitTest:IsGround() then
        GroundUnitCount = GroundUnitCount + 1
      end
    end

    return GroundUnitCount
  end

  --- Returns if the @{Core.Set} has air targets.
  -- @param #SET_UNIT self
  -- @return #number The amount of air targets in the Set.
  function SET_UNIT:HasAirUnits()
    --self:F2()

    local AirUnitCount = 0
    for UnitID, UnitData in pairs( self:GetSet() ) do
      local UnitTest = UnitData -- Wrapper.Unit#UNIT
      if UnitTest:IsAir() then
        AirUnitCount = AirUnitCount + 1
      end
    end

    return AirUnitCount
  end

  --- Returns if the @{Core.Set} has friendly ground units.
  -- @param #SET_UNIT self
  -- @return #number The amount of ground targets in the Set.
  function SET_UNIT:HasFriendlyUnits( FriendlyCoalition )
    --self:F2()

    local FriendlyUnitCount = 0
    for UnitID, UnitData in pairs( self:GetSet() ) do
      local UnitTest = UnitData -- Wrapper.Unit#UNIT
      if UnitTest:IsFriendly( FriendlyCoalition ) then
        FriendlyUnitCount = FriendlyUnitCount + 1
      end
    end

    return FriendlyUnitCount
  end



  ----- Iterate the SET_UNIT and call an iterator function for each **alive** player, providing the Unit of the player and optional parameters.
  -- @param #SET_UNIT self
  -- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a UNIT parameter.
  ---- @return #SET_UNIT self
  -- function SET_UNIT:ForEachPlayer( IteratorFunction, ... )
  --  --self:F2( arg )
  --
  --  self:ForEach( IteratorFunction, arg, self.PlayersAlive )
  --
  --  return self
  -- end
  --
  --
  ----- Iterate the SET_UNIT and call an iterator function for each client, providing the Client to the function and optional parameters.
  -- @param #SET_UNIT self
  -- @param #function IteratorFunction The function that will be called when there is an alive player in the SET_UNIT. The function needs to accept a CLIENT parameter.
  ---- @return #SET_UNIT self
  -- function SET_UNIT:ForEachClient( IteratorFunction, ... )
  --  --self:F2( arg )
  --
  --  self:ForEach( IteratorFunction, arg, self.Clients )
  --
  --  return self
  -- end

  ---
  -- @param #SET_UNIT self
  -- @param Wrapper.Unit#UNIT MUnit
  -- @return #SET_UNIT self
  function SET_UNIT:IsIncludeObject( MUnit )
    --self:F2( {MUnit} )

    local MUnitInclude = false

    if MUnit:IsAlive() ~= nil then

      MUnitInclude = true

      if self.Filter.Active ~= nil then
        local MUnitActive = false
        if self.Filter.Active == false or (self.Filter.Active == true and MUnit:IsActive() == true) then
          MUnitActive = true
        end
        MUnitInclude = MUnitInclude and MUnitActive
      end

      if self.Filter.Coalitions and MUnitInclude then
        local MUnitCoalition = false
        for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do
          --self:F( { "Coalition:", MUnit:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } )
          if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MUnit:GetCoalition() then
            MUnitCoalition = true
          end
        end
        MUnitInclude = MUnitInclude and MUnitCoalition
      end

      if self.Filter.Categories and MUnitInclude then
        local MUnitCategory = false
        for CategoryID, CategoryName in pairs( self.Filter.Categories ) do
          --self:T3( { "Category:", MUnit:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } )
          if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MUnit:GetDesc().category then
            MUnitCategory = true
          end
        end
        MUnitInclude = MUnitInclude and MUnitCategory
      end

      if self.Filter.Types and MUnitInclude then
        local MUnitType = false
        for TypeID, TypeName in pairs( self.Filter.Types ) do
          --self:T3( { "Type:", MUnit:GetTypeName(), TypeName } )
          if TypeName == MUnit:GetTypeName() then
            MUnitType = true
          end
        end
        MUnitInclude = MUnitInclude and MUnitType
      end

      if self.Filter.Countries and MUnitInclude then
        local MUnitCountry = false
        for CountryID, CountryName in pairs( self.Filter.Countries ) do
          --self:T3( { "Country:", MUnit:GetCountry(), CountryName } )
          if country.id[CountryName] == MUnit:GetCountry() then
            MUnitCountry = true
          end
        end
        MUnitInclude = MUnitInclude and MUnitCountry
      end

      if self.Filter.UnitPrefixes and MUnitInclude then
        local MUnitPrefix = false
        for UnitPrefixId, UnitPrefix in pairs( self.Filter.UnitPrefixes ) do
          --self:T3( { "Prefix:", string.find( MUnit:GetName(), UnitPrefix, 1 ), UnitPrefix } )
          if string.find( MUnit:GetName(), UnitPrefix, 1 ) then
            MUnitPrefix = true
          end
        end
        MUnitInclude = MUnitInclude and MUnitPrefix
      end

      if self.Filter.RadarTypes and MUnitInclude then
        local MUnitRadar = false
        for RadarTypeID, RadarType in pairs( self.Filter.RadarTypes ) do
          --self:T3( { "Radar:", RadarType } )
          if MUnit:HasSensors( Unit.SensorType.RADAR, RadarType ) == true then
            if MUnit:GetRadar() == true then -- This call is necessary to evaluate the SEAD capability.
              --self:T3( "RADAR Found" )
            end
            MUnitRadar = true
          end
        end
        MUnitInclude = MUnitInclude and MUnitRadar
      end

      if self.Filter.SEAD and MUnitInclude then
        local MUnitSEAD = false
        if MUnit:HasSEAD() == true then
          --self:T3( "SEAD Found" )
          MUnitSEAD = true
        end
        MUnitInclude = MUnitInclude and MUnitSEAD
      end
    end
    
    if self.Filter.Zones and MUnitInclude then
      local MGroupZone = false
      for ZoneName, Zone in pairs( self.Filter.Zones ) do
        --self:T3( "Zone:", ZoneName )
        if MUnit:IsInZone(Zone) then
          MGroupZone = true
        end
      end
      MUnitInclude = MUnitInclude  and MGroupZone
    end
    
    if self.Filter.Functions and MUnitInclude then
      local MUnitFunc = self:_EvalFilterFunctions(MUnit)
      MUnitInclude = MUnitInclude  and MUnitFunc
    end
    
    --self:T2( MUnitInclude )
    return MUnitInclude
  end

  --- Retrieve the type names of the @{Wrapper.Unit}s in the SET, delimited by an optional delimiter.
  -- @param #SET_UNIT self
  -- @param #string Delimiter (Optional) The delimiter, which is default a comma.
  -- @return #string The types of the @{Wrapper.Unit}s delimited.
  function SET_UNIT:GetTypeNames( Delimiter )

    Delimiter = Delimiter or ", "
    local TypeReport = REPORT:New()
    local Types = {}

    for UnitName, UnitData in pairs( self:GetSet() ) do

      local Unit = UnitData -- Wrapper.Unit#UNIT
      local UnitTypeName = Unit:GetTypeName()

      if not Types[UnitTypeName] then
        Types[UnitTypeName] = UnitTypeName
        TypeReport:Add( UnitTypeName )
      end
    end

    return TypeReport:Text( Delimiter )
  end

  --- Iterate the SET_UNIT and set for each unit the default cargo bay weight limit.
  -- @param #SET_UNIT self
  -- @usage
  -- -- Set the default cargo bay weight limits of the carrier units.
  -- local MySetUnit = SET_UNIT:New()
  -- MySetUnit:SetCargoBayWeightLimit()
  function SET_UNIT:SetCargoBayWeightLimit()
    local Set = self:GetSet()
    for UnitID, UnitData in pairs( Set ) do -- For each UNIT in SET_UNIT
      -- local UnitData = UnitData -- Wrapper.Unit#UNIT
      UnitData:SetCargoBayWeightLimit()
    end
  end

end

do -- SET_STATIC
  
  ---
  -- @type SET_STATIC
  -- @extends Core.Set#SET_BASE

  --- Mission designers can use the SET_STATIC class to build sets of Statics belonging to certain:
  --
  --  * Coalitions
  --  * Categories
  --  * Countries
  --  * Static types
  --  * Starting with certain prefix strings.
  --
  -- ## SET_STATIC constructor
  --
  -- Create a new SET_STATIC object with the @{#SET_STATIC.New} method:
  --
  --    * @{#SET_STATIC.New}: Creates a new SET_STATIC object.
  --
  -- ## Add or Remove STATIC(s) from SET_STATIC
  --
  -- STATICs can be added and removed using the @{Core.Set#SET_STATIC.AddStaticsByName} and @{Core.Set#SET_STATIC.RemoveStaticsByName} respectively.
  -- These methods take a single STATIC name or an array of STATIC names to be added or removed from SET_STATIC.
  --
  -- ## SET_STATIC filter criteria
  --
  -- You can set filter criteria to define the set of units within the SET_STATIC.
  -- Filter criteria are defined by:
  --
  --    * @{#SET_STATIC.FilterCoalitions}: Builds the SET_STATIC with the units belonging to the coalition(s).
  --    * @{#SET_STATIC.FilterCategories}: Builds the SET_STATIC with the units belonging to the category(ies).
  --    * @{#SET_STATIC.FilterTypes}: Builds the SET_STATIC with the units belonging to the unit type(s).
  --    * @{#SET_STATIC.FilterCountries}: Builds the SET_STATIC with the units belonging to the country(ies).
  --    * @{#SET_STATIC.FilterPrefixes}: Builds the SET_STATIC with the units containing the same string(s) in their name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. 
  -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching)
  --    * @{#SET_STATIC.FilterZones}: Builds the SET_STATIC with the units within a @{Core.Zone#ZONE}.
  --    * @{#SET_STATIC.FilterFunction}: Builds the SET_STATIC with a custom condition.
  --    
  -- Once the filter criteria have been set for the SET_STATIC, you can start filtering using:
  --
  --   * @{#SET_STATIC.FilterStart}: Starts the filtering of the units within the SET_STATIC.
  --
  -- ## SET_STATIC iterators
  --
  -- Once the filters have been defined and the SET_STATIC has been built, you can iterate the SET_STATIC with the available iterator methods.
  -- The iterator methods will walk the SET_STATIC set, and call for each element within the set a function that you provide.
  -- The following iterator methods are currently available within the SET_STATIC:
  --
  --   * @{#SET_STATIC.ForEachStatic}: Calls a function for each alive unit it finds within the SET_STATIC.
  --   * @{#SET_STATIC.ForEachStaticCompletelyInZone}: Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence completely in a @{Core.Zone}, providing the STATIC and optional parameters to the called function.
  --   * @{#SET_STATIC.ForEachStaticInZone}: Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence completely in a @{Core.Zone}, providing the STATIC and optional parameters to the called function.
  --   * @{#SET_STATIC.ForEachStaticNotInZone}: Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence not in a @{Core.Zone}, providing the STATIC and optional parameters to the called function.
  --
  -- ## SET_STATIC atomic methods
  --
  -- Various methods exist for a SET_STATIC to perform actions or calculations and retrieve results from the SET_STATIC:
  --
  --   * @{#SET_STATIC.GetTypeNames}(): Retrieve the type names of the @{Wrapper.Static}s in the SET, delimited by a comma.
  --
  -- ===
  -- @field #SET_STATIC SET_STATIC
  SET_STATIC = {
    ClassName = "SET_STATIC",
    Statics = {},
    Filter = {
      Coalitions = nil,
      Categories = nil,
      Types = nil,
      Countries = nil,
      StaticPrefixes = nil,
      Zones = nil,
    },
    FilterMeta = {
      Coalitions = {
        red = coalition.side.RED,
        blue = coalition.side.BLUE,
        neutral = coalition.side.NEUTRAL,
      },
      Categories = {
        plane = Unit.Category.AIRPLANE,
        helicopter = Unit.Category.HELICOPTER,
        ground = Unit.Category.GROUND_STATIC,
        ship = Unit.Category.SHIP,
        structure = Unit.Category.STRUCTURE,
      },
    },
  }


  --- Get the first unit from the set.
  -- @function [parent=#SET_STATIC] GetFirst
  -- @param #SET_STATIC self
  -- @return Wrapper.Static#STATIC The STATIC object.

  --- Creates a new SET_STATIC object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names.
  -- @param #SET_STATIC self
  -- @return #SET_STATIC
  -- @usage
  -- -- Define a new SET_STATIC Object. This DBObject will contain a reference to all alive Statics.
  -- DBObject = SET_STATIC:New()
  function SET_STATIC:New()

    -- Inherits from BASE
    local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.STATICS ) ) -- Core.Set#SET_STATIC

    return self
  end

  --- Add STATIC(s) to SET_STATIC.
  -- @param #SET_STATIC self
  -- @param Wrapper.Static#STATIC AddStatic A single STATIC.
  -- @return #SET_STATIC self
  function SET_STATIC:AddStatic( AddStatic )
    --self:F2( AddStatic:GetName() )

    self:Add( AddStatic:GetName(), AddStatic )

    return self
  end

  --- Add STATIC(s) to SET_STATIC.
  -- @param #SET_STATIC self
  -- @param #string AddStaticNames A single name or an array of STATIC names.
  -- @return #SET_STATIC self
  function SET_STATIC:AddStaticsByName( AddStaticNames )

    local AddStaticNamesArray = (type( AddStaticNames ) == "table") and AddStaticNames or { AddStaticNames }

    --self:T(( AddStaticNamesArray )
    for AddStaticID, AddStaticName in pairs( AddStaticNamesArray ) do
      self:Add( AddStaticName, STATIC:FindByName( AddStaticName ) )
    end

    return self
  end

  --- Remove STATIC(s) from SET_STATIC.
  -- @param Core.Set#SET_STATIC self
  -- @param Wrapper.Static#STATIC RemoveStaticNames A single name or an array of STATIC names.
  -- @return self
  function SET_STATIC:RemoveStaticsByName( RemoveStaticNames )

    local RemoveStaticNamesArray = (type( RemoveStaticNames ) == "table") and RemoveStaticNames or { RemoveStaticNames }

    for RemoveStaticID, RemoveStaticName in pairs( RemoveStaticNamesArray ) do
      self:Remove( RemoveStaticName )
    end

    return self
  end

  --- Finds a Static based on the Static Name.
  -- @param #SET_STATIC self
  -- @param #string StaticName
  -- @return Wrapper.Static#STATIC The found Static.
  function SET_STATIC:FindStatic( StaticName )

    local StaticFound = self.Set[StaticName]
    return StaticFound
  end

  --- Builds a set of units of coalitions.
  -- Possible current coalitions are red, blue and neutral.
  -- @param #SET_STATIC self
  -- @param #string Coalitions Can take the following values: "red", "blue", "neutral".
  -- @return #SET_STATIC self
  function SET_STATIC:FilterCoalitions( Coalitions )
    if not self.Filter.Coalitions then
      self.Filter.Coalitions = {}
    end
    if type( Coalitions ) ~= "table" then
      Coalitions = { Coalitions }
    end
    for CoalitionID, Coalition in pairs( Coalitions ) do
      self.Filter.Coalitions[Coalition] = Coalition
    end
    return self
  end
  
  
   --- Builds a set of statics in zones.
  -- @param #SET_STATIC self
  -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE
  -- @return #SET_STATIC self
  function SET_STATIC:FilterZones( Zones )
    if not self.Filter.Zones then
      self.Filter.Zones = {}
    end
    local zones = {}
    if Zones.ClassName and Zones.ClassName == "SET_ZONE" then
      zones = Zones.Set
    elseif type( Zones ) ~= "table" or (type( Zones ) == "table" and Zones.ClassName ) then
      self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!")
      return self     
    else
      zones = Zones
    end
    for _,Zone in pairs( zones ) do
      local zonename = Zone:GetName()
      self.Filter.Zones[zonename] = Zone
    end
    return self
  end

  --- Builds a set of units out of categories.
  -- Possible current categories are plane, helicopter, ground, ship.
  -- @param #SET_STATIC self
  -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship".
  -- @return #SET_STATIC self
  function SET_STATIC:FilterCategories( Categories )
    if not self.Filter.Categories then
      self.Filter.Categories = {}
    end
    if type( Categories ) ~= "table" then
      Categories = { Categories }
    end
    for CategoryID, Category in pairs( Categories ) do
      self.Filter.Categories[Category] = Category
    end
    return self
  end

  --- Builds a set of units of defined unit types.
  -- Possible current types are those types known within DCS world.
  -- @param #SET_STATIC self
  -- @param #string Types Can take those type strings known within DCS world.
  -- @return #SET_STATIC self
  function SET_STATIC:FilterTypes( Types )
    if not self.Filter.Types then
      self.Filter.Types = {}
    end
    if type( Types ) ~= "table" then
      Types = { Types }
    end
    for TypeID, Type in pairs( Types ) do
      self.Filter.Types[Type] = Type
    end
    return self
  end
  
  --- [User] Add a custom condition function.
  -- @function [parent=#SET_STATIC] FilterFunction
  -- @param #SET_STATIC self
  -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a STATIC object as first argument.
  -- @param ... Condition function arguments if any.
  -- @return #SET_STATIC self
  -- @usage
  --          -- Image you want to exclude a specific CLIENT from a SET:
  --          local groundset = SET_STATIC:New():FilterCoalitions("blue"):FilterActive(true):FilterFunction(
  --          -- The function needs to take a STATIC object as first - and in this case, only - argument.
  --          function(static)
  --              local isinclude = true
  --              if static:GetName() == "Exclude Me" then isinclude = false end
  --              return isinclude
  --          end
  --          ):FilterOnce()
  --          BASE:I(groundset:Flush())
  
  --- Builds a set of units of defined countries.
  -- Possible current countries are those known within DCS world.
  -- @param #SET_STATIC self
  -- @param #string Countries Can take those country strings known within DCS world.
  -- @return #SET_STATIC self
  function SET_STATIC:FilterCountries( Countries )
    if not self.Filter.Countries then
      self.Filter.Countries = {}
    end
    if type( Countries ) ~= "table" then
      Countries = { Countries }
    end
    for CountryID, Country in pairs( Countries ) do
      self.Filter.Countries[Country] = Country
    end
    return self
  end

  --- Builds a set of STATICs that contain the given string in their name.
  -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all statics that **contain** the string. 
  -- @param #SET_STATIC self
  -- @param #string Prefixes The string pattern(s) that need to be contained in the static name. Can also be passed as a `#table` of strings.
  -- @return #SET_STATIC self
  function SET_STATIC:FilterPrefixes( Prefixes )
    if not self.Filter.StaticPrefixes then
      self.Filter.StaticPrefixes = {}
    end
    if type( Prefixes ) ~= "table" then
      Prefixes = { Prefixes }
    end
    for PrefixID, Prefix in pairs( Prefixes ) do
      self.Filter.StaticPrefixes[Prefix] = Prefix
    end
    return self
  end

  --- Starts the filtering.
  -- @param #SET_STATIC self
  -- @return #SET_STATIC self
  function SET_STATIC:FilterStart()

    if _DATABASE then
      self:_FilterStart()
      self:HandleEvent( EVENTS.Birth, self._EventOnBirth )
      self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.UnitLost, self._EventOnDeadOrCrash )
    end

    return self
  end

  --- Iterate the SET_STATIC and count how many STATICSs are alive.
  -- @param #SET_STATIC self
  -- @return #number The number of UNITs alive.
  function SET_STATIC:CountAlive()

    local Set = self:GetSet()

    local CountU = 0
    for UnitID, UnitData in pairs( Set ) do
      if UnitData and UnitData:IsAlive() then
        CountU = CountU + 1
      end

    end

    return CountU
  end

  --- Handles the Database to check on an event (birth) that the Object was added in the Database.
  -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event!
  -- @param #SET_STATIC self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the STATIC
  -- @return #table The STATIC
  function SET_STATIC:AddInDatabase( Event )
    --self:F3( { Event } )

    if Event.IniObjectCategory == Object.Category.STATIC then
      if not self.Database[Event.IniDCSUnitName] then
        self.Database[Event.IniDCSUnitName] = STATIC:Register( Event.IniDCSUnitName )
        --self:T(3( self.Database[Event.IniDCSUnitName] )
      end
    end

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Handles the Database to check on any event that Object exists in the Database.
  -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
  -- @param #SET_STATIC self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the STATIC
  -- @return #table The STATIC
  function SET_STATIC:FindInDatabase( Event )
    --self:F2( { Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName], Event } )

    return Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName]
  end

  do -- Is Zone methods

    --- Check if minimal one element of the SET_STATIC is in the Zone.
    -- @param #SET_STATIC self
    -- @param Core.Zone#ZONE Zone The Zone to be tested for.
    -- @return #boolean
    function SET_STATIC:IsPartiallyInZone( Zone )

      local IsPartiallyInZone = false

      local function EvaluateZone( ZoneStatic )

        local ZoneStaticName = ZoneStatic:GetName()
        if self:FindStatic( ZoneStaticName ) then
          IsPartiallyInZone = true
          return false
        end

        return true
      end

      return IsPartiallyInZone
    end

    --- Check if no element of the SET_STATIC is in the Zone.
    -- @param #SET_STATIC self
    -- @param Core.Zone#ZONE Zone The Zone to be tested for.
    -- @return #boolean
    function SET_STATIC:IsNotInZone( Zone )

      local IsNotInZone = true

      local function EvaluateZone( ZoneStatic )

        local ZoneStaticName = ZoneStatic:GetName()
        if self:FindStatic( ZoneStaticName ) then
          IsNotInZone = false
          return false
        end

        return true
      end

      Zone:Search( EvaluateZone )

      return IsNotInZone
    end

    --- Check if minimal one element of the SET_STATIC is in the Zone.
    -- @param #SET_STATIC self
    -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter.
    -- @return #SET_STATIC self
    function SET_STATIC:ForEachStaticInZone( IteratorFunction, ... )
      --self:F2( arg )

      self:ForEach( IteratorFunction, arg, self:GetSet() )

      return self
    end

  end


  --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC, providing the STATIC and optional parameters.
  -- @param #SET_STATIC self
  -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter.
  -- @return #SET_STATIC self
  function SET_STATIC:ForEachStatic( IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet() )

    return self
  end

  --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence completely in a @{Core.Zone}, providing the STATIC and optional parameters to the called function.
  -- @param #SET_STATIC self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter.
  -- @return #SET_STATIC self
  function SET_STATIC:ForEachStaticCompletelyInZone( ZoneObject, IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet(),
      -- @param Core.Zone#ZONE_BASE ZoneObject
      -- @param Wrapper.Static#STATIC StaticObject
      function( ZoneObject, StaticObject )
        if StaticObject:IsInZone( ZoneObject ) then
          return true
        else
          return false
        end
      end, { ZoneObject } )

    return self
  end

  --- Iterate the SET_STATIC and call an iterator function for each **alive** STATIC presence not in a @{Core.Zone}, providing the STATIC and optional parameters to the called function.
  -- @param #SET_STATIC self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @param #function IteratorFunction The function that will be called when there is an alive STATIC in the SET_STATIC. The function needs to accept a STATIC parameter.
  -- @return #SET_STATIC self
  function SET_STATIC:ForEachStaticNotInZone( ZoneObject, IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet(),
      -- @param Core.Zone#ZONE_BASE ZoneObject
      -- @param Wrapper.Static#STATIC StaticObject
      function( ZoneObject, StaticObject )
        if StaticObject:IsNotInZone( ZoneObject ) then
          return true
        else
          return false
        end
      end, { ZoneObject } )

    return self
  end

  --- Returns map of unit types.
  -- @param #SET_STATIC self
  -- @return #map<#string,#number> A map of the unit types found. The key is the StaticTypeName and the value is the amount of unit types found.
  function SET_STATIC:GetStaticTypes()
    --self:F2()

    local MT = {} -- Message Text
    local StaticTypes = {}

    for StaticID, StaticData in pairs( self:GetSet() ) do
      local TextStatic = StaticData -- Wrapper.Static#STATIC
      if TextStatic:IsAlive() then
        local StaticType = TextStatic:GetTypeName()

        if not StaticTypes[StaticType] then
          StaticTypes[StaticType] = 1
        else
          StaticTypes[StaticType] = StaticTypes[StaticType] + 1
        end
      end
    end

    for StaticTypeID, StaticType in pairs( StaticTypes ) do
      MT[#MT + 1] = StaticType .. " of " .. StaticTypeID
    end

    return StaticTypes
  end

  --- Returns a comma separated string of the unit types with a count in the  @{Core.Set}.
  -- @param #SET_STATIC self
  -- @return #string The unit types string
  function SET_STATIC:GetStaticTypesText()
    --self:F2()

    local MT = {} -- Message Text
    local StaticTypes = self:GetStaticTypes()

    for StaticTypeID, StaticType in pairs( StaticTypes ) do
      MT[#MT + 1] = StaticType .. " of " .. StaticTypeID
    end

    return table.concat( MT, ", " )
  end

  --- Get the center coordinate of the SET_STATIC.
  -- @param #SET_STATIC self
  -- @return Core.Point#COORDINATE The center coordinate of all the units in the set, including heading in degrees and speed in mps in case of moving units.
  function SET_STATIC:GetCoordinate()

    local Coordinate = self:GetFirst():GetCoordinate()

    local x1 = Coordinate.x
    local x2 = Coordinate.x
    local y1 = Coordinate.y
    local y2 = Coordinate.y
    local z1 = Coordinate.z
    local z2 = Coordinate.z
    local MaxVelocity = 0
    local AvgHeading = nil
    local MovingCount = 0

    for StaticName, StaticData in pairs( self:GetSet() ) do

      local Static = StaticData -- Wrapper.Static#STATIC
      local Coordinate = Static:GetCoordinate()

      x1 = (Coordinate.x < x1) and Coordinate.x or x1
      x2 = (Coordinate.x > x2) and Coordinate.x or x2
      y1 = (Coordinate.y < y1) and Coordinate.y or y1
      y2 = (Coordinate.y > y2) and Coordinate.y or y2
      z1 = (Coordinate.y < z1) and Coordinate.z or z1
      z2 = (Coordinate.y > z2) and Coordinate.z or z2

      local Velocity = Coordinate:GetVelocity()
      if Velocity ~= 0 then
        MaxVelocity = (MaxVelocity < Velocity) and Velocity or MaxVelocity
        local Heading = Coordinate:GetHeading()
        AvgHeading = AvgHeading and (AvgHeading + Heading) or Heading
        MovingCount = MovingCount + 1
      end
    end

    AvgHeading = AvgHeading and (AvgHeading / MovingCount)

    Coordinate.x = (x2 - x1) / 2 + x1
    Coordinate.y = (y2 - y1) / 2 + y1
    Coordinate.z = (z2 - z1) / 2 + z1
    Coordinate:SetHeading( AvgHeading )
    Coordinate:SetVelocity( MaxVelocity )

    --self:F( { Coordinate = Coordinate } )
    return Coordinate

  end

  --- Get the maximum velocity of the SET_STATIC.
  -- @param #SET_STATIC self
  -- @return #number The speed in mps in case of moving units.
  function SET_STATIC:GetVelocity()

    return 0

  end

  --- Get the average heading of the SET_STATIC.
  -- @param #SET_STATIC self
  -- @return #number Heading Heading in degrees and speed in mps in case of moving units.
  function SET_STATIC:GetHeading()

    local HeadingSet = nil
    local MovingCount = 0

    for StaticName, StaticData in pairs( self:GetSet() ) do

      local Static = StaticData -- Wrapper.Static#STATIC
      local Coordinate = Static:GetCoordinate()

      local Velocity = Coordinate:GetVelocity()
      if Velocity ~= 0 then
        local Heading = Coordinate:GetHeading()
        if HeadingSet == nil then
          HeadingSet = Heading
        else
          local HeadingDiff = (HeadingSet - Heading + 180 + 360) % 360 - 180
          HeadingDiff = math.abs( HeadingDiff )
          if HeadingDiff > 5 then
            HeadingSet = nil
            break
          end
        end
      end
    end

    return HeadingSet

  end

  --- Calculate the maximum A2G threat level of the SET_STATIC.
  -- @param #SET_STATIC self
  -- @return #number The maximum threatlevel
  function SET_STATIC:CalculateThreatLevelA2G()

    local MaxThreatLevelA2G = 0
    local MaxThreatText = ""
    for StaticName, StaticData in pairs( self:GetSet() ) do
      local ThreatStatic = StaticData -- Wrapper.Static#STATIC
      local ThreatLevelA2G, ThreatText = ThreatStatic:GetThreatLevel()
      if ThreatLevelA2G > MaxThreatLevelA2G then
        MaxThreatLevelA2G = ThreatLevelA2G
        MaxThreatText = ThreatText
      end
    end

    --self:F( { MaxThreatLevelA2G = MaxThreatLevelA2G, MaxThreatText = MaxThreatText } )
    return MaxThreatLevelA2G, MaxThreatText

  end

  ---
  -- @param #SET_STATIC self
  -- @param Wrapper.Static#STATIC MStatic
  -- @return #SET_STATIC self
  function SET_STATIC:IsIncludeObject( MStatic )
    --self:F2( MStatic )
    local MStaticInclude = true

    if self.Filter.Coalitions then
      local MStaticCoalition = false
      for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do
        --self:T(3( { "Coalition:", MStatic:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } )
        if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == MStatic:GetCoalition() then
          MStaticCoalition = true
        end
      end
      MStaticInclude = MStaticInclude and MStaticCoalition
    end

    if self.Filter.Categories then
      local MStaticCategory = false
      for CategoryID, CategoryName in pairs( self.Filter.Categories ) do
        --self:T(3( { "Category:", MStatic:GetDesc().category, self.FilterMeta.Categories[CategoryName], CategoryName } )
        if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == MStatic:GetDesc().category then
          MStaticCategory = true
        end
      end
      MStaticInclude = MStaticInclude and MStaticCategory
    end

    if self.Filter.Types then
      local MStaticType = false
      for TypeID, TypeName in pairs( self.Filter.Types ) do
        --self:T(3( { "Type:", MStatic:GetTypeName(), TypeName } )
        if TypeName == MStatic:GetTypeName() then
          MStaticType = true
        end
      end
      MStaticInclude = MStaticInclude and MStaticType
    end

    if self.Filter.Countries then
      local MStaticCountry = false
      for CountryID, CountryName in pairs( self.Filter.Countries ) do
        --self:T(3( { "Country:", MStatic:GetCountry(), CountryName } )
        if country.id[CountryName] == MStatic:GetCountry() then
          MStaticCountry = true
        end
      end
      MStaticInclude = MStaticInclude and MStaticCountry
    end

    if self.Filter.StaticPrefixes then
      local MStaticPrefix = false
      for StaticPrefixId, StaticPrefix in pairs( self.Filter.StaticPrefixes ) do
        --self:T(3( { "Prefix:", string.find( MStatic:GetName(), StaticPrefix, 1 ), StaticPrefix } )
        if string.find( MStatic:GetName(), StaticPrefix, 1 ) then
          MStaticPrefix = true
        end
      end
      MStaticInclude = MStaticInclude and MStaticPrefix
    end
    
    if self.Filter.Zones then
      local MStaticZone = false
      for ZoneName, Zone in pairs( self.Filter.Zones ) do
        --self:T(3( "Zone:", ZoneName )
        if MStatic and MStatic:IsInZone(Zone) then
          MStaticZone = true
        end
      end
      MStaticInclude = MStaticInclude and MStaticZone
    end
    
    if self.Filter.Functions and MStaticInclude then
      local MClientFunc = self:_EvalFilterFunctions(MStatic)
      MStaticInclude = MStaticInclude and MClientFunc
    end
    
    --self:T(2( MStaticInclude )
    return MStaticInclude
  end

  --- Retrieve the type names of the @{Wrapper.Static}s in the SET, delimited by an optional delimiter.
  -- @param #SET_STATIC self
  -- @param #string Delimiter (Optional) The delimiter, which is default a comma.
  -- @return #string The types of the @{Wrapper.Static}s delimited.
  function SET_STATIC:GetTypeNames( Delimiter )

    Delimiter = Delimiter or ", "
    local TypeReport = REPORT:New()
    local Types = {}

    for StaticName, StaticData in pairs( self:GetSet() ) do

      local Static = StaticData -- Wrapper.Static#STATIC
      local StaticTypeName = Static:GetTypeName()

      if not Types[StaticTypeName] then
        Types[StaticTypeName] = StaticTypeName
        TypeReport:Add( StaticTypeName )
      end
    end

    return TypeReport:Text( Delimiter )
  end

  --- Get the closest static of the set with respect to a given reference coordinate. Optionally, only statics of given coalitions are considered in the search.
  -- @param #SET_STATIC self
  -- @param Core.Point#COORDINATE Coordinate Reference Coordinate from which the closest static is determined.
  -- @return Wrapper.Static#STATIC The closest static (if any).
  -- @return #number Distance in meters to the closest static.
  function SET_STATIC:GetClosestStatic(Coordinate, Coalitions)
  
    local Set = self:GetSet()
    
    local dmin=math.huge
    local gmin=nil
    
    for GroupID, GroupData in pairs( Set ) do -- For each STATIC in SET_STATIC
      local group=GroupData --Wrapper.Static#STATIC
      
      if group and group:IsAlive() and (Coalitions==nil or UTILS.IsAnyInTable(Coalitions, group:GetCoalition())) then
      
        local coord=group:GetCoord()
        
        -- Distance between ref. coordinate and group coordinate.
        local d=UTILS.VecDist3D(Coordinate, coord)
      
        if d<dmin then
          dmin=d
          gmin=group
        end
        
      end
    
    end
    
    return gmin, dmin
  end

end

do -- SET_CLIENT
  
  ---
  -- @type SET_CLIENT
  -- @field Core.Timer#TIMER ZoneTimer
  -- @field #number ZoneTimerInterval
  -- @extends Core.Set#SET_BASE

  --- Mission designers can use the @{Core.Set#SET_CLIENT} class to build sets of units belonging to certain:
  --
  --  * Coalitions
  --  * Categories
  --  * Countries
  --  * Client types
  --  * Starting with certain prefix strings.
  --
  -- ## 1) SET_CLIENT constructor
  --
  -- Create a new SET_CLIENT object with the @{#SET_CLIENT.New} method:
  --
  --    * @{#SET_CLIENT.New}: Creates a new SET_CLIENT object.
  --
  -- ## 2) Add or Remove CLIENT(s) from SET_CLIENT
  --
  -- CLIENTs can be added and removed using the @{Core.Set#SET_CLIENT.AddClientsByName} and @{Core.Set#SET_CLIENT.RemoveClientsByName} respectively.
  -- These methods take a single CLIENT name or an array of CLIENT names to be added or removed from SET_CLIENT.
  --
  -- ## 3) SET_CLIENT filter criteria
  --
  -- You can set filter criteria to define the set of clients within the SET_CLIENT.
  -- Filter criteria are defined by:
  --
  --    * @{#SET_CLIENT.FilterCoalitions}: Builds the SET_CLIENT with the clients belonging to the coalition(s).
  --    * @{#SET_CLIENT.FilterCategories}: Builds the SET_CLIENT with the clients belonging to the category(ies).
  --    * @{#SET_CLIENT.FilterTypes}: Builds the SET_CLIENT with the clients belonging to the client type(s).
  --    * @{#SET_CLIENT.FilterCountries}: Builds the SET_CLIENT with the clients belonging to the country(ies).
  --    * @{#SET_CLIENT.FilterPrefixes}: Builds the SET_CLIENT with the clients containing the same string(s) in their unit/pilot name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. 
  -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching)
  --    * @{#SET_CLIENT.FilterActive}: Builds the SET_CLIENT with the units that are only active. Units that are inactive (late activation) won't be included in the set!
  --    * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Core.Zone#ZONE}.
  --    * @{#SET_CLIENT.FilterFunction}: Builds the SET_CLIENT with a custom condition.
  --    
  -- Once the filter criteria have been set for the SET_CLIENT, you can start filtering using:
  --
  --   * @{#SET_CLIENT.FilterStart}: Starts the filtering of the clients **dynamically**.
  --   * @{#SET_CLIENT.FilterOnce}: Filters the clients **once**.
  --
  -- ## 4) SET_CLIENT iterators
  --
  -- Once the filters have been defined and the SET_CLIENT has been built, you can iterate the SET_CLIENT with the available iterator methods.
  -- The iterator methods will walk the SET_CLIENT set, and call for each element within the set a function that you provide.
  -- The following iterator methods are currently available within the SET_CLIENT:
  --
  --   * @{#SET_CLIENT.ForEachClient}: Calls a function for each alive client it finds within the SET_CLIENT.
  --
  -- ===
  -- @field #SET_CLIENT SET_CLIENT
  SET_CLIENT = {
    ClassName = "SET_CLIENT",
    Clients = {},
    Filter = {
      Coalitions = nil,
      Categories = nil,
      Types = nil,
      Countries = nil,
      ClientPrefixes = nil,
      Zones = nil,
      Playernames = nil,
      Callsigns = nil,
    },
    FilterMeta = {
      Coalitions = {
        red = coalition.side.RED,
        blue = coalition.side.BLUE,
        neutral = coalition.side.NEUTRAL,
      },
      Categories = {
        plane = Unit.Category.AIRPLANE,
        helicopter = Unit.Category.HELICOPTER,
        ground = Unit.Category.GROUND_UNIT,
        ship = Unit.Category.SHIP,
        structure = Unit.Category.STRUCTURE,
      },
    },
  }

  --- Creates a new SET_CLIENT object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names.
  -- @param #SET_CLIENT self
  -- @return #SET_CLIENT
  -- @usage
  -- -- Define a new SET_CLIENT Object. This DBObject will contain a reference to all Clients.
  -- DBObject = SET_CLIENT:New()
  function SET_CLIENT:New()
    -- Inherits from BASE
    local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CLIENTS ) ) -- #SET_CLIENT

    self:FilterActive( false )

    return self
  end

  --- Add CLIENT(s) to SET_CLIENT.
  -- @param Core.Set#SET_CLIENT self
  -- @param #string AddClientNames A single name or an array of CLIENT names.
  -- @return self
  function SET_CLIENT:AddClientsByName( AddClientNames )

    local AddClientNamesArray = (type( AddClientNames ) == "table") and AddClientNames or { AddClientNames }

    for AddClientID, AddClientName in pairs( AddClientNamesArray ) do
      self:Add( AddClientName, CLIENT:FindByName( AddClientName ) )
    end

    return self
  end

  --- Remove CLIENT(s) from SET_CLIENT.
  -- @param Core.Set#SET_CLIENT self
  -- @param Wrapper.Client#CLIENT RemoveClientNames A single object or an array of CLIENT objects.
  -- @return self
  function SET_CLIENT:RemoveClientsByName( RemoveClientNames )

    local RemoveClientNamesArray = (type( RemoveClientNames ) == "table") and RemoveClientNames or { RemoveClientNames }

    for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do
      self:Remove( RemoveClientName.ClientName )
    end

    return self
  end

  --- Finds a Client based on the Client Name.
  -- @param #SET_CLIENT self
  -- @param #string ClientName
  -- @return Wrapper.Client#CLIENT The found Client.
  function SET_CLIENT:FindClient( ClientName )

    local ClientFound = self.Set[ClientName]
    return ClientFound
  end

  --- Builds a set of clients of certain callsigns.
  -- @param #SET_CLIENT self
  -- @param #string Callsigns Can be a single string e.g. "Ford", or a table of strings e.g. {"Uzi","Enfield","Chevy"}. Refers to the callsigns as they can be set in the mission editor.
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterCallsigns( Callsigns )
    if not self.Filter.Callsigns then
      self.Filter.Callsigns = {}
    end
    if type( Callsigns ) ~= "table" then
      Callsigns = { Callsigns }
    end
    for callsignID, callsign in pairs( Callsigns ) do
      self.Filter.Callsigns[callsign] = callsign
    end
    return self
  end

  --- Builds a set of clients of certain playernames.
  -- @param #SET_CLIENT self
  -- @param #string Playernames Can be a single string e.g. "Apple", or a table of strings e.g. {"Walter","Hermann","Gonzo"}. Useful if you have e.g. a common squadron prefix.
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterPlayernames( Playernames )
    if not self.Filter.Playernames then
      self.Filter.Playernames = {}
    end
    if type( Playernames ) ~= "table" then
      Playernames = { Playernames }
    end
    for PlayernameID, playername in pairs( Playernames ) do
      self.Filter.Playernames[playername] = playername
    end
    return self
  end

  --- Builds a set of clients of coalitions.
  -- Possible current coalitions are red, blue and neutral.
  -- @param #SET_CLIENT self
  -- @param #string Coalitions Can take the following values: "red", "blue", "neutral".
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterCoalitions( Coalitions )
    if not self.Filter.Coalitions then
      self.Filter.Coalitions = {}
    end
    if type( Coalitions ) ~= "table" then
      Coalitions = { Coalitions }
    end
    for CoalitionID, Coalition in pairs( Coalitions ) do
      self.Filter.Coalitions[Coalition] = Coalition
    end
    return self
  end

  --- Builds a set of clients out of categories.
  -- Possible current categories are plane, helicopter, ground, ship.
  -- @param #SET_CLIENT self
  -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship".
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterCategories( Categories )
    if not self.Filter.Categories then
      self.Filter.Categories = {}
    end
    if type( Categories ) ~= "table" then
      Categories = { Categories }
    end
    for CategoryID, Category in pairs( Categories ) do
      self.Filter.Categories[Category] = Category
    end
    return self
  end

  --- Builds a set of clients of defined client types.
  -- Possible current types are those types known within DCS world.
  -- @param #SET_CLIENT self
  -- @param #string Types Can take those type strings known within DCS world.
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterTypes( Types )
    if not self.Filter.Types then
      self.Filter.Types = {}
    end
    if type( Types ) ~= "table" then
      Types = { Types }
    end
    for TypeID, Type in pairs( Types ) do
      self.Filter.Types[Type] = Type
    end
    return self
  end

  --- Builds a set of clients of defined countries.
  -- Possible current countries are those known within DCS world.
  -- @param #SET_CLIENT self
  -- @param #string Countries Can take those country strings known within DCS world.
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterCountries( Countries )
    if not self.Filter.Countries then
      self.Filter.Countries = {}
    end
    if type( Countries ) ~= "table" then
      Countries = { Countries }
    end
    for CountryID, Country in pairs( Countries ) do
      self.Filter.Countries[Country] = Country
    end
    return self
  end

  --- Builds a set of CLIENTs that contain the given string in their **unit/pilot** name and **NOT** the group name!
  -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all clients that **contain** the string. Pattern matching applies.
  -- @param #SET_CLIENT self
  -- @param #string Prefixes The string pattern(s) that needs to be contained in the unit/pilot name. Can also be passed as a `#table` of strings.
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterPrefixes( Prefixes )
    if not self.Filter.ClientPrefixes then
      self.Filter.ClientPrefixes = {}
    end
    if type( Prefixes ) ~= "table" then
      Prefixes = { Prefixes }
    end
    for PrefixID, Prefix in pairs( Prefixes ) do
      self.Filter.ClientPrefixes[Prefix] = Prefix
    end
    return self
  end
  
  --- Builds a set of clients which belong to groups with certain **group names**.
  -- @param #SET_CLIENT self
  -- @param #string Prefixes The (partial) group names to look for. Can be anywhere in the group name. Can be a single string or a table of strings.
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterGroupPrefixes(Prefixes)
    if type(Prefixes) == "string" then
      Prefixes = {Prefixes}
    end
    self:FilterFunction(
      function(unit,prefixes)
        local outcome = false
        if unit then
          local grp = unit:GetGroup()
          local gname = grp ~= nil and grp:GetName() or "none"
          for _,_fix in pairs(prefixes or {}) do
            if string.find(gname,_fix) then
              outcome = true
              break
            end
          end
        else
          return false
        end
        return outcome
      end, Prefixes
    )
    return self
  end

  --- Builds a set of clients that are only active.
  -- Only the clients that are active will be included within the set.
  -- @param #SET_CLIENT self
  -- @param #boolean Active (Optional) Include only active clients to the set.
  -- Include inactive clients if you provide false.
  -- @return #SET_CLIENT self
  -- @usage
  --
  -- -- Include only active clients to the set.
  -- ClientSet = SET_CLIENT:New():FilterActive():FilterStart()
  --
  -- -- Include only active clients to the set of the blue coalition, and filter one time.
  -- ClientSet = SET_CLIENT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce()
  --
  -- -- Include only active clients to the set of the blue coalition, and filter one time.
  -- -- Later, reset to include back inactive clients to the set.
  -- ClientSet = SET_CLIENT:New():FilterActive():FilterCoalition( "blue" ):FilterOnce()
  -- ... logic ...
  -- ClientSet = SET_CLIENT:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce()
  --
  function SET_CLIENT:FilterActive( Active )
    Active = Active or not (Active == false)
    self.Filter.Active = Active
    return self
  end

  --- Builds a set of units which exist and are alive.
  -- @param #SET_CLIENT self
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterAlive()
    self:FilterFunction(
      function(unit)
        if unit and unit:IsExist() and unit:IsAlive() then
          return true
        else
          return false
        end
      end
    )
    return self
  end


   --- Builds a set of clients in zones.
  -- @param #SET_CLIENT self
  -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterZones( Zones )
    if not self.Filter.Zones then
      self.Filter.Zones = {}
    end
    local zones = {}
    if Zones.ClassName and Zones.ClassName == "SET_ZONE" then
      zones = Zones.Set
    elseif type( Zones ) ~= "table" or (type( Zones ) == "table" and Zones.ClassName ) then
      self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!")
      return self     
    else
      zones = Zones
    end
    for _,Zone in pairs( zones ) do
      local zonename = Zone:GetName()
      self.Filter.Zones[zonename] = Zone
    end
    return self
  end

  --- [Internal] Private function for use of continous zone filter
  -- @param #SET_CLIENT self
  -- @return #SET_CLIENT self
  function SET_CLIENT:_ContinousZoneFilter()
    
    local Database = _DATABASE.CLIENTS
    
    for ObjectName, Object in pairs( Database ) do
      if self:IsIncludeObject( Object ) and self:IsNotInSet(Object) then
        self:Add( ObjectName, Object )
      elseif (not self:IsIncludeObject( Object )) and self:IsInSet(Object) then
        self:Remove(ObjectName)
      end
    end
    
    return self
    
  end

  --- Set filter timer interval for FilterZones if using active filtering with FilterStart().
  -- @param #SET_CLIENT self
  -- @param #number Seconds Seconds between check intervals, defaults to 30. **Caution** - do not be too agressive with timing! Groups are usually not moving fast enough
  -- to warrant a check of below 10 seconds.
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterZoneTimer(Seconds)
    self.ZoneTimerInterval = Seconds or 30
    return self
  end
  
  --- Stops the filtering.
  -- @param #SET_CLIENT self
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterStop()

    if _DATABASE then
      
      self:UnHandleEvent(EVENTS.Birth)
      self:UnHandleEvent(EVENTS.Dead)
      self:UnHandleEvent(EVENTS.Crash)
      --self:UnHandleEvent(EVENTS.PlayerEnterUnit)
      self:UnHandleEvent(EVENTS.PlayerLeaveUnit)
      
      if self.Filter.Zones and self.ZoneTimer and self.ZoneTimer:IsRunning() then
        self.ZoneTimer:Stop()
      end
    end

    return self
  end

  --- Starts the filtering.
  -- @param #SET_CLIENT self
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterStart()

    if _DATABASE then
      self:HandleEvent( EVENTS.Birth, self._EventOnBirth )
      self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash )
      --self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventPlayerEnterUnit)
      self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventPlayerLeaveUnit)
      --self:SetEventPriority(1)
      if self.Filter.Zones then
        self.ZoneTimer = TIMER:New(self._ContinousZoneFilter,self)
        local timing = self.ZoneTimerInterval or 30
        self.ZoneTimer:Start(timing,timing)
      end
      self:_FilterStart()
    end

    return self
  end
  
  --- Handle CA slots addition
  -- @param #SET_CLIENT self
  -- @param Core.Event#EVENTDATA Event
  -- @return #SET_CLIENT self
  function SET_CLIENT:_EventPlayerEnterUnit(Event)
    --self:I( "_EventPlayerEnterUnit" )
    if Event.IniDCSUnit then
      if Event.IniObjectCategory == Object.Category.UNIT and Event.IniGroup and Event.IniGroup:IsGround() then
        -- CA Slot entered
        local ObjectName, Object = self:AddInDatabase( Event )
        --self:T(( ObjectName, UTILS.PrintTableToLog(Object) )
        if Object and self:IsIncludeObject( Object ) then
          self:Add( ObjectName, Object )
        end
      end
    end
    return self
  end
  
  --- Handle CA slots removal
  -- @param #SET_CLIENT self
  -- @param Core.Event#EVENTDATA Event
  -- @return #SET_CLIENT self
  function SET_CLIENT:_EventPlayerLeaveUnit(Event)
    --self:I( "_EventPlayerLeaveUnit" )
    if Event.IniDCSUnit then
      if Event.IniObjectCategory == Object.Category.UNIT and Event.IniGroup then --and Event.IniGroup:IsGround() then
        -- CA Slot left
        local ObjectName, Object = self:FindInDatabase( Event )
        if ObjectName then
          self:Remove( ObjectName )
        end
      end
    end
    return self
  end
  
  --- Make the SET handle CA slots **only** (GROUND units used by any player). Needs active filtering with `FilterStart()`
  -- @param #SET_CLIENT self
  -- @return #SET_CLIENT self
  function SET_CLIENT:HandleCASlots()
    self:HandleEvent(EVENTS.PlayerEnterUnit,SET_CLIENT._EventPlayerEnterUnit)
    self:HandleEvent(EVENTS.PlayerLeaveUnit,SET_CLIENT._EventPlayerLeaveUnit)
    self:FilterFunction(function(client) if client and client:IsAlive() and client:IsGround() then return true else return false end end)
    return self
  end  

  --- Handles the Database to check on an event (birth) that the Object was added in the Database.
  -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event!
  -- @param #SET_CLIENT self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the CLIENT
  -- @return #table The CLIENT
  function SET_CLIENT:AddInDatabase( Event )
    --self:F3( { Event } )

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Handles the Database to check on any event that Object exists in the Database.
  -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
  -- @param #SET_CLIENT self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the CLIENT
  -- @return #table The CLIENT
  function SET_CLIENT:FindInDatabase( Event )
    --self:F3( { Event } )

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT, providing the CLIENT and optional parameters.
  -- @param #SET_CLIENT self
  -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter.
  -- @return #SET_CLIENT self
  function SET_CLIENT:ForEachClient( IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet() )

    return self
  end

  --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence completely in a @{Core.Zone}, providing the CLIENT and optional parameters to the called function.
  -- @param #SET_CLIENT self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter.
  -- @return #SET_CLIENT self
  function SET_CLIENT:ForEachClientInZone( ZoneObject, IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet(),
      -- @param Core.Zone#ZONE_BASE ZoneObject
      -- @param Wrapper.Client#CLIENT ClientObject
      function( ZoneObject, ClientObject )
        if ClientObject:IsInZone( ZoneObject ) then
          return true
        else
          return false
        end
      end, { ZoneObject } )

    return self
  end

  --- Iterate the SET_CLIENT and call an iterator function for each **alive** CLIENT presence not in a @{Core.Zone}, providing the CLIENT and optional parameters to the called function.
  -- @param #SET_CLIENT self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_CLIENT. The function needs to accept a CLIENT parameter.
  -- @return #SET_CLIENT self
  function SET_CLIENT:ForEachClientNotInZone( ZoneObject, IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet(),
      -- @param Core.Zone#ZONE_BASE ZoneObject
      -- @param Wrapper.Client#CLIENT ClientObject
      function( ZoneObject, ClientObject )
        if ClientObject:IsNotInZone( ZoneObject ) then
          return true
        else
          return false
        end
      end, { ZoneObject } )

    return self
  end

  --- Iterate the SET_CLIENT and count alive units.
  -- @param #SET_CLIENT self
  -- @return #number count
  function SET_CLIENT:CountAlive()

    local Set = self:GetSet()

    local CountU = 0
    for UnitID, UnitData in pairs( Set ) do -- For each GROUP in SET_GROUP
      if UnitData and UnitData:IsAlive() then
        CountU = CountU + 1
      end

    end

    return CountU
  end
  
  
  --- Gets the alive set.
  -- @param #SET_CLIENT self
  -- @return #table Table of SET objects
  function SET_CLIENT:GetAliveSet()

    local AliveSet = {}

    -- Clean the Set before returning with only the alive Groups.
    for GroupName, GroupObject in pairs(self.Set) do      
      if GroupObject and GroupObject:IsAlive() then      
        AliveSet[GroupName] = GroupObject
      end
    end

    return AliveSet or {}
  end

  --- [User] Add a custom condition function.
  -- @function [parent=#SET_CLIENT] FilterFunction
  -- @param #SET_CLIENT self
  -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a CLIENT object as first argument.
  -- @param ... Condition function arguments if any.
  -- @return #SET_CLIENT self
  -- @usage
  --          -- Image you want to exclude a specific CLIENT from a SET:
  --          local groundset = SET_CLIENT:New():FilterCoalitions("blue"):FilterActive(true):FilterFunction(
  --          -- The function needs to take a UNIT object as first - and in this case, only - argument.
  --          function(client)
  --              local isinclude = true
  --              if client:GetPlayerName() == "Exclude Me" then isinclude = false end
  --              return isinclude
  --          end
  --          ):FilterOnce()
  --          BASE:I(groundset:Flush())


  ---
  -- @param #SET_CLIENT self
  -- @param Wrapper.Client#CLIENT MClient
  -- @return #SET_CLIENT self
  function SET_CLIENT:IsIncludeObject( MClient )
    --self:F2( MClient )

    local MClientInclude = true

    if MClient then
      local MClientName = MClient.UnitName

      if self.Filter.Active ~= nil then
        local MClientActive = false
        if self.Filter.Active == false or (self.Filter.Active == true and MClient:IsActive() == true and MClient:IsAlive() == true) then
          MClientActive = true
        end
        --self:T( { "Evaluated Active", MClientActive } )
        MClientInclude = MClientInclude and MClientActive
      end

      if self.Filter.Coalitions and MClientInclude then
        local MClientCoalition = false
        for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do
          local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName )
          if ClientCoalitionID==nil and MClient:IsAlive()~=nil then
            ClientCoalitionID=MClient:GetCoalition()
          end
          --self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } )
          if self.FilterMeta.Coalitions[CoalitionName] and ClientCoalitionID and self.FilterMeta.Coalitions[CoalitionName] == ClientCoalitionID then
            MClientCoalition = true
          end
        end
        --self:T( { "Evaluated Coalition", MClientCoalition } )
        MClientInclude = MClientInclude and MClientCoalition
      end
      
      if self.Filter.Categories and MClientInclude then
        local MClientCategory = false
        for CategoryID, CategoryName in pairs( self.Filter.Categories ) do
          local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName )
          local UnitCategory = 0
          if ClientCategoryID==nil and MClient:IsExist() then
            ClientCategoryID,UnitCategory=MClient:GetCategory()
            --self:T3("Applying Category Workaround .. Outcome: Obj is "..tostring(ClientCategoryID).." Unit is "..tostring(UnitCategory))
            --self:T(3( { "Category:", UnitCategory, self.FilterMeta.Categories[CategoryName], CategoryName } )
            if self.FilterMeta.Categories[CategoryName] and UnitCategory and self.FilterMeta.Categories[CategoryName] == UnitCategory then
              MClientCategory = true
            end
            --self:T3("Filter Outcome is "..tostring(MClientCategory))
          else
            --self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } )
            if self.FilterMeta.Categories[CategoryName] and ClientCategoryID and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then
              MClientCategory = true
            end
          end
        end
        --self:T( { "Evaluated Category", MClientCategory } )
        MClientInclude = MClientInclude and MClientCategory
      end

      if self.Filter.Types and MClientInclude then
        local MClientType = false
        for TypeID, TypeName in pairs( self.Filter.Types ) do
          --self:T3( { "Type:", MClient:GetTypeName(), TypeName } )
          if TypeName == MClient:GetTypeName() then
            MClientType = true
          end
        end
        --self:T(( { "Evaluated Type", MClientType } )
        MClientInclude = MClientInclude and MClientType
      end

      if self.Filter.Countries and MClientInclude then
        local MClientCountry = false
        for CountryID, CountryName in pairs( self.Filter.Countries ) do
          local ClientCountryID = _DATABASE:GetCountryFromClientTemplate( MClientName )
          if ClientCountryID==nil and MClient:IsAlive()~=nil then
            ClientCountryID=MClient:GetCountry()
          end
          --self:T(3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } )
          if country.id[CountryName] and ClientCountryID and country.id[CountryName] == ClientCountryID then
            MClientCountry = true
          end
        end
        --self:T(( { "Evaluated Country", MClientCountry } )
        MClientInclude = MClientInclude and MClientCountry
      end

      if self.Filter.ClientPrefixes and MClientInclude then
        local MClientPrefix = false
        for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do
          --self:T3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } )
          if string.find( MClient.UnitName, ClientPrefix, 1 ) then
            MClientPrefix = true
          end
        end
        --self:T( { "Evaluated Prefix", MClientPrefix } )
        MClientInclude = MClientInclude and MClientPrefix
      end

    if self.Filter.Zones and MClientInclude then
      local MClientZone = false
      for ZoneName, Zone in pairs( self.Filter.Zones ) do
      --self:T3( "Zone:", ZoneName )
      local unit = MClient:GetClientGroupUnit()
      if unit and unit:IsInZone(Zone) then
        MClientZone = true
      end
      end
      MClientInclude = MClientInclude and MClientZone
    end
    
    if self.Filter.Playernames and MClientInclude then
      local MClientPlayername = false
      local playername = MClient:GetPlayerName() or "Unknown"
      --self:T(playername)
      for _,_Playername in pairs(self.Filter.Playernames) do
        if playername and string.find(playername,_Playername) then
          MClientPlayername = true
        end
      end
      --self:T( { "Evaluated Playername", MClientPlayername } )
      MClientInclude = MClientInclude and MClientPlayername
    end
    
    if self.Filter.Callsigns and MClientInclude then
      local MClientCallsigns = false
      local callsign = MClient:GetCallsign()
      --self:I(callsign)
      for _,_Callsign in pairs(self.Filter.Callsigns) do
        if callsign and string.find(callsign,_Callsign,1,true) then
          MClientCallsigns = true
        end
      end
      --self:T( { "Evaluated Callsign", MClientCallsigns } )
      MClientInclude = MClientInclude and MClientCallsigns
    end
    
    if self.Filter.Functions and MClientInclude then
      local MClientFunc = self:_EvalFilterFunctions(MClient)
      MClientInclude = MClientInclude and MClientFunc
    end
    
  end
    --self:T2( MClientInclude )
    return MClientInclude
  end

end

do -- SET_PLAYER

  ---
  -- @type SET_PLAYER
  -- @extends Core.Set#SET_BASE

  --- Mission designers can use the @{Core.Set#SET_PLAYER} class to build sets of units belonging to alive players:
  --
  -- ## SET_PLAYER constructor
  --
  -- Create a new SET_PLAYER object with the @{#SET_PLAYER.New} method:
  --
  --    * @{#SET_PLAYER.New}: Creates a new SET_PLAYER object.
  --
  -- ## SET_PLAYER filter criteria
  --
  -- You can set filter criteria to define the set of clients within the SET_PLAYER.
  -- Filter criteria are defined by:
  --
  --    * @{#SET_PLAYER.FilterCoalitions}: Builds the SET_PLAYER with the clients belonging to the coalition(s).
  --    * @{#SET_PLAYER.FilterCategories}: Builds the SET_PLAYER with the clients belonging to the category(ies).
  --    * @{#SET_PLAYER.FilterTypes}: Builds the SET_PLAYER with the clients belonging to the client type(s).
  --    * @{#SET_PLAYER.FilterCountries}: Builds the SET_PLAYER with the clients belonging to the country(ies).
  --    * @{#SET_PLAYER.FilterPrefixes}: Builds the SET_PLAYER with the clients sharing the same string(s) in their unit/pilot name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. 
  -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching)
  --
  -- Once the filter criteria have been set for the SET_PLAYER, you can start filtering using:
  --
  --   * @{#SET_PLAYER.FilterStart}: Starts the filtering of the clients within the SET_PLAYER.
  --
  -- Planned filter criteria within development are (so these are not yet available):
  --
  --    * @{#SET_PLAYER.FilterZones}: Builds the SET_PLAYER with the clients within a @{Core.Zone#ZONE}.
  --
  -- ## SET_PLAYER iterators
  --
  -- Once the filters have been defined and the SET_PLAYER has been built, you can iterate the SET_PLAYER with the available iterator methods.
  -- The iterator methods will walk the SET_PLAYER set, and call for each element within the set a function that you provide.
  -- The following iterator methods are currently available within the SET_PLAYER:
  --
  --   * @{#SET_PLAYER.ForEachClient}: Calls a function for each alive client it finds within the SET_PLAYER.
  --
  -- ===
  -- @field #SET_PLAYER SET_PLAYER
  SET_PLAYER = {
    ClassName = "SET_PLAYER",
    Clients = {},
    Filter = {
      Coalitions = nil,
      Categories = nil,
      Types = nil,
      Countries = nil,
      ClientPrefixes = nil,
      Zones = nil,
    },
    FilterMeta = {
      Coalitions = {
        red = coalition.side.RED,
        blue = coalition.side.BLUE,
        neutral = coalition.side.NEUTRAL,
      },
      Categories = {
        plane = Unit.Category.AIRPLANE,
        helicopter = Unit.Category.HELICOPTER,
        ground = Unit.Category.GROUND_UNIT,
        ship = Unit.Category.SHIP,
        structure = Unit.Category.STRUCTURE,
      },
    },
  }

  --- Creates a new SET_PLAYER object, building a set of clients belonging to a coalitions, categories, countries, types or with defined prefix names.
  -- @param #SET_PLAYER self
  -- @return #SET_PLAYER
  -- @usage
  -- -- Define a new SET_PLAYER Object. This DBObject will contain a reference to all Clients.
  -- DBObject = SET_PLAYER:New()
  function SET_PLAYER:New()
    -- Inherits from BASE
    local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.PLAYERS ) )

    return self
  end

  --- Add CLIENT(s) to SET_PLAYER.
  -- @param Core.Set#SET_PLAYER self
  -- @param #string AddClientNames A single name or an array of CLIENT names.
  -- @return self
  function SET_PLAYER:AddClientsByName( AddClientNames )

    local AddClientNamesArray = (type( AddClientNames ) == "table") and AddClientNames or { AddClientNames }

    for AddClientID, AddClientName in pairs( AddClientNamesArray ) do
      self:Add( AddClientName, CLIENT:FindByName( AddClientName ) )
    end

    return self
  end

  --- Remove CLIENT(s) from SET_PLAYER.
  -- @param Core.Set#SET_PLAYER self
  -- @param Wrapper.Client#CLIENT RemoveClientNames A single name or an array of CLIENT names.
  -- @return self
  function SET_PLAYER:RemoveClientsByName( RemoveClientNames )

    local RemoveClientNamesArray = (type( RemoveClientNames ) == "table") and RemoveClientNames or { RemoveClientNames }

    for RemoveClientID, RemoveClientName in pairs( RemoveClientNamesArray ) do
      self:Remove( RemoveClientName.ClientName )
    end

    return self
  end

  --- Finds a Client based on the Player Name.
  -- @param #SET_PLAYER self
  -- @param #string PlayerName
  -- @return Wrapper.Client#CLIENT The found Client.
  function SET_PLAYER:FindClient( PlayerName )

    local ClientFound = self.Set[PlayerName]
    return ClientFound
  end

  --- Builds a set of clients of coalitions joined by specific players.
  -- Possible current coalitions are red, blue and neutral.
  -- @param #SET_PLAYER self
  -- @param #string Coalitions Can take the following values: "red", "blue", "neutral".
  -- @return #SET_PLAYER self
  function SET_PLAYER:FilterCoalitions( Coalitions )
    if not self.Filter.Coalitions then
      self.Filter.Coalitions = {}
    end
    if type( Coalitions ) ~= "table" then
      Coalitions = { Coalitions }
    end
    for CoalitionID, Coalition in pairs( Coalitions ) do
      self.Filter.Coalitions[Coalition] = Coalition
    end
    return self
  end
  
  --- Builds a set of players in zones.
  -- @param #SET_PLAYER self
  -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE
  -- @return #SET_PLAYER self
  function SET_PLAYER:FilterZones( Zones )
    if not self.Filter.Zones then
      self.Filter.Zones = {}
    end
    local zones = {}
    if Zones.ClassName and Zones.ClassName == "SET_ZONE" then
      zones = Zones.Set
    elseif type( Zones ) ~= "table" or (type( Zones ) == "table" and Zones.ClassName ) then
      self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!")
      return self     
    else
      zones = Zones
    end
    for _,Zone in pairs( zones ) do
      local zonename = Zone:GetName()
      self.Filter.Zones[zonename] = Zone
    end
    return self
  end
  

  --- Builds a set of clients out of categories joined by players.
  -- Possible current categories are plane, helicopter, ground, ship.
  -- @param #SET_PLAYER self
  -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship".
  -- @return #SET_PLAYER self
  function SET_PLAYER:FilterCategories( Categories )
    if not self.Filter.Categories then
      self.Filter.Categories = {}
    end
    if type( Categories ) ~= "table" then
      Categories = { Categories }
    end
    for CategoryID, Category in pairs( Categories ) do
      self.Filter.Categories[Category] = Category
    end
    return self
  end

  --- Builds a set of clients of defined client types joined by players.
  -- Possible current types are those types known within DCS world.
  -- @param #SET_PLAYER self
  -- @param #string Types Can take those type strings known within DCS world.
  -- @return #SET_PLAYER self
  function SET_PLAYER:FilterTypes( Types )
    if not self.Filter.Types then
      self.Filter.Types = {}
    end
    if type( Types ) ~= "table" then
      Types = { Types }
    end
    for TypeID, Type in pairs( Types ) do
      self.Filter.Types[Type] = Type
    end
    return self
  end

  --- Builds a set of clients of defined countries.
  -- Possible current countries are those known within DCS world.
  -- @param #SET_PLAYER self
  -- @param #string Countries Can take those country strings known within DCS world.
  -- @return #SET_PLAYER self
  function SET_PLAYER:FilterCountries( Countries )
    if not self.Filter.Countries then
      self.Filter.Countries = {}
    end
    if type( Countries ) ~= "table" then
      Countries = { Countries }
    end
    for CountryID, Country in pairs( Countries ) do
      self.Filter.Countries[Country] = Country
    end
    return self
  end

  --- Builds a set of PLAYERs that contain the given string in their unit/pilot name.
  -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all player clients that **contain** the string. 
  -- @param #SET_PLAYER self
  -- @param #string Prefixes The string pattern(s) that needs to be contained in the unit/pilot name. Can also be passed as a `#table` of strings.
  -- @return #SET_PLAYER self
  function SET_PLAYER:FilterPrefixes( Prefixes )
    if not self.Filter.ClientPrefixes then
      self.Filter.ClientPrefixes = {}
    end
    if type( Prefixes ) ~= "table" then
      Prefixes = { Prefixes }
    end
    for PrefixID, Prefix in pairs( Prefixes ) do
      self.Filter.ClientPrefixes[Prefix] = Prefix
    end
    return self
  end

  --- Starts the filtering.
  -- @param #SET_PLAYER self
  -- @return #SET_PLAYER self
  function SET_PLAYER:FilterStart()

    if _DATABASE then
      self:_FilterStart()
      self:HandleEvent( EVENTS.Birth, self._EventOnBirth )
      self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnDeadOrCrash )
    end

    return self
  end

  --- Handles the Database to check on an event (birth) that the Object was added in the Database.
  -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event!
  -- @param #SET_PLAYER self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the CLIENT
  -- @return #table The CLIENT
  function SET_PLAYER:AddInDatabase( Event )
    --self:F3( { Event } )

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Handles the Database to check on any event that Object exists in the Database.
  -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
  -- @param #SET_PLAYER self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the CLIENT
  -- @return #table The CLIENT
  function SET_PLAYER:FindInDatabase( Event )
    --self:F3( { Event } )

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT, providing the CLIENT and optional parameters.
  -- @param #SET_PLAYER self
  -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter.
  -- @return #SET_PLAYER self
  function SET_PLAYER:ForEachPlayer( IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet() )

    return self
  end

  --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence completely in a @{Core.Zone}, providing the CLIENT and optional parameters to the called function.
  -- @param #SET_PLAYER self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter.
  -- @return #SET_PLAYER self
  function SET_PLAYER:ForEachPlayerInZone( ZoneObject, IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet(),
      -- @param Core.Zone#ZONE_BASE ZoneObject
      -- @param Wrapper.Client#CLIENT ClientObject
      function( ZoneObject, ClientObject )
        if ClientObject:IsInZone( ZoneObject ) then
          return true
        else
          return false
        end
      end, { ZoneObject } )

    return self
  end

  --- Iterate the SET_PLAYER and call an iterator function for each **alive** CLIENT presence not in a @{Core.Zone}, providing the CLIENT and optional parameters to the called function.
  -- @param #SET_PLAYER self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @param #function IteratorFunction The function that will be called when there is an alive CLIENT in the SET_PLAYER. The function needs to accept a CLIENT parameter.
  -- @return #SET_PLAYER self
  function SET_PLAYER:ForEachPlayerNotInZone( ZoneObject, IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet(),
      -- @param Core.Zone#ZONE_BASE ZoneObject
      -- @param Wrapper.Client#CLIENT ClientObject
      function( ZoneObject, ClientObject )
        if ClientObject:IsNotInZone( ZoneObject ) then
          return true
        else
          return false
        end
      end, { ZoneObject } )

    return self
  end

  ---
  -- @param #SET_PLAYER self
  -- @param Wrapper.Client#CLIENT MClient
  -- @return #SET_PLAYER self
  function SET_PLAYER:IsIncludeObject( MClient )
    --self:F2( MClient )

    local MClientInclude = true

    if MClient then
      local MClientName = MClient.UnitName

      if self.Filter.Coalitions and MClientInclude then
        local MClientCoalition = false
        for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do
          local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName )
          if ClientCoalitionID==nil and MClient:IsAlive()~=nil then
            ClientCoalitionID=MClient:GetCoalition()
          end
          --self:T(3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } )
          if self.FilterMeta.Coalitions[CoalitionName] and ClientCoalitionID and self.FilterMeta.Coalitions[CoalitionName] == ClientCoalitionID then
            MClientCoalition = true
          end
        end
        --self:T(( { "Evaluated Coalition", MClientCoalition } )
        MClientInclude = MClientInclude and MClientCoalition
      end

      if self.Filter.Categories and MClientInclude then
        local MClientCategory = false
        for CategoryID, CategoryName in pairs( self.Filter.Categories ) do
          local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName )
          local UnitCategory = 0
          if ClientCategoryID==nil and MClient:IsExist() then
            ClientCategoryID,UnitCategory=MClient:GetCategory()
            --self:T(3( { "Category:", UnitCategory, self.FilterMeta.Categories[CategoryName], CategoryName } )
            if self.FilterMeta.Categories[CategoryName] and UnitCategory and self.FilterMeta.Categories[CategoryName] == UnitCategory then
              MClientCategory = true
            end
          else
            --self:T(3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } )
            if self.FilterMeta.Categories[CategoryName] and ClientCategoryID and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then
              MClientCategory = true
            end
          end
        end
        --self:T(( { "Evaluated Category", MClientCategory } )
        MClientInclude = MClientInclude and MClientCategory
      end

      if self.Filter.Types then
        local MClientType = false
        for TypeID, TypeName in pairs( self.Filter.Types ) do
          --self:T(3( { "Type:", MClient:GetTypeName(), TypeName } )
          if TypeName == MClient:GetTypeName() then
            MClientType = true
          end
        end
        --self:T(( { "Evaluated Type", MClientType } )
        MClientInclude = MClientInclude and MClientType
      end

      if self.Filter.Countries then
        local MClientCountry = false
        for CountryID, CountryName in pairs( self.Filter.Countries ) do
          local ClientCountryID = _DATABASE:GetCountryFromClientTemplate( MClientName )
          --self:T(3( { "Country:", ClientCountryID, country.id[CountryName], CountryName } )
          if country.id[CountryName] and country.id[CountryName] == ClientCountryID then
            MClientCountry = true
          end
        end
        --self:T(( { "Evaluated Country", MClientCountry } )
        MClientInclude = MClientInclude and MClientCountry
      end

      if self.Filter.ClientPrefixes then
        local MClientPrefix = false
        for ClientPrefixId, ClientPrefix in pairs( self.Filter.ClientPrefixes ) do
          --self:T(3( { "Prefix:", string.find( MClient.UnitName, ClientPrefix, 1 ), ClientPrefix } )
          if string.find( MClient.UnitName, ClientPrefix, 1 ) then
            MClientPrefix = true
          end
        end
        --self:T(( { "Evaluated Prefix", MClientPrefix } )
        MClientInclude = MClientInclude and MClientPrefix
      end
    end
    
    if self.Filter.Zones then
      local MClientZone = false
      for ZoneName, Zone in pairs( self.Filter.Zones ) do
        --self:T(3( "Zone:", ZoneName )
        local unit = MClient:GetClientGroupUnit()
        if unit and unit:IsInZone(Zone) then
          MClientZone = true
        end
      end
      MClientInclude = MClientInclude and MClientZone
    end
    
    if self.Filter.Functions and MClientInclude then
      local MClientFunc = self:_EvalFilterFunctions(MClient)
      MClientInclude = MClientInclude and MClientFunc
    end
    
    --self:T(2( MClientInclude )
    return MClientInclude
  end

end

do -- SET_AIRBASE
  
  ---
  -- @type SET_AIRBASE
  -- @extends Core.Set#SET_BASE

  --- Mission designers can use the @{Core.Set#SET_AIRBASE} class to build sets of airbases optionally belonging to certain:
  --
  --  * Coalitions
  --
  -- ## SET_AIRBASE constructor
  --
  -- Create a new SET_AIRBASE object with the @{#SET_AIRBASE.New} method:
  --
  --    * @{#SET_AIRBASE.New}: Creates a new SET_AIRBASE object.
  --
  -- ## Add or Remove AIRBASEs from SET_AIRBASE
  --
  -- AIRBASEs can be added and removed using the @{Core.Set#SET_AIRBASE.AddAirbasesByName} and @{Core.Set#SET_AIRBASE.RemoveAirbasesByName} respectively.
  -- These methods take a single AIRBASE name or an array of AIRBASE names to be added or removed from SET_AIRBASE.
  --
  -- ## SET_AIRBASE filter criteria
  --
  -- You can set filter criteria to define the set of clients within the SET_AIRBASE.
  -- Filter criteria are defined by:
  --
  --    * @{#SET_AIRBASE.FilterCoalitions}: Builds the SET_AIRBASE with the airbases belonging to the coalition(s).
  --
  -- Once the filter criteria have been set for the SET_AIRBASE, you can start filtering using:
  --
  --   * @{#SET_AIRBASE.FilterStart}: Starts the filtering of the airbases within the SET_AIRBASE.
  --
  -- ## SET_AIRBASE iterators
  --
  -- Once the filters have been defined and the SET_AIRBASE has been built, you can iterate the SET_AIRBASE with the available iterator methods.
  -- The iterator methods will walk the SET_AIRBASE set, and call for each airbase within the set a function that you provide.
  -- The following iterator methods are currently available within the SET_AIRBASE:
  --
  --   * @{#SET_AIRBASE.ForEachAirbase}: Calls a function for each airbase it finds within the SET_AIRBASE.
  --
  -- ===
  -- @field #SET_AIRBASE SET_AIRBASE
  SET_AIRBASE = {
    ClassName = "SET_AIRBASE",
    Airbases = {},
    Filter = {
      Coalitions = nil,
      Zones = nil,
    },
    FilterMeta = {
      Coalitions = {
        red = coalition.side.RED,
        blue = coalition.side.BLUE,
        neutral = coalition.side.NEUTRAL,
      },
      Categories = {
        airdrome = Airbase.Category.AIRDROME,
        helipad = Airbase.Category.HELIPAD,
        ship = Airbase.Category.SHIP,
      },
    },
  }

  --- Creates a new SET_AIRBASE object, building a set of airbases belonging to a coalitions and categories.
  -- @param #SET_AIRBASE self
  -- @return #SET_AIRBASE self
  -- @usage
  -- -- Define a new SET_AIRBASE Object. The DatabaseSet will contain a reference to all Airbases.
  -- DatabaseSet = SET_AIRBASE:New()
  function SET_AIRBASE:New()
    -- Inherits from BASE
    local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.AIRBASES ) )

    return self
  end

  --- Add an AIRBASE object to SET_AIRBASE.
  -- @param Core.Set#SET_AIRBASE self
  -- @param Wrapper.Airbase#AIRBASE airbase Airbase that should be added to the set.
  -- @return self
  function SET_AIRBASE:AddAirbase( airbase )

    self:Add( airbase:GetName(), airbase )

    return self
  end

  --- Add AIRBASEs to SET_AIRBASE.
  -- @param Core.Set#SET_AIRBASE self
  -- @param #string AddAirbaseNames A single name or an array of AIRBASE names.
  -- @return self
  function SET_AIRBASE:AddAirbasesByName( AddAirbaseNames )

    local AddAirbaseNamesArray = (type( AddAirbaseNames ) == "table") and AddAirbaseNames or { AddAirbaseNames }

    for AddAirbaseID, AddAirbaseName in pairs( AddAirbaseNamesArray ) do
      self:Add( AddAirbaseName, AIRBASE:FindByName( AddAirbaseName ) )
    end

    return self
  end

  --- Remove AIRBASEs from SET_AIRBASE.
  -- @param Core.Set#SET_AIRBASE self
  -- @param Wrapper.Airbase#AIRBASE RemoveAirbaseNames A single name or an array of AIRBASE names.
  -- @return self
  function SET_AIRBASE:RemoveAirbasesByName( RemoveAirbaseNames )

    local RemoveAirbaseNamesArray = (type( RemoveAirbaseNames ) == "table") and RemoveAirbaseNames or { RemoveAirbaseNames }

    for RemoveAirbaseID, RemoveAirbaseName in pairs( RemoveAirbaseNamesArray ) do
      self:Remove( RemoveAirbaseName )
    end

    return self
  end

  --- Finds a Airbase based on the Airbase Name.
  -- @param #SET_AIRBASE self
  -- @param #string AirbaseName
  -- @return Wrapper.Airbase#AIRBASE The found Airbase.
  function SET_AIRBASE:FindAirbase( AirbaseName )

    local AirbaseFound = self.Set[AirbaseName]
    return AirbaseFound
  end

  --- Finds an Airbase in range of a coordinate.
  -- @param #SET_AIRBASE self
  -- @param Core.Point#COORDINATE Coordinate
  -- @param #number Range
  -- @return Wrapper.Airbase#AIRBASE The found Airbase.
  function SET_AIRBASE:FindAirbaseInRange( Coordinate, Range )

    local AirbaseFound = nil

    for AirbaseName, AirbaseObject in pairs( self.Set ) do

      local AirbaseCoordinate = AirbaseObject:GetCoordinate()
      local Distance = Coordinate:Get2DDistance( AirbaseCoordinate )

      --self:F( { Distance = Distance } )

      if Distance <= Range then
        AirbaseFound = AirbaseObject
        break
      end

    end

    return AirbaseFound
  end

  --- Finds a random Airbase in the set.
  -- @param #SET_AIRBASE self
  -- @return Wrapper.Airbase#AIRBASE The found Airbase.
  function SET_AIRBASE:GetRandomAirbase()

    local RandomAirbase = self:GetRandom()
    --self:F( { RandomAirbase = RandomAirbase:GetName() } )

    return RandomAirbase
  end

  --- Builds a set of airbases of coalitions.
  -- Possible current coalitions are red, blue and neutral.
  -- @param #SET_AIRBASE self
  -- @param #string Coalitions Can take the following values: "red", "blue", "neutral".
  -- @return #SET_AIRBASE self
  function SET_AIRBASE:FilterCoalitions( Coalitions )
    if not self.Filter.Coalitions then
      self.Filter.Coalitions = {}
    end
    if type( Coalitions ) ~= "table" then
      Coalitions = { Coalitions }
    end
    for CoalitionID, Coalition in pairs( Coalitions ) do
      self.Filter.Coalitions[Coalition] = Coalition
    end
    return self
  end

  --- Builds a set of airbases out of categories.
  -- Possible current categories are plane, helicopter, ground, ship.
  -- @param #SET_AIRBASE self
  -- @param #string Categories Can take the following values: "airdrome", "helipad", "ship".
  -- @return #SET_AIRBASE self
  function SET_AIRBASE:FilterCategories( Categories )
    if not self.Filter.Categories then
      self.Filter.Categories = {}
    end
    if type( Categories ) ~= "table" then
      Categories = { Categories }
    end
    for CategoryID, Category in pairs( Categories ) do
      self.Filter.Categories[Category] = Category
    end
    return self
  end
  
  --- Builds a set of airbase objects in zones.
  -- @param #SET_AIRBASE self
  -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE
  -- @return #SET_AIRBASE self
  function SET_AIRBASE:FilterZones( Zones )
    if not self.Filter.Zones then
      self.Filter.Zones = {}
    end
    local zones = {}
    if Zones.ClassName and Zones.ClassName == "SET_ZONE" then
      zones = Zones.Set
    elseif type( Zones ) ~= "table" or (type( Zones ) == "table" and Zones.ClassName ) then
      self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!")
      return self     
    else
      zones = Zones
    end
    for _,Zone in pairs( zones ) do
      local zonename = Zone:GetName()
      --self:T((zonename)
      self.Filter.Zones[zonename] = Zone
    end
    return self
  end  

  --- Starts the filtering.
  -- @param #SET_AIRBASE self
  -- @return #SET_AIRBASE self
  function SET_AIRBASE:FilterStart()

    if _DATABASE then

      -- We use the BaseCaptured event, which is generated by DCS when a base got captured.
      self:HandleEvent( EVENTS.BaseCaptured )
      self:HandleEvent( EVENTS.Dead )

      -- We initialize the first set.
      for ObjectName, Object in pairs( self.Database ) do
        if self:IsIncludeObject( Object ) then
          self:Add( ObjectName, Object )
        else
          self:RemoveAirbasesByName( ObjectName )
        end
      end
    end

    return self
  end

  --- Base capturing event.
  -- @param #SET_AIRBASE self
  -- @param Core.Event#EVENT EventData
  function SET_AIRBASE:OnEventBaseCaptured( EventData )

    -- When a base got captured, we reevaluate the set.
    for ObjectName, Object in pairs( self.Database ) do
      if self:IsIncludeObject( Object ) then
        -- We add captured bases on yet in the set.
        self:Add( ObjectName, Object )
      else
        -- We remove captured bases that are not anymore part of the set.
        self:RemoveAirbasesByName( ObjectName )
      end
    end

  end

  --- Dead event.
  -- @param #SET_AIRBASE self
  -- @param Core.Event#EVENT EventData
  function SET_AIRBASE:OnEventDead( EventData )

    local airbaseName, airbase = self:FindInDatabase( EventData )

    if airbase and (airbase:IsShip() or airbase:IsHelipad()) then
      self:RemoveAirbasesByName( airbaseName )
    end

  end

  --- Handles the Database to check on an event (birth) that the Object was added in the Database.
  -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event!
  -- @param #SET_AIRBASE self
  -- @param Core.Event#EVENTDATA Event Event data.
  -- @return #string The name of the AIRBASE.
  -- @return Wrapper.Airbase#AIRBASE The AIRBASE object.
  function SET_AIRBASE:AddInDatabase( Event )
    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Handles the Database to check on any event that Object exists in the Database.
  -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
  -- @param #SET_AIRBASE self
  -- @param Core.Event#EVENTDATA Event Event data.
  -- @return #string The name of the AIRBASE.
  -- @return Wrapper.Airbase#AIRBASE The AIRBASE object.
  function SET_AIRBASE:FindInDatabase( Event )
    --self:F3( { Event } )

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Iterate the SET_AIRBASE and call an iterator function for each AIRBASE, providing the AIRBASE and optional parameters.
  -- @param #SET_AIRBASE self
  -- @param #function IteratorFunction The function that will be called when there is an alive AIRBASE in the SET_AIRBASE. The function needs to accept a AIRBASE parameter.
  -- @return #SET_AIRBASE self
  function SET_AIRBASE:ForEachAirbase( IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet() )

    return self
  end

  --- Iterate the SET_AIRBASE while identifying the nearest @{Wrapper.Airbase#AIRBASE} from a @{Core.Point#COORDINATE}.
  -- @param #SET_AIRBASE self
  -- @param Core.Point#COORDINATE Coordinate A @{Core.Point#COORDINATE} object from where to evaluate the closest @{Wrapper.Airbase#AIRBASE}.
  -- @return Wrapper.Airbase#AIRBASE The closest @{Wrapper.Airbase#AIRBASE}.
  function SET_AIRBASE:FindNearestAirbaseFromPointVec2( Coordinate )
    --self:F2( Coordinate )

    local NearestAirbase = self:FindNearestObjectFromPointVec2( Coordinate )
    return NearestAirbase
  end

  ---
  -- @param #SET_AIRBASE self
  -- @param Wrapper.Airbase#AIRBASE MAirbase
  -- @return #SET_AIRBASE self
  function SET_AIRBASE:IsIncludeObject( MAirbase )
    --self:F2( MAirbase )

    local MAirbaseInclude = true

    if MAirbase then
      local MAirbaseName = MAirbase:GetName()

      if self.Filter.Coalitions then
        local MAirbaseCoalition = false
        for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do
          local AirbaseCoalitionID = _DATABASE:GetCoalitionFromAirbase( MAirbaseName )
          --self:T(3( { "Coalition:", AirbaseCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } )
          if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == AirbaseCoalitionID then
            MAirbaseCoalition = true
          end
        end
        --self:T(( { "Evaluated Coalition", MAirbaseCoalition } )
        MAirbaseInclude = MAirbaseInclude and MAirbaseCoalition
      end

      if self.Filter.Categories and MAirbaseInclude then
        local MAirbaseCategory = false
        for CategoryID, CategoryName in pairs( self.Filter.Categories ) do
          local AirbaseCategoryID = _DATABASE:GetCategoryFromAirbase( MAirbaseName )
          --self:T(3( { "Category:", AirbaseCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } )
          if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == AirbaseCategoryID then
            MAirbaseCategory = true
          end
        end
        --self:T(( { "Evaluated Category", MAirbaseCategory } )
        MAirbaseInclude = MAirbaseInclude and MAirbaseCategory
      end
      
      if self.Filter.Zones and MAirbaseInclude then
        local MAirbaseZone = false
        for ZoneName, Zone in pairs( self.Filter.Zones ) do
          --self:T(( "Zone:", ZoneName )
          local coord = MAirbase:GetCoordinate()
          if coord and Zone:IsCoordinateInZone(coord) then
            MAirbaseZone = true
          end
          --self:T(( { "Evaluated Zone", MSceneryZone } )
        end
        MAirbaseInclude = MAirbaseInclude and MAirbaseZone
      end      
      
    end
    
    if self.Filter.Functions and MAirbaseInclude then
      local MClientFunc = self:_EvalFilterFunctions(MAirbase)
      MAirbaseInclude = MAirbaseInclude and MClientFunc
    end

    --self:T(2( MAirbaseInclude )
    return MAirbaseInclude
  end

end

do -- SET_CARGO
  
  ---
  -- @type SET_CARGO
  -- @extends Core.Set#SET_BASE

  --- Mission designers can use the @{Core.Set#SET_CARGO} class to build sets of cargos optionally belonging to certain:
  --
  --  * Coalitions
  --  * Types
  --  * Name or Prefix
  --
  -- ## SET_CARGO constructor
  --
  -- Create a new SET_CARGO object with the @{#SET_CARGO.New} method:
  --
  --    * @{#SET_CARGO.New}: Creates a new SET_CARGO object.
  --
  -- ## Add or Remove CARGOs from SET_CARGO
  --
  -- CARGOs can be added and removed using the @{Core.Set#SET_CARGO.AddCargosByName} and @{Core.Set#SET_CARGO.RemoveCargosByName} respectively.
  -- These methods take a single CARGO name or an array of CARGO names to be added or removed from SET_CARGO.
  --
  -- ## SET_CARGO filter criteria
  --
  -- You can set filter criteria to automatically maintain the SET_CARGO contents.
  -- Filter criteria are defined by:
  --
  --    * @{#SET_CARGO.FilterCoalitions}: Builds the SET_CARGO with the cargos belonging to the coalition(s).
  --    * @{#SET_CARGO.FilterPrefixes}: Builds the SET_CARGO with the cargos containing the same string(s). **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. 
  -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching)
  --    * @{#SET_CARGO.FilterTypes}: Builds the SET_CARGO with the cargos belonging to the cargo type(s).
  --    * @{#SET_CARGO.FilterCountries}: Builds the SET_CARGO with the cargos belonging to the country(ies).
  --
  -- Once the filter criteria have been set for the SET_CARGO, you can start filtering using:
  --
  --   * @{#SET_CARGO.FilterStart}: Starts the filtering of the cargos within the SET_CARGO.
  --
  -- ## SET_CARGO iterators
  --
  -- Once the filters have been defined and the SET_CARGO has been built, you can iterate the SET_CARGO with the available iterator methods.
  -- The iterator methods will walk the SET_CARGO set, and call for each cargo within the set a function that you provide.
  -- The following iterator methods are currently available within the SET_CARGO:
  --
  --   * @{#SET_CARGO.ForEachCargo}: Calls a function for each cargo it finds within the SET_CARGO.
  --
  -- @field #SET_CARGO SET_CARGO
  SET_CARGO = {
    ClassName = "SET_CARGO",
    Cargos = {},
    Filter = {
      Coalitions = nil,
      Types = nil,
      Countries = nil,
      ClientPrefixes = nil,
    },
    FilterMeta = {
      Coalitions = {
        red = coalition.side.RED,
        blue = coalition.side.BLUE,
        neutral = coalition.side.NEUTRAL,
      },
    },
  }

  --- Creates a new SET_CARGO object, building a set of cargos belonging to a coalitions and categories.
  -- @param #SET_CARGO self
  -- @return #SET_CARGO
  -- @usage
  -- -- Define a new SET_CARGO Object. The DatabaseSet will contain a reference to all Cargos.
  -- DatabaseSet = SET_CARGO:New()
  function SET_CARGO:New() -- R2.1
    -- Inherits from BASE
    local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.CARGOS ) ) -- #SET_CARGO

    return self
  end

  --- (R2.1) Add CARGO to SET_CARGO.
  -- @param Core.Set#SET_CARGO self
  -- @param Cargo.Cargo#CARGO Cargo A single cargo.
  -- @return  Core.Set#SET_CARGO self
  function SET_CARGO:AddCargo( Cargo ) -- R2.4

    self:Add( Cargo:GetName(), Cargo )

    return self
  end

  --- (R2.1) Add CARGOs to SET_CARGO.
  -- @param Core.Set#SET_CARGO self
  -- @param #string AddCargoNames A single name or an array of CARGO names.
  -- @return  Core.Set#SET_CARGO self
  function SET_CARGO:AddCargosByName( AddCargoNames ) -- R2.1

    local AddCargoNamesArray = (type( AddCargoNames ) == "table") and AddCargoNames or { AddCargoNames }

    for AddCargoID, AddCargoName in pairs( AddCargoNamesArray ) do
      self:Add( AddCargoName, CARGO:FindByName( AddCargoName ) )
    end

    return self
  end

  --- (R2.1) Remove CARGOs from SET_CARGO.
  -- @param Core.Set#SET_CARGO self
  -- @param Cargo.Cargo#CARGO RemoveCargoNames A single name or an array of CARGO names.
  -- @return Core.Set#SET_CARGO self
  function SET_CARGO:RemoveCargosByName( RemoveCargoNames ) -- R2.1

    local RemoveCargoNamesArray = (type( RemoveCargoNames ) == "table") and RemoveCargoNames or { RemoveCargoNames }

    for RemoveCargoID, RemoveCargoName in pairs( RemoveCargoNamesArray ) do
      self:Remove( RemoveCargoName.CargoName )
    end

    return self
  end

  --- (R2.1) Finds a Cargo based on the Cargo Name.
  -- @param #SET_CARGO self
  -- @param #string CargoName
  -- @return Cargo.Cargo#CARGO The found Cargo.
  function SET_CARGO:FindCargo( CargoName ) -- R2.1

    local CargoFound = self.Set[CargoName]
    return CargoFound
  end

  --- (R2.1) Builds a set of cargos of coalitions.
  -- Possible current coalitions are red, blue and neutral.
  -- @param #SET_CARGO self
  -- @param #string Coalitions Can take the following values: "red", "blue", "neutral".
  -- @return #SET_CARGO self
  function SET_CARGO:FilterCoalitions( Coalitions ) -- R2.1
    if not self.Filter.Coalitions then
      self.Filter.Coalitions = {}
    end
    if type( Coalitions ) ~= "table" then
      Coalitions = { Coalitions }
    end
    for CoalitionID, Coalition in pairs( Coalitions ) do
      self.Filter.Coalitions[Coalition] = Coalition
    end
    return self
  end

  --- (R2.1) Builds a set of cargos of defined cargo types.
  -- Possible current types are those types known within DCS world.
  -- @param #SET_CARGO self
  -- @param #string Types Can take those type strings known within DCS world.
  -- @return #SET_CARGO self
  function SET_CARGO:FilterTypes( Types ) -- R2.1
    if not self.Filter.Types then
      self.Filter.Types = {}
    end
    if type( Types ) ~= "table" then
      Types = { Types }
    end
    for TypeID, Type in pairs( Types ) do
      self.Filter.Types[Type] = Type
    end
    return self
  end

  --- (R2.1) Builds a set of cargos of defined countries.
  -- Possible current countries are those known within DCS world.
  -- @param #SET_CARGO self
  -- @param #string Countries Can take those country strings known within DCS world.
  -- @return #SET_CARGO self
  function SET_CARGO:FilterCountries( Countries ) -- R2.1
    if not self.Filter.Countries then
      self.Filter.Countries = {}
    end
    if type( Countries ) ~= "table" then
      Countries = { Countries }
    end
    for CountryID, Country in pairs( Countries ) do
      self.Filter.Countries[Country] = Country
    end
    return self
  end

  --- Builds a set of CARGOs that contain a given string in their name.
  -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all cargos that **contain** the string. 
  -- @param #SET_CARGO self
  -- @param #string Prefixes The string pattern(s) that need to be in the cargo name. Can also be passed as a `#table` of strings.
  -- @return #SET_CARGO self
  function SET_CARGO:FilterPrefixes( Prefixes ) -- R2.1
    if not self.Filter.CargoPrefixes then
      self.Filter.CargoPrefixes = {}
    end
    if type( Prefixes ) ~= "table" then
      Prefixes = { Prefixes }
    end
    for PrefixID, Prefix in pairs( Prefixes ) do
      self.Filter.CargoPrefixes[Prefix] = Prefix
    end
    return self
  end

  --- (R2.1) Starts the filtering.
  -- @param #SET_CARGO self
  -- @return #SET_CARGO self
  function SET_CARGO:FilterStart() -- R2.1

    if _DATABASE then
      self:_FilterStart()
      self:HandleEvent( EVENTS.NewCargo )
      self:HandleEvent( EVENTS.DeleteCargo )
    end

    return self
  end

  --- Stops the filtering for the defined collection.
  -- @param #SET_CARGO self
  -- @return #SET_CARGO self
  function SET_CARGO:FilterStop()

    self:UnHandleEvent( EVENTS.NewCargo )
    self:UnHandleEvent( EVENTS.DeleteCargo )

    return self
  end

  --- (R2.1) Handles the Database to check on an event (birth) that the Object was added in the Database.
  -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event!
  -- @param #SET_CARGO self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the CARGO
  -- @return #table The CARGO
  function SET_CARGO:AddInDatabase( Event ) -- R2.1
    --self:F3( { Event } )

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- (R2.1) Handles the Database to check on any event that Object exists in the Database.
  -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
  -- @param #SET_CARGO self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the CARGO
  -- @return #table The CARGO
  function SET_CARGO:FindInDatabase( Event ) -- R2.1
    --self:F3( { Event } )

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- (R2.1) Iterate the SET_CARGO and call an iterator function for each CARGO, providing the CARGO and optional parameters.
  -- @param #SET_CARGO self
  -- @param #function IteratorFunction The function that will be called when there is an alive CARGO in the SET_CARGO. The function needs to accept a CARGO parameter.
  -- @return #SET_CARGO self
  function SET_CARGO:ForEachCargo( IteratorFunction, ... ) -- R2.1
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet() )

    return self
  end

  --- (R2.1) Iterate the SET_CARGO while identifying the nearest @{Cargo.Cargo#CARGO} from a @{Core.Point#COORDINATE}.
  -- @param #SET_CARGO self
  -- @param Core.Point#COORDINATE Coordinate A @{Core.Point#COORDINATE} object from where to evaluate the closest @{Cargo.Cargo#CARGO}.
  -- @return Cargo.Cargo#CARGO The closest @{Cargo.Cargo#CARGO}.
  function SET_CARGO:FindNearestCargoFromPointVec2( Coordinate ) -- R2.1
    --self:F2( Coordinate )

    local NearestCargo = self:FindNearestObjectFromPointVec2( Coordinate )
    return NearestCargo
  end
  
  ---
  -- @param #SET_CARGO self
  function SET_CARGO:FirstCargoWithState( State )

    local FirstCargo = nil

    for CargoName, Cargo in pairs( self.Set ) do
      if Cargo:Is( State ) then
        FirstCargo = Cargo
        break
      end
    end

    return FirstCargo
  end

  ---
  -- @param #SET_CARGO self
  function SET_CARGO:FirstCargoWithStateAndNotDeployed( State )

    local FirstCargo = nil

    for CargoName, Cargo in pairs( self.Set ) do
      if Cargo:Is( State ) and not Cargo:IsDeployed() then
        FirstCargo = Cargo
        break
      end
    end

    return FirstCargo
  end

  --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is UnLoaded.
  -- @param #SET_CARGO self
  -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}.
  function SET_CARGO:FirstCargoUnLoaded()
    local FirstCargo = self:FirstCargoWithState( "UnLoaded" )
    return FirstCargo
  end

  --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is UnLoaded and not Deployed.
  -- @param #SET_CARGO self
  -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}.
  function SET_CARGO:FirstCargoUnLoadedAndNotDeployed()
    local FirstCargo = self:FirstCargoWithStateAndNotDeployed( "UnLoaded" )
    return FirstCargo
  end

  --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is Loaded.
  -- @param #SET_CARGO self
  -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}.
  function SET_CARGO:FirstCargoLoaded()
    local FirstCargo = self:FirstCargoWithState( "Loaded" )
    return FirstCargo
  end

  --- Iterate the SET_CARGO while identifying the first @{Cargo.Cargo#CARGO} that is Deployed.
  -- @param #SET_CARGO self
  -- @return Cargo.Cargo#CARGO The first @{Cargo.Cargo#CARGO}.
  function SET_CARGO:FirstCargoDeployed()
    local FirstCargo = self:FirstCargoWithState( "Deployed" )
    return FirstCargo
  end

  --- (R2.1)
  -- @param #SET_CARGO self
  -- @param AI.AI_Cargo#AI_CARGO MCargo
  -- @return #SET_CARGO self
  function SET_CARGO:IsIncludeObject( MCargo ) -- R2.1
    --self:F2( MCargo )

    local MCargoInclude = true

    if MCargo then
      local MCargoName = MCargo:GetName()

      if self.Filter.Coalitions then
        local MCargoCoalition = false
        for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do
          local CargoCoalitionID = MCargo:GetCoalition()
          --self:T(3( { "Coalition:", CargoCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } )
          if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == CargoCoalitionID then
            MCargoCoalition = true
          end
        end
        --self:F( { "Evaluated Coalition", MCargoCoalition } )
        MCargoInclude = MCargoInclude and MCargoCoalition
      end

      if self.Filter.Types then
        local MCargoType = false
        for TypeID, TypeName in pairs( self.Filter.Types ) do
          --self:T(3( { "Type:", MCargo:GetType(), TypeName } )
          if TypeName == MCargo:GetType() then
            MCargoType = true
          end
        end
        --self:F( { "Evaluated Type", MCargoType } )
        MCargoInclude = MCargoInclude and MCargoType
      end

      if self.Filter.CargoPrefixes then
        local MCargoPrefix = false
        for CargoPrefixId, CargoPrefix in pairs( self.Filter.CargoPrefixes ) do
          --self:T(3( { "Prefix:", string.find( MCargo.Name, CargoPrefix, 1 ), CargoPrefix } )
          if string.find( MCargo.Name, CargoPrefix, 1 ) then
            MCargoPrefix = true
          end
        end
        --self:F( { "Evaluated Prefix", MCargoPrefix } )
        MCargoInclude = MCargoInclude and MCargoPrefix
      end
    end
    
    if self.Filter.Functions and MCargoInclude then
      local MClientFunc = self:_EvalFilterFunctions(MCargo)
      MCargoInclude = MCargoInclude and MClientFunc
    end

    --self:T(2( MCargoInclude )
    return MCargoInclude
  end

  --- (R2.1) Handles the OnEventNewCargo event for the Set.
  -- @param #SET_CARGO self
  -- @param Core.Event#EVENTDATA EventData
  function SET_CARGO:OnEventNewCargo( EventData ) -- R2.1

    --self:F( { "New Cargo", EventData } )

    if EventData.Cargo then
      if EventData.Cargo and self:IsIncludeObject( EventData.Cargo ) then
        self:Add( EventData.Cargo.Name, EventData.Cargo )
      end
    end
  end

  --- (R2.1) Handles the OnDead or OnCrash event for alive units set.
  -- @param #SET_CARGO self
  -- @param Core.Event#EVENTDATA EventData
  function SET_CARGO:OnEventDeleteCargo( EventData ) -- R2.1
    --self:F3( { EventData } )

    if EventData.Cargo then
      local Cargo = _DATABASE:FindCargo( EventData.Cargo.Name )
      if Cargo and Cargo.Name then

        -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD.
        -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call.
        -- And this is a problem because it will remove all entries from the SET_CARGOs.
        -- To prevent this from happening, the Cargo object has a flag NoDestroy.
        -- When true, the SET_CARGO won't Remove the Cargo object from the set.
        -- This flag is switched off after the event handlers have been called in the EVENT class.
        --self:F( { CargoNoDestroy = Cargo.NoDestroy } )
        if Cargo.NoDestroy then
        else
          self:Remove( Cargo.Name )
        end
      end
    end
  end

end

do -- SET_ZONE
  
  ---
  -- @type SET_ZONE
  -- @extends Core.Set#SET_BASE

  --- Mission designers can use the @{Core.Set#SET_ZONE} class to build sets of zones of various types.
  --
  -- ## SET_ZONE constructor
  --
  -- Create a new SET_ZONE object with the @{#SET_ZONE.New} method:
  --
  --    * @{#SET_ZONE.New}: Creates a new SET_ZONE object.
  --
  -- ## Add or Remove ZONEs from SET_ZONE
  --
  -- ZONEs can be added and removed using the @{Core.Set#SET_ZONE.AddZonesByName} and @{Core.Set#SET_ZONE.RemoveZonesByName} respectively.
  -- These methods take a single ZONE name or an array of ZONE names to be added or removed from SET_ZONE.
  --
  -- ## SET_ZONE filter criteria
  --
  -- You can set filter criteria to build the collection of zones in SET_ZONE.
  -- Filter criteria are defined by:
  --
  --    * @{#SET_ZONE.FilterPrefixes}: Builds the SET_ZONE with the zones having a certain text pattern in their name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. 
  -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching)
  --
  -- Once the filter criteria have been set for the SET_ZONE, you can start filtering using:
  --
  --   * @{#SET_ZONE.FilterStart}: Starts the filtering of the zones within the SET_ZONE.
  --
  -- ## SET_ZONE iterators
  --
  -- Once the filters have been defined and the SET_ZONE has been built, you can iterate the SET_ZONE with the available iterator methods.
  -- The iterator methods will walk the SET_ZONE set, and call for each airbase within the set a function that you provide.
  -- The following iterator methods are currently available within the SET_ZONE:
  --
  --   * @{#SET_ZONE.ForEachZone}: Calls a function for each zone it finds within the SET_ZONE.
  --
  -- ===
  -- @field #SET_ZONE SET_ZONE
  SET_ZONE = {
    ClassName = "SET_ZONE",
    Zones = {},
    Filter = {
      Prefixes = nil,
    },
      FilterMeta = {
    },
    Checktime = 5,
  }

  --- Creates a new SET_ZONE object, building a set of zones.
  -- @param #SET_ZONE self
  -- @return #SET_ZONE self
  -- @usage
  -- -- Define a new SET_ZONE Object. The DatabaseSet will contain a reference to all Zones.
  -- DatabaseSet = SET_ZONE:New()
  function SET_ZONE:New()
    -- Inherits from BASE
    local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.ZONES ) )

    return self
  end

  --- Add ZONEs by a search name to SET_ZONE.
  -- @param Core.Set#SET_ZONE self
  -- @param #string AddZoneNames A single name or an array of ZONE_BASE names.
  -- @return self
  function SET_ZONE:AddZonesByName( AddZoneNames )

    local AddZoneNamesArray = (type( AddZoneNames ) == "table") and AddZoneNames or { AddZoneNames }

    for AddAirbaseID, AddZoneName in pairs( AddZoneNamesArray ) do
      self:Add( AddZoneName, ZONE:FindByName( AddZoneName ) )
    end

    return self
  end

  --- Add ZONEs to SET_ZONE.
  -- @param Core.Set#SET_ZONE self
  -- @param Core.Zone#ZONE_BASE Zone A ZONE_BASE object.
  -- @return self
  function SET_ZONE:AddZone( Zone )

    self:Add( Zone:GetName(), Zone )

    return self
  end

  --- Remove ZONEs from SET_ZONE.
  -- @param Core.Set#SET_ZONE self
  -- @param Core.Zone#ZONE_BASE RemoveZoneNames A single name or an array of ZONE_BASE names.
  -- @return self
  function SET_ZONE:RemoveZonesByName( RemoveZoneNames )

    local RemoveZoneNamesArray = (type( RemoveZoneNames ) == "table") and RemoveZoneNames or { RemoveZoneNames }

    for RemoveZoneID, RemoveZoneName in pairs( RemoveZoneNamesArray ) do
      self:Remove( RemoveZoneName )
    end

    return self
  end

  --- Finds a Zone based on the Zone Name.
  -- @param #SET_ZONE self
  -- @param #string ZoneName
  -- @return Core.Zone#ZONE_BASE The found Zone.
  function SET_ZONE:FindZone( ZoneName )

    local ZoneFound = self.Set[ZoneName]
    return ZoneFound
  end

  --- Get a random zone from the set.
  -- @param #SET_ZONE self
  -- @param #number margin Number of tries to find a zone
  -- @return Core.Zone#ZONE_BASE The random Zone.
  -- @return #nil if no zone in the collection.
  function SET_ZONE:GetRandomZone( margin )

    local margin = margin or 100
    if self:Count() ~= 0 then

      local Index = self.Index
      local ZoneFound = nil -- Core.Zone#ZONE_BASE

      -- Loop until a zone has been found.
      -- The :GetZoneMaybe() call will evaluate the probability for the zone to be selected.
      -- If the zone is not selected, then nil is returned by :GetZoneMaybe() and the loop continues!
      local counter = 0
      while (not ZoneFound) or (counter < margin) do
        local ZoneRandom = math.random( 1, #Index )
        ZoneFound = self.Set[Index[ZoneRandom]]:GetZoneMaybe()
        counter = counter + 1
      end

      return ZoneFound
    end

    return nil
  end

  --- Set a zone probability.
  -- @param #SET_ZONE self
  -- @param #string ZoneName The name of the zone.
  function SET_ZONE:SetZoneProbability( ZoneName, ZoneProbability )
    local Zone = self:FindZone( ZoneName )
    Zone:SetZoneProbability( ZoneProbability )
  end

  --- Builds a set of ZONEs that contain the given string in their name.
  -- **ATTENTION!** Bad naming convention as this **does not** filter only **prefixes** but all zones that **contain** the string. 
  -- @param #SET_ZONE self
  -- @param #string Prefixes The string pattern(s) that need to be contained in the zone name. Can also be passed as a `#table` of strings.
  -- @return #SET_ZONE self
  function SET_ZONE:FilterPrefixes( Prefixes )
    if not self.Filter.Prefixes then
      self.Filter.Prefixes = {}
    end
    if type( Prefixes ) ~= "table" then
      Prefixes = { Prefixes }
    end
    for PrefixID, Prefix in pairs( Prefixes ) do
      self.Filter.Prefixes[Prefix] = Prefix
    end
    return self
  end

  --- Starts the filtering.
  -- @param #SET_ZONE self
  -- @return #SET_ZONE self
  function SET_ZONE:FilterStart()

    if _DATABASE then

      -- We initialize the first set.
      for ObjectName, Object in pairs( self.Database ) do
        if self:IsIncludeObject( Object ) then
          self:Add( ObjectName, Object )
        else
          self:RemoveZonesByName( ObjectName )
        end
      end
    end

    self:HandleEvent( EVENTS.NewZone )
    self:HandleEvent( EVENTS.DeleteZone )

    return self
  end

  --- Stops the filtering for the defined collection.
  -- @param #SET_ZONE self
  -- @return #SET_ZONE self
  function SET_ZONE:FilterStop()

    self:UnHandleEvent( EVENTS.NewZone )
    self:UnHandleEvent( EVENTS.DeleteZone )

    return self
  end

  --- Handles the Database to check on an event (birth) that the Object was added in the Database.
  -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event!
  -- @param #SET_ZONE self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the AIRBASE
  -- @return #table The AIRBASE
  function SET_ZONE:AddInDatabase( Event )
    --self:F3( { Event } )

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Handles the Database to check on any event that Object exists in the Database.
  -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
  -- @param #SET_ZONE self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the AIRBASE
  -- @return #table The AIRBASE
  function SET_ZONE:FindInDatabase( Event )
    --self:F3( { Event } )

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Iterate the SET_ZONE and call an iterator function for each ZONE, providing the ZONE and optional parameters.
  -- @param #SET_ZONE self
  -- @param #function IteratorFunction The function that will be called when there is an alive ZONE in the SET_ZONE. The function needs to accept a AIRBASE parameter.
  -- @return #SET_ZONE self
  function SET_ZONE:ForEachZone( IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet() )

    return self
  end

  --- Draw all zones in the set on the F10 map.
  -- @param #SET_ZONE self
  -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All.
  -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red.
  -- @param #number Alpha Transparency [0,1]. Default 1.
  -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value.
  -- @param #number FillAlpha Transparency [0,1]. Default 0.15.
  -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
  -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
  -- @return #SET_ZONE self
  function SET_ZONE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly)
  
    for _,_zone in pairs(self.Set) do
      local zone=_zone --Core.Zone#ZONE
      zone:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly)
    end

    return self
  end
  
  --- Get the average aggregated coordinate of this set of zones.
  -- @param #SET_ZONE self
  -- @return Core.Point#COORDINATE
  function SET_ZONE:GetAverageCoordinate()
    local x,y,z = 0,0,0
    local count = 0
    for _,_zone in pairs(self.Set) do
      local zone=_zone --Core.Zone#ZONE
      local vec3 = zone:GetVec3()
      x = x + vec3.x
      y = y + vec3.y
      z = z + vec3.z
      count = count + 1
    end
    if count > 1 then
      x = x/count
      y = y/count
      z = z/count
    end
    local coord = COORDINATE:New(x,y,z)
    return coord
  end

  --- Private function.
  -- @param #SET_ZONE self
  -- @param Core.Zone#ZONE_BASE MZone
  -- @return #SET_ZONE self
  function SET_ZONE:IsIncludeObject( MZone )
    --self:F2( MZone )

    local MZoneInclude = true

    if MZone then
      local MZoneName = MZone:GetName()

      if self.Filter.Prefixes then
        local MZonePrefix = false
        for ZonePrefixId, ZonePrefix in pairs( self.Filter.Prefixes ) do
          --self:T(2( { "Prefix:", string.find( MZoneName, ZonePrefix, 1 ), ZonePrefix } )
          if string.find( MZoneName, ZonePrefix, 1 ) then
            MZonePrefix = true
          end
        end
        --self:T(( { "Evaluated Prefix", MZonePrefix } )
        MZoneInclude = MZoneInclude and MZonePrefix
      end
    end
    
    if self.Filter.Functions and MZoneInclude then
      local MClientFunc = self:_EvalFilterFunctions(MZone)
      MZoneInclude = MZoneInclude and MClientFunc
    end

    --self:T(2( MZoneInclude )
    return MZoneInclude
  end

  --- Handles the OnEventNewZone event for the Set.
  -- @param #SET_ZONE self
  -- @param Core.Event#EVENTDATA EventData
  function SET_ZONE:OnEventNewZone( EventData ) -- R2.1

    --self:F( { "New Zone", EventData } )

    if EventData.Zone then
      if EventData.Zone and self:IsIncludeObject( EventData.Zone ) then
        self:Add( EventData.Zone.ZoneName, EventData.Zone )
      end
    end
  end

  --- Handles the OnDead or OnCrash event for alive units set.
  -- @param #SET_ZONE self
  -- @param Core.Event#EVENTDATA EventData
  function SET_ZONE:OnEventDeleteZone( EventData ) -- R2.1
    --self:F3( { EventData } )

    if EventData.Zone then
      local Zone = _DATABASE:FindZone( EventData.Zone.ZoneName )
      if Zone and Zone.ZoneName then

        -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD.
        -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call.
        -- And this is a problem because it will remove all entries from the SET_ZONEs.
        -- To prevent this from happening, the Zone object has a flag NoDestroy.
        -- When true, the SET_ZONE won't Remove the Zone object from the set.
        -- This flag is switched off after the event handlers have been called in the EVENT class.
        --self:F( { ZoneNoDestroy = Zone.NoDestroy } )
        if Zone.NoDestroy then
        else
          self:Remove( Zone.ZoneName )
        end
      end
    end
  end

  --- Validate if a coordinate is in one of the zones in the set.
  -- Returns the ZONE object where the coordinate is located.
  -- If zones overlap, the first zone that validates the test is returned.
  -- @param #SET_ZONE self
  -- @param Core.Point#COORDINATE Coordinate The coordinate to be searched.
  -- @return Core.Zone#ZONE_BASE The zone (if any) that validates the coordinate location.
  function SET_ZONE:IsCoordinateInZone( Coordinate )

    for _, Zone in pairs( self:GetSet() ) do
      local Zone = Zone -- Core.Zone#ZONE_BASE
      if Zone:IsCoordinateInZone( Coordinate ) then
        return Zone
      end
    end

    return nil
  end
  
  --- Get the closest zone to a given coordinate.
  -- @param #SET_ZONE self
  -- @param Core.Point#COORDINATE Coordinate The reference coordinate from which the closest zone is determined.
  -- @return Core.Zone#ZONE_BASE The closest zone (if any).
  -- @return #number Distance to ref coordinate in meters.
  function SET_ZONE:GetClosestZone( Coordinate )

    local dmin=math.huge
    local zmin=nil
    for _, Zone in pairs( self:GetSet() ) do
      local Zone = Zone -- Core.Zone#ZONE_BASE
      local d=Zone:Get2DDistance(Coordinate)
      if d<dmin then
        dmin=d
        zmin=Zone
      end
    end

    return zmin, dmin
  end  
  
  --- Set the check time for SET_ZONE:Trigger()
  -- @param #SET_ZONE self
  -- @param #number seconds Check every seconds for objects entering or leaving the zone. Defaults to 5 secs.
  -- @return #SET_ZONE self
  function SET_ZONE:SetCheckTime(seconds)
    self.Checktime = seconds or 5
    return self
  end
  
  --- Start watching if the Object or Objects move into or out of our set of zones.
  -- @param #SET_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Objects Object or Objects to watch, can be of type UNIT, GROUP, CLIENT, or SET\_UNIT, SET\_GROUP, SET\_CLIENT
  -- @return #SET_ZONE self
  -- @usage
  --          -- Create a SET_GROUP and a SET_ZONE for this:
  -- 
  --          local groupset = SET_GROUP:New():FilterPrefixes("Aerial"):FilterStart()
  --          
  --          -- Trigger will check each zone of the SET_ZONE every 5 secs for objects entering or leaving from the groupset
  --          local zoneset = SET_ZONE:New():FilterPrefixes("Target Zone"):FilterOnce():Trigger(groupset)
  --          
  --          -- Draw zones on map so we see what's going on
  --          zoneset:ForEachZone(
  --            function(zone)
  --              zone:DrawZone(-1, {0,1,0}, Alpha, FillColor, FillAlpha, 4, ReadOnly)
  --            end 
  --          )
  --          
  --          -- This FSM function will be called for entering objects
  --          function zoneset:OnAfterEnteredZone(From,Event,To,Controllable,Zone)
  --            MESSAGE:New("Group "..Controllable:GetName() .. " entered zone "..Zone:GetName(),10,"Set Trigger"):ToAll()
  --          end
  --          
  --          -- This FSM function will be called for leaving objects
  --          function zoneset:OnAfterLeftZone(From,Event,To,Controllable,Zone)
  --            MESSAGE:New("Group "..Controllable:GetName() .. " left zone "..Zone:GetName(),10,"Set Trigger"):ToAll()
  --          end
  --          
  --          -- Stop watching after 1 hour
  --          zoneset:__TriggerStop(3600)
  --          -- Call :SetPartlyInside() on any zone (or SET_ZONE) if you want GROUPs to count as inside when any of their units enters even if they are far apart.
  --          -- Make sure to call :SetPartlyInside() before :Trigger()!.
  function SET_ZONE:Trigger(Objects)
    --self:I("Added Set_Zone Trigger")
    self:AddTransition("*","TriggerStart","TriggerRunning")
    self:AddTransition("*","EnteredZone","*")
    self:AddTransition("*","LeftZone","*")
    self:AddTransition("*","TriggerRunCheck","*")
    self:AddTransition("*","TriggerStop","TriggerStopped")
    self:TriggerStart()
    self.checkobjects = Objects
    if UTILS.IsInstanceOf(Objects,"SET_BASE") then
      self.objectset = Objects.Set
    else
      self.objectset = {Objects}
    end
    self:_TriggerCheck(true)
    self:__TriggerRunCheck(self.Checktime)
    return self
    
    ------------------------
    --- Pseudo Functions ---
    ------------------------
    
    --- Triggers the FSM event "TriggerStop". Stops the SET_ZONE Trigger.
    -- @function [parent=#SET_ZONE] TriggerStop
    -- @param #SET_ZONE self
  
    --- Triggers the FSM event "TriggerStop" after a delay. 
    -- @function [parent=#SET_ZONE] __TriggerStop
    -- @param #SET_ZONE self
    -- @param #number delay Delay in seconds.
    
    --- On After "EnteredZone" event. An observed object has entered the zone.
    -- @function [parent=#SET_ZONE] OnAfterEnteredZone
    -- @param #SET_ZONE self
    -- @param #string From From state.
    -- @param #string Event Event.
    -- @param #string To To state.
    -- @param Wrapper.Controllable#CONTROLLABLE Controllable The controllable entering the zone.
    -- @param Core.Zone#ZONE_BASE Zone The zone entered.
  
    --- On After "LeftZone" event. An observed object has left the zone.
    -- @function [parent=#SET_ZONE] OnAfterLeftZone
    -- @param #SET_ZONE self
    -- @param #string From From state.
    -- @param #string Event Event.
    -- @param #string To To state.
    -- @param Wrapper.Controllable#CONTROLLABLE Controllable The controllable leaving the zone.
    -- @param Core.Zone#ZONE_BASE Zone The zone left.
  end
  
  --- Toggle “partly-inside” handling for every zone in the set when those zones are used with :Trigger().
  -- * Call with no argument or **true** → enable for all.  
  -- * Call with **false** → disable again (handy if it was enabled before).
  -- @param #SET_ZONE self
  -- @return #SET_ZONE self
  function SET_ZONE:SetPartlyInside(state)
    for _,Zone in pairs(self.Set) do
        if Zone.SetPartlyInside then
            Zone:SetPartlyInside(state)
        end
    end
    return self
  end
  
  --- (Internal) Check the assigned objects for being in/out of the zone
  -- @param #SET_ZONE self
  -- @param #boolean fromstart If true, do the init of the objects
  -- @return #SET_ZONE self
  function SET_ZONE:_TriggerCheck(fromstart)
    --self:I("_TriggerCheck | FromStart = "..tostring(fromstart))
    if fromstart then
      for _,_object in pairs(self.objectset) do
        local obj = _object -- Wrapper.Controllable#CONTROLLABLE
        if obj and obj:IsAlive() then
          for _,_zone in pairs(self.Set) do
            if not obj.TriggerInZone then obj.TriggerInZone = {} end
            if _zone:IsCoordinateInZone(obj:GetCoordinate()) then
              obj.TriggerInZone[_zone.ZoneName] = true
            else
              obj.TriggerInZone[_zone.ZoneName] = false
            end
            --self:I("Object "..obj:GetName().." is in zone = "..tostring(obj.TriggerInZone[_zone.ZoneName]))
          end
        end
      end
    else
      for _,_object in pairs(self.objectset) do
        local obj = _object -- Wrapper.Controllable#CONTROLLABLE
        if obj and obj:IsAlive() then
          for _,_zone in pairs(self.Set) do
            -- Check for pop-up objects
            if not obj.TriggerInZone then
              -- has not been tagged previously - wasn't in set! 
              obj.TriggerInZone = {}
            end
            if not obj.TriggerInZone[_zone.ZoneName] then
              -- has not been tagged previously - wasn't in set! 
              obj.TriggerInZone[_zone.ZoneName] = false 
            end
            -- is obj in this zone?
            local inzone
            if _zone.PartlyInside and obj.ClassName == "GROUP" then
                inzone = obj:IsAnyInZone(_zone)                 -- TRUE as soon as any unit is inside
            else
                inzone = _zone:IsCoordinateInZone(obj:GetCoordinate())  -- original centroid test
            end
            --self:I("Object "..obj:GetName().." is in zone: "..tostring(inzone))
            if inzone and not obj.TriggerInZone[_zone.ZoneName] then
              -- wasn't in zone before
              --self:I("Newly entered")
              self:__EnteredZone(0.5,obj,_zone)
              obj.TriggerInZone[_zone.ZoneName] = true
            elseif (not inzone) and obj.TriggerInZone[_zone.ZoneName] then
              -- has left the zone
              --self:I("Newly left")
              self:__LeftZone(0.5,obj,_zone)
              obj.TriggerInZone[_zone.ZoneName] = false
            else
              --self:I("Not left or not entered, or something went wrong!")
            end
          end
        end
      end
    end 
    return self
  end
  
  --- (Internal) Check the assigned objects for being in/out of the zone
  -- @param #SET_ZONE self
  -- @param #string From
  -- @param #string Event
  -- @param #string to
  -- @return #SET_ZONE self
  function SET_ZONE:onafterTriggerRunCheck(From,Event,To)
    --self:I("onafterTriggerRunCheck")
    --self:I({From, Event, To})  
    if self:GetState() ~= "TriggerStopped" then
      self:_TriggerCheck()
      self:__TriggerRunCheck(self.Checktime)
    end
    return self
  end
end

do -- SET_ZONE_GOAL
  
  ---
  -- @type SET_ZONE_GOAL
  -- @extends Core.Set#SET_BASE

  --- Mission designers can use the @{Core.Set#SET_ZONE_GOAL} class to build sets of zones of various types.
  --
  -- ## SET_ZONE_GOAL constructor
  --
  -- Create a new SET_ZONE_GOAL object with the @{#SET_ZONE_GOAL.New} method:
  --
  --    * @{#SET_ZONE_GOAL.New}: Creates a new SET_ZONE_GOAL object.
  --
  -- ## Add or Remove ZONEs from SET_ZONE_GOAL
  --
  -- ZONEs can be added and removed using the @{Core.Set#SET_ZONE_GOAL.AddZonesByName} and @{Core.Set#SET_ZONE_GOAL.RemoveZonesByName} respectively.
  -- These methods take a single ZONE name or an array of ZONE names to be added or removed from SET_ZONE_GOAL.
  --
  -- ## SET_ZONE_GOAL filter criteria
  --
  -- You can set filter criteria to build the collection of zones in SET_ZONE_GOAL.
  -- Filter criteria are defined by:
  --
  --    * @{#SET_ZONE_GOAL.FilterPrefixes}: Builds the SET_ZONE_GOAL with the zones having a certain text pattern in their name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. 
  -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching)
  --
  -- Once the filter criteria have been set for the SET_ZONE_GOAL, you can start filtering using:
  --
  --   * @{#SET_ZONE_GOAL.FilterStart}: Starts the filtering of the zones within the SET_ZONE_GOAL.
  --
  -- ## SET_ZONE_GOAL iterators
  --
  -- Once the filters have been defined and the SET_ZONE_GOAL has been built, you can iterate the SET_ZONE_GOAL with the available iterator methods.
  -- The iterator methods will walk the SET_ZONE_GOAL set, and call for each airbase within the set a function that you provide.
  -- The following iterator methods are currently available within the SET_ZONE_GOAL:
  --
  --   * @{#SET_ZONE_GOAL.ForEachZone}: Calls a function for each zone it finds within the SET_ZONE_GOAL.
  --
  -- ===
  -- @field #SET_ZONE_GOAL SET_ZONE_GOAL
  SET_ZONE_GOAL = {
    ClassName = "SET_ZONE_GOAL",
    Zones = {},
    Filter = {
      Prefixes = nil,
    },
      FilterMeta = {
    },
  }

  --- Creates a new SET_ZONE_GOAL object, building a set of zones.
  -- @param #SET_ZONE_GOAL self
  -- @return #SET_ZONE_GOAL self
  -- @usage
  -- -- Define a new SET_ZONE_GOAL Object. The DatabaseSet will contain a reference to all Zones.
  -- DatabaseSet = SET_ZONE_GOAL:New()
  function SET_ZONE_GOAL:New()
    -- Inherits from BASE
    local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.ZONES_GOAL ) )

    return self
  end

  --- Add ZONEs to SET_ZONE_GOAL.
  -- @param Core.Set#SET_ZONE_GOAL self
  -- @param Core.Zone#ZONE_BASE Zone A ZONE_BASE object.
  -- @return self
  function SET_ZONE_GOAL:AddZone( Zone )

    self:Add( Zone:GetName(), Zone )

    return self
  end

  --- Remove ZONEs from SET_ZONE_GOAL.
  -- @param Core.Set#SET_ZONE_GOAL self
  -- @param Core.Zone#ZONE_BASE RemoveZoneNames A single name or an array of ZONE_BASE names.
  -- @return self
  function SET_ZONE_GOAL:RemoveZonesByName( RemoveZoneNames )

    local RemoveZoneNamesArray = (type( RemoveZoneNames ) == "table") and RemoveZoneNames or { RemoveZoneNames }

    for RemoveZoneID, RemoveZoneName in pairs( RemoveZoneNamesArray ) do
      self:Remove( RemoveZoneName )
    end

    return self
  end

  --- Finds a Zone based on the Zone Name.
  -- @param #SET_ZONE_GOAL self
  -- @param #string ZoneName
  -- @return Core.Zone#ZONE_BASE The found Zone.
  function SET_ZONE_GOAL:FindZone( ZoneName )

    local ZoneFound = self.Set[ZoneName]
    return ZoneFound
  end

  --- Get a random zone from the set.
  -- @param #SET_ZONE_GOAL self
  -- @return Core.Zone#ZONE_BASE The random Zone.
  -- @return #nil if no zone in the collection.
  function SET_ZONE_GOAL:GetRandomZone()

    if self:Count() ~= 0 then

      local Index = self.Index
      local ZoneFound = nil -- Core.Zone#ZONE_BASE

      -- Loop until a zone has been found.
      -- The :GetZoneMaybe() call will evaluate the probability for the zone to be selected.
      -- If the zone is not selected, then nil is returned by :GetZoneMaybe() and the loop continues!
      while not ZoneFound do
        local ZoneRandom = math.random( 1, #Index )
        ZoneFound = self.Set[Index[ZoneRandom]]:GetZoneMaybe()
      end

      return ZoneFound
    end

    return nil
  end

  --- Set a zone probability.
  -- @param #SET_ZONE_GOAL self
  -- @param #string ZoneName The name of the zone.
  function SET_ZONE_GOAL:SetZoneProbability( ZoneName, ZoneProbability )
    local Zone = self:FindZone( ZoneName )
    Zone:SetZoneProbability( ZoneProbability )
  end

  --- Builds a set of ZONE_GOALs that contain the given string in their name.
  -- **ATTENTION!** Bad naming convention as this **does not** filter only **prefixes** but all zones that **contain** the string. 
  -- @param #SET_ZONE_GOAL self
  -- @param #string Prefixes The string pattern(s) that needs to be contained in the zone name. Can also be passed as a `#table` of strings.
  -- @return #SET_ZONE_GOAL self
  function SET_ZONE_GOAL:FilterPrefixes( Prefixes )
    if not self.Filter.Prefixes then
      self.Filter.Prefixes = {}
    end
    if type( Prefixes ) ~= "table" then
      Prefixes = { Prefixes }
    end
    for PrefixID, Prefix in pairs( Prefixes ) do
      self.Filter.Prefixes[Prefix] = Prefix
    end
    return self
  end

  --- Starts the filtering.
  -- @param #SET_ZONE_GOAL self
  -- @return #SET_ZONE_GOAL self
  function SET_ZONE_GOAL:FilterStart()

    if _DATABASE then

      -- We initialize the first set.
      for ObjectName, Object in pairs( self.Database ) do
        if self:IsIncludeObject( Object ) then
          self:Add( ObjectName, Object )
        else
          self:RemoveZonesByName( ObjectName )
        end
      end
    end

    self:HandleEvent( EVENTS.NewZoneGoal )
    self:HandleEvent( EVENTS.DeleteZoneGoal )

    return self
  end

  --- Stops the filtering for the defined collection.
  -- @param #SET_ZONE_GOAL self
  -- @return #SET_ZONE_GOAL self
  function SET_ZONE_GOAL:FilterStop()

    self:UnHandleEvent( EVENTS.NewZoneGoal )
    self:UnHandleEvent( EVENTS.DeleteZoneGoal )

    return self
  end

  --- Handles the Database to check on an event (birth) that the Object was added in the Database.
  -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event!
  -- @param #SET_ZONE_GOAL self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the AIRBASE
  -- @return #table The AIRBASE
  function SET_ZONE_GOAL:AddInDatabase( Event )
    --self:F3( { Event } )

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Handles the Database to check on any event that Object exists in the Database.
  -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
  -- @param #SET_ZONE_GOAL self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the AIRBASE
  -- @return #table The AIRBASE
  function SET_ZONE_GOAL:FindInDatabase( Event )
    --self:F3( { Event } )

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Iterate the SET_ZONE_GOAL and call an iterator function for each ZONE, providing the ZONE and optional parameters.
  -- @param #SET_ZONE_GOAL self
  -- @param #function IteratorFunction The function that will be called when there is an alive ZONE in the SET_ZONE_GOAL. The function needs to accept a AIRBASE parameter.
  -- @return #SET_ZONE_GOAL self
  function SET_ZONE_GOAL:ForEachZone( IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet() )

    return self
  end

  ---
  -- @param #SET_ZONE_GOAL self
  -- @param Core.Zone#ZONE_BASE MZone
  -- @return #SET_ZONE_GOAL self
  function SET_ZONE_GOAL:IsIncludeObject( MZone )
    --self:F2( MZone )

    local MZoneInclude = true

    if MZone then
      local MZoneName = MZone:GetName()

      if self.Filter.Prefixes then
        local MZonePrefix = false
        for ZonePrefixId, ZonePrefix in pairs( self.Filter.Prefixes ) do
          --self:T(3( { "Prefix:", string.find( MZoneName, ZonePrefix, 1 ), ZonePrefix } )
          if string.find( MZoneName, ZonePrefix, 1 ) then
            MZonePrefix = true
          end
        end
        --self:T(( { "Evaluated Prefix", MZonePrefix } )
        MZoneInclude = MZoneInclude and MZonePrefix
      end
    end
    
    if self.Filter.Functions and MZoneInclude then
      local MClientFunc = self:_EvalFilterFunctions(MZone)
      MZoneInclude = MZoneInclude and MClientFunc
    end

    --self:T(2( MZoneInclude )
    return MZoneInclude
  end

  --- Handles the OnEventNewZone event for the Set.
  -- @param #SET_ZONE_GOAL self
  -- @param Core.Event#EVENTDATA EventData
  function SET_ZONE_GOAL:OnEventNewZoneGoal( EventData )

    -- Debug info.
    --self:T(( { "New Zone Capture Coalition", EventData } )
    --self:T(( { "Zone Capture Coalition", EventData.ZoneGoal } )

    if EventData.ZoneGoal then
      if EventData.ZoneGoal and self:IsIncludeObject( EventData.ZoneGoal ) then
        --self:T(( { "Adding Zone Capture Coalition", EventData.ZoneGoal.ZoneName, EventData.ZoneGoal } )
        self:Add( EventData.ZoneGoal.ZoneName, EventData.ZoneGoal )
      end
    end
  end

  --- Handles the OnDead or OnCrash event for alive units set.
  -- @param #SET_ZONE_GOAL self
  -- @param Core.Event#EVENTDATA EventData
  function SET_ZONE_GOAL:OnEventDeleteZoneGoal( EventData ) -- R2.1
    --self:F3( { EventData } )

    if EventData.ZoneGoal then
      local Zone = _DATABASE:FindZone( EventData.ZoneGoal.ZoneName )
      if Zone and Zone.ZoneName then

        -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD.
        -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call.
        -- And this is a problem because it will remove all entries from the SET_ZONE_GOALs.
        -- To prevent this from happening, the Zone object has a flag NoDestroy.
        -- When true, the SET_ZONE_GOAL won't Remove the Zone object from the set.
        -- This flag is switched off after the event handlers have been called in the EVENT class.
        --self:F( { ZoneNoDestroy = Zone.NoDestroy } )
        if Zone.NoDestroy then
        else
          self:Remove( Zone.ZoneName )
        end
      end
    end
  end

  --- Validate if a coordinate is in one of the zones in the set.
  -- Returns the ZONE object where the coordiante is located.
  -- If zones overlap, the first zone that validates the test is returned.
  -- @param #SET_ZONE_GOAL self
  -- @param Core.Point#COORDINATE Coordinate The coordinate to be searched.
  -- @return Core.Zone#ZONE_BASE The zone that validates the coordinate location.
  -- @return #nil No zone has been found.
  function SET_ZONE_GOAL:IsCoordinateInZone( Coordinate )

    for _, Zone in pairs( self:GetSet() ) do
      local Zone = Zone -- Core.Zone#ZONE_BASE
      if Zone:IsCoordinateInZone( Coordinate ) then
        return Zone
      end
    end

    return nil
  end

end

do -- SET_OPSZONE
  
  ---
  -- @type SET_OPSZONE
  -- @extends Core.Set#SET_BASE

  --- Mission designers can use the @{Core.Set#SET_OPSZONE} class to build sets of zones of various types.
  --
  -- ## SET_OPSZONE constructor
  --
  -- Create a new SET_OPSZONE object with the @{#SET_OPSZONE.New} method:
  --
  --    * @{#SET_OPSZONE.New}: Creates a new SET_OPSZONE object.
  --
  -- ## Add or Remove ZONEs from SET_OPSZONE
  --
  -- ZONEs can be added and removed using the @{Core.Set#SET_OPSZONE.AddZonesByName} and @{Core.Set#SET_OPSZONE.RemoveZonesByName} respectively.
  -- These methods take a single ZONE name or an array of ZONE names to be added or removed from SET_OPSZONE.
  --
  -- ## SET_OPSZONE filter criteria
  --
  -- You can set filter criteria to build the collection of zones in SET_OPSZONE.
  -- Filter criteria are defined by:
  --
  --    * @{#SET_OPSZONE.FilterPrefixes}: Builds the SET_OPSZONE with the zones having a certain text pattern in their name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. 
  -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching)
  --
  -- Once the filter criteria have been set for the SET_OPSZONE, you can start filtering using:
  --
  --   * @{#SET_OPSZONE.FilterStart}: Starts the filtering of the zones within the SET_OPSZONE.
  --
  -- ## SET_OPSZONE iterators
  --
  -- Once the filters have been defined and the SET_OPSZONE has been built, you can iterate the SET_OPSZONE with the available iterator methods.
  -- The iterator methods will walk the SET_OPSZONE set, and call for each airbase within the set a function that you provide.
  -- The following iterator methods are currently available within the SET_OPSZONE:
  --
  --   * @{#SET_OPSZONE.ForEachZone}: Calls a function for each zone it finds within the SET_OPSZONE.
  --
  -- ===
  -- @field #SET_OPSZONE SET_OPSZONE
  SET_OPSZONE = {
    ClassName = "SET_OPSZONE",
    Zones = {},
    Filter = {
      Prefixes   = nil,
      Coalitions = nil,      
    },
    FilterMeta = {
      Coalitions = {
        red     = coalition.side.RED,
        blue    = coalition.side.BLUE,
        neutral = coalition.side.NEUTRAL,
      },
    }, --FilterMeta
  }

  --- Creates a new SET_OPSZONE object, building a set of zones.
  -- @param #SET_OPSZONE self
  -- @return #SET_OPSZONE self
  function SET_OPSZONE:New()
  
    -- Inherits from BASE
    local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.OPSZONES ) )

    return self
  end

  --- Add an OPSZONE to set.
  -- @param Core.Set#SET_OPSZONE self
  -- @param Ops.OpsZone#OPSZONE Zone The OPSZONE object.
  -- @return #SET_OPSZONE self
  function SET_OPSZONE:AddZone( Zone )

    self:Add( Zone:GetName(), Zone )

    return self
  end 

  --- Remove ZONEs from SET_OPSZONE.
  -- @param Core.Set#SET_OPSZONE self
  -- @param #table RemoveZoneNames A single name or an array of OPSZONE names.
  -- @return #SET_OPSZONE  self
  function SET_OPSZONE:RemoveZonesByName( RemoveZoneNames )

    local RemoveZoneNamesArray = (type( RemoveZoneNames ) == "table") and RemoveZoneNames or { RemoveZoneNames }
    
    --UTILS.EnsureTable(Object,ReturnNil)

    for RemoveZoneID, RemoveZoneName in pairs( RemoveZoneNamesArray ) do
      self:Remove( RemoveZoneName )
    end

    return self
  end

  --- Finds a Zone based on its name.
  -- @param #SET_OPSZONE self
  -- @param #string ZoneName
  -- @return Ops.OpsZone#OPSZONE The found Zone.
  function SET_OPSZONE:FindZone( ZoneName )

    local ZoneFound = self.Set[ZoneName]
    
    return ZoneFound
  end

  --- Get a random zone from the set.
  -- @param #SET_OPSZONE self
  -- @return Ops.OpsZone#OPSZONE The random Zone.
  function SET_OPSZONE:GetRandomZone()

    if self:Count() ~= 0 then

      local Index = self.Index
      local ZoneFound = nil -- Core.Zone#ZONE_BASE

      -- Loop until a zone has been found.
      -- The :GetZoneMaybe() call will evaluate the probability for the zone to be selected.
      -- If the zone is not selected, then nil is returned by :GetZoneMaybe() and the loop continues!
      while not ZoneFound do
        local ZoneRandom = math.random( 1, #Index )
        ZoneFound = self.Set[Index[ZoneRandom]]:GetZoneMaybe()
      end

      return ZoneFound
    end

    return nil
  end

  --- Set a zone probability.
  -- @param #SET_OPSZONE self
  -- @param #string ZoneName The name of the zone.
  -- @param #number Probability The probability in percent.
  function SET_OPSZONE:SetZoneProbability( ZoneName, Probability )
    local Zone = self:FindZone( ZoneName )
    Zone:SetZoneProbability( Probability )
    return self
  end

  --- Builds a set of OPSZONEs that contain the given string in their name.
  -- **ATTENTION!** Bad naming convention as this **does not** filter only **prefixes** but all zones that **contain** the string. 
  -- @param #SET_OPSZONE self
  -- @param #string Prefixes The string pattern(s) that needs to be contained in the zone name. Can also be passed as a `#table` of strings.
  -- @return #SET_OPSZONE self
  function SET_OPSZONE:FilterPrefixes( Prefixes )
  
    if not self.Filter.Prefixes then
      self.Filter.Prefixes = {}
    end
    
    Prefixes=UTILS.EnsureTable(Prefixes, false)
    
    for PrefixID, Prefix in pairs( Prefixes ) do
      self.Filter.Prefixes[Prefix] = Prefix
    end
    
    return self
  end
  
  --- Builds a set of groups of coalitions. Possible current coalitions are red, blue and neutral.
  -- @param #SET_OPSZONE self
  -- @param #string Coalitions Can take the following values: "red", "blue", "neutral" or combinations as a table, for example `{"red", "neutral"}`.
  -- @return #SET_OPSZONE self
  function SET_OPSZONE:FilterCoalitions(Coalitions)
  
    -- Create an empty set.
    if not self.Filter.Coalitions then
      self.Filter.Coalitions={}
    end
    
    -- Ensure we got a table.
    Coalitions=UTILS.EnsureTable(Coalitions, false)
    
    -- Set filter.
    for CoalitionID, Coalition in pairs( Coalitions ) do
      self.Filter.Coalitions[Coalition] = Coalition
    end
    
    return self
  end  

  --- Filters for the defined collection.
  -- @param #SET_OPSZONE self
  -- @return #SET_OPSZONE self
  function SET_OPSZONE:FilterOnce()

    for ObjectName, Object in pairs( self.Database ) do
    
      -- First remove the object (without creating an event).
      self:Remove(ObjectName, true)

      if self:IsIncludeObject( Object ) then
        self:Add( ObjectName, Object )
      end
      
    end

    return self
  end
  
  --- Clear all filters. You still need to apply `FilterOnce()` to have an effect on the set.
  -- @param #SET_OPSZONE self
  -- @return #SET_OPSZONE self
  function SET_OPSZONE:FilterClear()
  
    local parent=self:GetParent(self, SET_OPSZONE) --#SET_BASE

    parent:FilterClear()

    return self
  end  


  --- Starts the filtering.
  -- @param #SET_OPSZONE self
  -- @return #SET_OPSZONE self
  function SET_OPSZONE:FilterStart()

    if _DATABASE then

      -- We initialize the first set.
      for ObjectName, Object in pairs( self.Database ) do
        if self:IsIncludeObject( Object ) then
          self:Add( ObjectName, Object )
        else
          self:RemoveZonesByName( ObjectName )
        end
      end
    end

    self:HandleEvent( EVENTS.NewZoneGoal )
    self:HandleEvent( EVENTS.DeleteZoneGoal )

    return self
  end

  --- Stops the filtering for the defined collection.
  -- @param #SET_OPSZONE self
  -- @return #SET_OPSZONE self
  function SET_OPSZONE:FilterStop()

    self:UnHandleEvent( EVENTS.NewZoneGoal )
    self:UnHandleEvent( EVENTS.DeleteZoneGoal )

    return self
  end

  --- Handles the Database to check on an event (birth) that the Object was added in the Database.
  -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event!
  -- @param #SET_OPSZONE self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the AIRBASE
  -- @return #table The AIRBASE
  function SET_OPSZONE:AddInDatabase( Event )
    --self:F3( { Event } )

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Handles the Database to check on any event that Object exists in the Database.
  -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
  -- @param #SET_OPSZONE self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the AIRBASE
  -- @return #table The AIRBASE
  function SET_OPSZONE:FindInDatabase( Event )
    --self:F3( { Event } )

    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end

  --- Iterate the SET_OPSZONE and call an iterator function for each ZONE, providing the ZONE and optional parameters.
  -- @param #SET_OPSZONE self
  -- @param #function IteratorFunction The function that will be called when there is an alive ZONE in the SET_OPSZONE. The function needs to accept a AIRBASE parameter.
  -- @return #SET_OPSZONE self
  function SET_OPSZONE:ForEachZone( IteratorFunction, ... )
    --self:F2( arg )

    self:ForEach( IteratorFunction, arg, self:GetSet() )

    return self
  end

  --- Private function that checks if an object is contained in the set or filtered.
  -- @param #SET_OPSZONE self
  -- @param Ops.OpsZone#OPSZONE MZone The OPSZONE object.
  -- @return #SET_OPSZONE self
  function SET_OPSZONE:IsIncludeObject( MZone )
    --self:F2( MZone )

    local MZoneInclude = true

    if MZone then
    
      local MZoneName = MZone:GetName()

      if self.Filter.Prefixes then
      
        local MZonePrefix = false
        
        -- Loop over prefixes.
        for ZonePrefixId, ZonePrefix in pairs( self.Filter.Prefixes ) do
        
          -- Prifix
          --self:T(3( { "Prefix:", string.find( MZoneName, ZonePrefix, 1 ), ZonePrefix } )
          
          if string.find(MZoneName, ZonePrefix, 1) then
            MZonePrefix = true
            break --Break the loop as we found the prefix.
          end
          
        end
        
        --self:T(( { "Evaluated Prefix", MZonePrefix } )
        
        MZoneInclude = MZoneInclude and MZonePrefix
      end
    
      -- Filter coalitions.
      if self.Filter.Coalitions then
      
        local MGroupCoalition = false
        
        local coalition=MZone:GetOwner()
        
        for _, CoalitionName in pairs( self.Filter.Coalitions ) do
        
          if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName]==coalition then
            MGroupCoalition = true
            break -- Break the loop the coalition is contains.      
          end
          
        end
      
        MZoneInclude = MZoneInclude and MGroupCoalition
      end
    
    end    
    
    if self.Filter.Functions and MZoneInclude then
      local MClientFunc = self:_EvalFilterFunctions(MZone)
      MZoneInclude = MZoneInclude and MClientFunc
    end
    
    --self:T(2( MZoneInclude )
    return MZoneInclude
  end

  --- Handles the OnEventNewZone event for the Set.
  -- @param #SET_OPSZONE self
  -- @param Core.Event#EVENTDATA EventData
  function SET_OPSZONE:OnEventNewZoneGoal( EventData )

    -- Debug info.
    --self:T(( { "New Zone Capture Coalition", EventData } )
    --self:T(( { "Zone Capture Coalition", EventData.ZoneGoal } )

    if EventData.ZoneGoal then
      if EventData.ZoneGoal and self:IsIncludeObject( EventData.ZoneGoal ) then
        --self:T(( { "Adding Zone Capture Coalition", EventData.ZoneGoal.ZoneName, EventData.ZoneGoal } )
        self:Add( EventData.ZoneGoal.ZoneName, EventData.ZoneGoal )
      end
    end
  end

  --- Handles the OnDead or OnCrash event for alive units set.
  -- @param #SET_OPSZONE self
  -- @param Core.Event#EVENTDATA EventData
  function SET_OPSZONE:OnEventDeleteZoneGoal( EventData ) -- R2.1
    --self:F3( { EventData } )

    if EventData.ZoneGoal then
      local Zone = _DATABASE:FindZone( EventData.ZoneGoal.ZoneName )
      if Zone and Zone.ZoneName then

        -- When cargo was deleted, it may probably be because of an S_EVENT_DEAD.
        -- However, in the loading logic, an S_EVENT_DEAD is also generated after a Destroy() call.
        -- And this is a problem because it will remove all entries from the SET_OPSZONEs.
        -- To prevent this from happening, the Zone object has a flag NoDestroy.
        -- When true, the SET_OPSZONE won't Remove the Zone object from the set.
        -- This flag is switched off after the event handlers have been called in the EVENT class.
        --self:F( { ZoneNoDestroy = Zone.NoDestroy } )
        if Zone.NoDestroy then
        else
          self:Remove( Zone.ZoneName )
        end
      end
    end
  end
  
  --- Start all opszones of the set.
  -- @param #SET_OPSZONE self
  -- @return #SET_OPSZONE self
  function SET_OPSZONE:Start()

    for _,_Zone in pairs( self:GetSet() ) do
      local Zone = _Zone --Ops.OpsZone#OPSZONE
      if Zone:IsStopped() then
        Zone:Start()
      end
    end  

    return self
  end  

  --- Validate if a coordinate is in one of the zones in the set.
  -- Returns the ZONE object where the coordiante is located.
  -- If zones overlap, the first zone that validates the test is returned.
  -- @param #SET_OPSZONE self
  -- @param Core.Point#COORDINATE Coordinate The coordinate to be searched.
  -- @return Core.Zone#ZONE_BASE The zone that validates the coordinate location.
  -- @return #nil No zone has been found.
  function SET_OPSZONE:IsCoordinateInZone( Coordinate )

    for _,_Zone in pairs( self:GetSet() ) do
      local Zone = _Zone --Ops.OpsZone#OPSZONE
      if Zone:GetZone():IsCoordinateInZone( Coordinate ) then
        return Zone
      end
    end

    return nil
  end


  --- Get the closest OPSZONE from a given reference coordinate. Only started zones are considered.
  -- @param #SET_OPSZONE self
  -- @param Core.Point#COORDINATE Coordinate The reference coordinate from which the closest zone is determined.
  -- @param #table Coalitions Only consider the given coalition(s), *e.g.* `{coaliton.side.RED}` to find the closest red zone.
  -- @return Ops.OpsZone#OPSZONE The closest OPSZONE (if any).
  -- @return #number Distance to ref coordinate in meters.
  function SET_OPSZONE:GetClosestZone( Coordinate, Coalitions )
   
    Coalitions=UTILS.EnsureTable(Coalitions, true)

    local dmin=math.huge --#number
    local zmin=nil       --Ops.OpsZone#OPSZONE
    
    for _,_opszone in pairs(self:GetSet()) do
      local opszone=_opszone --Ops.OpsZone#OPSZONE
            
      local coal=opszone:GetOwner()
      
      if opszone:IsStarted() and (Coalitions==nil or (Coalitions and UTILS.IsInTable(Coalitions, coal))) then
      
        -- Get 2D distance.
        local d=opszone:GetZone():Get2DDistance(Coordinate)
        
        if d<dmin then        
          dmin=d
          zmin=opszone
        end
        
      end
    end

    return zmin, dmin
  end  

end


do -- SET_OPSGROUP
  
  ---
  -- @type SET_OPSGROUP
  -- @extends Core.Set#SET_BASE

  --- Mission designers can use the @{Core.Set#SET_OPSGROUP} class to build sets of OPS groups belonging to certain:
  --
  --  * Coalitions
  --  * Categories
  --  * Countries
  --  * Contain a certain string pattern
  --
  -- ## SET_OPSGROUP constructor
  --
  -- Create a new SET_OPSGROUP object with the @{#SET_OPSGROUP.New} method:
  --
  --    * @{#SET_OPSGROUP.New}: Creates a new SET_OPSGROUP object.
  --
  -- ## Add or Remove GROUP(s) from SET_OPSGROUP
  --
  -- GROUPS can be added and removed using the @{Core.Set#SET_OPSGROUP.AddGroupsByName} and @{Core.Set#SET_OPSGROUP.RemoveGroupsByName} respectively.
  -- These methods take a single GROUP name or an array of GROUP names to be added or removed from SET_OPSGROUP.
  --
  -- ## SET_OPSGROUP filter criteria
  --
  -- You can set filter criteria to define the set of groups within the SET_OPSGROUP.
  -- Filter criteria are defined by:
  --
  --    * @{#SET_OPSGROUP.FilterCoalitions}: Builds the SET_OPSGROUP with the groups belonging to the coalition(s).
  --    * @{#SET_OPSGROUP.FilterCategories}: Builds the SET_OPSGROUP with the groups belonging to the category(ies).
  --    * @{#SET_OPSGROUP.FilterCountries}: Builds the SET_OPSGROUP with the groups belonging to the country(ies).
  --    * @{#SET_OPSGROUP.FilterPrefixes}: Builds the SET_OPSGROUP with the groups *containing* the given string in the group name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. 
  -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching)
  --    * @{#SET_OPSGROUP.FilterActive}: Builds the SET_OPSGROUP with the groups that are only active. Groups that are inactive (late activation) won't be included in the set!
  --
  -- For the Category Filter, extra methods have been added:
  --
  --    * @{#SET_OPSGROUP.FilterCategoryAirplane}: Builds the SET_OPSGROUP from airplanes.
  --    * @{#SET_OPSGROUP.FilterCategoryHelicopter}: Builds the SET_OPSGROUP from helicopters.
  --    * @{#SET_OPSGROUP.FilterCategoryGround}: Builds the SET_OPSGROUP from ground vehicles or infantry.
  --    * @{#SET_OPSGROUP.FilterCategoryShip}: Builds the SET_OPSGROUP from ships.
  --
  --
  -- Once the filter criteria have been set for the SET_OPSGROUP, you can start filtering using:
  --
  --    * @{#SET_OPSGROUP.FilterStart}: Starts the filtering of the groups within the SET_OPSGROUP and add or remove GROUP objects **dynamically**.
  --    * @{#SET_OPSGROUP.FilterOnce}: Filters of the groups **once**.
  --
  --
  -- ## SET_OPSGROUP iterators
  --
  -- Once the filters have been defined and the SET_OPSGROUP has been built, you can iterate the SET_OPSGROUP with the available iterator methods.
  -- The iterator methods will walk the SET_OPSGROUP set, and call for each element within the set a function that you provide.
  -- The following iterator methods are currently available within the SET_OPSGROUP:
  --
  --   * @{#SET_OPSGROUP.ForEachGroup}: Calls a function for each alive group it finds within the SET_OPSGROUP.
  --
  -- ## SET_OPSGROUP trigger events on the GROUP objects.
  --
  -- The SET is derived from the FSM class, which provides extra capabilities to track the contents of the GROUP objects in the SET_OPSGROUP.
  --
  -- ### When a GROUP object crashes or is dead, the SET_OPSGROUP will trigger a **Dead** event.
  --
  -- You can handle the event using the OnBefore and OnAfter event handlers.
  -- The event handlers need to have the parameters From, Event, To, GroupObject.
  -- The GroupObject is the GROUP object that is dead and within the SET_OPSGROUP, and is passed as a parameter to the event handler.
  -- See the following example:
  --
  --        -- Create the SetCarrier SET_OPSGROUP collection.
  --
  --        local SetHelicopter = SET_OPSGROUP:New():FilterPrefixes( "Helicopter" ):FilterStart()
  --
  --        -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset.
  --
  --        function SetHelicopter:OnAfterDead( From, Event, To, GroupObject )
  --          --self:F( { GroupObject = GroupObject:GetName() } )
  --        end
  --
  --
  -- ===
  -- 
  -- @field #SET_OPSGROUP SET_OPSGROUP
  -- 
  SET_OPSGROUP = {
    ClassName = "SET_OPSGROUP",
    Filter = {
      Coalitions    = nil,
      Categories    = nil,
      Countries     = nil,
      GroupPrefixes = nil,
    },
    FilterMeta = {
      Coalitions = {
        red     = coalition.side.RED,
        blue    = coalition.side.BLUE,
        neutral = coalition.side.NEUTRAL,
      },
      Categories = {
        plane      = Group.Category.AIRPLANE,
        helicopter = Group.Category.HELICOPTER,
        ground     = Group.Category.GROUND,
        ship       = Group.Category.SHIP,
      },
    }, -- FilterMeta
  }


  --- Creates a new SET_OPSGROUP object, building a set of groups belonging to a coalitions, categories, countries, types or with defined prefix names.
  -- @param #SET_OPSGROUP self
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:New()

    -- Inherit SET_BASE.
    local self = BASE:Inherit(self, SET_BASE:New(_DATABASE.GROUPS)) -- #SET_OPSGROUP

    -- Include non activated 
    self:FilterActive( false )

    return self
  end

  --- Gets a **new** set that only contains alive groups.
  -- @param #SET_OPSGROUP self
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:GetAliveSet()

    local AliveSet = SET_OPSGROUP:New()

    -- Clean the Set before returning with only the alive Groups.
    for GroupName, GroupObject in pairs(self.Set) do    
      local GroupObject=GroupObject --Wrapper.Group#GROUP
      
      if GroupObject and GroupObject:IsAlive() then      
        AliveSet:Add(GroupName, GroupObject)
      end
    end

    return AliveSet.Set or {}
  end
  
  --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using a given ObjectName as the index.
  -- @param #SET_BASE self
  -- @param #string ObjectName The name of the object.
  -- @param Core.Base#BASE Object The object itself.
  -- @return Core.Base#BASE The added BASE Object.
  function SET_OPSGROUP:Add(ObjectName, Object)
    --self:T(( { ObjectName = ObjectName, Object = Object } )

    -- Ensure that the existing element is removed from the Set before a new one is inserted to the Set
    if self.Set[ObjectName] then
      self:Remove(ObjectName, true)
    end
    
    local object=nil --Ops.OpsGroup#OPSGROUP
    if Object:IsInstanceOf("GROUP") then
    
      ---
      -- GROUP Object
      ---
    
      -- Fist, look up in the DATABASE if an OPSGROUP already exists.
      object=_DATABASE:FindOpsGroup(ObjectName)
    
      if not object then
      
        if Object:IsShip() then
          object=NAVYGROUP:New(Object)
        elseif Object:IsGround() then
          object=ARMYGROUP:New(Object)
        elseif Object:IsAir() then
          object=FLIGHTGROUP:New(Object)
        else
          env.error("ERROR: Unknown category of group object!")
        end
      end
      
    elseif Object:IsInstanceOf("OPSGROUP") then
      -- We already have an OPSGROUP.
      object=Object
    else
      env.error("ERROR: Object must be a GROUP or OPSGROUP!")
    end
    
    -- Add object to set.
    self.Set[ObjectName]=object

    -- Add Object name to Index.
    table.insert(self.Index, ObjectName)

    -- Trigger Added event.
    self:Added(ObjectName, object)
  end
  
  --- Adds a @{Core.Base#BASE} object in the @{Core.Set#SET_BASE}, using the Object Name as the index.
  -- @param #SET_BASE self
  -- @param Ops.OpsGroup#OPSGROUP Object Ops group
  -- @return Core.Base#BASE The added BASE Object.
  function SET_OPSGROUP:AddObject(Object)
    self:Add(Object.groupname, Object)
  end  

  --- Add a GROUP or OPSGROUP object to the set.
  -- **NOTE** that an OPSGROUP is automatically created from the GROUP if it does not exist already.
  -- @param Core.Set#SET_OPSGROUP self
  -- @param Wrapper.Group#GROUP group The GROUP which should be added to the set. Can also be given as an #OPSGROUP object.
  -- @return Core.Set#SET_OPSGROUP self
  function SET_OPSGROUP:AddGroup(group)
  
    local groupname=group:GetName()
    
    self:Add(groupname, group )

    return self
  end

  --- Add GROUP(s) or OPSGROUP(s) to the set.
  -- @param Core.Set#SET_OPSGROUP self
  -- @param #string AddGroupNames A single name or an array of GROUP names.
  -- @return Core.Set#SET_OPSGROUP self
  function SET_OPSGROUP:AddGroupsByName( AddGroupNames )

    local AddGroupNamesArray = ( type( AddGroupNames ) == "table" ) and AddGroupNames or { AddGroupNames }

    for AddGroupID, AddGroupName in pairs( AddGroupNamesArray ) do
      self:Add(AddGroupName, GROUP:FindByName(AddGroupName))
    end

    return self
  end

  --- Remove GROUP(s) or OPSGROUP(s) from the set.
  -- @param Core.Set#SET_OPSGROUP self
  -- @param Wrapper.Group#GROUP RemoveGroupNames A single name or an array of GROUP names.
  -- @return Core.Set#SET_OPSGROUP self
  function SET_OPSGROUP:RemoveGroupsByName( RemoveGroupNames )

    local RemoveGroupNamesArray = ( type( RemoveGroupNames ) == "table" ) and RemoveGroupNames or { RemoveGroupNames }

    for RemoveGroupID, RemoveGroupName in pairs( RemoveGroupNamesArray ) do
      self:Remove( RemoveGroupName )
    end

    return self
  end

  --- Iterate the SET_OPSGROUP and count how many GROUPs and UNITs are alive.
  -- @param #SET_GROUP self
  -- @return #number The number of GROUPs alive.
  -- @return #number The number of UNITs alive.
  function SET_OPSGROUP:CountAlive()
    local CountG = 0
    local CountU = 0

    local Set = self:GetSet()

    for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP
      if GroupData and GroupData:IsAlive() then
        CountG = CountG + 1
        -- Count Units.
        CountU = CountU + GroupData:GetGroup():CountAliveUnits()
      end

    end

    return CountG, CountU
  end

  --- Finds an OPSGROUP based on the group name.
  -- @param #SET_OPSGROUP self
  -- @param #string GroupName Name of the group.
  -- @return Ops.OpsGroup#OPSGROUP The found OPSGROUP (FLIGHTGROUP, ARMYGROUP or NAVYGROUP) or `#nil` if the group is not in the set.
  function SET_OPSGROUP:FindGroup(GroupName)
    local GroupFound = self.Set[GroupName]
    return GroupFound
  end
  
  --- Finds a FLIGHTGROUP based on the group name.
  -- @param #SET_OPSGROUP self
  -- @param #string GroupName Name of the group.
  -- @return Ops.FlightGroup#FLIGHTGROUP The found FLIGHTGROUP or `#nil` if the group is not in the set.
  function SET_OPSGROUP:FindFlightGroup(GroupName)
    local GroupFound = self:FindGroup(GroupName)
    return GroupFound
  end
  
  --- Finds a ARMYGROUP based on the group name.
  -- @param #SET_OPSGROUP self
  -- @param #string GroupName Name of the group.
  -- @return Ops.ArmyGroup#ARMYGROUP The found ARMYGROUP or `#nil` if the group is not in the set.
  function SET_OPSGROUP:FindArmyGroup(GroupName)
    local GroupFound = self:FindGroup(GroupName)
    return GroupFound
  end  


  --- Finds a NAVYGROUP based on the group name.
  -- @param #SET_OPSGROUP self
  -- @param #string GroupName Name of the group.
  -- @return Ops.NavyGroup#NAVYGROUP The found NAVYGROUP or `#nil` if the group is not in the set.
  function SET_OPSGROUP:FindNavyGroup(GroupName)
    local GroupFound = self:FindGroup(GroupName)
    return GroupFound
  end  
    
  --- Builds a set of groups of coalitions.
  -- Possible current coalitions are red, blue and neutral.
  -- @param #SET_OPSGROUP self
  -- @param #string Coalitions Can take the following values: "red", "blue", "neutral" or combinations as a table, for example `{"red", "neutral"}`.
  -- @param #boolean Clear If `true`, clear any previously defined filters.
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:FilterCoalitions(Coalitions, Clear)
  
    -- Create an empty set.
    if Clear or not self.Filter.Coalitions then
      self.Filter.Coalitions={}
    end
    
    -- Ensure we got a table.
    if type(Coalitions)~="table" then
      Coalitions = {Coalitions}
    end
    
    -- Set filter.
    for CoalitionID, Coalition in pairs( Coalitions ) do
      self.Filter.Coalitions[Coalition] = Coalition
    end
    
    return self
  end


  --- Builds a set of groups out of categories.
  -- 
  -- Possible current categories are:
  -- 
  -- * "plane" for fixed wing groups
  -- * "helicopter" for rotary wing groups
  -- * "ground" for ground groups
  -- * "ship" for naval groups
  -- 
  -- @param #SET_OPSGROUP self
  -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship" or combinations as a table, for example `{"plane", "helicopter"}`.
  -- @param #boolean Clear If `true`, clear any previously defined filters.
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:FilterCategories( Categories, Clear )
  
    if Clear or not self.Filter.Categories then
      self.Filter.Categories={}
    end
    
    if type(Categories)~="table" then
      Categories={Categories}
    end
    
    for CategoryID, Category in pairs( Categories ) do
      self.Filter.Categories[Category] = Category
    end
    
    return self
  end

  --- Builds a set of groups out of ground category.
  -- @param #SET_OPSGROUP self
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:FilterCategoryGround()
    self:FilterCategories("ground")
    return self
  end

  --- Builds a set of groups out of airplane category.
  -- @param #SET_OPSGROUP self
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:FilterCategoryAirplane()
    self:FilterCategories("plane")
    return self
  end
  
  --- Builds a set of groups out of aicraft category (planes and helicopters).
  -- @param #SET_OPSGROUP self
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:FilterCategoryAircraft()
    self:FilterCategories({"plane", "helicopter"})
    return self
  end  

  --- Builds a set of groups out of helicopter category.
  -- @param #SET_OPSGROUP self
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:FilterCategoryHelicopter()
    self:FilterCategories( "helicopter" )
    return self
  end

  --- Builds a set of groups out of ship category.
  -- @param #SET_OPSGROUP self
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:FilterCategoryShip()
    self:FilterCategories("ship")
    return self
  end

  --- Builds a set of groups of defined countries.
  -- @param #SET_OPSGROUP self
  -- @param #string Countries Can take those country strings known within DCS world.
  -- @param #boolean Clear If `true`, clear any previously defined filters.
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:FilterCountries(Countries, Clear)
  
    -- Create empty table if necessary.
    if Clear or not self.Filter.Countries then
      self.Filter.Countries = {}
    end
    
    -- Ensure input is a table.
    if type(Countries)~="table" then
      Countries={Countries}
    end
    
    -- Set filter.
    for CountryID, Country in pairs( Countries ) do
      self.Filter.Countries[Country] = Country
    end
    
    return self
  end


  --- Builds a set of groups that contain the given string in their group name.
  -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all groups that **contain** the string. 
  -- @param #SET_OPSGROUP self
  -- @param #string Prefixes The string pattern(s) that needs to be contained in the group name. Can also be passed as a `#table` of strings.
  -- @param #boolean Clear If `true`, clear any previously defined filters.
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:FilterPrefixes(Prefixes, Clear)
  
    -- Create emtpy table if necessary.
    if Clear or not self.Filter.GroupPrefixes then
      self.Filter.GroupPrefixes={}
    end
    
    -- Ensure we have a table.
    if type(Prefixes)~="table" then
      Prefixes={Prefixes}
    end
    
    -- Set group prefixes.
    for PrefixID, Prefix in pairs(Prefixes) do
      self.Filter.GroupPrefixes[Prefix]=Prefix
    end
    
    return self
  end

  --- Builds a set of groups that are only active.
  -- Only the groups that are active will be included within the set.
  -- @param #SET_OPSGROUP self
  -- @param #boolean Active (optional) Include only active groups to the set.
  -- Include inactive groups if you provide false.
  -- @return #SET_OPSGROUP self
  -- @usage
  --
  -- -- Include only active groups to the set.
  -- GroupSet = SET_OPSGROUP:New():FilterActive():FilterStart()
  --
  -- -- Include only active groups to the set of the blue coalition, and filter one time.
  -- GroupSet = SET_OPSGROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce()
  --
  -- -- Include only active groups to the set of the blue coalition, and filter one time.
  -- -- Later, reset to include back inactive groups to the set.
  -- GroupSet = SET_OPSGROUP:New():FilterActive():FilterCoalition( "blue" ):FilterOnce()
  -- ... logic ...
  -- GroupSet = SET_OPSGROUP:New():FilterActive( false ):FilterCoalition( "blue" ):FilterOnce()
  --
  function SET_OPSGROUP:FilterActive( Active )
    Active = Active or not ( Active == false )
    self.Filter.Active = Active
    return self
  end


  --- Starts the filtering.
  -- @param #SET_OPSGROUP self
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:FilterStart()

    if _DATABASE then
      self:_FilterStart()
      self:HandleEvent( EVENTS.Birth, self._EventOnBirth )
      self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.UnitLost, self._EventOnDeadOrCrash )
    end

    return self
  end
  
  --- Activate late activated groups in the set.
  -- @param #SET_OPSGROUP self
  -- @param #number Delay Delay in seconds.
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:Activate(Delay)
    local Set = self:GetSet()
    for GroupID, GroupData in pairs(Set) do
      local group=GroupData --Ops.OpsGroup#OPSGROUP
      if group and group:IsAlive()==false then
        group:Activate(Delay)
      end
    end
    return self
  end  

  --- Handles the OnBirth event for the Set.
  -- @param #SET_OPSGROUP self
  -- @param Core.Event#EVENTDATA Event Event data.
function SET_OPSGROUP:_EventOnBirth(Event)
    --self:F3( { Event } )

    if Event.IniDCSUnit and Event.IniDCSGroup then
        local DCSgroup = Event.IniDCSGroup --DCS#Group

        -- group:CountAliveUnits() alternative as this fails for Respawn/Teleport
        local CountAliveActive = 0
        for index, data in pairs(DCSgroup:getUnits()) do
            if data:isExist() and data:isActive() then
                CountAliveActive = CountAliveActive + 1
            end
        end

        if DCSgroup:getInitialSize() == DCSgroup:getSize() then

            local groupname, group = self:AddInDatabase(Event)

            -- group:CountAliveUnits() alternative
            if group and CountAliveActive == DCSgroup:getInitialSize() then
                if group and self:IsIncludeObject(group) then
                    self:Add(groupname, group)
                end
            end
        end
    end
  end

  --- Handles the OnDead or OnCrash event for alive groups set.
  -- Note: The GROUP object in the SET_OPSGROUP collection will only be removed if the last unit is destroyed of the GROUP.
  -- @param #SET_OPSGROUP self
  -- @param Core.Event#EVENTDATA Event
  function SET_OPSGROUP:_EventOnDeadOrCrash( Event )
    --self:F( { Event } )

    if Event.IniDCSGroup then
      local ObjectName, Object = self:FindInDatabase( Event )
      if ObjectName then
        if Event.IniDCSGroup:getSize() == 1 then -- Only remove if the last unit of the group was destroyed.
          self:Remove( ObjectName )
        end
      end
    end
  end

  --- Handles the Database to check on an event (birth) that the Object was added in the Database.
  -- This is required, because sometimes the _DATABASE birth event gets called later than the SET_BASE birth event!
  -- @param #SET_OPSGROUP self
  -- @param Core.Event#EVENTDATA Event Event data.
  -- @return #string The name of the GROUP.
  -- @return Wrapper.Group#GROUP The GROUP object.
  function SET_OPSGROUP:AddInDatabase( Event )
  
    if Event.IniObjectCategory==Object.Category.UNIT then
    
      if not self.Database[Event.IniDCSGroupName] then
        self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName )
      end
      
    end

    return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName]
  end

  --- Handles the Database to check on any event that Object exists in the Database.
  -- This is required, because sometimes the _DATABASE event gets called later than the SET_BASE event or vise versa!
  -- @param #SET_OPSGROUP self
  -- @param Core.Event#EVENTDATA Event Event data table.
  -- @return #string The name of the GROUP.
  -- @return Wrapper.Group#GROUP The GROUP object.
  function SET_OPSGROUP:FindInDatabase(Event)
    return Event.IniDCSGroupName, self.Database[Event.IniDCSGroupName]
  end

  --- Iterate the set and call an iterator function for each OPSGROUP object.
  -- @param #SET_OPSGROUP self
  -- @param #function IteratorFunction The function that will be called for all OPSGROUPs in the set. **NOTE** that the function must have the OPSGROUP as first parameter!
  -- @param ... (Optional) arguments passed to the `IteratorFunction`.
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:ForEachGroup( IteratorFunction, ... )

    self:ForEach(IteratorFunction, arg, self:GetSet())

    return self
  end

  --- Check include object.
  -- @param #SET_OPSGROUP self
  -- @param Wrapper.Group#GROUP MGroup The group that is checked for inclusion.
  -- @return #SET_OPSGROUP self
  function SET_OPSGROUP:IsIncludeObject(MGroup)
  
    -- Assume it is and check later if not.
    local MGroupInclude=true

    -- Filter active.
    if self.Filter.Active~=nil then
    
      local MGroupActive = false
      
      if self.Filter.Active==false or (self.Filter.Active==true and MGroup:IsActive()==true) then
        MGroupActive = true
      end
      
      MGroupInclude = MGroupInclude and MGroupActive
    end

    -- Filter coalitions.
    if self.Filter.Coalitions and MGroupInclude then
    
      local MGroupCoalition = false
      
      for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do
        if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName]==MGroup:GetCoalition() then
          MGroupCoalition = true      
        end
      end
      
      MGroupInclude = MGroupInclude and MGroupCoalition
    end

    -- Filter categories.
    if self.Filter.Categories and MGroupInclude then
    
      local MGroupCategory = false
      
      for CategoryID, CategoryName in pairs( self.Filter.Categories ) do
        if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName]==MGroup:GetCategory() then
          MGroupCategory = true
        end
      end
      
      MGroupInclude = MGroupInclude and MGroupCategory
    end

    -- Filter countries.
    if self.Filter.Countries and MGroupInclude then
      local MGroupCountry = false
      for CountryID, CountryName in pairs( self.Filter.Countries ) do
        if country.id[CountryName] == MGroup:GetCountry() then
          MGroupCountry = true
        end
      end
      MGroupInclude = MGroupInclude and MGroupCountry
    end

    -- Filter "prefixes".
    if self.Filter.GroupPrefixes and MGroupInclude then
    
      local MGroupPrefix = false
      
      for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do
        if string.find( MGroup:GetName(), GroupPrefix:gsub ("-", "%%-"), 1 ) then --Not sure why "-" is replaced by "%-" ?! - So we can still match group names with a dash in them
          MGroupPrefix = true
        end
      end
      
      MGroupInclude = MGroupInclude and MGroupPrefix
    end
    
    if self.Filter.Functions and MGroupInclude then
      local MClientFunc = self:_EvalFilterFunctions(MGroup)
      MGroupInclude = MGroupInclude and MClientFunc
    end
    
    return MGroupInclude
  end
  
end
            
do -- SET_SCENERY

  ---
  -- @type SET_SCENERY
  -- @extends Core.Set#SET_BASE

  --- Mission designers can use the SET_SCENERY class to build sets of scenery belonging to certain:
  --
  --  * Zone Sets
  --
  -- ## SET_SCENERY constructor
  --
  -- Create a new SET_SCENERY object with the @{#SET_SCENERY.New} method:
  --
  --    * @{#SET_SCENERY.New}: Creates a new SET_SCENERY object.
  --
  -- ## Add or Remove SCENERY(s) from SET_SCENERY
  --
  -- SCENERYs can be added and removed using the @{Core.Set#SET_SCENERY.AddSceneryByName} and @{Core.Set#SET_SCENERY.RemoveSceneryByName} respectively.
  -- These methods take a single SCENERY name or an array of SCENERY names to be added or removed from SET_SCENERY.
  --
  -- ## SET_SCENERY filter criteria
  --
  -- N/A at the moment
  --    
  -- ## SET_SCENERY iterators
  --
  -- Once the filters have been defined and the SET_SCENERY has been built, you can iterate the SET_SCENERY with the available iterator methods.
  -- The iterator methods will walk the SET_SCENERY set, and call for each element within the set a function that you provide.
  -- The following iterator methods are currently available within the SET_SCENERY:
  --
  --   * @{#SET_SCENERY.ForEachScenery}: Calls a function for each alive object it finds within the SET_SCENERY.
  --
  -- ## SET_SCENERY atomic methods
  --
  -- N/A at the moment
  --
  -- ===
  -- @field #SET_SCENERY SET_SCENERY
  SET_SCENERY = {
    ClassName = "SET_SCENERY",
    Scenerys = {},
    Filter = {
      SceneryPrefixes = nil,
      SceneryRoles = nil,
      Zones = nil,
    },
  }

  --- Creates a new SET_SCENERY object. Scenery is **not** auto-registered in the Moose database, there are too many objects on each map. Hence we need to find them first. For this we are using a SET_ZONE. 
  -- @param #SET_SCENERY self
  -- @param #SET_ZONE ZoneSet SET_ZONE of ZONE objects as created by right-clicks on the map in the mission editor, choosing "assign as...". Rename the zones for grouping purposes, e.g. all sections of a bridge as "Bridge-1" to "Bridge-3".
  -- @return #SET_SCENERY
  -- @usage
  -- -- Define a new SET_SCENERY Object. This Object will contain a reference to all added Scenery Objects.
  --    ZoneSet = SET_ZONE:New():FilterPrefixes("Bridge"):FilterOnce()
  --    mysceneryset = SET_SCENERY:New(ZoneSet)
  function SET_SCENERY:New(ZoneSet)
  
    local zoneset = {}  
      -- Inherits from BASE
    local self = BASE:Inherit( self, SET_BASE:New( zoneset ) ) -- Core.Set#SET_SCENERY
    
    local zonenames = {}
    
    if ZoneSet then
      for _,_zone in pairs(ZoneSet.Set) do
        --self:T(("Zone type handed: "..tostring(_zone.ClassName))
        table.insert(zonenames,_zone:GetName())
      end   
      self:AddSceneryByName(zonenames)
    end
    
    return self
  end
  
  --- Creates a new SET_SCENERY object. Scenery is **not** auto-registered in the Moose database, there are too many objects on each map. Hence we need to find them first. For this we scan the zone. 
  -- @param #SET_SCENERY self
  -- @param Core.Zone#ZONE Zone The zone to be scanned. Can be a ZONE_RADIUS (round) or a ZONE_POLYGON (e.g. Quad-Point)
  -- @return #SET_SCENERY
  function SET_SCENERY:NewFromZone(Zone)
    local zone = Zone -- Core.Zone#ZONE_RADIUS
    if type(Zone) == "string" then
      zone = ZONE:FindByName(Zone)
    end
    zone:Scan({Object.Category.SCENERY})
    return zone:GetScannedSetScenery()
  end
  
  --- Add SCENERY(s) to SET_SCENERY.
  -- @param #SET_SCENERY self
  -- @param Wrapper.Scenery#SCENERY AddScenery A single SCENERY object.
  -- @return #SET_SCENERY self
  function SET_SCENERY:AddScenery( AddScenery )
    --self:F2( AddScenery:GetName() )

    self:Add( AddScenery:GetName(), AddScenery )

    return self
  end


  --- Add SCENERY(s) to SET_SCENERY.
  -- @param #SET_SCENERY self
  -- @param #string AddSceneryNames A single name or an array of SCENERY zone names.
  -- @return #SET_SCENERY self
  function SET_SCENERY:AddSceneryByName( AddSceneryNames )

    local AddSceneryNamesArray = ( type( AddSceneryNames ) == "table" ) and AddSceneryNames or { AddSceneryNames }

    --self:T(( AddSceneryNamesArray )
    for AddSceneryID, AddSceneryName in pairs( AddSceneryNamesArray ) do
      self:Add( AddSceneryName, SCENERY:FindByZoneName( AddSceneryName ) )
    end

    return self
  end

  --- Remove SCENERY(s) from SET_SCENERY.
  -- @param Core.Set#SET_SCENERY self
  -- @param Wrapper.Scenery#SCENERY RemoveSceneryNames A single name or an array of SCENERY zone names.
  -- @return self
  function SET_SCENERY:RemoveSceneryByName( RemoveSceneryNames )

    local RemoveSceneryNamesArray = ( type( RemoveSceneryNames ) == "table" ) and RemoveSceneryNames or { RemoveSceneryNames }

    for RemoveSceneryID, RemoveSceneryName in pairs( RemoveSceneryNamesArray ) do
      self:Remove( RemoveSceneryName )
    end

    return self
  end

  --- Finds a Scenery in the SET, based on the Scenery Name.
  -- @param #SET_SCENERY self
  -- @param #string SceneryName
  -- @return Wrapper.Scenery#SCENERY The found Scenery.
  function SET_SCENERY:FindScenery( SceneryName )
    local SceneryFound = self.Set[SceneryName]
    return SceneryFound
  end

   --- Builds a set of scenery objects in zones.
  -- @param #SET_SCENERY self
  -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE
  -- @return #SET_SCENERY self
  function SET_SCENERY:FilterZones( Zones )
    if not self.Filter.Zones then
      self.Filter.Zones = {}
    end
    local zones = {}
    if Zones.ClassName and Zones.ClassName == "SET_ZONE" then
      zones = Zones.Set
    elseif type( Zones ) ~= "table" or (type( Zones ) == "table" and Zones.ClassName ) then
      self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!")
      return self     
    else
      zones = Zones
    end
    for _,Zone in pairs( zones ) do
      local zonename = Zone:GetName()
      --self:T((zonename)
      self.Filter.Zones[zonename] = Zone
    end
    return self
  end

  --- Builds a set of SCENERYs that contain the given string in their name.
  -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all scenery that **contain** the string. 
  -- @param #SET_SCENERY self
  -- @param #string Prefixes The string pattern(s) that need to be contained in the scenery name. Can also be passed as a `#table` of strings.
  -- @return #SET_SCENERY self
  function SET_SCENERY:FilterPrefixes( Prefixes )
    if not self.Filter.SceneryPrefixes then
      self.Filter.SceneryPrefixes = {}
    end
    if type( Prefixes ) ~= "table" then
      Prefixes = { Prefixes }
    end
    for PrefixID, Prefix in pairs( Prefixes ) do
      --self:T((Prefix)
      self.Filter.SceneryPrefixes[Prefix] = Prefix
    end
    return self
  end
  
  --- Builds a set of SCENERYs that **contain** an exact match of the "ROLE" property.
  -- @param #SET_SCENERY self
  -- @param #string Role The string pattern(s) that needs to exactly match the scenery "ROLE" property from the ME quad-zone properties. Can also be passed as a `#table` of strings.
  -- @return #SET_SCENERY self
  function SET_SCENERY:FilterRoles( Role )
    if not self.Filter.SceneryRoles then
      self.Filter.SceneryRoles = {}
    end
    if type( Role ) ~= "table" then
      Role = { Role }
    end
    for PrefixID, Prefix in pairs( Role ) do
      --self:T(Prefix)
      self.Filter.SceneryRoles[Prefix] = Prefix
    end
    return self
  end
  
  --- Iterate the SET_SCENERY and count how many SCENERYSs are alive.
  -- @param #SET_SCENERY self
  -- @return #number The number of SCENERYSs alive.
  function SET_SCENERY:CountAlive()

    local Set = self:GetSet()

    local CountU = 0
    for UnitID, UnitData in pairs(Set) do
      if UnitData and UnitData:IsAlive() then
        CountU = CountU + 1
      end

    end

    return CountU
  end
  
  --- Get a table of alive objects.
  -- @param #SET_SCENERY self
  -- @return #table Table of alive objects
  -- @return Core.Set#SET_SCENERY SET of alive objects
  function SET_SCENERY:GetAliveSet()
    --self:F2()

    local AliveSet = SET_SCENERY:New()

    -- Clean the Set before returning with only the alive Groups.
    for GroupName, GroupObject in pairs( self.Set ) do
      local GroupObject = GroupObject -- Wrapper.Group#GROUP
      if GroupObject then
        if GroupObject:IsAlive() then
          AliveSet:Add( GroupName, GroupObject )
        end
      end
    end

    return AliveSet.Set or {}, AliveSet
  end
  
  --- Iterate the SET_SCENERY and call an iterator function for each **alive** SCENERY, providing the SCENERY and optional parameters.
  -- @param #SET_SCENERY self
  -- @param #function IteratorFunction The function that will be called when there is an alive SCENERY in the SET_SCENERY. The function needs to accept a SCENERY parameter.
  -- @return #SET_SCENERY self
  function SET_SCENERY:ForEachScenery( IteratorFunction, ... )
    --self:F2( arg )
    self:ForEach( IteratorFunction, arg, self:GetSet() )
    return self
  end

  --- Get the center coordinate of the SET_SCENERY.
  -- @param #SET_SCENERY self
  -- @return Core.Point#COORDINATE The center coordinate of all the objects in the set.
  function SET_SCENERY:GetCoordinate()
    --[[
    local Coordinate = COORDINATE:New({0,0,0})

    local Item = self:GetRandomSurely()
    
    if Item then
      Coordinate:GetCoordinate()
    end
    --]]
    
    local Coordinate = self:GetFirst():GetCoordinate()
    
    local x1 = Coordinate.x
    local x2 = Coordinate.x
    local y1 = Coordinate.y
    local y2 = Coordinate.y
    local z1 = Coordinate.z
    local z2 = Coordinate.z

    for SceneryName, SceneryData in pairs( self:GetSet() ) do

      local Scenery = SceneryData -- Wrapper.Scenery#SCENERY
      local Coordinate = Scenery:GetCoordinate()

      x1 = ( Coordinate.x < x1 ) and Coordinate.x or x1
      x2 = ( Coordinate.x > x2 ) and Coordinate.x or x2
      y1 = ( Coordinate.y < y1 ) and Coordinate.y or y1
      y2 = ( Coordinate.y > y2 ) and Coordinate.y or y2
      z1 = ( Coordinate.y < z1 ) and Coordinate.z or z1
      z2 = ( Coordinate.y > z2 ) and Coordinate.z or z2

    end

    Coordinate.x = ( x2 - x1 ) / 2 + x1
    Coordinate.y = ( y2 - y1 ) / 2 + y1
    Coordinate.z = ( z2 - z1 ) / 2 + z1

    --self:F( { Coordinate = Coordinate } )
    return Coordinate

  end

  --- [Internal] Determine if an object is to be included in the SET
  -- @param #SET_SCENERY self
  -- @param Wrapper.Scenery#SCENERY MScenery
  -- @return #SET_SCENERY self
  function SET_SCENERY:IsIncludeObject( MScenery )
    --self:T(( MScenery.SceneryName )

    local MSceneryInclude = true
    
    if MScenery then
      local MSceneryName = MScenery:GetName()
      
      -- Filter Prefixes
      if self.Filter.Prefixes then
        local MSceneryPrefix = false
        for ZonePrefixId, ZonePrefix in pairs( self.Filter.Prefixes ) do
          --self:T(( { "Prefix:", string.find( MSceneryName, ZonePrefix, 1 ), ZonePrefix } )
          if string.find( MSceneryName, ZonePrefix, 1 ) then
            MSceneryPrefix = true
          end
        end
        --self:T(( { "Evaluated Prefix", MSceneryPrefix } )
        MSceneryInclude = MSceneryInclude and MSceneryPrefix
      end
      
      if self.Filter.Zones then
        local MSceneryZone = false
        for ZoneName, Zone in pairs( self.Filter.Zones ) do
          --self:T(( "Zone:", ZoneName )
          local coord = MScenery:GetCoordinate()
          if coord and Zone:IsCoordinateInZone(coord) then
            MSceneryZone = true
          end
          --self:T(( { "Evaluated Zone", MSceneryZone } )
        end
        MSceneryInclude = MSceneryInclude and MSceneryZone
      end

      -- Filter Roles
      if self.Filter.SceneryRoles then
        local MSceneryRole = false
        local Role = MScenery:GetProperty("ROLE") or "none"
        for ZoneRoleId, ZoneRole in pairs( self.Filter.SceneryRoles ) do
          --self:T(( { "Role:", ZoneRole, Role } )
          if ZoneRole == Role then
            MSceneryRole = true
          end
        end
        --self:T(( { "Evaluated Role ", MSceneryRole } )
        MSceneryInclude = MSceneryInclude and MSceneryRole
      end
    end
    
    if self.Filter.Functions and MSceneryInclude then
      local MClientFunc = self:_EvalFilterFunctions(MScenery)
      MSceneryInclude = MSceneryInclude and MClientFunc
    end
    
    --self:T(2( MSceneryInclude )
    return MSceneryInclude
  end
  
  --- Filters for the defined collection.
  -- @param #SET_SCENERY self
  -- @return #SET_SCENERY self
  function SET_SCENERY:FilterOnce()

    for ObjectName, Object in pairs( self:GetSet() ) do
      --self:T((ObjectName)
      if self:IsIncludeObject( Object ) then
        self:Add( ObjectName, Object )
      else
        self:Remove(ObjectName, true)
      end
    end

    return self --FilteredSet
  end
  
  
  --- Count overall initial (Life0) lifepoints of the SET objects.
  -- @param #SET_SCENERY self
  -- @return #number LIfe0Points
  function SET_SCENERY:GetLife0()
    local life0 = 0
    self:ForEachScenery(
      function(obj)
        local Obj = obj -- Wrapper.Scenery#SCENERY
        life0 = life0 + Obj:GetLife0()
      end
    )
    return life0
  end
  
  --- Count overall current lifepoints of the SET objects.
  -- @param #SET_SCENERY self
  -- @return #number LifePoints
  function SET_SCENERY:GetLife()
    local life = 0
    self:ForEachScenery(
      function(obj)
        local Obj = obj -- Wrapper.Scenery#SCENERY
        life = life + Obj:GetLife()
      end
    )
    return life
  end
  
  --- Calculate current relative lifepoints of the SET objects, i.e. Life divided by Life0 as percentage value, eg 75 meaning 75% alive. 
  -- **CAVEAT**: Some objects change their life value or "hitpoints" **after** the first hit. Hence we will adjust the Life0 value to 120% 
  -- of the last life value if life exceeds life0 ata any point.
  -- Thus we will get a smooth percentage decrease, if you use this e.g. as success criteria for a bombing task.
  -- @param #SET_SCENERY self
  -- @return #number LifePoints
  function SET_SCENERY:GetRelativeLife()
    local life = self:GetLife()
    local life0 = self:GetLife0()
    --self:T(2(string.format("Set Lifepoints: %d life0 | %d life",life0,life))
    local rlife = math.floor((life / life0) * 100)
    return rlife
  end
  
end

-- TODO SET_DYNAMICCARGO

do -- SET_DYNAMICCARGO
  
  ---
  -- @type SET_DYNAMICCARGO
  -- @field #table Filter Table of filters.
  -- @field #table Set Table of objects.
  -- @field #table Index Table of indices.
  -- @field #table List Unused table.
  -- @field Core.Scheduler#SCHEDULER CallScheduler.
  -- @field #SET_DYNAMICCARGO.Filters Filter Filters.
  -- @field #number ZoneTimerInterval.
  -- @field Core.Timer#TIMER ZoneTimer Timer for active filtering of zones.
  -- @extends Core.Set#SET_BASE
  
  ---
  -- @type SET_DYNAMICCARGO.Filters
  -- @field #string Coalitions
  -- @field #string Types
  -- @field #string Countries
  -- @field #string StaticPrefixes
  -- @field #string Zones
  
  --- The @{Core.Set#SET_DYNAMICCARGO} class defines the functions that define a collection of objects form @{Wrapper.DynamicCargo#DYNAMICCARGO}.
  -- A SET provides iterators to iterate the SET.
  --- Mission designers can use the SET_DYNAMICCARGO class to build sets of cargos belonging to certain:
  --
  --  * Coalitions
  --  * Categories
  --  * Countries
  --  * Static types
  --  * Starting with certain prefix strings.
  --  * Etc.
  --
  -- ## SET_DYNAMICCARGO constructor
  --
  -- Create a new SET_DYNAMICCARGO object with the @{#SET_DYNAMICCARGO.New} method:
  --
  --    * @{#SET_DYNAMICCARGO.New}: Creates a new SET_DYNAMICCARGO object.
  --
  -- ## SET_DYNAMICCARGO filter criteria
  --
  -- You can set filter criteria to define the set of objects within the SET_DYNAMICCARGO.
  -- Filter criteria are defined by:
  --
  --    * @{#SET_DYNAMICCARGO.FilterCoalitions}: Builds the SET_DYNAMICCARGO with the objects belonging to the coalition(s).
  --    * @{#SET_DYNAMICCARGO.FilterTypes}: Builds the SET_DYNAMICCARGO with the cargos belonging to the statiy type name(s).
  --    * @{#SET_DYNAMICCARGO.FilterCountries}: Builds the SET_DYNAMICCARGO with the objects belonging to the country(ies).
  --    * @{#SET_DYNAMICCARGO.FilterNamePatterns}, @{#SET_DYNAMICCARGO.FilterPrefixes}: Builds the SET_DYNAMICCARGO with the cargo containing the same string(s) in their name. **Attention!** LUA regular expression apply here, so special characters in names like minus, dot, hash (#) etc might lead to unexpected results. 
  -- Have a read through here to understand the application of regular expressions: [LUA regular expressions](https://riptutorial.com/lua/example/20315/lua-pattern-matching)
  --    * @{#SET_DYNAMICCARGO.FilterZones}: Builds the SET_DYNAMICCARGO with the cargo within a @{Core.Zone#ZONE}.
  --    * @{#SET_DYNAMICCARGO.FilterFunction}: Builds the SET_DYNAMICCARGO with a custom condition.
  --    * @{#SET_DYNAMICCARGO.FilterCurrentOwner}: Builds the SET_DYNAMICCARGO with a specific owner name.
  --    * @{#SET_DYNAMICCARGO.FilterIsLoaded}: Builds the SET_DYNAMICCARGO which is in state LOADED.
  --    * @{#SET_DYNAMICCARGO.FilterIsNew}: Builds the SET_DYNAMICCARGO with is in state NEW.
  --    * @{#SET_DYNAMICCARGO.FilterIsUnloaded}: Builds the SET_DYNAMICCARGO with is in state UNLOADED.
  --    
  -- Once the filter criteria have been set for the SET\_DYNAMICCARGO, you can start and stop filtering using:
  --
  --   * @{#SET_DYNAMICCARGO.FilterStart}: Starts the continous filtering of the objects within the SET_DYNAMICCARGO.
  --   * @{#SET_DYNAMICCARGO.FilterStop}: Stops the continous filtering of the objects within the SET_DYNAMICCARGO.
  --   * @{#SET_DYNAMICCARGO.FilterOnce}: Filters once for the objects within the SET_DYNAMICCARGO.
  --
  -- ## SET_DYNAMICCARGO iterators
  --
  -- Once the filters have been defined and the SET\_DYNAMICCARGO has been built, you can iterate the SET\_DYNAMICCARGO with the available iterator methods.
  -- The iterator methods will walk the SET\_DYNAMICCARGO set, and call for each element within the set a function that you provide.
  -- The following iterator methods are currently available within the SET\_DYNAMICCARGO:
  --
  --   * @{#SET_DYNAMICCARGO.ForEach}: Calls a function for each alive dynamic cargo it finds within the SET\_DYNAMICCARGO.
  --
  -- ## SET_DYNAMICCARGO atomic methods
  --
  -- Various methods exist for a SET_DYNAMICCARGO to perform actions or calculations and retrieve results from the SET\_DYNAMICCARGO:
  --
  --   * @{#SET_DYNAMICCARGO.GetOwnerClientObjects}(): Retrieve the type names of the @{Wrapper.Static}s in the SET, delimited by a comma.
  --   * @{#SET_DYNAMICCARGO.GetOwnerNames}(): Retrieve the type names of the @{Wrapper.Static}s in the SET, delimited by a comma.
  --   * @{#SET_DYNAMICCARGO.GetStorageObjects}(): Retrieve the type names of the @{Wrapper.Static}s in the SET, delimited by a comma.  
  -- 
  -- ===
  -- @field #SET_DYNAMICCARGO SET_DYNAMICCARGO
  SET_DYNAMICCARGO = {
    ClassName = "SET_DYNAMICCARGO",
    Set = {},
    List = {},
    Index = {},
    Database = nil,
    CallScheduler = nil,
    Filter = {
      Coalitions = nil,
      Types = nil,
      Countries = nil,
      StaticPrefixes = nil,
      Zones = nil,
    },
    FilterMeta = {
      Coalitions = {
        red = coalition.side.RED,
        blue = coalition.side.BLUE,
        neutral = coalition.side.NEUTRAL,
      }
    },
    ZoneTimerInterval = 20,
    ZoneTimer = nil,
  }
  
  --- Creates a new SET_DYNAMICCARGO object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names.
  -- @param #SET_DYNAMICCARGO self
  -- @return #SET_DYNAMICCARGO
  -- @usage
  -- -- Define a new SET_DYNAMICCARGO Object. This DBObject will contain a reference to all alive Statics.
  -- DBObject = SET_DYNAMICCARGO:New()
  function SET_DYNAMICCARGO:New()

    --- Inherits from BASE
    local self = BASE:Inherit( self, SET_BASE:New( _DATABASE.DYNAMICCARGO ) ) -- Core.Set#SET_DYNAMICCARGO

    return self
  end
  
  ---
  -- @param #SET_DYNAMICCARGO self
  -- @param Wrapper.DynamicCargo#DYNAMICCARGO DCargo
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:IsIncludeObject( DCargo )
    --self:F2( DCargo )
    local DCargoInclude = true

    if self.Filter.Coalitions then
      local DCargoCoalition = false
      for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do
        --self:T2( { "Coalition:", DCargo:GetCoalition(), self.FilterMeta.Coalitions[CoalitionName], CoalitionName } )
        if self.FilterMeta.Coalitions[CoalitionName] and self.FilterMeta.Coalitions[CoalitionName] == DCargo:GetCoalition() then
          DCargoCoalition = true
        end
      end
      DCargoInclude = DCargoInclude and DCargoCoalition
    end

    if self.Filter.Types then
      local DCargoType = false
      for TypeID, TypeName in pairs( self.Filter.Types ) do
        --self:T2( { "Type:", DCargo:GetTypeName(), TypeName } )
        if TypeName == DCargo:GetTypeName() then
          DCargoType = true
        end
      end
      DCargoInclude = DCargoInclude and DCargoType
    end

    if self.Filter.Countries then
      local DCargoCountry = false
      for CountryID, CountryName in pairs( self.Filter.Countries ) do
        --self:T2( { "Country:", DCargo:GetCountry(), CountryName } )
        if country.id[CountryName] == DCargo:GetCountry() then
          DCargoCountry = true
        end
      end
      DCargoInclude = DCargoInclude and DCargoCountry
    end

    if self.Filter.StaticPrefixes then
      local DCargoPrefix = false
      for StaticPrefixId, StaticPrefix in pairs( self.Filter.StaticPrefixes ) do
        --self:T2( { "Prefix:", string.find( DCargo:GetName(), StaticPrefix, 1 ), StaticPrefix } )
        if string.find( DCargo:GetName(), StaticPrefix, 1 ) then
          DCargoPrefix = true
        end
      end
      DCargoInclude = DCargoInclude and DCargoPrefix
    end
    
    if self.Filter.Zones then
      local DCargoZone = false
      for ZoneName, Zone in pairs( self.Filter.Zones ) do
        --self:T2( "In zone: "..ZoneName )
        if DCargo and DCargo:IsInZone(Zone) then
          DCargoZone = true
        end
      end
      DCargoInclude = DCargoInclude and DCargoZone
    end
    
    if self.Filter.Functions and DCargoInclude then
      local MClientFunc = self:_EvalFilterFunctions(DCargo)
      DCargoInclude = DCargoInclude and MClientFunc
    end
    
    --self:T2( DCargoInclude )
    return DCargoInclude
  end
  
  --- Builds a set of dynamic cargo of defined coalitions.
  -- Possible current coalitions are red, blue and neutral.
  -- @param #SET_DYNAMICCARGO self
  -- @param #string Coalitions Can take the following values: "red", "blue", "neutral".
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterCoalitions( Coalitions )
    if not self.Filter.Coalitions then
      self.Filter.Coalitions = {}
    end
    if type( Coalitions ) ~= "table" then
      Coalitions = { Coalitions }
    end
    for CoalitionID, Coalition in pairs( Coalitions ) do
      self.Filter.Coalitions[Coalition] = Coalition
    end
    return self
  end
  
  --- Builds a set of dynamic cargo of defined dynamic cargo type names.
  -- @param #SET_DYNAMICCARGO self
  -- @param #string Types Can take those type name strings known within DCS world.
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterTypes( Types )
    if not self.Filter.Types then
      self.Filter.Types = {}
    end
    if type( Types ) ~= "table" then
      Types = { Types }
    end
    for TypeID, Type in pairs( Types ) do
      self.Filter.Types[Type] = Type
    end
    return self
  end
  
  --- [User] Add a custom condition function.
  -- @function [parent=#SET_DYNAMICCARGO] FilterFunction
  -- @param #SET_DYNAMICCARGO self
  -- @param #function ConditionFunction If this function returns `true`, the object is added to the SET. The function needs to take a DYNAMICCARGO object as first argument.
  -- @param ... Condition function arguments if any.
  -- @return #SET_DYNAMICCARGO self
  -- @usage
  --          -- Image you want to exclude a specific DYNAMICCARGO from a SET:
  --          local cargoset = SET_DYNAMICCARGO:New():FilterCoalitions("blue"):FilterFunction(
  --          -- The function needs to take a DYNAMICCARGO object as first - and in this case, only - argument.
  --          function(dynamiccargo)
  --              local isinclude = true
  --              if dynamiccargo:GetName() == "Exclude Me" then isinclude = false end
  --              return isinclude
  --          end
  --          ):FilterOnce()
  --          BASE:I(cargoset:Flush())
  
  --- Builds a set of dynamic cargo of defined countries.
  -- Possible current countries are those known within DCS world.
  -- @param #SET_DYNAMICCARGO self
  -- @param #string Countries Can take those country strings known within DCS world.
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterCountries( Countries )
    if not self.Filter.Countries then
      self.Filter.Countries = {}
    end
    if type( Countries ) ~= "table" then
      Countries = { Countries }
    end
    for CountryID, Country in pairs( Countries ) do
      self.Filter.Countries[Country] = Country
    end
    return self
  end

  --- Builds a set of DYNAMICCARGOs that contain the given string in their name.
  -- **Attention!** Bad naming convention as this **does not** filter only **prefixes** but all names that **contain** the string. LUA Regex applies.
  -- @param #SET_DYNAMICCARGO self
  -- @param #string Prefixes The string pattern(s) that need to be contained in the dynamic cargo name. Can also be passed as a `#table` of strings.
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterPrefixes( Prefixes )
    if not self.Filter.StaticPrefixes then
      self.Filter.StaticPrefixes = {}
    end
    if type( Prefixes ) ~= "table" then
      Prefixes = { Prefixes }
    end
    for PrefixID, Prefix in pairs( Prefixes ) do
      self.Filter.StaticPrefixes[Prefix] = Prefix
    end
    return self
  end
  
  --- Builds a set of DYNAMICCARGOs that contain the given string in their name.
  -- **Attention!** LUA Regex applies!
  -- @param #SET_DYNAMICCARGO self
  -- @param #string Patterns The string pattern(s) that need to be contained in the dynamic cargo name. Can also be passed as a `#table` of strings.
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterNamePattern( Patterns )
    return self:FilterPrefixes(Patterns)
  end
   
  --- Builds a set of DYNAMICCARGOs that are in state DYNAMICCARGO.State.LOADED (i.e. is on board of a Chinook).
  -- @param #SET_DYNAMICCARGO self
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterIsLoaded()
    self:FilterFunction(
      function(cargo)
        if cargo and cargo.CargoState and cargo.CargoState == DYNAMICCARGO.State.LOADED then
          return true
        else
          return false
        end
      end
    )
    return self
  end
  
  --- Builds a set of DYNAMICCARGOs that are in state DYNAMICCARGO.State.LOADED (i.e. was on board of a Chinook previously and is now unloaded).
  -- @param #SET_DYNAMICCARGO self
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterIsUnloaded()
    self:FilterFunction(
      function(cargo)
        if cargo and cargo.CargoState and cargo.CargoState == DYNAMICCARGO.State.UNLOADED then
          return true
        else
          return false
        end
      end
    )
    return self
  end
  
  --- Builds a set of DYNAMICCARGOs that are in state DYNAMICCARGO.State.NEW (i.e. new and never loaded into a Chinook).
  -- @param #SET_DYNAMICCARGO self
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterIsNew()
    self:FilterFunction(
      function(cargo)
        if cargo and cargo.CargoState and cargo.CargoState == DYNAMICCARGO.State.NEW then
          return true
        else
          return false
        end
      end
    )
    return self
  end
  
  --- Builds a set of DYNAMICCARGOs that are owned at the moment by this player name.
  -- @param #SET_DYNAMICCARGO self
  -- @param #string PlayerName
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterCurrentOwner(PlayerName)
    self:FilterFunction(
      function(cargo)
        if cargo and cargo.Owner and string.find(cargo.Owner,PlayerName,1,true) then
          return true
        else
          return false
        end
      end
    )
    return self
  end
  
  --- Builds a set of dynamic cargo in zones.
  -- @param #SET_DYNAMICCARGO self
  -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterZones( Zones )
    if not self.Filter.Zones then
      self.Filter.Zones = {}
    end
    local zones = {}
    if Zones.ClassName and Zones.ClassName == "SET_ZONE" then
      zones = Zones.Set
    elseif type( Zones ) ~= "table" or (type( Zones ) == "table" and Zones.ClassName ) then
      self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!")
      return self     
    else
      zones = Zones
    end
    for _,Zone in pairs( zones ) do
      local zonename = Zone:GetName()
      self.Filter.Zones[zonename] = Zone
    end
    return self
  end
  
  --- Starts the filtering.
  -- @param #SET_DYNAMICCARGO self
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterStart()
    if _DATABASE then
      self:HandleEvent( EVENTS.NewDynamicCargo, self._EventHandlerDCAdd )
      self:HandleEvent( EVENTS.DynamicCargoRemoved, self._EventHandlerDCRemove )
      if self.Filter.Zones then
        self.ZoneTimer = TIMER:New(self._ContinousZoneFilter,self)
        local timing = self.ZoneTimerInterval or 30
        self.ZoneTimer:Start(timing,timing)
      end
      self:_FilterStart()
    end

    return self
  end
  
    --- Stops the filtering.
  -- @param #SET_DYNAMICCARGO self
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterStop()
    if _DATABASE then
      self:UnHandleEvent( EVENTS.NewDynamicCargo)
      self:UnHandleEvent( EVENTS.DynamicCargoRemoved )
      if self.ZoneTimer and self.ZoneTimer:IsRunning() then
        self.ZoneTimer:Stop()
      end
    end

    return self
  end
  
  --- [Internal] Private function for use of continous zone filter
  -- @param #SET_DYNAMICCARGO self
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:_ContinousZoneFilter()   
    local Database = _DATABASE.DYNAMICCARGO  
    for ObjectName, Object in pairs( Database ) do
      if self:IsIncludeObject( Object ) and self:IsNotInSet(Object) then
        self:Add( ObjectName, Object )
      elseif (not self:IsIncludeObject( Object )) and self:IsInSet(Object) then
        self:Remove(ObjectName)
      end
    end
    
    return self  
  end
  
  --- Handles the events for the Set.
  -- @param #SET_DYNAMICCARGO self
  -- @param Core.Event#EVENTDATA Event
  function SET_DYNAMICCARGO:_EventHandlerDCAdd( Event )
    if Event.IniDynamicCargo and Event.IniDynamicCargoName then
      if not _DATABASE.DYNAMICCARGO[Event.IniDynamicCargoName] then
        _DATABASE:AddDynamicCargo( Event.IniDynamicCargoName )
      end
      local ObjectName, Object = self:FindInDatabase( Event )
      if Object and self:IsIncludeObject( Object ) then
        self:Add( ObjectName, Object )
      end
    end
    
    return self
  end
  
   --- Handles the remove event for dynamic cargo set.
  -- @param #SET_DYNAMICCARGO self
  -- @param Core.Event#EVENTDATA Event
  function SET_DYNAMICCARGO:_EventHandlerDCRemove( Event )
    if Event.IniDCSUnitName then
      local ObjectName, Object = self:FindInDatabase( Event )
      if ObjectName then
        self:Remove( ObjectName )
      end
    end
    
    return self
  end
  
  --- Handles the Database to check on any event that Object exists in the Database.
  -- This is required, because sometimes the _DATABASE event gets called later than the SET_DYNAMICCARGO event or vise versa!
  -- @param #SET_DYNAMICCARGO self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the DYNAMICCARGO
  -- @return Wrapper.DynamicCargo#DYNAMICCARGO The DYNAMICCARGO object
  function SET_DYNAMICCARGO:FindInDatabase( Event )
    return Event.IniDCSUnitName, self.Set[Event.IniDCSUnitName]
  end
  
  --- Set filter timer interval for FilterZones if using active filtering with FilterStart().
  -- @param #SET_DYNAMICCARGO self
  -- @param #number Seconds Seconds between check intervals, defaults to 30. **Caution** - do not be too agressive with timing! Objects are usually not moving fast enough
  -- to warrant a check of below 10 seconds.
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterZoneTimer(Seconds) 
    self.ZoneTimerInterval = Seconds or 30
    return self
  end
  
  --- This filter is N/A for SET_DYNAMICCARGO
  -- @param #SET_DYNAMICCARGO self
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterDeads()
    return self
  end

  --- This filter is N/A for SET_DYNAMICCARGO
  -- @param #SET_DYNAMICCARGO self
  -- @return #SET_DYNAMICCARGO self
  function SET_DYNAMICCARGO:FilterCrashes()
    return self
  end
  
  --- Returns a list of current owners (playernames) indexed by playername from the SET.
  -- @param #SET_DYNAMICCARGO self
  -- @return #list<#string> Ownerlist
  function SET_DYNAMICCARGO:GetOwnerNames()
    local owners = {}
    self:ForEach(
      function(cargo)
        if cargo and cargo.Owner then
          table.insert(owners, cargo.Owner, cargo.Owner)
        end
      end
    )  
    return owners
  end
  
  --- Returns a list of @{Wrapper.Storage#STORAGE} objects from the SET indexed by cargo name.
  -- @param #SET_DYNAMICCARGO self
  -- @return #list<Wrapper.Storage#STORAGE> Storagelist
  function SET_DYNAMICCARGO:GetStorageObjects()
    local owners = {}
    self:ForEach(
      function(cargo)
        if cargo and cargo.warehouse then
          table.insert(owners, cargo.StaticName, cargo.warehouse)
        end
      end
    )  
    return owners
  end
  
  --- Returns a list of current owners (Wrapper.Client#CLIENT objects) indexed by playername from the SET.
  -- @param #SET_DYNAMICCARGO self
  -- @return #list<#string> Ownerlist
  function SET_DYNAMICCARGO:GetOwnerClientObjects()
    local owners = {}
    self:ForEach(
      function(cargo)
        if cargo and cargo.Owner then
          local client = CLIENT:FindByPlayerName(cargo.Owner)
          if client then
            table.insert(owners, cargo.Owner, client)
          end
        end
      end
    )  
    return owners
  end

end
--- **Core** - Defines an extensive API to manage 3D points in the DCS World 3D simulation space.
--
-- ## Features:
--
--   * Provides a COORDINATE class, which allows to manage points in 3D space and perform various operations on it.
--   * Provides a POINT\_VEC2 class, which is derived from COORDINATE, and allows to manage points in 3D space, but from a Lat/Lon and Altitude perspective.
--   * Provides a POINT\_VEC3 class, which is derived from COORDINATE, and allows to manage points in 3D space, but from a X, Z and Y vector perspective.
--
-- ===
--
-- ### Authors:
--
--   * FlightControl (Design & Programming)
--
-- ### Contributions:
-- 
--   * funkyfranky
--   * Applevangelist
--   
-- ===
--
-- @module Core.Point
-- @image Core_Coordinate.JPG


do -- COORDINATE
  
  --- Coordinate class
  -- @type COORDINATE
  -- @field #string ClassName Name of the class
  -- @field #number x Component of the 3D vector.
  -- @field #number y Component of the 3D vector.
  -- @field #number z Component of the 3D vector.
  -- @field #number Heading Heading in degrees. Needs to be set first.
  -- @field #number Velocity Velocity in meters per second. Needs to be set first.
  -- @extends Core.Base#BASE


  --- Defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space.
  --
  -- # 1) Create a COORDINATE object.
  --
  -- A new COORDINATE object can be created with 3 various methods:
  --
  --  * @{#COORDINATE.New}(): from a 3D point.
  --  * @{#COORDINATE.NewFromVec2}(): from a @{DCS#Vec2} and possible altitude.
  --  * @{#COORDINATE.NewFromVec3}(): from a @{DCS#Vec3}.
  --
  --
  -- # 2) Smoke, flare, explode, illuminate at the coordinate.
  --
  -- At the point a smoke, flare, explosion and illumination bomb can be triggered. Use the following methods:
  --
  -- ## 2.1) Smoke
  --
  --   * @{#COORDINATE.Smoke}(): To smoke the point in a certain color.
  --   * @{#COORDINATE.SmokeBlue}(): To smoke the point in blue.
  --   * @{#COORDINATE.SmokeRed}(): To smoke the point in red.
  --   * @{#COORDINATE.SmokeOrange}(): To smoke the point in orange.
  --   * @{#COORDINATE.SmokeWhite}(): To smoke the point in white.
  --   * @{#COORDINATE.SmokeGreen}(): To smoke the point in green.
  --   * @{#COORDINATE.SetSmokeOffsetDirection}(): To set an offset point direction for smoke.
  --   * @{#COORDINATE.SetSmokeOffsetDistance}(): To set an offset point distance for smoke.
  --   * @{#COORDINATE.SwitchSmokeOffsetOn}(): To set an offset point for smoke to on.
  --   * @{#COORDINATE.SwitchSmokeOffsetOff}(): To set an offset point for smoke to off.
  --
  -- ## 2.2) Flare
  --
  --   * @{#COORDINATE.Flare}(): To flare the point in a certain color.
  --   * @{#COORDINATE.FlareRed}(): To flare the point in red.
  --   * @{#COORDINATE.FlareYellow}(): To flare the point in yellow.
  --   * @{#COORDINATE.FlareWhite}(): To flare the point in white.
  --   * @{#COORDINATE.FlareGreen}(): To flare the point in green.
  --
  -- ## 2.3) Explode
  --
  --   * @{#COORDINATE.Explosion}(): To explode the point with a certain intensity.
  --
  -- ## 2.4) Illuminate
  --
  --   * @{#COORDINATE.IlluminationBomb}(): To illuminate the point.
  --
  --
  -- # 3) Create markings on the map.
  --
  -- Place markers (text boxes with clarifications for briefings, target locations or any other reference point)
  -- on the map for all players, coalitions or specific groups:
  --
  --   * @{#COORDINATE.MarkToAll}(): Place a mark to all players.
  --   * @{#COORDINATE.MarkToCoalition}(): Place a mark to a coalition.
  --   * @{#COORDINATE.MarkToCoalitionRed}(): Place a mark to the red coalition.
  --   * @{#COORDINATE.MarkToCoalitionBlue}(): Place a mark to the blue coalition.
  --   * @{#COORDINATE.MarkToGroup}(): Place a mark to a group (needs to have a client in it or a CA group (CA group is bugged)).
  --   * @{#COORDINATE.RemoveMark}(): Removes a mark from the map.
  --
  -- # 4) Coordinate calculation methods.
  --
  -- Various calculation methods exist to use or manipulate 3D space. Find below a short description of each method:
  --
  -- ## 4.1) Get the distance between 2 points.
  --
  --   * @{#COORDINATE.Get3DDistance}(): Obtain the distance from the current 3D point to the provided 3D point in 3D space.
  --   * @{#COORDINATE.Get2DDistance}(): Obtain the distance from the current 3D point to the provided 3D point in 2D space.
  --
  -- ## 4.2) Get the angle.
  --
  --   * @{#COORDINATE.GetAngleDegrees}(): Obtain the angle in degrees from the current 3D point with the provided 3D direction vector.
  --   * @{#COORDINATE.GetAngleRadians}(): Obtain the angle in radians from the current 3D point with the provided 3D direction vector.
  --   * @{#COORDINATE.GetDirectionVec3}(): Obtain the 3D direction vector from the current 3D point to the provided 3D point.
  --
  -- ## 4.3) Coordinate translation.
  --
  --   * @{#COORDINATE.Translate}(): Translate the current 3D point towards an other 3D point using the given Distance and Angle.
  --
  -- ## 4.4) Get the North correction of the current location.
  --
  --   * @{#COORDINATE.GetNorthCorrectionRadians}(): Obtains the north correction at the current 3D point.
  --
  -- ## 4.5) Point Randomization
  --
  -- Various methods exist to calculate random locations around a given 3D point.
  --
  --   * @{#COORDINATE.GetRandomVec2InRadius}(): Provides a random 2D vector around the current 3D point, in the given inner to outer band.
  --   * @{#COORDINATE.GetRandomVec3InRadius}(): Provides a random 3D vector around the current 3D point, in the given inner to outer band.
  --
  -- ## 4.6) LOS between coordinates.
  --
  -- Calculate if the coordinate has Line of Sight (LOS) with the other given coordinate.
  -- Mountains, trees and other objects can be positioned between the two 3D points, preventing visibilty in a straight continuous line.
  -- The method @{#COORDINATE.IsLOS}() returns if the two coordinates have LOS.
  --
  -- ## 4.7) Check the coordinate position.
  --
  -- Various methods are available that allow to check if a coordinate is:
  --
  --   * @{#COORDINATE.IsInRadius}(): in a give radius.
  --   * @{#COORDINATE.IsInSphere}(): is in a given sphere.
  --   * @{#COORDINATE.IsAtCoordinate2D}(): is in a given coordinate within a specific precision.
  --
  --
  --
  -- # 5) Measure the simulation environment at the coordinate.
  --
  -- ## 5.1) Weather specific.
  --
  -- Within the DCS simulator, a coordinate has specific environmental properties, like wind, temperature, humidity etc.
  --
  --   * @{#COORDINATE.GetWind}(): Retrieve the wind at the specific coordinate within the DCS simulator.
  --   * @{#COORDINATE.GetTemperature}(): Retrieve the temperature at the specific height within the DCS simulator.
  --   * @{#COORDINATE.GetPressure}(): Retrieve the pressure at the specific height within the DCS simulator.
  --
  -- ## 5.2) Surface specific.
  --
  -- Within the DCS simulator, the surface can have various objects placed at the coordinate, and the surface height will vary.
  --
  --   * @{#COORDINATE.GetLandHeight}(): Retrieve the height of the surface (on the ground) within the DCS simulator.
  --   * @{#COORDINATE.GetSurfaceType}(): Retrieve the surface type (on the ground) within the DCS simulator.
  --
  -- # 6) Create waypoints for routes.
  --
  -- A COORDINATE can prepare waypoints for Ground and Air groups to be embedded into a Route.
  --
  --   * @{#COORDINATE.WaypointAir}(): Build an air route point.
  --   * @{#COORDINATE.WaypointGround}(): Build a ground route point.
  --   * @{#COORDINATE.WaypointNaval}(): Build a naval route point.
  --
  -- Route points can be used in the Route methods of the @{Wrapper.Group#GROUP} class.
  --
  -- ## 7) Manage the roads.
  --
  -- Important for ground vehicle transportation and movement, the method @{#COORDINATE.GetClosestPointToRoad}() will calculate
  -- the closest point on the nearest road.
  --
  -- In order to use the most optimal road system to transport vehicles, the method @{#COORDINATE.GetPathOnRoad}() will calculate
  -- the most optimal path following the road between two coordinates.
  --
  -- ## 8) Metric or imperial system
  --
  --   * @{#COORDINATE.IsMetric}(): Returns if the 3D point is Metric or Nautical Miles.
  --   * @{#COORDINATE.SetMetric}(): Sets the 3D point to Metric or Nautical Miles.
  --
  --
  -- ## 9) Coordinate text generation
  --
  --   * @{#COORDINATE.ToStringBR}(): Generates a Bearing & Range text in the format of DDD for DI where DDD is degrees and DI is distance.
  --   * @{#COORDINATE.ToStringBRA}(): Generates a Bearing, Range & Altitude text.
  --   * @{#COORDINATE.ToStringBRAANATO}(): Generates a Generates a Bearing, Range, Aspect & Altitude text in NATOPS.
  --   * @{#COORDINATE.ToStringLL}(): Generates a Latitude & Longitude text.
  --   * @{#COORDINATE.ToStringLLDMS}(): Generates a Lat, Lon, Degree, Minute, Second text.
  --   * @{#COORDINATE.ToStringLLDDM}(): Generates a Lat, Lon, Degree, decimal Minute text.
  --   * @{#COORDINATE.ToStringMGRS}(): Generates a MGRS grid coordinate text.
  --
  -- ## 10) Drawings on F10 map
  --
  --   * @{#COORDINATE.CircleToAll}(): Draw a circle on the F10 map.
  --   * @{#COORDINATE.LineToAll}(): Draw a line on the F10 map.
  --   * @{#COORDINATE.RectToAll}(): Draw a rectangle on the F10 map.
  --   * @{#COORDINATE.QuadToAll}(): Draw a shape with four points on the F10 map.
  --   * @{#COORDINATE.TextToAll}(): Write some text on the F10 map.
  --   * @{#COORDINATE.ArrowToAll}(): Draw an arrow on the F10 map.
  --
  -- @field #COORDINATE
  COORDINATE = {
    ClassName = "COORDINATE",
  }

  --- Waypoint altitude types.
  -- @type COORDINATE.WaypointAltType
  -- @field #string BARO Barometric altitude.
  -- @field #string RADIO Radio altitude.
  COORDINATE.WaypointAltType = {
    BARO = "BARO",
    RADIO = "RADIO",
  }

  --- Waypoint actions.
  -- @type COORDINATE.WaypointAction
  -- @field #string TurningPoint Turning point.
  -- @field #string FlyoverPoint Fly over point.
  -- @field #string FromParkingArea From parking area.
  -- @field #string FromParkingAreaHot From parking area hot.
  -- @field #string FromGroundAreaHot From ground area hot.
  -- @field #string FromGroundArea From ground area.
  -- @field #string FromRunway From runway.
  -- @field #string Landing Landing.
  -- @field #string LandingReFuAr Landing and refuel and rearm.
  COORDINATE.WaypointAction = {
    TurningPoint       = "Turning Point",
    FlyoverPoint       = "Fly Over Point",
    FromParkingArea    = "From Parking Area",
    FromParkingAreaHot = "From Parking Area Hot",
    FromGroundAreaHot  = "From Ground Area Hot",
    FromGroundArea     = "From Ground Area",
    FromRunway         = "From Runway",
    Landing            = "Landing",
    LandingReFuAr      = "LandingReFuAr",
  }

  --- Waypoint types.
  -- @type COORDINATE.WaypointType
  -- @field #string TakeOffParking Take of parking.
  -- @field #string TakeOffParkingHot Take of parking hot.
  -- @field #string TakeOff Take off parking hot.
  -- @field #string TakeOffGroundHot Take of from ground hot.
  -- @field #string TurningPoint Turning point.
  -- @field #string Land Landing point.
  -- @field #string LandingReFuAr Landing and refuel and rearm.
  COORDINATE.WaypointType = {
    TakeOffParking    = "TakeOffParking",
    TakeOffParkingHot = "TakeOffParkingHot",
    TakeOff           = "TakeOffParkingHot",
    TakeOffGroundHot  = "TakeOffGroundHot",
    TakeOffGround     = "TakeOffGround",
    TurningPoint      = "Turning Point",
    Land              = "Land",
    LandingReFuAr     = "LandingReFuAr",    
  }


  --- COORDINATE constructor.
  -- @param #COORDINATE self
  -- @param DCS#Distance x The x coordinate of the Vec3 point, pointing to the North.
  -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing to up.
  -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the right.
  -- @return #COORDINATE self
  function COORDINATE:New( x, y, z )

    local self=BASE:Inherit(self, BASE:New()) -- #COORDINATE
    
    self.x = x
    self.y = y
    self.z = z

    return self
  end

  --- COORDINATE constructor.
  -- @param #COORDINATE self
  -- @param #COORDINATE Coordinate.
  -- @return #COORDINATE self
  function COORDINATE:NewFromCoordinate( Coordinate )

    local self = BASE:Inherit( self, BASE:New() ) -- #COORDINATE
    self.x = Coordinate.x
    self.y = Coordinate.y
    self.z = Coordinate.z

    return self
  end

  --- Create a new COORDINATE object from  Vec2 coordinates.
  -- @param #COORDINATE self
  -- @param DCS#Vec2 Vec2 The Vec2 point.
  -- @param DCS#Distance LandHeightAdd (Optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height.
  -- @return #COORDINATE self
  function COORDINATE:NewFromVec2( Vec2, LandHeightAdd )

    local LandHeight = land.getHeight( Vec2 )

    LandHeightAdd = LandHeightAdd or 0
    LandHeight = LandHeight + LandHeightAdd

    local self = self:New( Vec2.x, LandHeight, Vec2.y ) -- #COORDINATE

    return self

  end

  --- Create a new COORDINATE object from  Vec3 coordinates.
  -- @param #COORDINATE self
  -- @param DCS#Vec3 Vec3 The Vec3 point.
  -- @return #COORDINATE self
  function COORDINATE:NewFromVec3( Vec3 )

    local self = self:New( Vec3.x, Vec3.y, Vec3.z ) -- #COORDINATE

    self:F2( self )

    return self
  end
  
  --- Create a new COORDINATE object from a waypoint. This uses the components
  -- 
  --  * `waypoint.x`
  --  * `waypoint.alt`
  --  * `waypoint.y`
  --  
  -- @param #COORDINATE self
  -- @param DCS#Waypoint Waypoint The waypoint.
  -- @return #COORDINATE self
  function COORDINATE:NewFromWaypoint(Waypoint)

    local self=self:New(Waypoint.x, Waypoint.alt, Waypoint.y) -- #COORDINATE
    
    return self
  end  

  --- Return the coordinates itself. Sounds stupid but can be useful for compatibility.
  -- @param #COORDINATE self
  -- @return #COORDINATE self
  function COORDINATE:GetCoordinate()
    return self
  end

  --- Return the coordinates of the COORDINATE in Vec3 format.
  -- @param #COORDINATE self
  -- @return DCS#Vec3 The Vec3 format coordinate.
  function COORDINATE:GetVec3()
    return { x = self.x, y = self.y, z = self.z }
  end


  --- Return the coordinates of the COORDINATE in Vec2 format.
  -- @param #COORDINATE self
  -- @return DCS#Vec2 The Vec2 format coordinate.
  function COORDINATE:GetVec2()
    return { x = self.x, y = self.z }
  end

  --- Update x,y,z coordinates from a given 3D vector.
  -- @param #COORDINATE self
  -- @param DCS#Vec3 Vec3 The 3D vector with x,y,z components.
  -- @return #COORDINATE The modified COORDINATE itself.
  function COORDINATE:UpdateFromVec3(Vec3)

    self.x=Vec3.x
    self.y=Vec3.y
    self.z=Vec3.z

    return self
  end

  --- Update x,y,z coordinates from another given COORDINATE.
  -- @param #COORDINATE self
  -- @param #COORDINATE Coordinate The coordinate with the new x,y,z positions.
  -- @return #COORDINATE The modified COORDINATE itself.
  function COORDINATE:UpdateFromCoordinate(Coordinate)

    self.x=Coordinate.x
    self.y=Coordinate.y
    self.z=Coordinate.z

    return self
  end

  --- Update x and z coordinates from a given 2D vector.
  -- @param #COORDINATE self
  -- @param DCS#Vec2 Vec2 The 2D vector with x,y components. x is overwriting COORDINATE.x while y is overwriting COORDINATE.z.
  -- @return #COORDINATE The modified COORDINATE itself.
  function COORDINATE:UpdateFromVec2(Vec2)

    self.x=Vec2.x
    self.z=Vec2.y

    return self
  end

  --- Returns the magnetic declination at the given coordinate.
  -- NOTE that this needs `require` to be available so you need to desanitize the `MissionScripting.lua` file in your DCS/Scrips folder.
  -- If `require` is not available, a constant value for the whole map.
  -- @param #COORDINATE self
  -- @param #number Month (Optional) The month at which the declination is calculated. Default is the mission month.
  -- @param #number Year (Optional) The year at which the declination is calculated. Default is the mission year.
  -- @return #number Magnetic declination in degrees.
  function COORDINATE:GetMagneticDeclination(Month, Year)
  
    local decl=UTILS.GetMagneticDeclination()
    
    if require then
      
      local magvar = require('magvar')
      
      if magvar then
      
        local date, year, month, day=UTILS.GetDCSMissionDate()
        
        magvar.init(Month or month, Year or year)
      
        local lat, lon=self:GetLLDDM()
        
        decl=magvar.get_mag_decl(lat, lon)
        
        if decl then
          decl=math.deg(decl)
        end
        
      end
    else
      self:T("The require package is not available. Using constant value for magnetic declination")    
    end
  
    return decl
  end

  --- Returns the coordinate from the latitude and longitude given in decimal degrees.
  -- @param #COORDINATE self
  -- @param #number latitude Latitude in decimal degrees.
  -- @param #number longitude Longitude in decimal degrees.
  -- @param #number altitude (Optional) Altitude in meters. Default is the land height at the coordinate.
  -- @return #COORDINATE
  function COORDINATE:NewFromLLDD( latitude, longitude, altitude)

    -- Returns a point from latitude and longitude in the vec3 format.
    local vec3=coord.LLtoLO(latitude, longitude)

    -- Convert vec3 to coordinate object.
    local _coord=self:NewFromVec3(vec3)

    -- Adjust height
    if altitude==nil then
      _coord.y=self:GetLandHeight()
    else
      _coord.y=altitude
    end

    return _coord
  end


  --- Returns if the 2 coordinates are at the same 2D position.
  -- @param #COORDINATE self
  -- @param #COORDINATE Coordinate
  -- @param #number Precision
  -- @return #boolean true if at the same position.
  function COORDINATE:IsAtCoordinate2D( Coordinate, Precision )

    self:F( { Coordinate = Coordinate:GetVec2() } )
    self:F( { self = self:GetVec2() } )

    local x = Coordinate.x
    local z = Coordinate.z

    return x - Precision <= self.x and x + Precision >= self.x and z - Precision <= self.z and z + Precision >= self.z
  end

  --- Scan/find objects (units, statics, scenery) within a certain radius around the coordinate using the world.searchObjects() DCS API function.
  -- @param #COORDINATE self
  -- @param #number radius (Optional) Scan radius in meters. Default 100 m.
  -- @param #boolean scanunits (Optional) If true scan for units. Default true.
  -- @param #boolean scanstatics (Optional) If true scan for static objects. Default true.
  -- @param #boolean scanscenery (Optional) If true scan for scenery objects. Default false.
  -- @return #boolean True if units were found.
  -- @return #boolean True if statics were found.
  -- @return #boolean True if scenery objects were found.
  -- @return #table Table of MOOSE @{Wrapper.Unit#UNIT} objects found.
  -- @return #table Table of DCS static objects found.
  -- @return #table Table of DCS scenery objects found.
  function COORDINATE:ScanObjects(radius, scanunits, scanstatics, scanscenery)
    self:F(string.format("Scanning in radius %.1f m.", radius or 100))

    local SphereSearch = {
      id = world.VolumeType.SPHERE,
        params = {
        point = self:GetVec3(),
        radius = radius,
        }
      }

    -- Defaults
    radius=radius or 100
    if scanunits==nil then
      scanunits=true
    end
    if scanstatics==nil then
      scanstatics=true
    end
    if scanscenery==nil then
      scanscenery=false
    end

    --{Object.Category.UNIT, Object.Category.STATIC, Object.Category.SCENERY}
    local scanobjects={}
    if scanunits then
      table.insert(scanobjects, Object.Category.UNIT)
    end
    if scanstatics then
      table.insert(scanobjects, Object.Category.STATIC)
    end
    if scanscenery then
      table.insert(scanobjects, Object.Category.SCENERY)
    end

    -- Found stuff.
    local Units = {}
    local Statics = {}
    local Scenery = {}
    local gotstatics=false
    local gotunits=false
    local gotscenery=false

    local function EvaluateZone(ZoneObject)

      if ZoneObject then

        -- Get category of scanned object.
        local ObjectCategory = Object.getCategory(ZoneObject)

        -- Check for unit or static objects
        if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() then

          table.insert(Units, UNIT:Find(ZoneObject))
          gotunits=true

        elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then

          table.insert(Statics, ZoneObject)
          gotstatics=true

        elseif ObjectCategory==Object.Category.SCENERY then

          table.insert(Scenery, ZoneObject)
          gotscenery=true

        end

      end

      return true
    end

    -- Search the world.
    world.searchObjects(scanobjects, SphereSearch, EvaluateZone)

    for _,unit in pairs(Units) do
      self:T(string.format("Scan found unit %s", unit:GetName()))
    end
    for _,static in pairs(Statics) do
      self:T(string.format("Scan found static %s", static:getName()))
      _DATABASE:AddStatic(static:getName())
    end
    for _,scenery in pairs(Scenery) do
      self:T(string.format("Scan found scenery %s typename=%s", scenery:getName(), scenery:getTypeName()))
      --SCENERY:Register(scenery:getName(), scenery)
    end

    return gotunits, gotstatics, gotscenery, Units, Statics, Scenery
  end

  --- Scan/find UNITS within a certain radius around the coordinate using the world.searchObjects() DCS API function.
  -- @param #COORDINATE self
  -- @param #number radius (Optional) Scan radius in meters. Default 100 m.
  -- @return Core.Set#SET_UNIT Set of units.
  function COORDINATE:ScanUnits(radius)

    local _,_,_,units=self:ScanObjects(radius, true, false, false)

    local set=SET_UNIT:New()

    for _,unit in pairs(units) do
      set:AddUnit(unit)
    end

    return set
  end
  
  --- Scan/find STATICS within a certain radius around the coordinate using the world.searchObjects() DCS API function.
  -- @param #COORDINATE self
  -- @param #number radius (Optional) Scan radius in meters. Default 100 m.
  -- @return Core.Set#SET_UNIT Set of units.
  function COORDINATE:ScanStatics(radius)

    local _,_,_,_,statics=self:ScanObjects(radius, false, true, false)

    local set=SET_STATIC:New()

    for _,stat in pairs(statics) do
      set:AddStatic(STATIC:Find(stat))
    end

    return set
  end

  --- Find the closest static to the COORDINATE within a certain radius.
  -- @param #COORDINATE self
  -- @param #number radius Scan radius in meters. Default 100 m.
  -- @return Wrapper.Static#STATIC The closest static or #nil if no unit is inside the given radius.
  function COORDINATE:FindClosestStatic(radius)

    local units=self:ScanStatics(radius)

    local umin=nil --Wrapper.Unit#UNIT
    local dmin=math.huge
    for _,_unit in pairs(units.Set) do
      local unit=_unit --Wrapper.Static#STATIC
      local coordinate=unit:GetCoordinate()
      local d=self:Get2DDistance(coordinate)
      if d<dmin then
        dmin=d
        umin=unit
      end
    end

    return umin
  end

  --- Find the closest unit to the COORDINATE within a certain radius.
  -- @param #COORDINATE self
  -- @param #number radius Scan radius in meters. Default 100 m.
  -- @return Wrapper.Unit#UNIT The closest unit or #nil if no unit is inside the given radius.
  function COORDINATE:FindClosestUnit(radius)

    local units=self:ScanUnits(radius)

    local umin=nil --Wrapper.Unit#UNIT
    local dmin=math.huge
    for _,_unit in pairs(units.Set) do
      local unit=_unit --Wrapper.Unit#UNIT
      local coordinate=unit:GetCoordinate()
      local d=self:Get2DDistance(coordinate)
      if d<dmin then
        dmin=d
        umin=unit
      end
    end

    return umin
  end

  --- Scan/find SCENERY objects within a certain radius around the coordinate using the world.searchObjects() DCS API function.
  -- @param #COORDINATE self
  -- @param #number radius (Optional) Scan radius in meters. Default 100 m.
  -- @return table Table of SCENERY objects.
  function COORDINATE:ScanScenery(radius)

    local _,_,_,_,_,scenerys=self:ScanObjects(radius, false, false, true)

    local set={}

    for _,_scenery in pairs(scenerys) do
      local scenery=_scenery --DCS#Object

      local name=scenery:getName()
      local s=SCENERY:Register(name, scenery)
      table.insert(set, s)

    end

    return set
  end

  --- Find the closest scenery to the COORDINATE within a certain radius.
  -- @param #COORDINATE self
  -- @param #number radius Scan radius in meters. Default 100 m.
  -- @return Wrapper.Scenery#SCENERY The closest scenery or #nil if no object is inside the given radius.
  function COORDINATE:FindClosestScenery(radius)

    local sceneries=self:ScanScenery(radius)

    local umin=nil --Wrapper.Scenery#SCENERY
    local dmin=math.huge
    for _,_scenery in pairs(sceneries) do
      local scenery=_scenery --Wrapper.Scenery#SCENERY
      local coordinate=scenery:GetCoordinate()
      local d=self:Get2DDistance(coordinate)
      if d<dmin then
        dmin=d
        umin=scenery
      end
    end

    return umin
  end

  --- Calculate the distance from a reference @{#COORDINATE}.
  -- @param #COORDINATE self
  -- @param #COORDINATE PointVec2Reference The reference @{#COORDINATE}.
  -- @return DCS#Distance The distance from the reference @{#COORDINATE} in meters.
  function COORDINATE:DistanceFromPointVec2( PointVec2Reference )
    self:F2( PointVec2Reference )  
    if not PointVec2Reference then return math.huge end
    
    local Distance = ( ( PointVec2Reference.x - self.x ) ^ 2 + ( PointVec2Reference.z - self.z ) ^2 ) ^ 0.5

    self:T2( Distance )
    return Distance
  end

  --- Add a Distance in meters from the COORDINATE orthonormal plane, with the given angle, and calculate the new COORDINATE.
  -- @param #COORDINATE self
  -- @param DCS#Distance Distance The Distance to be added in meters.
  -- @param DCS#Angle Angle The Angle in degrees. Defaults to 0 if not specified (nil).
  -- @param #boolean Keepalt If true, keep altitude of original coordinate. Default is that the new coordinate is created at the translated land height.
  -- @param #boolean Overwrite If true, overwrite the original COORDINATE with the translated one. Otherwise, create a new COORDINATE.
  -- @return #COORDINATE The new calculated COORDINATE.
  function COORDINATE:Translate( Distance, Angle, Keepalt, Overwrite )

    -- Angle in rad.
    local alpha = math.rad((Angle or 0))

    local x = Distance * math.cos(alpha) + self.x  -- New x
    local z = Distance * math.sin(alpha) + self.z  -- New z

    local y=Keepalt and self.y or land.getHeight({x=x, y=z})

    if Overwrite then
      self.x=x
      self.y=y
      self.z=z
      return self
    else
      --env.info("FF translate with NEW coordinate T="..timer.getTime())
      local coord=COORDINATE:New(x, y, z)
      return coord
    end

  end

  --- Rotate coordinate in 2D (x,z) space.
  -- @param #COORDINATE self
  -- @param DCS#Angle Angle Angle of rotation in degrees.
  -- @return Core.Point#COORDINATE The rotated coordinate.
  function COORDINATE:Rotate2D(Angle)

    if not Angle then
      return self
    end

    local phi=math.rad(Angle)

    local X=self.z
    local Y=self.x

    --slocal R=math.sqrt(X*X+Y*Y)

    local x=X*math.cos(phi)-Y*math.sin(phi)
    local y=X*math.sin(phi)+Y*math.cos(phi)

    -- Coordinate assignment looks bit strange but is correct.
    local coord=COORDINATE:NewFromVec3({x=y, y=self.y, z=x})
    return coord
  end

  --- Return a random Vec2 within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE.
  -- @param #COORDINATE self
  -- @param DCS#Distance OuterRadius
  -- @param DCS#Distance InnerRadius
  -- @return DCS#Vec2 Vec2
  function COORDINATE:GetRandomVec2InRadius( OuterRadius, InnerRadius )
    self:F2( { OuterRadius, InnerRadius } )
    math.random()
    math.random()
    math.random()
    local Theta = 2 * math.pi * math.random()
    local Radials = math.random() + math.random()
    if Radials > 1 then
      Radials = 2 - Radials
    end

    local RadialMultiplier
    if InnerRadius and InnerRadius <= OuterRadius then
      RadialMultiplier = ( OuterRadius - InnerRadius ) * Radials + InnerRadius
    else
      RadialMultiplier = OuterRadius * Radials
    end

    local RandomVec2
    if OuterRadius > 0 then
      RandomVec2 = { x = math.cos( Theta ) * RadialMultiplier + self.x, y = math.sin( Theta ) * RadialMultiplier + self.z }
    else
      RandomVec2 = { x = self.x, y = self.z }
    end

    return RandomVec2
  end


  --- Return a random Coordinate within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE.
  -- @param #COORDINATE self
  -- @param DCS#Distance OuterRadius Outer radius in meters.
  -- @param DCS#Distance InnerRadius Inner radius in meters.
  -- @return #COORDINATE self
  function COORDINATE:GetRandomCoordinateInRadius( OuterRadius, InnerRadius )
    self:F2( { OuterRadius, InnerRadius } )

    local coord=COORDINATE:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) )
    return coord
  end


  --- Return a random Vec3 within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE.
  -- @param #COORDINATE self
  -- @param DCS#Distance OuterRadius
  -- @param DCS#Distance InnerRadius
  -- @return DCS#Vec3 Vec3
  function COORDINATE:GetRandomVec3InRadius( OuterRadius, InnerRadius )

    local RandomVec2 = self:GetRandomVec2InRadius( OuterRadius, InnerRadius )
    local y = self.y + math.random( InnerRadius, OuterRadius )
    local RandomVec3 = { x = RandomVec2.x, y = y, z = RandomVec2.y }

    return RandomVec3
  end

  --- Return the height of the land at the coordinate.
  -- @param #COORDINATE self
  -- @return #number Land height (ASL) in meters.
  function COORDINATE:GetLandHeight()
    local Vec2 = { x = self.x, y = self.z }
    return land.getHeight( Vec2 )
  end

  --- Returns a table of DCS#Vec3 points representing the terrain profile between two points.
  -- @param #COORDINATE self
  -- @param Destination DCS#Vec3 Ending point of the profile.
  -- @return #table DCS#Vec3 table of the profile
  function COORDINATE:GetLandProfileVec3(Destination)
    return land.profile(self:GetVec3(), Destination)
  end

  --- Returns a table of #COORDINATE representing the terrain profile between two points.
  -- @param #COORDINATE self
  -- @param Destination #COORDINATE Ending coordinate of the profile.
  -- @return #table #COORDINATE table of the profile
  function COORDINATE:GetLandProfileCoordinates(Destination)
    local points = self:GetLandProfileVec3(Destination:GetVec3())
    local coords = {}
    for _, point in ipairs(points) do
      table.insert(coords, COORDINATE:NewFromVec3(point))
    end
    return coords
  end

  --- Set the heading of the coordinate, if applicable.
  -- @param #COORDINATE self
  function COORDINATE:SetHeading( Heading )
    self.Heading = Heading
  end


  --- Get the heading of the coordinate, if applicable.
  -- @param #COORDINATE self
  -- @return #number or nil
  function COORDINATE:GetHeading()
    return self.Heading
  end


  --- Set the velocity of the COORDINATE.
  -- @param #COORDINATE self
  -- @param #string Velocity Velocity in meters per second.
  function COORDINATE:SetVelocity( Velocity )
    self.Velocity = Velocity
  end


  --- Return the velocity of the COORDINATE.
  -- @param #COORDINATE self
  -- @return #number Velocity in meters per second.
  function COORDINATE:GetVelocity()
    local Velocity = self.Velocity
    return Velocity or 0
  end

  --- Return the "name" of the COORDINATE. Obviously, a coordinate does not have a name like a unit, static or group. So here we take the MGRS coordinates of the position.
  -- @param #COORDINATE self
  -- @return #string MGRS coordinates.
  function COORDINATE:GetName()
    local name=self:ToStringMGRS()
    return name
  end

  --- Return velocity text of the COORDINATE.
  -- @param #COORDINATE self
  -- @return #string
  function COORDINATE:GetMovingText( Settings )

    return self:GetVelocityText( Settings ) .. ", " .. self:GetHeadingText( Settings )
  end


  --- Return a direction vector Vec3 from COORDINATE to the COORDINATE.
  -- @param #COORDINATE self
  -- @param #COORDINATE TargetCoordinate The target COORDINATE.
  -- @return DCS#Vec3 DirectionVec3 The direction vector in Vec3 format.
  function COORDINATE:GetDirectionVec3( TargetCoordinate )
    if TargetCoordinate then
      return { x = TargetCoordinate.x - self.x, y = TargetCoordinate.y - self.y, z = TargetCoordinate.z - self.z }
    else
      return { x=0,y=0,z=0}
    end
  end


  --- Get a correction in radians of the real magnetic north of the COORDINATE.
  -- @param #COORDINATE self
  -- @return #number CorrectionRadians The correction in radians.
  function COORDINATE:GetNorthCorrectionRadians()
    local TargetVec3 = self:GetVec3()
    local lat, lon = coord.LOtoLL(TargetVec3)
    local north_posit = coord.LLtoLO(lat + 1, lon)
    return math.atan2( north_posit.z - TargetVec3.z, north_posit.x - TargetVec3.x )
  end


  --- Return an angle in radians from the COORDINATE using a **direction vector in Vec3 format**.
  -- @param #COORDINATE self
  -- @param DCS#Vec3 DirectionVec3 The direction vector in Vec3 format.
  -- @return #number DirectionRadians The angle in radians.
  function COORDINATE:GetAngleRadians( DirectionVec3 )
    local DirectionRadians = math.atan2( DirectionVec3.z, DirectionVec3.x )
    --DirectionRadians = DirectionRadians + self:GetNorthCorrectionRadians()
    if DirectionRadians < 0 then
      DirectionRadians = DirectionRadians + 2 * math.pi  -- put dir in range of 0 to 2*pi ( the full circle )
    end
    return DirectionRadians
  end

  --- Return an angle in degrees from the COORDINATE using a **direction vector in Vec3 format**.
  -- @param #COORDINATE self
  -- @param DCS#Vec3 DirectionVec3 The direction vector in Vec3 format.
  -- @return #number DirectionRadians The angle in degrees.
  -- @usage
  --         local directionAngle = currentCoordinate:GetAngleDegrees(currentCoordinate:GetDirectionVec3(sourceCoordinate:GetVec3()))
  function COORDINATE:GetAngleDegrees( DirectionVec3 )
    local AngleRadians = self:GetAngleRadians( DirectionVec3 )
    local Angle = UTILS.ToDegree( AngleRadians )
    return Angle
  end

  --- Return an intermediate COORDINATE between this an another coordinate.
  -- @param #COORDINATE self
  -- @param #COORDINATE ToCoordinate The other coordinate.
  -- @param #number Fraction The fraction (0,1) where the new coordinate is created. Default 0.5, i.e. in the middle.
  -- @return #COORDINATE Coordinate between this and the other coordinate.
  function COORDINATE:GetIntermediateCoordinate( ToCoordinate, Fraction )

    local f=Fraction or 0.5

    -- Get the vector from A to B
    local vec=UTILS.VecSubstract(ToCoordinate, self)
    
    if f>1 then
      local norm=UTILS.VecNorm(vec)      
      f=Fraction/norm
    end

    -- Scale the vector.
    vec.x=f*vec.x
    vec.y=f*vec.y
    vec.z=f*vec.z

    -- Move the vector to start at the end of A.
    vec=UTILS.VecAdd(self, vec)

    -- Create a new coordiante object.
    local coord=COORDINATE:New(vec.x,vec.y,vec.z)
    
    return coord
  end

  --- Return the 2D distance in meters between the target COORDINATE and the COORDINATE.
  -- @param #COORDINATE self
  -- @param #COORDINATE TargetCoordinate The target COORDINATE. Can also be a DCS#Vec3.
  -- @return DCS#Distance Distance The distance in meters.
  function COORDINATE:Get2DDistance(TargetCoordinate)
    if not TargetCoordinate then return 1000000 end
    --local a={x=TargetCoordinate.x-self.x, y=0, z=TargetCoordinate.z-self.z}
    local a = self:GetVec2()
    if not TargetCoordinate.ClassName then
      TargetCoordinate=COORDINATE:NewFromVec3(TargetCoordinate)
    end
    local b = TargetCoordinate:GetVec2()
    local norm=UTILS.VecDist2D(a,b)
    return norm
  end

  --- Returns the temperature in Degrees Celsius.
  -- @param #COORDINATE self
  -- @param height (Optional) parameter specifying the height ASL.
  -- @return Temperature in Degrees Celsius.
  function COORDINATE:GetTemperature(height)
    self:F2(height)
    local y=height or self.y
    local point={x=self.x, y=height or self.y, z=self.z}
    -- get temperature [K] and pressure [Pa] at point
    local T,P=atmosphere.getTemperatureAndPressure(point)
    -- Return Temperature in Deg C
    return T-273.15
  end

  --- Returns a text of the temperature according the measurement system @{Core.Settings}.
  -- The text will reflect the temperature like this:
  --
  --   - For Russian and European aircraft using the metric system - Degrees Celcius (°C)
  --   - For American aircraft we link to the imperial system - Degrees Fahrenheit (°F)
  --
  -- A text containing a pressure will look like this:
  --
  --   - `Temperature: %n.d °C`
  --   - `Temperature: %n.d °F`
  --
   -- @param #COORDINATE self
  -- @param height (Optional) parameter specifying the height ASL.
  -- @return #string Temperature according the measurement system @{Core.Settings}.
  function COORDINATE:GetTemperatureText( height, Settings )

    local DegreesCelcius = self:GetTemperature( height )

    local Settings = Settings or _SETTINGS

    if DegreesCelcius then
      if Settings:IsMetric() then
        return string.format( " %-2.2f °C", DegreesCelcius )
      else
        return string.format( " %-2.2f °F", UTILS.CelsiusToFahrenheit( DegreesCelcius ) )
      end
    else
      return " no temperature"
    end

    return nil
  end


  --- Returns the pressure in hPa.
  -- @param #COORDINATE self
  -- @param height (Optional) parameter specifying the height ASL. E.g. set height=0 for QNH.
  -- @return Pressure in hPa.
  function COORDINATE:GetPressure(height)
    local point={x=self.x, y=height or self.y, z=self.z}
    -- get temperature [K] and pressure [Pa] at point
    local T,P=atmosphere.getTemperatureAndPressure(point)
    -- Return Pressure in hPa.
    return P/100
  end

  --- Returns a text of the pressure according the measurement system @{Core.Settings}.
  -- The text will contain always the pressure in hPa and:
  --
  --   - For Russian and European aircraft using the metric system - hPa and mmHg
  --   - For American and European aircraft we link to the imperial system - hPa and inHg
  --
  -- A text containing a pressure will look like this:
  --
  --   - `QFE: x hPa (y mmHg)`
  --   - `QFE: x hPa (y inHg)`
  --
  -- @param #COORDINATE self
  -- @param height (Optional) parameter specifying the height ASL. E.g. set height=0 for QNH.
  -- @return #string Pressure in hPa and mmHg or inHg depending on the measurement system @{Core.Settings}.
  function COORDINATE:GetPressureText( height, Settings )

    local Pressure_hPa = self:GetPressure( height )
    local Pressure_mmHg = Pressure_hPa * 0.7500615613030
    local Pressure_inHg = Pressure_hPa * 0.0295299830714

    local Settings = Settings or _SETTINGS

    if Pressure_hPa then
      if Settings:IsMetric() then
        return string.format( " %4.1f hPa (%3.1f mmHg)", Pressure_hPa, Pressure_mmHg )
      else
        return string.format( " %4.1f hPa (%3.2f inHg)", Pressure_hPa, Pressure_inHg )
      end
    else
      return " no pressure"
    end

    return nil
  end

  --- Returns the heading from this to another coordinate.
  -- @param #COORDINATE self
  -- @param #COORDINATE ToCoordinate
  -- @return #number Heading in degrees.
  function COORDINATE:HeadingTo(ToCoordinate)
    local dz=ToCoordinate.z-self.z
    local dx=ToCoordinate.x-self.x
    local heading=math.deg(math.atan2(dz, dx))
    if heading < 0 then
      heading = 360 + heading
    end
    return heading
  end

  --- Returns the 3D wind direction vector. Note that vector points into the direction the wind in blowing to.
  -- @param #COORDINATE self
  -- @param #number height (Optional) parameter specifying the height ASL in meters. The minimum height will be always be the land height since the wind is zero below the ground.
  -- @param #boolean turbulence (Optional) If `true`, include turbulence.
  -- @return DCS#Vec3 Wind 3D vector. Components in m/s.
  function COORDINATE:GetWindVec3(height, turbulence)
  
    -- We at 0.1 meters to be sure to be above ground since wind is zero below ground level.
    local landheight=self:GetLandHeight()+0.1 
  
    local point={x=self.x, y=math.max(height or self.y, landheight), z=self.z}
        
    -- Get wind velocity vector.
    local wind = nil --DCS#Vec3
    
    if turbulence then
      wind = atmosphere.getWindWithTurbulence(point)
    else
      wind = atmosphere.getWind(point)
    end
    
    return wind
  end

  --- Returns the wind direction (from) and strength.
  -- @param #COORDINATE self
  -- @param #number height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground.
  -- @param #boolean turbulence If `true`, include turbulence. If `false` or `nil`, wind without turbulence.
  -- @return #number Direction the wind is blowing from in degrees.
  -- @return #number Wind strength in m/s.
  function COORDINATE:GetWind(height, turbulence)

    -- Get wind velocity vector
    local wind = self:GetWindVec3(height, turbulence)

    -- Calculate the direction of the vector.    
    local direction=UTILS.VecHdg(wind)
    
    -- Invert "to" direction to "from" direction.
    if direction > 180 then
      direction = direction-180
    else
      direction = direction+180
    end
    
    -- Wind strength in m/s.
    local strength=UTILS.VecNorm(wind) -- math.sqrt((wind.x)^2+(wind.z)^2)
    
    -- Return wind direction and strength.
    return direction, strength
  end

  --- Returns the wind direction (from) and strength.
  -- @param #COORDINATE self
  -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground.
  -- @return Direction the wind is blowing from in degrees.
  function COORDINATE:GetWindWithTurbulenceVec3(height)

    -- AGL height if
    local landheight=self:GetLandHeight()+0.1 -- we at 0.1 meters to be sure to be above ground since wind is zero below ground level.

    -- Point at which the wind is evaluated.
    local point={x=self.x, y=math.max(height or self.y, landheight), z=self.z}

    -- Get wind velocity vector including turbulences.
    local vec3 = atmosphere.getWindWithTurbulence(point)

    return vec3
  end

  --- Return the x coordinate of the COORDINATE.
  -- @param #COORDINATE self
  -- @return #number The x coordinate.
  function COORDINATE:GetX()
    return self.x
  end

  --- Return the y coordinate of the COORDINATE.
  -- @param #COORDINATE self
  -- @return #number The y coordinate.
  function COORDINATE:GetY()
    if self:IsInstanceOf("POINT_VEC2") then
      return self.z
    end
    return self.y
  end

  --- Return the z coordinate of the COORDINATE.
  -- @param #COORDINATE self
  -- @return #number The z coordinate.
  function COORDINATE:GetZ()
    return self.z
  end

  --- Set the x coordinate of the COORDINATE.
  -- @param #COORDINATE self
  -- @param #number x The x coordinate.
  -- @return #COORDINATE
  function COORDINATE:SetX( x )
    self.x = x
    return self
  end

  --- Set the y coordinate of the COORDINATE.
  -- @param #COORDINATE self
  -- @param #number y The y coordinate.
  -- @return #COORDINATE
  function COORDINATE:SetY( y )
    if self:IsInstanceOf("POINT_VEC2") then
      self.z = y
    else
      self.y = y
    end
    return self
  end

  --- Set the z coordinate of the COORDINATE.
  -- @param #COORDINATE self
  -- @param #number z The z coordinate.
  -- @return #COORDINATE
  function COORDINATE:SetZ( z )
    self.z = z
    return self
  end

  --- Add to the x coordinate of the COORDINATE.
  -- @param #COORDINATE self
  -- @param #number x The x coordinate value to add to the current x coordinate.
  -- @return #COORDINATE
  function COORDINATE:AddX( x )
    self.x = self.x + x
    return self
  end
  
  
  --- Return Return the Lat(itude) coordinate of the COORDINATE (ie: (parent)COORDINATE.x).
  -- @param #COORDINATE self
  -- @return #number The x coordinate.
  function COORDINATE:GetLat()
    return self.x
  end

  --- Set the Lat(itude) coordinate of the COORDINATE (ie: COORDINATE.x).
  -- @param #COORDINATE self
  -- @param #number x The x coordinate.
  -- @return #COORDINATE
  function COORDINATE:SetLat( x )
    self.x = x
    return self
  end

  --- Return the Lon(gitude) coordinate of the COORDINATE (ie: (parent)COORDINATE.z).
  -- @param #COORDINATE self
  -- @return #number The y coordinate.
  function COORDINATE:GetLon()
    return self.z
  end

  --- Set the Lon(gitude) coordinate of the COORDINATE (ie: COORDINATE.z).
  -- @param #COORDINATE self
  -- @param #number y The y coordinate.
  -- @return #COORDINATE
  function COORDINATE:SetLon( z )
    self.z = z
    return self
  end

  --- Return the altitude (height) of the land at the COORDINATE.
  -- @param #COORDINATE self
  -- @return #number The land altitude.
  function COORDINATE:GetAlt()
    return self.y ~= 0 or land.getHeight( { x = self.x, y = self.z } )
  end

  --- Set the altitude of the COORDINATE.
  -- @param #COORDINATE self
  -- @param #number Altitude The land altitude. If nothing (nil) is given, then the current land altitude is set.
  -- @return #COORDINATE
  function COORDINATE:SetAlt( Altitude )
    self.y = Altitude or land.getHeight( { x = self.x, y = self.z } )
    return self
  end

  --- Add to the current land height an altitude.
  -- @param #COORDINATE self
  -- @param #number Altitude The Altitude to add. If nothing (nil) is given, then the current land altitude is set.
  -- @return #COORDINATE
  function COORDINATE:AddAlt( Altitude )
    self.y = land.getHeight( { x = self.x, y = self.z } ) + Altitude or 0
    return self
  end


  --- Return a random COORDINATE within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE.
  -- @param #COORDINATE self
  -- @param DCS#Distance OuterRadius
  -- @param DCS#Distance InnerRadius
  -- @return #COORDINATE
  function COORDINATE:GetRandomPointVec2InRadius( OuterRadius, InnerRadius )
    self:F2( { OuterRadius, InnerRadius } )

    return COORDINATE:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) )
  end

  --- Add to the y coordinate of the COORDINATE.
  -- @param #COORDINATE self
  -- @param #number y The y coordinate value to add to the current y coordinate.
  -- @return #COORDINATE
  function COORDINATE:AddY( y )
    if self:IsInstanceOf("POINT_VEC2") then
      return self:AddZ(y)
    else
      self.y = self.y + y
    end
    return self
  end

  --- Add to the z coordinate of the COORDINATE.
  -- @param #COORDINATE self
  -- @param #number z The z coordinate value to add to the current z coordinate.
  -- @return #COORDINATE
  function COORDINATE:AddZ( z )
    self.z = self.z +z
    return self
  end


  --- Returns a text documenting the wind direction (from) and strength according the measurement system @{Core.Settings}.
  -- The text will reflect the wind like this:
  --
  --   - For Russian and European aircraft using the metric system - Wind direction in degrees (°) and wind speed in meters per second (mps).
  --   - For American aircraft we link to the imperial system - Wind direction in degrees (°) and wind speed in knots per second (kps).
  --
  -- A text containing a pressure will look like this:
  --
  --   - `Wind: %n ° at n.d mps`
  --   - `Wind: %n ° at n.d kps`
  --
  -- @param #COORDINATE self
  -- @param height (Optional) parameter specifying the height ASL. The minimum height will be always be the land height since the wind is zero below the ground.
  -- @return #string Wind direction and strength according the measurement system @{Core.Settings}.
  function COORDINATE:GetWindText( height, Settings )

    local Direction, Strength = self:GetWind( height )

    local Settings = Settings or _SETTINGS

    if Direction and Strength then
      if Settings:IsMetric() then
        return string.format( " %d ° at %3.2f mps", Direction, UTILS.MpsToKmph( Strength ) )
      else
        return string.format( " %d ° at %3.2f kps", Direction, UTILS.MpsToKnots( Strength ) )
      end
    else
      return " no wind"
    end

    return nil
  end

  --- Return the 3D distance in meters between the target COORDINATE and the COORDINATE.
  -- @param #COORDINATE self
  -- @param #COORDINATE TargetCoordinate The target COORDINATE. Can also be a DCS#Vec3.
  -- @return DCS#Distance Distance The distance in meters.
  function COORDINATE:Get3DDistance( TargetCoordinate )
    --local TargetVec3 = TargetCoordinate:GetVec3()
    local TargetVec3 = {x=TargetCoordinate.x, y=TargetCoordinate.y, z=TargetCoordinate.z}
    local SourceVec3 = self:GetVec3()
    --local dist=( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.y - SourceVec3.y ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5
    local dist=UTILS.VecDist3D(TargetVec3, SourceVec3)
    return dist
  end


  --- Provides a bearing text in degrees.
  -- @param #COORDINATE self
  -- @param #number AngleRadians The angle in randians.
  -- @param #number Precision The precision.
  -- @param Core.Settings#SETTINGS Settings
  -- @param #boolean MagVar If true, include magentic degrees
  -- @return #string The bearing text in degrees.
  function COORDINATE:GetBearingText( AngleRadians, Precision, Settings, MagVar )

    local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS

    local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), Precision )

    local s = string.format( '%03d°', AngleDegrees )
    
    if MagVar then
      local variation = self:GetMagneticDeclination() or 0
      local AngleMagnetic = AngleDegrees - variation
      
      if AngleMagnetic < 0 then AngleMagnetic = 360-AngleMagnetic end
      
      s = string.format( '%03d°M|%03d°', AngleMagnetic,AngleDegrees )
    end
    
    return s
  end

  --- Provides a distance text expressed in the units of measurement.
  -- @param #COORDINATE self
  -- @param #number Distance The distance in meters.
  -- @param Core.Settings#SETTINGS Settings
  -- @param #string Language (optional) "EN" or "RU"
  -- @param #number Precision (optional) round to this many decimal places
  -- @return #string The distance text expressed in the units of measurement.
  function COORDINATE:GetDistanceText( Distance, Settings, Language, Precision )

    local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS
    local Language = Language or Settings.Locale or _SETTINGS.Locale or "EN"
    Language = string.lower(Language)
    local Precision = Precision or 0
    
    local DistanceText

    if Settings:IsMetric() then
      if     Language == "en" then
        DistanceText = " for " .. UTILS.Round( Distance / 1000, Precision ) .. " km"
      elseif Language == "ru" then
        DistanceText = " за " .. UTILS.Round( Distance / 1000, Precision ) .. " километров"
      end
    else
      if     Language == "en" then
        DistanceText = " for " .. UTILS.Round( UTILS.MetersToNM( Distance ), Precision ) .. " miles"
      elseif Language == "ru" then
        DistanceText = " за " .. UTILS.Round( UTILS.MetersToNM( Distance ), Precision ) .. " миль"
      end
    end

    return DistanceText
  end

  --- Return the altitude text of the COORDINATE.
  -- @param #COORDINATE self
  -- @return #string Altitude text.
  function COORDINATE:GetAltitudeText( Settings, Language )
    local Altitude = self.y
    local Settings = Settings or _SETTINGS
    local Language = Language or Settings.Locale or _SETTINGS.Locale or "EN"
    
    Language = string.lower(Language)
    
    if Altitude ~= 0 then
      if Settings:IsMetric() then
        if     Language == "en" then
          return " at " .. UTILS.Round( self.y, -3 ) .. " meters"
        elseif Language == "ru" then
          return " в " .. UTILS.Round( self.y, -3 ) .. " метры"
        end
      else
        if     Language == "en" then
          return " at " .. UTILS.Round( UTILS.MetersToFeet( self.y ), -3 ) .. " feet"
        elseif Language == "ru" then
          return " в " .. UTILS.Round( self.y, -3 ) .. " ноги"
        end
      end
    else
      return ""
    end
  end



  --- Return the velocity text of the COORDINATE.
  -- @param #COORDINATE self
  -- @return #string Velocity text.
  function COORDINATE:GetVelocityText( Settings )
    local Velocity = self:GetVelocity()
    local Settings = Settings or _SETTINGS
    if Velocity then
      if Settings:IsMetric() then
        return string.format( " moving at %d km/h", UTILS.MpsToKmph( Velocity ) )
      else
        return string.format( " moving at %d mi/h", UTILS.MpsToKmph( Velocity ) / 1.852 )
      end
    else
      return " stationary"
    end
  end


  --- Return the heading text of the COORDINATE.
  -- @param #COORDINATE self
  -- @return #string Heading text.
  function COORDINATE:GetHeadingText( Settings )
    local Heading = self:GetHeading()
    if Heading then
      return string.format( " bearing %3d°", Heading )
    else
      return " bearing unknown"
    end
  end


  --- Provides a Bearing / Range string
  -- @param #COORDINATE self
  -- @param #number AngleRadians The angle in randians
  -- @param #number Distance The distance
  -- @param Core.Settings#SETTINGS Settings
  -- @param #string Language (Optional) Language "en" or "ru"
  -- @param #boolean MagVar If true, also state angle in magnetic
  -- @param #number Precision Rounding precision, defaults to 0
  -- @return #string The BR Text
  function COORDINATE:GetBRText( AngleRadians, Distance, Settings, Language, MagVar, Precision )

    local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS
      
    Precision = Precision or 0
      
    local BearingText = self:GetBearingText( AngleRadians, 0, Settings, MagVar )
    local DistanceText = self:GetDistanceText( Distance, Settings, Language, Precision )

    local BRText = BearingText .. DistanceText

    return BRText
  end

  --- Provides a Bearing / Range / Altitude string
  -- @param #COORDINATE self
  -- @param #number AngleRadians The angle in randians
  -- @param #number Distance The distance
  -- @param Core.Settings#SETTINGS Settings
  -- @param #string Language (Optional) Language "en" or "ru"
  -- @param #boolean MagVar If true, also state angle in magnetic
  -- @return #string The BRA Text
  function COORDINATE:GetBRAText( AngleRadians, Distance, Settings, Language, MagVar )

    local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS

    local BearingText = self:GetBearingText( AngleRadians, 0, Settings, MagVar )
    local DistanceText = self:GetDistanceText( Distance, Settings, Language, 0  )
    local AltitudeText = self:GetAltitudeText( Settings, Language  )

    local BRAText = BearingText .. DistanceText .. AltitudeText -- When the POINT is a VEC2, there will be no altitude shown.

    return BRAText
  end


  --- Set altitude.
  -- @param #COORDINATE self
  -- @param #number altitude New altitude in meters.
  -- @param #boolean asl Altitude above sea level. Default is above ground level.
  -- @return #COORDINATE The COORDINATE with adjusted altitude.
  function COORDINATE:SetAltitude(altitude, asl)
    local alt=altitude
    if asl then
      alt=altitude
    else
      alt=self:GetLandHeight()+altitude
    end
    self.y=alt
    return self
  end
  
  --- Set altitude to be at land height (i.e. on the ground!)
  -- @param #COORDINATE self
  function COORDINATE:SetAtLandheight()
    local alt=self:GetLandHeight()
    self.y=alt
    return self
  end
  
  --- Build an air type route point.
  -- @param #COORDINATE self
  -- @param #COORDINATE.WaypointAltType AltType The altitude type.
  -- @param #COORDINATE.WaypointType Type The route point type.
  -- @param #COORDINATE.WaypointAction Action The route point action.
  -- @param DCS#Speed Speed Airspeed in km/h. Default is 500 km/h.
  -- @param #boolean SpeedLocked true means the speed is locked.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points.
  -- @param #table DCSTasks A table of @{DCS#Task} items which are executed at the waypoint.
  -- @param #string description A text description of the waypoint, which will be shown on the F10 map.
  -- @param #number timeReFuAr Time in minutes the aircraft stays at the airport for ReFueling and ReArming.
  -- @return #table The route point.
  function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked, airbase, DCSTasks, description, timeReFuAr )
    self:F2( { AltType, Type, Action, Speed, SpeedLocked } )

    -- Set alttype or "RADIO" which is AGL.
    AltType=AltType or "RADIO"

    -- Speedlocked by default
    if SpeedLocked==nil then
      SpeedLocked=true
    end

    -- Speed or default 500 km/h.
    Speed=Speed or 500

    -- Waypoint array.
    local RoutePoint = {}

    -- Coordinates.
    RoutePoint.x = self.x
    RoutePoint.y = self.z

    -- Altitude.
    RoutePoint.alt = self.y
    RoutePoint.alt_type = AltType

    -- Waypoint type.
    RoutePoint.type = Type or nil
    RoutePoint.action = Action or nil

    -- Speed.
    RoutePoint.speed = Speed/3.6
    RoutePoint.speed_locked = SpeedLocked

    -- ETA.
    RoutePoint.ETA=0
    RoutePoint.ETA_locked=false

    -- Waypoint description.
    RoutePoint.name=description

    -- Airbase parameters for takeoff and landing points.
    if airbase then
      local AirbaseID = airbase:GetID()
      local AirbaseCategory = airbase:GetAirbaseCategory()
      if AirbaseCategory == Airbase.Category.SHIP or AirbaseCategory == Airbase.Category.HELIPAD then
        RoutePoint.linkUnit = AirbaseID
        RoutePoint.helipadId = AirbaseID
      elseif AirbaseCategory == Airbase.Category.AIRDROME then
        RoutePoint.airdromeId = AirbaseID
      else
        self:E("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!")
      end
    end

    -- Time in minutes to stay at the airbase before resuming route.
    if Type==COORDINATE.WaypointType.LandingReFuAr then
      RoutePoint.timeReFuAr=timeReFuAr or 10
    end

    -- Waypoint tasks.
    RoutePoint.task = {}
    RoutePoint.task.id = "ComboTask"
    RoutePoint.task.params = {}
    RoutePoint.task.params.tasks = DCSTasks or {}

    --RoutePoint.properties={}
    --RoutePoint.properties.addopt={}

    --RoutePoint.formation_template=""

    -- Debug.
    self:T({RoutePoint=RoutePoint})

    -- Return waypoint.
    return RoutePoint
  end


  --- Build a Waypoint Air "Turning Point".
  -- @param #COORDINATE self
  -- @param #COORDINATE.WaypointAltType AltType The altitude type.
  -- @param DCS#Speed Speed Airspeed in km/h.
  -- @param #table DCSTasks (Optional) A table of @{DCS#Task} items which are executed at the waypoint.
  -- @param #string description (Optional) A text description of the waypoint, which will be shown on the F10 map.
  -- @return #table The route point.
  function COORDINATE:WaypointAirTurningPoint( AltType, Speed, DCSTasks, description )
    return self:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true, nil, DCSTasks, description )
  end


  --- Build a Waypoint Air "Fly Over Point".
  -- @param #COORDINATE self
  -- @param #COORDINATE.WaypointAltType AltType The altitude type.
  -- @param DCS#Speed Speed Airspeed in km/h.
  -- @return #table The route point.
  function COORDINATE:WaypointAirFlyOverPoint( AltType, Speed )
    return self:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.FlyoverPoint, Speed )
  end


  --- Build a Waypoint Air "Take Off Parking Hot".
  -- @param #COORDINATE self
  -- @param #COORDINATE.WaypointAltType AltType The altitude type.
  -- @param DCS#Speed Speed Airspeed in km/h.
  -- @return #table The route point.
  function COORDINATE:WaypointAirTakeOffParkingHot( AltType, Speed )
    return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOffParkingHot, COORDINATE.WaypointAction.FromParkingAreaHot, Speed )
  end


  --- Build a Waypoint Air "Take Off Parking".
  -- @param #COORDINATE self
  -- @param #COORDINATE.WaypointAltType AltType The altitude type.
  -- @param DCS#Speed Speed Airspeed in km/h.
  -- @return #table The route point.
  function COORDINATE:WaypointAirTakeOffParking( AltType, Speed )
    return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, Speed )
  end


  --- Build a Waypoint Air "Take Off Runway".
  -- @param #COORDINATE self
  -- @param #COORDINATE.WaypointAltType AltType The altitude type.
  -- @param DCS#Speed Speed Airspeed in km/h.
  -- @return #table The route point.
  function COORDINATE:WaypointAirTakeOffRunway( AltType, Speed )
    return self:WaypointAir( AltType, COORDINATE.WaypointType.TakeOff, COORDINATE.WaypointAction.FromRunway, Speed )
  end


  --- Build a Waypoint Air "Landing".
  -- @param #COORDINATE self
  -- @param DCS#Speed Speed Airspeed in km/h.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points.
  -- @param #table DCSTasks A table of @{DCS#Task} items which are executed at the waypoint.
  -- @param #string description A text description of the waypoint, which will be shown on the F10 map.
  -- @return #table The route point.
  -- @usage
  --
  --    LandingZone = ZONE:New( "LandingZone" )
  --    LandingCoord = LandingZone:GetCoordinate()
  --    LandingWaypoint = LandingCoord:WaypointAirLanding( 60 )
  --    HeliGroup:Route( { LandWaypoint }, 1 ) -- Start landing the helicopter in one second.
  --
  function COORDINATE:WaypointAirLanding( Speed, airbase, DCSTasks, description )
    return self:WaypointAir(nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, false, airbase, DCSTasks, description)
  end

  --- Build a Waypoint Air "LandingReFuAr". Mimics the aircraft ReFueling and ReArming.
  -- @param #COORDINATE self
  -- @param DCS#Speed Speed Airspeed in km/h.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase for takeoff and landing points.
  -- @param #number timeReFuAr Time in minutes, the aircraft stays at the airbase. Default 10 min.
  -- @param #table DCSTasks A table of @{DCS#Task} items which are executed at the waypoint.
  -- @param #string description A text description of the waypoint, which will be shown on the F10 map.
  -- @return #table The route point.
  function COORDINATE:WaypointAirLandingReFu( Speed, airbase, timeReFuAr, DCSTasks, description )
    return self:WaypointAir(nil, COORDINATE.WaypointType.LandingReFuAr, COORDINATE.WaypointAction.LandingReFuAr, Speed, false, airbase, DCSTasks, description, timeReFuAr or 10)
  end


  --- Build an ground type route point.
  -- @param #COORDINATE self
  -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h.
  -- @param #string Formation (Optional) The route point Formation, which is a text string that specifies exactly the Text in the Type of the route point, like "Vee", "Echelon Right".
  -- @param #table DCSTasks (Optional) A table of DCS tasks that are executed at the waypoints. Mind the curly brackets {}!
  -- @return #table The route point.
  function COORDINATE:WaypointGround( Speed, Formation, DCSTasks )
    self:F2( { Speed, Formation, DCSTasks } )

    local RoutePoint = {}

    RoutePoint.x    = self.x
    RoutePoint.y    = self.z

    RoutePoint.alt      = self:GetLandHeight()+1
    RoutePoint.alt_type = COORDINATE.WaypointAltType.BARO

    RoutePoint.type = "Turning Point"

    RoutePoint.action = Formation or "Off Road"
    RoutePoint.formation_template=""

    RoutePoint.ETA=0
    RoutePoint.ETA_locked=false

    RoutePoint.speed = ( Speed or 20 ) / 3.6
    RoutePoint.speed_locked = true

    RoutePoint.task = {}
    RoutePoint.task.id = "ComboTask"
    RoutePoint.task.params = {}
    RoutePoint.task.params.tasks = DCSTasks or {}

    return RoutePoint
  end

  --- Build route waypoint point for Naval units.
  -- @param #COORDINATE self
  -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h.
  -- @param #string Depth (Optional) Dive depth in meters. Only for submarines. Default is COORDINATE.y component.
  -- @param #table DCSTasks (Optional) A table of DCS tasks that are executed at the waypoints. Mind the curly brackets {}!
  -- @return #table The route point.
  function COORDINATE:WaypointNaval( Speed, Depth, DCSTasks )
    self:F2( { Speed, Depth, DCSTasks } )

    local RoutePoint = {}

    RoutePoint.x    = self.x
    RoutePoint.y    = self.z

    RoutePoint.alt  = Depth or self.y  -- Depth is for submarines only. Ships should have alt=0.
    RoutePoint.alt_type = "BARO"

    RoutePoint.type   = "Turning Point"
    RoutePoint.action = "Turning Point"
    RoutePoint.formation_template = ""

    RoutePoint.ETA=0
    RoutePoint.ETA_locked=false

    RoutePoint.speed = ( Speed or 20 ) / 3.6
    RoutePoint.speed_locked = true

    RoutePoint.task = {}
    RoutePoint.task.id = "ComboTask"
    RoutePoint.task.params = {}
    RoutePoint.task.params.tasks = DCSTasks or {}

    return RoutePoint
  end

  --- Gets the nearest airbase with respect to the current coordinates.
  -- @param #COORDINATE self
  -- @param #number Category (Optional) Category of the airbase. Enumerator of @{Wrapper.Airbase#AIRBASE.Category}.
  -- @param #number Coalition (Optional) Coalition of the airbase.
  -- @return Wrapper.Airbase#AIRBASE Closest Airbase to the given coordinate.
  -- @return #number Distance to the closest airbase in meters.
  function COORDINATE:GetClosestAirbase(Category, Coalition)

    -- Get all airbases of the map.
    local airbases=AIRBASE.GetAllAirbases(Coalition)

    local closest=nil
    local distmin=nil
    -- Loop over all airbases.
    for _,_airbase in pairs(airbases) do
      local airbase=_airbase --Wrapper.Airbase#AIRBASE
      if airbase then
        local category=airbase:GetAirbaseCategory()
        if Category and Category==category or Category==nil then

          -- Distance to airbase.
          local dist=self:Get2DDistance(airbase:GetCoordinate())

          if closest==nil then
            distmin=dist
            closest=airbase
          else
            if dist<distmin then
              distmin=dist
              closest=airbase
            end
          end

        end
      end
    end

    return closest,distmin
  end

  --- [kept for downwards compatibility only] Gets the nearest airbase with respect to the current coordinates.
  -- @param #COORDINATE self
  -- @param #number Category (Optional) Category of the airbase. Enumerator of @{Wrapper.Airbase#AIRBASE.Category}.
  -- @param #number Coalition (Optional) Coalition of the airbase.
  -- @return Wrapper.Airbase#AIRBASE Closest Airbase to the given coordinate.
  -- @return #number Distance to the closest airbase in meters.
  function COORDINATE:GetClosestAirbase2(Category, Coalition)
    local closest, distmin = self:GetClosestAirbase(Category, Coalition)
    return closest, distmin 
  end

  --- Gets the nearest parking spot.
  -- @param #COORDINATE self
  -- @param Wrapper.Airbase#AIRBASE airbase (Optional) Search only parking spots at this airbase.
  -- @param Wrapper.Airbase#Terminaltype terminaltype (Optional) Type of the terminal. Default any execpt valid spawn points on runway.
  -- @param #boolean free (Optional) If true, returns the closest free spot. If false, returns the closest occupied spot. If nil, returns the closest spot regardless of free or occupied.
  -- @return Core.Point#COORDINATE Coordinate of the nearest parking spot.
  -- @return #number Terminal ID.
  -- @return #number Distance to closest parking spot in meters.
  -- @return Wrapper.Airbase#AIRBASE#ParkingSpot Parking spot table.
  function COORDINATE:GetClosestParkingSpot(airbase, terminaltype, free)

    -- Get airbase table.
    local airbases={}
    if airbase then
      table.insert(airbases,airbase)
    else
      airbases=AIRBASE.GetAllAirbases()
    end

    -- Init.
    local _closest=nil --Core.Point#COORDINATE
    local _termID=nil
    local _distmin=nil
    local spot=nil --Wrapper.Airbase#AIRBASE.ParkingSpot

    -- Loop over all airbases.
    for _,_airbase in pairs(airbases) do

      local mybase=_airbase --Wrapper.Airbase#AIRBASE
      local parkingdata=mybase:GetParkingSpotsTable(terminaltype)

      for _,_spot in pairs(parkingdata) do

        -- Check for parameters.
        if (free==true and _spot.Free==true) or (free==false and _spot.Free==false) or free==nil then

          local _coord=_spot.Coordinate --Core.Point#COORDINATE

          local _dist=self:Get2DDistance(_coord)
          if _distmin==nil then
            _closest=_coord
            _distmin=_dist
            _termID=_spot.TerminalID
            spot=_spot
          else
            if _dist<_distmin then
              _distmin=_dist
              _closest=_coord
              _termID=_spot.TerminalID
              spot=_spot
            end
          end

        end
      end
    end

    return _closest, _termID, _distmin, spot
  end

  --- Gets the nearest free parking spot.
  -- @param #COORDINATE self
  -- @param Wrapper.Airbase#AIRBASE airbase (Optional) Search only parking spots at that airbase.
  -- @param Wrapper.Airbase#Terminaltype terminaltype (Optional) Type of the terminal.
  -- @return #COORDINATE Coordinate of the nearest free parking spot.
  -- @return #number Terminal ID.
  -- @return #number Distance to closest free parking spot in meters.
  function COORDINATE:GetClosestFreeParkingSpot(airbase, terminaltype)
    return self:GetClosestParkingSpot(airbase, terminaltype, true)
  end

  --- Gets the nearest occupied parking spot.
  -- @param #COORDINATE self
  -- @param Wrapper.Airbase#AIRBASE airbase (Optional) Search only parking spots at that airbase.
  -- @param Wrapper.Airbase#Terminaltype terminaltype (Optional) Type of the terminal.
  -- @return #COORDINATE Coordinate of the nearest occupied parking spot.
  -- @return #number Terminal ID.
  -- @return #number Distance to closest occupied parking spot in meters.
  function COORDINATE:GetClosestOccupiedParkingSpot(airbase, terminaltype)
    return self:GetClosestParkingSpot(airbase, terminaltype, false)
  end

  --- Gets the nearest coordinate to a road (or railroad).
  -- @param #COORDINATE self
  -- @param #boolean Railroad (Optional) If true, closest point to railroad is returned rather than closest point to conventional road. Default false.
  -- @return #COORDINATE Coordinate of the nearest road.
  function COORDINATE:GetClosestPointToRoad(Railroad)
    local roadtype="roads"
    if Railroad==true then
      roadtype="railroads"
    end
    local x,y = land.getClosestPointOnRoads(roadtype, self.x, self.z)
    local coord=nil
    if x and y then
      local vec2={ x = x, y = y }
      coord=COORDINATE:NewFromVec2(vec2)
    end
    return coord
  end


  --- Returns a table of coordinates to a destination using only roads or railroads.
  -- The first point is the closest point on road of the given coordinate.
  -- By default, the last point is the closest point on road of the ToCoord. Hence, the coordinate itself and the final ToCoord are not necessarily included in the path.
  -- @param #COORDINATE self
  -- @param #COORDINATE ToCoord Coordinate of destination.
  -- @param #boolean IncludeEndpoints (Optional) Include the coordinate itself and the ToCoordinate in the path.
  -- @param #boolean Railroad (Optional) If true, path on railroad is returned. Default false.
  -- @param #boolean MarkPath (Optional) If true, place markers on F10 map along the path.
  -- @param #boolean SmokePath (Optional) If true, put (green) smoke along the
  -- @return #table Table of coordinates on road. If no path on road can be found, nil is returned or just the endpoints.
  -- @return #number Tonal length of path.
  -- @return #boolean If true a valid path on road/rail was found. If false, only the direct way is possible.
  function COORDINATE:GetPathOnRoad(ToCoord, IncludeEndpoints, Railroad, MarkPath, SmokePath)

    -- Set road type.
    local RoadType="roads"
    if Railroad==true then
      RoadType="railroads"
    end

    -- DCS API function returning a table of vec2.
    local path = land.findPathOnRoads(RoadType, self.x, self.z, ToCoord.x, ToCoord.z)

    -- Array holding the path coordinates.
    local Path={}
    local Way=0

    -- Include currrent position.
    if IncludeEndpoints then
      Path[1]=self
    end

    -- Assume we could get a valid path.
    local GotPath=true

    -- Check that DCS routine actually returned a path. There are situations where this is not the case.
    if path then

      -- Include all points on road.
      for _i,_vec2 in ipairs(path) do

        local coord=COORDINATE:NewFromVec2(_vec2)

        Path[#Path+1]=coord
      end

    else
      self:E("Path is nil. No valid path on road could be found.")
      GotPath=false
    end

    -- Include end point, which might not be on road.
    if IncludeEndpoints then
      Path[#Path+1]=ToCoord
    end

    -- Mark or smoke.
    if MarkPath or SmokePath then
      for i,c in pairs(Path) do
        local coord=c --#COORDINATE
        if MarkPath then
          coord:MarkToAll(string.format("Path segment %d", i))
        end
        if SmokePath then
          if i==1 or i==#Path then
            coord:SmokeBlue()
          else
            coord:SmokeGreen()
          end
        end
      end
    end

    -- Sum up distances.
    if #Path>=2 then
      for i=1,#Path-1 do
        Way=Way+Path[i+1]:Get2DDistance(Path[i])
      end
    else
      -- There are cases where no path on road can be found.
      return nil,nil,false
    end

    return Path, Way, GotPath
  end

  --- Gets the surface type at the coordinate.
  -- @param #COORDINATE self
  -- @return DCS#SurfaceType Surface type.
  function COORDINATE:GetSurfaceType()
    local vec2=self:GetVec2()
    local surface=land.getSurfaceType(vec2)
    return surface
  end

  --- Checks if the surface type is on land.
  -- @param #COORDINATE self
  -- @return #boolean If true, the surface type at the coordinate is land.
  function COORDINATE:IsSurfaceTypeLand()
    return self:GetSurfaceType()==land.SurfaceType.LAND
  end

  --- Checks if the surface type is land.
  -- @param #COORDINATE self
  -- @return #boolean If true, the surface type at the coordinate is land.
  function COORDINATE:IsSurfaceTypeLand()
    return self:GetSurfaceType()==land.SurfaceType.LAND
  end


  --- Checks if the surface type is road.
  -- @param #COORDINATE self
  -- @return #boolean If true, the surface type at the coordinate is a road.
  function COORDINATE:IsSurfaceTypeRoad()
    return self:GetSurfaceType()==land.SurfaceType.ROAD
  end

  --- Checks if the surface type is runway.
  -- @param #COORDINATE self
  -- @return #boolean If true, the surface type at the coordinate is a runway or taxi way.
  function COORDINATE:IsSurfaceTypeRunway()
    return self:GetSurfaceType()==land.SurfaceType.RUNWAY
  end

  --- Checks if the surface type is shallow water.
  -- @param #COORDINATE self
  -- @return #boolean If true, the surface type at the coordinate is a shallow water.
  function COORDINATE:IsSurfaceTypeShallowWater()
    return self:GetSurfaceType()==land.SurfaceType.SHALLOW_WATER
  end

  --- Checks if the surface type is water.
  -- @param #COORDINATE self
  -- @return #boolean If true, the surface type at the coordinate is a deep water.
  function COORDINATE:IsSurfaceTypeWater()
    return self:GetSurfaceType()==land.SurfaceType.WATER
  end


  --- Creates an explosion at the point of a certain intensity.
  -- @param #COORDINATE self
  -- @param #number ExplosionIntensity Intensity of the explosion in kg TNT. Default 100 kg.
  -- @param #number Delay (Optional) Delay before explosion is triggered in seconds.
  -- @return #COORDINATE self
  function COORDINATE:Explosion( ExplosionIntensity, Delay )
    ExplosionIntensity=ExplosionIntensity or 100
    if Delay and Delay>0 then
      self:ScheduleOnce(Delay, self.Explosion, self, ExplosionIntensity)
    else
      trigger.action.explosion(self:GetVec3(), ExplosionIntensity)
    end
    return self
  end

  --- Creates an illumination bomb at the point.
  -- @param #COORDINATE self
  -- @param #number Power Power of illumination bomb in Candela. Default 1000 cd.
  -- @param #number Delay (Optional) Delay before bomb is ignited in seconds.
  -- @return #COORDINATE self
  function COORDINATE:IlluminationBomb(Power, Delay)
    Power=Power or 1000
    if Delay and Delay>0 then
      self:ScheduleOnce(Delay, self.IlluminationBomb, self, Power)
    else
      trigger.action.illuminationBomb(self:GetVec3(), Power)
    end
    return self
  end


  --- Create colored smoke the point. The smoke we last up to 5 min (DCS limitation) but you can optionally specify a shorter duration or stop it manually.
  -- @param #COORDINATE self
  -- @param #number SmokeColor Color of smoke, e.g. `SMOKECOLOR.Green` for green smoke.
  -- @param #number Duration (Optional) Duration of the smoke in seconds. DCS stopps the smoke automatically after 5 min.
  -- @param #number Delay (Optional) Delay before the smoke is started in seconds.
  -- @param #string Name (Optional) Name if you want to stop the smoke early (normal duration: 5mins)
  -- @param #boolean Offset (Optional) If true, offset the smokle a bit.
  -- @param #number Direction (Optional) If Offset is true this is the direction of the offset, 1-359 (degrees). Default random.
  -- @param #number Distance (Optional) If Offset is true this is the distance of the offset in meters. Default random 10-20.
  -- @return #COORDINATE self
  function COORDINATE:Smoke( SmokeColor, Duration, Delay, Name, Offset,Direction,Distance)
    self:F2( { SmokeColor, Name, Duration, Delay, Offset } )

    SmokeColor=SmokeColor or SMOKECOLOR.Green
    
    if Delay and Delay>0 then
      self:ScheduleOnce(Delay, COORDINATE.Smoke, self, SmokeColor, Duration, 0, Name, Direction,Distance)
    else
    
      -- Create a name which is used to stop the smoke manually
      self.firename = Name or "Smoke-"..math.random(1,100000)
      
      -- Create smoke
      if Offset or self.SmokeOffset then
        local Angle = Direction or self:GetSmokeOffsetDirection()
        local Distance = Distance or self:GetSmokeOffsetDistance()
        local newpos = self:Translate(Distance,Angle,true,false)
        local newvec3 = newpos:GetVec3()
        trigger.action.smoke( newvec3, SmokeColor, self.firename )
      else
        trigger.action.smoke( self:GetVec3(), SmokeColor, self.firename )
      end
      
      -- Stop smoke
      if Duration and Duration>0 then
        self:ScheduleOnce(Duration, COORDINATE.StopSmoke, self, self.firename )
      end
    end
    
    return self
  end
  
  --- Get the offset direction when using `COORDINATE:Smoke()`.
  -- @param #COORDINATE self
  -- @return #number Direction in degrees.
  function COORDINATE:GetSmokeOffsetDirection()
    local direction = self.SmokeOffsetDirection or math.random(1,359)
    return direction
  end
  
  --- Set the offset direction when using `COORDINATE:Smoke()`.
  -- @param #COORDINATE self
  -- @param #number Direction (Optional) This is the direction of the offset, 1-359 (degrees). Default random.
  -- @return #COORDINATE self
  function COORDINATE:SetSmokeOffsetDirection(Direction)
    if self then
      self.SmokeOffsetDirection = Direction or math.random(1,359)
      return self
    else
      COORDINATE.SmokeOffsetDirection = Direction or math.random(1,359)
    end
  end
  
  --- Get the offset distance when using `COORDINATE:Smoke()`.
  -- @param #COORDINATE self
  -- @return #number Distance Distance in meters.
  function COORDINATE:GetSmokeOffsetDistance()
    local distance = self.SmokeOffsetDistance or math.random(10,20)
    return distance
  end
  
  --- Set the offset distance when using `COORDINATE:Smoke()`.
  -- @param #COORDINATE self
  -- @param #number Distance (Optional) This is the distance of the offset in meters. Default random 10-20.
  -- @return #COORDINATE self
  function COORDINATE:SetSmokeOffsetDistance(Distance)
    if self then
      self.SmokeOffsetDistance = Distance or math.random(10,20)
      return self
    else
      COORDINATE.SmokeOffsetDistance = Distance or math.random(10,20)
    end
  end
  
  --- Set the offset on when using `COORDINATE:Smoke()`.
  -- @param #COORDINATE self
  -- @return #COORDINATE self
  function COORDINATE:SwitchSmokeOffsetOn()
    if self then
      self.SmokeOffset = true
      return self
    else
      COORDINATE.SmokeOffset = true
    end
  end
  
  --- Set the offset off when using `COORDINATE:Smoke()`.
  -- @param #COORDINATE self
  -- @return #COORDINATE self
  function COORDINATE:SwitchSmokeOffsetOff()
    if self then
      self.SmokeOffset = false
      return self
    else
      COORDINATE.SmokeOffset = false
    end
  end

  --- Stops smoking the point in a color.
  -- @param #COORDINATE self
  -- @param #string name (Optional) Name if you want to stop the smoke early (normal duration: 5mins)
  function COORDINATE:StopSmoke( name )
    self:StopBigSmokeAndFire( name )
  end

  --- Smoke the COORDINATE Green.
  -- @param #COORDINATE self
  -- @param #number Duration (Optional) Duration of the smoke in seconds. DCS stopps the smoke automatically after 5 min.
  -- @param #number Delay (Optional) Delay before the smoke is started in seconds.
  -- @return #COORDINATE self
  function COORDINATE:SmokeGreen(Duration, Delay)
    self:Smoke( SMOKECOLOR.Green, Duration, Delay )
    return self
  end

  --- Smoke the COORDINATE Red.
  -- @param #COORDINATE self
  -- @param #number Duration (Optional) Duration of the smoke in seconds. DCS stopps the smoke automatically after 5 min.
  -- @param #number Delay (Optional) Delay before the smoke is started in seconds.
  -- @return #COORDINATE self
  function COORDINATE:SmokeRed(Duration, Delay)
    self:Smoke( SMOKECOLOR.Red, Duration, Delay )
    return self    
  end

  --- Smoke the COORDINATE White.
  -- @param #COORDINATE self
  -- @param #number Duration (Optional) Duration of the smoke in seconds. DCS stopps the smoke automatically after 5 min.
  -- @param #number Delay (Optional) Delay before the smoke is started in seconds.
  -- @return #COORDINATE self 
  function COORDINATE:SmokeWhite(Duration, Delay)
    self:Smoke( SMOKECOLOR.White, Duration, Delay )
    return self    
  end

  --- Smoke the COORDINATE Orange.
  -- @param #COORDINATE self
  -- @param #number Duration (Optional) Duration of the smoke in seconds. DCS stopps the smoke automatically after 5 min.
  -- @param #number Delay (Optional) Delay before the smoke is started in seconds.
  -- @return #COORDINATE self
  function COORDINATE:SmokeOrange(Duration, Delay)
    self:Smoke( SMOKECOLOR.Orange, Duration, Delay )
    return self    
  end

  --- Smoke the COORDINATE Blue.
  -- @param #COORDINATE self
  -- @param #number Duration (Optional) Duration of the smoke in seconds. DCS stopps the smoke automatically after 5 min.
  -- @param #number Delay (Optional) Delay before the smoke is started in seconds.
  -- @return #COORDINATE self
  function COORDINATE:SmokeBlue(Duration, Delay)
    self:Smoke( SMOKECOLOR.Blue, Duration, Delay )
    return self    
  end

  --- Big smoke and fire at the coordinate.
  -- @param #COORDINATE self
  -- @param #number Preset Smoke preset (1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke).
  -- @param #number Density (Optional) Smoke density. Number in [0,...,1]. Default 0.5.
  -- @param #number Duration (Optional) Duration of the smoke and fire in seconds.
  -- @param #number Delay (Optional) Delay before the smoke and fire is started in seconds.
  -- @param #string Name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number.
  -- @return #COORDINATE self
  function COORDINATE:BigSmokeAndFire( Preset, Density, Duration, Delay, Name )
    self:F2( { preset=Preset, density=Density } )
    
    Preset=Preset or BIGSMOKEPRESET.SmallSmokeAndFire    
    Density=Density or 0.5
    
    if Delay and Delay>0 then
      self:ScheduleOnce(Delay, COORDINATE.BigSmokeAndFire, self, Preset, Density, Duration, 0, Name)
    else
    
      self.firename = Name or "Fire-"..math.random(1,10000)
      
      trigger.action.effectSmokeBig( self:GetVec3(), Preset, Density, self.firename )
      
      -- Stop smoke
      if Duration and Duration>0 then
        self:ScheduleOnce(Duration, COORDINATE.StopBigSmokeAndFire, self, self.firename )
      end      
    end
    
    return self
  end
  
  --- Stop big smoke and fire at the coordinate.
  -- @param #COORDINATE self
  -- @param #string name (Optional) Name of the fire to stop it, if not using the same COORDINATE object.
  function COORDINATE:StopBigSmokeAndFire( name )
    name = name or self.firename
    trigger.action.effectSmokeStop( name )
  end

  --- Small smoke and fire at the coordinate.
  -- @param #COORDINATE self
  -- @param #number Density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  -- @param #number Duration (Optional) Duration of the smoke and fire in seconds.
  -- @param #number Delay (Optional) Delay before the smoke and fire is started in seconds.
  -- @param #string Name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number.
  -- @return #COORDINATE self
  function COORDINATE:BigSmokeAndFireSmall( Density, Duration, Delay, Name )
    self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmokeAndFire, Density, Duration, Delay, Name)
    return self
  end

  --- Medium smoke and fire at the coordinate.
  -- @param #COORDINATE self
  -- @param #number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  -- @param #number Duration (Optional) Duration of the smoke and fire in seconds.
  -- @param #number Delay (Optional) Delay before the smoke and fire is started in seconds.
  -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number.
  -- @return #COORDINATE self
  function COORDINATE:BigSmokeAndFireMedium( Density, Duration, Delay, Name )
    self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmokeAndFire, Density, Duration, Delay, Name)
    return self    
  end

  --- Large smoke and fire at the coordinate.
  -- @param #COORDINATE self
  -- @param #number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  -- @param #number Duration (Optional) Duration of the smoke and fire in seconds.
  -- @param #number Delay (Optional) Delay before the smoke and fire is started in seconds.
  -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number.
  -- @return #COORDINATE self
  function COORDINATE:BigSmokeAndFireLarge( Density, Duration, Delay, Name )
    self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmokeAndFire, Density, Duration, Delay, Name)
    return self
  end

  --- Huge smoke and fire at the coordinate.
  -- @param #COORDINATE self
  -- @param #number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  -- @param #number Duration (Optional) Duration of the smoke and fire in seconds.
  -- @param #number Delay (Optional) Delay before the smoke and fire is started in seconds.
  -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number.
  -- @return #COORDINATE self
  function COORDINATE:BigSmokeAndFireHuge( Density, Duration, Delay, Name )
    self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmokeAndFire, Density, Duration, Delay, Name)
    return self
  end

  --- Small smoke at the coordinate.
  -- @param #COORDINATE self
  -- @param #number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  -- @param #number Duration (Optional) Duration of the smoke and fire in seconds.
  -- @param #number Delay (Optional) Delay before the smoke and fire is started in seconds.
  -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number.
  -- @return #COORDINATE self
  function COORDINATE:BigSmokeSmall( Density, Duration, Delay, Name )
    self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmoke, Density, Duration, Delay, Name)
    return self
  end

  --- Medium smoke at the coordinate.
  -- @param #COORDINATE self
  -- @param number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  -- @param #number Duration (Optional) Duration of the smoke and fire in seconds.
  -- @param #number Delay (Optional) Delay before the smoke and fire is started in seconds.
  -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number.
  -- @return #COORDINATE self
  function COORDINATE:BigSmokeMedium( Density, Duration, Delay, Name )
    self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmoke, Density, Duration, Delay, Name)
    return self
  end

  --- Large smoke at the coordinate.
  -- @param #COORDINATE self
  -- @param #number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  -- @param #number Duration (Optional) Duration of the smoke and fire in seconds.
  -- @param #number Delay (Optional) Delay before the smoke and fire is started in seconds.
  -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number.
  -- @return #COORDINATE self
  function COORDINATE:BigSmokeLarge( Density, Duration, Delay, Name )
    self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmoke, Density, Duration, Delay, Name)
    return self
  end

  --- Huge smoke at the coordinate.
  -- @param #COORDINATE self
  -- @param #number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  -- @param #number Duration (Optional) Duration of the smoke and fire in seconds.
  -- @param #number Delay (Optional) Delay before the smoke and fire is started in seconds.
  -- @param #string name (Optional) Name of the fire to stop it later again if not using the same COORDINATE object. Defaults to "Fire-" plus a random 5-digit-number.
  -- @return #COORDINATE self
  function COORDINATE:BigSmokeHuge( Density, Duration, Delay, Name )
    self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmoke, Density, Duration, Delay, Name)
    return self
  end

  --- Flares the point in a color.
  -- @param #COORDINATE self
  -- @param Utilities.Utils#FLARECOLOR FlareColor
  -- @param DCS#Azimuth Azimuth (optional) The azimuth of the flare direction. The default azimuth is 0.
  function COORDINATE:Flare( FlareColor, Azimuth )
    self:F2( { FlareColor } )
    trigger.action.signalFlare( self:GetVec3(), FlareColor, Azimuth and Azimuth or 0 )
  end

  --- Flare the COORDINATE White.
  -- @param #COORDINATE self
  -- @param DCS#Azimuth Azimuth (optional) The azimuth of the flare direction. The default azimuth is 0.
  function COORDINATE:FlareWhite( Azimuth )
    self:F2( Azimuth )
    self:Flare( FLARECOLOR.White, Azimuth )
  end

  --- Flare the COORDINATE Yellow.
  -- @param #COORDINATE self
  -- @param DCS#Azimuth Azimuth (optional) The azimuth of the flare direction. The default azimuth is 0.
  function COORDINATE:FlareYellow( Azimuth )
    self:F2( Azimuth )
    self:Flare( FLARECOLOR.Yellow, Azimuth )
  end

  --- Flare the COORDINATE Green.
  -- @param #COORDINATE self
  -- @param DCS#Azimuth Azimuth (optional) The azimuth of the flare direction. The default azimuth is 0.
  function COORDINATE:FlareGreen( Azimuth )
    self:F2( Azimuth )
    self:Flare( FLARECOLOR.Green, Azimuth )
  end

  --- Flare the COORDINATE Red.
  -- @param #COORDINATE self
  function COORDINATE:FlareRed( Azimuth )
    self:F2( Azimuth )
    self:Flare( FLARECOLOR.Red, Azimuth )
  end

  do -- Markings

    --- Mark to All
    -- @param #COORDINATE self
    -- @param #string MarkText Free format text that shows the marking clarification.
    -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
    -- @param #string Text (Optional) Text displayed when mark is added. Default none.
    -- @return #number The resulting Mark ID which is a number.
    -- @usage
    --   local TargetCoord = TargetGroup:GetCoordinate()
    --   local MarkID = TargetCoord:MarkToAll( "This is a target for all players" )
    function COORDINATE:MarkToAll( MarkText, ReadOnly, Text )
      local MarkID = UTILS.GetMarkID()
      if ReadOnly==nil then
        ReadOnly=false
      end
      local text=Text or ""
      trigger.action.markToAll( MarkID, MarkText, self:GetVec3(), ReadOnly, text)
      return MarkID
    end

    --- Mark to Coalition
    -- @param #COORDINATE self
    -- @param #string MarkText Free format text that shows the marking clarification.
    -- @param Coalition
    -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
    -- @param #string Text (Optional) Text displayed when mark is added. Default none.
    -- @return #number The resulting Mark ID which is a number.
    -- @usage
    --   local TargetCoord = TargetGroup:GetCoordinate()
    --   local MarkID = TargetCoord:MarkToCoalition( "This is a target for the red coalition", coalition.side.RED )
    function COORDINATE:MarkToCoalition( MarkText, Coalition, ReadOnly, Text )
      local MarkID = UTILS.GetMarkID()
      if ReadOnly==nil then
        ReadOnly=false
      end
      local text=Text or ""
      trigger.action.markToCoalition( MarkID, MarkText, self:GetVec3(), Coalition, ReadOnly, text )
      return MarkID
    end

    --- Mark to Red Coalition
    -- @param #COORDINATE self
    -- @param #string MarkText Free format text that shows the marking clarification.
    -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
    -- @param #string Text (Optional) Text displayed when mark is added. Default none.
    -- @return #number The resulting Mark ID which is a number.
    -- @usage
    --   local TargetCoord = TargetGroup:GetCoordinate()
    --   local MarkID = TargetCoord:MarkToCoalitionRed( "This is a target for the red coalition" )
    function COORDINATE:MarkToCoalitionRed( MarkText, ReadOnly, Text )
      return self:MarkToCoalition( MarkText, coalition.side.RED, ReadOnly, Text )
    end

    --- Mark to Blue Coalition
    -- @param #COORDINATE self
    -- @param #string MarkText Free format text that shows the marking clarification.
    -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
    -- @param #string Text (Optional) Text displayed when mark is added. Default none.
    -- @return #number The resulting Mark ID which is a number.
    -- @usage
    --   local TargetCoord = TargetGroup:GetCoordinate()
    --   local MarkID = TargetCoord:MarkToCoalitionBlue( "This is a target for the blue coalition" )
    function COORDINATE:MarkToCoalitionBlue( MarkText, ReadOnly, Text )
      return self:MarkToCoalition( MarkText, coalition.side.BLUE, ReadOnly, Text )
    end

    --- Mark to Group
    -- @param #COORDINATE self
    -- @param #string MarkText Free format text that shows the marking clarification.
    -- @param Wrapper.Group#GROUP MarkGroup The @{Wrapper.Group} that receives the mark.
    -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
    -- @param #string Text (Optional) Text displayed when mark is added. Default none.
    -- @return #number The resulting Mark ID which is a number.
    -- @usage
    --   local TargetCoord = TargetGroup:GetCoordinate()
    --   local MarkGroup = GROUP:FindByName( "AttackGroup" )
    --   local MarkID = TargetCoord:MarkToGroup( "This is a target for the attack group", AttackGroup )
    function COORDINATE:MarkToGroup( MarkText, MarkGroup, ReadOnly, Text )
      local MarkID = UTILS.GetMarkID()
      if ReadOnly==nil then
        ReadOnly=false
      end
      local text=Text or ""
      trigger.action.markToGroup( MarkID, MarkText, self:GetVec3(), MarkGroup:GetID(), ReadOnly, text )
      return MarkID
    end

    --- Remove a mark
    -- @param #COORDINATE self
    -- @param #number MarkID The ID of the mark to be removed.
    -- @usage
    --   local TargetCoord = TargetGroup:GetCoordinate()
    --   local MarkGroup = GROUP:FindByName( "AttackGroup" )
    --   local MarkID = TargetCoord:MarkToGroup( "This is a target for the attack group", AttackGroup )
    --   <<< logic >>>
    --   TargetCoord:RemoveMark( MarkID ) -- The mark is now removed
    function COORDINATE:RemoveMark( MarkID )
      trigger.action.removeMark( MarkID )
    end

    --- Line to all.
    -- Creates a line on the F10 map from one point to another.
    -- @param #COORDINATE self
    -- @param #COORDINATE Endpoint COORDINATE to where the line is drawn.
    -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All.
    -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default).
    -- @param #number Alpha Transparency [0,1]. Default 1.
    -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
    -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
    -- @param #string Text (Optional) Text displayed when mark is added. Default none.
    -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again.
    function COORDINATE:LineToAll(Endpoint, Coalition, Color, Alpha, LineType, ReadOnly, Text)
      local MarkID = UTILS.GetMarkID()
      if ReadOnly==nil then
        ReadOnly=false
      end
      local vec3=Endpoint:GetVec3()
      Coalition=Coalition or -1
      Color=Color or {1,0,0}
      Color[4]=Alpha or 1.0
      LineType=LineType or 1
      trigger.action.lineToAll(Coalition, MarkID, self:GetVec3(), vec3, Color, LineType, ReadOnly, Text or "")
      return MarkID
    end

    --- Circle to all.
    -- Creates a circle on the map with a given radius, color, fill color, and outline.
    -- @param #COORDINATE self
    -- @param #number Radius Radius in meters. Default 1000 m.
    -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All.
    -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default).
    -- @param #number Alpha Transparency [0,1]. Default 1.
    -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value.
    -- @param #number FillAlpha Transparency [0,1]. Default 0.15.
    -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
    -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
    -- @param #string Text (Optional) Text displayed when mark is added. Default none.
    -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again.
    function COORDINATE:CircleToAll(Radius, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text)
      local MarkID = UTILS.GetMarkID()
      if ReadOnly==nil then
        ReadOnly=false
      end
      
      local vec3=self:GetVec3()
      
      Radius=Radius or 1000
      
      Coalition=Coalition or -1
      
      Color=Color or {1,0,0}
      Color[4]=Alpha or 1.0
      
      LineType=LineType or 1
      
      FillColor=FillColor or UTILS.DeepCopy(Color)
      FillColor[4]=FillAlpha or 0.15
      
      trigger.action.circleToAll(Coalition, MarkID, vec3, Radius, Color, FillColor, LineType, ReadOnly, Text or "")
      return MarkID
    end

  end -- Markings

    --- Rectangle to all. Creates a rectangle on the map from the COORDINATE in one corner to the end COORDINATE in the opposite corner.
    -- Creates a line on the F10 map from one point to another.
    -- @param #COORDINATE self
    -- @param #COORDINATE Endpoint COORDINATE in the opposite corner.
    -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All.
    -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default).
    -- @param #number Alpha Transparency [0,1]. Default 1.
    -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value.
    -- @param #number FillAlpha Transparency [0,1]. Default 0.15.
    -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
    -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
    -- @param #string Text (Optional) Text displayed when mark is added. Default none.
    -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again.
    function COORDINATE:RectToAll(Endpoint, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text)
      local MarkID = UTILS.GetMarkID()
      if ReadOnly==nil then
        ReadOnly=false
      end
      
      local vec3=Endpoint:GetVec3()
      
      Coalition=Coalition or -1
      
      Color=Color or {1,0,0}
      Color[4]=Alpha or 1.0
      
      LineType=LineType or 1
      
      FillColor=FillColor or UTILS.DeepCopy(Color)
      FillColor[4]=FillAlpha or 0.15
      
      trigger.action.rectToAll(Coalition, MarkID, self:GetVec3(), vec3, Color, FillColor, LineType, ReadOnly, Text or "")
      return MarkID
    end

    --- Creates a shape defined by 4 points on the F10 map. The first point is the current COORDINATE. The remaining three points need to be specified.
    -- @param #COORDINATE self
    -- @param #COORDINATE Coord2 Second COORDINATE of the quad shape.
    -- @param #COORDINATE Coord3 Third COORDINATE of the quad shape.
    -- @param #COORDINATE Coord4 Fourth COORDINATE of the quad shape.
    -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All.
    -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default).
    -- @param #number Alpha Transparency [0,1]. Default 1.
    -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value.
    -- @param #number FillAlpha Transparency [0,1]. Default 0.15.
    -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
    -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
    -- @param #string Text (Optional) Text displayed when mark is added. Default none.
    -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again.
    function COORDINATE:QuadToAll(Coord2, Coord3, Coord4, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text)
      local MarkID = UTILS.GetMarkID()
      if ReadOnly==nil then
        ReadOnly=false
      end
      
      local point1=self:GetVec3()
      local point2=Coord2:GetVec3()
      local point3=Coord3:GetVec3()
      local point4=Coord4:GetVec3()
      
      Coalition=Coalition or -1
      
      Color=Color or {1,0,0}
      Color[4]=Alpha or 1.0
      
      LineType=LineType or 1
      
      FillColor=FillColor or UTILS.DeepCopy(Color)
      FillColor[4]=FillAlpha or 0.15
      
      trigger.action.quadToAll(Coalition, MarkID, point1, point2, point3, point4, Color, FillColor, LineType, ReadOnly, Text or "")
      return MarkID
    end

    --- Creates a free form shape on the F10 map. The first point is the current COORDINATE. The remaining points need to be specified.
    -- @param #COORDINATE self
    -- @param #table Coordinates Table of coordinates of the remaining points of the shape.
    -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All.
    -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default).
    -- @param #number Alpha Transparency [0,1]. Default 1.
    -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value.
    -- @param #number FillAlpha Transparency [0,1]. Default 0.15.
    -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
    -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
    -- @param #string Text (Optional) Text displayed when mark is added. Default none.
    -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again.
    function COORDINATE:MarkupToAllFreeForm(Coordinates, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text)

      local MarkID = UTILS.GetMarkID()
      if ReadOnly==nil then
        ReadOnly=false
      end

      Coalition=Coalition or -1

      Color=Color or {1,0,0}
      Color[4]=Alpha or 1.0

      LineType=LineType or 1

      FillColor=FillColor or UTILS.DeepCopy(Color)
      FillColor[4]=FillAlpha or 0.15

      local vecs={}
      vecs[1]=self:GetVec3()
      for i,coord in ipairs(Coordinates) do
        vecs[i+1]=coord:GetVec3()
      end
      
      if #vecs<3 then
        self:E("ERROR: A free form polygon needs at least three points!")
      elseif #vecs==3 then
        trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], Color, FillColor, LineType, ReadOnly, Text or "")
      elseif #vecs==4 then
        trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], Color, FillColor, LineType, ReadOnly, Text or "")
      elseif #vecs==5 then
        trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], Color, FillColor, LineType, ReadOnly, Text or "")
      elseif #vecs==6 then
        trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], Color, FillColor, LineType, ReadOnly, Text or "")
      elseif #vecs==7 then
        trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], Color, FillColor, LineType, ReadOnly, Text or "")
      elseif #vecs==8 then
        trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], Color, FillColor, LineType, ReadOnly, Text or "")
      elseif #vecs==9 then
        trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], Color, FillColor, LineType, ReadOnly, Text or "")
      elseif #vecs==10 then
        trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10], Color, FillColor, LineType, ReadOnly, Text or "")
      elseif #vecs==11 then
        trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10],
                                                         vecs[11], 
                                                         Color, FillColor, LineType, ReadOnly, Text or "")        
      elseif #vecs==12 then
        trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10],
                                                         vecs[11], vecs[12],
                                                         Color, FillColor, LineType, ReadOnly, Text or "")
      elseif #vecs==13 then
        trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10],
                                                         vecs[11], vecs[12], vecs[13],
                                                         Color, FillColor, LineType, ReadOnly, Text or "")
      elseif #vecs==14 then
        trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10],
                                                         vecs[11], vecs[12], vecs[13], vecs[14],
                                                         Color, FillColor, LineType, ReadOnly, Text or "")                                                                                                                                                                                                           
      elseif #vecs==15 then
        trigger.action.markupToAll(7, Coalition, MarkID, vecs[1], vecs[2], vecs[3], vecs[4], vecs[5], vecs[6], vecs[7], vecs[8], vecs[9], vecs[10],
                                                         vecs[11], vecs[12], vecs[13], vecs[14], vecs[15],
                                                         Color, FillColor, LineType, ReadOnly, Text or "")
      else
        
        -- Unfortunately, unpack(vecs) does not work! So no idea how to generalize this :(
        --trigger.action.markupToAll(7, Coalition, MarkID, unpack(vecs), Color, FillColor, LineType, ReadOnly, Text or "")
        
        -- Write command as string and execute that. Idea by Grimes https://forum.dcs.world/topic/324201-mark-to-all-function/#comment-5273793
        local s=string.format("trigger.action.markupToAll(7, %d, %d,", Coalition, MarkID)
        for _,vec in pairs(vecs) do
          --s=s..string.format("%s,", UTILS._OneLineSerialize(vec))
          s=s..string.format("{x=%.1f, y=%.1f, z=%.1f},", vec.x, vec.y, vec.z)
        end
        s=s..string.format("{%.3f, %.3f, %.3f, %.3f},", Color[1], Color[2], Color[3], Color[4])
        s=s..string.format("{%.3f, %.3f, %.3f, %.3f},", FillColor[1], FillColor[2], FillColor[3], FillColor[4])
        s=s..string.format("%d,", LineType or 1)
        s=s..string.format("%s", tostring(ReadOnly))
        if Text and type(Text)=="string" and string.len(Text)>0 then
          s=s..string.format(", \"%s\"", tostring(Text))
        end
        s=s..")"
        
        -- Execute string command
        local success=UTILS.DoString(s)
                
        if not success then
          self:E("ERROR: Could not draw polygon")
          env.info(s)
        end
        
      end

      return MarkID
    end

    --- Text to all. Creates a text imposed on the map at the COORDINATE. Text scales with the map.
    -- @param #COORDINATE self
    -- @param #string Text Text displayed on the F10 map.
    -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All.
    -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default).
    -- @param #number Alpha Transparency [0,1]. Default 1.
    -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value.
    -- @param #number FillAlpha Transparency [0,1]. Default 0.3.
    -- @param #number FontSize Font size. Default 14.
    -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
    -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again.
    function COORDINATE:TextToAll(Text, Coalition, Color, Alpha, FillColor, FillAlpha, FontSize, ReadOnly)
      local MarkID = UTILS.GetMarkID()
      if ReadOnly==nil then
        ReadOnly=false
      end
      Coalition=Coalition or -1
      
      Color=Color or {1,0,0}
      Color[4]=Alpha or 1.0
      
      FillColor=FillColor or UTILS.DeepCopy(Color)
      FillColor[4]=FillAlpha or 0.3
      
      FontSize=FontSize or 14
      
      trigger.action.textToAll(Coalition, MarkID, self:GetVec3(), Color, FillColor, FontSize, ReadOnly, Text or "Hello World")
      return MarkID
    end

    --- Arrow to all. Creates an arrow from the COORDINATE to the endpoint COORDINATE on the F10 map. There is no control over other dimensions of the arrow.
    -- @param #COORDINATE self
    -- @param #COORDINATE Endpoint COORDINATE where the tip of the arrow is pointing at.
    -- @param #number Coalition Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1=All.
    -- @param #table Color RGB color table {r, g, b}, e.g. {1,0,0} for red (default).
    -- @param #number Alpha Transparency [0,1]. Default 1.
    -- @param #table FillColor RGB color table {r, g, b}, e.g. {1,0,0} for red. Default is same as `Color` value.
    -- @param #number FillAlpha Transparency [0,1]. Default 0.15.
    -- @param #number LineType Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
    -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.
    -- @param #string Text (Optional) Text displayed when mark is added. Default none.
    -- @return #number The resulting Mark ID, which is a number. Can be used to remove the object again.
    function COORDINATE:ArrowToAll(Endpoint, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, Text)
      local MarkID = UTILS.GetMarkID()
      if ReadOnly==nil then
        ReadOnly=false
      end
      
      local vec3=Endpoint:GetVec3()
      
      Coalition=Coalition or -1
      
      Color=Color or {1,0,0}
      Color[4]=Alpha or 1.0
      
      LineType=LineType or 1
      
      FillColor=FillColor or UTILS.DeepCopy(Color)
      FillColor[4]=FillAlpha or 0.15
      
      --trigger.action.textToAll(Coalition, MarkID, self:GetVec3(), Color, FillColor, FontSize, ReadOnly, Text or "Hello World")
      trigger.action.arrowToAll(Coalition, MarkID, vec3, self:GetVec3(), Color, FillColor, LineType, ReadOnly, Text or "")
      return MarkID
    end

  --- Returns if a Coordinate has Line of Sight (LOS) with the ToCoordinate.
  -- @param #COORDINATE self
  -- @param #COORDINATE ToCoordinate
  -- @param #number Offset Height offset in meters. Default 2 m.
  -- @return #boolean true If the ToCoordinate has LOS with the Coordinate, otherwise false.
  function COORDINATE:IsLOS( ToCoordinate, Offset )

    Offset=Offset or 2

    -- Measurement of visibility should not be from the ground, so Adding a hypothetical 2 meters to each Coordinate.
    local FromVec3 = self:GetVec3()
    FromVec3.y = FromVec3.y + Offset

    local ToVec3 = ToCoordinate:GetVec3()
    ToVec3.y = ToVec3.y + Offset

    local IsLOS = land.isVisible( FromVec3, ToVec3 )

    return IsLOS
  end


  --- Returns if a Coordinate is in a certain Radius of this Coordinate in 2D plane using the X and Z axis.
  -- @param #COORDINATE self
  -- @param #COORDINATE Coordinate The coordinate that will be tested if it is in the radius of this coordinate.
  -- @param #number Radius The radius of the circle on the 2D plane around this coordinate.
  -- @return #boolean true if in the Radius.
  function COORDINATE:IsInRadius( Coordinate, Radius )

    local InVec2 = self:GetVec2()
    local Vec2 = Coordinate:GetVec2()

    local InRadius = UTILS.IsInRadius( InVec2, Vec2, Radius)

    return InRadius
  end


  --- Returns if a Coordinate is in a certain radius of this Coordinate in 3D space using the X, Y and Z axis.
  -- So Radius defines the radius of the a Sphere in 3D space around this coordinate.
  -- @param #COORDINATE self
  -- @param #COORDINATE ToCoordinate The coordinate that will be tested if it is in the radius of this coordinate.
  -- @param #number Radius The radius of the sphere in the 3D space around this coordinate.
  -- @return #boolean true if in the Sphere.
  function COORDINATE:IsInSphere( Coordinate, Radius )

    local InVec3 = self:GetVec3()
    local Vec3 = Coordinate:GetVec3()

    local InSphere = UTILS.IsInSphere( InVec3, Vec3, Radius)

    return InSphere
  end

  --- Get sun rise time for a specific date at the coordinate.
  -- @param #COORDINATE self
  -- @param #number Day The day.
  -- @param #number Month The month.
  -- @param #number Year The year.
  -- @param #boolean InSeconds If true, return the sun rise time in seconds.
  -- @return #string Sunrise time, e.g. "05:41".
  function COORDINATE:GetSunriseAtDate(Day, Month, Year, InSeconds)

    -- Day of the year.
    local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day)

    local Latitude, Longitude=self:GetLLDDM()

    local Tdiff=UTILS.GMTToLocalTimeDifference()

    local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff)

    if InSeconds then
      return sunrise
    else
      return UTILS.SecondsToClock(sunrise, true)
    end

  end

  --- Get sun rise time for a specific day of the year at the coordinate.
  -- @param #COORDINATE self
  -- @param #number DayOfYear The day of the year.
  -- @param #boolean InSeconds If true, return the sun rise time in seconds.
  -- @return #string Sunrise time, e.g. "05:41".
  function COORDINATE:GetSunriseAtDayOfYear(DayOfYear, InSeconds)

    local Latitude, Longitude=self:GetLLDDM()

    local Tdiff=UTILS.GMTToLocalTimeDifference()

    local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff)

    if InSeconds then
      return sunrise
    else
      return UTILS.SecondsToClock(sunrise, true)
    end

  end

  --- Get todays sun rise time.
  -- @param #COORDINATE self
  -- @param #boolean InSeconds If true, return the sun rise time in seconds.
  -- @return #string Sunrise time, e.g. "05:41".
  function COORDINATE:GetSunrise(InSeconds)

    -- Get current day of the year.
    local DayOfYear=UTILS.GetMissionDayOfYear()

    -- Lat and long at this point.
    local Latitude, Longitude=self:GetLLDDM()

    -- GMT time diff.
    local Tdiff=UTILS.GMTToLocalTimeDifference()

    -- Sunrise in seconds of the day.
    local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff)

    local date=UTILS.GetDCSMissionDate()

    -- Debug output.
    --self:I(string.format("Sun rise at lat=%.3f long=%.3f on %s (DayOfYear=%d): %s (%s sec of the day) (GMT %d)", Latitude, Longitude, date, DayOfYear, tostring(UTILS.SecondsToClock(sunrise)), tonumber(sunrise) or "0", Tdiff))

    if InSeconds or type(sunrise) == "string" then
      return sunrise
    else
      return UTILS.SecondsToClock(sunrise, true)
    end

  end

  --- Get minutes until the next sun rise at this coordinate.
  -- @param #COORDINATE self
  -- @param OnlyToday If true, only calculate the sun rise of today. If sun has already risen, the time in negative minutes since sunrise is reported.
  -- @return #number Minutes to the next sunrise.
  function COORDINATE:GetMinutesToSunrise(OnlyToday)

    -- Seconds of today
    local time=UTILS.SecondsOfToday()

    -- Next Sunrise in seconds.
    local sunrise=nil

    -- Time to sunrise.
    local delta=nil

    if OnlyToday then

      ---
      -- Sunrise of today
      ---

      sunrise=self:GetSunrise(true)

      delta=sunrise-time

    else

      ---
      -- Sunrise of tomorrow
      ---

      -- Tomorrows day of the year.
      local DayOfYear=UTILS.GetMissionDayOfYear()+1

      local Latitude, Longitude=self:GetLLDDM()

      local Tdiff=UTILS.GMTToLocalTimeDifference()

      sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff)

      delta=sunrise+UTILS.SecondsToMidnight()

    end

    return delta/60
  end

  --- Check if it is day, i.e. if the sun has risen about the horizon at this coordinate.
  -- @param #COORDINATE self
  -- @param #string Clock (Optional) Time in format "HH:MM:SS+D", e.g. "05:40:00+3" to check if is day at 5:40 at third day after mission start. Default is to check right now.
  -- @return #boolean If true, it is day. If false, it is night time.
  function COORDINATE:IsDay(Clock)

    if Clock then

      local Time=UTILS.ClockToSeconds(Clock)

      local clock=UTILS.Split(Clock, "+")[1]

      -- Tomorrows day of the year.
      local DayOfYear=UTILS.GetMissionDayOfYear(Time)

      local Latitude, Longitude=self:GetLLDDM()

      local Tdiff=UTILS.GMTToLocalTimeDifference()

      local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, true, Tdiff)
      local sunset=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff)
      
      if type(sunrise) == "string" or type(sunset) == "string" then
        if sunrise == "N/R" then return false end
        if sunset == "N/S" then return true end
      end
      
      local time=UTILS.ClockToSeconds(clock)

      -- Check if time is between sunrise and sunset.
      if time>sunrise and time<=sunset then
        return true
      else
        return false
      end

    else

      -- Todays sun rise in sec.
      local sunrise=self:GetSunrise(true)

      -- Todays sun set in sec.
      local sunset=self:GetSunset(true)
      
      if type(sunrise) == "string" or type(sunset) == "string" then
        if sunrise == "N/R" then return false end
        if sunset == "N/S" then return true end
      end

      -- Seconds passed since midnight.
      local time=UTILS.SecondsOfToday()

      -- Check if time is between sunrise and sunset.
      if time>sunrise and time<=sunset then
        return true
      else
        return false
      end

    end

  end

  --- Check if it is night, i.e. if the sun has set below the horizon at this coordinate.
  -- @param #COORDINATE self
  -- @param #string Clock (Optional) Time in format "HH:MM:SS+D", e.g. "05:40:00+3" to check if is night at 5:40 at third day after mission start. Default is to check right now.
  -- @return #boolean If true, it is night. If false, it is day time.
  function COORDINATE:IsNight(Clock)
    return not self:IsDay(Clock)
  end

  --- Get sun set time for a specific date at the coordinate.
  -- @param #COORDINATE self
  -- @param #number Day The day.
  -- @param #number Month The month.
  -- @param #number Year The year.
  -- @param #boolean InSeconds If true, return the sun rise time in seconds.
  -- @return #string Sunset time, e.g. "20:41".
  function COORDINATE:GetSunsetAtDate(Day, Month, Year, InSeconds)

    -- Day of the year.
    local DayOfYear=UTILS.GetDayOfYear(Year, Month, Day)

    local Latitude, Longitude=self:GetLLDDM()

    local Tdiff=UTILS.GMTToLocalTimeDifference()

    local sunset=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff)

    if InSeconds then
      return sunset
    else
      return UTILS.SecondsToClock(sunset, true)
    end

  end

  --- Get todays sun set time.
  -- @param #COORDINATE self
  -- @param #boolean InSeconds If true, return the sun set time in seconds.
  -- @return #string Sunrise time, e.g. "20:41".
  function COORDINATE:GetSunset(InSeconds)

    -- Get current day of the year.
    local DayOfYear=UTILS.GetMissionDayOfYear()

    -- Lat and long at this point.
    local Latitude, Longitude=self:GetLLDDM()

    -- GMT time diff.
    local Tdiff=UTILS.GMTToLocalTimeDifference()

    -- Sunrise in seconds of the day.
    local sunrise=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff)

    local date=UTILS.GetDCSMissionDate()

    -- Debug output.
    --self:I(string.format("Sun set at lat=%.3f long=%.3f on %s (DayOfYear=%d): %s (%s sec of the day) (GMT %d)", Latitude, Longitude, date, DayOfYear, tostring(UTILS.SecondsToClock(sunrise)), tostring(sunrise) or "0", Tdiff))

    if InSeconds or type(sunrise) == "string" then
      return sunrise
    else
      return UTILS.SecondsToClock(sunrise, true)
    end

  end

  --- Get minutes until the next sun set at this coordinate.
  -- @param #COORDINATE self
  -- @param OnlyToday If true, only calculate the sun set of today. If sun has already set, the time in negative minutes since sunset is reported.
  -- @return #number Minutes to the next sunrise.
  function COORDINATE:GetMinutesToSunset(OnlyToday)

    -- Seconds of today
    local time=UTILS.SecondsOfToday()

    -- Next Sunset in seconds.
    local sunset=nil

    -- Time to sunrise.
    local delta=nil

    if OnlyToday then

      ---
      -- Sunset of today
      ---

      sunset=self:GetSunset(true)

      delta=sunset-time

    else

      ---
      -- Sunset of tomorrow
      ---

      -- Tomorrows day of the year.
      local DayOfYear=UTILS.GetMissionDayOfYear()+1

      local Latitude, Longitude=self:GetLLDDM()

      local Tdiff=UTILS.GMTToLocalTimeDifference()

      sunset=UTILS.GetSunRiseAndSet(DayOfYear, Latitude, Longitude, false, Tdiff)

      delta=sunset+UTILS.SecondsToMidnight()

    end

    return delta/60
  end


  --- Return a BR string from a COORDINATE to the COORDINATE.
  -- @param #COORDINATE self
  -- @param #COORDINATE FromCoordinate The coordinate to measure the distance and the bearing from.
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @param #boolean MagVar If true, also get angle in MagVar for BR/BRA
  -- @param #number Precision Rounding precision, currently full km as default (=0)
  -- @return #string The BR text.
  function COORDINATE:ToStringBR( FromCoordinate, Settings, MagVar, Precision )
    local DirectionVec3 = FromCoordinate:GetDirectionVec3( self )
    local AngleRadians =  self:GetAngleRadians( DirectionVec3 )
    local Distance = self:Get2DDistance( FromCoordinate )
    return "BR, " .. self:GetBRText( AngleRadians, Distance, Settings, nil, MagVar, Precision )
  end

  --- Return a BRA string from a COORDINATE to the COORDINATE.
  -- @param #COORDINATE self
  -- @param #COORDINATE FromCoordinate The coordinate to measure the distance and the bearing from.
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @param #boolean MagVar If true, also get angle in MagVar for BR/BRA
  -- @return #string The BR text.
  function COORDINATE:ToStringBRA( FromCoordinate, Settings, MagVar )
    local DirectionVec3 = FromCoordinate:GetDirectionVec3( self )
    local AngleRadians =  self:GetAngleRadians( DirectionVec3 )
    local Distance = FromCoordinate:Get2DDistance( self )
    local Altitude = self:GetAltitudeText()
    return "BRA, " .. self:GetBRAText( AngleRadians, Distance, Settings, nil, MagVar )
  end
  
  --- Create a BRAA NATO call string to this COORDINATE from the FromCOORDINATE. Note - BRA delivered if no aspect can be obtained and "Merged" if range < 3nm
  -- @param #COORDINATE self
  -- @param #COORDINATE FromCoordinate The coordinate to measure the distance and the bearing from.
  -- @param #boolean Bogey Add "Bogey" at the end if true (not yet declared hostile or friendly)
  -- @param #boolean Spades Add "Spades" at the end if true (no IFF/VID ID yet known)
  -- @param #boolean SSML Add SSML tags speaking aspect as 0 1 2 and "brah" instead of BRAA
  -- @param #boolean Angels If true, altitude is e.g. "Angels 25" (i.e., a friendly plane), else "25 thousand"
  -- @param #boolean Zeros If using SSML, be aware that Google TTS will say "oh" and not "zero" for "0"; if Zeros is set to true, "0" will be replaced with "zero"
  -- @return #string The BRAA text.
  function COORDINATE:ToStringBRAANATO(FromCoordinate,Bogey,Spades,SSML,Angels,Zeros)
    
    -- Thanks to @Pikey
    local BRAANATO = "Merged."

    local currentCoord = FromCoordinate
    local DirectionVec3 = FromCoordinate:GetDirectionVec3( self )
    local AngleRadians =  self:GetAngleRadians( DirectionVec3 )
    
    local bearing = UTILS.Round( UTILS.ToDegree( AngleRadians ),0 )
    local magnetic = self:GetMagneticDeclination() or 0
    bearing = bearing - magnetic
    
    local rangeMetres = self:Get2DDistance(currentCoord)
    local rangeNM = UTILS.Round( UTILS.MetersToNM(rangeMetres), 0)
    
    local aspect = self:ToStringAspect(currentCoord)

    local alt = UTILS.Round(UTILS.MetersToFeet(self.y)/1000,0)--*1000
    
    local alttext = string.format("%d thousand",alt)
    
    if Angels then
      alttext = string.format("Angels %d",alt)
    end
    
    if alt < 1 then
      alttext = "very low"
    end

    -- corrected Track to be direction of travel of bogey (self in this case)
    local track = "Maneuver"
  
  if self.Heading then
    track = UTILS.BearingToCardinal(self.Heading) or "North"
    end
    
    if rangeNM > 3 then
      if SSML then -- google says "oh" instead of zero, be aware
        if Zeros then
          bearing = string.format("%03d",bearing)
          local AngleDegText = string.gsub(bearing,"%d","%1 ") -- "0 5 1 "
          AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1"
          AngleDegText = string.gsub(AngleDegText,"0","zero")
          if aspect == "" then
            BRAANATO = string.format("brah %s, %d miles, %s, Track %s", AngleDegText, rangeNM, alttext, track)
          else
            BRAANATO = string.format("brah %s, %d miles, %s, %s, Track %s", AngleDegText, rangeNM, alttext, aspect, track)      
          end  
        else
          if aspect == "" then
            BRAANATO = string.format("brah <say-as interpret-as='characters'>%03d</say-as>, %d miles, %s, Track %s", bearing, rangeNM, alttext, track)
          else
            BRAANATO = string.format("brah <say-as interpret-as='characters'>%03d</say-as>, %d miles, %s, %s, Track %s", bearing, rangeNM, alttext, aspect, track)      
          end
        end
        if Bogey and Spades then
          BRAANATO = BRAANATO..", Bogey, Spades."
        elseif Bogey then
          BRAANATO = BRAANATO..", Bogey."
        elseif Spades then
         BRAANATO = BRAANATO..", Spades."
        else
         BRAANATO = BRAANATO.."."
        end
      else
        if aspect == "" then
          BRAANATO = string.format("BRA %03d, %d miles, %s, Track %s",bearing, rangeNM, alttext, track)
        else
          BRAANATO = string.format("BRAA %03d, %d miles, %s, %s, Track %s",bearing, rangeNM, alttext, aspect, track)      
        end
        if Bogey and Spades then
          BRAANATO = BRAANATO..", Bogey, Spades."
        elseif Bogey then
          BRAANATO = BRAANATO..", Bogey."
        elseif Spades then
         BRAANATO = BRAANATO..", Spades."
        else
         BRAANATO = BRAANATO.."."
        end
      end
    end
      
    return BRAANATO 
  end
  
  --- Return the BULLSEYE as COORDINATE Object
  -- @param #number Coalition Coalition of the bulls eye to return, e.g. coalition.side.BLUE
  -- @return #COORDINATE self
  -- @usage
  --          -- note the dot (.) here,not using the colon (:)
  --          local redbulls = COORDINATE.GetBullseyeCoordinate(coalition.side.RED)
  function COORDINATE.GetBullseyeCoordinate(Coalition)
    return COORDINATE:NewFromVec3( coalition.getMainRefPoint( Coalition ) )
  end
  
  --- Return a BULLS string out of the BULLS of the coalition to the COORDINATE.
  -- @param #COORDINATE self
  -- @param DCS#coalition.side Coalition The coalition.
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @param #boolean MagVar If true, als get angle in magnetic
  -- @return #string The BR text.
  function COORDINATE:ToStringBULLS( Coalition, Settings, MagVar )
    local BullsCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( Coalition ) )
    local DirectionVec3 = BullsCoordinate:GetDirectionVec3( self )
    local AngleRadians =  self:GetAngleRadians( DirectionVec3 )
    local Distance = self:Get2DDistance( BullsCoordinate )
    local Altitude = self:GetAltitudeText()
    return "BULLS, " .. self:GetBRText( AngleRadians, Distance, Settings, nil, MagVar )
  end

  --- Return an aspect string from a COORDINATE to the Angle of the object.
  -- @param #COORDINATE self
  -- @param #COORDINATE TargetCoordinate The target COORDINATE.
  -- @return #string The Aspect string, which is Hot, Cold or Flanking.
  function COORDINATE:ToStringAspect( TargetCoordinate )
    local Heading = self.Heading
    local DirectionVec3 = self:GetDirectionVec3( TargetCoordinate )
    local Angle = self:GetAngleDegrees( DirectionVec3 )

    if Heading then
      local Aspect = Angle - Heading
      if Aspect > -135 and Aspect <= -45 then
        return "Flanking"
      end
      if Aspect > -45 and Aspect <= 45 then
        return "Hot"
      end
      if Aspect > 45 and Aspect <= 135 then
        return "Flanking"
      end
      if Aspect > 135 or Aspect <= -135 then
        return "Cold"
      end
    end
    return ""
  end

  --- Get Latitude and Longitude in Degrees Decimal Minutes (DDM).
  -- @param #COORDINATE self
  -- @return #number Latitude in DDM.
  -- @return #number Lontitude in DDM.
  function COORDINATE:GetLLDDM()
    return coord.LOtoLL( self:GetVec3() )
  end

  --- Get Latitude & Longitude text.
  -- @param #COORDINATE self
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @return #string LLText
  function COORDINATE:ToStringLL( Settings )
  
    local LL_Accuracy = Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy
    local lat, lon = coord.LOtoLL( self:GetVec3() )
    return string.format('%f', lat) .. ' ' .. string.format('%f', lon)
  end

  
  --- Provides a Lat Lon string in Degree Minute Second format.
  -- @param #COORDINATE self
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @return #string The LL DMS Text
  function COORDINATE:ToStringLLDMS( Settings )

    local LL_Accuracy = Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy
    local lat, lon = coord.LOtoLL( self:GetVec3() )
    return "LL DMS " .. UTILS.tostringLL( lat, lon, LL_Accuracy, true )
  end

  --- Provides a Lat Lon string in Degree Decimal Minute format.
  -- @param #COORDINATE self
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @return #string The LL DDM Text
  function COORDINATE:ToStringLLDDM( Settings )

    local LL_Accuracy = Settings and Settings.LL_Accuracy or _SETTINGS.LL_Accuracy
    local lat, lon = coord.LOtoLL( self:GetVec3() )
    return "LL DDM " .. UTILS.tostringLL( lat, lon, LL_Accuracy, false )
  end

  --- Provides a MGRS string
  -- @param #COORDINATE self
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @return #string The MGRS Text
  function COORDINATE:ToStringMGRS( Settings )

    local MGRS_Accuracy = Settings and Settings.MGRS_Accuracy or _SETTINGS.MGRS_Accuracy
    local lat, lon = coord.LOtoLL( self:GetVec3() )
    local MGRS = coord.LLtoMGRS( lat, lon )
    return "MGRS " .. UTILS.tostringMGRS( MGRS, MGRS_Accuracy )
  end
  
  --- Provides a COORDINATE from an MGRS String
  -- @param #COORDINATE self
  -- @param #string MGRSString MGRS String, e.g. "MGRS 37T DK 12345 12345"
  -- @return #COORDINATE self
  function COORDINATE:NewFromMGRSString( MGRSString )
    local myparts = UTILS.Split(MGRSString," ")
    local northing = tostring(myparts[5]) or ""
    local easting = tostring(myparts[4]) or ""
    if string.len(easting) < 5 then easting = easting..string.rep("0",5-string.len(easting)) end  
    if string.len(northing) < 5 then northing = northing..string.rep("0",5-string.len(northing)) end
    local MGRS = {
            UTMZone = myparts[2],
            MGRSDigraph = myparts[3],
            Easting = easting,
            Northing = northing,
          } 
    local lat, lon = coord.MGRStoLL(MGRS)
    local point = coord.LLtoLO(lat, lon, 0)
    local coord = COORDINATE:NewFromVec2({x=point.x,y=point.z})
    return coord
  end
  
  --- Provides a COORDINATE from an MGRS Coordinate
  -- @param #COORDINATE self
  -- @param #string UTMZone UTM Zone, e.g. "37T"
  -- @param #string MGRSDigraph Digraph, e.g. "DK"
  -- @param #string Easting Meters easting - string in order to allow for leading zeros, e.g. "01234". Should be 5 digits.
  -- @param #string Northing Meters northing - string in order to allow for leading zeros, e.g. "12340". Should be 5 digits.
  -- @return #COORDINATE self
  function COORDINATE:NewFromMGRS( UTMZone, MGRSDigraph, Easting, Northing )
    if string.len(Easting) < 5 then Easting = tostring(Easting..string.rep("0",5-string.len(Easting) )) end  
    if string.len(Northing) < 5 then Northing = tostring(Northing..string.rep("0",5-string.len(Northing) )) end
    local MGRS = {
            UTMZone = UTMZone,
            MGRSDigraph = MGRSDigraph,
            Easting = tostring(Easting),
            Northing = tostring(Northing),
          }
    local lat, lon = coord.MGRStoLL(MGRS)
    local point = coord.LLtoLO(lat, lon, 0)
    local coord = COORDINATE:NewFromVec2({x=point.x,y=point.z})
    return coord
  end

  --- Provides a coordinate string of the point, based on a coordinate format system:
  --   * Uses default settings in COORDINATE.
  --   * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default.
  -- @param #COORDINATE self
  -- @param #COORDINATE ReferenceCoord The reference coordinate.
  -- @param #string ReferenceName The reference name.
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @param #boolean MagVar If true also show angle in magnetic
  -- @return #string The coordinate Text in the configured coordinate system.
  function COORDINATE:ToStringFromRP( ReferenceCoord, ReferenceName, Controllable, Settings, MagVar )

    self:F2( { ReferenceCoord = ReferenceCoord, ReferenceName = ReferenceName } )

    local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS

    local IsAir = Controllable and Controllable:IsAirPlane() or false

    if IsAir then
      local DirectionVec3 = ReferenceCoord:GetDirectionVec3( self )
      local AngleRadians =  self:GetAngleRadians( DirectionVec3 )
      local Distance = self:Get2DDistance( ReferenceCoord )
      return "Targets are the last seen " .. self:GetBRText( AngleRadians, Distance, Settings, nil, MagVar ) .. " from " .. ReferenceName
    else
      local DirectionVec3 = ReferenceCoord:GetDirectionVec3( self )
      local AngleRadians =  self:GetAngleRadians( DirectionVec3 )
      local Distance = self:Get2DDistance( ReferenceCoord )
      return "Target are located " .. self:GetBRText( AngleRadians, Distance, Settings, nil, MagVar ) .. " from " .. ReferenceName
    end

    return nil

  end
  
  --- Provides a coordinate string of the point, based on a coordinate format system:
  --   * Uses default settings in COORDINATE.
  --   * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default.
  -- @param #COORDINATE self
  -- @param #COORDINATE ReferenceCoord The reference coordinate.
  -- @param #string ReferenceName The reference name.
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @param #boolean MagVar If true also get the angle as magnetic
  -- @return #string The coordinate Text in the configured coordinate system.
  function COORDINATE:ToStringFromRPShort( ReferenceCoord, ReferenceName, Controllable, Settings, MagVar )

    self:F2( { ReferenceCoord = ReferenceCoord, ReferenceName = ReferenceName } )

    local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS

    local IsAir = Controllable and Controllable:IsAirPlane() or false

    if IsAir then
      local DirectionVec3 = ReferenceCoord:GetDirectionVec3( self )
      local AngleRadians =  self:GetAngleRadians( DirectionVec3 )
      local Distance = self:Get2DDistance( ReferenceCoord )
      return self:GetBRText( AngleRadians, Distance, Settings, nil, MagVar ) .. " from " .. ReferenceName
    else
      local DirectionVec3 = ReferenceCoord:GetDirectionVec3( self )
      local AngleRadians =  self:GetAngleRadians( DirectionVec3 )
      local Distance = self:Get2DDistance( ReferenceCoord )
      return self:GetBRText( AngleRadians, Distance, Settings, nil, MagVar ) .. " from " .. ReferenceName
    end

    return nil

  end
  
  --- Provides a coordinate string of the point, based on the A2G coordinate format system.
  -- @param #COORDINATE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @param #boolean MagVar If true, also get angle in MagVar for BR/BRA
  -- @return #string The coordinate Text in the configured coordinate system.
  function COORDINATE:ToStringA2G( Controllable, Settings, MagVar )

    self:F2( { Controllable = Controllable and Controllable:GetName() } )

    local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS

    if Settings:IsA2G_BR()  then
      -- If no Controllable is given to calculate the BR from, then MGRS will be used!!!
      if Controllable then
        local Coordinate = Controllable:GetCoordinate()
        return Controllable and self:ToStringBR( Coordinate, Settings, MagVar ) or self:ToStringMGRS( Settings )
      else
        return self:ToStringMGRS( Settings )
      end
    end
    if Settings:IsA2G_LL_DMS()  then
      return self:ToStringLLDMS( Settings )
    end
    if Settings:IsA2G_LL_DDM()  then
      return self:ToStringLLDDM( Settings )
    end
    if Settings:IsA2G_MGRS() then
      return self:ToStringMGRS( Settings )
    end

    return nil

  end


  --- Provides a coordinate string of the point, based on the A2A coordinate format system.
  -- @param #COORDINATE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @param #boolean MagVar If true, also get angle in MagVar for BR/BRA
  -- @return #string The coordinate Text in the configured coordinate system.
  function COORDINATE:ToStringA2A( Controllable, Settings, MagVar ) 

    self:F2( { Controllable = Controllable and Controllable:GetName() } )

    local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS

    if Settings:IsA2A_BRAA() then
      if Controllable then
        local Coordinate = Controllable:GetCoordinate()
        return self:ToStringBRA( Coordinate, Settings, MagVar )
      else
        return self:ToStringMGRS( Settings )
      end
    end
    if Settings:IsA2A_BULLS() then
      local Coalition = Controllable:GetCoalition()
      return self:ToStringBULLS( Coalition, Settings, MagVar )
    end
    if Settings:IsA2A_LL_DMS()  then
      return self:ToStringLLDMS( Settings )
    end
    if Settings:IsA2A_LL_DDM()  then
      return self:ToStringLLDDM( Settings )
    end
    if Settings:IsA2A_MGRS() then
      return self:ToStringMGRS( Settings )
    end

    return nil

  end

  --- Provides a coordinate string of the point, based on a coordinate format system:
  --   * Uses default settings in COORDINATE.
  --   * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default.
  -- @param #COORDINATE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The controllable to retrieve the settings from, otherwise the default settings will be chosen.
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @return #string The coordinate Text in the configured coordinate system.
  function COORDINATE:ToString( Controllable, Settings )

--    self:E( { Controllable = Controllable and Controllable:GetName() } )

    local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS

    local ModeA2A = nil
    
    --[[
    if Task then
      if Task:IsInstanceOf( TASK_A2A ) then
        ModeA2A = true
      else
        if Task:IsInstanceOf( TASK_A2G ) then
          ModeA2A = false
        else
          if Task:IsInstanceOf( TASK_CARGO ) then
            ModeA2A = false
          end
            if Task:IsInstanceOf( TASK_CAPTURE_ZONE ) then
              ModeA2A = false
            end
        end
      end
    end
    --]]

    if ModeA2A == nil then
      local IsAir = Controllable and ( Controllable:IsAirPlane() or Controllable:IsHelicopter() ) or false
      if IsAir  then
        ModeA2A = true
      else
        ModeA2A = false
      end
    end


    if ModeA2A == true then
      return self:ToStringA2A( Controllable, Settings )
    else
      return self:ToStringA2G( Controllable, Settings )
    end

    return nil

  end

  --- Provides a pressure string of the point, based on a measurement system:
  --   * Uses default settings in COORDINATE.
  --   * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default.
  -- @param #COORDINATE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @return #string The pressure text in the configured measurement system.
  function COORDINATE:ToStringPressure( Controllable, Settings ) 

    self:F2( { Controllable = Controllable and Controllable:GetName() } )

    local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS

    return self:GetPressureText( nil, Settings )
  end

  --- Provides a wind string of the point, based on a measurement system:
  --   * Uses default settings in COORDINATE.
  --   * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default.
  -- @param #COORDINATE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable
  -- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
  -- @return #string The wind text in the configured measurement system.
  function COORDINATE:ToStringWind( Controllable, Settings )

    self:F2( { Controllable = Controllable and Controllable:GetName() } )

    local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS

    return self:GetWindText( nil, Settings )
  end

  --- Provides a temperature string of the point, based on a measurement system:
  --   * Uses default settings in COORDINATE.
  --   * Can be overridden if for a GROUP containing x clients, a menu was selected to override the default.
  -- @param #COORDINATE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable
  -- @param Core.Settings#SETTINGS
  -- @return #string The temperature text in the configured measurement system.
  function COORDINATE:ToStringTemperature( Controllable, Settings )

    self:F2( { Controllable = Controllable and Controllable:GetName() } )

    local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS

    return self:GetTemperatureText( nil, Settings )
  end
  
  
  --- Function to check if a coordinate is in a steep (>8% elevation) area of the map
  -- @param #COORDINATE self
  -- @param #number Radius (Optional) Radius to check around the coordinate, defaults to 50m (100m diameter)
  -- @param #number Minelevation (Optional) Elevation from which on a area is defined as steep, defaults to 8% (8m height gain across 100 meters)
  -- @return #boolean IsSteep If true, area is steep
  -- @return #number MaxElevation Elevation in meters measured over 100m
  function COORDINATE:IsInSteepArea(Radius,Minelevation)
    local steep = false
    local elev = Minelevation or 8
    local bdelta = 0
    local h0 = self:GetLandHeight()
    local radius = Radius or 50
    local diam = radius * 2
    for i=0,150,30 do
      local polar = math.fmod(i+180,360)
      local c1 = self:Translate(radius,i,false,false)
      local c2 = self:Translate(radius,polar,false,false)
      local h1 = c1:GetLandHeight()
      local h2 = c2:GetLandHeight()
      local d1 = math.abs(h1-h2)
      local d2 = math.abs(h0-h1)
      local d3 = math.abs(h0-h2)
      local dm = d1 > d2 and d1 or d2
      local dm1 = dm > d3 and dm or d3
      bdelta = dm1 > bdelta and dm1 or bdelta
      self:T(string.format("d1=%d, d2=%d, d3=%d, max delta=%d",d1,d2,d3,bdelta))
    end
    local steepness = bdelta / (radius / 100)    
    if steepness >= elev then steep = true end
    return steep, math.floor(steepness)
  end
  
    --- Function to check if a coordinate is in a flat (<8% elevation) area of the map
  -- @param #COORDINATE self
  -- @param #number Radius (Optional) Radius to check around the coordinate, defaults to 50m (100m diameter)
  -- @param #number Minelevation (Optional) Elevation from which on a area is defined as steep, defaults to 8% (8m height gain across 100 meters)
  -- @return #boolean IsFlat If true, area is flat
  -- @return #number MaxElevation Elevation in meters measured over 100m
  function COORDINATE:IsInFlatArea(Radius,Minelevation)
    local steep, elev = self:IsInSteepArea(Radius,Minelevation)
    local flat = not steep
    return flat, elev
  end
  
  --- Return a random COORDINATE within an Outer Radius and optionally NOT within an Inner Radius of the COORDINATE.
  -- @param #COORDINATE self
  -- @param DCS#Distance OuterRadius
  -- @param DCS#Distance InnerRadius
  -- @return #COORDINATE
  function COORDINATE:GetRandomPointVec3InRadius( OuterRadius, InnerRadius )
    return COORDINATE:NewFromVec3( self:GetRandomVec3InRadius( OuterRadius, InnerRadius ) )
  end


--- Search for clear zones in a given area. A powerful and efficient function using Disposition to find clear areas for spawning ground units avoiding trees, water and map scenery.
-- @param #number SearchRadius Radius of the search area.
-- @param #number PosRadius Required clear radius around each position.
-- @param #number NumPositions Number of positions to find.
-- @return #table A table of Core.Point#COORDINATE that are clear of map objects within the given PosRadius. nil if no positions are found.
  function COORDINATE:GetSimpleZones(SearchRadius, PosRadius, NumPositions)
    local clearPositions = UTILS.GetSimpleZones(self:GetVec3(), SearchRadius, PosRadius, NumPositions)
    if clearPositions and #clearPositions > 0 then
        local coords = {}
        for _, pos in pairs(clearPositions) do
          local coord = COORDINATE:NewFromVec2(pos)
          table.insert(coords, coord)
        end
        return coords
    end
    return nil
  end

end

do 

  --- The POINT_VEC3 class
  -- @type POINT_VEC3
  -- @field #number x The x coordinate in 3D space.
  -- @field #number y The y coordinate in 3D space.
  -- @field #number z The z COORDINATE in 3D space.
  -- @field Utilities.Utils#SMOKECOLOR SmokeColor
  -- @field Utilities.Utils#FLARECOLOR FlareColor
  -- @field #POINT_VEC3.RoutePointAltType RoutePointAltType
  -- @field #POINT_VEC3.RoutePointType RoutePointType
  -- @field #POINT_VEC3.RoutePointAction RoutePointAction
  -- @extends #COORDINATE


  --- Defines a 3D point in the simulator and with its methods, you can use or manipulate the point in 3D space.
  --
  -- **DEPRECATED - PLEASE USE COORDINATE!**
  --
  -- **Important Note:** Most of the functions in this section were taken from MIST, and reworked to OO concepts.
  -- In order to keep the credibility of the the author,
  -- I want to emphasize that the formulas embedded in the MIST framework were created by Grimes or previous authors,
  -- who you can find on the Eagle Dynamics Forums.
  --
  --
  -- ## POINT_VEC3 constructor
  --
  -- A new POINT_VEC3 object can be created with:
  --
  --  * @{#POINT_VEC3.New}(): a 3D point.
  --  * @{#POINT_VEC3.NewFromVec3}(): a 3D point created from a @{DCS#Vec3}.
  --
  --
  -- ## Manupulate the X, Y, Z coordinates of the POINT_VEC3
  --
  -- A POINT_VEC3 class works in 3D space. It contains internally an X, Y, Z coordinate.
  -- Methods exist to manupulate these coordinates.
  --
  -- The current X, Y, Z axis can be retrieved with the methods @{#POINT_VEC3.GetX}(), @{#POINT_VEC3.GetY}(), @{#POINT_VEC3.GetZ}() respectively.
  -- The methods @{#POINT_VEC3.SetX}(), @{#POINT_VEC3.SetY}(), @{#POINT_VEC3.SetZ}() change the respective axis with a new value.
  -- The current axis values can be changed by using the methods @{#POINT_VEC3.AddX}(), @{#POINT_VEC3.AddY}(), @{#POINT_VEC3.AddZ}()
  -- to add or substract a value from the current respective axis value.
  -- Note that the Set and Add methods return the current POINT_VEC3 object, so these manipulation methods can be chained... For example:
  --
  --      local Vec3 = PointVec3:AddX( 100 ):AddZ( 150 ):GetVec3()
  --
  --
  -- ## 3D calculation methods
  --
  -- Various calculation methods exist to use or manipulate 3D space. Find below a short description of each method:
  --
  --
  -- ## Point Randomization
  --
  -- Various methods exist to calculate random locations around a given 3D point.
  --
  --   * @{#POINT_VEC3.GetRandomPointVec3InRadius}(): Provides a random 3D point around the current 3D point, in the given inner to outer band.
  --
  --
  -- @field #POINT_VEC3
  POINT_VEC3 = {
    ClassName = "POINT_VEC3",
    Metric = true,
    RoutePointAltType = {
      BARO = "BARO",
    },
    RoutePointType = {
      TakeOffParking = "TakeOffParking",
      TurningPoint = "Turning Point",
    },
    RoutePointAction = {
      FromParkingArea = "From Parking Area",
      TurningPoint = "Turning Point",
    },
  }

  --- RoutePoint AltTypes
  -- @type POINT_VEC3.RoutePointAltType
  -- @field BARO "BARO"

  --- RoutePoint Types
  -- @type POINT_VEC3.RoutePointType
  -- @field TakeOffParking "TakeOffParking"
  -- @field TurningPoint "Turning Point"

  --- RoutePoint Actions
  -- @type POINT_VEC3.RoutePointAction
  -- @field FromParkingArea "From Parking Area"
  -- @field TurningPoint "Turning Point"

  -- Constructor.

  --- Create a new POINT_VEC3 object.
  -- @param #POINT_VEC3 self
  -- @param DCS#Distance x The x coordinate of the Vec3 point, pointing to the North.
  -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing Upwards.
  -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the Right.
  -- @return Core.Point#POINT_VEC3
  function POINT_VEC3:New( x, y, z )

    local self = BASE:Inherit( self, COORDINATE:New( x, y, z ) ) -- Core.Point#POINT_VEC3
    self:F2( self )

    return self
  end

end

do

  --- @type POINT_VEC2
  -- @field DCS#Distance x The x coordinate in meters.
  -- @field DCS#Distance y the y coordinate in meters.
  -- @extends Core.Point#COORDINATE

  --- Defines a 2D point in the simulator. The height coordinate (if needed) will be the land height + an optional added height specified.
  --
  --  **DEPRECATED - PLEASE USE COORDINATE!**
  --
  -- ## POINT_VEC2 constructor
  --
  -- A new POINT_VEC2 instance can be created with:
  --
  --  * @{Core.Point#POINT_VEC2.New}(): a 2D point, taking an additional height parameter.
  --  * @{Core.Point#POINT_VEC2.NewFromVec2}(): a 2D point created from a @{DCS#Vec2}.
  --
  -- ## Manupulate the X, Altitude, Y coordinates of the 2D point
  --
  -- A POINT_VEC2 class works in 2D space, with an altitude setting. It contains internally an X, Altitude, Y coordinate.
  -- Methods exist to manupulate these coordinates.
  --
  -- The current X, Altitude, Y axis can be retrieved with the methods @{#POINT_VEC2.GetX}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetY}() respectively.
  -- The methods @{#POINT_VEC2.SetX}(), @{#POINT_VEC2.SetAlt}(), @{#POINT_VEC2.SetY}() change the respective axis with a new value.
  -- The current Lat(itude), Alt(itude), Lon(gitude) values can also be retrieved with the methods @{#POINT_VEC2.GetLat}(), @{#POINT_VEC2.GetAlt}(), @{#POINT_VEC2.GetLon}() respectively.
  -- The current axis values can be changed by using the methods @{#POINT_VEC2.AddX}(), @{#POINT_VEC2.AddAlt}(), @{#POINT_VEC2.AddY}()
  -- to add or substract a value from the current respective axis value.
  -- Note that the Set and Add methods return the current POINT_VEC2 object, so these manipulation methods can be chained... For example:
  --
  --      local Vec2 = PointVec2:AddX( 100 ):AddY( 2000 ):GetVec2()
  --
  -- @field #POINT_VEC2
  POINT_VEC2 = {
    ClassName = "POINT_VEC2",
  }



  --- POINT_VEC2 constructor.
  -- @param #POINT_VEC2 self
  -- @param DCS#Distance x The x coordinate of the Vec3 point, pointing to the North.
  -- @param DCS#Distance y The y coordinate of the Vec3 point, pointing to the Right.
  -- @param DCS#Distance LandHeightAdd (optional) The default height if required to be evaluated will be the land height of the x, y coordinate. You can specify an extra height to be added to the land height.
  -- @return Core.Point#POINT_VEC2
  function POINT_VEC2:New( x, y, LandHeightAdd )

    local LandHeight = land.getHeight( { ["x"] = x, ["y"] = y } )

    LandHeightAdd = LandHeightAdd or 0
    LandHeight = LandHeight + LandHeightAdd

    local self = BASE:Inherit( self, COORDINATE:New( x, LandHeight, y ) ) -- Core.Point#POINT_VEC2
    self:F2( self )

    return self
  end

end
--- **Core** - Models a velocity or speed, which can be expressed in various formats according the settings.
-- 
-- ===
-- 
-- ## Features:
-- 
--   * Convert velocity in various metric systems.
--   * Set the velocity.
--   * Create a text in a specific format of a velocity.
--   
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Core.Velocity
-- @image MOOSE.JPG

do -- Velocity

  -- @type VELOCITY
  -- @extends Core.Base#BASE


  --- VELOCITY models a speed, which can be expressed in various formats according the Settings.
  -- 
  -- ## VELOCITY constructor
  --   
  --   * @{#VELOCITY.New}(): Creates a new VELOCITY object.
  -- 
  -- @field #VELOCITY
  VELOCITY = {
    ClassName = "VELOCITY",
  }

  --- VELOCITY Constructor.
  -- @param #VELOCITY self
  -- @param #number VelocityMps The velocity in meters per second. 
  -- @return #VELOCITY
  function VELOCITY:New( VelocityMps )
    local self = BASE:Inherit( self, BASE:New() ) -- #VELOCITY
    self:F( {} )
    self.Velocity = VelocityMps
    return self
  end

  --- Set the velocity in Mps (meters per second).
  -- @param #VELOCITY self
  -- @param #number VelocityMps The velocity in meters per second. 
  -- @return #VELOCITY
  function VELOCITY:Set( VelocityMps )
    self.Velocity = VelocityMps
    return self
  end

  --- Get the velocity in Mps (meters per second).
  -- @param #VELOCITY self
  -- @return #number The velocity in meters per second. 
  function VELOCITY:Get()
    return self.Velocity
  end

  --- Set the velocity in Kmph (kilometers per hour).
  -- @param #VELOCITY self
  -- @param #number VelocityKmph The velocity in kilometers per hour. 
  -- @return #VELOCITY
  function VELOCITY:SetKmph( VelocityKmph )
    self.Velocity = UTILS.KmphToMps( VelocityKmph )
    return self
  end

  --- Get the velocity in Kmph (kilometers per hour).
  -- @param #VELOCITY self
  -- @return #number The velocity in kilometers per hour. 
  function VELOCITY:GetKmph()

    return UTILS.MpsToKmph( self.Velocity )
  end

  --- Set the velocity in Miph (miles per hour).
  -- @param #VELOCITY self
  -- @param #number VelocityMiph The velocity in miles per hour. 
  -- @return #VELOCITY
  function VELOCITY:SetMiph( VelocityMiph )
    self.Velocity = UTILS.MiphToMps( VelocityMiph )
    return self
  end

  --- Get the velocity in Miph (miles per hour).
  -- @param #VELOCITY self
  -- @return #number The velocity in miles per hour. 
  function VELOCITY:GetMiph()
    return UTILS.MpsToMiph( self.Velocity )
  end

  --- Get the velocity in text, according the player @{Core.Settings}.
  -- @param #VELOCITY self
  -- @param Core.Settings#SETTINGS Settings
  -- @return #string The velocity in text. 
  function VELOCITY:GetText( Settings )
    local Settings = Settings or _SETTINGS
    if self.Velocity ~= 0 then
      if Settings:IsMetric() then
        return string.format( "%d km/h", UTILS.MpsToKmph( self.Velocity ) )
      else
        return string.format( "%d mi/h", UTILS.MpsToMiph( self.Velocity ) )
      end
    else
      return "stationary"
    end
  end

  --- Get the velocity in text, according the player or default @{Core.Settings}.
  -- @param #VELOCITY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable
  -- @param Core.Settings#SETTINGS Settings
  -- @return #string The velocity in text according the player or default @{Core.Settings}
  function VELOCITY:ToString( VelocityGroup, Settings ) -- R2.3
    self:F( { Group = VelocityGroup and VelocityGroup:GetName() } )
    local Settings = Settings or ( VelocityGroup and _DATABASE:GetPlayerSettings( VelocityGroup:GetPlayerName() ) ) or _SETTINGS
    return self:GetText( Settings )
  end

end

do -- VELOCITY_POSITIONABLE

  -- @type VELOCITY_POSITIONABLE
  -- @extends Core.Base#BASE


  --- # VELOCITY_POSITIONABLE class, extends @{Core.Base#BASE}
  -- 
  -- @{#VELOCITY_POSITIONABLE} monitors the speed of a @{Wrapper.Positionable#POSITIONABLE} in the simulation, which can be expressed in various formats according the Settings.
  -- 
  -- ## 1. VELOCITY_POSITIONABLE constructor
  --   
  --   * @{#VELOCITY_POSITIONABLE.New}(): Creates a new VELOCITY_POSITIONABLE object.
  -- 
  -- @field #VELOCITY_POSITIONABLE
  VELOCITY_POSITIONABLE = {
    ClassName = "VELOCITY_POSITIONABLE",
  }

  --- VELOCITY_POSITIONABLE Constructor.
  -- @param #VELOCITY_POSITIONABLE self
  -- @param Wrapper.Positionable#POSITIONABLE Positionable The Positionable to monitor the speed. 
  -- @return #VELOCITY_POSITIONABLE
  function VELOCITY_POSITIONABLE:New( Positionable )
    local self = BASE:Inherit( self, VELOCITY:New() ) -- #VELOCITY_POSITIONABLE
    self:F( {} )
    self.Positionable = Positionable
    return self
  end

  --- Get the velocity in Mps (meters per second).
  -- @param #VELOCITY_POSITIONABLE self
  -- @return #number The velocity in meters per second. 
  function VELOCITY_POSITIONABLE:Get()
    return self.Positionable:GetVelocityMPS() or 0
  end

  --- Get the velocity in Kmph (kilometers per hour).
  -- @param #VELOCITY_POSITIONABLE self
  -- @return #number The velocity in kilometers per hour. 
  function VELOCITY_POSITIONABLE:GetKmph()

    return UTILS.MpsToKmph( self.Positionable:GetVelocityMPS() or 0)
  end

  --- Get the velocity in Miph (miles per hour).
  -- @param #VELOCITY_POSITIONABLE self
  -- @return #number The velocity in miles per hour. 
  function VELOCITY_POSITIONABLE:GetMiph()
    return UTILS.MpsToMiph( self.Positionable:GetVelocityMPS() or 0 )
  end

  --- Get the velocity in text, according the player or default @{Core.Settings}.
  -- @param #VELOCITY_POSITIONABLE self
  -- @return #string The velocity in text according the player or default @{Core.Settings}
  function VELOCITY_POSITIONABLE:ToString() -- R2.3
    self:F( { Group = self.Positionable and self.Positionable:GetName() } )
    local Settings = Settings or ( self.Positionable and _DATABASE:GetPlayerSettings( self.Positionable:GetPlayerName() ) ) or _SETTINGS
    self.Velocity = self.Positionable:GetVelocityMPS()
    return self:GetText( Settings )
  end

end--- **Core** - Informs the players using messages during a simulation.
--
-- ===
--
-- ## Features:
--
--   * A more advanced messaging system using the DCS message system.
--   * Time messages.
--   * Send messages based on a message type, which has a pre-defined duration that can be tweaked in SETTINGS.
--   * Send message to all players.
--   * Send messages to a coalition.
--   * Send messages to a specific group.
--   * Send messages to a specific unit or client.
-- 
-- ===
--
-- @module Core.Message
-- @image Core_Message.JPG

--- The MESSAGE class
-- @type MESSAGE
-- @extends Core.Base#BASE

--- Message System to display Messages to Clients, Coalitions or All.
-- Messages are shown on the display panel for an amount of seconds, and will then disappear.
-- Messages can contain a category which is indicating the category of the message.
--
-- ## MESSAGE construction
--
-- Messages are created with @{#MESSAGE.New}. Note that when the MESSAGE object is created, no message is sent yet.
-- To send messages, you need to use the To functions.
--
-- ## Send messages to an audience
--
-- Messages are sent:
--
--   * To a @{Wrapper.Client} using @{#MESSAGE.ToClient}().
--   * To a @{Wrapper.Group} using @{#MESSAGE.ToGroup}()
--   * To a @{Wrapper.Unit} using @{#MESSAGE.ToUnit}()
--   * To a coalition using @{#MESSAGE.ToCoalition}().
--   * To the red coalition using @{#MESSAGE.ToRed}().
--   * To the blue coalition using @{#MESSAGE.ToBlue}().
--   * To all Players using @{#MESSAGE.ToAll}().
--
-- ## Send conditionally to an audience
--
-- Messages can be sent conditionally to an audience (when a condition is true):
--
--   * To all players using @{#MESSAGE.ToAllIf}().
--   * To a coalition using @{#MESSAGE.ToCoalitionIf}().
--
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions: **Applevangelist**
--
-- ===
--
-- @field #MESSAGE
MESSAGE = {
  ClassName = "MESSAGE",
  MessageCategory = 0,
  MessageID = 0,
}

--- Message Types
-- @type MESSAGE.Type
MESSAGE.Type = {
  Update = "Update",
  Information = "Information",
  Briefing = "Briefing Report",
  Overview = "Overview Report",
  Detailed = "Detailed Report",
}

--- Creates a new MESSAGE object. Note that these MESSAGE objects are not yet displayed on the display panel. You must use the functions @{#MESSAGE.ToClient} or @{#MESSAGE.ToCoalition} or @{#MESSAGE.ToAll} to send these Messages to the respective recipients.
-- @param self
-- @param #string Text is the text of the Message.
-- @param #number Duration Duration in seconds how long the message text is shown.
-- @param #string Category (Optional) String expressing the "category" of the Message. The category will be shown as the first text in the message followed by a ": ".
-- @param #boolean ClearScreen (optional) Clear all previous messages if true.
-- @return #MESSAGE self
-- @usage
--
--   -- Create a series of new Messages.
--   -- MessageAll is meant to be sent to all players, for 25 seconds, and is classified as "Score".
--   -- MessageRED is meant to be sent to the RED players only, for 10 seconds, and is classified as "End of Mission", with ID "Win".
--   -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score".
--   -- MessageClient1 is meant to be sent to a Client, for 25 seconds, and is classified as "Score", with ID "Score".
--   MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!",  25, "End of Mission" )
--   MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty" )
--   MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target",  25, "Score" )
--   MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", 25, "Score")
--
function MESSAGE:New( Text, Duration, Category, ClearScreen )

  local self = BASE:Inherit( self, BASE:New() )
  
  self:F( { Text, Duration, Category } )

  self.MessageType = nil

  -- When no MessageCategory is given, we don't show it as a title... 
  if Category and Category ~= "" then
    if Category:sub( -1 ) ~= "\n" then
      self.MessageCategory = Category .. ": "
    else
      self.MessageCategory = Category:sub( 1, -2 ) .. ":\n"
    end
  else
    self.MessageCategory = ""
  end

  self.ClearScreen = false
  if ClearScreen ~= nil then
    self.ClearScreen = ClearScreen
  end

  self.MessageDuration = Duration or 5
  self.MessageTime = timer.getTime()
  self.MessageText = Text:gsub( "^\n", "", 1 ):gsub( "\n$", "", 1 )

  self.MessageSent = false
  self.MessageGroup = false
  self.MessageCoalition = false

  return self
end

--- Creates a new MESSAGE object of a certain type.
-- Note that these MESSAGE objects are not yet displayed on the display panel.
-- You must use the functions @{Core.Message#ToClient} or @{Core.Message#ToCoalition} or @{Core.Message#ToAll} to send these Messages to the respective recipients.
-- The message display times are automatically defined based on the timing settings in the @{Core.Settings} menu.
-- @param self
-- @param #string MessageText is the text of the Message.
-- @param #MESSAGE.Type MessageType The type of the message.
-- @param #boolean ClearScreen (optional) Clear all previous messages.
-- @return #MESSAGE
-- @usage
--
--   MessageAll = MESSAGE:NewType( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", MESSAGE.Type.Information )
--   MessageRED = MESSAGE:NewType( "To the RED Players: You receive a penalty because you've killed one of your own units", MESSAGE.Type.Information )
--   MessageClient1 = MESSAGE:NewType( "Congratulations, you've just hit a target", MESSAGE.Type.Update )
--   MessageClient2 = MESSAGE:NewType( "Congratulations, you've just killed a target", MESSAGE.Type.Update )
--
function MESSAGE:NewType( MessageText, MessageType, ClearScreen )

  local self = BASE:Inherit( self, BASE:New() )
  self:F( { MessageText } )

  self.MessageType = MessageType

  self.ClearScreen = false
  if ClearScreen ~= nil then
    self.ClearScreen = ClearScreen
  end

  self.MessageTime = timer.getTime()
  self.MessageText = MessageText:gsub( "^\n", "", 1 ):gsub( "\n$", "", 1 )

  return self
end

--- Clears all previous messages from the screen before the new message is displayed. Not that this must come before all functions starting with ToX(), e.g. ToAll(), ToGroup() etc.
-- @param #MESSAGE self
-- @return #MESSAGE
function MESSAGE:Clear()
  self:F()
  self.ClearScreen = true
  return self
end

--- Sends a MESSAGE to a Client Group. Note that the Group needs to be defined within the ME with the skillset "Client" or "Player".
-- @param #MESSAGE self
-- @param Wrapper.Client#CLIENT Client is the Group of the Client.
-- @param Core.Settings#SETTINGS Settings used to display the message.
-- @return #MESSAGE
-- @usage
--
--   -- Send the 2 messages created with the @{New} method to the Client Group.
--   -- Note that the Message of MessageClient2 is overwriting the Message of MessageClient1.
--   Client = CLIENT:FindByName("NameOfClientUnit")
--
--   MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", 25, "Score" ):ToClient( Client )
--   MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", 25, "Score" ):ToClient( Client )
--   or
--   MESSAGE:New( "Congratulations, you've just hit a target", 25, "Score"):ToClient( Client )
--   MESSAGE:New( "Congratulations, you've just killed a target", 25, "Score"):ToClient( Client )
--   or
--   MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", 25, "Score")
--   MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", 25, "Score")
--   MessageClient1:ToClient( Client )
--   MessageClient2:ToClient( Client )
--
function MESSAGE:ToClient( Client, Settings )
  self:F( Client )
  self:ToUnit(Client,Settings)
  return self
end

--- Sends a MESSAGE to a Group.
-- @param #MESSAGE self
-- @param Wrapper.Group#GROUP Group to which the message is displayed.
-- @param Core.Settings#Settings Settings (Optional) Settings for message display.
-- @return #MESSAGE Message object.
function MESSAGE:ToGroup( Group, Settings )
  self:F( Group.GroupName )

  if Group and Group:IsAlive() then

    if self.MessageType then
      local Settings = Settings or (Group and _DATABASE:GetPlayerSettings( Group:GetPlayerName() )) or _SETTINGS -- Core.Settings#SETTINGS
      self.MessageDuration = Settings:GetMessageTime( self.MessageType )
      self.MessageCategory = "" -- self.MessageType .. ": "
    end

    if self.MessageDuration ~= 0 then
      self:T( self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ) .. " / " .. self.MessageDuration )
      trigger.action.outTextForGroup( Group:GetID(), self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ), self.MessageDuration, self.ClearScreen )
    end
  end

  return self
end

--- Sends a MESSAGE to a Unit. 
-- @param #MESSAGE self
-- @param Wrapper.Unit#UNIT Unit to which the message is displayed.
-- @param Core.Settings#Settings Settings (Optional) Settings for message display.
-- @return #MESSAGE Message object.
function MESSAGE:ToUnit( Unit, Settings )
  self:F( Unit.IdentifiableName )

  if Unit and Unit:IsAlive() then
    
    if self.MessageType then
      local Settings = Settings or ( Unit and _DATABASE:GetPlayerSettings( Unit:GetPlayerName() ) ) or _SETTINGS -- Core.Settings#SETTINGS
      self.MessageDuration = Settings:GetMessageTime( self.MessageType )
      self.MessageCategory = "" -- self.MessageType .. ": "
    end

    if self.MessageDuration ~= 0 then
      self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration )
      local ID = Unit:GetID()
      trigger.action.outTextForUnit( Unit:GetID(), self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen )
    end
  end
  
  return self
end

--- Sends a MESSAGE to a Country. 
-- @param #MESSAGE self
-- @param #number Country to which the message is displayed, e.g. country.id.GERMANY. For all country numbers see here: [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_country)
-- @param Core.Settings#Settings Settings (Optional) Settings for message display.
-- @return #MESSAGE Message object.
function MESSAGE:ToCountry( Country, Settings )
  self:F(Country )
  if Country then   
    if self.MessageType then
      local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS
      self.MessageDuration = Settings:GetMessageTime( self.MessageType )
      self.MessageCategory = "" -- self.MessageType .. ": "
    end
    if self.MessageDuration ~= 0 then
      self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration )
      trigger.action.outTextForCountry( Country, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen )
    end
  end  
  return self
end

--- Sends a MESSAGE to a Country. 
-- @param #MESSAGE self
-- @param #number Country to which the message is displayed, , e.g. country.id.GERMANY. For all country numbers see here: [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_country)
-- @param #boolean Condition Sends the message only if the condition is true.
-- @param Core.Settings#Settings Settings (Optional) Settings for message display.
-- @return #MESSAGE Message object.
function MESSAGE:ToCountryIf( Country, Condition, Settings )
  self:F(Country )
  if Country and Condition == true then
    self:ToCountry( Country, Settings )
  end
  return self
end

--- Sends a MESSAGE to the Blue coalition.
-- @param #MESSAGE self
-- @return #MESSAGE
-- @usage
--
--   -- Send a message created with the @{New} method to the BLUE coalition.
--   MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", 25, "Penalty"):ToBlue()
--   or
--   MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", 25, "Penalty"):ToBlue()
--   or
--   MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", 25, "Penalty")
--   MessageBLUE:ToBlue()
--
function MESSAGE:ToBlue()
  self:F()

  self:ToCoalition( coalition.side.BLUE )

  return self
end

--- Sends a MESSAGE to the Red Coalition.
-- @param #MESSAGE self
-- @return #MESSAGE
-- @usage
--
--   -- Send a message created with the @{New} method to the RED coalition.
--   MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty"):ToRed()
--   or
--   MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty"):ToRed()
--   or
--   MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty")
--   MessageRED:ToRed()
--
function MESSAGE:ToRed()
  self:F()

  self:ToCoalition( coalition.side.RED )

  return self
end

--- Sends a MESSAGE to a Coalition.
-- @param #MESSAGE self
-- @param DCS#coalition.side CoalitionSide @{#DCS.coalition.side} to which the message is displayed.
-- @param Core.Settings#SETTINGS Settings (Optional) Settings for message display.
-- @return #MESSAGE Message object.
-- @usage
--
--   -- Send a message created with the @{New} method to the RED coalition.
--   MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty"):ToCoalition( coalition.side.RED )
--   or
--   MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty"):ToCoalition( coalition.side.RED )
--   or
--   MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", 25, "Penalty")
--   MessageRED:ToCoalition( coalition.side.RED )
--
function MESSAGE:ToCoalition( CoalitionSide, Settings )
  self:F( CoalitionSide )

  if self.MessageType then
    local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS
    self.MessageDuration = Settings:GetMessageTime( self.MessageType )
    self.MessageCategory = "" -- self.MessageType .. ": "
  end

  if CoalitionSide then
    if self.MessageDuration ~= 0 then
      self:T( self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ) .. " / " .. self.MessageDuration )
      trigger.action.outTextForCoalition( CoalitionSide, self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ), self.MessageDuration, self.ClearScreen )
    end
  end
  
  self.CoalitionSide = CoalitionSide
  
  return self
end

--- Sends a MESSAGE to a Coalition if the given Condition is true.
-- @param #MESSAGE self
-- @param CoalitionSide needs to be filled out by the defined structure of the standard scripting engine @{#DCS.coalition.side}.
-- @param #boolean Condition Sends the message only if the condition is true.
-- @return #MESSAGE self
function MESSAGE:ToCoalitionIf( CoalitionSide, Condition )
  self:F( CoalitionSide )

  if Condition and Condition == true then
    self:ToCoalition( CoalitionSide )
  end

  return self
end

--- Sends a MESSAGE to all players. 
-- @param #MESSAGE self
-- @param Core.Settings#Settings Settings (Optional) Settings for message display.
-- @param #number Delay (Optional) Delay in seconds before the message is send. Default instantly (`nil`).
-- @return #MESSAGE self
-- @usage
--
--   -- Send a message created to all players.
--   MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", 25, "End of Mission"):ToAll()
--   or
--   MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", 25, "End of Mission"):ToAll()
--   or
--   MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", 25, "End of Mission")
--   MessageAll:ToAll()
--
function MESSAGE:ToAll( Settings, Delay )
  self:F()

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, MESSAGE.ToAll, self, Settings, 0)
  else

    if self.MessageType then
      local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS
      self.MessageDuration = Settings:GetMessageTime( self.MessageType )
      self.MessageCategory = "" -- self.MessageType .. ": "
    end
  
    if self.MessageDuration ~= 0 then
      self:T( self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ) .. " / " .. self.MessageDuration )
      trigger.action.outText( self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ), self.MessageDuration, self.ClearScreen )
    end
    
  end

  return self
end

--- Sends a MESSAGE to all players if the given Condition is true.
-- @param #MESSAGE self
-- @param #boolean Condition
-- @return #MESSAGE
function MESSAGE:ToAllIf( Condition )

  if Condition and Condition == true then
    self:ToAll()
  end

  return self
end

--- Sends a MESSAGE to DCS log file.
-- @param #MESSAGE self
-- @return #MESSAGE self
function MESSAGE:ToLog()

  env.info(self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ))

  return self
end

--- Sends a MESSAGE to DCS log file if the given Condition is true.
-- @param #MESSAGE self
-- @return #MESSAGE self
function MESSAGE:ToLogIf( Condition )
  
  if Condition and Condition == true then
    env.info(self.MessageCategory .. self.MessageText:gsub( "\n$", "" ):gsub( "\n$", "" ))
  end
  return self
end

_MESSAGESRS = {}

--- Set up MESSAGE generally to allow Text-To-Speech via SRS and TTS functions. `SetMSRS()` will try to use as many attributes configured with @{Sound.SRS#MSRS.LoadConfigFile}() as possible.
-- @param #string PathToSRS (optional) Path to SRS TTS Folder, defaults to "C:\\\\Program Files\\\\DCS-SimpleRadio-Standalone\\ExternalAudio" or your configuration file setting.
-- @param #number Port Port (optional) number of SRS, defaults to 5002 or your configuration file setting.
-- @param #string PathToCredentials (optional) Path to credentials file for Google.
-- @param #number Frequency Frequency in MHz. Can also be given as a #table of frequencies.
-- @param #number Modulation Modulation, i.e. radio.modulation.AM  or radio.modulation.FM. Can also be given as a #table of modulations.
-- @param #string Gender (optional) Gender, i.e. "male" or "female", defaults to "female" or your configuration file setting.
-- @param #string Culture (optional) Culture, e.g. "en-US", defaults to "en-GB" or your configuration file setting.
-- @param #string Voice (optional) Voice. Will override gender and culture settings, e.g. MSRS.Voices.Microsoft.Hazel or MSRS.Voices.Google.Standard.de_DE_Standard_D. Hint on Microsoft voices - working voices are limited to Hedda, Hazel, David, Zira and Hortense. **Must** be installed on your Desktop or Server!
-- @param #number Coalition (optional) Coalition, can be coalition.side.RED, coalition.side.BLUE or coalition.side.NEUTRAL. Defaults to coalition.side.NEUTRAL.
-- @param #number Volume (optional) Volume, can be between 0.0 and 1.0 (loudest).
-- @param #string Label (optional) Label, defaults to "MESSAGE" or the Message Category set.
-- @param Core.Point#COORDINATE Coordinate (optional) Coordinate this messages originates from.
-- @param #string Backend (optional) Backend to be used, can be MSRS.Backend.SRSEXE or MSRS.Backend.GRPC
-- @usage
--          -- Mind the dot here, not using the colon this time around!
--          -- Needed once only
--          MESSAGE.SetMSRS("D:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio",5012,nil,127,radio.modulation.FM,"female","en-US",nil,coalition.side.BLUE)
--          -- later on in your code
--          MESSAGE:New("Test message!",15,"SPAWN"):ToSRS()
--          
function MESSAGE.SetMSRS(PathToSRS,Port,PathToCredentials,Frequency,Modulation,Gender,Culture,Voice,Coalition,Volume,Label,Coordinate,Backend)
  
  _MESSAGESRS.PathToSRS = PathToSRS or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio"
  
  _MESSAGESRS.frequency = Frequency or MSRS.frequencies or 243
  _MESSAGESRS.modulation = Modulation or MSRS.modulations or radio.modulation.AM
  
  _MESSAGESRS.MSRS = MSRS:New(_MESSAGESRS.PathToSRS,_MESSAGESRS.frequency, _MESSAGESRS.modulation)

  _MESSAGESRS.coalition = Coalition or MSRS.coalition or coalition.side.NEUTRAL    
  _MESSAGESRS.MSRS:SetCoalition(_MESSAGESRS.coalition)

  _MESSAGESRS.coordinate = Coordinate
  
  if Coordinate then
    _MESSAGESRS.MSRS:SetCoordinate(Coordinate)
  end
  
  if Backend then
   _MESSAGESRS.MSRS:SetBackend(Backend)
  end
  
  _MESSAGESRS.Culture = Culture or MSRS.culture or "en-GB"
  _MESSAGESRS.MSRS:SetCulture(Culture)

  _MESSAGESRS.Gender = Gender or MSRS.gender or "female"
  _MESSAGESRS.MSRS:SetGender(Gender)

  if PathToCredentials then
    _MESSAGESRS.MSRS:SetProviderOptionsGoogle(PathToCredentials)
    _MESSAGESRS.MSRS:SetProvider(MSRS.Provider.GOOGLE)
  end
  
  _MESSAGESRS.label = Label or MSRS.Label or "MESSAGE"
  _MESSAGESRS.MSRS:SetLabel(_MESSAGESRS.label)

  _MESSAGESRS.port = Port or MSRS.port or 5002
  _MESSAGESRS.MSRS:SetPort(_MESSAGESRS.port)

  _MESSAGESRS.volume = Volume or MSRS.volume or 1
  _MESSAGESRS.MSRS:SetVolume(_MESSAGESRS.volume)
  
  if Voice then _MESSAGESRS.MSRS:SetVoice(Voice) end
  
  _MESSAGESRS.voice = Voice or MSRS.voice --or MSRS.Voices.Microsoft.Hedda
  
  _MESSAGESRS.SRSQ = MSRSQUEUE:New(_MESSAGESRS.label)
end

--- Sends a message via SRS. `ToSRS()` will try to use as many attributes configured with @{Core.Message#MESSAGE.SetMSRS}() and @{Sound.SRS#MSRS.LoadConfigFile}() as possible.
-- @param #MESSAGE self
-- @param #number frequency (optional) Frequency in MHz. Can also be given as a #table of frequencies. Only needed if you want to override defaults set with `MESSAGE.SetMSRS()` for this one setting.
-- @param #number modulation (optional) Modulation, i.e. radio.modulation.AM  or radio.modulation.FM. Can also be given as a #table of modulations. Only needed if you want to override defaults set with `MESSAGE.SetMSRS()` for this one setting.
-- @param #string gender (optional) Gender, i.e. "male" or "female". Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param #string culture (optional) Culture, e.g. "en-US". Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param #string voice (optional) Voice. Will override gender and culture settings. Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param #number coalition (optional) Coalition, can be coalition.side.RED, coalition.side.BLUE or coalition.side.NEUTRAL. Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param #number volume (optional) Volume, can be between 0.0 and 1.0 (loudest). Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param Core.Point#COORDINATE coordinate (optional) Coordinate this messages originates from. Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @return #MESSAGE self
-- @usage
--          -- Mind the dot here, not using the colon this time around!
--          -- Needed once only
--          MESSAGE.SetMSRS("D:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio",5012,nil,127,radio.modulation.FM,"female","en-US",nil,coalition.side.BLUE)
--          -- later on in your code
--          MESSAGE:New("Test message!",15,"SPAWN"):ToSRS()
--          
function MESSAGE:ToSRS(frequency,modulation,gender,culture,voice,coalition,volume,coordinate)
  local tgender = gender or _MESSAGESRS.Gender
  if _MESSAGESRS.SRSQ then
      if voice then
        _MESSAGESRS.MSRS:SetVoice(voice or _MESSAGESRS.voice)
      end
      if coordinate then
        _MESSAGESRS.MSRS:SetCoordinate(coordinate)  
      end
      local category = string.gsub(self.MessageCategory,":","")
      _MESSAGESRS.SRSQ:NewTransmission(self.MessageText,nil,_MESSAGESRS.MSRS,0.5,1,nil,nil,nil,frequency or _MESSAGESRS.frequency,modulation or _MESSAGESRS.modulation, gender or _MESSAGESRS.Gender,culture or _MESSAGESRS.Culture,nil,volume or _MESSAGESRS.volume,category,coordinate or _MESSAGESRS.coordinate)
  end
  return self
end

--- Sends a message via SRS on the blue coalition side.
-- @param #MESSAGE self
-- @param #number frequency (optional) Frequency in MHz. Can also be given as a #table of frequencies. Only needed if you want to override defaults set with `MESSAGE.SetMSRS()` for this one setting.
-- @param #number modulation (optional) Modulation, i.e. radio.modulation.AM  or radio.modulation.FM. Can also be given as a #table of modulations. Only needed if you want to override defaults set with `MESSAGE.SetMSRS()` for this one setting.
-- @param #string gender (optional) Gender, i.e. "male" or "female". Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param #string culture (optional) Culture, e.g. "en-US. Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param #string voice (optional) Voice. Will override gender and culture settings. Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param #number volume (optional) Volume, can be between 0.0 and 1.0 (loudest). Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param Core.Point#COORDINATE coordinate (optional) Coordinate this messages originates from. Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @return #MESSAGE self
-- @usage
--          -- Mind the dot here, not using the colon this time around!
--          -- Needed once only
--          MESSAGE.SetMSRS("D:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio",5012,nil,127,radio.modulation.FM,"female","en-US",nil,coalition.side.BLUE)
--          -- later on in your code
--          MESSAGE:New("Test message!",15,"SPAWN"):ToSRSBlue()
--          
function MESSAGE:ToSRSBlue(frequency,modulation,gender,culture,voice,volume,coordinate)
  self:ToSRS(frequency,modulation,gender,culture,voice,coalition.side.BLUE,volume,coordinate)
  return self
end

--- Sends a message via SRS on the red coalition side.
-- @param #MESSAGE self
-- @param #number frequency (optional) Frequency in MHz. Can also be given as a #table of frequencies. Only needed if you want to override defaults set with `MESSAGE.SetMSRS()` for this one setting.
-- @param #number modulation (optional) Modulation, i.e. radio.modulation.AM  or radio.modulation.FM. Can also be given as a #table of modulations. Only needed if you want to override defaults set with `MESSAGE.SetMSRS()` for this one setting.
-- @param #string gender (optional) Gender, i.e. "male" or "female". Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param #string culture (optional) Culture, e.g. "en-US. Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param #string voice (optional) Voice. Will override gender and culture settings. Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param #number volume (optional) Volume, can be between 0.0 and 1.0 (loudest). Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param Core.Point#COORDINATE coordinate (optional) Coordinate this messages originates from. Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @return #MESSAGE self
-- @usage
--          -- Mind the dot here, not using the colon this time around!
--          -- Needed once only
--          MESSAGE.SetMSRS("D:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio",5012,nil,127,radio.modulation.FM,"female","en-US",nil,coalition.side.RED)
--          -- later on in your code
--          MESSAGE:New("Test message!",15,"SPAWN"):ToSRSRed()
--          
function MESSAGE:ToSRSRed(frequency,modulation,gender,culture,voice,volume,coordinate)
  self:ToSRS(frequency,modulation,gender,culture,voice,coalition.side.RED,volume,coordinate)
  return self
end

--- Sends a message via SRS to all - via the neutral coalition side.
-- @param #MESSAGE self
-- @param #number frequency (optional) Frequency in MHz. Can also be given as a #table of frequencies. Only needed if you want to override defaults set with `MESSAGE.SetMSRS()` for this one setting.
-- @param #number modulation (optional) Modulation, i.e. radio.modulation.AM  or radio.modulation.FM. Can also be given as a #table of modulations. Only needed if you want to override defaults set with `MESSAGE.SetMSRS()` for this one setting.
-- @param #string gender (optional) Gender, i.e. "male" or "female". Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param #string culture (optional) Culture, e.g. "en-US. Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param #string voice (optional) Voice. Will override gender and culture settings. Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param #number volume (optional) Volume, can be between 0.0 and 1.0 (loudest). Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @param Core.Point#COORDINATE coordinate (optional) Coordinate this messages originates from. Only needed if you want to change defaults set with `MESSAGE.SetMSRS()`.
-- @return #MESSAGE self
-- @usage
--          -- Mind the dot here, not using the colon this time around!
--          -- Needed once only
--          MESSAGE.SetMSRS("D:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio",5012,nil,127,radio.modulation.FM,"female","en-US",nil,coalition.side.NEUTRAL)
--          -- later on in your code
--          MESSAGE:New("Test message!",15,"SPAWN"):ToSRSAll()
--          
function MESSAGE:ToSRSAll(frequency,modulation,gender,culture,voice,volume,coordinate)
  self:ToSRS(frequency,modulation,gender,culture,voice,coalition.side.NEUTRAL,volume,coordinate)
  return self
end
--- **Core** - FSM (Finite State Machine) are objects that model and control long lasting business processes and workflow.
--
-- ===
--
-- ## Features:
--
--   * Provide a base class to model your own state machines.
--   * Trigger events synchronously.
--   * Trigger events asynchronously.
--   * Handle events before or after the event was triggered.
--   * Handle state transitions as a result of event before and after the state change.
--   * For internal moose purposes, further state machines have been designed:
--     - to handle controllables (groups and units).
--     - to handle tasks.
--     - to handle processes.
--
-- ===
--
-- A Finite State Machine (FSM) models a process flow that transitions between various **States** through triggered **Events**.
--
-- A FSM can only be in one of a finite number of states.
-- The machine is in only one state at a time; the state it is in at any given time is called the **current state**.
-- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**.
-- A **FSM implementation** is defined by **a list of its states**, **its initial state**, and **the triggering events** for **each possible transition**.
-- A FSM implementation is composed out of **two parts**, a set of **state transition rules**, and an implementation set of **state transition handlers**, implementing those transitions.
--
-- The FSM class supports a **hierarchical implementation of a Finite State Machine**,
-- that is, it allows to **embed existing FSM implementations in a master FSM**.
-- FSM hierarchies allow for efficient FSM re-use, **not having to re-invent the wheel every time again** when designing complex processes.
--
-- ![Workflow Example](..\Presentations\FSM\Dia2.JPG)
--
-- The above diagram shows a graphical representation of a FSM implementation for a **Task**, which guides a Human towards a Zone,
-- orders him to destroy x targets and account the results.
-- Other examples of ready made FSM could be:
--
--   * Route a plane to a zone flown by a human.
--   * Detect targets by an AI and report to humans.
--   * Account for destroyed targets by human players.
--   * Handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle.
--   * Let an AI patrol a zone.
--
-- The **MOOSE framework** extensively uses the FSM class and derived FSM\_ classes,
-- because **the goal of MOOSE is to simplify mission design complexity for mission building**.
-- By efficiently utilizing the FSM class and derived classes, MOOSE allows mission designers to quickly build processes.
-- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used,
-- and tailored** by mission designers through **the implementation of Transition Handlers**.
-- Each of these FSM implementation classes start either with:
--
--   * an acronym **AI\_**, which indicates a FSM implementation directing **AI controlled** @{Wrapper.Group#GROUP} and/or @{Wrapper.Unit#UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class.
--   * an acronym **TASK\_**, which indicates a FSM implementation executing a @{Tasking.Task#TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class.
--   * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{Tasking.Task#TASK}, seated in a @{Wrapper.Client#CLIENT} (slot) or a @{Wrapper.Unit#UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class.
--
-- Detailed explanations and API specifics are further below clarified and FSM derived class specifics are described in those class documentation sections.
--
-- ##__Disclaimer:__
-- The FSM class development is based on a finite state machine implementation made by Conroy Kyle.
-- The state machine can be found on [github](https://github.com/kyleconroy/lua-state-machine)
-- I've reworked this development (taken the concept), and created a **hierarchical state machine** out of it, embedded within the DCS simulator.
-- Additionally, I've added extendability and created an API that allows seamless FSM implementation.
--
-- The following derived classes are available in the MOOSE framework, that implement a specialized form of a FSM:
--
--   * @{#FSM_TASK}: Models Finite State Machines for @{Tasking.Task}s.
--   * @{#FSM_PROCESS}: Models Finite State Machines for @{Tasking.Task} actions, which control @{Wrapper.Client}s.
--   * @{#FSM_CONTROLLABLE}: Models Finite State Machines for @{Wrapper.Controllable}s, which are @{Wrapper.Group}s, @{Wrapper.Unit}s, @{Wrapper.Client}s.
--   * @{#FSM_SET}: Models Finite State Machines for @{Core.Set}s. Note that these FSMs control multiple objects!!! So State concerns here
--     for multiple objects or the position of the state machine in the process.
--
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions: **funkyfranky**
--
-- ===
--
-- @module Core.Fsm
-- @image Core_Finite_State_Machine.JPG

do -- FSM

  -- @type FSM
  -- @field #string ClassName Name of the class.
  -- @field Core.Scheduler#SCHEDULER CallScheduler Call scheduler.
  -- @field #table options Options.
  -- @field #table subs Subs.
  -- @field #table Scores Scores.
  -- @field #string current Current state name.
  -- @extends Core.Base#BASE

  --- A Finite State Machine (FSM) models a process flow that transitions between various **States** through triggered **Events**.
  --
  -- A FSM can only be in one of a finite number of states. 
  -- The machine is in only one state at a time; the state it is in at any given time is called the **current state**.
  -- It can change from one state to another when initiated by an **__internal__ or __external__ triggering event**, which is called a **transition**.
  -- An **FSM implementation** is defined by **a list of its states**, **its initial state**, and **the triggering events** for **each possible transition**.
  -- An FSM implementation is composed out of **two parts**, a set of **state transition rules**, and an implementation set of **state transition handlers**, implementing those transitions.
  --
  -- The FSM class supports a **hierarchical implementation of a Finite State Machine**,
  -- that is, it allows to **embed existing FSM implementations in a master FSM**.
  -- FSM hierarchies allow for efficient FSM re-use, **not having to re-invent the wheel every time again** when designing complex processes.
  --
  -- ![Workflow Example](..\Presentations\FSM\Dia2.JPG)
  --
  -- The above diagram shows a graphical representation of a FSM implementation for a **Task**, which guides a Human towards a Zone,
  -- orders him to destroy x targets and account the results.
  -- Other examples of ready made FSM could be:
  --
  --   * route a plane to a zone flown by a human
  --   * detect targets by an AI and report to humans
  --   * account for destroyed targets by human players
  --   * handle AI infantry to deploy from or embark to a helicopter or airplane or vehicle
  --   * let an AI patrol a zone
  --
  -- The **MOOSE framework** uses extensively the FSM class and derived FSM\_ classes,
  -- because **the goal of MOOSE is to simplify mission design complexity for mission building**.
  -- By efficiently utilizing the FSM class and derived classes, MOOSE allows mission designers to quickly build processes.
  -- **Ready made FSM-based implementations classes** exist within the MOOSE framework that **can easily be re-used,
  -- and tailored** by mission designers through **the implementation of Transition Handlers**.
  -- Each of these FSM implementation classes start either with:
  --
  --   * an acronym **AI\_**, which indicates an FSM implementation directing **AI controlled** @{Wrapper.Group#GROUP} and/or @{Wrapper.Unit#UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class.
  --   * an acronym **TASK\_**, which indicates an FSM implementation executing a @{Tasking.Task#TASK} executed by Groups of players. These TASK\_ classes derive the @{#FSM_TASK} class.
  --   * an acronym **ACT\_**, which indicates an Sub-FSM implementation, directing **Humans actions** that need to be done in a @{Tasking.Task#TASK}, seated in a @{Wrapper.Client#CLIENT} (slot) or a @{Wrapper.Unit#UNIT} (CA join). These ACT\_ classes derive the @{#FSM_PROCESS} class.
  --
  -- ![Transition Rules and Transition Handlers and Event Triggers](..\Presentations\FSM\Dia3.JPG)
  --
  -- The FSM class is the base class of all FSM\_ derived classes. It implements the main functionality to define and execute Finite State Machines.
  -- The derived FSM\_ classes extend the Finite State Machine functionality to run a workflow process for a specific purpose or component.
  --
  -- Finite State Machines have **Transition Rules**, **Transition Handlers** and **Event Triggers**.
  --
  -- The **Transition Rules** define the "Process Flow Boundaries", that is,
  -- the path that can be followed hopping from state to state upon triggered events.
  -- If an event is triggered, and there is no valid path found for that event,
  -- an error will be raised and the FSM will stop functioning.
  --
  -- The **Transition Handlers** are special methods that can be defined by the mission designer, following a defined syntax.
  -- If the FSM object finds a method of such a handler, then the method will be called by the FSM, passing specific parameters.
  -- The method can then define its own custom logic to implement the FSM workflow, and to conduct other actions.
  --
  -- The **Event Triggers** are methods that are defined by the FSM, which the mission designer can use to implement the workflow.
  -- Most of the time, these Event Triggers are used within the Transition Handler methods, so that a workflow is created running through the state machine.
  --
  -- As explained above, a FSM supports **Linear State Transitions** and **Hierarchical State Transitions**, and both can be mixed to make a comprehensive FSM implementation.
  -- The below documentation has a separate chapter explaining both transition modes, taking into account the **Transition Rules**, **Transition Handlers** and **Event Triggers**.
  --
  -- ## FSM Linear Transitions
  --
  -- Linear Transitions are Transition Rules allowing an FSM to transition from one or multiple possible **From** state(s) towards a **To** state upon a Triggered **Event**.
  -- The Linear transition rule evaluation will always be done from the **current state** of the FSM.
  -- If no valid Transition Rule can be found in the FSM, the FSM will log an error and stop.
  --
  -- ### FSM Transition Rules
  --
  -- The FSM has transition rules that it follows and validates, as it walks the process.
  -- These rules define when an FSM can transition from a specific state towards an other specific state upon a triggered event.
  --
  -- The method @{#FSM.AddTransition}() specifies a new possible Transition Rule for the FSM.
  --
  -- The initial state can be defined using the method @{#FSM.SetStartState}(). The default start state of an FSM is "None".
  --
  -- Find below an example of a Linear Transition Rule definition for an FSM.
  --
  --      local Fsm3Switch = FSM:New() -- #FsmDemo
  --      FsmSwitch:SetStartState( "Off" )
  --      FsmSwitch:AddTransition( "Off", "SwitchOn", "On" )
  --      FsmSwitch:AddTransition( "Off", "SwitchMiddle", "Middle" )
  --      FsmSwitch:AddTransition( "On", "SwitchOff", "Off" )
  --      FsmSwitch:AddTransition( "Middle", "SwitchOff", "Off" )
  --
  -- The above code snippet models a 3-way switch Linear Transition:
  --
  --    * It can be switched **On** by triggering event **SwitchOn**.
  --    * It can be switched to the **Middle** position, by triggering event **SwitchMiddle**.
  --    * It can be switched **Off** by triggering event **SwitchOff**.
  --    * Note that once the Switch is **On** or **Middle**, it can only be switched **Off**.
  --
  -- #### Some additional comments:
  --
  -- Note that Linear Transition Rules **can be declared in a few variations**:
  --
  --    * The From states can be **a table of strings**, indicating that the transition rule will be valid **if the current state** of the FSM will be **one of the given From states**.
  --    * The From state can be a **"*"**, indicating that **the transition rule will always be valid**, regardless of the current state of the FSM.
  --
  -- The below code snippet shows how the two last lines can be rewritten and condensed.
  --
  --      FsmSwitch:AddTransition( { "On",  "Middle" }, "SwitchOff", "Off" )
  --
  -- ### Transition Handling
  --
  -- ![Transition Handlers](..\Presentations\FSM\Dia4.JPG)
  --
  -- An FSM transitions in **4 moments** when an Event is being triggered and processed.
  -- The mission designer can define for each moment specific logic within methods implementations following a defined API syntax.
  -- These methods define the flow of the FSM process; because in those methods the FSM Internal Events will be triggered.
  --
  --    * To handle **State** transition moments, create methods starting with OnLeave or OnEnter concatenated with the State name.
  --    * To handle **Event** transition moments, create methods starting with OnBefore or OnAfter concatenated with the Event name.
  --
  -- **The OnLeave and OnBefore transition methods may return false, which will cancel the transition!**
  --
  -- Transition Handler methods need to follow the above specified naming convention, but are also passed parameters from the FSM.
  -- These parameters are on the correct order: From, Event, To:
  --
  --    * From = A string containing the From state.
  --    * Event = A string containing the Event name that was triggered.
  --    * To = A string containing the To state.
  --
  -- On top, each of these methods can have a variable amount of parameters passed. See the example in section [1.1.3](#1.1.3\)-event-triggers).
  --
  -- ### Event Triggers
  --
  -- ![Event Triggers](..\Presentations\FSM\Dia5.JPG)
  --
  -- The FSM creates for each Event two **Event Trigger methods**.
  -- There are two modes how Events can be triggered, which is **synchronous** and **asynchronous**:
  --
  --    * The method **FSM:Event()** triggers an Event that will be processed **synchronously** or **immediately**.
  --    * The method **FSM:__Event( __seconds__ )** triggers an Event that will be processed **asynchronously** over time, waiting __x seconds__.
  --
  -- The distinction between these 2 Event Trigger methods are important to understand. An asynchronous call will "log" the Event Trigger to be executed at a later time.
  -- Processing will just continue. Synchronous Event Trigger methods are useful to change states of the FSM immediately, but may have a larger processing impact.
  --
  -- The following example provides a little demonstration on the difference between synchronous and asynchronous Event Triggering.
  --
  --       function FSM:OnAfterEvent( From, Event, To, Amount )
  --         self:T( { Amount = Amount } )
  --       end
  --
  --       local Amount = 1
  --       FSM:__Event( 5, Amount )
  --
  --       Amount = Amount + 1
  --       FSM:Event( Text, Amount )
  --
  -- In this example, the **:OnAfterEvent**() Transition Handler implementation will get called when **Event** is being triggered.
  -- Before we go into more detail, let's look at the last 4 lines of the example.
  -- The last line triggers synchronously the **Event**, and passes Amount as a parameter.
  -- The 3rd last line of the example triggers asynchronously **Event**.
  -- Event will be processed after 5 seconds, and Amount is given as a parameter.
  --
  -- The output of this little code fragment will be:
  --
  --    * Amount = 2
  --    * Amount = 2
  --
  -- Because ... When Event was asynchronously processed after 5 seconds, Amount was set to 2. So be careful when processing and passing values and objects in asynchronous processing!
  --
  -- ### Linear Transition Example
  --
  -- This example is fully implemented in the MOOSE test mission on GitHub: [FSM-100 - Transition Explanation](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Core/FSM/FSM-100%20-%20Transition%20Explanation)
  --
  -- It models a unit standing still near Batumi, and flaring every 5 seconds while switching between a Green flare and a Red flare.
  -- The purpose of this example is not to show how exciting flaring is, but it demonstrates how a Linear Transition FSM can be build.
  -- Have a look at the source code. The source code is also further explained below in this section.
  --
  -- The example creates a new FsmDemo object from class FSM.
  -- It will set the start state of FsmDemo to state **Green**.
  -- Two Linear Transition Rules are created, where upon the event **Switch**,
  -- the FsmDemo will transition from state **Green** to **Red** and from **Red** back to **Green**.
  --
  -- ![Transition Example](..\Presentations\FSM\Dia6.JPG)
  --
  --      local FsmDemo = FSM:New() -- #FsmDemo
  --      FsmDemo:SetStartState( "Green" )
  --      FsmDemo:AddTransition( "Green", "Switch", "Red" )
  --      FsmDemo:AddTransition( "Red", "Switch", "Green" )
  --
  -- In the above example, the FsmDemo could flare every 5 seconds a Green or a Red flare into the air.
  -- The next code implements this through the event handling method **OnAfterSwitch**.
  --
  -- ![Transition Flow](..\Presentations\FSM\Dia7.JPG)
  --
  --      function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit )
  --        self:T( { From, Event, To, FsmUnit } )
  --
  --        if From == "Green" then
  --          FsmUnit:Flare(FLARECOLOR.Green)
  --        else
  --          if From == "Red" then
  --            FsmUnit:Flare(FLARECOLOR.Red)
  --          end
  --        end
  --        self:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds.
  --      end
  --
  --      FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the first Switch event to happen in 5 seconds.
  --
  -- The OnAfterSwitch implements a loop. The last line of the code fragment triggers the Switch Event within 5 seconds.
  -- Upon the event execution (after 5 seconds), the OnAfterSwitch method is called of FsmDemo (cfr. the double point notation!!! ":").
  -- The OnAfterSwitch method receives from the FSM the 3 transition parameter details ( From, Event, To ),
  -- and one additional parameter that was given when the event was triggered, which is in this case the Unit that is used within OnSwitchAfter.
  --
  --      function FsmDemo:OnAfterSwitch( From, Event, To, FsmUnit )
  --
  -- For debugging reasons the received parameters are traced within the DCS.log.
  --
  --         self:T( { From, Event, To, FsmUnit } )
  --
  -- The method will check if the From state received is either "Green" or "Red" and will flare the respective color from the FsmUnit.
  --
  --        if From == "Green" then
  --          FsmUnit:Flare(FLARECOLOR.Green)
  --        else
  --          if From == "Red" then
  --            FsmUnit:Flare(FLARECOLOR.Red)
  --          end
  --        end
  --
  -- It is important that the Switch event is again triggered, otherwise, the FsmDemo would stop working after having the first Event being handled.
  --
  --        FsmDemo:__Switch( 5, FsmUnit ) -- Trigger the next Switch event to happen in 5 seconds.
  --
  -- The below code fragment extends the FsmDemo, demonstrating multiple **From states declared as a table**, adding a **Linear Transition Rule**.
  -- The new event **Stop** will cancel the Switching process.
  -- The transition for event Stop can be executed if the current state of the FSM is either "Red" or "Green".
  --
  --      local FsmDemo = FSM:New() -- #FsmDemo
  --      FsmDemo:SetStartState( "Green" )
  --      FsmDemo:AddTransition( "Green", "Switch", "Red" )
  --      FsmDemo:AddTransition( "Red", "Switch", "Green" )
  --      FsmDemo:AddTransition( { "Red", "Green" }, "Stop", "Stopped" )
  --
  -- The transition for event Stop can also be simplified, as any current state of the FSM is valid.
  --
  --      FsmDemo:AddTransition( "*", "Stop", "Stopped" )
  --
  -- So... When FsmDemo:Stop() is being triggered, the state of FsmDemo will transition from Red or Green to Stopped.
  -- And there is no transition handling method defined for that transition, thus, no new event is being triggered causing the FsmDemo process flow to halt.
  --
  -- ## FSM Hierarchical Transitions
  --
  -- Hierarchical Transitions allow to re-use readily available and implemented FSMs.
  -- This becomes in very useful for mission building, where mission designers build complex processes and workflows,
  -- combining smaller FSMs to one single FSM.
  --
  -- The FSM can embed **Sub-FSMs** that will execute and return **multiple possible Return (End) States**.
  -- Depending upon **which state is returned**, the main FSM can continue the flow **triggering specific events**.
  --
  -- The method @{#FSM.AddProcess}() adds a new Sub-FSM to the FSM.
  --
  -- ===
  --
  -- @field #FSM
  FSM = {
    ClassName = "FSM",
  }

  --- Creates a new FSM object.
  -- @param #FSM self
  -- @return #FSM
  function FSM:New()

    -- Inherits from BASE
    self = BASE:Inherit( self, BASE:New() )

    self.options = options or {}
    self.options.subs = self.options.subs or {}
    self.current = self.options.initial or 'none'
    self.Events = {}
    self.subs = {}
    self.endstates = {}

    self.Scores = {}

    self._StartState = "none"
    self._Transitions = {}
    self._Processes = {}
    self._EndStates = {}
    self._Scores = {}
    self._EventSchedules = {}

    self.CallScheduler = SCHEDULER:New( self )

    return self
  end

  --- Sets the start state of the FSM.
  -- @param #FSM self
  -- @param #string State A string defining the start state.
  function FSM:SetStartState( State )
    self._StartState = State
    self.current = State
  end

  --- Returns the start state of the FSM.
  -- @param #FSM self
  -- @return #string A string containing the start state.
  function FSM:GetStartState()
    return self._StartState or {}
  end

  --- Add a new transition rule to the FSM.
  -- A transition rule defines when and if the FSM can transition from a state towards another state upon a triggered event.
  -- @param #FSM self
  -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states.
  -- @param #string Event The Event name.
  -- @param #string To The To state.
  function FSM:AddTransition( From, Event, To )

    local Transition = {}
    Transition.From = From
    Transition.Event = Event
    Transition.To = To

    -- Debug message.
    --self:T3( Transition )
    
    self._Transitions[Transition] = Transition
    self:_eventmap( self.Events, Transition )
  end

  --- Returns a table of the transition rules defined within the FSM.
  -- @param #FSM self
  -- @return #table Transitions.
  function FSM:GetTransitions()
    return self._Transitions or {}
  end

  --- Set the default @{#FSM_PROCESS} template with key ProcessName providing the ProcessClass and the process object when it is assigned to a @{Wrapper.Controllable} by the task.
  -- @param #FSM self
  -- @param #table From Can contain a string indicating the From state or a table of strings containing multiple From states.
  -- @param #string Event The Event name.
  -- @param Core.Fsm#FSM_PROCESS Process An sub-process FSM.
  -- @param #table ReturnEvents A table indicating for which returned events of the SubFSM which Event must be triggered in the FSM.
  -- @return Core.Fsm#FSM_PROCESS The SubFSM.
  function FSM:AddProcess( From, Event, Process, ReturnEvents )
    --self:T3( { From, Event } )
  
    local Sub = {}
    Sub.From = From
    Sub.Event = Event
    Sub.fsm = Process
    Sub.StartEvent = "Start"
    Sub.ReturnEvents = ReturnEvents

    self._Processes[Sub] = Sub

    self:_submap( self.subs, Sub, nil )

    self:AddTransition( From, Event, From )

    return Process
  end

  --- Returns a table of the SubFSM rules defined within the FSM.
  -- @param #FSM self
  -- @return #table Sub processes.
  function FSM:GetProcesses()

    self:F( { Processes = self._Processes } )

    return self._Processes or {}
  end

  function FSM:GetProcess( From, Event )

    for ProcessID, Process in pairs( self:GetProcesses() ) do
      if Process.From == From and Process.Event == Event then
        return Process.fsm
      end
    end

    error( "Sub-Process from state " .. From .. " with event " .. Event .. " not found!" )
  end

  function FSM:SetProcess( From, Event, Fsm )

    for ProcessID, Process in pairs( self:GetProcesses() ) do
      if Process.From == From and Process.Event == Event then
        Process.fsm = Fsm
        return true
      end
    end

    error( "Sub-Process from state " .. From .. " with event " .. Event .. " not found!" )
  end

  --- Adds an End state.
  -- @param #FSM self
  -- @param #string State The FSM state.
  function FSM:AddEndState( State )
    self._EndStates[State] = State
    self.endstates[State] = State
  end

  --- Returns the End states.
  -- @param #FSM self
  -- @return #table End states.
  function FSM:GetEndStates()
    return self._EndStates or {}
  end

  --- Adds a score for the FSM to be achieved.
  -- @param #FSM self
  -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process).
  -- @param #string ScoreText is a text describing the score that is given according the status.
  -- @param #number Score is a number providing the score of the status.
  -- @return #FSM self
  function FSM:AddScore( State, ScoreText, Score )
    self:F( { State, ScoreText, Score } )

    self._Scores[State] = self._Scores[State] or {}
    self._Scores[State].ScoreText = ScoreText
    self._Scores[State].Score = Score

    return self
  end

  --- Adds a score for the FSM_PROCESS to be achieved.
  -- @param #FSM self
  -- @param #string From is the From State of the main process.
  -- @param #string Event is the Event of the main process.
  -- @param #string State is the state of the process when the score needs to be given. (See the relevant state descriptions of the process).
  -- @param #string ScoreText is a text describing the score that is given according the status.
  -- @param #number Score is a number providing the score of the status.
  -- @return #FSM self
  function FSM:AddScoreProcess( From, Event, State, ScoreText, Score )
    self:F( { From, Event, State, ScoreText, Score } )

    local Process = self:GetProcess( From, Event )

    Process._Scores[State] = Process._Scores[State] or {}
    Process._Scores[State].ScoreText = ScoreText
    Process._Scores[State].Score = Score
    
    --self:T3( Process._Scores )
  
    return Process
  end

  --- Returns a table with the scores defined.
  -- @param #FSM self
  -- @return #table Scores.
  function FSM:GetScores()
    return self._Scores or {}
  end

  --- Returns a table with the Subs defined.
  -- @param #FSM self
  -- @return #table Sub processes.
  function FSM:GetSubs()
    return self.options.subs
  end

  --- Load call backs.
  -- @param #FSM self
  -- @param #table CallBackTable Table of call backs.
  function FSM:LoadCallBacks( CallBackTable )

    for name, callback in pairs( CallBackTable or {} ) do
      self[name] = callback
    end

  end

  --- Event map.
  -- @param #FSM self
  -- @param #table Events Events.
  -- @param #table EventStructure Event structure.
  function FSM:_eventmap( Events, EventStructure )
  
      local Event = EventStructure.Event
      local __Event = "__" .. EventStructure.Event
      
      self[Event] = self[Event] or self:_create_transition(Event)
      self[__Event] = self[__Event] or self:_delayed_transition(Event)
      
      -- Debug message.
      --self:T3( "Added methods: " .. Event .. ", " .. __Event )
      
      Events[Event] = self.Events[Event] or { map = {} }
      self:_add_to_map( Events[Event].map, EventStructure )
  
  end

  --- Sub maps.
  -- @param #FSM self
  -- @param #table subs Subs.
  -- @param #table sub Sub.
  -- @param #string name Name.
  function FSM:_submap( subs, sub, name )

    subs[sub.From] = subs[sub.From] or {}
    subs[sub.From][sub.Event] = subs[sub.From][sub.Event] or {}

    -- Make the reference table weak.
    -- setmetatable( subs[sub.From][sub.Event], { __mode = "k" } )

    subs[sub.From][sub.Event][sub] = {}
    subs[sub.From][sub.Event][sub].fsm = sub.fsm
    subs[sub.From][sub.Event][sub].StartEvent = sub.StartEvent
    subs[sub.From][sub.Event][sub].ReturnEvents = sub.ReturnEvents or {} -- these events need to be given to find the correct continue event ... if none given, the processing will stop.
    subs[sub.From][sub.Event][sub].name = name
    subs[sub.From][sub.Event][sub].fsmparent = self

  end

  --- Call handler.
  -- @param #FSM self
  -- @param #string step Step "onafter", "onbefore", "onenter", "onleave".
  -- @param #string trigger Trigger.
  -- @param #table params Parameters.
  -- @param #string EventName Event name.
  -- @return Value.
  function FSM:_call_handler( step, trigger, params, EventName )
    -- env.info(string.format("FF T=%.3f _call_handler step=%s, trigger=%s, event=%s", timer.getTime(), step, trigger, EventName))

    local handler = step .. trigger

    if self[handler] then

      --[[
      if step == "onafter" or step == "OnAfter" then
        self:T( ":::>" .. step .. params[2] .. " : " .. params[1] .. " >> " .. params[2] .. ">" .. step .. params[2] .. "()" .. " >> " .. params[3] )
      elseif step == "onbefore" or step == "OnBefore" then
        self:T( ":::>" .. step .. params[2] .. " : " .. params[1] .. " >> " .. step .. params[2] .. "()" .. ">" .. params[2] .. " >> " .. params[3] )
      elseif step == "onenter" or step == "OnEnter" then
        self:T( ":::>" .. step .. params[3] .. " : " .. params[1] .. " >> " .. params[2] .. " >> "  .. step .. params[3] .. "()" .. ">" .. params[3] )
      elseif step == "onleave" or step == "OnLeave" then
        self:T( ":::>" .. step .. params[1] .. " : " .. params[1] .. ">" .. step .. params[1] .. "()" .. " >> " .. params[2] .. " >> " .. params[3] )
      else
        self:T( ":::>" .. step .. " : " .. params[1] .. " >> " .. params[2] .. " >> " .. params[3] )
      end
      ]]

      self._EventSchedules[EventName] = nil

      -- Error handler.
      local ErrorHandler = function( errmsg )
        env.info( "Error in SCHEDULER function:" .. errmsg )
        if BASE.Debug ~= nil then
          env.info( BASE.Debug.traceback() )
        end
        return errmsg
      end

      -- return self[handler](self, unpack( params ))

      -- Protected call.
      local Result, Value = xpcall( function()
        return self[handler]( self, unpack( params ) )
      end, ErrorHandler )
      return Value
    end

  end

  --- Handler.
  -- @param #FSM self
  -- @param #string EventName Event name.
  -- @param ... Arguments.
  function FSM._handler( self, EventName, ... )

    local Can, To = self:can( EventName )

    if To == "*" then
      To = self.current
    end

    if Can then

      -- From state.
      local From = self.current

      -- Parameters.
      local Params = { From, EventName, To, ... }

      if self["onleave" .. From] or
         self["OnLeave" .. From] or
         self["onbefore" .. EventName] or
         self["OnBefore" .. EventName] or
         self["onafter" .. EventName] or
         self["OnAfter" .. EventName] or
         self["onenter" .. To] or
         self["OnEnter" .. To] then

        if self:_call_handler( "onbefore", EventName, Params, EventName ) == false then
          self:T( "*** FSM ***    Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** onbefore" .. EventName )
          return false
        else
          if self:_call_handler( "OnBefore", EventName, Params, EventName ) == false then
            self:T( "*** FSM ***    Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** OnBefore" .. EventName )
            return false
          else
            if self:_call_handler( "onleave", From, Params, EventName ) == false then
              self:T( "*** FSM ***    Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** onleave" .. From )
              return false
            else
              if self:_call_handler( "OnLeave", From, Params, EventName ) == false then
                self:T( "*** FSM ***    Cancel" .. " *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** OnLeave" .. From )
                return false
              end
            end
          end
        end

      else

        local ClassName = self:GetClassName()

        if ClassName == "FSM" then
          self:T( "*** FSM ***    Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To )
        end

        if ClassName == "FSM_TASK" then
          self:T( "*** FSM ***    Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** Task: " .. self.TaskName )
        end

        if ClassName == "FSM_CONTROLLABLE" then
          self:T( "*** FSM ***    Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** TaskUnit: " .. self.Controllable.ControllableName .. " *** " )
        end

        if ClassName == "FSM_PROCESS" then
          self:T( "*** FSM ***    Transit *** " .. self.current .. " --> " .. EventName .. " --> " .. To .. " *** Task: " .. self.Task:GetName() .. ", TaskUnit: " .. self.Controllable.ControllableName .. " *** " )
        end
      end

      -- New current state.
      self.current = To

      local execute = true

      local subtable = self:_gosub( From, EventName )

      for _, sub in pairs( subtable ) do

        -- if sub.nextevent then
        --  self:F2( "nextevent = " .. sub.nextevent )
        --  self[sub.nextevent]( self )
        -- end

        self:T( "*** FSM ***    Sub *** " .. sub.StartEvent )

        sub.fsm.fsmparent = self
        sub.fsm.ReturnEvents = sub.ReturnEvents
        sub.fsm[sub.StartEvent]( sub.fsm )

        execute = false
      end

      local fsmparent, Event = self:_isendstate( To )

      if fsmparent and Event then

        self:T( "*** FSM ***    End *** " .. Event )

        self:_call_handler( "onenter", To, Params, EventName )
        self:_call_handler( "OnEnter", To, Params, EventName )
        self:_call_handler( "onafter", EventName, Params, EventName )
        self:_call_handler( "OnAfter", EventName, Params, EventName )
        self:_call_handler( "onstate", "change", Params, EventName )

        fsmparent[Event]( fsmparent )

        execute = false
      end

      if execute then

        self:_call_handler( "onafter", EventName, Params, EventName )
        self:_call_handler( "OnAfter", EventName, Params, EventName )

        self:_call_handler( "onenter", To, Params, EventName )
        self:_call_handler( "OnEnter", To, Params, EventName )

        self:_call_handler( "onstate", "change", Params, EventName )

      end
    else
      self:T( "*** FSM *** NO Transition *** " .. self.current .. " --> " .. EventName .. " -->  ? " )
    end

    return nil
  end

  --- Delayed transition.
  -- @param #FSM self
  -- @param #string EventName Event name.
  -- @return #function Function.
  function FSM:_delayed_transition( EventName )

    return function( self, DelaySeconds, ... )

      -- Debug.
      self:T3( "Delayed Event: " .. EventName )
      
      local CallID = 0
      if DelaySeconds ~= nil then

        if DelaySeconds < 0 then -- Only call the event ONCE!

          DelaySeconds = math.abs( DelaySeconds )

          if not self._EventSchedules[EventName] then

            -- Call _handler.
            CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true )

            -- Set call ID.
            self._EventSchedules[EventName] = CallID

            -- Debug output.
            self:T2(string.format("NEGATIVE Event %s delayed by %.3f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID)))
          else
            self:T2(string.format("NEGATIVE Event %s delayed by %.3f sec CANCELLED as we already have such an event in the queue.", EventName, DelaySeconds))
            -- reschedule
          end
        else

          CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1, nil, nil, nil, 4, true )
          
          self:T2(string.format("Event %s delayed by %.3f sec SCHEDULED with CallID=%s", EventName, DelaySeconds, tostring(CallID)))
        end
      else
        error( "FSM: An asynchronous event trigger requires a DelaySeconds parameter!!! This can be positive or negative! Sorry, but will not process this." )
      end

      -- Debug.
      --self:T3( { CallID = CallID } )
    end

  end

  --- Create transition.
  -- @param #FSM self
  -- @param #string EventName Event name.
  -- @return #function Function.
  function FSM:_create_transition( EventName )
    return function( self, ... )
      return self._handler( self, EventName, ... )
    end
  end

  --- Go sub.
  -- @param #FSM self
  -- @param #string ParentFrom Parent from state.
  -- @param #string ParentEvent Parent event name.
  -- @return #table Subs.
  function FSM:_gosub( ParentFrom, ParentEvent )
    local fsmtable = {}
    if self.subs[ParentFrom] and self.subs[ParentFrom][ParentEvent] then
      --self:T3( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } )
      return self.subs[ParentFrom][ParentEvent]
    else
      return {}
    end
  end

  --- Is end state.
  -- @param #FSM self
  -- @param #string Current Current state name.
  -- @return #table FSM parent.
  -- @return #string Event name.
  function FSM:_isendstate( Current )
    local FSMParent = self.fsmparent

    if FSMParent and self.endstates[Current] then
      -- self:T( { state = Current, endstates = self.endstates, endstate = self.endstates[Current] } )
      FSMParent.current = Current
      local ParentFrom = FSMParent.current
      -- self:T( { ParentFrom, self.ReturnEvents } )
      local Event = self.ReturnEvents[Current]
      -- self:T( { Event } )
      if Event then
        return FSMParent, Event
      else
        -- self:T( { "Could not find parent event name for state ", ParentFrom } )
      end
    end

    return nil
  end

  --- Add to map.
  -- @param #FSM self
  -- @param #table Map Map.
  -- @param #table Event Event table.
  function FSM:_add_to_map( Map, Event )
    self:F3( { Map, Event } )

    if type( Event.From ) == 'string' then
      Map[Event.From] = Event.To
    else
      for _, From in ipairs( Event.From ) do
        Map[From] = Event.To
      end
    end
    
    --self:T3( {  Map, Event } )
  end

  --- Get current state.
  -- @param #FSM self
  -- @return #string Current FSM state.
  function FSM:GetState()
    return self.current
  end

  --- Get current state.
  -- @param #FSM self
  -- @return #string Current FSM state.
  function FSM:GetCurrentState()
    return self.current
  end

  --- Check if FSM is in state.
  -- @param #FSM self
  -- @param #string State State name.
  -- @return #boolean If true, FSM is in this state.
  function FSM:Is( State )
    return self.current == State
  end

  --- Check if FSM is in state.
  -- @param #FSM self
  -- @param #string State State name.
  -- @return #boolean If true, FSM is in this state.  
  function FSM:is(state)
    return self.current == state
  end

  --- Check if can do an event.
  -- @param #FSM self
  -- @param #string e Event name.
  -- @return #boolean If true, FSM can do the event.
  -- @return #string To state.
  function FSM:can( e )

    local Event = self.Events[e]

    -- self:F3( { self.current, Event } )

    local To = Event and Event.map[self.current] or Event.map['*']

    return To ~= nil, To
  end

  --- Check if cannot do an event.
  -- @param #FSM self
  -- @param #string e Event name.
  -- @return #boolean If true, FSM cannot do the event.
  function FSM:cannot( e )
    return not self:can( e )
  end

end

do -- FSM_CONTROLLABLE
  
  ---
  -- @type FSM_CONTROLLABLE
  -- @field Wrapper.Controllable#CONTROLLABLE Controllable
  -- @extends Core.Fsm#FSM

  --- Models Finite State Machines for @{Wrapper.Controllable}s, which are @{Wrapper.Group}s, @{Wrapper.Unit}s, @{Wrapper.Client}s.
  --
  -- ===
  --
  -- @field #FSM_CONTROLLABLE
  FSM_CONTROLLABLE = {
    ClassName = "FSM_CONTROLLABLE",
  }

  --- Creates a new FSM_CONTROLLABLE object.
  -- @param #FSM_CONTROLLABLE self
  -- @param #table FSMT Finite State Machine Table
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable (optional) The CONTROLLABLE object that the FSM_CONTROLLABLE governs.
  -- @return #FSM_CONTROLLABLE
  function FSM_CONTROLLABLE:New( Controllable )

    -- Inherits from BASE
    local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_CONTROLLABLE

    if Controllable then
      self:SetControllable( Controllable )
    end

    self:AddTransition( "*", "Stop", "Stopped" )

    --- OnBefore Transition Handler for Event Stop.
    -- @function [parent=#FSM_CONTROLLABLE] OnBeforeStop
    -- @param #FSM_CONTROLLABLE self
    -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @return #boolean Return false to cancel Transition.

    --- OnAfter Transition Handler for Event Stop.
    -- @function [parent=#FSM_CONTROLLABLE] OnAfterStop
    -- @param #FSM_CONTROLLABLE self
    -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.

    --- Synchronous Event Trigger for Event Stop.
    -- @function [parent=#FSM_CONTROLLABLE] Stop
    -- @param #FSM_CONTROLLABLE self

    --- Asynchronous Event Trigger for Event Stop.
    -- @function [parent=#FSM_CONTROLLABLE] __Stop
    -- @param #FSM_CONTROLLABLE self
    -- @param #number Delay The delay in seconds.  

    --- OnLeave Transition Handler for State Stopped.
    -- @function [parent=#FSM_CONTROLLABLE] OnLeaveStopped
    -- @param #FSM_CONTROLLABLE self
    -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @return #boolean Return false to cancel Transition.

    --- OnEnter Transition Handler for State Stopped.
    -- @function [parent=#FSM_CONTROLLABLE] OnEnterStopped
    -- @param #FSM_CONTROLLABLE self
    -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.

    return self
  end

  --- OnAfter Transition Handler for Event Stop.
  -- @function [parent=#FSM_CONTROLLABLE] OnAfterStop
  -- @param #FSM_CONTROLLABLE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  function FSM_CONTROLLABLE:OnAfterStop( Controllable, From, Event, To )

    -- Clear all pending schedules
    self.CallScheduler:Clear()
  end

  --- Sets the CONTROLLABLE object that the FSM_CONTROLLABLE governs.
  -- @param #FSM_CONTROLLABLE self
  -- @param Wrapper.Controllable#CONTROLLABLE FSMControllable
  -- @return #FSM_CONTROLLABLE
  function FSM_CONTROLLABLE:SetControllable( FSMControllable )
    -- self:F( FSMControllable:GetName() )
    self.Controllable = FSMControllable
  end

  --- Gets the CONTROLLABLE object that the FSM_CONTROLLABLE governs.
  -- @param #FSM_CONTROLLABLE self
  -- @return Wrapper.Controllable#CONTROLLABLE
  function FSM_CONTROLLABLE:GetControllable()
    return self.Controllable
  end

  function FSM_CONTROLLABLE:_call_handler( step, trigger, params, EventName )

    local handler = step .. trigger

    local ErrorHandler = function( errmsg )

      env.info( "Error in SCHEDULER function:" .. errmsg )
      if BASE.Debug ~= nil then
        env.info( BASE.Debug.traceback() )
      end

      return errmsg
    end

    if self[handler] then
      self:T( "*** FSM ***    " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] .. " *** TaskUnit: " .. self.Controllable:GetName() )
      self._EventSchedules[EventName] = nil
      local Result, Value = xpcall( function()
        return self[handler]( self, self.Controllable, unpack( params ) )
      end, ErrorHandler )
      return Value
      -- return self[handler]( self, self.Controllable, unpack( params ) )
    end
  end

end

do -- FSM_PROCESS
  
  ---
  -- @type FSM_PROCESS
  -- @field Tasking.Task#TASK Task
  -- @extends Core.Fsm#FSM_CONTROLLABLE

  --- FSM_PROCESS class models Finite State Machines for @{Tasking.Task} actions, which control @{Wrapper.Client}s.
  -- 
  -- ===
  -- 
  -- @field #FSM_PROCESS FSM_PROCESS
  -- 
  FSM_PROCESS = { ClassName = "FSM_PROCESS" }

  --- Creates a new FSM_PROCESS object.
  -- @param #FSM_PROCESS self
  -- @return #FSM_PROCESS
  function FSM_PROCESS:New( Controllable, Task )

    local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- Core.Fsm#FSM_PROCESS

    -- self:F( Controllable )

    self:Assign( Controllable, Task )

    return self
  end

  function FSM_PROCESS:Init( FsmProcess )
    self:T( "No Initialisation" )
  end

  function FSM_PROCESS:_call_handler( step, trigger, params, EventName )

    local handler = step .. trigger

    local ErrorHandler = function( errmsg )

      env.info( "Error in FSM_PROCESS call handler:" .. errmsg )
      if BASE.Debug ~= nil then
        env.info( BASE.Debug.traceback() )
      end

      return errmsg
    end

    if self[handler] then
      if handler ~= "onstatechange" then
        self:T( "*** FSM ***    " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] .. " *** Task: " .. self.Task:GetName() .. ", TaskUnit: " .. self.Controllable:GetName() )
      end
      self._EventSchedules[EventName] = nil
      local Result, Value
      if self.Controllable and self.Controllable:IsAlive() == true then
        Result, Value = xpcall( function()
          return self[handler]( self, self.Controllable, self.Task, unpack( params ) )
        end, ErrorHandler )
      end
      return Value
      -- return self[handler]( self, self.Controllable, unpack( params ) )
    end
  end

  --- Creates a new FSM_PROCESS object based on this FSM_PROCESS.
  -- @param #FSM_PROCESS self
  -- @return #FSM_PROCESS
  function FSM_PROCESS:Copy( Controllable, Task )
    --self:T3( { self:GetClassNameAndID() } )

    local NewFsm = self:New( Controllable, Task ) -- Core.Fsm#FSM_PROCESS

    NewFsm:Assign( Controllable, Task )

    -- Polymorphic call to initialize the new FSM_PROCESS based on self FSM_PROCESS
    NewFsm:Init( self )

    -- Set Start State
    NewFsm:SetStartState( self:GetStartState() )

    -- Copy Transitions
    for TransitionID, Transition in pairs( self:GetTransitions() ) do
      NewFsm:AddTransition( Transition.From, Transition.Event, Transition.To )
    end

    -- Copy Processes
    for ProcessID, Process in pairs( self:GetProcesses() ) do
      -- self:E( { Process:GetName() } )
      local FsmProcess = NewFsm:AddProcess( Process.From, Process.Event, Process.fsm:Copy( Controllable, Task ), Process.ReturnEvents )
    end

    -- Copy End States
    for EndStateID, EndState in pairs( self:GetEndStates() ) do
      --self:T3( EndState )
      NewFsm:AddEndState( EndState )
    end

    -- Copy the score tables
    for ScoreID, Score in pairs( self:GetScores() ) do
      --self:T3( Score )
      NewFsm:AddScore( ScoreID, Score.ScoreText, Score.Score )
    end

    return NewFsm
  end

  --- Removes an FSM_PROCESS object.
  -- @param #FSM_PROCESS self
  -- @return #FSM_PROCESS
  function FSM_PROCESS:Remove()
    self:F( { self:GetClassNameAndID() } )

    self:F( "Clearing Schedules" )
    self.CallScheduler:Clear()

    -- Copy Processes
    for ProcessID, Process in pairs( self:GetProcesses() ) do
      if Process.fsm then
        Process.fsm:Remove()
        Process.fsm = nil
      end
    end

    return self
  end

  --- Sets the task of the process.
  -- @param #FSM_PROCESS self
  -- @param Tasking.Task#TASK Task
  -- @return #FSM_PROCESS
  function FSM_PROCESS:SetTask( Task )

    self.Task = Task

    return self
  end

  --- Gets the task of the process.
  -- @param #FSM_PROCESS self
  -- @return Tasking.Task#TASK
  function FSM_PROCESS:GetTask()

    return self.Task
  end

  --- Gets the mission of the process.
  -- @param #FSM_PROCESS self
  -- @return Tasking.Mission#MISSION
  function FSM_PROCESS:GetMission()

    return self.Task.Mission
  end

  --- Gets the mission of the process.
  -- @param #FSM_PROCESS self
  -- @return Tasking.CommandCenter#COMMANDCENTER
  function FSM_PROCESS:GetCommandCenter()

    return self:GetTask():GetMission():GetCommandCenter()
  end

  -- TODO: Need to check and fix that an FSM_PROCESS is only for a UNIT. Not for a GROUP.  

  --- Send a message of the @{Tasking.Task} to the Group of the Unit.
  -- @param #FSM_PROCESS self
  function FSM_PROCESS:Message( Message )
    self:F( { Message = Message } )

    local CC = self:GetCommandCenter()
    local TaskGroup = self.Controllable:GetGroup()

    local PlayerName = self.Controllable:GetPlayerName() -- Only for a unit
    PlayerName = PlayerName and " (" .. PlayerName .. ")" or "" -- If PlayerName is nil, then keep it nil, otherwise add brackets.
    local Callsign = self.Controllable:GetCallsign()
    local Prefix = Callsign and " @ " .. Callsign .. PlayerName or ""

    Message = Prefix .. ": " .. Message
    CC:MessageToGroup( Message, TaskGroup )
  end

  --- Assign the process to a @{Wrapper.Unit} and activate the process.
  -- @param #FSM_PROCESS self
  -- @param Tasking.Task#TASK Task
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @return #FSM_PROCESS self
  function FSM_PROCESS:Assign( ProcessUnit, Task )
    -- self:T( { Task:GetName(), ProcessUnit:GetName() } )

    self:SetControllable( ProcessUnit )
    self:SetTask( Task )

    -- self.ProcessGroup = ProcessUnit:GetGroup()

    return self
  end

  --  function FSM_PROCESS:onenterAssigned( ProcessUnit, Task, From, Event, To )
  --  
  --    if From( "Planned" ) then
  --      self:T( "*** FSM ***    Assign *** " .. Task:GetName() .. "/" .. ProcessUnit:GetName() .. " *** " .. From .. " --> " .. Event .. " --> " .. To )
  --      self.Task:Assign()
  --    end
  --  end

  function FSM_PROCESS:onenterFailed( ProcessUnit, Task, From, Event, To )
    self:T( "*** FSM ***    Failed *** " .. Task:GetName() .. "/" .. ProcessUnit:GetName() .. " *** " .. From .. " --> " .. Event .. " --> " .. To )

    self.Task:Fail()
  end

  --- StateMachine callback function for a FSM_PROCESS
  -- @param #FSM_PROCESS self
  -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function FSM_PROCESS:onstatechange( ProcessUnit, Task, From, Event, To )

    if From ~= To then
      self:T( "*** FSM ***    Change *** " .. Task:GetName() .. "/" .. ProcessUnit:GetName() .. " *** " .. From .. " --> " .. Event .. " --> " .. To )
    end

    --    if self:IsTrace() then
    --      MESSAGE:New( "@ Process " .. self:GetClassNameAndID() .. " : " .. Event .. " changed to state " .. To, 2 ):ToAll()
    --      self:F2( { Scores = self._Scores, To = To } )
    --    end

    -- TODO: This needs to be reworked with a callback functions allocated within Task, and set within the mission script from the Task Objects...
    if self._Scores[To] then

      local Task = self.Task
      local Scoring = Task:GetScoring()
      if Scoring then
        Scoring:_AddMissionTaskScore( Task.Mission, ProcessUnit, self._Scores[To].ScoreText, self._Scores[To].Score )
      end
    end
  end

end

do -- FSM_TASK

  --- FSM_TASK class
  -- @type FSM_TASK
  -- @field Tasking.Task#TASK Task
  -- @extends #FSM

  --- Models Finite State Machines for @{Tasking.Task}s.
  --
  -- ===
  --
  -- @field #FSM_TASK FSM_TASK
  --
  FSM_TASK = {
    ClassName = "FSM_TASK",
  }

  --- Creates a new FSM_TASK object.
  -- @param #FSM_TASK self
  -- @param #string TaskName The name of the task.
  -- @return #FSM_TASK
  function FSM_TASK:New( TaskName )

    local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- Core.Fsm#FSM_TASK

    self["onstatechange"] = self.OnStateChange
    self.TaskName = TaskName

    return self
  end

  function FSM_TASK:_call_handler( step, trigger, params, EventName )
    local handler = step .. trigger

    local ErrorHandler = function( errmsg )

      env.info( "Error in SCHEDULER function:" .. errmsg )
      if BASE.Debug ~= nil then
        env.info( BASE.Debug.traceback() )
      end

      return errmsg
    end

    if self[handler] then
      self:T( "*** FSM ***    " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] .. " *** Task: " .. self.TaskName )
      self._EventSchedules[EventName] = nil
      -- return self[handler]( self, unpack( params ) )
      local Result, Value = xpcall( function()
        return self[handler]( self, unpack( params ) )
      end, ErrorHandler )
      return Value
    end
  end

end -- FSM_TASK

do -- FSM_SET

  --- FSM_SET class
  -- @type FSM_SET
  -- @field Core.Set#SET_BASE Set
  -- @extends Core.Fsm#FSM

  --- FSM_SET class models Finite State Machines for @{Core.Set}s. Note that these FSMs control multiple objects!!! So State concerns here
  -- for multiple objects or the position of the state machine in the process.
  --
  -- ===
  --
  -- @field #FSM_SET FSM_SET
  FSM_SET = {
    ClassName = "FSM_SET",
  }

  --- Creates a new FSM_SET object.
  -- @param #FSM_SET self
  -- @param #table FSMT Finite State Machine Table
  -- @param Set_SET_BASE FSMSet (optional) The Set object that the FSM_SET governs.
  -- @return #FSM_SET
  function FSM_SET:New( FSMSet )

    -- Inherits from BASE
    self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM_SET

    if FSMSet then
      self:Set( FSMSet )
    end

    return self
  end

  --- Sets the SET_BASE object that the FSM_SET governs.
  -- @param #FSM_SET self
  -- @param Core.Set#SET_BASE FSMSet
  -- @return #FSM_SET
  function FSM_SET:Set( FSMSet )
    self:F( FSMSet )
    self.Set = FSMSet
  end

  --- Gets the SET_BASE object that the FSM_SET governs.
  -- @param #FSM_SET self
  -- @return Core.Set#SET_BASE
  function FSM_SET:Get()
    return self.Set
  end

  function FSM_SET:_call_handler( step, trigger, params, EventName )
    local handler = step .. trigger
    if self[handler] then
      self:T( "*** FSM ***    " .. step .. " *** " .. params[1] .. " --> " .. params[2] .. " --> " .. params[3] )
      self._EventSchedules[EventName] = nil
      return self[handler]( self, self.Set, unpack( params ) )
    end
  end

end -- FSM_SET

--- **Core** - Spawn dynamically new groups of units in running missions.
--
-- ===
--
-- ## Features:
--
--   * Spawn new groups in running missions.
--   * Schedule spawning of new groups.
--   * Put limits on the amount of groups that can be spawned, and the amount of units that can be alive at the same time.
--   * Randomize the spawning location between different zones.
--   * Randomize the initial positions within the zones.
--   * Spawn in array formation.
--   * Spawn uncontrolled (for planes or helicopters only).
--   * Clean up inactive helicopters that "crashed".
--   * Place a hook to capture a spawn event, and tailor with customer code.
--   * Spawn late activated.
--   * Spawn with or without an initial delay.
--   * Respawn after landing, on the runway or at the ramp after engine shutdown.
--   * Spawn with custom heading, both for a group formation and for the units in the group.
--   * Spawn with different skills.
--   * Spawn with different liveries.
--   * Spawn with an inner and outer radius to set the initial position.
--   * Spawn with a randomize route.
--   * Spawn with a randomized template.
--   * Spawn with a randomized start points on a route.
--   * Spawn with an alternative name.
--   * Spawn and keep the unit names.
--   * Spawn with a different coalition and country.
--   * Enquiry methods to check on spawn status.
--
-- ===
-- 
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Core/Spawn)
-- 
-- ===
--
-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1jirWIo4t4YxqN-HxjqRkL)
--
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions: A lot of people within this community!
--
-- ===
--
-- @module Core.Spawn
-- @image Core_Spawn.JPG

--- SPAWN Class
-- @type SPAWN
-- @field ClassName
-- @field #string SpawnTemplatePrefix
-- @field #string SpawnAliasPrefix
-- @field #number AliveUnits
-- @field #number MaxAliveUnits
-- @field #number SpawnIndex
-- @field #number MaxAliveGroups
-- @field #SPAWN.SpawnZoneTable SpawnZoneTable
-- @extends Core.Base#BASE

--- Allows to spawn dynamically new @{Wrapper.Group}s.
--
-- Each SPAWN object needs to be have related **template groups** setup in the Mission Editor (ME),
-- which is a normal group with the **Late Activation** flag set.
-- This template group will never be activated in your mission.
-- SPAWN uses that **template group** to reference to all the characteristics
-- (air, ground, livery, unit composition, formation, skill level etc) of each new group to be spawned.
--
-- Therefore, when creating a SPAWN object, the @{#SPAWN.New} and @{#SPAWN.NewWithAlias} require
-- **the name of the template group** to be given as a string to those constructor methods.
--
-- Initialization settings can be applied on the SPAWN object,
-- which modify the behavior or the way groups are spawned.
-- These initialization methods have the prefix **Init**.
-- There are also spawn methods with the prefix **Spawn** and will spawn new groups in various ways.
--
-- ### IMPORTANT! The methods with prefix **Init** must be used before any methods with prefix **Spawn** method are used, or unexpected results may appear!!!
--
-- Because SPAWN can spawn multiple groups of a template group,
-- SPAWN has an **internal index** that keeps track
-- which was the latest group that was spawned.
--
-- **Limits** can be set on how many groups can be spawn in each SPAWN object,
-- using the method @{#SPAWN.InitLimit}. SPAWN has 2 kind of limits:
--
--   * The maximum amount of @{Wrapper.Unit}s that can be **alive** at the same time...
--   * The maximum amount of @{Wrapper.Group}s that can be **spawned**... This is more of a **resource**-type of limit.
--
-- When new groups get spawned using the **Spawn** methods,
-- it will be evaluated whether any limits have been reached.
-- When no spawn limit is reached, a new group will be created by the spawning methods,
-- and the internal index will be increased with 1.
--
-- These limits ensure that your mission does not accidentally get flooded with spawned groups.
-- Additionally, it also guarantees that independent of the group composition,
-- at any time, the most optimal amount of groups are alive in your mission.
-- For example, if your template group has a group composition of 10 units, and you specify a limit of 100 units alive at the same time,
-- with unlimited resources = :InitLimit( 100, 0 ) and 10 groups are alive, but two groups have only one unit alive in the group,
-- then a sequent Spawn(Scheduled) will allow a new group to be spawned!!!
--
-- ### IMPORTANT!! If a limit has been reached, it is possible that a **Spawn** method returns **nil**, meaning, no @{Wrapper.Group} had been spawned!!!
--
-- Spawned groups get **the same name** as the name of the template group.
-- Spawned units in those groups keep _by default_ **the same name** as the name of the template group.
-- However, because multiple groups and units are created from the template group,
-- a suffix is added to each spawned group and unit.
--
-- Newly spawned groups will get the following naming structure at run-time:
--
--   1. Spawned groups will have the name _GroupName_#_nnn_, where _GroupName_ is the name of the **template group**,
--   and _nnn_ is a **counter from 0 to 999**.
--   2. Spawned units will have the name _GroupName_#_nnn_-_uu_,
--   where _uu_ is a **counter from 0 to 99** for each new spawned unit belonging to the group.
--
-- That being said, there is a way to keep the same unit names!
-- The method @{#SPAWN.InitKeepUnitNames}() will keep the same unit names as defined within the template group, thus:
--
--   3. Spawned units will have the name _UnitName_#_nnn_-_uu_,
--   where _UnitName_ is the **unit name as defined in the template group*,
--   and _uu_ is a **counter from 0 to 99** for each new spawned unit belonging to the group.
--
-- Some **additional notes that need to be considered!!**:
--
--   * templates are actually groups defined within the mission editor, with the flag "Late Activation" set.
--   As such, these groups are never used within the mission, but are used by the @{#SPAWN} module.
--   * It is important to defined BEFORE you spawn new groups, 
--   a proper initialization of the SPAWN instance is done with the options you want to use.
--   * When designing a mission, NEVER name groups using a "#" within the name of the group Spawn template(s),
--   or the SPAWN module logic won't work anymore.
--
-- ## SPAWN construction methods
--
-- Create a new SPAWN object with the @{#SPAWN.New}() or the @{#SPAWN.NewWithAlias}() methods:
--
--   * @{#SPAWN.New}(): Creates a new SPAWN object taking the name of the group that represents the GROUP template (definition).
--   * @{#SPAWN.NewWithAlias}(): Creates a new SPAWN object taking the name of the group that represents the GROUP template (definition), and gives each spawned @{Wrapper.Group} an different name.
--
-- It is important to understand how the SPAWN class works internally. The SPAWN object created will contain internally a list of groups that will be spawned and that are already spawned.
-- The initialization methods will modify this list of groups so that when a group gets spawned, ALL information is already prepared when spawning. This is done for performance reasons.
-- So in principle, the group list will contain all parameters and configurations after initialization, and when groups get actually spawned, this spawning can be done quickly and efficient.
--
-- ## SPAWN **Init**ialization methods
--
-- A spawn object will behave differently based on the usage of **initialization** methods, which all start with the **Init** prefix:
--
-- ### Unit Names
--
--   * @{#SPAWN.InitKeepUnitNames}(): Keeps the unit names as defined within the mission editor, but note that anything after a # mark is ignored, and any spaces before and after the resulting name are removed. IMPORTANT! This method MUST be the first used after :New !!!
--
-- ### Route randomization
--
--   * @{#SPAWN.InitRandomizeRoute}(): Randomize the routes of spawned groups, and for air groups also optionally the height.
--
-- ### Group composition randomization
--
--   * @{#SPAWN.InitRandomizeTemplate}(): Randomize the group templates so that when a new group is spawned, a random group template is selected from one of the templates defined. 
--
-- ### Uncontrolled
--
--   * @{#SPAWN.InitUnControlled}(): Spawn plane groups uncontrolled.
--
-- ### Array formation
--
--   * @{#SPAWN.InitArray}(): Make groups visible before they are actually activated, and order these groups like a battalion in an array.
--   
-- ### Group initial position - if wanted different from template position, for use with e.g. @{#SPAWN.SpawnScheduled}().   
-- 
--   * @{#SPAWN.InitPositionCoordinate}(): Set initial position of group via a COORDINATE.
--   * @{#SPAWN.InitPositionVec2}(): Set initial position of group via a VEC2. 
--   
-- ### Set the positions of a group's units to absolute positions, or relative positions to unit No. 1
-- 
--   * @{#SPAWN.InitSetUnitRelativePositions}(): Spawn the UNITs of this group with individual relative positions to unit #1 and individual headings.
--   * @{#SPAWN.InitSetUnitAbsolutePositions}(): Spawn the UNITs of this group with individual absolute positions and individual headings.
--   
-- ### Position randomization
--
--   * @{#SPAWN.InitRandomizePosition}(): Randomizes the position of @{Wrapper.Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens.
--   * @{#SPAWN.InitRandomizeUnits}(): Randomizes the @{Wrapper.Unit}s in the @{Wrapper.Group} that is spawned within a **radius band**, given an Outer and Inner radius.
--   * @{#SPAWN.InitRandomizeZones}(): Randomizes the spawning between a predefined list of @{Core.Zone}s that are declared using this function. Each zone can be given a probability factor.
--
-- ### Enable / Disable AI when spawning a new @{Wrapper.Group}
--
--   * @{#SPAWN.InitAIOn}(): Turns the AI On when spawning the new @{Wrapper.Group} object.
--   * @{#SPAWN.InitAIOff}(): Turns the AI Off when spawning the new @{Wrapper.Group} object.
--   * @{#SPAWN.InitAIOnOff}(): Turns the AI On or Off when spawning the new @{Wrapper.Group} object.
--
-- ### Limit scheduled spawning
--
--   * @{#SPAWN.InitLimit}(): Limits the amount of groups that can be alive at the same time and that can be dynamically spawned.
--
-- ### Delay initial scheduled spawn
--
--   * @{#SPAWN.InitDelayOnOff}(): Turns the initial delay On/Off when scheduled spawning the first @{Wrapper.Group} object.
--   * @{#SPAWN.InitDelayOn}(): Turns the initial delay On when scheduled spawning the first @{Wrapper.Group} object.
--   * @{#SPAWN.InitDelayOff}(): Turns the initial delay Off when scheduled spawning the first @{Wrapper.Group} object.
--
-- ### Repeat spawned @{Wrapper.Group}s upon landing
--
--   * @{#SPAWN.InitRepeat}() or @{#SPAWN.InitRepeatOnLanding}(): This method is used to re-spawn automatically the same group after it has landed.
--   * @{#SPAWN.InitRepeatOnEngineShutDown}(): This method is used to re-spawn automatically the same group after it has landed and it shuts down the engines at the ramp.
--   * @{#SPAWN.StopRepeat}(): This method is used to stop the repeater.
--   
-- ### Link-16 Datalink STN and SADL IDs (limited at the moment to F15/16/18/AWACS/Tanker/B1B, but not the F15E for clients, SADL A10CII only)
-- 
--   * @{#SPAWN.InitSTN}(): Set the STN of the first unit in the group. All other units will have consecutive STNs, provided they have not been used yet.
--   * @{#SPAWN.InitSADL}(): Set the SADL of the first unit in the group. All other units will have consecutive SADLs, provided they have not been used yet.
--   
-- ### Callsigns
-- 
--   * @{#SPAWN.InitRandomizeCallsign}(): Set a random callsign name per spawn.
--   * @{#SPAWN.SpawnInitCallSign}(): Set a specific callsign for a spawned group.
--   
-- ### Speed
-- 
--   * @{#SPAWN.InitSpeedMps}(): Set the initial speed on spawning in meters per second.
--   * @{#SPAWN.InitSpeedKph}(): Set the initial speed on spawning in kilometers per hour.
--   * @{#SPAWN.InitSpeedKnots}(): Set the initial speed on spawning in knots.
--
-- ## SPAWN **Spawn** methods
--
-- Groups can be spawned at different times and methods:
--
-- ### **Single** spawning methods
--
--   * @{#SPAWN.Spawn}(): Spawn one new group based on the last spawned index.
--   * @{#SPAWN.ReSpawn}(): Re-spawn a group based on a given index.
--   * @{#SPAWN.SpawnFromVec3}(): Spawn a new group from a Vec3 coordinate. (The group will can be spawned at a point in the air).
--   * @{#SPAWN.SpawnFromVec2}(): Spawn a new group from a Vec2 coordinate. (The group will be spawned at land height ).
--   * @{#SPAWN.SpawnFromStatic}(): Spawn a new group from a structure, taking the position of a @{Wrapper.Static}.
--   * @{#SPAWN.SpawnFromUnit}(): Spawn a new group taking the position of a @{Wrapper.Unit}.
--   * @{#SPAWN.SpawnInZone}(): Spawn a new group in a @{Core.Zone}.
--   * @{#SPAWN.SpawnAtAirbase}(): Spawn a new group at an @{Wrapper.Airbase}, which can be an airdrome, ship or helipad.
--
-- Note that @{#SPAWN.Spawn} and @{#SPAWN.ReSpawn} return a @{Wrapper.Group#GROUP.New} object, that contains a reference to the DCSGroup object.
-- You can use the @{Wrapper.Group#GROUP} object to do further actions with the DCSGroup.
--
-- ### **Scheduled** spawning methods
--
--   * @{#SPAWN.SpawnScheduled}(): Spawn groups at scheduled but randomized intervals.
---  * @{#SPAWN.SpawnScheduleStart}(): Start or continue to spawn groups at scheduled time intervals.
--   * @{#SPAWN.SpawnScheduleStop}(): Stop the spawning of groups at scheduled time intervals.
--
-- ## Retrieve alive GROUPs spawned by the SPAWN object
--
-- The SPAWN class administers which GROUPS it has reserved (in stock) or has created during mission execution.
-- Every time a SPAWN object spawns a new GROUP object, a reference to the GROUP object is added to an internal table of GROUPS.
-- SPAWN provides methods to iterate through that internal GROUP object reference table:
--
--   * @{#SPAWN.GetFirstAliveGroup}(): Will find the first alive GROUP it has spawned, and return the alive GROUP object and the first Index where the first alive GROUP object has been found.
--   * @{#SPAWN.GetNextAliveGroup}(): Will find the next alive GROUP object from a given Index, and return a reference to the alive GROUP object and the next Index where the alive GROUP has been found.
--   * @{#SPAWN.GetLastAliveGroup}(): Will find the last alive GROUP object, and will return a reference to the last live GROUP object and the last Index where the last alive GROUP object has been found.
--
-- You can use the methods @{#SPAWN.GetFirstAliveGroup}() and sequently @{#SPAWN.GetNextAliveGroup}() to iterate through the alive GROUPS within the SPAWN object, and to actions... See the respective methods for an example.
-- The method @{#SPAWN.GetGroupFromIndex}() will return the GROUP object reference from the given Index, dead or alive...
--
-- ## Spawned cleaning of inactive groups
--
-- Sometimes, it will occur during a mission run-time, that ground or especially air objects get damaged, and will while being damaged stop their activities, while remaining alive.
-- In such cases, the SPAWN object will just sit there and wait until that group gets destroyed, but most of the time it won't,
-- and it may occur that no new groups are or can be spawned as limits are reached.
-- To prevent this, a @{#SPAWN.InitCleanUp}() initialization method has been defined that will silently monitor the status of each spawned group.
-- Once a group has a velocity = 0, and has been waiting for a defined interval, that group will be cleaned or removed from run-time.
-- There is a catch however :-) If a damaged group has returned to an airbase within the coalition, that group will not be considered as "lost"...
-- In such a case, when the inactive group is cleaned, a new group will Re-spawned automatically.
-- This models AI that has successfully returned to their airbase, to restart their combat activities.
-- Check the @{#SPAWN.InitCleanUp}() for further info.
--
-- ## Catch the @{Wrapper.Group} Spawn Event in a callback function!
--
-- When using the @{#SPAWN.SpawnScheduled)() method, new @{Wrapper.Group}s are created following the spawn time interval parameters.
-- When a new @{Wrapper.Group} is spawned, you maybe want to execute actions with that group spawned at the spawn event.
-- The SPAWN class supports this functionality through the method @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ),
-- which takes a function as a parameter that you can define locally.
-- Whenever a new @{Wrapper.Group} is spawned, the given function is called, and the @{Wrapper.Group} that was just spawned, is given as a parameter.
-- As a result, your spawn event handling function requires one parameter to be declared, which will contain the spawned @{Wrapper.Group} object.
-- A coding example is provided at the description of the @{#SPAWN.OnSpawnGroup}( **function( SpawnedGroup ) end ** ) method.
--
-- ## Delay the initial spawning
--
-- When using the @{#SPAWN.SpawnScheduled)() method, the default behavior of this method will be that it will spawn the initial (first) @{Wrapper.Group}
-- immediately when :SpawnScheduled() is initiated. The methods @{#SPAWN.InitDelayOnOff}() and @{#SPAWN.InitDelayOn}() can be used to
-- activate a delay before the first @{Wrapper.Group} is spawned. For completeness, a method @{#SPAWN.InitDelayOff}() is also available, that
-- can be used to switch off the initial delay. Because there is no delay by default, this method would only be used when a
-- @{#SPAWN.SpawnScheduleStop}() ; @{#SPAWN.SpawnScheduleStart}() sequence would have been used.
--
-- @field #SPAWN SPAWN
SPAWN = {
  ClassName = "SPAWN",
  SpawnTemplatePrefix = nil,
  SpawnAliasPrefix = nil,
}

--- Enumerator for spawns at airbases
-- @type SPAWN.Takeoff
-- @field #number Air Take off happens in air.
-- @field #number Runway Spawn on runway. Does not work in MP!
-- @field #number Hot Spawn at parking with engines on.
-- @field #number Cold Spawn at parking with engines off.
SPAWN.Takeoff = {
  Air = 1,
  Runway = 2,
  Hot = 3,
  Cold = 4,
}

---
-- @type SPAWN.SpawnZoneTable
-- @list <Core.Zone#ZONE_BASE> SpawnZone

--- Creates the main object to spawn a @{Wrapper.Group} defined in the DCS ME.
-- @param #SPAWN self
-- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template.  Each new group will have the name starting with SpawnTemplatePrefix.
-- @return #SPAWN
-- @usage
-- -- NATO helicopters engaging in the battle field.
-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' )
-- @usage local Plane = SPAWN:New( "Plane" ) -- Creates a new local variable that can initiate new planes with the name "Plane#ddd" using the template "Plane" as defined within the ME.
function SPAWN:New( SpawnTemplatePrefix )
  local self = BASE:Inherit( self, BASE:New() ) -- #SPAWN
  --self:F( { SpawnTemplatePrefix } )

  local TemplateGroup = GROUP:FindByName( SpawnTemplatePrefix )
  if TemplateGroup then
    self.SpawnTemplatePrefix = SpawnTemplatePrefix
    self.SpawnIndex = 0
    self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart.
    self.AliveUnits = 0 -- Contains the counter how many units are currently alive
    self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not.
    self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!!
    self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning.
    self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts.
    self.SpawnInitLimit = false -- By default, no InitLimit
    self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time.
    self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned.
    self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false.
    self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned.
    self.AIOnOff = true -- The AI is on by default when spawning a group.
    self.SpawnUnControlled = false
    self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name.
    self.DelayOnOff = false -- No initial delay when spawning the first group.
    self.SpawnGrouping = nil -- No grouping.
    self.SpawnInitLivery = nil -- No special livery.
    self.SpawnInitSkill = nil -- No special skill.
    self.SpawnInitFreq = nil -- No special frequency.
    self.SpawnInitModu = nil -- No special modulation.
    self.SpawnInitRadio = nil -- No radio comms setting.
    self.SpawnInitModex = nil
    self.SpawnInitModexPrefix = nil
    self.SpawnInitModexPostfix = nil
    self.SpawnInitAirbase = nil
    self.TweakedTemplate = false -- Check if the user is using self made template.
    self.SpawnRandomCallsign = false

    self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned.
  else
    error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" )
  end

  self:SetEventPriority( 5 )
  self.SpawnHookScheduler = SCHEDULER:New( nil )

  return self
end

--- Creates a new SPAWN instance to create new groups based on the defined template and using a new alias for each new group.
-- @param #SPAWN self
-- @param #string SpawnTemplatePrefix is the name of the Group in the ME that defines the Template.
-- @param #string SpawnAliasPrefix is the name that will be given to the Group at runtime.
-- @return #SPAWN self
-- @usage
-- -- NATO helicopters engaging in the battle field.
-- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' )
-- @usage local PlaneWithAlias = SPAWN:NewWithAlias( "Plane", "Bomber" ) -- Creates a new local variable that can instantiate new planes with the name "Bomber#ddd" using the template "Plane" as defined within the ME.
function SPAWN:NewWithAlias( SpawnTemplatePrefix, SpawnAliasPrefix )
  local self = BASE:Inherit( self, BASE:New() )
  --self:F( { SpawnTemplatePrefix, SpawnAliasPrefix } )

  local TemplateGroup = GROUP:FindByName( SpawnTemplatePrefix )
  if TemplateGroup then
    self.SpawnTemplatePrefix = SpawnTemplatePrefix
    self.SpawnAliasPrefix = SpawnAliasPrefix
    self.SpawnIndex = 0
    self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart.
    self.AliveUnits = 0 -- Contains the counter how many units are currently alive
    self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not.
    self.SpawnTemplate = self._GetTemplate( self, SpawnTemplatePrefix ) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!!
    self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning.
    self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts.
    self.SpawnInitLimit = false -- By default, no InitLimit
    self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time.
    self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned.
    self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false.
    self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned.
    self.AIOnOff = true -- The AI is on by default when spawning a group.
    self.SpawnUnControlled = false
    self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name.
    self.DelayOnOff = false -- No initial delay when spawning the first group.
    self.SpawnGrouping = nil -- No grouping.
    self.SpawnInitLivery = nil -- No special livery.
    self.SpawnInitSkill = nil -- No special skill.
    self.SpawnInitFreq = nil -- No special frequency.
    self.SpawnInitModu = nil -- No special modulation.
    self.SpawnInitRadio = nil -- No radio communication setting.
    self.SpawnInitModex = nil
    self.SpawnInitModexPrefix = nil
    self.SpawnInitModexPostfix = nil
    self.SpawnInitAirbase = nil
    self.TweakedTemplate = false -- Check if the user is using self made template.

    self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned.
  else
    error( "SPAWN:New: There is no group declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" )
  end

  self:SetEventPriority( 5 )
  self.SpawnHookScheduler = SCHEDULER:New( nil )

  return self
end

--- Creates a new SPAWN instance to create new groups based on the provided template. This will also register the template for future use.
-- @param #SPAWN self
-- @param #table SpawnTemplate is the Template of the Group. This must be a valid Group Template structure - see [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_func_addGroup)!
-- @param #string SpawnTemplatePrefix [Mandatory] is the name of the template and the prefix of the GROUP on spawn. The name in the template **will** be overwritten!
-- @param #string SpawnAliasPrefix [Optional] is the prefix that will be given to the GROUP on spawn.
-- @param #boolean NoMooseNamingPostfix [Optional] If true, skip the Moose naming additions (like groupname#001-01) - **but** you need to ensure yourself no duplicate group names exist!
-- @return #SPAWN self
-- @usage
-- -- Spawn a P51 Mustang from scratch
-- local ttemp =  
--   {
--       ["modulation"] = 0,
--       ["tasks"] = 
--       {
--       }, -- end of ["tasks"]
--       ["task"] = "Reconnaissance",
--       ["uncontrolled"] = false,
--       ["route"] = 
--       {
--           ["points"] = 
--           {
--               [1] = 
--               {
--                   ["alt"] = 2000,
--                   ["action"] = "Turning Point",
--                   ["alt_type"] = "BARO",
--                   ["speed"] = 125,
--                   ["task"] = 
--                   {
--                       ["id"] = "ComboTask",
--                       ["params"] = 
--                       {
--                           ["tasks"] = 
--                           {
--                           }, -- end of ["tasks"]
--                       }, -- end of ["params"]
--                   }, -- end of ["task"]
--                   ["type"] = "Turning Point",
--                   ["ETA"] = 0,
--                   ["ETA_locked"] = true,
--                   ["y"] = 666285.71428571,
--                   ["x"] = -312000,
--                   ["formation_template"] = "",
--                   ["speed_locked"] = true,
--               }, -- end of [1]
--           }, -- end of ["points"]
--       }, -- end of ["route"]
--       ["groupId"] = 1,
--       ["hidden"] = false,
--       ["units"] = 
--       {
--           [1] = 
--           {
--               ["alt"] = 2000,
--               ["alt_type"] = "BARO",
--               ["livery_id"] = "USAF 364th FS",
--               ["skill"] = "High",
--               ["speed"] = 125,
--               ["type"] = "TF-51D",
--               ["unitId"] = 1,
--               ["psi"] = 0,
--               ["y"] = 666285.71428571,
--               ["x"] = -312000,
--               ["name"] = "P51-1-1",
--               ["payload"] = 
--               {
--                   ["pylons"] = 
--                   {
--                   }, -- end of ["pylons"]
--                   ["fuel"] = 340.68,
--                   ["flare"] = 0,
--                   ["chaff"] = 0,
--                   ["gun"] = 100,
--               }, -- end of ["payload"]
--               ["heading"] = 0,
--               ["callsign"] = 
--               {
--                   [1] = 1,
--                   [2] = 1,
--                   ["name"] = "Enfield11",
--                   [3] = 1,
--               }, -- end of ["callsign"]
--               ["onboard_num"] = "010",
--           }, -- end of [1]
--       }, -- end of ["units"]
--       ["y"] = 666285.71428571,
--       ["x"] = -312000,
--       ["name"] = "P51",
--       ["communication"] = true,
--       ["start_time"] = 0,
--       ["frequency"] = 124,
--   } 
-- 
-- 
-- local mustang = SPAWN:NewFromTemplate(ttemp,"P51D")
-- -- you MUST set the next three:
-- mustang:InitCountry(country.id.FRANCE)
-- mustang:InitCategory(Group.Category.AIRPLANE)
-- mustang:InitCoalition(coalition.side.BLUE)
-- mustang:OnSpawnGroup(
--   function(grp)
--     MESSAGE:New("Group Spawned: "..grp:GetName(),15,"SPAWN"):ToAll()
--   end
-- )
-- mustang:Spawn()
-- 
function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix, NoMooseNamingPostfix )
   local self = BASE:Inherit( self, BASE:New() )
   --self:F( { SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix } )
   --if SpawnAliasPrefix == nil or SpawnAliasPrefix == "" then
     --BASE:I( "ERROR: in function NewFromTemplate, required parameter SpawnAliasPrefix is not set" )
     --return nil
  --end
  if SpawnTemplatePrefix == nil or SpawnTemplatePrefix == "" then
    BASE:I( "ERROR: in function NewFromTemplate, required parameter SpawnTemplatePrefix is not set" )
    return nil
  end

  if SpawnTemplate then
    self.SpawnTemplate = UTILS.DeepCopy(SpawnTemplate) -- Contains the template structure for a Group Spawn from the Mission Editor. Note that this group must have lateActivation always on!!!
    self.SpawnTemplatePrefix = SpawnTemplatePrefix
    self.SpawnAliasPrefix = SpawnAliasPrefix or SpawnTemplatePrefix
    self.SpawnTemplate.name = SpawnTemplatePrefix
    self.SpawnIndex = 0
    self.SpawnCount = 0 -- The internal counter of the amount of spawning the has happened since SpawnStart.
    self.AliveUnits = 0 -- Contains the counter how many units are currently alive
    self.SpawnIsScheduled = false -- Reflects if the spawning for this SpawnTemplatePrefix is going to be scheduled or not.
    self.Repeat = false -- Don't repeat the group from Take-Off till Landing and back Take-Off by ReSpawning.
    self.UnControlled = false -- When working in UnControlled mode, all planes are Spawned in UnControlled mode before the scheduler starts.
    self.SpawnInitLimit = false -- By default, no InitLimit.
    self.SpawnMaxUnitsAlive = 0 -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time.
    self.SpawnMaxGroups = 0 -- The maximum amount of groups that can be spawned.
    self.SpawnRandomize = false -- Sets the randomization flag of new Spawned units to false.
    self.SpawnVisible = false -- Flag that indicates if all the Groups of the SpawnGroup need to be visible when Spawned.
    self.AIOnOff = true -- The AI is on by default when spawning a group.
    self.SpawnUnControlled = false
    self.SpawnInitKeepUnitNames = false -- Overwrite unit names by default with group name.
    self.DelayOnOff = false -- No initial delay when spawning the first group.
    self.Grouping = nil -- No grouping.
    self.SpawnInitLivery = nil -- No special livery.
    self.SpawnInitSkill = nil -- No special skill.
    self.SpawnInitFreq = nil -- No special frequency.
    self.SpawnInitModu = nil -- No special modulation.
    self.SpawnInitRadio = nil -- No radio communication setting.
    self.SpawnInitModex = nil
    self.SpawnInitModexPrefix = nil
    self.SpawnInitModexPostfix = nil
    self.SpawnInitAirbase = nil
    self.TweakedTemplate = true -- Check if the user is using self made template.
    self.MooseNameing  = true
    if NoMooseNamingPostfix == true then
     self.MooseNameing  = false
    end
    
    self.SpawnGroups = {} -- Array containing the descriptions of each Group to be Spawned.
  else
    error( "There is no template provided for SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" )
  end

  self:SetEventPriority( 5 )
  self.SpawnHookScheduler = SCHEDULER:New( nil )

  return self
end

--- Stops any more repeat spawns from happening once the UNIT count of Alive units, spawned by the same SPAWN object, exceeds the first parameter. Also can stop spawns from happening once a total GROUP still alive is met.
-- Exceptionally powerful when combined with SpawnSchedule for Respawning.
-- Note that this method is exceptionally important to balance the performance of the mission. Depending on the machine etc, a mission can only process a maximum amount of units.
-- If the time interval must be short, but there should not be more Units or Groups alive than a maximum amount of units, then this method should be used...
-- When a @{#SPAWN.New} is executed and the limit of the amount of units alive is reached, then no new spawn will happen of the group, until some of these units of the spawn object will be destroyed.
-- @param #SPAWN self
-- @param #number SpawnMaxUnitsAlive The maximum amount of units that can be alive at runtime.
-- @param #number SpawnMaxGroups The maximum amount of groups that can be spawned. When the limit is reached, then no more actual spawns will happen of the group.
-- This parameter is useful to define a maximum amount of airplanes, ground troops, helicopters, ships etc within a supply area.
-- This parameter accepts the value 0, which defines that there are no maximum group limits, but there are limits on the maximum of units that can be alive at the same time.
-- @return #SPAWN self
-- @usage
--
--   -- NATO helicopters engaging in the battle field.
--   -- This helicopter group consists of one Unit. So, this group will SPAWN maximum 2 groups simultaneously within the DCSRTE.
--   -- There will be maximum 24 groups spawned during the whole mission lifetime. 
--   Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitLimit( 2, 24 )
--
function SPAWN:InitLimit( SpawnMaxUnitsAlive, SpawnMaxGroups )
  --self:F( { self.SpawnTemplatePrefix, SpawnMaxUnitsAlive, SpawnMaxGroups } )

  self.SpawnInitLimit = true
  self.SpawnMaxUnitsAlive = SpawnMaxUnitsAlive -- The maximum amount of groups that can be alive of SpawnTemplatePrefix at the same time.
  self.SpawnMaxGroups = SpawnMaxGroups -- The maximum amount of groups that can be spawned.

  for SpawnGroupID = 1, self.SpawnMaxGroups do
    self:_InitializeSpawnGroups( SpawnGroupID )
  end

  return self
end

--- Keeps the unit names as defined within the mission editor, 
-- but note that anything after a # mark is ignored, 
-- and any spaces before and after the resulting name are removed.
-- IMPORTANT! This method MUST be the first used after :New !!!
-- @param #SPAWN self
-- @param #boolean KeepUnitNames (optional) If true, the unit names are kept, false or not provided create new unit names.
-- @return #SPAWN self
function SPAWN:InitKeepUnitNames( KeepUnitNames )
  --self:F()

  self.SpawnInitKeepUnitNames = false
  
  if KeepUnitNames == true then self.SpawnInitKeepUnitNames = true end

  return self
end

--- Flags that the spawned groups must be spawned late activated. 
-- @param #SPAWN self
-- @param #boolean LateActivated (optional) If true, the spawned groups are late activated.
-- @return #SPAWN self
function SPAWN:InitLateActivated( LateActivated )
  --self:F()

  self.LateActivated = LateActivated or true

  return self
end

--- Set spawns to happen at a particular airbase. Only for aircraft, of course.
-- @param #SPAWN self
-- @param #string AirbaseName Name of the airbase.
-- @param #number Takeoff (Optional) Takeoff type. Can be SPAWN.Takeoff.Hot (default), SPAWN.Takeoff.Cold or SPAWN.Takeoff.Runway.
-- @param #number TerminalType (Optional) The terminal type.
-- @return #SPAWN self
function SPAWN:InitAirbase( AirbaseName, Takeoff, TerminalType )
  --self:F()

  self.SpawnInitAirbase = AIRBASE:FindByName( AirbaseName )

  self.SpawnInitTakeoff = Takeoff or SPAWN.Takeoff.Hot

  self.SpawnInitTerminalType = TerminalType

  return self
end

--- Defines the Heading for the new spawned units.
-- The heading can be given as one fixed degree, or can be randomized between minimum and maximum degrees.
-- @param #SPAWN self
-- @param #number HeadingMin The minimum or fixed heading in degrees.
-- @param #number HeadingMax (optional) The maximum heading in degrees. This there is no maximum heading, then the heading will be fixed for all units using minimum heading.
-- @return #SPAWN self
-- @usage
--
--   Spawn = SPAWN:New( ... )
--
--   -- Spawn the units pointing to 100 degrees.
--   Spawn:InitHeading( 100 )
--
--   -- Spawn the units pointing between 100 and 150 degrees.
--   Spawn:InitHeading( 100, 150 )
--
function SPAWN:InitHeading( HeadingMin, HeadingMax )
  --self:F()

  self.SpawnInitHeadingMin = HeadingMin
  self.SpawnInitHeadingMax = HeadingMax

  return self
end

--- Defines the heading of the overall formation of the new spawned group. 
-- The heading can be given as one fixed degree, or can be randomized between minimum and maximum degrees.
-- The Group's formation as laid out in its template will be rotated around the first unit in the group
-- Group individual units facings will rotate to match.  If InitHeading is also applied to this SPAWN then that will take precedence for individual unit facings.
-- Note that InitGroupHeading does *not* rotate the groups route; only its initial facing!
-- @param #SPAWN self
-- @param #number HeadingMin The minimum or fixed heading in degrees.
-- @param #number HeadingMax (optional) The maximum heading in degrees. This there is no maximum heading, then the heading for the group will be HeadingMin.
-- @param #number unitVar (optional) Individual units within the group will have their heading randomized by +/- unitVar degrees.  Default is zero.
-- @return #SPAWN self
-- @usage
--
-- mySpawner = SPAWN:New( ... )
--
--   -- Spawn the Group with the formation rotated +100 degrees around unit #1, compared to the mission template.
--   mySpawner:InitGroupHeading( 100 )
--
--   -- Spawn the Group with the formation rotated units between +100 and +150 degrees around unit #1, compared to the mission template, and with individual units varying by +/- 10 degrees from their templated facing.
--   mySpawner:InitGroupHeading( 100, 150, 10 )
--
--   -- Spawn the Group with the formation rotated -60 degrees around unit #1, compared to the mission template, but with all units facing due north regardless of how they were laid out in the template.
--   mySpawner:InitGroupHeading(-60):InitHeading(0)
--   -- or
--   mySpawner:InitHeading(0):InitGroupHeading(-60)
--
function SPAWN:InitGroupHeading( HeadingMin, HeadingMax, unitVar )
  self:F( { HeadingMin = HeadingMin, HeadingMax = HeadingMax, unitVar = unitVar } )

  self.SpawnInitGroupHeadingMin = HeadingMin
  self.SpawnInitGroupHeadingMax = HeadingMax
  self.SpawnInitGroupUnitVar = unitVar
  return self
end

--- Sets the coalition of the spawned group. Note that it might be necessary to also set the country explicitly!
-- @param #SPAWN self
-- @param DCS#coalition.side Coalition Coalition of the group as number of enumerator:
--
--   * @{DCS#coalition.side.NEUTRAL}
--   * @{DCS#coalition.side.RED}
--   * @{DCS#coalition.side.BLUE}
--  
-- @return #SPAWN self
function SPAWN:InitCoalition( Coalition )
  self:F( { coalition = Coalition } )

  self.SpawnInitCoalition = Coalition

  return self
end

--- Sets the country of the spawn group. Note that the country determines the coalition of the group depending on which country is defined to be on which side for each specific mission!
-- @param #SPAWN self
-- @param #number Country Country id as number or enumerator:
--
--   * @{DCS#country.id.RUSSIA}
--   * @{DCS#country.id.USA}
--
-- @return #SPAWN self
function SPAWN:InitCountry( Country )
  --self:F()

  self.SpawnInitCountry = Country

  return self
end

--- Sets category ID of the group.
-- @param #SPAWN self
-- @param #number Category Category id.
-- @return #SPAWN self
function SPAWN:InitCategory( Category )
  --self:F()

  self.SpawnInitCategory = Category

  return self
end

--- Sets livery of the group.
-- @param #SPAWN self
-- @param #string Livery Livery name. Note that this is not necessarily the same name as displayed in the mission editor.
-- @return #SPAWN self
function SPAWN:InitLivery( Livery )
  --self:F( { livery = Livery } )

  self.SpawnInitLivery = Livery

  return self
end

--- Sets skill of the group.
-- @param #SPAWN self
-- @param #string Skill Skill, possible values "Average", "Good", "High", "Excellent" or "Random".
-- @return #SPAWN self
function SPAWN:InitSkill( Skill )
  --self:F( { skill = Skill } )
  if Skill:lower() == "average" then
    self.SpawnInitSkill = "Average"
  elseif Skill:lower() == "good" then
    self.SpawnInitSkill = "Good"
  elseif Skill:lower() == "excellent" then
    self.SpawnInitSkill = "Excellent"
  elseif Skill:lower() == "random" then
    self.SpawnInitSkill = "Random"
  else
    self.SpawnInitSkill = "High"
  end

  return self
end

--- [Airplane - F15/16/18/AWACS/B1B/Tanker only] Set the STN Link16 starting number of the Group; each unit of the spawned group will have a consecutive STN set.
-- @param #SPAWN self
-- @param #number Octal The octal number (digits 1..7, max 5 digits, i.e. 1..77777) to set the STN to. Every STN needs to be unique!
-- @return #SPAWN self
function SPAWN:InitSTN(Octal)
  --self:F( { Octal = Octal } )
  self.SpawnInitSTN = Octal or 77777
  local num = UTILS.OctalToDecimal(Octal)
  if num == nil or num < 1 then
    self:E("WARNING - STN "..tostring(Octal).." is not valid!")
    return self
  end
  if _DATABASE.STNS[num] ~= nil then
    self:E("WARNING - STN already assigned: "..tostring(Octal).." is used for ".._DATABASE.STNS[Octal])
  end
  return self
end

--- [Airplane - A10-C II only] Set the SADL TN starting number of the Group; each unit of the spawned group will have a consecutive SADL set.
-- @param #SPAWN self
-- @param #number Octal The octal number (digits 1..7, max 4 digits, i.e. 1..7777) to set the SADL to. Every SADL needs to be unique!
-- @return #SPAWN self
function SPAWN:InitSADL(Octal)
  --self:F( { Octal = Octal } )
  self.SpawnInitSADL = Octal or 7777
  local num = UTILS.OctalToDecimal(Octal)
  if num == nil or num < 1 then
    self:E("WARNING - SADL "..tostring(Octal).." is not valid!")
    return self
  end
  if _DATABASE.SADL[num] ~= nil then
    self:E("WARNING - SADL already assigned: "..tostring(Octal).." is used for ".._DATABASE.SADL[Octal])
  end
  return self
end

--- [Airplane] Set the initial speed on spawning in meters per second. Useful when spawning in-air only.
-- @param #SPAWN self
-- @param #number MPS The speed in MPS to use.
-- @return #SPAWN self
function SPAWN:InitSpeedMps(MPS)
--self:F( { MPS = MPS } )
  if MPS == nil or tonumber(MPS)<0 then
    MPS=125
  end
  self.InitSpeed = MPS
  return self
end

--- [Airplane] Set the initial speed on spawning in knots. Useful when spawning in-air only.
-- @param #SPAWN self
-- @param #number Knots The speed in knots to use.
-- @return #SPAWN self
function SPAWN:InitSpeedKnots(Knots)
--self:F( { Knots = Knots } )
  if Knots == nil or tonumber(Knots)<0 then
    Knots=300
  end
  self.InitSpeed = UTILS.KnotsToMps(Knots)
  return self
end

--- [Airplane] Set the initial speed on spawning in kilometers per hour. Useful when spawning in-air only.
-- @param #SPAWN self
-- @param #number KPH The speed in KPH to use.
-- @return #SPAWN self
function SPAWN:InitSpeedKph(KPH)
  --self:F( { KPH = KPH } )
  if KPH == nil or tonumber(KPH)<0 then
    KPH=UTILS.KnotsToKmph(300)
  end
  self.InitSpeed = UTILS.KmphToMps(KPH)
  return self
end


--- Sets the radio communication on or off. Same as checking/unchecking the COMM box in the mission editor.
-- @param #SPAWN self
-- @param #number switch If true (or nil), enables the radio communication. If false, disables the radio for the spawned group.
-- @return #SPAWN self
function SPAWN:InitRadioCommsOnOff( switch )
  --self:F( { switch = switch } )
  self.SpawnInitRadio = switch or true
  return self
end

--- Sets the radio frequency of the group.
-- @param #SPAWN self 
-- @param #number frequency The frequency in MHz.
-- @return #SPAWN self
function SPAWN:InitRadioFrequency( frequency )
  --self:F( { frequency = frequency } )

  self.SpawnInitFreq = frequency

  return self
end

--- Set radio modulation. Default is AM.
-- @param #SPAWN self
-- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM.
-- @return #SPAWN self
function SPAWN:InitRadioModulation( modulation )
  --self:F( { modulation = modulation } )
  if modulation and modulation:lower() == "fm" then
    self.SpawnInitModu = radio.modulation.FM
  else
    self.SpawnInitModu = radio.modulation.AM
  end
  return self
end

--- Sets the modex of the first unit of the group. If more units are in the group, the number is increased by one with every unit.
-- @param #SPAWN self
-- @param #number modex Modex of the first unit.
-- @param #string prefix (optional) String to prefix to modex, e.g. for French AdA Modex, eg. -L-102 then "-L-" would be the prefix.
-- @param #string postfix (optional) String to postfix to modex, example tbd.
-- @return #SPAWN self
function SPAWN:InitModex( modex, prefix, postfix )

  if modex then
    self.SpawnInitModex = tonumber( modex )
  end
  
  self.SpawnInitModexPrefix = prefix
  self.SpawnInitModexPostfix = postfix

  return self
end

--- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behavior of groups.
-- @param #SPAWN self
-- @param #number SpawnStartPoint is the waypoint where the randomization begins.
-- Note that the StartPoint = 0 equaling the point where the group is spawned.
-- @param #number SpawnEndPoint is the waypoint where the randomization ends counting backwards.
-- This parameter is useful to avoid randomization to end at a waypoint earlier than the last waypoint on the route.
-- @param #number SpawnRadius is the radius in meters in which the randomization of the new waypoints, with the original waypoint of the original template located in the middle ...
-- @param #number SpawnHeight (optional) Specifies the **additional** height in meters that can be added to the base height specified at each waypoint in the ME.
-- @return #SPAWN
-- @usage
--
--   -- NATO helicopters engaging in the battle field.
--   -- The KA-50 has waypoints Start point ( =0 or SP ), 1, 2, 3, 4, End point (= 5 or DP).
--   -- Waypoints 2 and 3 will only be randomized. The others will remain on their original position with each new spawn of the helicopter.
--   -- The randomization of waypoint 2 and 3 will take place within a radius of 2000 meters.
--   Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeRoute( 2, 2, 2000 )
--
function SPAWN:InitRandomizeRoute( SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight )
  --self:F( { self.SpawnTemplatePrefix, SpawnStartPoint, SpawnEndPoint, SpawnRadius, SpawnHeight } )

  self.SpawnRandomizeRoute = true
  self.SpawnRandomizeRouteStartPoint = SpawnStartPoint
  self.SpawnRandomizeRouteEndPoint = SpawnEndPoint
  self.SpawnRandomizeRouteRadius = SpawnRadius
  self.SpawnRandomizeRouteHeight = SpawnHeight

  for GroupID = 1, self.SpawnMaxGroups do
    self:_RandomizeRoute( GroupID )
  end

  return self
end

--- Randomizes the position of @{Wrapper.Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens.
-- @param #SPAWN self
-- @param #boolean RandomizePosition If true, SPAWN will perform the randomization of the @{Wrapper.Group}s position between a given outer and inner radius.
-- @param DCS#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned.
-- @param DCS#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned.
-- @return #SPAWN
function SPAWN:InitRandomizePosition( RandomizePosition, OuterRadius, InnerRadius )
  --self:F( { self.SpawnTemplatePrefix, RandomizePosition, OuterRadius, InnerRadius } )

  self.SpawnRandomizePosition = RandomizePosition or false
  self.SpawnRandomizePositionOuterRadius = OuterRadius or 0
  self.SpawnRandomizePositionInnerRadius = InnerRadius or 0

  for GroupID = 1, self.SpawnMaxGroups do
    self:_RandomizeRoute( GroupID )
  end

  return self
end

--- Randomizes the UNITs that are spawned within a radius band given an Outer and Inner radius.
-- @param #SPAWN self
-- @param #boolean RandomizeUnits If true, SPAWN will perform the randomization of the @{Wrapper.Unit#UNIT}s position within the group between a given outer and inner radius.
-- @param DCS#Distance OuterRadius (optional) The outer radius in meters where the new group will be spawned.
-- @param DCS#Distance InnerRadius (optional) The inner radius in meters where the new group will NOT be spawned.
-- @return #SPAWN
-- @usage
--
--   -- NATO helicopters engaging in the battle field.
--   -- UNIT positions of this group will be randomized around the base unit #1 in a circle of 50 to 500 meters.
--   Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitRandomizeUnits( true, 500, 50 )
--
function SPAWN:InitRandomizeUnits( RandomizeUnits, OuterRadius, InnerRadius )
  --self:F( { self.SpawnTemplatePrefix, RandomizeUnits, OuterRadius, InnerRadius } )

  self.SpawnRandomizeUnits = RandomizeUnits or false
  self.SpawnOuterRadius = OuterRadius or 0
  self.SpawnInnerRadius = InnerRadius or 0

  for GroupID = 1, self.SpawnMaxGroups do
    self:_RandomizeRoute( GroupID )
  end

  return self
end

--- Spawn the UNITs of this group with individual relative positions to unit #1 and individual headings.
-- @param #SPAWN self
-- @param #table Positions Table of positions, needs to one entry per unit in the group(!). The table contains one table each for each unit, with x,y, and optionally z 
-- relative positions, and optionally an individual heading.
-- @return #SPAWN
-- @usage
--
--   -- NATO helicopter group of three units engaging in the battle field.
--   local Positions = { [1] = {x = 0, y = 0, heading = 0}, [2] = {x = 50, y = 50, heading = 90}, [3] = {x = -50, y = 50, heading = 180} }
--   Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitSetUnitRelativePositions(Positions)
--
function SPAWN:InitSetUnitRelativePositions(Positions)
  --self:F({self.SpawnTemplatePrefix, Positions})
  
  self.SpawnUnitsWithRelativePositions = true
  self.UnitsRelativePositions = Positions
  
  return self
end

--- Spawn the UNITs of this group with individual absolute positions and individual headings.
-- @param #SPAWN self
-- @param #table Positions Table of positions, needs to one entry per unit in the group(!). The table contains one table each for each unit, with x,y, and optionally z 
-- absolute positions, and optionally an individual heading.
-- @return #SPAWN
-- @usage
--
--   -- NATO helicopter group of three units engaging in the battle field.
--   local Positions = { [1] = {x = 0, y = 0, heading = 0}, [2] = {x = 50, y = 50, heading = 90}, [3] = {x = -50, y = 50, heading = 180} }
--   Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):InitSetUnitAbsolutePositions(Positions)
--
function SPAWN:InitSetUnitAbsolutePositions(Positions)
  --self:F({self.SpawnTemplatePrefix, Positions})
  
  self.SpawnUnitsWithAbsolutePositions = true
  self.UnitsAbsolutePositions = Positions
  
  return self
end


--- Uses Disposition and other fallback logic to find better ground positions for ground units.
--- NOTE: This is not a spawn randomizer.
--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area.
--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit.
-- @param #SPAWN self
-- @param #boolean OnOff Enable/disable the feature.
-- @param #number MaxRadius (Optional) Max radius to search for valid ground locations in meters. Default is double the max radius of the units.
-- @param #number Spacing (Optional) Minimum spacing between units in meters. Default is 5% of the search radius or 5 meters, whichever is larger.
-- @return #SPAWN
function SPAWN:InitValidateAndRepositionGroundUnits(OnOff, MaxRadius, Spacing)
    self.SpawnValidateAndRepositionGroundUnits = OnOff
    self.SpawnValidateAndRepositionGroundUnitsRadius = MaxRadius
    self.SpawnValidateAndRepositionGroundUnitsSpacing = Spacing
    return self
end

--- This method is rather complicated to understand. But I'll try to explain.
-- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor,
-- but they will all follow the same Template route and have the same prefix name.
-- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group.
-- @param #SPAWN self
-- @param #list<#string> SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be chosen when a new group will be spawned.
-- @return #SPAWN
-- @usage
--
--   -- NATO Tank Platoons invading Gori.
--   -- Choose between 13 different 'US Tank Platoon' configurations for each new SPAWN the Group to be spawned for the
--   -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SpawnTemplatePrefixes.
--   -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and
--   -- with a limit set of maximum 12 Units alive simultaneously  and 150 Groups to be spawned during the whole mission.
--   Spawn_US_Platoon = { 'US Tank Platoon 1', 'US Tank Platoon 2', 'US Tank Platoon 3', 'US Tank Platoon 4', 'US Tank Platoon 5',
--                        'US Tank Platoon 6', 'US Tank Platoon 7', 'US Tank Platoon 8', 'US Tank Platoon 9', 'US Tank Platoon 10',
--                        'US Tank Platoon 11', 'US Tank Platoon 12', 'US Tank Platoon 13' }
--   Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 )
--   Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 )
--   Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 )
function SPAWN:InitRandomizeTemplate( SpawnTemplatePrefixTable )
  --self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } )
  
  local temptable = {}
  for _,_temp in pairs(SpawnTemplatePrefixTable) do
    temptable[#temptable+1] = _temp
  end
  
  self.SpawnTemplatePrefixTable = UTILS.ShuffleTable(temptable)
  self.SpawnRandomizeTemplate = true

  for SpawnGroupID = 1, self.SpawnMaxGroups do
    self:_RandomizeTemplate( SpawnGroupID, RandomizePositionInZone )
  end

  return self
end

--- Randomize templates to be used as the unit representatives for the Spawned group, defined using a SET_GROUP object.
-- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor,
-- but they will all follow the same Template route and have the same prefix name.
-- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group.
-- @param #SPAWN self
-- @param Core.Set#SET_GROUP SpawnTemplateSet A SET_GROUP object set, that contains the groups that are possible unit representatives of the group to be spawned.
-- @param #boolean RandomizePositionInZone If nil or true, also the position inside the selected random zone will be randomized. Set to false to use the center of the zone.
-- @return #SPAWN
-- @usage
--
--   -- NATO Tank Platoons invading Gori.
--
--   -- Choose between different 'US Tank Platoon Template' configurations to be spawned for the
--   -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SPAWN objects.
--
--   -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and
--   -- with a limit set of maximum 12 Units alive simultaneously  and 150 Groups to be spawned during the whole mission.
--
--   Spawn_US_PlatoonSet = SET_GROUP:New():FilterPrefixes( "US Tank Platoon Templates" ):FilterOnce()
--
--   -- Now use the Spawn_US_PlatoonSet to define the templates using InitRandomizeTemplateSet.
--   Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 )
--   Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 )
--   Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 )
--
function SPAWN:InitRandomizeTemplateSet( SpawnTemplateSet,RandomizePositionInZone )
  --self:F( { self.SpawnTemplatePrefix } )

  local setnames = SpawnTemplateSet:GetSetNames()
  self:InitRandomizeTemplate(setnames,RandomizePositionInZone)
  
  return self
end

--- Randomize templates to be used as the unit representatives for the Spawned group, defined by specifying the prefix names.
-- This method becomes useful when you need to spawn groups with random templates of groups defined within the mission editor,
-- but they will all follow the same Template route and have the same prefix name.
-- In other words, this method randomizes between a defined set of groups the template to be used for each new spawn of a group.
-- @param #SPAWN self
-- @param #string SpawnTemplatePrefixes A string or a list of string that contains the prefixes of the groups that are possible unit representatives of the group to be spawned.
-- @param #boolean RandomizePositionInZone If nil or true, also the position inside the selected random zone will be randomized. Set to false to use the center of the zone. 
-- @return #SPAWN
-- @usage
--
--  -- NATO Tank Platoons invading Gori.
--
--   -- Choose between different 'US Tank Platoon Templates' configurations to be spawned for the
--   -- 'US Tank Platoon Left', 'US Tank Platoon Middle' and 'US Tank Platoon Right' SPAWN objects.
--
--   -- Each new SPAWN will randomize the route, with a defined time interval of 200 seconds with 40% time variation (randomization) and
--   -- with a limit set of maximum 12 Units alive simultaneously  and 150 Groups to be spawned during the whole mission.
--
--   Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 )
--   Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 )
--   Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):SpawnScheduled( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 )
--
function SPAWN:InitRandomizeTemplatePrefixes( SpawnTemplatePrefixes, RandomizePositionInZone ) -- R2.3
  --self:F( { self.SpawnTemplatePrefix } )

  local SpawnTemplateSet = SET_GROUP:New():FilterPrefixes( SpawnTemplatePrefixes ):FilterOnce()

  self:InitRandomizeTemplateSet( SpawnTemplateSet, RandomizePositionInZone )

  return self
end

--- When spawning a new group, make the grouping of the units according the InitGrouping setting.
-- @param #SPAWN self
-- @param #number Grouping Indicates the maximum amount of units in the group.
-- @return #SPAWN
function SPAWN:InitGrouping( Grouping ) -- R2.2
  --self:F( { self.SpawnTemplatePrefix, Grouping } )

  self.SpawnGrouping = Grouping

  return self
end

--- This method provides the functionality to randomize the spawning of the Groups at a given list of zones of different types.
-- @param #SPAWN self
-- @param #table SpawnZoneTable A table with @{Core.Zone} objects. If this table is given, then each spawn will be executed within the given list of @{Core.Zone}s objects.
-- @param #boolean RandomizePositionInZone If nil or true, also the position inside the selected random zone will be randomized. Set to false to use the center of the zone.
-- @return #SPAWN self
-- @usage
--
--    -- Create a zone table of the 2 zones.
--    ZoneTable = { ZONE:New( "Zone1" ), ZONE:New( "Zone2" ) }
--
--    Spawn_Vehicle_1 = SPAWN:New( "Spawn Vehicle 1" )
--                           :InitLimit( 10, 10 )
--                           :InitRandomizeRoute( 1, 1, 200 )
--                           :InitRandomizeZones( ZoneTable )
--                           :SpawnScheduled( 5, .5 )
--
function SPAWN:InitRandomizeZones( SpawnZoneTable, RandomizePositionInZone )
  --self:F( { self.SpawnTemplatePrefix, SpawnZoneTable } )
  
  local temptable = {}
  for _,_temp in pairs(SpawnZoneTable) do
    temptable[#temptable+1] = _temp
  end
  
  self.SpawnZoneTable = UTILS.ShuffleTable(temptable)
  self.SpawnRandomizeZones = true

  for SpawnGroupID = 1, self.SpawnMaxGroups do
    self:_RandomizeZones( SpawnGroupID, RandomizePositionInZone )
  end

  return self
end

--- [AIR/Fighter only!] This method randomizes the callsign for a new group.
-- @param #SPAWN self
-- @return #SPAWN self
function SPAWN:InitRandomizeCallsign()
  self.SpawnRandomCallsign = true
  return self
end

--- [BLUE AIR only!] This method sets a specific callsign for a spawned group. Use for a group with one unit only!
-- @param #SPAWN self
-- @param #number ID ID of the callsign enumerator, e.g. CALLSIGN.Tanker.Texaco - - resulting in e.g. Texaco-2-1
-- @param #string Name Name of this callsign as it cannot be determined from the ID because of the dependency on the task type of the plane, and the plane type. E.g. "Texaco"
-- @param #number Minor Minor number, i.e. the unit number within the group, e.g 2 - resulting in e.g. Texaco-2-1
-- @param #number Major Major number, i.e. the group number of this name, e.g. 1 - resulting in e.g. Texaco-2-1
-- @return #SPAWN self
function SPAWN:InitCallSign(ID,Name,Minor,Major)
  local Name = Name or "Enfield"
  self.SpawnInitCallSign = true
  self.SpawnInitCallSignID = ID or 1 
  self.SpawnInitCallSignMinor = Minor or 1
  self.SpawnInitCallSignMajor = Major or 1 
  self.SpawnInitCallSignName=string.lower(Name):gsub("^%l", string.upper)
  return self
end

--- This method sets a spawn position for the group that is different from the location of the template.
-- @param #SPAWN self
-- @param Core.Point#COORDINATE Coordinate The position to spawn from
-- @return #SPAWN self
function SPAWN:InitPositionCoordinate(Coordinate)
  --self:T2( { self.SpawnTemplatePrefix, Coordinate:GetVec2()} )
  self:InitPositionVec2(Coordinate:GetVec2())
  return self
end

--- This method sets a spawn position for the group that is different from the location of the template.
-- @param #SPAWN self
-- @param DCS#Vec2 Vec2 The position to spawn from
-- @return #SPAWN self
function SPAWN:InitPositionVec2(Vec2)
  --self:T2( { self.SpawnTemplatePrefix, Vec2} )
  self.SpawnInitPosition = Vec2
  self.SpawnFromNewPosition = true
  --self:T2("MaxGroups:"..self.SpawnMaxGroups)
  for SpawnGroupID = 1, self.SpawnMaxGroups do
    self:_SetInitialPosition( SpawnGroupID )
  end
  return self
end

--- For planes and helicopters, when these groups go home and land on their home airbases and FARPs, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment.
-- This method is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed.
-- This will enable a spawned group to be re-spawned after it lands, until it is destroyed...
-- Note: When the group is respawned, it will re-spawn from the original airbase where it took off.
-- So ensure that the routes for groups that respawn, always return to the original airbase, or players may get confused ...
-- @param #SPAWN self
-- @return #SPAWN self
-- @usage
--
--   -- RU Su-34 - AI Ship Attack
--   -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically.
--   SpawnRU_SU34 = SPAWN:New( 'Su-34' )
--                       :Schedule( 2, 3, 1800, 0.4 )
--                       :SpawnUncontrolled()
--                       :InitRandomizeRoute( 1, 1, 3000 )
--                       :InitRepeatOnEngineShutDown()
--
function SPAWN:InitRepeat()
  --self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } )

  self.Repeat = true
  self.RepeatOnEngineShutDown = false
  self.RepeatOnLanding = true

  return self
end

--- Respawn group after landing.
-- @param #SPAWN self
-- @param #number WaitingTime Wait this many seconds before despawning the alive group after landing. Defaults to 3 .
-- @return #SPAWN self
-- @usage
--
--   -- RU Su-34 - AI Ship Attack
--   -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically.
--   SpawnRU_SU34 = SPAWN:New( 'Su-34' )
--                       :InitRandomizeRoute( 1, 1, 3000 )
--                       :InitRepeatOnLanding(20)
--                       :Spawn()
--
function SPAWN:InitRepeatOnLanding(WaitingTime)
  --self:F( { self.SpawnTemplatePrefix } )

  self:InitRepeat()
  self.RepeatOnEngineShutDown = false
  self.RepeatOnLanding = true
  self.RepeatOnLandingTime = (WaitingTime and WaitingTime > 3) and WaitingTime or 3

  return self
end

--- Respawn after landing when its engines have shut down.
-- @param #SPAWN self
-- @return #SPAWN self
-- @usage
--
--   -- RU Su-34 - AI Ship Attack
--   -- Re-SPAWN the Group(s) after each landing and Engine Shut-Down automatically.
--   SpawnRU_SU34 = SPAWN:New( 'Su-34' )
--                       :SpawnUncontrolled()
--                       :InitRandomizeRoute( 1, 1, 3000 )
--                       :InitRepeatOnEngineShutDown()
--                       :Spawn()
function SPAWN:InitRepeatOnEngineShutDown()
  --self:F( { self.SpawnTemplatePrefix } )

  self:InitRepeat()
  self.RepeatOnEngineShutDown = true
  self.RepeatOnLanding = false

  return self
end

--- Delete groups that have not moved for X seconds - AIR ONLY!!!
-- DO NOT USE ON GROUPS THAT DO NOT MOVE OR YOUR SERVER WILL BURN IN HELL (Pikes - April 2020)
-- When groups are still alive and have become inactive due to damage and are unable to contribute anything, then this group will be removed at defined intervals in seconds.
-- @param #SPAWN self
-- @param #string SpawnCleanUpInterval The interval to check for inactive groups within seconds.
-- @return #SPAWN self
-- @usage
--
--   Spawn_Helicopter:InitCleanUp( 20 )  -- CleanUp the spawning of the helicopters every 20 seconds when they become inactive.
--
function SPAWN:InitCleanUp( SpawnCleanUpInterval )
  --self:F( { self.SpawnTemplatePrefix, SpawnCleanUpInterval } )

  self.SpawnCleanUpInterval = SpawnCleanUpInterval
  self.SpawnCleanUpTimeStamps = {}

  local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup()
  --self:T2( { "CleanUp Scheduler:", SpawnGroup } )

  self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 )
  return self
end

--- Makes the groups visible before start (like a battalion).
-- The method will take the position of the group as the first position in the array.
-- CAUTION: this directive will NOT work with OnSpawnGroup function.
-- @param #SPAWN self
-- @param #number SpawnAngle The angle in degrees how the groups and each unit of the group will be positioned.
-- @param #number SpawnWidth The amount of Groups that will be positioned on the X axis.
-- @param #number SpawnDeltaX The space between each Group on the X-axis.
-- @param #number SpawnDeltaY The space between each Group on the Y-axis.
-- @return #SPAWN self
-- @usage
--
--   -- Define an array of Groups.
--   Spawn_BE_Ground = SPAWN:New( 'BE Ground' )
--                          :InitLimit( 2, 24 )
--                          :InitArray( 90, 10, 100, 50 )
--
function SPAWN:InitArray( SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY )
  --self:F( { self.SpawnTemplatePrefix, SpawnAngle, SpawnWidth, SpawnDeltaX, SpawnDeltaY } )

  self.SpawnVisible = true -- When the first Spawn executes, all the Groups need to be made visible before start.

  local SpawnX = 0
  local SpawnY = 0
  local SpawnXIndex = 0
  local SpawnYIndex = 0

  for SpawnGroupID = 1, self.SpawnMaxGroups do
    --self:T2( { SpawnX, SpawnY, SpawnXIndex, SpawnYIndex } )

    self.SpawnGroups[SpawnGroupID].Visible = true
    self.SpawnGroups[SpawnGroupID].Spawned = false

    SpawnXIndex = SpawnXIndex + 1
    if SpawnWidth and SpawnWidth ~= 0 then
      if SpawnXIndex >= SpawnWidth then
        SpawnXIndex = 0
        SpawnYIndex = SpawnYIndex + 1
      end
    end

    local SpawnRootX = self.SpawnGroups[SpawnGroupID].SpawnTemplate.x
    local SpawnRootY = self.SpawnGroups[SpawnGroupID].SpawnTemplate.y

    self:_TranslateRotate( SpawnGroupID, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle )

    self.SpawnGroups[SpawnGroupID].SpawnTemplate.lateActivation = true
    self.SpawnGroups[SpawnGroupID].SpawnTemplate.visible = true

    self.SpawnGroups[SpawnGroupID].Visible = true

    self:HandleEvent( EVENTS.Birth, self._OnBirth )
    self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash )
    self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash )
    self:HandleEvent( EVENTS.RemoveUnit, self._OnDeadOrCrash )
    self:HandleEvent( EVENTS.UnitLost, self._OnDeadOrCrash )
    if self.Repeat then
      self:HandleEvent( EVENTS.Takeoff, self._OnTakeOff )
      self:HandleEvent( EVENTS.Land, self._OnLand )
    end
    if self.RepeatOnEngineShutDown then
      self:HandleEvent( EVENTS.EngineShutdown, self._OnEngineShutDown )
    end

    self.SpawnGroups[SpawnGroupID].Group = _DATABASE:Spawn( self.SpawnGroups[SpawnGroupID].SpawnTemplate )

    SpawnX = SpawnXIndex * SpawnDeltaX
    SpawnY = SpawnYIndex * SpawnDeltaY
  end

  return self
end

--- Stop the SPAWN InitRepeat function (EVENT handler for takeoff, land and engine shutdown)
-- @param #SPAWN self
-- @return #SPAWN self
-- @usage
--          local spawn = SPAWN:New("Template Group")
--            :InitRepeatOnEngineShutDown()
--          local plane = spawn:Spawn() -- it is important that we keep the SPAWN object and do not overwrite it with the resulting GROUP object by just calling :Spawn()
--          
--          -- later on
--          spawn:StopRepeat()
function SPAWN:StopRepeat()
  if self.Repeat then
    self:UnHandleEvent(EVENTS.Takeoff)
    self:UnHandleEvent(EVENTS.Land)
  end
  if self.RepeatOnEngineShutDown then
    self:UnHandleEvent(EVENTS.EngineShutdown)
  end
  self.Repeat = false
  self.RepeatOnEngineShutDown = false
  self.RepeatOnLanding = false
  return self
end

do -- AI methods

  --- Turns the AI On or Off for the @{Wrapper.Group} when spawning.
  -- @param #SPAWN self
  -- @param #boolean AIOnOff A value of true sets the AI On, a value of false sets the AI Off.
  -- @return #SPAWN The SPAWN object
  function SPAWN:InitAIOnOff( AIOnOff )

    self.AIOnOff = AIOnOff
    return self
  end

  --- Turns the AI On for the @{Wrapper.Group} when spawning.
  -- @param #SPAWN self
  -- @return #SPAWN The SPAWN object
  function SPAWN:InitAIOn()

    return self:InitAIOnOff( true )
  end

  --- Turns the AI Off for the @{Wrapper.Group} when spawning.
  -- @param #SPAWN self
  -- @return #SPAWN The SPAWN object
  function SPAWN:InitAIOff()

    return self:InitAIOnOff( false )
  end

end -- AI methods

do -- Delay methods
  --- Turns the Delay On or Off for the first @{Wrapper.Group} scheduled spawning.
  -- The default value is that for scheduled spawning, there is an initial delay when spawning the first @{Wrapper.Group}.
  -- @param #SPAWN self
  -- @param #boolean DelayOnOff A value of true sets the Delay On, a value of false sets the Delay Off.
  -- @return #SPAWN The SPAWN object
  function SPAWN:InitDelayOnOff( DelayOnOff )

    self.DelayOnOff = DelayOnOff
    return self
  end

  --- Turns the Delay On for the @{Wrapper.Group} when spawning with @{#SpawnScheduled}(). In effect then the 1st group will only be spawned
  -- after the number of seconds given in SpawnScheduled as arguments, and not immediately.
  -- @param #SPAWN self
  -- @return #SPAWN The SPAWN object
  function SPAWN:InitDelayOn()

    return self:InitDelayOnOff( true )
  end

  --- Turns the Delay Off for the @{Wrapper.Group} when spawning.
  -- @param #SPAWN self
  -- @return #SPAWN The SPAWN object
  function SPAWN:InitDelayOff()

    return self:InitDelayOnOff( false )
  end

end -- Delay methods

--- Hide the group on the map view (visible to game master slots!).
-- @param #SPAWN self
-- @param #boolean OnOff Defaults to true
-- @return #SPAWN The SPAWN object
function SPAWN:InitHiddenOnMap(OnOff)
  self.SpawnHiddenOnMap = OnOff == false and false or true
  return self
end

--- Hide the group on MFDs (visible to game master slots!).
-- @param #SPAWN self
-- @return #SPAWN The SPAWN object
function SPAWN:InitHiddenOnMFD()
  self.SpawnHiddenOnMFD = true
  return self
end

--- Hide the group on planner (visible to game master slots!).
-- @param #SPAWN self
-- @return #SPAWN The SPAWN object
function SPAWN:InitHiddenOnPlanner()
  self.SpawnHiddenOnPlanner = true
  return self
end

--- Will spawn a group based on the internal index.
-- Note: This method uses the global _DATABASE object (an instance of @{Core.Database#DATABASE}), which contains ALL initial and new spawned objects in MOOSE.
-- @param #SPAWN self
-- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions.
function SPAWN:Spawn()
  --self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, self.AliveUnits } )

  if self.SpawnInitAirbase then
    return self:SpawnAtAirbase( self.SpawnInitAirbase, self.SpawnInitTakeoff, nil, self.SpawnInitTerminalType )
  else
    return self:SpawnWithIndex( self.SpawnIndex + 1 )
  end

end

--- Will re-spawn a group based on a given index.
-- Note: This method uses the global _DATABASE object (an instance of @{Core.Database#DATABASE}), which contains ALL initial and new spawned objects in MOOSE.
-- @param #SPAWN self
-- @param #string SpawnIndex The index of the group to be spawned.
-- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions.
function SPAWN:ReSpawn( SpawnIndex )
  --self:F( { self.SpawnTemplatePrefix, SpawnIndex } )

  if not SpawnIndex then
    SpawnIndex = 1
  end

  -- TODO: This logic makes DCS crash and i don't know why (yet). -- ED (Pikes -- not in the least bit scary to see this, right?)
  local SpawnGroup = self:GetGroupFromIndex( SpawnIndex )
  local WayPoints = SpawnGroup and SpawnGroup.WayPoints or nil
  if SpawnGroup then
    local SpawnDCSGroup = SpawnGroup:GetDCSObject()
    if SpawnDCSGroup then
      SpawnGroup:Destroy()
    end
  end

  local SpawnGroup = self:SpawnWithIndex( SpawnIndex )
  if SpawnGroup and WayPoints then
    -- If there were WayPoints set, then Re-Execute those WayPoints!
    SpawnGroup:WayPointInitialize( WayPoints )
    SpawnGroup:WayPointExecute( 1, 5 )
  end

  if SpawnGroup and SpawnGroup.ReSpawnFunction then
    SpawnGroup:ReSpawnFunction()
  end

  if SpawnGroup then SpawnGroup:ResetEvents() end

  return SpawnGroup
end

--- Set the spawn index to a specified index number.
-- This method can be used to "reset" the spawn counter to a specific index number.
-- This will actually enable a respawn of groups from the specific index.
-- @param #SPAWN self
-- @param #string SpawnIndex The index of the group from where the spawning will start again. The default value would be 0, which means a complete reset of the spawnindex.
-- @return #SPAWN self
function SPAWN:SetSpawnIndex( SpawnIndex )
  self.SpawnIndex = SpawnIndex or 0
end

--- Will spawn a group with a specified index number.
-- Note: This method uses the global _DATABASE object (an instance of @{Core.Database#DATABASE}), which contains ALL initial and new spawned objects in MOOSE.
-- @param #SPAWN self
-- @param #string SpawnIndex The index of the group to be spawned.
-- @return Wrapper.Group#GROUP The group that was spawned. You can use this group for further actions.
function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth )
  --[[
  local set = SET_GROUP:New():FilterAlive():FilterPrefixes({self.SpawnTemplatePrefix, self.SpawnAliasPrefix}):FilterOnce()
  local aliveunits = 0
  set:ForEachGroupAlive(
  function(grp)
    aliveunits = aliveunits + grp:CountAliveUnits()
  end
  )
  
  if aliveunits ~= self.AliveUnits then 
    self.AliveUnits = aliveunits
    --self:T2("***** self.AliveUnits accounting failure! Corrected! *****") 
  end
  
  set= nil
  --]]
  --self:T2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } )
  
  if self:_GetSpawnIndex( SpawnIndex ) then
    
    if self.SpawnFromNewPosition then
     self:_SetInitialPosition( SpawnIndex )
    end
      
      
    if self.SpawnGroups[self.SpawnIndex].Visible then
      self.SpawnGroups[self.SpawnIndex].Group:Activate()
    else

      local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate
      local SpawnZone = self.SpawnGroups[self.SpawnIndex].SpawnZone
      --self:T2( SpawnTemplate.name )

      if SpawnTemplate then

        local PointVec3 = COORDINATE:New( SpawnTemplate.route.points[1].x, SpawnTemplate.route.points[1].alt, SpawnTemplate.route.points[1].y )
        --self:T2( { "Current point of ", self.SpawnTemplatePrefix, PointVec3 } )

        -- If RandomizePosition, then Randomize the formation in the zone band, keeping the template.
        if self.SpawnRandomizePosition then
          local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnRandomizePositionOuterRadius, self.SpawnRandomizePositionInnerRadius )
          local CurrentX = SpawnTemplate.units[1].x
          local CurrentY = SpawnTemplate.units[1].y
          SpawnTemplate.x = RandomVec2.x
          SpawnTemplate.y = RandomVec2.y
          for UnitID = 1, #SpawnTemplate.units do
            SpawnTemplate.units[UnitID].x = SpawnTemplate.units[UnitID].x + (RandomVec2.x - CurrentX)
            SpawnTemplate.units[UnitID].y = SpawnTemplate.units[UnitID].y + (RandomVec2.y - CurrentY)
            --self:T2( 'SpawnTemplate.units[' .. UnitID .. '].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. SpawnTemplate.units[UnitID].y )
          end
        end

        -- If RandomizeUnits, then Randomize the formation at the start point.
        if self.SpawnRandomizeUnits then
          for UnitID = 1, #SpawnTemplate.units do
            local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius )
            if (SpawnZone) then
              local inZone = SpawnZone:IsVec2InZone(RandomVec2)
              local numTries = 1
              while (not inZone) and (numTries < 20) do
                if not inZone then
                  RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius )
                  numTries = numTries + 1
                  inZone = SpawnZone:IsVec2InZone(RandomVec2)
                  --self:T2("Retrying " .. numTries .. "spawn " .. SpawnTemplate.name .. " in Zone " .. SpawnZone:GetName() .. "!")
                  --self:T2(SpawnZone)
                end
              end
              if (not inZone) then
                --self:T2("Could not place unit within zone and within radius!")
                RandomVec2 = SpawnZone:GetRandomVec2()
              end
            end
            SpawnTemplate.units[UnitID].x = RandomVec2.x
            SpawnTemplate.units[UnitID].y = RandomVec2.y
            --self:T2( 'SpawnTemplate.units[' .. UnitID .. '].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. SpawnTemplate.units[UnitID].y )
          end
        end

        -- Get correct heading in Radians.
        local function _Heading( courseDeg )
          local h
          if courseDeg <= 180 then
            h = math.rad( courseDeg )
          else
            h = -math.rad( 360 - courseDeg )
          end
          return h
        end

        local Rad180 = math.rad( 180 )
        local function _HeadingRad( courseRad )
          if courseRad <= Rad180 then
            return courseRad
          else
            return -((2 * Rad180) - courseRad)
          end
        end

        -- Generate a random value somewhere between two floating point values.
        local function _RandomInRange( min, max )
          if min and max then
            return min + (math.random() * (max - min))
          else
            return min
          end
        end

        -- Apply InitGroupHeading rotation if requested.
        -- We do this before InitHeading unit rotation so that can take precedence
        -- NOTE: Does *not* rotate the groups route; only its initial facing.
        if self.SpawnInitGroupHeadingMin and #SpawnTemplate.units > 0 then

          local pivotX = SpawnTemplate.units[1].x -- unit #1 is the pivot point
          local pivotY = SpawnTemplate.units[1].y

          local headingRad = math.rad( _RandomInRange( self.SpawnInitGroupHeadingMin or 0, self.SpawnInitGroupHeadingMax ) )
          local cosHeading = math.cos( headingRad )
          local sinHeading = math.sin( headingRad )

          local unitVarRad = math.rad( self.SpawnInitGroupUnitVar or 0 )

          for UnitID = 1, #SpawnTemplate.units do

            if not self.SpawnRandomizeUnits then
              if UnitID > 1 then -- don't rotate position of unit #1
                local unitXOff = SpawnTemplate.units[UnitID].x - pivotX -- rotate position offset around unit #1
                local unitYOff = SpawnTemplate.units[UnitID].y - pivotY

                SpawnTemplate.units[UnitID].x = pivotX + (unitXOff * cosHeading) - (unitYOff * sinHeading)
                SpawnTemplate.units[UnitID].y = pivotY + (unitYOff * cosHeading) + (unitXOff * sinHeading)
              end
            end

            -- adjust heading of all units, including unit #1
            local unitHeading = SpawnTemplate.units[UnitID].heading + headingRad -- add group rotation to units default rotation
            SpawnTemplate.units[UnitID].heading = _HeadingRad( _RandomInRange( unitHeading - unitVarRad, unitHeading + unitVarRad ) )
            SpawnTemplate.units[UnitID].psi = -SpawnTemplate.units[UnitID].heading

          end

        end

        -- If Heading is given, point all the units towards the given Heading.  Overrides any heading set in InitGroupHeading above.
        if self.SpawnInitHeadingMin then
          for UnitID = 1, #SpawnTemplate.units do
            SpawnTemplate.units[UnitID].heading = _Heading( _RandomInRange( self.SpawnInitHeadingMin, self.SpawnInitHeadingMax ) )
            SpawnTemplate.units[UnitID].psi = -SpawnTemplate.units[UnitID].heading
          end
        end
        
        -- Individual relative unit positions + heading
        if self.SpawnUnitsWithRelativePositions and self.UnitsRelativePositions then
          local BaseX = SpawnTemplate.units[1].x or 0
          local BaseY = SpawnTemplate.units[1].y or 0
          local BaseZ = SpawnTemplate.units[1].z or 0 
          for UnitID = 1, #SpawnTemplate.units do
            if self.UnitsRelativePositions[UnitID].heading then
              SpawnTemplate.units[UnitID].heading = math.rad(self.UnitsRelativePositions[UnitID].heading or 0)
            end
            SpawnTemplate.units[UnitID].x = BaseX + (self.UnitsRelativePositions[UnitID].x or 0)
            SpawnTemplate.units[UnitID].y = BaseY  + (self.UnitsRelativePositions[UnitID].y or 0)
            if self.UnitsRelativePositions[UnitID].z then
              SpawnTemplate.units[UnitID].z =  BaseZ + (self.UnitsRelativePositions[UnitID].z or 0)
            end
          end
        end
        
       -- Individual asbolute unit positions + heading
        if self.SpawnUnitsWithAbsolutePositions and self.UnitsAbsolutePositions then
          for UnitID = 1, #SpawnTemplate.units do
            if self.UnitsAbsolutePositions[UnitID].heading then
              SpawnTemplate.units[UnitID].heading = math.rad(self.UnitsAbsolutePositions[UnitID].heading or 0)
            end
            SpawnTemplate.units[UnitID].x = self.UnitsAbsolutePositions[UnitID].x or 0
            SpawnTemplate.units[UnitID].y = self.UnitsAbsolutePositions[UnitID].y or 0
            if self.UnitsAbsolutePositions[UnitID].z then
              SpawnTemplate.units[UnitID].z = self.UnitsAbsolutePositions[UnitID].z or 0
            end
          end
        end
        
        -- Set livery.
        if self.SpawnInitLivery then
          for UnitID = 1, #SpawnTemplate.units do
            SpawnTemplate.units[UnitID].livery_id = self.SpawnInitLivery
          end
        end

        -- Set skill.
        if self.SpawnInitSkill then
          for UnitID = 1, #SpawnTemplate.units do
            SpawnTemplate.units[UnitID].skill = self.SpawnInitSkill
          end
        end

        -- Set tail number.
        if self.SpawnInitModex then
          for UnitID = 1, #SpawnTemplate.units do
            local modexnumber = string.format( "%03d", self.SpawnInitModex + (UnitID - 1) )
            if self.SpawnInitModexPrefix then modexnumber = self.SpawnInitModexPrefix..modexnumber end
            if self.SpawnInitModexPostfix then modexnumber = modexnumber..self.SpawnInitModexPostfix end
            SpawnTemplate.units[UnitID].onboard_num = modexnumber
          end
        end

        -- Set radio comms on/off.
        if self.SpawnInitRadio then
          SpawnTemplate.communication = self.SpawnInitRadio
        end

        -- Set radio frequency.
        if self.SpawnInitFreq then
          SpawnTemplate.frequency = self.SpawnInitFreq
        end

        -- Set radio modulation.
        if self.SpawnInitModu then
          SpawnTemplate.modulation = self.SpawnInitModu
        end
        
        -- hiding options
        if self.SpawnHiddenOnPlanner then
          SpawnTemplate.hiddenOnPlanner=true
        end

        if self.SpawnHiddenOnMFD then
          SpawnTemplate.hiddenOnMFD=true
        end
        
        if self.SpawnHiddenOnMap then
          SpawnTemplate.hidden=self.SpawnHiddenOnMap
        end

        if self.SpawnValidateAndRepositionGroundUnits then
            local units = SpawnTemplate.units
            local gPos = { x = SpawnTemplate.x, y = SpawnTemplate.y }
            UTILS.ValidateAndRepositionGroundUnits(units, gPos, self.SpawnValidateAndRepositionGroundUnitsRadius, self.SpawnValidateAndRepositionGroundUnitsSpacing)
        end

        -- Set country, coalition and category.
        SpawnTemplate.CategoryID = self.SpawnInitCategory or SpawnTemplate.CategoryID
        SpawnTemplate.CountryID = self.SpawnInitCountry or SpawnTemplate.CountryID
        SpawnTemplate.CoalitionID = self.SpawnInitCoalition or SpawnTemplate.CoalitionID

        --        if SpawnTemplate.CategoryID == Group.Category.HELICOPTER or SpawnTemplate.CategoryID == Group.Category.AIRPLANE then
        --          if SpawnTemplate.route.points[1].type == "TakeOffParking" then
        --            SpawnTemplate.uncontrolled = self.SpawnUnControlled
        --          end
        --        end
      end

      if not NoBirth then
        self:HandleEvent( EVENTS.Birth, self._OnBirth )
      end
      --self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash )
      self:HandleEvent( EVENTS.Crash, self._OnDeadOrCrash )
      self:HandleEvent( EVENTS.UnitLost, self._OnDeadOrCrash )
      self:HandleEvent( EVENTS.RemoveUnit, self._OnDeadOrCrash )
      if self.Repeat then
        self:HandleEvent( EVENTS.Takeoff, self._OnTakeOff )
        self:HandleEvent( EVENTS.Land, self._OnLand )
      end
      if self.RepeatOnEngineShutDown then
        self:HandleEvent( EVENTS.EngineShutdown, self._OnEngineShutDown )
      end

      self.SpawnGroups[self.SpawnIndex].Group = _DATABASE:Spawn( SpawnTemplate )

      local SpawnGroup = self.SpawnGroups[self.SpawnIndex].Group -- Wrapper.Group#GROUP

      -- TODO: Need to check if this function doesn't need to be scheduled, as the group may not be immediately there!
      if SpawnGroup then

        SpawnGroup:SetAIOnOff( self.AIOnOff )
      end

      self:T3( SpawnTemplate.name )

      -- If there is a SpawnFunction hook defined, call it.
      if self.SpawnFunctionHook then
        -- delay calling this for .3 seconds so that it hopefully comes after the BIRTH event of the group.
        self.SpawnHookScheduler:Schedule( nil, self.SpawnFunctionHook, { self.SpawnGroups[self.SpawnIndex].Group, unpack( self.SpawnFunctionArguments ) }, 0.3 )
      end
      -- TODO: Need to fix this by putting an "R" in the name of the group when the group repeats.
      -- if self.Repeat then
      --  _DATABASE:SetStatusGroup( SpawnTemplate.name, "ReSpawn" )
      -- end
    end

    self.SpawnGroups[self.SpawnIndex].Spawned = true
    self.SpawnGroups[self.SpawnIndex].Group.TemplateDonor = self.SpawnTemplatePrefix
    return self.SpawnGroups[self.SpawnIndex].Group
  else
    -- self:E( { self.SpawnTemplatePrefix, "No more Groups to Spawn:", SpawnIndex, self.SpawnMaxGroups } )
  end

  return nil
end

--- Spawns new groups at varying time intervals.
-- This is useful if you want to have continuity within your missions of certain (AI) groups to be present (alive) within your missions.
-- **WARNING** - Setting a very low SpawnTime heavily impacts your mission performance and CPU time, it is NOT useful to check the alive state of an object every split second! Be reasonable and stay at 15 seconds and above!
-- @param #SPAWN self
-- @param #number SpawnTime The time interval defined in seconds between each new spawn of new groups.
-- @param #number SpawnTimeVariation The variation to be applied on the defined time interval between each new spawn.
-- The variation is a number between 0 and 1, representing the % of variation to be applied on the time interval.
-- @param #boolean WithDelay Do not spawn the **first** group immediately, but delay the spawn as per the calculation below.
-- Effectively the same as @{#InitDelayOn}().
-- @return #SPAWN self
-- @usage
-- -- NATO helicopters engaging in the battle field.
-- -- The time interval is set to SPAWN new helicopters between each 600 seconds, with a time variation of 50%.
-- -- The time variation in this case will be between 450 seconds and 750 seconds.
-- -- This is calculated as follows:
-- --      Low limit:   600 * ( 1 - 0.5 / 2 ) = 450
-- --      High limit:  600 * ( 1 + 0.5 / 2 ) = 750
-- -- Between these two values, a random amount of seconds will be chosen for each new spawn of the helicopters.
-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):SpawnScheduled( 600, 0.5 )
function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation, WithDelay )
  --self:F( { SpawnTime, SpawnTimeVariation } )
  
  local SpawnTime = SpawnTime or 60
  local SpawnTimeVariation = SpawnTimeVariation or 0.5
  
  -- Noob catch
  if SpawnTime < 15 then
   self:E("****SPAWN SCHEDULED****\nWARNING - Setting a very low SpawnTime heavily impacts your mission performance and CPU time, it is NOT useful to check the alive state of an object every "..tostring(SpawnTime).." seconds.\nSetting to 15 second intervals.\n*****")
   SpawnTime = 15
  end
  
  if SpawnTimeVariation > 1 or SpawnTimeVariation < 0  then SpawnTimeVariation = 0.5 end
  
  if SpawnTime ~= nil and SpawnTimeVariation ~= nil then
    local InitialDelay = 0
    if WithDelay or self.DelayOnOff == true then
      InitialDelay = math.random( SpawnTime - SpawnTime * SpawnTimeVariation, SpawnTime + SpawnTime * SpawnTimeVariation )
    end
    self.SpawnScheduler = SCHEDULER:New( self, self._Scheduler, {}, InitialDelay, SpawnTime, SpawnTimeVariation )
  end
  
  return self
end

--- Will re-start the spawning scheduler.
-- Note: This method is only required to be called when the schedule was stopped.
-- @param #SPAWN self
-- @return #SPAWN
function SPAWN:SpawnScheduleStart()
  --self:F( { self.SpawnTemplatePrefix } )

  self.SpawnScheduler:Start()
  return self
end

--- Will stop the scheduled spawning scheduler.
-- @param #SPAWN self
-- @return #SPAWN
function SPAWN:SpawnScheduleStop()
  --self:F( { self.SpawnTemplatePrefix } )

  self.SpawnScheduler:Stop()
  return self
end

--- Allows to place a CallFunction hook when a new group spawns.
-- The provided method will be called when a new group is spawned, including its given parameters.
-- The first parameter of the SpawnFunction is the @{Wrapper.Group#GROUP} that was spawned.
-- @param #SPAWN self
-- @param #function SpawnCallBackFunction The function to be called when a group spawns.
-- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns.
-- @return #SPAWN
-- @usage
--
--    -- Declare SpawnObject and call a function when a new Group is spawned.
--    local SpawnObject = SPAWN:New( "SpawnObject" )
--                             :InitLimit( 2, 10 )
--                             :OnSpawnGroup( function( SpawnGroup )
--                                 SpawnGroup:E( "I am spawned" )
--                                 end
--                               )
--                             :SpawnScheduled( 300, 0.3 )
--
function SPAWN:OnSpawnGroup( SpawnCallBackFunction, ... )
  --self:F( "OnSpawnGroup" )

  self.SpawnFunctionHook = SpawnCallBackFunction
  self.SpawnFunctionArguments = {}
  if arg then
    self.SpawnFunctionArguments = arg
  end

  return self
end

--- Will spawn a group at an @{Wrapper.Airbase}.
-- This method is mostly advisable to be used if you want to simulate spawning units at an airbase.
-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn.
-- You can use the returned group to further define the route to be followed.
--
-- The @{Wrapper.Airbase#AIRBASE} object must refer to a valid airbase known in the sim.
-- You can use the following enumerations to search for the pre-defined airbases on the current known maps of DCS:
--
--   * @{Wrapper.Airbase#AIRBASE.Caucasus}: The airbases on the Caucasus map.
--   * @{Wrapper.Airbase#AIRBASE.Nevada}: The airbases on the Nevada (NTTR) map.
--   * @{Wrapper.Airbase#AIRBASE.Normandy}: The airbases on the Normandy map.
--
-- Use the method @{Wrapper.Airbase#AIRBASE.FindByName}() to retrieve the airbase object.
-- The known AIRBASE objects are automatically imported at mission start by MOOSE.
-- Therefore, there isn't any New() constructor defined for AIRBASE objects.
--
-- Ships and FARPs are added within the mission, and are therefore not known.
-- For these AIRBASE objects, there isn't an @{Wrapper.Airbase#AIRBASE} enumeration defined.
-- You need to provide the **exact name** of the airbase as the parameter to the @{Wrapper.Airbase#AIRBASE.FindByName}() method!
--
-- @param #SPAWN self
-- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group.
-- @param #SPAWN.Takeoff Takeoff (optional) The location and takeoff method. Default is Hot.
-- @param #number TakeoffAltitude (optional) The altitude above the ground.
-- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}.
-- @param #boolean EmergencyAirSpawn (optional) If true (default), groups are spawned in air if there is no parking spot at the airbase. If false, nothing is spawned if no parking spot is available.
-- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactly these spots!
-- @return Wrapper.Group#GROUP The group that was spawned or nil when nothing was spawned.
-- @usage
--
--   Spawn_Plane = SPAWN:New( "Plane" )
--   Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Cold )
--   Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Hot )
--   Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Runway )
--
--   Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( "Carrier" ), SPAWN.Takeoff.Cold )
--
--   Spawn_Heli = SPAWN:New( "Heli")
--
--   Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Cold" ), SPAWN.Takeoff.Cold )
--   Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Hot" ), SPAWN.Takeoff.Hot )
--   Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Runway" ), SPAWN.Takeoff.Runway )
--   Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "FARP Air" ), SPAWN.Takeoff.Air )
--
--   Spawn_Heli:SpawnAtAirbase( AIRBASE:FindByName( "Carrier" ), SPAWN.Takeoff.Cold )
--
--   Spawn_Plane:SpawnAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), SPAWN.Takeoff.Cold, nil, AIRBASE.TerminalType.OpenBig )
--
function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalType, EmergencyAirSpawn, Parkingdata )

  -- Get position of airbase.
  local PointVec3 = SpawnAirbase:GetCoordinate()

  -- Set take off type. Default is hot.
  Takeoff = Takeoff or SPAWN.Takeoff.Hot

  -- By default, groups are spawned in air if no parking spot is available.
  if EmergencyAirSpawn == nil then
    EmergencyAirSpawn = true
  end

  if self:_GetSpawnIndex( self.SpawnIndex + 1 ) then

    -- Get group template.
    local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate

    if SpawnTemplate then

      -- Template group, unit and its attributes.
      local group = GROUP:FindByName( self.SpawnTemplatePrefix )
      local unit  = group:GetUnit( 1 )

      -- General category of spawned group.
      local istransport = group:HasAttribute( "Transports" ) and group:HasAttribute( "Planes" )
      local isawacs = group:HasAttribute( "AWACS" )
      local isfighter = group:HasAttribute( "Fighters" ) or group:HasAttribute( "Interceptors" ) or group:HasAttribute( "Multirole fighters" ) or (group:HasAttribute( "Bombers" ) and not group:HasAttribute( "Strategic bombers" ))
      local isbomber = group:HasAttribute( "Strategic bombers" )
      local istanker = group:HasAttribute( "Tankers" )
      local ishelo = unit:HasAttribute( "Helicopters" )

      -- Number of units in the group. With grouping this can actually differ from the template group size!
      local nunits = #SpawnTemplate.units

      -- First waypoint of the group.
      local SpawnPoint = SpawnTemplate.route.points[1]

      -- These are only for ships and FARPS.
      SpawnPoint.linkUnit = nil
      SpawnPoint.helipadId = nil
      SpawnPoint.airdromeId = nil

      -- Get airbase ID and category.
      local AirbaseID = SpawnAirbase:GetID()
      local AirbaseCategory = SpawnAirbase:GetAirbaseCategory()
      --self:F( { AirbaseCategory = AirbaseCategory } )

      -- Set airdrome ID. For helipads and ships we need to add the helipad ID and linked unit.
      -- Note, it is important not to set the airdrome ID for at least ships, because spawn will happen at origin of the map
      if AirbaseCategory == Airbase.Category.SHIP then
        SpawnPoint.linkUnit = AirbaseID
        SpawnPoint.helipadId = AirbaseID
      elseif AirbaseCategory == Airbase.Category.HELIPAD then
        SpawnPoint.linkUnit = AirbaseID
        SpawnPoint.helipadId = AirbaseID
      else
        SpawnPoint.airdromeId = AirbaseID
      end

      -- Set waypoint type/action.
      SpawnPoint.alt    = 0
      SpawnPoint.type   = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type
      SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action

      -- Check if we spawn on ground. 
      local spawnonground = not (Takeoff == SPAWN.Takeoff.Air)

      -- Check where we actually spawn if we spawn on ground.
      local autoparking=false
      if SpawnAirbase.isAirdrome then
        autoparking=false
      else
        autoparking=true
      end

      -- Array with parking spots coordinates.
      local parkingspots = {}
      local parkingindex = {}
      local spots

      -- Spawn happens on ground, i.e. at an airbase, a FARP or a ship.
      if spawnonground and not SpawnTemplate.parked then

        -- Number of free parking spots.
        local nfree = 0

        -- Set terminal type.
        local termtype = TerminalType
        if Takeoff==SPAWN.Takeoff.Runway then
          if SpawnAirbase.isShip then
            -- Looks like there are no runway spawn spots on the stennis!
            if ishelo then
              termtype = AIRBASE.TerminalType.HelicopterUsable
            else
              termtype = AIRBASE.TerminalType.OpenMedOrBig
            end
          else
            termtype = AIRBASE.TerminalType.Runway
          end
        end

        -- Scan options. Might make that input somehow.
        local scanradius = 50
        local scanunits = true
        local scanstatics = true
        local scanscenery = false
        local verysafe = false

        -- Number of free parking spots at the airbase.
        if autoparking then
          -- These places work procedural and have some kind of build in queue ==> Less effort.
          nfree = SpawnAirbase:GetFreeParkingSpotsNumber( termtype, true )
          spots = SpawnAirbase:GetFreeParkingSpotsTable( termtype, true )
        elseif Parkingdata~=nil then
          -- Parking data explicitly set by user as input parameter. (This was commented out for some unknown reason. But I need it this way.)
          nfree=#Parkingdata
          spots=Parkingdata
        else
          if ishelo then
            if termtype == nil then
              -- Helo is spawned. Try exclusive helo spots first.
              --self:T2( string.format( "Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly ) )
              spots = SpawnAirbase:FindFreeParkingSpotForAircraft( group, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
              nfree = #spots
              if nfree < nunits then
                -- Not enough helo ports. Let's try also other terminal types.
                --self:T2( string.format( "Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterUsable ) )
                spots = SpawnAirbase:FindFreeParkingSpotForAircraft( group, AIRBASE.TerminalType.HelicopterUsable, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
                nfree = #spots
              end
            else
              -- No terminal type specified. We try all spots except shelters.
              --self:T2( string.format( "Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), termtype ) )
              spots = SpawnAirbase:FindFreeParkingSpotForAircraft( group, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
              nfree = #spots
            end
          else
            -- Fixed wing aircraft is spawned.
            if termtype == nil then
              if isbomber or istransport or istanker or isawacs then
                -- First we fill the potentially bigger spots.
                --self:T2( string.format( "Transport/bomber group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.OpenBig ) )
                spots = SpawnAirbase:FindFreeParkingSpotForAircraft( group, AIRBASE.TerminalType.OpenBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
                nfree = #spots
                if nfree < nunits then
                  -- Now we try the smaller ones.
                  --self:T2( string.format( "Transport/bomber group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.OpenMedOrBig ) )
                  spots = SpawnAirbase:FindFreeParkingSpotForAircraft( group, AIRBASE.TerminalType.OpenMedOrBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
                  nfree = #spots
                end
              else
                --self:T2( string.format( "Fighter group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.FighterAircraft ) )
                spots = SpawnAirbase:FindFreeParkingSpotForAircraft( group, AIRBASE.TerminalType.FighterAircraft, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
                nfree = #spots
              end
            else
              -- Terminal type explicitly given.
              --self:T2( string.format( "Plane group %s is at %s using terminal type %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), tostring( termtype ) ) )
              spots = SpawnAirbase:FindFreeParkingSpotForAircraft( group, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
              nfree = #spots
            end
          end
        end

        -- Set this to true if not enough spots are available for emergency air start.
        local _notenough = false

        -- Need to differentiate some cases again.
        if autoparking then

          -- On free spot required in these cases.
          if nfree >= 1 then

            -- All units get the same spot. DCS takes care of the rest.
            for i = 1, nunits do
              table.insert( parkingspots, spots[1].Coordinate )
              table.insert( parkingindex, spots[1].TerminalID )
            end
            -- This is actually used...
            PointVec3 = spots[1].Coordinate

          else
            -- If there is absolutely no spot ==> air start!
            _notenough = true
          end

        else

          if nfree >= nunits then

            for i = 1, nunits do
              table.insert( parkingspots, spots[i].Coordinate )
              table.insert( parkingindex, spots[i].TerminalID )
            end

          else
            -- Not enough spots for the whole group ==> air start!
            _notenough = true
          end
        end

        -- Not enough spots ==> Prepare airstart.
        if _notenough then

          if EmergencyAirSpawn and not self.SpawnUnControlled then
            self:E( string.format( "WARNING: Group %s has no parking spots at %s ==> air start!", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) )

            -- Not enough parking spots at the airport ==> Spawn in air.
            autoparking=false

            -- Set waypoint type/action to turning point.
            SpawnPoint.type   = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][1] -- type   = Turning Point
            SpawnPoint.action = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][2] -- action = Turning Point

            -- Adjust altitude to be 500-1000 m above the airbase.
            PointVec3.x = PointVec3.x + math.random( -500, 500 )
            PointVec3.z = PointVec3.z + math.random( -500, 500 )
            if ishelo then
              PointVec3.y = PointVec3:GetLandHeight() + math.random( 100, 1000 )
            else
              -- Randomize position so that multiple AC wont be spawned on top even in air.
              PointVec3.y = PointVec3:GetLandHeight() + math.random( 500, 2500 )
            end

            Takeoff = GROUP.Takeoff.Air
          else
            self:E( string.format( "WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) )
            return nil
          end
        end

      else

        -- Air start requested initially ==> Set altitude. 
        if TakeoffAltitude then
          PointVec3.y = TakeoffAltitude
        else
          if ishelo then
            PointVec3.y = PointVec3:GetLandHeight() + math.random( 100, 1000 )
          else
            -- Randomize position so that multiple AC wont be spawned on top even in air.
            PointVec3.y = PointVec3:GetLandHeight() + math.random( 500, 2500 )
          end
        end

      end

      if not SpawnTemplate.parked then
        -- Translate the position of the Group Template to the Vec3.

        SpawnTemplate.parked = true

        for UnitID = 1, nunits do

          -- Template of the current unit.
          local UnitTemplate = SpawnTemplate.units[UnitID]

          -- Tranlate position and preserve the relative position/formation of all aircraft.  
          local SX = UnitTemplate.x
          local SY = UnitTemplate.y
          local BX = SpawnTemplate.route.points[1].x
          local BY = SpawnTemplate.route.points[1].y
          local TX = PointVec3.x + (SX - BX)
          local TY = PointVec3.z + (SY - BY)

          if spawnonground then

            -- Ships and FARPS seem to have a build in queue.
            if autoparking then

              -- Spawn on ship. We take only the position of the ship.
              SpawnTemplate.units[UnitID].x = PointVec3.x -- TX
              SpawnTemplate.units[UnitID].y = PointVec3.z -- TY
              SpawnTemplate.units[UnitID].alt = PointVec3.y

            else

              -- Get coordinates of parking spot.
              SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x
              SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z
              SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y

            end

          else

            -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set.
            SpawnTemplate.units[UnitID].x = TX
            SpawnTemplate.units[UnitID].y = TY
            SpawnTemplate.units[UnitID].alt = PointVec3.y

          end

          -- Parking spot id.
          UnitTemplate.parking = nil
          UnitTemplate.parking_id = nil
          if parkingindex[UnitID] then
            UnitTemplate.parking = parkingindex[UnitID]
          end
        end
      end

      -- Set gereral spawnpoint position.
      SpawnPoint.x = PointVec3.x
      SpawnPoint.y = PointVec3.z
      SpawnPoint.alt = PointVec3.y

      SpawnTemplate.x = PointVec3.x
      SpawnTemplate.y = PointVec3.z

      SpawnTemplate.uncontrolled = self.SpawnUnControlled

      -- Spawn group.
      local GroupSpawned = self:SpawnWithIndex( self.SpawnIndex )

      -- When spawned in the air, we need to generate a Takeoff Event.
      if Takeoff == GROUP.Takeoff.Air then
        for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do
          --SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() }, 5 )  --No need to create a new SCHEDULER instance every time!
          self:ScheduleOnce(5, BASE.CreateEventTakeoff, {GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject()})
        end
      end

      -- Check if we accidentally spawned on the runway. Needs to be schedules, because group is not immidiately alive.
      --if Takeoff ~= SPAWN.Takeoff.Runway and Takeoff ~= SPAWN.Takeoff.Air and spawnonairport then
      --  SCHEDULER:New( nil, AIRBASE.CheckOnRunWay, { SpawnAirbase, GroupSpawned, 75, true }, 1.0 )
      --end

      return GroupSpawned
    end
  end

  return nil
end

--- Spawn a group on an @{Wrapper.Airbase} at a specific parking spot.
-- @param #SPAWN self
-- @param Wrapper.Airbase#AIRBASE Airbase The @{Wrapper.Airbase} where to spawn the group.
-- @param #table Spots Table of parking spot IDs. Note that these in general are different from the numbering in the mission editor!
-- @param #SPAWN.Takeoff Takeoff (Optional) Takeoff type, i.e. either SPAWN.Takeoff.Cold or SPAWN.Takeoff.Hot. Default is Hot.
-- @return Wrapper.Group#GROUP The group that was spawned or nil when nothing was spawned.
function SPAWN:SpawnAtParkingSpot( Airbase, Spots, Takeoff )
  --self:F( { Airbase = Airbase, Spots = Spots, Takeoff = Takeoff } )
  
  -- Ensure that Spots parameter is a table.
  if type( Spots ) ~= "table" then
    Spots = { Spots }
  end
  
  if type(Airbase) == "string" then
    Airbase = AIRBASE:FindByName(Airbase)
  end
  
  -- Get template group.
  local group = GROUP:FindByName( self.SpawnTemplatePrefix )

  -- Get number of units in group.
  local nunits = self.SpawnGrouping or #group:GetUnits()

  -- Quick check.
  if nunits then

    -- Check that number of provided parking spots is large enough.
    if #Spots < nunits then
      self:E( "ERROR: Number of provided parking spots is less than number of units in group!" )
      return nil
    end

    -- Table of parking data.
    local Parkingdata = {}

    -- Loop over provided Terminal IDs.
    for _, TerminalID in pairs( Spots ) do

      -- Get parking spot data.
      local spot = Airbase:GetParkingSpotData( TerminalID )

      --self:T2( { spot = spot } )

      if spot and spot.Free then
        --self:T2( string.format( "Adding parking spot ID=%d TermType=%d", spot.TerminalID, spot.TerminalType ) )
        table.insert( Parkingdata, spot )
      end

    end

    if #Parkingdata >= nunits then
      return self:SpawnAtAirbase( Airbase, Takeoff, nil, nil, nil, Parkingdata )
    else
      self:E( "ERROR: Could not find enough free parking spots!" )
    end

  else
    self:E( "ERROR: Could not get number of units in group!" )
  end

  return nil
end

--- Will park a group at an @{Wrapper.Airbase}.
--
-- @param #SPAWN self
-- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group.
-- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}.
-- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactily these spots!
-- @return #nil Nothing is returned!
function SPAWN:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex )

  --self:F( { SpawnIndex = SpawnIndex, SpawnMaxGroups = self.SpawnMaxGroups } )

  -- Get position of airbase.
  local PointVec3 = SpawnAirbase:GetCoordinate()
  --self:T2( PointVec3 )

  -- Set take off type. Default is hot.
  local Takeoff = SPAWN.Takeoff.Cold

  -- Get group template.
  local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate

  if SpawnTemplate then

    -- Check if the aircraft with the specified SpawnIndex is already spawned.
    -- If yes, ensure that the aircraft is spawned at the same aircraft spot.

    local GroupAlive = self:GetGroupFromIndex( SpawnIndex )

    -- Debug output
    --self:T2( { "Current point of ", self.SpawnTemplatePrefix, SpawnAirbase } )

    -- Template group, unit and its attributes.
    local TemplateGroup = GROUP:FindByName( self.SpawnTemplatePrefix )
    local TemplateUnit = TemplateGroup:GetUnit( 1 )
    local ishelo = TemplateUnit:HasAttribute( "Helicopters" )
    local isbomber = TemplateUnit:HasAttribute( "Bombers" )
    local istransport = TemplateUnit:HasAttribute( "Transports" )
    local isfighter = TemplateUnit:HasAttribute( "Battleplanes" )

    -- Number of units in the group. With grouping this can actually differ from the template group size!
    local nunits = #SpawnTemplate.units

    -- First waypoint of the group.
    local SpawnPoint = SpawnTemplate.route.points[1]

    -- These are only for ships and FARPS.
    SpawnPoint.linkUnit = nil
    SpawnPoint.helipadId = nil
    SpawnPoint.airdromeId = nil

    -- Get airbase ID and category.
    local AirbaseID = SpawnAirbase:GetID()
    local AirbaseCategory = SpawnAirbase:GetAirbaseCategory()
    --self:F( { AirbaseCategory = AirbaseCategory } )

    -- Set airdromeId.
    if AirbaseCategory == Airbase.Category.SHIP then
      SpawnPoint.linkUnit = AirbaseID
      SpawnPoint.helipadId = AirbaseID
    elseif AirbaseCategory == Airbase.Category.HELIPAD then
      SpawnPoint.linkUnit = AirbaseID
      SpawnPoint.helipadId = AirbaseID
    elseif AirbaseCategory == Airbase.Category.AIRDROME then
      SpawnPoint.airdromeId = AirbaseID
    end

    -- Set waypoint type/action.
    SpawnPoint.alt = 0
    SpawnPoint.type = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type
    SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action

    -- Check if we spawn on ground. 
    local spawnonground = not (Takeoff == SPAWN.Takeoff.Air)
    --self:T2( { spawnonground = spawnonground, TOtype = Takeoff, TOair = Takeoff == SPAWN.Takeoff.Air } )

    -- Check where we actually spawn if we spawn on ground.
    local spawnonship = false
    local spawnonfarp = false
    local spawnonrunway = false
    local spawnonairport = false
    if spawnonground then
      if AirbaseCategory == Airbase.Category.SHIP then
        spawnonship = true
      elseif AirbaseCategory == Airbase.Category.HELIPAD then
        spawnonfarp = true
      elseif AirbaseCategory == Airbase.Category.AIRDROME then
        spawnonairport = true
      end
      spawnonrunway = Takeoff == SPAWN.Takeoff.Runway
    end

    -- Array with parking spots coordinates.
    local parkingspots = {}
    local parkingindex = {}
    local spots

    -- Spawn happens on ground, i.e. at an airbase, a FARP or a ship.
    if spawnonground and not SpawnTemplate.parked then

      -- Number of free parking spots.
      local nfree = 0

      -- Set terminal type.
      local termtype = TerminalType

      -- Scan options. Might make that input somehow.
      local scanradius = 50
      local scanunits = true
      local scanstatics = true
      local scanscenery = false
      local verysafe = false

      -- Number of free parking spots at the airbase.
      if spawnonship or spawnonfarp or spawnonrunway then
        -- These places work procedural and have some kind of build in queue ==> Less effort.
        --self:T2( string.format( "Group %s is spawned on farp/ship/runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) )
        nfree = SpawnAirbase:GetFreeParkingSpotsNumber( termtype, true )
        spots = SpawnAirbase:GetFreeParkingSpotsTable( termtype, true )
        --[[
      elseif Parkingdata~=nil then
        -- Parking data explicitly set by user as input parameter.
        nfree=#Parkingdata
        spots=Parkingdata
      ]]
      else
        if ishelo then
          if termtype == nil then
            -- Helo is spawned. Try exclusive helo spots first.
            --self:T2( string.format( "Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterOnly ) )
            spots = SpawnAirbase:FindFreeParkingSpotForAircraft( TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
            nfree = #spots
            if nfree < nunits then
              -- Not enough helo ports. Let's try also other terminal types.
              --self:T2( string.format( "Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.HelicopterUsable ) )
              spots = SpawnAirbase:FindFreeParkingSpotForAircraft( TemplateGroup, AIRBASE.TerminalType.HelicopterUsable, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
              nfree = #spots
            end
          else
            -- No terminal type specified. We try all spots except shelters.
            --self:T2( string.format( "Helo group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), termtype ) )
            spots = SpawnAirbase:FindFreeParkingSpotForAircraft( TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
            nfree = #spots
          end
        else
          -- Fixed wing aircraft is spawned.
          if termtype == nil then
            -- TODO: Add some default cases for transport, bombers etc. if no explicit terminal type is provided.
            -- TODO: We don't want Bombers to spawn in shelters. But I don't know a good attribute for just fighers.
            -- TODO: Some attributes are "Helicopters", "Bombers", "Transports", "Battleplanes". Need to check it out.
            if isbomber or istransport then
              -- First we fill the potentially bigger spots.
              --self:T2( string.format( "Transport/bomber group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.OpenBig ) )
              spots = SpawnAirbase:FindFreeParkingSpotForAircraft( TemplateGroup, AIRBASE.TerminalType.OpenBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
              nfree = #spots
              if nfree < nunits then
                -- Now we try the smaller ones.
                --self:T2( string.format( "Transport/bomber group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.OpenMedOrBig ) )
                spots = SpawnAirbase:FindFreeParkingSpotForAircraft( TemplateGroup, AIRBASE.TerminalType.OpenMedOrBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
                nfree = #spots
              end
            else
              --self:T2( string.format( "Fighter group %s is at %s using terminal type %d.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), AIRBASE.TerminalType.FighterAircraft ) )
              spots = SpawnAirbase:FindFreeParkingSpotForAircraft( TemplateGroup, AIRBASE.TerminalType.FighterAircraft, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
              nfree = #spots
            end
          else
            -- Terminal type explicitly given.
            --self:T2( string.format( "Plane group %s is at %s using terminal type %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), tostring( termtype ) ) )
            spots = SpawnAirbase:FindFreeParkingSpotForAircraft( TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits, Parkingdata )
            nfree = #spots
          end
        end
      end

      -- Debug: Get parking data.
      --[[
      local parkingdata=SpawnAirbase:GetParkingSpotsTable(termtype)
      --self:T2(string.format("Parking at %s, terminal type %s:", SpawnAirbase:GetName(), tostring(termtype)))
      for _,_spot in pairs(parkingdata) do
        --self:T2(string.format("%s, Termin Index = %3d, Term Type = %03d, Free = %5s, TOAC = %5s, Term ID0 = %3d, Dist2Rwy = %4d",
        SpawnAirbase:GetName(), _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy))
      end
      --self:T2(string.format("%s at %s: free parking spots = %d - number of units = %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), nfree, nunits))
      ]]

      -- Set this to true if not enough spots are available for emergency air start.
      local _notenough = false

      -- Need to differentiate some cases again.
      if spawnonship or spawnonfarp or spawnonrunway then

        -- On free spot required in these cases. 
        if nfree >= 1 then

          -- All units get the same spot. DCS takes care of the rest.
          for i = 1, nunits do
            table.insert( parkingspots, spots[1].Coordinate )
            table.insert( parkingindex, spots[1].TerminalID )
          end
          -- This is actually used...
          PointVec3 = spots[1].Coordinate

        else
          -- If there is absolutely no spot ==> air start!
          _notenough = true
        end

      elseif spawnonairport then

        if nfree >= nunits then

          for i = 1, nunits do
            table.insert( parkingspots, spots[i].Coordinate )
            table.insert( parkingindex, spots[i].TerminalID )
          end

        else
          -- Not enough spots for the whole group ==> air start!
          _notenough = true
        end
      end

      -- Not enough spots ==> Prepare airstart.
      if _notenough then

        if not self.SpawnUnControlled then
        else
          self:E( string.format( "WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) )
          return nil
        end
      end

    else

    end

    if not SpawnTemplate.parked then
      -- Translate the position of the Group Template to the Vec3.

      SpawnTemplate.parked = true

      for UnitID = 1, nunits do
        --self:F( 'Before Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. SpawnTemplate.units[UnitID].y )

        -- Template of the current unit.
        local UnitTemplate = SpawnTemplate.units[UnitID]

        -- Tranlate position and preserve the relative position/formation of all aircraft.  
        local SX = UnitTemplate.x
        local SY = UnitTemplate.y
        local BX = SpawnTemplate.route.points[1].x
        local BY = SpawnTemplate.route.points[1].y
        local TX = PointVec3.x + (SX - BX)
        local TY = PointVec3.z + (SY - BY)

        if spawnonground then

          -- Ships and FARPS seem to have a build in queue.
          if spawnonship or spawnonfarp or spawnonrunway then

            --self:T2( string.format( "Group %s spawning at farp, ship or runway %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) )

            -- Spawn on ship. We take only the position of the ship.
            SpawnTemplate.units[UnitID].x = PointVec3.x -- TX
            SpawnTemplate.units[UnitID].y = PointVec3.z -- TY
            SpawnTemplate.units[UnitID].alt = PointVec3.y

          else

            --self:T2( string.format( "Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID] ) )

            -- Get coordinates of parking spot.
            SpawnTemplate.units[UnitID].x = parkingspots[UnitID].x
            SpawnTemplate.units[UnitID].y = parkingspots[UnitID].z
            SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y

            -- parkingspots[UnitID]:MarkToAll(string.format("Group %s spawning at airbase %s on parking spot id %d", self.SpawnTemplatePrefix, SpawnAirbase:GetName(), parkingindex[UnitID]))
          end

        else

          --self:T2( string.format( "Group %s spawning in air at %s.", self.SpawnTemplatePrefix, SpawnAirbase:GetName() ) )

          -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set.
          SpawnTemplate.units[UnitID].x = TX
          SpawnTemplate.units[UnitID].y = TY
          SpawnTemplate.units[UnitID].alt = PointVec3.y

        end

        -- Parking spot id.
        UnitTemplate.parking = nil
        UnitTemplate.parking_id = nil
        if parkingindex[UnitID] then
          UnitTemplate.parking = parkingindex[UnitID]
        end

        -- Debug output.
        --self:T2( string.format( "Group %s unit number %d: Parking    = %s", self.SpawnTemplatePrefix, UnitID, tostring( UnitTemplate.parking ) ) )
        --self:T2( string.format( "Group %s unit number %d: Parking ID = %s", self.SpawnTemplatePrefix, UnitID, tostring( UnitTemplate.parking_id ) ) )
        --self:T2( 'After Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. SpawnTemplate.units[UnitID].y )
      end
    end

    -- Set general spawnpoint position.
    SpawnPoint.x = PointVec3.x
    SpawnPoint.y = PointVec3.z
    SpawnPoint.alt = PointVec3.y

    SpawnTemplate.x = PointVec3.x
    SpawnTemplate.y = PointVec3.z

    SpawnTemplate.uncontrolled = true

    -- Spawn group.
    local GroupSpawned = self:SpawnWithIndex( SpawnIndex, true )

    -- When spawned in the air, we need to generate a Takeoff Event.
    if Takeoff == GROUP.Takeoff.Air then
      for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do
        SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() }, 5 )
      end
    end

    -- Check if we accidentally spawned on the runway. Needs to be schedules, because group is not immidiately alive.
    if Takeoff ~= SPAWN.Takeoff.Runway and Takeoff ~= SPAWN.Takeoff.Air and spawnonairport then
      SCHEDULER:New( nil, AIRBASE.CheckOnRunWay, { SpawnAirbase, GroupSpawned, 75, true }, 1.0 )
    end

  end

end

--- Will park a group at an @{Wrapper.Airbase}.
-- This method is mostly advisable to be used if you want to simulate parking units at an airbase and be visible.
-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn.
--
-- All groups that are in the spawn collection and that are alive, and not in the air, are parked.
--
-- The @{Wrapper.Airbase#AIRBASE} object must refer to a valid airbase known in the sim.
-- You can use the following enumerations to search for the pre-defined airbases on the current known maps of DCS:
--
--   * @{Wrapper.Airbase#AIRBASE.Caucasus}: The airbases on the Caucasus map.
--   * @{Wrapper.Airbase#AIRBASE.Nevada}: The airbases on the Nevada (NTTR) map.
--   * @{Wrapper.Airbase#AIRBASE.Normandy}: The airbases on the Normandy map.
--
-- Use the method @{Wrapper.Airbase#AIRBASE.FindByName}() to retrieve the airbase object.
-- The known AIRBASE objects are automatically imported at mission start by MOOSE.
-- Therefore, there isn't any New() constructor defined for AIRBASE objects.
--
-- Ships and FARPs are added within the mission, and are therefore not known.
-- For these AIRBASE objects, there isn't an @{Wrapper.Airbase#AIRBASE} enumeration defined.
-- You need to provide the **exact name** of the airbase as the parameter to the @{Wrapper.Airbase#AIRBASE.FindByName}() method!
--
-- @param #SPAWN self
-- @param Wrapper.Airbase#AIRBASE SpawnAirbase The @{Wrapper.Airbase} where to spawn the group.
-- @param Wrapper.Airbase#AIRBASE.TerminalType TerminalType (optional) The terminal type the aircraft should be spawned at. See @{Wrapper.Airbase#AIRBASE.TerminalType}.
-- @param #table Parkingdata (optional) Table holding the coordinates and terminal ids for all units of the group. Spawning will be forced to happen at exactily these spots!
-- @return #nil Nothing is returned!
-- @usage
--   Spawn_Plane = SPAWN:New( "Plane" )
--   Spawn_Plane:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ) )
--
--   Spawn_Heli = SPAWN:New( "Heli")
--
--   Spawn_Heli:ParkAtAirbase( AIRBASE:FindByName( "FARP Cold" ) )
--
--   Spawn_Heli:ParkAtAirbase( AIRBASE:FindByName( "Carrier" ) )
--
--   Spawn_Plane:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Krymsk ), AIRBASE.TerminalType.OpenBig )
--
function SPAWN:ParkAtAirbase( SpawnAirbase, TerminalType, Parkingdata ) -- R2.2, R2.4, R2.5
  --self:F( { self.SpawnTemplatePrefix, SpawnAirbase, TerminalType } )

  self:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, 1 )

  for SpawnIndex = 2, self.SpawnMaxGroups do
    self:ParkAircraft( SpawnAirbase, TerminalType, Parkingdata, SpawnIndex )
    -- self:ScheduleOnce( SpawnIndex * 0.1, SPAWN.ParkAircraft, self, SpawnAirbase, TerminalType, Parkingdata, SpawnIndex )
  end

  self:SetSpawnIndex( 0 )

  return nil
end

--- Will spawn a group from a Vec3 in 3D space.
-- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes.
-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn.
-- You can use the returned group to further define the route to be followed.
-- @param #SPAWN self
-- @param DCS#Vec3 Vec3 The Vec3 coordinates where to spawn the group.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
function SPAWN:SpawnFromVec3( Vec3, SpawnIndex )
  --self:F( { self.SpawnTemplatePrefix, Vec3, SpawnIndex } )

  local PointVec3 = COORDINATE:NewFromVec3( Vec3 )
  --self:T2( PointVec3 )

  if SpawnIndex then
  else
    SpawnIndex = self.SpawnIndex + 1
  end

  if self:_GetSpawnIndex( SpawnIndex ) then

    local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate

    if SpawnTemplate then

      --self:T2( { "Current point of ", self.SpawnTemplatePrefix, Vec3 } )

      local TemplateHeight = SpawnTemplate.route and SpawnTemplate.route.points[1].alt or nil

      SpawnTemplate.route = SpawnTemplate.route or {}
      SpawnTemplate.route.points = SpawnTemplate.route.points or {}
      SpawnTemplate.route.points[1] = SpawnTemplate.route.points[1] or {}
      SpawnTemplate.route.points[1].x = SpawnTemplate.route.points[1].x or 0
      SpawnTemplate.route.points[1].y = SpawnTemplate.route.points[1].y or 0

      -- Translate the position of the Group Template to the Vec3.
      for UnitID = 1, #SpawnTemplate.units do
        -- self:T2( 'Before Translation SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y )
        local UnitTemplate = SpawnTemplate.units[UnitID]
        local SX = UnitTemplate.x or 0
        local SY = UnitTemplate.y or 0
        local BX = SpawnTemplate.route.points[1].x
        local BY = SpawnTemplate.route.points[1].y
        local TX = Vec3.x + (SX - BX)
        local TY = Vec3.z + (SY - BY)
        SpawnTemplate.units[UnitID].x = TX
        SpawnTemplate.units[UnitID].y = TY
        if SpawnTemplate.CategoryID ~= Group.Category.SHIP then
          SpawnTemplate.units[UnitID].alt = Vec3.y or TemplateHeight
        end
        --self:T2( 'After Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. SpawnTemplate.units[UnitID].y )
      end
      SpawnTemplate.route.points[1].x = Vec3.x
      SpawnTemplate.route.points[1].y = Vec3.z
      if SpawnTemplate.CategoryID ~= Group.Category.SHIP then
        SpawnTemplate.route.points[1].alt = Vec3.y or TemplateHeight
      end
      SpawnTemplate.x = Vec3.x
      SpawnTemplate.y = Vec3.z
      SpawnTemplate.alt = Vec3.y or TemplateHeight

      return self:SpawnWithIndex( self.SpawnIndex )
    end
  end

  return nil
end

--- Will spawn a group from a Coordinate in 3D space.
-- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes.
-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn.
-- You can use the returned group to further define the route to be followed.
-- @param #SPAWN self
-- @param Core.Point#Coordinate Coordinate The Coordinate coordinates where to spawn the group.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
function SPAWN:SpawnFromCoordinate( Coordinate, SpawnIndex )
  --self:F( { self.SpawnTemplatePrefix, SpawnIndex } )

  return self:SpawnFromVec3( Coordinate:GetVec3(), SpawnIndex )
end

--- Will spawn a group from a PointVec3 in 3D space. 
-- This method is mostly advisable to be used if you want to simulate spawning units in the air, like helicopters or airplanes.
-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn.
-- You can use the returned group to further define the route to be followed.
-- @param #SPAWN self
-- @param Core.Point#COORDINATE PointVec3 The COORDINATE coordinates where to spawn the group.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
-- @usage
--
--   local SpawnPointVec3 = ZONE:New( ZoneName ):GetPointVec3( 2000 ) -- Get the center of the ZONE object at 2000 meters from the ground.
--
--   -- Spawn at the zone center position at 2000 meters from the ground!
--   SpawnAirplanes:SpawnFromPointVec3( SpawnPointVec3 )
--
function SPAWN:SpawnFromPointVec3( PointVec3, SpawnIndex )
  --self:F( { self.SpawnTemplatePrefix, SpawnIndex } )

  return self:SpawnFromVec3( PointVec3:GetVec3(), SpawnIndex )
end

--- Will spawn a group from a Vec2 in 3D space. 
-- This method is mostly advisable to be used if you want to simulate spawning groups on the ground from air units, like vehicles.
-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn.
-- You can use the returned group to further define the route to be followed.
-- @param #SPAWN self
-- @param DCS#Vec2 Vec2 The Vec2 coordinates where to spawn the group.
-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone.
-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
-- @usage
--
--   local SpawnVec2 = ZONE:New( ZoneName ):GetVec2()
--
--   -- Spawn at the zone center position at the height specified in the ME of the group template!
--   SpawnAirplanes:SpawnFromVec2( SpawnVec2 )
--
--   -- Spawn from the static position at the height randomized between 2000 and 4000 meters.
--   SpawnAirplanes:SpawnFromVec2( SpawnVec2, 2000, 4000 )
--
function SPAWN:SpawnFromVec2( Vec2, MinHeight, MaxHeight, SpawnIndex )
  --self:F( { self.SpawnTemplatePrefix, self.SpawnIndex, Vec2, MinHeight, MaxHeight, SpawnIndex } )

  local Height = nil

  if MinHeight and MaxHeight then
    Height = math.random( MinHeight, MaxHeight )
  end

  return self:SpawnFromVec3( { x = Vec2.x, y = Height, z = Vec2.y }, SpawnIndex ) -- y can be nil. In this case, spawn on the ground for vehicles, and in the template altitude for air.
end

--- Will spawn a group from a COORDINATE in 3D space.
-- This method is mostly advisable to be used if you want to simulate spawning groups on the ground from air units, like vehicles.
-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn.
-- You can use the returned group to further define the route to be followed.
-- @param #SPAWN self
-- @param Core.Point#COORDINATE PointVec2 The coordinates where to spawn the group.
-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone.
-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
-- @usage
--
--   local SpawnPointVec2 = ZONE:New( ZoneName ):GetPointVec2() 
--
--   -- Spawn at the zone center position at the height specified in the ME of the group template!
--   SpawnAirplanes:SpawnFromPointVec2( SpawnPointVec2 )
--
--   -- Spawn from the static position at the height randomized between 2000 and 4000 meters.
--   SpawnAirplanes:SpawnFromPointVec2( SpawnPointVec2, 2000, 4000 )
--
function SPAWN:SpawnFromPointVec2( PointVec2, MinHeight, MaxHeight, SpawnIndex )
  --self:F( { self.SpawnTemplatePrefix, self.SpawnIndex } )

  return self:SpawnFromVec2( PointVec2:GetVec2(), MinHeight, MaxHeight, SpawnIndex )
end

--- Will spawn a group from a hosting unit. This method is mostly advisable to be used if you want to simulate spawning from air units, like helicopters, which are dropping infantry into a defined Landing Zone.
-- Note that each point in the route assigned to the spawning group is reset to the point of the spawn.
-- You can use the returned group to further define the route to be followed.
-- @param #SPAWN self
-- @param Wrapper.Unit#UNIT HostUnit The air or ground unit dropping or unloading the group.
-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone.
-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned.
-- @return #nil Nothing was spawned.
-- @usage
--
--   local SpawnStatic = STATIC:FindByName( StaticName )
--
--   -- Spawn from the static position at the height specified in the ME of the group template!
--   SpawnAirplanes:SpawnFromUnit( SpawnStatic )
--
--   -- Spawn from the static position at the height randomized between 2000 and 4000 meters.
--   SpawnAirplanes:SpawnFromUnit( SpawnStatic, 2000, 4000 )
--
function SPAWN:SpawnFromUnit( HostUnit, MinHeight, MaxHeight, SpawnIndex )
  --self:F( { self.SpawnTemplatePrefix, HostUnit, MinHeight, MaxHeight, SpawnIndex } )

  if HostUnit and HostUnit:IsAlive() ~= nil then -- and HostUnit:getUnit(1):inAir() == false then
    return self:SpawnFromVec2( HostUnit:GetVec2(), MinHeight, MaxHeight, SpawnIndex )
  end

  return nil
end

--- Will spawn a group from a hosting static. This method is mostly advisable to be used if you want to simulate spawning from buldings and structures (static buildings).
-- You can use the returned group to further define the route to be followed.
-- @param #SPAWN self
-- @param Wrapper.Static#STATIC HostStatic The static dropping or unloading the group.
-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone.
-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned.
-- @usage
--
--   local SpawnStatic = STATIC:FindByName( StaticName )
--
--   -- Spawn from the static position at the height specified in the ME of the group template!
--   SpawnAirplanes:SpawnFromStatic( SpawnStatic )
--
--   -- Spawn from the static position at the height randomized between 2000 and 4000 meters.
--   SpawnAirplanes:SpawnFromStatic( SpawnStatic, 2000, 4000 )
--
function SPAWN:SpawnFromStatic( HostStatic, MinHeight, MaxHeight, SpawnIndex )
  --self:F( { self.SpawnTemplatePrefix, HostStatic, MinHeight, MaxHeight, SpawnIndex } )

  if HostStatic and HostStatic:IsAlive() then
    return self:SpawnFromVec2( HostStatic:GetVec2(), MinHeight, MaxHeight, SpawnIndex )
  end

  return nil
end

--- Will spawn a Group within a given @{Core.Zone}.
-- The @{Core.Zone} can be of any type derived from @{Core.Zone#ZONE_BASE}.
-- Once the @{Wrapper.Group} is spawned within the zone, the @{Wrapper.Group} will continue on its route.
-- The **first waypoint** (where the group is spawned) is replaced with the zone location coordinates.
-- @param #SPAWN self
-- @param Core.Zone#ZONE Zone The zone where the group is to be spawned.
-- @param #boolean RandomizeGroup (optional) Randomization of the @{Wrapper.Group} position in the zone.
-- @param #number MinHeight (optional) The minimum height to spawn an airborne group into the zone.
-- @param #number MaxHeight (optional) The maximum height to spawn an airborne group into the zone.
-- @param #number SpawnIndex (optional) The index which group to spawn within the given zone.
-- @return Wrapper.Group#GROUP that was spawned or #nil if nothing was spawned. 
-- @usage
--
--   local SpawnZone = ZONE:New( ZoneName )
--
--   -- Spawn at the zone center position at the height specified in the ME of the group template!
--   SpawnAirplanes:SpawnInZone( SpawnZone )
--
--   -- Spawn in the zone at a random position at the height specified in the Me of the group template.
--   SpawnAirplanes:SpawnInZone( SpawnZone, true )
--
--   -- Spawn in the zone at a random position at the height randomized between 2000 and 4000 meters.
--   SpawnAirplanes:SpawnInZone( SpawnZone, true, 2000, 4000 )
--
--   -- Spawn at the zone center position at the height randomized between 2000 and 4000 meters.
--   SpawnAirplanes:SpawnInZone( SpawnZone, false, 2000, 4000 )
--
--   -- Spawn at the zone center position at the height randomized between 2000 and 4000 meters.
--   SpawnAirplanes:SpawnInZone( SpawnZone, nil, 2000, 4000 )
--
function SPAWN:SpawnInZone( Zone, RandomizeGroup, MinHeight, MaxHeight, SpawnIndex )
  --self:F( { self.SpawnTemplatePrefix, Zone, RandomizeGroup, MinHeight, MaxHeight, SpawnIndex } )

  if Zone then
    if RandomizeGroup then
      return self:SpawnFromVec2( Zone:GetRandomVec2(), MinHeight, MaxHeight, SpawnIndex )
    else
      return self:SpawnFromVec2( Zone:GetVec2(), MinHeight, MaxHeight, SpawnIndex )
    end
  end

  return nil
end

--- (**AIR**) Will spawn a plane group in UnControlled or Controlled mode...
-- This will be similar to the uncontrolled flag setting in the ME.
-- You can use UnControlled mode to simulate planes startup and ready for take-off but aren't moving (yet).
-- ReSpawn the plane in Controlled mode, and the plane will move...
-- @param #SPAWN self
-- @param #boolean UnControlled true if UnControlled, false if Controlled.
-- @return #SPAWN self
function SPAWN:InitUnControlled( UnControlled )
  self:F2( { self.SpawnTemplatePrefix, UnControlled } )

  self.SpawnUnControlled = (UnControlled == true) and true or nil

  for SpawnGroupID = 1, self.SpawnMaxGroups do
    self.SpawnGroups[SpawnGroupID].UnControlled = self.SpawnUnControlled
  end

  return self
end

--- Get the Coordinate of the Group that is Late Activated as the template for the SPAWN object.
-- @param #SPAWN self
-- @return Core.Point#COORDINATE The Coordinate
function SPAWN:GetCoordinate()

  local LateGroup = GROUP:FindByName( self.SpawnTemplatePrefix )
  if LateGroup then
    return LateGroup:GetCoordinate()
  end

  return nil
end

--- Will return the SpawnGroupName either with with a specific count number or without any count.
-- @param #SPAWN self
-- @param #number SpawnIndex Is the number of the Group that is to be spawned.
-- @return #string SpawnGroupName
function SPAWN:SpawnGroupName( SpawnIndex )
  --self:F( { self.SpawnTemplatePrefix, SpawnIndex } )

  local SpawnPrefix = self.SpawnTemplatePrefix
  if self.SpawnAliasPrefix then
    SpawnPrefix = self.SpawnAliasPrefix
  end

  if SpawnIndex then
    local SpawnName = string.format( '%s#%03d', SpawnPrefix, SpawnIndex )
    --self:T2( SpawnName )
    return SpawnName
  else
    --self:T2( SpawnPrefix )
    return SpawnPrefix
  end

end

--- Will find the first alive @{Wrapper.Group} it has spawned, and return the alive @{Wrapper.Group} object and the first Index where the first alive @{Wrapper.Group} object has been found.
-- @param #SPAWN self
-- @return Wrapper.Group#GROUP, #number The @{Wrapper.Group} object found, the new Index where the group was found.
-- @return #nil, #nil When no group is found, #nil is returned.
-- @usage
--
--   -- Find the first alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission.
--   local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup()
--   while GroupPlane ~= nil do
--     -- Do actions with the GroupPlane object.
--     GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index )
--   end
--
function SPAWN:GetFirstAliveGroup()
  --self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } )

  for SpawnIndex = 1, self.SpawnCount do
    local SpawnGroup = self:GetGroupFromIndex( SpawnIndex )
    if SpawnGroup and SpawnGroup:IsAlive() then
      return SpawnGroup, SpawnIndex
    end
  end

  return nil, nil
end

--- Will find the next alive @{Wrapper.Group} object from a given Index, and return a reference to the alive @{Wrapper.Group} object and the next Index where the alive @{Wrapper.Group} has been found.
-- @param #SPAWN self
-- @param #number SpawnIndexStart A Index holding the start position to search from. This method can also be used to find the first alive @{Wrapper.Group} object from the given Index.
-- @return Wrapper.Group#GROUP, #number The next alive @{Wrapper.Group} object found, the next Index where the next alive @{Wrapper.Group} object was found.
-- @return #nil, #nil When no alive @{Wrapper.Group} object is found from the start Index position, #nil is returned.
-- @usage
--
--   -- Find the first alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission.
--   local GroupPlane, Index = SpawnPlanes:GetFirstAliveGroup()
--   while GroupPlane ~= nil do
--     -- Do actions with the GroupPlane object.
--     GroupPlane, Index = SpawnPlanes:GetNextAliveGroup( Index )
--   end
--
function SPAWN:GetNextAliveGroup( SpawnIndexStart )
  --self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndexStart } )

  SpawnIndexStart = SpawnIndexStart + 1
  for SpawnIndex = SpawnIndexStart, self.SpawnCount do
    local SpawnGroup = self:GetGroupFromIndex( SpawnIndex )
    if SpawnGroup and SpawnGroup:IsAlive() then
      return SpawnGroup, SpawnIndex
    end
  end

  return nil, nil
end

--- Will find the last alive @{Wrapper.Group} object, and will return a reference to the last live @{Wrapper.Group} object and the last Index where the last alive @{Wrapper.Group} object has been found.
-- @param #SPAWN self
-- @return Wrapper.Group#GROUP, #number The last alive @{Wrapper.Group} object found, the last Index where the last alive @{Wrapper.Group} object was found.
-- @return #nil, #nil When no alive @{Wrapper.Group} object is found, #nil is returned.
-- @usage
--
--   -- Find the last alive @{Wrapper.Group} object of the SpawnPlanes SPAWN object @{Wrapper.Group} collection that it has spawned during the mission.
--   local GroupPlane, Index = SpawnPlanes:GetLastAliveGroup()
--   if GroupPlane then -- GroupPlane can be nil!!!
--     -- Do actions with the GroupPlane object.
--   end
--
function SPAWN:GetLastAliveGroup()
  --self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } )

  for SpawnIndex = self.SpawnCount, 1, -1 do -- Added
    local SpawnGroup = self:GetGroupFromIndex( SpawnIndex )
    if SpawnGroup and SpawnGroup:IsAlive() then
      self.SpawnIndex = SpawnIndex
      return SpawnGroup
    end
  end

  self.SpawnIndex = nil
  return nil
end

--- Get the group from an index.
-- Returns the group from the SpawnGroups list.
-- If no index is given, it will return the first group in the list.
-- @param #SPAWN self
-- @param #number SpawnIndex The index of the group to return.
-- @return Wrapper.Group#GROUP self
function SPAWN:GetGroupFromIndex( SpawnIndex )
  --self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } )

  if not SpawnIndex then
    SpawnIndex = 1
  end

  if self.SpawnGroups and self.SpawnGroups[SpawnIndex] then
    local SpawnGroup = self.SpawnGroups[SpawnIndex].Group
    return SpawnGroup
  else
    return nil
  end
end

--- Return the prefix of a SpawnUnit.
-- The method will search for a #-mark, and will return the text before the #-mark.
-- It will return nil of no prefix was found.
-- @param #SPAWN self
-- @param Wrapper.Group#GROUP SpawnGroup The GROUP object.
-- @return #string The prefix or #nil if nothing was found.
function SPAWN:_GetPrefixFromGroup( SpawnGroup )

  local GroupName = SpawnGroup:GetName()
  
  if GroupName then
  
    local SpawnPrefix=self:_GetPrefixFromGroupName(GroupName)
    
    return SpawnPrefix
  end
  
  return nil
end

--- Return the prefix of a spawned group.
-- The method will search for a `#`-mark, and will return the text before the `#`-mark. It will return nil of no prefix was found.
-- @param #SPAWN self
-- @param #string SpawnGroupName The name of the spawned group.
-- @return #string The prefix or #nil if nothing was found.
function SPAWN:_GetPrefixFromGroupName(SpawnGroupName)

  if SpawnGroupName then
  
    local SpawnPrefix=string.match(SpawnGroupName, ".*#")
    
    if SpawnPrefix then
      SpawnPrefix = SpawnPrefix:sub(1, -2)
    end
    
    return SpawnPrefix
  end
  
  return nil
end

--- Get the index from a given group.
-- The function will search the name of the group for a #, and will return the number behind the #-mark.
function SPAWN:GetSpawnIndexFromGroup( SpawnGroup )
  --self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } )

  local IndexString = string.match( SpawnGroup:GetName(), "#(%d*)$" ):sub( 2 )
  local Index = tonumber( IndexString )

  self:T3( IndexString, Index )
  return Index

end

--- Return the last maximum index that can be used.
function SPAWN:_GetLastIndex()
  --self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } )

  return self.SpawnMaxGroups
end

--- Initalize the SpawnGroups collection.
-- @param #SPAWN self
function SPAWN:_InitializeSpawnGroups( SpawnIndex )
  --self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnIndex } )

  if not self.SpawnGroups[SpawnIndex] then
    self.SpawnGroups[SpawnIndex] = {}
    self.SpawnGroups[SpawnIndex].Visible = false
    self.SpawnGroups[SpawnIndex].Spawned = false
    self.SpawnGroups[SpawnIndex].UnControlled = false
    self.SpawnGroups[SpawnIndex].SpawnTime = 0

    self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefix
    self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex )
  end

  self:_RandomizeTemplate( SpawnIndex )
  self:_RandomizeRoute( SpawnIndex )
  -- self:_TranslateRotate( SpawnIndex )

  return self.SpawnGroups[SpawnIndex]
end

--- Gets the CategoryID of the Group with the given SpawnPrefix
function SPAWN:_GetGroupCategoryID( SpawnPrefix )
  local TemplateGroup = Group.getByName( SpawnPrefix )

  if TemplateGroup then
    return TemplateGroup:getCategory()
  else
    return nil
  end
end

--- Gets the CoalitionID of the Group with the given SpawnPrefix
function SPAWN:_GetGroupCoalitionID( SpawnPrefix )
  local TemplateGroup = Group.getByName( SpawnPrefix )

  if TemplateGroup then
    return TemplateGroup:getCoalition()
  else
    return nil
  end
end

--- Gets the CountryID of the Group with the given SpawnPrefix
function SPAWN:_GetGroupCountryID( SpawnPrefix )
  --self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnPrefix } )

  local TemplateGroup = Group.getByName( SpawnPrefix )

  if TemplateGroup then
    local TemplateUnits = TemplateGroup:getUnits()
    return TemplateUnits[1]:getCountry()
  else
    return nil
  end
end

--- Gets the Group Template from the ME environment definition.
-- Note: This method uses the global _DATABASE object (an instance of @{Core.Database#DATABASE}), which contains ALL initial and new spawned objects in MOOSE.
-- @param #SPAWN self
-- @param #string SpawnTemplatePrefix
-- @return @SPAWN self
function SPAWN:_GetTemplate( SpawnTemplatePrefix )
  --self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } )

  local SpawnTemplate = nil
  
  if _DATABASE.Templates.Groups[SpawnTemplatePrefix] == nil then
    error( 'No Template exists for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix )
  end
  
  local Template = _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template
  --self:F( { Template = Template } )

  SpawnTemplate = UTILS.DeepCopy( _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template )

  if SpawnTemplate == nil then
    error( 'No Template returned for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix )
  end

  -- SpawnTemplate.SpawnCoalitionID = self:_GetGroupCoalitionID( SpawnTemplatePrefix )
  -- SpawnTemplate.SpawnCategoryID = self:_GetGroupCategoryID( SpawnTemplatePrefix )
  -- SpawnTemplate.SpawnCountryID = self:_GetGroupCountryID( SpawnTemplatePrefix )

  self:T3( { SpawnTemplate } )
  return SpawnTemplate
end

--- Prepares the new Group Template.
-- @param #SPAWN self
-- @param #string SpawnTemplatePrefix
-- @param #number SpawnIndex
-- @return #SPAWN self
function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2
  --self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix } )

  --  if not self.SpawnTemplate then
  --    self.SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix )
  --  end

  local SpawnTemplate
  if self.TweakedTemplate ~= nil and self.TweakedTemplate == true then
    BASE:I( "WARNING: You are using a tweaked template." )
    SpawnTemplate = self.SpawnTemplate
    if self.MooseNameing == true then
      SpawnTemplate.name = self:SpawnGroupName( SpawnIndex )
    else
      SpawnTemplate.name = self:SpawnGroupName()
    end
  else
    SpawnTemplate = self:_GetTemplate( SpawnTemplatePrefix )
    SpawnTemplate.name = self:SpawnGroupName( SpawnIndex )
  end

  SpawnTemplate.groupId = nil
  -- SpawnTemplate.lateActivation = false
  SpawnTemplate.lateActivation = self.LateActivated or false

  if SpawnTemplate.CategoryID == Group.Category.GROUND then
    self:T3( "For ground units, visible needs to be false..." )
    SpawnTemplate.visible = false
  end

  if self.SpawnGrouping then
    local UnitAmount = #SpawnTemplate.units
    --self:F( { UnitAmount = UnitAmount, SpawnGrouping = self.SpawnGrouping } )
    if UnitAmount > self.SpawnGrouping then
      for UnitID = self.SpawnGrouping + 1, UnitAmount do
        SpawnTemplate.units[UnitID] = nil
      end
    else
      if UnitAmount < self.SpawnGrouping then
        for UnitID = UnitAmount + 1, self.SpawnGrouping do
          SpawnTemplate.units[UnitID] = UTILS.DeepCopy( SpawnTemplate.units[1] )
          SpawnTemplate.units[UnitID].unitId = nil
        end
      end
    end
  end
  
  if self.SpawnInitKeepUnitNames == false then
    for UnitID = 1, #SpawnTemplate.units do      
      if not string.find(SpawnTemplate.units[UnitID].name,"#IFF_",1,true) then --Razbam IFF hack for F15E etc
        SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID )    
      end
      SpawnTemplate.units[UnitID].unitId = nil
    end
  else
    for UnitID = 1, #SpawnTemplate.units do
      local SpawnInitKeepUnitIFF = false
      if string.find(SpawnTemplate.units[UnitID].name,"#IFF_",1,true) then --Razbam IFF hack for F15E etc
        SpawnInitKeepUnitIFF = true
      end
      local UnitPrefix, Rest
      if SpawnInitKeepUnitIFF == false then       
        UnitPrefix, Rest = string.match( SpawnTemplate.units[UnitID].name, "^([^#]+)#?" ):gsub( "^%s*(.-)%s*$", "%1" )
        SpawnTemplate.units[UnitID].name = string.format( '%s#%03d-%02d', UnitPrefix, SpawnIndex, UnitID )       
        --self:T2( { UnitPrefix, Rest } )
      --else
       --UnitPrefix=SpawnTemplate.units[UnitID].name
      end
      --SpawnTemplate.units[UnitID].name = string.format( '%s#%03d-%02d', UnitPrefix, SpawnIndex, UnitID )
      
      SpawnTemplate.units[UnitID].unitId = nil
    end
  end

  -- Callsign
  
  if self.SpawnRandomCallsign and SpawnTemplate.units[1].callsign then
    if type( SpawnTemplate.units[1].callsign ) ~= "number" then
      -- change callsign
      local min = 1
      local max = 8
      local ctable = CALLSIGN.Aircraft
      if string.find(SpawnTemplate.units[1].type, "A-10",1,true) then 
        max = 12
      end
      if string.find(SpawnTemplate.units[1].type, "18",1,true) then
        min = 9 
        max = 20 
        ctable = CALLSIGN.F18
      end
      if string.find(SpawnTemplate.units[1].type, "16",1,true) then
        min = 9 
        max = 20 
        ctable = CALLSIGN.F16
      end
      if SpawnTemplate.units[1].type == "F-15E" then
        min = 9 
        max = 18 
        ctable = CALLSIGN.F15E
      end
      local callsignnr = math.random(min,max)
      local callsignname = "Enfield"
      for name, value in pairs(ctable) do
        if value==callsignnr then
          callsignname = name
        end
      end
      for UnitID = 1, #SpawnTemplate.units do
        SpawnTemplate.units[UnitID].callsign[1] = callsignnr
        SpawnTemplate.units[UnitID].callsign[2] = UnitID
        SpawnTemplate.units[UnitID].callsign[3] = "1"
        SpawnTemplate.units[UnitID].callsign["name"] = tostring(callsignname)..tostring(UnitID).."1"
        -- UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].callsign,1) 
      end         
    else
      -- Russkis
      for UnitID = 1, #SpawnTemplate.units do
        SpawnTemplate.units[UnitID].callsign = math.random(1,999)
      end
    end
  end
  
  if self.SpawnInitCallSign then
    for UnitID = 1, #SpawnTemplate.units do
      local Callsign = SpawnTemplate.units[UnitID].callsign
      if Callsign and type( Callsign ) ~= "number" then
        SpawnTemplate.units[UnitID].callsign[1] = self.SpawnInitCallSignID 
        SpawnTemplate.units[UnitID].callsign[2] = self.SpawnInitCallSignMinor
        SpawnTemplate.units[UnitID].callsign[3] = self.SpawnInitCallSignMajor
        SpawnTemplate.units[UnitID].callsign["name"] = string.format("%s%d%d",self.SpawnInitCallSignName,self.SpawnInitCallSignMinor,self.SpawnInitCallSignMajor)
        --UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].callsign,1)
      end
    end
  end
  
  for UnitID = 1, #SpawnTemplate.units do
    local Callsign = SpawnTemplate.units[UnitID].callsign
    if Callsign then
      if type( Callsign ) ~= "number" and not self.SpawnInitCallSign then -- blue callsign
        -- UTILS.PrintTableToLog(Callsign,1)
        Callsign[2] = ((SpawnIndex - 1) % 10) + 1
        local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string
        CallsignName = string.match(CallsignName,"^(%a+)") -- 2.8 - only the part w/o numbers
        local CallsignLen = CallsignName:len()
        SpawnTemplate.units[UnitID].callsign[2] = UnitID
        SpawnTemplate.units[UnitID].callsign["name"] = CallsignName:sub( 1, CallsignLen ) .. SpawnTemplate.units[UnitID].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3]
      elseif type( Callsign ) == "number" then
        SpawnTemplate.units[UnitID].callsign = Callsign + SpawnIndex
      end
    end
    -- Speed
    if self.InitSpeed then
      SpawnTemplate.units[UnitID].speed = self.InitSpeed
    end
    -- Link16
    local AddProps = SpawnTemplate.units[UnitID].AddPropAircraft
    if AddProps then
      if SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 then
        if self.SpawnInitSTN then
          local octal = self.SpawnInitSTN
          if UnitID > 1 then
            octal = _DATABASE:GetNextSTN(self.SpawnInitSTN,SpawnTemplate.units[UnitID].name)
          end
          SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 = string.format("%05d",octal)
        else
          -- 5 digit octal with leading 0
          if tonumber(SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16) ~= nil then
            local octal = SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16
            local num = UTILS.OctalToDecimal(octal)
            if _DATABASE.STNS[num] ~= nil or UnitID > 1 then -- STN taken or next unit
              octal = _DATABASE:GetNextSTN(octal,SpawnTemplate.units[UnitID].name)
            end
            SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 = string.format("%05d",octal)
          else -- ED bug - chars in here
            local OSTN = _DATABASE:GetNextSTN(1,SpawnTemplate.units[UnitID].name)
            SpawnTemplate.units[UnitID].AddPropAircraft.STN_L16 = string.format("%05d",OSTN)
          end
        end
      end
      -- A10CII
      if SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN then
        -- 4 digit octal with leading 0
        if self.SpawnInitSADL then
          local octal = self.SpawnInitSADL
          if UnitID > 1 then
            octal = _DATABASE:GetNextSADL(self.SpawnInitSADL,SpawnTemplate.units[UnitID].name)
          end
          SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN = string.format("%04d",octal)
        else
          if tonumber(SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN) ~= nil then
            local octal = SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN
            local num = UTILS.OctalToDecimal(octal)
            self.SpawnInitSADL = num -- we arrived here seeing that self.SpawnInitSADL == nil, but now that we have a SADL (num), we also need to set it to self.SpawnInitSADL in case
                                     -- we need to get the next SADL from _DATABASE, or else UTILS.OctalToDecimal() will fail in GetNextSADL
            if  _DATABASE.SADL[num] ~= nil or UnitID > 1 then -- SADL taken or next unit
              octal = _DATABASE:GetNextSADL(self.SpawnInitSADL,SpawnTemplate.units[UnitID].name)
            end
            SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN = string.format("%04d",octal)
          else -- ED bug - chars in here
            local OSTN = _DATABASE:GetNextSADL(1,SpawnTemplate.units[UnitID].name)
            SpawnTemplate.units[UnitID].AddPropAircraft.SADL_TN = string.format("%04d",OSTN)
          end
        end
      end
      -- VoiceCallsignNumber
      if SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignNumber and type( Callsign ) ~= "number" then
        SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignNumber = SpawnTemplate.units[UnitID].callsign[2] .. SpawnTemplate.units[UnitID].callsign[3]
      end
      -- VoiceCallsignLabel
      if SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignLabel and type( Callsign ) ~= "number" then
        local CallsignName = SpawnTemplate.units[UnitID].callsign["name"] -- #string
        CallsignName = string.match(CallsignName,"^(%a+)") -- 2.8 - only the part w/o numbers
        local label = "NY" -- Navy One exception
        if not string.find(CallsignName," ") then
          label = string.upper(string.match(CallsignName,"^%a")..string.match(CallsignName,"%a$"))
        end
        SpawnTemplate.units[UnitID].AddPropAircraft.VoiceCallsignLabel = label
      end
      -- UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].AddPropAircraft,1)
      -- FlightLead
      if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.Link16 and SpawnTemplate.units[UnitID].datalinks.Link16.settings then
        SpawnTemplate.units[UnitID].datalinks.Link16.settings.flightLead = UnitID == 1 and true or false
      end
      -- A10CII
      if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.SADL and SpawnTemplate.units[UnitID].datalinks.SADL.settings then
        SpawnTemplate.units[UnitID].datalinks.SADL.settings.flightLead = UnitID == 1 and true or false
      end
      -- UTILS.PrintTableToLog(SpawnTemplate.units[UnitID].datalinks,1)   
    end
  end
  -- Link16 team members
  for UnitID = 1, #SpawnTemplate.units do
    if SpawnTemplate.units[UnitID].datalinks and SpawnTemplate.units[UnitID].datalinks.Link16 and SpawnTemplate.units[UnitID].datalinks.Link16.network then
      local team = {}
      local isF16 = string.find(SpawnTemplate.units[UnitID].type,"F-16",1,true) and true or false
      for ID = 1, #SpawnTemplate.units do
        local member = {}
        member.missionUnitId = ID 
        if isF16 then
          member.TDOA = true
        end
        table.insert(team,member)
      end
      SpawnTemplate.units[UnitID].datalinks.Link16.network.teamMembers = team
    end
  end

  self:T3( { "Template:", SpawnTemplate } )
  --UTILS.PrintTableToLog(SpawnTemplate,1)
  return SpawnTemplate

end

--- Private method randomizing the routes.
-- @param #SPAWN self
-- @param #number SpawnIndex The index of the group to be spawned.
-- @return #SPAWN
function SPAWN:_RandomizeRoute( SpawnIndex )
  --self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeRoute, self.SpawnRandomizeRouteStartPoint, self.SpawnRandomizeRouteEndPoint, self.SpawnRandomizeRouteRadius } )

  if self.SpawnRandomizeRoute then
    local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate
    local RouteCount = #SpawnTemplate.route.points

    for t = self.SpawnRandomizeRouteStartPoint + 1, (RouteCount - self.SpawnRandomizeRouteEndPoint) do

      SpawnTemplate.route.points[t].x = SpawnTemplate.route.points[t].x + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius )
      SpawnTemplate.route.points[t].y = SpawnTemplate.route.points[t].y + math.random( self.SpawnRandomizeRouteRadius * -1, self.SpawnRandomizeRouteRadius )

      -- Manage randomization of altitude for airborne units ...
      if SpawnTemplate.CategoryID == Group.Category.AIRPLANE or SpawnTemplate.CategoryID == Group.Category.HELICOPTER then
        if SpawnTemplate.route.points[t].alt and self.SpawnRandomizeRouteHeight then
          SpawnTemplate.route.points[t].alt = SpawnTemplate.route.points[t].alt + math.random( 1, self.SpawnRandomizeRouteHeight )
        end
      else
        SpawnTemplate.route.points[t].alt = nil
      end

      --self:T2( 'SpawnTemplate.route.points[' .. t .. '].x = ' .. SpawnTemplate.route.points[t].x .. ', SpawnTemplate.route.points[' .. t .. '].y = ' .. SpawnTemplate.route.points[t].y )
    end
  end

  self:_RandomizeZones( SpawnIndex )

  return self
end

--- Private method that randomizes the template of the group.
-- @param #SPAWN self
-- @param #number SpawnIndex
-- @return #SPAWN self
function SPAWN:_RandomizeTemplate( SpawnIndex )
  --self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeTemplate } )

  if self.SpawnRandomizeTemplate then
    self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix = self.SpawnTemplatePrefixTable[math.random( 1, #self.SpawnTemplatePrefixTable )]
    self.SpawnGroups[SpawnIndex].SpawnTemplate = self:_Prepare( self.SpawnGroups[SpawnIndex].SpawnTemplatePrefix, SpawnIndex )
    self.SpawnGroups[SpawnIndex].SpawnTemplate.route = UTILS.DeepCopy( self.SpawnTemplate.route )
    self.SpawnGroups[SpawnIndex].SpawnTemplate.x = self.SpawnTemplate.x
    self.SpawnGroups[SpawnIndex].SpawnTemplate.y = self.SpawnTemplate.y
    self.SpawnGroups[SpawnIndex].SpawnTemplate.start_time = self.SpawnTemplate.start_time
    local OldX = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].x
    local OldY = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[1].y
    for UnitID = 1, #self.SpawnGroups[SpawnIndex].SpawnTemplate.units do
      self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].heading = self.SpawnTemplate.units[1].heading
      self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x = self.SpawnTemplate.units[1].x + (self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].x - OldX)
      self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y = self.SpawnTemplate.units[1].y + (self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].y - OldY)
      self.SpawnGroups[SpawnIndex].SpawnTemplate.units[UnitID].alt = self.SpawnTemplate.units[1].alt
    end
  end

  self:_RandomizeRoute( SpawnIndex )

  return self
end

--- Private method that sets the DCS#Vec2 where the Group will be spawned.
-- @param #SPAWN self
-- @param #number SpawnIndex
-- @return #SPAWN self
function SPAWN:_SetInitialPosition( SpawnIndex )
  --self:T2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeZones } )
  
  if self.SpawnFromNewPosition then
    
    --self:T2( "Preparing Spawn at Vec2 ", self.SpawnInitPosition )

    local SpawnVec2 = self.SpawnInitPosition

    --self:T2( { SpawnVec2 = SpawnVec2 } )

    local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate

    SpawnTemplate.route = SpawnTemplate.route or {}
    SpawnTemplate.route.points = SpawnTemplate.route.points or {}
    SpawnTemplate.route.points[1] = SpawnTemplate.route.points[1] or {}
    SpawnTemplate.route.points[1].x = SpawnTemplate.route.points[1].x or 0
    SpawnTemplate.route.points[1].y = SpawnTemplate.route.points[1].y or 0
      
    --self:T2( { Route = SpawnTemplate.route } )

    for UnitID = 1, #SpawnTemplate.units do
      local UnitTemplate = SpawnTemplate.units[UnitID]
      --self:T2( 'Before Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. UnitTemplate.y )
      local SX = UnitTemplate.x
      local SY = UnitTemplate.y
      local BX = SpawnTemplate.route.points[1].x
      local BY = SpawnTemplate.route.points[1].y
      local TX = SpawnVec2.x + (SX - BX)
      local TY = SpawnVec2.y + (SY - BY)
      UnitTemplate.x = TX
      UnitTemplate.y = TY
      -- TODO: Manage altitude based on landheight...
      -- SpawnTemplate.units[UnitID].alt = SpawnVec2:
      --self:T2( 'After Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. UnitTemplate.y )
    end
    
    SpawnTemplate.route.points[1].x = SpawnVec2.x
    SpawnTemplate.route.points[1].y = SpawnVec2.y
    SpawnTemplate.x = SpawnVec2.x
    SpawnTemplate.y = SpawnVec2.y
    
  end

  return self
end

--- Private method that randomizes the @{Core.Zone}s where the Group will be spawned.
-- @param #SPAWN self
-- @param #number SpawnIndex
-- @param #boolean RandomizePositionInZone If nil or true, also the position inside the selected random zone will be randomized. Set to false to use the center of the zone.
-- @return #SPAWN self
function SPAWN:_RandomizeZones( SpawnIndex,  RandomizePositionInZone)
  --self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeZones } )

  if self.SpawnRandomizeZones then
    local SpawnZone = nil -- Core.Zone#ZONE_BASE
    while not SpawnZone do
      --self:T2( { SpawnZoneTableCount = #self.SpawnZoneTable, self.SpawnZoneTable } )
      local ZoneID = math.random( #self.SpawnZoneTable )
      --self:T2( ZoneID )
      SpawnZone = self.SpawnZoneTable[ZoneID]:GetZoneMaybe()
    end

    --self:T2( "Preparing Spawn in Zone", SpawnZone:GetName() )

    local SpawnVec2 = SpawnZone:GetVec2()

    if RandomizePositionInZone ~= false then
        SpawnVec2 = SpawnZone:GetRandomVec2()
    end

    --self:T2( { SpawnVec2 = SpawnVec2 } )

    local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate
    self.SpawnGroups[SpawnIndex].SpawnZone = SpawnZone

    --self:T2( { Route = SpawnTemplate.route } )

    for UnitID = 1, #SpawnTemplate.units do
      local UnitTemplate = SpawnTemplate.units[UnitID]
      --self:T2( 'Before Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. UnitTemplate.y )
      local SX = UnitTemplate.x
      local SY = UnitTemplate.y
      local BX = SpawnTemplate.route.points[1].x
      local BY = SpawnTemplate.route.points[1].y
      local TX = SpawnVec2.x + (SX - BX)
      local TY = SpawnVec2.y + (SY - BY)
      UnitTemplate.x = TX
      UnitTemplate.y = TY
      -- TODO: Manage altitude based on landheight...
      -- SpawnTemplate.units[UnitID].alt = SpawnVec2:
      --self:T2( 'After Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. UnitTemplate.y )
    end
    SpawnTemplate.x = SpawnVec2.x
    SpawnTemplate.y = SpawnVec2.y
    SpawnTemplate.route.points[1].x = SpawnVec2.x
    SpawnTemplate.route.points[1].y = SpawnVec2.y
  end

  return self

end

function SPAWN:_TranslateRotate( SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle )
  --self:F( { self.SpawnTemplatePrefix, SpawnIndex, SpawnRootX, SpawnRootY, SpawnX, SpawnY, SpawnAngle } )

  -- Translate
  local TranslatedX = SpawnX
  local TranslatedY = SpawnY

  -- Rotate
  -- From Wikipedia: https://en.wikipedia.org/wiki/Rotation_matrix#Common_rotations
  -- x' = x \cos \theta - y \sin \theta\
  -- y' = x \sin \theta + y \cos \theta\ 
  local RotatedX = -TranslatedX * math.cos( math.rad( SpawnAngle ) ) + TranslatedY * math.sin( math.rad( SpawnAngle ) )
  local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + TranslatedY * math.cos( math.rad( SpawnAngle ) )

  -- Assign
  self.SpawnGroups[SpawnIndex].SpawnTemplate.x = SpawnRootX - RotatedX
  self.SpawnGroups[SpawnIndex].SpawnTemplate.y = SpawnRootY + RotatedY

  local SpawnUnitCount = table.getn( self.SpawnGroups[SpawnIndex].SpawnTemplate.units )
  for u = 1, SpawnUnitCount do

    -- Translate
    local TranslatedX = SpawnX
    local TranslatedY = SpawnY - 10 * (u - 1)

    -- Rotate
    local RotatedX = -TranslatedX * math.cos( math.rad( SpawnAngle ) ) + TranslatedY * math.sin( math.rad( SpawnAngle ) )
    local RotatedY = TranslatedX * math.sin( math.rad( SpawnAngle ) ) + TranslatedY * math.cos( math.rad( SpawnAngle ) )

    -- Assign
    self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].x = SpawnRootX - RotatedX
    self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].y = SpawnRootY + RotatedY
    self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading = self.SpawnGroups[SpawnIndex].SpawnTemplate.units[u].heading + math.rad( SpawnAngle )
  end

  return self
end

--- Get the next index of the groups to be spawned. This method is complicated, as it is used at several spaces.
-- @param #SPAWN self
-- @param #number SpawnIndex Spawn index.
-- @return #number self.SpawnIndex
function SPAWN:_GetSpawnIndex( SpawnIndex )
  --self:T2( { template=self.SpawnTemplatePrefix, SpawnIndex=SpawnIndex, SpawnMaxGroups=self.SpawnMaxGroups, SpawnMaxUnitsAlive=self.SpawnMaxUnitsAlive, AliveUnits=self.AliveUnits, TemplateUnits=#self.SpawnTemplate.units } )

  if (self.SpawnMaxGroups == 0) or (SpawnIndex <= self.SpawnMaxGroups) then
    if (self.SpawnMaxUnitsAlive == 0) or (self.AliveUnits + #self.SpawnTemplate.units <= self.SpawnMaxUnitsAlive) or self.UnControlled == true then
      --self:T2( { SpawnCount = self.SpawnCount, SpawnIndex = SpawnIndex } )
      if SpawnIndex and SpawnIndex >= self.SpawnCount + 1 then
        self.SpawnCount = self.SpawnCount + 1
        SpawnIndex = self.SpawnCount
      end
      self.SpawnIndex = SpawnIndex
      if not self.SpawnGroups[self.SpawnIndex] then
        self:_InitializeSpawnGroups( self.SpawnIndex )
      end
    else
      return nil
    end
  else
    return nil
  end

  return self.SpawnIndex
end

-- TODO Need to delete this... _DATABASE does this now ...

-- @param #SPAWN self 
-- @param Core.Event#EVENTDATA EventData
function SPAWN:_OnBirth( EventData )
  --self:F( self.SpawnTemplatePrefix )

  local SpawnGroup = EventData.IniGroup

  if SpawnGroup then
    local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup )
    if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group!
      --self:T2( { "Birth Event:", EventPrefix, self.SpawnTemplatePrefix } )
      if EventPrefix == self.SpawnTemplatePrefix or (self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix) then
        self.AliveUnits = self.AliveUnits + 1
        --self:T2( "Alive Units: " .. self.AliveUnits )
      end
    end
  end

end

---
-- @param #SPAWN self 
-- @return #number count
function SPAWN:_CountAliveUnits()
  local count = 0
  --self:I("self.SpawnAliasPrefix="..tostring(self.SpawnAliasPrefix).." | self.SpawnTemplatePrefix="..tostring(self.SpawnTemplatePrefix))
  if self.SpawnAliasPrefix then
    if not self.SpawnAliasPrefixEscaped then self.SpawnAliasPrefixEscaped = string.gsub(self.SpawnAliasPrefix,"[%p%s]",".") end
    --self:I("self.SpawnAliasPrefixEscaped="..tostring(self.SpawnAliasPrefixEscaped))
    local SpawnAliasPrefix = self.SpawnAliasPrefixEscaped
    local agroups = GROUP:FindAllByMatching(SpawnAliasPrefix)
    for _,_grp in pairs(agroups) do
      --self:I("Group Name = " .. _grp:GetName())
      local game = self:_GetPrefixFromGroupName(_grp.GroupName)
      --self:I("Game = "..game)
      --self:I("Count = ".._grp:CountAliveUnits())
      if game and game == self.SpawnAliasPrefix then
        count = count + _grp:CountAliveUnits()
      end
    end
  else
    if not self.SpawnTemplatePrefixEscaped then self.SpawnTemplatePrefixEscaped = string.gsub(self.SpawnTemplatePrefix,"[%p%s]",".") end
    local SpawnTemplatePrefix = self.SpawnTemplatePrefixEscaped
    local groups = GROUP:FindAllByMatching(SpawnTemplatePrefix)
    for _,_grp in pairs(groups) do
      local game = self:_GetPrefixFromGroupName(_grp.GroupName)
      if game and game == self.SpawnTemplatePrefix then
        count = count + _grp:CountAliveUnits()
      end
    end
  end
  self.AliveUnits = count
  return self
end

---
-- @param #SPAWN self 
-- @param Core.Event#EVENTDATA EventData
function SPAWN:_OnDeadOrCrash( EventData )
  --self:I( "Dead or crash event ID "..tostring(EventData.id or 0))
  --self:I( "Dead or crash event for "..tostring(EventData.IniUnitName or "none") )
  
  --if EventData.id == EVENTS.Dead then return end
  
  local unit=UNIT:FindByName(EventData.IniUnitName)
  --local group=GROUP:FindByName(EventData.IniGroupName)
  
  if unit then
  
    local EventPrefix = self:_GetPrefixFromGroupName(unit.GroupName)
   
    if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group!
      --self:I(string.format("EventPrefix = %s | SpawnAliasPrefix = %s  | Old AliveUnits = %d",EventPrefix or "",self.SpawnAliasPrefix or "",self.AliveUnits or 0))
      if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) and self.AliveUnits > 0 then
       --self:I( { "Dead event: " .. EventPrefix } )
       --self.AliveUnits = self.AliveUnits - 1
       self:ScheduleOnce(1,self._CountAliveUnits,self)   
       --self.AliveUnits = self:_CountAliveUnits()
       --self:I( "New Alive Units: " .. self.AliveUnits ) 
      end
    end
  end
end

--- Will detect AIR Units taking off... When the event takes place, the spawned Group is registered as airborne...
-- This is needed to ensure that Re-SPAWNing only is done for landed AIR Groups.
-- @param #SPAWN self
-- @param Core.Event#EVENTDATA EventData
function SPAWN:_OnTakeOff( EventData )
  --self:F( self.SpawnTemplatePrefix )

  local SpawnGroup = EventData.IniGroup
  if SpawnGroup then
    local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup )
    if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group!
      --self:T2( { "TakeOff event: " .. EventPrefix } )
      if EventPrefix == self.SpawnTemplatePrefix or (self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix) then
        --self:T2( "self.Landed = false" )
        SpawnGroup:SetState( SpawnGroup, "Spawn_Landed", false )
      end
    end
  end
end

--- Will detect AIR Units landing... When the event takes place, the spawned Group is registered as landed.
-- This is needed to ensure that Re-SPAWNing is only done for landed AIR Groups.
-- @param #SPAWN self
-- @param Core.Event#EVENTDATA EventData
function SPAWN:_OnLand( EventData )
  --self:F( self.SpawnTemplatePrefix )

  local SpawnGroup = EventData.IniGroup
  if SpawnGroup then
    local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup )
    if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group!
      --self:T2( { "Land event: " .. EventPrefix } )
      if EventPrefix == self.SpawnTemplatePrefix or (self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix) then
        -- TODO: Check if this is the last unit of the group that lands.
        SpawnGroup:SetState( SpawnGroup, "Spawn_Landed", true )
        if self.RepeatOnLanding then
          local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup )
          --self:T2( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } )
          -- self:ReSpawn( SpawnGroupIndex )
          -- Delay respawn by three seconds due to DCS 2.5.4.26368 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076
          -- Bug was initially only for engine shutdown event but after ED "fixed" it, it now happens on landing events.
          SCHEDULER:New( nil, self.ReSpawn, { self, SpawnGroupIndex }, self.RepeatOnLandingTime or 3 )
        end
      end
    end
  end
end

--- Will detect AIR Units shutting down their engines ...
-- When the event takes place, and the method @{#InitRepeatOnEngineShutDown} was called, the spawned Group will Re-SPAWN.
-- But only when the Unit was registered to have landed.
-- @param #SPAWN self
-- @param Core.Event#EVENTDATA EventData
function SPAWN:_OnEngineShutDown( EventData )
  --self:F( self.SpawnTemplatePrefix )

  local SpawnGroup = EventData.IniGroup
  if SpawnGroup then
    local EventPrefix = self:_GetPrefixFromGroup( SpawnGroup )
    if EventPrefix then -- EventPrefix can be nil if no # is found, which means, no spawnable group!
      --self:T2( { "EngineShutdown event: " .. EventPrefix } )
      if EventPrefix == self.SpawnTemplatePrefix or (self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix) then
        -- todo: test if on the runway
        local Landed = SpawnGroup:GetState( SpawnGroup, "Spawn_Landed" )
        if Landed and self.RepeatOnEngineShutDown then
          local SpawnGroupIndex = self:GetSpawnIndexFromGroup( SpawnGroup )
          --self:T2( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } )
          -- self:ReSpawn( SpawnGroupIndex )
          -- Delay respawn by three seconds due to DCS 2.5.4 OB bug https://github.com/FlightControl-Master/MOOSE/issues/1076
          SCHEDULER:New( nil, self.ReSpawn, { self, SpawnGroupIndex }, 3 )
        end
      end
    end
  end
end

--- This function is called automatically by the Spawning scheduler.
-- It is the internal worker method SPAWNing new Groups on the defined time intervals.
-- @param #SPAWN self
function SPAWN:_Scheduler()
  self:F2( { "_Scheduler", self.SpawnTemplatePrefix, self.SpawnAliasPrefix, self.SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive } )

  -- Validate if there are still groups left in the batch...
  self:Spawn()

  return true
end

--- Schedules the CleanUp of Groups
-- @param #SPAWN self
-- @return #boolean True = Continue Scheduler
function SPAWN:_SpawnCleanUpScheduler()
  --self:F( { "CleanUp Scheduler:", self.SpawnTemplatePrefix } )
  
  local SpawnGroup, SpawnCursor = self:GetFirstAliveGroup()
  --self:T2( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } )

  local IsHelo = false
  
  while SpawnGroup do
    
    IsHelo = SpawnGroup:IsHelicopter()
    
    local SpawnUnits = SpawnGroup:GetUnits()

    for UnitID, UnitData in pairs( SpawnUnits ) do

      local SpawnUnit = UnitData -- Wrapper.Unit#UNIT
      local SpawnUnitName = SpawnUnit:GetName()

      self.SpawnCleanUpTimeStamps[SpawnUnitName] = self.SpawnCleanUpTimeStamps[SpawnUnitName] or {}
      local Stamp = self.SpawnCleanUpTimeStamps[SpawnUnitName]
      --self:T2( { SpawnUnitName, Stamp } )

      if Stamp.Vec2 then
        if (SpawnUnit:InAir() == false and SpawnUnit:GetVelocityKMH() < 1) or IsHelo then
          local NewVec2 = SpawnUnit:GetVec2() or {x=0, y=0}
          if (Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y) or (SpawnUnit:GetLife() <= 1) then
            -- If the plane is not moving or dead , and is on the ground, assign it with a timestamp...
            if Stamp.Time + self.SpawnCleanUpInterval < timer.getTime() then
              --self:T2( { "CleanUp Scheduler:", "ReSpawning:", SpawnGroup:GetName() } )
              --self:ReSpawn( SpawnCursor )
              SCHEDULER:New( nil, self.ReSpawn, { self, SpawnCursor }, 3 )
              Stamp.Vec2 = nil
              Stamp.Time = nil
            end
          else
            Stamp.Time = timer.getTime()
            Stamp.Vec2 = SpawnUnit:GetVec2()
          end
        else
          Stamp.Vec2 = nil
          Stamp.Time = nil
        end
      else
        if SpawnUnit:InAir() == false or (IsHelo and SpawnUnit:GetLife() <= 1) then
          Stamp.Vec2 = SpawnUnit:GetVec2() or {x=0, y=0}
          if (SpawnUnit:GetVelocityKMH() < 1) then
            Stamp.Time = timer.getTime()
          end
        else
          Stamp.Time = nil
          Stamp.Vec2 = nil
        end
      end
    end

    SpawnGroup, SpawnCursor = self:GetNextAliveGroup( SpawnCursor )

    --self:T2( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } )

  end

  return true -- Repeat

end
--- **Core** - Spawn statics.
--
-- ===
--
-- ## Features:
--
--   * Spawn new statics from a static already defined in the mission editor.
--   * Spawn new statics from a given template.
--   * Spawn new statics from a given type.
--   * Spawn with a custom heading and location.
--   * Spawn within a zone.
--   * Spawn statics linked to units, .e.g on aircraft carriers.
--
-- ===
--
-- # Demo Missions
--
-- ## [SPAWNSTATIC Demo Missions](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/Core/SpawnStatic)
--
--
-- ===
--
-- # YouTube Channel
--
-- ## No videos yet!
--
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions: **funkyfranky**
--
-- ===
--
-- @module Core.SpawnStatic
-- @image Core_Spawnstatic.JPG

--- @type SPAWNSTATIC
-- @field #string SpawnTemplatePrefix Name of the template group.
-- @field #number CountryID Country ID.
-- @field #number CoalitionID Coalition ID.
-- @field #number CategoryID Category ID.
-- @field #number SpawnIndex Running number increased with each new Spawn.
-- @field Wrapper.Unit#UNIT InitLinkUnit The unit the static is linked to.
-- @field #number InitOffsetX Link offset X coordinate.
-- @field #number InitOffsetY Link offset Y coordinate.
-- @field #number InitOffsetAngle Link offset angle in degrees.
-- @field #number InitStaticHeading Heading of the static.
-- @field #string InitStaticLivery Livery for aircraft.
-- @field #string InitStaticShape Shape of the static.
-- @field #string InitStaticType Type of the static.
-- @field #string InitStaticCategory Categrory of the static.
-- @field #string InitStaticName Name of the static.
-- @field Core.Point#COORDINATE InitStaticCoordinate Coordinate where to spawn the static.
-- @field #boolean InitStaticDead Set static to be dead if true.
-- @field #boolean InitStaticCargo If true, static can act as cargo.
-- @field #number InitStaticCargoMass Mass of cargo in kg.
-- @extends Core.Base#BASE


--- Allows to spawn dynamically new @{Wrapper.Static}s into your mission.
--
-- Through creating a copy of an existing static object template as defined in the Mission Editor (ME), SPAWNSTATIC can retireve the properties of the defined static object template (like type, category etc),
-- and "copy" these properties to create a new static object and place it at the desired coordinate.
--
-- New spawned @{Wrapper.Static}s get **the same name** as the name of the template Static, or gets the given name when a new name is provided at the Spawn method.
-- By default, spawned @{Wrapper.Static}s will follow a naming convention at run-time:
--
--   * Spawned @{Wrapper.Static}s will have the name _StaticName_#_nnn_, where _StaticName_ is the name of the **Template Static**, and _nnn_ is a **counter from 0 to 99999**.
--
-- # SPAWNSTATIC Constructors
--
-- Firstly, we need to create a SPAWNSTATIC object that will be used to spawn new statics into the mission. There are three ways to do this.
--
-- ## Use another Static
--
-- A new SPAWNSTATIC object can be created using another static by the @{#SPAWNSTATIC.NewFromStatic}() function. All parameters such as position, heading, country will be initialized
-- from the static.
--
-- ## From a Template
--
-- A SPAWNSTATIC object can also be created from a template table using the @{#SPAWNSTATIC.NewFromTemplate}(SpawnTemplate, CountryID) function. All parameters are taken from the template.
--
-- ## From a Type
--
-- A very basic method is to create a SPAWNSTATIC object by just giving the type of the static. All parameters must be initialized from the InitXYZ functions described below. Otherwise default values
-- are used. For example, if no spawn coordinate is given, the static will be created at the origin of the map.
--
-- # Setting Parameters
--
-- Parameters such as the spawn position, heading, country etc. can be set via :Init*XYZ* functions. Note that these functions must be given before the actual spawn command!
--
--   * @{#SPAWNSTATIC.InitCoordinate}(Coordinate) Sets the coordinate where the static is spawned. Statics are always spawnd on the ground.
--   * @{#SPAWNSTATIC.InitHeading}(Heading) sets the orientation of the static.
--   * @{#SPAWNSTATIC.InitLivery}(LiveryName) sets the livery of the static. Not all statics support this.
--   * @{#SPAWNSTATIC.InitType}(StaticType) sets the type of the static.
--   * @{#SPAWNSTATIC.InitShape}(StaticType) sets the shape of the static. Not all statics have this parameter.
--   * @{#SPAWNSTATIC.InitNamePrefix}(NamePrefix) sets the name prefix of the spawned statics.
--   * @{#SPAWNSTATIC.InitCountry}(CountryID) sets the country and therefore the coalition of the spawned statics.
--   * @{#SPAWNSTATIC.InitLinkToUnit}(Unit, OffsetX, OffsetY, OffsetAngle) links the static to a unit, e.g. to an aircraft carrier.
--
-- # Spawning the Statics
--
-- Once the SPAWNSTATIC object is created and parameters are initialized, the spawn command can be given. There are different methods where some can be used to directly set parameters
-- such as position and heading.
--
--   * @{#SPAWNSTATIC.Spawn}(Heading, NewName) spawns the static with the set parameters. Optionally, heading and name can be given. The name **must be unique**!
--   * @{#SPAWNSTATIC.SpawnFromCoordinate}(Coordinate, Heading, NewName) spawn the static at the given coordinate. Optionally, heading and name can be given. The name **must be unique**!
--   * @{#SPAWNSTATIC.SpawnFromPointVec2}(PointVec2, Heading, NewName) spawns the static at a COORDINATE coordinate. Optionally, heading and name can be given. The name **must be unique**!
--   * @{#SPAWNSTATIC.SpawnFromZone}(Zone, Heading, NewName) spawns the static at the center of a @{Core.Zone}. Optionally, heading and name can be given. The name **must be unique**!
--
-- @field #SPAWNSTATIC SPAWNSTATIC
--
SPAWNSTATIC = {
  ClassName  = "SPAWNSTATIC",
  SpawnIndex = 0,
}

--- Static template table data.
-- @type SPAWNSTATIC.TemplateData
-- @field #string name Name of the static.
-- @field #string type Type of the static.
-- @field #string category Category of the static.
-- @field #number x X-coordinate of the static.
-- @field #number y Y-coordinate of teh static.
-- @field #number heading Heading in rad.
-- @field #boolean dead Static is dead if true.
-- @field #string livery_id Livery name.
-- @field #number unitId Unit ID.
-- @field #number groupId Group ID.
-- @field #table offsets Offset parameters when linked to a unit.
-- @field #number mass Cargo mass in kg.
-- @field #boolean canCargo Static can be a cargo.

--- Creates the main object to spawn a @{Wrapper.Static} defined in the mission editor (ME).
-- @param #SPAWNSTATIC self
-- @param #string SpawnTemplateName Name of the static object in the ME. Each new static will have the name starting with this prefix.
-- @param DCS#country.id SpawnCountryID (Optional) The ID of the country.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:NewFromStatic(SpawnTemplateName, SpawnCountryID)

  local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC

  local TemplateStatic, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate(SpawnTemplateName)

  if TemplateStatic then
    self.SpawnTemplatePrefix = SpawnTemplateName
    self.TemplateStaticUnit  = UTILS.DeepCopy(TemplateStatic.units[1])
    self.CountryID           = SpawnCountryID or CountryID
    self.CategoryID          = CategoryID
    self.CoalitionID         = CoalitionID
    self.SpawnIndex          = 0
    self.StaticCopyFrom      = SpawnTemplateName
  else
    error( "SPAWNSTATIC:New: There is no static declared in the mission editor with SpawnTemplatePrefix = '" .. tostring(SpawnTemplateName) .. "'" )
  end

  self:SetEventPriority( 5 )

  return self
end

--- Creates the main object to spawn a @{Wrapper.Static} given a template table.
-- @param #SPAWNSTATIC self
-- @param #table SpawnTemplate Template used for spawning.
-- @param DCS#country.id CountryID The ID of the country. Default `country.id.USA`.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:NewFromTemplate(SpawnTemplate, CountryID)

  local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC

  self.TemplateStaticUnit  = UTILS.DeepCopy(SpawnTemplate)
  self.SpawnTemplatePrefix = SpawnTemplate.name
  self.CountryID           = CountryID or country.id.USA

  return self
end

--- Creates the main object to spawn a @{Wrapper.Static} from a given type.
-- NOTE that you have to init many other parameters as spawn coordinate etc.
-- @param #SPAWNSTATIC self
-- @param #string StaticType Type of the static.
-- @param #string StaticCategory Category of the static, e.g. "Planes".
-- @param DCS#country.id CountryID The ID of the country. Default `country.id.USA`.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:NewFromType(StaticType, StaticCategory, CountryID)

  local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC

  self.InitStaticType=StaticType
  self.InitStaticCategory=StaticCategory
  self.CountryID=CountryID or country.id.USA
  self.SpawnTemplatePrefix=self.InitStaticType
  self.TemplateStaticUnit = {}

  self.InitStaticCoordinate=COORDINATE:New(0, 0, 0)
  self.InitStaticHeading=0

  return self
end

--- (Internal/Cargo) Init the resource table for STATIC object that should be spawned containing storage objects.
-- NOTE that you have to init many other parameters as the resources.
-- @param #SPAWNSTATIC self
-- @param #number CombinedWeight The weight this cargo object should have (some have fixed weights!), defaults to 1kg.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:_InitResourceTable(CombinedWeight)
  if not self.TemplateStaticUnit.resourcePayload then
    self.TemplateStaticUnit.resourcePayload = {
      ["weapons"] = {},
      ["aircrafts"] = {},
      ["gasoline"] = 0,
      ["diesel"] = 0,
      ["methanol_mixture"] = 0,
      ["jet_fuel"] = 0,   
    }
  end
  self:InitCargo(true)
  self:InitCargoMass(CombinedWeight or 1)
  return self
end

--- (User/Cargo) Add to resource table for STATIC object that should be spawned containing storage objects. Inits the object table if necessary and sets it to be cargo for helicopters.
-- @param #SPAWNSTATIC self
-- @param #string Type Type of cargo. Known types are: STORAGE.Type.WEAPONS, STORAGE.Type.LIQUIDS, STORAGE.Type.AIRCRAFT. Liquids are fuel.
-- @param #string Name Name of the cargo type. Liquids can be STORAGE.LiquidName.JETFUEL, STORAGE.LiquidName.GASOLINE, STORAGE.LiquidName.MW50 and STORAGE.LiquidName.DIESEL. The currently available weapon items are available in the `ENUMS.Storage.weapons`, e.g. `ENUMS.Storage.weapons.bombs.Mk_82Y`. Aircraft go by their typename.
-- @param #number Amount of tons (liquids) or number (everything else) to add.
-- @param #number CombinedWeight Combined weight to be set to this static cargo object. NOTE - some static cargo objects have fixed weights!
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:AddCargoResource(Type,Name,Amount,CombinedWeight)
  if not self.TemplateStaticUnit.resourcePayload then
    self:_InitResourceTable(CombinedWeight)
  end
  if Type == STORAGE.Type.LIQUIDS and type(Name) == "string" then
    self.TemplateStaticUnit.resourcePayload[Name] = Amount
  else
  self.TemplateStaticUnit.resourcePayload[Type] = {
    [Name] = {
      ["amount"] = Amount,
      }
  }
  end
  UTILS.PrintTableToLog(self.TemplateStaticUnit)
  return self
end

--- (User/Cargo) Resets resource table to zero for STATIC object that should be spawned containing storage objects. Inits the object table if necessary and sets it to be cargo for helicopters.
-- Handy if you spawn from cargo statics which have resources already set.
-- @param #SPAWNSTATIC self
-- @return #SPAWNSTATIC self 
function SPAWNSTATIC:ResetCargoResources()
  self.TemplateStaticUnit.resourcePayload = nil
  self:_InitResourceTable()
  return self
end

--- Initialize heading of the spawned static.
-- @param #SPAWNSTATIC self
-- @param Core.Point#COORDINATE Coordinate Position where the static is spawned.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitCoordinate(Coordinate)
  self.InitStaticCoordinate=Coordinate
  return self
end

--- Initialize heading of the spawned static.
-- @param #SPAWNSTATIC self
-- @param #number Heading The heading in degrees.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitHeading(Heading)
  self.InitStaticHeading=Heading
  return self
end

--- Initialize livery of the spawned static.
-- @param #SPAWNSTATIC self
-- @param #string LiveryName Name of the livery to use.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitLivery(LiveryName)
  self.InitStaticLivery=LiveryName
  return self
end

--- Initialize type of the spawned static.
-- @param #SPAWNSTATIC self
-- @param #string StaticType Type of the static, e.g. "FA-18C_hornet".
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitType(StaticType)
  self.InitStaticType=StaticType
  return self
end

--- Initialize shape of the spawned static. Required by some but not all statics.
-- @param #SPAWNSTATIC self
-- @param #string StaticShape Shape of the static, e.g. "carrier_tech_USA".
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitShape(StaticShape)
  self.InitStaticShape=StaticShape
  return self
end

--- Initialize parameters for spawning FARPs.
-- @param #SPAWNSTATIC self
-- @param #number CallsignID Callsign ID. Default 1 (="London").
-- @param #number Frequency Frequency in MHz. Default 127.5 MHz.
-- @param #number Modulation Modulation 0=AM, 1=FM.
-- @param #boolean DynamicSpawns If true, allow Dynamic Spawns
-- @param #boolean DynamicHotStarts If true, and DynamicSpawns is true, then allow Dynamic Spawns with hot starts.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitFARP(CallsignID, Frequency, Modulation, DynamicSpawns,DynamicHotStarts)
  self.InitFarp=true
  self.InitFarpCallsignID=CallsignID or 1
  self.InitFarpFreq=Frequency or 127.5
  self.InitFarpModu=Modulation or 0
  self.InitFarpDynamicSpawns = DynamicSpawns
  self.InitFarpDynamicHotStarts = (DynamicSpawns == true and DynamicHotStarts == true) and true or nil
  return self
end

--- Initialize cargo mass.
-- @param #SPAWNSTATIC self
-- @param #number Mass Mass of the cargo in kg.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitCargoMass(Mass)
  self.InitStaticCargoMass=Mass
  return self
end

--- Initialize as cargo.
-- @param #SPAWNSTATIC self
-- @param #boolean IsCargo If true, this static can act as cargo.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitCargo(IsCargo)
  self.InitStaticCargo=IsCargo
  return self
end

--- Initialize as dead.
-- @param #SPAWNSTATIC self
-- @param #boolean IsDead If true, this static is dead.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitDead(IsDead)
  self.InitStaticDead=IsDead
  return self
end

--- Initialize country of the spawned static. This determines the category.
-- @param #SPAWNSTATIC self
-- @param #string CountryID The country ID, e.g. country.id.USA.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitCountry(CountryID)
  self.CountryID=CountryID
  return self
end

--- Initialize name prefix statics get. This will be appended by "#0001", "#0002" etc.
-- @param #SPAWNSTATIC self
-- @param #string NamePrefix Name prefix of statics spawned. Will append #0001, etc to the name.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitNamePrefix(NamePrefix)
  self.SpawnTemplatePrefix=NamePrefix
  return self
end

--- Init link to a unit.
-- @param #SPAWNSTATIC self
-- @param Wrapper.Unit#UNIT Unit The unit to which the static is linked.
-- @param #number OffsetX Offset in X.
-- @param #number OffsetY Offset in Y.
-- @param #number OffsetAngle Offset angle in degrees.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitLinkToUnit(Unit, OffsetX, OffsetY, OffsetAngle)

  self.InitLinkUnit=Unit
  self.InitOffsetX=OffsetX or 0
  self.InitOffsetY=OffsetY or 0
  self.InitOffsetAngle=OffsetAngle or 0

  return self
end

--- Uses Disposition and other fallback logic to find a better and valid ground spawn position.
--- NOTE: This is not a spawn randomizer.
--- It will try to a find clear ground location avoiding trees, water, roads, runways, map scenery, other statics and other units in the area.
--- Uses the initial position if it's a valid location.
-- @param #SPAWNSTATIC self
-- @param #boolean OnOff Enable/disable the feature.
-- @param #number MaxRadius (Optional) Max radius to search for a valid ground location in meters. Default is 10 times the max radius of the static.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:InitValidateAndRepositionStatic(OnOff, MaxRadius)
    self.ValidateAndRepositionStatic = OnOff
    self.ValidateAndRepositionStaticMaxRadius = MaxRadius
    return self
end

--- Allows to place a CallFunction hook when a new static spawns.
-- The provided method will be called when a new group is spawned, including its given parameters.
-- The first parameter of the SpawnFunction is the @{Wrapper.Static#STATIC} that was spawned.
-- @param #SPAWNSTATIC self
-- @param #function SpawnCallBackFunction The function to be called when a group spawns.
-- @param SpawnFunctionArguments A random amount of arguments to be provided to the function when the group spawns.
-- @return #SPAWNSTATIC self
function SPAWNSTATIC:OnSpawnStatic( SpawnCallBackFunction, ... )
  self:F( "OnSpawnStatic" )

  self.SpawnFunctionHook = SpawnCallBackFunction
  self.SpawnFunctionArguments = {}
  if arg then
    self.SpawnFunctionArguments = arg
  end

  return self
end

--- Spawn a new STATIC object.
-- @param #SPAWNSTATIC self
-- @param #number Heading (Optional) The heading of the static, which is a number in degrees from 0 to 360. Default is the heading of the template.
-- @param #string NewName (Optional) The name of the new static.
-- @return Wrapper.Static#STATIC The static spawned.
function SPAWNSTATIC:Spawn(Heading, NewName)

  if Heading then
    self.InitStaticHeading=Heading
  end

  if NewName then
    self.InitStaticName=NewName
  end

  return self:_SpawnStatic(self.TemplateStaticUnit, self.CountryID)

end

--- Creates a new @{Wrapper.Static} from a COORDINATE.
-- @param #SPAWNSTATIC self
-- @param Core.Point#COORDINATE PointVec2 The 2D coordinate where to spawn the static.
-- @param #number Heading The heading of the static, which is a number in degrees from 0 to 360.
-- @param #string NewName (Optional) The name of the new static.
-- @return Wrapper.Static#STATIC The static spawned.
function SPAWNSTATIC:SpawnFromPointVec2(PointVec2, Heading, NewName)

  local vec2={x=PointVec2:GetX(), y=PointVec2:GetY()}

  local Coordinate=COORDINATE:NewFromVec2(vec2)

  return self:SpawnFromCoordinate(Coordinate, Heading, NewName)
end


--- Creates a new @{Wrapper.Static} from a COORDINATE.
-- @param #SPAWNSTATIC self
-- @param Core.Point#COORDINATE Coordinate The 3D coordinate where to spawn the static.
-- @param #number Heading (Optional) Heading The heading of the static in degrees. Default is 0 degrees.
-- @param #string NewName (Optional) The name of the new static.
-- @return Wrapper.Static#STATIC The spawned STATIC object.
function SPAWNSTATIC:SpawnFromCoordinate(Coordinate, Heading, NewName)

  -- Set up coordinate.
  self.InitStaticCoordinate=Coordinate

  if Heading then
    self.InitStaticHeading=Heading
  end

  if NewName then
    self.InitStaticName=NewName
  end

  return self:_SpawnStatic(self.TemplateStaticUnit, self.CountryID)
end


--- Creates a new @{Wrapper.Static} from a @{Core.Zone}.
-- @param #SPAWNSTATIC self
-- @param Core.Zone#ZONE_BASE Zone The Zone where to spawn the static.
-- @param #number Heading (Optional)The heading of the static in degrees. Default is the heading of the template.
-- @param #string NewName (Optional) The name of the new static.
-- @return Wrapper.Static#STATIC The static spawned.
function SPAWNSTATIC:SpawnFromZone(Zone, Heading, NewName)

  -- Spawn the new static at the center of the zone.
  --local Static = self:SpawnFromPointVec2( Zone:GetPointVec2(), Heading, NewName )
  local Static = self:SpawnFromCoordinate(Zone:GetCoordinate(), Heading, NewName)
    
  return Static
end

--- Spawns a new static using a given template. Additionally, the country ID needs to be specified, which also determines the coalition of the spawned static.
-- @param #SPAWNSTATIC self
-- @param #SPAWNSTATIC.TemplateData Template Spawn unit template.
-- @param #number CountryID The country ID.
-- @return Wrapper.Static#STATIC The static spawned.
function SPAWNSTATIC:_SpawnStatic(Template, CountryID)

  Template=Template or {}

  local CountryID=CountryID or self.CountryID

  if self.InitStaticType then
    Template.type=self.InitStaticType
  end

  if self.InitStaticCategory then
    Template.category=self.InitStaticCategory
  end

  if self.InitStaticCoordinate then
    Template.x   = self.InitStaticCoordinate.x
    Template.y   = self.InitStaticCoordinate.z
    Template.alt = self.InitStaticCoordinate.y
  end

  if self.InitStaticHeading then
    Template.heading = math.rad(self.InitStaticHeading)
  end

  if self.InitStaticShape then
    Template.shape_name=self.InitStaticShape
  end

  if self.InitStaticLivery then
    Template.livery_id=self.InitStaticLivery
  end

  if self.InitStaticDead~=nil then
    Template.dead=self.InitStaticDead
  end

  if self.InitStaticCargo~=nil then
    Template.canCargo=self.InitStaticCargo
  end

  if self.InitStaticCargoMass~=nil then
    Template.mass=self.InitStaticCargoMass
  end

  if self.InitLinkUnit then
    Template.linkUnit=self.InitLinkUnit:GetID()
    Template.linkOffset=true
    Template.offsets={}
    Template.offsets.y=self.InitOffsetY
    Template.offsets.x=self.InitOffsetX
    Template.offsets.angle=self.InitOffsetAngle and math.rad(self.InitOffsetAngle) or 0
  end

  if self.InitFarp then
    Template.heliport_callsign_id = self.InitFarpCallsignID
    Template.heliport_frequency   = self.InitFarpFreq
    Template.heliport_modulation  = self.InitFarpModu
    Template.unitId=nil
  end

  -- Increase spawn index counter.
  self.SpawnIndex = self.SpawnIndex + 1

  -- Name of the spawned static.
  Template.name = self.InitStaticName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex)

  -- Add static to the game.
  local Static=nil  --DCS#StaticObject

  if self.ValidateAndRepositionStatic then
    local validPos = UTILS.ValidateAndRepositionStatic(CountryID, Template.category, Template.type, Template, Template.shape_name, self.ValidateAndRepositionStaticMaxRadius)
    if validPos then
        Template.x = validPos.x
        Template.y = validPos.y
    end
  end

  if self.InitFarp then

    local TemplateGroup={}
    TemplateGroup.units={}
    TemplateGroup.units[1]=Template

    TemplateGroup.visible=true
    TemplateGroup.hidden=false
    TemplateGroup.x=Template.x
    TemplateGroup.y=Template.y
    TemplateGroup.name=Template.name
    
    if self.InitFarpDynamicSpawns == true then
      TemplateGroup.units[1].dynamicSpawn = true
      if self.InitFarpDynamicHotStarts == true then
        TemplateGroup.units[1].allowHotStart = true
      end
    end

    self:T("Spawning FARP")
    self:T({Template=Template})
    self:T({TemplateGroup=TemplateGroup})

    -- ED's dirty way to spawn FARPS.
    Static=coalition.addGroup(CountryID, -1, TemplateGroup)
    --Static=coalition.addStaticObject(CountryID, Template)
    
    -- Currently DCS 2.8 does not trigger birth events if FARPS are spawned!
    -- We create such an event. The airbase is registered in Core.Event
    local Event = {
      id = EVENTS.Birth,
      time = timer.getTime(),
      initiator = Static
      }
    -- Create BIRTH event.
    world.onEvent(Event)

  else
    self:T("Spawning Static")
    self:T2({Template=Template})
    Static=coalition.addStaticObject(CountryID, Template)
    
    if Static then
      self:T(string.format("Succesfully spawned static object \"%s\" ID=%d", Static:getName(), Static:getID()))
      --[[
      local static=StaticObject.getByName(Static:getName())
      if static then
        env.info(string.format("FF got static from StaticObject.getByName"))
      else
        env.error(string.format("FF error did NOT get static from StaticObject.getByName"))
      end ]]      
    else
      self:E(string.format("ERROR: DCS static object \"%s\" is nil!", tostring(Template.name)))
    end
  end
  
  -- Add and register the new static.
  local mystatic=_DATABASE:AddStatic(Template.name)
  
  -- If there is a SpawnFunction hook defined, call it.
  if self.SpawnFunctionHook then
    -- delay calling this for .3 seconds so that it hopefully comes after the BIRTH event of the group.
    self:ScheduleOnce(0.3, self.SpawnFunctionHook, mystatic, unpack(self.SpawnFunctionArguments))
  end
  
  if self.StaticCopyFrom ~= nil then
    mystatic.StaticCopyFrom = self.StaticCopyFrom
  end

  local TemplateGroup={}
  TemplateGroup.units={}
  TemplateGroup.units[1]=Template
  TemplateGroup.x=Template.x
  TemplateGroup.y=Template.y
  TemplateGroup.name=Template.name
  _DATABASE:_RegisterStaticTemplate( TemplateGroup, self.CoalitionID, self.CategoryID, CountryID )
  
  return mystatic
end
--- **Core** - Timer scheduler.
--
-- **Main Features:**
--
--    * Delay function calls
--    * Easy set up and little overhead
--    * Set start, stop and time interval
--    * Define max function calls
--
-- ===
--
-- ### Author: **funkyfranky**
-- @module Core.Timer
-- @image Core_Scheduler.JPG


--- TIMER class.
-- @type TIMER
-- @field #string ClassName Name of the class.
-- @field #string lid Class id string for output to DCS log file.
-- @field #number tid Timer ID returned by the DCS API function.
-- @field #number uid Unique ID of the timer.
-- @field #function func Timer function.
-- @field #table para Parameters passed to the timer function.
-- @field #number Tstart Relative start time in seconds.
-- @field #number Tstop Relative stop time in seconds.
-- @field #number dT Time interval between function calls in seconds.
-- @field #number ncalls Counter of function calls.
-- @field #number ncallsMax Max number of function calls. If reached, timer is stopped.
-- @field #boolean isrunning If `true`, timer is running. Else it was not started yet or was stopped.
-- @extends Core.Base#BASE

--- *Better three hours too soon than a minute too late.* - William Shakespeare
--
-- ===
--
-- # The TIMER Concept
-- 
-- The TIMER class is the little sister of the @{Core.Scheduler#SCHEDULER} class. It does the same thing but is a bit easier to use and has less overhead. It should be sufficient in many cases.
-- 
-- It provides an easy interface to the DCS [timer.scheduleFunction](https://wiki.hoggitworld.com/view/DCS_func_scheduleFunction).
-- 
-- # Construction
-- 
-- A new TIMER is created by the @{#TIMER.New}(*func*, *...*) function
-- 
--     local mytimer=TIMER:New(myfunction, a, b)
--     
-- The first parameter *func* is the function that is called followed by the necessary comma separeted parameters that are passed to that function.
-- 
-- ## Starting the Timer
-- 
-- The timer is started by the @{#TIMER.Start}(*Tstart*, *dT*, *Duration*) function
-- 
--     mytimer:Start(5, 1, 20)
--     
-- where
-- 
-- * *Tstart* is the relative start time in seconds. In the example, the first function call happens after 5 sec.
-- * *dT* is the time interval between function calls in seconds. Above, the function is called once per second.
-- * *Duration* is the duration in seconds after which the timer is stopped. This is relative to the start time. Here, the timer will run for 20 seconds.
--  
-- Note that
-- 
-- * if *Tstart* is not specified (*nil*), the first function call happens immediately, i.e. after one millisecond.
-- * if *dT* is not specified (*nil*), the function is called only once.
-- * if *Duration* is not specified (*nil*), the timer runs forever or until stopped manually or until the max function calls are reached (see below).
--
-- For example,
-- 
--     mytimer:Start(3)            -- Will call the function once after 3 seconds.
--     mytimer:Start(nil, 0.5)     -- Will call right now and then every 0.5 sec until all eternity.
--     mytimer:Start(nil, 2.0, 20) -- Will call right now and then every 2.0 sec for 20 sec.
--     mytimer:Start(1.0, nil, 10) -- Does not make sense as the function is only called once anyway. 
--
-- ## Stopping the Timer
--
-- The timer can be stopped manually by the @{#TIMER.Stop}(*Delay*) function
-- 
--     mytimer:Stop()
--
-- If the optional paramter *Delay* is specified, the timer is stopped after *delay* seconds.
-- 
-- ## Limit Function Calls
-- 
-- The timer can be stopped after a certain amount of function calles with the @{#TIMER.SetMaxFunctionCalls}(*Nmax*) function
-- 
--     mytimer:SetMaxFunctionCalls(20)
--
-- where *Nmax* is the number of calls after which the timer is stopped, here 20.
-- 
-- For example,
-- 
--     mytimer:SetMaxFunctionCalls(66):Start(1.0, 0.1)
--     
-- will start the timer after one second and call the function every 0.1 seconds. Once the function has been called 66 times, the timer is stopped.
-- 
--
-- @field #TIMER
TIMER = {
  ClassName      = "TIMER",
  lid            =   nil,
}

--- Timer ID.
_TIMERID=0

--- TIMER class version.
-- @field #string version
TIMER.version="0.2.0"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Randomization.
-- TODO: Pause/unpause.
-- DONE: Write docs.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new TIMER object.
-- @param #TIMER self
-- @param #function Function The function to call.
-- @param ... Parameters passed to the function if any.
-- @return #TIMER self
function TIMER:New(Function, ...)

  -- Inherit BASE.
  local self=BASE:Inherit(self, BASE:New()) --#TIMER
  
  -- Function to call.
  self.func=Function
  
  -- Function arguments.
  self.para=arg or {}
  
  -- Number of function calls.
  self.ncalls=0
  
  -- Not running yet.
  self.isrunning=false
  
  -- Increase counter
  _TIMERID=_TIMERID+1
  
  -- Set UID.
  self.uid=_TIMERID
  
  -- Log id.
  self.lid=string.format("TIMER UID=%d | ", self.uid)
  
  return self
end

--- Start TIMER object.
-- @param #TIMER self
-- @param #number Tstart Relative start time in seconds.
-- @param #number dT Interval between function calls in seconds. If not specified `nil`, the function is called only once.
-- @param #number Duration Time in seconds for how long the timer is running. If not specified `nil`, the timer runs forever or until stopped manually by the `TIMER:Stop()` function.
-- @return #TIMER self
function TIMER:Start(Tstart, dT, Duration)

  -- Current time.
  local Tnow=timer.getTime()

  -- Start time in sec.
  self.Tstart=Tstart and Tnow+math.max(Tstart, 0.001) or Tnow+0.001  -- one millisecond delay if Tstart=nil
  
  -- Set time interval.
  self.dT=dT
  
  -- Stop time.
  if Duration then
    self.Tstop=self.Tstart+Duration
  end
  
  -- Call DCS timer function.
  self.tid=timer.scheduleFunction(self._Function, self, self.Tstart)
  
  -- Set log id.
  self.lid=string.format("TIMER UID=%d/%d | ", self.uid, self.tid)
  
  -- Is now running.
  self.isrunning=true
  
  -- Debug info.
  self:T(self.lid..string.format("Starting Timer in %.3f sec, dT=%s, Tstop=%s", self.Tstart-Tnow, tostring(self.dT), tostring(self.Tstop)))

  return self
end

--- Start TIMER object if a condition is met. Useful for e.g. debugging.
-- @param #TIMER self
-- @param #boolean Condition Must be true for the TIMER to start
-- @param #number Tstart Relative start time in seconds.
-- @param #number dT Interval between function calls in seconds. If not specified `nil`, the function is called only once.
-- @param #number Duration Time in seconds for how long the timer is running. If not specified `nil`, the timer runs forever or until stopped manually by the `TIMER:Stop()` function.
-- @return #TIMER self
function TIMER:StartIf(Condition,Tstart, dT, Duration)
  if Condition then
    self:Start(Tstart, dT, Duration)
  end
  return self
end

--- Stop the timer by removing the timer function.
-- @param #TIMER self
-- @param #number Delay (Optional) Delay in seconds, before the timer is stopped.
-- @return #TIMER self
function TIMER:Stop(Delay)

  if Delay and Delay>0 then
  
    self.Tstop=timer.getTime()+Delay
    
  else

    if self.tid then
    
      -- Remove timer function.
      self:T(self.lid..string.format("Stopping timer by removing timer function after %d calls!", self.ncalls))

      -- We use a pcall here because if the DCS timer does not exist any more, it crashes the whole script!
      local status=pcall(
        function ()
          timer.removeFunction(self.tid)
        end 
      )
      
      -- Debug messages.
      if status then
        self:T2(self.lid..string.format("Stopped timer!"))
      else
        self:E(self.lid..string.format("WARNING: Could not remove timer function! isrunning=%s", tostring(self.isrunning)))
      end
      
      -- Not running any more.
      self.isrunning=false

    end
    
  end

  return self
end

--- Set max number of function calls. When the function has been called this many times, the TIMER is stopped.
-- @param #TIMER self
-- @param #number Nmax Set number of max function calls.
-- @return #TIMER self
function TIMER:SetMaxFunctionCalls(Nmax)
  self.ncallsMax=Nmax
  return self
end

--- Set time interval. Can also be set when the timer is already running and is applied after the next function call.
-- @param #TIMER self
-- @param #number dT Time interval in seconds.
-- @return #TIMER self
function TIMER:SetTimeInterval(dT)
  self.dT=dT
  return self
end

--- Check if the timer has been started and was not stopped.
-- @param #TIMER self
-- @return #boolean If `true`, the timer is running.
function TIMER:IsRunning()
  return self.isrunning
end

--- Call timer function.
-- @param #TIMER self
-- @param #number time DCS model time in seconds.
-- @return #number Time when the function is called again or `nil` if the timer is stopped.
function TIMER:_Function(time)

  -- Call function.
  self.func(unpack(self.para))
  
  -- Increase number of calls.
  self.ncalls=self.ncalls+1
  
  -- Next time.
  local Tnext=self.dT and time+self.dT or nil
  
  -- Check if we stop the timer.
  local stopme=false
  if Tnext==nil then
    -- No next time.
    self:T(self.lid..string.format("No next time as dT=nil ==> Stopping timer after %d function calls", self.ncalls))
    stopme=true
  elseif self.Tstop and Tnext>self.Tstop then
    -- Stop time passed.
    self:T(self.lid..string.format("Stop time passed %.3f > %.3f ==> Stopping timer after %d function calls", Tnext, self.Tstop, self.ncalls))
    stopme=true
  elseif self.ncallsMax and self.ncalls>=self.ncallsMax then
    -- Number of max function calls reached.
    self:T(self.lid..string.format("Max function calls Nmax=%d reached ==> Stopping timer after %d function calls", self.ncallsMax, self.ncalls))
    stopme=true
  end
  
  if stopme then
    -- Remove timer function.
    self:Stop()
    return nil
  else
    -- Call again in Tnext seconds.
    return Tnext
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- **Core** - Models the process to achieve goal(s).
--
-- ===
--
-- ## Features:
--
--   * Define the goal.
--   * Monitor the goal achievement.
--   * Manage goal contribution by players.
--
-- ===
--
-- Classes that implement a goal achievement, will derive from GOAL to implement the ways how the achievements can be realized.
--
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions: **funkyfranky**
--
-- ===
--
-- @module Core.Goal
-- @image Core_Goal.JPG

do -- Goal

  -- @type GOAL
  -- @extends Core.Fsm#FSM

  --- Models processes that have an objective with a defined achievement. Derived classes implement the ways how the achievements can be realized.
  --
  -- # 1. GOAL constructor
  --
  --   * @{#GOAL.New}(): Creates a new GOAL object.
  --
  -- # 2. GOAL is a finite state machine (FSM).
  --
  -- ## 2.1. GOAL States
  --
  --   * **Pending**: The goal object is in progress.
  --   * **Achieved**: The goal objective is Achieved.
  --
  -- ## 2.2. GOAL Events
  --
  --   * **Achieved**: Set the goal objective to Achieved.
  --
  -- # 3. Player contributions.
  --
  -- Goals are most of the time achieved by players. These player achievements can be registered as part of the goal achievement.
  -- Use @{#GOAL.AddPlayerContribution}() to add a player contribution to the goal.
  -- The player contributions are based on a points system, an internal counter per player.
  -- So once the goal has been achieved, the player contributions can be queried using @{#GOAL.GetPlayerContributions}(),
  -- that retrieves all contributions done by the players. For one player, the contribution can be queried using @{#GOAL.GetPlayerContribution}().
  -- The total amount of player contributions can be queried using @{#GOAL.GetTotalContributions}().
  --
  -- # 4. Goal achievement.
  --
  -- Once the goal is achieved, the mission designer will need to trigger the goal achievement using the **Achieved** event.
  -- The underlying 2 examples will achieve the goals for the `Goal` object:
  --
  --       Goal:Achieved() -- Achieve the goal immediately.
  --       Goal:__Achieved( 30 ) -- Achieve the goal within 30 seconds.
  --
  -- # 5. Check goal achievement.
  --
  -- The method @{#GOAL.IsAchieved}() will return true if the goal is achieved (the trigger **Achieved** was executed).
  -- You can use this method to check asynchronously if a goal has been achieved, for example using a scheduler.
  --
  -- @field #GOAL
  GOAL = {
    ClassName = "GOAL",
  }

  -- @field #table GOAL.Players
  GOAL.Players = {}

  -- @field #number GOAL.TotalContributions
  GOAL.TotalContributions = 0

  --- GOAL Constructor.
  -- @param #GOAL self
  -- @return #GOAL
  function GOAL:New()

    local self = BASE:Inherit( self, FSM:New() ) -- #GOAL
    self:F( {} )

    --- Achieved State for GOAL
    -- @field GOAL.Achieved

    --- Achieved State Handler OnLeave for GOAL
    -- @function [parent=#GOAL] OnLeaveAchieved
    -- @param #GOAL self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean

    --- Achieved State Handler OnEnter for GOAL
    -- @function [parent=#GOAL] OnEnterAchieved
    -- @param #GOAL self
    -- @param #string From
    -- @param #string Event
    -- @param #string To

    self:SetStartState( "Pending" )
    self:AddTransition( "*", "Achieved", "Achieved" )

    --- Achieved Handler OnBefore for GOAL
    -- @function [parent=#GOAL] OnBeforeAchieved
    -- @param #GOAL self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean

    --- Achieved Handler OnAfter for GOAL
    -- @function [parent=#GOAL] OnAfterAchieved
    -- @param #GOAL self
    -- @param #string From
    -- @param #string Event
    -- @param #string To

    --- Achieved Trigger for GOAL
    -- @function [parent=#GOAL] Achieved
    -- @param #GOAL self

    --- Achieved Asynchronous Trigger for GOAL
    -- @function [parent=#GOAL] __Achieved
    -- @param #GOAL self
    -- @param #number Delay

    self:SetEventPriority( 5 )

    return self
  end

  --- Add a new contribution by a player.
  -- @param #GOAL self
  -- @param #string PlayerName The name of the player.
  function GOAL:AddPlayerContribution( PlayerName )
    self:F( { PlayerName } )
    self.Players[PlayerName] = self.Players[PlayerName] or 0
    self.Players[PlayerName] = self.Players[PlayerName] + 1
    self.TotalContributions = self.TotalContributions + 1
  end

  -- @param #GOAL self
  -- @param #number Player contribution.
  function GOAL:GetPlayerContribution( PlayerName )
    return self.Players[PlayerName] or 0
  end

  --- Get the players who contributed to achieve the goal.
  -- The result is a list of players, sorted by the name of the players.
  -- @param #GOAL self
  -- @return #list The list of players, indexed by the player name.
  function GOAL:GetPlayerContributions()
    return self.Players or {}
  end

  --- Gets the total contributions that happened to achieve the goal.
  -- The result is a number.
  -- @param #GOAL self
  -- @return #number The total number of contributions. 0 is returned if there were no contributions (yet).
  function GOAL:GetTotalContributions()
    return self.TotalContributions or 0
  end

  --- Validates if the goal is achieved.
  -- @param #GOAL self
  -- @return #boolean true if the goal is achieved.
  function GOAL:IsAchieved()
    return self:Is( "Achieved" )
  end

end
--- **Core** - Management of spotting logistics, that can be activated and deactivated upon command.
--
-- ===
-- 
-- SPOT implements the DCS Spot class functionality, but adds additional luxury to be able to:
-- 
--   * Spot for a defined duration.
--   * Updates of laser spot position every 0.2 seconds for moving targets.
--   * Wiggle the spot at the target.
--   * Provide a @{Wrapper.Unit} as a target, instead of a point.
--   * Implement a status machine, LaseOn, LaseOff.
--
-- ===
-- 
-- # Demo Missions
--
-- ### [Demo Missions on GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS)
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
--   * **Ciribob**: Showing the way how to lase targets + how laser codes work!!! Explained the autolase script.
--   * **EasyEB**: Ideas and Beta Testing
--   * **Wingthor**: Beta Testing
-- 
-- ===
-- 
-- @module Core.Spot
-- @image Core_Spot.JPG


do

  ---
  -- @type SPOT
  -- @extends Core.Fsm#FSM


  --- Implements the target spotting or marking functionality, but adds additional luxury to be able to:
  -- 
  --   * Mark targets for a defined duration.
  --   * Updates of laser spot position every 0.25 seconds for moving targets.
  --   * Wiggle the spot at the target.
  --   * Provide a @{Wrapper.Unit} as a target, instead of a point.
  --   * Implement a status machine, LaseOn, LaseOff.
  -- 
  -- ## 1. SPOT constructor
  --   
  --   * @{#SPOT.New}(): Creates a new SPOT object.
  -- 
  -- ## 2. SPOT is a FSM
  -- 
  -- ![Process]()
  -- 
  -- ### 2.1 SPOT States
  -- 
  --   * **Off**: Lasing is switched off.
  --   * **On**: Lasing is switched on.
  --   * **Destroyed**: Target is destroyed.
  -- 
  -- ### 2.2 SPOT Events
  -- 
  --   * **@{#SPOT.LaseOn}(Target, LaserCode, Duration)**: Lase to a target.
  --   * **@{#SPOT.LaseOff}()**: Stop lasing the target.
  --   * **@{#SPOT.Lasing}()**: Target is being lased.
  --   * **@{#SPOT.Destroyed}()**: Triggered when target is destroyed.
  -- 
  -- ## 3. Check if a Target is being lased
  -- 
  -- The method @{#SPOT.IsLasing}() indicates whether lasing is on or off.
  -- 
  -- @field #SPOT
  SPOT = {
    ClassName = "SPOT",
  }
  
  --- SPOT Constructor.
  -- @param #SPOT self
  -- @param Wrapper.Unit#UNIT Recce Unit that is lasing
  -- @return #SPOT
  function SPOT:New( Recce )
  
    local self = BASE:Inherit( self, FSM:New() ) -- #SPOT
    self:F( {} )
    
    self:SetStartState( "Off" )
    self:AddTransition( "Off", "LaseOn", "On" )
    
    --- LaseOn Handler OnBefore for SPOT
    -- @function [parent=#SPOT] OnBeforeLaseOn
    -- @param #SPOT self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- LaseOn Handler OnAfter for SPOT
    -- @function [parent=#SPOT] OnAfterLaseOn
    -- @param #SPOT self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- LaseOn Trigger for SPOT
    -- @function [parent=#SPOT] LaseOn
    -- @param #SPOT self
    -- @param Wrapper.Positionable#POSITIONABLE Target
    -- @param #number LaserCode Laser code.
    -- @param #number Duration Duration of lasing in seconds.
    
    --- LaseOn Asynchronous Trigger for SPOT
    -- @function [parent=#SPOT] __LaseOn
    -- @param #SPOT self
    -- @param #number Delay
    -- @param Wrapper.Positionable#POSITIONABLE Target
    -- @param #number LaserCode Laser code.
    -- @param #number Duration Duration of lasing in seconds.

    self:AddTransition( "Off", "LaseOnCoordinate", "On" )
    
    --- LaseOnCoordinate Handler OnBefore for SPOT.
    -- @function [parent=#SPOT] OnBeforeLaseOnCoordinate
    -- @param #SPOT self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- LaseOnCoordinate Handler OnAfter for SPOT.
    -- @function [parent=#SPOT] OnAfterLaseOnCoordinate
    -- @param #SPOT self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- LaseOnCoordinate Trigger for SPOT.
    -- @function [parent=#SPOT] LaseOnCoordinate
    -- @param #SPOT self
    -- @param Core.Point#COORDINATE Coordinate The coordinate to lase.
    -- @param #number LaserCode Laser code.
    -- @param #number Duration Duration of lasing in seconds.
    
    --- LaseOn Asynchronous Trigger for SPOT
    -- @function [parent=#SPOT] __LaseOn
    -- @param #SPOT self
    -- @param #number Delay
    -- @param Wrapper.Positionable#POSITIONABLE Target
    -- @param #number LaserCode Laser code.
    -- @param #number Duration Duration of lasing in seconds.

    
    
    self:AddTransition( "On",  "Lasing", "On" )
    self:AddTransition( { "On", "Destroyed" } , "LaseOff", "Off" )
    
    --- LaseOff Handler OnBefore for SPOT
    -- @function [parent=#SPOT] OnBeforeLaseOff
    -- @param #SPOT self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- LaseOff Handler OnAfter for SPOT
    -- @function [parent=#SPOT] OnAfterLaseOff
    -- @param #SPOT self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- LaseOff Trigger for SPOT
    -- @function [parent=#SPOT] LaseOff
    -- @param #SPOT self
    
    --- LaseOff Asynchronous Trigger for SPOT
    -- @function [parent=#SPOT] __LaseOff
    -- @param #SPOT self
    -- @param #number Delay
    
    self:AddTransition( "*" , "Destroyed", "Destroyed" )
    
    --- Destroyed Handler OnBefore for SPOT
    -- @function [parent=#SPOT] OnBeforeDestroyed
    -- @param #SPOT self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Destroyed Handler OnAfter for SPOT
    -- @function [parent=#SPOT] OnAfterDestroyed
    -- @param #SPOT self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Destroyed Trigger for SPOT
    -- @function [parent=#SPOT] Destroyed
    -- @param #SPOT self
    
    --- Destroyed Asynchronous Trigger for SPOT
    -- @function [parent=#SPOT] __Destroyed
    -- @param #SPOT self
    -- @param #number Delay
    
    
  
    self.Recce = Recce
    
    self.RecceName = self.Recce:GetName()
  
    self.LaseScheduler = SCHEDULER:New( self )
  
    self:SetEventPriority( 5 )
    
    self.Lasing = false
  
    return self
  end
  
  --- On after LaseOn event. Activates the laser spot.
  -- @param #SPOT self
  -- @param From
  -- @param Event
  -- @param To
  -- @param Wrapper.Positionable#POSITIONABLE Target Unit that is being lased.
  -- @param #number LaserCode Laser code.
  -- @param #number Duration Duration of lasing in seconds.
  function SPOT:onafterLaseOn( From, Event, To, Target, LaserCode, Duration )
    self:T({From, Event, To})
    self:T2( { "LaseOn", Target, LaserCode, Duration } )

    local function StopLase( self )
      self:LaseOff()
    end
    
    self.Target = Target
    
    self.TargetName = Target:GetName()
    
    self.LaserCode = LaserCode
    
    self.Lasing = true
    
    local RecceDcsUnit = self.Recce:GetDCSObject()
    
    local relativespot = self.relstartpos or { x = 0, y = 2, z = 0 }
    
    self.SpotIR = Spot.createInfraRed( RecceDcsUnit, relativespot, Target:GetPointVec3():AddY(1):GetVec3() )
    self.SpotLaser = Spot.createLaser( RecceDcsUnit, relativespot, Target:GetPointVec3():AddY(1):GetVec3(), LaserCode )

    if Duration then
      self.ScheduleID = self.LaseScheduler:Schedule( self, StopLase, {self}, Duration )
    end
    
    self:HandleEvent( EVENTS.Dead )
    
    self:__Lasing( -1 )
    
    return self
  end
  
  
  --- On after LaseOnCoordinate event. Activates the laser spot.
  -- @param #SPOT self
  -- @param From
  -- @param Event
  -- @param To
  -- @param Core.Point#COORDINATE Coordinate The coordinate at which the laser is pointing.
  -- @param #number LaserCode Laser code.
  -- @param #number Duration Duration of lasing in seconds.
  function SPOT:onafterLaseOnCoordinate(From, Event, To, Coordinate, LaserCode, Duration)
    self:T2( { "LaseOnCoordinate", Coordinate, LaserCode, Duration } )

    local function StopLase( self )
      self:LaseOff()
    end
    
    self.Target = nil
    self.TargetCoord=Coordinate
    self.LaserCode = LaserCode
    
    self.Lasing = true
    
    local RecceDcsUnit = self.Recce:GetDCSObject()
    
    self.SpotIR = Spot.createInfraRed( RecceDcsUnit, { x = 0, y = 1, z = 0 }, Coordinate:GetVec3() )
    self.SpotLaser = Spot.createLaser( RecceDcsUnit, { x = 0, y = 1, z = 0 }, Coordinate:GetVec3(), LaserCode )

    if Duration then
      self.ScheduleID = self.LaseScheduler:Schedule( self, StopLase, {self}, Duration )
    end
    
    self:__Lasing(-1)
    return self
  end  
  
  ---
  -- @param #SPOT self
  -- @param Core.Event#EVENTDATA EventData
  function SPOT:OnEventDead(EventData)
    self:T2( { Dead = EventData.IniDCSUnitName, Target = self.Target } )
    if self.Target then
      if EventData.IniDCSUnitName == self.TargetName then
        self:F( {"Target dead ", self.TargetName } )
        self:Destroyed()
        self:LaseOff()
      end
    end
    if self.Recce then
      if EventData.IniDCSUnitName == self.RecceName then
        self:F( {"Recce dead ", self.RecceName } )
        self:LaseOff()
      end
    end
    return self
  end
  
  ---
  -- @param #SPOT self
  -- @param From
  -- @param Event
  -- @param To
  function SPOT:onafterLasing( From, Event, To )
    self:T({From, Event, To})
    
    if self.Lasing then
      if self.Target and self.Target:IsAlive() then
        
        self.SpotIR:setPoint( self.Target:GetPointVec3():AddY(1):AddY(math.random(-100,100)/200):AddX(math.random(-100,100)/200):GetVec3() )
        self.SpotLaser:setPoint( self.Target:GetPointVec3():AddY(1):GetVec3() )
        
        self:__Lasing(0.2)
      elseif self.TargetCoord then
      
        -- Wiggle the IR spot a bit.  
        local irvec3={x=self.TargetCoord.x+math.random(-100,100)/200, y=self.TargetCoord.y+math.random(-100,100)/200, z=self.TargetCoord.z} --#DCS.Vec3
        local lsvec3={x=self.TargetCoord.x, y=self.TargetCoord.y, z=self.TargetCoord.z} --#DCS.Vec3
        
        self.SpotIR:setPoint(irvec3)
        self.SpotLaser:setPoint(lsvec3)
        
        self:__Lasing(0.2)    
      else
        self:F( { "Target is not alive", self.Target:IsAlive() } )
      end
    end
    return self
  end
  
  ---
  -- @param #SPOT self
  -- @param From
  -- @param Event
  -- @param To
  -- @return #SPOT
  function SPOT:onafterLaseOff( From, Event, To )
    self:T({From, Event, To})
    
    self:T2( {"Stopped lasing for ", self.Target and self.Target:GetName() or "coord", SpotIR = self.SportIR, SpotLaser = self.SpotLaser } )
    
    self.Lasing = false
    
    self.SpotIR:destroy()
    self.SpotLaser:destroy()

    self.SpotIR = nil
    self.SpotLaser = nil
    
    if self.ScheduleID then
      self.LaseScheduler:Stop(self.ScheduleID)
    end
    self.ScheduleID = nil
    
    self.Target = nil
    
    return self
  end
  
  --- Check if the SPOT is lasing
  -- @param #SPOT self
  -- @return #boolean true if it is lasing
  function SPOT:IsLasing()
    return self.Lasing
  end
  
  --- Set laser start position relative to the lasing unit.
  -- @param #SPOT self
  -- @param #table position Start position of the laser relative to the lasing unit. Default is { x = 0, y = 2, z = 0 }
  -- @return #SPOT self
  -- @usage
  --      -- Set lasing position to be the position of the optics of the Gazelle M:
  --      myspot:SetRelativeStartPosition({ x = 1.7, y = 1.2, z = 0 })
  function SPOT:SetRelativeStartPosition(position)
    self.relstartpos = position or { x = 0, y = 2, z = 0 }
    return self
  end
  
end
--- **Core** - Tap into markers added to the F10 map by users.
--
-- **Main Features:**
--
--    * Create an easy way to tap into markers added to the F10 map by users.
--    * Recognize own tag and list of keywords.
--    * Matched keywords are handed down to functions.
-- ##Listen for your tag
--    myMarker = MARKEROPS_BASE:New("tag", {}, false)
--    function myMarker:OnAfterMarkChanged(From, Event, To, Text, Keywords, Coord, idx)
--
--    end
-- Make sure to use the "MarkChanged" event as "MarkAdded" comes in right after the user places a blank marker and your callback will never be called.
--
-- ===
--
-- ### Author: **Applevangelist**
-- 
-- Date: 5 May 2021
-- Last Update: Mar 2023
-- 
-- ===
---
-- @module Core.MarkerOps_Base
-- @image MOOSE_Core.JPG

--------------------------------------------------------------------------
-- MARKEROPS_BASE Class Definition.
--------------------------------------------------------------------------

--- MARKEROPS_BASE class.
-- @type MARKEROPS_BASE
-- @field #string ClassName Name of the class.
-- @field #string Tag Tag to identify commands.
-- @field #table Keywords Table of keywords to recognize.
-- @field #string version Version of #MARKEROPS_BASE.
-- @field #boolean Casesensitive Enforce case when identifying the Tag, i.e. tag ~= Tag
-- @extends Core.Fsm#FSM

--- *Fiat lux.* -- Latin proverb.
--
-- ===
--
-- # The MARKEROPS_BASE Concept
-- 
-- This class enable scripting text-based actions from markers.
-- 
-- @field #MARKEROPS_BASE
MARKEROPS_BASE = {
  ClassName = "MARKEROPS",
  Tag = "mytag",
  Keywords = {},
  version = "0.1.4",
  debug = false,
  Casesensitive = true,
}

--- Function to instantiate a new #MARKEROPS_BASE object.
-- @param #MARKEROPS_BASE self
-- @param #string Tagname Name to identify us from the event text.
-- @param #table Keywords Table of keywords  recognized from the event text.
-- @param #boolean Casesensitive (Optional) Switch case sensitive identification of Tagname. Defaults to true.
-- @return #MARKEROPS_BASE self
function MARKEROPS_BASE:New(Tagname,Keywords,Casesensitive)
   -- Inherit FSM
  local self=BASE:Inherit(self, FSM:New()) -- #MARKEROPS_BASE
  
    -- Set some string id for output to DCS.log file.
  self.lid=string.format("MARKEROPS_BASE %s | ", tostring(self.version))
  
  self.Tag = Tagname or "mytag"-- #string
  self.Keywords = Keywords or {} -- #table - might want to use lua regex here, too
  self.debug = false
  self.Casesensitive = true
  
  if Casesensitive and Casesensitive == false then
    self.Casesensitive = false
  end
  
  -----------------------
  --- FSM Transitions ---
  -----------------------
  
  -- Start State.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  --                 From State  -->   Event   -->   To State
  self:AddTransition("Stopped", "Start",        "Running")        -- Start the FSM.
  self:AddTransition("*",       "MarkAdded",    "*")        -- Start the FSM.
  self:AddTransition("*",       "MarkChanged",   "*")        -- Start the FSM.
  self:AddTransition("*",       "MarkDeleted",  "*")        -- Start the FSM.
  self:AddTransition("Running", "Stop",         "Stopped")        -- Stop the FSM.
  
  self:HandleEvent(EVENTS.MarkAdded, self.OnEventMark)
  self:HandleEvent(EVENTS.MarkChange, self.OnEventMark)
  self:HandleEvent(EVENTS.MarkRemoved, self.OnEventMark)
  
  -- start
  self:I(self.lid..string.format("started for %s",self.Tag))
  self:__Start(1)
  return self
  
  -------------------
  -- PSEUDO Functions
  -------------------
  
   --- On after "MarkAdded" event. Triggered when a Marker is added to the F10 map.
   -- @function [parent=#MARKEROPS_BASE] OnAfterMarkAdded
   -- @param #MARKEROPS_BASE self
   -- @param #string From The From state.
   -- @param #string Event The Event called.
   -- @param #string To The To state.
   -- @param #string Text The text on the marker.
   -- @param #table Keywords Table of matching keywords found in the Event text.
   -- @param Core.Point#COORDINATE Coord Coordinate of the marker.
   -- @param #number MarkerID Id of this marker.
   -- @param #number CoalitionNumber Coalition of the marker creator.
   -- @param #string PlayerName Name of the player creating/changing the mark. nil if it cannot be obtained. 
   -- @param Core.Event#EVENTDATA EventData the event data table. 
   
   --- On after "MarkChanged" event. Triggered when a Marker is changed on the F10 map.
   -- @function [parent=#MARKEROPS_BASE] OnAfterMarkChanged
   -- @param #MARKEROPS_BASE self
   -- @param #string From The From state.
   -- @param #string Event The Event called.
   -- @param #string To The To state.
   -- @param #string Text The text on the marker.
   -- @param #table Keywords Table of matching keywords found in the Event text.
   -- @param Core.Point#COORDINATE Coord Coordinate of the marker.
   -- @param #number MarkerID Id of this marker.
   -- @param #number CoalitionNumber Coalition of the marker creator.
   -- @param #string PlayerName Name of the player creating/changing the mark. nil if it cannot be obtained. 
   -- @param Core.Event#EVENTDATA EventData the event data table

   --- On after "MarkDeleted" event. Triggered when a Marker is deleted from the F10 map.
   -- @function [parent=#MARKEROPS_BASE] OnAfterMarkDeleted
   -- @param #MARKEROPS_BASE self
   -- @param #string From The From state
   -- @param #string Event The Event called
   -- @param #string To The To state
   
   --- "Stop" trigger. Used to stop the function an unhandle events
   -- @function [parent=#MARKEROPS_BASE] Stop

end

--- (internal) Handle events.
-- @param #MARKEROPS_BASE self
-- @param Core.Event#EVENTDATA Event
function MARKEROPS_BASE:OnEventMark(Event)
  self:T({Event})
    if Event == nil or Event.idx == nil then
      self:E("Skipping onEvent. Event or Event.idx unknown.")
      return true
    end

    local coalition = Event.MarkCoalition
    -- decision
    if Event.id==world.event.S_EVENT_MARK_ADDED then
      self:T({event="S_EVENT_MARK_ADDED", carrier=Event.IniGroupName, vec3=Event.pos})
      -- Handle event
      local Eventtext = tostring(Event.text)
      if Eventtext~=nil then
        if self:_MatchTag(Eventtext) then
          local coord=COORDINATE:NewFromVec3({y=Event.pos.y, x=Event.pos.x, z=Event.pos.z})
          if self.debug then
            local coordtext = coord:ToStringLLDDM()
            local text = tostring(Event.text)
            local m = MESSAGE:New(string.format("Mark added at %s with text: %s",coordtext,text),10,"Info",false):ToAll()
          end
          local matchtable = self:_MatchKeywords(Eventtext)
          self:MarkAdded(Eventtext,matchtable,coord,Event.idx,coalition,Event.PlayerName,Event)
        end
      end
    elseif Event.id==world.event.S_EVENT_MARK_CHANGE then
      self:T({event="S_EVENT_MARK_CHANGE", carrier=Event.IniGroupName, vec3=Event.pos})
      -- Handle event.
      local Eventtext = tostring(Event.text)
      if Eventtext~=nil then
        if self:_MatchTag(Eventtext) then
           local coord=COORDINATE:NewFromVec3({y=Event.pos.y, x=Event.pos.x, z=Event.pos.z})
           if self.debug then
              local coordtext = coord:ToStringLLDDM()
              local text = tostring(Event.text)
              local m = MESSAGE:New(string.format("Mark changed at %s with text: %s",coordtext,text),10,"Info",false):ToAll()
           end
           local matchtable = self:_MatchKeywords(Eventtext)
           self:MarkChanged(Eventtext,matchtable,coord,Event.idx,coalition,Event.PlayerName,Event)
        end
      end
    elseif Event.id==world.event.S_EVENT_MARK_REMOVED then
      self:T({event="S_EVENT_MARK_REMOVED", carrier=Event.IniGroupName, vec3=Event.pos})
      -- Hande event.
      local Eventtext = tostring(Event.text)
      if Eventtext~=nil then
        if self:_MatchTag(Eventtext) then
         self:MarkDeleted()
        end
      end
    end
end

--- (internal) Match tag.
-- @param #MARKEROPS_BASE self
-- @param #string Eventtext Text added to the marker.
-- @return #boolean
function MARKEROPS_BASE:_MatchTag(Eventtext)
  local matches = false
  if not self.Casesensitive then
    local type = string.lower(self.Tag) -- #string
    if string.find(string.lower(Eventtext),type) then
      matches = true --event text contains tag
    end
  else
    local type = self.Tag -- #string
    if string.find(Eventtext,type) then
      matches = true --event text contains tag
    end
  end
  return matches
end

--- (internal) Match keywords table.
-- @param #MARKEROPS_BASE self
-- @param #string Eventtext Text added to the marker.
-- @return #table
function MARKEROPS_BASE:_MatchKeywords(Eventtext)
  local matchtable = {}
  local keytable = self.Keywords
  for _index,_word in pairs (keytable) do
    if string.find(string.lower(Eventtext),string.lower(_word))then
      table.insert(matchtable,_word)
    end
  end
  return matchtable
end

--- On before "MarkAdded" event. Triggered when a Marker is added to the F10 map.
 -- @param #MARKEROPS_BASE self
 -- @param #string From The From state
 -- @param #string Event The Event called
 -- @param #string To The To state
 -- @param #string Text The text on the marker
 -- @param #table Keywords Table of matching keywords found in the Event text
 -- @param #number MarkerID Id of this marker
 -- @param #number CoalitionNumber Coalition of the marker creator
 -- @param Core.Point#COORDINATE Coord Coordinate of the marker.
function MARKEROPS_BASE:onbeforeMarkAdded(From,Event,To,Text,Keywords,Coord,MarkerID,CoalitionNumber)
  self:T({self.lid,From,Event,To,Text,Keywords,Coord:ToStringLLDDM()})
end

--- On before "MarkChanged" event. Triggered when a Marker is changed on the F10 map.
 -- @param #MARKEROPS_BASE self
 -- @param #string From The From state
 -- @param #string Event The Event called
 -- @param #string To The To state
 -- @param #string Text The text on the marker
 -- @param #table Keywords Table of matching keywords found in the Event text
 -- @param #number MarkerID Id of this marker
 -- @param #number CoalitionNumber Coalition of the marker creator
 -- @param Core.Point#COORDINATE Coord Coordinate of the marker.
function MARKEROPS_BASE:onbeforeMarkChanged(From,Event,To,Text,Keywords,Coord,MarkerID,CoalitionNumber)
  self:T({self.lid,From,Event,To,Text,Keywords,Coord:ToStringLLDDM()})
end

--- On before "MarkDeleted" event. Triggered when a Marker is removed from the F10 map.
 -- @param #MARKEROPS_BASE self
 -- @param #string From The From state
 -- @param #string Event The Event called
 -- @param #string To The To state
function MARKEROPS_BASE:onbeforeMarkDeleted(From,Event,To)
  self:T({self.lid,From,Event,To})
end

--- On enter "Stopped" event. Unsubscribe events.
 -- @param #MARKEROPS_BASE self
 -- @param #string From The From state
 -- @param #string Event The Event called
 -- @param #string To The To state
function MARKEROPS_BASE:onenterStopped(From,Event,To)
  self:T({self.lid,From,Event,To})
  -- unsubscribe from events
  self:UnHandleEvent(EVENTS.MarkAdded)
  self:UnHandleEvent(EVENTS.MarkChange)
  self:UnHandleEvent(EVENTS.MarkRemoved)
end

--------------------------------------------------------------------------
-- MARKEROPS_BASE Class Definition End.
--------------------------------------------------------------------------
--- **Core** - A Moose GetText system.
--
-- ===
-- 
-- ## Main Features:
--
--    * A GetText for Moose
--    * Build a set of localized text entries, alongside their sounds and subtitles
--    * Aimed at class developers to offer localizable language support
--
-- ===
--
-- ## Example Missions:
-- 
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/).
--       
-- ===
-- 
-- ### Author: **applevangelist**
-- ## Date: April 2022
-- 
-- ===
-- 
-- @module Core.TextAndSound
-- @image MOOSE.JPG

--- Text and Sound class.
-- @type TEXTANDSOUND
-- @field #string ClassName Name of this class.
-- @field #string version Versioning.
-- @field #string lid LID for log entries.
-- @field #string locale Default locale of this object.
-- @field #table entries Table of entries.
-- @field #string textclass Name of the class the texts belong to.
-- @extends Core.Base#BASE

---
--
-- @field #TEXTANDSOUND
TEXTANDSOUND = {
  ClassName = "TEXTANDSOUND",
  version = "0.0.1",
  lid = "",
  locale = "en",
  entries = {},
  textclass = "",
}

--- Text and Sound entry.
-- @type TEXTANDSOUND.Entry
-- @field #string Classname Name of the class this entry is for.
-- @field #string Locale Locale of this entry, defaults to "en".
-- @field #table Data The list of entries.

--- Text and Sound data
-- @type TEXTANDSOUND.Data
-- @field #string ID ID of this entry for retrieval.
-- @field #string Text Text of this entry.
-- @field #string Soundfile (optional) Soundfile File name of the corresponding sound file.
-- @field #number Soundlength (optional)  Length of the sound file in seconds.
-- @field #string Subtitle (optional)  Subtitle for the sound file.

--- Instantiate a new object
-- @param #TEXTANDSOUND self
-- @param #string ClassName Name of the class this instance is providing texts for.
-- @param #string Defaultlocale (Optional) Default locale of this instance, defaults to "en". 
-- @return #TEXTANDSOUND self
function TEXTANDSOUND:New(ClassName,Defaultlocale)
    -- Inherit everything from BASE class.
  local self=BASE:Inherit(self, BASE:New())
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", self.ClassName, self.version)
  self.locale = Defaultlocale or (_SETTINGS:GetLocale() or "en")
  self.textclass = ClassName or "none"
  self.entries = {}
  local initentry = {} -- #TEXTANDSOUND.Entry
  initentry.Classname = ClassName
  initentry.Data = {}
  initentry.Locale = self.locale
  self.entries[self.locale] = initentry
  self:I(self.lid .. "Instantiated.")
  self:T({self.entries[self.locale]})
  return self
end

--- Add an entry
-- @param #TEXTANDSOUND self
-- @param #string Locale Locale to set for this entry, e.g. "de".
-- @param #string ID Unique(!) ID of this entry under this locale (i.e. use the same ID to get localized text for the entry in another language).
-- @param #string Text Text for this entry.
-- @param #string Soundfile (Optional) Sound file name for this entry.
-- @param #number Soundlength (Optional) Length of the sound file in seconds.
-- @param #string Subtitle (Optional) Subtitle to be used alongside the sound file.
-- @return #TEXTANDSOUND self
function TEXTANDSOUND:AddEntry(Locale,ID,Text,Soundfile,Soundlength,Subtitle)
  self:T(self.lid .. "AddEntry")
  local locale = Locale or self.locale
  local dataentry = {} -- #TEXTANDSOUND.Data
  dataentry.ID = ID or "1"
  dataentry.Text = Text or "none"
  dataentry.Soundfile = Soundfile
  dataentry.Soundlength = Soundlength or 0
  dataentry.Subtitle = Subtitle
  if not self.entries[locale] then
    local initentry = {} -- #TEXTANDSOUND.Entry
    initentry.Classname = self.textclass -- class name entry
    initentry.Data = {} -- data array
    initentry.Locale = locale -- default locale
    self.entries[locale] = initentry
  end
  self.entries[locale].Data[ID] = dataentry
  self:T({self.entries[locale].Data})
  return self
end

--- Get an entry
-- @param #TEXTANDSOUND self
-- @param #string ID The unique ID of the data to be retrieved.
-- @param #string Locale (Optional) The locale of the text to be retrieved - defauls to default locale set with `New()`.
-- @return #string Text Text or nil if not found and no fallback.
-- @return #string Soundfile Filename or nil if not found and no fallback.
-- @return #string Soundlength Length of the sound or 0 if not found and no fallback.
-- @return #string Subtitle Text for subtitle or nil if not found and no fallback.
function TEXTANDSOUND:GetEntry(ID,Locale)
  self:T(self.lid .. "GetEntry")
  local locale = Locale or self.locale
  if not self.entries[locale] then
    -- fall back to default "en"
    locale = self.locale
  end
  local Text,Soundfile,Soundlength,Subtitle = nil, nil, 0, nil
  if self.entries[locale] then
    if self.entries[locale].Data then
      local data = self.entries[locale].Data[ID] -- #TEXTANDSOUND.Data
      if data then 
        Text = data.Text
        Soundfile = data.Soundfile
        Soundlength = data.Soundlength
        Subtitle = data.Subtitle
      elseif self.entries[self.locale].Data[ID] then
        -- no matching entry, try default
        local data = self.entries[self.locale].Data[ID]
        Text = data.Text
        Soundfile = data.Soundfile
        Soundlength = data.Soundlength
        Subtitle = data.Subtitle
      end
    end
  else
    return nil, nil, 0, nil
  end
  return Text,Soundfile,Soundlength,Subtitle
end

--- Get the default locale of this object
-- @param #TEXTANDSOUND self
-- @return #string locale
function TEXTANDSOUND:GetDefaultLocale()
  self:T(self.lid .. "GetDefaultLocale")
  return self.locale
end

--- Set default locale of this object
-- @param #TEXTANDSOUND self
-- @param #string locale
-- @return #TEXTANDSOUND self 
function TEXTANDSOUND:SetDefaultLocale(locale)
  self:T(self.lid .. "SetDefaultLocale")
  self.locale = locale or "en"
  return self
end

--- Check if a locale exists
-- @param #TEXTANDSOUND self
-- @return #boolean outcome
function TEXTANDSOUND:HasLocale(Locale)
  self:T(self.lid .. "HasLocale")
  return self.entries[Locale] and true or false
end

--- Flush all entries to the log
-- @param #TEXTANDSOUND self
-- @return #TEXTANDSOUND self
function TEXTANDSOUND:FlushToLog()
  self:I(self.lid .. "Flushing entries:")
  local text = string.format("Textclass: %s | Default Locale: %s",self.textclass, self.locale)
  for _,_entry in pairs(self.entries) do
    local entry = _entry -- #TEXTANDSOUND.Entry
    local text = string.format("Textclassname: %s | Locale: %s",entry.Classname, entry.Locale)
    self:I(text)
    for _ID,_data in pairs(entry.Data) do
      local data = _data -- #TEXTANDSOUND.Data
      local text = string.format("ID: %s\nText: %s\nSoundfile: %s With length: %d\nSubtitle: %s",tostring(_ID), data.Text or "none",data.Soundfile or "none",data.Soundlength or 0,data.Subtitle or "none")
      self:I(text)
    end
  end
  return self
end

----------------------------------------------------------------
-- End TextAndSound
----------------------------------------------------------------
--- **Core** - Path from A to B.
--
-- **Main Features:**
--
--    * Path from A to B
--    * Arbitrary number of points
--    * Automatically from lines drawtool
--
-- ===
--
-- ### Author: **funkyfranky**
-- 
-- ===
-- @module Core.Pathline
-- @image CORE_Pathline.png


--- PATHLINE class.
-- @type PATHLINE
-- @field #string ClassName Name of the class.
-- @field #string lid Class id string for output to DCS log file.
-- @field #string name Name of the path line.
-- @field #table points List of 3D points defining the path.
-- @extends Core.Base#BASE

--- *The shortest distance between two points is a straight line.* -- Archimedes
--
-- ===
--
-- # The PATHLINE Concept
-- 
-- List of points defining a path from A to B. The pathline can consist of multiple points. Each point holds the information of its position, the surface type, the land height
-- and the water depth (if over sea).
-- 
-- Line drawings created in the mission editor are automatically registered as pathlines and stored in the MOOSE database.
-- They can be accessed with the @{#PATHLINE.FindByName) function.
-- 
-- # Constructor
-- 
-- The @{PATHLINE.New) function creates a new PATHLINE object. This does not hold any points. Points can be added with the @{#PATHLINE.AddPointFromVec2} and @{#PATHLINE.AddPointFromVec3}
-- 
-- For a given table of 2D or 3D positions, a new PATHLINE object can be created with the @{#PATHLINE.NewFromVec2Array} or @{#PATHLINE.NewFromVec3Array}, respectively.
-- 
-- # Line Drawings
-- 
-- The most convenient way to create a pathline is the draw panel feature in the DCS mission editor. You can select "Line" and then "Segments", "Segment" or "Free" to draw your lines.
-- These line drawings are then automatically added to the MOOSE database as PATHLINE objects and can be retrieved with the @{#PATHLINE.FindByName) function, where the name is the one
-- you specify in the draw panel.
-- 
-- # Mark on F10 map
-- 
-- The ponints of the PATHLINE can be marked on the F10 map with the @{#PATHLINE.MarkPoints}(`true`) function. The mark points contain information of the surface type, land height and 
-- water depth.
-- 
-- To remove the marks, use @{#PATHLINE.MarkPoints}(`false`).
--
-- @field #PATHLINE
PATHLINE = {
  ClassName      = "PATHLINE",
  lid            =   nil,
  points         =    {},
}

--- Point of line.
-- @type PATHLINE.Point
-- @field DCS#Vec3 vec3 3D position.
-- @field DCS#Vec2 vec2 2D position.
-- @field #number surfaceType Surface type.
-- @field #number landHeight Land height in meters.
-- @field #number depth Water depth in meters.
-- @field #number markerID Marker ID.


--- PATHLINE class version.
-- @field #string version
PATHLINE.version="0.1.1"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: A lot...

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new PATHLINE object. Points need to be added later.
-- @param #PATHLINE self
-- @param #string Name Name of the path.
-- @return #PATHLINE self
function PATHLINE:New(Name)

  -- Inherit everything from INTEL class.
  local self=BASE:Inherit(self, BASE:New()) --#PATHLINE
  
  self.name=Name or "Unknown Path"

  self.lid=string.format("PATHLINE %s | ", Name)

  return self
end

--- Create a new PATHLINE object from a given list of 2D points.
-- @param #PATHLINE self
-- @param #string Name Name of the pathline.
-- @param #table Vec2Array List of DCS#Vec2 points.
-- @return #PATHLINE self
function PATHLINE:NewFromVec2Array(Name, Vec2Array)

  local self=PATHLINE:New(Name)

  for i=1,#Vec2Array do
    self:AddPointFromVec2(Vec2Array[i])
  end

  return self
end

--- Create a new PATHLINE object from a given list of 3D points.
-- @param #PATHLINE self
-- @param #string Name Name of the pathline.
-- @param #table Vec3Array List of DCS#Vec3 points.
-- @return #PATHLINE self
function PATHLINE:NewFromVec3Array(Name, Vec3Array)

  local self=PATHLINE:New(Name)

  for i=1,#Vec3Array do
    self:AddPointFromVec3(Vec3Array[i])
  end

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Find a pathline in the database.
-- @param #PATHLINE self
-- @param #string Name The name of the pathline.
-- @return #PATHLINE self
function PATHLINE:FindByName(Name)
  local pathline = _DATABASE:FindPathline(Name)
  return pathline
end



--- Add a point to the path from a given 2D position. The third dimension is determined from the land height.
-- @param #PATHLINE self
-- @param DCS#Vec2 Vec2 The 2D vector (x,y) to add.
-- @return #PATHLINE self
function PATHLINE:AddPointFromVec2(Vec2)

  if Vec2 then
  
    local point=self:_CreatePoint(Vec2)

    table.insert(self.points, point)
    
  end
  
  return self
end

--- Add a point to the path from a given 3D position.
-- @param #PATHLINE self
-- @param DCS#Vec3 Vec3 The 3D vector (x,y) to add.
-- @return #PATHLINE self
function PATHLINE:AddPointFromVec3(Vec3)

  if Vec3 then
  
    local point=self:_CreatePoint(Vec3)

    table.insert(self.points, point)
    
  end
  
  return self
end

--- Get name of pathline.
-- @param #PATHLINE self
-- @return #string Name of the pathline.
function PATHLINE:GetName()  
  return self.name
end

--- Get number of points.
-- @param #PATHLINE self
-- @return #number Number of points.
function PATHLINE:GetNumberOfPoints()
  local N=#self.points
  return N
end

--- Get points of pathline. Not that points are tables, that contain more information as just the 2D or 3D position but also the surface type etc.
-- @param #PATHLINE self
-- @return #list <#PATHLINE.Point> List of points.
function PATHLINE:GetPoints()  
  return self.points
end

--- Get 3D points of pathline.
-- @param #PATHLINE self
-- @return <DCS#Vec3> List of DCS#Vec3 points.
function PATHLINE:GetPoints3D()

  local vecs={}
  
  for _,_point in pairs(self.points) do
    local point=_point --#PATHLINE.Point
    table.insert(vecs, point.vec3)
  end

  return vecs
end

--- Get 2D points of pathline.
-- @param #PATHLINE self
-- @return <DCS#Vec2> List of DCS#Vec2 points.
function PATHLINE:GetPoints2D()

  local vecs={}
  
  for _,_point in pairs(self.points) do
    local point=_point --#PATHLINE.Point
    table.insert(vecs, point.vec2)
  end

  return vecs
end

--- Get COORDINATES of pathline. Note that COORDINATE objects are created when calling this function. That does involve deep copy calls and can have an impact on performance if done too often.
-- @param #PATHLINE self
-- @return <Core.Point#COORDINATE> List of COORDINATES points.
function PATHLINE:GetCoordinates()

  local vecs={}
  
  for _,_point in pairs(self.points) do
    local point=_point --#PATHLINE.Point
    local coord=COORDINATE:NewFromVec3(point.vec3)
    table.insert(vecs,coord)
  end

  return vecs
end

--- Get the n-th point of the pathline.
-- @param #PATHLINE self
-- @param #number n The index of the point. Default is the first point.
-- @return #PATHLINE.Point Point.
function PATHLINE:GetPointFromIndex(n)

  local N=self:GetNumberOfPoints()
  
  n=n or 1

  local point=nil --#PATHLINE.Point
  
  if n>=1 and n<=N then
    point=self.points[n]
  else
    self:E(self.lid..string.format("ERROR: No point in pathline for N=%s", tostring(n)))
  end

  return point
end

--- Get the 3D position of the n-th point.
-- @param #PATHLINE self
-- @param #number n The n-th point.
-- @return DCS#VEC3 Position in 3D.
function PATHLINE:GetPoint3DFromIndex(n)

  local point=self:GetPointFromIndex(n)
  
  if point then
    return point.vec3
  end
  
  return nil
end

--- Get the 2D position of the n-th point.
-- @param #PATHLINE self
-- @param #number n The n-th point.
-- @return DCS#VEC2 Position in 3D.
function PATHLINE:GetPoint2DFromIndex(n)

  local point=self:GetPointFromIndex(n)
  
  if point then
    return point.vec2
  end
  
  return nil
end


--- Mark points on F10 map.
-- @param #PATHLINE self
-- @param #boolean Switch If `true` or nil, set marks. If `false`, remove marks.
-- @return <DCS#Vec3> List of DCS#Vec3 points.
function PATHLINE:MarkPoints(Switch)
  for i,_point in pairs(self.points) do
    local point=_point --#PATHLINE.Point
    if Switch==false then
      
      if point.markerID then
        UTILS.RemoveMark(point.markerID, Delay)
      end
      
    else
    
      if point.markerID then
        UTILS.RemoveMark(point.markerID)
      end
    
      point.markerID=UTILS.GetMarkID()
      
      local text=string.format("Pathline %s: Point #%d\nSurface Type=%d\nHeight=%.1f m\nDepth=%.1f m", self.name, i, point.surfaceType, point.landHeight, point.depth)
      
      trigger.action.markToAll(point.markerID, text, point.vec3, "")
    
    end
  end
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get 3D points of pathline.
-- @param #PATHLINE self
-- @param DCS#Vec3 Vec Position vector. Can also be a DCS#Vec2 in which case the altitude at landheight is taken.
-- @return #PATHLINE.Point
function PATHLINE:_CreatePoint(Vec)

  local point={} --#PATHLINE.Point

  if Vec.z then
    -- Given vec is 3D
    point.vec3=UTILS.DeepCopy(Vec)
    point.vec2={x=Vec.x, y=Vec.z}
  else
    -- Given vec is 2D  
    point.vec2=UTILS.DeepCopy(Vec)
    point.vec3={x=Vec.x, y=land.getHeight(Vec), z=Vec.y}
  end

  -- Get surface type.
  point.surfaceType=land.getSurfaceType(point.vec2)
  
  -- Get land height and depth.
  point.landHeight, point.depth=land.getSurfaceHeightWithSeabed(point.vec2)
  
  point.markerID=nil

  return point
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Core** - Client Menu Management.
--
-- **Main Features:**
--
--    * For complex, non-static menu structures
--    * Lightweigt implementation as alternative to MENU
--    * Separation of menu tree creation from menu on the clients's side
--    * Works with a SET_CLIENT set of clients
--    * Allow manipulation of the shadow tree in various ways
--    * Push to all or only one client
--    * Change entries' menu text
--    * Option to make an entry usable once only across all clients
--    * Auto appends GROUP and CLIENT objects to menu calls
--
-- ===
--
-- ### Author: **applevangelist**
-- 
-- ===
-- 
-- @module Core.ClientMenu
-- @image Core_Menu.JPG
-- last change: Sept 2025

-- TODO
----------------------------------------------------------------------------------------------------------------
--
-- CLIENTMENU
--
----------------------------------------------------------------------------------------------------------------

---
-- @type CLIENTMENU
-- @field #string ClassName Class Name
-- @field #string lid Lid for log entries
-- @field #string version Version string
-- @field #string name Name
-- @field #string groupname Group name
-- @field #table path
-- @field #table parentpath
-- @field #CLIENTMENU Parent
-- @field Wrapper.Client#CLIENT client
-- @field #number GroupID Group ID
-- @field #number ID Entry ID
-- @field Wrapper.Group#GROUP group
-- @field #string UUID Unique ID based on path+name
-- @field #string Function
-- @field #table Functionargs
-- @field #table Children
-- @field #boolean Once
-- @field #boolean Generic
-- @field #boolean debug
-- @field #CLIENTMENUMANAGER Controller
-- @field #active boolean
-- @extends Core.Base#BASE

---
-- @field #CLIENTMENU
CLIENTMENU = {
  ClassName = "CLIENTMENU",
  lid = "",
  version = "0.1.3",
  name = nil,
  path = nil,
  group = nil,
  client = nil,
  GroupID = nil,
  Children = {},
  Once = false,
  Generic = false,
  debug = false,
  Controller = nil,
  groupname = nil,
  active = false,
}

---
-- @field #CLIENTMENU_ID
CLIENTMENU_ID = 0

--- Create an new CLIENTMENU object.
-- @param #CLIENTMENU self
-- @param Wrapper.Client#CLIENT Client The client for whom this entry is. Leave as nil for a generic entry.
-- @param #string Text Text of the F10 menu entry.
-- @param #CLIENTMENU Parent The parent menu entry.
-- @param #string Function (optional) Function to call when the entry is used.
-- @param ... (optional) Arguments for the Function, comma separated
-- @return #CLIENTMENU self 
function CLIENTMENU:NewEntry(Client,Text,Parent,Function,...)
  -- Inherit everything from BASE class.
  local self=BASE:Inherit(self, BASE:New()) -- #CLIENTMENU
  CLIENTMENU_ID = CLIENTMENU_ID + 1
  self.ID = CLIENTMENU_ID
  if Client then
    self.group = Client:GetGroup()
    self.client = Client
    self.GroupID = self.group:GetID()
    self.groupname = self.group:GetName() or "Unknown Groupname"
  else
    self.Generic = true
  end
  self.name = Text or "unknown entry"
  if Parent then
    if Parent:IsInstanceOf("MENU_BASE") then
      self.parentpath = Parent.MenuPath
    else
      self.parentpath = Parent:GetPath()
      Parent:AddChild(self)
    end
  end
  self.Parent = Parent
  self.Function = Function
  self.Functionargs = arg or {}
  table.insert(self.Functionargs,self.group)
  table.insert(self.Functionargs,self.client)
  if self.Functionargs and self.debug then
    self:T({"Functionargs",self.Functionargs})
  end
  if not self.Generic and self.active == false then
    if Function ~= nil then
      local ErrorHandler = function( errmsg )
        env.info( "MOOSE Error in CLIENTMENU COMMAND function: " .. errmsg )
        if BASE.Debug ~= nil then
          env.info( BASE.Debug.traceback() )
        end
        return errmsg
      end
      self.CallHandler = function()
      local function MenuFunction() 
        return self.Function( unpack( self.Functionargs ) )
      end
        local Status, Result = xpcall( MenuFunction, ErrorHandler)
        if self.Once == true then
          self:Clear()
        end
      end
      self.path = missionCommands.addCommandForGroup(self.GroupID,Text,self.parentpath, self.CallHandler)
      self.active = true
    else
      self.path = missionCommands.addSubMenuForGroup(self.GroupID,Text,self.parentpath)
      self.active = true
    end
  else
    if self.parentpath then
      self.path = UTILS.DeepCopy(self.parentpath)
    else
      self.path = {}
    end
    self.path[#self.path+1] = Text
  end
  self.UUID = table.concat(self.path,";")
  self:T({self.UUID})
  self.Once = false
  -- Log id.
  self.lid=string.format("CLIENTMENU %s | %s | ", self.ID, self.name)
  self:T(self.lid.."Created")
  return self
end

--- Create a UUID
-- @param #CLIENTMENU self
-- @param #CLIENTMENU Parent The parent object if any
-- @param #string Text The menu entry text
-- @return #string UUID
function CLIENTMENU:CreateUUID(Parent,Text)
  local path = {}
  if Parent and Parent.path then
    path = Parent.path
  end
  path[#path+1] = Text
  local UUID = table.concat(path,";")
  return UUID
end

--- Set the CLIENTMENUMANAGER for this entry.
-- @param #CLIENTMENU self
-- @param #CLIENTMENUMANAGER Controller The controlling object.
-- @return #CLIENTMENU self 
function CLIENTMENU:SetController(Controller)
  self.Controller = Controller
  return self
end

--- The entry will be deleted after being used used - for menu entries with functions only.
-- @param #CLIENTMENU self
-- @return #CLIENTMENU self 
function CLIENTMENU:SetOnce()
  self:T(self.lid.."SetOnce")
  self.Once = true
  return self
end

--- Remove the entry from the F10 menu.
-- @param #CLIENTMENU self
-- @return #CLIENTMENU self 
function CLIENTMENU:RemoveF10()
  self:T(self.lid.."RemoveF10")
  if self.GroupID then
    --self:I(self.lid.."Removing "..table.concat(self.path,";"))
    local function RemoveFunction()
      return missionCommands.removeItemForGroup(self.GroupID , self.path )
    end
    local status, err = pcall(RemoveFunction)
    if not status then
      self:I(string.format("**** Error Removing Menu Entry %s for %s!",tostring(self.name),self.groupname))
    end
    self.active = false
  end
  return self
end

--- Get the menu path table.
-- @param #CLIENTMENU self
-- @return #table Path
function CLIENTMENU:GetPath()
  self:T(self.lid.."GetPath")
  return self.path 
end

--- Get the UUID.
-- @param #CLIENTMENU self
-- @return #string UUID
function CLIENTMENU:GetUUID()
  self:T(self.lid.."GetUUID")
  return self.UUID
end

--- Link a child entry.
-- @param #CLIENTMENU self
-- @param #CLIENTMENU Child The entry to link as a child.
-- @return #CLIENTMENU self
function CLIENTMENU:AddChild(Child)
  self:T(self.lid.."AddChild "..Child.ID)
  table.insert(self.Children,Child.ID,Child)
  return self
end

--- Remove a child entry.
-- @param #CLIENTMENU self
-- @param #CLIENTMENU Child The entry to remove from the children.
-- @return #CLIENTMENU self
function CLIENTMENU:RemoveChild(Child)
  self:T(self.lid.."RemoveChild "..Child.ID)
  table.remove(self.Children,Child.ID)
  return self
end

--- Remove all subentries (children) from this entry.
-- @param #CLIENTMENU self
-- @return #CLIENTMENU self
function CLIENTMENU:RemoveSubEntries()
  self:T(self.lid.."RemoveSubEntries")
  self:T({self.Children})
  for _id,_entry in pairs(self.Children) do
    self:T("Removing ".._id)
    if _entry then
      _entry:RemoveSubEntries()
      _entry:RemoveF10()
      if _entry.Parent then
        _entry.Parent:RemoveChild(self)
      end
      --if self.Controller then
        --self.Controller:_RemoveByID(_entry.ID)
      --end
      --_entry = nil
    end
  end
  return self
end

--- Remove this entry and all subentries (children) from this entry.
-- @param #CLIENTMENU self
-- @return #CLIENTMENU self
function CLIENTMENU:Clear()
  self:T(self.lid.."Clear")
  for _id,_entry in pairs(self.Children) do
    if _entry then
      _entry:RemoveSubEntries()
      _entry = nil
    end
  end
  self:RemoveF10()
  if self.Parent then
    self.Parent:RemoveChild(self)
  end
  --if self.Controller then
    --self.Controller:_RemoveByID(self.ID)
  --end
  return self
end

-- TODO
----------------------------------------------------------------------------------------------------------------
--
-- CLIENTMENUMANAGER
--
----------------------------------------------------------------------------------------------------------------


--- Class CLIENTMENUMANAGER              
-- @type CLIENTMENUMANAGER
-- @field #string ClassName Class Name
-- @field #string lid Lid for log entries
-- @field #string version Version string
-- @field #string name Name
-- @field Core.Set#SET_CLIENT clientset The set of clients this menu manager is for
-- @field #table flattree
-- @field #table rootentries
-- @field #table menutree
-- @field #number entrycount
-- @field #boolean debug
-- @field #table PlayerMenu
-- @field #number Coalition
-- @extends Core.Base#BASE

--- *As a child my family's menu consisted of two choices: take it, or leave it.*
--
-- ===
--
-- ## CLIENTMENU and CLIENTMENUMANAGER
-- 
-- Manage menu structures for a SET_CLIENT of clients.
-- 
-- ## Concept
-- 
-- Separate creation of a menu tree structure from pushing it to each client. Create a shadow "reference" menu structure tree for your client pilot's in a mission. 
-- This can then be propagated to all clients. Manipulate the entries in the structure with removing, clearing or changing single entries, create replacement sub-structures 
-- for entries etc, push to one or all clients.
-- 
-- Many functions can either change the tree for one client or for all clients.
-- 
-- ## Conceptual remarks
-- 
-- There's a couple of things to fully understand: 
-- 
-- 1) **CLIENTMENUMANAGER** manages a set of entries from **CLIENTMENU**, it's main purpose is to administer the *shadow menu tree*, ie. a menu structure which is not 
-- (yet) visible to any client   
-- 2) The entries are **CLIENTMENU** objects, which are linked in a tree form. There's two ways to create them:   
--          A) in the manager with ":NewEntry()" which initially 
--              adds it to the shadow menu **only**   
--          B) stand-alone directly as `CLIENTMENU:NewEntry()` - here it depends on whether or not you gave a CLIENT object if the entry is created as generic entry or pushed 
--              a **specific** client. **Be aware** though that the entries are not managed by the CLIENTMANAGER before the next step!   
-- A generic entry can be added to the manager (and the shadow tree) with `:AddEntry()` - this will also push it to all clients(!) if no client is given, or a specific client only.  
-- 3) Pushing only works for alive clients.   
-- 4) Live and shadow tree entries are managed via the CLIENTMENUMANAGER object.   
-- 5) `Propagate()`refreshes the menu tree for all, or a single client.   
-- 
-- ## Create a base reference tree and send to all clients
-- 
--            local clientset = SET_CLIENT:New():FilterStart()
--            
--            local menumgr = CLIENTMENUMANAGER:New(clientset,"Dayshift")
--            local mymenu = menumgr:NewEntry("Top")
--            local mymenu_lv1a = menumgr:NewEntry("Level 1 a",mymenu)
--            local mymenu_lv1b = menumgr:NewEntry("Level 1 b",mymenu)
--            -- next one is a command menu entry, which can only be used once
--            local mymenu_lv1c = menumgr:NewEntry("Action Level 1 c",mymenu, testfunction, "testtext"):SetOnce()
--            
--            local mymenu_lv2a = menumgr:NewEntry("Go here",mymenu_lv1a)
--            local mymenu_lv2b = menumgr:NewEntry("Level 2 ab",mymenu_lv1a)
--            local mymenu_lv2c = menumgr:NewEntry("Level 2 ac",mymenu_lv1a)
--            
--            local mymenu_lv2ba = menumgr:NewEntry("Level 2 ba",mymenu_lv1b)
--            local mymenu_lv2bb = menumgr:NewEntry("Level 2 bb",mymenu_lv1b)
--            local mymenu_lv2bc = menumgr:NewEntry("Level 2 bc",mymenu_lv1b)
--            
--            local mymenu_lv3a = menumgr:NewEntry("Level 3 aaa",mymenu_lv2a)
--            local mymenu_lv3b = menumgr:NewEntry("Level 3 aab",mymenu_lv2a)
--            local mymenu_lv3c = menumgr:NewEntry("Level 3 aac",mymenu_lv2a)
--            
--            menumgr:Propagate() -- propagate **once** to all clients in the SET_CLIENT
--            
-- ## Remove a single entry's subtree
-- 
--            menumgr:RemoveSubEntries(mymenu_lv3a)
--            
 -- ## Remove a single entry and also it's subtree
-- 
--            menumgr:DeleteEntry(mymenu_lv3a)
-- 
-- ## Add a single entry
-- 
--            local baimenu = menumgr:NewEntry("BAI",mymenu_lv1b)
--            
--            menumgr:AddEntry(baimenu)  
--            
-- ## Add an entry with a function 
-- 
--            local baimenu = menumgr:NewEntry("Task Action", mymenu_lv1b, TestFunction, Argument1, Argument1)
--            
--  Now, the class will **automatically append the call with GROUP and CLIENT objects**, as this is can only be done when pushing the entry to the clients. So, the actual function implementation needs to look like this:
--  
--            function TestFunction( Argument1, Argument2, Group, Client)
--            
--  **Caveat is**, that you need to ensure your arguments are not **nil** or **false**, as LUA will optimize those away. You would end up having Group and Client in wrong places in the function call. Hence,
--  if you need/ want to send **nil** or **false**, send a place holder instead and ensure your function can handle this, e.g.
--  
--            local baimenu = menumgr:NewEntry("Task Action", mymenu_lv1b, TestFunction, "nil", Argument1)
--     
-- ## Change the text of a leaf entry in the menu tree          
--            
--            menumgr:ChangeEntryTextForAll(mymenu_lv1b,"Attack")
--            
-- ## Reset a single clients menu tree
-- 
--            menumgr:ResetMenu(client)
--            
-- ## Reset all and clear the reference tree
-- 
--            menumgr:ResetMenuComplete()
--            
-- ## Set to auto-propagate for CLIENTs joining the SET_CLIENT **after** the script is loaded - handy if you have a single menu tree.
-- 
--            menumgr:InitAutoPropagation()
--
-- @field #CLIENTMENUMANAGER
CLIENTMENUMANAGER = {
  ClassName = "CLIENTMENUMANAGER",
  lid = "",
  version = "0.1.7",
  name = nil,
  clientset = nil,
  menutree = {},
  flattree = {},
  playertree = {},
  entrycount = 0,
  rootentries = {},
  debug = true,
  PlayerMenu = {},
  Coalition = nil,
}

--- Create a new ClientManager instance.
-- @param #CLIENTMENUMANAGER self
-- @param Core.Set#SET_CLIENT ClientSet The set of clients to manage.
-- @param #string Alias The name of this manager.
-- @param #number Coalition (Optional) Coalition of this Manager, defaults to coalition.side.BLUE
-- @return #CLIENTMENUMANAGER self
function CLIENTMENUMANAGER:New(ClientSet, Alias, Coalition)
  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, BASE:New()) -- #CLIENTMENUMANAGER
  self.clientset = ClientSet
  self.PlayerMenu = {}
  self.name = Alias or "Nightshift"
  self.Coalition = Coalition or coalition.side.BLUE
    -- Log id.
  self.lid=string.format("CLIENTMENUMANAGER %s | %s | ", self.version, self.name)
  if self.debug then
    self:I(self.lid.."Created")
  end
  return self
end

--- [Internal] Event handling
-- @param #CLIENTMENUMANAGER self
-- @param Core.Event#EVENTDATA EventData
-- @return #CLIENTMENUMANAGER self
function CLIENTMENUMANAGER:_EventHandler(EventData,Retry)
  self:T(self.lid.."_EventHandler: "..EventData.id)
  --self:I(self.lid.."_EventHandler: "..tostring(EventData.IniPlayerName))
  if EventData.id == EVENTS.PlayerLeaveUnit or EventData.id == EVENTS.Ejection or EventData.id == EVENTS.Crash or EventData.id == EVENTS.PilotDead then
    self:T(self.lid.."Leave event for player: "..tostring(EventData.IniPlayerName)) 
    local Client = _DATABASE:FindClient( EventData.IniUnitName )
    if Client then
      self:ResetMenu(Client)
    end
  elseif (EventData.id == EVENTS.PlayerEnterAircraft) and EventData.IniCoalition == self.Coalition then
    if EventData.IniPlayerName and EventData.IniGroup then
      if (not self.clientset:IsIncludeObject(_DATABASE:FindClient( EventData.IniUnitName ))) then
        self:T(self.lid.."Client not in SET: "..EventData.IniPlayerName)
        if not Retry then
          -- try again in 2 secs
          self:ScheduleOnce(2,CLIENTMENUMANAGER._EventHandler,self,EventData,true)
        end
        return self
      end
      --self:I(self.lid.."Join event for player: "..EventData.IniPlayerName)
      local player = _DATABASE:FindClient( EventData.IniUnitName )
      self:Propagate(player)
    end
  elseif EventData.id == EVENTS.PlayerEnterUnit then
    -- special for CA slots
    local grp = GROUP:FindByName(EventData.IniGroupName)
    if grp:IsGround() then
      self:T(string.format("Player %s entered GROUND unit %s!",EventData.IniPlayerName,EventData.IniUnitName))
      local IsPlayer = EventData.IniDCSUnit:getPlayerName()
      if IsPlayer then
        
        local client=_DATABASE.CLIENTS[EventData.IniDCSUnitName] --Wrapper.Client#CLIENT
        
        -- Add client in case it does not exist already.
        if not client then
          
          -- Debug info.
          self:I(string.format("Player '%s' joined ground unit '%s' of group '%s'", tostring(EventData.IniPlayerName), tostring(EventData.IniDCSUnitName), tostring(EventData.IniDCSGroupName)))
        
          client=_DATABASE:AddClient(EventData.IniDCSUnitName)
            
          -- Add player.
          client:AddPlayer(EventData.IniPlayerName)
          
          -- Add player.
          if not _DATABASE.PLAYERS[EventData.IniPlayerName] then
            _DATABASE:AddPlayer( EventData.IniUnitName, EventData.IniPlayerName )
          end
          
          -- Player settings.
          local Settings = SETTINGS:Set( EventData.IniPlayerName )
          Settings:SetPlayerMenu(EventData.IniUnit)
        end
        --local player = _DATABASE:FindClient( EventData.IniPlayerName )
        self:Propagate(client)
      end
    end
  end
  
  return self
end

--- Set this Client Manager to auto-propagate menus **once** to newly joined players. Useful if you have **one** menu structure only. Does not automatically push follow-up changes to the client(s).
-- @param #CLIENTMENUMANAGER self
-- @return #CLIENTMENUMANAGER self
function CLIENTMENUMANAGER:InitAutoPropagation()
  -- Player Events
  self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler)
  self:HandleEvent(EVENTS.Ejection, self._EventHandler)
  self:HandleEvent(EVENTS.Crash, self._EventHandler)
  self:HandleEvent(EVENTS.PilotDead, self._EventHandler)
  self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler)
  self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler)
  self:SetEventPriority(6) 
  return self 
end

--- Create a new entry in the **generic** structure.
-- @param #CLIENTMENUMANAGER self
-- @param #string Text Text of the F10 menu entry.
-- @param #CLIENTMENU Parent The parent menu entry.
-- @param #string Function (optional) Function to call when the entry is used.
-- @param ... (optional) Arguments for the Function, comma separated.
-- @return #CLIENTMENU Entry
function CLIENTMENUMANAGER:NewEntry(Text,Parent,Function,...)
  self:T(self.lid.."NewEntry "..Text or "None")
  self.entrycount = self.entrycount + 1
  local entry = CLIENTMENU:NewEntry(nil,Text,Parent,Function,unpack(arg))
  if not Parent then
    self.rootentries[self.entrycount] = entry
  end
  local depth = #entry.path
  if not self.menutree[depth] then self.menutree[depth] = {} end
  table.insert(self.menutree[depth],entry.UUID)
  self.flattree[entry.UUID] = entry
  return entry
end

--- Check matching entry in the generic structure by UUID.
-- @param #CLIENTMENUMANAGER self
-- @param #string UUID UUID of the menu entry.
-- @return #boolean Exists
function CLIENTMENUMANAGER:EntryUUIDExists(UUID)
  local exists = self.flattree[UUID] and true or false
  return exists
end

--- Find matching entry in the generic structure by UUID.
-- @param #CLIENTMENUMANAGER self
-- @param #string UUID UUID of the menu entry.
-- @return #CLIENTMENU Entry The #CLIENTMENU object found or nil.
function CLIENTMENUMANAGER:FindEntryByUUID(UUID)
  self:T(self.lid.."FindEntryByUUID "..UUID or "None")
  local entry = nil
  for _gid,_entry in pairs(self.flattree) do
    local Entry = _entry -- #CLIENTMENU
    if Entry and Entry.UUID == UUID then
      entry = Entry
    end
  end
  return entry
end

--- Find matching entries by text in the generic structure by UUID.
-- @param #CLIENTMENUMANAGER self
-- @param #string Text Text or partial text of the menu entry to find.
-- @param #CLIENTMENU Parent (Optional) Only find entries under this parent entry.
-- @return #table Table of matching UUIDs of #CLIENTMENU objects
-- @return #table Table of matching #CLIENTMENU objects
-- @return #number Number of matches
function CLIENTMENUMANAGER:FindUUIDsByText(Text,Parent)
  self:T(self.lid.."FindUUIDsByText "..Text or "None")
  local matches = {}
  local entries = {}
  local n = 0
  for _uuid,_entry in pairs(self.flattree) do
    local Entry = _entry -- #CLIENTMENU
    if Parent then
      if Entry and string.find(Entry.name,Text,1,true) and string.find(Entry.UUID,Parent.UUID,1,true) then
        table.insert(matches,_uuid)
        table.insert(entries,Entry )
        n=n+1
      end
    else
      if Entry and string.find(Entry.name,Text,1,true) then
        table.insert(matches,_uuid)
        table.insert(entries,Entry )
        n=n+1
      end
    end
  end
  return matches, entries, n
end

--- Find matching entries in the generic structure by the menu text.
-- @param #CLIENTMENUMANAGER self
-- @param #string Text Text or partial text of the F10 menu entry.
-- @param #CLIENTMENU Parent (Optional) Only find entries under this parent entry.
-- @return #table Table of matching #CLIENTMENU objects.
-- @return #number Number of matches
function CLIENTMENUMANAGER:FindEntriesByText(Text,Parent)
  self:T(self.lid.."FindEntriesByText "..Text or "None")
  local matches, objects, number = self:FindUUIDsByText(Text, Parent)
  return objects, number
end

--- Find matching entries under a parent in the generic structure by UUID.
-- @param #CLIENTMENUMANAGER self
-- @param #CLIENTMENU Parent Find entries under this parent entry.
-- @return #table Table of matching UUIDs of #CLIENTMENU objects
-- @return #table Table of matching #CLIENTMENU objects
-- @return #number Number of matches
function CLIENTMENUMANAGER:FindUUIDsByParent(Parent)
  self:T(self.lid.."FindUUIDsByParent")
  local matches = {}
  local entries = {}
  local n = 0
  for _uuid,_entry in pairs(self.flattree) do
    local Entry = _entry -- #CLIENTMENU
    if Parent then
      if Entry and string.find(Entry.UUID,Parent.UUID,1,true) then
        table.insert(matches,_uuid)
        table.insert(entries,Entry )
        n=n+1
      end
    end
  end
  return matches, entries, n
end

--- Find matching entries in the generic structure under a parent.
-- @param #CLIENTMENUMANAGER self
-- @param #CLIENTMENU Parent Find entries under this parent entry.
-- @return #table Table of matching #CLIENTMENU objects.
-- @return #number Number of matches
function CLIENTMENUMANAGER:FindEntriesByParent(Parent)
  self:T(self.lid.."FindEntriesByParent")
  local matches, objects, number = self:FindUUIDsByParent(Parent)
  return objects, number
end

--- Alter the text of a leaf entry in the generic structure and push to one specific client's F10 menu.
-- @param #CLIENTMENUMANAGER self
-- @param #CLIENTMENU Entry The menu entry.
-- @param #string Text New Text of the F10 menu entry.
-- @param Wrapper.Client#CLIENT Client (optional) The client for whom to alter the entry, if nil done for all clients.
-- @return #CLIENTMENUMANAGER self
function CLIENTMENUMANAGER:ChangeEntryText(Entry, Text, Client)
  self:T(self.lid.."ChangeEntryText "..Text or "None")
  local newentry = CLIENTMENU:NewEntry(nil,Text,Entry.Parent,Entry.Function,unpack(Entry.Functionargs))
  self:DeleteF10Entry(Entry,Client)
  self:DeleteGenericEntry(Entry)
  if not Entry.Parent then
    self.rootentries[self.entrycount] = newentry
  end
  local depth = #newentry.path
  if not self.menutree[depth] then self.menutree[depth] = {} end
  table.insert(self.menutree[depth],newentry.UUID)
  self.flattree[newentry.UUID] = newentry
  self:AddEntry(newentry,Client)
  return self
end

--- Push the complete menu structure to each of the clients in the set - refresh the menu tree of the clients.
-- @param #CLIENTMENUMANAGER self
-- @param Wrapper.Client#CLIENT Client (optional) If given, propagate only for this client.
-- @return #CLIENTMENU Entry
function CLIENTMENUMANAGER:Propagate(Client)
  self:T(self.lid.."Propagate")
  --self:I(UTILS.PrintTableToLog(Client,1))
  local knownunits = {} -- track so we can ID multi seated
  local Set = self.clientset.Set
  if Client then
    Set = {Client}
  end
  self:ResetMenu(Client)
  for _,_client in pairs(Set) do
    local client = _client -- Wrapper.Client#CLIENT
    if client and client:IsAlive() then
      local playerunit = client:GetName()
      --local playergroup = client:GetGroup()
      local playername = client:GetPlayerName() or "none"
      if not knownunits[playerunit] then
        knownunits[playerunit] = true
      else
        self:I("Player in multi seat unit: "..playername)
        break -- multi seat already build
      end
      if not self.playertree[playername] then
        self.playertree[playername] = {}
      end
      for level,branch in pairs (self.menutree) do
        self:T("Building branch:" .. level)
        for _,leaf in pairs(branch) do
          self:T("Building leaf:" .. leaf)
          local entry = self:FindEntryByUUID(leaf)
          if entry then
            self:T("Found generic entry:" .. entry.UUID)
            local parent = nil
            if entry.Parent and entry.Parent.UUID then
              parent = self.playertree[playername][entry.Parent.UUID] or self:FindEntryByUUID(entry.Parent.UUID)
            end
            self.playertree[playername][entry.UUID] = CLIENTMENU:NewEntry(client,entry.name,parent,entry.Function,unpack(entry.Functionargs))
            self.playertree[playername][entry.UUID].Once = entry.Once
          else
            self:T("NO generic entry for:" .. leaf)
          end  
        end
      end
    end
  end
  return self
end

--- Push a single previously created entry into the F10 menu structure of all clients.
-- @param #CLIENTMENUMANAGER self
-- @param #CLIENTMENU Entry The entry to add.
-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. 
-- @return #CLIENTMENUMANAGER self
function CLIENTMENUMANAGER:AddEntry(Entry,Client)
  self:T(self.lid.."AddEntry")
  local Set = self.clientset.Set
  local knownunits = {}
  if Client then
    Set = {Client}
  end
  for _,_client in pairs(Set) do
    local client = _client -- Wrapper.Client#CLIENT
    if client and client:IsAlive() then
      local playername = client:GetPlayerName() or "None"
      local unitname = client:GetName()
      if not knownunits[unitname] then
        knownunits[unitname] = true
      else
        self:I("Player in multi seat unit: "..playername)  
        break
      end
      if Entry then
        self:T("Adding generic entry:" .. Entry.UUID)
        local parent = nil
        if not self.playertree[playername] then
          self.playertree[playername] = {}
        end
        if Entry.Parent and Entry.Parent.UUID then
          parent = self.playertree[playername][Entry.Parent.UUID] or self:FindEntryByUUID(Entry.Parent.UUID)
        end
        self.playertree[playername][Entry.UUID] = CLIENTMENU:NewEntry(client,Entry.name,parent,Entry.Function,unpack(Entry.Functionargs))
        self.playertree[playername][Entry.UUID].Once = Entry.Once
      else
        self:T("NO generic entry given")
      end 
    end
  end
  return self
end

--- Blank out the menu - remove **all root entries** and all entries below from the client's F10 menus, leaving the generic structure untouched.
-- @param #CLIENTMENUMANAGER self
-- @param Wrapper.Client#CLIENT Client (optional) If given, remove only for this client.
-- @return #CLIENTMENUMANAGER self
function CLIENTMENUMANAGER:ResetMenu(Client)
  self:T(self.lid.."ResetMenu")
  for _,_entry in pairs(self.rootentries) do
    --local RootEntry = self.structure.generic[_entry]
    if _entry then
      self:DeleteF10Entry(_entry,Client)
    end
  end
  return self
end

--- Blank out the menu - remove **all root entries** and all entries below from all clients' F10 menus, and **delete** the generic structure.
-- @param #CLIENTMENUMANAGER self
-- @return #CLIENTMENUMANAGER self
function CLIENTMENUMANAGER:ResetMenuComplete()
  self:T(self.lid.."ResetMenuComplete")
  for _,_entry in pairs(self.rootentries) do
    --local RootEntry = self.structure.generic[_entry]
    if _entry then
      self:DeleteF10Entry(_entry)
    end
  end
  self.playertree = nil
  self.playertree = {}
  self.rootentries = nil
  self.rootentries = {}
  self.menutree = nil
  self.menutree = {}
  return self
end

--- Remove the entry and all entries below the given entry from the client's F10 menus.
-- @param #CLIENTMENUMANAGER self
-- @param #CLIENTMENU Entry The entry to remove
-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. 
-- @return #CLIENTMENUMANAGER self
function CLIENTMENUMANAGER:DeleteEntry(Entry,Client)
  self:T(self.lid.."DeleteEntry")
  return self:DeleteF10Entry(Entry,Client)
end

--- Remove the entry and all entries below the given entry from the client's F10 menus.
-- @param #CLIENTMENUMANAGER self
-- @param #CLIENTMENU Entry The entry to remove
-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. 
-- @return #CLIENTMENUMANAGER self
function CLIENTMENUMANAGER:DeleteF10Entry(Entry,Client)
  self:T(self.lid.."DeleteF10Entry")
  local Set = self.clientset.Set
  if Client then
    Set = {Client}
  end
  for _,_client in pairs(Set) do
    if _client and _client:IsAlive() then
      local playername = _client:GetPlayerName()
      if self.playertree[playername] then
        local centry = self.playertree[playername][Entry.UUID] -- #CLIENTMENU
        if centry then
          --self:I("Match for "..Entry.UUID)
          centry:Clear()
        end
      end
    end
  end
  return self
end

--- Remove the entry and all entries below the given entry from the generic tree.
-- @param #CLIENTMENUMANAGER self
-- @param #CLIENTMENU Entry The entry to remove
-- @return #CLIENTMENUMANAGER self
function CLIENTMENUMANAGER:DeleteGenericEntry(Entry)
  self:T(self.lid.."DeleteGenericEntry")
  
  if Entry.Children and #Entry.Children > 0 then
    self:RemoveGenericSubEntries(Entry)
  end
  
  local depth = #Entry.path
  local uuid = Entry.UUID
  
  local tbl = UTILS.DeepCopy(self.menutree)
  
  if tbl[depth] then
    for i=depth,#tbl do
      --self:I("Level = "..i)
      for _id,_uuid in pairs(tbl[i]) do
        self:T(_uuid)
        if string.find(_uuid,uuid,1,true) or _uuid == uuid then
          --self:I("Match for ".._uuid)
          self.menutree[i][_id] = nil
          self.flattree[_uuid] = nil
        end  
      end 
    end
  end
  
  return self
end

--- Remove all entries below the given entry from the generic tree.
-- @param #CLIENTMENUMANAGER self
-- @param #CLIENTMENU Entry The entry where to start. This entry stays.
-- @return #CLIENTMENUMANAGER self
function CLIENTMENUMANAGER:RemoveGenericSubEntries(Entry)
  self:T(self.lid.."RemoveGenericSubEntries")
  
  local depth = #Entry.path + 1
  local uuid = Entry.UUID
  
  local tbl = UTILS.DeepCopy(self.menutree)
  
  if tbl[depth] then
    for i=depth,#tbl do
      self:T("Level = "..i)
      for _id,_uuid in pairs(tbl[i]) do
        self:T(_uuid)
        if string.find(_uuid,uuid,1,true) then
          self:T("Match for ".._uuid)
          self.menutree[i][_id] = nil
          self.flattree[_uuid] = nil
        end  
      end 
    end
  end  
  return self
end


--- Remove all entries below the given entry from the client's F10 menus.
-- @param #CLIENTMENUMANAGER self
-- @param #CLIENTMENU Entry The entry where to start. This entry stays.
-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. In this case the generic structure will not be touched.
-- @return #CLIENTMENUMANAGER self
function CLIENTMENUMANAGER:RemoveF10SubEntries(Entry,Client)
  self:T(self.lid.."RemoveSubEntries")
  local Set = self.clientset.Set
  if Client then
    Set = {Client}
  end
  for _,_client in pairs(Set) do
    if _client and _client:IsAlive() then
      local playername = _client:GetPlayerName()
      if self.playertree[playername] then
        local centry = self.playertree[playername][Entry.UUID] -- #CLIENTMENU
        centry:RemoveSubEntries()
      end
    end
  end
  return self
end

----------------------------------------------------------------------------------------------------------------
--
-- End ClientMenu
--
----------------------------------------------------------------------------------------------------------------
--- **Wrapper** - OBJECT wraps the DCS Object derived objects.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: **funkyfranky
-- 
-- ===
-- 
-- @module Wrapper.Object
-- @image MOOSE.JPG


--- OBJECT class.
-- @type OBJECT
-- @extends Core.Base#BASE
-- @field #string ObjectName The name of the Object.


--- Wrapper class to handle the DCS Object objects.
--
--  * Support all DCS Object APIs.
--  * Enhance with Object specific APIs not in the DCS Object API set.
--  * Manage the "state" of the DCS Object.
--
-- ## OBJECT constructor:
-- 
-- The OBJECT class provides the following functions to construct a OBJECT instance:
--
--  * @{Wrapper.Object#OBJECT.New}(): Create a OBJECT instance.
--
-- @field #OBJECT
OBJECT = {
  ClassName = "OBJECT",
  ObjectName = "",
}

--- A DCSObject
-- @type DCSObject
-- @field id_ The ID of the controllable in DCS

--- Create a new OBJECT from a DCSObject
-- @param #OBJECT self
-- @param DCS#Object ObjectName The Object name
-- @return #OBJECT self
function OBJECT:New( ObjectName)

  -- Inherit BASE class.
  local self = BASE:Inherit( self, BASE:New() )
  
  -- Debug output.
  self:F2( ObjectName )
  
  -- Set object name.
  self.ObjectName = ObjectName

  return self
end


--- Returns the unit's unique identifier.
-- @param Wrapper.Object#OBJECT self
-- @return DCS#Object.ID ObjectID or #nil if the DCS Object is not existing or alive. Note that the ID is passed as a string and not a number. 
function OBJECT:GetID()

  local DCSObject = self:GetDCSObject()
  
  if DCSObject then
    local ObjectID = DCSObject:getID()
    return ObjectID
  end 

  self:E( { "Cannot GetID", Name = self.ObjectName, Class = self:GetClassName() } )

  return nil
end

--- Destroys the OBJECT.
-- @param #OBJECT self
-- @return #boolean Returns `true` if the object is destroyed or #nil if the object is nil.
function OBJECT:Destroy()

  local DCSObject = self:GetDCSObject()
  
  if DCSObject then
    --BASE:CreateEventCrash( timer.getTime(), DCSObject )
    DCSObject:destroy( false )
    return true
  end

  self:E( { "Cannot Destroy", Name = self.ObjectName, Class = self:GetClassName() } )

  return nil
end






--- **Wrapper** - IDENTIFIABLE is an intermediate class wrapping DCS Object class derived Objects.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Wrapper.Identifiable
-- @image MOOSE.JPG

---
-- @type IDENTIFIABLE
-- @extends Wrapper.Object#OBJECT
-- @field #string IdentifiableName The name of the identifiable.

--- Wrapper class to handle the DCS Identifiable objects.
--
--  * Support all DCS Identifiable APIs.
--  * Enhance with Identifiable specific APIs not in the DCS Identifiable API set.
--  * Manage the "state" of the DCS Identifiable.
--
-- ## IDENTIFIABLE constructor
-- 
-- The IDENTIFIABLE class provides the following functions to construct a IDENTIFIABLE instance:
--
--  * @{#IDENTIFIABLE.New}(): Create a IDENTIFIABLE instance.
--
-- @field #IDENTIFIABLE
IDENTIFIABLE = {
  ClassName = "IDENTIFIABLE",
  IdentifiableName = "",
}

local _CategoryName = { 
  [Unit.Category.AIRPLANE]      = "Airplane",
  [Unit.Category.HELICOPTER]    = "Helicopter",
  [Unit.Category.GROUND_UNIT]   = "Ground Identifiable",
  [Unit.Category.SHIP]          = "Ship",
  [Unit.Category.STRUCTURE]     = "Structure",
  }

--- Create a new IDENTIFIABLE from a DCSIdentifiable
-- @param #IDENTIFIABLE self
-- @param #string IdentifiableName The DCS Identifiable name
-- @return #IDENTIFIABLE self
function IDENTIFIABLE:New( IdentifiableName )
  local self = BASE:Inherit( self, OBJECT:New( IdentifiableName ) )
  self:F2( IdentifiableName )
  self.IdentifiableName = IdentifiableName
  return self
end

--- Returns if the Identifiable is alive.  
-- If the Identifiable is not alive, nil is returned.  
-- If the Identifiable is alive, true is returned.  
-- @param #IDENTIFIABLE self
-- @return #boolean true if Identifiable is alive or `#nil` if the Identifiable is not existing or is not alive.  
function IDENTIFIABLE:IsAlive()
  self:F3( self.IdentifiableName )

  local DCSIdentifiable = self:GetDCSObject() -- DCS#Object
  
  if DCSIdentifiable then
    local IdentifiableIsAlive  = DCSIdentifiable:isExist()
    return IdentifiableIsAlive
  end 
  
  return false
end




--- Returns DCS Identifiable object name. 
-- The function provides access to non-activated objects too.
-- @param #IDENTIFIABLE self
-- @return #string The name of the DCS Identifiable or `#nil`.  
function IDENTIFIABLE:GetName()
  local IdentifiableName = self.IdentifiableName
  return IdentifiableName
end


--- Returns the type name of the DCS Identifiable.
-- @param #IDENTIFIABLE self
-- @return #string The type name of the DCS Identifiable.
function IDENTIFIABLE:GetTypeName()
  self:F2( self.IdentifiableName )
  
  local DCSIdentifiable = self:GetDCSObject()
  
  if DCSIdentifiable then
    local IdentifiableTypeName = DCSIdentifiable:getTypeName()
    self:T3( IdentifiableTypeName )
    return IdentifiableTypeName
  end

  self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" )
  return nil
end


--- Returns object category of the DCS Identifiable. One of
-- 
--  * Object.Category.UNIT = 1
--  * Object.Category.WEAPON = 2
--  * Object.Category.STATIC = 3
--  * Object.Category.BASE = 4
--  * Object.Category.SCENERY = 5
--  * Object.Category.Cargo = 6
--  
--  For UNITs this returns a second value, one of
--  
--  Unit.Category.AIRPLANE      = 0  
--  Unit.Category.HELICOPTER    = 1  
--  Unit.Category.GROUND_UNIT   = 2  
--  Unit.Category.SHIP          = 3  
--  Unit.Category.STRUCTURE     = 4  
--  
-- @param #IDENTIFIABLE self
-- @return DCS#Object.Category The category ID, i.e. a number.
-- @return DCS#Unit.Category The unit category ID, i.e. a number. For units only.
function IDENTIFIABLE:GetCategory()
  self:F2( self.ObjectName )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    local ObjectCategory, UnitCategory = DCSObject:getCategory()
    self:T3( ObjectCategory )
    return ObjectCategory, UnitCategory
  end

  return nil,nil
end


--- Returns the DCS Identifiable category name as defined within the DCS Identifiable Descriptor.
-- @param #IDENTIFIABLE self
-- @return #string The DCS Identifiable Category Name
function IDENTIFIABLE:GetCategoryName()
  local DCSIdentifiable = self:GetDCSObject()
  
  if DCSIdentifiable then
    local IdentifiableCategoryName = _CategoryName[ self:GetDesc().category ]
    return IdentifiableCategoryName
  end
  
  self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" )
  return nil
end

--- Returns coalition of the Identifiable.
-- @param #IDENTIFIABLE self
-- @return DCS#coalition.side The side of the coalition or `#nil` The DCS Identifiable is not existing or alive.  
function IDENTIFIABLE:GetCoalition()
  self:F2( self.IdentifiableName )

  local DCSIdentifiable = self:GetDCSObject()
  
  if DCSIdentifiable then
    local IdentifiableCoalition = DCSIdentifiable:getCoalition()
    self:T3( IdentifiableCoalition )
    return IdentifiableCoalition
  end 
  
  self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" )
  return nil
end

--- Returns the name of the coalition of the Identifiable.
-- @param #IDENTIFIABLE self
-- @return #string The name of the coalition.
-- @return #nil The DCS Identifiable is not existing or alive.  
function IDENTIFIABLE:GetCoalitionName()
  self:F2( self.IdentifiableName )

  local DCSIdentifiable = self:GetDCSObject()
  
  if DCSIdentifiable then
  
    -- Get coalition ID.
    local IdentifiableCoalition = DCSIdentifiable:getCoalition()
    self:T3( IdentifiableCoalition )
    
    return UTILS.GetCoalitionName(IdentifiableCoalition)
    
  end 
  
  self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" )
  return nil
end

--- Returns country of the Identifiable.
-- @param #IDENTIFIABLE self
-- @return DCS#country.id The country identifier or `#nil` The DCS Identifiable is not existing or alive.  
function IDENTIFIABLE:GetCountry()
  self:F2( self.IdentifiableName )

  local DCSIdentifiable = self:GetDCSObject()
  
  if DCSIdentifiable then
    local IdentifiableCountry = DCSIdentifiable:getCountry()
    self:T3( IdentifiableCountry )
    return IdentifiableCountry
  end 
  
  self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" )
  return nil
end

--- Returns country name of the Identifiable.
-- @param #IDENTIFIABLE self
-- @return #string Name of the country.  
function IDENTIFIABLE:GetCountryName()
  self:F2( self.IdentifiableName ) 
  local countryid=self:GetCountry()
  for name,id in pairs(country.id) do
    if countryid==id then
      return name
    end
  end
end

--- Returns Identifiable descriptor. Descriptor type depends on Identifiable category.
-- @param #IDENTIFIABLE self
-- @return DCS#Object.Desc The Identifiable descriptor or `#nil` The DCS Identifiable is not existing or alive.  
function IDENTIFIABLE:GetDesc()
  self:F2( self.IdentifiableName )

  local DCSIdentifiable = self:GetDCSObject() -- DCS#Object
  
  if DCSIdentifiable then
    local IdentifiableDesc = DCSIdentifiable:getDesc()
    self:T2( IdentifiableDesc )
    return IdentifiableDesc
  end
  
  self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" )
  return nil
end

--- Check if the Object has the attribute.
-- @param #IDENTIFIABLE self
-- @param #string AttributeName The attribute name.
-- @return #boolean true if the attribute exists or `#nil` The DCS Identifiable is not existing or alive.  
function IDENTIFIABLE:HasAttribute( AttributeName )
  self:F2( self.IdentifiableName )

  local DCSIdentifiable = self:GetDCSObject()
  
  if DCSIdentifiable then
    local IdentifiableHasAttribute = DCSIdentifiable:hasAttribute( AttributeName )
    self:T2( IdentifiableHasAttribute )
    return IdentifiableHasAttribute
  end
  
  self:F( self.ClassName .. " " .. self.IdentifiableName .. " not found!" )
  return nil
end

--- Gets the CallSign of the IDENTIFIABLE, which is a blank by default.
-- @param #IDENTIFIABLE self
-- @return #string The CallSign of the IDENTIFIABLE.
function IDENTIFIABLE:GetCallsign()
  return ''
end

--- Gets the threat level.
-- @param #IDENTIFIABLE self
-- @return #number Threat level.
-- @return #string Type.
function IDENTIFIABLE:GetThreatLevel()
  return 0, "Scenery"
end
--- **Wrapper** - POSITIONABLE wraps DCS classes that are "positionable".
--
-- ===
--
-- ### Author: **FlightControl**
--
-- ### Contributions: **Hardcard**, **funkyfranky**
--
-- ===
--
-- @module Wrapper.Positionable
-- @image Wrapper_Positionable.JPG

--- @type POSITIONABLE.__ Methods which are not intended for mission designers, but which are used internally by the moose designer :-)
-- @extends Wrapper.Identifiable#IDENTIFIABLE

--- @type POSITIONABLE
-- @field Core.Point#COORDINATE coordinate Coordinate object.
-- @field Core.Point#COORDINATE pointvec3 Point Vec3 object.
-- @extends Wrapper.Identifiable#IDENTIFIABLE


--- Wrapper class to handle the POSITIONABLE objects.
--
--  * Support all DCS APIs.
--  * Enhance with POSITIONABLE specific APIs not in the DCS API set.
--  * Manage the "state" of the POSITIONABLE.
--
-- ## POSITIONABLE constructor
--
-- The POSITIONABLE class provides the following functions to construct a POSITIONABLE instance:
--
--  * @{#POSITIONABLE.New}(): Create a POSITIONABLE instance.
--
-- ## Get the current speed
--
-- There are 3 methods that can be used to determine the speed.
-- Use @{#POSITIONABLE.GetVelocityKMH}() to retrieve the current speed in km/h. Use @{#POSITIONABLE.GetVelocityMPS}() to retrieve the speed in meters per second.
-- The method @{#POSITIONABLE.GetVelocity}() returns the speed vector (a Vec3).
--
-- ## Get the current altitude
--
-- Altitude can be retrieved using the method @{#POSITIONABLE.GetHeight}() and returns the current altitude in meters from the orthonormal plane.
--
--
-- @field #POSITIONABLE
POSITIONABLE = {
  ClassName = "POSITIONABLE",
  PositionableName = "",
  coordinate = nil,
  pointvec3  = nil,
}

--- @field #POSITIONABLE.__
POSITIONABLE.__ = {}

--- @field #POSITIONABLE.__.Cargo
POSITIONABLE.__.Cargo = {}

--- A DCSPositionable
-- @type DCSPositionable
-- @field id_ The ID of the controllable in DCS

--- Create a new POSITIONABLE from a DCSPositionable
-- @param #POSITIONABLE self
-- @param #string PositionableName The POSITIONABLE name
-- @return #POSITIONABLE self
function POSITIONABLE:New( PositionableName )
  local self = BASE:Inherit( self, IDENTIFIABLE:New( PositionableName ) ) -- #POSITIONABLE

  self.PositionableName = PositionableName
  return self
end

--- Destroys the POSITIONABLE.
-- @param #POSITIONABLE self
-- @param #boolean GenerateEvent (Optional) If true, generates a crash or dead event for the unit. If false, no event generated. If nil, a remove event is generated. 
-- @return #nil The DCS Unit is not existing or alive.
-- @usage
--
-- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group.
-- Helicopter = UNIT:FindByName( "Helicopter" )
-- Helicopter:Destroy( true )
--
-- @usage
-- -- Ground unit example: destroy the Tanks and generate a S_EVENT_DEAD for each unit in the Tanks group.
-- Tanks = UNIT:FindByName( "Tanks" )
-- Tanks:Destroy( true )
--
-- @usage
-- -- Ship unit example: destroy the Ship silently.
-- Ship = STATIC:FindByName( "Ship" )
-- Ship:Destroy()
--
-- @usage
-- -- Destroy without event generation example.
-- Ship = STATIC:FindByName( "Boat" )
-- Ship:Destroy( false ) -- Don't generate an event upon destruction.
--
function POSITIONABLE:Destroy( GenerateEvent )
  self:F2( self.ObjectName )

  local DCSObject = self:GetDCSObject()

  if DCSObject then

    local UnitGroup = self:GetGroup()
    local UnitGroupName = UnitGroup:GetName()
    self:F( { UnitGroupName = UnitGroupName } )

    if GenerateEvent and GenerateEvent == true then
      if self:IsAir() then
        --self:ScheduleOnce(1,self.CreateEventCrash,self,timer.getTime(),DCSObject)
        self:CreateEventCrash( timer.getTime(), DCSObject )
      else
        --self:ScheduleOnce(1,self.CreateEventDead,self,timer.getTime(),DCSObject)
        self:CreateEventDead( timer.getTime(), DCSObject )
      end
    elseif GenerateEvent == false then
      -- Do nothing!
    else
      self:CreateEventRemoveUnit( timer.getTime(), DCSObject )
      --self:ScheduleOnce(1,self.CreateEventRemoveUnit,self,timer.getTime(),DCSObject)
    end

    USERFLAG:New( UnitGroupName ):Set( 100 )
    DCSObject:destroy()
  end

  return nil
end

--- Returns the DCS object. Polymorphic for other classes like UNIT, STATIC, GROUP, AIRBASE.
-- @param #POSITIONABLE self
-- @return DCS#Object The DCS object.
function POSITIONABLE:GetDCSObject()
  return nil
end

--- Returns a pos3 table of the objects current position and orientation in 3D space. X, Y, Z values are unit vectors defining the objects orientation.
-- Coordinates are dependent on the position of the maps origin.
-- @param #POSITIONABLE self
-- @return DCS#Position3 Table consisting of the point and orientation tables.
function POSITIONABLE:GetPosition()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()
  
  if self:IsInstanceOf("GROUP") then
    DCSPositionable = self:GetFirstUnitAlive():GetDCSObject()
  end
    
  if DCSPositionable then
    local PositionablePosition = DCSPositionable:getPosition()
    self:T3( PositionablePosition )
    return PositionablePosition
  end

  BASE:E( { "Cannot GetPosition", Positionable = self, Alive = self:IsAlive() } )
  return nil
end

--- Returns a {@DCS#Vec3} table of the objects current orientation in 3D space. X, Y, Z values are unit vectors defining the objects orientation.
-- X is the orientation parallel to the movement of the object, Z perpendicular and Y vertical orientation.
-- @param #POSITIONABLE self
-- @return DCS#Vec3 X orientation, i.e. parallel to the direction of movement.
-- @return DCS#Vec3 Y orientation, i.e. vertical.
-- @return DCS#Vec3 Z orientation, i.e. perpendicular to the direction of movement.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetOrientation()
  local position = self:GetPosition()
  if position then
    return position.x, position.y, position.z
  else
    BASE:E( { "Cannot GetOrientation", Positionable = self, Alive = self:IsAlive() } )
    return nil, nil, nil
  end
end

--- Returns a {@DCS#Vec3} table of the objects current X orientation in 3D space, i.e. along the direction of movement.
-- @param #POSITIONABLE self
-- @return DCS#Vec3 X orientation, i.e. parallel to the direction of movement.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetOrientationX()
  local position = self:GetPosition()
  if position then
    return position.x
  else
    BASE:E( { "Cannot GetOrientationX", Positionable = self, Alive = self:IsAlive() } )
    return nil
  end
end

--- Returns a {@DCS#Vec3} table of the objects current Y orientation in 3D space, i.e. vertical orientation.
-- @param #POSITIONABLE self
-- @return DCS#Vec3 Y orientation, i.e. vertical.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetOrientationY()
  local position = self:GetPosition()
  if position then
    return position.y
  else
    BASE:E( { "Cannot GetOrientationY", Positionable = self, Alive = self:IsAlive() } )
    return nil
  end
end

--- Returns a {@DCS#Vec3} table of the objects current Z orientation in 3D space, i.e. perpendicular to direction of movement.
-- @param #POSITIONABLE self
-- @return DCS#Vec3 Z orientation, i.e. perpendicular to movement.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetOrientationZ()
  local position = self:GetPosition()
  if position then
    return position.z
  else
    BASE:E( { "Cannot GetOrientationZ", Positionable = self, Alive = self:IsAlive() } )
    return nil
  end
end

--- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission.
-- @param #POSITIONABLE self
-- @return DCS#Position The 3D position vectors of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetPositionVec3()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable then
    local PositionablePosition = DCSPositionable:getPosition().p
    self:T3( PositionablePosition )
    return PositionablePosition
  end

  BASE:E( { "Cannot GetPositionVec3", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns the @{DCS#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission.
-- @param #POSITIONABLE self
-- @return DCS#Vec3 The 3D point vector of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetVec3()
  local DCSPositionable = self:GetDCSObject()
  if DCSPositionable then

    local vec3 = DCSPositionable:getPoint()
    
    if not vec3 then 
      local pos = DCSPositionable:getPosition()
      if pos and pos.p then 
       vec3 = pos.p
      else
        self:E( { "Cannot get the position from DCS Object for GetVec3", Positionable = self, Alive = self:IsAlive() } )
      end
    end

    return vec3

  end
  -- ERROR!
  self:E( { "Cannot get the Positionable DCS Object for GetVec3", Positionable = self, Alive = self:IsAlive() } )
  return nil
end

--- Returns the @{DCS#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission.
-- @param #POSITIONABLE self
-- @return DCS#Vec2 The 2D point vector of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetVec2()

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable then

    local Vec3 = DCSPositionable:getPoint() -- DCS#Vec3

    return { x = Vec3.x, y = Vec3.z }
  end

  self:E( { "Cannot GetVec2", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns a COORDINATE object indicating the point in 2D of the POSITIONABLE within the mission.
-- @param #POSITIONABLE self
-- @return Core.Point#COORDINATE The 3D point vector of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetPointVec2()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable then
    local PositionableVec3 = DCSPositionable:getPosition().p

    local PositionablePointVec2 = COORDINATE:NewFromVec3( PositionableVec3 )

    -- self:F( PositionablePointVec2 )
    return PositionablePointVec2
  end

  self:E( { "Cannot Coordinate", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns a COORDINATE object indicating the point in 3D of the POSITIONABLE within the mission.
-- @param #POSITIONABLE self
-- @return Core.Point#COORDINATE The 3D point vector of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetPointVec3()

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable then

    -- Get 3D vector.
    local PositionableVec3 = self:GetPositionVec3()

    if false and self.pointvec3 then

      -- Update vector.
      self.pointvec3.x = PositionableVec3.x
      self.pointvec3.y = PositionableVec3.y
      self.pointvec3.z = PositionableVec3.z

    else

      -- Create a new COORDINATE object.
      self.pointvec3 = COORDINATE:NewFromVec3( PositionableVec3 )

    end

    return self.pointvec3
  end

  BASE:E( { "Cannot GetPointVec3", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns a reference to a COORDINATE object indicating the point in 3D of the POSITIONABLE within the mission.
-- This function works similar to POSITIONABLE.GetCoordinate(), however, this function caches, updates and re-uses the same COORDINATE object stored
-- within the POSITIONABLE. This has higher performance, but comes with all considerations associated with the possible referencing to the same COORDINATE object.
-- This should only be used when performance is critical and there is sufficient awareness of the possible pitfalls. However, in most instances, GetCoordinate() is
-- preferred as it will return a fresh new COORDINATE and thus avoid potentially unexpected issues.
-- @param #POSITIONABLE self
-- @return Core.Point#COORDINATE A reference to the COORDINATE object of the POSITIONABLE.
function POSITIONABLE:GetCoord()

  -- Get DCS object.
  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable then

    -- Get the current position.
    local PositionableVec3 = self:GetVec3()

    if PositionableVec3 then
        if self.coordinate then
          -- Update COORDINATE from 3D vector.
          self.coordinate:UpdateFromVec3( PositionableVec3 )
        else
          -- New COORDINATE.
          self.coordinate = COORDINATE:NewFromVec3( PositionableVec3 )
        end

        return self.coordinate
    end
  end

  -- Error message.
  BASE:E( { "Cannot GetCoordinate", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns a new COORDINATE object indicating the point in 3D of the POSITIONABLE within the mission.
-- @param #POSITIONABLE self
-- @return Core.Point#COORDINATE A new COORDINATE object of the POSITIONABLE.
function POSITIONABLE:GetCoordinate()

  -- Get DCS object.
  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable then

    -- Get the current position.
    local PositionableVec3 = self:GetVec3()
    if PositionableVec3 then
      local coord=COORDINATE:NewFromVec3(PositionableVec3)
      local heading = self:GetHeading()
      coord.Heading = heading
      -- Return a new coordiante object.
      return coord
    end
  end

  -- Error message.
  self:E( { "Cannot GetCoordinate", Positionable = self, Alive = self:IsAlive() } )
  return nil
end


--- Triggers an explosion at the coordinates of the positionable.
-- @param #POSITIONABLE self
-- @param #number power Power of the explosion in kg TNT. Default 100 kg TNT.
-- @param #number delay (Optional) Delay of explosion in seconds.
-- @return #POSITIONABLE self
function POSITIONABLE:Explode(power, delay)

  -- Default.
  power=power or 100
  
  -- Check if delay or not.
  if delay and delay>0 then
    -- Delayed call.
    self:ScheduleOnce(delay, POSITIONABLE.Explode, self, power, 0)
  else
  
    local coord=self:GetCoord()
    
    if coord then  
      -- Create an explotion at the coordinate of the positionable.
      coord:Explosion(power)
    end
  
  end
  
  return self
end

--- Returns a COORDINATE object, which is offset with respect to the orientation of the POSITIONABLE.
-- @param #POSITIONABLE self
-- @param #number x Offset in the direction "the nose" of the unit is pointing in meters. Default 0 m.
-- @param #number y Offset "above" the unit in meters. Default 0 m.
-- @param #number z Offset in the direction "the wing" of the unit is pointing in meters. z>0 starboard, z<0 port. Default 0 m.
-- @return Core.Point#COORDINATE The COORDINATE of the offset with respect to the orientation of the  POSITIONABLE.
function POSITIONABLE:GetOffsetCoordinate( x, y, z )

  -- Default if nil.
  x = x or 0
  y = y or 0
  z = z or 0

  -- Vectors making up the coordinate system.
  local X = self:GetOrientationX()
  local Y = self:GetOrientationY()
  local Z = self:GetOrientationZ()

  -- Offset vector: x meters ahead, z meters starboard, y meters above.
  local A = { x = x, y = y, z = z }

  -- Scale components of orthonormal coordinate vectors.
  local x = { x = X.x * A.x, y = X.y * A.x, z = X.z * A.x }
  local y = { x = Y.x * A.y, y = Y.y * A.y, z = Y.z * A.y }
  local z = { x = Z.x * A.z, y = Z.y * A.z, z = Z.z * A.z }

  -- Add up vectors in the unit coordinate system ==> this gives the offset vector relative the the origin of the map.
  local a = { x = x.x + y.x + z.x, y = x.y + y.y + z.y, z = x.z + y.z + z.z }

  -- Vector from the origin of the map to the unit.
  local u = self:GetVec3()

  -- Translate offset vector from map origin to the unit: v=u+a.
  local v = { x = a.x + u.x, y = a.y + u.y, z = a.z + u.z }

  local coord = COORDINATE:NewFromVec3( v )

  -- Return the offset coordinate.
  return coord
end

--- Returns a COORDINATE object, which is transformed to be relative to the POSITIONABLE. Inverse of @{#POSITIONABLE.GetOffsetCoordinate}.
-- @param #POSITIONABLE self
-- @param #number x Offset along the world x-axis in meters. Default 0 m.
-- @param #number y Offset along the world y-axis in meters. Default 0 m.
-- @param #number z Offset along the world z-axis in meters. Default 0 m.
-- @return Core.Point#COORDINATE The relative COORDINATE with respect to the orientation of the  POSITIONABLE.
function POSITIONABLE:GetRelativeCoordinate( x, y, z )

  -- Default if nil.
  x = x or 0
  y = y or 0
  z = z or 0
  
  
  -- Vector from the origin of the map to self.
  local selfPos = self:GetVec3()
  
  -- Vectors making up self's local coordinate system.
  local X = self:GetOrientationX()
  local Y = self:GetOrientationY()
  local Z = self:GetOrientationZ()
  
  -- Offset from self to self's position (still in the world coordinate system).
  local off = {
    x = x - selfPos.x,
    y = y - selfPos.y,
    z = z - selfPos.z
  }
  
  -- The end result
  local res = { x = 0, y = 0, z = 0 }
  
  -- Matrix equation to solve:
  -- | X.x, Y.x, Z.x |   | res.x |   | off.x |
  -- | X.y, Y.y, Z.y | . | res.y | = | off.y |
  -- | X.z, Y.z, Z.z |   | res.z |   | off.z |
  
  -- Use gaussian elimination to solve the system of equations.
  -- https://en.wikipedia.org/wiki/Gaussian_elimination

  local mat = {
    { X.x, Y.x, Z.x, off.x },
    { X.y, Y.y, Z.y, off.y },
    { X.z, Y.z, Z.z, off.z }
  }
  
  -- Matrix dimensions
  local m = 3
  local n = 4
  
  -- Pivot indices
  local h = 1
  local k = 1
  
  while h <= m and k <= n do
    local v_max = math.abs( mat[h][k] )
    local i_max = h
    for i = h,m,1 do
      local value = math.abs( mat[i][k] )
        if value > v_max then
          i_max = i
          v_max = value
        end
    end
    
    if mat[i_max][k] == 0 then
      -- Already all 0s, nothing to do.
      k = k + 1
    else
      -- Swap rows h and i_max
      local tmp = mat[h]
      mat[h] = mat[i_max]
      mat[i_max] = tmp
      
      for i = h + 1,m,1 do
        -- The scaling factor to use to reduce all values in this column
        local f = mat[i][k] / mat[h][k]
        mat[i][k] = 0
        for j = k+1,n,1 do
          mat[i][j] = mat[i][j] - f*mat[h][j]
        end
      end
      
      h = h + 1
      k = k + 1
    end
  end
  
  -- mat is now in row echelon form:
  -- | #, #, #, # |
  -- | 0, #, #, # |
  -- | 0, 0, #, # |
  --
  -- and the linear equation is now effectively:
  -- | #, #, # |   | res.x |   | # |
  -- | 0, #, # | . | res.y | = | # |
  -- | 0, 0, # |   | res.z |   | # |
  -- this resulting system of equations can be easily solved via substitution.
  
  res.z = mat[3][4] / mat[3][3]
  res.y = (mat[2][4] - res.z * mat[2][3]) / mat[2][2]
  res.x = (mat[1][4] - res.y * mat[1][2] - res.z * mat[1][3]) / mat[1][1]
  
  local coord = COORDINATE:NewFromVec3( res )

  -- Return the relative coordinate.
  return coord
end

--- Returns a random @{DCS#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission.
-- @param #POSITIONABLE self
-- @param #number Radius
-- @return DCS#Vec3 The 3D point vector of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.
-- @usage
-- -- If Radius is ignored, returns the DCS#Vec3 of first UNIT of the GROUP
function POSITIONABLE:GetRandomVec3( Radius )
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable then
    local PositionablePointVec3 = DCSPositionable:getPosition().p

    if Radius then
      local PositionableRandomVec3 = {}
      local angle = math.random() * math.pi * 2
      PositionableRandomVec3.x = PositionablePointVec3.x + math.cos( angle ) * math.random() * Radius
      PositionableRandomVec3.y = PositionablePointVec3.y
      PositionableRandomVec3.z = PositionablePointVec3.z + math.sin( angle ) * math.random() * Radius

      self:T3( PositionableRandomVec3 )
      return PositionableRandomVec3
    else
      self:F( "Radius is nil, returning the PointVec3 of the POSITIONABLE", PositionablePointVec3 )
      return PositionablePointVec3
    end
  end

  BASE:E( { "Cannot GetRandomVec3", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Get the bounding box of the underlying POSITIONABLE DCS Object.
-- @param #POSITIONABLE self
-- @return DCS#Box3 The bounding box of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetBoundingBox()
  self:F2()

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable then
    local PositionableDesc = DCSPositionable:getDesc() -- DCS#Desc
    if PositionableDesc then
      local PositionableBox = PositionableDesc.box
      return PositionableBox
    end
  end

  BASE:E( { "Cannot GetBoundingBox", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Get the object size.
-- @param #POSITIONABLE self
-- @return DCS#Distance Max size of object in x, z or 0 if bounding box could not be obtained.
-- @return DCS#Distance Length x or 0 if bounding box could not be obtained.
-- @return DCS#Distance Height y or 0 if bounding box could not be obtained.
-- @return DCS#Distance Width z or 0 if bounding box could not be obtained.
function POSITIONABLE:GetObjectSize()

  -- Get bounding box.
  local box = self:GetBoundingBox()

  if box then
    local x = box.max.x + math.abs( box.min.x ) -- length
    local y = box.max.y + math.abs( box.min.y ) -- height
    local z = box.max.z + math.abs( box.min.z ) -- width
    return math.max( x, z ), x, y, z
  end

  return 0, 0, 0, 0
end

--- Get the bounding radius of the underlying POSITIONABLE DCS Object.
-- @param #POSITIONABLE self
-- @param #number MinDist (Optional) If bounding box is smaller than this value, MinDist is returned.
-- @return DCS#Distance The bounding radius of the POSITIONABLE
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetBoundingRadius( MinDist )
  self:F2()

  local Box = self:GetBoundingBox()

  local boxmin = MinDist or 0
  if Box then
    local X = Box.max.x - Box.min.x
    local Z = Box.max.z - Box.min.z
    local CX = X / 2
    local CZ = Z / 2
    return math.max( math.max( CX, CZ ), boxmin )
  end

  BASE:T( { "Cannot GetBoundingRadius", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns the altitude above sea level of the POSITIONABLE.
-- @param #POSITIONABLE self
-- @return DCS#Distance The altitude of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetAltitude()
  self:F2()

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable then
    local PositionablePointVec3 = DCSPositionable:getPoint() -- DCS#Vec3
    return PositionablePointVec3.y
  end

  BASE:E( { "Cannot GetAltitude", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns if the Positionable is located above a runway.
-- @param #POSITIONABLE self
-- @return #boolean true if Positionable is above a runway.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:IsAboveRunway()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable then

    local Vec2 = self:GetVec2()
    local SurfaceType = land.getSurfaceType( Vec2 )
    local IsAboveRunway = SurfaceType == land.SurfaceType.RUNWAY

    self:T2( IsAboveRunway )
    return IsAboveRunway
  end

  BASE:E( { "Cannot IsAboveRunway", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

function POSITIONABLE:GetSize()

  local DCSObject = self:GetDCSObject()

  if DCSObject then
    return 1
  else
    return 0
  end
end

--- Returns the POSITIONABLE heading in degrees.
-- @param #POSITIONABLE self
-- @return #number The POSITIONABLE heading in degrees.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetHeading()

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable then

    local PositionablePosition = DCSPositionable:getPosition()

    if PositionablePosition then
      local PositionableHeading = math.atan2( PositionablePosition.x.z, PositionablePosition.x.x )

      if PositionableHeading < 0 then
        PositionableHeading = PositionableHeading + 2 * math.pi
      end

      PositionableHeading = PositionableHeading * 180 / math.pi

      return PositionableHeading
    end
  end

  self:E( { "Cannot GetHeading", Positionable = self, Alive = self:IsAlive() } )
  return nil
end

-- Is Methods

--- Returns if the unit is of an air category.
-- If the unit is a helicopter or a plane, then this method will return true, otherwise false.
-- @param #POSITIONABLE self
-- @return #boolean Air category evaluation result.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:IsAir()
  self:F2()

  local DCSUnit = self:GetDCSObject()

  if DCSUnit then
    local UnitDescriptor = DCSUnit:getDesc()
    self:T3( { UnitDescriptor.category, Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } )

    local IsAirResult = ( UnitDescriptor.category == Unit.Category.AIRPLANE ) or ( UnitDescriptor.category == Unit.Category.HELICOPTER )

    self:T3( IsAirResult )
    return IsAirResult
  end

  self:E( { "Cannot check IsAir", Positionable = self, Alive = self:IsAlive() } )
  return nil
end

--- Returns if the unit is of an ground category.
-- If the unit is a ground vehicle or infantry, this method will return true, otherwise false.
-- @param #POSITIONABLE self
-- @return #boolean Ground category evaluation result.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:IsGround()
  self:F2()

  local DCSUnit = self:GetDCSObject()

  if DCSUnit then
    local UnitDescriptor = DCSUnit:getDesc()
    self:T3( { UnitDescriptor.category, Unit.Category.GROUND_UNIT } )

    local IsGroundResult = (UnitDescriptor.category == Unit.Category.GROUND_UNIT)

    self:T3( IsGroundResult )
    return IsGroundResult
  end

  self:E( { "Cannot check IsGround", Positionable = self, Alive = self:IsAlive() } )
  return nil
end

--- Returns if the unit is of ship category.
-- @param #POSITIONABLE self
-- @return #boolean Ship category evaluation result.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:IsShip()
  self:F2()

  local DCSUnit = self:GetDCSObject()

  if DCSUnit then
    local UnitDescriptor = DCSUnit:getDesc()
    self:T3( { UnitDescriptor.category, Unit.Category.SHIP } )

    local IsShipResult = (UnitDescriptor.category == Unit.Category.SHIP)

    self:T3( IsShipResult )
    return IsShipResult
  end

  self:E( { "Cannot check IsShip", Positionable = self, Alive = self:IsAlive() } )
  return nil
end

--- Returns if the unit is a submarine.
-- @param #POSITIONABLE self
-- @return #boolean Submarines attributes result.
function POSITIONABLE:IsSubmarine()
  self:F2()

  local DCSUnit = self:GetDCSObject()

  if DCSUnit then
    local UnitDescriptor = DCSUnit:getDesc()
    if UnitDescriptor.attributes["Submarines"] == true then
      return true
    else
      return false
    end
  end

  self:E( { "Cannot check IsSubmarine", Positionable = self, Alive = self:IsAlive() } )
  return nil
end

--- Returns true if the POSITIONABLE is in the air.
-- Polymorphic, is overridden in GROUP and UNIT.
-- @param #POSITIONABLE self
-- @return #boolean true if in the air.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:InAir()
  self:F2( self.PositionableName )

  return nil
end

--- Returns the @{Core.Velocity} object from the POSITIONABLE.
-- @param #POSITIONABLE self
-- @return Core.Velocity#VELOCITY Velocity The Velocity object.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetVelocity()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable then
    local Velocity = VELOCITY:New( self )
    return Velocity
  end

  BASE:E( { "Cannot GetVelocity", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns the POSITIONABLE velocity Vec3 vector.
-- @param #POSITIONABLE self
-- @return DCS#Vec3 The velocity Vec3 vector
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetVelocityVec3()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable and DCSPositionable:isExist() then
    local PositionableVelocityVec3 = DCSPositionable:getVelocity()
    self:T3( PositionableVelocityVec3 )
    return PositionableVelocityVec3
  end

  BASE:E( { "Cannot GetVelocityVec3", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Get relative velocity with respect to another POSITIONABLE.
-- @param #POSITIONABLE self
-- @param #POSITIONABLE Positionable Other POSITIONABLE.
-- @return #number Relative velocity in m/s.
function POSITIONABLE:GetRelativeVelocity( Positionable )
  self:F2( self.PositionableName )

  local v1 = self:GetVelocityVec3()
  local v2 = Positionable:GetVelocityVec3()

  local vtot = UTILS.VecAdd( v1, v2 )

  return UTILS.VecNorm( vtot )
end


--- Returns the POSITIONABLE height above sea level in meters.
-- @param #POSITIONABLE self
-- @return DCS#Vec3 Height of the positionable in meters (or nil, if the object does not exist).
function POSITIONABLE:GetHeight() --R2.1
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable and DCSPositionable:isExist() then
    local PositionablePosition = DCSPositionable:getPosition()
    if PositionablePosition then
      local PositionableHeight = PositionablePosition.p.y
      self:T2( PositionableHeight )
      return PositionableHeight
    end
  end

  return nil
end

--- Returns the POSITIONABLE velocity in km/h.
-- @param #POSITIONABLE self
-- @return #number The velocity in km/h.
function POSITIONABLE:GetVelocityKMH()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable and DCSPositionable:isExist() then
    local VelocityVec3 = self:GetVelocityVec3()
    local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec
    local Velocity = Velocity * 3.6 -- now it is in km/h.
    self:T3( Velocity )
    return Velocity
  end

  return 0
end

--- Returns the POSITIONABLE velocity in meters per second.
-- @param #POSITIONABLE self
-- @return #number The velocity in meters per second.
function POSITIONABLE:GetVelocityMPS()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable and DCSPositionable:isExist() then
    local VelocityVec3 = self:GetVelocityVec3()
    local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec
    self:T3( Velocity )
    return Velocity
  end

  return 0
end

--- Returns the POSITIONABLE velocity in knots.
-- @param #POSITIONABLE self
-- @return #number The velocity in knots.
function POSITIONABLE:GetVelocityKNOTS()
  self:F2( self.PositionableName )
  return UTILS.MpsToKnots( self:GetVelocityMPS() )
end

--- Returns the true airspeed (TAS). This is calculated from the current velocity minus wind in 3D.
-- @param #POSITIONABLE self
-- @return #number TAS in m/s. Returns 0 if the POSITIONABLE does not exist.
function POSITIONABLE:GetAirspeedTrue()

  -- TAS
  local tas=0

  -- Get current coordinate.
  local coord=self:GetCoord()
  
  if coord then
  
    -- Altitude in meters.
    local alt=coord.y
  
    -- Wind velocity vector.
    local wvec3=coord:GetWindVec3(alt, false)
    
    -- Velocity vector.
    local vvec3=self:GetVelocityVec3()

    --GS=TAS+WIND ==> TAS=GS-WIND
    local tasvec3=UTILS.VecSubstract(vvec3, wvec3)

    -- True airspeed in m/s
    tas=UTILS.VecNorm(tasvec3)
  
  end

  return tas
end

--- Returns the indicated airspeed (IAS).
-- The IAS is calculated from the TAS under the approximation that TAS increases by ~2% with every 1000 feet altitude ASL.
-- @param #POSITIONABLE self
-- @param #number oatcorr (Optional) Outside air temperature (OAT) correction factor. Default 0.017 (=1.7%).
-- @return #number IAS in m/s. Returns 0 if the POSITIONABLE does not exist.
function POSITIONABLE:GetAirspeedIndicated(oatcorr)

  -- Get true airspeed.
  local tas=self:GetAirspeedTrue()
  
  -- Get altitude.
  local altitude=self:GetAltitude()
  
  -- Convert TAS to IAS.
  local ias=UTILS.TasToIas(tas, altitude, oatcorr)

  return ias
end

--- Returns the horizonal speed relative to eath's surface. The vertical component of the velocity vector is projected out (set to zero).
-- @param #POSITIONABLE self
-- @return #number Ground speed in m/s. Returns 0 if the POSITIONABLE does not exist.
function POSITIONABLE:GetGroundSpeed()

  local gs=0
  
  local vel=self:GetVelocityVec3()
  
  if vel then
    
    local vec2={x=vel.x, y=vel.z}
    
    gs=UTILS.Vec2Norm(vel)
  
  end

  return gs
end

--- Returns the Angle of Attack of a POSITIONABLE.
-- @param #POSITIONABLE self
-- @return #number Angle of attack in degrees.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetAoA()

  -- Get position of the unit.
  local unitpos = self:GetPosition()

  if unitpos then

    -- Get velocity vector of the unit.
    local unitvel = self:GetVelocityVec3()

    if unitvel and UTILS.VecNorm( unitvel ) ~= 0 then

      -- Get wind vector including turbulences.
      local wind = self:GetCoordinate():GetWindWithTurbulenceVec3()

      -- Include wind vector.
      unitvel.x = unitvel.x - wind.x
      unitvel.y = unitvel.y - wind.y
      unitvel.z = unitvel.z - wind.z

      -- Unit velocity transformed into aircraft axes directions.
      local AxialVel = {}

      -- Transform velocity components in direction of aircraft axes.
      AxialVel.x = UTILS.VecDot( unitpos.x, unitvel )
      AxialVel.y = UTILS.VecDot( unitpos.y, unitvel )
      AxialVel.z = UTILS.VecDot( unitpos.z, unitvel )

      -- AoA is angle between unitpos.x and the x and y velocities.
      local AoA = math.acos( UTILS.VecDot( { x = 1, y = 0, z = 0 }, { x = AxialVel.x, y = AxialVel.y, z = 0 } ) / UTILS.VecNorm( { x = AxialVel.x, y = AxialVel.y, z = 0 } ) )

      -- Set correct direction:
      if AxialVel.y > 0 then
        AoA = -AoA
      end

      -- Return AoA value in degrees.
      return math.deg( AoA )
    end

  end

  return nil
end

--- Returns the climb or descent angle of the POSITIONABLE.
-- @param #POSITIONABLE self
-- @return #number Climb or descent angle in degrees. Or 0 if velocity vector norm is zero.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetClimbAngle()

  -- Get position of the unit.
  local unitpos = self:GetPosition()

  if unitpos then

    -- Get velocity vector of the unit.
    local unitvel = self:GetVelocityVec3()

    if unitvel and UTILS.VecNorm( unitvel ) ~= 0 then

      -- Calculate climb angle.
      local angle = math.asin( unitvel.y / UTILS.VecNorm( unitvel ) )

      -- Return angle in degrees.
      return math.deg( angle )
    else
      return 0
    end

  end

  return nil
end

--- Returns the pitch angle of a POSITIONABLE.
-- @param #POSITIONABLE self
-- @return #number Pitch angle in degrees.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetPitch()

  -- Get position of the unit.
  local unitpos = self:GetPosition()

  if unitpos then
    return math.deg( math.asin( unitpos.x.y ) )
  end

  return nil
end

--- Returns the roll angle of a unit.
-- @param #POSITIONABLE self
-- @return #number Pitch angle in degrees.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetRoll()

  -- Get position of the unit.
  local unitpos = self:GetPosition()

  if unitpos then

    -- First, make a vector that is perpendicular to y and unitpos.x with cross product
    local cp = UTILS.VecCross( unitpos.x, { x = 0, y = 1, z = 0 } )

    -- Now, get dot product of of this cross product with unitpos.z
    local dp = UTILS.VecDot( cp, unitpos.z )

    -- Now get the magnitude of the roll (magnitude of the angle between two vectors is acos(vec1.vec2/|vec1||vec2|)
    local Roll = math.acos( dp / (UTILS.VecNorm( cp ) * UTILS.VecNorm( unitpos.z )) )

    -- Now, have to get sign of roll. By convention, making right roll positive
    -- To get sign of roll, use the y component of unitpos.z. For right roll, y component is negative.

    if unitpos.z.y > 0 then -- left roll, flip the sign of the roll
      Roll = -Roll
    end

    return math.deg( Roll )

  end

  return nil
end

--- Returns the yaw angle of a POSITIONABLE.
-- @param #POSITIONABLE self
-- @return #number Yaw angle in degrees.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetYaw()

  -- Get position of the unit.
  local unitpos = self:GetPosition()

  if unitpos then

    -- get unit velocity
    local unitvel = self:GetVelocityVec3()

    if unitvel and UTILS.VecNorm( unitvel ) ~= 0 then -- must have non-zero velocity!
      local AxialVel = {} -- unit velocity transformed into aircraft axes directions

      -- transform velocity components in direction of aircraft axes.
      AxialVel.x = UTILS.VecDot( unitpos.x, unitvel )
      AxialVel.y = UTILS.VecDot( unitpos.y, unitvel )
      AxialVel.z = UTILS.VecDot( unitpos.z, unitvel )

      -- Yaw is the angle between unitpos.x and the x and z velocities
      -- define right yaw as positive
      local Yaw = math.acos( UTILS.VecDot( { x = 1, y = 0, z = 0 }, { x = AxialVel.x, y = 0, z = AxialVel.z } ) / UTILS.VecNorm( { x = AxialVel.x, y = 0, z = AxialVel.z } ) )

      -- now set correct direction:
      if AxialVel.z > 0 then
        Yaw = -Yaw
      end
      return Yaw
    end

  end

  return nil
end

--- Returns the message text with the callsign embedded (if there is one).
-- @param #POSITIONABLE self
-- @param #string Message The message text.
-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE.
-- @return #string The message text.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetMessageText( Message, Name )

  local DCSObject = self:GetDCSObject()

  if DCSObject then

    local Callsign = string.format( "%s", ((Name ~= "" and Name) or self:GetCallsign() ~= "" and self:GetCallsign()) or self:GetName() )
    local MessageText = string.format( "%s - %s", Callsign, Message )
    return MessageText

  end

  return nil
end

--- Returns a message with the callsign embedded (if there is one).
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param DCS#Duration Duration The duration of the message.
-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE.
-- @return Core.Message#MESSAGE
function POSITIONABLE:GetMessage( Message, Duration, Name )

  local DCSObject = self:GetDCSObject()

  if DCSObject then
    local MessageText = self:GetMessageText( Message, Name )
    return MESSAGE:New( MessageText, Duration )
  end

  return nil
end

--- Returns a message of a specified type with the callsign embedded (if there is one).
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param Core.Message#MESSAGE MessageType MessageType The message type.
-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE.
-- @return Core.Message#MESSAGE
function POSITIONABLE:GetMessageType( Message, MessageType, Name ) -- R2.2 changed callsign and name and using GetMessageText

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    local MessageText = self:GetMessageText( Message, Name )
    return MESSAGE:NewType( MessageText, MessageType )
  end

  return nil
end

--- Send a message to all coalitions.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param DCS#Duration Duration The duration of the message.
-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE.
function POSITIONABLE:MessageToAll( Message, Duration, Name )
  self:F2( { Message, Duration } )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    self:GetMessage( Message, Duration, Name ):ToAll()
  end

  return nil
end

--- Send a message to a coalition.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param DCS#Duration Duration The duration of the message.
-- @param DCS#coalition MessageCoalition The Coalition receiving the message.
-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE.
function POSITIONABLE:MessageToCoalition( Message, Duration, MessageCoalition, Name )
  self:F2( { Message, Duration } )

  local Name = Name or ""

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    self:GetMessage( Message, Duration, Name ):ToCoalition( MessageCoalition )
  end

  return nil
end

--- Send a message to a coalition.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param Core.Message#MESSAGE.Type MessageType The message type that determines the duration.
-- @param DCS#coalition MessageCoalition The Coalition receiving the message.
-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE.
function POSITIONABLE:MessageTypeToCoalition( Message, MessageType, MessageCoalition, Name )
  self:F2( { Message, MessageType } )

  local Name = Name or ""

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    self:GetMessageType( Message, MessageType, Name ):ToCoalition( MessageCoalition )
  end

  return nil
end

--- Send a message to the red coalition.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param DCS#Duration Duration The duration of the message.
-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE.
function POSITIONABLE:MessageToRed( Message, Duration, Name )
  self:F2( { Message, Duration } )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    self:GetMessage( Message, Duration, Name ):ToRed()
  end

  return nil
end

--- Send a message to the blue coalition.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param DCS#Duration Duration The duration of the message.
-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE.
function POSITIONABLE:MessageToBlue( Message, Duration, Name )
  self:F2( { Message, Duration } )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    self:GetMessage( Message, Duration, Name ):ToBlue()
  end

  return nil
end

--- Send a message to a client.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param DCS#Duration Duration The duration of the message.
-- @param Wrapper.Client#CLIENT Client The client object receiving the message.
-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE.
function POSITIONABLE:MessageToClient( Message, Duration, Client, Name )
  self:F2( { Message, Duration } )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    self:GetMessage( Message, Duration, Name ):ToClient( Client )
  end

  return nil
end

--- Send a message to a @{Wrapper.Unit}.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param DCS#Duration Duration The duration of the message.
-- @param Wrapper.Unit#UNIT MessageUnit The UNIT object receiving the message.
-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable.
function POSITIONABLE:MessageToUnit( Message, Duration, MessageUnit, Name )
  self:F2( { Message, Duration } )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    if DCSObject:isExist() then
      if MessageUnit:IsAlive() then
        self:GetMessage( Message, Duration, Name ):ToUnit( MessageUnit )
      else
        BASE:E( { "Message not sent to Unit; Unit is not alive...", Message = Message, MessageUnit = MessageUnit } )
      end
    else
      BASE:E( { "Message not sent to Unit; Positionable is not alive ...", Message = Message, Positionable = self, MessageUnit = MessageUnit } )
    end
  end
end

--- Send a message to a @{Wrapper.Group}.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param DCS#Duration Duration The duration of the message.
-- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message.
-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE.
function POSITIONABLE:MessageToGroup( Message, Duration, MessageGroup, Name )
  self:F2( { Message, Duration } )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    if DCSObject:isExist() then
      if MessageGroup:IsAlive() then
        self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup )
      else
        BASE:E( { "Message not sent to Group; Group is not alive...", Message = Message, MessageGroup = MessageGroup } )
      end
    else
      BASE:E( {
        "Message not sent to Group; Positionable is not alive ...",
        Message = Message,
        Positionable = self,
        MessageGroup = MessageGroup
      } )
    end
  end

  return nil
end

--- Send a message to a @{Wrapper.Unit}.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param DCS#Duration Duration The duration of the message.
-- @param Wrapper.Unit#UNIT MessageUnit The UNIT object receiving the message.
-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable.
function POSITIONABLE:MessageToUnit( Message, Duration, MessageUnit, Name )
  self:F2( { Message, Duration } )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    if DCSObject:isExist() then
      if MessageUnit:IsAlive() then
        self:GetMessage( Message, Duration, Name ):ToUnit( MessageUnit )
      else
        BASE:E( { "Message not sent to Unit; Unit is not alive...", Message = Message, MessageUnit = MessageUnit } )
      end
    else
      BASE:E( { "Message not sent to Unit; Positionable is not alive ...", Message = Message, Positionable = self, MessageUnit = MessageUnit } )
    end
  end


  return nil
end

--- Send a message of a message type to a @{Wrapper.Group}.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param Core.Message#MESSAGE.Type MessageType The message type that determines the duration.
-- @param Wrapper.Group#GROUP MessageGroup The GROUP object receiving the message.
-- @param #string Name (Optional) The Name of the sender. If not provided, the Name is the type of the POSITIONABLE.
function POSITIONABLE:MessageTypeToGroup( Message, MessageType, MessageGroup, Name )
  self:F2( { Message, MessageType } )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    if DCSObject:isExist() then
      self:GetMessageType( Message, MessageType, Name ):ToGroup( MessageGroup )
    end
  end

  return nil
end

--- Send a message to a @{Core.Set#SET_GROUP}.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param DCS#Duration Duration The duration of the message.
-- @param Core.Set#SET_GROUP MessageSetGroup The SET_GROUP collection receiving the message.
-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE.
function POSITIONABLE:MessageToSetGroup( Message, Duration, MessageSetGroup, Name )
  self:F2( { Message, Duration } )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    if DCSObject:isExist() then
      MessageSetGroup:ForEachGroupAlive( function( MessageGroup )
        self:GetMessage( Message, Duration, Name ):ToGroup( MessageGroup )
      end )
    end
  end

  return nil
end

--- Send a message to a @{Core.Set#SET_UNIT}.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param DCS#Duration Duration The duration of the message.
-- @param Core.Set#SET_UNIT MessageSetUnit The SET_UNIT collection receiving the message.
-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable.
function POSITIONABLE:MessageToSetUnit( Message, Duration, MessageSetUnit, Name )
  self:F2( { Message, Duration } )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    if DCSObject:isExist() then
      MessageSetUnit:ForEachUnit(
        function( MessageGroup )
          self:GetMessage( Message, Duration, Name ):ToUnit( MessageGroup )
        end
      )
    end
  end

  return nil
end

--- Send a message to a @{Core.Set#SET_UNIT}.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param DCS#Duration Duration The duration of the message.
-- @param Core.Set#SET_UNIT MessageSetUnit The SET_UNIT collection receiving the message.
-- @param #string Name (optional) The Name of the sender. If not provided, the Name is the type of the Positionable.
function POSITIONABLE:MessageToSetUnit( Message, Duration, MessageSetUnit, Name )
  self:F2( { Message, Duration } )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    if DCSObject:isExist() then
      MessageSetUnit:ForEachUnit(
        function( MessageGroup )
          self:GetMessage( Message, Duration, Name ):ToUnit( MessageGroup )
        end
      )
    end
  end

  return nil
end

--- Send a message to the players in the @{Wrapper.Group}.
-- The message will appear in the message area. The message will begin with the callsign of the group and the type of the first unit sending the message.
-- @param #POSITIONABLE self
-- @param #string Message The message text
-- @param DCS#Duration Duration The duration of the message.
-- @param #string Name (Optional) The Name of the sender. If not provided, Name is set to the type of the POSITIONABLE.
function POSITIONABLE:Message( Message, Duration, Name )
  self:F2( { Message, Duration } )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    self:GetMessage( Message, Duration, Name ):ToGroup( self )
  end

  return nil
end

--- Create a @{Sound.Radio#RADIO}, to allow radio transmission for this POSITIONABLE.
-- Set parameters with the methods provided, then use RADIO:Broadcast() to actually broadcast the message
-- @param #POSITIONABLE self
-- @return Sound.Radio#RADIO Radio
function POSITIONABLE:GetRadio()
  self:F2( self )
  return RADIO:New( self )
end

--- Create a @{Core.Beacon#BEACON}, to allow this POSITIONABLE to broadcast beacon signals.
-- @param #POSITIONABLE self
-- @return Core.Beacon#BEACON Beacon
function POSITIONABLE:GetBeacon()
  self:F2( self )
  return BEACON:New( self )
end

--- Start Lasing a POSITIONABLE.
-- @param #POSITIONABLE self
-- @param #POSITIONABLE Target The target to lase.
-- @param #number LaserCode Laser code or random number in [1000, 9999].
-- @param #number Duration Duration of lasing in seconds.
-- @return Core.Spot#SPOT
function POSITIONABLE:LaseUnit( Target, LaserCode, Duration )
  self:F2()

  LaserCode = LaserCode or math.random( 1000, 9999 )

  local RecceDcsUnit = self:GetDCSObject()
  local TargetVec3 = Target:GetVec3()

  self:F( "building spot" )
  self.Spot = SPOT:New( self ) -- Core.Spot#SPOT
  self.Spot:LaseOn( Target, LaserCode, Duration )
  self.LaserCode = LaserCode

  return self.Spot

end

--- Start Lasing a COORDINATE.
-- @param #POSITIONABLE self
-- @param Core.Point#COORDINATE Coordinate The coordinate where the lase is pointing at.
-- @param #number LaserCode Laser code or random number in [1000, 9999].
-- @param #number Duration Duration of lasing in seconds.
-- @return Core.Spot#SPOT
function POSITIONABLE:LaseCoordinate( Coordinate, LaserCode, Duration )
  self:F2()

  LaserCode = LaserCode or math.random( 1000, 9999 )

  self.Spot = SPOT:New( self ) -- Core.Spot#SPOT
  self.Spot:LaseOnCoordinate( Coordinate, LaserCode, Duration )
  self.LaserCode = LaserCode

  return self.Spot
end

--- Stop Lasing a POSITIONABLE.
-- @param #POSITIONABLE self
-- @return #POSITIONABLE
function POSITIONABLE:LaseOff()
  self:F2()

  if self.Spot then
    self.Spot:LaseOff()
    self.Spot = nil
  end

  return self
end

--- Check if the POSITIONABLE is lasing a target.
-- @param #POSITIONABLE self
-- @return #boolean true if it is lasing a target
function POSITIONABLE:IsLasing()
  self:F2()

  local Lasing = false

  if self.Spot then
    Lasing = self.Spot:IsLasing()
  end

  return Lasing
end

--- Get the Spot
-- @param #POSITIONABLE self
-- @return Core.Spot#SPOT The Spot
function POSITIONABLE:GetSpot()

  return self.Spot
end

--- Get the last assigned laser code
-- @param #POSITIONABLE self
-- @return #number The laser code
function POSITIONABLE:GetLaserCode()

  return self.LaserCode
end

do -- Cargo

  --- Add cargo.
  -- @param #POSITIONABLE self
  -- @param Cargo.Cargo#CARGO Cargo
  -- @return #POSITIONABLE
  function POSITIONABLE:AddCargo( Cargo )
    self.__.Cargo[Cargo] = Cargo
    return self
  end

  --- Get all contained cargo.
  -- @param #POSITIONABLE self
  -- @return #POSITIONABLE
  function POSITIONABLE:GetCargo()
    return self.__.Cargo
  end

  --- Remove cargo.
  -- @param #POSITIONABLE self
  -- @param Cargo.Cargo#CARGO Cargo
  -- @return #POSITIONABLE
  function POSITIONABLE:RemoveCargo( Cargo )
    self.__.Cargo[Cargo] = nil
    return self
  end

  --- Returns if carrier has given cargo.
  -- @param #POSITIONABLE self
  -- @return Cargo.Cargo#CARGO Cargo
  function POSITIONABLE:HasCargo( Cargo )
    return self.__.Cargo[Cargo]
  end

  --- Clear all cargo.
  -- @param #POSITIONABLE self
  function POSITIONABLE:ClearCargo()
    self.__.Cargo = {}
  end

  --- Is cargo bay empty.
  -- @param #POSITIONABLE self
  function POSITIONABLE:IsCargoEmpty()
    local IsEmpty = true
    for _, Cargo in pairs( self.__.Cargo ) do
      IsEmpty = false
      break
    end
    return IsEmpty
  end

  --- Get cargo item count.
  -- @param #POSITIONABLE self
  -- @return Cargo.Cargo#CARGO Cargo
  function POSITIONABLE:CargoItemCount()
    local ItemCount = 0
    for CargoName, Cargo in pairs( self.__.Cargo ) do
      ItemCount = ItemCount + Cargo:GetCount()
    end
    return ItemCount
  end

  --- Get the number of infantry soldiers that can be embarked into an aircraft (airplane or helicopter).
  -- Returns `nil` for ground or ship units.
  -- @param #POSITIONABLE self
  -- @return #number Descent number of soldiers that fit into the unit. Returns `#nil` for ground and ship units. 
  function POSITIONABLE:GetTroopCapacity()
    local DCSunit=self:GetDCSObject() --DCS#Unit
    local capacity=DCSunit:getDescentCapacity()
    return capacity
  end

  --- Get Cargo Bay Free Weight in kg.
  -- @param #POSITIONABLE self
  -- @return #number CargoBayFreeWeight
  function POSITIONABLE:GetCargoBayFreeWeight()

    -- When there is no cargo bay weight limit set, then calculate this for this POSITIONABLE!
    if not self.__.CargoBayWeightLimit then
      self:SetCargoBayWeightLimit()
    end

    local CargoWeight = 0
    for CargoName, Cargo in pairs( self.__.Cargo ) do
      CargoWeight = CargoWeight + Cargo:GetWeight()
    end
    return self.__.CargoBayWeightLimit - CargoWeight
  end
  
  ---
  -- @field DefaultInfantryWeight
  -- Abstract out the "XYZ*95" calculation in the Ground POSITIONABLE case, where the table 
  -- holds number of infantry that the vehicle could carry, instead of actual cargo weights.
  POSITIONABLE.DefaultInfantryWeight = 95

  ---
  -- @field CargoBayCapacityValues
  -- @field CargoBayCapacityValues.Air
  -- @field CargoBayCapacityValues.Naval
  -- @field CargoBayCapacityValues.Ground
  POSITIONABLE.CargoBayCapacityValues = {
    ["Air"] = {
      -- C-17A
      -- Wiki says: max=265,352, empty=128,140, payload=77,516 (134 troops, 1 M1 Abrams tank, 2 M2 Bradley or 3 Stryker)
      -- DCS  says: max=265,350, empty=125,645, fuel=132,405 ==> Cargo Bay=7300 kg with a full fuel load (lot of fuel!) and 73300 with half a fuel load.
      --["C-17A"] = 35000,   --77519 cannot be used, because it loads way too much apcs and infantry.
      -- C-130:
      -- DCS  says: max=79,380, empty=36,400, fuel=10,415 kg ==> Cargo Bay=32,565 kg with fuel load.
      -- Wiki says: max=70,307, empty=34,382, payload=19,000 kg (92 passengers, 2-3 Humvees or 2 M113s), max takeoff weight 70,037 kg.
      -- Here we say two M113s should be transported. Each one weights 11,253 kg according to DCS. So the cargo weight should be 23,000 kg with a full load of fuel.
      -- This results in a max takeoff weight of 69,815 kg (23,000+10,415+36,400), which is very close to the Wiki value of 70,037 kg.
      ["C_130"] = 70000,
    },
    ["Naval"] = {
      ["Type_071"]         =   245000,
      ["LHA_Tarawa"]       =   500000,
      ["Ropucha_class"]    =   150000,
      ["Dry_cargo_ship_1"] =    70000,
      ["Dry_cargo_ship_2"] =    70000,
      ["Higgins_boat"]     =     3700, -- Higgins Boat can load 3700 kg of general cargo or 36 men (source wikipedia).
      ["USS_Samuel_Chase"] =    25000, -- Let's say 25 tons for now. Wiki says 33 Higgins boats, which would be 264 tons (can't be right!) and/or 578 troops.
      ["LST_Mk2"]          =  2100000, -- Can carry 2100 tons according to wiki source!
      ["speedboat"]        =      500, -- 500 kg ~ 5 persons
      ["Seawise_Giant"]    =261000000, -- Gross tonnage is 261,000 tonns.
    },
    ["Ground"] = {
      ["AAV7"] = 25*POSITIONABLE.DefaultInfantryWeight,
      ["Bedford_MWD"] = 8*POSITIONABLE.DefaultInfantryWeight, -- new by kappa
      ["Blitz_36_6700A"] = 10*POSITIONABLE.DefaultInfantryWeight, -- new by kappa
      ["BMD_1"] = 9*POSITIONABLE.DefaultInfantryWeight, -- IRL should be 4 passengers
      ["BMP_1"] = 8*POSITIONABLE.DefaultInfantryWeight,
      ["BMP_2"] = 7*POSITIONABLE.DefaultInfantryWeight,
      ["BMP_3"] = 8*POSITIONABLE.DefaultInfantryWeight, -- IRL should be 7+2 passengers
      ["Boman"] = 25*POSITIONABLE.DefaultInfantryWeight,
      ["BTR_80"] = 9*POSITIONABLE.DefaultInfantryWeight, -- IRL should be 7 passengers
      ["BTR_82A"] = 9*POSITIONABLE.DefaultInfantryWeight, -- new by kappa -- IRL should be 7 passengers
      ["BTR_D"] = 12*POSITIONABLE.DefaultInfantryWeight, -- IRL should be 10 passengers
      ["Cobra"] = 8*POSITIONABLE.DefaultInfantryWeight,
      ["Land_Rover_101_FC"] = 11*POSITIONABLE.DefaultInfantryWeight, -- new by kappa
      ["Land_Rover_109_S3"] = 7*POSITIONABLE.DefaultInfantryWeight, -- new by kappa
      ["LAV_25"] = 6*POSITIONABLE.DefaultInfantryWeight,
      ["M_2_Bradley"] = 6*POSITIONABLE.DefaultInfantryWeight,
      ["M1043_HMMWV_Armament"] = 4*POSITIONABLE.DefaultInfantryWeight,
      ["M1045_HMMWV_TOW"] = 4*POSITIONABLE.DefaultInfantryWeight,
      ["M1126_Stryker_ICV"] = 9*POSITIONABLE.DefaultInfantryWeight,
      ["M1134_Stryker_ATGM"] = 9*POSITIONABLE.DefaultInfantryWeight,
      ["M2A1_halftrack"] = 9*POSITIONABLE.DefaultInfantryWeight,
      ["M_113"] = 9*POSITIONABLE.DefaultInfantryWeight, -- IRL should be 11 passengers
      ["Marder"] = 6*POSITIONABLE.DefaultInfantryWeight,
      ["MCV_80"] = 9*POSITIONABLE.DefaultInfantryWeight, -- IRL should be 7 passengers
      ["MLRS_FDDM"] = 4*POSITIONABLE.DefaultInfantryWeight,
      ["MTLB"] = 25*POSITIONABLE.DefaultInfantryWeight, -- IRL should be 11 passengers
      ["GAZ_66"] = 8*POSITIONABLE.DefaultInfantryWeight,
      ["GAZ_3307"] = 12*POSITIONABLE.DefaultInfantryWeight,
      ["GAZ_3308"] = 14*POSITIONABLE.DefaultInfantryWeight,
      ["Grad_FDDM"] = 6*POSITIONABLE.DefaultInfantryWeight, -- new by kappa
      ["KAMAZ_Truck"] = 12*POSITIONABLE.DefaultInfantryWeight,
      ["KrAZ6322"] = 12*POSITIONABLE.DefaultInfantryWeight,
      ["M_818"] = 12*POSITIONABLE.DefaultInfantryWeight,
      ["Tigr_233036"] = 6*POSITIONABLE.DefaultInfantryWeight,
      ["TPZ"] = 10*POSITIONABLE.DefaultInfantryWeight, -- Fuchs
      ["UAZ_469"] = 4*POSITIONABLE.DefaultInfantryWeight, -- new by kappa
      ["Ural_375"] = 12*POSITIONABLE.DefaultInfantryWeight,
      ["Ural_4320_31"] = 14*POSITIONABLE.DefaultInfantryWeight,
      ["Ural_4320_APA_5D"] = 10*POSITIONABLE.DefaultInfantryWeight,
      ["Ural_4320T"] = 14*POSITIONABLE.DefaultInfantryWeight,
      ["ZBD04A"] = 7*POSITIONABLE.DefaultInfantryWeight, -- new by kappa
      ["VAB_Mephisto"] = 8*POSITIONABLE.DefaultInfantryWeight, -- new by Apple
      ["tt_KORD"] = 6*POSITIONABLE.DefaultInfantryWeight, -- 2.7.1 HL/TT
      ["tt_DSHK"] = 6*POSITIONABLE.DefaultInfantryWeight,
      ["HL_KORD"] = 6*POSITIONABLE.DefaultInfantryWeight,
      ["HL_DSHK"] = 6*POSITIONABLE.DefaultInfantryWeight,
      ["CCKW_353"] = 16*POSITIONABLE.DefaultInfantryWeight, --GMC CCKW 2½-ton 6×6 truck, estimating 16 soldiers,
      ["MaxxPro_MRAP"] = 7*POSITIONABLE.DefaultInfantryWeight,
    }
  }

  --- Set Cargo Bay Weight Limit in kg.
  -- @param #POSITIONABLE self
  -- @param #number WeightLimit (Optional) Weight limit in kg. If not given, the value is taken from the descriptors or hard coded. 
  function POSITIONABLE:SetCargoBayWeightLimit( WeightLimit )

    if WeightLimit then 
      ---
      -- User defined value
      ---
      self.__.CargoBayWeightLimit = WeightLimit
    elseif self.__.CargoBayWeightLimit ~= nil then
      -- Value already set ==> Do nothing!
    else
      ---
      -- Weightlimit is not provided, we will calculate it depending on the type of unit.
      ---

      -- Descriptors that contain the type name and for aircraft also weights.
      local Desc = self:GetDesc()
      self:F({Desc=Desc})
      
      -- Unit type name.
      local TypeName=Desc.typeName or "Unknown Type"
      TypeName = string.gsub(TypeName,"[%p%s]","_")
      
      -- When an airplane or helicopter, we calculate the WeightLimit based on the descriptor.
      if self:IsAir() then

        -- Max takeoff weight if DCS descriptors have unrealstic values.
        local Weights = POSITIONABLE.CargoBayCapacityValues.Air
        
        -- Max (takeoff) weight (empty+fuel+cargo weight).
        local massMax= Desc.massMax or 0
        
        -- Adjust value if set above.
        local maxTakeoff=Weights[TypeName]
        if maxTakeoff then
          massMax=maxTakeoff
        end
        
        -- Empty weight.
        local massEmpty=Desc.massEmpty or 0
        
        -- Fuel. The descriptor provides the max fuel mass in kg. This needs to be multiplied by the relative fuel amount to calculate the actual fuel mass on board.
        local massFuelMax=Desc.fuelMassMax or 0
        local relFuel=math.min(self:GetFuel() or 1.0, 1.0)  -- We take 1.0 as max in case of external fuel tanks.
        local massFuel=massFuelMax*relFuel
        
        -- Number of soldiers according to DCS function 
        --local troopcapacity=self:GetTroopCapacity() or 0
        
        -- Calculate max cargo weight, which is the max (takeoff) weight minus the empty weight minus the actual fuel weight.
        local CargoWeight=massMax-(massEmpty+massFuel)
        
        -- Debug info.
        self:T(string.format("Setting Cargo bay weight limit [%s]=%d kg (Mass max=%d, empty=%d, fuelMax=%d kg (rel=%.3f), fuel=%d kg", TypeName, CargoWeight, massMax, massEmpty, massFuelMax, relFuel, massFuel))
        --self:T(string.format("Descent Troop Capacity=%d ==> %d kg (for 95 kg soldier)", troopcapacity, troopcapacity*95))        
        
        -- Set value.
        self.__.CargoBayWeightLimit = CargoWeight
        
      elseif self:IsShip() then

        -- Hard coded cargo weights in kg.
        local Weights = POSITIONABLE.CargoBayCapacityValues.Naval
        self.__.CargoBayWeightLimit = ( Weights[TypeName] or 50000 )

      else

        -- Hard coded number of soldiers.
        local Weights = POSITIONABLE.CargoBayCapacityValues.Ground

        -- Assuming that each passenger weighs 95 kg on average.
        local CargoBayWeightLimit = ( Weights[TypeName] or 0 )

        self.__.CargoBayWeightLimit = CargoBayWeightLimit
      end
    end

    self:F({CargoBayWeightLimit = self.__.CargoBayWeightLimit})
  end
  
  --- Get Cargo Bay Weight Limit in kg.
  -- @param #POSITIONABLE self
  -- @return #number Max cargo weight in kg. 
  function POSITIONABLE:GetCargoBayWeightLimit()
  
    if self.__.CargoBayWeightLimit==nil then
      self:SetCargoBayWeightLimit()
    end
  
    return self.__.CargoBayWeightLimit  
  end
  
end --- Cargo

--- Signal a flare at the position of the POSITIONABLE.
-- @param #POSITIONABLE self
-- @param Utilities.Utils#FLARECOLOR FlareColor
function POSITIONABLE:Flare( FlareColor )
  self:F2()
  trigger.action.signalFlare( self:GetVec3(), FlareColor, 0 )
end

--- Signal a white flare at the position of the POSITIONABLE.
-- @param #POSITIONABLE self
function POSITIONABLE:FlareWhite()
  self:F2()
  trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.White, 0 )
end

--- Signal a yellow flare at the position of the POSITIONABLE.
-- @param #POSITIONABLE self
function POSITIONABLE:FlareYellow()
  self:F2()
  trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Yellow, 0 )
end

--- Signal a green flare at the position of the POSITIONABLE.
-- @param #POSITIONABLE self
function POSITIONABLE:FlareGreen()
  self:F2()
  trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Green, 0 )
end

--- Signal a red flare at the position of the POSITIONABLE.
-- @param #POSITIONABLE self
function POSITIONABLE:FlareRed()
  self:F2()
  local Vec3 = self:GetVec3()
  if Vec3 then
    trigger.action.signalFlare( Vec3, trigger.flareColor.Red, 0 )
  end
end

--- Smoke the POSITIONABLE.
-- @param #POSITIONABLE self
-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color.
-- @param #number Range The range in meters to randomize the smoking around the POSITIONABLE.
-- @param #number AddHeight The height in meters to add to the altitude of the POSITIONABLE.
function POSITIONABLE:Smoke( SmokeColor, Range, AddHeight )
  self:F2()
  if Range then
    local Vec3 = self:GetRandomVec3( Range )
    Vec3.y = Vec3.y + AddHeight or 0
    trigger.action.smoke( Vec3, SmokeColor )
  else
    local Vec3 = self:GetVec3()
    Vec3.y = Vec3.y + AddHeight or 0
    trigger.action.smoke( self:GetVec3(), SmokeColor )
  end

end

--- Smoke the POSITIONABLE Green.
-- @param #POSITIONABLE self
function POSITIONABLE:SmokeGreen()
  self:F2()
  trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Green )
end

--- Smoke the POSITIONABLE Red.
-- @param #POSITIONABLE self
function POSITIONABLE:SmokeRed()
  self:F2()
  trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Red )
end

--- Smoke the POSITIONABLE White.
-- @param #POSITIONABLE self
function POSITIONABLE:SmokeWhite()
  self:F2()
  trigger.action.smoke( self:GetVec3(), trigger.smokeColor.White )
end

--- Smoke the POSITIONABLE Orange.
-- @param #POSITIONABLE self
function POSITIONABLE:SmokeOrange()
  self:F2()
  trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Orange )
end

--- Smoke the POSITIONABLE Blue.
-- @param #POSITIONABLE self
function POSITIONABLE:SmokeBlue()
  self:F2()
  trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Blue )
end

--- Returns true if the unit is within a @{Core.Zone}.
-- @param #POSITIONABLE self
-- @param Core.Zone#ZONE_BASE Zone The zone to test.
-- @return #boolean Returns true if the unit is within the @{Core.Zone#ZONE_BASE}
function POSITIONABLE:IsInZone( Zone )
  self:F2( { self.PositionableName, Zone } )

  if self:IsAlive() then
    local IsInZone = Zone:IsVec3InZone( self:GetVec3() )

    return IsInZone
  end
  return false
end

--- Returns true if the unit is not within a @{Core.Zone}.
-- @param #POSITIONABLE self
-- @param Core.Zone#ZONE_BASE Zone The zone to test.
-- @return #boolean Returns true if the unit is not within the @{Core.Zone#ZONE_BASE}
function POSITIONABLE:IsNotInZone( Zone )
  self:F2( { self.PositionableName, Zone } )

  if self:IsAlive() then
    local IsNotInZone = not Zone:IsVec3InZone( self:GetVec3() )

    return IsNotInZone
  else
    return false
  end
end
--- **Wrapper** - CONTROLLABLE is an intermediate class wrapping Group and Unit classes "controllers".
--
-- ===
--
-- ### Author: **FlightControl**
--
-- ### Contributions:
--
-- ===
--
-- @module Wrapper.Controllable
-- @image Wrapper_Controllable.JPG

--- @type CONTROLLABLE
-- @field DCS#Controllable DCSControllable The DCS controllable class.
-- @field #string ControllableName The name of the controllable.
-- @extends Wrapper.Positionable#POSITIONABLE

--- Wrapper class to handle the "DCS Controllable objects", which are Groups and Units:
--
--  * Support all DCS Controllable APIs.
--  * Enhance with Controllable specific APIs not in the DCS Controllable API set.
--  * Handle local Controllable Controller.
--  * Manage the "state" of the DCS Controllable.
--
-- # 1) CONTROLLABLE constructor
--
-- The CONTROLLABLE class provides the following functions to construct a CONTROLLABLE instance:
--
--  * @{#CONTROLLABLE.New}(): Create a CONTROLLABLE instance.
--
-- # 2) CONTROLLABLE Task methods
--
-- Several controllable task methods are available that help you to prepare tasks.
-- These methods return a string consisting of the task description, which can then be given to either a @{#CONTROLLABLE.PushTask}() or @{#CONTROLLABLE.SetTask}() method to assign the task to the CONTROLLABLE.
-- Tasks are specific for the category of the CONTROLLABLE, more specific, for AIR, GROUND or AIR and GROUND.
-- Each task description where applicable indicates for which controllable category the task is valid.
-- There are 2 main subdivisions of tasks: Assigned tasks and EnRoute tasks.
--
-- ## 2.1) Task assignment
--
-- Assigned task methods make the controllable execute the task where the location of the (possible) targets of the task are known before being detected.
-- This is different from the EnRoute tasks, where the targets of the task need to be detected before the task can be executed.
--
-- Find below a list of the **assigned task** methods:
--
--   * @{#CONTROLLABLE.TaskAttackGroup}: (AIR) Attack a Controllable.
--   * @{#CONTROLLABLE.TaskAttackMapObject}: (AIR) Attacking the map object (building, structure, e.t.c).
--   * @{#CONTROLLABLE.TaskAttackUnit}: (AIR) Attack the Unit.
--   * @{#CONTROLLABLE.TaskBombing}: (AIR) Delivering weapon at the point on the ground.
--   * @{#CONTROLLABLE.TaskBombingRunway}: (AIR) Delivering weapon on the runway.
--   * @{#CONTROLLABLE.TaskEmbarking}: (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable.
--   * @{#CONTROLLABLE.TaskEmbarkToTransport}: (GROUND) Embark to a Transport landed at a location.
--   * @{#CONTROLLABLE.TaskEscort}: (AIR) Escort another airborne controllable.
--   * @{#CONTROLLABLE.TaskGroundEscort}: (AIR/HELO) Escort a ground controllable.
--   * @{#CONTROLLABLE.TaskFAC_AttackGroup}: (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction.
--   * @{#CONTROLLABLE.TaskFireAtPoint}: (GROUND) Fire some or all ammunition at a VEC2 point.
--   * @{#CONTROLLABLE.TaskFollow}: (AIR) Following another airborne controllable.
--   * @{#CONTROLLABLE.TaskHold}: (GROUND) Hold ground controllable from moving.
--   * @{#CONTROLLABLE.TaskHoldPosition}: (AIR) Hold position at the current position of the first unit of the controllable.
--   * @{#CONTROLLABLE.TaskLandAtVec2}: (AIR HELICOPTER) Landing at the ground. For helicopters only.
--   * @{#CONTROLLABLE.TaskLandAtZone}: (AIR) Land the controllable at a @{Core.Zone#ZONE_RADIUS).
--   * @{#CONTROLLABLE.TaskOrbitCircle}: (AIR) Orbit at the current position of the first unit of the controllable at a specified altitude.
--   * @{#CONTROLLABLE.TaskOrbitCircleAtVec2}: (AIR) Orbit at a specified position at a specified altitude during a specified duration with a specified speed.
--   * @{#CONTROLLABLE.TaskStrafing}: (AIR) Strafe a point Vec2 with onboard weapons.
--   * @{#CONTROLLABLE.TaskRefueling}: (AIR) Refueling from the nearest tanker. No parameters.
--   * @{#CONTROLLABLE.TaskRecoveryTanker}: (AIR) Set  group to act as recovery tanker for a naval group.
--   * @{#CONTROLLABLE.TaskRoute}: (AIR + GROUND) Return a Mission task to follow a given route defined by Points.
--   * @{#CONTROLLABLE.TaskRouteToVec2}: (AIR + GROUND) Make the Controllable move to a given point.
--   * @{#CONTROLLABLE.TaskRouteToVec3}: (AIR + GROUND) Make the Controllable move to a given point.
--   * @{#CONTROLLABLE.TaskRouteToZone}: (AIR + GROUND) Route the controllable to a given zone.
--
-- ## 2.2) EnRoute assignment
--
-- EnRoute tasks require the targets of the task need to be detected by the controllable (using its sensors) before the task can be executed:
--
--   * @{#CONTROLLABLE.EnRouteTaskAWACS}: (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters.
--   * @{#CONTROLLABLE.EnRouteTaskEngageControllable}: (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now; it just allows the unit/controllable to engage the target controllable as well as other assigned targets.
--   * @{#CONTROLLABLE.EnRouteTaskEngageTargets}: (AIR) Engaging targets of defined types.
--   * @{#CONTROLLABLE.EnRouteTaskEngageTargetsInZone}: (AIR) Engaging a targets of defined types at circle-shaped zone.
--   * @{#CONTROLLABLE.EnRouteTaskEWR}: (AIR) Attack the Unit.
--   * @{#CONTROLLABLE.EnRouteTaskFAC}: (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets.
--   * @{#CONTROLLABLE.EnRouteTaskFAC_EngageControllable}: (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets.
--   * @{#CONTROLLABLE.EnRouteTaskTanker}: (AIR) Aircraft will act as a tanker for friendly units. No parameters.
--
-- ## 2.3) Task preparation
--
-- There are certain task methods that allow to tailor the task behavior:
--
--   * @{#CONTROLLABLE.TaskWrappedAction}: Return a WrappedAction Task taking a Command.
--   * @{#CONTROLLABLE.TaskCombo}: Return a Combo Task taking an array of Tasks.
--   * @{#CONTROLLABLE.TaskCondition}: Return a condition section for a controlled task.
--   * @{#CONTROLLABLE.TaskControlled}: Return a Controlled Task taking a Task and a TaskCondition.
--
-- ## 2.4) Call a function as a Task
--
-- A function can be called which is part of a Task. The method @{#CONTROLLABLE.TaskFunction}() prepares
-- a Task that can call a GLOBAL function from within the Controller execution.
-- This method can also be used to **embed a function call when a certain waypoint has been reached**.
-- See below the **Tasks at Waypoints** section.
--
-- Demonstration Mission: [GRP-502 - Route at waypoint to random point](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/Wrapper/Group/502-Route-at-waypoint-to-random-point)
--
-- ## 2.5) Tasks at Waypoints
--
-- Special Task methods are available to set tasks at certain waypoints.
-- The method @{#CONTROLLABLE.SetTaskWaypoint}() helps preparing a Route, embedding a Task at the Waypoint of the Route.
--
-- This creates a Task element, with an action to call a function as part of a Wrapped Task.
--
-- ## 2.6) Obtain the mission from controllable templates
--
-- Controllable templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a controllable and assign it to another:
--
--   * @{#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template.
--
-- # 3) Command methods
--
-- Controllable **command methods** prepare the execution of commands using the @{#CONTROLLABLE.SetCommand} method:
--
--   * @{#CONTROLLABLE.CommandDoScript}: Do Script command.
--   * @{#CONTROLLABLE.CommandSwitchWayPoint}: Perform a switch waypoint command.
--
-- # 4) Routing of Controllables
--
-- Different routing methods exist to route GROUPs and UNITs to different locations:
--
--   * @{#CONTROLLABLE.Route}(): Make the Controllable to follow a given route.
--   * @{#CONTROLLABLE.RouteGroundTo}(): Make the GROUND Controllable to drive towards a specific coordinate.
--   * @{#CONTROLLABLE.RouteAirTo}(): Make the AIR Controllable to fly towards a specific coordinate.
--   * @{#CONTROLLABLE.RelocateGroundRandomInRadius}(): Relocate the GROUND controllable to a random point in a given radius.
--
-- # 5) Option methods
--
-- Controllable **Option methods** change the behavior of the Controllable while being alive.
--
-- ## 5.1) Rule of Engagement:
--
--   * @{#CONTROLLABLE.OptionROEWeaponFree}
--   * @{#CONTROLLABLE.OptionROEOpenFire}
--   * @{#CONTROLLABLE.OptionROEReturnFire}
--   * @{#CONTROLLABLE.OptionROEEvadeFire}
--
-- To check whether an ROE option is valid for a specific controllable, use:
--
--   * @{#CONTROLLABLE.OptionROEWeaponFreePossible}
--   * @{#CONTROLLABLE.OptionROEOpenFirePossible}
--   * @{#CONTROLLABLE.OptionROEReturnFirePossible}
--   * @{#CONTROLLABLE.OptionROEEvadeFirePossible}
--
-- ## 5.2) Reaction On Thread:
--
--   * @{#CONTROLLABLE.OptionROTNoReaction}
--   * @{#CONTROLLABLE.OptionROTPassiveDefense}
--   * @{#CONTROLLABLE.OptionROTEvadeFire}
--   * @{#CONTROLLABLE.OptionROTVertical}
--
-- To test whether an ROT option is valid for a specific controllable, use:
--
--   * @{#CONTROLLABLE.OptionROTNoReactionPossible}
--   * @{#CONTROLLABLE.OptionROTPassiveDefensePossible}
--   * @{#CONTROLLABLE.OptionROTEvadeFirePossible}
--   * @{#CONTROLLABLE.OptionROTVerticalPossible}
--
-- ## 5.3) Alarm state:
--
--   * @{#CONTROLLABLE.OptionAlarmStateAuto}
--   * @{#CONTROLLABLE.OptionAlarmStateGreen}
--   * @{#CONTROLLABLE.OptionAlarmStateRed}
--
-- ## 5.4) [AIR] Jettison weapons:
--
--   * @{#CONTROLLABLE.OptionAllowJettisonWeaponsOnThreat}
--   * @{#CONTROLLABLE.OptionKeepWeaponsOnThreat}
--
-- ## 5.5) [AIR] Air-2-Air missile attack range:
--   * @{#CONTROLLABLE.OptionAAAttackRange}(): Defines the usage of A2A missiles against possible targets.
--   
-- # 6) [GROUND] IR Maker Beacons for GROUPs and UNITs
--  * @{#CONTROLLABLE:NewIRMarker}(): Create a blinking IR Marker on a GROUP or UNIT.
--  
-- # 7) [HELICOPTER] Units prefer vertical landing and takeoffs:
--  * @{#CONTROLLABLE.OptionPreferVerticalLanding}(): Set aircraft to prefer vertical landing and takeoff.
--  
-- # 8) [AIRCRAFT] Landing approach options
--  * @{#CONTROLLABLE.SetOptionLandingStraightIn}(): Landing approach straight in.
--  * @{#CONTROLLABLE.SetOptionLandingForcePair}(): Landing approach in pairs for groups > 1 unit.
--  * @{#CONTROLLABLE.SetOptionLandingRestrictPair}(): Landing approach single.
--  * @{#CONTROLLABLE.SetOptionLandingOverheadBreak}():  Landing approach overhead break.
--
-- @field #CONTROLLABLE
CONTROLLABLE = {
  ClassName = "CONTROLLABLE",
  ControllableName = "",
  WayPointFunctions = {},
}

--- Create a new CONTROLLABLE from a DCSControllable
-- @param #CONTROLLABLE self
-- @param #string ControllableName The DCS Controllable name
-- @return #CONTROLLABLE self
function CONTROLLABLE:New( ControllableName )
  local self = BASE:Inherit( self, POSITIONABLE:New( ControllableName ) ) -- #CONTROLLABLE
  -- self:F( ControllableName )
  self.ControllableName = ControllableName

  self.TaskScheduler = SCHEDULER:New( self )
  return self
end

-- DCS Controllable methods support.

--- Get the controller for the CONTROLLABLE.
-- @param #CONTROLLABLE self
-- @return DCS#Controller
function CONTROLLABLE:_GetController()
  local DCSControllable = self:GetDCSObject()

  if DCSControllable then
    local ControllableController = DCSControllable:getController()
    return ControllableController
  end

  return nil
end

-- Get methods

--- Returns the health. Dead controllables have health <= 1.0.
-- @param #CONTROLLABLE self
-- @return #number The controllable health value (unit or group average).
-- @return #nil The controllable is not existing or alive.
function CONTROLLABLE:GetLife()
  self:F2( self.ControllableName )

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then
    local UnitLife = 0
    local Units = self:GetUnits()
    if #Units == 1 then
      local Unit = Units[1] -- Wrapper.Unit#UNIT
      UnitLife = Unit:GetLife()
    else
      local UnitLifeTotal = 0
      for UnitID, Unit in pairs( Units ) do
        local Unit = Unit -- Wrapper.Unit#UNIT
        UnitLifeTotal = UnitLifeTotal + Unit:GetLife()
      end
      UnitLife = UnitLifeTotal / #Units
    end
    return UnitLife
  end

  return nil
end

--- Returns the initial health.
-- @param #CONTROLLABLE self
-- @return #number The controllable health value (unit or group average) or `nil` if the controllable does not exist.
function CONTROLLABLE:GetLife0()
  self:F2( self.ControllableName )

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then
    local UnitLife = 0
    local Units = self:GetUnits()
    if #Units == 1 then
      local Unit = Units[1] -- Wrapper.Unit#UNIT
      UnitLife = Unit:GetLife0()
    else
      local UnitLifeTotal = 0
      for UnitID, Unit in pairs( Units ) do
        local Unit = Unit -- Wrapper.Unit#UNIT
        UnitLifeTotal = UnitLifeTotal + Unit:GetLife0()
      end
      UnitLife = UnitLifeTotal / #Units
    end
    return UnitLife
  end

  return nil
end

--- Returns relative minimum amount of fuel (from 0.0 to 1.0) a unit or group has in its internal tanks.
-- This method returns nil to ensure polymorphic behavior! This method needs to be overridden by GROUP or UNIT.
-- @param #CONTROLLABLE self
-- @return #nil The CONTROLLABLE is not existing or alive.
function CONTROLLABLE:GetFuelMin()
  self:F( self.ControllableName )

  return nil
end

--- Returns relative average amount of fuel (from 0.0 to 1.0) a unit or group has in its internal tanks.
-- This method returns nil to ensure polymorphic behavior! This method needs to be overridden by GROUP or UNIT.
-- @param #CONTROLLABLE self
-- @return #nil The CONTROLLABLE is not existing or alive.
function CONTROLLABLE:GetFuelAve()
  self:F( self.ControllableName )

  return nil
end

--- Returns relative amount of fuel (from 0.0 to 1.0) the unit has in its internal tanks.
-- This method returns nil to ensure polymorphic behavior! This method needs to be overridden by GROUP or UNIT.
-- @param #CONTROLLABLE self
-- @return #nil The CONTROLLABLE is not existing or alive.
function CONTROLLABLE:GetFuel()
  self:F( self.ControllableName )
  return nil
end

-- Tasks

--- Clear all tasks from the controllable.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE
function CONTROLLABLE:ClearTasks()

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then
    local Controller = self:_GetController()
    Controller:resetTask()
    return self
  end

  return nil
end

--- Popping current Task from the controllable.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:PopCurrentTask()
  self:F2()

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then
    local Controller = self:_GetController()
    Controller:popTask()
    return self
  end

  return nil
end

--- Pushing Task on the queue from the controllable.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:PushTask( DCSTask, WaitTime )
  self:F2()

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then

    local DCSControllableName = self:GetName()

    -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results.
    -- Therefore we schedule the functions to set the mission and options for the Controllable.
    -- Controller:pushTask( DCSTask )

    local function PushTask( Controller, DCSTask )
      if self and self:IsAlive() then
        local Controller = self:_GetController()
        Controller:pushTask( DCSTask )
      else
        BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } )
      end
    end

    if not WaitTime or WaitTime == 0 then
      PushTask( self, DCSTask )
    else
      self.TaskScheduler:Schedule( self, PushTask, { DCSTask }, WaitTime )
    end

    return self
  end

  return nil
end

--- Clearing the Task Queue and Setting the Task on the queue from the controllable.
-- @param #CONTROLLABLE self
-- @param DCS#Task DCSTask DCS Task array.
-- @param #number WaitTime Time in seconds, before the task is set.
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetTask( DCSTask, WaitTime )
  self:F( { "SetTask", WaitTime, DCSTask = DCSTask } )

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then

    local DCSControllableName = self:GetName()

    self:T2( "Controllable Name = " .. DCSControllableName )

    -- When a controllable SPAWNs, it takes about a second to get the controllable in the simulator. Setting tasks to unspawned controllables provides unexpected results.
    -- Therefore we schedule the functions to set the mission and options for the Controllable.
    -- Controller.setTask( Controller, DCSTask )

    local function SetTask( Controller, DCSTask )
      if self and self:IsAlive() then
        local Controller = self:_GetController()
        -- self:I( "Before SetTask" )
        Controller:setTask( DCSTask )
        -- AI_FORMATION class (used by RESCUEHELO) calls SetTask twice per second! hence spamming the DCS log file ==> setting this to trace.
        self:T( { ControllableName = self:GetName(), DCSTask = DCSTask } )
      else
        BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } )
      end
    end

    if not WaitTime or WaitTime == 0 then
      SetTask( self, DCSTask )
      -- See above.
      self:T( { ControllableName = self:GetName(), DCSTask = DCSTask } )
    else
      self.TaskScheduler:Schedule( self, SetTask, { DCSTask }, WaitTime )
    end

    return self
  end

  return nil
end

--- Checking the Task Queue of the controllable. Returns false if no task is on the queue. true if there is a task.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:HasTask() -- R2.2

  local HasTaskResult = false

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then

    local Controller = self:_GetController()
    HasTaskResult = Controller:hasTask()
  end

  return HasTaskResult
end

--- Return a condition section for a controlled task.
-- @param #CONTROLLABLE self
-- @param DCS#Time time DCS mission time.
-- @param #string userFlag Name of the user flag.
-- @param #boolean userFlagValue User flag value *true* or *false*. Could also be numeric, i.e. either 0=*false* or 1=*true*. Other numeric values don't work!
-- @param #string condition Lua string.
-- @param DCS#Time duration Duration in seconds.
-- @param #number lastWayPoint Last waypoint.
-- return DCS#Task
function CONTROLLABLE:TaskCondition( time, userFlag, userFlagValue, condition, duration, lastWayPoint )

  --[[
 StopCondition = {
   time = Time,
   userFlag = string,
   userFlagValue = boolean,
   condition = string,
   duration = Time,
   lastWaypoint = number,
 }
--]]

  local DCSStopCondition = {}
  DCSStopCondition.time = time
  DCSStopCondition.userFlag = userFlag
  DCSStopCondition.userFlagValue = userFlagValue
  DCSStopCondition.condition = condition
  DCSStopCondition.duration = duration
  DCSStopCondition.lastWayPoint = lastWayPoint

  return DCSStopCondition
end

--- Return a Controlled Task taking a Task and a TaskCondition.
-- @param #CONTROLLABLE self
-- @param DCS#Task DCSTask
-- @param DCS#DCSStopCondition DCSStopCondition
-- @return DCS#Task
function CONTROLLABLE:TaskControlled( DCSTask, DCSStopCondition )

  local DCSTaskControlled = {
    id = 'ControlledTask',
    params = {
      task = DCSTask,
      stopCondition = DCSStopCondition,
    },
  }

  return DCSTaskControlled
end

--- Return a Combo Task taking an array of Tasks.
-- @param #CONTROLLABLE self
-- @param DCS#TaskArray DCSTasks Array of DCSTasking.Task#Task
-- @return DCS#Task
function CONTROLLABLE:TaskCombo( DCSTasks )

  local DCSTaskCombo = {
    id = 'ComboTask',
    params = {
      tasks = DCSTasks,
    },
  }

  return DCSTaskCombo
end

--- Return a WrappedAction Task taking a Command.
-- @param #CONTROLLABLE self
-- @param DCS#Command DCSCommand
-- @return DCS#Task
function CONTROLLABLE:TaskWrappedAction( DCSCommand, Index )

  local DCSTaskWrappedAction = {
    id = "WrappedAction",
    enabled = true,
    number = Index or 1,
    auto = false,
    params = {
      action = DCSCommand,
    },
  }

  return DCSTaskWrappedAction
end

--- Return an Empty Task.
-- @param #CONTROLLABLE self
-- @return DCS#Task
function CONTROLLABLE:TaskEmptyTask()

  local DCSTaskWrappedAction = {
            ["id"] = "WrappedAction",
            ["params"] = {
              ["action"] = {
                ["id"] = "Script",
                ["params"] = {
                  ["command"] = "",
                },
              },
            },
          }

  return DCSTaskWrappedAction
end

--- Set a Task at a Waypoint using a Route list.
-- @param #CONTROLLABLE self
-- @param #table Waypoint The Waypoint!
-- @param DCS#Task Task The Task structure to be executed!
-- @return DCS#Task
function CONTROLLABLE:SetTaskWaypoint( Waypoint, Task )

  Waypoint.task = self:TaskCombo( { Task } )

  self:F( { Waypoint.task } )
  return Waypoint.task
end

--- Executes a command action for the CONTROLLABLE.
-- @param #CONTROLLABLE self
-- @param DCS#Command DCSCommand The command to be executed.
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetCommand( DCSCommand )
  self:F2( DCSCommand )

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then
    local Controller = self:_GetController()
    Controller:setCommand( DCSCommand )
    return self
  end

  return nil
end

--- Perform a switch waypoint command
-- @param #CONTROLLABLE self
-- @param #number FromWayPoint
-- @param #number ToWayPoint
-- @return DCS#Task
-- @usage
--
--   -- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class.
--   HeliGroup = GROUP:FindByName( "Helicopter" )
--
--   -- Route the helicopter back to the FARP after 60 seconds.
--   -- We use the SCHEDULER class to do this.
--   SCHEDULER:New( nil,
--     function( HeliGroup )
--       local CommandRTB = HeliGroup:CommandSwitchWayPoint( 2, 8 )
--       HeliGroup:SetCommand( CommandRTB )
--     end, { HeliGroup }, 90
--   )
function CONTROLLABLE:CommandSwitchWayPoint( FromWayPoint, ToWayPoint )
  self:F2( { FromWayPoint, ToWayPoint } )

  local CommandSwitchWayPoint = {
    id = 'SwitchWaypoint',
    params = {
      fromWaypointIndex = FromWayPoint,
      goToWaypointIndex = ToWayPoint,
    },
  }

  self:T3( { CommandSwitchWayPoint } )
  return CommandSwitchWayPoint
end

--- Create a stop route command, which returns a string containing the command.
-- Use the result in the method @{#CONTROLLABLE.SetCommand}().
-- A value of true will make the ground group stop, a value of false will make it continue.
-- Note that this can only work on GROUP level, although individual UNITs can be commanded, the whole GROUP will react.
--
-- Example missions:
--
--   * GRP-310
--
-- @param #CONTROLLABLE self
-- @param #boolean StopRoute true if the ground unit needs to stop, false if it needs to continue to move.
-- @return DCS#Task
function CONTROLLABLE:CommandStopRoute( StopRoute )
  self:F2( { StopRoute } )

  local CommandStopRoute = {
    id = 'StopRoute',
    params = {
      value = StopRoute,
    },
  }

  self:T3( { CommandStopRoute } )
  return CommandStopRoute
end

--- Give an uncontrolled air controllable the start command.
-- @param #CONTROLLABLE self
-- @param #number delay (Optional) Delay before start command in seconds.
-- @return #CONTROLLABLE self
function CONTROLLABLE:StartUncontrolled( delay )
  if delay and delay > 0 then
    SCHEDULER:New( nil, CONTROLLABLE.StartUncontrolled, { self }, delay )
  else
    self:SetCommand( { id = 'Start', params = {} } )
  end
  return self
end

--- Give the CONTROLLABLE the command to activate a beacon. See [DCS_command_activateBeacon](https://wiki.hoggitworld.com/view/DCS_command_activateBeacon) on Hoggit.
-- For specific beacons like TACAN use the more convenient @{#BEACON} class.
-- Note that a controllable can only have one beacon activated at a time with the execption of ICLS.
-- @param #CONTROLLABLE self
-- @param Core.Beacon#BEACON.Type Type Beacon type (VOR, DME, TACAN, RSBN, ILS etc).
-- @param Core.Beacon#BEACON.System System Beacon system (VOR, DME, TACAN, RSBN, ILS etc).
-- @param #number Frequency Frequency in Hz the beacon is running on. Use @{#UTILS.TACANToFrequency} to generate a frequency for TACAN beacons.
-- @param #number UnitID The ID of the unit the beacon is attached to. Useful if more units are in one group.
-- @param #number Channel Channel the beacon is using. For, e.g. TACAN beacons.
-- @param #string ModeChannel The TACAN mode of the beacon, i.e. "X" or "Y".
-- @param #boolean AA If true, create and Air-Air beacon. IF nil, automatically set if CONTROLLABLE depending on whether unit is and aircraft or not.
-- @param #string Callsign Morse code identification callsign.
-- @param #boolean Bearing If true, beacon provides bearing information - if supported by the unit the beacon is attached to.
-- @param #number Delay (Optional) Delay in seconds before the beacon is activated.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandActivateBeacon( Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing, Delay )

  AA = AA or self:IsAir()
  UnitID = UnitID or self:GetID()

  -- Command
  local CommandActivateBeacon = {
    id = "ActivateBeacon",
    params = {
      ["type"] = Type,
      ["system"] = System,
      ["frequency"] = Frequency,
      ["unitId"] = UnitID,
      ["channel"] = Channel,
      ["modeChannel"] = ModeChannel,
      ["AA"] = AA,
      ["callsign"] = Callsign,
      ["bearing"] = Bearing,
    },
  }

  if Delay and Delay > 0 then
    SCHEDULER:New( nil, self.CommandActivateBeacon, { self, Type, System, Frequency, UnitID, Channel, ModeChannel, AA, Callsign, Bearing }, Delay )
  else
    self:SetCommand( CommandActivateBeacon )
  end

  return self
end

--- Activate ACLS system of the CONTROLLABLE. The controllable should be an aircraft carrier! Also needs Link4 to work.
-- @param #CONTROLLABLE self
-- @param #number UnitID (Optional) The DCS UNIT ID of the unit the ACLS system is attached to. Defaults to the UNIT itself.
-- @param #string Name (Optional) Name of the ACLS Beacon
-- @param #number Delay (Optional) Delay in seconds before the ICLS is activated.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandActivateACLS( UnitID, Name, Delay )

  -- Command to activate ACLS system.
  local CommandActivateACLS= {
  id = 'ActivateACLS',
  params = {
    unitId = UnitID or self:GetID(),
    name = Name or "ACL",
  }
}

  self:T({CommandActivateACLS})

  if Delay and Delay > 0 then
    SCHEDULER:New( nil, self.CommandActivateACLS, { self, UnitID, Name }, Delay )
  else
    local controller = self:_GetController()
    controller:setCommand( CommandActivateACLS )
  end

  return self
end

--- Deactivate ACLS system of the CONTROLLABLE. The controllable should be an aircraft carrier!
-- @param #CONTROLLABLE self
-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandDeactivateACLS( Delay )

  -- Command to activate ACLS system.
  local CommandDeactivateACLS= {
  id = 'DeactivateACLS',
  params = { }
}

  if Delay and Delay > 0 then
    SCHEDULER:New( nil, self.CommandDeactivateACLS, { self }, Delay )
  else
    local controller = self:_GetController()
    controller:setCommand( CommandDeactivateACLS )
  end

  return self
end

--- Activate ICLS system of the CONTROLLABLE. The controllable should be an aircraft carrier!
-- @param #CONTROLLABLE self
-- @param #number Channel ICLS channel.
-- @param #number UnitID The DCS UNIT ID of the unit the ICLS system is attached to. Useful if more units are in one group.
-- @param #string Callsign Morse code identification callsign.
-- @param #number Delay (Optional) Delay in seconds before the ICLS is activated.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandActivateICLS( Channel, UnitID, Callsign, Delay )

  -- Command to activate ICLS system.
  local CommandActivateICLS = {
    id = "ActivateICLS",
    params = {
      ["type"] = BEACON.Type.ICLS,
      ["channel"] = Channel,
      ["unitId"] = UnitID or self:GetID(),
      ["callsign"] = Callsign,
    },
  }

  if Delay and Delay > 0 then
    SCHEDULER:New( nil, self.CommandActivateICLS, { self, Channel, UnitID, Callsign }, Delay )
  else
    self:SetCommand( CommandActivateICLS )
  end

  return self
end

--- Activate LINK4 system of the CONTROLLABLE. The controllable should be an aircraft carrier!
-- @param #CONTROLLABLE self
-- @param #number Frequency Link4 Frequency in MHz, e.g. 336 (defaults to 336 MHz)
-- @param #number UnitID (Optional) The DCS UNIT ID of the unit the LINK4 system is attached to. Defaults to the UNIT itself.
-- @param #string Callsign (Optional) Morse code identification callsign.
-- @param #number Delay (Optional) Delay in seconds before the LINK4 is activated.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandActivateLink4(Frequency, UnitID, Callsign, Delay)

  local freq = Frequency or 336

  -- Command to activate Link4 system.
  local CommandActivateLink4= {
    id = "ActivateLink4",
    params= {
      ["frequency"] = freq*1000000,
      ["unitId"] = UnitID or self:GetID(),
      ["name"] = Callsign or "LNK",
    }
  }

  self:T({CommandActivateLink4})

  if Delay and Delay>0 then
    SCHEDULER:New(nil, self.CommandActivateLink4, {self, Frequency, UnitID, Callsign}, Delay)
  else
    local controller = self:_GetController()
    controller:setCommand(CommandActivateLink4)
  end

  return self
end

--- Deactivate the active beacon of the CONTROLLABLE.
-- @param #CONTROLLABLE self
-- @param #number Delay (Optional) Delay in seconds before the beacon is deactivated.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandDeactivateBeacon( Delay )

  -- Command to deactivate
  local CommandDeactivateBeacon = { id = 'DeactivateBeacon', params = {} }

  local CommandDeactivateBeacon={id='DeactivateBeacon', params={}}

  if Delay and Delay>0 then
    SCHEDULER:New(nil, self.CommandDeactivateBeacon, {self}, Delay)
  else
    self:SetCommand( CommandDeactivateBeacon )
  end

  return self
end

--- Deactivate the active Link4 of the CONTROLLABLE.
-- @param #CONTROLLABLE self
-- @param #number Delay (Optional) Delay in seconds before the Link4 is deactivated.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandDeactivateLink4(Delay)

  -- Command to deactivate
  local CommandDeactivateLink4={id='DeactivateLink4', params={}}

  if Delay and Delay>0 then
    SCHEDULER:New(nil, self.CommandDeactivateLink4, {self}, Delay)
  else
    local controller = self:_GetController()
    controller:setCommand(CommandDeactivateLink4)
  end

  return self
end

--- Deactivate the ICLS of the CONTROLLABLE.
-- @param #CONTROLLABLE self
-- @param #number Delay (Optional) Delay in seconds before the ICLS is deactivated.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandDeactivateICLS( Delay )

  -- Command to deactivate
  local CommandDeactivateICLS = { id = 'DeactivateICLS', params = {} }

  if Delay and Delay > 0 then
    SCHEDULER:New( nil, self.CommandDeactivateICLS, { self }, Delay )
  else
    self:SetCommand( CommandDeactivateICLS )
  end

  return self
end

--- Set callsign of the CONTROLLABLE. See [DCS command setCallsign](https://wiki.hoggitworld.com/view/DCS_command_setCallsign)
-- @param #CONTROLLABLE self
-- @param DCS#CALLSIGN CallName Number corresponding the the callsign identifier you wish this group to be called.
-- @param #number CallNumber The number value the group will be referred to as. Only valid numbers are 1-9. For example Uzi **5**-1. Default 1.
-- @param #number Delay (Optional) Delay in seconds before the callsign is set. Default is immediately.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandSetCallsign( CallName, CallNumber, Delay )

  -- Command to set the callsign.
  local CommandSetCallsign = { id = 'SetCallsign', params = { callname = CallName, number = CallNumber or 1 } }

  if Delay and Delay > 0 then
    SCHEDULER:New( nil, self.CommandSetCallsign, { self, CallName, CallNumber }, Delay )
  else
    self:SetCommand( CommandSetCallsign )
  end

  return self
end

--- Set EPLRS of the CONTROLLABLE on/off. See [DCS command EPLRS](https://wiki.hoggitworld.com/view/DCS_command_eplrs)
-- @param #CONTROLLABLE self
-- @param #boolean SwitchOnOff If true (or nil) switch EPLRS on. If false switch off.
-- @param #number Delay (Optional) Delay in seconds before the callsign is set. Default is immediately.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandEPLRS( SwitchOnOff, Delay )

  if SwitchOnOff == nil then
    SwitchOnOff = true
  end

  -- Command to set the callsign.
  local CommandEPLRS = {
    id = 'EPLRS',
    params = {
      value = SwitchOnOff,
      groupId = self:GetID(),
    },
  }
  
  --if self:IsGround() then
   --CommandEPLRS.params.groupId = self:GetID()
  --end
  
  if Delay and Delay > 0 then
    SCHEDULER:New( nil, self.CommandEPLRS, { self, SwitchOnOff }, Delay )
  else
    self:T( string.format( "EPLRS=%s for controllable %s (id=%s)", tostring( SwitchOnOff ), tostring( self:GetName() ), tostring( self:GetID() ) ) )
    self:SetCommand( CommandEPLRS )
  end

  return self
end

--- Set unlimited fuel. See [DCS command Unlimited Fuel](https://wiki.hoggitworld.com/view/DCS_command_setUnlimitedFuel).
-- @param #CONTROLLABLE self
-- @param #boolean OnOff Set unlimited fuel on = true or off = false.
-- @param #number Delay (Optional) Set the option only after x seconds.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandSetUnlimitedFuel(OnOff, Delay)

  local CommandSetFuel = {
    id = 'SetUnlimitedFuel',
    params = {
    value = OnOff
  }
}

  if Delay and Delay > 0 then
    SCHEDULER:New( nil, self.CommandSetUnlimitedFuel, { self, OnOff }, Delay )
  else
    self:SetCommand( CommandSetFuel )
  end

  return self
end


--- Set radio frequency. See [DCS command SetFrequency](https://wiki.hoggitworld.com/view/DCS_command_setFrequency)
-- @param #CONTROLLABLE self
-- @param #number Frequency Radio frequency in MHz.
-- @param #number Modulation Radio modulation. Default `radio.modulation.AM`.
-- @param #number Power (Optional) Power of the Radio in Watts. Defaults to 10.
-- @param #number Delay (Optional) Delay in seconds before the frequency is set. Default is immediately.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandSetFrequency( Frequency, Modulation, Power, Delay )

  local CommandSetFrequency = {
    id = 'SetFrequency',
    params = {
      frequency = Frequency * 1000000,
      modulation = Modulation or radio.modulation.AM,
      power=Power or 10,
    },
  }

  if Delay and Delay > 0 then
    SCHEDULER:New( nil, self.CommandSetFrequency, { self, Frequency, Modulation, Power },Delay )
  else
    self:SetCommand( CommandSetFrequency )
  end

  return self
end

--- [AIR] Set radio frequency. See [DCS command SetFrequencyForUnit](https://wiki.hoggitworld.com/view/DCS_command_setFrequencyForUnit)
-- @param #CONTROLLABLE self
-- @param #number Frequency Radio frequency in MHz.
-- @param #number Modulation Radio modulation. Default `radio.modulation.AM`.
-- @param #number Power (Optional) Power of the Radio in Watts. Defaults to 10.
-- @param #number UnitID (Optional, if your object is a UNIT) The UNIT ID this is for.
-- @param #number Delay (Optional) Delay in seconds before the frequency is set. Default is immediately.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandSetFrequencyForUnit(Frequency,Modulation,Power,UnitID,Delay)
  local CommandSetFrequencyForUnit={
    id='SetFrequencyForUnit',
    params={
        frequency=Frequency*1000000,
        modulation=Modulation or radio.modulation.AM,
        unitId=UnitID or self:GetID(),
        power=Power or 10,
    },
  }
  if Delay and Delay>0 then
    SCHEDULER:New(nil,self.CommandSetFrequencyForUnit,{self,Frequency,Modulation,Power,UnitID},Delay)
  else
    self:SetCommand(CommandSetFrequencyForUnit)
  end
  return self
end

--- [AIR] Set smoke on or off. See [DCS command smoke on off](https://wiki.hoggitworld.com/view/DCS_command_smoke_on_off)
-- @param #CONTROLLABLE self
-- @param #boolean OnOff Set to true for on and false for off. Defaults to true.
-- @param #number Delay (Optional) Delay the command by this many seconds.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandSmokeOnOff(OnOff, Delay)
  local switch = (OnOff == nil) and true or OnOff
  local command = {
      id = 'SMOKE_ON_OFF', 
      params = { 
          value = switch
          }
      }
  if Delay and Delay>0 then
    SCHEDULER:New(nil,self.CommandSmokeOnOff,{self,switch},Delay)
  else
    self:SetCommand(command)
  end
  return self
end

--- [AIR] Set smoke on. See [DCS command smoke on off](https://wiki.hoggitworld.com/view/DCS_command_smoke_on_off)
-- @param #CONTROLLABLE self
-- @param #number Delay (Optional) Delay the command by this many seconds.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandSmokeON(Delay)
  local command = {
      id = 'SMOKE_ON_OFF', 
      params = { 
          value = true
          }
      }
  if Delay and Delay>0 then
    SCHEDULER:New(nil,self.CommandSmokeON,{self},Delay)
  else
    self:SetCommand(command)
  end
  return self
end

--- [AIR] Set smoke off. See [DCS command smoke on off](https://wiki.hoggitworld.com/view/DCS_command_smoke_on_off)
-- @param #CONTROLLABLE self
-- @param #number Delay (Optional) Delay the command by this many seconds.
-- @return #CONTROLLABLE self
function CONTROLLABLE:CommandSmokeOFF(Delay)
  local command = {
      id = 'SMOKE_ON_OFF', 
      params = { 
          value = false
          }
      }
  if Delay and Delay>0 then
    SCHEDULER:New(nil,self.CommandSmokeOFF,{self},Delay)
  else
    self:SetCommand(command)
  end
  return self
end

--- Set EPLRS data link on/off.
-- @param #CONTROLLABLE self
-- @param #boolean SwitchOnOff If true (or nil) switch EPLRS on. If false switch off.
-- @param #number idx Task index. Default 1.
-- @return #table Task wrapped action.
function CONTROLLABLE:TaskEPLRS( SwitchOnOff, idx )

  if SwitchOnOff == nil then
    SwitchOnOff = true
  end

  -- Command to set the callsign.
  local CommandEPLRS = {
    id = 'EPLRS',
    params = {
      value = SwitchOnOff,
      groupId = self:GetID(),
    },
  }
  
  --if self:IsGround() then
   --CommandEPLRS.params.groupId = self:GetID()
  --end
  
  return self:TaskWrappedAction( CommandEPLRS, idx or 1 )
end

-- TASKS FOR AIR CONTROLLABLES

--- (AIR + GROUND) Attack a Controllable.
-- @param #CONTROLLABLE self
-- @param Wrapper.Group#GROUP AttackGroup The Group to be attacked.
-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage.
-- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion.
-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aircraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.
-- @param DCS#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude.
-- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks.
-- @param #boolean GroupAttack (Optional) If true, attack as group.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit, GroupAttack )
  -- self:F2( { self.ControllableName, AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } )

  --  AttackGroup = {
  --   id = 'AttackGroup',
  --   params = {
  --     groupId = Group.ID,
  --     weaponType = number,
  --     expend = enum AI.Task.WeaponExpend,
  --     attackQty = number,
  --     directionEnabled = boolean,
  --     direction = Azimuth,
  --     altitudeEnabled = boolean,
  --     altitude = Distance,
  --     attackQtyLimit = boolean,
  --   }
  -- }


  local DCSTask = { id = 'AttackGroup',
    params = {
      groupId          = AttackGroup:GetID(),
      weaponType       = WeaponType or 1073741822,
      expend           = WeaponExpend or "Auto",
      attackQtyLimit   = AttackQty and true or false,
      attackQty        = AttackQty or 1,
      directionEnabled = Direction and true or false,
      direction        = Direction and math.rad(Direction) or 0,
      altitudeEnabled  = Altitude and true or false,
      altitude         = Altitude,
      groupAttack      = GroupAttack and true or false,
    },
  }

  return DCSTask
end

--- (AIR + GROUND) Attack the Unit.
-- @param #CONTROLLABLE self
-- @param Wrapper.Unit#UNIT AttackUnit The UNIT to be attacked
-- @param #boolean GroupAttack (Optional) If true, all units in the group will attack the Unit when found. Default false.
-- @param DCS#AI.Task.WeaponExpend WeaponExpend (Optional) Determines how many weapons will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion.
-- @param #number AttackQty (Optional) Limits maximal quantity of attack. The aircraft/controllable will not make more attacks than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
-- @param DCS#Azimuth Direction (Optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction.
-- @param #number Altitude (Optional) The (minimum) altitude in meters from where to attack. Default is altitude of unit to attack but at least 1000 m.
-- @param #number WeaponType (optional) The WeaponType. See [DCS Enumerator Weapon Type](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag) on Hoggit.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskAttackUnit( AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType )

  local DCSTask = {
    id = 'AttackUnit',
    params = {
      unitId           = AttackUnit:GetID(),
      groupAttack      = GroupAttack and GroupAttack or false,
      expend           = WeaponExpend or "Auto",
      directionEnabled = Direction and true or false,
      direction        = Direction and math.rad(Direction) or 0,
      altitudeEnabled  = Altitude and true or false,
      altitude         = Altitude,
      attackQtyLimit   = AttackQty and true or false,
      attackQty        = AttackQty,
      weaponType       = WeaponType or 1073741822,
    },
  }

  return DCSTask
end

--- (AIR) Delivering weapon at the point on the ground.
-- @param #CONTROLLABLE self
-- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at.
-- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found.
-- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion.
-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aircraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.
-- @param #number Altitude (optional) The altitude from where to attack.
-- @param #number WeaponType (optional) The WeaponType.
-- @param #boolean Divebomb (optional) Perform dive bombing. Default false.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb )

  local DCSTask = {
    id = 'Bombing',
    params = {
      point            = Vec2,
      x                = Vec2.x,
      y                = Vec2.y,
      groupAttack      = GroupAttack and GroupAttack or false,
      expend           = WeaponExpend or "Auto",
      attackQtyLimit   = AttackQty and true or false,
      attackQty        = AttackQty or 1,
      directionEnabled = Direction and true or false,
      direction        = Direction and math.rad(Direction) or 0,
      altitudeEnabled  = Altitude and true or false,
      altitude         = Altitude or 2000,
      weaponType       = WeaponType or 1073741822,
      attackType       = Divebomb and "Dive" or nil,
      },
  }

  return DCSTask
end

--- (AIR) Strafe the point on the ground.
-- @param #CONTROLLABLE self
-- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver strafing at.
-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aircraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
-- @param #number Length (optional) Length of the strafing area.
-- @param #number WeaponType (optional) The WeaponType. WeaponType is a number associated with a [corresponding weapons flags](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag)
-- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much ammunition will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion, e.g. AI.Task.WeaponExpend.ALL.
-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.
-- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found.
-- @return DCS#Task The DCS task structure.
-- @usage
-- local attacker = GROUP:FindByName("Aerial-1")
-- local attackVec2 = ZONE:New("Strafe Attack"):GetVec2()
-- -- Attack with any cannons = 805306368, 4 runs, strafe a field of 200 meters
-- local task = attacker:TaskStrafing(attackVec2,4,200,805306368,AI.Task.WeaponExpend.ALL)
-- attacker:SetTask(task,2)
function CONTROLLABLE:TaskStrafing( Vec2, AttackQty, Length, WeaponType, WeaponExpend, Direction, GroupAttack )

  local DCSTask =  {
    id = 'Strafing',
    params = {
     point = Vec2, -- req
     weaponType = WeaponType or 805337088, -- Default 805337088 corresponds to guns/cannons (805306368) + any rocket (30720). You can set other types but then the AI uses even bombs for a strafing run!
     expend = WeaponExpend or "Auto",
     attackQty = AttackQty or 1,  -- req
     attackQtyLimit = AttackQty~=nil and true or false,
     direction = Direction and math.rad(Direction) or 0,
     directionEnabled = Direction and true or false,
     groupAttack = GroupAttack or false,
     length = Length,
    }
}

  return DCSTask
end

--- (AIR) Attacking the map object (building, structure, etc).
-- @param #CONTROLLABLE self
-- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at.
-- @param #boolean GroupAttack (Optional) If true, all units in the group will attack the Unit when found.
-- @param DCS#AI.Task.WeaponExpend WeaponExpend (Optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit will choose expend on its own discretion.
-- @param #number AttackQty (Optional) This parameter limits maximal quantity of attack. The aircraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
-- @param DCS#Azimuth Direction (Optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.
-- @param #number Altitude (Optional) The altitude [meters] from where to attack. Default 30 m.
-- @param #number WeaponType (Optional) The WeaponType. Default Auto=1073741822.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskAttackMapObject( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType )

  local DCSTask = {
    id = 'AttackMapObject',
    params = {
      point            = Vec2,
      x                = Vec2.x,
      y                = Vec2.y,
      groupAttack      = GroupAttack or false,
      expend           = WeaponExpend or "Auto",
      attackQtyLimit   = AttackQty and true or false,
      directionEnabled = Direction and true or false,
      direction        = Direction and math.rad(Direction) or 0,
      altitudeEnabled  = Altitude and true or false,
      altitude         = Altitude,
      weaponType       = WeaponType or 1073741822,
    },
  }

  return DCSTask
end

--- (AIR) Delivering weapon via CarpetBombing (all bombers in formation release at same time) at the point on the ground.
-- @param #CONTROLLABLE self
-- @param DCS#Vec2 Vec2 2D-coordinates of the point to deliver weapon at.
-- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found.
-- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit will choose expend on its own discretion.
-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aircraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.
-- @param #number Altitude (optional) The altitude from where to attack.
-- @param #number WeaponType (optional) The WeaponType.
-- @param #number CarpetLength (optional) default to 500 m.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskCarpetBombing( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, CarpetLength )

  -- Build Task Structure
  local DCSTask = {
    id = 'CarpetBombing',
    params = {
      attackType       = "Carpet",
      x                = Vec2.x,
      y                = Vec2.y,
      groupAttack      = GroupAttack and GroupAttack or false,
      carpetLength     = CarpetLength or 500,
      weaponType       = WeaponType or ENUMS.WeaponFlag.AnyBomb,
      expend           = WeaponExpend or "All",
      attackQtyLimit   = AttackQty and true or false,
      attackQty        = AttackQty or 1,
      directionEnabled = Direction and true or false,
      direction        = Direction and math.rad(Direction) or 0,
      altitudeEnabled  = Altitude and true or false,
      altitude         = Altitude,
    },
  }

  return DCSTask
end

--- (AIR) Following another airborne controllable.
-- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders.
-- Used to support CarpetBombing Task
-- @param #CONTROLLABLE self
-- @param #CONTROLLABLE FollowControllable The controllable to be followed.
-- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around.
-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskFollowBigFormation( FollowControllable, Vec3, LastWaypointIndex )

  local DCSTask = {
    id = 'FollowBigFormation',
    params = {
      groupId          = FollowControllable:GetID(),
      pos              = Vec3,
      lastWptIndexFlag = LastWaypointIndex and true or false,
      lastWptIndex     = LastWaypointIndex,
    },
  }

  return DCSTask
end

--- (AIR HELICOPTER) Move the controllable to a Vec2 Point, wait for a defined duration and embark infantry groups.
-- @param #CONTROLLABLE self
-- @param Core.Point#COORDINATE Coordinate The point where to pickup the troops.
-- @param Core.Set#SET_GROUP GroupSetForEmbarking Set of groups to embark.
-- @param #number Duration (Optional) The maximum duration in seconds to wait until all groups have embarked.
-- @param #table Distribution (Optional) Distribution used to put the infantry groups into specific carrier units.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskEmbarking( Coordinate, GroupSetForEmbarking, Duration, Distribution )

  -- Table of group IDs for embarking.
  local g4e = {}

  if GroupSetForEmbarking then
    for _, _group in pairs( GroupSetForEmbarking:GetSet() ) do
      local group = _group -- Wrapper.Group#GROUP
      table.insert( g4e, group:GetID() )
    end
  else
    self:E( "ERROR: No groups for embarking specified!" )
    return nil
  end

  -- Table of group IDs for embarking.
  -- local Distribution={}

  -- Distribution
  -- local distribution={}
  -- distribution[id]=gids

  local groupID = self and self:GetID()

  local DCSTask = {
    id = 'Embarking',
    params = {
      selectedTransport  = groupID,
      x                  = Coordinate.x,
      y                  = Coordinate.z,
      groupsForEmbarking = g4e,
      durationFlag       = Duration and true or false,
      duration           = Duration,
      distributionFlag   = Distribution and true or false,
      distribution       = Distribution,
    },
  }

  return DCSTask
end

--- Used in conjunction with the embarking task for a transport helicopter group. The Ground units will move to the specified location and wait to be picked up by a helicopter.
-- The helicopter will then fly them to their dropoff point defined by another task for the ground forces; DisembarkFromTransport task.
-- The controllable has to be an infantry group!
-- @param #CONTROLLABLE self
-- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up.
-- @param #number Radius Radius in meters. Default 200 m.
-- @param #string UnitType The unit type name of the carrier, e.g. "UH-1H". Must not be specified.
-- @return DCS#Task Embark to transport task.
function CONTROLLABLE:TaskEmbarkToTransport( Coordinate, Radius, UnitType )

  local EmbarkToTransport = {
   id = "EmbarkToTransport",
   params={
       x            = Coordinate.x,
       y            = Coordinate.z,
       zoneRadius   = Radius or 200,
       selectedType = UnitType,
     },
   }

  return EmbarkToTransport
end

--- Specifies the location infantry groups that is being transported by helicopters will be unloaded at. Used in conjunction with the EmbarkToTransport task.
-- @param #CONTROLLABLE self
-- @param Core.Point#COORDINATE Coordinate Coordinates where AI is expecting to be picked up.
-- @return DCS#Task Embark to transport task.
function CONTROLLABLE:TaskDisembarking( Coordinate, GroupSetToDisembark )

  -- Table of group IDs for disembarking.
  local g4e = {}

  if GroupSetToDisembark then
    for _, _group in pairs( GroupSetToDisembark:GetSet() ) do
      local group = _group -- Wrapper.Group#GROUP
      table.insert( g4e, group:GetID() )
    end
  else
    self:E( "ERROR: No groups for disembarking specified!" )
    return nil
  end

  local Disembarking = {
    id = "Disembarking",
    params = {
      x = Coordinate.x,
      y = Coordinate.z,
      groupsForEmbarking = g4e, -- This is no bug, the entry is really "groupsForEmbarking" even if we disembark the troops.
    },
  }

  return Disembarking
end

--- (AIR) Orbit at a specified position at a specified altitude during a specified duration with a specified speed.
-- @param #CONTROLLABLE self
-- @param DCS#Vec2 Point The point to hold the position.
-- @param #number Altitude The altitude AGL in meters to hold the position.
-- @param #number Speed The speed [m/s] flying when holding the position.
-- @return #CONTROLLABLE self
function CONTROLLABLE:TaskOrbitCircleAtVec2( Point, Altitude, Speed )
  --self:F2( { self.ControllableName, Point, Altitude, Speed } )

  local DCSTask = {
    id = 'Orbit',
    params = {
      pattern  = AI.Task.OrbitPattern.CIRCLE,
      point    = Point,
      speed    = Speed,
      altitude = Altitude + land.getHeight( Point ),
    },
  }

  return DCSTask
end

--- (AIR) Orbit at a position with at a given altitude and speed. Optionally, a race track pattern can be specified.
-- @param #CONTROLLABLE self
-- @param Core.Point#COORDINATE Coord Coordinate at which the CONTROLLABLE orbits. Can also be given as a `DCS#Vec3` or `DCS#Vec2` object.
-- @param #number Altitude Altitude in meters of the orbit pattern. Default y component of Coord.
-- @param #number Speed Speed [m/s] flying the orbit pattern. Default 128 m/s = 250 knots.
-- @param Core.Point#COORDINATE CoordRaceTrack (Optional) If this coordinate is specified, the CONTROLLABLE will fly a race-track pattern using this and the initial coordinate.
-- @return #CONTROLLABLE self
function CONTROLLABLE:TaskOrbit( Coord, Altitude, Speed, CoordRaceTrack )

  local Pattern = AI.Task.OrbitPattern.CIRCLE

  local P1 = {x=Coord.x, y=Coord.z or Coord.y}
  local P2 = nil
  if CoordRaceTrack then
    Pattern = AI.Task.OrbitPattern.RACE_TRACK
    P2 = {x=CoordRaceTrack.x, y=CoordRaceTrack.z or CoordRaceTrack.y}
  end

  local Task = {
    id = 'Orbit',
    params = {
      pattern  = Pattern,
      point    = P1,
      point2   = P2,
      speed    = Speed or UTILS.KnotsToMps(250),
      altitude = Altitude or Coord.y,
    },
  }

  return Task
end

--- (AIR) Orbit at the current position of the first unit of the controllable at a specified altitude.
-- @param #CONTROLLABLE self
-- @param #number Altitude The altitude [m] to hold the position.
-- @param #number Speed The speed [m/s] flying when holding the position.
-- @param Core.Point#COORDINATE Coordinate (Optional) The coordinate where to orbit. If the coordinate is not given, then the current position of the controllable is used.
-- @return #CONTROLLABLE self
function CONTROLLABLE:TaskOrbitCircle( Altitude, Speed, Coordinate )
  self:F2( { self.ControllableName, Altitude, Speed } )

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then
    local OrbitVec2 = Coordinate and Coordinate:GetVec2() or self:GetVec2()
    return self:TaskOrbitCircleAtVec2( OrbitVec2, Altitude, Speed )
  end

  return nil
end

--- (AIR) Hold position at the current position of the first unit of the controllable.
-- @param #CONTROLLABLE self
-- @param #number Duration The maximum duration in seconds to hold the position.
-- @return #CONTROLLABLE self
function CONTROLLABLE:TaskHoldPosition()
  self:F2( { self.ControllableName } )

  return self:TaskOrbitCircle( 30, 10 )
end

--- (AIR) Delivering weapon on the runway. See [hoggit](https://wiki.hoggitworld.com/view/DCS_task_bombingRunway)
--
-- Make sure the aircraft has the following role:
--
-- * CAS
-- * Ground Attack
-- * Runway Attack
-- * Anti-Ship Strike
-- * AFAC
-- * Pinpoint Strike
--
-- @param #CONTROLLABLE self
-- @param Wrapper.Airbase#AIRBASE Airbase Airbase to attack.
-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. See [DCS enum weapon flag](https://wiki.hoggitworld.com/view/DCS_enum_weapon_flag). Default 2147485694 = AnyBomb (GuidedBomb + AnyUnguidedBomb).
-- @param DCS#AI.Task.WeaponExpend WeaponExpend Enum AI.Task.WeaponExpend that defines how much munitions the AI will expend per attack run. Default "ALL".
-- @param #number AttackQty Number of times the group will attack if the target. Default 1.
-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.
-- @param #boolean GroupAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a group and not to a single aircraft.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskBombingRunway( Airbase, WeaponType, WeaponExpend, AttackQty, Direction, GroupAttack )

  local DCSTask = {
    id = 'BombingRunway',
    params = {
    runwayId    = Airbase:GetID(),
    weaponType  = WeaponType or ENUMS.WeaponFlag.AnyBomb,
    expend      = WeaponExpend or AI.Task.WeaponExpend.ALL,
    attackQty   = AttackQty or 1,
    direction   = Direction and math.rad(Direction) or 0,
    groupAttack = GroupAttack and true or false,
    },
  }

  return DCSTask
end

--- (AIR) Refueling from the nearest tanker. No parameters.
-- @param #CONTROLLABLE self
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskRefueling()

  local DCSTask = {
    id = 'Refueling',
    params = {},
  }

  return DCSTask
end

--- (AIR) Act as Recovery Tanker for a naval/carrier group.
-- @param #CONTROLLABLE self
-- @param Wrapper.Group#GROUP CarrierGroup
-- @param #number Speed Speed in meters per second
-- @param #number Altitude Altitude the tanker orbits at in meters
-- @param #number LastWptNumber (optional) Waypoint of carrier group that when reached, ends the recovery tanker task
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskRecoveryTanker(CarrierGroup, Speed, Altitude, LastWptNumber)

  local LastWptFlag = type(LastWptNumber) == "number" and true or false

  local DCSTask = {
   id = "RecoveryTanker",
   params = {
       groupId = CarrierGroup:GetID(),
       speed = Speed,
       altitude = Altitude,
       lastWptIndexFlag = LastWptFlag,
       lastWptIndex = LastWptNumber
      }
    }

  return DCSTask
end

--- (AIR HELICOPTER) Landing at the ground. For helicopters only.
-- @param #CONTROLLABLE self
-- @param DCS#Vec2 Vec2 The point where to land.
-- @param #number Duration The duration in seconds to stay on the ground.
-- @param #boolean CombatLanding (optional) If true, set the Combat Landing option.
-- @param #number DirectionAfterLand (optional) Heading after landing in degrees.
-- @return #CONTROLLABLE self
function CONTROLLABLE:TaskLandAtVec2( Vec2, Duration , CombatLanding, DirectionAfterLand)

  local DCSTask = {
    id = 'Land',
    params = {
      point        = Vec2,
      durationFlag = Duration and true or false,
      duration     = Duration,
      combatLandingFlag = CombatLanding == true and true or false,
    },
  }
  
  if DirectionAfterLand ~= nil and type(DirectionAfterLand) == "number" then
    DCSTask.params.directionEnabled = true
    DCSTask.params.direction = math.rad(DirectionAfterLand) 
  end
  
  return DCSTask
end

--- (AIR) Land the controllable at a @{Core.Zone#ZONE_RADIUS).
-- @param #CONTROLLABLE self
-- @param Core.Zone#ZONE Zone The zone where to land.
-- @param #number Duration The duration in seconds to stay on the ground.
-- @param #boolean RandomPoint (optional) If true,land at a random point inside of the zone. 
-- @param #boolean CombatLanding (optional) If true, set the Combat Landing option.
-- @param #number DirectionAfterLand (optional) Heading after landing in degrees.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint, CombatLanding, DirectionAfterLand )

  -- Get landing point
  local Point = RandomPoint and Zone:GetRandomVec2() or Zone:GetVec2()

  local DCSTask = CONTROLLABLE.TaskLandAtVec2( self, Point, Duration, CombatLanding, DirectionAfterLand)

  return DCSTask
end

--- (AIR) Following another airborne controllable.
-- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders.
-- If another controllable is on land the unit / controllable will orbit around.
-- @param #CONTROLLABLE self
-- @param #CONTROLLABLE FollowControllable The controllable to be followed.
-- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around.
-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex )
  self:F2( { self.ControllableName, FollowControllable, Vec3, LastWaypointIndex } )

  --  Follow = {
  --    id = 'Follow',
  --    params = {
  --      groupId = Group.ID,
  --      pos = Vec3,
  --      lastWptIndexFlag = boolean,
  --      lastWptIndex = number
  --    }
  --  }

  local LastWaypointIndexFlag = false
  local lastWptIndexFlagChangedManually = false
  if LastWaypointIndex then
    LastWaypointIndexFlag = true
    lastWptIndexFlagChangedManually = true
  end

  local DCSTask = {
    id = 'Follow',
    params = {
      groupId                         = FollowControllable:GetID(),
      pos                             = Vec3,
      lastWptIndexFlag                = LastWaypointIndexFlag,
      lastWptIndex                    = LastWaypointIndex,
      lastWptIndexFlagChangedManually = lastWptIndexFlagChangedManually,
    },
  }

  self:T3( { DCSTask } )
  return DCSTask
end

--- (AIR/HELO) Escort a ground controllable.
-- The unit / controllable will follow lead unit of the other controllable, additional units of both controllables will continue following their leaders.
-- The unit / controllable will also protect that controllable from threats of specified types.
-- @param #CONTROLLABLE self
-- @param #CONTROLLABLE FollowControllable The controllable to be escorted.
-- @param #number LastWaypointIndex (optional) Detach waypoint of another controllable. Once reached the unit / controllable Escort task is finished.
-- @param #number OrbitDistance (optional) Maximum distance helo will orbit around the ground unit in meters. Defaults to 2000 meters.
-- @param DCS#AttributeNameArray TargetTypes (optional) Array of AttributeName that is contains threat categories allowed to engage. Default {"Ground vehicles"}. See [https://wiki.hoggit.us/view/DCS_enum_attributes](https://wiki.hoggit.us/view/DCS_enum_attributes)
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskGroundEscort( FollowControllable, LastWaypointIndex, OrbitDistance, TargetTypes )

  --  Escort = {
  --    id = 'GroundEscort',
  --    params = {
  --      groupId = Group.ID, -- must
  --      engagementDistMax = Distance, -- Must. With his task it does not appear to actually define the range AI are allowed to attack at, rather it defines the size length of the orbit. The helicopters will fly up to this set distance before returning to the escorted group.
  --      lastWptIndexFlag = boolean, -- optional
  --      lastWptIndex = number, -- optional
  --      targetTypes = array of AttributeName, -- must
  --      lastWptIndexFlagChangedManually = boolean, -- must be true
  --    }
  --  }

  local DCSTask = {
    id = 'GroundEscort',
    params = {
    groupId           = FollowControllable and FollowControllable:GetID() or nil,
    engagementDistMax = OrbitDistance or 2000,
    lastWptIndexFlag  = LastWaypointIndex and true or false,
    lastWptIndex      = LastWaypointIndex,
    targetTypes       = TargetTypes or {"Ground vehicles"},
    lastWptIndexFlagChangedManually = true,
    },
  }

  return DCSTask
end

--- (AIR) Escort another airborne controllable.
-- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders.
-- The unit / controllable will also protect that controllable from threats of specified types.
-- @param #CONTROLLABLE self
-- @param #CONTROLLABLE FollowControllable The controllable to be escorted.
-- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around.
-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Escort task is finished.
-- @param #number EngagementDistance Maximal distance from escorted controllable to threat in meters. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax.
-- @param DCS#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. Default {"Air"}. See https://wiki.hoggit.us/view/DCS_enum_attributes
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes )

  --  Escort = {
  --    id = 'Escort',
  --    params = {
  --      groupId = Group.ID,
  --      pos = Vec3,
  --      lastWptIndexFlag = boolean,
  --      lastWptIndex = number,
  --      engagementDistMax = Distance,
  --      targetTypes = array of AttributeName,
  --    }
  --  }

  local DCSTask = {
    id = 'Escort',
    params = {
      groupId           = FollowControllable and FollowControllable:GetID() or nil,
      pos               = Vec3,
      lastWptIndexFlag  = LastWaypointIndex and true or false,
      lastWptIndex      = LastWaypointIndex,
      engagementDistMax = EngagementDistance,
      targetTypes       = TargetTypes or {"Air"},
    },
  }

  return DCSTask
end

-- GROUND TASKS

--- (GROUND) Fire at a VEC2 point until ammunition is finished.
-- @param #CONTROLLABLE self
-- @param DCS#Vec2 Vec2 The point to fire at.
-- @param DCS#Distance Radius The radius of the zone to deploy the fire at.
-- @param #number AmmoCount (optional) Quantity of ammunition to expand (omit to fire until ammunition is depleted).
-- @param #number WeaponType (optional) Enum for weapon type ID. This value is only required if you want the group firing to use a specific weapon, for instance using the task on a ship to force it to fire guided missiles at targets within cannon range. See http://wiki.hoggit.us/view/DCS_enum_weapon_flag
-- @param #number Altitude (Optional) Altitude in meters.
-- @param #number ASL Altitude is above mean sea level. Default is above ground level.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType, Altitude, ASL )

  local DCSTask = {
    id = 'FireAtPoint',
    params = {
      point            = Vec2,
      x                = Vec2.x,
      y                = Vec2.y,
      zoneRadius       = Radius,
      radius           = Radius,
      expendQty        = 1, -- dummy value
      expendQtyEnabled = false,
      alt_type         = ASL and 0 or 1,
    },
  }

  if AmmoCount then
    DCSTask.params.expendQty = AmmoCount
    DCSTask.params.expendQtyEnabled = true
  end

  if Altitude then
    DCSTask.params.altitude = Altitude
  end

  if WeaponType then
    DCSTask.params.weaponType = WeaponType
  end

  --env.info("FF fireatpoint")
  --BASE:I(DCSTask)

  return DCSTask
end

--- (GROUND) Hold ground controllable from moving.
-- @param #CONTROLLABLE self
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskHold()
  local DCSTask = { id = 'Hold', params = {} }
  return DCSTask
end

-- TASKS FOR AIRBORNE AND GROUND UNITS/CONTROLLABLES

--- (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction.
-- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC.
-- If the task is assigned to the controllable lead unit will be a FAC.
-- It's important to note that depending on the type of unit that is being assigned the task (AIR or GROUND), you must choose the correct type of callsign enumerator. For airborne controllables use CALLSIGN.Aircraft and for ground based use CALLSIGN.JTAC enumerators.
-- @param #CONTROLLABLE self
-- @param Wrapper.Group#GROUP AttackGroup Target GROUP object.
-- @param #number WeaponType Bitmask of weapon types, which are allowed to use.
-- @param DCS#AI.Task.Designation Designation (Optional) Designation type.
-- @param #boolean Datalink (Optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default.
-- @param #number Frequency Frequency in MHz used to communicate with the FAC. Default 133 MHz.
-- @param #number Modulation Modulation of radio for communication. Default 0=AM.
-- @param #number CallsignName Callsign enumerator name of the FAC. (CALLSIGN.Aircraft.{name} for airborne controllables, CALLSIGN.JTACS.{name} for ground units)
-- @param #number CallsignNumber Callsign number, e.g. Axeman-**1**.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink, Frequency, Modulation, CallsignName, CallsignNumber )

  local DCSTask = {
    id = 'FAC_AttackGroup',
    params = {
      groupId     = AttackGroup:GetID(),
      weaponType  = WeaponType or ENUMS.WeaponFlag.AutoDCS,
      designation = Designation or "Auto",
      datalink    = Datalink and Datalink or true,
      frequency   = (Frequency or 133)*1000000,
      modulation  = Modulation or radio.modulation.AM,
      callname    = CallsignName,
      number      = CallsignNumber,
    },
  }

  return DCSTask
end

--- (AIR) Engaging targets of defined types.
-- @param #CONTROLLABLE self
-- @param DCS#Distance Distance Maximal distance from the target to a route leg. If the target is on a greater distance it will be ignored.
-- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage.
-- @param #number Priority All enroute tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority )

  local DCSTask = {
    id = 'EngageTargets',
    params = {
      maxDistEnabled = Distance and true or false,
      maxDist        = Distance,
      targetTypes    = TargetTypes or {"Air"},
      priority       = Priority or 0,
    },
  }

  return DCSTask
end

--- (AIR) Engaging a targets of defined types at circle-shaped zone.
-- @param #CONTROLLABLE self
-- @param DCS#Vec2 Vec2 2D-coordinates of the zone.
-- @param DCS#Distance Radius Radius of the zone.
-- @param DCS#AttributeNameArray TargetTypes (Optional) Array of target categories allowed to engage. Default {"Air"}.
-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, Priority )

  local DCSTask = {
    id = 'EngageTargetsInZone',
    params = {
      point       = Vec2,
      zoneRadius  = Radius,
      targetTypes = TargetTypes or {"Air"},
      priority    = Priority or 0
    },
  }

  return DCSTask
end

--- (AIR) Enroute anti-ship task.
-- @param #CONTROLLABLE self
-- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. Default `{"Ships"}`.
-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskAntiShip(TargetTypes, Priority)

  local DCSTask = {
    id      = 'EngageTargets',
    key     = "AntiShip",
    --auto    = false,
    --enabled = true,
    params  = {
      targetTypes = TargetTypes or {"Ships"},
      priority    = Priority or 0
    }
  }

  return DCSTask
end

--- (AIR) Enroute SEAD task.
-- @param #CONTROLLABLE self
-- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. Default `{"Air Defence"}`.
-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskSEAD(TargetTypes, Priority)

  local DCSTask = {
    id      = 'EngageTargets',
    key     = "SEAD",
    --auto    = false,
    --enabled = true,
    params  = {
      targetTypes = TargetTypes or {"Air Defence"},
      priority    = Priority or 0
    }
  }

  return DCSTask
end

--- (AIR) Enroute CAP task.
-- @param #CONTROLLABLE self
-- @param DCS#AttributeNameArray TargetTypes Array of target categories allowed to engage. Default `{"Air"}`.
-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default 0.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskCAP(TargetTypes, Priority)

  local DCSTask = {
    id      = 'EngageTargets',
    key     = "CAP",
    --auto    = true,
    enabled = true,
    params  = {
      targetTypes = TargetTypes or {"Air"},
      priority    = Priority or 0
    }
  }

  return DCSTask
end

--- (AIR) Engaging a controllable. The task does not assign the target controllable to the unit/controllable to attack now;
-- it just allows the unit/controllable to engage the target controllable as well as other assigned targets.
-- See [hoggit](https://wiki.hoggitworld.com/view/DCS_task_engageGroup).
-- @param #CONTROLLABLE self
-- @param #CONTROLLABLE AttackGroup The Controllable to be attacked.
-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first.
-- @param #number WeaponType (optional) Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage.
-- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion.
-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aircraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.
-- @param DCS#Distance Altitude (optional) Desired attack start altitude. Controllable/aircraft will make its attacks from the altitude. If the altitude is too low or too high to use weapon aircraft/controllable will choose closest altitude to the desired attack start altitude. If the desired altitude is defined controllable/aircraft will not attack from safe altitude.
-- @param #boolean AttackQtyLimit (optional) The flag determines how to interpret attackQty parameter. If the flag is true then attackQty is a limit on maximal attack quantity for "AttackGroup" and "AttackUnit" tasks. If the flag is false then attackQty is a desired attack quantity for "Bombing" and "BombingRunway" tasks.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskEngageGroup( AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit )

  local DCSTask = {
    id = 'EngageGroup',
    params = {
      groupId          = AttackGroup:GetID(),
      weaponType       = WeaponType,
      expend           = WeaponExpend or "Auto",
      directionEnabled = Direction and true or false,
      direction        = Direction,
      altitudeEnabled  = Altitude and true or false,
      altitude         = Altitude,
      attackQtyLimit   = AttackQty and true or false,
      attackQty        = AttackQty,
      priority         = Priority or 1,
    },
  }

  return DCSTask
end

--- (AIR) Search and attack the Unit.
-- See [hoggit](https://wiki.hoggitworld.com/view/DCS_task_engageUnit).
-- @param #CONTROLLABLE self
-- @param Wrapper.Unit#UNIT EngageUnit The UNIT.
-- @param #number Priority (optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first.
-- @param #boolean GroupAttack (optional) If true, all units in the group will attack the Unit when found.
-- @param DCS#AI.Task.WeaponExpend WeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion.
-- @param #number AttackQty (optional) This parameter limits maximal quantity of attack. The aircraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aircraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
-- @param DCS#Azimuth Direction (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.
-- @param DCS#Distance Altitude (optional) Desired altitude to perform the unit engagement.
-- @param #boolean Visible (optional) Unit must be visible.
-- @param #boolean ControllableAttack (optional) Flag indicates that the target must be engaged by all aircrafts of the controllable. Has effect only if the task is assigned to a controllable, not to a single aircraft.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskEngageUnit( EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack )

  local DCSTask = {
    id = 'EngageUnit',
    params = {
      unitId             = EngageUnit:GetID(),
      priority           = Priority or 1,
      groupAttack        = GroupAttack and GroupAttack or false,
      visible            = Visible and Visible or false,
      expend             = WeaponExpend or "Auto",
      directionEnabled   = Direction and true or false,
      direction          = Direction and math.rad(Direction) or nil,
      altitudeEnabled    = Altitude and true or false,
      altitude           = Altitude,
      attackQtyLimit     = AttackQty and true or false,
      attackQty          = AttackQty,
      controllableAttack = ControllableAttack,
    },
  }

  return DCSTask
end

--- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters.
-- [hoggit](https://wiki.hoggitworld.com/view/DCS_task_awacs).
-- @param #CONTROLLABLE self
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskAWACS()

  local DCSTask = {
    id = 'AWACS',
    params = {},
  }

  return DCSTask
end

--- (AIR) Aircraft will act as a tanker for friendly units. No parameters.
-- See [hoggit](https://wiki.hoggitworld.com/view/DCS_task_tanker).
-- @param #CONTROLLABLE self
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskTanker()

  local DCSTask = {
    id = 'Tanker',
    params = {},
  }

  return DCSTask
end

-- En-route tasks for ground units/controllables

--- (GROUND) Ground unit (EW-radar) will act as an EWR for friendly units (will provide them with information about contacts). No parameters.
-- See [hoggit](https://wiki.hoggitworld.com/view/DCS_task_ewr).
-- @param #CONTROLLABLE self
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskEWR()

  local DCSTask = {
    id = 'EWR',
    params = {},
  }

  return DCSTask
end

-- En-route tasks for airborne and ground units/controllables

--- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose the target (enemy ground controllable) as well as other assigned targets.
-- The killer is player-controlled allied CAS-aircraft that is in contact with the FAC.
-- If the task is assigned to the controllable lead unit will be a FAC.
-- See [hoggit](https://wiki.hoggitworld.com/view/DCS_task_fac_engageGroup).
-- @param #CONTROLLABLE self
-- @param #CONTROLLABLE AttackGroup Target CONTROLLABLE.
-- @param #number Priority (Optional) All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first. Default is 0.
-- @param #number WeaponType (Optional) Bitmask of weapon types those allowed to use. Default is "Auto".
-- @param DCS#AI.Task.Designation Designation (Optional) Designation type.
-- @param #boolean Datalink (optional) Allows to use datalink to send the target information to attack aircraft. Enabled by default.
-- @param #number CallsignID CallsignID, e.g. `CALLSIGN.JTAC.Anvil` for ground or `CALLSIGN.Aircraft.Ford` for air.
-- @param #number CallsignNumber Callsign first number, e.g. 2 for `Ford-2`.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink, Frequency, Modulation, CallsignID, CallsignNumber )

  local DCSTask = {
    id = 'FAC_EngageGroup',
    params = {
      groupId     = AttackGroup:GetID(),
      weaponType  = WeaponType or "Auto",
      designation = Designation,
      datalink    = Datalink and Datalink or false,
      frequency   = (Frequency or 133)*1000000,
      modulation  = Modulation or radio.modulation.AM,
      callname    = CallsignID,
      number      = CallsignNumber,
      priority    = Priority or 0,
    },
  }

  return DCSTask
end

--- (AIR + GROUND) The task makes the controllable/unit a FAC and lets the FAC to choose a targets (enemy ground controllable) around as well as other assigned targets.
-- Assigns the controlled group to act as a Forward Air Controller or JTAC. Any detected targets will be assigned as targets to the player via the JTAC radio menu.
-- Target designation is set to auto and is dependent on the circumstances.
-- See [hoggit](https://wiki.hoggitworld.com/view/DCS_task_fac).
-- @param #CONTROLLABLE self
-- @param #number Frequency Frequency in MHz. Default 133 MHz.
-- @param #number Modulation Radio modulation. Default `radio.modulation.AM`.
-- @param #number CallsignID CallsignID, e.g. `CALLSIGN.JTAC.Anvil` for ground or `CALLSIGN.Aircraft.Ford` for air.
-- @param #number CallsignNumber Callsign first number, e.g. 2 for `Ford-2`.
-- @param #number Priority All en-route tasks have the priority parameter. This is a number (less value - higher priority) that determines actions related to what task will be performed first.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskFAC( Frequency, Modulation, CallsignID, CallsignNumber, Priority )

  local DCSTask = {
    id = 'FAC',
    params = {
      frequency  = (Frequency or 133)*1000000,
      modulation = Modulation or radio.modulation.AM,
      callname   = CallsignID,
      number     = CallsignNumber,
      priority   = Priority or 0
    }
  }

  return DCSTask
end

--- This creates a Task element, with an action to call a function as part of a Wrapped Task.
-- This Task can then be embedded at a Waypoint by calling the method @{#CONTROLLABLE.SetTaskWaypoint}.
-- @param #CONTROLLABLE self
-- @param #string FunctionString The function name embedded as a string that will be called.
-- @param ... The variable arguments passed to the function when called! These arguments can be of any type!
-- @return #CONTROLLABLE
-- @usage
--
--  local ZoneList = {
--    ZONE:New( "ZONE1" ),
--    ZONE:New( "ZONE2" ),
--    ZONE:New( "ZONE3" ),
--    ZONE:New( "ZONE4" ),
--    ZONE:New( "ZONE5" )
--  }
--
--  GroundGroup = GROUP:FindByName( "Vehicle" )
--
--  --- @param Wrapper.Group#GROUP GroundGroup
--  function RouteToZone( Vehicle, ZoneRoute )
--
--    local Route = {}
--
--    Vehicle:E( { ZoneRoute = ZoneRoute } )
--
--    Vehicle:MessageToAll( "Moving to zone " .. ZoneRoute:GetName(), 10 )
--
--    -- Get the current coordinate of the Vehicle
--    local FromCoord = Vehicle:GetCoordinate()
--
--    -- Select a random Zone and get the Coordinate of the new Zone.
--    local RandomZone = ZoneList[ math.random( 1, #ZoneList ) ] -- Core.Zone#ZONE
--    local ToCoord = RandomZone:GetCoordinate()
--
--    -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task
--    Route[#Route+1] = FromCoord:WaypointGround( 72 )
--    Route[#Route+1] = ToCoord:WaypointGround( 60, "Vee" )
--
--    local TaskRouteToZone = Vehicle:TaskFunction( "RouteToZone", RandomZone )
--
--    Vehicle:SetTaskWaypoint( Route[#Route], TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone.
--
--    Vehicle:Route( Route, math.random( 10, 20 ) ) -- Move after a random seconds to the Route. See the Route method for details.
--
--  end
--
--    RouteToZone( GroundGroup, ZoneList[1] )
--
function CONTROLLABLE:TaskFunction( FunctionString, ... )

  -- Script
  local DCSScript = {}
  DCSScript[#DCSScript + 1] = "local MissionControllable = GROUP:Find( ... ) "
  if arg and arg.n > 0 then
    local ArgumentKey = '_' .. tostring( arg ):match( "table: (.*)" )
    self:SetState( self, ArgumentKey, arg )
    DCSScript[#DCSScript + 1] = "local Arguments = MissionControllable:GetState( MissionControllable, '" .. ArgumentKey .. "' ) "
    DCSScript[#DCSScript + 1] = FunctionString .. "( MissionControllable, unpack( Arguments ) )"
  else
    DCSScript[#DCSScript + 1] = FunctionString .. "( MissionControllable )"
  end

  -- DCS task.
  local DCSTask = self:TaskWrappedAction( self:CommandDoScript( table.concat( DCSScript ) ) )

  return DCSTask
end

--- (AIR + GROUND) Return a mission task from a mission template.
-- @param #CONTROLLABLE self
-- @param #table TaskMission A table containing the mission task.
-- @return DCS#Task
function CONTROLLABLE:TaskMission( TaskMission )

  local DCSTask = {
    id = 'Mission',
    params = {
      TaskMission,
    },
  }

  return DCSTask
end

do -- Patrol methods

  --- (GROUND) Patrol iteratively using the waypoints of the (parent) group.
  -- @param #CONTROLLABLE self
  -- @return #CONTROLLABLE
  function CONTROLLABLE:PatrolRoute()

    local PatrolGroup = self -- Wrapper.Group#GROUP

    if not self:IsInstanceOf( "GROUP" ) then
      PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP
    end

    self:F( { PatrolGroup = PatrolGroup:GetName() } )

    if PatrolGroup:IsGround() or PatrolGroup:IsShip() then

      local Waypoints = PatrolGroup:GetTemplateRoutePoints()

      -- Calculate the new Route.
      local FromCoord = PatrolGroup:GetCoordinate()

      -- test for submarine
      local depth = 0
      local IsSub = false
      if PatrolGroup:IsShip() then
        local navalvec3 = FromCoord:GetVec3()
        if navalvec3.y < 0 then
          depth = navalvec3.y
          IsSub = true
        end
      end


      local Waypoint = Waypoints[1]
      local Speed = Waypoint.speed or (20 / 3.6)
       local From = FromCoord:WaypointGround( Speed )

      if IsSub then
         From = FromCoord:WaypointNaval( Speed, Waypoint.alt )
      end

      table.insert( Waypoints, 1, From )

      local TaskRoute = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRoute" )

      self:F( { Waypoints = Waypoints } )
      local Waypoint = Waypoints[#Waypoints]
      PatrolGroup:SetTaskWaypoint( Waypoint, TaskRoute ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone.

      PatrolGroup:Route( Waypoints, 2 ) -- Move after a random seconds to the Route. See the Route method for details.
    end
  end

  --- (GROUND) Patrol randomly to the waypoints the for the (parent) group.
  -- A random waypoint will be picked and the group will move towards that point.
  -- @param #CONTROLLABLE self
  -- @param #number Speed Speed in km/h.
  -- @param #string Formation The formation the group uses.
  -- @param Core.Point#COORDINATE ToWaypoint The waypoint where the group should move to.
  -- @return #CONTROLLABLE
  function CONTROLLABLE:PatrolRouteRandom( Speed, Formation, ToWaypoint )

    local PatrolGroup = self -- Wrapper.Group#GROUP

    if not self:IsInstanceOf( "GROUP" ) then
      PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP
    end

    self:F( { PatrolGroup = PatrolGroup:GetName() } )

    if PatrolGroup:IsGround() or PatrolGroup:IsShip() then

      local Waypoints = PatrolGroup:GetTemplateRoutePoints()

      -- Calculate the new Route.
      local FromCoord = PatrolGroup:GetCoordinate()
      local FromWaypoint = 1
      if ToWaypoint then
        FromWaypoint = ToWaypoint
      end
      -- test for submarine
      local depth = 0
      local IsSub = false
      if PatrolGroup:IsShip() then
        local navalvec3 = FromCoord:GetVec3()
        if navalvec3.y < 0 then
          depth = navalvec3.y
          IsSub = true
        end
      end
      -- Loop until a waypoint has been found that is not the same as the current waypoint.
      -- Otherwise the object won't move, or drive in circles, and the algorithm would not do exactly
      -- what it is supposed to do, which is making groups drive around.
      local ToWaypoint
      repeat
        -- Select a random waypoint and check if it is not the same waypoint as where the object is about.
        ToWaypoint = math.random( 1, #Waypoints )
      until (ToWaypoint ~= FromWaypoint)
      self:F( { FromWaypoint = FromWaypoint, ToWaypoint = ToWaypoint } )

      local Waypoint = Waypoints[ToWaypoint] -- Select random waypoint.
      local ToCoord = COORDINATE:NewFromVec2( { x = Waypoint.x, y = Waypoint.y } )
      -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task
      local Route = {}
      if IsSub then
        Route[#Route + 1] = FromCoord:WaypointNaval( Speed, depth )
        Route[#Route + 1] = ToCoord:WaypointNaval( Speed, Waypoint.alt )
      else
        Route[#Route + 1] = FromCoord:WaypointGround( Speed, Formation )
        Route[#Route + 1] = ToCoord:WaypointGround( Speed, Formation )
      end

      local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRouteRandom", Speed, Formation, ToWaypoint )

      PatrolGroup:SetTaskWaypoint( Route[#Route], TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone.

      PatrolGroup:Route( Route, 1 ) -- Move after a random seconds to the Route. See the Route method for details.
    end
  end

  --- (GROUND) Patrol randomly to the waypoints the for the (parent) group.
  -- A random waypoint will be picked and the group will move towards that point.
  -- @param #CONTROLLABLE self
  -- @param #table ZoneList Table of zones.
  -- @param #number Speed Speed in km/h the group moves at.
  -- @param #string Formation (Optional) Formation the group should use.
  -- @param #number DelayMin Delay in seconds before the group progresses to the next route point. Default 1 sec.
  -- @param #number DelayMax Max. delay in seconds. Actual delay is randomly chosen between DelayMin and DelayMax. Default equal to DelayMin.
  -- @return #CONTROLLABLE
  function CONTROLLABLE:PatrolZones( ZoneList, Speed, Formation, DelayMin, DelayMax )

    if type( ZoneList ) ~= "table" then
      ZoneList = { ZoneList }
    end

    local PatrolGroup = self -- Wrapper.Group#GROUP

    if not self:IsInstanceOf( "GROUP" ) then
      PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP
    end

    DelayMin = DelayMin or 1
    if not DelayMax or DelayMax < DelayMin then
      DelayMax = DelayMin
    end

    local Delay = math.random( DelayMin, DelayMax )

    self:F( { PatrolGroup = PatrolGroup:GetName() } )

    if PatrolGroup:IsGround() or PatrolGroup:IsShip() then

      -- Calculate the new Route.
      local FromCoord = PatrolGroup:GetCoordinate()

      -- test for submarine
      local depth = 0
      local IsSub = false
      if PatrolGroup:IsShip() then
        local navalvec3 = FromCoord:GetVec3()
        if navalvec3.y < 0 then
          depth = navalvec3.y
          IsSub = true
        end
      end

      -- Select a random Zone and get the Coordinate of the new Zone.
      local RandomZone = ZoneList[math.random( 1, #ZoneList )] -- Core.Zone#ZONE
      local ToCoord = RandomZone:GetRandomCoordinate( 10 )

      -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task
      local Route = {}
      if IsSub then
        Route[#Route + 1] = FromCoord:WaypointNaval( Speed, depth )
        Route[#Route + 1] = ToCoord:WaypointNaval( Speed, depth )
      else
        Route[#Route + 1] = FromCoord:WaypointGround( Speed, Formation )
        Route[#Route + 1] = ToCoord:WaypointGround( Speed, Formation )
      end

      local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolZones", ZoneList, Speed, Formation, DelayMin, DelayMax )

      PatrolGroup:SetTaskWaypoint( Route[#Route], TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone.

      PatrolGroup:Route( Route, Delay ) -- Move after a random seconds to the Route. See the Route method for details.
    end
  end

end


--- Return a "Misson" task to follow a given route defined by Points.
-- @param #CONTROLLABLE self
-- @param #table Points A table of route points.
-- @return DCS#Task DCS mission task. Has entries `.id="Mission"`, `params`, were params has entries `airborne` and `route`, which is a table of `points`.
function CONTROLLABLE:TaskRoute( Points )

  local DCSTask = {
    id = 'Mission',
    params = {
      airborne = self:IsAir(),  -- This is important to make aircraft land without respawning them (which was a long standing DCS issue).
      route = {points = Points},
    },
  }

  return DCSTask
end

do -- Route methods

  --- (AIR + GROUND) Make the Controllable move to fly to a given point.
  -- @param #CONTROLLABLE self
  -- @param DCS#Vec3 Point The destination point in Vec3 format.
  -- @param #number Speed The speed [m/s] to travel.
  -- @return #CONTROLLABLE self
  function CONTROLLABLE:RouteToVec2( Point, Speed )
    self:F2( { Point, Speed } )

    local ControllablePoint = self:GetUnit( 1 ):GetVec2()

    local PointFrom = {}
    PointFrom.x = ControllablePoint.x
    PointFrom.y = ControllablePoint.y
    PointFrom.type = "Turning Point"
    PointFrom.action = "Turning Point"
    PointFrom.speed = Speed
    PointFrom.speed_locked = true
    PointFrom.properties = {
      ["vnav"] = 1,
      ["scale"] = 0,
      ["angle"] = 0,
      ["vangle"] = 0,
      ["steer"] = 2,
    }

    local PointTo = {}
    PointTo.x = Point.x
    PointTo.y = Point.y
    PointTo.type = "Turning Point"
    PointTo.action = "Fly Over Point"
    PointTo.speed = Speed
    PointTo.speed_locked = true
    PointTo.properties = {
      ["vnav"] = 1,
      ["scale"] = 0,
      ["angle"] = 0,
      ["vangle"] = 0,
      ["steer"] = 2,
    }

    local Points = { PointFrom, PointTo }

    self:T3( Points )

    self:Route( Points )

    return self
  end

  --- (AIR + GROUND) Make the Controllable move to a given point.
  -- @param #CONTROLLABLE self
  -- @param DCS#Vec3 Point The destination point in Vec3 format.
  -- @param #number Speed The speed [m/s] to travel.
  -- @return #CONTROLLABLE self
  function CONTROLLABLE:RouteToVec3( Point, Speed )
    self:F2( { Point, Speed } )

    local ControllableVec3 = self:GetUnit( 1 ):GetVec3()

    local PointFrom = {}
    PointFrom.x = ControllableVec3.x
    PointFrom.y = ControllableVec3.z
    PointFrom.alt = ControllableVec3.y
    PointFrom.alt_type = "BARO"
    PointFrom.type = "Turning Point"
    PointFrom.action = "Turning Point"
    PointFrom.speed = Speed
    PointFrom.speed_locked = true
    PointFrom.properties = {
      ["vnav"] = 1,
      ["scale"] = 0,
      ["angle"] = 0,
      ["vangle"] = 0,
      ["steer"] = 2,
    }

    local PointTo = {}
    PointTo.x = Point.x
    PointTo.y = Point.z
    PointTo.alt = Point.y
    PointTo.alt_type = "BARO"
    PointTo.type = "Turning Point"
    PointTo.action = "Fly Over Point"
    PointTo.speed = Speed
    PointTo.speed_locked = true
    PointTo.properties = {
      ["vnav"] = 1,
      ["scale"] = 0,
      ["angle"] = 0,
      ["vangle"] = 0,
      ["steer"] = 2,
    }

    local Points = { PointFrom, PointTo }

    self:T3( Points )

    self:Route( Points )

    return self
  end

  --- Make the controllable to follow a given route.
  -- @param #CONTROLLABLE self
  -- @param #table Route A table of Route Points.
  -- @param #number DelaySeconds (Optional) Wait for the specified seconds before executing the Route. Default is one second.
  -- @return #CONTROLLABLE The CONTROLLABLE.
  function CONTROLLABLE:Route( Route, DelaySeconds )
    self:F2( Route )

    local DCSControllable = self:GetDCSObject()
    if DCSControllable then
      local RouteTask = self:TaskRoute( Route ) -- Create a RouteTask, that will route the CONTROLLABLE to the Route.
      self:SetTask( RouteTask, DelaySeconds or 1 ) -- Execute the RouteTask after the specified seconds (default is 1).
      return self
    end

    return nil
  end

  --- Make the controllable to push follow a given route.
  -- @param #CONTROLLABLE self
  -- @param #table Route A table of Route Points.
  -- @param #number DelaySeconds (Optional) Wait for the specified seconds before executing the Route. Default is one second.
  -- @return #CONTROLLABLE The CONTROLLABLE.
  function CONTROLLABLE:RoutePush( Route, DelaySeconds )
    self:F2( Route )

    local DCSControllable = self:GetDCSObject()
    if DCSControllable then
      local RouteTask = self:TaskRoute( Route ) -- Create a RouteTask, that will route the CONTROLLABLE to the Route.
      self:PushTask( RouteTask, DelaySeconds or 1 ) -- Execute the RouteTask after the specified seconds (default is 1).
      return self
    end

    return nil
  end

  --- Stops the movement of the vehicle on the route.
  -- @param #CONTROLLABLE self
  -- @return #CONTROLLABLE
  function CONTROLLABLE:RouteStop()
    self:F( self:GetName() .. " RouteStop" )

    local CommandStop = self:CommandStopRoute( true )
    self:SetCommand( CommandStop )

  end

  --- Resumes the movement of the vehicle on the route.
  -- @param #CONTROLLABLE self
  -- @return #CONTROLLABLE
  function CONTROLLABLE:RouteResume()
    self:F( self:GetName() .. " RouteResume" )

    local CommandResume = self:CommandStopRoute( false )
    self:SetCommand( CommandResume )

  end

  --- Make the GROUND Controllable to drive towards a specific point.
  -- @param #CONTROLLABLE self
  -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to.
  -- @param #number Speed (optional) Speed in km/h. The default speed is 20 km/h.
  -- @param #string Formation (optional) The route point Formation, which is a text string that specifies exactly the Text in the Type of the route point, like "Vee", "Echelon Right".
  -- @param #number DelaySeconds Wait for the specified seconds before executing the Route.
  -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{#CONTROLLABLE} object, the number of the waypoint and the total number of waypoints.
  -- @param #table WaypointFunctionArguments (Optional) List of parameters passed to the *WaypointFunction*.
  -- @return #CONTROLLABLE The CONTROLLABLE.
  function CONTROLLABLE:RouteGroundTo( ToCoordinate, Speed, Formation, DelaySeconds, WaypointFunction, WaypointFunctionArguments )

    local FromCoordinate = self:GetCoordinate()

    local FromWP = FromCoordinate:WaypointGround( Speed, Formation )
    local ToWP = ToCoordinate:WaypointGround( Speed, Formation )

    local route = { FromWP, ToWP }

    -- Add passing waypoint function.
    if WaypointFunction then
      local N = #route
      for n, waypoint in pairs( route ) do
        waypoint.task = {}
        waypoint.task.id = "ComboTask"
        waypoint.task.params = {}
        waypoint.task.params.tasks = { self:TaskFunction( "CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack( WaypointFunctionArguments or {} ) ) }
      end
    end

    self:Route( route, DelaySeconds )

    return self
  end

  --- Make the GROUND Controllable to drive towards a specific point using (mostly) roads.
  -- @param #CONTROLLABLE self
  -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to.
  -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h.
  -- @param #number DelaySeconds (Optional) Wait for the specified seconds before executing the Route. Default is one second.
  -- @param #string OffRoadFormation (Optional) The formation at initial and final waypoint. Default is "Off Road".
  -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{#CONTROLLABLE} object, the number of the waypoint and the total number of waypoints.
  -- @param #table WaypointFunctionArguments (Optional) List of parameters passed to the *WaypointFunction*.
  -- @return #CONTROLLABLE The CONTROLLABLE.
  function CONTROLLABLE:RouteGroundOnRoad( ToCoordinate, Speed, DelaySeconds, OffRoadFormation, WaypointFunction, WaypointFunctionArguments )

    -- Defaults.
    Speed = Speed or 20
    DelaySeconds = DelaySeconds or 1
    OffRoadFormation = OffRoadFormation or "Off Road"

    -- Get the route task.
    local route = self:TaskGroundOnRoad( ToCoordinate, Speed, OffRoadFormation, nil, nil, WaypointFunction, WaypointFunctionArguments )

    -- Route controllable to destination.
    self:Route( route, DelaySeconds )

    return self
  end

  --- Make the TRAIN Controllable to drive towards a specific point using railroads.
  -- @param #CONTROLLABLE self
  -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to.
  -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h.
  -- @param #number DelaySeconds (Optional) Wait for the specified seconds before executing the Route. Default is one second.
  -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{#CONTROLLABLE} object, the number of the waypoint and the total number of waypoints.
  -- @param #table WaypointFunctionArguments (Optional) List of parameters passed to the *WaypointFunction*.
  -- @return #CONTROLLABLE The CONTROLLABLE.
  function CONTROLLABLE:RouteGroundOnRailRoads( ToCoordinate, Speed, DelaySeconds, WaypointFunction, WaypointFunctionArguments )

    -- Defaults.
    Speed = Speed or 20
    DelaySeconds = DelaySeconds or 1

    -- Get the route task.
    local route = self:TaskGroundOnRailRoads( ToCoordinate, Speed, WaypointFunction, WaypointFunctionArguments )

    -- Route controllable to destination.
    self:Route( route, DelaySeconds )

    return self
  end

  --- Make a task for a GROUND Controllable to drive towards a specific point using (mostly) roads.
  -- @param #CONTROLLABLE self
  -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to.
  -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h.
  -- @param #string OffRoadFormation (Optional) The formation at initial and final waypoint. Default is "Off Road".
  -- @param #boolean Shortcut (Optional) If true, controllable will take the direct route if the path on road is 10x longer or path on road is less than 5% of total path.
  -- @param Core.Point#COORDINATE FromCoordinate (Optional) Explicit initial coordinate. Default is the position of the controllable.
  -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{#CONTROLLABLE} object, the number of the waypoint and the total number of waypoints.
  -- @param #table WaypointFunctionArguments (Optional) List of parameters passed to the *WaypointFunction*.
  -- @return DCS#Task Task.
  -- @return #boolean If true, path on road is possible. If false, task will route the group directly to its destination.
  function CONTROLLABLE:TaskGroundOnRoad( ToCoordinate, Speed, OffRoadFormation, Shortcut, FromCoordinate, WaypointFunction, WaypointFunctionArguments )
    self:T( { ToCoordinate = ToCoordinate, Speed = Speed, OffRoadFormation = OffRoadFormation, WaypointFunction = WaypointFunction, Args = WaypointFunctionArguments } )

    -- Defaults.
    Speed = Speed or 20
    OffRoadFormation = OffRoadFormation or "Off Road"

    -- Initial (current) coordinate.
    FromCoordinate = FromCoordinate or self:GetCoordinate()

    -- Get path and path length on road including the end points (From and To).
    local PathOnRoad, LengthOnRoad, GotPath = FromCoordinate:GetPathOnRoad( ToCoordinate, true )

    -- Get the length only(!) on the road.
    local _, LengthRoad = FromCoordinate:GetPathOnRoad( ToCoordinate, false )

    -- Off road part of the rout: Total=OffRoad+OnRoad.
    local LengthOffRoad
    local LongRoad

    -- Calculate the direct distance between the initial and final points.
    local LengthDirect = FromCoordinate:Get2DDistance( ToCoordinate )

    if GotPath and LengthRoad then

      -- Off road part of the rout: Total=OffRoad+OnRoad.
      LengthOffRoad = LengthOnRoad - LengthRoad

      -- Length on road is 10 times longer than direct route or path on road is very short (<5% of total path).
      LongRoad = LengthOnRoad and ((LengthOnRoad > LengthDirect * 10) or (LengthRoad / LengthOnRoad * 100 < 5))

      -- Debug info.
      self:T( string.format( "Length on road   = %.3f km", LengthOnRoad / 1000 ) )
      self:T( string.format( "Length directly  = %.3f km", LengthDirect / 1000 ) )
      self:T( string.format( "Length fraction  = %.3f km", LengthOnRoad / LengthDirect ) )
      self:T( string.format( "Length only road = %.3f km", LengthRoad / 1000 ) )
      self:T( string.format( "Length off road  = %.3f km", LengthOffRoad / 1000 ) )
      self:T( string.format( "Percent on road  = %.1f", LengthRoad / LengthOnRoad * 100 ) )

    end

    -- Route, ground waypoints along road.
    local route = {}
    local canroad = false

    -- Check if a valid path on road could be found.
    if GotPath and LengthRoad and LengthDirect > 2000 then -- if the length of the movement is less than 1 km, drive directly.
      -- Check whether the road is very long compared to direct path.
      if LongRoad and Shortcut then

        -- Road is long ==> we take the short cut.

        table.insert( route, FromCoordinate:WaypointGround( Speed, OffRoadFormation ) )
        table.insert( route, ToCoordinate:WaypointGround( Speed, OffRoadFormation ) )

      else

        -- Create waypoints.
        table.insert( route, FromCoordinate:WaypointGround( Speed, OffRoadFormation ) )
        table.insert( route, PathOnRoad[2]:WaypointGround( Speed, "On Road" ) )
        table.insert( route, PathOnRoad[#PathOnRoad - 1]:WaypointGround( Speed, "On Road" ) )

        -- Add the final coordinate because the final might not be on the road.
        local dist = ToCoordinate:Get2DDistance( PathOnRoad[#PathOnRoad - 1] )
        if dist > 10 then
          table.insert( route, ToCoordinate:WaypointGround( Speed, OffRoadFormation ) )
          table.insert( route, ToCoordinate:GetRandomCoordinateInRadius( 10, 5 ):WaypointGround( 5, OffRoadFormation ) )
          table.insert( route, ToCoordinate:GetRandomCoordinateInRadius( 10, 5 ):WaypointGround( 5, OffRoadFormation ) )
        end

      end

      canroad = true
    else

      -- No path on road could be found (can happen!) ==> Route group directly from A to B.
      table.insert( route, FromCoordinate:WaypointGround( Speed, OffRoadFormation ) )
      table.insert( route, ToCoordinate:WaypointGround( Speed, OffRoadFormation ) )

    end

    -- Add passing waypoint function.
    if WaypointFunction then
      local N = #route
      for n, waypoint in pairs( route ) do
        waypoint.task = {}
        waypoint.task.id = "ComboTask"
        waypoint.task.params = {}
        waypoint.task.params.tasks = { self:TaskFunction( "CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack( WaypointFunctionArguments or {} ) ) }
      end
    end

    return route, canroad
  end

  --- Make a task for a TRAIN Controllable to drive towards a specific point using railroad.
  -- @param #CONTROLLABLE self
  -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to.
  -- @param #number Speed (Optional) Speed in km/h. The default speed is 20 km/h.
  -- @param #function WaypointFunction (Optional) Function called when passing a waypoint. First parameters of the function are the @{#CONTROLLABLE} object, the number of the waypoint and the total number of waypoints.
  -- @param #table WaypointFunctionArguments (Optional) List of parameters passed to the *WaypointFunction*.
  -- @return Task
  function CONTROLLABLE:TaskGroundOnRailRoads( ToCoordinate, Speed, WaypointFunction, WaypointFunctionArguments )
    self:F2( { ToCoordinate = ToCoordinate, Speed = Speed } )

    -- Defaults.
    Speed = Speed or 20

    -- Current coordinate.
    local FromCoordinate = self:GetCoordinate()

    -- Get path and path length on railroad.
    local PathOnRail, LengthOnRail = FromCoordinate:GetPathOnRoad( ToCoordinate, false, true )

    -- Debug info.
    self:T( string.format( "Length on railroad = %.3f km", LengthOnRail / 1000 ) )

    -- Route, ground waypoints along road.
    local route = {}

    -- Check if a valid path on railroad could be found.
    if PathOnRail then

      table.insert( route, PathOnRail[1]:WaypointGround( Speed, "On Railroad" ) )
      table.insert( route, PathOnRail[2]:WaypointGround( Speed, "On Railroad" ) )

    end

    -- Add passing waypoint function.
    if WaypointFunction then
      local N = #route
      for n, waypoint in pairs( route ) do
        waypoint.task = {}
        waypoint.task.id = "ComboTask"
        waypoint.task.params = {}
        waypoint.task.params.tasks = { self:TaskFunction( "CONTROLLABLE.___PassingWaypoint", n, N, WaypointFunction, unpack( WaypointFunctionArguments or {} ) ) }
      end
    end

    return route
  end

  --- Task function when controllable passes a waypoint.
  -- @param #CONTROLLABLE controllable The controllable object.
  -- @param #number n Current waypoint number passed.
  -- @param #number N Total number of waypoints.
  -- @param #function waypointfunction Function called when a waypoint is passed.
  function CONTROLLABLE.___PassingWaypoint( controllable, n, N, waypointfunction, ... )
    waypointfunction( controllable, n, N, ... )
  end

  --- Make the AIR Controllable fly towards a specific point.
  -- @param #CONTROLLABLE self
  -- @param Core.Point#COORDINATE ToCoordinate A Coordinate to drive to.
  -- @param Core.Point#COORDINATE.RoutePointAltType AltType The altitude type.
  -- @param Core.Point#COORDINATE.RoutePointType Type The route point type.
  -- @param Core.Point#COORDINATE.RoutePointAction Action The route point action.
  -- @param #number Speed (optional) Speed in km/h. The default speed is 500 km/h.
  -- @param #number DelaySeconds Wait for the specified seconds before executing the Route.
  -- @return #CONTROLLABLE The CONTROLLABLE.
  function CONTROLLABLE:RouteAirTo( ToCoordinate, AltType, Type, Action, Speed, DelaySeconds )

    local FromCoordinate = self:GetCoordinate()
    local FromWP = FromCoordinate:WaypointAir()

    local ToWP = ToCoordinate:WaypointAir( AltType, Type, Action, Speed )

    self:Route( { FromWP, ToWP }, DelaySeconds )

    return self
  end

  --- (AIR + GROUND) Route the controllable to a given zone.
  -- The controllable final destination point can be randomized.
  -- A speed can be given in km/h.
  -- A given formation can be given.
  -- @param #CONTROLLABLE self
  -- @param Core.Zone#ZONE Zone The zone where to route to.
  -- @param #boolean Randomize Defines whether to target point gets randomized within the Zone.
  -- @param #number Speed The speed in m/s. Default is 5.555 m/s = 20 km/h.
  -- @param Core.Base#FORMATION Formation The formation string.
  function CONTROLLABLE:TaskRouteToZone( Zone, Randomize, Speed, Formation )
    self:F2( Zone )

    local DCSControllable = self:GetDCSObject()

    if DCSControllable then

      local ControllablePoint = self:GetVec2()

      local PointFrom = {}
      PointFrom.x = ControllablePoint.x
      PointFrom.y = ControllablePoint.y
      PointFrom.type = "Turning Point"
      PointFrom.action = Formation or "Cone"
      PointFrom.speed = 20 / 3.6

      local PointTo = {}
      local ZonePoint

      if Randomize then
        ZonePoint = Zone:GetRandomVec2()
      else
        ZonePoint = Zone:GetVec2()
      end

      PointTo.x = ZonePoint.x
      PointTo.y = ZonePoint.y
      PointTo.type = "Turning Point"

      if Formation then
        PointTo.action = Formation
      else
        PointTo.action = "Cone"
      end

      if Speed then
        PointTo.speed = Speed
      else
        PointTo.speed = 20 / 3.6
      end

      local Points = { PointFrom, PointTo }

      self:T3( Points )

      self:Route( Points )

      return self
    end

    return nil
  end

  --- (GROUND) Route the controllable to a given Vec2.
  -- A speed can be given in km/h.
  -- A given formation can be given.
  -- @param #CONTROLLABLE self
  -- @param DCS#Vec2 Vec2 The Vec2 where to route to.
  -- @param #number Speed The speed in m/s. Default is 5.555 m/s = 20 km/h.
  -- @param Core.Base#FORMATION Formation The formation string.
  function CONTROLLABLE:TaskRouteToVec2( Vec2, Speed, Formation )

    local DCSControllable = self:GetDCSObject()

    if DCSControllable then

      local ControllablePoint = self:GetVec2()

      local PointFrom = {}
      PointFrom.x = ControllablePoint.x
      PointFrom.y = ControllablePoint.y
      PointFrom.type = "Turning Point"
      PointFrom.action = Formation or "Cone"
      PointFrom.speed = 20 / 3.6

      local PointTo = {}

      PointTo.x = Vec2.x
      PointTo.y = Vec2.y
      PointTo.type = "Turning Point"

      if Formation then
        PointTo.action = Formation
      else
        PointTo.action = "Cone"
      end

      if Speed then
        PointTo.speed = Speed
      else
        PointTo.speed = 20 / 3.6
      end

      local Points = { PointFrom, PointTo }

      self:T3( Points )

      self:Route( Points )

      return self
    end

    return nil
  end

end -- Route methods

-- Commands

--- Do Script command
-- @param #CONTROLLABLE self
-- @param #string DoScript
-- @return DCS#DCSCommand
function CONTROLLABLE:CommandDoScript( DoScript )

  local DCSDoScript = {
    id = "Script",
    params = {
      command = DoScript,
    },
  }

  self:T3( DCSDoScript )
  return DCSDoScript
end

--- Return the mission template of the controllable.
-- @param #CONTROLLABLE self
-- @return #table The MissionTemplate
-- TODO: Rework the method how to retrieve a template ...
function CONTROLLABLE:GetTaskMission()
  self:F2( self.ControllableName )

  return UTILS.DeepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template )
end

--- Return the mission route of the controllable.
-- @param #CONTROLLABLE self
-- @return #table The mission route defined by points.
function CONTROLLABLE:GetTaskRoute()
  self:F2( self.ControllableName )

  return UTILS.DeepCopy( _DATABASE.Templates.Controllables[self.ControllableName].Template.route.points )
end

--- Return the route of a controllable by using the @{Core.Database#DATABASE} class.
-- @param #CONTROLLABLE self
-- @param #number Begin The route point from where the copy will start. The base route point is 0.
-- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0.
-- @param #boolean Randomize Randomization of the route, when true.
-- @param #number Radius When randomization is on, the randomization is within the radius.
function CONTROLLABLE:CopyRoute( Begin, End, Randomize, Radius )
  self:F2( { Begin, End } )

  local Points = {}

  -- Could be a Spawned Controllable
  local ControllableName = string.match( self:GetName(), ".*#" )
  if ControllableName then
    ControllableName = ControllableName:sub( 1, -2 )
  else
    ControllableName = self:GetName()
  end

  self:T3( { ControllableName } )

  local Template = _DATABASE.Templates.Controllables[ControllableName].Template

  if Template then
    if not Begin then
      Begin = 0
    end
    if not End then
      End = 0
    end

    for TPointID = Begin + 1, #Template.route.points - End do
      if Template.route.points[TPointID] then
        Points[#Points + 1] = UTILS.DeepCopy( Template.route.points[TPointID] )
        if Randomize then
          if not Radius then
            Radius = 500
          end
          Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius )
          Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius )
        end
      end
    end
    return Points
  else
    error( "Template not found for Controllable : " .. ControllableName )
  end

  return nil
end

--- Return the detected targets of the controllable.
-- The optional parameters specify the detection methods that can be applied.
-- If no detection method is given, the detection will use all the available methods by default.
-- @param #CONTROLLABLE self
-- @param #boolean DetectVisual (optional)
-- @param #boolean DetectOptical (optional)
-- @param #boolean DetectRadar (optional)
-- @param #boolean DetectIRST (optional)
-- @param #boolean DetectRWR (optional)
-- @param #boolean DetectDLINK (optional)
-- @return #table DetectedTargets
function CONTROLLABLE:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK )
  self:F2( self.ControllableName )

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then

    local DetectionVisual = (DetectVisual and DetectVisual == true) and Controller.Detection.VISUAL or nil
    local DetectionOptical = (DetectOptical and DetectOptical == true) and Controller.Detection.OPTIC or nil
    local DetectionRadar = (DetectRadar and DetectRadar == true) and Controller.Detection.RADAR or nil
    local DetectionIRST = (DetectIRST and DetectIRST == true) and Controller.Detection.IRST or nil
    local DetectionRWR = (DetectRWR and DetectRWR == true) and Controller.Detection.RWR or nil
    local DetectionDLINK = (DetectDLINK and DetectDLINK == true) and Controller.Detection.DLINK or nil

    local Params = {}
    if DetectionVisual then
      Params[#Params + 1] = DetectionVisual
    end
    if DetectionOptical then
      Params[#Params + 1] = DetectionOptical
    end
    if DetectionRadar then
      Params[#Params + 1] = DetectionRadar
    end
    if DetectionIRST then
      Params[#Params + 1] = DetectionIRST
    end
    if DetectionRWR then
      Params[#Params + 1] = DetectionRWR
    end
    if DetectionDLINK then
      Params[#Params + 1] = DetectionDLINK
    end

    self:T2( { DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK } )

    return self:_GetController():getDetectedTargets( Params[1], Params[2], Params[3], Params[4], Params[5], Params[6] )
  end

  return nil
end

--- Check if a DCS object (unit or static) is detected by the controllable. 
-- Note that after a target is detected it remains "detected" for a certain amount of time, even if the controllable cannot "see" the target any more with it's sensors.
-- The optional parametes specify the detection methods that can be applied.
-- 
-- If **no** detection method is given, the detection will use **all** the available methods by default.
-- If **at least one** detection method is specified, only the methods set to *true* will be used.
-- @param #CONTROLLABLE self
-- @param DCS#Object DCSObject The DCS object that is checked.
-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets.
-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets.
-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar.
-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST.
-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR.
-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link.
-- @return #boolean `true` if target is detected.
-- @return #boolean `true` if target is *currently* visible by line of sight. Target must be detected (first parameter returns `true`).
-- @return #boolean `true` if target type is known. Target must be detected (first parameter returns `true`).
-- @return #boolean `true` if distance to target is known. Target must be detected (first parameter returns `true`).
-- @return #number Mission time in seconds when target was last detected. Only present if the target is currently not visible (second parameter returns `false`) otherwise `nil` is returned.
-- @return DCS#Vec3 Last known position vector of the target. Only present if the target is currently not visible (second parameter returns `false`) otherwise `nil` is returned.
-- @return DCS#Vec3 Last known velocity vector of the target. Only present if the target is currently not visible (second parameter returns `false`) otherwise `nil` is returned.
function CONTROLLABLE:IsTargetDetected( DCSObject, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK )
  self:F2( self.ControllableName )

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then

    local DetectionVisual = (DetectVisual and DetectVisual == true) and Controller.Detection.VISUAL or nil
    local DetectionOptical = (DetectOptical and DetectOptical == true) and Controller.Detection.OPTIC or nil
    local DetectionRadar = (DetectRadar and DetectRadar == true) and Controller.Detection.RADAR or nil
    local DetectionIRST = (DetectIRST and DetectIRST == true) and Controller.Detection.IRST or nil
    local DetectionRWR = (DetectRWR and DetectRWR == true) and Controller.Detection.RWR or nil
    local DetectionDLINK = (DetectDLINK and DetectDLINK == true) and Controller.Detection.DLINK or nil

    local Controller = self:_GetController()

    local TargetIsDetected, TargetIsVisible, TargetKnowType, TargetKnowDistance, TargetLastTime, TargetLastPos, TargetLastVelocity
      = Controller:isTargetDetected( DCSObject, DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK )

    return TargetIsDetected, TargetIsVisible, TargetKnowType, TargetKnowDistance, TargetLastTime, TargetLastPos, TargetLastVelocity
  end

  return nil
end

--- Check if a certain UNIT is detected by the controllable.
-- The optional parametes specify the detection methods that can be applied.
-- 
-- If **no** detection method is given, the detection will use **all** the available methods by default.
-- If **at least one** detection method is specified, only the methods set to *true* will be used.
-- @param #CONTROLLABLE self
-- @param Wrapper.Unit#UNIT Unit The unit that is supposed to be detected.
-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets.
-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets.
-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar.
-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST.
-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR.
-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link.
-- @return #boolean `true` if target is detected.
-- @return #boolean `true` if target is *currently* visible by line of sight. Target must be detected (first parameter returns `true`).
-- @return #boolean `true` if target type is known. Target must be detected (first parameter returns `true`).
-- @return #boolean `true` if distance to target is known. Target must be detected (first parameter returns `true`).
-- @return #number Mission time in seconds when target was last detected. Only present if the target is currently not visible (second parameter returns `false`) otherwise `nil` is returned.
-- @return DCS#Vec3 Last known position vector of the target. Only present if the target is currently not visible (second parameter returns `false`) otherwise `nil` is returned.
-- @return DCS#Vec3 Last known velocity vector of the target. Only present if the target is currently not visible (second parameter returns `false`) otherwise `nil` is returned.
function CONTROLLABLE:IsUnitDetected( Unit, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK )
  self:F2( self.ControllableName )

  if Unit and Unit:IsAlive() then
    return self:IsTargetDetected( Unit:GetDCSObject(), DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK )
  end

  return nil
end

--- Check if a certain GROUP is detected by the controllable.
-- The optional parametes specify the detection methods that can be applied.
-- If **no** detection method is given, the detection will use **all** the available methods by default.
-- If **at least one** detection method is specified, only the methods set to *true* will be used.
-- @param #CONTROLLABLE self
-- @param Wrapper.Group#GROUP Group The group that is supposed to be detected.
-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets.
-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets.
-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar.
-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST.
-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR.
-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link.
-- @return #boolean True if any unit of the group is detected.
function CONTROLLABLE:IsGroupDetected( Group, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK )
  self:F2( self.ControllableName )

  if Group and Group:IsAlive() then
    for _, _unit in pairs( Group:GetUnits() ) do
      local unit = _unit -- Wrapper.Unit#UNIT
      if unit and unit:IsAlive() then

        local isdetected = self:IsUnitDetected( unit, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK )

        if isdetected then
          return true
        end
      end
    end
    return false
  end

  return nil
end

--- Return the detected targets of the controllable.
-- The optional parametes specify the detection methods that can be applied.
-- If **no** detection method is given, the detection will use **all** the available methods by default.
-- If **at least one** detection method is specified, only the methods set to *true* will be used.
-- @param #CONTROLLABLE self
-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets.
-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets.
-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar.
-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST.
-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR.
-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link.
-- @return Core.Set#SET_UNIT Set of detected units.
function CONTROLLABLE:GetDetectedUnitSet( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK )

  -- Get detected DCS units.
  local detectedtargets = self:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK )

  local unitset = SET_UNIT:New()

  for DetectionObjectID, Detection in pairs( detectedtargets or {} ) do
    local DetectedObject = Detection.object -- DCS#Object

    if DetectedObject and DetectedObject:isExist() and DetectedObject.id_ < 50000000 then
      local unit = UNIT:Find( DetectedObject )

      if unit and unit:IsAlive() then

        if not unitset:FindUnit( unit:GetName() ) then
          unitset:AddUnit( unit )
        end

      end
    end
  end

  return unitset
end

--- Return the detected target groups of the controllable as a @{Core.Set#SET_GROUP}.
-- The optional parametes specify the detection methods that can be applied.
-- If no detection method is given, the detection will use all the available methods by default.
-- @param #CONTROLLABLE self
-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets.
-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets.
-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar.
-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST.
-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR.
-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link.
-- @return Core.Set#SET_GROUP Set of detected groups.
function CONTROLLABLE:GetDetectedGroupSet( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK )

  -- Get detected DCS units.
  local detectedtargets = self:GetDetectedTargets( DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK )

  local groupset = SET_GROUP:New()

  for DetectionObjectID, Detection in pairs( detectedtargets or {} ) do
    local DetectedObject = Detection.object -- DCS#Object

    if DetectedObject and DetectedObject:isExist() and DetectedObject.id_ < 50000000 then
      local unit = UNIT:Find( DetectedObject )

      if unit and unit:IsAlive() then
        local group = unit:GetGroup()

        if group and not groupset:FindGroup( group:GetName() ) then
          groupset:AddGroup( group )
        end

      end
    end
  end

  return groupset
end

-- Options

--- Set option.
-- @param #CONTROLLABLE self
-- @param #number OptionID ID/Type of the option.
-- @param #number OptionValue Value of the option
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOption( OptionID, OptionValue )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    Controller:setOption( OptionID, OptionValue )

    return self
  end

  return nil
end

--- Set option for Rules of Engagement (ROE).
-- @param #CONTROLLABLE self
-- @param #number ROEvalue ROE value. See ENUMS.ROE.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionROE( ROEvalue )

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then

    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.ROE, ROEvalue )
    elseif self:IsGround() then
      Controller:setOption( AI.Option.Ground.id.ROE, ROEvalue )
    elseif self:IsShip() then
      Controller:setOption( AI.Option.Naval.id.ROE, ROEvalue )
    end

    return self
  end

  return nil
end

--- Can the CONTROLLABLE hold their weapons?
-- @param #CONTROLLABLE self
-- @return #boolean
function CONTROLLABLE:OptionROEHoldFirePossible()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    if self:IsAir() or self:IsGround() or self:IsShip() then
      return true
    end

    return false
  end

  return nil
end

--- Weapons Hold: AI will hold fire under all circumstances.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionROEHoldFire()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD )
    elseif self:IsGround() then
      Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.WEAPON_HOLD )
    elseif self:IsShip() then
      Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.WEAPON_HOLD )
    end

    return self
  end

  return nil
end

--- Can the CONTROLLABLE attack returning on enemy fire?
-- @param #CONTROLLABLE self
-- @return #boolean
function CONTROLLABLE:OptionROEReturnFirePossible()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    if self:IsAir() or self:IsGround() or self:IsShip() then
      return true
    end

    return false
  end

  return nil
end

--- Return Fire: AI will only engage threats that shoot first.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionROEReturnFire()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.RETURN_FIRE )
    elseif self:IsGround() then
      Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.RETURN_FIRE )
    elseif self:IsShip() then
      Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.RETURN_FIRE )
    end

    return self
  end

  return nil
end

--- Can the CONTROLLABLE attack designated targets?
-- @param #CONTROLLABLE self
-- @return #boolean
function CONTROLLABLE:OptionROEOpenFirePossible()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    if self:IsAir() or self:IsGround() or self:IsShip() then
      return true
    end

    return false
  end

  return nil
end

--- Open Fire (Only Designated): AI will engage only targets specified in its taskings.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionROEOpenFire()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE )
    elseif self:IsGround() then
      Controller:setOption( AI.Option.Ground.id.ROE, AI.Option.Ground.val.ROE.OPEN_FIRE )
    elseif self:IsShip() then
      Controller:setOption( AI.Option.Naval.id.ROE, AI.Option.Naval.val.ROE.OPEN_FIRE )
    end

    return self
  end

  return nil
end

--- Can the CONTROLLABLE attack priority designated targets? Only for AIR!
-- @param #CONTROLLABLE self
-- @return #boolean
function CONTROLLABLE:OptionROEOpenFireWeaponFreePossible()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    if self:IsAir() then
      return true
    end

    return false
  end

  return nil
end

--- Open Fire, Weapons Free (Priority Designated): AI will engage any enemy group it detects, but will prioritize targets specified in the groups tasking.
-- **Only for AIR units!**
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionROEOpenFireWeaponFree()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE_WEAPON_FREE )
    end

    return self
  end

  return nil
end

--- Can the CONTROLLABLE attack targets of opportunity?
-- @param #CONTROLLABLE self
-- @return #boolean
function CONTROLLABLE:OptionROEWeaponFreePossible()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    if self:IsAir() then
      return true
    end

    return false
  end

  return nil
end

--- Weapon free.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionROEWeaponFree()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_FREE )
    end

    return self
  end

  return nil
end

--- Can the CONTROLLABLE ignore enemy fire?
-- @param #CONTROLLABLE self
-- @return #boolean
function CONTROLLABLE:OptionROTNoReactionPossible()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    if self:IsAir() then
      return true
    end

    return false
  end

  return nil
end

--- No evasion on enemy threats.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionROTNoReaction()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.NO_REACTION )
    end

    return self
  end

  return nil
end

--- Set Reation On Threat behaviour.
-- @param #CONTROLLABLE self
-- @param #number ROTvalue ROT value. See ENUMS.ROT.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionROT( ROTvalue )
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, ROTvalue )
    end

    return self
  end

  return nil
end

--- Can the CONTROLLABLE evade using passive defenses?
-- @param #CONTROLLABLE self
-- @return #boolean
function CONTROLLABLE:OptionROTPassiveDefensePossible()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    if self:IsAir() then
      return true
    end

    return false
  end

  return nil
end

--- Evasion passive defense.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionROTPassiveDefense()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.PASSIVE_DEFENCE )
    end

    return self
  end

  return nil
end

--- Helicopter - prefer vertical landing.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionPreferVerticalLanding()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.PREFER_VERTICAL, true )
    end

    return self
  end

  return nil
end

--- Can the CONTROLLABLE evade on enemy fire?
-- @param #CONTROLLABLE self
-- @return #boolean
function CONTROLLABLE:OptionROTEvadeFirePossible()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    if self:IsAir() then
      return true
    end

    return false
  end

  return nil
end

--- Evade on fire.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionROTEvadeFire()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE )
    end

    return self
  end

  return nil
end

--- Can the CONTROLLABLE evade on fire using vertical manoeuvres?
-- @param #CONTROLLABLE self
-- @return #boolean
function CONTROLLABLE:OptionROTVerticalPossible()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    if self:IsAir() then
      return true
    end

    return false
  end

  return nil
end

--- Evade on fire using vertical manoeuvres.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionROTVertical()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE )
    end

    return self
  end

  return nil
end

--- Alarm state to Auto: AI will automatically switch alarm states based on the presence of threats. The AI kind of cheats in this regard.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionAlarmStateAuto()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsGround() then
      Controller:setOption( AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.AUTO )
    elseif self:IsShip() then
      -- Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.AUTO)
      Controller:setOption( 9, 0 )
    end

    return self
  end

  return nil
end

--- Alarm state to Green: Group is not combat ready. Sensors are stowed if possible.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionAlarmStateGreen()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsGround() then
      Controller:setOption( AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.GREEN )
    elseif self:IsShip() then
      -- AI.Option.Naval.id.ALARM_STATE does not seem to exist!
      -- Controller:setOption( AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.GREEN )
      Controller:setOption( 9, 1 )
    end

    return self
  end

  return nil
end

--- Alarm state to Red: Group is combat ready and actively searching for targets.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionAlarmStateRed()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsGround() then
      Controller:setOption( AI.Option.Ground.id.ALARM_STATE, AI.Option.Ground.val.ALARM_STATE.RED )
    elseif self:IsShip() then
      -- Controller:setOption(AI.Option.Naval.id.ALARM_STATE, AI.Option.Naval.val.ALARM_STATE.RED)
      Controller:setOption( 9, 2 )
    end

    return self
  end

  return nil
end

--- Set RTB on bingo fuel.
-- @param #CONTROLLABLE self
-- @param #boolean RTB true if RTB on bingo fuel (default), false if no RTB on bingo fuel.
-- Warning! When you switch this option off, the airborne group will continue to fly until all fuel has been consumed, and will crash.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionRTBBingoFuel( RTB ) -- R2.2
  self:F2( { self.ControllableName } )

  -- RTB = RTB or true
  if RTB == nil then
    RTB = true
  end

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.RTB_ON_BINGO, RTB )
    end

    return self
  end

  return nil
end

--- Set RTB on ammo.
-- @param #CONTROLLABLE self
-- @param #boolean WeaponsFlag Weapons.flag enumerator.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionRTBAmmo( WeaponsFlag )
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.RTB_ON_OUT_OF_AMMO, WeaponsFlag )
    end

    return self
  end

  return nil
end

--- Allow to Jettison of weapons upon threat.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionAllowJettisonWeaponsOnThreat()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.PROHIBIT_JETT, false )
    end

    return self
  end

  return nil
end

--- Keep weapons upon threat.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionKeepWeaponsOnThreat()
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.PROHIBIT_JETT, true )
    end

    return self
  end

  return nil
end

--- Prohibit Afterburner.
-- @param #CONTROLLABLE self
-- @param #boolean Prohibit If true or nil, prohibit. If false, do not prohibit.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionProhibitAfterburner( Prohibit )
  self:F2( { self.ControllableName } )

  if Prohibit == nil then
    Prohibit = true
  end

  if self:IsAir() then
    self:SetOption( AI.Option.Air.id.PROHIBIT_AB, Prohibit )
  end

  return self
end

--- [Ground] Allows AI radar units to take defensive actions to avoid anti radiation missiles. Units are allowed to shut radar off and displace. 
-- @param #CONTROLLABLE self
-- @param #number Seconds Can be - nil, 0 or false = switch off this option, any positive number = number of seconds the escape sequency runs.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionEvasionOfARM(Seconds)
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsGround() then
      if Seconds == nil then Seconds = false end
      Controller:setOption( AI.Option.Ground.id.EVASION_OF_ARM, Seconds)
    end

  end

  return self
end

--- [Ground] Option that defines the vehicle spacing when in an on road and off road formation. 
-- @param #CONTROLLABLE self
-- @param #number meters Can be zero to 100 meters. Defaults to 50 meters.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionFormationInterval(meters)
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsGround() then
      if meters == nil or meters > 100 or meters < 0 then meters = 50 end
      Controller:setOption( 30, meters)
    end

  end

  return self
end

--- [Air] Defines the usage of Electronic Counter Measures by airborne forces.
-- @param #CONTROLLABLE self
-- @param #number ECMvalue Can be - 0=Never on, 1=if locked by radar, 2=if detected by radar, 3=always on, defaults to 1
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionECM( ECMvalue )
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()

    if self:IsAir() then
      Controller:setOption( AI.Option.Air.id.ECM_USING, ECMvalue or 1 )
    end

  end

  return self
end

--- [Air] Defines the usage of Electronic Counter Measures by airborne forces. Disables the ability for AI to use their ECM.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionECM_Never()
  self:F2( { self.ControllableName } )

  self:OptionECM(0)

  return self
end

--- [Air] Defines the usage of Electronic Counter Measures by airborne forces. If the AI is actively being locked by an enemy radar they will enable their ECM jammer.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionECM_OnlyLockByRadar()
  self:F2( { self.ControllableName } )

  self:OptionECM(1)

  return self
end

--- [Air] Defines the usage of Electronic Counter Measures by airborne forces. If the AI is being detected by a radar they will enable their ECM.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionECM_DetectedLockByRadar()
  self:F2( { self.ControllableName } )

  self:OptionECM(2)

  return self
end

--- [Air] Defines the usage of Electronic Counter Measures by airborne forces. AI will leave their ECM on all the time.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionECM_AlwaysOn()
  self:F2( { self.ControllableName } )

  self:OptionECM(3)

  return self
end

--- Retrieve the controllable mission and allow to place function hooks within the mission waypoint plan.
-- Use the method @{#CONTROLLABLE.WayPointFunction}() to define the hook functions for specific waypoints.
-- Use the method @{#CONTROLLABLE.WayPointExecute}() to start the execution of the new mission plan.
-- Note that when WayPointInitialize is called, the Mission of the controllable is RESTARTED!
-- @param #CONTROLLABLE self
-- @param #table WayPoints If WayPoints is given, then use the route.
-- @return #CONTROLLABLE self
-- @usage Intended Workflow is:
-- mygroup:WayPointInitialize()
-- mygroup:WayPointFunction( WayPoint, WayPointIndex, WayPointFunction, ... )
-- mygroup:WayPointExecute()
function CONTROLLABLE:WayPointInitialize( WayPoints )
  self:F( { WayPoints } )

  if WayPoints then
    self.WayPoints = WayPoints
  else
    self.WayPoints = self:GetTaskRoute()
  end

  return self
end

--- Get the current WayPoints set with the WayPoint functions( Note that the WayPoints can be nil, although there ARE waypoints).
-- @param #CONTROLLABLE self
-- @return #table WayPoints If WayPoints is given, then return the WayPoints structure.
function CONTROLLABLE:GetWayPoints()
  self:F()

  if self.WayPoints then
    return self.WayPoints
  end

  return nil
end

--- Registers a waypoint function that will be executed when the controllable moves over the WayPoint.
-- @param #CONTROLLABLE self
-- @param #number WayPoint The waypoint number. Note that the start waypoint on the route is WayPoint 1!
-- @param #number WayPointIndex When defining multiple WayPoint functions for one WayPoint, use WayPointIndex to set the sequence of actions.
-- @param #function WayPointFunction The waypoint function to be called when the controllable moves over the waypoint. The waypoint function takes variable parameters.
-- @return #CONTROLLABLE self
-- @usage Intended Workflow is:
-- mygroup:WayPointInitialize()
-- mygroup:WayPointFunction( WayPoint, WayPointIndex, WayPointFunction, ... )
-- mygroup:WayPointExecute()
function CONTROLLABLE:WayPointFunction( WayPoint, WayPointIndex, WayPointFunction, ... )
  self:F2( { WayPoint, WayPointIndex, WayPointFunction } )
  if not self.WayPoints then
    self:WayPointInitialize()
  end
  table.insert( self.WayPoints[WayPoint].task.params.tasks, WayPointIndex )
  self.WayPoints[WayPoint].task.params.tasks[WayPointIndex] = self:TaskFunction( WayPointFunction, arg )
  return self
end

--- Executes the WayPoint plan.
-- The function gets a WayPoint parameter, that you can use to restart the mission at a specific WayPoint.
-- Note that when the WayPoint parameter is used, the new start mission waypoint of the controllable will be 1!
-- @param #CONTROLLABLE self
-- @param #number WayPoint The WayPoint from where to execute the mission.
-- @param #number WaitTime The amount seconds to wait before initiating the mission.
-- @return #CONTROLLABLE self
-- @usage Intended Workflow is:
-- mygroup:WayPointInitialize()
-- mygroup:WayPointFunction( WayPoint, WayPointIndex, WayPointFunction, ... )
-- mygroup:WayPointExecute()
function CONTROLLABLE:WayPointExecute( WayPoint, WaitTime )
  self:F( { WayPoint, WaitTime } )

  if not WayPoint then
    WayPoint = 1
  end

  -- When starting the mission from a certain point, the TaskPoints need to be deleted before the given WayPoint.
  for TaskPointID = 1, WayPoint - 1 do
    table.remove( self.WayPoints, 1 )
  end

  self:T3( self.WayPoints )

  self:SetTask( self:TaskRoute( self.WayPoints ), WaitTime )

  return self
end

--- Returns if the Controllable contains AirPlanes.
-- @param #CONTROLLABLE self
-- @return #boolean true if Controllable contains AirPlanes.
function CONTROLLABLE:IsAirPlane()
  self:F2()

  local DCSObject = self:GetDCSObject()

  if DCSObject then
    local Category = DCSObject:getDesc().category
    return Category == Unit.Category.AIRPLANE
  end

  return nil
end

--- Returns if the Controllable contains Helicopters.
-- @param #CONTROLLABLE self
-- @return #boolean true if Controllable contains Helicopters.
function CONTROLLABLE:IsHelicopter()
  self:F2()

  local DCSObject = self:GetDCSObject()

  if DCSObject then
    local Category = DCSObject:getDesc().category
    return Category == Unit.Category.HELICOPTER
  end

  return nil
end

--- Sets Controllable Option for Restriction of Afterburner.
-- @param #CONTROLLABLE self
-- @param #boolean RestrictBurner If true, restrict burner. If false or nil, allow (unrestrict) burner.
function CONTROLLABLE:OptionRestrictBurner( RestrictBurner )
  self:F2( { self.ControllableName } )

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then
    local Controller = self:_GetController()

    if Controller then

      -- Issue https://github.com/FlightControl-Master/MOOSE/issues/1216
      if RestrictBurner == true then
        if self:IsAir() then
          Controller:setOption( 16, true )
        end
      else
        if self:IsAir() then
          Controller:setOption( 16, false )
        end
      end

    end
  end

end

--- [AIR] Sets Controllable Option for A2A attack range for AIR FIGHTER units.
-- @param #CONTROLLABLE self
-- @param #number range Defines the range
-- @return #CONTROLLABLE self
-- @usage Range can be one of MAX_RANGE = 0, NEZ_RANGE = 1, HALF_WAY_RMAX_NEZ = 2, TARGET_THREAT_EST = 3, RANDOM_RANGE = 4. Defaults to 3. See: https://wiki.hoggitworld.com/view/DCS_option_missileAttack
function CONTROLLABLE:OptionAAAttackRange( range )
  self:F2( { self.ControllableName } )
  -- defaults to 3
  local range = range or 3
  if range < 0 or range > 4 then
    range = 3
  end
  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()
    if Controller then
      if self:IsAir() then
        self:SetOption( AI.Option.Air.id.MISSILE_ATTACK, range )
      end
    end
    return self
  end
  return nil
end

--- [GROUND/AAA] Sets Controllable Option for Ground AAA minimum firing height.
-- @param #CONTROLLABLE self
-- @param #number meters The minimum height in meters.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionAAAMinFiringHeightMeters(meters)
 self:F2( { self.ControllableName } )
 local meters = meters or 20
 local DCSControllable = self:GetDCSObject()
 if DCSControllable then
   local Controller = self:_GetController()
   if Controller then
     if self:IsGround() then
       self:SetOption(27, meters)
     end
   end
   return self
 end
 return nil
end

--- [GROUND/AAA] Sets Controllable Option for Ground AAA maximum firing height.
-- @param #CONTROLLABLE self
-- @param #number meters The maximum height in meters.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionAAAMaxFiringHeightMeters(meters)
 self:F2( { self.ControllableName } )
 local meters = meters or 1000
 local DCSControllable = self:GetDCSObject()
 if DCSControllable then
   local Controller = self:_GetController()
   if Controller then
     if self:IsGround() then
       self:SetOption(29, meters)
     end
   end
   return self
 end
 return nil
end

--- [GROUND/AAA] Sets Controllable Option for Ground AAA minimum firing height.
-- @param #CONTROLLABLE self
-- @param #number feet The minimum height in feet.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionAAAMinFiringHeightFeet(feet)
 self:F2( { self.ControllableName } )
 local feet = feet or 60
 return self:OptionAAAMinFiringHeightMeters(UTILS.FeetToMeters(feet))
end

--- [GROUND/AAA] Sets Controllable Option for Ground AAA maximum firing height.
-- @param #CONTROLLABLE self
-- @param #number feet The maximum height in feet.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionAAAMaxFiringHeightfeet(feet)
 self:F2( { self.ControllableName } )
 local feet = feet or 3000
 return self:OptionAAAMaxFiringHeightMeters(UTILS.FeetToMeters(feet))
end

--- Defines the range at which a GROUND unit/group is allowed to use its weapons automatically.
-- @param #CONTROLLABLE self
-- @param #number EngageRange Engage range limit in percent (a number between 0 and 100). Default 100.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionEngageRange( EngageRange )
  self:F2( { self.ControllableName } )
  -- Set default if not specified.
  EngageRange = EngageRange or 100
  if EngageRange < 0 or EngageRange > 100 then
    EngageRange = 100
  end
  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()
    if Controller then
      if self:IsGround() then
        self:SetOption( AI.Option.Ground.id.AC_ENGAGEMENT_RANGE_RESTRICTION, EngageRange )
      end
    end
    return self
  end
  return nil
end

--- [AIR] Set how the AI lands on an airfield. Here: Straight in.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionLandingStraightIn()
  self:F2( { self.ControllableName } )
  if self:IsAir() then
    self:SetOption("36","0")
  end
  return self
end

--- [AIR] Set how the AI lands on an airfield. Here: In pairs (if > 1 aircraft in group)
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionLandingForcePair()
  self:F2( { self.ControllableName } )
  if self:IsAir() then
    self:SetOption("36","1")
  end
  return self
end

--- [AIR] Set how the AI lands on an airfield. Here: No landing in pairs.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionLandingRestrictPair()
  self:F2( { self.ControllableName } )
  if self:IsAir() then
    self:SetOption("36","2")
  end
  return self
end

--- [AIR] Set how the AI lands on an airfield. Here: Overhead break.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionLandingOverheadBreak()
  self:F2( { self.ControllableName } )
  if self:IsAir() then
    self:SetOption("36","3")
  end
  return self
end

--- [AIR] Set how the AI uses the onboard radar.
-- @param #CONTROLLABLE self
-- @param #number Option Options are: `NEVER = 0, FOR_ATTACK_ONLY = 1,FOR_SEARCH_IF_REQUIRED = 2, FOR_CONTINUOUS_SEARCH = 3`
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionRadarUsing(Option)
 self:F2( { self.ControllableName } )
  if self:IsAir() then
    self:SetOption(AI.Option.Air.id.RADAR_USING,Option)
  end
  return self
end

--- [AIR] Set how the AI uses the onboard radar. Here: never.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionRadarUsingNever()
 self:F2( { self.ControllableName } )
  if self:IsAir() then
    self:SetOption(AI.Option.Air.id.RADAR_USING,0)
  end
  return self
end

--- [AIR] Set how the AI uses the onboard radar, here: for attack only.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionRadarUsingForAttackOnly()
 self:F2( { self.ControllableName } )
  if self:IsAir() then
    self:SetOption(AI.Option.Air.id.RADAR_USING,1)
  end
  return self
end

--- [AIR] Set how the AI uses the onboard radar, here: when required for searching.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionRadarUsingForSearchIfRequired()
 self:F2( { self.ControllableName } )
  if self:IsAir() then
    self:SetOption(AI.Option.Air.id.RADAR_USING,2)
  end
  return self
end

--- [AIR] Set how the AI uses the onboard radar, here: always on.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionRadarUsingForContinousSearch()
 self:F2( { self.ControllableName } )
  if self:IsAir() then
    self:SetOption(AI.Option.Air.id.RADAR_USING,3)
  end
  return self
end

--- [AIR] Set if the AI is reporting passing of waypoints
-- @param #CONTROLLABLE self
-- @param #boolean OnOff If true or nil, AI will report passing waypoints, if false, it will not.
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionWaypointPassReport(OnOff)
 self:F2( { self.ControllableName } )
 local onoff = (OnOff == nil or OnOff == true) and false or true
  if self:IsAir() then
    self:SetOption(AI.Option.Air.id.PROHIBIT_WP_PASS_REPORT,onoff)
  end
  return self
end

--- [AIR] Set the AI to not report anything over the radio - radio silence
-- @param #CONTROLLABLE self
-- @param #boolean OnOff If true or nil, radio is set to silence, if false radio silence is lifted.
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionRadioSilence(OnOff)
 local onoff = (OnOff == true or OnOff == nil) and true or false
 self:F2( { self.ControllableName } )
  if self:IsAir() then
    self:SetOption(AI.Option.Air.id.SILENCE,onoff)
  end
  return self
end

--- [AIR] Set the AI to report contact for certain types of objects.
-- @param #CONTROLLABLE self
-- @param #table Objects Table of attribute names for which AI reports contact. Defaults to {"Air"}. See [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_attributes)
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionRadioContact(Objects)
 self:F2( { self.ControllableName } )
 if not Objects then Objects = {"Air"} end
 if type(Objects) ~= "table" then Objects = {Objects} end
  if self:IsAir() then
    self:SetOption(AI.Option.Air.id.OPTION_RADIO_USAGE_CONTACT,Objects)
  end
  return self
end

--- [AIR] Set the AI to report engaging certain types of objects.
-- @param #CONTROLLABLE self
-- @param #table Objects Table of attribute names for which AI reports contact. Defaults to {"Air"}, see [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_attributes)
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionRadioEngage(Objects)
 self:F2( { self.ControllableName } )
 if not Objects then Objects = {"Air"} end
 if type(Objects) ~= "table" then Objects = {Objects} end
  if self:IsAir() then
    self:SetOption(AI.Option.Air.id.OPTION_RADIO_USAGE_ENGAGE,Objects)
  end
  return self
end

--- [AIR] Set the AI to report killing certain types of objects.
-- @param #CONTROLLABLE self
-- @param #table Objects Table of attribute names for which AI reports contact. Defaults to {"Air"}, see [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_attributes)
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetOptionRadioKill(Objects)
 self:F2( { self.ControllableName } )
 if not Objects then Objects = {"Air"} end
 if type(Objects) ~= "table" then Objects = {Objects} end
  if self:IsAir() then
    self:SetOption(AI.Option.Air.id.OPTION_RADIO_USAGE_KILL,Objects)
  end
  return self
end

--- (GROUND) Relocate controllable to a random point within a given radius; use e.g.for evasive actions; Note that not all ground controllables can actually drive, also the alarm state of the controllable might stop it from moving.
-- @param #CONTROLLABLE self
-- @param #number speed Speed of the controllable, default 20
-- @param #number radius Radius of the relocation zone, default 500
-- @param #boolean onroad If true, route on road (less problems with AI way finding), default true
-- @param #boolean shortcut If true and onroad is set, take a shorter route - if available - off road, default false
-- @param #string formation Formation string as in the mission editor, e.g. "Vee", "Diamond", "Line abreast", etc. Defaults to "Off Road"
-- @param #boolean onland (optional) If true, try up to 50 times to get a coordinate on land.SurfaceType.LAND. Note - this descriptor value is not reliably implemented on all maps.
-- @return #CONTROLLABLE self
function CONTROLLABLE:RelocateGroundRandomInRadius( speed, radius, onroad, shortcut, formation, onland )
  self:F2( { self.ControllableName } )

  local _coord = self:GetCoordinate()
  if not _coord then
  return self
  end
  local _radius = radius or 500
  local _speed = speed or 20
  local _tocoord = _coord:GetRandomCoordinateInRadius( _radius, 100 )
  if onland then
    for i=1,50 do
      local island = _tocoord:GetSurfaceType() == land.SurfaceType.LAND and true or false
      if island then break end
      _tocoord = _coord:GetRandomCoordinateInRadius( _radius, 100 )
    end
  end
  local _onroad = onroad or true
  local _grptsk = {}
  local _candoroad = false
  local _shortcut = shortcut or false
  local _formation = formation or "Off Road"

  -- create a DCS Task an push it on the group
  if onroad then
    _grptsk, _candoroad = self:TaskGroundOnRoad( _tocoord, _speed, _formation, _shortcut )
    self:Route( _grptsk, 5 )
  else
    self:TaskRouteToVec2( _tocoord:GetVec2(), _speed, _formation )
  end

  return self
end

--- Defines how long a GROUND unit/group will move to avoid an ongoing attack.
-- @param #CONTROLLABLE self
-- @param #number Seconds Any positive number: AI will disperse, but only for the specified time before continuing their route. 0: AI will not disperse.
-- @return #CONTROLLABLE self
function CONTROLLABLE:OptionDisperseOnAttack( Seconds )
  self:F2( { self.ControllableName } )
  -- Set default if not specified.
  local seconds = Seconds or 0
  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()
    if Controller then
      if self:IsGround() then
        self:SetOption( AI.Option.Ground.id.DISPERSE_ON_ATTACK, seconds )
      end
    end
    return self
  end
  return nil
end

--- Returns if the unit is a submarine.
-- @param #CONTROLLABLE self
-- @return #boolean Submarines attributes result.
function CONTROLLABLE:IsSubmarine()
  self:F2()

  local DCSUnit = self:GetDCSObject()

  if DCSUnit then
    local UnitDescriptor = DCSUnit:getDesc()
    if UnitDescriptor.attributes["Submarines"] == true then
      return true
    else
      return false
    end
  end

  return nil
end


--- Sets the controlled group to go at the specified speed in meters per second.
-- @param #CONTROLLABLE self
-- @param #number Speed Speed in meters per second
-- @param #boolean Keep (Optional) When set to true, will maintain the speed on passing waypoints. If not present or false, the controlled group will return to the speed as defined by their route.
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetSpeed(Speed, Keep)
  self:F2( { self.ControllableName } )
  -- Set default if not specified.
  local speed = Speed or 5
  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()
    if Controller then
      Controller:setSpeed(speed, Keep)
    end
  end
  return self
end

--- [AIR] Sets the controlled aircraft group to fly at the specified altitude in meters.
-- @param #CONTROLLABLE self
-- @param #number Altitude Altitude in meters.
-- @param #boolean Keep (Optional) When set to true, will maintain the altitude on passing waypoints. If not present or false, the controlled group will return to the altitude as defined by their route.
-- @param #string AltType (Optional) Specifies the altitude type used. If nil, the altitude type of the current waypoint will be used. Accepted values are "BARO" and "RADIO".
-- @return #CONTROLLABLE self
function CONTROLLABLE:SetAltitude(Altitude, Keep, AltType)
  self:F2( { self.ControllableName } )
  -- Set default if not specified.
  local altitude = Altitude or 1000
  local DCSControllable = self:GetDCSObject()
  if DCSControllable then
    local Controller = self:_GetController()
    if Controller then
      if self:IsAir() then
        Controller:setAltitude(altitude, Keep, AltType)
      end
    end
  end
  return self
end

--- Return an empty task shell for Aerobatics.
-- @param #CONTROLLABLE self
-- @return DCS#Task
-- @usage
--        local plane = GROUP:FindByName("Aerial-1")
--        -- get a task shell
--        local aerotask = plane:TaskAerobatics()
--        -- add a series of maneuvers
--        aerotask = plane:TaskAerobaticsHorizontalEight(aerotask,1,5000,850,true,false,1,70)
--        aerotask = plane:TaskAerobaticsWingoverFlight(aerotask,1,0,0,true,true,20)
--        aerotask = plane:TaskAerobaticsLoop(aerotask,1,0,0,false,true)
--        -- set the task
--        plane:SetTask(aerotask)
function CONTROLLABLE:TaskAerobatics()

  local DCSTaskAerobatics = {
    id = "Aerobatics",
    params = {
      ["maneuversSequency"] = {},
    },
    ["enabled"] = true,
    ["auto"] = false,
  }

  return DCSTaskAerobatics
end

--- Add an aerobatics entry of type "CANDLE" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsCandle(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local CandleTask = {
    ["name"] = "CANDLE",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      }
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],CandleTask)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "EDGE_FLIGHT" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number FlightTime (Optional) Time to fly this manoever in seconds, defaults to 10.
-- @param #number Side (Optional) On which side to fly,  0 == left, 1 == right side, defaults to 0.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsEdgeFlight(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,FlightTime,Side)

  local maxrepeats = 10
  local maxflight = 200

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local flighttime = FlightTime or 10

  if flighttime > 200 then maxflight = flighttime end

  local EdgeTask = {
    ["name"] = "EDGE_FLIGHT",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["FlightTime"] = {
        ["max_v"] = maxflight,
        ["min_v"] = 1,
        ["order"] = 6,
        ["step"] = 0.1,
        ["value"] = flighttime or 10, -- Secs?
      },
      ["SIDE"] = {
        ["order"] = 7,
        ["value"] = Side or 0, --0 == left, 1 == right side
      },
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],EdgeTask)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "WINGOVER_FLIGHT" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number FlightTime (Optional) Time to fly this manoever in seconds, defaults to 10.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsWingoverFlight(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,FlightTime)

  local maxrepeats = 10
  local maxflight = 200

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local flighttime = FlightTime or 10

  if flighttime > 200 then maxflight = flighttime end

  local WingoverTask = {
    ["name"] = "WINGOVER_FLIGHT",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["FlightTime"] = {
        ["max_v"] = maxflight,
        ["min_v"] = 1,
        ["order"] = 6,
        ["step"] = 0.1,
        ["value"] = flighttime or 10, -- Secs?
      },
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],WingoverTask)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "LOOP" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsLoop(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local LoopTask = {
    ["name"] = "LOOP",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      }
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],LoopTask)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "HORIZONTAL_EIGHT" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number Side (Optional) On which side to fly,  0 == left, 1 == right side, defaults to 0.
-- @param #number RollDeg (Optional) Roll degrees for Roll 1 and 2, defaults to 60.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsHorizontalEight(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Side,RollDeg)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local LoopTask = {
    ["name"] = "HORIZONTAL_EIGHT",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["SIDE"] = {
        ["order"] = 6,
        ["value"] = Side or 0,
      },
      ["ROLL1"] = {
        ["order"] = 7,
        ["value"] = RollDeg or 60,
      },
      ["ROLL2"] = {
        ["order"] = 8,
        ["value"] = RollDeg or 60,
      },

    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],LoopTask)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "HAMMERHEAD" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number Side (Optional) On which side to fly,  0 == left, 1 == right side, defaults to 0.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsHammerhead(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Side)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local Task = {
    ["name"] = "HUMMERHEAD",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["SIDE"] = {
        ["order"] = 6,
        ["value"] = Side or 0,
      },
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],Task)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "SKEWED_LOOP" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number Side (Optional) On which side to fly,  0 == left, 1 == right side, defaults to 0.
-- @param #number RollDeg (Optional) Roll degrees for Roll 1 and 2, defaults to 60.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsSkewedLoop(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Side,RollDeg)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local Task = {
    ["name"] = "SKEWED_LOOP",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["ROLL"] = {
        ["order"] = 6,
        ["value"] = RollDeg or 60,
      },
      ["SIDE"] = {
        ["order"] = 7,
        ["value"] = Side or 0,
      },
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],Task)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "TURN" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number Side (Optional) On which side to fly,  0 == left, 1 == right side, defaults to 0.
-- @param #number RollDeg (Optional) Roll degrees for Roll 1 and 2, defaults to 60.
-- @param #number Pull (Optional) How many Gs to pull in this, defaults to 2.
-- @param #number Angle (Optional) How many degrees to turn, defaults to 180.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsTurn(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Side,RollDeg,Pull,Angle)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local Task = {
    ["name"] = "TURN",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["Ny_req"] = {
        ["order"] = 6,
        ["value"] = Pull or 2, --amount of G to pull
      },
      ["ROLL"] = {
        ["order"] = 7,
        ["value"] = RollDeg or 60,
      },
      ["SECTOR"] = {
        ["order"] = 8,
        ["value"] = Angle or 180,
      },
      ["SIDE"] = {
        ["order"] = 9,
        ["value"] = Side or 0,
      },
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],Task)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "DIVE" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 5000.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number Angle (Optional) With how many degrees to dive, defaults to 45. Can be 15 to 90 degrees.
-- @param #number FinalAltitude (Optional) Final altitude in meters, defaults to 1000.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsDive(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Angle,FinalAltitude)

  local maxrepeats = 10

  local angle = Angle

  if angle < 15 then angle = 15 elseif angle > 90 then angle = 90 end

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local Task = {
    ["name"] = "DIVE",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 5000,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["Angle"] = {
        ["max_v"] = 90,
        ["min_v"] = 15,
        ["order"] = 6,
        ["step"] = 5,
        ["value"] = angle or 45,
      },
      ["FinalAltitude"] = {
        ["order"] = 7,
        ["value"] = FinalAltitude or 1000,
      },
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],Task)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "MILITARY_TURN" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsMilitaryTurn(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local Task = {
    ["name"] = "MILITARY_TURN",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      }
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],Task)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "IMMELMAN" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsImmelmann(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local Task = {
    ["name"] = "IMMELMAN",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      }
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],Task)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "STRAIGHT_FLIGHT" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number FlightTime (Optional) Time to fly this manoever in seconds, defaults to 10.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsStraightFlight(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,FlightTime)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local maxflight = 200

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local flighttime = FlightTime or 10

  if flighttime > 200 then maxflight = flighttime end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local Task = {
    ["name"] = "STRAIGHT_FLIGHT",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["FlightTime"] = {
        ["max_v"] = maxflight,
        ["min_v"] = 1,
        ["order"] = 6,
        ["step"] = 0.1,
        ["value"] = flighttime or 10,
      },
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],Task)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "CLIMB" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number Angle (Optional) Angle to climb. Can be between 15 and 90 degrees. Defaults to 45 degrees.
-- @param #number FinalAltitude (Optional) Altitude to climb to in meters. Defaults to 5000m.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsClimb(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Angle,FinalAltitude)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local Task = {
    ["name"] = "CLIMB",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["Angle"] = {
        ["max_v"] = 90,
        ["min_v"] = 15,
        ["order"] = 6,
        ["step"] = 5,
        ["value"] = Angle or 45,
      },
      ["FinalAltitude"] = {
        ["order"] = 7,
        ["value"] = FinalAltitude or 5000,
      },
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],Task)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "SPIRAL" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number TurnAngle (Optional) Turn angle, defaults to 360 degrees.
-- @param #number Roll (Optional) Roll to take, defaults to 60 degrees.
-- @param #number Side (Optional) On which side to fly,  0 == left, 1 == right side, defaults to 0.
-- @param #number UpDown (Optional) Spiral upwards (1) or downwards (0). Defaults to 0 - downwards.
-- @param #number Angle (Optional) Angle to spiral. Can be between 15 and 90 degrees. Defaults to 45 degrees.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsSpiral(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,TurnAngle,Roll,Side,UpDown,Angle)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0
  local updown = UpDown and 1 or 0
  local side = Side and 1 or 0

  local Task = {
    ["name"] = "SPIRAL",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["SECTOR"] = {
        ["order"] = 6,
        ["value"] = TurnAngle or 360,
      },
      ["ROLL"] = {
        ["order"] = 7,
        ["value"] = Roll or 60,
      },
      ["SIDE"] = {
        ["order"] = 8,
        ["value"] = side or 0,
      },
      ["UPDOWN"] = {
        ["order"] = 9,
        ["value"] = updown or 0,
      },
      ["Angle"] = {
        ["max_v"] = 90,
        ["min_v"] = 15,
        ["order"] = 10,
        ["step"] = 5,
        ["value"] = Angle or 45,
      },
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],Task)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "SPLIT_S" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number FinalSpeed (Optional) Final speed to reach in KPH. Defaults to 500 kph.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsSplitS(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,FinalSpeed)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local maxflight = 200

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local finalspeed = FinalSpeed or 500

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local Task = {
    ["name"] = "SPLIT_S",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["FinalSpeed"] = {
        ["order"] = 6,
        ["value"] = finalspeed,
      },
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],Task)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "AILERON_ROLL" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number Side (Optional) On which side to fly,  0 == left, 1 == right side, defaults to 0.
-- @param #number RollRate (Optional) How many degrees to roll per sec(?), can be between 15 and 450, defaults to 90.
-- @param #number TurnAngle (Optional) Angles to turn overall, defaults to 360.
-- @param #number FixAngle (Optional) No idea what this does, can be between 0 and 180 degrees, defaults to 180.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsAileronRoll(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Side,RollRate,TurnAngle,FixAngle)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local maxflight = 200

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local Task = {
    ["name"] = "AILERON_ROLL",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["SIDE"] = {
        ["order"] = 6,
        ["value"] = Side or 0,
      },
      ["RollRate"] = {
        ["max_v"] = 450,
        ["min_v"] = 15,
        ["order"] = 7,
        ["step"] = 5,
        ["value"] = RollRate or 90,
      },
      ["SECTOR"] = {
        ["order"] = 8,
        ["value"] = TurnAngle or 360,
      },
      ["FIXSECTOR"] = {
        ["max_v"] = 180,
        ["min_v"] = 0,
        ["order"] = 9,
        ["step"] = 5,
        ["value"] = FixAngle or 0, -- TODO: Need to find out what this does
      },
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],Task)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "FORCED_TURN" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number TurnAngle (Optional) Angles to turn, defaults to 360.
-- @param #number Side (Optional) On which side to fly,  0 == left, 1 == right side, defaults to 0.
-- @param #number FlightTime (Optional) Flight time in seconds for thos maneuver. Defaults to 30.
-- @param #number MinSpeed (Optional) Minimum speed to keep in kph, defaults to 250 kph.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsForcedTurn(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,TurnAngle,Side,FlightTime,MinSpeed)

  local maxrepeats = 10
  local flighttime = FlightTime or 30
  local maxtime = 200
  if flighttime > 200 then maxtime = flighttime end

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local Task = {
    ["name"] = "FORCED_TURN",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["SECTOR"] = {
        ["order"] = 6,
        ["value"] = TurnAngle or 360,
      },
      ["SIDE"] = {
        ["order"] = 7,
        ["value"] = Side or 0,
      },
      ["FlightTime"] = {
        ["max_v"] = maxtime or 200,
        ["min_v"] = 0,
        ["order"] = 8,
        ["step"] = 0.1,
        ["value"] = flighttime or 30,
      },
      ["MinSpeed"] = {
        ["max_v"] = 3000,
        ["min_v"] = 30,
        ["order"] = 9,
        ["step"] = 10,
        ["value"] = MinSpeed or 250,
      },
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],Task)

  return TaskAerobatics
end

--- Add an aerobatics entry of type "BARREL_ROLL" to the Aerobatics Task.
-- @param #CONTROLLABLE self
-- @param DCS#Task TaskAerobatics The Aerobatics Task
-- @param #number Repeats (Optional) The number of repeats, defaults to 1.
-- @param #number InitAltitude (Optional) Starting altitude in meters, defaults to 0.
-- @param #number InitSpeed (Optional) Starting speed in KPH, defaults to 0.
-- @param #boolean UseSmoke (Optional)  Using smoke, defaults to false.
-- @param #boolean StartImmediately (Optional) If true, start immediately and ignore  InitAltitude and InitSpeed.
-- @param #number Side (Optional) On which side to fly,  0 == left, 1 == right side, defaults to 0.
-- @param #number RollRate (Optional) How many degrees to roll per sec(?), can be between 15 and 450, defaults to 90.
-- @param #number TurnAngle (Optional) Turn angle, defaults to 360 degrees.
-- @return DCS#Task
function CONTROLLABLE:TaskAerobaticsBarrelRoll(TaskAerobatics,Repeats,InitAltitude,InitSpeed,UseSmoke,StartImmediately,Side,RollRate,TurnAngle)

  local maxrepeats = 10

  if Repeats > maxrepeats then maxrepeats = Repeats end

  local usesmoke = UseSmoke and 1 or 0

  local startimmediately = StartImmediately and 1 or 0

  local Task = {
    ["name"] = "BARREL_ROLL",
    ["params"] = {
      ["RepeatQty"] = {
        ["max_v"] = maxrepeats,
        ["min_v"] = 1,
        ["order"] = 1,
        ["value"] = Repeats or 1,
      },
      ["InitAltitude"] = {
        ["order"] = 2,
        ["value"] = InitAltitude or 0,
      },
      ["InitSpeed"] = {
        ["order"] = 3,
        ["value"] = InitSpeed or 0,
      },
      ["UseSmoke"] = {
        ["order"] = 4,
        ["value"] = usesmoke,
      },
      ["StartImmediatly"] = {
        ["order"] = 5,
        ["value"] = startimmediately,
      },
      ["SIDE"] = {
        ["order"] = 6,
        ["value"] = Side or 0,
      },
      ["RollRate"] = {
        ["max_v"] = 450,
        ["min_v"] = 15,
        ["order"] = 7,
        ["step"] = 5,
        ["value"] = RollRate or 90,
      },
      ["SECTOR"] = {
        ["order"] = 8,
        ["value"] = TurnAngle or 360,
      },
    }
  }

  table.insert(TaskAerobatics.params["maneuversSequency"],Task)

  return TaskAerobatics
end


--- [Air] Make an airplane or helicopter patrol between two points in a racetrack - resulting in a much tighter track around the start and end points.
-- @param #CONTROLLABLE self
-- @param Core.Point#COORDINATE Point1 Start point.
-- @param Core.Point#COORDINATE Point2 End point.
-- @param #number Altitude (Optional) Altitude in meters. Defaults to the altitude of the coordinate.
-- @param #number Speed (Optional) Speed in kph. Defaults to 500 kph.
-- @param #number Formation (Optional) Formation to take, e.g. ENUMS.Formation.FixedWing.Trail.Close, also see [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_option_formation).
-- @param #boolean AGL (Optional) If true, set altitude to above ground level (AGL), not above sea level (ASL).
-- @param #number Delay  (Optional) Set the task after delay seconds only.
-- @return #CONTROLLABLE self
function CONTROLLABLE:PatrolRaceTrack(Point1, Point2, Altitude, Speed, Formation, AGL, Delay)

  local PatrolGroup = self -- Wrapper.Group#GROUP

  if not self:IsInstanceOf( "GROUP" ) then
    PatrolGroup = self:GetGroup() -- Wrapper.Group#GROUP
  end

  local delay = Delay or 1

  self:F( { PatrolGroup = PatrolGroup:GetName() } )

  if PatrolGroup:IsAir() then
    if Formation then
       PatrolGroup:SetOption(AI.Option.Air.id.FORMATION,Formation) -- https://wiki.hoggitworld.com/view/DCS_option_formation
   end

   local FromCoord = PatrolGroup:GetCoordinate()
   local ToCoord = Point1:GetCoordinate()

   -- Calculate the new Route
   if Altitude then
     local asl = true
     if AGL then asl = false end
     FromCoord:SetAltitude(Altitude, asl)
     ToCoord:SetAltitude(Altitude, asl)
   end

   -- Create a "air waypoint", which is a "point" structure that can be given as a parameter to a Task
   local Route = {}
   Route[#Route + 1] = FromCoord:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true, nil, DCSTasks, description, timeReFuAr )
   Route[#Route + 1] = ToCoord:WaypointAir( AltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true, nil, DCSTasks, description, timeReFuAr )

   local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolRaceTrack", Point2, Point1, Altitude, Speed, Formation, Delay )
   PatrolGroup:SetTaskWaypoint( Route[#Route], TaskRouteToZone ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone.
   PatrolGroup:Route( Route, Delay ) -- Move after delay seconds to the Route. See the Route method for details.
  end

  return self
end

--- IR Marker courtesy Florian Brinker (fbrinker)

--- [GROUND] Create and enable a new IR Marker for the given controllable UNIT or GROUP.
-- @param #CONTROLLABLE self
-- @param #boolean EnableImmediately (Optionally) If true start up the IR Marker immediately. Else you need to call `myobject:EnableIRMarker()` later on.
-- @param #number Runtime (Optionally) Run this IR Marker for the given number of seconds, then stop. Use in conjunction with EnableImmediately. Defaults to 60 seconds.
-- @return #CONTROLLABLE self
function CONTROLLABLE:NewIRMarker(EnableImmediately, Runtime)
  self:T2("NewIRMarker")
    if self:IsInstanceOf("GROUP") then
      if self.IRMarkerGroup == true then return end
      self.IRMarkerGroup = true
      self.IRMarkerUnit = false
    elseif self:IsInstanceOf("UNIT") then
      if self.IRMarkerUnit == true then return end
      self.IRMarkerGroup = false
      self.IRMarkerUnit = true
    end
    
    self.Runtime = Runtime or 60
    if EnableImmediately and EnableImmediately == true then
      self:EnableIRMarker(Runtime)
    end
    
    return self
end

--- [GROUND] Enable the IR marker.
-- @param #CONTROLLABLE self
-- @param #number Runtime (Optionally) Run this IR Marker for the given number of seconds, then stop. Else run until you call `myobject:DisableIRMarker()`.
-- @return #CONTROLLABLE self 
function CONTROLLABLE:EnableIRMarker(Runtime)
  self:T2("EnableIRMarker")
    if self.IRMarkerGroup == nil then
      self:NewIRMarker(true,Runtime)
      return
    end
    
    if self:IsInstanceOf("GROUP") then
        self:EnableIRMarkerForGroup(Runtime)
        return
    end
    
    if self.timer and self.timer:IsRunning() then return self end
    
    local Runtime = Runtime or self.Runtime
    self.timer = TIMER:New(CONTROLLABLE._MarkerBlink, self)
    self.timer:Start(nil, 1 - math.random(1, 5) / 10 / 2, Runtime) -- start randomized
    self.IRMarkerUnit = true
    
    return self
end

--- [GROUND] Disable the IR marker.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self 
function CONTROLLABLE:DisableIRMarker()
 self:T2("DisableIRMarker")
    if self:IsInstanceOf("GROUP") then
        self:DisableIRMarkerForGroup()
        return
    end

      if self.spot then
          self.spot = nil
      end
      if self.timer and self.timer:IsRunning() then
          self.timer:Stop()
          self.timer = nil
      end

    if self:IsInstanceOf("GROUP") then
      self.IRMarkerGroup = nil
    elseif self:IsInstanceOf("UNIT") then
      self.IRMarkerUnit = nil
    end

    return self
end

--- [GROUND] Enable the IR markers for a whole group.
-- @param #CONTROLLABLE self
-- @param #number Runtime Runtime of the marker in seconds
-- @return #CONTROLLABLE self 
function CONTROLLABLE:EnableIRMarkerForGroup(Runtime)
  self:T2("EnableIRMarkerForGroup")
  if self:IsInstanceOf("GROUP") 
  then
    local units = self:GetUnits() or {}
    for _,_unit in pairs(units) do
      _unit:EnableIRMarker(Runtime)
    end
    self.IRMarkerGroup = true
  end
  return self
end

--- [GROUND] Disable the IR markers for a whole group.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self 
function CONTROLLABLE:DisableIRMarkerForGroup()
  self:T2("DisableIRMarkerForGroup")
  if self:IsInstanceOf("GROUP") then
    local units = self:GetUnits() or {}
    for _,_unit in pairs(units) do
      _unit:DisableIRMarker()
    end
    self.IRMarkerGroup = nil
  end
  return self
end

--- [GROUND] Check if an IR Spot exists.
-- @param #CONTROLLABLE self
-- @return #boolean outcome
function CONTROLLABLE:HasIRMarker()
  self:T2("HasIRMarker")
  if self:IsInstanceOf("GROUP") then
    local units = self:GetUnits() or {}
    for _,_unit in pairs(units) do
      if _unit.timer and _unit.timer:IsRunning() then return true end
    end
  elseif self.timer and self.timer:IsRunning() then return true end
  return false
end

--- [Internal] This method is called by the scheduler to blink the IR marker.
function CONTROLLABLE._StopSpot(spot)
    if spot then 
        spot:destroy() 
    end
end

--- [Internal] This method is called by the scheduler after enabling the IR marker.
-- @param #CONTROLLABLE self
-- @return #CONTROLLABLE self 
function CONTROLLABLE:_MarkerBlink()
  self:T2("_MarkerBlink")
    if self:IsAlive() ~= true then
        self:DisableIRMarker()
        return
    end

    self.timer.dT = 1 - (math.random(1, 2) / 10 / 2) -- randomize the blinking by a small amount

    local _, _, unitBBHeight, _ = self:GetObjectSize()
    local unitPos = self:GetPositionVec3()

    if self.timer:IsRunning() then
        self:T2("Create Spot")
        local spot = Spot.createInfraRed(
            self.DCSUnit,
            { x = 0, y = (unitBBHeight + 1), z = 0 },
            { x = unitPos.x, y = (unitPos.y + unitBBHeight), z = unitPos.z }
        )
        self.spot = spot
        local offTimer = nil
        local offTimer = TIMER:New(CONTROLLABLE._StopSpot, spot)
        offTimer:Start(0.5)
    end
    return self
end
--- **Wrapper** - GROUP wraps the DCS Class Group objects.
--
-- ===
--
-- The @{#GROUP} class is a wrapper class to handle the DCS Group objects.
--
-- ## Features:
--
--  * Support all DCS Group APIs.
--  * Enhance with Group specific APIs not in the DCS Group API set.
--  * Handle local Group Controller.
--  * Manage the "state" of the DCS Group.
--
-- **IMPORTANT: ONE SHOULD NEVER SANITIZE these GROUP OBJECT REFERENCES! (make the GROUP object references nil).**
--
-- ===
--
-- For each DCS Group object alive within a running mission, a GROUP wrapper object (instance) will be created within the global _DATABASE object (an instance of @{Core.Database#DATABASE}).
-- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Group objects are spawned (using the @{Core.Spawn} class).
--
-- The GROUP class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference
-- using the DCS Group or the DCS GroupName.
--
-- The GROUP methods will reference the DCS Group object by name when it is needed during API execution.
-- If the DCS Group object does not exist or is nil, the GROUP methods will return nil and may log an exception in the DCS.log file.
--
-- ===
--
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/Wrapper/Group)
--
-- ===
--
-- ### Author: **FlightControl**
--
-- ### Contributions:
--
--   * **Entropy**, **Afinegan**: Came up with the requirement for AIOnOff().
--   * **Applevangelist**: various
--
-- ===
--
-- @module Wrapper.Group
-- @image Wrapper_Group.JPG

---
-- @type GROUP
-- @extends Wrapper.Controllable#CONTROLLABLE
-- @field #string GroupName The name of the group.


--- Wrapper class of the DCS world Group object.
--
-- ## Finding groups
--
-- The GROUP class provides the following functions to retrieve quickly the relevant GROUP instance:
--
--  * @{#GROUP.Find}(): Find a GROUP instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Group object.
--  * @{#GROUP.FindByName}(): Find a GROUP instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Group name.
--  * @{#GROUP.FindByMatching}(): Find a GROUP instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using pattern matching.
--  * @{#GROUP.FindAllByMatching}(): Find all GROUP instances from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using pattern matching.
--
-- ## Tasking of groups
--
-- A GROUP is derived from the wrapper class CONTROLLABLE (@{Wrapper.Controllable#CONTROLLABLE}).
-- See the @{Wrapper.Controllable} task methods section for a description of the task methods.
--
-- But here is an example how a group can be assigned a task.
--
-- This test demonstrates the use(s) of the SwitchWayPoint method of the GROUP class.
--
-- First we look up the objects. We create a GROUP object `HeliGroup`, using the @{#GROUP:FindByName}() method, looking up the `"Helicopter"` group object.
-- Same for the `"AttackGroup"`.
--
--          local HeliGroup = GROUP:FindByName( "Helicopter" )
--          local AttackGroup = GROUP:FindByName( "AttackGroup" )
--
-- Now we retrieve the @{Wrapper.Unit#UNIT} objects of the `AttackGroup` object, using the method `:GetUnits()`.
--
--          local AttackUnits = AttackGroup:GetUnits()
--
-- Tasks are actually text strings that we build using methods of GROUP.
-- So first, we declare an list of `Tasks`.
--
--          local Tasks = {}
--
-- Now we loop over the `AttackUnits` using a for loop.
-- We retrieve the `AttackUnit` using the `AttackGroup:GetUnit()` method.
-- Each `AttackUnit` found, will be attacked by `HeliGroup`, using the method `HeliGroup:TaskAttackUnit()`.
-- This method returns a string containing a command line to execute the task to the `HeliGroup`.
-- The code will assign the task string command to the next element in the `Task` list, using `Tasks[#Tasks+1]`.
-- This little code will take the count of `Task` using `#` operator, and will add `1` to the count.
-- This result will be the index of the `Task` element.
--
--          for i = 1, #AttackUnits do
--            local AttackUnit = AttackGroup:GetUnit( i )
--            Tasks[#Tasks+1] = HeliGroup:TaskAttackUnit( AttackUnit )
--          end
--
-- Once these tasks have been executed, a function `_Resume` will be called ...
--
--          Tasks[#Tasks+1] = HeliGroup:TaskFunction( "_Resume", { "''" } )
--
--          -- @param Wrapper.Group#GROUP HeliGroup
--          function _Resume( HeliGroup )
--            env.info( '_Resume' )
--
--            HeliGroup:MessageToAll( "Resuming",10,"Info")
--          end
--
-- Now here is where the task gets assigned!
-- Using `HeliGroup:PushTask`, the task is pushed onto the task queue of the group `HeliGroup`.
-- Since `Tasks` is an array of tasks, we use the `HeliGroup:TaskCombo` method to execute the tasks.
-- The `HeliGroup:PushTask` method can receive a delay parameter in seconds.
-- In the example, `30` is given as a delay.
--
--
--          HeliGroup:PushTask(
--            HeliGroup:TaskCombo(
--            Tasks
--            ), 30
--          )
--
-- That's it!
-- But again, please refer to the @{Wrapper.Controllable} task methods section for a description of the different task methods that are available.
--
--
--
-- ### Obtain the mission from group templates
--
-- Group templates contain complete mission descriptions. Sometimes you want to copy a complete mission from a group and assign it to another:
--
--   * @{Wrapper.Controllable#CONTROLLABLE.TaskMission}: (AIR + GROUND) Return a mission task from a mission template.
--
-- ## GROUP Command methods
--
-- A GROUP is a @{Wrapper.Controllable}. See the @{Wrapper.Controllable} command methods section for a description of the command methods.
--
-- ## GROUP option methods
--
-- A GROUP is a @{Wrapper.Controllable}. See the @{Wrapper.Controllable} option methods section for a description of the option methods.
--
-- ## GROUP Zone validation methods
--
-- The group can be validated whether it is completely, partly or not within a @{Core.Zone}.
-- Use the following Zone validation methods on the group:
--
--   * @{#GROUP.IsCompletelyInZone}: Returns true if all units of the group are within a @{Core.Zone}.
--   * @{#GROUP.IsPartlyInZone}: Returns true if some units of the group are within a @{Core.Zone}.
--   * @{#GROUP.IsNotInZone}: Returns true if none of the group units of the group are within a @{Core.Zone}.
--
-- The zone can be of any @{Core.Zone} class derived from @{Core.Zone#ZONE_BASE}. So, these methods are polymorphic to the zones tested on.
--
-- ## GROUP AI methods
--
-- A GROUP has AI methods to control the AI activation.
--
--   * @{#GROUP.SetAIOnOff}(): Turns the GROUP AI On or Off.
--   * @{#GROUP.SetAIOn}(): Turns the GROUP AI On.
--   * @{#GROUP.SetAIOff}(): Turns the GROUP AI Off.
--
-- @field #GROUP GROUP
GROUP = {
  ClassName = "GROUP",
}


--- Enumerator for location at airbases
-- @type GROUP.Takeoff
GROUP.Takeoff = {
  Air = 1,
  Runway = 2,
  Hot = 3,
  Cold = 4,
}

GROUPTEMPLATE = {}

GROUPTEMPLATE.Takeoff = {
  [GROUP.Takeoff.Air] =     { "Turning Point", "Turning Point" },
  [GROUP.Takeoff.Runway] =  { "TakeOff", "From Runway" },
  [GROUP.Takeoff.Hot] =     { "TakeOffParkingHot", "From Parking Area Hot" },
  [GROUP.Takeoff.Cold] =    { "TakeOffParking", "From Parking Area" }
}

--- Generalized group attributes. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit.
-- @type GROUP.Attribute
-- @field #string AIR_TRANSPORTPLANE Airplane with transport capability. This can be used to transport other assets.
-- @field #string AIR_AWACS Airborne Early Warning and Control System.
-- @field #string AIR_FIGHTER Fighter, interceptor, ... airplane.
-- @field #string AIR_BOMBER Aircraft which can be used for strategic bombing.
-- @field #string AIR_TANKER Airplane which can refuel other aircraft.
-- @field #string AIR_TRANSPORTHELO Helicopter with transport capability. This can be used to transport other assets.
-- @field #string AIR_ATTACKHELO Attack helicopter.
-- @field #string AIR_UAV Unpiloted Aerial Vehicle, e.g. drones.
-- @field #string AIR_OTHER Any airborne unit that does not fall into any other airborne category.
-- @field #string GROUND_APC Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets.
-- @field #string GROUND_TRUCK Unarmed ground vehicles, which has the DCS "Truck" attribute.
-- @field #string GROUND_INFANTRY Ground infantry assets.
-- @field #string GROUND_IFV Ground Infantry Fighting Vehicle.
-- @field #string GROUND_ARTILLERY Artillery assets.
-- @field #string GROUND_TANK Tanks (modern or old).
-- @field #string GROUND_TRAIN Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently.
-- @field #string GROUND_EWR Early Warning Radar.
-- @field #string GROUND_AAA Anti-Aircraft Artillery.
-- @field #string GROUND_SAM Surface-to-Air Missile system or components.
-- @field #string GROUND_OTHER Any ground unit that does not fall into any other ground category.
-- @field #string NAVAL_AIRCRAFTCARRIER Aircraft carrier.
-- @field #string NAVAL_WARSHIP War ship, i.e. cruisers, destroyers, firgates and corvettes.
-- @field #string NAVAL_ARMEDSHIP Any armed ship that is not an aircraft carrier, a cruiser, destroyer, firgatte or corvette.
-- @field #string NAVAL_UNARMEDSHIP Any unarmed naval vessel.
-- @field #string NAVAL_OTHER Any naval unit that does not fall into any other naval category.
-- @field #string OTHER_UNKNOWN Anything that does not fall into any other category.
GROUP.Attribute = {
  AIR_TRANSPORTPLANE="Air_TransportPlane",
  AIR_AWACS="Air_AWACS",
  AIR_FIGHTER="Air_Fighter",
  AIR_BOMBER="Air_Bomber",
  AIR_TANKER="Air_Tanker",
  AIR_TRANSPORTHELO="Air_TransportHelo",
  AIR_ATTACKHELO="Air_AttackHelo",
  AIR_UAV="Air_UAV",
  AIR_OTHER="Air_OtherAir",
  GROUND_APC="Ground_APC",
  GROUND_TRUCK="Ground_Truck",
  GROUND_INFANTRY="Ground_Infantry",
  GROUND_IFV="Ground_IFV",
  GROUND_ARTILLERY="Ground_Artillery",
  GROUND_TANK="Ground_Tank",
  GROUND_TRAIN="Ground_Train",
  GROUND_EWR="Ground_EWR",
  GROUND_AAA="Ground_AAA",
  GROUND_SAM="Ground_SAM",
  GROUND_SHORAD="Ground_SHORAD",
  GROUND_BALLISTICMISSILE="Ground_BallisticMissile",
  GROUND_OTHER="Ground_OtherGround",
  NAVAL_AIRCRAFTCARRIER="Naval_AircraftCarrier",
  NAVAL_WARSHIP="Naval_WarShip",
  NAVAL_ARMEDSHIP="Naval_ArmedShip",
  NAVAL_UNARMEDSHIP="Naval_UnarmedShip",
  NAVAL_OTHER="Naval_OtherNaval",
  OTHER_UNKNOWN="Other_Unknown",
}


--- Create a new GROUP from a given GroupTemplate as a parameter.
-- Note that the GroupTemplate is NOT spawned into the mission.
-- It is merely added to the @{Core.Database}.
-- @param #GROUP self
-- @param #table GroupTemplate The GroupTemplate Structure exactly as defined within the mission editor.
-- @param DCS#coalition.side CoalitionSide The coalition.side of the group.
-- @param DCS#Group.Category CategoryID The Group.Category of the group.
-- @param DCS#country.id CountryID the country.id of the group.
-- @return #GROUP self
function GROUP:NewTemplate( GroupTemplate, CoalitionSide, CategoryID, CountryID )
  local GroupName = GroupTemplate.name

  _DATABASE:_RegisterGroupTemplate( GroupTemplate, CoalitionSide, CategoryID, CountryID, GroupName )

  local self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) )
  self.GroupName = GroupName

  if not _DATABASE.GROUPS[GroupName] then
    _DATABASE.GROUPS[GroupName] = self
  end

  self:SetEventPriority( 4 )
  return self
end



--- Create a new GROUP from an existing Group in the Mission.
-- @param #GROUP self
-- @param #string GroupName The Group name
-- @return #GROUP self
function GROUP:Register( GroupName )

  local self = BASE:Inherit( self, CONTROLLABLE:New( GroupName ) ) -- #GROUP

  self.GroupName = GroupName

  self:SetEventPriority( 4 )
  return self
end

-- Reference methods.

--- Find the GROUP wrapper class instance using the DCS Group.
-- @param #GROUP self
-- @param DCS#Group DCSGroup The DCS Group.
-- @return #GROUP The GROUP.
function GROUP:Find( DCSGroup )

  local GroupName = DCSGroup:getName() -- Wrapper.Group#GROUP
  local GroupFound = _DATABASE:FindGroup( GroupName )
  return GroupFound
end

--- Find a GROUP using the DCS Group Name.
-- @param #GROUP self
-- @param #string GroupName The DCS Group Name.
-- @return #GROUP The GROUP.
function GROUP:FindByName( GroupName )

  local GroupFound = _DATABASE:FindGroup( GroupName )
  return GroupFound
end

--- Find the first(!) GROUP matching using patterns. Note that this is **a lot** slower than `:FindByName()`!
-- @param #GROUP self
-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA.
-- @return #GROUP The GROUP.
-- @usage
--          -- Find a group with a partial group name
--          local grp = GROUP:FindByMatching( "Apple" )
--          -- will return e.g. a group named "Apple-1-1"
--
--          -- using a pattern
--          local grp = GROUP:FindByMatching( ".%d.%d$" )
--          -- will return the first group found ending in "-1-1" to "-9-9", but not e.g. "-10-1"
function GROUP:FindByMatching( Pattern )
  local GroupFound = nil

  for name,group in pairs(_DATABASE.GROUPS) do
    if string.match(name, Pattern ) then
      GroupFound = group
      break
    end
  end

  return GroupFound
end

--- Find all GROUP objects matching using patterns. Note that this is **a lot** slower than `:FindByName()`!
-- @param #GROUP self
-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA.
-- @return #table Groups Table of matching #GROUP objects found
-- @usage
--          -- Find all group with a partial group name
--          local grptable = GROUP:FindAllByMatching( "Apple" )
--          -- will return all groups with "Apple" in the name
--
--          -- using a pattern
--          local grp = GROUP:FindAllByMatching( ".%d.%d$" )
--          -- will return the all groups found ending in "-1-1" to "-9-9", but not e.g. "-10-1" or "-1-10"
function GROUP:FindAllByMatching( Pattern )
  local GroupsFound = {}

  for name,group in pairs(_DATABASE.GROUPS) do
    if string.match(name, Pattern ) then
      GroupsFound[#GroupsFound+1] = group
    end
  end

  return GroupsFound
end

-- DCS Group methods support.

--- Returns the DCS Group.
-- @param #GROUP self
-- @return DCS#Group The DCS Group.
function GROUP:GetDCSObject()

  --if (not self.LastCallDCSObject) or (self.LastCallDCSObject and timer.getTime() - self.LastCallDCSObject  > 1) then

    -- Get DCS group.
    local DCSGroup = Group.getByName( self.GroupName )

    if DCSGroup then
      self.LastCallDCSObject = timer.getTime()
      self.DCSObject = DCSGroup
      return DCSGroup
   -- else
     -- self.DCSObject = nil
     -- self.LastCallDCSObject = nil
    end
  
  --else
    --return self.DCSObject
  --end
  
  --self:E(string.format("ERROR: Could not get DCS group object of group %s because DCS object could not be found!", tostring(self.GroupName)))
  return nil
end

--- Returns the @{DCS#Position3} position vectors indicating the point and direction vectors in 3D of the POSITIONABLE within the mission.
-- @param Wrapper.Positionable#POSITIONABLE self
-- @return DCS#Position The 3D position vectors of the POSITIONABLE or #nil if the groups not existing or alive.
function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3()
  --self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()

  if DCSPositionable then
   local unit = DCSPositionable:getUnits()[1]
   if unit then
     local PositionablePosition = unit:getPosition().p
    --self:T3( PositionablePosition )
    return PositionablePosition
    end
  end

  return nil
end

--- Returns if the group is alive.
-- The Group must:
--
--   * Exist at run-time.
--   * Has at least one unit.
--
-- When the first @{Wrapper.Unit} of the group is active, it will return true.
-- If the first @{Wrapper.Unit} of the group is inactive, it will return false.
--
-- @param #GROUP self
-- @return #boolean `true` if the group is alive *and* active, `false` if the group is alive but inactive or `#nil` if the group does not exist anymore.
function GROUP:IsAlive()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject() -- DCS#Group

  if DCSGroup then
    if DCSGroup:isExist() then
      local DCSUnit = DCSGroup:getUnit(1) -- DCS#Unit
      if DCSUnit then
        local GroupIsAlive = DCSUnit:isActive()
        --self:T3( GroupIsAlive )
        return GroupIsAlive
      end
    end
  end

  return nil
end

--- Returns if the group is activated.
-- @param #GROUP self
-- @return #boolean `true` if group is activated or `#nil` The group is not existing or alive.
function GROUP:IsActive()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject() -- DCS#Group

  if DCSGroup and DCSGroup:isExist() then
    local unit = DCSGroup:getUnit(1)
    if unit then
      local GroupIsActive = unit:isActive()
    return GroupIsActive
    end
  end

  return nil
end



--- Destroys the DCS Group and all of its DCS Units.
-- Note that this destroy method also can raise a destroy event at run-time.
-- So all event listeners will catch the destroy event of this group for each unit in the group.
-- To raise these events, provide the `GenerateEvent` parameter.
-- @param #GROUP self
-- @param #boolean GenerateEvent If true, a crash [AIR] or dead [GROUND] event for each unit is generated. If false, if no event is triggered. If nil, a RemoveUnit event is triggered.
-- @param #number delay Delay in seconds before despawning the group.
-- @usage
-- -- Air unit example: destroy the Helicopter and generate a S_EVENT_CRASH for each unit in the Helicopter group.
-- Helicopter = GROUP:FindByName( "Helicopter" )
-- Helicopter:Destroy( true )
-- @usage
-- -- Ground unit example: destroy the Tanks and generate a S_EVENT_DEAD for each unit in the Tanks group.
-- Tanks = GROUP:FindByName( "Tanks" )
-- Tanks:Destroy( true )
-- @usage
-- -- Ship unit example: destroy the Ship silently.
-- Ship = GROUP:FindByName( "Ship" )
-- Ship:Destroy()
--
-- @usage
-- -- Destroy without event generation example.
-- Ship = GROUP:FindByName( "Boat" )
-- Ship:Destroy( false ) -- Don't generate an event upon destruction.
--
function GROUP:Destroy( GenerateEvent, delay )
  --self:F2( self.GroupName )

  if delay and delay>0 then
    self:ScheduleOnce(delay, GROUP.Destroy, self, GenerateEvent)
  else

    --local DCSGroup = self:GetDCSObject()
    local DCSGroup = Group.getByName( self.GroupName )

    if DCSGroup then
      for Index, UnitData in pairs( DCSGroup:getUnits() ) do
        if GenerateEvent and GenerateEvent == true then
          if self:IsAir() then
            self:CreateEventCrash( timer.getTime(), UnitData )
            --self:ScheduleOnce(1,self.CreateEventCrash,self,timer.getTime(),UnitData)
          else
            self:CreateEventDead( timer.getTime(), UnitData )
            --self:ScheduleOnce(1,self.CreateEventDead,self,timer.getTime(),UnitData)
          end
        elseif GenerateEvent == false then
          -- Do nothing!
        else
          self:CreateEventRemoveUnit( timer.getTime(), UnitData )
          --self:ScheduleOnce(1,self.CreateEventRemoveUnit,self,timer.getTime(),UnitData)
        end
      end
      USERFLAG:New( self:GetName() ):Set( 100 )
      DCSGroup:destroy()
      DCSGroup = nil
    end
  end

  return nil
end


--- Returns category of the DCS Group. Returns one of
--
-- * Group.Category.AIRPLANE
-- * Group.Category.HELICOPTER
-- * Group.Category.GROUND
-- * Group.Category.SHIP
-- * Group.Category.TRAIN
--
-- @param #GROUP self
-- @return DCS#Group.Category The category ID.
function GROUP:GetCategory()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()
  if DCSGroup then
    local GroupCategory = DCSGroup:getCategory()
    --self:T3( GroupCategory )
    return GroupCategory
  end

  return nil
end

--- Returns the category name of the #GROUP.
-- @param #GROUP self
-- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship, Train.
function GROUP:GetCategoryName()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()
  if DCSGroup then
    local CategoryNames = {
      [Group.Category.AIRPLANE] = "Airplane",
      [Group.Category.HELICOPTER] = "Helicopter",
      [Group.Category.GROUND] = "Ground Unit",
      [Group.Category.SHIP] = "Ship",
      [Group.Category.TRAIN] = "Train",
    }
    local GroupCategory = DCSGroup:getCategory()
    --self:T3( GroupCategory )

    return CategoryNames[GroupCategory]
  end

  return nil
end

--- Returns the coalition of the DCS Group.
-- @param #GROUP self
-- @return DCS#coalition.side The coalition side of the DCS Group.
function GROUP:GetCoalition()
  --self:F2( self.GroupName )
  if self.GroupCoalition ~= nil then
    return self.GroupCoalition
  else
    local DCSGroup = self:GetDCSObject()
    if DCSGroup then
      local GroupCoalition = DCSGroup:getCoalition()
      --self:T3( GroupCoalition )
      self.GroupCoalition = GroupCoalition
      return GroupCoalition
    end
  end
  return nil
end

--- Returns the country of the DCS Group.
-- @param #GROUP self
-- @return DCS#country.id The country identifier or nil if the DCS Group is not existing or alive.
function GROUP:GetCountry()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()
  if DCSGroup then
    local GroupCountry = DCSGroup:getUnit(1):getCountry()
    --self:T3( GroupCountry )
    return GroupCountry
  end

  return nil
end


--- Check if at least one (or all) unit(s) has (have) a certain attribute.
-- See [hoggit documentation](https://wiki.hoggitworld.com/view/DCS_func_hasAttribute).
-- @param #GROUP self
-- @param #string attribute The name of the attribute the group is supposed to have. Valid attributes can be found in the "db_attributes.lua" file which is located at in "C:\Program Files\Eagle Dynamics\DCS World\Scripts\Database".
-- @param #boolean all If true, all units of the group must have the attribute in order to return true. Default is only one unit of a heterogenious group needs to have the attribute.
-- @return #boolean Group has this attribute.
function GROUP:HasAttribute(attribute, all)

  -- Get all units of the group.
  local _units=self:GetUnits()

  if _units then

    local _allhave=true
    local _onehas=false

    for _,_unit in pairs(_units) do
      local _unit=_unit --Wrapper.Unit#UNIT
      if _unit then
        local _hastit=_unit:HasAttribute(attribute)
        if _hastit==true then
          _onehas=true
        else
          _allhave=false
        end
      end
    end

    if all==true then
      return _allhave
    else
      return _onehas
    end

  end

  return nil
end

--- Returns the maximum speed of the group.
-- If the group is heterogenious and consists of different units, the max speed of the slowest unit is returned.
-- @param #GROUP self
-- @return #number Speed in km/h.
function GROUP:GetSpeedMax()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()
  if DCSGroup then

    local Units=self:GetUnits()

    local speedmax=nil

    for _,unit in pairs(Units) do
      local unit=unit --Wrapper.Unit#UNIT

      local speed=unit:GetSpeedMax()

      if speedmax==nil or speed<speedmax then
        speedmax=speed
      end

      --env.info(string.format("FF unit %s: speed=%.1f, speedmax=%.1f", unit:GetName(), speed, speedmax))

    end

    return speedmax
  end

  return nil
end

--- Returns the maximum range of the group.
-- If the group is heterogenious and consists of different units, the smallest range of all units is returned.
-- @param #GROUP self
-- @return #number Range in meters.
function GROUP:GetRange()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()
  if DCSGroup then

    local Units=self:GetUnits()

    local Rangemin=nil

    for _,unit in pairs(Units) do
      local unit=unit --Wrapper.Unit#UNIT
      local range=unit:GetRange()
      if range then
        if Rangemin==nil then
          Rangemin=range
        elseif range<Rangemin then
          Rangemin=range
        end
      end
    end

    return Rangemin
  end

  return nil
end

--- Returns a list of @{Wrapper.Unit} objects of the @{Wrapper.Group}.
-- @param #GROUP self
-- @return #table of Wrapper.Unit#UNIT objects, indexed by number.
function GROUP:GetUnits()
  --self:F2( { self.GroupName } )
  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local DCSUnits = DCSGroup:getUnits() or {}
    local Units = {}
    for Index, UnitData in pairs( DCSUnits ) do

      local unit=UNIT:Find( UnitData )
      if unit then
        Units[#Units+1] = UNIT:Find( UnitData )
      else
        local UnitName=UnitData:getName()
        unit=_DATABASE:AddUnit(UnitName)
        Units[#Units+1]=unit
      end
    end
    --self:T3( Units )
    return Units
  end

  return nil
end

--- Returns a list of @{Wrapper.Unit} objects of the @{Wrapper.Group} that are occupied by a player.
-- @param #GROUP self
-- @return #list<Wrapper.Unit#UNIT> The list of player occupied @{Wrapper.Unit} objects of the @{Wrapper.Group}.
function GROUP:GetPlayerUnits()
  --self:F2( { self.GroupName } )
  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local DCSUnits = DCSGroup:getUnits()
    local Units = {}
    for Index, UnitData in pairs( DCSUnits ) do
      local PlayerUnit = UNIT:Find( UnitData )
      if PlayerUnit:GetPlayerName() then
        Units[#Units+1] = PlayerUnit
      end
    end
    --self:T3( Units )
    return Units
  end

  return nil
end

--- Check if an (air) group is a client or player slot. Information is retrieved from the group template.
-- @param #GROUP self
-- @return #boolean If true, group is associated with a client or player slot.
function GROUP:IsPlayer()
  local unit = self:GetUnit(1)
  if unit then
    return unit:IsPlayer()
  end
  return false
end

--- Returns the UNIT wrapper object with number UnitNumber. If it doesn't exist, tries to return the next available unit.
-- If no underlying DCS Units exist, the method will return nil.
-- @param #GROUP self
-- @param #number UnitNumber The number of the UNIT wrapper class to be returned.
-- @return Wrapper.Unit#UNIT The UNIT object or nil
function GROUP:GetUnit( UnitNumber )
  local DCSGroup = self:GetDCSObject()
  if DCSGroup then
    local UnitFound = nil
    -- 2.7.1 dead event bug, return the first alive unit instead
    -- Maybe fixed with 2.8?
    local units = DCSGroup:getUnits() or {}
    if units[UnitNumber] then
      local UnitFound = UNIT:Find(units[UnitNumber])
      if UnitFound then
        return UnitFound
      end
    else
      for _,_unit in pairs(units) do
        local UnitFound = UNIT:Find(_unit)
        if UnitFound then
          return UnitFound
        end
      end
    end
  end
  return nil
end


--- Returns the DCS Unit with number UnitNumber.
-- If the underlying DCS Unit does not exist, the method will return try to find the next unit. Returns nil if no units are found.
-- @param #GROUP self
-- @param #number UnitNumber The number of the DCS Unit to be returned.
-- @return DCS#Unit The DCS Unit.
function GROUP:GetDCSUnit( UnitNumber )

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then

    if DCSGroup.getUnit and DCSGroup:getUnit( UnitNumber ) then
      return DCSGroup:getUnit( UnitNumber )
    else

      -- 2.7.1 dead event bug, return the first alive unit instead
      local units = DCSGroup:getUnits() or {}

      for _,_unit in pairs(units) do
        if _unit and _unit:isExist() then
          return _unit
        end
      end
    end
  end

  return nil
end

--- Returns current size of the DCS Group.
-- If some of the DCS Units of the DCS Group are destroyed the size of the DCS Group is changed.
-- @param #GROUP self
-- @return #number The DCS Group size.
function GROUP:GetSize()

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then

    local GroupSize = DCSGroup:getSize()

    if GroupSize then
      return GroupSize
    else
      return 0
    end
  end

  return nil
end

--- Count number of alive units in the group.
-- @param #GROUP self
-- @return #number Number of alive units. If DCS group is nil, 0 is returned.
function GROUP:CountAliveUnits()
  --self:F3( { self.GroupName } )
  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local units=self:GetUnits()
    local n=0
    for _,_unit in pairs(units) do
      local unit=_unit --Wrapper.Unit#UNIT
      if unit and unit:IsAlive() then
        n=n+1
      end
    end
    return n
  end

  return 0
end

--- Get the first unit of the group which is alive.
-- @param #GROUP self
-- @return Wrapper.Unit#UNIT First unit alive.
function GROUP:GetFirstUnitAlive()
  --self:F3({self.GroupName})
  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local units=self:GetUnits()
    for _,_unit in pairs(units) do
      local unit=_unit --Wrapper.Unit#UNIT
      if unit and unit:IsAlive() then
        return unit
      end
    end
  end

  return nil
end

--- Get the first unit of the group. Might be nil!
-- @param #GROUP self
-- @return Wrapper.Unit#UNIT First unit or nil if it does not exist.
function GROUP:GetFirstUnit()
  --self:F3({self.GroupName})
  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local units=self:GetUnits()
    return units[1]
  end

  return nil
end

--- Returns the average velocity Vec3 vector.
-- @param Wrapper.Group#GROUP self
-- @return DCS#Vec3 The velocity Vec3 vector or `#nil` if the GROUP is not existing or alive.
function GROUP:GetVelocityVec3()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()

  if DCSGroup and DCSGroup:isExist() then
    local GroupUnits = DCSGroup:getUnits()
    local GroupCount = 0

    local VelocityVec3 = { x = 0, y = 0, z = 0 }

    for _, DCSUnit in pairs( GroupUnits ) do
        if DCSUnit:isExist() and DCSUnit:isActive() then
          local UnitVelocityVec3 = DCSUnit:getVelocity()
          VelocityVec3.x = VelocityVec3.x + UnitVelocityVec3.x
          VelocityVec3.y = VelocityVec3.y + UnitVelocityVec3.y
          VelocityVec3.z = VelocityVec3.z + UnitVelocityVec3.z
            GroupCount = GroupCount + 1
        end
    end

    VelocityVec3.x = VelocityVec3.x / GroupCount
    VelocityVec3.y = VelocityVec3.y / GroupCount
    VelocityVec3.z = VelocityVec3.z / GroupCount

    return VelocityVec3
  end

  BASE:E( { "Cannot GetVelocityVec3", Group = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns the average group altitude in meters.
-- @param Wrapper.Group#GROUP self
-- @param #boolean FromGround Measure from the ground or from sea level (ASL). Provide **true** for measuring from the ground (AGL). **false** or **nil** if you measure from sea level.
-- @return #number The altitude of the group or nil if is not existing or alive.
function GROUP:GetAltitude(FromGround)
  --self:F2( self.GroupName )
  return self:GetHeight(FromGround)
end

--- Returns the average group height in meters.
-- @param Wrapper.Group#GROUP self
-- @param #boolean FromGround Measure from the ground or from sea level (ASL). Provide **true** for measuring from the ground (AGL). **false** or **nil** if you measure from sea level.
-- @return #number The height of the group or nil if is not existing or alive.
function GROUP:GetHeight( FromGround )
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local GroupUnits = DCSGroup:getUnits()
    local GroupCount = #GroupUnits

    local GroupHeight = 0

    for _, DCSUnit in pairs( GroupUnits ) do
      local GroupPosition = DCSUnit:getPosition()

      if FromGround == true then
        local LandHeight =  land.getHeight( { x = GroupPosition.p.x, y = GroupPosition.p.z } )
        GroupHeight = GroupHeight + ( GroupPosition.p.y - LandHeight )
      else
        GroupHeight = GroupHeight + GroupPosition.p.y
      end
    end

    return GroupHeight / GroupCount
  end

  return nil
end




---
--- Returns the initial size of the DCS Group.
-- If some of the DCS Units of the DCS Group are destroyed, the initial size of the DCS Group is unchanged.
-- @param #GROUP self
-- @return #number The DCS Group initial size.
function GROUP:GetInitialSize()
  --self:F3( { self.GroupName } )
  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local GroupInitialSize = DCSGroup:getInitialSize()
    --self:T3( GroupInitialSize )
    return GroupInitialSize
  end

  return nil
end


--- Returns the DCS Units of the DCS Group.
-- @param #GROUP self
-- @return #table The DCS Units.
function GROUP:GetDCSUnits()
  --self:F2( { self.GroupName } )
  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local DCSUnits = DCSGroup:getUnits()
    --self:T3( DCSUnits )
    return DCSUnits
  end

  return nil
end


--- Activates a late activated GROUP.
-- @param #GROUP self
-- @param #number delay Delay in seconds, before the group is activated.
-- @return #GROUP self
function GROUP:Activate(delay)
  --self:F2( { self.GroupName } )
  if delay and delay>0 then
    self:ScheduleOnce(delay, GROUP.Activate, self)
  else
    trigger.action.activateGroup( self:GetDCSObject() )
  end
  return self
end

--- Deactivates an activated GROUP.
-- @param #GROUP self
-- @param #number delay Delay in seconds, before the group is activated.
-- @return #GROUP self
function GROUP:Deactivate(delay)
  --self:F2( { self.GroupName } )
  if delay and delay>0 then
    self:ScheduleOnce(delay, GROUP.Deactivate, self)
  else
    trigger.action.deactivateGroup( self:GetDCSObject() )
  end
  return self
end


--- Gets the type name of the group.
-- @param #GROUP self
-- @return #string The type name of the group.
function GROUP:GetTypeName()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local GroupTypeName = DCSGroup:getUnit(1):getTypeName()
    --self:T3( GroupTypeName )
    return( GroupTypeName )
  end

  return nil
end

--- [AIRPLANE] Get the NATO reporting name (platform, e.g. "Flanker") of a GROUP (note - first unit the group). "Bogey" if not found. Currently airplanes only!
--@param #GROUP self
--@return #string NatoReportingName or "Bogey" if unknown.
function GROUP:GetNatoReportingName()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local GroupTypeName = DCSGroup:getUnit(1):getTypeName()
    --self:T3( GroupTypeName )
    return UTILS.GetReportingName(GroupTypeName)
  end

  return "Bogey"

end

--- Gets the player name of the group.
-- @param #GROUP self
-- @return #string The player name of the group.
function GROUP:GetPlayerName()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local PlayerName = DCSGroup:getUnit(1):getPlayerName()
    --self:T3( PlayerName )
    return( PlayerName )
  end

  return nil
end


--- Gets the CallSign of the first DCS Unit of the DCS Group.
-- @param #GROUP self
-- @return #string The CallSign of the first DCS Unit of the DCS Group.
function GROUP:GetCallsign()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local GroupCallSign = DCSGroup:getUnit(1):getCallsign()
    --self:T3( GroupCallSign )
    return GroupCallSign
  end

  BASE:E( { "Cannot GetCallsign", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns the current point (Vec2 vector) of the first DCS Unit in the DCS Group.
-- @param #GROUP self
-- @return DCS#Vec2 Current Vec2 point of the first DCS Unit of the DCS Group.
function GROUP:GetVec2()

  local Unit=self:GetUnit(1)

  if Unit then
    local vec2=Unit:GetVec2()
    return vec2
  end

end

--- Returns the current Vec3 vector of the first Unit in the GROUP.
-- @param #GROUP self
-- @return DCS#Vec3 Current Vec3 of the first Unit of the GROUP or nil if cannot be found.
function GROUP:GetVec3()

  -- Get first unit.
  local unit=self:GetUnit(1)

  if unit then
    local vec3=unit:GetVec3()
    return vec3
  end

  self:E("ERROR: Cannot get Vec3 of group "..tostring(self.GroupName))
  return nil
end

--- Returns the average Vec3 vector of the Units in the GROUP.
-- @param #GROUP self
-- @return DCS#Vec3 Current Vec3 of the GROUP  or nil if cannot be found.
function GROUP:GetAverageVec3()
  local units = self:GetUnits() or {}
    -- Init.
  local x=0 ; local y=0 ; local z=0 ; local n=0
  -- Loop over all units.
  for _,unit in pairs(units) do
    local vec3=nil --DCS#Vec3
    if unit and unit:IsAlive() then
      vec3 = unit:GetVec3()
    end
    if vec3 then
      -- Sum up posits.
      x=x+vec3.x
      y=y+vec3.y
      z=z+vec3.z
      -- Increase counter.
      n=n+1
    end
  end

  if n>0 then
    -- Average.
    local Vec3={x=x/n, y=y/n, z=z/n} --DCS#Vec3
    return Vec3
  else
    return self:GetVec3()
  end
end

--- Returns a COORDINATE object indicating the point in 2D of the first UNIT of the GROUP within the mission.
-- @param #GROUP self
-- @return Core.Point#COORDINATE The 3D point vector of the first DCS Unit of the GROUP.
-- @return #nil The first UNIT is not existing or alive.
function GROUP:GetPointVec2()
  --self:F2(self.GroupName)

  local FirstUnit = self:GetUnit(1)

  if FirstUnit then
    local FirstUnitPointVec2 = FirstUnit:GetPointVec2()
    --self:T3(FirstUnitPointVec2)
    return FirstUnitPointVec2
  end

  BASE:E( { "Cannot get COORDINATE", Group = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns a COORDINATE object indicating the average position of the GROUP within the mission.
-- @param Wrapper.Group#GROUP self
-- @return Core.Point#COORDINATE The COORDINATE of the GROUP.
function GROUP:GetAverageCoordinate()
  local vec3 = self:GetAverageVec3()
  if vec3 then
    local coord = COORDINATE:NewFromVec3(vec3)
    local Heading = self:GetHeading()
    coord.Heading = Heading
    return coord
  else
    local coord = self:GetCoordinate()
    if coord then
      return coord
    else
      BASE:E( { "Cannot GetAverageCoordinate", Group = self, Alive = self:IsAlive() } )
      return nil
    end
  end
end

--- Returns a COORDINATE object indicating the point of the first UNIT of the GROUP within the mission.
-- @param Wrapper.Group#GROUP self
-- @return Core.Point#COORDINATE The COORDINATE of the GROUP.
function GROUP:GetCoordinate()

  -- First try to get the 3D vector of the group. This uses 
  local vec3=self:GetVec3()
  local coord
  if vec3 then
    coord=COORDINATE:NewFromVec3(vec3)
    coord.Heading = self:GetHeading() or 0
    return coord
  end

  -- No luck try units and add Heading data
  local Units = self:GetUnits() or {}

  for _,_unit in pairs(Units) do
    local FirstUnit = _unit -- Wrapper.Unit#UNIT

    if FirstUnit and FirstUnit:IsAlive() then

      local FirstUnitCoordinate = FirstUnit:GetCoordinate()

      if FirstUnitCoordinate then
        local Heading = self:GetHeading() or 0
        FirstUnitCoordinate.Heading = Heading
        return FirstUnitCoordinate
      end

    end
  end
  
  -- no luck, try the API way  
  local DCSGroup = Group.getByName(self.GroupName)
  if DCSGroup then
    local DCSUnits = DCSGroup:getUnits() or {}
    for _,_unit in pairs(DCSUnits) do
      if Object.isExist(_unit) then
        local position = _unit:getPosition()
        local point = position.p ~= nil and position.p or _unit:GetPoint()
        if point then
          --self:I(point)
          local coord = COORDINATE:NewFromVec3(point)
          coord.Heading = 0
          local munit = UNIT:Find(_unit)
          if munit then
            coord.Heading = munit:GetHeading() or 0
          end
          return coord
        end
      end
    end
  end
    
  BASE:E( { "Cannot GetCoordinate", Group = self, Alive = self:IsAlive() } )
  
end


--- Returns a random @{DCS#Vec3} vector (point in 3D of the UNIT within the mission) within a range around the first UNIT of the GROUP.
-- @param #GROUP self
-- @param #number Radius Radius in meters.
-- @return DCS#Vec3 The random 3D point vector around the first UNIT of the GROUP or #nil The GROUP is invalid or empty.
-- @usage
-- -- If Radius is ignored, returns the DCS#Vec3 of first UNIT of the GROUP
function GROUP:GetRandomVec3(Radius)
  --self:F2(self.GroupName)

  local FirstUnit = self:GetUnit(1)

  if FirstUnit then
    local FirstUnitRandomPointVec3 = FirstUnit:GetRandomVec3(Radius)
    --self:T3(FirstUnitRandomPointVec3)
    return FirstUnitRandomPointVec3
  end

  BASE:E( { "Cannot GetRandomVec3", Group = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns the mean heading of every UNIT in the GROUP in degrees
-- @param #GROUP self
-- @return #number Mean heading of the GROUP in degrees or #nil The first UNIT is not existing or alive.
function GROUP:GetHeading()
  --self:F2(self.GroupName)

  --self:F2(self.GroupName)

  local GroupSize = self:GetSize()
  local HeadingAccumulator = 0
  local n=0
  local Units = self:GetUnits()

  if GroupSize then
    for _,unit in pairs(Units) do
      if unit and unit:IsAlive() then
        HeadingAccumulator = HeadingAccumulator + unit:GetHeading()
        n=n+1
      end
    end
    return math.floor(HeadingAccumulator / n)
  end

  BASE:E( { "Cannot GetHeading", Group = self, Alive = self:IsAlive() } )

  return nil

end

--- Return the fuel state and unit reference for the unit with the least
-- amount of fuel in the group.
-- @param #GROUP self
-- @return #number The fuel state of the unit with the least amount of fuel.
-- @return Wrapper.Unit#UNIT reference to #Unit object for further processing.
function GROUP:GetFuelMin()
  --self:F3(self.ControllableName)

  if not self:GetDCSObject() then
    BASE:E( { "Cannot GetFuel", Group = self, Alive = self:IsAlive() } )
    return 0
  end

  local min  = 65535  -- some sufficiently large number to init with
  local unit = nil
  local tmp  = nil

  for UnitID, UnitData in pairs( self:GetUnits() ) do
    if UnitData and UnitData:IsAlive() then
      tmp = UnitData:GetFuel()
      if tmp < min then
        min = tmp
        unit = UnitData
      end
    end
  end

  return min, unit
end

--- Returns relative amount of fuel (from 0.0 to 1.0) the group has in its
--  internal tanks. If there are additional fuel tanks the value may be
--  greater than 1.0.
-- @param #GROUP self
-- @return #number The relative amount of fuel (from 0.0 to 1.0).
-- @return #nil The GROUP is not existing or alive.
function GROUP:GetFuelAvg()
  --self:F( self.ControllableName )

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then
    local GroupSize = self:GetSize()
    local TotalFuel = 0
    for UnitID, UnitData in pairs( self:GetUnits() ) do
      local Unit = UnitData -- Wrapper.Unit#UNIT
      local UnitFuel = Unit:GetFuel() or 0
      --self:F( { Fuel = UnitFuel } )
      TotalFuel = TotalFuel + UnitFuel
    end
    local GroupFuel = TotalFuel / GroupSize
    return GroupFuel
  end

  BASE:E( { "Cannot GetFuel", Group = self, Alive = self:IsAlive() } )

  return 0
end

--- Returns relative amount of fuel (from 0.0 to 1.0) the group has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0.
-- @param #GROUP self
-- @return #number The relative amount of fuel (from 0.0 to 1.0).
-- @return #nil The GROUP is not existing or alive.
function GROUP:GetFuel()
  return self:GetFuelAvg()
end


--- Get the number of shells, rockets, bombs and missiles the whole group currently has.
-- @param #GROUP self
-- @return #number Total amount of ammo the group has left. This is the sum of shells, rockets, bombs and missiles of all units.
-- @return #number Number of shells left.
-- @return #number Number of rockets left.
-- @return #number Number of bombs left.
-- @return #number Number of missiles left.
-- @return #number Number of artillery shells left (with explosive mass, included in shells; shells can also be machine gun ammo)
function GROUP:GetAmmunition()
  --self:F( self.ControllableName )

  local DCSControllable = self:GetDCSObject()

  local Ntot=0
  local Nshells=0
  local Nrockets=0
  local Nmissiles=0
  local Nbombs=0
  local Narti=0

  if DCSControllable then

    -- Loop over units.
    for UnitID, UnitData in pairs( self:GetUnits() ) do
      local Unit = UnitData -- Wrapper.Unit#UNIT

      -- Get ammo of the unit
      local ntot, nshells, nrockets, nbombs, nmissiles, narti = Unit:GetAmmunition()

      Ntot=Ntot+ntot
      Nshells=Nshells+nshells
      Nrockets=Nrockets+nrockets
      Nmissiles=Nmissiles+nmissiles
      Nbombs=Nbombs+nbombs
      Narti=Narti+narti
    end

  end

  return Ntot, Nshells, Nrockets, Nbombs, Nmissiles, Narti
end


do -- Is Zone methods


--- Check if any unit of a group is inside a @{Core.Zone}.
-- @param #GROUP self
-- @param Core.Zone#ZONE_BASE Zone The zone to test.
-- @return #boolean Returns `true` if *at least one unit* is inside the zone or `false` if *no* unit is inside.
function GROUP:IsInZone( Zone )

  if self:IsAlive() then

    for UnitID, UnitData in pairs(self:GetUnits()) do
      local Unit = UnitData -- Wrapper.Unit#UNIT

      local vec2 = nil
      if Unit then
       -- Get 2D vector. That's all we need for the zone check.
       vec2=Unit:GetVec2()
      end

      if vec2 and Zone:IsVec2InZone(vec2) then
        return true  -- At least one unit is in the zone. That is enough.
      end

    end

    return false
  end

  return nil
end

--- Returns true if all units of the group are within a @{Core.Zone}.
-- @param #GROUP self
-- @param Core.Zone#ZONE_BASE Zone The zone to test.
-- @return #boolean Returns true if the Group is completely within the @{Core.Zone#ZONE_BASE}
function GROUP:IsCompletelyInZone( Zone )
  --self:F2( { self.GroupName, Zone } )

  if not self:IsAlive() then return false end

  for UnitID, UnitData in pairs( self:GetUnits() ) do
    local Unit = UnitData -- Wrapper.Unit#UNIT
    if Zone:IsVec3InZone( Unit:GetVec3() ) then
    else
      return false
    end
  end

  return true
end

--- Returns true if some but NOT ALL units of the group are within a @{Core.Zone}.
-- @param #GROUP self
-- @param Core.Zone#ZONE_BASE Zone The zone to test.
-- @return #boolean Returns true if the Group is partially within the @{Core.Zone#ZONE_BASE}
function GROUP:IsPartlyInZone( Zone )
  --self:F2( { self.GroupName, Zone } )

  local IsOneUnitInZone = false
  local IsOneUnitOutsideZone = false

  if not self:IsAlive() then return false end

  for UnitID, UnitData in pairs( self:GetUnits() ) do
    local Unit = UnitData -- Wrapper.Unit#UNIT
    if Zone:IsVec3InZone( Unit:GetVec3() ) then
      IsOneUnitInZone = true
    else
      IsOneUnitOutsideZone = true
    end
  end

  if IsOneUnitInZone and IsOneUnitOutsideZone then
    return true
  else
    return false
  end
end

--- Returns true if part or all units of the group are within a @{Core.Zone}.
-- @param #GROUP self
-- @param Core.Zone#ZONE_BASE Zone The zone to test.
-- @return #boolean Returns true if the Group is partially or completely within the @{Core.Zone#ZONE_BASE}.
function GROUP:IsPartlyOrCompletelyInZone( Zone )
  return self:IsPartlyInZone(Zone) or self:IsCompletelyInZone(Zone)
end

--- Returns true if none of the group units of the group are within a @{Core.Zone}.
-- @param #GROUP self
-- @param Core.Zone#ZONE_BASE Zone The zone to test.
-- @return #boolean Returns true if the Group is not within the @{Core.Zone#ZONE_BASE}
function GROUP:IsNotInZone( Zone )
  --self:F2( { self.GroupName, Zone } )

  if not self:IsAlive() then return true end

  for UnitID, UnitData in pairs( self:GetUnits() ) do
    local Unit = UnitData -- Wrapper.Unit#UNIT
    if Zone:IsVec3InZone( Unit:GetVec3() ) then
      return false
    end
  end

  return true
end

--- Returns true if any units of the group are within a @{Core.Zone}.
-- @param #GROUP self
-- @param Core.Zone#ZONE_BASE Zone The zone to test.
-- @return #boolean Returns true if any unit of the Group is within the @{Core.Zone#ZONE_BASE}
function GROUP:IsAnyInZone( Zone )

  if not self:IsAlive() then return false end

  for UnitID, UnitData in pairs( self:GetUnits() ) do
    local Unit = UnitData -- Wrapper.Unit#UNIT
    if Zone:IsVec3InZone( Unit:GetVec3() ) then
      return true
    end
  end
  return false
end

--- Returns the number of UNITs that are in the @{Core.Zone}
-- @param #GROUP self
-- @param Core.Zone#ZONE_BASE Zone The zone to test.
-- @return #number The number of UNITs that are in the @{Core.Zone}
function GROUP:CountInZone( Zone )
  --self:F2( {self.GroupName, Zone} )
  local Count = 0

  if not self:IsAlive() then return Count end

  for UnitID, UnitData in pairs( self:GetUnits() ) do
    local Unit = UnitData -- Wrapper.Unit#UNIT
    if Zone:IsVec3InZone( Unit:GetVec3() ) then
      Count = Count + 1
    end
  end

  return Count
end

--- Returns if the group is of an air category.
-- If the group is a helicopter or a plane, then this method will return true, otherwise false.
-- @param #GROUP self
-- @return #boolean Air category evaluation result.
function GROUP:IsAir()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local IsAirResult = DCSGroup:getCategory() == Group.Category.AIRPLANE or DCSGroup:getCategory() == Group.Category.HELICOPTER
    --self:T3( IsAirResult )
    return IsAirResult
  end

  return nil
end

--- Returns if the DCS Group contains Helicopters.
-- @param #GROUP self
-- @return #boolean true if DCS Group contains Helicopters.
function GROUP:IsHelicopter()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local GroupCategory = DCSGroup:getCategory()
    --self:T2( GroupCategory )
    return GroupCategory == Group.Category.HELICOPTER
  end

  return nil
end

--- Returns if the DCS Group contains AirPlanes.
-- @param #GROUP self
-- @return #boolean true if DCS Group contains AirPlanes.
function GROUP:IsAirPlane()
  --self:F2()

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local GroupCategory = DCSGroup:getCategory()
    --self:T2( GroupCategory )
    return GroupCategory == Group.Category.AIRPLANE
  end

  return nil
end

--- Returns if the DCS Group contains Ground troops.
-- @param #GROUP self
-- @return #boolean true if DCS Group contains Ground troops.
function GROUP:IsGround()
  --self:F2()

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local GroupCategory = DCSGroup:getCategory()
    --self:T2( GroupCategory )
    return GroupCategory == Group.Category.GROUND
  end

  return nil
end

--- Returns if the DCS Group contains Ships.
-- @param #GROUP self
-- @return #boolean true if DCS Group contains Ships.
function GROUP:IsShip()
  --self:F2()

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local GroupCategory = DCSGroup:getCategory()
    --self:T2( GroupCategory )
    return GroupCategory == Group.Category.SHIP
  end

  return nil
end

--- Returns if all units of the group are on the ground or landed.
-- If all units of this group are on the ground, this function will return true, otherwise false.
-- @param #GROUP self
-- @return #boolean All units on the ground result.
function GROUP:AllOnGround()
  --self:F2()

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local AllOnGroundResult = true

    for Index, UnitData in pairs( DCSGroup:getUnits() ) do
      if UnitData:inAir() then
        AllOnGroundResult = false
      end
    end

    --self:T3( AllOnGroundResult )
    return AllOnGroundResult
  end

  return nil
end

end

do -- AI methods

  --- Turns the AI On or Off for the GROUP.
  -- @param #GROUP self
  -- @param #boolean AIOnOff The value true turns the AI On, the value false turns the AI Off.
  -- @return #GROUP The GROUP.
  function GROUP:SetAIOnOff( AIOnOff )

    local DCSGroup = self:GetDCSObject() -- DCS#Group

    if DCSGroup then
      local DCSController = DCSGroup:getController() -- DCS#Controller
      if DCSController then
        DCSController:setOnOff( AIOnOff )
        return self
      end
    end

    return nil
  end

  --- Turns the AI On for the GROUP.
  -- @param #GROUP self
  -- @return #GROUP The GROUP.
  function GROUP:SetAIOn()

    return self:SetAIOnOff( true )
  end

  --- Turns the AI Off for the GROUP.
  -- @param #GROUP self
  -- @return #GROUP The GROUP.
  function GROUP:SetAIOff()

    return self:SetAIOnOff( false )
  end

end



--- Returns the current maximum velocity of the group.
-- Each unit within the group gets evaluated, and the maximum velocity (= the unit which is going the fastest) is returned.
-- @param #GROUP self
-- @return #number Maximum velocity found.
function GROUP:GetMaxVelocity()
  --self:F2()

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local GroupVelocityMax = 0

    for Index, UnitData in pairs( DCSGroup:getUnits() ) do

        if UnitData:isExist() and UnitData:isActive() then
          local UnitVelocityVec3 = UnitData:getVelocity()
          local UnitVelocity = math.abs( UnitVelocityVec3.x ) + math.abs( UnitVelocityVec3.y ) + math.abs( UnitVelocityVec3.z )

          if UnitVelocity > GroupVelocityMax then
            GroupVelocityMax = UnitVelocity
          end
      end
    end

    return GroupVelocityMax
  end

  return nil
end

--- Returns the current minimum height of the group.
-- Each unit within the group gets evaluated, and the minimum height (= the unit which is the lowest elevated) is returned.
-- @param #GROUP self
-- @return #number Minimum height found.
function GROUP:GetMinHeight()
  --self:F2()

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local GroupHeightMin = 999999999

    for Index, UnitData in pairs( DCSGroup:getUnits() ) do
      local UnitData = UnitData -- DCS#Unit

      local UnitHeight = UnitData:getPoint()

      if UnitHeight < GroupHeightMin then
        GroupHeightMin = UnitHeight
      end
    end

    return GroupHeightMin
  end

  return nil
end

--- Returns the current maximum height of the group, i.e. the highest unit height of that group.
-- Each unit within the group gets evaluated, and the maximum height (= the unit which is the highest elevated) is returned.
-- @param #GROUP self
-- @return #number Maximum height found.
function GROUP:GetMaxHeight()
  --self:F2()

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local GroupHeightMax = -999999999

    for Index, UnitData in pairs( DCSGroup:getUnits() ) do
      local UnitData = UnitData -- DCS#Unit

      local UnitHeight = UnitData:getPoint().p.y -- Height -- found by @Heavydrinker

      if UnitHeight > GroupHeightMax then
        GroupHeightMax = UnitHeight
      end
    end

    return GroupHeightMax
  end

  return nil
end

-- RESPAWNING

--- Returns the group template from the global _DATABASE object (an instance of @{Core.Database#DATABASE}).
-- @param #GROUP self
-- @return #table Template table.
function GROUP:GetTemplate()
  local GroupName = self:GetName()
  local template=_DATABASE:GetGroupTemplate( GroupName )
  if template then
    return UTILS.DeepCopy( template )
  end
  return nil
end

--- Returns the group template route.points[] (the waypoints) from the global _DATABASE object (an instance of @{Core.Database#DATABASE}).
-- @param #GROUP self
-- @return #table
function GROUP:GetTemplateRoutePoints()
  local GroupName = self:GetName()
  local template = _DATABASE:GetGroupTemplate(GroupName)
  if template and template.route and template.route.points then
    return UTILS.DeepCopy(template.route.points)
  end
end



--- Sets the controlled status in a Template.
-- @param #GROUP self
-- @param #boolean Controlled true is controlled, false is uncontrolled.
-- @return #table
function GROUP:SetTemplateControlled( Template, Controlled )
  Template.uncontrolled = not Controlled
  return Template
end

--- Sets the CountryID of the group in a Template.
-- @param #GROUP self
-- @param DCS#country.id CountryID The country ID.
-- @return #table
function GROUP:SetTemplateCountry( Template, CountryID )
  Template.CountryID = CountryID
  return Template
end

--- Sets the CoalitionID of the group in a Template.
-- @param #GROUP self
-- @param DCS#coalition.side CoalitionID The coalition ID.
-- @return #table
function GROUP:SetTemplateCoalition( Template, CoalitionID )
  Template.CoalitionID = CoalitionID
  return Template
end


--- Set the heading for the units in degrees within the respawned group.
-- @param #GROUP self
-- @param #number Heading The heading in meters.
-- @return #GROUP self
function GROUP:InitHeading( Heading )
  self.InitRespawnHeading = Heading
  return self
end


--- Set the height for the units in meters for the respawned group. (This is applicable for air units).
-- @param #GROUP self
-- @param #number Height The height in meters.
-- @return #GROUP self
function GROUP:InitHeight( Height )
  self.InitRespawnHeight = Height
  return self
end


--- Set the respawn @{Core.Zone} for the respawned group.
-- @param #GROUP self
-- @param Core.Zone#ZONE Zone The zone in meters.
-- @return #GROUP self
function GROUP:InitZone( Zone )
  self.InitRespawnZone = Zone
  return self
end


--- Randomize the positions of the units of the respawned group within the @{Core.Zone}.
-- When a Respawn happens, the units of the group will be placed at random positions within the Zone (selected).
-- NOTE: InitRandomizePositionZone will not ensure, that every unit is placed within the zone!
-- @param #GROUP self
-- @param #boolean PositionZone true will randomize the positions within the Zone.
-- @return #GROUP self
function GROUP:InitRandomizePositionZone( PositionZone )

  self.InitRespawnRandomizePositionZone = PositionZone
  self.InitRespawnRandomizePositionInner = nil
  self.InitRespawnRandomizePositionOuter = nil

  return self
end


--- Randomize the positions of the units of the respawned group in a circle band.
-- When a Respawn happens, the units of the group will be positioned at random places within the Outer and Inner radius.
-- Thus, a band is created around the respawn location where the units will be placed at random positions.
-- @param #GROUP self
-- @param #boolean OuterRadius Outer band in meters from the center.
-- @param #boolean InnerRadius Inner band in meters from the center.
-- @return #GROUP self
function GROUP:InitRandomizePositionRadius( OuterRadius, InnerRadius )

  self.InitRespawnRandomizePositionZone = nil
  self.InitRespawnRandomizePositionOuter = OuterRadius
  self.InitRespawnRandomizePositionInner = InnerRadius

  return self
end

--- Set respawn coordinate.
-- @param #GROUP self
-- @param Core.Point#COORDINATE coordinate Coordinate where the group should be respawned.
-- @return #GROUP self
function GROUP:InitCoordinate(coordinate)
  --self:F({coordinate=coordinate})
  self.InitCoord=coordinate
  return self
end

--- Sets the radio comms on or off when the group is respawned. Same as checking/unchecking the COMM box in the mission editor.
-- @param #GROUP self
-- @param #boolean switch If true (or nil), enables the radio comms. If false, disables the radio for the spawned group.
-- @return #GROUP self
function GROUP:InitRadioCommsOnOff(switch)
  --self:F({switch=switch})
  if switch==true or switch==nil then
    self.InitRespawnRadio=true
  else
    self.InitRespawnRadio=false
  end
  return self
end

--- Sets the radio frequency of the group when it is respawned.
-- @param #GROUP self
-- @param #number frequency The frequency in MHz.
-- @return #GROUP self
function GROUP:InitRadioFrequency(frequency)
  --self:F({frequency=frequency})

  self.InitRespawnFreq=frequency

  return self
end

--- Set radio modulation when the group is respawned. Default is AM.
-- @param #GROUP self
-- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM.
-- @return #GROUP self
function GROUP:InitRadioModulation(modulation)
  --self:F({modulation=modulation})
  if modulation and modulation:lower()=="fm" then
    self.InitRespawnModu=radio.modulation.FM
  else
    self.InitRespawnModu=radio.modulation.AM
  end
  return self
end

--- Sets the modex (tail number) of the first unit of the group. If more units are in the group, the number is increased with every unit.
-- @param #GROUP self
-- @param #string modex Tail number of the first unit.
-- @return #GROUP self
function GROUP:InitModex(modex)
  --self:F({modex=modex})
  if modex then
    self.InitRespawnModex=tonumber(modex)
  end
  return self
end

--- Respawn the @{Wrapper.Group} at a @{Core.Point}.
-- The method will setup the new group template according the Init(Respawn) settings provided for the group.
-- These settings can be provided by calling the relevant Init...() methods of the Group.
--
--   - @{#GROUP.InitHeading}: Set the heading for the units in degrees within the respawned group.
--   - @{#GROUP.InitHeight}: Set the height for the units in meters for the respawned group. (This is applicable for air units).
--   - @{#GROUP.InitRandomizeHeading}: Randomize the headings for the units within the respawned group.
--   - @{#GROUP.InitZone}: Set the respawn @{Core.Zone} for the respawned group.
--   - @{#GROUP.InitRandomizePositionZone}: Randomize the positions of the units of the respawned group within the @{Core.Zone}.
--   - @{#GROUP.InitRandomizePositionRadius}: Randomize the positions of the units of the respawned group in a circle band.
--
-- Notes:
--
--   - The current alive group will always be destroyed and respawned using the template definition.
--
-- @param Wrapper.Group#GROUP self
-- @param #table Template (optional) The template of the Group retrieved with GROUP:GetTemplate(). If the template is not provided, the template will be retrieved of the group itself.
-- @param #boolean Reset Reset positions if TRUE.
-- @return Wrapper.Group#GROUP self
function GROUP:Respawn( Template, Reset )

  -- Given template or get old.
  Template = Template or self:GetTemplate()

  -- Get correct heading.
  local function _Heading(course)
    local h
    if course<=180 then
      h=math.rad(course)
    else
      h=-math.rad(360-course)
    end
    return h
  end
  
  local function TransFormRoute(Template,OldPos,NewPos)
    if Template.route and Template.route.points then
      for _,_point in ipairs(Template.route.points) do
        --self:I(string.format("Point x = %f Point y = %f",_point.x,_point.y))
        _point.x = _point.x - OldPos.x + NewPos.x
        _point.y = _point.y - OldPos.y + NewPos.y
        --self:I(string.format("Point x = %f Point y = %f",_point.x,_point.y))
      end
    end
    return Template
  end

  -- First check if group is alive.
  if self:IsAlive() then
    
    local OldPos = self:GetVec2()
    
    -- Respawn zone.
    local Zone = self.InitRespawnZone -- Core.Zone#ZONE

    -- Zone position or current group position.
    local Vec3 = Zone and Zone:GetVec3() or self:GetVec3()

    -- From point of the template.
    local From = { x = Template.x, y = Template.y }

    -- X, Y
    Template.x = Vec3.x
    Template.y = Vec3.z
    
    local NewPos = { x = Vec3.x, y = Vec3.z }

    --Template.x = nil
    --Template.y = nil

    -- Debug number of units.
    --self:F( #Template.units )

    -- Reset position etc?
    if Reset == true then

      -- Loop over units in group.
      for UnitID, UnitData in pairs( self:GetUnits() ) do
        local GroupUnit = UnitData -- Wrapper.Unit#UNIT
        --self:F(GroupUnit:GetName())

        if GroupUnit:IsAlive() then
          --self:I("FF Alive")

          -- Get unit position vector.
          local GroupUnitVec3 = GroupUnit:GetVec3()

          -- Check if respawn zone is set.
          if Zone then
            if self.InitRespawnRandomizePositionZone then
              GroupUnitVec3 = Zone:GetRandomVec3()
            else
              if self.InitRespawnRandomizePositionInner and self.InitRespawnRandomizePositionOuter then
                GroupUnitVec3 = COORDINATE:NewFromVec3(From):GetRandomVec3InRadius(self.InitRespawnRandomizePositionsOuter, self.InitRespawnRandomizePositionsInner)
              else
                GroupUnitVec3 = Zone:GetVec3()
              end
            end
          end

          -- Coordinate where the group should be respawned.
          if self.InitCoord then
            GroupUnitVec3=self.InitCoord:GetVec3()
          end

          -- Altitude
          Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y

          -- Unit position. Why not simply take the current positon?
          if Zone then
            Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position.
            Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position.
          else
            Template.units[UnitID].x=GroupUnitVec3.x
            Template.units[UnitID].y=GroupUnitVec3.z
          end

          -- Set heading.
          Template.units[UnitID].heading = _Heading(self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading())
          Template.units[UnitID].psi     = -Template.units[UnitID].heading
          
          -- Debug.
          --self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } )
        end
      end
      
      Template = TransFormRoute(Template,OldPos,NewPos)

    elseif Reset==false then  -- Reset=false or nil

      -- Loop over template units.
      for UnitID, TemplateUnitData in pairs( Template.units ) do

        --self:F( "Reset"  )

        -- Position from template.
        local GroupUnitVec3 = { x = TemplateUnitData.x, y = TemplateUnitData.alt, z = TemplateUnitData.y }

        -- Respawn zone position.
        if Zone then
          if self.InitRespawnRandomizePositionZone then
            GroupUnitVec3 = Zone:GetRandomVec3()
          else
            if self.InitRespawnRandomizePositionInner and self.InitRespawnRandomizePositionOuter then
              GroupUnitVec3 = COORDINATE:NewFromVec2( From ):GetRandomPointVec3InRadius( self.InitRespawnRandomizePositionsOuter, self.InitRespawnRandomizePositionsInner )
            else
              GroupUnitVec3 = Zone:GetVec3()
            end
          end
        end

        -- Coordinate where the group should be respawned.
        if self.InitCoord then
          GroupUnitVec3=self.InitCoord:GetVec3()
        end

        -- Set altitude.
        Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y

        -- Unit position.
        Template.units[UnitID].x = ( Template.units[UnitID].x - From.x ) + GroupUnitVec3.x -- Keep the original x position of the template and translate to the new position.
        Template.units[UnitID].y = ( Template.units[UnitID].y - From.y ) + GroupUnitVec3.z -- Keep the original z position of the template and translate to the new position.

        -- Heading
        Template.units[UnitID].heading = self.InitRespawnHeading and self.InitRespawnHeading or TemplateUnitData.heading
        
        -- Debug.
        --self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } )
      end
      
      Template = TransFormRoute(Template,OldPos,NewPos)
      
    else

      local units=self:GetUnits()

      -- Loop over template units.
      for UnitID, Unit in pairs(Template.units) do

        for _,_unit in pairs(units) do
          local unit=_unit --Wrapper.Unit#UNIT

          if unit:GetName()==Unit.name then
            local coord=unit:GetCoordinate()
            local heading=unit:GetHeading()
            Unit.x=coord.x
            Unit.y=coord.z
            Unit.alt=coord.y
            Unit.heading=math.rad(heading)
            Unit.psi=-Unit.heading
          end
        end

      end

    end

  end

  -- Set tail number.
  if self.InitRespawnModex then
    for UnitID=1,#Template.units do
      Template.units[UnitID].onboard_num=string.format("%03d", self.InitRespawnModex+(UnitID-1))
    end
  end

  -- Set radio frequency and modulation.
  if self.InitRespawnRadio then
    Template.communication=self.InitRespawnRadio
  end
  if self.InitRespawnFreq then
    Template.frequency=self.InitRespawnFreq
  end
  if self.InitRespawnModu then
    Template.modulation=self.InitRespawnModu
  end

  -- Destroy old group. Dont trigger any dead/crash events since this is a respawn.
  self:Destroy(false)

  --UTILS.PrintTableToLog(Template)

  if self.ValidateAndRepositionGroundUnits then
    UTILS.ValidateAndRepositionGroundUnits(Template.units)
  end

  -- Spawn new group.
  self:ScheduleOnce(0.1,_DATABASE.Spawn,_DATABASE,Template)
  --_DATABASE:Spawn(Template)

  -- Reset events.
  self:ResetEvents()

  return self
end

--- Respawn the @{Wrapper.Group} at a @{Core.Point#COORDINATE}.
-- The method will setup the new group template according the Init(Respawn) settings provided for the group.
-- These settings can be provided by calling the relevant Init...() methods of the Group prior.
--
--   - @{#GROUP.InitHeading}: Set the heading for the units in degrees within the respawned group.
--   - @{#GROUP.InitHeight}: Set the height for the units in meters for the respawned group. (This is applicable for air units).
--   - @{#GROUP.InitRandomizeHeading}: Randomize the headings for the units within the respawned group.
--   - @{#GROUP.InitRandomizePositionZone}: Randomize the positions of the units of the respawned group within the @{Core.Zone}.
--   - @{#GROUP.InitRandomizePositionRadius}: Randomize the positions of the units of the respawned group in a circle band.
--
-- Notes:
--
--   - When no coordinate is given, the position of the respawned group will be its current position.
--   - The current alive group will always be destroyed first.
--   - The new group will have all of its original units and health restored.
--
-- @param Wrapper.Group#GROUP self
-- @param Core.Point#COORDINATE Coordinate Where to respawn the group. Can be handed as a @{Core.Zone#ZONE_BASE} object.
-- @return Wrapper.Group#GROUP self
function GROUP:Teleport(Coordinate)
  self:InitZone(Coordinate)
  return self:Respawn(nil,false)
end

--- Respawn a group at an airbase.
-- Note that the group has to be on parking spots at the airbase already in order for this to work.
-- So each unit of the group is respawned at exactly the same parking spot as it currently occupies.
-- @param Wrapper.Group#GROUP self
-- @param #table SpawnTemplate (Optional) The spawn template for the group. If no template is given it is exacted from the group.
-- @param Core.Spawn#SPAWN.Takeoff Takeoff (Optional) Takeoff type. Sould be either SPAWN.Takeoff.Cold or SPAWN.Takeoff.Hot. Default is SPAWN.Takeoff.Hot.
-- @param #boolean Uncontrolled (Optional) If true, spawn in uncontrolled state.
-- @return Wrapper.Group#GROUP Group spawned at airbase or nil if group could not be spawned.
function GROUP:RespawnAtCurrentAirbase(SpawnTemplate, Takeoff, Uncontrolled) -- R2.4
  --self:F2( { SpawnTemplate, Takeoff, Uncontrolled} )

  if self and self:IsAlive() then

    -- Get closest airbase. Should be the one we are currently on.
    local airbase=self:GetCoordinate():GetClosestAirbase()

    if airbase then
      --self:F2("Closest airbase = "..airbase:GetName())
    else
      self:E("ERROR: could not find closest airbase!")
      return nil
    end
    -- Takeoff type. Default hot.
    Takeoff = Takeoff or SPAWN.Takeoff.Hot

    -- Coordinate of the airbase.
    local AirbaseCoord=airbase:GetCoordinate()

    -- Spawn template.
    SpawnTemplate = SpawnTemplate or self:GetTemplate()

    if SpawnTemplate then

      local SpawnPoint = SpawnTemplate.route.points[1]

      -- These are only for ships.
      SpawnPoint.linkUnit = nil
      SpawnPoint.helipadId = nil
      SpawnPoint.airdromeId = nil

      -- Aibase id and category.
      local AirbaseID       = airbase:GetID()
      local AirbaseCategory = airbase:GetAirbaseCategory()

      if AirbaseCategory == Airbase.Category.SHIP or AirbaseCategory == Airbase.Category.HELIPAD then
        SpawnPoint.linkUnit  = AirbaseID
        SpawnPoint.helipadId = AirbaseID
      elseif AirbaseCategory == Airbase.Category.AIRDROME then
        SpawnPoint.airdromeId = AirbaseID
      end


      SpawnPoint.type   = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type
      SpawnPoint.action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action

      -- Get the units of the group.
      local units=self:GetUnits()

      local x
      local y
      for UnitID=1,#units do

        local unit=units[UnitID] --Wrapper.Unit#UNIT

        -- Get closest parking spot of current unit. Note that we look for occupied spots since the unit is currently sitting on it!
        local Parkingspot, TermialID, Distance=unit:GetCoordinate():GetClosestParkingSpot(airbase)

        --Parkingspot:MarkToAll("parking spot")
        --self:T2(string.format("Closest parking spot distance = %s, terminal ID=%s", tostring(Distance), tostring(TermialID)))

        -- Get unit coordinates for respawning position.
        local uc=unit:GetCoordinate()
        --uc:MarkToAll(string.format("re-spawnplace %s terminal %d", unit:GetName(), TermialID))

        SpawnTemplate.units[UnitID].x   = uc.x --Parkingspot.x
        SpawnTemplate.units[UnitID].y   = uc.z --Parkingspot.z
        SpawnTemplate.units[UnitID].alt = uc.y --Parkingspot.y

        SpawnTemplate.units[UnitID].parking    = TermialID
        SpawnTemplate.units[UnitID].parking_id = nil

        --SpawnTemplate.units[UnitID].unitId=nil
      end

      --SpawnTemplate.groupId=nil

      SpawnPoint.x   = SpawnTemplate.units[1].x   --x --AirbaseCoord.x
      SpawnPoint.y   = SpawnTemplate.units[1].y   --y --AirbaseCoord.z
      SpawnPoint.alt = SpawnTemplate.units[1].alt --AirbaseCoord:GetLandHeight()

      SpawnTemplate.x = SpawnTemplate.units[1].x  --x --AirbaseCoord.x
      SpawnTemplate.y = SpawnTemplate.units[1].y  --y --AirbaseCoord.z

      -- Set uncontrolled state.
      SpawnTemplate.uncontrolled=Uncontrolled

      -- Set radio frequency and modulation.
      if self.InitRespawnRadio then
        SpawnTemplate.communication=self.InitRespawnRadio
      end
      if self.InitRespawnFreq then
        SpawnTemplate.frequency=self.InitRespawnFreq
      end
      if self.InitRespawnModu then
        SpawnTemplate.modulation=self.InitRespawnModu
      end

      -- Destroy old group.
      self:Destroy(false)

      -- Spawn new group.
      _DATABASE:Spawn(SpawnTemplate)

      -- Reset events.
      self:ResetEvents()

      return self
    end
  else
    self:E("WARNING: GROUP is not alive!")
  end

  return nil
end


--- Return the mission template of the group.
-- @param #GROUP self
-- @return #table The MissionTemplate
function GROUP:GetTaskMission()
  --self:F2( self.GroupName )

  return UTILS.DeepCopy( _DATABASE.Templates.Groups[self.GroupName].Template )
end

--- Return the mission route of the group.
-- @param #GROUP self
-- @return #table The mission route defined by points.
function GROUP:GetTaskRoute()
  --self:F2( self.GroupName )
  if _DATABASE.Templates.Groups[self.GroupName].Template and _DATABASE.Templates.Groups[self.GroupName].Template.route and _DATABASE.Templates.Groups[self.GroupName].Template.route.points then
    return UTILS.DeepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points )
  else
    return {}
  end
end

--- Return the route of a group by using the global _DATABASE object (an instance of @{Core.Database#DATABASE}).
-- @param #GROUP self
-- @param #number Begin The route point from where the copy will start. The base route point is 0.
-- @param #number End The route point where the copy will end. The End point is the last point - the End point. The last point has base 0.
-- @param #boolean Randomize Randomization of the route, when true.
-- @param #number Radius When randomization is on, the randomization is within the radius.
function GROUP:CopyRoute( Begin, End, Randomize, Radius )
  --self:F2( { Begin, End } )

  local Points = {}

  -- Could be a Spawned Group
  local GroupName = string.match( self:GetName(), ".*#" )
  if GroupName then
    GroupName = GroupName:sub( 1, -2 )
  else
    GroupName = self:GetName()
  end

  --self:T3( { GroupName } )

  local Template = _DATABASE.Templates.Groups[GroupName].Template

  if Template then
    if not Begin then
      Begin = 0
    end
    if not End then
      End = 0
    end

    for TPointID = Begin + 1, #Template.route.points - End do
      if Template.route.points[TPointID] then
        Points[#Points+1] = UTILS.DeepCopy( Template.route.points[TPointID] )
        if Randomize then
          if not Radius then
            Radius = 500
          end
          Points[#Points].x = Points[#Points].x + math.random( Radius * -1, Radius )
          Points[#Points].y = Points[#Points].y + math.random( Radius * -1, Radius )
        end
      end
    end
    return Points
  else
    error( "Template not found for Group : " .. GroupName )
  end

  return nil
end

--- Calculate the maxium A2G threat level of the Group.
-- @param #GROUP self
-- @return #number Number between 0 and 10.
function GROUP:CalculateThreatLevelA2G()

  local MaxThreatLevelA2G = 0
  for UnitName, UnitData in pairs( self:GetUnits() ) do
    local ThreatUnit = UnitData -- Wrapper.Unit#UNIT
    local ThreatLevelA2G = ThreatUnit:GetThreatLevel()
    if ThreatLevelA2G > MaxThreatLevelA2G then
      MaxThreatLevelA2G = ThreatLevelA2G
    end
  end

  --self:T3( MaxThreatLevelA2G )
  return MaxThreatLevelA2G
end

--- Get threat level of the group.
-- @param #GROUP self
-- @return #number Max threat level (a number between 0 and 10).
function GROUP:GetThreatLevel()

  local threatlevelMax = 0
  for UnitName, UnitData in pairs(self:GetUnits()) do
    local ThreatUnit = UnitData -- Wrapper.Unit#UNIT

    local threatlevel = ThreatUnit:GetThreatLevel()
    if threatlevel > threatlevelMax then
      threatlevelMax=threatlevel
    end
  end

  return threatlevelMax
end


--- Returns true if the first unit of the GROUP is in the air.
-- @param Wrapper.Group#GROUP self
-- @return #boolean true if in the first unit of the group is in the air or #nil if the GROUP is not existing or not alive.
function GROUP:InAir()
  --self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local DCSUnit = DCSGroup:getUnit(1)
    if DCSUnit then
      local GroupInAir = DCSGroup:getUnit(1):inAir()
      --self:T3( GroupInAir )
      return GroupInAir
    end
  end

  return nil
end

--- Checks whether any unit (or optionally) all units of a group is(are) airbore or not.
-- @param Wrapper.Group#GROUP self
-- @param #boolean AllUnits (Optional) If true, check whether all units of the group are airborne.
-- @return #boolean True if at least one (optionally all) unit(s) is(are) airborne or false otherwise. Nil if no unit exists or is alive.
function GROUP:IsAirborne(AllUnits)
  --self:F2( self.GroupName )

  -- Get all units of the group.
  local units=self:GetUnits()

  if units then

    if AllUnits then

      --- We want to know if ALL units are airborne.

      for _,_unit in pairs(units) do
        local unit=_unit --Wrapper.Unit#UNIT

        if unit then

          -- Unit in air or not.
          local inair=unit:InAir()

          -- At least one unit is not in air.
          if not inair then
            return false
          end
        end

      end

      -- All units are in air.
      return true

    else

      --- We want to know if ANY unit is airborne.

      for _,_unit in pairs(units) do
        local unit=_unit --Wrapper.Unit#UNIT

        if unit then

          -- Unit in air or not.
          local inair=unit:InAir()

          if inair then
            -- At least one unit is in air.
            return true
          end

        end

        -- No unit is in air.
        return false

      end
    end
  end

  return nil
end



--- Returns the DCS descriptor table of the nth unit of the group.
-- @param #GROUP self
-- @param #number n (Optional) The number of the unit for which the dscriptor is returned.
-- @return DCS#Object.Desc The descriptor of the first unit of the group or #nil if the group does not exist any more.
function GROUP:GetDCSDesc(n)
  -- Default.
  n=n or 1

  local unit=self:GetUnit(n)
  if unit and unit:IsAlive()~=nil then
    local desc=unit:GetDesc()
    return desc
  end

  return nil
end


--- Get the generalized attribute of a self.
-- Note that for a heterogenious self, the attribute is determined from the attribute of the first unit!
-- @param #GROUP self
-- @return #string Generalized attribute of the self.
function GROUP:GetAttribute()

  -- Default
  local attribute=GROUP.Attribute.OTHER_UNKNOWN --#GROUP.Attribute

  if self then

    -----------
    --- Air ---
    -----------
    -- Planes
    local transportplane=self:HasAttribute("Transports") and self:HasAttribute("Planes")
    local awacs=self:HasAttribute("AWACS")
    local fighter=self:HasAttribute("Fighters") or self:HasAttribute("Interceptors") or self:HasAttribute("Multirole fighters") or (self:HasAttribute("Bombers") and not self:HasAttribute("Strategic bombers"))
    local bomber=self:HasAttribute("Strategic bombers")
    local tanker=self:HasAttribute("Tankers")
    local uav=self:HasAttribute("UAVs")
    -- Helicopters
    local transporthelo=self:HasAttribute("Transport helicopters")
    local attackhelicopter=self:HasAttribute("Attack helicopters")

    --------------
    --- Ground ---
    --------------
    -- Ground
    local apc=self:HasAttribute("APC")
    local truck=self:HasAttribute("Trucks") and self:GetCategory()==Group.Category.GROUND
    local infantry=self:HasAttribute("Infantry")
    local artillery=self:HasAttribute("Artillery")
    local tank=self:HasAttribute("Old Tanks") or self:HasAttribute("Modern Tanks") or self:HasAttribute("Tanks")
    local aaa=self:HasAttribute("AAA") and (not self:HasAttribute("SAM elements"))
    local ballisticMissile=artillery and self:HasAttribute("SS_missile")
    local shorad=self:HasAttribute("SR SAM")
    local ewr=self:HasAttribute("EWR")
    local ifv=self:HasAttribute("IFV")
    local sam=self:HasAttribute("SAM elements") or self:HasAttribute("Optical Tracker")
    -- Train
    local train=self:GetCategory()==Group.Category.TRAIN

    -------------
    --- Naval ---
    -------------
    -- Ships
    local aircraftcarrier=self:HasAttribute("Aircraft Carriers")
    local warship=self:HasAttribute("Heavy armed ships")
    local armedship=self:HasAttribute("Armed ships")
    local unarmedship=self:HasAttribute("Unarmed ships")


    -- Define attribute. Order of attack is important.
    if fighter then
      attribute=GROUP.Attribute.AIR_FIGHTER
    elseif bomber then
      attribute=GROUP.Attribute.AIR_BOMBER
    elseif awacs then
      attribute=GROUP.Attribute.AIR_AWACS
    elseif transportplane then
      attribute=GROUP.Attribute.AIR_TRANSPORTPLANE
    elseif tanker then
      attribute=GROUP.Attribute.AIR_TANKER
      -- helos
    elseif attackhelicopter then
      attribute=GROUP.Attribute.AIR_ATTACKHELO
    elseif transporthelo then
      attribute=GROUP.Attribute.AIR_TRANSPORTHELO
    elseif uav then
      attribute=GROUP.Attribute.AIR_UAV
      -- ground - order of attack
    elseif ewr then
      attribute=GROUP.Attribute.GROUND_EWR
    elseif sam then
      attribute=GROUP.Attribute.GROUND_SAM
    elseif aaa then
      attribute=GROUP.Attribute.GROUND_AAA
    elseif artillery and ballisticMissile then
      attribute=GROUP.Attribute.GROUND_BALLISTICMISSILE
    elseif artillery then
      attribute=GROUP.Attribute.GROUND_ARTILLERY
    elseif tank then
      attribute=GROUP.Attribute.GROUND_TANK
    elseif ifv then
      attribute=GROUP.Attribute.GROUND_IFV
    elseif apc then
      attribute=GROUP.Attribute.GROUND_APC
    elseif infantry then
      attribute=GROUP.Attribute.GROUND_INFANTRY
    elseif truck then
      attribute=GROUP.Attribute.GROUND_TRUCK
    elseif train then
      attribute=GROUP.Attribute.GROUND_TRAIN
      -- ships
    elseif aircraftcarrier then
      attribute=GROUP.Attribute.NAVAL_AIRCRAFTCARRIER
    elseif warship then
      attribute=GROUP.Attribute.NAVAL_WARSHIP
    elseif armedship then
      attribute=GROUP.Attribute.NAVAL_ARMEDSHIP
    elseif unarmedship then
      attribute=GROUP.Attribute.NAVAL_UNARMEDSHIP
    else
      if self:IsGround() then
        attribute=GROUP.Attribute.GROUND_OTHER
      elseif self:IsShip() then
        attribute=GROUP.Attribute.NAVAL_OTHER
      elseif self:IsAir() then
        attribute=GROUP.Attribute.AIR_OTHER
      else
        attribute=GROUP.Attribute.OTHER_UNKNOWN
      end
    end
  end

  return attribute
end


do -- Route methods

  --- (AIR) Return the Group to an @{Wrapper.Airbase#AIRBASE}.
  -- The following things are to be taken into account:
  --
  --   * The group is respawned to achieve the RTB, there may be side artefacts as a result of this. (Like weapons suddenly come back).
  --   * A group consisting out of more than one unit, may rejoin formation when respawned.
  --   * A speed can be given in km/h. If no speed is specified, the maximum speed of the first unit will be taken to return to base.
  --   * When there is no @{Wrapper.Airbase} object specified, the group will return to the home base if the route of the group is pinned at take-off or at landing to a base.
  --   * When there is no @{Wrapper.Airbase} object specified and the group route is not pinned to any airbase, it will return to the nearest airbase.
  --
  -- @param #GROUP self
  -- @param Wrapper.Airbase#AIRBASE RTBAirbase (optional) The @{Wrapper.Airbase} to return to. If blank, the controllable will return to the nearest friendly airbase.
  -- @param #number Speed (optional) The Speed, if no Speed is given, 80% of maximum Speed of the group is selected.
  -- @return #GROUP self
  function GROUP:RouteRTB( RTBAirbase, Speed )
    --self:F( { RTBAirbase:GetName(), Speed } )

    local DCSGroup = self:GetDCSObject()

    if DCSGroup then

      if RTBAirbase then

        -- If speed is not given take 80% of max speed.
        local Speed=Speed or self:GetSpeedMax()*0.8

        -- Curent (from) waypoint.
        local coord=self:GetCoordinate()
        local PointFrom=coord:WaypointAirTurningPoint(nil, Speed)

        -- Airbase coordinate.
        --local PointAirbase=RTBAirbase:GetCoordinate():SetAltitude(coord.y):WaypointAirTurningPoint(nil ,Speed)

        -- Landing waypoint. More general than prev version since it should also work with FAPRS and ships.
        local PointLanding=RTBAirbase:GetCoordinate():WaypointAirLanding(Speed, RTBAirbase)

        -- Waypoint table.
        local Points={PointFrom, PointLanding}
        --local Points={PointFrom, PointAirbase, PointLanding}

        -- Debug info.
        --self:T3(Points)

        -- Get group template.
        local Template=self:GetTemplate()

        -- Set route points.
        Template.route.points=Points

        -- Respawn the group.
        self:Respawn(Template, true)

        -- Route the group or this will not work.
        self:Route(Points)
      else

        -- Clear all tasks.
        self:ClearTasks()

      end
    end

    return self
  end

end

function GROUP:OnReSpawn( ReSpawnFunction )

  self.ReSpawnFunction = ReSpawnFunction
end

do -- Event Handling

  --- Subscribe to a DCS Event.
  -- @param #GROUP self
  -- @param Core.Event#EVENTS Event
  -- @param #function EventFunction (optional) The function to be called when the event occurs for the GROUP.
  -- @return #GROUP
  function GROUP:HandleEvent( Event, EventFunction, ... )

    self:EventDispatcher():OnEventForGroup( self:GetName(), EventFunction, self, Event, ... )

    return self
  end

  --- UnSubscribe to a DCS event.
  -- @param #GROUP self
  -- @param Core.Event#EVENTS Event
  -- @return #GROUP
  function GROUP:UnHandleEvent( Event )

    self:EventDispatcher():RemoveEvent( self, Event )

    return self
  end

  --- Reset the subscriptions.
  -- @param #GROUP self
  -- @return #GROUP
  function GROUP:ResetEvents()

    self:EventDispatcher():Reset( self )

    for UnitID, UnitData in pairs( self:GetUnits() or {}) do
      UnitData:ResetEvents()
    end

    return self
  end

end

do -- Players

  --- Get player names
  -- @param #GROUP self
  -- @return #table The group has players, an array of player names is returned.
  -- @return #nil The group has no players
  function GROUP:GetPlayerNames()

    local HasPlayers = false

    local PlayerNames = {}

    local Units = self:GetUnits()
    for UnitID, UnitData in pairs( Units or {}) do
      local Unit = UnitData -- Wrapper.Unit#UNIT
      local PlayerName = Unit:GetPlayerName()
      if PlayerName and PlayerName ~= "" then
        PlayerNames = PlayerNames or {}
        table.insert( PlayerNames, PlayerName )
        HasPlayers = true
      end
    end

    if HasPlayers == true then
      --self:F2( PlayerNames )
      return PlayerNames
    end

    return nil
  end


  --- Get the active player count in the group.
  -- @param #GROUP self
  -- @return #number The amount of players.
  function GROUP:GetPlayerCount()

    local PlayerCount = 0

    local Units = self:GetUnits()
    for UnitID, UnitData in pairs( Units or {} ) do
      local Unit = UnitData -- Wrapper.Unit#UNIT
      local PlayerName = Unit:GetPlayerName()
      if PlayerName and PlayerName ~= "" then
        PlayerCount = PlayerCount + 1
      end
    end

    return PlayerCount
  end

end

--- GROUND - Switch on/off radar emissions for the group.
-- @param #GROUP self
-- @param #boolean switch If true, emission is enabled. If false, emission is disabled.
-- @return #GROUP self
function GROUP:EnableEmission(switch)
  --self:F2( self.GroupName )
  local switch = switch or false

  local DCSUnit = self:GetDCSObject()

  if DCSUnit then

    DCSUnit:enableEmission(switch)

  end

  return self
end

--- Switch on/off invisible flag for the group.
-- @param #GROUP self
-- @param #boolean switch If true, Invisible is enabled. If false, Invisible is disabled.
-- @return #GROUP self
function GROUP:SetCommandInvisible(switch)
  return self:CommandSetInvisible(switch)
end

--- Switch on/off invisible flag for the group.
-- @param #GROUP self
-- @param #boolean switch If true, Invisible is enabled. If false, Invisible is disabled.
-- @return #GROUP self
function GROUP:CommandSetInvisible(switch)
  --self:F2( self.GroupName )
  if switch==nil then
    switch=false
  end
  local SetInvisible = {id = 'SetInvisible', params = {value = switch}}
  self:SetCommand(SetInvisible)
  return self
end

--- Switch on/off immortal flag for the group.
-- @param #GROUP self
-- @param #boolean switch If true, Immortal is enabled. If false, Immortal is disabled.
-- @return #GROUP self
function GROUP:SetCommandImmortal(switch)
  return self:CommandSetImmortal(switch)
end

--- Switch on/off immortal flag for the group.
-- @param #GROUP self
-- @param #boolean switch If true, Immortal is enabled. If false, Immortal is disabled.
-- @return #GROUP self
function GROUP:CommandSetImmortal(switch)
  --self:F2( self.GroupName )
  if switch==nil then
    switch=false
  end
  local SetImmortal = {id = 'SetImmortal', params = {value = switch}}
  self:SetCommand(SetImmortal)
  return self
end

--- Get skill from Group. Effectively gets the skill from Unit 1 as the group holds no skill value.
-- @param #GROUP self
-- @return #string Skill String of skill name.
function GROUP:GetSkill()
  --self:F2( self.GroupName )
  local unit = self:GetUnit(1)
  local name = unit:GetName()
  local skill = _DATABASE.Templates.Units[name].Template.skill or "Random"
  return skill
end


--- Get the unit in the group with the highest threat level, which is still alive.
-- @param #GROUP self
-- @return Wrapper.Unit#UNIT The most dangerous unit in the group.
-- @return #number Threat level of the unit.
function GROUP:GetHighestThreat()

  -- Get units of the group.
  local units=self:GetUnits()

  if units then

    local threat=nil ; local maxtl=0
    for _,_unit in pairs(units or {}) do
      local unit=_unit --Wrapper.Unit#UNIT

      if unit and unit:IsAlive() then

        -- Threat level of group.
        local tl=unit:GetThreatLevel()

        -- Check if greater the current threat.
        if tl>maxtl then
          maxtl=tl
          threat=unit
        end
      end
    end

    return threat, maxtl
  end

  return nil, nil
end

--- Get TTS friendly, optionally customized callsign mainly for **player groups**. A customized callsign is taken from the #GROUP name, after an optional '#' sign, e.g. "Aerial 1-1#Ghostrider" resulting in "Ghostrider 9", or,
-- if that isn't available, from the playername, as set in the mission editor main screen under Logbook, after an optional '|' sign (actually, more of a personal call sign), e.g. "Apple|Moose" results in "Moose 9 1". Options see below.
-- @param #GROUP self
-- @param #boolean ShortCallsign Return a shortened customized callsign, i.e. "Ghostrider 9" and not "Ghostrider 9 1"
-- @param #boolean Keepnumber (Player only) Return customized callsign, incl optional numbers at the end, e.g. "Aerial 1-1#Ghostrider 109" results in "Ghostrider 109", if you want to e.g. use historical US Navy Callsigns
-- @param #table CallsignTranslations (Optional) Table to translate between DCS standard callsigns and bespoke ones. Overrides personal/parsed callsigns if set
-- callsigns from playername or group name.
-- @param #func CustomFunction (Optional) For player names only(!). If given, this function will return the callsign. Needs to take the groupname and the playername as first arguments.
-- @param #arg ... (Optional) Comma separated arguments to add to the CustomFunction call after groupname and playername.
-- @return #string Callsign
-- @usage
--            -- suppose there are three groups with one (client) unit each:
--            -- Slot 1               -- with mission editor callsign Enfield-1
--            -- Slot 2 # Apollo 403  -- with mission editor callsign Enfield-2
--            -- Slot 3 | Apollo      -- with mission editor callsign Enfield-3
--            -- Slot 4 | Apollo      -- with mission editor callsign Devil-4
--            -- and suppose these Custom CAP Flight Callsigns for use with TTS are set
--            mygroup:GetCustomCallSign(true,false,{
--              Devil = 'Bengal',
--              Snake = 'Winder',
--              Colt = 'Camelot',
--              Enfield = 'Victory',
--              Uzi = 'Evil Eye'
--            })
--            -- then GetCustomCallsign will return
--            -- Enfield-1 for Slot 1
--            -- Apollo for Slot 2 or Apollo 403 if Keepnumber is set
--            -- Apollo for Slot 3
--            -- Bengal-4 for Slot 4
-- 
--            -- Using a custom function (for player units **only**):
--            -- Imagine your playernames are looking like so: "[Squadname] | Cpt Apple" and you only want to have the last word as callsign, i.e. "Apple" here. Then this custom function will return this:
--            local callsign = mygroup:GetCustomCallSign(true,false,nil,function(groupname,playername) return string.match(playername,"([%a]+)$") end)
-- 
function GROUP:GetCustomCallSign(ShortCallsign,Keepnumber,CallsignTranslations,CustomFunction,...)
  self:T("GetCustomCallSign")

  local callsign = "Ghost 1"
  if self:IsAlive() then
    local IsPlayer = self:IsPlayer()
    local shortcallsign = self:GetCallsign() or "unknown91" -- e.g.Uzi91, but we want Uzi 9 1
    local callsignroot = string.match(shortcallsign, '(%a+)') or "Ghost" -- Uzi
    --self:I("CallSign = " .. callsignroot)
    local groupname = self:GetName()
    local callnumber = string.match(shortcallsign, "(%d+)$" ) or "91" -- 91
    local callnumbermajor = string.char(string.byte(callnumber,1)) -- 9
    local callnumberminor = string.char(string.byte(callnumber,2)) -- 1
    local personalized = false
    --local playername = IsPlayer == true and self:GetPlayerName() or shortcallsign
    local playername = shortcallsign
    
    if IsPlayer then playername = self:GetPlayerName() end
    
    self:T2("GetCustomCallSign outcome = "..playername)
    if CustomFunction and IsPlayer then
      local arguments = arg or {}
      local callsign = CustomFunction(groupname,playername,unpack(arguments))
      return callsign
    end
    
    -- prioritize bespoke callsigns over parsing, prefer parsing over default callsigns
    if CallsignTranslations and CallsignTranslations[callsignroot] then
      callsignroot = CallsignTranslations[callsignroot]
    elseif IsPlayer and string.find(groupname,"#") then
      -- personalized flight name in group naming
      if Keepnumber then
        shortcallsign = string.match(groupname,"#(.+)") or "Ghost 111" -- Ghostrider 219
      else
        shortcallsign = string.match(groupname,"#%s*([%a]+)") or "Ghost" -- Ghostrider
      end
      personalized = true
    elseif IsPlayer and string.find(playername,"|") then
      -- personalized flight name in group naming
      shortcallsign = string.match(playername,"|%s*([%a]+)") or string.match(self:GetPlayerName(),"|%s*([%d]+)") or "Ghost" -- Ghostrider
      personalized = true
    end

    if personalized then
      -- player personalized callsign
      -- remove trailing/leading spaces
      shortcallsign=string.gsub(shortcallsign,"^%s*","")
      shortcallsign=string.gsub(shortcallsign,"%s*$","")
      if Keepnumber then
        return shortcallsign -- Ghostrider 219
      elseif ShortCallsign then
        callsign = shortcallsign.." "..callnumbermajor -- Ghostrider 9
      else
        callsign = shortcallsign.." "..callnumbermajor.." "..callnumberminor -- Ghostrider 9 1
      end
      return callsign
    end

    -- AI or not personalized
    if ShortCallsign then
      callsign = callsignroot.." "..callnumbermajor -- Uzi/Victory 9
    else
      callsign = callsignroot.." "..callnumbermajor.." "..callnumberminor -- Uzi/Victory 9 1
    end

    --self:I("Generated Callsign = " .. callsign)
  end

  return callsign
end

--- Set a GROUP to act as recovery tanker
-- @param #GROUP self
-- @param Wrapper.Group#GROUP CarrierGroup.
-- @param #number Speed Speed in knots.
-- @param #boolean ToKIAS If true, adjust speed to altitude (KIAS).
-- @param #number Altitude Altitude the tanker orbits at in feet.
-- @param #number Delay (optional) Set the task after this many seconds. Defaults to one.
-- @param #number LastWaypoint (optional) Waypoint number of carrier group that when reached, ends the recovery tanker task.
-- @return #GROUP self
function GROUP:SetAsRecoveryTanker(CarrierGroup,Speed,ToKIAS,Altitude,Delay,LastWaypoint)

  local speed = ToKIAS == true and UTILS.KnotsToAltKIAS(Speed,Altitude) or Speed
  speed = UTILS.KnotsToMps(speed)

  local alt = UTILS.FeetToMeters(Altitude)
  local delay = Delay or 1

  local task = self:TaskRecoveryTanker(CarrierGroup,speed,alt,LastWaypoint)

  self:SetTask(task,delay)

  local tankertask = self:EnRouteTaskTanker()
  self:PushTask(tankertask,delay+2)

  return self
end

--- Get a list of Link16 S/TN data from a GROUP. Can (as of Nov 2023) be obtained from F-18, F-16, F-15E (not the user flyable one) and A-10C-II groups.
-- @param #GROUP self
-- @return #table Table of data entries, indexed by unit name, each entry is a table containing STN, VCL (voice call label), VCN (voice call number), and Lead (#boolean, if true it's the flight lead)
-- @return #string Report Formatted report of all data
function GROUP:GetGroupSTN()
  local tSTN = {} -- table
  local units = self:GetUnits()
  local gname = self:GetName()
  gname = string.gsub(gname,"(#%d+)$","")
  local report = REPORT:New()
  report:Add("Link16 S/TN Report")
  report:Add("Group: "..gname)
  report:Add("==================")
  for _,_unit in pairs(units) do
   local unit = _unit -- Wrapper.Unit#UNIT
   if unit and unit:IsAlive() then
     local STN, VCL, VCN, Lead = unit:GetSTN()
     local name = unit:GetName()
     tSTN[name] = {
      STN=STN,
      VCL=VCL,
      VCN=VCN,
      Lead=Lead,
     }
     local lead = Lead == true and "(*)" or ""
     report:Add(string.format("| %s%s %s %s",tostring(VCL),tostring(VCN),tostring(STN),lead))
   end
  end
  report:Add("==================")
  local text = report:Text()
  return tSTN,text
end

--- [GROUND] Determine if a GROUP is a SAM unit, i.e. has radar or optical tracker and is no mobile AAA.
-- @param #GROUP self
-- @return #boolean IsSAM True if SAM, else false
function GROUP:IsSAM()
  local issam = false
  local units = self:GetUnits()
  for _,_unit in pairs(units or {}) do
    local unit = _unit -- Wrapper.Unit#UNIT
    if unit:IsSAM() then
      issam = true
      break
    end
  end
  return issam
end

--- [GROUND] Determine if a GROUP has a AAA unit, i.e. has no radar or optical tracker but the AAA = true or the "Mobile AAA" = true attribute.
-- @param #GROUP self
-- @return #boolean IsAAA True if AAA, else false
function GROUP:IsAAA()
  local isAAA = false
  local units = self:GetUnits()
  for _,_unit in pairs(units or {}) do
    local unit = _unit -- Wrapper.Unit#UNIT
    if unit:IsAAA() then
      isAAA = true
      break
    end
  end
  return isAAA
end

--- This function uses Disposition and other fallback logic to find better ground positions for ground units.
--- NOTE: This is not a spawn randomizer.
--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table.
--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit.
--- Uses UTILS.ValidateAndRepositionGroundUnits.
-- @param #GROUP self
-- @param #boolean Enabled Enable/disable the feature.
function GROUP:SetValidateAndRepositionGroundUnits(Enabled)
    self.ValidateAndRepositionGroundUnits = Enabled
end


--- Get the bounding box of the group combining UNIT:GetBoundingBox() units.
-- @param #GROUP self
-- @return DCS#Box3 The bounding box of the GROUP.
-- @return #nil The GROUP does not have any alive units.
function GROUP:GetBoundingBox()
    local bbox = { min = { x = math.huge, y = math.huge, z = math.huge },
                   max = { x = -math.huge, y = -math.huge, z = -math.huge }
    }

    local Units = self:GetUnits() or {}
    if #Units == 0 then
        return nil
    end

    for _, unit in pairs(Units) do
        if unit and unit:IsAlive() then
            local ubox = unit:GetBoundingBox()

            if ubox then
                if ubox.min.x < bbox.min.x then
                    bbox.min.x = ubox.min.x
                end
                if ubox.min.y < bbox.min.y then
                    bbox.min.y = ubox.min.y
                end
                if ubox.min.z < bbox.min.z then
                    bbox.min.z = ubox.min.z
                end

                if ubox.max.x > bbox.max.x then
                    bbox.max.x = ubox.max.x
                end
                if ubox.max.y > bbox.max.y then
                    bbox.max.y = ubox.max.y
                end
                if ubox.max.z > bbox.max.z then
                    bbox.max.z = ubox.max.z
                end
            end
        end
    end

    return bbox
end
--- **Wrapper** - UNIT is a wrapper class for the DCS Class Unit.
-- 
-- ===
-- 
-- The @{#UNIT} class is a wrapper class to handle the DCS Unit objects:
-- 
--  * Support all DCS Unit APIs.
--  * Enhance with Unit specific APIs not in the DCS Unit API set.
--  * Handle local Unit Controller.
--  * Manage the "state" of the DCS Unit.
--  
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: **funkyfranky**, **Applevangelist**
-- 
-- ===
-- 
-- @module Wrapper.Unit
-- @image Wrapper_Unit.JPG

---
-- @type UNIT
-- @field #string ClassName Name of the class.
-- @field #string UnitName Name of the unit.
-- @field #string GroupName Name of the group the unit belongs to.
-- @field #table DCSUnit The DCS Unit object from the API.
-- @extends Wrapper.Controllable#CONTROLLABLE

--- For each DCS Unit object alive within a running mission, a UNIT wrapper object (instance) will be created within the global _DATABASE object (an instance of @{Core.Database#DATABASE}).
-- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Unit objects are spawned (using the @{Core.Spawn} class).
--  
-- The UNIT class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference
-- using the DCS Unit or the DCS UnitName.
-- 
-- Another thing to know is that UNIT objects do not "contain" the DCS Unit object. 
-- The UNIT methods will reference the DCS Unit object by name when it is needed during API execution.
-- If the DCS Unit object does not exist or is nil, the UNIT methods will return nil and log an exception in the DCS.log file.
--  
-- The UNIT class provides the following functions to retrieve quickly the relevant UNIT instance:
-- 
--  * @{#UNIT.Find}(): Find a UNIT instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Unit object.
--  * @{#UNIT.FindByName}(): Find a UNIT instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Unit name.
--  * @{#UNIT.FindByMatching}(): Find a UNIT instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a pattern.
--  * @{#UNIT.FindAllByMatching}(): Find all UNIT instances from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a pattern.
--  
-- IMPORTANT: ONE SHOULD NEVER SANITIZE these UNIT OBJECT REFERENCES! (make the UNIT object references nil).
-- 
-- ## DCS UNIT APIs
-- 
-- The DCS Unit APIs are used extensively within MOOSE. The UNIT class has for each DCS Unit API a corresponding method.
-- To be able to distinguish easily in your code the difference between a UNIT API call and a DCS Unit API call,
-- the first letter of the method is also capitalized. So, by example, the DCS Unit method @{DCS#Unit.getName}()
-- is implemented in the UNIT class as @{#UNIT.GetName}().
-- 
-- ## Smoke, Flare Units
-- 
-- The UNIT class provides methods to smoke or flare units easily. 
-- The @{#UNIT.SmokeBlue}(), @{#UNIT.SmokeGreen}(),@{#UNIT.SmokeOrange}(), @{#UNIT.SmokeRed}(), @{#UNIT.SmokeRed}() methods
-- will smoke the unit in the corresponding color. Note that smoking a unit is done at the current position of the DCS Unit. 
-- When the DCS Unit moves for whatever reason, the smoking will still continue!
-- The @{#UNIT.FlareGreen}(), @{#UNIT.FlareRed}(), @{#UNIT.FlareWhite}(), @{#UNIT.FlareYellow}() 
-- methods will fire off a flare in the air with the corresponding color. Note that a flare is a one-off shot and its effect is of very short duration.
-- 
-- ## Location Position, Point
-- 
-- The UNIT class provides methods to obtain the current point or position of the DCS Unit.
-- The @{#UNIT.GetPointVec2}(), @{#UNIT.GetVec3}() will obtain the current **location** of the DCS Unit in a Vec2 (2D) or a **point** in a Vec3 (3D) vector respectively.
-- If you want to obtain the complete **3D position** including orientation and direction vectors, consult the @{#UNIT.GetPositionVec3}() method respectively.
-- 
-- ## Test if alive
-- 
-- The @{#UNIT.IsAlive}(), @{#UNIT.IsActive}() methods determines if the DCS Unit is alive, meaning, it is existing and active.
-- 
-- ## Test for proximity
-- 
-- The UNIT class contains methods to test the location or proximity against zones or other objects.
-- 
-- ### Zones range
-- 
-- To test whether the Unit is within a **zone**, use the @{#UNIT.IsInZone}() or the @{#UNIT.IsNotInZone}() methods. Any zone can be tested on, but the zone must be derived from @{Core.Zone#ZONE_BASE}. 
-- 
-- ### Unit range
-- 
--   * Test if another DCS Unit is within a given radius of the current DCS Unit, use the @{#UNIT.OtherUnitInRadius}() method.
--   
-- ## Test Line of Sight
-- 
--   * Use the @{#UNIT.IsLOS}() method to check if the given unit is within line of sight.
-- 
-- 
-- @field #UNIT
UNIT = {
    ClassName = "UNIT",
    UnitName = nil,
    GroupName = nil,
    DCSUnit = nil,
}


--- Unit.SensorType
-- @type Unit.SensorType
-- @field OPTIC
-- @field RADAR
-- @field IRST
-- @field RWR


-- Registration.

--- Create a new UNIT from DCSUnit.
-- @param #UNIT self
-- @param #string UnitName The name of the DCS unit.
-- @return #UNIT self
function UNIT:Register(UnitName)

    -- Inherit CONTROLLABLE.
    local self = BASE:Inherit(self, CONTROLLABLE:New(UnitName)) --#UNIT

    -- Set unit name.
    self.UnitName = UnitName

    local unit = Unit.getByName(self.UnitName)

    if unit then
        local group = unit:getGroup()
        if group then
            self.GroupName = group:getName()
            self.groupId = group:getID()
        end
        self.DCSUnit = unit
    end

    -- Set event prio.
    self:SetEventPriority(3)

    return self
end

-- Reference methods.

--- Finds a UNIT from the _DATABASE using a DCSUnit object.
-- @param #UNIT self
-- @param DCS#Unit DCSUnit An existing DCS Unit object reference.
-- @return #UNIT self
function UNIT:Find(DCSUnit)
    if DCSUnit then
        local UnitName = DCSUnit:getName()
        local UnitFound = _DATABASE:FindUnit(UnitName)
        return UnitFound
    end
    return nil
end

--- Find a UNIT in the _DATABASE using the name of an existing DCS Unit.
-- @param #UNIT self
-- @param #string UnitName The Unit Name.
-- @return #UNIT self
function UNIT:FindByName(UnitName)

    local UnitFound = _DATABASE:FindUnit(UnitName)
    return UnitFound
end

--- Find the first(!) UNIT matching using patterns. Note that this is **a lot** slower than `:FindByName()`!
-- @param #UNIT self
-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA.
-- @return #UNIT The UNIT.
-- @usage
--          -- Find a group with a partial group name
--          local unit = UNIT:FindByMatching( "Apple" )
--          -- will return e.g. a group named "Apple-1-1"
--          
--          -- using a pattern
--          local unit = UNIT:FindByMatching( ".%d.%d$" )
--          -- will return the first group found ending in "-1-1" to "-9-9", but not e.g. "-10-1"
function UNIT:FindByMatching(Pattern)
    local GroupFound = nil

    for name, group in pairs(_DATABASE.UNITS) do
        if string.match(name, Pattern) then
            GroupFound = group
            break
        end
    end

    return GroupFound
end

--- Find all UNIT objects matching using patterns. Note that this is **a lot** slower than `:FindByName()`!
-- @param #UNIT self
-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA.
-- @return #table Units Table of matching #UNIT objects found
-- @usage
--          -- Find all group with a partial group name
--          local unittable = UNIT:FindAllByMatching( "Apple" )
--          -- will return all units with "Apple" in the name
--          
--          -- using a pattern
--          local unittable = UNIT:FindAllByMatching( ".%d.%d$" )
--          -- will return the all units found ending in "-1-1" to "-9-9", but not e.g. "-10-1" or "-1-10"
function UNIT:FindAllByMatching(Pattern)
    local GroupsFound = {}

    for name, group in pairs(_DATABASE.UNITS) do
        if string.match(name, Pattern) then
            GroupsFound[#GroupsFound + 1] = group
        end
    end

    return GroupsFound
end

--- Return the name of the UNIT.
-- @param #UNIT self
-- @return #string The UNIT name.
function UNIT:Name()

    return self.UnitName
end

--[[
--- Get the DCS unit object.
-- @param #UNIT self
-- @return DCS#Unit The DCS unit object.
function UNIT:GetDCSObject()

  local DCSUnit = Unit.getByName( self.UnitName )

  if DCSUnit then
    return DCSUnit
  end
  
  return nil
end
--]]

--- Returns the DCS Unit.
-- @param #UNIT self
-- @return DCS#Unit The DCS Group.
function UNIT:GetDCSObject()

    -- FF: Added checks that DCSObject exists because otherwise there were problems when respawning the unit right after it was initially spawned (e.g. teleport in OPSGROUP).
    --     Got "Unit does not exit" after coalition.addGroup() when trying to access unit data because LastCallDCSObject<=1.
    if (not self.LastCallDCSObject) or (self.LastCallDCSObject and timer.getTime() - self.LastCallDCSObject > 1) or (self.DCSObject == nil) or (self.DCSObject:isExist() == false) then

        -- Get DCS group.
        local DCSUnit = Unit.getByName(self.UnitName)

        if DCSUnit then
            self.LastCallDCSObject = timer.getTime()
            self.DCSObject = DCSUnit
            return DCSUnit
        else
            self.DCSObject = nil
            self.LastCallDCSObject = nil
        end

    else
        return self.DCSObject
    end

    --self:E(string.format("ERROR: Could not get DCS group object of group %s because DCS object could not be found!", tostring(self.UnitName)))
    return nil
end

--- Returns the unit altitude above sea level in meters.
-- @param Wrapper.Unit#UNIT self
-- @param #boolean FromGround Measure from the ground or from sea level (ASL). Provide **true** for measuring from the ground (AGL). **false** or **nil** if you measure from sea level. 
-- @return #number The height of the group or nil if is not existing or alive.  
function UNIT:GetAltitude(FromGround)

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local altitude = 0
        local point = DCSUnit:getPoint() --DCS#Vec3
        altitude = point.y
        if FromGround then
            local land = land.getHeight({ x = point.x, y = point.z }) or 0
            altitude = altitude - land
        end
        return altitude
    end

    return nil

end

--- Respawn the @{Wrapper.Unit} using a (tweaked) template of the parent Group.
-- 
-- This function will:
-- 
--  * Get the current position and heading of the group.
--  * When the unit is alive, it will tweak the template x, y and heading coordinates of the group and the embedded units to the current units positions.
--  * Then it will respawn the re-modelled group.
--  
-- @param #UNIT self
-- @param Core.Point#COORDINATE Coordinate The position where to Spawn the new Unit at.
-- @param #number Heading The heading of the unit respawn.
function UNIT:ReSpawnAt(Coordinate, Heading)

    --self:T( self:Name() )
    local SpawnGroupTemplate = UTILS.DeepCopy(_DATABASE:GetGroupTemplateFromUnitName(self:Name()))
    --self:T( SpawnGroupTemplate )

    local SpawnGroup = self:GetGroup()
    --self:T( { SpawnGroup = SpawnGroup } )

    if SpawnGroup then

        local Vec3 = SpawnGroup:GetVec3()
        SpawnGroupTemplate.x = Coordinate.x
        SpawnGroupTemplate.y = Coordinate.z

        --self:F( #SpawnGroupTemplate.units )
        for UnitID, UnitData in pairs(SpawnGroup:GetUnits() or {}) do
            local GroupUnit = UnitData -- #UNIT
            --self:F( GroupUnit:GetName() )
            if GroupUnit:IsAlive() then
                local GroupUnitVec3 = GroupUnit:GetVec3()
                local GroupUnitHeading = GroupUnit:GetHeading()
                SpawnGroupTemplate.units[UnitID].alt = GroupUnitVec3.y
                SpawnGroupTemplate.units[UnitID].x = GroupUnitVec3.x
                SpawnGroupTemplate.units[UnitID].y = GroupUnitVec3.z
                SpawnGroupTemplate.units[UnitID].heading = GroupUnitHeading
                --self:F( { UnitID, SpawnGroupTemplate.units[UnitID], SpawnGroupTemplate.units[UnitID] } )
            end
        end
    end

    for UnitTemplateID, UnitTemplateData in pairs(SpawnGroupTemplate.units) do
        --self:T( { UnitTemplateData.name, self:Name() } )
        SpawnGroupTemplate.units[UnitTemplateID].unitId = nil
        if UnitTemplateData.name == self:Name() then
            --self:T("Adjusting")
            SpawnGroupTemplate.units[UnitTemplateID].alt = Coordinate.y
            SpawnGroupTemplate.units[UnitTemplateID].x = Coordinate.x
            SpawnGroupTemplate.units[UnitTemplateID].y = Coordinate.z
            SpawnGroupTemplate.units[UnitTemplateID].heading = Heading
            --self:F( { UnitTemplateID, SpawnGroupTemplate.units[UnitTemplateID], SpawnGroupTemplate.units[UnitTemplateID] } )
        else
            --self:F( SpawnGroupTemplate.units[UnitTemplateID].name )
            local GroupUnit = UNIT:FindByName(SpawnGroupTemplate.units[UnitTemplateID].name) -- #UNIT
            if GroupUnit and GroupUnit:IsAlive() then
                local GroupUnitVec3 = GroupUnit:GetVec3()
                local GroupUnitHeading = GroupUnit:GetHeading()
                UnitTemplateData.alt = GroupUnitVec3.y
                UnitTemplateData.x = GroupUnitVec3.x
                UnitTemplateData.y = GroupUnitVec3.z
                UnitTemplateData.heading = GroupUnitHeading
            else
                if SpawnGroupTemplate.units[UnitTemplateID].name ~= self:Name() then
                    --self:T("nilling")
                    SpawnGroupTemplate.units[UnitTemplateID].delete = true
                end
            end
        end
    end

    -- Remove obscolete units from the group structure
    local i = 1
    while i <= #SpawnGroupTemplate.units do

        local UnitTemplateData = SpawnGroupTemplate.units[i]
        --self:T( UnitTemplateData.name )

        if UnitTemplateData.delete then
            table.remove(SpawnGroupTemplate.units, i)
        else
            i = i + 1
        end
    end

    SpawnGroupTemplate.groupId = nil

    --self:T( SpawnGroupTemplate )

    if self.ValidateAndRepositionGroundUnits then
        UTILS.ValidateAndRepositionGroundUnits(SpawnGroupTemplate.units)
    end

    _DATABASE:Spawn(SpawnGroupTemplate)
end



--- Returns if the unit is activated.
-- @param #UNIT self
-- @return #boolean `true` if Unit is activated. `nil` The DCS Unit is not existing or alive.  
function UNIT:IsActive()
    --self:F2( self.UnitName )

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then

        local UnitIsActive = DCSUnit:isActive()
        return UnitIsActive
    end

    return nil
end

--- Returns if the unit is exists in the mission.
-- If not even the DCS unit object does exist, `nil` is returned.  
-- If the unit object exists, the value of the DCS API function [isExist](https://wiki.hoggitworld.com/view/DCS_func_isExist) is returned.  
-- @param #UNIT self
-- @return #boolean Returns `true` if unit exists in the mission.
function UNIT:IsExist()

    local DCSUnit = self:GetDCSObject() -- DCS#Unit

    if DCSUnit then
        local exists = DCSUnit:isExist()
        return exists
    end

    return nil
end

--- Returns if the Unit is alive.  
-- If the Unit is not alive/existent, `nil` is returned.  
-- If the Unit is alive and active, `true` is returned.    
-- If the Unit is alive but not active, `false`` is returned.  
-- @param #UNIT self
-- @return #boolean Returns `true` if Unit is alive and active, `false` if it exists but is not active and `nil` if the object does not exist or DCS `isExist` function returns false.
function UNIT:IsAlive()
    --self:F3( self.UnitName )

    local DCSUnit = self:GetDCSObject() -- DCS#Unit

    if DCSUnit and DCSUnit:isExist() then
        local UnitIsAlive = DCSUnit:isActive()
        return UnitIsAlive
    end

    return nil
end

--- Returns if the Unit is dead.
-- @param #UNIT self  
-- @return #boolean `true` if Unit is dead, else false or nil if the unit does not exist
function UNIT:IsDead()
    return not self:IsAlive()
end

--- Returns the Unit's callsign - the localized string.
-- @param #UNIT self
-- @return #string The Callsign of the Unit.
function UNIT:GetCallsign()
    --self:F2( self.UnitName )

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local UnitCallSign = DCSUnit:getCallsign()
        if UnitCallSign == "" then
            UnitCallSign = DCSUnit:getName()
        end
        return UnitCallSign
    end

    --self:F( self.ClassName .. " " .. self.UnitName .. " not found!" )
    return nil
end

--- Check if an (air) unit is a client or player slot. Information is retrieved from the group template.
-- @param #UNIT self
-- @return #boolean If true, unit is associated with a client or player slot.
function UNIT:IsPlayer()

    -- Get group.
    local group = self:GetGroup()

    if not group then
        return false
    end

    -- Units of template group.
    local template = group:GetTemplate()

    if (template == nil) or (template.units == nil) then
        local DCSObject = self:GetDCSObject()
        if DCSObject then
            if DCSObject:getPlayerName() ~= nil then
                return true
            else
                return false
            end
        else
            return false
        end
    end

    local units = template.units

    -- Get numbers.
    for _, unit in pairs(units) do

        -- Check if unit name matach and skill is Client or Player.
        if unit.name == self:GetName() and (unit.skill == "Client" or unit.skill == "Player") then
            return true
        end

    end

    return false
end


--- Returns name of the player that control the unit or nil if the unit is controlled by A.I.
-- @param #UNIT self
-- @return #string Player Name
-- @return #nil The DCS Unit is not existing or alive.  
function UNIT:GetPlayerName()
    --self:F( self.UnitName )

    local DCSUnit = self:GetDCSObject() -- DCS#Unit

    if DCSUnit then

        local PlayerName = DCSUnit:getPlayerName()
        -- TODO Workaround DCS-BUG-3 - https://github.com/FlightControl-Master/MOOSE/issues/696
        --    if PlayerName == nil or PlayerName == "" then
        --      local PlayerCategory = DCSUnit:getDesc().category
        --      if PlayerCategory == Unit.Category.GROUND_UNIT or PlayerCategory == Unit.Category.SHIP then
        --        PlayerName = "Player" .. DCSUnit:getID()
        --      end
        --    end
        --    -- Good code
        --    if PlayerName == nil then
        --      PlayerName = nil
        --    else
        --      if PlayerName == "" then
        --        PlayerName = "Player" .. DCSUnit:getID()
        --      end
        --    end
        return PlayerName
    end

    return nil

end

--- Checks is the unit is a *Player* or *Client* slot.  
-- @param #UNIT self
-- @return #boolean If true, unit is a player or client aircraft  
function UNIT:IsClient()

    if _DATABASE.CLIENTS[self.UnitName] then
        return true
    end

    return false
end

--- Get the CLIENT of the unit  
-- @param #UNIT self
-- @return Wrapper.Client#CLIENT  
function UNIT:GetClient()

    local client = _DATABASE.CLIENTS[self.UnitName]

    if client then
        return client
    end

    return nil
end

--- [AIRPLANE] Get the NATO reporting name of a UNIT. Currently airplanes only!
--@param #UNIT self
--@return #string NatoReportingName or "Bogey" if unknown.
function UNIT:GetNatoReportingName()

    local typename = self:GetTypeName()
    return UTILS.GetReportingName(typename)

end


--- Returns the unit's number in the group. 
-- The number is the same number the unit has in ME. 
-- It may not be changed during the mission. 
-- If any unit in the group is destroyed, the numbers of another units will not be changed.
-- @param #UNIT self
-- @return #number The Unit number. 
-- @return #nil The DCS Unit is not existing or alive.  
function UNIT:GetNumber()
    --self:F2( self.UnitName )

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local UnitNumber = DCSUnit:getNumber()
        return UnitNumber
    end

    return nil
end


--- Returns the unit's max speed in km/h derived from the DCS descriptors.
-- @param #UNIT self
-- @return #number Speed in km/h. 
function UNIT:GetSpeedMax()
    --self:F2( self.UnitName )

    local Desc = self:GetDesc()

    if Desc then
        local SpeedMax = Desc.speedMax or 0
        return SpeedMax * 3.6
    end

    return 0
end

--- Returns the unit's max range in meters derived from the DCS descriptors.
-- For ground units it will return a range of 10,000 km as they have no real range.
-- @param #UNIT self
-- @return #number Range in meters.
function UNIT:GetRange()
    --self:F2( self.UnitName )

    local Desc = self:GetDesc()

    if Desc then
        local Range = Desc.range --This is in kilometers (not meters) for some reason. But should check again!
        if Range then
            Range = Range * 1000 -- convert to meters.
        else
            Range = 10000000 --10.000 km if no range
        end
        return Range
    end

    return nil
end

--- Check if the unit is refuelable. Also retrieves the refuelling system (boom or probe) if applicable.
-- @param #UNIT self
-- @return #boolean If true, unit is refuelable (checks for the attribute "Refuelable").
-- @return #number Refueling system (if any): 0=boom, 1=probe.
function UNIT:IsRefuelable()
    --self:F2( self.UnitName )

    local refuelable = self:HasAttribute("Refuelable")

    local system = nil

    local Desc = self:GetDesc()
    if Desc and Desc.tankerType then
        system = Desc.tankerType
    end

    return refuelable, system
end

--- Check if the unit is a tanker. Also retrieves the refuelling system (boom or probe) if applicable.
-- @param #UNIT self
-- @return #boolean If true, unit is a tanker (checks for the attribute "Tankers").
-- @return #number Refueling system (if any): 0=boom, 1=probe.
function UNIT:IsTanker()
    --self:F2( self.UnitName )

    local tanker = self:HasAttribute("Tankers")

    local system = nil

    if tanker then

        local Desc = self:GetDesc()
        if Desc and Desc.tankerType then
            system = Desc.tankerType
        end

        local typename = self:GetTypeName()

        -- Some hard coded data as this is not in the descriptors...
        if typename == "IL-78M" then
            system = 1 --probe
        elseif typename == "KC130" or typename == "KC130J" then
            system = 1 --probe
        elseif typename == "KC135BDA" then
            system = 1 --probe
        elseif typename == "KC135MPRS" then
            system = 1 --probe
        elseif typename == "S-3B Tanker" then
            system = 1 --probe
        elseif typename == "KC_10_Extender" then
            system = 1 --probe
        elseif typename == "KC_10_Extender_D" then
            system = 0 --boom
        end

    end

    return tanker, system
end

--- Check if the unit can supply ammo. Currently, we have
-- 
-- * M 818
-- * Ural-375
-- * ZIL-135
-- 
-- This list needs to be extended, if DCS adds other units capable of supplying ammo.
-- 
-- @param #UNIT self
-- @return #boolean If `true`, unit can supply ammo.
function UNIT:IsAmmoSupply()

    -- Type name is the only thing we can check. There is no attribute (Sep. 2021) which would tell us.
    local typename = self:GetTypeName()

    if typename == "M 818" then
        -- Blue ammo truck.
        return true
    elseif typename == "Ural-375" then
        -- Red ammo truck.
        return true
    elseif typename == "ZIL-135" then
        -- Red ammo truck. Checked that it can also provide ammo.
        return true
    end

    return false
end

--- Check if the unit can supply fuel. Currently, we have
-- 
-- * M978 HEMTT Tanker
-- * ATMZ-5
-- * ATMZ-10
-- * ATZ-5
-- 
-- This list needs to be extended, if DCS adds other units capable of supplying fuel.
-- 
-- @param #UNIT self
-- @return #boolean If `true`, unit can supply fuel.
function UNIT:IsFuelSupply()

    -- Type name is the only thing we can check. There is no attribute (Sep. 2021) which would tell us.
    local typename = self:GetTypeName()

    if typename == "M978 HEMTT Tanker" then
        return true
    elseif typename == "ATMZ-5" then
        return true
    elseif typename == "ATMZ-10" then
        return true
    elseif typename == "ATZ-5" then
        return true
    end

    return false
end

--- Returns the unit's group if it exists and nil otherwise.
-- @param Wrapper.Unit#UNIT self
-- @return Wrapper.Group#GROUP The Group of the Unit or `nil` if the unit does not exist.  
function UNIT:GetGroup()
    --self:F2( self.UnitName )
    local UnitGroup = GROUP:FindByName(self.GroupName)
    if UnitGroup then
        return UnitGroup
    else
        local DCSUnit = self:GetDCSObject()
        if DCSUnit then
            local grp = DCSUnit:getGroup()
            if grp then
                local UnitGroup = GROUP:FindByName(grp:getName())
                return UnitGroup
            end
        end
    end
    return nil
end

--- Returns the prefix name of the DCS Unit. A prefix name is a part of the name before a '#'-sign.
-- DCS Units spawned with the @{Core.Spawn#SPAWN} class contain a '#'-sign to indicate the end of the (base) DCS Unit name. 
-- The spawn sequence number and unit number are contained within the name after the '#' sign. 
-- @param #UNIT self
-- @return #string The name of the DCS Unit.
-- @return #nil The DCS Unit is not existing or alive.  
function UNIT:GetPrefix()
    --self:F2( self.UnitName )

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local UnitPrefix = string.match(self.UnitName, ".*#"):sub(1, -2)
        --self:T3( UnitPrefix )
        return UnitPrefix
    end

    return nil
end

--- Returns the Unit's ammunition.
-- @param #UNIT self
-- @return DCS#Unit.Ammo Table with ammuntion of the unit (or nil). This can be a complex table! 
function UNIT:GetAmmo()
    --self:F2( self.UnitName )
    local DCSUnit = self:GetDCSObject()
    if DCSUnit then
        --local status, unitammo = pcall(
        -- function()
        -- local UnitAmmo = DCSUnit:getAmmo()
        -- return UnitAmmo
        --end
        --)
        --if status then
        --return unitammo
        --end
        local UnitAmmo = DCSUnit:getAmmo()
        return UnitAmmo
    end
    return nil
end


--- Sets the Unit's Internal Cargo Mass, in kg
-- @param #UNIT self
-- @param #number mass to set cargo to
-- @return #UNIT self
function UNIT:SetUnitInternalCargo(mass)
    local DCSUnit = self:GetDCSObject()
    if DCSUnit then
        trigger.action.setUnitInternalCargo(DCSUnit:getName(), mass)
    end
    return self
end

--- Get the number of ammunition and in particular the number of shells, rockets, bombs and missiles a unit currently has.
-- @param #UNIT self
-- @return #number Total amount of ammo the unit has left. This is the sum of shells, rockets, bombs and missiles.
-- @return #number Number of shells left. Shells include MG ammunition, AP and HE shells, and artillery shells where applicable.
-- @return #number Number of rockets left.
-- @return #number Number of bombs left.
-- @return #number Number of missiles left.
-- @return #number Number of artillery shells left (with explosive mass, included in shells; HE will also be reported as artillery shells for tanks)
-- @return #number Number of tank AP shells left (for tanks, if applicable)
-- @return #number Number of tank HE shells left (for tanks, if applicable)
function UNIT:GetAmmunition()

    -- Init counter.
    local nammo = 0
    local nshells = 0
    local nrockets = 0
    local nmissiles = 0
    local nbombs = 0
    local narti = 0
    local nAPshells = 0
    local nHEshells = 0

    local unit = self

    -- Get ammo table.
    local ammotable = unit:GetAmmo()

    if ammotable then

        local weapons = #ammotable

        -- Loop over all weapons.
        for w = 1, weapons do

            -- Number of current weapon.
            local Nammo = ammotable[w]["count"]

            -- Type name of current weapon.
            local Tammo = ammotable[w]["desc"]["typeName"]

            --local _weaponString = UTILS.Split(Tammo,"%.")
            --local _weaponName   = _weaponString[#_weaponString]

            -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3
            local Category = ammotable[w].desc.category

            -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6
            local MissileCategory = nil
            if Category == Weapon.Category.MISSILE then
                MissileCategory = ammotable[w].desc.missileCategory
            end

            -- We are specifically looking for shells or rockets here.
            if Category == Weapon.Category.SHELL then

                -- Add up all shells.
                nshells = nshells + Nammo

                if ammotable[w].desc.warhead and ammotable[w].desc.warhead.explosiveMass and ammotable[w].desc.warhead.explosiveMass > 0 then
                    narti = narti + Nammo
                end

                if ammotable[w].desc.typeName and string.find(ammotable[w].desc.typeName, "_AP", 1, true) then
                    nAPshells = nAPshells + Nammo
                end

                if ammotable[w].desc.typeName and (string.find(ammotable[w].desc.typeName, "_HE", 1, true) or string.find(ammotable[w].desc.typeName, "HESH", 1, true)) then
                    nHEshells = nHEshells + Nammo
                end

            elseif Category == Weapon.Category.ROCKET then

                -- Add up all rockets.
                nrockets = nrockets + Nammo

            elseif Category == Weapon.Category.BOMB then

                -- Add up all rockets.
                nbombs = nbombs + Nammo

            elseif Category == Weapon.Category.MISSILE then


                -- Add up all  missiles (category 5)
                if MissileCategory == Weapon.MissileCategory.AAM then
                    nmissiles = nmissiles + Nammo
                elseif MissileCategory == Weapon.MissileCategory.ANTI_SHIP then
                    nmissiles = nmissiles + Nammo
                elseif MissileCategory == Weapon.MissileCategory.BM then
                    nmissiles = nmissiles + Nammo
                elseif MissileCategory == Weapon.MissileCategory.OTHER then
                    nmissiles = nmissiles + Nammo
                elseif MissileCategory == Weapon.MissileCategory.SAM then
                    nmissiles = nmissiles + Nammo
                elseif MissileCategory == Weapon.MissileCategory.CRUISE then
                    nmissiles = nmissiles + Nammo
                end

            end

        end
    end

    -- Total amount of ammunition.
    nammo = nshells + nrockets + nmissiles + nbombs

    return nammo, nshells, nrockets, nbombs, nmissiles, narti, nAPshells, nHEshells
end

--- Checks if a tank still has AP shells.
-- @param #UNIT self
-- @return #boolean HasAPShells  
function UNIT:HasAPShells()
    local _, _, _, _, _, _, shells = self:GetAmmunition()
    if shells > 0 then
        return true
    else
        return false
    end
end

--- Get number of AP shells from a tank.
-- @param #UNIT self
-- @return #number Number of AP shells 
function UNIT:GetAPShells()
    local _, _, _, _, _, _, shells = self:GetAmmunition()
    return shells or 0
end

--- Get number of HE shells from a tank.
-- @param #UNIT self
-- @return #number Number of HE shells
function UNIT:GetHEShells()
    local _, _, _, _, _, _, _, shells = self:GetAmmunition()
    return shells or 0
end

--- Checks if a tank still has HE shells.
-- @param #UNIT self
-- @return #boolean HasHEShells  
function UNIT:HasHEShells()
    local _, _, _, _, _, _, _, shells = self:GetAmmunition()
    if shells > 0 then
        return true
    else
        return false
    end
end

--- Checks if an artillery unit still has artillery shells.
-- @param #UNIT self
-- @return #boolean HasArtiShells  
function UNIT:HasArtiShells()
    local _, _, _, _, _, shells = self:GetAmmunition()
    if shells > 0 then
        return true
    else
        return false
    end
end

--- Get number of artillery shells from an artillery unit.
-- @param #UNIT self
-- @return #number Number of artillery shells
function UNIT:GetArtiShells()
    local _, _, _, _, _, shells = self:GetAmmunition()
    return shells or 0
end

--- Returns the unit sensors.
-- @param #UNIT self
-- @return DCS#Unit.Sensors Table of sensors.  
function UNIT:GetSensors()
    --self:F2( self.UnitName )

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local UnitSensors = DCSUnit:getSensors()
        return UnitSensors
    end

    return nil
end

-- Need to add here a function per sensortype
--  unit:hasSensors(Unit.SensorType.RADAR, Unit.RadarType.AS)

--- Returns if the unit has sensors of a certain type.
-- @param #UNIT self
-- @return #boolean returns true if the unit has specified types of sensors. This function is more preferable than Unit.getSensors() if you don't want to get information about all the unit's sensors, and just want to check if the unit has specified types of sensors. 
function UNIT:HasSensors(...)
    --self:F2( arg )

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local HasSensors = DCSUnit:hasSensors(unpack(arg))
        return HasSensors
    end

    return nil
end

--- Returns if the unit is SEADable.
-- @param #UNIT self
-- @return #boolean returns true if the unit is SEADable. 
function UNIT:HasSEAD()
    --self:F2()

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local UnitSEADAttributes = DCSUnit:getDesc().attributes

        local HasSEAD = false
        if UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND1_FOR_ARM"] == true or
        UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] and UnitSEADAttributes["RADAR_BAND2_FOR_ARM"] == true or
        UnitSEADAttributes["Optical Tracker"] and UnitSEADAttributes["Optical Tracker"] == true
        then
            HasSEAD = true
        end
        return HasSEAD
    end

    return nil
end

--- Returns two values:
-- 
--  * First value indicates if at least one of the unit's radar(s) is on.
--  * Second value is the object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target.
-- @param #UNIT self
-- @return #boolean  Indicates if at least one of the unit's radar(s) is on.
-- @return DCS#Object The object of the radar's interest. Not nil only if at least one radar of the unit is tracking a target.
function UNIT:GetRadar()
    --self:F2( self.UnitName )

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local UnitRadarOn, UnitRadarObject = DCSUnit:getRadar()
        return UnitRadarOn, UnitRadarObject
    end

    return nil, nil
end

--- Returns relative amount of fuel (from 0.0 to 1.0) the UNIT has in its internal tanks. If there are additional fuel tanks the value may be greater than 1.0.
-- @param #UNIT self
-- @return #number The relative amount of fuel (from 0.0 to 1.0) or *nil* if the DCS Unit is not existing or alive. 
function UNIT:GetFuel()
    --self:F3( self.UnitName )

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local UnitFuel = DCSUnit:getFuel()
        return UnitFuel
    end

    return nil
end


--- Returns a list of one @{Wrapper.Unit}.
-- @param #UNIT self
-- @return #list<Wrapper.Unit#UNIT> A list of one @{Wrapper.Unit}.
function UNIT:GetUnits()
    --self:F3( { self.UnitName } )
    local DCSUnit = self:GetDCSObject()

    local Units = {}

    if DCSUnit then
        Units[1] = UNIT:Find(DCSUnit)
        return Units
    end

    return nil
end


--- Returns the unit's health. Dead units has health <= 1.0.
-- @param #UNIT self
-- @return #number The Unit's health value or -1 if unit does not exist any more.
function UNIT:GetLife()
    --self:F2( self.UnitName )

    local DCSUnit = self:GetDCSObject()

    if DCSUnit and DCSUnit:isExist() then
        local UnitLife = DCSUnit:getLife()
        return UnitLife
    end

    return -1
end

--- Returns the Unit's initial health.
-- @param #UNIT self
-- @return #number The Unit's initial health value or 0 if unit does not exist any more.  
function UNIT:GetLife0()
    --self:F2( self.UnitName )

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local UnitLife0 = DCSUnit:getLife0()
        return UnitLife0
    end

    return 0
end

--- Returns the unit's relative health.
-- @param #UNIT self
-- @return #number The Unit's relative health value, i.e. a number in [0,1] or -1 if unit does not exist any more.
function UNIT:GetLifeRelative()
    --self:F2(self.UnitName)

    if self and self:IsAlive() then
        local life0 = self:GetLife0()
        local lifeN = self:GetLife()
        return lifeN / life0
    end

    return -1
end

--- Returns the unit's relative damage, i.e. 1-life.
-- @param #UNIT self
-- @return #number The Unit's relative health value, i.e. a number in [0,1] or 1 if unit does not exist any more.
function UNIT:GetDamageRelative()
    --self:F2(self.UnitName)

    if self and self:IsAlive() then
        return 1 - self:GetLifeRelative()
    end

    return 1
end

--- Returns the current value for an animation argument on the external model of the given object. 
-- Each model animation has an id tied to with different values representing different states of the model. 
-- Animation arguments can be figured out by opening the respective 3d model in the modelviewer.
-- @param #UNIT self
-- @param #number AnimationArgument Number corresponding to the animated part of the unit.
-- @return #number Value of the animation argument [-1, 1]. If draw argument value is invalid for the unit in question a value of 0 will be returned.
function UNIT:GetDrawArgumentValue(AnimationArgument)

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local value = DCSUnit:getDrawArgumentValue(AnimationArgument or 0)
        return value
    end

    return 0
end

--- Returns the category of the #UNIT from descriptor. Returns one of
-- 
-- * Unit.Category.AIRPLANE
-- * Unit.Category.HELICOPTER
-- * Unit.Category.GROUND_UNIT
-- * Unit.Category.SHIP
-- * Unit.Category.STRUCTURE
-- 
-- @param #UNIT self
-- @return #number Unit category from `getDesc().category`.
function UNIT:GetUnitCategory()
    --self:F3( self.UnitName )

    local DCSUnit = self:GetDCSObject()
    if DCSUnit then
        return DCSUnit:getDesc().category
    end

    return nil
end

--- Returns the category name of the #UNIT.
-- @param #UNIT self
-- @return #string Category name = Helicopter, Airplane, Ground Unit, Ship
function UNIT:GetCategoryName()
    --self:F3( self.UnitName )

    local DCSUnit = self:GetDCSObject()
    if DCSUnit then
        local CategoryNames = {
            [Unit.Category.AIRPLANE] = "Airplane",
            [Unit.Category.HELICOPTER] = "Helicopter",
            [Unit.Category.GROUND_UNIT] = "Ground Unit",
            [Unit.Category.SHIP] = "Ship",
            [Unit.Category.STRUCTURE] = "Structure",
        }
        local UnitCategory = DCSUnit:getDesc().category
        --self:T3( UnitCategory )

        return CategoryNames[UnitCategory]
    end

    return nil
end


--- Returns the Unit's A2G threat level on a scale from 1 to 10 ...
-- Depending on the era and the type of unit, the following threat levels are foreseen:
-- 
-- **Modern**:
-- 
--   * Threat level  0: Unit is unarmed.
--   * Threat level  1: Unit is infantry.
--   * Threat level  2: Unit is an infantry vehicle.
--   * Threat level  3: Unit is ground artillery.
--   * Threat level  4: Unit is a tank.
--   * Threat level  5: Unit is a modern tank or ifv with ATGM.
--   * Threat level  6: Unit is a AAA.
--   * Threat level  7: Unit is a SAM or manpad, IR guided.
--   * Threat level  8: Unit is a Short Range SAM, radar guided.
--   * Threat level  9: Unit is a Medium Range SAM, radar guided.
--   * Threat level 10: Unit is a Long Range SAM, radar guided.
-- 
-- **Cold**:
-- 
--   * Threat level  0: Unit is unarmed.
--   * Threat level  1: Unit is infantry.
--   * Threat level  2: Unit is an infantry vehicle.
--   * Threat level  3: Unit is ground artillery.
--   * Threat level  4: Unit is a tank.
--   * Threat level  5: Unit is a modern tank or ifv with ATGM.
--   * Threat level  6: Unit is a AAA.
--   * Threat level  7: Unit is a SAM or manpad, IR guided.
--   * Threat level  8: Unit is a Short Range SAM, radar guided.
--   * Threat level  10: Unit is a Medium Range SAM, radar guided.
--  
-- **Korea**:
-- 
--   * Threat level  0: Unit is unarmed.
--   * Threat level  1: Unit is infantry.
--   * Threat level  2: Unit is an infantry vehicle.
--   * Threat level  3: Unit is ground artillery.
--   * Threat level  5: Unit is a tank.
--   * Threat level  6: Unit is a AAA.
--   * Threat level  7: Unit is a SAM or manpad, IR guided.
--   * Threat level  10: Unit is a Short Range SAM, radar guided.
--  
-- **WWII**:
-- 
--   * Threat level  0: Unit is unarmed.
--   * Threat level  1: Unit is infantry.
--   * Threat level  2: Unit is an infantry vehicle.
--   * Threat level  3: Unit is ground artillery.
--   * Threat level  5: Unit is a tank.
--   * Threat level  7: Unit is FLAK.
--   * Threat level  10: Unit is AAA.
--  
-- 
-- @param #UNIT self
-- @return #number Number between 0 (low threat level) and 10 (high threat level).
-- @return #string Some text.
function UNIT:GetThreatLevel()


    local ThreatLevel = 0
    local ThreatText = ""

    local Descriptor = self:GetDesc()

    if Descriptor then

        local Attributes = Descriptor.attributes

        if self:IsGround() then

            local ThreatLevels = {
                [1] = "Unarmed",
                [2] = "Infantry",
                [3] = "Old Tanks & APCs",
                [4] = "Tanks & IFVs without ATGM",
                [5] = "Tanks & IFV with ATGM",
                [6] = "Modern Tanks",
                [7] = "AAA",
                [8] = "IR Guided SAMs",
                [9] = "SR SAMs",
                [10] = "MR SAMs",
                [11] = "LR SAMs"
            }

            if Attributes["LR SAM"] then
                ThreatLevel = 10
            elseif Attributes["MR SAM"] then
                ThreatLevel = 9
            elseif Attributes["SR SAM"] and
            not Attributes["IR Guided SAM"] then
                ThreatLevel = 8
            elseif (Attributes["SR SAM"] or Attributes["MANPADS"]) and
            Attributes["IR Guided SAM"] then
                ThreatLevel = 7
            elseif Attributes["AAA"] then
                ThreatLevel = 6
            elseif Attributes["Modern Tanks"] then
                ThreatLevel = 5
            elseif (Attributes["Tanks"] or Attributes["IFV"]) and
            Attributes["ATGM"] then
                ThreatLevel = 4
            elseif (Attributes["Tanks"] or Attributes["IFV"]) and
            not Attributes["ATGM"] then
                ThreatLevel = 3
            elseif Attributes["Old Tanks"] or Attributes["APC"] or Attributes["Artillery"] then
                ThreatLevel = 2
            elseif Attributes["Infantry"] or Attributes["EWR"] then
                ThreatLevel = 1
            end

            ThreatText = ThreatLevels[ThreatLevel + 1]
        end

        if self:IsAir() then

            local ThreatLevels = {
                [1] = "Unarmed",
                [2] = "Tanker",
                [3] = "AWACS",
                [4] = "Transport Helicopter",
                [5] = "UAV",
                [6] = "Bomber",
                [7] = "Strategic Bomber",
                [8] = "Attack Helicopter",
                [9] = "Battleplane",
                [10] = "Multirole Fighter",
                [11] = "Fighter"
            }

            if Attributes["Fighters"] then
                ThreatLevel = 10
            elseif Attributes["Multirole fighters"] then
                ThreatLevel = 9
            elseif Attributes["Interceptors"] then
                ThreatLevel = 9
            elseif Attributes["Battleplanes"] then
                ThreatLevel = 8
            elseif Attributes["Battle airplanes"] then
                ThreatLevel = 8
            elseif Attributes["Attack helicopters"] then
                ThreatLevel = 7
            elseif Attributes["Strategic bombers"] then
                ThreatLevel = 6
            elseif Attributes["Bombers"] then
                ThreatLevel = 5
            elseif Attributes["UAVs"] then
                ThreatLevel = 4
            elseif Attributes["Transport helicopters"] then
                ThreatLevel = 3
            elseif Attributes["AWACS"] then
                ThreatLevel = 2
            elseif Attributes["Tankers"] then
                ThreatLevel = 1
            end

            ThreatText = ThreatLevels[ThreatLevel + 1]
        end

        if self:IsShip() then

            --["Aircraft Carriers"] = {"Heavy armed ships",},
            --["Cruisers"] = {"Heavy armed ships",},
            --["Destroyers"] = {"Heavy armed ships",},
            --["Frigates"] = {"Heavy armed ships",},
            --["Corvettes"] = {"Heavy armed ships",},
            --["Heavy armed ships"] = {"Armed ships", "Armed Air Defence", "HeavyArmoredUnits",},
            --["Light armed ships"] = {"Armed ships","NonArmoredUnits"},
            --["Armed ships"] = {"Ships"},
            --["Unarmed ships"] = {"Ships","HeavyArmoredUnits",},

            local ThreatLevels = {
                [1] = "Unarmed ship",
                [2] = "Light armed ships",
                [3] = "Corvettes",
                [4] = "",
                [5] = "Frigates",
                [6] = "",
                [7] = "Cruiser",
                [8] = "",
                [9] = "Destroyer",
                [10] = "",
                [11] = "Aircraft Carrier"
            }

            if Attributes["Aircraft Carriers"] then
                ThreatLevel = 10
            elseif Attributes["Destroyers"] then
                ThreatLevel = 8
            elseif Attributes["Cruisers"] then
                ThreatLevel = 6
            elseif Attributes["Frigates"] then
                ThreatLevel = 4
            elseif Attributes["Corvettes"] then
                ThreatLevel = 2
            elseif Attributes["Light armed ships"] then
                ThreatLevel = 1
            end

            ThreatText = ThreatLevels[ThreatLevel + 1]
        end
    end

    return ThreatLevel, ThreatText

end

--- Triggers an explosion at the coordinates of the unit.
-- @param #UNIT self
-- @param #number power Power of the explosion in kg TNT. Default 100 kg TNT.
-- @param #number delay (Optional) Delay of explosion in seconds.
-- @return #UNIT self
function UNIT:Explode(power, delay)

    -- Default.
    power = power or 100

    local DCSUnit = self:GetDCSObject()
    if DCSUnit then

        -- Check if delay or not.
        if delay and delay > 0 then
            -- Delayed call.
            SCHEDULER:New(nil, self.Explode, { self, power }, delay)
        else
            -- Create an explotion at the coordinate of the unit.
            self:GetCoordinate():Explosion(power)
        end

        return self
    end

    return nil
end

-- Is functions



--- Returns true if there is an **other** DCS Unit within a radius of the current 2D point of the DCS Unit.
-- @param #UNIT self
-- @param #UNIT AwaitUnit The other UNIT wrapper object.
-- @param Radius The radius in meters with the DCS Unit in the centre.
-- @return true If the other DCS Unit is within the radius of the 2D point of the DCS Unit. 
-- @return #nil The DCS Unit is not existing or alive.  
function UNIT:OtherUnitInRadius(AwaitUnit, Radius)
    --self:F2( { self.UnitName, AwaitUnit.UnitName, Radius } )

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local UnitVec3 = self:GetVec3()
        local AwaitUnitVec3 = AwaitUnit:GetVec3()

        if (((UnitVec3.x - AwaitUnitVec3.x) ^ 2 + (UnitVec3.z - AwaitUnitVec3.z) ^ 2) ^ 0.5 <= Radius) then
            --self:T3( "true" )
            return true
        else
            --self:T3( "false" )
            return false
        end
    end

    return nil
end







--- Returns if the unit is a friendly unit.
-- @param #UNIT self
-- @return #boolean IsFriendly evaluation result.
function UNIT:IsFriendly(FriendlyCoalition)
    --self:F2()

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local UnitCoalition = DCSUnit:getCoalition()
        --self:T3( { UnitCoalition, FriendlyCoalition } )

        local IsFriendlyResult = (UnitCoalition == FriendlyCoalition)

        --self:F( IsFriendlyResult )
        return IsFriendlyResult
    end

    return nil
end

--- Returns if the unit is of a ship category.
-- If the unit is a ship, this method will return true, otherwise false.
-- @param #UNIT self
-- @return #boolean Ship category evaluation result.
function UNIT:IsShip()
    --self:F2()

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then
        local UnitDescriptor = DCSUnit:getDesc()
        --self:T3( { UnitDescriptor.category, Unit.Category.SHIP } )

        local IsShipResult = (UnitDescriptor.category == Unit.Category.SHIP)

        --self:T3( IsShipResult )
        return IsShipResult
    end

    return nil
end

--- Returns true if the UNIT is in the air.
-- @param #UNIT self
-- @param #boolean NoHeloCheck If true, no additonal checks for helos are performed.
-- @return #boolean Return true if in the air or #nil if the UNIT is not existing or alive.   
function UNIT:InAir(NoHeloCheck)
    --self:F2( self.UnitName )

    -- Get DCS unit object.
    local DCSUnit = self:GetDCSObject() --DCS#Unit

    if DCSUnit then

        -- Get DCS result of whether unit is in air or not.
        local UnitInAir = DCSUnit:inAir()

        -- Get unit category.
        local UnitCategory = DCSUnit:getDesc().category

        -- If DCS says that it is in air, check if this is really the case, since we might have landed on a building where inAir()=true but actually is not.
        -- This is a workaround since DCS currently does not acknowledge that helos land on buildings.
        -- Note however, that the velocity check will fail if the ground is moving, e.g. on an aircraft carrier!
        if UnitInAir == true and UnitCategory == Unit.Category.HELICOPTER and (not NoHeloCheck) then
            local VelocityVec3 = DCSUnit:getVelocity()
            local Velocity = UTILS.VecNorm(VelocityVec3)
            local Coordinate = DCSUnit:getPoint()
            local LandHeight = land.getHeight({ x = Coordinate.x, y = Coordinate.z })
            local Height = Coordinate.y - LandHeight
            if Velocity < 1 and Height <= 60 then
                UnitInAir = false
            end
        end

        --self:T3( UnitInAir )
        return UnitInAir
    end

    return nil
end

do
    -- Event Handling

    --- Subscribe to a DCS Event.
    -- @param #UNIT self
    -- @param Core.Event#EVENTS EventID Event ID.
    -- @param #function EventFunction (Optional) The function to be called when the event occurs for the unit.
    -- @return #UNIT self
    function UNIT:HandleEvent(EventID, EventFunction)

        self:EventDispatcher():OnEventForUnit(self:GetName(), EventFunction, self, EventID)

        return self
    end

    --- UnSubscribe to a DCS event.
    -- @param #UNIT self
    -- @param Core.Event#EVENTS EventID Event ID.
    -- @return #UNIT self
    function UNIT:UnHandleEvent(EventID)

        --self:EventDispatcher():RemoveForUnit( self:GetName(), self, EventID )

        -- Fixes issue #1365 https://github.com/FlightControl-Master/MOOSE/issues/1365
        self:EventDispatcher():RemoveEvent(self, EventID)

        return self
    end

    --- Reset the subscriptions.
    -- @param #UNIT self
    -- @return #UNIT
    function UNIT:ResetEvents()

        self:EventDispatcher():Reset(self)

        return self
    end

end

do
    -- Detection

    --- Returns if a unit is detecting the TargetUnit.
    -- @param #UNIT self
    -- @param #UNIT TargetUnit
    -- @return #boolean true If the TargetUnit is detected by the unit, otherwise false.
    function UNIT:IsDetected(TargetUnit)
        --R2.1

        local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity = self:IsTargetDetected(TargetUnit:GetDCSObject())

        return TargetIsDetected
    end

    --- Returns if a unit has Line of Sight (LOS) with the TargetUnit.
    -- @param #UNIT self
    -- @param #UNIT TargetUnit
    -- @return #boolean true If the TargetUnit has LOS with the unit, otherwise false.
    function UNIT:IsLOS(TargetUnit)
        --R2.1

        local IsLOS = self:GetPointVec3():IsLOS(TargetUnit:GetPointVec3())

        return IsLOS
    end

    --- Forces the unit to become aware of the specified target, without the unit manually detecting the other unit itself.
    -- Applies only to a Unit Controller. Cannot be used at the group level.
    -- @param #UNIT self
    -- @param #UNIT TargetUnit The unit to be known.
    -- @param #boolean TypeKnown The target type is known. If *false*, the type is not known.
    -- @param #boolean DistanceKnown The distance to the target is known. If *false*, distance is unknown.
    function UNIT:KnowUnit(TargetUnit, TypeKnown, DistanceKnown)

        -- Defaults.
        if TypeKnown ~= false then
            TypeKnown = true
        end
        if DistanceKnown ~= false then
            DistanceKnown = true
        end

        local DCSControllable = self:GetDCSObject()

        if DCSControllable then

            local Controller = DCSControllable:getController()  --self:_GetController()

            if Controller then

                local object = TargetUnit:GetDCSObject()

                if object then

                    self:I(string.format("Unit %s now knows target unit %s. Type known=%s, distance known=%s", self:GetName(), TargetUnit:GetName(), tostring(TypeKnown), tostring(DistanceKnown)))

                    Controller:knowTarget(object, TypeKnown, DistanceKnown)

                end

            end

        end

    end

end

--- Get the unit table from a unit's template.
-- @param #UNIT self
-- @return #table Table of the unit template (deep copy) or #nil.
function UNIT:GetTemplate()

    local group = self:GetGroup()

    local name = self:GetName()

    if group then
        local template = group:GetTemplate()

        if template then

            for _, unit in pairs(template.units) do

                if unit.name == name then
                    return UTILS.DeepCopy(unit)
                end
            end

        end
    end

    return nil
end


--- Get the payload table from a unit's template.
-- The payload table has elements:
-- 
--    * pylons
--    * fuel
--    * chaff
--    * gun
--    
-- @param #UNIT self
-- @return #table Payload table (deep copy) or #nil.
function UNIT:GetTemplatePayload()

    local unit = self:GetTemplate()

    if unit then
        return unit.payload
    end

    return nil
end

--- Get the pylons table from a unit's template. This can be a complex table depending on the weapons the unit is carrying.
-- @param #UNIT self
-- @return #table Table of pylons (deepcopy) or #nil.
function UNIT:GetTemplatePylons()

    local payload = self:GetTemplatePayload()

    if payload then
        return payload.pylons
    end

    return nil
end

--- Get the fuel of the unit from its template.
-- @param #UNIT self
-- @return #number Fuel of unit in kg.
function UNIT:GetTemplateFuel()

    local payload = self:GetTemplatePayload()

    if payload then
        return payload.fuel
    end

    return nil
end

--- GROUND - Switch on/off radar emissions of a unit.
-- @param #UNIT self
-- @param #boolean switch If true, emission is enabled. If false, emission is disabled. 
-- @return #UNIT self
function UNIT:EnableEmission(switch)
    --self:F2( self.UnitName )

    local switch = switch or false

    local DCSUnit = self:GetDCSObject()

    if DCSUnit then

        DCSUnit:enableEmission(switch)

    end

    return self
end

--- Get skill from Unit.
-- @param #UNIT self
-- @return #string Skill String of skill name.
function UNIT:GetSkill()
    --self:F2( self.UnitName )
    local name = self.UnitName
    local skill = "Random"
    if _DATABASE.Templates.Units[name] and _DATABASE.Templates.Units[name].Template and _DATABASE.Templates.Units[name].Template.skill then
        skill = _DATABASE.Templates.Units[name].Template.skill or "Random"
    end
    return skill
end

--- Get Link16 STN or SADL TN and other datalink info from Unit, if any.
-- @param #UNIT self
-- @return #string STN STN or TN Octal as string, or nil if not set/capable.
-- @return #string VCL Voice Callsign Label or nil if not set/capable.
-- @return #string VCN Voice Callsign Number or nil if not set/capable.
-- @return #string Lead If true, unit is Flight Lead, else false or nil.
function UNIT:GetSTN()
    --self:F2(self.UnitName)
    local STN = nil -- STN/TN
    local VCL = nil -- VoiceCallsignLabel
    local VCN = nil -- VoiceCallsignNumber
    local FGL = false -- FlightGroupLeader
    local template = self:GetTemplate()
    if template then
        if template.AddPropAircraft then
            if template.AddPropAircraft.STN_L16 then
                STN = template.AddPropAircraft.STN_L16
            elseif template.AddPropAircraft.SADL_TN then
                STN = template.AddPropAircraft.SADL_TN
            end
            VCN = template.AddPropAircraft.VoiceCallsignNumber
            VCL = template.AddPropAircraft.VoiceCallsignLabel
        end
        if template.datalinks and template.datalinks.Link16 and template.datalinks.Link16.settings then
            FGL = template.datalinks.Link16.settings.flightLead
        end
        -- A10CII
        if template.datalinks and template.datalinks.SADL and template.datalinks.SADL.settings then
            FGL = template.datalinks.SADL.settings.flightLead
        end
    end
    return STN, VCL, VCN, FGL
end

do
    -- AI methods

    --- Turns the AI On or Off for the UNIT.
    -- @param #UNIT self
    -- @param #boolean AIOnOff The value true turns the AI On, the value false turns the AI Off.
    -- @return #UNIT The UNIT.
    function UNIT:SetAIOnOff(AIOnOff)

        local DCSUnit = self:GetDCSObject() -- DCS#Group

        if DCSUnit then
            local DCSController = DCSUnit:getController() -- DCS#Controller
            if DCSController then
                DCSController:setOnOff(AIOnOff)
                return self
            end
        end

        return nil
    end

    --- Turns the AI On for the UNIT.
    -- @param #UNIT self
    -- @return #UNIT The UNIT.
    function UNIT:SetAIOn()

        return self:SetAIOnOff(true)
    end

    --- Turns the AI Off for the UNIT.
    -- @param #UNIT self
    -- @return #UNIT The UNIT.
    function UNIT:SetAIOff()

        return self:SetAIOnOff(false)
    end

end

--- [GROUND] Determine if a UNIT is a SAM unit, i.e. has radar or optical tracker and is no mobile AAA.
-- @param #UNIT self
-- @return #boolean IsSAM True if SAM, else false
function UNIT:IsSAM()
    if self:HasSEAD() and self:IsGround() and (not self:HasAttribute("Mobile AAA")) then
        return true
    end
    return false
end

--- [GROUND] Determine if a UNIT is a EWR unit
-- @param #UNIT self
-- @return #boolean IsEWR True if EWR, else false
function UNIT:IsEWR()
    if self:IsGround() then
        local DCSUnit = self:GetDCSObject()

        if DCSUnit then
            local attrs = DCSUnit:getDesc().attributes
            return attrs["EWR"] == true
        end
    end
    return false
end

--- [GROUND] Determine if a UNIT is a AAA unit, i.e. has no radar or optical tracker but the AAA = true or the "Mobile AAA" = true attribute.
-- @param #UNIT self
-- @return #boolean IsAAA True if AAA, else false
function UNIT:IsAAA()
    local unit = self -- Wrapper.Unit#UNIT
    local desc = unit:GetDesc() or {}
    local attr = desc.attributes or {}
    if unit:HasSEAD() then
        return false
    end
    if attr["AAA"] or attr["SAM related"] then
        return true
    end
    return false
end

--- Set the relative life points of a UNIT object
-- @param #UNIT self
-- @param #number Percent Percent to set, can be 0..100.
function UNIT:SetLife(Percent)
    net.dostring_in("mission",string.format("a_unit_set_life_percentage(%d, %f)", self:GetID(), Percent))
end

--- Set the carrier illumination mode. -2: OFF, -1: AUTO, 0: NAVIGATION, 1: AC LAUNCH, 2: AC RECOVERY
-- @param #UNIT self
-- @param #number Mode Illumination mode, can be -2: OFF, -1: AUTO, 0: NAVIGATION, 1: AC LAUNCH, 2: AC RECOVERY
function UNIT:SetCarrierIlluminationMode(Mode)
    UTILS.SetCarrierIlluminationMode(self:GetID(), Mode)
end

--- This function uses Disposition and other fallback logic to find better ground positions for ground units.
--- NOTE: This is not a spawn randomizer.
--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table.
--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit.
--- Uses UTILS.ValidateAndRepositionGroundUnits.
-- @param #UNIT self
-- @param #boolean Enabled Enable/disable the feature.
function UNIT:SetValidateAndRepositionGroundUnits(Enabled)
    self.ValidateAndRepositionGroundUnits = Enabled
end
--- **Wrapper** - CLIENT wraps DCS Unit objects acting as a __Client__ or __Player__ within a mission.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: **funkyfranky**
-- 
-- ===
-- 
-- @module Wrapper.Client
-- @image Wrapper_Client.JPG


--- The CLIENT class
-- @type CLIENT
-- @field #string ClassName Name of the class.
-- @field #string ClientName Name of the client.
-- @field #string ClientBriefing Briefing.
-- @field #function ClientCallBack Callback function.
-- @field #table ClientParameters Parameters of the callback function.
-- @field #number ClientGroupID Group ID of the client.
-- @field #string ClientGroupName Group name.
-- @field #boolean ClientAlive Client alive.
-- @field #boolean ClientAlive2 Client alive 2.
-- @field #table Players Player table.
-- @field Core.Point#COORDINATE SpawnCoord Spawn coordinate from the template.
-- @extends Wrapper.Unit#UNIT


--- Wrapper class of those **Units** defined within the Mission Editor that have the skillset defined as __Client__ or __Player__.
-- 
-- Note that clients are NOT the same as Units, they are NOT necessarily alive.
-- The CLIENT class is a wrapper class to handle the DCS Unit objects that have the skillset defined as __Client__ or __Player__:
-- 
--  * Wraps the DCS Unit objects with skill level set to Player or Client.
--  * Support all DCS Unit APIs.
--  * Enhance with Unit specific APIs not in the DCS Group API set.
--  * When player joins Unit, execute alive init logic.
--  * Handles messages to players.
--  * Manage the "state" of the DCS Unit.
-- 
-- Clients are being used by the @{Tasking.Mission#MISSION} class to follow players and register their successes.
--  
-- ## CLIENT reference methods
-- 
-- For each DCS Unit having skill level Player or Client, a CLIENT wrapper object (instance) will be created within the global _DATABASE object (an instance of @{Core.Database#DATABASE}).
-- This is done at the beginning of the mission (when the mission starts).
--  
-- The CLIENT class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference
-- using the DCS Unit or the DCS UnitName.
-- 
-- Another thing to know is that CLIENT objects do not "contain" the DCS Unit object. 
-- The CLIENT methods will reference the DCS Unit object by name when it is needed during API execution.
-- If the DCS Unit object does not exist or is nil, the CLIENT methods will return nil and log an exception in the DCS.log file.
--  
-- The CLIENT class provides the following functions to retrieve quickly the relevant CLIENT instance:
-- 
--  * @{#CLIENT.Find}(): Find a CLIENT instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Unit object.
--  * @{#CLIENT.FindByName}(): Find a CLIENT instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Unit name.
--  
-- **IMPORTANT: ONE SHOULD NEVER SANITIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil).**
-- 
-- @field #CLIENT
CLIENT = {
  ClassName = "CLIENT",
  ClientName = nil,
  ClientAlive = false,
  ClientTransport = false,
  ClientBriefingShown = false,
  _Menus = {},
  _Tasks = {},
  Messages = {},
  Players = {},
}


--- Finds a CLIENT from the _DATABASE using the relevant DCS Unit.
-- @param #CLIENT self
-- @param DCS#Unit DCSUnit The DCS unit of the client.
-- @param #boolean Error Throw an error message.
-- @return #CLIENT The CLIENT found in the _DATABASE.
function CLIENT:Find(DCSUnit, Error)

  local ClientName = DCSUnit:getName()
  
  local ClientFound = _DATABASE:FindClient( ClientName )
  
  if ClientFound then
    ClientFound:F( ClientName )
    return ClientFound
  end
  
  if not Error then
    error( "CLIENT not found for: " .. ClientName )
  end
end

--- Finds a CLIENT from the _DATABASE using the relevant player name.
-- @param #CLIENT self
-- @param #string Name Name of the player
-- @return #CLIENT or nil if not found
function CLIENT:FindByPlayerName(Name)
  
  local foundclient = nil
  _DATABASE:ForEachClient(
    function(client)
      if client:GetPlayerName() == Name then
        foundclient = client
      end
    end
  )
  return foundclient
end

--- Finds a CLIENT from the _DATABASE using the relevant Client Unit Name.
-- As an optional parameter, a briefing text can be given also.
-- @param #CLIENT self
-- @param #string ClientName Name of the DCS **Unit** as defined within the Mission Editor.
-- @param #string ClientBriefing Text that describes the briefing of the mission when a Player logs into the Client.
-- @param #boolean Error A flag that indicates whether an error should be raised if the CLIENT cannot be found. By default an error will be raised.
-- @return #CLIENT
-- @usage
-- -- Create new Clients.
--  local Mission = MISSIONSCHEDULER.AddMission( 'Russia Transport Troops SA-6', 'Operational', 'Transport troops from the control center to one of the SA-6 SAM sites to activate their operation.', 'Russia' )
--  Mission:AddGoal( DeploySA6TroopsGoal )
--
--  Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 1' ):Transport() )
--  Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 3' ):Transport() )
--  Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*HOT-Deploy Troops 2' ):Transport() )
--  Mission:AddClient( CLIENT:FindByName( 'RU MI-8MTV2*RAMP-Deploy Troops 4' ):Transport() )
function CLIENT:FindByName( ClientName, ClientBriefing, Error )

  -- Client
  local ClientFound = _DATABASE:FindClient( ClientName )

  if ClientFound then
    ClientFound:F( { ClientName, ClientBriefing } )
    
    ClientFound:AddBriefing(ClientBriefing)
    
    ClientFound.MessageSwitch = true

    return ClientFound
  end
  
  if not Error then
    error( "CLIENT not found for: " .. ClientName )
  end
end

--- Transport defines that the Client is a Transport. Transports show cargo.
-- @param #CLIENT self
-- @param #string ClientName Name of the client unit.
-- @return #CLIENT self
function CLIENT:Register(ClientName)

  -- Inherit unit.
  local self = BASE:Inherit( self, UNIT:Register(ClientName )) -- #CLIENT
  
  -- Set client name.
  self.ClientName = ClientName
  
  -- Message switch.
  self.MessageSwitch = true
  
  -- Alive2.
  self.ClientAlive2 = false

  return self
end


--- Transport defines that the Client is a Transport. Transports show cargo.
-- @param #CLIENT self
-- @return #CLIENT self
function CLIENT:Transport()
  self.ClientTransport = true
  return self
end

--- Adds a briefing to a CLIENT when a player joins a mission.
-- @param #CLIENT self
-- @param #string ClientBriefing is the text defining the Mission briefing.
-- @return #CLIENT self
function CLIENT:AddBriefing( ClientBriefing )

  self.ClientBriefing = ClientBriefing
  self.ClientBriefingShown = false
  
  return self
end

--- Add player name.
-- @param #CLIENT self
-- @param #string PlayerName Name of the player.
-- @return #CLIENT self
function CLIENT:AddPlayer(PlayerName)

  table.insert(self.Players, PlayerName)
  
  return self
end

--- Get number of associated players.
-- @param #CLIENT self
-- @return #number Count
function CLIENT:CountPlayers()
  return #self.Players or 0
end

--- Get player name(s).
-- @param #CLIENT self
-- @return #table List of player names or an empty table `{}`.
function CLIENT:GetPlayers()
  return self.Players
end

--- Get name of player.
-- @param #CLIENT self
-- @return #string Player name or `nil`.
function CLIENT:GetPlayer()
  if #self.Players>0 then
    return self.Players[1]
  end
  return nil
end

--- Remove player.
-- @param #CLIENT self
-- @param #string PlayerName Name of the player.
-- @return #CLIENT self
function CLIENT:RemovePlayer(PlayerName)

  for i,playername in pairs(self.Players) do
    if PlayerName==playername then
      table.remove(self.Players, i)
      break
    end
  end

  return self
end

--- Remove all players.
-- @param #CLIENT self
-- @return #CLIENT self
function CLIENT:RemovePlayers()
  self.Players={}
  return self
end


--- Show the briefing of a CLIENT.
-- @param #CLIENT self
-- @return #CLIENT self
function CLIENT:ShowBriefing()

  if not self.ClientBriefingShown then
    self.ClientBriefingShown = true
    local Briefing = ""
    if self.ClientBriefing and self.ClientBriefing ~= "" then
      Briefing = Briefing .. self.ClientBriefing
      self:Message( Briefing, 60, "Briefing" )
    end
  end

  return self
end

--- Show the mission briefing of a MISSION to the CLIENT.
-- @param #CLIENT self
-- @param #string MissionBriefing
-- @return #CLIENT self
function CLIENT:ShowMissionBriefing( MissionBriefing )
  self:F( { self.ClientName } )

  if MissionBriefing then
    self:Message( MissionBriefing, 60, "Mission Briefing" )
  end

  return self
end

--- Resets a CLIENT.
-- @param #CLIENT self
-- @param #string ClientName Name of the Group as defined within the Mission Editor. The Group must have a Unit with the type Client.
function CLIENT:Reset( ClientName )
  self:F()
  self._Menus = {}
end

-- Is Functions

--- Checks if the CLIENT is a multi-seated UNIT.
-- @param #CLIENT self
-- @return #boolean true if multi-seated.
function CLIENT:IsMultiSeated()
  self:F( self.ClientName )

  local ClientMultiSeatedTypes = { 
    ["Mi-8MT"]  = "Mi-8MT", 
    ["UH-1H"]   = "UH-1H", 
    ["P-51B"]   = "P-51B" 
  }
  
  if self:IsAlive() then
    local ClientTypeName = self:GetClientGroupUnit():GetTypeName()
    if ClientMultiSeatedTypes[ClientTypeName] then
      return true
    end
  end
  
  return false
end

--- Checks for a client alive event and calls a function on a continuous basis. Does **NOT** work for dynamic spawn client slots!
-- @param #CLIENT self
-- @param #function CallBackFunction Create a function that will be called when a player joins the slot.
-- @param ... (Optional) Arguments for callback function as comma separated list.
-- @return #CLIENT
function CLIENT:Alive( CallBackFunction, ... )
  self:F()
  
  self.ClientCallBack = CallBackFunction
  self.ClientParameters = arg

  self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. self.ClientName }, 0.1, 5, 0.5 )
  self.AliveCheckScheduler:NoTrace()

  return self
end

-- @param #CLIENT self
function CLIENT:_AliveCheckScheduler( SchedulerName )
  self:T2( { SchedulerName, self.ClientName, self.ClientAlive2, self.ClientBriefingShown, self.ClientCallBack } )

  if self:IsAlive() then
   
    if self.ClientAlive2 == false then
    
      -- Show briefing.
      self:ShowBriefing()
      
      -- Callback function.
      if self.ClientCallBack then
        self:T("Calling Callback function")
        self.ClientCallBack( self, unpack( self.ClientParameters ) )
      end
      
      -- Alive.
      self.ClientAlive2 = true
    end
    
  else
  
    if self.ClientAlive2 == true then
      self.ClientAlive2 = false
    end
    
  end
  
  return true
end

--- Return the DCSGroup of a Client.
-- This function is modified to deal with a couple of bugs in DCS 1.5.3
-- @param #CLIENT self
-- @return DCS#Group The group of the Client.
function CLIENT:GetDCSGroup()
  self:F3()

--  local ClientData = Group.getByName( self.ClientName )
--  if ClientData and ClientData:isExist() then
--    self:T( self.ClientName .. " : group found!" )
--    return ClientData
--  else
--    return nil
--  end
  
  local ClientUnit = Unit.getByName( self.ClientName )

  local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) }
  
  for CoalitionId, CoalitionData in pairs( CoalitionsData ) do
    self:T3( { "CoalitionData:", CoalitionData } )
    for UnitId, UnitData in pairs( CoalitionData ) do
      self:T3( { "UnitData:", UnitData } )
      if UnitData and UnitData:isExist() then

        --self:F(self.ClientName)
        if ClientUnit then
        
          local ClientGroup = ClientUnit:getGroup()
          
          if ClientGroup then
            self:T3( "ClientGroup = " .. self.ClientName )
            
            if ClientGroup:isExist() and UnitData:getGroup():isExist() then
             
              if ClientGroup:getID() == UnitData:getGroup():getID() then
                self:T3( "Normal logic" )
                self:T3( self.ClientName .. " : group found!" )
                self.ClientGroupID = ClientGroup:getID()
                self.ClientGroupName = ClientGroup:getName()
                return ClientGroup
              end
              
            else
            
              -- Now we need to resolve the bugs in DCS 1.5 ...
              -- Consult the database for the units of the Client Group. (ClientGroup:getUnits() returns nil)
              self:T3( "Bug 1.5 logic" )
              
              local ClientGroupTemplate = _DATABASE.Templates.Units[self.ClientName].GroupTemplate
              
              self.ClientGroupID = ClientGroupTemplate.groupId
              
              self.ClientGroupName = _DATABASE.Templates.Units[self.ClientName].GroupName
              
              self:T3( self.ClientName .. " : group found in bug 1.5 resolvement logic!" )
              return ClientGroup
              
            end
  --        else
  --          error( "Client " .. self.ClientName .. " not found!" )
          end
        else
          --self:F( { "Client not found!", self.ClientName } )
        end
      end
    end
  end

  -- For non player clients
  if ClientUnit then
    local ClientGroup = ClientUnit:getGroup()
    if ClientGroup then
      self:T3( "ClientGroup = " .. self.ClientName )
      if ClientGroup:isExist() then 
        self:T3( "Normal logic" )
        self:T3( self.ClientName .. " : group found!" )
        return ClientGroup
      end
    end
  end
  
  -- Nothing could be found :(
  self.ClientGroupID = nil
  self.ClientGroupName = nil
  
  return nil
end 


--- Get the group ID of the client.
-- @param #CLIENT self
-- @return #number DCS#Group ID.
function CLIENT:GetClientGroupID()

  -- This updates the ID.
  self:GetDCSGroup()

  return self.ClientGroupID
end


--- Get the name of the group of the client.
-- @param #CLIENT self
-- @return #string
function CLIENT:GetClientGroupName()

  -- This updates the group name.
  self:GetDCSGroup()
  
  return self.ClientGroupName
end

--- Returns the UNIT of the CLIENT.
-- @param #CLIENT self
-- @return Wrapper.Unit#UNIT The client UNIT or `nil`.
function CLIENT:GetClientGroupUnit()
  self:F2()
  
  local ClientDCSUnit = Unit.getByName( self.ClientName )

  self:T( self.ClientDCSUnit )
  
  if ClientDCSUnit and ClientDCSUnit:isExist() then
    local ClientUnit=_DATABASE:FindUnit( self.ClientName )
    return ClientUnit
  end
  
  return nil
end

--- Returns the DCSUnit of the CLIENT.
-- @param #CLIENT self
-- @return DCS#Unit
function CLIENT:GetClientGroupDCSUnit()
  self:F2()

  local ClientDCSUnit = Unit.getByName( self.ClientName )
  
  if ClientDCSUnit and ClientDCSUnit:isExist() then
    self:T2( ClientDCSUnit )
    return ClientDCSUnit
  end
end


--- Evaluates if the CLIENT is a transport.
-- @param #CLIENT self
-- @return #boolean true is a transport.
function CLIENT:IsTransport()
  self:F()
  return self.ClientTransport
end

--- Shows the @{AI.AI_Cargo#CARGO} contained within the CLIENT to the player as a message.
-- The @{AI.AI_Cargo#CARGO} is shown using the @{Core.Message#MESSAGE} distribution system.
-- @param #CLIENT self
function CLIENT:ShowCargo()
  self:F()

  local CargoMsg = ""
  
  for CargoName, Cargo in pairs( CARGOS ) do
    if self == Cargo:IsLoadedInClient() then
      CargoMsg = CargoMsg .. Cargo.CargoName .. " Type:" ..  Cargo.CargoType .. " Weight: " .. Cargo.CargoWeight .. "\n"
    end
  end
  
  if CargoMsg == "" then
    CargoMsg = "empty"
  end
  
  self:Message( CargoMsg, 15, "Co-Pilot: Cargo Status", 30 )

end

--- The main message driver for the CLIENT.
-- This function displays various messages to the Player logged into the CLIENT through the DCS World Messaging system.
-- @param #CLIENT self
-- @param #string Message is the text describing the message.
-- @param #number MessageDuration is the duration in seconds that the Message should be displayed.
-- @param #string MessageCategory is the category of the message (the title).
-- @param #number MessageInterval is the interval in seconds between the display of the @{Core.Message#MESSAGE} when the CLIENT is in the air.
-- @param #string MessageID is the identifier of the message when displayed with intervals.
function CLIENT:Message( Message, MessageDuration, MessageCategory, MessageInterval, MessageID )
  self:F( { Message, MessageDuration, MessageCategory, MessageInterval } )

  if self.MessageSwitch == true then
    if MessageCategory == nil then
      MessageCategory = "Messages"
    end
    if MessageID ~= nil then
      if self.Messages[MessageID] == nil then
        self.Messages[MessageID] = {}
        self.Messages[MessageID].MessageId = MessageID
        self.Messages[MessageID].MessageTime = timer.getTime()
        self.Messages[MessageID].MessageDuration = MessageDuration
        if MessageInterval == nil then
          self.Messages[MessageID].MessageInterval = 600
        else
          self.Messages[MessageID].MessageInterval = MessageInterval
        end
        MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self )
      else
        if self:GetClientGroupDCSUnit() and not self:GetClientGroupDCSUnit():inAir() then
          if timer.getTime() - self.Messages[MessageID].MessageTime >= self.Messages[MessageID].MessageDuration + 10 then
            MESSAGE:New( Message, MessageDuration , MessageCategory):ToClient( self )
            self.Messages[MessageID].MessageTime = timer.getTime()
          end
        else
          if timer.getTime() - self.Messages[MessageID].MessageTime  >= self.Messages[MessageID].MessageDuration + self.Messages[MessageID].MessageInterval then
            MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self )
            self.Messages[MessageID].MessageTime = timer.getTime()
          end
        end
      end
    else
      MESSAGE:New( Message, MessageDuration, MessageCategory ):ToClient( self )
    end
  end
end

--- [Multi-Player Server] Get UCID from a CLIENT.
-- @param #CLIENT self
-- @return #string UCID
function CLIENT:GetUCID()
  local PID = NET.GetPlayerIDByName(nil,self:GetPlayerName())
  return net.get_player_info(tonumber(PID), 'ucid')
end

--- [Multi-Player Server] Return a table of attributes from CLIENT. If optional attribute is present, only that value is returned. 
-- @param #CLIENT self
-- @param #string Attribute (Optional) The attribute to obtain. List see below.
-- @return #table PlayerInfo or nil if it cannot be found
-- @usage
-- Returned table holds these attributes:
--
--          'id'    : player ID
--          'name'  : player name
--          'side'  : 0 - spectators, 1 - red, 2 - blue
--          'slot'  : slot ID of the player or 
--          'ping'  : ping of the player in ms
--          'ipaddr': IP address of the player, SERVER ONLY
--          'ucid'  : Unique Client Identifier, SERVER ONLY
--
function CLIENT:GetPlayerInfo(Attribute)
  local PID = NET.GetPlayerIDByName(nil,self:GetPlayerName())
  if PID then
    return net.get_player_info(tonumber(PID), Attribute)
  else
    return nil
  end
end
--- **Wrapper** - STATIC wraps the DCS StaticObject class.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: **funkyfranky**
-- 
-- ===
-- 
-- @module Wrapper.Static
-- @image Wrapper_Static.JPG


---
-- @type STATIC
-- @extends Wrapper.Positionable#POSITIONABLE

--- Wrapper class to handle Static objects.
-- 
-- Note that Statics are almost the same as Units, but they don't have a controller.
-- The @{Wrapper.Static#STATIC} class is a wrapper class to handle the DCS Static objects:
-- 
--  * Wraps the DCS Static objects.
--  * Support all DCS Static APIs.
--  * Enhance with Static specific APIs not in the DCS API set.
-- 
-- ## STATIC reference methods
-- 
-- For each DCS Static will have a STATIC wrapper object (instance) within the global _DATABASE object (an instance of @{Core.Database#DATABASE}).
-- This is done at the beginning of the mission (when the mission starts).
--  
-- The @{#STATIC} class does not contain a :New() method, rather it provides :Find() methods to retrieve the object reference
-- using the Static Name.
-- 
-- Another thing to know is that STATIC objects do not "contain" the DCS Static object. 
-- The @{#STATIC} methods will reference the DCS Static object by name when it is needed during API execution.
-- If the DCS Static object does not exist or is nil, the STATIC methods will return nil and log an exception in the DCS.log file.
--  
-- The @{#STATIC} class provides the following functions to retrieve quickly the relevant STATIC instance:
-- 
--  * @{#STATIC.FindByName}(): Find a STATIC instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Static name.
--  
-- IMPORTANT: ONE SHOULD NEVER SANITIZE these STATIC OBJECT REFERENCES! (make the STATIC object references nil).
-- 
-- @field #STATIC
STATIC = {
  ClassName = "STATIC",
}


--- Register a static object.
-- @param #STATIC self
-- @param #string StaticName Name of the static object.
-- @return #STATIC self
function STATIC:Register( StaticName )
  local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) )
  self.StaticName = StaticName
  
  local DCSStatic = StaticObject.getByName( self.StaticName )
  if DCSStatic then
    local Life0 = DCSStatic:getLife() or 1
    self.Life0 = Life0
  else
    self:E(string.format("Static object %s does not exist!", tostring(self.StaticName)))
  end
  
  return self
end

--- Get initial life points
-- @param #STATIC self
-- @return #number lifepoints
function STATIC:GetLife0()
  return self.Life0 or 1
end

--- Get current life points
-- @param #STATIC self
-- @return #number lifepoints or nil
function STATIC:GetLife()
  local DCSStatic = StaticObject.getByName( self.StaticName )
  if DCSStatic then
    return DCSStatic:getLife() or 1
  end
  return nil
end

--- Finds a STATIC from the _DATABASE using a DCSStatic object.
-- @param #STATIC self
-- @param DCS#StaticObject DCSStatic An existing DCS Static object reference.
-- @return #STATIC self
function STATIC:Find( DCSStatic )

  local StaticName = DCSStatic:getName()
  local StaticFound = _DATABASE:FindStatic( StaticName )
  return StaticFound
end

--- Finds a STATIC from the _DATABASE using the relevant Static Name.
-- As an optional parameter, a briefing text can be given also.
-- @param #STATIC self
-- @param #string StaticName Name of the DCS **Static** as defined within the Mission Editor.
-- @param #boolean RaiseError Raise an error if not found.
-- @return #STATIC self or *nil*
function STATIC:FindByName( StaticName, RaiseError )

  -- Find static in DB.
  local StaticFound = _DATABASE:FindStatic( StaticName )

  -- Set static name.
  self.StaticName = StaticName
  
  if StaticFound then
    return StaticFound
  end
  
  if RaiseError == nil or RaiseError == true then 
    error( "STATIC not found for: " .. StaticName ) 
  end

  return nil
end

--- Destroys the STATIC.
-- @param #STATIC self
-- @param #boolean GenerateEvent (Optional) true if you want to generate a crash or dead event for the static.
-- @return #nil The DCS StaticObject is not existing or alive.  
-- @usage
-- -- Air static example: destroy the static Helicopter and generate a S_EVENT_CRASH.
-- Helicopter = STATIC:FindByName( "Helicopter" )
-- Helicopter:Destroy( true )
-- 
-- @usage
-- -- Ground static example: destroy the static Tank and generate a S_EVENT_DEAD.
-- Tanks = UNIT:FindByName( "Tank" )
-- Tanks:Destroy( true )
-- 
-- @usage
-- -- Ship static example: destroy the Ship silently.
-- Ship = STATIC:FindByName( "Ship" )
-- Ship:Destroy()
-- 
-- @usage
-- -- Destroy without event generation example.
-- Ship = STATIC:FindByName( "Boat" )
-- Ship:Destroy( false ) -- Don't generate an event upon destruction.
-- 
function STATIC:Destroy( GenerateEvent )
  self:F2( self.ObjectName )

  local DCSObject = self:GetDCSObject()
  
  if DCSObject then
  
    local StaticName = DCSObject:getName()
    self:F( { StaticName = StaticName } )
    
    if GenerateEvent and GenerateEvent == true then
      if self:IsAir() then
        self:CreateEventCrash( timer.getTime(), DCSObject )
      else
        self:CreateEventDead( timer.getTime(), DCSObject )
      end
    elseif GenerateEvent == false then
      -- Do nothing!
    else
      self:CreateEventRemoveUnit( timer.getTime(), DCSObject )
    end
    
    DCSObject:destroy()
    return true
  end

  return nil
end


--- Get DCS object of static of static.
-- @param #STATIC self
-- @return DCS static object
function STATIC:GetDCSObject()
  local DCSStatic = StaticObject.getByName( self.StaticName ) 
  
  if DCSStatic then
    return DCSStatic
  end
    
  return nil
end

--- Returns a list of one @{Wrapper.Static}.
-- @param #STATIC self
-- @return #list<Wrapper.Static#STATIC> A list of one @{Wrapper.Static}.
function STATIC:GetUnits()
  self:F2( { self.StaticName } )
  local DCSStatic = self:GetDCSObject()

  local Statics = {}
  
  if DCSStatic then
    Statics[1] = STATIC:Find( DCSStatic )
    self:T3( Statics )
    return Statics
  end

  return nil
end


--- Get threat level of static.
-- @param #STATIC self
-- @return #number Threat level 1.
-- @return #string "Static"
function STATIC:GetThreatLevel()
  return 1, "Static"
end

--- Spawn the @{Wrapper.Static} at a specific coordinate and heading.
-- @param #STATIC self
-- @param Core.Point#COORDINATE Coordinate The coordinate where to spawn the new Static.
-- @param #number Heading The heading of the static respawn in degrees. Default is 0 deg.
-- @param #number Delay Delay in seconds before the static is spawned.
function STATIC:SpawnAt(Coordinate, Heading, Delay)

  Heading=Heading or 0

  if Delay and Delay>0 then
    SCHEDULER:New(nil, self.SpawnAt, {self, Coordinate, Heading}, Delay)
  else

    local SpawnStatic=SPAWNSTATIC:NewFromStatic(self.StaticName)
  
    SpawnStatic:SpawnFromPointVec2( Coordinate, Heading, self.StaticName )
    
  end
  
  return self
end


--- Respawn the @{Wrapper.Static} at the same location with the same properties.
-- This is useful to respawn a cargo after it has been destroyed.
-- @param #STATIC self
-- @param DCS#country.id CountryID (Optional) The country ID used for spawning the new static. Default is same as currently.
-- @param #number Delay (Optional) Delay in seconds before static is respawned. Default now.
function STATIC:ReSpawn(CountryID, Delay)

  if Delay and Delay>0 then
    SCHEDULER:New(nil, self.ReSpawn, {self, CountryID}, Delay)
  else

    CountryID=CountryID or self:GetCountry()  
   
    local SpawnStatic=SPAWNSTATIC:NewFromStatic(self.StaticName, CountryID)
    
    SpawnStatic:Spawn(nil, self.StaticName)
    
  end
  
  return self
end


--- Respawn the @{Wrapper.Unit} at a defined Coordinate with an optional heading.
-- @param #STATIC self
-- @param Core.Point#COORDINATE Coordinate The coordinate where to spawn the new Static.
-- @param #number Heading (Optional) The heading of the static respawn in degrees. Default the current heading.
-- @param #number Delay (Optional) Delay in seconds before static is respawned. Default now.
function STATIC:ReSpawnAt(Coordinate, Heading, Delay)

  --Heading=Heading or 0

  if Delay and Delay>0 then
    SCHEDULER:New(nil, self.ReSpawnAt, {self, Coordinate, Heading}, Delay)
  else      
      
    local SpawnStatic=SPAWNSTATIC:NewFromStatic(self.StaticName, self:GetCountry())
    
    SpawnStatic:SpawnFromCoordinate(Coordinate, Heading, self.StaticName)
    
  end
  
  return self
end

--- Find the first(!) STATIC matching using patterns. Note that this is **a lot** slower than `:FindByName()`!
-- @param #STATIC self
-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA.
-- @return #STATIC The STATIC.
-- @usage
--          -- Find a static with a partial static name
--          local grp = STATIC:FindByMatching( "Apple" )
--          -- will return e.g. a static named "Apple-1-1"
--
--          -- using a pattern
--          local grp = STATIC:FindByMatching( ".%d.%d$" )
--          -- will return the first static found ending in "-1-1" to "-9-9", but not e.g. "-10-1"
function STATIC:FindByMatching( Pattern )
  local GroupFound = nil

  for name,static in pairs(_DATABASE.STATICS) do
    if string.match(name, Pattern ) then
      GroupFound = static
      break
    end
  end

  return GroupFound
end

--- Find all STATIC objects matching using patterns. Note that this is **a lot** slower than `:FindByName()`!
-- @param #STATIC self
-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA.
-- @return #table Groups Table of matching #STATIC objects found
-- @usage
--          -- Find all static with a partial static name
--          local grptable = STATIC:FindAllByMatching( "Apple" )
--          -- will return all statics with "Apple" in the name
--
--          -- using a pattern
--          local grp = STATIC:FindAllByMatching( ".%d.%d$" )
--          -- will return the all statics found ending in "-1-1" to "-9-9", but not e.g. "-10-1" or "-1-10"
function STATIC:FindAllByMatching( Pattern )
  local GroupsFound = {}

  for name,static in pairs(_DATABASE.STATICS) do
    if string.match(name, Pattern ) then
      GroupsFound[#GroupsFound+1] = static
    end
  end

  return GroupsFound
end

--- Get the Wrapper.Storage#STORAGE object of an static if it is used as cargo and has been set up as storage object.
-- @param #STATIC self
-- @return Wrapper.Storage#STORAGE Storage or `nil` if not fund or set up.
function STATIC:GetStaticStorage()
  local name = self:GetName()
  local storage = STORAGE:NewFromStaticCargo(name)
  return storage
end

--- Get the Cargo Weight of a static object in kgs. Returns -1 if not found.
-- @param #STATIC self
-- @return #number Mass Weight in kgs.
function STATIC:GetCargoWeight()
  local DCSObject = StaticObject.getByName(self.StaticName )
  local mass = -1
  if DCSObject then
     mass = DCSObject:getCargoWeight() or 0
     local masstxt = DCSObject:getCargoDisplayName() or "none"
     --BASE:I("GetCargoWeight "..tostring(mass).." MassText "..masstxt)
  end
  return mass
end
--- **Wrapper** - AIRBASE is a wrapper class to handle the DCS Airbase objects.
--
-- ===
--
-- ### Author: **FlightControl**
--
-- ### Contributions: **funkyfranky**
--
-- ===
--
-- @module Wrapper.Airbase
-- @image Wrapper_Airbase.JPG

---
-- @type AIRBASE
-- @field #string ClassName Name of the class, i.e. "AIRBASE".
-- @field #table CategoryName Names of airbase categories.
-- @field #string AirbaseName Name of the airbase.
-- @field #number AirbaseID Airbase ID.
-- @field Core.Zone#ZONE AirbaseZone Circular zone around the airbase with a radius of 2500 meters. For ships this is a ZONE_UNIT object.
-- @field #number category Airbase category.
-- @field #table descriptors DCS descriptors.
-- @field #boolean isAirdrome Airbase is an airdrome.
-- @field #boolean isHelipad Airbase is a helipad.
-- @field #boolean isShip Airbase is a ship.
-- @field #table parking Parking spot data.
-- @field #table parkingByID Parking spot data table with ID as key.
-- @field #table parkingWhitelist List of parking spot terminal IDs considered for spawning.
-- @field #table parkingBlacklist List of parking spot terminal IDs **not** considered for spawning.
-- @field #table runways Runways of airdromes.
-- @field #AIRBASE.Runway runwayLanding Runway used for landing.
-- @field #AIRBASE.Runway runwayTakeoff Runway used for takeoff.
-- @field Wrapper.Storage#STORAGE storage The DCS warehouse storage.
-- @extends Wrapper.Positionable#POSITIONABLE

--- Wrapper class to handle the DCS Airbase objects:
--
--  * Support all DCS Airbase APIs.
--  * Enhance with Airbase specific APIs not in the DCS Airbase API set.
--
-- ## AIRBASE reference methods
--
-- For each DCS Airbase object alive within a running mission, a AIRBASE wrapper object (instance) will be created within the global _DATABASE object (an instance of @{Core.Database#DATABASE}).
-- This is done at the beginning of the mission (when the mission starts).
--
-- The AIRBASE class **does not contain a :New()** method, rather it provides **:Find()** methods to retrieve the object reference
-- using the DCS Airbase or the DCS AirbaseName.
--
-- Another thing to know is that AIRBASE objects do not "contain" the DCS Airbase object.
-- The AIRBASE methods will reference the DCS Airbase object by name when it is needed during API execution.
-- If the DCS Airbase object does not exist or is nil, the AIRBASE methods will return nil and log an exception in the DCS.log file.
--
-- The AIRBASE class provides the following functions to retrieve quickly the relevant AIRBASE instance:
--
--  * @{#AIRBASE.Find}(): Find a AIRBASE instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Airbase object.
--  * @{#AIRBASE.FindByName}(): Find a AIRBASE instance from the global _DATABASE object (an instance of @{Core.Database#DATABASE}) using a DCS Airbase name.
--
-- IMPORTANT: ONE SHOULD NEVER SANITIZE these AIRBASE OBJECT REFERENCES! (make the AIRBASE object references nil).
--
-- ## DCS Airbase APIs
--
-- The DCS Airbase APIs are used extensively within MOOSE. The AIRBASE class has for each DCS Airbase API a corresponding method.
-- To be able to distinguish easily in your code the difference between a AIRBASE API call and a DCS Airbase API call,
-- the first letter of the method is also capitalized. So, by example, the DCS Airbase method DCSWrapper.Airbase#Airbase.getName()
-- is implemented in the AIRBASE class as @{#AIRBASE.GetName}().
-- 
-- ## Note on the "H" heli pads in the Syria map:
-- 
-- As of the time of writing (Oct 2024, DCS DCS 2.9.8.1107), these 143 objects have the **same name and object ID**, which makes them unusable in Moose, e.g. you cannot find a specific one for spawning etc.
-- Waiting for Ugra and ED to fix this issue.
--
-- @field #AIRBASE AIRBASE
AIRBASE = {
  ClassName = "AIRBASE",
  CategoryName = {
    [Airbase.Category.AIRDROME] = "Airdrome",
    [Airbase.Category.HELIPAD] = "Helipad",
    [Airbase.Category.SHIP] = "Ship",
  },
  activerwyno = nil,
}

--- Enumeration to identify the airbases in the Caucasus region.
--
-- Airbases of the Caucasus map:
--
-- * AIRBASE.Caucasus.Anapa_Vityazevo
-- * AIRBASE.Caucasus.Batumi
-- * AIRBASE.Caucasus.Beslan
-- * AIRBASE.Caucasus.Gelendzhik
-- * AIRBASE.Caucasus.Gudauta
-- * AIRBASE.Caucasus.Kobuleti
-- * AIRBASE.Caucasus.Krasnodar_Center
-- * AIRBASE.Caucasus.Krasnodar_Pashkovsky
-- * AIRBASE.Caucasus.Krymsk
-- * AIRBASE.Caucasus.Kutaisi
-- * AIRBASE.Caucasus.Maykop_Khanskaya
-- * AIRBASE.Caucasus.Mineralnye_Vody
-- * AIRBASE.Caucasus.Mozdok
-- * AIRBASE.Caucasus.Nalchik
-- * AIRBASE.Caucasus.Novorossiysk
-- * AIRBASE.Caucasus.Senaki_Kolkhi
-- * AIRBASE.Caucasus.Sochi_Adler
-- * AIRBASE.Caucasus.Soganlug
-- * AIRBASE.Caucasus.Sukhumi_Babushara
-- * AIRBASE.Caucasus.Tbilisi_Lochini
-- * AIRBASE.Caucasus.Vaziani
--
-- @field Caucasus
AIRBASE.Caucasus = {
  ["Anapa_Vityazevo"] = "Anapa-Vityazevo",
  ["Batumi"] = "Batumi",
  ["Beslan"] = "Beslan",
  ["Gelendzhik"] = "Gelendzhik",
  ["Gudauta"] = "Gudauta",
  ["Kobuleti"] = "Kobuleti",
  ["Krasnodar_Center"] = "Krasnodar-Center",
  ["Krasnodar_Pashkovsky"] = "Krasnodar-Pashkovsky",
  ["Krymsk"] = "Krymsk",
  ["Kutaisi"] = "Kutaisi",
  ["Maykop_Khanskaya"] = "Maykop-Khanskaya",
  ["Mineralnye_Vody"] = "Mineralnye Vody",
  ["Mozdok"] = "Mozdok",
  ["Nalchik"] = "Nalchik",
  ["Novorossiysk"] = "Novorossiysk",
  ["Senaki_Kolkhi"] = "Senaki-Kolkhi",
  ["Sochi_Adler"] = "Sochi-Adler",
  ["Soganlug"] = "Soganlug",
  ["Sukhumi_Babushara"] = "Sukhumi-Babushara",
  ["Tbilisi_Lochini"] = "Tbilisi-Lochini",
  ["Vaziani"] = "Vaziani",
}

--- Airbases of the Nevada map:
--
-- * AIRBASE.Nevada.Beatty
-- * AIRBASE.Nevada.Boulder_City
-- * AIRBASE.Nevada.Creech
-- * AIRBASE.Nevada.Echo_Bay
-- * AIRBASE.Nevada.Groom_Lake
-- * AIRBASE.Nevada.Henderson_Executive
-- * AIRBASE.Nevada.Jean
-- * AIRBASE.Nevada.Laughlin
-- * AIRBASE.Nevada.Lincoln_County
-- * AIRBASE.Nevada.McCarran_International
-- * AIRBASE.Nevada.Mesquite
-- * AIRBASE.Nevada.Mina
-- * AIRBASE.Nevada.Nellis
-- * AIRBASE.Nevada.North_Las_Vegas
-- * AIRBASE.Nevada.Pahute_Mesa
-- * AIRBASE.Nevada.Tonopah
-- * AIRBASE.Nevada.Tonopah_Test_Range
--
-- @field Nevada
AIRBASE.Nevada = {
  ["Beatty"] = "Beatty",
  ["Boulder_City"] = "Boulder City",
  ["Creech"] = "Creech",
  ["Echo_Bay"] = "Echo Bay",
  ["Groom_Lake"] = "Groom Lake",
  ["Henderson_Executive"] = "Henderson Executive",
  ["Jean"] = "Jean",
  ["Laughlin"] = "Laughlin",
  ["Lincoln_County"] = "Lincoln County",
  ["McCarran_International"] = "McCarran International",
  ["Mesquite"] = "Mesquite",
  ["Mina"] = "Mina",
  ["Nellis"] = "Nellis",
  ["North_Las_Vegas"] = "North Las Vegas",
  ["Pahute_Mesa"] = "Pahute Mesa",
  ["Tonopah"] = "Tonopah",
  ["Tonopah_Test_Range"] = "Tonopah Test Range",
}

--- Airbases of the Normandy map:
--
-- * AIRBASE.Normandy.Abbeville_Drucat
-- * AIRBASE.Normandy.Amiens_Glisy
-- * AIRBASE.Normandy.Argentan
-- * AIRBASE.Normandy.Avranches_Le_Val_Saint_Pere
-- * AIRBASE.Normandy.Azeville
-- * AIRBASE.Normandy.Barville
-- * AIRBASE.Normandy.Bazenville
-- * AIRBASE.Normandy.Beaumont_le_Roger
-- * AIRBASE.Normandy.Beauvais_Tille
-- * AIRBASE.Normandy.Beny_sur_Mer
-- * AIRBASE.Normandy.Bernay_Saint_Martin
-- * AIRBASE.Normandy.Beuzeville
-- * AIRBASE.Normandy.Biggin_Hill
-- * AIRBASE.Normandy.Biniville
-- * AIRBASE.Normandy.Broglie
-- * AIRBASE.Normandy.Brucheville
-- * AIRBASE.Normandy.Cardonville
-- * AIRBASE.Normandy.Carpiquet
-- * AIRBASE.Normandy.Chailey
-- * AIRBASE.Normandy.Chippelle
-- * AIRBASE.Normandy.Conches
-- * AIRBASE.Normandy.Cormeilles_en_Vexin
-- * AIRBASE.Normandy.Creil
-- * AIRBASE.Normandy.Cretteville
-- * AIRBASE.Normandy.Cricqueville_en_Bessin
-- * AIRBASE.Normandy.Deanland
-- * AIRBASE.Normandy.Deauville
-- * AIRBASE.Normandy.Detling
-- * AIRBASE.Normandy.Deux_Jumeaux
-- * AIRBASE.Normandy.Dinan_Trelivan
-- * AIRBASE.Normandy.Dunkirk_Mardyck
-- * AIRBASE.Normandy.Essay
-- * AIRBASE.Normandy.Evreux
-- * AIRBASE.Normandy.Farnborough
-- * AIRBASE.Normandy.Fecamp_Benouville
-- * AIRBASE.Normandy.Flers
-- * AIRBASE.Normandy.Ford
-- * AIRBASE.Normandy.Friston
-- * AIRBASE.Normandy.Funtington
-- * AIRBASE.Normandy.Goulet
-- * AIRBASE.Normandy.Gravesend
-- * AIRBASE.Normandy.Guyancourt
-- * AIRBASE.Normandy.Hauterive
-- * AIRBASE.Normandy.Heathrow
-- * AIRBASE.Normandy.High_Halden
-- * AIRBASE.Normandy.Kenley
-- * AIRBASE.Normandy.Lantheuil
-- * AIRBASE.Normandy.Le_Molay
-- * AIRBASE.Normandy.Lessay
-- * AIRBASE.Normandy.Lignerolles
-- * AIRBASE.Normandy.Longues_sur_Mer
-- * AIRBASE.Normandy.Lonrai
-- * AIRBASE.Normandy.Lymington
-- * AIRBASE.Normandy.Lympne
-- * AIRBASE.Normandy.Manston
-- * AIRBASE.Normandy.Maupertus
-- * AIRBASE.Normandy.Meautis
-- * AIRBASE.Normandy.Merville_Calonne
-- * AIRBASE.Normandy.Needs_Oar_Point
-- * AIRBASE.Normandy.Odiham
-- * AIRBASE.Normandy.Orly
-- * AIRBASE.Normandy.Picauville
-- * AIRBASE.Normandy.Poix
-- * AIRBASE.Normandy.Ronai
-- * AIRBASE.Normandy.Rouen_Boos
-- * AIRBASE.Normandy.Rucqueville
-- * AIRBASE.Normandy.Saint_Andre_de_lEure
-- * AIRBASE.Normandy.Saint_Aubin
-- * AIRBASE.Normandy.Saint_Omer_Wizernes
-- * AIRBASE.Normandy.Saint_Pierre_du_Mont
-- * AIRBASE.Normandy.Sainte_Croix_sur_Mer
-- * AIRBASE.Normandy.Sainte_Laurent_sur_Mer
-- * AIRBASE.Normandy.Sommervieu
-- * AIRBASE.Normandy.Stoney_Cross
-- * AIRBASE.Normandy.Tangmere
-- * AIRBASE.Normandy.Triqueville
-- * AIRBASE.Normandy.Villacoublay
-- * AIRBASE.Normandy.Vrigny
-- * AIRBASE.Normandy.West_Malling
-- * AIRBASE.Normandy.Eastchurch
-- * AIRBASE.Normandy.Headcorn
-- * AIRBASE.Normandy.Hawkinge
--
-- @field Normandy
AIRBASE.Normandy = {
  ["Abbeville_Drucat"] = "Abbeville Drucat",
  ["Amiens_Glisy"] = "Amiens-Glisy",
  ["Argentan"] = "Argentan",
  ["Avranches_Le_Val_Saint_Pere"] = "Avranches Le Val-Saint-Pere",
  ["Azeville"] = "Azeville",
  ["Barville"] = "Barville",
  ["Bazenville"] = "Bazenville",
  ["Beaumont_le_Roger"] = "Beaumont-le-Roger",
  ["Beauvais_Tille"] = "Beauvais-Tille",
  ["Beny_sur_Mer"] = "Beny-sur-Mer",
  ["Bernay_Saint_Martin"] = "Bernay Saint Martin",
  ["Beuzeville"] = "Beuzeville",
  ["Biggin_Hill"] = "Biggin Hill",
  ["Biniville"] = "Biniville",
  ["Broglie"] = "Broglie",
  ["Brucheville"] = "Brucheville",
  ["Cardonville"] = "Cardonville",
  ["Carpiquet"] = "Carpiquet",
  ["Chailey"] = "Chailey",
  ["Chippelle"] = "Chippelle",
  ["Conches"] = "Conches",
  ["Cormeilles_en_Vexin"] = "Cormeilles-en-Vexin",
  ["Creil"] = "Creil",
  ["Cretteville"] = "Cretteville",
  ["Cricqueville_en_Bessin"] = "Cricqueville-en-Bessin",
  ["Deanland"] = "Deanland",
  ["Deauville"] = "Deauville",
  ["Detling"] = "Detling",
  ["Deux_Jumeaux"] = "Deux Jumeaux",
  ["Dinan_Trelivan"] = "Dinan-Trelivan",
  ["Dunkirk_Mardyck"] = "Dunkirk-Mardyck",
  ["Essay"] = "Essay",
  ["Evreux"] = "Evreux",
  ["Farnborough"] = "Farnborough",
  ["Fecamp_Benouville"] = "Fecamp-Benouville",
  ["Flers"] = "Flers",
  ["Ford"] = "Ford",
  ["Friston"] = "Friston",
  ["Funtington"] = "Funtington",
  ["Goulet"] = "Goulet",
  ["Gravesend"] = "Gravesend",
  ["Guyancourt"] = "Guyancourt",
  ["Hauterive"] = "Hauterive",
  ["Heathrow"] = "Heathrow",
  ["High_Halden"] = "High Halden",
  ["Kenley"] = "Kenley",
  ["Lantheuil"] = "Lantheuil",
  ["Le_Molay"] = "Le Molay",
  ["Lessay"] = "Lessay",
  ["Lignerolles"] = "Lignerolles",
  ["Longues_sur_Mer"] = "Longues-sur-Mer",
  ["Lonrai"] = "Lonrai",
  ["Lymington"] = "Lymington",
  ["Lympne"] = "Lympne",
  ["Manston"] = "Manston",
  ["Maupertus"] = "Maupertus",
  ["Meautis"] = "Meautis",
  ["Merville_Calonne"] = "Merville Calonne",
  ["Needs_Oar_Point"] = "Needs Oar Point",
  ["Odiham"] = "Odiham",
  ["Orly"] = "Orly",
  ["Picauville"] = "Picauville",
  ["Poix"] = "Poix",
  ["Ronai"] = "Ronai",
  ["Rouen_Boos"] = "Rouen-Boos",
  ["Rucqueville"] = "Rucqueville",
  ["Saint_Andre_de_lEure"] = "Saint-Andre-de-lEure",
  ["Saint_Aubin"] = "Saint-Aubin",
  ["Saint_Omer_Wizernes"] = "Saint-Omer Wizernes",
  ["Saint_Pierre_du_Mont"] = "Saint Pierre du Mont",
  ["Sainte_Croix_sur_Mer"] = "Sainte-Croix-sur-Mer",
  ["Sainte_Laurent_sur_Mer"] = "Sainte-Laurent-sur-Mer",
  ["Sommervieu"] = "Sommervieu",
  ["Stoney_Cross"] = "Stoney Cross",
  ["Tangmere"] = "Tangmere",
  ["Triqueville"] = "Triqueville",
  ["Villacoublay"] = "Villacoublay",
  ["Vrigny"] = "Vrigny",
  ["West_Malling"] = "West Malling",
  ["Eastchurch"] = "Eastchurch",
  ["Headcorn"] = "Headcorn",
  ["Hawkinge"] = "Hawkinge",
}

--- Airbases of the Persion Gulf Map:
--
-- * AIRBASE.PersianGulf.Abu_Dhabi_Intl
-- * AIRBASE.PersianGulf.Abu_Musa_Island
-- * AIRBASE.PersianGulf.Al_Ain_Intl
-- * AIRBASE.PersianGulf.Al_Bateen
-- * AIRBASE.PersianGulf.Al_Dhafra_AFB
-- * AIRBASE.PersianGulf.Al_Maktoum_Intl
-- * AIRBASE.PersianGulf.Al_Minhad_AFB
-- * AIRBASE.PersianGulf.Bandar_Abbas_Intl
-- * AIRBASE.PersianGulf.Bandar_Lengeh
-- * AIRBASE.PersianGulf.Bandar_e_Jask
-- * AIRBASE.PersianGulf.Dubai_Intl
-- * AIRBASE.PersianGulf.Fujairah_Intl
-- * AIRBASE.PersianGulf.Havadarya
-- * AIRBASE.PersianGulf.Jiroft
-- * AIRBASE.PersianGulf.Kerman
-- * AIRBASE.PersianGulf.Khasab
-- * AIRBASE.PersianGulf.Kish_Intl
-- * AIRBASE.PersianGulf.Lar
-- * AIRBASE.PersianGulf.Lavan_Island
-- * AIRBASE.PersianGulf.Liwa_AFB
-- * AIRBASE.PersianGulf.Qeshm_Island
-- * AIRBASE.PersianGulf.Quasoura_airport
-- * AIRBASE.PersianGulf.Ras_Al_Khaimah_Intl
-- * AIRBASE.PersianGulf.Sas_Al_Nakheel
-- * AIRBASE.PersianGulf.Sharjah_Intl
-- * AIRBASE.PersianGulf.Shiraz_Intl
-- * AIRBASE.PersianGulf.Sir_Abu_Nuayr
-- * AIRBASE.PersianGulf.Sirri_Island
-- * AIRBASE.PersianGulf.Tunb_Island_AFB
-- * AIRBASE.PersianGulf.Tunb_Kochak
--
-- @field PersianGulf
AIRBASE.PersianGulf = {
  ["Abu_Dhabi_Intl"] = "Abu Dhabi Intl",
  ["Abu_Musa_Island"] = "Abu Musa Island",
  ["Al_Ain_Intl"] = "Al Ain Intl",
  ["Al_Bateen"] = "Al-Bateen",
  ["Al_Dhafra_AFB"] = "Al Dhafra AFB",
  ["Al_Maktoum_Intl"] = "Al Maktoum Intl",
  ["Al_Minhad_AFB"] = "Al Minhad AFB",
  ["Bandar_Abbas_Intl"] = "Bandar Abbas Intl",
  ["Bandar_Lengeh"] = "Bandar Lengeh",
  ["Bandar_e_Jask"] = "Bandar-e-Jask",
  ["Dubai_Intl"] = "Dubai Intl",
  ["Fujairah_Intl"] = "Fujairah Intl",
  ["Havadarya"] = "Havadarya",
  ["Jiroft"] = "Jiroft",
  ["Kerman"] = "Kerman",
  ["Khasab"] = "Khasab",
  ["Kish_Intl"] = "Kish Intl",
  ["Lar"] = "Lar",
  ["Lavan_Island"] = "Lavan Island",
  ["Liwa_AFB"] = "Liwa AFB",
  ["Qeshm_Island"] = "Qeshm Island",
  ["Quasoura_airport"] = "Quasoura_airport",
  ["Ras_Al_Khaimah_Intl"] = "Ras Al Khaimah Intl",
  ["Sas_Al_Nakheel"] = "Sas Al Nakheel",
  ["Sharjah_Intl"] = "Sharjah Intl",
  ["Shiraz_Intl"] = "Shiraz Intl",
  ["Sir_Abu_Nuayr"] = "Sir Abu Nuayr",
  ["Sirri_Island"] = "Sirri Island",
  ["Tunb_Island_AFB"] = "Tunb Island AFB",
  ["Tunb_Kochak"] = "Tunb Kochak",
}

--- Airbases of The Channel Map:
--
-- * AIRBASE.TheChannel.Abbeville_Drucat
-- * AIRBASE.TheChannel.Biggin_Hill
-- * AIRBASE.TheChannel.Detling
-- * AIRBASE.TheChannel.Dunkirk_Mardyck
-- * AIRBASE.TheChannel.Eastchurch
-- * AIRBASE.TheChannel.Hawkinge
-- * AIRBASE.TheChannel.Headcorn
-- * AIRBASE.TheChannel.High_Halden
-- * AIRBASE.TheChannel.Lympne
-- * AIRBASE.TheChannel.Manston
-- * AIRBASE.TheChannel.Merville_Calonne
-- * AIRBASE.TheChannel.Saint_Omer_Longuenesse
--
-- @field TheChannel
AIRBASE.TheChannel = {
  ["Abbeville_Drucat"] = "Abbeville Drucat",
  ["Biggin_Hill"] = "Biggin Hill",
  ["Detling"] = "Detling",
  ["Dunkirk_Mardyck"] = "Dunkirk Mardyck",
  ["Eastchurch"] = "Eastchurch",
  ["Hawkinge"] = "Hawkinge",
  ["Headcorn"] = "Headcorn",
  ["High_Halden"] = "High Halden",
  ["Lympne"] = "Lympne",
  ["Manston"] = "Manston",
  ["Merville_Calonne"] = "Merville Calonne",
  ["Saint_Omer_Longuenesse"] = "Saint Omer Longuenesse",
}

--- Airbases of the Syria map:
--
-- * AIRBASE.Syria.Abu_al_Duhur
-- * AIRBASE.Syria.Adana_Sakirpasa
-- * AIRBASE.Syria.Akrotiri
-- * AIRBASE.Syria.Al_Dumayr
-- * AIRBASE.Syria.Al_Qusayr
-- * AIRBASE.Syria.Aleppo
-- * AIRBASE.Syria.An_Nasiriyah
-- * AIRBASE.Syria.At_Tanf
-- * AIRBASE.Syria.Bassel_Al_Assad
-- * AIRBASE.Syria.Beirut_Rafic_Hariri
-- * AIRBASE.Syria.Damascus
-- * AIRBASE.Syria.Deir_ez_Zor
-- * AIRBASE.Syria.Ercan
-- * AIRBASE.Syria.Eyn_Shemer
-- * AIRBASE.Syria.Gaziantep
-- * AIRBASE.Syria.Gazipasa
-- * AIRBASE.Syria.Gecitkale
-- * AIRBASE.Syria.H
-- * AIRBASE.Syria.H3
-- * AIRBASE.Syria.H3_Northwest
-- * AIRBASE.Syria.H3_Southwest
-- * AIRBASE.Syria.H4
-- * AIRBASE.Syria.Haifa
-- * AIRBASE.Syria.Hama
-- * AIRBASE.Syria.Hatay
-- * AIRBASE.Syria.Herzliya
-- * AIRBASE.Syria.Incirlik
-- * AIRBASE.Syria.Jirah
-- * AIRBASE.Syria.Khalkhalah
-- * AIRBASE.Syria.Kharab_Ishk
-- * AIRBASE.Syria.King_Abdullah_II
-- * AIRBASE.Syria.King_Hussein_Air_College
-- * AIRBASE.Syria.Kingsfield
-- * AIRBASE.Syria.Kiryat_Shmona
-- * AIRBASE.Syria.Kuweires
-- * AIRBASE.Syria.Lakatamia
-- * AIRBASE.Syria.Larnaca
-- * AIRBASE.Syria.Marj_Ruhayyil
-- * AIRBASE.Syria.Marj_as_Sultan_North
-- * AIRBASE.Syria.Marj_as_Sultan_South
-- * AIRBASE.Syria.Megiddo
-- * AIRBASE.Syria.Mezzeh
-- * AIRBASE.Syria.Minakh
-- * AIRBASE.Syria.Muwaffaq_Salti
-- * AIRBASE.Syria.Naqoura
-- * AIRBASE.Syria.Nicosia
-- * AIRBASE.Syria.Palmyra
-- * AIRBASE.Syria.Paphos
-- * AIRBASE.Syria.Pinarbashi
-- * AIRBASE.Syria.Prince_Hassan
-- * AIRBASE.Syria.Qabr_as_Sitt
-- * AIRBASE.Syria.Ramat_David
-- * AIRBASE.Syria.Rayak
-- * AIRBASE.Syria.Rene_Mouawad
-- * AIRBASE.Syria.Rosh_Pina
-- * AIRBASE.Syria.Ruwayshid
-- * AIRBASE.Syria.Sanliurfa
-- * AIRBASE.Syria.Sayqal
-- * AIRBASE.Syria.Shayrat
-- * AIRBASE.Syria.Tabqa
-- * AIRBASE.Syria.Taftanaz
-- * AIRBASE.Syria.Tal_Siman
-- * AIRBASE.Syria.Tha_lah
-- * AIRBASE.Syria.Tiyas
-- * AIRBASE.Syria.Wujah_Al_Hajar
-- * AIRBASE.Syria.Ben_Gurion 
-- * AIRBASE.Syria.Hatzor
-- * AIRBASE.Syria.Palmachim
-- * AIRBASE.Syria.Tel_Nof
-- * AIRBASE.Syria.Marka
--
--@field Syria
AIRBASE.Syria={
  ["Abu_al_Duhur"] = "Abu al-Duhur",
  ["Adana_Sakirpasa"] = "Adana Sakirpasa",
  ["Akrotiri"] = "Akrotiri",
  ["Al_Dumayr"] = "Al-Dumayr",
  ["Al_Qusayr"] = "Al Qusayr",
  ["Aleppo"] = "Aleppo",
  ["An_Nasiriyah"] = "An Nasiriyah",
  ["At_Tanf"] = "At Tanf",
  ["Bassel_Al_Assad"] = "Bassel Al-Assad",
  ["Beirut_Rafic_Hariri"] = "Beirut-Rafic Hariri",
  ["Damascus"] = "Damascus",
  ["Deir_ez_Zor"] = "Deir ez-Zor",
  ["Ercan"] = "Ercan",
  ["Eyn_Shemer"] = "Eyn Shemer",
  ["Gaziantep"] = "Gaziantep",
  ["Gazipasa"] = "Gazipasa",
  ["Gecitkale"] = "Gecitkale",
  ["H"] = "H",
  ["H3"] = "H3",
  ["H3_Northwest"] = "H3 Northwest",
  ["H3_Southwest"] = "H3 Southwest",
  ["H4"] = "H4",
  ["Haifa"] = "Haifa",
  ["Hama"] = "Hama",
  ["Hatay"] = "Hatay",
  ["Herzliya"] = "Herzliya",
  ["Incirlik"] = "Incirlik",
  ["Jirah"] = "Jirah",
  ["Khalkhalah"] = "Khalkhalah",
  ["Kharab_Ishk"] = "Kharab Ishk",
  ["King_Abdullah_II"] = "King Abdullah II",
  ["King_Hussein_Air_College"] = "King Hussein Air College",
  ["Kingsfield"] = "Kingsfield",
  ["Kiryat_Shmona"] = "Kiryat Shmona",
  ["Kuweires"] = "Kuweires",
  ["Lakatamia"] = "Lakatamia",
  ["Larnaca"] = "Larnaca",
  ["Marka"] = "Marka",
  ["Marj_Ruhayyil"] = "Marj Ruhayyil",
  ["Marj_as_Sultan_North"] = "Marj as Sultan North",
  ["Marj_as_Sultan_South"] = "Marj as Sultan South",
  ["Megiddo"] = "Megiddo",
  ["Mezzeh"] = "Mezzeh",
  ["Minakh"] = "Minakh",
  ["Muwaffaq_Salti"] = "Muwaffaq Salti",
  ["Naqoura"] = "Naqoura",
  ["Nicosia"] = "Nicosia",
  ["Palmyra"] = "Palmyra",
  ["Paphos"] = "Paphos",
  ["Pinarbashi"] = "Pinarbashi",
  ["Prince_Hassan"] = "Prince Hassan",
  ["Qabr_as_Sitt"] = "Qabr as Sitt",
  ["Ramat_David"] = "Ramat David",
  ["Rayak"] = "Rayak",
  ["Rene_Mouawad"] = "Rene Mouawad",
  ["Rosh_Pina"] = "Rosh Pina",
  ["Ruwayshid"] = "Ruwayshid",
  ["Sanliurfa"] = "Sanliurfa",
  ["Sayqal"] = "Sayqal",
  ["Shayrat"] = "Shayrat",
  ["Tabqa"] = "Tabqa",
  ["Taftanaz"] = "Taftanaz",
  ["Tal_Siman"] = "Tal Siman",
  ["Tha_lah"] = "Tha'lah",
  ["Tiyas"] = "Tiyas",
  ["Wujah_Al_Hajar"] = "Wujah Al Hajar",
  ["Ben_Gurion"] = "Ben Gurion",
  ["Hatzor"] = "Hatzor",
  ["Palmachim"] = "Palmachim",
  ["Tel_Nof"] = "Tel Nof",
}

--- Airbases of the Mariana Islands map:
--
-- * AIRBASE.MarianaIslands.Andersen_AFB
-- * AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl
-- * AIRBASE.MarianaIslands.North_West_Field
-- * AIRBASE.MarianaIslands.Olf_Orote
-- * AIRBASE.MarianaIslands.Pagan_Airstrip
-- * AIRBASE.MarianaIslands.Rota_Intl
-- * AIRBASE.MarianaIslands.Saipan_Intl
-- * AIRBASE.MarianaIslands.Tinian_Intl
--
-- @field MarianaIslands
AIRBASE.MarianaIslands = {
  ["Andersen_AFB"] = "Andersen AFB",
  ["Antonio_B_Won_Pat_Intl"] = "Antonio B. Won Pat Intl",
  ["North_West_Field"] = "North West Field",
  ["Olf_Orote"] = "Olf Orote",
  ["Pagan_Airstrip"] = "Pagan Airstrip",
  ["Rota_Intl"] = "Rota Intl",
  ["Saipan_Intl"] = "Saipan Intl",
  ["Tinian_Intl"] = "Tinian Intl",
}

--- Airbase of the Marianas WWII map
--
-- * AIRBASE.MarianaIslandsWWII.Agana
-- * AIRBASE.MarianaIslandsWWII.Airfield_3
-- * AIRBASE.MarianaIslandsWWII.Charon_Kanoa
-- * AIRBASE.MarianaIslandsWWII.Gurguan_Point
-- * AIRBASE.MarianaIslandsWWII.Isley
-- * AIRBASE.MarianaIslandsWWII.Kagman
-- * AIRBASE.MarianaIslandsWWII.Marpi
-- * AIRBASE.MarianaIslandsWWII.Orote
-- * AIRBASE.MarianaIslandsWWII.Pagan
-- * AIRBASE.MarianaIslandsWWII.Rota
-- * AIRBASE.MarianaIslandsWWII.Ushi
-- @field AIRBASE.MarianaIslandsWWII
AIRBASE.MarianaIslandsWWII = 
{
  ["Agana"] = "Agana",
  ["Airfield_3"] = "Airfield 3",
  ["Charon_Kanoa"] = "Charon Kanoa",
  ["Gurguan_Point"] = "Gurguan Point",
  ["Isley"] = "Isley",
  ["Kagman"] = "Kagman",
  ["Marpi"] = "Marpi",
  ["Orote"] = "Orote",
  ["Pagan"] = "Pagan",
  ["Rota"] = "Rota",
  ["Ushi"] = "Ushi",
}

--- Airbases of the South Atlantic map:
--
-- * AIRBASE.SouthAtlantic.Almirante_Schroeders
-- * AIRBASE.SouthAtlantic.Comandante_Luis_Piedrabuena
-- * AIRBASE.SouthAtlantic.Cullen
-- * AIRBASE.SouthAtlantic.El_Calafate
-- * AIRBASE.SouthAtlantic.Franco_Bianco
-- * AIRBASE.SouthAtlantic.Gobernador_Gregores
-- * AIRBASE.SouthAtlantic.Goose_Green
-- * AIRBASE.SouthAtlantic.Gull_Point
-- * AIRBASE.SouthAtlantic.Hipico_Flying_Club
-- * AIRBASE.SouthAtlantic.Mount_Pleasant
-- * AIRBASE.SouthAtlantic.O_Higgins
-- * AIRBASE.SouthAtlantic.Pampa_Guanaco
-- * AIRBASE.SouthAtlantic.Port_Stanley
-- * AIRBASE.SouthAtlantic.Porvenir
-- * AIRBASE.SouthAtlantic.Puerto_Natales
-- * AIRBASE.SouthAtlantic.Puerto_Santa_Cruz
-- * AIRBASE.SouthAtlantic.Puerto_Williams
-- * AIRBASE.SouthAtlantic.Punta_Arenas
-- * AIRBASE.SouthAtlantic.Rio_Chico
-- * AIRBASE.SouthAtlantic.Rio_Gallegos
-- * AIRBASE.SouthAtlantic.Rio_Grande
-- * AIRBASE.SouthAtlantic.Rio_Turbio
-- * AIRBASE.SouthAtlantic.San_Carlos_FOB
-- * AIRBASE.SouthAtlantic.San_Julian
-- * AIRBASE.SouthAtlantic.Tolhuin
-- * AIRBASE.SouthAtlantic.Ushuaia
-- * AIRBASE.SouthAtlantic.Ushuaia_Helo_Port
--
--@field SouthAtlantic
AIRBASE.SouthAtlantic={
  ["Almirante_Schroeders"] = "Almirante Schroeders",
  ["Comandante_Luis_Piedrabuena"] = "Comandante Luis Piedrabuena",
  ["Cullen"] = "Cullen",
  ["El_Calafate"] = "El Calafate",
  ["Franco_Bianco"] = "Franco Bianco",
  ["Gobernador_Gregores"] = "Gobernador Gregores",
  ["Goose_Green"] = "Goose Green",
  ["Gull_Point"] = "Gull Point",
  ["Hipico_Flying_Club"] = "Hipico Flying Club",
  ["Mount_Pleasant"] = "Mount Pleasant",
  ["O_Higgins"] = "O'Higgins",
  ["Pampa_Guanaco"] = "Pampa Guanaco",
  ["Port_Stanley"] = "Port Stanley",
  ["Porvenir"] = "Porvenir",
  ["Puerto_Natales"] = "Puerto Natales",
  ["Puerto_Santa_Cruz"] = "Puerto Santa Cruz",
  ["Puerto_Williams"] = "Puerto Williams",
  ["Punta_Arenas"] = "Punta Arenas",
  ["Rio_Chico"] = "Rio Chico",
  ["Rio_Gallegos"] = "Rio Gallegos",
  ["Rio_Grande"] = "Rio Grande",
  ["Rio_Turbio"] = "Rio Turbio",
  ["San_Carlos_FOB"] = "San Carlos FOB",
  ["San_Julian"] = "San Julian",
  ["Tolhuin"] = "Tolhuin",
  ["Ushuaia"] = "Ushuaia",
  ["Ushuaia_Helo_Port"] = "Ushuaia Helo Port",
}

--- Airbases of the Sinai map:
--
-- * AIRBASE.SinaiMap.Abu_Rudeis
-- * AIRBASE.SinaiMap.Abu_Suwayr
-- * AIRBASE.SinaiMap.Al_Bahr_al_Ahmar
-- * AIRBASE.SinaiMap.Al_Ismailiyah
-- * AIRBASE.SinaiMap.Al_Khatatbah
-- * AIRBASE.SinaiMap.Al_Mansurah
-- * AIRBASE.SinaiMap.Al_Rahmaniyah_Air_Base
-- * AIRBASE.SinaiMap.As_Salihiyah
-- * AIRBASE.SinaiMap.AzZaqaziq
-- * AIRBASE.SinaiMap.Baluza
-- * AIRBASE.SinaiMap.Ben_Gurion
-- * AIRBASE.SinaiMap.Beni_Suef
-- * AIRBASE.SinaiMap.Bilbeis_Air_Base
-- * AIRBASE.SinaiMap.Bir_Hasanah
-- * AIRBASE.SinaiMap.Birma_Air_Base
-- * AIRBASE.SinaiMap.Borg_El_Arab_International_Airport
-- * AIRBASE.SinaiMap.Cairo_International_Airport
-- * AIRBASE.SinaiMap.Cairo_West
-- * AIRBASE.SinaiMap.Damascus_Intl
-- * AIRBASE.SinaiMap.Difarsuwar_Airfield
-- * AIRBASE.SinaiMap.El_Arish
-- * AIRBASE.SinaiMap.El_Gora
-- * AIRBASE.SinaiMap.El_Minya
-- * AIRBASE.SinaiMap.Fayed
-- * AIRBASE.SinaiMap.Gebel_El_Basur_Air_Base
-- * AIRBASE.SinaiMap.Hatzerim
-- * AIRBASE.SinaiMap.Hatzor
-- * AIRBASE.SinaiMap.Hurghada_International_Airport
-- * AIRBASE.SinaiMap.Inshas_Airbase
-- * AIRBASE.SinaiMap.Jiyanklis_Air_Base
-- * AIRBASE.SinaiMap.Kedem
-- * AIRBASE.SinaiMap.Kibrit_Air_Base
-- * AIRBASE.SinaiMap.Kom_Awshim
-- * AIRBASE.SinaiMap.Melez
-- * AIRBASE.SinaiMap.Mezzeh_Air_Base
-- * AIRBASE.SinaiMap.Nevatim
-- * AIRBASE.SinaiMap.Ovda
-- * AIRBASE.SinaiMap.Palmachim
-- * AIRBASE.SinaiMap.Quwaysina
-- * AIRBASE.SinaiMap.Rafic_Hariri_Intl
-- * AIRBASE.SinaiMap.Ramat_David
-- * AIRBASE.SinaiMap.Ramon_Airbase
-- * AIRBASE.SinaiMap.Ramon_International_Airport
-- * AIRBASE.SinaiMap.Sde_Dov
-- * AIRBASE.SinaiMap.Sharm_El_Sheikh_International_Airport
-- * AIRBASE.SinaiMap.St_Catherine
-- * AIRBASE.SinaiMap.Tabuk
-- * AIRBASE.SinaiMap.Tel_Nof
-- * AIRBASE.SinaiMap.Wadi_Abu_Rish
-- * AIRBASE.SinaiMap.Wadi_al_Jandali
--
-- @field Sinai
AIRBASE.Sinai = {
  ["Abu_Rudeis"] = "Abu Rudeis",
  ["Abu_Suwayr"] = "Abu Suwayr",
  ["Al_Bahr_al_Ahmar"] = "Al Bahr al Ahmar",
  ["Al_Ismailiyah"] = "Al Ismailiyah",
  ["Al_Khatatbah"] = "Al Khatatbah",
  ["Al_Mansurah"] = "Al Mansurah",
  ["Al_Rahmaniyah_Air_Base"] = "Al Rahmaniyah Air Base",
  ["As_Salihiyah"] = "As Salihiyah",
  ["AzZaqaziq"] = "AzZaqaziq",
  ["Baluza"] = "Baluza",
  ["Ben_Gurion"] = "Ben-Gurion",
  ["Beni_Suef"] = "Beni Suef",
  ["Bilbeis_Air_Base"] = "Bilbeis Air Base",
  ["Bir_Hasanah"] = "Bir Hasanah",
  ["Birma_Air_Base"] = "Birma Air Base",
  ["Borg_El_Arab_International_Airport"] = "Borg El Arab International Airport",
  ["Cairo_International_Airport"] = "Cairo International Airport",
  ["Cairo_West"] = "Cairo West",
  ["Damascus_Intl"] = "Damascus Intl",
  ["Difarsuwar_Airfield"] = "Difarsuwar Airfield",
  ["El_Arish"] = "El Arish",
  ["El_Gora"] = "El Gora",
  ["El_Minya"] = "El Minya",
  ["Fayed"] = "Fayed",
  ["Gebel_El_Basur_Air_Base"] = "Gebel El Basur Air Base",
  ["Hatzerim"] = "Hatzerim",
  ["Hatzor"] = "Hatzor",
  ["Hurghada_International_Airport"] = "Hurghada International Airport",
  ["Inshas_Airbase"] = "Inshas Airbase",
  ["Jiyanklis_Air_Base"] = "Jiyanklis Air Base",
  ["Kedem"] = "Kedem",
  ["Kibrit_Air_Base"] = "Kibrit Air Base",
  ["Kom_Awshim"] = "Kom Awshim",
  ["Melez"] = "Melez",
  ["Mezzeh_Air_Base"] = "Mezzeh Air Base",
  ["Nevatim"] = "Nevatim",
  ["Ovda"] = "Ovda",
  ["Palmachim"] = "Palmachim",
  ["Quwaysina"] = "Quwaysina",
  ["Rafic_Hariri_Intl"] = "Rafic Hariri Intl",
  ["Ramat_David"] = "Ramat David",
  ["Ramon_Airbase"] = "Ramon Airbase",
  ["Ramon_International_Airport"] = "Ramon International Airport",
  ["Sde_Dov"] = "Sde Dov",
  ["Sharm_El_Sheikh_International_Airport"] = "Sharm El Sheikh International Airport",
  ["St_Catherine"] = "St Catherine",
  ["Tabuk"] = "Tabuk",
  ["Tel_Nof"] = "Tel Nof",
  ["Wadi_Abu_Rish"] = "Wadi Abu Rish",
  ["Wadi_al_Jandali"] = "Wadi al Jandali",
}

--- Airbases of the Kola map
-- 
-- * AIRBASE.Kola.Banak
-- * AIRBASE.Kola.Bodo
-- * AIRBASE.Kola.Ivalo
-- * AIRBASE.Kola.Jokkmokk
-- * AIRBASE.Kola.Kalixfors
-- * AIRBASE.Kola.Kallax
-- * AIRBASE.Kola.Kemi_Tornio
-- * AIRBASE.Kola.Kirkenes
-- * AIRBASE.Kola.Kiruna
-- * AIRBASE.Kola.Kuusamo
-- * AIRBASE.Kola.Monchegorsk
-- * AIRBASE.Kola.Murmansk_International
-- * AIRBASE.Kola.Olenya
-- * AIRBASE.Kola.Rovaniemi
-- * AIRBASE.Kola.Severomorsk_1
-- * AIRBASE.Kola.Severomorsk_3
-- * AIRBASE.Kola.Vidsel
-- * AIRBASE.Kola.Vuojarvi
-- * AIRBASE.Kola.Andoya
-- * AIRBASE.Kola.Alakurtti
-- * AIRBASE.Kola.Kittila
-- * AIRBASE.Kola.Bardufoss
-- * AIRBASE.Kola.Alta
-- * AIRBASE.Kola.Sodankyla
-- * AIRBASE.Kola.Enontekio
-- * AIRBASE.Kola.Evenes
-- * AIRBASE.Kola.Hosio
--
-- @field Kola
AIRBASE.Kola = {
  ["Banak"] = "Banak",
  ["Bodo"] = "Bodo",
  ["Ivalo"] = "Ivalo",
  ["Jokkmokk"] = "Jokkmokk",
  ["Kalixfors"] = "Kalixfors",
  ["Kallax"] = "Kallax",
  ["Kemi_Tornio"] = "Kemi Tornio",
  ["Kirkenes"] = "Kirkenes",
  ["Kiruna"] = "Kiruna",
  ["Kuusamo"] = "Kuusamo",
  ["Monchegorsk"] = "Monchegorsk",
  ["Murmansk_International"] = "Murmansk International",
  ["Olenya"] = "Olenya",
  ["Rovaniemi"] = "Rovaniemi",
  ["Severomorsk_1"] = "Severomorsk-1",
  ["Severomorsk_3"] = "Severomorsk-3",
  ["Vidsel"] = "Vidsel",
  ["Vuojarvi"] = "Vuojarvi",
  ["Andoya"] = "Andoya",
  ["Alakurtti"] = "Alakurtti",
  ["Kittila"] = "Kittila",
  ["Bardufoss"] = "Bardufoss",
  ["Alta"] = "Alta",
  ["Sodankyla"] = "Sodankyla",
  ["Enontekio"] = "Enontekio",
  ["Evenes"] = "Evenes",
  ["Hosio"] = "Hosio",
  ["Kilpyavr"] = "Kilpyavr",
  ["Afrikanda"] = "Afrikanda",
  ["Kalevala"] = "Kalevala",
  ["Koshka_Yavr"] = "Koshka Yavr",
  ["Poduzhemye"] = "Poduzhemye",
  ["Luostari_Pechenga"] = "Luostari Pechenga",
}

--- Airbases of the Afghanistan map
-- 
-- * AIRBASE.Afghanistan.Bost
-- * AIRBASE.Afghanistan.Bagram
-- * AIRBASE.Afghanistan.Bamyan
-- * AIRBASE.Afghanistan.Camp_Bastion
-- * AIRBASE.Afghanistan.Camp_Bastion_Heliport
-- * AIRBASE.Afghanistan.Chaghcharan
-- * AIRBASE.Afghanistan.Dwyer
-- * AIRBASE.Afghanistan.Farah
-- * AIRBASE.Afghanistan.Herat
-- * AIRBASE.Afghanistan.Gardez
-- * AIRBASE.Afghanistan.Ghazni_Heliport
-- * AIRBASE.Afghanistan.Jalalabad
-- * AIRBASE.Afghanistan.Kabul
-- * AIRBASE.Afghanistan.Kandahar
-- * AIRBASE.Afghanistan.Kandahar_Heliport
-- * AIRBASE.Afghanistan.Khost
-- * AIRBASE.Afghanistan.Khost_Heliport
-- * AIRBASE.Afghanistan.Maymana_Zahiraddin_Faryabi
-- * AIRBASE.Afghanistan.Nimroz
-- * AIRBASE.Afghanistan.Qala_i_Naw
-- * AIRBASE.Afghanistan.Shindand
-- * AIRBASE.Afghanistan.Shindand_Heliport
-- * AIRBASE.Afghanistan.Tarinkot
-- * AIRBASE.Afghanistan.Urgoon_Heliport
-- 
-- @field Afghanistan
AIRBASE.Afghanistan = {
  ["Bagram"] = "Bagram",
  ["Bamyan"] = "Bamyan",
  ["Bost"] = "Bost",
  ["Camp_Bastion"] = "Camp Bastion",
  ["Camp_Bastion_Heliport"] = "Camp Bastion Heliport",
  ["Chaghcharan"] = "Chaghcharan",
  ["Dwyer"] = "Dwyer",
  ["Farah"] = "Farah",
  ["Gardez"] = "Gardez",
  ["Ghazni_Heliport"] = "Ghazni Heliport",
  ["Herat"] = "Herat",
  ["Jalalabad"] = "Jalalabad",
  ["Kabul"] = "Kabul",
  ["Kandahar"] = "Kandahar",
  ["Kandahar_Heliport"] = "Kandahar Heliport",
  ["Khost"] = "Khost",
  ["Khost_Heliport"] = "Khost Heliport",
  ["Maymana_Zahiraddin_Faryabi"] = "Maymana Zahiraddin Faryabi",
  ["Nimroz"] = "Nimroz",
  ["Qala_i_Naw"] = "Qala i Naw",
  ["Sharana"] = "Sharana",
  ["Shindand"] = "Shindand",
  ["Shindand_Heliport"] = "Shindand Heliport",
  ["Tarinkot"] = "Tarinkot",
  ["Urgoon_Heliport"] = "Urgoon Heliport",
}

--- Airbases of the Iraq map
--
-- * AIRBASE.Iraq.Al_Asad_Airbase
-- * AIRBASE.Iraq.Al_Kut_Airbase
-- * AIRBASE.Iraq.Al_Sahra_Airport
-- * AIRBASE.Iraq.Al_Salam_Airbase
-- * AIRBASE.Iraq.Al_Taji_Airport
-- * AIRBASE.Iraq.Al_Taquddum_Airport
-- * AIRBASE.Iraq.Baghdad_International_Airport
-- * AIRBASE.Iraq.Balad_Airbase
-- * AIRBASE.Iraq.Bashur_Airport
-- * AIRBASE.Iraq.Erbil_International_Airport
-- * AIRBASE.Iraq.Sulaimaniyah_International_Airport
-- * AIRBASE.Iraq.H2_Airbase
-- * AIRBASE.Iraq.H3_Main_Airbase
-- * AIRBASE.Iraq.H3_Northwest_Airbase
-- * AIRBASE.Iraq.H3_Southwest_Airbase
-- * AIRBASE.Iraq.K1_Base
-- * AIRBASE.Iraq.Kirkuk_International_Airport
-- * AIRBASE.Iraq.Mosul_International_Airport
-- * AIRBASE.Iraq.Qayyarah_Airfield_West
-- * AIRBASE.Iraq.Sulaimaniyah_International_Airport
--
-- @field Iraq
AIRBASE.Iraq = {
  ["Al_Asad_Airbase"] = "Al-Asad Airbase",
  ["Al_Kut_Airport"] = "Al-Kut Airport",
  ["Al_Sahra_Airport"] = "Al-Sahra Airport",
  ["Al_Salam_Airbase"] = "Al-Salam Airbase",
  ["Al_Taji_Airport"] = "Al-Taji Airport",
  ["Al_Taquddum_Airport"] = "Al-Taquddum Airport",
  ["Baghdad_International_Airport"] = "Baghdad International Airport",
  ["Balad_Airbase"] = "Balad Airbase",
  ["Bashur_Airport"] = "Bashur Airport",
  ["Erbil_International_Airport"] = "Erbil International Airport",
  ["H2_Airbase"] = "H-2 Airbase",
  ["H3_Main_Airbase"] = "H-3 Main Airbase",
  ["H3_Northwest_Airbase"] = "H-3 Northwest Airbase",
  ["H3_Southwest_Airbase"] = "H-3 Southwest Airbase",
  ["K1_Base"] = "K1 Base",
  ["Kirkuk_International_Airport"] = "Kirkuk International Airport",
  ["Mosul_International_Airport"] = "Mosul International Airport",
  ["Qayyarah_Airfield_West"] = "Qayyarah Airfield West",
  ["Sulaimaniyah_International_Airport"] = "Sulaimaniyah International Airport",
}

    
--- Airbases of the Germany Cold War map  
-- * AIRBASE.GermanyCW.Airracing_Frankfurt  
-- * AIRBASE.GermanyCW.Airracing_Frankfurt  
-- * AIRBASE.GermanyCW.Airracing_Koblenz  
-- * AIRBASE.GermanyCW.Airracing_Luebeck  
-- * AIRBASE.GermanyCW.Allstedt  
-- * AIRBASE.GermanyCW.Altes_Lager  
-- * AIRBASE.GermanyCW.Bad_Duerkheim  
-- * AIRBASE.GermanyCW.Barth  
-- * AIRBASE.GermanyCW.Bienenfarm  
-- * AIRBASE.GermanyCW.Bindersleben  
-- * AIRBASE.GermanyCW.Bitburg  
-- * AIRBASE.GermanyCW.Braunschweig  
-- * AIRBASE.GermanyCW.Bremen  
-- * AIRBASE.GermanyCW.Briest  
-- * AIRBASE.GermanyCW.Buechel  
-- * AIRBASE.GermanyCW.Bueckeburg  
-- * AIRBASE.GermanyCW.Celle  
-- * AIRBASE.GermanyCW.Cochstedt  
-- * AIRBASE.GermanyCW.Damgarten  
-- * AIRBASE.GermanyCW.Dedelow  
-- * AIRBASE.GermanyCW.Dessau  
-- * AIRBASE.GermanyCW.Fassberg  
-- * AIRBASE.GermanyCW.Finow  
-- * AIRBASE.GermanyCW.Frankfurt  
-- * AIRBASE.GermanyCW.Fritzlar  
-- * AIRBASE.GermanyCW.Fulda  
-- * AIRBASE.GermanyCW.Gardelegen  
-- * AIRBASE.GermanyCW.Garz  
-- * AIRBASE.GermanyCW.Gatow  
-- * AIRBASE.GermanyCW.Gelnhausen  
-- * AIRBASE.GermanyCW.Giebelstadt  
-- * AIRBASE.GermanyCW.Glindbruchkippe
-- * AIRBASE.GermanyCW.Gross_Mohrdorf  
-- * AIRBASE.GermanyCW.Grosse_Wiese  
-- * AIRBASE.GermanyCW.Guetersloh  
-- * AIRBASE.GermanyCW.H_FRG_01  
-- * AIRBASE.GermanyCW.H_FRG_02  
-- * AIRBASE.GermanyCW.H_FRG_03  
-- * AIRBASE.GermanyCW.H_FRG_04  
-- * AIRBASE.GermanyCW.H_FRG_05  
-- * AIRBASE.GermanyCW.H_FRG_06  
-- * AIRBASE.GermanyCW.H_FRG_07  
-- * AIRBASE.GermanyCW.H_FRG_08  
-- * AIRBASE.GermanyCW.H_FRG_09  
-- * AIRBASE.GermanyCW.H_FRG_10  
-- * AIRBASE.GermanyCW.H_FRG_11  
-- * AIRBASE.GermanyCW.H_FRG_12  
-- * AIRBASE.GermanyCW.H_FRG_13  
-- * AIRBASE.GermanyCW.H_FRG_14  
-- * AIRBASE.GermanyCW.H_FRG_15  
-- * AIRBASE.GermanyCW.H_FRG_16  
-- * AIRBASE.GermanyCW.H_FRG_17  
-- * AIRBASE.GermanyCW.H_FRG_18  
-- * AIRBASE.GermanyCW.H_FRG_19  
-- * AIRBASE.GermanyCW.H_FRG_20  
-- * AIRBASE.GermanyCW.H_FRG_21  
-- * AIRBASE.GermanyCW.H_FRG_23  
-- * AIRBASE.GermanyCW.H_FRG_25  
-- * AIRBASE.GermanyCW.H_FRG_27  
-- * AIRBASE.GermanyCW.H_FRG_30  
-- * AIRBASE.GermanyCW.H_FRG_31  
-- * AIRBASE.GermanyCW.H_FRG_32  
-- * AIRBASE.GermanyCW.H_FRG_34  
-- * AIRBASE.GermanyCW.H_FRG_38  
-- * AIRBASE.GermanyCW.H_FRG_39  
-- * AIRBASE.GermanyCW.H_FRG_40  
-- * AIRBASE.GermanyCW.H_FRG_41  
-- * AIRBASE.GermanyCW.H_FRG_42  
-- * AIRBASE.GermanyCW.H_FRG_43  
-- * AIRBASE.GermanyCW.H_FRG_44  
-- * AIRBASE.GermanyCW.H_FRG_45  
-- * AIRBASE.GermanyCW.H_FRG_46  
-- * AIRBASE.GermanyCW.H_FRG_47  
-- * AIRBASE.GermanyCW.H_FRG_48  
-- * AIRBASE.GermanyCW.H_FRG_49  
-- * AIRBASE.GermanyCW.H_FRG_50  
-- * AIRBASE.GermanyCW.H_FRG_51  
-- * AIRBASE.GermanyCW.H_GDR_01  
-- * AIRBASE.GermanyCW.H_GDR_02  
-- * AIRBASE.GermanyCW.H_GDR_03  
-- * AIRBASE.GermanyCW.H_GDR_04  
-- * AIRBASE.GermanyCW.H_GDR_05  
-- * AIRBASE.GermanyCW.H_GDR_06  
-- * AIRBASE.GermanyCW.H_GDR_07  
-- * AIRBASE.GermanyCW.H_GDR_08  
-- * AIRBASE.GermanyCW.H_GDR_09  
-- * AIRBASE.GermanyCW.H_GDR_10  
-- * AIRBASE.GermanyCW.H_GDR_11  
-- * AIRBASE.GermanyCW.H_GDR_12  
-- * AIRBASE.GermanyCW.H_GDR_13  
-- * AIRBASE.GermanyCW.H_GDR_14  
-- * AIRBASE.GermanyCW.H_GDR_15  
-- * AIRBASE.GermanyCW.H_GDR_16  
-- * AIRBASE.GermanyCW.H_GDR_17  
-- * AIRBASE.GermanyCW.H_GDR_18  
-- * AIRBASE.GermanyCW.H_GDR_19  
-- * AIRBASE.GermanyCW.H_GDR_21  
-- * AIRBASE.GermanyCW.H_GDR_22  
-- * AIRBASE.GermanyCW.H_GDR_24  
-- * AIRBASE.GermanyCW.H_GDR_25  
-- * AIRBASE.GermanyCW.H_GDR_26  
-- * AIRBASE.GermanyCW.H_GDR_30  
-- * AIRBASE.GermanyCW.H_GDR_31  
-- * AIRBASE.GermanyCW.H_GDR_32  
-- * AIRBASE.GermanyCW.H_GDR_33  
-- * AIRBASE.GermanyCW.H_GDR_34  
-- * AIRBASE.GermanyCW.H_Med_FRG_01  
-- * AIRBASE.GermanyCW.H_Med_FRG_02  
-- * AIRBASE.GermanyCW.H_Med_FRG_04  
-- * AIRBASE.GermanyCW.H_Med_FRG_06  
-- * AIRBASE.GermanyCW.H_Med_FRG_11  
-- * AIRBASE.GermanyCW.H_Med_FRG_12  
-- * AIRBASE.GermanyCW.H_Med_FRG_13  
-- * AIRBASE.GermanyCW.H_Med_FRG_14  
-- * AIRBASE.GermanyCW.H_Med_FRG_15  
-- * AIRBASE.GermanyCW.H_Med_FRG_16  
-- * AIRBASE.GermanyCW.H_Med_FRG_17  
-- * AIRBASE.GermanyCW.H_Med_FRG_21  
-- * AIRBASE.GermanyCW.H_Med_FRG_24  
-- * AIRBASE.GermanyCW.H_Med_FRG_26  
-- * AIRBASE.GermanyCW.H_Med_FRG_27  
-- * AIRBASE.GermanyCW.H_Med_FRG_29  
-- * AIRBASE.GermanyCW.H_Med_GDR_01  
-- * AIRBASE.GermanyCW.H_Med_GDR_02  
-- * AIRBASE.GermanyCW.H_Med_GDR_03  
-- * AIRBASE.GermanyCW.H_Med_GDR_08  
-- * AIRBASE.GermanyCW.H_Med_GDR_09  
-- * AIRBASE.GermanyCW.H_Med_GDR_10  
-- * AIRBASE.GermanyCW.H_Med_GDR_11  
-- * AIRBASE.GermanyCW.H_Med_GDR_12  
-- * AIRBASE.GermanyCW.H_Med_GDR_13  
-- * AIRBASE.GermanyCW.H_Med_GDR_14  
-- * AIRBASE.GermanyCW.H_Med_GDR_16  
-- * AIRBASE.GermanyCW.H_Radar_FRG_02  
-- * AIRBASE.GermanyCW.H_Radar_GDR_01  
-- * AIRBASE.GermanyCW.H_Radar_GDR_02  
-- * AIRBASE.GermanyCW.H_Radar_GDR_03  
-- * AIRBASE.GermanyCW.H_Radar_GDR_04  
-- * AIRBASE.GermanyCW.H_Radar_GDR_05  
-- * AIRBASE.GermanyCW.H_Radar_GDR_06  
-- * AIRBASE.GermanyCW.H_Radar_GDR_07  
-- * AIRBASE.GermanyCW.H_Radar_GDR_08  
-- * AIRBASE.GermanyCW.H_Radar_GDR_09  
-- * AIRBASE.GermanyCW.Hahn  
-- * AIRBASE.GermanyCW.Haina  
-- * AIRBASE.GermanyCW.Hamburg  
-- * AIRBASE.GermanyCW.Hamburg_Finkenwerder  
-- * AIRBASE.GermanyCW.Hannover  
-- * AIRBASE.GermanyCW.Hasselfelde  
-- * AIRBASE.GermanyCW.Herrenteich  
-- * AIRBASE.GermanyCW.Hildesheim  
-- * AIRBASE.GermanyCW.Hockenheim  
-- * AIRBASE.GermanyCW.Holzdorf  
-- * AIRBASE.GermanyCW.Kammermark  
-- * AIRBASE.GermanyCW.Koethen  
-- * AIRBASE.GermanyCW.Laage  
-- * AIRBASE.GermanyCW.Langenselbold  
-- * AIRBASE.GermanyCW.Laerz  
-- * AIRBASE.GermanyCW.Leipzig_Halle  
-- * AIRBASE.GermanyCW.Leipzig_Mockau  
-- * AIRBASE.GermanyCW.Luebeck  
-- * AIRBASE.GermanyCW.Lueneburg  
-- * AIRBASE.GermanyCW.Mahlwinkel  
-- * AIRBASE.GermanyCW.Mendig  
-- * AIRBASE.GermanyCW.Merseburg  
-- * AIRBASE.GermanyCW.Neubrandenburg  
-- * AIRBASE.GermanyCW.Neuruppin  
-- * AIRBASE.GermanyCW.Northeim  
-- * AIRBASE.GermanyCW.Ober_Moerlen  
-- * AIRBASE.GermanyCW.Obermehler_Schlotheim  
-- * AIRBASE.GermanyCW.Parchim  
-- * AIRBASE.GermanyCW.Peenemuende  
-- * AIRBASE.GermanyCW.Pferdsfeld  
-- * AIRBASE.GermanyCW.Pinnow  
-- * AIRBASE.GermanyCW.Pottschutthoehe  
-- * AIRBASE.GermanyCW.Ramstein  
-- * AIRBASE.GermanyCW.Rinteln  
-- * AIRBASE.GermanyCW.Schoenefeld  
-- * AIRBASE.GermanyCW.Schweinfurt  
-- * AIRBASE.GermanyCW.Sembach  
-- * AIRBASE.GermanyCW.Spangdahlem  
-- * AIRBASE.GermanyCW.Sperenberg  
-- * AIRBASE.GermanyCW.Stendal  
-- * AIRBASE.GermanyCW.Tegel  
-- * AIRBASE.GermanyCW.Tempelhof  
-- * AIRBASE.GermanyCW.Templin  
-- * AIRBASE.GermanyCW.Tutow  
-- * AIRBASE.GermanyCW.Uelzen  
-- * AIRBASE.GermanyCW.Uetersen  
-- * AIRBASE.GermanyCW.Ummern  
-- * AIRBASE.GermanyCW.Verden_Scharnhorst  
-- * AIRBASE.GermanyCW.Walldorf  
-- * AIRBASE.GermanyCW.Waren_Vielist  
-- * AIRBASE.GermanyCW.Werneuchen  
-- * AIRBASE.GermanyCW.Weser_Wuemme  
-- * AIRBASE.GermanyCW.Wiesbaden  
-- * AIRBASE.GermanyCW.Wismar  
-- * AIRBASE.GermanyCW.Wittstock  
-- * AIRBASE.GermanyCW.Worms  
-- * AIRBASE.GermanyCW.Wunstorf  
-- * AIRBASE.GermanyCW.Zerbst  
-- * AIRBASE.GermanyCW.Zweibruecken  
--
-- @field GermanyCW
AIRBASE.GermanyCW = {
  ["Airracing_Frankfurt"] = "Airracing Frankfurt",
  ["Airracing_Koblenz"] = "Airracing Koblenz",
  ["Airracing_Luebeck"] = "Airracing Lubeck",
  ["Allstedt"] = "Allstedt",
  ["Altes_Lager"] = "Altes Lager",
  ["Bad_Duerkheim"] = "Bad Durkheim",
  ["Barth"] = "Barth",
  ["Bienenfarm"] = "Bienenfarm",
  ["Bindersleben"] = "Bindersleben",
  ["Bitburg"] = "Bitburg",
  ["Braunschweig"] = "Braunschweig",
  ["Bremen"] = "Bremen",
  ["Briest"] = "Briest",
  ["Buechel"] = "Buchel",
  ["Bueckeburg"] = "Buckeburg",
  ["Celle"] = "Celle",
  ["Cochstedt"] = "Cochstedt",
  ["Damgarten"] = "Damgarten",
  ["Dedelow"] = "Dedelow",
  ["Dessau"] = "Dessau",
  ["Fassberg"] = "Fassberg",
  ["Finow"] = "Finow",
  ["Frankfurt"] = "Frankfurt",
  ["Fritzlar"] = "Fritzlar",
  ["Fulda"] = "Fulda",
  ["Gardelegen"] = "Gardelegen",
  ["Garz"] = "Garz",
  ["Gatow"] = "Gatow",
  ["Gelnhausen"] = "Gelnhausen",
  ["Giebelstadt"] = "Giebelstadt",
  ["Glindbruchkippe"] = "Glindbruchkippe ",
  ["Gross_Mohrdorf"] = "Gross Mohrdorf",
  ["Grosse_Wiese"] = "Grosse Wiese",
  ["Guetersloh"] = "Gutersloh",
  ["H_FRG_01"] = "H FRG 01",
  ["H_FRG_02"] = "H FRG 02",
  ["H_FRG_03"] = "H FRG 03",
  ["H_FRG_04"] = "H FRG 04",
  ["H_FRG_05"] = "H FRG 05",
  ["H_FRG_06"] = "H FRG 06",
  ["H_FRG_07"] = "H FRG 07",
  ["H_FRG_08"] = "H FRG 08",
  ["H_FRG_09"] = "H FRG 09",
  ["H_FRG_10"] = "H FRG 10",
  ["H_FRG_11"] = "H FRG 11",
  ["H_FRG_12"] = "H FRG 12",
  ["H_FRG_13"] = "H FRG 13",
  ["H_FRG_14"] = "H FRG 14",
  ["H_FRG_15"] = "H FRG 15",
  ["H_FRG_16"] = "H FRG 16",
  ["H_FRG_17"] = "H FRG 17",
  ["H_FRG_18"] = "H FRG 18",
  ["H_FRG_19"] = "H FRG 19",
  ["H_FRG_20"] = "H FRG 20",
  ["H_FRG_21"] = "H FRG 21",
  ["H_FRG_23"] = "H FRG 23",
  ["H_FRG_25"] = "H FRG 25",
  ["H_FRG_27"] = "H FRG 27",
  ["H_FRG_30"] = "H FRG 30",
  ["H_FRG_31"] = "H FRG 31",
  ["H_FRG_32"] = "H FRG 32",
  ["H_FRG_34"] = "H FRG 34",
  ["H_FRG_38"] = "H FRG 38",
  ["H_FRG_39"] = "H FRG 39",
  ["H_FRG_40"] = "H FRG 40",
  ["H_FRG_41"] = "H FRG 41",
  ["H_FRG_42"] = "H FRG 42",
  ["H_FRG_43"] = "H FRG 43",
  ["H_FRG_44"] = "H FRG 44",
  ["H_FRG_45"] = "H FRG 45",
  ["H_FRG_46"] = "H FRG 46",
  ["H_FRG_47"] = "H FRG 47",
  ["H_FRG_48"] = "H FRG 48",
  ["H_FRG_49"] = "H FRG 49",
  ["H_FRG_50"] = "H FRG 50",
  ["H_FRG_51"] = "H FRG 51",
  ["H_GDR_01"] = "H GDR 01",
  ["H_GDR_02"] = "H GDR 02",
  ["H_GDR_03"] = "H GDR 03",
  ["H_GDR_04"] = "H GDR 04",
  ["H_GDR_05"] = "H GDR 05",
  ["H_GDR_06"] = "H GDR 06",
  ["H_GDR_07"] = "H GDR 07",
  ["H_GDR_08"] = "H GDR 08",
  ["H_GDR_09"] = "H GDR 09",
  ["H_GDR_10"] = "H GDR 10",
  ["H_GDR_11"] = "H GDR 11",
  ["H_GDR_12"] = "H GDR 12",
  ["H_GDR_13"] = "H GDR 13",
  ["H_GDR_14"] = "H GDR 14",
  ["H_GDR_15"] = "H GDR 15",
  ["H_GDR_16"] = "H GDR 16",
  ["H_GDR_17"] = "H GDR 17",
  ["H_GDR_18"] = "H GDR 18",
  ["H_GDR_19"] = "H GDR 19",
  ["H_GDR_21"] = "H GDR 21",
  ["H_GDR_22"] = "H GDR 22",
  ["H_GDR_24"] = "H GDR 24",
  ["H_GDR_25"] = "H GDR 25",
  ["H_GDR_26"] = "H GDR 26",
  ["H_GDR_30"] = "H GDR 30",
  ["H_GDR_31"] = "H GDR 31",
  ["H_GDR_32"] = "H GDR 32",
  ["H_GDR_33"] = "H GDR 33",
  ["H_GDR_34"] = "H GDR 34",
  ["H_Med_FRG_01"] = "H Med FRG 01",
  ["H_Med_FRG_02"] = "H Med FRG 02",
  ["H_Med_FRG_04"] = "H Med FRG 04",
  ["H_Med_FRG_06"] = "H Med FRG 06",
  ["H_Med_FRG_11"] = "H Med FRG 11",
  ["H_Med_FRG_12"] = "H Med FRG 12",
  ["H_Med_FRG_13"] = "H Med FRG 13",
  ["H_Med_FRG_14"] = "H Med FRG 14",
  ["H_Med_FRG_15"] = "H Med FRG 15",
  ["H_Med_FRG_16"] = "H Med FRG 16",
  ["H_Med_FRG_17"] = "H Med FRG 17",
  ["H_Med_FRG_21"] = "H Med FRG 21",
  ["H_Med_FRG_24"] = "H Med FRG 24",
  ["H_Med_FRG_26"] = "H Med FRG 26",
  ["H_Med_FRG_27"] = "H Med FRG 27",
  ["H_Med_FRG_29"] = "H Med FRG 29",
  ["H_Med_GDR_01"] = "H Med GDR 01",
  ["H_Med_GDR_02"] = "H Med GDR 02",
  ["H_Med_GDR_03"] = "H Med GDR 03",
  ["H_Med_GDR_08"] = "H Med GDR 08",
  ["H_Med_GDR_09"] = "H Med GDR 09",
  ["H_Med_GDR_10"] = "H Med GDR 10",
  ["H_Med_GDR_11"] = "H Med GDR 11",
  ["H_Med_GDR_12"] = "H Med GDR 12",
  ["H_Med_GDR_13"] = "H Med GDR 13",
  ["H_Med_GDR_14"] = "H Med GDR 14",
  ["H_Med_GDR_16"] = "H Med GDR 16",
  ["H_Radar_FRG_02"] = "H Radar FRG 02",
  ["H_Radar_GDR_01"] = "H Radar GDR 01",
  ["H_Radar_GDR_02"] = "H Radar GDR 02",
  ["H_Radar_GDR_03"] = "H Radar GDR 03",
  ["H_Radar_GDR_04"] = "H Radar GDR 04",
  ["H_Radar_GDR_05"] = "H Radar GDR 05",
  ["H_Radar_GDR_06"] = "H Radar GDR 06",
  ["H_Radar_GDR_07"] = "H Radar GDR 07",
  ["H_Radar_GDR_08"] = "H Radar GDR 08",
  ["H_Radar_GDR_09"] = "H Radar GDR 09",
  ["Hahn"] = "Hahn",
  ["Haina"] = "Haina",
  ["Hamburg"] = "Hamburg",
  ["Hamburg_Finkenwerder"] = "Hamburg Finkenwerder",
  ["Hannover"] = "Hannover",
  ["Hasselfelde"] = "Hasselfelde",
  ["Herrenteich"] = "Herrenteich",
  ["Hildesheim"] = "Hildesheim",
  ["Hockenheim"] = "Hockenheim",
  ["Holzdorf"] = "Holzdorf",
  ["Kammermark"] = "Kammermark",
  ["Koethen"] = "Kothen",
  ["Laage"] = "Laage",
  ["Langenselbold"] = "Langenselbold",
  ["Laerz"] = "Larz",
  ["Leipzig_Halle"] = "Leipzig Halle",
  ["Leipzig_Mockau"] = "Leipzig Mockau",
  ["Luebeck"] = "Lubeck",
  ["Lueneburg"] = "Luneburg",
  ["Mahlwinkel"] = "Mahlwinkel",
  ["Mendig"] = "Mendig",
  ["Merseburg"] = "Merseburg",
  ["Neubrandenburg"] = "Neubrandenburg",
  ["Neuruppin"] = "Neuruppin",
  ["Northeim"] = "Northeim",
  ["Ober_Moerlen"] = "Ober-Morlen",
  ["Obermehler_Schlotheim"] = "Obermehler Schlotheim",
  ["Parchim"] = "Parchim",
  ["Peenemuende"] = "Peenemunde",
  ["Pferdsfeld"] = "Pferdsfeld",
  ["Pinnow"] = "Pinnow",
  ["Pottschutthoehe"] = "Pottschutthohe",
  ["Ramstein"] = "Ramstein",
  ["Rinteln"] = "Rinteln",
  ["Schoenefeld"] = "Schonefeld",
  ["Schweinfurt"] = "Schweinfurt",
  ["Sembach"] = "Sembach",
  ["Spangdahlem"] = "Spangdahlem",
  ["Sperenberg"] = "Sperenberg",
  ["Stendal"] = "Stendal",
  ["Tegel"] = "Tegel",
  ["Tempelhof"] = "Tempelhof",
  ["Templin"] = "Templin",
  ["Tutow"] = "Tutow",
  ["Uelzen"] = "Uelzen",
  ["Uetersen"] = "Uetersen",
  ["Ummern"] = "Ummern",
  ["Verden_Scharnhorst"] = "Verden-Scharnhorst",
  ["Walldorf"] = "Walldorf",
  ["Waren_Vielist"] = "Waren Vielist",
  ["Werneuchen"] = "Werneuchen",
  ["Weser_Wuemme"] = "Weser Wumme",
  ["Wiesbaden"] = "Wiesbaden",
  ["Wismar"] = "Wismar",
  ["Wittstock"] = "Wittstock",
  ["Worms"] = "Worms",
  ["Wunstorf"] = "Wunstorf",
  ["Zerbst"] = "Zerbst",
  ["Zweibruecken"] = "Zweibrucken",
}


--- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy".
-- @type AIRBASE.ParkingSpot
-- @field Core.Point#COORDINATE Coordinate Coordinate of the parking spot.
-- @field #number TerminalID Terminal ID of the spot. Generally, this is not the same number as displayed in the mission editor.
-- @field #AIRBASE.TerminalType TerminalType Type of the spot, i.e. for which type of aircraft it can be used.
-- @field #boolean TOAC Takeoff or landing aircarft. I.e. this stop is occupied currently by an aircraft until it took of or until it landed.
-- @field #boolean Free This spot is currently free, i.e. there is no alive aircraft on it at the present moment.
-- @field #number TerminalID0 Unknown what this means. If you know, please tell us!
-- @field #number DistToRwy Distance to runway in meters. Currently bugged and giving the same number as the TerminalID.
-- @field #string AirbaseName Name of the airbase.
-- @field #number MarkerID Numerical ID of marker placed at parking spot.
-- @field Wrapper.Marker#MARKER Marker The marker on the F10 map.
-- @field #string ClientSpot If `true`, this is a parking spot of a client aircraft.
-- @field #string ClientName Client unit name of this spot.
-- @field #string Status Status of spot e.g. `AIRBASE.SpotStatus.FREE`.
-- @field #string OccupiedBy Name of the aircraft occupying the spot or "unknown". Can be *nil* if spot is not occupied.
-- @field #string ReservedBy Name of the aircraft for which this spot is reserved. Can be *nil* if spot is not reserved.

--- Terminal Types of parking spots. See also https://wiki.hoggitworld.com/view/DCS_func_getParking
--
-- Supported types are:
--
-- * AIRBASE.TerminalType.Runway = 16: Valid spawn points on runway.
-- * AIRBASE.TerminalType.HelicopterOnly = 40: Special spots for Helicopers.
-- * AIRBASE.TerminalType.Shelter = 68: Hardened Air Shelter. Currently only on Caucaus map.
-- * AIRBASE.TerminalType.OpenMed = 72: Open/Shelter air airplane only.
-- * AIRBASE.TerminalType.OpenBig = 104: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there.
-- * AIRBASE.TerminalType.OpenMedOrBig = 176: Combines OpenMed and OpenBig spots.
-- * AIRBASE.TerminalType.HelicopterUsable = 216: Combines HelicopterOnly, OpenMed and OpenBig.
-- * AIRBASE.TerminalType.FighterAircraft = 244: Combines Shelter. OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft.
--
-- @type AIRBASE.TerminalType
-- @field #number Runway 16: Valid spawn points on runway.
-- @field #number HelicopterOnly 40: Special spots for Helicopers.
-- @field #number Shelter 68: Hardened Air Shelter. Currently only on Caucaus map.
-- @field #number OpenMed 72: Open/Shelter air airplane only.
-- @field #number SmallSizeFigher 100: Tight spots for smaller type fixed wing aircraft, like the F-16. Example of these spots: 04, 05, 06 on Muwaffaq_Salti. A Viper sized plane can spawn here, but an A-10 or Strike Eagle can't
-- @field #number OpenBig 104: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there.
-- @field #number OpenMedOrBig 176: Combines OpenMed and OpenBig spots.
-- @field #number HelicopterUsable 216: Combines HelicopterOnly, OpenMed and OpenBig.
-- @field #number FighterAircraft 244: Combines Shelter, OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft.
-- @field #number FighterAircraftSmall 344: Combines Shelter, SmallsizeFighter, OpenMed and OpenBig spots. So effectively all spots usable by small fixed wing aircraft.
AIRBASE.TerminalType = {
  Runway=16,
  HelicopterOnly=40,
  Shelter=68,
  OpenMed=72,
  SmallSizeFighter=100,
  OpenBig=104,
  OpenMedOrBig=176,
  HelicopterUsable=216,
  FighterAircraft=244,
  FighterAircraftSmall=344,
}

--- Status of a parking spot.
-- @type AIRBASE.SpotStatus
-- @field #string FREE Spot is free.
-- @field #string OCCUPIED Spot is occupied.
-- @field #string RESERVED Spot is reserved.
AIRBASE.SpotStatus = {
  FREE="Free",
  OCCUPIED="Occupied",
  RESERVED="Reserved",
}

--- Runway data.
-- @type AIRBASE.Runway
-- @field #string name Runway name.
-- @field #string idx Runway ID: heading 070° ==> idx="07".
-- @field #number heading True heading of the runway in degrees.
-- @field #number magheading Magnetic heading of the runway in degrees. This is what is marked on the runway.
-- @field #number length Length of runway in meters.
-- @field #number width Width of runway in meters.
-- @field Core.Zone#ZONE_POLYGON zone Runway zone.
-- @field Core.Point#COORDINATE center Center of the runway.
-- @field Core.Point#COORDINATE position Position of runway start.
-- @field Core.Point#COORDINATE endpoint End point of runway.
-- @field #boolean isLeft If `true`, this is the left of two parallel runways. If `false`, this is the right of two runways. If `nil`, no parallel runway exists.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Registration
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new AIRBASE from DCSAirbase.
-- @param #AIRBASE self
-- @param #string AirbaseName The name of the airbase.
-- @return #AIRBASE self
function AIRBASE:Register(AirbaseName)

  -- Inherit everything from positionable.
  local self=BASE:Inherit(self, POSITIONABLE:New(AirbaseName)) --#AIRBASE

  -- Set airbase name.
  self.AirbaseName=AirbaseName

  -- Set airbase ID.
  self.AirbaseID=self:GetID(true)

  -- Get descriptors.
  self.descriptors=self:GetDesc()

  -- Debug info.
  --self:T({airbase=AirbaseName, descriptors=self.descriptors})

  -- Category.
  self.category=self.descriptors and self.descriptors.category or Airbase.Category.AIRDROME

  -- H2 is bugged
  --if self.AirbaseName == "H4" and self.descriptors == nil then
    --self:E("***** H4 on Syria map is currently bugged!")
    --return nil
  --end

  -- Set category.
if self.category==Airbase.Category.AIRDROME then
  self.isAirdrome=true
elseif self.category==Airbase.Category.HELIPAD or self.descriptors.typeName=="FARP_SINGLE_01" then
  self.isHelipad=true
  self.category=Airbase.Category.HELIPAD
elseif self.category==Airbase.Category.SHIP then
  self.isShip=true
  -- DCS bug: Oil rigs and gas platforms have category=2 (ship). Also they cannot be retrieved by coalition.getStaticObjects()
  if self.descriptors.typeName=="Oil rig" or self.descriptors.typeName=="Ga" then
    self.isHelipad=true
    self.isShip=false
    self.category=Airbase.Category.HELIPAD
    _DATABASE:AddStatic(AirbaseName)
  end
else
  self:E("ERROR: Unknown airbase category!")
end

  -- Init Runways.
  self:_InitRunways()
  
  -- Number of runways
  local Nrunways=#self.runways
  
  -- Set the active runways based on wind direction.
  if Nrunways>0 then
    self:SetActiveRunway()
  end

  -- Init parking spots.
  self:_InitParkingSpots()
  
  -- Some heliports identify as airdromes in the airbase category. This is buggy in the descriptors category but also in the getCategory() and getCategoryEx() functions.
  -- Well, thinking about it, this is actually not that "buggy" since these are really helicopter airdromes, which do not have an automatic parking spot routine.
  -- I am still changing the category but marking it as airdrome and heliport at the same time via isAirdrome=true and isHelipad=true (important in SPAWN.SpawnAtAirbase).
  -- The main reason for changing the category is to be able to filter airdromes from helipads, e.g. in SET_AIRBASE.
  if self.category==Airbase.Category.AIRDROME and (Nrunways==0 or self.NparkingTotal==self.NparkingTerminal[AIRBASE.TerminalType.HelicopterOnly]) then
    --self:E(string.format("WARNING: %s identifies as airdrome (category=0) but has no runways or just helo parking ==> will change to helipad (category=1)", self.AirbaseName))
    self.category=Airbase.Category.HELIPAD
    self.isAirdrome=true
    self.isHelipad=true
  end  

  -- Get 2D position vector.
  local vec2=self:GetVec2()

  -- Init coordinate.
  self:GetCoordinate()
  
  -- Storage.
  self.storage=_DATABASE:AddStorage(AirbaseName)

  if vec2 then
    if self.isShip then
      local unit=UNIT:FindByName(AirbaseName)
      if unit then
        self.AirbaseZone=ZONE_UNIT:New(AirbaseName, unit, 2500)
      end
    else
      self.AirbaseZone=ZONE_RADIUS:New(AirbaseName, vec2, 2500)
    end
  else
    self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s", AirbaseName))
  end

  -- Debug info.
  self:T2(string.format("Registered airbase %s", tostring(self.AirbaseName)))

  return self
end


--- Get the true airbase center as seen in the ME. The position returned by the dcs object is is wrong and often at the start of the runway.
-- @return DCS#Vec2 The center of the true center of the airbase if it contains runways, otherwise the default DCS object position.
function AIRBASE:GetVec2()
    local runways = self:GetRunways()
    if runways and #runways > 0 then
        return runways[1].center:GetVec2()
    end
    return self:GetCoordinate():GetVec2()
end

--- Get the category of this airbase. This is only a debug function because DCS 2.9 incorrectly returns heliports as airdromes.
-- @param #AIRBASE self
function AIRBASE:_GetCategory()

  local name=self.AirbaseName
  
  local static=StaticObject.getByName(name)
  local airbase=Airbase.getByName(name)
  local unit=Unit.getByName(name)
  
  local text=string.format("\n=====================================================")
  text=text..string.format("\nAirbase %s:", name)
  if static then
    local oc, uc=static:getCategory()
    local ex=static:getCategoryEx()
    text=text..string.format("\nSTATIC: oc=%d, uc=%d, ex=%d", oc, uc, ex)
    --text=text..UTILS.PrintTableToLog(static:getDesc(), nil, true)
    text=text..string.format("\n--------------------------------------------------")
  end
  if unit then
    local oc, uc=unit:getCategory()
    local ex=unit:getCategoryEx()
    text=text..string.format("\nUNIT: oc=%d, uc=%d, ex=%d", oc, uc, ex)
    --text=text..UTILS.PrintTableToLog(unit:getDesc(), nil, true)
    text=text..string.format("\n--------------------------------------------------")
  end  
  if airbase then
    local oc, uc=airbase:getCategory()
    local ex=airbase:getCategoryEx()
    text=text..string.format("\nAIRBASE: oc=%d, uc=%d, ex=%d", oc, uc, ex)
    text=text..string.format("\n--------------------------------------------------")
    text=text..UTILS.PrintTableToLog(airbase:getDesc(), nil, true)  
  end
  
  text=text..string.format("\n=====================================================")
  

  env.info(text)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Reference methods
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Finds a AIRBASE from the _DATABASE using a DCSAirbase object.
-- @param #AIRBASE self
-- @param DCS#Airbase DCSAirbase An existing DCS Airbase object reference.
-- @return Wrapper.Airbase#AIRBASE self
function AIRBASE:Find( DCSAirbase )

  local AirbaseName = DCSAirbase:getName()
  local AirbaseFound = _DATABASE:FindAirbase( AirbaseName )
  return AirbaseFound
end

--- Find a AIRBASE in the _DATABASE using the name of an existing DCS Airbase.
-- @param #AIRBASE self
-- @param #string AirbaseName The Airbase Name.
-- @return #AIRBASE self
function AIRBASE:FindByName( AirbaseName )

  local AirbaseFound = _DATABASE:FindAirbase( AirbaseName )
  return AirbaseFound
end

--- Find a AIRBASE in the _DATABASE by its ID.
-- @param #AIRBASE self
-- @param #number id Airbase ID.
-- @return #AIRBASE self
function AIRBASE:FindByID(id)

  for name,_airbase in pairs(_DATABASE.AIRBASES) do
    local airbase=_airbase --#AIRBASE

    local aid=tonumber(airbase:GetID(true))

    if aid==id then
      return airbase
    end

  end

  return nil
end

--- Get the DCS object of an airbase
-- @param #AIRBASE self
-- @return DCS#Airbase DCS airbase object.
function AIRBASE:GetDCSObject()

  -- Get the DCS object.
  local DCSAirbase = Airbase.getByName(self.AirbaseName)

  if DCSAirbase then
    return DCSAirbase
  end

  return nil
end

--- Get the airbase zone.
-- @param #AIRBASE self
-- @return Core.Zone#ZONE_RADIUS The zone radius of the airbase.
function AIRBASE:GetZone()
  return self.AirbaseZone
end

--- Get the DCS warehouse.
-- @param #AIRBASE self
-- @return DCS#Warehouse The DCS warehouse object.
function AIRBASE:GetWarehouse()
  local warehouse=nil --DCS#Warehouse
  local airbase=self:GetDCSObject()
  if airbase and Airbase.getWarehouse then
    warehouse=airbase:getWarehouse()
  end
  return warehouse
end

--- Get the warehouse storage of this airbase. The returned `STORAGE` object is the wrapper of the DCS warehouse.
-- This allows you to add and remove items such as aircraft, liquids, weapons and other equipment.
-- @param #AIRBASE self
-- @return Wrapper.Storage#STORAGE The storage.
function AIRBASE:GetStorage()
  return self.storage
end

--- Enables or disables automatic capturing of the airbase.
-- @param #AIRBASE self
-- @param #boolean Switch If `true`, enable auto capturing. If `false`, disable it.
-- @return #AIRBASE self
function AIRBASE:SetAutoCapture(Switch)

  local airbase=self:GetDCSObject()

  if airbase then
    airbase:autoCapture(Switch)
  end

  return self
end

--- Enables automatic capturing of the airbase.
-- @param #AIRBASE self
-- @return #AIRBASE self
function AIRBASE:SetAutoCaptureON()
  self:SetAutoCapture(true)
  return self
end

--- Disables automatic capturing of the airbase.
-- @param #AIRBASE self
-- @return #AIRBASE self
function AIRBASE:SetAutoCaptureOFF()
  self:SetAutoCapture(false)
  return self
end

--- Returns whether auto capturing of the airbase is on or off.
-- @param #AIRBASE self
-- @return #boolean Returns `true` if auto capturing is on, `false` if off and `nil` if the airbase object cannot be retrieved.
function AIRBASE:IsAutoCapture()

  local airbase=self:GetDCSObject()

  local auto=nil
  if airbase then
    auto=airbase:autoCaptureIsOn()
  end

  return auto
end

--- Sets the coalition of the airbase.
-- @param #AIRBASE self
-- @param #number Coal Coalition that the airbase should have (0=Neutral, 1=Red, 2=Blue).
-- @return #AIRBASE self
function AIRBASE:SetCoalition(Coal)

  local airbase=self:GetDCSObject()

  if airbase then
    airbase:setCoalition(Coal)
  end

  return self
end

--- Get all airbases of the current map. This includes ships and FARPS.
-- @param DCS#Coalition coalition (Optional) Return only airbases belonging to the specified coalition. By default, all airbases of the map are returned.
-- @param #number category (Optional) Return only airbases of a certain category, e.g. Airbase.Category.FARP
-- @return #table Table containing all airbase objects of the current map.
function AIRBASE.GetAllAirbases(coalition, category)

  local airbases={}
  for _,_airbase in pairs(_DATABASE.AIRBASES) do
    local airbase=_airbase --#AIRBASE
    if coalition==nil or airbase:GetCoalition()==coalition then
      if category==nil or category==airbase:GetAirbaseCategory() then
        table.insert(airbases, airbase)
      end
    end
  end

  return airbases
end

--- Get all airbase names of the current map. This includes ships and FARPS.
-- @param DCS#Coalition coalition (Optional) Return only airbases belonging to the specified coalition. By default, all airbases of the map are returned.
-- @param #number category (Optional) Return only airbases of a certain category, e.g. `Airbase.Category.HELIPAD`.
-- @return #table Table containing all airbase names of the current map.
function AIRBASE.GetAllAirbaseNames(coalition, category)

  local airbases={}
  for airbasename,_airbase in pairs(_DATABASE.AIRBASES) do
    local airbase=_airbase --#AIRBASE
    if coalition==nil or airbase:GetCoalition()==coalition then
      if category==nil or category==airbase:GetAirbaseCategory() then
        table.insert(airbases, airbasename)
      end
    end
  end

  return airbases
end

--- Get ID of the airbase.
-- @param #AIRBASE self
-- @param #boolean unique (Optional) If true, ships will get a negative sign as the unit ID might be the same as an airbase ID. Default off!
-- @return #number The airbase ID.
function AIRBASE:GetID(unique)

  if self.AirbaseID then

    return unique and self.AirbaseID or math.abs(self.AirbaseID)

  else

   for DCSAirbaseId, DCSAirbase in ipairs(world.getAirbases()) do

      -- Get the airbase name.
      local AirbaseName = DCSAirbase:getName()

      -- This gives the incorrect value to be inserted into the airdromeID for DCS 2.5.6!
      local airbaseID=tonumber(DCSAirbase:getID())

      local airbaseCategory=self:GetAirbaseCategory()

      if AirbaseName==self.AirbaseName then
        if airbaseCategory==Airbase.Category.SHIP or airbaseCategory==Airbase.Category.HELIPAD then
          -- Ships get a negative sign as their unit number might be the same as the ID of another airbase.
          return unique and -airbaseID or airbaseID
        else
          return airbaseID
        end
      end

    end

  end

  return nil
end

--- Set parking spot whitelist. Only these spots will be considered for spawning.
-- Black listed spots overrule white listed spots.
-- **NOTE** that terminal IDs are not necessarily the same as those displayed in the mission editor!
-- @param #AIRBASE self
-- @param #table TerminalIdWhitelist Table of white listed terminal IDs.
-- @return #AIRBASE self
-- @usage AIRBASE:FindByName("Batumi"):SetParkingSpotWhitelist({2, 3, 4}) --Only allow terminal IDs 2, 3, 4
function AIRBASE:SetParkingSpotWhitelist(TerminalIdWhitelist)

  if TerminalIdWhitelist==nil then
    self.parkingWhitelist={}
    return self
  end

  -- Ensure we got a table.
  if type(TerminalIdWhitelist)~="table" then
    TerminalIdWhitelist={TerminalIdWhitelist}
  end

  self.parkingWhitelist=TerminalIdWhitelist

  return self
end

--- Set parking spot blacklist. These parking spots will *not* be used for spawning.
-- Black listed spots overrule white listed spots.
-- **NOTE** that terminal IDs are not necessarily the same as those displayed in the mission editor!
-- @param #AIRBASE self
-- @param #table TerminalIdBlacklist Table of black listed terminal IDs.
-- @return #AIRBASE self
-- @usage AIRBASE:FindByName("Batumi"):SetParkingSpotBlacklist({2, 3, 4}) --Forbit terminal IDs 2, 3, 4
function AIRBASE:SetParkingSpotBlacklist(TerminalIdBlacklist)

  if TerminalIdBlacklist==nil then
    self.parkingBlacklist={}
    return self
  end

  -- Ensure we got a table.
  if type(TerminalIdBlacklist)~="table" then
    TerminalIdBlacklist={TerminalIdBlacklist}
  end

  self.parkingBlacklist=TerminalIdBlacklist

  return self
end

--- Sets the ATC belonging to an airbase object to be silent and unresponsive. This is useful for disabling the award winning ATC behavior in DCS.
-- Note that this DOES NOT remove the airbase from the list. It just makes it unresponsive and silent to any radio calls to it.
-- @param #AIRBASE self
-- @param #boolean Silent If `true`, enable silent mode. If `false` or `nil`, disable silent mode.
-- @return #AIRBASE self
function AIRBASE:SetRadioSilentMode(Silent)

  -- Get DCS airbase object.
  local airbase=self:GetDCSObject()

  -- Set mode.
  if airbase then
    airbase:setRadioSilentMode(Silent)
  end

  return self
end

--- Check whether or not the airbase has been silenced.
-- @param #AIRBASE self
-- @return #boolean If `true`, silent mode is enabled.
function AIRBASE:GetRadioSilentMode()

  -- Is silent?
  local silent=nil

  -- Get DCS airbase object.
  local airbase=self:GetDCSObject()

  -- Set mode.
  if airbase then
    silent=airbase:getRadioSilentMode()
  end

  return silent
end

--- Get category of airbase.
-- @param #AIRBASE self
-- @return #number Category of airbase from GetDesc().category.
function AIRBASE:GetAirbaseCategory()
  return self.category
end

--- Check if airbase is an airdrome.
-- @param #AIRBASE self
-- @return #boolean If true, airbase is an airdrome.
function AIRBASE:IsAirdrome()
  return self.isAirdrome
end

--- Check if airbase is a helipad.
-- @param #AIRBASE self
-- @return #boolean If true, airbase is a helipad.
function AIRBASE:IsHelipad()
  return self.isHelipad
end

--- Check if airbase is a ship.
-- @param #AIRBASE self
-- @return #boolean If true, airbase is a ship.
function AIRBASE:IsShip()
  return self.isShip
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Parking
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Returns a table of parking data for a given airbase. If the optional parameter *available* is true only available parking will be returned, otherwise all parking at the base is returned. Term types have the following enumerated values:
--
-- * 16 : Valid spawn points on runway
-- * 40 : Helicopter only spawn
-- * 68 : Hardened Air Shelter
-- * 72 : Open/Shelter air airplane only
-- * 104: Open air spawn
--
-- Note that only Caucuses will return 68 as it is the only map currently with hardened air shelters.
-- 104 are also generally larger, but does not guarantee a large aircraft like the B-52 or a C-130 are capable of spawning there.
--
-- Table entries:
--
-- * Term_index is the id for the parking
-- * vTerminal pos is its vec3 position in the world
-- * fDistToRW is the distance to the take-off position for the active runway from the parking.
--
-- @param #AIRBASE self
-- @param #boolean available If true, only available parking spots will be returned.
-- @return #table Table with parking data. See https://wiki.hoggitworld.com/view/DCS_func_getParking
function AIRBASE:GetParkingData(available)
  self:F2(available)

  -- Get DCS airbase object.
  local DCSAirbase=self:GetDCSObject()

  -- Get parking data.
  local parkingdata=nil
  if DCSAirbase then
    parkingdata=DCSAirbase:getParking(available)
  end

  self:T2({parkingdata=parkingdata})
  return parkingdata
end

--- Get number of parking spots at an airbase. Optionally, a specific terminal type can be requested.
-- @param #AIRBASE self
-- @param #AIRBASE.TerminalType termtype Terminal type of which the number of spots is counted. Default all spots but spawn points on runway.
-- @return #number Number of parking spots at this airbase.
function AIRBASE:GetParkingSpotsNumber(termtype)

  -- Get free parking spots data.
  local parkingdata=self:GetParkingData(false)

  local nspots=0
  for _,parkingspot in pairs(parkingdata) do
    if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then
      nspots=nspots+1
    end
  end

  return nspots
end

--- Get number of free parking spots at an airbase.
-- @param #AIRBASE self
-- @param #AIRBASE.TerminalType termtype Terminal type.
-- @param #boolean allowTOAC If true, spots are considered free even though TO_AC is true. Default is off which is saver to avoid spawning aircraft on top of each other. Option might be enabled for FARPS and ships.
-- @return #number Number of free parking spots at this airbase.
function AIRBASE:GetFreeParkingSpotsNumber(termtype, allowTOAC)

  -- Get free parking spots data.
  local parkingdata=self:GetParkingData(true)

  local nfree=0
  for _,parkingspot in pairs(parkingdata) do
    -- Spots on runway are not counted unless explicitly requested.
    if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then
      if (allowTOAC and allowTOAC==true) or parkingspot.TO_AC==false then
        nfree=nfree+1
      end
    end
  end

  return nfree
end

--- Get the coordinates of free parking spots at an airbase.
-- @param #AIRBASE self
-- @param #AIRBASE.TerminalType termtype Terminal type.
-- @param #boolean allowTOAC If true, spots are considered free even though TO_AC is true. Default is off which is saver to avoid spawning aircraft on top of each other. Option might be enabled for FARPS and ships.
-- @return #table Table of coordinates of the free parking spots.
function AIRBASE:GetFreeParkingSpotsCoordinates(termtype, allowTOAC)

  -- Get free parking spots data.
  local parkingdata=self:GetParkingData(true)

  -- Put coordinates of free spots into table.
  local spots={}
  for _,parkingspot in pairs(parkingdata) do
    -- Coordinates on runway are not returned unless explicitly requested.
    if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then
      if (allowTOAC and allowTOAC==true) or parkingspot.TO_AC==false then
        table.insert(spots, COORDINATE:NewFromVec3(parkingspot.vTerminalPos))
      end
    end
  end

  return spots
end

--- Get the coordinates of all parking spots at an airbase. Optionally only those of a specific terminal type. Spots on runways are excluded if not explicitly requested by terminal type.
-- @param #AIRBASE self
-- @param #AIRBASE.TerminalType termtype (Optional) Terminal type. Default all.
-- @return #table Table of coordinates of parking spots.
function AIRBASE:GetParkingSpotsCoordinates(termtype)

  -- Get all parking spots data.
  local parkingdata=self:GetParkingData(false)

  -- Put coordinates of free spots into table.
  local spots={}
  for _,parkingspot in ipairs(parkingdata) do

    -- Coordinates on runway are not returned unless explicitly requested.
    if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then

      -- Get coordinate from Vec3 terminal position.
      local _coord=COORDINATE:NewFromVec3(parkingspot.vTerminalPos)

      -- Add to table.
      table.insert(spots, _coord)
    end

  end

  return spots
end

--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase.
-- @param #AIRBASE self
-- @return#AIRBASE self
function AIRBASE:_InitParkingSpots()

  -- Get parking data of all spots (free or occupied)
  local parkingdata=self:GetParkingData(false)

  -- Init table.
  self.parking={}
  self.parkingByID={}

  self.NparkingTotal=0
  self.NparkingTerminal={}
  for _,terminalType in pairs(AIRBASE.TerminalType) do
    self.NparkingTerminal[terminalType]=0
  end

  -- Get client coordinates.
  local function isClient(coord)
    local clients=_DATABASE.CLIENTS
    for clientname, _client in pairs(clients) do
      local client=_client --Wrapper.Client#CLIENT
      if client and client.SpawnCoord then
        local dist=client.SpawnCoord:Get2DDistance(coord)
        if dist<2 then
          return true, clientname
        end
      end
    end
    return false, nil
  end

  -- Put coordinates of parking spots into table.
  for _,spot in pairs(parkingdata) do

    -- New parking spot.
    local park={} --#AIRBASE.ParkingSpot
    park.Vec3=spot.vTerminalPos
    park.Coordinate=COORDINATE:NewFromVec3(spot.vTerminalPos)
    park.DistToRwy=spot.fDistToRW
    park.Free=nil
    park.TerminalID=spot.Term_Index
    park.TerminalID0=spot.Term_Index_0
    park.TerminalType=spot.Term_Type
    park.TOAC=spot.TO_AC
    park.ClientSpot, park.ClientName=isClient(park.Coordinate)
    park.AirbaseName=self.AirbaseName

    self.NparkingTotal=self.NparkingTotal+1

    for _,terminalType in pairs(AIRBASE.TerminalType) do
      if self._CheckTerminalType(park.TerminalType, terminalType) then
        self.NparkingTerminal[terminalType]=self.NparkingTerminal[terminalType]+1
      end
    end

    self.parkingByID[park.TerminalID]=park
    table.insert(self.parking, park)
  end
  
  -- Runways are not included in total number of parking spots
  self.NparkingTotal=self.NparkingTotal-self.NparkingTerminal[AIRBASE.TerminalType.Runway]

  return self
end

--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase.
-- @param #AIRBASE self
-- @param #number TerminalID Terminal ID.
-- @return #AIRBASE.ParkingSpot Parking spot.
function AIRBASE:_GetParkingSpotByID(TerminalID)
  return self.parkingByID[TerminalID]
end

--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase.
-- @param #AIRBASE self
-- @param #AIRBASE.TerminalType termtype Terminal type.
-- @return #table Table free parking spots. Table has the elements ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy".
function AIRBASE:GetParkingSpotsTable(termtype)

  -- Get parking data of all spots (free or occupied)
  local parkingdata=self:GetParkingData(false)

  -- Get parking data of all free spots.
  local parkingfree=self:GetParkingData(true)

  -- Function to ckeck if any parking spot is free.
  local function _isfree(_tocheck)
    for _,_spot in pairs(parkingfree) do
      if _spot.Term_Index==_tocheck.Term_Index then
        return true
      end
    end
    return false
  end

  -- Put coordinates of parking spots into table.
  local spots={}
  for _,_spot in pairs(parkingdata) do

    if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) then

      local spot=self:_GetParkingSpotByID(_spot.Term_Index)

      if spot then

        spot.Free=_isfree(_spot) -- updated
        spot.TOAC=_spot.TO_AC    -- updated
        spot.AirbaseName=self.AirbaseName

        table.insert(spots, spot)

      else

        self:E(string.format("ERROR: Parking spot %s is nil!", tostring(_spot.Term_Index)))

      end

    end

  end

  return spots
end

--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase.
-- @param #AIRBASE self
-- @param #AIRBASE.TerminalType termtype Terminal type.
-- @param #boolean allowTOAC If true, spots are considered free even though TO_AC is true. Default is off which is saver to avoid spawning aircraft on top of each other. Option might be enabled for FARPS and ships.
-- @return #table Table free parking spots. Table has the elements ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy".
function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC)

  -- Get parking data of all free spots.
  local parkingfree=self:GetParkingData(true)

  -- Put coordinates of free spots into table.
  local freespots={}
  for _,_spot in pairs(parkingfree) do
    if AIRBASE._CheckTerminalType(_spot.Term_Type, termtype) then -- and _spot.Term_Index>0 then --Not sure why I had this in. But caused problems now for a Gas platform where a valid spot was not included!
      if (allowTOAC and allowTOAC==true) or _spot.TO_AC==false then

        local spot=self:_GetParkingSpotByID(_spot.Term_Index)

        spot.Free=true -- updated
        spot.TOAC=_spot.TO_AC    -- updated
        spot.AirbaseName=self.AirbaseName

        table.insert(freespots, spot)

      end
    end
  end

  return freespots
end

--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase.
-- @param #AIRBASE self
-- @param #number TerminalID The terminal ID of the parking spot.
-- @return #AIRBASE.ParkingSpot Table free parking spots. Table has the elements ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy".
function AIRBASE:GetParkingSpotData(TerminalID)

  -- Get parking data.
  local parkingdata=self:GetParkingSpotsTable()

  for _,_spot in pairs(parkingdata) do
    local spot=_spot --#AIRBASE.ParkingSpot
    self:T({TerminalID=spot.TerminalID,TerminalType=spot.TerminalType})
    if TerminalID==spot.TerminalID then
      return spot
    end
  end

  self:E("ERROR: Could not find spot with Terminal ID="..tostring(TerminalID))
  return nil
end

--- Place markers of parking spots on the F10 map.
-- @param #AIRBASE self
-- @param #AIRBASE.TerminalType termtype Terminal type for which marks should be placed.
-- @param #boolean mark If false, do not place markers but only give output to DCS.log file. Default true.
function AIRBASE:MarkParkingSpots(termtype, mark)

  -- Default is true.
  if mark==nil then
    mark=true
  end

  -- Get parking data from getParking() wrapper function.
  local parkingdata=self:GetParkingSpotsTable(termtype)

  -- Get airbase name.
  local airbasename=self:GetName()
  self:E(string.format("Parking spots at %s for terminal type %s:", airbasename, tostring(termtype)))

  for _,_spot in pairs(parkingdata) do

    -- Mark text.
    local _text=string.format("Term Index=%d, Term Type=%d, Free=%s, TOAC=%s, Term ID0=%d, Dist2Rwy=%.1f m",
    _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy)

    -- Create mark on the F10 map.
    if mark then
      _spot.Coordinate:MarkToAll(_text)
    end

    -- Info to DCS.log file.
    local _text=string.format("%s, Term Index=%3d, Term Type=%03d, Free=%5s, TOAC=%5s, Term ID0=%3d, Dist2Rwy=%.1f m",
    airbasename, _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy)
    self:E(_text)
  end
end

--- Seach unoccupied parking spots at the airbase for a specific group of aircraft. The routine also optionally checks for other unit, static and scenery options in a certain radius around the parking spot.
-- The dimension of the spawned aircraft and of the potential obstacle are taken into account. Note that the routine can only return so many spots that are free.
-- @param #AIRBASE self
-- @param Wrapper.Group#GROUP group Aircraft group for which the parking spots are requested.
-- @param #AIRBASE.TerminalType terminaltype (Optional) Only search spots at a specific terminal type. Default is all types execpt on runway.
-- @param #number scanradius (Optional) Radius in meters around parking spot to scan for obstacles. Default 50 m.
-- @param #boolean scanunits (Optional) Scan for units as obstacles. Default true.
-- @param #boolean scanstatics (Optional) Scan for statics as obstacles. Default true.
-- @param #boolean scanscenery (Optional) Scan for scenery as obstacles. Default false. Can cause problems with e.g. shelters.
-- @param #boolean verysafe (Optional) If true, wait until an aircraft has taken off until the parking spot is considered to be free. Defaul false.
-- @param #number nspots (Optional) Number of freeparking spots requested. Default is the number of aircraft in the group.
-- @param #table parkingdata (Optional) Parking spots data table. If not given it is automatically derived from the GetParkingSpotsTable() function.
-- @return #table Table of coordinates and terminal IDs of free parking spots. Each table entry has the elements .Coordinate and .TerminalID.
function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nspots, parkingdata)

  -- Init default
  scanradius=scanradius or 50
  if scanunits==nil then
    scanunits=true
  end
  if scanstatics==nil then
    scanstatics=true
  end
  if scanscenery==nil then
    scanscenery=false
  end
  if verysafe==nil then
    verysafe=false
  end

  -- Function calculating the overlap of two (square) objects.
  local function _overlap(object1, object2, dist)
    local pos1=object1 --Wrapper.Positionable#POSITIONABLE
    local pos2=object2 --Wrapper.Positionable#POSITIONABLE
    local r1=pos1:GetBoundingRadius()
    local r2=pos2:GetBoundingRadius()
    if r1 and r2 then
      local safedist=(r1+r2)*1.1
      local safe = (dist > safedist)
      self:T2(string.format("r1=%.1f r2=%.1f s=%.1f d=%.1f ==> safe=%s", r1, r2, safedist, dist, tostring(safe)))
      return safe
    else
      return true
    end
  end

  -- Get airport name.
  local airport=self:GetName()

  -- Get parking spot data table. This contains free and "non-free" spots.
  -- Note that there are three major issues with the DCS getParking() function:
  -- 1. A spot is considered as NOT free until an aircraft that is present has finally taken off. This might be a bit long especiall at smaller airports.
  -- 2. A "free" spot does not take the aircraft size into accound. So if two big aircraft are spawned on spots next to each other, they might overlap and get destroyed.
  -- 3. The routine return a free spot, if there a static objects placed on the spot.
  parkingdata=parkingdata or self:GetParkingSpotsTable(terminaltype)

  -- Get the aircraft size, i.e. it's longest side of x,z.
  local aircraft = nil -- fix local problem below
  -- SU27 dimensions as default
  local _aircraftsize = 23
  local ax = 23 -- l
  local ay = 7 -- h
  local az = 17 -- w
  if group and group.ClassName == "GROUP" then
    aircraft=group:GetUnit(1)
    if aircraft then
      _aircraftsize, ax,ay,az=aircraft:GetObjectSize()
    end
  end


  -- Number of spots we are looking for. Note that, e.g. grouping can require a number different from the group size!
  local _nspots=nspots or group:GetSize()

  -- Debug info.
  self:T(string.format("%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at terminal type %s.", airport, _nspots, _aircraftsize, ax, ay, az, tostring(terminaltype)))

  -- Table of valid spots.
  local validspots={}
  local nvalid=0

  -- Test other stuff if no parking spot is available.
  local _test=false
  if _test then
    return validspots
  end

  -- Mark all found obstacles on F10 map for debugging.
  local markobstacles=false

  -- Loop over all known parking spots
  for _,parkingspot in pairs(parkingdata) do

    -- Coordinate of the parking spot.
    local _spot=parkingspot.Coordinate   -- Core.Point#COORDINATE
    local _termid=parkingspot.TerminalID

    -- Check terminal type and black/white listed parking spots.
    if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) and self:_CheckParkingLists(_termid) then

      -- Very safe uses the DCS getParking() info to check if a spot is free. Unfortunately, the function returns free=false until the aircraft has actually taken-off.
      if verysafe and (parkingspot.Free==false or parkingspot.TOAC==true) then

        -- DCS getParking() routine returned that spot is not free.
        self:T(string.format("%s: Parking spot id %d NOT free (or aircraft has not taken off yet). Free=%s, TOAC=%s.", airport, parkingspot.TerminalID, tostring(parkingspot.Free), tostring(parkingspot.TOAC)))

      else

        -- Scan a radius of 50 meters around the spot.
        local _,_,_,_units,_statics,_sceneries=_spot:ScanObjects(scanradius, scanunits, scanstatics, scanscenery)

        -- Loop over objects within scan radius.
        local occupied=false

        -- Check all units.
        for _,unit in pairs(_units) do
          local _coord=unit:GetCoordinate()
          local _dist=_coord:Get2DDistance(_spot)
          local _safe=_overlap(aircraft, unit, _dist)

          if markobstacles then
            local l,x,y,z=unit:GetObjectSize()
            _coord:MarkToAll(string.format("Unit %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", unit:GetName(),x,y,z,l,_dist, _termid, tostring(_safe)))
          end

          if scanunits and not _safe then
            occupied=true
          end
        end

        -- Check all statics.
        for _,static in pairs(_statics) do
          local _static=STATIC:Find(static)
          local _vec3=static:getPoint()
          local _coord=COORDINATE:NewFromVec3(_vec3)
          local _dist=_coord:Get2DDistance(_spot)
          local _safe=_overlap(aircraft,_static,_dist)

          if markobstacles then
            local l,x,y,z=_static:GetObjectSize()
            _coord:MarkToAll(string.format("Static %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", static:getName(),x,y,z,l,_dist, _termid, tostring(_safe)))
          end

          if scanstatics and not _safe then
            occupied=true
          end
        end

        -- Check all scenery.
        for _,scenery in pairs(_sceneries) do
          local _scenery=SCENERY:Register(scenery:getTypeName(), scenery)
          local _vec3=scenery:getPoint()
          local _coord=COORDINATE:NewFromVec3(_vec3)
          local _dist=_coord:Get2DDistance(_spot)
          local _safe=_overlap(aircraft,_scenery,_dist)

          if markobstacles then
            local l,x,y,z=scenery:GetObjectSize(scenery)
            _coord:MarkToAll(string.format("Scenery %s\nx=%.1f y=%.1f z=%.1f\nl=%.1f d=%.1f\nspot %d safe=%s", scenery:getTypeName(),x,y,z,l,_dist, _termid, tostring(_safe)))
          end

          if scanscenery and not _safe then
            occupied=true
          end
        end

        -- Now check the already given spots so that we do not put a large aircraft next to one we already assigned a nearby spot.
        for _,_takenspot in pairs(validspots) do
          local _dist=_takenspot.Coordinate:Get2DDistance(_spot)
          local _safe=_overlap(aircraft, aircraft, _dist)
          if not _safe then
            occupied=true
          end
        end

        --_spot:MarkToAll(string.format("Parking spot %d free=%s", parkingspot.TerminalID, tostring(not occupied)))
        if occupied then
          self:T(string.format("%s: Parking spot id %d occupied.", airport, _termid))
        else
          self:T(string.format("%s: Parking spot id %d free.", airport, _termid))
          if nvalid<_nspots then
            table.insert(validspots, {Coordinate=_spot, TerminalID=_termid})
          end
          nvalid=nvalid+1
          self:T(string.format("%s: Parking spot id %d free. Nfree=%d/%d.", airport, _termid, nvalid,_nspots))
        end

      end -- loop over units

      -- We found enough spots.
      if nvalid>=_nspots then
        return validspots
      end
    end -- check terminal type
  end

  -- Retrun spots we found, even if there were not enough.
  return validspots

end

--- Check black and white lists.
-- @param #AIRBASE self
-- @param #number TerminalID Terminal ID to check.
-- @return #boolean `true` if this is a valid spot.
function AIRBASE:_CheckParkingLists(TerminalID)

  -- First check the black list. If we find a match, this spot is forbidden!
  if self.parkingBlacklist and #self.parkingBlacklist>0 then
    for _,terminalID in pairs(self.parkingBlacklist or {}) do
      if terminalID==TerminalID then
        -- This is a invalid spot.
        return false
      end
    end
  end


  -- Check if a whitelist was defined.
  if self.parkingWhitelist and #self.parkingWhitelist>0 then
    for _,terminalID in pairs(self.parkingWhitelist or {}) do
      if terminalID==TerminalID then
        -- This is a valid spot.
        return true
      end
    end
    -- No match ==> invalid spot
    return false
  end

  -- Neither black nor white lists were defined or spot is not in black list.
  return true
end

--- Helper function to check for the correct terminal type including "artificial" ones.
-- @param #number Term_Type Terminal type from getParking routine.
-- @param #AIRBASE.TerminalType termtype Terminal type from AIRBASE.TerminalType enumerator.
-- @return #boolean True if terminal types match.
function AIRBASE._CheckTerminalType(Term_Type, termtype)

  -- Nill check for Term_Type.
  if Term_Type==nil then
    return false
  end

  -- If no terminal type is requested, we return true. BUT runways are excluded unless explicitly requested.
  if termtype==nil then
    if Term_Type==AIRBASE.TerminalType.Runway then
      return false
    else
      return true
    end
  end

  -- Init no match.
  local match=false

  -- Standar case.
  if Term_Type==termtype then
    match=true
  end

  -- Artificial cases. Combination of terminal types.
  if termtype==AIRBASE.TerminalType.OpenMedOrBig then
    if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig then
      match=true
    end
  elseif termtype==AIRBASE.TerminalType.HelicopterUsable then
    if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.HelicopterOnly then
      match=true
     end
  elseif termtype==AIRBASE.TerminalType.FighterAircraft then
    if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.Shelter then
      match=true
    end
  elseif termtype==AIRBASE.TerminalType.FighterAircraftSmall then
    if Term_Type==AIRBASE.TerminalType.OpenMed or Term_Type==AIRBASE.TerminalType.OpenBig or Term_Type==AIRBASE.TerminalType.Shelter or Term_Type==AIRBASE.TerminalType.SmallSizeFighter then
      match=true
    end    
  end

  return match
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Runway
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get runways.
-- @param #AIRBASE self
-- @return #table Runway data.
function AIRBASE:GetRunways()
  return self.runways or {}
end

--- Get runway by its name.
-- @param #AIRBASE self
-- @param #string Name Name of the runway, e.g. "31" or "21L".
-- @return #AIRBASE.Runway Runway data.
function AIRBASE:GetRunwayByName(Name)

  if Name==nil then
    return
  end

  if Name then
    for _,_runway in pairs(self.runways) do
      local runway=_runway --#AIRBASE.Runway

      -- Name including L or R, e.g. "31L".
      local name=self:GetRunwayName(runway)
      self:T("Check Runway Name: "..name)
      if name==Name:upper() then
        return runway
      end
    end
  end

  self:E("ERROR: Could not find runway with name "..tostring(Name))
  return nil
end

--- Init runways.
-- @param #AIRBASE self
-- @param #boolean IncludeInverse If `true` or `nil`, include inverse runways.
-- @return #table Runway data.
function AIRBASE:_InitRunways(IncludeInverse)

  -- Default is true.
  if IncludeInverse==nil then
    IncludeInverse=true
  end

  -- Runway table.
  local Runways={}

  --- Function to create a runway data table.
  local function _createRunway(name, course, width, length, center)
    self:T("Create Runway: name = "..name)
    -- Bearing in rad.
    local bearing=-1*course

    -- Heading in degrees.
    local heading=math.deg(bearing)

    -- Data table.
    local runway={} --#AIRBASE.Runway

    local namefromheading = math.floor(heading/10)

    if self.AirbaseName == AIRBASE.Syria.Beirut_Rafic_Hariri and math.abs(namefromheading-name) > 1 then
      runway.name=string.format("%02d", tonumber(namefromheading))
    else
     runway.name=string.format("%02d", tonumber(name))
     --self:I("RunwayName: "..runway.name)
    end

    --runway.name=string.format("%02d", tonumber(name))
    runway.magheading=tonumber(runway.name)*10
    runway.idx=runway.magheading
    runway.heading=heading
    runway.width=width or 0
    runway.length=length or 0
    runway.center=COORDINATE:NewFromVec3(center)

    -- Ensure heading is [0,360]
    if runway.heading>360 then
      runway.heading=runway.heading-360
    elseif runway.heading<0 then
      runway.heading=runway.heading+360
    end

    -- For example at Nellis, DCS reports two runways, i.e. 03 and 21, BUT the "course" of both is -0.700 rad = 40 deg!
    -- As a workaround, I check the difference between the "magnetic" heading derived from the name and the true heading.
    -- If this is too large then very likely the "inverse" heading is the one we are looking for.
    if math.abs(runway.heading-runway.magheading)>60 then
      self:T(string.format("WARNING: Runway %s: heading=%.1f magheading=%.1f", runway.name, runway.heading, runway.magheading))
      runway.heading=runway.heading-180
    end

    -- Ensure heading is [0,360]
    if runway.heading>360 then
      runway.heading=runway.heading-360
    elseif runway.heading<0 then
      runway.heading=runway.heading+360
    end

    -- Start and endpoint of runway.
    runway.position=runway.center:Translate(-runway.length/2, runway.heading)
    runway.endpoint=runway.center:Translate( runway.length/2, runway.heading)

    local init=runway.center:GetVec3()
    local width = runway.width/2
    local L2=runway.length/2

    local offset1 = {x = init.x + (math.cos(bearing + math.pi) * L2), y = init.z + (math.sin(bearing + math.pi) * L2)}
    local offset2 = {x = init.x - (math.cos(bearing + math.pi) * L2), y = init.z - (math.sin(bearing + math.pi) * L2)}

    local points={}
    points[1] = {x = offset1.x + (math.cos(bearing + (math.pi/2)) * width), y = offset1.y + (math.sin(bearing + (math.pi/2)) * width)}
    points[2] = {x = offset1.x + (math.cos(bearing - (math.pi/2)) * width), y = offset1.y + (math.sin(bearing - (math.pi/2)) * width)}
    points[3] = {x = offset2.x + (math.cos(bearing - (math.pi/2)) * width), y = offset2.y + (math.sin(bearing - (math.pi/2)) * width)}
    points[4] = {x = offset2.x + (math.cos(bearing + (math.pi/2)) * width), y = offset2.y + (math.sin(bearing + (math.pi/2)) * width)}

    -- Runway zone.
    runway.zone=ZONE_POLYGON_BASE:New(string.format("%s Runway %s", self.AirbaseName, runway.name), points)

    return runway
  end


  -- Get DCS object.
  local airbase=self:GetDCSObject()

  if airbase then


    -- Get DCS runways.
    local runways=airbase:getRunways()

    -- Debug info.
    self:T2(runways)

    if runways and #runways>0 then

      -- Loop over runways.
      for _,rwy in pairs(runways) do

        -- Debug info.
        self:T(rwy)

        -- Get runway data.
        local runway=_createRunway(rwy.Name, rwy.course, rwy.width, rwy.length, rwy.position) --#AIRBASE.Runway

        -- Add to table.
        table.insert(Runways, runway)

        -- Include "inverse" runway.
        if IncludeInverse then

          -- Create "inverse".
          local idx=tonumber(runway.name)
          local name2=tostring(idx-18)
          if idx<18 then
            name2=tostring(idx+18)
          end

          -- Create "inverse" runway.
          local runway=_createRunway(name2, rwy.course-math.pi, rwy.width, rwy.length, rwy.position) --#AIRBASE.Runway

          -- Add inverse to table.
          table.insert(Runways, runway)

        end

      end
      
    else
    
      -- No runways
      self.runways={}
      return {}      

    end

  end

  -- Look for identical (parallel) runways, e.g. 03L and 03R at Nellis.
  local rpairs={}
  for i,_ri in pairs(Runways) do
    local ri=_ri --#AIRBASE.Runway
    for j,_rj in pairs(Runways) do
      local rj=_rj --#AIRBASE.Runway
      if i<j then
        if ri.name==rj.name then
          rpairs[i]=j
        end
      end
    end
  end

  local function isLeft(a, b, c)
    --return ((b.x - a.x)*(c.z - a.z) - (b.z - a.z)*(c.x - a.x)) > 0
    return ((b.z - a.z)*(c.x - a.x) - (b.x - a.x)*(c.z - a.z)) > 0
  end

  for i,j in pairs(rpairs) do
    local ri=Runways[i] --#AIRBASE.Runway
    local rj=Runways[j] --#AIRBASE.Runway

    -- Draw arrow.
    --ri.center:ArrowToAll(rj.center)

    local c0=ri.center

    -- Vector in the direction of the runway.
    local a=UTILS.VecTranslate(c0, 1000, ri.heading)

    -- Vector from runway i to runway j.
    local b=UTILS.VecSubstract(rj.center, ri.center)
    b=UTILS.VecAdd(ri.center, b)

    -- Check if rj is left of ri.
    local left=isLeft(c0, a, b)

    --env.info(string.format("Found pair %s: i=%d, j=%d, left==%s", ri.name, i, j, tostring(left)))

    if left then
      ri.isLeft=false
      rj.isLeft=true
    else
      ri.isLeft=true
      rj.isLeft=false
    end

    --break
  end

  -- Set runways.
  self.runways=Runways

  return Runways
end


--- Get runways data. Only for airdromes!
-- @param #AIRBASE self
-- @param #number magvar (Optional) Magnetic variation in degrees.
-- @param #boolean mark (Optional) Place markers with runway data on F10 map.
-- @return #table Runway data.
function AIRBASE:GetRunwayData(magvar, mark)

  -- Runway table.
  local runways={}

  if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME then
    return {}
  end

  -- Get spawn points on runway. These can be used to determine the runway heading.
  local runwaycoords=self:GetParkingSpotsCoordinates(AIRBASE.TerminalType.Runway)

  -- Debug: For finding the numbers of the spawn points belonging to each runway.
  if false then
    for i,_coord in pairs(runwaycoords) do
      local coord=_coord --Core.Point#COORDINATE
      coord:Translate(100, 0):MarkToAll("Runway i="..i)
    end
  end

  -- Magnetic declination.
  magvar=magvar or UTILS.GetMagneticDeclination()

  -- Number of runways.
  local N=#runwaycoords
  local N2=N/2
  local exception=false

  -- Airbase name.
  local name=self:GetName()


  -- Exceptions
  if name==AIRBASE.Nevada.Jean_Airport or
     name==AIRBASE.Nevada.Creech_AFB   or
     name==AIRBASE.PersianGulf.Abu_Dhabi_International_Airport or
     name==AIRBASE.PersianGulf.Dubai_Intl or
     name==AIRBASE.PersianGulf.Shiraz_International_Airport or
     name==AIRBASE.PersianGulf.Kish_International_Airport or
     name==AIRBASE.MarianaIslands.Andersen_AFB then

    -- 1-->4, 2-->3, 3-->2, 4-->1
    exception=1

  elseif UTILS.GetDCSMap()==DCSMAP.Syria and N>=2 and
    name~=AIRBASE.Syria.Minakh and
    name~=AIRBASE.Syria.Damascus and
    name~=AIRBASE.Syria.Khalkhalah and
    name~=AIRBASE.Syria.Marj_Ruhayyil and
    name~=AIRBASE.Syria.Beirut_Rafic_Hariri then

    -- 1-->3, 2-->4, 3-->1, 4-->2
    exception=2

  end

  --- Function returning the index of the runway coordinate belonding to the given index i.
  local function f(i)

    local j

    if exception==1 then

      j=N-(i-1)  -- 1-->4, 2-->3

    elseif exception==2 then

      if i<=N2 then
        j=i+N2  -- 1-->3, 2-->4
      else
        j=i-N2  -- 3-->1, 4-->3
      end

    else

      if i%2==0 then
        j=i-1  -- even 2-->1, 4-->3
      else
        j=i+1  -- odd  1-->2, 3-->4
      end

    end

    -- Special case where there is no obvious order.
    if name==AIRBASE.Syria.Beirut_Rafic_Hariri then
      if i==1 then
        j=3
      elseif i==2 then
        j=6
      elseif i==3 then
        j=1
      elseif i==4 then
        j=5
      elseif i==5 then
        j=4
      elseif i==6 then
        j=2
      end
    end

    if name==AIRBASE.Syria.Ramat_David then
      if i==1 then
        j=4
      elseif i==2 then
        j=6
      elseif i==3 then
        j=5
      elseif i==4 then
        j=1
      elseif i==5 then
        j=3
      elseif i==6 then
        j=2
      end
    end

    return j
  end


  for i=1,N do

    -- Get the other spawn point coordinate.
    local j=f(i)

    -- Debug info.
    --env.info(string.format("Runway i=%s j=%s (N=%d #runwaycoord=%d)", tostring(i), tostring(j), N, #runwaycoords))

    -- Coordinates of the two runway points.
    local c1=runwaycoords[i] --Core.Point#COORDINATE
    local c2=runwaycoords[j] --Core.Point#COORDINATE

    -- Heading of runway.
    local hdg=c1:HeadingTo(c2)

    -- Runway ID: heading=070° ==> idx="07"
    local idx=string.format("%02d", UTILS.Round((hdg-magvar)/10, 0))

    -- Runway table.
    local runway={} --#AIRBASE.Runway
    runway.heading=hdg
    runway.idx=idx
    runway.magheading=idx
    runway.length=c1:Get2DDistance(c2)
    runway.position=c1
    runway.endpoint=c2

    -- Debug info.
    self:T(string.format("Airbase %s: Adding runway id=%s, heading=%03d, length=%d m i=%d j=%d", self:GetName(), runway.idx, runway.heading, runway.length, i, j))

    -- Debug mark
    if mark then
      runway.position:MarkToAll(string.format("Runway %s: true heading=%03d (magvar=%d), length=%d m, i=%d, j=%d", runway.idx, runway.heading, magvar, runway.length, i, j))
    end

    -- Add runway.
    table.insert(runways, runway)

  end
  
    -- Look for identical (parallel) runways, e.g. 03L and 03R at Nellis.
  local rpairs={}
  for i,_ri in pairs(runways) do
    local ri=_ri --#AIRBASE.Runway
    for j,_rj in pairs(runways) do
      local rj=_rj --#AIRBASE.Runway
      if i<j then
        if ri.name==rj.name then
          rpairs[i]=j
        end
      end
    end
  end

  local function isLeft(a, b, c)
    --return ((b.x - a.x)*(c.z - a.z) - (b.z - a.z)*(c.x - a.x)) > 0
    return ((b.z - a.z)*(c.x - a.x) - (b.x - a.x)*(c.z - a.z)) > 0
  end

  for i,j in pairs(rpairs) do
    local ri=runways[i] --#AIRBASE.Runway
    local rj=runways[j] --#AIRBASE.Runway

    -- Draw arrow.
    --ri.center:ArrowToAll(rj.center)

    local c0=ri.position

    -- Vector in the direction of the runway.
    local a=UTILS.VecTranslate(c0, 1000, ri.heading)

    -- Vector from runway i to runway j.
    local b=UTILS.VecSubstract(rj.position, ri.position)
    b=UTILS.VecAdd(ri.position, b)

    -- Check if rj is left of ri.
    local left=isLeft(c0, a, b)

    --env.info(string.format("Found pair %s: i=%d, j=%d, left==%s", ri.name, i, j, tostring(left)))

    if left then
      ri.isLeft=false
      rj.isLeft=true
    else
      ri.isLeft=true
      rj.isLeft=false
    end

    --break
  end

  return runways
end

--- Set the active runway for landing and takeoff.
-- @param #AIRBASE self
-- @param #string Name Name of the runway, e.g. "31" or "02L" or "90R". If not given, the runway is determined from the wind direction.
-- @param #boolean PreferLeft If `true`, perfer the left runway. If `false`, prefer the right runway. If `nil` (default), do not care about left or right.
function AIRBASE:SetActiveRunway(Name, PreferLeft)

  self:SetActiveRunwayTakeoff(Name, PreferLeft)

  self:SetActiveRunwayLanding(Name,PreferLeft)

end

--- Set the active runway for landing.
-- @param #AIRBASE self
-- @param #string Name Name of the runway, e.g. "31" or "02L" or "90R". If not given, the runway is determined from the wind direction.
-- @param #boolean PreferLeft If `true`, perfer the left runway. If `false`, prefer the right runway. If `nil` (default), do not care about left or right.
-- @return #AIRBASE.Runway The active runway for landing.
function AIRBASE:SetActiveRunwayLanding(Name, PreferLeft)

  local runway=self:GetRunwayByName(Name)

  if not runway then
    runway=self:GetRunwayIntoWind(PreferLeft)
  end

  if runway then
    self:T(string.format("%s: Setting active runway for landing as %s", self.AirbaseName, self:GetRunwayName(runway)))
  else
    self:E("ERROR: Could not set the runway for landing!")
  end

  self.runwayLanding=runway

  return runway
end

--- Get the active runways.
-- @param #AIRBASE self
-- @return #AIRBASE.Runway The active runway for landing.
-- @return #AIRBASE.Runway The active runway for takeoff.
function AIRBASE:GetActiveRunway()
  return self.runwayLanding, self.runwayTakeoff
end


--- Get the active runway for landing.
-- @param #AIRBASE self
-- @return #AIRBASE.Runway The active runway for landing.
function AIRBASE:GetActiveRunwayLanding()
  return self.runwayLanding
end

--- Get the active runway for takeoff.
-- @param #AIRBASE self
-- @return #AIRBASE.Runway The active runway for takeoff.
function AIRBASE:GetActiveRunwayTakeoff()
  return self.runwayTakeoff
end


--- Set the active runway for takeoff.
-- @param #AIRBASE self
-- @param #string Name Name of the runway, e.g. "31" or "02L" or "90R". If not given, the runway is determined from the wind direction.
-- @param #boolean PreferLeft If `true`, perfer the left runway. If `false`, prefer the right runway. If `nil` (default), do not care about left or right.
-- @return #AIRBASE.Runway The active runway for landing.
function AIRBASE:SetActiveRunwayTakeoff(Name, PreferLeft)

  local runway=self:GetRunwayByName(Name)

  if not runway then
    runway=self:GetRunwayIntoWind(PreferLeft)
  end

  if runway then
    self:T(string.format("%s: Setting active runway for takeoff as %s", self.AirbaseName, self:GetRunwayName(runway)))
  else
    self:E("ERROR: Could not set the runway for takeoff!")
  end

  self.runwayTakeoff=runway

  return runway
end


--- Get the runway where aircraft would be taking of or landing into the direction of the wind.
-- NOTE that this requires the wind to be non-zero as set in the mission editor.
-- @param #AIRBASE self
-- @param #boolean PreferLeft If `true`, perfer the left runway. If `false`, prefer the right runway. If `nil` (default), do not care about left or right.
-- @return #AIRBASE.Runway Active runway data table.
function AIRBASE:GetRunwayIntoWind(PreferLeft)

  -- Get runway data.
  local runways=self:GetRunways()

  -- Get wind vector.
  local Vwind=self:GetCoordinate():GetWindWithTurbulenceVec3()
  local norm=UTILS.VecNorm(Vwind)

  -- Active runway number.
  local iact=1

  -- Check if wind is blowing (norm>0).
  if norm>0 then

    -- Normalize wind (not necessary).
    Vwind.x=Vwind.x/norm
    Vwind.y=0
    Vwind.z=Vwind.z/norm

    -- Loop over runways.
    local dotmin=nil
    for i                                                                                                                                                                                                             ,_runway in pairs(runways) do
      local runway=_runway --#AIRBASE.Runway                                                                    

      if PreferLeft==nil or PreferLeft==runway.isLeft then

        -- Angle in rad.
        local alpha=math.rad(runway.heading)

        -- Runway vector.
        local Vrunway={x=math.cos(alpha), y=0, z=math.sin(alpha)}

        -- Dot product: parallel component of the two vectors.
        local dot=UTILS.VecDot(Vwind, Vrunway)

        -- New min?
        if dotmin==nil or dot<dotmin then
          dotmin=dot
          iact=i
        end

      end

    end
  else
    self:E("WARNING: Norm of wind is zero! Cannot determine runway based on wind direction")
  end

  return runways[iact]
end

--- Get name of a given runway, e.g. "31L".
-- @param #AIRBASE self
-- @param #AIRBASE.Runway Runway The runway. Default is the active runway.
-- @param #boolean LongLeftRight If `true`, return "Left" or "Right" instead of "L" or "R".
-- @return #string Name of the runway or "XX" if it could not be found.
function AIRBASE:GetRunwayName(Runway, LongLeftRight)

  Runway=Runway or self:GetActiveRunway()

  local name="XX"
  if Runway then
    name=Runway.name
    if Runway.isLeft==true then
      if LongLeftRight then
        name=name.." Left"
      else
        name=name.."L"
      end
    elseif Runway.isLeft==false then
      if LongLeftRight then
        name=name.." Right"
      else
        name=name.."R"
      end
    end
  end

  return name
end

--- Function that checks if at leat one unit of a group has been spawned close to a spawn point on the runway.
-- @param #AIRBASE self
-- @param Wrapper.Group#GROUP group Group to be checked.
-- @param #number radius Radius around the spawn point to be checked. Default is 50 m.
-- @param #boolean despawn If true, the group is destroyed.
-- @return #boolean True if group is within radius around spawn points on runway.
function AIRBASE:CheckOnRunWay(group, radius, despawn)

  -- Default radius.
  radius=radius or 50

  -- We only check at real airbases (not FARPS or ships).
  if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME then
    return false
  end

  if group and group:IsAlive() then

    -- Debug.
    self:T(string.format("%s, checking if group %s is on runway?",self:GetName(), group:GetName()))

    -- Get coordinates on runway.
    local runwaypoints=self:GetParkingSpotsCoordinates(AIRBASE.TerminalType.Runway)

    -- Get units of group.
    local units=group:GetUnits()

    -- Loop over units.
    for _,_unit in pairs(units) do

      local unit=_unit --Wrapper.Unit#UNIT

      -- Check if unit is alive and not in air.
      if unit and unit:IsAlive() and not unit:InAir() then
        self:T(string.format("%s, checking if unit %s is on runway?",self:GetName(), unit:GetName()))

        -- Loop over runway spawn points.
        for _i,_coord in pairs(runwaypoints) do

          -- Distance between unit and spawn pos.
          local dist=unit:GetCoordinate():Get2DDistance(_coord)

          -- Mark unit spawn points for debugging.
          --unit:GetCoordinate():MarkToAll(string.format("unit %s distance to rwy %d = %d",unit:GetName(),_i, dist))

          -- Check if unit is withing radius.
          if dist<radius  then
            self:E(string.format("%s, unit %s of group %s was spawned on runway #%d. Distance %.1f < radius %.1f m. Despawn = %s.", self:GetName(), unit:GetName(), group:GetName(),_i, dist, radius, tostring(despawn)))
            --unit:FlareRed()
            if despawn then
              group:Destroy(true)
            end
            return true
          else
            self:T(string.format("%s, unit %s of group %s was NOT spawned on runway #%d. Distance %.1f > radius %.1f m. Despawn = %s.", self:GetName(), unit:GetName(), group:GetName(),_i, dist, radius, tostring(despawn)))
            --unit:FlareGreen()
          end

        end
      else
        self:T(string.format("%s, checking if unit %s of group %s is on runway. Unit is NOT alive.",self:GetName(), unit:GetName(), group:GetName()))
      end
    end
  else
    self:T(string.format("%s, checking if group %s is on runway. Group is NOT alive.",self:GetName(), group:GetName()))
  end

  return false
end

--- Get category of airbase.
-- @param #AIRBASE self
-- @return #number Category of airbase from GetDesc().category.
function AIRBASE:GetCategory()
  return self.category
end

--- Get category name of airbase.
-- @param #AIRBASE self
-- @return #string Category of airbase, i.e. Airdrome, Ship, or Helipad
function AIRBASE:GetCategoryName()
  return AIRBASE.CategoryName[self.category]
end
--- **Wrapper** - SCENERY models scenery within the DCS simulator.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: **Applevangelist**, **funkyfranky**
-- 
-- ===
-- 
-- @module Wrapper.Scenery
-- @image Wrapper_Scenery.JPG


--- SCENERY Class
-- @type SCENERY
-- @field #string ClassName Name of the class.
-- @field #string SceneryName Name of the scenery object.
-- @field DCS#Object SceneryObject DCS scenery object.
-- @field #number Life0 Initial life points.
-- @field #table Properties
-- @extends Wrapper.Positionable#POSITIONABLE


--- Wrapper class to handle Scenery objects that are defined on the map.
-- 
-- The @{Wrapper.Scenery#SCENERY} class is a wrapper class to handle the DCS Scenery objects:
-- 
--  * Wraps the DCS Scenery objects.
--  * Support all DCS Scenery APIs.
--  * Enhance with Scenery specific APIs not in the DCS API set.
--  
--  @field #SCENERY
SCENERY = {
  ClassName = "SCENERY",
}

--- Register scenery object as POSITIONABLE.
--@param #SCENERY self
--@param #string SceneryName Scenery name.
--@param DCS#Object SceneryObject DCS scenery object.
--@return #SCENERY Scenery object.
function SCENERY:Register( SceneryName, SceneryObject )

  local self = BASE:Inherit( self, POSITIONABLE:New( SceneryName ) )
  
  self.SceneryName = tostring(SceneryName)
  
  self.SceneryObject = SceneryObject
  
  if self.SceneryObject and self.SceneryObject.getLife then -- fix some objects do not have all functions
    self.Life0 = self.SceneryObject:getLife() or 0
  else
    self.Life0 = 0
  end
  
  self.Properties = {}
  
  return self
end

--- Returns the value of the scenery with the given PropertyName, or nil if no matching property exists.
-- @param #SCENERY self
-- @param #string PropertyName The name of a the QuadZone Property from the scenery assignment to be retrieved.
-- @return #string The Value of the QuadZone Property from the scenery assignment with the given PropertyName, or nil if absent.
function SCENERY:GetProperty(PropertyName)
  return self.Properties[PropertyName]
end

--- Checks if the value of the scenery with the given PropertyName exists.
-- @param #SCENERY self
-- @param #string PropertyName The name of a the QuadZone Property from the scenery assignment to be retrieved.
-- @return #boolean Outcome True if it exists, else false.
function SCENERY:HasProperty(PropertyName)
  return self.Properties[PropertyName] ~= nil and true or false
end

--- Returns the scenery Properties table.
-- @param #SCENERY self
-- @return #table The Key:Value table of QuadZone properties of the zone from the scenery assignment .
function SCENERY:GetAllProperties()
  return self.Properties
end

--- Set a scenery property
-- @param #SCENERY self
-- @param #string PropertyName
-- @param #string PropertyValue
-- @return #SCENERY self
function SCENERY:SetProperty(PropertyName, PropertyValue)
  self.Properties[PropertyName] = PropertyValue
  return self
end

--- Obtain object name.
--@param #SCENERY self
--@return #string Name
function SCENERY:GetName()
  return self.SceneryName
end

--- Obtain DCS Object from the SCENERY Object.
--@param #SCENERY self
--@return DCS#Object DCS scenery object.
function SCENERY:GetDCSObject()
  return self.SceneryObject
end

--- Get current life points from the SCENERY Object. Note - Some scenery objects always have 0 life points.
--  **CAVEAT**: Some objects change their life value or "hitpoints" **after** the first hit. Hence we will adjust the life0 value to 120% 
--  of the last life value if life exceeds life0 (initial life) at any point. Thus will will get a smooth percentage decrease, if you use this e.g. as success 
--  criteria for a bombing task.
--@param #SCENERY self
--@return #number life
function SCENERY:GetLife()
  local life = 0
  if self.SceneryObject and self.SceneryObject.getLife then
    life = self.SceneryObject:getLife()
    if life > self.Life0 then
      self.Life0 = math.floor(life * 1.2)
    end
  end
  return life
end

--- Get initial life points of the SCENERY Object.
--@param #SCENERY self
--@return #number life
function SCENERY:GetLife0()
  return self.Life0 or 0
end

--- Check if SCENERY Object is alive. Note - Some scenery objects always have 0 life points.
--@param #SCENERY self
--@param #number Threshold (Optional) If given, SCENERY counts as alive above this relative life in percent (1..100).
--@return #number life
function SCENERY:IsAlive(Threshold)
  if not Threshold then
    return self:GetLife() >= 1 and true or false
  else
    return self:GetRelativeLife() > Threshold and true or false
  end
end 

--- Check if SCENERY Object is dead. Note - Some scenery objects always have 0 life points.
--@param #SCENERY self
--@param #number Threshold (Optional) If given, SCENERY counts as dead below this relative life in percent (1..100).
--@return #number life
function SCENERY:IsDead(Threshold)
  if not Threshold then
    return self:GetLife() < 1 and true or false
  else
    return self:GetRelativeLife() <= Threshold and true or false
  end
end 

--- Get SCENERY relative life in percent, e.g. 75. Note - Some scenery objects always have 0 life points.
--@param #SCENERY self
--@return #number rlife
function SCENERY:GetRelativeLife()
  local life = self:GetLife()
  local life0 = self:GetLife0()
  if life == 0 or life0 == 0 then return 0 end
  local rlife = math.floor((life/life0)*100)
  return rlife
end

--- Get the threat level of a SCENERY object. Always 0 as scenery does not pose a threat to anyone.
--@param #SCENERY self
--@return #number Threat level 0.
--@return #string  "Scenery".
function SCENERY:GetThreatLevel()
  return 0, "Scenery"
end

--- Find a SCENERY object from its name or id. Since SCENERY isn't registered in the Moose database (just too many objects per map), we need to do a scan first
-- to find the correct object.
--@param #SCENERY self
--@param #string Name The name/id of the scenery object as taken from the ME. Ex. '595785449'
--@param Core.Point#COORDINATE Coordinate Where to find the scenery object
--@param #number Radius (optional) Search radius around coordinate, defaults to 100
--@return #SCENERY Scenery Object or `nil` if it cannot be found
function SCENERY:FindByName(Name, Coordinate, Radius, Role)

  local radius = Radius or 100
  local name = Name or "unknown"
  local scenery = nil
  
  ---
  -- @param Core.Point#COORDINATE coordinate
  -- @param #number radius
  -- @param #string name
  local function SceneryScan(scoordinate, sradius, sname)
    if scoordinate ~= nil then
      local Vec2 = scoordinate:GetVec2()
      local scanzone = ZONE_RADIUS:New("Zone-"..sname,Vec2,sradius,true)
      scanzone:Scan({Object.Category.SCENERY})
      local scanned = scanzone:GetScannedSceneryObjects()
      local rscenery = nil -- Wrapper.Scenery#SCENERY
      for _,_scenery in pairs(scanned) do
        local scenery = _scenery -- Wrapper.Scenery#SCENERY
        if tostring(scenery.SceneryName) == tostring(sname) then
          rscenery = scenery
          if Role then rscenery:SetProperty("ROLE",Role) end
          break
        end
      end
      return rscenery
    end
    return nil
  end
  
  if Coordinate then
    --BASE:I("Coordinate Scenery Scan")
    scenery = SceneryScan(Coordinate, radius, name)
  end

  return scenery  
end

--- Find a SCENERY object from its name or id. Since SCENERY isn't registered in the Moose database (just too many objects per map), we need to do a scan first
-- to find the correct object.
--@param #SCENERY self
--@param #string Name The name or id of the scenery object as taken from the ME. Ex. '595785449'
--@param Core.Zone#ZONE_BASE Zone Where to find the scenery object. Can be handed as zone name.
--@param #number Radius (optional) Search radius around coordinate, defaults to 100
--@return #SCENERY Scenery Object or `nil` if it cannot be found
function SCENERY:FindByNameInZone(Name, Zone, Radius)   
  local radius = Radius or 100
  local name = Name or "unknown"
  if type(Zone) == "string" then
    Zone = ZONE:FindByName(Zone)
  end
  local coordinate = Zone:GetCoordinate()
  return self:FindByName(Name,coordinate,Radius,Zone:GetProperty("ROLE"))
end

--- Find a SCENERY object from its zone name. Since SCENERY isn't registered in the Moose database (just too many objects per map), we need to do a scan first
-- to find the correct object.
--@param #SCENERY self
--@param #string ZoneName The name of the scenery zone as created with a right-click on the map in the mission editor and select "assigned to...". Can be handed over as ZONE object.
--@return #SCENERY First found Scenery Object or `nil` if it cannot be found
function SCENERY:FindByZoneName( ZoneName )
  local zone = ZoneName -- Core.Zone#ZONE_BASE
  if type(ZoneName) == "string" then
    zone = ZONE:FindByName(ZoneName)  -- Core.Zone#ZONE_POLYGON
  end
  local _id = zone:GetProperty('OBJECT ID')
  --local properties = zone:GetAllProperties() or {}
  --BASE:I(string.format("Object ID is: %s",_id or "none"))
  --BASE:T("Object ID ".._id)
  if not _id then
    -- this zone has no object ID
    BASE:E("**** Zone without object ID: "..ZoneName.." | Type: "..tostring(zone.ClassName))
    if string.find(zone.ClassName,"POLYGON") then
      zone:Scan({Object.Category.SCENERY})
      local scanned = zone:GetScannedSceneryObjects()
      for _,_scenery in (scanned) do
        local scenery = _scenery -- Wrapper.Scenery#SCENERY
        if scenery:IsAlive() then
          local role = zone:GetProperty("ROLE")
          if role then scenery:SetProperty("ROLE",role) end
          return scenery
        end
      end
      return nil
    else
      return self:FindByName(_id, zone:GetCoordinate(),nil,zone:GetProperty("ROLE"))
    end
  else
    return self:FindByName(_id, zone:GetCoordinate(),nil,zone:GetProperty("ROLE"))
  end
end

--- Scan and find all SCENERY objects from a zone by zone-name. Since SCENERY isn't registered in the Moose database (just too many objects per map), we need to do a scan first
-- to find the correct object.
--@param #SCENERY self
--@param #string ZoneName The name of the zone, can be handed as ZONE_RADIUS or ZONE_POLYGON object
--@return #table of SCENERY Objects, or `nil` if nothing found
function SCENERY:FindAllByZoneName( ZoneName )
  local zone = ZoneName -- Core.Zone#ZONE_RADIUS
  if type(ZoneName) == "string" then
    zone = ZONE:FindByName(ZoneName) 
  end
  local _id = zone:GetProperty('OBJECT ID')
  --local properties = zone:GetAllProperties() or {}
  if not _id then
    -- this zone has no object ID
    --BASE:E("**** Zone without object ID: "..ZoneName.." | Type: "..tostring(zone.ClassName))
    zone:Scan({Object.Category.SCENERY})
    local scanned = zone:GetScannedSceneryObjects()
    if #scanned > 0 then
      return scanned
    else
      return nil
    end
  else
    local obj = self:FindByName(_id, zone:GetCoordinate(),nil,zone:GetProperty("ROLE"))
    if obj then
      return {obj}
    else
      return nil
    end 
  end
end

--- SCENERY objects cannot be destroyed via the API (at the punishment of game crash).
--@param #SCENERY self
--@return #SCENERY self
function SCENERY:Destroy()
  return self
end
--- **Wrapper** - Markers On the F10 map.
--
-- **Main Features:**
--
--    * Convenient handling of markers via multiple user API functions.
--    * Update text and position of marker easily via scripting.
--    * Delay creation and removal of markers via (optional) parameters.
--    * Retrieve data such as text and coordinate.
--    * Marker specific FSM events when a marker is added, removed or changed.
--    * Additional FSM events when marker text or position is changed.
--
-- ===
--
-- ### Author: **funkyfranky**
-- @module Wrapper.Marker
-- @image MOOSE_Core.JPG

--- Marker class.
-- @type MARKER
-- @field #string ClassName Name of the class.
-- @field #boolean Debug Debug mode. Messages to all about status.
-- @field #string lid Class id string for output to DCS log file.
-- @field #number mid Marker ID.
-- @field Core.Point#COORDINATE coordinate Coordinate of the mark.
-- @field #string text Text displayed in the mark panel.
-- @field #string message Message displayed when the mark is added.
-- @field #boolean readonly Marker is read-only.
-- @field #number coalition Coalition to which the marker is displayed.
-- @extends Core.Fsm#FSM

--- Just because...
--
-- ===
--
-- # The MARKER Class Idea
--
-- The MARKER class simplifies creating, updating and removing of markers on the F10 map.
--
-- # Create a Marker
--
--     -- Create a MARKER object at Batumi with a trivial text.
--     local Coordinate = AIRBASE:FindByName( "Batumi" ):GetCoordinate()
--     mymarker = MARKER:New( Coordinate, "I am Batumi Airfield" )
--
-- Now this does **not** show the marker yet. We still need to specify to whom it is shown. There are several options, i.e.
-- show the marker to everyone, to a specific coalition only, or only to a specific group.
--
-- ## For Everyone
--
-- If the marker should be visible to everyone, you can use the :ToAll() function.
--
--     mymarker = MARKER:New( Coordinate, "I am Batumi Airfield" ):ToAll()
--
-- ## For a Coalition
--
-- If the maker should be visible to a specific coalition, you can use the :ToCoalition() function.
--
--     mymarker = MARKER:New( Coordinate , "I am Batumi Airfield" ):ToCoalition( coalition.side.BLUE )
--     
-- This would show the marker only to the Blue coalition.
--
-- ## For a Group
--
--     mymarker = MARKER:New( Coordinate , "Target Location" ):ToGroup( tankGroup )
--
-- # Removing a Marker
--     mymarker:Remove(60)
-- This removes the marker after 60 seconds
--
-- # Updating a Marker
--
-- The marker text and coordinate can be updated easily as shown below.
--
-- However, note that **updating involves to remove and recreate the marker if either text or its coordinate is changed**.
-- *This is a DCS scripting engine limitation.*
--
-- ## Update Text
--
-- If you created a marker "mymarker" as shown above, you can update the displayed test by
--
--     mymarker:UpdateText( "I am the new text at Batumi" )
--
-- The update can also be delayed by, e.g. 90 seconds, using
--
--     mymarker:UpdateText( "I am the new text at Batumi", 90 )
--
-- ## Update Coordinate
--
-- If you created a marker "mymarker" as shown above, you can update its coordinate on the F10 map by
--
--     mymarker:UpdateCoordinate( NewCoordinate )
--
-- The update can also be delayed by, e.g. 60 seconds, using
--
--     mymarker:UpdateCoordinate( NewCoordinate , 60 )
--
-- # Retrieve Data
--
-- The important data as the displayed text and the coordinate of the marker can be retrieved easily.
--
-- ## Text
--
--     local text  =mymarker:GetText()
--     env.info( "Marker Text = " .. text )
--
-- ## Coordinate
--
--     local Coordinate = mymarker:GetCoordinate()
--     env.info( "Marker Coordinate LL DSM = " .. Coordinate:ToStringLLDMS() )
--
--
-- # FSM Events
--
-- Moose creates additional events, so called FSM event, when markers are added, changed, removed, and text or the coordinate is updated.
--
-- These events can be captured and used for processing via OnAfter functions as shown below.
--
-- ## Added
--
-- ## Changed
--
-- ## Removed
--
-- ## TextUpdate
--
-- ## CoordUpdate
--
--
-- # Examples
--
-- @field #MARKER
MARKER = {
  ClassName = "MARKER",
  Debug = false,
  lid = nil,
  mid = nil,
  coordinate = nil,
  text = nil,
  message = nil,
  readonly = nil,
  coalition = nil,
}

--- Marker ID. Running number.
_MARKERID = 0

--- Marker class version.
-- @field #string version
MARKER.version="0.1.1"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: User "Get" functions. E.g., :GetCoordinate()
-- DONE: Add delay to user functions.
-- DONE: Handle events.
-- DONE: Create FSM events.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new MARKER class object.
-- @param #MARKER self
-- @param Core.Point#COORDINATE Coordinate Coordinate where to place the marker.
-- @param #string Text Text displayed on the mark panel.
-- @return #MARKER self
function MARKER:New( Coordinate, Text )

  -- Inherit everything from FSM class.
  local self = BASE:Inherit( self, FSM:New() ) -- #MARKER

  self.coordinate=UTILS.DeepCopy(Coordinate)

  self.text = Text

  -- Defaults
  self.readonly = false
  self.message = ""

  -- New marker ID. This is not the one of the actual marker.
  _MARKERID = _MARKERID + 1

  self.myid = _MARKERID

  -- Log ID.
  self.lid = string.format( "Marker #%d | ", self.myid )

  -- Start State.
  self:SetStartState( "Invisible" )

  -- Add FSM transitions.
  --                 From State  -->   Event      -->     To State
  self:AddTransition( "Invisible", "Added", "Visible" ) -- Marker was added.
  self:AddTransition( "Visible", "Removed", "Invisible" ) -- Marker was removed.
  self:AddTransition( "*", "Changed", "*" ) -- Marker was changed.

  self:AddTransition( "*", "TextUpdate", "*" ) -- Text updated.
  self:AddTransition( "*", "CoordUpdate", "*" ) -- Coordinates updated.

  --- Triggers the FSM event "Added".
  -- @function [parent=#MARKER] Added
  -- @param #MARKER self
  -- @param Core.Event#EVENTDATA EventData Event data table.

  --- Triggers the delayed FSM event "Added".
  -- @function [parent=#MARKER] __Added
  -- @param #MARKER self
  -- @param Core.Event#EVENTDATA EventData Event data table.

  --- On after "Added" event user function.
  -- @function [parent=#MARKER] OnAfterAdded
  -- @param #MARKER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Event#EVENTDATA EventData Event data table.

  --- Triggers the FSM event "Removed".
  -- @function [parent=#MARKER] Removed
  -- @param #MARKER self
  -- @param Core.Event#EVENTDATA EventData Event data table.

  --- Triggers the delayed FSM event "Removed".
  -- @function [parent=#MARKER] __Removed
  -- @param #MARKER self
  -- @param Core.Event#EVENTDATA EventData Event data table.

  --- On after "Removed" event user function.
  -- @function [parent=#MARKER] OnAfterRemoved
  -- @param #MARKER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Event#EVENTDATA EventData Event data table.

  --- Triggers the FSM event "Changed".
  -- @function [parent=#MARKER] Changed
  -- @param #MARKER self
  -- @param Core.Event#EVENTDATA EventData Event data table.

  --- Triggers the delayed FSM event "Changed".
  -- @function [parent=#MARKER] __Changed
  -- @param #MARKER self
  -- @param Core.Event#EVENTDATA EventData Event data table.

  --- On after "Changed" event user function.
  -- @function [parent=#MARKER] OnAfterChanged
  -- @param #MARKER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Event#EVENTDATA EventData Event data table.

  --- Triggers the FSM event "TextUpdate".
  -- @function [parent=#MARKER] TextUpdate
  -- @param #MARKER self
  -- @param #string Text The new text.

  --- Triggers the delayed FSM event "TextUpdate".
  -- @function [parent=#MARKER] __TextUpdate
  -- @param #MARKER self
  -- @param #string Text The new text.

  --- On after "TextUpdate" event user function.
  -- @function [parent=#MARKER] OnAfterTextUpdate
  -- @param #MARKER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string Text The new text.

  --- Triggers the FSM event "CoordUpdate".
  -- @function [parent=#MARKER] CoordUpdate
  -- @param #MARKER self
  -- @param Core.Point#COORDINATE Coordinate The new Coordinate.

  --- Triggers the delayed FSM event "CoordUpdate".
  -- @function [parent=#MARKER] __CoordUpdate
  -- @param #MARKER self
  -- @param Core.Point#COORDINATE Coordinate The updated Coordinate.

  --- On after "CoordUpdate" event user function.
  -- @function [parent=#MARKER] OnAfterCoordUpdate
  -- @param #MARKER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Point#COORDINATE Coordinate The updated Coordinate.

  -- Handle events.
  self:HandleEvent( EVENTS.MarkAdded )
  self:HandleEvent( EVENTS.MarkRemoved )
  self:HandleEvent( EVENTS.MarkChange )

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User API Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Marker is readonly. Text cannot be changed and marker cannot be removed. The will not update the marker in the game, Call MARKER:Refresh to update state.
-- @param #MARKER self
-- @return #MARKER self
function MARKER:ReadOnly()

  self.readonly = true

  return self
end

--- Marker is read and write. Text cannot be changed and marker cannot be removed. The will not update the marker in the game, Call MARKER:Refresh to update state.
-- @param #MARKER self
-- @return #MARKER self
function MARKER:ReadWrite()

  self.readonly=false

  return self
end

--- Set message that is displayed on screen if the marker is added.
-- @param #MARKER self
-- @param #string Text Message displayed when the marker is added.
-- @return #MARKER self
function MARKER:Message( Text )

  self.message = Text or ""

  return self
end

--- Place marker visible for everyone.
-- @param #MARKER self
-- @param #number Delay (Optional) Delay in seconds, before the marker is created.
-- @return #MARKER self
function MARKER:ToAll( Delay )

  if Delay and Delay > 0 then
    self:ScheduleOnce( Delay, MARKER.ToAll, self )
  else

    self.toall = true
    self.tocoalition = nil
    self.coalition = nil
    self.togroup = nil
    self.groupname = nil
    self.groupid = nil

    -- First remove an existing mark.
    if self.shown then
      self:Remove()
    end

    self.mid = UTILS.GetMarkID()

    -- Call DCS function.
    trigger.action.markToAll( self.mid, self.text, self.coordinate:GetVec3(), self.readonly, self.message )

  end

  return self
end

--- Place marker visible for a specific coalition only.
-- @param #MARKER self
-- @param #number Coalition Coalition 1=Red, 2=Blue, 0=Neutral. See `coalition.side.RED`.
-- @param #number Delay (Optional) Delay in seconds, before the marker is created.
-- @return #MARKER self
function MARKER:ToCoalition( Coalition, Delay )

  if Delay and Delay > 0 then
    self:ScheduleOnce( Delay, MARKER.ToCoalition, self, Coalition )
  else

    self.coalition = Coalition

    self.tocoalition = true
    self.toall = false
    self.togroup = false
    self.groupname = nil
    self.groupid = nil

    -- First remove an existing mark.
    if self.shown then
      self:Remove()
    end

    self.mid = UTILS.GetMarkID()

    -- Call DCS function.
    trigger.action.markToCoalition( self.mid, self.text, self.coordinate:GetVec3(), self.coalition, self.readonly, self.message )

  end

  return self
end

--- Place marker visible for the blue coalition only.
-- @param #MARKER self
-- @param #number Delay (Optional) Delay in seconds, before the marker is created.
-- @return #MARKER self
function MARKER:ToBlue( Delay )
  self:ToCoalition( coalition.side.BLUE, Delay )
  return self
end

--- Place marker visible for the blue coalition only.
-- @param #MARKER self
-- @param #number Delay (Optional) Delay in seconds, before the marker is created.
-- @return #MARKER self
function MARKER:ToRed( Delay )
  self:ToCoalition( coalition.side.RED, Delay )
  return self
end

--- Place marker visible for the neutral coalition only.
-- @param #MARKER self
-- @param #number Delay (Optional) Delay in seconds, before the marker is created.
-- @return #MARKER self
function MARKER:ToNeutral( Delay )
  self:ToCoalition( coalition.side.NEUTRAL, Delay )
  return self
end

--- Place marker visible for a specific group only.
-- @param #MARKER self
-- @param Wrapper.Group#GROUP Group The group to which the marker is displayed.
-- @param #number Delay (Optional) Delay in seconds, before the marker is created.
-- @return #MARKER self
function MARKER:ToGroup( Group, Delay )

  if Delay and Delay > 0 then
    self:ScheduleOnce( Delay, MARKER.ToGroup, self, Group )
  else

    -- Check if group exists.
    if Group and Group:IsAlive() ~= nil then

      self.groupid = Group:GetID()

      if self.groupid then

        self.groupname = Group:GetName()

        self.togroup = true
        self.tocoalition = nil
        self.coalition = nil
        self.toall = nil

        -- First remove an existing mark.
        if self.shown then
          self:Remove()
        end

        self.mid = UTILS.GetMarkID()

        -- Call DCS function.
        trigger.action.markToGroup( self.mid, self.text, self.coordinate:GetVec3(), self.groupid, self.readonly, self.message )

      end

    else
      -- TODO: Warning!
    end

  end

  return self
end

--- Update the text displayed on the mark panel.
-- @param #MARKER self
-- @param #string Text Updated text.
-- @param #number Delay (Optional) Delay in seconds, before the marker is created.
-- @return #MARKER self
function MARKER:UpdateText( Text, Delay )

  if Delay and Delay > 0 then
    self:ScheduleOnce( Delay, MARKER.UpdateText, self, Text )
  else

    self.text = tostring( Text )

    self:Refresh()

    self:TextUpdate( tostring( Text ) )

  end

  return self
end

--- Update the coordinate where the marker is displayed.
-- @param #MARKER self
-- @param Core.Point#COORDINATE Coordinate The new coordinate.
-- @param #number Delay (Optional) Delay in seconds, before the marker is created.
-- @return #MARKER self
function MARKER:UpdateCoordinate( Coordinate, Delay )

  if Delay and Delay > 0 then
    self:ScheduleOnce( Delay, MARKER.UpdateCoordinate, self, Coordinate )
  else

    self.coordinate = Coordinate

    self:Refresh()

    self:CoordUpdate( Coordinate )

  end

  return self
end

--- Refresh the marker.
-- @param #MARKER self
-- @param #number Delay (Optional) Delay in seconds, before the marker is created.
-- @return #MARKER self
function MARKER:Refresh( Delay )

  if Delay and Delay > 0 then
    self:ScheduleOnce( Delay, MARKER.Refresh, self )
  else

    if self.toall then

      self:ToAll()

    elseif self.tocoalition then

      self:ToCoalition( self.coalition )

    elseif self.togroup then

      local group = GROUP:FindByName( self.groupname )

      self:ToGroup( group )

    else
      self:E( self.lid .. "ERROR: unknown To in :Refresh()!" )
    end

  end

  return self
end

--- Remove a marker.
-- @param #MARKER self
-- @param #number Delay (Optional) Delay in seconds, before the marker is removed.
-- @return #MARKER self
function MARKER:Remove( Delay )

  if Delay and Delay > 0 then
    self:ScheduleOnce( Delay, MARKER.Remove, self )
  else

    if self.shown then

      -- Call DCS function.
      trigger.action.removeMark( self.mid )

    end

  end

  return self
end

--- Get position of the marker.
-- @param #MARKER self
-- @return Core.Point#COORDINATE The coordinate of the marker.
function MARKER:GetCoordinate()
  return self.coordinate
end

--- Get text that is displayed in the marker panel.
-- @param #MARKER self
-- @return #string Marker text.
function MARKER:GetText()
  return self.text
end

--- Set text that is displayed in the marker panel. Note this does not show the marker.
-- @param #MARKER self
-- @param #string Text Marker text. Default is an empty string "".
-- @return #MARKER self
function MARKER:SetText( Text )
  self.text = Text and tostring( Text ) or ""
  return self
end

--- Check if marker is currently visible on the F10 map.
-- @param #MARKER self
-- @return #boolean True if the marker is currently visible.
function MARKER:IsVisible()
  return self:Is( "Visible" )
end

--- Check if marker is currently invisible on the F10 map.
-- @param #MARKER self
-- @return
function MARKER:IsInvisible()
  return self:Is( "Invisible" )
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Event function when a MARKER is added.
-- @param #MARKER self
-- @param Core.Event#EVENTDATA EventData
function MARKER:OnEventMarkAdded( EventData )

  if EventData and EventData.MarkID then

    local MarkID = EventData.MarkID

    self:T3( self.lid .. string.format( "Captured event MarkAdded for Mark ID=%s", tostring( MarkID ) ) )

    if MarkID == self.mid then

      self.shown = true

      self:Added( EventData )

    end

  end

end

--- Event function when a MARKER is removed.
-- @param #MARKER self
-- @param Core.Event#EVENTDATA EventData
function MARKER:OnEventMarkRemoved( EventData )

  if EventData and EventData.MarkID then

    local MarkID = EventData.MarkID

    local MarkID=EventData.MarkID

    self:T3(self.lid..string.format("Captured event MarkRemoved for Mark ID=%s", tostring(MarkID)))

    if MarkID == self.mid then

      self.shown = false

      self:Removed( EventData )

    end

  end

end

--- Event function when a MARKER changed.
-- @param #MARKER self
-- @param Core.Event#EVENTDATA EventData
function MARKER:OnEventMarkChange( EventData )

  if EventData and EventData.MarkID then

    local MarkID = EventData.MarkID

    self:T3( self.lid .. string.format( "Captured event MarkChange for Mark ID=%s", tostring( MarkID ) ) )

    if MarkID == self.mid then

    local MarkID=EventData.MarkID

    self:T3(self.lid..string.format("Captured event MarkChange for Mark ID=%s", tostring(MarkID)))

    if MarkID==self.mid then

      self.text=tostring(EventData.MarkText)

      self:Changed(EventData)

    end

  end

end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Event Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "Added" event.
-- @param #MARKER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Event#EVENTDATA EventData Event data table.
function MARKER:onafterAdded( From, Event, To, EventData )

  -- Debug info.
  local text = string.format( "Captured event MarkAdded for myself:\n" )
  text = text .. string.format( "Marker ID  = %s\n", tostring( EventData.MarkID ) )
  text = text .. string.format( "Coalition  = %s\n", tostring( EventData.MarkCoalition ) )
  text = text .. string.format( "Group  ID  = %s\n", tostring( EventData.MarkGroupID ) )
  text = text .. string.format( "Initiator  = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody" )
  text = text .. string.format( "Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere" )
  text = text .. string.format( "Text:          \n%s", tostring( EventData.MarkText ) )
  self:T2( self.lid .. text )

end

--- On after "Removed" event.
-- @param #MARKER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Event#EVENTDATA EventData Event data table.
function MARKER:onafterRemoved( From, Event, To, EventData )

  -- Debug info.
  local text = string.format( "Captured event MarkRemoved for myself:\n" )
  text = text .. string.format( "Marker ID  = %s\n", tostring( EventData.MarkID ) )
  text = text .. string.format( "Coalition  = %s\n", tostring( EventData.MarkCoalition ) )
  text = text .. string.format( "Group  ID  = %s\n", tostring( EventData.MarkGroupID ) )
  text = text .. string.format( "Initiator  = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody" )
  text = text .. string.format( "Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere" )
  text = text .. string.format( "Text:          \n%s", tostring( EventData.MarkText ) )
  self:T2( self.lid .. text )

end

--- On after "Changed" event.
-- @param #MARKER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Event#EVENTDATA EventData Event data table.
function MARKER:onafterChanged( From, Event, To, EventData )

  -- Debug info.
  local text = string.format( "Captured event MarkChange for myself:\n" )
  text = text .. string.format( "Marker ID  = %s\n", tostring( EventData.MarkID ) )
  text = text .. string.format( "Coalition  = %s\n", tostring( EventData.MarkCoalition ) )
  text = text .. string.format( "Group  ID  = %s\n", tostring( EventData.MarkGroupID ) )
  text = text .. string.format( "Initiator  = %s\n", EventData.IniUnit and EventData.IniUnit:GetName() or "Nobody" )
  text = text .. string.format( "Coordinate = %s\n", EventData.MarkCoordinate and EventData.MarkCoordinate:ToStringLLDMS() or "Nowhere" )
  text = text .. string.format( "Text:          \n%s", tostring( EventData.MarkText ) )
  self:T2( self.lid .. text )

end

--- On after "TextUpdate" event.
-- @param #MARKER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string Text The updated text, displayed in the mark panel.
function MARKER:onafterTextUpdate( From, Event, To, Text )

  self:T( self.lid .. string.format( "New Marker Text:\n%s", Text ) )

end

--- On after "CoordUpdate" event.
-- @param #MARKER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Coordinate The updated coordinates.
function MARKER:onafterCoordUpdate( From, Event, To, Coordinate )

  self:T( self.lid .. string.format( "New Marker Coordinate in LL DMS: %s", Coordinate:ToStringLLDMS() ) )

end
--- **Wrapper** - Weapon functions.
--
-- ## Main Features:
--
--    * Convenient access to DCS API functions
--    * Track weapon and get impact position
--    * Get launcher and target of weapon
--    * Define callback function when weapon impacts
--    * Define callback function when tracking weapon
--    * Mark impact points on F10 map
--    * Put coloured smoke on impact points
--
-- ===
--
-- ## Additional Material:
--
-- * **Demo Missions:** [GitHub](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/Wrapper/Weapon)
-- * **YouTube videos:** None
-- * **Guides:** None
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ===
-- @module Wrapper.Weapon
-- @image Wrapper_Weapon.png


--- WEAPON class.
-- @type WEAPON
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field DCS#Weapon weapon The DCS weapon object.
-- @field #string name Name of the weapon object.
-- @field #string typeName Type name of the weapon.
-- @field #number category Weapon category 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB, 4=TORPEDO (Weapon.Category.X).
-- @field #number categoryMissile Missile category 0=AAM, 1=SAM, 2=BM, 3=ANTI_SHIP, 4=CRUISE, 5=OTHER (Weapon.MissileCategory.X).
-- @field #number coalition Coalition ID.
-- @field #number country Country ID.
-- @field DCS#Desc desc Descriptor table.
-- @field DCS#Desc guidance Missile guidance descriptor.
-- @field DCS#Unit launcher Launcher DCS unit.
-- @field Wrapper.Unit#UNIT launcherUnit Launcher Unit.
-- @field #string launcherName Name of launcher unit.
-- @field #number dtTrack Time step in seconds for tracking scheduler.
-- @field #function impactFunc Callback function for weapon impact.
-- @field #table impactArg Optional arguments for the impact callback function.
-- @field #function trackFunc Callback function when weapon is tracked and alive.
-- @field #table trackArg Optional arguments for the track callback function.
-- @field DCS#Vec3 vec3 Last known 3D position vector of the tracked weapon.
-- @field DCS#Position3 pos3 Last known 3D position and direction vector of the tracked weapon.
-- @field Core.Point#COORDINATE coordinate Coordinate object of the weapon. Can be used in other classes.
-- @field DCS#Vec3 impactVec3 Impact 3D vector.
-- @field Core.Point#COORDINATE impactCoord Impact coordinate.
-- @field #number trackScheduleID Tracking scheduler ID. Can be used to remove/destroy the scheduler function.
-- @field #boolean tracking If `true`, scheduler will keep tracking. Otherwise, function will return nil and stop tracking.
-- @field #boolean impactMark If `true`, the impact point is marked on the F10 map. Requires tracking to be started.
-- @field #boolean impactSmoke If `true`, the impact point is marked by smoke. Requires tracking to be started.
-- @field #number impactSmokeColor Colour of impact point smoke.
-- @field #boolean impactDestroy If `true`, destroy weapon before impact. Requires tracking to be started and sufficiently small time step.
-- @field #number impactDestroyDist Distance in meters to the estimated impact point. If smaller, then weapon is destroyed.
-- @field #number distIP Distance in meters for the intercept point estimation.
-- @field Wrapper.Unit#UNIT target Last known target.
-- @extends Wrapper.Positionable#POSITIONABLE

--- *In the long run, the sharpest weapon of all is a kind and gentle spirit.* -- Anne Frank
--
-- ===
--
-- # The WEAPON Concept
--
-- The WEAPON class offers an easy-to-use wrapper interface to all DCS API functions.
--
-- Probably, the most striking highlight is that the position of the weapon can be tracked and its impact position can be determined, which is not
-- possible with the native DCS scripting engine functions.
--
-- **Note** that this wrapper class is different from most others as weapon objects cannot be found with a DCS API function like `getByName()`.
-- They can only be found in DCS events like the "Shot" event, where the weapon object is contained in the event data.
--
-- # Tracking
--
-- The status of the weapon can be tracked with the @{#WEAPON.StartTrack} function. This function will try to determin the position of the weapon in (normally) relatively
-- small time steps. The time step can be set via the @{#WEAPON.SetTimeStepTrack} function and is by default set to 0.01 seconds.
--
-- Once the position cannot be retrieved any more, the weapon has impacted (or was destroyed otherwise) and the last known position is safed as the impact point.
-- The impact point can be accessed with the @{#WEAPON.GetImpactVec3} or @{#WEAPON.GetImpactCoordinate} functions.
--
-- ## Impact Point Marking
--
-- You can mark the impact point on the F10 map with @{#WEAPON.SetMarkImpact}.
--
-- You can also trigger coloured smoke at the impact point via @{#WEAPON.SetSmokeImpact}.
--
-- ## Callback functions
--
-- It is possible to define functions that are called during the tracking of the weapon and upon impact, which help you to customize further actions.
--
-- ### Callback on Impact
--
-- The function called on impact can be set with @{#WEAPON.SetFuncImpact}
--
-- ### Callback when Tracking
--
-- The function called each time the weapon status is tracked can be set with @{#WEAPON.SetFuncTrack}
--
-- # Target
--
-- If the weapon has a specific target, you can get it with the @{#WEAPON.GetTarget} function. Note that the object, which is returned can vary. Normally, it is a UNIT
-- but it could also be a STATIC object.
--
-- Also note that the weapon does not always have a target, it can loose a target and re-aquire it and the target might change to another unit.
--
-- You can get the target name with the @{#WEAPON.GetTargetName} function.
--
-- The distance to the target is returned by the @{#WEAPON.GetTargetDistance} function.
--
-- # Category
--
-- The category (bomb, rocket, missile, shell, torpedo) of the weapon can be retrieved with the @{#WEAPON.GetCategory} function.
--
-- You can check if the weapon is a
--
-- * bomb with @{#WEAPON.IsBomb}
-- * rocket with @{#WEAPON.IsRocket}
-- * missile with @{#WEAPON.IsMissile}
-- * shell with @{#WEAPON.IsShell}
-- * torpedo with @{#WEAPON.IsTorpedo}
--
-- # Parameters
--
-- You can get various parameters of the weapon, *e.g.*
--
-- * position: @{#WEAPON.GetVec3}, @{#WEAPON.GetVec2 }, @{#WEAPON.GetCoordinate}
-- * speed: @{#WEAPON.GetSpeed}
-- * coalition: @{#WEAPON.GetCoalition}
-- * country: @{#WEAPON.GetCountry}
--
-- # Dependencies
--
-- This class is used (at least) in the MOOSE classes:
--
-- * RANGE (to determine the impact points of bombs and missiles)
-- * ARTY (to destroy and replace shells with smoke or illumination)
-- * FOX (to destroy the missile before it hits the target)
--
-- @field #WEAPON
WEAPON = {
  ClassName          = "WEAPON",
  verbose            =     0,
}


--- WEAPON class version.
-- @field #string version
WEAPON.version="0.1.0"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: A lot...
-- TODO: Destroy before impact.
-- TODO: Monitor target.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new WEAPON object from the DCS weapon object.
-- @param #WEAPON self
-- @param DCS#Weapon WeaponObject The DCS weapon object.
-- @return #WEAPON self
function WEAPON:New(WeaponObject)

  -- Nil check on object.
  if WeaponObject==nil then
    env.error("ERROR: Weapon object does NOT exist")
    return nil
  end

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, POSITIONABLE:New("Weapon")) -- #WEAPON

  -- Set DCS weapon object.
  self.weapon=WeaponObject

  -- Descriptors containing a lot of info.
  self.desc=WeaponObject:getDesc()

  -- This gives the object category which is always Object.Category.WEAPON!
  --self.category=WeaponObject:getCategory()

  -- Weapon category: 0=SHELL, 1=MISSILE, 2=ROCKET, 3=BOMB (Weapon.Category.X)
  self.category = self.desc.category

  if self:IsMissile() and self.desc.missileCategory then
    self.categoryMissile=self.desc.missileCategory
    if self.desc.guidance then
      self.guidance = self.desc.guidance
    end
  end

  -- Get type name.
  self.typeName=WeaponObject:getTypeName() or "Unknown Type"

  -- Get name of object. Usually a number like "1234567".
  self.name=WeaponObject:getName()

  -- Get coaliton of weapon.
  self.coalition=WeaponObject:getCoalition()

  -- Get country of weapon.
  self.country=WeaponObject:getCountry()

  -- Get DCS unit of the launcher.
  self.launcher=WeaponObject:getLauncher()

  -- Get launcher of weapon.
  self.launcherName="Unknown Launcher"
  if self.launcher then
    self.launcherName=self.launcher:getName()
    self.launcherUnit=UNIT:Find(self.launcher)
  end

  -- Init the coordinate of the weapon from that of the launcher.
  self.coordinate=COORDINATE:NewFromVec3(self.launcher:getPoint())

  -- Set log ID.
  self.lid=string.format("[%s] %s | ", self.typeName, self.name)

  if self.launcherUnit then
    self.releaseHeading = self.launcherUnit:GetHeading()
    self.releaseAltitudeASL = self.launcherUnit:GetAltitude()
    self.releaseAltitudeAGL = self.launcherUnit:GetAltitude(true)
    self.releaseCoordinate = self.launcherUnit:GetCoordinate()
    self.releasePitch = self.launcherUnit:GetPitch()
  end

  -- Set default parameters
  self:SetTimeStepTrack()
  self:SetDistanceInterceptPoint()

  -- Debug info.
  local text=string.format("Weapon v%s\nName=%s, TypeName=%s, Category=%s, Coalition=%d, Country=%d, Launcher=%s",
  self.version, self.name, self.typeName, self.category, self.coalition, self.country, self.launcherName)
  self:T(self.lid..text)

  -- Descriptors.
  self:T2(self.desc)

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User API Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set verbosity level.
-- @param #WEAPON self
-- @param #number VerbosityLevel Level of output (higher=more). Default 0.
-- @return #WEAPON self
function WEAPON:SetVerbosity(VerbosityLevel)
  self.verbose=VerbosityLevel or 0
  return self
end

--- Set track position time step.
-- @param #WEAPON self
-- @param #number TimeStep Time step in seconds when the position is updated. Default 0.01 sec ==> 100 evaluations per second.
-- @return #WEAPON self
function WEAPON:SetTimeStepTrack(TimeStep)
  self.dtTrack=TimeStep or 0.01
  return self
end

--- Set distance of intercept point for estimated impact point.
-- If the weapon cannot be tracked any more, the intercept point from its last known position and direction is used to get
-- a better approximation of the impact point. Can be useful when using longer time steps in the tracking and still achieve
-- a good result on the impact point.
-- It uses the DCS function [getIP](https://wiki.hoggitworld.com/view/DCS_func_getIP).
-- @param #WEAPON self
-- @param #number Distance Distance in meters. Default is 50 m. Set to 0 to deactivate.
-- @return #WEAPON self
function WEAPON:SetDistanceInterceptPoint(Distance)
  self.distIP=Distance or 50
  return self
end

--- Mark impact point on the F10 map. This requires that the tracking has been started.
-- @param #WEAPON self
-- @param #boolean Switch If `true` or nil, impact is marked.
-- @return #WEAPON self
function WEAPON:SetMarkImpact(Switch)

  if Switch==false then
    self.impactMark=false
  else
    self.impactMark=true
  end

  return self
end


--- Put smoke on impact point. This requires that the tracking has been started.
-- @param #WEAPON self
-- @param #boolean Switch If `true` or nil, impact is smoked.
-- @param #number SmokeColor Color of smoke. Default is `SMOKECOLOR.Red`.
-- @return #WEAPON self
function WEAPON:SetSmokeImpact(Switch, SmokeColor)

  if Switch==false then
    self.impactSmoke=false
  else
    self.impactSmoke=true
  end

  self.impactSmokeColor=SmokeColor or SMOKECOLOR.Red

  return self
end

--- Set callback function when weapon is tracked and still alive. The first argument will be the WEAPON object.
-- Note that this can be called many times per second. So be careful for performance reasons.
-- @param #WEAPON self
-- @param #function FuncTrack Function called during tracking.
-- @param ... Optional function arguments.
-- @return #WEAPON self
function WEAPON:SetFuncTrack(FuncTrack, ...)
  self.trackFunc=FuncTrack
  self.trackArg=arg or {}
  return self
end

--- Set callback function when weapon impacted or was destroyed otherwise, *i.e.* cannot be tracked any more.
-- @param #WEAPON self
-- @param #function FuncImpact Function called once the weapon impacted.
-- @param ... Optional function arguments.
-- @return #WEAPON self
--
-- @usage
-- -- Function called on impact.
-- local function OnImpact(Weapon)
--   Weapon:GetImpactCoordinate():MarkToAll("Impact Coordinate of weapon")
-- end
--
-- -- Set which function to call.
-- myweapon:SetFuncImpact(OnImpact)
--
-- -- Start tracking.
-- myweapon:Track()
--
function WEAPON:SetFuncImpact(FuncImpact, ...)
  self.impactFunc=FuncImpact
  self.impactArg=arg or {}
  return self
end


--- Get the unit that launched the weapon.
-- @param #WEAPON self
-- @return Wrapper.Unit#UNIT Laucher unit.
function WEAPON:GetLauncher()
  return self.launcherUnit
end

--- Get the target, which the weapon is guiding to.
-- @param #WEAPON self
-- @return Wrapper.Object#OBJECT The target object, which can be a UNIT or STATIC object.
function WEAPON:GetTarget()

  local target=nil --Wrapper.Object#OBJECT

  if self.weapon then

    -- Get the DCS target object, which can be a Unit, Weapon, Static, Scenery, Airbase.
    local object=self.weapon:getTarget()

    if object then

      -- Get object category.
      local category=Object.getCategory(object)

      --Target name
      local name=object:getName()
      
      if name then 
      
        -- Debug info.
        self:T(self.lid..string.format("Got Target Object %s, category=%d", name, category))
  
        if category==Object.Category.UNIT then
  
          target=UNIT:FindByName(name)
  
        elseif category==Object.Category.STATIC then
  
          target=STATIC:FindByName(name, false)
  
        elseif category==Object.Category.SCENERY then
          self:E(self.lid..string.format("ERROR: Scenery target not implemented yet!"))
        else
          self:E(self.lid..string.format("ERROR: Object category=%d is not implemented yet!", category))
        end
      end
    end
  end

  return target
end

--- Get the distance to the current target the weapon is guiding to.
-- @param #WEAPON self
-- @param #function ConversionFunction (Optional) Conversion function from meters to desired unit, *e.g.* `UTILS.MpsToKmph`.
-- @return #number Distance from weapon to target in meters.
function WEAPON:GetTargetDistance(ConversionFunction)

  -- Get the target of the weapon.
  local target=self:GetTarget() --Wrapper.Unit#UNIT

  local distance=nil
  if target then

    -- Current position of target.
    local tv3=target:GetVec3()

    -- Current position of weapon.
    local wv3=self:GetVec3()

    if tv3 and wv3 then
      distance=UTILS.VecDist3D(tv3, wv3)

      if ConversionFunction then
        distance=ConversionFunction(distance)
      end

    end

  end

  return distance
end


--- Get name the current target the weapon is guiding to.
-- @param #WEAPON self
-- @return #string Name of the target or "None" if no target.
function WEAPON:GetTargetName()

  -- Get the target of the weapon.
  local target=self:GetTarget() --Wrapper.Unit#UNIT

  local name="None"
  if target then
    name=target:GetName()
  end

  return name
end

--- Get velocity vector of weapon.
-- @param #WEAPON self
-- @return DCS#Vec3 Velocity vector with x, y and z components in meters/second.
function WEAPON:GetVelocityVec3()
  local Vvec3=nil
  if self.weapon then
    Vvec3=self.weapon:getVelocity()
  end
  return Vvec3
end

--- Get speed of weapon.
-- @param #WEAPON self
-- @param #function ConversionFunction (Optional) Conversion function from m/s to desired unit, *e.g.* `UTILS.MpsToKmph`.
-- @return #number Speed in meters per second.
function WEAPON:GetSpeed(ConversionFunction)

  local speed=nil

  if self.weapon then

    local v=self:GetVelocityVec3()

    speed=UTILS.VecNorm(v)

    if ConversionFunction then
      speed=ConversionFunction(speed)
    end

  end

  return speed
end

--- Get the current 3D position vector.
-- @param #WEAPON self
-- @return DCS#Vec3 Current position vector in 3D.
function WEAPON:GetVec3()

  local vec3=nil
  if self.weapon then
    vec3=self.weapon:getPoint()
  end

  return vec3
end


--- Get the current 2D position vector.
-- @param #WEAPON self
-- @return DCS#Vec2 Current position vector in 2D.
function WEAPON:GetVec2()

  local vec3=self:GetVec3()

  if vec3 then

    local vec2={x=vec3.x, y=vec3.z}

    return vec2
  end

  return nil
end

--- Get type name.
-- @param #WEAPON self
-- @return #string The type name.
function WEAPON:GetTypeName()
  return self.typeName
end

--- Get coalition.
-- @param #WEAPON self
-- @return #number Coalition ID.
function WEAPON:GetCoalition()
  return self.coalition
end

--- Get country.
-- @param #WEAPON self
-- @return #number Country ID.
function WEAPON:GetCountry()
  return self.country
end

--- Get DCS object.
-- @param #WEAPON self
-- @return DCS#Weapon The weapon object.
function WEAPON:GetDCSObject()
  -- This polymorphic function is used in Wrapper.Identifiable#IDENTIFIABLE
  return self.weapon
end

--- Get the impact position vector. Note that this might not exist if the weapon has not impacted yet!
-- @param #WEAPON self
-- @return DCS#Vec3 Impact position vector (if any).
function WEAPON:GetImpactVec3()
  return self.impactVec3
end

--- Get the impact coordinate. Note that this might not exist if the weapon has not impacted yet!
-- @param #WEAPON self
-- @return Core.Point#COORDINATE Impact coordinate (if any).
function WEAPON:GetImpactCoordinate()
  return self.impactCoord
end

--- Get the heading on which the weapon was released
-- @param #WEAPON self
-- @param #bool AccountForMagneticInclination (Optional) If true will account for the magnetic declination of the current map. Default is true
-- @return #number Heading
function WEAPON:GetReleaseHeading(AccountForMagneticInclination)
    AccountForMagneticInclination = AccountForMagneticInclination or true
    if AccountForMagneticInclination then return UTILS.ClampAngle(self.releaseHeading - UTILS.GetMagneticDeclination()) else return UTILS.ClampAngle(self.releaseHeading) end
end

--- Get the altitude above sea level at which the weapon was released
-- @param #WEAPON self
-- @return #number Altitude in meters
function WEAPON:GetReleaseAltitudeASL()
    return self.releaseAltitudeASL
end

--- Get the altitude above ground level at which the weapon was released
-- @param #WEAPON self
-- @return #number Altitude in meters
function WEAPON:GetReleaseAltitudeAGL()
    return self.releaseAltitudeAGL
end

--- Get the coordinate where the weapon was released
-- @param #WEAPON self
-- @return Core.Point#COORDINATE Impact coordinate (if any).
function WEAPON:GetReleaseCoordinate()
    return self.releaseCoordinate
end

--- Get the pitch of the unit when the weapon was released
-- @param #WEAPON self
-- @return #number Degrees
function WEAPON:GetReleasePitch()
    return self.releasePitch
end

--- Get the heading of the weapon when it impacted. Note that this might not exist if the weapon has not impacted yet!
-- @param #WEAPON self
-- @param #bool AccountForMagneticInclination (Optional) If true will account for the magnetic declination of the current map. Default is true
-- @return #number Heading
function WEAPON:GetImpactHeading(AccountForMagneticInclination)
    AccountForMagneticInclination = AccountForMagneticInclination or true
    if AccountForMagneticInclination then return UTILS.ClampAngle(self.impactHeading - UTILS.GetMagneticDeclination()) else return self.impactHeading end
end

--- Check if weapon is in the air. Obviously not really useful for torpedos. Well, then again, this is DCS...
-- @param #WEAPON self
-- @return #boolean If `true`, weapon is in the air and `false` if not. Returns `nil` if weapon object itself is `nil`.
function WEAPON:InAir()
  local inAir=nil
  if self.weapon then
    inAir=self.weapon:inAir()
  end
  return inAir
end


--- Check if weapon object (still) exists.
-- @param #WEAPON self
-- @return #boolean If `true`, the weapon object still exists and `false` otherwise. Returns `nil` if weapon object itself is `nil`.
function WEAPON:IsExist()
  local isExist=nil
  if self.weapon then
    isExist=self.weapon:isExist()
  end
  return isExist
end


--- Check if weapon is a bomb.
-- @param #WEAPON self
-- @return #boolean If `true`, is a bomb.
function WEAPON:IsBomb()
  return self.category==Weapon.Category.BOMB
end

--- Check if weapon is a missile.
-- @param #WEAPON self
-- @return #boolean If `true`, is a missile.
function WEAPON:IsMissile()
  return self.category==Weapon.Category.MISSILE
end

--- Check if weapon is a rocket.
-- @param #WEAPON self
-- @return #boolean If `true`, is a missile.
function WEAPON:IsRocket()
  return self.category==Weapon.Category.ROCKET
end

--- Check if weapon is a shell.
-- @param #WEAPON self
-- @return #boolean If `true`, is a shell.
function WEAPON:IsShell()
  return self.category==Weapon.Category.SHELL
end

--- Check if weapon is a torpedo.
-- @param #WEAPON self
-- @return #boolean If `true`, is a torpedo.
function WEAPON:IsTorpedo()
  return self.category==Weapon.Category.TORPEDO
end

--- Check if weapon is a Fox One missile (Radar Semi-Active).
-- @param #WEAPON self
-- @return #boolean If `true`, is a Fox One.
function WEAPON:IsFoxOne()
  return self.guidance==Weapon.GuidanceType.RADAR_SEMI_ACTIVE
end

--- Check if weapon is a Fox Two missile (IR guided).
-- @param #WEAPON self
-- @return #boolean If `true`, is a Fox Two.
function WEAPON:IsFoxTwo()
  return self.guidance==Weapon.GuidanceType.IR
end

--- Check if weapon is a Fox Three missile (Radar Active).
-- @param #WEAPON self
-- @return #boolean If `true`, is a Fox Three.
function WEAPON:IsFoxThree()
 return self.guidance==Weapon.GuidanceType.RADAR_ACTIVE
end

--- Destroy the weapon object.
-- @param #WEAPON self
-- @param #number Delay Delay before destroy in seconds.
-- @return #WEAPON self
function WEAPON:Destroy(Delay)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, WEAPON.Destroy, self, 0)
  else
    if self.weapon then
      self:T(self.lid.."Destroying Weapon NOW!")
      self:StopTrack()
      self.weapon:destroy()
    end
  end

  return self
end

--- Start tracking the weapon until it impacts or is destroyed otherwise.
-- The position of the weapon is monitored in small time steps. Once the position cannot be determined anymore, the monitoring is stopped and the last known position is
-- the (approximate) impact point. Of course, the smaller the time step, the better the position can be determined. However, this can hit the performance as many
-- calculations per second need to be carried out.
-- @param #WEAPON self
-- @param #number Delay Delay in seconds before the tracking starts. Default 0.001 sec.
-- @return #WEAPON self
function WEAPON:StartTrack(Delay)

  -- Set delay before start.
  Delay=math.max(Delay or 0.001, 0.001)

  -- Debug info.
  self:T(self.lid..string.format("Start tracking weapon in %.4f sec", Delay))

  -- Weapon is not yet "alife" just yet. Start timer in 0.001 seconds.
  self.trackScheduleID=timer.scheduleFunction(WEAPON._TrackWeapon, self, timer.getTime() + Delay)

  return self
end


--- Stop tracking the weapon by removing the scheduler function.
-- @param #WEAPON self
-- @param #number Delay (Optional) Delay in seconds before the tracking is stopped.
-- @return #WEAPON self
function WEAPON:StopTrack(Delay)

  if Delay and Delay>0 then
    -- Delayed call.
    self:ScheduleOnce(Delay, WEAPON.StopTrack, self, 0)
  else

    if self.trackScheduleID then

      timer.removeFunction(self.trackScheduleID)

    end

  end

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Track weapon until impact.
-- @param #WEAPON self
-- @param DCS#Time time Time in seconds.
-- @return #number Time when called next or nil if not called again.
function WEAPON:_TrackWeapon(time)

  -- Debug info.
  if self.verbose>=20 then
    self:I(self.lid..string.format("Tracking at T=%.5f", time))
  end

  -- Protected call to get the weapon position. If the position cannot be determined any more, the weapon has impacted and status is nil.
  local status, pos3= pcall(
    function()
      local point=self.weapon:getPosition()
      return point
    end
  )

  if status then

    -------------------------------
    -- Weapon is still in exists --
    -------------------------------

    -- Update last known position.
    self.pos3 = pos3

    -- Update last known vec3.
    self.vec3 = UTILS.DeepCopy(self.pos3.p)

    -- Update coordinate.
    self.coordinate:UpdateFromVec3(self.vec3)

    -- Safe the last velocity of the weapon. This is needed to get the impact heading
    self.last_velocity = self.weapon:getVelocity()

    -- Keep on tracking by returning the next time below.
    self.tracking=true

    -- Callback function.
    if self.trackFunc then
      self.trackFunc(self, unpack(self.trackArg))
    end

    -- Verbose output.
    if self.verbose>=5 then

      -- Get vec2 of current position.
      local vec2={x=self.vec3.x, y=self.vec3.z}

      -- Land hight.
      local height=land.getHeight(vec2)

      -- Current height above ground level.
      local agl=self.vec3.y-height

      -- Estimated IP (if any)
      local ip=self:_GetIP(self.distIP)

      -- Distance between positon and estimated impact.
      local d=0
      if ip then
        d=UTILS.VecDist3D(self.vec3, ip)
      end

      -- Output.
      self:I(self.lid..string.format("T=%.3f: Height=%.3f m AGL=%.3f m, dIP=%.3f", time, height, agl, d))

    end

  else

    ---------------------------
    -- Weapon does NOT exist --
    ---------------------------

    -- Get intercept point from position (p) and direction (x) in 50 meters.
    local ip = self:_GetIP(self.distIP)

    if self.verbose>=10 and ip then

      -- Output.
      self:I(self.lid.."Got intercept point!")

      -- Coordinate of the impact point.
      local coord=COORDINATE:NewFromVec3(ip)

      -- Mark coordinate.
      coord:MarkToAll("Intercept point")
      coord:SmokeBlue()

      -- Distance to last known pos.
      local d=UTILS.VecDist3D(ip, self.vec3)

      -- Output.
      self:I(self.lid..string.format("FF d(ip, vec3)=%.3f meters", d))

    end

    -- Safe impact vec3.
    self.impactVec3=ip or self.vec3

    -- Safe impact coordinate.
    self.impactCoord=COORDINATE:NewFromVec3(self.vec3)

    -- Safe impact heading, using last_velocity because self:GetVelocityVec3() is no longer possible
    self.impactHeading =  UTILS.VecHdg(self.last_velocity)

    -- Mark impact point on F10 map.
    if self.impactMark then
      self.impactCoord:MarkToAll(string.format("Impact point of weapon %s\ntype=%s\nlauncher=%s", self.name, self.typeName, self.launcherName))
    end

    -- Smoke on impact point.
    if self.impactSmoke then
      self.impactCoord:Smoke(self.impactSmokeColor)
    end

    -- Call callback function.
    if self.impactFunc then
      self.impactFunc(self, unpack(self.impactArg or {}))
    end

    -- Stop tracking by returning nil below.
    self.tracking=false

  end

  -- Return next time the function is called or nil to stop the scheduler.
  if self.tracking then
    if self.dtTrack and self.dtTrack>=0.00001 then
      return time+self.dtTrack
    else
      return nil
    end
  end

  return nil
end

--- Compute estimated intercept/impact point (IP) based on last known position and direction.
-- @param #WEAPON self
-- @param #number Distance Distance in meters. Default 50 m.
-- @return DCS#Vec3 Estimated intercept/impact point. Can also return `nil`, if no IP can be determined.
function WEAPON:_GetIP(Distance)

  Distance=Distance or 50

  local ip=nil --DCS#Vec3

  if Distance>0 and self.pos3 then

    -- Get intercept point from position (p) and direction (x) in 20 meters.
    ip = land.getIP(self.pos3.p, self.pos3.x, Distance or 20) --DCS#Vec3

  end

  return ip
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Wrapper** - DCS net functions.
--
-- Encapsules **multiplayer server** environment scripting functions from [net](https://wiki.hoggitworld.com/view/DCS_singleton_net)
--
-- ===
--
-- ### Author: **Applevangelist**
-- # Last Update Oct 2023
-- 
-- ===
--
-- @module Wrapper.Net
-- @image Utils_Profiler.jpg

do
--- The NET class
-- @type NET
-- @field #string ClassName
-- @field #string Version
-- @field #string lid
-- @field #number BlockTime
-- @field #table BlockedPilots
-- @field #table KnownPilots
-- @field #string BlockMessage
-- @field #string UnblockMessage
-- @field #table BlockedUCIDs
-- @field #table BlockedSlots
-- @field #table BlockedSides
-- @extends Core.Fsm#FSM

---
-- @type NET.PlayerData
-- @field #string name
-- @field #string ucid
-- @field #number id
-- @field #number side
-- @field #number slot
-- @field #numner timestamp

--- Encapsules multiplayer environment scripting functions from [net](https://wiki.hoggitworld.com/view/DCS_singleton_net)
-- with some added FSM functions and options to block/unblock players in MP environments.
-- 
-- @field #NET
NET = {
  ClassName = "NET",
  Version = "0.1.4",
  BlockTime = 600,
  BlockedPilots = {},
  BlockedUCIDs = {},
  BlockedSides = {},
  BlockedSlots = {},
  KnownPilots = {},
  BlockMessage = nil,
  UnblockMessage = nil,
  lid = nil,
}

--- Instantiate a new NET object.
-- @param #NET self
-- @return #NET self
function NET:New()
  -- Inherit base.
  local self = BASE:Inherit(self, FSM:New()) -- #NET
  
  self.BlockTime = 600
  self.BlockedPilots = {}
  self.KnownPilots = {}
  self:SetBlockMessage()
  self:SetUnblockMessage()
  self.BlockedSides = {}
  self.BlockedSides[1] = false
  self.BlockedSides[2] = false
  
    -- Start State.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  -- From State  -->   Event  -->  To State
  self:AddTransition("Stopped", "Run",                "Running")     -- Start FSM.
  self:AddTransition("*",       "PlayerJoined",       "*")
  self:AddTransition("*",       "PlayerLeft",         "*")
  self:AddTransition("*",       "PlayerDied",         "*")
  self:AddTransition("*",       "PlayerEjected",      "*")
  self:AddTransition("*",       "PlayerBlocked",      "*")
  self:AddTransition("*",       "PlayerUnblocked",    "*")
  self:AddTransition("*",       "Status",             "*")
  self:AddTransition("*",       "Stop",               "Stopped")
  
  self.lid = string.format("NET %s | ",self.Version)
  
  --- FSM Function OnAfterPlayerJoined.
  -- @function [parent=#NET] OnAfterPlayerJoined
  -- @param #NET self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Client#CLIENT Client Object.
  -- @param #string Name Name of joining Pilot.
  -- @return #NET self
  
  --- FSM Function OnAfterPlayerLeft.
  -- @function [parent=#NET] OnAfterPlayerLeft
  -- @param #NET self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Unit#UNIT Client Unit Object, might be nil.
  -- @param #string Name Name of leaving Pilot.
  -- @return #NET self
  
  --- FSM Function OnAfterPlayerEjected.
  -- @function [parent=#NET] OnAfterPlayerEjected
  -- @param #NET self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Unit#UNIT Client Unit Object, might be nil.
  -- @param #string Name Name of leaving Pilot.
  -- @return #NET self
  
  --- FSM Function OnAfterPlayerDied.
  -- @function [parent=#NET] OnAfterPlayerDied
  -- @param #NET self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Unit#UNIT Client Unit Object, might be nil.
  -- @param #string Name Name of dead Pilot.
  -- @return #NET self
  
  --- FSM Function OnAfterPlayerBlocked.
  -- @function [parent=#NET] OnAfterPlayerBlocked
  -- @param #NET self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Client#CLIENT Client Client Object, might be nil.
  -- @param #string Name Name of blocked Pilot.
  -- @param #number Seconds Blocked for this number of seconds
  -- @return #NET self
  
  --- FSM Function OnAfterPlayerUnblocked.
  -- @function [parent=#NET] OnAfterPlayerUnblocked
  -- @param #NET self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Client#CLIENT Client Client Object, might be nil.
  -- @param #string Name Name of unblocked Pilot.
  -- @return #NET self
  
  self:Run()
  
  return self
end

--- [Internal] Check any blockers
-- @param #NET self
-- @param #string UCID
-- @param #string Name
-- @param #number PlayerID
-- @param #number PlayerSide
-- @param #string PlayerSlot
-- @return #boolean IsBlocked
function NET:IsAnyBlocked(UCID,Name,PlayerID,PlayerSide,PlayerSlot)
  self:T({UCID,Name,PlayerID,PlayerSide,PlayerSlot})
  local blocked = false
  local TNow = timer.getTime()
  -- UCID
  if UCID and self.BlockedUCIDs[UCID] and TNow < self.BlockedUCIDs[UCID] then
    blocked =  true
  end
  -- ID/Name
  if PlayerID and not Name then
    Name = self:GetPlayerIDByName(Name)
  end
  -- Name
  if Name and self.BlockedPilots[Name] and TNow < self.BlockedPilots[Name] then
    blocked =  true
  end
  -- Side
  self:T({time = self.BlockedSides[PlayerSide]})
  if PlayerSide and type(self.BlockedSides[PlayerSide]) == "number" and TNow < self.BlockedSides[PlayerSide] then
    blocked =  true
  end
  -- Slot
  if PlayerSlot and self.BlockedSlots[PlayerSlot] and TNow < self.BlockedSlots[PlayerSlot] then
    blocked =  true
  end
  self:T("IsAnyBlocked: "..tostring(blocked))
  return blocked
end

--- [Internal] Event Handler
-- @param #NET self
-- @param Core.Event#EVENTDATA EventData
-- @return #NET self
function NET:_EventHandler(EventData)
  self:T(self.lid .. " _EventHandler")
  self:T2({Event = EventData.id})
  local data = EventData -- Core.Event#EVENTDATA EventData
  if data.id and data.IniUnit and (data.IniPlayerName or data.IniUnit:GetPlayerName()) then
    
    -- Get Player Data
    local name = data.IniPlayerName and data.IniPlayerName or data.IniUnit:GetPlayerName()
    local ucid = self:GetPlayerUCID(nil,name) or "none"
    local PlayerID = self:GetPlayerIDByName(name) or "none"
    local PlayerSide, PlayerSlot = self:GetSlot(data.IniUnit)
    if not PlayerSide then PlayerSide = EventData.IniCoalition end
    if not PlayerSlot then PlayerSlot = EventData.IniUnit:GetID() or -1 end
    local TNow = timer.getTime()
    
    --self:T(self.lid.."Event for: "..name.." | UCID: "..ucid .. " | ID/SIDE/SLOT "..PlayerID.."/"..PlayerSide.."/"..PlayerSlot)
    
    -- Joining
    if data.id == EVENTS.PlayerEnterUnit or data.id == EVENTS.PlayerEnterAircraft then
      self:T(self.lid.."Pilot Joining: "..name.." | UCID: "..ucid.." | Event ID: "..data.id)
      -- Check for blockages
      local blocked = self:IsAnyBlocked(ucid,name,PlayerID,PlayerSide,PlayerSlot)  
      if blocked and PlayerID then -- and tonumber(PlayerID) ~= 1 then
        self:T("Player blocked")
        -- block pilot
        local outcome = net.force_player_slot(tonumber(PlayerID), PlayerSide, data.IniUnit:GetID() )
        self:T({Blocked_worked=outcome})
        if outcome == false then
          local unit = data.IniUnit
          local sched = TIMER:New(unit.Destroy,unit,3):Start(3)
          self:__PlayerBlocked(5,unit,name,1)
        end
      else
        local client = CLIENT:FindByPlayerName(name) or data.IniUnit
        if not self.KnownPilots[name] or (self.KnownPilots[name] and TNow-self.KnownPilots[name].timestamp > 3) then
          self:__PlayerJoined(1,client,name)
          self.KnownPilots[name] = {
            name = name,
            ucid = ucid,
            id = PlayerID,
            side = PlayerSide,
            slot = PlayerSlot,
            timestamp = TNow,
          }
          --UTILS.PrintTableToLog(self.KnownPilots[name])
        end
        return self
      end
    end
    
    -- Leaving
    if data.id == EVENTS.PlayerLeaveUnit and self.KnownPilots[name] then
     self:T(self.lid.."Pilot Leaving: "..name.." | UCID: "..ucid)
     self:__PlayerLeft(1,data.IniUnit,name)
     self.KnownPilots[name] = false
     return self
    end
    
    -- Ejected
    if data.id == EVENTS.Ejection and self.KnownPilots[name] then
     self:T(self.lid.."Pilot Ejecting: "..name.." | UCID: "..ucid)
     self:__PlayerEjected(1,data.IniUnit,name)
     self.KnownPilots[name] = false
     return self
    end
    
    -- Dead, Crash, Suicide
    if (data.id == EVENTS.PilotDead or data.id == EVENTS.SelfKillPilot or data.id == EVENTS.Crash) and self.KnownPilots[name] then
     self:T(self.lid.."Pilot Dead: "..name.." | UCID: "..ucid)
     self:__PlayerDied(1,data.IniUnit,name)
     self.KnownPilots[name] = false
     return self
    end
  end
  
  return self
end

--- Block a player.
-- @param #NET self
-- @param Wrapper.Client#CLIENT Client CLIENT object.
-- @param #string PlayerName (optional) Name of the player.
-- @param #number Seconds (optional) Number of seconds the player has to wait before rejoining.
-- @param #string Message (optional) Message to be sent via chat.
-- @return #NET self
function NET:BlockPlayer(Client,PlayerName,Seconds,Message)
  self:T({PlayerName,Seconds,Message})
  local name = PlayerName
  if Client and (not PlayerName) then
    name = Client:GetPlayerName()
  elseif PlayerName then
    name = PlayerName
  else
    self:F(self.lid.."Block: No Client or PlayerName given or nothing found!")
    return self
  end
  local ucid = self:GetPlayerUCID(Client,name)
  local addon = Seconds or self.BlockTime
  self.BlockedPilots[name] = timer.getTime()+addon
  self.BlockedUCIDs[ucid] = timer.getTime()+addon
  local message = Message or self.BlockMessage
  if name then
    self:SendChatToPlayer(message,name)
  else
    self:SendChat(name..": "..message)
  end
  self:__PlayerBlocked(1,Client,name,Seconds)
  local PlayerID = self:GetPlayerIDByName(name)
  if PlayerID and tonumber(PlayerID) ~= 1 then
    local outcome = net.force_player_slot(tonumber(PlayerID), 0, '' )
  end
  return self
end

--- Block a SET_CLIENT of players
-- @param #NET self
-- @param Core.Set#SET_CLIENT PlayerSet The SET to block.
-- @param #number Seconds Seconds (optional) Number of seconds the player has to wait before rejoining.
-- @param #string Message (optional) Message to be sent via chat.
-- @return #NET self
function NET:BlockPlayerSet(PlayerSet,Seconds,Message)
  self:T({PlayerSet.Set,Seconds,Message})
  local addon = Seconds or self.BlockTime
  local message = Message or self.BlockMessage
  for _,_client in pairs(PlayerSet.Set) do
    local name = _client:GetPlayerName()
    self:BlockPlayer(_client,name,addon,message)
  end
  return self
end

--- Unblock a SET_CLIENT of players
-- @param #NET self
-- @param Core.Set#SET_CLIENT PlayerSet The SET to unblock.
-- @param #string Message (optional) Message to be sent via chat.
-- @return #NET self
function NET:UnblockPlayerSet(PlayerSet,Message)
  self:T({PlayerSet.Set,Seconds,Message})
  local message = Message or self.UnblockMessage
  for _,_client in pairs(PlayerSet.Set) do
    local name = _client:GetPlayerName()
    self:UnblockPlayer(_client,name,message)
  end
  return self
end

--- Block a specific UCID of a player, does NOT automatically kick the player with the UCID if already joined.
-- @param #NET self
-- @param #string ucid
-- @param #number Seconds Seconds (optional) Number of seconds the player has to wait before rejoining.
-- @return #NET self
function NET:BlockUCID(ucid,Seconds)
  self:T({ucid,Seconds})
  local addon = Seconds or self.BlockTime
  self.BlockedUCIDs[ucid] = timer.getTime()+addon
  return self
end

--- Unblock a specific UCID of a player
-- @param #NET self
-- @param #string ucid
-- @return #NET self
function NET:UnblockUCID(ucid)
  self:T({ucid})
  self.BlockedUCIDs[ucid] = nil
  return self
end

--- Block a specific coalition side, does NOT automatically kick all players of that side or kick out joined players
-- @param #NET self
-- @param #number Side The side to block - 1 : Red, 2 : Blue
-- @param #number Seconds Seconds (optional) Number of seconds the player has to wait before rejoining.
-- @return #NET self
function NET:BlockSide(Side,Seconds)
  local addon = Seconds or self.BlockTime
  if Side == 1 or Side == 2 then
    self.BlockedSides[Side] = timer.getTime()+addon
  end
  return self
end

--- Unblock a specific coalition side. Does NOT unblock specifically blocked playernames or UCIDs.
-- @param #number side The side to block - 1 : Red, 2 : Blue
-- @param #number Seconds Seconds (optional) Number of seconds the player has to wait before rejoining.
-- @return #NET self
function NET:UnblockSide(Side,Seconds)
  local addon = Seconds or self.BlockTime
  if Side == 1 or Side == 2 then
    self.BlockedSides[Side] = false
  end
  return self
end

--- Block a specific player slot, does NOT automatically kick a player in that slot or kick out joined players
-- @param #NET self
-- @param #string slot The slot to block
-- @param #number Seconds Seconds (optional) Number of seconds the player has to wait before rejoining.
-- @return #NET self
function NET:BlockSlot(Slot,Seconds)
  self:T({Slot,Seconds})
  local addon = Seconds or self.BlockTime
  self.BlockedSlots[Slot] = timer.getTime()+addon
  return self
end

--- Unblock a specific slot.
-- @param #string slot The slot to block
-- @return #NET self
function NET:UnblockSlot(Slot)
  self:T({Slot})
  self.BlockedSlots[Slot] = nil
  return self
end

--- Unblock a player.
-- @param #NET self
-- @param Wrapper.Client#CLIENT Client CLIENT object
-- @param #string PlayerName (optional) Name of the player.
-- @param #string Message (optional) Message to be sent via chat.
-- @return #NET self
function NET:UnblockPlayer(Client,PlayerName,Message)
  local name = PlayerName
  if Client then
    name = Client:GetPlayerName()
  elseif PlayerName then
    name = PlayerName
  else
    self:F(self.lid.."Unblock: No PlayerName given or not found!")
    return self
  end
  local ucid = self:GetPlayerUCID(Client,name)
  self.BlockedPilots[name] = nil
  self.BlockedUCIDs[ucid] = nil
  local message = Message or self.UnblockMessage
  if name then
    self:SendChatToPlayer(message,name)
  else
    self:SendChat(name..": "..message)
  end
  self:__PlayerUnblocked(1,Client,name)
  return self
end

--- Set block chat message.
-- @param #NET self
-- @param #string Text The message
-- @return #NET self
function NET:SetBlockMessage(Text)
  self.BlockMessage = Text or "You are blocked from joining. Wait time is: "..self.BlockTime.." seconds!"
  return self
end

--- Set block time in seconds.
-- @param #NET self
-- @param #number Seconds Numnber of seconds this block will last. Defaults to 600.
-- @return #NET self
function NET:SetBlockTime(Seconds)
  self.BlockTime = Seconds or 600
  return self
end

--- Set unblock chat message.
-- @param #NET self
-- @param #string Text The message
-- @return #NET self
function NET:SetUnblockMessage(Text)
  self.UnblockMessage = Text or "You are unblocked now and can join again."
  return self
end

--- Send chat message.
-- @param #NET self
-- @param #string Message Message to send
-- @param #boolean ToAll (Optional)
-- @return #NET self
function NET:SendChat(Message,ToAll)
  if Message then
    net.send_chat(Message, ToAll)
  end
  return self
end

--- Find the PlayerID by name
-- @param #NET self
-- @param #string Name The player name whose ID to find
-- @return #number PlayerID or nil
function NET:GetPlayerIDByName(Name)
  if not Name then return nil end
  local playerList = net.get_player_list()
  for i=1,#playerList do
    local playerName = net.get_name(i)
      if playerName == Name then
        return playerList[i]
      end
  end
  return nil
end

--- Find the PlayerID from a CLIENT object.
-- @param #NET self
-- @param Wrapper.Client#CLIENT Client The client
-- @return #number PlayerID or nil
function NET:GetPlayerIDFromClient(Client)
  self:T("GetPlayerIDFromClient")
  self:T({Client=Client})
  if Client then
    local name = Client:GetPlayerName()
    self:T({name=name})
    local id = self:GetPlayerIDByName(name)
    return id
  else
    return nil
  end
end

--- Send chat message to a specific player using the CLIENT object.
-- @param #NET self
-- @param #string Message The text message
-- @param Wrapper.Client#CLIENT ToClient Client receiving the message
-- @param Wrapper.Client#CLIENT FromClient (Optional) Client sending the message
-- @return #NET self
function NET:SendChatToClient(Message, ToClient, FromClient)
  local PlayerId = self:GetPlayerIDFromClient(ToClient)
  local FromId = self:GetPlayerIDFromClient(FromClient)
  if Message and PlayerId and FromId then
    net.send_chat_to(Message, tonumber(PlayerId) , tonumber(FromId))
  elseif Message and PlayerId then
    net.send_chat_to(Message, tonumber(PlayerId))
  end
  return self
end

--- Send chat message to a specific player using the player name
-- @param #NET self
-- @param #string Message The text message
-- @param #string ToPlayer Player receiving the message
-- @param #string  FromPlayer(Optional) Player sending the message
-- @return #NET self
function NET:SendChatToPlayer(Message, ToPlayer, FromPlayer)
  local PlayerId = self:GetPlayerIDByName(ToPlayer)
  local FromId = self:GetPlayerIDByName(FromPlayer)
  if Message and PlayerId and FromId then
    net.send_chat_to(Message, tonumber(PlayerId) , tonumber(FromId))
  elseif Message and PlayerId then
    net.send_chat_to(Message, tonumber(PlayerId))
  end
  return self
end

--[[ not in 2.97 MSE any longer
--- Load a specific mission.
-- @param #NET self
-- @param #string Path and Mission
-- @return #boolean success
-- @usage
--        mynet:LoadMission(lfs.writeDir() .. 'Missions\\' .. 'MyTotallyAwesomeMission.miz')
function NET:LoadMission(Path)
  local outcome = false
  if Path then
    outcome = net.load_mission(Path)
  end
  return outcome
end

--- Load next mission. Returns false if at the end of list. 
-- @param #NET self
-- @return #boolean success
function NET:LoadNextMission()
  local outcome = false
  outcome = net.load_next_mission()
  return outcome
end
--]]

--- Return a table of players currently connected to the server.  
-- @param #NET self
-- @return #table PlayerList
function NET:GetPlayerList()
  local plist = nil
  plist = net.get_player_list()
  return plist
end

--- Returns the playerID of the local player. Always returns 1 for server.
-- @param #NET self
-- @return #number ID
function NET:GetMyPlayerID()
  return net.get_my_player_id()
end

--- Returns the playerID of the server. Currently always returns 1. 
-- @param #NET self
-- @return #number ID
function NET:GetServerID()
  return net.get_server_id()
end

--- Return a table of attributes for a given client. If optional attribute is present, only that value is returned. 
-- @param #NET self
-- @param Wrapper.Client#CLIENT Client The client.
-- @param #string Attribute (Optional) The attribute to obtain. List see below.
-- @return #table PlayerInfo or nil if it cannot be found
-- @usage
-- Table holds these attributes:
--
--          'id'    : playerID
--          'name'  : player name
--          'side'  : 0 - spectators, 1 - red, 2 - blue
--          'slot'  : slotID of the player or 
--          'ping'  : ping of the player in ms
--          'ipaddr': IP address of the player, SERVER ONLY
--          'ucid'  : Unique Client Identifier, SERVER ONLY
-- 
function NET:GetPlayerInfo(Client,Attribute)
  local PlayerID = self:GetPlayerIDFromClient(Client)
  if PlayerID then
    return net.get_player_info(tonumber(PlayerID), Attribute)
  else
    return nil
  end
end


--- Get player UCID from player CLIENT object or player name. Provide either one.  
-- @param #NET self
-- @param Wrapper.Client#CLIENT Client The client object to be used.
-- @param #string Name Player name to be used.
-- @return #boolean success
function NET:GetPlayerUCID(Client,Name)
  local PlayerID = nil
  if Client then
    PlayerID = self:GetPlayerIDFromClient(Client)
  elseif Name then
    PlayerID = self:GetPlayerIDByName(Name)
  else
    self:E(self.lid.."Neither client nor name provided!")
  end
  local ucid = net.get_player_info(tonumber(PlayerID), 'ucid')
  return ucid
end

--- Kicks a player from the server. Can display a message to the user.  
-- @param #NET self
-- @param Wrapper.Client#CLIENT Client The client
-- @param #string Message (Optional) The message to send.
-- @return #boolean success
function NET:Kick(Client,Message)
  local PlayerID = self:GetPlayerIDFromClient(Client)
  if PlayerID and tonumber(PlayerID) ~= 1 then
    return net.kick(tonumber(PlayerID), Message)
  else
    return false
  end
end

--- Return a statistic for a given client.  
-- @param #NET self
-- @param Wrapper.Client#CLIENT Client The client
-- @param #number StatisticID The statistic to obtain
-- @return #number Statistic or nil
-- @usage
-- StatisticIDs are:
--
-- net.PS_PING  (0) - ping (in ms)
-- net.PS_CRASH (1) - number of crashes
-- net.PS_CAR   (2) - number of destroyed vehicles
-- net.PS_PLANE (3) - ... planes/helicopters
-- net.PS_SHIP  (4) - ... ships
-- net.PS_SCORE (5) - total score
-- net.PS_LAND  (6) - number of landings
-- net.PS_EJECT (7) - of ejects
-- 
--          mynet:GetPlayerStatistic(Client,7) -- return number of ejects
function NET:GetPlayerStatistic(Client,StatisticID)
  local PlayerID = self:GetPlayerIDFromClient(Client)
  local stats = StatisticID or 0
  if stats > 7 or stats < 0 then stats = 0 end
  if PlayerID then
    return net.get_stat(tonumber(PlayerID),stats)
  else
    return nil
  end
end

--- Return the name of a given client. Effectively the same as CLIENT:GetPlayerName().
-- @param #NET self
-- @param Wrapper.Client#CLIENT Client The client
-- @return #string Name or nil if not obtainable
function NET:GetName(Client)
  local PlayerID = self:GetPlayerIDFromClient(Client)
  if PlayerID then
    return net.get_name(tonumber(PlayerID))
  else
    return nil
  end
end

--- Returns the SideId and SlotId of a given client. 
-- @param #NET self
-- @param Wrapper.Client#CLIENT Client The client
-- @return #number SideID i.e. 0 : spectators, 1 : Red, 2 : Blue
-- @return #number SlotID
function NET:GetSlot(Client)
  self:T("NET.GetSlot")
  local PlayerID = self:GetPlayerIDFromClient(Client)
  self:T("NET.GetSlot PlayerID = "..tostring(PlayerID))
  if PlayerID then
    local side,slot = net.get_slot(tonumber(PlayerID))
    self:T("NET.GetSlot side, slot = "..tostring(side)..","..tostring(slot))
    return side,slot
  else
    return nil,nil
  end
end

--- Force the slot for a specific client. If this returns false, it didn't work via `net` (which is ALWAYS the case as of Nov 2024)!
-- @param #NET self
-- @param Wrapper.Client#CLIENT Client The client
-- @param #number SideID i.e. 0 : spectators, 1 : Red, 2 : Blue
-- @param #number SlotID Slot number
-- @return #boolean Success
function NET:ForceSlot(Client,SideID,SlotID)
  local PlayerID = self:GetPlayerIDFromClient(Client)
  local SlotID = SlotID or Client:GetID()
  if PlayerID then -- and tonumber(PlayerID) ~= 1 then
    return net.force_player_slot(tonumber(PlayerID), SideID, SlotID )
  else
    return false
  end
end

--- Force a client back to spectators. If this returns false, it didn't work via `net` (which is ALWAYS the case as of Nov 2024)!
-- @param #NET self
-- @param Wrapper.Client#CLIENT Client The client
-- @return #boolean Succes
function NET:ReturnToSpectators(Client)
  local outcome = self:ForceSlot(Client,0)
  -- workaround
  local sched = TIMER:New(Client.Destroy,Client,1):Start(1)
  return outcome 
end

--- Converts a lua value to a JSON string.
-- @param #string Lua Anything lua
-- @return #table Json
function NET.Lua2Json(Lua)
  return net.lua2json(Lua)
end

--- Converts a JSON string to a lua value. 
-- @param #string Json Anything JSON
-- @return #table Lua
function NET.Json2Lua(Json)
  return net.json2lua(Json)
end

--- Executes a lua string in a given lua environment in the game. 
-- @param #NET self
-- @param #string State The state in which to execute - see below.
-- @param #string DoString The lua string to be executed.
-- @return #string Output
-- @usage
-- States are:
-- 'config': the state in which $INSTALL_DIR/Config/main.cfg is executed, as well as $WRITE_DIR/Config/autoexec.cfg  - used for configuration settings
-- 'mission': holds current mission
-- 'export': runs $WRITE_DIR/Scripts/Export.lua and the relevant export API
function NET:DoStringIn(State,DoString)
  return net.dostring_in(State,DoString)
end

---  Write an "INFO" entry to the DCS log file, with the message Message.
-- @param #NET self
-- @param #string Message The message to be logged.
-- @return #NET self
function NET:Log(Message)
  net.log(Message) 
  return self
end

---  Get some data of pilots who have currently joined
-- @param #NET self
-- @param Wrapper.Client#CLIENT Client Provide either the client object whose data to find **or**
-- @param #string Name The player name whose data to find 
-- @return #table Table of #NET.PlayerData or nil if not found
function NET:GetKnownPilotData(Client,Name)
  local name = Name
  if Client and not Name then
    name = Client:GetPlayerName()
  end
  if name then
    return self.KnownPilots[name]
  else
    return nil
  end
end

---  Status - housekeeping
-- @param #NET self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #NET self
function NET:onafterStatus(From,Event,To)
  self:T({From,Event,To})
  
  local function HouseHold(tavolo)
    local TNow = timer.getTime()
    for _,entry in pairs (tavolo) do
      if type(entry) == "number" and entry >= TNow then entry =  false end
    end
  end
  
  HouseHold(self.BlockedPilots)
  HouseHold(self.BlockedSides)
  HouseHold(self.BlockedSlots)
  HouseHold(self.BlockedUCIDs)
  
  if self:Is("Running") then
    self:__Status(-60)
  end
  
  return self
end

---  Stop the event functions
-- @param #NET self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #NET self
function NET:onafterRun(From,Event,To)
  self:T({From,Event,To})
  self:HandleEvent(EVENTS.PlayerEnterUnit,self._EventHandler)
  self:HandleEvent(EVENTS.PlayerEnterAircraft,self._EventHandler)
  self:HandleEvent(EVENTS.PlayerLeaveUnit,self._EventHandler)
  self:HandleEvent(EVENTS.PilotDead,self._EventHandler)
  self:HandleEvent(EVENTS.Ejection,self._EventHandler)
  self:HandleEvent(EVENTS.Crash,self._EventHandler)
  self:HandleEvent(EVENTS.SelfKillPilot,self._EventHandler)
  self:__Status(-10)
end

---  Stop the event functions
-- @param #NET self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #NET self
function NET:onafterStop(From,Event,To)
  self:T({From,Event,To})
  self:UnHandleEvent(EVENTS.PlayerEnterUnit)
  self:UnHandleEvent(EVENTS.PlayerEnterAircraft)
  self:UnHandleEvent(EVENTS.PlayerLeaveUnit)
  self:UnHandleEvent(EVENTS.PilotDead)
  self:UnHandleEvent(EVENTS.Ejection)
  self:UnHandleEvent(EVENTS.Crash)
  self:UnHandleEvent(EVENTS.SelfKillPilot)
  return self
end
-------------------------------------------------------------------------------
-- End of NET
-------------------------------------------------------------------------------
end
--- **Wrapper** - Warehouse storage of DCS airbases.
--
-- ## Main Features:
--
--    * Convenient access to DCS API functions
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/Wrapper/Storage).
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ===
-- @module Wrapper.Storage
-- @image Wrapper_Storage.png


--- STORAGE class.
-- @type STORAGE
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field DCS#Warehouse warehouse The DCS warehouse object.
-- @field DCS#Airbase airbase The DCS airbase object.
-- @field Core.Timer#TIMER SaverTimer The TIMER for autosave.
-- @extends Core.Base#BASE

--- *The capitalist cannot store labour-power in warehouses after he has bought it, as he may do with the raw material.* -- Karl Marx
--
-- ===
--
-- # The STORAGE Concept
--
-- The STORAGE class offers an easy-to-use wrapper interface to all DCS API functions of DCS warehouses.
-- We named the class STORAGE, because the name WAREHOUSE is already taken by another MOOSE class.
--
-- This class allows you to add and remove items to a DCS warehouse, such as aircraft, liquids, weapons and other equipment.
--
-- # Constructor
--
-- A DCS warehouse is associated with an airbase. Therefore, a `STORAGE` instance is automatically created, once an airbase is registered and added to the MOOSE database.
--
-- You can get the `STORAGE` object from the
--
--     -- Create a STORAGE instance of the Batumi warehouse
--     local storage=STORAGE:FindByName("Batumi")
--
-- An other way to get the `STORAGE` object is to retrieve it from the AIRBASE function `AIRBASE:GetStorage()`
--
--     -- Get storage instance of Batumi airbase
--     local Batumi=AIRBASE:FindByName("Batumi")
--     local storage=Batumi:GetStorage()
--
-- # Aircraft, Weapons and Equipment
--
-- ## Adding Items
--
-- To add aircraft, weapons and/or othe equipment, you can use the @{#STORAGE.AddItem}() function
--
--     storage:AddItem("A-10C", 3)
--     storage:AddItem("weapons.missiles.AIM_120C", 10)
--
-- This will add three A-10Cs and ten AIM-120C missiles to the warehouse inventory.
--
-- ## Setting Items
--
-- You can also explicitly set, how many items are in the inventory with the @{#STORAGE.SetItem}() function.
--
-- ## Removing Items
--
-- Items can be removed from the inventory with the @{#STORAGE.RemoveItem}() function.
--
-- ## Getting Amount
--
-- The number of items currently in the inventory can be obtained with the @{#STORAGE.GetItemAmount}() function
--
--     local N=storage:GetItemAmount("A-10C")
--     env.info(string.format("We currently have %d A-10Cs available", N))
--
-- # Liquids
--
-- Liquids can be added and removed by slightly different functions as described below. Currently there are four types of liquids
--
-- * Jet fuel `STORAGE.Liquid.JETFUEL`
-- * Aircraft gasoline `STORAGE.Liquid.GASOLINE`
-- * MW 50 `STORAGE.Liquid.MW50`
-- * Diesel `STORAGE.Liquid.DIESEL`
--
-- ## Adding Liquids
--
-- To add a certain type of liquid, you can use the @{#STORAGE.AddItem}(Type, Amount) function
--
--     storage:AddLiquid(STORAGE.Liquid.JETFUEL, 10000)
--     storage:AddLiquid(STORAGE.Liquid.DIESEL, 20000)
--
-- This will add 10,000 kg of jet fuel and 20,000 kg of diesel to the inventory.
--
-- ## Setting Liquids
--
-- You can also explicitly set the amount of liquid with the @{#STORAGE.SetLiquid}(Type, Amount) function.
--
-- ## Removing Liquids
--
-- Liquids can be removed with @{#STORAGE.RemoveLiquid}(Type, Amount) function.
--
-- ## Getting Amount
--
-- The current amount of a certain liquid can be obtained with the @{#STORAGE.GetLiquidAmount}(Type) function
--
--     local N=storage:GetLiquidAmount(STORAGE.Liquid.DIESEL)
--     env.info(string.format("We currently have %d kg of Diesel available", N))
--
--
-- # Inventory
--
-- The current inventory of the warehouse can be obtained with the @{#STORAGE.GetInventory}() function. This returns three tables with the aircraft, liquids and weapons:
--
--     local aircraft, liquids, weapons=storage:GetInventory()
--
--     UTILS.PrintTableToLog(aircraft)
--     UTILS.PrintTableToLog(liquids)
--     UTILS.PrintTableToLog(weapons)
--
-- # Weapons Helper Enumerater
-- 
-- The currently available weapon items are available in the `ENUMS.Storage.weapons`, e.g. `ENUMS.Storage.weapons.bombs.Mk_82Y`.
-- 
-- # Persistence
-- 
-- The contents of the storage can be saved to and read from disk. For this to function, `io` and `lfs` need to be desanitized in `MissionScripting.lua`.
-- 
-- ## Save once
-- 
-- ### To save once, e.g. this is sufficient:
--    
--      -- Filenames created are the Filename given amended by "_Liquids", "_Aircraft" and "_Weapons" followed by a ".csv". Only Storage NOT set to unlimited will be saved.
--      local Path = "C:\\Users\\UserName\\Saved Games\\DCS\\Missions\\"
--      local Filename = "Batumi"
--      storage:SaveToFile(Path,Filename)
--    
-- ### Autosave
-- 
--      storage:StartAutoSave(Path,Filename,300,true) -- save every 300 secs/5 mins starting in 5 mins, load the existing storage - if any - first if the last parameter is **not** `false`.
-- 
-- ### Stop Autosave
-- 
--      storage:StopAutoSave() -- stop the scheduler.
--    
-- ### Load back with e.g.
-- 
--      -- Filenames searched for the Filename given amended by "_Liquids", "_Aircraft" and "_Weapons" followed by a ".csv". Only Storage NOT set to unlimited will be loaded.
--      local Path = "C:\\Users\\UserName\\Saved Games\\DCS\\Missions\\"
--      local Filename = "Batumi"
--      storage:LoadFromFile(Path,Filename)
--    
--
-- @field #STORAGE
STORAGE = {
  ClassName          = "STORAGE",
  verbose            =     0,
}

--- Liquid types.
-- @type STORAGE.Liquid
-- @field #number JETFUEL Jet fuel (0).
-- @field #number GASOLINE Aviation gasoline (1).
-- @field #number MW50 MW50 (2).
-- @field #number DIESEL Diesel (3).
STORAGE.Liquid = {
  JETFUEL = 0,
  GASOLINE = 1,
  MW50 = 2,
  DIESEL = 3,
}

--- Liquid Names for the static cargo resource table.
-- @type STORAGE.LiquidName
-- @field #number JETFUEL "jet_fuel".
-- @field #number GASOLINE "gasoline".
-- @field #number MW50 "methanol_mixture".
-- @field #number DIESEL "diesel".
STORAGE.LiquidName = {
   GASOLINE = "gasoline",
   DIESEL =    "diesel",
   MW50 =  "methanol_mixture",
   JETFUEL = "jet_fuel",  
}

--- Storage types.
-- @type STORAGE.Type
-- @field #number WEAPONS weapons.
-- @field #number LIQUIDS liquids. Also see #list<#STORAGE.Liquid> for types of liquids.
-- @field #number AIRCRAFT aircraft.
STORAGE.Type = {
  WEAPONS = "weapons",
  LIQUIDS = "liquids",
  AIRCRAFT = "aircrafts",
}

--- STORAGE class version.
-- @field #string version
STORAGE.version="0.1.5"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: A lot...
-- DONE: Persistence

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new STORAGE object from the DCS airbase object.
-- @param #STORAGE self
-- @param #string AirbaseName Name of the airbase.
-- @return #STORAGE self
function STORAGE:New(AirbaseName)

  -- Inherit everything from BASE class.
  local self=BASE:Inherit(self, BASE:New()) -- #STORAGE

  self.airbase=Airbase.getByName(AirbaseName)

  if Airbase.getWarehouse and self.airbase then
    self.warehouse=self.airbase:getWarehouse()
  end

  self.lid = string.format("STORAGE %s | ", AirbaseName)

  return self
end

--- Create a new STORAGE object from an DCS static cargo object.
-- @param #STORAGE self
-- @param #string StaticCargoName Unit name of the static.
-- @return #STORAGE self
function STORAGE:NewFromStaticCargo(StaticCargoName)

  -- Inherit everything from BASE class.
  local self=BASE:Inherit(self, BASE:New()) -- #STORAGE

  self.airbase=StaticObject.getByName(StaticCargoName)

  if Airbase.getWarehouse then
    self.warehouse=Warehouse.getCargoAsWarehouse(self.airbase)
  end

  self.lid = string.format("STORAGE %s | ", StaticCargoName)

  return self
end

--- Create a new STORAGE object from a Wrapper.DynamicCargo#DYNAMICCARGO object.
-- @param #STORAGE self
-- @param #string DynamicCargoName Unit name of the dynamic cargo.
-- @return #STORAGE self
function STORAGE:NewFromDynamicCargo(DynamicCargoName)

  -- Inherit everything from BASE class.
  local self=BASE:Inherit(self, BASE:New()) -- #STORAGE

  self.airbase=Unit.getByName(DynamicCargoName) or StaticObject.getByName(DynamicCargoName)

  if Airbase.getWarehouse then
    self.warehouse=Warehouse.getCargoAsWarehouse(self.airbase)
  end

  self.lid = string.format("STORAGE %s | ", DynamicCargoName)

  return self
end


--- Airbases only - Find a STORAGE in the **_DATABASE** using the name associated airbase.
-- @param #STORAGE self
-- @param #string AirbaseName The Airbase Name.
-- @return #STORAGE self
function STORAGE:FindByName( AirbaseName )
  local storage = _DATABASE:FindStorage( AirbaseName )
  return storage
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User API Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set verbosity level.
-- @param #STORAGE self
-- @param #number VerbosityLevel Level of output (higher=more). Default 0.
-- @return #STORAGE self
function STORAGE:SetVerbosity(VerbosityLevel)
  self.verbose=VerbosityLevel or 0
  if self.verbose > 1 then
    BASE:TraceOn()
    BASE:TraceClass("STORAGE")
  end
  return self
end


--- Adds the passed amount of a given item to the warehouse.
-- @param #STORAGE self
-- @param #string Name Name of the item to add.
-- @param #number Amount Amount of items to add.
-- @return #STORAGE self
function STORAGE:AddItem(Name, Amount)

  self:T(self.lid..string.format("Adding %d items of %s", Amount, UTILS.OneLineSerialize(Name)))

  self.warehouse:addItem(Name, Amount)

  return self
end


--- Sets the specified amount of a given item to the warehouse.
-- @param #STORAGE self
-- @param #string Name Name of the item.
-- @param #number Amount Amount of items.
-- @return #STORAGE self
function STORAGE:SetItem(Name, Amount)

  self:T(self.lid..string.format("Setting item %s to N=%d", UTILS.OneLineSerialize(Name), Amount))

  self.warehouse:setItem(Name, Amount)

  return self
end

--- Gets the amount of a given item currently present the warehouse.
-- @param #STORAGE self
-- @param #string Name Name of the item.
-- @return #number Amount of items.
function STORAGE:GetItemAmount(Name)

  local N=self.warehouse:getItemCount(Name)

  return N
end



--- Removes the amount of the passed item from the warehouse.
-- @param #STORAGE self
-- @param #string Name Name of the item.
-- @param #number Amount Amount of items.
-- @return #STORAGE self
function STORAGE:RemoveItem(Name, Amount)

  self:T(self.lid..string.format("Removing N=%d of item %s", Amount, Name))

  self.warehouse:removeItem(Name, Amount)

  return self
end




--- Adds the passed amount of a given liquid to the warehouse.
-- @param #STORAGE self
-- @param #number Type Type of liquid.
-- @param #number Amount Amount of liquid to add.
-- @return #STORAGE self
function STORAGE:AddLiquid(Type, Amount)

  self:T(self.lid..string.format("Adding %d liquids of %s", Amount, self:GetLiquidName(Type)))

  self.warehouse:addLiquid(Type, Amount)

  return self
end


--- Sets the specified amount of a given liquid to the warehouse.
-- @param #STORAGE self
-- @param #number Type Type of liquid.
-- @param #number Amount Amount of liquid.
-- @return #STORAGE self
function STORAGE:SetLiquid(Type, Amount)

  self:T(self.lid..string.format("Setting liquid %s to N=%d", self:GetLiquidName(Type), Amount))

  self.warehouse:setLiquidAmount(Type, Amount)

  return self
end

--- Removes the amount of the given liquid type from the warehouse.
-- @param #STORAGE self
-- @param #number Type Type of liquid.
-- @param #number Amount Amount of liquid in kg to be removed.
-- @return #STORAGE self
function STORAGE:RemoveLiquid(Type, Amount)

  self:T(self.lid..string.format("Removing N=%d of liquid %s", Amount, self:GetLiquidName(Type)))

  self.warehouse:removeLiquid(Type, Amount)

  return self
end

--- Gets the amount of a given liquid currently present the warehouse.
-- @param #STORAGE self
-- @param #number Type Type of liquid.
-- @return #number Amount of liquid in kg.
function STORAGE:GetLiquidAmount(Type)

  local N=self.warehouse:getLiquidAmount(Type)

  return N
end

--- Returns the name of the liquid from its numeric type.
-- @param #STORAGE self
-- @param #number Type Type of liquid.
-- @return #string Name of the liquid.
function STORAGE:GetLiquidName(Type)

  local name="Unknown"

  if Type==STORAGE.Liquid.JETFUEL then
    name = "Jet fuel"
  elseif Type==STORAGE.Liquid.GASOLINE then
    name = "Aircraft gasoline"
  elseif Type==STORAGE.Liquid.MW50 then
    name = "MW 50"
  elseif Type==STORAGE.Liquid.DIESEL then
    name = "Diesel"
  else
    self:E(self.lid..string.format("ERROR: Unknown liquid type %s", tostring(Type)))
  end

  return name
end

--- Adds the amount of a given type of aircraft, liquid, weapon currently present the warehouse.
-- @param #STORAGE self
-- @param #number Type Type of liquid or name of aircraft, weapon or equipment.
-- @param #number Amount Amount of given type to add. Liquids in kg.
-- @return #STORAGE self
function STORAGE:AddAmount(Type, Amount)

  if type(Type)=="number" then
    self:AddLiquid(Type, Amount)
  else
    self:AddItem(Type, Amount)
  end

  return self
end

--- Removes the amount of a given type of aircraft, liquid, weapon from the warehouse.
-- @param #STORAGE self
-- @param #number Type Type of liquid or name of aircraft, weapon or equipment.
-- @param #number Amount Amount of given type to remove. Liquids in kg.
-- @return #STORAGE self
function STORAGE:RemoveAmount(Type, Amount)

  if type(Type)=="number" then
    self:RemoveLiquid(Type, Amount)
  else
    self:RemoveItem(Type, Amount)
  end

  return self
end

--- Sets the amount of a given type of aircraft, liquid, weapon currently present the warehouse.
-- @param #STORAGE self
-- @param #number Type Type of liquid or name of aircraft, weapon or equipment.
-- @param #number Amount of given type. Liquids in kg.
-- @return #STORAGE self
function STORAGE:SetAmount(Type, Amount)

  if type(Type)=="number" then
    self:SetLiquid(Type, Amount)
  else
    self:SetItem(Type, Amount)
  end

  return self
end


--- Gets the amount of a given type of aircraft, liquid, weapon currently present the warehouse.
-- @param #STORAGE self
-- @param #number Type Type of liquid or name of aircraft, weapon or equipment.
-- @return #number Amount of given type. Liquids in kg.
function STORAGE:GetAmount(Type)

  local N=0
  if type(Type)=="number" then
    N=self:GetLiquidAmount(Type)
  else
    N=self:GetItemAmount(Type)
  end

  return N
end

--- Returns whether a given type of aircraft, liquid, weapon is set to be unlimited.
-- @param #STORAGE self
-- @param #string Type Name of aircraft, weapon or equipment or type of liquid (as `#number`).
-- @return #boolean If `true` the given type is unlimited or `false` otherwise.
function STORAGE:IsUnlimited(Type)

  -- Get current amount of type.
  local N=self:GetAmount(Type)

  local unlimited=false

  if N>0 then

    -- Remove one item.
    self:RemoveAmount(Type, 1)

    -- Get amount.
    local n=self:GetAmount(Type)

    -- If amount did not change, it is unlimited.
    unlimited=unlimited or n > 2^29 or n==N

    -- Add item back.
    if not unlimited then
      self:AddAmount(Type, 1)
    end

    -- Debug info.
    self:T(self.lid..string.format("Type=%s: unlimited=%s (N=%d n=%d)", tostring(Type), tostring(unlimited), N, n))
  end

  return unlimited
end

--- Returns whether a given type of aircraft, liquid, weapon is set to be limited.
-- @param #STORAGE self
-- @param #number Type Type of liquid or name of aircraft, weapon or equipment.
-- @return #boolean If `true` the given type is limited or `false` otherwise.
function STORAGE:IsLimited(Type)

  local limited=not self:IsUnlimited(Type)

  return limited
end

--- Returns whether aircraft are unlimited.
-- @param #STORAGE self
-- @return #boolean If `true` aircraft are unlimited or `false` otherwise.
function STORAGE:IsUnlimitedAircraft()

  -- We test with a specific type but if it is unlimited, than all aircraft are.
  local unlimited=self:IsUnlimited("A-10C")

  return unlimited
end

--- Returns whether liquids are unlimited.
-- @param #STORAGE self
-- @return #boolean If `true` liquids are unlimited or `false` otherwise.
function STORAGE:IsUnlimitedLiquids()

  -- We test with a specific type but if it is unlimited, than all are.
  local unlimited=self:IsUnlimited(STORAGE.Liquid.DIESEL)

  return unlimited
end

--- Returns whether weapons and equipment are unlimited.
-- @param #STORAGE self
-- @return #boolean If `true` weapons and equipment are unlimited or `false` otherwise.
function STORAGE:IsUnlimitedWeapons()

  -- We test with a specific type but if it is unlimited, than all are.
  local unlimited=self:IsUnlimited(ENUMS.Storage.weapons.bombs.Mk_82)

  return unlimited
end

--- Returns whether aircraft are limited.
-- @param #STORAGE self
-- @return #boolean If `true` aircraft are limited or `false` otherwise.
function STORAGE:IsLimitedAircraft()

  -- We test with a specific type but if it is limited, than all are.
  local limited=self:IsLimited("A-10C")

  return limited
end

--- Returns whether liquids are limited.
-- @param #STORAGE self
-- @return #boolean If `true` liquids are limited or `false` otherwise.
function STORAGE:IsLimitedLiquids()

  -- We test with a specific type but if it is limited, than all are.
  local limited=self:IsLimited(STORAGE.Liquid.DIESEL)

  return limited
end

--- Returns whether weapons and equipment are limited.
-- @param #STORAGE self
-- @return #boolean If `true` liquids are limited or `false` otherwise.
function STORAGE:IsLimitedWeapons()

  -- We test with a specific type but if it is limited, than all are.
  local limited=self:IsLimited(ENUMS.Storage.weapons.bombs.Mk_82)

  return limited
end

--- Returns a full itemized list of everything currently in a warehouse. If a category is set to unlimited then the table will be returned empty.
-- @param #STORAGE self
-- @param #string Item Name of item as #string or type of liquid as #number.
-- @return #table Table of aircraft. Table is emtpy `{}` if number of aircraft is set to be unlimited.
-- @return #table Table of liquids. Table is emtpy `{}` if number of liquids is set to be unlimited.
-- @return #table Table of weapons and other equipment. Table is emtpy `{}` if number of liquids is set to be unlimited.
function STORAGE:GetInventory(Item)

  local inventory=self.warehouse:getInventory(Item)

  return inventory.aircraft, inventory.liquids, inventory.weapon
end

--- Save the contents of a STORAGE to files in CSV format. Filenames created are the Filename given amended by "_Liquids", "_Aircraft" and "_Weapons" followed by a ".csv". Requires io and lfs to be desanitized to be working.
-- @param #STORAGE self
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The base name of the files. Existing files will be overwritten.
-- @return #STORAGE self
function STORAGE:SaveToFile(Path,Filename)

    if not io then
      BASE:E("ERROR: io not desanitized. Can't save the files.")
      return false
    end
  
    -- Check default path.
    if Path==nil and not lfs then
      BASE:E("WARNING: lfs not desanitized. File will be saved in DCS installation root directory rather than your given path.")
    end
    
    local ac, lq, wp = self:GetInventory()
    local DataAircraft = ""
    local DataLiquids = ""
    local DataWeapons = ""
    
    if #lq > 0 then
      DataLiquids = DataLiquids .."Liquids in Storage:\n"
      for key,amount in pairs(lq) do
        DataLiquids = DataLiquids..tostring(key).."="..tostring(amount).."\n"
      end
      UTILS.SaveToFile(Path,Filename.."_Liquids.csv",DataLiquids)
      if self.verbose and self.verbose > 0 then
        self:I(self.lid.."Saving Liquids to "..tostring(Path).."\\"..tostring(Filename).."_Liquids.csv")
      end
    end
    
    if UTILS.TableLength(ac) > 0 then
      DataAircraft = DataAircraft .."Aircraft in Storage:\n"
      for key,amount in pairs(ac) do
        DataAircraft = DataAircraft..tostring(key).."="..tostring(amount).."\n"
      end
      UTILS.SaveToFile(Path,Filename.."_Aircraft.csv",DataAircraft)
      if self.verbose and self.verbose > 0 then
        self:I(self.lid.."Saving Aircraft to "..tostring(Path).."\\"..tostring(Filename).."_Aircraft.csv")
      end
    end
    
    if UTILS.TableLength(wp) > 0 then
      DataWeapons = DataWeapons .."Weapons and Materiel in Storage:\n"

      for _,_category in pairs(ENUMS.Storage.weapons) do
        for _,_key in pairs(_category) do
          local amount = self:GetAmount(_key)
          if type(_key) == "table" then
            _key = "{"..table.concat(_key,",").."}"
          end
          DataWeapons = DataWeapons..tostring(_key).."="..tostring(amount).."\n"
        end
      end

      -- Gazelle table keys
      for key,amount in pairs(ENUMS.Storage.weapons.Gazelle) do
        amount = self:GetItemAmount(ENUMS.Storage.weapons.Gazelle[key])
        DataWeapons = DataWeapons.."ENUMS.Storage.weapons.Gazelle."..tostring(key).."="..tostring(amount).."\n"
      end
      -- CH47
      for key,amount in pairs(ENUMS.Storage.weapons.CH47) do
        amount = self:GetItemAmount(ENUMS.Storage.weapons.CH47[key])
        DataWeapons = DataWeapons.."ENUMS.Storage.weapons.CH47."..tostring(key).."="..tostring(amount).."\n"
      end
      -- UH1H
      for key,amount in pairs(ENUMS.Storage.weapons.UH1H) do
        amount = self:GetItemAmount(ENUMS.Storage.weapons.UH1H[key])
        DataWeapons = DataWeapons.."ENUMS.Storage.weapons.UH1H."..tostring(key).."="..tostring(amount).."\n"
      end
      -- OH58D
      for key,amount in pairs(ENUMS.Storage.weapons.OH58) do
        amount = self:GetItemAmount(ENUMS.Storage.weapons.OH58[key])
        DataWeapons = DataWeapons.."ENUMS.Storage.weapons.OH58."..tostring(key).."="..tostring(amount).."\n"
      end
      -- AH64D
      for key,amount in pairs(ENUMS.Storage.weapons.AH64D) do
        amount = self:GetItemAmount(ENUMS.Storage.weapons.AH64D[key])
        DataWeapons = DataWeapons.."ENUMS.Storage.weapons.AH64D."..tostring(key).."="..tostring(amount).."\n"
      end
       UTILS.SaveToFile(Path,Filename.."_Weapons.csv",DataWeapons)
       if self.verbose and self.verbose > 0 then
         self:I(self.lid.."Saving Weapons to "..tostring(Path).."\\"..tostring(Filename).."_Weapons.csv")
       end
    end

  return self
end

--- Load the contents of a STORAGE from files. Filenames searched for are the Filename given amended by "_Liquids", "_Aircraft" and "_Weapons" followed by a ".csv". Requires io and lfs to be desanitized to be working.
-- @param #STORAGE self
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return #STORAGE self
function STORAGE:LoadFromFile(Path,Filename)
 
 if not io then
      BASE:E("ERROR: io not desanitized. Can't read the files.")
    return false
  end
  
  -- Check default path.
  if Path==nil and not lfs then
    BASE:E("WARNING: lfs not desanitized. File will be read from DCS installation root directory rather than your give path.")
  end
  
  --Liquids
  if self:IsLimitedLiquids() then
    local Ok,Liquids = UTILS.LoadFromFile(Path,Filename.."_Liquids.csv")
    if Ok then
       if self.verbose and self.verbose > 0 then
         self:I(self.lid.."Loading Liquids from "..tostring(Path).."\\"..tostring(Filename).."_Liquids.csv")
       end
      for _id,_line in pairs(Liquids) do
        if string.find(_line,"Storage") == nil then
            local tbl=UTILS.Split(_line,"=")
            local lqno = tonumber(tbl[1])
            local lqam = tonumber(tbl[2])
            self:SetLiquid(lqno,lqam)
        end
      end
    else
        self:E("File for Liquids could not be found: "..tostring(Path).."\\"..tostring(Filename).."_Liquids.csv")
    end
  end
  
  --Aircraft
  if self:IsLimitedAircraft() then
    local Ok,Aircraft = UTILS.LoadFromFile(Path,Filename.."_Aircraft.csv")
    if Ok then
       if self.verbose and self.verbose > 0 then
         self:I(self.lid.."Loading Aircraft from "..tostring(Path).."\\"..tostring(Filename).."_Aircraft.csv")
       end
      for _id,_line in pairs(Aircraft) do
        if string.find(_line,"Storage") == nil then
            local tbl=UTILS.Split(_line,"=")
            local acname = tbl[1]
            local acnumber = tonumber(tbl[2])
            self:SetAmount(acname,acnumber)
        end
      end
    else
        self:E("File for Aircraft could not be found: "..tostring(Path).."\\"..tostring(Filename).."_Aircraft.csv")
    end
  end
  
  --Weapons
  if self:IsLimitedWeapons() then
    local Ok,Weapons = UTILS.LoadFromFile(Path,Filename.."_Weapons.csv")
    if Ok then
       if self.verbose and self.verbose > 0 then
         self:I(self.lid.."Loading Weapons from "..tostring(Path).."\\"..tostring(Filename).."_Weapons.csv")
       end
      for _id,_line in pairs(Weapons) do
        if string.find(_line,"Storage") == nil then
            local tbl=UTILS.Split(_line,"=")
            local wpname = tbl[1]
            local wpnumber = tonumber(tbl[2])
            if string.find(wpname,"{") == 1 then
             --self:I("Found a table: "..wpname)
             wpname = string.gsub(wpname,"{","")
             wpname = string.gsub(wpname,"}","")
             local tbl = UTILS.Split(wpname,",")
             local wptbl = {}
             for _id,_key in ipairs(tbl) do
                table.insert(wptbl,_id,_key)
             end
             self:SetAmount(wptbl,wpnumber)  
            else
             self:SetAmount(wpname,wpnumber)
            end
        end
      end
    else
        self:E("File for Weapons could not be found: "..tostring(Path).."\\"..tostring(Filename).."_Weapons.csv")
    end
  end
   
  return self
end

--- Start a STORAGE autosave process.
-- @param #STORAGE self
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @param #number Interval The interval, start after this many seconds and repeat every interval seconds. Defaults to 300.
-- @param #boolean LoadOnce If LoadOnce is true or nil, we try to load saved storage first.
-- @return #STORAGE self
function STORAGE:StartAutoSave(Path,Filename,Interval,LoadOnce)
  if LoadOnce ~= false then
    self:LoadFromFile(Path,Filename)
  end
  local interval = Interval or 300
  self.SaverTimer = TIMER:New(STORAGE.SaveToFile,self,Path,Filename)
  self.SaverTimer:Start(interval,interval)
  return self
end

--- Stop a running STORAGE autosave process.
-- @param #STORAGE self
-- @return #STORAGE self
function STORAGE:StopAutoSave()
  if self.SaverTimer and self.SaverTimer:IsRunning() then
    self.SaverTimer:Stop()
    self.SaverTimer = nil
  end
  return self
end

--- Try to find the #STORAGE object of one of the many "H"-Helipads in Syria. You need to put a (small, round) zone on top of it, because the name is not unique(!).
-- @param #STORAGE self
-- @param #string ZoneName The name of the zone where to find the helipad.
-- @return #STORAGE self or nil if not found.
function STORAGE:FindSyriaHHelipadWarehouse(ZoneName)
 local findzone = ZONE:New(ZoneName)
 local base = world.getAirbases()
 for i = 1, #base do
   local info = {}
   --info.desc = Airbase.getDesc(base[i])
   info.callsign = Airbase.getCallsign(base[i])
   info.id = Airbase.getID(base[i])
   --info.cat = Airbase.getCategory(base[i])
   info.point = Airbase.getPoint(base[i])
   info.coordinate = COORDINATE:NewFromVec3(info.point)
   info.DCSObject = base[i]
   --if Airbase.getUnit(base[i]) then
       --info.unitId = Airbase.getUnit(base[i]):getID()
   --end
   if info.callsign == "H" and findzone:IsCoordinateInZone(info.coordinate) then
    info.warehouse = info.DCSObject:getWarehouse()
    info.Storage = STORAGE:New(info.callsign..info.id)
    info.Storage.airbase = info.DCSObject
    info.Storage.warehouse = info.warehouse         
    return info.Storage
   end
 end
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Wrapper** - Dynamic Cargo create from the F8 menu.
--
-- ## Main Features:
--
--    * Convenient access to Ground Crew created cargo items.
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/).
--
-- ===
--
-- ### Author: **Applevangelist**; additional checks **Chesster**
--
-- ===
-- @module Wrapper.DynamicCargo
-- @image Wrapper_Storage.png


--- DYNAMICCARGO class.
-- @type DYNAMICCARGO
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field Wrapper.Storage#STORAGE warehouse The STORAGE object.
-- @field #string version.
-- @field #string CargoState.
-- @field #table DCS#Vec3 LastPosition.
-- @field #number Interval Check Interval. 20 secs default.
-- @field #boolean testing
-- @field Core.Timer#TIMER timer Timmer to run intervals
-- @field #string Owner The playername who has created, loaded or unloaded this cargo. Depends on state.
-- @extends Wrapper.Positionable#POSITIONABLE

--- *The capitalist cannot store labour-power in warehouses after he has bought it, as he may do with the raw material.* -- Karl Marx
--
-- ===
--
-- # The DYNAMICCARGO Concept
--
-- The DYNAMICCARGO class offers an easy-to-use wrapper interface to all DCS API functions of DCS dynamically spawned cargo crates.
-- We named the class DYNAMICCARGO, because the name WAREHOUSE is already taken by another MOOSE class..
--
-- # Constructor
--
-- @field #DYNAMICCARGO
DYNAMICCARGO = {
  ClassName          = "DYNAMICCARGO",
  verbose            = 0,
  testing            = false,
  Interval           = 10,
  
}

--- Liquid types.
-- @type DYNAMICCARGO.Liquid
-- @field #number JETFUEL Jet fuel (0).
-- @field #number GASOLINE Aviation gasoline (1).
-- @field #number MW50 MW50 (2).
-- @field #number DIESEL Diesel (3).
DYNAMICCARGO.Liquid = {
  JETFUEL = 0,
  GASOLINE = 1,
  MW50 = 2,
  DIESEL = 3,
}

--- Liquid Names for the static cargo resource table.
-- @type DYNAMICCARGO.LiquidName
-- @field #number JETFUEL "jet_fuel".
-- @field #number GASOLINE "gasoline".
-- @field #number MW50 "methanol_mixture".
-- @field #number DIESEL "diesel".
DYNAMICCARGO.LiquidName = {
   GASOLINE = "gasoline",
   DIESEL =    "diesel",
   MW50 =  "methanol_mixture",
   JETFUEL = "jet_fuel",  
}

--- Storage types.
-- @type DYNAMICCARGO.Type
-- @field #number WEAPONS weapons.
-- @field #number LIQUIDS liquids. Also see #list<#DYNAMICCARGO.Liquid> for types of liquids.
-- @field #number AIRCRAFT aircraft.
DYNAMICCARGO.Type = {
  WEAPONS = "weapons",
  LIQUIDS = "liquids",
  AIRCRAFT = "aircrafts",
}

--- State types
-- @type DYNAMICCARGO.State
-- @field #string NEW
-- @field #string LOADED
-- @field #string UNLOADED
-- @field #string REMOVED
DYNAMICCARGO.State = {
  NEW = "NEW",
  LOADED = "LOADED",
  UNLOADED = "UNLOADED",
  REMOVED = "REMOVED",
}

--- Helo types possible.
-- @type DYNAMICCARGO.AircraftTypes
DYNAMICCARGO.AircraftTypes = {
  ["CH-47Fbl1"] = "CH-47Fbl1",
  ["Mi-8MTV2"] = "CH-47Fbl1",
  ["Mi-8MT"] = "CH-47Fbl1",
}

--- Helo types possible.
-- @type DYNAMICCARGO.AircraftDimensions
DYNAMICCARGO.AircraftDimensions = {
  -- CH-47 model start coordinate is quite exactly in the middle of the model, so half values here
  ["CH-47Fbl1"] = {
    ["width"] = 4,
    ["height"] = 6,
    ["length"] = 11,
    ["ropelength"] = 30,
  },
    ["Mi-8MTV2"] = {
    ["width"] = 6,
    ["height"] = 6,
    ["length"] = 15,
    ["ropelength"] = 30,
  },
    ["Mi-8MT"] = {
    ["width"] = 6,
    ["height"] = 6,
    ["length"] = 15,
    ["ropelength"] = 30,
  },
}

--- DYNAMICCARGO class version.
-- @field #string version
DYNAMICCARGO.version="0.0.9"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: A lot...
-- DONE: Added Mi-8 type and dimensions

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new DYNAMICCARGO object from the DCS static cargo object.
-- @param #DYNAMICCARGO self
-- @param #string CargoName Name of the Cargo.
-- @return #DYNAMICCARGO self
function DYNAMICCARGO:Register(CargoName)

  -- Inherit everything from a BASE class.
  local self=BASE:Inherit(self, POSITIONABLE:New(CargoName)) -- #DYNAMICCARGO
    
  self.StaticName = CargoName
  
  self.LastPosition = self:GetCoordinate()
  
  self.CargoState = DYNAMICCARGO.State.NEW
  
  self.Interval = DYNAMICCARGO.Interval or 10
  
  local DCSObject = self:GetDCSObject()
  
  if DCSObject then
    local warehouse = STORAGE:NewFromDynamicCargo(CargoName)
    self.warehouse = warehouse
  end
  
  self.lid = string.format("DYNAMICCARGO %s", CargoName)
  
  self.Owner = string.match(CargoName,"^(.+)|%d%d:%d%d|PKG%d+") or "None"
  
  self.timer = TIMER:New(DYNAMICCARGO._UpdatePosition,self)
  self.timer:Start(self.Interval,self.Interval)
  
  if not _DYNAMICCARGO_HELOS then
      _DYNAMICCARGO_HELOS = SET_CLIENT:New():FilterAlive():FilterFunction(DYNAMICCARGO._FilterHeloTypes):FilterStart()
  end
  
  if self.testing then
    BASE:TraceOn()
    BASE:TraceClass("DYNAMICCARGO")
  end
  
  return self
end

--- Get DCS object.
-- @param #DYNAMICCARGO self
-- @return DCS static object
function DYNAMICCARGO:GetDCSObject()
  local DCSStatic = StaticObject.getByName( self.StaticName ) or Unit.getByName( self.StaticName ) 
  if DCSStatic then
    return DCSStatic
  end
  return nil
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User API Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get last known owner name of this DYNAMICCARGO
-- @param #DYNAMICCARGO self
-- @return #string Owner
function DYNAMICCARGO:GetLastOwner()
  return self.Owner
end

--- Returns true if the cargo is new and has never been loaded into a Helo.
-- @param #DYNAMICCARGO self
-- @return #boolean Outcome
function DYNAMICCARGO:IsNew()
  if self.CargoState and self.CargoState == DYNAMICCARGO.State.NEW then
    return true
  else
    return false
  end
end

--- Returns true if the cargo been loaded into a Helo.
-- @param #DYNAMICCARGO self
-- @return #boolean Outcome
function DYNAMICCARGO:IsLoaded()
  if self.CargoState and self.CargoState == DYNAMICCARGO.State.LOADED then
    return true
  else
    return false
  end
end

--- Returns true if the cargo has been unloaded from a Helo.
-- @param #DYNAMICCARGO self
-- @return #boolean Outcome
function DYNAMICCARGO:IsUnloaded()
  if self.CargoState and self.CargoState == DYNAMICCARGO.State.UNLOADED then
    return true
  else
    return false
  end
end

--- Returns true if the cargo has been removed.
-- @param #DYNAMICCARGO self
-- @return #boolean Outcome
function DYNAMICCARGO:IsRemoved()
  if self.CargoState and self.CargoState == DYNAMICCARGO.State.REMOVED then
    return true
  else
    return false
  end
end

--- [CTLD] Get number of crates this DYNAMICCARGO consists of. Always one.
-- @param #DYNAMICCARGO self
-- @return #number crate number, always one
function DYNAMICCARGO:GetCratesNeeded()
  return 1
end

--- [CTLD] Get this DYNAMICCARGO drop state. True if DYNAMICCARGO.State.UNLOADED
-- @param #DYNAMICCARGO self
-- @return #boolean Dropped
function DYNAMICCARGO:WasDropped()
  return self.CargoState == DYNAMICCARGO.State.UNLOADED and true or false
end

--- [CTLD] Get CTLD_CARGO.Enum type of this DYNAMICCARGO
-- @param #DYNAMICCARGO self
-- @return #string Type, only one at the moment is CTLD_CARGO.Enum.GCLOADABLE
function DYNAMICCARGO:GetType()
  return CTLD_CARGO.Enum.GCLOADABLE
end


--- Find last known position of this DYNAMICCARGO
-- @param #DYNAMICCARGO self
-- @return DCS#Vec3 Position in 3D space
function DYNAMICCARGO:GetLastPosition()
  return self.LastPosition
end

--- Find current state of this DYNAMICCARGO
-- @param #DYNAMICCARGO self
-- @return string The current state
function DYNAMICCARGO:GetState()
  return self.CargoState
end

--- Find a DYNAMICCARGO in the **_DATABASE** using the name associated with it.
-- @param #DYNAMICCARGO self
-- @param #string Name The dynamic cargo name
-- @return #DYNAMICCARGO self
function DYNAMICCARGO:FindByName( Name )
  local storage = _DATABASE:FindDynamicCargo( Name )
  return storage
end

--- Find the first(!) DYNAMICCARGO matching using patterns. Note that this is **a lot** slower than `:FindByName()`!
-- @param #DYNAMICCARGO self
-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA.
-- @return #DYNAMICCARGO The DYNAMICCARGO.
-- @usage
--          -- Find a dynamic cargo with a partial dynamic cargo name
--          local grp = DYNAMICCARGO:FindByMatching( "Apple" )
--          -- will return e.g. a dynamic cargo named "Apple|08:00|PKG08"
--
--          -- using a pattern
--          local grp = DYNAMICCARGO:FindByMatching( ".%d.%d$" )
--          -- will return the first dynamic cargo found ending in "-1-1" to "-9-9", but not e.g. "-10-1"
function DYNAMICCARGO:FindByMatching( Pattern )
  local GroupFound = nil

  for name,static in pairs(_DATABASE.DYNAMICCARGO) do
    if string.match(name, Pattern ) then
      GroupFound = static
      break
    end
  end

  return GroupFound
end

--- Find all DYNAMICCARGO objects matching using patterns. Note that this is **a lot** slower than `:FindByName()`!
-- @param #DYNAMICCARGO self
-- @param #string Pattern The pattern to look for. Refer to [LUA patterns](http://www.easyuo.com/openeuo/wiki/index.php/Lua_Patterns_and_Captures_\(Regular_Expressions\)) for regular expressions in LUA.
-- @return #table Groups Table of matching #DYNAMICCARGO objects found
-- @usage
--          -- Find all dynamic cargo with a partial dynamic cargo name
--          local grptable = DYNAMICCARGO:FindAllByMatching( "Apple" )
--          -- will return all dynamic cargos with "Apple" in the name
--
--          -- using a pattern
--          local grp = DYNAMICCARGO:FindAllByMatching( ".%d.%d$" )
--          -- will return the all dynamic cargos found ending in "-1-1" to "-9-9", but not e.g. "-10-1" or "-1-10"
function DYNAMICCARGO:FindAllByMatching( Pattern )
  local GroupsFound = {}

  for name,static in pairs(_DATABASE.DYNAMICCARGO) do
    if string.match(name, Pattern ) then
      GroupsFound[#GroupsFound+1] = static
    end
  end

  return GroupsFound
end

--- Get the #STORAGE object from this dynamic cargo.
-- @param #DYNAMICCARGO self
-- @return Wrapper.Storage#STORAGE Storage The #STORAGE object
function DYNAMICCARGO:GetStorageObject()
  return self.warehouse
end

--- Get the weight in kgs from this dynamic cargo.
-- @param #DYNAMICCARGO self
-- @return #number Weight in kgs.
function DYNAMICCARGO:GetCargoWeight()
  local DCSObject = self:GetDCSObject()
  if DCSObject then
    local weight = DCSObject:getCargoWeight()
    return weight
  else
    return 0
  end
end

--- Get the cargo display name from this dynamic cargo.
-- @param #DYNAMICCARGO self
-- @return #string The display name
function DYNAMICCARGO:GetCargoDisplayName()
  local DCSObject = self:GetDCSObject()
  if DCSObject then
    local weight = DCSObject:getCargoDisplayName()
    return weight
  else
    return self.StaticName
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- [Internal] _Get helo hovering intel
-- @param #DYNAMICCARGO self
-- @param Wrapper.Unit#UNIT Unit The Unit to test
-- @param #number ropelength Ropelength to test
-- @return #boolean Outcome
function DYNAMICCARGO:_HeloHovering(Unit,ropelength)
    local DCSUnit = Unit:GetDCSObject() --DCS#Unit
    local hovering = false
    local Height = 0
    if DCSUnit then
        local UnitInAir = DCSUnit:inAir()
        local UnitCategory = DCSUnit:getDesc().category       
        if UnitInAir == true and UnitCategory == 1 then
            local VelocityVec3 = DCSUnit:getVelocity()
            local Velocity = UTILS.VecNorm(VelocityVec3)
            local Coordinate = DCSUnit:getPoint()
            local LandHeight = land.getHeight({ x = Coordinate.x, y = Coordinate.z })
            Height = Coordinate.y - LandHeight
            if Velocity < 1 and Height <= ropelength and Height > 6 then -- hover lower than ropelength but higher than the normal FARP height.
                hovering = true
            end
        end
        return hovering, Height
    end
    return false
end

--- [Internal] _Get Possible Player Helo Nearby
-- @param #DYNAMICCARGO self
-- @param Core.Point#COORDINATE pos
-- @param #boolean loading If true measure distance for loading else for unloading
-- @return #boolean Success
-- @return Wrapper.Client#CLIENT Helo
-- @return #string PlayerName
function DYNAMICCARGO:_GetPossibleHeloNearby(pos,loading)
  local set = _DYNAMICCARGO_HELOS:GetAliveSet()
  local success = false
  local Helo = nil
  local Playername = nil
  for _,_helo in pairs (set or {}) do
    local helo = _helo -- Wrapper.Client#CLIENT
    local name = helo:GetPlayerName() or _DATABASE:_FindPlayerNameByUnitName(helo:GetName()) or "None"
    self:T(self.lid.." Checking: "..name)
    local hpos = helo:GetCoordinate()
    -- TODO Check unloading via sling load?
    local typename = helo:GetTypeName()
    local dimensions = DYNAMICCARGO.AircraftDimensions[typename]
    local hovering, height = self:_HeloHovering(helo,dimensions.ropelength)
    local helolanded = not helo:InAir()
    self:T(self.lid.." InAir: AGL/Hovering: "..hpos.y-hpos:GetLandHeight().."/"..tostring(hovering))
    if hpos and typename and dimensions then
      local delta2D = hpos:Get2DDistance(pos)
      local delta3D = hpos:Get3DDistance(pos)
      if self.testing then
        self:T(string.format("Cargo relative position: 2D %dm | 3D %dm",delta2D,delta3D))
        self:T(string.format("Helo dimension: length %dm | width %dm | rope %dm",dimensions.length,dimensions.width,dimensions.ropelength))
        self:T(string.format("Helo hovering: %s at %dm",tostring(hovering),height))
      end
      -- unloading from ground
      if loading~=true and (delta2D > dimensions.length or delta2D > dimensions.width) and helolanded then  -- Theoretically the cargo could still be attached to the sling if landed next to the cargo. But once moved again it would go back into loaded state once lifted again.
        success = true
        Helo = helo
        Playername = name
      end
      -- unloading from hover/rope
      if loading~=true and delta3D > dimensions.ropelength then     
        success = true
        Helo = helo
        Playername = name
      end
      -- loading
      if loading == true and ((delta2D < dimensions.length and delta2D < dimensions.width and helolanded) or (delta3D == dimensions.ropelength and helo:InAir())) then -- Loaded via ground or sling                  
        success = true
        Helo = helo
        Playername = name
      end
    end
  end
  return success,Helo,Playername
end

--- [Internal] Update internal states.
-- @param #DYNAMICCARGO self
-- @return #DYNAMICCARGO self
function DYNAMICCARGO:_UpdatePosition()
  self:T(self.lid.." _UpdatePositionAndState")
  if self:IsAlive() then
    local pos = self:GetCoordinate()
    if self.testing then
      self:T(string.format("Cargo position: x=%d, y=%d, z=%d",pos.x,pos.y,pos.z))
      self:T(string.format("Last position: x=%d, y=%d, z=%d",self.LastPosition.x,self.LastPosition.y,self.LastPosition.z))
    end
    if UTILS.Round(UTILS.VecDist3D(pos,self.LastPosition),2) > 0.5 then      -- This checks if the cargo has moved more than 0.5m since last check. If so then the cargo is loaded
      ---------------
      -- LOAD Cargo
      ---------------
      if self.CargoState == DYNAMICCARGO.State.NEW or self.CargoState == DYNAMICCARGO.State.UNLOADED then
        local isloaded, client, playername = self:_GetPossibleHeloNearby(pos,true)         
        self:T(self.lid.." moved! NEW -> LOADED by "..tostring(playername))
        self.CargoState = DYNAMICCARGO.State.LOADED
        self.Owner = playername
        _DATABASE:CreateEventDynamicCargoLoaded(self)        
      end  
      ---------------
      -- UNLOAD Cargo
      ---------------
      --  If the cargo is stationary then we need to end this condition here to check whether it is unloaded or still onboard or still hooked if anyone can hover that precisly   
    elseif self.CargoState == DYNAMICCARGO.State.LOADED then
        -- TODO add checker if we are in flight somehow
        -- ensure not just the helo is moving
        local count = _DYNAMICCARGO_HELOS:CountAlive()
        -- Testing
        local landheight = pos:GetLandHeight()
        local agl = pos.y-landheight
        agl = UTILS.Round(agl,2)
        self:T(self.lid.." AGL: "..agl or -1)
        local isunloaded = true
        local client
        local playername = self.Owner
        if count > 0 then
          self:T(self.lid.." Possible alive helos: "..count or -1)          
            isunloaded, client, playername = self:_GetPossibleHeloNearby(pos,false)       
          if isunloaded then
            self:T(self.lid.." moved! LOADED -> UNLOADED by "..tostring(playername))
            self.CargoState = DYNAMICCARGO.State.UNLOADED
            self.Owner = playername
            _DATABASE:CreateEventDynamicCargoUnloaded(self)
          end        
        end
      end
      self.LastPosition = pos
    --end
  else
    ---------------
    -- REMOVED Cargo
    --------------- 
    if self.timer and self.timer:IsRunning() then self.timer:Stop() end
    self:T(self.lid.." dead! " ..self.CargoState.."-> REMOVED")
    self.CargoState = DYNAMICCARGO.State.REMOVED
    _DATABASE:CreateEventDynamicCargoRemoved(self)
  end
  return self
end

--- [Internal] Track helos for loaded/unloaded decision making.
-- @param Wrapper.Client#CLIENT client
-- @return #boolean IsIn
function DYNAMICCARGO._FilterHeloTypes(client)
  if not client then return false end
  local typename = client:GetTypeName()
  local isinclude = DYNAMICCARGO.AircraftTypes[typename] ~= nil and true or false
  return isinclude
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Cargo** - Management of CARGO logistics, that can be transported from and to transportation carriers.
--
-- ===
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- # 1) MOOSE Cargo System.
-- 
-- #### Those who have used the mission editor, know that the DCS mission editor provides cargo facilities.
-- However, these are merely static objects. Wouldn't it be nice if cargo could bring a new dynamism into your
-- simulations? Where various objects of various types could be treated also as cargo?
-- 
-- This is what MOOSE brings to you, a complete new cargo object model that used the cargo capabilities of 
-- DCS world, but enhances it.
-- 
-- MOOSE Cargo introduces also a new concept, called a "carrier". These can be:
-- 
--   - Helicopters
--   - Planes
--   - Ground Vehicles
--   - Ships
-- 
-- With the MOOSE Cargo system, you can:
-- 
--   - Take full control of the cargo as objects within your script (see below).
--   - Board/Unboard infantry into carriers. Also other objects can be boarded, like mortars.
--   - Load/Unload dcs world cargo objects into carriers.
--   - Load/Unload other static objects into carriers (like tires etc).
--   - Slingload cargo objects.
--   - Board units one by one...
--   
-- # 2) MOOSE Cargo Objects.
-- 
-- In order to make use of the MOOSE cargo system, you need to **declare** the DCS objects as MOOSE cargo objects!
-- 
-- This sounds complicated, but it is actually quite simple.
-- 
-- See here an example:
-- 
--     local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 )
--     
-- The above code declares a MOOSE cargo object called `EngineerCargoGroup`.
-- It actually just refers to an infantry group created within the sim called `"Engineers"`.
-- The infantry group now becomes controlled by the MOOSE cargo object `EngineerCargoGroup`.
-- A MOOSE cargo object also has properties, like the type of cargo, the logical name, and the reporting range.
-- 
-- There are 4 types of MOOSE cargo objects possible, each represented by its own class:
-- 
--   - @{Cargo.CargoGroup#CARGO_GROUP}: A MOOSE cargo that is represented by a DCS world GROUP object.
--   - @{Cargo.CargoCrate#CARGO_CRATE}: A MOOSE cargo that is represented by a DCS world cargo object (static object).
--   - @{Cargo.CargoUnit#CARGO_UNIT}: A MOOSE cargo that is represented by a DCS world unit object or static object.
--   - @{Cargo.CargoSlingload#CARGO_SLINGLOAD}: A MOOSE cargo that is represented by a DCS world cargo object (static object), that can be slingloaded.
--   
-- Note that a CARGO crate is not meant to be slingloaded (it can, but it is not **meant** to be handled like that.
-- Instead, a CARGO_CRATE is able to load itself into the bays of a carrier.
-- 
-- Each of these MOOSE cargo objects behave in its own way, and have methods to be handled.
-- 
--     local InfantryGroup = GROUP:FindByName( "Infantry" )
--     local InfantryCargo = CARGO_GROUP:New( InfantryGroup, "Engineers", "Infantry Engineers", 2000 )
--     local CargoCarrier = UNIT:FindByName( "Carrier" )
--     -- This call will make the Cargo run to the CargoCarrier.
--     -- Upon arrival at the CargoCarrier, the Cargo will be Loaded into the Carrier.
--     -- This process is now fully automated.
--     InfantryCargo:Board( CargoCarrier, 25 ) 
-- 
-- The above would create a MOOSE cargo object called `InfantryCargo`, and using that object,
-- you can board the cargo into the carrier `CargoCarrier`.
-- Simple, isn't it? Told you, and this is only the beginning.
-- 
-- The boarding, unboarding, loading, unloading of cargo is however something that is not meant to be coded manually by mission designers.
-- It would be too low-level and not end-user friendly to deal with cargo handling complexity.
-- Things can become really complex if you want to make cargo being handled and behave in multiple scenarios.
-- 
-- # 3) Cargo Handling Classes, the main engines for mission designers!
-- 
-- For this reason, the MOOSE Cargo System is heavily used by 3 important **cargo handling class hierarchies** within MOOSE,
-- that make cargo come "alive" within your mission in a full automatic manner!
-- 
-- ## 3.1) AI Cargo handlers.
-- 
--   - @{AI.AI_Cargo_APC} will create for you the capability to make an APC group handle cargo.
--   - @{AI.AI_Cargo_Helicopter} will create for you the capability to make a Helicopter group handle cargo.
--   
--   
-- ## 3.2) AI Cargo transportation dispatchers.
-- 
-- There are also dispatchers that make AI work together to transport cargo automatically!!!
--   
--   - @{AI.AI_Cargo_Dispatcher_APC} derived classes will create for your dynamic cargo handlers controlled by AI ground vehicle groups (APCs) to transport cargo between sites.
--   - @{AI.AI_Cargo_Dispatcher_Helicopter} derived classes will create for your dynamic cargo handlers controlled by AI helicopter groups to transport cargo between sites.
-- 
-- ## 3.3) Cargo transportation tasking.
--   
-- And there is cargo transportation tasking for human players.
--   
--   - @{Tasking.Task_CARGO} derived classes will create for you cargo transportation tasks, that allow human players to interact with MOOSE cargo objects to complete tasks.
-- 
-- Please refer to the documentation reflected within these modules to understand the detailed capabilities.
-- 
-- # 4) Cargo SETs.
-- 
-- To make life a bit more easy, MOOSE cargo objects can be grouped into a @{Core.Set#SET_CARGO}.
-- This is a collection of MOOSE cargo objects.
-- 
-- This would work as follows:
-- 
--      -- Define the cargo set.
--      local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart()
--      
--      -- Now add cargo the cargo set.
--      local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 )
--      local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 )
--      local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 )
--      local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 )
--      local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 )
-- 
-- This is a very powerful concept!
-- Instead of having to deal with multiple MOOSE cargo objects yourself, the cargo set capability will group cargo objects into one set.
-- The key is the **cargo type** name given at each cargo declaration!
-- In the above example, the cargo type name is `"Workmaterials"`. Each cargo object declared is given that type name. (the 2nd parameter).
-- What happens now is that the cargo set `CargoSetWorkmaterials` will be added with each cargo object **dynamically** when the cargo object is created.
-- In other words, the cargo set `CargoSetWorkmaterials` will incorporate any `"Workmaterials"` dynamically into its set.
-- 
-- The cargo sets are extremely important for the AI cargo transportation dispatchers and the cargo transporation tasking.
-- 
-- # 5) Declare cargo directly in the mission editor!
-- 
-- But I am not finished! There is something more, that is even more great!
-- Imagine the mission designers having to code all these lines every time it wants to embed cargo within a mission.
--  
--      -- Now add cargo the cargo set.
--      local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 )
--      local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 )
--      local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 )
--      local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 )
--      local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 )
-- 
-- This would be extremely tiring and a huge overload.
-- However, the MOOSE framework allows to declare MOOSE cargo objects within the mission editor!!!
-- 
-- So, at mission startup, MOOSE will search for objects following a special naming convention, and will **create** for you **dynamically
-- cargo objects** at **mission start**!!! -- These cargo objects can then be automatically incorporated within cargo set(s)!!!
-- In other words, your mission will be reduced to about a few lines of code, providing you with a full dynamic cargo handling mission!
-- 
-- ## 5.1) Use \#CARGO tags in the mission editor:
-- 
-- MOOSE can create automatically cargo objects, if the name of the cargo contains the **\#CARGO** tag.
-- When a mission starts, MOOSE will scan all group and static objects it found for the presence of the \#CARGO tag.
-- When found, MOOSE will declare the object as cargo (create in the background a CARGO_ object, like CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD.
-- The creation of these CARGO_ objects will allow to be filtered and automatically added in SET_CARGO objects.
-- In other words, with very minimal code as explained in the above code section, you are able to create vast amounts of cargo objects just from within the editor.
-- 
-- What I talk about is this:
-- 
--      -- BEFORE THIS SCRIPT STARTS, MOOSE WILL ALREADY HAVE SCANNED FOR OBJECTS WITH THE #CARGO TAG IN THE NAME.
--      -- FOR EACH OF THESE OBJECT, MOOSE WILL HAVE CREATED CARGO_ OBJECTS LIKE CARGO_GROUP, CARGO_CRATE AND CARGO_SLINGLOAD.
-- 
--      HQ = GROUP:FindByName( "HQ", "Bravo" )
--      
--      CommandCenter = COMMANDCENTER
--        :New( HQ, "Lima" )
--      
--      Mission = MISSION
--        :New( CommandCenter, "Operation Cargo Fun", "Tactical", "Transport Cargo", coalition.side.RED )
--      
--      TransportGroups = SET_GROUP:New():FilterCoalitions( "blue" ):FilterPrefixes( "Transport" ):FilterStart()
--      
--      TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups )
--      
--      -- This is the most important now. You setup a new SET_CARGO filtering the relevant type.
--      -- The actual cargo objects are now created by MOOSE in the background.
--      -- Each cargo is setup in the Mission Editor using the #CARGO tag in the group name.
--      -- This allows a truly dynamic setup.
--      local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart()
--      
--      local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." )
--      TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) )
--      
-- The above code example has the `CargoSetWorkmaterials`, which is a SET_CARGO collection and will include the CARGO_ objects of the type "Workmaterials".    
-- And there is NO cargo object actually declared within the script! However, if you would open the mission, there would be hundreds of cargo objects...
-- 
-- The \#CARGO tag even allows for several options to be specified, which are important to learn.
-- 
-- ## 5.2) The \#CARGO tag to create CARGO_GROUP objects:
-- 
-- You can also use the \#CARGO tag on **group** objects of the mission editor.
-- 
-- For example, the following #CARGO naming in the **group name** of the object, will create a CARGO_GROUP object when the mission starts.
-- 
--   `Infantry #CARGO(T=Workmaterials,RR=500,NR=25)`
-- 
-- This will create a CARGO_GROUP object:
-- 
--    * with the group name `Infantry #CARGO`
--    * is of type `Workmaterials`
--    * will report when a carrier is within 500 meters
--    * will board to carriers when the carrier is within 500 meters from the cargo object
--    * will disappear when the cargo is within 25 meters from the carrier during boarding
-- 
-- So the overall syntax of the #CARGO naming tag and arguments are:
-- 
--   `GroupName #CARGO(T=CargoTypeName,RR=Range,NR=Range)`
-- 
--    * **T=** Provide a text that contains the type name of the cargo object. This type name can be used to filter cargo within a SET_CARGO object.
--    * **RR=** Provide the minimal range in meters when the report to the carrier, and board to the carrier.
--      Note that this option is optional, so can be omitted. The default value of the RR is 250 meters.
--    * **NR=** Provide the maximum range in meters when the cargo units will be boarded within the carrier during boarding.
--      Note that this option is optional, so can be omitted. The default value of the RR is 10 meters.
-- 
-- ## 5.2) The \#CARGO tag to create CARGO_CRATE or CARGO_SLINGLOAD objects:
-- 
-- You can also use the \#CARGO tag on **static** objects, including **static cargo** objects of the mission editor.
-- 
-- For example, the following #CARGO naming in the **static name** of the object, will create a CARGO_CRATE object when the mission starts.
-- 
--   `Static #CARGO(T=Workmaterials,C=CRATE,RR=500,NR=25)`
-- 
-- This will create a CARGO_CRATE object:
-- 
--    * with the group name `Static #CARGO`
--    * is of type `Workmaterials`
--    * is of category `CRATE` (as opposed to `SLING`)
--    * will report when a carrier is within 500 meters
--    * will board to carriers when the carrier is within 500 meters from the cargo object
--    * will disappear when the cargo is within 25 meters from the carrier during boarding
-- 
-- So the overall syntax of the #CARGO naming tag and arguments are:
-- 
--   `StaticName #CARGO(T=CargoTypeName,C=Category,RR=Range,NR=Range)`
-- 
--    * **T=** Provide a text that contains the type name of the cargo object. This type name can be used to filter cargo within a SET_CARGO object.
--    * **C=** Provide either `CRATE` or `SLING` to have this static created as a CARGO_CRATE or CARGO_SLINGLOAD respectively.
--    * **RR=** Provide the minimal range in meters when the report to the carrier, and board to the carrier.
--      Note that this option is optional, so can be omitted. The default value of the RR is 250 meters.
--    * **NR=** Provide the maximum range in meters when the cargo units will be boarded within the carrier during boarding.
--      Note that this option is optional, so can be omitted. The default value of the RR is 10 meters.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Cargo.Cargo
-- @image Cargo.JPG

-- Events

-- Board

--- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier.
-- The cargo must be in the **UnLoaded** state.
-- @function [parent=#CARGO] Board
-- @param #CARGO self
-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo.
-- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision).

--- Boards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo to the Carrier.
-- The cargo must be in the **UnLoaded** state.
-- @function [parent=#CARGO] __Board
-- @param #CARGO self
-- @param #number DelaySeconds The amount of seconds to delay the action.
-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo.
-- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision).


-- UnBoard

--- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier.
-- The cargo must be in the **Loaded** state.
-- @function [parent=#CARGO] UnBoard
-- @param #CARGO self
-- @param Core.Point#COORDINATE ToPointVec2 (optional) @{Core.Point#COORDINATE) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location.

--- UnBoards the cargo to a Carrier. The event will create a movement (= running or driving) of the cargo from the Carrier.
-- The cargo must be in the **Loaded** state.
-- @function [parent=#CARGO] __UnBoard
-- @param #CARGO self
-- @param #number DelaySeconds The amount of seconds to delay the action.
-- @param Core.Point#COORDINATE ToPointVec2 (optional) @{Core.Point#COORDINATE) to where the cargo should run after onboarding. If not provided, the cargo will run to 60 meters behind the Carrier location.


-- Load

--- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading.
-- The cargo must be in the **UnLoaded** state.
-- @function [parent=#CARGO] Load
-- @param #CARGO self
-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo.

--- Loads the cargo to a Carrier. The event will load the cargo into the Carrier regardless of its position. There will be no movement simulated of the cargo loading.
-- The cargo must be in the **UnLoaded** state.
-- @function [parent=#CARGO] __Load
-- @param #CARGO self
-- @param #number DelaySeconds The amount of seconds to delay the action.
-- @param Wrapper.Controllable#CONTROLLABLE ToCarrier The Carrier that will hold the cargo.


-- UnLoad

--- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading.
-- The cargo must be in the **Loaded** state.
-- @function [parent=#CARGO] UnLoad
-- @param #CARGO self
-- @param Core.Point#COORDINATE ToPointVec2 (optional) @{Core.Point#COORDINATE) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location.

--- UnLoads the cargo to a Carrier. The event will unload the cargo from the Carrier. There will be no movement simulated of the cargo loading.
-- The cargo must be in the **Loaded** state.
-- @function [parent=#CARGO] __UnLoad
-- @param #CARGO self
-- @param #number DelaySeconds The amount of seconds to delay the action.
-- @param Core.Point#COORDINATE ToPointVec2 (optional) @{Core.Point#COORDINATE) to where the cargo will be placed after unloading. If not provided, the cargo will be placed 60 meters behind the Carrier location.

-- State Transition Functions

-- UnLoaded

--- @function [parent=#CARGO] OnLeaveUnLoaded
-- @param #CARGO self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable
-- @return #boolean

--- @function [parent=#CARGO] OnEnterUnLoaded
-- @param #CARGO self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable

-- Loaded

--- @function [parent=#CARGO] OnLeaveLoaded
-- @param #CARGO self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable
-- @return #boolean

--- @function [parent=#CARGO] OnEnterLoaded
-- @param #CARGO self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable

-- Boarding

--- @function [parent=#CARGO] OnLeaveBoarding
-- @param #CARGO self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable
-- @return #boolean

--- @function [parent=#CARGO] OnEnterBoarding
-- @param #CARGO self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable
-- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision).

-- UnBoarding

--- @function [parent=#CARGO] OnLeaveUnBoarding
-- @param #CARGO self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable
-- @return #boolean

--- @function [parent=#CARGO] OnEnterUnBoarding
-- @param #CARGO self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable


-- TODO: Find all Carrier objects and make the type of the Carriers Wrapper.Unit#UNIT in the documentation.

CARGOS = {}

do -- CARGO

  -- @type CARGO
  -- @extends Core.Fsm#FSM_PROCESS
  -- @field #string Type A string defining the type of the cargo. eg. Engineers, Equipment, Screwdrivers.
  -- @field #string Name A string defining the name of the cargo. The name is the unique identifier of the cargo.
  -- @field #number Weight A number defining the weight of the cargo. The weight is expressed in kg.
  -- @field #number NearRadius (optional) A number defining the radius in meters when the cargo is near to a Carrier, so that it can be loaded.
  -- @field Wrapper.Unit#UNIT CargoObject The alive DCS object representing the cargo. This value can be nil, meaning, that the cargo is not represented anywhere...
  -- @field Wrapper.Client#CLIENT CargoCarrier The alive DCS object carrying the cargo. This value can be nil, meaning, that the cargo is not contained anywhere...
  -- @field #boolean Slingloadable This flag defines if the cargo can be slingloaded.
  -- @field #boolean Moveable This flag defines if the cargo is moveable.
  -- @field #boolean Representable This flag defines if the cargo can be represented by a DCS Unit.
  -- @field #boolean Containable This flag defines if the cargo can be contained within a DCS Unit.

  --- Defines the core functions that defines a cargo object within MOOSE.
  -- 
  -- A cargo is a **logical object** defined that is available for transport, and has a life status within a simulation.
  -- 
  -- CARGO is not meant to be used directly by mission designers, but provides a base class for **concrete cargo implementation classes** to handle:
  -- 
  --   * Cargo **group objects**, implemented by the @{Cargo.CargoGroup#CARGO_GROUP} class.
  --   * Cargo **Unit objects**, implemented by the @{Cargo.CargoUnit#CARGO_UNIT} class.
  --   * Cargo **Crate objects**, implemented by the @{Cargo.CargoCrate#CARGO_CRATE} class.
  --   * Cargo **Sling Load objects**, implemented by the @{Cargo.CargoSlingload#CARGO_SLINGLOAD} class.
  --
  -- The above cargo classes are used by the AI\_CARGO\_ classes to allow AI groups to transport cargo:
  -- 
  --   * AI Armoured Personnel Carriers to transport cargo and engage in battles, using the @{AI.AI_Cargo_APC#AI_CARGO_APC} class.
  --   * AI Helicopters to transport cargo, using the @{AI.AI_Cargo_Helicopter#AI_CARGO_HELICOPTER} class.
  --   * AI Planes to transport cargo, using the @{AI.AI_Cargo_Airplane#AI_CARGO_AIRPLANE} class.
  --   * AI Ships is planned.
  -- 
  -- The above cargo classes are also used by the TASK\_CARGO\_ classes to allow human players to transport cargo as part of a tasking:
  -- 
  --   * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} to transport cargo by human players.
  --   * @{Tasking.Task_Cargo_Transport#TASK_CARGO_CSAR} to transport downed pilots by human players.
  -- 
  -- 
  -- The CARGO is a state machine: it manages the different events and states of the cargo.
  -- All derived classes from CARGO follow the same state machine, expose the same cargo event functions, and provide the same cargo states.
  -- 
  -- ## CARGO Events:
  -- 
  --   * @{#CARGO.Board}( ToCarrier ):  Boards the cargo to a carrier.
  --   * @{#CARGO.Load}( ToCarrier ): Loads the cargo into a carrier, regardless of its position.
  --   * @{#CARGO.UnBoard}( ToPointVec2 ): UnBoard the cargo from a carrier. This will trigger a movement of the cargo to the option ToPointVec2.
  --   * @{#CARGO.UnLoad}( ToPointVec2 ): UnLoads the cargo from a carrier.
  --   * @{#CARGO.Destroyed}( Controllable ): The cargo is dead. The cargo process will be ended.
  -- 
  -- @field #CARGO
  CARGO = {
    ClassName = "CARGO",
    Type = nil,
    Name = nil,
    Weight = nil,
    CargoObject = nil,
    CargoCarrier = nil,
    Representable = false,
    Slingloadable = false,
    Moveable = false,
    Containable = false,
    Reported = {},
  }

  -- @type CARGO.CargoObjects
  -- @map < #string, Wrapper.Positionable#POSITIONABLE > The alive POSITIONABLE objects representing the the cargo.

  --- CARGO Constructor. This class is an abstract class and should not be instantiated.
  -- @param #CARGO self
  -- @param #string Type
  -- @param #string Name
  -- @param #number Weight
  -- @param #number LoadRadius (optional)
  -- @param #number NearRadius (optional)
  -- @return #CARGO
  function CARGO:New( Type, Name, Weight, LoadRadius, NearRadius ) --R2.1

    local self = BASE:Inherit( self, FSM:New() ) -- #CARGO
    self:T( { Type, Name, Weight, LoadRadius, NearRadius } )

    self:SetStartState( "UnLoaded" )
    self:AddTransition( { "UnLoaded", "Boarding" }, "Board", "Boarding" )
    self:AddTransition( "Boarding" , "Boarding", "Boarding" )
    self:AddTransition( "Boarding", "CancelBoarding", "UnLoaded" )
    self:AddTransition( "Boarding", "Load", "Loaded" )
    self:AddTransition( "UnLoaded", "Load", "Loaded" )
    self:AddTransition( "Loaded", "UnBoard", "UnBoarding" )
    self:AddTransition( "UnBoarding", "UnBoarding", "UnBoarding" )
    self:AddTransition( "UnBoarding", "UnLoad", "UnLoaded" )
    self:AddTransition( "Loaded", "UnLoad", "UnLoaded" )
    self:AddTransition( "*", "Damaged", "Damaged" )
    self:AddTransition( "*", "Destroyed", "Destroyed" )
    self:AddTransition( "*", "Respawn", "UnLoaded" )
    self:AddTransition( "*", "Reset", "UnLoaded" )

    self.Type = Type
    self.Name = Name
    self.Weight = Weight or 0
    self.CargoObject = nil -- Wrapper.Group#GROUP
    self.CargoCarrier = nil -- Wrapper.Client#CLIENT
    self.Representable = false
    self.Slingloadable = false
    self.Moveable = false
    self.Containable = false

    self.CargoLimit = 0

    self.LoadRadius = LoadRadius or 500
    --self.NearRadius = NearRadius or 25

    self:SetDeployed( false )

    self.CargoScheduler = SCHEDULER:New()

    CARGOS[self.Name] = self

    return self
  end

  --- Find a CARGO in the _DATABASE.
  -- @param #CARGO self
  -- @param #string CargoName The Cargo Name.
  -- @return #CARGO self
  function CARGO:FindByName( CargoName )

    local CargoFound = _DATABASE:FindCargo( CargoName )
    return CargoFound
  end

  --- Get the x position of the cargo.
  -- @param #CARGO self
  -- @return #number
  function CARGO:GetX()
    if self:IsLoaded() then
      return self.CargoCarrier:GetCoordinate().x
    else
      return self.CargoObject:GetCoordinate().x
    end
  end

  --- Get the y position of the cargo.
  -- @param #CARGO self
  -- @return #number
  function CARGO:GetY()
    if self:IsLoaded() then
      return self.CargoCarrier:GetCoordinate().z
    else
      return self.CargoObject:GetCoordinate().z
    end
  end

  --- Get the heading of the cargo.
  -- @param #CARGO self
  -- @return #number
  function CARGO:GetHeading()
    if self:IsLoaded() then
      return self.CargoCarrier:GetHeading()
    else
      return self.CargoObject:GetHeading()
    end
  end

  --- Check if the cargo can be Slingloaded.
  -- @param #CARGO self
  function CARGO:CanSlingload()
    return false
  end

  --- Check if the cargo can be Boarded.
  -- @param #CARGO self
  function CARGO:CanBoard()
    return true
  end

  --- Check if the cargo can be Unboarded.
  -- @param #CARGO self
  function CARGO:CanUnboard()
    return true
  end

  --- Check if the cargo can be Loaded.
  -- @param #CARGO self
  function CARGO:CanLoad()
    return true
  end

  --- Check if the cargo can be Unloaded.
  -- @param #CARGO self
  function CARGO:CanUnload()
    return true
  end

  --- Destroy the cargo.
  -- @param #CARGO self
  function CARGO:Destroy()
    if self.CargoObject then
      self.CargoObject:Destroy()
    end
    self:Destroyed()
  end

  --- Get the name of the Cargo.
  -- @param #CARGO self
  -- @return #string The name of the Cargo.
  function CARGO:GetName() --R2.1
    return self.Name
  end

  --- Get the current active object representing or being the Cargo.
  -- @param #CARGO self
  -- @return Wrapper.Positionable#POSITIONABLE The object representing or being the Cargo.
  function CARGO:GetObject()
    if self:IsLoaded() then
      return self.CargoCarrier
    else
      return self.CargoObject
    end
  end

  --- Get the object name of the Cargo.
  -- @param #CARGO self
  -- @return #string The object name of the Cargo.
  function CARGO:GetObjectName() --R2.1
    if self:IsLoaded() then
      return self.CargoCarrier:GetName()
    else
      return self.CargoObject:GetName()
    end
  end

  --- Get the amount of Cargo.
  -- @param #CARGO self
  -- @return #number The amount of Cargo.
  function CARGO:GetCount()
    return 1
  end

  --- Get the type of the Cargo.
  -- @param #CARGO self
  -- @return #string The type of the Cargo.
  function CARGO:GetType()
    return self.Type
  end

  --- Get the transportation method of the Cargo.
  -- @param #CARGO self
  -- @return #string The transportation method of the Cargo.
  function CARGO:GetTransportationMethod()
    return self.TransportationMethod
  end

  --- Get the coalition of the Cargo.
  -- @param #CARGO self
  -- @return Coalition
  function CARGO:GetCoalition()
    if self:IsLoaded() then
      return self.CargoCarrier:GetCoalition()
    else
      return self.CargoObject:GetCoalition()
    end
  end

  --- Get the current coordinates of the Cargo.
  -- @param #CARGO self
  -- @return Core.Point#COORDINATE The coordinates of the Cargo.
  function CARGO:GetCoordinate()
    return self.CargoObject:GetCoordinate()
  end

  --- Check if cargo is destroyed.
  -- @param #CARGO self
  -- @return #boolean true if destroyed
  function CARGO:IsDestroyed()
    return self:Is( "Destroyed" )
  end

  --- Check if cargo is loaded.
  -- @param #CARGO self
  -- @return #boolean true if loaded
  function CARGO:IsLoaded()
    return self:Is( "Loaded" )
  end

  --- Check if cargo is loaded.
  -- @param #CARGO self
  -- @param Wrapper.Unit#UNIT Carrier
  -- @return #boolean true if loaded
  function CARGO:IsLoadedInCarrier( Carrier )
    return self.CargoCarrier and self.CargoCarrier:GetName() == Carrier:GetName()
  end

  --- Check if cargo is unloaded.
  -- @param #CARGO self
  -- @return #boolean true if unloaded
  function CARGO:IsUnLoaded()
    return self:Is( "UnLoaded" )
  end

  --- Check if cargo is boarding.
  -- @param #CARGO self
  -- @return #boolean true if boarding
  function CARGO:IsBoarding()
    return self:Is( "Boarding" )
  end

  --- Check if cargo is unboarding.
  -- @param #CARGO self
  -- @return #boolean true if unboarding
  function CARGO:IsUnboarding()
    return self:Is( "UnBoarding" )
  end

  --- Check if cargo is alive.
  -- @param #CARGO self
  -- @return #boolean true if unloaded
  function CARGO:IsAlive()

    if self:IsLoaded() then
      return self.CargoCarrier:IsAlive()
    else
      return self.CargoObject:IsAlive()
    end
  end

  --- Set the cargo as deployed.
  -- @param #CARGO self
  -- @param #boolean Deployed true if the cargo is to be deployed. false or nil otherwise.
  function CARGO:SetDeployed( Deployed )
    self.Deployed = Deployed
  end

  --- Is the cargo deployed
  -- @param #CARGO self
  -- @return #boolean
  function CARGO:IsDeployed()
    return self.Deployed
  end

  --- Template method to spawn a new representation of the CARGO in the simulator.
  -- @param #CARGO self
  -- @return #CARGO
  function CARGO:Spawn( PointVec2 )
    self:T()

  end

  --- Signal a flare at the position of the CARGO.
  -- @param #CARGO self
  -- @param Utilities.Utils#FLARECOLOR FlareColor
  function CARGO:Flare( FlareColor )
    if self:IsUnLoaded() then
      trigger.action.signalFlare( self.CargoObject:GetVec3(), FlareColor , 0 )
    end
  end

  --- Signal a white flare at the position of the CARGO.
  -- @param #CARGO self
  function CARGO:FlareWhite()
    self:Flare( trigger.flareColor.White )
  end

  --- Signal a yellow flare at the position of the CARGO.
  -- @param #CARGO self
  function CARGO:FlareYellow()
    self:Flare( trigger.flareColor.Yellow )
  end

  --- Signal a green flare at the position of the CARGO.
  -- @param #CARGO self
  function CARGO:FlareGreen()
    self:Flare( trigger.flareColor.Green )
  end

  --- Signal a red flare at the position of the CARGO.
  -- @param #CARGO self
  function CARGO:FlareRed()
    self:Flare( trigger.flareColor.Red )
  end

  --- Smoke the CARGO.
  -- @param #CARGO self
  -- @param Utilities.Utils#SMOKECOLOR SmokeColor The color of the smoke.
  -- @param #number Radius The radius of randomization around the center of the Cargo.
  function CARGO:Smoke( SmokeColor, Radius )
    if self:IsUnLoaded() then
      if Radius then
        trigger.action.smoke( self.CargoObject:GetRandomVec3( Radius ), SmokeColor )
      else
        trigger.action.smoke( self.CargoObject:GetVec3(), SmokeColor )
      end
    end
  end

  --- Smoke the CARGO Green.
  -- @param #CARGO self
  function CARGO:SmokeGreen()
    self:Smoke( trigger.smokeColor.Green, Range )
  end

  --- Smoke the CARGO Red.
  -- @param #CARGO self
  function CARGO:SmokeRed()
    self:Smoke( trigger.smokeColor.Red, Range )
  end

  --- Smoke the CARGO White.
  -- @param #CARGO self
  function CARGO:SmokeWhite()
    self:Smoke( trigger.smokeColor.White, Range )
  end

  --- Smoke the CARGO Orange.
  -- @param #CARGO self
  function CARGO:SmokeOrange()
    self:Smoke( trigger.smokeColor.Orange, Range )
  end

  --- Smoke the CARGO Blue.
  -- @param #CARGO self
  function CARGO:SmokeBlue()
    self:Smoke( trigger.smokeColor.Blue, Range )
  end

  --- Set the Load radius, which is the radius till when the Cargo can be loaded.
  -- @param #CARGO self
  -- @param #number LoadRadius The radius till Cargo can be loaded.
  -- @return #CARGO
  function CARGO:SetLoadRadius( LoadRadius )
    self.LoadRadius = LoadRadius or 150
  end

  --- Get the Load radius, which is the radius till when the Cargo can be loaded.
  -- @param #CARGO self
  -- @return #number The radius till Cargo can be loaded.
  function CARGO:GetLoadRadius()
    return self.LoadRadius
  end

  --- Check if Cargo is in the LoadRadius for the Cargo to be Boarded or Loaded.
  -- @param #CARGO self
  -- @param Core.Point#COORDINATE Coordinate
  -- @return #boolean true if the CargoGroup is within the loading radius.
  function CARGO:IsInLoadRadius( Coordinate )
    self:T( { Coordinate, LoadRadius = self.LoadRadius } )

    local Distance = 0
    if self:IsUnLoaded() then
      local CargoCoordinate = self.CargoObject:GetCoordinate()
      Distance = Coordinate:Get2DDistance( CargoCoordinate )
      self:T( Distance )
      if Distance <= self.LoadRadius then
        return true
      end
    end

    return false
  end

  --- Check if the Cargo can report itself to be Boarded or Loaded.
  -- @param #CARGO self
  -- @param Core.Point#COORDINATE Coordinate
  -- @return #boolean true if the Cargo can report itself.
  function CARGO:IsInReportRadius( Coordinate )
    self:T( { Coordinate } )

    local Distance = 0
    if self:IsUnLoaded() then
      Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() )
      self:T( Distance )
      if Distance <= self.LoadRadius then
        return true
      end
    end

    return false
  end


  --- Check if CargoCarrier is near the coordinate within NearRadius.
  -- @param #CARGO self
  -- @param Core.Point#COORDINATE Coordinate
  -- @param #number NearRadius The radius when the cargo will board the Carrier (to avoid collision).
  -- @return #boolean
  function CARGO:IsNear( Coordinate, NearRadius )
    --self:T( { PointVec2 = PointVec2, NearRadius = NearRadius } )

    if self.CargoObject:IsAlive() then
      --local Distance = PointVec2:Get2DDistance( self.CargoObject:GetPointVec2() )
      --self:T( { CargoObjectName = self.CargoObject:GetName() } )
      --self:T( { CargoObjectVec2 = self.CargoObject:GetVec2() } )
      --self:T( { PointVec2 = PointVec2:GetVec2() } )
      local Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() )
      --self:T( { Distance = Distance, NearRadius = NearRadius or "nil" }  )

      if Distance <= NearRadius then
        --self:T( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = true } )
        return true
      end
    end

    --self:T( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = false } )
    return false
  end

  --- Check if Cargo is the given @{Core.Zone}.
  -- @param #CARGO self
  -- @param Core.Zone#ZONE_BASE Zone
  -- @return #boolean **true** if cargo is in the Zone, **false** if cargo is not in the Zone.
  function CARGO:IsInZone( Zone )
    --self:T( { Zone } )

    if self:IsLoaded() then
      return Zone:IsPointVec2InZone( self.CargoCarrier:GetPointVec2() )
    else
      --self:T( { Size = self.CargoObject:GetSize(), Units = self.CargoObject:GetUnits() } )
      if self.CargoObject:GetSize() ~= 0 then
        return Zone:IsPointVec2InZone( self.CargoObject:GetPointVec2() )
      else
        return false
      end
    end

    return nil

  end

  --- Get the current PointVec2 of the cargo.
  -- @param #CARGO self
  -- @return Core.Point#COORDINATE
  function CARGO:GetPointVec2()
    return self.CargoObject:GetPointVec2()
  end

  --- Get the current Coordinate of the cargo.
  -- @param #CARGO self
  -- @return Core.Point#COORDINATE
  function CARGO:GetCoordinate()
    return self.CargoObject:GetCoordinate()
  end

  --- Get the weight of the cargo.
  -- @param #CARGO self
  -- @return #number Weight The weight in kg.
  function CARGO:GetWeight()
    return self.Weight 
  end

  --- Set the weight of the cargo.
  -- @param #CARGO self
  -- @param #number Weight The weight in kg.
  -- @return #CARGO
  function CARGO:SetWeight( Weight )
    self.Weight = Weight
    return self
  end

  --- Get the volume of the cargo.
  -- @param #CARGO self
  -- @return #number Volume The volume in kg.
  function CARGO:GetVolume()
    return self.Volume 
  end

  --- Set the volume of the cargo.
  -- @param #CARGO self
  -- @param #number Volume The volume in kg.
  -- @return #CARGO
  function CARGO:SetVolume( Volume )
    self.Volume = Volume
    return self
  end

  --- Send a CC message to a @{Wrapper.Group}.
  -- @param #CARGO self
  -- @param #string Message
  -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group.
  -- @param #string Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown.
  function CARGO:MessageToGroup( Message, CarrierGroup, Name )

    MESSAGE:New( Message, 20, "Cargo " .. self:GetName() ):ToGroup( CarrierGroup )

  end

  --- Report to a Carrier Group.
  -- @param #CARGO self
  -- @param #string Action The string describing the action for the cargo.
  -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group to send the report to.
  -- @return #CARGO
  function CARGO:Report( ReportText, Action, CarrierGroup )

    if not self.Reported[CarrierGroup] or not self.Reported[CarrierGroup][Action] then
      self.Reported[CarrierGroup] = {}
      self.Reported[CarrierGroup][Action] = true  
      self:MessageToGroup( ReportText, CarrierGroup )
      if self.ReportFlareColor then
        if not self.Reported[CarrierGroup]["Flaring"] then
          self:Flare( self.ReportFlareColor )
          self.Reported[CarrierGroup]["Flaring"] = true
        end
      end
      if self.ReportSmokeColor then
        if not self.Reported[CarrierGroup]["Smoking"] then
          self:Smoke( self.ReportSmokeColor )
          self.Reported[CarrierGroup]["Smoking"] = true
        end
      end
    end
  end

  --- Report to a Carrier Group with a Flaring signal.
  -- @param #CARGO self
  -- @param Utilities.Utils#UTILS.FlareColor FlareColor the color of the flare.
  -- @return #CARGO
  function CARGO:ReportFlare( FlareColor )

    self.ReportFlareColor = FlareColor 
  end

  --- Report to a Carrier Group with a Smoking signal.
  -- @param #CARGO self
  -- @param Utilities.Utils#UTILS.SmokeColor SmokeColor the color of the smoke.
  -- @return #CARGO
  function CARGO:ReportSmoke( SmokeColor )

    self.ReportSmokeColor = SmokeColor 
  end

  --- Reset the reporting for a Carrier Group.
  -- @param #CARGO self
  -- @param #string Action The string describing the action for the cargo.
  -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group to send the report to.
  -- @return #CARGO
  function CARGO:ReportReset( Action, CarrierGroup )

    self.Reported[CarrierGroup][Action] = nil
  end

  --- Reset all the reporting for a Carrier Group.
  -- @param #CARGO self
  -- @param Wrapper.Group#GROUP CarrierGroup The Carrier Group to send the report to.
  -- @return #CARGO
  function CARGO:ReportResetAll( CarrierGroup )

    self.Reported[CarrierGroup] = nil
  end

  --- Respawn the cargo when destroyed
  -- @param #CARGO self
  -- @param #boolean RespawnDestroyed
  function CARGO:RespawnOnDestroyed( RespawnDestroyed )

    if RespawnDestroyed then
      self.onenterDestroyed = function( self )
        self:Respawn()
      end
    else
      self.onenterDestroyed = nil
    end

  end

end -- CARGO

do -- CARGO_REPRESENTABLE

  -- @type CARGO_REPRESENTABLE
  -- @extends #CARGO
  -- @field test

  --- Models CARGO that is representable by a Unit.
  -- @field #CARGO_REPRESENTABLE CARGO_REPRESENTABLE
  CARGO_REPRESENTABLE = {
    ClassName = "CARGO_REPRESENTABLE"
  }

  --- CARGO_REPRESENTABLE Constructor.
  -- @param #CARGO_REPRESENTABLE self
  -- @param Wrapper.Positionable#POSITIONABLE CargoObject The cargo object.
  -- @param #string Type Type name
  -- @param #string Name Name.
  -- @param #number LoadRadius (optional) Radius in meters.
  -- @param #number NearRadius (optional) Radius in meters when the cargo is loaded into the carrier.
  -- @return #CARGO_REPRESENTABLE
  function CARGO_REPRESENTABLE:New( CargoObject, Type, Name, LoadRadius, NearRadius )

    -- Inherit CARGO.
    local self = BASE:Inherit( self, CARGO:New( Type, Name, 0, LoadRadius, NearRadius ) ) -- #CARGO_REPRESENTABLE
    self:T( { Type, Name, LoadRadius, NearRadius } )

    -- Descriptors.
    local Desc=CargoObject:GetDesc()
    self:T({Desc=Desc})

    -- Weight.
    local Weight = math.random( 80, 120 )

    -- Adjust weight..
    if Desc then
      if Desc.typeName == "2B11 mortar" then
        Weight = 210
      else
        Weight = Desc.massEmpty
      end
    end

    -- Set weight.
    self:SetWeight( Weight )

    return self
  end

  --- CARGO_REPRESENTABLE Destructor.
  -- @param #CARGO_REPRESENTABLE self
  -- @return #CARGO_REPRESENTABLE
  function CARGO_REPRESENTABLE:Destroy()

    -- Cargo objects are deleted from the _DATABASE and SET_CARGO objects.
    self:T( { CargoName = self:GetName() } )
    --_EVENTDISPATCHER:CreateEventDeleteCargo( self )

    return self
  end

  --- Route a cargo unit to a PointVec2.
  -- @param #CARGO_REPRESENTABLE self
  -- @param Core.Point#COORDINATE ToPointVec2
  -- @param #number Speed
  -- @return #CARGO_REPRESENTABLE
  function CARGO_REPRESENTABLE:RouteTo( ToPointVec2, Speed )
    self:F2( ToPointVec2 )

    local Points = {}

    local PointStartVec2 = self.CargoObject:GetPointVec2()

    Points[#Points+1] = PointStartVec2:WaypointGround( Speed )
    Points[#Points+1] = ToPointVec2:WaypointGround( Speed )

    local TaskRoute = self.CargoObject:TaskRoute( Points )
    self.CargoObject:SetTask( TaskRoute, 2 )
    return self
  end

  --- Send a message to a @{Wrapper.Group} through a communication channel near the cargo.
  -- @param #CARGO_REPRESENTABLE self
  -- @param #string Message
  -- @param Wrapper.Group#GROUP TaskGroup
  -- @param #string Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown.
  function CARGO_REPRESENTABLE:MessageToGroup( Message, TaskGroup, Name )

    local CoordinateZone = ZONE_RADIUS:New( "Zone" , self:GetCoordinate():GetVec2(), 500 )
    CoordinateZone:Scan( { Object.Category.UNIT } )
    for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do
      local NearUnit = UNIT:Find( DCSUnit )
      self:T({NearUnit=NearUnit})
      local NearUnitCoalition = NearUnit:GetCoalition()
      local CargoCoalition = self:GetCoalition()
      if NearUnitCoalition == CargoCoalition then
        local Attributes = NearUnit:GetDesc()
        self:T({Desc=Attributes})
        if NearUnit:HasAttribute( "Trucks" ) then
          MESSAGE:New( Message, 20, NearUnit:GetCallsign() .. " reporting - Cargo " .. self:GetName() ):ToGroup( TaskGroup )
          break
        end
      end
    end

  end

end -- CARGO_REPRESENTABLE

do -- CARGO_REPORTABLE

    -- @type CARGO_REPORTABLE
    -- @extends #CARGO
    CARGO_REPORTABLE = {
      ClassName = "CARGO_REPORTABLE"
    }

  --- CARGO_REPORTABLE Constructor.
  -- @param #CARGO_REPORTABLE self
  -- @param #string Type
  -- @param #string Name
  -- @param #number Weight
  -- @param #number LoadRadius (optional)
  -- @param #number NearRadius (optional)
  -- @return #CARGO_REPORTABLE
  function CARGO_REPORTABLE:New( Type, Name, Weight, LoadRadius, NearRadius )
    local self = BASE:Inherit( self, CARGO:New( Type, Name, Weight, LoadRadius, NearRadius ) ) -- #CARGO_REPORTABLE
    self:T( { Type, Name, Weight, LoadRadius, NearRadius } )

    return self
  end

  --- Send a CC message to a @{Wrapper.Group}.
  -- @param #CARGO_REPORTABLE self
  -- @param #string Message
  -- @param Wrapper.Group#GROUP TaskGroup
  -- @param #string Name (optional) The name of the Group used as a prefix for the message to the Group. If not provided, there will be nothing shown.
  function CARGO_REPORTABLE:MessageToGroup( Message, TaskGroup, Name )

    MESSAGE:New( Message, 20, "Cargo " .. self:GetName() .. " reporting" ):ToGroup( TaskGroup )

  end

end

do -- CARGO_PACKAGE

  -- @type CARGO_PACKAGE
  -- @extends #CARGO_REPRESENTABLE
  CARGO_PACKAGE = {
    ClassName = "CARGO_PACKAGE"
  }

--- CARGO_PACKAGE Constructor.
-- @param #CARGO_PACKAGE self
-- @param Wrapper.Unit#UNIT CargoCarrier The UNIT carrying the package.
-- @param #string Type
-- @param #string Name
-- @param #number Weight
-- @param #number LoadRadius (optional)
-- @param #number NearRadius (optional)
-- @return #CARGO_PACKAGE
function CARGO_PACKAGE:New( CargoCarrier, Type, Name, Weight, LoadRadius, NearRadius )
  local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoCarrier, Type, Name, Weight, LoadRadius, NearRadius ) ) -- #CARGO_PACKAGE
  self:T( { Type, Name, Weight, LoadRadius, NearRadius } )

  self:T( CargoCarrier )
  self.CargoCarrier = CargoCarrier

  return self
end

--- Board Event.
-- @param #CARGO_PACKAGE self
-- @param #string Event
-- @param #string From
-- @param #string To
-- @param Wrapper.Unit#UNIT CargoCarrier
-- @param #number Speed
-- @param #number BoardDistance
-- @param #number Angle
function CARGO_PACKAGE:onafterOnBoard( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle )
  self:T()

  self.CargoInAir = self.CargoCarrier:InAir()

  self:T( self.CargoInAir )

  -- Only move the CargoCarrier to the New CargoCarrier when the New CargoCarrier is not in the air.
  if not self.CargoInAir then

    local Points = {}

    local StartPointVec2 = self.CargoCarrier:GetPointVec2()
    local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees.
    local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle )
    self:T( { CargoCarrierHeading, CargoDeployHeading } )
    local CargoDeployPointVec2 = CargoCarrier:GetPointVec2():Translate( BoardDistance, CargoDeployHeading )

    Points[#Points+1] = StartPointVec2:WaypointGround( Speed )
    Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed )

    local TaskRoute = self.CargoCarrier:TaskRoute( Points )
    self.CargoCarrier:SetTask( TaskRoute, 1 )
  end

  self:Boarded( CargoCarrier, Speed, BoardDistance, LoadDistance, Angle )

end

--- Check if CargoCarrier is near the Cargo to be Loaded.
-- @param #CARGO_PACKAGE self
-- @param Wrapper.Unit#UNIT CargoCarrier
-- @return #boolean
function CARGO_PACKAGE:IsNear( CargoCarrier )
  self:T()

  local CargoCarrierPoint = CargoCarrier:GetCoordinate()

  local Distance = CargoCarrierPoint:Get2DDistance( self.CargoCarrier:GetCoordinate() )
  self:T( Distance )

  if Distance <= self.NearRadius then
    return true
  else
    return false
  end
end

--- Boarded Event.
-- @param #CARGO_PACKAGE self
-- @param #string Event
-- @param #string From
-- @param #string To
-- @param Wrapper.Unit#UNIT CargoCarrier
-- @param #number Speed
-- @param #number BoardDistance
-- @param #number LoadDistance
-- @param #number Angle
function CARGO_PACKAGE:onafterOnBoarded( From, Event, To, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle )
  self:T()

  if self:IsNear( CargoCarrier ) then
    self:__Load( 1, CargoCarrier, Speed, LoadDistance, Angle )
  else
    self:__Boarded( 1, CargoCarrier, Speed, BoardDistance, LoadDistance, Angle )
  end
end

--- UnBoard Event.
-- @param #CARGO_PACKAGE self
-- @param #string Event
-- @param #string From
-- @param #string To
-- @param Wrapper.Unit#UNIT CargoCarrier
-- @param #number Speed
-- @param #number UnLoadDistance
-- @param #number UnBoardDistance
-- @param #number Radius
-- @param #number Angle
function CARGO_PACKAGE:onafterUnBoard( From, Event, To, CargoCarrier, Speed, UnLoadDistance, UnBoardDistance, Radius, Angle )
  self:T()

  self.CargoInAir = self.CargoCarrier:InAir()

  self:T( self.CargoInAir )

  -- Only unboard the cargo when the carrier is not in the air.
  -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea).
  if not self.CargoInAir then

    self:_Next( self.FsmP.UnLoad, UnLoadDistance, Angle )

    local Points = {}

    local StartPointVec2 = CargoCarrier:GetPointVec2()
    local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees.
    local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle )
    self:T( { CargoCarrierHeading, CargoDeployHeading } )
    local CargoDeployPointVec2 = StartPointVec2:Translate( UnBoardDistance, CargoDeployHeading )

    Points[#Points+1] = StartPointVec2:WaypointGround( Speed )
    Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed )

    local TaskRoute = CargoCarrier:TaskRoute( Points )
    CargoCarrier:SetTask( TaskRoute, 1 )
  end

  self:__UnBoarded( 1 , CargoCarrier, Speed )

end

--- UnBoarded Event.
-- @param #CARGO_PACKAGE self
-- @param #string Event
-- @param #string From
-- @param #string To
-- @param Wrapper.Unit#UNIT CargoCarrier
-- @param #number Speed
function CARGO_PACKAGE:onafterUnBoarded( From, Event, To, CargoCarrier, Speed )
  self:T()

  if self:IsNear( CargoCarrier ) then
    self:__UnLoad( 1, CargoCarrier, Speed )
  else
    self:__UnBoarded( 1, CargoCarrier, Speed )
  end
end

--- Load Event.
-- @param #CARGO_PACKAGE self
-- @param #string Event
-- @param #string From
-- @param #string To
-- @param Wrapper.Unit#UNIT CargoCarrier
-- @param #number Speed
-- @param #number LoadDistance
-- @param #number Angle
function CARGO_PACKAGE:onafterLoad( From, Event, To, CargoCarrier, Speed, LoadDistance, Angle )
  self:T()

  self.CargoCarrier = CargoCarrier

  local StartPointVec2 = self.CargoCarrier:GetPointVec2()
  local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees.
  local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle )
  local CargoDeployPointVec2 = StartPointVec2:Translate( LoadDistance, CargoDeployHeading )

  local Points = {}
  Points[#Points+1] = StartPointVec2:WaypointGround( Speed )
  Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed )

  local TaskRoute = self.CargoCarrier:TaskRoute( Points )
  self.CargoCarrier:SetTask( TaskRoute, 1 )

end

--- UnLoad Event.
-- @param #CARGO_PACKAGE self
-- @param #string Event
-- @param #string From
-- @param #string To
-- @param Wrapper.Unit#UNIT CargoCarrier
-- @param #number Speed
-- @param #number Distance
-- @param #number Angle
function CARGO_PACKAGE:onafterUnLoad( From, Event, To, CargoCarrier, Speed, Distance, Angle )
  self:T()

  local StartPointVec2 = self.CargoCarrier:GetPointVec2()
  local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees.
  local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle )
  local CargoDeployPointVec2 = StartPointVec2:Translate( Distance, CargoDeployHeading )

  self.CargoCarrier = CargoCarrier

  local Points = {}
  Points[#Points+1] = StartPointVec2:WaypointGround( Speed )
  Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed )

  local TaskRoute = self.CargoCarrier:TaskRoute( Points )
  self.CargoCarrier:SetTask( TaskRoute, 1 )

end

end
--- **Cargo** - Management of single cargo logistics, which are based on a UNIT object.
--
-- ===
-- 
-- ### [Demo Missions]()
-- 
-- ### [YouTube Playlist]()
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Cargo.CargoUnit
-- @image Cargo_Units.JPG

do -- CARGO_UNIT

  --- Models CARGO in the form of units, which can be boarded, unboarded, loaded, unloaded. 
  -- @type CARGO_UNIT
  -- @extends Cargo.Cargo#CARGO_REPRESENTABLE
  
  --- Defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier.
  -- Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO_UNIT objects to and from carriers.
  -- Note that ground forces behave in a group, and thus, act in formation, regardless if one unit is commanded to move.
  -- 
  -- This class is used in CARGO_GROUP, and is not meant to be used by mission designers individually.
  --
  -- # Developer Note
  -- 
  -- ![Banner Image](..\Images\deprecated.png)
  -- 
  -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
  -- Therefore, this class is considered to be deprecated
  --
  -- ===
  -- 
  -- @field #CARGO_UNIT CARGO_UNIT
  --
  CARGO_UNIT = {
    ClassName = "CARGO_UNIT"
  }

  --- CARGO_UNIT Constructor.
  -- @param #CARGO_UNIT self
  -- @param Wrapper.Unit#UNIT CargoUnit
  -- @param #string Type
  -- @param #string Name
  -- @param #number Weight
  -- @param #number LoadRadius (optional)
  -- @param #number NearRadius (optional)
  -- @return #CARGO_UNIT
  function CARGO_UNIT:New( CargoUnit, Type, Name, LoadRadius, NearRadius )
  
    -- Inherit CARGO_REPRESENTABLE.
    local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, LoadRadius, NearRadius ) ) -- #CARGO_UNIT
    
    -- Debug info.
    self:T({Type=Type, Name=Name, LoadRadius=LoadRadius, NearRadius=NearRadius})
  
    -- Set cargo object.
    self.CargoObject = CargoUnit
  
    -- Set event prio.
    self:SetEventPriority( 5 )
  
    return self
  end
  
  --- Enter UnBoarding State.
  -- @param #CARGO_UNIT self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Core.Point#COORDINATE ToPointVec2
  -- @param #number NearRadius (optional) Defaut 25 m.
  function CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius )
    self:T( { From, Event, To, ToPointVec2, NearRadius } )
  
    local Angle = 180
    local Speed = 60
    local DeployDistance = 9
    local RouteDistance = 60
  
    if From == "Loaded" then
  
      if not self:IsDestroyed() then
  
        local CargoCarrier = self.CargoCarrier -- Wrapper.Controllable#CONTROLLABLE
        
        if CargoCarrier:IsAlive() then
    
          local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2()
          local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees.
          local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle )
      
      
          local CargoRoutePointVec2 = CargoCarrierPointVec2:Translate( RouteDistance, CargoDeployHeading )
          
          
          -- if there is no ToPointVec2 given, then use the CargoRoutePointVec2
          local FromDirectionVec3 = CargoCarrierPointVec2:GetDirectionVec3( ToPointVec2 or CargoRoutePointVec2 )
          local FromAngle = CargoCarrierPointVec2:GetAngleDegrees(FromDirectionVec3)
          local FromPointVec2 = CargoCarrierPointVec2:Translate( DeployDistance, FromAngle )
        --local CargoDeployPointVec2 = CargoCarrierPointVec2:GetRandomCoordinateInRadius( 10, 5 )
  
          ToPointVec2 = ToPointVec2 or CargoCarrierPointVec2:GetRandomCoordinateInRadius( NearRadius, DeployDistance )
          
          -- Respawn the group...
          if self.CargoObject then
            if CargoCarrier:IsShip() then
              -- If CargoCarrier is a ship, we don't want to spawn the units in the water next to the boat. Use destination coord instead.
              self.CargoObject:ReSpawnAt( ToPointVec2, CargoDeployHeading )
            else
              self.CargoObject:ReSpawnAt( FromPointVec2, CargoDeployHeading )
            end
            self:T( { "CargoUnits:", self.CargoObject:GetGroup():GetName() } )
            self.CargoCarrier = nil
      
            local Points = {}
            
            -- From
            Points[#Points+1] = FromPointVec2:WaypointGround( Speed, "Vee" )
            
            -- To
            Points[#Points+1] = ToPointVec2:WaypointGround( Speed, "Vee" )
        
            local TaskRoute = self.CargoObject:TaskRoute( Points )
            self.CargoObject:SetTask( TaskRoute, 1 )
      
            
            self:__UnBoarding( 1, ToPointVec2, NearRadius )
          end
        else
          -- the Carrier is dead. This cargo is dead too!
          self:Destroyed()
        end
      end
    end
  
  end
  
  --- Leave UnBoarding State.
  -- @param #CARGO_UNIT self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Core.Point#COORDINATE ToPointVec2
  -- @param #number NearRadius (optional) Defaut 100 m.
  function CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius )
    self:T( { From, Event, To, ToPointVec2, NearRadius } )
  
    local Angle = 180
    local Speed = 10
    local Distance = 5
  
    if From == "UnBoarding" then
      --if self:IsNear( ToPointVec2, NearRadius ) then
        return true
      --else
        
        --self:__UnBoarding( 1, ToPointVec2, NearRadius )
      --end
      --return false
    end
  
  end
  
  --- UnBoard Event.
  -- @param #CARGO_UNIT self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Core.Point#COORDINATE ToPointVec2
  -- @param #number NearRadius (optional) Defaut 100 m.
  function CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius )
    self:T( { From, Event, To, ToPointVec2, NearRadius } )
  
    self.CargoInAir = self.CargoObject:InAir()
  
    self:T( self.CargoInAir )
  
    -- Only unboard the cargo when the carrier is not in the air.
    -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea).
    if not self.CargoInAir then
  
    end
  
    self:__UnLoad( 1, ToPointVec2, NearRadius )
  
  end
  
  
  
  --- Enter UnLoaded State.
  -- @param #CARGO_UNIT self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Core.Point#COORDINATE
  function CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 )
    self:T( { ToPointVec2, From, Event, To } )
  
    local Angle = 180
    local Speed = 10
    local Distance = 5
  
    if From == "Loaded" then
      local StartPointVec2 = self.CargoCarrier:GetPointVec2()
      local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees.
      local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle )
      local CargoDeployCoord = StartPointVec2:Translate( Distance, CargoDeployHeading )
  
      ToPointVec2 = ToPointVec2 or COORDINATE:New( CargoDeployCoord.x, CargoDeployCoord.z )
  
      -- Respawn the group...
      if self.CargoObject then
        self.CargoObject:ReSpawnAt( ToPointVec2, 0 )
        self.CargoCarrier = nil
      end
      
    end
  
    if self.OnUnLoadedCallBack then
      self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) )
      self.OnUnLoadedCallBack = nil
    end
  
  end
  
  --- Board Event.
  -- @param #CARGO_UNIT self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Wrapper.Group#GROUP CargoCarrier
  -- @param #number NearRadius
  function CARGO_UNIT:onafterBoard( From, Event, To, CargoCarrier, NearRadius, ... )
    self:T( { From, Event, To, CargoCarrier, NearRadius = NearRadius } )
  
    self.CargoInAir = self.CargoObject:InAir()
    
    local Desc = self.CargoObject:GetDesc()
    local MaxSpeed = Desc.speedMaxOffRoad
    local TypeName = Desc.typeName
    
    --self:T({Unit=self.CargoObject:GetName()})
    
    -- A cargo unit can only be boarded if it is not dead
    
      -- Only move the group to the carrier when the cargo is not in the air
      -- (eg. cargo can be on a oil derrick, moving the cargo on the oil derrick will drop the cargo on the sea).
      if not self.CargoInAir then
        -- If NearRadius is given, then use the given NearRadius, otherwise calculate the NearRadius 
        -- based upon the Carrier bounding radius, which is calculated from the bounding rectangle on the Y axis.
        local NearRadius = NearRadius or CargoCarrier:GetBoundingRadius() + 5
        if self:IsNear( CargoCarrier:GetPointVec2(), NearRadius ) then
          self:Load( CargoCarrier, NearRadius, ... )
        else
          if MaxSpeed and MaxSpeed == 0 or TypeName and TypeName == "Stinger comm" then
            self:Load( CargoCarrier, NearRadius, ... )
          else
            
            local Speed = 90
            local Angle = 180
            local Distance = 0
            
            local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2()
            local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees.
            local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle )
            local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading )
            
            -- Set the CargoObject to state Green to ensure it is boarding!
            self.CargoObject:OptionAlarmStateGreen()
            
            local Points = {}
          
            local PointStartVec2 = self.CargoObject:GetPointVec2()
          
            Points[#Points+1] = PointStartVec2:WaypointGround( Speed )
            Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed )
            
            local TaskRoute = self.CargoObject:TaskRoute( Points )
            self.CargoObject:SetTask( TaskRoute, 2 )
            self:__Boarding( -5, CargoCarrier, NearRadius, ... )
            self.RunCount = 0
          end
        end
      end
  end
  
  
  --- Boarding Event.
  -- @param #CARGO_UNIT self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Wrapper.Client#CLIENT CargoCarrier
  -- @param #number NearRadius Default 25 m.
  function CARGO_UNIT:onafterBoarding( From, Event, To, CargoCarrier, NearRadius, ... )
    self:T( { From, Event, To, CargoCarrier:GetName(), NearRadius = NearRadius } )
    
    self:T( { IsAlive=self.CargoObject:IsAlive() }  )
    
      if CargoCarrier and CargoCarrier:IsAlive() then -- and self.CargoObject and self.CargoObject:IsAlive() then 
        if (CargoCarrier:IsAir() and not CargoCarrier:InAir()) or true then
          local NearRadius = NearRadius or CargoCarrier:GetBoundingRadius( NearRadius ) + 5
          if self:IsNear( CargoCarrier:GetPointVec2(), NearRadius ) then
            self:__Load( -1, CargoCarrier, ... )
          else
            if self:IsNear( CargoCarrier:GetPointVec2(), 20 ) then
              self:__Boarding( -1, CargoCarrier, NearRadius, ... )
              self.RunCount = self.RunCount + 1
            else
              self:__Boarding( -2, CargoCarrier, NearRadius, ... )
              self.RunCount = self.RunCount + 2
            end
            if self.RunCount >= 40 then
              self.RunCount = 0
              local Speed = 90
              local Angle = 180
              local Distance = 0
              
              --self:T({Unit=self.CargoObject:GetName()})
  
              local CargoCarrierPointVec2 = CargoCarrier:GetPointVec2()
              local CargoCarrierHeading = CargoCarrier:GetHeading() -- Get Heading of object in degrees.
              local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle )
              local CargoDeployPointVec2 = CargoCarrierPointVec2:Translate( Distance, CargoDeployHeading )
            
              -- Set the CargoObject to state Green to ensure it is boarding!
              self.CargoObject:OptionAlarmStateGreen()
  
              local Points = {}
            
              local PointStartVec2 = self.CargoObject:GetPointVec2()
            
              Points[#Points+1] = PointStartVec2:WaypointGround( Speed, "Off road" )
              Points[#Points+1] = CargoDeployPointVec2:WaypointGround( Speed, "Off road" )
            
              local TaskRoute = self.CargoObject:TaskRoute( Points )
              self.CargoObject:SetTask( TaskRoute, 0.2 )
            end
          end
        else
          self.CargoObject:MessageToGroup( "Cancelling Boarding... Get back on the ground!", 5, CargoCarrier:GetGroup(), self:GetName() )
          self:CancelBoarding( CargoCarrier, NearRadius, ... )
          self.CargoObject:SetCommand( self.CargoObject:CommandStopRoute( true ) )
        end
    else
      self:T("Something is wrong")
    end
    
  end
  
  
  --- Loaded State.
  -- @param #CARGO_UNIT self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Wrapper.Unit#UNIT CargoCarrier
  function CARGO_UNIT:onenterLoaded( From, Event, To, CargoCarrier )
    self:T( { From, Event, To, CargoCarrier } )
  
    self.CargoCarrier = CargoCarrier
    
    --self:T({Unit=self.CargoObject:GetName()})
    
    -- Only destroy the CargoObject if there is a CargoObject (packages don't have CargoObjects).
    if self.CargoObject then
      self.CargoObject:Destroy( false )
      --self.CargoObject:ReSpawnAt( COORDINATE:NewFromVec2( {x=0,y=0} ), 0 )
    end
  end

  --- Get the transportation method of the Cargo.
  -- @param #CARGO_UNIT self
  -- @return #string The transportation method of the Cargo.
  function CARGO_UNIT:GetTransportationMethod()
    if self:IsLoaded() then
      return "for unboarding"
    else
      if self:IsUnLoaded() then
        return "for boarding"
      else
        if self:IsDeployed() then
          return "delivered"
        end
      end
    end
    return ""
  end

end -- CARGO_UNIT
--- **Cargo** - Management of single cargo crates, which are based on a STATIC object. The cargo can only be slingloaded.
--
-- ===
-- 
-- ### [Demo Missions]()
-- 
-- ### [YouTube Playlist]()
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Cargo.CargoSlingload
-- @image Cargo_Slingload.JPG


do -- CARGO_SLINGLOAD

  --- Models the behaviour of cargo crates, which can only be slingloaded. 
  -- @type CARGO_SLINGLOAD
  -- @extends Cargo.Cargo#CARGO_REPRESENTABLE
  
  --- Defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier.
  -- 
  -- The above cargo classes are also used by the TASK_CARGO_ classes to allow human players to transport cargo as part of a tasking:
  -- 
  --   * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} to transport cargo by human players.
  --   * @{Tasking.Task_Cargo_Transport#TASK_CARGO_CSAR} to transport downed pilots by human players.
  --
  -- # Developer Note
  -- 
  -- ![Banner Image](..\Images\deprecated.png)
  -- 
  -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
  -- Therefore, this class is considered to be deprecated
  --
  -- ===
  -- 
  -- @field #CARGO_SLINGLOAD
  CARGO_SLINGLOAD = {
    ClassName = "CARGO_SLINGLOAD"
  }
  
  --- CARGO_SLINGLOAD Constructor.
  -- @param #CARGO_SLINGLOAD self
  -- @param Wrapper.Static#STATIC CargoStatic
  -- @param #string Type
  -- @param #string Name
  -- @param #number LoadRadius (optional)
  -- @param #number NearRadius (optional)
  -- @return #CARGO_SLINGLOAD
  function CARGO_SLINGLOAD:New( CargoStatic, Type, Name, LoadRadius, NearRadius )
    local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoStatic, Type, Name, nil, LoadRadius, NearRadius ) ) -- #CARGO_SLINGLOAD
    self:T( { Type, Name, NearRadius } )
  
    self.CargoObject = CargoStatic
  
    -- Cargo objects are added to the _DATABASE and SET_CARGO objects.
    _EVENTDISPATCHER:CreateEventNewCargo( self )
    
    self:HandleEvent( EVENTS.Dead, self.OnEventCargoDead )
    self:HandleEvent( EVENTS.Crash, self.OnEventCargoDead )
    --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCargoDead )
    self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventCargoDead )
    
    self:SetEventPriority( 4 )
    
    self.NearRadius = NearRadius or 25
  
    return self
  end


  -- @param #CARGO_SLINGLOAD self
  -- @param Core.Event#EVENTDATA EventData 
  function CARGO_SLINGLOAD:OnEventCargoDead( EventData )

    local Destroyed = false
  
    if self:IsDestroyed() or self:IsUnLoaded() then
      if self.CargoObject:GetName() == EventData.IniUnitName then
        if not self.NoDestroy then 
          Destroyed = true
        end
      end
    end
    
    if Destroyed then
      self:I( { "Cargo crate destroyed: " .. self.CargoObject:GetName() } )
      self:Destroyed()
    end
  
  end
  
  
  --- Check if the cargo can be Slingloaded.
  -- @param #CARGO_SLINGLOAD self
  function CARGO_SLINGLOAD:CanSlingload()
    return true
  end
  
  --- Check if the cargo can be Boarded.
  -- @param #CARGO_SLINGLOAD self
  function CARGO_SLINGLOAD:CanBoard()
    return false
  end

  --- Check if the cargo can be Unboarded.
  -- @param #CARGO_SLINGLOAD self
  function CARGO_SLINGLOAD:CanUnboard()
    return false
  end

  --- Check if the cargo can be Loaded.
  -- @param #CARGO_SLINGLOAD self
  function CARGO_SLINGLOAD:CanLoad()
    return false
  end

  --- Check if the cargo can be Unloaded.
  -- @param #CARGO_SLINGLOAD self
  function CARGO_SLINGLOAD:CanUnload()
    return false
  end


  --- Check if Cargo Crate is in the radius for the Cargo to be reported.
  -- @param #CARGO_SLINGLOAD self
  -- @param Core.Point#COORDINATE Coordinate
  -- @return #boolean true if the Cargo Crate is within the report radius.
  function CARGO_SLINGLOAD:IsInReportRadius( Coordinate )
    --self:T( { Coordinate, LoadRadius = self.LoadRadius } )
  
    local Distance = 0
    if self:IsUnLoaded() then
      Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() )
      if Distance <= self.LoadRadius then
        return true
      end
    end
    
    return false
  end


  --- Check if Cargo Slingload is in the radius for the Cargo to be Boarded or Loaded.
  -- @param #CARGO_SLINGLOAD self
  -- @param Core.Point#COORDINATE Coordinate
  -- @return #boolean true if the Cargo Slingload is within the loading radius.
  function CARGO_SLINGLOAD:IsInLoadRadius( Coordinate )
    --self:T( { Coordinate } )
  
    local Distance = 0
    if self:IsUnLoaded() then
      Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() )
      if Distance <= self.NearRadius then
        return true
      end
    end
    
    return false
  end



  --- Get the current Coordinate of the CargoGroup.
  -- @param #CARGO_SLINGLOAD self
  -- @return Core.Point#COORDINATE The current Coordinate of the first Cargo of the CargoGroup.
  -- @return #nil There is no valid Cargo in the CargoGroup.
  function CARGO_SLINGLOAD:GetCoordinate()
    --self:T()
    
    return self.CargoObject:GetCoordinate()
  end

  --- Check if the CargoGroup is alive.
  -- @param #CARGO_SLINGLOAD self
  -- @return #boolean true if the CargoGroup is alive.
  -- @return #boolean false if the CargoGroup is dead.
  function CARGO_SLINGLOAD:IsAlive()

    local Alive = true
  
    -- When the Cargo is Loaded, the Cargo is in the CargoCarrier, so we check if the CargoCarrier is alive.
    -- When the Cargo is not Loaded, the Cargo is the CargoObject, so we check if the CargoObject is alive.
    if self:IsLoaded() then
      Alive = Alive == true and self.CargoCarrier:IsAlive()
    else
      Alive = Alive == true and self.CargoObject:IsAlive()
    end 
    
    return Alive
  
  end

  
  --- Route Cargo to Coordinate and randomize locations.
  -- @param #CARGO_SLINGLOAD self
  -- @param Core.Point#COORDINATE Coordinate
  function CARGO_SLINGLOAD:RouteTo( Coordinate )
    --self:T( {Coordinate = Coordinate } )
    
  end

  
  --- Check if Cargo is near to the Carrier.
  -- The Cargo is near to the Carrier within NearRadius.
  -- @param #CARGO_SLINGLOAD self
  -- @param Wrapper.Group#GROUP CargoCarrier
  -- @param #number NearRadius
  -- @return #boolean The Cargo is near to the Carrier.
  -- @return #nil The Cargo is not near to the Carrier.
  function CARGO_SLINGLOAD:IsNear( CargoCarrier, NearRadius )
    --self:T( {NearRadius = NearRadius } )
    
    return self:IsNear( CargoCarrier:GetCoordinate(), NearRadius )
  end


  --- Respawn the CargoGroup.
  -- @param #CARGO_SLINGLOAD self
  function CARGO_SLINGLOAD:Respawn()

    --self:T( { "Respawning slingload " .. self:GetName() } )


    -- Respawn the group...
    if self.CargoObject then
      self.CargoObject:ReSpawn() -- A cargo destroy crates a DEAD event.
      self:__Reset( -0.1 )
    end

    
  end


  --- Respawn the CargoGroup.
  -- @param #CARGO_SLINGLOAD self
  function CARGO_SLINGLOAD:onafterReset()

    --self:T( { "Reset slingload " .. self:GetName() } )


    -- Respawn the group...
    if self.CargoObject then
      self:SetDeployed( false )
      self:SetStartState( "UnLoaded" )
      self.CargoCarrier = nil
      -- Cargo objects are added to the _DATABASE and SET_CARGO objects.
      _EVENTDISPATCHER:CreateEventNewCargo( self )
    end

    
  end

  --- Get the transportation method of the Cargo.
  -- @param #CARGO_SLINGLOAD self
  -- @return #string The transportation method of the Cargo.
  function CARGO_SLINGLOAD:GetTransportationMethod()
    if self:IsLoaded() then
      return "for sling loading"
    else
      if self:IsUnLoaded() then
        return "for sling loading"
      else
        if self:IsDeployed() then
          return "delivered"
        end
      end
    end
    return ""
  end
   
end
--- **Cargo** - Management of single cargo crates, which are based on a STATIC object.
--
-- ===
-- 
-- ### [Demo Missions]()
-- 
-- ### [YouTube Playlist]()
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Cargo.CargoCrate
-- @image Cargo_Crates.JPG

do -- CARGO_CRATE

  --- Models the behaviour of cargo crates, which can be slingloaded and boarded on helicopters. 
  -- @type CARGO_CRATE
  -- @extends Cargo.Cargo#CARGO_REPRESENTABLE
  
  ---
  -- ![Banner Image](..\Images\deprecated.png)
  --
  --- Defines a cargo that is represented by a UNIT object within the simulator, and can be transported by a carrier.
  -- Use the event functions as described above to Load, UnLoad, Board, UnBoard the CARGO\_CRATE objects to and from carriers.
  -- 
  -- The above cargo classes are used by the following AI_CARGO_ classes to allow AI groups to transport cargo:
  -- 
  --   * AI Armoured Personnel Carriers to transport cargo and engage in battles, using the @{AI.AI_Cargo_APC} module.
  --   * AI Helicopters to transport cargo, using the @{AI.AI_Cargo_Helicopter} module.
  --   * AI Planes to transport cargo, using the @{AI.AI_Cargo_Airplane} module.
  --   * AI Ships is planned.
  -- 
  -- The above cargo classes are also used by the TASK_CARGO_ classes to allow human players to transport cargo as part of a tasking:
  -- 
  --   * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} to transport cargo by human players.
  --   * @{Tasking.Task_Cargo_Transport#TASK_CARGO_CSAR} to transport downed pilots by human players.
  --
  -- # Developer Note
  -- 
  -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
  -- Therefore, this class is considered to be deprecated
  --
  -- ===
  -- 
  -- @field #CARGO_CRATE
  CARGO_CRATE = {
    ClassName = "CARGO_CRATE"
  }
  
  --- CARGO_CRATE Constructor.
  -- @param #CARGO_CRATE self
  -- @param Wrapper.Static#STATIC CargoStatic
  -- @param #string Type
  -- @param #string Name
  -- @param #number LoadRadius (optional)
  -- @param #number NearRadius (optional)
  -- @return #CARGO_CRATE
  function CARGO_CRATE:New( CargoStatic, Type, Name, LoadRadius, NearRadius )
    local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoStatic, Type, Name, nil, LoadRadius, NearRadius ) ) -- #CARGO_CRATE
    self:T( { Type, Name, NearRadius } )
  
    self.CargoObject = CargoStatic -- Wrapper.Static#STATIC
 
    -- Cargo objects are added to the _DATABASE and SET_CARGO objects.
    _EVENTDISPATCHER:CreateEventNewCargo( self )
    
    self:HandleEvent( EVENTS.Dead, self.OnEventCargoDead )
    self:HandleEvent( EVENTS.Crash, self.OnEventCargoDead )
    --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCargoDead )
    self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventCargoDead )
    
    self:SetEventPriority( 4 )
    
    self.NearRadius = NearRadius or 25
  
    return self
  end
  
  -- @param #CARGO_CRATE self
  -- @param Core.Event#EVENTDATA EventData 
  function CARGO_CRATE:OnEventCargoDead( EventData )

    local Destroyed = false
  
    if self:IsDestroyed() or self:IsUnLoaded() or self:IsBoarding() then
      if self.CargoObject:GetName() == EventData.IniUnitName then
        if not self.NoDestroy then 
          Destroyed = true
        end
      end
    else
      if self:IsLoaded() then
        local CarrierName = self.CargoCarrier:GetName()
        if CarrierName == EventData.IniDCSUnitName then
          MESSAGE:New( "Cargo is lost from carrier " .. CarrierName, 15 ):ToAll()
          Destroyed = true
          self.CargoCarrier:ClearCargo()
        end
      end
    end
    
    if Destroyed then
      self:I( { "Cargo crate destroyed: " .. self.CargoObject:GetName() } )
      self:Destroyed()
    end
    
  end
  
  
  --- Enter UnLoaded State.
  -- @param #CARGO_CRATE self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Core.Point#COORDINATE
  function CARGO_CRATE:onenterUnLoaded( From, Event, To, ToPointVec2 )
    --self:T( { ToPointVec2, From, Event, To } )
  
    local Angle = 180
    local Speed = 10
    local Distance = 10
  
    if From == "Loaded" then
      local StartCoordinate = self.CargoCarrier:GetCoordinate()
      local CargoCarrierHeading = self.CargoCarrier:GetHeading() -- Get Heading of object in degrees.
      local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle )
      local CargoDeployCoord = StartCoordinate:Translate( Distance, CargoDeployHeading )
  
      ToPointVec2 = ToPointVec2 or COORDINATE:NewFromVec2( { x= CargoDeployCoord.x, y = CargoDeployCoord.z } )
  
      -- Respawn the group...
      if self.CargoObject then
        self.CargoObject:ReSpawnAt( ToPointVec2, 0 )
        self.CargoCarrier = nil
      end
      
    end
  
    if self.OnUnLoadedCallBack then
      self.OnUnLoadedCallBack( self, unpack( self.OnUnLoadedParameters ) )
      self.OnUnLoadedCallBack = nil
    end
  
  end

  
  --- Loaded State.
  -- @param #CARGO_CRATE self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Wrapper.Unit#UNIT CargoCarrier
  function CARGO_CRATE:onenterLoaded( From, Event, To, CargoCarrier )
    --self:T( { From, Event, To, CargoCarrier } )
  
    self.CargoCarrier = CargoCarrier
    
    -- Only destroy the CargoObject is if there is a CargoObject (packages don't have CargoObjects).
    if self.CargoObject then
      self:T("Destroying")
      self.NoDestroy = true
      self.CargoObject:Destroy( false ) -- Do not generate a remove unit event, because we want to keep the template for later respawn in the database.
      --local Coordinate = self.CargoObject:GetCoordinate():GetRandomCoordinateInRadius( 50, 20 )
      --self.CargoObject:ReSpawnAt( Coordinate, 0 )
    end
  end

  --- Check if the cargo can be Boarded.
  -- @param #CARGO_CRATE self
  function CARGO_CRATE:CanBoard()
    return false
  end

  --- Check if the cargo can be Unboarded.
  -- @param #CARGO_CRATE self
  function CARGO_CRATE:CanUnboard()
    return false
  end

  --- Check if the cargo can be sling loaded.
  -- @param #CARGO_CRATE self
  function CARGO_CRATE:CanSlingload()
    return false
  end

  --- Check if Cargo Crate is in the radius for the Cargo to be reported.
  -- @param #CARGO_CRATE self
  -- @param Core.Point#COORDINATE Coordinate
  -- @return #boolean true if the Cargo Crate is within the report radius.
  function CARGO_CRATE:IsInReportRadius( Coordinate )
    --self:T( { Coordinate, LoadRadius = self.LoadRadius } )
  
    local Distance = 0
    if self:IsUnLoaded() then
      Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() )
      --self:T( Distance )
      if Distance <= self.LoadRadius then
        return true
      end
    end
    
    return false
  end


  --- Check if Cargo Crate is in the radius for the Cargo to be Boarded or Loaded.
  -- @param #CARGO_CRATE self
  -- @param Core.Point#Coordinate Coordinate
  -- @return #boolean true if the Cargo Crate is within the loading radius.
  function CARGO_CRATE:IsInLoadRadius( Coordinate )
    --self:T( { Coordinate, LoadRadius = self.NearRadius } )
  
    local Distance = 0
    if self:IsUnLoaded() then
      Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() )
      --self:T( Distance )
      if Distance <= self.NearRadius then
        return true
      end
    end
    
    return false
  end



  --- Get the current Coordinate of the CargoGroup.
  -- @param #CARGO_CRATE self
  -- @return Core.Point#COORDINATE The current Coordinate of the first Cargo of the CargoGroup.
  -- @return #nil There is no valid Cargo in the CargoGroup.
  function CARGO_CRATE:GetCoordinate()
    --self:T()
    
    return self.CargoObject:GetCoordinate()
  end

  --- Check if the CargoGroup is alive.
  -- @param #CARGO_CRATE self
  -- @return #boolean true if the CargoGroup is alive.
  -- @return #boolean false if the CargoGroup is dead.
  function CARGO_CRATE:IsAlive()

    local Alive = true
  
    -- When the Cargo is Loaded, the Cargo is in the CargoCarrier, so we check if the CargoCarrier is alive.
    -- When the Cargo is not Loaded, the Cargo is the CargoObject, so we check if the CargoObject is alive.
    if self:IsLoaded() then
      Alive = Alive == true and self.CargoCarrier:IsAlive()
    else
      Alive = Alive == true and self.CargoObject:IsAlive()
    end 
    
    return Alive
  
  end

  
  --- Route Cargo to Coordinate and randomize locations.
  -- @param #CARGO_CRATE self
  -- @param Core.Point#COORDINATE Coordinate
  function CARGO_CRATE:RouteTo( Coordinate )
    self:T( {Coordinate = Coordinate } )
    
  end

  
  --- Check if Cargo is near to the Carrier.
  -- The Cargo is near to the Carrier within NearRadius.
  -- @param #CARGO_CRATE self
  -- @param Wrapper.Group#GROUP CargoCarrier
  -- @param #number NearRadius
  -- @return #boolean The Cargo is near to the Carrier.
  -- @return #nil The Cargo is not near to the Carrier.
  function CARGO_CRATE:IsNear( CargoCarrier, NearRadius )
    self:T( {NearRadius = NearRadius } )
    
    return self:IsNear( CargoCarrier:GetCoordinate(), NearRadius )
  end

  --- Respawn the CargoGroup.
  -- @param #CARGO_CRATE self
  function CARGO_CRATE:Respawn()

    self:T( { "Respawning crate " .. self:GetName() } )


    -- Respawn the group...
    if self.CargoObject then
      self.CargoObject:ReSpawn() -- A cargo destroy crates a DEAD event.
      self:__Reset( -0.1 )
    end

    
  end


  --- Respawn the CargoGroup.
  -- @param #CARGO_CRATE self
  function CARGO_CRATE:onafterReset()

    self:T( { "Reset crate " .. self:GetName() } )


    -- Respawn the group...
    if self.CargoObject then
      self:SetDeployed( false )
      self:SetStartState( "UnLoaded" )
      self.CargoCarrier = nil
      -- Cargo objects are added to the _DATABASE and SET_CARGO objects.
      _EVENTDISPATCHER:CreateEventNewCargo( self )
    end

    
  end

  --- Get the transportation method of the Cargo.
  -- @param #CARGO_CRATE self
  -- @return #string The transportation method of the Cargo.
  function CARGO_CRATE:GetTransportationMethod()
    if self:IsLoaded() then
      return "for unloading"
    else
      if self:IsUnLoaded() then
        return "for loading"
      else
        if self:IsDeployed() then
          return "delivered"
        end
      end
    end
    return ""
  end
  
end

--- **Cargo** - Management of grouped cargo logistics, which are based on a GROUP object.
--
-- ===
-- 
-- ### [Demo Missions]()
-- 
-- ### [YouTube Playlist]()
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Cargo.CargoGroup
-- @image Cargo_Groups.JPG


do -- CARGO_GROUP

  --- @type CARGO_GROUP
  -- @field Core.Set#SET_CARGO CargoSet The collection of derived CARGO objects.
  -- @field #string GroupName The name of the CargoGroup.
  -- @field Wrapper.Group#GROUÜ CargoCarrier The carrier group.
  -- @extends Cargo.Cargo#CARGO_REPORTABLE
  
  --- Defines a cargo that is represented by a @{Wrapper.Group} object within the simulator.
  -- 
  -- ![Banner Image](..\Images\deprecated.png)
  -- The cargo can be Loaded, UnLoaded, Boarded, UnBoarded to and from Carriers.
  -- 
  -- The above cargo classes are used by the following AI_CARGO_ classes to allow AI groups to transport cargo:
  -- 
  --   * AI Armoured Personnel Carriers to transport cargo and engage in battles, using the @{AI.AI_Cargo_APC} module.
  --   * AI Helicopters to transport cargo, using the @{AI.AI_Cargo_Helicopter} module.
  --   * AI Planes to transport cargo, using the @{AI.AI_Cargo_Airplane} module.
  --   * AI Ships is planned.
  -- 
  -- The above cargo classes are also used by the TASK_CARGO_ classes to allow human players to transport cargo as part of a tasking:
  -- 
  --   * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT} to transport cargo by human players.
  --   * @{Tasking.Task_Cargo_Transport#TASK_CARGO_CSAR} to transport downed pilots by human players.
  --
  -- # Developer Note
  -- 
  -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
  -- Therefore, this class is considered to be deprecated
  --
  -- @field #CARGO_GROUP CARGO_GROUP
  -- 
  CARGO_GROUP = {
    ClassName = "CARGO_GROUP",
  }

  --- CARGO_GROUP constructor.
  -- This make a new CARGO_GROUP from a @{Wrapper.Group} object.
  -- It will "ungroup" the group object within the sim, and will create a @{Core.Set} of individual Unit objects.
  -- @param #CARGO_GROUP self
  -- @param Wrapper.Group#GROUP CargoGroup Group to be transported as cargo.
  -- @param #string Type Cargo type, e.g. "Infantry". This is the type used in SET_CARGO:New():FilterTypes("Infantry") to define the valid cargo groups of the set.
  -- @param #string Name A user defined name of the cargo group. This name CAN be the same as the group object but can also have a different name. This name MUST be unique!
  -- @param #number LoadRadius (optional) Distance in meters until which a cargo is loaded into the carrier. Cargo outside this radius has to be routed by other means to within the radius to be loaded.
  -- @param #number NearRadius (optional) Once the units are within this radius of the carrier, they are actually loaded, i.e. disappear from the scene.
  -- @return #CARGO_GROUP Cargo group object.
  function CARGO_GROUP:New( CargoGroup, Type, Name, LoadRadius, NearRadius )
  
    -- Inherit CAROG_REPORTABLE
    local self = BASE:Inherit( self, CARGO_REPORTABLE:New( Type, Name, 0, LoadRadius, NearRadius ) ) -- #CARGO_GROUP
    self:T( { Type, Name, LoadRadius } )
  
    self.CargoSet = SET_CARGO:New()
    self.CargoGroup = CargoGroup
    self.Grouped = true
    self.CargoUnitTemplate = {}
    
    self.NearRadius = NearRadius
    
    self:SetDeployed( false )
    
    local WeightGroup = 0
    local VolumeGroup = 0
    
    self.CargoGroup:Destroy() -- destroy and generate a unit removal event, so that the database gets cleaned, and the linked sets get properly cleaned.

    local GroupName = CargoGroup:GetName()
    self.CargoName = Name
    self.CargoTemplate = UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ) )
    
    -- Deactivate late activation.
    self.CargoTemplate.lateActivation=false

    self.GroupTemplate = UTILS.DeepCopy( self.CargoTemplate )
    self.GroupTemplate.name = self.CargoName .. "#CARGO"
    self.GroupTemplate.groupId = nil
    
    self.GroupTemplate.units = {}
    
    for UnitID, UnitTemplate in pairs( self.CargoTemplate.units ) do
      UnitTemplate.name = UnitTemplate.name .. "#CARGO"
      local CargoUnitName = UnitTemplate.name
      self.CargoUnitTemplate[CargoUnitName] = UnitTemplate      

       self.GroupTemplate.units[#self.GroupTemplate.units+1] = self.CargoUnitTemplate[CargoUnitName]
       self.GroupTemplate.units[#self.GroupTemplate.units].unitId = nil
      
      -- And we register the spawned unit as part of the CargoSet.
      local Unit = UNIT:Register( CargoUnitName )
      
    end

    -- Then we register the new group in the database
    self.CargoGroup = GROUP:NewTemplate( self.GroupTemplate, self.GroupTemplate.CoalitionID, self.GroupTemplate.CategoryID, self.GroupTemplate.CountryID )
    
    -- Now we spawn the new group based on the template created.
    self.CargoObject = _DATABASE:Spawn( self.GroupTemplate )
    
    for CargoUnitID, CargoUnit in pairs( self.CargoObject:GetUnits() ) do
      

      local CargoUnitName = CargoUnit:GetName()

      local Cargo = CARGO_UNIT:New( CargoUnit, Type, CargoUnitName, LoadRadius, NearRadius )
      self.CargoSet:Add( CargoUnitName, Cargo )

      WeightGroup = WeightGroup + Cargo:GetWeight()

    end
  
    self:SetWeight( WeightGroup )
    
    self:T( { "Weight Cargo", WeightGroup } )
  
    -- Cargo objects are added to the _DATABASE and SET_CARGO objects.
    _EVENTDISPATCHER:CreateEventNewCargo( self )
    
    self:HandleEvent( EVENTS.Dead, self.OnEventCargoDead )
    self:HandleEvent( EVENTS.Crash, self.OnEventCargoDead )
    --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCargoDead )
    self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventCargoDead )
    
    self:SetEventPriority( 4 )
    
    return self
  end


  --- Respawn the CargoGroup.
  -- @param #CARGO_GROUP self
  function CARGO_GROUP:Respawn()

    self:T( { "Respawning" } )

    for CargoID, CargoData in pairs( self.CargoSet:GetSet() ) do
      local Cargo = CargoData -- Cargo.Cargo#CARGO
      Cargo:Destroy() -- Destroy the cargo and generate a remove unit event to update the sets.
      Cargo:SetStartState( "UnLoaded" )
    end

    -- Now we spawn the new group based on the template created.
    _DATABASE:Spawn( self.GroupTemplate )
      
    for CargoUnitID, CargoUnit in pairs( self.CargoObject:GetUnits() ) do

      local CargoUnitName = CargoUnit:GetName()

      local Cargo = CARGO_UNIT:New( CargoUnit, self.Type, CargoUnitName, self.LoadRadius )
      self.CargoSet:Add( CargoUnitName, Cargo )

    end
  
    self:SetDeployed( false )
    self:SetStartState( "UnLoaded" )
    
  end
  
  --- Ungroup the cargo group into individual groups with one unit.
  -- This is required because by default a group will move in formation and this is really an issue for group control.
  -- Therefore this method is made to be able to ungroup a group.
  -- This works for ground only groups.
  -- @param #CARGO_GROUP self
  function CARGO_GROUP:Ungroup()

    if self.Grouped == true then
    
      self.Grouped = false
      
      self.CargoGroup:Destroy()
      
      for CargoUnitName, CargoUnit in pairs( self.CargoSet:GetSet() ) do
        local CargoUnit = CargoUnit -- Cargo.CargoUnit#CARGO_UNIT

        if CargoUnit:IsUnLoaded() then
          local GroupTemplate = UTILS.DeepCopy( self.CargoTemplate )
          --local GroupName = env.getValueDictByKey( GroupTemplate.name )
          
          -- We create a new group object with one unit...
          -- First we prepare the template...
          GroupTemplate.name = self.CargoName .. "#CARGO#" .. CargoUnitName 
          GroupTemplate.groupId = nil
          
          if CargoUnit:IsUnLoaded() then
            GroupTemplate.units = {}
            GroupTemplate.units[1] = self.CargoUnitTemplate[CargoUnitName]
            GroupTemplate.units[#GroupTemplate.units].unitId = nil
            GroupTemplate.units[#GroupTemplate.units].x = CargoUnit:GetX()
            GroupTemplate.units[#GroupTemplate.units].y = CargoUnit:GetY()
            GroupTemplate.units[#GroupTemplate.units].heading = CargoUnit:GetHeading()
          end
      
      
          -- Then we register the new group in the database
          local CargoGroup = GROUP:NewTemplate( GroupTemplate, GroupTemplate.CoalitionID, GroupTemplate.CategoryID, GroupTemplate.CountryID)
          
          -- Now we spawn the new group based on the template created.
          _DATABASE:Spawn( GroupTemplate )
        end
      end
      
      self.CargoObject = nil
    end
    
  
  end

  --- Regroup the cargo group into one group with multiple unit.
  -- This is required because by default a group will move in formation and this is really an issue for group control.
  -- Therefore this method is made to be able to regroup a group.
  -- This works for ground only groups.
  -- @param #CARGO_GROUP self
  function CARGO_GROUP:Regroup()
  
    self:T("Regroup")

    if self.Grouped == false then
    
      self.Grouped = true
      
      local GroupTemplate = UTILS.DeepCopy( self.CargoTemplate )
      GroupTemplate.name = self.CargoName .. "#CARGO"
      GroupTemplate.groupId = nil
      GroupTemplate.units = {}

      for CargoUnitName, CargoUnit in pairs( self.CargoSet:GetSet() ) do
        local CargoUnit = CargoUnit -- Cargo.CargoUnit#CARGO_UNIT

        self:T( { CargoUnit:GetName(), UnLoaded = CargoUnit:IsUnLoaded() } )

        if CargoUnit:IsUnLoaded() then
    
          CargoUnit.CargoObject:Destroy()

          GroupTemplate.units[#GroupTemplate.units+1] = self.CargoUnitTemplate[CargoUnitName]
          GroupTemplate.units[#GroupTemplate.units].unitId = nil
          GroupTemplate.units[#GroupTemplate.units].x = CargoUnit:GetX()
          GroupTemplate.units[#GroupTemplate.units].y = CargoUnit:GetY()
          GroupTemplate.units[#GroupTemplate.units].heading = CargoUnit:GetHeading()
        end
      end

      -- Then we register the new group in the database
      self.CargoGroup = GROUP:NewTemplate( GroupTemplate, GroupTemplate.CoalitionID, GroupTemplate.CategoryID, GroupTemplate.CountryID )

      self:T( { "Regroup", GroupTemplate } )
        
      -- Now we spawn the new group based on the template created.
      self.CargoObject = _DATABASE:Spawn( GroupTemplate )
    end
  
  end


  --- @param #CARGO_GROUP self
  -- @param Core.Event#EVENTDATA EventData 
  function CARGO_GROUP:OnEventCargoDead( EventData )
  
    self:T(EventData)

    local Destroyed = false
  
    if self:IsDestroyed() or self:IsUnLoaded() or self:IsBoarding() or self:IsUnboarding() then
      Destroyed = true
      for CargoID, CargoData in pairs( self.CargoSet:GetSet() ) do
        local Cargo = CargoData -- Cargo.Cargo#CARGO
        if Cargo:IsAlive() then
          Destroyed = false
        else
          Cargo:Destroyed()
        end
      end
    else
      local CarrierName = self.CargoCarrier:GetName()
      if CarrierName == EventData.IniDCSUnitName then
        MESSAGE:New( "Cargo is lost from carrier " .. CarrierName, 15 ):ToAll()
        Destroyed = true
        self.CargoCarrier:ClearCargo()
      end
    end
    
    if Destroyed then
      self:Destroyed()
      self:T( { "Cargo group destroyed" } )
    end
  
  end

  --- After Board Event.
  -- @param #CARGO_GROUP self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Wrapper.Unit#UNIT CargoCarrier
  -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier.
  function CARGO_GROUP:onafterBoard( From, Event, To, CargoCarrier, NearRadius, ... )
    self:T( { CargoCarrier.UnitName, From, Event, To, NearRadius = NearRadius } )
    
    NearRadius = NearRadius or self.NearRadius
    
    -- For each Cargo object within the CARGO_GROUPED, route each object to the CargoLoadPointVec2
    self.CargoSet:ForEach(
      function( Cargo, ... )
        self:T( { "Board Unit", Cargo:GetName( ), Cargo:IsDestroyed(), Cargo.CargoObject:IsAlive() } )
        local CargoGroup = Cargo.CargoObject --Wrapper.Group#GROUP
        CargoGroup:OptionAlarmStateGreen()
        Cargo:__Board( 1, CargoCarrier, NearRadius, ... )
      end, ...
    )
    
    self:__Boarding( -1, CargoCarrier, NearRadius, ... )
    
  end

  --- Enter Loaded State.
  -- @param #CARGO_GROUP self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Wrapper.Unit#UNIT CargoCarrier
  function CARGO_GROUP:onafterLoad( From, Event, To, CargoCarrier, ... )
    --self:T( { From, Event, To, CargoCarrier, ...} )
    
    if From == "UnLoaded" then
      -- For each Cargo object within the CARGO_GROUP, load each cargo to the CargoCarrier.
      for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do
        if not Cargo:IsDestroyed() then
          Cargo:Load( CargoCarrier )
        end
      end
    end
    
    --self.CargoObject:Destroy()
    self.CargoCarrier = CargoCarrier
    self.CargoCarrier:AddCargo( self )
    
  end

  --- Leave Boarding State.
  -- @param #CARGO_GROUP self  
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Wrapper.Unit#UNIT CargoCarrier
  -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier.
  function CARGO_GROUP:onafterBoarding( From, Event, To, CargoCarrier, NearRadius, ... )
    --self:T( { CargoCarrier.UnitName, From, Event, To } )
  
    local Boarded = true
    local Cancelled = false
    local Dead = true
  
    self.CargoSet:Flush()
  
    -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2
    for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do
      --self:T( { Cargo:GetName(), Cargo.current } )
      
      
      if not Cargo:is( "Loaded" )
      and (not Cargo:is( "Destroyed" )) then -- If one or more units of a group defined as CARGO_GROUP died, the CARGO_GROUP:Board() command does not trigger the CARGO_GRUOP:OnEnterLoaded() function.
        Boarded = false
      end
      
      if Cargo:is( "UnLoaded" ) then
        Cancelled = true
      end
  
      if not Cargo:is( "Destroyed" ) then
        Dead = false
      end
      
    end
  
    if not Dead then
  
      if not Cancelled then
        if not Boarded then
          self:__Boarding( -5, CargoCarrier, NearRadius, ... )
        else
          self:T("Group Cargo is loaded")
          self:__Load( 1, CargoCarrier, ... )
        end
      else
          self:__CancelBoarding( 1, CargoCarrier, NearRadius, ... )
      end
    else
      self:__Destroyed( 1, CargoCarrier, NearRadius, ... )
    end
    
  end

  --- Enter UnBoarding State.
  -- @param #CARGO_GROUP self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Core.Point#COORDINATE ToPointVec2
  -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier.
  function CARGO_GROUP:onafterUnBoard( From, Event, To, ToPointVec2, NearRadius, ... )
    self:T( {From, Event, To, ToPointVec2, NearRadius } )
  
    NearRadius = NearRadius or 25
  
    local Timer = 1
  
    if From == "Loaded" then
    
      if self.CargoObject then
        self.CargoObject:Destroy()
      end
  
      -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2
      self.CargoSet:ForEach(
        --- @param Cargo.Cargo#CARGO Cargo
        function( Cargo, NearRadius )
          if not Cargo:IsDestroyed() then
            local ToVec=nil
            if ToPointVec2==nil then
              ToVec=self.CargoCarrier:GetPointVec2():GetRandomPointVec2InRadius(2*NearRadius, NearRadius)
            else
              ToVec=ToPointVec2
            end
            Cargo:__UnBoard( Timer, ToVec, NearRadius )
            Timer = Timer + 1
          end
        end, { NearRadius }
      )
      
      
      self:__UnBoarding( 1, ToPointVec2, NearRadius, ... )
    end
  
  end

  --- Leave UnBoarding State.
  -- @param #CARGO_GROUP self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Core.Point#COORDINATE ToPointVec2
  -- @param #number NearRadius If distance is smaller than this number, cargo is loaded into the carrier.
  function CARGO_GROUP:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius, ... )
    --self:T( { From, Event, To, ToPointVec2, NearRadius } )
  
    --local NearRadius = NearRadius or 25
  
    local Angle = 180
    local Speed = 10
    local Distance = 5
  
    if From == "UnBoarding" then
      local UnBoarded = true
  
      -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2
      for CargoID, Cargo in pairs( self.CargoSet:GetSet() ) do
        self:T( { Cargo:GetName(), Cargo.current } )
        if not Cargo:is( "UnLoaded" ) and not Cargo:IsDestroyed() then
          UnBoarded = false
        end
      end
    
      if UnBoarded then
        self:__UnLoad( 1, ToPointVec2, ... )
      else
        self:__UnBoarding( 1, ToPointVec2, NearRadius, ... )
      end
      
      return false
    end
    
  end

  --- Enter UnLoaded State.
  -- @param #CARGO_GROUP self
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  -- @param Core.Point#COORDINATE ToPointVec2
  function CARGO_GROUP:onafterUnLoad( From, Event, To, ToPointVec2, ... )
    --self:T( { From, Event, To, ToPointVec2 } )
  
    if From == "Loaded" then
      
      -- For each Cargo object within the CARGO_GROUP, route each object to the CargoLoadPointVec2
      self.CargoSet:ForEach(
        function( Cargo )
          --Cargo:UnLoad( ToPointVec2 )
          local RandomVec2=nil
          if ToPointVec2 then 
            RandomVec2=ToPointVec2:GetRandomPointVec2InRadius(20, 10)
          end
          Cargo:UnBoard( RandomVec2 )
        end
      )
  
    end
    
    self.CargoCarrier:RemoveCargo( self )
    self.CargoCarrier = nil
    
  end


  --- Get the current Coordinate of the CargoGroup.
  -- @param #CARGO_GROUP self
  -- @return Core.Point#COORDINATE The current Coordinate of the first Cargo of the CargoGroup.
  -- @return #nil There is no valid Cargo in the CargoGroup.
  function CARGO_GROUP:GetCoordinate()
    local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO
    
    if Cargo then
      return Cargo.CargoObject:GetCoordinate()
    end
    
    return nil
  end

  --- Get the x position of the cargo.
  -- @param #CARGO_GROUP self
  -- @return #number
  function CARGO:GetX()

    local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO

    if Cargo then
      return Cargo:GetCoordinate().x
    end 
    
    return nil
  end
  
  --- Get the y position of the cargo.
  -- @param #CARGO_GROUP self
  -- @return #number
  function CARGO:GetY()

    local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO

    if Cargo then
      return Cargo:GetCoordinate().z
    end
    
    return nil 
  end
  


  --- Check if the CargoGroup is alive.
  -- @param #CARGO_GROUP self
  -- @return #boolean true if the CargoGroup is alive.
  -- @return #boolean false if the CargoGroup is dead.
  function CARGO_GROUP:IsAlive()

    local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO
    return Cargo ~= nil
  
  end

  
  --- Get the first alive Cargo Unit of the Cargo Group.
  -- @param #CARGO_GROUP self
  -- @return #CARGO_GROUP
  function CARGO_GROUP:GetFirstAlive()
    
    local CargoFirstAlive = nil
    
    for _, Cargo in pairs( self.CargoSet:GetSet() ) do
      if not Cargo:IsDestroyed() then
        CargoFirstAlive = Cargo
        break
      end
    end
    return CargoFirstAlive
  end

  
  --- Get the amount of cargo units in the group.
  -- @param #CARGO_GROUP self
  -- @return #CARGO_GROUP
  function CARGO_GROUP:GetCount()
    return self.CargoSet:Count()
  end


  --- Get the underlying GROUP object from the CARGO_GROUP.
  -- @param #CARGO_GROUP self
  -- @return #CARGO_GROUP
  function CARGO_GROUP:GetGroup( Cargo )
    local Cargo = Cargo or self:GetFirstAlive() -- Cargo.Cargo#CARGO
    return Cargo.CargoObject:GetGroup()
  end


  --- Route Cargo to Coordinate and randomize locations.
  -- @param #CARGO_GROUP self
  -- @param Core.Point#COORDINATE Coordinate
  function CARGO_GROUP:RouteTo( Coordinate )
    --self:T( {Coordinate = Coordinate } )
    
    -- For each Cargo within the CargoSet, route each object to the Coordinate
    self.CargoSet:ForEach(
      function( Cargo )
        Cargo.CargoObject:RouteGroundTo( Coordinate, 10, "vee", 0 )
      end
    )

  end
  
  --- Check if Cargo is near to the Carrier.
  -- The Cargo is near to the Carrier if the first unit of the Cargo Group is within NearRadius.
  -- @param #CARGO_GROUP self
  -- @param Wrapper.Group#GROUP CargoCarrier
  -- @param #number NearRadius
  -- @return #boolean The Cargo is near to the Carrier or #nil if the Cargo is not near to the Carrier.
  function CARGO_GROUP:IsNear( CargoCarrier, NearRadius )
    self:T( {NearRadius = NearRadius } )
    
    for _, Cargo in pairs( self.CargoSet:GetSet() ) do
      local Cargo = Cargo -- Cargo.Cargo#CARGO
      if Cargo:IsAlive() then
        if Cargo:IsNear( CargoCarrier:GetCoordinate(), NearRadius ) then
          self:T( "Near" )
          return true
        end
      end
    end
    
    return nil
  end

  --- Check if Cargo Group is in the radius for the Cargo to be Boarded.
  -- @param #CARGO_GROUP self
  -- @param Core.Point#COORDINATE Coordinate
  -- @return #boolean true if the Cargo Group is within the load radius.
  function CARGO_GROUP:IsInLoadRadius( Coordinate )
    --self:T( { Coordinate } )
  
    local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO

    if Cargo then
      local Distance = 0
      local CargoCoordinate
      if Cargo:IsLoaded() then
        CargoCoordinate = Cargo.CargoCarrier:GetCoordinate()
      else
        CargoCoordinate = Cargo.CargoObject:GetCoordinate()
      end

      -- FF check if coordinate could be obtained. This was commented out for some (unknown) reason. But the check seems valid!      
      if CargoCoordinate then
        Distance = Coordinate:Get2DDistance( CargoCoordinate )
      else
        return false
      end
      
      self:T( { Distance = Distance, LoadRadius = self.LoadRadius } )
      if Distance <= self.LoadRadius then
        return true
      else
        return false
      end
    end
    
    return nil
  
  end


  --- Check if Cargo Group is in the report radius.
  -- @param #CARGO_GROUP self
  -- @param Core.Point#Coordinate Coordinate
  -- @return #boolean true if the Cargo Group is within the report radius.
  function CARGO_GROUP:IsInReportRadius( Coordinate )
    --self:T( { Coordinate } )
  
    local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO

    if Cargo then
      self:T( { Cargo } )
      local Distance = 0
      if Cargo:IsUnLoaded() then
        Distance = Coordinate:Get2DDistance( Cargo.CargoObject:GetCoordinate() )
        --self:T( Distance )
        if Distance <= self.LoadRadius then
          return true
        end
      end
    end
    
    return nil
  
  end


  --- Signal a flare at the position of the CargoGroup.
  -- @param #CARGO_GROUP self
  -- @param Utilities.Utils#FLARECOLOR FlareColor
  function CARGO_GROUP:Flare( FlareColor )

    local Cargo = self.CargoSet:GetFirst() -- Cargo.Cargo#CARGO
    if Cargo then
      Cargo:Flare( FlareColor )
    end
  end
  
  --- Smoke the CargoGroup.
  -- @param #CARGO_GROUP self
  -- @param Utilities.Utils#SMOKECOLOR SmokeColor The color of the smoke.
  -- @param #number Radius The radius of randomization around the center of the first element of the CargoGroup.
  function CARGO_GROUP:Smoke( SmokeColor, Radius )

    local Cargo = self.CargoSet:GetFirst() -- Cargo.Cargo#CARGO

    if Cargo then
      Cargo:Smoke( SmokeColor, Radius )
    end
  end
  
  --- Check if the first element of the CargoGroup is the given @{Core.Zone}.
  -- @param #CARGO_GROUP self
  -- @param Core.Zone#ZONE_BASE Zone
  -- @return #boolean **true** if the first element of the CargoGroup is in the Zone
  -- @return #boolean **false** if there is no element of the CargoGroup in the Zone.
  function CARGO_GROUP:IsInZone( Zone )
    --self:T( { Zone } )
  
    local Cargo = self.CargoSet:GetFirst() -- Cargo.Cargo#CARGO

    if Cargo then
      return Cargo:IsInZone( Zone )
    end
    
    return nil
  
  end

  --- Get the transportation method of the Cargo.
  -- @param #CARGO_GROUP self
  -- @return #string The transportation method of the Cargo.
  function CARGO_GROUP:GetTransportationMethod()
    if self:IsLoaded() then
      return "for unboarding"
    else
      if self:IsUnLoaded() then
        return "for boarding"
      else
        if self:IsDeployed() then
          return "delivered"
        end
      end
    end
    return ""
  end

    

end -- CARGO_GROUP

--- **Functional** - Administer the scoring of player achievements, file and log the scoring events for use at websites.
--
-- ===
--
-- ## Features:
--
--   * Set the scoring scales based on threat level.
--   * Positive scores and negative scores.
--   * A contribution model to score achievements.
--   * Score goals.
--   * Score specific achievements.
--   * Score the hits and destroys of units.
--   * Score the hits and destroys of statics.
--   * Score the hits and destroys of scenery.
--   * (optional) Log scores into a CSV file.
--   * Connect to a remote server using JSON and IP.
--
-- ===
--
-- ## Missions:
--
-- [SCO - Scoring](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Functional/Scoring)
--
-- ===
--
-- Administers the scoring of player achievements,
-- and creates a CSV file logging the scoring events and results for use at team or squadron websites.
--
-- SCORING automatically calculates the threat level of the objects hit and destroyed by players,
-- which can be @{Wrapper.Unit}, @{Wrapper.Static) and @{Scenery} objects.
--
-- Positive score points are granted when enemy or neutral targets are destroyed.
-- Negative score points or penalties are given when a friendly target is hit or destroyed.
-- This brings a lot of dynamism in the scoring, where players need to take care to inflict damage on the right target.
-- By default, penalties weight heavier in the scoring, to ensure that players don't commit fratricide.
-- The total score of the player is calculated by **adding the scores minus the penalties**.
--
-- ![Banner Image](..\Presentations\SCORING\Dia4.JPG)
--
-- The score value is calculated based on the **threat level of the player** and the **threat level of the target**.
-- A calculated score takes the threat level of the target divided by a balanced threat level of the player unit.
-- As such, if the threat level of the target is high, and the player threat level is low, a higher score will be given than
-- if the threat level of the player would be high too.
--
-- ![Banner Image](..\Presentations\SCORING\Dia5.JPG)
--
-- When multiple players hit the same target, and finally succeed in destroying the target, then each player who contributed to the target
-- destruction, will receive a score. This is important for targets that require significant damage before it can be destroyed, like
-- ships or heavy planes.
--
-- ![Banner Image](..\Presentations\SCORING\Dia13.JPG)
--
-- Optionally, the score values can be **scaled** by a **scale**. Specific scales can be set for positive cores or negative penalties.
-- The default range of the scores granted is a value between 0 and 10. The default range of penalties given is a value between 0 and 30.
--
-- ![Banner Image](..\Presentations\SCORING\Dia7.JPG)
--
-- **Additional scores** can be granted to **specific objects**, when the player(s) destroy these objects.
--
-- ![Banner Image](..\Presentations\SCORING\Dia9.JPG)
--
-- Various @{Core.Zone}s can be defined for which scores are also granted when objects in that @{Core.Zone} are destroyed.
-- This is **specifically useful** to designate **scenery targets on the map** that will generate points when destroyed.
--
-- With a small change in MissionScripting.lua, the scoring results can also be logged in a **CSV file**.
-- These CSV files can be used to:
--
--   * Upload scoring to a database or a BI tool to publish the scoring results to the player community.
--   * Upload scoring in an (online) Excel like tool, using pivot tables and pivot charts to show mission results.
--   * Share scoring among players after the mission to discuss mission results.
--
-- Scores can be **reported**. **Menu options** are automatically added to **each player group** when a player joins a client slot or a CA unit.
-- Use the radio menu F10 to consult the scores while running the mission.
-- Scores can be reported for your user, or an overall score can be reported of all players currently active in the mission.
--
-- ===
--
-- ### Authors: **FlightControl**
--
-- ### Contributions:
--  
--   * **Applevangelist**: Additional functionality, fixes.
--   * **Wingthor (TAW)**: Testing & Advice.
--   * **Dutch-Baron (TAW)**: Testing & Advice.
--   * **Whisper**: Testing and Advice.
--
-- ===
--
-- @module Functional.Scoring
-- @image Scoring.JPG

--- @type SCORING
-- @field Players A collection of the current players that have joined the game.
-- @extends Core.Base#BASE

--- SCORING class
--
-- # Constructor:
--
--      local Scoring = SCORING:New( "Scoring File" )
--
-- # Set the destroy score or penalty scale:
--
-- Score scales can be set for scores granted when enemies or friendlies are destroyed.
-- Use the method @{#SCORING.SetScaleDestroyScore}() to set the scale of enemy destroys (positive destroys).
-- Use the method @{#SCORING.SetScaleDestroyPenalty}() to set the scale of friendly destroys (negative destroys).
--
--      local Scoring = SCORING:New( "Scoring File" )
--      Scoring:SetScaleDestroyScore( 10 )
--      Scoring:SetScaleDestroyPenalty( 40 )
--
-- The above sets the scale for valid scores to 10. So scores will be given in a scale from 0 to 10.
-- The penalties will be given in a scale from 0 to 40.
--
-- # Define special targets that will give extra scores:
--
-- Special targets can be set that will give extra scores to the players when these are destroyed.
-- Use the methods @{#SCORING.AddUnitScore}() and @{#SCORING.RemoveUnitScore}() to specify a special additional score for a specific @{Wrapper.Unit}s.
-- Use the methods @{#SCORING.AddStaticScore}() and @{#SCORING.RemoveStaticScore}() to specify a special additional score for a specific @{Wrapper.Static}s.
-- Use the method @{#SCORING.AddScoreSetGroup}() to specify a special additional score for a specific @{Wrapper.Group}s gathered in a @{Core.Set#SET_GROUP}.
--
--      local Scoring = SCORING:New( "Scoring File" )
--      Scoring:AddUnitScore( UNIT:FindByName( "Unit #001" ), 200 )
--      Scoring:AddStaticScore( STATIC:FindByName( "Static #1" ), 100 )
--      local GroupSet = SET_GROUP:New():FilterPrefixes("RAT"):FilterStart()
--      Scoring:AddScoreSetGroup( GroupSet, 100)
--
-- The above grants an additional score of 200 points for Unit #001 and an additional 100 points of Static #1 if these are destroyed.
-- Note that later in the mission, one can remove these scores set, for example, when the a goal achievement time limit is over.
-- For example, this can be done as follows:
--
--      Scoring:RemoveUnitScore( UNIT:FindByName( "Unit #001" ) )
--
-- # Define destruction zones that will give extra scores:
--
-- Define zones of destruction. Any object destroyed within the zone of the given category will give extra points.
-- Use the method @{#SCORING.AddZoneScore}() to add a @{Core.Zone} for additional scoring.
-- Use the method @{#SCORING.RemoveZoneScore}() to remove a @{Core.Zone} for additional scoring.
-- There are interesting variations that can be achieved with this functionality. For example, if the @{Core.Zone} is a @{Core.Zone#ZONE_UNIT},
-- then the zone is a moving zone, and anything destroyed within that @{Core.Zone} will generate points.
-- The other implementation could be to designate a scenery target (a building) in the mission editor surrounded by a @{Core.Zone},
-- just large enough around that building.
--
-- # Add extra Goal scores upon an event or a condition:
--
-- A mission has goals and achievements. The scoring system provides an API to set additional scores when a goal or achievement event happens.
-- Use the method @{#SCORING.AddGoalScore}() to add a score for a Player at any time in your mission.
--
-- # (Decommissioned) Configure fratricide level.
--
-- **This functionality is decommissioned until the DCS bug concerning Unit:destroy() not being functional in multi player for player units has been fixed by ED**.
--
-- When a player commits too much damage to friendlies, his penalty score will reach a certain level.
-- Use the method @{#SCORING.SetFratricide}() to define the level when a player gets kicked.
-- By default, the fratricide level is the default penalty multiplier * 2 for the penalty score.
--
-- # Penalty score when a player changes the coalition.
--
-- When a player changes the coalition, he can receive a penalty score.
-- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition.
-- By default, the penalty for changing coalition is the default penalty scale.
--
-- # Define output CSV files.
--
-- The CSV file is given the name of the string given in the @{#SCORING.New}{} constructor, followed by the .csv extension.
-- The file is incrementally saved in the **<User>\\Saved Games\\DCS\\Logs** folder, and has a time stamp indicating each mission run.
-- See the following example:
--
--     local ScoringFirstMission = SCORING:New( "FirstMission" )
--     local ScoringSecondMission = SCORING:New( "SecondMission" )
--
-- The above documents that 2 Scoring objects are created, ScoringFirstMission and ScoringSecondMission. 
--
-- ### **IMPORTANT!!!*
-- In order to allow DCS world to write CSV files, you need to adapt a configuration file in your DCS world installation **on the server**.
-- For this, browse to the **missionscripting.lua** file in your DCS world installation folder.
-- For me, this installation folder is in _D:\\Program Files\\Eagle Dynamics\\DCS World\Scripts_.
--
-- Edit a few code lines in the MissionScripting.lua file. Comment out the lines **os**, **io** and **lfs**:
--
--        do
--          --sanitizeModule('os')
--          --sanitizeModule('io')
--          --sanitizeModule('lfs')
--          require = nil
--          loadlib = nil
--        end
--
-- When these lines are not sanitized, functions become available to check the time, and to write files to your system at the above specified location.
-- Note that the MissionScripting.lua file provides a warning. So please beware of this warning as outlined by Eagle Dynamics!
--
--        --Sanitize Mission Scripting environment
--        --This makes unavailable some unsecure functions.
--        --Mission downloaded from server to client may contain potentially harmful lua code that may use these functions.
--        --You can remove the code below and make available these functions at your own risk.
--
-- The MOOSE designer cannot take any responsibility of any damage inflicted as a result of the de-sanitization.
-- That being said, I hope that the SCORING class provides you with a great add-on to score your squad mates achievements.
--
-- # Configure messages.
--
-- When players hit or destroy targets, messages are sent.
-- Various methods exist to configure:
--
--   * Which messages are sent upon the event.
--   * Which audience receives the message.
--
-- ## Configure the messages sent upon the event.
--
-- Use the following methods to configure when to send messages. By default, all messages are sent.
--
--   * @{#SCORING.SetMessagesHit}(): Configure to send messages after a target has been hit.
--   * @{#SCORING.SetMessagesDestroy}(): Configure to send messages after a target has been destroyed.
--   * @{#SCORING.SetMessagesAddon}(): Configure to send messages for additional score, after a target has been destroyed.
--   * @{#SCORING.SetMessagesZone}(): Configure to send messages for additional score, after a target has been destroyed within a given zone.
--
-- ## Configure the audience of the messages.
--
-- Use the following methods to configure the audience of the messages. By default, the messages are sent to all players in the mission.
--
--   * @{#SCORING.SetMessagesToAll}(): Configure to send messages to all players.
--   * @{#SCORING.SetMessagesToCoalition}(): Configure to send messages to only those players within the same coalition as the player.
--
-- ===
--
-- @field #SCORING
SCORING = {
  ClassName = "SCORING",
  ClassID = 0,
  Players = {},
  AutoSave = true,
  version = "1.18.4"
}

local _SCORINGCoalition = {
  [1] = "Red",
  [2] = "Blue",
}

local _SCORINGCategory = {
  [Unit.Category.AIRPLANE] = "Plane",
  [Unit.Category.HELICOPTER] = "Helicopter",
  [Unit.Category.GROUND_UNIT] = "Vehicle",
  [Unit.Category.SHIP] = "Ship",
  [Unit.Category.STRUCTURE] = "Structure",
}

--- Creates a new SCORING object to administer the scoring achieved by players.
-- @param #SCORING self
-- @param #string GameName The name of the game. This name is also logged in the CSV score file.
-- @param #string SavePath (Optional) Path where to save the CSV file, defaults to your **<User>\\Saved Games\\DCS\\Logs** folder.
-- @param #boolean AutoSave (Optional) If passed as `false`, then swith autosave off.
-- @return #SCORING self
-- @usage
--
--   -- Define a new scoring object for the mission Gori Valley.
--   ScoringObject = SCORING:New( "Gori Valley" )
--
function SCORING:New( GameName, SavePath, AutoSave )

  -- Inherits from BASE
  local self = BASE:Inherit( self, BASE:New() ) -- #SCORING

  if GameName then
    self.GameName = GameName
  else
    error( "A game name must be given to register the scoring results" )
  end

  -- Additional Object scores
  self.ScoringObjects = {}

  -- Additional Zone scores.
  self.ScoringZones = {}

  -- Configure Messages
  self:SetMessagesToAll()
  self:SetMessagesHit( false )
  self:SetMessagesDestroy( true )
  self:SetMessagesScore( true )
  self:SetMessagesZone( true )

  -- Scales
  
  self:SetScaleDestroyScore( 10 )
  self:SetScaleDestroyPenalty( 30 )

  -- Hitting a target multiple times before destoying it should not result in a higger score
  -- Multiple hits is typically a results of bombs/missles missing their target but still inflict some spash damage
  -- Making this configurable to anyone can enable this anyway if they want
  self:SetScoreIncrementOnHit(0)

  -- Default fratricide penalty level (maximum penalty that can be assigned to a player before he gets kicked).
  self:SetFratricide( self.ScaleDestroyPenalty * 3 )
  self.penaltyonfratricide = true
  
  -- Default penalty when a player changes coalition.
  self:SetCoalitionChangePenalty( self.ScaleDestroyPenalty )
  self.penaltyoncoalitionchange = true
  
  self:SetDisplayMessagePrefix()

  -- Event handlers  
  self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
  self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash )
  self:HandleEvent( EVENTS.Hit, self._EventOnHit )
  self:HandleEvent( EVENTS.Birth )
  -- self:HandleEvent( EVENTS.PlayerEnterUnit )
  self:HandleEvent( EVENTS.PlayerLeaveUnit )

  -- During mission startup, especially for single player,
  -- iterate the database for the player that has joined, and add him to the scoring, and set the menu.
  -- But this can only be started one second after the mission has started, so i need to schedule this ...
  self.ScoringPlayerScan = BASE:ScheduleOnce( 1, function()
    for PlayerName, PlayerUnit in pairs( _DATABASE:GetPlayerUnits() ) do
      self:_AddPlayerFromUnit( PlayerUnit )
      self:SetScoringMenu( PlayerUnit:GetGroup() )
    end
  end )

  -- Create the CSV file.
  self.AutoSavePath = SavePath
  self.AutoSave = AutoSave or true
  if self.AutoSave == true then
    self:OpenCSV( GameName )
  end

  return self

end

--- Set a prefix string that will be displayed at each scoring message sent.
-- @param #SCORING self
-- @param #string DisplayMessagePrefix (Default="Scoring: ") The scoring prefix string.
-- @return #SCORING
function SCORING:SetDisplayMessagePrefix( DisplayMessagePrefix )
  self.DisplayMessagePrefix = DisplayMessagePrefix or ""
  return self
end

--- Set the scale for scoring valid destroys (enemy destroys).
-- A default calculated score is a value between 1 and 10.
-- The scale magnifies the scores given to the players.
-- @param #SCORING self
-- @param #number Scale The scale of the score given.
function SCORING:SetScaleDestroyScore( Scale )
  self.ScaleDestroyScore = Scale
  return self
end

--- Set the scale for scoring penalty destroys (friendly destroys).
-- A default calculated penalty is a value between 1 and 10.
-- The scale magnifies the scores given to the players.
-- @param #SCORING self
-- @param #number Scale The scale of the score given.
-- @return #SCORING
function SCORING:SetScaleDestroyPenalty( Scale )

  self.ScaleDestroyPenalty = Scale

  return self
end

--- Add a @{Wrapper.Unit} for additional scoring when the @{Wrapper.Unit} is destroyed.
-- Note that if there was already a @{Wrapper.Unit} declared within the scoring with the same name,
-- then the old @{Wrapper.Unit}  will be replaced with the new @{Wrapper.Unit}.
-- @param #SCORING self
-- @param Wrapper.Unit#UNIT ScoreUnit The @{Wrapper.Unit} for which the Score needs to be given.
-- @param #number Score The Score value.
-- @return #SCORING
function SCORING:AddUnitScore( ScoreUnit, Score )

  local UnitName = ScoreUnit:GetName()

  self.ScoringObjects[UnitName] = Score

  return self
end

--- Removes a @{Wrapper.Unit} for additional scoring when the @{Wrapper.Unit} is destroyed.
-- @param #SCORING self
-- @param Wrapper.Unit#UNIT ScoreUnit The @{Wrapper.Unit} for which the Score needs to be given.
-- @return #SCORING
function SCORING:RemoveUnitScore( ScoreUnit )

  local UnitName = ScoreUnit:GetName()

  self.ScoringObjects[UnitName] = nil

  return self
end

--- Add a @{Wrapper.Static} for additional scoring when the @{Wrapper.Static} is destroyed.
-- Note that if there was already a @{Wrapper.Static} declared within the scoring with the same name,
-- then the old @{Wrapper.Static}  will be replaced with the new @{Wrapper.Static}.
-- @param #SCORING self
-- @param Wrapper.Static#UNIT ScoreStatic The @{Wrapper.Static} for which the Score needs to be given.
-- @param #number Score The Score value.
-- @return #SCORING
function SCORING:AddStaticScore( ScoreStatic, Score )

  local StaticName = ScoreStatic:GetName()

  self.ScoringObjects[StaticName] = Score

  return self
end

--- Removes a @{Wrapper.Static} for additional scoring when the @{Wrapper.Static} is destroyed.
-- @param #SCORING self
-- @param Wrapper.Static#UNIT ScoreStatic The @{Wrapper.Static} for which the Score needs to be given.
-- @return #SCORING
function SCORING:RemoveStaticScore( ScoreStatic )

  local StaticName = ScoreStatic:GetName()

  self.ScoringObjects[StaticName] = nil

  return self
end

--- Specify a special additional score for a @{Wrapper.Group}.
-- @param #SCORING self
-- @param Wrapper.Group#GROUP ScoreGroup The @{Wrapper.Group} for which each @{Wrapper.Unit} a Score is given.
-- @param #number Score The Score value.
-- @return #SCORING
function SCORING:AddScoreGroup( ScoreGroup, Score )

  local ScoreUnits = ScoreGroup:GetUnits()

  for ScoreUnitID, ScoreUnit in pairs( ScoreUnits ) do
    local UnitName = ScoreUnit:GetName()
    self.ScoringObjects[UnitName] = Score
  end

  return self
end

--- Specify a special additional score for a @{Core.Set#SET_GROUP}.
-- @param #SCORING self
-- @param Core.Set#SET_GROUP Set The @{Core.Set#SET_GROUP} for which each @{Wrapper.Unit} in each Group a Score is given.
-- @param #number Score The Score value.
-- @return #SCORING
function SCORING:AddScoreSetGroup(Set, Score)
  local set = Set:GetSetObjects()
  
  for _,_group in pairs (set) do
    if _group and _group:IsAlive() then
      self:AddScoreGroup(_group,Score)
    end
  end
  
  local function AddScore(group)
    self:AddScoreGroup(group,Score)
  end
  
  function Set:OnAfterAdded(From,Event,To,ObjectName,Object)
    AddScore(Object)
  end
  
  return self
end

--- Add a @{Core.Zone} to define additional scoring when any object is destroyed in that zone.
-- Note that if a @{Core.Zone} with the same name is already within the scoring added, the @{Core.Zone} (type) and Score will be replaced!
-- This allows for a dynamic destruction zone evolution within your mission.
-- @param #SCORING self
-- @param Core.Zone#ZONE_BASE ScoreZone The @{Core.Zone} which defines the destruction score perimeters.
-- Note that a zone can be a polygon or a moving zone.
-- @param #number Score The Score value.
-- @return #SCORING
function SCORING:AddZoneScore( ScoreZone, Score )

  local ZoneName = ScoreZone:GetName()

  self.ScoringZones[ZoneName] = {}
  self.ScoringZones[ZoneName].ScoreZone = ScoreZone
  self.ScoringZones[ZoneName].Score = Score

  return self
end

--- Remove a @{Core.Zone} for additional scoring.
-- The scoring will search if any @{Core.Zone} is added with the given name, and will remove that zone from the scoring.
-- This allows for a dynamic destruction zone evolution within your mission.
-- @param #SCORING self
-- @param Core.Zone#ZONE_BASE ScoreZone The @{Core.Zone} which defines the destruction score perimeters.
-- Note that a zone can be a polygon or a moving zone.
-- @return #SCORING
function SCORING:RemoveZoneScore( ScoreZone )

  local ZoneName = ScoreZone:GetName()

  self.ScoringZones[ZoneName] = nil

  return self
end

--- Configure to send messages after a target has been hit.
-- @param #SCORING self
-- @param #boolean OnOff If true is given, the messages are sent.
-- @return #SCORING
function SCORING:SetMessagesHit( OnOff )

  self.MessagesHit = OnOff
  return self
end

--- Configure to increment score after a target has been hit.
-- @param #SCORING self
-- @param #number score  amount of point to inclement score on each hit
-- @return #SCORING
function SCORING:SetScoreIncrementOnHit( score )

  self.ScoreIncrementOnHit = score
  return self
end

--- If to send messages after a target has been hit.
-- @param #SCORING self
-- @return #boolean
function SCORING:IfMessagesHit()

  return self.MessagesHit
end

--- Configure to send messages after a target has been destroyed.
-- @param #SCORING self
-- @param #boolean OnOff If true is given, the messages are sent.
-- @return #SCORING
function SCORING:SetMessagesDestroy( OnOff )

  self.MessagesDestroy = OnOff
  return self
end

--- If to send messages after a target has been destroyed.
-- @param #SCORING self
-- @return #boolean
function SCORING:IfMessagesDestroy()

  return self.MessagesDestroy
end

--- Configure to send messages after a target has been destroyed and receives additional scores.
-- @param #SCORING self
-- @param #boolean OnOff If true is given, the messages are sent.
-- @return #SCORING
function SCORING:SetMessagesScore( OnOff )

  self.MessagesScore = OnOff
  return self
end

--- If to send messages after a target has been destroyed and receives additional scores.
-- @param #SCORING self
-- @return #boolean
function SCORING:IfMessagesScore()

  return self.MessagesScore
end

--- Configure to send messages after a target has been hit in a zone, and additional score is received.
-- @param #SCORING self
-- @param #boolean OnOff If true is given, the messages are sent.
-- @return #SCORING
function SCORING:SetMessagesZone( OnOff )

  self.MessagesZone = OnOff
  return self
end

--- If to send messages after a target has been hit in a zone, and additional score is received.
-- @param #SCORING self
-- @return #boolean
function SCORING:IfMessagesZone()

  return self.MessagesZone
end

--- Configure to send messages to all players.
-- @param #SCORING self
-- @return #SCORING
function SCORING:SetMessagesToAll()

  self.MessagesAudience = 1
  return self
end

--- If to send messages to all players.
-- @param #SCORING self
-- @return #boolean
function SCORING:IfMessagesToAll()

  return self.MessagesAudience == 1
end

--- Configure to send messages to only those players within the same coalition as the player.
-- @param #SCORING self
-- @return #SCORING
function SCORING:SetMessagesToCoalition()

  self.MessagesAudience = 2
  return self
end

--- If to send messages to only those players within the same coalition as the player.
-- @param #SCORING self
-- @return #boolean
function SCORING:IfMessagesToCoalition()

  return self.MessagesAudience == 2
end

--- When a player commits too much damage to friendlies, his penalty score will reach a certain level.
-- Use this method to define the level when a player gets kicked.
-- By default, the fratricide level is the default penalty multiplier * 2 for the penalty score.
-- @param #SCORING self
-- @param #number Fratricide The amount of maximum penalty that may be inflicted by a friendly player before he gets kicked. 
-- @return #SCORING
function SCORING:SetFratricide( Fratricide )

  self.Fratricide = Fratricide
  return self
end

--- Decide if fratricide is leading to penalties (true) or not (false)
-- @param #SCORING self
-- @param #boolean OnOff Switch for Fratricide
-- @return #SCORING
function SCORING:SwitchFratricide( OnOff )
  self.penaltyonfratricide = OnOff
  return self
end

--- Decide if a change of coalition is leading to penalties (true) or not (false)
-- @param #SCORING self
-- @param #boolean OnOff Switch for Coalition Changes.
-- @return #SCORING
function SCORING:SwitchTreason( OnOff )
  self.penaltyoncoalitionchange = OnOff
  return self
end

--- When a player changes the coalition, he can receive a penalty score.
-- Use the method @{#SCORING.SetCoalitionChangePenalty}() to define the penalty when a player changes coalition.
-- By default, the penalty for changing coalition is the default penalty scale.
-- @param #SCORING self
-- @param #number CoalitionChangePenalty The amount of penalty that is given.
-- @return #SCORING
function SCORING:SetCoalitionChangePenalty( CoalitionChangePenalty )

  self.CoalitionChangePenalty = CoalitionChangePenalty
  return self
end

--- Sets the scoring menu.
-- @param #SCORING self
-- @return #SCORING
function SCORING:SetScoringMenu( ScoringGroup )
  local Menu = MENU_GROUP:New( ScoringGroup, 'Scoring and Statistics' )
  local ReportGroupSummary = MENU_GROUP_COMMAND:New( ScoringGroup, 'Summary report players in group', Menu, SCORING.ReportScoreGroupSummary, self, ScoringGroup )
  local ReportGroupDetailed = MENU_GROUP_COMMAND:New( ScoringGroup, 'Detailed report players in group', Menu, SCORING.ReportScoreGroupDetailed, self, ScoringGroup )
  local ReportToAllSummary = MENU_GROUP_COMMAND:New( ScoringGroup, 'Summary report all players', Menu, SCORING.ReportScoreAllSummary, self, ScoringGroup )
  self:SetState( ScoringGroup, "ScoringMenu", Menu )
  return self
end

--- Add a new player entering a Unit.
-- @param #SCORING self
-- @param Wrapper.Unit#UNIT UnitData
function SCORING:_AddPlayerFromUnit( UnitData )
  self:F( UnitData )

  if UnitData:IsAlive() then
    local UnitName = UnitData:GetName()
    local PlayerName = UnitData:GetPlayerName()
    local UnitDesc = UnitData:GetDesc()
    local UnitCategory = UnitDesc.category
    local UnitCoalition = UnitData:GetCoalition()
    local UnitTypeName = UnitData:GetTypeName()
    local UnitThreatLevel, UnitThreatType = UnitData:GetThreatLevel()

    self:T( { PlayerName, UnitName, UnitCategory, UnitCoalition, UnitTypeName } )

    if self.Players[PlayerName] == nil then -- I believe this is the place where a Player gets a life in a mission when he enters a unit ...
      self.Players[PlayerName] = {}
      self.Players[PlayerName].Hit = {}
      self.Players[PlayerName].Destroy = {}
      self.Players[PlayerName].Goals = {}
      self.Players[PlayerName].Mission = {}

      -- for CategoryID, CategoryName in pairs( SCORINGCategory ) do
      -- self.Players[PlayerName].Hit[CategoryID] = {}
      -- self.Players[PlayerName].Destroy[CategoryID] = {}
      -- end
      self.Players[PlayerName].HitPlayers = {}
      self.Players[PlayerName].Score = 0
      self.Players[PlayerName].Penalty = 0
      self.Players[PlayerName].PenaltyCoalition = 0
      self.Players[PlayerName].PenaltyWarning = 0
    end

    if not self.Players[PlayerName].UnitCoalition then
      self.Players[PlayerName].UnitCoalition = UnitCoalition
    else
      -- TODO: switch for coalition changes, make penalty alterable
      if self.Players[PlayerName].UnitCoalition ~= UnitCoalition and self.penaltyoncoalitionchange then
        self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + self.CoalitionChangePenalty or 50
        self.Players[PlayerName].PenaltyCoalition = self.Players[PlayerName].PenaltyCoalition + 1
        MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' changed coalition from " .. _SCORINGCoalition[self.Players[PlayerName].UnitCoalition] .. " to " .. _SCORINGCoalition[UnitCoalition] ..
          "(changed " .. self.Players[PlayerName].PenaltyCoalition .. " times the coalition). ".. self.CoalitionChangePenalty .." penalty points added.",
          MESSAGE.Type.Information
        ):ToAll()
        self:ScoreCSV( PlayerName, "", "COALITION_PENALTY",  1, -1*self.CoalitionChangePenalty, self.Players[PlayerName].UnitName, _SCORINGCoalition[self.Players[PlayerName].UnitCoalition], _SCORINGCategory[self.Players[PlayerName].UnitCategory], self.Players[PlayerName].UnitType,
          UnitName, _SCORINGCoalition[UnitCoalition], _SCORINGCategory[UnitCategory], UnitData:GetTypeName() )
      end
    end

    self.Players[PlayerName].UnitName = UnitName
    self.Players[PlayerName].UnitCoalition = UnitCoalition
    self.Players[PlayerName].UnitCategory = UnitCategory
    self.Players[PlayerName].UnitType = UnitTypeName
    self.Players[PlayerName].UNIT = UnitData
    self.Players[PlayerName].ThreatLevel = UnitThreatLevel
    self.Players[PlayerName].ThreatType = UnitThreatType
    
    -- TODO: make fratricide switchable
    if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 and self.penaltyonfratricide then
      if self.Players[PlayerName].PenaltyWarning < 1 then
        MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "': WARNING! If you continue to commit FRATRICIDE and have a PENALTY score higher than " .. self.Fratricide .. ", you will be COURT MARTIALED and DISMISSED from this mission! \nYour total penalty is: " .. self.Players[PlayerName].Penalty,
                         MESSAGE.Type.Information )
               :ToAll()
        self.Players[PlayerName].PenaltyWarning = self.Players[PlayerName].PenaltyWarning + 1
      end
    end

    if self.Players[PlayerName].Penalty > self.Fratricide and self.penaltyonfratricide then
      MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' committed FRATRICIDE, he will be COURT MARTIALED and is DISMISSED from this mission!",
                       MESSAGE.Type.Information )
             :ToAll()
      UnitData:GetGroup():Destroy()
    end
  end
end

--- Add a goal score for a player.
-- The method takes the Player name for which the Goal score needs to be set.
-- The GoalTag is a string or identifier that is taken into the CSV file scoring log to identify the goal.
-- A free text can be given that is shown to the players.
-- The Score can be both positive and negative.
-- @param #SCORING self
-- @param #string PlayerName The name of the Player.
-- @param #string GoalTag The string or identifier that is used in the CSV file to identify the goal (sort or group later in Excel).
-- @param #string Text A free text that is shown to the players.
-- @param #number Score The score can be both positive or negative ( Penalty ).
function SCORING:AddGoalScorePlayer( PlayerName, GoalTag, Text, Score )

  self:F( { PlayerName, PlayerName, GoalTag, Text, Score } )

  -- PlayerName can be nil, if the Unit with the player crashed or due to another reason.
  if PlayerName then
    local PlayerData = self.Players[PlayerName]

    PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 }
    PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score
    PlayerData.Score = PlayerData.Score + Score
    if Text then
      MESSAGE:NewType( self.DisplayMessagePrefix .. Text,
                       MESSAGE.Type.Information )
             :ToAll()
    end
    self:ScoreCSV( PlayerName, "", "GOAL_" .. string.upper( GoalTag ), 1, Score, nil )
  end
end

--- Add a goal score for a player.
-- The method takes the PlayerUnit for which the Goal score needs to be set.
-- The GoalTag is a string or identifier that is taken into the CSV file scoring log to identify the goal.
-- A free text can be given that is shown to the players.
-- The Score can be both positive and negative.
-- @param #SCORING self
-- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the Player. Other Properties for the scoring are taken from this PlayerUnit, like coalition, type etc.
-- @param #string GoalTag The string or identifier that is used in the CSV file to identify the goal (sort or group later in Excel).
-- @param #string Text A free text that is shown to the players.
-- @param #number Score The score can be both positive or negative ( Penalty ).
function SCORING:AddGoalScore( PlayerUnit, GoalTag, Text, Score )

  local PlayerName = PlayerUnit:GetPlayerName()

  self:T2( { PlayerUnit.UnitName, PlayerName, GoalTag, Text, Score } )

  -- PlayerName can be nil, if the Unit with the player crashed or due to another reason.
  if PlayerName then
    local PlayerData = self.Players[PlayerName]

    PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 }
    PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score
    PlayerData.Score = PlayerData.Score + Score
    
    if Text then
      MESSAGE:NewType( self.DisplayMessagePrefix .. Text,
                     MESSAGE.Type.Information )
           :ToAll()
    end
    self:ScoreCSV( PlayerName, "", "GOAL_" .. string.upper( GoalTag ), 1, Score, PlayerUnit:GetName() )
  end
end

--- Registers Scores the players completing a Mission Task.
-- @param #SCORING self
-- @param Tasking.Mission#MISSION Mission
-- @param Wrapper.Unit#UNIT PlayerUnit
-- @param #string Text
-- @param #number Score
function SCORING:_AddMissionTaskScore( Mission, PlayerUnit, Text, Score )

  local PlayerName = PlayerUnit:GetPlayerName()
  local MissionName = Mission:GetName()

  self:F( { Mission:GetName(), PlayerUnit.UnitName, PlayerName, Text, Score } )

  -- PlayerName can be nil, if the Unit with the player crashed or due to another reason.
  if PlayerName then
    local PlayerData = self.Players[PlayerName]

    if not PlayerData.Mission[MissionName] then
      PlayerData.Mission[MissionName] = {}
      PlayerData.Mission[MissionName].ScoreTask = 0
      PlayerData.Mission[MissionName].ScoreMission = 0
    end

    self:T( PlayerName )
    self:T( PlayerData.Mission[MissionName] )

    PlayerData.Score = self.Players[PlayerName].Score + Score
    PlayerData.Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score
    
    if Text then
      MESSAGE:NewType( self.DisplayMessagePrefix .. Mission:GetText() .. " : " .. Text .. " Score: " .. Score,
                       MESSAGE.Type.Information )
             :ToAll()
    end
    self:ScoreCSV( PlayerName, "", "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score, PlayerUnit:GetName() )
  end
end

--- Registers Scores the players completing a Mission Task.
-- @param #SCORING self
-- @param Tasking.Mission#MISSION Mission
-- @param #string PlayerName
-- @param #string Text
-- @param #number Score
function SCORING:_AddMissionGoalScore( Mission, PlayerName, Text, Score )

  local MissionName = Mission:GetName()

  self:F( { Mission:GetName(), PlayerName, Text, Score } )

  -- PlayerName can be nil, if the Unit with the player crashed or due to another reason.
  if PlayerName then
    local PlayerData = self.Players[PlayerName]

    if not PlayerData.Mission[MissionName] then
      PlayerData.Mission[MissionName] = {}
      PlayerData.Mission[MissionName].ScoreTask = 0
      PlayerData.Mission[MissionName].ScoreMission = 0
    end

    self:T( PlayerName )
    self:T( PlayerData.Mission[MissionName] )

    PlayerData.Score = self.Players[PlayerName].Score + Score
    PlayerData.Mission[MissionName].ScoreTask = self.Players[PlayerName].Mission[MissionName].ScoreTask + Score
    
    if Text then  
      MESSAGE:NewType( string.format( "%s%s: %s! Player %s receives %d score!", self.DisplayMessagePrefix, Mission:GetText(), Text, PlayerName, Score ), MESSAGE.Type.Information ):ToAll()
    end
    
    self:ScoreCSV( PlayerName, "", "TASK_" .. MissionName:gsub( ' ', '_' ), 1, Score )
  end
end

--- Registers Mission Scores for possible multiple players that contributed in the Mission.
-- @param #SCORING self
-- @param Tasking.Mission#MISSION Mission
-- @param Wrapper.Unit#UNIT PlayerUnit
-- @param #string Text
-- @param #number Score
function SCORING:_AddMissionScore( Mission, Text, Score )

  local MissionName = Mission:GetName()

  self:F( { Mission, Text, Score } )
  self:F( self.Players )

  for PlayerName, PlayerData in pairs( self.Players ) do

    self:F( PlayerData )
    if PlayerData.Mission[MissionName] then

      PlayerData.Score = PlayerData.Score + Score
      PlayerData.Mission[MissionName].ScoreMission = PlayerData.Mission[MissionName].ScoreMission + Score
      
      if Text then
        MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' has " .. Text .. " in " .. Mission:GetText() .. ". " .. Score .. " mission score!",
                       MESSAGE.Type.Information )
             :ToAll()
      end
      self:ScoreCSV( PlayerName, "", "MISSION_" .. MissionName:gsub( ' ', '_' ), 1, Score )
    end
  end
end

--- Handles the OnPlayerEnterUnit event for the scoring.
-- @param #SCORING self
-- @param Core.Event#EVENTDATA Event
-- function SCORING:OnEventPlayerEnterUnit( Event )
--  if Event.IniUnit then
--    self:_AddPlayerFromUnit( Event.IniUnit )
--    self:SetScoringMenu( Event.IniGroup )
--  end
-- end

--- Handles the OnBirth event for the scoring.
-- @param #SCORING self
-- @param Core.Event#EVENTDATA Event
function SCORING:OnEventBirth( Event )

  if Event.IniUnit then
    Event.IniUnit.ThreatLevel, Event.IniUnit.ThreatType = Event.IniUnit:GetThreatLevel()
    if Event.IniObjectCategory == 1 then
      local PlayerName = Event.IniUnit:GetPlayerName()
      Event.IniUnit.BirthTime = timer.getTime()
      if PlayerName then
        self:_AddPlayerFromUnit( Event.IniUnit )
        self.Players[PlayerName].PlayerKills = 0
        self:SetScoringMenu( Event.IniGroup )
      end
    end
  end
end

--- Handles the OnPlayerLeaveUnit event for the scoring.
-- @param #SCORING self
-- @param Core.Event#EVENTDATA Event
function SCORING:OnEventPlayerLeaveUnit( Event )
  if Event.IniUnit then
    local Menu = self:GetState( Event.IniUnit:GetGroup(), "ScoringMenu" ) -- Core.Menu#MENU_GROUP
    if Menu then
      -- TODO: Check if this fixes #281.
      -- Menu:Remove()
    end
  end
end

--- Handles the OnHit event for the scoring.
-- @param #SCORING self
-- @param Core.Event#EVENTDATA Event
function SCORING:_EventOnHit( Event )
  self:F( { Event } )

  local InitUnit = nil
  local InitUNIT = nil
  local InitUnitName = ""
  local InitGroup = nil
  local InitGroupName = ""
  local InitPlayerName = nil

  local InitCoalition = nil
  local InitCategory = nil
  local InitType = nil
  local InitUnitCoalition = nil
  local InitUnitCategory = nil
  local InitUnitType = nil

  local TargetUnit = nil
  local TargetUNIT = nil
  local TargetUnitName = ""
  local TargetGroup = nil
  local TargetGroupName = ""
  local TargetPlayerName = nil

  local TargetCoalition = nil
  local TargetCategory = nil
  local TargetType = nil
  local TargetUnitCoalition = nil
  local TargetUnitCategory = nil
  local TargetUnitType = nil
  local TargetIsScenery = false

  if Event.IniDCSUnit then

    InitUnit = Event.IniDCSUnit
    InitUNIT = Event.IniUnit
    InitUnitName = Event.IniDCSUnitName
    InitGroup = Event.IniDCSGroup
    InitGroupName = Event.IniDCSGroupName
    InitPlayerName = Event.IniPlayerName

    InitCoalition = Event.IniCoalition
    -- TODO: Workaround Client DCS Bug
    -- InitCategory = InitUnit:getCategory()
    -- InitCategory = InitUnit:getDesc().category
    InitCategory = Event.IniCategory
    InitType = Event.IniTypeName

    InitUnitCoalition = _SCORINGCoalition[InitCoalition]
    InitUnitCategory = _SCORINGCategory[InitCategory]
    InitUnitType = InitType

    self:T( { InitUnitName, InitGroupName, InitPlayerName, InitCoalition, InitCategory, InitType, InitUnitCoalition, InitUnitCategory, InitUnitType } )
  end

  if Event.TgtDCSUnit then

    TargetUnit = Event.TgtDCSUnit
    TargetUNIT = Event.TgtUnit
    TargetUnitName = Event.TgtDCSUnitName
    TargetGroup = Event.TgtDCSGroup
    TargetGroupName = Event.TgtDCSGroupName
    TargetPlayerName = Event.TgtPlayerName

    TargetCoalition = Event.TgtCoalition
    -- TODO: Workaround Client DCS Bug
    -- TargetCategory = TargetUnit:getCategory()
    -- TargetCategory = TargetUnit:getDesc().category
    TargetCategory = Event.TgtCategory
    TargetType = Event.TgtTypeName

    -- Scenery hit
    if (not TargetCategory) and TargetUNIT ~= nil and TargetUnit:IsInstanceOf("SCENERY") then
        TargetCategory = Unit.Category.STRUCTURE
        TargetIsScenery = true
    end
    
    TargetUnitCoalition = _SCORINGCoalition[TargetCoalition]
    TargetUnitCategory = _SCORINGCategory[TargetCategory]
    TargetUnitType = TargetType

    self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType, TargetUnitCoalition, TargetUnitCategory, TargetUnitType } )
  end

  if InitPlayerName ~= nil then -- It is a player that is hitting something
    self:_AddPlayerFromUnit( InitUNIT )
    if self.Players[InitPlayerName] then -- This should normally not happen, but i'll test it anyway.
      if TargetPlayerName ~= nil then -- It is a player hitting another player ...
        self:_AddPlayerFromUnit( TargetUNIT )
      end

      self:T( "Hitting Something" )

      -- What is he hitting?
      if TargetCategory then

        -- A target got hit, score it.
        -- Player contains the score data from self.Players[InitPlayerName]
        local Player = self.Players[InitPlayerName]

        -- Ensure there is a hit table per TargetCategory and TargetUnitName.
        Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {}
        Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {}

        -- PlayerHit contains the score counters and data per unit that was hit.
        local PlayerHit = Player.Hit[TargetCategory][TargetUnitName]

        PlayerHit.Score = PlayerHit.Score or 0
        PlayerHit.Penalty = PlayerHit.Penalty or 0
        PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0
        PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0
        PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0
        PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT
        -- After an instant kill we can't compute the threat level anymore. To fix this we compute at OnEventBirth
        if PlayerHit.UNIT.ThreatType == nil then
          PlayerHit.ThreatLevel, PlayerHit.ThreatType = PlayerHit.UNIT:GetThreatLevel()
          -- if this fails for some reason, set a good default value
          if PlayerHit.ThreatType == nil or PlayerHit.ThreatType == "" then
            PlayerHit.ThreatLevel = 1
            PlayerHit.ThreatType = "Unknown"
          end
        else
          PlayerHit.ThreatLevel = PlayerHit.UNIT.ThreatLevel
          PlayerHit.ThreatType = PlayerHit.UNIT.ThreatType
        end
        
        -- Only grant hit scores if there was more than one second between the last hit.        
        if timer.getTime() - PlayerHit.TimeStamp > 1 then
          PlayerHit.TimeStamp = timer.getTime()

          if TargetPlayerName ~= nil then -- It is a player hitting another player ...

            -- Ensure there is a Player to Player hit reference table.
            Player.HitPlayers[TargetPlayerName] = true
          end

          local Score = 0

          if InitCoalition then -- A coalition object was hit.
            if InitCoalition == TargetCoalition then
              local Penalty = 10
              Player.Penalty = Player.Penalty + Penalty
              PlayerHit.Penalty = PlayerHit.Penalty + Penalty
              PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1

              if TargetPlayerName ~= nil then -- It is a player hitting another player ...
                MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit friendly player '" .. TargetPlayerName .. "' " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " ..
                                 "Penalty: -" .. Penalty .. ".  Score Total:" .. Player.Score - Player.Penalty,
                                 MESSAGE.Type.Update )
                       :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() )
                       :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() )
              else
                MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit friendly target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.PenaltyHit .. " times. " ..
                                 "Penalty: -" .. Penalty .. ".  Score Total:" .. Player.Score - Player.Penalty,
                                 MESSAGE.Type.Update )
                      :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() )
                      :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() )
              end
              self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
            else
              Player.Score = Player.Score + self.ScoreIncrementOnHit
              PlayerHit.Score = PlayerHit.Score + self.ScoreIncrementOnHit
              PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1
              if TargetPlayerName ~= nil then -- It is a player hitting another player ...
                MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit enemy player '" .. TargetPlayerName .. "' " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " ..
                                 "Score: " .. PlayerHit.Score .. ".  Score Total:" .. Player.Score - Player.Penalty,
                                 MESSAGE.Type.Update )
                       :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() )
                       :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() )
              elseif TargetIsScenery ~= true then
                MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit enemy target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " .. PlayerHit.ScoreHit .. " times. " ..
                                 "Score: " .. PlayerHit.Score .. ".  Score Total:" .. Player.Score - Player.Penalty,
                                 MESSAGE.Type.Update )
                       :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() )
                       :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() )
              elseif TargetIsScenery == true then
                MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit scenery object." .. " Score: " .. PlayerHit.Score .. ".  Score Total:" .. Player.Score - Player.Penalty,
                             MESSAGE.Type.Update )
                   :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() )
                   :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() )
              end
              self:ScoreCSV( InitPlayerName, TargetPlayerName, "HIT_SCORE", 1, 1, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
            end
          else -- A scenery object was hit.
            MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. InitPlayerName .. "' hit nothing special.",
                             MESSAGE.Type.Update )
                   :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() )
                   :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() )

            self:ScoreCSV( InitPlayerName, "", "HIT_SCORE", 1, 0, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType )
          end
        end
      end
    end
  elseif InitPlayerName == nil then -- It is an AI hitting a player???

  end

  -- It is a weapon initiated by a player, that is hitting something
  -- This seems to occur only with scenery and static objects.
  if Event.WeaponPlayerName ~= nil then
    self:_AddPlayerFromUnit( Event.WeaponUNIT )
    if self.Players[Event.WeaponPlayerName] then -- This should normally not happen, but i'll test it anyway.
      if TargetPlayerName ~= nil then -- It is a player hitting another player ...
        self:_AddPlayerFromUnit( TargetUNIT )
      end

      self:T( "Hitting Scenery" )

      -- What is he hitting?
      if TargetCategory then

        -- A scenery or static got hit, score it.
        -- Player contains the score data from self.Players[WeaponPlayerName]
        local Player = self.Players[Event.WeaponPlayerName]

        -- Ensure there is a hit table per TargetCategory and TargetUnitName.
        Player.Hit[TargetCategory] = Player.Hit[TargetCategory] or {}
        Player.Hit[TargetCategory][TargetUnitName] = Player.Hit[TargetCategory][TargetUnitName] or {}

        -- PlayerHit contains the score counters and data per unit that was hit.
        local PlayerHit = Player.Hit[TargetCategory][TargetUnitName]

        PlayerHit.Score = PlayerHit.Score or 0
        PlayerHit.Penalty = PlayerHit.Penalty or 0
        PlayerHit.ScoreHit = PlayerHit.ScoreHit or 0
        PlayerHit.PenaltyHit = PlayerHit.PenaltyHit or 0
        PlayerHit.TimeStamp = PlayerHit.TimeStamp or 0
        PlayerHit.UNIT = PlayerHit.UNIT or TargetUNIT
        -- After an instant kill we can't compute the threat level anymore. To fix this we compute at OnEventBirth
        if PlayerHit.UNIT.ThreatType == nil then
          PlayerHit.ThreatLevel, PlayerHit.ThreatType = PlayerHit.UNIT:GetThreatLevel()
          -- if this fails for some reason, set a good default value
          if PlayerHit.ThreatType == nil then
            PlayerHit.ThreatLevel = 1
            PlayerHit.ThreatType = "Unknown"
          end
        else
          PlayerHit.ThreatLevel = PlayerHit.UNIT.ThreatLevel
          PlayerHit.ThreatType = PlayerHit.UNIT.ThreatType
        end

        -- Only grant hit scores if there was more than one second between the last hit.        
        if timer.getTime() - PlayerHit.TimeStamp > 1 then
          PlayerHit.TimeStamp = timer.getTime()

          local Score = 0

          if InitCoalition then -- A coalition object was hit, probably a static.
            if InitCoalition == TargetCoalition then
              -- TODO: Penalty according scale
              local Penalty = 10
              Player.Penalty = Player.Penalty + Penalty --* self.ScaleDestroyPenalty
              PlayerHit.Penalty = PlayerHit.Penalty + Penalty --* self.ScaleDestroyPenalty
              PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1 * self.ScaleDestroyPenalty
      
              MESSAGE
                :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit friendly target " .. 
                      TargetUnitCategory .. " ( " .. TargetType .. " ) " .. 
                      "Penalty: -" .. Penalty .. " = " .. Player.Score - Player.Penalty,
                      MESSAGE.Type.Update
                    )
                :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() )
                :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() )
              self:ScoreCSV( Event.WeaponPlayerName, TargetPlayerName, "HIT_PENALTY", 1, -10, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
            else
              Player.Score = Player.Score + self.ScoreIncrementOnHit
              PlayerHit.Score = PlayerHit.Score + self.ScoreIncrementOnHit
              PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1
              MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit enemy target " .. TargetUnitCategory .. " ( " .. TargetType .. " ) " ..
                               "Score: " .. PlayerHit.Score .. ".  Score Total:" .. Player.Score - Player.Penalty,
                               MESSAGE.Type.Update )
                     :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() )
                     :ToCoalitionIf( Event.WeaponCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() )

              self:ScoreCSV( Event.WeaponPlayerName, TargetPlayerName, "HIT_SCORE", 1, 1, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
            end
          else -- A scenery object was hit.
            MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit scenery object.",
                             MESSAGE.Type.Update )
                   :ToAllIf( self:IfMessagesHit() and self:IfMessagesToAll() )
                   :ToCoalitionIf( InitCoalition, self:IfMessagesHit() and self:IfMessagesToCoalition() )

            self:ScoreCSV( Event.WeaponPlayerName, "", "HIT_SCORE", 1, 0, Event.WeaponName, Event.WeaponCoalition, Event.WeaponCategory, Event.WeaponTypeName, TargetUnitName, "", "Scenery", TargetUnitType )
          end
        end
      end
    end
  end
end

--- Track  DEAD or CRASH events for the scoring.
-- @param #SCORING self
-- @param Core.Event#EVENTDATA Event
function SCORING:_EventOnDeadOrCrash( Event )
  self:F( { Event } )

  local TargetUnit = nil
  local TargetGroup = nil
  local TargetUnitName = ""
  local TargetGroupName = ""
  local TargetPlayerName = ""
  local TargetCoalition = nil
  local TargetCategory = nil
  local TargetType = nil
  local TargetUnitCoalition = nil
  local TargetUnitCategory = nil
  local TargetUnitType = nil

  if Event.IniDCSUnit then

    TargetUnit = Event.IniUnit
    TargetUnitName = Event.IniDCSUnitName
    TargetGroup = Event.IniDCSGroup
    TargetGroupName = Event.IniDCSGroupName
    TargetPlayerName = Event.IniPlayerName

    TargetCoalition = Event.IniCoalition
    -- TargetCategory = TargetUnit:getCategory()
    -- TargetCategory = TargetUnit:getDesc().category  -- Workaround
    TargetCategory = Event.IniCategory
    TargetType = Event.IniTypeName

    TargetUnitCoalition = _SCORINGCoalition[TargetCoalition]
    TargetUnitCategory = _SCORINGCategory[TargetCategory]
    TargetUnitType = TargetType

    self:T( { TargetUnitName, TargetGroupName, TargetPlayerName, TargetCoalition, TargetCategory, TargetType } )
  end

  -- Player contains the score and reference data for the player.
  for PlayerName, Player in pairs( self.Players ) do
    if Player then -- This should normally not happen, but i'll test it anyway.
      self:T( "Something got destroyed" )

      -- Some variables
      local InitUnitName = Player.UnitName
      local InitUnitType = Player.UnitType
      local InitCoalition = Player.UnitCoalition
      local InitCategory = Player.UnitCategory
      local InitUnitCoalition = _SCORINGCoalition[InitCoalition]
      local InitUnitCategory = _SCORINGCategory[InitCategory]

      self:T( { InitUnitName, InitUnitType, InitUnitCoalition, InitCoalition, InitUnitCategory, InitCategory } )

      local Destroyed = false

      -- What is the player destroying?
      if Player and Player.Hit and Player.Hit[TargetCategory] and Player.Hit[TargetCategory][TargetUnitName] and Player.Hit[TargetCategory][TargetUnitName].TimeStamp ~= 0 and (TargetUnit.BirthTime == nil or Player.Hit[TargetCategory][TargetUnitName].TimeStamp > TargetUnit.BirthTime) then -- Was there a hit for this unit for this player before registered???

        local TargetThreatLevel = Player.Hit[TargetCategory][TargetUnitName].ThreatLevel
        local TargetThreatType = Player.Hit[TargetCategory][TargetUnitName].ThreatType

        Player.Destroy[TargetCategory] = Player.Destroy[TargetCategory] or {}
        Player.Destroy[TargetCategory][TargetType] = Player.Destroy[TargetCategory][TargetType] or {}

        -- PlayerDestroy contains the destroy score data per category and target type of the player.
        local TargetDestroy = Player.Destroy[TargetCategory][TargetType]
        TargetDestroy.Score = TargetDestroy.Score or 0
        TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy or 0
        TargetDestroy.Penalty = TargetDestroy.Penalty or 0
        TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy or 0

        if TargetCoalition then
          if InitCoalition == TargetCoalition then
            local ThreatLevelTarget = TargetThreatLevel
            local ThreatTypeTarget = TargetThreatType
            local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1
            local ThreatPenalty = math.ceil( (ThreatLevelTarget / ThreatLevelPlayer) * self.ScaleDestroyPenalty / 10 )
            self:F( { ThreatLevel = ThreatPenalty, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } )

            Player.Penalty = Player.Penalty + ThreatPenalty
            TargetDestroy.Penalty = TargetDestroy.Penalty + ThreatPenalty
            TargetDestroy.PenaltyDestroy = TargetDestroy.PenaltyDestroy + 1


           --self:OnKillPvP(PlayerName, TargetPlayerName, true, TargetThreatLevel, Player.ThreatLevel, ThreatPenalty)

            if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player
              self:OnKillPvP(PlayerName, TargetPlayerName, true)
              MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed friendly player '" .. TargetPlayerName .. "' " .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " ..
                               "Penalty: -" .. ThreatPenalty .. " = " .. Player.Score - Player.Penalty,
                               MESSAGE.Type.Information )
                     :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() )
                     :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() )
            else
              self:OnKillPvE(PlayerName, TargetUnitName, true, TargetThreatLevel, Player.ThreatLevel, ThreatPenalty)
              MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed friendly target " .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " ..
                               "Penalty: -" .. ThreatPenalty .. " = " .. Player.Score - Player.Penalty,
                               MESSAGE.Type.Information )
                     :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() )
                     :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() )
            end

            self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_PENALTY", 1, ThreatPenalty, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
            Destroyed = true
          else

            local ThreatLevelTarget = TargetThreatLevel
            local ThreatTypeTarget = TargetThreatType
            local ThreatLevelPlayer = Player.ThreatLevel / 10 + 1
            local ThreatScore = math.ceil( (ThreatLevelTarget / ThreatLevelPlayer) * self.ScaleDestroyScore / 10 )

            self:F( { ThreatLevel = ThreatScore, ThreatLevelTarget = ThreatLevelTarget, ThreatTypeTarget = ThreatTypeTarget, ThreatLevelPlayer = ThreatLevelPlayer } )

            Player.Score = Player.Score + ThreatScore
            TargetDestroy.Score = TargetDestroy.Score + ThreatScore
            TargetDestroy.ScoreDestroy = TargetDestroy.ScoreDestroy + 1
            if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player
              if Player.PlayerKills ~= nil then
                Player.PlayerKills = Player.PlayerKills + 1
              else
                Player.PlayerKills = 1
              end
              self:OnKillPvP(PlayerName, TargetPlayerName, false, TargetThreatLevel, Player.ThreatLevel, ThreatScore)
              MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed enemy player '" .. TargetPlayerName .. "' " .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " ..
                               "Score: +" .. ThreatScore .. " = " .. Player.Score - Player.Penalty,
                               MESSAGE.Type.Information )
                     :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() )
                     :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() )
            else
              self:OnKillPvE(PlayerName, TargetUnitName, false, TargetThreatLevel, Player.ThreatLevel, ThreatScore)
              MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed enemy " .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " ..
                               "Score: +" .. ThreatScore .. " = " .. Player.Score - Player.Penalty,
                               MESSAGE.Type.Information )
                     :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() )
                     :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() )
            end

            self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, ThreatScore, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
            Destroyed = true

            local UnitName = TargetUnit:GetName()
            local Score = self.ScoringObjects[UnitName]
            if Score then
              Player.Score = Player.Score + Score
              TargetDestroy.Score = TargetDestroy.Score + Score
              MESSAGE:NewType( self.DisplayMessagePrefix .. "Special target '" .. TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. " destroyed! " ..
                               "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! Total: " .. Player.Score - Player.Penalty,
                               MESSAGE.Type.Information )
                     :ToAllIf( self:IfMessagesScore() and self:IfMessagesToAll() )
                     :ToCoalitionIf( InitCoalition, self:IfMessagesScore() and self:IfMessagesToCoalition() )

              self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
              Destroyed = true
            end

            -- Check if there are Zones where the destruction happened.
            for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do
              self:F( { ScoringZone = ScoreZoneData } )
              local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE
              local Score = ScoreZoneData.Score
              if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then
                Player.Score = Player.Score + Score
                TargetDestroy.Score = TargetDestroy.Score + Score
                MESSAGE:NewType( self.DisplayMessagePrefix .. "Target destroyed in zone '" .. ScoreZone:GetName() .. "'." ..
                                 "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty,
                                 MESSAGE.Type.Information )
                       :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() )
                       :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() )

                self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
                Destroyed = true
              end
            end

          end
        else
          -- Check if there are Zones where the destruction happened.
          for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do
            self:F( { ScoringZone = ScoreZoneData } )
            local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE
            local Score = ScoreZoneData.Score
            if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then
              Player.Score = Player.Score + Score
              TargetDestroy.Score = TargetDestroy.Score + Score
              MESSAGE:NewType( self.DisplayMessagePrefix .. "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." ..
                               "Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty,
                               MESSAGE.Type.Information )
                     :ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() )
                     :ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() )

              self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType )
              Destroyed = true
            end
          end
        end

        -- Delete now the hit cache if the target was destroyed.
        -- Otherwise points will be granted every time a target gets killed by the players that hit that target.
        -- This is only relevant for player to player destroys.
        if Destroyed then
          Player.Hit[TargetCategory][TargetUnitName].TimeStamp = 0
        end
      end
    end
  end
end

--- Produce detailed report of player hit scores.
-- @param #SCORING self
-- @param #string PlayerName The name of the player.
-- @return #string The report.
function SCORING:ReportDetailedPlayerHits( PlayerName )

  local ScoreMessage = ""
  local PlayerScore = 0
  local PlayerPenalty = 0

  local PlayerData = self.Players[PlayerName]
  if PlayerData then -- This should normally not happen, but i'll test it anyway.
    self:T( "Score Player: " .. PlayerName )

    -- Some variables
    local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition]
    local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory]
    local InitUnitType = PlayerData.UnitType
    local InitUnitName = PlayerData.UnitName

    local ScoreMessageHits = ""
    for CategoryID, CategoryName in pairs( _SCORINGCategory ) do
      self:T( CategoryName )
      if PlayerData.Hit[CategoryID] then
        self:T( "Hit scores exist for player " .. PlayerName )
        local Score = 0
        local ScoreHit = 0
        local Penalty = 0
        local PenaltyHit = 0
        for UnitName, UnitData in pairs( PlayerData.Hit[CategoryID] ) do
          Score = Score + UnitData.Score
          ScoreHit = ScoreHit + UnitData.ScoreHit
          Penalty = Penalty + UnitData.Penalty
          PenaltyHit = UnitData.PenaltyHit
        end
        local ScoreMessageHit = string.format( "%s: %d  ", CategoryName, Score - Penalty )
        self:T( ScoreMessageHit )
        ScoreMessageHits = ScoreMessageHits .. ScoreMessageHit
        PlayerScore = PlayerScore + Score
        PlayerPenalty = PlayerPenalty + Penalty
      else
        -- ScoreMessageHits = ScoreMessageHits .. string.format( "%s:%d  ", string.format(CategoryName, 1, 1), 0 )
      end
    end
    if ScoreMessageHits ~= "" then
      ScoreMessage = "Hits: " .. ScoreMessageHits
    end
  end

  return ScoreMessage, PlayerScore, PlayerPenalty
end

--- Produce detailed report of player destroy scores.
-- @param #SCORING self
-- @param #string PlayerName The name of the player.
-- @return #string The report.
function SCORING:ReportDetailedPlayerDestroys( PlayerName )

  local ScoreMessage = ""
  local PlayerScore = 0
  local PlayerPenalty = 0

  local PlayerData = self.Players[PlayerName]
  if PlayerData then -- This should normally not happen, but i'll test it anyway.
    self:T( "Score Player: " .. PlayerName )

    -- Some variables
    local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition]
    local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory]
    local InitUnitType = PlayerData.UnitType
    local InitUnitName = PlayerData.UnitName

    local ScoreMessageDestroys = ""
    for CategoryID, CategoryName in pairs( _SCORINGCategory ) do
      if PlayerData.Destroy[CategoryID] then
        self:T( "Destroy scores exist for player " .. PlayerName )
        local Score = 0
        local ScoreDestroy = 0
        local Penalty = 0
        local PenaltyDestroy = 0

        for UnitName, UnitData in pairs( PlayerData.Destroy[CategoryID] ) do
          self:F( { UnitData = UnitData } )
          if UnitData ~= {} then
            Score = Score + UnitData.Score
            ScoreDestroy = ScoreDestroy + UnitData.ScoreDestroy
            Penalty = Penalty + UnitData.Penalty
            PenaltyDestroy = PenaltyDestroy + UnitData.PenaltyDestroy
          end
        end

        local ScoreMessageDestroy = string.format( "  %s: %d  ", CategoryName, Score - Penalty )
        self:T( ScoreMessageDestroy )
        ScoreMessageDestroys = ScoreMessageDestroys .. ScoreMessageDestroy

        PlayerScore = PlayerScore + Score
        PlayerPenalty = PlayerPenalty + Penalty
      else
        -- ScoreMessageDestroys = ScoreMessageDestroys .. string.format( "%s:%d  ", string.format(CategoryName, 1, 1), 0 )
      end
    end
    if ScoreMessageDestroys ~= "" then
      ScoreMessage = "Destroys: " .. ScoreMessageDestroys
    end
  end

  return ScoreMessage, PlayerScore, PlayerPenalty
end

--- Produce detailed report of player penalty scores because of changing the coalition.
-- @param #SCORING self
-- @param #string PlayerName The name of the player.
-- @return #string The report.
function SCORING:ReportDetailedPlayerCoalitionChanges( PlayerName )

  local ScoreMessage = ""
  local PlayerScore = 0
  local PlayerPenalty = 0

  local PlayerData = self.Players[PlayerName]
  if PlayerData then -- This should normally not happen, but i'll test it anyway.
    self:T( "Score Player: " .. PlayerName )

    -- Some variables
    local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition]
    local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory]
    local InitUnitType = PlayerData.UnitType
    local InitUnitName = PlayerData.UnitName

    local ScoreMessageCoalitionChangePenalties = ""
    if PlayerData.PenaltyCoalition ~= 0 then
      ScoreMessageCoalitionChangePenalties = ScoreMessageCoalitionChangePenalties .. string.format( " -%d (%d changed)", PlayerData.Penalty, PlayerData.PenaltyCoalition )
      PlayerPenalty = PlayerPenalty + PlayerData.Penalty
    end
    if ScoreMessageCoalitionChangePenalties ~= "" then
      ScoreMessage = ScoreMessage .. "Coalition Penalties: " .. ScoreMessageCoalitionChangePenalties
    end
  end

  return ScoreMessage, PlayerScore, PlayerPenalty
end

--- Produce detailed report of player goal scores.
-- @param #SCORING self
-- @param #string PlayerName The name of the player.
-- @return #string The report.
function SCORING:ReportDetailedPlayerGoals( PlayerName )

  local ScoreMessage = ""
  local PlayerScore = 0
  local PlayerPenalty = 0

  local PlayerData = self.Players[PlayerName]
  if PlayerData then -- This should normally not happen, but i'll test it anyway.
    self:T( "Score Player: " .. PlayerName )

    -- Some variables
    local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition]
    local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory]
    local InitUnitType = PlayerData.UnitType
    local InitUnitName = PlayerData.UnitName

    local ScoreMessageGoal = ""
    local ScoreGoal = 0
    local ScoreTask = 0
    for GoalName, GoalData in pairs( PlayerData.Goals ) do
      ScoreGoal = ScoreGoal + GoalData.Score
      ScoreMessageGoal = ScoreMessageGoal .. "'" .. GoalName .. "':" .. GoalData.Score .. "; "
    end
    PlayerScore = PlayerScore + ScoreGoal

    if ScoreMessageGoal ~= "" then
      ScoreMessage = "Goals: " .. ScoreMessageGoal
    end
  end

  return ScoreMessage, PlayerScore, PlayerPenalty
end

--- Produce detailed report of player penalty scores because of changing the coalition.
-- @param #SCORING self
-- @param #string PlayerName The name of the player.
-- @return #string The report.
function SCORING:ReportDetailedPlayerMissions( PlayerName )

  local ScoreMessage = ""
  local PlayerScore = 0
  local PlayerPenalty = 0

  local PlayerData = self.Players[PlayerName]
  if PlayerData then -- This should normally not happen, but i'll test it anyway.
    self:T( "Score Player: " .. PlayerName )

    -- Some variables
    local InitUnitCoalition = _SCORINGCoalition[PlayerData.UnitCoalition]
    local InitUnitCategory = _SCORINGCategory[PlayerData.UnitCategory]
    local InitUnitType = PlayerData.UnitType
    local InitUnitName = PlayerData.UnitName

    local ScoreMessageMission = ""
    local ScoreMission = 0
    local ScoreTask = 0
    for MissionName, MissionData in pairs( PlayerData.Mission ) do
      ScoreMission = ScoreMission + MissionData.ScoreMission
      ScoreTask = ScoreTask + MissionData.ScoreTask
      ScoreMessageMission = ScoreMessageMission .. "'" .. MissionName .. "'; "
    end
    PlayerScore = PlayerScore + ScoreMission + ScoreTask

    if ScoreMessageMission ~= "" then
      ScoreMessage = "Tasks: " .. ScoreTask .. " Mission: " .. ScoreMission .. " ( " .. ScoreMessageMission .. ")"
    end
  end

  return ScoreMessage, PlayerScore, PlayerPenalty
end

--- Report Group Score Summary
-- @param #SCORING self
-- @param Wrapper.Group#GROUP PlayerGroup The player group.
function SCORING:ReportScoreGroupSummary( PlayerGroup )

  local PlayerMessage = ""

  self:T( "Report Score Group Summary" )

  local PlayerUnits = PlayerGroup:GetUnits()
  for UnitID, PlayerUnit in pairs( PlayerUnits ) do
    local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT
    local PlayerName = PlayerUnit:GetPlayerName()

    if PlayerName then

      local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName )
      ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits
      self:F( { ReportHits, ScoreHits, PenaltyHits } )

      local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName )
      ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys
      self:F( { ReportDestroys, ScoreDestroys, PenaltyDestroys } )

      local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName )
      ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges
      self:F( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } )

      local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName )
      ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals
      self:F( { ReportGoals, ScoreGoals, PenaltyGoals } )

      local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName )
      ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions
      self:F( { ReportMissions, ScoreMissions, PenaltyMissions } )

      local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions
      local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions

      PlayerMessage = string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )",
                                     PlayerName,
                                     PlayerScore - PlayerPenalty,
                                     PlayerScore,
                                     PlayerPenalty
                                    )
      MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Detailed ):ToGroup( PlayerGroup )
    end
  end

end

--- Report Group Score Detailed
-- @param #SCORING self
-- @param Wrapper.Group#GROUP PlayerGroup The player group.
function SCORING:ReportScoreGroupDetailed( PlayerGroup )

  local PlayerMessage = ""

  self:T( "Report Score Group Detailed" )

  local PlayerUnits = PlayerGroup:GetUnits()
  for UnitID, PlayerUnit in pairs( PlayerUnits ) do
    local PlayerUnit = PlayerUnit -- Wrapper.Unit#UNIT
    local PlayerName = PlayerUnit:GetPlayerName()

    if PlayerName then

      local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName )
      ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits
      self:F( { ReportHits, ScoreHits, PenaltyHits } )

      local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName )
      ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys
      self:F( { ReportDestroys, ScoreDestroys, PenaltyDestroys } )

      local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName )
      ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges
      self:F( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } )

      local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName )
      ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals
      self:F( { ReportGoals, ScoreGoals, PenaltyGoals } )

      local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName )
      ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions
      self:F( { ReportMissions, ScoreMissions, PenaltyMissions } )

      local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions
      local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions
  
      PlayerMessage = 
        string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )%s%s%s%s%s", 
                       PlayerName, 
                       PlayerScore - PlayerPenalty, 
                       PlayerScore, 
                       PlayerPenalty, 
                       ReportHits,
                       ReportDestroys,
                       ReportCoalitionChanges,
                       ReportGoals,
                       ReportMissions
                     )
      MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Detailed ):ToGroup( PlayerGroup )
    end
  end

end

--- Report all players score
-- @param #SCORING self
-- @param Wrapper.Group#GROUP PlayerGroup The player group.
function SCORING:ReportScoreAllSummary( PlayerGroup )

  local PlayerMessage = ""

  self:T( { "Summary Score Report of All Players", Players = self.Players } )

  for PlayerName, PlayerData in pairs( self.Players ) do

    self:T( { PlayerName = PlayerName, PlayerGroup = PlayerGroup } )

    if PlayerName then

      local ReportHits, ScoreHits, PenaltyHits = self:ReportDetailedPlayerHits( PlayerName )
      ReportHits = ReportHits ~= "" and "\n- " .. ReportHits or ReportHits
      self:F( { ReportHits, ScoreHits, PenaltyHits } )

      local ReportDestroys, ScoreDestroys, PenaltyDestroys = self:ReportDetailedPlayerDestroys( PlayerName )
      ReportDestroys = ReportDestroys ~= "" and "\n- " .. ReportDestroys or ReportDestroys
      self:F( { ReportDestroys, ScoreDestroys, PenaltyDestroys } )

      local ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges = self:ReportDetailedPlayerCoalitionChanges( PlayerName )
      ReportCoalitionChanges = ReportCoalitionChanges ~= "" and "\n- " .. ReportCoalitionChanges or ReportCoalitionChanges
      self:F( { ReportCoalitionChanges, ScoreCoalitionChanges, PenaltyCoalitionChanges } )

      local ReportGoals, ScoreGoals, PenaltyGoals = self:ReportDetailedPlayerGoals( PlayerName )
      ReportGoals = ReportGoals ~= "" and "\n- " .. ReportGoals or ReportGoals
      self:F( { ReportGoals, ScoreGoals, PenaltyGoals } )

      local ReportMissions, ScoreMissions, PenaltyMissions = self:ReportDetailedPlayerMissions( PlayerName )
      ReportMissions = ReportMissions ~= "" and "\n- " .. ReportMissions or ReportMissions
      self:F( { ReportMissions, ScoreMissions, PenaltyMissions } )

      local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions
      local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions
  
      PlayerMessage = 
        string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )", 
                       PlayerName, 
                       PlayerScore - PlayerPenalty, 
                       PlayerScore, 
                       PlayerPenalty 
                     )
      MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Overview ):ToGroup( PlayerGroup )
    end
  end

end

function SCORING:SecondsToClock( sSeconds )
  local nSeconds = sSeconds
  if nSeconds == 0 then
    -- return nil;
    return "00:00:00";
  else
    local nHours = string.format( "%02.f", math.floor( nSeconds / 3600 ) );
    local nMins = string.format( "%02.f", math.floor( nSeconds / 60 - (nHours * 60) ) );
    local nSecs = string.format( "%02.f", math.floor( nSeconds - nHours * 3600 - nMins * 60 ) );
    return nHours .. ":" .. nMins .. ":" .. nSecs
  end
end

--- Opens a score CSV file to log the scores.
-- @param #SCORING self
-- @param #string ScoringCSV
-- @return #SCORING self
-- @usage
-- -- Open a new CSV file to log the scores of the game Gori Valley. Let the name of the CSV file begin with "Player Scores".
-- ScoringObject = SCORING:New( "Gori Valley" )
-- ScoringObject:OpenCSV( "Player Scores" )
function SCORING:OpenCSV( ScoringCSV )
  self:F( ScoringCSV )

  if lfs and io and os and self.AutoSave == true then
    if ScoringCSV then
      self.ScoringCSV = ScoringCSV
      local path = self.AutoSavePath or lfs.writedir() .. [[Logs\]]
      local fdir = path .. self.ScoringCSV .. " " .. os.date( "%Y-%m-%d %H-%M-%S" ) .. ".csv"

      self.CSVFile, self.err = io.open( fdir, "w+" )
      if not self.CSVFile then
        error( "Error: Cannot open CSV file in " .. lfs.writedir() )
      end

      self.CSVFile:write( '"GameName","RunTime","Time","PlayerName","TargetPlayerName","ScoreType","PlayerUnitCoalition","PlayerUnitCategory","PlayerUnitType","PlayerUnitName","TargetUnitCoalition","TargetUnitCategory","TargetUnitType","TargetUnitName","Times","Score"\n' )

      self.RunTime = os.date( "%y-%m-%d_%H-%M-%S" )
    else
      error( "A string containing the CSV file name must be given." )
    end
  else
    self:F( "The MissionScripting.lua file has not been changed to allow lfs, io and os modules to be used..." )
  end
  return self
end

--- Registers a score for a player.
-- @param #SCORING self
-- @param #string PlayerName The name of the player.
-- @param #string TargetPlayerName The name of the target player.
-- @param #string ScoreType The type of the score.
-- @param #string ScoreTimes The amount of scores achieved.
-- @param #string ScoreAmount The score given.
-- @param #string PlayerUnitName The unit name of the player.
-- @param #string PlayerUnitCoalition The coalition of the player unit.
-- @param #string PlayerUnitCategory The category of the player unit.
-- @param #string PlayerUnitType The type of the player unit.
-- @param #string TargetUnitName The name of the target unit.
-- @param #string TargetUnitCoalition The coalition of the target unit.
-- @param #string TargetUnitCategory The category of the target unit.
-- @param #string TargetUnitType The type of the target unit.
-- @return #SCORING self
function SCORING:ScoreCSV( PlayerName, TargetPlayerName, ScoreType, ScoreTimes, ScoreAmount, PlayerUnitName, PlayerUnitCoalition, PlayerUnitCategory, PlayerUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
  -- write statistic information to file
  local ScoreTime = self:SecondsToClock( timer.getTime() )
  PlayerName = PlayerName:gsub( '"', '_' )

  TargetPlayerName = TargetPlayerName or ""
  TargetPlayerName = TargetPlayerName:gsub( '"', '_' )

  if PlayerUnitName and PlayerUnitName ~= '' then
    local PlayerUnit = Unit.getByName( PlayerUnitName )

    if PlayerUnit then
      if not PlayerUnitCategory then
        -- PlayerUnitCategory = SCORINGCategory[PlayerUnit:getCategory()]
        PlayerUnitCategory = _SCORINGCategory[PlayerUnit:getDesc().category]
      end

      if not PlayerUnitCoalition then
        PlayerUnitCoalition = _SCORINGCoalition[PlayerUnit:getCoalition()]
      end

      if not PlayerUnitType then
        PlayerUnitType = PlayerUnit:getTypeName()
      end
    else
      PlayerUnitName = ''
      PlayerUnitCategory = ''
      PlayerUnitCoalition = ''
      PlayerUnitType = ''
    end
  else
    PlayerUnitName = ''
    PlayerUnitCategory = ''
    PlayerUnitCoalition = ''
    PlayerUnitType = ''
  end

  TargetUnitCoalition = TargetUnitCoalition or ""
  TargetUnitCategory = TargetUnitCategory or ""
  TargetUnitType = TargetUnitType or ""
  TargetUnitName = TargetUnitName or ""

  if lfs and io and os and self.AutoSave == true and self.CSVFile ~= nil then
    self.CSVFile:write(
      '"' .. self.GameName        .. '"' .. ',' ..
      '"' .. self.RunTime         .. '"' .. ',' ..
      ''  .. ScoreTime            .. ''  .. ',' ..
      '"' .. PlayerName           .. '"' .. ',' ..
      '"' .. TargetPlayerName     .. '"' .. ',' ..
      '"' .. ScoreType            .. '"' .. ',' ..
      '"' .. PlayerUnitCoalition  .. '"' .. ',' ..
      '"' .. PlayerUnitCategory   .. '"' .. ',' ..
      '"' .. PlayerUnitType       .. '"' .. ',' ..
      '"' .. PlayerUnitName       .. '"' .. ',' ..
      '"' .. TargetUnitCoalition  .. '"' .. ',' ..
      '"' .. TargetUnitCategory   .. '"' .. ',' ..
      '"' .. TargetUnitType       .. '"' .. ',' ..
      '"' .. TargetUnitName       .. '"' .. ',' ..
      ''  .. ScoreTimes           .. ''  .. ',' ..
      ''  .. ScoreAmount
    )

    self.CSVFile:write( "\n" )
  end
end

--- Close CSV file
-- @param #SCORING self
-- @return #SCORING self
function SCORING:CloseCSV()
  if lfs and io and os and self.AutoSave then
    self.CSVFile:close()
  end
end

--- Registers a score for a player.
-- @param #SCORING self
-- @param #boolean OnOff Switch saving to CSV on = true or off = false
-- @return #SCORING self
function SCORING:SwitchAutoSave(OnOff)
  self.AutoSave = OnOff
  return self
end

--- Handles the event when one player kill another player
-- @param #SCORING self
-- @param #string PlayerName The attacking player
-- @param #string TargetPlayerName The name of the killed player
-- @param #boolean IsTeamKill true if this kill was a team kill
-- @param #number TargetThreatLevel Threat level of the target
-- @param #number PlayerThreatLevel Threat level of the player
-- @param #number Score The score based on both threat levels
function SCORING:OnKillPvP(PlayerName, TargetPlayerName, IsTeamKill, TargetThreatLevel, PlayerThreatLevel, Score)
  
end
--- Handles the event when one player kill another player
-- @param #SCORING self
-- @param #string PlayerName The attacking player
-- @param #string TargetUnitName the name of the killed unit
-- @param #boolean IsTeamKill true if this kill was a team kill
-- @param #number TargetThreatLevel Threat level of the target
-- @param #number PlayerThreatLevel Threat level of the player
-- @param #number Score The score based on both threat levels
function SCORING:OnKillPvE(PlayerName, TargetUnitName, IsTeamKill, TargetThreatLevel, PlayerThreatLevel, Score)
  
end
--- **Functional** - Keep airbases clean of crashing or colliding airplanes, and kill missiles when being fired at airbases.
--
-- ===
--
-- ## Features:
--
--
--  * Try to keep the airbase clean and operational.
--  * Prevent airplanes from crashing.
--  * Clean up obstructing airplanes from the runway that are standing still for a period of time.
--  * Prevent airplanes firing missiles within the airbase zone.
--
-- ===
--
-- ## Missions:
--
-- [CLA - CleanUp Airbase](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Functional/CleanUp)
--
-- ===
--
-- Specific airbases need to be provided that need to be guarded. Each airbase registered, will be guarded within a zone of 8 km around the airbase.
-- Any unit that fires a missile, or shoots within the zone of an airbase, will be monitored by CLEANUP_AIRBASE.
-- Within the 8km zone, units cannot fire any missile, which prevents the airbase runway to receive missile or bomb hits.
-- Any airborne or ground unit that is on the runway below 30 meters (default value) will be automatically removed if it is damaged.
--
-- This is not a full 100% secure implementation. It is still possible that CLEANUP_AIRBASE cannot prevent (in-time) to keep the airbase clean.
-- The following situations may happen that will still stop the runway of an airbase:
--
--   * A damaged unit is not removed on time when above the runway, and crashes on the runway.
--   * A bomb or missile is still able to dropped on the runway.
--   * Units collide on the airbase, and could not be removed on time.
--
-- When a unit is within the airbase zone and needs to be monitored,
-- its status will be checked every 0.25 seconds! This is required to ensure that the airbase is kept clean.
-- But as a result, there is more CPU overload.
--
-- So as an advise, I suggest you use the CLEANUP_AIRBASE class with care:
--
--   * Only monitor airbases that really need to be monitored!
--   * Try not to monitor airbases that are likely to be invaded by enemy troops.
--     For these airbases, there is little use to keep them clean, as they will be invaded anyway...
--
-- By following the above guidelines, you can add airbase cleanup with acceptable CPU overhead.
--
-- ===
--
-- ### Author: **FlightControl**
-- ### Contributions:
--
-- ===
--
-- @module Functional.CleanUp
-- @image CleanUp_Airbases.JPG

---
-- @type CLEANUP_AIRBASE.__ Methods which are not intended for mission designers, but which are used interally by the moose designer :-)
-- @field #map<#string,Wrapper.Airbase#AIRBASE> Airbases Map of Airbases.
-- @extends Core.Base#BASE

---
-- @type CLEANUP_AIRBASE
-- @extends #CLEANUP_AIRBASE.__

--- Keeps airbases clean, and tries to guarantee continuous airbase operations, even under combat.
--
-- # 1. CLEANUP_AIRBASE Constructor
--
-- Creates the main object which is preventing the airbase to get polluted with debris on the runway, which halts the airbase.
--
--      -- Clean these Zones.
--      CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi } )
--
--      -- or
--      CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi )
--      CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi )
--
-- # 2. Add or Remove airbases
--
-- The method @{#CLEANUP_AIRBASE.AddAirbase}() to add an airbase to the cleanup validation process.
-- The method @{#CLEANUP_AIRBASE.RemoveAirbase}() removes an airbase from the cleanup validation process.
--
-- # 3. Clean missiles and bombs within the airbase zone.
--
-- When missiles or bombs hit the runway, the airbase operations stop.
-- Use the method @{#CLEANUP_AIRBASE.SetCleanMissiles}() to control the cleaning of missiles, which will prevent airbases to stop.
-- Note that this method will not allow anymore airbases to be attacked, so there is a trade-off here to do.
--
-- @field #CLEANUP_AIRBASE
CLEANUP_AIRBASE = {
	ClassName = "CLEANUP_AIRBASE",
	TimeInterval = 0.2,
	CleanUpList = {},
}

-- @field #CLEANUP_AIRBASE.__
CLEANUP_AIRBASE.__ = {}

-- @field #CLEANUP_AIRBASE.__.Airbases
CLEANUP_AIRBASE.__.Airbases = {}

--- Creates the main object which is handling the cleaning of the debris within the given Zone Names.
-- @param #CLEANUP_AIRBASE self
-- @param #list<#string> AirbaseNames Is a table of airbase names where the debris should be cleaned. Also a single string can be passed with one airbase name.
-- @return #CLEANUP_AIRBASE
-- @usage
--  -- Clean these Zones.
-- CleanUpAirports = CLEANUP_AIRBASE:New( { AIRBASE.Caucasus.Tbilisi, AIRBASE.Caucasus.Kutaisi )
-- or
-- CleanUpTbilisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Tbilisi )
-- CleanUpKutaisi = CLEANUP_AIRBASE:New( AIRBASE.Caucasus.Kutaisi )
function CLEANUP_AIRBASE:New( AirbaseNames )

  local self = BASE:Inherit( self, BASE:New() ) -- #CLEANUP_AIRBASE
	self:F( { AirbaseNames } )

	if type( AirbaseNames ) == 'table' then
    for AirbaseID, AirbaseName in pairs( AirbaseNames ) do
      self:AddAirbase( AirbaseName )
    end
	else
    local AirbaseName = AirbaseNames
    self:AddAirbase( AirbaseName )
	end

	self:HandleEvent( EVENTS.Birth, self.__.OnEventBirth )

  self.__.CleanUpScheduler = SCHEDULER:New( self, self.__.CleanUpSchedule, {}, 1, self.TimeInterval )

  self:HandleEvent( EVENTS.EngineShutdown , self.__.EventAddForCleanUp )
  self:HandleEvent( EVENTS.EngineStartup, self.__.EventAddForCleanUp )
  self:HandleEvent( EVENTS.Hit, self.__.EventAddForCleanUp )
  self:HandleEvent( EVENTS.PilotDead, self.__.OnEventCrash )
  self:HandleEvent( EVENTS.Dead, self.__.OnEventCrash )
  self:HandleEvent( EVENTS.Crash, self.__.OnEventCrash )

  for UnitName, Unit in pairs( _DATABASE.UNITS ) do
    local Unit = Unit -- Wrapper.Unit#UNIT
    if Unit:IsAlive() ~= nil then
      if self:IsInAirbase( Unit:GetVec2() ) then
        self:F( { UnitName = UnitName } )
        self.CleanUpList[UnitName] = {}
        self.CleanUpList[UnitName].CleanUpUnit = Unit
        self.CleanUpList[UnitName].CleanUpGroup = Unit:GetGroup()
        self.CleanUpList[UnitName].CleanUpGroupName = Unit:GetGroup():GetName()
        self.CleanUpList[UnitName].CleanUpUnitName = Unit:GetName()
      end
    end
  end

	return self
end

--- Adds an airbase to the airbase validation list.
-- @param #CLEANUP_AIRBASE self
-- @param #string AirbaseName
-- @return #CLEANUP_AIRBASE
function CLEANUP_AIRBASE:AddAirbase( AirbaseName )
  self.__.Airbases[AirbaseName] = AIRBASE:FindByName( AirbaseName )
  self:F({"Airbase:", AirbaseName, self.__.Airbases[AirbaseName]:GetDesc()})

  return self
end

--- Removes an airbase from the airbase validation list.
-- @param #CLEANUP_AIRBASE self
-- @param #string AirbaseName
-- @return #CLEANUP_AIRBASE
function CLEANUP_AIRBASE:RemoveAirbase( AirbaseName )
  self.__.Airbases[AirbaseName] = nil
  return self
end

--- Enables or disables the cleaning of missiles within the airbase zones.
-- Airbase operations stop when a missile or bomb is dropped at a runway.
-- Note that when this method is used, the airbase operations won't stop if
-- the missile or bomb was cleaned within the airbase zone, which is 8km from the center of the airbase.
-- However, there is a trade-off to make. Attacks on airbases won't be possible anymore if this method is used.
-- Note, one can also use the method @{#CLEANUP_AIRBASE.RemoveAirbase}() to remove the airbase from the control process as a whole,
-- when an enemy unit is near. That is also an option...
-- @param #CLEANUP_AIRBASE self
-- @param #string CleanMissiles (Default=true) If true, missiles fired are immediately destroyed. If false missiles are not controlled.
-- @return #CLEANUP_AIRBASE
function CLEANUP_AIRBASE:SetCleanMissiles( CleanMissiles )

  if CleanMissiles then
    self:HandleEvent( EVENTS.Shot, self.__.OnEventShot )
  else
    self:UnHandleEvent( EVENTS.Shot )
  end
end

function CLEANUP_AIRBASE.__:IsInAirbase( Vec2 )

  local InAirbase = false
  for AirbaseName, Airbase in pairs( self.__.Airbases ) do
    local Airbase = Airbase -- Wrapper.Airbase#AIRBASE
    if Airbase:GetZone():IsVec2InZone( Vec2 ) then
      InAirbase = true
      break;
    end
  end

  return InAirbase
end



--- Destroys a @{Wrapper.Unit} from the simulator, but checks first if it is still existing!
-- @param #CLEANUP_AIRBASE self
-- @param Wrapper.Unit#UNIT CleanUpUnit The object to be destroyed.
function CLEANUP_AIRBASE.__:DestroyUnit( CleanUpUnit )
	self:F( { CleanUpUnit } )

	if CleanUpUnit then
	  local CleanUpUnitName = CleanUpUnit:GetName()
		local CleanUpGroup = CleanUpUnit:GetGroup()
    -- TODO DCS BUG - Client bug in 1.5.3
		if CleanUpGroup:IsAlive() then
			local CleanUpGroupUnits = CleanUpGroup:GetUnits()
			if #CleanUpGroupUnits == 1 then
				local CleanUpGroupName = CleanUpGroup:GetName()
				CleanUpGroup:Destroy()
			else
				CleanUpUnit:Destroy()
			end
			self.CleanUpList[CleanUpUnitName] = nil
		end
	end
end



--- Destroys a missile from the simulator, but checks first if it is still existing!
-- @param #CLEANUP_AIRBASE self
-- @param DCS#Weapon MissileObject
function CLEANUP_AIRBASE.__:DestroyMissile( MissileObject )
	self:F( { MissileObject } )

	if MissileObject and MissileObject:isExist() then
		MissileObject:destroy()
		self:T( "MissileObject Destroyed")
	end
end

---
-- @param #CLEANUP_AIRBASE self
-- @param Core.Event#EVENTDATA EventData
function CLEANUP_AIRBASE.__:OnEventBirth( EventData )
  self:F( { EventData } )
  
  if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() ~= nil then
    if self:IsInAirbase( EventData.IniUnit:GetVec2() ) then
      self.CleanUpList[EventData.IniDCSUnitName] = {}
      self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnit = EventData.IniUnit
      self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroup = EventData.IniGroup
      self.CleanUpList[EventData.IniDCSUnitName].CleanUpGroupName = EventData.IniDCSGroupName
      self.CleanUpList[EventData.IniDCSUnitName].CleanUpUnitName = EventData.IniDCSUnitName
    end
  end

end


--- Detects if a crash event occurs.
-- Crashed units go into a CleanUpList for removal.
-- @param #CLEANUP_AIRBASE self
-- @param Core.Event#EVENTDATA Event
function CLEANUP_AIRBASE.__:OnEventCrash( Event )
	self:F( { Event } )

  --TODO: DCS BUG - This stuff is not working due to a DCS bug. Burning units cannot be destroyed.
	-- self:T("before getGroup")
	-- local _grp = Unit.getGroup(event.initiator)-- Identify the group that fired
	-- self:T("after getGroup")
	-- _grp:destroy()
	-- self:T("after deactivateGroup")
	-- event.initiator:destroy()

  if Event.IniDCSUnitName and Event.IniCategory == Object.Category.UNIT then
    self.CleanUpList[Event.IniDCSUnitName] = {}
    self.CleanUpList[Event.IniDCSUnitName].CleanUpUnit = Event.IniUnit
    self.CleanUpList[Event.IniDCSUnitName].CleanUpGroup = Event.IniGroup
    self.CleanUpList[Event.IniDCSUnitName].CleanUpGroupName = Event.IniDCSGroupName
    self.CleanUpList[Event.IniDCSUnitName].CleanUpUnitName = Event.IniDCSUnitName
  end

end

--- Detects if a unit shoots a missile.
-- If this occurs within one of the airbases, then the weapon used must be destroyed.
-- @param #CLEANUP_AIRBASE self
-- @param Core.Event#EVENTDATA Event
function CLEANUP_AIRBASE.__:OnEventShot( Event )
	self:F( { Event } )

	-- Test if the missile was fired within one of the CLEANUP_AIRBASE.AirbaseNames.
	if self:IsInAirbase( Event.IniUnit:GetVec2() ) then
		-- Okay, the missile was fired within the CLEANUP_AIRBASE.AirbaseNames, destroy the fired weapon.
    self:DestroyMissile( Event.Weapon )
	end
end

--- Detects if the Unit has an S_EVENT_HIT within the given AirbaseNames. If this is the case, destroy the unit.
-- @param #CLEANUP_AIRBASE self
-- @param Core.Event#EVENTDATA Event
function CLEANUP_AIRBASE.__:OnEventHit( Event )
	self:F( { Event } )

	if Event.IniUnit then
		if self:IsInAirbase( Event.IniUnit:GetVec2() ) then
			self:T( { "Life: ", Event.IniDCSUnitName, ' = ',  Event.IniUnit:GetLife(), "/", Event.IniUnit:GetLife0() } )
			if Event.IniUnit:GetLife() < Event.IniUnit:GetLife0() then
				self:T( "CleanUp: Destroy: " .. Event.IniDCSUnitName )
        CLEANUP_AIRBASE.__:DestroyUnit( Event.IniUnit )
			end
		end
	end

	if Event.TgtUnit then
		if self:IsInAirbase( Event.TgtUnit:GetVec2() ) then
			self:T( { "Life: ", Event.TgtDCSUnitName, ' = ', Event.TgtUnit:GetLife(), "/", Event.TgtUnit:GetLife0() } )
			if Event.TgtUnit:GetLife() < Event.TgtUnit:GetLife0() then
				self:T( "CleanUp: Destroy: " .. Event.TgtDCSUnitName )
        CLEANUP_AIRBASE.__:DestroyUnit( Event.TgtUnit )
			end
		end
	end
end

--- Add the @{DCS#Unit} to the CleanUpList for CleanUp.
-- @param #CLEANUP_AIRBASE self
-- @param DCS#UNIT CleanUpUnit
-- @oaram #string CleanUpUnitName
function CLEANUP_AIRBASE.__:AddForCleanUp( CleanUpUnit, CleanUpUnitName )
	self:F( { CleanUpUnit, CleanUpUnitName } )

	self.CleanUpList[CleanUpUnitName] = {}
	self.CleanUpList[CleanUpUnitName].CleanUpUnit = CleanUpUnit
	self.CleanUpList[CleanUpUnitName].CleanUpUnitName = CleanUpUnitName

	local CleanUpGroup = CleanUpUnit:GetGroup()

	self.CleanUpList[CleanUpUnitName].CleanUpGroup = CleanUpGroup
	self.CleanUpList[CleanUpUnitName].CleanUpGroupName = CleanUpGroup:GetName()
	self.CleanUpList[CleanUpUnitName].CleanUpTime = timer.getTime()
	self.CleanUpList[CleanUpUnitName].CleanUpMoved = false

	self:T( { "CleanUp: Add to CleanUpList: ", CleanUpGroup:GetName(), CleanUpUnitName } )

end

--- Detects if the Unit has an S_EVENT_ENGINE_SHUTDOWN or an S_EVENT_HIT within the given AirbaseNames. If this is the case, add the Group to the CLEANUP_AIRBASE List.
-- @param #CLEANUP_AIRBASE.__ self
-- @param Core.Event#EVENTDATA Event
function CLEANUP_AIRBASE.__:EventAddForCleanUp( Event )

  self:F({Event})


	if Event.IniDCSUnit and Event.IniUnit and Event.IniCategory == Object.Category.UNIT then
		if self.CleanUpList[Event.IniDCSUnitName] == nil then
			if self:IsInAirbase( Event.IniUnit:GetVec2() ) then
				self:AddForCleanUp( Event.IniUnit, Event.IniDCSUnitName )
			end
		end
	end

	if Event.TgtDCSUnit and Event.TgtUnit and Event.TgtCategory == Object.Category.UNIT then
		if self.CleanUpList[Event.TgtDCSUnitName] == nil then
			if self:IsInAirbase( Event.TgtUnit:GetVec2() ) then
				self:AddForCleanUp( Event.TgtUnit, Event.TgtDCSUnitName )
			end
		end
	end

end


--- At the defined time interval, CleanUp the Groups within the CleanUpList.
-- @param #CLEANUP_AIRBASE self
function CLEANUP_AIRBASE.__:CleanUpSchedule()

  local CleanUpCount = 0
	for CleanUpUnitName, CleanUpListData in pairs( self.CleanUpList ) do
	  CleanUpCount = CleanUpCount + 1

		local CleanUpUnit = CleanUpListData.CleanUpUnit -- Wrapper.Unit#UNIT
		local CleanUpGroupName = CleanUpListData.CleanUpGroupName

		if CleanUpUnit and CleanUpUnit:IsAlive() ~= nil then

		  if self:IsInAirbase( CleanUpUnit:GetVec2() ) then

  			if _DATABASE:GetStatusGroup( CleanUpGroupName ) ~= "ReSpawn" then

  				local CleanUpCoordinate = CleanUpUnit:GetCoordinate()

          self:T( { "CleanUp Scheduler", CleanUpUnitName } )
          if CleanUpUnit:GetLife() <= CleanUpUnit:GetLife0() * 0.95 then
  					if CleanUpUnit:IsAboveRunway() then
  						if CleanUpUnit:InAir() then

  							local CleanUpLandHeight = CleanUpCoordinate:GetLandHeight()
  							local CleanUpUnitHeight = CleanUpCoordinate.y - CleanUpLandHeight

  							if CleanUpUnitHeight < 100 then
  								self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because below safe height and damaged." } )
  								self:DestroyUnit( CleanUpUnit )
  							end
  						else
  							self:T( { "CleanUp Scheduler", "Destroy " .. CleanUpUnitName .. " because on runway and damaged." } )
  							self:DestroyUnit( CleanUpUnit )
  						end
  					end
  				end
  				-- Clean Units which are waiting for a very long time in the CleanUpZone.
  				if CleanUpUnit and (CleanUpUnit.GetPlayerName == nil or not CleanUpUnit:GetPlayerName()) then
  					local CleanUpUnitVelocity = CleanUpUnit:GetVelocityKMH()
  					if CleanUpUnitVelocity < 1 then
  						if CleanUpListData.CleanUpMoved then
  							if CleanUpListData.CleanUpTime + 180 <= timer.getTime() then
  								self:T( { "CleanUp Scheduler", "Destroy due to not moving anymore " .. CleanUpUnitName } )
  								self:DestroyUnit( CleanUpUnit )
  							end
  						end
  					else
  						CleanUpListData.CleanUpTime = timer.getTime()
  						CleanUpListData.CleanUpMoved = true
  					end
  				end
        else
          -- not anymore in an airbase zone, remove from cleanup list.
          self.CleanUpList[CleanUpUnitName] = nil
        end
			else
				-- Do nothing ...
				self.CleanUpList[CleanUpUnitName] = nil
			end
		else
			self:T( "CleanUp: Group " .. CleanUpUnitName .. " cannot be found in DCS RTE, removing ..." )
			self.CleanUpList[CleanUpUnitName] = nil
		end
	end
	self:T(CleanUpCount)

	return true
end
--- **Functional** - Limit the movement of simulaneous moving ground vehicles.
-- 
-- ===
--  
-- Limit the simultaneous movement of Groups within a running Mission.
-- This module is defined to improve the performance in missions, and to bring additional realism for GROUND vehicles.
-- Performance: If in a DCSRTE there are a lot of moving GROUND units, then in a multi player mission, this WILL create lag if
-- the main DCS execution core of your CPU is fully utilized. So, this class will limit the amount of simultaneous moving GROUND units
-- on defined intervals (currently every minute).
-- @module Functional.Movement
-- @image MOOSE.JPG

---
-- @type MOVEMENT
-- @extends Core.Base#BASE

---
--@field #MOVEMENT
MOVEMENT = {
	ClassName = "MOVEMENT",
}

--- Creates the main object which is handling the GROUND forces movement.
-- @param table{string,...}|string MovePrefixes is a table of the Prefixes (names) of the GROUND Groups that need to be controlled by the MOVEMENT Object.
-- @param number MoveMaximum is a number that defines the maximum amount of GROUND Units to be moving during one minute.
-- @return MOVEMENT
-- @usage
-- -- Limit the amount of simultaneous moving units on the ground to prevent lag.
-- Movement_US_Platoons = MOVEMENT:New( { 'US Tank Platoon Left', 'US Tank Platoon Middle', 'US Tank Platoon Right', 'US CH-47D Troops' }, 15 )

function MOVEMENT:New( MovePrefixes, MoveMaximum )
	local self = BASE:Inherit( self, BASE:New() ) -- #MOVEMENT
	self:F( { MovePrefixes, MoveMaximum } )

	if type( MovePrefixes ) == 'table' then
		self.MovePrefixes = MovePrefixes
	else
		self.MovePrefixes = { MovePrefixes }
	end
	self.MoveCount = 0              -- The internal counter of the amount of Moving the has happened since MoveStart.
	self.MoveMaximum = MoveMaximum  -- Contains the Maximum amount of units that are allowed to move.
	self.AliveUnits = 0             -- Contains the counter how many units are currently alive.
	self.MoveUnits = {}             -- Reflects if the Moving for this MovePrefixes is going to be scheduled or not.

	self:HandleEvent( EVENTS.Birth )

--	self:AddEvent( world.event.S_EVENT_BIRTH, self.OnBirth )
--	
--	self:EnableEvents()

	self:ScheduleStart()

	return self
end

--- Call this function to start the MOVEMENT scheduling.
function MOVEMENT:ScheduleStart()
	self:F()
  self.MoveFunction = SCHEDULER:New( self, self._Scheduler, {}, 1, 120 )
end

--- Call this function to stop the MOVEMENT scheduling.
-- @todo need to implement it ... Forgot.
function MOVEMENT:ScheduleStop()
	self:F()

end

--- Captures the birth events when new Units were spawned.
-- @todo This method should become obsolete. The global _DATABASE object (an instance of @{Core.Database#DATABASE}) will handle the collection administration.
-- @param #MOVEMENT self
-- @param Core.Event#EVENTDATA self
function MOVEMENT:OnEventBirth( EventData )
	self:F( { EventData } )

	if timer.getTime0() < timer.getAbsTime() then -- dont need to add units spawned in at the start of the mission if mist is loaded in init line
		if EventData.IniDCSUnit then
			self:T( "Birth object : " .. EventData.IniDCSUnitName )
			if EventData.IniDCSGroup and EventData.IniDCSGroup:isExist() then
				for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do
					if string.find( EventData.IniDCSUnitName, MovePrefix, 1, true ) then
						self.AliveUnits = self.AliveUnits + 1
						self.MoveUnits[EventData.IniDCSUnitName] = EventData.IniDCSGroupName
						self:T( self.AliveUnits )
					end
				end
			end
		end
	
		EventData.IniUnit:HandleEvent( EVENTS.DEAD, self.OnDeadOrCrash )
	end

end

--- Captures the Dead or Crash events when Units crash or are destroyed.
-- @todo This method should become obsolete. The global _DATABASE object (an instance of @{Core.Database#DATABASE}) will handle the collection administration.
function MOVEMENT:OnDeadOrCrash( Event )
	self:F( { Event } )

	if Event.IniDCSUnit then
		self:T( "Dead object : " .. Event.IniDCSUnitName )
		for MovePrefixID, MovePrefix in pairs( self.MovePrefixes ) do
			if string.find( Event.IniDCSUnitName, MovePrefix, 1, true ) then
				self.AliveUnits = self.AliveUnits - 1
				self.MoveUnits[Event.IniDCSUnitName] = nil
				self:T( self.AliveUnits )
			end
		end
	end
end

--- This function is called automatically by the MOVEMENT scheduler. A new function is scheduled when MoveScheduled is true.
function MOVEMENT:_Scheduler()
	self:F( { self.MovePrefixes, self.MoveMaximum, self.AliveUnits, self.MovementGroups } )
	
	if self.AliveUnits > 0 then
		local MoveProbability = ( self.MoveMaximum * 100 ) / self.AliveUnits
		self:T( 'Move Probability = ' .. MoveProbability )
		
		for MovementUnitName, MovementGroupName in pairs( self.MoveUnits ) do
			local MovementGroup = Group.getByName( MovementGroupName )
			if MovementGroup and MovementGroup:isExist() then
				local MoveOrStop = math.random( 1, 100 )
				self:T( 'MoveOrStop = ' .. MoveOrStop )
				if MoveOrStop <= MoveProbability then
					self:T( 'Group continues moving = ' .. MovementGroupName )
					trigger.action.groupContinueMoving( MovementGroup )
				else
					self:T( 'Group stops moving = ' .. MovementGroupName )
					trigger.action.groupStopMoving( MovementGroup )
				end
			else
				self.MoveUnits[MovementUnitName] = nil
			end
		end
	end
	return true
end
--- **Functional** - Make SAM sites evasive and execute defensive behaviour when being fired upon.
--
-- ===
--
-- ## Features:
--
--   * When SAM sites are being fired upon, the SAMs will take evasive action will reposition themselves when possible.
--   * When SAM sites are being fired upon, the SAMs will take defensive action by shutting down their radars.
--   * SEAD calculates the time it takes for a HARM to reach the target - and will attempt to minimize the shut-down time.
--   * Detection and evasion of shots has a random component based on the skill level of the SAM groups.
--
-- ===
--
-- ## Missions:
--
-- [SEV - SEAD Evasion](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Functional/Sead)
--
-- ===
--
-- ### Authors: **applevangelist**, **FlightControl**
--
-- Last Update: Dec 2024
--
-- ===
--
-- @module Functional.Sead
-- @image SEAD.JPG

---
-- @type SEAD
-- @field #string ClassName The Class Name.
-- @field #table TargetSkill Table of target skills.
-- @field #table SEADGroupPrefixes Table of SEAD prefixes.
-- @field #table SuppressedGroups Table of currently suppressed groups.
-- @field #number EngagementRange Engagement Range.
-- @field #number Padding Padding in seconds.
-- @field #function CallBack Callback function for suppression plans.
-- @field #boolean UseCallBack Switch for callback function to be used.
-- @field #boolean debug Debug switch.
-- @field #boolen WeaponTrack Track switch, if true track weapon speed for 30 secs.
-- @extends Core.Base#BASE

--- Make SAM sites execute evasive and defensive behaviour when being fired upon.
--
-- This class is very easy to use. Just setup a SEAD object by using @{#SEAD.New}() and SAMs will evade and take defensive action when being fired upon.
-- Once a HARM attack is detected, SEAD will shut down the radars of the attacked SAM site and take evasive action by moving the SAM
-- vehicles around (*if* they are driveable, that is). There's a component of randomness in detection and evasion, which is based on the
-- skill set of the SAM set (the higher the skill, the more likely). When a missile is fired from far away, the SAM will stay active for a 
-- period of time to stay defensive, before it takes evasive actions.
-- 
-- # Constructor:
--
-- Use the @{#SEAD.New}() constructor to create a new SEAD object.
--
--       SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } )
--
-- @field #SEAD
SEAD = {
  ClassName = "SEAD",
  TargetSkill = {
    Average   = { Evade = 30, DelayOn = { 40, 60 } } ,
    Good      = { Evade = 20, DelayOn = { 30, 50 } } ,
    High      = { Evade = 15, DelayOn = { 20, 40 } } ,
    Excellent = { Evade = 10, DelayOn = { 10, 30 } }
  },
  SEADGroupPrefixes = {},
  SuppressedGroups = {},
  EngagementRange = 75, --  default 75% engagement range Feature Request #1355
  Padding = 15,
  CallBack = nil,
  UseCallBack = false,
  debug = false,
  WeaponTrack = false,
}

  --- Missile enumerators
  -- @field Harms
  SEAD.Harms = {
  ["AGM_88"] = "AGM_88",
  ["AGM_122"] = "AGM_122",
  ["AGM_84"] = "AGM_84",
  ["AGM_45"] = "AGM_45",
  ["AGM_65"] = "AGM_65",
  ["ALARM"] = "ALARM",
  ["LD-10"] = "LD-10",
  ["X_58"] = "X_58",
  ["X_28"] = "X_28",
  ["X_25"] = "X_25",
  ["X_31"] = "X_31",
  ["Kh25"] = "Kh25",
  ["BGM_109"] = "BGM_109",
  ["AGM_154"] = "AGM_154",
  ["HY-2"] = "HY-2",
  ["ADM_141A"] = "ADM_141A",
  }

  --- Missile enumerators - from DCS ME and Wikipedia
  -- @field HarmData
  SEAD.HarmData = {
  -- km and mach
  ["AGM_88"] = { 150, 3},
  ["AGM_45"] = { 12, 2},
  ["AGM_65"] = { 16, 0.9},
  ["AGM_122"] = { 16.5, 2.3},
  ["AGM_84"] = { 280, 0.8},
  ["ALARM"] = { 45, 2},
  ["LD-10"] = { 60, 4},
  ["X_58"] = { 70, 4},
  ["X_28"] = { 80, 2.5},
  ["X_25"] = { 25, 0.76},
  ["X_31"] = {150, 3},
  ["Kh25"] = {25, 0.8},
  ["BGM_109"] = {460, 0.705}, --in-game ~465kn
  ["AGM_154"] = {130, 0.61},
  ["HY-2"] = {90,1},
  ["ADM_141A"] = {126,0.6},
  }

--- Creates the main object which is handling defensive actions for SA sites or moving SA vehicles.
-- When an anti radiation missile is fired (KH-58, KH-31P, KH-31A, KH-25MPU, HARM missiles), the SA will shut down their radars and will take evasive actions...
-- Chances are big that the missile will miss.
-- @param #SEAD self
-- @param #table SEADGroupPrefixes Table of #string entries or single #string, which is a table of Prefixes of the SA Groups in the DCS mission editor on which evasive actions need to be taken.
-- @param #number Padding (Optional) Extra number of seconds to add to radar switch-back-on time
-- @return #SEAD self
-- @usage
-- -- CCCP SEAD Defenses
-- -- Defends the Russian SA installations from SEAD attacks.
-- SEAD_RU_SAM_Defenses = SEAD:New( { 'RU SA-6 Kub', 'RU SA-6 Defenses', 'RU MI-26 Troops', 'RU Attack Gori' } )
function SEAD:New( SEADGroupPrefixes, Padding )

  local self = BASE:Inherit( self, FSM:New() )
  self:T( SEADGroupPrefixes )

  if type( SEADGroupPrefixes ) == 'table' then
    for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do
      self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix
    end
  else
    self.SEADGroupPrefixes[SEADGroupPrefixes] = SEADGroupPrefixes
  end

  local padding = Padding or 10
  if padding < 10 then padding = 10 end
  self.Padding = padding
  self.UseEmissionsOnOff = true
  
  self.debug = false
  
  self.CallBack = nil
  self.UseCallBack = false
    
  self:HandleEvent( EVENTS.Shot, self.HandleEventShot )
  
  -- Start State.
  self:SetStartState("Running")
  self:AddTransition("*",             "ManageEvasion",                "*")
  self:AddTransition("*",             "CalculateHitZone",             "*")
  
  self:I("*** SEAD - Started Version 0.4.9")
  return self
end

--- Update the active SEAD Set (while running)
-- @param #SEAD self
-- @param #table SEADGroupPrefixes The prefixes to add, note: can also be a single #string
-- @return #SEAD self
function SEAD:UpdateSet( SEADGroupPrefixes )

  self:T( SEADGroupPrefixes )

  if type( SEADGroupPrefixes ) == 'table' then
    for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do
      self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix
    end
  else
    self.SEADGroupPrefixes[SEADGroupPrefixes] = SEADGroupPrefixes
  end

  return self
end

--- Sets the engagement range of the SAMs. Defaults to 75% to make it more deadly. Feature Request #1355
-- @param #SEAD self
-- @param #number range Set the engagement range in percent, e.g. 55 (default 75)
-- @return #SEAD self
function SEAD:SetEngagementRange(range)
  self:T( { range } )
  range = range or 75
  if range < 0 or range > 100 then
    range = 75
  end
  self.EngagementRange = range
  self:T(string.format("*** SEAD - Engagement range set to %s",range))
  return self
end

--- Set the padding in seconds, which extends the radar off time calculated by SEAD
-- @param #SEAD self
-- @param #number Padding Extra number of seconds to add for the switch-on (default 10 seconds)
-- @return #SEAD self
function SEAD:SetPadding(Padding)
  self:T( { Padding } )
  local padding = Padding or 10
  if padding < 10 then padding = 10 end
  self.Padding = padding
  return self
end

--- Set SEAD to use emissions on/off in addition to alarm state.
-- @param #SEAD self
-- @param #boolean Switch True for on, false for off.
-- @return #SEAD self
function SEAD:SwitchEmissions(Switch)
  self:T({Switch})
  self.UseEmissionsOnOff = Switch
  return self
end

--- Set an object to call back when going evasive.
-- @param #SEAD self
-- @param #table Object The object to call. Needs to have object functions as follows:
-- `:SeadSuppressionPlanned(Group, Name, SuppressionStartTime, SuppressionEndTime)` 
-- `:SeadSuppressionStart(Group, Name)`, 
-- `:SeadSuppressionEnd(Group, Name)`, 
-- @return #SEAD self
function SEAD:AddCallBack(Object)
  self:T({Class=Object.ClassName})
  self.CallBack = Object
  self.UseCallBack = true
  return self
end

--- (Internal) Check if a known HARM was fired
-- @param #SEAD self
-- @param #string WeaponName
-- @return #boolean Returns true for a match
-- @return #string name Name of hit in table
function SEAD:_CheckHarms(WeaponName)
  self:T( { WeaponName } )
  local hit = false
  local name = ""
    for _,_name in pairs (SEAD.Harms) do
      if string.find(WeaponName,_name,1,true) then
        hit = true
        name = _name
        break
      end
    end
  return hit, name
end

--- (Internal) Return distance in meters between two coordinates or -1 on error.
-- @param #SEAD self
-- @param Core.Point#COORDINATE _point1 Coordinate one
-- @param Core.Point#COORDINATE _point2 Coordinate two
-- @return #number Distance in meters
function SEAD:_GetDistance(_point1, _point2)
  self:T("_GetDistance")
  if _point1 and _point2 then
    local distance1 = _point1:Get2DDistance(_point2)
    local distance2 = _point1:DistanceFromPointVec2(_point2)
    --self:T({dist1=distance1, dist2=distance2})
    if distance1 and type(distance1) == "number" then
      return distance1
    elseif distance2 and type(distance2) == "number" then
      return distance2
    else
      self:E("*****Cannot calculate distance!")
      self:E({_point1,_point2})
      return -1
    end
  else
    self:E("******Cannot calculate distance!")
    self:E({_point1,_point2})
    return -1
  end
end

--- (Internal) Calculate hit zone of an AGM-88
-- @param #SEAD self
-- @param #table SEADWeapon DCS.Weapon object
-- @param Core.Point#COORDINATE pos0 Position of the plane when it fired
-- @param #number height Height when the missile was fired
-- @param Wrapper.Group#GROUP SEADGroup Attacker group
-- @param #string SEADWeaponName Weapon Name
-- @return #SEAD self 
function SEAD:onafterCalculateHitZone(From,Event,To,SEADWeapon,pos0,height,SEADGroup,SEADWeaponName)
  self:T("**** Calculating hit zone for " .. (SEADWeaponName or "None"))
  if SEADWeapon and SEADWeapon:isExist() then
    --local pos = SEADWeapon:getPoint()
    
    -- postion and height
    local position = SEADWeapon:getPosition()
    local mheight = height
    -- heading
    local wph = math.atan2(position.x.z, position.x.x)      
    if wph < 0 then
      wph=wph+2*math.pi
    end   
    wph=math.deg(wph)
    
    -- velocity
    local wpndata = SEAD.HarmData["AGM_88"]
    if string.find(SEADWeaponName,"154",1) then
      wpndata = SEAD.HarmData["AGM_154"]
    end
    local mveloc = math.floor(wpndata[2] * 340.29)
    local c1 = (2*mheight*9.81)/(mveloc^2)
    local c2 = (mveloc^2) / 9.81
    local Ropt = c2 * math.sqrt(c1+1) 
    if height <= 5000 then
      Ropt = Ropt * 0.72
    elseif height <= 7500 then
      Ropt = Ropt * 0.82  
    elseif height <= 10000 then
      Ropt = Ropt * 0.87
    elseif height <= 12500 then
      Ropt = Ropt * 0.98
    end
    
    -- look at a couple of zones across the trajectory
    for n=1,3 do
      local dist = Ropt - ((n-1)*20000)
      local predpos= pos0:Translate(dist,wph)
      if predpos then
  
        local targetzone = ZONE_RADIUS:New("Target Zone",predpos:GetVec2(),20000)
        
        if self.debug then
          predpos:MarkToAll(string.format("height=%dm | heading=%d | velocity=%ddeg | Ropt=%dm",mheight,wph,mveloc,Ropt),false)
          targetzone:DrawZone(coalition.side.BLUE,{0,0,1},0.2,nil,nil,3,true)
        end  
        
        local seadset = SET_GROUP:New():FilterPrefixes(self.SEADGroupPrefixes):FilterZones({targetzone}):FilterOnce()
          local tgtgrp = seadset:GetRandom()
          local _targetgroup = nil
          local _targetgroupname = "none"
          local _targetskill = "Random"
          if tgtgrp and tgtgrp:IsAlive() then
            _targetgroup = tgtgrp
            _targetgroupname = tgtgrp:GetName() -- group name
            _targetskill = tgtgrp:GetUnit(1):GetSkill()
            self:T("*** Found Target = ".. _targetgroupname)
            self:ManageEvasion(_targetskill,_targetgroup,pos0,"AGM_88",SEADGroup, 20)
          end
        --end
      end
    end     
  end
  return self
end

--- (Internal) Handle Evasion
-- @param #SEAD self
-- @param #string _targetskill
-- @param Wrapper.Group#GROUP _targetgroup
-- @param Core.Point#COORDINATE SEADPlanePos
-- @param #string SEADWeaponName
-- @param Wrapper.Group#GROUP SEADGroup Attacker Group
-- @param #number timeoffset Offset for tti calc
-- @param Wrapper.Weapon#WEAPON Weapon
-- @return #SEAD self 
function SEAD:onafterManageEvasion(From,Event,To,_targetskill,_targetgroup,SEADPlanePos,SEADWeaponName,SEADGroup,timeoffset,Weapon)
  local timeoffset = timeoffset  or 0
  if _targetskill == "Random" then -- when skill is random, choose a skill
    local Skills = { "Average", "Good", "High", "Excellent" }
    _targetskill = Skills[ math.random(1,4) ]
  end
  --self:T( _targetskill )
  if self.TargetSkill[_targetskill] then
    local _evade = math.random (1,100) -- random number for chance of evading action
    if (_evade > self.TargetSkill[_targetskill].Evade) then
      self:T("*** SEAD - Evading")
      -- calculate distance of attacker
      local _targetpos = _targetgroup:GetCoordinate()
      local _distance = self:_GetDistance(SEADPlanePos, _targetpos)
      -- weapon speed
      local hit, data = self:_CheckHarms(SEADWeaponName)
      local wpnspeed = 666 -- ;)
      local reach = 10
      if hit then
        local wpndata = SEAD.HarmData[data]
        reach = wpndata[1] * 1.1
        local mach = wpndata[2]
        wpnspeed = math.floor(mach * 340.29)
        if Weapon and Weapon:GetSpeed() > 0 then
          wpnspeed = Weapon:GetSpeed()
          self:T(string.format("*** SEAD - Weapon Speed from WEAPON: %f m/s",wpnspeed))
        end
      end
      -- time to impact
      local _tti = math.floor(_distance / wpnspeed) - timeoffset -- estimated impact time
      if _distance > 0 then
        _distance = math.floor(_distance / 1000) -- km
      else
        _distance = 0
      end
  
      self:T( string.format("*** SEAD - target skill %s, distance %dkm, reach %dkm, tti %dsec", _targetskill, _distance,reach,_tti ))
  
      if reach >= _distance then
        self:T("*** SEAD - Shot in Reach")
  
        local function SuppressionStart(args)
          self:T(string.format("*** SEAD - %s Radar Off & Relocating",args[2]))
          local grp = args[1] -- Wrapper.Group#GROUP
          local name = args[2] -- #string Group Name
          local attacker = args[3] -- Wrapper.Group#GROUP
          if self.UseEmissionsOnOff then
            grp:EnableEmission(false)
          end
          grp:OptionAlarmStateGreen() -- needed else we cannot move around
          grp:RelocateGroundRandomInRadius(20,300,false,false,"Diamond",true)
          if self.UseCallBack then
            local object = self.CallBack
            object:SeadSuppressionStart(grp,name,attacker)
          end
        end
  
        local function SuppressionStop(args)
          self:T(string.format("*** SEAD - %s Radar On",args[2]))
          local grp = args[1]  -- Wrapper.Group#GROUP
          local name = args[2] -- #string Group Name
          if self.UseEmissionsOnOff then
            grp:EnableEmission(true)
          end
          grp:OptionAlarmStateRed()
          grp:OptionEngageRange(self.EngagementRange)
          self.SuppressedGroups[name] = false
          if self.UseCallBack then
            local object = self.CallBack
            object:SeadSuppressionEnd(grp,name)
          end
        end
  
        -- randomize switch-on time
        local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2])
        if delay > _tti then delay = delay / 2 end -- speed up
        if _tti > 600 then delay =  _tti - 90 end -- shot from afar, 600 is default shorad ontime
  
        local SuppressionStartTime = timer.getTime() + delay
        local SuppressionEndTime = timer.getTime() + delay + _tti + self.Padding + delay
        local _targetgroupname = _targetgroup:GetName()
        if not self.SuppressedGroups[_targetgroupname] then
          self:T(string.format("*** SEAD - %s | Parameters TTI %ds | Switch-Off in %ds",_targetgroupname,_tti,delay))
          timer.scheduleFunction(SuppressionStart,{_targetgroup,_targetgroupname, SEADGroup},SuppressionStartTime)
          timer.scheduleFunction(SuppressionStop,{_targetgroup,_targetgroupname},SuppressionEndTime)
          self.SuppressedGroups[_targetgroupname] = true
          if self.UseCallBack then
            local object = self.CallBack
            object:SeadSuppressionPlanned(_targetgroup,_targetgroupname,SuppressionStartTime,SuppressionEndTime, SEADGroup)
          end
        end
  
      end
    end
  end
  return self
end

--- (Internal) Detects if an SAM site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME.
-- @param #SEAD self
-- @param Core.Event#EVENTDATA EventData
-- @return #SEAD self
function SEAD:HandleEventShot( EventData )
  self:T( { EventData.id } )
  
  local SEADWeapon = EventData.Weapon -- Identify the weapon fired
  local SEADWeaponName = EventData.WeaponName or "None" -- return weapon type
  
  if self:_CheckHarms(SEADWeaponName) then
    --UTILS.PrintTableToLog(EventData)
    local SEADPlane = EventData.IniUnit -- Wrapper.Unit#UNIT
    
    if not SEADPlane then return self end -- case IniUnit is empty
    
    local SEADGroup = EventData.IniGroup -- Wrapper.Group#GROUP
    local SEADPlanePos = SEADPlane:GetCoordinate() -- Core.Point#COORDINATE
    local SEADUnit = EventData.IniDCSUnit
    local SEADUnitName = EventData.IniDCSUnitName
  
    local WeaponWrapper = WEAPON:New(EventData.Weapon) -- Wrapper.Weapon#WEAPON
    
    self:T( "*** SEAD - Missile Launched = " .. SEADWeaponName)

    self:T( '*** SEAD - Weapon Match' )
    if self.WeaponTrack == true then
      WeaponWrapper:SetFuncTrack(function(weapon) env.info(string.format("*** Weapon Speed: %d m/s",weapon:GetSpeed() or -1)) end)
      WeaponWrapper:StartTrack(0.1)
      WeaponWrapper:StopTrack(30)
    end
    local _targetskill = "Random"
    local _targetgroupname = "none"
    local _target = EventData.Weapon:getTarget() -- Identify target
    if not _target or self.debug  then -- AGM-88 or 154 w/o target data
      self:E("***** SEAD - No target data for " .. (SEADWeaponName or "None"))
      if string.find(SEADWeaponName,"AGM_88",1,true) or string.find(SEADWeaponName,"AGM_154",1,true) then
        self:T("**** Tracking AGM-88/154 with no target data.")
        local pos0 = SEADPlane:GetCoordinate()
        local fheight = SEADPlane:GetHeight()
        self:__CalculateHitZone(20,SEADWeapon,pos0,fheight,SEADGroup,SEADWeaponName)
      end
      return self
    end
    local targetcat = Object.getCategory(_target) -- Identify category
    local _targetUnit = nil -- Wrapper.Unit#UNIT
    local _targetgroup = nil -- Wrapper.Group#GROUP
    self:T(string.format("*** Targetcat = %d",targetcat))
    if targetcat == Object.Category.UNIT then -- UNIT
      self:T("*** Target Category UNIT")
      _targetUnit = UNIT:Find(_target) -- Wrapper.Unit#UNIT
      if _targetUnit and _targetUnit:IsAlive() then
        _targetgroup = _targetUnit:GetGroup()
        _targetgroupname = _targetgroup:GetName() -- group name
        local _targetUnitName = _targetUnit:GetName()
        _targetUnit:GetSkill()
        _targetskill = _targetUnit:GetSkill()
      end
    elseif targetcat == Object.Category.STATIC then
      self:T("*** Target Category STATIC")
      local seadset = SET_GROUP:New():FilterPrefixes(self.SEADGroupPrefixes):FilterOnce()
      local targetpoint = _target:getPoint() or {x=0,y=0,z=0}
      local tgtcoord = COORDINATE:NewFromVec3(targetpoint)
      local tgtgrp = seadset:FindNearestGroupFromPointVec2(tgtcoord)
      if tgtgrp and tgtgrp:IsAlive() then
        _targetgroup = tgtgrp
        _targetgroupname = tgtgrp:GetName() -- group name
        _targetskill = tgtgrp:GetUnit(1):GetSkill()
        self:T("*** Found Target = ".. _targetgroupname)
      end
    end
    -- see if we are shot at
    local SEADGroupFound = false
    for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do
      self:T("Target = ".. _targetgroupname .. " | Prefix = " .. SEADGroupPrefix )
      if string.find( _targetgroupname, SEADGroupPrefix,1,true ) then
        SEADGroupFound = true
        self:T( '*** SEAD - Group Match Found' )
        break
      end
    end
    if SEADGroupFound == true then -- yes we are being attacked
      if string.find(SEADWeaponName,"ADM_141",1,true) then
        self:__ManageEvasion(2,_targetskill,_targetgroup,SEADPlanePos,SEADWeaponName,SEADGroup,2,WeaponWrapper)
      else
        self:ManageEvasion(_targetskill,_targetgroup,SEADPlanePos,SEADWeaponName,SEADGroup,0,WeaponWrapper)
      end
    end
  end
  return self
end
--- **Functional** - Taking the lead of AI escorting your flight.
-- 
-- ===
-- 
-- ## Features:
-- 
--   * Escort navigation commands.
--   * Escort hold at position commands.
--   * Escorts reporting detected targets.
--   * Escorts scanning targets in advance.
--   * Escorts attacking specific targets.
--   * Request assistance from other groups for attack.
--   * Manage rule of engagement of escorts.
--   * Manage the allowed evasion techniques of escorts.
--   * Make escort to execute a defined mission or path.
--   * Escort tactical situation reporting.
-- 
-- ===
--
-- ## Additional Material:
--
-- * **Demo Missions:** [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Functional/Escort)
-- * **YouTube videos:** None
-- * **Guides:** None
--
-- ===
-- 
-- Allows you to interact with escorting AI on your flight and take the lead.
-- 
-- Each escorting group can be commanded with a whole set of radio commands (radio menu in your flight, and then F10).
--
-- The radio commands will vary according the category of the group. The richest set of commands are with Helicopters and AirPlanes.
-- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts.
--
-- # RADIO MENUs that can be created:
-- 
-- Find a summary below of the current available commands:
--
-- ## Navigation ...:
-- 
-- Escort group navigation functions:
--
--   * **"Join-Up and Follow at x meters":** The escort group fill follow you at about x meters, and they will follow you.
--   * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color.
--   * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops.
--
-- ## Hold position ...:
-- 
-- Escort group navigation functions:
--
--   * **"At current location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped.
--   * **"At client location":** Stops the escort group and they will hover 30 meters above the ground at the position they stopped.
--
-- ## Report targets ...:
-- 
-- Report targets will make the escort group to report any target that it identifies within a 8km range. Any detected target can be attacked using the 4. Attack nearby targets function. (see below).
--
--   * **"Report now":** Will report the current detected targets.
--   * **"Report targets on":** Will make the escort group to report detected targets and will fill the "Attack nearby targets" menu list.
--   * **"Report targets off":** Will stop detecting targets.
--
-- ## Scan targets ...:
-- 
-- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or defined task.
--
--   * **"Scan targets 30 seconds":** Scan 30 seconds for targets.
--   * **"Scan targets 60 seconds":** Scan 60 seconds for targets.
--
-- ## Attack targets ...:
-- 
-- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed.
--
-- ## Request assistance from ...:
-- 
-- This menu item will list all detected targets within a 15km range, as with the menu item **Attack Targets**.
-- This menu item allows to request attack support from other escorts supporting the current client group.
-- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles.
-- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area.
--
-- ## ROE ...:
-- 
-- Sets the Rules of Engagement (ROE) of the escort group when in flight.
--
--   * **"Hold Fire":** The escort group will hold fire.
--   * **"Return Fire":** The escort group will return fire.
--   * **"Open Fire":** The escort group will open fire on designated targets.
--   * **"Weapon Free":** The escort group will engage with any target.
--
-- ## Evasion ...:
-- 
-- Will define the evasion techniques that the escort group will perform during flight or combat.
--
--   * **"Fight until death":** The escort group will have no reaction to threats.
--   * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed.
--   * **"Evade enemy fire":** The rescort group will evade enemy fire before firing.
--   * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres.
--
-- ## Resume Mission ...:
-- 
-- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint.
-- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission.
--
-- ===
-- 
-- ### Authors: **FlightControl** 
-- 
-- ===
--
-- @module Functional.Escort
-- @image Escorting.JPG


---
-- @type ESCORT
-- @extends Core.Base#BASE
-- @field Wrapper.Client#CLIENT EscortClient
-- @field Wrapper.Group#GROUP EscortGroup
-- @field #string EscortName
-- @field #ESCORT.MODE EscortMode The mode the escort is in.
-- @field Core.Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class.
-- @field #number FollowDistance The current follow distance.
-- @field #boolean ReportTargets If true, nearby targets are reported.
-- @Field DCS#AI.Option.Air.val.ROE OptionROE Which ROE is set to the EscortGroup.
-- @field DCS#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the EscortGroup.
-- @field FunctionalMENU_GROUPDETECTION_BASE Detection

--- ESCORT class
-- 
-- # ESCORT construction methods.
-- 
-- Create a new SPAWN object with the @{#ESCORT.New} method:
--
--  * @{#ESCORT.New}: Creates a new ESCORT object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text.
--
-- @usage
-- -- Declare a new EscortPlanes object as follows:
-- 
-- -- First find the GROUP object and the CLIENT object.
-- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor.
-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client.
-- 
-- -- Now use these 2 objects to construct the new EscortPlanes object.
-- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." )
--
-- @field #ESCORT
ESCORT = {
  ClassName = "ESCORT",
  EscortName = nil, -- The Escort Name
  EscortClient = nil,
  EscortGroup = nil,
  EscortMode = 1,
  MODE = {
    FOLLOW = 1,
    MISSION = 2,
  },
  Targets = {}, -- The identified targets
  FollowScheduler = nil,
  ReportTargets = true,
  OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE,
  OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION,
  SmokeDirectionVector = false,
  TaskPoints = {}
}

--- ESCORT.Mode class
-- @type ESCORT.MODE
-- @field #number FOLLOW
-- @field #number MISSION

--- MENUPARAM type
-- @type MENUPARAM
-- @field #ESCORT ParamSelf
-- @field #Distance ParamDistance
-- @field #function ParamFunction
-- @field #string ParamMessage

--- ESCORT class constructor for an AI group
-- @param #ESCORT self
-- @param Wrapper.Client#CLIENT EscortClient The client escorted by the EscortGroup.
-- @param Wrapper.Group#GROUP EscortGroup The group AI escorting the EscortClient.
-- @param #string EscortName Name of the escort.
-- @param #string EscortBriefing A text showing the ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown.
-- @return #ESCORT self
-- @usage
-- -- Declare a new EscortPlanes object as follows:
-- 
-- -- First find the GROUP object and the CLIENT object.
-- local EscortClient = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor.
-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client.
-- 
-- -- Now use these 2 objects to construct the new EscortPlanes object.
-- EscortPlanes = ESCORT:New( EscortClient, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." )
function ESCORT:New( EscortClient, EscortGroup, EscortName, EscortBriefing )
  
  local self = BASE:Inherit( self, BASE:New() ) -- #ESCORT
  self:F( { EscortClient, EscortGroup, EscortName } )

  self.EscortClient = EscortClient -- Wrapper.Client#CLIENT
  self.EscortGroup = EscortGroup -- Wrapper.Group#GROUP
  self.EscortName = EscortName
  self.EscortBriefing = EscortBriefing
 
  self.EscortSetGroup = SET_GROUP:New()
  self.EscortSetGroup:AddObject( self.EscortGroup )
  self.EscortSetGroup:Flush()
  self.Detection = DETECTION_UNITS:New( self.EscortSetGroup, 15000 )
  
  self.EscortGroup.Detection = self.Detection
  
  -- Set EscortGroup known at EscortClient.
  if not self.EscortClient._EscortGroups then
    self.EscortClient._EscortGroups = {}
  end

  if not self.EscortClient._EscortGroups[EscortGroup:GetName()] then
    self.EscortClient._EscortGroups[EscortGroup:GetName()] = {}
    self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortGroup = self.EscortGroup
    self.EscortClient._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName
    self.EscortClient._EscortGroups[EscortGroup:GetName()].Detection = self.EscortGroup.Detection
  end

  self.EscortMenu = MENU_GROUP:New( self.EscortClient:GetGroup(), self.EscortName )

  self.EscortGroup:WayPointInitialize(1)

  self.EscortGroup:OptionROTVertical()
  self.EscortGroup:OptionROEOpenFire()
  
  if not EscortBriefing then
    EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " ..
      "We're escorting your flight. " ..
      "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n",
      60, EscortClient
    )
  else
    EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") " .. EscortBriefing,
      60, EscortClient
    )
  end

  self.FollowDistance = 100
  self.CT1 = 0
  self.GT1 = 0

  self.FollowScheduler, self.FollowSchedule = SCHEDULER:New( self, self._FollowScheduler, {}, 1, .5, .01 )
  self.FollowScheduler:Stop( self.FollowSchedule )

  self.EscortMode = ESCORT.MODE.MISSION
  
 
  return self
end

--- Set a Detection method for the EscortClient to be reported upon.
-- Detection methods are based on the derived classes from DETECTION_BASE.
-- @param #ESCORT self
-- @param Functional.Detection#DETECTION_BASE Detection
function ESCORT:SetDetection( Detection )

  self.Detection = Detection
  self.EscortGroup.Detection = self.Detection
  self.EscortClient._EscortGroups[self.EscortGroup:GetName()].Detection = self.EscortGroup.Detection
  
  Detection:__Start( 1 )
  
end

--- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to.
-- This allows to visualize where the escort is flying to.
-- @param #ESCORT self
-- @param #boolean SmokeDirection If true, then the direction vector will be smoked.
function ESCORT:TestSmokeDirectionVector( SmokeDirection )
  self.SmokeDirectionVector = ( SmokeDirection == true ) and true or false
end


--- Defines the default menus
-- @param #ESCORT self
-- @return #ESCORT
function ESCORT:Menus()
  self:F()

  self:MenuFollowAt( 100 )
  self:MenuFollowAt( 200 )
  self:MenuFollowAt( 300 )
  self:MenuFollowAt( 400 )

  self:MenuScanForTargets( 100, 60 )

  self:MenuHoldAtEscortPosition( 30 )
  self:MenuHoldAtLeaderPosition( 30 )

  self:MenuFlare()
  self:MenuSmoke()

  self:MenuReportTargets( 60 )
  self:MenuAssistedAttack()
  self:MenuROE()
  self:MenuEvasion()
  self:MenuResumeMission()


  return self
end



--- Defines a menu slot to let the escort Join and Follow you at a certain distance.
-- This menu will appear under **Navigation**.
-- @param #ESCORT self
-- @param DCS#Distance Distance The distance in meters that the escort needs to follow the client.
-- @return #ESCORT
function ESCORT:MenuFollowAt( Distance )
  self:F(Distance)

  if self.EscortGroup:IsAir() then
    if not self.EscortMenuReportNavigation then
      self.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortClient:GetGroup(), "Navigation", self.EscortMenu )
    end

    if not self.EscortMenuJoinUpAndFollow then
      self.EscortMenuJoinUpAndFollow = {}
    end

    self.EscortMenuJoinUpAndFollow[#self.EscortMenuJoinUpAndFollow+1] = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Join-Up and Follow at " .. Distance, self.EscortMenuReportNavigation, ESCORT._JoinUpAndFollow, self, Distance )

    self.EscortMode = ESCORT.MODE.FOLLOW
  end

  return self
end

--- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds.
-- This menu will appear under **Hold position**.
-- @param #ESCORT self
-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters.
-- @param DCS#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given.
-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed.
-- @return #ESCORT
-- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function.
function ESCORT:MenuHoldAtEscortPosition( Height, Seconds, MenuTextFormat )
  self:F( { Height, Seconds, MenuTextFormat } )

  if self.EscortGroup:IsAir() then

    if not self.EscortMenuHold then
      self.EscortMenuHold = MENU_GROUP:New( self.EscortClient:GetGroup(), "Hold position", self.EscortMenu )
    end

    if not Height then
      Height = 30
    end

    if not Seconds then
      Seconds = 0
    end

    local MenuText = ""
    if not MenuTextFormat then
      if Seconds == 0 then
        MenuText = string.format( "Hold at %d meter", Height )
      else
        MenuText = string.format( "Hold at %d meter for %d seconds", Height, Seconds )
      end
    else
      if Seconds == 0 then
        MenuText = string.format( MenuTextFormat, Height )
      else
        MenuText = string.format( MenuTextFormat, Height, Seconds )
      end
    end

    if not self.EscortMenuHoldPosition then
      self.EscortMenuHoldPosition = {}
    end

    self.EscortMenuHoldPosition[#self.EscortMenuHoldPosition+1] = MENU_GROUP_COMMAND
      :New(
        self.EscortClient:GetGroup(),
        MenuText,
        self.EscortMenuHold,
        ESCORT._HoldPosition,
        self,
        self.EscortGroup,
        Height,
        Seconds
      )
  end

  return self
end


--- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds.
-- This menu will appear under **Navigation**.
-- @param #ESCORT self
-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters.
-- @param DCS#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given.
-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed.
-- @return #ESCORT
-- TODO: Implement Seconds parameter. Challenge is to first develop the "continue from last activity" function.
function ESCORT:MenuHoldAtLeaderPosition( Height, Seconds, MenuTextFormat )
  self:F( { Height, Seconds, MenuTextFormat } )

  if self.EscortGroup:IsAir() then

    if not self.EscortMenuHold then
      self.EscortMenuHold = MENU_GROUP:New( self.EscortClient:GetGroup(), "Hold position", self.EscortMenu )
    end

    if not Height then
      Height = 30
    end

    if not Seconds then
      Seconds = 0
    end

    local MenuText = ""
    if not MenuTextFormat then
      if Seconds == 0 then
        MenuText = string.format( "Rejoin and hold at %d meter", Height )
      else
        MenuText = string.format( "Rejoin and hold at %d meter for %d seconds", Height, Seconds )
      end
    else
      if Seconds == 0 then
        MenuText = string.format( MenuTextFormat, Height )
      else
        MenuText = string.format( MenuTextFormat, Height, Seconds )
      end
    end

    if not self.EscortMenuHoldAtLeaderPosition then
      self.EscortMenuHoldAtLeaderPosition = {}
    end

    self.EscortMenuHoldAtLeaderPosition[#self.EscortMenuHoldAtLeaderPosition+1] = MENU_GROUP_COMMAND
      :New(
        self.EscortClient:GetGroup(),
        MenuText,
        self.EscortMenuHold,
        ESCORT._HoldPosition,
        { ParamSelf = self,
          ParamOrbitGroup = self.EscortClient,
          ParamHeight = Height,
          ParamSeconds = Seconds
        }
      )
  end

  return self
end

--- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds.
-- This menu will appear under **Scan targets**.
-- @param #ESCORT self
-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters.
-- @param DCS#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given.
-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed.
-- @return #ESCORT
function ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat )
  self:F( { Height, Seconds, MenuTextFormat } )

  if self.EscortGroup:IsAir() then
    if not self.EscortMenuScan then
      self.EscortMenuScan = MENU_GROUP:New( self.EscortClient:GetGroup(), "Scan for targets", self.EscortMenu )
    end

    if not Height then
      Height = 100
    end

    if not Seconds then
      Seconds = 30
    end

    local MenuText = ""
    if not MenuTextFormat then
      if Seconds == 0 then
        MenuText = string.format( "At %d meter", Height )
      else
        MenuText = string.format( "At %d meter for %d seconds", Height, Seconds )
      end
    else
      if Seconds == 0 then
        MenuText = string.format( MenuTextFormat, Height )
      else
        MenuText = string.format( MenuTextFormat, Height, Seconds )
      end
    end

    if not self.EscortMenuScanForTargets then
      self.EscortMenuScanForTargets = {}
    end

    self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_GROUP_COMMAND
      :New(
        self.EscortClient:GetGroup(),
        MenuText,
        self.EscortMenuScan,
        ESCORT._ScanTargets,
        self,
        30
      )
  end

  return self
end



--- Defines a menu slot to let the escort disperse a flare in a certain color.
-- This menu will appear under **Navigation**.
-- The flare will be fired from the first unit in the group.
-- @param #ESCORT self
-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed.
-- @return #ESCORT
function ESCORT:MenuFlare( MenuTextFormat )
  self:F()

  if not self.EscortMenuReportNavigation then
    self.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortClient:GetGroup(), "Navigation", self.EscortMenu )
  end

  local MenuText = ""
  if not MenuTextFormat then
    MenuText = "Flare"
  else
    MenuText = MenuTextFormat
  end

  if not self.EscortMenuFlare then
    self.EscortMenuFlare = MENU_GROUP:New( self.EscortClient:GetGroup(), MenuText, self.EscortMenuReportNavigation, ESCORT._Flare, self )
    self.EscortMenuFlareGreen  = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release green flare",  self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Green,  "Released a green flare!"   )
    self.EscortMenuFlareRed    = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release red flare",    self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Red,    "Released a red flare!"     )
    self.EscortMenuFlareWhite  = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release white flare",  self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.White,  "Released a white flare!"   )
    self.EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release yellow flare", self.EscortMenuFlare, ESCORT._Flare, self, FLARECOLOR.Yellow, "Released a yellow flare!"  )
  end

  return self
end

--- Defines a menu slot to let the escort disperse a smoke in a certain color.
-- This menu will appear under **Navigation**.
-- Note that smoke menu options will only be displayed for ships and ground units. Not for air units.
-- The smoke will be fired from the first unit in the group.
-- @param #ESCORT self
-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed.
-- @return #ESCORT
function ESCORT:MenuSmoke( MenuTextFormat )
  self:F()

  if not self.EscortGroup:IsAir() then
    if not self.EscortMenuReportNavigation then
      self.EscortMenuReportNavigation = MENU_GROUP:New( self.EscortClient:GetGroup(), "Navigation", self.EscortMenu )
    end

    local MenuText = ""
    if not MenuTextFormat then
      MenuText = "Smoke"
    else
      MenuText = MenuTextFormat
    end

    if not self.EscortMenuSmoke then
      self.EscortMenuSmoke = MENU_GROUP:New( self.EscortClient:GetGroup(), "Smoke", self.EscortMenuReportNavigation, ESCORT._Smoke, self )
      self.EscortMenuSmokeGreen  = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release green smoke",  self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Green,  "Releasing green smoke!"   )
      self.EscortMenuSmokeRed    = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release red smoke",    self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Red,    "Releasing red smoke!"     )
      self.EscortMenuSmokeWhite  = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release white smoke",  self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.White,  "Releasing white smoke!"   )
      self.EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release orange smoke", self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!"  )
      self.EscortMenuSmokeBlue   = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Release blue smoke",   self.EscortMenuSmoke, ESCORT._Smoke, self, SMOKECOLOR.Blue,   "Releasing blue smoke!"    )
    end
  end

  return self
end

--- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds.
-- This menu will appear under **Report targets**.
-- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed.
-- @param #ESCORT self
-- @param DCS#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds.
-- @return #ESCORT
function ESCORT:MenuReportTargets( Seconds )
  self:F( { Seconds } )

  if not self.EscortMenuReportNearbyTargets then
    self.EscortMenuReportNearbyTargets = MENU_GROUP:New( self.EscortClient:GetGroup(), "Report targets", self.EscortMenu )
  end

  if not Seconds then
    Seconds = 30
  end

  -- Report Targets
  self.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Report targets now!", self.EscortMenuReportNearbyTargets, ESCORT._ReportNearbyTargetsNow, self )
  self.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Report targets on", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, self, true )
  self.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Report targets off", self.EscortMenuReportNearbyTargets, ESCORT._SwitchReportNearbyTargets, self, false )

  -- Attack Targets
  self.EscortMenuAttackNearbyTargets = MENU_GROUP:New( self.EscortClient:GetGroup(), "Attack targets", self.EscortMenu )


  self.ReportTargetsScheduler, self.ReportTargetsSchedulerID = SCHEDULER:New( self, self._ReportTargetsScheduler, {}, 1, Seconds )

  return self
end

--- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client.
-- This menu will appear under **Request assistance from**.
-- Note that this method needs to be preceded with the method MenuReportTargets.
-- @param #ESCORT self
-- @return #ESCORT
function ESCORT:MenuAssistedAttack()
  self:F()

  -- Request assistance from other escorts.
  -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane...
  self.EscortMenuTargetAssistance = MENU_GROUP:New( self.EscortClient:GetGroup(), "Request assistance from", self.EscortMenu )

  return self
end

--- Defines a menu to let the escort set its rules of engagement.
-- All rules of engagement will appear under the menu **ROE**.
-- @param #ESCORT self
-- @return #ESCORT
function ESCORT:MenuROE( MenuTextFormat )
  self:F( MenuTextFormat )

  if not self.EscortMenuROE then
    -- Rules of Engagement
    self.EscortMenuROE = MENU_GROUP:New( self.EscortClient:GetGroup(), "ROE", self.EscortMenu )
    if self.EscortGroup:OptionROEHoldFirePossible() then
      self.EscortMenuROEHoldFire = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Hold Fire", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEHoldFire(), "Holding weapons!" )
    end
    if self.EscortGroup:OptionROEReturnFirePossible() then
      self.EscortMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Return Fire", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEReturnFire(), "Returning fire!" )
    end
    if self.EscortGroup:OptionROEOpenFirePossible() then
      self.EscortMenuROEOpenFire = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Open Fire", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEOpenFire(), "Opening fire on designated targets!!" )
    end
    if self.EscortGroup:OptionROEWeaponFreePossible() then
      self.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Weapon Free", self.EscortMenuROE, ESCORT._ROE, self, self.EscortGroup:OptionROEWeaponFree(), "Opening fire on targets of opportunity!" )
    end
  end

  return self
end


--- Defines a menu to let the escort set its evasion when under threat.
-- All rules of engagement will appear under the menu **Evasion**.
-- @param #ESCORT self
-- @return #ESCORT
function ESCORT:MenuEvasion( MenuTextFormat )
  self:F( MenuTextFormat )

  if self.EscortGroup:IsAir() then
    if not self.EscortMenuEvasion then
      -- Reaction to Threats
      self.EscortMenuEvasion = MENU_GROUP:New( self.EscortClient:GetGroup(), "Evasion", self.EscortMenu )
      if self.EscortGroup:OptionROTNoReactionPossible() then
        self.EscortMenuEvasionNoReaction = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Fight until death", self.EscortMenuEvasion, ESCORT._ROT, self, self.EscortGroup:OptionROTNoReaction(), "Fighting until death!" )
      end
      if self.EscortGroup:OptionROTPassiveDefensePossible() then
        self.EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Use flares, chaff and jammers", self.EscortMenuEvasion, ESCORT._ROT, self, self.EscortGroup:OptionROTPassiveDefense(), "Defending using jammers, chaff and flares!" )
      end
      if self.EscortGroup:OptionROTEvadeFirePossible() then
        self.EscortMenuEvasionEvadeFire = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Evade enemy fire", self.EscortMenuEvasion, ESCORT._ROT, self, self.EscortGroup:OptionROTEvadeFire(), "Evading on enemy fire!" )
      end
      if self.EscortGroup:OptionROTVerticalPossible() then
        self.EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(), "Go below radar and evade fire", self.EscortMenuEvasion, ESCORT._ROT, self, self.EscortGroup:OptionROTVertical(), "Evading on enemy fire with vertical manoeuvres!" )
      end
    end
  end

  return self
end

--- Defines a menu to let the escort resume its mission from a waypoint on its route.
-- All rules of engagement will appear under the menu **Resume mission from**.
-- @param #ESCORT self
-- @return #ESCORT
function ESCORT:MenuResumeMission()
  self:F()

  if not self.EscortMenuResumeMission then
    -- Mission Resume Menu Root
    self.EscortMenuResumeMission = MENU_GROUP:New( self.EscortClient:GetGroup(), "Resume mission from", self.EscortMenu )
  end

  return self
end


-- @param #MENUPARAM MenuParam
function ESCORT:_HoldPosition( OrbitGroup, OrbitHeight, OrbitSeconds )

  local EscortGroup = self.EscortGroup
  local EscortClient = self.EscortClient

  local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT

  self.FollowScheduler:Stop( self.FollowSchedule )

  local PointFrom = {}
  local GroupVec3 = EscortGroup:GetUnit(1):GetVec3()
  PointFrom = {}
  PointFrom.x = GroupVec3.x
  PointFrom.y = GroupVec3.z
  PointFrom.speed = 250
  PointFrom.type = AI.Task.WaypointType.TURNING_POINT
  PointFrom.alt = GroupVec3.y
  PointFrom.alt_type = AI.Task.AltitudeType.BARO

  local OrbitPoint = OrbitUnit:GetVec2()
  local PointTo = {}
  PointTo.x = OrbitPoint.x
  PointTo.y = OrbitPoint.y
  PointTo.speed = 250
  PointTo.type = AI.Task.WaypointType.TURNING_POINT
  PointTo.alt = OrbitHeight
  PointTo.alt_type = AI.Task.AltitudeType.BARO
  PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 )

  local Points = { PointFrom, PointTo }

  EscortGroup:OptionROEHoldFire()
  EscortGroup:OptionROTPassiveDefense()

  EscortGroup:SetTask( EscortGroup:TaskRoute( Points ) )
  EscortGroup:MessageToClient( "Orbiting at location.", 10, EscortClient )

end

-- @param #MENUPARAM MenuParam
function ESCORT:_JoinUpAndFollow( Distance )

  local EscortGroup = self.EscortGroup
  local EscortClient = self.EscortClient

  self.Distance = Distance

  self:JoinUpAndFollow( EscortGroup, EscortClient, self.Distance )
end

--- JoinsUp and Follows a CLIENT.
-- @param Functional.Escort#ESCORT self
-- @param Wrapper.Group#GROUP EscortGroup
-- @param Wrapper.Client#CLIENT EscortClient
-- @param DCS#Distance Distance
function ESCORT:JoinUpAndFollow( EscortGroup, EscortClient, Distance )
  self:F( { EscortGroup, EscortClient, Distance } )

  self.FollowScheduler:Stop( self.FollowSchedule )

  EscortGroup:OptionROEHoldFire()
  EscortGroup:OptionROTPassiveDefense()

  self.EscortMode = ESCORT.MODE.FOLLOW

  self.CT1 = 0
  self.GT1 = 0
  self.FollowScheduler:Start( self.FollowSchedule )

  EscortGroup:MessageToClient( "Rejoining and Following at " .. Distance .. "!", 30, EscortClient )
end

-- @param #MENUPARAM MenuParam
function ESCORT:_Flare( Color, Message )

  local EscortGroup = self.EscortGroup
  local EscortClient = self.EscortClient

  EscortGroup:GetUnit(1):Flare( Color )
  EscortGroup:MessageToClient( Message, 10, EscortClient )
end

-- @param #MENUPARAM MenuParam
function ESCORT:_Smoke( Color, Message )

  local EscortGroup = self.EscortGroup
  local EscortClient = self.EscortClient

  EscortGroup:GetUnit(1):Smoke( Color )
  EscortGroup:MessageToClient( Message, 10, EscortClient )
end


-- @param #MENUPARAM MenuParam
function ESCORT:_ReportNearbyTargetsNow()

  local EscortGroup = self.EscortGroup
  local EscortClient = self.EscortClient

  self:_ReportTargetsScheduler()

end

function ESCORT:_SwitchReportNearbyTargets( ReportTargets )

  local EscortGroup = self.EscortGroup
  local EscortClient = self.EscortClient

  self.ReportTargets = ReportTargets

  if self.ReportTargets then
    if not self.ReportTargetsScheduler then
      self.ReportTargetsScheduler:Schedule( self, self._ReportTargetsScheduler, {}, 1, 30 )
    end
  else
    self.ReportTargetsScheduler:Remove(self.ReportTargetsSchedulerID)
    self.ReportTargetsScheduler = nil
  end
end

-- @param #MENUPARAM MenuParam
function ESCORT:_ScanTargets( ScanDuration )

  local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP
  local EscortClient = self.EscortClient

  self.FollowScheduler:Stop( self.FollowSchedule )

  if EscortGroup:IsHelicopter() then
    EscortGroup:PushTask(
      EscortGroup:TaskControlled(
        EscortGroup:TaskOrbitCircle( 200, 20 ),
        EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil )
      ), 1 )
  elseif EscortGroup:IsAirPlane() then
    EscortGroup:PushTask(
      EscortGroup:TaskControlled(
        EscortGroup:TaskOrbitCircle( 1000, 500 ),
        EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil )
      ), 1 )
  end

  EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortClient )

  if self.EscortMode == ESCORT.MODE.FOLLOW then
    self.FollowScheduler:Start( self.FollowSchedule )
  end

end

-- @param Wrapper.Group#GROUP EscortGroup
function _Resume( EscortGroup )
  env.info( '_Resume' )

  local Escort = EscortGroup:GetState( EscortGroup, "Escort" )
  env.info( "EscortMode = "  .. Escort.EscortMode )
  if Escort.EscortMode == ESCORT.MODE.FOLLOW then
    Escort:JoinUpAndFollow( EscortGroup, Escort.EscortClient, Escort.Distance )
  end

end

-- @param #ESCORT self
-- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem
function ESCORT:_AttackTarget( DetectedItem )

  local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP
  self:F( EscortGroup )
  
  local EscortClient = self.EscortClient

  self.FollowScheduler:Stop( self.FollowSchedule )

  if EscortGroup:IsAir() then
    EscortGroup:OptionROEOpenFire()
    EscortGroup:OptionROTPassiveDefense()
    EscortGroup:SetState( EscortGroup, "Escort", self )

    local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem )
    
    local Tasks = {}

    DetectedSet:ForEachUnit(
      -- @param Wrapper.Unit#UNIT DetectedUnit
      function( DetectedUnit, Tasks )
        if DetectedUnit:IsAlive() then
          Tasks[#Tasks+1] = EscortGroup:TaskAttackUnit( DetectedUnit )
        end
      end, Tasks
    )    

    Tasks[#Tasks+1] = EscortGroup:TaskFunction( "_Resume", { "''" } )
    
    EscortGroup:SetTask( 
      EscortGroup:TaskCombo(
        Tasks
      ), 1
    )
    
  else
  
    local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem )
    
    local Tasks = {}

    DetectedSet:ForEachUnit(
      -- @param Wrapper.Unit#UNIT DetectedUnit
      function( DetectedUnit, Tasks )
        if DetectedUnit:IsAlive() then
          Tasks[#Tasks+1] = EscortGroup:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 )
        end
      end, Tasks
    )    

    EscortGroup:SetTask( 
      EscortGroup:TaskCombo(
        Tasks
      ), 1
    )

  end
  
  EscortGroup:MessageToClient( "Engaging Designated Unit!", 10, EscortClient )

end

--- 
-- @param #ESCORT self
-- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem
function ESCORT:_AssistTarget( EscortGroupAttack, DetectedItem )

  local EscortGroup = self.EscortGroup
  local EscortClient = self.EscortClient

  self.FollowScheduler:Stop( self.FollowSchedule )

  if EscortGroupAttack:IsAir() then
    EscortGroupAttack:OptionROEOpenFire()
    EscortGroupAttack:OptionROTVertical()
    
    local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem )
    
    local Tasks = {}

    DetectedSet:ForEachUnit(
      -- @param Wrapper.Unit#UNIT DetectedUnit
      function( DetectedUnit, Tasks )
        if DetectedUnit:IsAlive() then
          Tasks[#Tasks+1] = EscortGroupAttack:TaskAttackUnit( DetectedUnit )
        end
      end, Tasks
    )    

    Tasks[#Tasks+1] = EscortGroupAttack:TaskOrbitCircle( 500, 350 )
    
    EscortGroupAttack:SetTask( 
      EscortGroupAttack:TaskCombo(
        Tasks
      ), 1
    )

  else
    local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem )
    
    local Tasks = {}

    DetectedSet:ForEachUnit(
      -- @param Wrapper.Unit#UNIT DetectedUnit
      function( DetectedUnit, Tasks )
        if DetectedUnit:IsAlive() then
          Tasks[#Tasks+1] = EscortGroupAttack:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 )
        end
      end, Tasks
    )    

    EscortGroupAttack:SetTask( 
      EscortGroupAttack:TaskCombo(
        Tasks
      ), 1
    )

  end

  EscortGroupAttack:MessageToClient( "Assisting with the destroying the enemy unit!", 10, EscortClient )

end

-- @param #MENUPARAM MenuParam
function ESCORT:_ROE( EscortROEFunction, EscortROEMessage )

  local EscortGroup = self.EscortGroup
  local EscortClient = self.EscortClient

  pcall( function() EscortROEFunction() end )
  EscortGroup:MessageToClient( EscortROEMessage, 10, EscortClient )
end

-- @param #MENUPARAM MenuParam
function ESCORT:_ROT( EscortROTFunction, EscortROTMessage )

  local EscortGroup = self.EscortGroup
  local EscortClient = self.EscortClient

  pcall( function() EscortROTFunction() end )
  EscortGroup:MessageToClient( EscortROTMessage, 10, EscortClient )
end

-- @param #MENUPARAM MenuParam
function ESCORT:_ResumeMission( WayPoint )

  local EscortGroup = self.EscortGroup
  local EscortClient = self.EscortClient

  self.FollowScheduler:Stop( self.FollowSchedule )

  local WayPoints = EscortGroup:GetTaskRoute()
  self:T( WayPoint, WayPoints )

  for WayPointIgnore = 1, WayPoint do
    table.remove( WayPoints, 1 )
  end

  SCHEDULER:New( EscortGroup, EscortGroup.SetTask, { EscortGroup:TaskRoute( WayPoints ) }, 1 )

  EscortGroup:MessageToClient( "Resuming mission from waypoint " .. WayPoint .. ".", 10, EscortClient )
end

--- Registers the waypoints
-- @param #ESCORT self
-- @return #table
function ESCORT:RegisterRoute()
  self:F()

  local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP

  local TaskPoints = EscortGroup:GetTaskRoute()

  self:T( TaskPoints )

  return TaskPoints
end

-- @param Functional.Escort#ESCORT self
function ESCORT:_FollowScheduler()
  self:F( { self.FollowDistance } )

  self:T( {self.EscortClient.UnitName, self.EscortGroup.GroupName } )
  if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then

    local ClientUnit = self.EscortClient:GetClientGroupUnit()
    local GroupUnit = self.EscortGroup:GetUnit( 1 )
    local FollowDistance = self.FollowDistance
    
    self:T( {ClientUnit.UnitName, GroupUnit.UnitName } )

    if self.CT1 == 0 and self.GT1 == 0 then
      self.CV1 = ClientUnit:GetVec3()
      self:T( { "self.CV1", self.CV1 } )
      self.CT1 = timer.getTime()
      self.GV1 = GroupUnit:GetVec3()
      self.GT1 = timer.getTime()
    else
      local CT1 = self.CT1
      local CT2 = timer.getTime()
      local CV1 = self.CV1
      local CV2 = ClientUnit:GetVec3()
      self.CT1 = CT2
      self.CV1 = CV2

      local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5
      local CT = CT2 - CT1

      local CS = ( 3600 / CT ) * ( CD / 1000 )

      self:T2( { "Client:", CS, CD, CT, CV2, CV1, CT2, CT1 } )

      local GT1 = self.GT1
      local GT2 = timer.getTime()
      local GV1 = self.GV1
      local GV2 = GroupUnit:GetVec3()
      self.GT1 = GT2
      self.GV1 = GV2

      local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5
      local GT = GT2 - GT1

      local GS = ( 3600 / GT ) * ( GD / 1000 )

      self:T2( { "Group:", GS, GD, GT, GV2, GV1, GT2, GT1 } )

      -- Calculate the group direction vector
      local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z }

      -- Calculate GH2, GH2 with the same height as CV2.
      local GH2 = { x = GV2.x, y = CV2.y, z = GV2.z }

      -- Calculate the angle of GV to the orthonormal plane
      local alpha = math.atan2( GV.z, GV.x )

      -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2.
      -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2))
      local CVI = { x = CV2.x + FollowDistance * math.cos(alpha),
        y = GH2.y,
        z = CV2.z + FollowDistance * math.sin(alpha),
      }

      -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction.
      local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z }

      -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s.
      -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed.
      -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right...
      local DVu = { x = DV.x / FollowDistance, y = DV.y / FollowDistance, z = DV.z / FollowDistance }

      -- Now we can calculate the group destination vector GDV.
      local GDV = { x = DVu.x * CS * 8 + CVI.x, y = CVI.y, z = DVu.z * CS * 8 + CVI.z }
      
      if self.SmokeDirectionVector == true then
        trigger.action.smoke( GDV, trigger.smokeColor.Red )
      end
      
      self:T2( { "CV2:", CV2 } )
      self:T2( { "CVI:", CVI } )
      self:T2( { "GDV:", GDV } )

      -- Measure distance between client and group
      local CatchUpDistance = ( ( GDV.x - GV2.x )^2 + ( GDV.y - GV2.y )^2 + ( GDV.z - GV2.z )^2 ) ^ 0.5

      -- The calculation of the Speed would simulate that the group would take 30 seconds to overcome
      -- the requested Distance).
      local Time = 10
      local CatchUpSpeed = ( CatchUpDistance - ( CS * 8.4 ) ) / Time

      local Speed = CS + CatchUpSpeed
      if Speed < 0 then
        Speed = 0
      end

      self:T( { "Client Speed, Escort Speed, Speed, FollowDistance, Time:", CS, GS, Speed, FollowDistance, Time } )

      -- Now route the escort to the desired point with the desired speed.
      self.EscortGroup:RouteToVec3( GDV, Speed / 3.6 ) -- DCS models speed in Mps (Miles per second)
    end

    return true
  end

  return false
end


--- Report Targets Scheduler.
-- @param #ESCORT self
function ESCORT:_ReportTargetsScheduler()
  self:F( self.EscortGroup:GetName() )

  if self.EscortGroup:IsAlive() and self.EscortClient:IsAlive() then

      local EscortGroupName = self.EscortGroup:GetName() 
    
      self.EscortMenuAttackNearbyTargets:RemoveSubMenus()

      if self.EscortMenuTargetAssistance then
        self.EscortMenuTargetAssistance:RemoveSubMenus()
      end

      local DetectedItems = self.Detection:GetDetectedItems()
      self:F( DetectedItems )

      local DetectedTargets = false
  
      local DetectedMsgs = {}
      
      for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do

        local ClientEscortTargets = EscortGroupData.Detection
        --local EscortUnit = EscortGroupData:GetUnit( 1 )

        for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do
          self:F( { DetectedItemIndex, DetectedItem } )
          -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup.
  
          local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, EscortGroupData.EscortGroup, _DATABASE:GetPlayerSettings( self.EscortClient:GetPlayerName() ) )

          if ClientEscortGroupName == EscortGroupName then
          
            local DetectedMsg = DetectedItemReportSummary:Text("\n")
            DetectedMsgs[#DetectedMsgs+1] = DetectedMsg

            self:T( DetectedMsg )
  
            MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(),
              DetectedMsg,
              self.EscortMenuAttackNearbyTargets,
              ESCORT._AttackTarget,
              self,
              DetectedItem
            )
          else
            if self.EscortMenuTargetAssistance then
            
              local DetectedMsg = DetectedItemReportSummary:Text("\n")
              self:T( DetectedMsg )

              local MenuTargetAssistance = MENU_GROUP:New( self.EscortClient:GetGroup(), EscortGroupData.EscortName, self.EscortMenuTargetAssistance )
              MENU_GROUP_COMMAND:New( self.EscortClient:GetGroup(),
                DetectedMsg,
                MenuTargetAssistance,
                ESCORT._AssistTarget,
                self,
                EscortGroupData.EscortGroup,
                DetectedItem
              )
            end
          end
          
          DetectedTargets = true
                  
        end
      end
      self:F( DetectedMsgs )
      if DetectedTargets then
        self.EscortGroup:MessageToClient( "Reporting detected targets:\n" .. table.concat( DetectedMsgs, "\n" ), 20, self.EscortClient )
      else
        self.EscortGroup:MessageToClient( "No targets detected.", 10, self.EscortClient )
      end
      
      return true
  end
  
  return false
end
--- **Functional** - Train missile defence and deflection.
--
-- ===
--
-- ## Features:
--
--   * Track the missiles fired at you and other players, providing bearing and range information of the missiles towards the airplanes.
--   * Provide alerts of missile launches, including detailed information of the units launching, including bearing, range
--   * Provide alerts when a missile would have killed your aircraft.
--   * Provide alerts when the missile self destructs.
--   * Enable / Disable and Configure the Missile Trainer using the various menu options.
--
-- ===
--
-- ## Missions:
--
-- [MIT - Missile Trainer](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Functional/MissileTrainer)
--
-- ===
--
-- Uses the MOOSE messaging system to be alerted of any missiles fired, and when a missile would hit your aircraft,
-- the class will destroy the missile within a certain range, to avoid damage to your aircraft.
--
-- When running a mission where the missile trainer is used, the following radio menu structure ( 'Radio Menu' -> 'Other (F10)' -> 'MissileTrainer' ) options are available for the players:
--
--  * **Messages**: Menu to configure all messages.
--     * **Messages On**: Show all messages.
--     * **Messages Off**: Disable all messages.
--  * **Tracking**: Menu to configure missile tracking messages.
--     * **To All**: Shows missile tracking messages to all players.
--     * **To Target**: Shows missile tracking messages only to the player where the missile is targeted at.
--     * **Tracking On**: Show missile tracking messages.
--     * **Tracking Off**: Disable missile tracking messages.
--     * **Frequency Increase**: Increases the missile tracking message frequency with one second.
--     * **Frequency Decrease**: Decreases the missile tracking message frequency with one second.
--  * **Alerts**: Menu to configure alert messages.
--     * **To All**: Shows alert messages to all players.
--     * **To Target**: Shows alert messages only to the player where the missile is (was) targeted at.
--     * **Hits On**: Show missile hit alert messages.
--     * **Hits Off**: Disable missile hit alert messages.
--     * **Launches On**: Show missile launch messages.
--     * **Launches Off**: Disable missile launch messages.
--  * **Details**: Menu to configure message details.
--     * **Range On**: Shows range information when a missile is fired to a target.
--     * **Range Off**: Disable range information when a missile is fired to a target.
--     * **Bearing On**: Shows bearing information when a missile is fired to a target.
--     * **Bearing Off**: Disable bearing information when a missile is fired to a target.
--  * **Distance**: Menu to configure the distance when a missile needs to be destroyed when near to a player, during tracking. This will improve/influence hit calculation accuracy, but has the risk of damaging the aircraft when the missile reaches the aircraft before the distance is measured.
--     * **50 meter**: Destroys the missile when the distance to the aircraft is below or equal to 50 meter.
--     * **100 meter**: Destroys the missile when the distance to the aircraft is below or equal to 100 meter.
--     * **150 meter**: Destroys the missile when the distance to the aircraft is below or equal to 150 meter.
--     * **200 meter**: Destroys the missile when the distance to the aircraft is below or equal to 200 meter.
--     
-- # Developer Note
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE.
-- Therefore, this class is considered to be deprecated and superseded by the [Functional.Fox](https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Functional.Fox.html) class, which provides the same functionality.
--
-- ===
--
-- ### Authors: **FlightControl**
--
-- ### Contributions:
--
--    * **Stuka (Danny)**: Who you can search on the Eagle Dynamics Forums. Working together with Danny has resulted in the MISSILETRAINER class.
--      Danny has shared his ideas and together we made a design.
--      Together with the **476 virtual team**, we tested the MISSILETRAINER class, and got much positive feedback!
--    * **132nd Squadron**: Testing and optimizing the logic.
--
-- ===
--
-- @module Functional.MissileTrainer
-- @image Missile_Trainer.JPG

---
-- @type MISSILETRAINER
-- @field Core.Set#SET_CLIENT DBClients
-- @extends Core.Base#BASE


---
--
-- # Constructor:
--
-- Create a new MISSILETRAINER object with the @{#MISSILETRAINER.New} method:
--
--   * @{#MISSILETRAINER.New}: Creates a new MISSILETRAINER object taking the maximum distance to your aircraft to evaluate when a missile needs to be destroyed.
--
-- MISSILETRAINER will collect each unit declared in the mission with a skill level "Client" and "Player", and will monitor the missiles shot at those.
--
-- # Initialization:
--
-- A MISSILETRAINER object will behave differently based on the usage of initialization methods:
--
--  * @{#MISSILETRAINER.InitMessagesOnOff}: Sets by default the display of any message to be ON or OFF.
--  * @{#MISSILETRAINER.InitTrackingToAll}: Sets by default the missile tracking report for all players or only for those missiles targeted to you.
--  * @{#MISSILETRAINER.InitTrackingOnOff}: Sets by default the display of missile tracking report to be ON or OFF.
--  * @{#MISSILETRAINER.InitTrackingFrequency}: Increases, decreases the missile tracking message display frequency with the provided time interval in seconds.
--  * @{#MISSILETRAINER.InitAlertsToAll}: Sets by default the display of alerts to be shown to all players or only to you.
--  * @{#MISSILETRAINER.InitAlertsHitsOnOff}: Sets by default the display of hit alerts ON or OFF.
--  * @{#MISSILETRAINER.InitAlertsLaunchesOnOff}: Sets by default the display of launch alerts ON or OFF.
--  * @{#MISSILETRAINER.InitRangeOnOff}: Sets by default the display of range information of missiles ON of OFF.
--  * @{#MISSILETRAINER.InitBearingOnOff}: Sets by default the display of bearing information of missiles ON of OFF.
--  * @{#MISSILETRAINER.InitMenusOnOff}: Allows to configure the options through the radio menu.
--  
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE.
-- Therefore, this class is considered to be deprecated and superseded by the [Functional.Fox](https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Functional.Fox.html) class, which provides the same functionality.
--
-- @field #MISSILETRAINER
MISSILETRAINER = {
  ClassName = "MISSILETRAINER",
  TrackingMissiles = {},
}

function MISSILETRAINER._Alive( Client, self )

  if self.Briefing then
    Client:Message( self.Briefing, 15, "Trainer" )
  end

  if self.MenusOnOff == true then
    Client:Message( "Use the 'Radio Menu' -> 'Other (F10)' -> 'Missile Trainer' menu options to change the Missile Trainer settings (for all players).", 15, "Trainer" )

    Client.MainMenu = MENU_GROUP:New( Client:GetGroup(), "Missile Trainer", nil ) -- Menu#MENU_GROUP

    Client.MenuMessages = MENU_GROUP:New( Client:GetGroup(), "Messages", Client.MainMenu )
    Client.MenuOn = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Messages On", Client.MenuMessages, self._MenuMessages, { MenuSelf = self, MessagesOnOff = true } )
    Client.MenuOff = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Messages Off", Client.MenuMessages, self._MenuMessages, { MenuSelf = self, MessagesOnOff = false } )

    Client.MenuTracking = MENU_GROUP:New( Client:GetGroup(), "Tracking", Client.MainMenu )
    Client.MenuTrackingToAll = MENU_GROUP_COMMAND:New( Client:GetGroup(), "To All", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingToAll = true } )
    Client.MenuTrackingToTarget = MENU_GROUP_COMMAND:New( Client:GetGroup(), "To Target", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingToAll = false } )
    Client.MenuTrackOn = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Tracking On", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingOnOff = true } )
    Client.MenuTrackOff = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Tracking Off", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingOnOff = false } )
    Client.MenuTrackIncrease = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Frequency Increase", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingFrequency = -1 } )
    Client.MenuTrackDecrease = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Frequency Decrease", Client.MenuTracking, self._MenuMessages, { MenuSelf = self, TrackingFrequency = 1 } )

    Client.MenuAlerts = MENU_GROUP:New( Client:GetGroup(), "Alerts", Client.MainMenu )
    Client.MenuAlertsToAll = MENU_GROUP_COMMAND:New( Client:GetGroup(), "To All", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsToAll = true } )
    Client.MenuAlertsToTarget = MENU_GROUP_COMMAND:New( Client:GetGroup(), "To Target", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsToAll = false } )
    Client.MenuHitsOn = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Hits On", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsHitsOnOff = true } )
    Client.MenuHitsOff = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Hits Off", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsHitsOnOff = false } )
    Client.MenuLaunchesOn = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Launches On", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsLaunchesOnOff = true } )
    Client.MenuLaunchesOff = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Launches Off", Client.MenuAlerts, self._MenuMessages, { MenuSelf = self, AlertsLaunchesOnOff = false } )

    Client.MenuDetails = MENU_GROUP:New( Client:GetGroup(), "Details", Client.MainMenu )
    Client.MenuDetailsDistanceOn = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Range On", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsRangeOnOff = true } )
    Client.MenuDetailsDistanceOff = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Range Off", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsRangeOnOff = false } )
    Client.MenuDetailsBearingOn = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Bearing On", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsBearingOnOff = true } )
    Client.MenuDetailsBearingOff = MENU_GROUP_COMMAND:New( Client:GetGroup(), "Bearing Off", Client.MenuDetails, self._MenuMessages, { MenuSelf = self, DetailsBearingOnOff = false } )

    Client.MenuDistance = MENU_GROUP:New( Client:GetGroup(), "Set distance to plane", Client.MainMenu )
    Client.MenuDistance50 = MENU_GROUP_COMMAND:New( Client:GetGroup(), "50 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 50 / 1000 } )
    Client.MenuDistance100 = MENU_GROUP_COMMAND:New( Client:GetGroup(), "100 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 100 / 1000 } )
    Client.MenuDistance150 = MENU_GROUP_COMMAND:New( Client:GetGroup(), "150 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 150 / 1000 } )
    Client.MenuDistance200 = MENU_GROUP_COMMAND:New( Client:GetGroup(), "200 meter", Client.MenuDistance, self._MenuMessages, { MenuSelf = self, Distance = 200 / 1000 } )
  else
    if Client.MainMenu then
      Client.MainMenu:Remove()
    end
  end

  local ClientID = Client:GetID()
  self:T( ClientID )
  if not self.TrackingMissiles[ClientID] then
    self.TrackingMissiles[ClientID] = {}
  end
  self.TrackingMissiles[ClientID].Client = Client
  if not self.TrackingMissiles[ClientID].MissileData then
    self.TrackingMissiles[ClientID].MissileData = {}
  end
end

--- Creates the main object which is handling missile tracking.
-- When a missile is fired a SCHEDULER is set off that follows the missile. When near a certain a client player, the missile will be destroyed.
-- @param #MISSILETRAINER self
-- @param #number Distance The distance in meters when a tracked missile needs to be destroyed when close to a player.
-- @param #string Briefing (Optional) Will show a text to the players when starting their mission. Can be used for briefing purposes.
-- @return #MISSILETRAINER
function MISSILETRAINER:New( Distance, Briefing )
  local self = BASE:Inherit( self, BASE:New() )
  self:F( Distance )

  if Briefing then
    self.Briefing = Briefing
  end

  self.Schedulers = {}
  self.SchedulerID = 0

  self.MessageInterval = 2
  self.MessageLastTime = timer.getTime()

  self.Distance = Distance / 1000

  self:HandleEvent( EVENTS.Shot )

  self.DBClients = SET_CLIENT:New():FilterStart()


--  for ClientID, Client in pairs( self.DBClients.Database ) do
--      self:F( "ForEach:" .. Client.UnitName )
--      Client:Alive( self._Alive, self )
--  end
--
  self.DBClients:ForEachClient(
    function( Client )
      self:F( "ForEach:" .. Client.UnitName )
      Client:Alive( self._Alive, self )
    end
  )



--  	self.DB:ForEachClient(
--  	 -- @param Wrapper.Client#CLIENT Client
--  	 function( Client )
--
--        ... actions ...
--
--  	 end
--  	)

  self.MessagesOnOff = true

  self.TrackingToAll = false
  self.TrackingOnOff = true
  self.TrackingFrequency = 3

  self.AlertsToAll = true
  self.AlertsHitsOnOff = true
  self.AlertsLaunchesOnOff = true

  self.DetailsRangeOnOff = true
  self.DetailsBearingOnOff = true

  self.MenusOnOff = true

  self.TrackingMissiles = {}

  self.TrackingScheduler = SCHEDULER:New( self, self._TrackMissiles, {}, 0.5, 0.05, 0 )

  return self
end

-- Initialization methods.



--- Sets by default the display of any message to be ON or OFF.
-- @param #MISSILETRAINER self
-- @param #boolean MessagesOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitMessagesOnOff( MessagesOnOff )
  self:F( MessagesOnOff )

  self.MessagesOnOff = MessagesOnOff
  if self.MessagesOnOff == true then
    MESSAGE:New( "Messages ON", 15, "Menu" ):ToAll()
  else
    MESSAGE:New( "Messages OFF", 15, "Menu" ):ToAll()
  end

  return self
end

--- Sets by default the missile tracking report for all players or only for those missiles targeted to you.
-- @param #MISSILETRAINER self
-- @param #boolean TrackingToAll true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitTrackingToAll( TrackingToAll )
  self:F( TrackingToAll )

  self.TrackingToAll = TrackingToAll
  if self.TrackingToAll == true then
    MESSAGE:New( "Missile tracking to all players ON", 15, "Menu" ):ToAll()
  else
    MESSAGE:New( "Missile tracking to all players OFF", 15, "Menu" ):ToAll()
  end

  return self
end

--- Sets by default the display of missile tracking report to be ON or OFF.
-- @param #MISSILETRAINER self
-- @param #boolean TrackingOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitTrackingOnOff( TrackingOnOff )
  self:F( TrackingOnOff )

  self.TrackingOnOff = TrackingOnOff
  if self.TrackingOnOff == true then
    MESSAGE:New( "Missile tracking ON", 15, "Menu" ):ToAll()
  else
    MESSAGE:New( "Missile tracking OFF", 15, "Menu" ):ToAll()
  end

  return self
end

--- Increases, decreases the missile tracking message display frequency with the provided time interval in seconds.
-- The default frequency is a 3 second interval, so the Tracking Frequency parameter specifies the increase or decrease from the default 3 seconds or the last frequency update.
-- @param #MISSILETRAINER self
-- @param #number TrackingFrequency Provide a negative or positive value in seconds to incraese or decrease the display frequency.
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitTrackingFrequency( TrackingFrequency )
  self:F( TrackingFrequency )

  self.TrackingFrequency = self.TrackingFrequency + TrackingFrequency
  if self.TrackingFrequency < 0.5 then
    self.TrackingFrequency = 0.5
  end
  if self.TrackingFrequency then
    MESSAGE:New( "Missile tracking frequency is " .. self.TrackingFrequency .. " seconds.", 15, "Menu" ):ToAll()
  end

  return self
end

--- Sets by default the display of alerts to be shown to all players or only to you.
-- @param #MISSILETRAINER self
-- @param #boolean AlertsToAll true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitAlertsToAll( AlertsToAll )
  self:F( AlertsToAll )

  self.AlertsToAll = AlertsToAll
  if self.AlertsToAll == true then
    MESSAGE:New( "Alerts to all players ON", 15, "Menu" ):ToAll()
  else
    MESSAGE:New( "Alerts to all players OFF", 15, "Menu" ):ToAll()
  end

  return self
end

--- Sets by default the display of hit alerts ON or OFF.
-- @param #MISSILETRAINER self
-- @param #boolean AlertsHitsOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitAlertsHitsOnOff( AlertsHitsOnOff )
  self:F( AlertsHitsOnOff )

  self.AlertsHitsOnOff = AlertsHitsOnOff
  if self.AlertsHitsOnOff == true then
    MESSAGE:New( "Alerts Hits ON", 15, "Menu" ):ToAll()
  else
    MESSAGE:New( "Alerts Hits OFF", 15, "Menu" ):ToAll()
  end

  return self
end

--- Sets by default the display of launch alerts ON or OFF.
-- @param #MISSILETRAINER self
-- @param #boolean AlertsLaunchesOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitAlertsLaunchesOnOff( AlertsLaunchesOnOff )
  self:F( AlertsLaunchesOnOff )

  self.AlertsLaunchesOnOff = AlertsLaunchesOnOff
  if self.AlertsLaunchesOnOff == true then
    MESSAGE:New( "Alerts Launches ON", 15, "Menu" ):ToAll()
  else
    MESSAGE:New( "Alerts Launches OFF", 15, "Menu" ):ToAll()
  end

  return self
end

--- Sets by default the display of range information of missiles ON of OFF.
-- @param #MISSILETRAINER self
-- @param #boolean DetailsRangeOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitRangeOnOff( DetailsRangeOnOff )
  self:F( DetailsRangeOnOff )

  self.DetailsRangeOnOff = DetailsRangeOnOff
  if self.DetailsRangeOnOff == true then
    MESSAGE:New( "Range display ON", 15, "Menu" ):ToAll()
  else
    MESSAGE:New( "Range display OFF", 15, "Menu" ):ToAll()
  end

  return self
end

--- Sets by default the display of bearing information of missiles ON of OFF.
-- @param #MISSILETRAINER self
-- @param #boolean DetailsBearingOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitBearingOnOff( DetailsBearingOnOff )
  self:F( DetailsBearingOnOff )

  self.DetailsBearingOnOff = DetailsBearingOnOff
  if self.DetailsBearingOnOff == true then
    MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll()
  else
    MESSAGE:New( "Bearing display OFF", 15, "Menu" ):ToAll()
  end

  return self
end

--- Enables / Disables the menus.
-- @param #MISSILETRAINER self
-- @param #boolean MenusOnOff true or false
-- @return #MISSILETRAINER self
function MISSILETRAINER:InitMenusOnOff( MenusOnOff )
  self:F( MenusOnOff )

  self.MenusOnOff = MenusOnOff
  if self.MenusOnOff == true then
    MESSAGE:New( "Menus are ENABLED (only when a player rejoins a slot)", 15, "Menu" ):ToAll()
  else
    MESSAGE:New( "Menus are DISABLED", 15, "Menu" ):ToAll()
  end

  return self
end


-- Menu functions

function MISSILETRAINER._MenuMessages( MenuParameters )

  local self = MenuParameters.MenuSelf

  if MenuParameters.MessagesOnOff ~= nil then
    self:InitMessagesOnOff( MenuParameters.MessagesOnOff )
  end

  if MenuParameters.TrackingToAll ~= nil then
    self:InitTrackingToAll( MenuParameters.TrackingToAll )
  end

  if MenuParameters.TrackingOnOff ~= nil then
    self:InitTrackingOnOff( MenuParameters.TrackingOnOff )
  end

  if MenuParameters.TrackingFrequency ~= nil then
    self:InitTrackingFrequency( MenuParameters.TrackingFrequency )
  end

  if MenuParameters.AlertsToAll ~= nil then
    self:InitAlertsToAll( MenuParameters.AlertsToAll )
  end

  if MenuParameters.AlertsHitsOnOff ~= nil then
    self:InitAlertsHitsOnOff( MenuParameters.AlertsHitsOnOff )
  end

  if MenuParameters.AlertsLaunchesOnOff ~= nil then
    self:InitAlertsLaunchesOnOff( MenuParameters.AlertsLaunchesOnOff )
  end

  if MenuParameters.DetailsRangeOnOff ~= nil then
    self:InitRangeOnOff( MenuParameters.DetailsRangeOnOff )
  end

  if MenuParameters.DetailsBearingOnOff ~= nil then
    self:InitBearingOnOff( MenuParameters.DetailsBearingOnOff )
  end

  if MenuParameters.Distance ~= nil then
    self.Distance = MenuParameters.Distance
    MESSAGE:New( "Hit detection distance set to " .. ( self.Distance * 1000 ) .. " meters", 15, "Menu" ):ToAll()
  end

end

--- Detects if an SA site was shot with an anti radiation missile. In this case, take evasive actions based on the skill level set within the ME.
-- @param #MISSILETRAINER self
-- @param Core.Event#EVENTDATA EventData
function MISSILETRAINER:OnEventShot( EVentData )
  self:F( { EVentData } )

  local TrainerSourceDCSUnit = EVentData.IniDCSUnit
  local TrainerSourceDCSUnitName = EVentData.IniDCSUnitName
  local TrainerWeapon = EVentData.Weapon -- Identify the weapon fired
  local TrainerWeaponName = EVentData.WeaponName	-- return weapon type

  self:T( "Missile Launched = " .. TrainerWeaponName )

  local TrainerTargetDCSUnit = TrainerWeapon:getTarget() -- Identify target
  if TrainerTargetDCSUnit then
    local TrainerTargetDCSUnitName = Unit.getName( TrainerTargetDCSUnit )
    local TrainerTargetSkill =  _DATABASE.Templates.Units[TrainerTargetDCSUnitName].Template.skill

    self:T(TrainerTargetDCSUnitName )

    local Client = self.DBClients:FindClient( TrainerTargetDCSUnitName )
    if Client then

      local TrainerSourceUnit = UNIT:Find( TrainerSourceDCSUnit )
      local TrainerTargetUnit = UNIT:Find( TrainerTargetDCSUnit )

      if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then

        local Message = MESSAGE:New(
          string.format( "%s launched a %s",
            TrainerSourceUnit:GetTypeName(),
            TrainerWeaponName
          ) .. self:_AddRange( Client, TrainerWeapon ) .. self:_AddBearing( Client, TrainerWeapon ), 5, "Launch Alert" )

        if self.AlertsToAll then
          Message:ToAll()
        else
          Message:ToClient( Client )
        end
      end

      local ClientID = Client:GetID()
      self:T( ClientID )
      local MissileData = {}
      MissileData.TrainerSourceUnit = TrainerSourceUnit
      MissileData.TrainerWeapon = TrainerWeapon
      MissileData.TrainerTargetUnit = TrainerTargetUnit
      MissileData.TrainerWeaponTypeName = TrainerWeapon:getTypeName()
      MissileData.TrainerWeaponLaunched = true
      table.insert( self.TrackingMissiles[ClientID].MissileData, MissileData )
      --self:T( self.TrackingMissiles )
    end
  else
     -- TODO: some weapons don't know the target unit... Need to develop a workaround for this.
 if ( TrainerWeapon:getTypeName() == "9M311" ) then
		SCHEDULER:New( TrainerWeapon, TrainerWeapon.destroy, {}, 1 )
		else
		end
  end
end

function MISSILETRAINER:_AddRange( Client, TrainerWeapon )

  local RangeText = ""

  if self.DetailsRangeOnOff then

    local PositionMissile = TrainerWeapon:getPoint()
    local TargetVec3 = Client:GetVec3()

    local Range = ( ( PositionMissile.x - TargetVec3.x )^2 +
      ( PositionMissile.y - TargetVec3.y )^2 +
      ( PositionMissile.z - TargetVec3.z )^2
      ) ^ 0.5 / 1000

    RangeText = string.format( ", at %4.2fkm", Range )
  end

  return RangeText
end

function MISSILETRAINER:_AddBearing( Client, TrainerWeapon )

  local BearingText = ""

  if self.DetailsBearingOnOff then

    local PositionMissile = TrainerWeapon:getPoint()
    local TargetVec3 = Client:GetVec3()

    self:T2( { TargetVec3, PositionMissile })

    local DirectionVector = { x = PositionMissile.x - TargetVec3.x, y = PositionMissile.y - TargetVec3.y, z = PositionMissile.z - TargetVec3.z }
    local DirectionRadians = math.atan2( DirectionVector.z, DirectionVector.x )

    if DirectionRadians < 0 then
      DirectionRadians = DirectionRadians + 2 * math.pi
    end
    local DirectionDegrees = DirectionRadians * 180 / math.pi

    BearingText = string.format( ", %d degrees", DirectionDegrees )
  end

  return BearingText
end


function MISSILETRAINER:_TrackMissiles()
  self:F2()


  local ShowMessages = false
  if self.MessagesOnOff and self.MessageLastTime + self.TrackingFrequency <= timer.getTime() then
    self.MessageLastTime = timer.getTime()
    ShowMessages = true
  end

  -- ALERTS PART

  -- Loop for all Player Clients to check the alerts and deletion of missiles.
  for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do

    local Client = ClientData.Client

    if Client and Client:IsAlive() then

      for MissileDataID, MissileData in pairs( ClientData.MissileData ) do
        self:T3( MissileDataID )

        local TrainerSourceUnit = MissileData.TrainerSourceUnit
        local TrainerWeapon = MissileData.TrainerWeapon
        local TrainerTargetUnit = MissileData.TrainerTargetUnit
        local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName
        local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched

        if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then
          local PositionMissile = TrainerWeapon:getPosition().p
          local TargetVec3 = Client:GetVec3()

          local Distance = ( ( PositionMissile.x - TargetVec3.x )^2 +
            ( PositionMissile.y - TargetVec3.y )^2 +
            ( PositionMissile.z - TargetVec3.z )^2
            ) ^ 0.5 / 1000

          if Distance <= self.Distance then
            -- Hit alert
            TrainerWeapon:destroy()
            if self.MessagesOnOff == true and self.AlertsHitsOnOff == true then

              self:T( "killed" )

              local Message = MESSAGE:New(
                string.format( "%s launched by %s killed %s",
                  TrainerWeapon:getTypeName(),
                  TrainerSourceUnit:GetTypeName(),
                  TrainerTargetUnit:GetPlayerName()
                ), 15, "Hit Alert" )

              if self.AlertsToAll == true then
                Message:ToAll()
              else
                Message:ToClient( Client )
              end

              MissileData = nil
              table.remove( ClientData.MissileData, MissileDataID )
              self:T(ClientData.MissileData)
            end
          end
        else
          if not ( TrainerWeapon and TrainerWeapon:isExist() ) then
            if self.MessagesOnOff == true and self.AlertsLaunchesOnOff == true then
              -- Weapon does not exist anymore. Delete from Table
              local Message = MESSAGE:New(
                string.format( "%s launched by %s self destructed!",
                  TrainerWeaponTypeName,
                  TrainerSourceUnit:GetTypeName()
                ), 5, "Tracking" )

              if self.AlertsToAll == true then
                Message:ToAll()
              else
                Message:ToClient( Client )
              end
            end
            MissileData = nil
            table.remove( ClientData.MissileData, MissileDataID )
            self:T( ClientData.MissileData )
          end
        end
      end
    else
      self.TrackingMissiles[ClientDataID] = nil
    end
  end

  if ShowMessages == true and self.MessagesOnOff == true and self.TrackingOnOff == true then -- Only do this when tracking information needs to be displayed.

    -- TRACKING PART

    -- For the current client, the missile range and bearing details are displayed To the Player Client.
    -- For the other clients, the missile range and bearing details are displayed To the other Player Clients.
    -- To achieve this, a cross loop is done for each Player Client <-> Other Player Client missile information.

    -- Main Player Client loop
    for ClientDataID, ClientData in pairs( self.TrackingMissiles ) do

      local Client = ClientData.Client
      --self:T2( { Client:GetName() } )


      ClientData.MessageToClient = ""
      ClientData.MessageToAll = ""

      -- Other Players Client loop
      for TrackingDataID, TrackingData in pairs( self.TrackingMissiles ) do

        for MissileDataID, MissileData in pairs( TrackingData.MissileData ) do
          --self:T3( MissileDataID )

          local TrainerSourceUnit = MissileData.TrainerSourceUnit
          local TrainerWeapon = MissileData.TrainerWeapon
          local TrainerTargetUnit = MissileData.TrainerTargetUnit
          local TrainerWeaponTypeName = MissileData.TrainerWeaponTypeName
          local TrainerWeaponLaunched = MissileData.TrainerWeaponLaunched

          if Client and Client:IsAlive() and TrainerSourceUnit and TrainerSourceUnit:IsAlive() and TrainerWeapon and TrainerWeapon:isExist() and TrainerTargetUnit and TrainerTargetUnit:IsAlive() then

            if ShowMessages == true then
              local TrackingTo
              TrackingTo = string.format( "  -> %s",
                TrainerWeaponTypeName
              )

              if ClientDataID == TrackingDataID then
                if ClientData.MessageToClient == "" then
                  ClientData.MessageToClient = "Missiles to You:\n"
                end
                ClientData.MessageToClient = ClientData.MessageToClient .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. "\n"
              else
                if self.TrackingToAll == true then
                  if ClientData.MessageToAll == "" then
                    ClientData.MessageToAll = "Missiles to other Players:\n"
                  end
                  ClientData.MessageToAll = ClientData.MessageToAll .. TrackingTo .. self:_AddRange( ClientData.Client, TrainerWeapon ) .. self:_AddBearing( ClientData.Client, TrainerWeapon ) .. " ( " .. TrainerTargetUnit:GetPlayerName()  ..   " )\n"
                end
              end
            end
          end
        end
      end

      -- Once the Player Client and the Other Player Client tracking messages are prepared, show them.
      if ClientData.MessageToClient ~= "" or ClientData.MessageToAll ~= "" then
        local Message = MESSAGE:New( ClientData.MessageToClient .. ClientData.MessageToAll, 1, "Tracking" ):ToClient( Client )
      end
    end
  end

  return true
end
--- **Functional** - Monitor airbase traffic and regulate speed while taxiing.
--
-- ===
--
-- ## Features:
-- 
--   * Monitor speed of the airplanes of players during taxi.
--   * Communicate ATC ground operations.
--   * Kick speeding players during taxi.
-- 
-- ===
-- 
-- ## Missions: None
--
-- ===
--
-- ### Contributions: Dutch Baron - Concept & Testing
-- ### Author: FlightControl - Framework Design &  Programming
-- ### Refactoring to use the Runway auto-detection: Applevangelist
-- @date August 2022
-- Last Update Feb 2025
--
-- ===
-- 
-- @module Functional.ATC_Ground
-- @image Air_Traffic_Control_Ground_Operations.JPG

---
-- @type ATC_GROUND
-- @field Core.Set#SET_CLIENT SetClient
-- @extends Core.Base#BASE

--- [DEPRECATED, use ATC_GROUND_UNIVERSAL] Base class for ATC\_GROUND implementations.
-- @field #ATC_GROUND
ATC_GROUND = {
  ClassName = "ATC_GROUND",
  SetClient = nil,
  Airbases = nil,
  AirbaseNames = nil,
}

---
-- @type ATC_GROUND.AirbaseNames
-- @list <#string>


--- [DEPRECATED, use ATC_GROUND_UNIVERSAL] Creates a new ATC\_GROUND object.
-- @param #ATC_GROUND self
-- @param Airbases A table of Airbase Names.
-- @return #ATC_GROUND self
function ATC_GROUND:New( Airbases, AirbaseList )

  -- Inherits from BASE
  local self = BASE:Inherit( self, BASE:New() ) -- #ATC_GROUND
  self:T( { self.ClassName, Airbases } )

  self.Airbases = Airbases
  self.AirbaseList = AirbaseList
  
  self.SetClient = SET_CLIENT:New():FilterCategories( "plane" ):FilterStart()
  

  for AirbaseID, Airbase in pairs( self.Airbases ) do
    -- Specified ZoneBoundary is used if set or Airbase radius by default
    if Airbase.ZoneBoundary then
      Airbase.ZoneBoundary = ZONE_POLYGON_BASE:New( "Boundary " .. AirbaseID, Airbase.ZoneBoundary )
    else
      Airbase.ZoneBoundary = _DATABASE:FindAirbase( AirbaseID ):GetZone()
    end

    Airbase.ZoneRunways = {}
    if Airbase.PointsRunways then
      for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do
        Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway )
      end
    end
    Airbase.Monitor = self.AirbaseList and false or true -- When AirbaseList is not given, monitor every Airbase, otherwise don't monitor any (yet). 
  end

  -- Now activate the monitoring for the airbases that need to be monitored.
  for AirbaseID, AirbaseName in pairs( self.AirbaseList or {} ) do
    self.Airbases[AirbaseName].Monitor = true
  end

  self.SetClient:ForEachClient(
    -- @param Wrapper.Client#CLIENT Client
    function( Client )
      Client:SetState( self, "Speeding", false )
      Client:SetState( self, "Warnings", 0)
      Client:SetState( self, "IsOffRunway", false )
      Client:SetState( self, "OffRunwayWarnings", 0 )
      Client:SetState( self, "Taxi", false )
    end
  )

  -- This is simple slot blocker is used on the server.  
  SSB = USERFLAG:New( "SSB" )
  SSB:Set( 100 )
  
  return self
end

--- Smoke the airbases runways.
-- @param #ATC_GROUND self
-- @param Utilities.Utils#SMOKECOLOR SmokeColor The color of the smoke around the runways.
-- @return #ATC_GROUND self
function ATC_GROUND:SmokeRunways( SmokeColor )

  for AirbaseID, Airbase in pairs( self.Airbases ) do
    for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do
      Airbase.ZoneRunways[PointsRunwayID]:SmokeZone( SmokeColor )
      end
  end
end

--- Set the maximum speed in meters per second (Mps) until the player gets kicked.
-- An airbase can be specified to set the kick speed for.
-- @param #ATC_GROUND self
-- @param #number KickSpeed The speed in Mps.
-- @param Wrapper.Airbase#AIRBASE Airbase (optional) The airbase to set the kick speed for.
-- @return #ATC_GROUND self
-- @usage
-- 
--   -- Declare Atc_Ground using one of those, depending on the map.
-- 
--   Atc_Ground = ATC_GROUND_CAUCAUS:New()
--   Atc_Ground = ATC_GROUND_NEVADA:New()
--   Atc_Ground = ATC_GROUND_NORMANDY:New()
--   Atc_Ground = ATC_GROUND_PERSIANGULF:New()
--   
--   -- Then use one of these methods...
-- 
--   Atc_Ground:SetKickSpeed( UTILS.KmphToMps( 80 ) ) -- Kick the players at 80 kilometers per hour
-- 
--   Atc_Ground:SetKickSpeed( UTILS.MiphToMps( 100 ) ) -- Kick the players at 100 miles per hour
-- 
--   Atc_Ground:SetKickSpeed( 24 ) -- Kick the players at 24 meters per second ( 24 * 3.6 = 86.4 kilometers per hour )
-- 
function ATC_GROUND:SetKickSpeed( KickSpeed, Airbase )

  if not Airbase then
    self.KickSpeed = KickSpeed
  else
    self.Airbases[Airbase].KickSpeed = KickSpeed
  end
  
  return self
end

--- Set the maximum speed in Kmph until the player gets kicked.
-- @param #ATC_GROUND self
-- @param #number KickSpeed Set the speed in Kmph.
-- @param Wrapper.Airbase#AIRBASE Airbase (optional) The airbase to set the kick speed for.
-- @return #ATC_GROUND self
-- 
--   Atc_Ground:SetKickSpeedKmph( 80 ) -- Kick the players at 80 kilometers per hour
-- 
function ATC_GROUND:SetKickSpeedKmph( KickSpeed, Airbase )

  self:SetKickSpeed( UTILS.KmphToMps( KickSpeed ), Airbase )
  
  return self
end

--- Set the maximum speed in Miph until the player gets kicked.
-- @param #ATC_GROUND self
-- @param #number KickSpeedMiph Set the speed in Mph.
-- @param Wrapper.Airbase#AIRBASE Airbase (optional) The airbase to set the kick speed for.
-- @return #ATC_GROUND self
-- 
--   Atc_Ground:SetKickSpeedMiph( 100 ) -- Kick the players at 100 miles per hour
-- 
function ATC_GROUND:SetKickSpeedMiph( KickSpeedMiph, Airbase )

  self:SetKickSpeed( UTILS.MiphToMps( KickSpeedMiph ), Airbase )
  
  return self
end


--- Set the maximum kick speed in meters per second (Mps) until the player gets kicked.
-- There are no warnings given if this speed is reached, and is to prevent players to take off from the airbase!
-- An airbase can be specified to set the maximum kick speed for.
-- @param #ATC_GROUND self
-- @param #number MaximumKickSpeed The speed in Mps.
-- @param Wrapper.Airbase#AIRBASE Airbase (optional) The airbase to set the kick speed for.
-- @return #ATC_GROUND self
-- @usage
-- 
--   -- Declare Atc_Ground using one of those, depending on the map.
-- 
--   Atc_Ground = ATC_GROUND_CAUCAUS:New()
--   Atc_Ground = ATC_GROUND_NEVADA:New()
--   Atc_Ground = ATC_GROUND_NORMANDY:New()
--   Atc_Ground = ATC_GROUND_PERSIANGULF:New()
--   
--   -- Then use one of these methods...
-- 
--   Atc_Ground:SetMaximumKickSpeed( UTILS.KmphToMps( 80 ) ) -- Kick the players at 80 kilometers per hour
-- 
--   Atc_Ground:SetMaximumKickSpeed( UTILS.MiphToMps( 100 ) ) -- Kick the players at 100 miles per hour
-- 
--   Atc_Ground:SetMaximumKickSpeed( 24 ) -- Kick the players at 24 meters per second ( 24 * 3.6 = 86.4 kilometers per hour )
-- 
function ATC_GROUND:SetMaximumKickSpeed( MaximumKickSpeed, Airbase )

  if not Airbase then
    self.MaximumKickSpeed = MaximumKickSpeed
  else
    self.Airbases[Airbase].MaximumKickSpeed = MaximumKickSpeed
  end
  
  return self
end

--- Set the maximum kick speed in kilometers per hour (Kmph) until the player gets kicked.
-- There are no warnings given if this speed is reached, and is to prevent players to take off from the airbase!
-- An airbase can be specified to set the maximum kick speed for.
-- @param #ATC_GROUND self
-- @param #number MaximumKickSpeed Set the speed in Kmph.
-- @param Wrapper.Airbase#AIRBASE Airbase (optional) The airbase to set the kick speed for.
-- @return #ATC_GROUND self
-- 
--   Atc_Ground:SetMaximumKickSpeedKmph( 150 ) -- Kick the players at 150 kilometers per hour
-- 
function ATC_GROUND:SetMaximumKickSpeedKmph( MaximumKickSpeed, Airbase )

  self:SetMaximumKickSpeed( UTILS.KmphToMps( MaximumKickSpeed ), Airbase )
  
  return self
end

--- Set the maximum kick speed in miles per hour (Miph) until the player gets kicked.
-- There are no warnings given if this speed is reached, and is to prevent players to take off from the airbase!
-- An airbase can be specified to set the maximum kick speed for.
-- @param #ATC_GROUND self
-- @param #number MaximumKickSpeedMiph Set the speed in Mph.
-- @param Wrapper.Airbase#AIRBASE Airbase (optional) The airbase to set the kick speed for.
-- @return #ATC_GROUND self
-- 
--   Atc_Ground:SetMaximumKickSpeedMiph( 100 ) -- Kick the players at 100 miles per hour
-- 
function ATC_GROUND:SetMaximumKickSpeedMiph( MaximumKickSpeedMiph, Airbase )

  self:SetMaximumKickSpeed( UTILS.MiphToMps( MaximumKickSpeedMiph ), Airbase )
  
  return self
end

-- @param #ATC_GROUND self
function ATC_GROUND:_AirbaseMonitor()

  self.SetClient:ForEachClient(
    -- @param Wrapper.Client#CLIENT Client
    function( Client )

      if Client:IsAlive() then

        local IsOnGround = Client:InAir() == false

        for AirbaseID, AirbaseMeta in pairs( self.Airbases ) do
          self:T( AirbaseID, AirbaseMeta.KickSpeed )
  
          if AirbaseMeta.Monitor == true and Client:IsInZone( AirbaseMeta.ZoneBoundary )  then

            local NotInRunwayZone = true
            for ZoneRunwayID, ZoneRunway in pairs( AirbaseMeta.ZoneRunways ) do
              NotInRunwayZone = ( Client:IsNotInZone( ZoneRunway ) == true ) and NotInRunwayZone or false
            end

            if NotInRunwayZone then
              
              if IsOnGround then
                local Taxi = Client:GetState( self, "Taxi" )
                self:T( Taxi )
                if Taxi == false then
                  local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed )
                  Client:Message( "Welcome to " .. AirbaseID .. ". The maximum taxiing speed is " .. 
                                  Velocity:ToString() , 20, "ATC" )
                  Client:SetState( self, "Taxi", true )
                end
  
                -- TODO: GetVelocityKMH function usage
                local Velocity = VELOCITY_POSITIONABLE:New( Client )
                --MESSAGE:New( "Velocity = " .. Velocity:ToString(), 1 ):ToAll()
                local IsAboveRunway = Client:IsAboveRunway()
                self:T( IsAboveRunway, IsOnGround )
  
                if IsOnGround then
                  local Speeding = false
                  if AirbaseMeta.MaximumKickSpeed then 
                    if Velocity:Get() > AirbaseMeta.MaximumKickSpeed then
                      Speeding = true
                    end
                  else
                    if Velocity:Get() > self.MaximumKickSpeed then
                      Speeding = true
                    end
                  end
                  if Speeding == true then
                    MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. 
                                 " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll()
                    Client:Destroy()
                    Client:SetState( self, "Speeding", false )
                    Client:SetState( self, "Warnings", 0 )
                  end
                end                  
                  
  
                if IsOnGround then
  
                  local Speeding = false
                  if AirbaseMeta.KickSpeed then -- If there is a speed defined for the airbase, use that only.
                    if Velocity:Get() > AirbaseMeta.KickSpeed then
                      Speeding = true
                    end
                  else
                    if Velocity:Get() > self.KickSpeed then
                      Speeding = true
                    end
                  end
                  if Speeding == true then  
                    local IsSpeeding = Client:GetState( self, "Speeding" )
  
                    if IsSpeeding == true then
                      local SpeedingWarnings = Client:GetState( self, "Warnings" )
                      self:T( SpeedingWarnings )
  
                      if SpeedingWarnings <= 3 then
                        Client:Message( "Warning " .. SpeedingWarnings .. "/3! Airbase traffic rule violation! Slow down now! Your speed is " .. 
                                        Velocity:ToString(), 5, "ATC" )
                        Client:SetState( self, "Warnings", SpeedingWarnings + 1 )
                      else
                        MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll()
                        -- @param Wrapper.Client#CLIENT Client
                        Client:Destroy()
                        Client:SetState( self, "Speeding", false )
                        Client:SetState( self, "Warnings", 0 )
                      end
  
                    else
                      Client:Message( "Attention! You are speeding on the taxiway, slow down! Your speed is " .. 
                                      Velocity:ToString(), 5, "ATC" )
                      Client:SetState( self, "Speeding", true )
                      Client:SetState( self, "Warnings", 1 )
                    end
  
                  else
                    Client:SetState( self, "Speeding", false )
                    Client:SetState( self, "Warnings", 0 )
                  end
                end

                if IsOnGround and not IsAboveRunway then
  
                  local IsOffRunway = Client:GetState( self, "IsOffRunway" )
  
                  if IsOffRunway == true then
                    local OffRunwayWarnings = Client:GetState( self, "OffRunwayWarnings" )
                    self:T( OffRunwayWarnings )
  
                    if OffRunwayWarnings <= 3 then
                      Client:Message( "Warning " .. OffRunwayWarnings .. "/3! Airbase traffic rule violation! Get back on the taxi immediately!", 5, "ATC" )
                      Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 )
                    else
                      MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll()
                      -- @param Wrapper.Client#CLIENT Client
                      Client:Destroy()
                      Client:SetState( self, "IsOffRunway", false )
                      Client:SetState( self, "OffRunwayWarnings", 0 )
                    end
                  else
                    Client:Message( "Attention! You are off the taxiway. Get back on the taxiway immediately!", 5, "ATC" )
                    Client:SetState( self, "IsOffRunway", true )
                    Client:SetState( self, "OffRunwayWarnings", 1 )
                  end
  
                else
                  Client:SetState( self, "IsOffRunway", false )
                  Client:SetState( self, "OffRunwayWarnings", 0 )
                end
              end
            else
              Client:SetState( self, "Speeding", false )
              Client:SetState( self, "Warnings", 0 )
              Client:SetState( self, "IsOffRunway", false )
              Client:SetState( self, "OffRunwayWarnings", 0 )
              local Taxi = Client:GetState( self, "Taxi" )
              if Taxi == true then
                Client:Message( "You have progressed to the runway ... Await take-off clearance ...", 20, "ATC" )
                Client:SetState( self, "Taxi", false )
              end
            end
          end
        end
      else
        Client:SetState( self, "Taxi", false )
      end
    end
  )

  return true
end

---
-- @type ATC_GROUND_UNIVERSAL
-- @field Core.Set#SET_CLIENT SetClient
-- @field #string Version
-- @field #string ClassName
-- @field #table Airbases
-- @field #table AirbaseList
-- @field #number KickSpeed
-- @extends Core.Base#BASE

--- Base class for ATC\_GROUND\_UNIVERSAL implementations.
-- @field #ATC_GROUND_UNIVERSAL
ATC_GROUND_UNIVERSAL = {
  ClassName = "ATC_GROUND_UNIVERSAL",
  Version = "0.0.2",
  SetClient = nil,
  Airbases = nil,
  AirbaseList = nil,
  KickSpeed = nil, -- The maximum speed in meters per second for all airbases until a player gets kicked. This is overridden at each derived class.
}

--- Creates a new ATC\_GROUND\_UNIVERSAL object. This works on any map.
-- @param #ATC_GROUND_UNIVERSAL self
-- @param AirbaseList A table of Airbase Names. Leave empty to cover **all** airbases of the map.
-- @return #ATC_GROUND_UNIVERSAL self
-- @usage
--            -- define monitoring for one airbase
--            local atc=ATC_GROUND_UNIVERSAL:New({AIRBASE.Syria.Gecitkale})
--            -- set kick speed
--            atc:SetKickSpeed(UTILS.KnotsToMps(20))
--            -- start monitoring evey 10 secs
--            atc:Start(10)
function ATC_GROUND_UNIVERSAL:New(AirbaseList)

  -- Inherits from BASE
  local self = BASE:Inherit( self, BASE:New() ) -- #ATC_GROUND
  self:T( { self.ClassName } )

  self.Airbases = {}
  
  self.AirbaseList = AirbaseList
  
  if not self.AirbaseList then
    self.AirbaseList = {}
    for _name,_base in pairs(_DATABASE.AIRBASES) do
      -- DONE exclude FARPS and Ships
      if _base and _base.isAirdrome == true then
        self.AirbaseList[_name]=_name
        self.Airbases[_name]={}
      end
    end
  else
    for _,_name in pairs(AirbaseList) do
      -- DONE exclude FARPS and Ships
      local airbase = _DATABASE:FindAirbase(_name)
      if airbase and (airbase.isAirdrome == true) then
        self.Airbases[_name]={} 
      end 
    end
  end
  
  self.SetClient = SET_CLIENT:New():FilterCategories( "plane" ):FilterStart()
  

  for AirbaseID, Airbase in pairs( self.Airbases ) do
    -- Specified ZoneBoundary is used if set or Airbase radius by default
    if Airbase.ZoneBoundary then
      Airbase.ZoneBoundary = ZONE_POLYGON_BASE:New( "Boundary " .. AirbaseID, Airbase.ZoneBoundary )
    else
      Airbase.ZoneBoundary = _DATABASE:FindAirbase( AirbaseID ):GetZone()
    end

    Airbase.ZoneRunways = AIRBASE:FindByName(AirbaseID):GetRunways()
    Airbase.Monitor = self.AirbaseList and false or true -- When AirbaseList is not given, monitor every Airbase, otherwise don't monitor any (yet). 
  end

  -- Now activate the monitoring for the airbases that need to be monitored.
  for AirbaseID, AirbaseName in pairs( self.AirbaseList or {} ) do
    self.Airbases[AirbaseName].Monitor = true
  end


  self.SetClient:ForEachClient(
    -- @param Wrapper.Client#CLIENT Client
    function( Client )
      Client:SetState( self, "Speeding", false )
      Client:SetState( self, "Warnings", 0)
      Client:SetState( self, "IsOffRunway", false )
      Client:SetState( self, "OffRunwayWarnings", 0 )
      Client:SetState( self, "Taxi", false )
    end
  )

  -- This is simple slot blocker is used on the server.  
  SSB = USERFLAG:New( "SSB" )
  SSB:Set( 100 )
  
  -- Kickspeed
  self.KickSpeed = UTILS.KnotsToMps(10)
  self:SetMaximumKickSpeedMiph(30)
  
  return self
end


--- Add a specific Airbase Boundary if you don't want to use the round zone that is auto-created.
-- @param #ATC_GROUND_UNIVERSAL self
-- @param #string Airbase The name of the Airbase
-- @param Core.Zone#ZONE Zone The ZONE object to be used, e.g. a ZONE_POLYGON
-- @return #ATC_GROUND_UNIVERSAL self
function ATC_GROUND_UNIVERSAL:SetAirbaseBoundaries(Airbase, Zone)
  self.Airbases[Airbase].ZoneBoundary = Zone
  return self
end

--- Smoke the airbases runways.
-- @param #ATC_GROUND_UNIVERSAL self
-- @param Utilities.Utils#SMOKECOLOR SmokeColor The color of the smoke around the runways.
-- @return #ATC_GROUND_UNIVERSAL self
function ATC_GROUND_UNIVERSAL:SmokeRunways( SmokeColor )
  
  local SmokeColor = SmokeColor or SMOKECOLOR.Red
  for AirbaseID, Airbase in pairs( self.Airbases ) do
    if Airbase.ZoneRunways then
      for _,_runwaydata in pairs (Airbase.ZoneRunways) do
        local runwaydata = _runwaydata -- Wrapper.Airbase#AIRBASE.Runway
        runwaydata.zone:SmokeZone(SmokeColor)
      end
    end
  end
  
  return self
end

--- Draw the airbases runways.
-- @param #ATC_GROUND_UNIVERSAL self
-- @param #table Color The color of the line around the runways, in RGB, e.g `{1,0,0}` for red.
-- @return #ATC_GROUND_UNIVERSAL self
function ATC_GROUND_UNIVERSAL:DrawRunways( Color )
  
  local Color = Color or {1,0,0}
  for AirbaseID, Airbase in pairs( self.Airbases ) do
    if Airbase.ZoneRunways then
      for _,_runwaydata in pairs (Airbase.ZoneRunways) do
        local runwaydata = _runwaydata -- Wrapper.Airbase#AIRBASE.Runway
        runwaydata.zone:DrawZone(-1,Color)
      end
    end
  end
  
  return self
end

--- Draw the airbases boundaries.
-- @param #ATC_GROUND_UNIVERSAL self
-- @param #table Color The color of the line around the runways, in RGB, e.g `{1,0,0}` for red.
-- @return #ATC_GROUND_UNIVERSAL self
function ATC_GROUND_UNIVERSAL:DrawBoundaries( Color )
  
  local Color = Color or {1,0,0}
  for AirbaseID, Airbase in pairs( self.Airbases ) do
    if Airbase.ZoneBoundary then
       Airbase.ZoneBoundary:DrawZone(-1, Color) 
    end
  end
  
  return self
end

--- Set the maximum speed in meters per second (Mps) until the player gets kicked.
-- An airbase can be specified to set the kick speed for.
-- @param #ATC_GROUND_UNIVERSAL self
-- @param #number KickSpeed The speed in Mps.
-- @param #string Airbase (optional) The airbase name to set the kick speed for.
-- @return #ATC_GROUND_UNIVERSAL self
-- @usage
-- 
--   -- Declare Atc_Ground
-- 
--   Atc_Ground = ATC_GROUND_UNIVERSAL:New()
--   
--   -- Then use one of these methods...
-- 
--   Atc_Ground:SetKickSpeed( UTILS.KmphToMps( 80 ) ) -- Kick the players at 80 kilometers per hour
-- 
--   Atc_Ground:SetKickSpeed( UTILS.MiphToMps( 100 ) ) -- Kick the players at 100 miles per hour
-- 
--   Atc_Ground:SetKickSpeed( 24 ) -- Kick the players at 24 meters per second ( 24 * 3.6 = 86.4 kilometers per hour )
-- 
function ATC_GROUND_UNIVERSAL:SetKickSpeed( KickSpeed, Airbase )

  if not Airbase then
    self.KickSpeed = KickSpeed
  else
    self.Airbases[Airbase].KickSpeed = KickSpeed
  end
  
  return self
end

--- Set the maximum speed in Kmph until the player gets kicked.
-- @param #ATC_GROUND_UNIVERSAL self
-- @param #number KickSpeed Set the speed in Kmph.
-- @param #string Airbase (optional) The airbase name to set the kick speed for.
-- @return #ATC_GROUND_UNIVERSAL self
-- 
--   Atc_Ground:SetKickSpeedKmph( 80 ) -- Kick the players at 80 kilometers per hour
-- 
function ATC_GROUND_UNIVERSAL:SetKickSpeedKmph( KickSpeed, Airbase )

  self:SetKickSpeed( UTILS.KmphToMps( KickSpeed ), Airbase )
  
  return self
end

--- Set the maximum speed in Miph until the player gets kicked.
-- @param #ATC_GROUND_UNIVERSAL self
-- @param #number KickSpeedMiph Set the speed in Mph.
-- @param #string Airbase (optional) The airbase name to set the kick speed for.
-- @return #ATC_GROUND_UNIVERSAL self
-- 
--   Atc_Ground:SetKickSpeedMiph( 100 ) -- Kick the players at 100 miles per hour
-- 
function ATC_GROUND_UNIVERSAL:SetKickSpeedMiph( KickSpeedMiph, Airbase )

  self:SetKickSpeed( UTILS.MiphToMps( KickSpeedMiph ), Airbase )
  
  return self
end


--- Set the maximum kick speed in meters per second (Mps) until the player gets kicked.
-- There are no warnings given if this speed is reached, and is to prevent players to take off from the airbase!
-- An airbase can be specified to set the maximum kick speed for.
-- @param #ATC_GROUND_UNIVERSAL self
-- @param #number MaximumKickSpeed The speed in Mps.
-- @param #string Airbase (optional) The airbase name to set the kick speed for.
-- @return #ATC_GROUND_UNIVERSAL self
-- @usage
-- 
--   -- Declare Atc_Ground
-- 
--   Atc_Ground = ATC_GROUND_UNIVERSAL:New()
--   
--   -- Then use one of these methods...
-- 
--   Atc_Ground:SetMaximumKickSpeed( UTILS.KmphToMps( 80 ) ) -- Kick the players at 80 kilometers per hour
-- 
--   Atc_Ground:SetMaximumKickSpeed( UTILS.MiphToMps( 100 ) ) -- Kick the players at 100 miles per hour
-- 
--   Atc_Ground:SetMaximumKickSpeed( 24 ) -- Kick the players at 24 meters per second ( 24 * 3.6 = 86.4 kilometers per hour )
-- 
function ATC_GROUND_UNIVERSAL:SetMaximumKickSpeed( MaximumKickSpeed, Airbase )

  if not Airbase then
    self.MaximumKickSpeed = MaximumKickSpeed
  else
    self.Airbases[Airbase].MaximumKickSpeed = MaximumKickSpeed
  end
  
  return self
end

--- Set the maximum kick speed in kilometers per hour (Kmph) until the player gets kicked.
-- There are no warnings given if this speed is reached, and is to prevent players to take off from the airbase!
-- An airbase can be specified to set the maximum kick speed for.
-- @param #ATC_GROUND_UNIVERSAL self
-- @param #number MaximumKickSpeed Set the speed in Kmph.
-- @param #string Airbase (optional) The airbase name to set the kick speed for.
-- @return #ATC_GROUND_UNIVERSAL self
-- 
--   Atc_Ground:SetMaximumKickSpeedKmph( 150 ) -- Kick the players at 150 kilometers per hour
-- 
function ATC_GROUND_UNIVERSAL:SetMaximumKickSpeedKmph( MaximumKickSpeed, Airbase )

  self:SetMaximumKickSpeed( UTILS.KmphToMps( MaximumKickSpeed ), Airbase )
  
  return self
end

--- Set the maximum kick speed in miles per hour (Miph) until the player gets kicked.
-- There are no warnings given if this speed is reached, and is to prevent players to take off from the airbase!
-- An airbase can be specified to set the maximum kick speed for.
-- @param #ATC_GROUND_UNIVERSAL self
-- @param #number MaximumKickSpeedMiph Set the speed in Mph.
-- @param #string Airbase (optional) The airbase name to set the kick speed for.
-- @return #ATC_GROUND_UNIVERSAL self
-- 
--   Atc_Ground:SetMaximumKickSpeedMiph( 100 ) -- Kick the players at 100 miles per hour
-- 
function ATC_GROUND_UNIVERSAL:SetMaximumKickSpeedMiph( MaximumKickSpeedMiph, Airbase )

  self:SetMaximumKickSpeed( UTILS.MiphToMps( MaximumKickSpeedMiph ), Airbase )
  
  return self
end

--- [Internal] Monitoring function
-- @param #ATC_GROUND_UNIVERSAL self
-- @return #ATC_GROUND_UNIVERSAL self
function ATC_GROUND_UNIVERSAL:_AirbaseMonitor()
  self:I("_AirbaseMonitor")
  self.SetClient:ForEachClient(
    --- Nameless function
    -- @param Wrapper.Client#CLIENT Client
    function( Client )

      if Client:IsAlive() then

        local IsOnGround = Client:InAir() == false

        for AirbaseID, AirbaseMeta in pairs( self.Airbases ) do
          self:T( AirbaseID, AirbaseMeta.KickSpeed )
  
          if AirbaseMeta.Monitor == true and Client:IsInZone( AirbaseMeta.ZoneBoundary )  then

            local NotInRunwayZone = true
            
            if AirbaseMeta.ZoneRunways then
              for _,_runwaydata in pairs (AirbaseMeta.ZoneRunways) do
                local runwaydata = _runwaydata -- Wrapper.Airbase#AIRBASE.Runway
                  NotInRunwayZone = ( Client:IsNotInZone( _runwaydata.zone ) == true ) and NotInRunwayZone or false
              end
            end

            if NotInRunwayZone then
              
              local Taxi = Client:GetState( self, "Taxi" )
              
              if IsOnGround then
                
                self:T( Taxi )
                if Taxi == false then
                  local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed )
                  Client:Message( "Welcome to " .. AirbaseID .. ". The maximum taxiing speed is " .. 
                                  Velocity:ToString() , 20, "ATC" )
                  Client:SetState( self, "Taxi", true )
                  Client:SetState( self, "Speeding", false )
                  Client:SetState( self, "Warnings", 0 )
                end
  
                -- TODO: GetVelocityKMH function usage
                local Velocity = VELOCITY_POSITIONABLE:New( Client )
                --MESSAGE:New( "Velocity = " .. Velocity:ToString(), 1 ):ToAll()
                local IsAboveRunway = Client:IsAboveRunway()
                self:T( {IsAboveRunway, IsOnGround, Velocity:Get() })
  
                if IsOnGround and not Taxi then
                  local Speeding = false
                  if AirbaseMeta.MaximumKickSpeed then 
                    if Velocity:Get() > AirbaseMeta.MaximumKickSpeed then
                      Speeding = true
                    end
                  else
                    if Velocity:Get() > self.MaximumKickSpeed then
                      Speeding = true
                    end
                  end
                  if Speeding == true then
                    --MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. 
                    --             " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll()
                    --Client:Destroy()
                    Client:SetState( self, "Speeding", true )
                    local SpeedingWarnings = Client:GetState( self, "Warnings" )
                    Client:SetState( self, "Warnings", SpeedingWarnings + 1 )
                    Client:Message( "Warning " .. SpeedingWarnings .. "/3! Airbase traffic rule violation! Slow down now! Your speed is " .. 
                                        Velocity:ToString(), 5, "ATC" )
                  end
                end                  
           
                if IsOnGround then
  
                  local Speeding = false
                  if AirbaseMeta.KickSpeed then -- If there is a speed defined for the airbase, use that only.
                    if Velocity:Get() > AirbaseMeta.KickSpeed then
                      Speeding = true
                    end
                  else
                    if Velocity:Get() > self.KickSpeed then
                      Speeding = true
                    end
                  end
                  if Speeding == true then  
                    local IsSpeeding = Client:GetState( self, "Speeding" )
  
                    if IsSpeeding == true then
                      local SpeedingWarnings = Client:GetState( self, "Warnings" )
                      self:T( SpeedingWarnings )
  
                      if SpeedingWarnings <= 3 then
                        Client:Message( "Warning " .. SpeedingWarnings .. "/3! Airbase traffic rule violation! Slow down now! Your speed is " .. 
                                        Velocity:ToString(), 5, "ATC" )
                        Client:SetState( self, "Warnings", SpeedingWarnings + 1 )
                      else
                        MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll()
                        -- @param Wrapper.Client#CLIENT Client
                        Client:Destroy()
                        Client:SetState( self, "Speeding", false )
                        Client:SetState( self, "Warnings", 0 )
                      end
  
                    else
                      Client:Message( "Attention! You are speeding on the taxiway, slow down! Your speed is " .. 
                                      Velocity:ToString(), 5, "ATC" )
                      Client:SetState( self, "Speeding", true )
                      Client:SetState( self, "Warnings", 1 )
                    end
  
                  else
                    Client:SetState( self, "Speeding", false )
                    Client:SetState( self, "Warnings", 0 )
                  end
                end

                if IsOnGround and not IsAboveRunway then
  
                  local IsOffRunway = Client:GetState( self, "IsOffRunway" )
  
                  if IsOffRunway == true then
                    local OffRunwayWarnings = Client:GetState( self, "OffRunwayWarnings" )
                    self:T( OffRunwayWarnings )
  
                    if OffRunwayWarnings <= 3 then
                      Client:Message( "Warning " .. OffRunwayWarnings .. "/3! Airbase traffic rule violation! Get back on the taxi immediately!", 5, "ATC" )
                      Client:SetState( self, "OffRunwayWarnings", OffRunwayWarnings + 1 )
                    else
                      MESSAGE:New( "Penalty! Player " .. Client:GetPlayerName() .. " has been kicked, due to a severe airbase traffic rule violation ...", 10, "ATC" ):ToAll()
                      -- @param Wrapper.Client#CLIENT Client
                      Client:Destroy()
                      Client:SetState( self, "IsOffRunway", false )
                      Client:SetState( self, "OffRunwayWarnings", 0 )
                    end
                  else
                    Client:Message( "Attention! You are off the taxiway. Get back on the taxiway immediately!", 5, "ATC" )
                    Client:SetState( self, "IsOffRunway", true )
                    Client:SetState( self, "OffRunwayWarnings", 1 )
                  end
  
                else
                  Client:SetState( self, "IsOffRunway", false )
                  Client:SetState( self, "OffRunwayWarnings", 0 )
                end
              end
            else
              Client:SetState( self, "Speeding", false )
              Client:SetState( self, "Warnings", 0 )
              Client:SetState( self, "IsOffRunway", false )
              Client:SetState( self, "OffRunwayWarnings", 0 )
              local Taxi = Client:GetState( self, "Taxi" )
              if Taxi == true then
                Client:Message( "You have progressed to the runway ... Await take-off clearance ...", 20, "ATC" )
                Client:SetState( self, "Taxi", false )
              end
            end
          end
        end
      else
        Client:SetState( self, "Taxi", false )
      end
    end
  )

  return true
end

--- Start SCHEDULER for ATC_GROUND_UNIVERSAL object.
-- @param #ATC_GROUND_UNIVERSAL self
-- @param RepeatScanSeconds Time in second for defining schedule of alerts.
-- @return #ATC_GROUND_UNIVERSAL self
function ATC_GROUND_UNIVERSAL:Start( RepeatScanSeconds )
  RepeatScanSeconds = RepeatScanSeconds or 0.05
  self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds )
  return self
end

---
-- @type ATC_GROUND_CAUCASUS
-- @extends #ATC_GROUND

--- # ATC\_GROUND\_CAUCASUS, extends @{#ATC_GROUND_UNIVERSAL}
-- 
-- The ATC\_GROUND\_CAUCASUS class monitors the speed of the airplanes at the airbase during taxi.
-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned.
-- 
-- ---
--
-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG)
--
-- ---
--  
-- The default maximum speed for the airbases at Caucasus is **50 km/h**. Warnings are given if this speed limit is trespassed.
-- Players will be immediately kicked when driving faster than **150 km/h** on the taxi way.
-- 
-- 
-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving
-- faster than the maximum allowed speed, the pilot will be kicked.
-- 
-- Different airbases have different maximum speeds, according safety regulations.
-- 
-- # Airbases monitored
-- 
-- The following airbases are monitored at the Caucasus region.
-- Use the @{Wrapper.Airbase#AIRBASE.Caucasus} enumeration to select the airbases to be monitored.
-- 
--   * `AIRBASE.Caucasus.Anapa_Vityazevo`
--   * `AIRBASE.Caucasus.Batumi`
--   * `AIRBASE.Caucasus.Beslan`
--   * `AIRBASE.Caucasus.Gelendzhik`
--   * `AIRBASE.Caucasus.Gudauta`
--   * `AIRBASE.Caucasus.Kobuleti`
--   * `AIRBASE.Caucasus.Krasnodar_Center`
--   * `AIRBASE.Caucasus.Krasnodar_Pashkovsky`
--   * `AIRBASE.Caucasus.Krymsk`
--   * `AIRBASE.Caucasus.Kutaisi`
--   * `AIRBASE.Caucasus.Maykop_Khanskaya`
--   * `AIRBASE.Caucasus.Mineralnye_Vody`
--   * `AIRBASE.Caucasus.Mozdok`
--   * `AIRBASE.Caucasus.Nalchik`
--   * `AIRBASE.Caucasus.Novorossiysk`
--   * `AIRBASE.Caucasus.Senaki_Kolkhi`
--   * `AIRBASE.Caucasus.Sochi_Adler`
--   * `AIRBASE.Caucasus.Soganlug`
--   * `AIRBASE.Caucasus.Sukhumi_Babushara`
--   * `AIRBASE.Caucasus.Tbilisi_Lochini`
--   * `AIRBASE.Caucasus.Vaziani`
--
-- 
-- # Installation
-- 
-- ## In Single Player Missions
-- 
-- ATC\_GROUND is fully functional in single player.
-- 
-- ## In Multi Player Missions
-- 
-- ATC\_GROUND is functional in multi player, however ...
-- 
-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player.
-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed
-- by Ciribob. 
-- 
-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight.
-- ATC\_GROUND is communicating with this modified script to kick players!
-- 
-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob.
-- 
-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock)
-- 
-- # Script it!
-- 
-- ## 1. ATC\_GROUND\_CAUCASUS Constructor
-- 
-- Creates a new ATC_GROUND_CAUCASUS object that will monitor pilots taxiing behaviour.
-- 
--     -- This creates a new ATC_GROUND_CAUCASUS object.
-- 
--     -- Monitor all the airbases.
--     ATC_Ground = ATC_GROUND_CAUCASUS:New()
--     
--     -- Monitor specific airbases only.
-- 
--     ATC_Ground = ATC_GROUND_CAUCASUS:New(
--       { AIRBASE.Caucasus.Gelendzhik,     
--         AIRBASE.Caucasus.Krymsk          
--       }                                  
--     )                                    
-- 
-- ## 2. Set various options
-- 
-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes.
-- 
-- ### 2.1 Speed limit at an airbase.
-- 
--   * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second.
--   * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour.
--   * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour.
--   
-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately.
-- 
--   * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. 
--   * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour.
--   * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour.
--   
-- 
-- @field #ATC_GROUND_CAUCASUS
ATC_GROUND_CAUCASUS = {
  ClassName = "ATC_GROUND_CAUCASUS",
}

--- Creates a new ATC_GROUND_CAUCASUS object.
-- @param #ATC_GROUND_CAUCASUS self
-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.Caucasus enumerator).
-- @return #ATC_GROUND_CAUCASUS self
function ATC_GROUND_CAUCASUS:New( AirbaseNames )

  -- Inherits from BASE
  local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New(AirbaseNames) )

  self:SetKickSpeedKmph( 50 )
  self:SetMaximumKickSpeedKmph( 150 )

  return self
end


--- Start SCHEDULER for ATC_GROUND_CAUCASUS object.
-- @param #ATC_GROUND_CAUCASUS self
-- @param RepeatScanSeconds Time in second for defining occurency of alerts.
-- @return nothing
function ATC_GROUND_CAUCASUS:Start( RepeatScanSeconds )
  RepeatScanSeconds = RepeatScanSeconds or 0.05
  self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds )
end


---
-- @type ATC_GROUND_NEVADA
-- @extends #ATC_GROUND


--- # ATC\_GROUND\_NEVADA, extends @{#ATC_GROUND}
-- 
-- The ATC\_GROUND\_NEVADA class monitors the speed of the airplanes at the airbase during taxi.
-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned.
-- 
-- ---
--
-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG)
-- 
-- ---
-- 
-- The default maximum speed for the airbases at Nevada is **50 km/h**. Warnings are given if this speed limit is trespassed.
-- Players will be immediately kicked when driving faster than **150 km/h** on the taxi way.
-- 
-- The ATC\_GROUND\_NEVADA class monitors the speed of the airplanes at the airbase during taxi.
-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned.
-- 
-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving
-- faster than the maximum allowed speed, the pilot will be kicked.
-- 
-- Different airbases have different maximum speeds, according safety regulations.
-- 
-- # Airbases monitored
-- 
-- The following airbases are monitored at the Nevada region.
-- Use the @{Wrapper.Airbase#AIRBASE.Nevada} enumeration to select the airbases to be monitored.
-- 
--    * `AIRBASE.Nevada.Beatty`
--    * `AIRBASE.Nevada.Boulder_City`
--    * `AIRBASE.Nevada.Creech`
--    * `AIRBASE.Nevada.Echo_Bay`
--    * `AIRBASE.Nevada.Groom_Lake`
--    * `AIRBASE.Nevada.Henderson_Executive`
--    * `AIRBASE.Nevada.Jean`
--    * `AIRBASE.Nevada.Laughlin`
--    * `AIRBASE.Nevada.Lincoln_County`
--    * `AIRBASE.Nevada.McCarran_International`
--    * `AIRBASE.Nevada.Mesquite`
--    * `AIRBASE.Nevada.Mina`
--    * `AIRBASE.Nevada.Nellis`
--    * `AIRBASE.Nevada.North_Las_Vegas`
--    * `AIRBASE.Nevada.Pahute_Mesa`
--    * `AIRBASE.Nevada.Tonopah`
--    * `AIRBASE.Nevada.Tonopah_Test_Range`
--
-- # Installation
-- 
-- ## In Single Player Missions
-- 
-- ATC\_GROUND is fully functional in single player.
-- 
-- ## In Multi Player Missions
-- 
-- ATC\_GROUND is functional in multi player, however ...
-- 
-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player.
-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed
-- by Ciribob. 
-- 
-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight.
-- ATC\_GROUND is communicating with this modified script to kick players!
-- 
-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob.
-- 
-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock)
-- 
-- # Script it!
-- 
-- ## 1. ATC_GROUND_NEVADA Constructor
-- 
-- Creates a new ATC_GROUND_NEVADA object that will monitor pilots taxiing behaviour.
-- 
--     -- This creates a new ATC_GROUND_NEVADA object.
-- 
--     -- Monitor all the airbases.
--     ATC_Ground = ATC_GROUND_NEVADA:New()
-- 
--    
--     -- Monitor specific airbases.
--     ATC_Ground = ATC_GROUND_NEVADA:New(              
--       { AIRBASE.Nevada.Laughlin,                        
--         AIRBASE.Nevada.Lincoln_County,               
--         AIRBASE.Nevada.North_Las_Vegas,              
--         AIRBASE.Nevada.McCarran_International
--       }                                              
--     )                                                
-- 
-- ## 2. Set various options
-- 
-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes.
-- 
-- ### 2.1 Speed limit at an airbase.
-- 
--   * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second.
--   * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour.
--   * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour.
--   
-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately.
-- 
--   * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. 
--   * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour.
--   * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour.
-- 
--   
-- @field #ATC_GROUND_NEVADA
ATC_GROUND_NEVADA = {
  ClassName = "ATC_GROUND_NEVADA",
}

--- Creates a new ATC_GROUND_NEVADA object.
-- @param #ATC_GROUND_NEVADA self
-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.Nevada enumerator).
-- @return #ATC_GROUND_NEVADA self
function ATC_GROUND_NEVADA:New( AirbaseNames )

  -- Inherits from BASE
  local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New( AirbaseNames ) )

  self:SetKickSpeedKmph( 50 )
  self:SetMaximumKickSpeedKmph( 150 )

  return self
end

--- Start SCHEDULER for ATC_GROUND_NEVADA object.
-- @param #ATC_GROUND_NEVADA self
-- @param RepeatScanSeconds Time in second for defining occurency of alerts.
-- @return nothing
function ATC_GROUND_NEVADA:Start( RepeatScanSeconds )
  RepeatScanSeconds = RepeatScanSeconds or 0.05
  self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds )
end

---
-- @type ATC_GROUND_NORMANDY
-- @extends #ATC_GROUND


--- # ATC\_GROUND\_NORMANDY, extends @{#ATC_GROUND}
-- 
-- The ATC\_GROUND\_NORMANDY class monitors the speed of the airplanes at the airbase during taxi.
-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned.
-- 
-- ---
-- 
-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG)
-- 
-- ---
-- 
-- The default maximum speed for the airbases at Normandy is **40 km/h**. Warnings are given if this speed limit is trespassed.
-- Players will be immediately kicked when driving faster than **100 km/h** on the taxi way.
-- 
-- The ATC\_GROUND\_NORMANDY class monitors the speed of the airplanes at the airbase during taxi.
-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned.
-- 
-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving
-- faster than the maximum allowed speed, the pilot will be kicked.
-- 
-- Different airbases have different maximum speeds, according safety regulations.
-- 
-- # Airbases monitored
-- 
-- The following airbases are monitored at the Normandy region.
-- Use the @{Wrapper.Airbase#AIRBASE.Normandy} enumeration to select the airbases to be monitored.
-- 
--   * `AIRBASE.Normandy.Azeville`
--   * `AIRBASE.Normandy.Bazenville`
--   * `AIRBASE.Normandy.Beny_sur_Mer`
--   * `AIRBASE.Normandy.Beuzeville`
--   * `AIRBASE.Normandy.Biniville`
--   * `AIRBASE.Normandy.Brucheville`
--   * `AIRBASE.Normandy.Cardonville`
--   * `AIRBASE.Normandy.Carpiquet`
--   * `AIRBASE.Normandy.Chailey`
--   * `AIRBASE.Normandy.Chippelle`
--   * `AIRBASE.Normandy.Cretteville`
--   * `AIRBASE.Normandy.Cricqueville_en_Bessin`
--   * `AIRBASE.Normandy.Deux_Jumeaux`
--   * `AIRBASE.Normandy.Evreux`
--   * `AIRBASE.Normandy.Ford`
--   * `AIRBASE.Normandy.Funtington`
--   * `AIRBASE.Normandy.Lantheuil`
--   * `AIRBASE.Normandy.Le_Molay`
--   * `AIRBASE.Normandy.Lessay`
--   * `AIRBASE.Normandy.Lignerolles`
--   * `AIRBASE.Normandy.Longues_sur_Mer`
--   * `AIRBASE.Normandy.Maupertus`
--   * `AIRBASE.Normandy.Meautis`
--   * `AIRBASE.Normandy.Needs_Oar_Point`
--   * `AIRBASE.Normandy.Picauville`
--   * `AIRBASE.Normandy.Rucqueville`
--   * `AIRBASE.Normandy.Saint_Pierre_du_Mont`
--   * `AIRBASE.Normandy.Sainte_Croix_sur_Mer`
--   * `AIRBASE.Normandy.Sainte_Laurent_sur_Mer`
--   * `AIRBASE.Normandy.Sommervieu`
--   * `AIRBASE.Normandy.Tangmere`
--   * `AIRBASE.Normandy.Argentan`
--   * `AIRBASE.Normandy.Goulet`
--   * `AIRBASE.Normandy.Essay`
--   * `AIRBASE.Normandy.Hauterive`
--   * `AIRBASE.Normandy.Barville`
--   * `AIRBASE.Normandy.Conches`
--   * `AIRBASE.Normandy.Vrigny`
--
-- # Installation
-- 
-- ## In Single Player Missions
-- 
-- ATC\_GROUND is fully functional in single player.
-- 
-- ## In Multi Player Missions
-- 
-- ATC\_GROUND is functional in multi player, however ...
-- 
-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player.
-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed
-- by Ciribob. 
-- 
-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight.
-- ATC\_GROUND is communicating with this modified script to kick players!
-- 
-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob.
-- 
-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock)
-- 
-- # Script it!
-- 
-- ## 1. ATC_GROUND_NORMANDY Constructor
-- 
-- Creates a new ATC_GROUND_NORMANDY object that will monitor pilots taxiing behaviour.
-- 
--     -- This creates a new ATC_GROUND_NORMANDY object.
-- 
--     -- Monitor for these clients the airbases.
--     AirbasePoliceCaucasus = ATC_GROUND_NORMANDY:New()
--     
--     ATC_Ground = ATC_GROUND_NORMANDY:New( 
--       { AIRBASE.Normandy.Chippelle,
--         AIRBASE.Normandy.Beuzeville 
--       } 
--     )
-- 
--     
-- ## 2. Set various options
-- 
-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes.
-- 
-- ### 2.1 Speed limit at an airbase.
-- 
--   * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second.
--   * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour.
--   * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour.
--   
-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately.
-- 
--   * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. 
--   * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour.
--   * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour.
--   
-- @field #ATC_GROUND_NORMANDY
ATC_GROUND_NORMANDY = {
  ClassName = "ATC_GROUND_NORMANDY", 
}


--- Creates a new ATC_GROUND_NORMANDY object.
-- @param #ATC_GROUND_NORMANDY self
-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.Normandy enumerator).
-- @return #ATC_GROUND_NORMANDY self
function ATC_GROUND_NORMANDY:New( AirbaseNames )

  -- Inherits from BASE
  local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New( AirbaseNames ) ) -- #ATC_GROUND_NORMANDY
  
  self:SetKickSpeedKmph( 40 )
  self:SetMaximumKickSpeedKmph( 100 )
  
  return self
end

     
--- Start SCHEDULER for ATC_GROUND_NORMANDY object.
-- @param #ATC_GROUND_NORMANDY self
-- @param RepeatScanSeconds Time in second for defining occurency of alerts.
-- @return nothing
function ATC_GROUND_NORMANDY:Start( RepeatScanSeconds )
  RepeatScanSeconds = RepeatScanSeconds or 0.05
  self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds )
end

---
-- @type ATC_GROUND_PERSIANGULF
-- @extends #ATC_GROUND


--- # ATC\_GROUND\_PERSIANGULF, extends @{#ATC_GROUND}
-- 
-- The ATC\_GROUND\_PERSIANGULF class monitors the speed of the airplanes at the airbase during taxi.
-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned.
-- 
-- ---
-- 
-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG)
-- 
-- ---
-- 
-- The default maximum speed for the airbases at Persian Gulf is **50 km/h**. Warnings are given if this speed limit is trespassed.
-- Players will be immediately kicked when driving faster than **150 km/h** on the taxi way.
-- 
-- The ATC\_GROUND\_PERSIANGULF class monitors the speed of the airplanes at the airbase during taxi.
-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned.
-- 
-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving
-- faster than the maximum allowed speed, the pilot will be kicked.
-- 
-- Different airbases have different maximum speeds, according safety regulations.
-- 
-- # Airbases monitored
-- 
-- The following airbases are monitored at the PersianGulf region.
-- Use the @{Wrapper.Airbase#AIRBASE.PersianGulf} enumeration to select the airbases to be monitored.
-- 
--   * `AIRBASE.PersianGulf.Abu_Musa_Island`
--   * `AIRBASE.PersianGulf.Al_Dhafra_AFB`
--   * `AIRBASE.PersianGulf.Al_Maktoum_Intl`
--   * `AIRBASE.PersianGulf.Al_Minhad_AFB`
--   * `AIRBASE.PersianGulf.Bandar_Abbas_Intl`
--   * `AIRBASE.PersianGulf.Bandar_Lengeh`
--   * `AIRBASE.PersianGulf.Dubai_Intl`
--   * `AIRBASE.PersianGulf.Fujairah_Intl`
--   * `AIRBASE.PersianGulf.Havadarya`
--   * `AIRBASE.PersianGulf.Kerman`
--   * `AIRBASE.PersianGulf.Khasab`
--   * `AIRBASE.PersianGulf.Lar`
--   * `AIRBASE.PersianGulf.Qeshm_Island`
--   * `AIRBASE.PersianGulf.Sharjah_Intl`
--   * `AIRBASE.PersianGulf.Shiraz_Intl`
--   * `AIRBASE.PersianGulf.Sir_Abu_Nuayr`
--   * `AIRBASE.PersianGulf.Sirri_Island`
--   * `AIRBASE.PersianGulf.Tunb_Island_AFB`
--   * `AIRBASE.PersianGulf.Tunb_Kochak`
--   * `AIRBASE.PersianGulf.Sas_Al_Nakheel`
--   * `AIRBASE.PersianGulf.Bandar_e_Jask`
--   * `AIRBASE.PersianGulf.Abu_Dhabi_Intl`
--   * `AIRBASE.PersianGulf.Al_Bateen`
--   * `AIRBASE.PersianGulf.Kish_Intl`
--   * `AIRBASE.PersianGulf.Al_Ain_Intl`
--   * `AIRBASE.PersianGulf.Lavan_Island`
--   * `AIRBASE.PersianGulf.Jiroft`
--
-- # Installation
-- 
-- ## In Single Player Missions
-- 
-- ATC\_GROUND is fully functional in single player.
-- 
-- ## In Multi Player Missions
-- 
-- ATC\_GROUND is functional in multi player, however ...
-- 
-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player.
-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed
-- by Ciribob. 
-- 
-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight.
-- ATC\_GROUND is communicating with this modified script to kick players!
-- 
-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob.
-- 
-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock)
-- 
-- # Script it!
-- 
-- ## 1. ATC_GROUND_PERSIANGULF Constructor
-- 
-- Creates a new ATC_GROUND_PERSIANGULF object that will monitor pilots taxiing behaviour.
-- 
--     -- This creates a new ATC_GROUND_PERSIANGULF object.
-- 
--     -- Monitor for these clients the airbases.
--     AirbasePoliceCaucasus = ATC_GROUND_PERSIANGULF:New()
--     
--     ATC_Ground = ATC_GROUND_PERSIANGULF:New( 
--       { AIRBASE.PersianGulf.Kerman,
--         AIRBASE.PersianGulf.Al_Minhad_AFB 
--       } 
--     )
-- 
--     
-- ## 2. Set various options
-- 
-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes.
-- 
-- ### 2.1 Speed limit at an airbase.
-- 
--   * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second.
--   * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour.
--   * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour.
--   
-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately.
-- 
--   * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. 
--   * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour.
--   * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour.
--   
-- @field #ATC_GROUND_PERSIANGULF
ATC_GROUND_PERSIANGULF = {
  ClassName = "ATC_GROUND_PERSIANGULF",
}

--- Creates a new ATC_GROUND_PERSIANGULF object.
-- @param #ATC_GROUND_PERSIANGULF self
-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.PersianGulf enumerator).
-- @return #ATC_GROUND_PERSIANGULF self
function ATC_GROUND_PERSIANGULF:New( AirbaseNames )

  -- Inherits from BASE
  local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New( AirbaseNames ) ) -- #ATC_GROUND_PERSIANGULF
  
  self:SetKickSpeedKmph( 50 )
  self:SetMaximumKickSpeedKmph( 150 )
  
end

--- Start SCHEDULER for ATC_GROUND_PERSIANGULF object.
-- @param #ATC_GROUND_PERSIANGULF self
-- @param RepeatScanSeconds Time in second for defining occurency of alerts.
-- @return nothing
function ATC_GROUND_PERSIANGULF:Start( RepeatScanSeconds )
  RepeatScanSeconds = RepeatScanSeconds or 0.05
  self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds )
end
          
---
-- @type ATC_GROUND_MARIANAISLANDS
-- @extends #ATC_GROUND


--- # ATC\_GROUND\_MARIANA, extends @{#ATC_GROUND}
-- 
-- The ATC\_GROUND\_MARIANA class monitors the speed of the airplanes at the airbase during taxi.
-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned.
-- 
-- ---
-- 
-- ![Banner Image](..\Presentations\ATC_GROUND\Dia1.JPG)
-- 
-- ---
-- 
-- The default maximum speed for the airbases at Persian Gulf is **50 km/h**. Warnings are given if this speed limit is trespassed.
-- Players will be immediately kicked when driving faster than **150 km/h** on the taxi way.
-- 
-- The ATC\_GROUND\_MARIANA class monitors the speed of the airplanes at the airbase during taxi.
-- The pilots may not drive faster than the maximum speed for the airbase, or they will be despawned.
-- 
-- The pilot will receive 3 times a warning during speeding. After the 3rd warning, if the pilot is still driving
-- faster than the maximum allowed speed, the pilot will be kicked.
-- 
-- Different airbases have different maximum speeds, according safety regulations.
-- 
-- # Airbases monitored
-- 
-- The following airbases are monitored at the Mariana Island region.
-- Use the @{Wrapper.Airbase#AIRBASE.MarianaIslands} enumeration to select the airbases to be monitored.
-- 
-- * AIRBASE.MarianaIslands.Rota_Intl
-- * AIRBASE.MarianaIslands.Andersen_AFB
-- * AIRBASE.MarianaIslands.Antonio_B_Won_Pat_Intl
-- * AIRBASE.MarianaIslands.Saipan_Intl
-- * AIRBASE.MarianaIslands.Tinian_Intl
-- * AIRBASE.MarianaIslands.Olf_Orote
--
-- # Installation
-- 
-- ## In Single Player Missions
-- 
-- ATC\_GROUND is fully functional in single player.
-- 
-- ## In Multi Player Missions
-- 
-- ATC\_GROUND is functional in multi player, however ...
-- 
-- Due to a bug in DCS since release 1.5, the despawning of clients are not anymore working in multi player.
-- To **work around this problem**, a much better solution has been made, using the **slot blocker** script designed
-- by Ciribob. 
-- 
-- With the help of __Ciribob__, this script has been extended to also kick client players while in flight.
-- ATC\_GROUND is communicating with this modified script to kick players!
-- 
-- Install the file **SimpleSlotBlockGameGUI.lua** on the server, following the installation instructions described by Ciribob.
-- 
-- [Simple Slot Blocker from Ciribob & FlightControl](https://github.com/ciribob/DCS-SimpleSlotBlock)
-- 
-- # Script it!
-- 
-- ## 1. ATC_GROUND_MARIANAISLANDS Constructor
-- 
-- Creates a new ATC_GROUND_MARIANAISLANDS object that will monitor pilots taxiing behaviour.
-- 
--     -- This creates a new ATC_GROUND_MARIANAISLANDS object.
-- 
--     -- Monitor for these clients the airbases.
--     AirbasePoliceCaucasus = ATC_GROUND_MARIANAISLANDS:New()
--     
--     ATC_Ground = ATC_GROUND_MARIANAISLANDS:New( 
--       { AIRBASE.MarianaIslands.Andersen_AFB,
--         AIRBASE.MarianaIslands.Saipan_Intl 
--       } 
--     )
-- 
--     
-- ## 2. Set various options
-- 
-- There are various methods that you can use to tweak the behaviour of the ATC\_GROUND classes.
-- 
-- ### 2.1 Speed limit at an airbase.
-- 
--   * @{#ATC_GROUND.SetKickSpeed}(): Set the speed limit allowed at an airbase in meters per second.
--   * @{#ATC_GROUND.SetKickSpeedKmph}(): Set the speed limit allowed at an airbase in kilometers per hour.
--   * @{#ATC_GROUND.SetKickSpeedMiph}(): Set the speed limit allowed at an airbase in miles per hour.
--   
-- ### 2.2 Prevent Takeoff at an airbase. Players will be kicked immediately.
-- 
--   * @{#ATC_GROUND.SetMaximumKickSpeed}(): Set the maximum speed allowed at an airbase in meters per second. 
--   * @{#ATC_GROUND.SetMaximumKickSpeedKmph}(): Set the maximum speed allowed at an airbase in kilometers per hour.
--   * @{#ATC_GROUND.SetMaximumKickSpeedMiph}(): Set the maximum speed allowed at an airbase in miles per hour.
--     
-- @field #ATC_GROUND_MARIANAISLANDS
ATC_GROUND_MARIANAISLANDS = {
  ClassName = "ATC_GROUND_MARIANAISLANDS",
}

--- Creates a new ATC_GROUND_MARIANAISLANDS object.
-- @param #ATC_GROUND_MARIANAISLANDS self
-- @param AirbaseNames A list {} of airbase names (Use AIRBASE.MarianaIslands enumerator).
-- @return #ATC_GROUND_MARIANAISLANDS self
function ATC_GROUND_MARIANAISLANDS:New( AirbaseNames )

  -- Inherits from BASE
  local self = BASE:Inherit( self, ATC_GROUND_UNIVERSAL:New( AirbaseNames ) )

  self:SetKickSpeedKmph( 50 )
  self:SetMaximumKickSpeedKmph( 150 )

  return self
end

--- Start SCHEDULER for ATC_GROUND_MARIANAISLANDS object.
-- @param #ATC_GROUND_MARIANAISLANDS self
-- @param RepeatScanSeconds Time in second for defining occurency of alerts.
-- @return nothing
function ATC_GROUND_MARIANAISLANDS:Start( RepeatScanSeconds )
  RepeatScanSeconds = RepeatScanSeconds or 0.05
  self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, RepeatScanSeconds )
end
--- **Functional** - Models the detection of enemy units by FACs or RECCEs and group them according various methods.
--
-- ===
--
-- ## Features:
--
--   * Detection of targets by recce units.
--   * Group detected targets per unit, type or area (zone).
--   * Keep persistency of detected targets, if when detection is lost.
--   * Provide an indication of detected targets.
--   * Report detected targets.
--   * Refresh detection upon specified time intervals.
--
-- ===
--
-- ## Missions:
--
-- [DET - Detection](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Functional/Detection)
--
-- ===
--
-- Facilitate the detection of enemy units within the battle zone executed by FACs (Forward Air Controllers) or RECCEs (Reconnaissance Units).
-- It uses the in-built detection capabilities of DCS World, but adds new functionalities.
--
-- ===
--
-- ### Contributions:
--
--   * Mechanist : Early concept of DETECTION_AREAS.
--
-- ### Authors:
--
--   * FlightControl : Analysis, Design, Programming, Testing
--
-- ===
--
-- @module Functional.Detection
-- @image Detection.JPG

do -- DETECTION_BASE
  
  ---
  -- @type DETECTION_BASE
  -- @field Core.Set#SET_GROUP DetectionSetGroup The @{Core.Set} of GROUPs in the Forward Air Controller role.
  -- @field DCS#Distance DetectionRange The range till which targets are accepted to be detected.
  -- @field #DETECTION_BASE.DetectedObjects DetectedObjects The list of detected objects.
  -- @field #table DetectedObjectsIdentified Map of the DetectedObjects identified.
  -- @field #number DetectionRun
  -- @extends Core.Fsm#FSM

  --- Defines the core functions to administer detected objects.
  -- The DETECTION_BASE class will detect objects within the battle zone for a list of @{Wrapper.Group}s detecting targets following (a) detection method(s).
  --
  -- ## DETECTION_BASE constructor
  --
  -- Construct a new DETECTION_BASE instance using the @{#DETECTION_BASE.New}() method.
  --
  -- ## Initialization
  --
  -- By default, detection will return detected objects with all the detection sensors available.
  -- However, you can ask how the objects were found with specific detection methods.
  -- If you use one of the below methods, the detection will work with the detection method specified.
  -- You can specify to apply multiple detection methods.
  --
  -- Use the following functions to report the objects it detected using the methods Visual, Optical, Radar, IRST, RWR, DLINK:
  --
  --   * @{#DETECTION_BASE.InitDetectVisual}(): Detected using Visual.
  --   * @{#DETECTION_BASE.InitDetectOptical}(): Detected using Optical.
  --   * @{#DETECTION_BASE.InitDetectRadar}(): Detected using Radar.
  --   * @{#DETECTION_BASE.InitDetectIRST}(): Detected using IRST.
  --   * @{#DETECTION_BASE.InitDetectRWR}(): Detected using RWR.
  --   * @{#DETECTION_BASE.InitDetectDLINK}(): Detected using DLINK.
  --
  -- ## **Filter** detected units based on **category of the unit**
  --
  -- Filter the detected units based on Unit.Category using the method @{#DETECTION_BASE.FilterCategories}().
  -- The different values of Unit.Category can be:
  --
  --   * Unit.Category.AIRPLANE
  --   * Unit.Category.GROUND_UNIT
  --   * Unit.Category.HELICOPTER
  --   * Unit.Category.SHIP
  --   * Unit.Category.STRUCTURE
  --
  -- Multiple Unit.Category entries can be given as a table and then these will be evaluated as an OR expression.
  --
  -- Example to filter a single category (Unit.Category.AIRPLANE).
  --
  --     DetectionObject:FilterCategories( Unit.Category.AIRPLANE )
  --
  -- Example to filter multiple categories (Unit.Category.AIRPLANE, Unit.Category.HELICOPTER). Note the {}.
  --
  --     DetectionObject:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } )
  --
  -- 
  -- ## Radar Blur - use to make the radar less exact, e.g. for WWII scenarios
  -- 
  --  * @{#DETECTION_BASE.SetRadarBlur}(): Set the radar blur to be used.
  --  
  -- ## **DETECTION_ derived classes** group the detected units into a **DetectedItems[]** list
  --
  -- DETECTION_BASE derived classes build a list called DetectedItems[], which is essentially a first later
  -- of grouping of detected units. Each DetectedItem within the DetectedItems[] list contains
  -- a SET_UNIT object that contains the  detected units that belong to that group.
  --
  -- Derived classes will apply different methods to group the detected units.
  -- Examples are per area, per quadrant, per distance, per type.
  -- See further the derived DETECTION classes on which grouping methods are currently supported.
  --
  -- Various methods exist how to retrieve the grouped items from a DETECTION_BASE derived class:
  --
  --   * The method @{Functional.Detection#DETECTION_BASE.GetDetectedItems}() retrieves the DetectedItems[] list.
  --   * A DetectedItem from the DetectedItems[] list can be retrieved using the method @{Functional.Detection#DETECTION_BASE.GetDetectedItem}( DetectedItemIndex ).
  --     Note that this method returns a DetectedItem element from the list, that contains a Set variable and further information
  --     about the DetectedItem that is set by the DETECTION_BASE derived classes, used to group the DetectedItem.
  --   * A DetectedSet from the DetectedItems[] list can be retrieved using the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}( DetectedItemIndex ).
  --     This method retrieves the Set from a DetectedItem element from the DetectedItem list (DetectedItems[ DetectedItemIndex ].Set ).
  --
  -- ## **Visual filters** to fine-tune the probability of the detected objects
  --
  -- By default, DCS World will return any object that is in LOS and within "visual reach", or detectable through one of the electronic detection means.
  -- That being said, the DCS World detection algorithm can sometimes be unrealistic.
  -- Especially for a visual detection, DCS World is able to report within 1 second a detailed detection of a group of 20 units (including types of the units) that are 10 kilometers away, using only visual capabilities.
  -- Additionally, trees and other obstacles are not accounted during the DCS World detection.
  --
  -- Therefore, an additional (optional) filtering has been built into the DETECTION_BASE class, that can be set for visual detected units.
  -- For electronic detection, this filtering is not applied, only for visually detected targets.
  --
  -- The following additional filtering can be applied for visual filtering:
  --
  --   * A probability factor per kilometer distance.
  --   * A probability factor based on the alpha angle between the detected object and the unit detecting.
  --     A detection from a higher altitude allows for better detection than when on the ground.
  --   * Define a probability factor for "cloudy zones", which are zones where forests or villages are located. In these zones, detection will be much more difficult.
  --     The mission designer needs to define these cloudy zones within the mission, and needs to register these zones in the DETECTION_ objects adding a probability factor per zone.
  --
  -- I advise however, that, when you first use the DETECTION derived classes, that you don't use these filters.
  -- Only when you experience unrealistic behavior in your missions, these filters could be applied.
  --
  -- ### Distance visual detection probability
  --
  -- Upon a **visual** detection, the further away a detected object is, the less likely it is to be detected properly.
  -- Also, the speed of accurate detection plays a role.
  --
  -- A distance probability factor between 0 and 1 can be given, that will model a linear extrapolated probability over 10 km distance.
  --
  -- For example, if a probability factor of 0.6 (60%) is given, the extrapolated probabilities over 15 kilometers would like like:
  -- 1 km: 96%, 2 km: 92%, 3 km: 88%, 4 km: 84%, 5 km: 80%, 6 km: 76%, 7 km: 72%, 8 km: 68%, 9 km: 64%, 10 km: 60%, 11 km: 56%, 12 km: 52%, 13 km: 48%, 14 km: 44%, 15 km: 40%.
  --
  -- Note that based on this probability factor, not only the detection but also the **type** of the unit will be applied!
  --
  -- Use the method @{Functional.Detection#DETECTION_BASE.SetDistanceProbability}() to set the probability factor upon a 10 km distance.
  --
  -- ### Alpha Angle visual detection probability
  --
  -- Upon a **visual** detection, the higher the unit is during the detecting process, the more likely the detected unit is to be detected properly.
  -- A detection at a 90% alpha angle is the most optimal, a detection at 10% is less and a detection at 0% is less likely to be correct.
  --
  -- A probability factor between 0 and 1 can be given, that will model a progressive extrapolated probability if the target would be detected at a 0° angle.
  --
  -- For example, if a alpha angle probability factor of 0.7 is given, the extrapolated probabilities of the different angles would look like:
  -- 0°: 70%, 10°: 75,21%, 20°: 80,26%, 30°: 85%, 40°: 89,28%, 50°: 92,98%, 60°: 95,98%, 70°: 98,19%, 80°: 99,54%, 90°: 100%
  --
  -- Use the method @{Functional.Detection#DETECTION_BASE.SetAlphaAngleProbability}() to set the probability factor if 0°.
  --
  -- ### Cloudy Zones detection probability
  --
  -- Upon a **visual** detection, the more a detected unit is within a cloudy zone, the less likely the detected unit is to be detected successfully.
  -- The Cloudy Zones work with the ZONE_BASE derived classes. The mission designer can define within the mission
  -- zones that reflect cloudy areas where detected units may not be so easily visually detected.
  --
  -- Use the method @{Functional.Detection#DETECTION_BASE.SetZoneProbability}() to set for a defined number of zones, the probability factors.
  --
  -- Note however, that the more zones are defined to be "cloudy" within a detection, the more performance it will take
  -- from the DETECTION_BASE to calculate the presence of the detected unit within each zone.
  -- Especially for ZONE_POLYGON, try to limit the amount of nodes of the polygon!
  --
  -- Typically, this kind of filter would be applied for very specific areas where a detection needs to be very realistic for
  -- AI not to detect so easily targets within a forrest or village rich area.
  --
  -- ## Accept / Reject detected units
  --
  -- DETECTION_BASE can accept or reject successful detections based on the location of the detected object,
  -- if it is located in range or located inside or outside of specific zones.
  --
  -- ### Detection acceptance of within range limit
  --
  -- A range can be set that will limit a successful detection for a unit.
  -- Use the method @{Functional.Detection#DETECTION_BASE.SetAcceptRange}() to apply a range in meters till where detected units will be accepted.
  --
  --      local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers.
  --
  --      -- Build a detect object.
  --      local Detection = DETECTION_UNITS:New( SetGroup )
  --
  --      -- This will accept detected units if the range is below 5000 meters.
  --      Detection:SetAcceptRange( 5000 )
  --
  --      -- Start the Detection.
  --      Detection:Start()
  --
  --
  -- ### Detection acceptance if within zone(s).
  --
  -- Specific ZONE_BASE object(s) can be given as a parameter, which will only accept a detection if the unit is within the specified ZONE_BASE object(s).
  -- Use the method @{Functional.Detection#DETECTION_BASE.SetAcceptZones}() will accept detected units if they are within the specified zones.
  --
  --      local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers.
  --
  --      -- Search fo the zones where units are to be accepted.
  --      local ZoneAccept1 = ZONE:New( "AcceptZone1" )
  --      local ZoneAccept2 = ZONE:New( "AcceptZone2" )
  --
  --      -- Build a detect object.
  --      local Detection = DETECTION_UNITS:New( SetGroup )
  --
  --      -- This will accept detected units by Detection when the unit is within ZoneAccept1 OR ZoneAccept2.
  --      Detection:SetAcceptZones( { ZoneAccept1, ZoneAccept2 } ) 
  --
  --      -- Start the Detection.
  --      Detection:Start()
  --
  -- ### Detection rejection if within zone(s).
  --
  -- Specific ZONE_BASE object(s) can be given as a parameter, which will reject detection if the unit is within the specified ZONE_BASE object(s).
  -- Use the method @{Functional.Detection#DETECTION_BASE.SetRejectZones}() will reject detected units if they are within the specified zones.
  -- An example of how to use the method is shown below.
  --
  --      local SetGroup = SET_GROUP:New():FilterPrefixes( "FAC" ):FilterStart() -- Build a SetGroup of Forward Air Controllers.
  --
  --      -- Search fo the zones where units are to be rejected.
  --      local ZoneReject1 = ZONE:New( "RejectZone1" )
  --      local ZoneReject2 = ZONE:New( "RejectZone2" )
  --
  --      -- Build a detect object.
  --      local Detection = DETECTION_UNITS:New( SetGroup )
  --
  --      -- This will reject detected units by Detection when the unit is within ZoneReject1 OR ZoneReject2.
  --      Detection:SetRejectZones( { ZoneReject1, ZoneReject2 } )
  --
  --      -- Start the Detection.
  --      Detection:Start()
  --
  -- ## Detection of Friendlies Nearby
  --
  -- Use the method @{Functional.Detection#DETECTION_BASE.SetFriendliesRange}() to set the range what will indicate when friendlies are nearby
  -- a DetectedItem. The default range is 6000 meters. For air detections, it is advisory to use about 30.000 meters.
  --
  -- ## DETECTION_BASE is a Finite State Machine
  --
  -- Various Events and State Transitions can be tailored using DETECTION_BASE.
  --
  -- ### DETECTION_BASE States
  --
  --   * **Detecting**: The detection is running.
  --   * **Stopped**: The detection is stopped.
  --
  -- ### DETECTION_BASE Events
  --
  --   * **Start**: Start the detection process.
  --   * **Detect**: Detect new units.
  --   * **Detected**: New units have been detected.
  --   * **Stop**: Stop the detection process.
  --
  -- @field #DETECTION_BASE DETECTION_BASE
  --
  DETECTION_BASE = {
    ClassName = "DETECTION_BASE",
    DetectionSetGroup = nil,
    DetectionRange = nil,
    DetectedObjects = {},
    DetectionRun = 0,
    DetectedObjectsIdentified = {},
    DetectedItems = {},
    DetectedItemsByIndex = {},
  }
  
  ---
  -- @type DETECTION_BASE.DetectedObjects
  -- @list <#DETECTION_BASE.DetectedObject>

  ---
  -- @type DETECTION_BASE.DetectedObject
  -- @field #string Name
  -- @field #boolean IsVisible
  -- @field #boolean KnowType
  -- @field #boolean KnowDistance
  -- @field #string Type
  -- @field #number Distance
  -- @field #boolean Identified
  -- @field #number LastTime
  -- @field #boolean LastPos
  -- @field #number LastVelocity
  
  ---
  -- @type DETECTION_BASE.DetectedItems
  -- @list <#DETECTION_BASE.DetectedItem>

  --- Detected item data structure.
  -- @type DETECTION_BASE.DetectedItem
  -- @field #boolean IsDetected Indicates if the DetectedItem has been detected or not.
  -- @field Core.Set#SET_UNIT Set The Set of Units in the detected area.
  -- @field Core.Zone#ZONE_UNIT Zone The Zone of the detected area.
  -- @field #boolean Changed Documents if the detected area has changed.
  -- @field #table Changes A list of the changes reported on the detected area. (It is up to the user of the detected area to consume those changes).
  -- @field #number ID The identifier of the detected area.
  -- @field #boolean FriendliesNearBy Indicates if there are friendlies within the detected area.
  -- @field Wrapper.Unit#UNIT NearestFAC The nearest FAC near the Area.
  -- @field Core.Point#COORDINATE Coordinate The last known coordinate of the DetectedItem.
  -- @field Core.Point#COORDINATE InterceptCoord Intercept coordinate.
  -- @field #number DistanceRecce Distance in meters of the Recce.
  -- @field #number Index Detected item key. Could also be a string.
  -- @field #string ItemID ItemPrefix .. "." .. self.DetectedItemMax.
  -- @field #boolean Locked Lock detected item.
  -- @field #table PlayersNearBy Table of nearby players.
  -- @field #table FriendliesDistance Table of distances to friendly units.
  -- @field #string TypeName Type name of the detected unit.
  -- @field #string CategoryName Category name of the detected unit.
  -- @field #string Name Name of the detected object.
  -- @field #boolean IsVisible If true, detected object is visible.
  -- @field #number LastTime Last time the detected item was seen.
  -- @field DCS#Vec3 LastPos Last known position of the detected item.
  -- @field DCS#Vec3 LastVelocity Last recorded 3D velocity vector of the detected item.
  -- @field #boolean KnowType Type of detected item is known.
  -- @field #boolean KnowDistance Distance to the detected item is known.
  -- @field #number Distance Distance to the detected item.

  --- DETECTION constructor.
  -- @param #DETECTION_BASE self
  -- @param Core.Set#SET_GROUP DetectionSet The @{Core.Set} of @{Wrapper.Group}s that is used to detect the units.
  -- @return #DETECTION_BASE self
  function DETECTION_BASE:New( DetectionSet )

    -- Inherits from BASE
    local self = BASE:Inherit( self, FSM:New() ) -- #DETECTION_BASE

    self.DetectedItemCount = 0
    self.DetectedItemMax = 0
    self.DetectedItems = {}

    self.DetectionSet = DetectionSet

    self.RefreshTimeInterval = 30

    self:InitDetectVisual( nil )
    self:InitDetectOptical( nil )
    self:InitDetectRadar( nil )
    self:InitDetectRWR( nil )
    self:InitDetectIRST( nil )
    self:InitDetectDLINK( nil )

    self:FilterCategories( {
      Unit.Category.AIRPLANE,
      Unit.Category.GROUND_UNIT,
      Unit.Category.HELICOPTER,
      Unit.Category.SHIP,
      Unit.Category.STRUCTURE
    } )

    self:SetFriendliesRange( 6000 )

    -- Create FSM transitions.

    self:SetStartState( "Stopped" )

    self:AddTransition( "Stopped", "Start", "Detecting" )

    --- OnLeave Transition Handler for State Stopped.
    -- @function [parent=#DETECTION_BASE] OnLeaveStopped
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @return #boolean Return false to cancel Transition.

    --- OnEnter Transition Handler for State Stopped.
    -- @function [parent=#DETECTION_BASE] OnEnterStopped
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.

    --- OnBefore Transition Handler for Event Start.
    -- @function [parent=#DETECTION_BASE] OnBeforeStart
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @return #boolean Return false to cancel Transition.

    --- OnAfter Transition Handler for Event Start.
    -- @function [parent=#DETECTION_BASE] OnAfterStart
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.

    --- Synchronous Event Trigger for Event Start.
    -- @function [parent=#DETECTION_BASE] Start
    -- @param #DETECTION_BASE self

    --- Asynchronous Event Trigger for Event Start.
    -- @function [parent=#DETECTION_BASE] __Start
    -- @param #DETECTION_BASE self
    -- @param #number Delay The delay in seconds.

    --- OnLeave Transition Handler for State Detecting.
    -- @function [parent=#DETECTION_BASE] OnLeaveDetecting
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @return #boolean Return false to cancel Transition.

    --- OnEnter Transition Handler for State Detecting.
    -- @function [parent=#DETECTION_BASE] OnEnterDetecting
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.

    self:AddTransition( "Detecting", "Detect", "Detecting" )
    self:AddTransition( "Detecting", "Detection", "Detecting" )

    --- OnBefore Transition Handler for Event Detect.
    -- @function [parent=#DETECTION_BASE] OnBeforeDetect
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @return #boolean Return false to cancel Transition.

    --- OnAfter Transition Handler for Event Detect.
    -- @function [parent=#DETECTION_BASE] OnAfterDetect
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.

    --- Synchronous Event Trigger for Event Detect.
    -- @function [parent=#DETECTION_BASE] Detect
    -- @param #DETECTION_BASE self

    --- Asynchronous Event Trigger for Event Detect.
    -- @function [parent=#DETECTION_BASE] __Detect
    -- @param #DETECTION_BASE self
    -- @param #number Delay The delay in seconds.

    self:AddTransition( "Detecting", "Detected", "Detecting" )

    --- OnBefore Transition Handler for Event Detected.
    -- @function [parent=#DETECTION_BASE] OnBeforeDetected
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @return #boolean Return false to cancel Transition.

    --- OnAfter Transition Handler for Event Detected.
    -- @function [parent=#DETECTION_BASE] OnAfterDetected
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @param #table Units Table of detected units.

    --- Synchronous Event Trigger for Event Detected.
    -- @function [parent=#DETECTION_BASE] Detected
    -- @param #DETECTION_BASE self
    -- @param #table Units Table of detected units.

    --- Asynchronous Event Trigger for Event Detected.
    -- @function [parent=#DETECTION_BASE] __Detected
    -- @param #DETECTION_BASE self
    -- @param #number Delay The delay in seconds.
    -- @param #table Units Table of detected units.

    self:AddTransition( "Detecting", "DetectedItem", "Detecting" )

    --- OnAfter Transition Handler for Event DetectedItem.
    -- @function [parent=#DETECTION_BASE] OnAfterDetectedItem
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @param #DetectedItem DetectedItem The DetectedItem data structure.

    self:AddTransition( "*", "Stop", "Stopped" )

    --- OnBefore Transition Handler for Event Stop.
    -- @function [parent=#DETECTION_BASE] OnBeforeStop
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @return #boolean Return false to cancel Transition.

    --- OnAfter Transition Handler for Event Stop.
    -- @function [parent=#DETECTION_BASE] OnAfterStop
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.

    --- Synchronous Event Trigger for Event Stop.
    -- @function [parent=#DETECTION_BASE] Stop
    -- @param #DETECTION_BASE self

    --- Asynchronous Event Trigger for Event Stop.
    -- @function [parent=#DETECTION_BASE] __Stop
    -- @param #DETECTION_BASE self
    -- @param #number Delay The delay in seconds.

    --- OnLeave Transition Handler for State Stopped.
    -- @function [parent=#DETECTION_BASE] OnLeaveStopped
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @return #boolean Return false to cancel Transition.

    --- OnEnter Transition Handler for State Stopped.
    -- @function [parent=#DETECTION_BASE] OnEnterStopped
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.

    return self
  end

  do -- State Transition Handling

    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    function DETECTION_BASE:onafterStart( From, Event, To )
      self:__Detect( 1 )
    end

    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    function DETECTION_BASE:onafterDetect( From, Event, To )

      local DetectDelay = 0.15
      self.DetectionCount = 0
      self.DetectionRun = 0
      self:UnIdentifyAllDetectedObjects() -- Resets the DetectedObjectsIdentified table

      local DetectionTimeStamp = timer.getTime()

      -- Reset detection cache for the next detection run.
      for DetectionObjectName, DetectedObjectData in pairs( self.DetectedObjects ) do

        self.DetectedObjects[DetectionObjectName].IsDetected = false
        self.DetectedObjects[DetectionObjectName].IsVisible = false
        self.DetectedObjects[DetectionObjectName].KnowDistance = nil
        self.DetectedObjects[DetectionObjectName].LastTime = nil
        self.DetectedObjects[DetectionObjectName].LastPos = nil
        self.DetectedObjects[DetectionObjectName].LastVelocity = nil
        self.DetectedObjects[DetectionObjectName].Distance = 10000000

      end

      -- Count alive(!) groups only. Solves issue #1173 https://github.com/FlightControl-Master/MOOSE/issues/1173
      self.DetectionCount = self:CountAliveRecce()

      local DetectionInterval = self.DetectionCount / (self.RefreshTimeInterval - 1)

      self:ForEachAliveRecce( function( DetectionGroup )
        self:__Detection( DetectDelay, DetectionGroup, DetectionTimeStamp ) -- Process each detection asynchronously.
        DetectDelay = DetectDelay + DetectionInterval
      end )

      self:__Detect( -self.RefreshTimeInterval )

    end

    -- @param #DETECTION_BASE self
    -- @param #number The amount of alive recce.
    function DETECTION_BASE:CountAliveRecce()

      return self.DetectionSet:CountAlive()

    end

    -- @param #DETECTION_BASE self
    function DETECTION_BASE:ForEachAliveRecce( IteratorFunction, ... )
      self:F2( arg )

      self.DetectionSet:ForEachGroupAlive( IteratorFunction, arg )

      return self
    end
    
    ---
    -- @param #DETECTION_BASE self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @param Wrapper.Group#GROUP Detection The Group detecting.
    -- @param #number DetectionTimeStamp Time stamp of detection event.
    function DETECTION_BASE:onafterDetection( From, Event, To, Detection, DetectionTimeStamp )

      self:T( { DetectedObjects = self.DetectedObjects } )

      self.DetectionRun = self.DetectionRun + 1

      local HasDetectedObjects = false

      if Detection and Detection:IsAlive() then

        self:T( { "DetectionGroup is Alive", Detection:GetName() } )

        local DetectionGroupName = Detection:GetName()
        local DetectionUnit = Detection:GetFirstUnitAlive()

        local DetectedUnits = {}

        local DetectedTargets = DetectionUnit:GetDetectedTargets(
          self.DetectVisual,
          self.DetectOptical,
          self.DetectRadar,
          self.DetectIRST,
          self.DetectRWR,
          self.DetectDLINK
        )

        --self:T( { DetectedTargets = DetectedTargets } )
        --self:T(UTILS.PrintTableToLog(DetectedTargets))
        
        
        for DetectionObjectID, Detection in pairs( DetectedTargets or {}) do
          local DetectedObject = Detection.object -- DCS#Object

          if DetectedObject and DetectedObject:isExist() and DetectedObject.id_ < 50000000 then -- and ( DetectedObject:getCategory() == Object.Category.UNIT or DetectedObject:getCategory() == Object.Category.STATIC ) then
            local DetectedObjectName = DetectedObject:getName()
            if not self.DetectedObjects[DetectedObjectName] then
              self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {}
              self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName 
              self.DetectedObjects[DetectedObjectName].Object = DetectedObject
            end
          end
        end

        for DetectionObjectName, DetectedObjectData in pairs( self.DetectedObjects or {}) do

          local DetectedObject = DetectedObjectData.Object

          if DetectedObject:isExist() then

            local TargetIsDetected, TargetIsVisible, TargetKnowType, TargetKnowDistance, TargetLastTime, TargetLastPos, TargetLastVelocity = DetectionUnit:IsTargetDetected(
              DetectedObject,
              self.DetectVisual,
              self.DetectOptical,
              self.DetectRadar,
              self.DetectIRST,
              self.DetectRWR,
              self.DetectDLINK
            )

            -- self:T2( { TargetIsDetected = TargetIsDetected, TargetIsVisible = TargetIsVisible, TargetLastTime = TargetLastTime, TargetKnowType = TargetKnowType, TargetKnowDistance = TargetKnowDistance, TargetLastPos = TargetLastPos, TargetLastVelocity = TargetLastVelocity } )

            -- Only process if the target is visible. Detection also returns invisible units.
            --if Detection.visible == true then
            
              local DetectionAccepted = true
              
              local DetectedObjectName = DetectedObject:getName()
              local DetectedObjectType = DetectedObject:getTypeName()
      
              local DetectedObjectVec3 = DetectedObject:getPoint()
              local DetectedObjectVec2 = { x = DetectedObjectVec3.x, y = DetectedObjectVec3.z }
              local DetectionGroupVec3 = Detection:GetVec3() or {x=0,y=0,z=0}
              local DetectionGroupVec2 = { x = DetectionGroupVec3.x, y = DetectionGroupVec3.z }
      
              local Distance = ( ( DetectedObjectVec3.x - DetectionGroupVec3.x )^2 +
                ( DetectedObjectVec3.y - DetectionGroupVec3.y )^2 +
                ( DetectedObjectVec3.z - DetectionGroupVec3.z )^2
                ) ^ 0.5 / 1000
  
              local DetectedUnitCategory = DetectedObject:getDesc().category
      
              --self:F( { "Detected Target:", DetectionGroupName, DetectedObjectName, DetectedObjectType, Distance, DetectedUnitCategory } )
  
              -- Calculate Acceptance
              
              DetectionAccepted = self._.FilterCategories[DetectedUnitCategory] ~= nil and DetectionAccepted or false
      
  --            if Distance > 15000 then
  --              if DetectedUnitCategory == Unit.Category.GROUND_UNIT or DetectedUnitCategory == Unit.Category.SHIP then
  --                if DetectedObject:hasSensors( Unit.SensorType.RADAR, Unit.RadarType.AS ) == false then
  --                  DetectionAccepted = false
  --                end
  --              end
  --            end
      
              if self.AcceptRange and Distance * 1000 > self.AcceptRange then
                DetectionAccepted = false
              end
              
              if self.AcceptZones then
                local AnyZoneDetection = false
                for AcceptZoneID, AcceptZone in pairs( self.AcceptZones ) do
                  local AcceptZone = AcceptZone -- Core.Zone#ZONE_BASE
                  if AcceptZone:IsVec2InZone( DetectedObjectVec2 ) then
                    AnyZoneDetection = true
                  end
                end
                if not AnyZoneDetection then
                  DetectionAccepted = false            
                end
              end
  
              if self.RejectZones then
                for RejectZoneID, RejectZone in pairs( self.RejectZones ) do
                  local RejectZone = RejectZone -- Core.Zone#ZONE_BASE
                  if RejectZone:IsVec2InZone( DetectedObjectVec2 ) == true then
                    DetectionAccepted = false
                  end
                end
              end
              
              -- Calculate radar blur probability
              
              if self.RadarBlur then
                MESSAGE:New("Radar Blur",10):ToLogIf(self.debug):ToAllIf(self.verbose)
                local minheight = self.RadarBlurMinHeight or 250 -- meters
                local thresheight = self.RadarBlurThresHeight or 90 -- 10% chance to find a low flying group
                local thresblur = self.RadarBlurThresBlur or 85 -- 25% chance to escape the radar overall
                local dist = math.floor(Distance)
                if dist <= self.RadarBlurClosing  then
                  thresheight = (((dist*dist)/self.RadarBlurClosingSquare)*thresheight)
                  thresblur = (((dist*dist)/self.RadarBlurClosingSquare)*thresblur)
                end
                local fheight = math.floor(math.random(1,10000)/100)
                local fblur = math.floor(math.random(1,10000)/100)
                local unit = UNIT:FindByName(DetectedObjectName)
                if unit and unit:IsAlive() then
                  local AGL = unit:GetAltitude(true)
                  MESSAGE:New("Unit "..DetectedObjectName.." is at "..math.floor(AGL).."m. Distance "..math.floor(Distance).."km.",10):ToLogIf(self.debug):ToAllIf(self.verbose)
                  MESSAGE:New(string.format("fheight = %d/%d | fblur = %d/%d",fheight,thresheight,fblur,thresblur),10):ToLogIf(self.debug):ToAllIf(self.verbose)
                  if fblur > thresblur then DetectionAccepted = false end
                  if AGL <= minheight and fheight < thresheight then DetectionAccepted = false end  
                  MESSAGE:New("Detection Accepted = "..tostring(DetectionAccepted),10):ToLogIf(self.debug):ToAllIf(self.verbose)              
                end
              end
              
              -- Calculate additional probabilities
              
              if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.DistanceProbability then
                local DistanceFactor = Distance / 4
                local DistanceProbabilityReversed = ( 1 - self.DistanceProbability ) * DistanceFactor
                local DistanceProbability = 1 - DistanceProbabilityReversed
                DistanceProbability = DistanceProbability * 30 / 300
                local Probability = math.random() -- Selects a number between 0 and 1
                --self:T( { Probability, DistanceProbability } )
                if Probability > DistanceProbability then
                  DetectionAccepted = false
                end
              end
              
              if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.AlphaAngleProbability then
                local NormalVec2 = { x = DetectedObjectVec2.x - DetectionGroupVec2.x, y = DetectedObjectVec2.y - DetectionGroupVec2.y }
                local AlphaAngle = math.atan2( NormalVec2.y, NormalVec2.x )
                local Sinus = math.sin( AlphaAngle )
                local AlphaAngleProbabilityReversed = ( 1 - self.AlphaAngleProbability ) * ( 1 - Sinus )
                local AlphaAngleProbability = 1 - AlphaAngleProbabilityReversed
                
                AlphaAngleProbability = AlphaAngleProbability * 30 / 300
                
                local Probability =  math.random() -- Selects a number between 0 and 1
                --self:T( { Probability, AlphaAngleProbability } )
                if Probability > AlphaAngleProbability then
                  DetectionAccepted = false
                end
                 
              end
              
              if not self.DetectedObjects[DetectedObjectName] and TargetIsVisible and self.ZoneProbability then
              
                for ZoneDataID, ZoneData in pairs( self.ZoneProbability ) do
                  self:F({ZoneData})
                  local ZoneObject = ZoneData[1] -- Core.Zone#ZONE_BASE
                  local ZoneProbability = ZoneData[2] -- #number
                  ZoneProbability = ZoneProbability * 30 / 300
                  
                  if ZoneObject:IsVec2InZone( DetectedObjectVec2 ) == true then
                    local Probability =  math.random() -- Selects a number between 0 and 1
                    --self:T( { Probability, ZoneProbability } )
                    if Probability > ZoneProbability then
                      DetectionAccepted = false
                      break
                    end
                  end
                end
              end
              
              if DetectionAccepted then
                
                HasDetectedObjects = true
      
                self.DetectedObjects[DetectedObjectName] = self.DetectedObjects[DetectedObjectName] or {}
                self.DetectedObjects[DetectedObjectName].Name = DetectedObjectName
  
                if TargetIsDetected and TargetIsDetected == true then
                  self.DetectedObjects[DetectedObjectName].IsDetected = TargetIsDetected
                end
                
                if TargetIsDetected and TargetIsVisible and TargetIsVisible == true then
                  self.DetectedObjects[DetectedObjectName].IsVisible = TargetIsDetected and TargetIsVisible
                end
                
                if TargetIsDetected and not self.DetectedObjects[DetectedObjectName].KnowType then
                  self.DetectedObjects[DetectedObjectName].KnowType = TargetIsDetected and TargetKnowType
              end
              self.DetectedObjects[DetectedObjectName].KnowDistance = TargetKnowDistance -- Detection.distance   -- TargetKnowDistance
              self.DetectedObjects[DetectedObjectName].LastTime = (TargetIsDetected and TargetIsVisible == false) and TargetLastTime
              self.DetectedObjects[DetectedObjectName].LastPos = (TargetIsDetected and TargetIsVisible == false) and TargetLastPos
              self.DetectedObjects[DetectedObjectName].LastVelocity = (TargetIsDetected and TargetIsVisible == false) and TargetLastVelocity

              if not self.DetectedObjects[DetectedObjectName].Distance or (Distance and self.DetectedObjects[DetectedObjectName].Distance > Distance) then
                self.DetectedObjects[DetectedObjectName].Distance = Distance
              end

              self.DetectedObjects[DetectedObjectName].DetectionTimeStamp = DetectionTimeStamp

              self:F( { DetectedObject = self.DetectedObjects[DetectedObjectName] } )

              local DetectedUnit = UNIT:FindByName( DetectedObjectName )

              DetectedUnits[DetectedObjectName] = DetectedUnit
            else
              -- if beyond the DetectionRange then nullify...
              self:F( { DetectedObject = "No more detection for " .. DetectedObjectName } )
              if self.DetectedObjects[DetectedObjectName] then
                self.DetectedObjects[DetectedObjectName] = nil
              end
            end

            -- self:T2( self.DetectedObjects )
          else
            -- The previously detected object does not exist anymore, delete from the cache.
            self:F( "Removing from DetectedObjects: " .. DetectionObjectName )
            self.DetectedObjects[DetectionObjectName] = nil
          end
        end

        if HasDetectedObjects then
          self:__Detected( 0.1, DetectedUnits )
        end

      end

      if self.DetectionCount > 0 and self.DetectionRun == self.DetectionCount then

        -- First check if all DetectedObjects were detected.
        -- This is important. When there are DetectedObjects in the list, but were not detected,
        -- And these remain undetected for more than 60 seconds, then these DetectedObjects will be flagged as not Detected.
        -- IsDetected = false!
        -- This is used in A2A_TASK_DISPATCHER to initiate fighter sweeping! The TASK_A2A_INTERCEPT tasks will be replaced with TASK_A2A_SWEEP tasks.
        for DetectedObjectName, DetectedObject in pairs( self.DetectedObjects ) do
          if self.DetectedObjects[DetectedObjectName].IsDetected == true and self.DetectedObjects[DetectedObjectName].DetectionTimeStamp + 300 <= DetectionTimeStamp then
            self.DetectedObjects[DetectedObjectName].IsDetected = false
          end
        end

        self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method.

        for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do

          self:UpdateDetectedItemDetection( DetectedItem )

          self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list.

          if DetectedItem then
            self:__DetectedItem( 0.1, DetectedItem )
          end

        end
      end

    end

  end

  do -- DetectionItems Creation

    --- Clean the DetectedItem table.
    -- @param #DETECTION_BASE self
    -- @return #DETECTION_BASE
    function DETECTION_BASE:CleanDetectionItem( DetectedItem, DetectedItemID )

      -- We clean all DetectedItems.
      -- if there are any remaining DetectedItems with no Set Objects then the Item in the DetectedItems must be deleted.

      local DetectedSet = DetectedItem.Set

      if DetectedSet:Count() == 0 then
        self:RemoveDetectedItem( DetectedItemID )
      end

      return self
    end

    --- Forget a Unit from a DetectionItem
    -- @param #DETECTION_BASE self
    -- @param #string UnitName The UnitName that needs to be forgotten from the DetectionItem Sets.
    -- @return #DETECTION_BASE
    function DETECTION_BASE:ForgetDetectedUnit( UnitName )

      local DetectedItems = self:GetDetectedItems()

      for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do
        local DetectedSet = self:GetDetectedItemSet( DetectedItem )
        if DetectedSet then
          DetectedSet:RemoveUnitsByName( UnitName )
        end
      end

      return self
    end

    --- Make a DetectionSet table. This function will be overridden in the derived clsses.
    -- @param #DETECTION_BASE self
    -- @return #DETECTION_BASE
    function DETECTION_BASE:CreateDetectionItems()

      self:F( "Error, in DETECTION_BASE class..." )
      return self
    end

  end

  do -- Initialization methods

    --- Detect Visual.
    -- @param #DETECTION_BASE self
    -- @param #boolean DetectVisual
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:InitDetectVisual( DetectVisual )

      self.DetectVisual = DetectVisual

      return self
    end

    --- Detect Optical.
    -- @param #DETECTION_BASE self
    -- @param #boolean DetectOptical
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:InitDetectOptical( DetectOptical )
      self:F2()

      self.DetectOptical = DetectOptical

      return self
    end

    --- Detect Radar.
    -- @param #DETECTION_BASE self
    -- @param #boolean DetectRadar
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:InitDetectRadar( DetectRadar )
      self:F2()

      self.DetectRadar = DetectRadar

      return self
    end

    --- Detect IRST.
    -- @param #DETECTION_BASE self
    -- @param #boolean DetectIRST
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:InitDetectIRST( DetectIRST )
      self:F2()

      self.DetectIRST = DetectIRST

      return self
    end

    --- Detect RWR.
    -- @param #DETECTION_BASE self
    -- @param #boolean DetectRWR
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:InitDetectRWR( DetectRWR )
      self:F2()

      self.DetectRWR = DetectRWR

      return self
    end

    --- Detect DLINK.
    -- @param #DETECTION_BASE self
    -- @param #boolean DetectDLINK
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:InitDetectDLINK( DetectDLINK )
      self:F2()

      self.DetectDLINK = DetectDLINK

      return self
    end

  end

  do -- Filter methods

    --- Filter the detected units based on Unit.Category  
    -- The different values of Unit.Category can be:
    -- 
    --   * Unit.Category.AIRPLANE
    --   * Unit.Category.GROUND_UNIT
    --   * Unit.Category.HELICOPTER
    --   * Unit.Category.SHIP
    --   * Unit.Category.STRUCTURE
    --   
    -- Multiple Unit.Category entries can be given as a table and then these will be evaluated as an OR expression.
    -- 
    -- Example to filter a single category (Unit.Category.AIRPLANE).
    -- 
    --     DetectionObject:FilterCategories( Unit.Category.AIRPLANE ) 
    -- 
    -- Example to filter multiple categories (Unit.Category.AIRPLANE, Unit.Category.HELICOPTER). Note the {}.
    -- 
    --     DetectionObject:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } )
    -- 
    -- @param #DETECTION_BASE self
    -- @param #list<DCS#Unit> FilterCategories The Categories entries
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:FilterCategories( FilterCategories )
      self:F2()

      self._.FilterCategories = {}
      if type( FilterCategories ) == "table" then
        for CategoryID, Category in pairs( FilterCategories ) do
          self._.FilterCategories[Category] = Category
        end
      else
        self._.FilterCategories[FilterCategories] = FilterCategories
      end
      return self

    end
    
    --- Method to make the radar detection less accurate, e.g. for WWII scenarios.
    -- @param #DETECTION_BASE self
    -- @param #number minheight Minimum flight height to be detected, in meters AGL (above ground)
    -- @param #number thresheight Threshold to escape the radar if flying below minheight, defaults to 90 (90% escape chance)
    -- @param #number thresblur Threshold to be detected by the radar overall, defaults to 85 (85% chance to be found)
    -- @param #number closing Closing-in in km - the limit of km from which on it becomes increasingly difficult to escape radar detection if flying towards the radar position. Should be about 1/3 of the radar detection radius in kilometers, defaults to 20.
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:SetRadarBlur(minheight,thresheight,thresblur,closing)
      self.RadarBlur = true
      self.RadarBlurMinHeight = minheight or 250 -- meters
      self.RadarBlurThresHeight = thresheight or 90 -- 10% chance to find a low flying group
      self.RadarBlurThresBlur = thresblur or 85 -- 25% chance to escape the radar overall
      self.RadarBlurClosing = closing or 20 -- 20km
      self.RadarBlurClosingSquare = self.RadarBlurClosing * self.RadarBlurClosing 
      return self
    end
    
  end

  do

    --- Set the detection interval time in seconds.
    -- @param #DETECTION_BASE self
    -- @param #number RefreshTimeInterval Interval in seconds.
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:SetRefreshTimeInterval( RefreshTimeInterval )
      self:F2()

      self.RefreshTimeInterval = RefreshTimeInterval

      return self
    end

  end

  do -- Friendlies Radius

    --- Set the radius in meters to validate if friendlies are nearby.
    -- @param #DETECTION_BASE self
    -- @param #number FriendliesRange Radius to use when checking if Friendlies are nearby.
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:SetFriendliesRange( FriendliesRange ) -- R2.2 Friendlies range
      self:F2()

      self.FriendliesRange = FriendliesRange

      return self
    end

  end

  do -- Intercept Point

    --- Set the parameters to calculate to optimal intercept point.
    -- @param #DETECTION_BASE self
    -- @param #boolean Intercept Intercept is true if an intercept point is calculated. Intercept is false if it is disabled. The default Intercept is false.
    -- @param #number InterceptDelay If Intercept is true, then InterceptDelay is the average time it takes to get airplanes airborne.
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:SetIntercept( Intercept, InterceptDelay )
      self:F2()

      self.Intercept = Intercept
      self.InterceptDelay = InterceptDelay

      return self
    end

  end

  do -- Accept / Reject detected units

    --- Accept detections if within a range in meters.
    -- @param #DETECTION_BASE self
    -- @param #number AcceptRange Accept a detection if the unit is within the AcceptRange in meters.
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:SetAcceptRange( AcceptRange )
      self:F2()

      self.AcceptRange = AcceptRange

      return self
    end

    --- Accept detections if within the specified zone(s).
    -- @param #DETECTION_BASE self
    -- @param Core.Zone#ZONE_BASE AcceptZones Can be a list or ZONE_BASE objects, or a single ZONE_BASE object.
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:SetAcceptZones( AcceptZones )
      self:F2()

      if type( AcceptZones ) == "table" then
        if AcceptZones.ClassName and AcceptZones:IsInstanceOf( ZONE_BASE ) then
          self.AcceptZones = { AcceptZones }
        else
          self.AcceptZones = AcceptZones
        end
      else
        self:F( { "AcceptZones must be a list of ZONE_BASE derived objects or one ZONE_BASE derived object", AcceptZones } )
        error()
      end

      return self
    end

    --- Reject detections if within the specified zone(s).
    -- @param #DETECTION_BASE self
    -- @param Core.Zone#ZONE_BASE RejectZones Can be a list or ZONE_BASE objects, or a single ZONE_BASE object.
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:SetRejectZones( RejectZones )
      self:F2()

      if type( RejectZones ) == "table" then
        if RejectZones.ClassName and RejectZones:IsInstanceOf( ZONE_BASE ) then
          self.RejectZones = { RejectZones }
        else
          self.RejectZones = RejectZones
        end
      else
        self:F( { "RejectZones must be a list of ZONE_BASE derived objects or one ZONE_BASE derived object", RejectZones } )
        error()
      end

      return self
    end

  end

  do -- Probability methods

    --- Upon a **visual** detection, the further away a detected object is, the less likely it is to be detected properly.
    -- Also, the speed of accurate detection plays a role.
    -- A distance probability factor between 0 and 1 can be given, that will model a linear extrapolated probability over 10 km distance.
    -- For example, if a probability factor of 0.6 (60%) is given, the extrapolated probabilities over 15 kilometers would like like:
    -- 1 km: 96%, 2 km: 92%, 3 km: 88%, 4 km: 84%, 5 km: 80%, 6 km: 76%, 7 km: 72%, 8 km: 68%, 9 km: 64%, 10 km: 60%, 11 km: 56%, 12 km: 52%, 13 km: 48%, 14 km: 44%, 15 km: 40%.
    -- @param #DETECTION_BASE self
    -- @param DistanceProbability The probability factor.
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:SetDistanceProbability( DistanceProbability )
      self:F2()

      self.DistanceProbability = DistanceProbability

      return self
    end

    --- Upon a **visual** detection, the higher the unit is during the detecting process, the more likely the detected unit is to be detected properly.
    -- A detection at a 90% alpha angle is the most optimal, a detection at 10% is less and a detection at 0% is less likely to be correct.
    -- 
    -- A probability factor between 0 and 1 can be given, that will model a progressive extrapolated probability if the target would be detected at a 0° angle.
    -- 
    -- For example, if a alpha angle probability factor of 0.7 is given, the extrapolated probabilities of the different angles would look like:
    -- 0°: 70%, 10°: 75,21%, 20°: 80,26%, 30°: 85%, 40°: 89,28%, 50°: 92,98%, 60°: 95,98%, 70°: 98,19%, 80°: 99,54%, 90°: 100%
    -- @param #DETECTION_BASE self
    -- @param AlphaAngleProbability The probability factor.
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:SetAlphaAngleProbability( AlphaAngleProbability )
      self:F2()

      self.AlphaAngleProbability = AlphaAngleProbability

      return self
    end

    --- Upon a **visual** detection, the more a detected unit is within a cloudy zone, the less likely the detected unit is to be detected successfully.
    -- The Cloudy Zones work with the ZONE_BASE derived classes. The mission designer can define within the mission
    -- zones that reflect cloudy areas where detected units may not be so easily visually detected.
    -- @param #DETECTION_BASE self
    -- @param ZoneArray Aray of a The ZONE_BASE object and a ZoneProbability pair..
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:SetZoneProbability( ZoneArray )
      self:F2()

      self.ZoneProbability = ZoneArray

      return self
    end

  end

  do -- Change processing

    --- Accepts changes from the detected item.
    -- @param #DETECTION_BASE self
    -- @param #DETECTION_BASE.DetectedItem DetectedItem
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:AcceptChanges( DetectedItem )

      DetectedItem.Changed = false
      DetectedItem.Changes = {}

      return self
    end

    --- Add a change to the detected zone.
    -- @param #DETECTION_BASE self
    -- @param #DETECTION_BASE.DetectedItem DetectedItem
    -- @param #string ChangeCode
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:AddChangeItem( DetectedItem, ChangeCode, ItemUnitType )

      DetectedItem.Changed = true
      local ID = DetectedItem.ID

      DetectedItem.Changes = DetectedItem.Changes or {}
      DetectedItem.Changes[ChangeCode] = DetectedItem.Changes[ChangeCode] or {}
      DetectedItem.Changes[ChangeCode].ID = ID
      DetectedItem.Changes[ChangeCode].ItemUnitType = ItemUnitType

      self:F( { "Change on Detected Item:", DetectedItemID = DetectedItem.ID, ChangeCode = ChangeCode, ItemUnitType = ItemUnitType } )

      return self
    end

    --- Add a change to the detected zone.
    -- @param #DETECTION_BASE self
    -- @param #DETECTION_BASE.DetectedItem DetectedItem
    -- @param #string ChangeCode
    -- @param #string ChangeUnitType
    -- @return #DETECTION_BASE self
    function DETECTION_BASE:AddChangeUnit( DetectedItem, ChangeCode, ChangeUnitType )

      DetectedItem.Changed = true
      local ID = DetectedItem.ID

      DetectedItem.Changes = DetectedItem.Changes or {}
      DetectedItem.Changes[ChangeCode] = DetectedItem.Changes[ChangeCode] or {}
      DetectedItem.Changes[ChangeCode][ChangeUnitType] = DetectedItem.Changes[ChangeCode][ChangeUnitType] or 0
      DetectedItem.Changes[ChangeCode][ChangeUnitType] = DetectedItem.Changes[ChangeCode][ChangeUnitType] + 1
      DetectedItem.Changes[ChangeCode].ID = ID

      self:F( { "Change on Detected Unit:", DetectedItemID = DetectedItem.ID, ChangeCode = ChangeCode, ChangeUnitType = ChangeUnitType } )

      return self
    end

  end

  do -- Friendly calculations

    --- This will allow during friendly search any recce or detection unit to be also considered as a friendly.
    -- By default, recce aren't considered friendly, because that would mean that a recce would be also an attacking friendly,
    -- and this is wrong.
    -- However, in a CAP situation, when the CAP is part of an EWR network, the CAP is also an attacker.
    -- This, this method allows to register for a detection the CAP unit name prefixes to be considered CAP.
    -- @param #DETECTION_BASE self
    -- @param #string FriendlyPrefixes A string or a list of prefixes.
    -- @return #DETECTION_BASE 
    function DETECTION_BASE:SetFriendlyPrefixes( FriendlyPrefixes )

      self.FriendlyPrefixes = self.FriendlyPrefixes or {}
      if type( FriendlyPrefixes ) ~= "table" then
        FriendlyPrefixes = { FriendlyPrefixes }
      end
      for PrefixID, Prefix in pairs( FriendlyPrefixes ) do
        self:F( { FriendlyPrefix = Prefix } )
        self.FriendlyPrefixes[Prefix] = Prefix
      end
      return self
    end

    --- This will allow during friendly search only units of the specified list of categories.
    -- @param #DETECTION_BASE self
    -- @param #string FriendlyCategories A list of unit categories.
    -- @return #DETECTION_BASE 
    -- @usage
    --    -- Only allow Ships and Vehicles to be part of the friendly team.
    --    Detection:SetFriendlyCategories( { Unit.Category.SHIP, Unit.Category.GROUND_UNIT } )

    --- Returns if there are friendlies nearby the FAC units ...
    -- @param #DETECTION_BASE self
    -- @param #DETECTION_BASE.DetectedItem DetectedItem
    -- @param DCS#Unit.Category Category The category of the unit.
    -- @return #boolean true if there are friendlies nearby 
    function DETECTION_BASE:IsFriendliesNearBy( DetectedItem, Category )
      --      self:F( { "FriendliesNearBy Test", DetectedItem.FriendliesNearBy } )
      return (DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category] ~= nil) or false
    end

    --- Returns friendly units nearby the FAC units ...
    -- @param #DETECTION_BASE self
    -- @param #DETECTION_BASE.DetectedItem DetectedItem
    -- @param DCS#Unit.Category Category The category of the unit.
    -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs.
    function DETECTION_BASE:GetFriendliesNearBy( DetectedItem, Category )

      return DetectedItem.FriendliesNearBy and DetectedItem.FriendliesNearBy[Category]
    end

    --- Returns if there are friendlies nearby the intercept ...
    -- @param #DETECTION_BASE self
    -- @param #DETECTION_BASE.DetectedItem DetectedItem
    -- @return #boolean trhe if there are friendlies near the intercept.
    function DETECTION_BASE:IsFriendliesNearIntercept( DetectedItem )

      return DetectedItem.FriendliesNearIntercept ~= nil or false
    end

    --- Returns friendly units nearby the intercept point ...
    -- @param #DETECTION_BASE self
    -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item.
    -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs. 
    function DETECTION_BASE:GetFriendliesNearIntercept( DetectedItem )

      return DetectedItem.FriendliesNearIntercept
    end

    --- Returns the distance used to identify friendlies near the detected item ...
    -- @param #DETECTION_BASE self
    -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item.
    -- @return #table A table of distances to friendlies. 
    function DETECTION_BASE:GetFriendliesDistance( DetectedItem )

      return DetectedItem.FriendliesDistance
    end

    --- Returns if there are friendlies nearby the FAC units ...
    -- @param #DETECTION_BASE self
    -- @param #DETECTION_BASE.DetectedItem DetectedItem
    -- @return #boolean true if there are friendlies nearby
    function DETECTION_BASE:IsPlayersNearBy( DetectedItem )

      return DetectedItem.PlayersNearBy ~= nil
    end

    --- Returns friendly units nearby the FAC units ...
    -- @param #DETECTION_BASE self
    -- @param #DETECTION_BASE.DetectedItem DetectedItem The detected item.
    -- @return #map<#string,Wrapper.Unit#UNIT> The map of Friendly UNITs.
    function DETECTION_BASE:GetPlayersNearBy( DetectedItem )

      return DetectedItem.PlayersNearBy
    end

    --- Background worker function to determine if there are friendlies nearby ...
    -- @param #DETECTION_BASE self
    -- @param #table TargetData
    function DETECTION_BASE:ReportFriendliesNearBy( TargetData )
      -- self:F( { "Search Friendlies", DetectedItem = TargetData.DetectedItem } )

      local DetectedItem = TargetData.DetectedItem -- #DETECTION_BASE.DetectedItem
      local DetectedSet = TargetData.DetectedItem.Set
      local DetectedUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT

      DetectedItem.FriendliesNearBy = nil

      -- We need to ensure that the DetectedUnit is alive!
      if DetectedUnit and DetectedUnit:IsAlive() then

        local DetectedUnitCoord = DetectedUnit:GetCoordinate()
        local InterceptCoord = TargetData.InterceptCoord or DetectedUnitCoord

        local SphereSearch = {
          id = world.VolumeType.SPHERE,
          params = {
            point = InterceptCoord:GetVec3(),
            radius = self.FriendliesRange,
          }
        }

        -- @param DCS#Unit FoundDCSUnit
        -- @param Wrapper.Group#GROUP ReportGroup
        -- @param Core.Set#SET_GROUP ReportSetGroup
        local FindNearByFriendlies = function( FoundDCSUnit, ReportGroupData )

          local DetectedItem = ReportGroupData.DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem
          local DetectedSet = ReportGroupData.DetectedItem.Set
          local DetectedUnit = DetectedSet:GetFirst() -- Wrapper.Unit#UNIT
          local DetectedUnitCoord = DetectedUnit:GetCoordinate()
          local InterceptCoord = ReportGroupData.InterceptCoord or DetectedUnitCoord
          local ReportSetGroup = ReportGroupData.ReportSetGroup

          local EnemyCoalition = DetectedUnit:GetCoalition()

          local FoundUnitCoalition = FoundDCSUnit:getCoalition()
          local FoundUnitCategory = FoundDCSUnit:getDesc().category
          local FoundUnitName = FoundDCSUnit:getName()
          local FoundUnitGroupName = FoundDCSUnit:getGroup():getName()
          local EnemyUnitName = DetectedUnit:GetName()

          local FoundUnitInReportSetGroup = ReportSetGroup:FindGroup( FoundUnitGroupName ) ~= nil
          -- self:T( { "Friendlies search:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } )

          if FoundUnitInReportSetGroup == true then
            -- If the recce was part of the friendlies found, then check if the recce is part of the allowed friendly unit prefixes.
            for PrefixID, Prefix in pairs( self.FriendlyPrefixes or {} ) do
              -- self:F( { "Friendly Prefix:", Prefix = Prefix } )
              -- In case a match is found (so a recce unit name is part of the friendly prefixes), then report that recce to be part of the friendlies.
              -- This is important if CAP planes (so planes using their own radar) to be scanning for targets as part of the EWR network.
              -- But CAP planes are also attackers, so they need to be considered friendlies too!
              -- I chose to use prefixes because it is the fastest way to check.
              if string.find( FoundUnitName, Prefix:gsub( "-", "%%-" ), 1 ) then
                FoundUnitInReportSetGroup = false
                break
              end
            end
          end

          -- self:F( { "Friendlies near Target:", FoundUnitName, FoundUnitCoalition, EnemyUnitName, EnemyCoalition, FoundUnitInReportSetGroup } )

          if FoundUnitCoalition ~= EnemyCoalition and FoundUnitInReportSetGroup == false then
            local FriendlyUnit = UNIT:Find( FoundDCSUnit )
            local FriendlyUnitName = FriendlyUnit:GetName()
            local FriendlyUnitCategory = FriendlyUnit:GetDesc().category

            -- Friendlies are sorted per unit category.            
            DetectedItem.FriendliesNearBy = DetectedItem.FriendliesNearBy or {}
            DetectedItem.FriendliesNearBy[FoundUnitCategory] = DetectedItem.FriendliesNearBy[FoundUnitCategory] or {}
            DetectedItem.FriendliesNearBy[FoundUnitCategory][FriendlyUnitName] = FriendlyUnit

            local Distance = DetectedUnitCoord:Get2DDistance( FriendlyUnit:GetCoordinate() )
            DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {}
            DetectedItem.FriendliesDistance[Distance] = FriendlyUnit
            -- self:F( { "Friendlies Found:", FriendlyUnitName = FriendlyUnitName, Distance = Distance, FriendlyUnitCategory = FriendlyUnitCategory, FriendliesCategory = self.FriendliesCategory } )
            return true
          end

          return true
        end

        world.searchObjects( Object.Category.UNIT, SphereSearch, FindNearByFriendlies, TargetData )

        DetectedItem.PlayersNearBy = nil

        _DATABASE:ForEachPlayer(
        -- @param Wrapper.Unit#UNIT PlayerUnit
        function( PlayerUnitName )
          local PlayerUnit = UNIT:FindByName( PlayerUnitName )

          -- Fix for issue https://github.com/FlightControl-Master/MOOSE/issues/1225
          if PlayerUnit and PlayerUnit:IsAlive() then
            local coord = PlayerUnit:GetCoordinate()

            if coord and coord:IsInRadius( DetectedUnitCoord, self.FriendliesRange ) then

              local PlayerUnitCategory = PlayerUnit:GetDesc().category

              if (not self.FriendliesCategory) or (self.FriendliesCategory and (self.FriendliesCategory == PlayerUnitCategory)) then

                local PlayerUnitName = PlayerUnit:GetName()

                DetectedItem.PlayersNearBy = DetectedItem.PlayersNearBy or {}
                DetectedItem.PlayersNearBy[PlayerUnitName] = PlayerUnit

                -- Friendlies are sorted per unit category.            
                DetectedItem.FriendliesNearBy = DetectedItem.FriendliesNearBy or {}
                DetectedItem.FriendliesNearBy[PlayerUnitCategory] = DetectedItem.FriendliesNearBy[PlayerUnitCategory] or {}
                DetectedItem.FriendliesNearBy[PlayerUnitCategory][PlayerUnitName] = PlayerUnit

                local Distance = DetectedUnitCoord:Get2DDistance( PlayerUnit:GetCoordinate() )
                DetectedItem.FriendliesDistance = DetectedItem.FriendliesDistance or {}
                DetectedItem.FriendliesDistance[Distance] = PlayerUnit

              end
            end
          end
        end )
      end

      self:F( { Friendlies = DetectedItem.FriendliesNearBy, Players = DetectedItem.PlayersNearBy } )

    end

  end

  --- Determines if a detected object has already been identified during detection processing.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedObject DetectedObject
  -- @return #boolean true if already identified.
  function DETECTION_BASE:IsDetectedObjectIdentified( DetectedObject )

    local DetectedObjectName = DetectedObject.Name
    if DetectedObjectName then
      local DetectedObjectIdentified = self.DetectedObjectsIdentified[DetectedObjectName] == true
      return DetectedObjectIdentified
    else
      return nil
    end
  end

  --- Identifies a detected object during detection processing.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedObject DetectedObject
  function DETECTION_BASE:IdentifyDetectedObject( DetectedObject )
    -- self:F( { "Identified:", DetectedObject.Name } )

    local DetectedObjectName = DetectedObject.Name
    self.DetectedObjectsIdentified[DetectedObjectName] = true
  end

  --- UnIdentify a detected object during detection processing.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedObject DetectedObject
  function DETECTION_BASE:UnIdentifyDetectedObject( DetectedObject )

    local DetectedObjectName = DetectedObject.Name
    self.DetectedObjectsIdentified[DetectedObjectName] = false
  end

  --- UnIdentify all detected objects during detection processing.
  -- @param #DETECTION_BASE self
  function DETECTION_BASE:UnIdentifyAllDetectedObjects()

    self.DetectedObjectsIdentified = {} -- Table will be garbage collected.
  end

  --- Gets a detected object with a given name.
  -- @param #DETECTION_BASE self
  -- @param #string ObjectName
  -- @return #DETECTION_BASE.DetectedObject
  function DETECTION_BASE:GetDetectedObject( ObjectName )
    self:F2( { ObjectName = ObjectName } )

    if ObjectName then
      local DetectedObject = self.DetectedObjects[ObjectName]

      if DetectedObject then
        -- self:F( { DetectedObjects = self.DetectedObjects } )
        -- Only return detected objects that are alive!
        local DetectedUnit = UNIT:FindByName( ObjectName )
        if DetectedUnit and DetectedUnit:IsAlive() then
          if self:IsDetectedObjectIdentified( DetectedObject ) == false then
            -- self:F( { DetectedObject = DetectedObject } )
            return DetectedObject
          end
        end
      end
    end

    return nil
  end

  --- Gets a detected unit type name, taking into account the detection results.
  -- @param #DETECTION_BASE self
  -- @param Wrapper.Unit#UNIT DetectedUnit
  -- @return #string The type name
  function DETECTION_BASE:GetDetectedUnitTypeName( DetectedUnit )
    -- self:F2( ObjectName )

    if DetectedUnit and DetectedUnit:IsAlive() then
      local DetectedUnitName = DetectedUnit:GetName()
      local DetectedObject = self.DetectedObjects[DetectedUnitName]

      if DetectedObject then
        if DetectedObject.KnowType then
          return DetectedUnit:GetTypeName()
        else
          return "Unknown"
        end
      else
        return "Unknown"
      end
    else
      return "Dead:" .. DetectedUnit:GetName()
    end

    return "Undetected:" .. DetectedUnit:GetName()
  end

  --- Adds a new DetectedItem to the DetectedItems list.
  -- The DetectedItem is a table and contains a SET_UNIT in the field Set.
  -- @param #DETECTION_BASE self
  -- @param #string ItemPrefix Prefix of detected item.
  -- @param #number DetectedItemKey The key of the DetectedItem. Default self.DetectedItemMax. Could also be a string in principle.
  -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added.
  -- @return #DETECTION_BASE.DetectedItem
  function DETECTION_BASE:AddDetectedItem( ItemPrefix, DetectedItemKey, Set )

    local DetectedItem = {} -- #DETECTION_BASE.DetectedItem
    self.DetectedItemCount = self.DetectedItemCount + 1
    self.DetectedItemMax = self.DetectedItemMax + 1

    DetectedItemKey = DetectedItemKey or self.DetectedItemMax
    self.DetectedItems[DetectedItemKey] = DetectedItem
    self.DetectedItemsByIndex[DetectedItemKey] = DetectedItem
    DetectedItem.Index = DetectedItemKey

    DetectedItem.Set = Set or SET_UNIT:New():FilterDeads():FilterCrashes()
    DetectedItem.ItemID = ItemPrefix .. "." .. self.DetectedItemMax
    DetectedItem.ID = self.DetectedItemMax
    DetectedItem.Removed = false

    if self.Locking then
      self:LockDetectedItem( DetectedItem )
    end

    return DetectedItem
  end

  --- Adds a new DetectedItem to the DetectedItems list.
  -- The DetectedItem is a table and contains a SET_UNIT in the field Set.
  -- @param #DETECTION_BASE self
  -- @param DetectedItemKey The key of the DetectedItem.
  -- @param Core.Set#SET_UNIT Set (optional) The Set of Units to be added.
  -- @param Core.Zone#ZONE_UNIT Zone (optional) The Zone to be added where the Units are located.
  -- @return #DETECTION_BASE.DetectedItem
  function DETECTION_BASE:AddDetectedItemZone( ItemPrefix, DetectedItemKey, Set, Zone )

    self:F( { ItemPrefix, DetectedItemKey, Set, Zone } )

    local DetectedItem = self:AddDetectedItem( ItemPrefix, DetectedItemKey, Set )

    DetectedItem.Zone = Zone

    return DetectedItem
  end

  --- Removes an existing DetectedItem from the DetectedItems list.
  -- The DetectedItem is a table and contains a SET_UNIT in the field Set.
  -- @param #DETECTION_BASE self
  -- @param DetectedItemKey The key in the DetectedItems list where the item needs to be removed.
  function DETECTION_BASE:RemoveDetectedItem( DetectedItemKey )

    local DetectedItem = self.DetectedItems[DetectedItemKey]

    if DetectedItem then
      self.DetectedItemCount = self.DetectedItemCount - 1
      local DetectedItemIndex = DetectedItem.Index
      self.DetectedItemsByIndex[DetectedItemIndex] = nil
      self.DetectedItems[DetectedItemKey] = nil
    end
  end

  --- Get the DetectedItems by Key.
  -- This will return the DetectedItems collection, indexed by the Key, which can be any object that acts as the key of the detection.
  -- @param #DETECTION_BASE self
  -- @return #DETECTION_BASE.DetectedItems
  function DETECTION_BASE:GetDetectedItems()

    return self.DetectedItems
  end

  --- Get the DetectedItems by Index.
  -- This will return the DetectedItems collection, indexed by an internal numerical Index.
  -- @param #DETECTION_BASE self
  -- @return #DETECTION_BASE.DetectedItems
  function DETECTION_BASE:GetDetectedItemsByIndex()

    return self.DetectedItemsByIndex
  end

  --- Get the amount of SETs with detected objects.
  -- @param #DETECTION_BASE self
  -- @return #number The amount of detected items. Note that the amount of detected items can differ with the reality, because detections are not real-time but done in intervals!
  function DETECTION_BASE:GetDetectedItemsCount()

    local DetectedCount = self.DetectedItemCount
    return DetectedCount
  end

  --- Get a detected item using a given Key.
  -- @param #DETECTION_BASE self
  -- @param Key
  -- @return #DETECTION_BASE.DetectedItem
  function DETECTION_BASE:GetDetectedItemByKey( Key )

    self:F( { DetectedItems = self.DetectedItems } )

    local DetectedItem = self.DetectedItems[Key]
    if DetectedItem then
      return DetectedItem
    end

    return nil
  end

  --- Get a detected item using a given numeric index.
  -- @param #DETECTION_BASE self
  -- @param #number Index
  -- @return #DETECTION_BASE.DetectedItem
  function DETECTION_BASE:GetDetectedItemByIndex( Index )

    self:F( { self.DetectedItemsByIndex } )

    local DetectedItem = self.DetectedItemsByIndex[Index]
    if DetectedItem then
      return DetectedItem
    end

    return nil
  end

  --- Get a detected ItemID using a given numeric index.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem.
  -- @return #string DetectedItemID
  function DETECTION_BASE:GetDetectedItemID( DetectedItem ) -- R2.1

    return DetectedItem and DetectedItem.ItemID or ""
  end

  --- Get a detected ID using a given numeric index.
  -- @param #DETECTION_BASE self
  -- @param #number Index
  -- @return #string DetectedItemID
  function DETECTION_BASE:GetDetectedID( Index ) -- R2.1

    local DetectedItem = self.DetectedItemsByIndex[Index]
    if DetectedItem then
      return DetectedItem.ID
    end

    return ""
  end

  --- Get the @{Core.Set#SET_UNIT} of a detection area using a given numeric index.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem
  -- @return Core.Set#SET_UNIT DetectedSet
  function DETECTION_BASE:GetDetectedItemSet( DetectedItem )

    local DetectedSetUnit = DetectedItem and DetectedItem.Set
    if DetectedSetUnit then
      return DetectedSetUnit
    end

    return nil
  end

  --- Set IsDetected flag for the DetectedItem, which can have more units.
  -- @param #DETECTION_BASE self
  -- @return #DETECTION_BASE.DetectedItem DetectedItem
  -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet.
  function DETECTION_BASE:UpdateDetectedItemDetection( DetectedItem )

    local IsDetected = false

    for UnitName, UnitData in pairs( DetectedItem.Set:GetSet() ) do
      local DetectedObject = self.DetectedObjects[UnitName]
      self:F( { UnitName = UnitName, IsDetected = DetectedObject.IsDetected } )
      if DetectedObject.IsDetected then
        IsDetected = true
        break
      end
    end

    self:F( { IsDetected = DetectedItem.IsDetected } )

    DetectedItem.IsDetected = IsDetected

    return IsDetected
  end

  --- Checks if there is at least one UNIT detected in the Set of the the DetectedItem.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem
  -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet.
  function DETECTION_BASE:IsDetectedItemDetected( DetectedItem )

    return DetectedItem.IsDetected
  end

  do -- Zones

    --- Get the @{Core.Zone#ZONE_UNIT} of a detection area using a given numeric index.
    -- @param #DETECTION_BASE self
    -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem.
    -- @return Core.Zone#ZONE_UNIT DetectedZone
    function DETECTION_BASE:GetDetectedItemZone( DetectedItem )

      local DetectedZone = DetectedItem and DetectedItem.Zone
      if DetectedZone then
        return DetectedZone
      end

      local Detected

      return nil
    end

  end

  --- Lock the detected items when created and lock all existing detected items.
  -- @param #DETECTION_BASE self
  -- @return #DETECTION_BASE
  function DETECTION_BASE:LockDetectedItems()

    for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do
      self:LockDetectedItem( DetectedItem )
    end
    self.Locking = true

    return self
  end

  --- Unlock the detected items when created and unlock all existing detected items.
  -- @param #DETECTION_BASE self
  -- @return #DETECTION_BASE
  function DETECTION_BASE:UnlockDetectedItems()

    for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do
      self:UnlockDetectedItem( DetectedItem )
    end
    self.Locking = nil

    return self
  end

  --- Validate if the detected item is locked.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem.
  -- @return #boolean
  function DETECTION_BASE:IsDetectedItemLocked( DetectedItem )

    return self.Locking and DetectedItem.Locked == true

  end

  --- Lock a detected item.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem.
  -- @return #DETECTION_BASE
  function DETECTION_BASE:LockDetectedItem( DetectedItem )

    DetectedItem.Locked = true

    return self
  end

  --- Unlock a detected item.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem.
  -- @return #DETECTION_BASE
  function DETECTION_BASE:UnlockDetectedItem( DetectedItem )

    DetectedItem.Locked = nil

    return self
  end

  --- Set the detected item coordinate.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem to set the coordinate at.
  -- @param Core.Point#COORDINATE Coordinate The coordinate to set the last know detected position at.
  -- @param Wrapper.Unit#UNIT DetectedItemUnit The unit to set the heading and altitude from.
  -- @return #DETECTION_BASE
  function DETECTION_BASE:SetDetectedItemCoordinate( DetectedItem, Coordinate, DetectedItemUnit )
    self:F( { Coordinate = Coordinate } )

    if DetectedItem then
      if DetectedItemUnit then
        DetectedItem.Coordinate = Coordinate
        DetectedItem.Coordinate:SetHeading( DetectedItemUnit:GetHeading() )
        DetectedItem.Coordinate.y = DetectedItemUnit:GetAltitude()
        DetectedItem.Coordinate:SetVelocity( DetectedItemUnit:GetVelocityMPS() )
      end
    end
  end

  --- Get the detected item coordinate.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem to set the coordinate at.
  -- @return Core.Point#COORDINATE
  function DETECTION_BASE:GetDetectedItemCoordinate( DetectedItem )
    self:F( { DetectedItem = DetectedItem } )

    if DetectedItem then
      return DetectedItem.Coordinate
    end

    return nil
  end

  --- Get a list of the detected item coordinates.
  -- @param #DETECTION_BASE self
  -- @return #table A table of Core.Point#COORDINATE
  function DETECTION_BASE:GetDetectedItemCoordinates()

    local Coordinates = {}

    for DetectedItemID, DetectedItem in pairs( self:GetDetectedItems() ) do
      Coordinates[DetectedItem] = self:GetDetectedItemCoordinate( DetectedItem )
    end

    return Coordinates
  end

  --- Set the detected item threat level.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem The DetectedItem to calculate the threat level for.
  -- @return #DETECTION_BASE
  function DETECTION_BASE:SetDetectedItemThreatLevel( DetectedItem )

    local DetectedSet = DetectedItem.Set

    if DetectedItem then
      DetectedItem.ThreatLevel, DetectedItem.ThreatText = DetectedSet:CalculateThreatLevelA2G()
    end
  end

  --- Get the detected item coordinate.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem.
  -- @return #number ThreatLevel
  function DETECTION_BASE:GetDetectedItemThreatLevel( DetectedItem )
    self:F( { DetectedItem = DetectedItem } )

    if DetectedItem then
      self:F( { ThreatLevel = DetectedItem.ThreatLevel, ThreatText = DetectedItem.ThreatText } )
      return DetectedItem.ThreatLevel or 0, DetectedItem.ThreatText or ""
    end

    return nil, ""
  end

  --- Report summary of a detected item using a given numeric index.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem.
  -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for.
  -- @param Core.Settings#SETTINGS Settings Message formatting settings to use.
  -- @return Core.Report#REPORT
  function DETECTION_BASE:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings )
    self:F()
    return nil
  end

  --- Report detailed of a detection result.
  -- @param #DETECTION_BASE self
  -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for.
  -- @return #string
  function DETECTION_BASE:DetectedReportDetailed( AttackGroup )
    self:F()
    return nil
  end

  --- Get the Detection Set.
  -- @param #DETECTION_BASE self
  -- @return #DETECTION_BASE self
  function DETECTION_BASE:GetDetectionSet()

    local DetectionSet = self.DetectionSet
    return DetectionSet
  end

  --- Find the nearest Recce of the DetectedItem.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem
  -- @return Wrapper.Unit#UNIT The nearest FAC unit
  function DETECTION_BASE:NearestRecce( DetectedItem )

    local NearestRecce = nil
    local DistanceRecce = 1000000000 -- Units are not further than 1000000 km away from an area :-)

    for RecceGroupName, RecceGroup in pairs( self.DetectionSet:GetSet() ) do
      if RecceGroup and RecceGroup:IsAlive() then
        for RecceUnit, RecceUnit in pairs( RecceGroup:GetUnits() ) do
          if RecceUnit:IsActive() then
            local RecceUnitCoord = RecceUnit:GetCoordinate()
            local Distance = RecceUnitCoord:Get2DDistance( self:GetDetectedItemCoordinate( DetectedItem ) )
            if Distance < DistanceRecce then
              DistanceRecce = Distance
              NearestRecce = RecceUnit
            end
          end
        end
      end
    end

    DetectedItem.NearestFAC = NearestRecce
    DetectedItem.DistanceRecce = DistanceRecce

  end

  --- Schedule the DETECTION construction.
  -- @param #DETECTION_BASE self
  -- @param #number DelayTime The delay in seconds to wait the reporting.
  -- @param #number RepeatInterval The repeat interval in seconds for the reporting to happen repeatedly.
  -- @return #DETECTION_BASE self
  function DETECTION_BASE:Schedule( DelayTime, RepeatInterval )
    self:F2()

    self.ScheduleDelayTime = DelayTime
    self.ScheduleRepeatInterval = RepeatInterval

    self.DetectionScheduler = SCHEDULER:New( self, self._DetectionScheduler, { self, "Detection" }, DelayTime, RepeatInterval )
    return self
  end

end

do -- DETECTION_UNITS
  
  ---
  -- @type DETECTION_UNITS
  -- @field DCS#Distance DetectionRange The range till which targets are detected.
  -- @extends Functional.Detection#DETECTION_BASE

  --- Will detect units within the battle zone.
  --
  -- It will build a DetectedItems list filled with DetectedItems. Each DetectedItem will contain a field Set, which contains a @{Core.Set#SET_UNIT} containing ONE @{Wrapper.Unit#UNIT} object reference.
  -- Beware that when the amount of units detected is large, the DetectedItems list will be large also.
  --
  -- @field #DETECTION_UNITS
  DETECTION_UNITS = {
    ClassName = "DETECTION_UNITS",
    DetectionRange = nil,
  }

  --- DETECTION_UNITS constructor.
  -- @param Functional.Detection#DETECTION_UNITS self
  -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Core.Set} of GROUPs in the Forward Air Controller role.
  -- @return Functional.Detection#DETECTION_UNITS self
  function DETECTION_UNITS:New( DetectionSetGroup )

    -- Inherits from DETECTION_BASE
    local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) -- #DETECTION_UNITS

    self._SmokeDetectedUnits = false
    self._FlareDetectedUnits = false
    self._SmokeDetectedZones = false
    self._FlareDetectedZones = false
    self._BoundDetectedZones = false

    return self
  end

  --- Make text documenting the changes of the detected zone.
  -- @param #DETECTION_UNITS self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem
  -- @return #string The Changes text
  function DETECTION_UNITS:GetChangeText( DetectedItem )
    self:F( DetectedItem )

    local MT = {}

    for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do

      if ChangeCode == "AU" then
        local MTUT = {}
        for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do
          if ChangeUnitType ~= "ID" then
            MTUT[#MTUT + 1] = ChangeUnitCount .. " of " .. ChangeUnitType
          end
        end
        MT[#MT + 1] = "   New target(s) detected: " .. table.concat( MTUT, ", " ) .. "."
      end

      if ChangeCode == "RU" then
        local MTUT = {}
        for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do
          if ChangeUnitType ~= "ID" then
            MTUT[#MTUT + 1] = ChangeUnitCount .. " of " .. ChangeUnitType
          end
        end
        MT[#MT + 1] = "   Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "."
      end

    end

    return table.concat( MT, "\n" )

  end

  --- Create the DetectedItems list from the DetectedObjects table.
  -- For each DetectedItem, a one field array is created containing the Unit detected.
  -- @param #DETECTION_UNITS self
  -- @return #DETECTION_UNITS self
  function DETECTION_UNITS:CreateDetectionItems()
    -- Loop the current detected items, and check if each object still exists and is detected.

    for DetectedItemKey, _DetectedItem in pairs( self.DetectedItems ) do
      local DetectedItem = _DetectedItem -- #DETECTION_BASE.DetectedItem

      local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT

      for DetectedUnitName, DetectedUnitData in pairs( DetectedItemSet:GetSet() ) do
        local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT

        local DetectedObject = nil
        -- self:F( DetectedUnit )
        if DetectedUnit:IsAlive() then
          -- self:F(DetectedUnit:GetName())
          DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() )
        end
        if DetectedObject then

          -- Yes, the DetectedUnit is still detected or exists. Flag as identified.
          self:IdentifyDetectedObject( DetectedObject )

          self:F( { "**DETECTED**", IsVisible = DetectedObject.IsVisible } )
          -- Update the detection with the new data provided.
          DetectedItem.TypeName = DetectedUnit:GetTypeName()
          DetectedItem.CategoryName = DetectedUnit:GetCategoryName()
          DetectedItem.Name = DetectedObject.Name
          DetectedItem.IsVisible = DetectedObject.IsVisible
          DetectedItem.LastTime = DetectedObject.LastTime
          DetectedItem.LastPos = DetectedObject.LastPos
          DetectedItem.LastVelocity = DetectedObject.LastVelocity
          DetectedItem.KnowType = DetectedObject.KnowType
          DetectedItem.KnowDistance = DetectedObject.KnowDistance
          DetectedItem.Distance = DetectedObject.Distance
        else
          -- There was no DetectedObject, remove DetectedUnit from the Set.
          self:AddChangeUnit( DetectedItem, "RU", DetectedUnitName )
          DetectedItemSet:Remove( DetectedUnitName )
        end
      end
      if DetectedItemSet:Count() == 0 then
        -- Now the Set is empty, meaning that a detected item has no units anymore.
        -- Delete the DetectedItem from the detections
        self:RemoveDetectedItem( DetectedItemKey )
      end
    end

    -- Now we need to loop through the unidentified detected units and add these... These are all new items.
    for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do

      local DetectedObject = self:GetDetectedObject( DetectedUnitName )
      if DetectedObject then
        self:T( { "Detected Unit #", DetectedUnitName } )

        local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT

        if DetectedUnit then
          local DetectedTypeName = DetectedUnit:GetTypeName()
          local DetectedItem = self:GetDetectedItemByKey( DetectedUnitName )
          if not DetectedItem then
            self:T( "Added new DetectedItem" )
            DetectedItem = self:AddDetectedItem( "UNIT", DetectedUnitName )
            DetectedItem.TypeName = DetectedUnit:GetTypeName()
            DetectedItem.Name = DetectedObject.Name
            DetectedItem.IsVisible = DetectedObject.IsVisible
            DetectedItem.LastTime = DetectedObject.LastTime
            DetectedItem.LastPos = DetectedObject.LastPos
            DetectedItem.LastVelocity = DetectedObject.LastVelocity
            DetectedItem.KnowType = DetectedObject.KnowType
            DetectedItem.KnowDistance = DetectedObject.KnowDistance
            DetectedItem.Distance = DetectedObject.Distance
          end

          DetectedItem.Set:AddUnit( DetectedUnit )
          self:AddChangeUnit( DetectedItem, "AU", DetectedTypeName )
        end
      end
    end

    for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do

      local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem
      local DetectedSet = DetectedItem.Set

      -- Set the last known coordinate.
      local DetectedFirstUnit = DetectedSet:GetFirst()
      local DetectedFirstUnitCoord = DetectedFirstUnit:GetCoordinate()
      self:SetDetectedItemCoordinate( DetectedItem, DetectedFirstUnitCoord, DetectedFirstUnit )

      self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table
      self:SetDetectedItemThreatLevel( DetectedItem )
      self:NearestRecce( DetectedItem )

    end

  end

  --- Report summary of a DetectedItem using a given numeric index.
  -- @param #DETECTION_UNITS self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem.
  -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for.
  -- @param Core.Settings#SETTINGS Settings Message formatting settings to use.
  -- @param #boolean ForceA2GCoordinate Set creation of A2G coordinate
  -- @return Core.Report#REPORT The report of the detection items.
  function DETECTION_UNITS:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings, ForceA2GCoordinate )
    self:F( { DetectedItem = DetectedItem } )

    local DetectedItemID = self:GetDetectedItemID( DetectedItem )

    if DetectedItem then
      local ReportSummary = ""
      local UnitDistanceText = ""
      local UnitCategoryText = ""

      if DetectedItem.KnowType then
        local UnitCategoryName = DetectedItem.CategoryName
        if UnitCategoryName then
          UnitCategoryText = UnitCategoryName
        end
        if DetectedItem.TypeName then
          UnitCategoryText = UnitCategoryText .. " (" .. DetectedItem.TypeName .. ")"
        end
      else
        UnitCategoryText = "Unknown"
      end

      if DetectedItem.KnowDistance then
        if DetectedItem.IsVisible then
          UnitDistanceText = " at " .. string.format( "%.2f", DetectedItem.Distance ) .. " km"
        end
      else
        if DetectedItem.IsVisible then
          UnitDistanceText = " at +/- " .. string.format( "%.0f", DetectedItem.Distance ) .. " km"
        end
      end

      -- TODO: solve Index reference
      local DetectedItemCoordinate = self:GetDetectedItemCoordinate( DetectedItem )
      local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings )
      
      if ForceA2GCoordinate then
        DetectedItemCoordText = DetectedItemCoordinate:ToStringA2G(AttackGroup,Settings)
      end
      
      local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem )

      local Report = REPORT:New()
      Report:Add( DetectedItemID .. ", " .. DetectedItemCoordText )
      Report:Add( string.format( "Threat: [%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10 - ThreatLevelA2G ) ) )
      Report:Add( string.format( "Type: %s%s", UnitCategoryText, UnitDistanceText ) )
      Report:Add( string.format( "Visible: %s", DetectedItem.IsVisible and "yes" or "no" ) )
      Report:Add( string.format( "Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) )
      Report:Add( string.format( "Distance: %s", DetectedItem.KnowDistance and "yes" or "no" ) )
      return Report
    end
    return nil
  end

  --- Report detailed of a detection result.
  -- @param #DETECTION_UNITS self
  -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for.
  -- @return #string
  function DETECTION_UNITS:DetectedReportDetailed( AttackGroup )
    self:F()

    local Report = REPORT:New()
    for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do
      local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem
      local ReportSummary = self:DetectedItemReportSummary( DetectedItem, AttackGroup )
      Report:SetTitle( "Detected units:" )
      Report:Add( ReportSummary:Text() )
    end

    local ReportText = Report:Text()

    return ReportText
  end

end

do -- DETECTION_TYPES
  
  ---
  -- @type DETECTION_TYPES
  -- @extends Functional.Detection#DETECTION_BASE

  --- Will detect units within the battle zone.
  -- It will build a DetectedItems[] list filled with DetectedItems, grouped by the type of units detected.
  -- Each DetectedItem will contain a field Set, which contains a @{Core.Set#SET_UNIT} containing ONE @{Wrapper.Unit#UNIT} object reference.
  -- Beware that when the amount of different types detected is large, the DetectedItems[] list will be large also.
  --
  -- @field #DETECTION_TYPES
  DETECTION_TYPES = {
    ClassName = "DETECTION_TYPES",
    DetectionRange = nil,
  }

  --- DETECTION_TYPES constructor.
  -- @param Functional.Detection#DETECTION_TYPES self
  -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Core.Set} of GROUPs in the Recce role.
  -- @return Functional.Detection#DETECTION_TYPES self
  function DETECTION_TYPES:New( DetectionSetGroup )

    -- Inherits from DETECTION_BASE
    local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) ) -- #DETECTION_TYPES

    self._SmokeDetectedUnits = false
    self._FlareDetectedUnits = false
    self._SmokeDetectedZones = false
    self._FlareDetectedZones = false
    self._BoundDetectedZones = false

    return self
  end

  --- Make text documenting the changes of the detected zone.
  -- @param #DETECTION_TYPES self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem
  -- @return #string The Changes text
  function DETECTION_TYPES:GetChangeText( DetectedItem )
    self:F( DetectedItem )

    local MT = {}

    for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do

      if ChangeCode == "AU" then
        local MTUT = {}
        for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do
          if ChangeUnitType ~= "ID" then
            MTUT[#MTUT + 1] = ChangeUnitCount .. " of " .. ChangeUnitType
          end
        end
        MT[#MT + 1] = "   New target(s) detected: " .. table.concat( MTUT, ", " ) .. "."
      end

      if ChangeCode == "RU" then
        local MTUT = {}
        for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do
          if ChangeUnitType ~= "ID" then
            MTUT[#MTUT + 1] = ChangeUnitCount .. " of " .. ChangeUnitType
          end
        end
        MT[#MT + 1] = "   Invisible or destroyed target(s): " .. table.concat( MTUT, ", " ) .. "."
      end

    end

    return table.concat( MT, "\n" )

  end

  --- Create the DetectedItems list from the DetectedObjects table.
  -- For each DetectedItem, a one field array is created containing the Unit detected.
  -- @param #DETECTION_TYPES self
  -- @return #DETECTION_TYPES self
  function DETECTION_TYPES:CreateDetectionItems()

    -- Loop the current detected items, and check if each object still exists and is detected.

    for DetectedItemKey, DetectedItem in pairs( self.DetectedItems ) do

      local DetectedItemSet = DetectedItem.Set -- Core.Set#SET_UNIT
      local DetectedTypeName = DetectedItem.TypeName

      for DetectedUnitName, DetectedUnitData in pairs( DetectedItemSet:GetSet() ) do
        local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT

        local DetectedObject = nil
        if DetectedUnit:IsAlive() then
          -- self:F(DetectedUnit:GetName())
          DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() )
        end
        if DetectedObject then

          -- Yes, the DetectedUnit is still detected or exists. Flag as identified.
          self:IdentifyDetectedObject( DetectedObject )
        else
          -- There was no DetectedObject, remove DetectedUnit from the Set.
          self:AddChangeUnit( DetectedItem, "RU", DetectedUnitName )
          DetectedItemSet:Remove( DetectedUnitName )
        end
      end
      if DetectedItemSet:Count() == 0 then
        -- Now the Set is empty, meaning that a detected item has no units anymore.
        -- Delete the DetectedItem from the detections
        self:RemoveDetectedItem( DetectedItemKey )
      end
    end

    -- Now we need to loop through the unidentified detected units and add these... These are all new items.
    for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do

      local DetectedObject = self:GetDetectedObject( DetectedUnitName )
      if DetectedObject then
        self:T( { "Detected Unit #", DetectedUnitName } )

        local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT

        if DetectedUnit then
          local DetectedTypeName = DetectedUnit:GetTypeName()
          local DetectedItem = self:GetDetectedItemByKey( DetectedTypeName )
          if not DetectedItem then
            DetectedItem = self:AddDetectedItem( "TYPE", DetectedTypeName )
            DetectedItem.TypeName = DetectedTypeName
            DetectedItem.Name = DetectedUnitName -- fix by @Nocke
          end

          DetectedItem.Set:AddUnit( DetectedUnit )
          self:AddChangeUnit( DetectedItem, "AU", DetectedTypeName )
        end
      end
    end

    -- Check if there are any friendlies nearby.    
    for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do

      local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem
      local DetectedSet = DetectedItem.Set

      -- Set the last known coordinate.
      local DetectedFirstUnit = DetectedSet:GetFirst()
      local DetectedUnitCoord = DetectedFirstUnit:GetCoordinate()
      self:SetDetectedItemCoordinate( DetectedItem, DetectedUnitCoord, DetectedFirstUnit )

      self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table
      self:SetDetectedItemThreatLevel( DetectedItem )
      self:NearestRecce( DetectedItem )
    end

  end

  --- Report summary of a DetectedItem using a given numeric index.
  -- @param #DETECTION_TYPES self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem.
  -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for.
  -- @param Core.Settings#SETTINGS Settings Message formatting settings to use.
  -- @return Core.Report#REPORT The report of the detection items.
  function DETECTION_TYPES:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings )
    self:F( { DetectedItem = DetectedItem } )

    local DetectedSet = self:GetDetectedItemSet( DetectedItem )
    local DetectedItemID = self:GetDetectedItemID( DetectedItem )

    self:T( DetectedItem )
    if DetectedItem then

      local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem )
      local DetectedItemsCount = DetectedSet:Count()
      local DetectedItemType = DetectedItem.TypeName

      local DetectedItemCoordinate = self:GetDetectedItemCoordinate( DetectedItem )
      local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings )

      local Report = REPORT:New()
      Report:Add( DetectedItemID .. ", " .. DetectedItemCoordText )
      Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10 - ThreatLevelA2G ) ) )
      Report:Add( string.format( "Type: %2d of %s", DetectedItemsCount, DetectedItemType ) )
      return Report
    end
  end

  --- Report detailed of a detection result.
  -- @param #DETECTION_TYPES self
  -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for.
  -- @return #string
  function DETECTION_TYPES:DetectedReportDetailed( AttackGroup )
    self:F()

    local Report = REPORT:New()
    for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do
      local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem
      local ReportSummary = self:DetectedItemReportSummary( DetectedItem, AttackGroup )
      Report:SetTitle( "Detected types:" )
      Report:Add( ReportSummary:Text() )
    end

    local ReportText = Report:Text()

    return ReportText
  end

end

do -- DETECTION_AREAS
  
  ---
  -- @type DETECTION_AREAS
  -- @field DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target.
  -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Wrapper.Unit}s, @{Core.Zone}s, the center @{Wrapper.Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange.
  -- @extends Functional.Detection#DETECTION_BASE

  --- Detect units within the battle zone for a list of @{Wrapper.Group}s detecting targets following (a) detection method(s),
  -- and will build a list (table) of @{Core.Set#SET_UNIT}s containing the @{Wrapper.Unit#UNIT}s detected.
  -- The class is group the detected units within zones given a DetectedZoneRange parameter.
  -- A set with multiple detected zones will be created as there are groups of units detected.
  --
  -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones
  -- 
  -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Functional.Detection#DECTECTION_BASE} and 
  -- the methods to manage the DetectedItems[].Zone(s) are implemented in @{Functional.Detection#DETECTION_AREAS}.
  -- 
  -- Retrieve the DetectedItems[].Set with the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}(). A @{Core.Set#SET_UNIT} object will be returned.
  -- 
  -- Retrieve the formed @{Core.Zone@ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Functional.Detection#DETECTION_AREAS.GetDetectionZones}().
  -- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_AREAS.GetDetectionZoneCount}(). 
  -- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_AREAS.GetDetectionZoneByID}() with a given index.
  -- 
  -- ## 4.4) Flare or Smoke detected units
  --
  -- Use the methods @{Functional.Detection#DETECTION_AREAS.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_AREAS.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place.
  --
  -- ## 4.5) Flare or Smoke or Bound detected zones
  --
  -- Use the methods:
  --
  --   * @{Functional.Detection#DETECTION_AREAS.FlareDetectedZones}() to flare in a color
  --   * @{Functional.Detection#DETECTION_AREAS.SmokeDetectedZones}() to smoke in a color
  --   * @{Functional.Detection#DETECTION_AREAS.SmokeDetectedZones}() to bound with a tire with a white flag
  --
  -- the detected zones when a new detection has taken place.
  --
  -- @field #DETECTION_AREAS
  DETECTION_AREAS = {
    ClassName = "DETECTION_AREAS",
    DetectionZoneRange = nil,
  }

  --- DETECTION_AREAS constructor.
  -- @param #DETECTION_AREAS self
  -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Core.Set} of GROUPs in the Forward Air Controller role.
  -- @param #number DetectionZoneRange The range in meters within which targets are grouped upon the first detected target. Default 5000m.
  -- @return #DETECTION_AREAS
  function DETECTION_AREAS:New( DetectionSetGroup, DetectionZoneRange )

    -- Inherits from DETECTION_BASE
    local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetGroup ) )

    self.DetectionZoneRange = DetectionZoneRange or 5000

    self._SmokeDetectedUnits = false
    self._FlareDetectedUnits = false
    self._SmokeDetectedZones = false
    self._FlareDetectedZones = false
    self._BoundDetectedZones = false

    return self
  end

  --- Retrieve set of detected zones.
  -- @param #DETECTION_AREAS self
  -- @return Core.Set#SET_ZONE The @{Core.Set} of ZONE_UNIT objects detected.
  function DETECTION_AREAS:GetDetectionZones()
    local zoneset = SET_ZONE:New()
    for _ID,_Item in pairs (self.DetectedItems) do
      local item = _Item -- #DETECTION_BASE.DetectedItem
      if item.Zone then
        zoneset:AddZone(item.Zone)
      end
    end
    return zoneset
  end
  
  --- Retrieve a specific zone by its ID (number)
  -- @param #DETECTION_AREAS self
  -- @param #number ID
  -- @return Core.Zone#ZONE_UNIT The zone or nil if it does not exist
  function DETECTION_AREAS:GetDetectionZoneByID(ID)
    local zone = nil
    for _ID,_Item in pairs (self.DetectedItems) do
      local item = _Item -- #DETECTION_BASE.DetectedItem
      if item.ID == ID then
        zone = item.Zone
        break
      end
    end
    return zone
  end
  
  --- Retrieve number of detected zones.
  -- @param #DETECTION_AREAS self
  -- @return #number The number of zones.
  function DETECTION_AREAS:GetDetectionZoneCount()
    local zoneset = 0
    for _ID,_Item in pairs (self.DetectedItems) do
      if _Item.Zone then
        zoneset = zoneset + 1
      end
    end
    return zoneset
  end
  
  --- Report summary of a detected item using a given numeric index.
  -- @param #DETECTION_AREAS self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem.
  -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for.
  -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use.
  -- @return Core.Report#REPORT The report of the detection items.
  function DETECTION_AREAS:DetectedItemReportMenu( DetectedItem, AttackGroup, Settings )
    self:F( { DetectedItem = DetectedItem } )

    local DetectedItemID = self:GetDetectedItemID( DetectedItem )

    if DetectedItem then
      local DetectedSet = self:GetDetectedItemSet( DetectedItem )
      local ReportSummaryItem

      local DetectedZone = self:GetDetectedItemZone( DetectedItem )
      local DetectedItemCoordinate = DetectedZone:GetCoordinate()
      local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings )

      local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem )

      local Report = REPORT:New()
      Report:Add( DetectedItemID )
      Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10 - ThreatLevelA2G ) ) )

      return Report
    end

    return nil
  end

  --- Report summary of a detected item using a given numeric index.
  -- @param #DETECTION_AREAS self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem.
  -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for.
  -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use.
  -- @return Core.Report#REPORT The report of the detection items.
  function DETECTION_AREAS:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings )
    self:F( { DetectedItem = DetectedItem } )

    local DetectedItemID = self:GetDetectedItemID( DetectedItem )

    if DetectedItem then
      local DetectedSet = self:GetDetectedItemSet( DetectedItem )
      local ReportSummaryItem

      -- local DetectedZone = self:GetDetectedItemZone( DetectedItem )
      local DetectedItemCoordinate = self:GetDetectedItemCoordinate( DetectedItem )
      local DetectedAir = DetectedSet:HasAirUnits()
      local DetectedAltitude = self:GetDetectedItemCoordinate( DetectedItem )
      local DetectedItemCoordText = ""
      if DetectedAir > 0 then
        DetectedItemCoordText = DetectedItemCoordinate:ToStringA2A( AttackGroup, Settings )
      else
        DetectedItemCoordText = DetectedItemCoordinate:ToStringA2G( AttackGroup, Settings )
      end

      local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem )
      local DetectedItemsCount = DetectedSet:Count()
      local DetectedItemsTypes = DetectedSet:GetTypeNames()

      local Report = REPORT:New()
      Report:Add( DetectedItemID .. ", " .. DetectedItemCoordText )
      Report:Add( string.format( "Threat: [%s%s]", string.rep( "■", ThreatLevelA2G ), string.rep( "□", 10 - ThreatLevelA2G ) ) )
      Report:Add( string.format( "Type: %2d of %s", DetectedItemsCount, DetectedItemsTypes ) )
      -- Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) )

      return Report
    end

    return nil
  end

  --- Report detailed of a detection result.
  -- @param #DETECTION_AREAS self
  -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for.
  -- @return #string
  function DETECTION_AREAS:DetectedReportDetailed( AttackGroup ) -- R2.1  Fixed missing report
    self:F()

    local Report = REPORT:New()
    for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do
      local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem
      local ReportSummary = self:DetectedItemReportSummary( DetectedItem, AttackGroup )
      Report:SetTitle( "Detected areas:" )
      Report:Add( ReportSummary:Text() )
    end

    local ReportText = Report:Text()

    return ReportText
  end

  --- Calculate the optimal intercept point of the DetectedItem.
  -- @param #DETECTION_AREAS self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem
  function DETECTION_AREAS:CalculateIntercept( DetectedItem )

    local DetectedCoord = DetectedItem.Coordinate
    local DetectedSpeed = DetectedCoord:GetVelocity()
    local DetectedHeading = DetectedCoord:GetHeading()

    if self.Intercept then
      local DetectedSet = DetectedItem.Set
      -- todo: speed

      local TranslateDistance = DetectedSpeed * self.InterceptDelay

      local InterceptCoord = DetectedCoord:Translate( TranslateDistance, DetectedHeading )

      DetectedItem.InterceptCoord = InterceptCoord
    else
      DetectedItem.InterceptCoord = DetectedCoord
    end

  end

  --- Smoke the detected units
  -- @param #DETECTION_AREAS self
  -- @return #DETECTION_AREAS self
  function DETECTION_AREAS:SmokeDetectedUnits()
    self:F2()

    self._SmokeDetectedUnits = true
    return self
  end

  --- Flare the detected units
  -- @param #DETECTION_AREAS self
  -- @return #DETECTION_AREAS self
  function DETECTION_AREAS:FlareDetectedUnits()
    self:F2()

    self._FlareDetectedUnits = true
    return self
  end

  --- Smoke the detected zones
  -- @param #DETECTION_AREAS self
  -- @return #DETECTION_AREAS self
  function DETECTION_AREAS:SmokeDetectedZones()
    self:F2()

    self._SmokeDetectedZones = true
    return self
  end

  --- Flare the detected zones
  -- @param #DETECTION_AREAS self
  -- @return #DETECTION_AREAS self
  function DETECTION_AREAS:FlareDetectedZones()
    self:F2()

    self._FlareDetectedZones = true
    return self
  end

  --- Bound the detected zones
  -- @param #DETECTION_AREAS self
  -- @return #DETECTION_AREAS self
  function DETECTION_AREAS:BoundDetectedZones()
    self:F2()

    self._BoundDetectedZones = true
    return self
  end

  --- Make text documenting the changes of the detected zone.
  -- @param #DETECTION_AREAS self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem
  -- @return #string The Changes text
  function DETECTION_AREAS:GetChangeText( DetectedItem )
    self:F( DetectedItem )

    local MT = {}

    for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do

      if ChangeCode == "AA" then
        MT[#MT + 1] = "Detected new area " .. ChangeData.ID .. ". The center target is a " .. ChangeData.ItemUnitType .. "."
      end

      if ChangeCode == "RAU" then
        MT[#MT + 1] = "Changed area " .. ChangeData.ID .. ". Removed the center target."
      end

      if ChangeCode == "AAU" then
        MT[#MT + 1] = "Changed area " .. ChangeData.ID .. ". The new center target is a " .. ChangeData.ItemUnitType .. "."
      end

      if ChangeCode == "RA" then
        MT[#MT + 1] = "Removed old area " .. ChangeData.ID .. ". No more targets in this area."
      end

      if ChangeCode == "AU" then
        local MTUT = {}
        for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do
          if ChangeUnitType ~= "ID" then
            MTUT[#MTUT + 1] = ChangeUnitCount .. " of " .. ChangeUnitType
          end
        end
        MT[#MT + 1] = "Detected for area " .. ChangeData.ID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "."
      end

      if ChangeCode == "RU" then
        local MTUT = {}
        for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do
          if ChangeUnitType ~= "ID" then
            MTUT[#MTUT + 1] = ChangeUnitCount .. " of " .. ChangeUnitType
          end
        end
        MT[#MT + 1] = "Removed for area " .. ChangeData.ID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "."
      end

    end

    return table.concat( MT, "\n" )

  end

  --- Make a DetectionSet table. This function will be overridden in the derived classes.
  -- @param #DETECTION_AREAS self
  -- @return #DETECTION_AREAS self
  function DETECTION_AREAS:CreateDetectionItems()

    self:F( "Checking Detected Items for new Detected Units ..." )
    -- self:F( { DetectedObjects = self.DetectedObjects } )

    -- First go through all detected sets, and check if there are new detected units, match all existing detected units and identify undetected units.
    -- Regroup when needed, split groups when needed.
    for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do

      local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem

      if DetectedItem then

        self:T2( { "Detected Item ID: ", DetectedItemID } )

        local DetectedSet = DetectedItem.Set

        local AreaExists = false -- This flag will determine of the detected area is still existing.

        -- First test if the center unit is detected in the detection area.
        self:T3( { "Zone Center Unit:", DetectedItem.Zone.ZoneUNIT.UnitName } )
        local DetectedZoneObject = self:GetDetectedObject( DetectedItem.Zone.ZoneUNIT.UnitName )
        self:T3( { "Detected Zone Object:", DetectedItem.Zone:GetName(), DetectedZoneObject } )

        if DetectedZoneObject then

          -- self:IdentifyDetectedObject( DetectedZoneObject )
          AreaExists = true

        else
          -- The center object of the detected area has not been detected. Find an other unit of the set to become the center of the area.
          -- First remove the center unit from the set.
          DetectedSet:RemoveUnitsByName( DetectedItem.Zone.ZoneUNIT.UnitName )

          self:AddChangeItem( DetectedItem, 'RAU', self:GetDetectedUnitTypeName( DetectedItem.Zone.ZoneUNIT ) )

          -- Then search for a new center area unit within the set. Note that the new area unit candidate must be within the area range.
          for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do

            local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT
            local DetectedObject = self:GetDetectedObject( DetectedUnit.UnitName )
            local DetectedUnitTypeName = self:GetDetectedUnitTypeName( DetectedUnit )

            -- The DetectedObject can be nil when the DetectedUnit is not alive anymore or it is not in the DetectedObjects map.
            -- If the DetectedUnit was already identified, DetectedObject will be nil.
            if DetectedObject then
              self:IdentifyDetectedObject( DetectedObject )
              AreaExists = true

              -- DetectedItem.Zone:BoundZone( 12, self.CountryID, true)

              -- Assign the Unit as the new center unit of the detected area.
              DetectedItem.Zone = ZONE_UNIT:New( DetectedUnit:GetName(), DetectedUnit, self.DetectionZoneRange )

              self:AddChangeItem( DetectedItem, "AAU", DetectedUnitTypeName )

              -- We don't need to add the DetectedObject to the area set, because it is already there ...
              break
            else
              DetectedSet:Remove( DetectedUnitName )
              self:AddChangeUnit( DetectedItem, "RU", DetectedUnitTypeName )
            end
          end
        end

        -- Now we've determined the center unit of the area, now we can iterate the units in the detected area.
        -- Note that the position of the area may have moved due to the center unit repositioning.
        -- If no center unit was identified, then the detected area does not exist anymore and should be deleted, as there are no valid units that can be the center unit.
        if AreaExists then

          -- ok, we found the center unit of the area, now iterate through the detected area set and see which units are still within the center unit zone ...
          -- Those units within the zone are flagged as Identified.
          -- If a unit was not found in the set, remove it from the set. This may be added later to other existing or new sets.
          for DetectedUnitName, DetectedUnitData in pairs( DetectedSet:GetSet() ) do

            local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT
            local DetectedUnitTypeName = self:GetDetectedUnitTypeName( DetectedUnit )

            local DetectedObject = nil
            if DetectedUnit:IsAlive() then
              -- self:F(DetectedUnit:GetName())
              DetectedObject = self:GetDetectedObject( DetectedUnit:GetName() )
            end
            if DetectedObject then

              -- Check if the DetectedUnit is within the DetectedItem.Zone
              if DetectedUnit:IsInZone( DetectedItem.Zone ) then

                -- Yes, the DetectedUnit is within the DetectedItem.Zone, no changes, DetectedUnit can be kept within the Set.
                self:IdentifyDetectedObject( DetectedObject )
                DetectedSet:AddUnit( DetectedUnit )

              else
                -- No, the DetectedUnit is not within the DetectedItem.Zone, remove DetectedUnit from the Set.
                DetectedSet:Remove( DetectedUnitName )
                self:AddChangeUnit( DetectedItem, "RU", DetectedUnitTypeName )
              end

            else
              -- There was no DetectedObject, remove DetectedUnit from the Set.
              self:AddChangeUnit( DetectedItem, "RU", "destroyed target" )
              DetectedSet:Remove( DetectedUnitName )

              -- The DetectedObject has been identified, because it does not exist ...
              -- self:IdentifyDetectedObject( DetectedObject )
            end
          end
        else
          -- DetectedItem.Zone:BoundZone( 12, self.CountryID, true)
          self:RemoveDetectedItem( DetectedItemID )
          self:AddChangeItem( DetectedItem, "RA" )
        end
      end
    end

    -- We iterated through the existing detection areas and:
    --  - We checked which units are still detected in each detection area. Those units were flagged as Identified.
    --  - We re-centered the detection area to new center units where it was needed.
    --
    -- Now we need to loop through the unidentified detected units and see where they belong:
    --  - They can be added to a new detection area and become the new center unit.
    --  - They can be added to a new detection area.
    for DetectedUnitName, DetectedObjectData in pairs( self.DetectedObjects ) do

      local DetectedObject = self:GetDetectedObject( DetectedUnitName )

      if DetectedObject then

        -- We found an unidentified unit outside of any existing detection area.
        local DetectedUnit = UNIT:FindByName( DetectedUnitName ) -- Wrapper.Unit#UNIT
        local DetectedUnitTypeName = self:GetDetectedUnitTypeName( DetectedUnit )

        local AddedToDetectionArea = false

        for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do

          local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem
          if DetectedItem then
            local DetectedSet = DetectedItem.Set
            if not self:IsDetectedObjectIdentified( DetectedObject ) and DetectedUnit:IsInZone( DetectedItem.Zone ) then
              self:IdentifyDetectedObject( DetectedObject )
              DetectedSet:AddUnit( DetectedUnit )
              AddedToDetectionArea = true
              self:AddChangeUnit( DetectedItem, "AU", DetectedUnitTypeName )
            end
          end
        end

        if AddedToDetectionArea == false then

          -- New detection area
          local DetectedItem = self:AddDetectedItemZone( "AREA", nil,
            SET_UNIT:New():FilterDeads():FilterCrashes(),
            ZONE_UNIT:New( DetectedUnitName, DetectedUnit, self.DetectionZoneRange )
          )
          -- self:F( DetectedItem.Zone.ZoneUNIT.UnitName )
          DetectedItem.Set:AddUnit( DetectedUnit )
          self:AddChangeItem( DetectedItem, "AA", DetectedUnitTypeName )
        end
      end
    end

    -- Now all the tests should have been build, now make some smoke and flares...
    -- We also report here the friendlies within the detected areas.

    for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do

      local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem
      local DetectedSet = DetectedItem.Set
      local DetectedFirstUnit = DetectedSet:GetFirst()
      local DetectedZone = DetectedItem.Zone

      -- Set the last known coordinate to the detection item.
      local DetectedZoneCoord = DetectedZone:GetCoordinate()
      self:SetDetectedItemCoordinate( DetectedItem, DetectedZoneCoord, DetectedFirstUnit )

      self:CalculateIntercept( DetectedItem )

      -- We search for friendlies nearby.
      -- If there weren't any friendlies nearby, and now there are friendlies nearby, we flag the area as "changed".
      -- If there were friendlies nearby, and now there aren't any friendlies nearby, we flag the area as "changed".
      -- This is for the A2G dispatcher to detect if there is a change in the tactical situation.
      local OldFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT )
      self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSet } ) -- Fill the Friendlies table
      local NewFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT )
      if OldFriendliesNearbyGround ~= NewFriendliesNearbyGround then
        DetectedItem.Changed = true
      end

      self:SetDetectedItemThreatLevel( DetectedItem ) -- Calculate A2G threat level
      self:NearestRecce( DetectedItem )

      if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then
        DetectedZone.ZoneUNIT:SmokeRed()
      end

      -- DetectedSet:Flush( self )

      DetectedSet:ForEachUnit(  -- @param Wrapper.Unit#UNIT DetectedUnit
      function( DetectedUnit )
        if DetectedUnit:IsAlive() then
          -- self:T( "Detected Set #" .. DetectedItem.ID .. ":" .. DetectedUnit:GetName() )
          if DETECTION_AREAS._FlareDetectedUnits or self._FlareDetectedUnits then
            DetectedUnit:FlareGreen()
          end
          if DETECTION_AREAS._SmokeDetectedUnits or self._SmokeDetectedUnits then
            DetectedUnit:SmokeGreen()
          end
        end
      end )
      if DETECTION_AREAS._FlareDetectedZones or self._FlareDetectedZones then
        DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0, 90 ) )
      end
      if DETECTION_AREAS._SmokeDetectedZones or self._SmokeDetectedZones then
        DetectedZone:SmokeZone( SMOKECOLOR.White, 30 )
      end

      if DETECTION_AREAS._BoundDetectedZones or self._BoundDetectedZones then
        self.CountryID = DetectedSet:GetFirst():GetCountry()
        DetectedZone:BoundZone( 12, self.CountryID )
      end
    end

  end

end
--- **Functional** - Captures the class DETECTION_ZONES.
-- @module Functional.DetectionZones
-- @image MOOSE.JPG

do -- DETECTION_ZONES

  -- @type DETECTION_ZONES
  -- @field DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target.
  -- @field #DETECTION_BASE.DetectedItems DetectedItems A list of areas containing the set of @{Wrapper.Unit}s, @{Core.Zone}s, the center @{Wrapper.Unit} within the zone, and ID of each area that was detected within a DetectionZoneRange.
  -- @extends Functional.Detection#DETECTION_BASE

  --- (old, to be revised ) Detect units within the battle zone for a list of @{Core.Zone}s detecting targets following (a) detection method(s), 
  -- and will build a list (table) of @{Core.Set#SET_UNIT}s containing the @{Wrapper.Unit#UNIT}s detected.
  -- The class is group the detected units within zones given a DetectedZoneRange parameter.
  -- A set with multiple detected zones will be created as there are groups of units detected.
  -- 
  -- ## 4.1) Retrieve the Detected Unit Sets and Detected Zones
  -- 
  -- The methods to manage the DetectedItems[].Set(s) are implemented in @{Functional.Detection#DECTECTION_BASE} and 
  -- the methods to manage the DetectedItems[].Zone(s) is implemented in @{Functional.Detection#DETECTION_ZONES}.
  -- 
  -- Retrieve the DetectedItems[].Set with the method @{Functional.Detection#DETECTION_BASE.GetDetectedSet}(). A @{Core.Set#SET_UNIT} object will be returned.
  -- 
  -- Retrieve the formed @{Core.Zone#ZONE_UNIT}s as a result of the grouping the detected units within the DetectionZoneRange, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZones}().
  -- To understand the amount of zones created, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZoneCount}(). 
  -- If you want to obtain a specific zone from the DetectedZones, use the method @{Functional.Detection#DETECTION_BASE.GetDetectionZone}() with a given index.
  -- 
  -- ## 4.4) Flare or Smoke detected units
  -- 
  -- Use the methods @{Functional.Detection#DETECTION_ZONES.FlareDetectedUnits}() or @{Functional.Detection#DETECTION_ZONES.SmokeDetectedUnits}() to flare or smoke the detected units when a new detection has taken place.
  -- 
  -- ## 4.5) Flare or Smoke or Bound detected zones
  -- 
  -- Use the methods:
  -- 
  --   * @{Functional.Detection#DETECTION_ZONES.FlareDetectedZones}() to flare in a color 
  --   * @{Functional.Detection#DETECTION_ZONES.SmokeDetectedZones}() to smoke in a color
  --   * @{Functional.Detection#DETECTION_ZONES.SmokeDetectedZones}() to bound with a tire with a white flag
  --   
  -- the detected zones when a new detection has taken place.
  -- 
  -- @field #DETECTION_ZONES
  DETECTION_ZONES = {
    ClassName = "DETECTION_ZONES",
    DetectionZoneRange = nil,
  }


  --- DETECTION_ZONES constructor.
  -- @param #DETECTION_ZONES self
  -- @param Core.Set#SET_ZONE DetectionSetZone The @{Core.Set} of ZONE_RADIUS.
  -- @param DCS#Coalition.side DetectionCoalition The coalition of the detection.
  -- @return #DETECTION_ZONES
  function DETECTION_ZONES:New( DetectionSetZone, DetectionCoalition )

    -- Inherits from DETECTION_BASE
    local self = BASE:Inherit( self, DETECTION_BASE:New( DetectionSetZone ) ) -- #DETECTION_ZONES

    self.DetectionSetZone = DetectionSetZone  -- Core.Set#SET_ZONE
    self.DetectionCoalition = DetectionCoalition

    self._SmokeDetectedUnits = false
    self._FlareDetectedUnits = false
    self._SmokeDetectedZones = false
    self._FlareDetectedZones = false
    self._BoundDetectedZones = false

    return self
  end

  -- @param #DETECTION_ZONES self
  -- @param #number The amount of alive recce.
  function DETECTION_ZONES:CountAliveRecce()

    return self.DetectionSetZone:Count()

  end    
  
  -- @param #DETECTION_ZONES self
  function DETECTION_ZONES:ForEachAliveRecce( IteratorFunction, ... )
    self:F2( arg )
    
    self.DetectionSetZone:ForEachZone( IteratorFunction, arg )
  
    return self
  end

  --- Report summary of a detected item using a given numeric index.
  -- @param #DETECTION_ZONES self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem The DetectedItem.
  -- @param Wrapper.Group#GROUP AttackGroup The group to get the settings for.
  -- @param Core.Settings#SETTINGS Settings (Optional) Message formatting settings to use.
  -- @return Core.Report#REPORT The report of the detection items.
  function DETECTION_ZONES:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings )
    self:F( { DetectedItem = DetectedItem } )
  
    local DetectedItemID = self:GetDetectedItemID( DetectedItem )
    
    if DetectedItem then
      local DetectedSet = self:GetDetectedItemSet( DetectedItem )
      local ReportSummaryItem
      
      local DetectedZone = self:GetDetectedItemZone( DetectedItem )
      local DetectedItemCoordinate = DetectedZone:GetCoordinate()
      local DetectedItemCoordText = DetectedItemCoordinate:ToString( AttackGroup, Settings )

      local ThreatLevelA2G = self:GetDetectedItemThreatLevel( DetectedItem )
      local DetectedItemsCount = DetectedSet:Count()
      local DetectedItemsTypes = DetectedSet:GetTypeNames()
      
      local Report = REPORT:New()
      Report:Add(DetectedItemID .. ", " .. DetectedItemCoordText)
      Report:Add( string.format( "Threat: [%s]", string.rep(  "■", ThreatLevelA2G ), string.rep(  "□", 10-ThreatLevelA2G ) ) )
      Report:Add( string.format("Type: %2d of %s", DetectedItemsCount, DetectedItemsTypes ) )
      Report:Add( string.format("Detected: %s", DetectedItem.IsDetected and "yes" or "no" ) )
      
      return Report
    end
    
    return nil
  end

  --- Report detailed of a detection result.
  -- @param #DETECTION_ZONES self
  -- @param Wrapper.Group#GROUP AttackGroup The group to generate the report for.
  -- @return #string
  function DETECTION_ZONES:DetectedReportDetailed( AttackGroup ) --R2.1  Fixed missing report
    self:F()
    
    local Report = REPORT:New()
    for DetectedItemIndex, DetectedItem in pairs( self.DetectedItems ) do
      local DetectedItem = DetectedItem -- #DETECTION_BASE.DetectedItem
      local ReportSummary = self:DetectedItemReportSummary( DetectedItem, AttackGroup )
      Report:SetTitle( "Detected areas:" )
      Report:Add( ReportSummary:Text() )
    end
    
    local ReportText = Report:Text()
    
    return ReportText
  end

  
  --- Calculate the optimal intercept point of the DetectedItem.
  -- @param #DETECTION_ZONES self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem
  function DETECTION_ZONES:CalculateIntercept( DetectedItem )

    local DetectedCoord = DetectedItem.Coordinate
--    local DetectedSpeed = DetectedCoord:GetVelocity()
--    local DetectedHeading = DetectedCoord:GetHeading()
--
--    if self.Intercept then
--      local DetectedSet = DetectedItem.Set
--      -- todo: speed
--  
--      local TranslateDistance = DetectedSpeed * self.InterceptDelay
--      
--      local InterceptCoord = DetectedCoord:Translate( TranslateDistance, DetectedHeading )
--      
--      DetectedItem.InterceptCoord = InterceptCoord
--    else
--      DetectedItem.InterceptCoord = DetectedCoord
--    end
    DetectedItem.InterceptCoord = DetectedCoord    
  end
  
  

  --- Smoke the detected units
  -- @param #DETECTION_ZONES self
  -- @return #DETECTION_ZONES self
  function DETECTION_ZONES:SmokeDetectedUnits()
    self:F2()
  
    self._SmokeDetectedUnits = true
    return self
  end
  
  --- Flare the detected units
  -- @param #DETECTION_ZONES self
  -- @return #DETECTION_ZONES self
  function DETECTION_ZONES:FlareDetectedUnits()
    self:F2()
  
    self._FlareDetectedUnits = true
    return self
  end
  
  --- Smoke the detected zones
  -- @param #DETECTION_ZONES self
  -- @return #DETECTION_ZONES self
  function DETECTION_ZONES:SmokeDetectedZones()
    self:F2()
  
    self._SmokeDetectedZones = true
    return self
  end
  
  --- Flare the detected zones
  -- @param #DETECTION_ZONES self
  -- @return #DETECTION_ZONES self
  function DETECTION_ZONES:FlareDetectedZones()
    self:F2()
  
    self._FlareDetectedZones = true
    return self
  end

  --- Bound the detected zones
  -- @param #DETECTION_ZONES self
  -- @return #DETECTION_ZONES self
  function DETECTION_ZONES:BoundDetectedZones()
    self:F2()
  
    self._BoundDetectedZones = true
    return self
  end
  
  --- Make text documenting the changes of the detected zone.
  -- @param #DETECTION_ZONES self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem
  -- @return #string The Changes text
  function DETECTION_ZONES:GetChangeText( DetectedItem )
    self:F( DetectedItem )
    
    local MT = {}
    
    for ChangeCode, ChangeData in pairs( DetectedItem.Changes ) do
  
      if ChangeCode == "AA" then
        MT[#MT+1] = "Detected new area " .. ChangeData.ID .. ". The center target is a " .. ChangeData.ItemUnitType .. "."
      end
  
      if ChangeCode == "RAU" then
        MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". Removed the center target."
      end
      
      if ChangeCode == "AAU" then
        MT[#MT+1] = "Changed area " .. ChangeData.ID .. ". The new center target is a " .. ChangeData.ItemUnitType .. "."
      end
      
      if ChangeCode == "RA" then
        MT[#MT+1] = "Removed old area " .. ChangeData.ID .. ". No more targets in this area."
      end
      
      if ChangeCode == "AU" then
        local MTUT = {}
        for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do
          if ChangeUnitType  ~= "ID" then
            MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType
          end
        end
        MT[#MT+1] = "Detected for area " .. ChangeData.ID .. " new target(s) " .. table.concat( MTUT, ", " ) .. "."
      end
  
      if ChangeCode == "RU" then
        local MTUT = {}
        for ChangeUnitType, ChangeUnitCount in pairs( ChangeData ) do
          if ChangeUnitType  ~= "ID" then
            MTUT[#MTUT+1] = ChangeUnitCount .. " of " .. ChangeUnitType
          end
        end
        MT[#MT+1] = "Removed for area " .. ChangeData.ID .. " invisible or destroyed target(s) " .. table.concat( MTUT, ", " ) .. "."
      end
      
    end
    
    return table.concat( MT, "\n" )
    
  end
  
  
  --- Make a DetectionSet table. This function will be overridden in the derived clsses.
  -- @param #DETECTION_ZONES self
  -- @return #DETECTION_ZONES self
  function DETECTION_ZONES:CreateDetectionItems()
  
  
    self:F( "Checking Detected Items for new Detected Units ..." )
    
    local DetectedUnits = SET_UNIT:New()
    
    -- First go through all zones, and check if there are new Zones.
    -- New Zones become a new DetectedItem.
    for ZoneName, DetectionZone in pairs( self.DetectionSetZone:GetSet() ) do
      
      local DetectedItem = self:GetDetectedItemByKey( ZoneName )

      if DetectedItem == nil then
        DetectedItem = self:AddDetectedItemZone( "ZONE", ZoneName, nil, DetectionZone )
      end

      local DetectedItemSetUnit = self:GetDetectedItemSet( DetectedItem )
      
      -- Scan the zone
      DetectionZone:Scan( { Object.Category.UNIT }, { Unit.Category.GROUND_UNIT } )
      
      -- For all the units in the zone,
      -- check if they are of the same coalition to be included.
      local ZoneUnits = DetectionZone:GetScannedUnits()
      for DCSUnitID, DCSUnit in pairs( ZoneUnits ) do
        local UnitName = DCSUnit:getName()
        local ZoneUnit = UNIT:FindByName( UnitName )
        local ZoneUnitCoalition = ZoneUnit:GetCoalition()
        if ZoneUnitCoalition == self.DetectionCoalition then
          if DetectedItemSetUnit:FindUnit( UnitName ) == nil and DetectedUnits:FindUnit( UnitName ) == nil then
            self:F( "Adding " .. UnitName )
            DetectedItemSetUnit:AddUnit( ZoneUnit )
            DetectedUnits:AddUnit( ZoneUnit )
          end
        end
      end
    end
            
    
    -- Now all the tests should have been build, now make some smoke and flares...
    -- We also report here the friendlies within the detected areas.
    
    for DetectedItemID, DetectedItemData in pairs( self.DetectedItems ) do
  
      local DetectedItem = DetectedItemData -- #DETECTION_BASE.DetectedItem
      local DetectedSet = self:GetDetectedItemSet( DetectedItem )
      local DetectedFirstUnit = DetectedSet:GetFirst()
      local DetectedZone = self:GetDetectedItemZone( DetectedItem )
 
      -- Set the last known coordinate to the detection item.
      local DetectedZoneCoord = DetectedZone:GetCoordinate()
      self:SetDetectedItemCoordinate( DetectedItem, DetectedZoneCoord, DetectedFirstUnit )
      
      self:CalculateIntercept( DetectedItem )
  
      -- We search for friendlies nearby.
      -- If there weren't any friendlies nearby, and now there are friendlies nearby, we flag the area as "changed".
      -- If there were friendlies nearby, and now there aren't any friendlies nearby, we flag the area as "changed".
      -- This is for the A2G dispatcher to detect if there is a change in the tactical situation.
      local OldFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT )
      self:ReportFriendliesNearBy( { DetectedItem = DetectedItem, ReportSetGroup = self.DetectionSetGroup } ) -- Fill the Friendlies table
      local NewFriendliesNearbyGround = self:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT  )
      if OldFriendliesNearbyGround ~= NewFriendliesNearbyGround then
        DetectedItem.Changed = true
      end

      self:SetDetectedItemThreatLevel( DetectedItem )  -- Calculate A2G threat level
      --self:NearestRecce( DetectedItem )

      
      if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then
        DetectedZone:SmokeZone( SMOKECOLOR.Red, 30 )
      end
      
      --DetectedSet:Flush( self )
      
      DetectedSet:ForEachUnit(
        -- @param Wrapper.Unit#UNIT DetectedUnit
        function( DetectedUnit )
          if DetectedUnit:IsAlive() then
            --self:T( "Detected Set #" .. DetectedItem.ID .. ":" .. DetectedUnit:GetName() )
            if DETECTION_ZONES._FlareDetectedUnits or self._FlareDetectedUnits then
              DetectedUnit:FlareGreen()
            end
            if DETECTION_ZONES._SmokeDetectedUnits or self._SmokeDetectedUnits then
              DetectedUnit:SmokeGreen()
            end
          end
        end
      )
      if DETECTION_ZONES._FlareDetectedZones or self._FlareDetectedZones then
        DetectedZone:FlareZone( SMOKECOLOR.White, 30, math.random( 0,90 ) )
      end
      if DETECTION_ZONES._SmokeDetectedZones or self._SmokeDetectedZones then
        DetectedZone:SmokeZone( SMOKECOLOR.White, 30 )
      end

      if DETECTION_ZONES._BoundDetectedZones or self._BoundDetectedZones then
        self.CountryID = DetectedSet:GetFirst():GetCountry()
        DetectedZone:BoundZone( 12, self.CountryID )
      end
    end
  
  end

  -- @param #DETECTION_ZONES self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @param Detection The element on which the detection is based.
  -- @param #number DetectionTimeStamp Time stamp of detection event.
  function DETECTION_ZONES:onafterDetection( From, Event, To, Detection, DetectionTimeStamp  )

    self.DetectionRun = self.DetectionRun + 1
    if self.DetectionCount > 0 and self.DetectionRun == self.DetectionCount then
      self:CreateDetectionItems() -- Polymorphic call to Create/Update the DetectionItems list for the DETECTION_ class grouping method.
  
      for DetectedItemID, DetectedItem in pairs( self.DetectedItems ) do
        self:UpdateDetectedItemDetection( DetectedItem )
        self:CleanDetectionItem( DetectedItem, DetectedItemID ) -- Any DetectionItem that has a Set with zero elements in it, must be removed from the DetectionItems list.
        if DetectedItem then
          self:__DetectedItem( 0.1, DetectedItem )
        end
      end
      self:__Detect( -self.RefreshTimeInterval )
    end
  end
  
  
  --- Set IsDetected flag for the DetectedItem, which can have more units.
  -- @param #DETECTION_ZONES self
  -- @return #DETECTION_ZONES.DetectedItem DetectedItem
  -- @return #boolean true if at least one UNIT is detected from the DetectedSet, false if no UNIT was detected from the DetectedSet.
  function DETECTION_ZONES:UpdateDetectedItemDetection( DetectedItem )
    
    local IsDetected = true
    
    DetectedItem.IsDetected = true
    
    return IsDetected
  end
  
end --- **Functional** - Management of target **Designation**. Lase, smoke and illuminate targets.
--
-- ===
--
-- ## Features:
-- 
--   * Faciliate the communication of detected targets to players.
--   * Designate targets using lasers, through a menu system.
--   * Designate targets using smoking, through a menu system.
--   * Designate targets using illumination, through a menu system.
--   * Auto lase targets.
--   * Refresh detection upon specified time intervals.
--   * Prioritization on threat levels.
--   * Reporting system of threats.
--  
-- ===
-- 
-- ## Additional Material:
--
-- * **Demo Missions:** [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Functional/Designate)
-- * **YouTube videos:** None
-- * **Guides:** None
--
-- ===
--
-- Targets detected by recce will be communicated to a group of attacking players.  
-- A menu system is made available that allows to: 
-- 
--   * **Lased** for a period of time.
--   * **Smoked**. Artillery or airplanes with Illuminatino ordonance need to be present. (WIP, but early demo ready.)
--   * **Illuminated** through an illumination bomb. Artillery or airplanes with Illuminatino ordonance need to be present. (WIP, but early demo ready.
-- 
-- The following terminology is being used throughout this document:
-- 
--   * The **DesignateObject** is the object of the DESIGNATE class, which is this class explained in the document.
--   * The **DetectionObject** is the object of a DETECTION_ class (DETECTION_TYPES, DETECTION_AREAS, DETECTION_UNITS), which is executing the detection and grouping of Targets into _DetectionItems_.
--   * **TargetGroups** is the list of detected target groupings by the _DetectionObject_. Each _TargetGroup_ contains a _TargetSet_.
--   * **TargetGroup** is one element of the __TargetGroups__ list, and contains a _TargetSet_.
--   * The **TargetSet** is a SET_UNITS collection of _Targets_, that have been detected by the _DetectionObject_.
--   * A **Target** is a detected UNIT object by the _DetectionObject_.
--   * A **Threat Level** is a number from 0 to 10 that is calculated based on the threat of the Target in an Air to Ground battle scenario.
--   * The **RecceSet** is a SET_GROUP collection that contains the **RecceGroups**.
--   * A **RecceGroup** is a GROUP object containing the **Recces**.
--   * A **Recce** is a UNIT object executing the reconnaissance as part the _DetectionObject_. A Recce can be of any UNIT type.
--   * An **AttackGroup** is a GROUP object that contain _Players_.
--   * A **Player** is an active CLIENT object containing a human player.
--   * A **Designate Menu** is the menu that is dynamically created during the designation process for each _AttackGroup_.
-- 
-- # Player Manual
-- 
-- ![Banner Image](..\Presentations\DESIGNATE\Dia3.JPG)
-- 
-- A typical mission setup would require Recce (a @{Core.Set} of Recce) to be detecting potential targets.
-- The DetectionObject will group the detected targets based on the detection method being used.
-- Possible detection methods could be by Area, by Type or by Unit.
-- Each grouping will result in a **TargetGroup**, for terminology and clarity we will use this term throughout the document.
-- 
-- **Recce** require to have Line of Sight (LOS) towards the targets.
-- The **Recce** will report any detected targets to the Players (on the picture Observers).
-- When targets are detected, a menu will be made available that allows those **TargetGroups** to be designated.
-- Designation can be done by Lasing, Smoking and Illumination.
-- Smoking is useful during the day, while illumination is recommended to be used during the night.
-- Smoking can designate specific targets, but not very precise, while lasing is very accurate and allows to
-- players to attack the targets using laser guided bombs or rockets.
-- Illumination will lighten up the Target Area.
-- 
-- **Recce** can be ground based or airborne. Airborne **Recce** (AFAC) can be really useful to designate a large amount of targets
-- in a wide open area, as airborne **Recce** has a large LOS.
-- However, ground based **Recce** are very useful to smoke or illuminate targets, as they can be much closer
-- to the Target Area.
-- 
-- It is recommended to make the **Recce** invisible and immortal using the Mission Editor in DCS World.
-- This will ensure that the detection process won't be interrupted and that targets can be designated.
-- However, you don't have to, so to simulate a more real-word situation or simulation, **Recce can also be destroyed**!
--  
-- ## 1. Player View (Observer)
-- 
-- ![Banner Image](..\Presentations\DESIGNATE\Dia4.JPG)
-- 
-- The RecceSet is continuously detecting for potential Targets, 
-- executing its task as part of the DetectionObject.
-- Once Targets have been detected, the DesignateObject will trigger the **Detect Event**.
-- 
-- In order to prevent an overflow in the DesignateObject of detected targets, 
-- there is a maximum amount of TargetGroups 
-- that can be put in **scope** of the DesignateObject.
-- We call this the **MaximumDesignations** term.
-- 
-- ## 2. Designate Menu
-- 
-- ![Banner Image](..\Presentations\DESIGNATE\Dia5.JPG)
-- 
-- For each detected TargetGroup, there is:
-- 
--   * A **Designate Menu** are created and continuously refreshed, containing the **DesignationID** and the **Designation Status**.
--   * The RecceGroups are reporting to each AttackGroup, sending **Messages** containing the Threat Level and the TargetSet composition.
-- 
-- A Player can then select an action from the **Designate Menu**. 
-- The Designation Status is shown between the (   ).
-- 
-- It indicates for each TargetGroup the current active designation action applied:
-- 
--   * An "I" for Illumnation designation.
--   * An "S" for Smoking designation.
--   * An "L" for Lasing designation.
--
-- Note that multiple designation methods can be active at the same time!
-- Note the **Auto Lase** option. When switched on, the available **Recce** will lase 
-- Targets when detected.
-- 
-- Targets are designated per **Threat Level**. 
-- The most threatening targets from an Air to Ground perspective, are designated first!
-- This is for all designation methods.
-- 
-- ![Banner Image](..\Presentations\DESIGNATE\Dia6.JPG)
-- 
-- Each Designate Menu has a sub menu structure, which allows specific actions to be triggered:
-- 
--   * Lase Targets using a specific laser code.
--   * Smoke Targets using a specific smoke color.
--   * Illuminate areas.
-- 
-- ## 3. Lasing Targets
-- 
-- ![Banner Image](..\Presentations\DESIGNATE\Dia7.JPG)
-- 
-- Lasing targets is done as expected. Each available Recce can lase only ONE target through!
-- 
-- ![Banner Image](..\Presentations\DESIGNATE\Dia8.JPG)
-- 
-- Lasing can be done for specific laser codes. The Su-25T requires laser code 1113, while the A-10A requires laser code 1680.
-- For those, specific menu options can be made available for players to lase with these codes.
-- Auto Lase (as explained above), will ensure continuous lasing of available targets.
-- The status report shows which targets are being designated.
-- 
-- The following logic is executed when a TargetGroup is selected to be *lased* from the Designation Menu:
-- 
--   * The RecceSet is searched for any Recce that is within *designation distance* from a Target in the TargetGroup that is currently not being designated.
--   * If there is a Recce found that is currently no designating a target, and is within designation distance from the Target, then that Target will be designated.
--   * During designation, any Recce that does not have Line of Sight (LOS) and is not within disignation distance from the Target, will stop designating the Target, and a report is given.
--   * When a Recce is designating a Target, and that Target is destroyed, then the Recce will stop designating the Target, and will report the event.
--   * When a Recce is designating a Target, and that Recce is destroyed, then the Recce will be removed from the RecceSet and designation will stop without reporting.
--   * When all RecceGroups are destroyed from the RecceSet, then the DesignationObject will stop functioning, and nothing will be reported.
--   
-- In this way, DESIGNATE assists players to designate ground targets for a coordinated attack!
-- 
-- ## 4. Illuminating Targets
-- 
-- ![Banner Image](..\Presentations\DESIGNATE\Dia9.JPG)
-- 
-- Illumination bombs are fired between 500 and 700 meters altitude and will burn about 2 minutes, while slowly decending.
-- Each available recce within range will fire an illumination bomb.
-- Illumination bombs can be fired in while lasing targets.
-- When illumination bombs are fired, it will take about 2 minutes until a sequent bomb run can be requested using the menus.
-- 
-- ## 5. Smoking Targets
-- 
-- ![Banner Image](..\Presentations\DESIGNATE\Dia10.JPG)
-- 
-- Smoke will fire for 5 minutes.
-- Each available recce within range will smoke a target.
-- Smoking can be requested while lasing targets.
-- Smoke will appear "around" the targets, because of accuracy limitations.
-- 
-- 
-- Have FUN!
-- 
-- ===
-- 
-- ### Contributions: 
-- 
--   * **Ciribob**: Showing the way how to lase targets + how laser codes work!!! Explained the autolase script.
--   * **EasyEB**: Ideas and Beta Testing
--   * **Wingthor**: Beta Testing
-- 
-- ### Authors: 
-- 
--   * **FlightControl**: Design & Programming
-- 
-- ===
-- 
-- @module Functional.Designate
-- @image Designation.JPG

do -- DESIGNATE

  -- @type DESIGNATE
  -- @extends Core.Fsm#FSM_PROCESS

  --- Manage the designation of detected targets.
  -- 
  -- 
  -- # 1. DESIGNATE constructor
  --   
  --   * @{#DESIGNATE.New}(): Creates a new DESIGNATE object.
  -- 
  -- # 2. DESIGNATE is a FSM
  -- 
  -- Designate is a finite state machine, which allows for controlled transitions of states.
  -- 
  -- ## 2.1 DESIGNATE States
  -- 
  --   * **Designating** ( Group ): The designation process.
  -- 
  -- ## 2.2 DESIGNATE Events
  -- 
  --   * **@{#DESIGNATE.Detect}**: Detect targets.
  --   * **@{#DESIGNATE.LaseOn}**: Lase the targets with the specified Index.
  --   * **@{#DESIGNATE.LaseOff}**: Stop lasing the targets with the specified Index.
  --   * **@{#DESIGNATE.Smoke}**: Smoke the targets with the specified Index.
  --   * **@{#DESIGNATE.Status}**: Report designation status.
  -- 
  -- # 3. Maximum Designations
  -- 
  -- In order to prevent an overflow of designations due to many Detected Targets, there is a 
  -- Maximum Designations scope that is set in the DesignationObject.
  -- 
  -- The method @{#DESIGNATE.SetMaximumDesignations}() will put a limit on the amount of designations (target groups) put in scope of the DesignationObject.
  -- Using the menu system, the player can "forget" a designation, so that gradually a new designation can be put in scope when detected.
  -- 
  -- # 4. Laser codes
  -- 
  -- ## 4.1. Set possible laser codes
  -- 
  -- An array of laser codes can be provided, that will be used by the DESIGNATE when lasing.
  -- The laser code is communicated by the Recce when it is lasing a larget.
  -- Note that the default laser code is 1113.
  -- Working known laser codes are: 1113,1462,1483,1537,1362,1214,1131,1182,1644,1614,1515,1411,1621,1138,1542,1678,1573,1314,1643,1257,1467,1375,1341,1275,1237
  -- 
  -- Use the method @{#DESIGNATE.SetLaserCodes}() to set the possible laser codes to be selected from.
  -- One laser code can be given or an sequence of laser codes through an table...
  -- 
  --     Designate:SetLaserCodes( 1214 )
  --     
  -- The above sets one laser code with the value 1214.
  -- 
  --     Designate:SetLaserCodes( { 1214, 1131, 1614, 1138 } )
  --     
  -- The above sets a collection of possible laser codes that can be assigned. **Note the { } notation!**
  -- 
  -- ## 4.2. Auto generate laser codes
  -- 
  -- Use the method @{#DESIGNATE.GenerateLaserCodes}() to generate all possible laser codes. Logic implemented and advised by Ciribob!
  -- 
  -- ## 4.3. Add specific lase codes to the lase menu
  -- 
  -- Certain plane types can only drop laser guided ordonnance when targets are lased with specific laser codes.
  -- The SU-25T needs targets to be lased using laser code 1113.
  -- The A-10A needs targets to be lased using laser code 1680.
  -- 
  -- The method @{#DESIGNATE.AddMenuLaserCode}() to allow a player to lase a target using a specific laser code.
  -- Remove such a lase menu option using @{#DESIGNATE.RemoveMenuLaserCode}().
  -- 
  -- # 5. Autolase to automatically lase detected targets.
  -- 
  -- DetectionItems can be auto lased once detected by Recces. As such, there is almost no action required from the Players using the Designate Menu.
  -- The **auto lase** function can be activated through the Designation Menu.
  -- Use the method @{#DESIGNATE.SetAutoLase}() to activate or deactivate the auto lase function programmatically.
  -- Note that autolase will automatically activate lasing for ALL DetectedItems. Individual items can be switched-off if required using the Designation Menu.
  -- 
  --     Designate:SetAutoLase( true )
  -- 
  -- Activate the auto lasing.
  -- 
  -- # 6. Target prioritization on threat level
  -- 
  -- Targets can be detected of different types in one DetectionItem. Depending on the type of the Target, a different threat level applies in an Air to Ground combat context.
  -- SAMs are of a higher threat than normal tanks. So, if the Target type was recognized, the Recces will select those targets that form the biggest threat first,
  -- and will continue this until the remaining vehicles with the lowest threat have been reached.
  -- 
  -- This threat level prioritization can be activated using the method @{#DESIGNATE.SetThreatLevelPrioritization}().
  -- If not activated, Targets will be selected in a random order, but most like those first which are the closest to the Recce marking the Target.
  -- 
  --     Designate:SetThreatLevelPrioritization( true )
  --     
  -- The example will activate the threat level prioritization for this the Designate object. Threats will be marked based on the threat level of the Target.
  -- 
  -- # 7. Designate Menu Location for a Mission
  -- 
  -- You can make DESIGNATE work for a @{Tasking.Mission#MISSION} object. In this way, the designate menu will not appear in the root of the radio menu, but in the menu of the Mission.
  -- Use the method @{#DESIGNATE.SetMission}() to set the @{Tasking.Mission} object for the designate function.
  -- 
  -- # 8. Status Report
  -- 
  -- A status report is available that displays the current Targets detected, grouped per DetectionItem, and a list of which Targets are currently being marked.
  -- 
  --   * The status report can be shown by selecting "Status" -> "Report Status" from the Designation menu .
  --   * The status report can be automatically flashed by selecting "Status" -> "Flash Status On".
  --   * The automatic flashing of the status report can be deactivated by selecting "Status" -> "Flash Status Off".
  --   * The flashing of the status menu is disabled by default.
  --   * The method @{#DESIGNATE.SetFlashStatusMenu}() can be used to enable or disable to flashing of the status menu.
  --   
  --     Designate:SetFlashStatusMenu( true )
  --     
  -- The example will activate the flashing of the status menu for this Designate object.
  -- 
  -- @field #DESIGNATE
  DESIGNATE = {
    ClassName = "DESIGNATE",
  }

  --- DESIGNATE Constructor. This class is an abstract class and should not be instantiated.
  -- @param #DESIGNATE self
  -- @param Tasking.CommandCenter#COMMANDCENTER CC
  -- @param Functional.Detection#DETECTION_BASE Detection
  -- @param Core.Set#SET_GROUP AttackSet The Attack collection of GROUP objects to designate and report for.
  -- @param Tasking.Mission#MISSION Mission (Optional) The Mission where the menu needs to be attached.
  -- @return #DESIGNATE
  function DESIGNATE:New( CC, Detection, AttackSet, Mission )
  
    local self = BASE:Inherit( self, FSM:New() ) -- #DESIGNATE
    self:F( { Detection } )
  
    self:SetStartState( "Designating" )
    
    self:AddTransition( "*", "Detect", "*" )
    --- Detect Handler OnBefore for DESIGNATE
    -- @function [parent=#DESIGNATE] OnBeforeDetect
    -- @param #DESIGNATE self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Detect Handler OnAfter for DESIGNATE
    -- @function [parent=#DESIGNATE] OnAfterDetect
    -- @param #DESIGNATE self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Detect Trigger for DESIGNATE
    -- @function [parent=#DESIGNATE] Detect
    -- @param #DESIGNATE self
    
    --- Detect Asynchronous Trigger for DESIGNATE
    -- @function [parent=#DESIGNATE] __Detect
    -- @param #DESIGNATE self
    -- @param #number Delay
    
    self:AddTransition( "*", "LaseOn", "Lasing" )
    --- LaseOn Handler OnBefore for DESIGNATE 
    -- @function [parent=#DESIGNATE ] OnBeforeLaseOn
    -- @param #DESIGNATE  self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- LaseOn Handler OnAfter for DESIGNATE 
    -- @function [parent=#DESIGNATE ] OnAfterLaseOn
    -- @param #DESIGNATE  self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- LaseOn Trigger for DESIGNATE 
    -- @function [parent=#DESIGNATE ] LaseOn
    -- @param #DESIGNATE  self
    
    --- LaseOn Asynchronous Trigger for DESIGNATE 
    -- @function [parent=#DESIGNATE ] __LaseOn
    -- @param #DESIGNATE  self
    -- @param #number Delay
    
    self:AddTransition( "Lasing", "Lasing", "Lasing" )
    
    self:AddTransition( "*", "LaseOff", "Designate" )
    --- LaseOff Handler OnBefore for DESIGNATE 
    -- @function [parent=#DESIGNATE ] OnBeforeLaseOff
    -- @param #DESIGNATE  self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- LaseOff Handler OnAfter for DESIGNATE 
    -- @function [parent=#DESIGNATE ] OnAfterLaseOff
    -- @param #DESIGNATE  self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- LaseOff Trigger for DESIGNATE 
    -- @function [parent=#DESIGNATE ] LaseOff
    -- @param #DESIGNATE  self
    
    --- LaseOff Asynchronous Trigger for DESIGNATE 
    -- @function [parent=#DESIGNATE ] __LaseOff
    -- @param #DESIGNATE  self
    -- @param #number Delay
    
    self:AddTransition( "*", "Smoke", "*" )
    --- Smoke Handler OnBefore for DESIGNATE 
    -- @function [parent=#DESIGNATE ] OnBeforeSmoke
    -- @param #DESIGNATE  self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Smoke Handler OnAfter for DESIGNATE 
    -- @function [parent=#DESIGNATE ] OnAfterSmoke
    -- @param #DESIGNATE  self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Smoke Trigger for DESIGNATE 
    -- @function [parent=#DESIGNATE ] Smoke
    -- @param #DESIGNATE  self
    
    --- Smoke Asynchronous Trigger for DESIGNATE 
    -- @function [parent=#DESIGNATE ] __Smoke
    -- @param #DESIGNATE  self
    -- @param #number Delay
    
    self:AddTransition( "*", "Illuminate", "*" )
    --- Illuminate Handler OnBefore for DESIGNATE
    -- @function [parent=#DESIGNATE] OnBeforeIlluminate
    -- @param #DESIGNATE self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Illuminate Handler OnAfter for DESIGNATE
    -- @function [parent=#DESIGNATE] OnAfterIlluminate
    -- @param #DESIGNATE self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Illuminate Trigger for DESIGNATE
    -- @function [parent=#DESIGNATE] Illuminate
    -- @param #DESIGNATE self
    
    --- Illuminate Asynchronous Trigger for DESIGNATE
    -- @function [parent=#DESIGNATE] __Illuminate
    -- @param #DESIGNATE self
    -- @param #number Delay

    self:AddTransition( "*", "DoneSmoking", "*" )
    self:AddTransition( "*", "DoneIlluminating", "*" )
    
    self:AddTransition( "*", "Status", "*" )
    --- Status Handler OnBefore for DESIGNATE 
    -- @function [parent=#DESIGNATE ] OnBeforeStatus
    -- @param #DESIGNATE  self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Status Handler OnAfter for DESIGNATE 
    -- @function [parent=#DESIGNATE ] OnAfterStatus
    -- @param #DESIGNATE  self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Status Trigger for DESIGNATE 
    -- @function [parent=#DESIGNATE ] Status
    -- @param #DESIGNATE  self
    
    --- Status Asynchronous Trigger for DESIGNATE 
    -- @function [parent=#DESIGNATE ] __Status
    -- @param #DESIGNATE  self
    -- @param #number Delay
    
    self.CC = CC
    self.Detection = Detection
    self.AttackSet = AttackSet
    self.RecceSet = Detection:GetDetectionSet()
    self.Recces = {}
    self.Designating = {}
    self:SetDesignateName()
    
    self:SetLaseDuration() -- Default is 120 seconds.
    
    self:SetFlashStatusMenu( false )
    self:SetFlashDetectionMessages( true )
    self:SetMission( Mission )
    
    self:SetLaserCodes( { 1688, 1130, 4785, 6547, 1465, 4578 } ) -- set self.LaserCodes
    self:SetAutoLase( false, false ) -- set self.Autolase and don't send message.
    
    self:SetThreatLevelPrioritization( false ) -- self.ThreatLevelPrioritization, default is threat level priorization off
    self:SetMaximumDesignations( 5 ) -- Sets the maximum designations. The default is 5 designations.
    self:SetMaximumDistanceDesignations( 8000 )  -- Sets the maximum distance on which designations can be accepted. The default is 8000 meters.
    self:SetMaximumMarkings( 2 ) -- Per target group, a maximum of 2 markings will be made by default.

    self:SetDesignateMenu()
    
    self.LaserCodesUsed = {}
    
    self.MenuLaserCodes = {} -- This map contains the laser codes that will be shown in the designate menu to lase with specific laser codes.
        
    self.Detection:__Start( 2 )
    
    self:__Detect( -15 )
    
    self.MarkScheduler = SCHEDULER:New( self )
    
    return self
  end

  --- Set the flashing of the status menu for all AttackGroups.
  -- @param #DESIGNATE self
  -- @param #boolean FlashMenu true: the status menu will be flashed every detection run; false: no flashing of the menu.
  -- @return #DESIGNATE
  -- @usage
  -- 
  -- -- Enable the designate status message flashing...
  -- Designate:SetFlashStatusMenu( true )
  -- 
  -- -- Disable the designate statusmessage flashing...
  -- Designate:SetFlashStatusMenu()
  -- 
  -- -- Disable the designate status message flashing...
  -- Designate:SetFlashStatusMenu( false )
  function DESIGNATE:SetFlashStatusMenu( FlashMenu ) --R2.1

    self.FlashStatusMenu = {}

    self.AttackSet:ForEachGroupAlive(
    
      -- @param Wrapper.Group#GROUP AttackGroup
      function( AttackGroup )
        self.FlashStatusMenu[AttackGroup] = FlashMenu
      end
    )

    return self
  end

  --- Set the flashing of the new detection messages.
  -- @param #DESIGNATE self
  -- @param #boolean FlashDetectionMessage true: The detection message will be flashed every time a new detection was done; false: no messages will be displayed.
  -- @return #DESIGNATE
  -- @usage
  -- 
  -- -- Enable the message flashing...
  -- Designate:SetFlashDetectionMessages( true )
  -- 
  -- -- Disable the message flashing...
  -- Designate:SetFlashDetectionMessages()
  -- 
  -- -- Disable the message flashing...
  -- Designate:SetFlashDetectionMessages( false )
  function DESIGNATE:SetFlashDetectionMessages( FlashDetectionMessage )

    self.FlashDetectionMessage = {}

    self.AttackSet:ForEachGroupAlive(
    
      -- @param Wrapper.Group#GROUP AttackGroup
      function( AttackGroup )
        self.FlashDetectionMessage[AttackGroup] = FlashDetectionMessage
      end
    )

    return self
  end


  --- Set the maximum amount of designations (target groups). This will put a limit on the amount of designations in scope.
  -- Using the menu system, the player can "forget" a designation, so that gradually a new designation can be put in scope when detected.
  -- @param #DESIGNATE self
  -- @param #number MaximumDesignations
  -- @return #DESIGNATE
  function DESIGNATE:SetMaximumDesignations( MaximumDesignations )
    self.MaximumDesignations = MaximumDesignations
    return self
  end
  

  --- Set the maximum ground designation distance.
  -- @param #DESIGNATE self
  -- @param #number MaximumDistanceGroundDesignation Maximum ground designation distance in meters.
  -- @return #DESIGNATE
  function DESIGNATE:SetMaximumDistanceGroundDesignation( MaximumDistanceGroundDesignation )
    self.MaximumDistanceGroundDesignation = MaximumDistanceGroundDesignation
    return self
  end
  
  
  --- Set the maximum air designation distance.
  -- @param #DESIGNATE self
  -- @param #number MaximumDistanceAirDesignation Maximum air designation distance in meters.
  -- @return #DESIGNATE
  function DESIGNATE:SetMaximumDistanceAirDesignation( MaximumDistanceAirDesignation )
    self.MaximumDistanceAirDesignation = MaximumDistanceAirDesignation
    return self
  end
  
  
  --- Set the overall maximum distance when designations can be accepted.
  -- @param #DESIGNATE self
  -- @param #number MaximumDistanceDesignations Maximum distance in meters to accept designations.
  -- @return #DESIGNATE
  function DESIGNATE:SetMaximumDistanceDesignations( MaximumDistanceDesignations )
    self.MaximumDistanceDesignations = MaximumDistanceDesignations
    return self
  end
  
  
  --- Set the maximum amount of markings FACs will do, per designated target group. This will limit the number of parallelly marked units of a target group.
  -- @param #DESIGNATE self
  -- @param #number MaximumMarkings Maximum markings FACs will do, per designated target group.
  -- @return #DESIGNATE
  function DESIGNATE:SetMaximumMarkings( MaximumMarkings )
    self.MaximumMarkings = MaximumMarkings
    return self
  end
  
  
  --- Set an array of possible laser codes.
  -- Each new lase will select a code from this table.
  -- @param #DESIGNATE self
  -- @param #list<#number> LaserCodes
  -- @return #DESIGNATE
  function DESIGNATE:SetLaserCodes( LaserCodes ) --R2.1

    self.LaserCodes = ( type( LaserCodes ) == "table" ) and LaserCodes or { LaserCodes }
    self:F( { LaserCodes = self.LaserCodes } )
    
    self.LaserCodesUsed = {}

    return self
  end
  

  --- Add a specific lase code to the designate lase menu to lase targets with a specific laser code.
  -- The MenuText will appear in the lase menu.
  -- @param #DESIGNATE self
  -- @param #number LaserCode The specific laser code to be added to the lase menu.
  -- @param #string MenuText The text to be shown to the player. If you specify a %d in the MenuText, the %d will be replaced with the LaserCode specified.
  -- @return #DESIGNATE
  -- @usage
  --   RecceDesignation:AddMenuLaserCode( 1113, "Lase with %d for Su-25T" )
  --   RecceDesignation:AddMenuLaserCode( 1680, "Lase with %d for A-10A" )
  -- 
  function DESIGNATE:AddMenuLaserCode( LaserCode, MenuText )

    self.MenuLaserCodes[LaserCode] = MenuText
    self:SetDesignateMenu()
    
    return self
  end
  
  
  --- Removes a specific lase code from the designate lase menu.
  -- @param #DESIGNATE self
  -- @param #number LaserCode The specific laser code that was set to be added to the lase menu.
  -- @return #DESIGNATE
  -- @usage
  --   RecceDesignation:RemoveMenuLaserCode( 1113 )
  --   
  function DESIGNATE:RemoveMenuLaserCode( LaserCode )

    self.MenuLaserCodes[LaserCode] = nil
    self:SetDesignateMenu()

    return self
  end
  
  
  

  --- Set the name of the designation. The name will appear in the menu.
  -- This method can be used to control different designations for different plane types.
  -- @param #DESIGNATE self
  -- @param #string DesignateName
  -- @return #DESIGNATE
  function DESIGNATE:SetDesignateName( DesignateName ) 

    self.DesignateName = "Designation" .. ( DesignateName and ( " for " .. DesignateName ) or "" )

    return self
  end
  
  --- Set the lase duration for designations.
  -- @param #DESIGNATE self
  -- @param #number LaseDuration The time in seconds a lase will continue to hold on target. The default is 120 seconds.
  -- @return #DESIGNATE
  function DESIGNATE:SetLaseDuration( LaseDuration )
    self.LaseDuration = LaseDuration or 120
    return self
  end

  --- Generate an array of possible laser codes.
  -- Each new lase will select a code from this table.
  -- The entered value can range from 1111 - 1788,
  -- -- but the first digit of the series must be a 1 or 2
  -- -- and the last three digits must be between 1 and 8.
  --  The range used to be bugged so its not 1 - 8 but 0 - 7.
  -- function below will use the range 1-7 just in case
  -- @param #DESIGNATE self
  -- @return #DESIGNATE
  function DESIGNATE:GenerateLaserCodes() --R2.1

    self.LaserCodes = {}
    
    local function containsDigit(_number, _numberToFind)

      local _thisNumber = _number
      local _thisDigit = 0
  
      while _thisNumber ~= 0 do
        _thisDigit = _thisNumber % 10
        _thisNumber = math.floor(_thisNumber / 10)
        if _thisDigit == _numberToFind then
          return true
        end
      end
  
      return false
    end

    -- generate list of laser codes
    local _code = 1111
    local _count = 1
    while _code < 1777 and _count < 30 do
      while true do
        _code = _code + 1
        if not containsDigit(_code, 8)
       and not containsDigit(_code, 9)
       and not containsDigit(_code, 0) then
          self:T(_code)            
          table.insert( self.LaserCodes, _code )
          break
        end
      end
      _count = _count + 1
    end

    self.LaserCodesUsed = {}

    return self
  end
  
  
  
  --- Set auto lase.
  -- Auto lase will start lasing targets immediately when these are in range.
  -- @param #DESIGNATE self
  -- @param #boolean AutoLase (optional) true sets autolase on, false off. Default is off.
  -- @param #boolean Message (optional) true is send message, false or nil won't send a message. Default is no message sent.
  -- @return #DESIGNATE
  function DESIGNATE:SetAutoLase( AutoLase, Message )

    self.AutoLase = AutoLase or false
    
    if Message then
      local AutoLaseOnOff = ( self.AutoLase == true ) and "On" or "Off"
      local CC = self.CC:GetPositionable()
      if CC then
        CC:MessageToSetGroup( self.DesignateName .. ": Auto Lase " .. AutoLaseOnOff .. ".", 15, self.AttackSet )
      end
    end

    self:CoordinateLase()
    self:SetDesignateMenu()      

    return self
  end

  --- Set priorization of Targets based on the **Threat Level of the Target** in an Air to Ground context.
  -- @param #DESIGNATE self
  -- @param #boolean Prioritize
  -- @return #DESIGNATE
  function DESIGNATE:SetThreatLevelPrioritization( Prioritize ) --R2.1

    self.ThreatLevelPrioritization = Prioritize
    
    return self
  end
  
  --- Set the MISSION object for which designate will function.
  -- When a MISSION object is assigned, the menu for the designation will be located at the Mission Menu.
  -- @param #DESIGNATE self
  -- @param Tasking.Mission#MISSION Mission The MISSION object.
  -- @return #DESIGNATE
  function DESIGNATE:SetMission( Mission ) --R2.2

    self.Mission = Mission

    return self
  end
  

  --- 
  -- @param #DESIGNATE self
  -- @return #DESIGNATE
  function DESIGNATE:onafterDetect()
    
    self:__Detect( -math.random( 60 ) )
    
    self:DesignationScope()
    self:CoordinateLase()
    self:SendStatus()
    self:SetDesignateMenu()      
  
    return self
  end


  --- Adapt the designation scope according the detected items.
  -- @param #DESIGNATE self
  -- @return #DESIGNATE
  function DESIGNATE:DesignationScope()

    local DetectedItems = self.Detection:GetDetectedItemsByIndex()
    
    local DetectedItemCount = 0
    
    for DesignateIndex, Designating in pairs( self.Designating ) do
      local DetectedItem = self.Detection:GetDetectedItemByIndex( DesignateIndex )
      if DetectedItem then
        -- Check LOS...
        local IsDetected = self.Detection:IsDetectedItemDetected( DetectedItem )
        self:F({IsDetected = IsDetected })
        if IsDetected == false then
          self:F("Removing")
          -- This Detection is obsolete, remove from the designate scope
          self.Designating[DesignateIndex] = nil
          self.AttackSet:ForEachGroupAlive(
            -- @param Wrapper.Group#GROUP AttackGroup
            function( AttackGroup )
              if AttackGroup:IsAlive() == true then
                local DetectionText = self.Detection:DetectedItemReportSummary( DetectedItem, AttackGroup ):Text( ", " )
                self.CC:GetPositionable():MessageToGroup( "Targets out of LOS\n" .. DetectionText, 10, AttackGroup, self.DesignateName )
              end
            end
          )
        else
          DetectedItemCount = DetectedItemCount + 1
        end
      else
        -- This Detection is obsolete, remove from the designate scope
        self.Designating[DesignateIndex] = nil
      end
    end
    
    if DetectedItemCount < 5 then
      for DesignateIndex, DetectedItem in pairs( DetectedItems ) do
        local IsDetected = self.Detection:IsDetectedItemDetected( DetectedItem )
        if IsDetected == true then
          self:F( { DistanceRecce = DetectedItem.DistanceRecce } )
          if DetectedItem.DistanceRecce <= self.MaximumDistanceDesignations then
            if self.Designating[DesignateIndex] == nil then
              -- ok, we added one item to the designate scope.
              self.AttackSet:ForEachGroupAlive(
                function( AttackGroup )
                  if self.FlashDetectionMessage[AttackGroup] == true then
                    local DetectionText = self.Detection:DetectedItemReportSummary( DetectedItem, AttackGroup ):Text( ", " )
                    self.CC:GetPositionable():MessageToGroup( "Targets detected at \n" .. DetectionText, 10, AttackGroup, self.DesignateName )
                  end
                end
              )
              self.Designating[DesignateIndex] = ""
              
              -- When we found an item for designation, we stop the loop.
              -- So each iteration over the detected items, a new detected item will be selected to be designated.
              -- Until all detected items were found or until there are about 5 designations allocated.
              break
            end
          end
        end
      end
    end
    
    return self
  end

  --- Coordinates the Auto Lase.
  -- @param #DESIGNATE self
  -- @return #DESIGNATE
  function DESIGNATE:CoordinateLase()

    local DetectedItems = self.Detection:GetDetectedItemsByIndex()
    
    for DesignateIndex, Designating in pairs( self.Designating ) do
      local DetectedItem = DetectedItems[DesignateIndex]
      if DetectedItem then
        if self.AutoLase then
          self:LaseOn( DesignateIndex, self.LaseDuration )
        end
      end
    end 
    
    return self
  end


  --- Sends the status to the Attack Groups.
  -- @param #DESIGNATE self
  -- @param Wrapper.Group#GROUP AttackGroup
  -- @param #number Duration The time in seconds the report should be visible.
  -- @return #DESIGNATE
  function DESIGNATE:SendStatus( MenuAttackGroup )

    self.AttackSet:ForEachGroupAlive(
    
      -- @param Wrapper.Group#GROUP GroupReport
      function( AttackGroup )
      
        if self.FlashStatusMenu[AttackGroup] or ( MenuAttackGroup and ( AttackGroup:GetName() == MenuAttackGroup:GetName() ) ) then

          local DetectedReport = REPORT:New( "Targets ready for Designation:" )
          local DetectedItems = self.Detection:GetDetectedItemsByIndex()
          
          for DesignateIndex, Designating in pairs( self.Designating ) do
            local DetectedItem = DetectedItems[DesignateIndex]
            if DetectedItem then
              local Report = self.Detection:DetectedItemReportSummary( DetectedItem, AttackGroup, nil, true ):Text( ", " )
              DetectedReport:Add( string.rep( "-", 40 ) )
              DetectedReport:Add( " - " .. Report )
              if string.find( Designating, "L" ) then
                DetectedReport:Add( " - " .. "Lasing Targets" )
              end
              if string.find( Designating, "S" ) then
                DetectedReport:Add( " - " .. "Smoking Targets" )
              end
              if string.find( Designating, "I" ) then
                DetectedReport:Add( " - " .. "Illuminating Area" )
              end
            end
          end
          
          local CC = self.CC:GetPositionable()
      
          CC:MessageTypeToGroup( DetectedReport:Text( "\n" ), MESSAGE.Type.Information, AttackGroup, self.DesignateName )
          
          local DesignationReport = REPORT:New( "Marking Targets:" )
      
          self.RecceSet:ForEachGroupAlive(
            function( RecceGroup )
              local RecceUnits = RecceGroup:GetUnits()
              for UnitID, RecceData in pairs( RecceUnits ) do
                local Recce = RecceData -- Wrapper.Unit#UNIT
                if Recce:IsLasing() then
                  DesignationReport:Add( " - " .. Recce:GetMessageText( "Marking " .. Recce:GetSpot().Target:GetTypeName() .. " with laser " .. Recce:GetSpot().LaserCode .. "." ) )
                end
              end
            end
          )
      
          CC:MessageTypeToGroup( DesignationReport:Text(), MESSAGE.Type.Information, AttackGroup, self.DesignateName )
        end
      end
    )
    
    return self
  end


  --- Sets the Designate Menu for one attack groups.
  -- @param #DESIGNATE self
  -- @return #DESIGNATE
  function DESIGNATE:SetMenu( AttackGroup )

    self.MenuDesignate = self.MenuDesignate or {}
    
    local MissionMenu = nil
    
    if self.Mission then
      --MissionMenu = self.Mission:GetRootMenu( AttackGroup )
      MissionMenu = self.Mission:GetMenu( AttackGroup )
    end
    
    local MenuTime = timer.getTime()
    
    self.MenuDesignate[AttackGroup] = MENU_GROUP_DELAYED:New( AttackGroup, self.DesignateName, MissionMenu ):SetTime( MenuTime ):SetTag( self.DesignateName ) 
    local MenuDesignate = self.MenuDesignate[AttackGroup] -- Core.Menu#MENU_GROUP_DELAYED

    -- Set Menu option for auto lase

    if self.AutoLase then        
      MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Auto Lase Off", MenuDesignate, self.MenuAutoLase, self, false ):SetTime( MenuTime ):SetTag( self.DesignateName )
    else
      MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Auto Lase On", MenuDesignate, self.MenuAutoLase, self, true ):SetTime( MenuTime ):SetTag( self.DesignateName )
    end        

    local StatusMenu = MENU_GROUP_DELAYED:New( AttackGroup, "Status", MenuDesignate ):SetTime( MenuTime ):SetTag( self.DesignateName )
    MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Report Status", StatusMenu, self.MenuStatus, self, AttackGroup ):SetTime( MenuTime ):SetTag( self.DesignateName )
    
    if self.FlashStatusMenu[AttackGroup] then
      MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Flash Status Report Off", StatusMenu, self.MenuFlashStatus, self, AttackGroup, false ):SetTime( MenuTime ):SetTag( self.DesignateName )
    else
       MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Flash Status Report On", StatusMenu, self.MenuFlashStatus, self, AttackGroup, true ):SetTime( MenuTime ):SetTag( self.DesignateName )
    end        
  
    local DesignateCount = 0
  
    for DesignateIndex, Designating in pairs( self.Designating ) do

      local DetectedItem = self.Detection:GetDetectedItemByIndex( DesignateIndex )

      if DetectedItem then
      
        local Coord = self.Detection:GetDetectedItemCoordinate( DetectedItem )
        local ID = self.Detection:GetDetectedItemID( DetectedItem )
        local MenuText = ID --.. ", " .. Coord:ToStringA2G( AttackGroup )
        
        -- Use injected MenuName from TaskA2GDispatcher if using same Detection Object
        if DetectedItem.DesignateMenuName then
          MenuText = string.format( "(%3s) %s", Designating, DetectedItem.DesignateMenuName )
        else
          MenuText = string.format( "(%3s) %s", Designating, MenuText )
        end
        
        local DetectedMenu = MENU_GROUP_DELAYED:New( AttackGroup, MenuText, MenuDesignate ):SetTime( MenuTime ):SetTag( self.DesignateName )
        
        -- Build the Lasing menu.
        if string.find( Designating, "L", 1, true ) == nil then
          MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Search other target", DetectedMenu, self.MenuForget, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName )
          for LaserCode, MenuText in pairs( self.MenuLaserCodes ) do
            MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, string.format( MenuText, LaserCode ), DetectedMenu, self.MenuLaseCode, self, DesignateIndex, self.LaseDuration, LaserCode ):SetTime( MenuTime ):SetTag( self.DesignateName )
          end
          MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Lase with random laser code(s)", DetectedMenu, self.MenuLaseOn, self, DesignateIndex, self.LaseDuration ):SetTime( MenuTime ):SetTag( self.DesignateName )
        else
          MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Stop lasing", DetectedMenu, self.MenuLaseOff, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName )
        end
        
        -- Build the Smoking menu.
        if string.find( Designating, "S", 1, true ) == nil then
          MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke red", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Red ):SetTime( MenuTime ):SetTag( self.DesignateName )
          MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke blue", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Blue ):SetTime( MenuTime ):SetTag( self.DesignateName )
          MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke green", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Green ):SetTime( MenuTime ):SetTag( self.DesignateName )
          MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke white", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.White ):SetTime( MenuTime ):SetTag( self.DesignateName )
          MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Smoke orange", DetectedMenu, self.MenuSmoke, self, DesignateIndex, SMOKECOLOR.Orange ):SetTime( MenuTime ):SetTag( self.DesignateName )
        end            

        -- Build the Illuminate menu.
        if string.find( Designating, "I", 1, true ) == nil then
          MENU_GROUP_COMMAND_DELAYED:New( AttackGroup, "Illuminate", DetectedMenu, self.MenuIlluminate, self, DesignateIndex ):SetTime( MenuTime ):SetTag( self.DesignateName )
        end
      end
      
      DesignateCount = DesignateCount + 1
      if DesignateCount > 10 then
        break
      end
    end
    MenuDesignate:Remove( MenuTime, self.DesignateName )
    MenuDesignate:Set()
  end


  --- Sets the Designate Menu for all the attack groups.
  -- @param #DESIGNATE self
  -- @return #DESIGNATE
  function DESIGNATE:SetDesignateMenu()

    self.AttackSet:Flush( self )
    
    local Delay = 1

    self.AttackSet:ForEachGroupAlive(
    
      -- @param Wrapper.Group#GROUP GroupReport
      function( AttackGroup )
      
        self:ScheduleOnce( Delay, self.SetMenu, self, AttackGroup )
        Delay = Delay + 1
      end  
        
    )
    
    return self
  end

  --- 
  -- @param #DESIGNATE self
  function DESIGNATE:MenuStatus( AttackGroup )

    self:F("Status")

    self:SendStatus( AttackGroup )  
  end
  
  --- 
  -- @param #DESIGNATE self
  function DESIGNATE:MenuFlashStatus( AttackGroup, Flash )

    self:F("Flash Status")

    self.FlashStatusMenu[AttackGroup] = Flash
    self:SetDesignateMenu()
  end

  
  --- 
  -- @param #DESIGNATE self
  function DESIGNATE:MenuForget( Index )

    self:F("Forget")

    self.Designating[Index] = ""
    self:SetDesignateMenu()
  end

  --- 
  -- @param #DESIGNATE self
  function DESIGNATE:MenuAutoLase( AutoLase )

    self:F("AutoLase")

    self:SetAutoLase( AutoLase, true )
  end

  --- 
  -- @param #DESIGNATE self
  function DESIGNATE:MenuSmoke( Index, Color )

    self:F("Designate through Smoke")

    if string.find( self.Designating[Index], "S" ) == nil then
      self.Designating[Index] = self.Designating[Index] .. "S"
    end
    self:Smoke( Index, Color )    
    self:SetDesignateMenu()
  end

  --- 
  -- @param #DESIGNATE self
  function DESIGNATE:MenuIlluminate( Index )

    self:F("Designate through Illumination")

    if string.find( self.Designating[Index], "I", 1, true ) == nil then
      self.Designating[Index] = self.Designating[Index] .. "I"
    end
    
    self:__Illuminate( 1, Index )
    self:SetDesignateMenu()
  end

  --- 
  -- @param #DESIGNATE self
  function DESIGNATE:MenuLaseOn( Index, Duration )

    self:F("Designate through Lase")
    
    self:__LaseOn( 1, Index, Duration ) 
    self:SetDesignateMenu()
  end


  --- 
  -- @param #DESIGNATE self
  function DESIGNATE:MenuLaseCode( Index, Duration, LaserCode )

    self:F( "Designate through Lase using " .. LaserCode )
    
    self:__LaseOn( 1, Index, Duration, LaserCode ) 
    self:SetDesignateMenu()
  end


  --- 
  -- @param #DESIGNATE self
  function DESIGNATE:MenuLaseOff( Index, Duration )

    self:F("Lasing off")

    self.Designating[Index] = string.gsub( self.Designating[Index], "L", "" )
    self:__LaseOff( 1, Index ) 
    self:SetDesignateMenu()
  end

  --- 
  -- @param #DESIGNATE self
  function DESIGNATE:onafterLaseOn( From, Event, To, Index, Duration, LaserCode )
  
    if string.find( self.Designating[Index], "L", 1, true ) == nil then
      self.Designating[Index] = self.Designating[Index] .. "L"
      self.LaseStart = timer.getTime()
      self.LaseDuration = Duration
      self:Lasing( Index, Duration, LaserCode )
    end
  end
  

  --- 
  -- @param #DESIGNATE self
  -- @return #DESIGNATE
  function DESIGNATE:onafterLasing( From, Event, To, Index, Duration, LaserCodeRequested )
  
  
    local DetectedItem = self.Detection:GetDetectedItemByIndex( Index )
    local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem )

    local MarkingCount = 0
    local MarkedTypes = {}
    --local ReportTypes = REPORT:New()
    --local ReportLaserCodes = REPORT:New()
    
    --TargetSetUnit:Flush( self )

    --self:F( { Recces = self.Recces } ) 
    for TargetUnit, RecceData in pairs( self.Recces ) do
      local Recce = RecceData -- Wrapper.Unit#UNIT
      self:F( { TargetUnit = TargetUnit, Recce = Recce:GetName() } )
      if not Recce:IsLasing() then
        local LaserCode = Recce:GetLaserCode() -- (Not deleted when stopping with lasing).
        self:F( { ClearingLaserCode = LaserCode } )
        self.LaserCodesUsed[LaserCode] = nil
        self.Recces[TargetUnit] = nil
      end
    end
    
    -- If a specific lasercode is requested, we disable one active lase!
    if LaserCodeRequested then
      for TargetUnit, RecceData in pairs( self.Recces ) do -- We break after the first has been processed.
        local Recce = RecceData -- Wrapper.Unit#UNIT
        self:F( { TargetUnit = TargetUnit, Recce = Recce:GetName() } )
        if Recce:IsLasing() then
          -- When a Recce is lasing, we switch the lasing off, and clear the references to the lasing in the DESIGNATE class.
          Recce:LaseOff() -- Switch off the lasing.
          local LaserCode = Recce:GetLaserCode() -- (Not deleted when stopping with lasing).
          self:F( { ClearingLaserCode = LaserCode } )
          self.LaserCodesUsed[LaserCode] = nil
          self.Recces[TargetUnit] = nil
          break
        end
      end
    end    
    
    if TargetSetUnit == nil then return end
    
    if self.AutoLase or ( not self.AutoLase and ( self.LaseStart + Duration >= timer.getTime() ) ) then

      TargetSetUnit:ForEachUnitPerThreatLevel( 10, 0,
        -- @param Wrapper.Unit#UNIT SmokeUnit
        function( TargetUnit )
        
          self:F( { TargetUnit = TargetUnit:GetName() } )
  
          if MarkingCount < self.MaximumMarkings then
  
            if TargetUnit:IsAlive() then
    
              local Recce = self.Recces[TargetUnit]
    
              if not Recce then
    
                self:F( "Lasing..." )
                --self.RecceSet:Flush( self)

                for RecceGroupID, RecceGroup in pairs( self.RecceSet:GetSet() ) do
                  for UnitID, UnitData in pairs( RecceGroup:GetUnits() or {} ) do
    
                    local RecceUnit = UnitData -- Wrapper.Unit#UNIT
                    local RecceUnitDesc = RecceUnit:GetDesc()
                    --self:F( { RecceUnit = RecceUnit:GetName(), RecceDescription = RecceUnitDesc } )x
    
                    if RecceUnit:IsLasing() == false then
                      --self:F( { IsDetected = RecceUnit:IsDetected( TargetUnit ), IsLOS = RecceUnit:IsLOS( TargetUnit ) } )
    
                      if RecceUnit:IsDetected( TargetUnit ) and RecceUnit:IsLOS( TargetUnit ) then
    
                        local LaserCodeIndex = math.random( 1, #self.LaserCodes )
                        local LaserCode = self.LaserCodes[LaserCodeIndex]
                        --self:F( { LaserCode = LaserCode, LaserCodeUsed = self.LaserCodesUsed[LaserCode] } )
    
                        if LaserCodeRequested and LaserCodeRequested ~= LaserCode then
                          LaserCode = LaserCodeRequested
                          LaserCodeRequested = nil
                        end
    
                        if not self.LaserCodesUsed[LaserCode] then
    
                          self.LaserCodesUsed[LaserCode] = LaserCodeIndex
                          local Spot = RecceUnit:LaseUnit( TargetUnit, LaserCode, Duration )
                          local AttackSet = self.AttackSet
                          local DesignateName = self.DesignateName
                          local typename = TargetUnit:GetTypeName()
    
                          function Spot:OnAfterDestroyed( From, Event, To )
                            self.Recce:MessageToSetGroup( "Target " ..typename .. " destroyed. " .. TargetSetUnit:CountAlive() .. " targets left.", 
                                                          5, AttackSet, self.DesignateName )
                          end
    
                          self.Recces[TargetUnit] = RecceUnit
                          -- OK. We have assigned for the Recce a TargetUnit. We can exit the function.
                          MarkingCount = MarkingCount + 1
                          local TargetUnitType = TargetUnit:GetTypeName()
                          RecceUnit:MessageToSetGroup( "Marking " .. TargetUnitType .. " with laser " .. RecceUnit:GetSpot().LaserCode .. " for " .. Duration .. "s.", 
                                                     10, self.AttackSet, DesignateName )
                          if not MarkedTypes[TargetUnitType] then
                            MarkedTypes[TargetUnitType] = true
                            --ReportTypes:Add(TargetUnitType)
                          end
                          --ReportLaserCodes:Add(RecceUnit.LaserCode)
                          return
                        end
                      else
                        --RecceUnit:MessageToSetGroup( "Can't mark " .. TargetUnit:GetTypeName(), 5, self.AttackSet )
                      end
                    else
                      -- The Recce is lasing, but the Target is not detected or within LOS. So stop lasing and send a report.
    
                      if not RecceUnit:IsDetected( TargetUnit ) or not RecceUnit:IsLOS( TargetUnit ) then
    
                        local Recce = self.Recces[TargetUnit] -- Wrapper.Unit#UNIT
    
                        if Recce then
                          Recce:LaseOff()
                          Recce:MessageToSetGroup( "Target " .. TargetUnit:GetTypeName() "out of LOS. Cancelling lase!", 10, self.AttackSet, self.DesignateName )
                        end
                      else
                        --MarkingCount = MarkingCount + 1
                        local TargetUnitType = TargetUnit:GetTypeName()
                        if not MarkedTypes[TargetUnitType] then
                          MarkedTypes[TargetUnitType] = true
                          --ReportTypes:Add(TargetUnitType)
                        end
                        --ReportLaserCodes:Add(RecceUnit.LaserCode)
                      end  
                    end
                  end
                end
              else
                MarkingCount = MarkingCount + 1
                local TargetUnitType = TargetUnit:GetTypeName()
                if not MarkedTypes[TargetUnitType] then
                  MarkedTypes[TargetUnitType] = true
                  --ReportTypes:Add(TargetUnitType)
                end
                --ReportLaserCodes:Add(Recce.LaserCode)
                Recce:MessageToSetGroup( self.DesignateName .. ": Marking " .. TargetUnit:GetTypeName() .. " with laser " .. Recce.LaserCode .. ".", 10, self.AttackSet )
              end
            end
          end
        end
      )

      --local MarkedTypesText = ReportTypes:Text(', ')
      --local MarkedLaserCodesText = ReportLaserCodes:Text(', ')
      --self.CC:GetPositionable():MessageToSetGroup( "Marking " .. MarkingCount .. " x "  .. MarkedTypesText .. ", code " .. MarkedLaserCodesText .. ".", 5, self.AttackSet, self.DesignateName )
  
      self:__Lasing( -self.LaseDuration, Index, Duration, LaserCodeRequested )
      
      self:SetDesignateMenu()

    else
      self:LaseOff( Index )
    end

  end
    
  --- 
  -- @param #DESIGNATE self
  -- @return #DESIGNATE
  function DESIGNATE:onafterLaseOff( From, Event, To, Index )
  
    local CC = self.CC:GetPositionable()
    
    if CC then 
      CC:MessageToSetGroup( "Stopped lasing.", 5, self.AttackSet, self.DesignateName )
    end
    
    local DetectedItem = self.Detection:GetDetectedItemByIndex( Index )
    local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem )
    
    local Recces = self.Recces
    
    for TargetID, RecceData in pairs( Recces ) do
      local Recce = RecceData -- Wrapper.Unit#UNIT
      Recce:MessageToSetGroup( "Stopped lasing " .. Recce:GetSpot().Target:GetTypeName() .. ".", 5, self.AttackSet, self.DesignateName )
      Recce:LaseOff()
    end
    
    Recces = nil
    self.Recces = {}
    self.LaserCodesUsed = {}

    self.Designating[Index] = string.gsub( self.Designating[Index], "L", "" )
    self:SetDesignateMenu()
  end


  --- 
  -- @param #DESIGNATE self
  -- @return #DESIGNATE
  function DESIGNATE:onafterSmoke( From, Event, To, Index, Color )
  
    local DetectedItem = self.Detection:GetDetectedItemByIndex( Index )
    local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem )
    local TargetSetUnitCount = TargetSetUnit:Count()
  
    local MarkedCount = 0
  
    TargetSetUnit:ForEachUnitPerThreatLevel( 10, 0,
      -- @param Wrapper.Unit#UNIT SmokeUnit
      function( SmokeUnit )

        if MarkedCount < self.MaximumMarkings then
      
          MarkedCount = MarkedCount + 1        
      
          self:F( "Smoking ..." )

          local RecceGroup = self.RecceSet:FindNearestGroupFromPointVec2(SmokeUnit:GetPointVec2())
          local RecceUnit = RecceGroup:GetUnit( 1 ) -- Wrapper.Unit#UNIT

          if RecceUnit then

            RecceUnit:MessageToSetGroup( "Smoking " .. SmokeUnit:GetTypeName() .. ".", 5, self.AttackSet, self.DesignateName )

            if SmokeUnit:IsAlive() then
              SmokeUnit:Smoke( Color, 50, 2 )
            end
            
            self.MarkScheduler:Schedule( self,
              function()
              self:DoneSmoking( Index )
              end, {}, math.random( 180, 240 ) 
            )
          end
        end
      end
    )
    

  end

  --- Illuminating
  -- @param #DESIGNATE self
  -- @return #DESIGNATE
  function DESIGNATE:onafterIlluminate( From, Event, To, Index )
  
    local DetectedItem = self.Detection:GetDetectedItemByIndex( Index )
    local TargetSetUnit = self.Detection:GetDetectedItemSet( DetectedItem )
    local TargetUnit = TargetSetUnit:GetFirst()
  
    if TargetUnit then
      local RecceGroup = self.RecceSet:FindNearestGroupFromPointVec2(TargetUnit:GetPointVec2())
      local RecceUnit = RecceGroup:GetUnit( 1 )
      if RecceUnit then
        RecceUnit:MessageToSetGroup( "Illuminating " .. TargetUnit:GetTypeName() .. ".", 5, self.AttackSet, self.DesignateName )
        if TargetUnit:IsAlive() then
          -- Fire 2 illumination bombs at random locations.
          TargetUnit:GetPointVec3():AddY(math.random( 350, 500) ):AddX(math.random(-50,50) ):AddZ(math.random(-50,50) ):IlluminationBomb()
          TargetUnit:GetPointVec3():AddY(math.random( 350, 500) ):AddX(math.random(-50,50) ):AddZ(math.random(-50,50) ):IlluminationBomb()
        end
        self.MarkScheduler:Schedule( self,
          function()
            self:DoneIlluminating( Index )
          end, {}, math.random( 60, 90 ) 
        )
      end
    end
  end

  --- DoneSmoking
  -- @param #DESIGNATE self
  -- @return #DESIGNATE
  function DESIGNATE:onafterDoneSmoking( From, Event, To, Index )
    if self.Designating[Index] ~= nil then
      self.Designating[Index] = string.gsub( self.Designating[Index], "S", "" )
      self:SetDesignateMenu()
    end
  end

  --- DoneIlluminating
  -- @param #DESIGNATE self
  -- @return #DESIGNATE
  function DESIGNATE:onafterDoneIlluminating( From, Event, To, Index )

    self.Designating[Index] = string.gsub( self.Designating[Index], "I", "" )
    self:SetDesignateMenu()
  end

end
--- **Functional** - Create random air traffic in your missions.
--
-- ===
--
-- The aim of the RAT class is to fill the empty DCS world with randomized air traffic and bring more life to your airports.
-- In particular, it is designed to spawn AI air units at random airports. These units will be assigned a random flight path to another random airport on the map.
-- Even the mission designer will not know where aircraft will be spawned and which route they follow.
--
-- ## Features:
--
--   * Very simple interface. Just one unit and two lines of Lua code needed to fill your map.
--   * High degree of randomization. Aircraft will spawn at random airports, have random routes and random destinations.
--   * Specific departure and/or destination airports can be chosen.
--   * Departure and destination airports can be restricted by coalition.
--   * Planes and helicopters supported. Helicopters can also be send to FARPs and ships.
--   * Units can also be spawned in air within pre-defined zones of the map.
--   * Aircraft will be removed when they arrive at their destination (or get stuck on the ground).
--   * When a unit is removed a new unit with a different flight plan is respawned.
--   * Aircraft can report their status during the route.
--   * All of the above can be customized by the user if necessary.
--   * All current (Caucasus, Nevada, Normandy, Persian Gulf) and future maps are supported.
--
-- The RAT class creates an entry in the F10 radio menu which allows to:
--
--   * Create new groups on-the-fly, i.e. at run time within the mission,
--   * Destroy specific groups (e.g. if they get stuck or damaged and block a runway),
--   * Request the status of all RAT aircraft or individual groups,
--   * Place markers at waypoints on the F10 map for each group.
--
-- Note that by its very nature, this class is suited best for civil or transport aircraft. However, it also works perfectly fine for military aircraft of any kind.
--
-- More of the documentation include some simple examples can be found further down this page.
--
-- ===
--
-- ## Additional Material:
--
-- * **Demo Missions:** [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Functional/RAT)
-- * **YouTube videos:** [Random Air Traffic](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0u4Zxywtg-mx_ov4vi68CO)
-- * **Guides:** None
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ===
-- @module Functional.RAT
-- @image RAT.JPG

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- RAT class
-- @type RAT
-- @field #string ClassName Name of the Class.
-- @field #string lid Log identifier.
-- @field #boolean Debug Turn debug messages on or off.
-- @field Wrapper.Group#GROUP templategroup Group serving as template for the RAT aircraft.
-- @field #string alias Alias for spawned group.
-- @field #boolean spawninitialized If RAT:Spawn() was already called this RAT object is set to true to prevent users to call it again.
-- @field #number spawndelay Delay time in seconds before first spawning happens.
-- @field #number spawninterval Interval between spawning units/groups. Note that we add a randomization of 50%.
-- @field #number coalition Coalition of spawn group template.
-- @field #number country Country of spawn group template.
-- @field #string category Category of aircarft: "plane" or "heli".
-- @field #number groupsize Number of aircraft in group.
-- @field #string friendly Possible departure/destination airport: all=blue+red+neutral, same=spawn+neutral, spawnonly=spawn, blue=blue+neutral, blueonly=blue, red=red+neutral, redonly=red.
-- @field #table ctable Table with the valid coalitions from choice self.friendly.
-- @field #table aircraft Table which holds the basic aircraft properties (speed, range, ...).
-- @field #number Vcruisemax Max cruise speed in m/s (250 m/s = 900 km/h = 486 kt) set by user.
-- @field #number Vclimb Default climb rate in ft/min.
-- @field #number AlphaDescent Default angle of descenti in degrees. A value of 3.6 follows the 3:1 rule of 3 miles of travel and 1000 ft descent.
-- @field #string roe ROE of spawned groups, default is weapon hold (this is a peaceful class for civil aircraft or ferry missions). Possible: "hold", "return", "free".
-- @field #string rot ROT of spawned groups, default is no reaction. Possible: "noreaction", "passive", "evade".
-- @field #number takeoff Takeoff type. 0=coldorhot.
-- @field #number landing Landing type. Determines if we actually land at an airport or treat it as zone.
-- @field #number mindist Min distance from departure to destination in meters. Default 5 km.
-- @field #number maxdist Max distance from departure to destination in meters. Default 5000 km.
-- @field #table airports_map All airports available on current map (Caucasus, Nevada, Normandy, ...).
-- @field #table airports All airports of friedly coalitions.
-- @field #boolean random_departure By default a random friendly airport is chosen as departure.
-- @field #boolean random_destination By default a random friendly airport is chosen as destination.
-- @field #table departure_ports Array containing the names of the destination airports or zones.
-- @field #table destination_ports Array containing the names of the destination airports or zones.
-- @field #number Ndestination_Airports Number of destination airports set via SetDestination().
-- @field #number Ndestination_Zones Number of destination zones set via SetDestination().
-- @field #number Ndeparture_Airports Number of departure airports set via SetDeparture().
-- @field #number Ndeparture_Zones Number of departure zones set via SetDeparture.
-- @field #table excluded_ports Array containing the names of explicitly excluded airports.
-- @field #boolean destinationzone Destination is a zone and not an airport.
-- @field #table return_zones Array containing the names of the return zones.
-- @field #boolean returnzone Zone where aircraft will fly to before returning to their departure airport.
-- @field Core.Zone#ZONE departure_Azone Zone containing the departure airports.
-- @field Core.Zone#ZONE destination_Azone Zone containing the destination airports.
-- @field #boolean addfriendlydepartures Add all friendly airports to departures.
-- @field #boolean addfriendlydestinations Add all friendly airports to destinations.
-- @field #table ratcraft Array with the spawned RAT aircraft.
-- @field #number Tinactive Time in seconds after which inactive units will be destroyed. Default is 300 seconds.
-- @field #boolean reportstatus Aircraft report status.
-- @field #number statusinterval Intervall between status checks (and reports if enabled).
-- @field #boolean placemarkers Place markers of waypoints on F10 map.
-- @field #number FLcruise Cruise altitude of aircraft. Default FL200 for planes and F005 for helos.
-- @field #number FLuser Flight level set by users explicitly.
-- @field #number FLminuser Minimum flight level set by user.
-- @field #number FLmaxuser Maximum flight level set by user.
-- @field #boolean commute Aircraft commute between departure and destination, i.e. when respawned the departure airport becomes the new destiation.
-- @field #boolean starshape If true, aircraft travel A-->B-->A-->C-->A-->D... for commute.
-- @field #string homebase Home base for commute and return zone. Aircraft will always return to this base but otherwise travel in a star shaped way.
-- @field #boolean continuejourney Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination.
-- @field #number ngroups Number of groups to be spawned in total.
-- @field #number alive Number of groups which are alive.
-- @field #boolean f10menu If true, add an F10 radiomenu for RAT. Default is false.
-- @field #table Menu F10 menu items for this RAT object.
-- @field #string SubMenuName Submenu name for RAT object.
-- @field #boolean respawn_at_landing Respawn aircraft the moment they land rather than at engine shutdown.
-- @field #boolean norespawn Aircraft will not be respawned after they have finished their route.
-- @field #boolean respawn_after_takeoff Aircraft will be respawned directly after take-off.
-- @field #boolean respawn_after_crash Aircraft will be respawned after a crash, e.g. when they get shot down.
-- @field #boolean respawn_inair Aircraft are allowed to spawned in air if they cannot be respawned on ground because there is not free parking spot. Default is true.
-- @field #number respawn_delay Delay in seconds until a repawn happens.
-- @field #table markerids Array with marker IDs.
-- @field #table waypointdescriptions Table with strings for waypoint descriptions of markers.
-- @field #table waypointstatus Table with strings of waypoint status.
-- @field #string livery Livery of the aircraft set by user.
-- @field #string skill Skill of AI.
-- @field #boolean ATCswitch Enable/disable ATC if set to true/false.
-- @field #boolean radio If true/false disables radio messages from the RAT groups.
-- @field #number frequency Radio frequency used by the RAT groups.
-- @field #string modulation Ratio modulation. Either "FM" or "AM".
-- @field #boolean uncontrolled If true aircraft are spawned in uncontrolled state and will only sit on their parking spots. They can later be activated.
-- @field #boolean invisible If true aircraft are set to invisible for other AI forces.
-- @field #boolean immortal If true, aircraft are spawned as immortal.
-- @field #boolean activate_uncontrolled If true, uncontrolled are activated randomly after certain time intervals.
-- @field #number activate_delay Delay in seconds before first uncontrolled group is activated. Default is 5 seconds.
-- @field #number activate_delta Time interval in seconds between activation of uncontrolled groups. Default is 5 seconds.
-- @field #number activate_frand Randomization factor of time interval (activate_delta) between activating uncontrolled groups. Default is 0.
-- @field #number activate_max Maximum number of uncontrolled aircraft, which will be activated at the same time. Default is 1.
-- @field #string onboardnum Sets the onboard number prefix. Same as setting "TAIL #" in the mission editor.
-- @field #number onboardnum0 (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 1.
-- @field #boolean checkonrunway Aircraft are checked if they were accidentally spawned on the runway. Default is true.
-- @field #number onrunwayradius Distance (in meters) from a runway spawn point until a unit is considered to have accidentally been spawned on a runway. Default is 75 m.
-- @field #number onrunwaymaxretry Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway. Default is 3.
-- @field #boolean checkontop Aircraft are checked if they were accidentally spawned on top of another unit. Default is true.
-- @field #number ontopradius Radius in meters until which a unit is considered to be on top of another. Default is 2 m.
-- @field Wrapper.Airbase#AIRBASE.TerminalType termtype Type of terminal to be used when spawning at an airbase.
-- @field #number  parkingscanradius Radius in meters until which parking spots are scanned for obstacles like other units, statics or scenery.
-- @field #boolean parkingscanscenery If true, area around parking spots is scanned for scenery objects. Default is false.
-- @field #boolean parkingverysafe If true, parking spots are considered as non-free until a possible aircraft has left and taken off. Default false.
-- @field #boolean despawnair If true, aircraft are despawned when they reach their destination zone. Default.
-- @field #boolean eplrs If true, turn on EPLSR datalink for the RAT group.
-- @field #number NspawnMax Max number of spawns.
-- @extends Core.Spawn#SPAWN

--- Implements an easy to use way to randomly fill your map with AI aircraft.
--
-- ## Airport Selection
--
-- ![Process](..\Presentations\RAT\RAT_Airport_Selection.png)
--
-- ### Default settings:
--
-- * By default, aircraft are spawned at airports of their own coalition (blue or red) or neutral airports.
-- * Destination airports are by default also of neutral or of the same coalition as the template group of the spawned aircraft.
-- * Possible destinations are restricted by their distance to the departure airport. The maximal distance depends on the max range of spawned aircraft type and its initial fuel amount.
--
-- ### The default behavior can be changed:
--
-- * A specific departure and/or destination airport can be chosen.
-- * Valid coalitions can be set, e.g. only red, blue or neutral, all three "colours".
-- * It is possible to start in air within a zone or within a zone above an airport of the map.
--
-- ## Flight Plan
--
-- ![Process](..\Presentations\RAT\RAT_Flight_Plan.png)
--
-- * A general flight plan has five main airborne segments: Climb, cruise, descent, holding and final approach.
-- * Events monitored during the flight are: birth, engine-start, take-off, landing and engine-shutdown.
-- * The default flight level (FL) is set to ~FL200, i.e. 20000 feet ASL but randomized for each aircraft.
-- Service ceiling of aircraft type is into account for max FL as well as the distance between departure and destination.
-- * Maximal distance between destination and departure airports depends on range and initial fuel of aircraft.
-- * Climb rate is set to a moderate value of ~1500 ft/min.
-- * The standard descent rate follows the 3:1 rule, i.e. 1000 ft decent per 3 miles of travel. Hence, angle of descent is ~3.6 degrees.
-- * A holding point is randomly selected at a distance between 5 and 10 km away from destination airport.
-- * The altitude of the holding point is ~1200 m AGL. Holding patterns might or might not happen with variable duration.
-- * If an aircraft is spawned in air, the procedure omits taxi and take-off and starts with the climb/cruising part.
-- * All values are randomized for each spawned aircraft.
--
-- ## Mission Editor Setup
--
-- ![Process](..\Presentations\RAT\RAT_Mission_Setup.png)
--
-- Basic mission setup is very simple and essentially a three step process:
--
-- * Place your aircraft **anywhere** on the map. It really does not matter where you put it.
-- * Give the group a good name. In the example above the group is named "RAT_YAK".
-- * Activate the "LATE ACTIVATION" tick box. Note that this aircraft will not be spawned itself but serves a template for each RAT aircraft spawned when the mission starts.
--
-- Voilà, your already done!
--
-- Optionally, you can set a specific livery for the aircraft or give it some weapons.
-- However, the aircraft will by default not engage any enemies. Think of them as being on a peaceful or ferry mission.
--
-- ## Basic Lua Script
--
-- ![Process](..\Presentations\RAT\RAT_Basic_Lua_Script.png)
--
-- The basic Lua script for one template group consists of two simple lines as shown in the picture above.
--
-- * **Line 2** creates a new RAT object "yak". The only required parameter for the constructor @{#RAT.New}() is the name of the group as defined in the mission editor. In this example it is "RAT_YAK".
-- * **Line 5** trigger the command to spawn the aircraft. The (optional) parameter for the @{#RAT.Spawn}() function is the number of aircraft to be spawned of this object.
-- By default each of these aircraft gets a random departure airport anywhere on the map and a random destination airport, which lies within range of the of the selected aircraft type.
--
-- In this simple example aircraft are respawned with a completely new flightplan when they have reached their destination airport.
-- The "old" aircraft is despawned (destroyed) after it has shut-down its engines and a new aircraft of the same type is spawned at a random departure airport anywhere on the map.
-- Hence, the default flight plan for a RAT aircraft will be: Fly from airport A to B, get respawned at C and fly to D, get respawned at E and fly to F, ...
-- This ensures that you always have a constant number of AI aircraft on your map.
--
-- ## Parking Problems
--
-- One big issue in DCS is that not all aircraft can be spawned on every airport or airbase. In particular, bigger aircraft might not have a valid parking spot at smaller airports and
-- airstrips. This can lead to multiple problems in DCS.
--
-- * Landing: When an aircraft tries to land at an airport where it does not have a valid parking spot, it is immediately despawned the moment its wheels touch the runway, i.e.
-- when a landing event is triggered. This leads to the loss of the RAT aircraft. On possible way to circumvent the this problem is to let another RAT aircraft spawn at landing
-- and not when it shuts down its engines. See the @{#RAT.RespawnAfterLanding}() function.
-- * Spawning: When a big aircraft is dynamically spawned on a small airbase a few things can go wrong. For example, it could be spawned at a parking spot with a shelter.
-- Or it could be damaged by a scenery object when it is taxiing out to the runway, or it could overlap with other aircraft on parking spots near by.
--
-- You can check yourself if an aircraft has a valid parking spot at an airbase by dragging its group on the airport in the mission editor and set it to start from ramp.
-- If it stays at the airport, it has a valid parking spot, if it jumps to another airport, it does not have a valid parking spot on that airbase.
--
-- ### Setting the Terminal Type
-- Each parking spot has a specific type depending on its size or if a helicopter spot or a shelter etc. The classification is not perfect but it is the best we have.
-- If you encounter problems described above, you can request a specific terminal type for the RAT aircraft. This can be done by the @{#RAT.SetTerminalType}(*terminaltype*)
-- function. The parameter *terminaltype* can be set as follows
--
-- * AIRBASE.TerminalType.HelicopterOnly: Special spots for Helicopers.
-- * AIRBASE.TerminalType.Shelter: Hardened Air Shelter. Currently only on Caucaus map.
-- * AIRBASE.TerminalType.OpenMed: Open/Shelter air airplane only.
-- * AIRBASE.TerminalType.OpenBig: Open air spawn points. Generally larger but does not guarantee large aircraft are capable of spawning there.
-- * AIRBASE.TerminalType.OpenMedOrBig: Combines OpenMed and OpenBig spots.
-- * AIRBASE.TerminalType.HelicopterUsable: Combines HelicopterOnly, OpenMed and OpenBig.
-- * AIRBASE.TerminalType.FighterAircraft: Combines Shelter, OpenMed and OpenBig spots. So effectively all spots usable by fixed wing aircraft.
--
-- So for example
--      c17=RAT:New("C-17")
--      c17:SetTerminalType(AIRBASE.TerminalType.OpenBig)
--      c17:Spawn(5)
--
-- This would randomly spawn five C-17s but only on airports which have big open air parking spots. Note that also only destination airports are allowed
-- which do have this type of parking spot. This should ensure that the aircraft is able to land at the destination without being despawned immediately.
--
-- Also, the aircraft are spawned only on the requested parking spot types and not on any other type. If no parking spot of this type is available at the
-- moment of spawning, the group is automatically spawned in air above the selected airport.
--
-- ## Examples
--
-- Here are a few examples, how you can modify the default settings of RAT class objects.
--
-- ### Specify Departure and Destinations
--
-- ![Process](..\Presentations\RAT\RAT_Examples_Specify_Departure_and_Destination.png)
--
-- In the picture above you find a few possibilities how to modify the default behaviour to spawn at random airports and fly to random destinations.
--
-- In particular, you can specify fixed departure and/or destination airports. This is done via the @{#RAT.SetDeparture}() or @{#RAT.SetDestination}() functions, respectively.
--
-- * If you only fix a specific departure airport via @{#RAT.SetDeparture}() all aircraft will be spawned at that airport and get random destination airports.
-- * If you only fix the destination airport via @{#RAT.SetDestination}(), aircraft a spawned at random departure airports but will all fly to the destination airport.
-- * If you fix departure and destination airports, aircraft will only travel from between those airports.
-- When the aircraft reaches its destination, it will be respawned at its departure and fly again to its destination.
--
-- There is also an option that allows aircraft to "continue their journey" from their destination. This is achieved by the @{#RAT.ContinueJourney}() function.
-- In that case, when the aircraft arrives at its first destination it will be respawned at that very airport and get a new random destination.
-- So the flight plan in this case would be: Fly from airport A to B, then from B to C, then from C to D, ...
--
-- It is also possible to make aircraft "commute" between two airports, i.e. flying from airport A to B and then back from B to A, etc.
-- This can be done by the @{#RAT.Commute}() function. Note that if no departure or destination airports are specified, the first departure and destination are chosen randomly.
-- Then the aircraft will fly back and forth between those two airports indefinitely.
--
--
-- ### Spawn in Air
--
-- ![Process](..\Presentations\RAT\RAT_Examples_Spawn_in_Air.png)
--
-- Aircraft can also be spawned in air rather than at airports on the ground. This is done by setting @{#RAT.SetTakeoff}() to "air".
--
-- By default, aircraft are spawned randomly above airports of the map.
--
-- The @{#RAT.SetDeparture}() option can be used to specify zones, which have been defined in the mission editor as departure zones.
-- Aircraft will then be spawned at a random point within the zone or zones.
--
-- Note that @{#RAT.SetDeparture}() also accepts airport names. For an air takeoff these are treated like zones with a radius of XX kilometers.
-- Again, aircraft are spawned at random points within these zones around the airport.
--
-- ### Misc Options
--
-- ![Process](..\Presentations\RAT\RAT_Examples_Misc.png)
--
-- The default "takeoff" type of RAT aircraft is that they are spawned with hot or cold engines.
-- The choice is random, so 50% of aircraft will be spawned with hot engines while the other 50% will be spawned with cold engines.
-- This setting can be changed using the @{#RAT.SetTakeoff}() function. The possible parameters for starting on ground are:
--
-- *  @{#RAT.SetTakeoff}("cold"), which means that all aircraft are spawned with their engines off,
-- *  @{#RAT.SetTakeoff}("hot"), which means that all aircraft are spawned with their engines on,
-- *  @{#RAT.SetTakeoff}("runway"), which means that all aircraft are spawned already at the runway ready to takeoff.
-- Note that in this case the default spawn intervall is set to 180 seconds in order to avoid aircraft jams on the runway. Generally, this takeoff at runways should be used with care and problems are to be expected.
--
--
-- The options @{#RAT.SetMinDistance}() and @{#RAT.SetMaxDistance}() can be used to restrict the range from departure to destination. For example
--
-- * @{#RAT.SetMinDistance}(100) will cause only random destination airports to be selected which are **at least** 100 km away from the departure airport.
-- * @{#RAT.SetMaxDistance}(150) will allow only destination airports which are **less than** 150 km away from the departure airport.
--
-- ![Process](..\Presentations\RAT\RAT_Gaussian.png)
--
-- By default planes get a cruise altitude of ~20,000 ft ASL. The actual altitude is sampled from a Gaussian distribution. The picture shows this distribution
-- if one would spawn 1000 planes. As can be seen most planes get a cruising alt of around FL200. Other values are possible but less likely the further away
-- one gets from the expectation value.
--
-- The expectation value, i.e. the altitude most aircraft get, can be set with the function @{#RAT.SetFLcruise}().
-- It is possible to restrict the minimum cruise altitude by @{#RAT.SetFLmin}() and the maximum cruise altitude by @{#RAT.SetFLmax}()
--
-- The cruise altitude can also be given in meters ASL by the functions @{#RAT.SetCruiseAltitude}(), @{#RAT.SetMinCruiseAltitude}() and @{#RAT.SetMaxCruiseAltitude}().
--
-- For example:
--
-- * @{#RAT.SetFLcruise}(300) will cause most planes fly around FL300.
-- * @{#RAT.SetFLmin}(100) restricts the cruising alt such that no plane will fly below FL100. Note that this automatically changes the minimum distance from departure to destination.
-- That means that only destinations are possible for which the aircraft has had enough time to reach that flight level and descent again.
-- * @{#RAT.SetFLmax}(200) will restrict the cruise alt to maximum FL200, i.e. no aircraft will travel above this height.
--
--
-- @field #RAT
RAT={
  ClassName = "RAT",        -- Name of class: RAT = Random Air Traffic.
  Debug=false,              -- Turn debug messages on or off.
  templategroup=nil,        -- Template group for the RAT aircraft.
  alias=nil,                -- Alias for spawned group.
  spawninitialized=false,   -- If RAT:Spawn() was already called this is set to true to prevent users to call it again.
  spawndelay=5,             -- Delay time in seconds before first spawning happens.
  spawninterval=5,          -- Interval between spawning units/groups. Note that we add a randomization of 50%.
  coalition = nil,          -- Coalition of spawn group template.
  country = nil,            -- Country of the group template.
  category = nil,           -- Category of aircarft: "plane" or "heli".
  groupsize=nil,            -- Number of aircraft in the group.
  friendly = "same",        -- Possible departure/destination airport: same=spawn+neutral, spawnonly=spawn, blue=blue+neutral, blueonly=blue, red=red+neutral, redonly=red, neutral.
  ctable = {},              -- Table with the valid coalitions from choice self.friendly.
  aircraft = {},            -- Table which holds the basic aircraft properties (speed, range, ...).
  Vcruisemax=nil,           -- Max cruise speed in set by user.
  Vclimb=1500,              -- Default climb rate in ft/min.
  AlphaDescent=3.6,         -- Default angle of descenti in degrees. A value of 3.6 follows the 3:1 rule of 3 miles of travel and 1000 ft descent.
  roe = "hold",             -- ROE of spawned groups, default is weapon hold (this is a peaceful class for civil aircraft or ferry missions). Possible: "hold", "return", "free".
  rot = "noreaction",       -- ROT of spawned groups, default is no reaction. Possible: "noreaction", "passive", "evade".
  takeoff = 0,              -- Takeoff type. 0=coldorhot.
  landing = 9,              -- Landing type. 9=landing.
  mindist = 5000,           -- Min distance from departure to destination in meters. Default 5 km.
  maxdist = 5000000,        -- Max distance from departure to destination in meters. Default 5000 km.
  airports_map={},          -- All airports available on current map (Caucasus, Nevada, Normandy, ...).
  airports={},              -- All airports of friedly coalitions.
  random_departure=true,    -- By default a random friendly airport is chosen as departure.
  random_destination=true,  -- By default a random friendly airport is chosen as destination.
  departure_ports={},       -- Array containing the names of the departure airports or zones.
  destination_ports={},     -- Array containing the names of the destination airports or zones.
  Ndestination_Airports=0,  -- Number of destination airports set via SetDestination().
  Ndestination_Zones=0,     -- Number of destination zones set via SetDestination().
  Ndeparture_Airports=0,    -- Number of departure airports set via SetDeparture().
  Ndeparture_Zones=0,       -- Number of departure zones set via SetDeparture.
  destinationzone=false,    -- Destination is a zone and not an airport.
  return_zones={},          -- Array containing the names of return zones.
  returnzone=false,         -- Aircraft will fly to a zone and back.
  excluded_ports={},        -- Array containing the names of explicitly excluded airports.
  departure_Azone=nil,      -- Zone containing the departure airports.
  destination_Azone=nil,    -- Zone containing the destination airports.
  addfriendlydepartures=false,   -- Add all friendly airports to departures.
  addfriendlydestinations=false, -- Add all friendly airports to destinations.
  ratcraft={},              -- Array with the spawned RAT aircraft.
  Tinactive=600,            -- Time in seconds after which inactive units will be destroyed. Default is 600 seconds.
  reportstatus=false,       -- Aircraft report status.
  statusinterval=30,        -- Intervall between status checks (and reports if enabled).
  placemarkers=false,       -- Place markers of waypoints on F10 map.
  FLcruise=nil,             -- Cruise altitude of aircraft. Default FL200 for planes and F005 for helos.
  FLminuser=nil,            -- Minimum flight level set by user.
  FLmaxuser=nil,            -- Maximum flight level set by user.
  FLuser=nil,               -- Flight level set by users explicitly.
  commute=false,            -- Aircraft commute between departure and destination, i.e. when respawned the departure airport becomes the new destiation.
  starshape=false,          -- If true, aircraft travel A-->B-->A-->C-->A-->D... for commute.
  homebase=nil,             -- Home base for commute.
  continuejourney=false,    -- Aircraft will continue their journey, i.e. get respawned at their destination with a new random destination.
  alive=0,                  -- Number of groups which are alive.
  ngroups=nil,              -- Number of groups to be spawned in total.
  f10menu=false,            -- Add an F10 menu for RAT.
  Menu={},                  -- F10 menu items for this RAT object.
  SubMenuName=nil,          -- Submenu name for RAT object.
  respawn_at_landing=false, -- Respawn aircraft the moment they land rather than at engine shutdown.
  norespawn=false,          -- Aircraft will not get respawned.
  respawn_after_takeoff=false, -- Aircraft will be respawned directly after takeoff.
  respawn_after_crash=true, -- Aircraft will be respawned after a crash.
  respawn_inair=true,        -- Aircraft are spawned in air if there is no free parking spot on the ground.
  respawn_delay=0,        -- Delay in seconds until repawn happens after landing.
  markerids={},             -- Array with marker IDs.
  waypointdescriptions={},  -- Array with descriptions for waypoint markers.
  waypointstatus={},        -- Array with status info on waypoints.
  livery=nil,               -- Livery of the aircraft.
  skill="High",             -- Skill of AI.
  ATCswitch=true,           -- Enable ATC.
  radio=nil,                -- If true/false disables radio messages from the RAT groups.
  frequency=nil,            -- Radio frequency used by the RAT groups.
  modulation=nil,           -- Ratio modulation. Either "FM" or "AM".
  actype=nil,               -- Aircraft type set by user. Changes the type of the template group.
  uncontrolled=false,       -- Spawn uncontrolled aircraft.
  invisible=false,          -- Spawn aircraft as invisible.
  immortal=false,           -- Spawn aircraft as indestructible.
  activate_uncontrolled=false, -- Activate uncontrolled aircraft (randomly).
  activate_delay=5,         -- Delay in seconds before first uncontrolled group is activated.
  activate_delta=5,         -- Time interval in seconds between activation of uncontrolled groups.
  activate_frand=0,         -- Randomization factor of time interval (activate_delta) between activating uncontrolled groups.
  activate_max=1,           -- Max number of uncontrolle aircraft, which will be activated at a time.
  onboardnum=nil,           -- Tail number.
  onboardnum0=1,            -- (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is one.
  checkonrunway=true,       -- Check whether aircraft have been spawned on the runway.
  onrunwayradius=75,        -- Distance from a runway spawn point until a unit is considered to have accidentally been spawned on a runway.
  onrunwaymaxretry=3,       -- Number of respawn retries (on ground) at other airports if a group gets accidentally spawned on the runway.
  checkontop=false,         -- Check whether aircraft have been spawned on top of another unit.
  ontopradius=2,            -- Radius in meters until which a unit is considered to be on top of another.
  termtype=nil,             -- Terminal type.
  parkingscanradius=40,     -- Scan radius.
  parkingscanscenery=false, -- Scan parking spots for scenery obstacles.
  parkingverysafe=false,    -- Very safe option.
  despawnair=true,
  eplrs=false,
}

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Categories of the RAT class.
-- @type RAT.cat
-- @field #string plane Plane.
-- @field #string heli Heli.
RAT.cat={
  plane="plane",
  heli="heli",
}

--- RAT waypoint type.
-- @type RAT.wp
RAT.wp={
  coldorhot=0,
  air=1,
  runway=2,
  hot=3,
  cold=4,
  climb=5,
  cruise=6,
  descent=7,
  holding=8,
  landing=9,
  finalwp=10,
}

--- RAT aircraft status.
-- @type RAT.status
RAT.status={
  -- Waypoint states.
  Departure="At departure point",
  Climb="Climbing",
  Cruise="Cruising",
  Uturn="Flying back home",
  Descent="Descending",
  DescentHolding="Descend to holding point",
  Holding="Holding",
  Destination="Arrived at destination",
  -- Spawn states.
  Uncontrolled="Uncontrolled",
  Spawned="Spawned",
  -- Event states.
  EventBirthAir="Born in air",
  EventBirth="Ready and starting engines",
  EventEngineStartAir="On journey", -- Started engines (in air)
  EventEngineStart="Started engines and taxiing",
  EventTakeoff="Airborne after take-off",
  EventLand="Landed and taxiing",
  EventEngineShutdown="Engines off",
  EventDead="Dead",
  EventCrash="Crashed",
}

--- Datastructure of a spawned RAT group.
-- @type RAT.RatCraft
-- @field #number index Spawn index.
-- @field Wrapper.Group#Group group The aircraft group.
-- @field Ops.FlightGroup#FLIGHTGROUP flightgroup The flight group.
-- @field Wrapper.Airbase#AIRBASE destination Destination of this group. Can also be a ZONE.
-- @field Wrapper.Airbase#AIRBASE departure Departure place of this group. Can also be a ZONE.
-- @field #table waypoints Waypoints.
-- @field #boolean airborne Whether this group is airborne.
-- @field #number nunits Number of units.
-- @field Core.Point#COORDINATE Pnow Current position.
-- @field #number Distance Distance travelled in meters.
-- @field #number takeoff Takeoff type.
-- @field #number landing Laning type.
-- @field #table wpdesc Waypoint descriptins.
-- @field #table wpstatus Waypoint status.
-- @field #boolean active Whether the group is active or uncontrolled.
-- @field #string status Status of the group.
-- @field #string livery Livery of the group.
-- @field #boolean despawnme Despawn group if `true` in the next status update.
-- @field #number nrespawn Number of respawns.

--- RAT friendly coalitions.
-- @type RAT.coal
RAT.coal={
  same="same",
  sameonly="sameonly",
  neutral="neutral",
}

--- RAT unit conversions.
-- @type RAT.unit
RAT.unit={
  ft2meter=0.305,
  kmh2ms=0.278,
  FL2m=30.48,
  nm2km=1.852,
  nm2m=1852,
}

--- RAT rules of engagement.
-- @type RAT.ROE
RAT.ROE={
  weaponhold="hold",
  weaponfree="free",
  returnfire="return",
}

--- RAT reaction to threat.
-- @type RAT.ROT
RAT.ROT={
  evade="evade",
  passive="passive",
  noreaction="noreaction",
}

--- RAT ATC.
-- @type RAT.ATC
-- @field #boolean init True if ATC was initialized.
-- @field #table flight List of flights.
-- @field #table airport List of airports.
-- @field #number unregistered Enumerator for unregistered flights unregistered=-1.
-- @field #number Nclearance Number of flights that get landing clearance simultaniously. Default 2.
-- @field #number delay Delay between landing flights in seconds. Default 240 sec.
-- @field #boolean messages If `true`, ATC sends messages.
-- @field #number T0 Time stamp [sec, timer.getTime()] when ATC was initialized.
-- @field #number onfinal Enumerator onfinal=100.
RAT.ATC={
  init=false,
  flight={},
  airport={},
  unregistered=-1,
  onfinal=-100,
  Nclearance=2,
  delay=240,
  messages=true,
}

--- Running number of placed markers on the F10 map.
-- @field #number markerid
RAT.markerid=0

--- Main F10 menu.
-- @field #string MenuF10
RAT.MenuF10=nil

--- Some ID to identify who we are in output of the DCS.log file.
-- @field #string id
RAT.id="RAT | "

--- RAT version.
-- @list version
RAT.version={
  version = "3.0.0",
  print = true,
}

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--TODO list:
--TODO: Add unlimited fuel option (and disable range check). This also needs to be added to FLIGHTGROUP
--TODO: 
--TODO: Add max number of spawns
--TODO: Add Stop function
--TODO: Integrate FLIGHTGROUP
--DONE: Add scheduled spawn.
--DONE: Add possibility to spawn in air.
--DONE: Add departure zones for air start.
--DONE: Make more functions to adjust/set RAT parameters.
--DONE: Clean up debug messages.
--DONE: Improve flight plan. Especially check FL against route length.
--DONE: Add event handlers.
--DONE: Respawn units when they have landed.
--DONE: Change ROE state.
--DONE: Make ROE state user function
--DONE: Improve status reports.
--DONE: Check compatibility with other #SPAWN functions. nope, not all!
--DONE: Add possibility to continue journey at destination. Need "place" in event data for that.
--DONE: Add enumerators and get rid off error prone string comparisons.
--DONE: Check that FARPS are not used as airbases for planes.
--DONE: Add special cases for ships (similar to FARPs).
--DONE: Add cases for helicopters.
--DONE: Add F10 menu.
--DONE: Add markers to F10 menu.
--DONE: Add respawn limit. Later...
--DONE: Make takeoff method random between cold and hot start.
--DONE: Check out uncontrolled spawning. Not now!
--DONE: Check aircraft spawning in air at Sochi after third aircraft was spawned. ==> DCS behaviour.
--DONE: Improve despawn after stationary. Might lead to despawning if many aircraft spawn at the same time.
--DONE: Check why birth event is not handled. ==> Seems to be okay if it is called _OnBirth rather than _OnBirthday. Dont know why actually!?
--DONE: Improve behaviour when no destination or departure airports were found. Leads to crash, e.g. 1184: attempt to get length of local 'destinations' (a nil value)
--DONE: Check cases where aircraft get shot down.
--DONE: Handle the case where more than 10 RAT objects are spawned. Likewise, more than 10 groups of one object. Causes problems with the number of menu items! ==> not now!
--DONE: Add custom livery choice if possible.
--DONE: Add function to include all airports to selected destinations/departures.
--DONE: Find way to respawn aircraft at same position where the last was despawned for commute and journey.
--TODO: Check that same alias is not given twice. Need to store previous ones and compare.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor New
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new RAT object.
-- @param #RAT self
-- @param #string groupname Name of the group as defined in the mission editor. This group is serving as a template for all spawned units.
-- @param #string alias (Optional) Alias of the group. This is and optional parameter but must(!) be used if the same template group is used for more than one RAT object.
-- @return #RAT Object of RAT class or nil if the group does not exist in the mission editor.
-- @usage yak1:RAT("RAT_YAK") will create a RAT object called "yak1". The template group in the mission editor must have the name "RAT_YAK".
-- @usage yak2:RAT("RAT_YAK", "Yak2") will create a RAT object "yak2". The template group in the mission editor must have the name "RAT_YAK" but the group will be called "Yak2" in e.g. the F10 menu.
function RAT:New(groupname, alias)

  -- Inherit SPAWN class.
  self=BASE:Inherit(self, SPAWN:NewWithAlias(groupname, alias)) -- #RAT
  
  -- Log id.
  self.lid=string.format("RAT %s | ", alias or groupname)
  
  -- Version info.
  if RAT.version.print then
    env.info(self.lid.."Version "..RAT.version.version)
    RAT.version.print=false
  end  

  -- Welcome message.
  self:F(self.lid..string.format("Creating new RAT object from template: %s.", groupname))

  -- Set alias.
  alias=alias or groupname

  -- Alias of groupname.
  self.alias=alias

  -- Get template group defined in the mission editor.
  local DCSgroup=Group.getByName(groupname)

  -- Check the group actually exists.
  if DCSgroup==nil then
    self:E(self.lid..string.format("ERROR: Group with name %s does not exist in the mission editor!", groupname))
    return nil
  end

  -- Store template group.
  self.templategroup=GROUP:FindByName(groupname)

  -- Get number of aircraft in group.
  self.groupsize=self.templategroup:GetSize()

  -- Set own coalition.
  self.coalition=DCSgroup:getCoalition()

  -- Initialize aircraft parameters based on ME group template.
  self:_InitAircraft(DCSgroup)

  -- Get all airports of current map (Caucasus, NTTR, Normandy, ...).
  self:_GetAirportsOfMap()

  return self
end

--- Stop RAT spawning by unhandling events, stoping schedulers etc.
-- @param #RAT self
-- @param #number delay Delay before stop in seconds.
function RAT:Stop(delay)
  self:T3(self.lid..string.format("Stopping RAT! Delay %s sec!", tostring(delay)))

  if delay and delay>0 then
    self:T2(self.lid..string.format("Stopping RAT in %d sec!", delay))
    self:ScheduleOnce(delay, RAT.Stop, self)
  else
  
    self:T(self.lid.."Stopping RAT: Clearing schedulers and unhandling events!")
  
    if self.sid_Activate then
      self.Scheduler:ScheduleStop(self.sid_Activate)
    end
    
    if self.sid_Spawn then
      self.Scheduler:ScheduleStop(self.sid_Spawn)
    end
    
    if self.sid_Status then
      self.Scheduler:ScheduleStop(self.sid_Status)
    end
  
  
    if self.Scheduler then
      self.Scheduler:Clear()
    end
    
    self.norespawn=true    
    
    -- Un-Handle events.
    self:UnHandleEvent(EVENTS.Birth)
    self:UnHandleEvent(EVENTS.EngineStartup)
    self:UnHandleEvent(EVENTS.Takeoff)
    self:UnHandleEvent(EVENTS.Land)
    self:UnHandleEvent(EVENTS.EngineShutdown)
    self:UnHandleEvent(EVENTS.Dead)
    self:UnHandleEvent(EVENTS.Crash)
    self:UnHandleEvent(EVENTS.Hit)    
  
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Spawn function
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Triggers the spawning of AI aircraft. Note that all additional options should be set before giving the spawn command.
-- @param #RAT self
-- @param #number naircraft (Optional) Number of aircraft to spawn. Default is one aircraft.
-- @return #boolean True if spawning was successful or nil if nothing was spawned.
-- @usage yak:Spawn(5) will spawn five aircraft. By default aircraft will spawn at neutral and red airports if the template group is part of the red coalition.
function RAT:Spawn(naircraft)

  -- Make sure that this function is only been called once per RAT object.
  if self.spawninitialized==true then
    self:E("ERROR: Spawn function should only be called once per RAT object! Exiting and returning nil.")
    return nil
  else
    self.spawninitialized=true
  end

  -- Number of aircraft to spawn. Default is one.
  self.ngroups=naircraft or 1

  -- Init RAT ATC if not already done.
  if self.ATCswitch and not RAT.ATC.init then
    RAT._ATCInit(self.airports_map)
  end

  -- Create F10 main menu if it does not exists yet.
  if self.f10menu and not RAT.MenuF10 then
    RAT.MenuF10 = MENU_MISSION:New("RAT")
  end

    -- Set the coalition table based on choice of self.coalition and self.friendly.
  self:_SetCoalitionTable()

  -- Get all airports of this map belonging to friendly coalition(s).
  self:_GetAirportsOfCoalition()

  -- Set sub-menu name if it has not been set by user.
  if not self.SubMenuName then
    self.SubMenuName=self.alias
  end

  -- Get all departure airports inside a Moose zone.
  if self.departure_Azone~=nil then
    self.departure_ports=self:_GetAirportsInZone(self.departure_Azone)
  end

  -- Get all destination airports inside a Moose zone.
  if self.destination_Azone~=nil then
    self.destination_ports=self:_GetAirportsInZone(self.destination_Azone)
  end

  -- Add all friendly airports to possible departures/destinations
  if self.addfriendlydepartures then
    self:_AddFriendlyAirports(self.departure_ports)
  end
  if self.addfriendlydestinations then
    self:_AddFriendlyAirports(self.destination_ports)
  end

  -- Setting and possibly correction min/max/cruise flight levels.
  if self.FLcruise==nil then
    -- Default flight level (ASL).
    if self.category==RAT.cat.plane then
      -- For planes: FL200 = 20000 ft = 6096 m.
      self.FLcruise=200*RAT.unit.FL2m
    else
      -- For helos: FL005 = 500 ft = 152 m.
      self.FLcruise=005*RAT.unit.FL2m
    end
  end

  -- Enable helos to go to destinations 100 meters away.
  if self.category==RAT.cat.heli then
    self.mindist=50
  end

  -- Run consistency checks.
  self:_CheckConsistency()

  -- Settings info
  local text=string.format("\n******************************************************\n")
  text=text..string.format("Spawning %i aircraft from template %s of type %s.\n", self.ngroups, self.SpawnTemplatePrefix, self.aircraft.type)
  text=text..string.format("Alias: %s\n", self.alias)
  text=text..string.format("Category: %s\n", self.category)
  text=text..string.format("Friendly coalitions: %s\n", self.friendly)
  text=text..string.format("Number of airports on map  : %i\n", #self.airports_map)
  text=text..string.format("Number of friendly airports: %i\n", #self.airports)
  text=text..string.format("Totally random departure: %s\n", tostring(self.random_departure))
  if not self.random_departure then
    text=text..string.format("Number of departure airports: %d\n", self.Ndeparture_Airports)
    text=text..string.format("Number of departure zones   : %d\n", self.Ndeparture_Zones)
  end
  text=text..string.format("Totally random destination: %s\n", tostring(self.random_destination))
  if not self.random_destination then
    text=text..string.format("Number of destination airports: %d\n", self.Ndestination_Airports)
    text=text..string.format("Number of destination zones   : %d\n", self.Ndestination_Zones)
  end
  text=text..string.format("Min dist to destination: %4.1f\n", self.mindist)
  text=text..string.format("Max dist to destination: %4.1f\n", self.maxdist)
  text=text..string.format("Terminal type: %s\n", tostring(self.termtype))
  text=text..string.format("Takeoff type: %i\n", self.takeoff)
  text=text..string.format("Landing type: %i\n", self.landing)
  text=text..string.format("Commute: %s\n", tostring(self.commute))
  text=text..string.format("Journey: %s\n", tostring(self.continuejourney))
  text=text..string.format("Destination Zone: %s\n", tostring(self.destinationzone))
  text=text..string.format("Return Zone: %s\n", tostring(self.returnzone))
  text=text..string.format("Spawn delay: %4.1f\n", self.spawndelay)
  text=text..string.format("Spawn interval: %4.1f\n", self.spawninterval)
  text=text..string.format("Respawn delay: %s\n", tostring(self.respawn_delay))
  text=text..string.format("Respawn off: %s\n", tostring(self.norespawn))
  text=text..string.format("Respawn after landing: %s\n", tostring(self.respawn_at_landing))
  text=text..string.format("Respawn after take-off: %s\n", tostring(self.respawn_after_takeoff))
  text=text..string.format("Respawn after crash: %s\n", tostring(self.respawn_after_crash))
  text=text..string.format("Respawn in air: %s\n", tostring(self.respawn_inair))
  text=text..string.format("ROE: %s\n", tostring(self.roe))
  text=text..string.format("ROT: %s\n", tostring(self.rot))
  text=text..string.format("Immortal: %s\n", tostring(self.immortal))
  text=text..string.format("Invisible: %s\n", tostring(self.invisible))
  text=text..string.format("Vclimb: %4.1f\n", self.Vclimb)
  text=text..string.format("AlphaDescent: %4.2f\n", self.AlphaDescent)
  text=text..string.format("Vcruisemax: %s\n", tostring(self.Vcruisemax))
  text=text..string.format("FLcruise =  %6.1f km = FL%3.0f\n", self.FLcruise/1000, self.FLcruise/RAT.unit.FL2m)
  text=text..string.format("FLuser: %s\n", tostring(self.Fluser))
  text=text..string.format("FLminuser: %s\n", tostring(self.FLminuser))
  text=text..string.format("FLmaxuser: %s\n", tostring(self.FLmaxuser))
  text=text..string.format("Place markers: %s\n", tostring(self.placemarkers))
  text=text..string.format("Report status: %s\n", tostring(self.reportstatus))
  text=text..string.format("Status interval: %4.1f\n", self.statusinterval)
  text=text..string.format("Time inactive: %4.1f\n", self.Tinactive)
  text=text..string.format("Create F10 menu : %s\n", tostring(self.f10menu))
  text=text..string.format("F10 submenu name: %s\n", self.SubMenuName)
  text=text..string.format("ATC enabled : %s\n", tostring(self.ATCswitch))
  text=text..string.format("Radio comms      : %s\n", tostring(self.radio))
  text=text..string.format("Radio frequency  : %s\n", tostring(self.frequency))
  text=text..string.format("Radio modulation : %s\n", tostring(self.frequency))
  text=text..string.format("Tail # prefix    : %s\n", tostring(self.onboardnum))
  text=text..string.format("Check on runway: %s\n", tostring(self.checkonrunway))
  text=text..string.format("Max respawn attempts: %s\n", tostring(self.onrunwaymaxretry))
  text=text..string.format("Check on top: %s\n", tostring(self.checkontop))
  text=text..string.format("Uncontrolled: %s\n", tostring(self.uncontrolled))
  if self.uncontrolled and self.activate_uncontrolled then
    text=text..string.format("Uncontrolled max  : %4.1f\n", self.activate_max)
    text=text..string.format("Uncontrolled delay: %4.1f\n", self.activate_delay)
    text=text..string.format("Uncontrolled delta: %4.1f\n", self.activate_delta)
    text=text..string.format("Uncontrolled frand: %4.1f\n", self.activate_frand)
  end
  if self.livery then
    text=text..string.format("Available liveries:\n")
    for _,livery in pairs(self.livery) do
      text=text..string.format("- %s\n", livery)
    end
  end
  text=text..string.format("******************************************************\n")
  self:T(self.lid..text)

  -- Create submenus.
  if self.f10menu then
    self.Menu[self.SubMenuName]=MENU_MISSION:New(self.SubMenuName, RAT.MenuF10)
    self.Menu[self.SubMenuName]["groups"]=MENU_MISSION:New("Groups", self.Menu[self.SubMenuName])
    MENU_MISSION_COMMAND:New("Spawn new group", self.Menu[self.SubMenuName], self._SpawnWithRoute, self)
    MENU_MISSION_COMMAND:New("Delete markers", self.Menu[self.SubMenuName], self._DeleteMarkers, self)
    MENU_MISSION_COMMAND:New("Status report", self.Menu[self.SubMenuName], self.Status, self, true)
  end

  -- Schedule spawning of aircraft.
  local Tstart=self.spawndelay
  local dt=self.spawninterval
  -- Ensure that interval is >= 180 seconds if spawn at runway is chosen. Aircraft need time to takeoff or the runway gets jammed.
  if self.takeoff==RAT.wp.runway and not self.random_departure then
    dt=math.max(dt, 180)
  end
  local Tstop=Tstart+dt*(self.ngroups-1)

  -- Status check and report scheduler.
  self.sid_Status=self:ScheduleRepeat(Tstart+1, self.statusinterval, nil, nil, RAT.Status, self)
  

  -- Handle events.
  self:HandleEvent(EVENTS.Birth,          self._OnBirth)
  self:HandleEvent(EVENTS.EngineStartup,  self._OnEngineStartup)
  self:HandleEvent(EVENTS.Takeoff,        self._OnTakeoff)
  self:HandleEvent(EVENTS.Land,           self._OnLand)
  self:HandleEvent(EVENTS.EngineShutdown, self._OnEngineShutdown)
  self:HandleEvent(EVENTS.Dead,           self._OnDeadOrCrash)
  self:HandleEvent(EVENTS.Crash,          self._OnDeadOrCrash)
  self:HandleEvent(EVENTS.Hit,            self._OnHit)

  -- No groups should be spawned.
  if self.ngroups==0 then
    return nil
  end

  -- Start scheduled spawning.
  self.sid_Spawn=self:ScheduleRepeat(Tstart, dt, 0.0, Tstop, RAT._SpawnWithRoute, self)

  -- Start scheduled activation of uncontrolled groups.
  if self.uncontrolled and self.activate_uncontrolled then
    self.sid_Activate=self:ScheduleRepeat(self.activate_delay, self.activate_delta, self.activate_frand, nil, RAT._ActivateUncontrolled, self)
  end

  return true
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Consistency Check
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function checks consistency of user input and automatically adjusts parameters if necessary.
-- @param #RAT self
function RAT:_CheckConsistency()
  self:F2()

  -- User has used SetDeparture()
  if not self.random_departure then

    -- Count departure airports and zones.
    for _,name in pairs(self.departure_ports) do
      if self:_AirportExists(name) then
        self.Ndeparture_Airports=self.Ndeparture_Airports+1
      elseif self:_ZoneExists(name) then
        self.Ndeparture_Zones=self.Ndeparture_Zones+1
      end
    end

    -- What can go wrong?
    -- Only zones but not takeoff air == > Enable takeoff air.
    if self.Ndeparture_Zones>0 and self.takeoff~=RAT.wp.air then
      self.takeoff=RAT.wp.air
      self:E(self.lid..string.format("WARNING: At least one zone defined as departure and takeoff is NOT set to air. Enabling air start for RAT group %s!", self.alias))
    end
    -- No airport and no zone specified.
    if self.Ndeparture_Airports==0 and self.Ndeparture_Zone==0 then
      self.random_departure=true
      local text=string.format("No airports or zones found given in SetDeparture(). Enabling random departure airports for RAT group %s!", self.alias)
      self:E(self.lid.."ERROR: "..text)
      MESSAGE:New(text, 30):ToAll()
    end
  end

  -- User has used SetDestination()
  if not self.random_destination then

    -- Count destination airports and zones.
    for _,name in pairs(self.destination_ports) do
      if self:_AirportExists(name) then
        self.Ndestination_Airports=self.Ndestination_Airports+1
      elseif self:_ZoneExists(name) then
        self.Ndestination_Zones=self.Ndestination_Zones+1
      end
    end

    -- One zone specified as destination ==> Enable destination zone.
    -- This does not apply to return zone because the destination is the zone and not the final destination which can be an airport.
    if self.Ndestination_Zones>0 and self.landing~=RAT.wp.air and not self.returnzone then
      self.landing=RAT.wp.air
      self.destinationzone=true
      self:E(self.lid.."WARNING: At least one zone defined as destination and landing is NOT set to air. Enabling destination zone!")
    end
    -- No specified airport and no zone found at all.
    if self.Ndestination_Airports==0 and self.Ndestination_Zones==0 then
      self.random_destination=true
      local text="No airports or zones found given in SetDestination(). Enabling random destination airports!"
      self:E(self.lid.."ERROR: "..text)
      MESSAGE:New(text, 30):ToAll()
    end
  end

  -- Destination zone and return zone should not be used together.
  if self.destinationzone and self.returnzone then
    self:E(self.lid.."ERROR: Destination zone _and_ return to zone not possible! Disabling return to zone.")
    self.returnzone=false
  end
  -- If returning to a zone, we set the landing type to "air" if takeoff is in air.
  -- Because if we start in air we want to end in air. But default landing is ground.
  if self.returnzone and self.takeoff==RAT.wp.air then
    self.landing=RAT.wp.air
  end

  -- Ensure that neither FLmin nor FLmax are above the aircrafts service ceiling.
  if self.FLminuser then
    self.FLminuser=math.min(self.FLminuser, self.aircraft.ceiling)
  end
  if self.FLmaxuser then
    self.FLmaxuser=math.min(self.FLmaxuser, self.aircraft.ceiling)
  end
  if self.FLcruise then
    self.FLcruise=math.min(self.FLcruise, self.aircraft.ceiling)
  end

  -- FL min > FL max case ==> spaw values
  if self.FLminuser and self.FLmaxuser then
    if self.FLminuser > self.FLmaxuser then
      local min=self.FLminuser
      local max=self.FLmaxuser
      self.FLminuser=max
      self.FLmaxuser=min
    end
  end

  -- Cruise alt < FL min
  if self.FLminuser and self.FLcruise<self.FLminuser then
    -- Here we have two possibilities.
    -- 1) Set cruise alt to min FL, i.e. shift cruise alt up.
    -- 2) Set min FL to cruise alt, i.e. shift min FL down.
    self.FLcruise=self.FLminuser
  end

  -- Cruise alt > FL max
  if self.FLmaxuser and self.FLcruise>self.FLmaxuser then
    self.FLcruise=self.FLmaxuser
  end

  -- Uncontrolled aircraft must start with engines off.
  if self.uncontrolled then
    -- SOLVED: Strangly, it does not work with RAT.wp.cold only with RAT.wp.hot!
    -- Figured out why. SPAWN:SpawnWithIndex is overwriting some values. Now it should work with cold as expected!
    self.takeoff=RAT.wp.cold
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set the friendly coalitions from which the airports can be used as departure and destination.
-- @param #RAT self
-- @param #string friendly "same"=own coalition+neutral (default), "sameonly"=own coalition only, "neutral"=all neutral airports.
-- Default is "same", so aircraft will use airports of the coalition their spawn template has plus all neutral airports.
-- @return #RAT RAT self object.
-- @usage yak:SetCoalition("neutral") will spawn aircraft randomly on all neutral airports.
-- @usage yak:SetCoalition("sameonly") will spawn aircraft randomly on airports belonging to the same coalition only as the template.
function RAT:SetCoalition(friendly)
  self:F2(friendly)
  if friendly:lower()=="sameonly" then
    self.friendly=RAT.coal.sameonly
  elseif friendly:lower()=="neutral" then
    self.friendly=RAT.coal.neutral
  else
    self.friendly=RAT.coal.same
  end
  return self
end

--- Set coalition of RAT group. You can make red templates blue and vice versa.
-- Note that a country is also set automatically if it has not done before via RAT:SetCountry.
--
-- * For blue, the country is set to USA.
-- * For red, the country is set to RUSSIA.
-- * For neutral, the country is set to SWITZERLAND.
--
-- This is important, since it is ultimately the COUNTRY that determines the coalition of the aircraft.
-- You can set the country explicitly via the RAT:SetCountry() function if necessary.
-- @param #RAT self
-- @param #string color Color of coalition, i.e. "red" or blue" or "neutral".
-- @return #RAT RAT self object.
function RAT:SetCoalitionAircraft(color)
  self:F2(color)
  if color:lower()=="blue" then
    self.coalition=coalition.side.BLUE
    if not self.country then
      self.country=country.id.USA
    end
  elseif color:lower()=="red" then
    self.coalition=coalition.side.RED
    if not self.country then
      self.country=country.id.RUSSIA
    end
  elseif color:lower()=="neutral" then
    self.coalition=coalition.side.NEUTRAL
    if not self.country then
      self.country=country.id.SWITZERLAND
    end
  end
  return self
end

--- Set country of RAT group.
-- See [DCS_enum_country](https://wiki.hoggitworld.com/view/DCS_enum_country).
--
-- This overrules the coalition settings. So if you want your group to be of a specific coalition, you have to set a country that is part of that coalition.
-- @param #RAT self
-- @param DCS#country.id id DCS country enumerator ID. For example country.id.USA or country.id.RUSSIA.
-- @return #RAT RAT self object.
function RAT:SetCountry(id)
  self:F2(id)
  self.country=id
  return self
end

--- Set the terminal type the aircraft use when spawning at an airbase. See [DCS_func_getParking](https://wiki.hoggitworld.com/view/DCS_func_getParking).
-- Note that some additional terminal types have been introduced. Check @{Wrapper.Airbase#AIRBASE} class for details.
-- Also note that only airports which have this kind of terminal are possible departures and/or destinations.
-- @param #RAT self
-- @param Wrapper.Airbase#AIRBASE.TerminalType termtype Type of terminal. Use enumerator AIRBASE.TerminalType.XXX.
-- @return #RAT RAT self object.
--
-- @usage
-- c17=RAT:New("C-17 BIG Plane")
-- c17:SetTerminalType(AIRBASE.TerminalType.OpenBig) -- Only very big parking spots are used.
-- c17:Spawn(5)
function RAT:SetTerminalType(termtype)
  self:F2(termtype)
  self.termtype=termtype
  return self
end

--- Set the scan radius around parking spots. Parking spot is considered to be occupied if any obstacle is found with the radius.
-- @param #RAT self
-- @param #number radius Radius in meters. Default 50 m.
-- @return #RAT RAT self object.
function RAT:SetParkingScanRadius(radius)
  self:F2(radius)
  self.parkingscanradius=radius or 50
  return self
end

--- Enables scanning for scenery objects around parking spots which might block the spot.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:SetParkingScanSceneryON()
  self:F2()
  self.parkingscanscenery=true
  return self
end

--- Disables scanning for scenery objects around parking spots which might block the spot. This is also the default setting.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:SetParkingScanSceneryOFF()
  self:F2()
  self.parkingscanscenery=false
  return self
end

--- A parking spot is not free until a possible aircraft has left and taken off.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:SetParkingSpotSafeON()
  self:F2()
  self.parkingverysafe=true
  return self
end

--- A parking spot is free as soon as possible aircraft has left the place. This is the default.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:SetParkingSpotSafeOFF()
  self:F2()
  self.parkingverysafe=false
  return self
end

--- Aircraft that reach their destination zone are not despawned. They will probably go the the nearest airbase and try to land.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:SetDespawnAirOFF()
  self.despawnair=false
  return self
end

--- Set takeoff type. Starting cold at airport, starting hot at airport, starting at runway, starting in the air.
-- Default is "takeoff-coldorhot". So there is a 50% chance that the aircraft starts with cold engines and 50% that it starts with hot engines.
-- @param #RAT self
-- @param #string type Type can be "takeoff-cold" or "cold", "takeoff-hot" or "hot", "takeoff-runway" or "runway", "air".
-- @return #RAT RAT self object.
-- @usage RAT:Takeoff("hot") will spawn RAT objects at airports with engines started.
-- @usage RAT:Takeoff("cold") will spawn RAT objects at airports with engines off.
-- @usage RAT:Takeoff("air") will spawn RAT objects in air over random airports or within pre-defined zones.
function RAT:SetTakeoff(type)
  self:F2(type)

  local _Type
  if type:lower()=="takeoff-cold" or type:lower()=="cold" then
    _Type=RAT.wp.cold
  elseif type:lower()=="takeoff-hot" or type:lower()=="hot" then
    _Type=RAT.wp.hot
  elseif type:lower()=="takeoff-runway" or type:lower()=="runway" then
    _Type=RAT.wp.runway
  elseif type:lower()=="air" then
    _Type=RAT.wp.air
  else
    _Type=RAT.wp.coldorhot
  end

  self.takeoff=_Type

  return self
end

--- Set takeoff type cold. Aircraft will spawn at a parking spot with engines off.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:SetTakeoffCold()
  self.takeoff=RAT.wp.cold
  return self
end

--- Set takeoff type to hot. Aircraft will spawn at a parking spot with engines on.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:SetTakeoffHot()
  self.takeoff=RAT.wp.hot
  return self
end

--- Set takeoff type to runway. Aircraft will spawn directly on the runway.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:SetTakeoffRunway()
  self.takeoff=RAT.wp.runway
  return self
end

--- Set takeoff type to cold or hot. Aircraft will spawn at a parking spot with 50:50 change of engines on or off.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:SetTakeoffColdOrHot()
  self.takeoff=RAT.wp.coldorhot
  return self
end

--- Set takeoff type to air. Aircraft will spawn in the air.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:SetTakeoffAir()
  self.takeoff=RAT.wp.air
  return self
end

--- Set possible departure ports. This can be an airport or a zone.
-- @param #RAT self
-- @param #string departurenames Name or table of names of departure airports or zones.
-- @return #RAT RAT self object.
-- @usage RAT:SetDeparture("Sochi-Adler") will spawn RAT objects at Sochi-Adler airport.
-- @usage RAT:SetDeparture({"Sochi-Adler", "Gudauta"}) will spawn RAT aircraft radomly at Sochi-Adler or Gudauta airport.
-- @usage RAT:SetDeparture({"Zone A", "Gudauta"}) will spawn RAT aircraft in air randomly within Zone A, or within a zone around Gudauta airport. Note that this also requires RAT:takeoff("air") to be set.
function RAT:SetDeparture(departurenames)
  self:F2(departurenames)

  -- Random departure is deactivated now that user specified departure ports.
  self.random_departure=false

  -- Convert input to table.
  local names
  if type(departurenames)=="table" then
    names=departurenames
  elseif type(departurenames)=="string" then
    names={departurenames}
  else
    -- error message
    self:E(self.lid.."ERROR: Input parameter must be a string or a table in SetDeparture()!")
  end

  -- Put names into arrays.
  for _,name in pairs(names) do

    if self:_AirportExists(name) then
      -- If an airport with this name exists, we put it in the ports array.
      table.insert(self.departure_ports, name)
    elseif self:_ZoneExists(name) then
      -- If it is not an airport, we assume it is a zone.
      table.insert(self.departure_ports, name)
     else
      self:E(self.lid.."ERROR: No departure airport or zone found with name "..name)
    end

  end

  return self
end

--- Set name of destination airports or zones for the AI aircraft.
-- @param #RAT self
-- @param #string destinationnames Name of the destination airport or #table of destination airports.
-- @return #RAT RAT self object.
-- @usage RAT:SetDestination("Krymsk") makes all aircraft of this RAT object fly to Krymsk airport.
function RAT:SetDestination(destinationnames)
  self:F2(destinationnames)

  -- Random departure is deactivated now that user specified departure ports.
  self.random_destination=false

  -- Convert input to table
  local names
  if type(destinationnames)=="table" then
    names=destinationnames
  elseif type(destinationnames)=="string" then
    names={destinationnames}
  else
    -- Error message.
    self:E(self.lid.."ERROR: Input parameter must be a string or a table in SetDestination()!")
  end

  -- Put names into arrays.
  for _,name in pairs(names) do

    if self:_AirportExists(name) then
      -- If an airport with this name exists, we put it in the ports array.
      table.insert(self.destination_ports, name)
    elseif self:_ZoneExists(name) then
      -- If it is not an airport, we assume it is a zone.
      table.insert(self.destination_ports, name)
    else
      self:E(self.lid.."ERROR: No destination airport or zone found with name "..name)
    end

  end

  return self
end

--- Destinations are treated as zones. Aircraft will not land but rather be despawned when they reach a random point in the zone.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:DestinationZone()
  self:F2()

  -- Destination is a zone. Needs special care.
  self.destinationzone=true

  -- Landing type is "air" because we don't actually land at the airport.
  self.landing=RAT.wp.air

  return self
end

--- Aircraft will fly to a random point within a zone and then return to its departure airport or zone.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:ReturnZone()
  self:F2()
  -- Destination is a zone. Needs special care.
  self.returnzone=true
  return self
end


--- Include all airports which lie in a zone as possible destinations.
-- @param #RAT self
-- @param Core.Zone#ZONE zone Zone in which the destination airports lie. Has to be a MOOSE zone.
-- @return #RAT RAT self object.
function RAT:SetDestinationsFromZone(zone)
  self:F2(zone)

  -- Random departure is deactivated now that user specified departure ports.
  self.random_destination=false

  -- Set zone.
  self.destination_Azone=zone

  return self
end

--- Include all airports which lie in a zone as possible destinations.
-- @param #RAT self
-- @param Core.Zone#ZONE zone Zone in which the departure airports lie. Has to be a MOOSE zone.
-- @return #RAT RAT self object.
function RAT:SetDeparturesFromZone(zone)
  self:F2(zone)

  -- Random departure is deactivated now that user specified departure ports.
  self.random_departure=false

  -- Set zone.
  self.departure_Azone=zone

  return self
end

--- Add all friendly airports to the list of possible departures.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:AddFriendlyAirportsToDepartures()
  self:F2()
  self.addfriendlydepartures=true
  return self
end

--- Add all friendly airports to the list of possible destinations
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:AddFriendlyAirportsToDestinations()
  self:F2()
  self.addfriendlydestinations=true
  return self
end

--- Airports, FARPs and ships explicitly excluded as departures and destinations.
-- @param #RAT self
-- @param #string ports Name or table of names of excluded airports.
-- @return #RAT RAT self object.
function RAT:ExcludedAirports(ports)
  self:F2(ports)
  if type(ports)=="string" then
    self.excluded_ports={ports}
  else
    self.excluded_ports=ports
  end
  return self
end

--- Set skill of AI aircraft. Default is "High".
-- @param #RAT self
-- @param #string skill Skill, options are "Average", "Good", "High", "Excellent" and "Random". Parameter is case insensitive.
-- @return #RAT RAT self object.
function RAT:SetAISkill(skill)
  self:F2(skill)
  if skill:lower()=="average" then
    self.skill="Average"
  elseif skill:lower()=="good" then
    self.skill="Good"
  elseif skill:lower()=="excellent" then
    self.skill="Excellent"
  elseif skill:lower()=="random" then
    self.skill="Random"
  else
    self.skill="High"
  end
  return self
end

--- Set livery of aircraft. If more than one livery is specified in a table, the actually used one is chosen randomly from the selection.
-- @param #RAT self
-- @param #table skins Name of livery or table of names of liveries.
-- @return #RAT RAT self object.
function RAT:Livery(skins)
  self:F2(skins)
  if type(skins)=="string" then
    self.livery={skins}
  else
    self.livery=skins
  end
  return self
end

--- Change aircraft type. This is a dirty hack which allows to change the aircraft type of the template group.
-- Note that all parameters like cruise speed, climb rate, range etc are still taken from the template group which likely leads to strange behaviour.
-- @param #RAT self
-- @param #string actype Type of aircraft which is spawned independent of the template group. Use with care and expect problems!
-- @return #RAT RAT self object.
function RAT:ChangeAircraft(actype)
  self:F2(actype)
  self.actype=actype
  return self
end

--- Aircraft will continue their journey from their destination. This means they are respawned at their destination and get a new random destination.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:ContinueJourney()
  self:F2()
  self.continuejourney=true
  self.commute=false
  return self
end

--- Aircraft will commute between their departure and destination airports or zones.
-- @param #RAT self
-- @param #boolean starshape If true, keep homebase, i.e. travel A-->B-->A-->C-->A-->D... instead of A-->B-->A-->B-->A...
-- @return #RAT RAT self object.
function RAT:Commute(starshape)
  self:F2()
  self.commute=true
  self.continuejourney=false
  if starshape then
    self.starshape=starshape
  else
    self.starshape=false
  end
  return self
end

--- Set the delay before first group is spawned.
-- @param #RAT self
-- @param #number delay Delay in seconds. Default is 5 seconds. Minimum delay is 0.5 seconds.
-- @return #RAT RAT self object.
function RAT:SetSpawnDelay(delay)
  self:F2(delay)
  delay=delay or 5
  self.spawndelay=math.max(0.5, delay)
  return self
end

--- Set the interval between spawnings of the template group.
-- @param #RAT self
-- @param #number interval Interval in seconds. Default is 5 seconds. Minimum is 0.5 seconds.
-- @return #RAT RAT self object.
function RAT:SetSpawnInterval(interval)
  self:F2(interval)
  interval=interval or 5
  self.spawninterval=math.max(0.5, interval)
  return self
end

--- Set max number of groups that will be spawned. When this limit is reached, no more RAT groups are spawned.
-- @param #RAT self
-- @param #number Nmax Max number of groups. Default `nil`=unlimited.
-- @return #RAT RAT self object.
function RAT:SetSpawnLimit(Nmax)
  self.NspawnMax=Nmax
  return self
end

--- Make aircraft respawn the moment they land rather than at engine shut down.
-- @param #RAT self
-- @param #number delay (Optional) Delay in seconds until respawn happens after landing. Default is 1 second. Minimum is 1 second.
-- @return #RAT RAT self object.
function RAT:RespawnAfterLanding(delay)
  self:F2(delay)
  self.respawn_at_landing=true
  self:SetRespawnDelay(delay)
  return self
end

--- Sets the delay between despawning and respawning aircraft.
-- @param #RAT self
-- @param #number delay Delay in seconds until respawn happens. Default is 1 second. Minimum is 1 second.
-- @return #RAT RAT self object.
function RAT:SetRespawnDelay(delay)
  self:F2(delay)
  delay = delay or 1.0
  delay=math.max(1.0, delay)
  self.respawn_delay=delay
  return self
end

--- Aircraft will not get respawned when they finished their route.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:NoRespawn()
  self:F2()
  self.norespawn=true
  return self
end

--- Number of tries to respawn an aircraft in case it has accidentally been spawned on runway.
-- @param #RAT self
-- @param #number n Number of retries. Default is 3.
-- @return #RAT RAT self object.
function RAT:SetMaxRespawnTriedWhenSpawnedOnRunway(n)
  self:F2(n)
  n=n or 3
  self.onrunwaymaxretry=n
  return self
end

--- A new aircraft is spawned directly after the last one took off. This creates a lot of outbound traffic. Aircraft are not respawned after they reached their destination.
-- Therefore, this option is not to be used with the "commute" or "continue journey" options.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:RespawnAfterTakeoff()
  self:F2()
  self.respawn_after_takeoff=true
  return self
end

--- Aircraft will be respawned after they crashed or get shot down. This is the default behavior.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:RespawnAfterCrashON()
  self:F2()
  self.respawn_after_crash=true
  return self
end

--- Aircraft will not be respawned after they crashed or get shot down.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:RespawnAfterCrashOFF()
  self:F2()
  self.respawn_after_crash=false
  return self
end

--- If aircraft cannot be spawned on parking spots, it is allowed to spawn them in air above the same airport. Note that this is also the default behavior.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:RespawnInAirAllowed()
  self:F2()
  self.respawn_inair=true
  return self
end

--- If aircraft cannot be spawned on parking spots, it is NOT allowed to spawn them in air. This has only impact if aircraft are supposed to be spawned on the ground (and not in a zone).
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:RespawnInAirNotAllowed()
  self:F2()
  self.respawn_inair=false
  return self
end

--- Check if aircraft have accidentally been spawned on the runway. If so they will be removed immediately.
-- @param #RAT self
-- @param #boolean switch If true, check is performed. If false, this check is omitted.
-- @param #number radius Distance in meters until a unit is considered to have spawned accidentally on the runway. Default is 75 m.
-- @return #RAT RAT self object.
function RAT:CheckOnRunway(switch, distance)
  self:F2(switch)
  if switch==nil then
    switch=true
  end
  self.checkonrunway=switch
  self.onrunwayradius=distance or 75
  return self
end

--- Check if aircraft have accidentally been spawned on top of each other. If yes, they will be removed immediately.
-- @param #RAT self
-- @param #boolean switch If true, check is performed. If false, this check is omitted.
-- @param #number radius Radius in meters until which a unit is considered to be on top of each other. Default is 2 m.
-- @return #RAT RAT self object.
function RAT:CheckOnTop(switch, radius)
  self:F2(switch)
  if switch==nil then
    switch=true
  end
  self.checkontop=switch
  self.ontopradius=radius or 2
  return self
end

--- Enable Radio. Overrules the ME setting.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:RadioON()
  self:F2()
  self.radio=true
  return self
end

--- Disable Radio. Overrules the ME setting.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:RadioOFF()
  self:F2()
  self.radio=false
  return self
end

--- Set radio frequency.
-- @param #RAT self
-- @param #number frequency Radio frequency.
-- @return #RAT RAT self object.
function RAT:RadioFrequency(frequency)
  self:F2(frequency)
  self.frequency=frequency
  return self
end

--- Set radio modulation. Default is AM.
-- @param #RAT self
-- @param #string modulation Either "FM" or "AM". If no value is given, modulation is set to AM.
-- @return #RAT RAT self object.
function RAT:RadioModulation(modulation)
  self:F2(modulation)
  if modulation=="AM" then
    self.modulation=radio.modulation.AM
  elseif modulation=="FM" then
    self.modulation=radio.modulation.FM
  else
    self.modulation=radio.modulation.AM
  end
  return self
end

--- Radio menu On. Default is off.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:RadioMenuON()
  self:F2()
  self.f10menu=true
  return self
end

--- Radio menu Off. This is the default setting.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:RadioMenuOFF()
  self:F2()
  self.f10menu=false
  return self
end

--- Aircraft are invisible.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:Invisible()
  self:F2()
  self.invisible=true
  return self
end

--- Turn EPLRS datalink on/off.
-- @param #RAT self
-- @param #boolean switch If true (or nil), turn EPLRS on.
-- @return #RAT RAT self object.
function RAT:SetEPLRS(switch)
  if switch==nil or switch==true then
    self.eplrs=true
  else
    self.eplrs=false
  end
  return self
end

--- Aircraft are immortal.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:Immortal()
  self:F2()
  self.immortal=true
  return self
end

--- Spawn aircraft in uncontrolled state. Aircraft will only sit at their parking spots. They can be activated randomly by the RAT:ActivateUncontrolled() function.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:Uncontrolled()
  self:F2()
  self.uncontrolled=true
  return self
end

--- Define how aircraft that are spawned in uncontrolled state are activate.
-- @param #RAT self
-- @param #number maxactivated Maximal numnber of activated aircraft. Absolute maximum will be the number of spawned groups. Default is 1.
-- @param #number delay Time delay in seconds before (first) aircraft is activated. Default is 1 second.
-- @param #number delta Time difference in seconds before next aircraft is activated. Default is 1 second.
-- @param #number frand Factor [0,...,1] for randomization of time difference between aircraft activations. Default is 0, i.e. no randomization.
-- @return #RAT RAT self object.
function RAT:ActivateUncontrolled(maxactivated, delay, delta, frand)
  self:F2({max=maxactivated, delay=delay, delta=delta, rand=frand})

  self.activate_uncontrolled=true
  self.activate_max=maxactivated or 1
  self.activate_delay=delay or 1
  self.activate_delta=delta or 1
  self.activate_frand=frand or 0

  -- Ensure min delay is one second.
  self.activate_delay=math.max(self.activate_delay,1)

  -- Ensure min delta is one second.
  self.activate_delta=math.max(self.activate_delta,0)

  -- Ensure frand is in [0,...,1]
  self.activate_frand=math.max(self.activate_frand,0)
  self.activate_frand=math.min(self.activate_frand,1)

  return self
end

--- Set the time after which inactive groups will be destroyed.
-- @param #RAT self
-- @param #number time Time in seconds. Default is 600 seconds = 10 minutes. Minimum is 60 seconds.
-- @return #RAT RAT self object.
function RAT:TimeDestroyInactive(time)
  self:F2(time)
  time=time or self.Tinactive
  time=math.max(time, 60)
  self.Tinactive=time
  return self
end

--- Set the maximum cruise speed of the aircraft.
-- @param #RAT self
-- @param #number speed Speed in km/h.
-- @return #RAT RAT self object.
function RAT:SetMaxCruiseSpeed(speed)
  self:F2(speed)
  -- Convert to m/s.
  self.Vcruisemax=speed/3.6
  return self
end

--- Set the climb rate. This automatically sets the climb angle.
-- @param #RAT self
-- @param #number rate Climb rate in ft/min. Default is 1500 ft/min. Minimum is 100 ft/min. Maximum is 15,000 ft/min.
-- @return #RAT RAT self object.
function RAT:SetClimbRate(rate)
  self:F2(rate)
  rate=rate or self.Vclimb
  rate=math.max(rate, 100)
  rate=math.min(rate, 15000)
  self.Vclimb=rate
  return self
end

--- Set the angle of descent. Default is 3.6 degrees, which corresponds to 3000 ft descent after one mile of travel.
-- @param #RAT self
-- @param #number angle Angle of descent in degrees. Minimum is 0.5 deg. Maximum 50 deg.
-- @return #RAT RAT self object.
function RAT:SetDescentAngle(angle)
  self:F2(angle)
  angle=angle or self.AlphaDescent
  angle=math.max(angle, 0.5)
  angle=math.min(angle, 50)
  self.AlphaDescent=angle
  return self
end

--- Set rules of engagement (ROE). Default is weapon hold. This is a peaceful class.
-- @param #RAT self
-- @param #string roe "hold" = weapon hold, "return" = return fire, "free" = weapons free.
-- @return #RAT RAT self object.
function RAT:SetROE(roe)
  self:F2(roe)
  if roe=="return" then
    self.roe=RAT.ROE.returnfire
  elseif roe=="free" then
    self.roe=RAT.ROE.weaponfree
  else
    self.roe=RAT.ROE.weaponhold
  end
  return self
end

--- Set reaction to threat (ROT). Default is no reaction, i.e. aircraft will simply ignore all enemies.
-- @param #RAT self
-- @param #string rot "noreaction" = no reaction to threats, "passive" = passive defence, "evade" = evade enemy attacks.
-- @return #RAT RAT self object.
function RAT:SetROT(rot)
  self:F2(rot)
  if rot=="passive" then
    self.rot=RAT.ROT.passive
  elseif rot=="evade" then
    self.rot=RAT.ROT.evade
  else
    self.rot=RAT.ROT.noreaction
  end
  return self
end

--- Set the name of the F10 submenu. Default is the name of the template group.
-- @param #RAT self
-- @param #string name Submenu name.
-- @return #RAT RAT self object.
function RAT:MenuName(name)
  self:F2(name)
  self.SubMenuName=tostring(name)
  return self
end

--- Enable ATC, which manages the landing queue for RAT aircraft if they arrive simultaniously at the same airport.
-- @param #RAT self
-- @param #boolean switch Enable ATC (true) or Disable ATC (false). No argument means ATC enabled.
-- @return #RAT RAT self object.
function RAT:EnableATC(switch)
  self:F2(switch)
  if switch==nil then
    switch=true
  end
  self.ATCswitch=switch
  return self
end

--- Turn messages from ATC on or off. Default is on. This setting effects all RAT objects and groups!
-- @param #RAT self
-- @param #boolean switch Enable (true) or disable (false) messages from ATC.
-- @return #RAT RAT self object.
function RAT:ATC_Messages(switch)
  self:F2(switch)
  if switch==nil then
    switch=true
  end
  RAT.ATC.messages=switch
  return self
end

--- Max number of planes that get landing clearance of the RAT ATC. This setting effects all RAT objects and groups!
-- @param #RAT self
-- @param #number n Number of aircraft that are allowed to land simultaniously. Default is 2.
-- @return #RAT RAT self object.
function RAT:ATC_Clearance(n)
  self:F2(n)
  RAT.ATC.Nclearance=n or 2
  return self
end

--- Delay between granting landing clearance for simultanious landings. This setting effects all RAT objects and groups!
-- @param #RAT self
-- @param #number time Delay time when the next aircraft will get landing clearance event if the previous one did not land yet. Default is 240 sec.
-- @return #RAT RAT self object.
function RAT:ATC_Delay(time)
  self:F2(time)
  RAT.ATC.delay=time or 240
  return self
end

--- Set minimum distance between departure and destination. Default is 5 km.
-- Minimum distance should not be smaller than maybe ~100 meters to ensure that departure and destination are different.
-- @param #RAT self
-- @param #number dist Distance in km.
-- @return #RAT RAT self object.
function RAT:SetMinDistance(dist)
  self:F2(dist)
  -- Distance in meters. Absolute minimum is 500 m.
  self.mindist=math.max(100, dist*1000)
  return self
end

--- Set maximum distance between departure and destination. Default is 5000 km but aircarft range is also taken into account automatically.
-- @param #RAT self
-- @param #number dist Distance in km.
-- @return #RAT RAT self object.
function RAT:SetMaxDistance(dist)
  self:F2(dist)
  -- Distance in meters.
  self.maxdist=dist*1000
  return self
end

--- Turn debug messages on or off. Default is off.
-- @param #RAT self
-- @param #boolean switch Turn debug on=true or off=false. No argument means on.
-- @return #RAT RAT self object.
function RAT:_Debug(switch)
  self:F2(switch)
  if switch==nil then
    switch=true
  end
  self.Debug=switch
  return self
end

--- Enable debug mode. More output in dcs.log file and onscreen messages to all.
-- @param #RAT self
-- @return #RAT RAT self object.
function RAT:Debugmode()
  self:F2()
  self.Debug=true
  return self
end

--- Aircraft report status update messages along the route.
-- @param #RAT self
-- @param #boolean switch Swtich reports on (true) or off (false). No argument is on.
-- @return #RAT RAT self object.
function RAT:StatusReports(switch)
  self:F2(switch)
  if switch==nil then
    switch=true
  end
  self.reportstatus=switch
  return self
end

--- Place markers of waypoints on the F10 map. Default is off.
-- @param #RAT self
-- @param #boolean switch true=yes, false=no.
-- @return #RAT RAT self object.
function RAT:PlaceMarkers(switch)
  self:F2(switch)
  if switch==nil then
    switch=true
  end
  self.placemarkers=switch
  return self
end

--- Set flight level. Setting this value will overrule all other logic. Aircraft will try to fly at this height regardless.
-- @param #RAT self
-- @param #number FL Fight Level in hundrets of feet. E.g. FL200 = 20000 ft ASL.
-- @return #RAT RAT self object.
function RAT:SetFL(FL)
  self:F2(FL)
  FL=FL or self.FLcruise
  FL=math.max(FL,0)
  self.FLuser=FL*RAT.unit.FL2m
  return self
end

--- Set max flight level. Setting this value will overrule all other logic. Aircraft will try to fly at less than this FL regardless.
-- @param #RAT self
-- @param #number FL Maximum Fight Level in hundrets of feet.
-- @return #RAT RAT self object.
function RAT:SetFLmax(FL)
  self:F2(FL)
  self.FLmaxuser=FL*RAT.unit.FL2m
  return self
end

--- Set max cruising altitude above sea level.
-- @param #RAT self
-- @param #number alt Altitude ASL in meters.
-- @return #RAT RAT self object.
function RAT:SetMaxCruiseAltitude(alt)
  self:F2(alt)
  self.FLmaxuser=alt
  return self
end

--- Set min flight level. Setting this value will overrule all other logic. Aircraft will try to fly at higher than this FL regardless.
-- @param #RAT self
-- @param #number FL Maximum Fight Level in hundrets of feet.
-- @return #RAT RAT self object.
function RAT:SetFLmin(FL)
  self:F2(FL)
  self.FLminuser=FL*RAT.unit.FL2m
  return self
end

--- Set min cruising altitude above sea level.
-- @param #RAT self
-- @param #number alt Altitude ASL in meters.
-- @return #RAT RAT self object.
function RAT:SetMinCruiseAltitude(alt)
  self:F2(alt)
  self.FLminuser=alt
  return self
end

--- Set flight level of cruising part. This is still be checked for consitancy with selected route and prone to radomization.
-- Default is FL200 for planes and FL005 for helicopters.
-- @param #RAT self
-- @param #number FL Flight level in hundrets of feet. E.g. FL200 = 20000 ft ASL.
-- @return #RAT RAT self object.
function RAT:SetFLcruise(FL)
  self:F2(FL)
  self.FLcruise=FL*RAT.unit.FL2m
  return self
end

--- Set cruising altitude. This is still be checked for consitancy with selected route and prone to radomization.
-- @param #RAT self
-- @param #number alt Cruising altitude ASL in meters.
-- @return #RAT RAT self object.
function RAT:SetCruiseAltitude(alt)
  self:F2(alt)
  self.FLcruise=alt
  return self
end

--- Set onboard number prefix. Same as setting "TAIL #" in the mission editor. Note that if you dont use this function, the values defined in the template group of the ME are taken.
-- @param #RAT self
-- @param #string tailnumprefix String of the tail number prefix. If flight consists of more than one aircraft, two digits are appended automatically, i.e. <tailnumprefix>001, <tailnumprefix>002, ...
-- @param #number zero (Optional) Starting value of the automatically appended numbering of aircraft within a flight. Default is 0.
-- @return #RAT RAT self object.
function RAT:SetOnboardNum(tailnumprefix, zero)
  self:F2({tailnumprefix=tailnumprefix, zero=zero})
  self.onboardnum=tailnumprefix
  if zero ~= nil then
    self.onboardnum0=zero
  end
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Private functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Initialize basic parameters of the aircraft based on its (template) group in the mission editor.
-- @param #RAT self
-- @param DCS#Group DCSgroup Group of the aircraft in the mission editor.
function RAT:_InitAircraft(DCSgroup)
  self:F2(DCSgroup)

  local DCSunit=DCSgroup:getUnit(1)
  local DCSdesc=DCSunit:getDesc()
  local DCScategory=DCSgroup:getCategory()
  local DCStype=DCSunit:getTypeName()

  -- set category
  if DCScategory==Group.Category.AIRPLANE then
    self.category=RAT.cat.plane
  elseif DCScategory==Group.Category.HELICOPTER then
    self.category=RAT.cat.heli
  else
    self.category="other"
    self:E(self.lid.."ERROR: Group of RAT is neither airplane nor helicopter!")
  end

  -- Get type of aircraft.
  self.aircraft.type=DCStype

  -- inital fuel in %
  self.aircraft.fuel=DCSunit:getFuel()

  -- operational range in NM converted to m
  self.aircraft.Rmax = DCSdesc.range*RAT.unit.nm2m

  -- effective range taking fuel into accound and a 5% reserve
  self.aircraft.Reff = self.aircraft.Rmax*self.aircraft.fuel*0.95

  -- max airspeed from group
  self.aircraft.Vmax = DCSdesc.speedMax

  -- max climb speed in m/s
  self.aircraft.Vymax=DCSdesc.VyMax

  -- service ceiling in meters
  self.aircraft.ceiling=DCSdesc.Hmax

  -- Store all descriptors.
  --self.aircraft.descriptors=DCSdesc

  -- aircraft dimensions
  if DCSdesc.box then
    self.aircraft.length=DCSdesc.box.max.x
    self.aircraft.height=DCSdesc.box.max.y
    self.aircraft.width=DCSdesc.box.max.z
  elseif DCStype == "Mirage-F1CE" then
    self.aircraft.length=16
    self.aircraft.height=5
    self.aircraft.width=9
  elseif DCStype == "Saab340" then      --   <- These lines added
    self.aircraft.length=19.73          --   <- These lines added
    self.aircraft.height=6.97           --   <- These lines added
    self.aircraft.width=21.44           --   <- These lines added
  end

  self.aircraft.box=math.max(self.aircraft.length,self.aircraft.width)

  -- info message
  local text=string.format("\n******************************************************\n")
  text=text..string.format("Aircraft parameters:\n")
  text=text..string.format("Template group  =  %s\n",       self.SpawnTemplatePrefix)
  text=text..string.format("Alias           =  %s\n",       self.alias)
  text=text..string.format("Category        =  %s\n",       self.category)
  text=text..string.format("Type            =  %s\n",       self.aircraft.type)
  text=text..string.format("Length (x)      = %6.1f m\n",   self.aircraft.length)
  text=text..string.format("Width  (z)      = %6.1f m\n",   self.aircraft.width)
  text=text..string.format("Height (y)      = %6.1f m\n",   self.aircraft.height)
  text=text..string.format("Max air speed   = %6.1f m/s\n", self.aircraft.Vmax)
  text=text..string.format("Max climb speed = %6.1f m/s\n", self.aircraft.Vymax)
  text=text..string.format("Initial Fuel    = %6.1f\n",     self.aircraft.fuel*100)
  text=text..string.format("Max range       = %6.1f km\n",  self.aircraft.Rmax/1000)
  text=text..string.format("Eff range       = %6.1f km (with 95 percent initial fuel amount)\n",  self.aircraft.Reff/1000)
  text=text..string.format("Ceiling         = %6.1f km = FL%3.0f\n", self.aircraft.ceiling/1000, self.aircraft.ceiling/RAT.unit.FL2m)
  text=text..string.format("******************************************************\n")
  self:T(self.lid..text)

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Spawn the AI aircraft with a route.
-- Sets the departure and destination airports and waypoints.
-- Modifies the spawn template.
-- Sets ROE/ROT.
-- Initializes the ratcraft array and group menu.
-- @param #RAT self
-- @param #string _departure (Optional) Name of departure airbase.
-- @param #string _destination (Optional) Name of destination airbase.
-- @param #number _takeoff Takeoff type id.
-- @param #number _landing Landing type id.
-- @param #string _livery Livery to use for this group.
-- @param #table _waypoint First waypoint to be used (for continue journey, commute, etc).
-- @param Core.Point#COORDINATE _lastpos (Optional) Position where the aircraft will be spawned.
-- @param #number _nrespawn Number of already performed respawn attempts (e.g. spawning on runway bug).
-- @param #table parkingdata Explicitly specify the parking spots when spawning at an airport.
-- @return #number Spawn index.
function RAT:_SpawnWithRoute(_departure, _destination, _takeoff, _landing, _livery, _waypoint, _lastpos, _nrespawn, parkingdata)
  self:F({rat=self.lid, departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, waypoint=_waypoint, lastpos=_lastpos, nrespawn=_nrespawn})
  
  -- Check if max spawn limit exists and is reached. SpawnIndex counting starts at 0, hence greater equal and not greater.
  if self.NspawnMax and self.SpawnIndex>=self.NspawnMax then
    self:T(self.lid..string.format("Max limit of spawns reached %d >= %d! Will not spawn any more groups", self.NspawnMax, self.SpawnIndex))
    return
  else
    self:T2(self.lid..string.format("Spawning with spawn index=%d", self.SpawnIndex))
  end

  -- Set takeoff type.
  local takeoff=self.takeoff
  local landing=self.landing

  -- Overrule takeoff/landing by what comes in.
  if _takeoff then
    takeoff=_takeoff
  end
  if _landing then
    landing=_landing
  end

  -- Random choice between cold and hot.
  if takeoff==RAT.wp.coldorhot then
    local temp={RAT.wp.cold, RAT.wp.hot}
    takeoff=temp[math.random(2)]
  end

  -- Number of respawn attempts after spawning on runway.
  local nrespawn=0
  if _nrespawn then
    nrespawn=_nrespawn
  end

  -- Set flight plan.
  local departure, destination, waypoints, wpdesc, wpstatus = self:_SetRoute(takeoff, landing, _departure, _destination, _waypoint)

  -- Return nil if we could not find a departure destination or waypoints
  if not (departure and destination and waypoints) then
    return nil
  end

  -- Set (another) livery.
  local livery
  if _livery then
    -- Take livery from previous flight (continue journey).
    livery=_livery
  elseif self.livery then
    -- Choose random livery.
    livery=self.livery[math.random(#self.livery)]
    local text=string.format("Chosen livery for group %s: %s", self:_AnticipatedGroupName(), livery)
    self:T(self.lid..text)
  else
    livery=nil
  end
  
  -- We set the aircraft to uncontrolled if the departure airbase has a FLIGHTCONTROL.
  local uncontrolled=self.uncontrolled
  local isFlightcontrol=self:_IsFlightControlAirbase(departure)
  if takeoff~=RAT.wp.air and departure and isFlightcontrol then
    takeoff=RAT.wp.cold
    uncontrolled=true
  end 

  -- Modify the spawn template to follow the flight plan.
  local successful=self:_ModifySpawnTemplate(waypoints, livery, _lastpos, departure, takeoff, parkingdata, uncontrolled)
  if not successful then
    return nil
  end

  -- Actually spawn the group.
  local group=self:SpawnWithIndex(self.SpawnIndex) -- Wrapper.Group#GROUP
  
  -- Group name.
  local groupname=group:GetName()
  
  -- Create a flightgroup object.
  local flightgroup=FLIGHTGROUP:New(group)
  
  -- Setting holding time to nil so that flight never gets landing clearance. This is done by the RAT ATC (FC sets holdtime to nil in FLIGHTGROUP).
  if self.ATCswitch then
    flightgroup.holdtime=nil
  end
  
  -- No automatic despawning if group gets stuck.
  flightgroup.stuckDespawn=false
  
  
  -- Increase counter of alive groups (also uncontrolled ones).
  self.alive=self.alive+1
  self:T(self.lid..string.format("Alive groups counter now = %d.",self.alive))

  -- ATC is monitoring this flight (if it is supposed to land).
  if self.ATCswitch and landing==RAT.wp.landing then
  
    -- Get destination airbase name. For returnzone, this is the departure airbase.
    local airbasename=destination:GetName()
    if self.returnzone then
      airbasename=departure:GetName()
    end
    
    -- Add flight (if there is no FC at the airbase)
    if not self:_IsFlightControlAirbase(airbasename) then
      self:_ATCAddFlight(groupname, airbasename)
    end
  end

  -- Place markers of waypoints on F10 map.
  if self.placemarkers then
    self:_PlaceMarkers(waypoints, wpdesc, self.SpawnIndex)
  end
  
  -- Set group ready for takeoff at the FLIGHTCONTROL (if we do not do via a scheduler).
  if isFlightcontrol and not self.activate_uncontrolled then
    local N=math.random(120)
    self:T(self.lid..string.format("Flight will be ready for takeoff in %d seconds", N))
    flightgroup:SetReadyForTakeoff(true, N)
  end

  -- Set group to be invisible.
  if self.invisible then
    flightgroup:SetDefaultInvisible(true)
    flightgroup:SwitchInvisible(true)
  end

  -- Set group to be immortal.
  if self.immortal then
    flightgroup:SetDefaultImmortal(true)
    flightgroup:SwitchImmortal(true)
  end

  -- Set group to be immortal.
  if self.eplrs then
    flightgroup:SetDefaultEPLRS(true)
    flightgroup:SwitchEPLRS(true)
  end

  -- Set ROE, default is "weapon hold".
  self:_SetROE(flightgroup, self.roe)

  -- Set ROT, default is "no reaction".
  self:_SetROT(flightgroup, self.rot)
  
  -- Init ratcraft array.
  local ratcraft={} --#RAT.RatCraft
  ratcraft.index=self.SpawnIndex
  ratcraft.group=group
  ratcraft.flightgroup=flightgroup
  ratcraft.destination=destination
  ratcraft.departure=departure
  ratcraft.waypoints=waypoints
  ratcraft.airborne=group:InAir()
  ratcraft.nunits=group:GetInitialSize()
  
  -- Initial and current position. For calculating the travelled distance.
  ratcraft.Pnow=group:GetCoordinate()
  ratcraft.Distance=0

  -- Each aircraft gets its own takeoff type.
  ratcraft.takeoff=takeoff
  ratcraft.landing=landing
  ratcraft.wpdesc=wpdesc
  ratcraft.wpstatus=wpstatus

  -- Aircraft is active or spawned in uncontrolled state.
  ratcraft.active=not uncontrolled

  -- Set status to spawned. This will be overwritten in birth event.
  ratcraft.status=RAT.status.Spawned

  -- Livery
  ratcraft.livery=livery

  -- If this switch is set to true, the aircraft will be despawned the next time the status function is called.
  ratcraft.despawnme=false

  -- Number of preformed spawn attempts for this group.
  ratcraft.nrespawn=nrespawn
  
  -- Add ratcaft to table.
  self.ratcraft[self.SpawnIndex]=ratcraft

  -- Create submenu for this group.
  if self.f10menu then
    local name=self.aircraft.type.." ID "..tostring(self.SpawnIndex)
    -- F10/RAT/<templatename>/Group X
    self.Menu[self.SubMenuName].groups[self.SpawnIndex]=MENU_MISSION:New(name, self.Menu[self.SubMenuName].groups)
    -- F10/RAT/<templatename>/Group X/Set ROE
    self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"]=MENU_MISSION:New("Set ROE", self.Menu[self.SubMenuName].groups[self.SpawnIndex])
    MENU_MISSION_COMMAND:New("Weapons hold", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, flightgroup, RAT.ROE.weaponhold)
    MENU_MISSION_COMMAND:New("Weapons free", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, flightgroup, RAT.ROE.weaponfree)
    MENU_MISSION_COMMAND:New("Return fire",  self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, flightgroup, RAT.ROE.returnfire)
    -- F10/RAT/<templatename>/Group X/Set ROT
    self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"]=MENU_MISSION:New("Set ROT", self.Menu[self.SubMenuName].groups[self.SpawnIndex])
    MENU_MISSION_COMMAND:New("No reaction",     self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, flightgroup, RAT.ROT.noreaction)
    MENU_MISSION_COMMAND:New("Passive defense", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, flightgroup, RAT.ROT.passive)
    MENU_MISSION_COMMAND:New("Evade on fire",   self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, flightgroup, RAT.ROT.evade)
    -- F10/RAT/<templatename>/Group X/
    MENU_MISSION_COMMAND:New("Despawn group",  self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._Despawn, self, group)
    MENU_MISSION_COMMAND:New("Place markers",  self.Menu[self.SubMenuName].groups[self.SpawnIndex], self._PlaceMarkers, self, waypoints, self.SpawnIndex)
    MENU_MISSION_COMMAND:New("Status report",  self.Menu[self.SubMenuName].groups[self.SpawnIndex], self.Status, self, true, self.SpawnIndex)
  end

  --- Function called when passing a waypoint.
  function flightgroup.OnAfterPassingWaypoint(Flightgroup, From, Event, To, Waypoint)
    local waypoint=Waypoint --Ops.OpsGroup#OPSGROUP.Waypoint
    local flightgroup=Flightgroup --Ops.FlightGroup#FLIGHTGROUP
    
    local wpid=waypoint.uid
    
    local ratcraft=self:_GetRatcraftFromGroup(flightgroup.group)
    
    local wpdescription=tostring(ratcraft.wpdesc[wpid])
    local wpstatus=ratcraft.wpstatus[wpid]
    
    -- Debug info.
    self:T(self.lid..string.format("RAT passed waypoint %s [uid=%d]: %s [status=%s]", waypoint.name, wpid, wpdescription, wpstatus))

    -- Set status    
    self:_SetStatus(group, wpstatus)
    
    if waypoint.uid==3 then
      --flightgroup:SelfDestruction(Delay,ExplosionPower,ElementName)    
    end
  
  end
  
  --- Function called when passing the FINAL waypoint
  function flightgroup.OnAfterPassedFinalWaypoint(flightgroup, From, Event, To)
  
    self:T(self.lid..string.format("RAT passed FINAL waypoint"))
    
    local ratcraft=self:_GetRatcraftFromGroup(flightgroup.group)
    
    -- Info message.
    local text=string.format("Flight %s arrived at final destination %s.", group:GetName(), destination:GetName())
    MESSAGE:New(text, 10):ToAllIf(self.reportstatus)
    self:T(self.lid..text)

    if landing==RAT.wp.air then
      -- Final waypoint is air ==> Despawn flight
    
      -- Info message.
      local text=string.format("Activating despawn switch for flight %s! Group will be detroyed soon.", group:GetName())
      MESSAGE:New(text, 10):ToAllIf(self.Debug)
      self:T(self.lid..text)
      
      -- Enable despawn switch. Next time the status function is called, the aircraft will be despawned.
      ratcraft.despawnme=true
    end    
    
  end
  
  --- Function called when flight is RTB.
  function flightgroup.OnAfterRTB(flightgroup, From, Event, To, airbase, SpeedTo, SpeedHold, SpeedLand)
    self:T(self.lid..string.format("RAT group is RTB"))
  end
  
  --- Function called when flight is holding.
  function flightgroup.OnAfterHolding(Flightgroup, From, Event, To)
    local flightgroup=Flightgroup --Ops.FlightGroup#FLIGHTGROUP
    
    local ratcraft=self:_GetRatcraftFromGroup(flightgroup.group)
    
    local destinationname=ratcraft.destination:GetName()
    
    -- Aircraft arrived at holding point
    local text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.", groupname, destinationname)
    self:T(self.lid..text)
    MESSAGE:New(text, 10):ToAllIf(self.reportstatus)

    -- Get FLIGHTCONTROL if there is any.    
    local fc=_DATABASE:GetFlightControl(destinationname)
    
    -- Register aircraft at RAT ATC (but only if there is no FLIGHTCONTROL)
    if self.ATCswitch and not fc then
      self:T(self.lid..string.format("RAT group is HOLDING ==> ATCRegisterFlight"))
    
      -- Create F10 menu
      if self.f10menu then       
        MENU_MISSION_COMMAND:New("Clear for landing", self.Menu[self.SubMenuName].groups[self.SpawnIndex], flightgroup.ClearToLand, flightgroup)
      end
      
      -- Register at ATC
      RAT._ATCRegisterFlight(groupname, timer.getTime())
    end
  end
  
  --- Function called when the group landed at an airbase.
  function flightgroup.OnAfterLanded(Flightgroup, From, Event, To, Airport)
    self:T(self.lid..string.format("RAT group landed at airbase"))
  end
  
  --- Function called when the group arrived at their parking positions.
  function flightgroup.OnAfterArrived(Flightgroup, From, Event, To)
    self:T(self.lid..string.format("RAT group arrived"))
  end
  
  --- Function called when a group got stuck.
  function flightgroup.OnAfterStuck(Flightgroup, From, Event, To, Stucktime)
    local flightgroup=Flightgroup --Ops.FlightGroup#FLIGHTGROUP
    self:T(self.lid..string.format("Group %s got stuck for %d seconds", flightgroup:GetName(), Stucktime))
    if Stucktime>10*60 then
      self:_Respawn(flightgroup.group)
    end
  
  end
  

  return self.SpawnIndex
end

--- Check if a given airbase has a FLIGHTCONTROL.
-- @param #RAT self
-- @param Wrapper.Airbase#AIRBASE airbase The airbase.
-- @return #boolean `true` if the airbase has a FLIGHTCONTROL.
function RAT:_IsFlightControlAirbase(airbase)
  
  if type(airbase)=="table" then
    airbase=airbase:GetName()
  end
  
  if airbase then
  
    local fc=_DATABASE:GetFlightControl(airbase)
    
    if fc then
      self:T(self.lid..string.format("Airbase %s has a FLIGHTCONTROL running", airbase))
      return true
    else
      return false
    end
  
  end
  
  return nil
end

--- Clear flight for landing. Sets tigger value to 1.
-- @param #RAT self
-- @param #string name Name of flight to be cleared for landing.
function RAT:ClearForLanding(name)
  trigger.action.setUserFlag(name, 1)
  local flagvalue=trigger.misc.getUserFlag(name)
  self:T(self.lid.."ATC: User flag value (landing) for "..name.." set to "..flagvalue)
end

--- Despawn the original group and re-spawn a new one.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group The group that should be respawned.
-- @param Core.Point#COORDINATE lastpos Last known position of the group.
-- @param #number delay Delay before despawn in seconds.
function RAT:_Respawn(group, lastpos, delay)

  if delay and delay>0 then
  
    self:ScheduleOnce(delay, RAT._Respawn, self, group, lastpos, 0)
  
  else
    if group then
      self:T(self.lid..string.format("Respawning ratcraft from group %s", group:GetName()))
    else
      self:E(self.lid..string.format("ERROR: group is nil in _Respawn!"))
      return nil
    end
    
    -- Get ratcraft from group.
    local ratcraft=self:_GetRatcraftFromGroup(group)

    -- Get last known position.    
    lastpos=lastpos or group:GetCoordinate()
  
    -- Get departure and destination from previous journey.
    local departure=ratcraft.departure
    local destination=ratcraft.destination   --Wrapper.Airbase#AIRBASE
    local takeoff=ratcraft.takeoff
    local landing=ratcraft.landing
    local livery=ratcraft.livery
    local lastwp=ratcraft.waypoints[#ratcraft.waypoints]
    local flightgroup=ratcraft.flightgroup
    
    
    -- In case we stay at the same airport, we save the parking data to respawn at the same spot.
    local parkingdata=nil
    if self.continuejourney or self.commute then
      for _,_element in pairs(flightgroup.elements) do
        local element=_element --Ops.OpsGroup#OPSGROUP.Element
            
        if element.parking then
          -- Init table.
          if parkingdata==nil then
            parkingdata={}
          end
          
          self:T(self.lid..string.format("Element %s was parking at spot id=%d", element.name, element.parking.TerminalID))
          table.insert(parkingdata, UTILS.DeepCopy(element.parking))
        else
          self:E(self.lid..string.format("WARNING: Element %s did NOT have a not parking spot!", tostring(element.name)))
        end
      end
    end
    
    -- Despawn old group.
    self:_Despawn(ratcraft.group)
  
    local _departure=nil
    local _destination=nil --Wrapper.Airbase#AIRBASE
    local _takeoff=nil
    local _landing=nil
    local _livery=nil
    local _lastwp=nil
    local _lastpos=nil
  
    if self.continuejourney then
    
      ---
      -- Continue Journey
      ---
  
      -- We continue our journey from the old departure airport.
      _departure=destination:GetName()
  
      -- Use the same livery for next aircraft.
      _livery=livery
  
      -- Last known position of the aircraft, which should be the sparking spot location.
      -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff.
      -- DONE: Need to think if continuejourney with respawn_after_takeoff actually makes sense?
      --       No, does not make sense. Disable it in consistency check.
      if landing==RAT.wp.landing and not (self.respawn_at_landing or self.respawn_after_takeoff) then
        -- Check that we have an airport or FARP but not a ship (which would be categroy 1).
        if destination:GetCategory()==4 then
          _lastpos=lastpos
        end
      end
  
      if self.destinationzone then
  
        -- Case: X --> Zone --> Zone --> Zone
        _takeoff=RAT.wp.air
        _landing=RAT.wp.air
  
      elseif self.returnzone then
  
        -- Case: X --> Zone --> X,  X --> Zone --> X
        -- We flew to a zone and back. Takeoff type does not change.
        _takeoff=self.takeoff
  
        -- If we took of in air we also want to land "in air".
        if self.takeoff==RAT.wp.air then
          _landing=RAT.wp.air
        else
          _landing=RAT.wp.landing
        end
  
        -- Departure stays the same. (The destination is the zone here.)
        _departure=departure:GetName()
  
      else
  
        -- Default case. Takeoff and landing type does not change.
        _takeoff=self.takeoff
        _landing=self.landing
  
      end
  
    elseif self.commute then
    
      ---
      -- Commute
      ---
    
      -- We commute between departure and destination.
  
      if self.starshape==true then
        if destination:GetName()==self.homebase then
          -- We are at our home base ==> destination is again randomly selected.
          _departure=self.homebase
          _destination=nil -- destination will be set anew
        else
          -- We are not a our home base ==> we fly back to our home base.
          _departure=destination:GetName()
          _destination=self.homebase
        end
      else
        -- Simply switch departure and destination.
        _departure=destination:GetName()
        _destination=departure:GetName()
      end
  
      -- Use the same livery for next aircraft.
      _livery=livery
  
      -- Last known position of the aircraft, which should be the sparking spot location.
      -- Note: we have to check that it was supposed to land and not respawned directly after landing or after takeoff.
      -- TODO: Need to think if commute with respawn_after_takeoff actually makes sense.
      if landing==RAT.wp.landing and lastpos and not (self.respawn_at_landing or self.respawn_after_takeoff) then
        -- Check that we have landed on an airport or FARP but not a ship (which would be categroy 1).
        if destination:GetCategory()==4 then
          _lastpos=lastpos
        end
      end
  
      -- Handle takeoff type.
      if self.destinationzone then
        -- self.takeoff is either RAT.wp.air or RAT.wp.cold
        -- self.landing is RAT.wp.Air
  
        if self.takeoff==RAT.wp.air then
  
          -- Case: Zone <--> Zone (both have takeoff air)
          _takeoff=RAT.wp.air  -- = self.takeoff (because we just checked)
          _landing=RAT.wp.air  -- = self.landing (because destinationzone)
  
        else
  
          -- Case: Airport <--> Zone
          if takeoff==RAT.wp.air then
            -- Last takeoff was air so we are at the airport now, takeoff is from ground.
            _takeoff=self.takeoff   -- must be either hot/cold/runway/hotcold
            _landing=RAT.wp.air     -- must be air = self.landing (because destinationzone)
          else
            -- Last takeoff was on ground so we are at a zone now ==> takeoff in air, landing at airport.
            _takeoff=RAT.wp.air
            _landing=RAT.wp.landing
          end
  
        end
  
      elseif self.returnzone then
  
        -- We flew to a zone and back. No need to swap departure and destination.
        _departure=departure:GetName()
        _destination=destination:GetName()
  
        -- Takeoff and landing should also not change.
        _takeoff=self.takeoff
        _landing=self.landing
  
      end
  
    end
  
    -- Take the last waypoint as initial waypoint for next plane.
    if _takeoff==RAT.wp.air and (self.continuejourney or self.commute) then
      _lastwp=lastwp
    end
  
    -- Debug
    self:T2({departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, lastwp=_lastwp})
  
    -- We should give it at least 3 sec since this seems to be the time until free parking spots after despawn are available again (Sirri Island test).
    local respawndelay=self.respawn_delay or 1
  
    -- Spawn new group.
    self:T(self.lid..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay))  
    self:ScheduleOnce(respawndelay, RAT._SpawnWithRoute, self,_departure,_destination,_takeoff,_landing,_livery, nil,_lastpos, nil, parkingdata)
    
  end

end

--- Despawn group. The `FLIGHTGROUP` is despawned and stopped. The ratcraft is removed from the self.ratcraft table. Menues are removed.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group Group to be despawned.
-- @param #number delay Delay in seconds before the despawn happens. Default is immidiately.
function RAT:_Despawn(group, delay)

  if delay and delay>0 then
    -- Delayed call.
    self:ScheduleOnce(delay, RAT._Despawn, self, group, 0)
  else

    if group then
  
      -- Get spawnindex of group.
      local index=self:GetSpawnIndexFromGroup(group)
  
      if index then
      
        -- Debug info.
        self:T(self.lid..string.format("Despawning group %s (index=%d)", group:GetName(), index))
      
        -- Get ratcraft.
        local ratcraft=self.ratcraft[index] --#RAT.RatCraft
        
        -- Despawn flightgroup and stop.
        ratcraft.flightgroup:Despawn()
        ratcraft.flightgroup:__Stop(0.1)
        
        -- Nil ratcraft in table.
        self.ratcraft[index].group=nil
        self.ratcraft[index]["status"]="Dead"        
        self.ratcraft[index]=nil
 
        -- Remove submenu for this group.
        if self.f10menu and self.SubMenuName ~= nil then
          self.Menu[self.SubMenuName]["groups"][index]:Remove()
        end
  
      end
    end
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set the route of the AI plane. Due to DCS landing bug, this has to be done before the unit is spawned.
-- @param #RAT self
-- @param #number takeoff Takeoff type. Could also be air start.
-- @param #number landing Landing type. Could also be a destination in air.
-- @param Wrapper.Airbase#AIRBASE _departure (Optional) Departure airbase.
-- @param Wrapper.Airbase#AIRBASE _destination (Optional) Destination airbase.
-- @param #table _waypoint Initial waypoint.
-- @return Wrapper.Airbase#AIRBASE Departure airbase.
-- @return Wrapper.Airbase#AIRBASE Destination airbase.
-- @return #table Table of flight plan waypoints.
-- @return #table Table of waypoint descriptions.
-- @return #table Table of waypoint status.
function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint)

  -- Max cruise speed.
  local VxCruiseMax
  if self.Vcruisemax then
    -- User input.
    VxCruiseMax = math.min(self.Vcruisemax, self.aircraft.Vmax)
  else
    -- Max cruise speed 90% of Vmax or 900 km/h whichever is lower.
    VxCruiseMax = math.min(self.aircraft.Vmax*0.90, 250)
  end

  -- Min cruise speed 70% of max cruise or 600 km/h whichever is lower.
  local VxCruiseMin = math.min(VxCruiseMax*0.70, 166)

  -- Cruise speed (randomized). Expectation value at midpoint between min and max.
  local VxCruise = UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax)

  -- Climb speed 90% ov Vmax but max 720 km/h.
  local VxClimb = math.min(self.aircraft.Vmax*0.90, 200)

  -- Descent speed 60% of Vmax but max 500 km/h.
  local VxDescent = math.min(self.aircraft.Vmax*0.60, 140)

  -- Holding speed is 90% of descent speed.
  local VxHolding = VxDescent*0.9

  -- Final leg is 90% of holding speed.
  local VxFinal = VxHolding*0.9

  -- Reasonably civil climb speed Vy=1500 ft/min = 7.6 m/s but max aircraft specific climb rate.
  local VyClimb=math.min(self.Vclimb*RAT.unit.ft2meter/60, self.aircraft.Vymax)

  -- Climb angle in rad.
  local AlphaClimb=math.asin(VyClimb/VxClimb)

  -- Descent angle in rad.
  local AlphaDescent=math.rad(self.AlphaDescent)

  -- Expected cruise level (peak of Gaussian distribution)
  local FLcruise_expect=self.FLcruise


  -- DEPARTURE AIRPORT
  -- Departure airport or zone.
  local departure=nil --Wrapper.Airbase#AIRBASE
  if _departure then
    if self:_AirportExists(_departure) then
      -- Check if new departure is an airport.
      departure=AIRBASE:FindByName(_departure)
      -- If we spawn in air, we convert departure to a zone.
      if takeoff == RAT.wp.air then
        departure=departure:GetZone()
      end
    elseif self:_ZoneExists(_departure) then
      -- If it's not an airport, check whether it's a zone.
      departure=ZONE:FindByName(_departure)
    else
      local text=string.format("ERROR! Specified departure airport %s does not exist for %s.", _departure, self.alias)
      self:E(self.lid..text)
    end

  else
    departure=self:_PickDeparture(takeoff)
    if self.commute and self.starshape==true and self.homebase==nil then
      self.homebase=departure:GetName()
    end
  end

  -- Return nil if no departure could be found.
  if not departure then
    local text=string.format("ERROR! No valid departure airport could be found for %s.", self.alias)
    self:E(self.lid..text)
    return nil
  end

  -- Coordinates of departure point.
  local Pdeparture  --Core.Point#COORDINATE
  if takeoff==RAT.wp.air then
    if _waypoint then
      -- Use coordinates of previous flight (commute or journey).
      Pdeparture=COORDINATE:New(_waypoint.x, _waypoint.alt, _waypoint.y)
    else
      -- For an air start, we take a random point within the spawn zone.
      local vec2=departure:GetRandomVec2()
      Pdeparture=COORDINATE:NewFromVec2(vec2)
      end
  else
    Pdeparture=departure:GetCoordinate()
  end

  -- Height ASL of departure point.
  local H_departure
  if takeoff==RAT.wp.air then
    -- Absolute minimum AGL
    local Hmin
    if self.category==RAT.cat.plane then
      Hmin=1000
    else
      Hmin=50
    end
    -- Departure altitude is 70% of default cruise with 30% variation and limited to 1000 m AGL (50 m for helos).
    H_departure=self:_Randomize(FLcruise_expect*0.7, 0.3, Pdeparture.y+Hmin, FLcruise_expect)
    if self.FLminuser then
      H_departure=math.max(H_departure,self.FLminuser)
    end
    -- Use alt of last flight.
    if _waypoint then
      H_departure=_waypoint.alt
    end
  else
    H_departure=Pdeparture.y
  end

  -- Adjust min distance between departure and destination for user set min flight level.
  local mindist=self.mindist
  if self.FLminuser then

    -- We can conly consider the symmetric case, because no destination selected yet.
    local hclimb=self.FLminuser-H_departure
    local hdescent=self.FLminuser-H_departure

    -- Minimum distance for l
    local Dclimb, Ddescent, Dtot=self:_MinDistance(AlphaClimb, AlphaDescent, hclimb, hdescent)

    if takeoff==RAT.wp.air and landing==RAT.wpair then
      mindist=0         -- Takeoff and landing are in air. No mindist required.
    elseif takeoff==RAT.wp.air then
      mindist=Ddescent  -- Takeoff in air. Need only space to descent.
    elseif landing==RAT.wp.air then
      mindist=Dclimb    -- Landing "in air". Need only space to climb.
    else
      mindist=Dtot      -- Takeoff and landing on ground. Need both space to climb and descent.
    end

    -- Mindist is at least self.mindist.
    mindist=math.max(self.mindist, mindist)

    local text=string.format("Adjusting min distance to %d km (for given min FL%03d)", mindist/1000, self.FLminuser/RAT.unit.FL2m)
    self:T(self.lid..text)
  end

  -- DESTINATION AIRPORT
  local destination=nil --Wrapper.Airbase#AIRBASE
  if _destination then

    if self:_AirportExists(_destination) then

      destination=AIRBASE:FindByName(_destination)
      if landing==RAT.wp.air or self.returnzone then
        destination=destination:GetZone()
      end

    elseif self:_ZoneExists(_destination) then
      destination=ZONE:FindByName(_destination)
    else
      local text=string.format("ERROR: Specified destination airport/zone %s does not exist for %s!", _destination, self.alias)
      self:E(self.lid.."ERROR: "..text)
    end

  else

    -- This handles the case where we have a journey and the first flight is done, i.e. _departure is set.
    -- If a user specified more than two destination airport explicitly, then we will stick to this.
    -- Otherwise, the route is random from now on.
    local random=self.random_destination
    if self.continuejourney and _departure and #self.destination_ports<3 then
      random=true
    end

    -- In case of a returnzone the destination (i.e. return point) is always a zone.
    local mylanding=landing
    local acrange=self.aircraft.Reff
    if self.returnzone then
      mylanding=RAT.wp.air
      acrange=self.aircraft.Reff/2  -- Aircraft needs to go to zone and back home.
    end

    -- Pick a destination airport.
    destination=self:_PickDestination(departure, Pdeparture, mindist, math.min(acrange, self.maxdist), random, mylanding)
  end

  -- Return nil if no departure could be found.
  if not destination then
    local text=string.format("No valid destination airport could be found for %s!", self.alias)
    MESSAGE:New(text, 60):ToAll()
    self:E(self.lid.."ERROR: "..text)
    return nil
  end

  -- Check that departure and destination are not the same. Should not happen due to mindist.
  if destination:GetName()==departure:GetName() then
    local text=string.format("%s: Destination and departure are identical. Airport/zone %s.", self.alias, destination:GetName())
    MESSAGE:New(text, 30):ToAll()
    self:E(self.lid.."ERROR: "..text)
  end

  -- Get a random point inside zone return zone.
  local Preturn
  local destination_returnzone
  if self.returnzone then
    -- Get a random point inside zone return zone.
    local vec2=destination:GetRandomVec2()
    Preturn=COORDINATE:NewFromVec2(vec2)
    -- Returnzone becomes destination.
    destination_returnzone=destination
    -- Set departure to destination.
    destination=departure
  end

  -- Get destination coordinate. Either in a zone or exactly at the airport.
  local Pdestination --Core.Point#COORDINATE
  if landing==RAT.wp.air then
    local vec2=destination:GetRandomVec2()
    Pdestination=COORDINATE:NewFromVec2(vec2)
  else
    Pdestination=destination:GetCoordinate()
  end

  -- Height ASL of destination airport/zone.
  local H_destination=Pdestination.y

  -- DESCENT/HOLDING POINT
  -- Get a random point between 5 and 10 km away from the destination.
  local Rhmin=8000
  local Rhmax=20000
  if self.category==RAT.cat.heli then
    -- For helos we set a distance between 500 to 1000 m.
    Rhmin=500
    Rhmax=1000
  end

  -- Coordinates of the holding point. y is the land height at that point.
  local Vholding=Pdestination:GetRandomVec2InRadius(Rhmax, Rhmin)
  local Pholding=COORDINATE:NewFromVec2(Vholding)

  -- AGL height of holding point.
  local H_holding=Pholding.y

  -- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL.
  local h_holding
  if self.category==RAT.cat.plane then
    h_holding=1200
  else
    h_holding=150
  end
  h_holding=self:_Randomize(h_holding, 0.2)

  -- This is the actual height ASL of the holding point we want to fly to
  local Hh_holding=H_holding+h_holding

  -- When we dont land, we set the holding altitude to the departure or cruise alt.
  -- This is used in the calculations.
  if landing==RAT.wp.air then
    Hh_holding=H_departure
  end

  -- Distance from holding point to final destination.
  local d_holding=Pholding:Get2DDistance(Pdestination)

  -- GENERAL
  local heading
  local d_total
  if self.returnzone then

    -- Heading from departure to destination in return zone.
    heading=self:_Course(Pdeparture, Preturn)

    -- Total distance to return zone and back.
    d_total=Pdeparture:Get2DDistance(Preturn) + Preturn:Get2DDistance(Pholding)

  else
    -- Heading from departure to holding point of destination.
    heading=self:_Course(Pdeparture, Pholding)

    -- Total distance between departure and holding point near destination.
    d_total=Pdeparture:Get2DDistance(Pholding)
  end

  -- Max height in case of air start, i.e. if we only would descent to holding point for the given distance.
  if takeoff==RAT.wp.air then
    local H_departure_max
    if landing==RAT.wp.air then
      H_departure_max = H_departure  -- If we fly to a zone, there is no descent necessary.
    else
      H_departure_max = d_total * math.tan(AlphaDescent) + Hh_holding
    end
    H_departure=math.min(H_departure, H_departure_max)
  end

  --------------------------------------------

  -- Height difference between departure and destination.
  local deltaH=math.abs(H_departure-Hh_holding)

  -- Slope between departure and destination.
  local phi = math.atan(deltaH/d_total)

  -- Adjusted climb/descent angles.
  local phi_climb
  local phi_descent
  if (H_departure > Hh_holding) then
    phi_climb=AlphaClimb+phi
    phi_descent=AlphaDescent-phi
  else
    phi_climb=AlphaClimb-phi
    phi_descent=AlphaDescent+phi
  end

  -- Total distance including slope.
  local D_total
  if self.returnzone then
    D_total = math.sqrt(deltaH*deltaH+d_total/2*d_total/2)
  else
    D_total = math.sqrt(deltaH*deltaH+d_total*d_total)
  end

  -- SSA triangle for sloped case.
  local gamma=math.rad(180)-phi_climb-phi_descent
  local a = D_total*math.sin(phi_climb)/math.sin(gamma)
  local b = D_total*math.sin(phi_descent)/math.sin(gamma)
  local hphi_max  = b*math.sin(phi_climb)
  local hphi_max2 = a*math.sin(phi_descent)

  -- Height of triangle.
  local h_max1 = b*math.sin(AlphaClimb)
  local h_max2 = a*math.sin(AlphaDescent)

  -- Max height relative to departure or destination.
  local h_max
  if (H_departure > Hh_holding) then
    h_max=math.min(h_max1, h_max2)
  else
    h_max=math.max(h_max1, h_max2)
  end

  -- Max flight level aircraft can reach for given angles and distance.
  local FLmax = h_max+H_departure

  --CRUISE
  -- Min cruise alt is just above holding point at destination or departure height, whatever is larger.
  local FLmin=math.max(H_departure, Hh_holding)

  -- For helicopters we take cruise alt between 50 to 1000 meters above ground. Default cruise alt is ~150 m.
  if self.category==RAT.cat.heli then
    FLmin=math.max(H_departure, H_destination)+50
    FLmax=math.max(H_departure, H_destination)+1000
  end

  -- Ensure that FLmax not above its service ceiling.
  FLmax=math.min(FLmax, self.aircraft.ceiling)

  -- Overrule setting if user specified min/max flight level explicitly.
  if self.FLminuser then
    FLmin=math.max(self.FLminuser, FLmin)  -- Still take care that we dont fly too high.
  end
  if self.FLmaxuser then
    FLmax=math.min(self.FLmaxuser, FLmax)  -- Still take care that we dont fly too low.
  end

  -- If the route is very short we set FLmin a bit lower than FLmax.
  if FLmin>FLmax then
    FLmin=FLmax
  end

  -- Expected cruise altitude - peak of gaussian distribution.
  if FLcruise_expect<FLmin then
    FLcruise_expect=FLmin
  end
  if FLcruise_expect>FLmax then
    FLcruise_expect=FLmax
  end

  -- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax.
  local FLcruise=UTILS.RandomGaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax)

  -- Overrule setting if user specified a flight level explicitly.
  if self.FLuser then
    FLcruise=self.FLuser
    -- Still cruise alt should be with parameters!
    FLcruise=math.max(FLcruise, FLmin)
    FLcruise=math.min(FLcruise, FLmax)
  end

  -- Climb and descent heights.
  local h_climb   = FLcruise - H_departure
  local h_descent = FLcruise - Hh_holding

  -- Distances.
  local d_climb   = h_climb/math.tan(AlphaClimb)
  local d_descent = h_descent/math.tan(AlphaDescent)
  local d_cruise  = d_total-d_climb-d_descent

  -- debug message
  local text=string.format("\n******************************************************\n")
  text=text..string.format("Template      =  %s\n",   self.SpawnTemplatePrefix)
  text=text..string.format("Alias         =  %s\n",   self.alias)
  text=text..string.format("Group name    =  %s\n\n", self:_AnticipatedGroupName())
  text=text..string.format("Speeds:\n")
  text=text..string.format("VxCruiseMin   = %6.1f m/s = %5.1f km/h\n", VxCruiseMin, VxCruiseMin*3.6)
  text=text..string.format("VxCruiseMax   = %6.1f m/s = %5.1f km/h\n", VxCruiseMax, VxCruiseMax*3.6)
  text=text..string.format("VxCruise      = %6.1f m/s = %5.1f km/h\n", VxCruise, VxCruise*3.6)
  text=text..string.format("VxClimb       = %6.1f m/s = %5.1f km/h\n", VxClimb, VxClimb*3.6)
  text=text..string.format("VxDescent     = %6.1f m/s = %5.1f km/h\n", VxDescent, VxDescent*3.6)
  text=text..string.format("VxHolding     = %6.1f m/s = %5.1f km/h\n", VxHolding, VxHolding*3.6)
  text=text..string.format("VxFinal       = %6.1f m/s = %5.1f km/h\n", VxFinal, VxFinal*3.6)
  text=text..string.format("VyClimb       = %6.1f m/s\n", VyClimb)
  text=text..string.format("\nDistances:\n")
  text=text..string.format("d_climb       = %6.1f km\n", d_climb/1000)
  text=text..string.format("d_cruise      = %6.1f km\n", d_cruise/1000)
  text=text..string.format("d_descent     = %6.1f km\n", d_descent/1000)
  text=text..string.format("d_holding     = %6.1f km\n", d_holding/1000)
  text=text..string.format("d_total       = %6.1f km\n", d_total/1000)
  text=text..string.format("\nHeights:\n")
  text=text..string.format("H_departure   = %6.1f m ASL\n", H_departure)
  text=text..string.format("H_destination = %6.1f m ASL\n", H_destination)
  text=text..string.format("H_holding     = %6.1f m ASL\n", H_holding)
  text=text..string.format("h_climb       = %6.1f m\n",     h_climb)
  text=text..string.format("h_descent     = %6.1f m\n",     h_descent)
  text=text..string.format("h_holding     = %6.1f m\n",     h_holding)
  text=text..string.format("delta H       = %6.1f m\n",     deltaH)
  text=text..string.format("FLmin         = %6.1f m ASL = FL%03d\n", FLmin, FLmin/RAT.unit.FL2m)
  text=text..string.format("FLcruise      = %6.1f m ASL = FL%03d\n", FLcruise, FLcruise/RAT.unit.FL2m)
  text=text..string.format("FLmax         = %6.1f m ASL = FL%03d\n", FLmax, FLmax/RAT.unit.FL2m)
  text=text..string.format("\nAngles:\n")
  text=text..string.format("Alpha climb   = %6.2f Deg\n",   math.deg(AlphaClimb))
  text=text..string.format("Alpha descent = %6.2f Deg\n",   math.deg(AlphaDescent))
  text=text..string.format("Phi (slope)   = %6.2f Deg\n",   math.deg(phi))
  text=text..string.format("Phi climb     = %6.2f Deg\n",   math.deg(phi_climb))
  text=text..string.format("Phi descent   = %6.2f Deg\n",   math.deg(phi_descent))
  if self.Debug then
    -- Max heights and distances if we would travel at FLmax.
    local h_climb_max   = FLmax - H_departure
    local h_descent_max = FLmax - Hh_holding
    local d_climb_max   = h_climb_max/math.tan(AlphaClimb)
    local d_descent_max = h_descent_max/math.tan(AlphaDescent)
    local d_cruise_max  = d_total-d_climb_max-d_descent_max
    text=text..string.format("Heading       = %6.1f Deg\n",   heading)
    text=text..string.format("\nSSA triangle:\n")
    text=text..string.format("D_total       = %6.1f km\n",  D_total/1000)
    text=text..string.format("gamma         = %6.1f Deg\n", math.deg(gamma))
    text=text..string.format("a             = %6.1f m\n",   a)
    text=text..string.format("b             = %6.1f m\n",   b)
    text=text..string.format("hphi_max      = %6.1f m\n",   hphi_max)
    text=text..string.format("hphi_max2     = %6.1f m\n",   hphi_max2)
    text=text..string.format("h_max1        = %6.1f m\n",   h_max1)
    text=text..string.format("h_max2        = %6.1f m\n",   h_max2)
    text=text..string.format("h_max         = %6.1f m\n",   h_max)
    text=text..string.format("\nMax heights and distances:\n")
    text=text..string.format("d_climb_max   = %6.1f km\n", d_climb_max/1000)
    text=text..string.format("d_cruise_max  = %6.1f km\n", d_cruise_max/1000)
    text=text..string.format("d_descent_max = %6.1f km\n", d_descent_max/1000)
    text=text..string.format("h_climb_max   = %6.1f m\n",  h_climb_max)
    text=text..string.format("h_descent_max = %6.1f m\n",  h_descent_max)
  end
  text=text..string.format("******************************************************\n")
  self:T2(self.lid..text)

  -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back.
  if d_cruise<0 then
    d_cruise=100
  end

  -- Waypoints and coordinates
  local wp={}
  local c={}
  local waypointdescriptions={}
  local waypointstatus={}
  local wpholding=nil
  local wpfinal=nil

  -- Departure/Take-off
  c[#c+1]=Pdeparture
  wp[#wp+1]=self:_Waypoint(#wp+1, "Departure", takeoff, c[#wp+1], VxClimb, H_departure, departure)
  waypointdescriptions[#wp]="Departure"
  waypointstatus[#wp]=RAT.status.Departure

  -- Climb
  if takeoff==RAT.wp.air then

    -- Air start.
    if d_climb < 5000 or d_cruise < 5000 then
      -- We omit the climb phase completely and add it to the cruise part.
      d_cruise=d_cruise+d_climb
    else
      -- Only one waypoint at the end of climb = begin of cruise.
      c[#c+1]=c[#c]:Translate(d_climb, heading)

      wp[#wp+1]=self:_Waypoint(#wp+1, "Begin of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise)
      waypointdescriptions[#wp]="Begin of Cruise"
      waypointstatus[#wp]=RAT.status.Cruise
    end

  else

    -- Ground start.
    c[#c+1]=c[#c]:Translate(d_climb/2, heading)
    c[#c+1]=c[#c]:Translate(d_climb/2, heading)

    wp[#wp+1]=self:_Waypoint(#wp+1, "Climb", RAT.wp.climb,  c[#wp+1], VxClimb, H_departure+(FLcruise-H_departure)/2)
    waypointdescriptions[#wp]="Climb"
    waypointstatus[#wp]=RAT.status.Climb

    wp[#wp+1]=self:_Waypoint(#wp+1, "Begin of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise)
    waypointdescriptions[#wp]="Begin of Cruise"
    waypointstatus[#wp]=RAT.status.Cruise

  end

  -- Cruise

  -- First add the little bit from begin of cruise to the return point.
  if self.returnzone then
    c[#c+1]=Preturn
    wp[#wp+1]=self:_Waypoint(#wp+1, "Return Zone", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise)
    waypointdescriptions[#wp]="Return Zone"
    waypointstatus[#wp]=RAT.status.Uturn
  end

  if landing==RAT.wp.air then

    -- Next waypoint is already the final destination.
    c[#c+1]=Pdestination
    wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", RAT.wp.finalwp, c[#wp+1], VxCruise,  FLcruise)
    waypointdescriptions[#wp]="Final Destination"
    waypointstatus[#wp]=RAT.status.Destination

  elseif self.returnzone then

    -- The little bit back to end of cruise.
    c[#c+1]=c[#c]:Translate(d_cruise/2, heading-180)
    wp[#wp+1]=self:_Waypoint(#wp+1, "End of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise,  FLcruise)
    waypointdescriptions[#wp]="End of Cruise"
    waypointstatus[#wp]=RAT.status.Descent

  else

    c[#c+1]=c[#c]:Translate(d_cruise, heading)
    wp[#wp+1]=self:_Waypoint(#wp+1, "End of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise,  FLcruise)
    waypointdescriptions[#wp]="End of Cruise"
    waypointstatus[#wp]=RAT.status.Descent

  end

  -- Descent (only if we acually want to land)
  if landing==RAT.wp.landing then
    if self.returnzone then
      c[#c+1]=c[#c]:Translate(d_descent/2, heading-180)
      wp[#wp+1]=self:_Waypoint(#wp+1, "Descent", RAT.wp.descent, c[#wp+1], VxDescent, FLcruise-(FLcruise-(h_holding+H_holding))/2)
      waypointdescriptions[#wp]="Descent"
      waypointstatus[#wp]=RAT.status.DescentHolding
    else
      c[#c+1]=c[#c]:Translate(d_descent/2, heading)
      wp[#wp+1]=self:_Waypoint(#wp+1, "Descent", RAT.wp.descent, c[#wp+1], VxDescent, FLcruise-(FLcruise-(h_holding+H_holding))/2)
      waypointdescriptions[#wp]="Descent"
      waypointstatus[#wp]=RAT.status.DescentHolding
    end
  end

  -- Holding and final destination.
  if landing==RAT.wp.landing then

    -- Holding point (removed the holding point because FLIGHTGROUP sends group to holding point with RTB command after the last waypoint)
--    c[#c+1]=Pholding
--    wp[#wp+1]=self:_Waypoint(#wp+1, "Holding Point", RAT.wp.holding, c[#wp+1], VxHolding, H_holding+h_holding)
--    waypointdescriptions[#wp]="Holding Point"
--    waypointstatus[#wp]=RAT.status.Holding
--    wpholding=#wp

    -- Final destination (leave this in because FLIGHTGROUP needs to know that we want to land and removes the landing waypoint automatically)
    c[#c+1]=Pdestination
    wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", landing, c[#wp+1], VxFinal, H_destination, destination)
    waypointdescriptions[#wp]="Final Destination"
    waypointstatus[#wp]=RAT.status.Destination

  end

  -- Final Waypoint
  wpfinal=#wp

  -- Fill table with waypoints.
  local waypoints={}
  for _,p in ipairs(wp) do
    table.insert(waypoints, p)
  end

  -- Some info on the route.
  self:_Routeinfo(waypoints, "Waypoint info in set_route:", waypointdescriptions)

  -- Return departure, destination and waypoints.
  if self.returnzone then
    -- We return the actual zone here because returning the departure leads to problems with commute.
    return departure, destination_returnzone, waypoints, waypointdescriptions, waypointstatus
  else
    return departure, destination, waypoints, waypointdescriptions, waypointstatus
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set the departure airport of the AI. If no airport name is given explicitly an airport from the coalition is chosen randomly.
-- If takeoff style is set to "air", we use zones around the airports or the zones specified by user input.
-- @param #RAT self
-- @param #number takeoff Takeoff type.
-- @return Wrapper.Airbase#AIRBASE Departure airport if spawning at airport.
-- @return Core.Zone#ZONE Departure zone if spawning in air.
function RAT:_PickDeparture(takeoff)

  -- Array of possible departure airports or zones.
  local departures={}

  if self.random_departure then

    -- Airports of friendly coalitions.
    for _,_airport in pairs(self.airports) do

      local airport=_airport --Wrapper.Airbase#AIRBASE

      local name=airport:GetName()
      if not self:_Excluded(name) then
        if takeoff==RAT.wp.air then

          table.insert(departures, airport:GetZone())  -- insert zone object.

        else

          -- Check if airbase has the right terminals.
          local nspots=1
          if self.termtype~=nil then
            nspots=airport:GetParkingSpotsNumber(self.termtype)
          end

          if nspots>0 then
            table.insert(departures, airport)            -- insert airport object.
          end
        end
      end

    end

  else

    -- Destination airports or zones specified by user.
    for _,name in pairs(self.departure_ports) do

      local dep=nil
      if self:_AirportExists(name) then
        if takeoff==RAT.wp.air then
          dep=AIRBASE:FindByName(name):GetZone()
        else
          dep=AIRBASE:FindByName(name)
          -- Check if the airport has a valid parking spot
          if self.termtype~=nil and dep~=nil then
            local _dep=dep --Wrapper.Airbase#AIRBASE
            local nspots=_dep:GetParkingSpotsNumber(self.termtype)
            if nspots==0 then
              dep=nil
            end
          end
        end
      elseif self:_ZoneExists(name) then
        if takeoff==RAT.wp.air then
          dep=ZONE:FindByName(name)
        else
          self:E(self.lid..string.format("ERROR! Takeoff is not in air. Cannot use %s as departure.", name))
        end
      else
        self:E(self.lid..string.format("ERROR: No airport or zone found with name %s.", name))
      end

      -- Add to departures table.
      if dep then
        table.insert(departures, dep)
      end

    end

  end

    -- Info message.
  self:T(self.lid..string.format("Number of possible departures for %s= %d", self.alias, #departures))

  -- Select departure airport or zone.
  local departure=departures[math.random(#departures)]

  local text
  if departure and departure:GetName() then
    if takeoff==RAT.wp.air then
      text=string.format("%s: Chosen departure zone: %s", self.alias, departure:GetName())
    else
      text=string.format("%s: Chosen departure airport: %s (ID %d)", self.alias, departure:GetName(), departure:GetID())
    end
    --MESSAGE:New(text, 30):ToAllIf(self.Debug)
    self:T(self.lid..text)
  else
    self:E(self.lid..string.format("ERROR! No departure airport or zone found for %s.", self.alias))
    departure=nil
  end

  return departure
end

--- Pick destination airport or zone depending on departure position.
-- @param #RAT self
-- @param Wrapper.Airbase#AIRBASE departure Departure airport or zone.
-- @param Core.Point#COORDINATE q Coordinate of the departure point.
-- @param #number minrange Minimum range to q in meters.
-- @param #number maxrange Maximum range to q in meters.
-- @param #boolean random Destination is randomly selected from friendly airport (true) or from destinations specified by user input (false).
-- @param #number landing Number indicating whether we land at a destination airport or fly to a zone object.
-- @return Wrapper.Airbase#AIRBASE destination Destination airport or zone.
function RAT:_PickDestination(departure, q, minrange, maxrange, random, landing)

  -- Min/max range to destination.
  minrange=minrange or self.mindist
  maxrange=maxrange or self.maxdist

  -- All possible destinations.
  local destinations={}

  if random then

    -- Airports of friendly coalitions.
    for _,_airport in pairs(self.airports) do
      local airport=_airport --Wrapper.Airbase#AIRBASE
      local name=airport:GetName()
      if self:_IsFriendly(name) and not self:_Excluded(name) and name~=departure:GetName() then

        -- Distance from departure to possible destination
        local distance=q:Get2DDistance(airport:GetCoordinate())

        -- Check if distance form departure to destination is within min/max range.
        if distance>=minrange and distance<=maxrange then
          if landing==RAT.wp.air then
            table.insert(destinations, airport:GetZone())  -- insert zone object.
          else
            -- Check if the requested terminal type is available.
            local nspot=1
            if self.termtype then
              nspot=airport:GetParkingSpotsNumber(self.termtype)
            end
            if nspot>0 then
              table.insert(destinations, airport)            -- insert airport object.
            end
          end
        end
      end
    end

  else

    -- Destination airports or zones specified by user.
    for _,name in pairs(self.destination_ports) do

      -- Make sure departure and destination are not identical.
      if name ~= departure:GetName() then

        local dest=nil
        if self:_AirportExists(name) then
          if landing==RAT.wp.air then
            dest=AIRBASE:FindByName(name):GetZone()
          else
            dest=AIRBASE:FindByName(name)
            -- Check if the requested terminal type is available.
            local nspot=1
            if self.termtype then
              nspot=dest:GetParkingSpotsNumber(self.termtype)
            end
            if nspot==0 then
              dest=nil
            end
          end
        elseif self:_ZoneExists(name) then
          if landing==RAT.wp.air then
            dest=ZONE:FindByName(name)
          else
            self:E(self.lid..string.format("ERROR! Landing is not in air. Cannot use zone %s as destination!", name))
          end
        else
          self:E(self.lid..string.format("ERROR! No airport or zone found with name %s", name))
        end

        if dest then
          -- Distance from departure to possible destination
          local distance=q:Get2DDistance(dest:GetCoordinate())

          -- Add as possible destination if zone is within range.
          if distance>=minrange and distance<=maxrange then
            table.insert(destinations, dest)
          else
            local text=string.format("Destination %s is ouside range. Distance = %5.1f km, min = %5.1f km, max = %5.1f km.", name, distance, minrange, maxrange)
            self:T(self.lid..text)
          end
        end

      end
    end
  end

  -- Info message.
  self:T(self.lid..string.format("Number of possible destinations = %s.", #destinations))

  if #destinations > 0 then
    --- Compare distance of destination airports.
    -- @param Core.Point#COORDINATE a Coordinate of point a.
    -- @param Core.Point#COORDINATE b Coordinate of point b.
    -- @return #list Table sorted by distance.
    local function compare(a,b)
      local qa=q:Get2DDistance(a:GetCoordinate())
      local qb=q:Get2DDistance(b:GetCoordinate())
      return qa < qb
    end
    table.sort(destinations, compare)
  else
    destinations=nil
  end


  -- Randomly select one possible destination.
  local destination
  if destinations and #destinations>0 then

    -- Random selection.
    destination=destinations[math.random(#destinations)] -- Wrapper.Airbase#AIRBASE

    -- Debug message.
    local text
    if landing==RAT.wp.air then
      text=string.format("%s: Chosen destination zone: %s.", self.alias, destination:GetName())
    else
      text=string.format("%s Chosen destination airport: %s (ID %d).", self.alias, destination:GetName(), destination:GetID())
    end
    self:T(self.lid..text)
    --MESSAGE:New(text, 30):ToAllIf(self.Debug)

  else
    self:E(self.lid.."ERROR! No destination airport or zone found.")
    destination=nil
  end

  -- Return the chosen destination.
  return destination

end

--- Find airports within a zone.
-- @param #RAT self
-- @param Core.Zone#ZONE zone
-- @return #list Table with airport names that lie within the zone.
function RAT:_GetAirportsInZone(zone)
  local airports={}
  for _,airport in pairs(self.airports) do
    local name=airport:GetName()
    local coord=airport:GetCoordinate()

    if zone:IsPointVec3InZone(coord) then
      table.insert(airports, name)
    end
  end
  return airports
end

--- Check if airport is excluded from possible departures and destinations.
-- @param #RAT self
-- @param #string port Name of airport, FARP or ship to check.
-- @return #boolean true if airport is excluded and false otherwise.
function RAT:_Excluded(port)
  for _,name in pairs(self.excluded_ports) do
    if name==port then
      return true
    end
  end
  return false
end

--- Check if airport is friendly, i.e. belongs to the right coalition.
-- @param #RAT self
-- @param #string port Name of airport, FARP or ship to check.
-- @return #boolean true if airport is friendly and false otherwise.
function RAT:_IsFriendly(port)
  for _,airport in pairs(self.airports) do
    local name=airport:GetName()
    if name==port then
      return true
    end
  end
  return false
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get all airports of the current map.
-- @param #RAT self
function RAT:_GetAirportsOfMap()
  local _coalition

  for i=0,2 do -- cycle coalition.side 0=NEUTRAL, 1=RED, 2=BLUE

    -- set coalition
    if i==0 then
      _coalition=coalition.side.NEUTRAL
    elseif i==1 then
      _coalition=coalition.side.RED
    elseif i==2 then
      _coalition=coalition.side.BLUE
    end

    -- get airbases of coalition
    local ab=coalition.getAirbases(i)

    -- loop over airbases and put them in a table
    for _,airbase in pairs(ab) do

      local _id=airbase:getID()
      local _p=airbase:getPosition().p
      local _name=airbase:getName()
      local _myab=AIRBASE:FindByName(_name)

      if _myab then

        -- Add airport to table.
        table.insert(self.airports_map, _myab)

        local text="MOOSE: Airport ID = ".._myab:GetID().." and Name = ".._myab:GetName()..", Category = ".._myab:GetCategory()..", TypeName = ".._myab:GetTypeName()
        self:T(self.lid..text)

      else

        self:E(self.lid..string.format("WARNING: Airbase %s does not exsist as MOOSE object!", tostring(_name)))

      end
    end

  end
end

--- Get all "friendly" airports of the current map. Fills the self.airports{} table.
-- @param #RAT self
function RAT:_GetAirportsOfCoalition()
  for _,coalition in pairs(self.ctable) do
    for _,_airport in pairs(self.airports_map) do
      local airport=_airport --Wrapper.Airbase#AIRBASE
      local category=airport:GetAirbaseCategory()
      if airport:GetCoalition()==coalition then

        -- Planes cannot land on FARPs.
        local condition1=self.category==RAT.cat.plane and category==Airbase.Category.HELIPAD

        -- Planes cannot land on ships.
        local condition2=self.category==RAT.cat.plane and category==Airbase.Category.SHIP

        if not (condition1 or condition2) then
          table.insert(self.airports, airport)
        end
      end
    end
  end

  if #self.airports==0 then
    local text=string.format("No possible departure/destination airports found for RAT %s.", tostring(self.alias))
    MESSAGE:New(text, 10):ToAll()
    self:E(self.lid..text)
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Report status of RAT groups.
-- @param #RAT self
-- @param #boolean message (Optional) Send message with report to all if true.
-- @param #number forID (Optional) Send message only for this ID.
function RAT:Status(message, forID)

  self:T(self.lid.."Checking status")

  -- Current time.
  local Tnow=timer.getTime()

  -- Alive counter.
  local nalive=0

  -- Loop over all ratcraft.
  for spawnindex,_ratcraft in pairs(self.ratcraft) do
    local ratcraft=_ratcraft --#RAT.RatCraft
  
    self:T(self.lid..string.format("Ratcraft Index=%s", tostring(spawnindex)))

    -- Get group.
    local group=ratcraft.group  --Wrapper.Group#GROUP

    if group and group:IsAlive() then
      nalive=nalive+1
      
      self:T(self.lid..string.format("Ratcraft Index=%s is ALIVE", tostring(spawnindex)))

      -- Gather some information.
      local prefix=self:_GetPrefixFromGroup(group)
      local life=self:_GetLife(group)
      local fuel=group:GetFuel()*100.0
      local airborne=group:InAir()
      local coords=group:GetCoordinate()
      local alt=coords~=nil and coords.y or 1000
      local departure=ratcraft.departure:GetName()
      local destination=ratcraft.destination:GetName()
      local type=self.aircraft.type
      local status=ratcraft.status
      local active=ratcraft.active
      local Nunits=ratcraft.nunits
      local N0units=group:GetInitialSize()

      -- Monitor travelled distance since last check.
      local Dtravel=0
      if coords and ratcraft.Pnow then
        local Dtravel=coords:Get2DDistance(ratcraft.Pnow)
        ratcraft.Pnow=coords
      end

      -- Add up the travelled distance.
      ratcraft.Distance=ratcraft.Distance+Dtravel

      -- Distance remaining to destination.
      local Ddestination=-1
      if ratcraft.Pnow then
        Ddestination=ratcraft.Pnow:Get2DDistance(ratcraft.destination:GetCoordinate())
      end

      -- Status report.
      if (forID and spawnindex==forID) or (not forID) then
        local text=string.format("ID %i of flight %s", spawnindex, prefix)
        if N0units>1 then
          text=text..string.format(" (%d/%d)\n", Nunits, N0units)
        else
          text=text.."\n"
        end
        if self.commute then
          text=text..string.format("%s commuting between %s and %s\n", type, departure, destination)
        elseif self.continuejourney then
          text=text..string.format("%s travelling from %s to %s (and continueing form there)\n", type, departure, destination)
        else
          text=text..string.format("%s travelling from %s to %s\n", type, departure, destination)
        end
        text=text..string.format("Status: %s", status)
        if airborne then
          text=text.." [airborne]\n"
        else
          text=text.." [on ground]\n"
        end
        text=text..string.format("Fuel    = %3.0f %%\n", fuel)
        text=text..string.format("Life    = %3.0f %%\n", life)
        text=text..string.format("FL%03d  = %i m ASL\n", alt/RAT.unit.FL2m, alt)
        text=text..string.format("Distance travelled = %6.1f km\n", ratcraft["Distance"]/1000)
        text=text..string.format("Distance to dest   = %6.1f km", Ddestination/1000)
        self:T(self.lid..text)
        if message then
          MESSAGE:New(text, 20):ToAll()
        end
      end

      -- Despawn groups after they have reached their destination zones.
      if ratcraft.despawnme then

        if self.norespawn or self.respawn_after_takeoff then
        
          -- Despawn old group.
          if self.despawnair then          
            self:T(self.lid..string.format("[STATUS despawnme] Flight %s will be despawned NOW and NO new group is created!", self.alias))            
            self:_Despawn(group)
          end
                
        else
        
          -- Despawn old group and respawn a new one.
          self:T(self.lid..string.format("[STATUS despawnme] Flight %s will be despawned NOW and a new group is respawned!", self.alias))
          self:_Respawn(group)
                
        end

      end

    else
      -- Group does not exist.
      local text=string.format("Group does not exist in loop ratcraft status for spawn index=%d", spawnindex)
      self:T2(self.lid..text)
      self:T2(ratcraft)
    end

  end

  -- Alive groups.
  local text=string.format("Alive groups of %s: %d, nalive=%d/%d", self.alias, self.alive, nalive, self.ngroups)
  self:T(self.lid..text)
  MESSAGE:New(text, 20):ToAllIf(message and not forID)

end

--- Remove ratcraft from self.ratcraft table.
-- @param #RAT self
-- @param #RAT.RatCraft ratcraft The ratcraft to be removed.
-- @return #RAT self
function RAT:_RemoveRatcraft(ratcraft)

  self.ratcraft[ratcraft.index]=nil

  return self
end

--- Get ratcraft from group.
-- @param #RAT self
-- @param Wrapper.Group#Group group The group object.
-- @return #RAT.RatCraft The ratcraft object.
function RAT:_GetRatcraftFromGroup(group)

  local index=self:GetSpawnIndexFromGroup(group)
  
  local ratcraft=self.ratcraft[index]

  return ratcraft
end

--- Get (relative) life of first unit of a group.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group Group of unit.
-- @return #number Life of unit in percent.
function RAT:_GetLife(group)
  local life=0.0
  if group and group:IsAlive() then
    local unit=group:GetUnit(1)
    if unit then
      life=unit:GetLife()/unit:GetLife0()*100
    else
      self:T2(self.lid.."ERROR! Unit does not exist in RAT_Getlife(). Returning zero.")
    end
  else
    self:T2(self.lid.."ERROR! Group does not exist in RAT_Getlife(). Returning zero.")
  end
  return life
end

--- Set status of group.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group Group.
-- @param #string status Status of group.
function RAT:_SetStatus(group, status)

  if group and group:IsAlive() then

    -- Get index from groupname.
    local ratcraft=self:_GetRatcraftFromGroup(group)

    if ratcraft then

      -- Set new status.
      ratcraft.status=status

      -- No status update message for "first waypoint", "holding"
      local no1 = status==RAT.status.Departure
      local no2 = status==RAT.status.EventBirthAir
      local no3 = status==RAT.status.Holding

      local text=string.format("Flight %s: %s", group:GetName(), status)
      self:T(self.lid..text)

      if not (no1 or no2 or no3) then
        MESSAGE:New(text, 10):ToAllIf(self.reportstatus)
      end

    end

  end
end

--- Get RatCraft from a given group.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group Group.
-- @return #RAT.RatCraft Rat craft object.
function RAT:_GetRatcraftFromGroup(group)

  if group then

    -- Get index from groupname.
    local index=self:GetSpawnIndexFromGroup(group)

    if self.ratcraft[index] then
      return self.ratcraft[index]
    end
    
  end
  
  return nil
end

--- Get status of group.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group Group.
-- @return #string status Status of group.
function RAT:GetStatus(group)

  if group and group:IsAlive() then

    -- Get index from groupname.
    local ratcraft=self:_GetRatcraftFromGroup(group)

    if ratcraft then

      -- Set new status.
      return ratcraft.status

    end

  end

  return "nonexistant"
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function is executed when a unit is spawned.
-- @param #RAT self
-- @param Core.Event#EVENTDATA EventData
function RAT:_OnBirth(EventData)
  self:F3(EventData)
  self:T3(self.lid.."Captured event birth!")

  local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP

  if SpawnGroup then

    -- Get the template name of the group. This can be nil if this was not a spawned group.
    local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup)

    -- Check that the template name actually belongs to this object.
    if EventPrefix and EventPrefix == self.alias then

      local text="Event: Group "..SpawnGroup:GetName().." was born."
      self:T(self.lid..text)

      -- Set status.
      local status="unknown in birth"
      if SpawnGroup:InAir() then
        status=RAT.status.EventBirthAir
      elseif self.uncontrolled then
        status=RAT.status.Uncontrolled
      else
        status=RAT.status.EventBirth
      end
      self:_SetStatus(SpawnGroup, status)        
      

      -- Get some info ablout this flight.
      local i=self:GetSpawnIndexFromGroup(SpawnGroup)
      
      local ratcraft=self.ratcraft[i] --#RAT.RatCraft
      
      local _departure=ratcraft.departure:GetName()
      local _destination=ratcraft.destination:GetName()
      local _nrespawn=ratcraft.nrespawn
      local _takeoff=ratcraft.takeoff
      local _landing=ratcraft.landing
      local _livery=ratcraft.livery

      -- Some is only useful for an actual airbase (not a zone).
      local _airbase=AIRBASE:FindByName(_departure)

      -- Check if aircraft group was accidentally spawned on the runway.
      -- This can happen due to no parking slots available and other DCS bugs.
      local onrunway=false
      if _airbase then
        -- Check that we did not want to spawn at a runway or in air.
        if self.checkonrunway and _takeoff ~= RAT.wp.runway and _takeoff ~= RAT.wp.air then
          onrunway=_airbase:CheckOnRunWay(SpawnGroup, self.onrunwayradius, false)
        end
      end

      -- Workaround if group was spawned on runway.
      if onrunway then

        -- Error message.
        local text=string.format("ERROR: RAT group of %s was spawned on runway. Group #%d will be despawned immediately!", self.alias, i)
        MESSAGE:New(text,30):ToAllIf(self.Debug)
        self:E(self.lid..text)
        if self.Debug then
          SpawnGroup:FlareRed()
        end

        -- Despawn the group.
        self:_Despawn(SpawnGroup)

        -- Try to respawn the group if there is at least another airport or random airport selection is used.
        if (self.Ndeparture_Airports>=2 or self.random_departure) and _nrespawn<self.onrunwaymaxretry then
          -- Increase counter.
          _nrespawn=_nrespawn+1

          -- This creates a completely new group, i.e. livery etc from earlier flights (continuejourney, commute) is not taken over.
          text=string.format("Try spawning new aircraft of group %s at another location. Attempt %d of max %d.", self.alias,_nrespawn,self.onrunwaymaxretry)
          MESSAGE:New(text,10):ToAllIf(self.Debug)
          self:T(self.lid..text)

          -- Spawn new group.
          self:_SpawnWithRoute(nil, nil, nil, nil, nil, nil, nil, _nrespawn)
        else
          -- This will respawn the same fight (maybe with a different route) but already in the air.
          -- Note: Uncontrolled aircraft are not respawned in air.
          if self.respawn_inair and not self.uncontrolled then
            text=string.format("Spawning new aircraft of group %s in air since no parking slot is available at %s.", self.alias, _departure)
            MESSAGE:New(text,10):ToAll()
            self:T(self.lid..text)

            -- Spawn new group at this airport but already in air.
            self:_SpawnWithRoute(_departure, _destination, RAT.wp.air, _landing, _livery)
          end
        end
      end -- end of workaround

      -- Check if any unit of the group was spawned on top of another unit in the MOOSE data base.
      local ontop=false
      if self.checkontop and (_airbase and _airbase:GetAirbaseCategory()==Airbase.Category.AIRDROME) then
        ontop=self:_CheckOnTop(SpawnGroup, self.ontopradius)
      end

      if ontop then
        local text=string.format("ERROR: Group of %s was spawned on top of another unit. Group #%d will be despawned immediately!", self.alias, i)
        MESSAGE:New(text,30):ToAllIf(self.Debug)
        self:T(self.lid..text)
        if self.Debug then
          SpawnGroup:FlareYellow()
        end
        -- Despawn group.
        self:_Despawn(SpawnGroup)
      end
      
    end
  else
    self:T2(self.lid.."ERROR: Group does not exist in RAT:_OnBirth().")
  end
end


--- Function is executed when a unit starts its engines.
-- @param #RAT self
-- @param Core.Event#EVENTDATA EventData
function RAT:_OnEngineStartup(EventData)
  self:F3(EventData)
  self:T3(self.lid.."Captured event EngineStartup!")

  local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP

  if SpawnGroup then

    -- Get the template name of the group. This can be nil if this was not a spawned group.
    local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup)

    -- Check that the template name actually belongs to this object.
    if EventPrefix and EventPrefix == self.alias then

      local text="Event: Group "..SpawnGroup:GetName().." started engines."
      self:T(self.lid..text)

      -- Set status.
      local status
      if SpawnGroup:InAir() then
        status=RAT.status.EventEngineStartAir
      else
        status=RAT.status.EventEngineStart
      end
      
      self:_SetStatus(SpawnGroup, status)

    end
    
  else
    self:T2(self.lid.."ERROR: Group does not exist in RAT:_EngineStartup().")
  end
end

--- Function is executed when a unit takes off.
-- @param #RAT self
-- @param Core.Event#EVENTDATA EventData
function RAT:_OnTakeoff(EventData)

  local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP

  if SpawnGroup then

    -- Get the template name of the group. This can be nil if this was not a spawned group.
    local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup)

    -- Check that the template name actually belongs to this object.
    if EventPrefix and EventPrefix == self.alias then

      local text="Event: Group "..SpawnGroup:GetName().." is airborne."
      self:T(self.lid..text)

      -- Set status.
      local status=RAT.status.EventTakeoff
      self:_SetStatus(SpawnGroup, status)

      if self.respawn_after_takeoff then
        text="Event: Group "..SpawnGroup:GetName().." will be respawned after takeoff."
        self:T(self.lid..text)

        -- Respawn group. We respawn with no parameters from the old flight.
        self:_SpawnWithRoute(nil, nil, nil, nil, nil, nil, nil, nil)
      end
      
    end
  else
    self:T2(self.lid.."ERROR: Group does not exist in RAT:_OnTakeoff().")
  end
end

--- Function is executed when a unit lands.
-- @param #RAT self
-- @param Core.Event#EVENTDATA EventData
function RAT:_OnLand(EventData)

  local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP

  if SpawnGroup then

    -- Get the template name of the group. This can be nil if this was not a spawned group.
    local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup)

    -- Check that the template name actually belongs to this object.
    if EventPrefix and EventPrefix == self.alias then

      local text="Event: Group "..SpawnGroup:GetName().." landed"
      self:T(self.lid..text)

      -- Set status.
      local status=RAT.status.EventLand
      self:_SetStatus(SpawnGroup, status)

      -- ATC plane landed. Take it out of the queue and set runway to free.
      if self.ATCswitch then
        RAT._ATCFlightLanded(SpawnGroup:GetName())
      end

      if self.respawn_at_landing and not self.norespawn then
        text="Event: Group "..SpawnGroup:GetName().." will be respawned"
        self:T(self.lid..text)

        -- Respawn group.
        self:_Respawn(SpawnGroup)
      end
      
    end

  else
    self:T2(self.lid.."ERROR: Group does not exist in RAT:_OnLand()")
  end
end

--- Function is executed when a unit shuts down its engines.
-- @param #RAT self
-- @param Core.Event#EVENTDATA EventData
function RAT:_OnEngineShutdown(EventData)
  self:F3(EventData)
  self:T3(self.lid.."Captured event EngineShutdown!")

  local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP

  if SpawnGroup then

    -- Get the template name of the group. This can be nil if this was not a spawned group.
    local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup)

    -- Check that the template name actually belongs to this object.
    if EventPrefix and EventPrefix == self.alias then

      -- Despawn group only if it on the ground.
      if not SpawnGroup:InAir() then

        -- Current status.
        local currentstate=self:GetStatus(SpawnGroup)

        local text=string.format("Event: Unit %s of group %s shut down its engines. Current state %s.", EventData.IniUnitName, SpawnGroup:GetName(), currentstate)
        self:T(self.lid..text)

        -- Check that this is not the second unit of the group so that we dont trigger re- and despawns twice.
        if currentstate~=RAT.status.EventEngineShutdown and currentstate~="Dead" then

          -- Set status.
          local status=RAT.status.EventEngineShutdown
          self:_SetStatus(SpawnGroup, status)

          if not self.respawn_at_landing and not self.norespawn then
            text=string.format("Event: Group %s will be respawned. Current state %s => new state %s.", SpawnGroup:GetName(), currentstate, status)
            self:T(self.lid..text)

            -- Respawn group.
            self:_Respawn(SpawnGroup, nil, 3)
            
          else

            -- Despawn group.
            text="Event: Group "..SpawnGroup:GetName().." will be destroyed now"
            self:T(self.lid..text)
            self:_Despawn(SpawnGroup)
          
          end


        end

      end
    end

  else
    self:T2(self.lid.."ERROR: Group does not exist in RAT:_OnEngineShutdown().")
  end
end

--- Function is executed when a unit is hit.
-- @param #RAT self
-- @param Core.Event#EVENTDATA EventData
function RAT:_OnHit(EventData)
  self:F3(EventData)
  self:T(self.lid..string.format("Captured event Hit by %s! Initiator %s. Target %s", self.alias, tostring(EventData.IniUnitName), tostring(EventData.TgtUnitName)))

  local SpawnGroup = EventData.TgtGroup --Wrapper.Group#GROUP

  if SpawnGroup then

    -- Get the template name of the group. This can be nil if this was not a spawned group.
    local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup)

    -- Check that the template name actually belongs to this object.
    if EventPrefix and EventPrefix == self.alias then
      -- Debug info.
      self:T(self.lid..string.format("Event: Group %s was hit. Unit %s.", SpawnGroup:GetName(), tostring(EventData.TgtUnitName)))

      local text=string.format("%s, unit %s was hit!", self.alias, EventData.TgtUnitName)
      MESSAGE:New(text, 10):ToAllIf(self.reportstatus or self.Debug)
    end
  end
end

--- Function is executed when a unit is dead or crashes.
-- @param #RAT self
-- @param Core.Event#EVENTDATA EventData
function RAT:_OnDeadOrCrash(EventData)
  self:F3(EventData)
  self:T3(self.lid.."Captured event DeadOrCrash!")

  local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP

  if SpawnGroup then

    -- Get the template name of the group. This can be nil if this was not a spawned group.
    local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup)

    if EventPrefix then

      -- Check that the template name actually belongs to this object.
      if EventPrefix == self.alias then

        -- Decrease group alive counter.
        self.alive=self.alive-1

        -- Debug info.
        local text=string.format("Event: Group %s crashed or died. Alive counter = %d.", SpawnGroup:GetName(), self.alive)
        self:T(self.lid..text)

        -- Split crash and dead events.
        if EventData.id == world.event.S_EVENT_CRASH  then

          -- Call crash event. This handles when a group crashed or
          self:_OnCrash(EventData)

        elseif EventData.id == world.event.S_EVENT_DEAD  then

          -- Call dead event.
          self:_OnDead(EventData)

        end
      end
    end
  end
end

--- Function is executed when a unit is dead.
-- @param #RAT self
-- @param Core.Event#EVENTDATA EventData
function RAT:_OnDead(EventData)
  self:F3(EventData)
  self:T3(self.lid.."Captured event Dead!")

  local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP

  if SpawnGroup then

    -- Get the template name of the group. This can be nil if this was not a spawned group.
    local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup)

    if EventPrefix then

      -- Check that the template name actually belongs to this object.
      if EventPrefix == self.alias then

        local text=string.format("Event: Group %s died. Unit %s.", SpawnGroup:GetName(), EventData.IniUnitName)
        self:T(self.lid..text)

        -- Set status.
        local status=RAT.status.EventDead
        self:_SetStatus(SpawnGroup, status)

      end
    end

  else
    self:T2(self.lid.."ERROR: Group does not exist in RAT:_OnDead().")
  end
end

--- Function is executed when a unit crashes.
-- @param #RAT self
-- @param Core.Event#EVENTDATA EventData
function RAT:_OnCrash(EventData)
  self:F3(EventData)
  self:T3(self.lid.."Captured event Crash!")

  local SpawnGroup = EventData.IniGroup --Wrapper.Group#GROUP

  if SpawnGroup then

    -- Get the template name of the group. This can be nil if this was not a spawned group.
    local EventPrefix = self:_GetPrefixFromGroup(SpawnGroup)

    -- Check that the template name actually belongs to this object.
    if EventPrefix and EventPrefix == self.alias then

      -- Get ratcraft object of this group.
      local ratcraft=self:_GetRatcraftFromGroup(SpawnGroup)
      
      if ratcraft then

        -- Update number of alive units in the group.
        ratcraft.nunits=ratcraft.nunits-1
        
        -- Number of initial units.
        local _n0=SpawnGroup:GetInitialSize()
  
        -- Debug info.
        local text=string.format("Event: Group %s crashed. Unit %s. Units still alive %d of %d.", SpawnGroup:GetName(), EventData.IniUnitName, ratcraft.nunits, _n0)
        self:T(self.lid..text)
  
        -- Set status.
        local status=RAT.status.EventCrash
        self:_SetStatus(SpawnGroup, status)
  
        -- Respawn group if all units are dead.
        if ratcraft.nunits==0 and self.respawn_after_crash and not self.norespawn then
        
          -- Debug info.
          self:T(self.lid..string.format("No units left of group %s. Group will be respawned now.", SpawnGroup:GetName()))
                    
          -- Respawn group.
          self:_Respawn(SpawnGroup)
        end
        
      else
        self:E(self.lid..string.format("ERROR: Could not find ratcraft object for crashed group %s!", SpawnGroup:GetName()))
      end

    end

  else
    if self.Debug then
      self:E(self.lid.."ERROR: Group does not exist in RAT:_OnCrash()!")
    end
  end
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a waypoint that can be used with the Route command.
-- @param #RAT self
-- @param #number index Running index of waypoints. Starts with 1 which is normally departure/spawn waypoint.
-- @param #string description Descrition of Waypoint.
-- @param #number Type Type of waypoint.
-- @param Core.Point#COORDINATE Coord 3D coordinate of the waypoint.
-- @param #number Speed Speed in m/s.
-- @param #number Altitude Altitude in m.
-- @param Wrapper.Airbase#AIRBASE Airport Airport of object to spawn.
-- @return #table Waypoints for DCS task route or spawn template.
function RAT:_Waypoint(index, description, Type, Coord, Speed, Altitude, Airport)

  -- Altitude of input parameter or y-component of 3D-coordinate.
  local _Altitude=Altitude or Coord.y

  -- Land height at given coordinate.
  local Hland=Coord:GetLandHeight()

  -- convert type and action in DCS format
  local _Type=nil
  local _Action=nil
  local _alttype="RADIO"

  if Type==RAT.wp.cold then
    -- take-off with engine off
    _Type="TakeOffParking"
    _Action="From Parking Area"
    _Altitude = 10
    _alttype="RADIO"
  elseif Type==RAT.wp.hot then
    -- take-off with engine on
    _Type="TakeOffParkingHot"
    _Action="From Parking Area Hot"
    _Altitude = 10
    _alttype="RADIO"
  elseif Type==RAT.wp.runway then
    -- take-off from runway
    _Type="TakeOff"
    _Action="From Parking Area"
    _Altitude = 10
    _alttype="RADIO"
  elseif Type==RAT.wp.air then
    -- air start
    _Type="Turning Point"
    _Action="Turning Point"
    _alttype="BARO"
  elseif Type==RAT.wp.climb then
    _Type="Turning Point"
    _Action="Turning Point"
    _alttype="BARO"
  elseif Type==RAT.wp.cruise then
    _Type="Turning Point"
    _Action="Turning Point"
    _alttype="BARO"
  elseif Type==RAT.wp.descent then
    _Type="Turning Point"
    _Action="Turning Point"
    _alttype="BARO"
  elseif Type==RAT.wp.holding then
    _Type="Turning Point"
    _Action="Turning Point"
    --_Action="Fly Over Point"
    _alttype="BARO"
  elseif Type==RAT.wp.landing then
    _Type="Land"
    _Action="Landing"
    _Altitude = 10
    _alttype="RADIO"
  elseif Type==RAT.wp.finalwp then
    _Type="Turning Point"
    --_Action="Fly Over Point"
    _Action="Turning Point"
    _alttype="BARO"
  else
    self:E(self.lid.."ERROR: Unknown waypoint type in RAT:Waypoint() function!")
    _Type="Turning Point"
    _Action="Turning Point"
    _alttype="RADIO"
  end

  -- some debug info about input parameters
  local text=string.format("\n******************************************************\n")
  text=text..string.format("Waypoint =  %d\n", index)
  text=text..string.format("Template =  %s\n", self.SpawnTemplatePrefix)
  text=text..string.format("Alias    =  %s\n", self.alias)
  text=text..string.format("Type: %i - %s\n", Type, _Type)
  text=text..string.format("Action: %s\n", _Action)
  text=text..string.format("Coord: x = %6.1f km, y = %6.1f km, alt = %6.1f m\n", Coord.x/1000, Coord.z/1000, Coord.y)
  text=text..string.format("Speed = %6.1f m/s = %6.1f km/h = %6.1f knots\n", Speed, Speed*3.6, Speed*1.94384)
  text=text..string.format("Land     = %6.1f m ASL\n", Hland)
  text=text..string.format("Altitude = %6.1f m (%s)\n", _Altitude, _alttype)
  if Airport then
    if Type==RAT.wp.air then
      text=text..string.format("Zone = %s\n", Airport:GetName())
    else
      --text=text..string.format("Airport = %s with ID %i\n", Airport:GetName(), Airport:GetID())
      text=text..string.format("Airport = %s\n", Airport:GetName())
    end
  else
    text=text..string.format("No airport/zone specified\n")
  end
  text=text.."******************************************************\n"
  self:T2(self.lid..text)

  -- define waypoint
  local RoutePoint = {}
  -- coordinates and altitude
  RoutePoint.x = Coord.x
  RoutePoint.y = Coord.z
  RoutePoint.alt = _Altitude
  -- altitude type: BARO=ASL or RADIO=AGL
  RoutePoint.alt_type = _alttype
  -- type
  RoutePoint.type = _Type
  RoutePoint.action = _Action
  -- speed in m/s
  RoutePoint.speed = Speed
  RoutePoint.speed_locked = true
  -- ETA (not used)
  RoutePoint.ETA=nil
  RoutePoint.ETA_locked = false
  -- waypoint description
  RoutePoint.name=description

  if (Airport~=nil) and (Type~=RAT.wp.air) then
    local AirbaseID = Airport:GetID()
    local AirbaseCategory = Airport:GetAirbaseCategory()
    if AirbaseCategory == Airbase.Category.SHIP then
      RoutePoint.linkUnit = AirbaseID
      RoutePoint.helipadId = AirbaseID
    elseif AirbaseCategory == Airbase.Category.HELIPAD then
      RoutePoint.linkUnit = AirbaseID
      RoutePoint.helipadId = AirbaseID
    elseif AirbaseCategory == Airbase.Category.AIRDROME then
      RoutePoint.airdromeId = AirbaseID
    else
      self:T(self.lid.."Unknown Airport category in _Waypoint()!")
    end
  end

  -- Return waypoint.
  return RoutePoint
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Provide information about the assigned flightplan.
-- @param #RAT self
-- @param #table waypoints Waypoints of the flight plan.
-- @param #string comment Some comment to identify the provided information.
-- @param #table waypointdescriptions Waypoint descriptions.
-- @return #number total Total route length in meters.
function RAT:_Routeinfo(waypoints, comment, waypointdescriptions)
  local text=string.format("\n******************************************************\n")
  text=text..string.format("Template =  %s\n", self.SpawnTemplatePrefix)
  if comment then
    text=text..comment.."\n"
  end
  text=text..string.format("Number of waypoints = %i\n", #waypoints)
  -- info on coordinate and altitude
  for i=1,#waypoints do
    local p=waypoints[i]
    text=text..string.format("WP #%i: x = %6.1f km, y = %6.1f km, alt = %6.1f m  %s\n", i-1, p.x/1000, p.y/1000, p.alt, waypointdescriptions[i])
  end
  -- info on distance between waypoints
  local total=0.0
  for i=1,#waypoints-1 do
    local point1=waypoints[i]
    local point2=waypoints[i+1]
    local x1=point1.x
    local y1=point1.y
    local x2=point2.x
    local y2=point2.y
    local d=math.sqrt((x1-x2)^2 + (y1-y2)^2)
    local heading=self:_Course(point1, point2)
    total=total+d
    text=text..string.format("Distance from WP %i-->%i = %6.1f km. Heading = %03d :  %s - %s\n", i-1, i, d/1000, heading, waypointdescriptions[i], waypointdescriptions[i+1])
  end
  text=text..string.format("Total distance = %6.1f km\n", total/1000)
  text=text..string.format("******************************************************\n")

  -- Debug info.
  self:T2(self.lid..text)

  -- return total route length in meters
  return total
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Anticipated group name from alias and spawn index.
-- @param #RAT self
-- @param #number index Spawnindex of group if given or self.SpawnIndex+1 by default.
-- @return #string Name the group will get after it is spawned.
function RAT:_AnticipatedGroupName(index)
  local index=index or self.SpawnIndex+1
  return string.format("%s#%03d", self.alias, index)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Randomly activates an uncontrolled aircraft.
-- @param #RAT self
function RAT:_ActivateUncontrolled()

  -- Spawn indices of uncontrolled inactive aircraft.
  local idx={}
  local rat={}

  -- Number of active aircraft.
  local nactive=0

  -- Loop over RAT groups and count the active ones.
  for spawnindex,_ratcraft in pairs(self.ratcraft) do
    local ratcraft=_ratcraft --#RAT.RatCraft

    local group=ratcraft.group --Wrapper.Group#GROUP

    if group and group:IsAlive() then

      local text=string.format("Uncontrolled: Group = %s (spawnindex = %d), active = %s", ratcraft.group:GetName(), spawnindex, tostring(ratcraft.active))
      self:T2(self.lid..text)

      if ratcraft.active then
        nactive=nactive+1
      else
        table.insert(idx, spawnindex)
      end

    end
  end

  -- Debug message.
  local text=string.format("Uncontrolled: Ninactive = %d,  Nactive = %d (of max %d)", #idx, nactive, self.activate_max)
  self:T(self.lid..text)

  if #idx>0 and nactive<self.activate_max then

    -- Randomly pick on group, which is activated.
    local index=idx[math.random(#idx)]
    
    local ratcraft=self.ratcraft[index] --#RAT.RatCraft

    -- Get corresponding group.
    local group=ratcraft.group --Wrapper.Group#GROUP
    
    -- Get departure airbase (if exists)
    local departureAirbase=self:_GetDepartureAirbase(ratcraft)
    
    -- Get FLIGHTCONTROL of departure airbase (if it exists)
    local departureFlightControl=nil
    if departureAirbase then
      departureFlightControl=_DATABASE:GetFlightControl(departureAirbase:GetName())      
    end
        
    if departureFlightControl then
      -- Group is ready to taxi. FLIGHCONTROL will start
      self:T(self.lid..string.format("RAT group %s is ready for takeoff", group:GetName()))
      ratcraft.flightgroup:SetReadyForTakeoff(true)      
    else
      -- Start aircraft.
      self:T(self.lid..string.format("RAT group %s is switching to controlled now", group:GetName()))
      ratcraft.flightgroup:StartUncontrolled()
    end
    
    -- Set status to active.
    ratcraft.active=true
    
    -- Set status to "Ready and Starting Engines".
    self:_SetStatus(group, RAT.status.EventBirth)    
  end

end

--- Set RAT group to (in-)visible for other AI forces.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group Group to be set (in)visible.
-- @param #boolean switch If true, the group is invisible. If false the group will be visible.
function RAT:_CommandInvisible(group,switch)

  -- Command structure for setting groups to invisible.
  local SetInvisible = {id = 'SetInvisible', params = {value = switch}}

  -- Execute command.
  group:SetCommand(SetInvisible)
end

--- Set RAT group to be (im-)mortal.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group Group to be set (im-)mortal.
-- @param #boolean switch True enables immortality, false disables it.
function RAT:_CommandImmortal(group, switch)

  -- Command structure for setting groups to invisible.
  local SetImmortal = {id = 'SetImmortal', params = {value = switch}}

  -- Execute command.
  group:SetCommand(SetImmortal)
end


--- Get aircraft dimensions length, width, height.
-- @param #RAT self
-- @param Wrapper.Unit#UNIT unit The unit which is we want the size of.
-- @return #number Size, i.e. max(length,width) of unit.
function RAT:_GetObjectSize(unit)
  local DCSunit=unit:GetDCSObject()
  if DCSunit then
    local DCSdesc=DCSunit:getDesc()
    -- dimensions
    local length=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x)
    local height=DCSdesc.box.max.y+math.abs(DCSdesc.box.max.y)
    local width =DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z)
    return math.max(length,width)
  end
  return nil
end

--- Find aircraft that have accidentally been spawned on top of each other.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group Units of this group will be checked.
-- @param #number distmin Allowed distance in meters between units. Units with a distance smaller than this number are considered to be on top of each other.
-- @return #boolean True if group was destroyed because it was on top of another unit. False if otherwise.
function RAT:_CheckOnTop(group, distmin)

  distmin=distmin or 2

  for i,uniti in pairs(group:GetUnits()) do
    local uniti=uniti --Wrapper.Unit#UNIT

    if uniti then

      local namei=uniti:GetName()

      for j,unitj in pairs(_DATABASE.UNITS) do

        if unitj then
          local unitj=unitj --Wrapper.Unit#UNIT
          local namej=unitj:GetName()

          if namei ~= namej then

            local DCSuniti=uniti:GetDCSObject()
            local DCSunitj=unitj:GetDCSObject()

            if DCSuniti and DCSuniti:isExist() and DCSunitj and DCSunitj:isExist() then

              -- Distance between units.
              local _dist=uniti:GetCoordinate():Get3DDistance(unitj:GetCoordinate())

              -- Check for min distance.
              if _dist < distmin then
                if (not uniti:InAir()) and (not unitj:InAir()) then
                  -- Trigger immidiate destuction of unit.
                  self:T(self.lid..string.format("Unit %s is on top of unit %s. Distance %.2f m.", namei, namej,_dist))
                  return true
                end
              end

            end -- if DCSunit exists
          end -- if namei==namej then
        end --if unitj then
      end -- for j, unitj
    end -- if uniti then
  end -- for i,uniti in

  return false
end


--- Calculate minimum distance between departure and destination for given minimum flight level and climb/decent rates.
-- @param #RAT self
-- @param #number alpha Angle of climb [rad].
-- @param #number beta Angle of descent [rad].
-- @param #number ha Height difference between departure and cruise altiude.
-- @param #number hb Height difference between cruise altitude and destination.
-- @return #number d1 Minimum distance for climb phase to reach cruise altitude.
-- @return #number d2 Minimum distance for descent phase to reach destination height.
-- @return #number dtot Minimum total distance to climb and descent.
function RAT:_MinDistance(alpha, beta, ha, hb)
  local d1=ha/math.tan(alpha)
  local d2=hb/math.tan(beta)
  return d1, d2, d1+d2
end


--- Add names of all friendly airports to possible departure or destination airports if they are not already in the list.
-- @param #RAT self
-- @param #table ports List of departure or destination airports/zones that will be added.
function RAT:_AddFriendlyAirports(ports)
  for _,airport in pairs(self.airports) do
    if not self:_NameInList(ports, airport:GetName()) then
      table.insert(ports, airport:GetName())
    end
  end
end

--- Check if a name/string is in a list or not.
-- @param #RAT self
-- @param #table liste List of names to be checked.
-- @param #string name Name to be checked for.
function RAT:_NameInList(liste, name)
  for _,item in pairs(liste) do
    if item==name then
      return true
    end
  end
  return false
end

--- Get destination airbase of a given ratcraft.
-- @param #RAT self
-- @param #RAT.RatCraft ratcraft Ratcraft object.
-- @return Wrapper.Airbase#AIRBASE Destination airbase or nil.
function RAT:_GetDestinationAirbase(ratcraft)

  if ratcraft and ratcraft.destination then
  
    local airbase=AIRBASE:FindByName(ratcraft.destination)
    
    return airbase
  
  end

  return nil
end


--- Get departure airbase of a given ratcraft.
-- @param #RAT self
-- @param #RAT.RatCraft ratcraft Ratcraft object.
-- @return Wrapper.Airbase#AIRBASE Destination airbase or nil.
function RAT:_GetDepartureAirbase(ratcraft)

  if ratcraft and ratcraft.departure then
  
    local name=ratcraft.departure:GetName()  
    local airbase=AIRBASE:FindByName(name)
    
    return airbase  
  end

  return nil
end




--- Test if an airport exists on the current map.
-- @param #RAT self
-- @param #string name
-- @return #boolean True if airport exsits, false otherwise.
function RAT:_AirportExists(name)
  for _,airport in pairs(self.airports_map) do
    if airport:GetName()==name then
      return true
    end
  end
  return false
end

--- Test if a zone exists.
-- @param #RAT self
-- @param #string name
-- @return #boolean True if zone exsits, false otherwise.
function RAT:_ZoneExists(name)
  local z=ZONE:FindByName(name) --trigger.misc.getZone(name) as suggested by @Viking on MOOSE discord #rat
  if z then
    return true
  end
  return false
end

--- Set ROE for a group.
-- @param #RAT self
-- @param Ops.FlightGroup#FLIGHTGROUP flightgroup Group for which the ROE is set.
-- @param #string roe ROE of group.
function RAT:_SetROE(flightgroup, roe)
  
  roe=roe or self.roe
  
  self:T(self.lid.."Setting ROE to "..roe.." for group "..flightgroup:GetName())  
  
  local _roe=ENUMS.ROE.WeaponHold
  if roe==RAT.ROE.returnfire then
    _roe=ENUMS.ROE.ReturnFire
  elseif roe==RAT.ROE.weaponfree then
    _roe=ENUMS.ROE.OpenFireWeaponFree
  else
    
  end
  
  flightgroup:SetDefaultROE(_roe)
  flightgroup:SwitchROE(_roe)
  
end


--- Set ROT for a group.
-- @param #RAT self
-- @param Ops.FlightGroup#FLIGHTGROUP flightgroup Group for which the ROT is set.
-- @param #string rot ROT of group.
function RAT:_SetROT(flightgroup, rot)

  rot=rot or self.rot
  
  self:T(self.lid.."Setting ROT to "..rot.." for group "..flightgroup:GetName())
  
  local _rot=ENUMS.ROT.NoReaction
  if rot==RAT.ROT.passive then
    _rot=ENUMS.ROT.PassiveDefense
  elseif rot==RAT.ROT.evade then
    _rot=ENUMS.ROT.EvadeFire
  else
    
  end
  
  flightgroup:SetDefaultROT(_rot)
  flightgroup:SwitchROT(_rot)  
  
end


--- Create a table with the valid coalitions for departure and destination airports.
-- @param #RAT self
function RAT:_SetCoalitionTable()
  -- get all possible departures/destinations depending on coalition
  if self.friendly==RAT.coal.neutral then
    self.ctable={coalition.side.NEUTRAL}
  elseif self.friendly==RAT.coal.same then
    self.ctable={self.coalition, coalition.side.NEUTRAL}
  elseif self.friendly==RAT.coal.sameonly then
    self.ctable={self.coalition}
  else
    self:E(self.lid.."ERROR: Unknown friendly coalition in _SetCoalitionTable(). Defaulting to NEUTRAL.")
    self.ctable={self.coalition, coalition.side.NEUTRAL}
  end
end


---Determine the heading from point a to point b.
--@param #RAT self
--@param Core.Point#COORDINATE a Point from.
--@param Core.Point#COORDINATE b Point to.
--@return #number Heading/angle in degrees.
function RAT:_Course(a,b)
  local dx = b.x-a.x
  -- take the right value for y-coordinate (if we have "alt" then "y" if not "z")
  local ay
  if a.alt then
    ay=a.y
  else
    ay=a.z
  end
  local by
  if b.alt then
    by=b.y
  else
    by=b.z
  end
  local dy = by-ay
  local angle = math.deg(math.atan2(dy,dx))
  if angle < 0 then
    angle = 360 + angle
  end
  return angle
end

---Determine the heading for an aircraft to be entered in the route template.
--@param #RAT self
--@param #number course The course between two points in degrees.
--@return #number heading Heading in rad.
function RAT:_Heading(course)
  local h
  if course<=180 then
    h=math.rad(course)
  else
    h=-math.rad(360-course)
  end
  return h
end


--- Randomize a value by a certain amount.
-- @param #RAT self
-- @param #number value The value which should be randomized
-- @param #number fac Randomization factor.
-- @param #number lower (Optional) Lower limit of the returned value.
-- @param #number upper (Optional) Upper limit of the returned value.
-- @return #number Randomized value.
-- @usage _Randomize(100, 0.1) returns a value between 90 and 110, i.e. a plus/minus ten percent variation.
-- @usage _Randomize(100, 0.5, nil, 120) returns a value between 50 and 120, i.e. a plus/minus fivty percent variation with upper bound 120.
function RAT:_Randomize(value, fac, lower, upper)
  local min
  if lower then
    min=math.max(value-value*fac, lower)
  else
    min=value-value*fac
  end
  local max
  if upper then
    max=math.min(value+value*fac, upper)
  else
    max=value+value*fac
  end

  local r=math.random(min, max)

  -- debug info
  if self.Debug then
    local text=string.format("Random: value = %6.2f, fac = %4.2f, min = %6.2f, max = %6.2f, r = %6.2f", value, fac, min, max, r)
    self:T3(self.lid..text)
  end

  return r
end


--- Place markers of the waypoints. Note we assume a very specific number and type of waypoints here.
-- @param #RAT self
-- @param #table waypoints Table with waypoints.
-- @param #table waypointdescriptions Table with waypoint descriptions
-- @param #number index Spawn index of group.
function RAT:_PlaceMarkers(waypoints, waypointdescriptions, index)
  for i=1,#waypoints do
    self:_SetMarker(waypointdescriptions[i], waypoints[i], index)
    if self.Debug then
      local text=string.format("Marker at waypoint #%d: %s for flight #%d", i, waypointdescriptions[i], index)
      self:T2(self.lid..text)
    end
  end
end


--- Set a marker visible for all on the F10 map.
-- @param #RAT self
-- @param #string text Info text displayed at maker.
-- @param #table wp Position of marker coming in as waypoint, i.e. has x, y and alt components.
-- @param #number index Spawn index of group.
function RAT:_SetMarker(text, wp, index)
  RAT.markerid=RAT.markerid+1
  self.markerids[#self.markerids+1]=RAT.markerid
  if self.Debug then
    local text2=string.format("%s: placing marker with ID %d and text %s", self.alias, RAT.markerid, text)
    self:T2(self.lid..text2)
  end
  -- Convert to coordinate.
  local vec={x=wp.x, y=wp.alt, z=wp.y}
  local flight=self:GetGroupFromIndex(index):GetName()
  -- Place maker visible for all on the F10 map.
  local text1=string.format("%s:\n%s", flight, text)
  trigger.action.markToAll(RAT.markerid, text1, vec, false, "")
end

--- Delete all markers on F10 map.
-- @param #RAT self
function RAT:_DeleteMarkers()
  for k,v in ipairs(self.markerids) do
    trigger.action.removeMark(v)
  end
  for k,v in ipairs(self.markerids) do
    self.markerids[k]=nil
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Modifies the template of the group to be spawned.
-- In particular, the waypoints of the group's flight plan are copied into the spawn template.
-- This allows to spawn at airports and also land at other airports, i.e. circumventing the DCS "landing bug".
-- @param #RAT self
-- @param #table waypoints The waypoints of the AI flight plan.
-- @param #string livery (Optional) Livery of the aircraft. All members of a flight will get the same livery.
-- @param Core.Point#COORDINATE spawnplace (Optional) Place where spawning should happen. If not present, first waypoint is taken.
-- @param Wrapper.Airbase#AIRBASE departure Departure airbase or zone.
-- @param #number takeoff Takeoff type.
-- @param #table parkingdata Parking data, i.e. parking spot coordinates and terminal ids for all units of the group.
-- @param #boolean uncontrolled If `true`, group is spawned uncontrolled.
-- @return #boolean True if modification was successful or nil if not, e.g. when no parking space was found and spawn in air is disabled.
function RAT:_ModifySpawnTemplate(waypoints, livery, spawnplace, departure, takeoff, parkingdata, uncontrolled)
  self:F2({waypoints=waypoints, livery=livery, spawnplace=spawnplace, departure=departure, takeoff=takeoff, parking=parkingdata})

  -- The 3D vector of the first waypoint, i.e. where we actually spawn the template group.
  local PointVec3 = COORDINATE:New(waypoints[1].x, waypoints[1].alt, waypoints[1].y)
  if spawnplace then
    PointVec3 = COORDINATE:NewFromCoordinate(spawnplace)
  end

  -- Template group and unit.
  local TemplateGroup = GROUP:FindByName(self.SpawnTemplatePrefix)
  local TemplateUnit=TemplateGroup:GetUnit(1)

  -- Check if we spawn on ground.
  local spawnonground=takeoff==RAT.wp.cold or takeoff==RAT.wp.hot or takeoff==RAT.wp.runway

  -- Check where we actually spawn if we spawn on ground.
  local spawnonship=false
  local spawnonfarp=false
  local spawnonrunway=false
  local spawnonairport=false
  if spawnonground then
    local AirbaseCategory = departure:GetAirbaseCategory()
    if AirbaseCategory == Airbase.Category.SHIP then
      spawnonship=true
    elseif AirbaseCategory == Airbase.Category.HELIPAD then
      spawnonfarp=true
    elseif AirbaseCategory == Airbase.Category.AIRDROME then
      spawnonairport=true
    end
    spawnonrunway=takeoff==RAT.wp.runway
  end

  local automatic=false
  if automatic and spawnonground then
    PointVec3=PointVec3:GetClosestParkingSpot(true, departure)
  end

  -- Heading from first to seconds waypoints to align units in case of air start.
  local course  = self:_Course(waypoints[1], waypoints[2])
  local heading = self:_Heading(course)

  if self:_GetSpawnIndex(self.SpawnIndex+1) then

    -- Get template from group.
    local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate

    if SpawnTemplate then
      self:T(SpawnTemplate)

      -- Spawn aircraft in uncontrolled state.
      if self.uncontrolled or uncontrolled then
        -- This is used in the SPAWN:SpawnWithIndex() function. Some values are overwritten there!
        --self.SpawnUnControlled=true
        SpawnTemplate.uncontrolled=true
      end

      -- Number of units in the group. With grouping this can actually differ from the template group size!
      local nunits=#SpawnTemplate.units

      -- Array with parking spots coordinates.
      local parkingspots={}
      local parkingindex={}
      local spots

      -- Spawn happens on ground, i.e. at an airbase, a FARP or a ship.
      if spawnonground then

        -- Number of free parking spots.
        local nfree=0

        -- Set terminal type. Can also be nil.
        local termtype=self.termtype
        if spawnonrunway then
          termtype=AIRBASE.TerminalType.Runway
        end

        -- Scan options. Might make that input somehow.
        local scanradius=self.parkingscanradius
        local scanunits=true
        local scanstatics=true
        local scanscenery=self.parkingscanscenery
        local verysafe=self.parkingverysafe

        -- Get free parking spots depending on where we spawn.
        if  spawnonship or spawnonfarp or spawnonrunway then
          -- These places work procedural and have some kind of build in queue ==> Less effort.
          self:T(self.lid..string.format("Group %s is spawned on farp/ship/runway %s.", self.alias, departure:GetName()))
          nfree=departure:GetFreeParkingSpotsNumber(termtype, true)
          spots=departure:GetFreeParkingSpotsTable(termtype, true)
          -- Had a case at a Gas Platform where nfree=1 but spots from GetFreeParkingSpotsTable were empty.
          --spots=departure:GetParkingSpotsTable(termtype)
          self:T(self.lid..string.format("Free nfree=%d nspots=%d", nfree, #spots))
        elseif parkingdata~=nil then
          -- Parking data explicitly set by user as input parameter.
          self:T2("Spawning with explicit parking data")
          nfree=#parkingdata
          spots=parkingdata
        else
          -- Helo is spawned.
          if self.category==RAT.cat.heli then
            if termtype==nil then
              -- Try exclusive helo spots first.
              self:T(self.lid..string.format("Helo group %s is spawned at %s using terminal type %d.", self.alias, departure:GetName(), AIRBASE.TerminalType.HelicopterOnly))
              spots=departure:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterOnly, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
              nfree=#spots
              if nfree<nunits then
                -- Not enough helo ports. Let's try also other terminal types.
                self:T(self.lid..string.format("Helo group %s is spawned at %s using terminal type %d.", self.alias, departure:GetName(), AIRBASE.TerminalType.HelicopterUsable))
                spots=departure:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.HelicopterUsable, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
                nfree=#spots
              end
            else
              -- Terminal type specified explicitly.
              self:T(self.lid..string.format("Helo group %s is at %s using terminal type %d.", self.alias, departure:GetName(), termtype))
              spots=departure:FindFreeParkingSpotForAircraft(TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
              nfree=#spots
            end
          else
            -- Fixed wing aircraft is spawned.
            if termtype==nil then
            --TODO: Add some default cases for transport, bombers etc. if no explicit terminal type is provided.
            --TODO: We don't want Bombers to spawn in shelters. But I don't know a good attribute for just fighers.
            --TODO: Some attributes are "Helicopters", "Bombers", "Transports", "Battleplanes". Need to check it out.
              local bomber=TemplateUnit:HasAttribute("Bombers")
              local transport=TemplateUnit:HasAttribute("Transports")
              if bomber or transport then
                -- First we fill the potentially bigger spots.
                self:T(self.lid..string.format("Transport/bomber group %s is spawned at %s using terminal type %d.", self.alias, departure:GetName(), AIRBASE.TerminalType.OpenBig))
                spots=departure:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.OpenBig, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
                nfree=#spots
                if nfree<nunits then
                  -- Now we try the smaller ones.
                  self:T(self.lid..string.format("Transport/bomber group %s is at %s using terminal type %d.", self.alias, departure:GetName(), AIRBASE.TerminalType.OpenMed))
                  spots=departure:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.OpenMed, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
                  nfree=#spots
                end
              else
                self:T(self.lid..string.format("Fighter group %s is spawned at %s using terminal type %d.", self.alias, departure:GetName(), AIRBASE.TerminalType.FighterAircraft))
                spots=departure:FindFreeParkingSpotForAircraft(TemplateGroup, AIRBASE.TerminalType.FighterAircraft, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
                nfree=#spots
              end
            else
              -- Terminal type explicitly given.
              self:T(self.lid..string.format("Plane group %s is spawned at %s using terminal type %d.", self.alias, departure:GetName(), termtype))
              spots=departure:FindFreeParkingSpotForAircraft(TemplateGroup, termtype, scanradius, scanunits, scanstatics, scanscenery, verysafe, nunits)
              nfree=#spots
            end
          end
        end

        -- Get parking data (just for debugging).
        if false then
          local parkingdata=departure:GetParkingSpotsTable(termtype)
          self:T2(self.lid..string.format("Parking at %s, terminal type %s:", departure:GetName(), tostring(termtype)))
          for _,_spot in pairs(parkingdata) do
            self:T2(self.lid..string.format("%s, Termin Index = %3d, Term Type = %03d, Free = %5s, TOAC = %5s, Term ID0 = %3d, Dist2Rwy = %4d",
            departure:GetName(), _spot.TerminalID, _spot.TerminalType,tostring(_spot.Free),tostring(_spot.TOAC),_spot.TerminalID0,_spot.DistToRwy))
          end
          self:T(self.lid..string.format("%s at %s: free parking spots = %d - number of units = %d", self.alias, departure:GetName(), nfree, nunits))
        end


        -- Set this to true if not enough spots are available for emergency air start.
        local _notenough=false

        -- Need to differentiate some cases again.
        if spawnonship or spawnonfarp or spawnonrunway then

          -- On free spot required in these cases.
          if nfree >=1 then

            -- All units get the same spot. DCS takes care of the rest.
            for i=1,nunits do
              table.insert(parkingspots, spots[1].Coordinate)
              table.insert(parkingindex, spots[1].TerminalID)
            end
            -- This is actually used...
            PointVec3=spots[1].Coordinate

          else
            -- If there is absolutely not spot ==> air start!
            _notenough=true
          end

        elseif spawnonairport then

          if nfree>=nunits then

            for i=1,nunits do
              table.insert(parkingspots, spots[i].Coordinate)
              table.insert(parkingindex, spots[i].TerminalID)
            end

          else
            -- Not enough spots for the whole group ==> air start!
            _notenough=true
          end
        end

        -- Not enough spots ==> Prepare airstart.
        if _notenough then

          if self.respawn_inair and not self.SpawnUnControlled then
            self:E(self.lid..string.format("WARNING: Group %s has no parking spots at %s ==> air start!", self.SpawnTemplatePrefix, departure:GetName()))

            -- Not enough parking spots at the airport ==> Spawn in air.
            spawnonground=false
            spawnonship=false
            spawnonfarp=false
            spawnonrunway=false

            -- Set waypoint type/action to turning point.
            waypoints[1].type   = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][1] -- type   = Turning Point
            waypoints[1].action = GROUPTEMPLATE.Takeoff[GROUP.Takeoff.Air][2] -- action = Turning Point

            -- Adjust altitude to be 500-1000 m above the airbase.
            PointVec3.x=PointVec3.x+math.random(-1500,1500)
            PointVec3.z=PointVec3.z+math.random(-1500,1500)
            if self.category==RAT.cat.heli then
              PointVec3.y=PointVec3:GetLandHeight()+math.random(100,1000)
            else
              -- Randomize position so that multiple AC wont be spawned on top even in air.
              PointVec3.y=PointVec3:GetLandHeight()+math.random(500,3000)
            end
          else
            self:E(self.lid..string.format("WARNING: Group %s has no parking spots at %s ==> No emergency air start or uncontrolled spawning ==> No spawn!", self.SpawnTemplatePrefix, departure:GetName()))
            return nil
          end
        end

      else

        -- Air start requested initially!

        --PointVec3.y is already set from first waypoint here!

      end

      -- Translate the position of the Group Template to the Vec3.
      for UnitID = 1, nunits do

        -- Template of the current unit.
        local UnitTemplate = SpawnTemplate.units[UnitID]

        -- Tranlate position and preserve the relative position/formation of all aircraft.
        local SX = UnitTemplate.x
        local SY = UnitTemplate.y
        local BX = SpawnTemplate.route.points[1].x
        local BY = SpawnTemplate.route.points[1].y
        local TX = PointVec3.x + (SX-BX)
        local TY = PointVec3.z + (SY-BY)

        if spawnonground then

          -- Ships and FARPS seem to have a build in queue.
          if spawnonship or spawnonfarp or spawnonrunway or automatic then
            self:T(self.lid..string.format("RAT group %s spawning at farp, ship or runway %s.", self.alias, departure:GetName()))

            -- Spawn on ship. We take only the position of the ship.
            SpawnTemplate.units[UnitID].x   = PointVec3.x --TX
            SpawnTemplate.units[UnitID].y   = PointVec3.z --TY
            SpawnTemplate.units[UnitID].alt = PointVec3.y
          else
            self:T(self.lid..string.format("RAT group %s spawning at airbase %s on parking spot id %d", self.alias, departure:GetName(), parkingindex[UnitID]))

            -- Get coordinates of parking spot.
            SpawnTemplate.units[UnitID].x   = parkingspots[UnitID].x
            SpawnTemplate.units[UnitID].y   = parkingspots[UnitID].z
            SpawnTemplate.units[UnitID].alt = parkingspots[UnitID].y
          end

        else
          self:T(self.lid..string.format("RAT group %s spawning in air at %s.", self.alias, departure:GetName()))

          -- Spawn in air as requested initially. Original template orientation is perserved, altitude is already correctly set.
          SpawnTemplate.units[UnitID].x   = TX
          SpawnTemplate.units[UnitID].y   = TY
          SpawnTemplate.units[UnitID].alt = PointVec3.y
        end

        -- Place marker at spawn position.
        if self.Debug then
          local unitspawn=COORDINATE:New(SpawnTemplate.units[UnitID].x, SpawnTemplate.units[UnitID].alt, SpawnTemplate.units[UnitID].y)
          unitspawn:MarkToAll(string.format("RAT %s Spawnplace unit #%d", self.alias, UnitID))
        end

        -- Parking spot id.
        UnitTemplate.parking = nil
        UnitTemplate.parking_id = nil
        if parkingindex[UnitID] and not automatic then
          UnitTemplate.parking = parkingindex[UnitID]
        end

        -- Debug info.
        self:T2(self.lid..string.format("RAT group %s unit number %d: Parking    = %s",self.alias, UnitID, tostring(UnitTemplate.parking)))
        self:T2(self.lid..string.format("RAT group %s unit number %d: Parking ID = %s",self.alias, UnitID, tostring(UnitTemplate.parking_id)))


        -- Set initial heading.
        SpawnTemplate.units[UnitID].heading = heading
        SpawnTemplate.units[UnitID].psi = -heading

        -- Set livery (will be the same for all units of the group).
        if livery then
          SpawnTemplate.units[UnitID].livery_id = livery
        end

        -- Set type of aircraft.
        if self.actype then
          SpawnTemplate.units[UnitID]["type"] = self.actype
        end

        -- Set AI skill.
        SpawnTemplate.units[UnitID]["skill"] = self.skill

        -- Onboard number.
        if self.onboardnum then
          SpawnTemplate.units[UnitID]["onboard_num"] = string.format("%s%d%02d", self.onboardnum, (self.SpawnIndex-1)%10, (self.onboardnum0-1)+UnitID)
        end

        -- Modify coalition and country of template.
        SpawnTemplate.CoalitionID=self.coalition
        if self.country then
          SpawnTemplate.CountryID=self.country
        end

      end

      -- Copy waypoints into spawntemplate. By this we avoid the nasty DCS "landing bug" :)
      for i,wp in ipairs(waypoints) do
        SpawnTemplate.route.points[i]=wp
      end

      -- Also modify x,y of the template. Not sure why.
      SpawnTemplate.x = PointVec3.x
      SpawnTemplate.y = PointVec3.z

      -- Enable/disable radio. Same as checking the COMM box in the ME
      if self.radio then
        SpawnTemplate.communication=self.radio
      end

      -- Set radio frequency and modulation.
      if self.frequency then
        SpawnTemplate.frequency=self.frequency
      end
      if self.modulation then
        SpawnTemplate.modulation=self.modulation
      end

      -- Debug output.
      self:T(SpawnTemplate)
    end
  end

  return true
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- RAT ATC
---

--- Data structure a RAT ATC airbase object.
-- @type RAT.AtcAirport
-- @field #table queue Queue.
-- @field #boolean busy Whether airport is busy.
-- @field #table onfinal List of flights on final.
-- @field #number Nonfinal Number of flights on final.
-- @field #number traffic Number of flights that landed (just for stats).
-- @field #number Tlastclearance Time stamp when last flight started final approach.

--- Data structure a RAT ATC airbase object.
-- @type RAT.AtcFlight
-- @field #string destination Name of the destination airbase.
-- @field #number Tarrive Time stamp when flight arrived at holding.
-- @field #number holding Holding time.
-- @field #number Tonfinal Time stamp when flight started final approach.

--- Initializes the ATC arrays and starts schedulers.
-- @param #table airports_map List of all airports of the map.
function RAT._ATCInit(airports_map)

  if not RAT.ATC.init then
  
    local text="Starting RAT ATC.\nSimultanious = "..RAT.ATC.Nclearance.."\n".."Delay        = "..RAT.ATC.delay
    BASE:I(RAT.id..text)
    
    for _,ap in pairs(airports_map) do
      local airbase=ap --Wrapper.Airbase#AIRBASE
      local name=airbase:GetName()
      
      local fc=_DATABASE:GetFlightControl(name)
      
      if not fc then
      
        local airport={} --#RAT.AtcAirport
        airport.queue={}
        airport.busy=false
        airport.onfinal={}
        airport.Nonfinal=0
        airport.traffic=0
        airport.Tlastclearance=nil
      
        RAT.ATC.airport[name]=airport
        
      end
    end
    
    SCHEDULER:New(nil, RAT._ATCCheck,  {}, 5, 15)
    SCHEDULER:New(nil, RAT._ATCStatus, {}, 5, 60)
    
    RAT.ATC.T0=timer.getTime()    
  end
  
  -- Init done
  RAT.ATC.init=true
end

--- Adds andd initializes a new flight after it was spawned.
-- @param #RAT self
-- @param #string name Group name of the flight.
-- @param #string dest Name of the destination airport.
function RAT:_ATCAddFlight(name, dest)
  -- Debug info
  BASE:I(RAT.id..string.format("ATC %s: Adding flight %s with destination %s.", dest, name, dest))

  -- Create new flight  
  local flight={} --#RAT.AtcFlight
  flight.destination=dest
  flight.Tarrive=-1
  flight.holding=-1
  flight.Tarrive=-1
  --flight.Tonfinal=-1
  
  RAT.ATC.flight[name]=flight
end

--- Deletes a flight from ATC lists after it landed.
-- @param #table t Table.
-- @param #string entry Flight name which shall be deleted.
function RAT._ATCDelFlight(t,entry)
  for k,_ in pairs(t) do
    if k==entry then
      BASE:I(RAT.id..string.format("Removing flight %s from queue", entry))
      t[entry]=nil
    end
  end
end

--- Registers a flight once it is near its holding point at the final destination.
-- @param #RAT self
-- @param #string name Group name of the flight.
-- @param #number time Time the fight first registered.
function RAT._ATCRegisterFlight(name, time)
  BASE:I(RAT.id..string.format("Flight %s registered at ATC for landing clearance.", name))
  RAT.ATC.flight[name].Tarrive=time
  RAT.ATC.flight[name].holding=0
end


--- ATC status report about flights.
function RAT._ATCStatus()

  -- Current time.
  local Tnow=timer.getTime()

  for name,_flight in pairs(RAT.ATC.flight) do
    local flight=_flight --#RAT.AtcFlight

    -- Holding time at destination.
    local hold=RAT.ATC.flight[name].holding
    local dest=RAT.ATC.flight[name].destination
    
    local airport=RAT.ATC.airport[dest] --#RAT.AtcAirport
    
    if airport then

      if hold >= 0 then
  
        -- Some string whether the runway is busy or not.
        local busy="Runway state is unknown"
        if airport.Nonfinal>0 then
          busy="Runway is occupied by "..airport.Nonfinal
        else
          busy="Runway is currently clear"
        end
  
        -- Aircraft is holding.
        local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy)
        BASE:I(RAT.id..text)
  
      elseif hold==RAT.ATC.onfinal then
  
        -- Aircarft is on final approach for landing.
        local Tfinal=Tnow-flight.Tonfinal
  
        local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60)
        BASE:I(RAT.id..text)
  
      elseif hold==RAT.ATC.unregistered then
  
        -- Aircraft has not arrived at holding point.
        --self:T(string.format("ATC %s: Flight %s is not registered yet (hold %d).", dest, name, hold))
  
      else
        BASE:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().")
      end
    else
      -- Not a RAT.ATC airport (should be managed by a FLIGHTCONTROL)
    end
  end

end

--- Main ATC function. Updates the landing queue of all airports and inceases holding time for all flights.
function RAT._ATCCheck()

  -- Init queue of flights at all airports.
  RAT._ATCQueue()

  -- Current time.
  local Tnow=timer.getTime()

  for airportname,_airport in pairs(RAT.ATC.airport) do
    local airport=_airport --#RAT.AtcAirport

    for qID,flightname in pairs(airport.queue) do
      local flight=RAT.ATC.flight[flightname] --#RAT.AtcFlight

      -- Number of aircraft in queue.
      local nqueue=#airport.queue

      -- Conditions to clear an aircraft for landing
      local landing1=false
      if airport.Tlastclearance then
        -- Landing if time is enough and less then two planes are on final.
        landing1=(Tnow-airport.Tlastclearance > RAT.ATC.delay) and airport.Nonfinal < RAT.ATC.Nclearance
      end
      
      -- No other aircraft is on final.
      local landing2=airport.Nonfinal==0


      if not landing1 and not landing2 then

        -- Update holding time.
        flight.holding=Tnow-flight.Tarrive

        -- Debug message.
        local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.", 
        airportname, flightname, qID, nqueue, flight.holding/60, flight.holding%60)
        BASE:I(RAT.id..text)

      else

        local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", 
        airportname, flightname, flight.holding/60, flight.holding%60)
        BASE:I(RAT.id..text)

        -- Clear flight for landing.
        RAT._ATCClearForLanding(airportname, flightname)

      end

    end

  end

  -- Update queue of flights at all airports.
  RAT._ATCQueue()

end

--- Giving landing clearance for aircraft by setting user flag.
-- @param #string airportname Name of destination airport.
-- @param #string flightname Group name of flight, which gets landing clearence.
function RAT._ATCClearForLanding(airportname, flightname)

  -- Find FLIGHTGROUP in database.
  local flightgroup=_DATABASE:FindOpsGroup(flightname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flightgroup then
  
    -- Give clear to land signal.
    flightgroup:ClearToLand()
    
    
    local flight=RAT.ATC.flight[flightname] --#RAT.AtcFlight
    
    -- Flight is cleared for landing.
    flight.holding=RAT.ATC.onfinal
    
    -- Current time.
    flight.Tonfinal=timer.getTime()    


    local airport=RAT.ATC.airport[airportname] --#RAT.AtcAirport
    
    -- Airport runway is busy now.
    airport.busy=true
    
    -- Flight which is landing.
    airport.onfinal[flightname]=flight
    
    -- Number of planes on final approach.
    airport.Nonfinal=airport.Nonfinal+1
    
    -- Last time an aircraft got landing clearance.
    airport.Tlastclearance=timer.getTime()

  
    -- Debug message.
    BASE:I(RAT.id..string.format("ATC %s: Flight %s cleared for landing", airportname, flightname))
  
    if string.find(flightname,"#") then
      flightname =  string.match(flightname,"^(.+)#")
    end
    local text=string.format("ATC %s: Flight %s you are cleared for landing.", airportname, flightname)  
    MESSAGE:New(text, 10):ToAllIf(RAT.ATC.messages)

  else
    BASE:E("Could not clear flight for landing!")
  end
    
end

--- Takes care of organisational stuff after a plane has landed.
-- @param #string name Group name of flight.
function RAT._ATCFlightLanded(name)

  local flight=RAT.ATC.flight[name] --#RAT.AtcFlight

  if flight then

    -- Destination airport.
    local dest=flight.destination

    -- Times for holding and final approach.
    local Tnow=timer.getTime()
    local Tfinal=Tnow-flight.Tonfinal
    local Thold=flight.Tonfinal-flight.Tarrive
    
    local airport=RAT.ATC.airport[dest] --#RAT.AtcAirport

    -- Airport is not busy any more.
    airport.busy=false

    -- No aircraft on final any more.
    airport.onfinal[name]=nil

    -- Decrease number of aircraft on final.
    airport.Nonfinal=airport.Nonfinal-1

    -- Remove this flight from list of flights.
    RAT._ATCDelFlight(RAT.ATC.flight, name)

    -- Increase landing counter to monitor traffic.
    airport.traffic=airport.traffic+1

    -- Number of planes landing per hour.
    local TrafficPerHour=airport.traffic/(timer.getTime()-RAT.ATC.T0)*3600

    -- Debug info
    BASE:I(RAT.id..string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60))
    BASE:I(RAT.id..string.format("ATC %s: Number of flights still on final %d.", dest, airport.Nonfinal))
    BASE:I(RAT.id..string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, airport.traffic, TrafficPerHour))
    
    if string.find(name,"#") then
      name =  string.match(name,"^(.+)#")
    end
    local text=string.format("ATC %s: Flight %s landed. Welcome to %s.", dest, name, dest)    
    MESSAGE:New(text, 10):ToAllIf(RAT.ATC.messages)
  end

end

--- Creates a landing queue for all flights holding at airports. Aircraft with longest holding time gets first permission to land.
function RAT._ATCQueue()

  -- Current time
  local Tnow=timer.getTime()

  for airport,_ in pairs(RAT.ATC.airport) do

    -- Local airport queue.
    local _queue={}

    -- Loop over all flights.
    for name,_flight in pairs(RAT.ATC.flight) do
      local flight=_flight --#RAT.AtcFlight

      -- Update holding time (unless holing is set to onfinal=-100)
      if flight.holding>=0 then
        flight.holding=Tnow-flight.Tarrive
      end
      
      local hold=flight.holding
      local dest=flight.destination

      -- Flight is holding at this airport.
      if hold>=0 and airport==dest then
        _queue[#_queue+1]={name,hold}
      end
    end

    -- Sort queue w.r.t holding time in ascending order.
    local function compare(a,b)
      return a[2] > b[2]
    end
    table.sort(_queue, compare)

    -- Transfer queue to airport queue.
    RAT.ATC.airport[airport].queue={}
    for k,v in ipairs(_queue) do
      table.insert(RAT.ATC.airport[airport].queue, v[1])
    end

  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- RATMANAGER class
-- @type RATMANAGER
-- @field #string ClassName Name of the Class.
-- @field #boolean Debug If true, be more verbose on output in DCS.log file.
-- @field #table rat Array holding RAT objects etc.
-- @field #string name Name (alias) of RAT object.
-- @field #table alive Number of currently alive groups.
-- @field #table min Minimum number of RAT groups alive.
-- @field #number nrat Number of RAT objects.
-- @field #number ntot Total number of active RAT groups.
-- @field #number Tcheck Time interval in seconds between checking of alive groups.
-- @field #number dTspawn Time interval in seconds between spawns of groups.
-- @field Core.Scheduler#SCHEDULER manager Scheduler managing the RAT objects.
-- @field #number managerid Managing scheduler id.
-- @extends Core.Base#BASE

---# RATMANAGER class, extends @{Core.Base#BASE}
-- The RATMANAGER class manages spawning of multiple RAT objects in a very simple way. It is created by the  @{#RATMANAGER.New}() contructor.
-- RAT objects with different "tasks" can be defined as usual. However, they **must not** be spawned via the @{#RAT.Spawn}() function.
--
-- Instead, these objects can be added to the manager via the @{#RATMANAGER.Add}(ratobject, min) function, where the first parameter "ratobject" is the @{#RAT} object, while the second parameter "min" defines the
-- minimum number of RAT aircraft of that object, which are alive at all time.
--
-- The @{#RATMANAGER} must be started by the @{#RATMANAGER.Start}(startime) function, where the optional argument "startime" specifies the delay time in seconds after which the manager is started and the spawning beginns.
-- If desired, the @{#RATMANAGER} can be stopped by the @{#RATMANAGER.Stop}(stoptime) function. The parameter "stoptime" specifies the time delay in seconds after which the manager stops.
-- When this happens, no new aircraft will be spawned and the population will eventually decrease to zero.
--
-- When you are using a time intervall like @{#RATMANAGER.dTspawn}(delay), @{#RATMANAGER} will ignore the amount set with @{#RATMANAGER.New}(). @{#RATMANAGER.dTspawn}(delay) will spawn infinite groups.
--
-- ## Example
-- In this example, three different @{#RAT} objects are created (but not spawned manually). The @{#RATMANAGER} takes care that at least five aircraft of each type are alive and that the total number of aircraft
-- spawned is 25. The @{#RATMANAGER} is started after 30 seconds and stopped after two hours.
--
--     local a10c=RAT:New("RAT_A10C", "A-10C managed")
--     a10c:SetDeparture({"Batumi"})
--
--     local f15c=RAT:New("RAT_F15C", "F15C managed")
--     f15c:SetDeparture({"Sochi-Adler"})
--     f15c:DestinationZone()
--     f15c:SetDestination({"Zone C"})
--
--     local av8b=RAT:New("RAT_AV8B", "AV8B managed")
--     av8b:SetDeparture({"Zone C"})
--     av8b:SetTakeoff("air")
--     av8b:DestinationZone()
--     av8b:SetDestination({"Zone A"})
--
--     local manager=RATMANAGER:New(25)
--     manager:Add(a10c, 5)
--     manager:Add(f15c, 5)
--     manager:Add(av8b, 5)
--     manager:Start(30)
--     manager:Stop(7200)
--
-- @field #RATMANAGER
RATMANAGER={
  ClassName="RATMANAGER",
  Debug=false,
  rat={},
  name={},
  alive={},
  planned={},
  min={},
  nrat=0,
  ntot=nil,
  Tcheck=60,
  dTspawn=1.0,
  manager=nil,
  managerid=nil,
}

--- Some ID to identify who we are in output of the DCS.log file.
-- @field #string id
RATMANAGER.id="RATMANAGER | "

--- Creates a new RATMANAGER object.
-- @param #RATMANAGER self
-- @param #number ntot Total number of RAT flights.
-- @return #RATMANAGER RATMANAGER object
function RATMANAGER:New(ntot)

  -- Inherit BASE.
  local self=BASE:Inherit(self, BASE:New()) -- #RATMANAGER

  -- Total number of RAT groups.
  self.ntot=ntot or 1

  -- Debug info
  self:I(RATMANAGER.id..string.format("Creating manager for %d groups", ntot))

  return self
end


--- Adds a RAT object to the RAT manager. Parameter min specifies the limit how many RAT groups are at least alive.
-- @param #RATMANAGER self
-- @param #RAT ratobject RAT object to be managed.
-- @param #number min Minimum number of groups for this RAT object. Default is 1.
-- @return #RATMANAGER RATMANAGER self object.
function RATMANAGER:Add(ratobject,min)

  --Automatic respawning is disabled.
  ratobject.norespawn=true
  ratobject.f10menu=false

  -- Increase RAT object counter.
  self.nrat=self.nrat+1

  self.rat[self.nrat]=ratobject
  self.alive[self.nrat]=0
  self.planned[self.nrat]=0
  self.name[self.nrat]=ratobject.alias
  self.min[self.nrat]=min or 1

  -- Debug info.
  self:T(RATMANAGER.id..string.format("Adding ratobject %s with min flights = %d", self.name[self.nrat],self.min[self.nrat]))

  -- Call spawn to initialize RAT parameters.
  ratobject:Spawn(0)

  return self
end

--- Starts the RAT manager and spawns the initial random number RAT groups for each RAT object.
-- @param #RATMANAGER self
-- @param #number delay Time delay in seconds after which the RAT manager is started. Default is 5 seconds.
-- @return #RATMANAGER RATMANAGER self object.
function RATMANAGER:Start(delay)

  -- Time delay.
  delay=delay or 5

  if delay and delay>0 then
  
    -- Info text.
    local text=string.format(RATMANAGER.id.."RAT manager will be started in %d seconds.\n", delay)
    text=text..string.format("Managed groups:\n")
    for i=1,self.nrat do
      text=text..string.format("- %s with min groups %d\n", self.name[i], self.min[i])
    end
    text=text..string.format("Number of constantly alive groups %d", self.ntot)
    self:E(text)  
    
    -- Delayed call
    self:ScheduleOnce(delay, RATMANAGER.Start, self, 0)
    
  else

    -- Ensure that ntot is at least sum of min RAT groups.
    local n=0
    for i=1,self.nrat do
      n=n+self.min[i]
    end
    self.ntot=math.max(self.ntot, n)
  
    -- Get randum number of new RAT groups.
    local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive)
  
    -- Loop over all RAT objects and spawn groups.
    local time=0.0
    for i=1,self.nrat do
      for j=1,N[i] do
        time=time+self.dTspawn
        --SCHEDULER:New(nil, RAT._SpawnWithRoute, {self.rat[i]}, time)
        self:ScheduleOnce(time, RAT._SpawnWithRoute, self.rat[i])
      end
    end
  
    -- Start activation scheduler for uncontrolled aircraft.
    for i=1,self.nrat do
      local rat=self.rat[i] --#RAT
      if rat.uncontrolled and rat.activate_uncontrolled then
        -- Start activating stuff but not before the latest spawn has happend.
        local Tactivate=math.max(time+1, rat.activate_delay)
        --SCHEDULER:New(self.rat[i], self.rat[i]._ActivateUncontrolled, {self.rat[i]}, Tactivate, self.rat[i].activate_delta, self.rat[i].activate_frand)
        self:ScheduleRepeat(Tactivate,rat.activate_delta, rat.activate_frand, nil,rat._ActivateUncontrolled, rat)
      end
    end
  
    -- Start the manager. But not earlier than the latest spawn has happened!
    local TstartManager=math.max(time+1, self.Tcheck)
  
    -- Start manager scheduler.
    self.manager, self.managerid = SCHEDULER:New(self, self._Manage, {self}, TstartManager, self.Tcheck) --Core.Scheduler#SCHEDULER
  
    -- Info
    local text=string.format(RATMANAGER.id.."Starting RAT manager with scheduler ID %s in %d seconds. Repeat interval %d seconds.", self.managerid, TstartManager, self.Tcheck)
    self:I(text)  
  
  
  end

  return self
end


--- Stops the RAT manager.
-- @param #RATMANAGER self
-- @param #number delay Delay in seconds before the manager is stopped. Default is 1 second.
-- @return #RATMANAGER RATMANAGER self object.
function RATMANAGER:Stop(delay)
  delay=delay or 1

  
  if delay and delay>0 then
  
    self:I(RATMANAGER.id..string.format("Manager will be stopped in %d seconds.", delay))
    
    self:ScheduleOnce(delay, RATMANAGER.Stop, self, 0)
  
  else

    self:I(RATMANAGER.id..string.format("Stopping manager with scheduler ID %s", self.managerid))
    
    self.manager:Stop(self.managerid)  
  
  end
  
  
  return self
end


--- Sets the time interval between checks of alive RAT groups. Default is 60 seconds.
-- @param #RATMANAGER self
-- @param #number dt Time interval in seconds.
-- @return #RATMANAGER RATMANAGER self object.
function RATMANAGER:SetTcheck(dt)
  self.Tcheck=dt or 60
  return self
end

--- Sets the time interval between spawning of groups.
-- @param #RATMANAGER self
-- @param #number dt Time interval in seconds. Default is 1 second.
-- @return #RATMANAGER RATMANAGER self object.
function RATMANAGER:SetTspawn(dt)
  self.dTspawn=dt or 1.0
  return self
end


--- Manager function. Calculating the number of current groups and respawning new groups if necessary.
-- @param #RATMANAGER self
function RATMANAGER:_Manage()

  -- Count total number of groups.
  local ntot=self:_Count()

  -- Debug info.
  self:T(RATMANAGER.id..string.format("Number of alive groups %d. New groups to be spawned %d.", ntot, self.ntot-ntot))

  -- Get number of necessary spawns.
  local N=self:_RollDice(self.nrat, self.ntot, self.min, self.alive)

  -- Loop over all RAT objects and spawn new groups if necessary.
  local time=0.0
  for i=1,self.nrat do
    for j=1,N[i] do
      time=time+self.dTspawn
      self.planned[i]=self.planned[i]+1
      --SCHEDULER:New(nil, RATMANAGER._Spawn, {self, i}, time)
      self:ScheduleOnce(time, RATMANAGER._Spawn, self, i)
    end
  end
end

--- Instantly starts the RAT manager and spawns the initial random number RAT groups for each RAT object.
-- @param #RATMANAGER self
-- @param #RATMANAGER RATMANAGER self object.
-- @param #number i Index.
function RATMANAGER:_Spawn(i)

  local rat=self.rat[i] --#RAT
  
  rat:_SpawnWithRoute()
  self.planned[i]=self.planned[i]-1

end

--- Counts the number of alive RAT objects.
-- @param #RATMANAGER self
function RATMANAGER:_Count()

  -- Init total counter.
  local ntotal=0

  -- Loop over all RAT objects.
  for i=1,self.nrat do
    local n=0

    local ratobject=self.rat[i] --#RAT

    -- Loop over the RAT groups of this object.
    for spawnindex,ratcraft in pairs(ratobject.ratcraft) do
      local group=ratcraft.group --Wrapper.Group#GROUP
      if group and group:IsAlive() then
        n=n+1
      end
    end

    -- Alive groups of this RAT object.
    self.alive[i]=n

    -- Grand total.
    ntotal=ntotal+n

    -- Debug output.
    local text=string.format("Number of alive groups of %s = %d, planned=%d", self.name[i], n, self.planned[i])
    self:T(RATMANAGER.id..text)
  end

  -- Return grand total.
  return ntotal
end

--- Rolls the dice for the number of necessary spawns.
-- @param #RATMANAGER self
-- @param #number nrat Number of RAT objects.
-- @param #number ntot Total number of RAT flights.
-- @param #table min Minimum number of groups for each RAT object.
-- @param #table alive Number of alive groups of each RAT object.
function RATMANAGER:_RollDice(nrat,ntot,min,alive)

  -- Calculate sum.
  local function sum(A,index)
    local summe=0
    for _,i in ipairs(index) do
      summe=summe+A[i]
    end
    return summe
  end

  -- Table of number of groups.
  local N={}
  local M={}
  local P={}
  for i=1,nrat do
    local a=alive[i]+self.planned[i]
    N[#N+1]=0
    M[#M+1]=math.max(a, min[i])
    P[#P+1]=math.max(min[i]-a,0)
  end

  -- Min/max group arrays.
  local mini={}
  local maxi={}

  -- Arrays.
  local rattab={}
  for i=1,nrat do
    table.insert(rattab,i)
  end
  local done={}

  -- Number of new groups to be added.
  local nnew=ntot
  for i=1,nrat do
    nnew=nnew-alive[i]-self.planned[i]
  end

  for i=1,nrat-1 do

    -- Random entry from .
    local r=math.random(#rattab)
    -- Get value
    local j=rattab[r]

    table.remove(rattab, r)
    table.insert(done,j)

    -- Sum up the number of already distributed groups.
    local sN=sum(N, done)
    -- Sum up the minimum number of yet to be distributed groups.
    local sP=sum(P, rattab)

    -- Max number that can be distributed for this object.
    maxi[j]=nnew-sN-sP

    -- Min number that should be distributed for this object
    mini[j]=P[j]

    -- Random number of new groups for this RAT object.
    if maxi[j] >= mini[j] then
      N[j]=math.random(mini[j], maxi[j])
    else
      N[j]=0
    end

    -- Debug info
    self:T3(string.format("RATMANAGER: i=%d, alive=%d, planned=%d, min=%d, mini=%d, maxi=%d, add=%d, sumN=%d, sumP=%d", j, alive[j], self.planned[i], min[j], mini[j], maxi[j], N[j],sN, sP))

  end

  -- Last RAT object, number of groups is determined from number of already distributed groups and nnew.
  local j=rattab[1]
  N[j]=nnew-sum(N, done)
  mini[j]=nnew-sum(N, done)
  maxi[j]=nnew-sum(N, done)
  table.remove(rattab, 1)
  table.insert(done,j)

  -- Debug info
  local text=RATMANAGER.id.."\n"
  for i=1,nrat do
    text=text..string.format("%s: i=%d, alive=%d, planned=%d, min=%d, mini=%d, maxi=%d, add=%d\n", self.name[i], i, alive[i], self.planned[i], min[i], mini[i], maxi[i], N[i])
  end
  text=text..string.format("Total # of groups to add = %d", sum(N, done))
  self:T(text)

  -- Return number of groups to be spawned.
  return N
end
--- **Functional** - Range Practice.
--
-- ===
--
-- The RANGE class enables easy set up of bombing and strafing ranges within DCS World.
--
-- Implementation is based on the [Simple Range Script](https://forums.eagle.ru/showthread.php?t=157991) by Ciribob, which itself was motivated
-- by a script by SNAFU [see here](https://forums.eagle.ru/showthread.php?t=109174).
--
-- [476th - Air Weapons Range Objects mod](https://www.476vfightergroup.com/downloads.php?do=download&downloadid=482) is highly recommended for this class.
--
-- **Main Features:**
--
--   * Impact points of bombs, rockets and missiles are recorded and distance to closest range target is measured and reported to the player.
--   * Number of hits on strafing passes are counted and reported. Also the percentage of hits w.r.t fired shots is evaluated.
--   * Results of all bombing and strafing runs are stored and top 10 results can be displayed.
--   * Range targets can be marked by smoke.
--   * Range can be illuminated by illumination bombs for night missions.
--   * Bomb, rocket and missile impact points can be marked by smoke.
--   * Direct hits on targets can trigger flares.
--   * Smoke and flare colors can be adjusted for each player via radio menu.
--   * Range information and weather at the range can be obtained via radio menu.
--   * Persistence: Bombing range results can be saved to disk and loaded the next time the mission is started.
--   * Range control voice overs (>40) for hit assessment.
--
-- ===
--
-- ## Youtube Videos:
--
--    * [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg)
--    * [MOOSE - On the Range - Demonstration Video](https://www.youtube.com/watch?v=kIXcxNB9_3M)
--
-- ===
--
-- ## Missions:
--
--    * [MAR - On the Range - MOOSE - SC](https://www.digitalcombatsimulator.com/en/files/3317765/) by shagrat
--
-- ===
--
-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases)
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ### Contributions: FlightControl, Ciribob
-- ### SRS Additions: Applevangelist
--
-- ===
-- @module Functional.Range
-- @image Range.JPG

--- RANGE class
-- @type RANGE
-- @field #string ClassName Name of the Class.
-- @field #boolean Debug If true, debug info is sent as messages on the screen.
-- @field #boolean verbose Verbosity level. Higher means more output to DCS log file.
-- @field #string lid String id of range for output in DCS log.
-- @field #string rangename Name of the range.
-- @field Core.Point#COORDINATE location Coordinate of the range location.
-- @field #number rangeradius Radius of range defining its total size for e.g. smoking bomb impact points and sending radio messages. Default 5 km.
-- @field Core.Zone#ZONE rangezone MOOSE zone object of the range. For example, no bomb impacts are smoked if bombs fall outside of the range zone.
-- @field #table strafeTargets Table of strafing targets.
-- @field #table bombingTargets Table of targets to bomb.
-- @field #number nbombtargets Number of bombing targets.
-- @field #number nstrafetargets Number of strafing targets.
-- @field #boolean messages Globally enable/disable all messages to players.
-- @field #table MenuAddedTo Table for monitoring which players already got an F10 menu.
-- @field #table planes Table for administration.
-- @field #table strafeStatus Table containing the current strafing target a player as assigned to.
-- @field #table strafePlayerResults Table containing the strafing results of each player.
-- @field #table bombPlayerResults Table containing the bombing results of each player.
-- @field #table PlayerSettings Individual player settings.
-- @field #number dtBombtrack Time step [sec] used for tracking released bomb/rocket positions. Default 0.005 seconds.
-- @field #number BombtrackThreshold Bombs/rockets/missiles are only tracked if player-range distance is smaller than this threshold [m]. Default 25000 m.
-- @field #number Tmsg Time [sec] messages to players are displayed. Default 30 sec.
-- @field #string examinergroupname Name of the examiner group which should get all messages.
-- @field #boolean examinerexclusive If true, only the examiner gets messages. If false, clients and examiner get messages.
-- @field #number strafemaxalt Maximum altitude in meters AGL for registering for a strafe run. Default is 914 m = 3000 ft.
-- @field #number ndisplayresult Number of (player) results that a displayed. Default is 10.
-- @field Utilities.Utils#SMOKECOLOR BombSmokeColor Color id used for smoking bomb targets.
-- @field Utilities.Utils#SMOKECOLOR StrafeSmokeColor Color id used to smoke strafe targets.
-- @field Utilities.Utils#SMOKECOLOR StrafePitSmokeColor Color id used to smoke strafe pit approach boxes.
-- @field #number illuminationminalt Minimum altitude in meters AGL at which illumination bombs are fired. Default is 500 m.
-- @field #number illuminationmaxalt Maximum altitude in meters AGL at which illumination bombs are fired. Default is 1000 m.
-- @field #number scorebombdistance Distance from closest target up to which bomb hits are counted. Default 1000 m.
-- @field #number TdelaySmoke Time delay in seconds between impact of bomb and starting the smoke. Default 3 seconds.
-- @field #boolean trackbombs If true (default), all bomb types are tracked and impact point to closest bombing target is evaluated.
-- @field #boolean trackrockets If true (default), all rocket types are tracked and impact point to closest bombing target is evaluated.
-- @field #boolean trackmissiles If true (default), all missile types are tracked and impact point to closest bombing target is evaluated.
-- @field #boolean defaultsmokebomb If true, initialize player settings to smoke bomb.
-- @field #boolean autosave If true, automatically save results every X seconds.
-- @field #number instructorfreq Frequency on which the range control transmitts.
-- @field Sound.RadioQueue#RADIOQUEUE instructor Instructor radio queue.
-- @field #number rangecontrolfreq Frequency on which the range control transmitts.
-- @field Sound.RadioQueue#RADIOQUEUE rangecontrol Range control radio queue.
-- @field #string rangecontrolrelayname Name of relay unit.
-- @field #string instructorrelayname Name of relay unit.
-- @field #string soundpath Path inside miz file where the sound files are located. Default is "Range Soundfiles/".
-- @field #boolean targetsheet If true, players can save their target sheets. Rangeboss will not work if targetsheets do not save.
-- @field #string targetpath Path where to save the target sheets.
-- @field #string targetprefix File prefix for target sheet files.
-- @field Sound.SRS#MSRS controlmsrs SRS wrapper for range controller.
-- @field Sound.SRS#MSRSQUEUE controlsrsQ SRS queue for range controller.
-- @field Sound.SRS#MSRS instructmsrs SRS wrapper for range instructor.
-- @field Sound.SRS#MSRSQUEUE instructsrsQ SRS queue for range instructor.
-- @field #number Coalition Coalition side for the menu, if any.
-- @field Core.Menu#MENU_MISSION menuF10root Specific user defined root F10 menu.
-- @field #number ceilingaltitude Range ceiling altitude in ft MSL.  Aircraft above this altitude are not considered to be in the range. Default is 20000 ft.
-- @field #boolean ceilingenabled Range has a ceiling and is not unlimited.  Default is false.

-- @extends Core.Fsm#FSM

--- *Don't only practice your art, but force your way into its secrets; art deserves that, for it and knowledge can raise man to the Divine.* - Ludwig van Beethoven
--
-- ===
--
-- ![Banner Image](..\Presentations\RANGE\RANGE_Main.png)
--
-- # The Range Concept
--
-- The RANGE class enables a mission designer to easily set up practice ranges in DCS. A new RANGE object can be created with the @{#RANGE.New}(*rangename*) contructor.
-- The parameter *rangename* defines the name of the range. It has to be unique since this is also the name displayed in the radio menu.
--
-- Generally, a range consists of strafe pits and bombing targets. For strafe pits the number of hits for each pass is counted and tabulated.
-- For bombing targets, the distance from the impact point of the bomb, rocket or missile to the closest range target is measured and tabulated.
-- Each player can display his best results via a function in the radio menu or see the best best results from all players.
--
-- When all targets have been defined in the script, the range is started by the @{#RANGE.Start}() command.
--
-- **IMPORTANT**
--
-- Due to a DCS bug, it is not possible to directly monitor when a player enters a plane. So in a mission with client slots, it is vital that
-- a player first enters as spectator or hits ESC twice and **after that** jumps into the slot of his aircraft!
-- If that is not done, the script is not started correctly. This can be checked by looking at the radio menues. If the mission was entered correctly,
-- there should be an "On the Range" menu items in the "F10. Other..." menu.
--
-- # Strafe Pits
--
-- Each strafe pit can consist of multiple targets. Often one finds two or three strafe targets next to each other.
--
-- A strafe pit can be added to the range by the @{#RANGE.AddStrafePit}(*targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*) function.
--
-- * The first parameter *targetnames* defines the target or targets. This can be a single item or a Table with the name(s) of @{Wrapper.Unit} or @{Wrapper.Static} objects defined in the mission editor.
-- * In order to perform a valid pass on the strafe pit, the pilot has to begin his run from the correct direction. Therefore, an "approach box" is defined in front
--   of the strafe targets. The parameters *boxlength* and *boxwidth* define the size of the box in meters, while the *heading* parameter defines the heading of the box FROM the target.
--   For example, if heading 120 is set, the approach box will start FROM the target and extend outwards on heading 120. A strafe run approach must then be flown apx. heading 300 TOWARDS the target.
--   If the parameter *heading* is passed as **nil**, the heading is automatically taken from the heading set in the ME for the first target unit.
-- * The parameter *inverseheading* turns the heading around by 180 degrees. This is useful when the default heading of strafe target units point in the wrong/opposite direction.
-- * The parameter *goodpass* defines the number of hits a pilot has to achieve during a run to be judged as a "good" pass.
-- * The last parameter *foulline* sets the distance from the pit targets to the foul line. Hit from closer than this line are not counted!
--
-- Another function to add a strafe pit is @{#RANGE.AddStrafePitGroup}(*group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline*). Here,
-- the first parameter *group* is a MOOSE @{Wrapper.Group} object and **all** units in this group define **one** strafe pit.
--
-- Finally, a valid approach has to be performed below a certain maximum altitude. The default is 914 meters (3000 ft) AGL. This is a parameter valid for all
-- strafing pits of the range and can be adjusted by the @{#RANGE.SetMaxStrafeAlt}(maxalt) function.
--
-- # Bombing targets
--
-- One ore multiple bombing targets can be added to the range by the @{#RANGE.AddBombingTargets}(targetnames, goodhitrange, randommove) function.
--
-- * The first parameter *targetnames* defines the target or targets. This can be a single item or a Table with the name(s) of @{Wrapper.Unit} or @{Wrapper.Static} objects defined in the mission editor.
-- * The (optional) parameter *goodhitrange* specifies the radius in metres around the target within which a bomb/rocket hit is considered to be "good".
-- * If final (optional) parameter "*randommove*" can be enabled to create moving targets. If this parameter is set to true, the units of this bombing target will randomly move within the range zone.
--   Note that there might be quirks since DCS units can get stuck in buildings etc. So it might be safer to manually define a route for the units in the mission editor if moving targets are desired.
--
-- ## Adding Groups
--
-- Another possibility to add bombing targets is the @{#RANGE.AddBombingTargetGroup}(*group, goodhitrange, randommove*) function. Here the parameter *group* is a MOOSE @{Wrapper.Group} object
-- and **all** units in this group are defined as bombing targets.
--
-- ## Specifying Coordinates
--
-- It is also possible to specify coordinates rather than unit or static objects as bombing target locations. This has the advantage, that even when the unit/static object is dead, the specified
-- coordinate will still be a valid impact point. This can be done via the @{#RANGE.AddBombingTargetCoordinate}(*coord*, *name*, *goodhitrange*) function.
--
-- # Fine Tuning
--
-- Many range parameters have good default values. However, the mission designer can change these settings easily with the supplied user functions:
--
-- * @{#RANGE.SetMaxStrafeAlt}() sets the max altitude for valid strafing runs.
-- * @{#RANGE.SetMessageTimeDuration}() sets the duration how long (most) messages are displayed.
-- * @{#RANGE.SetDisplayedMaxPlayerResults}() sets the number of results displayed.
-- * @{#RANGE.SetRangeRadius}() defines the total range area.
-- * @{#RANGE.SetBombTargetSmokeColor}() sets the color used to smoke bombing targets.
-- * @{#RANGE.SetStrafeTargetSmokeColor}() sets the color used to smoke strafe targets.
-- * @{#RANGE.SetStrafePitSmokeColor}() sets the color used to smoke strafe pit approach boxes.
-- * @{#RANGE.SetSmokeTimeDelay}() sets the time delay between smoking bomb/rocket impact points after impact.
-- * @{#RANGE.TrackBombsON}() or @{#RANGE.TrackBombsOFF}() can be used to enable/disable tracking and evaluating of all bomb types a player fires.
-- * @{#RANGE.TrackRocketsON}() or @{#RANGE.TrackRocketsOFF}() can be used to enable/disable tracking and evaluating of all rocket types a player fires.
-- * @{#RANGE.TrackMissilesON}() or @{#RANGE.TrackMissilesOFF}() can be used to enable/disable tracking and evaluating of all missile types a player fires.
--
-- # Radio Menu
--
-- Each range gets a radio menu with various submenus where each player can adjust his individual settings or request information about the range or his scores.
--
-- The main range menu can be found at "F10. Other..." --> "F*X*. On the Range..." --> "F1. <Range Name>...".
--
-- The range menu contains the following submenus:
--
-- ![Banner Image](..\Presentations\RANGE\Menu_Main.png)
--
-- * "F1. Statistics...": Range results of all players and personal stats.
-- * "F2. Mark Targets": Mark range targets by smoke or flares.
-- * "F3. My Settings" Personal settings.
-- * "F4. Range Info": Information about the range, such as bearing and range.
--
-- ## F1 Statistics
--
-- ![Banner Image](..\Presentations\RANGE\Menu_Stats.png)
--
-- ## F2 Mark Targets
--
-- ![Banner Image](..\Presentations\RANGE\Menu_Stats.png)
--
-- ## F3 My Settings
--
-- ![Banner Image](..\Presentations\RANGE\Menu_MySettings.png)
--
-- ## F4 Range Info
--
-- ![Banner Image](..\Presentations\RANGE\Menu_RangeInfo.png)
--
-- # Voice Overs
--
-- Voice over sound files can be downloaded from the Moose Discord. Check the pinned messages in the *#func-range* channel.
--
-- Instructor radio will inform players when they enter or exit the range zone and provide the radio frequency of the range control for hit assessment.
-- This can be enabled via the @{#RANGE.SetInstructorRadio}(*frequency*) functions, where *frequency* is the AM frequency in MHz.
--
-- The range control can be enabled via the @{#RANGE.SetRangeControl}(*frequency*) functions, where *frequency* is the AM frequency in MHz.
--
-- By default, the sound files are placed in the "Range Soundfiles/" folder inside the mission (.miz) file. Another folder can be specified via the @{#RANGE.SetSoundfilesPath}(*path*) function.
--
-- ## Voice output via SRS
--
-- Alternatively, the voice output can be fully done via SRS, **no sound file additions needed**. Set up SRS with @{#RANGE.SetSRS}().
-- Range control and instructor frequencies and voices can then be set via @{#RANGE.SetSRSRangeControl}() and @{#RANGE.SetSRSRangeInstructor}().
--
-- # Persistence
--
-- To automatically save bombing results to disk, use the @{#RANGE.SetAutosave}() function. Bombing results will be saved as csv file in your "Saved Games\DCS.openbeta\Logs" directory.
-- Each range has a separate file, which is named "RANGE-<*RangeName*>_BombingResults.csv".
--
-- The next time you start the mission, these results are also automatically loaded.
--
-- Strafing results are currently **not** saved.
--
-- # FSM Events
--
-- This class creates additional events that can be used by mission designers for custom reactions
--
-- * `EnterRange` when a player enters a range zone. See @{#RANGE.OnAfterEnterRange}
-- * `ExitRange`  when a player leaves a range zone. See @{#RANGE.OnAfterExitRange}
-- * `Impact` on impact of a player's weapon on a bombing target. See @{#RANGE.OnAfterImpact}
-- * `RollingIn` when a player rolls in on a strafing target. See @{#RANGE.OnAfterRollingIn}
-- * `StrafeResult` when a player finishes a strafing run. See @{#RANGE.OnAfterStrafeResult}
--
-- # Examples
--
-- ## Goldwater Range
--
-- This example shows hot to set up the [Barry M. Goldwater range](https://en.wikipedia.org/wiki/Barry_M._Goldwater_Air_Force_Range).
-- It consists of two strafe pits each has two targets plus three bombing targets.
--
--      -- Strafe pits. Each pit can consist of multiple targets. Here we have two pits and each of the pits has two targets.
--      -- These are names of the corresponding units defined in the ME.
--      local strafepit_left={"GWR Strafe Pit Left 1", "GWR Strafe Pit Left 2"}
--      local strafepit_right={"GWR Strafe Pit Right 1", "GWR Strafe Pit Right 2"}
--
--      -- Table of bombing target names. Again these are the names of the corresponding units as defined in the ME.
--      local bombtargets={"GWR Bomb Target Circle Left", "GWR Bomb Target Circle Right", "GWR Bomb Target Hard"}
--
--      -- Create a range object.
--      GoldwaterRange=RANGE:New("Goldwater Range")
--
--      -- Set and enable the range ceiling altitude in feet MSL.  If aircraft are above this altitude they are not considered to be in the range.
--      GoldwaterRange:SetRangeCeiling(20000)
--      GoldwaterRange:EnableRangeCeiling(true)
--
--      -- Distance between strafe target and foul line. You have to specify the names of the unit or static objects.
--      -- Note that this could also be done manually by simply measuring the distance between the target and the foul line in the ME.
--      GoldwaterRange:GetFoullineDistance("GWR Strafe Pit Left 1", "GWR Foul Line Left")
--
--      -- Add strafe pits. Each pit (left and right) consists of two targets. Where "nil" is used as input, the default value is used.
--      GoldwaterRange:AddStrafePit(strafepit_left, 3000, 300, nil, true, 30, 500)
--      GoldwaterRange:AddStrafePit(strafepit_right, nil, nil, nil, true, nil, 500)
--
--      -- Add bombing targets. A good hit is if the bomb falls less then 50 m from the target.
--      GoldwaterRange:AddBombingTargets(bombtargets, 50)
--
--      -- Start range.
--      GoldwaterRange:Start()
--
-- The [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is (implicitly) used in this example.
--
--
-- # Debugging
--
-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in
--      C:\Users\<yourname>\Saved Games\DCS\Logs\dcs.log
-- All output concerning the RANGE class should have the string "RANGE" in the corresponding line.
--
-- The verbosity of the output can be increased by adding the following lines to your script:
--
--      BASE:TraceOnOff(true)
--      BASE:TraceLevel(1)
--      BASE:TraceClass("RANGE")
--
-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details.
--
-- The function @{#RANGE.DebugON}() can be used to send messages on screen. It also smokes all defined strafe and bombing targets, the strafe pit approach boxes and the range zone.
--
-- Note that it can happen that the RANGE radio menu is not shown. Check that the range object is defined as a **global** variable rather than a local one.
-- The could avoid the lua garbage collection to accidentally/falsely deallocate the RANGE objects.
--
-- @field #RANGE
RANGE = {
  ClassName = "RANGE",
  Debug = false,
  verbose = 0,
  id = nil,
  rangename = nil,
  location = nil,
  messages = true,
  rangeradius = 5000,
  rangezone = nil,
  strafeTargets = {},
  bombingTargets = {},
  nbombtargets = 0,
  nstrafetargets = 0,
  MenuAddedTo = {},
  planes = {},
  strafeStatus = {},
  strafePlayerResults = {},
  bombPlayerResults = {},
  PlayerSettings = {},
  dtBombtrack = 0.005,
  BombtrackThreshold = 25000,
  Tmsg = 30,
  examinergroupname = nil,
  examinerexclusive = nil,
  strafemaxalt = 914,
  ndisplayresult = 10,
  BombSmokeColor = SMOKECOLOR.Red,
  StrafeSmokeColor = SMOKECOLOR.Green,
  StrafePitSmokeColor = SMOKECOLOR.White,
  illuminationminalt = 500,
  illuminationmaxalt = 1000,
  scorebombdistance = 1000,
  TdelaySmoke = 3.0,
  trackbombs = true,
  trackrockets = true,
  trackmissiles = true,
  defaultsmokebomb = true,
  autosave = false,
  instructorfreq = nil,
  instructor = nil,
  rangecontrolfreq = nil,
  rangecontrol = nil,
  soundpath = "Range Soundfiles/",
  targetsheet = nil,
  targetpath = nil,
  targetprefix = nil,
  Coalition = nil,
  ceilingaltitude = 20000,
  ceilingenabled = false,
  }

--- Default range parameters.
-- @type RANGE.Defaults
-- @param #number goodhitrange Radius for good hits in meters.
-- @param #number strafemaxalt Max altitude in meters for players to enter a strafing pit.
-- @param #number dtBombtrack Timer interval in seconds.
-- @param #number Tmsg Message display time in seconds.
-- @param #number ndisplayresults Number of results to display.
-- @param #number rangeradius Radius of range in meters.
-- @param #number TdelaySmoke Time delay in seconds before smoke is triggered.
-- @param #number boxlength Length of strafe pit box in meters.
-- @param #number boxwidth Width of strafe pit box in meters.
-- @param #number goodpass Number of hits for a good strafing pit pass.
-- @param #number foulline Distance of foul line in meters.
RANGE.Defaults = {
  goodhitrange = 25,
  strafemaxalt = 914,
  dtBombtrack = 0.005,
  Tmsg = 30,
  ndisplayresult = 10,
  rangeradius = 5000,
  TdelaySmoke = 3.0,
  boxlength = 3000,
  boxwidth = 300,
  goodpass = 20,
  foulline = 610
}

--- Target type, i.e. unit, static, or coordinate.
-- @type RANGE.TargetType
-- @field #string UNIT Target is a unitobject.
-- @field #string STATIC Target is a static object.
-- @field #string COORD Target is a coordinate.
-- @field #string SCENERY Target is a scenery object.
RANGE.TargetType = {
  UNIT = "Unit",
  STATIC = "Static",
  COORD = "Coordinate",
  SCENERY = "Scenery"
}

--- Player settings.
-- @type RANGE.PlayerData
-- @field #boolean smokebombimpact Smoke bomb impact points.
-- @field #boolean flaredirecthits Flare when player directly hits a target.
-- @field #number smokecolor Color of smoke.
-- @field #number flarecolor Color of flares.
-- @field #boolean messages Display info messages.
-- @field Wrapper.Client#CLIENT client Client object of player.
-- @field #string unitname Name of player aircraft unit.
-- @field Wrapper.Unit#UNIT unit Player unit.
-- @field #string playername Name of player.
-- @field #string airframe Aircraft type name.
-- @field #boolean inzone If true, player is inside the range zone.
-- @field #boolean targeton Target on.

--- Bomb target data.
-- @type RANGE.BombTarget
-- @field #string name Name of unit.
-- @field Wrapper.Unit#UNIT target Target unit.
-- @field Core.Point#COORDINATE coordinate Coordinate of the target.
-- @field #number goodhitrange Range in meters for a good hit.
-- @field #boolean move If true, unit move randomly.
-- @field #number speed Speed of unit.
-- @field #RANGE.TargetType type Type of target.

--- Strafe target data.
-- @type RANGE.StrafeTarget
-- @field #string name Name of the unit.
-- @field Core.Zone#ZONE_POLYGON polygon Polygon zone.
-- @field Core.Point#COORDINATE coordinate Center coordinate of the pit.
-- @field #number goodPass Number of hits for a good pass.
-- @field #table targets Table of target units.
-- @field #number foulline Foul line
-- @field #number smokepoints Number of smoke points.
-- @field #number heading Heading of pit.

--- Strafe status for player.
-- @type RANGE.StrafeStatus
-- @field #number hits Number of hits on target.
-- @field #number time Number of times.
-- @field #number ammo Amount of ammo.
-- @field #boolean pastfoulline If `true`, player passed foul line. Invalid pass.
-- @field #RANGE.StrafeTarget zone Strafe target.

--- Bomb target result.
-- @type RANGE.BombResult
-- @field #string name Name of closest target.
-- @field #number distance Distance in meters.
-- @field #number radial Radial in degrees.
-- @field #string weapon Name of the weapon.
-- @field #string quality Hit quality.
-- @field #string player Player name.
-- @field #string airframe Aircraft type of player.
-- @field #number time Time via timer.getAbsTime() in seconds of impact.
-- @field #string date OS date.
-- @field #number attackHdg Attack heading in degrees.
-- @field #number attackVel Attack velocity in knots.
-- @field #number attackAlt Attack altitude in feet.
-- @field #string clock Time of the run.
-- @field #string rangename Name of the range.

--- Strafe result.
-- @type RANGE.StrafeResult
-- @field #string player Player name.
-- @field #string airframe Aircraft type of player.
-- @field #number time Time via timer.getAbsTime() in seconds of impact.
-- @field #string date OS date.
-- @field #string name Name of the target pit.
-- @field #number roundsFired Number of rounds fired.
-- @field #number roundsHit Number of rounds that hit the target.
-- @field #number strafeAccuracy Accuracy of the run in percent.
-- @field #string clock Time of the run.
-- @field #string rangename Name of the range.
-- @field #boolean invalid Invalid pass.

--- Strafe result.
-- @type RANGE.StrafeResult
-- @field #string player Player name.
-- @field #string airframe Aircraft type of player.
-- @field #number time Time via timer.getAbsTime() in seconds of impact.
-- @field #string date OS date.

--- Sound file data.
-- @type RANGE.Soundfile
-- @field #string filename Name of the file
-- @field #number duration Duration in seconds.

--- Sound files.
-- @type RANGE.Sound
-- @field #RANGE.Soundfile RC0
-- @field #RANGE.Soundfile RC1
-- @field #RANGE.Soundfile RC2
-- @field #RANGE.Soundfile RC3
-- @field #RANGE.Soundfile RC4
-- @field #RANGE.Soundfile RC5
-- @field #RANGE.Soundfile RC6
-- @field #RANGE.Soundfile RC7
-- @field #RANGE.Soundfile RC8
-- @field #RANGE.Soundfile RC9
-- @field #RANGE.Soundfile RCAccuracy
-- @field #RANGE.Soundfile RCDegrees
-- @field #RANGE.Soundfile RCExcellentHit
-- @field #RANGE.Soundfile RCExcellentPass
-- @field #RANGE.Soundfile RCFeet
-- @field #RANGE.Soundfile RCFor
-- @field #RANGE.Soundfile RCGoodHit
-- @field #RANGE.Soundfile RCGoodPass
-- @field #RANGE.Soundfile RCHitsOnTarget
-- @field #RANGE.Soundfile RCImpact
-- @field #RANGE.Soundfile RCIneffectiveHit
-- @field #RANGE.Soundfile RCIneffectivePass
-- @field #RANGE.Soundfile RCInvalidHit
-- @field #RANGE.Soundfile RCLeftStrafePitTooQuickly
-- @field #RANGE.Soundfile RCPercent
-- @field #RANGE.Soundfile RCPoorHit
-- @field #RANGE.Soundfile RCPoorPass
-- @field #RANGE.Soundfile RCRollingInOnStrafeTarget
-- @field #RANGE.Soundfile RCTotalRoundsFired
-- @field #RANGE.Soundfile RCWeaponImpactedTooFar
-- @field #RANGE.Soundfile IR0
-- @field #RANGE.Soundfile IR1
-- @field #RANGE.Soundfile IR2
-- @field #RANGE.Soundfile IR3
-- @field #RANGE.Soundfile IR4
-- @field #RANGE.Soundfile IR5
-- @field #RANGE.Soundfile IR6
-- @field #RANGE.Soundfile IR7
-- @field #RANGE.Soundfile IR8
-- @field #RANGE.Soundfile IR9
-- @field #RANGE.Soundfile IRDecimal
-- @field #RANGE.Soundfile IRMegaHertz
-- @field #RANGE.Soundfile IREnterRange
-- @field #RANGE.Soundfile IRExitRange
RANGE.Sound = {
  RC0 = { filename = "RC-0.ogg", duration = 0.60 },
  RC1 = { filename = "RC-1.ogg", duration = 0.47 },
  RC2 = { filename = "RC-2.ogg", duration = 0.43 },
  RC3 = { filename = "RC-3.ogg", duration = 0.50 },
  RC4 = { filename = "RC-4.ogg", duration = 0.58 },
  RC5 = { filename = "RC-5.ogg", duration = 0.54 },
  RC6 = { filename = "RC-6.ogg", duration = 0.61 },
  RC7 = { filename = "RC-7.ogg", duration = 0.53 },
  RC8 = { filename = "RC-8.ogg", duration = 0.34 },
  RC9 = { filename = "RC-9.ogg", duration = 0.54 },
  RCAccuracy = { filename = "RC-Accuracy.ogg", duration = 0.67 },
  RCDegrees = { filename = "RC-Degrees.ogg", duration = 0.59 },
  RCExcellentHit = { filename = "RC-ExcellentHit.ogg", duration = 0.76 },
  RCExcellentPass = { filename = "RC-ExcellentPass.ogg", duration = 0.89 },
  RCFeet = { filename = "RC-Feet.ogg", duration = 0.49 },
  RCFor = { filename = "RC-For.ogg", duration = 0.64 },
  RCGoodHit = { filename = "RC-GoodHit.ogg", duration = 0.52 },
  RCGoodPass = { filename = "RC-GoodPass.ogg", duration = 0.62 },
  RCHitsOnTarget = { filename = "RC-HitsOnTarget.ogg", duration = 0.88 },
  RCImpact = { filename = "RC-Impact.ogg", duration = 0.61 },
  RCIneffectiveHit = { filename = "RC-IneffectiveHit.ogg", duration = 0.86 },
  RCIneffectivePass = { filename = "RC-IneffectivePass.ogg", duration = 0.99 },
  RCInvalidHit = { filename = "RC-InvalidHit.ogg", duration = 2.97 },
  RCLeftStrafePitTooQuickly = { filename = "RC-LeftStrafePitTooQuickly.ogg", duration = 3.09 },
  RCPercent = { filename = "RC-Percent.ogg", duration = 0.56 },
  RCPoorHit = { filename = "RC-PoorHit.ogg", duration = 0.54 },
  RCPoorPass = { filename = "RC-PoorPass.ogg", duration = 0.68 },
  RCRollingInOnStrafeTarget = { filename = "RC-RollingInOnStrafeTarget.ogg", duration = 1.38 },
  RCTotalRoundsFired = { filename = "RC-TotalRoundsFired.ogg", duration = 1.22 },
  RCWeaponImpactedTooFar = { filename = "RC-WeaponImpactedTooFar.ogg", duration = 3.73 },
  IR0 = { filename = "IR-0.ogg", duration = 0.55 },
  IR1 = { filename = "IR-1.ogg", duration = 0.41 },
  IR2 = { filename = "IR-2.ogg", duration = 0.37 },
  IR3 = { filename = "IR-3.ogg", duration = 0.41 },
  IR4 = { filename = "IR-4.ogg", duration = 0.37 },
  IR5 = { filename = "IR-5.ogg", duration = 0.43 },
  IR6 = { filename = "IR-6.ogg", duration = 0.55 },
  IR7 = { filename = "IR-7.ogg", duration = 0.43 },
  IR8 = { filename = "IR-8.ogg", duration = 0.38 },
  IR9 = { filename = "IR-9.ogg", duration = 0.55 },
  IRDecimal = { filename = "IR-Decimal.ogg", duration = 0.54 },
  IRMegaHertz = { filename = "IR-MegaHertz.ogg", duration = 0.87 },
  IREnterRange = { filename = "IR-EnterRange.ogg", duration = 4.83 },
  IRExitRange = { filename = "IR-ExitRange.ogg", duration = 3.10 },
}

--- Global list of all defined range names.
-- @field #table Names
RANGE.Names = {}

--- Main radio menu on group level.
-- @field #table MenuF10 Root menu table on group level.
RANGE.MenuF10 = {}

--- Main radio menu on mission level.
-- @field #table MenuF10Root Root menu on mission level.
RANGE.MenuF10Root = nil

--- Range script version.
-- @field #string version
RANGE.version = "2.8.1"

-- TODO list:
-- TODO: Verbosity level for messages.
-- TODO: Add option for default settings such as smoke off.
-- TODO: Add custom weapons, which can be specified by the user.
-- TODO: Check if units are still alive.
-- TODO: Option for custom sound files.
-- DONE: Scenery as targets.
-- DONE: Add statics for strafe pits.
-- DONE: Add missiles.
-- DONE: Convert env.info() to self:T()
-- DONE: Add user functions.
-- DONE: Rename private functions, i.e. start with _functionname.
-- DONE: number of displayed results variable.
-- DONE: Add tire option for strafe pits. ==> No really feasible since tires are very small and cannot be seen.
-- DONE: Check that menu texts are short enough to be correctly displayed in VR.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- RANGE contructor. Creates a new RANGE object.
-- @param #RANGE self
-- @param #string RangeName Name of the range. Has to be unique. Will we used to create F10 menu items etc.
-- @param #number Coalition (optional) Coalition of the range, if any, e.g. coalition.side.BLUE.
-- @return #RANGE RANGE object.
function RANGE:New( RangeName, Coalition )

  -- Inherit BASE.
  local self = BASE:Inherit( self, FSM:New() ) -- #RANGE

  -- Get range name.
  -- TODO: make sure that the range name is not given twice. This would lead to problems in the F10 radio menu.
  self.rangename = RangeName or "Practice Range"

  self.Coalition = Coalition

  -- Log id.
  self.lid = string.format( "RANGE %s | ", self.rangename )

  -- Debug info.
  local text = string.format( "Script version %s - creating new RANGE object %s.", RANGE.version, self.rangename )
  self:I( self.lid .. text )

  -- Defaults
  self:SetDefaultPlayerSmokeBomb()

  -- Start State.
  self:SetStartState( "Stopped" )

  ---
  -- Add FSM transitions.
  --                 From State   -->   Event        -->     To State
  self:AddTransition("Stopped",         "Start",             "Running")     -- Start RANGE script.
  self:AddTransition("*",               "Status",            "*")           -- Status of RANGE script.
  self:AddTransition("*",               "Impact",            "*")           -- Impact of bomb/rocket/missile.
  self:AddTransition("*",               "RollingIn",         "*")           -- Player rolling in on strafe target.
  self:AddTransition("*",               "StrafeResult",      "*")           -- Strafe result of player.
  self:AddTransition("*",               "EnterRange",        "*")           -- Player enters the range.
  self:AddTransition("*",               "ExitRange",         "*")           -- Player leaves the range.
  self:AddTransition("*",               "Save",              "*")           -- Save player results.
  self:AddTransition("*",               "Load",              "*")           -- Load player results.

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the RANGE. Initializes parameters and starts event handlers.
  -- @function [parent=#RANGE] Start
  -- @param #RANGE self

  --- Triggers the FSM event "Start" after a delay. Starts the RANGE. Initializes parameters and starts event handlers.
  -- @function [parent=#RANGE] __Start
  -- @param #RANGE self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the RANGE and all its event handlers.
  -- @param #RANGE self

  --- Triggers the FSM event "Stop" after a delay. Stops the RANGE and all its event handlers.
  -- @function [parent=#RANGE] __Stop
  -- @param #RANGE self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Status".
  -- @function [parent=#RANGE] Status
  -- @param #RANGE self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#RANGE] __Status
  -- @param #RANGE self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Impact".
  -- @function [parent=#RANGE] Impact
  -- @param #RANGE self
  -- @param #RANGE.BombResult result Data of bombing run.
  -- @param #RANGE.PlayerData player Data of player settings etc.

  --- Triggers the FSM delayed event "Impact".
  -- @function [parent=#RANGE] __Impact
  -- @param #RANGE self
  -- @param #number delay Delay in seconds before the function is called.
  -- @param #RANGE.BombResult result Data of the bombing run.
  -- @param #RANGE.PlayerData player Data of player settings etc.

  --- On after "Impact" event user function. Called when a bomb/rocket/missile impacted.
  -- @function [parent=#RANGE] OnAfterImpact
  -- @param #RANGE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #RANGE.BombResult result Data of the bombing run.
  -- @param #RANGE.PlayerData player Data of player settings etc.


  --- Triggers the FSM event "RollingIn".
  -- @function [parent=#RANGE] RollingIn
  -- @param #RANGE self
  -- @param #RANGE.PlayerData player Data of player settings etc.
  -- @param #RANGE.StrafeTarget target Strafe target.

  --- On after "RollingIn" event user function. Called when a player rolls in to a strafe taret.
  -- @function [parent=#RANGE] OnAfterRollingIn
  -- @param #RANGE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #RANGE.PlayerData player Data of player settings etc.
  -- @param #RANGE.StrafeTarget target Strafe target.

  --- Triggers the FSM event "StrafeResult".
  -- @function [parent=#RANGE] StrafeResult
  -- @param #RANGE self
  -- @param #RANGE.PlayerData player Data of player settings etc.
  -- @param #RANGE.StrafeResult result Data of the strafing run.

  --- On after "StrafeResult" event user function. Called when a player finished a strafing run.
  -- @function [parent=#RANGE] OnAfterStrafeResult
  -- @param #RANGE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #RANGE.PlayerData player Data of player settings etc.
  -- @param #RANGE.StrafeResult result Data of the strafing run.

  --- Triggers the FSM event "EnterRange".
  -- @function [parent=#RANGE] EnterRange
  -- @param #RANGE self
  -- @param #RANGE.PlayerData player Data of player settings etc.

  --- Triggers the FSM delayed event "EnterRange".
  -- @function [parent=#RANGE] __EnterRange
  -- @param #RANGE self
  -- @param #number delay Delay in seconds before the function is called.
  -- @param #RANGE.PlayerData player Data of player settings etc.

  --- On after "EnterRange" event user function. Called when a player enters the range zone.
  -- @function [parent=#RANGE] OnAfterEnterRange
  -- @param #RANGE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #RANGE.PlayerData player Data of player settings etc.

  --- Triggers the FSM event "ExitRange".
  -- @function [parent=#RANGE] ExitRange
  -- @param #RANGE self
  -- @param #RANGE.PlayerData player Data of player settings etc.

  --- Triggers the FSM delayed event "ExitRange".
  -- @function [parent=#RANGE] __ExitRange
  -- @param #RANGE self
  -- @param #number delay Delay in seconds before the function is called.
  -- @param #RANGE.PlayerData player Data of player settings etc.

  --- On after "ExitRange" event user function. Called when a player leaves the range zone.
  -- @function [parent=#RANGE] OnAfterExitRange
  -- @param #RANGE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #RANGE.PlayerData player Data of player settings etc.

  -- Return object.
  return self
end

--- Initializes number of targets and location of the range. Starts the event handlers.
-- @param #RANGE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RANGE:onafterStart()

  -- Location/coordinate of range.
  local _location = nil

  -- Count bomb targets.
  local _count = 0
  for _, _target in pairs( self.bombingTargets ) do
    _count = _count + 1

    -- Get range location.
    if _location == nil then
      _location = self:_GetBombTargetCoordinate( _target )
    end
  end
  self.nbombtargets = _count

  -- Count strafing targets.
  _count = 0
  for _, _target in pairs( self.strafeTargets ) do
    _count = _count + 1

    for _, _unit in pairs( _target.targets ) do
      if _location == nil then
        _location = _unit:GetCoordinate()
      end
    end
  end
  self.nstrafetargets = _count

  -- Location of the range. We simply take the first unit/target we find if it was not explicitly specified by the user.
  if self.location == nil then
    self.location = _location
  end

  if self.location == nil then
    local text = string.format( "ERROR! No range location found. Number of strafe targets = %d. Number of bomb targets = %d.", self.nstrafetargets, self.nbombtargets )
    self:E( self.lid .. text )
    return
  end

  -- Define a MOOSE zone of the range.
  if self.rangezone == nil then
    self.rangezone = ZONE_RADIUS:New( self.rangename, { x = self.location.x, y = self.location.z }, self.rangeradius )
  end

  -- Starting range.
  local text = string.format( "Starting RANGE %s. Number of strafe targets = %d. Number of bomb targets = %d.", self.rangename, self.nstrafetargets, self.nbombtargets )
  self:I( self.lid .. text )

  -- Event handling.
  self:HandleEvent( EVENTS.Birth )
  self:HandleEvent( EVENTS.Hit )
  self:HandleEvent( EVENTS.Shot )

  -- Make bomb target move randomly within the range zone.
  for _, _target in pairs( self.bombingTargets ) do
    local target=_target --#RANGE.BombTarget

    -- Check if unit and can move.
    if target.move and target.type==RANGE.TargetType.UNIT and target.speed > 1 then
      target.target:PatrolZones( { self.rangezone }, target.speed * 0.75, ENUMS.Formation.Vehicle.OffRoad )
    end

  end

  -- Init range control.
  if self.rangecontrolfreq and not self.useSRS then

    -- Radio queue.
    self.rangecontrol = RADIOQUEUE:New( self.rangecontrolfreq, nil, self.rangename )
    self.rangecontrol.schedonce = true

    -- Init numbers.
    self.rangecontrol:SetDigit( 0, self.Sound.RC0.filename, self.Sound.RC0.duration, self.soundpath )
    self.rangecontrol:SetDigit( 1, self.Sound.RC1.filename, self.Sound.RC1.duration, self.soundpath )
    self.rangecontrol:SetDigit( 2, self.Sound.RC2.filename, self.Sound.RC2.duration, self.soundpath )
    self.rangecontrol:SetDigit( 3, self.Sound.RC3.filename, self.Sound.RC3.duration, self.soundpath )
    self.rangecontrol:SetDigit( 4, self.Sound.RC4.filename, self.Sound.RC4.duration, self.soundpath )
    self.rangecontrol:SetDigit( 5, self.Sound.RC5.filename, self.Sound.RC5.duration, self.soundpath )
    self.rangecontrol:SetDigit( 6, self.Sound.RC6.filename, self.Sound.RC6.duration, self.soundpath )
    self.rangecontrol:SetDigit( 7, self.Sound.RC7.filename, self.Sound.RC7.duration, self.soundpath )
    self.rangecontrol:SetDigit( 8, self.Sound.RC8.filename, self.Sound.RC8.duration, self.soundpath )
    self.rangecontrol:SetDigit( 9, self.Sound.RC9.filename, self.Sound.RC9.duration, self.soundpath )

    -- Set location where the messages are transmitted from.
    self.rangecontrol:SetSenderCoordinate( self.location )
    self.rangecontrol:SetSenderUnitName( self.rangecontrolrelayname )

    -- Start range control radio queue.
    self.rangecontrol:Start( 1, 0.1 )

    -- Init range control.
    if self.instructorfreq and not self.useSRS then

      -- Radio queue.
      self.instructor = RADIOQUEUE:New( self.instructorfreq, nil, self.rangename )
      self.instructor.schedonce = true

      -- Init numbers.
      self.instructor:SetDigit( 0, self.Sound.IR0.filename, self.Sound.IR0.duration, self.soundpath )
      self.instructor:SetDigit( 1, self.Sound.IR1.filename, self.Sound.IR1.duration, self.soundpath )
      self.instructor:SetDigit( 2, self.Sound.IR2.filename, self.Sound.IR2.duration, self.soundpath )
      self.instructor:SetDigit( 3, self.Sound.IR3.filename, self.Sound.IR3.duration, self.soundpath )
      self.instructor:SetDigit( 4, self.Sound.IR4.filename, self.Sound.IR4.duration, self.soundpath )
      self.instructor:SetDigit( 5, self.Sound.IR5.filename, self.Sound.IR5.duration, self.soundpath )
      self.instructor:SetDigit( 6, self.Sound.IR6.filename, self.Sound.IR6.duration, self.soundpath )
      self.instructor:SetDigit( 7, self.Sound.IR7.filename, self.Sound.IR7.duration, self.soundpath )
      self.instructor:SetDigit( 8, self.Sound.IR8.filename, self.Sound.IR8.duration, self.soundpath )
      self.instructor:SetDigit( 9, self.Sound.IR9.filename, self.Sound.IR9.duration, self.soundpath )

      -- Set location where the messages are transmitted from.
      self.instructor:SetSenderCoordinate( self.location )
      self.instructor:SetSenderUnitName( self.instructorrelayname )

      -- Start instructor radio queue.
      self.instructor:Start( 1, 0.1 )

    end

  end

  -- Load prev results.
  if self.autosave then
    self:Load()
  end

  -- Debug mode: smoke all targets and range zone.
  if self.Debug then
    self:_MarkTargetsOnMap()
    self:_SmokeBombTargets()
    self:_SmokeStrafeTargets()
    self:_SmokeStrafeTargetBoxes()
    self.rangezone:SmokeZone( SMOKECOLOR.White )
  end

  self:__Status( -10 )
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set the root F10 menu under which the range F10 menu is created.
-- @param #RANGE self
-- @param Core.Menu#MENU_MISSION menu The root F10 menu.
-- @return #RANGE self
function RANGE:SetMenuRoot(menu)
  self.menuF10root=menu
  return self
end


--- Set maximal strafing altitude. Player entering a strafe pit above that altitude are not registered for a valid pass.
-- @param #RANGE self
-- @param #number maxalt Maximum altitude in meters AGL. Default is 914 m = 3000 ft.
-- @return #RANGE self
function RANGE:SetMaxStrafeAlt( maxalt )
  self.strafemaxalt = maxalt or RANGE.Defaults.strafemaxalt
  return self
end

--- Set time interval for tracking bombs. A smaller time step increases accuracy but needs more CPU time.
-- @param #RANGE self
-- @param #number dt Time interval in seconds. Default is 0.005 s.
-- @return #RANGE self
function RANGE:SetBombtrackTimestep( dt )
  self.dtBombtrack = dt or RANGE.Defaults.dtBombtrack
  return self
end

--- Set time how long (most) messages are displayed.
-- @param #RANGE self
-- @param #number time Time in seconds. Default is 30 s.
-- @return #RANGE self
function RANGE:SetMessageTimeDuration( time )
  self.Tmsg = time or RANGE.Defaults.Tmsg
  return self
end

--- Automatically save player results to disc.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:SetAutosaveOn()
  self.autosave = true
  return self
end

--- Switch off auto save player results.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:SetAutosaveOff()
  self.autosave = false
  return self
end

--- Enable saving of player's target sheets and specify an optional directory path.
-- @param #RANGE self
-- @param #string path (Optional) Path where to save the target sheets.
-- @param #string prefix (Optional) Prefix for target sheet files. File name will be saved as *prefix_aircrafttype-0001.csv*, *prefix_aircrafttype-0002.csv*, etc.
-- @return #RANGE self
function RANGE:SetTargetSheet( path, prefix )
  if io then
    self.targetsheet = true
    self.targetpath = path
    self.targetprefix = prefix
  else
    self:E( self.lid .. "ERROR: io is not desanitized. Cannot save target sheet." )
  end
  return self
end

--- Set FunkMan socket. Bombing and strafing results will be send to your Discord bot.
-- **Requires running FunkMan program**.
-- @param #RANGE self
-- @param #number Port Port. Default `10042`.
-- @param #string Host Host. Default "127.0.0.1".
-- @return #RANGE self
function RANGE:SetFunkManOn(Port, Host)

  self.funkmanSocket=SOCKET:New(Port, Host)

  return self
end

--- Set messages to examiner. The examiner will receive messages from all clients.
-- @param #RANGE self
-- @param #string examinergroupname Name of the group of the examiner.
-- @param #boolean exclusively If true, messages are send exclusively to the examiner, i.e. not to the clients.
-- @return #RANGE self
function RANGE:SetMessageToExaminer( examinergroupname, exclusively )
  self.examinergroupname = examinergroupname
  self.examinerexclusive = exclusively
  return self
end

--- Set max number of player results that are displayed.
-- @param #RANGE self
-- @param #number nmax Number of results. Default is 10.
-- @return #RANGE self
function RANGE:SetDisplayedMaxPlayerResults( nmax )
  self.ndisplayresult = nmax or RANGE.Defaults.ndisplayresult
  return self
end

--- Set range radius. Defines the area in which e.g. bomb impacts are smoked.
-- @param #RANGE self
-- @param #number radius Radius in km. Default 5 km.
-- @return #RANGE self
function RANGE:SetRangeRadius( radius )
  self.rangeradius = radius * 1000 or RANGE.Defaults.rangeradius
  return self
end

--- Set player setting whether bomb impact points are smoked or not.
-- @param #RANGE self
-- @param #boolean switch If true nor nil default is to smoke impact points of bombs.
-- @return #RANGE self
function RANGE:SetDefaultPlayerSmokeBomb( switch )
  if switch == true or switch == nil then
    self.defaultsmokebomb = true
  else
    self.defaultsmokebomb = false
  end
  return self
end

--- Set bomb track threshold distance. Bombs/rockets/missiles are only tracked if player-range distance is less than this distance. Default 25 km.
-- @param #RANGE self
-- @param #number distance Threshold distance in km. Default 25 km.
-- @return #RANGE self
function RANGE:SetBombtrackThreshold( distance )
  self.BombtrackThreshold = (distance or 25) * 1000
  return self
end

--- Set range location. If this is not done, one (random) unit position of the range is used to determine the location of the range.
-- The range location determines the position at which the weather data is evaluated.
-- @param #RANGE self
-- @param Core.Point#COORDINATE coordinate Coordinate of the range.
-- @return #RANGE self
function RANGE:SetRangeLocation( coordinate )
  self.location = coordinate
  return self
end

--- Set range zone. For example, no bomb impact points are smoked if a bomb falls outside of this zone.
-- If a zone is not explicitly specified, the range zone is determined by its location and radius.
-- @param #RANGE self
-- @param Core.Zone#ZONE zone MOOSE zone defining the range perimeters.
-- @return #RANGE self
function RANGE:SetRangeZone( zone )
  if zone and type(zone)=="string" then
    zone=ZONE:FindByName(zone)
  end
  self.rangezone = zone
  return self
end

--- Set range ceiling altitude in feet MSL.
-- @param #RANGE self
-- @param #number altitude (optional) Ceiling altitude of the range in ft MSL. Default 20000ft MSL
-- @return #RANGE self
function RANGE:SetRangeCeiling( altitude )
  self:T(self.lid.."SetRangeCeiling")
  if altitude and type(altitude) == "number" then
    self.ceilingaltitude=altitude
  else
    self:E(self.lid.."Altitude either not provided or is not a number, using default setting (20000).")
    self.ceilingaltitude=20000
  end
  return self
end

--- Enable range ceiling. Aircraft must be below the ceiling altitude to be considered in the range zone. 
-- @param #RANGE self
-- @param #boolean enabled True if you would like to enable the ceiling check.  If no value give, will Default to false.
-- @return #RANGE self
function RANGE:EnableRangeCeiling( enabled )
  self:T(self.lid.."EnableRangeCeiling")
  if enabled and type(enabled) == "boolean" then
    self.ceilingenabled=enabled
  else
    self:E(self.lid.."Enabled either not provide or is not a boolean, using default setting (false).")
    self.ceilingenabled=false
  end

  return self
end

--- Set smoke color for marking bomb targets. By default bomb targets are marked by red smoke.
-- @param #RANGE self
-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default `SMOKECOLOR.Red`.
-- @return #RANGE self
function RANGE:SetBombTargetSmokeColor( colorid )
  self.BombSmokeColor = colorid or SMOKECOLOR.Red
  return self
end

--- Set score bomb distance.
-- @param #RANGE self
-- @param #number distance Distance in meters. Default 1000 m.
-- @return #RANGE self
function RANGE:SetScoreBombDistance( distance )
  self.scorebombdistance = distance or 1000
  return self
end

--- Set smoke color for marking strafe targets. By default strafe targets are marked by green smoke.
-- @param #RANGE self
-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default `SMOKECOLOR.Green`.
-- @return #RANGE self
function RANGE:SetStrafeTargetSmokeColor( colorid )
  self.StrafeSmokeColor = colorid or SMOKECOLOR.Green
  return self
end

--- Set smoke color for marking strafe pit approach boxes. By default strafe pit boxes are marked by white smoke.
-- @param #RANGE self
-- @param Utilities.Utils#SMOKECOLOR colorid Color id. Default `SMOKECOLOR.White`.
-- @return #RANGE self
function RANGE:SetStrafePitSmokeColor( colorid )
  self.StrafePitSmokeColor = colorid or SMOKECOLOR.White
  return self
end

--- Set time delay between bomb impact and starting to smoke the impact point.
-- @param #RANGE self
-- @param #number delay Time delay in seconds. Default is 3 seconds.
-- @return #RANGE self
function RANGE:SetSmokeTimeDelay( delay )
  self.TdelaySmoke = delay or RANGE.Defaults.TdelaySmoke
  return self
end

--- Enable debug modus.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:DebugON()
  self.Debug = true
  return self
end

--- Disable debug modus.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:DebugOFF()
  self.Debug = false
  return self
end

--- Disable ALL messages to players.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:SetMessagesOFF()
  self.messages = false
  return self
end

--- Enable messages to players. This is the default
-- @param #RANGE self
-- @return #RANGE self
function RANGE:SetMessagesON()
  self.messages = true
  return self
end

--- Enables tracking of all bomb types. Note that this is the default setting.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:TrackBombsON()
  self.trackbombs = true
  return self
end

--- Disables tracking of all bomb types.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:TrackBombsOFF()
  self.trackbombs = false
  return self
end

--- Enables tracking of all rocket types. Note that this is the default setting.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:TrackRocketsON()
  self.trackrockets = true
  return self
end

--- Disables tracking of all rocket types.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:TrackRocketsOFF()
  self.trackrockets = false
  return self
end

--- Enables tracking of all missile types. Note that this is the default setting.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:TrackMissilesON()
  self.trackmissiles = true
  return self
end

--- Disables tracking of all missile types.
-- @param #RANGE self
-- @return #RANGE self
function RANGE:TrackMissilesOFF()
  self.trackmissiles = false
  return self
end

--- Use SRS Simple-Text-To-Speech for transmissions. No sound files necessary.
-- @param #RANGE self
-- @param #string PathToSRS Path to SRS directory.
-- @param #number Port SRS port. Default 5002.
-- @param #number Coalition Coalition side, e.g. `coalition.side.BLUE` or `coalition.side.RED`. Default `coalition.side.BLUE`.
-- @param #number Frequency Frequency to use. Default is 256 MHz for range control and 305 MHz for instructor. If given, both control and instructor get this frequency.
-- @param #number Modulation Modulation to use, defaults to radio.modulation.AM
-- @param #number Volume Volume, between 0.0 and 1.0. Defaults to 1.0
-- @param #string PathToGoogleKey Path to Google TTS credentials.
-- @return #RANGE self
function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume, PathToGoogleKey)

  if PathToSRS or MSRS.path then

    self.useSRS=true

    self.controlmsrs=MSRS:New(PathToSRS or MSRS.path, Frequency or 256, Modulation or radio.modulation.AM)
    self.controlmsrs:SetPort(Port or MSRS.port)
    self.controlmsrs:SetCoalition(Coalition or coalition.side.BLUE)
    self.controlmsrs:SetLabel("RANGEC")
    self.controlmsrs:SetVolume(Volume or 1.0)
    self.controlsrsQ = MSRSQUEUE:New("CONTROL")

    self.instructmsrs=MSRS:New(PathToSRS or MSRS.path, Frequency or 305, Modulation or radio.modulation.AM)
    self.instructmsrs:SetPort(Port or MSRS.port)
    self.instructmsrs:SetCoalition(Coalition or coalition.side.BLUE)
    self.instructmsrs:SetLabel("RANGEI")
    self.instructmsrs:SetVolume(Volume or 1.0)
    self.instructsrsQ = MSRSQUEUE:New("INSTRUCT")
    
    if PathToGoogleKey then 
      self.controlmsrs:SetProviderOptionsGoogle(PathToGoogleKey,PathToGoogleKey)
      self.controlmsrs:SetProvider(MSRS.Provider.GOOGLE)
      self.instructmsrs:SetProviderOptionsGoogle(PathToGoogleKey,PathToGoogleKey)
      self.instructmsrs:SetProvider(MSRS.Provider.GOOGLE)
    end

  else
    self:E(self.lid..string.format("ERROR: No SRS path specified!"))
  end
  return self
end

--- (SRS) Set range control frequency and voice. Use `RANGE:SetSRS()` once first before using this function.
-- @param #RANGE self
-- @param #number frequency Frequency in MHz. Default 256 MHz.
-- @param #number modulation Modulation, defaults to radio.modulation.AM.
-- @param #string voice Voice.
-- @param #string culture Culture, defaults to "en-US".
-- @param #string gender Gender, defaults to "female".
-- @param #string relayunitname Name of the unit used for transmission location.
-- @return #RANGE self
function RANGE:SetSRSRangeControl( frequency, modulation, voice, culture, gender, relayunitname )
  if not self.instructmsrs then
    self:E(self.lid.."Use myrange:SetSRS() once first before using myrange:SetSRSRangeControl!")
    return self
  end
  self.rangecontrolfreq = frequency or 256
  self.controlmsrs:SetFrequencies(self.rangecontrolfreq)
  self.controlmsrs:SetModulations(modulation or radio.modulation.AM)
  self.controlmsrs:SetVoice(voice)
  self.controlmsrs:SetCulture(culture or "en-US")
  self.controlmsrs:SetGender(gender or "female")
  self.rangecontrol = true
  if relayunitname then
    local unit = UNIT:FindByName(relayunitname)
    local Coordinate = unit:GetCoordinate()
    self.rangecontrolrelayname = relayunitname
  end
  return self
end

--- (SRS) Set range instructor frequency and voice. Use `RANGE:SetSRS()` once first before using this function.
-- @param #RANGE self
-- @param #number frequency Frequency in MHz. Default 305 MHz.
-- @param #number modulation Modulation, defaults to radio.modulation.AM.
-- @param #string voice Voice.
-- @param #string culture Culture, defaults to "en-US".
-- @param #string gender Gender, defaults to "male".
-- @param #string relayunitname Name of the unit used for transmission location.
-- @return #RANGE self
function RANGE:SetSRSRangeInstructor( frequency, modulation, voice, culture, gender, relayunitname )
  if not self.instructmsrs then
    self:E(self.lid.."Use myrange:SetSRS() once first before using myrange:SetSRSRangeInstructor!")
    return self
  end
  self.instructorfreq = frequency or 305
  self.instructmsrs:SetFrequencies(self.instructorfreq)
  self.instructmsrs:SetModulations(modulation or radio.modulation.AM)
  self.instructmsrs:SetVoice(voice)
  self.instructmsrs:SetCulture(culture or "en-US")
  self.instructmsrs:SetGender(gender or "male")
  self.instructor = true
  if relayunitname then
    local unit = UNIT:FindByName(relayunitname)
    local Coordinate = unit:GetCoordinate()
    self.instructmsrs:SetCoordinate(Coordinate)
    self.instructorrelayname = relayunitname
  end
  return self
end

--- Enable range control and set frequency (non-SRS).
-- @param #RANGE self
-- @param #number frequency Frequency in MHz. Default 256 MHz.
-- @param #string relayunitname Name of the unit used for transmission.
-- @return #RANGE self
function RANGE:SetRangeControl( frequency, relayunitname )
  self.rangecontrolfreq = frequency or 256
  self.rangecontrolrelayname = relayunitname
  return self
end

--- Enable instructor radio and set frequency (non-SRS).
-- @param #RANGE self
-- @param #number frequency Frequency in MHz. Default 305 MHz.
-- @param #string relayunitname Name of the unit used for transmission.
-- @return #RANGE self
function RANGE:SetInstructorRadio( frequency, relayunitname )
  self.instructorfreq = frequency or 305
  self.instructorrelayname = relayunitname
  return self
end

--- Set sound files folder within miz file.
-- @param #RANGE self
-- @param #string path Path for sound files. Default "Range Soundfiles/". Mind the slash "/" at the end!
-- @return #RANGE self
function RANGE:SetSoundfilesPath( path )
  self.soundpath = tostring( path or "Range Soundfiles/" )
  self:T2( self.lid .. string.format( "Setting sound files path to %s", self.soundpath ) )
  return self
end

--- Set the path to the csv file that contains information about the used sound files.
-- The parameter file has to be located on your local disk (**not** inside the miz file).
-- @param #RANGE self
-- @param #string csvfile Full path to the csv file on your local disk.
-- @return #RANGE self
function RANGE:SetSoundfilesInfo( csvfile )

  --- Local function to return the ATIS.Soundfile for a given file name
  local function getSound(filename)
    for key,_soundfile in pairs(self.Sound) do
      local soundfile=_soundfile --#RANGE.Soundfile
      if filename==soundfile.filename then
        return soundfile
      end
    end
    return nil
  end

  -- Read csv file
  local data=UTILS.ReadCSV(csvfile)

  if data then

    for i,sound in pairs(data) do

      -- Get the ATIS.Soundfile
      local soundfile=getSound(sound.filename..".ogg") --#RANGE.Soundfile

      if soundfile then

        -- Set duration
        soundfile.duration=tonumber(sound.duration)

      else
        self:E(string.format("ERROR: Could not get info for sound file %s", sound.filename))
      end

    end
  else
    self:E(string.format("ERROR: Could not read sound csv file!"))
  end


  return self
end


--- Add new strafe pit. For a strafe pit, hits from guns are counted. One pit can consist of several units.
-- A strafe run approach is only valid if the player enters via a zone in front of the pit, which is defined by boxlength, boxwidth, and heading.
-- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach.
-- @param #RANGE self
-- @param #table targetnames Single or multiple (Table) unit or static names defining the strafe targets. The first target in the list determines the approach box origin (heading and box).
-- @param #number boxlength (Optional) Length of the approach box in meters. Default is 3000 m.
-- @param #number boxwidth (Optional) Width of the approach box in meters. Default is 300 m.
-- @param #number heading (Optional) Approach box heading in degrees (originating FROM the target). Default is the heading set in the ME for the first target unit
-- @param #boolean inverseheading (Optional) Use inverse heading (heading --> heading - 180 Degrees). Default is false.
-- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20.
-- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default is 610 m = 2000 ft. Set to 0 for no foul line.
-- @return #RANGE self
function RANGE:AddStrafePit( targetnames, boxlength, boxwidth, heading, inverseheading, goodpass, foulline )
  self:F( { targetnames = targetnames, boxlength = boxlength, boxwidth = boxwidth, heading = heading, inverseheading = inverseheading, goodpass = goodpass, foulline = foulline } )

  -- Create table if necessary.
  if type( targetnames ) ~= "table" then
    targetnames = { targetnames }
  end

  -- Make targets
  local _targets = {}
  local center = nil -- Wrapper.Unit#UNIT
  local ntargets = 0

  for _i, _name in ipairs( targetnames ) do

    -- Check if we have a static or unit object.
    local _isstatic = self:_CheckStatic( _name )

    local unit = nil
    if _isstatic == true then

      -- Add static object.
      self:T( self.lid .. string.format( "Adding STATIC object %s as strafe target #%d.", _name, _i ) )
      unit = STATIC:FindByName( _name, false )

    elseif _isstatic == false then

      -- Add unit object.
      self:T( self.lid .. string.format( "Adding UNIT object %s as strafe target #%d.", _name, _i ) )
      unit = UNIT:FindByName( _name )

    else

      -- Neither unit nor static object with this name could be found.
      local text = string.format( "ERROR! Could not find ANY strafe target object with name %s.", _name )
      self:E( self.lid .. text )

    end

    -- Add object to targets.
    if unit then
      table.insert( _targets, unit )
      -- Define center as the first unit we find
      if center == nil then
        center = unit
      end
      ntargets = ntargets + 1
    end

  end

  -- Check if at least one target could be found.
  if ntargets == 0 then
    local text = string.format( "ERROR! No strafe target could be found when calling RANGE:AddStrafePit() for range %s", self.rangename )
    self:E( self.lid .. text )
    return
  end

  -- Approach box dimensions.
  local l = boxlength or RANGE.Defaults.boxlength
  local w = (boxwidth or RANGE.Defaults.boxwidth) / 2

  -- Heading: either manually entered or automatically taken from unit heading.
  local heading = heading or center:GetHeading()

  -- Invert the heading since some units point in the "wrong" direction. In particular the strafe pit from 476th range objects.
  if inverseheading ~= nil then
    if inverseheading then
      heading = heading - 180
    end
  end
  if heading < 0 then
    heading = heading + 360
  end
  if heading > 360 then
    heading = heading - 360
  end

  -- Number of hits called a "good" pass.
  goodpass = goodpass or RANGE.Defaults.goodpass

  -- Foule line distance.
  foulline = foulline or RANGE.Defaults.foulline

  -- Coordinate of the range.
  local Ccenter = center:GetCoordinate()

  -- Name of the target defined as its unit name.
  local _name = center:GetName()

  -- Points defining the approach area.
  local p = {}
  p[#p + 1] = Ccenter:Translate( w, heading + 90 )
  p[#p + 1] = p[#p]:Translate( l, heading )
  p[#p + 1] = p[#p]:Translate( 2 * w, heading - 90 )
  p[#p + 1] = p[#p]:Translate( -l, heading )

  local pv2 = {}
  for i, p in ipairs( p ) do
    pv2[i] = { x = p.x, y = p.z }
  end

  -- Create polygon zone.
  local _polygon = ZONE_POLYGON_BASE:New( _name, pv2 )

  -- Create tires
  -- _polygon:BoundZone()

  local st = {} -- #RANGE.StrafeTarget
  st.name = _name
  st.polygon = _polygon
  st.coordinate = Ccenter
  st.goodPass = goodpass
  st.targets = _targets
  st.foulline = foulline
  st.smokepoints = p
  st.heading = heading

  -- Add zone to table.
  table.insert( self.strafeTargets, st )

  -- Debug info
  local text = string.format( "Adding new strafe target %s with %d targets: heading = %03d, box_L = %.1f, box_W = %.1f, goodpass = %d, foul line = %.1f", _name, ntargets, heading, l, w, goodpass, foulline )
  self:T( self.lid .. text )

  return self
end

--- Add all units of a group as one new strafe target pit.
-- For a strafe pit, hits from guns are counted. One pit can consist of several units.
-- Note, an approach is only valid, if the player enters via a zone in front of the pit, which defined by boxlength and boxheading.
-- Furthermore, the player must not be too high and fly in the direction of the pit to make a valid target apporoach.
-- @param #RANGE self
-- @param Wrapper.Group#GROUP group MOOSE group of unit names defining the strafe target pit. The first unit in the group determines the approach zone (heading and box).
-- @param #number boxlength (Optional) Length of the approach box in meters. Default is 3000 m.
-- @param #number boxwidth (Optional) Width of the approach box in meters. Default is 300 m.
-- @param #number heading (Optional) Approach heading in Degrees. Default is heading of the unit as defined in the mission editor.
-- @param #boolean inverseheading (Optional) Take inverse heading (heading --> heading - 180 Degrees). Default is false.
-- @param #number goodpass (Optional) Number of hits for a "good" strafing pass. Default is 20.
-- @param #number foulline (Optional) Foul line distance. Hits from closer than this distance are not counted. Default 610 m = 2000 ft. Set to 0 for no foul line.
-- @return #RANGE self
function RANGE:AddStrafePitGroup( group, boxlength, boxwidth, heading, inverseheading, goodpass, foulline )
  self:F( { group = group, boxlength = boxlength, boxwidth = boxwidth, heading = heading, inverseheading = inverseheading, goodpass = goodpass, foulline = foulline } )

  if group and group:IsAlive() then

    -- Get units of group.
    local _units = group:GetUnits()

    -- Make table of unit names.
    local _names = {}
    for _, _unit in ipairs( _units ) do

      local _unit = _unit -- Wrapper.Unit#UNIT

      if _unit and _unit:IsAlive() then
        local _name = _unit:GetName()
        table.insert( _names, _name )
      end

    end

    -- Add strafe pit.
    self:AddStrafePit( _names, boxlength, boxwidth, heading, inverseheading, goodpass, foulline )
  end

  return self
end

--- Add bombing target(s) to range.
-- @param #RANGE self
-- @param #table targetnames Single or multiple (Table) names of unit or static objects serving as bomb targets.
-- @param #number goodhitrange (Optional) Max distance from target unit (in meters) which is considered as a good hit. Default is 25 m.
-- @param #boolean randommove If true, unit will move randomly within the range. Default is false.
-- @return #RANGE self
function RANGE:AddBombingTargets( targetnames, goodhitrange, randommove )
  self:F( { targetnames = targetnames, goodhitrange = goodhitrange, randommove = randommove } )

  -- Create a table if necessary.
  if type( targetnames ) ~= "table" then
    targetnames = { targetnames }
  end

  -- Default range is 25 m.
  goodhitrange = goodhitrange or RANGE.Defaults.goodhitrange

  for _, name in pairs( targetnames ) do

    -- Check if we have a static or unit object.
    local _isstatic = self:_CheckStatic( name )

    if _isstatic == true then
      local _static = STATIC:FindByName( name )
      self:T2( self.lid .. string.format( "Adding static bombing target %s with hit range %d.", name, goodhitrange, false ) )
      self:AddBombingTargetUnit( _static, goodhitrange )
    elseif _isstatic == false then
      local _unit = UNIT:FindByName( name )
      self:T2( self.lid .. string.format( "Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove ) )
      self:AddBombingTargetUnit( _unit, goodhitrange, randommove )
    else
      self:E( self.lid .. string.format( "ERROR! Could not find bombing target %s.", name ) )
    end

  end

  return self
end

--- Add a unit or static object as bombing target.
-- @param #RANGE self
-- @param Wrapper.Positionable#POSITIONABLE unit Positionable (unit or static) of the bombing target.
-- @param #number goodhitrange Max distance from unit which is considered as a good hit.
-- @param #boolean randommove If true, unit will move randomly within the range. Default is false.
-- @return #RANGE self
function RANGE:AddBombingTargetUnit( unit, goodhitrange, randommove )
  self:F( { unit = unit, goodhitrange = goodhitrange, randommove = randommove } )

  -- Get name of positionable.
  local name = unit:GetName()

  -- Check if we have a static or unit object.
  local _isstatic = self:_CheckStatic( name )

  -- Default range is 25 m.
  goodhitrange = goodhitrange or RANGE.Defaults.goodhitrange

  -- Set randommove to false if it was not specified.
  if randommove == nil or _isstatic == true then
    randommove = false
  end

  -- Debug or error output.
  if _isstatic == true then
    self:T( self.lid .. string.format( "Adding STATIC bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring( randommove ) ) )
  elseif _isstatic == false then
    self:T( self.lid .. string.format( "Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring( randommove ) ) )
  else
    self:E( self.lid .. string.format( "ERROR! No bombing target with name %s could be found. Carefully check all UNIT and STATIC names defined in the mission editor!", name ) )
  end

  -- Get max speed of unit in km/h.
  local speed = 0
  if _isstatic == false then
    speed = self:_GetSpeed( unit )
  end

  local target = {} -- #RANGE.BombTarget
  target.name = name
  target.target = unit
  target.goodhitrange = goodhitrange
  target.move = randommove
  target.speed = speed
  target.coordinate = unit:GetCoordinate()
  if _isstatic then
    target.type = RANGE.TargetType.STATIC
  else
    target.type = RANGE.TargetType.UNIT
  end

  -- Insert target to table.
  table.insert( self.bombingTargets, target )

  return self
end

--- Add a coordinate of a bombing target. This
-- @param #RANGE self
-- @param Core.Point#COORDINATE coord The coordinate.
-- @param #string name Name of target.
-- @param #number goodhitrange Max distance from unit which is considered as a good hit.
-- @return #RANGE self
function RANGE:AddBombingTargetCoordinate( coord, name, goodhitrange )

  local target = {} -- #RANGE.BombTarget
  target.name = name or "Bomb Target"
  target.target = nil
  target.goodhitrange = goodhitrange or RANGE.Defaults.goodhitrange
  target.move = false
  target.speed = 0
  target.coordinate = coord
  target.type = RANGE.TargetType.COORD

  -- Insert target to table.
  table.insert( self.bombingTargets, target )

  return self
end

--- Add a scenery object as bombing target.
-- @param #RANGE self
-- @param Wrapper.Scenery#SCENERY scenery Scenary object.
-- @param #number goodhitrange Max distance from unit which is considered as a good hit.
-- @return #RANGE self
function RANGE:AddBombingTargetScenery( scenery, goodhitrange)

  -- Get name of positionable.
  local name = scenery:GetName()

  -- Default range is 25 m.
  goodhitrange = goodhitrange or RANGE.Defaults.goodhitrange

  -- Debug or error output.
  if name then
    self:T( self.lid .. string.format( "Adding SCENERY bombing target %s with good hit range %d", name, goodhitrange) )
  else
    self:E( self.lid .. string.format( "ERROR! No bombing target with name %s could be found!", name ) )
  end


  local target = {} -- #RANGE.BombTarget
  target.name = name
  target.target = scenery
  target.goodhitrange = goodhitrange
  target.move = false
  target.speed = 0
  target.coordinate = scenery:GetCoordinate()
  target.type = RANGE.TargetType.SCENERY

  -- Insert target to table.
  table.insert( self.bombingTargets, target )

  return self
end

--- Add all units of a group as bombing targets.
-- @param #RANGE self
-- @param Wrapper.Group#GROUP group Group of bombing targets. Can also be given as group name.
-- @param #number goodhitrange Max distance from unit which is considered as a good hit.
-- @param #boolean randommove If true, unit will move randomly within the range. Default is false.
-- @return #RANGE self
function RANGE:AddBombingTargetGroup( group, goodhitrange, randommove )
  self:F( { group = group, goodhitrange = goodhitrange, randommove = randommove } )
  
  if group and type(group)=="string" then
    group=GROUP:FindByName(group)
  end

  if group then

    local _units = group:GetUnits()

    for _, _unit in pairs( _units ) do
      if _unit and _unit:IsAlive() then
        self:AddBombingTargetUnit( _unit, goodhitrange, randommove )
      end
    end
  end

  return self
end

--- Measures the foule line distance between two unit or static objects.
-- @param #RANGE self
-- @param #string namepit Name of the strafe pit target object.
-- @param #string namefoulline Name of the fould line distance marker object.
-- @return #number Foul line distance in meters.
function RANGE:GetFoullineDistance( namepit, namefoulline )
  self:F( { namepit = namepit, namefoulline = namefoulline } )

  -- Check if we have units or statics.
  local _staticpit = self:_CheckStatic( namepit )
  local _staticfoul = self:_CheckStatic( namefoulline )

  -- Get the unit or static pit object.
  local pit = nil
  if _staticpit == true then
    pit = STATIC:FindByName( namepit, false )
  elseif _staticpit == false then
    pit = UNIT:FindByName( namepit )
  else
    self:E( self.lid .. string.format( "ERROR! Pit object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namepit ) )
  end

  -- Get the unit or static foul line object.
  local foul = nil
  if _staticfoul == true then
    foul = STATIC:FindByName( namefoulline, false )
  elseif _staticfoul == false then
    foul = UNIT:FindByName( namefoulline )
  else
    self:E( self.lid .. string.format( "ERROR! Foul line object %s could not be found in GetFoullineDistance function. Check the name in the ME.", namefoulline ) )
  end

  -- Get the distance between the two objects.
  local fouldist = 0
  if pit ~= nil and foul ~= nil then
    fouldist = pit:GetCoordinate():Get2DDistance( foul:GetCoordinate() )
  else
    self:E( self.lid .. string.format( "ERROR! Foul line distance could not be determined. Check pit object name %s and foul line object name %s in the ME.", namepit, namefoulline ) )
  end

  self:T( self.lid .. string.format( "Foul line distance = %.1f m.", fouldist ) )
  return fouldist
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event Handling
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Range event handler for event birth.
-- @param #RANGE self
-- @param Core.Event#EVENTDATA EventData
function RANGE:OnEventBirth( EventData )
  self:F( { eventbirth = EventData } )

  if not EventData.IniPlayerName then return end

  local _unitName = EventData.IniUnitName
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName, EventData.IniPlayerName )

  self:T3( self.lid .. "BIRTH: unit   = " .. tostring( EventData.IniUnitName ) )
  self:T3( self.lid .. "BIRTH: group  = " .. tostring( EventData.IniGroupName ) )
  self:T3( self.lid .. "BIRTH: player = " .. tostring( _playername ) )

  if _unit and _playername then

    local _uid = _unit:GetID()
    local _group = _unit:GetGroup()
    local _gid = _group:GetID()
    local _callsign = _unit:GetCallsign()

    -- Debug output.
    local text = string.format( "Player %s, callsign %s entered unit %s (UID %d) of group %s (GID %d)", _playername, _callsign, _unitName, _uid, _group:GetName(), _gid )
    self:T( self.lid .. text )

    -- Reset current strafe status.
    self.strafeStatus[_uid] = nil

    if self.Coalition then
      if EventData.IniCoalition == self.Coalition then
        self:ScheduleOnce( 0.1, self._AddF10Commands, self, _unitName )
      end
    else
      -- Add Menu commands after a delay of 0.1 seconds.
      self:ScheduleOnce( 0.1, self._AddF10Commands, self, _unitName )
    end

    -- By default, some bomb impact points and do not flare each hit on target.
    self.PlayerSettings[_playername] = {} -- #RANGE.PlayerData
    self.PlayerSettings[_playername].smokebombimpact = self.defaultsmokebomb
    self.PlayerSettings[_playername].flaredirecthits = false
    self.PlayerSettings[_playername].smokecolor = SMOKECOLOR.Blue
    self.PlayerSettings[_playername].flarecolor = FLARECOLOR.Red
    self.PlayerSettings[_playername].delaysmoke = true
    self.PlayerSettings[_playername].messages = true
    self.PlayerSettings[_playername].client = CLIENT:FindByName( _unitName, nil, true )
    self.PlayerSettings[_playername].unitname = _unitName
    self.PlayerSettings[_playername].unit = _unit
    self.PlayerSettings[_playername].playername = _playername
    self.PlayerSettings[_playername].airframe = EventData.IniUnit:GetTypeName()
    self.PlayerSettings[_playername].inzone = false

    -- Start check in zone timer.
    if self.planes[_uid] ~= true then
      self.timerCheckZone = TIMER:New( self._CheckInZone, self, EventData.IniUnitName ):Start( 1, 1 )
      self.planes[_uid] = true
    end

  end
end

--- Range event handler for event hit.
-- @param #RANGE self
-- @param Core.Event#EVENTDATA EventData
function RANGE:OnEventHit( EventData )
  self:F( { eventhit = EventData } )

  -- Debug info.
  self:T3( self.lid .. "HIT: Ini unit   = " .. tostring( EventData.IniUnitName ) )
  self:T3( self.lid .. "HIT: Ini group  = " .. tostring( EventData.IniGroupName ) )
  self:T3( self.lid .. "HIT: Tgt target = " .. tostring( EventData.TgtUnitName ) )

  -- Player info
  local _unitName = EventData.IniUnitName
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
  if _unit == nil or _playername == nil then
    return
  end

  -- Unit ID
  local _unitID = _unit:GetID()

  -- Target
  local target = EventData.TgtUnit
  local targetname = EventData.TgtUnitName

  -- Current strafe target of player.
  local _currentTarget = self.strafeStatus[_unitID] --#RANGE.StrafeStatus

  -- Player has rolled in on a strafing target.
  if _currentTarget and target and target:IsAlive() then

    local playerPos = _unit:GetCoordinate()
    local targetPos = target:GetCoordinate()

    -- Loop over valid targets for this run.
    for _, _target in pairs( _currentTarget.zone.targets ) do

      -- Check the the target is the same that was actually hit.
      if _target and _target:IsAlive() and _target:GetName() == targetname then

        -- Get distance between player and target.
        local dist = playerPos:Get2DDistance( targetPos )

        if dist > _currentTarget.zone.foulline then
          -- Increase hit counter of this run.
          _currentTarget.hits = _currentTarget.hits + 1

          -- Flare target.
          if _unit and _playername and self.PlayerSettings[_playername].flaredirecthits then
            targetPos:Flare( self.PlayerSettings[_playername].flarecolor )
          end
        else
          -- Too close to the target.
          if _currentTarget.pastfoulline == false and _unit and _playername then
            local _d = _currentTarget.zone.foulline
            -- DONE - SRS output
            local text = string.format( "%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname( _unitName ), _d, targetname )
            if self.useSRS then
              local ttstext = string.format( "%s, Invalid hit! You already passed foul line distance of %d meters for target %s.", self:_myname( _unitName ), _d, targetname )
              self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,2)
            end
            self:_DisplayMessageToGroup( _unit, text )
            self:T2( self.lid .. text )
            _currentTarget.pastfoulline = true
          end
        end

      end
    end
  end

  -- Bombing Targets
  for _, _bombtarget in pairs( self.bombingTargets ) do

    local _target = _bombtarget.target -- Wrapper.Positionable#POSITIONABLE

    -- Check if one of the bomb targets was hit.
    if _target and _target:IsAlive() and _bombtarget.name == targetname then

      if _unit and _playername then

        -- Flare target.
        if self.PlayerSettings[_playername].flaredirecthits then

          -- Position of target.
          local targetPos = _target:GetCoordinate()

          targetPos:Flare( self.PlayerSettings[_playername].flarecolor )
        end

      end
    end
  end
end

--- Function called on impact of a tracked weapon.
-- @param Wrapper.Weapon#WEAPON weapon The weapon object.
-- @param #RANGE self RANGE object.
-- @param #RANGE.PlayerData playerData Player data table.
-- @param #number attackHdg Attack heading.
-- @param #number attackAlt Attack altitude.
-- @param #number attackVel Attack velocity.
function RANGE._OnImpact(weapon, self, playerData, attackHdg, attackAlt, attackVel)
  
  if not playerData then return end
  
  -- Get closet target to last position.
  local _closetTarget = nil -- #RANGE.BombTarget
  local _distance = nil
  local _closeCoord = nil   --Core.Point#COORDINATE
  local _hitquality = "POOR"

  -- Get callsign.
  local _callsign = self:_myname( playerData.unitname )

  local _playername=playerData.playername

  local _unit=playerData.unit

  -- Coordinate of impact point.
  local impactcoord = weapon:GetImpactCoordinate()

  -- Check if impact happened in range zone.+
  local insidezone = self.rangezone:IsCoordinateInZone( impactcoord )


  -- Smoke impact point of bomb.
  if playerData and playerData.smokebombimpact and insidezone then
    if playerData.delaysmoke then
      impactcoord:Smoke(playerData.smokecolor, 30, self.TdelaySmoke)
    else
      impactcoord:Smoke(playerData.smokecolor, 30)
    end
  end

  -- Loop over defined bombing targets.
  for _, _bombtarget in pairs( self.bombingTargets ) do
    local bombtarget=_bombtarget  --#RANGE.BombTarget

    -- Get target coordinate.
    local targetcoord = self:_GetBombTargetCoordinate( _bombtarget )

    if targetcoord then

      -- Distance between bomb and target.
      local _temp = impactcoord:Get2DDistance( targetcoord )

      -- Find closest target to last known position of the bomb.
      if _distance == nil or _temp < _distance then
        _distance = _temp
        _closetTarget = bombtarget
        _closeCoord   = targetcoord
        if _distance <= 1.53 then -- Rangeboss Edit
          _hitquality = "SHACK" -- Rangeboss Edit
        elseif _distance <= 0.5 * bombtarget.goodhitrange then -- Rangeboss Edit
          _hitquality = "EXCELLENT"
        elseif _distance <= bombtarget.goodhitrange then
          _hitquality = "GOOD"
        elseif _distance <= 2 * bombtarget.goodhitrange then
          _hitquality = "INEFFECTIVE"
        else
          _hitquality = "POOR"
        end

      end
    end
  end

  -- Count if bomb fell less than ~1 km away from the target.
  if _distance and _distance <= self.scorebombdistance then
    -- Init bomb player results.
    if not self.bombPlayerResults[_playername] then
      self.bombPlayerResults[_playername] = {}
    end

    -- Local results.
    local _results = self.bombPlayerResults[_playername]

    local result = {} -- #RANGE.BombResult
    result.command=SOCKET.DataType.BOMBRESULT
    result.name = _closetTarget.name or "unknown"
    result.distance = _distance
    result.radial = _closeCoord:HeadingTo( impactcoord )
    result.weapon = weapon:GetTypeName() or "unknown"
    result.quality = _hitquality
    result.player = playerData.playername
    result.time = timer.getAbsTime()
    result.clock = UTILS.SecondsToClock(result.time, true)
    result.midate = UTILS.GetDCSMissionDate()
    result.theatre = env.mission.theatre
    result.airframe = playerData.airframe
    result.roundsFired = 0 -- Rangeboss Edit
    result.roundsHit = 0 -- Rangeboss Edit
    result.roundsQuality = "N/A" -- Rangeboss Edit
    result.rangename = self.rangename
    result.attackHdg = attackHdg
    result.attackVel = attackVel
    result.attackAlt = attackAlt
    if os and os.date then
        result.date=os.date()
    else
        self:E(self.lid.."os or os.date() not available")
        result.date = "n/a"
    end

    -- Add to table.
    table.insert( _results, result )

    -- Call impact.
    self:Impact( result, playerData )

  elseif insidezone then

    -- Send message.
    -- DONE SRS message
    local _message = string.format( "%s, weapon impacted too far from nearest range target (>%.1f km). No score!", _callsign, self.scorebombdistance / 1000 )
    if self.useSRS then
      local ttstext = string.format( "%s, weapon impacted too far from nearest range target, mor than %.1f kilometer. No score!", _callsign, self.scorebombdistance / 1000 )
      self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,2)
    end
    self:_DisplayMessageToGroup( _unit, _message, nil, false )

    if self.rangecontrol then
      -- weapon impacted too far from the nearest target! No Score!
      if self.useSRS then
        self.controlsrsQ:NewTransmission(_message,nil,self.controlmsrs,nil,1)
      else
        self.rangecontrol:NewTransmission( self.Sound.RCWeaponImpactedTooFar.filename, self.Sound.RCWeaponImpactedTooFar.duration, self.soundpath, nil, nil, _message, self.subduration )
      end
    end

  else
    self:T( self.lid .. "Weapon impacted outside range zone." )
  end

end

--- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun).
-- @param #RANGE self
-- @param Core.Event#EVENTDATA EventData
function RANGE:OnEventShot( EventData )
  self:F( { eventshot = EventData } )

  -- Nil checks.
  if EventData.Weapon == nil or EventData.IniDCSUnit == nil or EventData.IniPlayerName == nil then
    return
  end

  -- Create weapon object.
  local weapon=WEAPON:New(EventData.weapon)

  -- Check if any condition applies here.
  local _track = (weapon:IsBomb() and self.trackbombs) or (weapon:IsRocket() and self.trackrockets) or (weapon:IsMissile() and self.trackmissiles)

  -- Get unit name.
  local _unitName = EventData.IniUnitName

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName, EventData.IniPlayerName )

  -- Distance Player-to-Range. Set this to larger value than the threshold.
  local dPR = self.BombtrackThreshold * 2

  -- Distance player to range.
  if _unit and _playername then
    dPR = _unit:GetCoordinate():Get2DDistance( self.location )
    self:T( self.lid .. string.format( "Range %s, player %s, player-range distance = %d km.", self.rangename, _playername, dPR / 1000 ) )
  end

  -- Only track if distance player to range is < 25 km. Also check that a player shot. No need to track AI weapons.
  if _track and dPR <= self.BombtrackThreshold and _unit and _playername and self.PlayerSettings[_playername] then

    -- Player data.
    local playerData = self.PlayerSettings[_playername] -- #RANGE.PlayerData
    
    if not playerData then return end
    
    -- Attack parameters.
    local attackHdg=_unit:GetHeading()
    local attackAlt=_unit:GetHeight()
    attackAlt = UTILS.MetersToFeet(attackAlt)
    local attackVel=_unit:GetVelocityKNOTS()

    -- Tracking info and init of last bomb position.
    self:T( self.lid .. string.format( "RANGE %s: Tracking %s - %s.", self.rangename, weapon:GetTypeName(), weapon:GetName()))

    -- Set callback function on impact.
    weapon:SetFuncImpact(RANGE._OnImpact, self, playerData, attackHdg, attackAlt, attackVel)

    -- Weapon is not yet "alife" just yet. Start timer in 0.1 seconds.
    self:T( self.lid .. string.format( "Range %s, player %s: Tracking of weapon starts in 0.1 seconds.", self.rangename, _playername ) )
    weapon:StartTrack(0.1)

  end

end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Functions
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check spawn queue and spawn aircraft if necessary.
-- @param #RANGE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RANGE:onafterStatus( From, Event, To )

  if self.verbose > 0 then

    local fsmstate = self:GetState()

    local text = string.format( "Range status: %s", fsmstate )

    if self.instructor then
      local alive = "N/A"
      if self.instructorrelayname then
        local relay = UNIT:FindByName( self.instructorrelayname )
        if relay then
          alive = tostring( relay:IsAlive() )
        end
      end
      text = text .. string.format( ", Instructor %.3f MHz (Relay=%s alive=%s)", self.instructorfreq, tostring( self.instructorrelayname ), alive )
    end

    if self.rangecontrol then
      local alive = "N/A"
      if self.rangecontrolrelayname then
        local relay = UNIT:FindByName( self.rangecontrolrelayname )
        if relay then
          alive = tostring( relay:IsAlive() )
        end
      end
      text = text .. string.format( ", Control %.3f MHz (Relay=%s alive=%s)", self.rangecontrolfreq, tostring( self.rangecontrolrelayname ), alive )
    end

    -- Check range status.
    self:T( self.lid .. text )

  end

  -- Check player status.
  self:_CheckPlayers()

  -- Check back in ~10 seconds.
  self:__Status( -10 )
end

--- Function called after player enters the range zone.
-- @param #RANGE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #RANGE.PlayerData player Player data.
function RANGE:onafterEnterRange( From, Event, To, player )

  if self.instructor and self.rangecontrol then

    if self.useSRS then


      local text = string.format("You entered the bombing range. For hit assessment, contact the range controller at %.3f MHz", self.rangecontrolfreq)
      local ttstext = string.format("You entered the bombing range. For hit assessment, contact the range controller at %.3f mega hertz.", self.rangecontrolfreq)

      local group = player.client:GetGroup()

      self.instructsrsQ:NewTransmission(ttstext, nil, self.instructmsrs, nil, 1, {group}, text, 10)

    else

      -- Range control radio frequency split.
      local RF = UTILS.Split( string.format( "%.3f", self.rangecontrolfreq ), "." )

      -- Radio message that player entered the range

      -- You entered the bombing range. For hit assessment, contact the range controller at xy MHz
      self.instructor:NewTransmission( self.Sound.IREnterRange.filename, self.Sound.IREnterRange.duration, self.soundpath )
      self.instructor:Number2Transmission( RF[1] )

      if tonumber( RF[2] ) > 0 then
        self.instructor:NewTransmission( self.Sound.IRDecimal.filename, self.Sound.IRDecimal.duration, self.soundpath )
        self.instructor:Number2Transmission( RF[2] )
      end

      self.instructor:NewTransmission( self.Sound.IRMegaHertz.filename, self.Sound.IRMegaHertz.duration, self.soundpath )
    end
  end

end

--- Function called after player leaves the range zone.
-- @param #RANGE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #RANGE.PlayerData player Player data.
function RANGE:onafterExitRange( From, Event, To, player )

  if self.instructor then
    -- You left the bombing range zone. Have a nice day!
    if self.useSRS then

      local text = "You left the bombing range zone. "

      local r=math.random(5)

      if r==1 then
        text=text.."Have a nice day!"
      elseif r==2 then
        text=text.."Take care and bye bye!"
      elseif r==3 then
        text=text.."Talk to you soon!"
      elseif r==4 then
        text=text.."See you in two weeks!"
      elseif r==5 then
        text=text.."!"
      end

      self.instructsrsQ:NewTransmission(text, nil, self.instructmsrs, nil, 1, {player.client:GetGroup()}, text, 10)
    else
      self.instructor:NewTransmission( self.Sound.IRExitRange.filename, self.Sound.IRExitRange.duration, self.soundpath )
    end
  end

end

--- Function called after bomb impact on range.
-- @param #RANGE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #RANGE.BombResult result Result of bomb impact.
-- @param #RANGE.PlayerData player Player data table.
function RANGE:onafterImpact( From, Event, To, result, player )

  -- Only display target name if there is more than one bomb target.
  local targetname = nil
  if #self.bombingTargets > 1 then
    targetname = result.name
  end

  -- Send message to player.
  local text = string.format( "%s, impact %03d° for %d ft (%d m)", player.playername, result.radial, UTILS.MetersToFeet( result.distance ), result.distance )
  if targetname then
    text = text .. string.format( " from bulls of target %s.", targetname )
  else
    text = text .. "."
  end
  text = text .. string.format( " %s hit.", result.quality )

  if self.rangecontrol then

    if self.useSRS then
      local group = player.client:GetGroup()
      self.controlsrsQ:NewTransmission(text,nil,self.controlmsrs,nil,1,{group},text,10)
    else
      self.rangecontrol:NewTransmission( self.Sound.RCImpact.filename, self.Sound.RCImpact.duration, self.soundpath, nil, nil, text, self.subduration )
      self.rangecontrol:Number2Transmission( string.format( "%03d", result.radial ), nil, 0.1 )
      self.rangecontrol:NewTransmission( self.Sound.RCDegrees.filename, self.Sound.RCDegrees.duration, self.soundpath )
      self.rangecontrol:NewTransmission( self.Sound.RCFor.filename, self.Sound.RCFor.duration, self.soundpath )
      self.rangecontrol:Number2Transmission( string.format( "%d", UTILS.MetersToFeet( result.distance ) ) )
      self.rangecontrol:NewTransmission( self.Sound.RCFeet.filename, self.Sound.RCFeet.duration, self.soundpath )
      if result.quality == "POOR" then
        self.rangecontrol:NewTransmission( self.Sound.RCPoorHit.filename, self.Sound.RCPoorHit.duration, self.soundpath, nil, 0.5 )
      elseif result.quality == "INEFFECTIVE" then
        self.rangecontrol:NewTransmission( self.Sound.RCIneffectiveHit.filename, self.Sound.RCIneffectiveHit.duration, self.soundpath, nil, 0.5 )
      elseif result.quality == "GOOD" then
        self.rangecontrol:NewTransmission( self.Sound.RCGoodHit.filename, self.Sound.RCGoodHit.duration, self.soundpath, nil, 0.5 )
      elseif result.quality == "EXCELLENT" then
        self.rangecontrol:NewTransmission( self.Sound.RCExcellentHit.filename, self.Sound.RCExcellentHit.duration, self.soundpath, nil, 0.5 )
      end
    end
  end

  -- Unit.
  if player.unitname and not self.useSRS then

    -- Get unit.
    local unit = UNIT:FindByName( player.unitname )

    -- Send message.
      self:_DisplayMessageToGroup( unit, text, nil, true )
    self:T( self.lid .. text )
  end

  -- Save results.
  if self.autosave then
    self:Save()
  end

  -- Send result to FunkMan, which creates fancy MatLab figures and sends them to Discord via a bot.
  if self.funkmanSocket then
    self.funkmanSocket:SendTable(result)
  end

end

--- Function called after strafing run.
-- @param #RANGE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #RANGE.PlayerData player Player data table.
-- @param #RANGE.StrafeResult result Result of run.
function RANGE:onafterStrafeResult( From, Event, To, player, result)

  if self.funkmanSocket then
    self.funkmanSocket:SendTable(result)
  end

end

--- Function called before save event. Checks that io and lfs are desanitized.
-- @param #RANGE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RANGE:onbeforeSave( From, Event, To )
  if io and lfs then
    return true
  else
    self:E( self.lid .. string.format( "WARNING: io and/or lfs not desanitized. Cannot save player results." ) )
    return false
  end
end

--- Function called after save.
-- @param #RANGE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RANGE:onafterSave( From, Event, To )

  local function _savefile( filename, data )
    local f = io.open( filename, "wb" )
    if f then
      f:write( data )
      f:close()
      self:T( self.lid .. string.format( "Saving player results to file %s", tostring( filename ) ) )
    else
      self:E( self.lid .. string.format( "ERROR: Could not save results to file %s", tostring( filename ) ) )
    end
  end

  -- Path.
  local path = self.targetpath or lfs.writedir() .. [[Logs\]]

  -- Set file name.
  local filename = path .. string.format( "RANGE-%s_BombingResults.csv", self.rangename )

  -- Header line.
  local scores = "Name,Pass,Target,Distance,Radial,Quality,Weapon,Airframe,Mission Time"

  -- Loop over all players.
  for playername, results in pairs( self.bombPlayerResults ) do

    -- Loop over player grades table.
    for i, _result in pairs( results ) do
      local result = _result -- #RANGE.BombResult
      local distance = result.distance
      local weapon = result.weapon
      local target = result.name
      local radial = result.radial
      local quality = result.quality
      local time = UTILS.SecondsToClock(result.time, true)
      local airframe = result.airframe
      local date = result.date or "n/a"
      scores = scores .. string.format( "\n%s,%d,%s,%.2f,%03d,%s,%s,%s,%s,%s", playername, i, target, distance, radial, quality, weapon, airframe, time, date )
    end
  end

  _savefile( filename, scores )
end

--- Function called before save event. Checks that io and lfs are desanitized.
-- @param #RANGE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RANGE:onbeforeLoad( From, Event, To )
  if io and lfs then
    return true
  else
    self:E( self.lid .. string.format( "WARNING: io and/or lfs not desanitized. Cannot load player results." ) )
    return false
  end
end

--- On after "Load" event. Loads results of all players from file.
-- @param #RANGE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RANGE:onafterLoad( From, Event, To )

  --- Function that load data from a file.
  local function _loadfile( filename )
    local f = io.open( filename, "rb" )
    if f then
      -- self:I(self.lid..string.format("Loading player results from file %s", tostring(filename)))
      local data = f:read( "*all" )
      f:close()
      return data
    else
      self:E( self.lid .. string.format( "WARNING: Could not load player results from file %s. File might not exist just yet.", tostring( filename ) ) )
      return nil
    end
  end

  -- Path in DCS log file.
  local path = self.targetpath or lfs.writedir() .. [[Logs\]]

  -- Set file name.
  local filename = path .. string.format( "RANGE-%s_BombingResults.csv", self.rangename )

  -- Info message.
  local text = string.format( "Loading player bomb results from file %s", filename )
  self:T( self.lid .. text )

  -- Load asset data from file.
  local data = _loadfile( filename )

  if data then

    -- Split by line break.
    local results = UTILS.Split( data, "\n" )

    -- Remove first header line.
    table.remove( results, 1 )

    -- Init player scores table.
    self.bombPlayerResults = {}

    -- Loop over all lines.
    for _, _result in pairs( results ) do

      -- Parameters are separated by commata.
      local resultdata = UTILS.Split( _result, "," )

      -- Grade table
      local result = {} -- #RANGE.BombResult

      -- Player name.
      local playername = resultdata[1]
      result.player = playername

      -- Results data.
      result.name = tostring( resultdata[3] )
      result.distance = tonumber( resultdata[4] )
      result.radial = tonumber( resultdata[5] )
      result.quality = tostring( resultdata[6] )
      result.weapon = tostring( resultdata[7] )
      result.airframe = tostring( resultdata[8] )
      result.time = UTILS.ClockToSeconds( resultdata[9] or "00:00:00" )
      result.date = resultdata[10] or "n/a"

      -- Create player array if necessary.
      self.bombPlayerResults[playername] = self.bombPlayerResults[playername] or {}

      -- Add result to table.
      table.insert( self.bombPlayerResults[playername], result )
    end
  end
end

--- Save target sheet.
-- @param #RANGE self
-- @param #string _playername Player name.
-- @param #RANGE.StrafeResult result Results table.
function RANGE:_SaveTargetSheet( _playername, result ) -- RangeBoss Specific Function

  --- Function that saves data to file
  local function _savefile( filename, data )
    local f = io.open( filename, "wb" )
    if f then
      f:write( data )
      f:close()
    else
      env.info( "RANGEBOSS EDIT - could not save target sheet to file" )
      -- self:E(self.lid..string.format("ERROR: could not save target sheet to file %s.\nFile may contain invalid characters.", tostring(filename)))
    end
  end

  -- Set path or default.
  local path = self.targetpath
  if lfs then
    path = path or lfs.writedir() .. [[Logs\]]
  end

  -- Create unused file name.
  local filename = nil
  for i = 1, 9999 do

    -- Create file name
    if self.targetprefix then
      filename = string.format( "%s_%s-%04d.csv", self.targetprefix, result.airframe, i )
    else
      local name = UTILS.ReplaceIllegalCharacters( _playername, "_" )
      filename = string.format( "RANGERESULTS-%s_Targetsheet-%s-%04d.csv", self.rangename, name, i )
    end

    -- Set path.
    if path ~= nil then
      filename = path .. "\\" .. filename
    end

    -- Check if file exists.
    local _exists = UTILS.FileExists( filename )
    if not _exists then
      break
    end
  end

  -- Header line
  local data = "Name,Target,Rounds Fired,Rounds Hit,Rounds Quality,Airframe,Mission Time,OS Time\n"

  local target = result.name
  local airframe = result.airframe
  local roundsFired = result.roundsFired
  local roundsHit = result.roundsHit
  local strafeResult = result.roundsQuality
  local time = UTILS.SecondsToClock( result.time )
  local date = "n/a"
  if os then
    date = os.date()
  end
  data = data .. string.format( "%s,%s,%d,%d,%s,%s,%s,%s", _playername, target, roundsFired, roundsHit, strafeResult, airframe, time, date )

  -- Save file.
  _savefile( filename, data )
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Display Messages
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Display top 10 stafing results of a specific player.
-- @param #RANGE self
-- @param #string _unitName Name of the player unit.
function RANGE:_DisplayMyStrafePitResults( _unitName )
  self:F( _unitName )

  -- Get player unit and name
  local _unit, _playername, _multiplayer = self:_GetPlayerUnitAndName( _unitName )

  if _unit and _playername then

    -- Message header.
    local _message = string.format( "My Top %d Strafe Pit Results:\n", self.ndisplayresult )

    -- Get player results.
    local _results = self.strafePlayerResults[_playername]

    -- Create message.
    if _results == nil then
      -- No score yet.
      _message = string.format( "%s: No Score yet.", _playername )
    else

      -- Sort results table wrt number of hits.
      local _sort = function( a, b )
        return a.roundsHit > b.roundsHit
      end
      table.sort( _results, _sort )

      -- Prepare message of best results.
      local _bestMsg = ""
      local _count = 1

      -- Loop over results
      for _, _result in pairs( _results ) do
        local result=_result --#RANGE.StrafeResult

        -- Message text.
        _message = _message .. string.format( "\n[%d] Hits %d - %s - %s", _count, result.roundsHit, result.name, result.roundsQuality )

        -- Best result.
        if _bestMsg == "" then
          _bestMsg = string.format( "Hits %d - %s - %s", result.roundsHit, result.name, result.roundsQuality)
        end

        -- 10 runs
        if _count == self.ndisplayresult then
          break
        end

        -- Increase counter
        _count = _count + 1
      end

      -- Message text.
      _message = _message .. "\n\nBEST: " .. _bestMsg
    end

    -- Send message to group.
    self:_DisplayMessageToGroup( _unit, _message, nil, true, true, _multiplayer )
  end
end

--- Display top 10 strafing results of all players.
-- @param #RANGE self
-- @param #string _unitName Name fo the player unit.
function RANGE:_DisplayStrafePitResults( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, _playername, _multiplayer = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then

    -- Results table.
    local _playerResults = {}

    -- Message text.
    local _message = string.format( "Strafe Pit Results - Top %d Players:\n", self.ndisplayresult )

    -- Loop over player results.
    for _playerName, _results in pairs( self.strafePlayerResults ) do

      -- Get the best result of the player.
      local _best = nil
      for _, _result in pairs( _results ) do
        if _best == nil or _result.roundsHit > _best.roundsHit then
          _best = _result
        end
      end

      -- Add best result to table.
      if _best ~= nil then
        local text = string.format( "%s: Hits %i - %s - %s", _playerName, _best.roundsHit, _best.name, _best.roundsQuality )
        table.insert( _playerResults, { msg = text, hits = _best.roundsHit } )
      end

    end

    -- Sort list!
    local _sort = function( a, b )
      return a.hits > b.hits
    end
    table.sort( _playerResults, _sort )

    -- Add top 10 results.
    for _i = 1, math.min( #_playerResults, self.ndisplayresult ) do
      _message = _message .. string.format( "\n[%d] %s", _i, _playerResults[_i].msg )
    end

    -- In case there are no scores yet.
    if #_playerResults < 1 then
      _message = _message .. "No player scored yet."
    end

    -- Send message.
    self:_DisplayMessageToGroup( _unit, _message, nil, true, true, _multiplayer )
  end
end

--- Display top 10 bombing run results of specific player.
-- @param #RANGE self
-- @param #string _unitName Name of the player unit.
function RANGE:_DisplayMyBombingResults( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, _playername, _multiplayer = self:_GetPlayerUnitAndName( _unitName )

  if _unit and _playername then

    -- Init message.
    local _message = string.format( "My Top %d Bombing Results:\n", self.ndisplayresult )

    -- Results from player.
    local _results = self.bombPlayerResults[_playername]

    -- No score so far.
    if _results == nil then
      _message = _playername .. ": No Score yet."
    else

      -- Sort results wrt to distance.
      local _sort = function( a, b )
        return a.distance < b.distance
      end
      table.sort( _results, _sort )

      -- Loop over results.
      local _bestMsg = ""
      for i, _result in pairs( _results ) do
        local result = _result -- #RANGE.BombResult

        -- Message with name, weapon and distance.
        _message = _message .. "\n" .. string.format( "[%d] %d m %03d° - %s - %s - %s hit", i, result.distance, result.radial, result.name, result.weapon, result.quality )

        -- Store best/first result.
        if _bestMsg == "" then
          _bestMsg = string.format( "%d m %03d° - %s - %s - %s hit", result.distance, result.radial, result.name, result.weapon, result.quality )
        end

        -- Best 10 runs only.
        if i == self.ndisplayresult then
          break
        end

      end

      -- Message.
      _message = _message .. "\n\nBEST: " .. _bestMsg
    end

    -- Send message.
    self:_DisplayMessageToGroup( _unit, _message, nil, true, true, _multiplayer )
  end
end

--- Display best bombing results of top 10 players.
-- @param #RANGE self
-- @param #string _unitName Name of player unit.
function RANGE:_DisplayBombingResults( _unitName )
  self:F( _unitName )

  -- Results table.
  local _playerResults = {}

  -- Get player unit and name.
  local _unit, _player, _multiplayer = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit with a player.
  if _unit and _player then

    -- Message header.
    local _message = string.format( "Bombing Results - Top %d Players:\n", self.ndisplayresult )

    -- Loop over players.
    for _playerName, _results in pairs( self.bombPlayerResults ) do

      -- Find best result of player.
      local _best = nil
      for _, _result in pairs( _results ) do
        if _best == nil or _result.distance < _best.distance then
          _best = _result
        end
      end

      -- Put best result of player into table.
      if _best ~= nil then
        local bestres = string.format( "%s: %d m - %s - %s - %s hit", _playerName, _best.distance, _best.name, _best.weapon, _best.quality )
        table.insert( _playerResults, { msg = bestres, distance = _best.distance } )
      end

    end

    -- Sort list of player results.
    local _sort = function( a, b )
      return a.distance < b.distance
    end
    table.sort( _playerResults, _sort )

    -- Loop over player results.
    for _i = 1, math.min( #_playerResults, self.ndisplayresult ) do
      _message = _message .. string.format( "\n[%d] %s", _i, _playerResults[_i].msg )
    end

    -- In case there are no scores yet.
    if #_playerResults < 1 then
      _message = _message .. "No player scored yet."
    end

    -- Send message.
    self:_DisplayMessageToGroup( _unit, _message, nil, true, true, _multiplayer )
  end
end

--- Report information like bearing and range from player unit to range.
-- @param #RANGE self
-- @param #string _unitname Name of the player unit.
function RANGE:_DisplayRangeInfo( _unitname )
  self:F( _unitname )

  -- Get player unit and player name.
  local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( _unitname )

  -- Check if we have a player.
  if unit and playername then
    --self:I(playername)
    -- Message text.
    local text = ""

    -- Current coordinates.
    local coord = unit:GetCoordinate()

    if self.location then

      local settings = _DATABASE:GetPlayerSettings( playername ) or _SETTINGS -- Core.Settings#SETTINGS

      -- Direction vector from current position (coord) to target (position).
      local position = self.location -- Core.Point#COORDINATE
      local bulls = position:ToStringBULLS( unit:GetCoalition(), settings )
      local lldms = position:ToStringLLDMS( settings )
      local llddm = position:ToStringLLDDM( settings )
      local rangealt = position:GetLandHeight()
      local vec3 = coord:GetDirectionVec3( position )
      local angle = coord:GetAngleDegrees( vec3 )
      local range = coord:Get2DDistance( position )

      -- Bearing string.
      local Bs = string.format( '%03d°', angle )

      local texthit
      if self.PlayerSettings[playername].flaredirecthits then
        texthit = string.format( "Flare direct hits: ON (flare color %s)\n", self:_flarecolor2text( self.PlayerSettings[playername].flarecolor ) )
      else
        texthit = string.format( "Flare direct hits: OFF\n" )
      end
      local textbomb
      if self.PlayerSettings[playername].smokebombimpact then
        textbomb = string.format( "Smoke bomb impact points: ON (smoke color %s)\n", self:_smokecolor2text( self.PlayerSettings[playername].smokecolor ) )
      else
        textbomb = string.format( "Smoke bomb impact points: OFF\n" )
      end
      local textdelay
      if self.PlayerSettings[playername].delaysmoke then
        textdelay = string.format( "Smoke bomb delay: ON (delay %.1f seconds)", self.TdelaySmoke )
      else
        textdelay = string.format( "Smoke bomb delay: OFF" )
      end

      -- Player unit settings.
      local trange = string.format( "%.1f km", range / 1000 )
      local trangealt = string.format( "%d m", rangealt )
      local tstrafemaxalt = string.format( "%d m", self.strafemaxalt )
      if settings:IsImperial() then
        trange = string.format( "%.1f NM", UTILS.MetersToNM( range ) )
        trangealt = string.format( "%d feet", UTILS.MetersToFeet( rangealt ) )
        tstrafemaxalt = string.format( "%d feet", UTILS.MetersToFeet( self.strafemaxalt ) )
      end

      -- Message.
      text = text .. string.format( "Information on %s:\n", self.rangename )
      text = text .. string.format( "-------------------------------------------------------\n" )
      text = text .. string.format( "Bearing %s, Range %s\n", Bs, trange )
      text = text .. string.format( "%s\n", bulls )
      text = text .. string.format( "%s\n", lldms )
      text = text .. string.format( "%s\n", llddm )
      text = text .. string.format( "Altitude ASL: %s\n", trangealt )
      text = text .. string.format( "Max strafing alt AGL: %s\n", tstrafemaxalt )
      text = text .. string.format( "# of strafe targets: %d\n", self.nstrafetargets )
      text = text .. string.format( "# of bomb targets: %d\n", self.nbombtargets )
      if self.instructor then
        local alive = "N/A"
        if self.instructorrelayname then
          local relay = UNIT:FindByName( self.instructorrelayname )
          if relay then
            --alive = tostring( relay:IsAlive() )
            alive = relay:IsAlive() and "ok" or "N/A"
          end
        end
        text = text .. string.format( "Instructor %.3f MHz (Relay=%s)\n", self.instructorfreq, alive )
      end
      if self.rangecontrol then
        local alive = "N/A"
        if self.rangecontrolrelayname then
          local relay = UNIT:FindByName( self.rangecontrolrelayname )
          if relay then
            alive = tostring( relay:IsAlive() )
            alive = relay:IsAlive() and "ok" or "N/A"
          end
        end
        text = text .. string.format( "Control %.3f MHz (Relay=%s)\n", self.rangecontrolfreq, alive )
      end
      text = text .. texthit
      text = text .. textbomb
      text = text .. textdelay

      -- Send message to player group.
      self:_DisplayMessageToGroup( unit, text, nil, true, true, _multiplayer )

      -- Debug output.
      self:T2( self.lid .. text )
    end
  end
end

--- Display bombing target locations to player.
-- @param #RANGE self
-- @param #string _unitname Name of the player unit.
function RANGE:_DisplayBombTargets( _unitname )
  self:F( _unitname )

  -- Get player unit and player name.
  local _unit, _playername, _multiplayer = self:_GetPlayerUnitAndName( _unitname )

  -- Check if we have a player.
  if _unit and _playername then

    -- Player settings.
    local _settings = _DATABASE:GetPlayerSettings( _playername ) or _SETTINGS -- Core.Settings#SETTINGS

    -- Message text.
    local _text = "Bomb Target Locations:"

    for _, _bombtarget in pairs( self.bombingTargets ) do
      local bombtarget = _bombtarget -- #RANGE.BombTarget

      -- Coordinate of bombtarget.
      local coord = self:_GetBombTargetCoordinate( bombtarget )

      if coord then

        -- Get elevation
        local elevation = coord:GetLandHeight()
        local eltxt = string.format( "%d m", elevation )
        if not _settings:IsMetric() then
          elevation = UTILS.MetersToFeet( elevation )
          eltxt = string.format( "%d ft", elevation )
        end

        local ca2g = coord:ToStringA2G( _unit, _settings )
        _text = _text .. string.format( "\n- %s:\n%s @ %s", bombtarget.name or "unknown", ca2g, eltxt )
      end
    end

    self:_DisplayMessageToGroup( _unit, _text, 150, true, true, _multiplayer )
  end
end

--- Display pit location and heading to player.
-- @param #RANGE self
-- @param #string _unitname Name of the player unit.
function RANGE:_DisplayStrafePits( _unitname )
  self:F( _unitname )

  -- Get player unit and player name.
  local _unit, _playername, _multiplayer = self:_GetPlayerUnitAndName( _unitname )

  -- Check if we have a player.
  if _unit and _playername then

    -- Player settings.
    local _settings = _DATABASE:GetPlayerSettings( _playername ) or _SETTINGS -- Core.Settings#SETTINGS

    -- Message text.
    local _text = "Strafe Target Locations:"

    for _, _strafepit in pairs( self.strafeTargets ) do
      local _target = _strafepit -- Wrapper.Positionable#POSITIONABLE

      -- Pit parameters.
      local coord = _strafepit.coordinate -- Core.Point#COORDINATE
      local heading = _strafepit.heading

      -- Turn heading around ==> approach heading.
      if heading > 180 then
        heading = heading - 180
      else
        heading = heading + 180
      end

      local mycoord = coord:ToStringA2G( _unit, _settings )
      _text = _text .. string.format( "\n- %s: heading %03d°\n%s", _strafepit.name, heading, mycoord )
    end

    self:_DisplayMessageToGroup( _unit, _text, nil, true, true, _multiplayer )
  end
end

--- Report weather conditions at range. Temperature, QFE pressure and wind data.
-- @param #RANGE self
-- @param #string _unitname Name of the player unit.
function RANGE:_DisplayRangeWeather( _unitname )
  self:F( _unitname )

  -- Get player unit and player name.
  local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( _unitname )

  -- Check if we have a player.
  if unit and playername then

    -- Message text.
    local text = ""

    -- Current coordinates.
    local coord = unit:GetCoordinate()

    if self.location then

      -- Get atmospheric data at range location.
      local position = self.location -- Core.Point#COORDINATE
      local T = position:GetTemperature()
      local P = position:GetPressure()
      local Wd, Ws = position:GetWind()

      -- Get Beaufort wind scale.
      local Bn, Bd = UTILS.BeaufortScale( Ws )

      local WD = string.format( '%03d°', Wd )
      local Ts = string.format( "%d°C", T )

      local hPa2inHg = 0.0295299830714
      local hPa2mmHg = 0.7500615613030

      local settings = _DATABASE:GetPlayerSettings( playername ) or _SETTINGS -- Core.Settings#SETTINGS
      local tT = string.format( "%d°C", T )
      local tW = string.format( "%.1f m/s", Ws )
      local tP = string.format( "%.1f mmHg", P * hPa2mmHg )
      if settings:IsImperial() then
        -- tT=string.format("%d°F", UTILS.CelsiusToFahrenheit(T))
        tW = string.format( "%.1f knots", UTILS.MpsToKnots( Ws ) )
        tP = string.format( "%.2f inHg", P * hPa2inHg )
      end

      -- Message text.
      text = text .. string.format( "Weather Report at %s:\n", self.rangename )
      text = text .. string.format( "--------------------------------------------------\n" )
      text = text .. string.format( "Temperature %s\n", tT )
      text = text .. string.format( "Wind from %s at %s (%s)\n", WD, tW, Bd )
      text = text .. string.format( "QFE %.1f hPa = %s", P, tP )
    else
      text = string.format( "No range location defined for range %s.", self.rangename )
    end

    -- Send message to player group.
    self:_DisplayMessageToGroup( unit, text, nil, true, true, _multiplayer )

    -- Debug output.
    self:T2( self.lid .. text )
  else
    self:T( self.lid .. string.format( "ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname ) )
  end
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Timer Functions

--- Check status of players.
-- @param #RANGE self
-- @param #string _unitName Name of player unit.
function RANGE:_CheckPlayers()

  for playername, _playersettings in pairs( self.PlayerSettings ) do
    local playersettings = _playersettings -- #RANGE.PlayerData

    local unitname = playersettings.unitname
    local unit = UNIT:FindByName( unitname )

    if unit and unit:IsAlive() then

      local unitalt = unit:GetAltitude(false)
      local unitaltinfeet = UTILS.MetersToFeet(unitalt)

      if unit:IsInZone(self.rangezone) and (not self.ceilingenabled or unitaltinfeet < self.ceilingaltitude) then

        ------------------------------
        -- Player INSIDE Range Zone --
        ------------------------------

        if not playersettings.inzone then
          playersettings.inzone = true
          self:EnterRange( playersettings )
        end

      else

        -------------------------------
        -- Player OUTSIDE Range Zone --
        -------------------------------

        if playersettings.inzone == true then
          playersettings.inzone = false
          self:ExitRange( playersettings )
        end

      end
    end
  end

end

--- Check if player is inside a strafing zone. If he is, we start looking for hits. If he was and left the zone again, the result is stored.
-- @param #RANGE self
-- @param #string _unitName Name of player unit.
function RANGE:_CheckInZone( _unitName )
  self:F2( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
  local unitheading = 0 -- RangeBoss

  if _unit and _playername then

    -- Player data.
    local playerData=self.PlayerSettings[_playername] -- #RANGE.PlayerData

    --- Function to check if unit is in zone and facing in the right direction and is below the max alt.
    local function checkme( targetheading, _zone )
      local zone = _zone -- Core.Zone#ZONE

      -- Heading check.
      local unitheading = _unit:GetHeading()
      local pitheading = targetheading - 180
      local deltaheading = unitheading - pitheading
      local towardspit = math.abs( deltaheading ) <= 90 or math.abs( deltaheading - 360 ) <= 90

      if towardspit then

        local vec3 = _unit:GetVec3()
        local vec2 = { x = vec3.x, y = vec3.z } -- DCS#Vec2
        local landheight = land.getHeight( vec2 )
        local unitalt = vec3.y - landheight

        if unitalt <= self.strafemaxalt then
          local unitinzone = zone:IsVec2InZone( vec2 )
          return unitinzone
        end
      end

      return false
    end

    -- Current position of player unit.
    local _unitID = _unit:GetID()

    -- Currently strafing? (strafeStatus is nil if not)
    local _currentStrafeRun = self.strafeStatus[_unitID] --#RANGE.StrafeStatus

    if _currentStrafeRun then -- player has already registered for a strafing run.

      -- Get the current approach zone and check if player is inside.
      local zone = _currentStrafeRun.zone.polygon -- Core.Zone#ZONE_POLYGON_BASE

      -- Check if unit in zone and facing the right direction.
      local unitinzone = checkme( _currentStrafeRun.zone.heading, zone )

      -- Check if player is in strafe zone and below max alt.
      if unitinzone then
        -- Still in zone, keep counting hits. Increase counter.
        _currentStrafeRun.time = _currentStrafeRun.time + 1

      else

        -- Increase counter
        _currentStrafeRun.time = _currentStrafeRun.time + 1

        if _currentStrafeRun.time <= 3 then

          -- Reset current run.
          self.strafeStatus[_unitID] = nil

          -- Message text.
          local _msg = string.format( "%s left strafing zone %s too quickly. No Score.", _playername, _currentStrafeRun.zone.name )

          -- Send message.
          self:_DisplayMessageToGroup( _unit, _msg, nil, true )

          if self.rangecontrol then
            if self.useSRS then
              local group = _unit:GetGroup()
              local text = "You left the strafing zone too quickly! No score!"
              --self.controlsrsQ:NewTransmission(text,nil,self.controlmsrs,nil,1,{group},text,10)
              self.controlsrsQ:NewTransmission(text,nil,self.controlmsrs,nil,1)
            else
              -- You left the strafing zone too quickly! No score!
              self.rangecontrol:NewTransmission( self.Sound.RCLeftStrafePitTooQuickly.filename, self.Sound.RCLeftStrafePitTooQuickly.duration, self.soundpath )
            end
          end
        else

          -- Get current ammo.
          local _ammo = self:_GetAmmo( _unitName )

          -- Result.
          local _result = self.strafeStatus[_unitID] --#RANGE.StrafeStatus

          local _sound = nil -- #RANGE.Soundfile

          -- Calculate accuracy of run. Number of hits wrt number of rounds fired.
          local shots = _result.ammo - _ammo
          local accur = 0
          if shots > 0 then
            accur = _result.hits / shots * 100
            if accur > 100 then
              accur = 100
            end
          end

          -- Results text and sound message.
          local resulttext=""
          if _result.pastfoulline == true then --
            resulttext = "* INVALID - PASSED FOUL LINE *"
            _sound = self.Sound.RCPoorPass --
          else
            if accur >= 90 then
              resulttext = "DEADEYE PASS"
              _sound = self.Sound.RCExcellentPass
            elseif accur >= 75 then
              resulttext = "EXCELLENT PASS"
              _sound = self.Sound.RCExcellentPass
            elseif accur >= 50 then
              resulttext = "GOOD PASS"
              _sound = self.Sound.RCGoodPass
            elseif accur >= 25 then
              resulttext = "INEFFECTIVE PASS"
              _sound = self.Sound.RCIneffectivePass
            else
              resulttext = "POOR PASS"
              _sound = self.Sound.RCPoorPass
            end
          end

          -- Message text.
          local _text = string.format( "%s, hits on target %s: %d", self:_myname( _unitName ), _result.zone.name, _result.hits )
          local ttstext = string.format( "%s, hits on target %s: %d.", self:_myname( _unitName ), _result.zone.name, _result.hits )
          if shots and accur then
            _text = _text .. string.format( "\nTotal rounds fired %d. Accuracy %.1f %%.", shots, accur )
            ttstext = ttstext .. string.format( ". Total rounds fired %d. Accuracy %.1f percent.", shots, accur )
          end
          _text = _text .. string.format( "\n%s", resulttext )
          ttstext = ttstext .. string.format( " %s", resulttext )

          -- Send message.
          self:_DisplayMessageToGroup( _unit, _text )

          -- Strafe result.
          local result = {} -- #RANGE.StrafeResult
          result.command=SOCKET.DataType.STRAFERESULT
          result.player=_playername
          result.name=_result.zone.name or "unknown"
          result.time = timer.getAbsTime()
          result.clock = UTILS.SecondsToClock(result.time)
          result.midate = UTILS.GetDCSMissionDate()
          result.theatre = env.mission.theatre
          result.roundsFired = shots
          result.roundsHit = _result.hits
          result.roundsQuality = resulttext
          result.strafeAccuracy = accur
          result.rangename = self.rangename
          result.airframe=playerData.airframe
          result.invalid = _result.pastfoulline

          -- Griger Results.
          self:StrafeResult(playerData, result)

          -- Save trap sheet.
          if playerData and playerData.targeton and self.targetsheet then
            self:_SaveTargetSheet( _playername, result )
          end

          -- Voice over.
          if self.rangecontrol then
            if self.useSRS then
              self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,1)
            else
              self.rangecontrol:NewTransmission( self.Sound.RCHitsOnTarget.filename, self.Sound.RCHitsOnTarget.duration, self.soundpath )
              self.rangecontrol:Number2Transmission( string.format( "%d", _result.hits ) )
              if shots and accur then
                self.rangecontrol:NewTransmission( self.Sound.RCTotalRoundsFired.filename, self.Sound.RCTotalRoundsFired.duration, self.soundpath, nil, 0.2 )
                self.rangecontrol:Number2Transmission( string.format( "%d", shots ), nil, 0.2 )
                self.rangecontrol:NewTransmission( self.Sound.RCAccuracy.filename, self.Sound.RCAccuracy.duration, self.soundpath, nil, 0.2 )
                self.rangecontrol:Number2Transmission( string.format( "%d", UTILS.Round( accur, 0 ) ) )
                self.rangecontrol:NewTransmission( self.Sound.RCPercent.filename, self.Sound.RCPercent.duration, self.soundpath )
              end
              self.rangecontrol:NewTransmission( _sound.filename, _sound.duration, self.soundpath, nil, 0.5 )
            end
          end

          -- Set strafe status to nil.
          self.strafeStatus[_unitID] = nil

          -- Save stats so the player can retrieve them.
          local _stats = self.strafePlayerResults[_playername] or {}
          table.insert( _stats, result )
          self.strafePlayerResults[_playername] = _stats
        end

      end

    else

      -- Check to see if we're in any of the strafing zones (first time).
      for _, _targetZone in pairs( self.strafeTargets ) do
        local target=_targetZone --#RANGE.StrafeTarget

        -- Get the current approach zone and check if player is inside.
        local zone = target.polygon -- Core.Zone#ZONE_POLYGON_BASE

        -- Check if unit in zone and facing the right direction.
        local unitinzone = checkme( target.heading, zone )

        -- Player is inside zone.
        if unitinzone then

          -- Get ammo at the beginning of the run.
          local _ammo = self:_GetAmmo( _unitName )

          -- Init strafe status for this player.
          self.strafeStatus[_unitID] = { hits = 0, zone = target, time = 1, ammo = _ammo, pastfoulline = false }

          -- Rolling in!
          local _msg = string.format( "%s, rolling in on strafe pit %s.", self:_myname( _unitName ), target.name )

          if self.rangecontrol then
            if self.useSRS then
              self.controlsrsQ:NewTransmission(_msg,nil,self.controlmsrs,nil,1)
            else
              self.rangecontrol:NewTransmission( self.Sound.RCRollingInOnStrafeTarget.filename, self.Sound.RCRollingInOnStrafeTarget.duration, self.soundpath )
            end
          end

          -- Send message.
          self:_DisplayMessageToGroup( _unit, _msg, 10, true )

          -- Trigger event that player is rolling in.
          self:RollingIn(playerData, target)

          -- We found our player. Skip remaining checks.
          break

        end -- unit in zone check

      end -- loop over zones
    end
  end

end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Menu Functions
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Add menu commands for player.
-- @param #RANGE self
-- @param #string _unitName Name of player unit.
function RANGE:_AddF10Commands( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check for player unit.
  if _unit and playername then

    -- Get group and ID.
    local group = _unit:GetGroup()
    local _gid = group:GetID()

    if group and _gid then

      if not self.MenuAddedTo[_gid] then

        -- Enable switch so we don't do this twice.
        self.MenuAddedTo[_gid] = true

        -- Range root menu path.
        local _rootMenu = nil

        if self.menuF10root then
        
          -------------------
          -- MISSION LEVEL --
          -------------------        
        
          --_rootMenu = MENU_GROUP:New( group, self.rangename, self.menuF10root )
          _rootMenu = self.menuF10root
          self:T2(self.lid..string.format("Creating F10 menu for group %s", group:GetName()))

        elseif RANGE.MenuF10Root then

          -- Main F10 menu: F10/<RANGE.MenuF10Root>/<Range Name>
          --_rootMenu = MENU_GROUP:New( group, self.rangename, RANGE.MenuF10Root )
          _rootMenu = RANGE.MenuF10Root

        else

          -----------------
          -- GROUP LEVEL --
          -----------------

          -- Main F10 menu: F10/On the Range/<Range Name>/
          if RANGE.MenuF10[_gid] == nil then
            self:T2(self.lid..string.format("Creating F10 menu 'On the Range' for group %s", group:GetName()))
          else
            self:T2(self.lid..string.format("F10 menu 'On the Range' already EXISTS for group %s", group:GetName()))
          end
          
          _rootMenu=RANGE.MenuF10[_gid] or MENU_GROUP:New( group, "On the Range" )
                    
        end
        
        -- Range menu
        local _rangePath = MENU_GROUP:New( group, self.rangename, _rootMenu )

        local _infoPath = MENU_GROUP:New( group, "Range Info", _rangePath )
        local _markPath = MENU_GROUP:New( group, "Mark Targets", _rangePath )
        local _statsPath = MENU_GROUP:New( group, "Statistics", _rangePath )
        local _settingsPath = MENU_GROUP:New( group, "My Settings", _rangePath )

        -- F10/On the Range/<Range Name>/My Settings/
        local _mysmokePath = MENU_GROUP:New( group, "Smoke Color", _settingsPath )
        local _myflarePath = MENU_GROUP:New( group, "Flare Color", _settingsPath )

        -- F10/On the Range/<Range Name>/Mark Targets/
        local _MoMap = MENU_GROUP_COMMAND:New( group, "Mark On Map", _markPath, self._MarkTargetsOnMap, self, _unitName )
        local _IllRng = MENU_GROUP_COMMAND:New( group, "Illuminate Range", _markPath, self._IlluminateBombTargets, self, _unitName )
        local _SSpit = MENU_GROUP_COMMAND:New( group, "Smoke Strafe Pits", _markPath, self._SmokeStrafeTargetBoxes, self, _unitName )
        local _SStgts = MENU_GROUP_COMMAND:New( group, "Smoke Strafe Tgts", _markPath, self._SmokeStrafeTargets, self, _unitName )
        local _SBtgts = MENU_GROUP_COMMAND:New( group, "Smoke Bomb Tgts", _markPath, self._SmokeBombTargets, self, _unitName )
        -- F10/On the Range/<Range Name>/Stats/
        local _AllSR = MENU_GROUP_COMMAND:New( group, "All Strafe Results", _statsPath, self._DisplayStrafePitResults, self, _unitName )
        local _AllBR = MENU_GROUP_COMMAND:New( group, "All Bombing Results", _statsPath, self._DisplayBombingResults, self, _unitName )
        local _MySR = MENU_GROUP_COMMAND:New( group, "My Strafe Results", _statsPath, self._DisplayMyStrafePitResults, self, _unitName )
        local _MyBR = MENU_GROUP_COMMAND:New( group, "My Bomb Results", _statsPath, self._DisplayMyBombingResults, self, _unitName )
        local _ResetST = MENU_GROUP_COMMAND:New( group, "Reset All Stats", _statsPath, self._ResetRangeStats, self, _unitName )
        -- F10/On the Range/<Range Name>/My Settings/Smoke Color/
        local _BlueSM = MENU_GROUP_COMMAND:New( group, "Blue Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Blue )
        local _GrSM = MENU_GROUP_COMMAND:New( group, "Green Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Green )
        local _OrSM = MENU_GROUP_COMMAND:New( group, "Orange Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Orange )
        local _ReSM = MENU_GROUP_COMMAND:New( group, "Red Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Red )
        local _WhSm = MENU_GROUP_COMMAND:New( group, "White Smoke", _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.White )
        -- F10/On the Range/<Range Name>/My Settings/Flare Color/
        local _GrFl = MENU_GROUP_COMMAND:New( group, "Green Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Green )
        local _ReFl = MENU_GROUP_COMMAND:New( group, "Red Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Red )
        local _WhFl = MENU_GROUP_COMMAND:New( group, "White Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.White )
        local _YeFl = MENU_GROUP_COMMAND:New( group, "Yellow Flares", _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Yellow )
        -- F10/On the Range/<Range Name>/My Settings/
        local _SmDe = MENU_GROUP_COMMAND:New( group, "Smoke Delay On/Off", _settingsPath, self._SmokeBombDelayOnOff, self, _unitName )
        local _SmIm = MENU_GROUP_COMMAND:New( group, "Smoke Impact On/Off", _settingsPath, self._SmokeBombImpactOnOff, self, _unitName )
        local _FlHi = MENU_GROUP_COMMAND:New( group, "Flare Hits On/Off", _settingsPath, self._FlareDirectHitsOnOff, self, _unitName )
        local _AlMeA = MENU_GROUP_COMMAND:New( group, "All Messages On/Off", _settingsPath, self._MessagesToPlayerOnOff, self, _unitName )
        local _TrpSh = MENU_GROUP_COMMAND:New( group, "Targetsheet On/Off", _settingsPath, self._TargetsheetOnOff, self, _unitName )

        -- F10/On the Range/<Range Name>/Range Information
        local _WeIn = MENU_GROUP_COMMAND:New( group, "General Info", _infoPath, self._DisplayRangeInfo, self, _unitName )
        local _WeRe = MENU_GROUP_COMMAND:New( group, "Weather Report", _infoPath, self._DisplayRangeWeather, self, _unitName )
        local _BoTgtgs = MENU_GROUP_COMMAND:New( group, "Bombing Targets", _infoPath, self._DisplayBombTargets, self, _unitName )
        local _StrPits = MENU_GROUP_COMMAND:New( group, "Strafe Pits", _infoPath, self._DisplayStrafePits, self, _unitName ):Refresh()
      end
    else
      self:E( self.lid .. "Could not find group or group ID in AddF10Menu() function. Unit name: " .. _unitName or "N/A")
    end
  else
    self:E( self.lid .. "Player unit does not exist in AddF10Menu() function. Unit name: " .. _unitName or "N/A")
  end

end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Helper Functions
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get the number of shells a unit currently has.
-- @param #RANGE self
-- @param #RANGE.BombTarget target Bomb target data.
-- @return Core.Point#COORDINATE Target coordinate.
function RANGE:_GetBombTargetCoordinate( target )

  local coord = nil -- Core.Point#COORDINATE

  if target.type == RANGE.TargetType.UNIT then

    -- Check if alive
    if target.target and target.target:IsAlive() then
      -- Get current position.
      coord = target.target:GetCoordinate()
      -- Save as last known position in case target dies.
      target.coordinate=coord
    else
      -- Use stored position.
      coord = target.coordinate
    end

  elseif target.type == RANGE.TargetType.STATIC then

    -- Static targets dont move.
    coord = target.coordinate

  elseif target.type == RANGE.TargetType.COORD then

    -- Coordinates dont move.
    coord = target.coordinate

  elseif target.type == RANGE.TargetType.SCENERY then

    -- Coordinates dont move.
    coord = target.coordinate

  else
    self:E( self.lid .. "ERROR: Unknown target type." )
  end

  return coord
end

--- Get the number of shells a unit currently has.
-- @param #RANGE self
-- @param #string unitname Name of the player unit.
-- @return Number of shells left
function RANGE:_GetAmmo( unitname )
  self:F2( unitname )

  -- Init counter.
  local ammo = 0

  local unit, playername = self:_GetPlayerUnitAndName( unitname )

  if unit and playername then

    local has_ammo = false

    local ammotable = unit:GetAmmo()
    self:T2( { ammotable = ammotable } )

    if ammotable ~= nil then

      local weapons = #ammotable
      self:T2( self.lid .. string.format( "Number of weapons %d.", weapons ) )

      for w = 1, weapons do

        local Nammo = ammotable[w]["count"]
        local Tammo = ammotable[w]["desc"]["typeName"]

        -- We are specifically looking for shells here.
        if string.match( Tammo, "shell" ) then

          -- Add up all shells
          ammo = ammo + Nammo

          local text = string.format( "Player %s has %d rounds ammo of type %s", playername, Nammo, Tammo )
          self:T( self.lid .. text )
        else
          local text = string.format( "Player %s has %d ammo of type %s", playername, Nammo, Tammo )
          self:T( self.lid .. text )
        end
      end
    end
  end

  return ammo
end

--- Mark targets on F10 map.
-- @param #RANGE self
-- @param #string _unitName Name of the player unit.
function RANGE:_MarkTargetsOnMap( _unitName )
  self:F( _unitName )

  -- Get group.
  local group = nil -- Wrapper.Group#GROUP
  if _unitName then
    group = UNIT:FindByName( _unitName ):GetGroup()
  end

  -- Mark bomb targets.
  for _, _bombtarget in pairs( self.bombingTargets ) do
    local bombtarget = _bombtarget -- #RANGE.BombTarget
    local coord = self:_GetBombTargetCoordinate( _bombtarget )
    if group then
      coord:MarkToGroup( string.format( "Bomb target %s:\n%s\n%s", bombtarget.name, coord:ToStringLLDMS(), coord:ToStringBULLS( group:GetCoalition() ) ), group )
    else
      coord:MarkToAll( string.format( "Bomb target %s", bombtarget.name ) )
    end
  end

  -- Mark strafe targets.
  for _, _strafepit in pairs( self.strafeTargets ) do
    for _, _target in pairs( _strafepit.targets ) do
      local _target = _target -- Wrapper.Positionable#POSITIONABLE
      if _target and _target:IsAlive() then
        local coord = _target:GetCoordinate() -- Core.Point#COORDINATE
        if group then
          -- coord:MarkToGroup("Strafe target ".._target:GetName(), group)
          coord:MarkToGroup( string.format( "Strafe target %s:\n%s\n%s", _target:GetName(), coord:ToStringLLDMS(), coord:ToStringBULLS( group:GetCoalition() ) ), group )
        else
          coord:MarkToAll( "Strafe target " .. _target:GetName() )
        end
      end
    end
  end

  if _unitName then
    local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
    local text = string.format( "%s, %s, range targets are now marked on F10 map.", self.rangename, _playername )
    self:_DisplayMessageToGroup( _unit, text, 5 )
  end

end

--- Illuminate targets. Fires illumination bombs at one random bomb and one random strafe target at a random altitude between 400 and 800 m.
-- @param #RANGE self
-- @param #string _unitName (Optional) Name of the player unit.
function RANGE:_IlluminateBombTargets( _unitName )
  self:F( _unitName )

  -- All bombing target coordinates.
  local bomb = {}

  for _, _bombtarget in pairs( self.bombingTargets ) do
    local _target = _bombtarget.target -- Wrapper.Positionable#POSITIONABLE
    local coord = self:_GetBombTargetCoordinate( _bombtarget )
    if coord then
      table.insert( bomb, coord )
    end
  end

  if #bomb > 0 then
    local coord = bomb[math.random( #bomb )] -- Core.Point#COORDINATE
    local c = COORDINATE:New( coord.x, coord.y + math.random( self.illuminationminalt, self.illuminationmaxalt ), coord.z )
    c:IlluminationBomb()
  end

  -- All strafe target coordinates.
  local strafe = {}

  for _, _strafepit in pairs( self.strafeTargets ) do
    for _, _target in pairs( _strafepit.targets ) do
      local _target = _target -- Wrapper.Positionable#POSITIONABLE
      if _target and _target:IsAlive() then
        local coord = _target:GetCoordinate() -- Core.Point#COORDINATE
        table.insert( strafe, coord )
      end
    end
  end

  -- Pick a random strafe target.
  if #strafe > 0 then
    local coord = strafe[math.random( #strafe )] -- Core.Point#COORDINATE
    local c = COORDINATE:New( coord.x, coord.y + math.random( self.illuminationminalt, self.illuminationmaxalt ), coord.z )
    c:IlluminationBomb()
  end

  if _unitName then
    local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
    local text = string.format( "%s, %s, range targets are illuminated.", self.rangename, _playername )
    self:_DisplayMessageToGroup( _unit, text, 5 )
  end
end

--- Reset player statistics.
-- @param #RANGE self
-- @param #string _unitName Name of the player unit.
function RANGE:_ResetRangeStats( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  if _unit and _playername then
    self.strafePlayerResults[_playername] = nil
    self.bombPlayerResults[_playername] = nil
    local text = string.format( "%s, %s, your range stats were cleared.", self.rangename, _playername )
    self:DisplayMessageToGroup( _unit, text, 5, false, true )
  end
end

--- Display message to group.
-- @param #RANGE self
-- @param Wrapper.Unit#UNIT _unit Player unit.
-- @param #string _text Message text.
-- @param #number _time Duration how long the message is displayed.
-- @param #boolean _clear Clear up old messages.
-- @param #boolean display If true, display message regardless of player setting "Messages Off".
-- @param #boolean _togroup If true, display the message to the group in any case
function RANGE:_DisplayMessageToGroup( _unit, _text, _time, _clear, display, _togroup )
  self:F( { unit = _unit, text = _text, time = _time, clear = _clear } )

  -- Defaults
  _time = _time or self.Tmsg
  if _clear == nil or _clear == false then
    _clear = false
  else
    _clear = true
  end

  -- Messages globally disabled.
  if self.messages == false then
    return
  end

  -- Check if unit is alive.
  if _unit and _unit:IsAlive() then

    -- Group ID.
    local _gid = _unit:GetGroup():GetID()
    local _grp = _unit:GetGroup()

    -- Get playername and player settings
    local _, playername = self:_GetPlayerUnitAndName( _unit:GetName() )
    local playermessage = self.PlayerSettings[playername].messages

    -- Send message to player if messages enabled and not only for the examiner.

    if _gid and (playermessage == true or display) and (not self.examinerexclusive) then
      if _togroup and _grp then
        local m = MESSAGE:New(_text,_time,nil,_clear):ToGroup(_grp)
      else
        local m = MESSAGE:New(_text,_time,nil,_clear):ToUnit(_unit)
      end
    end

    -- Send message to examiner.
    if self.examinergroupname ~= nil then
      local _examinerid = GROUP:FindByName( self.examinergroupname )
      if _examinerid then
        local m = MESSAGE:New(_text,_time,nil,_clear):ToGroup(_examinerid)
      end
    end
  end

end

--- Toggle status of smoking bomb impact points.
-- @param #RANGE self
-- @param #string unitname Name of the player unit.
function RANGE:_SmokeBombImpactOnOff( unitname )
  self:F( unitname )

  local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( unitname )
  if unit and playername then
    local text
    if self.PlayerSettings[playername].smokebombimpact == true then
      self.PlayerSettings[playername].smokebombimpact = false
      text = string.format( "%s, %s, smoking impact points of bombs is now OFF.", self.rangename, playername )
    else
      self.PlayerSettings[playername].smokebombimpact = true
      text = string.format( "%s, %s, smoking impact points of bombs is now ON.", self.rangename, playername )
    end
    self:_DisplayMessageToGroup( unit, text, 5, false, true )
  end

end

--- Toggle status of time delay for smoking bomb impact points
-- @param #RANGE self
-- @param #string unitname Name of the player unit.
function RANGE:_SmokeBombDelayOnOff( unitname )
  self:F( unitname )

  local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( unitname )
  if unit and playername then
    local text
    if self.PlayerSettings[playername].delaysmoke == true then
      self.PlayerSettings[playername].delaysmoke = false
      text = string.format( "%s, %s, delayed smoke of bombs is now OFF.", self.rangename, playername )
    else
      self.PlayerSettings[playername].delaysmoke = true
      text = string.format( "%s, %s, delayed smoke of bombs is now ON.", self.rangename, playername )
    end
    self:_DisplayMessageToGroup( unit, text, 5, false, true )
  end

end

--- Toggle display messages to player.
-- @param #RANGE self
-- @param #string unitname Name of the player unit.
function RANGE:_MessagesToPlayerOnOff( unitname )
  self:F( unitname )

  local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( unitname )
  if unit and playername then
    local text
    if self.PlayerSettings[playername].messages == true then
      text = string.format( "%s, %s, display of ALL messages is now OFF.", self.rangename, playername )
    else
      text = string.format( "%s, %s, display of ALL messages is now ON.", self.rangename, playername )
    end
    self:_DisplayMessageToGroup( unit, text, 5, false, true )
    self.PlayerSettings[playername].messages = not self.PlayerSettings[playername].messages
  end

end

--- Targetsheet saves if player on or off.
-- @param #RANGE self
-- @param #string _unitname Name of the player unit.
function RANGE:_TargetsheetOnOff( _unitname )
  self:F2( _unitname )

  -- Get player unit and player name.
  local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( _unitname )

  -- Check if we have a player.
  if unit and playername then

    -- Player data.
    local playerData = self.PlayerSettings[playername] -- #RANGE.PlayerData

    if playerData then

      -- Check if option is enabled at all.
      local text = ""
      if self.targetsheet then

        -- Invert current setting.
        playerData.targeton = not playerData.targeton

        -- Inform player.
        if playerData and playerData.targeton == true then
          text = string.format( "Roger, your targetsheets are now SAVED." )
        else
          text = string.format( "Affirm, your targetsheets are NOT SAVED." )
        end

      else
        text = "Negative, target sheet data recorder is broken on this range."
      end

      -- Message to player.
      -- self:MessageToPlayer(playerData, text, nil, playerData.name, 5)
      self:_DisplayMessageToGroup( unit, text, 5, false, false )
    end
  end

end

--- Toggle status of flaring direct hits of range targets.
-- @param #RANGE self
-- @param #string unitname Name of the player unit.
function RANGE:_FlareDirectHitsOnOff( unitname )
  self:F( unitname )

  local unit, playername, _multiplayer = self:_GetPlayerUnitAndName( unitname )
  if unit and playername then
    local text
    if self.PlayerSettings[playername].flaredirecthits == true then
      self.PlayerSettings[playername].flaredirecthits = false
      text = string.format( "%s, %s, flaring direct hits is now OFF.", self.rangename, playername )
    else
      self.PlayerSettings[playername].flaredirecthits = true
      text = string.format( "%s, %s, flaring direct hits is now ON.", self.rangename, playername )
    end
    self:_DisplayMessageToGroup( unit, text, 5, false, true )
  end

end

--- Mark bombing targets with smoke.
-- @param #RANGE self
-- @param #string unitname Name of the player unit.
function RANGE:_SmokeBombTargets( unitname )
  self:F( unitname )

  for _, _bombtarget in pairs( self.bombingTargets ) do
    local _target = _bombtarget.target -- Wrapper.Positionable#POSITIONABLE
    local coord = self:_GetBombTargetCoordinate( _bombtarget )
    if coord then
      coord:Smoke( self.BombSmokeColor )
    end
  end

  if unitname then
    local unit, playername = self:_GetPlayerUnitAndName( unitname )
    local text = string.format( "%s, %s, bombing targets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text( self.BombSmokeColor ) )
    self:_DisplayMessageToGroup( unit, text, 5 )
  end

end

--- Mark strafing targets with smoke.
-- @param #RANGE self
-- @param #string unitname Name of the player unit.
function RANGE:_SmokeStrafeTargets( unitname )
  self:F( unitname )

  for _, _target in pairs( self.strafeTargets ) do
    _target.coordinate:Smoke( self.StrafeSmokeColor )
  end

  if unitname then
    local unit, playername = self:_GetPlayerUnitAndName( unitname )
    local text = string.format( "%s, %s, strafing tragets are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text( self.StrafeSmokeColor ) )
    self:_DisplayMessageToGroup( unit, text, 5 )
  end

end

--- Mark approach boxes of strafe targets with smoke.
-- @param #RANGE self
-- @param #string unitname Name of the player unit.
function RANGE:_SmokeStrafeTargetBoxes( unitname )
  self:F( unitname )

  for _, _target in pairs( self.strafeTargets ) do
    local zone = _target.polygon -- Core.Zone#ZONE
    zone:SmokeZone( self.StrafePitSmokeColor, 4 )
    for _, _point in pairs( _target.smokepoints ) do
      _point:SmokeOrange() -- Corners are smoked orange.
    end
  end

  if unitname then
    local unit, playername = self:_GetPlayerUnitAndName( unitname )
    local text = string.format( "%s, %s, strafing pit approach boxes are now marked with %s smoke.", self.rangename, playername, self:_smokecolor2text( self.StrafePitSmokeColor ) )
    self:_DisplayMessageToGroup( unit, text, 5 )
  end

end

--- Sets the smoke color used to smoke players bomb impact points.
-- @param #RANGE self
-- @param #string _unitName Name of the player unit.
-- @param Utilities.Utils#SMOKECOLOR color ID of the smoke color.
function RANGE:_playersmokecolor( _unitName, color )
  self:F( { unitname = _unitName, color = color } )

  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
  if _unit and _playername then
    self.PlayerSettings[_playername].smokecolor = color
    local text = string.format( "%s, %s, your bomb impacts are now smoked in %s.", self.rangename, _playername, self:_smokecolor2text( color ) )
    self:_DisplayMessageToGroup( _unit, text, 5 )
  end

end

--- Sets the flare color used when player makes a direct hit on target.
-- @param #RANGE self
-- @param #string _unitName Name of the player unit.
-- @param Utilities.Utils#FLARECOLOR color ID of flare color.
function RANGE:_playerflarecolor( _unitName, color )
  self:F( { unitname = _unitName, color = color } )

  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
  if _unit and _playername then
    self.PlayerSettings[_playername].flarecolor = color
    local text = string.format( "%s, %s, your direct hits are now flared in %s.", self.rangename, _playername, self:_flarecolor2text( color ) )
    self:_DisplayMessageToGroup( _unit, text, 5 )
  end

end

--- Converts a smoke color id to text. E.g. SMOKECOLOR.Blue --> "blue".
-- @param #RANGE self
-- @param Utilities.Utils#SMOKECOLOR color Color Id.
-- @return #string Color text.
function RANGE:_smokecolor2text( color )
  self:F( color )

  local txt = ""
  if color == SMOKECOLOR.Blue then
    txt = "blue"
  elseif color == SMOKECOLOR.Green then
    txt = "green"
  elseif color == SMOKECOLOR.Orange then
    txt = "orange"
  elseif color == SMOKECOLOR.Red then
    txt = "red"
  elseif color == SMOKECOLOR.White then
    txt = "white"
  else
    txt = string.format( "unknown color (%s)", tostring( color ) )
  end

  return txt
end

--- Sets the flare color used to flare players direct target hits.
-- @param #RANGE self
-- @param Utilities.Utils#FLARECOLOR color Color Id.
-- @return #string Color text.
function RANGE:_flarecolor2text( color )
  self:F( color )

  local txt = ""
  if color == FLARECOLOR.Green then
    txt = "green"
  elseif color == FLARECOLOR.Red then
    txt = "red"
  elseif color == FLARECOLOR.White then
    txt = "white"
  elseif color == FLARECOLOR.Yellow then
    txt = "yellow"
  else
    txt = string.format( "unknown color (%s)", tostring( color ) )
  end

  return txt
end

--- Checks if a static object with a certain name exists. It also added it to the MOOSE data base, if it is not already in there.
-- @param #RANGE self
-- @param #string name Name of the potential static object.
-- @return #boolean Returns true if a static with this name exists. Retruns false if a unit with this name exists. Returns nil if neither unit or static exist.
function RANGE:_CheckStatic( name )
  self:F2( name )

  -- Get DCS static object.
  local _DCSstatic = StaticObject.getByName( name )

  if _DCSstatic and _DCSstatic:isExist() then

    -- Static does exist at least in DCS. Check if it also in the MOOSE DB.
    local _MOOSEstatic = STATIC:FindByName( name, false )

    -- If static is not yet in MOOSE DB, we add it. Can happen for cargo statics!
    if not _MOOSEstatic then
      self:T( self.lid .. string.format( "Adding DCS static to MOOSE database. Name = %s.", name ) )
      _DATABASE:AddStatic( name )
    end

    return true
  else
    self:T3( self.lid .. string.format( "No static object with name %s exists.", name ) )
  end

  -- Check if a unit has this name.
  if UNIT:FindByName( name ) then
    return false
  else
    self:T3( self.lid .. string.format( "No unit object with name %s exists.", name ) )
  end

  -- If not unit or static exist, we return nil.
  return nil
end

--- Get max speed of controllable.
-- @param #RANGE self
-- @param Wrapper.Controllable#CONTROLLABLE controllable
-- @return Maximum speed in km/h.
function RANGE:_GetSpeed( controllable )
  self:F2( controllable )

  -- Get DCS descriptors
  local desc = controllable:GetDesc()

  -- Get speed
  local speed = 0
  if desc then
    speed = desc.speedMax * 3.6
    self:T( { speed = speed } )
  end

  return speed
end

--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned.
-- @param #RANGE self
-- @param #string _unitName Name of the player unit.
-- @return Wrapper.Unit#UNIT Unit of player.
-- @return #string Name of the player.
-- @return #boolean If true, group has > 1 player in it
function RANGE:_GetPlayerUnitAndName( _unitName, PlayerName )
  --self:I( _unitName )

  if _unitName ~= nil then

    local multiplayer = false

    -- Get DCS unit from its name.
    local DCSunit = Unit.getByName( _unitName )

    if DCSunit and DCSunit.getPlayerName then

      local playername = DCSunit:getPlayerName() or PlayerName or "None"
      local unit = UNIT:Find( DCSunit )

      self:T2( { DCSunit = DCSunit, unit = unit, playername = playername } )
      if DCSunit and unit and playername then
        self:F2(playername)
        local grp = unit:GetGroup()
        if grp and grp:CountAliveUnits() > 1 then
          multiplayer = true
        end
        return unit, playername, multiplayer
      end

    end

  end

  -- Return nil if we could not find a player.
  return nil, nil, nil
end

--- Returns a string which consists of the player name.
-- @param #RANGE self
-- @param #string unitname Name of the player unit.
function RANGE:_myname( unitname )
  self:F2( unitname )
  local pname = "Ghost 1 1"
  local unit = UNIT:FindByName( unitname )
  if unit and unit:IsAlive() then
    local grp = unit:GetGroup()
    if grp and grp:IsAlive() then
      pname = grp:GetCustomCallSign(true,true)
    end
  end
  return pname
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Functional** - Base class that models processes to achieve goals involving a Zone.
--
-- ===
-- 
-- ZONE_GOAL models processes that have a Goal with a defined achievement involving a Zone. 
-- Derived classes implement the ways how the achievements can be realized.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: **funkyfranky**, **Applevangelist**
-- 
-- ===
-- 
-- @module Functional.ZoneGoal
-- @image MOOSE.JPG

do -- Zone

  -- @type ZONE_GOAL
  -- @field #string ClassName Name of the class.
  -- @field Core.Goal#GOAL Goal The goal object.
  -- @field #number SmokeTime Time stamp in seconds when the last smoke of the zone was triggered.
  -- @field Core.Scheduler#SCHEDULER SmokeScheduler Scheduler responsible for smoking the zone.
  -- @field #number SmokeColor Color of the smoke.
  -- @field #boolean SmokeZone If true, smoke zone.
  -- @extends Core.Zone#ZONE_RADIUS

  --- Models processes that have a Goal with a defined achievement involving a Zone. 
  -- Derived classes implement the ways how the achievements can be realized.
  -- 
  -- ## 1. ZONE_GOAL constructor
  --   
  --   * @{#ZONE_GOAL.New}(): Creates a new ZONE_GOAL object.
  -- 
  -- ## 2. ZONE_GOAL is a finite state machine (FSM).
  -- 
  -- ### 2.1 ZONE_GOAL States
  -- 
  --  * None: Initial State
  -- 
  -- ### 2.2 ZONE_GOAL Events
  -- 
  --   * DestroyedUnit: A @{Wrapper.Unit} is destroyed in the Zone. The event will only get triggered if the method @{#ZONE_GOAL.MonitorDestroyedUnits}() is used.
  -- 
  -- @field #ZONE_GOAL
  ZONE_GOAL = {
    ClassName      = "ZONE_GOAL",
    Goal           = nil,
    SmokeTime      = nil,
    SmokeScheduler = nil,
    SmokeColor     = nil,
    SmokeZone      = nil,
  }

  --- ZONE_GOAL Constructor.
  -- @param #ZONE_GOAL self
  -- @param Core.Zone#ZONE_RADIUS Zone A @{Core.Zone} object with the goal to be achieved. Alternatively, can be handed as the name of late activated group describing a ZONE_POLYGON with its waypoints.
  -- @return #ZONE_GOAL
  function ZONE_GOAL:New( Zone )

    BASE:I({Zone=Zone})
    local self = BASE:Inherit( self, BASE:New())
    if type(Zone) == "string" then
       self = BASE:Inherit( self, ZONE_POLYGON:NewFromGroupName(Zone) )
    else
        self = BASE:Inherit( self, ZONE_RADIUS:New( Zone:GetName(), Zone:GetVec2(), Zone:GetRadius() ) ) -- #ZONE_GOAL
        self:F( { Zone = Zone } )
    end
    -- Goal object.
    self.Goal = GOAL:New()

    self.SmokeTime = nil

    -- Set smoke ON.
    self:SetSmokeZone(true)

    self:AddTransition( "*", "DestroyedUnit", "*" )

    --- DestroyedUnit event.
    -- @function [parent=#ZONE_GOAL] DestroyedUnit
    -- @param #ZONE_GOAL self

    --- DestroyedUnit delayed event
    -- @function [parent=#ZONE_GOAL] __DestroyedUnit
    -- @param #ZONE_GOAL self
    -- @param #number delay Delay in seconds.

    --- DestroyedUnit Handler OnAfter for ZONE_GOAL
    -- @function [parent=#ZONE_GOAL] OnAfterDestroyedUnit
    -- @param #ZONE_GOAL self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Wrapper.Unit#UNIT DestroyedUnit The destroyed unit.
    -- @param #string PlayerName The name of the player.

    return self
  end

  --- Get the Zone.
  -- @param #ZONE_GOAL self
  -- @return #ZONE_GOAL
  function ZONE_GOAL:GetZone()
    return self
  end


  --- Get the name of the Zone.
  -- @param #ZONE_GOAL self
  -- @return #string
  function ZONE_GOAL:GetZoneName()
    return self:GetName()
  end

  --- Activate smoking of zone with the color or the current owner.
  -- @param #ZONE_GOAL self
  -- @param #boolean switch If *true* or *nil* activate smoke. If *false* or *nil*, no smoke.
  -- @return #ZONE_GOAL
  function ZONE_GOAL:SetSmokeZone(switch)
    self.SmokeZone=switch
    --[[
    if switch==nil or switch==true then
      self.SmokeZone=true
    else
      self.SmokeZone=false
    end
    ]]
    return self
  end

  --- Set the smoke color.
  -- @param #ZONE_GOAL self
  -- @param DCS#SMOKECOLOR.Color SmokeColor
  function ZONE_GOAL:Smoke( SmokeColor ) 
    self:F( { SmokeColor = SmokeColor} )

    self.SmokeColor = SmokeColor
  end

  --- Flare the zone boundary.
  -- @param #ZONE_GOAL self
  -- @param DCS#SMOKECOLOR.Color FlareColor
  function ZONE_GOAL:Flare( FlareColor )
    self:FlareZone( FlareColor, 30)
  end

  --- When started, check the Smoke and the Zone status.
  -- @param #ZONE_GOAL self
  function ZONE_GOAL:onafterGuard()
    self:F("Guard")

    -- Start smoke
    if self.SmokeZone and not self.SmokeScheduler then
      self.SmokeScheduler = self:ScheduleRepeat(1, 1, 0.1, nil, self.StatusSmoke, self)
    end
  end

  --- Check status Smoke.
  -- @param #ZONE_GOAL self
  function ZONE_GOAL:StatusSmoke()
    self:F({self.SmokeTime, self.SmokeColor})

    if self.SmokeZone then

      -- Current time.
      local CurrentTime = timer.getTime()

      -- Restart smoke every 5 min.
      if self.SmokeTime == nil or self.SmokeTime + 300 <= CurrentTime then
        if self.SmokeColor then
          self:GetCoordinate():Smoke( self.SmokeColor )
          self.SmokeTime = CurrentTime
        end
      end

    end

  end

  -- @param #ZONE_GOAL self
  -- @param Core.Event#EVENTDATA EventData Event data table.
  function ZONE_GOAL:__Destroyed( EventData )
    self:F( { "EventDead", EventData } )

    self:F( { EventData.IniUnit } )

    if EventData.IniDCSUnit then

      local Vec3 = EventData.IniDCSUnit:getPosition().p
      self:F( { Vec3 = Vec3 } )    

      if Vec3 and self:IsVec3InZone(Vec3)  then

        local PlayerHits = _DATABASE.HITS[EventData.IniUnitName]

        if PlayerHits then

          for PlayerName, PlayerHit in pairs( PlayerHits.Players or {} ) do
            self.Goal:AddPlayerContribution( PlayerName )
            self:DestroyedUnit( EventData.IniUnitName, PlayerName )
          end

        end

      end
    end

  end

  --- Activate the event UnitDestroyed to be fired when a unit is destroyed in the zone.
  -- @param #ZONE_GOAL self
  function ZONE_GOAL:MonitorDestroyedUnits()

    self:HandleEvent( EVENTS.Dead,  self.__Destroyed )
    self:HandleEvent( EVENTS.Crash, self.__Destroyed )

  end

end
--- **Functional (WIP)** - Base class modeling processes to achieve goals involving coalition zones.
--
-- ===
--
-- ZONE_GOAL_COALITION models processes that have a Goal with a defined achievement involving a Zone for a Coalition.
-- Derived classes implement the ways how the achievements can be realized.
--
-- ===
--
-- ### Author: **FlightControl**
--
-- ===
--
-- @module Functional.ZoneGoalCoalition
-- @image MOOSE.JPG

do -- ZoneGoal

  -- @type ZONE_GOAL_COALITION
  -- @field #string ClassName Name of the Class.
  -- @field #number Coalition The current coalition ID of the zone owner.
  -- @field #number PreviousCoalition The previous owner of the zone.
  -- @field #table UnitCategories Table of unit categories that are able to capture and hold the zone. Default is only GROUND units.
  -- @field #table ObjectCategories Table of object categories that are able to hold a zone. Default is UNITS and STATICS.
  -- @extends Functional.ZoneGoal#ZONE_GOAL

  --- ZONE_GOAL_COALITION models processes that have a Goal with a defined achievement involving a Zone for a Coalition.
  -- Derived classes implement the ways how the achievements can be realized.
  --
  -- ## 1. ZONE_GOAL_COALITION constructor
  --
  --   * @{#ZONE_GOAL_COALITION.New}(): Creates a new ZONE_GOAL_COALITION object.
  --
  -- ## 2. ZONE_GOAL_COALITION is a finite state machine (FSM).
  --
  -- ### 2.1 ZONE_GOAL_COALITION States
  --
  -- ### 2.2 ZONE_GOAL_COALITION Events
  --
  -- ### 2.3 ZONE_GOAL_COALITION State Machine
  --
  -- @field #ZONE_GOAL_COALITION
  ZONE_GOAL_COALITION = {
    ClassName = "ZONE_GOAL_COALITION",
    Coalition = nil,
    PreviousCoalition = nil,
    UnitCategories = nil,
    ObjectCategories = nil,
  }

  -- @field #table ZONE_GOAL_COALITION.States
  ZONE_GOAL_COALITION.States = {}

  --- ZONE_GOAL_COALITION Constructor.
  -- @param #ZONE_GOAL_COALITION self
  -- @param Core.Zone#ZONE Zone A @{Core.Zone} object with the goal to be achieved.
  -- @param #number Coalition The initial coalition owning the zone. Default coalition.side.NEUTRAL.
  -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}.
  -- @return #ZONE_GOAL_COALITION
  function ZONE_GOAL_COALITION:New( Zone, Coalition, UnitCategories )

    if not Zone then
      BASE:E( "ERROR: No Zone specified in ZONE_GOAL_COALITION!" )
      return nil
    end

    -- Inherit ZONE_GOAL.
    local self = BASE:Inherit( self, ZONE_GOAL:New( Zone ) ) -- #ZONE_GOAL_COALITION
    self:F( { Zone = Zone, Coalition = Coalition } )

    -- Set initial owner.
    self:SetCoalition( Coalition or coalition.side.NEUTRAL )

    -- Set default unit and object categories for the zone scan.
    self:SetUnitCategories( UnitCategories )
    self:SetObjectCategories()

    return self
  end

  --- Set the owning coalition of the zone.
  -- @param #ZONE_GOAL_COALITION self
  -- @param #number Coalition The coalition ID, e.g. *coalition.side.RED*.
  -- @return #ZONE_GOAL_COALITION
  function ZONE_GOAL_COALITION:SetCoalition( Coalition )
    self.PreviousCoalition = self.Coalition or Coalition
    self.Coalition = Coalition
    return self
  end

  --- Set the owning coalition of the zone.
  -- @param #ZONE_GOAL_COALITION self
  -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}.
  -- @return #ZONE_GOAL_COALITION
  function ZONE_GOAL_COALITION:SetUnitCategories( UnitCategories )

    if UnitCategories and type( UnitCategories ) ~= "table" then
      UnitCategories = { UnitCategories }
    end

    self.UnitCategories = UnitCategories or { Unit.Category.GROUND_UNIT }

    return self
  end

  --- Set the owning coalition of the zone.
  -- @param #ZONE_GOAL_COALITION self
  -- @param #table ObjectCategories Table of unit categories. See [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object). Default {Object.Category.UNIT, Object.Category.STATIC}, i.e. all UNITS and STATICS.
  -- @return #ZONE_GOAL_COALITION
  function ZONE_GOAL_COALITION:SetObjectCategories( ObjectCategories )

    if ObjectCategories and type( ObjectCategories ) ~= "table" then
      ObjectCategories = { ObjectCategories }
    end

    self.ObjectCategories = ObjectCategories or { Object.Category.UNIT, Object.Category.STATIC }

    return self
  end

  --- Get the owning coalition of the zone.
  -- @param #ZONE_GOAL_COALITION self
  -- @return #number Coalition.
  function ZONE_GOAL_COALITION:GetCoalition()
    return self.Coalition
  end

  --- Get the previous coalition, i.e. the one owning the zone before the current one. 
  -- @param #ZONE_GOAL_COALITION self
  -- @return #number Coalition.
  function ZONE_GOAL_COALITION:GetPreviousCoalition()
    return self.PreviousCoalition
  end

  --- Get the owning coalition name of the zone.
  -- @param #ZONE_GOAL_COALITION self
  -- @return #string Coalition name.
  function ZONE_GOAL_COALITION:GetCoalitionName()
    return UTILS.GetCoalitionName( self.Coalition )
  end

  --- Check status Coalition ownership.
  -- @param #ZONE_GOAL_COALITION self
  -- @return #ZONE_GOAL_COALITION
  function ZONE_GOAL_COALITION:StatusZone()

    -- Get current state.
    local State = self:GetState()

    -- Debug text.
    local text = string.format( "Zone state=%s, Owner=%s, Scanning...", State, self:GetCoalitionName() )
    self:F( text )

    -- Scan zone.
    self:Scan( self.ObjectCategories, self.UnitCategories )

    return self
  end

end

--- **Functional** - Models the process to zone guarding and capturing.
--
-- ===
-- 
-- ## Features:
-- 
--   * Models the possible state transitions between the Guarded, Attacked, Empty and Captured states.
--   * A zone has an owning coalition, that means that at a specific point in time, a zone can be owned by the red or blue coalition.
--   * Provide event handlers to tailor the actions when a zone changes coalition or state.
-- 
-- ===
-- 
-- ## Missions:
-- 
-- [CAZ - Capture Zones](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Functional/ZoneCaptureCoalition)
-- 
-- ===
-- 
-- # Player Experience
-- 
-- ![States](..\Presentations\ZONE_CAPTURE_COALITION\Dia3.JPG)
--  
-- The above models the possible state transitions between the **Guarded**, **Attacked**, **Empty** and **Captured** states.  
-- A zone has an __owning coalition__, that means that at a specific point in time, a zone can be owned by the red or blue coalition.
--
-- The Zone can be in the state **Guarded** by the __owning coalition__, which is the coalition that initially occupies the zone with units of its coalition.  
-- Once units of an other coalition are entering the Zone, the state will change to **Attacked**. As long as these units remain in the zone, the state keeps set to Attacked.  
-- When all units are destroyed in the Zone, the state will change to **Empty**, which expresses that the Zone is empty, and can be captured.  
-- When units of the other coalition are in the Zone, and no other units of the owning coalition is in the Zone, the Zone is captured, and its state will change to **Captured**.  
-- 
-- The zone needs to be monitored regularly for the presence of units to interprete the correct state transition required.  
-- This monitoring process MUST be started using the @{#ZONE_CAPTURE_COALITION.Start}() method.  
-- Otherwise no monitoring will be active and the zone will stay in the current state forever.  
-- 
-- ===
-- 
-- ## [YouTube Playlist](https://www.youtube.com/watch?v=0m6K6Yxa-os&list=PL7ZUrU4zZUl0qqJsfa8DPvZWDY-OyDumE)
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: **Millertime** - Concept, **funkyfranky**
-- 
-- ===
-- 
-- @module Functional.ZoneCaptureCoalition
-- @image Capture_Zones.JPG

do -- ZONE_CAPTURE_COALITION

  -- @type ZONE_CAPTURE_COALITION
  -- @field #string ClassName Name of the class.
  -- @field #number MarkBlue ID of blue F10 mark.
  -- @field #number MarkRed ID of red F10 mark.
  -- @field #number StartInterval Time in seconds after the status monitor is started.
  -- @field #number RepeatInterval Time in seconds after which the zone status is updated.
  -- @field #boolean HitsOn If true, hit events are monitored and trigger the "Attack" event when a defending unit is hit.
  -- @field #number HitTimeLast Time stamp in seconds when the last unit inside the zone was hit.
  -- @field #number HitTimeAttackOver Time interval in seconds before the zone goes from "Attacked" to "Guarded" state after the last hit.
  -- @field #boolean MarkOn If true, create marks of zone status on F10 map.
  -- @extends Functional.ZoneGoalCoalition#ZONE_GOAL_COALITION


  --- Models the process to capture a Zone for a Coalition, which is guarded by another Coalition.
  -- This is a powerful concept that allows to create very dynamic missions based on the different state transitions of various zones.
  -- 
  -- ===
  -- 
  -- In order to use ZONE_CAPTURE_COALITION, you need to:
  -- 
  --   * Create a @{Core.Zone} object from one of the ZONE_ classes.    
  --     The functional ZONE_ classses are those derived from a ZONE_RADIUS.
  --     In order to use a ZONE_POLYGON, hand over the **GROUP name** of a late activated group forming a polygon with it's waypoints.
  --   * Set the state of the zone. Most of the time, Guarded would be the initial state.
  --   * Start the zone capturing **monitoring process**.  
  --     This will check the presence of friendly and/or enemy units within the zone and will transition the state of the zone when the tactical situation changed.
  --     The frequency of the monitoring must not be real-time, a 30 second interval to execute the checks is sufficient. 
  -- 
  -- ![New](..\Presentations\ZONE_CAPTURE_COALITION\Dia5.JPG)
  -- 
  -- ### Important:
  -- 
  -- You must start the monitoring process within your code, or there won't be any state transition checks executed.  
  -- See further the start/stop monitoring process.
  -- 
  -- ### Important:
  -- 
  -- Ensure that the object containing the ZONE_CAPTURE_COALITION object is persistent.
  -- Otherwise the garbage collector of lua will remove the object and the monitoring process will stop.
  -- This will result in your object to be destroyed (removed) from internal memory and there won't be any zone state transitions anymore detected!
  -- So use the `local` keyword in lua with thought! Most of the time, you can declare your object gobally.
  -- 
  -- 
  -- 
  -- # Example:
  -- 
  --       -- Define a new ZONE object, which is based on the trigger zone `CaptureZone`, which is defined within the mission editor.
  --       CaptureZone = ZONE:New( "CaptureZone" )
  --       
  --       -- Here we create a new ZONE_CAPTURE_COALITION object, using the :New constructor.
  --       ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( CaptureZone, coalition.side.RED )
  --       
  --       -- Set the zone to Guarding state.
  --       ZoneCaptureCoalition:__Guard( 1 )
  --       
  --       -- Start the zone monitoring process in 30 seconds and check every 30 seconds.
  --       ZoneCaptureCoalition:Start( 30, 30 ) 
  --   
  -- 
  -- # Constructor:
  --   
  -- Use the @{#ZONE_CAPTURE_COALITION.New}() constructor to create a new ZONE_CAPTURE_COALITION object.
  -- 
  -- # ZONE_CAPTURE_COALITION is a finite state machine (FSM).
  -- 
  -- ![States](..\Presentations\ZONE_CAPTURE_COALITION\Dia4.JPG)
  -- 
  -- ## ZONE_CAPTURE_COALITION States
  -- 
  --   * **Captured**: The Zone has been captured by an other coalition.
  --   * **Attacked**: The Zone is currently intruded by an other coalition. There are units of the owning coalition and an other coalition in the Zone.
  --   * **Guarded**: The Zone is guarded by the owning coalition. There is no other unit of an other coalition in the Zone.
  --   * **Empty**: The Zone is empty. There is not valid unit in the Zone.
  --   
  -- ## 2.2 ZONE_CAPTURE_COALITION Events
  -- 
  --   * **Capture**: The Zone has been captured by an other coalition.
  --   * **Attack**: The Zone is currently intruded by an other coalition. There are units of the owning coalition and an other coalition in the Zone.
  --   * **Guard**: The Zone is guarded by the owning coalition. There is no other unit of an other coalition in the Zone.
  --   * **Empty**: The Zone is empty. There is not valid unit in the Zone.
  -- 
  -- # "Script It"
  -- 
  -- ZONE_CAPTURE_COALITION allows to take action on the various state transitions and add your custom code and logic.
  -- 
  -- ## Take action using state- and event handlers.
  -- 
  -- ![States](..\Presentations\ZONE_CAPTURE_COALITION\Dia6.JPG)
  -- 
  -- The most important to understand is how states and events can be tailored.
  -- Carefully study the diagram and the explanations.
  -- 
  -- **State Handlers** capture the moment:
  -- 
  --   - On Leave from the old state. Return false to cancel the transition.
  --   - On Enter to the new state.
  -- 
  -- **Event Handlers** capture the moment:
  -- 
  --   - On Before the event is triggered. Return false to cancel the transition.
  --   - On After the event is triggered.
  -- 
  -- ![States](..\Presentations\ZONE_CAPTURE_COALITION\Dia7.JPG)
  -- 
  -- Each handler can receive optionally 3 parameters:
  -- 
  --   - **From**: A string containing the From State.
  --   - **Event**: A string containing the Event.
  --   - **To**: A string containing the To State.
  --   
  -- The mission designer can use these values to alter the logic.
  -- For example:
  -- 
  --     -- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self
  --     function ZoneCaptureCoalition:OnEnterGuarded( From, Event, To )
  --       if From ~= "Empty" then
  --         -- Display a message
  --       end
  --     end
  -- 
  -- This code checks that when the __Guarded__ state has been reached, that if the **From** state was __Empty__, then display a message.
  -- 
  -- ## Example Event Handler.
  -- 
  --     -- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self
  --     function ZoneCaptureCoalition:OnEnterGuarded( From, Event, To )
  --       if From ~= To then
  --         local Coalition = self:GetCoalition()
  --         self:E( { Coalition = Coalition } )
  --         if Coalition == coalition.side.BLUE then
  --           ZoneCaptureCoalition:Smoke( SMOKECOLOR.Blue )
  --           US_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --           RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --         else
  --           ZoneCaptureCoalition:Smoke( SMOKECOLOR.Red )
  --           RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --           US_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --         end
  --       end
  --     end
  --     
  -- ## Stop and Start the zone monitoring process.
  -- 
  -- At regular intervals, the state of the zone needs to be monitored.
  -- The zone needs to be scanned for the presence of units within the zone boundaries.
  -- Depending on the owning coalition of the zone and the presence of units (of the owning and/or other coalition(s)), the zone will transition to another state.
  -- 
  -- However, ... this scanning process is rather CPU intensive. Imagine you have 10 of these capture zone objects setup within your mission.
  -- That would mean that your mission would check 10 capture zones simultaneously, each checking for the presence of units.
  -- It would be highly **CPU inefficient**, as some of these zones are not required to be monitored (yet).
  -- 
  -- Therefore, the mission designer is given 2 methods that allow to take control of the CPU utilization efficiency:
  -- 
  --   * @{#ZONE_CAPTURE_COALITION.Start}(): This starts the monitoring process.
  --   * @{#ZONE_CAPTURE_COALITION.Stop}(): This stops the monitoring process.
  --   
  -- ### IMPORTANT
  -- 
  -- **Each capture zone object must have the monitoring process started specifically. The monitoring process is NOT started by default!**
  --   
  -- 
  -- # Full Example
  -- 
  -- The following annotated code shows a real example of how ZONE_CAPTURE_COALITION can be applied.
  -- 
  -- The concept is simple.
  -- 
  -- The USA (US), blue coalition, needs to capture the Russian (RU), red coalition, zone, which is near groom lake.
  -- 
  -- A capture zone has been setup that guards the presence of the troops.
  -- Troops are guarded by red forces. Blue is required to destroy the red forces and capture the zones.
  -- 
  -- At first, we setup the Command Centers
  -- 
  --      do
  --        
  --        RU_CC = COMMANDCENTER:New( GROUP:FindByName( "REDHQ" ), "Russia HQ" )
  --        US_CC = COMMANDCENTER:New( GROUP:FindByName( "BLUEHQ" ), "USA HQ" )
  --      
  --      end
  --      
  -- Next, we define the mission, and add some scoring to it.
  --      
  --      do -- Missions
  --        
  --        US_Mission_EchoBay = MISSION:New( US_CC, "Echo Bay", "Primary",
  --          "Welcome trainee. The airport Groom Lake in Echo Bay needs to be captured.\n" ..
  --          "There are five random capture zones located at the airbase.\n" ..
  --          "Move to one of the capture zones, destroy the fuel tanks in the capture zone, " ..
  --          "and occupy each capture zone with a platoon.\n " .. 
  --          "Your orders are to hold position until all capture zones are taken.\n" ..
  --          "Use the map (F10) for a clear indication of the location of each capture zone.\n" ..
  --          "Note that heavy resistance can be expected at the airbase!\n" ..
  --          "Mission 'Echo Bay' is complete when all five capture zones are taken, and held for at least 5 minutes!"
  --          , coalition.side.RED )
  --          
  --        US_Mission_EchoBay:Start()
  --      
  --      end
  --      
  --      
  -- Now the real work starts.
  -- We define a **CaptureZone** object, which is a ZONE object.
  -- Within the mission, a trigger zone is created with the name __CaptureZone__, with the defined radius within the mission editor.
  -- 
  --      CaptureZone = ZONE:New( "CaptureZone" )
  -- 
  -- Next, we define the **ZoneCaptureCoalition** object, as explained above.
  --      
  --      ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( CaptureZone, coalition.side.RED ) 
  --      
  -- Of course, we want to let the **ZoneCaptureCoalition** object do something when the state transitions.
  -- Do accomodate this, it is very simple, as explained above.
  -- We use **Event Handlers** to tailor the logic.
  -- 
  -- Here we place an Event Handler at the Guarded event. So when the **Guarded** event is triggered, then this method is called!
  -- With the variables **From**, **Event**, **To**. Each of these variables containing a string.
  -- 
  -- We check if the previous state wasn't Guarded also.
  -- If not, we retrieve the owning Coalition of the **ZoneCaptureCoalition**, using `self:GetCoalition()`.
  -- So **Coalition** will contain the current owning coalition of the zone.
  -- 
  -- Depending on the zone ownership, different messages are sent.
  -- Note the methods `ZoneCaptureCoalition:GetZoneName()`.
  --      
  --      -- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION self
  --      function ZoneCaptureCoalition:OnEnterGuarded( From, Event, To )
  --        if From ~= To then
  --          local Coalition = self:GetCoalition()
  --          self:E( { Coalition = Coalition } )
  --          if Coalition == coalition.side.BLUE then
  --            ZoneCaptureCoalition:Smoke( SMOKECOLOR.Blue )
  --            US_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --            RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of the USA", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --          else
  --            ZoneCaptureCoalition:Smoke( SMOKECOLOR.Red )
  --            RU_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --            US_CC:MessageTypeToCoalition( string.format( "%s is under protection of Russia", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --          end
  --        end
  --      end
  -- 
  -- As you can see, not a rocket science.
  -- Next is the Event Handler when the **Empty** state transition is triggered.
  -- Now we smoke the ZoneCaptureCoalition with a green color, using `self:Smoke( SMOKECOLOR.Green )`.
  --      
  --      -- @param Functional.Protect#ZONE_CAPTURE_COALITION self
  --      function ZoneCaptureCoalition:OnEnterEmpty()
  --        self:Smoke( SMOKECOLOR.Green )
  --        US_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --        RU_CC:MessageTypeToCoalition( string.format( "%s is unprotected, and can be captured!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --      end
  -- 
  -- The next Event Handlers speak for itself.
  -- When the zone is Attacked, we smoke the zone white and send some messages to each coalition.     
  --      
  --      -- @param Functional.Protect#ZONE_CAPTURE_COALITION self
  --      function ZoneCaptureCoalition:OnEnterAttacked()
  --        ZoneCaptureCoalition:Smoke( SMOKECOLOR.White )
  --        local Coalition = self:GetCoalition()
  --        self:E({Coalition = Coalition})
  --        if Coalition == coalition.side.BLUE then
  --          US_CC:MessageTypeToCoalition( string.format( "%s is under attack by Russia", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --          RU_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --        else
  --          RU_CC:MessageTypeToCoalition( string.format( "%s is under attack by the USA", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --          US_CC:MessageTypeToCoalition( string.format( "We are attacking %s", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --        end
  --      end
  -- 
  -- When the zone is Captured, we send some victory or loss messages to the correct coalition.
  -- And we add some score.
  -- 
  --      -- @param Functional.Protect#ZONE_CAPTURE_COALITION self
  --      function ZoneCaptureCoalition:OnEnterCaptured()
  --        local Coalition = self:GetCoalition()
  --        self:E({Coalition = Coalition})
  --        if Coalition == coalition.side.BLUE then
  --          RU_CC:MessageTypeToCoalition( string.format( "%s is captured by the USA, we lost it!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --          US_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --        else
  --          US_CC:MessageTypeToCoalition( string.format( "%s is captured by Russia, we lost it!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --          RU_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --        end
  --        
  --        self:__Guard( 30 )
  --      end
  -- 
  -- And this call is the most important of all!
  -- In the context of the mission, we need to start the zone capture monitoring process.
  -- Or nothing will be monitored and the zone won't change states.
  -- We start the monitoring after 5 seconds, and will repeat every 30 seconds a check.
  --        
  --      ZoneCaptureCoalition:Start( 5, 30 )
  --      
  -- 
  -- @field #ZONE_CAPTURE_COALITION
  ZONE_CAPTURE_COALITION = {
    ClassName         = "ZONE_CAPTURE_COALITION",
    MarkBlue          = nil,
    MarkRed           = nil,
    StartInterval     = nil,
    RepeatInterval    = nil,
    HitsOn            = nil,
    HitTimeLast       = nil,
    HitTimeAttackOver = nil,
    MarkOn            = nil,
  }

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor and Start/Stop Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  --- ZONE_CAPTURE_COALITION Constructor.
  -- @param #ZONE_CAPTURE_COALITION self
  -- @param Core.Zone#ZONE Zone A @{Core.Zone} object with the goal to be achieved. Alternatively, can be handed as the name of late activated group describing a @{Core.Zone#ZONE_POLYGON} with its waypoints.
  -- @param #number Coalition The initial coalition owning the zone.
  -- @param #table UnitCategories Table of unit categories. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit). Default {Unit.Category.GROUND_UNIT}.
  -- @param #table ObjectCategories Table of unit categories. See [DCS Class Object](https://wiki.hoggitworld.com/view/DCS_Class_Object). Default {Object.Category.UNIT, Object.Category.STATIC}, i.e. all UNITS and STATICS.
  -- @return #ZONE_CAPTURE_COALITION
  -- @usage
  -- 
  --  AttackZone = ZONE:New( "AttackZone" )
  --
  --  ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( AttackZone, coalition.side.RED, {UNITS ) -- Create a new ZONE_CAPTURE_COALITION object of zone AttackZone with ownership RED coalition.
  --  ZoneCaptureCoalition:__Guard( 1 ) -- Start the Guarding of the AttackZone.
  --  
  function ZONE_CAPTURE_COALITION:New( Zone, Coalition, UnitCategories, ObjectCategories )
  
    local self = BASE:Inherit( self, ZONE_GOAL_COALITION:New( Zone, Coalition, UnitCategories ) ) -- #ZONE_CAPTURE_COALITION
    self:F( { Zone = Zone, Coalition  = Coalition, UnitCategories = UnitCategories, ObjectCategories = ObjectCategories } )
    
    self:SetObjectCategories(ObjectCategories)
    
    -- Default is no smoke.
    self:SetSmokeZone(false)
    -- Default is F10 marks ON.
    self:SetMarkZone(true)
    -- Start in state "Empty".
    self:SetStartState("Empty")

    do 
    
      --- Captured State Handler OnLeave for ZONE_CAPTURE_COALITION
      -- @function [parent=#ZONE_CAPTURE_COALITION] OnLeaveCaptured
      -- @param #ZONE_CAPTURE_COALITION self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
      -- @return #boolean
  
      --- Captured State Handler OnEnter for ZONE_CAPTURE_COALITION
      -- @function [parent=#ZONE_CAPTURE_COALITION] OnEnterCaptured
      -- @param #ZONE_CAPTURE_COALITION self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
  
    end
  
  
    do 
    
      --- Attacked State Handler OnLeave for ZONE_CAPTURE_COALITION
      -- @function [parent=#ZONE_CAPTURE_COALITION] OnLeaveAttacked
      -- @param #ZONE_CAPTURE_COALITION self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
      -- @return #boolean
  
      --- Attacked State Handler OnEnter for ZONE_CAPTURE_COALITION
      -- @function [parent=#ZONE_CAPTURE_COALITION] OnEnterAttacked
      -- @param #ZONE_CAPTURE_COALITION self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
  
    end

    do 
    
      --- Guarded State Handler OnLeave for ZONE_CAPTURE_COALITION
      -- @function [parent=#ZONE_CAPTURE_COALITION] OnLeaveGuarded
      -- @param #ZONE_CAPTURE_COALITION self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
      -- @return #boolean
  
      --- Guarded State Handler OnEnter for ZONE_CAPTURE_COALITION
      -- @function [parent=#ZONE_CAPTURE_COALITION] OnEnterGuarded
      -- @param #ZONE_CAPTURE_COALITION self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
  
    end
  

    do 
    
      --- Empty State Handler OnLeave for ZONE_CAPTURE_COALITION
      -- @function [parent=#ZONE_CAPTURE_COALITION] OnLeaveEmpty
      -- @param #ZONE_CAPTURE_COALITION self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
      -- @return #boolean
  
      --- Empty State Handler OnEnter for ZONE_CAPTURE_COALITION
      -- @function [parent=#ZONE_CAPTURE_COALITION] OnEnterEmpty
      -- @param #ZONE_CAPTURE_COALITION self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
  
    end
  
    self:AddTransition( "*", "Guard", "Guarded" )
    
    --- Guard Handler OnBefore for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeGuard
    -- @param #ZONE_CAPTURE_COALITION self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Guard Handler OnAfter for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterGuard
    -- @param #ZONE_CAPTURE_COALITION self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Guard Trigger for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] Guard
    -- @param #ZONE_CAPTURE_COALITION self
    
    --- Guard Asynchronous Trigger for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] __Guard
    -- @param #ZONE_CAPTURE_COALITION self
    -- @param #number Delay
    
    self:AddTransition( "*", "Empty", "Empty" )
    
    --- Empty Handler OnBefore for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeEmpty
    -- @param #ZONE_CAPTURE_COALITION self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Empty Handler OnAfter for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterEmpty
    -- @param #ZONE_CAPTURE_COALITION self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Empty Trigger for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] Empty
    -- @param #ZONE_CAPTURE_COALITION self
    
    --- Empty Asynchronous Trigger for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] __Empty
    -- @param #ZONE_CAPTURE_COALITION self
    -- @param #number Delay
    
    
    self:AddTransition( {  "Guarded", "Empty" }, "Attack", "Attacked" )
  
    --- Attack Handler OnBefore for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeAttack
    -- @param #ZONE_CAPTURE_COALITION self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Attack Handler OnAfter for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterAttack
    -- @param #ZONE_CAPTURE_COALITION self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Attack Trigger for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] Attack
    -- @param #ZONE_CAPTURE_COALITION self
    
    --- Attack Asynchronous Trigger for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] __Attack
    -- @param #ZONE_CAPTURE_COALITION self
    -- @param #number Delay
    
    self:AddTransition( { "Guarded", "Attacked", "Empty" }, "Capture", "Captured" )
   
    --- Capture Handler OnBefore for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] OnBeforeCapture
    -- @param #ZONE_CAPTURE_COALITION self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Capture Handler OnAfter for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] OnAfterCapture
    -- @param #ZONE_CAPTURE_COALITION self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Capture Trigger for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] Capture
    -- @param #ZONE_CAPTURE_COALITION self
    
    --- Capture Asynchronous Trigger for ZONE_CAPTURE_COALITION
    -- @function [parent=#ZONE_CAPTURE_COALITION] __Capture
    -- @param #ZONE_CAPTURE_COALITION self
    -- @param #number Delay

    -- ZoneGoal objects are added to the _DATABASE.ZONES_GOAL and SET_ZONE_GOAL sets.
    _EVENTDISPATCHER:CreateEventNewZoneGoal(self)

    return self
  end


  --- Starts the zone capturing monitoring process.
  -- This process can be CPU intensive, ensure that you specify reasonable time intervals for the monitoring process.
  -- Note that the monitoring process is NOT started automatically during the `:New()` constructor.
  -- It is advised that the zone monitoring process is only started when the monitoring is of relevance in context of the current mission goals.
  -- When the zone is of no relevance, it is advised NOT to start the monitoring process, or to stop the monitoring process to save CPU resources.
  -- Therefore, the mission designer will need to use the `:Start()` method within his script to start the monitoring process specifically.  
  -- @param #ZONE_CAPTURE_COALITION self
  -- @param #number StartInterval (optional) Specifies the start time interval in seconds when the zone state will be checked for the first time.
  -- @param #number RepeatInterval (optional) Specifies the repeat time interval in seconds when the zone state will be checked repeatedly.
  -- @return #ZONE_CAPTURE_COALITION self
  -- @usage
  -- 
  -- -- Setup the zone.
  -- CaptureZone = ZONE:New( "CaptureZone" )
  -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( CaptureZone, coalition.side.RED )
  --
  -- -- This starts the monitoring process within 15 seconds, repeating every 15 seconds.
  -- ZoneCaptureCoalition:Start() 
  --      
  -- -- This starts the monitoring process immediately, but repeats every 30 seconds.
  -- ZoneCaptureCoalition:Start( 0, 30 ) 
  -- 
  function ZONE_CAPTURE_COALITION:Start( StartInterval, RepeatInterval )
  
    self.StartInterval = StartInterval or 1
    self.RepeatInterval = RepeatInterval or 15
  
    if self.ScheduleStatusZone then
      self:ScheduleStop( self.ScheduleStatusZone )
    end
    
    -- Start Status scheduler.
    self.ScheduleStatusZone = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 0.1, nil, self.StatusZone, self )
    
    -- We check if a unit within the zone is hit. If it is, then we must move the zone to attack state.
    self:HandleEvent(EVENTS.Hit, self.OnEventHit)
    
    -- Create mark on F10 map.
    self:Mark()
    
    return self
  end
  

  --- Stops the zone capturing monitoring process.
  -- When the zone capturing monitor process is stopped, there won't be any changes anymore in the state and the owning coalition of the zone.
  -- This method becomes really useful when the zone is of no relevance anymore within a long lasting mission.
  -- In this case, it is advised to stop the monitoring process, not to consume unnecessary the CPU intensive scanning of units presence within the zone.
  -- @param #ZONE_CAPTURE_COALITION self
  -- @usage 
  -- -- Setup the zone.
  -- CaptureZone = ZONE:New( "CaptureZone" )
  -- ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( CaptureZone, coalition.side.RED )
  --
  -- -- This starts the monitoring process within 15 seconds, repeating every 15 seconds.
  -- ZoneCaptureCoalition:Start() 
  -- 
  -- -- When the zone capturing is of no relevance anymore, stop the monitoring!
  -- ZoneCaptureCoalition:Stop()
  -- 
  -- @usage
  -- -- For example, one could stop the monitoring when the zone was captured!
  -- -- @param Functional.Protect#ZONE_CAPTURE_COALITION self
  -- function ZoneCaptureCoalition:OnEnterCaptured()
  --   local Coalition = self:GetCoalition()
  --   self:E({Coalition = Coalition})
  --   if Coalition == coalition.side.BLUE then
  --     RU_CC:MessageTypeToCoalition( string.format( "%s is captured by the USA, we lost it!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --     US_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --   else
  --     US_CC:MessageTypeToCoalition( string.format( "%s is captured by Russia, we lost it!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --     RU_CC:MessageTypeToCoalition( string.format( "We captured %s, Excellent job!", ZoneCaptureCoalition:GetZoneName() ), MESSAGE.Type.Information )
  --   end
  --  
  --   self:AddScore( "Captured", "Zone captured: Extra points granted.", 200 )    
  --  
  --   self:Stop()
  -- end
  -- 
  function ZONE_CAPTURE_COALITION:Stop()
  
    if self.ScheduleStatusZone then
      self:ScheduleStop(self.ScheduleStatusZone)
    end
    
    if self.SmokeScheduler then
      self:ScheduleStop(self.SmokeScheduler)
    end
    
    self:UnHandleEvent(EVENTS.Hit)
    
  end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User API Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  --- Set whether hit events of defending units are monitored and trigger "Attack" events.
  -- @param #ZONE_CAPTURE_COALITION self
  -- @param #boolean Switch If *true*, hit events are monitored. If *false* or *nil*, hit events are not monitored.
  -- @param #number TimeAttackOver (Optional) Time in seconds after an attack is over after the last hit and the zone state goes to "Guarded". Default is 300 sec = 5 min.
  -- @return #ZONE_CAPTURE_COALITION self
  function ZONE_CAPTURE_COALITION:SetMonitorHits(Switch, TimeAttackOver)
    self.HitsOn=Switch
    self.HitTimeAttackOver=TimeAttackOver or 5*60
    return self
  end

  --- Set whether marks on the F10 map are shown, which display the current zone status.
  -- @param #ZONE_CAPTURE_COALITION self
  -- @param #boolean Switch If *true* or *nil*, marks are shown. If *false*, marks are not displayed.
  -- @return #ZONE_CAPTURE_COALITION self
  function ZONE_CAPTURE_COALITION:SetMarkZone(Switch)
    if Switch==nil or Switch==true then
      self.MarkOn=true
    else
      self.MarkOn=false
    end
    return self
  end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- DCS Event Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  
  --- Monitor hit events.
  -- @param #ZONE_CAPTURE_COALITION self
  -- @param Core.Event#EVENTDATA EventData The event data.
  function ZONE_CAPTURE_COALITION:OnEventHit( EventData )
  
    if self.HitsOn then
  
      local UnitHit = EventData.TgtUnit
      
      if UnitHit and UnitHit.ClassName ~= "SCENERY" then
      -- Check if unit is inside the capture zone and that it is of the defending coalition.
          if UnitHit and UnitHit:IsInZone(self) and UnitHit:GetCoalition()==self.Coalition then
          
            -- Update last hit time.
            self.HitTimeLast=timer.getTime()
            
            -- Only trigger attacked event if not already in state "Attacked".
            if self:GetState()~="Attacked" then
              self:F2("Hit ==> Attack")
              self:Attack()
            end
            
          end
      end
    end
  
  end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Event Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  --- On after "Guard" event.
  -- @param #ZONE_CAPTURE_COALITION self
  function ZONE_CAPTURE_COALITION:onafterGuard()
    self:F2("After Guard")

    if self.SmokeZone and not self.SmokeScheduler then
      self.SmokeScheduler = self:ScheduleRepeat( self.StartInterval, self.RepeatInterval, 0.1, nil, self.StatusSmoke, self )
    end
    
  end
  
  --- On enter "Guarded" state.
  -- @param #ZONE_CAPTURE_COALITION self
  function ZONE_CAPTURE_COALITION:onenterGuarded()
    self:F2("Enter Guarded")
    self:Mark()
  end

  --- On enter "Captured" state.
  -- @param #ZONE_CAPTURE_COALITION self  
  function ZONE_CAPTURE_COALITION:onenterCaptured()
    self:F2("Enter Captured")

    -- Get new coalition.
    local NewCoalition = self:GetScannedCoalition()
    self:F( { NewCoalition = NewCoalition } )
    
    -- Set new owner of zone.
    self:SetCoalition(NewCoalition)
  
    -- Update mark.
    self:Mark()
    
    -- Goal achieved.
    self.Goal:Achieved()
  end
    
  --- On enter "Empty" state.
  -- @param #ZONE_CAPTURE_COALITION self    
  function ZONE_CAPTURE_COALITION:onenterEmpty()
    self:F2("Enter Empty")
    self:Mark()
  end
  
  --- On enter "Attacked" state.
  -- @param #ZONE_CAPTURE_COALITION self    
  function ZONE_CAPTURE_COALITION:onenterAttacked()
    self:F2("Enter Attacked")
    self:Mark()
  end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Status Check Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  --- Check if zone is "Empty".
  -- @param #ZONE_CAPTURE_COALITION self
  -- @return #boolean self:IsNoneInZone()
  function ZONE_CAPTURE_COALITION:IsEmpty()
  
    local IsEmpty = self:IsNoneInZone()
    self:F( { IsEmpty = IsEmpty } )
    
    return IsEmpty
  end

  --- Check if zone is "Guarded", i.e. only one (the defending) coalition is present inside the zone.
  -- @param #ZONE_CAPTURE_COALITION self
  -- @return #boolean self:IsAllInZoneOfCoalition( self.Coalition )
  function ZONE_CAPTURE_COALITION:IsGuarded()
  
    local IsGuarded = self:IsAllInZoneOfCoalition( self.Coalition )
    self:F( { IsGuarded = IsGuarded } )
    
    return IsGuarded
  end

  --- Check if zone is "Captured", i.e. another coalition took control over the zone and is the only one present.
  -- @param #ZONE_CAPTURE_COALITION self
  -- @return #boolean self:IsAllInZoneOfOtherCoalition( self.Coalition )
  function ZONE_CAPTURE_COALITION:IsCaptured()
  
    local IsCaptured = self:IsAllInZoneOfOtherCoalition( self.Coalition )
    self:F( { IsCaptured = IsCaptured } )
    
    return IsCaptured
  end
  
  --- Check if zone is "Attacked", i.e. another coalition entered the zone.
  -- @param #ZONE_CAPTURE_COALITION self
  -- @return #boolean self:IsSomeInZoneOfCoalition( self.Coalition )
  function ZONE_CAPTURE_COALITION:IsAttacked()
  
    local IsAttacked = self:IsSomeInZoneOfCoalition( self.Coalition )
    self:F( { IsAttacked = IsAttacked } )
    
    return IsAttacked
  end

  
  --- Check status Coalition ownership.
  -- @param #ZONE_CAPTURE_COALITION self
  function ZONE_CAPTURE_COALITION:StatusZone()
  
    -- Get FSM state.  
    local State = self:GetState()
  
    -- Scan zone in parent class ZONE_GOAL_COALITION
    self:GetParent( self, ZONE_CAPTURE_COALITION ).StatusZone( self )
    
    local Tnow=timer.getTime()
    
    -- Check if zone is guarded.
    if State ~= "Guarded" and self:IsGuarded() then
      
      -- Check that there was a sufficient amount of time after the last hit before going back to "Guarded".
      if self.HitTimeLast==nil or Tnow>=self.HitTimeLast+self.HitTimeAttackOver then
        self:Guard()
        self.HitTimeLast=nil
      end
    end
    
    -- Check if zone is empty.
    if State ~= "Empty" and self:IsEmpty() then  
      self:Empty()
    end

    -- Check if zone is attacked.
    if State ~= "Attacked" and self:IsAttacked() then
      self:Attack()
    end
    
    -- Check if zone is captured.
    if State ~= "Captured" and self:IsCaptured() then  
      self:Capture()
    end
    
    -- Count stuff in zone.
    local unitset=self:GetScannedSetUnit()    
    local nRed=0
    local nBlue=0
    for _,object in pairs(unitset:GetSet()) do
      local coal=object:GetCoalition()
      if object:IsAlive() then          
        if coal==coalition.side.RED then
          nRed=nRed+1
        elseif coal==coalition.side.BLUE then
          nBlue=nBlue+1
        end
      end
    end
    
    -- Status text.
    if false then
      local text=string.format("CAPTURE ZONE %s: Owner=%s (Previous=%s): #blue=%d, #red=%d, Status %s", self:GetZoneName(), self:GetCoalitionName(), UTILS.GetCoalitionName(self:GetPreviousCoalition()), nBlue, nRed, State)
      local NewState = self:GetState()
      if NewState~=State then
        text=text..string.format(" --> %s", NewState)
      end
      self:I(text)
    end

  end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  --- Update Mark on F10 map.
  -- @param #ZONE_CAPTURE_COALITION self
  function ZONE_CAPTURE_COALITION:Mark()

    if self.MarkOn then

      local Coord    = self:GetCoordinate()
      local ZoneName = self:GetZoneName()
      local State    = self:GetState()

      -- Remove marks.
      if self.MarkRed then
        Coord:RemoveMark(self.MarkRed)
      end
      if self.MarkBlue then
        Coord:RemoveMark(self.MarkBlue)
      end

      -- Create new marks for each coalition.
      if self.Coalition == coalition.side.BLUE then
        self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Blue\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State )
        self.MarkRed  = Coord:MarkToCoalitionRed(  "Coalition: Blue\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State )
      elseif self.Coalition == coalition.side.RED then
        self.MarkRed  = Coord:MarkToCoalitionRed(  "Coalition: Red\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State )
        self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Red\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State )
      else
        self.MarkRed  = Coord:MarkToCoalitionRed(  "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State )
        self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Neutral\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State )
      end

    end

  end

end
--- **Functional** - Control artillery units.
--
-- ===
--
-- The ARTY class can be used to easily assign and manage targets for artillery units using an advanced queueing system.
--
-- ## Features:
--
--   * Multiple targets can be assigned. No restriction on number of targets.
--   * Targets can be given a priority. Engagement of targets is executed a according to their priority.
--   * Engagements can be scheduled, i.e. will be executed at a certain time of the day.
--   * Multiple relocations of the group can be assigned and scheduled via queueing system.
--   * Special weapon types can be selected for each attack, e.g. cruise missiles for Naval units.
--   * Automatic rearming once the artillery is out of ammo (optional).
--   * Automatic relocation after each firing engagement to prevent counter strikes (optional).
--   * Automatic relocation movements to get the battery within firing range (optional).
--   * Simulation of tactical nuclear shells as well as illumination and smoke shells.
--   * New targets can be added during the mission, e.g. when they are detected by recon units.
--   * Targets and relocations can be assigned by placing markers on the F10 map.
--   * Finite state machine implementation. Mission designer can interact when certain events occur.
--
-- ====
--
-- ## [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg)
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ### Contributions: FlightControl
--
-- ====
-- @module Functional.Artillery
-- @image Artillery.JPG

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- ARTY class
-- @type ARTY
-- @field #string ClassName Name of the class.
-- @field #string lid Log id for DCS.log file.
-- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players.
-- @field #table targets All targets assigned.
-- @field #table moves All moves assigned.
-- @field #ARTY.Target currentTarget Holds the current target, if there is one assigned.
-- @field #table currentMove Holds the current commanded move, if there is one assigned.
-- @field #number Nammo0 Initial amount total ammunition (shells+rockets+missiles) of the whole group.
-- @field #number Nshells0 Initial amount of shells of the whole group.
-- @field #number Narty0 Initial amount of artillery shells of the whole group.
-- @field #number Nrockets0 Initial amount of rockets of the whole group.
-- @field #number Nmissiles0 Initial amount of missiles of the whole group.
-- @field #number Nukes0 Initial amount of tactical nukes of the whole group. Default is 0.
-- @field #number Nillu0 Initial amount of illumination shells of the whole group. Default is 0.
-- @field #number Nsmoke0 Initial amount of smoke shells of the whole group. Default is 0.
-- @field #number StatusInterval Update interval in seconds between status updates. Default 10 seconds.
-- @field #number WaitForShotTime Max time in seconds to wait until fist shot event occurs after target is assigned. If time is passed without shot, the target is deleted. Default is 300 seconds.
-- @field #table DCSdesc DCS descriptors of the ARTY group.
-- @field #string Type Type of the ARTY group.
-- @field #string DisplayName Extended type name of the ARTY group.
-- @field #number IniGroupStrength Inital number of units in the ARTY group.
-- @field #boolean IsArtillery If true, ARTY group has attribute "Artillery". This is automatically derived from the DCS descriptor table.
-- @field #boolean ismobile If true, ARTY group can move.
-- @field #boolean iscargo If true, ARTY group is defined as possible cargo. If it is immobile, targets out of range are not deleted from the queue.
-- @field Cargo.CargoGroup#CARGO_GROUP cargogroup Cargo group object if ARTY group is a cargo that will be transported to another place.
-- @field #string groupname Name of the ARTY group as defined in the mission editor.
-- @field #string alias Name of the ARTY group.
-- @field #table clusters Table of names of clusters the group belongs to. Can be used to address all groups within the cluster simultaniously.
-- @field #number SpeedMax Maximum speed of ARTY group in km/h. This is determined from the DCS descriptor table.
-- @field #number Speed Default speed in km/h the ARTY group moves at. Maximum speed possible is 80% of maximum speed the group can do.
-- @field #number RearmingDistance Safe distance in meters between ARTY group and rearming group or place at which rearming is possible. Default 100 m.
-- @field Wrapper.Group#GROUP RearmingGroup Unit designated to rearm the ARTY group.
-- @field #number RearmingGroupSpeed Speed in km/h the rearming unit moves at. Default is 50% of the max speed possible of the group.
-- @field #boolean RearmingGroupOnRoad If true, rearming group will move to ARTY group or rearming place using mainly roads. Default false.
-- @field Core.Point#COORDINATE RearmingGroupCoord Initial coordinates of the rearming unit. After rearming complete, the unit will return to this position.
-- @field Core.Point#COORDINATE RearmingPlaceCoord Coordinates of the rearming place. If the place is more than 100 m away from the ARTY group, the group will go there.
-- @field #boolean RearmingArtyOnRoad If true, ARTY group will move to rearming place using mainly roads. Default false.
-- @field Core.Point#COORDINATE InitialCoord Initial coordinates of the ARTY group.
-- @field #boolean report Arty group sends messages about their current state or target to its coalition.
-- @field #table ammoshells Table holding names of the shell types which are included when counting the ammo. Default is {"weapons.shells"} which include most shells.
-- @field #table ammorockets Table holding names of the rocket types which are included when counting the ammo. Default is {"weapons.nurs"} which includes most unguided rockets.
-- @field #table ammomissiles Table holding names of the missile types which are included when counting the ammo. Default is {"weapons.missiles"} which includes some guided missiles.
-- @field #number Nshots Number of shots fired on current target.
-- @field #number minrange Minimum firing range in kilometers. Targets closer than this distance are not engaged. Default 0.1 km.
-- @field #number maxrange Maximum firing range in kilometers. Targets further away than this distance are not engaged. Default 10000 km.
-- @field #number nukewarhead Explosion strength of tactical nuclear warhead in kg TNT. Default 75000.
-- @field #number Nukes Number of nuclear shells, the group has available. Note that if normal shells are empty, firing nukes is also not possible any more.
-- @field #number Nillu Number of illumination shells the group has available. Note that if normal shells are empty, firing illumination shells is also not possible any more.
-- @field #number illuPower Power of illumination warhead in mega candela. Default 1 mcd.
-- @field #number illuMinalt Minimum altitude in meters the illumination warhead will detonate.
-- @field #number illuMaxalt Maximum altitude in meters the illumination warhead will detonate.
-- @field #number Nsmoke Number of smoke shells the group has available. Note that if normal shells are empty, firing smoke shells is also not possible any more.
-- @field Utilities.Utils#SMOKECOLOR Smoke color of smoke shells. Default SMOKECOLOR.red.
-- @field #number nukerange Demolition range of tactical nuclear explostions.
-- @field #boolean nukefire Ignite additional fires and smoke for nuclear explosions Default true.
-- @field #number nukefires Number of nuclear fires and subexplosions.
-- @field #boolean relocateafterfire Group will relocate after each firing task. Default false.
-- @field #number relocateRmin Minimum distance in meters the group will look for places to relocate.
-- @field #number relocateRmax Maximum distance in meters the group will look for places to relocate.
-- @field #boolean markallow If true, Players are allowed to assign targets and moves for ARTY group by placing markers on the F10 map. Default is false.
-- @field #number markkey Authorization key. Only player who know this key can assign targets and moves via markers on the F10 map. Default no authorization required.
-- @field #boolean markreadonly Marks for targets are readonly and cannot be removed by players. Default is false.
-- @field #boolean autorelocate ARTY group will automatically move to within the max/min firing range.
-- @field #number autorelocatemaxdist Max distance [m] the ARTY group will travel to get within firing range. Default 50000 m = 50 km.
-- @field #boolean autorelocateonroad ARTY group will use mainly road to automatically get within firing range. Default is false.
-- @field #number coalition The coalition of the arty group.
-- @field #boolean respawnafterdeath Respawn arty group after all units are dead.
-- @field #number respawndelay Respawn delay in seconds.
-- @field #number dtTrack Time interval in seconds for weapon tracking.
-- @extends Core.Fsm#FSM_CONTROLLABLE

--- Enables mission designers easily to assign targets for artillery units. Since the implementation is based on a Finite State Model (FSM), the mission designer can
-- interact with the process at certain events or states.
--
-- A new ARTY object can be created with the @{#ARTY.New}(*group*) constructor.
-- The parameter *group* has to be a MOOSE Group object and defines ARTY group.
--
-- The ARTY FSM process can be started by the @{#ARTY.Start}() command.
--
-- ## The ARTY Process
--
-- ![Process](..\Presentations\ARTY\ARTY_Process.png)
--
-- ### Blue Branch
-- After the FMS process is started the ARTY group will be in the state **CombatReady**. Once a target is assigned the **OpenFire** event will be triggered and the group starts
-- firing. At this point the group in in the state **Firing**.
-- When the defined number of shots has been fired on the current target the event **CeaseFire** is triggered. The group will stop firing and go back to the state **CombatReady**.
-- If another target is defined (or multiple engagements of the same target), the cycle starts anew.
--
-- ### Violet Branch
-- When the ARTY group runs out of ammunition, the event **Winchester** is triggered and the group enters the state **OutOfAmmo**.
-- In this state, the group is unable to engage further targets.
--
-- ### Red Branch
-- With the @{#ARTY.SetRearmingGroup}(*group*) command, a special group can be defined to rearm the ARTY group. If this unit has been assigned and the group has entered the state
-- **OutOfAmmo** the event **Rearm** is triggered followed by a transition to the state **Rearming**.
-- If the rearming group is less than 100 meters away from the ARTY group, the rearming process starts. If the rearming group is more than 100 meters away from the ARTY unit, the
-- rearming group is routed to a point 20 to 100 m from the ARTY group.
--
-- Once the rearming is complete, the **Rearmed** event is triggered and the group enters the state **CombatReady**. At this point targeted can be engaged again.
--
-- ### Green Branch
-- The ARTY group can be ordered to change its position via the @{#ARTY.AssignMoveCoord}() function as described below. When the group receives the command to move
-- the event **Move** is triggered and the state changes to **Moving**. When the unit arrives to its destination the event **Arrived** is triggered and the group
-- becomes **CombatReady** again.
--
-- Note, that the ARTY group will not open fire while it is in state **Moving**. This property differentiates artillery from tanks.
--
-- ### Yellow Branch
-- When a new target is assigned via the @{#ARTY.AssignTargetCoord}() function (see below), the **NewTarget** event is triggered.
--
-- ## Assigning Targets
-- Assigning targets is a central point of the ARTY class. Multiple targets can be assigned simultaneously and are put into a queue.
-- Of course, targets can be added at any time during the mission. For example, once they are detected by a reconnaissance unit.
--
-- In order to add a target, the function @{#ARTY.AssignTargetCoord}(*coord*, *prio*, *radius*, *nshells*, *maxengage*, *time*, *weapontype*, *name*) has to be used.
-- Only the first parameter *coord* is mandatory while all remaining parameters are all optional.
--
-- ### Parameters:
--
-- * *coord*: Coordinates of the target, given as @{Core.Point#COORDINATE} object.
-- * *prio*: Priority of the target. This a number between 1 (high prio) and 100 (low prio). Targets with higher priority are engaged before targets with lower priority.
-- * *radius*: Radius in meters which defines the area the ARTY group will attempt to be hitting. Default is 100 meters.
-- * *nshells*: Number of shots (shells, rockets, missiles) fired by the group at each engagement of a target. Default is 5.
-- * *maxengage*: Number of times a target is engaged.
-- * *time*: Time of day the engagement is schedule in the format "hh:mm:ss" for hh=hours, mm=minutes, ss=seconds.
-- For example "10:15:35". In the case the attack will be executed at a quarter past ten in the morning at the day the mission started.
-- If the engagement should start on the following day the format can be specified as "10:15:35+1", where the +1 denotes the following day.
-- This is useful for longer running missions or if the mission starts at 23:00 hours and the attack should be scheduled at 01:00 hours on the following day.
-- Of course, later days are also possible by appending "+2", "+3", etc.
-- **Note** that the time has to be given as a string. So the enclosing quotation marks "" are important.
-- * *weapontype*: Specified the weapon type that should be used for this attack if the ARTY group has multiple weapons to engage the target.
-- For example, this is useful for naval units which carry a bigger arsenal (cannons and missiles). Default is Auto, i.e. DCS logic selects the appropriate weapon type.
-- *name*: A special name can be defined for this target. Default name are the coordinates of the target in LL DMS format. If a name is already given for another target
-- or the same target should be attacked two or more times with different parameters a suffix "#01", "#02", "#03" is automatically appended to the specified name.
--
-- ## Target Queue
-- In case multiple targets have been defined, it is important to understand how the target queue works.
--
-- Here, the essential parameters are the priority *prio*, the number of engagements *maxengage* and the scheduled *time* as described above.
--
-- For example, we have assigned two targets one with *prio*=10 and the other with *prio*=50 and both targets should be engaged three times (*maxengage*=3).
-- Let's first consider the case that none of the targets is scheduled to be executed at a certain time (*time*=nil).
-- The ARTY group will first engage the target with higher priority (*prio*=10). After the engagement is finished, the target with lower priority is attacked.
-- This is because the target with lower prio has been attacked one time less. After the attack on the lower priority task is finished and both targets
-- have been engaged equally often, the target with the higher priority is engaged again. This continues until a target has engaged three times.
-- Once the maximum number of engagements is reached, the target is deleted from the queue.
--
-- In other words, the queue is first sorted with respect to the number of engagements and targets with the same number of engagements are sorted with
-- respect to their priority.
--
-- ### Timed Engagements
--
-- As mentioned above, targets can be engaged at a specific time of the day via the *time* parameter.
--
-- If the *time* parameter is specified for a target, the first engagement of that target will happen at that time of the day and not before.
-- This also applies when multiple engagements are requested via the *maxengage* parameter. The first attack will not happen before the specified time.
-- When that timed attack is finished, the *time* parameter is deleted and the remaining engagements are carried out in the same manner as for untimed targets (described above).
--
-- Of course, it can happen that a scheduled task should be executed at a time, when another target is already under attack.
-- If the priority of the target is higher than the priority of the current target, then the current attack is cancelled and the engagement of the target with the higher
-- priority is started.
--
-- By contrast, if the current target has a higher priority than the target scheduled at that time, the current attack is finished before the scheduled attack is started.
--
-- ## Determining the Amount of Ammo
--
-- In order to determine when a unit is out of ammo and possible initiate the rearming process it is necessary to know which types of weapons have to be counted.
-- For most artillery unit types, this is simple because they only have one type of weapon and hence ammunition.
--
-- However, there are more complex scenarios. For example, naval units carry a big arsenal of different ammunition types ranging from various cannon shell types
-- over surface-to-air missiles to cruise missiles. Obviously, not all of these ammo types can be employed for artillery tasks.
--
-- Unfortunately, there is no easy way to count only those ammo types useable as artillery. Therefore, to keep the implementation general the user
-- can specify the names of the ammo types by the following functions:
--
-- * @{#ARTY.SetShellTypes}(*tableofnames*): Defines the ammo types for unguided cannons, e.g. *tableofnames*={"weapons.shells"}, i.e. **all** types of shells are counted.
-- * @{#ARTY.SetRocketTypes}(*tableofnames*): Defines the ammo types of unguided rockets, e.g. *tableofnames*={"weapons.nurs"}, i.e. **all** types of rockets are counted.
-- * @{#ARTY.SetMissileTypes}(*tableofnames*): Defines the ammo types of guided missiles, e.g. is *tableofnames*={"weapons.missiles"}, i.e. **all** types of missiles are counted.
--
-- **Note** that the default parameters "weapons.shells", "weapons.nurs", "weapons.missiles" **should in priciple** capture all the corresponding ammo types.
-- However, the logic searches for the string "weapon.missies" in the ammo type. Especially for missiles, this string is often not contained in the ammo type descriptor.
--
-- One way to determine which types of ammo the unit carries, one can use the debug mode of the arty class via @{#ARTY.SetDebugON}().
-- In debug mode, the all ammo types of the group are printed to the monitor as message and can be found in the DCS.log file.
--
-- ## Employing Selected Weapons
--
-- If an ARTY group carries multiple weapons, which can be used for artillery task, a certain weapon type can be selected to attack the target.
-- This is done via the *weapontype* parameter of the @{#ARTY.AssignTargetCoord}(..., *weapontype*, ...) function.
--
-- The enumerator @{#ARTY.WeaponType} has been defined to select a certain weapon type. Supported values are:
--
-- * @{#ARTY.WeaponType}.Auto: Automatic weapon selection by the DCS logic. This is the default setting.
-- * @{#ARTY.WeaponType}.Cannon: Only cannons are used during the attack. Corresponding ammo type are shells and can be defined by @{#ARTY.SetShellTypes}.
-- * @{#ARTY.WeaponType}.Rockets: Only unguided are used during the attack. Corresponding ammo type are rockets/nurs and can be defined by @{#ARTY.SetRocketTypes}.
-- * @{#ARTY.WeaponType}.CruiseMissile: Only cruise missiles are used during the attack. Corresponding ammo type are missiles and can be defined by @{#ARTY.SetMissileTypes}.
-- * @{#ARTY.WeaponType}.TacticalNukes: Use tactical nuclear shells. This works only with units that have shells and is described below.
-- * @{#ARTY.WeaponType}.IlluminationShells: Use illumination shells. This works only with units that have shells and is described below.
-- * @{#ARTY.WeaponType}.SmokeShells: Use smoke shells. This works only with units that have shells and is described below.
--
-- ## Assigning Relocation Movements
-- The ARTY group can be commanded to move. This is done by the @{#ARTY.AssignMoveCoord}(*coord*, *time*, *speed*, *onroad*, *cancel*, *name*) function.
-- With this multiple timed moves of the group can be scheduled easily. By default, these moves will only be executed if the group is state **CombatReady**.
--
-- ### Parameters
--
-- * *coord*: Coordinates where the group should move to given as @{Core.Point#COORDINATE} object.
-- * *time*: The time when the move should be executed. This has to be given as a string in the format "hh:mm:ss" (hh=hours, mm=minutes, ss=seconds).
-- * *speed*: Speed of the group in km/h.
-- * *onroad*: If this parameter is set to true, the group uses mainly roads to get to the commanded coordinates.
-- * *cancel*: If set to true, any current engagement of targets is cancelled at the time the move should be executed.
-- * *name*: Can be used to set a user defined name of the move. By default the name is created from the LL DMS coordinates.
--
-- ## Automatic Rearming
--
-- If an ARTY group runs out of ammunition, it can be rearmed automatically.
--
-- ### Rearming Group
-- The first way to activate the automatic rearming is to define a rearming group with the function @{#ARTY.SetRearmingGroup}(*group*). For the blue side, this
-- could be a M181 transport truck and for the red side an Ural-375 truck.
--
-- Once the ARTY group is out of ammo and the **Rearm** event is triggered, the defined rearming truck will drive to the ARTY group.
-- So the rearming truck does not have to be placed nearby the artillery group. When the rearming is complete, the rearming truck will drive back to its original position.
--
-- ### Rearming Place
-- The second alternative is to define a rearming place, e.g. a FRAP, airport or any other warehouse. This is done with the function @{#ARTY.SetRearmingPlace}(*coord*).
-- The parameter *coord* specifies the coordinate of the rearming place which should not be further away then 100 meters from the warehouse.
--
-- When the **Rearm** event is triggered, the ARTY group will move to the rearming place. Of course, the group must be mobil. So for a mortar this rearming procedure would not work.
--
-- After the rearming is complete, the ARTY group will move back to its original position and resume normal operations.
--
-- ### Rearming Group **and** Rearming Place
-- If both a rearming group *and* a rearming place are specified like described above, both the ARTY group and the rearming truck will move to the rearming place and meet there.
--
-- After the rearming is complete, both groups will move back to their original positions.
--
-- ## Simulated Weapons
--
-- In addition to the standard weapons a group has available some special weapon types that are not possible to use in the native DCS environment are simulated.
--
-- ### Tactical Nukes
--
-- ARTY groups that can fire shells can also be used to fire tactical nukes. This is achieved by setting the weapon type to **ARTY.WeaponType.TacticalNukes** in the
-- @{#ARTY.AssignTargetCoord}() function.
--
-- By default, they group does not have any nukes available. To give the group the ability the function @{#ARTY.SetTacNukeShells}(*n*) can be used.
-- This supplies the group with *n* nuclear shells, where *n* is restricted to the number of conventional shells the group can carry.
-- Note that the group must always have conventional shells left in order to fire a nuclear shell.
--
-- The default explosion strength is 0.075 kilo tons TNT. The can be changed with the @{#ARTY.SetTacNukeWarhead}(*strength*), where *strength* is given in kilo tons TNT.
--
-- ### Illumination Shells
--
-- ARTY groups that possess shells can fire shells with illumination bombs. First, the group needs to be equipped with this weapon. This is done by the
-- function @{#ARTY.SetIlluminationShells}(*n*, *power*), where *n* is the number of shells the group has available and *power* the illumination power in mega candela (mcd).
--
-- In order to execute an engagement with illumination shells one has to use the weapon type *ARTY.WeaponType.IlluminationShells* in the
-- @{#ARTY.AssignTargetCoord}() function.
--
-- In the simulation, the explosive shell that is fired is destroyed once it gets close to the target point but before it can actually impact.
-- At this position an illumination bomb is triggered at a random altitude between 500 and 1000 meters. This interval can be set by the function
-- @{#ARTY.SetIlluminationMinMaxAlt}(*minalt*, *maxalt*).
--
-- ### Smoke Shells
--
-- In a similar way to illumination shells, ARTY groups can also employ smoke shells. The number of smoke shells the group has available is set by the function
-- @{#ARTY.SetSmokeShells}(*n*, *color*), where *n* is the number of shells and *color* defines the smoke color. Default is SMOKECOLOR.Red.
--
-- The weapon type to be used in the @{#ARTY.AssignTargetCoord}() function is *ARTY.WeaponType.SmokeShells*.
--
-- The explosive shell the group fired is destroyed shortly before its impact on the ground and smoke of the specified color is triggered at that position.
--
--
-- ## Assignments via Markers on F10 Map
--
-- Targets and relocations can be assigned by players via placing a mark on the F10 map. The marker text must contain certain keywords.
--
-- This feature can be turned on with the @{#ARTY.SetMarkAssignmentsOn}(*key*, *readonly*). The parameter *key* is optional. When set, it can be used as PIN, i.e. only
-- players who know the correct key are able to assign and cancel targets or relocations. Default behavior is that all players belonging to the same coalition as the
-- ARTY group are able to assign targets and moves without a key.
--
-- ### Target Assignments
-- A new target can be assigned by writing **arty engage** in the marker text.
-- This is followed by a **comma separated list** of (optional) keywords and parameters.
-- First, it is important to address the ARTY group or groups that should engage. This can be done in numerous ways. The keywords are *battery*, *alias*, *cluster*.
-- It is also possible to address all ARTY groups by the keyword *everyone* or *allbatteries*. These two can be used synonymously.
-- **Note that**, if no battery is assigned nothing will happen.
--
-- * *everyone* or *allbatteries* The target is assigned to all batteries.
-- * *battery* Name of the ARTY group that the target is assigned to. Note that **the name is case sensitive** and has to be given in quotation marks. Default is all ARTY groups of the right coalition.
-- * *alias* Alias of the ARTY group that the target is assigned to. The alias is **case sensitive** and needs to be in quotation marks.
-- * *cluster* The cluster of ARTY groups that is addressed. Clusters can be defined by the function @{#ARTY.AddToCluster}(*clusters*). Names are **case sensitive** and need to be in quotation marks.
-- * *key* A number to authorize the target assignment. Only specifying the correct number will trigger an engagement.
-- * *time* Time for which which the engagement is schedules, e.g. 08:42. Default is as soon as possible.
-- * *prio*  Priority of the engagement as number between 1 (high prio) and 100 (low prio). Default is 50, i.e. medium priority.
-- * *shots* Number of shots (shells, rockets or missiles) fired at each engagement. Default is 5.
-- * *maxengage* Number of times the target is engaged. Default is 1.
-- * *radius* Scattering radius of the fired shots in meters. Default is 100 m.
-- * *weapon* Type of weapon to be used. Valid parameters are *cannon*, *rocket*, *missile*, *nuke*. Default is automatic selection.
-- * *lldms* Specify the coordinates in Lat/Long degrees, minutes and seconds format. The actual location of the marker is unimportant here. The group will engage the coordinates given in the lldms keyword.
-- Format is DD:MM:SS[N,S] DD:MM:SS[W,E]. See example below. This can be useful when coordinates in this format are obtained from elsewhere.
-- * *readonly* The marker is readonly and cannot be deleted by users. Hence, assignment cannot be cancelled by removing the marker.
--
-- Here are examples of valid marker texts:
--      arty engage, battery "Blue Paladin Alpha"
--      arty engage, everyone
--      arty engage, allbatteries
--      arty engage, alias "Bob", weapon missiles
--      arty engage, cluster "All Mortas"
--      arty engage, cluster "Northern Batteries" "Southern Batteries"
--      arty engage, cluster "Northern Batteries", cluster "Southern Batteries"
--      arty engage, cluster "Horwitzers", shots 20, prio 10, time 08:15, weapon cannons
--      arty engage, battery "Blue Paladin 1" "Blue MRLS 1", shots 10, time 10:15
--      arty engage, battery "Blue MRLS 1", key 666
--      arty engage, battery "Paladin Alpha", weapon nukes, shots 1, time 20:15
--      arty engage, battery "Horwitzer 1", lldms 41:51:00N 41:47:58E
--
-- Note that the keywords and parameters are *case insensitive*. Only exception are the battery, alias and cluster names.
-- These must be exactly the same as the names of the groups defined in the mission editor or the aliases and cluster names defined in the script.
--
-- ### Relocation Assignments
--
-- Markers can also be used to relocate the group with the keyphrase **arty move**. This is done in a similar way as assigning targets. Here, the (optional) keywords and parameters are:
--
-- * *time* Time for which which the relocation/move is schedules, e.g. 08:42. Default is as soon as possible.
-- * *speed* The speed in km/h the group will drive at. Default is 70% of its max possible speed.
-- * *on road* Group will use mainly roads. Default is off, i.e. it will go in a straight line from its current position to the assigned coordinate.
-- * *canceltarget* Group will cancel all running firing engagements and immediately start to move. Default is that group will wait until is current assignment is over.
-- * *battery* Name of the ARTY group that the relocation is assigned to.
-- * *alias* Alias of the ARTY group that the target is assigned to. The alias is **case sensitive** and needs to be in quotation marks.
-- * *cluster* The cluster of ARTY groups that is addressed. Clusters can be defined by the function @{#ARTY.AddToCluster}(*clusters*). Names are **case sensitive** and need to be in quotation marks.
-- * *key* A number to authorize the target assignment. Only specifying the correct number will trigger an engagement.
-- * *lldms* Specify the coordinates in Lat/Long degrees, minutes and seconds format. The actual location of the marker is unimportant. The group will move to the coordinates given in the lldms keyword.
-- Format is DD:MM:SS[N,S] DD:MM:SS[W,E]. See example below.
-- * *readonly* Marker cannot be deleted by users any more. Hence, assignment cannot be cancelled by removing the marker.
--
-- Here are some examples:
--      arty move, battery "Blue Paladin"
--      arty move, battery "Blue MRLS", canceltarget, speed 10, on road
--      arty move, cluster "mobile", lldms 41:51:00N 41:47:58E
--      arty move, alias "Bob", weapon missiles
--      arty move, cluster "All Howitzer"
--      arty move, cluster "Northern Batteries" "Southern Batteries"
--      arty move, cluster "Northern Batteries", cluster "Southern Batteries"
--      arty move, everyone
--
-- ### Requests
--
-- Marks can also be to send requests to the ARTY group. This is done by the keyword **arty request**, which can have the keywords
--
-- * *target* All assigned targets are reported.
-- * *move* All assigned relocation moves are reported.
-- * *ammo* Current ammunition status is reported.
--
-- For example
--      arty request, everyone, ammo
--      arty request, battery "Paladin Bravo", targets
--      arty request, cluster "All Mortars", move
--
-- The actual location of the marker is irrelevant for these requests.
--
-- ### Cancel
--
-- Current actions can be cancelled by the keyword **arty cancel**. Actions that can be cancelled are current engagements, relocations and rearming assignments.
--
-- For example
--      arty cancel, target, battery "Paladin Bravo"
--      arty cancel, everyone, move
--      arty cancel, rearming, battery "MRLS Charly"
--
-- ### Settings
--
-- A few options can be set by marks. The corresponding keyword is **arty set**. This can be used to define the rearming place and group for a battery.
--
-- To set the rearming place of a group at the marker position type
--      arty set, battery "Paladin Alpha", rearming place
--
-- Setting the rearming group is independent of the position of the mark. Just create one anywhere on the map and type
--      arty set, battery "Mortar Bravo", rearming group "Ammo Truck M939"
-- Note that the name of the rearming group has to be given in quotation marks and spelt exactly as the group name defined in the mission editor.
--
-- ## Transporting
--
-- ARTY groups can be transported to another location as @{Cargo.Cargo} by means of classes such as @{AI.AI_Cargo_APC}, @{AI.AI_Cargo_Dispatcher_APC},
-- @{AI.AI_Cargo_Helicopter}, @{AI.AI_Cargo_Dispatcher_Helicopter} or @{AI.AI_Cargo_Airplane}.
--
-- In order to do this, one needs to define an ARTY object via the @{#ARTY.NewFromCargoGroup}(*cargogroup*, *alias*) function.
-- The first argument *cargogroup* has to be a @{Cargo.CargoGroup#CARGO_GROUP} object. The second argument *alias* is a string which can be freely chosen by the user.
--
-- ## Fine Tuning
--
-- The mission designer has a few options to tailor the ARTY object according to his needs.
--
-- * @{#ARTY.SetAutoRelocateToFiringRange}(*maxdist*, *onroad*) lets the ARTY group automatically move to within firing range if a current target is outside the min/max firing range. The
-- optional parameter *maxdist* is the maximum distance im km the group will move. If the distance is greater no relocation is performed. Default is 50 km.
-- * @{#ARTY.SetAutoRelocateAfterEngagement}(*rmax*, *rmin*) will cause the ARTY group to change its position after each firing assignment.
-- Optional parameters *rmax*, *rmin* define the max/min distance for relocation of the group. Default distance is randomly between 300 and 800 m.
-- * @{#ARTY.AddToCluster}(*clusters*) Can be used to add the ARTY group to one or more clusters. All groups in a cluster can be addressed simultaniously with one marker command.
-- * @{#ARTY.SetSpeed}(*speed*) sets the speed in km/h the group moves at if not explicitly stated otherwise.
-- * @{#ARTY.RemoveAllTargets}() removes all targets from the target queue.
-- * @{#ARTY.RemoveTarget}(*name*) deletes the target with *name* from the target queue.
-- * @{#ARTY.SetMaxFiringRange}(*range*) defines the maximum firing range. Targets further away than this distance are not engaged.
-- * @{#ARTY.SetMinFiringRange}(*range*) defines the minimum firing range. Targets closer than this distance are not engaged.
-- * @{#ARTY.SetRearmingGroup}(*group*) sets the group responsible for rearming of the ARTY group once it is out of ammo.
-- * @{#ARTY.SetReportON}() and @{#ARTY.SetReportOFF}() can be used to enable/disable status reports of the ARTY group send to all coalition members.
-- * @{#ARTY.SetWaitForShotTime}(*waittime*) sets the time after which a target is deleted from the queue if no shooting event occured after the target engagement started.
-- Default is 300 seconds. Note that this can for example happen, when the assigned target is out of range.
-- *  @{#ARTY.SetDebugON}() and @{#ARTY.SetDebugOFF}() can be used to enable/disable the debug mode.
--
-- ## Examples
--
-- ### Assigning Multiple Targets
-- This basic example illustrates how to assign multiple targets and defining a rearming group.
--     -- Creat a new ARTY object from a Paladin group.
--     paladin=ARTY:New(GROUP:FindByName("Blue Paladin"))
--
--     -- Define a rearming group. This is a Transport M939 truck.
--     paladin:SetRearmingGroup(GROUP:FindByName("Blue Ammo Truck"))
--
--     -- Set the max firing range. A Paladin unit has a range of 20 km.
--     paladin:SetMaxFiringRange(20)
--
--     -- Low priorty (90) target, will be engage last. Target is engaged two times. At each engagement five shots are fired.
--     paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 3"):GetCoordinate(),  90, nil,  5, 2)
--     -- Medium priorty (nil=50) target, will be engage second. Target is engaged two times. At each engagement ten shots are fired.
--     paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), nil, nil, 10, 2)
--     -- High priorty (10) target, will be engage first. Target is engaged three times. At each engagement twenty shots are fired.
--     paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 2"):GetCoordinate(),  10, nil, 20, 3)
--
--     -- Start ARTY process.
--     paladin:Start()
-- **Note**
--
-- * If a parameter should be set to its default value, it has to be set to *nil* if other non-default parameters follow. Parameters at the end can simply be skiped.
-- * In this example, the target coordinates are taken from groups placed in the mission edit using the COORDINATE:GetCoordinate() function.
--
-- ### Scheduled Engagements
--     -- Mission starts at 8 o'clock.
--     -- Assign two scheduled targets.
--
--     -- Create ARTY object from Paladin group.
--     paladin=ARTY:New(GROUP:FindByName("Blue Paladin"))
--
--     -- Assign target coordinates. Priority=50 (medium), radius=100 m, use 5 shells per engagement, engage 1 time at two past 8 o'clock.
--     paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 50, 100,  5, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1")
--
--     -- Assign target coordinates. Priority=10 (high), radius=300 m, use 10 shells per engagement, engage 1 time at seven past 8 o'clock.
--     paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 2"):GetCoordinate(), 10, 300, 10, 1, "08:07:00", ARTY.WeaponType.Auto, "Target 2")
--
--     -- Start ARTY process.
--     paladin:Start()
--
-- ### Specific Weapons
-- This example demonstrates how to use specific weapons during an engagement.
--     -- Define the Normandy as ARTY object.
--     normandy=ARTY:New(GROUP:FindByName("Normandy"))
--
--     -- Add target: prio=50, radius=300 m, number of missiles=20, number of engagements=1, start time=08:05 hours, only use cruise missiles for this attack.
--     normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(),  20, 300,  50, 1, "08:01:00", ARTY.WeaponType.CruiseMissile)
--
--     -- Add target: prio=50, radius=300 m, number of shells=100, number of engagements=1, start time=08:15 hours, only use cannons during this attack.
--     normandy:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(),  50, 300, 100, 1, "08:15:00", ARTY.WeaponType.Cannon)
--
--     -- Define shells that are counted to check whether the ship is out of ammo.
--     -- Note that this is necessary because the Normandy has a lot of other shell type weapons which cannot be used to engage ground targets in an artillery style manner.
--     normandy:SetShellTypes({"MK45_127"})
--
--     -- Define missile types that are counted.
--     normandy:SetMissileTypes({"BGM"})
--
--     -- Start ARTY process.
--     normandy:Start()
--
-- ### Transportation as Cargo
-- This example demonstates how an ARTY group can be transported to another location as cargo.
--      -- Define a group as CARGO_GROUP
--      CargoGroupMortars=CARGO_GROUP:New(GROUP:FindByName("Mortars"), "Mortars", "Mortar Platoon Alpha", 100 , 10)
--
--      -- Define the mortar CARGO GROUP as ARTY object
--      mortars=ARTY:NewFromCargoGroup(CargoGroupMortars, "Mortar Platoon Alpha")
--
--      -- Start ARTY process
--      mortars:Start()
--
--      -- Setup AI cargo dispatcher for e.g. helos
--      SetHeloCarriers = SET_GROUP:New():FilterPrefixes("CH-47D"):FilterStart()
--      SetCargoMortars = SET_CARGO:New():FilterTypes("Mortars"):FilterStart()
--      SetZoneDepoly   = SET_ZONE:New():FilterPrefixes("Deploy"):FilterStart()
--      CargoHelo=AI_CARGO_DISPATCHER_HELICOPTER:New(SetHeloCarriers, SetCargoMortars, SetZoneDepoly)
--      CargoHelo:Start()
-- The ARTY group will be transported and resume its normal operation after it has been deployed. New targets can be assigned at any time also during the transportation process.
--
-- @field #ARTY
ARTY={
  ClassName="ARTY",
  lid=nil,
  Debug=false,
  targets={},
  moves={},
  currentTarget=nil,
  currentMove=nil,
  Nammo0=0,
  Nshells0=0,
  Nrockets0=0,
  Nmissiles0=0,
  Nukes0=0,
  Nillu0=0,
  Nsmoke0=0,
  StatusInterval=10,
  WaitForShotTime=300,
  DCSdesc=nil,
  Type=nil,
  DisplayName=nil,
  groupname=nil,
  alias=nil,
  clusters={},
  ismobile=true,
  iscargo=false,
  cargogroup=nil,
  IniGroupStrength=0,
  IsArtillery=nil,
  RearmingDistance=100,
  RearmingGroup=nil,
  RearmingGroupSpeed=nil,
  RearmingGroupOnRoad=false,
  RearmingGroupCoord=nil,
  RearmingPlaceCoord=nil,
  RearmingArtyOnRoad=false,
  InitialCoord=nil,
  report=true,
  ammoshells={},
  ammorockets={},
  ammomissiles={},
  Nshots=0,
  minrange=300,
  maxrange=1000000,
  nukewarhead=75000,
  Nukes=nil,
  nukefire=false,
  nukefires=nil,
  nukerange=nil,
  Nillu=nil,
  illuPower=1000000,
  illuMinalt=500,
  illuMaxalt=1000,
  Nsmoke=nil,
  smokeColor=SMOKECOLOR.Red,
  relocateafterfire=false,
  relocateRmin=300,
  relocateRmax=800,
  markallow=false,
  markkey=nil,
  markreadonly=false,
  autorelocate=false,
  autorelocatemaxdist=50000,
  autorelocateonroad=false,
  coalition=nil,
  respawnafterdeath=false,
  respawndelay=nil
}

--- Weapong type ID. See [here](http://wiki.hoggit.us/view/DCS_enum_weapon_flag).
-- @type ARTY.WeaponType
-- @field #number Auto Automatic selection of weapon type.
-- @field #number Cannon Cannons using conventional shells.
-- @field #number Rockets Unguided rockets.
-- @field #number CruiseMissile Cruise missiles.
-- @field #number TacticalNukes Tactical nuclear shells (simulated).
-- @field #number IlluminationShells Illumination shells (simulated).
-- @field #number SmokeShells Smoke shells (simulated).
ARTY.WeaponType={
  Auto=1073741822,
  Cannon=805306368,
  Rockets=30720,
  CruiseMissile=2097152,
  TacticalNukes=666,
  IlluminationShells=667,
  SmokeShells=668,
}

--- Database of common artillery unit properties.
-- @type ARTY.dbitem
-- @field #string displayname Name displayed in ME.
-- @field #number minrange Minimum firing range in meters.
-- @field #number maxrange Maximum firing range in meters.
-- @field #number reloadtime Reload time in seconds.

--- Database of common artillery unit properties.
-- Table key is the "type name" and table value is and `ARTY.dbitem`.
-- @type ARTY.db
ARTY.db={
  ["LeFH_18-40-105"] = {
    displayname = "FH LeFH-18 105mm", -- name displayed in the ME
    minrange    = 500,                -- min range (green circle) in meters
    maxrange    = 10500,              -- max range (red circle) in meters
    reloadtime  = nil,                -- reload time in seconds
  },
  ["M2A1-105"] = {
    displayname = "FH M2A1 105mm",
    minrange    = 500,
    maxrange    = 11500,
    reloadtime  = nil,
  },
  ["Pak40"] = {
    displayname = "FH Pak 40 75mm",
    minrange    = 500,
    maxrange    = 3000,
    reloadtime  = nil,
  },      
  ["L118_Unit"] = {
    displayname = "L118 Light Artillery Gun",
    minrange    = 500,
    maxrange    = 17500,
    reloadtime  = nil,
  },
  ["Smerch"] = {
    displayname = "MLRS 9A52 Smerch CM 300mm",
    minrange    = 20000,
    maxrange    = 70000,
    reloadtime  = 2160,
  },
  ["Smerch_HE"] = {
    displayname = "MLRS 9A52 Smerch HE 300mm",
    minrange    = 20000,
    maxrange    = 70000,
    reloadtime  = 2160,
  },
  ["Uragan_BM-27"] = {
    displayname = "MLRS 9K57 Uragan BM-27 220mm",
    minrange   = 11500,
    maxrange   = 35800,
    reloadtime = 840,
  },
  ["Grad-URAL"] = {
    displayname = "MLRS BM-21 Grad 122mm",
    minrange   = 5000,
    maxrange   = 19000,
    reloadtime = 420,
  },
  ["HL_B8M1"] = {
    displayname = "MLRS HL with B8M1 80mm",
    minrange   = 500,
    maxrange   = 5000,
    reloadtime = nil,
  },
  ["tt_B8M1"] = {
    displayname = "MLRS LC with B8M1 80mm",
    minrange   = 500,
    maxrange   = 5000,
    reloadtime = nil,
  },
  ["MLRS"] = {
    displayname = "MLRS M270 227mm",
    minrange   = 10000,
    maxrange   = 32000,
    reloadtime = 540,
  },
  ["2B11 mortar"] = {
    displayname = "Mortar 2B11 120mm", 
    minrange    = 500,
    maxrange    = 7000,
    reloadtime  = 30,
  },
  ["PLZ05"] = {
    displayname = "PLZ-05", 
    minrange    = 500,
    maxrange    = 23500,
    reloadtime  = nil,
  },
  ["SAU Gvozdika"] = {
    displayname = "SPH 2S1 Gvozdika 122mm",
    minrange    = 300,
    maxrange    = 15000,
    reloadtime  = nil,
  },
  ["SAU Msta"] = {
    displayname = "SPH 2S19 Msta 152mm",
    minrange    = 300,
    maxrange    = 23500,
    reloadtime  = nil,
  },
  ["SAU Akatsia"] = {
    displayname = "SPH 2S3 Akatsia 152mm",
    minrange    = 300,
    maxrange    = 17000,
    reloadtime  = nil,
  },
  ["SpGH_Dana"] = {
    displayname = "SPH Dana vz77 152mm",
    minrange    = 300,
    maxrange    = 18700,
    reloadtime  = nil,
  },
  ["M-109"] = {
    displayname = "SPH M109 Paladin 155mm",
    minrange    = 300,
    maxrange    = 22000,
    reloadtime  = nil,
  }, 
  ["M12_GMC"] = {
    displayname = "SPH M12 GMC 155mm",
    minrange    = 300,
    maxrange    = 18200,
    reloadtime  = nil,
  },
  ["Wespe124"] = {
    displayname = "SPH Sd.Kfz.124 Wespe 105mm",
    minrange    = 300,
    maxrange    = 7000,
    reloadtime  = nil,
  },
  ["T155_Firtina"] = {
    displayname = "SPH T155 Firtina 155mm",
    minrange    = 300,
    maxrange    = 41000,
    reloadtime  = nil,
  },
  ["SAU 2-C9"] = {
    displayname = "SPM 2S9 Nona 120mm M",
    minrange    = 500,
    maxrange    = 7000,
    reloadtime  = nil,
  },  
}

--- Target.
-- @type ARTY.Target
-- @field #string name Name of target.
-- @field Core.Point#COORDINATE coord Target coordinates.
-- @field #number radius Shelling radius in meters.
-- @field #number nshells Number of shells (or other weapon types) fired upon target.
-- @field #number engaged Number of times this target was engaged.
-- @field #boolean underfire If true, target is currently under fire.
-- @field #number prio Priority of target.
-- @field #number maxengage Max number of times, the target will be engaged.
-- @field #number time Abs. mission time in seconds, when the target is scheduled to be attacked.
-- @field #number weapontype Type of weapon used for engagement. See #ARTY.WeaponType.
-- @field #number Tassigned Abs. mission time when target was assigned.
-- @field #boolean attackgroup If true, use task attack group rather than fire at point for engagement.

--- Arty script version.
-- @field #string version
ARTY.version="1.3.3"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO list:
-- TODO: Add hit event and make the arty group relocate.
-- TODO: Handle rearming for ships. How?
-- DONE: Delete targets from queue user function.
-- DONE: Delete entire target queue user function.
-- DONE: Add weapon types. Done but needs improvements.
-- DONE: Add user defined rearm weapon types.
-- DONE: Check if target is in range. Maybe this requires a data base with the ranges of all arty units. <solved by user function>
-- DONE: Make ARTY move to rearming position.
-- DONE: Check that right rearming vehicle is specified. Blue M939, Red Ural-375. Are there more? <user needs to know!>
-- DONE: Check if ARTY group is still alive.
-- DONE: Handle dead events.
-- DONE: Abort firing task if no shooting event occured with 5(?) minutes. Something went wrong then. Min/max range for example.
-- DONE: Improve assigned time for engagement. Next day?
-- DONE: Improve documentation.
-- DONE: Add pseudo user transitions. OnAfter...
-- DONE: Make reaming unit a group.
-- DONE: Write documenation.
-- DONE: Add command move to make arty group move.
-- DONE: remove schedulers for status event.
-- DONE: Improve handling of special weapons. When winchester if using selected weapons?
-- DONE: Make coordinate after rearming general, i.e. also work after the group has moved to anonther location.
-- DONE: Add set commands via markers. E.g. set rearming place.
-- DONE: Test stationary types like mortas ==> rearming etc.
-- DONE: Add illumination and smoke.

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Creates a new ARTY object from a MOOSE group object.
-- @param #ARTY self
-- @param Wrapper.Group#GROUP group The GROUP object for which artillery tasks should be assigned.
-- @param alias (Optional) Alias name the group will be calling itself when sending messages. Default is the group name.
-- @return #ARTY ARTY object or nil if group does not exist or is not a ground or naval group.
function ARTY:New(group, alias)

  -- Inherits from FSM_CONTROLLABLE
  local self=BASE:Inherit(self, FSM_CONTROLLABLE:New()) -- #ARTY
  
  -- If group name was given.
  if type(group)=="string" then
    self.groupname=group
    group=GROUP:FindByName(group)
    if not group then
      self:E(string.format("ERROR: Requested ARTY group %s does not exist! (Has to be a MOOSE group.)", self.groupname))
      return nil      
    end
  end

  -- Check that group is present.
  if group then
    self:T(string.format("ARTY script version %s. Added group %s.", ARTY.version, group:GetName()))
  else
    self:E("ERROR: Requested ARTY group does not exist! (Has to be a MOOSE group.)")
    return nil
  end

  -- Check that we actually have a GROUND group.
  if not (group:IsGround() or group:IsShip()) then
    self:E(string.format("ERROR: ARTY group %s has to be a GROUND or SHIP group!", group:GetName()))
    return nil
  end

  -- Set the controllable for the FSM.
  self:SetControllable(group)

  -- Set the group name
  self.groupname=group:GetName()

  -- Get coalition.
  self.coalition=group:GetCoalition()

  -- Set an alias name.
  if alias~=nil then
    self.alias=tostring(alias)
  else
    self.alias=self.groupname
  end

  -- Log id.
  self.lid=string.format("ARTY %s | ", self.alias)

  -- Set the initial coordinates of the ARTY group.
  self.InitialCoord=group:GetCoordinate()

  -- Get DCS descriptors of group.
  local DCSgroup=Group.getByName(group:GetName())
  local DCSunit=DCSgroup:getUnit(1)
  self.DCSdesc=DCSunit:getDesc()

  -- DCS descriptors.
  self:T3(self.lid.."DCS descriptors for group "..group:GetName())
  for id,desc in pairs(self.DCSdesc) do
    self:T3({id=id, desc=desc})
  end

  -- Maximum speed in km/h.
  self.SpeedMax=group:GetSpeedMax()

  -- Group is mobile or not (e.g. mortars). Some immobile units have a speed of 1 m/s = 3.6 km/h. So we check this number.
  if self.SpeedMax>3.6 then
    self.ismobile=true
  else
    self.ismobile=false
  end
  
  -- Set track time interval.
  self.dtTrack=0.2

  -- Set speed to 0.7 of maximum.
  self.Speed=self.SpeedMax * 0.7

  -- Displayed name (similar to type name below)
  self.DisplayName=self.DCSdesc.displayName

  -- Is this infantry or not.
  self.IsArtillery=DCSunit:hasAttribute("Artillery")

  -- Type of group.
  self.Type=group:GetTypeName()

  -- Initial group strength.
  self.IniGroupStrength=#group:GetUnits()

  ---------------
  -- Transitions:
  ---------------

  -- Entry.
  self:AddTransition("*",           "Start",       "CombatReady")

  -- Blue branch.
  self:AddTransition("CombatReady", "OpenFire",    "Firing")
  self:AddTransition("Firing",      "CeaseFire",   "CombatReady")

  -- Violett branch.
  self:AddTransition("CombatReady", "Winchester",  "OutOfAmmo")

  -- Red branch.
  self:AddTransition({"CombatReady", "OutOfAmmo"},  "Rearm",       "Rearming")
  self:AddTransition("Rearming",                    "Rearmed",     "Rearmed")

  -- Green branch.
  self:AddTransition("*",           "Move",        "Moving")
  self:AddTransition("Moving",      "Arrived",     "Arrived")

  -- Yellow branch.
  self:AddTransition("*",           "NewTarget",   "*")

  -- Not in diagram.
  self:AddTransition("*",           "CombatReady", "CombatReady")
  self:AddTransition("*",           "Status",      "*")
  self:AddTransition("*",           "NewMove",     "*")
  self:AddTransition("*",           "Dead",        "*")
  self:AddTransition("*",           "Respawn",     "CombatReady")

  -- Transport as cargo (not in diagram).
  self:AddTransition("*",           "Loaded",      "InTransit")
  self:AddTransition("InTransit",   "UnLoaded",    "CombatReady")

  -- Unknown transitons. To be checked if adding these causes problems.
  self:AddTransition("Rearming",    "Arrived",     "Rearming")
  self:AddTransition("Rearming",    "Move",        "Rearming")


  --- User function for OnAfter "NewTarget" event.
  -- @function [parent=#ARTY] OnAfterNewTarget
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #table target Array holding the target info.

  --- User function for OnAfter "OpenFire" event.
  -- @function [parent=#ARTY] OnAfterOpenFire
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #table target Array holding the target info.

  --- User function for OnAfter "CeaseFire" event.
  -- @function [parent=#ARTY] OnAfterCeaseFire
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #table target Array holding the target info.

  --- User function for OnAfer "NewMove" event.
  -- @function [parent=#ARTY] OnAfterNewMove
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #table move Array holding the move info.

  --- User function for OnAfer "Move" event.
  -- @function [parent=#ARTY] OnAfterMove
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #table move Array holding the move info.

  --- User function for OnAfer "Arrived" event.
  -- @function [parent=#ARTY] OnAfterArrvied
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- User function for OnAfter "Winchester" event.
  -- @function [parent=#ARTY] OnAfterWinchester
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- User function for OnAfter "Rearm" event.
  -- @function [parent=#ARTY] OnAfterRearm
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- User function for OnAfter "Rearmed" event.
  -- @function [parent=#ARTY] OnAfterRearmed
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- User function for OnAfter "Start" event.
  -- @function [parent=#ARTY] OnAfterStart
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- User function for OnAfter "Status" event.
  -- @function [parent=#ARTY] OnAfterStatus
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- User function for OnAfter "Dead" event.
  -- @function [parent=#ARTY] OnAfterDead
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string Unitname Name of the dead unit.

  --- User function for OnAfter "Respawn" event.
  -- @function [parent=#ARTY] OnAfterRespawn
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- User function for OnEnter "CombatReady" state.
  -- @function [parent=#ARTY] OnEnterCombatReady
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- User function for OnEnter "Firing" state.
  -- @function [parent=#ARTY] OnEnterFiring
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- User function for OnEnter "OutOfAmmo" state.
  -- @function [parent=#ARTY] OnEnterOutOfAmmo
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- User function for OnEnter "Rearming" state.
  -- @function [parent=#ARTY] OnEnterRearming
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- User function for OnEnter "Rearmed" state.
  -- @function [parent=#ARTY] OnEnterRearmed
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- User function for OnEnter "Moving" state.
  -- @function [parent=#ARTY] OnEnterMoving
  -- @param #ARTY self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Function to start the ARTY FSM process.
  -- @function [parent=#ARTY] Start
  -- @param #ARTY self

  --- Function to start the ARTY FSM process after a delay.
  -- @function [parent=#ARTY] __Start
  -- @param #ARTY self
  -- @param #number Delay before start in seconds.

  --- Function to update the status of the ARTY group and tigger FSM events. Triggers the FSM event "Status".
  -- @function [parent=#ARTY] Status
  -- @param #ARTY self

  --- Function to update the status of the ARTY group and tigger FSM events after a delay. Triggers the FSM event "Status".
  -- @function [parent=#ARTY] __Status
  -- @param #ARTY self
  -- @param #number Delay in seconds.

  --- Function called when a unit of the ARTY group died. Triggers the FSM event "Dead".
  -- @function [parent=#ARTY] Dead
  -- @param #ARTY self
  -- @param #string unitname Name of the unit that died.

  --- Function called when a unit of the ARTY group died after a delay. Triggers the FSM event "Dead".
  -- @function [parent=#ARTY] __Dead
  -- @param #ARTY self
  -- @param #number Delay in seconds.
  -- @param #string unitname Name of the unit that died.

  --- Add a new target for the ARTY group. Triggers the FSM event "NewTarget".
  -- @function [parent=#ARTY] NewTarget
  -- @param #ARTY self
  -- @param #table target Array holding the target data.

  --- Add a new target for the ARTY group with a delay. Triggers the FSM event "NewTarget".
  -- @function [parent=#ARTY] __NewTarget
  -- @param #ARTY self
  -- @param #number delay Delay in seconds.
  -- @param #table target Array holding the target data.

  --- Add a new relocation move for the ARTY group. Triggers the FSM event "NewMove".
  -- @function [parent=#ARTY] NewMove
  -- @param #ARTY self
  -- @param #table move Array holding the relocation move data.

  --- Add a new relocation for the ARTY group after a delay. Triggers the FSM event "NewMove".
  -- @function [parent=#ARTY] __NewMove
  -- @param #ARTY self
  -- @param #number delay Delay in seconds.
  -- @param #table move Array holding the relocation move data.

  --- Order ARTY group to open fire on a target. Triggers the FSM event "OpenFire".
  -- @function [parent=#ARTY] OpenFire
  -- @param #ARTY self
  -- @param #table target Array holding the target data.

  --- Order ARTY group to open fire on a target with a delay. Triggers the FSM event "Move".
  -- @function [parent=#ARTY] __OpenFire
  -- @param #ARTY self
  -- @param #number delay Delay in seconds.
  -- @param #table target Array holding the target data.

  --- Order ARTY group to cease firing on a target. Triggers the FSM event "CeaseFire".
  -- @function [parent=#ARTY] CeaseFire
  -- @param #ARTY self
  -- @param #table target Array holding the target data.

  --- Order ARTY group to cease firing on a target after a delay. Triggers the FSM event "CeaseFire".
  -- @function [parent=#ARTY] __CeaseFire
  -- @param #ARTY self
  -- @param #number delay Delay in seconds.
  -- @param #table target Array holding the target data.

  --- Order ARTY group to move to another location. Triggers the FSM event "Move".
  -- @function [parent=#ARTY] Move
  -- @param #ARTY self
  -- @param #table move Array holding the relocation move data.

  --- Order ARTY group to move to another location after a delay. Triggers the FSM event "Move".
  -- @function [parent=#ARTY] __Move
  -- @param #ARTY self
  -- @param #number delay Delay in seconds.
  -- @param #table move Array holding the relocation move data.

  --- Tell ARTY group it has arrived at its destination. Triggers the FSM event "Arrived".
  -- @function [parent=#ARTY] Arrived
  -- @param #ARTY self

  --- Tell ARTY group it has arrived at its destination after a delay. Triggers the FSM event "Arrived".
  -- @function [parent=#ARTY] __Arrived
  -- @param #ARTY self
  -- @param #number delay Delay in seconds.

  --- Tell ARTY group it is combat ready. Triggers the FSM event "CombatReady".
  -- @function [parent=#ARTY] CombatReady
  -- @param #ARTY self

  --- Tell ARTY group it is combat ready after a delay. Triggers the FSM event "CombatReady".
  -- @function [parent=#ARTY] __CombatReady
  -- @param #ARTY self
  -- @param #number delay Delay in seconds.

  --- Tell ARTY group it is out of ammo. Triggers the FSM event "Winchester".
  -- @function [parent=#ARTY] Winchester
  -- @param #ARTY self

  --- Tell ARTY group it is out of ammo after a delay. Triggers the FSM event "Winchester".
  -- @function [parent=#ARTY] __Winchester
  -- @param #ARTY self
  -- @param #number delay Delay in seconds.

  --- Respawn ARTY group.
  -- @function [parent=#ARTY] Respawn
  -- @param #ARTY self

  --- Respawn ARTY group after a delay.
  -- @function [parent=#ARTY] __Respawn
  -- @param #ARTY self
  -- @param #number delay Delay in seconds.

  return self
end

--- Creates a new ARTY object from a MOOSE CARGO_GROUP object.
-- @param #ARTY self
-- @param Cargo.CargoGroup#CARGO_GROUP cargogroup The CARGO GROUP object for which artillery tasks should be assigned.
-- @param alias (Optional) Alias name the group will be calling itself when sending messages. Default is the group name.
-- @return #ARTY ARTY object or nil if group does not exist or is not a ground or naval group.
function ARTY:NewFromCargoGroup(cargogroup, alias)

  if cargogroup then
    BASE:T(string.format("ARTY script version %s. Added CARGO group %s.", ARTY.version, cargogroup:GetName()))
  else
    BASE:E("ERROR: Requested ARTY CARGO GROUP does not exist! (Has to be a MOOSE CARGO(!) group.)")
    return nil
  end

  -- Get group belonging to the cargo group.
  local group=cargogroup:GetObject()

  -- Create ARTY object.
  local arty=ARTY:New(group,alias)

  -- Set iscargo flag.
  arty.iscargo=true

  -- Set cargo group object.
  arty.cargogroup=cargogroup

  return arty
end



-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Assign target coordinates to the ARTY group. Only the first parameter, i.e. the coordinate of the target is mandatory. The remaining parameters are optional and can be used to fine tune the engagement.
-- @param #ARTY self
-- @param Core.Point#COORDINATE coord Coordinates of the target.
-- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50.
-- @param #number radius (Optional) Radius. Default is 100 m.
-- @param #number nshells (Optional) How many shells (or rockets) are fired on target per engagement. Default 5.
-- @param #number maxengage (Optional) How many times a target is engaged. Default 1.
-- @param #string time (Optional) Day time at which the target should be engaged. Passed as a string in format "08:13:45". Current task will be canceled.
-- @param #number weapontype (Optional) Type of weapon to be used to attack this target. Default ARTY.WeaponType.Auto, i.e. the DCS logic automatically determins the appropriate weapon.
-- @param #string name (Optional) Name of the target. Default is LL DMS coordinate of the target. If the name was already given, the numbering "#01", "#02",... is appended automatically.
-- @param #boolean unique (Optional) Target is unique. If the target name is already known, the target is rejected. Default false.
-- @return #string Name of the target. Can be used for further reference, e.g. deleting the target from the list.
-- @usage paladin=ARTY:New(GROUP:FindByName("Blue Paladin"))
-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 10, 300, 10, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1")
-- paladin:Start()
function ARTY:AssignTargetCoord(coord, prio, radius, nshells, maxengage, time, weapontype, name, unique)
  self:F({coord=coord, prio=prio, radius=radius, nshells=nshells, maxengage=maxengage, time=time, weapontype=weapontype, name=name, unique=unique})

  -- Set default values.
  nshells=nshells or 5
  radius=radius or 100
  maxengage=maxengage or 1
  prio=prio or 50
  prio=math.max(  1, prio)
  prio=math.min(100, prio)
  if unique==nil then
    unique=false
  end
  weapontype=weapontype or ARTY.WeaponType.Auto

  -- Check if we have a coordinate object.
  local text=nil
  if coord:IsInstanceOf("GROUP") then
    text="WARNING: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter - you gave a GROUP. Converting to COORDINATE..."
    coord=coord:GetCoordinate()
  elseif coord:IsInstanceOf("UNIT") then
    text="WARNING: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter - you gave a UNIT. Converting to COORDINATE..."
    coord=coord:GetCoordinate()
  elseif coord:IsInstanceOf("POSITIONABLE") then
    text="WARNING: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter - you gave a POSITIONABLE. Converting to COORDINATE..."
    coord=coord:GetCoordinate()
  elseif coord:IsInstanceOf("COORDINATE") then
    -- Nothing to do here.
  else
    text="ERROR: ARTY:AssignTargetCoordinate(coord, ...) needs a COORDINATE object as first parameter!"
    MESSAGE:New(text, 30):ToAll()
    self:E(self.lid..text)
    return nil
  end
  if text~=nil then
    self:E(self.lid..text)
  end

  -- Name of the target.
  local _name=name or coord:ToStringLLDMS()
  local _unique=true

  -- Check if the name has already been used for another target. If so, the function returns a new unique name.
  _name,_unique=self:_CheckName(self.targets, _name, not unique)

  -- Target name should be unique and is not.
  if unique==true and _unique==false then
    self:T(self.lid..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!", self.groupname, _name))
    return nil
  end

  -- Time in seconds.
  local _time
  if type(time)=="string" then
    _time=self:_ClockToSeconds(time)
  elseif type(time)=="number" then
    _time=timer.getAbsTime()+time
  else
    _time=timer.getAbsTime()
  end

  -- Prepare target array.
  local _target={name=_name, coord=coord, radius=radius, nshells=nshells, engaged=0, underfire=false, prio=prio, maxengage=maxengage, time=_time, weapontype=weapontype}

  -- Add to table.
  table.insert(self.targets, _target)

  -- Trigger new target event.
  self:__NewTarget(1, _target)

  return _name
end

--- Assign a target group to the ARTY group. Note that this will use the Attack Group Task rather than the Fire At Point Task.
-- @param #ARTY self
-- @param Wrapper.Group#GROUP group Target group.
-- @param #number prio (Optional) Priority of target. Number between 1 (high) and 100 (low). Default 50.
-- @param #number radius (Optional) Radius. Default is 100 m.
-- @param #number nshells (Optional) How many shells (or rockets) are fired on target per engagement. Default 5.
-- @param #number maxengage (Optional) How many times a target is engaged. Default 1.
-- @param #string time (Optional) Day time at which the target should be engaged. Passed as a string in format "08:13:45". Current task will be canceled.
-- @param #number weapontype (Optional) Type of weapon to be used to attack this target. Default ARTY.WeaponType.Auto, i.e. the DCS logic automatically determins the appropriate weapon.
-- @param #string name (Optional) Name of the target. Default is LL DMS coordinate of the target. If the name was already given, the numbering "#01", "#02",... is appended automatically.
-- @param #boolean unique (Optional) Target is unique. If the target name is already known, the target is rejected. Default false.
-- @return #string Name of the target. Can be used for further reference, e.g. deleting the target from the list.
-- @usage paladin=ARTY:New(GROUP:FindByName("Blue Paladin"))
-- paladin:AssignTargetCoord(GROUP:FindByName("Red Targets 1"):GetCoordinate(), 10, 300, 10, 1, "08:02:00", ARTY.WeaponType.Auto, "Target 1")
-- paladin:Start()
function ARTY:AssignAttackGroup(group, prio, radius, nshells, maxengage, time, weapontype, name, unique)

  -- Set default values.
  nshells=nshells or 5
  radius=radius or 100
  maxengage=maxengage or 1
  prio=prio or 50
  prio=math.max(  1, prio)
  prio=math.min(100, prio)
  if unique==nil then
    unique=false
  end
  weapontype=weapontype or ARTY.WeaponType.Auto

  -- TODO Check if we have a group object.
  if type(group)=="string" then
    group=GROUP:FindByName(group)
  end

  if group and group:IsAlive() then

    local coord=group:GetCoordinate()

    -- Name of the target.
    local _name=group:GetName()
    local _unique=true

    -- Check if the name has already been used for another target. If so, the function returns a new unique name.
    _name,_unique=self:_CheckName(self.targets, _name, not unique)

    -- Target name should be unique and is not.
    if unique==true and _unique==false then
      self:T(self.lid..string.format("%s: target %s should have a unique name but name was already given. Rejecting target!", self.groupname, _name))
      return nil
    end

    -- Time in seconds.
    local _time
    if type(time)=="string" then
      _time=self:_ClockToSeconds(time)
    elseif type(time)=="number" then
      _time=timer.getAbsTime()+time
    else
      _time=timer.getAbsTime()
    end

    -- Prepare target array.
    local target={} --#ARTY.Target
    target.attackgroup=true
    target.name=_name
    target.coord=coord
    target.radius=radius
    target.nshells=nshells
    target.engaged=0
    target.underfire=false
    target.prio=prio
    target.time=_time
    target.maxengage=maxengage
    target.weapontype=weapontype

    -- Add to table.
    table.insert(self.targets, target)

    -- Trigger new target event.
    self:__NewTarget(1, target)

    return _name
  else
    self:E("ERROR: Group does not exist!")
  end

  return nil
end



--- Assign coordinate to where the ARTY group should move.
-- @param #ARTY self
-- @param Core.Point#COORDINATE coord Coordinates of the new position.
-- @param #string time (Optional) Day time at which the group should start moving. Passed as a string in format "08:13:45". Default is now.
-- @param #number speed (Optinal) Speed in km/h the group should move at. Default 70% of max posible speed of group.
-- @param #boolean onroad (Optional) If true, group will mainly use roads. Default off, i.e. go directly towards the specified coordinate.
-- @param #boolean cancel (Optional) If true, cancel any running attack when move should begin. Default is false.
-- @param #string name (Optional) Name of the coordinate. Default is LL DMS string of the coordinate. If the name was already given, the numbering "#01", "#02",... is appended automatically.
-- @param #boolean unique (Optional) Move is unique. If the move name is already known, the move is rejected. Default false.
-- @return #string Name of the move. Can be used for further reference, e.g. deleting the move from the list.
function ARTY:AssignMoveCoord(coord, time, speed, onroad, cancel, name, unique)
  self:F({coord=coord, time=time, speed=speed, onroad=onroad, cancel=cancel, name=name, unique=unique})

  -- Reject move if the group is immobile.
  if not self.ismobile then
    self:T(self.lid..string.format("%s: group is immobile. Rejecting move request!", self.groupname))
    return nil
  end

  -- Default
  if unique==nil then
    unique=false
  end

  -- Name of the target.
  local _name=name or coord:ToStringLLDMS()
  local _unique=true

  -- Check if the name has already been used for another target. If so, the function returns a new unique name.
  _name,_unique=self:_CheckName(self.moves, _name, not unique)

  -- Move name should be unique and is not.
  if unique==true and _unique==false then
    self:T(self.lid..string.format("%s: move %s should have a unique name but name was already given. Rejecting move!", self.groupname, _name))
    return nil
  end

  -- Set speed.
  if speed then
    -- Make sure, given speed is less than max physiaclly possible speed of group.
    speed=math.min(speed, self.SpeedMax)
  elseif self.Speed then
    speed=self.Speed
  else
    speed=self.SpeedMax*0.7
  end

  -- Default is off road.
  if onroad==nil then
    onroad=false
  end

  -- Default is not to cancel a running attack.
  if cancel==nil then
    cancel=false
  end

  -- Time in seconds.
  local _time
  if type(time)=="string" then
    _time=self:_ClockToSeconds(time)
  elseif type(time)=="number" then
    _time=timer.getAbsTime()+time
  else
    _time=timer.getAbsTime()
  end

  -- Prepare move array.
  local _move={name=_name, coord=coord, time=_time, speed=speed, onroad=onroad, cancel=cancel}

  -- Add to table.
  table.insert(self.moves, _move)

  return _name
end

--- Set alias, i.e. the name the group will use when sending messages.
-- @param #ARTY self
-- @param #string alias The alias for the group.
-- @return self
function ARTY:SetAlias(alias)
  self:F({alias=alias})
  self.alias=tostring(alias)
  return self
end

--- Add ARTY group to one or more clusters. Enables addressing all ARTY groups within a cluster simultaniously via marker assignments.
-- @param #ARTY self
-- @param #table clusters Table of cluster names the group should belong to.
-- @return self
function ARTY:AddToCluster(clusters)
  self:F({clusters=clusters})

  -- Convert input to table.
  local names
  if type(clusters)=="table" then
    names=clusters
  elseif type(clusters)=="string" then
    names={clusters}
  else
    -- error message
    self:E(self.lid.."ERROR: Input parameter must be a string or a table in ARTY:AddToCluster()!")
    return
  end

  -- Add names to cluster array.
  for _,cluster in pairs(names) do
    table.insert(self.clusters, cluster)
  end

  return self
end

--- Set minimum firing range. Targets closer than this distance are not engaged.
-- @param #ARTY self
-- @param #number range Min range in kilometers. Default is 0.1 km.
-- @return self
function ARTY:SetMinFiringRange(range)
  self:F({range=range})
  self.minrange=range*1000 or 100
  return self
end

--- Set maximum firing range. Targets further away than this distance are not engaged.
-- @param #ARTY self
-- @param #number range Max range in kilometers. Default is 1000 km.
-- @return self
function ARTY:SetMaxFiringRange(range)
  self:F({range=range})
  self.maxrange=range*1000 or 1000*1000
  return self
end

--- Set time interval between status updates. During the status check, new events are triggered.
-- @param #ARTY self
-- @param #number interval Time interval in seconds. Default 10 seconds.
-- @return self
function ARTY:SetStatusInterval(interval)
  self:F({interval=interval})
  self.StatusInterval=interval or 10
  return self
end

--- Set time interval for weapon tracking.
-- @param #ARTY self
-- @param #number interval Time interval in seconds. Default 0.2 seconds.
-- @return self
function ARTY:SetTrackInterval(interval)
  self.dtTrack=interval or 0.2
  return self
end

--- Set time how it is waited a unit the first shot event happens. If no shot is fired after this time, the task to fire is aborted and the target removed.
-- @param #ARTY self
-- @param #number waittime Time in seconds. Default 300 seconds.
-- @return self
function ARTY:SetWaitForShotTime(waittime)
  self:F({waittime=waittime})
  self.WaitForShotTime=waittime or 300
  return self
end

--- Define the safe distance between ARTY group and rearming unit or rearming place at which rearming process is possible.
-- @param #ARTY self
-- @param #number distance Safe distance in meters. Default is 100 m.
-- @return self
function ARTY:SetRearmingDistance(distance)
  self:F({distance=distance})
  self.RearmingDistance=distance or 100
  return self
end

--- Assign a group, which is responsible for rearming the ARTY group. If the group is too far away from the ARTY group it will be guided towards the ARTY group.
-- @param #ARTY self
-- @param Wrapper.Group#GROUP group Group that is supposed to rearm the ARTY group. For the blue coalition, this is often a unarmed M939 transport whilst for red an unarmed Ural-375 transport can be used.
-- @return self
function ARTY:SetRearmingGroup(group)
  self:F({group=group})
  self.RearmingGroup=group
  return self
end

--- Set the speed the rearming group moves at towards the ARTY group or the rearming place.
-- @param #ARTY self
-- @param #number speed Speed in km/h.
-- @return self
function ARTY:SetRearmingGroupSpeed(speed)
  self:F({speed=speed})
  self.RearmingGroupSpeed=speed
  return self
end

--- Define if rearming group uses mainly roads to drive to the ARTY group or rearming place.
-- @param #ARTY self
-- @param #boolean onroad If true, rearming group uses mainly roads. If false, it drives directly to the ARTY group or rearming place.
-- @return self
function ARTY:SetRearmingGroupOnRoad(onroad)
  self:F({onroad=onroad})
  if onroad==nil then
    onroad=true
  end
  self.RearmingGroupOnRoad=onroad
  return self
end

--- Define if ARTY group uses mainly roads to drive to the rearming place.
-- @param #ARTY self
-- @param #boolean onroad If true, ARTY group uses mainly roads. If false, it drives directly to the rearming place.
-- @return self
function ARTY:SetRearmingArtyOnRoad(onroad)
  self:F({onroad=onroad})
  if onroad==nil then
    onroad=true
  end
  self.RearmingArtyOnRoad=onroad
  return self
end

--- Defines the rearming place of the ARTY group. If the place is too far away from the ARTY group it will be routed to the place.
-- @param #ARTY self
-- @param Core.Point#COORDINATE coord Coordinates of the rearming place.
-- @return self
function ARTY:SetRearmingPlace(coord)
  self:F({coord=coord})
  self.RearmingPlaceCoord=coord
  return self
end

--- Set automatic relocation of ARTY group if a target is assigned which is out of range. The unit will drive automatically towards or away from the target to be in max/min firing range.
-- @param #ARTY self
-- @param #number maxdistance (Optional) The maximum distance in km the group will travel to get within firing range. Default is 50 km. No automatic relocation is performed if targets are assigned which are further away.
-- @param #boolean onroad (Optional) If true, ARTY group uses roads whenever possible. Default false, i.e. group will move in a straight line to the assigned coordinate.
-- @return self
function ARTY:SetAutoRelocateToFiringRange(maxdistance, onroad)
  self:F({distance=maxdistance, onroad=onroad})
  self.autorelocate=true
  self.autorelocatemaxdist=maxdistance or 50
  self.autorelocatemaxdist=self.autorelocatemaxdist*1000
  if onroad==nil then
    onroad=false
  end
  self.autorelocateonroad=onroad
  return self
end

--- Set relocate after firing. Group will find a new location after each engagement. Default is off
-- @param #ARTY self
-- @param #number rmax (Optional) Max distance in meters, the group will move to relocate. Default is 800 m.
-- @param #number rmin (Optional) Min distance in meters, the group will move to relocate. Default is 300 m.
-- @return self
function ARTY:SetAutoRelocateAfterEngagement(rmax, rmin)
  self.relocateafterfire=true
  self.relocateRmax=rmax or 800
  self.relocateRmin=rmin or 300

  -- Ensure that Rmin<=Rmax
  self.relocateRmin=math.min(self.relocateRmin, self.relocateRmax)

  return self
end

--- Report messages of ARTY group turned on. This is the default.
-- @param #ARTY self
-- @return self
function ARTY:SetReportON()
  self.report=true
  return self
end

--- Report messages of ARTY group turned off. Default is on.
-- @param #ARTY self
-- @return self
function ARTY:SetReportOFF()
  self.report=false
  return self
end

--- Respawn group once all units are dead.
-- @param #ARTY self
-- @param #number delay (Optional) Delay before respawn in seconds.
-- @return self
function ARTY:SetRespawnOnDeath(delay)
  self.respawnafterdeath=true
  self.respawndelay=delay
  return self
end

--- Turn debug mode on. Information is printed to screen.
-- @param #ARTY self
-- @return self
function ARTY:SetDebugON()
  self.Debug=true
  return self
end

--- Turn debug mode off. This is the default setting.
-- @param #ARTY self
-- @return self
function ARTY:SetDebugOFF()
  self.Debug=false
  return self
end

--- Set default speed the group is moving at if not specified otherwise.
-- @param #ARTY self
-- @param #number speed Speed in km/h.
-- @return self
function ARTY:SetSpeed(speed)
  self.Speed=speed
  return self
end

--- Delete a target from target list. If the target is currently engaged, it is cancelled.
-- @param #ARTY self
-- @param #string name Name of the target.
function ARTY:RemoveTarget(name)
  self:F2(name)

  -- Get target ID from namd
  local id=self:_GetTargetIndexByName(name)

  if id then

    -- Remove target from table.
    self:T(self.lid..string.format("Group %s: Removing target %s (id=%d).", self.groupname, name, id))
    table.remove(self.targets, id)

    -- Delete marker belonging to this engagement.
    if self.markallow then
      local batteryname,markTargetID, markMoveID=self:_GetMarkIDfromName(name)
      if batteryname==self.groupname and markTargetID~=nil then
        COORDINATE:RemoveMark(markTargetID)
      end
    end

  end
  self:T(self.lid..string.format("Group %s: Number of targets = %d.", self.groupname, #self.targets))
end

--- Delete a move from move list.
-- @param #ARTY self
-- @param #string name Name of the target.
function ARTY:RemoveMove(name)
  self:F2(name)

  -- Get move ID from name.
  local id=self:_GetMoveIndexByName(name)

  if id then

    -- Remove move from table.
    self:T(self.lid..string.format("Group %s: Removing move %s (id=%d).", self.groupname, name, id))
    table.remove(self.moves, id)

    -- Delete marker belonging to this relocation move.
    if self.markallow then
      local batteryname,markTargetID,markMoveID=self:_GetMarkIDfromName(name)
      if batteryname==self.groupname and markMoveID~=nil then
        COORDINATE:RemoveMark(markMoveID)
      end
    end

  end
  self:T(self.lid..string.format("Group %s: Number of moves = %d.", self.groupname, #self.moves))
end

--- Delete ALL targets from current target list.
-- @param #ARTY self
function ARTY:RemoveAllTargets()
  self:F2()
  for _,target in pairs(self.targets) do
    self:RemoveTarget(target.name)
  end
end

--- Define shell types that are counted to determine the ammo amount the ARTY group has.
-- @param #ARTY self
-- @param #table tableofnames Table of shell type names.
-- @return self
function ARTY:SetShellTypes(tableofnames)
  self:F2(tableofnames)
  self.ammoshells={}
  for _,_type in pairs(tableofnames) do
    table.insert(self.ammoshells, _type)
  end
  return self
end

--- Define rocket types that are counted to determine the ammo amount the ARTY group has.
-- @param #ARTY self
-- @param #table tableofnames Table of rocket type names.
-- @return self
function ARTY:SetRocketTypes(tableofnames)
  self:F2(tableofnames)
  self.ammorockets={}
  for _,_type in pairs(tableofnames) do
    table.insert(self.ammorockets, _type)
  end
  return self
end

--- Define missile types that are counted to determine the ammo amount the ARTY group has.
-- @param #ARTY self
-- @param #table tableofnames Table of rocket type names.
-- @return self
function ARTY:SetMissileTypes(tableofnames)
  self:F2(tableofnames)
  self.ammomissiles={}
  for _,_type in pairs(tableofnames) do
    table.insert(self.ammomissiles, _type)
  end
  return self
end

--- Set number of tactical nuclear warheads available to the group.
-- Note that it can be max the number of normal shells. Also if all normal shells are empty, firing nuclear shells is also not possible any more until group gets rearmed.
-- @param #ARTY self
-- @param #number n Number of warheads for the whole group.
-- @return self
function ARTY:SetTacNukeShells(n)
  self.Nukes=n
  return self
end

--- Set nuclear warhead explosion strength.
-- @param #ARTY self
-- @param #number strength Explosion strength in kilo tons TNT. Default is 0.075 kt.
-- @return self
function ARTY:SetTacNukeWarhead(strength)
  self.nukewarhead=strength or 0.075
  self.nukewarhead=self.nukewarhead*1000*1000 -- convert to kg TNT.
  return self
end

--- Set number of illumination shells available to the group.
-- Note that it can be max the number of normal shells. Also if all normal shells are empty, firing illumination shells is also not possible any more until group gets rearmed.
-- @param #ARTY self
-- @param #number n Number of illumination shells for the whole group.
-- @param #number power (Optional) Power of illumination warhead in mega candela. Default 1.0 mcd.
-- @return self
function ARTY:SetIlluminationShells(n, power)
  self.Nillu=n
  self.illuPower=power or 1.0
  self.illuPower=self.illuPower * 1000000
  return self
end

--- Set minimum and maximum detotation altitude for illumination shells. A value between min/max is selected randomly.
-- The illumination bomb will burn for 300 seconds (5 minutes). Assuming a descent rate of ~3 m/s the "optimal" altitude would be 900 m.
-- @param #ARTY self
-- @param #number minalt (Optional) Minium altitude in meters. Default 500 m.
-- @param #number maxalt (Optional) Maximum altitude in meters. Default 1000 m.
-- @return self
function ARTY:SetIlluminationMinMaxAlt(minalt, maxalt)
  self.illuMinalt=minalt or 500
  self.illuMaxalt=maxalt or 1000

  if self.illuMinalt>self.illuMaxalt then
    self.illuMinalt=self.illuMaxalt
  end
  return self
end

--- Set number of smoke shells available to the group.
-- Note that it can be max the number of normal shells. Also if all normal shells are empty, firing smoke shells is also not possible any more until group gets rearmed.
-- @param #ARTY self
-- @param #number n Number of smoke shells for the whole group.
-- @param Utilities.Utils#SMOKECOLOR color (Optional) Color of the smoke. Default SMOKECOLOR.Red.
-- @return self
function ARTY:SetSmokeShells(n, color)
  self.Nsmoke=n
  self.smokeColor=color or SMOKECOLOR.Red
  return self
end

--- Set nuclear fires and extra demolition explosions.
-- @param #ARTY self
-- @param #number nfires (Optional) Number of big smoke and fire objects created in the demolition zone.
-- @param #number demolitionrange (Optional) Demolition range in meters.
-- @return self
function ARTY:SetTacNukeFires(nfires, range)
  self.nukefire=true
  self.nukefires=nfires
  self.nukerange=range
  return self
end

--- Enable assigning targets and moves by placing markers on the F10 map.
-- @param #ARTY self
-- @param #number key (Optional) Authorization key. Only players knowing this key can assign targets. Default is no authorization required.
-- @param #boolean readonly (Optional) Marks are readonly and cannot be removed by players. This also means that targets cannot be cancelled by removing the mark. Default false.
-- @return self
function ARTY:SetMarkAssignmentsOn(key, readonly)
  self.markkey=key
  self.markallow=true
  if readonly==nil then
    self.markreadonly=false
  end
  return self
end

--- Disable assigning targets by placing markers on the F10 map.
-- @param #ARTY self
-- @return self
function ARTY:SetMarkTargetsOff()
  self.markallow=false
  self.markkey=nil
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Start Event
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- After "Start" event. Initialized ROE and alarm state. Starts the event handler.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARTY:onafterStart(Controllable, From, Event, To)
  self:_EventFromTo("onafterStart", Event, From, To)

  -- Debug output.
  local text=string.format("Started ARTY version %s for group %s.", ARTY.version, Controllable:GetName())
  self:I(self.lid..text)
  MESSAGE:New(text, 5):ToAllIf(self.Debug)

  -- Get Ammo.
  self.Nammo0, self.Nshells0, self.Nrockets0, self.Nmissiles0, self.Narty0=self:GetAmmo(self.Debug)

  -- Init nuclear explosion parameters if they were not set by user.
  if self.nukerange==nil then
    self.nukerange=1500/75000*self.nukewarhead  -- linear dependence
  end
  if self.nukefires==nil then
    self.nukefires=20/1000/1000*self.nukerange*self.nukerange
  end

  -- Init nuclear shells.
  if self.Nukes~=nil then
    self.Nukes0=math.min(self.Nukes, self.Nshells0)
  else
    self.Nukes=0
    self.Nukes0=0
  end

  -- Init illumination shells.
  if self.Nillu~=nil then
    self.Nillu0=math.min(self.Nillu, self.Nshells0)
  else
    self.Nillu=0
    self.Nillu0=0
  end

  -- Init smoke shells.
  if self.Nsmoke~=nil then
    self.Nsmoke0=math.min(self.Nsmoke, self.Nshells0)
  else
    self.Nsmoke=0
    self.Nsmoke0=0
  end

  -- Check if we have and arty type that is in the DB.
  local _dbproperties=self:_CheckDB(self.Type)
  self:T({dbproperties=_dbproperties})
  if _dbproperties~=nil then
    for property,value in pairs(_dbproperties) do
      self:T({property=property, value=value})
        self[property]=value
    end
  end

  -- Some mobility consitency checks if group cannot move.
  if not self.ismobile then
    self.RearmingPlaceCoord=nil
    self.relocateafterfire=false
    self.autorelocate=false
    --self.RearmingGroupSpeed=20
  end

  -- Check that default speed is below max speed.
  self.Speed=math.min(self.Speed, self.SpeedMax)

  -- Set Rearming group speed if not specified by user
  if self.RearmingGroup then

    -- Get max speed of rearming group.
    local speedmax=self.RearmingGroup:GetSpeedMax()
    self:T(self.lid..string.format("%s, rearming group %s max speed = %.1f km/h.", self.groupname, self.RearmingGroup:GetName(), speedmax))

    if self.RearmingGroupSpeed==nil then
      -- Set rearming group speed to 50% of max possible speed.
      self.RearmingGroupSpeed=speedmax*0.5
    else
      -- Ensure that speed is <= max speed.
      self.RearmingGroupSpeed=math.min(self.RearmingGroupSpeed, self.RearmingGroup:GetSpeedMax())
    end
  else
    -- Just to have a reasonable number for output format below.
    self.RearmingGroupSpeed=23
  end

  local text=string.format("\n******************************************************\n")
  text=text..string.format("Arty group          = %s\n", self.groupname)
  text=text..string.format("Arty alias          = %s\n", self.alias)
  text=text..string.format("Artillery attribute = %s\n", tostring(self.IsArtillery))
  text=text..string.format("Type                = %s\n", self.Type)
  text=text..string.format("Display Name        = %s\n", self.DisplayName)
  text=text..string.format("Number of units     = %d\n", self.IniGroupStrength)
  text=text..string.format("Speed max           = %.1f km/h\n", self.SpeedMax)
  text=text..string.format("Speed default       = %.1f km/h\n", self.Speed)
  text=text..string.format("Is mobile           = %s\n", tostring(self.ismobile))
  text=text..string.format("Is cargo            = %s\n", tostring(self.iscargo))
  text=text..string.format("Min range           = %.1f km\n", self.minrange/1000)
  text=text..string.format("Max range           = %.1f km\n", self.maxrange/1000)
  text=text..string.format("Total ammo count    = %d\n", self.Nammo0)
  text=text..string.format("Number of shells    = %d\n", self.Nshells0)
  text=text..string.format("Number of rockets   = %d\n", self.Nrockets0)
  text=text..string.format("Number of missiles  = %d\n", self.Nmissiles0)
  text=text..string.format("Number of nukes     = %d\n", self.Nukes0)
  text=text..string.format("Nuclear warhead     = %d tons TNT\n", self.nukewarhead/1000)
  text=text..string.format("Nuclear demolition  = %d m\n", self.nukerange)
  text=text..string.format("Nuclear fires       = %d (active=%s)\n", self.nukefires, tostring(self.nukefire))
  text=text..string.format("Number of illum.    = %d\n", self.Nillu0)
  text=text..string.format("Illuminaton Power   = %.3f mcd\n", self.illuPower/1000000)
  text=text..string.format("Illuminaton Minalt  = %d m\n", self.illuMinalt)
  text=text..string.format("Illuminaton Maxalt  = %d m\n", self.illuMaxalt)
  text=text..string.format("Number of smoke     = %d\n", self.Nsmoke0)
  text=text..string.format("Smoke color         = %d\n", self.smokeColor)
  if self.RearmingGroup or self.RearmingPlaceCoord then
  text=text..string.format("Rearming safe dist. = %d m\n", self.RearmingDistance)
  end
  if self.RearmingGroup then
  text=text..string.format("Rearming group      = %s\n", self.RearmingGroup:GetName())
  text=text..string.format("Rearming group speed= %d km/h\n", self.RearmingGroupSpeed)
  text=text..string.format("Rearming group roads= %s\n", tostring(self.RearmingGroupOnRoad))
  end
  if self.RearmingPlaceCoord then
  local dist=self.InitialCoord:Get2DDistance(self.RearmingPlaceCoord)
  text=text..string.format("Rearming coord dist = %d m\n", dist)
  text=text..string.format("Rearming ARTY roads = %s\n", tostring(self.RearmingArtyOnRoad))
  end
  text=text..string.format("Relocate after fire = %s\n", tostring(self.relocateafterfire))
  text=text..string.format("Relocate min dist.  = %d m\n", self.relocateRmin)
  text=text..string.format("Relocate max dist.  = %d m\n", self.relocateRmax)
  text=text..string.format("Auto move in range  = %s\n", tostring(self.autorelocate))
  text=text..string.format("Auto move dist. max = %.1f km\n", self.autorelocatemaxdist/1000)
  text=text..string.format("Auto move on road   = %s\n", tostring(self.autorelocateonroad))
  text=text..string.format("Marker assignments  = %s\n", tostring(self.markallow))
  text=text..string.format("Marker auth. key    = %s\n", tostring(self.markkey))
  text=text..string.format("Marker readonly     = %s\n", tostring(self.markreadonly))
  text=text..string.format("Clusters:\n")
  for _,cluster in pairs(self.clusters) do
    text=text..string.format("- %s\n", tostring(cluster))
  end
  text=text..string.format("******************************************************\n")
  text=text..string.format("Targets:\n")
  for _, target in pairs(self.targets) do
    text=text..string.format("- %s\n", self:_TargetInfo(target))
    local possible=self:_CheckWeaponTypePossible(target)
    if not possible then
      self:E(self.lid..string.format("WARNING: Selected weapon type %s is not possible", self:_WeaponTypeName(target.weapontype)))
    end
    if self.Debug then
      local zone=ZONE_RADIUS:New(target.name, target.coord:GetVec2(), target.radius)
      zone:BoundZone(180, coalition.side.NEUTRAL)
    end
  end
  text=text..string.format("Moves:\n")
  for i=1,#self.moves do
    text=text..string.format("- %s\n", self:_MoveInfo(self.moves[i]))
  end
  text=text..string.format("******************************************************\n")
  text=text..string.format("Shell types:\n")
  for _,_type in pairs(self.ammoshells) do
    text=text..string.format("- %s\n", _type)
  end
  text=text..string.format("Rocket types:\n")
  for _,_type in pairs(self.ammorockets) do
    text=text..string.format("- %s\n", _type)
  end
  text=text..string.format("Missile types:\n")
  for _,_type in pairs(self.ammomissiles) do
    text=text..string.format("- %s\n", _type)
  end
  text=text..string.format("******************************************************")
  if self.Debug then
    self:I(self.lid..text)
  else
    self:T(self.lid..text)
  end

  -- Set default ROE to weapon hold.
  self.Controllable:OptionROEHoldFire()

  -- Add event handler.
  self:HandleEvent(EVENTS.Shot) --, self._OnEventShot)
  self:HandleEvent(EVENTS.Dead) --, self._OnEventDead)
  --self:HandleEvent(EVENTS.MarkAdded, self._OnEventMarkAdded)

  -- Add DCS event handler - necessary for S_EVENT_MARK_* events. So we only start it, if this was requested.
  if self.markallow then
    world.addEventHandler(self)
  end

  -- Start checking status.
  self:__Status(self.StatusInterval)
end

--- Check the DB for properties of the specified artillery unit type.
-- @param #ARTY self
-- @return #table Properties of the requested artillery type. Returns nil if no matching DB entry could be found.
function ARTY:_CheckDB(displayname)
  for _type,_properties in pairs(ARTY.db) do
    self:T({type=_type, properties=_properties})
    if _type==displayname then
      self:T({type=_type, properties=_properties})
      return _properties
    end
  end
  return nil
end

--- After "Start" event. Initialized ROE and alarm state. Starts the event handler.
-- @param #ARTY self
-- @param #boolean display (Optional) If true, send message to coalition. Default false.
function ARTY:_StatusReport(display)

  -- Set default.
  if display==nil then
    display=false
  end

  -- Get Ammo.
  local Nammo, Nshells, Nrockets, Nmissiles, Narty=self:GetAmmo()
  local Nnukes=self.Nukes
  local Nillu=self.Nillu
  local Nsmoke=self.Nsmoke

  local Tnow=timer.getTime()
  local Clock=self:_SecondsToClock(timer.getAbsTime())

  local text=string.format("\n******************* STATUS ***************************\n")
  text=text..string.format("ARTY group          = %s\n", self.groupname)
  text=text..string.format("Clock               = %s\n", Clock)
  text=text..string.format("FSM state           = %s\n", self:GetState())
  text=text..string.format("Total ammo count    = %d\n", Nammo)
  text=text..string.format("Number of shells    = %d\n", Narty)
  text=text..string.format("Number of rockets   = %d\n", Nrockets)
  text=text..string.format("Number of missiles  = %d\n", Nmissiles)
  text=text..string.format("Number of nukes     = %d\n", Nnukes)
  text=text..string.format("Number of illum.    = %d\n", Nillu)
  text=text..string.format("Number of smoke     = %d\n", Nsmoke)
  if self.currentTarget then
  text=text..string.format("Current Target      = %s\n", tostring(self.currentTarget.name))
  text=text..string.format("Curr. Tgt assigned  = %d\n", Tnow-self.currentTarget.Tassigned)
  else
  text=text..string.format("Current Target      = %s\n", "none")
  end
  text=text..string.format("Nshots curr. Target = %d\n", self.Nshots)
  text=text..string.format("Targets:\n")
  for i=1,#self.targets do
    text=text..string.format("- %s\n", self:_TargetInfo(self.targets[i]))
  end
  if self.currentMove then
  text=text..string.format("Current Move        = %s\n", tostring(self.currentMove.name))
  else
  text=text..string.format("Current Move        = %s\n", "none")
  end
  text=text..string.format("Moves:\n")
  for i=1,#self.moves do
    text=text..string.format("- %s\n", self:_MoveInfo(self.moves[i]))
  end
  text=text..string.format("******************************************************")
  env.info(self.lid..text)
  MESSAGE:New(text, 20):Clear():ToCoalitionIf(self.coalition, display)

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event Handling
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function called during tracking of weapon.
-- @param Wrapper.Weapon#WEAPON weapon Weapon object.
-- @param #ARTY self ARTY object.
-- @param #ARTY.Target target Target of the weapon.
function ARTY._FuncTrack(weapon, self, target)
  
  -- Coordinate and distance to target.
  local _coord=weapon.coordinate
  local _dist=_coord:Get2DDistance(target.coord)
  local _destroyweapon=false
  
  -- Debug
  self:T3(self.lid..string.format("ARTY %s weapon to target dist = %d m", self.groupname,_dist))
  
  if target.weapontype==ARTY.WeaponType.IlluminationShells then
  
    -- Check if within distace.
    if _dist<target.radius then
  
      -- Get random coordinate within certain radius of the target.
      local _cr=target.coord:GetRandomCoordinateInRadius(target.radius)
  
      -- Get random altitude over target.
      local _alt=_cr:GetLandHeight()+math.random(self.illuMinalt, self.illuMaxalt)
  
      -- Adjust explosion height of coordinate.
      local _ci=COORDINATE:New(_cr.x,_alt,_cr.z)
  
      -- Create illumination flare.
      _ci:IlluminationBomb(self.illuPower)
  
      -- Destroy actual shell.
      _destroyweapon=true
    end
  
  elseif target.weapontype==ARTY.WeaponType.SmokeShells then
  
    if _dist<target.radius then
  
      -- Get random coordinate within a certain radius.
      local _cr=_coord:GetRandomCoordinateInRadius(_data.target.radius)
  
      -- Fire smoke at this coordinate.
      _cr:Smoke(self.smokeColor)
  
      -- Destroy actual shell.
      _destroyweapon=true
  
    end
  
  end
  
  if _destroyweapon then
  
    self:T2(self.lid..string.format("ARTY %s destroying shell, stopping timer.", self.groupname))
  
    -- Destroy weapon and stop timer.
    weapon:Destroy()
    
    -- No more tracking.
    weapon.tracking=false

  end

end


--- Function called after impact of weapon.
-- @param Wrapper.Weapon#WEAPON weapon Weapon object.
-- @param #ARTY self ARTY object.
-- @param #ARTY.Target target Target of the weapon.
function ARTY._FuncImpact(weapon, self, target)

  -- Debug info.
  self:I(self.lid..string.format("ARTY %s weapon NOT ALIVE any more.", self.groupname))

  -- Get impact coordinate.
  local _impactcoord=weapon:GetImpactCoordinate()
    
  -- Create a "nuclear" explosion and blast at the impact point.
  if target.weapontype==ARTY.WeaponType.TacticalNukes then
    self:T(self.lid..string.format("ARTY %s triggering nuclear explosion in one second.", self.groupname))
    --SCHEDULER:New(nil, ARTY._NuclearBlast, {self,_impactcoord}, 1.0)
    self:ScheduleOnce(1.0, ARTY._NuclearBlast, self, _impactcoord)
  end

end


--- Eventhandler for shot event.
-- @param #ARTY self
-- @param Core.Event#EVENTDATA EventData
function ARTY:OnEventShot(EventData)
  self:F(EventData)

    -- Weapon data.
  local _weapon = EventData.Weapon:getTypeName()  -- should be the same as Event.WeaponTypeName
  local _weaponStrArray = self:_split(_weapon,"%.")
  local _weaponName = _weaponStrArray[#_weaponStrArray]

  -- Debug info.
  self:T3(self.lid.."EVENT SHOT: Ini unit    = "..EventData.IniUnitName)
  self:T3(self.lid.."EVENT SHOT: Ini group   = "..EventData.IniGroupName)
  self:T3(self.lid.."EVENT SHOT: Weapon type = ".._weapon)
  self:T3(self.lid.."EVENT SHOT: Weapon name = ".._weaponName)

  local group = EventData.IniGroup --Wrapper.Group#GROUP

  if group and group:IsAlive() then

    if EventData.IniGroupName == self.groupname then

      if self.currentTarget then

        -- Increase number of shots fired by this group on this target.
        self.Nshots=self.Nshots+1

        -- Debug output.
        local text=string.format("%s, fired shot %d of %d with weapon %s on target %s.", self.alias, self.Nshots, self.currentTarget.nshells, _weaponName, self.currentTarget.name)
        self:T(self.lid..text)
        MESSAGE:New(text, 5):Clear():ToAllIf(self.report or self.Debug)

        -- Start track the shell if we want to model a tactical nuke.
        local _tracknuke  = self.currentTarget.weapontype==ARTY.WeaponType.TacticalNukes and self.Nukes>0
        local _trackillu  = self.currentTarget.weapontype==ARTY.WeaponType.IlluminationShells and self.Nillu>0
        local _tracksmoke = self.currentTarget.weapontype==ARTY.WeaponType.SmokeShells and self.Nsmoke>0
        
        
        if _tracknuke or _trackillu or _tracksmoke then

          -- Debug info.  
          self:T(self.lid..string.format("ARTY %s: Tracking of weapon starts in two seconds.", self.groupname))
            
          -- Create a weapon object.
          local weapon=WEAPON:New(EventData.weapon)
          
          -- Set time step for tracking.
          weapon:SetTimeStepTrack(self.dtTrack)
          
          -- Copy target. We need a copy because it might already be overwritten with the next target during flight of weapon.
          local target=UTILS.DeepCopy(self.currentTarget)
          
          -- Set callback functions.
          weapon:SetFuncTrack(ARTY._FuncTrack, self, target)
          weapon:SetFuncImpact(ARTY._FuncImpact, self, target)
          
          -- Start tracking in 2 sec (arty ammo should fly a bit).
          weapon:StartTrack(2)
        end

        -- Get current ammo.
        local _nammo,_nshells,_nrockets,_nmissiles,_narty=self:GetAmmo()

        -- Decrease available nukes because we just fired one.
        if self.currentTarget.weapontype==ARTY.WeaponType.TacticalNukes then
          self.Nukes=self.Nukes-1
        end

        -- Decrease available illumination shells because we just fired one.
        if self.currentTarget.weapontype==ARTY.WeaponType.IlluminationShells then
          self.Nillu=self.Nillu-1
        end

        -- Decrease available smoke shells because we just fired one.
        if self.currentTarget.weapontype==ARTY.WeaponType.SmokeShells then
          self.Nsmoke=self.Nsmoke-1
        end

        -- Check if we are completely out of ammo.
        local _outofammo=false
        if _nammo==0 then
          self:T(self.lid..string.format("Group %s completely out of ammo.", self.groupname))
          _outofammo=true
        end

        -- Check if we are out of ammo of the weapon type used for this target.
        -- Note that should not happen because we only open fire with the available number of shots.
        local _partlyoutofammo=self:_CheckOutOfAmmo({self.currentTarget})

        -- Weapon type name for current target.
        local _weapontype=self:_WeaponTypeName(self.currentTarget.weapontype)
        self:T(self.lid..string.format("Group %s ammo: total=%d, shells=%d, rockets=%d, missiles=%d", self.groupname, _nammo, _narty, _nrockets, _nmissiles))
        self:T(self.lid..string.format("Group %s uses weapontype %s for current target.", self.groupname, _weapontype))

        -- Default switches for cease fire and relocation.
        local _ceasefire=false
        local _relocate=false

        -- Check if number of shots reached max.
        if self.Nshots >= self.currentTarget.nshells then

          -- Debug message
          local text=string.format("Group %s stop firing on target %s.", self.groupname, self.currentTarget.name)
          self:T(self.lid..text)
          MESSAGE:New(text, 5):ToAllIf(self.Debug)

          -- Cease fire.
          _ceasefire=true

          -- Relocate if enabled.
          _relocate=self.relocateafterfire
        end

        -- Check if we are (partly) out of ammo.
        if _outofammo or _partlyoutofammo then
          _ceasefire=true
        end

        -- Relocate position.
        if _relocate then
          self:_Relocate()
        end

        -- Cease fire on current target.
        if _ceasefire then
          self:CeaseFire(self.currentTarget)
        end

      else
        self:E(self.lid..string.format("WARNING: No current target for group %s?!", self.groupname))
      end
    end
  end
end


--- After "Start" event. Initialized ROE and alarm state. Starts the event handler.
-- @param #ARTY self
-- @param #table Event
function ARTY:onEvent(Event)

  if Event == nil or Event.idx == nil then
    self:T3("Skipping onEvent. Event or Event.idx unknown.")
    return true
  end

  -- Set battery and coalition.
  --local batteryname=self.groupname
  --local batterycoalition=self.Controllable:GetCoalition()

  self:T2(string.format("Event captured  = %s", tostring(self.groupname)))
  self:T2(string.format("Event id        = %s", tostring(Event.id)))
  self:T2(string.format("Event time      = %s", tostring(Event.time)))
  self:T2(string.format("Event idx       = %s", tostring(Event.idx)))
  self:T2(string.format("Event coalition = %s", tostring(Event.coalition)))
  self:T2(string.format("Event group id  = %s", tostring(Event.groupID)))
  self:T2(string.format("Event text      = %s", tostring(Event.text)))
  if Event.initiator~=nil then
    local _unitname=Event.initiator:getName()
    self:T2(string.format("Event ini unit name = %s", tostring(_unitname)))
  end

  if Event.id==world.event.S_EVENT_MARK_ADDED then
    self:T2({event="S_EVENT_MARK_ADDED", battery=self.groupname, vec3=Event.pos})

  elseif Event.id==world.event.S_EVENT_MARK_CHANGE then
    self:T({event="S_EVENT_MARK_CHANGE", battery=self.groupname, vec3=Event.pos})

    -- Handle event.
    self:_OnEventMarkChange(Event)

  elseif Event.id==world.event.S_EVENT_MARK_REMOVED then
    self:T2({event="S_EVENT_MARK_REMOVED", battery=self.groupname, vec3=Event.pos})

    -- Hande event.
    self:_OnEventMarkRemove(Event)
  end

end

--- Function called when a F10 map mark was removed.
-- @param #ARTY self
-- @param #table Event Event data.
function ARTY:_OnEventMarkRemove(Event)

  -- Get battery coalition and name.
  local batterycoalition=self.coalition
  --local batteryname=self.groupname

  if Event.text~=nil and Event.text:find("BATTERY") then

    -- Init defaults.
    local _cancelmove=false
    local _canceltarget=false
    local _name=""
    local _id=nil

    -- Check for key phrases of relocation or engagements in marker text. If not, return.
    if Event.text:find("Marked Relocation") then
      _cancelmove=true
      _name=self:_MarkMoveName(Event.idx)
      _id=self:_GetMoveIndexByName(_name)
    elseif Event.text:find("Marked Target") then
      _canceltarget=true
      _name=self:_MarkTargetName(Event.idx)
      _id=self:_GetTargetIndexByName(_name)
    else
      return
    end

    -- Check if there is a task which matches.
    if _id==nil then
      return
    end

    -- Check if the coalition is the same or an authorization key has been defined.
    if (batterycoalition==Event.coalition and self.markkey==nil) or self.markkey~=nil then

      -- Authentify key
      local _validkey=self:_MarkerKeyAuthentification(Event.text)

      -- Check if we have the right coalition.
      if _validkey then

        -- This should be the unique name of the target or move.
        if _cancelmove then
          if self.currentMove and self.currentMove.name==_name then
            -- We do clear tasks here because in Arrived() it can cause a CTD if the group did actually arrive!
            self.Controllable:ClearTasks()
            -- Current move is removed here. In contrast to RemoveTarget() there are is no maxengage parameter.
            self:Arrived()
          else
            -- Remove move from queue
            self:RemoveMove(_name)
          end
        elseif _canceltarget then
          if self.currentTarget and self.currentTarget.name==_name then
            -- Cease fire.
            self:CeaseFire(self.currentTarget)
            -- We still need to remove the target, because there might be more planned engagements (maxengage>1).
            self:RemoveTarget(_name)
          else
            -- Remove target from queue
            self:RemoveTarget(_name)
          end
        end

      end
    end
  end
end

--- Function called when a F10 map mark was changed. This happens when a user enters text.
-- @param #ARTY self
-- @param #table Event Event data.
function ARTY:_OnEventMarkChange(Event)

  -- Check if marker has a text and the "arty" keyword.
  if Event.text~=nil and Event.text:lower():find("arty") then

    -- Convert (wrong x-->z, z-->x) vec3
    -- DONE: This needs to be "fixed", once DCS gives the correct numbers for x and z.
    -- Was fixed in DCS 2.5.5.34644!
    local vec3={y=Event.pos.y, x=Event.pos.x, z=Event.pos.z}
    --local vec3={y=Event.pos.y, x=Event.pos.z, z=Event.pos.x}

    -- Get coordinate from vec3.
    local _coord=COORDINATE:NewFromVec3(vec3)

    -- Adjust y component to actual land height. When a coordinate is create it uses y=5 m!
    _coord.y=_coord:GetLandHeight()

    -- Get battery coalition and name.
    local batterycoalition=self.coalition
    local batteryname=self.groupname

    -- Check if the coalition is the same or an authorization key has been defined.
    if (batterycoalition==Event.coalition and self.markkey==nil) or self.markkey~=nil then

      -- Evaluate marker text and extract parameters.
      local _assign=self:_Markertext(Event.text)

      -- Check if ENGAGE or MOVE or REQUEST keywords were found.
      if _assign==nil or not (_assign.engage or _assign.move or _assign.request or _assign.cancel or _assign.set) then
          self:T(self.lid..string.format("WARNING: %s, no keyword ENGAGE, MOVE, REQUEST, CANCEL or SET in mark text! Command will not be executed. Text:\n%s", self.groupname, Event.text))
        return
      end

      -- Check if job is assigned to this ARTY group. Default is for all ARTY groups.
      local _assigned=false

      -- If any array is filled something has been assigned.
      if _assign.everyone then

        -- Everyone was addressed.
        _assigned=true

      else --#_assign.battery>0 or #_assign.aliases>0 or #_assign.cluster>0 then

        -- Loop over batteries.
        for _,bat in pairs(_assign.battery) do
          if self.groupname==bat then
            _assigned=true
          end
        end

        -- Loop over aliases.
        for _,alias in pairs(_assign.aliases) do
          if self.alias==alias then
            _assigned=true
          end
        end

        -- Loop over clusters.
        for _,bat in pairs(_assign.cluster) do
          for _,cluster in pairs(self.clusters) do
            if cluster==bat then
              _assigned=true
            end
          end
        end

      end

      -- We were not addressed.
      if not _assigned then
        self:T3(self.lid..string.format("INFO: ARTY group %s was not addressed! Mark text:\n%s", self.groupname, Event.text))
        return
      else
        if self.Controllable and self.Controllable:IsAlive() then
        
        else
          self:T3(self.lid..string.format("INFO: ARTY group %s was addressed but is NOT alive! Mark text:\n%s", self.groupname, Event.text))
          return
        end
      end

      -- Coordinate was given in text, e.g. as lat, long.
      if _assign.coord then
        _coord=_assign.coord
      end

      -- Check if the authorization key is required and if it is valid.
      local _validkey=self:_MarkerKeyAuthentification(Event.text)

       -- Handle requests and return.
      if _assign.request and _validkey then
        if _assign.requestammo then
          self:_MarkRequestAmmo()
        end
        if _assign.requestmoves then
          self:_MarkRequestMoves()
        end
        if _assign.requesttargets then
          self:_MarkRequestTargets()
        end
        if _assign.requeststatus then
          self:_MarkRequestStatus()
        end
        if _assign.requestrearming then
          self:Rearm()
        end
        -- Requests Done ==> End of story!
        return
      end

      -- Cancel stuff and return.
      if _assign.cancel and _validkey then
        if _assign.cancelmove and self.currentMove then
          self.Controllable:ClearTasks()
          self:Arrived()
        elseif _assign.canceltarget and self.currentTarget then
          self.currentTarget.engaged=self.currentTarget.engaged+1
          self:CeaseFire(self.currentTarget)
        elseif _assign.cancelrearm and self:is("Rearming") then
          local nammo=self:GetAmmo()
          if nammo>0 then
            self:Rearmed()
          else
            self:Winchester()
          end
        end
        -- Cancels Done ==> End of story!
        return
      end

      -- Set stuff and return.
      if _assign.set and _validkey then
        if _assign.setrearmingplace and self.ismobile then
          self:SetRearmingPlace(_coord)
          _coord:RemoveMark(Event.idx)
          _coord:MarkToCoalition(string.format("Rearming place for battery %s", self.groupname), self.coalition, false, string.format("New rearming place for battery %s defined.", self.groupname))
          if self.Debug then
            _coord:SmokeOrange()
          end
        end
        if _assign.setrearminggroup then
          _coord:RemoveMark(Event.idx)
          local rearminggroupcoord=_assign.setrearminggroup:GetCoordinate()
          rearminggroupcoord:MarkToCoalition(string.format("Rearming group for battery %s", self.groupname), self.coalition, false, string.format("New rearming group for battery %s defined.", self.groupname))
          self:SetRearmingGroup(_assign.setrearminggroup)
          if self.Debug then
            rearminggroupcoord:SmokeOrange()
          end
        end
        -- Set stuff Done ==> End of story!
        return
      end

      -- Handle engagements and relocations.
      if _validkey then

        -- Remove old mark because it might contain confidential data such as the key.
        -- Also I don't know who can see the mark which was created.
        _coord:RemoveMark(Event.idx)

        -- Anticipate marker ID.
        -- WARNING: Make sure, no marks are set until the COORDINATE:MarkToCoalition() is called or the target/move name will be wrong and target cannot be removed by deleting its marker.
        local _id=UTILS._MarkID+1

        if _assign.move then

          -- Create a new name. This determins the string we search when deleting a move!
          local _name=self:_MarkMoveName(_id)

          local text=string.format("%s, received new relocation assignment.", self.alias)
          text=text..string.format("\nCoordinates %s",_coord:ToStringLLDMS())
          MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug)

          -- Assign a relocation of the arty group.
          local _movename=self:AssignMoveCoord(_coord, _assign.time, _assign.speed, _assign.onroad, _assign.movecanceltarget,_name, true)

          if _movename~=nil then
            local _mid=self:_GetMoveIndexByName(_movename)
            local _move=self.moves[_mid]

            -- Create new target name.
            local clock=tostring(self:_SecondsToClock(_move.time))
            local _markertext=_movename..string.format(", Time=%s, Speed=%d km/h, Use Roads=%s.", clock, _move.speed, tostring(_move.onroad))

            -- Create a new mark. This will trigger the mark added event.
            local _randomcoord=_coord:GetRandomCoordinateInRadius(100)
            _randomcoord:MarkToCoalition(_markertext, batterycoalition, self.markreadonly or _assign.readonly)
          else
            local text=string.format("%s, relocation not possible.", self.alias)
            MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug)
          end

        else

          -- Create a new name.
          local _name=self:_MarkTargetName(_id)

          local text=string.format("%s, received new target assignment.", self.alias)
          text=text..string.format("\nCoordinates %s",_coord:ToStringLLDMS())
          if _assign.time then
            text=text..string.format("\nTime %s",_assign.time)
          end
          if _assign.prio then
            text=text..string.format("\nPrio %d",_assign.prio)
          end
          if _assign.radius then
            text=text..string.format("\nRadius %d m",_assign.radius)
          end
          if _assign.nshells then
            text=text..string.format("\nShots %d",_assign.nshells)
          end
          if _assign.maxengage then
            text=text..string.format("\nEngagements %d",_assign.maxengage)
          end
          if _assign.weapontype then
            text=text..string.format("\nWeapon %s",self:_WeaponTypeName(_assign.weapontype))
          end
          MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug)

          -- Assign a new firing engagement.
          -- Note, we set unique=true so this target gets only added once.
          local _targetname=self:AssignTargetCoord(_coord,_assign.prio,_assign.radius,_assign.nshells,_assign.maxengage,_assign.time,_assign.weapontype, _name, true)

          if _targetname~=nil then
            local _tid=self:_GetTargetIndexByName(_targetname)
            local _target=self.targets[_tid]

            -- Create new target name.
            local clock=tostring(self:_SecondsToClock(_target.time))
            local weapon=self:_WeaponTypeName(_target.weapontype)
            local _markertext=_targetname..string.format(", Priority=%d, Radius=%d m, Shots=%d, Engagements=%d, Weapon=%s, Time=%s", _target.prio, _target.radius, _target.nshells, _target.maxengage, weapon, clock)

            -- Create a new mark. This will trigger the mark added event.
            local _randomcoord=_coord:GetRandomCoordinateInRadius(250)
            _randomcoord:MarkToCoalition(_markertext, batterycoalition, self.markreadonly or _assign.readonly)
          end
        end
      end

    end
  end

end

--- Event handler for event Dead.
-- @param #ARTY self
-- @param Core.Event#EVENTDATA EventData
function ARTY:OnEventDead(EventData)
  self:F(EventData)

  -- Name of controllable.
  local _name=self.groupname

  -- Check for correct group.
  if EventData and EventData.IniGroupName and EventData.IniGroupName==_name then

    -- Name of the dead unit.
    local unitname=tostring(EventData.IniUnitName)

    -- Dead Unit.
    self:T(self.lid..string.format("%s: Captured dead event for unit %s.", _name, unitname))

    -- FSM Dead event. We give one second for update of data base.
    --self:__Dead(1, unitname)
    self:Dead(unitname)
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events and States
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- After "Status" event. Report status of group.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARTY:onafterStatus(Controllable, From, Event, To)
  self:_EventFromTo("onafterStatus", Event, From, To)
  
  -- Get ammo.
  local nammo, nshells, nrockets, nmissiles, narty=self:GetAmmo()
  
  -- We have a cargo group ==> check if group was loaded into a carrier.
  if self.iscargo and self.cargogroup then
    if self.cargogroup:IsLoaded() and not self:is("InTransit")  then
      -- Group is now InTransit state. Current target is canceled.
      self:T(self.lid..string.format("Group %s has been loaded into a carrier and is now transported.", self.alias))
      self:Loaded()
    elseif self.cargogroup:IsUnLoaded() then
      -- Group has been unloaded and is combat ready again.
      self:T(self.lid..string.format("Group %s has been unloaded from the carrier.", self.alias))
      self:UnLoaded()
    end
  end  

  -- FSM state.
  local fsmstate=self:GetState()
  self:T(self.lid..string.format("Status %s, Ammo total=%d: shells=%d [smoke=%d, illu=%d, nukes=%d*%.3f kT], rockets=%d, missiles=%d", fsmstate, nammo, narty, self.Nsmoke, self.Nillu, self.Nukes, self.nukewarhead/1000000, nrockets, nmissiles))

  if self.Controllable and self.Controllable:IsAlive() then

    -- Debug current status info.
    if self.Debug then
      self:_StatusReport()
    end

    -- Group on the move.
    if self:is("Moving") then
      self:T2(self.lid..string.format("%s: Moving", Controllable:GetName()))
    end

    -- Group is rearming.
    if self:is("Rearming") then
      local _rearmed=self:_CheckRearmed()
      if _rearmed then
        self:T2(self.lid..string.format("%s: Rearming ==> Rearmed", Controllable:GetName()))
        self:Rearmed()
      end
    end

    -- Group finished rearming.
    if self:is("Rearmed") then
      local distance=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord)
      self:T2(self.lid..string.format("%s: Rearmed. Distance ARTY to InitalCoord = %d m", Controllable:GetName(), distance))
      -- Check that ARTY group is back and set it to combat ready.
      if distance <= self.RearmingDistance then
        self:T2(self.lid..string.format("%s: Rearmed ==> CombatReady", Controllable:GetName()))
        self:CombatReady()
      end
    end

    -- Group arrived at destination.
    if self:is("Arrived") then
      self:T2(self.lid..string.format("%s: Arrived ==> CombatReady", Controllable:GetName()))
      self:CombatReady()
    end

    -- Group is firing on target.
    if self:is("Firing") then
      -- Check that firing started after ~5 min. If not, target is removed.
      self:_CheckShootingStarted()
    end

    -- Check if targets are in range and update target.inrange value.
    self:_CheckTargetsInRange()

    -- Check if selected weapon type for target is possible at all. E.g. request rockets for Paladin.
    local notpossible={}
    for i=1,#self.targets do
      local _target=self.targets[i]
      local possible=self:_CheckWeaponTypePossible(_target)
      if not possible then
        table.insert(notpossible, _target.name)
      end
    end
    for _,targetname in pairs(notpossible) do
      self:E(self.lid..string.format("%s: Removing target %s because requested weapon is not possible with this type of unit.", self.groupname, targetname))
      self:RemoveTarget(targetname)
    end

    -- Get a valid timed target if it is due to be attacked.
    local _timedTarget=self:_CheckTimedTargets()

    -- Get a valid normal target (one that is not timed).
    local _normalTarget=self:_CheckNormalTargets()

    -- Get a commaned move to another location.
    local _move=self:_CheckMoves()

    if _move then

      -- Command to move.
      self:Move(_move)

    elseif _timedTarget then

      -- Cease fire on current target first.
      if self.currentTarget then
        self:CeaseFire(self.currentTarget)
      end
      
      if self:is("CombatReady") then
        -- Open fire on timed target.
        self:OpenFire(_timedTarget)
      end
    elseif _normalTarget then
      
      if self:is("CombatReady") then  
        -- Open fire on normal target.
        self:OpenFire(_normalTarget)
      end
    end

    -- Check if we have a target in the queue for which weapons are still available.
    local gotsome=false
    if #self.targets>0 then
      for i=1,#self.targets do
        local _target=self.targets[i]
        if self:_CheckWeaponTypeAvailable(_target)>0 then
          gotsome=true
        end
      end
    else
      -- No targets in the queue.
      gotsome=true
    end

    -- No ammo available. Either completely blank or only queued targets for ammo which is out.
    if (nammo==0 or not gotsome) and not (self:is("Moving") or self:is("Rearming") or self:is("OutOfAmmo")) then
      self:Winchester()
    end

    -- Group is out of ammo.
    if self:is("OutOfAmmo") then
      self:T2(self.lid..string.format("%s: OutOfAmmo ==> Rearm ==> Rearming", Controllable:GetName()))
      self:Rearm()
    end

    -- Call status again in ~10 sec.
    self:__Status(self.StatusInterval)

  elseif self.iscargo then

    -- We have a cargo group ==> check if group was loaded into a carrier.
    if self.cargogroup and self.cargogroup:IsAlive() then

      -- Group is being transported as cargo ==> skip everything and check again in 5 seconds.
      if self:is("InTransit") then
        self:__Status(-5)
      end
      
    end  

  else
    self:E(self.lid..string.format("Arty group %s is not alive!", self.groupname))
  end
  
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Before "Loaded" event. Checks if group is currently firing and removes the target by calling CeaseFire.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @return #boolean If true, proceed to onafterLoaded.
function ARTY:onbeforeLoaded(Controllable, From, Event, To)
  if self.currentTarget then
    self:CeaseFire(self.currentTarget)
  end

  return true
end

--- After "UnLoaded" event. Group is combat ready again.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @return #boolean If true, proceed to onafterLoaded.
function ARTY:onafterUnLoaded(Controllable, From, Event, To)
  self:CombatReady()
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Enter "CombatReady" state. Route the group back if necessary.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARTY:onenterCombatReady(Controllable, From, Event, To)
  self:_EventFromTo("onenterCombatReady", Event, From, To)
  -- Debug info
  self:T3(self.lid..string.format("onenterComabReady, from=%s, event=%s, to=%s", From, Event, To))
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Before "OpenFire" event. Checks if group already has a target. Checks for valid min/max range and removes the target if necessary.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #table target Array holding the target info.
-- @return #boolean If true, proceed to onafterOpenfire.
function ARTY:onbeforeOpenFire(Controllable, From, Event, To, target)
  self:_EventFromTo("onbeforeOpenFire", Event, From, To)

  -- Check that group has no current target already.
  if self.currentTarget then
    -- This should not happen. Some earlier check failed.
    self:E(self.lid..string.format("ERROR: Group %s already has a target %s!", self.groupname, self.currentTarget.name))
    -- Deny transition.
    return false
  end

  -- Check if target is in range.
  if not self:_TargetInRange(target) then
    -- This should not happen. Some earlier check failed.
    self:E(self.lid..string.format("ERROR: Group %s, target %s is out of range!", self.groupname, self.currentTarget.name))
    -- Deny transition.
    return false
  end

  -- Get the number of available shells, rockets or missiles requested for this target.
  local nfire=self:_CheckWeaponTypeAvailable(target)

  -- Adjust if less than requested ammo is left.
  target.nshells=math.min(target.nshells, nfire)

  -- No ammo left ==> deny transition.
  if target.nshells<1 then
    local text=string.format("%s, no ammo left to engage target %s with selected weapon type %s.")
    return false
  end

  return true
end

--- After "OpenFire" event. Sets the current target and starts the fire at point task.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #ARTY.Target target Array holding the target info.
function ARTY:onafterOpenFire(Controllable, From, Event, To, target)
  self:_EventFromTo("onafterOpenFire", Event, From, To)

  -- Get target array index.
  local id=self:_GetTargetIndexByName(target.name)

  -- Target is now under fire and has been engaged once more.
  if id then
    -- Set under fire flag.
    self.targets[id].underfire=true
    -- Set current target.
    self.currentTarget=target
    -- Set time the target was assigned.
    self.currentTarget.Tassigned=timer.getTime()
  end

  -- Distance to target
  local range=Controllable:GetCoordinate():Get2DDistance(target.coord)

  -- Get ammo.
  local Nammo, Nshells, Nrockets, Nmissiles, Narty=self:GetAmmo()
  local nfire=Narty
  local _type="shots"
  if target.weapontype==ARTY.WeaponType.Auto then
    nfire=Nammo  -- We take everything that is available
    _type="shots"
  elseif target.weapontype==ARTY.WeaponType.Cannon then
    nfire=Narty
    _type="shells"
  elseif target.weapontype==ARTY.WeaponType.TacticalNukes then
    nfire=self.Nukes
    _type="nuclear shells"
  elseif target.weapontype==ARTY.WeaponType.IlluminationShells then
    nfire=self.Nillu
    _type="illumination shells"
  elseif target.weapontype==ARTY.WeaponType.SmokeShells then
    nfire=self.Nsmoke
    _type="smoke shells"
  elseif target.weapontype==ARTY.WeaponType.Rockets then
    nfire=Nrockets
    _type="rockets"
  elseif target.weapontype==ARTY.WeaponType.CruiseMissile then
    nfire=Nmissiles
    _type="cruise missiles"
  end
  
  --env.info(string.format("FF type=%s, Nrockets=%d, Nfire=%d target.nshells=%d", _type, Nrockets, nfire, target.nshells))

  -- Adjust if less than requested ammo is left.
  target.nshells=math.min(target.nshells, nfire)

  -- Send message.
  local text=string.format("%s, opening fire on target %s with %d %s. Distance %.1f km.", Controllable:GetName(), target.name, target.nshells, _type, range/1000)
  self:T(self.lid..text)
  MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report)

  --if self.Debug then
  --  local _coord=target.coord --Core.Point#COORDINATE
  --  local text=string.format("ARTY %s, Target %s, n=%d, weapon=%s", self.Controllable:GetName(), target.name, target.nshells, self:_WeaponTypeName(target.weapontype))
  --  _coord:MarkToAll(text)
  --end

  -- Start firing.
  if target.attackgroup then
    self:_AttackGroup(target)
  else
    self:_FireAtCoord(target.coord, target.radius, target.nshells, target.weapontype)
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- After "CeaseFire" event. Clears task of the group and removes the target if max engagement was reached.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #table target Array holding the target info.
function ARTY:onafterCeaseFire(Controllable, From, Event, To, target)
  self:_EventFromTo("onafterCeaseFire", Event, From, To)

  if target then

    -- Send message.
    local text=string.format("%s, ceasing fire on target %s.", Controllable:GetName(), target.name)
    self:T(self.lid..text)
    MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report)

    -- Get target array index.
    local id=self:_GetTargetIndexByName(target.name)

    -- We have a target.
    if id then
      -- Target was actually engaged. (Could happen that engagement was aborted while group was still aiming.)
      if self.Nshots>0 then
        self.targets[id].engaged=self.targets[id].engaged+1
        -- Clear the attack time.
        self.targets[id].time=nil
      end
      -- Target is not under fire any more.
      self.targets[id].underfire=false
    end

    -- If number of engagements has been reached, the target is removed.
    if target.engaged >= target.maxengage then
      self:RemoveTarget(target.name)
    end

    -- Set ROE to weapon hold.
    self.Controllable:OptionROEHoldFire()

    -- Clear tasks.
    self.Controllable:ClearTasks()

  else
    self:E(self.lid..string.format("ERROR: No target in cease fire for group %s.", self.groupname))
  end

  -- Set number of shots to zero.
  self.Nshots=0

  -- ARTY group has no current target any more.
  self.currentTarget=nil

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- After "Winchester" event. Group is out of ammo. Trigger "Rearm" event.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARTY:onafterWinchester(Controllable, From, Event, To)
  self:_EventFromTo("onafterWinchester", Event, From, To)

  -- Send message.
  local text=string.format("%s, winchester!", Controllable:GetName())
  self:T(self.lid..text)
  MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug)

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Before "Rearm" event. Check if a unit to rearm the ARTY group has been defined.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @return #boolean If true, proceed to onafterRearm.
function ARTY:onbeforeRearm(Controllable, From, Event, To)
  self:_EventFromTo("onbeforeRearm", Event, From, To)

  local _rearmed=self:_CheckRearmed()
  if _rearmed then
    self:T(self.lid..string.format("%s, group is already armed to the teeth. Rearming request denied!", self.groupname))
    return false
  else
    self:T(self.lid..string.format("%s, group might be rearmed.", self.groupname))
  end

  -- Check if a reaming unit or rearming place was specified.
  if self.RearmingGroup and self.RearmingGroup:IsAlive() then
    return true
  elseif self.RearmingPlaceCoord then
    return true
  else
    return false
  end

end

--- After "Rearm" event. Send message if reporting is on. Route rearming unit to ARTY group.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARTY:onafterRearm(Controllable, From, Event, To)
  self:_EventFromTo("onafterRearm", Event, From, To)

     -- Coordinate of ARTY unit.
    local coordARTY=self.Controllable:GetCoordinate()

    -- Remember current coordinates so that we find our way back home.
    self.InitialCoord=coordARTY

    -- Coordinate of rearming group.
    local coordRARM=nil
    if self.RearmingGroup then
      -- Coordinate of the rearming unit.
      coordRARM=self.RearmingGroup:GetCoordinate()
      -- Remember the coordinates of the rearming unit. After rearming it will go back to this position.
      self.RearmingGroupCoord=coordRARM
    end

    if self.RearmingGroup and self.RearmingPlaceCoord and self.ismobile then

      -- CASE 1: Rearming unit and ARTY group meet at rearming place.

      -- Send message.
      local text=string.format("%s, %s, request rearming at rearming place.", Controllable:GetName(), self.RearmingGroup:GetName())
      self:T(self.lid..text)
      MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug)

      -- Distances.
      local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord)
      local dR=coordRARM:Get2DDistance(self.RearmingPlaceCoord)

      -- Route ARTY group to rearming place.
      if dA > self.RearmingDistance then
        local _tocoord=self:_VicinityCoord(self.RearmingPlaceCoord, self.RearmingDistance/4, self.RearmingDistance/2)
        self:AssignMoveCoord(_tocoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE TO REARMING PLACE", true)
      end

      -- Route Rearming group to rearming place.
      if dR > self.RearmingDistance then
        local ToCoord=self:_VicinityCoord(self.RearmingPlaceCoord, self.RearmingDistance/4, self.RearmingDistance/2)
        self:_Move(self.RearmingGroup, ToCoord, self.RearmingGroupSpeed, self.RearmingGroupOnRoad)
      end

    elseif self.RearmingGroup then

      -- CASE 2: Rearming unit drives to ARTY group.

      -- Send message.
      local text=string.format("%s, %s, request rearming.", Controllable:GetName(), self.RearmingGroup:GetName())
      self:T(self.lid..text)
      MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug)

      -- Distance between ARTY group and rearming unit.
      local distance=coordARTY:Get2DDistance(coordRARM)

      -- If distance is larger than ~100 m, the Rearming unit is routed to the ARTY group.
      if distance > self.RearmingDistance then

        -- Route rearming group to ARTY group.
        self:_Move(self.RearmingGroup, self:_VicinityCoord(coordARTY), self.RearmingGroupSpeed, self.RearmingGroupOnRoad)
      end

    elseif self.RearmingPlaceCoord then

      -- CASE 3: ARTY drives to rearming place.

      -- Send message.
      local text=string.format("%s, moving to rearming place.", Controllable:GetName())
      self:T(self.lid..text)
      MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug)

      -- Distance.
      local dA=coordARTY:Get2DDistance(self.RearmingPlaceCoord)

      -- Route ARTY group to rearming place.
      if dA > self.RearmingDistance then
        local _tocoord=self:_VicinityCoord(self.RearmingPlaceCoord)
        self:AssignMoveCoord(_tocoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE TO REARMING PLACE", true)
      end

    end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- After "Rearmed" event. Send ARTY and rearming group back to their inital positions.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARTY:onafterRearmed(Controllable, From, Event, To)
  self:_EventFromTo("onafterRearmed", Event, From, To)

  -- Send message.
  local text=string.format("%s, rearming complete.", Controllable:GetName())
  self:T(self.lid..text)
  MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug)

  -- "Rearm" tactical nukes as well.
  self.Nukes=self.Nukes0
  self.Nillu=self.Nillu0
  self.Nsmoke=self.Nsmoke0

  -- Route ARTY group back to where it came from (if distance is > 100 m).
  local dist=self.Controllable:GetCoordinate():Get2DDistance(self.InitialCoord)
  if dist > self.RearmingDistance then
    self:AssignMoveCoord(self.InitialCoord, nil, nil, self.RearmingArtyOnRoad, false, "REARMING MOVE REARMING COMPLETE", true)
  end

  -- Route unit back to where it came from (if distance is > 100 m).
  if self.RearmingGroup and self.RearmingGroup:IsAlive() then
    local d=self.RearmingGroup:GetCoordinate():Get2DDistance(self.RearmingGroupCoord)
    if d > self.RearmingDistance then
      self:_Move(self.RearmingGroup, self.RearmingGroupCoord, self.RearmingGroupSpeed, self.RearmingGroupOnRoad)
    else
      -- Clear tasks.
      self.RearmingGroup:ClearTasks()
    end
  end

end

--- Check if ARTY group is rearmed, i.e. has its full amount of ammo.
-- @param #ARTY self
-- @return #boolean True if rearming is complete, false otherwise.
function ARTY:_CheckRearmed()
  self:F2()

  -- Get current ammo.
  local nammo,nshells,nrockets,nmissiles,narty=self:GetAmmo()

  -- Number of units still alive.
  local units=self.Controllable:GetUnits()
  local nunits=0
  if units then
    nunits=#units
  end

  -- Full Ammo count.
  local FullAmmo=self.Nammo0 * nunits / self.IniGroupStrength

  -- Rearming status in per cent.
  local _rearmpc=nammo/FullAmmo*100

  -- Send message if rearming > 1% complete
  if _rearmpc>1 then
    local text=string.format("%s, rearming %d %% complete.", self.alias, _rearmpc)
    self:T(self.lid..text)
    MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug)
  end

  -- Return if ammo is full.
  -- TODO: Strangely, I got the case that a Paladin got one more shell than it can max carry, i.e. 40 not 39 when rearming when it still had some ammo left. Need to report.
  if nammo>=FullAmmo then
    return true
  else
    return false
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Before "Move" event. Check if a unit to rearm the ARTY group has been defined.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #table move Table containing the move parameters.
-- @param Core.Point#COORDINATE ToCoord Coordinate to which the ARTY group should move.
-- @param #boolean OnRoad If true group should move on road mainly.
-- @return #boolean If true, proceed to onafterMove.
function ARTY:onbeforeMove(Controllable, From, Event, To, move)
  self:_EventFromTo("onbeforeMove", Event, From, To)

  -- Check if group can actually move...
  if not self.ismobile then
    return false
  end

  -- Check if group is engaging.
  if self.currentTarget then
    if move.cancel then
      -- Cancel current target.
      self:CeaseFire(self.currentTarget)
    else
      -- We should not cancel.
      return false
    end
  end

  return true
end

--- After "Move" event. Route group to given coordinate.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #table move Table containing the move parameters.
function ARTY:onafterMove(Controllable, From, Event, To, move)
  self:_EventFromTo("onafterMove", Event, From, To)

  -- Set alarm state to green and ROE to weapon hold.
  self.Controllable:OptionAlarmStateGreen()
  self.Controllable:OptionROEHoldFire()

  -- Take care of max speed.
  local _Speed=math.min(move.speed, self.SpeedMax)

  -- Smoke coordinate
  if self.Debug then
    move.coord:SmokeRed()
  end

  -- Set current move.
  self.currentMove=move

  -- Route group to coordinate.
  self:_Move(self.Controllable, move.coord, move.speed, move.onroad)

end

--- After "Arrived" event. Group has reached its destination.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARTY:onafterArrived(Controllable, From, Event, To)
  self:_EventFromTo("onafterArrived", Event, From, To)

  -- Set alarm state to auto.
  self.Controllable:OptionAlarmStateAuto()

  -- WARNING: calling ClearTasks() here causes CTD of DCS when move is over. Dont know why? combotask?
  --self.Controllable:ClearTasks()

  -- Send message
  local text=string.format("%s, arrived at destination.", Controllable:GetName())
  self:T(self.lid..text)
  MESSAGE:New(text, 10):ToCoalitionIf(self.coalition, self.report or self.Debug)

  -- Remove executed move from queue.
  if self.currentMove then
    self:RemoveMove(self.currentMove.name)
    self.currentMove=nil
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- After "NewTarget" event.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #table target Array holding the target parameters.
function ARTY:onafterNewTarget(Controllable, From, Event, To, target)
  self:_EventFromTo("onafterNewTarget", Event, From, To)

  -- Debug message.
  local text=string.format("Adding new target %s.", target.name)
  MESSAGE:New(text, 5):ToAllIf(self.Debug)
  self:T(self.lid..text)
end

--- After "NewMove" event.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #table move Array holding the move parameters.
function ARTY:onafterNewMove(Controllable, From, Event, To, move)
  self:_EventFromTo("onafterNewTarget", Event, From, To)

  -- Debug message.
  local text=string.format("Adding new move %s.", move.name)
  MESSAGE:New(text, 5):ToAllIf(self.Debug)
  self:T(self.lid..text)
end


--- After "Dead" event, when a unit has died. When all units of a group are dead trigger "Stop" event.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string Unitname Name of the unit that died.
function ARTY:onafterDead(Controllable, From, Event, To, Unitname)
  self:_EventFromTo("onafterDead", Event, From, To)

  -- Number of units still alive.
  --local nunits=self.Controllable and self.Controllable:CountAliveUnits() or 0
  local nunits=self.Controllable:CountAliveUnits()

  -- Message.
  local text=string.format("%s, our unit %s just died! %d units left.", self.groupname, Unitname, nunits)
  MESSAGE:New(text, 5):ToAllIf(self.Debug)
  self:I(self.lid..text)

  -- Go to stop state.
  if nunits==0 then

      -- Cease Fire on current target.
    if self.currentTarget then
      self:CeaseFire(self.currentTarget)
    end

    if self.respawnafterdeath then
      -- Respawn group.
      if not self.respawning then
        self.respawning=true
        self:__Respawn(self.respawndelay or 1)
      end
    else
      -- Stop FSM.
      self:Stop()
    end
  end

end


--- After "Dead" event, when a unit has died. When all units of a group are dead trigger "Stop" event.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARTY:onafterRespawn(Controllable, From, Event, To)
  self:_EventFromTo("onafterRespawn", Event, From, To)
  self:I("Respawning arty group")
  local group=self.Controllable --Wrapper.Group#GROUP

  -- Respawn group.
  self.Controllable=group:Respawn()

  self.respawning=false

  -- Call status again.
  self:__Status(-1)
end

--- After "Stop" event. Unhandle events and cease fire on current target.
-- @param #ARTY self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARTY:onafterStop(Controllable, From, Event, To)
  self:_EventFromTo("onafterStop", Event, From, To)

  -- Debug info.
  self:I(self.lid..string.format("Stopping ARTY FSM for group %s.", tostring(Controllable:GetName())))

    -- Cease Fire on current target.
  if self.currentTarget then
    self:CeaseFire(self.currentTarget)
  end

  -- Remove all targets.
  --self:RemoveAllTargets()

  -- Unhandle event.
  self:UnHandleEvent(EVENTS.Shot)
  self:UnHandleEvent(EVENTS.Dead)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set task for firing at a coordinate.
-- @param #ARTY self
-- @param Core.Point#COORDINATE coord Coordinates to fire upon.
-- @param #number radius Radius around coordinate.
-- @param #number nshells Number of shells to fire.
-- @param #number weapontype Type of weapon to use.
function ARTY:_FireAtCoord(coord, radius, nshells, weapontype)
  self:F({coord=coord, radius=radius, nshells=nshells})

  -- Controllable.
  local group=self.Controllable --Wrapper.Group#GROUP

  -- Tactical nukes are actually cannon shells.
  if weapontype==ARTY.WeaponType.TacticalNukes or weapontype==ARTY.WeaponType.IlluminationShells or weapontype==ARTY.WeaponType.SmokeShells then
    weapontype=ARTY.WeaponType.Cannon
  end
  
  if group:HasTask() then
    group:ClearTasks()
  end
  
  -- Set ROE to weapon free.
  group:OptionROEOpenFire()

  -- Get Vec2
  local vec2=coord:GetVec2()

  -- Get task.
  local fire=group:TaskFireAtPoint(vec2, radius, nshells, weapontype)

  -- Execute task.
  group:SetTask(fire,1)
end

--- Set task for attacking a group.
-- @param #ARTY self
-- @param #ARTY.Target target Target data.
function ARTY:_AttackGroup(target)

  -- Controllable.
  local group=self.Controllable --Wrapper.Group#GROUP

  local weapontype=target.weapontype

  -- Tactical nukes are actually cannon shells.
  if weapontype==ARTY.WeaponType.TacticalNukes or weapontype==ARTY.WeaponType.IlluminationShells or weapontype==ARTY.WeaponType.SmokeShells then
    weapontype=ARTY.WeaponType.Cannon
  end
  
  if group:HasTask() then
    group:ClearTasks()
  end
    
  -- Set ROE to weapon free.
  group:OptionROEOpenFire()

  -- Target group.
  local targetgroup=GROUP:FindByName(target.name)

  -- Get task.
  local fire=group:TaskAttackGroup(targetgroup, weapontype, AI.Task.WeaponExpend.ONE, 1)

  -- Execute task.
  group:SetTask(fire,1)
end



--- Model a nuclear blast/destruction by creating fires and destroy scenery.
-- @param #ARTY self
-- @param Core.Point#COORDINATE _coord Coordinate of the impact point (center of the blast).
function ARTY:_NuclearBlast(_coord)

  local S0=self.nukewarhead
  local R0=self.nukerange

  -- Number of fires
  local N0=self.nukefires

  -- Create an explosion at the last known position.
  _coord:Explosion(S0)

  -- Huge fire at direct impact point.
  --if self.nukefire then
  _coord:BigSmokeAndFireHuge()
  --end

  -- Create a table of fire coordinates within the demolition zone.
  local _fires={}
  for i=1,N0 do
    local _fire=_coord:GetRandomCoordinateInRadius(R0)
    local _dist=_fire:Get2DDistance(_coord)
    table.insert(_fires, {distance=_dist, coord=_fire})
  end

  -- Sort scenery wrt to distance from impact point.
  local _sort = function(a,b) return a.distance < b.distance end
  table.sort(_fires,_sort)

  local function _explosion(R)
    -- At R=R0 ==> explosion strength is 1% of S0 at impact point.
    local alpha=math.log(100)
    local strength=S0*math.exp(-alpha*R/R0)
    self:T2(self.lid..string.format("Nuclear explosion strength s(%.1f m) = %.5f (s/s0=%.1f %%), alpha=%.3f", R, strength, strength/S0*100, alpha))
    return strength
  end

  local function ignite(_fires)
    for _,fire in pairs(_fires) do
      local _fire=fire.coord --Core.Point#COORDINATE

      -- Get distance to impact and calc exponential explosion strength.
      local R=_fire:Get2DDistance(_coord)
      local S=_explosion(R)
      self:T2(self.lid..string.format("Explosion r=%.1f, s=%.3f", R, S))

      -- Get a random Big Smoke and fire object.
      local _preset=math.random(0,7)
      local _density=S/S0 --math.random()+0.1

      _fire:BigSmokeAndFire(_preset,_density)
      _fire:Explosion(S)

    end
  end

  if self.nukefire==true then
    ignite(_fires)
  end

end

--- Route group to a certain point.
-- @param #ARTY self
-- @param Wrapper.Group#GROUP group Group to route.
-- @param Core.Point#COORDINATE ToCoord Coordinate where we want to go.
-- @param #number Speed (Optional) Speed in km/h. Default is 70% of max speed the group can do.
-- @param #boolean OnRoad If true, use (mainly) roads.
function ARTY:_Move(group, ToCoord, Speed, OnRoad)
  self:F2({group=group:GetName(), Speed=Speed, OnRoad=OnRoad})

  -- Clear all tasks.
  group:ClearTasks()
  group:OptionAlarmStateGreen()
  group:OptionROEHoldFire()

  -- Set formation.
  local formation = "Off Road"

  -- Get max speed of group.
  local SpeedMax=group:GetSpeedMax()

  -- Set speed.
  Speed=Speed or SpeedMax*0.7

  -- Make sure, we do not go above max speed possible.
  Speed=math.min(Speed, SpeedMax)

  -- Current coordinates of group.
  local cpini=group:GetCoordinate() -- Core.Point#COORDINATE

  -- Distance between current and final point.
  local dist=cpini:Get2DDistance(ToCoord)

  -- Waypoint and task arrays.
  local path={}
  local task={}

  -- First waypoint is the current position of the group.
  path[#path+1]=cpini:WaypointGround(Speed, formation)
  task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false)

  -- Route group on road if requested.
  if OnRoad then

    -- Get path on road.
    local _pathonroad=cpini:GetPathOnRoad(ToCoord)

    -- Check if we actually got a path. There are situations where nil is returned. In that case, we go directly.
    if _pathonroad then

      -- Just take the first and last point.
      local _first=_pathonroad[1]
      local _last=_pathonroad[#_pathonroad]

      if self.Debug then
        _first:SmokeGreen()
        _last:SmokeGreen()
      end

      -- First point on road.
      path[#path+1]=_first:WaypointGround(Speed, "On Road")
      task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false)

      -- Last point on road.
      path[#path+1]=_last:WaypointGround(Speed, "On Road")
      task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, false)
    end

  end

  -- Last waypoint at ToCoord.
  path[#path+1]=ToCoord:WaypointGround(Speed, formation)
  task[#task+1]=group:TaskFunction("ARTY._PassingWaypoint", self, #path-1, true)

  --if self.Debug then
  --  cpini:SmokeBlue()
  --  ToCoord:SmokeBlue()
  --end

  -- Init waypoints of the group.
  local Waypoints={}

  -- New points are added to the default route.
  for i=1,#path do
    table.insert(Waypoints, i, path[i])
  end

  -- Set task for all waypoints.
  for i=1,#Waypoints do
    group:SetTaskWaypoint(Waypoints[i], task[i])
  end

  -- Submit task and route group along waypoints.
  group:Route(Waypoints)

end

--- Function called when group is passing a waypoint.
-- @param Wrapper.Group#GROUP group Group for which waypoint passing should be monitored.
-- @param #ARTY arty ARTY object.
-- @param #number i Waypoint number that has been reached.
-- @param #boolean final True if it is the final waypoint.
function ARTY._PassingWaypoint(group, arty, i, final)

  if group and group:IsAlive() then
  
    local groupname=tostring(group:GetName())

    -- Debug message.
    local text=string.format("%s, passing waypoint %d.", groupname, i)
    if final then
      text=string.format("%s, arrived at destination.", groupname)
    end
    arty:T(arty.lid..text)
  
    -- Arrived event.
    if final and arty.groupname==groupname then
      arty:Arrived()
    end
    
  end
end

--- Relocate to another position, e.g. after an engagement to avoid couter strikes.
-- @param #ARTY self
function ARTY:_Relocate()

  -- Current position.
  local _pos=self.Controllable:GetCoordinate()

  local _new=nil
  local _gotit=false
  local _n=0
  local _nmax=1000
  repeat
    -- Get a random coordinate.
    _new=_pos:GetRandomCoordinateInRadius(self.relocateRmax, self.relocateRmin)
    local _surface=_new:GetSurfaceType()

    -- Check that new coordinate is not water(-ish).
    if _surface~=land.SurfaceType.WATER and _surface~=land.SurfaceType.SHALLOW_WATER then
      _gotit=true
    end
    -- Increase counter.
    _n=_n+1
  until _gotit or _n>_nmax

  -- Assign relocation.
  if _gotit then
    self:AssignMoveCoord(_new, nil, nil, false, false, "RELOCATION MOVE AFTER FIRING")
  end
end

--- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up.
-- @param #ARTY self
-- @param #boolean display Display ammo table as message to all. Default false.
-- @return #number Total amount of ammo the whole group has left.
-- @return #number Number of shells the group has left.
-- @return #number Number of rockets the group has left.
-- @return #number Number of missiles the group has left.
-- @return #number Number of artillery shells the group has left.
function ARTY:GetAmmo(display)
  self:F3({display=display})

  -- Default is display false.
  if display==nil then
    display=false
  end

  -- Init counter.
  local nammo=0
  local nshells=0
  local nrockets=0
  local nmissiles=0
  local nartyshells=0

  -- Get all units.
  local units=self.Controllable:GetUnits()
  if units==nil then
    return nammo, nshells, nrockets, nmissiles
  end

  for _,_unit in pairs(units) do
    local unit=_unit --Wrapper.Unit#UNIT

    if unit then

      -- Output.
      local text=string.format("ARTY group %s - unit %s:\n", self.groupname, unit:GetName())

      -- Get ammo table.
      local ammotable=unit:GetAmmo()

      if ammotable ~= nil then

        local weapons=#ammotable

        -- Display ammo table
        if display then
          self:I(self.lid..string.format("Number of weapons %d.", weapons))
          self:I({ammotable=ammotable})
          self:I(self.lid.."Ammotable:")
          for id,bla in pairs(ammotable) do
            self:I({id=id, ammo=bla})
          end
        end

        -- Loop over all weapons.
        for w=1,weapons do

          -- Number of current weapon.
          local Nammo=ammotable[w]["count"]

          -- Typename of current weapon
          local Tammo=ammotable[w]["desc"]["typeName"]

          local _weaponString = self:_split(Tammo,"%.")
          local _weaponName   = _weaponString[#_weaponString]

          -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3
          local Category=ammotable[w].desc.category

          -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6
          local MissileCategory=nil
          if Category==Weapon.Category.MISSILE then
            MissileCategory=ammotable[w].desc.missileCategory
          end


          -- Check for correct shell type.
          local _gotshell=false
          if #self.ammoshells>0 then
            -- User explicitly specified the valid type(s) of shells.
            for _,_type in pairs(self.ammoshells) do
              if string.match(Tammo, _type) and Category==Weapon.Category.SHELL then
                _gotshell=true
              end
            end
          else
            if Category==Weapon.Category.SHELL then
              _gotshell=true
            end
          end

          -- Check for correct rocket type.
          local _gotrocket=false
          if #self.ammorockets>0 then
            for _,_type in pairs(self.ammorockets) do
              if string.match(Tammo, _type) and Category==Weapon.Category.ROCKET then
                _gotrocket=true
              end
            end
          else
            if Category==Weapon.Category.ROCKET then
              _gotrocket=true
            end
          end

          -- Check for correct missile type.
          local _gotmissile=false
          if #self.ammomissiles>0 then
            for _,_type in pairs(self.ammomissiles) do
              if string.match(Tammo,_type)  and Category==Weapon.Category.MISSILE then
                _gotmissile=true
              end
            end
          else
            if Category==Weapon.Category.MISSILE then
              _gotmissile=true
            end
          end

          -- We are specifically looking for shells or rockets here.
          if _gotshell then

            -- Add up all shells.
            nshells=nshells+Nammo
            local _,_,_,_,_,shells = unit:GetAmmunition()
            nartyshells=nartyshells+shells
            -- Debug info.
            text=text..string.format("- %d shells of type %s\n", Nammo, _weaponName)

          elseif _gotrocket then

            -- Add up all rockets.
            nrockets=nrockets+Nammo

            -- Debug info.
            text=text..string.format("- %d rockets of type %s\n", Nammo, _weaponName)

          elseif _gotmissile then

            -- Add up all cruise missiles (category 5)
            if MissileCategory==Weapon.MissileCategory.CRUISE then
              nmissiles=nmissiles+Nammo
            end

            -- Debug info.
            text=text..string.format("- %d %s missiles of type %s\n", Nammo, self:_MissileCategoryName(MissileCategory), _weaponName)

          else

            -- Debug info.
            text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory))

          end

        end
      end

      -- Debug text and send message.
      if display then
        self:I(self.lid..text)
      else
        self:T3(self.lid..text)
      end
      MESSAGE:New(text, 10):ToAllIf(display)

    end
  end

  -- Total amount of ammunition.
  nammo=nshells+nrockets+nmissiles

  return nammo, nshells, nrockets, nmissiles, nartyshells
end

--- Returns a name of a missile category.
-- @param #ARTY self
-- @param #number categorynumber Number of missile category from weapon missile category enumerator. See https://wiki.hoggitworld.com/view/DCS_Class_Weapon
-- @return #string Missile category name.
function ARTY:_MissileCategoryName(categorynumber)
  local cat="unknown"
  if categorynumber==Weapon.MissileCategory.AAM then
    cat="air-to-air"
  elseif categorynumber==Weapon.MissileCategory.SAM then
    cat="surface-to-air"
  elseif categorynumber==Weapon.MissileCategory.BM then
    cat="ballistic"
  elseif categorynumber==Weapon.MissileCategory.ANTI_SHIP then
    cat="anti-ship"
  elseif categorynumber==Weapon.MissileCategory.CRUISE then
    cat="cruise"
  elseif categorynumber==Weapon.MissileCategory.OTHER then
    cat="other"
  end
  return cat
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Mark Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Extract engagement assignments and parameters from mark text.
-- @param #ARTY self
-- @param #string text Marker text.
-- @return #boolean If true, authentification successful.
function ARTY:_MarkerKeyAuthentification(text)

  -- Set battery and coalition.
  --local batteryname=self.groupname
  local batterycoalition=self.coalition

  -- Get assignment.
  local mykey=nil
  if self.markkey~=nil then

    -- keywords are split by ","
    local keywords=self:_split(text, ",")
    for _,key in pairs(keywords) do
      local s=self:_split(key, " ")
      local val=s[2]
      if key:lower():find("key") then
        mykey=tonumber(val)
        self:T(self.lid..string.format("Authorisation Key=%s.", val))
      end
    end

  end

  -- Check if the authorization key is required and if it is valid.
  local _validkey=true

  -- Check if group needs authorization.
  if self.markkey~=nil then
    -- Assume key is incorrect.
    _validkey=false

    -- If key was found, check if matches.
    if mykey~=nil then
      _validkey=self.markkey==mykey
    end
    self:T2(self.lid..string.format("%s, authkey=%s == %s=playerkey ==> valid=%s", self.groupname, tostring(self.markkey), tostring(mykey), tostring(_validkey)))

    -- Send message
    local text=""
    if mykey==nil then
      text=string.format("%s, authorization required but did not receive a key!", self.alias)
    elseif _validkey==false then
      text=string.format("%s, authorization required but did receive an incorrect key (key=%s)!", self.alias, tostring(mykey))
    elseif _validkey==true then
      text=string.format("%s, authentification successful!", self.alias)
    end
    MESSAGE:New(text, 10):ToCoalitionIf(batterycoalition, self.report or self.Debug)
  end

  return _validkey
end

--- Extract engagement assignments and parameters from mark text.
-- @param #ARTY self
-- @param #string text Marker text to be analyzed.
-- @return #table Table with assignment parameters, e.g. number of shots, radius, time etc.
function ARTY:_Markertext(text)
  self:F(text)

  -- Assignment parameters.
  local assignment={}
  assignment.battery={}
  assignment.aliases={}
  assignment.cluster={}
  assignment.everyone=false
  assignment.move=false
  assignment.engage=false
  assignment.request=false
  assignment.cancel=false
  assignment.set=false
  assignment.readonly=false
  assignment.movecanceltarget=false
  assignment.cancelmove=false
  assignment.canceltarget=false
  assignment.cancelrearm=false
  assignment.setrearmingplace=false
  assignment.setrearminggroup=false

  -- Check for correct keywords.
  if text:lower():find("arty engage") or text:lower():find("arty attack") then
    assignment.engage=true
  elseif text:lower():find("arty move") or text:lower():find("arty relocate") then
    assignment.move=true
  elseif text:lower():find("arty request") then
    assignment.request=true
  elseif text:lower():find("arty cancel") then
    assignment.cancel=true
  elseif text:lower():find("arty set") then
    assignment.set=true
  else
    self:E(self.lid..'ERROR: Neither "ARTY ENGAGE" nor "ARTY MOVE" nor "ARTY RELOCATE" nor "ARTY REQUEST" nor "ARTY CANCEL" nor "ARTY SET" keyword specified!')
    return nil
  end

  -- keywords are split by ","
  local keywords=self:_split(text, ",")
  self:T({keywords=keywords})

  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=self:_split(keyphrase, " ")
    local key=str[1]
    local val=str[2]

    -- Debug output.
    self:T3(self.lid..string.format("%s, keyphrase = %s, key = %s, val = %s", self.groupname, tostring(keyphrase), tostring(key), tostring(val)))

    -- Battery name, i.e. which ARTY group should fire.
    if key:lower():find("battery") then

      local v=self:_split(keyphrase, '"')

      for i=2,#v,2 do
        table.insert(assignment.battery, v[i])
        self:T2(self.lid..string.format("Key Battery=%s.", v[i]))
      end

    elseif key:lower():find("alias") then

      local v=self:_split(keyphrase, '"')

      for i=2,#v,2 do
        table.insert(assignment.aliases, v[i])
        self:T2(self.lid..string.format("Key Aliases=%s.", v[i]))
      end

    elseif key:lower():find("cluster") then

      local v=self:_split(keyphrase, '"')

      for i=2,#v,2 do
        table.insert(assignment.cluster, v[i])
        self:T2(self.lid..string.format("Key Cluster=%s.", v[i]))
      end

    elseif keyphrase:lower():find("everyone") or keyphrase:lower():find("all batteries") or keyphrase:lower():find("allbatteries") then

      assignment.everyone=true
      self:T(self.lid..string.format("Key Everyone=true."))

    elseif keyphrase:lower():find("irrevocable") or keyphrase:lower():find("readonly") then

      assignment.readonly=true
      self:T2(self.lid..string.format("Key Readonly=true."))

    elseif (assignment.engage or assignment.move) and key:lower():find("time") then

      if val:lower():find("now") then
        assignment.time=self:_SecondsToClock(timer.getTime0()+2)
      else
        assignment.time=val
      end
      self:T2(self.lid..string.format("Key Time=%s.", val))

    elseif assignment.engage and key:lower():find("shot") then

      assignment.nshells=tonumber(val)
      self:T(self.lid..string.format("Key Shot=%s.", val))

    elseif assignment.engage and key:lower():find("prio") then

      assignment.prio=tonumber(val)
      self:T2(string.format("Key Prio=%s.", val))

    elseif assignment.engage and key:lower():find("maxengage") then

      assignment.maxengage=tonumber(val)
      self:T2(self.lid..string.format("Key Maxengage=%s.", val))

    elseif assignment.engage and key:lower():find("radius") then

      assignment.radius=tonumber(val)
      self:T2(self.lid..string.format("Key Radius=%s.", val))

    elseif assignment.engage and key:lower():find("weapon") then

      if val:lower():find("cannon") then
        assignment.weapontype=ARTY.WeaponType.Cannon
      elseif val:lower():find("rocket") then
        assignment.weapontype=ARTY.WeaponType.Rockets
      elseif val:lower():find("missile") then
        assignment.weapontype=ARTY.WeaponType.CruiseMissile
      elseif val:lower():find("nuke") then
        assignment.weapontype=ARTY.WeaponType.TacticalNukes
      elseif val:lower():find("illu") then
        assignment.weapontype=ARTY.WeaponType.IlluminationShells
      elseif val:lower():find("smoke") then
        assignment.weapontype=ARTY.WeaponType.SmokeShells
      else
        assignment.weapontype=ARTY.WeaponType.Auto
      end
      self:T2(self.lid..string.format("Key Weapon=%s.", val))

    elseif (assignment.move or assignment.set) and key:lower():find("speed") then

      assignment.speed=tonumber(val)
      self:T2(self.lid..string.format("Key Speed=%s.", val))

    elseif (assignment.move or assignment.set) and (keyphrase:lower():find("on road") or keyphrase:lower():find("onroad") or keyphrase:lower():find("use road")) then

      assignment.onroad=true
      self:T2(self.lid..string.format("Key Onroad=true."))

    elseif assignment.move and (keyphrase:lower():find("cancel target") or keyphrase:lower():find("canceltarget")) then

      assignment.movecanceltarget=true
      self:T2(self.lid..string.format("Key Cancel Target (before move)=true."))

    elseif assignment.request and keyphrase:lower():find("rearm") then

      assignment.requestrearming=true
      self:T2(self.lid..string.format("Key Request Rearming=true."))

    elseif assignment.request and keyphrase:lower():find("ammo") then

      assignment.requestammo=true
      self:T2(self.lid..string.format("Key Request Ammo=true."))

    elseif assignment.request and keyphrase:lower():find("target") then

      assignment.requesttargets=true
      self:T2(self.lid..string.format("Key Request Targets=true."))

    elseif assignment.request and keyphrase:lower():find("status") then

      assignment.requeststatus=true
      self:T2(self.lid..string.format("Key Request Status=true."))

    elseif assignment.request and (keyphrase:lower():find("move") or keyphrase:lower():find("relocation")) then

      assignment.requestmoves=true
      self:T2(self.lid..string.format("Key Request Moves=true."))

    elseif assignment.cancel and (keyphrase:lower():find("engagement") or keyphrase:lower():find("attack") or keyphrase:lower():find("target")) then

      assignment.canceltarget=true
      self:T2(self.lid..string.format("Key Cancel Target=true."))

    elseif assignment.cancel and (keyphrase:lower():find("move") or keyphrase:lower():find("relocation")) then

      assignment.cancelmove=true
      self:T2(self.lid..string.format("Key Cancel Move=true."))

    elseif assignment.cancel and keyphrase:lower():find("rearm") then

      assignment.cancelrearm=true
      self:T2(self.lid..string.format("Key Cancel Rearm=true."))

    elseif assignment.set and keyphrase:lower():find("rearming place") then

      assignment.setrearmingplace=true
      self:T(self.lid..string.format("Key Set Rearming Place=true."))

    elseif assignment.set and keyphrase:lower():find("rearming group") then

      local v=self:_split(keyphrase, '"')
      local groupname=v[2]

      local group=GROUP:FindByName(groupname)
      if group and group:IsAlive() then
        assignment.setrearminggroup=group
      end

      self:T2(self.lid..string.format("Key Set Rearming Group = %s.", tostring(groupname)))

    elseif key:lower():find("lldms") then

      local _flat = "%d+:%d+:%d+%s*[N,S]"
      local _flon = "%d+:%d+:%d+%s*[W,E]"
      local _lat=keyphrase:match(_flat)
      local _lon=keyphrase:match(_flon)
      self:T2(self.lid..string.format("Key LLDMS: lat=%s, long=%s  format=DMS", _lat,_lon))

      if _lat and _lon then

        -- Convert DMS string to DD numbers format.
        local _latitude, _longitude=self:_LLDMS2DD(_lat, _lon)
        self:T2(self.lid..string.format("Key LLDMS: lat=%.3f, long=%.3f  format=DD", _latitude,_longitude))

        -- Convert LL to coordinate object.
        if _latitude and _longitude then
          assignment.coord=COORDINATE:NewFromLLDD(_latitude,_longitude)
        end

      end
    end
  end

  return assignment
end

--- Request ammo via mark.
-- @param #ARTY self
function ARTY:_MarkRequestAmmo()
  self:GetAmmo(true)
end

--- Request status via mark.
-- @param #ARTY self
function ARTY:_MarkRequestStatus()
  self:_StatusReport(true)
end

--- Request Moves.
-- @param #ARTY self
function ARTY:_MarkRequestMoves()
  local text=string.format("%s, relocations:", self.groupname)
  if #self.moves>0 then
    for _,move in pairs(self.moves) do
      if self.currentMove and move.name == self.currentMove.name then
        text=text..string.format("\n- %s (current)", self:_MoveInfo(move))
      else
        text=text..string.format("\n- %s", self:_MoveInfo(move))
      end
    end
  else
    text=text..string.format("\n- no queued relocations")
  end
  MESSAGE:New(text, 20):Clear():ToCoalition(self.coalition)
end

--- Request Targets.
-- @param #ARTY self
function ARTY:_MarkRequestTargets()
  local text=string.format("%s, targets:", self.groupname)
  if #self.targets>0 then
    for _,target in pairs(self.targets) do
      if self.currentTarget and target.name == self.currentTarget.name then
        text=text..string.format("\n- %s (current)", self:_TargetInfo(target))
      else
        text=text..string.format("\n- %s", self:_TargetInfo(target))
      end
    end
  else
    text=text..string.format("\n- no queued targets")
  end
  MESSAGE:New(text, 20):Clear():ToCoalition(self.coalition)
end

--- Create a name for an engagement initiated by placing a marker.
-- @param #ARTY self
-- @param #number markerid ID of the placed marker.
-- @return #string Name of target engagement.
function ARTY:_MarkTargetName(markerid)
  return string.format("BATTERY=%s, Marked Target ID=%d", self.groupname, markerid)
end

--- Create a name for a relocation move initiated by placing a marker.
-- @param #ARTY self
-- @param #number markerid ID of the placed marker.
-- @return #string Name of relocation move.
function ARTY:_MarkMoveName(markerid)
  return string.format("BATTERY=%s, Marked Relocation ID=%d", self.groupname, markerid)
end

--- Get the marker ID from the assigned task name.
-- @param #ARTY self
-- @param #string name Name of the assignment.
-- @return #string Name of the ARTY group or nil
-- @return #number ID of the marked target or nil.
-- @return #number ID of the marked relocation move or nil
function ARTY:_GetMarkIDfromName(name)

    -- keywords are split by ","
    local keywords=self:_split(name, ",")

    local battery=nil
    local markTID=nil
    local markMID=nil

    for _,key in pairs(keywords) do

      local str=self:_split(key, "=")
      local par=str[1]
      local val=str[2]

      if par:find("BATTERY") then
        battery=val
      end
      if par:find("Marked Target ID") then
        markTID=tonumber(val)
      end
      if par:find("Marked Relocation ID") then
        markMID=tonumber(val)
      end

    end

    return battery, markTID, markMID
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Helper Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Sort targets with respect to priority and number of times it was already engaged.
-- @param #ARTY self
function ARTY:_SortTargetQueuePrio()
  self:F2()

  -- Sort results table wrt times they have already been engaged.
  local function _sort(a, b)
    return (a.engaged < b.engaged) or (a.engaged==b.engaged and a.prio < b.prio)
  end
  table.sort(self.targets, _sort)

  -- Debug output.
  self:T3(self.lid.."Sorted targets wrt prio and number of engagements:")
  for i=1,#self.targets do
    local _target=self.targets[i]
    self:T3(self.lid..string.format("Target %s", self:_TargetInfo(_target)))
  end
end

--- Sort array with respect to time. Array elements must have a .time entry.
-- @param #ARTY self
-- @param #table queue Array to sort. Should have elemnt .time.
function ARTY:_SortQueueTime(queue)
  self:F3({queue=queue})

  -- Sort targets w.r.t attack time.
  local function _sort(a, b)
    if a.time == nil and b.time == nil then
      return false
    end
    if a.time == nil then
      return false
    end
    if b.time == nil then
      return true
    end
    return a.time < b.time
  end
  table.sort(queue, _sort)

  -- Debug output.
  self:T3(self.lid.."Sorted queue wrt time:")
  for i=1,#queue do
    local _queue=queue[i]
    local _time=tostring(_queue.time)
    local _clock=tostring(self:_SecondsToClock(_queue.time))
    self:T3(self.lid..string.format("%s: time=%s, clock=%s", _queue.name, _time, _clock))
  end

end

--- Heading from point a to point b in degrees.
--@param #ARTY self
--@param Core.Point#COORDINATE a Coordinate.
--@param Core.Point#COORDINATE b Coordinate.
--@return #number angle Angle from a to b in degrees.
function ARTY:_GetHeading(a, b)
  local dx = b.x-a.x
  local dy = b.z-a.z
  local angle = math.deg(math.atan2(dy,dx))
  if angle < 0 then
    angle = 360 + angle
  end
  return angle
end

--- Check all targets whether they are in range.
-- @param #ARTY self
function ARTY:_CheckTargetsInRange()

  local targets2delete={}

  for i=1,#self.targets do
    local _target=self.targets[i]

    self:T3(self.lid..string.format("Before: Target %s - in range = %s", _target.name, tostring(_target.inrange)))

    -- Check if target is in range.
    local _inrange,_toofar,_tooclose,_remove=self:_TargetInRange(_target)
    self:T3(self.lid..string.format("Inbetw: Target %s - in range = %s, toofar = %s, tooclose = %s", _target.name, tostring(_target.inrange), tostring(_toofar), tostring(_tooclose)))

    if _remove then

      -- The ARTY group is immobile and not cargo but the target is not in range!
      table.insert(targets2delete, _target.name)

    else

      -- Init default for assigning moves into range.
      local _movetowards=false
      local _moveaway=false

      if _target.inrange==nil then

        -- First time the check is performed. We call the function again and send a message.
        _target.inrange,_toofar,_tooclose=self:_TargetInRange(_target, self.report or self.Debug)

        -- Send group towards/away from target.
        if _toofar then
          _movetowards=true
        elseif _tooclose then
          _moveaway=true
        end

      elseif _target.inrange==true then

        -- Target was in range at previous check...

        if _toofar then       --...but is now too far away.
          _movetowards=true
        elseif _tooclose then --...but is now too close.
          _moveaway=true
        end

      elseif _target.inrange==false then

        -- Target was out of range at previous check.

        if _inrange then
          -- Inform coalition that target is now in range.
          local text=string.format("%s, target %s is now in range.", self.alias, _target.name)
          self:T(self.lid..text)
          MESSAGE:New(text,10):ToCoalitionIf(self.coalition, self.report or self.Debug)
        end

      end

      -- Assign a relocation command so that the unit will be in range of the requested target.
      if self.autorelocate and (_movetowards or _moveaway) then

        -- Get current position.
        local _from=self.Controllable:GetCoordinate()
        local _dist=_from:Get2DDistance(_target.coord)

        if _dist<=self.autorelocatemaxdist then

          local _tocoord --Core.Point#COORDINATE
          local _name=""
          local _safetymargin=500

          if _movetowards then

            -- Target was in range on previous check but now we are too far away.
            local _waytogo=_dist-self.maxrange+_safetymargin
            local _heading=self:_GetHeading(_from,_target.coord)
            _tocoord=_from:Translate(_waytogo, _heading)
            _name=string.format("%s, relocation to within max firing range of target %s", self.alias, _target.name)

          elseif _moveaway then

            -- Target was in range on previous check but now we are too far away.
            local _waytogo=_dist-self.minrange+_safetymargin
            local _heading=self:_GetHeading(_target.coord,_from)
            _tocoord=_from:Translate(_waytogo, _heading)
            _name=string.format("%s, relocation to within min firing range of target %s", self.alias, _target.name)

          end

          -- Send info message.
          MESSAGE:New(_name.." assigned.", 10):ToCoalitionIf(self.coalition, self.report or self.Debug)

          -- Assign relocation move.
          self:AssignMoveCoord(_tocoord, nil, nil, self.autorelocateonroad, false, _name, true)

        end

      end

      -- Update value.
      _target.inrange=_inrange

      self:T3(self.lid..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange)))
    end
  end

  -- Remove targets not in range.
  for _,targetname in pairs(targets2delete) do
    self:RemoveTarget(targetname)
  end

end

--- Check all normal (untimed) targets and return the target with the highest priority which has been engaged the fewest times.
-- @param #ARTY self
-- @return #table Target which is due to be attacked now or nil if no target could be found.
function ARTY:_CheckNormalTargets()
  self:F3()

  -- Sort targets w.r.t. prio and number times engaged already.
  self:_SortTargetQueuePrio()

  -- No target engagements if rearming!
  if self:is("Rearming") then
    return nil
  end

  -- Loop over all sorted targets.
  for i=1,#self.targets do
    local _target=self.targets[i]

    -- Debug info.
    self:T3(self.lid..string.format("Check NORMAL target %d: %s", i, self:_TargetInfo(_target)))

    -- Check that target no time, is not under fire currently and in range.
    if _target.underfire==false and _target.time==nil and _target.maxengage > _target.engaged and self:_TargetInRange(_target) and self:_CheckWeaponTypeAvailable(_target)>0 then

      -- Debug info.
      self:T2(self.lid..string.format("Found NORMAL target %s", self:_TargetInfo(_target)))

      return _target
    end
  end

  return nil
end

--- Check all timed targets and return the target which should be attacked next.
-- @param #ARTY self
-- @return #table Target which is due to be attacked now.
function ARTY:_CheckTimedTargets()
  self:F3()

  -- Current time.
  local Tnow=timer.getAbsTime()

  -- Sort Targets wrt time.
  self:_SortQueueTime(self.targets)

  -- No target engagements if rearming!
  if self:is("Rearming") then
    return nil
  end

  for i=1,#self.targets do
    local _target=self.targets[i]

    -- Debug info.
    self:T3(self.lid..string.format("Check TIMED target %d: %s", i, self:_TargetInfo(_target)))

    -- Check if target has an attack time which has already passed. Also check that target is not under fire already and that it is in range.
    if _target.time and Tnow>=_target.time and _target.underfire==false and self:_TargetInRange(_target) and self:_CheckWeaponTypeAvailable(_target)>0 then

      -- Check if group currently has a target and whether its priorty is lower than the timed target.
      if self.currentTarget then
        if self.currentTarget.prio > _target.prio then
          -- Current target under attack but has lower priority than this target.
          self:T2(self.lid..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInfo(_target)))
          return _target
        end
      else
        -- No current target.
        self:T2(self.lid..string.format("Found TIMED target %s.", self:_TargetInfo(_target)))
        return _target
      end
    end

  end

  return nil
end

--- Check all moves and return the one which should be executed next.
-- @param #ARTY self
-- @return #table Move which is due.
function ARTY:_CheckMoves()
  self:F3()

  -- Current time.
  local Tnow=timer.getAbsTime()

  -- Sort Targets wrt time.
  self:_SortQueueTime(self.moves)

  -- Check if we are currently firing.
  local firing=false
  if self.currentTarget then
    firing=true
  end

  -- Loop over all moves in queue.
  for i=1,#self.moves do

    -- Shortcut.
    local _move=self.moves[i]

    if string.find(_move.name, "REARMING MOVE") and ((self.currentMove and self.currentMove.name~=_move.name) or self.currentMove==nil) then
      -- We got an rearming assignment which has priority.
      return _move
    elseif (Tnow >= _move.time) and (firing==false or _move.cancel) and (not self.currentMove) and (not self:is("Rearming")) then
      -- Time for move is reached and maybe current target should be cancelled.
      return _move
    end
  end

  return nil
end

--- Check whether shooting started within a certain time (~5 min). If not, the current target is considered invalid and removed from the target list.
-- @param #ARTY self
function ARTY:_CheckShootingStarted()
  self:F2()

  if self.currentTarget then

    -- Current time.
    local Tnow=timer.getTime()

    -- Get name and id of target.
    local name=self.currentTarget.name

    -- Time that passed after current target has been assigned.
    local dt=Tnow-self.currentTarget.Tassigned

    -- Debug info
    if self.Nshots==0 then
      self:T(self.lid..string.format("%s, waiting for %d seconds for first shot on target %s.", self.groupname, dt, name))
    end

    -- Check if we waited long enough and no shot was fired.
    --if dt > self.WaitForShotTime and self.Nshots==0 then
    
    self:T(string.format("dt = %d WaitTime = %d | shots = %d TargetShells = %d",dt,self.WaitForShotTime,self.Nshots,self.currentTarget.nshells))
    
    if (dt > self.WaitForShotTime and self.Nshots==0) or (self.currentTarget.nshells <= self.Nshots) then  --https://github.com/FlightControl-Master/MOOSE/issues/1356

      -- Debug info.
      self:T(self.lid..string.format("%s, no shot event after %d seconds. Removing current target %s from list.", self.groupname, self.WaitForShotTime, name))

      -- CeaseFire.
      self:CeaseFire(self.currentTarget)

      -- Remove target from list.
      self:RemoveTarget(name)

    end
  end
end

--- Get the index of a target by its name.
-- @param #ARTY self
-- @param #string name Name of target.
-- @return #number Arrayindex of target.
function ARTY:_GetTargetIndexByName(name)
  self:F2(name)

  for i=1,#self.targets do
    local targetname=self.targets[i].name
    self:T3(self.lid..string.format("Have target with name %s. Index = %d", targetname, i))
    if targetname==name then
      self:T2(self.lid..string.format("Found target with name %s. Index = %d", name, i))
      return i
    end
  end

  self:T2(self.lid..string.format("WARNING: Target with name %s could not be found. (This can happen.)", name))
  return nil
end

--- Get the index of a move by its name.
-- @param #ARTY self
-- @param #string name Name of move.
-- @return #number Arrayindex of move.
function ARTY:_GetMoveIndexByName(name)
  self:F2(name)

  for i=1,#self.moves do
    local movename=self.moves[i].name
    self:T3(self.lid..string.format("Have move with name %s. Index = %d", movename, i))
    if movename==name then
      self:T2(self.lid..string.format("Found move with name %s. Index = %d", name, i))
      return i
    end
  end

  self:T2(self.lid..string.format("WARNING: Move with name %s could not be found. (This can happen.)", name))
  return nil
end

--- Check if group is (partly) out of ammo of a special weapon type.
-- @param #ARTY self
-- @param #table targets Table of targets.
-- @return @boolean True if any target requests a weapon type that is empty.
function ARTY:_CheckOutOfAmmo(targets)

  -- Get current ammo.
  local _nammo,_nshells,_nrockets,_nmissiles,_narty=self:GetAmmo()

   -- Special weapon type requested ==> Check if corresponding ammo is empty.
  local _partlyoutofammo=false

  for _,Target in pairs(targets) do

    if Target.weapontype==ARTY.WeaponType.Auto and _nammo==0 then

      self:T(self.lid..string.format("Group %s, auto weapon requested for target %s but all ammo is empty.", self.groupname, Target.name))
      _partlyoutofammo=true

    elseif Target.weapontype==ARTY.WeaponType.Cannon and _narty==0 then

      self:T(self.lid..string.format("Group %s, cannons requested for target %s but shells empty.", self.groupname, Target.name))
      _partlyoutofammo=true

    elseif Target.weapontype==ARTY.WeaponType.TacticalNukes and self.Nukes<=0 then

      self:T(self.lid..string.format("Group %s, tactical nukes requested for target %s but nukes empty.", self.groupname, Target.name))
      _partlyoutofammo=true

    elseif Target.weapontype==ARTY.WeaponType.IlluminationShells and self.Nillu<=0 then

      self:T(self.lid..string.format("Group %s, illumination shells requested for target %s but illumination shells empty.", self.groupname, Target.name))
      _partlyoutofammo=true

    elseif Target.weapontype==ARTY.WeaponType.SmokeShells and self.Nsmoke<=0 then

      self:T(self.lid..string.format("Group %s, smoke shells requested for target %s but smoke shells empty.", self.groupname, Target.name))
      _partlyoutofammo=true

    elseif Target.weapontype==ARTY.WeaponType.Rockets and _nrockets==0 then

      self:T(self.lid..string.format("Group %s, rockets requested for target %s but rockets empty.", self.groupname, Target.name))
      _partlyoutofammo=true

    elseif Target.weapontype==ARTY.WeaponType.CruiseMissile and _nmissiles==0 then

      self:T(self.lid..string.format("Group %s, cruise missiles requested for target %s but all missiles empty.", self.groupname, Target.name))
      _partlyoutofammo=true

    end

  end

  return _partlyoutofammo
end

--- Check if a selected weapon type is available for this target, i.e. if the current amount of ammo of this weapon type is currently available.
-- @param #ARTY self
-- @param #boolean target Target array data structure.
-- @return #number Amount of shells, rockets or missiles available of the weapon type selected for the target.
function ARTY:_CheckWeaponTypeAvailable(target)

  -- Get current ammo of group.
  local Nammo, Nshells, Nrockets, Nmissiles, Narty=self:GetAmmo()

  -- Check if enough ammo is there for the selected weapon type.
  local nfire=Nammo
  if target.weapontype==ARTY.WeaponType.Auto then
    nfire=Nammo
  elseif target.weapontype==ARTY.WeaponType.Cannon then
    nfire=Narty
  elseif target.weapontype==ARTY.WeaponType.TacticalNukes then
    nfire=self.Nukes
  elseif target.weapontype==ARTY.WeaponType.IlluminationShells then
    nfire=self.Nillu
  elseif target.weapontype==ARTY.WeaponType.SmokeShells then
    nfire=self.Nsmoke
  elseif target.weapontype==ARTY.WeaponType.Rockets then
    nfire=Nrockets
  elseif target.weapontype==ARTY.WeaponType.CruiseMissile then
    nfire=Nmissiles
  end

  return nfire
end
--- Check if a selected weapon type is in principle possible for this group. The current amount of ammo might be zero but the group still can be rearmed at a later point in time.
-- @param #ARTY self
-- @param #boolean target Target array data structure.
-- @return #boolean True if the group can carry this weapon type, false otherwise.
function ARTY:_CheckWeaponTypePossible(target)

  -- Check if enough ammo is there for the selected weapon type.
  local possible=false
  if target.weapontype==ARTY.WeaponType.Auto then
    possible=self.Nammo0>0
  elseif target.weapontype==ARTY.WeaponType.Cannon then
    possible=self.Nshells0>0
  elseif target.weapontype==ARTY.WeaponType.TacticalNukes then
    possible=self.Nukes0>0
  elseif target.weapontype==ARTY.WeaponType.IlluminationShells then
    possible=self.Nillu0>0
  elseif target.weapontype==ARTY.WeaponType.SmokeShells then
    possible=self.Nsmoke0>0
  elseif target.weapontype==ARTY.WeaponType.Rockets then
    possible=self.Nrockets0>0
  elseif target.weapontype==ARTY.WeaponType.CruiseMissile then
    possible=self.Nmissiles0>0
  end

  return possible
end

--- Check if a name is unique. If not, a new unique name can be created by adding a running index #01, #02, ...
-- @param #ARTY self
-- @param #table givennames Table with entries of already given names. Must contain a .name item.
-- @param #string name Name to check if it already exists in givennames table.
-- @param #boolean makeunique If true, a new unique name is returned by appending the running index.
-- @return #string Unique name, which is not already given for another target.
function ARTY:_CheckName(givennames, name, makeunique)
  self:F2({givennames=givennames, name=name})

  local newname=name
  local counter=1
  local n=1
  local nmax=100
  if makeunique==nil then
    makeunique=true
  end

  repeat -- until a unique name is found.

    -- We assume the name is unique.
    local _unique=true

    -- Loop over all targets already defined.
    for _,_target in pairs(givennames) do

      -- Target name.
      local _givenname=_target.name

      -- Name is already used by another target.
      if _givenname==newname then

        -- Name is already used for another target ==> try again with new name.
        _unique=false

      end

      -- Debug info.
      self:T3(self.lid..string.format("%d: givenname = %s, newname=%s, unique = %s, makeunique = %s", n, tostring(_givenname), newname, tostring(_unique), tostring(makeunique)))
    end

    -- Create a new name if requested and try again.
    if _unique==false and makeunique==true then

      -- Define newname = "name #01"
      newname=string.format("%s #%02d", name, counter)

      -- Increase counter.
      counter=counter+1
    end

    -- Name is not unique and we don't want to make it unique.
    if _unique==false and makeunique==false then
      self:T3(self.lid..string.format("Name %s is not unique. Return false.", tostring(newname)))

      -- Return
      return name, false
    end

    -- Increase loop counter. We try max 100 times.
    n=n+1
  until (_unique or n==nmax)

  -- Debug output and return new name.
  self:T3(self.lid..string.format("Original name %s, new name = %s", name, newname))
  return newname, true
end

--- Check if target is in range.
-- @param #ARTY self
-- @param #table target Target table.
-- @param #boolean message (Optional) If true, send a message to the coalition if the target is not in range. Default is no message is send.
-- @return #boolean True if target is in range, false otherwise.
-- @return #boolean True if ARTY group is too far away from the target, i.e. distance > max firing range.
-- @return #boolean True if ARTY group is too close to the target, i.e. distance < min finring range.
-- @return #boolean True if target should be removed since ARTY group is immobile and not cargo.
function ARTY:_TargetInRange(target, message)
  self:F3(target)

  -- Default is no message.
  if message==nil then
    message=false
  end

  -- Distance between ARTY group and target.
  self:T3({controllable=self.Controllable, targetcoord=target.coord})
  local _dist=self.Controllable:GetCoordinate():Get2DDistance(target.coord)

  -- Assume we are in range.
  local _inrange=true
  local _tooclose=false
  local _toofar=false
  local text=""

  if _dist < self.minrange then
    _inrange=false
    _tooclose=true
    text=string.format("%s, target is out of range. Distance of %.1f km is below min range of %.1f km.", self.alias, _dist/1000, self.minrange/1000)
  elseif _dist > self.maxrange then
    _inrange=false
    _toofar=true
    text=string.format("%s, target is out of range. Distance of %.1f km is greater than max range of %.1f km.", self.alias, _dist/1000, self.maxrange/1000)
  end

  -- Debug output.
  if not _inrange then
    self:T(self.lid..text)
    MESSAGE:New(text, 5):ToCoalitionIf(self.coalition, (self.report and message) or (self.Debug and message))
  end

  -- Remove target if ARTY group cannot move, e.g. Mortas. No chance to be ever in range - unless they are cargo.
  local _remove=false
  if not (self.ismobile or self.iscargo) and _inrange==false then
    --self:RemoveTarget(target.name)
    _remove=true
  end

  return _inrange,_toofar,_tooclose,_remove
end

--- Get the weapon type name, which should be used to attack the target.
-- @param #ARTY self
-- @param #number tnumber Number of weapon type ARTY.WeaponType.XXX
-- @return #number tnumber of weapon type.
function ARTY:_WeaponTypeName(tnumber)
  self:F2(tnumber)
  local name="unknown"
  if tnumber==ARTY.WeaponType.Auto then
    name="Auto" -- (Cannon, Rockets, Missiles)
  elseif tnumber==ARTY.WeaponType.Cannon then
    name="Cannons"
  elseif tnumber==ARTY.WeaponType.Rockets then
    name="Rockets"
  elseif tnumber==ARTY.WeaponType.CruiseMissile then
    name="Cruise Missiles"
  elseif tnumber==ARTY.WeaponType.TacticalNukes then
    name="Tactical Nukes"
  elseif tnumber==ARTY.WeaponType.IlluminationShells then
    name="Illumination Shells"
  elseif tnumber==ARTY.WeaponType.SmokeShells then
    name="Smoke Shells"
  end
  return name
end

--- Find a random coordinate in the vicinity of another coordinate.
-- @param #ARTY self
-- @param Core.Point#COORDINATE coord Center coordinate.
-- @param #number rmin (Optional) Minimum distance in meters from center coordinate. Default 20 m.
-- @param #number rmax (Optional) Maximum distance in meters from center coordinate. Default 80 m.
-- @return Core.Point#COORDINATE Random coordinate in a certain distance from center coordinate.
function ARTY:_VicinityCoord(coord, rmin, rmax)
  self:F2({coord=coord, rmin=rmin, rmax=rmax})
  -- Set default if necessary.
  rmin=rmin or 20
  rmax=rmax or 80
  -- Random point withing range.
  local vec2=coord:GetRandomVec2InRadius(rmax, rmin)
  local pops=COORDINATE:NewFromVec2(vec2)
  -- Debug info.
  self:T3(self.lid..string.format("Vicinity distance = %d (rmin=%d, rmax=%d)", pops:Get2DDistance(coord), rmin, rmax))
  return pops
end

--- Print event-from-to string to DCS log file.
-- @param #ARTY self
-- @param #string BA Before/after info.
-- @param #string Event Event.
-- @param #string From From state.
-- @param #string To To state.
function ARTY:_EventFromTo(BA, Event, From, To)
  local text=string.format("%s: %s EVENT %s: %s --> %s", BA, self.groupname, Event, From, To)
  self:T3(self.lid..text)
end

--- Split string. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua
-- @param #ARTY self
-- @param #string str Sting to split.
-- @param #string sep Speparator for split.
-- @return #table Split text.
function ARTY:_split(str, sep)
  self:F3({str=str, sep=sep})
  local result = {}
  local regex = ("([^%s]+)"):format(sep)
  for each in str:gmatch(regex) do
    table.insert(result, each)
  end
  return result
end

--- Returns the target parameters as formatted string.
-- @param #ARTY self
-- @param #ARTY.Target target The target data.
-- @return #string name, prio, radius, nshells, engaged, maxengage, time, weapontype
function ARTY:_TargetInfo(target)
  local clock=tostring(self:_SecondsToClock(target.time))
  local weapon=self:_WeaponTypeName(target.weapontype)
  local _underfire=tostring(target.underfire)
  return string.format("%s: prio=%d, radius=%d, nshells=%d, engaged=%d/%d, weapontype=%s, time=%s, underfire=%s, attackgroup=%s",
  target.name, target.prio, target.radius, target.nshells, target.engaged, target.maxengage, weapon, clock,_underfire, tostring(target.attackgroup))
end

--- Returns a formatted string with information about all move parameters.
-- @param #ARTY self
-- @param #table move Move table item.
-- @return #string Info string.
function ARTY:_MoveInfo(move)
  self:F3(move)
  local _clock=self:_SecondsToClock(move.time)
  return string.format("%s: time=%s, speed=%d, onroad=%s, cancel=%s", move.name, _clock, move.speed, tostring(move.onroad), tostring(move.cancel))
end

--- Convert Latitude and Lontigude from DMS to DD.
-- @param #ARTY self
-- @param #string l1 Latitude or longitude as string in the format DD:MM:SS N/S/W/E
-- @param #string l2 Latitude or longitude as string in the format DD:MM:SS N/S/W/E
-- @return #number Latitude in decimal degree format.
-- @return #number Longitude in decimal degree format.
function ARTY:_LLDMS2DD(l1,l2)
  self:F2(l1,l2)

  -- Make an array of lat and long.
  local _latlong={l1,l2}

  local _latitude=nil
  local _longitude=nil

  for _,ll in pairs(_latlong) do

    -- Format is expected as "DD:MM:SS" or "D:M:S".
    local _format = "%d+:%d+:%d+"
    local _ldms=ll:match(_format)

    if _ldms then

      -- Split DMS to degrees, minutes and seconds.
      local _dms=self:_split(_ldms, ":")
      local _deg=tonumber(_dms[1])
      local _min=tonumber(_dms[2])
      local _sec=tonumber(_dms[3])

      -- Convert DMS to DD.
      local function DMS2DD(d,m,s)
       return d+m/60+s/3600
      end

      -- Detect with hemisphere is meant.
      if ll:match("N") then
        _latitude=DMS2DD(_deg,_min,_sec)
      elseif ll:match("S") then
        _latitude=-DMS2DD(_deg,_min,_sec)
      elseif ll:match("W") then
        _longitude=-DMS2DD(_deg,_min,_sec)
      elseif ll:match("E") then
        _longitude=DMS2DD(_deg,_min,_sec)
      end

      -- Debug text.
      local text=string.format("DMS %02d Deg %02d min %02d sec",_deg,_min,_sec)
      self:T2(self.lid..text)

    end
  end

  -- Debug text.
  local text=string.format("\nLatitude  %s", tostring(_latitude))
  text=text..string.format("\nLongitude %s", tostring(_longitude))
  self:T2(self.lid..text)

  return _latitude,_longitude
end

--- Convert time in seconds to hours, minutes and seconds.
-- @param #ARTY self
-- @param #number seconds Time in seconds.
-- @return #string Time in format Hours:minutes:seconds.
function ARTY:_SecondsToClock(seconds)
  self:F3({seconds=seconds})

  if seconds==nil then
    return nil
  end

  -- Seconds
  local seconds = tonumber(seconds)

  -- Seconds of this day.
  local _seconds=seconds%(60*60*24)

  if seconds <= 0 then
    return nil
  else
    local hours = string.format("%02.f", math.floor(_seconds/3600))
    local mins  = string.format("%02.f", math.floor(_seconds/60 - (hours*60)))
    local secs  = string.format("%02.f", math.floor(_seconds - hours*3600 - mins *60))
    local days  = string.format("%d", seconds/(60*60*24))
    return hours..":"..mins..":"..secs.."+"..days
  end
end

--- Convert clock time from hours, minutes and seconds to seconds.
-- @param #ARTY self
-- @param #string clock String of clock time. E.g., "06:12:35".
function ARTY:_ClockToSeconds(clock)
  self:F3({clock=clock})

  if clock==nil then
    return nil
  end

  -- Seconds init.
  local seconds=0

  -- Split additional days.
  local dsplit=self:_split(clock, "+")

  -- Convert days to seconds.
  if #dsplit>1 then
    seconds=seconds+tonumber(dsplit[2])*60*60*24
  end

  -- Split hours, minutes, seconds
  local tsplit=self:_split(dsplit[1], ":")

  -- Get time in seconds
  local i=1
  for _,time in ipairs(tsplit) do
    if i==1 then
      -- Hours
      seconds=seconds+tonumber(time)*60*60
    elseif i==2 then
      -- Minutes
      seconds=seconds+tonumber(time)*60
    elseif i==3 then
      -- Seconds
      seconds=seconds+tonumber(time)
    end
    i=i+1
  end

  self:T3(self.lid..string.format("Clock %s = %d seconds", clock, seconds))
  return seconds
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Functional** - Suppress fire of ground units when they get hit.
-- 
-- ===
-- 
-- ## Features:
-- 
--   * Hold fire of attacked units when being fired upon.
--   * Retreat to a user defined zone.
--   * Fall back on hits.
--   * Take cover on hits.
--   * Gaussian distribution of suppression time.
--
-- ===
-- 
-- ## Missions:
--
-- ## [MOOSE - ALL Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS)
-- 
-- === 
-- 
-- When ground units get hit by (suppressive) enemy fire, they will not be able to shoot back for a certain amount of time.
-- 
-- The implementation is based on an idea and script by MBot. See the [DCS forum threat](https://forums.eagle.ru/showthread.php?t=107635) for details.
-- 
-- In addition to suppressing the fire, conditions can be specified, which let the group retreat to a defined zone, move away from the attacker
-- or hide at a nearby scenery object.
-- 
-- ====
-- 
-- # YouTube Channel
-- 
-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg)
-- 
-- ===
-- 
-- ### Author: **funkyfranky**
-- 
-- ### Contributions: FlightControl
-- 
-- ===
-- 
-- @module Functional.Suppression
-- @image Suppression.JPG

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- SUPPRESSION class
-- @type SUPPRESSION
-- @field #string ClassName Name of the class.
-- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players.
-- @field #string lid String for DCS log file.
-- @field #boolean flare Flare units when they get hit or die.
-- @field #boolean smoke Smoke places to which the group retreats, falls back or hides.
-- @field #list DCSdesc Table containing all DCS descriptors of the group.
-- @field #string Type Type of the group.
-- @field #number SpeedMax Maximum speed of group in km/h.
-- @field #boolean IsInfantry True if group has attribute Infantry.
-- @field Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the FSM. Must be a ground group.
-- @field #number Tsuppress_ave Average time in seconds a group gets suppressed. Actual value is sampled randomly from a Gaussian distribution.
-- @field #number Tsuppress_min Minimum time in seconds the group gets suppressed.
-- @field #number Tsuppress_max Maximum time in seconds the group gets suppressed.
-- @field #number TsuppressionOver Time at which the suppression will be over.
-- @field #number IniGroupStrength Number of units in a group at start.
-- @field #number Nhit Number of times the group was hit.
-- @field #string Formation Formation which will be used when falling back, taking cover or retreating. Default "Vee".
-- @field #number Speed Speed the unit will use when falling back, taking cover or retreating. Default 999.
-- @field #boolean MenuON If true creates a entry in the F10 menu.
-- @field #boolean FallbackON If true, group can fall back, i.e. move away from the attacking unit.
-- @field #number FallbackWait Time in seconds the unit will wait at the fall back point before it resumes its mission.
-- @field #number FallbackDist Distance in meters the unit will fall back.
-- @field #number FallbackHeading Heading in degrees to which the group should fall back. Default is directly away from the attacking unit.
-- @field #boolean TakecoverON If true, group can hide at a nearby scenery object.
-- @field #number TakecoverWait Time in seconds the group will hide before it will resume its mission.
-- @field #number TakecoverRange Range in which the group will search for scenery objects to hide at.
-- @field Core.Point#COORDINATE hideout Coordinate/place where the group will try to take cover.
-- @field #number PminFlee Minimum probability in percent that a group will flee (fall back or take cover) at each hit event. Default is 10 %.
-- @field #number PmaxFlee Maximum probability in percent that a group will flee (fall back or take cover) at each hit event. Default is 90 %.
-- @field Core.Zone#ZONE RetreatZone Zone to which a group retreats.
-- @field #number RetreatDamage Damage in percent at which the group will be ordered to retreat.
-- @field #number RetreatWait Time in seconds the group will wait in the retreat zone before it resumes its mission. Default two hours. 
-- @field #string CurrentAlarmState Alam state the group is currently in.
-- @field #string CurrentROE ROE the group currently has.
-- @field #string DefaultAlarmState Alarm state the group will go to when it is changed back from another state. Default is "Auto".
-- @field #string DefaultROE ROE the group will get once suppression is over. Default is "Free".
-- @field #boolean eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true.
-- @field Core.Zone#ZONE BattleZone 
-- @field #boolean AutoEngage
-- @field #table waypoints Waypoints of the group as defined in the ME.
-- @extends Core.Fsm#FSM_CONTROLLABLE
-- 

--- Mimic suppressive enemy fire and let groups flee or retreat.
-- 
-- ## Suppression Process
-- 
-- ![Process](..\Presentations\SUPPRESSION\Suppression_Process.png)
-- 
-- The suppression process can be described as follows.
-- 
-- ### CombatReady
-- 
-- A group starts in the state **CombatReady**. In this state the group is ready to fight. The ROE is set to either "Weapon Free" or "Return Fire".
-- The alarm state is set to either "Auto" or "Red".
-- 
-- ### Event Hit
-- The most important event in this scenario is the **Hit** event. This is an event of the FSM and triggered by the DCS event hit.
-- 
-- ### Suppressed
-- After the **Hit** event the group changes its state to **Suppressed**. Technically, the ROE of the group is changed to "Weapon Hold".
-- The suppression of the group will last a certain amount of time. It is randomized an will vary each time the group is hit.
-- The expected suppression time is set to 15 seconds by default. But the actual value is sampled from a Gaussian distribution.
--  
-- ![Process](..\Presentations\SUPPRESSION\Suppression_Gaussian.png)
-- 
-- The graph shows the distribution of suppression times if a group would be hit 100,000 times. As can be seen, on most hits the group gets
-- suppressed for around 15 seconds. Other values are also possible but they become less likely the further away from the "expected" suppression time they are.
-- Minimal and maximal suppression times can also be specified. By default these are set to 5 and 25 seconds, respectively. This can also be seen in the graph
-- because the tails of the Gaussian distribution are cut off at these values.
-- 
-- ### Event Recovered
-- After the suppression time is over, the event **Recovered** is initiated and the group becomes **CombatReady** again.
-- The ROE of the group will be set to "Weapon Free".
-- 
-- Of course, it can also happen that a group is hit again while it is still suppressed. In that case a new random suppression time is calculated.
-- If the new suppression time is longer than the remaining suppression of the previous hit, then the group recovers when the suppression time of the last
-- hit has passed.
-- If the new suppression time is shorter than the remaining suppression, the group will recover after the longer time of the first suppression has passed.
-- 
-- For example:
-- 
-- * A group gets hit the first time and is suppressed for - let's say - 15 seconds.
-- * After 10 seconds, i.e. when 5 seconds of the old suppression are left, the group gets hit a again.
-- * A new suppression time is calculated which can be smaller or larger than the remaining 5 seconds.
-- * If the new suppression time is smaller, e.g. three seconds, than five seconds, the group will recover after the 5 remaining seconds of the first suppression have passed.
-- * If the new suppression time is longer than last suppression time, e.g. 10 seconds, then the group will recover after the 10 seconds of the new hit have passed.
-- 
-- Generally speaking, the suppression times are not just added on top of each other. Because this could easily lead to the situation that a group 
-- never becomes CombatReady again before it gets destroyed.
-- 
-- The mission designer can capture the event **Recovered** by the function @{#SUPPRESSION.OnAfterRecovered}().
-- 
-- ## Flee Events and States
-- Apart from being suppressed the groups can also flee from the enemy under certain conditions.
-- 
-- ### Event Retreat
-- The first option is a retreat. This can be enabled by setting a retreat zone, i.e. a trigger zone defined in the mission editor.
-- 
-- If the group takes a certain amount of damage, the event **Retreat** will be called and the group will start to move to the retreat zone.
-- The group will be in the state **Retreating**, which means that its ROE is set to "Weapon Hold" and the alarm state is set to "Green".
-- Setting the alarm state to green is necessary to enable the group to move under fire.
-- 
-- When the group has reached the retreat zone, the event **Retreated** is triggered and the state will change to **Retreated** (note that both the event and
-- the state of the same name in this case). ROE and alarm state are
-- set to "Return Fire" and "Auto", respectively. The group will stay in the retreat zone and not actively participate in the combat any more.
-- 
-- If no option retreat zone has been specified, the option retreat is not available.
-- 
-- The mission designer can capture the events **Retreat** and **Retreated** by the functions @{#SUPPRESSION.OnAfterRetreat}() and @{#SUPPRESSION.OnAfterRetreated}().
-- 
-- ### Fallback
-- 
-- If a group is attacked by another ground group, it has the option to fall back, i.e. move away from the enemy. The probability of the event **FallBack** to
-- happen depends on the damage of the group that was hit. The more a group gets damaged, the more likely **FallBack** event becomes.
-- 
-- If the group enters the state **FallingBack** it will move 100 meters in the opposite direction of the attacking unit. ROE and alarmstate are set to "Weapon Hold"
-- and "Green", respectively.
-- 
-- At the fallback point the group will wait for 60 seconds before it resumes its normal mission.
-- 
-- The mission designer can capture the event **FallBack** by the function @{#SUPPRESSION.OnAfterFallBack}().
-- 
-- ### TakeCover
-- 
-- If a group is hit by either another ground or air unit, it has the option to "take cover" or "hide". This means that the group will move to a random
-- scenery object in it vicinity.
-- 
-- Analogously to the fall back case, the probability of a **TakeCover** event to occur, depends on the damage of the group. The more a group is damaged, the more
-- likely it becomes that a group takes cover.
-- 
-- When a **TakeCover** event occurs an area with a radius of 300 meters around the hit group is searched for an arbitrary scenery object.
-- If at least one scenery object is found, the group will move there. One it has reached its "hideout", it will wait there for two minutes before it resumes its
-- normal mission.
-- 
-- If more than one scenery object is found, the group will move to a random one.
-- If no scenery object is near the group the **TakeCover** event is rejected and the group will not move.
-- 
-- The mission designer can capture the event **TakeCover** by the function @{#SUPPRESSION.OnAfterTakeCover}().
-- 
-- ### Choice of FallBack or TakeCover if both are enabled?
-- 
-- If both **FallBack** and **TakeCover** events are enabled by the functions @{#SUPPRESSION.Fallback}() and @{#SUPPRESSION.Takecover}() the algorithm does the following:
-- 
-- * If the attacking unit is a ground unit, then the **FallBack** event is executed.
-- * Otherwise, i.e. if the attacker is *not* a ground unit, then the **TakeCover** event is triggered.
-- 
-- ### FightBack
-- 
-- When a group leaves the states **TakingCover** or **FallingBack** the event **FightBack** is triggered. This changes the ROE and the alarm state back to their default values.
-- 
-- The mission designer can capture the event **FightBack** by the function @{#SUPPRESSION.OnAfterFightBack}()
-- 
-- # Examples
-- 
-- ## Simple Suppression
-- This example shows the basic steps to use suppressive fire for a group.
-- 
-- ![Process](..\Presentations\SUPPRESSION\Suppression_Example_01.png)
-- 
-- 
-- # Customization and Fine Tuning
-- The following user functions can be used to change the default values
-- 
-- * @{#SUPPRESSION.SetSuppressionTime}() can be used to set the time a goup gets suppressed.
-- * @{#SUPPRESSION.SetRetreatZone}() sets the retreat zone and enables the possiblity for the group to retreat.
-- * @{#SUPPRESSION.SetFallbackDistance}() sets a value how far the unit moves away from the attacker after the fallback event.
-- * @{#SUPPRESSION.SetFallbackWait}() sets the time after which the group resumes its mission after a FallBack event.
-- * @{#SUPPRESSION.SetTakecoverWait}() sets the time after which the group resumes its mission after a TakeCover event.
-- * @{#SUPPRESSION.SetTakecoverRange}() sets the radius in which hideouts are searched.
-- * @{#SUPPRESSION.SetTakecoverPlace}() explicitly sets the place where the group will run at a TakeCover event.
-- * @{#SUPPRESSION.SetMinimumFleeProbability}() sets the minimum probability that a group flees (FallBack or TakeCover) after a hit. Note taht the probability increases with damage.
-- * @{#SUPPRESSION.SetMaximumFleeProbability}() sets the maximum probability that a group flees (FallBack or TakeCover) after a hit. Default is 90%.
-- * @{#SUPPRESSION.SetRetreatDamage}() sets the damage a group/unit can take before it is ordered to retreat.
-- * @{#SUPPRESSION.SetRetreatWait}() sets the time a group waits in the retreat zone after a retreat.
-- * @{#SUPPRESSION.SetDefaultAlarmState}() sets the alarm state a group gets after it becomes CombatReady again.
-- * @{#SUPPRESSION.SetDefaultROE}() set the rules of engagement a group gets after it becomes CombatReady again.
-- * @{#SUPPRESSION.FlareOn}() is mainly for debugging. A flare is fired when a unit is hit, gets suppressed, recovers, dies.
-- * @{#SUPPRESSION.SmokeOn}() is mainly for debugging. Puts smoke on retreat zone, hideouts etc.
-- * @{#SUPPRESSION.MenuON}() is mainly for debugging. Activates a radio menu item where certain functions like retreat etc. can be triggered manually.
-- 
-- 
-- @field #SUPPRESSION
SUPPRESSION={
  ClassName         = "SUPPRESSION",
  Debug             = false,
  lid               = nil,
  flare             = false,
  smoke             = false,
  DCSdesc           = nil,
  Type              = nil,
  IsInfantry        = nil,
  SpeedMax          = nil,
  Tsuppress_ave     = 15,
  Tsuppress_min     = 5,
  Tsuppress_max     = 25,
  TsuppressOver     = nil,
  IniGroupStrength  = nil,
  Nhit              = 0,
  Formation         = "Off road",
  Speed             = 4,
  MenuON            = false,
  FallbackON        = false,
  FallbackWait      = 60,
  FallbackDist      = 100,
  FallbackHeading   = nil,
  TakecoverON       = false,
  TakecoverWait     = 120,
  TakecoverRange    = 300,
  hideout           = nil,
  PminFlee          = 10,
  PmaxFlee          = 90,
  RetreatZone       = nil,
  RetreatDamage     = nil,
  RetreatWait       = 7200,
  CurrentAlarmState = "unknown",
  CurrentROE        = "unknown",
  DefaultAlarmState = "Auto",
  DefaultROE        = "Weapon Free",
  eventmoose        = true,
  waypoints         = {},
}

--- Enumerator of possible rules of engagement.
-- @type SUPPRESSION.ROE
-- @field #string Hold Hold fire.
-- @field #string Free Weapon fire.
-- @field #string Return Return fire.
SUPPRESSION.ROE={
  Hold="Weapon Hold",
  Free="Weapon Free",
  Return="Return Fire",  
}

--- Enumerator of possible alarm states.
-- @type SUPPRESSION.AlarmState
-- @field #string Auto Automatic.
-- @field #string Green Green.
-- @field #string Red Red.
SUPPRESSION.AlarmState={
  Auto="Auto",
  Green="Green",
  Red="Red",
}

--- Main F10 menu for suppresion, i.e. F10/Suppression.
-- @field #string MenuF10
SUPPRESSION.MenuF10=nil

--- PSEUDOATC version.
-- @field #number version
SUPPRESSION.version="0.9.4"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--TODO list
--DONE: Figure out who was shooting and move away from him.
--DONE: Move behind a scenery building if there is one nearby.
--DONE: Retreat to a given zone or point.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Creates a new AI_suppression object.
-- @param #SUPPRESSION self
-- @param Wrapper.Group#GROUP group The GROUP object for which suppression should be applied.
-- @return #SUPPRESSION self
function SUPPRESSION:New(group)

  -- Inherits from FSM_CONTROLLABLE
  local self=BASE:Inherit(self, FSM_CONTROLLABLE:New()) -- #SUPPRESSION
  
  -- Check that group is present.
  if group then
    self.lid=string.format("SUPPRESSION %s | ", tostring(group:GetName()))
    self:T(self.lid..string.format("SUPPRESSION version %s. Activating suppressive fire for group %s", SUPPRESSION.version, group:GetName()))
  else
    self:E("SUPPRESSION | Requested group does not exist! (Has to be a MOOSE group)")
    return nil
  end
  
  -- Check that we actually have a GROUND group.
  if group:IsGround()==false then
    self:E(self.lid..string.format("SUPPRESSION fire group %s has to be a GROUND group!", group:GetName()))
    return nil
  end  
  
  -- Set the controllable for the FSM.
  self:SetControllable(group)
  
  -- Get DCS descriptors of group.
  self.DCSdesc=group:GetDCSDesc(1)
  
  -- Get max speed the group can do and convert to km/h.
  self.SpeedMax=group:GetSpeedMax()
  
  -- Set speed to maximum.
  self.Speed=self.SpeedMax
  
  -- Is this infantry or not.
  self.IsInfantry=group:GetUnit(1):HasAttribute("Infantry")
  
  -- Type of group.
  self.Type=group:GetTypeName()
  
  -- Initial group strength.
  self.IniGroupStrength=#group:GetUnits()
  
  -- Set ROE and Alarm State.
  self:SetDefaultROE("Free")
  self:SetDefaultAlarmState("Auto")
  
  -- Transitions 
  self:AddTransition("*",           "Start",     "CombatReady")
  self:AddTransition("*",           "Status",    "*")
  self:AddTransition("CombatReady", "Hit",       "Suppressed")
  self:AddTransition("Suppressed",  "Hit",       "Suppressed") 
  self:AddTransition("Suppressed",  "Recovered", "CombatReady")
  self:AddTransition("Suppressed",  "TakeCover", "TakingCover")
  self:AddTransition("Suppressed",  "FallBack",  "FallingBack")
  self:AddTransition("*",           "Retreat",   "Retreating")
  self:AddTransition("TakingCover", "FightBack", "CombatReady")
  self:AddTransition("FallingBack", "FightBack", "CombatReady")
  self:AddTransition("Retreating",  "Retreated", "Retreated")
  self:AddTransition("*",           "OutOfAmmo", "*")
  self:AddTransition("*",           "Dead",      "*")
  self:AddTransition("*",           "Stop",      "Stopped")
  
  self:AddTransition("TakingCover", "Hit",       "TakingCover")
  self:AddTransition("FallingBack", "Hit",       "FallingBack")


  --- Trigger "Status" event.
  -- @function [parent=#SUPPRESSION] Status
  -- @param #SUPPRESSION self

  --- Trigger "Status" event after a delay.
  -- @function [parent=#SUPPRESSION] __Status
  -- @param #SUPPRESSION self
  -- @param #number Delay Delay in seconds.

  --- User function for OnAfter "Status" event.
  -- @function [parent=#SUPPRESSION] OnAfterStatus
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Trigger "Hit" event.
  -- @function [parent=#SUPPRESSION] Hit
  -- @param #SUPPRESSION self
  -- @param Wrapper.Unit#UNIT Unit Unit that was hit.
  -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked.

  --- Trigger "Hit" event after a delay.
  -- @function [parent=#SUPPRESSION] __Hit
  -- @param #SUPPRESSION self
  -- @param #number Delay Delay in seconds. 
  -- @param Wrapper.Unit#UNIT Unit Unit that was hit.
  -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked.

  --- User function for OnBefore "Hit" event.
  -- @function [parent=#SUPPRESSION] OnBeforeHit
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Unit#UNIT Unit Unit that was hit.
  -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked.
  -- @return #boolean

  --- User function for OnAfter "Hit" event.
  -- @function [parent=#SUPPRESSION] OnAfterHit
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Unit#UNIT Unit Unit that was hit.
  -- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked.

  
  --- Trigger "Recovered" event.
  -- @function [parent=#SUPPRESSION] Recovered
  -- @param #SUPPRESSION self

  --- Trigger "Recovered" event after a delay.
  -- @function [parent=#SUPPRESSION] Recovered
  -- @param #number Delay Delay in seconds. 
  -- @param #SUPPRESSION self

  --- User function for OnBefore "Recovered" event.
  -- @function [parent=#SUPPRESSION] OnBeforeRecovered
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @return #boolean  

  --- User function for OnAfter "Recovered" event.
  -- @function [parent=#SUPPRESSION] OnAfterRecovered
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Trigger "TakeCover" event.
  -- @function [parent=#SUPPRESSION] TakeCover
  -- @param #SUPPRESSION self
  -- @param Core.Point#COORDINATE Hideout Place where the group will hide.

  --- Trigger "TakeCover" event after a delay.
  -- @function [parent=#SUPPRESSION] __TakeCover
  -- @param #SUPPRESSION self
  -- @param #number Delay Delay in seconds. 
  -- @param Core.Point#COORDINATE Hideout Place where the group will hide.

  --- User function for OnBefore "TakeCover" event.
  -- @function [parent=#SUPPRESSION] OnBeforeTakeCover
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Point#COORDINATE Hideout Place where the group will hide.
  -- @return #boolean

  --- User function for OnAfter "TakeCover" event.
  -- @function [parent=#SUPPRESSION] OnAfterTakeCover
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Point#COORDINATE Hideout Place where the group will hide.


  --- Trigger "FallBack" event.
  -- @function [parent=#SUPPRESSION] FallBack
  -- @param #SUPPRESSION self
  -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this.

  --- Trigger "FallBack" event after a delay.
  -- @function [parent=#SUPPRESSION] __FallBack
  -- @param #SUPPRESSION self
  -- @param #number Delay Delay in seconds. 
  -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this.

  --- User function for OnBefore "FallBack" event.
  -- @function [parent=#SUPPRESSION] OnBeforeFallBack
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this.
  -- @return #boolean

  --- User function for OnAfter "FallBack" event.
  -- @function [parent=#SUPPRESSION] OnAfterFallBack
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this.


  --- Trigger "Retreat" event.
  -- @function [parent=#SUPPRESSION] Retreat
  -- @param #SUPPRESSION self

  --- Trigger "Retreat" event after a delay.
  -- @function [parent=#SUPPRESSION] __Retreat
  -- @param #SUPPRESSION self
  -- @param #number Delay Delay in seconds. 

  --- User function for OnBefore "Retreat" event.
  -- @function [parent=#SUPPRESSION] OnBeforeRetreat
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @return #boolean
  
  --- User function for OnAfter "Retreat" event.
  -- @function [parent=#SUPPRESSION] OnAfterRetreat
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Trigger "Retreated" event.
  -- @function [parent=#SUPPRESSION] Retreated
  -- @param #SUPPRESSION self

  --- Trigger "Retreated" event after a delay.
  -- @function [parent=#SUPPRESSION] __Retreated
  -- @param #SUPPRESSION self
  -- @param #number Delay Delay in seconds. 

  --- User function for OnBefore "Retreated" event.
  -- @function [parent=#SUPPRESSION] OnBeforeRetreated
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @return #boolean
  
  --- User function for OnAfter "Retreated" event.
  -- @function [parent=#SUPPRESSION] OnAfterRetreated
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Trigger "FightBack" event.
  -- @function [parent=#SUPPRESSION] FightBack
  -- @param #SUPPRESSION self

  --- Trigger "FightBack" event after a delay.
  -- @function [parent=#SUPPRESSION] __FightBack
  -- @param #SUPPRESSION self
  -- @param #number Delay Delay in seconds. 

  --- User function for OnBefore "FlightBack" event.
  -- @function [parent=#SUPPRESSION] OnBeforeFightBack
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @return #boolean
  
  --- User function for OnAfter "FlightBack" event.
  -- @function [parent=#SUPPRESSION] OnAfterFightBack
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Trigger "OutOfAmmo" event.
  -- @function [parent=#SUPPRESSION] OutOfAmmo
  -- @param #SUPPRESSION self

  --- Trigger "OutOfAmmo" event after a delay.
  -- @function [parent=#SUPPRESSION] __OutOfAmmo
  -- @param #SUPPRESSION self
  -- @param #number Delay Delay in seconds. 

  --- User function for OnAfter "OutOfAmmo" event.
  -- @function [parent=#SUPPRESSION] OnAfterOutOfAmmo
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Trigger "Dead" event.
  -- @function [parent=#SUPPRESSION] Dead
  -- @param #SUPPRESSION self

  --- Trigger "Dead" event after a delay.
  -- @function [parent=#SUPPRESSION] __Dead
  -- @param #SUPPRESSION self
  -- @param #number Delay Delay in seconds. 

  --- User function for OnAfter "Dead" event.
  -- @function [parent=#SUPPRESSION] OnAfterDead
  -- @param #SUPPRESSION self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set average, minimum and maximum time a unit is suppressed each time it gets hit.
-- @param #SUPPRESSION self
-- @param #number Tave Average time [seconds] a group will be suppressed. Default is 15 seconds.
-- @param #number Tmin (Optional) Minimum time [seconds] a group will be suppressed. Default is 5 seconds.
-- @param #number Tmax (Optional) Maximum time a group will be suppressed. Default is 25 seconds.
function SUPPRESSION:SetSuppressionTime(Tave, Tmin, Tmax)
  self:F({Tave=Tave, Tmin=Tmin, Tmax=Tmax})

  -- Minimum suppression time is input or default but at least 1 second.
  self.Tsuppress_min=Tmin or self.Tsuppress_min
  self.Tsuppress_min=math.max(self.Tsuppress_min, 1)
  
  -- Maximum suppression time is input or dault but at least Tmin.
  self.Tsuppress_max=Tmax or self.Tsuppress_max
  self.Tsuppress_max=math.max(self.Tsuppress_max, self.Tsuppress_min)
  
  -- Expected suppression time is input or default but at leat Tmin and at most Tmax.
  self.Tsuppress_ave=Tave or self.Tsuppress_ave
  self.Tsuppress_ave=math.max(self.Tsuppress_min)
  self.Tsuppress_ave=math.min(self.Tsuppress_max)
  
  self:T(self.lid..string.format("Set ave suppression time to %d seconds.", self.Tsuppress_ave))
  self:T(self.lid..string.format("Set min suppression time to %d seconds.", self.Tsuppress_min))
  self:T(self.lid..string.format("Set max suppression time to %d seconds.", self.Tsuppress_max))
end

--- Set the zone to which a group retreats after being damaged too much.
-- @param #SUPPRESSION self
-- @param Core.Zone#ZONE zone MOOSE zone object.
function SUPPRESSION:SetRetreatZone(zone)
  self:F({zone=zone})
  self.RetreatZone=zone
end

--- Turn Debug mode on. Enables messages and more output to DCS log file.
-- @param #SUPPRESSION self
function SUPPRESSION:DebugOn()
  self:F()
  self.Debug=true
end

--- Flare units when they are hit, die or recover from suppression.
-- @param #SUPPRESSION self
function SUPPRESSION:FlareOn()
  self:F()
  self.flare=true
end

--- Smoke positions where units fall back to, hide or retreat.
-- @param #SUPPRESSION self
function SUPPRESSION:SmokeOn()
  self:F()
  self.smoke=true
end

--- Set the formation a group uses for fall back, hide or retreat.
-- @param #SUPPRESSION self
-- @param #string formation Formation of the group. Default "Vee".
function SUPPRESSION:SetFormation(formation)
  self:F(formation)
  self.Formation=formation or "Vee"
end

--- Set speed a group moves at for fall back, hide or retreat.
-- @param #SUPPRESSION self
-- @param #number speed Speed in km/h of group. Default max speed the group can do.
function SUPPRESSION:SetSpeed(speed)
  self:F(speed)
  self.Speed=speed or self.SpeedMax
  self.Speed=math.min(self.Speed, self.SpeedMax)
end

--- Enable fall back if a group is hit.
-- @param #SUPPRESSION self
-- @param #boolean switch Enable=true or disable=false fall back of group.
function SUPPRESSION:Fallback(switch)
  self:F(switch)
  if switch==nil then
    switch=true
  end
  self.FallbackON=switch
end

--- Set distance a group will fall back when it gets hit.
-- @param #SUPPRESSION self
-- @param #number distance Distance in meters.
function SUPPRESSION:SetFallbackDistance(distance)
  self:F(distance)
  self.FallbackDist=distance
end

--- Set time a group waits at its fall back position before it resumes its normal mission.
-- @param #SUPPRESSION self
-- @param #number time Time in seconds.
function SUPPRESSION:SetFallbackWait(time)
  self:F(time)
  self.FallbackWait=time
end

--- Enable take cover option if a unit is hit.
-- @param #SUPPRESSION self
-- @param #boolean switch Enable=true or disable=false fall back of group.
function SUPPRESSION:Takecover(switch)
  self:F(switch)
  if switch==nil then
    switch=true
  end
  self.TakecoverON=switch
end

--- Set time a group waits at its hideout position before it resumes its normal mission.
-- @param #SUPPRESSION self
-- @param #number time Time in seconds.
function SUPPRESSION:SetTakecoverWait(time)
  self:F(time)
  self.TakecoverWait=time
end

--- Set distance a group searches for hideout places.
-- @param #SUPPRESSION self
-- @param #number range Search range in meters.
function SUPPRESSION:SetTakecoverRange(range)
  self:F(range)
  self.TakecoverRange=range
end

--- Set hideout place explicitly.
-- @param #SUPPRESSION self
-- @param Core.Point#COORDINATE Hideout Place where the group will hide after the TakeCover event.
function SUPPRESSION:SetTakecoverPlace(Hideout)
  self.hideout=Hideout
end

--- Set minimum probability that a group flees (falls back or takes cover) after a hit event. Default is 10%.
-- @param #SUPPRESSION self
-- @param #number probability Probability in percent.
function SUPPRESSION:SetMinimumFleeProbability(probability)
  self:F(probability)
  self.PminFlee=probability or 10
end

--- Set maximum probability that a group flees (falls back or takes cover) after a hit event. Default is 90%.
-- @param #SUPPRESSION self
-- @param #number probability Probability in percent.
function SUPPRESSION:SetMaximumFleeProbability(probability)
  self:F(probability)
  self.PmaxFlee=probability or 90
end

--- Set damage threshold before a group is ordered to retreat if a retreat zone was defined.
-- If the group consists of only a singe unit, this referrs to the life of the unit.
-- If the group consists of more than one unit, this referrs to the group strength relative to its initial strength.
-- @param #SUPPRESSION self
-- @param #number damage Damage in percent. If group gets damaged above this value, the group will retreat. Default 50 %.
function SUPPRESSION:SetRetreatDamage(damage)
  self:F(damage)
  self.RetreatDamage=damage or 50
end

--- Set time a group waits in the retreat zone before it resumes its mission. Default is two hours.
-- @param #SUPPRESSION self
-- @param #number time Time in seconds. Default 7200 seconds = 2 hours.
function SUPPRESSION:SetRetreatWait(time)
  self:F(time)
  self.RetreatWait=time or 7200
end

--- Set alarm state a group will get after it returns from a fall back or take cover.
-- @param #SUPPRESSION self
-- @param #string alarmstate Alarm state. Possible "Auto", "Green", "Red". Default is "Auto".
function SUPPRESSION:SetDefaultAlarmState(alarmstate)
  self:F(alarmstate)
  if alarmstate:lower()=="auto" then
    self.DefaultAlarmState=SUPPRESSION.AlarmState.Auto
  elseif alarmstate:lower()=="green" then
    self.DefaultAlarmState=SUPPRESSION.AlarmState.Green
  elseif alarmstate:lower()=="red" then
    self.DefaultAlarmState=SUPPRESSION.AlarmState.Red
  else
    self.DefaultAlarmState=SUPPRESSION.AlarmState.Auto
  end
end

--- Set Rules of Engagement (ROE) a group will get when it recovers from suppression.
-- @param #SUPPRESSION self
-- @param #string roe ROE after suppression. Possible "Free", "Hold" or "Return". Default "Free".
function SUPPRESSION:SetDefaultROE(roe)
  self:F(roe)
  if roe:lower()=="free" then
    self.DefaultROE=SUPPRESSION.ROE.Free
  elseif roe:lower()=="hold" then
    self.DefaultROE=SUPPRESSION.ROE.Hold
  elseif roe:lower()=="return" then
    self.DefaultROE=SUPPRESSION.ROE.Return
  else
    self.DefaultROE=SUPPRESSION.ROE.Free
  end
end

--- Create an F10 menu entry for the suppressed group. The menu is mainly for Debugging purposes.
-- @param #SUPPRESSION self
-- @param #boolean switch Enable=true or disable=false menu group. Default is true.
function SUPPRESSION:MenuOn(switch)
  self:F(switch)
  if switch==nil then
    switch=true
  end
  self.MenuON=switch
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create F10 main menu, i.e. F10/Suppression. The menu is mainly for Debugging purposes.
-- @param #SUPPRESSION self
function SUPPRESSION:_CreateMenuGroup()
  local SubMenuName=self.Controllable:GetName()
  local MenuGroup=MENU_MISSION:New(SubMenuName, SUPPRESSION.MenuF10)
  MENU_MISSION_COMMAND:New("Fallback!", MenuGroup, self.OrderFallBack, self)
  MENU_MISSION_COMMAND:New("Take Cover!", MenuGroup, self.OrderTakeCover, self)
  MENU_MISSION_COMMAND:New("Retreat!", MenuGroup, self.OrderRetreat, self)
  MENU_MISSION_COMMAND:New("Report Status", MenuGroup, self.Status, self, true)
end

--- Order group to fall back between 100 and 150 meters in a random direction.
-- @param #SUPPRESSION self
function SUPPRESSION:OrderFallBack()
  local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE
  local vicinity=group:GetCoordinate():GetRandomVec2InRadius(150, 100)
  local coord=COORDINATE:NewFromVec2(vicinity)
  self:FallBack(self.Controllable)
end

--- Order group to take cover at a nearby scenery object.
-- @param #SUPPRESSION self
function SUPPRESSION:OrderTakeCover()
  -- Search place to hide or take specified one.
  local Hideout=self.hideout
  if self.hideout==nil then
    Hideout=self:_SearchHideout()
  end      
  -- Trigger TakeCover event.
  self:TakeCover(Hideout)
end

--- Order group to retreat to a pre-defined zone.
-- @param #SUPPRESSION self
function SUPPRESSION:OrderRetreat()
  self:Retreat()
end

--- Status of group. Current ROE, alarm state, life.
-- @param #SUPPRESSION self
-- @param #boolean message Send message to all players.
function SUPPRESSION:StatusReport(message)

  local group=self.Controllable --Wrapper.Group#GROUP

  local nunits=group:CountAliveUnits()
  local roe=self.CurrentROE
  local state=self.CurrentAlarmState
  local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife()
  local ammotot=group:GetAmmunition()
  local detectedG=group:GetDetectedGroupSet():CountAlive()
  local detectedU=group:GetDetectedUnitSet():Count()
  
  local text=string.format("State %s, Units=%d/%d, ROE=%s, AlarmState=%s, Hits=%d, Life(min/max/ave/ave0)=%d/%d/%d/%d, Total Ammo=%d, Detected=%d/%d", 
  self:GetState(), nunits, self.IniGroupStrength, self.CurrentROE, self.CurrentAlarmState, self.Nhit, life_min, life_max, life_ave, life_ave0, ammotot, detectedG, detectedU)
  
  MESSAGE:New(text, 10):ToAllIf(message or self.Debug)
  self:I(self.lid..text)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- After "Start" event. Initialized ROE and alarm state. Starts the event handler.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function SUPPRESSION:onafterStart(Controllable, From, Event, To)
  self:_EventFromTo("onafterStart", Event, From, To)
  
  local text=string.format("Started SUPPRESSION for group %s.", Controllable:GetName())
  self:I(self.lid..text)
  MESSAGE:New(text, 10):ToAllIf(self.Debug)
  
  local rzone="not defined"
  if self.RetreatZone then
    rzone=self.RetreatZone:GetName()
  end
  
  -- Set retreat damage value if it was not set by user input.
  if self.RetreatDamage==nil then
    if self.RetreatZone then
      if self.IniGroupStrength==1 then
        self.RetreatDamage=60.0  -- 40% of life is left.
      elseif self.IniGroupStrength==2 then
        self.RetreatDamage=50.0  -- 50% of group left, i.e. 1 of 2. We already order a retreat, because if for a group 2 two a zone is defined it would not be used at all.
      else
        self.RetreatDamage=66.5  -- 34% of the group is left, e.g. 1 of 3,4 or 5, 2 of 6,7 or 8, 3 of 9,10 or 11, 4/12, 4/13, 4/14, 5/15, ... 
      end
    else
      self.RetreatDamage=100   -- If no retreat then this should be set to 100%.
    end
  end
  
  -- Create main F10 menu if it is not there yet.
  if self.MenuON then 
    if not SUPPRESSION.MenuF10 then
      SUPPRESSION.MenuF10 = MENU_MISSION:New("Suppression")
    end
    self:_CreateMenuGroup()
  end
    
  -- Set the current ROE and alam state.
  self:_SetAlarmState(self.DefaultAlarmState)
  self:_SetROE(self.DefaultROE)
  
  local text=string.format("\n******************************************************\n")
  text=text..string.format("Suppressed group   = %s\n", Controllable:GetName())
  text=text..string.format("Type               = %s\n", self.Type)
  text=text..string.format("IsInfantry         = %s\n", tostring(self.IsInfantry))  
  text=text..string.format("Group strength     = %d\n", self.IniGroupStrength)
  text=text..string.format("Average time       = %5.1f seconds\n", self.Tsuppress_ave)
  text=text..string.format("Minimum time       = %5.1f seconds\n", self.Tsuppress_min)
  text=text..string.format("Maximum time       = %5.1f seconds\n", self.Tsuppress_max)
  text=text..string.format("Default ROE        = %s\n", self.DefaultROE)
  text=text..string.format("Default AlarmState = %s\n", self.DefaultAlarmState)
  text=text..string.format("Fall back ON       = %s\n", tostring(self.FallbackON))
  text=text..string.format("Fall back distance = %5.1f m\n", self.FallbackDist)
  text=text..string.format("Fall back wait     = %5.1f seconds\n", self.FallbackWait)
  text=text..string.format("Fall back heading  = %s degrees\n", tostring(self.FallbackHeading))
  text=text..string.format("Take cover ON      = %s\n", tostring(self.TakecoverON))
  text=text..string.format("Take cover search  = %5.1f m\n", self.TakecoverRange)
  text=text..string.format("Take cover wait    = %5.1f seconds\n", self.TakecoverWait)  
  text=text..string.format("Min flee probability = %5.1f\n", self.PminFlee)  
  text=text..string.format("Max flee probability = %5.1f\n", self.PmaxFlee)
  text=text..string.format("Retreat zone       = %s\n", rzone)
  text=text..string.format("Retreat damage     = %5.1f %%\n", self.RetreatDamage)
  text=text..string.format("Retreat wait       = %5.1f seconds\n", self.RetreatWait)
  text=text..string.format("Speed              = %5.1f km/h\n", self.Speed)
  text=text..string.format("Speed max          = %5.1f km/h\n", self.SpeedMax)
  text=text..string.format("Formation          = %s\n", self.Formation)
  text=text..string.format("******************************************************\n")
  self:T(self.lid..text)
    
  -- Add event handler.
  if self.eventmoose then
    self:HandleEvent(EVENTS.Hit,  self._OnEventHit)
    self:HandleEvent(EVENTS.Dead, self._OnEventDead)
  else
    world.addEventHandler(self)
  end
  
  self:__Status(-1)
end

--- After "Status" event.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function SUPPRESSION:onafterStatus(Controllable, From, Event, To)

  -- Suppressed group.  
  local group=self.Controllable --Wrapper.Group#GROUP
  
  -- Check if group object exists.
  if group then
  
    -- Number of alive units.
    local nunits=group:CountAliveUnits()
    
    -- Check if there are units.
    if nunits>0 then
      
      -- Retreat if completely out of ammo and retreat zone defined. 
      local nammo=group:GetAmmunition()
      if nammo==0 then
        self:OutOfAmmo()
      end

      -- Status report.
      self:StatusReport(false)
    
      -- Call status again if not "Stopped".
      if self:GetState()~="Stopped" then
        self:__Status(-30)
      end
      
    else
      -- Stop FSM as there are no units left.
      self:Stop()
    end
    
  else
    -- Stop FSM as there group object does not exist.
    self:Stop()
  end
  
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- After "Hit" event.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Unit#UNIT Unit Unit that was hit.
-- @param Wrapper.Unit#UNIT AttackUnit Unit that attacked.
function SUPPRESSION:onafterHit(Controllable, From, Event, To, Unit, AttackUnit)
  self:_EventFromTo("onafterHit", Event, From, To)
    
  -- Suppress unit.
  if From=="CombatReady" or From=="Suppressed" then
    self:_Suppress()
  end
  
  -- Get life of group in %.
  local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife()
  
  -- Damage in %. If group consists only of one unit, we take its life value.
  local Damage=100-life_ave0
  
  -- Condition for retreat.
  local RetreatCondition = Damage >= self.RetreatDamage-0.01 and self.RetreatZone
    
  -- Probability that a unit flees. The probability increases linearly with the damage of the group/unit.
  -- If Damage=0             ==> P=Pmin
  -- if Damage=RetreatDamage ==> P=Pmax
  -- If no retreat zone has been specified, RetreatDamage is 100.
  local Pflee=(self.PmaxFlee-self.PminFlee)/self.RetreatDamage * math.min(Damage, self.RetreatDamage) + self.PminFlee
  
  -- Evaluate flee condition.
  local P=math.random(0,100)
  local FleeCondition =  P < Pflee
  
  local text
  text=string.format("\nGroup %s: Life min=%5.1f, max=%5.1f, ave=%5.1f, ave0=%5.1f group=%5.1f\n", Controllable:GetName(), life_min, life_max, life_ave, life_ave0, groupstrength)
  text=string.format("Group %s: Damage = %8.4f (%8.4f retreat threshold).\n", Controllable:GetName(), Damage, self.RetreatDamage)
  text=string.format("Group %s: P_Flee = %5.1f %5.1f=P_rand (P_Flee > Prand ==> Flee)\n", Controllable:GetName(), Pflee, P)
  self:T(self.lid..text)
  
  -- Group is obviously destroyed.
  if Damage >= 99.9 then
    return
  end
  
  if RetreatCondition then
  
    -- Trigger Retreat event.
    self:Retreat()
    
  elseif FleeCondition then
  
    if self.FallbackON and AttackUnit:IsGround() then
    
      -- Trigger FallBack event.
      self:FallBack(AttackUnit)
      
    elseif self.TakecoverON then
    
      -- Search place to hide or take specified one.
      local Hideout=self.hideout
      if self.hideout==nil then
        Hideout=self:_SearchHideout()
      end
      
      -- Trigger TakeCover event.
      self:TakeCover(Hideout)
    end
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Before "Recovered" event. Check if suppression time is over.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @return #boolean
function SUPPRESSION:onbeforeRecovered(Controllable, From, Event, To)
  self:_EventFromTo("onbeforeRecovered", Event, From, To)
  
  -- Current time.
  local Tnow=timer.getTime()
  
  -- Debug info
  self:T(self.lid..string.format("onbeforeRecovered: Time now: %d  - Time over: %d", Tnow, self.TsuppressionOver))
  
  -- Recovery is only possible if enough time since the last hit has passed.
  if Tnow >= self.TsuppressionOver then
    return true
  else
    return false
  end
  
end

--- After "Recovered" event. Group has recovered and its ROE is set back to the "normal" unsuppressed state. Optionally the group is flared green.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function SUPPRESSION:onafterRecovered(Controllable, From, Event, To)
  self:_EventFromTo("onafterRecovered", Event, From, To)
  
  if Controllable and Controllable:IsAlive() then
  
    -- Debug message.
    local text=string.format("Group %s has recovered!", Controllable:GetName())
    MESSAGE:New(text, 10):ToAllIf(self.Debug)
    self:T(self.lid..text)
    
    -- Set ROE back to default.
    self:_SetROE()
    
    -- Flare unit green.
    if self.flare or self.Debug then
      Controllable:FlareGreen()
    end
    
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- After "FightBack" event. ROE and Alarm state are set back to default.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function SUPPRESSION:onafterFightBack(Controllable, From, Event, To)
  self:_EventFromTo("onafterFightBack", Event, From, To)
  
  -- Set ROE and alarm state back to default.
  self:_SetROE()
  self:_SetAlarmState()
  
  local group=Controllable --Wrapper.Group#GROUP
  
  local Waypoints = group:GetTemplateRoutePoints()
  
--  env.info("FF waypoints",showMessageBox)
--  self:I(Waypoints)
  
  group:Route(Waypoints, 5)
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Before "FallBack" event. We check that group is not already falling back.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this.
-- @return #boolean
function SUPPRESSION:onbeforeFallBack(Controllable, From, Event, To, AttackUnit)
  self:_EventFromTo("onbeforeFallBack", Event, From, To)
  
  --TODO: Add retreat? Only allowd transition is Suppressed-->Fallback. So in principle no need.
  if From == "FallingBack" then
    return false
  else
    return true
  end
end

--- After "FallBack" event. We get the heading away from the attacker and route the group a certain distance in that direction.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Unit#UNIT AttackUnit Attacking unit. We will move away from this.
function SUPPRESSION:onafterFallBack(Controllable, From, Event, To, AttackUnit)
  self:_EventFromTo("onafterFallback", Event, From, To)
  
  -- Debug info
  self:T(self.lid..string.format("Group %s is falling back after %d hits.", Controllable:GetName(), self.Nhit))
  
  -- Coordinate of the attacker and attacked unit.
  local ACoord=AttackUnit:GetCoordinate()
  local DCoord=Controllable:GetCoordinate()
  
  -- Heading from attacker to attacked unit.
  local heading=self:_Heading(ACoord, DCoord)
  
  -- Overwrite heading with user specified heading.
  if self.FallbackHeading then
    heading=self.FallbackHeading
  end
  
  -- Create a coordinate ~ 100 m in opposite direction of the attacking unit.
  local Coord=DCoord:Translate(self.FallbackDist, heading)
  
  -- Place marker
  if self.Debug then
    local MarkerID=Coord:MarkToAll("Fall back position for group "..Controllable:GetName())
  end
  
  -- Smoke the coordinate.
  if self.smoke or self.Debug then
    Coord:SmokeBlue()
  end
  
  -- Set ROE to weapon hold.
  self:_SetROE(SUPPRESSION.ROE.Hold)
  
  -- Set alarm state to GREEN and let the unit run away.
  self:_SetAlarmState(SUPPRESSION.AlarmState.Auto)

  -- Make the group run away.
  self:_Run(Coord, self.Speed, self.Formation, self.FallbackWait)
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Before "TakeCover" event. Search an area around the group for possible scenery objects where the group can hide.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Hideout Place where the group will hide.
-- @return #boolean
function SUPPRESSION:onbeforeTakeCover(Controllable, From, Event, To, Hideout)
  self:_EventFromTo("onbeforeTakeCover", Event, From, To)
  
  --TODO: Need to test this!
  if From=="TakingCover" then
    return false
  end
  
  -- Block transition if no hideout place is given.
  if Hideout ~= nil then
    return true
  else
    return false
  end

end

--- After "TakeCover" event. Group will run to a nearby scenery object and "hide" there for a certain time.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Hideout Place where the group will hide.
function SUPPRESSION:onafterTakeCover(Controllable, From, Event, To, Hideout)
  self:_EventFromTo("onafterTakeCover", Event, From, To)
     
  if self.Debug then
    local MarkerID=Hideout:MarkToAll(string.format("Hideout for group %s", Controllable:GetName()))
  end
  
  -- Smoke place of hideout.
  if self.smoke or self.Debug then
    Hideout:SmokeBlue()
  end
  
  -- Set ROE to weapon hold.
  self:_SetROE(SUPPRESSION.ROE.Hold)
  
  -- Set the ALARM STATE to GREEN. Then the unit will move even if it is under fire.
  self:_SetAlarmState(SUPPRESSION.AlarmState.Green)
  
  -- Make the group run away.
  self:_Run(Hideout, self.Speed, self.Formation, self.TakecoverWait)
    
end

--- After "OutOfAmmo" event. Triggered when group is completely out of ammo.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function SUPPRESSION:onafterOutOfAmmo(Controllable, From, Event, To)
  self:_EventFromTo("onafterOutOfAmmo", Event, From, To)

  -- Info to log.
  self:I(self.lid..string.format("Out of ammo!"))
    
  -- Order retreat if retreat zone was specified.
  if self.RetreatZone then
    self:Retreat()
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Before "Retreat" event. We check that the group is not already retreating.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @return #boolean True if transition is allowed, False if transition is forbidden.
function SUPPRESSION:onbeforeRetreat(Controllable, From, Event, To)
  self:_EventFromTo("onbeforeRetreat", Event, From, To)
  
  if From=="Retreating" then
    local text=string.format("Group %s is already retreating.", tostring(Controllable:GetName()))
    self:T2(self.lid..text)
    return false
  else
    return true
  end
  
end

--- After "Retreat" event. Find a random point in the retreat zone and route the group there.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function SUPPRESSION:onafterRetreat(Controllable, From, Event, To)
  self:_EventFromTo("onafterRetreat", Event, From, To)
  
  -- Route the group to a zone.
  local text=string.format("Group %s is retreating! Alarm state green.", Controllable:GetName())
  MESSAGE:New(text, 10):ToAllIf(self.Debug)
  self:T(self.lid..text)
  
  -- Get a random point in the retreat zone.
  local ZoneCoord=self.RetreatZone:GetRandomCoordinate() -- Core.Point#COORDINATE
  local ZoneVec2=ZoneCoord:GetVec2()

  -- Debug smoke zone and point.
  if self.smoke or self.Debug then
    ZoneCoord:SmokeBlue()
  end
  if self.Debug then
    self.RetreatZone:SmokeZone(SMOKECOLOR.Red, 12)
  end
  
  -- Set ROE to weapon hold.
  self:_SetROE(SUPPRESSION.ROE.Hold)
  
  -- Set the ALARM STATE to GREEN. Then the unit will move even if it is under fire.
  self:_SetAlarmState(SUPPRESSION.AlarmState.Green)
  
  -- Make unit run to retreat zone and wait there for ~two hours.
  self:_Run(ZoneCoord, self.Speed, self.Formation, self.RetreatWait)
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Before "Retreateded" event. Check that the group is really in the retreat zone.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function SUPPRESSION:onbeforeRetreated(Controllable, From, Event, To)
  self:_EventFromTo("onbeforeRetreated", Event, From, To)
  
  -- Check that the group is inside the zone.
  local inzone=self.RetreatZone:IsVec3InZone(Controllable:GetVec3())
  
  return inzone
end

--- After "Retreateded" event. Group has reached the retreat zone. Set ROE to return fire and alarm state to auto.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function SUPPRESSION:onafterRetreated(Controllable, From, Event, To)
  self:_EventFromTo("onafterRetreated", Event, From, To)
  
  -- Set ROE to weapon return fire.
  self:_SetROE(SUPPRESSION.ROE.Return)
  
  -- Set the ALARM STATE to GREEN. Then the unit will move even if it is under fire.
  self:_SetAlarmState(SUPPRESSION.AlarmState.Auto)
  
  -- TODO: Add hold task? Move from _Run()
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- After "Dead" event, when a unit has died. When all units of a group are dead, FSM is stopped and eventhandler removed.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function SUPPRESSION:onafterDead(Controllable, From, Event, To)
  self:_EventFromTo("onafterDead", Event, From, To)
  
  local group=self.Controllable --Wrapper.Group#GROUP
  
  if group then
  
    -- Number of units left in the group.
    local nunits=group:CountAliveUnits()
        
    local text=string.format("Group %s: One of our units just died! %d units left.", self.Controllable:GetName(), nunits)
    MESSAGE:New(text, 10):ToAllIf(self.Debug)
    self:T(self.lid..text)
        
    -- Go to stop state.
    if nunits==0 then
      self:Stop()
    end
    
  else
    self:Stop()
  end
  
end

--- After "Stop" event.
-- @param #SUPPRESSION self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function SUPPRESSION:onafterStop(Controllable, From, Event, To)
  self:_EventFromTo("onafterStop", Event, From, To)
      
  local text=string.format("Stopping SUPPRESSION for group %s", self.Controllable:GetName())
  MESSAGE:New(text, 10):ToAllIf(self.Debug)
  self:I(self.lid..text)
      
  -- Clear all pending schedules
  self.CallScheduler:Clear()
  
  if self.mooseevents then
    self:UnHandleEvent(EVENTS.Dead)
    self:UnHandleEvent(EVENTS.Hit)
  else
    world.removeEventHandler(self)
  end
  
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Event Handler
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Event handler for suppressed groups.
--@param #SUPPRESSION self
function SUPPRESSION:onEvent(Event)
  --self:E(event)
  
  if Event == nil or Event.initiator == nil or Unit.getByName(Event.initiator:getName()) == nil then
    return true
  end
    
  local EventData={}
  if Event.initiator then
    EventData.IniDCSUnit   = Event.initiator
    EventData.IniUnitName  = Event.initiator:getName()
    EventData.IniDCSGroup  = Event.initiator:getGroup()
    EventData.IniGroupName = Event.initiator:getGroup():getName()
    EventData.IniGroup     = GROUP:FindByName(EventData.IniGroupName)
    EventData.IniUnit      = UNIT:FindByName(EventData.IniUnitName)
  end

  if Event.target then
    EventData.TgtDCSUnit   = Event.target
    EventData.TgtUnitName  = Event.target:getName()
    EventData.TgtDCSGroup  = Event.target:getGroup()
    EventData.TgtGroupName = Event.target:getGroup():getName()
    EventData.TgtGroup     = GROUP:FindByName(EventData.TgtGroupName)
    EventData.TgtUnit      = UNIT:FindByName(EventData.TgtUnitName)
  end  
  
  
  -- Event HIT
  if Event.id == world.event.S_EVENT_HIT then
    self:_OnEventHit(EventData)
  end

  -- Event DEAD
  if Event.id == world.event.S_EVENT_DEAD then
    self:_OnEventDead(EventData)
  end
  
end

--- Event handler for Dead event of suppressed groups.
-- @param #SUPPRESSION self
-- @param Core.Event#EVENTDATA EventData
function SUPPRESSION:_OnEventHit(EventData)
  self:F3(EventData)

  local GroupNameSelf=self.Controllable:GetName()
  local GroupNameTgt=EventData.TgtGroupName
  local TgtUnit=EventData.TgtUnit
  local tgt=EventData.TgtDCSUnit
  local IniUnit=EventData.IniUnit

  -- Check that correct group was hit.
  if GroupNameTgt == GroupNameSelf then
  
    self:T(self.lid..string.format("Hit event at t = %5.1f", timer.getTime()))
  
    -- Flare unit that was hit.
    if self.flare or self.Debug then
      TgtUnit:FlareRed()
    end
    
    -- Increase Hit counter.
    self.Nhit=self.Nhit+1

    -- Info on hit times.
    self:T(self.lid..string.format("Group %s has just been hit %d times.", self.Controllable:GetName(), self.Nhit))
    
    --self:Status()
    local life=tgt:getLife()/(tgt:getLife0()+1)*100
    self:T2(self.lid..string.format("Target unit life = %5.1f", life))
  
    -- FSM Hit event.
    self:__Hit(3, TgtUnit, IniUnit)
  end

end

--- Event handler for Dead event of suppressed groups.
-- @param #SUPPRESSION self
-- @param Core.Event#EVENTDATA EventData
function SUPPRESSION:_OnEventDead(EventData)

  local GroupNameSelf=self.Controllable:GetName()
  local GroupNameIni=EventData.IniGroupName

  -- Check for correct group.
  if  GroupNameIni==GroupNameSelf then
    
    -- Dead Unit.
    local IniUnit=EventData.IniUnit --Wrapper.Unit#UNIT
    local IniUnitName=EventData.IniUnitName
    
    if EventData.IniUnit then
      self:T2(self.lid..string.format("Group %s: Dead MOOSE unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName))
    else
      self:T2(self.lid..string.format("Group %s: Dead MOOSE unit DOES NOT not exist! Unit name %s.", GroupNameIni, IniUnitName))
    end
    
    if EventData.IniDCSUnit then
      self:T2(self.lid..string.format("Group %s: Dead DCS unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName))
    else
      self:T2(self.lid..string.format("Group %s: Dead DCS unit DOES NOT exist! Unit name %s.", GroupNameIni, IniUnitName))
    end
    
    -- Flare unit that died.
    if IniUnit and (self.flare or self.Debug) then
      IniUnit:FlareWhite()
      self:T(self.lid..string.format("Flare Dead MOOSE unit."))
    end
    
    -- Flare unit that died.
    if EventData.IniDCSUnit and (self.flare or self.Debug) then
      local p=EventData.IniDCSUnit:getPosition().p
      trigger.action.signalFlare(p, trigger.flareColor.Yellow , 0)
      self:T(self.lid..string.format("Flare Dead DCS unit."))
    end
       
    -- Get status.
    self:Status()
    
    -- FSM Dead event.
    self:__Dead(0.1)
    
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Suppress fire of a unit by setting its ROE to "Weapon Hold".
-- @param #SUPPRESSION self
function SUPPRESSION:_Suppress()

  -- Current time.
  local Tnow=timer.getTime()
  
  -- Controllable
  local Controllable=self.Controllable --Wrapper.Controllable#CONTROLLABLE
  
  -- Group will hold their weapons.
  self:_SetROE(SUPPRESSION.ROE.Hold)
  
  -- Get randomized time the unit is suppressed.
  local sigma=(self.Tsuppress_max-self.Tsuppress_min)/4
  local Tsuppress=self:_Random_Gaussian(self.Tsuppress_ave,sigma,self.Tsuppress_min, self.Tsuppress_max)
  
  -- Time at which the suppression is over.
  local renew=true
  if self.TsuppressionOver ~= nil then
    if Tsuppress+Tnow > self.TsuppressionOver then
      self.TsuppressionOver=Tnow+Tsuppress
    else
      renew=false
    end
  else
    self.TsuppressionOver=Tnow+Tsuppress
  end
  
  -- Recovery event will be called in Tsuppress seconds.
  if renew then
    self:__Recovered(self.TsuppressionOver-Tnow)
  end
  
  -- Debug message.
  local text=string.format("Group %s is suppressed for %d seconds. Suppression ends at %d:%02d.", Controllable:GetName(), Tsuppress, self.TsuppressionOver/60, self.TsuppressionOver%60)
  MESSAGE:New(text, 10):ToAllIf(self.Debug)
  self:T(self.lid..text)

end


--- Make group run/drive to a certain point. We put in several intermediate waypoints because sometimes the group stops before it arrived at the desired point.
--@param #SUPPRESSION self
--@param Core.Point#COORDINATE fin Coordinate where we want to go.
--@param #number speed Speed of group. Default is 20.
--@param #string formation Formation of group. Default is "Vee".
--@param #number wait Time the group will wait/hold at final waypoint. Default is 30 seconds.
function SUPPRESSION:_Run(fin, speed, formation, wait)

  speed=speed or 20
  formation=formation or ENUMS.Formation.Vehicle.OffRoad
  wait=wait or 30

  local group=self.Controllable -- Wrapper.Group#GROUP
  
  if group and group:IsAlive() then
  
    -- Clear all tasks.
    --group:ClearTasks()
    
    -- Current coordinates of group.
    local ini=group:GetCoordinate()
    
    -- Distance between current and final point. 
    local dist=ini:Get2DDistance(fin)
    
    -- Heading from ini to fin.
    local heading=self:_Heading(ini, fin)
   
    -- Waypoint and task arrays.
    local wp={}
    local tasks={}
    
    -- First waypoint is the current position of the group.
    wp[1]=ini:WaypointGround(speed, formation)
  
    if self.Debug then  
      local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName()))
    end
    
      -- Task to hold.
    local ConditionWait=group:TaskCondition(nil, nil, nil, nil, wait, nil)
    local TaskHold = group:TaskHold()
    
    -- Task combo to make group hold at final waypoint.
    local TaskComboFin = {}
    TaskComboFin[#TaskComboFin+1] = group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, true)
    TaskComboFin[#TaskComboFin+1] = group:TaskControlled(TaskHold, ConditionWait)
    
    -- Final waypoint.
    wp[#wp+1]=fin:WaypointGround(speed, formation, TaskComboFin)
    if self.Debug then
      local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)", #wp, self.Controllable:GetName()))
    end    
    
    -- Submit task and route group along waypoints.
    group:Route(wp)
    
  else
    self:E(self.lid..string.format("ERROR: Group is not alive!"))
  end

end

--- Function called when group is passing a waypoint. At the last waypoint we set the group back to CombatReady.
--@param Wrapper.Group#GROUP group Group which is passing a waypoint.
--@param #SUPPRESSION Fsm The suppression object.
--@param #number i Waypoint number that has been reached.
--@param #boolean final True if it is the final waypoint. Start Fightback.
function SUPPRESSION._Passing_Waypoint(group, Fsm, i, final)

  -- Debug message.
  local text=string.format("Group %s passing waypoint %d (final=%s)", group:GetName(), i, tostring(final))
  MESSAGE:New(text,10):ToAllIf(Fsm.Debug)
  if Fsm.Debug then
    env.info(Fsm.lid..text)
  end

  if final then
    if Fsm:is("Retreating") then
      -- Retreated-->Retreated.
      Fsm:Retreated()
    else
    -- FightBack-->Combatready: Change alarm state back to default.  
      Fsm:FightBack()
    end
  end
end


--- Search a place to hide. This is any scenery object in the vicinity.
--@param #SUPPRESSION self
--@return Core.Point#COORDINATE Coordinate of the hideout place.
--@return nil If no scenery object is within search radius.
function SUPPRESSION:_SearchHideout()
  -- We search objects in a zone with radius ~300 m around the group.
  local Zone = ZONE_GROUP:New("Zone_Hiding", self.Controllable, self.TakecoverRange)
  local gpos = self.Controllable:GetCoordinate()

  -- Scan for Scenery objects to run/drive to.
  Zone:Scan(Object.Category.SCENERY)
  
  -- Array with all possible hideouts, i.e. scenery objects in the vicinity of the group.
  local hideouts={}

  for SceneryTypeName, SceneryData in pairs(Zone:GetScannedScenery()) do
    for SceneryName, SceneryObject in pairs(SceneryData) do
    
      local SceneryObject = SceneryObject -- Wrapper.Scenery#SCENERY
      
      -- Position of the scenery object.
      local spos=SceneryObject:GetCoordinate()
      
      -- Distance from group to hideout.
      local distance= spos:Get2DDistance(gpos)
      
      if self.Debug then
        -- Place markers on every possible scenery object.
        local MarkerID=SceneryObject:GetCoordinate():MarkToAll(string.format("%s scenery object %s", self.Controllable:GetName(),SceneryObject:GetTypeName()))
        local text=string.format("%s scenery: %s, Coord %s", self.Controllable:GetName(), SceneryObject:GetTypeName(), SceneryObject:GetCoordinate():ToStringLLDMS())
        self:T2(self.lid..text)
      end
      
      -- Add to table.
      table.insert(hideouts, {object=SceneryObject, distance=distance})      
    end
  end
  
  -- Get random hideout place.
  local Hideout=nil
  if #hideouts>0 then
  
    -- Debug info.
    self:T(self.lid.."Number of hideouts "..#hideouts)
    
    -- Sort results table wrt number of hits.
    local _sort = function(a,b) return a.distance < b.distance end
    table.sort(hideouts,_sort)
    
    -- Pick a random location.
    --Hideout=hideouts[math.random(#hideouts)].object
    
    -- Pick closest location.
    Hideout=hideouts[1].object:GetCoordinate()
    
  else
    self:E(self.lid.."No hideouts found!")
  end
  
  return Hideout

end

--- Get (relative) life in percent of a group. Function returns the value of the units with the smallest and largest life. Also the average value of all groups is returned.
-- @param #SUPPRESSION self
-- @return #number Smallest life value of all units.
-- @return #number Largest life value of all units.
-- @return #number Average life value of all alife groups
-- @return #number Average life value of all groups including already dead ones.
-- @return #number Relative group strength.
function SUPPRESSION:_GetLife()

  local group=self.Controllable --Wrapper.Group#GROUP
  
  if group and group:IsAlive() then
  
    local units=group:GetUnits()
  
    local life_min=nil
    local life_max=nil
    local life_ave=0
    local life_ave0=0
    local n=0
    
    local groupstrength=#units/self.IniGroupStrength*100
    
    self:T2(self.lid..string.format("Group %s _GetLife nunits = %d", self.Controllable:GetName(), #units))
    
    for _,unit in pairs(units) do
    
      local unit=unit -- Wrapper.Unit#UNIT
      if unit and unit:IsAlive() then
        n=n+1
        local life=unit:GetLife()/(unit:GetLife0()+1)*100
        if life_min==nil or life < life_min then
          life_min=life
        end
        if life_max== nil or life > life_max then
          life_max=life
        end
        life_ave=life_ave+life
        if self.Debug then
          local text=string.format("n=%02d: Life = %3.1f, Life0 = %3.1f, min=%3.1f, max=%3.1f, ave=%3.1f, group=%3.1f", n, unit:GetLife(), unit:GetLife0(), life_min, life_max, life_ave/n,groupstrength)
          self:T2(self.lid..text)
        end
      end
      
    end
    
    -- If the counter did not increase (can happen!) return 0
    if n==0 then
      return 0,0,0,0,0
    end
    
    -- Average life relative to initial group strength including the dead ones.
    life_ave0=life_ave/self.IniGroupStrength
    
    -- Average life of all alive units.
    life_ave=life_ave/n    
    
    return life_min, life_max, life_ave, life_ave0, groupstrength
  else
    return 0, 0, 0, 0, 0
  end
end


--- Heading from point a to point b in degrees.
--@param #SUPPRESSION self
--@param Core.Point#COORDINATE a Coordinate.
--@param Core.Point#COORDINATE b Coordinate.
--@return #number angle Angle from a to b in degrees.
function SUPPRESSION:_Heading(a, b)
  local dx = b.x-a.x
  local dy = b.z-a.z
  local angle = math.deg(math.atan2(dy,dx))
  if angle < 0 then
    angle = 360 + angle
  end
  return angle
end

--- Generate Gaussian pseudo-random numbers.
-- @param #SUPPRESSION self
-- @param #number x0 Expectation value of distribution.
-- @param #number sigma (Optional) Standard deviation. Default 10.
-- @param #number xmin (Optional) Lower cut-off value.
-- @param #number xmax (Optional) Upper cut-off value.
-- @return #number Gaussian random number.
function SUPPRESSION:_Random_Gaussian(x0, sigma, xmin, xmax)

  -- Standard deviation. Default 5 if not given.
  sigma=sigma or 5
    
  local r
  local gotit=false
  local i=0
  while not gotit do
  
    -- Uniform numbers in [0,1). We need two.
    local x1=math.random()
    local x2=math.random()
  
    -- Transform to Gaussian exp(-(x-x0)²/(2*sigma²).
    r = math.sqrt(-2*sigma*sigma * math.log(x1)) * math.cos(2*math.pi * x2) + x0
    
    i=i+1
    if (r>=xmin and r<=xmax) or i>100 then
      gotit=true
    end
  end
  
  return r

end

--- Sets the ROE for the group and updates the current ROE variable.
-- @param #SUPPRESSION self
-- @param #string roe ROE the group will get. Possible "Free", "Hold", "Return". Default is self.DefaultROE.
function SUPPRESSION:_SetROE(roe)
  local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE
  
  -- If no argument is given, we take the default ROE.
  roe=roe or self.DefaultROE
  
  -- Update the current ROE.
  self.CurrentROE=roe
  
  -- Set the ROE.
  if roe==SUPPRESSION.ROE.Free then
    group:OptionROEOpenFire()
  elseif roe==SUPPRESSION.ROE.Hold then
    group:OptionROEHoldFire()
  elseif roe==SUPPRESSION.ROE.Return then
    group:OptionROEReturnFire()
  else
    self:E(self.lid.."Unknown ROE requested: "..tostring(roe))
    group:OptionROEOpenFire()
    self.CurrentROE=SUPPRESSION.ROE.Free
  end
  
  local text=string.format("Group %s now has ROE %s.", self.Controllable:GetName(), self.CurrentROE)
  self:T(self.lid..text)
end

--- Sets the alarm state of the group and updates the current alarm state variable.
-- @param #SUPPRESSION self
-- @param #string state Alarm state the group will get. Possible "Auto", "Green", "Red". Default is self.DefaultAlarmState.
function SUPPRESSION:_SetAlarmState(state)
  local group=self.Controllable --Wrapper.Controllable#CONTROLLABLE
  
  -- Input or back to default alarm state.
  state=state or self.DefaultAlarmState
  
  -- Update the current alam state of the group.
  self.CurrentAlarmState=state
  
  -- Set the alarm state.
  if state==SUPPRESSION.AlarmState.Auto then
    group:OptionAlarmStateAuto()
  elseif state==SUPPRESSION.AlarmState.Green then
    group:OptionAlarmStateGreen()
  elseif state==SUPPRESSION.AlarmState.Red then
    group:OptionAlarmStateRed()
  else
    self:E(self.lid.."Unknown alarm state requested: "..tostring(state))
    group:OptionAlarmStateAuto()
    self.CurrentAlarmState=SUPPRESSION.AlarmState.Auto
  end
  
  local text=string.format("Group %s now has Alarm State %s.", self.Controllable:GetName(), self.CurrentAlarmState)
  self:T(self.lid..text)
end

--- Print event-from-to string to DCS log file. 
-- @param #SUPPRESSION self
-- @param #string BA Before/after info.
-- @param #string Event Event.
-- @param #string From From state.
-- @param #string To To state.
function SUPPRESSION:_EventFromTo(BA, Event, From, To)
  local text=string.format("\n%s: %s EVENT %s: %s --> %s", BA, self.Controllable:GetName(), Event, From, To)
  self:T2(self.lid..text)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- **Functional** - Basic ATC.
--  
-- ![Banner Image](..\Presentations\PSEUDOATC\PSEUDOATC_Main.jpg)
-- 
-- ====
-- 
-- The pseudo ATC enhances the standard DCS ATC functions.
-- 
-- In particular, a menu entry "Pseudo ATC" is created in the "F10 Other..." radiomenu.
-- 
-- ## Features:
-- 
-- * Weather report at nearby airbases and mission waypoints.
-- * Report absolute bearing and range to nearest airports and mission waypoints.
-- * Report current altitude AGL of own aircraft.
-- * Upon request, ATC reports altitude until touchdown.
-- * Works with static and dynamic weather.
-- * Player can select the unit system (metric or imperial) in which information is reported.
-- * All maps supported (Caucasus, NTTR, Normandy, Persian Gulf and all future maps).
--  
-- ====
-- 
-- # YouTube Channel
-- 
-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg)
-- 
-- ===
-- 
-- ### Author: **funkyfranky**
-- 
-- ### Contributions: FlightControl, Applevangelist
-- 
-- ====
-- @module Functional.PseudoATC
-- @image Pseudo_ATC.JPG

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- PSEUDOATC class
-- @type PSEUDOATC
-- @field #string ClassName Name of the Class.
-- @field #table player Table comprising each player info.
-- @field #boolean Debug If true, print debug info to dcs.log file.
-- @field #number mdur Duration in seconds how low messages to the player are displayed.
-- @field #number mrefresh Interval in seconds after which the F10 menu is refreshed. E.g. by the closest airports. Default is 120 sec.
-- @field #number talt Interval in seconds between reporting altitude until touchdown. Default 3 sec.
-- @field #boolean chatty Display some messages on events like take-off and touchdown.
-- @field #boolean eventsmoose [Deprecated] If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler.
-- @field #boolean reportplayername If true, use playername not callsign on callouts
-- @extends Core.Base#BASE

--- Adds some rudimentary ATC functionality via the radio menu.
-- 
-- Local weather reports can be requested for nearby airports and player's mission waypoints.
-- The weather report includes
-- 
-- * QFE and QNH pressures,
-- * Temperature,
-- * Wind direction and strength.
-- 
-- The list of airports is updated every 60 seconds. This interval can be adjusted by the function @{#PSEUDOATC.SetMenuRefresh}(*interval*).
-- 
-- Likewise, absolute bearing and range to the close by airports and mission waypoints can be requested.
-- 
-- The player can switch the unit system in which all information is displayed during the mission with the MOOSE settings radio menu.
-- The unit system can be set to either imperial or metric. Altitudes are reported in feet or meter, distances in kilometers or nautical miles,
-- temperatures in degrees Fahrenheit or Celsius and QFE/QNH pressues in inHg or mmHg.
-- Note that the pressures are also reported in hPa independent of the unit system setting.
-- 
-- In bad weather conditions, the ATC can "talk you down", i.e. will continuously report your altitude on the final approach.
-- Default reporting time interval is 3 seconds. This can be adjusted via the @{#PSEUDOATC.SetReportAltInterval}(*interval*) function.
-- The reporting stops automatically when the player lands or can be stopped manually by clicking on the radio menu item again.
-- So the radio menu item acts as a toggle to switch the reporting on and off.
-- 
-- ## Scripting
-- 
-- Scripting is almost trivial. Just add the following two lines to your script:
-- 
--     pseudoATC=PSEUDOATC:New()
--     pseudoATC:Start()
-- 
-- 
-- @field #PSEUDOATC
PSEUDOATC={
  ClassName = "PSEUDOATC",
  group={},
  Debug=false,
  mdur=30,
  mrefresh=120,
  talt=3,
  chatty=true,
  eventsmoose=true,
  reportplayername = false,
}

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Some ID to identify who we are in output of the DCS.log file.
-- @field #string id
PSEUDOATC.id="PseudoATC | "

--- PSEUDOATC version.
-- @field #number version
PSEUDOATC.version="0.10.5"

-----------------------------------------------------------------------------------------------------------------------------------------

-- TODO list
-- DONE: Add takeoff event.
-- DONE: Add user functions.
-- DONE: Refactor to use Moose event handling only

-----------------------------------------------------------------------------------------------------------------------------------------

--- PSEUDOATC contructor.
-- @param #PSEUDOATC self
-- @return #PSEUDOATC Returns a PSEUDOATC object.
function PSEUDOATC:New()

  -- Inherit BASE.
  local self=BASE:Inherit(self, BASE:New()) -- #PSEUDOATC
  
  -- Debug info
  self:E(PSEUDOATC.id..string.format("PseudoATC version %s", PSEUDOATC.version))

  -- Return object.
  return self
end

--- Starts the PseudoATC event handlers.
-- @param #PSEUDOATC self
function PSEUDOATC:Start()
  self:F()
  
  -- Debug info
  self:I(PSEUDOATC.id.."Starting PseudoATC")
  
  -- Handle events.
  self:HandleEvent(EVENTS.Birth,           self._OnBirth)
  self:HandleEvent(EVENTS.Land,            self._PlayerLanded)
  self:HandleEvent(EVENTS.Takeoff,         self._PlayerTakeOff)
  self:HandleEvent(EVENTS.PlayerLeaveUnit, self._PlayerLeft)
  self:HandleEvent(EVENTS.Crash,           self._PlayerLeft)
  
end

-----------------------------------------------------------------------------------------------------------------------------------------
-- User Functions

--- Debug mode on. Send messages to everone.
-- @param #PSEUDOATC self
function PSEUDOATC:DebugOn()
  self.Debug=true
end

--- Debug mode off. This is the default setting.
-- @param #PSEUDOATC self
function PSEUDOATC:DebugOff()
  self.Debug=false
end

--- Chatty mode on. Display some messages on take-off and touchdown.
-- @param #PSEUDOATC self
function PSEUDOATC:ChattyOn()
  self.chatty=true
end

--- Chatty mode off. Don't display some messages on take-off and touchdown.
-- @param #PSEUDOATC self
function PSEUDOATC:ChattyOff()
  self.chatty=false
end

--- Set duration how long messages are displayed.
-- @param #PSEUDOATC self
-- @param #number duration Time in seconds. Default is 30 sec.
function PSEUDOATC:SetMessageDuration(duration)
  self.mdur=duration or 30
end

--- Use player name, not call sign, in callouts
-- @param #PSEUDOATC self
function PSEUDOATC:SetReportPlayername()
  self.reportplayername = true
  return self
end

--- Set time interval after which the F10 radio menu is refreshed.
-- @param #PSEUDOATC self
-- @param #number interval Interval in seconds. Default is every 120 sec.
function PSEUDOATC:SetMenuRefresh(interval)
  self.mrefresh=interval or 120
end

--- [Deprecated] Enable/disable event handling by MOOSE or DCS.
-- @param #PSEUDOATC self
-- @param #boolean switch If true, events are handled by MOOSE (default). If false, events are handled directly by DCS.
function PSEUDOATC:SetEventsMoose(switch)
  self.eventsmoose=switch
end

--- Set time interval for reporting altitude until touchdown.
-- @param #PSEUDOATC self
-- @param #number interval Interval in seconds. Default is every 3 sec.
function PSEUDOATC:SetReportAltInterval(interval)
  self.talt=interval or 3
end

-----------------------------------------------------------------------------------------------------------------------------------------
-- Event Handling


--- Function called my MOOSE event handler when a player enters a unit.
-- @param #PSEUDOATC self
-- @param Core.Event#EVENTDATA EventData
function PSEUDOATC:_OnBirth(EventData)
  self:F({EventData=EventData})
  
  -- Get unit and player.
  local _unitName=EventData.IniUnitName  
  --local _unit, _playername=self:_GetPlayerUnitAndName(_unitName)
  local _unit = EventData.IniUnit
  local _playername = EventData.IniPlayerName
  
  -- Check if a player entered.
  if _unit and _playername then
    self:PlayerEntered(_unit)
  end               
 
end

--- Function called by MOOSE event handler when a player leaves a unit or dies. 
-- @param #PSEUDOATC self
-- @param Core.Event#EVENTDATA EventData
function PSEUDOATC:_PlayerLeft(EventData)
  self:F({EventData=EventData})

  -- Get unit and player.
  local _unitName=EventData.IniUnitName  
  --local _unit, _playername=self:_GetPlayerUnitAndName(_unitName)
  
  local _unit = EventData.IniUnit
  local _playername = EventData.IniPlayerName
  
  -- Check if a player left.
  if _unit and _playername then
    self:PlayerLeft(_unit)
  end
end

--- Function called by MOOSE event handler when a player landed. 
-- @param #PSEUDOATC self
-- @param Core.Event#EVENTDATA EventData
function PSEUDOATC:_PlayerLanded(EventData)
  self:F({EventData=EventData})

  -- Get unit, player and place.
  local _unitName=EventData.IniUnitName
  local _unit = EventData.IniUnit
  local _playername = EventData.IniPlayerName  
  --local _unit, _playername=self:_GetPlayerUnitAndName(_unitName)
  local _base=nil
  local _baseName=nil
  if EventData.place then
    _base=EventData.place
    _baseName=EventData.place:getName()
  end
  
  -- Call landed function.
  if _unit and _playername and _base then
    self:PlayerLanded(_unit, _baseName)
  end
end

--- Function called by MOOSE/DCS event handler when a player took off. 
-- @param #PSEUDOATC self
-- @param Core.Event#EVENTDATA EventData
function PSEUDOATC:_PlayerTakeOff(EventData)
  self:F({EventData=EventData})

  -- Get unit, player and place.
  local _unitName=EventData.IniUnitName
  local _unit = EventData.IniUnit
  local _playername = EventData.IniPlayerName  
  --local _unit,_playername=self:_GetPlayerUnitAndName(_unitName)
  local _base=nil
  local _baseName=nil
  if EventData.place then
    _base=EventData.place
    _baseName=EventData.place:getName()
  end
  
  -- Call take-off function.
  if _unit and _playername and _base then
    self:PlayerTakeOff(_unit, _baseName)
  end
end

-----------------------------------------------------------------------------------------------------------------------------------------
-- Event Functions

--- Function called when a player enters a unit.
-- @param #PSEUDOATC self
-- @param Wrapper.Unit#UNIT unit Unit the player entered.
function PSEUDOATC:PlayerEntered(unit)
  self:F2({unit=unit})

  -- Get player info.
  local group=unit:GetGroup() --Wrapper.Group#GROUP
  local GID=group:GetID()
  local GroupName=group:GetName()
  local PlayerName=unit:GetPlayerName()
  local UnitName=unit:GetName()
  local CallSign=unit:GetCallsign()
  local UID=unit:GetDCSObject():getID()
  
  if not self.group[GID] then
    self.group[GID]={}
    self.group[GID].player={}
  end
  
  
  -- Init player table.  
  self.group[GID].player[UID]={}
  self.group[GID].player[UID].group=group
  self.group[GID].player[UID].unit=unit
  self.group[GID].player[UID].groupname=GroupName
  self.group[GID].player[UID].unitname=UnitName
  self.group[GID].player[UID].playername=PlayerName
  self.group[GID].player[UID].callsign=CallSign
  self.group[GID].player[UID].waypoints=group:GetTaskRoute()
  
  -- Info message.
  local text=string.format("Player %s entered unit %s of group %s (id=%d).", PlayerName, UnitName, GroupName, GID)
  self:T(PSEUDOATC.id..text)
  MESSAGE:New(text, 30):ToAllIf(self.Debug)
  
  -- Create main F10 menu, i.e. "F10/Pseudo ATC"
    local countPlayerInGroup = 0
    for _ in pairs(self.group[GID].player) do countPlayerInGroup = countPlayerInGroup + 1 end
  if countPlayerInGroup <= 1 then
    self.group[GID].menu_main=missionCommands.addSubMenuForGroup(GID, "Pseudo ATC")
  end
  
  -- Create/update custom menu for player
  self:MenuCreatePlayer(GID,UID)
    
  -- Create/update list of nearby airports.
  self:LocalAirports(GID,UID)
  
  -- Create submenu of local airports.
  self:MenuAirports(GID,UID)
  
  -- Create submenu Waypoints.
  self:MenuWaypoints(GID,UID)
  
  -- Start scheduler to refresh the F10 menues.
  self.group[GID].player[UID].scheduler, self.group[GID].player[UID].schedulerid=SCHEDULER:New(nil, self.MenuRefresh, {self, GID, UID}, self.mrefresh, self.mrefresh)
 
end

--- Function called when a player has landed.
-- @param #PSEUDOATC self
-- @param Wrapper.Unit#UNIT unit Unit of player which has landed.
-- @param #string place Name of the place the player landed at.
function PSEUDOATC:PlayerLanded(unit, place)
  self:F2({unit=unit, place=place})
  
  -- Gather some information.
  local group=unit:GetGroup()
  local GID=group:GetID()
  local UID=unit:GetDCSObject():getID()
  local PlayerName = unit:GetPlayerName() or "Ghost"
  local UnitName = unit:GetName() or "Ghostplane"
  local GroupName = group:GetName() or "Ghostgroup"
  if self.Debug then
    -- Debug message.
    local text=string.format("Player %s in unit %s of group %s landed at %s.", PlayerName, UnitName, GroupName, place)
    self:T(PSEUDOATC.id..text)
    MESSAGE:New(text, 30):ToAllIf(self.Debug)
  end
  
  -- Stop altitude reporting timer if its activated.
  self:AltitudeTimerStop(GID,UID)
  
  -- Welcome message.
  if place and self.chatty then
    local text=string.format("Touchdown! Welcome to %s pilot %s. Have a nice day!", place,PlayerName)
    MESSAGE:New(text, self.mdur):ToGroup(group)
  end

end

--- Function called when a player took off.
-- @param #PSEUDOATC self
-- @param Wrapper.Unit#UNIT unit Unit of player which has landed.
-- @param #string place Name of the place the player landed at.
function PSEUDOATC:PlayerTakeOff(unit, place)
  self:F2({unit=unit, place=place})
  
  -- Gather some information.
  local group=unit:GetGroup()
  local PlayerName = unit:GetPlayerName() or "Ghost"
  local UnitName = unit:GetName() or "Ghostplane"
  local GroupName = group:GetName() or "Ghostgroup"
  local CallSign = unit:GetCallsign() or "Ghost11"
  if self.Debug then
    -- Debug message.
    local text=string.format("Player %s in unit %s of group %s took off at %s.", PlayerName, UnitName, GroupName, place)
    self:T(PSEUDOATC.id..text)
    MESSAGE:New(text, 30):ToAllIf(self.Debug)
  end
  -- Bye-Bye message.
  if place and self.chatty then
    local text=string.format("%s, %s, you are airborne. Have a safe trip!", place, CallSign)
  if self.reportplayername then
    text=string.format("%s, %s, you are airborne. Have a safe trip!", place, PlayerName)
  end
    MESSAGE:New(text, self.mdur):ToGroup(group)
  end

end

--- Function called when a player leaves a unit or dies. 
-- @param #PSEUDOATC self
-- @param Wrapper.Unit#UNIT unit Player unit which was left.
function PSEUDOATC:PlayerLeft(unit)
  self:F({unit=unit})
 
  -- Get id.
  local group=unit:GetGroup()
  local GID=group:GetID()
  local UID=unit:GetDCSObject():getID()
  
  if self.group[GID] and self.group[GID].player and self.group[GID].player[UID] then
    local PlayerName=self.group[GID].player[UID].playername
    local CallSign=self.group[GID].player[UID].callsign
    local UnitName=self.group[GID].player[UID].unitname
    local GroupName=self.group[GID].player[UID].groupname
  
    -- Debug message.
    local text=string.format("Player %s (callsign %s) of group %s just left unit %s.", PlayerName, CallSign, GroupName, UnitName)
    self:T(PSEUDOATC.id..text)
    MESSAGE:New(text, 30):ToAllIf(self.Debug)
    
    -- Stop scheduler for menu updates
    if self.group[GID].player[UID].schedulerid then
      self.group[GID].player[UID].scheduler:Stop(self.group[GID].player[UID].schedulerid)
    end
    
    -- Stop scheduler for reporting alt if it runs.
    self:AltitudeTimerStop(GID,UID)
    
    --  Remove own menu.
    if self.group[GID].player[UID].menu_own then
      missionCommands.removeItemForGroup(GID,self.group[GID].player[UID].menu_own)
    end
    -- Remove main menu.
    -- WARNING: Remove only if last human element of group 
    
    local countPlayerInGroup = 0
    for _ in pairs(self.group[GID].player) do countPlayerInGroup = countPlayerInGroup + 1 end
  
    if self.group[GID].menu_main and countPlayerInGroup==1 then
      missionCommands.removeItemForGroup(GID,self.group[GID].menu_main)
    end
  
    -- Remove player array.
    self.group[GID].player[UID]=nil
    
  end
end

-----------------------------------------------------------------------------------------------------------------------------------------
-- Menu Functions

--- Refreshes all player menues.
-- @param #PSEUDOATC self.
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player. 
function PSEUDOATC:MenuRefresh(GID,UID)
  self:F({GID=GID,UID=UID})
  -- Debug message.
  local text=string.format("Refreshing menues for player %s in group %s.", self.group[GID].player[UID].playername, self.group[GID].player[UID].groupname)
  self:T(PSEUDOATC.id..text)
  MESSAGE:New(text,30):ToAllIf(self.Debug)

  -- Clear menu.
  self:MenuClear(GID,UID)
  
  -- Create list of nearby airports.
  self:LocalAirports(GID,UID)
      
  -- Create submenu Local Airports.
  self:MenuAirports(GID,UID)
  
  -- Create submenu Waypoints etc.
  self:MenuWaypoints(GID,UID)
  
end

--- Create player menus.
-- @param #PSEUDOATC self.
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player. 
function PSEUDOATC:MenuCreatePlayer(GID,UID)
  self:F({GID=GID,UID=UID})
  -- Table for menu entries.
    local PlayerName=self.group[GID].player[UID].playername
    self.group[GID].player[UID].menu_own=missionCommands.addSubMenuForGroup(GID, PlayerName, self.group[GID].menu_main)
end



--- Clear player menus.
-- @param #PSEUDOATC self.
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player. 
function PSEUDOATC:MenuClear(GID,UID)
  self:F({GID=GID,UID=UID})

  -- Debug message.
  local text=string.format("Clearing menus for player %s in group %s.", self.group[GID].player[UID].playername, self.group[GID].player[UID].groupname)
  self:T(PSEUDOATC.id..text)
  MESSAGE:New(text,30):ToAllIf(self.Debug)
  
  -- Delete Airports menu.
  if self.group[GID].player[UID].menu_airports then
    missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_airports)
    self.group[GID].player[UID].menu_airports=nil
  else
    self:T2(PSEUDOATC.id.."No airports to clear menus.")
  end
 
  -- Delete waypoints menu.
  if self.group[GID].player[UID].menu_waypoints then
    missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_waypoints)
    self.group[GID].player[UID].menu_waypoints=nil
  end
  
  -- Delete report alt until touchdown menu command.
  if self.group[GID].player[UID].menu_reportalt then
    missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_reportalt)
    self.group[GID].player[UID].menu_reportalt=nil
  end

  -- Delete request current alt menu command.
  if self.group[GID].player[UID].menu_requestalt then
    missionCommands.removeItemForGroup(GID, self.group[GID].player[UID].menu_requestalt)
    self.group[GID].player[UID].menu_requestalt=nil
  end

end

--- Create "F10/Pseudo ATC/Local Airports/Airport Name/" menu items each containing weather report and BR request.
-- @param #PSEUDOATC self
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player. 
function PSEUDOATC:MenuAirports(GID,UID)
  self:F({GID=GID,UID=UID})

  -- Table for menu entries.
  self.group[GID].player[UID].menu_airports=missionCommands.addSubMenuForGroup(GID, "Local Airports", self.group[GID].player[UID].menu_own)
   
  local i=0
  for _,airport in pairs(self.group[GID].player[UID].airports) do
  
    i=i+1
    if i > 10 then
      break -- Max 10 airports due to 10 menu items restriction.
    end 
    
    local name=airport.name
    local d=airport.distance
    local pos=AIRBASE:FindByName(name):GetCoordinate()
    
    --F10menu_ATC_airports[ID][name] = missionCommands.addSubMenuForGroup(ID, name, F10menu_ATC)
    local submenu=missionCommands.addSubMenuForGroup(GID, name, self.group[GID].player[UID].menu_airports)
    
    -- Create menu reporting commands
    missionCommands.addCommandForGroup(GID, "Weather Report", submenu, self.ReportWeather, self, GID, UID, pos, name)
    missionCommands.addCommandForGroup(GID, "Request BR", submenu, self.ReportBR, self, GID, UID, pos, name)
    
    -- Debug message.
    self:T(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d", name, GID))
  end
end

--- Create "F10/Pseudo ATC/Waypoints/<Waypoint i>  menu items.
-- @param #PSEUDOATC self
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player. 
function PSEUDOATC:MenuWaypoints(GID, UID)
  self:F({GID=GID, UID=UID})

  -- Player unit and callsign.
--  local unit=self.group[GID].player[UID].unit --Wrapper.Unit#UNIT
  local callsign=self.group[GID].player[UID].callsign
  
  -- Debug info.
  self:T(PSEUDOATC.id..string.format("Creating waypoint menu for %s (ID %d).", callsign, GID))
     
  if #self.group[GID].player[UID].waypoints>0 then
  
    -- F10/PseudoATC/Waypoints
    self.group[GID].player[UID].menu_waypoints=missionCommands.addSubMenuForGroup(GID, "Waypoints", self.group[GID].player[UID].menu_own)

    local j=0    
    for i, wp in pairs(self.group[GID].player[UID].waypoints) do
    
      -- Increase counter
      j=j+1
      
      if j>10 then
        break -- max ten menu entries
      end
      
      -- Position of Waypoint
      local pos=COORDINATE:New(wp.x, wp.alt, wp.y)
      local name=string.format("Waypoint %d", i-1)
      if wp.name and wp.name ~= "" then
        name = string.format("Waypoint %s",wp.name)
      end
      -- "F10/PseudoATC/Waypoints/Waypoint X"
      local submenu=missionCommands.addSubMenuForGroup(GID, name, self.group[GID].player[UID].menu_waypoints)
      
      -- Menu commands for each waypoint "F10/PseudoATC/My Aircraft (callsign)/Waypoints/Waypoint X/<Commands>"
      missionCommands.addCommandForGroup(GID, "Weather Report", submenu, self.ReportWeather, self, GID, UID, pos, name)
      missionCommands.addCommandForGroup(GID, "Request BR", submenu, self.ReportBR, self, GID, UID, pos, name)
    end
  end
  
  self.group[GID].player[UID].menu_reportalt  = missionCommands.addCommandForGroup(GID, "Talk me down",     self.group[GID].player[UID].menu_own, self.AltidudeTimerToggle, self, GID, UID)
  self.group[GID].player[UID].menu_requestalt = missionCommands.addCommandForGroup(GID, "Request altitude", self.group[GID].player[UID].menu_own, self.ReportHeight, self, GID, UID)
end

-----------------------------------------------------------------------------------------------------------------------------------------
-- Reporting Functions

--- Weather Report. Report pressure QFE/QNH, temperature, wind at certain location.
-- @param #PSEUDOATC self
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player. 
-- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured.
-- @param #string location Name of the location at which the pressure is measured.
function PSEUDOATC:ReportWeather(GID, UID, position, location)
  self:F({GID=GID, UID=UID, position=position, location=location})
  
  -- Player unit system settings.
  local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS
  
  local text=string.format("Local weather at %s:\n", location)
   
  -- Get pressure in hPa.  
  local Pqnh=position:GetPressure(0)  -- Get pressure at sea level.
  local Pqfe=position:GetPressure()   -- Get pressure at (land) height of position.
  
  -- Pressure conversion
  local hPa2inHg=0.0295299830714
  local hPa2mmHg=0.7500615613030
  
  -- Unit conversion.
  local _Pqnh=string.format("%.2f inHg", Pqnh * hPa2inHg)
  local _Pqfe=string.format("%.2f inHg", Pqfe * hPa2inHg)
  if settings:IsMetric() then
    _Pqnh=string.format("%.1f mmHg", Pqnh * hPa2mmHg)
    _Pqfe=string.format("%.1f mmHg", Pqfe * hPa2mmHg)
  end  
 
  -- Message text. 
  text=text..string.format("QFE %.1f hPa = %s.\n", Pqfe, _Pqfe)
  text=text..string.format("QNH %.1f hPa = %s.\n", Pqnh, _Pqnh)
  
  -- Get temperature at position in degrees Celsius. 
  local T=position:GetTemperature()
    
  -- Correct unit system.
  local _T=string.format('%d°F', UTILS.CelsiusToFahrenheit(T))
  if settings:IsMetric() then
    _T=string.format('%d°C', T)
  end
  
  -- Message text.  
  local text=text..string.format("Temperature %s\n", _T)
  
  -- Get wind direction and speed.
  local Dir,Vel=position:GetWind()
  
  -- Get Beaufort wind scale.
  local Bn,Bd=UTILS.BeaufortScale(Vel)
  
  -- Formatted wind direction.
  local Ds = string.format('%03d°', Dir)
    
  -- Velocity in player units.
  local Vs=string.format("%.1f knots", UTILS.MpsToKnots(Vel))
  if settings:IsMetric() then
    Vs=string.format('%.1f m/s', Vel)  
  end  
  
  -- Message text.
  local text=text..string.format("%s, Wind from %s at %s (%s).", self.group[GID].player[UID].playername, Ds, Vs, Bd)
  
  -- Send message
  self:_DisplayMessageToGroup(self.group[GID].player[UID].unit, text, self.mdur, true)
  
end

--- Report absolute bearing and range form player unit to airport.
-- @param #PSEUDOATC self
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player. 
-- @param Core.Point#COORDINATE position Coordinates at which the pressure is measured.
-- @param #string location Name of the location at which the pressure is measured.
function PSEUDOATC:ReportBR(GID, UID, position, location)
  self:F({GID=GID, UID=UID, position=position, location=location})

  -- Current coordinates.
  local unit=self.group[GID].player[UID].unit --Wrapper.Unit#UNIT
  local coord=unit:GetCoordinate()
  
  -- Direction vector from current position (coord) to target (position).
  local angle=coord:HeadingTo(position)
  
  -- Range from current to 
  local range=coord:Get2DDistance(position)
  
  -- Bearing string.
  local Bs=string.format('%03d°', angle)
  
  -- Settings.
  local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS
  
  
  local Rs=string.format("%.1f NM", UTILS.MetersToNM(range))
  if settings:IsMetric() then
    Rs=string.format("%.1f km", range/1000)  
  end

  -- Message text.
  local text=string.format("%s: Bearing %s, Range %s.", location, Bs, Rs)

  -- Send message
  self:_DisplayMessageToGroup(self.group[GID].player[UID].unit, text, self.mdur, true)  
  
end

--- Report altitude above ground level of player unit.
-- @param #PSEUDOATC self
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player. 
-- @param #number dt (Optional) Duration the message is displayed.
-- @param #boolean _clear (Optional) Clear previouse messages. 
-- @return #number Altitude above ground.
function PSEUDOATC:ReportHeight(GID, UID, dt, _clear)
  self:F({GID=GID, UID=UID, dt=dt})

  local dt = dt or self.mdur
  if _clear==nil then
    _clear=false
  end

  -- Return height [m] above ground level.
  local function get_AGL(p)
    local agl=0
    local vec2={x=p.x,y=p.z}
    local ground=land.getHeight(vec2)
    local agl=p.y-ground
    return agl
  end

  -- Get height AGL.
  local unit=self.group[GID].player[UID].unit --Wrapper.Unit#UNIT
  
  if unit and unit:IsAlive() then
  
    local position=unit:GetCoordinate()
    local height=get_AGL(position)
    local callsign=unit:GetCallsign()
    local PlayerName=self.group[GID].player[UID].playername
  
    -- Settings.
    local settings=_DATABASE:GetPlayerSettings(self.group[GID].player[UID].playername) or _SETTINGS --Core.Settings#SETTINGS
    
    -- Height string.
    local Hs=string.format("%d ft", UTILS.MetersToFeet(height))
    if settings:IsMetric() then
      Hs=string.format("%d m", height)
    end
    
    -- Message text.
    local _text=string.format("%s, your altitude is %s AGL.", callsign, Hs)
    if self.reportplayername then
    _text=string.format("%s, your altitude is %s AGL.", PlayerName, Hs)
  end
    -- Append flight level.
    if _clear==false then
      _text=_text..string.format(" FL%03d.", position.y/30.48)
    end
    
    -- Send message to player group.  
    self:_DisplayMessageToGroup(self.group[GID].player[UID].unit,_text, dt,_clear)
    
    -- Return height
    return height
  end
  
  return 0        
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Toggle report altitude reporting on/off.
-- @param #PSEUDOATC self.
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player. 
function PSEUDOATC:AltidudeTimerToggle(GID,UID)
  self:F({GID=GID, UID=UID})
  
  if self.group[GID].player[UID].altimerid then
    -- If the timer is on, we turn it off.
    self:AltitudeTimerStop(GID, UID)
  else
    -- If the timer is off, we turn it on.
    self:AltitudeTimeStart(GID, UID)
  end
end

--- Start altitude reporting scheduler.
-- @param #PSEUDOATC self.
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player. 
function PSEUDOATC:AltitudeTimeStart(GID, UID)
  self:F({GID=GID, UID=UID})
  
  -- Debug info.
  self:T(PSEUDOATC.id..string.format("Starting altitude report timer for player ID %d.", UID))
  
  -- Start timer. Altitude is reported every ~3 seconds.
  self.group[GID].player[UID].altimer, self.group[GID].player[UID].altimerid=SCHEDULER:New(nil, self.ReportHeight, {self, GID, UID, 1, true}, 1, 3)
end

--- Stop/destroy DCS scheduler function for reporting altitude.
-- @param #PSEUDOATC self.
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player. 
function PSEUDOATC:AltitudeTimerStop(GID, UID)
  self:F({GID=GID,UID=UID})
  -- Debug info.
  self:T(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.", UID))
  
  -- Stop timer.
  if self.group[GID].player[UID].altimerid then
    self.group[GID].player[UID].altimer:Stop(self.group[GID].player[UID].altimerid)
  end
  
  self.group[GID].player[UID].altimer=nil
  self.group[GID].player[UID].altimerid=nil
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc

--- Create list of nearby airports sorted by distance to player unit.
-- @param #PSEUDOATC self
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player. 
function PSEUDOATC:LocalAirports(GID, UID)
  self:F({GID=GID, UID=UID})

  -- Airports table.  
  self.group[GID].player[UID].airports=nil
  self.group[GID].player[UID].airports={}
  
  -- Current player position.
  local pos=self.group[GID].player[UID].unit:GetCoordinate()
  
  -- Loop over coalitions.
  for i=0,2 do
    
    -- Get all airbases of coalition.
    local airports=coalition.getAirbases(i)
    
    -- Loop over airbases
    for _,airbase in pairs(airports) do
    
      local name=airbase:getName()
      local a=AIRBASE:FindByName(name)
      if a then
        local q=a:GetCoordinate()
        local d=q:Get2DDistance(pos)
      
        -- Add to table.
        table.insert(self.group[GID].player[UID].airports, {distance=d, name=name})
      end
      
    end
  end
  
  --- compare distance (for sorting airports)
  local function compare(a,b)
    return a.distance < b.distance
  end
  
  -- Sort airports table w.r.t. distance to player.
  table.sort(self.group[GID].player[UID].airports, compare)
  
end

--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned. 
-- @param #PSEUDOATC self
-- @param #string _unitName Name of the player unit.
-- @return Wrapper.Unit#UNIT Unit of player.
-- @return #string Name of the player.
-- @return nil If player does not exist.
function PSEUDOATC:_GetPlayerUnitAndName(_unitName)
  self:F(_unitName)

  if _unitName ~= nil then
  
    -- Get DCS unit from its name.
    local DCSunit=Unit.getByName(_unitName)
    if DCSunit then
    
      -- Get the player name to make sure a player entered.  
      local playername=DCSunit:getPlayerName()
      local unit=UNIT:Find(DCSunit)
      
      -- Debug output.
      self:T2({DCSunit=DCSunit, unit=unit, playername=playername})
      
      if unit and playername then        
        -- Return MOOSE unit and player name
        return unit, playername
      end
      
    end    
  end
    
  return nil,nil
end


--- Display message to group.
-- @param #PSEUDOATC self
-- @param Wrapper.Unit#UNIT _unit Player unit.
-- @param #string _text Message text.
-- @param #number _time Duration how long the message is displayed.
-- @param #boolean _clear Clear up old messages.
function PSEUDOATC:_DisplayMessageToGroup(_unit, _text, _time, _clear)
  self:F({unit=_unit, text=_text, time=_time, clear=_clear})
  
  _time=_time or self.Tmsg
  if _clear==nil then
    _clear=false
  end
  
  -- Group ID.
  local _gid=_unit:GetGroup():GetID()
  
  if _gid then
    if _clear == true then
      trigger.action.outTextForGroup(_gid, _text, _time, _clear)
    else
      trigger.action.outTextForGroup(_gid, _text, _time)
    end
  end
  
end

--- Returns a string which consits of this callsign and the player name.  
-- @param #PSEUDOATC self
-- @param #string unitname Name of the player unit.
function PSEUDOATC:_myname(unitname)
  self:F2(unitname)
  
  local unit=UNIT:FindByName(unitname)
  local pname=unit:GetPlayerName()
  local csign=unit:GetCallsign()
  
  return string.format("%s (%s)", csign, pname)
end
--- **Functional** - Simulation of logistic operations.
--
-- ===
--
-- ## Features:
--
--    * Holds (virtual) assets in stock and spawns them upon request.
--    * Manages requests of assets from other warehouses.
--    * Queueing system with optional prioritization of requests.
--    * Realistic transportation of assets between warehouses.
--    * Different means of automatic transportation (planes, helicopters, APCs, self propelled).
--    * Strategic components such as capturing, defending and destroying warehouses and their associated infrastructure.
--    * Intelligent spawning of aircraft on airports (only if enough parking spots are available).
--    * Possibility to hook into events and customize actions.
--    * Persistence of assets. Warehouse assets can be saved and loaded from file.
--    * Can be easily interfaced to other MOOSE classes.
--
-- ===
--
-- ## Youtube Videos:
--
--    * [Warehouse Trailer](https://www.youtube.com/watch?v=e98jzLi5fGk)
--    * [DCS Warehouse Airbase Resources Proof Of Concept](https://www.youtube.com/watch?v=YeuGL0duEgY)
--
-- ===
--
-- ## Missions:
--
-- ===
--
-- The MOOSE warehouse concept simulates the organization and implementation of complex operations regarding the flow of assets between the point of origin and the point of consumption
-- in order to meet requirements of a potential conflict. In particular, this class is concerned with maintaining army supply lines while disrupting those of the enemy, since an armed
-- force without resources and transportation is defenseless.
--
-- ===
--
-- ### Author: **funkyfranky**
-- ### Co-author: FlightControl (cargo dispatcher classes)
--
-- ===
--
-- @module Functional.Warehouse
-- @image Warehouse.JPG

--- WAREHOUSE class.
-- @type WAREHOUSE
-- @field #string ClassName Name of the class.
-- @field #boolean Debug If true, send debug messages to all.
-- @field #number verbosity Verbosity level.
-- @field #string wid Identifier of the warehouse printed before other output to DCS.log file.
-- @field #boolean Report If true, send status messages to coalition.
-- @field Wrapper.Static#STATIC warehouse The phyical warehouse structure.
-- @field #string alias Alias of the warehouse. Name its called when sending messages.
-- @field Core.Zone#ZONE zone Zone around the warehouse. If this zone is captured, the warehouse and all its assets goes to the capturing coalition.
-- @field Wrapper.Airbase#AIRBASE airbase Airbase the warehouse belongs to.
-- @field #string airbasename Name of the airbase associated to the warehouse.
-- @field Core.Point#COORDINATE road Closest point to warehouse on road.
-- @field Core.Point#COORDINATE rail Closest point to warehouse on rail.
-- @field Core.Zone#ZONE spawnzone Zone in which assets are spawned.
-- @field #number uid Unique ID of the warehouse.
-- @field #boolean markerOn If true, markers are displayed on the F10 map.
-- @field Wrapper.Marker#MARKER markerWarehouse Marker warehouse.
-- @field Wrapper.Marker#MARKER markerRoad Road connection.
-- @field Wrapper.Marker#MARKER markerRail Rail road connection.
-- @field #number markerid ID of the warehouse marker at the airbase.
-- @field #number dTstatus Time interval in seconds of updating the warehouse status and processing new events. Default 30 seconds.
-- @field #number queueid Unit id of each request in the queue. Essentially a running number starting at one and incremented when a new request is added.
-- @field #table stock Table holding all assets in stock. Table entries are of type @{#WAREHOUSE.Assetitem}.
-- @field #table queue Table holding all queued requests. Table entries are of type @{#WAREHOUSE.Queueitem}.
-- @field #table pending Table holding all pending requests, i.e. those that are currently in progress. Table elements are of type @{#WAREHOUSE.Pendingitem}.
-- @field #table transporting Table holding assets currently transporting cargo assets.
-- @field #table delivered Table holding all delivered requests. Table elements are #boolean. If true, all cargo has been delivered.
-- @field #table defending Table holding all defending requests, i.e. self requests that were if the warehouse is under attack. Table elements are of type @{#WAREHOUSE.Pendingitem}.
-- @field Core.Zone#ZONE portzone Zone defining the port of a warehouse. This is where naval assets are spawned.
-- @field #table shippinglanes Table holding the user defined shipping between warehouses.
-- @field #table offroadpaths Table holding user defined paths from one warehouse to another.
-- @field #boolean autodefence When the warehouse is under attack, automatically spawn assets to defend the warehouse.
-- @field #number spawnzonemaxdist Max distance between warehouse and spawn zone. Default 5000 meters.
-- @field #boolean autosave Automatically save assets to file when mission ends.
-- @field #string autosavepath Path where the asset file is saved on auto save.
-- @field #string autosavefile File name of the auto asset save file. Default is auto generated from warehouse id and name.
-- @field #boolean safeparking If true, parking spots for aircraft are considered as occupied if e.g. a client aircraft is parked there. Default false.
-- @field #boolean isUnit If `true`, warehouse is represented by a unit instead of a static.
-- @field #boolean isShip If `true`, warehouse is represented by a ship unit.
-- @field #number lowfuelthresh Low fuel threshold. Triggers the event AssetLowFuel if for any unit fuel goes below this number.
-- @field #boolean respawnafterdestroyed If true, warehouse is respawned after it was destroyed. Assets are kept.
-- @field #number respawndelay Delay before respawn in seconds.
-- @field #number runwaydestroyed Time stamp timer.getAbsTime() when the runway was destroyed.
-- @field #number runwayrepairtime Time in seconds until runway will be repaired after it was destroyed. Default is 3600 sec (one hour).
-- @field OPS.FlightControl#FLIGHTCONTROL flightcontrol Flight control of this warehouse.
-- @extends Core.Fsm#FSM

--- Have your assets at the right place at the right time - or not!
--
-- ===
--
-- # The Warehouse Concept
--
-- The MOOSE warehouse adds a new logistic component to the DCS World. *Assets*, i.e. ground, airborne and naval units, can be transferred from one place
-- to another in a realistic and highly automatic fashion. In contrast to a "DCS warehouse" these assets have a physical representation in game. In particular,
-- this means they can be destroyed during the transport and add more life to the DCS world.
--
-- This comes along with some additional interesting strategic aspects since capturing/defending and destroying/protecting an enemy or your
-- own warehouse becomes of critical importance for the development of a conflict.
--
-- In essence, creating an efficient network of warehouses is vital for the success of a battle or even the whole war. Likewise, of course, cutting off the enemy
-- of important supply lines by capturing or destroying warehouses or their associated infrastructure is equally important.
--
-- ## What is a warehouse?
--
-- A warehouse is an abstract object represented by a physical (static) building that can hold virtual assets in stock.
-- It can (but it must not) be associated with a particular airbase. The associated airbase can be an airdrome, a Helipad/FARP or a ship.
--
-- If another warehouse requests assets, the corresponding troops are spawned at the warehouse and being transported to the requestor or go their
-- by themselfs. Once arrived at the requesting warehouse, the assets go into the stock of the requestor and can be activated/deployed when necessary.
--
-- ## What assets can be stored?
--
-- Any kind of ground, airborne or naval asset can be stored and are spawned upon request.
-- The fact that the assets live only virtually in stock and are put into the game only when needed has a positive impact on the game performance.
-- It also alliviates the problem of limited parking spots at smaller airbases.
--
-- ## What means of transportation are available?
--
-- Firstly, all mobile assets can be send from warehouse to another on their own.
--
-- * Ground vehicles will use the road infrastructure. So a good road connection for both warehouses is important but also off road connections can be added if necessary.
-- * Airborne units get a flightplan from the airbase of the sending warehouse to the airbase of the receiving warehouse. This already implies that for airborne
-- assets both warehouses need an airbase. If either one of the warehouses does not have an associated airbase, direct transportation of airborne assets is not possible.
-- * Naval units can be exchanged between warehouses which possess a port, which can be defined by the user. Also shipping lanes must be specified manually but the user since DCS does not provide these.
-- * Trains (would) use the available railroad infrastructure and both warehouses must have a connection to the railroad. Unfortunately, however, trains are not yet implemented to
-- a reasonable degree in DCS at the moment and hence cannot be used yet.
--
-- Furthermore, ground assets can be transferred between warehouses by transport units. These are APCs, helicopters and airplanes. The transportation process is modeled
-- in a realistic way by using the corresponding cargo dispatcher classes, i.e.
--
-- * @{AI.AI_Cargo_Dispatcher_APC#AI_DISPATCHER_APC}
-- * @{AI.AI_Cargo_Dispatcher_Helicopter#AI_DISPATCHER_HELICOPTER}
-- * @{AI.AI_Cargo_Dispatcher_Airplane#AI_DISPATCHER_AIRPLANE}
--
-- Depending on which cargo dispatcher is used (ground or airbore), similar considerations like in the self propelled case are necessary. Howver, note that
-- the dispatchers as of yet cannot use user defined off road paths for example since they are classes of their own and use a different routing logic.
--
-- ===
--
-- # Creating a Warehouse
--
-- A MOOSE warehouse must be represented in game by a physical *static* object. For example, the mission editor already has warehouse as static object available.
-- This would be a good first choice but any static object will do.
--
-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Static.png)
--
-- The positioning of the warehouse static object is very important for a couple of reasons. Firstly, a warehouse needs a good infrastructure so that spawned assets
-- have a proper road connection or can reach the associated airbase easily.
--
-- ## Constructor and Start
--
-- Once the static warehouse object is placed in the mission editor it can be used as a MOOSE warehouse by the @{#WAREHOUSE.New}(*warehousestatic*, *alias*) constructor,
-- like for example:
--
--     warehouseBatumi=WAREHOUSE:New(STATIC:FindByName("Warehouse Batumi"), "My optional Warehouse Alias")
--     warehouseBatumi:Start()
--
-- The first parameter *warehousestatic* is the static MOOSE object. By default, the name of the warehouse will be the same as the name given to the static object.
-- The second parameter *alias* is optional and can be used to choose a more convenient name if desired. This will be the name the warehouse calls itself when reporting messages.
--
-- Note that a warehouse also needs to be started in order to be in service. This is done with the @{#WAREHOUSE.Start}() or @{#WAREHOUSE.__Start}(*delay*) functions.
-- The warehouse is now fully operational and requests are being processed.
--
-- # Adding Assets
--
-- Assets can be added to the warehouse stock by using the @{#WAREHOUSE.AddAsset}(*group*, *ngroups*, *forceattribute*, *forcecargobay*, *forceweight*, *loadradius*, *skill*, *liveries*, *assignment*) function.
-- The parameter *group* has to be a MOOSE @{Wrapper.Group#GROUP}. This is also the only mandatory parameters. All other parameters are optional and can be used for fine tuning if
-- nessary. The parameter *ngroups* specifies how many clones of this group are added to the stock.
--
--     infrantry=GROUP:FindByName("Some Infantry Group")
--     warehouseBatumi:AddAsset(infantry, 5)
--
-- This will add five infantry groups to the warehouse stock. Note that the group should normally be a late activated template group,
-- which was defined in the mission editor. But you can also add other groups which are already spawned and present in the mission.
--
-- Also note that the coalition of the template group (red, blue or neutral) does not matter. The coalition of the assets is determined by the coalition of the warehouse owner.
-- In other words, it is no problem to add red groups to blue warehouses and vice versa. The assets will automatically have the coalition of the warehouse.
--
-- You can add assets with a delay by using the @{#WAREHOUSE.__AddAsset}(*delay*, *group*, *ngroups*, *forceattribute*, *forcecargobay*, *forceweight*, *loadradius*,  *skill*, *liveries*, *assignment*),
-- where *delay* is the delay in seconds before the asset is added.
--
-- In game, the warehouse will get a mark which is regularly updated and showing the currently available assets in stock.
--
-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Stock-Marker.png)
--
-- ## Optional Parameters for Fine Tuning
--
-- By default, the generalized attribute of the asset is determined automatically from the DCS descriptor attributes. However, this might not always result in the desired outcome.
-- Therefore, it is possible, to force a generalized attribute for the asset with the third optional parameter *forceattribute*, which is of type @{#WAREHOUSE.Attribute}.
--
-- ### Setting the Generalized Attibute
-- For example, a UH-1H Huey has in DCS the attibute of an attack helicopter. But of course, it can also transport cargo. If you want to use it for transportation, you can specify this
-- manually when the asset is added
--
--     warehouseBatumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO)
--
-- This becomes important when assets are requested from other warehouses as described below. In this case, the five Hueys are now marked as transport helicopters and
-- not attack helicopters. This is also particularly useful when adding assets to a warehouse with the intention of using them to transport other units that are part of 
-- a subsequent request (see below). Setting the attribute will help to ensure that warehouse module can find the correct unit when attempting to service a request in its
-- queue. For example, if we want to add an Amphibious Landing Ship, even though most are indeed armed, it's recommended to do the following:
-- 
--     warehouseBatumi:AddAsset("Landing Ship", 1, WAREHOUSE.Attribute.NAVAL_UNARMEDSHIP)
--
-- Then when adding the request, you can simply specify WAREHOUSE.TransportType.SHIP (which corresponds to NAVAL_UNARMEDSHIP) as the TransportType.
--
-- ### Setting the Cargo Bay Weight Limit
-- You can ajust the cargo bay weight limit, in case it is not calculated correctly automatically. For example, the cargo bay of a C-17A is much smaller in DCS than that of a C-130, which is
-- unrealistic. This can be corrected by the *forcecargobay* parmeter which is here set to 77,000 kg
--
--     warehouseBatumi:AddAsset("C-17A", nil, nil, 77000)
--
-- The size of the cargo bay is only important when the group is used as transport carrier for other assets.
--
-- ### Setting the Weight
-- If an asset shall be transported by a carrier it important to note that - as in real life - a carrier can only carry cargo up to a certain weight. The weight of the
-- units is automatically determined from the DCS descriptor table.
-- However, in the current DCS version (2.5.3) a mortar unit has a weight of 5 tons. This confuses the transporter logic, because it appears to be too have for, e.g. all APCs.
--
-- As a workaround, you can manually adjust the weight by the optional *forceweight* parameter:
--
--     warehouseBatumi:AddAsset("Mortar Alpha", nil, nil, nil, 210)
--
--  In this case we set it to 210 kg. Note, the weight value set is meant for *each* unit in the group. Therefore, a group consisting of three mortars will have a total weight
--  of 630 kg. This is important as groups cannot be split between carrier units when transporting, i.e. the total weight of the whole group must be smaller than the
--  cargo bay of the transport carrier.
--
-- ### Setting the Load Radius
-- Boading and loading of cargo into a carrier is modeled in a realistic fashion in the AI\_CARGO\DISPATCHER classes, which are used inernally by the WAREHOUSE class.
-- Meaning that troops (cargo) will board, i.e. run or drive to the carrier, and only once they are in close proximity to the transporter they will be loaded (disappear).
--
-- Unfortunately, there are some situations where problems can occur. For example, in DCS tanks have the strong tentendcy not to drive around obstacles but rather to roll over them.
-- I have seen cases where an aircraft of the same coalition as the tank was in its way and the tank drove right through the plane waiting on a parking spot and destroying it.
--
-- As a workaround it is possible to set a larger load radius so that the cargo units are despawned further away from the carrier via the optional **loadradius** parameter:
--
--     warehouseBatumi:AddAsset("Leopard 2", nil, nil, nil, nil, 250)
--
-- Adding the asset like this will cause the units to be loaded into the carrier already at a distance of 250 meters.
--
-- ### Setting the AI Skill
--
-- By default, the asset has the skill of its template group. The optional parameter *skill* allows to set a different skill when the asset is added. See the
-- [hoggit page](https://wiki.hoggitworld.com/view/DCS_enum_AI) possible values of this enumerator.
-- For example you can use
--
--     warehouseBatumi:AddAsset("Leopard 2", nil, nil, nil, nil, nil, AI.Skill.EXCELLENT)
--
-- do set the skill of the asset to excellent.
--
-- ### Setting Liveries
--
-- By default ,the asset uses the livery of its template group. The optional parameter *liveries* allows to define one or multiple liveries.
-- If multiple liveries are given in form of a table of livery names, each asset gets a random one.
--
-- For example
--
--     warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, "China UN")
--
-- would spawn the asset with a Chinese UN livery.
--
-- Or
--
--     warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, {"China UN", "German"})
--
-- would spawn the asset with either a Chinese UN or German livery. Mind the curly brackets **{}** when you want to specify multiple liveries.
--
-- Four each unit type, the livery names can be found in the DCS root folder under Bazar\Liveries. You have to use the name of the livery subdirectory. The names of the liveries
-- as displayed in the mission editor might be different and won't work in general.
--
-- ### Setting an Assignment
--
-- Assets can be added with a specific assignment given as a text, e.g.
--
--     warehouseBatumi:AddAsset("Mi-8", nil, nil, nil, nil, nil, nil, nil, "Go to Warehouse Kobuleti")
--
-- This is helpful to establish supply chains once an asset has arrived at its (first) destination and is meant to be forwarded to another warehouse.
--
-- ## Retrieving the Asset
--
-- Once a an asset is added to a warehouse, the @{#WAREHOUSE.NewAsset} event is triggered. You can hook into this event with the @{#WAREHOUSE.OnAfterNewAsset}(*asset*, *assignment*) function.
--
-- The first parameter *asset* is a table of type @{#WAREHOUSE.Assetitem} and contains a lot of information about the asset. The seconed parameter *assignment* is optional and is the specific
-- assignment the asset got when it was added.
--
-- Note that the assignment is can also be the assignment that was specified when adding a request (see next section). Once an asset that was requested from another warehouse and an assignment
-- was specified in the @{#WAREHOUSE.AddRequest} function, the assignment can be checked when the asset has arrived and is added to the receiving warehouse.
--
-- ===
--
-- # Requesting Assets
--
-- Assets of the warehouse can be requested by other MOOSE warehouses. A request will first be scrutinized to check if can be fulfilled at all. If the request is valid, it is
-- put into the warehouse queue and processed as soon as possible.
--
-- Requested assets spawn in various "Rule of Engagement Rules" (ROE) and Alerts modes. If your assets will cross into dangerous areas, be sure to change these states. You can do this in @{#WAREHOUSE:OnAfterAssetSpawned}(*From, *Event, *To, *group, *asset, *request)) function.
--
-- Initial Spawn states is as follows:
--    GROUND: ROE, "Return Fire" Alarm, "Green"
--    AIR:  ROE, "Return Fire" Reaction to Threat, "Passive Defense"
--    NAVAL ROE, "Return Fire" Alarm,"N/A"
--
-- A request can be added by the @{#WAREHOUSE.AddRequest}(*warehouse*, *AssetDescriptor*, *AssetDescriptorValue*, *nAsset*, *TransportType*, *nTransport*, *Prio*, *Assignment*) function.
-- The parameters are
--
-- * *warehouse*: The requesting MOOSE @{#WAREHOUSE}. Assets will be delivered there.
-- * *AssetDescriptor*: The descriptor to describe the asset "type". See the @{#WAREHOUSE.Descriptor} enumerator. For example, assets requested by their generalized attibute.
-- * *AssetDescriptorValue*: The value of the asset descriptor.
-- * *nAsset*: (Optional) Number of asset group requested. Default is one group.
-- * *TransportType*: (Optional) The transport method used to deliver the assets to the requestor. Default is that assets go to the requesting warehouse on their own.
-- * *nTransport*: (Optional) Number of asset groups used to transport the cargo assets from A to B. Default is one group.
-- * *Prio*: (Optional) A number between 1 (high) and 100 (low) describing the priority of the request. Request with high priority are processed first. Default is 50, i.e. medium priority.
-- * *Assignment*: (Optional) A free to choose string describing the assignment. For self requests, this can be used to assign the spawned groups to specific tasks.
--
-- ## Requesting by Generalized Attribute
--
-- Generalized attributes are similar to [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). However, they are a bit more general and
-- an asset can only have one generalized attribute by which it is characterized.
--
-- For example:
--
--     warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.APC, 2)
--
-- Here, warehouse Kobuleti requests 5 infantry groups from warehouse Batumi. These "cargo" assets should be transported from Batumi to Kobuleti by 2 APCS.
-- Note that the warehouse at Batumi needs to have at least five infantry groups and two APC groups in their stock if the request can be processed.
-- If either to few infantry or APC groups are available when the request is made, the request is held in the warehouse queue until enough cargo and
-- transport assets are available.
--
-- Also note that the above request is for five infantry groups. So any group in stock that has the generalized attribute "GROUND_INFANTRY" can be selected for the request.
--
-- ### Generalized Attributes
--
-- Currently implemented are:
--
-- * @{#WAREHOUSE.Attribute.AIR_TRANSPORTPLANE} Airplane with transport capability. This can be used to transport other assets.
-- * @{#WAREHOUSE.Attribute.AIR_AWACS} Airborne Early Warning and Control System.
-- * @{#WAREHOUSE.Attribute.AIR_FIGHTER} Fighter, interceptor, ... airplane.
-- * @{#WAREHOUSE.Attribute.AIR_BOMBER} Aircraft which can be used for strategic bombing.
-- * @{#WAREHOUSE.Attribute.AIR_TANKER} Airplane which can refuel other aircraft.
-- * @{#WAREHOUSE.Attribute.AIR_TRANSPORTHELO} Helicopter with transport capability. This can be used to transport other assets.
-- * @{#WAREHOUSE.Attribute.AIR_ATTACKHELO} Attack helicopter.
-- * @{#WAREHOUSE.Attribute.AIR_UAV} Unpiloted Aerial Vehicle, e.g. drones.
-- * @{#WAREHOUSE.Attribute.AIR_OTHER} Any airborne unit that does not fall into any other airborne category.
-- * @{#WAREHOUSE.Attribute.GROUND_APC} Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets.
-- * @{#WAREHOUSE.Attribute.GROUND_TRUCK} Unarmed ground vehicles, which has the DCS "Truck" attribute.
-- * @{#WAREHOUSE.Attribute.GROUND_INFANTRY} Ground infantry assets.
-- * @{#WAREHOUSE.Attribute.GROUND_IFV} Ground infantry fighting vehicle.
-- * @{#WAREHOUSE.Attribute.GROUND_ARTILLERY} Artillery assets.
-- * @{#WAREHOUSE.Attribute.GROUND_TANK} Tanks (modern or old).
-- * @{#WAREHOUSE.Attribute.GROUND_TRAIN} Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently.
-- * @{#WAREHOUSE.Attribute.GROUND_EWR} Early Warning Radar.
-- * @{#WAREHOUSE.Attribute.GROUND_AAA} Anti-Aircraft Artillery.
-- * @{#WAREHOUSE.Attribute.GROUND_SAM} Surface-to-Air Missile system or components.
-- * @{#WAREHOUSE.Attribute.GROUND_OTHER} Any ground unit that does not fall into any other ground category.
-- * @{#WAREHOUSE.Attribute.NAVAL_AIRCRAFTCARRIER} Aircraft carrier.
-- * @{#WAREHOUSE.Attribute.NAVAL_WARSHIP} War ship, i.e. cruisers, destroyers, firgates and corvettes.
-- * @{#WAREHOUSE.Attribute.NAVAL_ARMEDSHIP} Any armed ship that is not an aircraft carrier, a cruiser, destroyer, firgatte or corvette.
-- * @{#WAREHOUSE.Attribute.NAVAL_UNARMEDSHIP} Any unarmed naval vessel.
-- * @{#WAREHOUSE.Attribute.NAVAL_OTHER} Any naval unit that does not fall into any other naval category.
-- * @{#WAREHOUSE.Attribute.OTHER_UNKNOWN} Anything that does not fall into any other category.
--
-- ## Requesting a Specific Unit Type
--
-- A more specific request could look like:
--
--     warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.UNITTYPE, "A-10C", 2)
--
-- Here, Kobuleti requests a specific unit type, in particular two groups of A-10Cs. Note that the spelling is important as it must exacly be the same as
-- what one get's when using the DCS unit type.
--
-- ## Requesting a Specific Group
--
-- An even more specific request would be:
--
--     warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Group Name as in ME", 3)
--
-- In this case three groups named "Group Name as in ME" are requested. This explicitly request the groups named like that in the Mission Editor.
--
-- ## Requesting a General Category
--
-- On the other hand, very general and unspecifc requests can be made by the categroy descriptor. The descriptor value parameter can be any [group category](https://wiki.hoggitworld.com/view/DCS_Class_Group), i.e.
--
-- * Group.Category.AIRPLANE for fixed wing aircraft,
-- * Group.Category.HELICOPTER for helicopters,
-- * Group.Category.GROUND for all ground troops,
-- * Group.Category.SHIP for naval assets,
-- * Group.Category.TRAIN for trains (not implemented and not working in DCS yet).
--
-- For example,
--
--     warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, 10)
--
-- means that Kubuleti requests 10 ground groups and does not care which ones. This could be a mix of infantry, APCs, trucks etc.
--
-- **Note** that these general requests should be made with *great care* due to the fact, that depending on what a warehouse has in stock a lot of different unit types can be spawned.
--
-- ## Requesting Relative Quantities
--
-- In addition to requesting absolute numbers of assets it is possible to request relative amounts of assets currently in stock. To this end the @{#WAREHOUSE.Quantity} enumerator
-- was introduced:
--
-- * @{#WAREHOUSE.Quantity.ALL}
-- * @{#WAREHOUSE.Quantity.HALF}
-- * @{#WAREHOUSE.Quantity.QUARTER}
-- * @{#WAREHOUSE.Quantity.THIRD}
-- * @{#WAREHOUSE.Quantity.THREEQUARTERS}
--
-- For example,
--
--     warehouseBatumi:AddRequest(warehouseKobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.HELICOPTER, WAREHOUSE.Quantity.HALF)
--
-- means that Kobuleti warehouse requests half of all available helicopters which Batumi warehouse currently has in stock.
--
-- # Employing Assets - The Self Request
--
-- Transferring assets from one warehouse to another is important but of course once the the assets are at the "right" place it is equally important that they
-- can be employed for specific tasks and assignments.
--
-- Assets in the warehouses stock can be used for user defined tasks quite easily. They can be spawned into the game by a "***self request***", i.e. the warehouse
-- requests the assets from itself:
--
--     warehouseBatumi:AddRequest(warehouseBatumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5)
--
-- Note that the *sending* and *requesting* warehouses are *identical* in this case.
--
-- This would simply spawn five infantry groups in the spawn zone of the Batumi warehouse if/when they are available.
--
-- ## Accessing the Assets
--
-- If a warehouse requests assets from itself, it triggers the event **SelfReqeuest**. The mission designer can capture this event with the associated
-- @{#WAREHOUSE.OnAfterSelfRequest}(*From*, *Event*, *To*, *groupset*, *request*) function.
--
--     --- OnAfterSelfRequest user function. Access groups spawned from the warehouse for further tasking.
--     -- @param #WAREHOUSE self
--     -- @param #string From From state.
--     -- @param #string Event Event.
--     -- @param #string To To state.
--     -- @param Core.Set#SET_GROUP groupset The set of cargo groups that was delivered to the warehouse itself.
--     -- @param #WAREHOUSE.Pendingitem request Pending self request.
--     function WAREHOUSE:OnAfterSelfRequest(From, Event, To, groupset, request)
--       local groupset=groupset --Core.Set#SET_GROUP
--       local request=request   --Functional.Warehouse#WAREHOUSE.Pendingitem
--
--       for _,group in pairs(groupset:GetSetObjects()) do
--         local group=group --Wrapper.Group#GROUP
--         group:SmokeGreen()
--       end
--
--     end
--
-- The variable *groupset* is a @{Core.Set#SET_GROUP} object and holds all asset groups from the request. The code above shows, how the mission designer can access the groups
-- for further tasking. Here, the groups are only smoked but, of course, you can use them for whatever assignment you fancy.
--
-- Note that airborne groups are spawned in **uncontrolled state** and need to be activated first before they can begin with their assigned tasks and missions.
-- This can be done with the @{Wrapper.Controllable#CONTROLLABLE.StartUncontrolled} function as demonstrated in the example section below.
--
-- ===
--
-- # Infrastructure
--
-- A good infrastructure is important for a warehouse to be efficient. Therefore, the location of a warehouse should be chosen with care.
-- This can also help to avoid many DCS related issues such as units getting stuck in buildings, blocking taxi ways etc.
--
-- ## Spawn Zone
--
-- By default, the zone were ground assets are spawned is a circular zone around the physical location of the warehouse with a radius of 200 meters. However, the location of the
-- spawn zone can be set by the @{#WAREHOUSE.SetSpawnZone}(*zone*) functions. It is advisable to choose a zone which is clear of obstacles.
--
-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Batumi.png)
--
-- The parameter *zone* is a MOOSE @{Core.Zone#ZONE} object. So one can, e.g., use trigger zones defined in the mission editor. If a cicular zone is not desired, one
-- can use a polygon zone (see @{Core.Zone#ZONE_POLYGON}).
--
-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_SpawnPolygon.png)
--
-- ## Road Connections
--
-- Ground assets will use a road connection to travel from one warehouse to another. Therefore, a proper road connection is necessary.
--
-- By default, the closest point on road to the center of the spawn zone is chosen as road connection automatically. But only, if distance between the spawn zone
-- and the road connection is less than 3 km.
--
-- The user can set the road connection manually with the @{#WAREHOUSE.SetRoadConnection} function. This is only functional for self propelled assets at the moment
-- and not if using the AI dispatcher classes since these have a different logic to find the route.
--
-- ## Off Road Connections
--
-- For ground troops it is also possible to define off road paths between warehouses if no proper road connection is available or should not be used.
--
-- An off road path can be defined via the @{#WAREHOUSE.AddOffRoadPath}(*remotewarehouse*, *group*, *oneway*) function, where
-- *remotewarehouse* is the warehouse to which the path leads.
-- The parameter *group* is a *late activated* template group. The waypoints of this group are used to define the path between the two warehouses.
-- By default, the reverse paths is automatically added to get *from* the remote warehouse *to* this warehouse unless the parameter *oneway* is set to *true*.
--
-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Off-Road_Paths.png)
--
-- **Note** that if an off road connection is defined between two warehouses this becomes the default path, i.e. even if there is a path *on road* possible
-- this will not be used.
--
-- Also note that you can define multiple off road connections between two warehouses. If there are multiple paths defined, the connection is chosen randomly.
-- It is also possible to add the same path multiple times. By this you can influence the probability of the chosen path. For example Path1(A->B) has been
-- added two times while Path2(A->B) was added only once. Hence, the group will choose Path1 with a probability of 66.6 % while Path2 is only chosen with
-- a probability of 33.3 %.
--
-- ## Rail Connections
--
-- A rail connection is automatically defined as the closest point on a railway measured from the center of the spawn zone. But only, if the distance is less than 3 km.
--
-- The mission designer can manually specify a rail connection with the @{#WAREHOUSE.SetRailConnection} function.
--
-- **NOTE** however, that trains in DCS are currently not implemented in a way so that they can be used.
--
-- ## Air Connections
--
-- In order to use airborne assets, a warehouse needs to have an associated airbase. This can be an airdrome, a FARP/HELOPAD or a ship.
--
-- If there is an airbase within 3 km range of the warehouse it is automatically set as the associated airbase. A user can set an airbase manually
-- with the @{#WAREHOUSE.SetAirbase} function. Keep in mind that sometimes ground units need to walk/drive from the spawn zone to the airport
-- to get to their transport carriers.
--
-- ## Naval Connections
--
-- Natively, DCS does not have the concept of a port/habour or shipping lanes. So in order to have a meaningful transfer of naval units between warehouses, these have to be
-- defined by the mission designer.
--
-- ### Defining a Port
--
-- A port in this context is the zone where all naval assets are spawned. This zone can be defined with the function @{#WAREHOUSE.SetPortZone}(*zone*), where the parameter
-- *zone* is a MOOSE zone. So again, this can be create from a trigger zone defined in the mission editor or if a general shape is desired by a @{Core.Zone#ZONE_POLYGON}.
--
-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_PortZone.png)
--
-- ### Defining Shipping Lanes
--
-- A shipping lane between to warehouses can be defined by the @{#WAREHOUSE.AddShippingLane}(*remotewarehouse*, *group*, *oneway*) function. The first parameter *remotewarehouse*
-- is the warehouse which should be connected to the present warehouse.
--
-- The parameter *group* should be a late activated group defined in the mission editor. The waypoints of this group are used as waypoints of the shipping lane.
--
-- By default, the reverse lane is automatically added to the remote warehouse. This can be disabled by setting the *oneway* parameter to *true*.
--
-- Similar to off road connections, you can also define multiple shipping lanes between two warehouse ports. If there are multiple lanes defined, one is chosen randomly.
-- It is possible to add the same lane multiple times. By this you can influence the probability of the chosen lane. For example Lane_1(A->B) has been
-- added two times while Lane_2(A->B) was added only once. Therefore, the ships will choose Lane_1 with a probability of 66.6 % while Path_2 is only chosen with
-- a probability of 33.3 %.
--
-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_ShippingLane.png)
--
-- ===
--
-- # Why is my request not processed?
--
-- For each request, the warehouse class logic does a lot of consistency and validation checks under the hood.
-- This helps to circumvent a lot of DCS issues and shortcomings. For example, it is checked that enough free
-- parking spots at an airport are available *before* the assets are spawned.
-- However, this also means that sometimes a request is deemed to be *invalid* in which case they are deleted
-- from the queue or considered to be valid but cannot be executed at this very moment.
--
-- ## Invalid Requests
--
-- Invalid request are requests which can **never** be processes because there is some logical or physical argument against it.
-- (Or simply because that feature was not implemented (yet).)
--
-- * All airborne assets need an associated airbase of any kind on the sending *and* receiving warehouse.
-- * Airplanes need an airdrome at the sending and receiving warehouses.
-- * Not enough parking spots of the right terminal type at the sending warehouse. This avoids planes spawning on runways or on top of each other.
-- * No parking spots of the right terminal type at the receiving warehouse. This avoids DCS despawning planes on landing if they have no valid parking spot.
-- * Ground assets need a road connection between both warehouses or an off-road path needs to be added manually.
-- * Ground assets cannot be send directly to ships, i.e. warehouses on ships.
-- * Naval units need a user defined shipping lane between both warehouses.
-- * Warehouses need a user defined port zone to spawn naval assets.
-- * The receiving warehouse is destroyed or stopped.
-- * If transport by airplane, both warehouses must have and airdrome.
-- * If transport by APC, both warehouses must have a road connection.
-- * If transport by helicopter, the sending airbase must have an associated airbase (airdrome or FARP).
--
-- All invalid requests are cancelled and **removed** from the warehouse queue!
--
-- ## Temporarily Unprocessable Requests
--
-- Temporarily unprocessable requests are possible in principle, but cannot be processed at the given time the warehouse checks its queue.
--
-- * No enough parking spaces are available for all requested assets but the airbase has enough parking spots in total so that this request is possible once other aircraft have taken off.
-- * The requesting warehouse is not in state "Running" (could be paused, not yet started or under attack).
-- * Not enough cargo assets available at this moment.
-- * Not enough free parking spots for all cargo or transport airborne assets at the moment.
-- * Not enough transport assets to carry all cargo assets.
--
-- Temporarily unprocessable requests are held in the queue. If at some point in time, the situation changes so that these requests can be processed, they are executed.
--
-- ## Cargo Bay and Weight Limitations
--
-- The transportation of cargo is handled by the AI\_Dispatcher classes. These take the cargo bay of a carrier and the weight of
-- the cargo into account so that a carrier can only load a realistic amount of cargo.
--
-- However, if troops are supposed to be transported between warehouses, there is one important limitations one has to keep in mind.
-- This is that **cargo asset groups cannot be split** and divided into separate carrier units!
--
-- For example, a TPz Fuchs has a cargo bay large enough to carry up to 10 soldiers at once, which is a realistic number.
-- If a group consisting of more than ten soldiers needs to be transported, it cannot be loaded into the APC.
-- Even if two APCs are available, which could in principle carry up to 20 soldiers, a group of, let's say 12 soldiers will not
-- be split into a group of ten soldiers using the first APC and a group two soldiers using the second APC.
--
-- In other words, **there must be at least one carrier unit available that has a cargo bay large enough to load the heaviest cargo group!**
-- The warehouse logic will automatically search all available transport assets for a large enough carrier.
-- But if none is available, the request will be queued until a suitable carrier becomes available.
--
-- The only realistic solution in this case is to either provide a transport carrier with a larger cargo bay or to reduce the number of soldiers
-- in the group.
--
-- A better way would be to have two groups of max. 10 soldiers each and one TPz Fuchs for transport. In this case, the first group is
-- loaded and transported to the receiving warehouse. Once this is done, the carrier will drive back and pick up the remaining
-- group.
--
-- As an artificial workaround one can manually set the cargo bay size to a larger value or alternatively reduce the weight of the cargo
-- when adding the assets via the @{#WAREHOUSE.AddAsset} function. This might even be unavoidable if, for example, a SAM group
-- should be transported since SAM sites only work when all units are in the same group.
--
-- ## Processing Speed
--
-- A warehouse has a limited speed to process requests. Each time the status of the warehouse is updated only one requests is processed.
-- The time interval between status updates is 30 seconds by default and can be adjusted via the @{#WAREHOUSE.SetStatusUpdate}(*interval*) function.
-- However, the status is also updated on other occasions, e.g. when a new request was added.
--
-- ===
--
-- # Strategic Considerations
--
-- Due to the fact that a warehouse holds (or can hold) a lot of valuable assets, it makes a (potentially) juicy target for enemy attacks.
-- There are several interesting situations, which can occur.
--
-- ## Capturing a Warehouses Airbase
--
-- If a warehouse has an associated airbase, it can be captured by the enemy. In this case, the warehouse looses its ability so employ all airborne assets and is also cut-off
-- from supply by airplanes. Supply of ground troops via helicopters is still possible, because they deliver the troops into the spawn zone.
--
-- Technically, the capturing of the airbase is triggered by the DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) event.
-- So the capturing takes place when only enemy ground units are in the airbase zone whilst no ground units of the present airbase owner are in that zone.
--
-- The warehouse will also create an event **AirbaseCaptured**, which can be captured by the @{#WAREHOUSE.OnAfterAirbaseCaptured} function. So the warehouse chief can react on
-- this attack and for example deploy ground groups to re-capture its airbase.
--
-- When an airbase is re-captured the event **AirbaseRecaptured** is triggered and can be captured by the @{#WAREHOUSE.OnAfterAirbaseRecaptured} function.
-- This can be used to put the defending assets back into the warehouse stock.
--
-- ## Capturing the Warehouse
--
-- A warehouse can be captured by the enemy coalition. If enemy ground troops enter the warehouse zone the event **Attacked** is triggered which can be captured by the
-- @{#WAREHOUSE.OnAfterAttacked} event. By default the warehouse zone circular zone with a radius of 500 meters located at the center of the physical warehouse.
-- The warehouse zone can be set via the @{#WAREHOUSE.SetWarehouseZone}(*zone*) function. The parameter *zone* must also be a circular zone.
--
-- The @{#WAREHOUSE.OnAfterAttacked} function can be used by the mission designer to react to the enemy attack. For example by deploying some or all ground troops
-- currently in stock to defend the warehouse. Note that the warehouse also has a self defence option which can be enabled by the @{#WAREHOUSE.SetAutoDefenceOn}()
-- function. In this case, the warehouse will automatically spawn all ground troops. If the spawn zone is further away from the warehouse zone, all mobile troops
-- are routed to the warehouse zone. The self request which is triggered on an automatic defence has the assignment "AutoDefence". So you can use this to
-- give orders to the groups that were spawned using the @{#WAREHOUSE.OnAfterSelfRequest} function.
--
-- If only ground troops of the enemy coalition are present in the warehouse zone, the warehouse and all its assets falls into the hands of the enemy.
-- In this case the event **Captured** is triggered which can be captured by the @{#WAREHOUSE.OnAfterCaptured} function.
--
-- The warehouse turns to the capturing coalition, i.e. its physical representation, and all assets as well. In particular, all requests to the warehouse will
-- spawn assets belonging to the new owner.
--
-- If the enemy troops could be defeated, i.e. no more troops of the opposite coalition are in the warehouse zone, the event **Defeated** is triggered and
-- the @{#WAREHOUSE.OnAfterDefeated} function can be used to adapt to the new situation. For example putting back all spawned defender troops back into
-- the warehouse stock. Note that if the automatic defence is enabled, all defenders are automatically put back into the warehouse on the **Defeated** event.
--
-- ## Destroying a Warehouse
--
-- If an enemy destroy the physical warehouse structure, the warehouse will of course stop all its services. In principle, all assets contained in the warehouse are
-- gone as well. So a warehouse should be properly defended.
--
-- Upon destruction of the warehouse, the event **Destroyed** is triggered, which can be captured by the @{#WAREHOUSE.OnAfterDestroyed} function.
-- So the mission designer can intervene at this point and for example choose to spawn all or particular types of assets before the warehouse is gone for good.
--
-- ===
--
-- # Hook in and Take Control
--
-- The Finite State Machine implementation allows mission designers to hook into important events and add their own code.
-- Most of these events have already been mentioned but here is the list at a glance:
--
-- * "NotReadyYet" --> "Start" --> "Running" (Starting the warehouse)
-- * "*" --> "Status" --> "*" (status updated in regular intervals)
-- * "*" --> "AddAsset" --> "*" (adding a new asset to the warehouse stock)
-- * "*" --> "NewAsset" --> "*" (a new asset has been added to the warehouse stock)
-- * "*" --> "AddRequest" --> "*" (adding a request for the warehouse assets)
-- * "Running" --> "Request" --> "*" (a request is processed when the warehouse is running)
-- * "Attacked" --> "Request" --> "*" (a request is processed when the warehouse is attacked)
-- * "*" --> "Arrived" --> "*" (asset group has arrived at its destination)
-- * "*" --> "Delivered" --> "*" (all assets of a request have been delivered)
-- * "Running" --> "SelfRequest" --> "*" (warehouse is requesting asset from itself when running)
-- * "Attacked" --> "SelfRequest" --> "*" (warehouse is requesting asset from itself while under attack)
-- * "*" --> "Attacked" --> "Attacked" (warehouse is being attacked)
-- * "Attacked" --> "Defeated" --> "Running" (an attack was defeated)
-- * "Attacked" --> "Captured" --> "Running" (warehouse was captured by the enemy)
-- * "*" --> "AirbaseCaptured" --> "*" (airbase belonging to the warehouse was captured by the enemy)
-- * "*" --> "AirbaseRecaptured" --> "*" (airbase was re-captured)
-- * "*" --> "AssetSpawned" --> "*" (an asset has been spawned into the world)
-- * "*" --> "AssetLowFuel" --> "*" (an asset is running low on fuel)
-- * "*" --> "AssetDead" --> "*" (a whole asset, i.e. all its units/groups, is dead)
-- * "*" --> "Destroyed" --> "Destroyed" (warehouse was destroyed)
-- * "Running" --> "Pause" --> "Paused" (warehouse is paused)
-- * "Paused" --> "Unpause" --> "Running" (warehouse is unpaused)
-- * "*" --> "Stop" --> "Stopped" (warehouse is stopped)
--
-- The transitions are of the general form "From State" --> "Event" --> "To State". The "*" star denotes that the transition is possible from *any* state.
-- Some transitions, however, are only allowed from certain "From States". For example, no requests can be processed if the warehouse is in "Paused" or "Destroyed" or "Stopped" state.
--
-- Mission designers can capture the events with OnAfterEvent functions, e.g. @{#WAREHOUSE.OnAfterDelivered} or @{#WAREHOUSE.OnAfterAirbaseCaptured}.
--
-- ===
--
-- # Persistence of Assets
--
-- Assets in stock of a warehouse can be saved to a file on your hard drive and then loaded from that file at a later point. This enables to restart the mission
-- and restore the warehouse stock.
--
-- ## Prerequisites
--
-- **Important** By default, DCS does not allow for writing data to files. Therefore, one first has to comment out the line "sanitizeModule('io')", i.e.
--
--     do
--       sanitizeModule('os')
--       --sanitizeModule('io')
--       sanitizeModule('lfs')
--       require = nil
--       loadlib = nil
--     end
--
-- in the file "MissionScripting.lua", which is located in the subdirectory "Scripts" of your DCS installation root directory.
--
-- ### Don't!
--
-- Do not use **semi-colons** or **equal signs** in the group names of your assets as these are used as separators in the saved and loaded files texts.
-- If you do, it will cause problems and give you a headache!
--
-- ## Save Assets
--
-- Saving asset data to file is achieved by the @{#WAREHOUSE.Save}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the
-- warehouse data is saved. If you do not specify a path, the file is saved your the DCS installation root directory.
-- The parameter *filename* is optional and defines the name of the saved file. By default this is automatically created from the warehouse id and name, for example
-- "Warehouse-1234_Batumi.txt".
--
--     warehouseBatumi:Save("D:\\My Warehouse Data\\")
--
-- This will save all asset data to in "D:\\My Warehouse Data\\Warehouse-1234_Batumi.txt".
--
-- ### Automatic Save at Mission End
--
-- The assets can be saved automatically when the mission is ended via the @{#WAREHOUSE.SetSaveOnMissionEnd}(*path*, *filename*) function, i.e.
--
--     warehouseBatumi:SetSaveOnMissionEnd("D:\\My Warehouse Data\\")
--
-- ## Load Assets
--
-- Loading assets data from file is achieved by the @{#WAREHOUSE.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the
-- warehouse data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory.
-- The parameter *filename* is optional and defines the name of the file to load. By default this is automatically generated from the warehouse id and name, for example
-- "Warehouse-1234_Batumi.txt".
--
-- Note that the warehouse **must not be started** and in the *Running* state in order to load the assets. In other words, loading should happen after the
-- @{#WAREHOUSE.New} command is specified in the code but before the @{#WAREHOUSE.Start} command is given.
--
-- Loading the assets is done by
--
--     warehouseBatumi:New(STATIC:FindByName("Warehouse Batumi"))
--     warehouseBatumi:Load("D:\\My Warehouse Data\\")
--     warehouseBatumi:Start()
--
-- This sequence loads all assets from file. If a warehouse was captured in the last mission, it also respawns the static warehouse structure with the right coalition.
-- However, it due to DCS limitations it is not possible to set the airbase coalition. This has to be done manually in the mission editor. Or alternatively, one could
-- spawn some ground units via a self request and let them capture the airbase.
--
-- ===
--
-- # Examples
--
-- This section shows some examples how the WAREHOUSE class is used in practice. This is one of the best ways to explain things, in my opinion.
--
-- But first, let me introduce a convenient way to define several warehouses in a table. This is absolutely *not necessary* but quite handy if you have
-- multiple WAREHOUSE objects in your mission.
--
-- ## Example 0: Setting up a Warehouse Array
--
-- If you have multiple warehouses, you can put them in a table. This makes it easier to access them or to loop over them.
--
--     -- Define Warehouses.
--     local warehouse={}
--     -- Blue warehouses
--     warehouse.Senaki   = WAREHOUSE:New(STATIC:FindByName("Warehouse Senaki"),   "Senaki")   --Functional.Warehouse#WAREHOUSE
--     warehouse.Batumi   = WAREHOUSE:New(STATIC:FindByName("Warehouse Batumi"),   "Batumi")   --Functional.Warehouse#WAREHOUSE
--     warehouse.Kobuleti = WAREHOUSE:New(STATIC:FindByName("Warehouse Kobuleti"), "Kobuleti") --Functional.Warehouse#WAREHOUSE
--     warehouse.Kutaisi  = WAREHOUSE:New(STATIC:FindByName("Warehouse Kutaisi"),  "Kutaisi")  --Functional.Warehouse#WAREHOUSE
--     warehouse.Berlin   = WAREHOUSE:New(STATIC:FindByName("Warehouse Berlin"),   "Berlin")   --Functional.Warehouse#WAREHOUSE
--     warehouse.London   = WAREHOUSE:New(STATIC:FindByName("Warehouse London"),   "London")   --Functional.Warehouse#WAREHOUSE
--     warehouse.Stennis  = WAREHOUSE:New(STATIC:FindByName("Warehouse Stennis"),  "Stennis")  --Functional.Warehouse#WAREHOUSE
--     warehouse.Pampa    = WAREHOUSE:New(STATIC:FindByName("Warehouse Pampa"),    "Pampa")    --Functional.Warehouse#WAREHOUSE
--     -- Red warehouses
--     warehouse.Sukhumi  = WAREHOUSE:New(STATIC:FindByName("Warehouse Sukhumi"),  "Sukhumi")  --Functional.Warehouse#WAREHOUSE
--     warehouse.Gudauta  = WAREHOUSE:New(STATIC:FindByName("Warehouse Gudauta"),  "Gudauta")  --Functional.Warehouse#WAREHOUSE
--     warehouse.Sochi    = WAREHOUSE:New(STATIC:FindByName("Warehouse Sochi"),    "Sochi")    --Functional.Warehouse#WAREHOUSE
--
-- Remarks:
--
-- * I defined the array as local, i.e. local warehouse={}. This is personal preference and sometimes causes trouble with the lua garbage collection. You can also define it as a global array/table!
-- * The "--Functional.Warehouse#WAREHOUSE" at the end is only to have the LDT intellisense working correctly. If you don't use LDT (which you should!), it can be omitted.
--
-- **NOTE** that all examples below need this bit or code at the beginning - or at least the warehouses which are used.
--
-- The example mission is based on the same template mission, which has defined a lot of airborne, ground and naval assets as templates. Only few of those are used here.
--
-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Assets.png)
--
-- ## Example 1: Self Request
--
-- Ground troops are taken from the Batumi warehouse stock and spawned in its spawn zone. After a short delay, they are added back to the warehouse stock.
-- Also a new request is made. Hence, the groups will be spawned, added back to the warehouse, spawned again and so on and so forth...
--
--     -- Start warehouse Batumi.
--     warehouse.Batumi:Start()
--
--     -- Add five groups of infantry as assets.
--     warehouse.Batumi:AddAsset(GROUP:FindByName("Infantry Platoon Alpha"), 5)
--
--     -- Add self request for three infantry at Batumi.
--     warehouse.Batumi:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 3)
--
--
--     --- Self request event. Triggered once the assets are spawned in the spawn zone or at the airbase.
--     function warehouse.Batumi:OnAfterSelfRequest(From, Event, To, groupset, request)
--       local mygroupset=groupset --Core.Set#SET_GROUP
--
--       -- Loop over all groups spawned from that request.
--       for _,group in pairs(mygroupset:GetSetObjects()) do
--         local group=group --Wrapper.Group#GROUP
--
--         -- Gree smoke on spawned group.
--         group:SmokeGreen()
--
--         -- Put asset back to stock after 10 seconds.
--         warehouse.Batumi:__AddAsset(10, group)
--       end
--
--       -- Add new self request after 20 seconds.
--       warehouse.Batumi:__AddRequest(20, warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 3)
--
--     end
--
-- ## Example 2: Self propelled Ground Troops
--
-- Warehouse Berlin, which is a FARP near Batumi, requests infantry and troop transports from the warehouse at Batumi.
-- The groups are spawned at Batumi and move by themselves from Batumi to Berlin using the roads.
-- Once the troops have arrived at Berlin, the troops are automatically added to the warehouse stock of Berlin.
-- While on the road, Batumi has requested back two APCs from Berlin. Since Berlin does not have the assets in stock,
-- the request is queued. After the troops have arrived, Berlin is sending back the APCs to Batumi.
--
--     -- Start Warehouse at Batumi.
--     warehouse.Batumi:Start()
--
--     -- Add 20 infantry groups and ten APCs as assets at Batumi.
--     warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20)
--     warehouse.Batumi:AddAsset("TPz Fuchs", 10)
--
--     -- Start Warehouse Berlin.
--     warehouse.Berlin:Start()
--
--     -- Warehouse Berlin requests 10 infantry groups and 5 APCs from warehouse Batumi.
--     warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 10)
--     warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 5)
--
--     -- Request from Batumi for 2 APCs. Initially these are not in stock. When they become available, the request is executed.
--     warehouse.Berlin:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, 2)
--
-- ## Example 3: Self Propelled Airborne Assets
--
-- Warehouse Senaki receives a high priority request from Kutaisi for one Yak-52s. At the same time, Kobuleti requests half of
-- all available Yak-52s. Request from Kutaisi is first executed and then Kobuleti gets half of the remaining assets.
-- Additionally, London requests one third of all available UH-1H Hueys from Senaki.
-- Once the units have arrived they are added to the stock of the receiving warehouses and can be used for further assignments.
--
--     -- Start warehouses
--     warehouse.Senaki:Start()
--     warehouse.Kutaisi:Start()
--     warehouse.Kobuleti:Start()
--     warehouse.London:Start()
--
--     -- Add assets to Senaki warehouse.
--     warehouse.Senaki:AddAsset("Yak-52", 10)
--     warehouse.Senaki:AddAsset("Huey", 6)
--
--     -- Kusaisi requests 3 Yak-52 form Senaki while Kobuleti wants all the rest.
--     warehouse.Senaki:AddRequest(warehouse.Kutaisi,  WAREHOUSE.Descriptor.GROUPNAME, "Yak-52", 1, nil, nil, 10)
--     warehouse.Senaki:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Yak-52", WAREHOUSE.Quantity.HALF,  nil, nil, 70)
--
--     -- FARP London wants 1/3 of the six available Hueys.
--     warehouse.Senaki:AddRequest(warehouse.London,  WAREHOUSE.Descriptor.GROUPNAME, "Huey", WAREHOUSE.Quantity.THIRD)
--
-- ## Example 4: Transport of Assets by APCs
--
-- Warehouse at FARP Berlin requests five infantry groups from Batumi. These assets shall be transported using two APC groups.
-- Infantry and APC are spawned in the spawn zone at Batumi. The APCs have a cargo bay large enough to pick up four of the
-- five infantry groups in the first run and will bring them to Berlin. There, they unboard and walk to the warehouse where they will be added to the stock.
-- Meanwhile the APCs go back to Batumi and one will pick up the last remaining soldiers.
-- Once the APCs have completed their mission, they return to Batumi and are added back to stock.
--
--     -- Start Warehouse at Batumi.
--     warehouse.Batumi:Start()
--
--     -- Start Warehouse Berlin.
--     warehouse.Berlin:Start()
--
--     -- Add 20 infantry groups and five APCs as assets at Batumi.
--     warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20)
--     warehouse.Batumi:AddAsset("TPz Fuchs", 5)
--
--     -- Warehouse Berlin requests 5 infantry groups from warehouse Batumi using 2 APCs for transport.
--     warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.APC, 2)
--
--## Example 5: Transport of Assets by Helicopters
--
--  Warehouse at FARP Berlin requests five infantry groups from Batumi. They shall be transported by all available transport helicopters.
--  Note that the UH-1H Huey in DCS is an attack and not a transport helo. So the warehouse logic would be default also
--  register it as an @{#WAREHOUSE.Attribute.AIR_ATTACKHELICOPTER}. In order to use it as a transport we need to force
--  it to be added as transport helo.
--  Also note that even though all (here five) helos are requested, only two of them are employed because this number is sufficient to
--  transport all requested assets in one go.
--
--     -- Start Warehouses.
--     warehouse.Batumi:Start()
--     warehouse.Berlin:Start()
--
--     -- Add 20 infantry groups as assets at Batumi.
--     warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20)
--
--     -- Add five Hueys for transport. Note that a Huey in DCS is an attack and not a transport helo. So we force this attribute!
--     warehouse.Batumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO)
--
--     -- Warehouse Berlin requests 5 infantry groups from warehouse Batumi using all available helos for transport.
--     warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 5, WAREHOUSE.TransportType.HELICOPTER, WAREHOUSE.Quantity.ALL)
--
--## Example 6: Transport of Assets by Airplanes
--
-- Warehoues Kobuleti requests all (three) APCs from Batumi using one airplane for transport.
-- The available C-130 is able to carry one APC at a time. So it has to commute three times between Batumi and Kobuleti to deliver all requested cargo assets.
-- Once the cargo is delivered, the C-130 transport returns to Batumi and is added back to stock.
--
--     -- Start warehouses.
--     warehouse.Batumi:Start()
--     warehouse.Kobuleti:Start()
--
--     -- Add assets to Batumi warehouse.
--     warehouse.Batumi:AddAsset("C-130", 1)
--     warehouse.Batumi:AddAsset("TPz Fuchs", 3)
--
--     warehouse.Batumi:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_APC, WAREHOUSE.Quantity.ALL, WAREHOUSE.TransportType.AIRPLANE)
--
-- ## Example 7: Capturing Airbase and Warehouse
--
-- A red BMP has made it through our defence lines and drives towards our unprotected airbase at Senaki.
-- Once the BMP captures the airbase (DCS [S\_EVENT\_BASE\_CAPTURED](https://wiki.hoggitworld.com/view/DCS_event_base_captured) is evaluated)
-- the warehouse at Senaki lost its air infrastructure and it is not possible any more to spawn airborne units. All requests for airborne units are rejected and cancelled in this case.
--
-- The red BMP then drives further to the warehouse. Once it enters the warehouse zone (500 m radius around the warehouse building), the warehouse is
-- considered to be under attack. This triggers the event **Attacked**. The @{#WAREHOUSE.OnAfterAttacked} function can be used to react to this situation.
-- Here, we only broadcast a distress call and launch a flare. However, it would also be reasonable to spawn all or selected ground troops in order to defend
-- the warehouse. Note, that the warehouse has a self defence option which can be activated via the @{#WAREHOUSE.SetAutoDefenceOn}() function. If activated,
-- *all* ground assets are automatically spawned and assigned to defend the warehouse. Once/if the attack is defeated, these assets go automatically back
-- into the warehouse stock.
--
-- If the red coalition manages to capture our warehouse, all assets go into their possession. Now red tries to steal three F/A-18 flights and send them to
-- Sukhumi. These aircraft will be spawned and begin to taxi. However, ...
--
-- A blue Bradley is in the area and will attempt to recapture the warehouse. It might also catch the red F/A-18s before they take off.
--
--     -- Start warehouses.
--     warehouse.Senaki:Start()
--     warehouse.Sukhumi:Start()
--
--     -- Add some assets.
--     warehouse.Senaki:AddAsset("TPz Fuchs", 5)
--     warehouse.Senaki:AddAsset("Infantry Platoon Alpha", 10)
--     warehouse.Senaki:AddAsset("F/A-18C 2ship", 10)
--
--     -- Enable auto defence, i.e. spawn all group troups into the spawn zone.
--     --warehouse.Senaki:SetAutoDefenceOn()
--
--     -- Activate Red BMP trying to capture the airfield and the warehouse.
--     local red1=GROUP:FindByName("Red BMP-80 Senaki"):Activate()
--
--     -- The red BMP first drives to the airbase which gets captured and changes from blue to red.
--     -- This triggers the "AirbaseCaptured" event where you can hook in and do things.
--     function warehouse.Senaki:OnAfterAirbaseCaptured(From, Event, To, Coalition)
--       -- This request cannot be processed since the warehouse has lost its airbase. In fact it is deleted from the queue.
--       warehouse.Senaki:AddRequest(warehouse.Senaki,WAREHOUSE.Descriptor.CATEGORY, Group.Category.AIRPLANE, 1)
--     end
--
--     -- Now the red BMP also captures the warehouse. This triggers the "Captured" event where you can hook in.
--     -- So now the warehouse and the airbase are both red and aircraft can be spawned again.
--     function warehouse.Senaki:OnAfterCaptured(From, Event, To, Coalition, Country)
--       -- These units will be spawned as red units because the warehouse has just been captured.
--       if Coalition==coalition.side.RED then
--         -- Sukhumi tries to "steals" three F/A-18 from Senaki and brings them to Sukhumi.
--         -- Well, actually the aircraft wont make it because blue1 will kill it on the taxi way leaving a blood bath. But that's life!
--         warehouse.Senaki:AddRequest(warehouse.Sukhumi, WAREHOUSE.Descriptor.CATEGORY, Group.Category.AIRPLANE, 3)
--         warehouse.Senaki.warehouse:SmokeRed()
--       elseif Coalition==coalition.side.BLUE then
--         warehouse.Senaki.warehouse:SmokeBlue()
--       end
--
--       -- Activate a blue vehicle to re-capture the warehouse. It will drive to the warehouse zone and kill the red intruder.
--       local blue1=GROUP:FindByName("blue1"):Activate()
--     end
--
-- ## Example 8: Destroying a Warehouse
--
-- FARP Berlin requests a Huey from Batumi warehouse. This helo is deployed and will be delivered.
-- After 30 seconds into the mission we create and (artificial) big explosion - or a terrorist attack if you like - which completely destroys the
-- the warehouse at Batumi. All assets are gone and requests cannot be processed anymore.
--
--     -- Start Batumi and Berlin warehouses.
--     warehouse.Batumi:Start()
--     warehouse.Berlin:Start()
--
--     -- Add some assets.
--     warehouse.Batumi:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO)
--     warehouse.Berlin:AddAsset("Huey", 5, WAREHOUSE.Attribute.AIR_TRANSPORTHELO)
--
--     -- Big explosion at the warehose. It has a very nice damage model by the way :)
--     local function DestroyWarehouse()
--       warehouse.Batumi:GetCoordinate():Explosion(999)
--     end
--     SCHEDULER:New(nil, DestroyWarehouse, {}, 30)
--
--     -- First request is okay since warehouse is still alive.
--     warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1)
--
--     -- These requests should both not be processed any more since the warehouse at Batumi is destroyed.
--     warehouse.Batumi:__AddRequest(35, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1)
--     warehouse.Berlin:__AddRequest(40, warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, 1)
--
-- ## Example 9: Self Propelled Naval Assets
--
-- Kobuleti requests all naval assets from Batumi.
-- However, before naval assets can be exchanged, both warehouses need a port and at least one shipping lane defined by the user.
-- See the @{#WAREHOUSE.SetPortZone}() and @{#WAREHOUSE.AddShippingLane}() functions.
-- We do not want to spawn them all at once, because this will probably be a disaster
-- in the port zone. Therefore, each ship is spawned with a delay of five minutes.
--
-- Batumi has quite a selection of different ships (for testing).
--
-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Naval_Assets.png)
--
--     -- Start warehouses.
--     warehouse.Batumi:Start()
--     warehouse.Kobuleti:Start()
--
--     -- Define ports. These are polygon zones created by the waypoints of late activated units.
--     warehouse.Batumi:SetPortZone(ZONE_POLYGON:NewFromGroupName("Warehouse Batumi Port Zone", "Warehouse Batumi Port Zone"))
--     warehouse.Kobuleti:SetPortZone(ZONE_POLYGON:NewFromGroupName("Warehouse Kobuleti Port Zone", "Warehouse Kobuleti Port Zone"))
--
--     -- Shipping lane. Again, the waypoints of late activated units are taken as points defining the shipping lane.
--     -- Some units will take lane 1 while others will take lane two. But both lead from Batumi to Kobuleti port.
--     warehouse.Batumi:AddShippingLane(warehouse.Kobuleti, GROUP:FindByName("Warehouse Batumi-Kobuleti Shipping Lane 1"))
--     warehouse.Batumi:AddShippingLane(warehouse.Kobuleti, GROUP:FindByName("Warehouse Batumi-Kobuleti Shipping Lane 2"))
--
--     -- Large selection of available naval units in DCS.
--     warehouse.Batumi:AddAsset("Speedboat")
--     warehouse.Batumi:AddAsset("Perry")
--     warehouse.Batumi:AddAsset("Normandy")
--     warehouse.Batumi:AddAsset("Stennis")
--     warehouse.Batumi:AddAsset("Carl Vinson")
--     warehouse.Batumi:AddAsset("Tarawa")
--     warehouse.Batumi:AddAsset("SSK 877")
--     warehouse.Batumi:AddAsset("SSK 641B")
--     warehouse.Batumi:AddAsset("Grisha")
--     warehouse.Batumi:AddAsset("Molniya")
--     warehouse.Batumi:AddAsset("Neustrashimy")
--     warehouse.Batumi:AddAsset("Rezky")
--     warehouse.Batumi:AddAsset("Moskva")
--     warehouse.Batumi:AddAsset("Pyotr Velikiy")
--     warehouse.Batumi:AddAsset("Kuznetsov")
--     warehouse.Batumi:AddAsset("Zvezdny")
--     warehouse.Batumi:AddAsset("Yakushev")
--     warehouse.Batumi:AddAsset("Elnya")
--     warehouse.Batumi:AddAsset("Ivanov")
--     warehouse.Batumi:AddAsset("Yantai")
--     warehouse.Batumi:AddAsset("Type 052C")
--     warehouse.Batumi:AddAsset("Guangzhou")
--
--     -- Get Number of ships at Batumi.
--     local nships=warehouse.Batumi:GetNumberOfAssets(WAREHOUSE.Descriptor.CATEGORY, Group.Category.SHIP)
--
--     -- Send one ship every 3 minutes (ships do not evade each other well, so we need a bit space between them).
--     for i=1, nships do
--       warehouse.Batumi:__AddRequest(180*(i-1)+10, warehouse.Kobuleti, WAREHOUSE.Descriptor.CATEGORY, Group.Category.SHIP, 1)
--     end
--
-- ## Example 10: Warehouse on Aircraft Carrier
--
-- This example shows how to spawn assets from a warehouse located on an aircraft carrier. The warehouse must still be represented by a
-- physical static object. However, on a carrier space is limit so we take a smaller static. In priciple one could also take something
-- like a windsock.
--
-- ![Banner Image](..\Presentations\WAREHOUSE\Warehouse_Carrier.png)
--
-- USS Stennis requests F/A-18s from Batumi. At the same time Kobuleti requests F/A-18s from the Stennis which currently does not have any.
-- So first, Batumi delivers the fighters to the Stennis. After they arrived they are deployed again and send to Kobuleti.
--
--     -- Start warehouses.
--     warehouse.Batumi:Start()
--     warehouse.Stennis:Start()
--     warehouse.Kobuleti:Start()
--
--     -- Add F/A-18 2-ship flight to Batmi.
--     warehouse.Batumi:AddAsset("F/A-18C 2ship", 1)
--
--     -- USS Stennis requests F/A-18 from Batumi.
--     warehouse.Batumi:AddRequest(warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "F/A-18C 2ship")
--
--     -- Kobuleti requests F/A-18 from USS Stennis.
--     warehouse.Stennis:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "F/A-18C 2ship")
--
-- ## Example 11: Aircraft Carrier - Rescue Helo and Escort
--
-- After 10 seconds we make a self request for a rescue helicopter. Note, that the @{#WAREHOUSE.AddRequest} function has a parameter which lets you
-- specify an "Assignment". This can be later used to identify the request and take the right actions.
--
-- Once the request is processed, the @{#WAREHOUSE.OnAfterSelfRequest} function is called. This is where we hook in and postprocess the spawned assets.
-- In particular, we use the @{AI.AI_Formation#AI_FORMATION} class to make some nice escorts for our carrier.
--
-- When the resue helo is spawned, we can check that this is the correct asset and make the helo go into formation with the carrier.
-- Once the helo runs out of fuel, it will automatically return to the ship and land. For the warehouse, this means that the "cargo", i.e. the helicopter
-- has been delivered - assets can be delivered to other warehouses and to the same warehouse - hence a *self* request.
-- When that happens, the **Delivered** event is triggered and the @{#WAREHOUSE.OnAfterDelivered} function called. This can now be used to spawn
-- a fresh helo. Effectively, there we created an infinite, never ending loop. So a rescue helo will be up at all times.
--
-- After 30 and 45 seconds requests for five groups of armed speedboats are made. These will be spawned in the port zone right behind the carrier.
-- The first five groups will go port of the carrier an form a left wing formation. The seconds groups will to the analogue on the starboard side.
-- **Note** that in order to spawn naval assets a warehouse needs a port (zone). Since the carrier and hence the warehouse is mobile, we define a moving
-- zone as @{Core.Zone#ZONE_UNIT} with the carrier as reference unit. The "port" of the Stennis at its stern so all naval assets are spawned behind the carrier.
--
--     -- Start warehouse on USS Stennis.
--     warehouse.Stennis:Start()
--
--     -- Aircraft carrier gets a moving zone right behind it as port.
--     warehouse.Stennis:SetPortZone(ZONE_UNIT:New("Warehouse Stennis Port Zone", UNIT:FindByName("USS Stennis"), 100, {rho=250, theta=180, relative_to_unit=true}))
--
--     -- Add speedboat assets.
--     warehouse.Stennis:AddAsset("Speedboat", 10)
--     warehouse.Stennis:AddAsset("CH-53E", 1)
--
--     -- Self request of speed boats.
--     warehouse.Stennis:__AddRequest(10, warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "CH-53E", 1, nil, nil, nil, "Rescue Helo")
--     warehouse.Stennis:__AddRequest(30, warehouse.Stennis, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.NAVAL_ARMEDSHIP, 5, nil, nil, nil, "Speedboats Left")
--     warehouse.Stennis:__AddRequest(45, warehouse.Stennis, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.NAVAL_ARMEDSHIP, 5, nil, nil, nil, "Speedboats Right")
--
--     --- Function called after self request
--     function warehouse.Stennis:OnAfterSelfRequest(From, Event, To,_groupset, request)
--       local groupset=_groupset --Core.Set#SET_GROUP
--       local request=request   --Functional.Warehouse#WAREHOUSE.Pendingitem
--
--       -- USS Stennis is the mother ship.
--       local Mother=UNIT:FindByName("USS Stennis")
--
--       -- Get assignment of the request.
--       local assignment=warehouse.Stennis:GetAssignment(request)
--
--       if assignment=="Speedboats Left" then
--
--         -- Define AI Formation object.
--         -- Note that this has to be a global variable or the garbage collector will remove it for some reason!
--         CarrierFormationLeft = AI_FORMATION:New(Mother, groupset, "Left Formation with Carrier", "Escort Carrier.")
--
--         -- Formation parameters.
--         CarrierFormationLeft:FormationLeftWing(200 ,50, 0, 0, 500, 50)
--         CarrierFormationLeft:__Start(2)
--
--         for _,group in pairs(groupset:GetSetObjects()) do
--           local group=group --Wrapper.Group#GROUP
--           group:FlareRed()
--         end
--
--       elseif assignment=="Speedboats Right" then
--
--         -- Define AI Formation object.
--         -- Note that this has to be a global variable or the garbage collector will remove it for some reason!
--         CarrierFormationRight = AI_FORMATION:New(Mother, groupset, "Right Formation with Carrier", "Escort Carrier.")
--
--         -- Formation parameters.
--         CarrierFormationRight:FormationRightWing(200 ,50, 0, 0, 500, 50)
--         CarrierFormationRight:__Start(2)
--
--         for _,group in pairs(groupset:GetSetObjects()) do
--           local group=group --Wrapper.Group#GROUP
--           group:FlareGreen()
--         end
--
--       elseif assignment=="Rescue Helo" then
--
--         -- Start uncontrolled helo.
--         local group=groupset:GetFirst() --Wrapper.Group#GROUP
--         group:StartUncontrolled()
--
--         -- Define AI Formation object.
--         CarrierFormationHelo = AI_FORMATION:New(Mother, groupset, "Helo Formation with Carrier", "Fly Formation.")
--
--         -- Formation parameters.
--         CarrierFormationHelo:FormationCenterWing(-150, 50, 20, 50, 100, 50)
--         CarrierFormationHelo:__Start(2)
--
--       end
--
--       --- When the helo is out of fuel, it will return to the carrier and should be delivered.
--       function warehouse.Stennis:OnAfterDelivered(From,Event,To,request)
--         local request=request   --Functional.Warehouse#WAREHOUSE.Pendingitem
--
--         -- So we start another request.
--         if request.assignment=="Rescue Helo" then
--           warehouse.Stennis:__AddRequest(10, warehouse.Stennis, WAREHOUSE.Descriptor.GROUPNAME, "CH-53E", 1, nil, nil, nil, "Rescue Helo")
--         end
--       end
--
--     end
--
-- ## Example 12: Pause a Warehouse
--
-- This example shows how to pause and unpause a warehouse. In paused state, requests will not be processed but assets can be added and requests be added.
--
--    * Warehouse Batumi is paused after 10 seconds.
--    * Request from Berlin after 15 which will not be processed.
--    * New tank assets for Batumi after 20 seconds. This is possible also in paused state.
--    * Batumi unpaused after 30 seconds. Queued request from Berlin can be processed.
--    * Berlin is paused after 60 seconds.
--    * Berlin requests tanks from Batumi after 90 seconds. Request is not processed because Berlin is paused and not running.
--    * Berlin is unpaused after 120 seconds. Queued request for tanks from Batumi can not be processed.
--
-- Here is the code:
--
--     -- Start Warehouse at Batumi.
--     warehouse.Batumi:Start()
--
--     -- Start Warehouse Berlin.
--     warehouse.Berlin:Start()
--
--     -- Add 20 infantry groups and 5 tank platoons as assets at Batumi.
--     warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 20)
--
--     -- Pause the warehouse after 10 seconds
--     warehouse.Batumi:__Pause(10)
--
--     -- Add a request from Berlin after 15 seconds. A request can be added but not be processed while warehouse is paused.
--     warehouse.Batumi:__AddRequest(15, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 1)
--
--     -- New asset added after 20 seconds. This is possible even if the warehouse is paused.
--     warehouse.Batumi:__AddAsset(20, "Abrams", 5)
--
--     -- Unpause warehouse after 30 seconds. Now the request from Berlin can be processed.
--     warehouse.Batumi:__Unpause(30)
--
--     -- Pause warehouse Berlin
--     warehouse.Berlin:__Pause(60)
--
--     -- After 90 seconds request from Berlin for tanks.
--     warehouse.Batumi:__AddRequest(90, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TANK, 1)
--
--     -- After 120 seconds unpause Berlin.
--     warehouse.Berlin:__Unpause(120)
--
-- ## Example 13: Battlefield Air Interdiction
--
-- This example show how to couple the WAREHOUSE class with the @{AI.AI_BAI} class.
-- Four enemy targets have been located at the famous Kobuleti X. All three available Viggen 2-ship flights are assigned to kill at least one of the BMPs to complete their mission.
--
--     -- Start Warehouse at Kobuleti.
--     warehouse.Kobuleti:Start()
--
--     -- Add three 2-ship groups of Viggens.
--     warehouse.Kobuleti:AddAsset("Viggen 2ship", 3)
--
--     -- Self request for all Viggen assets.
--     warehouse.Kobuleti:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.GROUPNAME, "Viggen 2ship", WAREHOUSE.Quantity.ALL, nil, nil, nil, "BAI")
--
--     -- Red targets at Kobuleti X (late activated).
--     local RedTargets=GROUP:FindByName("Red IVF Alpha")
--
--     -- Activate the targets.
--     RedTargets:Activate()
--
--     -- Do something with the spawned aircraft.
--     function warehouse.Kobuleti:OnAfterSelfRequest(From,Event,To,groupset,request)
--       local groupset=groupset --Core.Set#SET_GROUP
--       local request=request   --Functional.Warehouse#WAREHOUSE.Pendingitem
--
--       if request.assignment=="BAI" then
--
--         for _,group in pairs(groupset:GetSetObjects()) do
--           local group=group --Wrapper.Group#GROUP
--
--           -- Start uncontrolled aircraft.
--           group:StartUncontrolled()
--
--           local BAI=AI_BAI_ZONE:New(ZONE:New("Patrol Zone Kobuleti"), 500, 1000, 500, 600, ZONE:New("Patrol Zone Kobuleti"))
--
--           -- Tell the program to use the object (in this case called BAIPlane) as the group to use in the BAI function
--           BAI:SetControllable(group)
--
--           -- Function checking if targets are still alive
--           local function CheckTargets()
--             local nTargets=RedTargets:GetSize()
--             local nInitial=RedTargets:GetInitialSize()
--             local nDead=nInitial-nTargets
--             local nRequired=1  -- Let's make this easy.
--             if RedTargets:IsAlive() and nDead < nRequired then
--               MESSAGE:New(string.format("BAI Mission: %d of %d red targets still alive. At least %d targets need to be eliminated.", nTargets, nInitial, nRequired), 5):ToAll()
--             else
--               MESSAGE:New("BAI Mission: The required red targets are destroyed.", 30):ToAll()
--               BAI:__Accomplish(1) -- Now they should fly back to the patrolzone and patrol.
--             end
--           end
--
--           -- Start scheduler to monitor number of targets.
--           local Check, CheckScheduleID = SCHEDULER:New(nil, CheckTargets, {}, 60, 60)
--
--           -- When the targets in the zone are destroyed, (see scheduled function), the planes will return home ...
--           function BAI:OnAfterAccomplish( Controllable, From, Event, To )
--             MESSAGE:New( "BAI Mission: Sending the Viggens back to base.", 30):ToAll()
--             Check:Stop(CheckScheduleID)
--             BAI:__RTB(1)
--           end
--
--           -- Start BAI
--           BAI:Start()
--
--           -- Engage after 5 minutes.
--           BAI:__Engage(300)
--
--           -- RTB after 30 min max.
--           BAI:__RTB(-30*60)
--
--         end
--       end
--
--     end
--
-- ## Example 14: Strategic Bombing
--
-- This example shows how to employ strategic bombers in a mission. Three B-52s are launched at Kobuleti with the assignment to wipe out the enemy warehouse at Sukhumi.
-- The bombers will get a flight path and make their approach from the South at an altitude of 5000 m ASL. After their bombing run, they will return to Kobuleti and
-- added back to stock.
--
--     -- Start warehouses
--     warehouse.Kobuleti:Start()
--     warehouse.Sukhumi:Start()
--
--     -- Add a strategic bomber assets
--     warehouse.Kobuleti:AddAsset("B-52H", 3)
--
--     -- Request bombers for specific task of bombing Sukhumi warehouse.
--     warehouse.Kobuleti:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.AIR_BOMBER, WAREHOUSE.Quantity.ALL, nil, nil, nil, "Bomb Sukhumi")
--
--     -- Specify assignment after bombers have been spawned.
--     function warehouse.Kobuleti:OnAfterSelfRequest(From, Event, To, groupset, request)
--       local groupset=groupset --Core.Set#SET_GROUP
--
--       -- Get assignment of this request.
--       local assignment=warehouse.Kobuleti:GetAssignment(request)
--
--       if assignment=="Bomb Sukhumi" then
--
--         for _,_group in pairs(groupset:GetSet()) do
--           local group=_group --Wrapper.Group#GROUP
--
--           -- Start uncontrolled aircraft.
--           group:StartUncontrolled()
--
--           -- Target coordinate!
--           local ToCoord=warehouse.Sukhumi:GetCoordinate():SetAltitude(5000)
--
--           -- Home coordinate.
--           local HomeCoord=warehouse.Kobuleti:GetCoordinate():SetAltitude(3000)
--
--           -- Task bomb Sukhumi warehouse using all bombs (2032) from direction 180 at altitude 5000 m.
--           local task=group:TaskBombing(warehouse.Sukhumi:GetCoordinate():GetVec2(), false, "All", nil , 180, 5000, 2032)
--
--           -- Define waypoints.
--           local WayPoints={}
--
--           -- Take off position.
--           WayPoints[1]=warehouse.Kobuleti:GetCoordinate():WaypointAirTakeOffParking()
--           -- Begin bombing run 20 km south of target.
--           WayPoints[2]=ToCoord:Translate(20*1000, 180):WaypointAirTurningPoint(nil, 600, {task}, "Bombing Run")
--           -- Return to base.
--           WayPoints[3]=HomeCoord:WaypointAirTurningPoint()
--           -- Land at homebase. Bombers are added back to stock and can be employed in later assignments.
--           WayPoints[4]=warehouse.Kobuleti:GetCoordinate():WaypointAirLanding()
--
--           -- Route bombers.
--           group:Route(WayPoints)
--         end
--
--       end
--     end
--
-- ## Example 15: Defining Off-Road Paths
--
-- For self propelled assets it is possible to define custom off-road paths from one warehouse to another via the @{#WAREHOUSE.AddOffRoadPath} function.
-- The waypoints of a path are taken from late activated units. In this example, two paths have been defined between the warehouses Kobuleti and FARP London.
-- Trucks are spawned at each warehouse and are guided along the paths to the other warehouse.
-- Note that if more than one path was defined, each asset group will randomly select its route.
--
--     -- Start warehouses
--     warehouse.Kobuleti:Start()
--     warehouse.London:Start()
--
--     -- Define a polygon zone as spawn zone at Kobuleti.
--     warehouse.Kobuleti:SetSpawnZone(ZONE_POLYGON:New("Warehouse Kobuleti Spawn Zone", GROUP:FindByName("Warehouse Kobuleti Spawn Zone")))
--
--     -- Add assets.
--     warehouse.Kobuleti:AddAsset("M978", 20)
--     warehouse.London:AddAsset("M818", 20)
--
--     -- Off two road paths from Kobuleti to London. The reverse path from London to Kobuleti is added automatically.
--     warehouse.Kobuleti:AddOffRoadPath(warehouse.London, GROUP:FindByName("Warehouse Kobuleti-London OffRoad Path 1"))
--     warehouse.Kobuleti:AddOffRoadPath(warehouse.London, GROUP:FindByName("Warehouse Kobuleti-London OffRoad Path 2"))
--
--     -- London requests all available trucks from Kobuleti.
--     warehouse.Kobuleti:AddRequest(warehouse.London, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TRUCK, WAREHOUSE.Quantity.ALL)
--
--     -- Kobuleti requests all available trucks from London.
--     warehouse.London:AddRequest(warehouse.Kobuleti, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TRUCK, WAREHOUSE.Quantity.HALF)
--
-- ## Example 16: Resupply of Dead Assets
--
-- Warehouse at FARP Berlin is located at the front line and sends infantry groups to the battle zone.
-- Whenever a group dies, a new group is send from the warehouse to the battle zone.
-- Additionally, for each dead group, Berlin requests resupply from Batumi.
--
--     -- Start warehouses.
--     warehouse.Batumi:Start()
--     warehouse.Berlin:Start()
--
--     -- Front line warehouse.
--     warehouse.Berlin:AddAsset("Infantry Platoon Alpha", 6)
--
--     -- Resupply warehouse.
--     warehouse.Batumi:AddAsset("Infantry Platoon Alpha", 50)
--
--     -- Battle zone near FARP Berlin. This is where the action is!
--     local BattleZone=ZONE:New("Virtual Battle Zone")
--
--     -- Send infantry groups to the battle zone. Two groups every ~60 seconds.
--     for i=1,2 do
--       local time=(i-1)*60+10
--       warehouse.Berlin:__AddRequest(time, warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 2, nil, nil, nil, "To Battle Zone")
--     end
--
--     -- Take care of the spawned units.
--     function warehouse.Berlin:OnAfterSelfRequest(From,Event,To,groupset,request)
--       local groupset=groupset --Core.Set#SET_GROUP
--       local request=request   --Functional.Warehouse#WAREHOUSE.Pendingitem
--
--       -- Get assignment of this request.
--       local assignment=warehouse.Berlin:GetAssignment(request)
--
--       if assignment=="To Battle Zone" then
--
--         for _,group in pairs(groupset:GetSet()) do
--           local group=group --Wrapper.Group#GROUP
--
--           -- Route group to Battle zone.
--           local ToCoord=BattleZone:GetRandomCoordinate()
--           group:RouteGroundOnRoad(ToCoord, group:GetSpeedMax()*0.8)
--
--           -- After 3-5 minutes we create an explosion to destroy the group.
--           SCHEDULER:New(nil, Explosion, {group, 50}, math.random(180, 300))
--         end
--
--       end
--
--     end
--
--     -- An asset has died ==> request resupply for it.
--     function warehouse.Berlin:OnAfterAssetDead(From, Event, To, asset, request)
--       local asset=asset       --Functional.Warehouse#WAREHOUSE.Assetitem
--       local request=request   --Functional.Warehouse#WAREHOUSE.Pendingitem
--
--       -- Get assignment.
--       local assignment=warehouse.Berlin:GetAssignment(request)
--
--       -- Request resupply for dead asset from Batumi.
--       warehouse.Batumi:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, asset.attribute, nil, nil, nil, nil, "Resupply")
--
--       -- Send asset to Battle zone either now or when they arrive.
--       warehouse.Berlin:AddRequest(warehouse.Berlin, WAREHOUSE.Descriptor.ATTRIBUTE, asset.attribute, 1, nil, nil, nil, assignment)
--     end
--
-- ## Example 17: Supply Chains
--
-- Our remote warehouse "Pampa" south of Batumi needs assets but does not have any air infrastructure (FARP or airdrome).
-- Leopard 2 tanks are transported from Kobuleti to Batumi using two C-17As. From there they go be themselfs to Pampa.
-- Eight infantry groups and two mortar groups are also being transferred from Kobuleti to Batumi by helicopter.
-- The infantry has a higher priority and will be transported first using all available Mi-8 helicopters.
-- Once infantry has arrived at Batumi, it will walk by itself to warehouse Pampa.
-- The mortars can only be transported once the Mi-8 helos are available again, i.e. when the infantry has been delivered.
-- Once the mortars arrive at Batumi, they will be transported by APCs to Pampa.
--
--     -- Start warehouses.
--     warehouse.Kobuleti:Start()
--     warehouse.Batumi:Start()
--     warehouse.Pampa:Start()
--
--     -- Add assets to Kobuleti warehouse, which is our main hub.
--     warehouse.Kobuleti:AddAsset("C-130",  2)
--     warehouse.Kobuleti:AddAsset("C-17A",  2, nil, 77000)
--     warehouse.Kobuleti:AddAsset("Mi-8",  2, WAREHOUSE.Attribute.AIR_TRANSPORTHELO, nil, nil, nil, AI.Skill.EXCELLENT, {"Germany", "United Kingdom"})
--     warehouse.Kobuleti:AddAsset("Leopard 2", 10, nil, nil, 62000, 500)
--     warehouse.Kobuleti:AddAsset("Mortar Alpha", 10, nil, nil, 210)
--     warehouse.Kobuleti:AddAsset("Infantry Platoon Alpha", 20)
--
--     -- Transports at Batumi.
--     warehouse.Batumi:AddAsset("SPz Marder", 2)
--     warehouse.Batumi:AddAsset("TPz Fuchs", 2)
--
--     -- Tanks transported by plane from from Kobuleti to Batumi.
--     warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_TANK, 2, WAREHOUSE.TransportType.AIRPLANE, 2, 10, "Assets for Pampa")
--     -- Artillery transported by helicopter from Kobuleti to Batumi.
--     warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_ARTILLERY, 2, WAREHOUSE.TransportType.HELICOPTER, 2, 30, "Assets for Pampa via APC")
--     -- Infantry transported by helicopter from Kobuleti to Batumi.
--     warehouse.Kobuleti:AddRequest(warehouse.Batumi, WAREHOUSE.Descriptor.ATTRIBUTE, WAREHOUSE.Attribute.GROUND_INFANTRY, 8, WAREHOUSE.TransportType.HELICOPTER, 2, 20, "Assets for Pampa")
--
--     --- Function handling assets delivered from Kobuleti warehouse.
--     function warehouse.Kobuleti:OnAfterDelivered(From, Event, To, request)
--       local request=request --Functional.Warehouse#WAREHOUSE.Pendingitem
--
--       -- Get assignment.
--       local assignment=warehouse.Kobuleti:GetAssignment(request)
--
--       -- Check if these assets were meant for Warehouse Pampa.
--       if assignment=="Assets for Pampa via APC" then
--         -- Forward everything that arrived at Batumi to Pampa via APC.
--         warehouse.Batumi:AddRequest(warehouse.Pampa, WAREHOUSE.Descriptor.ATTRIBUTE, request.cargoattribute, request.ndelivered, WAREHOUSE.TransportType.APC, WAREHOUSE.Quantity.ALL)
--       end
--     end
--
--     -- Forward all mobile ground assets to Pampa once they arrived.
--     function warehouse.Batumi:OnAfterNewAsset(From, Event, To, asset, assignment)
--       local asset=asset --Functional.Warehouse#WAREHOUSE.Assetitem
--       if assignment=="Assets for Pampa" then
--         if asset.category==Group.Category.GROUND and asset.speedmax>0 then
--           warehouse.Batumi:AddRequest(warehouse.Pampa, WAREHOUSE.Descriptor.GROUPNAME, asset.templatename)
--         end
--       end
--     end
--
--
-- @field #WAREHOUSE
WAREHOUSE = {
  ClassName     = "WAREHOUSE",
  Debug         = false,
  verbosity     =     0,
  lid           =   nil,
  Report        =  true,
  warehouse     =   nil,
  alias         =   nil,
  zone          =   nil,
  airbase       =   nil,
  airbasename   =   nil,
  road          =   nil,
  rail          =   nil,
  spawnzone     =   nil,
  uid           =   nil,
  dTstatus      =    30,
  queueid       =     0,
  stock         =    {},
  queue         =    {},
  pending       =    {},
  transporting  =    {},
  delivered     =    {},
  defending     =    {},
  portzone      =   nil,
  harborzone    =   nil,
  shippinglanes =    {},
  offroadpaths  =    {},
  autodefence   = false,
  spawnzonemaxdist = 5000,
  autosave      = false,
  autosavepath  =   nil,
  autosavefile  =   nil,
  saveparking   = false,
  isUnit        = false,
  isShip        = false,
  lowfuelthresh =  0.15,
  respawnafterdestroyed=false,
  respawndelay  =   nil,
}

--- Item of the warehouse stock table.
-- @type WAREHOUSE.Assetitem
-- @field #number uid Unique id of the asset.
-- @field #number wid ID of the warehouse this asset belongs to.
-- @field #number rid Request ID of this asset (if any).
-- @field #string templatename Name of the template group.
-- @field #table template The spawn template of the group.
-- @field DCS#Group.Category category Category of the group.
-- @field #string unittype Type of the first unit of the group as obtained by the Object.getTypeName() DCS API function.
-- @field #number nunits Number of units in the group.
-- @field #number range Range of the unit in meters.
-- @field #number speedmax Maximum speed in km/h the group can do.
-- @field #number size Maximum size in length and with of the asset in meters.
-- @field #number weight The weight of the whole asset group in kilograms.
-- @field DCS#Object.Desc DCSdesc All DCS descriptors.
-- @field #WAREHOUSE.Attribute attribute Generalized attribute of the group.
-- @field #table cargobay Array of cargo bays of all units in an asset group.
-- @field #number cargobaytot Total weight in kg that fits in the cargo bay of all asset group units.
-- @field #number cargobaymax Largest cargo bay of all units in the group.
-- @field #number loadradius Distance when cargo is loaded into the carrier.
-- @field DCS#AI.Skill skill Skill of AI unit.
-- @field #string livery Livery of the asset.
-- @field #string assignment Assignment of the asset. This could, e.g., be used in the @{#WAREHOUSE.OnAfterNewAsset) function.
-- @field #boolean spawned If true, asset was spawned into the cruel world. If false, it is still in stock.
-- @field #string spawngroupname Name of the spawned group.
-- @field #boolean iscargo If true, asset is cargo. If false asset is transport. Nil if in stock.
-- @field #boolean arrived If true, asset arrived at its destination.
-- 
-- @field #number damage Damage of asset group in percent.
-- @field Ops.Airwing#AIRWING.Payload payload The payload of the asset.
-- @field Ops.OpsGroup#OPSGROUP flightgroup The flightgroup object.
-- @field Ops.Cohort#COHORT cohort The cohort this asset belongs to.
-- @field Ops.Legion#LEGION legion The legion this asset belonts to.
-- @field #string squadname Name of the squadron this asset belongs to.
-- @field #number Treturned Time stamp when asset returned to its legion (airwing, brigade).
-- @field #boolean requested If `true`, asset was requested and cannot be selected by another request.
-- @field #boolean isReserved If `true`, asset was reserved and cannot be selected by another request.

--- Item of the warehouse queue table.
-- @type WAREHOUSE.Queueitem
-- @field #number uid Unique id of the queue item.
-- @field #WAREHOUSE warehouse Requesting warehouse.
-- @field #WAREHOUSE.Descriptor assetdesc Descriptor of the requested asset. Enumerator of type @{#WAREHOUSE.Descriptor}.
-- @field assetdescval Value of the asset descriptor. Type depends on "assetdesc" descriptor.
-- @field #number nasset Number of asset groups requested.
-- @field #WAREHOUSE.TransportType transporttype Transport unit type.
-- @field #number ntransport Max. number of transport units requested.
-- @field #string assignment A keyword or text that later be used to identify this request and postprocess the assets.
-- @field #number prio Priority of the request. Number between 1 (high) and 100 (low).
-- @field Wrapper.Airbase#AIRBASE airbase The airbase beloning to requesting warehouse if any.
-- @field DCS#Airbase.Category category Category of the requesting airbase, i.e. airdrome, helipad/farp or ship.
-- @field #boolean toself Self request, i.e. warehouse requests assets from itself.
-- @field #table assets Table of self propelled (or cargo) and transport assets. Each element of the table is a @{#WAREHOUSE.Assetitem} and can be accessed by their asset ID.
-- @field #table cargoassets Table of cargo (or self propelled) assets. Each element of the table is a @{#WAREHOUSE.Assetitem}.
-- @field #number cargoattribute Attribute of cargo assets of type @{#WAREHOUSE.Attribute}.
-- @field #number cargocategory Category of cargo assets of type @{#WAREHOUSE.Category}.
-- @field #table transportassets Table of transport carrier assets. Each element of the table is a @{#WAREHOUSE.Assetitem}.
-- @field #number transportattribute Attribute of transport assets of type @{#WAREHOUSE.Attribute}.
-- @field #number transportcategory Category of transport assets of type @{#WAREHOUSE.Category}.
-- @field #boolean lateActivation Assets are spawned in late activated state.

--- Item of the warehouse pending queue table.
-- @type WAREHOUSE.Pendingitem
-- @field #number timestamp Absolute mission time in seconds when the request was processed.
-- @field #table assetproblem Table with assets that might have problems (damage or stuck).
-- @field Core.Set#SET_GROUP cargogroupset Set of cargo groups do be delivered.
-- @field #number ndelivered Number of groups delivered to destination.
-- @field Core.Set#SET_GROUP transportgroupset Set of cargo transport carrier groups.
-- @field Core.Set#SET_CARGO transportcargoset Set of cargo objects.
-- @field #table carriercargo Table holding the cargo groups of each carrier unit.
-- @field #number ntransporthome Number of transports back home.
-- @field #boolean lowfuel If true, at least one asset group is low on fuel.
-- @extends #WAREHOUSE.Queueitem

--- Descriptors enumerator describing the type of the asset.
-- @type WAREHOUSE.Descriptor
-- @field #string GROUPNAME Name of the asset template.
-- @field #string UNITTYPE Typename of the DCS unit, e.g. "A-10C".
-- @field #string ATTRIBUTE Generalized attribute @{#WAREHOUSE.Attribute}.
-- @field #string CATEGORY Asset category of type DCS#Group.Category, i.e. GROUND, AIRPLANE, HELICOPTER, SHIP, TRAIN.
-- @field #string ASSIGNMENT Assignment of asset when it was added.
-- @field #string ASSETLIST List of specific assets gives as a table of assets. Mind the curly brackets {}.
WAREHOUSE.Descriptor = {
  GROUPNAME="templatename",
  UNITTYPE="unittype",
  ATTRIBUTE="attribute",
  CATEGORY="category",
  ASSIGNMENT="assignment",
  ASSETLIST="assetlist,"
}

--- Generalized asset attributes. Can be used to request assets with certain general characteristics. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit.
-- @type WAREHOUSE.Attribute
-- @field #string AIR_TRANSPORTPLANE Airplane with transport capability. This can be used to transport other assets.
-- @field #string AIR_AWACS Airborne Early Warning and Control System.
-- @field #string AIR_FIGHTER Fighter, interceptor, ... airplane.
-- @field #string AIR_BOMBER Aircraft which can be used for strategic bombing.
-- @field #string AIR_TANKER Airplane which can refuel other aircraft.
-- @field #string AIR_TRANSPORTHELO Helicopter with transport capability. This can be used to transport other assets.
-- @field #string AIR_ATTACKHELO Attack helicopter.
-- @field #string AIR_UAV Unpiloted Aerial Vehicle, e.g. drones.
-- @field #string AIR_OTHER Any airborne unit that does not fall into any other airborne category.
-- @field #string GROUND_APC Infantry carriers, in particular Amoured Personell Carrier. This can be used to transport other assets.
-- @field #string GROUND_TRUCK Unarmed ground vehicles, which has the DCS "Truck" attribute.
-- @field #string GROUND_INFANTRY Ground infantry assets.
-- @field #string GROUND_IFV Ground infantry fighting vehicle.
-- @field #string GROUND_ARTILLERY Artillery assets.
-- @field #string GROUND_TANK Tanks (modern or old).
-- @field #string GROUND_TRAIN Trains. Not that trains are **not** yet properly implemented in DCS and cannot be used currently.
-- @field #string GROUND_EWR Early Warning Radar.
-- @field #string GROUND_AAA Anti-Aircraft Artillery.
-- @field #string GROUND_SAM Surface-to-Air Missile system or components.
-- @field #string GROUND_OTHER Any ground unit that does not fall into any other ground category.
-- @field #string NAVAL_AIRCRAFTCARRIER Aircraft carrier.
-- @field #string NAVAL_WARSHIP War ship, i.e. cruisers, destroyers, firgates and corvettes.
-- @field #string NAVAL_ARMEDSHIP Any armed ship that is not an aircraft carrier, a cruiser, destroyer, firgatte or corvette.
-- @field #string NAVAL_UNARMEDSHIP Any unarmed naval vessel.
-- @field #string NAVAL_OTHER Any naval unit that does not fall into any other naval category.
-- @field #string OTHER_UNKNOWN Anything that does not fall into any other category.
WAREHOUSE.Attribute = {
  AIR_TRANSPORTPLANE="Air_TransportPlane",
  AIR_AWACS="Air_AWACS",
  AIR_FIGHTER="Air_Fighter",
  AIR_BOMBER="Air_Bomber",
  AIR_TANKER="Air_Tanker",
  AIR_TRANSPORTHELO="Air_TransportHelo",
  AIR_ATTACKHELO="Air_AttackHelo",
  AIR_UAV="Air_UAV",
  AIR_OTHER="Air_OtherAir",
  GROUND_APC="Ground_APC",
  GROUND_TRUCK="Ground_Truck",
  GROUND_INFANTRY="Ground_Infantry",
  GROUND_IFV="Ground_IFV",
  GROUND_ARTILLERY="Ground_Artillery",
  GROUND_TANK="Ground_Tank",
  GROUND_TRAIN="Ground_Train",
  GROUND_EWR="Ground_EWR",
  GROUND_AAA="Ground_AAA",
  GROUND_SAM="Ground_SAM",
  GROUND_OTHER="Ground_OtherGround",
  NAVAL_AIRCRAFTCARRIER="Naval_AircraftCarrier",
  NAVAL_WARSHIP="Naval_WarShip",
  NAVAL_ARMEDSHIP="Naval_ArmedShip",
  NAVAL_UNARMEDSHIP="Naval_UnarmedShip",
  NAVAL_OTHER="Naval_OtherNaval",
  OTHER_UNKNOWN="Other_Unknown",
}

--- Cargo transport type. Defines how assets are transported to their destination.
-- @type WAREHOUSE.TransportType
-- @field #string AIRPLANE Transports are carried out by airplanes.
-- @field #string HELICOPTER Transports are carried out by helicopters.
-- @field #string APC Transports are conducted by APCs.
-- @field #string SHIP Transports are conducted by ships. Not implemented yet.
-- @field #string TRAIN Transports are conducted by trains. Not implemented yet. Also trains are buggy in DCS.
-- @field #string SELFPROPELLED Assets go to their destination by themselves. No transport carrier needed.
WAREHOUSE.TransportType = {
  AIRPLANE         = "Air_TransportPlane",
  HELICOPTER       = "Air_TransportHelo",
  APC              = "Ground_APC",
  TRAIN            = "Ground_Train",
  SHIP             = "Naval_UnarmedShip",
  AIRCRAFTCARRIER  = "Naval_AircraftCarrier",
  WARSHIP          = "Naval_WarShip",
  ARMEDSHIP        = "Naval_ArmedShip",
  SELFPROPELLED    = "Selfpropelled",
}

--- Warehouse quantity enumerator for selecting number of assets, e.g. all, half etc. of what is in stock rather than an absolute number.
-- @type WAREHOUSE.Quantity
-- @field #string ALL All "all" assets currently in stock.
-- @field #string THREEQUARTERS Three quarters "3/4" of assets in stock.
-- @field #string HALF Half "1/2" of assets in stock.
-- @field #string THIRD One third "1/3" of assets in stock.
-- @field #string QUARTER One quarter "1/4" of assets in stock.
WAREHOUSE.Quantity = {
  ALL           = "all",
  THREEQUARTERS = "3/4",
  HALF          = "1/2",
  THIRD         = "1/3",
  QUARTER       = "1/4",
}

--- Warehouse database. Note that this is a global array to have easier exchange between warehouses.
-- @type _WAREHOUSEDB
-- @field #number AssetID Unique ID of each asset. This is a running number, which is increased each time a new asset is added.
-- @field #table Assets Table holding registered assets, which are of type @{Functional.Warehouse#WAREHOUSE.Assetitem}.#
-- @field #number WarehouseID Unique ID of the warehouse. Running number.
-- @field #table Warehouses Table holding all defined @{#WAREHOUSE} objects by their unique ids.
_WAREHOUSEDB  = {
  AssetID     = 0,
  Assets      = {},
  WarehouseID = 0,
  Warehouses  = {}
}

--- Warehouse class version.
-- @field #string version
WAREHOUSE.version="1.0.2a"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: Warehouse todo list.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Add check if assets "on the move" are stationary. Can happen if ground units get stuck in buildings. If stationary auto complete transport by adding assets to request warehouse? Time?
-- TODO: Optimize findpathonroad. Do it only once (first time) and safe paths between warehouses similar to off-road paths.
-- NOGO: Spawn assets only virtually, i.e. remove requested assets from stock but do NOT spawn them ==> Interface to A2A dispatcher! Maybe do a negative sign on asset number?
-- TODO: Make more examples: ARTY, CAP, ...
-- TODO: Check also general requests like all ground. Is this a problem for self propelled if immobile units are among the assets? Check if transport.
-- TODO: Handle the case when units of a group die during the transfer.
-- DONE: Added harbours as interface for transport to/from warehouses. Simplifies process of spawning units near the ship, especially if cargo not self-propelled.
-- DONE: Test capturing a neutral warehouse.
-- DONE: Add save/load capability of warehouse <==> persistance after mission restart. Difficult in lua!
-- DONE: Get cargo bay and weight from CARGO_GROUP and GROUP. No necessary any more!
-- DONE: Add possibility to set weight and cargo bay manually in AddAsset function as optional parameters.
-- DONE: Check overlapping aircraft sometimes.
-- DONE: Case when all transports are killed and there is still cargo to be delivered. Put cargo back into warehouse. Should be done now!
-- DONE: Add transport units from dispatchers back to warehouse stock once they completed their mission.
-- DONE: Write documentation.
-- DONE: Add AAA, SAMs and UAVs to generalized attributes.
-- DONE: Add warehouse quantity enumerator.
-- DONE: Test mortars. Immobile units need a transport.
-- DONE: Set ROE for spawned groups.
-- DONE: Add offroad lanes between warehouses if road connection is not available.
-- DONE: Add possibility to add active groups. Need to create a pseudo template before destroy. <== Does not seem to be necessary any more.
-- DONE: Add a time stamp when an asset is added to the stock and for requests.
-- DONE: How to get a specific request once the cargo is delivered? Make addrequest addasset non FSM function? Callback for requests like in SPAWN?
-- DONE: Add autoselfdefence switch and user function. Default should be off.
-- DONE: Warehouse re-capturing not working?!
-- DONE: Naval assets dont go back into stock once arrived.
-- DONE: Take cargo weight into consideration, when selecting transport assets.
-- DONE: Add ports for spawning naval assets.
-- DONE: Add shipping lanes between warehouses.
-- DONE: Handle cases with immobile units <== should be handled by dispatcher classes.
-- DONE: Handle cases for aircraft carriers and other ships. Place warehouse on carrier possible? On others probably not - exclude them?
-- DONE: Add general message function for sending to coalition or debug.
-- DONE: Fine tune event handlers.
-- DONE: Improve generalized attributes.
-- DONE: If warehouse is destroyed, all asssets are gone.
-- DONE: Add event handlers.
-- DONE: Add AI_CARGO_AIRPLANE
-- DONE: Add AI_CARGO_APC
-- DONE: Add AI_CARGO_HELICOPTER
-- DONE: Switch to AI_CARGO_XXX_DISPATCHER
-- DONE: Add queue.
-- DONE: Put active groups into the warehouse, e.g. when they were transported to this warehouse.
-- NOGO: Spawn warehouse assets as uncontrolled or AI off and activate them when requested.
-- DONE: How to handle multiple units in a transport group? <== Cargo dispatchers.
-- DONE: Add phyical object.
-- DONE: If warehosue is captured, change warehouse and assets to other coalition.
-- NOGO: Use RAT for routing air units. Should be possible but might need some modifications of RAT, e.g. explit spawn place. But flight plan should be better.
-- DONE: Can I make a request with specific assets? E.g., once delivered, make a request for exactly those assests that were in the original request.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor(s)
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- The WAREHOUSE constructor. Creates a new WAREHOUSE object from a static object. Parameters like the coalition and country are taken from the static object structure.
-- @param #WAREHOUSE self
-- @param Wrapper.Static#STATIC warehouse The physical structure representing the warehouse. Can also be a @{Wrapper.Unit#UNIT}.
-- @param #string alias (Optional) Alias of the warehouse, i.e. the name it will be called when sending messages etc. Default is the name of the static/unit representing the warehouse.
-- @return #WAREHOUSE self
function WAREHOUSE:New(warehouse, alias)

  -- Inherit everthing from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #WAREHOUSE

  -- Check if just a string was given and convert to static.
  if type(warehouse)=="string" then
    local warehousename=warehouse    
    warehouse=UNIT:FindByName(warehousename)
    if warehouse==nil then
      warehouse=STATIC:FindByName(warehousename, true)
    end
  end

  -- Nil check.
  if warehouse==nil then
    env.error("ERROR: Warehouse does not exist!")
    return nil
  end
  
  -- Check if we have a STATIC or UNIT object.
  if warehouse:IsInstanceOf("STATIC") then
    self.isUnit=false
  elseif warehouse:IsInstanceOf("UNIT") then
    self.isUnit=true
    if warehouse:IsShip() then
      self.isShip=true
    end  
  else
    env.error("ERROR: Warehouse is neither STATIC nor UNIT object!")
    return nil    
  end

  -- Set alias.
  self.alias=alias or warehouse:GetName()

  -- Set some string id for output to DCS.log file.
  self.lid=string.format("WAREHOUSE %s | ", self.alias)
  
  -- Print version.
  self:I(self.lid..string.format("Adding warehouse v%s for structure %s [isUnit=%s, isShip=%s]", WAREHOUSE.version, warehouse:GetName(), tostring(self:IsUnit()), tostring(self:IsShip())))  

  -- Set some variables.
  self.warehouse=warehouse

  -- Increase global warehouse counter.
  _WAREHOUSEDB.WarehouseID=_WAREHOUSEDB.WarehouseID+1

  -- Set unique ID for this warehouse.
  self.uid=_WAREHOUSEDB.WarehouseID

  -- Coalition of the warehouse.
  self.coalition=self.warehouse:GetCoalition()

  -- Country of the warehouse.
  self.countryid=self.warehouse:GetCountry()

  -- Closest of the same coalition but within 5 km range.
  local _airbase=self:GetCoordinate():GetClosestAirbase(nil, self:GetCoalition())
  if _airbase and _airbase:GetCoordinate():Get2DDistance(self:GetCoordinate()) <= 5000 then
    self:SetAirbase(_airbase)
  end

  -- Define warehouse and default spawn zone.
  if self.isShip then
    self.zone=ZONE_AIRBASE:New(self.warehouse:GetName(), 1000)
    self.spawnzone=ZONE_AIRBASE:New(self.warehouse:GetName(), 1000)  
  else
    self.zone=ZONE_RADIUS:New(string.format("Warehouse zone %s", self.warehouse:GetName()), warehouse:GetVec2(), 500)
    self.spawnzone=ZONE_RADIUS:New(string.format("Warehouse %s spawn zone", self.warehouse:GetName()), warehouse:GetVec2(), 250)
  end
    

  -- Defaults
  self:SetMarker(true)
  self:SetReportOff()
  self:SetRunwayRepairtime()
  self.allowSpawnOnClientSpots=false

  -- Add warehouse to database.
  _WAREHOUSEDB.Warehouses[self.uid]=self

  -----------------------
  --- FSM Transitions ---
  -----------------------

  -- Start State.
  self:SetStartState("NotReadyYet")

  -- Add FSM transitions.
  --                 From State   -->   Event        -->     To State
  self:AddTransition("NotReadyYet",     "Load",              "Loaded")      -- Load the warehouse state from scatch.
  self:AddTransition("Stopped",         "Load",              "Loaded")      -- Load the warehouse state stopped state.

  self:AddTransition("NotReadyYet",     "Start",             "Running")     -- Start the warehouse from scratch.
  self:AddTransition("Loaded",          "Start",             "Running")     -- Start the warehouse when loaded from disk.

  self:AddTransition("*",               "Status",            "*")           -- Status update.

  self:AddTransition("*",               "AddAsset",          "*")           -- Add asset to warehouse stock.
  self:AddTransition("*",               "NewAsset",          "*")           -- New asset was added to warehouse stock.

  self:AddTransition("*",               "AddRequest",        "*")           -- New request from other warehouse.
  self:AddTransition("Running",         "Request",           "*")           -- Process a request. Only in running mode.
  self:AddTransition("Running",         "RequestSpawned",    "*")           -- Assets of request were spawned.
  self:AddTransition("Attacked",        "Request",           "*")           -- Process a request. Only in running mode.

  self:AddTransition("*",               "Unloaded",          "*")           -- Cargo has been unloaded from the carrier (unused ==> unnecessary?).
  self:AddTransition("*",               "AssetSpawned",      "*")           -- Asset has been spawned into the world.
  self:AddTransition("*",               "AssetLowFuel",      "*")           -- Asset is low on fuel.

  self:AddTransition("*",               "Arrived",           "*")           -- Cargo or transport group has arrived.

  self:AddTransition("*",               "Delivered",         "*")           -- All cargo groups of a request have been delivered to the requesting warehouse.
  self:AddTransition("Running",         "SelfRequest",       "*")           -- Request to warehouse itself. Requested assets are only spawned but not delivered anywhere.
  self:AddTransition("Attacked",        "SelfRequest",       "*")           -- Request to warehouse itself. Also possible when warehouse is under attack!
  self:AddTransition("Running",         "Pause",             "Paused")      -- Pause the processing of new requests. Still possible to add assets and requests.
  self:AddTransition("Paused",          "Unpause",           "Running")     -- Unpause the warehouse. Queued requests are processed again.
  self:AddTransition("*",               "Stop",              "Stopped")     -- Stop the warehouse.
  self:AddTransition("Stopped",         "Restart",           "Running")     -- Restart the warehouse when it was stopped before.
  self:AddTransition("Loaded",          "Restart",           "Running")     -- Restart the warehouse when assets were loaded from file before.
  self:AddTransition("*",               "Save",              "*")           -- Save the warehouse state to disk.
  self:AddTransition("*",               "Attacked",          "Attacked")    -- Warehouse is under attack by enemy coalition.
  self:AddTransition("Attacked",        "Defeated",          "Running")     -- Attack by other coalition was defeated!
  self:AddTransition("*",               "ChangeCountry",     "*")           -- Change country (and coalition) of the warehouse. Warehouse is respawned!
  self:AddTransition("Attacked",        "Captured",          "Running")     -- Warehouse was captured by another coalition. It must have been attacked first.
  self:AddTransition("*",               "AirbaseCaptured",   "*")           -- Airbase was captured by other coalition.
  self:AddTransition("*",               "AirbaseRecaptured", "*")           -- Airbase was re-captured from other coalition.
  self:AddTransition("*",               "RunwayDestroyed",   "*")           -- Runway of the airbase was destroyed.
  self:AddTransition("*",               "RunwayRepaired",    "*")           -- Runway of the airbase was repaired.
  self:AddTransition("*",               "AssetDead",         "*")           -- An asset group died.
  self:AddTransition("*",               "Destroyed",         "Destroyed")   -- Warehouse was destroyed. All assets in stock are gone and warehouse is stopped.
  self:AddTransition("Destroyed",       "Respawn",           "Running")     -- Respawn warehouse after it was destroyed.

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the warehouse. Initializes parameters and starts event handlers.
  -- @function [parent=#WAREHOUSE] Start
  -- @param #WAREHOUSE self

  --- Triggers the FSM event "Start" after a delay. Starts the warehouse. Initializes parameters and starts event handlers.
  -- @function [parent=#WAREHOUSE] __Start
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the warehouse and all its event handlers. All waiting and pending queue items are deleted as well and all assets are removed from stock.
  -- @function [parent=#WAREHOUSE] Stop
  -- @param #WAREHOUSE self

  --- Triggers the FSM event "Stop" after a delay. Stops the warehouse and all its event handlers. All waiting and pending queue items are deleted as well and all assets are removed from stock.
  -- @function [parent=#WAREHOUSE] __Stop
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Restart". Restarts the warehouse from stopped state by reactivating the event handlers *only*.
  -- @function [parent=#WAREHOUSE] Restart
  -- @param #WAREHOUSE self

  --- Triggers the FSM event "Restart" after a delay. Restarts the warehouse from stopped state by reactivating the event handlers *only*.
  -- @function [parent=#WAREHOUSE] __Restart
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Respawn".
  -- @function [parent=#WAREHOUSE] Respawn
  -- @param #WAREHOUSE self

  --- Triggers the FSM event "Respawn" after a delay.
  -- @function [parent=#WAREHOUSE] __Respawn
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.

  --- On after "Respawn" event user function.
  -- @function [parent=#WAREHOUSE] OnAfterRespawn
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- Triggers the FSM event "Pause". Pauses the warehouse. Assets can still be added and requests be made. However, requests are not processed.
  -- @function [parent=#WAREHOUSE] Pause
  -- @param #WAREHOUSE self

  --- Triggers the FSM event "Pause" after a delay. Pauses the warehouse. Assets can still be added and requests be made. However, requests are not processed.
  -- @function [parent=#WAREHOUSE] __Pause
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Unpause". Unpauses the warehouse. Processing of queued requests is resumed.
  -- @function [parent=#WAREHOUSE] UnPause
  -- @param #WAREHOUSE self

  --- Triggers the FSM event "Unpause" after a delay. Unpauses the warehouse. Processing of queued requests is resumed.
  -- @function [parent=#WAREHOUSE] __Unpause
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Status". Queue is updated and requests are executed.
  -- @function [parent=#WAREHOUSE] Status
  -- @param #WAREHOUSE self

  --- Triggers the FSM event "Status" after a delay. Queue is updated and requests are executed.
  -- @function [parent=#WAREHOUSE] __Status
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.


  --- Trigger the FSM event "AddAsset". Add a group to the warehouse stock.
  -- @function [parent=#WAREHOUSE] AddAsset
  -- @param #WAREHOUSE self
  -- @param Wrapper.Group#GROUP group Group to be added as new asset.
  -- @param #number ngroups (Optional) Number of groups to add to the warehouse stock. Default is 1.
  -- @param #WAREHOUSE.Attribute forceattribute (Optional) Explicitly force a generalized attribute for the asset. This has to be an @{#WAREHOUSE.Attribute}.
  -- @param #number forcecargobay (Optional) Explicitly force cargobay weight limit in kg for cargo carriers. This is for each *unit* of the group.
  -- @param #number forceweight (Optional) Explicitly force weight in kg of each unit in the group.
  -- @param #number loadradius (Optional) The distance in meters when the cargo is loaded into the carrier. Default is the bounding box size of the carrier.
  -- @param DCS#AI.Skill skill Skill of the asset.
  -- @param #table liveries Table of livery names. When the asset is spawned one livery is chosen randomly.
  -- @param #string assignment A free to choose string specifying an assignment for the asset. This can be used with the @{#WAREHOUSE.OnAfterNewAsset} function.

  --- Trigger the FSM event "AddAsset" with a delay. Add a group to the warehouse stock.
  -- @function [parent=#WAREHOUSE] __AddAsset
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param Wrapper.Group#GROUP group Group to be added as new asset.
  -- @param #number ngroups (Optional) Number of groups to add to the warehouse stock. Default is 1.
  -- @param #WAREHOUSE.Attribute forceattribute (Optional) Explicitly force a generalized attribute for the asset. This has to be an @{#WAREHOUSE.Attribute}.
  -- @param #number forcecargobay (Optional) Explicitly force cargobay weight limit in kg for cargo carriers. This is for each *unit* of the group.
  -- @param #number forceweight (Optional) Explicitly force weight in kg of each unit in the group.
  -- @param #number loadradius (Optional) The distance in meters when the cargo is loaded into the carrier. Default is the bounding box size of the carrier.
  -- @param DCS#AI.Skill skill Skill of the asset.
  -- @param #table liveries Table of livery names. When the asset is spawned one livery is chosen randomly.
  -- @param #string assignment A free to choose string specifying an assignment for the asset. This can be used with the @{#WAREHOUSE.OnAfterNewAsset} function.


  --- Triggers the FSM delayed event "NewAsset" when a new asset has been added to the warehouse stock.
  -- @function [parent=#WAREHOUSE] NewAsset
  -- @param #WAREHOUSE self
  -- @param #WAREHOUSE.Assetitem asset The new asset.
  -- @param #string assignment (Optional) Assignment text for the asset.

  --- Triggers the FSM delayed event "NewAsset" when a new asset has been added to the warehouse stock.
  -- @function [parent=#WAREHOUSE] __NewAsset
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param #WAREHOUSE.Assetitem asset The new asset.
  -- @param #string assignment (Optional) Assignment text for the asset.

  --- On after "NewAsset" event user function. A new asset has been added to the warehouse stock.
  -- @function [parent=#WAREHOUSE] OnAfterNewAsset
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #WAREHOUSE.Assetitem asset The asset that has just been added.
  -- @param #string assignment (Optional) Assignment text for the asset.


  --- Triggers the FSM event "AddRequest". Add a request to the warehouse queue, which is processed when possible.
  -- @function [parent=#WAREHOUSE] AddRequest
  -- @param #WAREHOUSE self
  -- @param #WAREHOUSE warehouse The warehouse requesting supply.
  -- @param #WAREHOUSE.Descriptor AssetDescriptor Descriptor describing the asset that is requested.
  -- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc.
  -- @param #number nAsset Number of groups requested that match the asset specification.
  -- @param #WAREHOUSE.TransportType TransportType Type of transport.
  -- @param #number nTransport Number of transport units requested.
  -- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low.
  -- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets.

  --- Triggers the FSM event "AddRequest" with a delay. Add a request to the warehouse queue, which is processed when possible.
  -- @function [parent=#WAREHOUSE] __AddRequest
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param #WAREHOUSE warehouse The warehouse requesting supply.
  -- @param #WAREHOUSE.Descriptor AssetDescriptor Descriptor describing the asset that is requested.
  -- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc.
  -- @param #number nAsset Number of groups requested that match the asset specification.
  -- @param #WAREHOUSE.TransportType TransportType Type of transport.
  -- @param #number nTransport Number of transport units requested.
  -- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low.
  -- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets.


  --- Triggers the FSM event "Request". Executes a request from the queue if possible.
  -- @function [parent=#WAREHOUSE] Request
  -- @param #WAREHOUSE self
  -- @param #WAREHOUSE.Queueitem Request Information table of the request.

  --- Triggers the FSM event "Request" after a delay. Executes a request from the queue if possible.
  -- @function [parent=#WAREHOUSE] __Request
  -- @param #WAREHOUSE self
  -- @param #number Delay Delay in seconds.
  -- @param #WAREHOUSE.Queueitem Request Information table of the request.

  --- On before "Request" user function. The necessary cargo and transport assets will be spawned. Time to set some additional asset parameters.
  -- @function [parent=#WAREHOUSE] OnBeforeRequest
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #WAREHOUSE.Queueitem Request Information table of the request.

  --- On after "Request" user function. The necessary cargo and transport assets were spawned.
  -- @function [parent=#WAREHOUSE] OnAfterRequest
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #WAREHOUSE.Queueitem Request Information table of the request.


  --- Triggers the FSM event "Arrived" when a group has arrived at the destination warehouse.
  -- This function should always be called from the sending and not the receiving warehouse.
  -- If the group is a cargo asset, it is added to the receiving warehouse. If the group is a transporter it
  -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once
  -- all cargo was delivered.
  -- @function [parent=#WAREHOUSE] Arrived
  -- @param #WAREHOUSE self
  -- @param Wrapper.Group#GROUP group Group that has arrived.

  --- Triggers the FSM event "Arrived" after a delay when a group has arrived at the destination.
  -- This function should always be called from the sending and not the receiving warehouse.
  -- If the group is a cargo asset, it is added to the receiving warehouse. If the group is a transporter it
  -- is added to the sending warehouse since carriers are supposed to return to their home warehouse once
  -- @function [parent=#WAREHOUSE] __Arrived
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param Wrapper.Group#GROUP group Group that has arrived.

  --- On after "Arrived" event user function. Called when a group has arrived at its destination.
  -- @function [parent=#WAREHOUSE] OnAfterArrived
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Group#GROUP group Group that has arrived.


  --- Triggers the FSM event "Delivered". All (cargo) assets of a request have been delivered to the receiving warehouse.
  -- @function [parent=#WAREHOUSE] Delivered
  -- @param #WAREHOUSE self
  -- @param #WAREHOUSE.Pendingitem request Pending request that was now delivered.

  --- Triggers the FSM event "Delivered" after a delay. A group has been delivered from the warehouse to another warehouse.
  -- @function [parent=#WAREHOUSE] __Delivered
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param #WAREHOUSE.Pendingitem request Pending request that was now delivered.

  --- On after "Delivered" event user function. Called when a group has been delivered from the warehouse to another warehouse.
  -- @function [parent=#WAREHOUSE] OnAfterDelivered
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #WAREHOUSE.Pendingitem request Pending request that was now delivered.


  --- Triggers the FSM event "SelfRequest". Request was initiated from the warehouse to itself. Groups are just spawned at the warehouse or the associated airbase.
  -- If the warehouse is currently under attack when the self request is made, the self request is added to the defending table. One the attack is defeated,
  -- this request is used to put the groups back into the warehouse stock.
  -- @function [parent=#WAREHOUSE] SelfRequest
  -- @param #WAREHOUSE self
  -- @param Core.Set#SET_GROUP groupset The set of cargo groups that was delivered to the warehouse itself.
  -- @param #WAREHOUSE.Pendingitem request Pending self request.

  --- Triggers the FSM event "SelfRequest" with a delay. Request was initiated from the warehouse to itself. Groups are just spawned at the warehouse or the associated airbase.
  -- If the warehouse is currently under attack when the self request is made, the self request is added to the defending table. One the attack is defeated,
  -- this request is used to put the groups back into the warehouse stock.
  -- @function [parent=#WAREHOUSE] __SelfRequest
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param Core.Set#SET_GROUP groupset The set of cargo groups that was delivered to the warehouse itself.
  -- @param #WAREHOUSE.Pendingitem request Pending self request.

  --- On after "SelfRequest" event. Request was initiated from the warehouse to itself. Groups are simply spawned at the warehouse or the associated airbase.
  -- All requested assets are passed as a @{Core.Set#SET_GROUP} and can be used for further tasks or in other MOOSE classes.
  -- Note that airborne assets are spawned in uncontrolled state so they do not simply "fly away" after spawning.
  --
  -- @usage
  -- --- Self request event. Triggered once the assets are spawned in the spawn zone or at the airbase.
  -- function mywarehouse:OnAfterSelfRequest(From, Event, To, groupset, request)
  --   local groupset=groupset --Core.Set#SET_GROUP
  --
  --   -- Loop over all groups spawned from that request.
  --   for _,group in pairs(groupset:GetSetObjects()) do
  --     local group=group --Wrapper.Group#GROUP
  --
  --     -- Gree smoke on spawned group.
  --     group:SmokeGreen()
  --
  --     -- Activate uncontrolled airborne group if necessary.
  --     group:StartUncontrolled()
  --   end
  -- end
  --
  -- @function [parent=#WAREHOUSE] OnAfterSelfRequest
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Set#SET_GROUP groupset The set of (cargo) groups that was delivered to the warehouse itself.
  -- @param #WAREHOUSE.Pendingitem request Pending self request.


  --- Triggers the FSM event "Attacked" when a warehouse is under attack by an another coalition.
  -- @function [parent=#WAREHOUSE] Attacked
  -- @param #WAREHOUSE self
  -- @param DCS#coalition.side Coalition Coalition side which is attacking the warehouse, i.e. a number of @{DCS#coalition.side} enumerator.
  -- @param DCS#country.id Country Country ID, which is attacking the warehouse, i.e. a number @{DCS#country.id} enumerator.

  --- Triggers the FSM event "Attacked" with a delay when a warehouse is under attack by an another coalition.
  -- @function [parent=#WAREHOUSE] __Attacked
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param DCS#coalition.side Coalition Coalition side which is attacking the warehouse, i.e. a number of @{DCS#coalition.side} enumerator.
  -- @param DCS#country.id Country Country ID, which is attacking the warehouse, i.e. a number @{DCS#country.id} enumerator.

  --- On after "Attacked" event user function. Called when a warehouse (zone) is under attack by an enemy.
  -- @function [parent=#WAREHOUSE] OnAfterAttacked
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param DCS#coalition.side Coalition Coalition side which is attacking the warehouse, i.e. a number of @{DCS#coalition.side} enumerator.
  -- @param DCS#country.id Country Country ID, which is attacking the warehouse, i.e. a number @{DCS#country.id} enumerator.


  --- Triggers the FSM event "Defeated" when an attack from an enemy was defeated.
  -- @function [parent=#WAREHOUSE] Defeated
  -- @param #WAREHOUSE self

  --- Triggers the FSM event "Defeated" with a delay when an attack from an enemy was defeated.
  -- @function [parent=#WAREHOUSE] __Defeated
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.

  --- On after "Defeated" event user function. Called when an enemy attack was defeated.
  -- @function [parent=#WAREHOUSE] OnAfterDefeate
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "ChangeCountry" so the warehouse is respawned with the new country.
  -- @function [parent=#WAREHOUSE] ChangeCountry
  -- @param #WAREHOUSE self
  -- @param DCS#country.id Country New country id of the warehouse.

  --- Triggers the FSM event "ChangeCountry" after a delay so the warehouse is respawned with the new country.
  -- @function [parent=#WAREHOUSE] __ChangeCountry
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param DCS#country.id Country Country id which has captured the warehouse.

  --- On after "ChangeCountry" event user function. Called when the warehouse has changed its country.
  -- @function [parent=#WAREHOUSE] OnAfterChangeCountry
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param DCS#country.id Country New country id of the warehouse, i.e. a number @{DCS#country.id} enumerator.


  --- Triggers the FSM event "Captured" when a warehouse has been captured by another coalition.
  -- @function [parent=#WAREHOUSE] Captured
  -- @param #WAREHOUSE self
  -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse.
  -- @param DCS#country.id Country Country id which has captured the warehouse.

  --- Triggers the FSM event "Captured" with a delay when a warehouse has been captured by another coalition.
  -- @function [parent=#WAREHOUSE] __Captured
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse.
  -- @param DCS#country.id Country Country id which has captured the warehouse.

  --- On after "Captured" event user function. Called when the warehouse has been captured by an enemy coalition.
  -- @function [parent=#WAREHOUSE] OnAfterCaptured
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param DCS#coalition.side Coalition Coalition side which captured the warehouse, i.e. a number of @{DCS#coalition.side} enumerator.
  -- @param DCS#country.id Country Country id which has captured the warehouse, i.e. a number @{DCS#country.id} enumerator.
  --

  --- Triggers the FSM event "AirbaseCaptured" when the airbase of the warehouse has been captured by another coalition.
  -- @function [parent=#WAREHOUSE] AirbaseCaptured
  -- @param #WAREHOUSE self
  -- @param DCS#coalition.side Coalition Coalition side which captured the airbase, i.e. a number of @{DCS#coalition.side} enumerator.

  --- Triggers the FSM event "AirbaseCaptured" with a delay when the airbase of the warehouse has been captured by another coalition.
  -- @function [parent=#WAREHOUSE] __AirbaseCaptured
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param DCS#coalition.side Coalition Coalition side which captured the airbase, i.e. a number of @{DCS#coalition.side} enumerator.

  --- On after "AirbaseCaptured" even user function. Called when the airbase of the warehouse has been captured by another coalition.
  -- @function [parent=#WAREHOUSE] OnAfterAirbaseCaptured
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param DCS#coalition.side Coalition Coalition side which captured the airbase, i.e. a number of @{DCS#coalition.side} enumerator.


  --- Triggers the FSM event "AirbaseRecaptured" when the airbase of the warehouse has been re-captured from the other coalition.
  -- @param #WAREHOUSE self
  -- @function [parent=#WAREHOUSE] AirbaseRecaptured
  -- @param DCS#coalition.side Coalition Coalition which re-captured the airbase, i.e. the same as the current warehouse owner coalition.

  --- Triggers the FSM event "AirbaseRecaptured" with a delay when the airbase of the warehouse has been re-captured from the other coalition.
  -- @function [parent=#WAREHOUSE] __AirbaseRecaptured
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param DCS#coalition.side Coalition Coalition which re-captured the airbase, i.e. the same as the current warehouse owner coalition.

  --- On after "AirbaseRecaptured" event user function. Called when the airbase of the warehouse has been re-captured from the other coalition.
  -- @function [parent=#WAREHOUSE] OnAfterAirbaseRecaptured
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param DCS#coalition.side Coalition Coalition which re-captured the airbase, i.e. the same as the current warehouse owner coalition.


  --- Triggers the FSM event "AssetDead" when an asset group has died.
  -- @function [parent=#WAREHOUSE] AssetDead
  -- @param #WAREHOUSE self
  -- @param #WAREHOUSE.Assetitem asset The asset that is dead.
  -- @param #WAREHOUSE.Pendingitem request The request of the dead asset.

  --- Triggers the delayed FSM event "AssetDead" when an asset group has died.
  -- @function [parent=#WAREHOUSE] __AssetDead
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param #WAREHOUSE.Assetitem asset The asset that is dead.
  -- @param #WAREHOUSE.Pendingitem request The request of the dead asset.

  --- On after "AssetDead" event user function. Called when an asset group died.
  -- @function [parent=#WAREHOUSE] OnAfterAssetDead
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #WAREHOUSE.Assetitem asset The asset that is dead.
  -- @param #WAREHOUSE.Pendingitem request The request of the dead asset.


  --- Triggers the FSM event "Destroyed" when the warehouse was destroyed. Services are stopped.
  -- @function [parent=#WAREHOUSE] Destroyed
  -- @param #WAREHOUSE self

  --- Triggers the FSM event "Destroyed" with a delay when the warehouse was destroyed. Services are stopped.
  -- @function [parent=#WAREHOUSE] __Destroyed
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.

  --- On after "Destroyed" event user function. Called when the warehouse was destroyed. Services are stopped.
  -- @function [parent=#WAREHOUSE] OnAfterDestroyed
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "AssetSpawned" when the warehouse has spawned an asset.
  -- @function [parent=#WAREHOUSE] AssetSpawned
  -- @param #WAREHOUSE self
  -- @param Wrapper.Group#GROUP group the group that was spawned.
  -- @param #WAREHOUSE.Assetitem asset The asset that was spawned.
  -- @param #WAREHOUSE.Pendingitem request The request of the spawned asset.

  --- Triggers the FSM event "AssetSpawned" with a delay when the warehouse has spawned an asset.
  -- @function [parent=#WAREHOUSE] __AssetSpawned
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param Wrapper.Group#GROUP group the group that was spawned.
  -- @param #WAREHOUSE.Assetitem asset The asset that was spawned.
  -- @param #WAREHOUSE.Pendingitem request The request of the spawned asset.

  --- On after "AssetSpawned" event user function. Called when the warehouse has spawned an asset.
  -- @function [parent=#WAREHOUSE] OnAfterAssetSpawned
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Group#GROUP group the group that was spawned.
  -- @param #WAREHOUSE.Assetitem asset The asset that was spawned.
  -- @param #WAREHOUSE.Pendingitem request The request of the spawned asset.


  --- Triggers the FSM event "AssetLowFuel" when an asset runs low on fuel
  -- @function [parent=#WAREHOUSE] AssetLowFuel
  -- @param #WAREHOUSE self
  -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel.
  -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel.

  --- Triggers the FSM event "AssetLowFuel" with a delay when an asset  runs low on fuel.
  -- @function [parent=#WAREHOUSE] __AssetLowFuel
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel.
  -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel.

  --- On after "AssetLowFuel" event user function. Called when the an asset is low on fuel.
  -- @function [parent=#WAREHOUSE] OnAfterAssetLowFuel
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #WAREHOUSE.Assetitem asset The asset that is low on fuel.
  -- @param #WAREHOUSE.Pendingitem request The request of the asset that is low on fuel.


  --- Triggers the FSM event "Save" when the warehouse assets are saved to file on disk.
  -- @function [parent=#WAREHOUSE] Save
  -- @param #WAREHOUSE self
  -- @param #string path Path where the file is saved. Default is the DCS installation root directory.
  -- @param #string filename (Optional) File name. Default is WAREHOUSE-<UID>_<ALIAS>.txt.

  --- Triggers the FSM event "Save" with a delay when the warehouse assets are saved to a file.
  -- @function [parent=#WAREHOUSE] __Save
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param #string path Path where the file is saved. Default is the DCS installation root directory.
  -- @param #string filename (Optional) File name. Default is WAREHOUSE-<UID>_<ALIAS>.txt.

  --- On after "Save" event user function. Called when the warehouse assets are saved to disk.
  -- @function [parent=#WAREHOUSE] OnAfterSave
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path Path where the file is saved. Default is the DCS installation root directory.
  -- @param #string filename (Optional) File name. Default is WAREHOUSE-<UID>_<ALIAS>.txt.


  --- Triggers the FSM event "Load" when the warehouse is loaded from a file on disk.
  -- @function [parent=#WAREHOUSE] Load
  -- @param #WAREHOUSE self
  -- @param #string path Path where the file is located. Default is the DCS installation root directory.
  -- @param #string filename (Optional) File name. Default is WAREHOUSE-<UID>_<ALIAS>.txt.

  --- Triggers the FSM event "Load" with a delay when the warehouse assets are loaded from disk.
  -- @function [parent=#WAREHOUSE] __Load
  -- @param #WAREHOUSE self
  -- @param #number delay Delay in seconds.
  -- @param #string path Path where the file is located. Default is the DCS installation root directory.
  -- @param #string filename (Optional) File name. Default is WAREHOUSE-<UID>_<ALIAS>.txt.

  --- On after "Load" event user function. Called when the warehouse assets are loaded from disk.
  -- @function [parent=#WAREHOUSE] OnAfterLoad
  -- @param #WAREHOUSE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path Path where the file is located. Default is the DCS installation root directory.
  -- @param #string filename (Optional) File name. Default is WAREHOUSE-<UID>_<ALIAS>.txt.


  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set debug mode on. Error messages will be displayed on screen, units will be smoked at some events.
-- @param #WAREHOUSE self
-- @return #WAREHOUSE self
function WAREHOUSE:SetDebugOn()
  self.Debug=true
  return self
end

--- Set debug mode off. This is the default
-- @param #WAREHOUSE self
-- @return #WAREHOUSE self
function WAREHOUSE:SetDebugOff()
  self.Debug=false
  return self
end

--- Set report on. Messages at events will be displayed on screen to the coalition owning the warehouse.
-- @param #WAREHOUSE self
-- @return #WAREHOUSE self
function WAREHOUSE:SetReportOn()
  self.Report=true
  return self
end

--- Set report off. Warehouse does not report about its status and at certain events.
-- @param #WAREHOUSE self
-- @return #WAREHOUSE self
function WAREHOUSE:SetReportOff()
  self.Report=false
  return self
end

--- Enable safe parking option, i.e. parking spots at an airbase will be considered as occupied when a client aircraft is parked there (even if the client slot is not taken by a player yet).
-- Note that also incoming aircraft can reserve/occupie parking spaces.
-- @param #WAREHOUSE self
-- @return #WAREHOUSE self
function WAREHOUSE:SetSafeParkingOn()
  self.safeparking=true
  return self
end

--- Disable safe parking option. Note that is the default setting.
-- @param #WAREHOUSE self
-- @return #WAREHOUSE self
function WAREHOUSE:SetSafeParkingOff()
  self.safeparking=false
  return self
end

--- Set wether client parking spots can be used for spawning.
-- @param #WAREHOUSE self
-- @return #WAREHOUSE self
function WAREHOUSE:SetAllowSpawnOnClientParking()
  self.allowSpawnOnClientSpots=true
  return self
end

--- Set low fuel threshold. If one unit of an asset has less fuel than this number, the event AssetLowFuel will be fired.
-- @param #WAREHOUSE self
-- @param #number threshold Relative low fuel threshold, i.e. a number in [0,1]. Default 0.15 (15%).
-- @return #WAREHOUSE self
function WAREHOUSE:SetLowFuelThreshold(threshold)
  self.lowfuelthresh=threshold or 0.15
  return self
end

--- Set interval of status updates. Note that normally only one request can be processed per time interval.
-- @param #WAREHOUSE self
-- @param #number timeinterval Time interval in seconds.
-- @return #WAREHOUSE self
function WAREHOUSE:SetStatusUpdate(timeinterval)
  self.dTstatus=timeinterval
  return self
end

--- Set verbosity level.
-- @param #WAREHOUSE self
-- @param #number VerbosityLevel Level of output (higher=more). Default 0.
-- @return #WAREHOUSE self
function WAREHOUSE:SetVerbosityLevel(VerbosityLevel)
  self.verbosity=VerbosityLevel or 0
  return self
end

--- Set a zone where the (ground) assets of the warehouse are spawned once requested.
-- @param #WAREHOUSE self
-- @param Core.Zone#ZONE zone The spawn zone.
-- @param #number maxdist (Optional) Maximum distance in meters between spawn zone and warehouse. Units are not spawned if distance is larger. Default is 5000 m.
-- @return #WAREHOUSE self
function WAREHOUSE:SetSpawnZone(zone, maxdist)
  self.spawnzone=zone
  self.spawnzonemaxdist=maxdist or 5000
  return self
end

--- Get the spawn zone.
-- @param #WAREHOUSE self
-- @return Core.Zone#ZONE The spawn zone.
function WAREHOUSE:GetSpawnZone()
  return self.spawnzone
end

--- Set a warehouse zone. If this zone is captured, the warehouse and all its assets fall into the hands of the enemy.
-- @param #WAREHOUSE self
-- @param Core.Zone#ZONE zone The warehouse zone. Note that this **cannot** be a polygon zone!
-- @return #WAREHOUSE self
function WAREHOUSE:SetWarehouseZone(zone)
  self.zone=zone
  return self
end

--- Get the warehouse zone.
-- @param #WAREHOUSE self
-- @return Core.Zone#ZONE The warehouse zone.
function WAREHOUSE:GetWarehouseZone()
  return self.zone
end

--- Set auto defence on. When the warehouse is under attack, all ground assets are spawned automatically and will defend the warehouse zone.
-- @param #WAREHOUSE self
-- @return #WAREHOUSE self
function WAREHOUSE:SetAutoDefenceOn()
  self.autodefence=true
  return self
end

--- Set auto defence off. This is the default.
-- @param #WAREHOUSE self
-- @return #WAREHOUSE self
function WAREHOUSE:SetAutoDefenceOff()
  self.autodefence=false
  return self
end

--- Set valid parking spot IDs.
-- @param #WAREHOUSE self
-- @param #table ParkingIDs Table of numbers.
-- @return #WAREHOUSE self
function WAREHOUSE:SetParkingIDs(ParkingIDs)
  if type(ParkingIDs)~="table" then
    ParkingIDs={ParkingIDs}
  end
  self.parkingIDs=ParkingIDs
  return self
end

--- Check parking ID.
-- @param #WAREHOUSE self
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot.
-- @return #boolean If true, parking is valid.
function WAREHOUSE:_CheckParkingValid(spot)

  if self.parkingIDs==nil then
    return true
  end

  for _,id in pairs(self.parkingIDs or {}) do
    if spot.TerminalID==id then
      return true
    end
  end

  return false
end

--- Check parking ID for an asset.
-- @param #WAREHOUSE self
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot.
-- @return #boolean If true, parking is valid.
function WAREHOUSE:_CheckParkingAsset(spot, asset)

  if asset.parkingIDs==nil then
    return true
  end

  for _,id in pairs(asset.parkingIDs or {}) do
    if spot.TerminalID==id then
      return true
    end
  end

  return false
end


--- Enable auto save of warehouse assets at mission end event.
-- @param #WAREHOUSE self
-- @param #string path Path where to save the asset data file.
-- @param #string filename File name. Default is generated automatically from warehouse id.
-- @return #WAREHOUSE self
function WAREHOUSE:SetSaveOnMissionEnd(path, filename)
  self.autosave=true
  self.autosavepath=path
  self.autosavefile=filename
  return self
end

--- Show or don't show markers on the F10 map displaying the Warehouse stock and road/rail connections.
-- @param #WAREHOUSE self
-- @param #boolean switch If true (or nil), markers are on. If false, markers are not displayed.
-- @return #WAREHOUSE self
function WAREHOUSE:SetMarker(switch)
  if switch==false then
    self.markerOn=false
  else
    self.markerOn=true
  end
  return self
end

--- Set respawn after destroy.
-- @param #WAREHOUSE self
-- @return #WAREHOUSE self
function WAREHOUSE:SetRespawnAfterDestroyed(delay)
  self.respawnafterdestroyed=true
  self.respawndelay=delay
  return self
end


--- Set the airbase belonging to this warehouse.
-- Note that it has to be of the same coalition as the warehouse.
-- Also, be reasonable and do not put it too far from the phyiscal warehouse structure because you troops might have a long way to get to their transports.
-- @param #WAREHOUSE self
-- @param Wrapper.Airbase#AIRBASE airbase The airbase object associated to this warehouse.
-- @return #WAREHOUSE self
function WAREHOUSE:SetAirbase(airbase)
  self.airbase=airbase
  if airbase~=nil then
    self.airbasename=airbase:GetName()
  else
    self.airbasename=nil
  end
  return self
end

--- Set the connection of the warehouse to the road.
-- Ground assets spawned in the warehouse spawn zone will first go to this point and from there travel on road to the requesting warehouse.
-- Note that by default the road connection is set to the closest point on road from the center of the spawn zone if it is withing 3000 meters.
-- Also note, that if the parameter "coordinate" is passed as nil, any road connection is disabled and ground assets cannot travel of be transportet on the ground.
-- @param #WAREHOUSE self
-- @param Core.Point#COORDINATE coordinate The road connection. Technically, the closest point on road from this coordinate is determined by DCS API function. So this point must not be exactly on the road.
-- @return #WAREHOUSE self
function WAREHOUSE:SetRoadConnection(coordinate)
  if coordinate then
    self.road=coordinate:GetClosestPointToRoad()
  else
    self.road=false
  end
  return self
end

--- Set the connection of the warehouse to the railroad.
-- This is the place where train assets or transports will be spawned.
-- @param #WAREHOUSE self
-- @param Core.Point#COORDINATE coordinate The railroad connection. Technically, the closest point on rails from this coordinate is determined by DCS API function. So this point must not be exactly on the a railroad connection.
-- @return #WAREHOUSE self
function WAREHOUSE:SetRailConnection(coordinate)
  if coordinate then
    self.rail=coordinate:GetClosestPointToRoad(true)
  else
    self.rail=false
  end
  return self
end

--- Set the port zone for this warehouse.
-- The port zone is the zone, where all naval assets of the warehouse are spawned.
-- @param #WAREHOUSE self
-- @param Core.Zone#ZONE zone The zone defining the naval port of the warehouse.
-- @return #WAREHOUSE self
function WAREHOUSE:SetPortZone(zone)
  self.portzone=zone
  return self
end

--- Add a Harbor Zone for this warehouse where naval cargo units will spawn and be received.
-- Both warehouses must have the harbor zone defined for units to properly spawn on both the 
-- sending and receiving side. The harbor zone should be within 3km of the port zone used for 
-- warehouse in order to facilitate the boarding process.
-- @param #WAREHOUSE self
-- @param Core.Zone#ZONE zone The zone defining the naval embarcation/debarcation point for cargo units
-- @return #WAREHOUSE self
function WAREHOUSE:SetHarborZone(zone)
  self.harborzone=zone
  return self
end

--- Add a shipping lane from this warehouse to another remote warehouse.
-- Note that both warehouses must have a port zone defined before a shipping lane can be added!
-- Shipping lane is taken from the waypoints of a (late activated) template group. So set up a group, e.g. a ship or a helicopter, and place its
-- waypoints along the shipping lane you want to add.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE remotewarehouse The remote warehouse to where the shipping lane is added
-- @param Wrapper.Group#GROUP group Waypoints of this group will define the shipping lane between to warehouses.
-- @param #boolean oneway (Optional) If true, the lane can only be used from this warehouse to the other but not other way around. Default false.
-- @return #WAREHOUSE self
function WAREHOUSE:AddShippingLane(remotewarehouse, group, oneway)

  -- Check that port zones are defined.
  if self.portzone==nil or remotewarehouse.portzone==nil then
    local text=string.format("ERROR: Sending or receiving warehouse does not have a port zone defined. Adding shipping lane not possible!")
    self:_ErrorMessage(text, 5)
    return self
  end

  -- Initial and final coordinates are random points within the port zones.
  local startcoord=self.portzone:GetRandomCoordinate()
  local finalcoord=remotewarehouse.portzone:GetRandomCoordinate()

  -- Create new lane from waypoints of the template group.
  local lane=self:_NewLane(group, startcoord, finalcoord)

  -- Debug info. Marks along shipping lane.
  if self.Debug then
    for i=1,#lane do
      local coord=lane[i] --Core.Point#COORDINATE
      local text=string.format("Shipping lane %s to %s. Point %d.", self.alias, remotewarehouse.alias, i)
      coord:MarkToCoalition(text, self:GetCoalition())
    end
  end

  -- Name of the remote warehouse.
  local remotename=remotewarehouse.warehouse:GetName()

  -- Create new table if no shipping lane exists yet.
  if self.shippinglanes[remotename]==nil then
    self.shippinglanes[remotename]={}
  end

  -- Add shipping lane.
  table.insert(self.shippinglanes[remotename], lane)

  -- Add shipping lane in the opposite direction.
  if not oneway then
    remotewarehouse:AddShippingLane(self, group, true)
  end

  return self
end


--- Add an off-road path from this warehouse to another and back.
-- The start and end points are automatically set to one random point in the respective spawn zones of the two warehouses.
-- By default, the reverse path is also added as path from the remote warehouse to this warehouse.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE remotewarehouse The remote warehouse to which the path leads.
-- @param Wrapper.Group#GROUP group Waypoints of this group will define the path between to warehouses.
-- @param #boolean oneway (Optional) If true, the path can only be used from this warehouse to the other but not other way around. Default false.
-- @return #WAREHOUSE self
function WAREHOUSE:AddOffRoadPath(remotewarehouse, group, oneway)

  -- Initial and final points are random points within the spawn zone.
  local startcoord=self.spawnzone:GetRandomCoordinate()
  local finalcoord=remotewarehouse.spawnzone:GetRandomCoordinate()

  -- Create new path from template group waypoints.
  local path=self:_NewLane(group, startcoord, finalcoord)

  if path==nil then
    self:E(self.lid.."ERROR: Offroad path could not be added. Group present in ME?")
    return
  end

  -- Debug info. Marks along path.
  if path and self.Debug then
    for i=1,#path do
      local coord=path[i] --Core.Point#COORDINATE
      local text=string.format("Off road path from %s to %s. Point %d.", self.alias, remotewarehouse.alias, i)
      coord:MarkToCoalition(text, self:GetCoalition())
    end
  end

  -- Name of the remote warehouse.
  local remotename=remotewarehouse.warehouse:GetName()

  -- Create new table if no shipping lane exists yet.
  if self.offroadpaths[remotename]==nil then
    self.offroadpaths[remotename]={}
  end

  -- Add off road path.
  table.insert(self.offroadpaths[remotename], path)

  -- Add off road path in the opposite direction (if not forbidden).
  if not oneway then
    remotewarehouse:AddOffRoadPath(self, group, true)
  end

  return self
end

--- Create a new path from a template group.
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP group Group used for extracting the waypoints.
-- @param Core.Point#COORDINATE startcoord First coordinate.
-- @param Core.Point#COORDINATE finalcoord Final coordinate.
-- @return #table Table with route points.
function WAREHOUSE:_NewLane(group, startcoord, finalcoord)

  local lane=nil

  if group then

    -- Get route from template.
    local lanepoints=group:GetTemplateRoutePoints()

    -- First and last waypoints
    local laneF=lanepoints[1]
    local laneL=lanepoints[#lanepoints]

    -- Get corresponding coordinates.
    local coordF=COORDINATE:New(laneF.x, 0, laneF.y)
    local coordL=COORDINATE:New(laneL.x, 0, laneL.y)

    -- Figure out which point is closer to the port of this warehouse.
    local distF=startcoord:Get2DDistance(coordF)
    local distL=startcoord:Get2DDistance(coordL)

    -- Add the lane. Need to take care of the wrong "direction".
    lane={}
    if distF<distL then
      for i=1,#lanepoints do
        local point=lanepoints[i]
        local coord=COORDINATE:New(point.x,0, point.y)
        table.insert(lane, coord)
      end
    else
      for i=#lanepoints,1,-1 do
        local point=lanepoints[i]
        local coord=COORDINATE:New(point.x,0, point.y)
        table.insert(lane, coord)
      end
    end

    -- Automatically add end point which is a random point inside the final port zone.
    table.insert(lane, #lane, finalcoord)

  end

  return lane
end


--- Check if the warehouse has not been started yet, i.e. is in the state "NotReadyYet".
-- @param #WAREHOUSE self
-- @return #boolean If true, the warehouse object has been created but the warehouse has not been started yet.
function WAREHOUSE:IsNotReadyYet()
  return self:is("NotReadyYet")
end

--- Check if the warehouse has been loaded from disk via the "Load" event.
-- @param #WAREHOUSE self
-- @return #boolean If true, the warehouse was loaded from disk.
function WAREHOUSE:IsLoaded()
  return self:is("Loaded")
end

--- Check if the warehouse is running.
-- @param #WAREHOUSE self
-- @return #boolean If true, the warehouse is running and requests are processed.
function WAREHOUSE:IsRunning()
  return self:is("Running")
end

--- Check if the warehouse is paused. In this state, requests are not processed.
-- @param #WAREHOUSE self
-- @return #boolean If true, the warehouse is paused.
function WAREHOUSE:IsPaused()
  return self:is("Paused")
end

--- Check if the warehouse is under attack by another coalition.
-- @param #WAREHOUSE self
-- @return #boolean If true, the warehouse is attacked.
function WAREHOUSE:IsAttacked()
  return self:is("Attacked")
end

--- Check if the warehouse has been destroyed.
-- @param #WAREHOUSE self
-- @return #boolean If true, the warehouse had been destroyed.
function WAREHOUSE:IsDestroyed()
  return self:is("Destroyed")
end

--- Check if the warehouse is stopped.
-- @param #WAREHOUSE self
-- @return #boolean If true, the warehouse is stopped.
function WAREHOUSE:IsStopped()
  return self:is("Stopped")
end

--- Check if the warehouse has a road connection to another warehouse. Both warehouses need to be started!
-- @param #WAREHOUSE self
-- @param #WAREHOUSE warehouse The remote warehouse to where the connection is checked.
-- @param #boolean markpath If true, place markers of path segments on the F10 map.
-- @param #boolean smokepath If true, put green smoke on path segments.
-- @return #boolean If true, the two warehouses are connected by road.
-- @return #number Path length in meters. Negative distance -1 meter indicates no connection.
function WAREHOUSE:HasConnectionRoad(warehouse, markpath, smokepath)
  if warehouse then
    if self.road and warehouse.road then
      local _,length,gotpath=self.road:GetPathOnRoad(warehouse.road, false, false, markpath, smokepath)
      return gotpath, length or -1
    else
      -- At least one of the warehouses has no road connection.
      return false, -1
    end
  end
  return nil, -1
end

--- Check if the warehouse has a railroad connection to another warehouse. Both warehouses need to be started!
-- @param #WAREHOUSE self
-- @param #WAREHOUSE warehouse The remote warehouse to where the connection is checked.
-- @param #boolean markpath If true, place markers of path segments on the F10 map.
-- @param #boolean smokepath If true, put green smoke on path segments.
-- @return #boolean If true, the two warehouses are connected by road.
-- @return #number Path length in meters. Negative distance -1 meter indicates no connection.
function WAREHOUSE:HasConnectionRail(warehouse, markpath, smokepath)
  if warehouse then
    if self.rail and warehouse.rail then
      local _,length,gotpath=self.road:GetPathOnRoad(warehouse.road, false, true, markpath, smokepath)
      return gotpath, length or -1
    else
      -- At least one of the warehouses has no rail connection.
      return false, -1
    end
  end
  return nil, -1
end

--- Check if the warehouse has a shipping lane defined to another warehouse.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE warehouse The remote warehouse to where the connection is checked.
-- @param #boolean markpath If true, place markers of path segments on the F10 map.
-- @param #boolean smokepath If true, put green smoke on path segments.
-- @return #boolean If true, the two warehouses are connected by road.
-- @return #number Path length in meters. Negative distance -1 meter indicates no connection.
function WAREHOUSE:HasConnectionNaval(warehouse, markpath, smokepath)

  if warehouse then

    -- Self request
    if warehouse.warehouse:GetName()==self.warehouse:GetName() then
      return true,1
    end

    -- Get shipping lane.
    local shippinglane=self.shippinglanes[warehouse.warehouse:GetName()]

    if shippinglane then
      return true,1
    else
      self:T2(string.format("No shipping lane defined between warehouse %s and %s!", self.alias, warehouse.alias))
    end

  end

  return nil, -1
end

--- Check if the warehouse has an off road path defined to another warehouse.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE warehouse The remote warehouse to where the connection is checked.
-- @param #boolean markpath If true, place markers of path segments on the F10 map.
-- @param #boolean smokepath If true, put green smoke on path segments.
-- @return #boolean If true, the two warehouses are connected by road.
-- @return #number Path length in meters. Negative distance -1 meter indicates no connection.
function WAREHOUSE:HasConnectionOffRoad(warehouse, markpath, smokepath)

  if warehouse then

    -- Self request
    if warehouse.warehouse:GetName()==self.warehouse:GetName() then
      return true,1
    end

    -- Get shipping lane.
    local offroadpath=self.offroadpaths[warehouse.warehouse:GetName()]

    if offroadpath~=nil then
      return true,1
    else
      self:T2(string.format("No off-road path defined between warehouse %s and %s!", self.alias, warehouse.alias))
    end

  end

  return nil, -1
end


--- Get number of assets in warehouse stock. Optionally, only specific assets can be counted.
-- @param #WAREHOUSE self
-- @param #string Descriptor (Optional) Descriptor return the number of a specifc asset type. See @{#WAREHOUSE.Descriptor} for possible values.
-- @param DescriptorValue (Optional) Descriptor value selecting the type of assets.
-- @param #boolean OnlyMobile (Optional) If true only mobile units are considered.
-- @return #number Number of assets in stock.
function WAREHOUSE:GetNumberOfAssets(Descriptor, DescriptorValue, OnlyMobile)

  if Descriptor==nil or DescriptorValue==nil then
    -- All assets.
    return #self.stock
  else
    -- Selected assets.
    local _stock,_nstock=self:_FilterStock(self.stock, Descriptor, DescriptorValue, nil, OnlyMobile)
    return _nstock
  end

end

--- Get coordinate of warehouse static.
-- @param #WAREHOUSE self
-- @return Core.Point#COORDINATE The coordinate of the warehouse.
function WAREHOUSE:GetCoordinate()
  return self.warehouse:GetCoord()
end

--- Get 3D vector of warehouse static.
-- @param #WAREHOUSE self
-- @return DCS#Vec3 The 3D vector of the warehouse.
function WAREHOUSE:GetVec3()
  local vec3=self.warehouse:GetVec3()
  return vec3
end

--- Get 2D vector of warehouse static.
-- @param #WAREHOUSE self
-- @return DCS#Vec2 The 2D vector of the warehouse.
function WAREHOUSE:GetVec2()
  local vec2=self.warehouse:GetVec2()
  return vec2
end


--- Get coalition side of warehouse static.
-- @param #WAREHOUSE self
-- @return #number Coalition side, i.e. number of @{DCS#coalition.side}.
function WAREHOUSE:GetCoalition()
  return self.warehouse:GetCoalition()
end

--- Get coalition name of warehouse static.
-- @param #WAREHOUSE self
-- @return #number Coalition side, i.e. number of @{DCS#coalition.side}.
function WAREHOUSE:GetCoalitionName()
  return self.warehouse:GetCoalitionName()
end

--- Get country id of warehouse static.
-- @param #WAREHOUSE self
-- @return #number Country id, i.e. number of @{DCS#country.id}.
function WAREHOUSE:GetCountry()
  return self.warehouse:GetCountry()
end

--- Get country name of warehouse static.
-- @param #WAREHOUSE self
-- @return #number Country id, i.e. number of @{DCS#coalition.side}.
function WAREHOUSE:GetCountryName()
  return self.warehouse:GetCountryName()
end

--- Get airbase associated to the warehouse.
-- @param #WAREHOUSE self
-- @return Wrapper.Airbase#AIRBASE Airbase object or nil if warehouse has no airbase connection.
function WAREHOUSE:GetAirbase()
  return self.airbase
end

--- Get name airbase associated to the warehouse.
-- @param #WAREHOUSE self
-- @return #string name of the airbase assosicated to the warehouse or "none" if the airbase has not airbase connection currently.
function WAREHOUSE:GetAirbaseName()
  local name="none"
  if self.airbase then
    name=self.airbase:GetName()
  end
  return name
end

--- Get category of airbase associated to the warehouse.
-- @param #WAREHOUSE self
-- @return #number Category of airbase or -1 if warehouse has (currently) no airbase.
function WAREHOUSE:GetAirbaseCategory()
  local category=-1
  if self.airbase then
    category=self.airbase:GetAirbaseCategory()
  end
  return category
end

--- Get assignment of a request.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE.Pendingitem request The request from which the assignment is extracted.
-- @return #string The assignment text.
function WAREHOUSE:GetAssignment(request)
  return tostring(request.assignment)
end

--- Find a warehouse in the global warehouse data base.
-- @param #WAREHOUSE self
-- @param #number uid The unique ID of the warehouse.
-- @return #WAREHOUSE The warehouse object or nil if no warehouse exists.
function WAREHOUSE:FindWarehouseInDB(uid)
  return _WAREHOUSEDB.Warehouses[uid]
end

--- Find nearest warehouse in service, i.e. warehouses which are not started, stopped or destroyed are not considered.
-- Optionally, only warehouses with (specific) assets can be included in the search or warehouses of a certain coalition.
-- @param #WAREHOUSE self
-- @param MinAssets (Optional) Minimum number of assets the warehouse should have. Default 0.
-- @param #string Descriptor (Optional) Descriptor describing the selected assets which should be in stock. See @{#WAREHOUSE.Descriptor} for possible values.
-- @param DescriptorValue (Optional) Descriptor value selecting the type of assets which should be in stock.
-- @param DCS#Coalition.side Coalition (Optional) Coalition side of the warehouse. Default is the same coalition as the present warehouse. Set to false for any coalition.
-- @param Core.Point#COORDINATE RefCoordinate (Optional) Coordinate to which the closest warehouse is searched. Default is the warehouse calling this function.
-- @return #WAREHOUSE The the nearest warehouse object. Or nil if no warehouse is found.
-- @return #number The distance to the nearest warehouse in meters. Or nil if no warehouse is found.
function WAREHOUSE:FindNearestWarehouse(MinAssets, Descriptor, DescriptorValue, Coalition, RefCoordinate)

  -- Defaults
  if Descriptor~=nil and DescriptorValue~=nil then
    MinAssets=MinAssets or 1
  else
    MinAssets=MinAssets or 0
  end

  -- Coalition - default only the same as this warehouse.
  local anycoalition=nil
  if Coalition~=nil then
    if Coalition==false then
      anycoalition=true
    else
      -- Nothing to do
    end
  else
    if self~=nil then
      Coalition=self:GetCoalition()
    else
      anycoalition=true
    end
  end

  -- Coordinate of this warehouse or user specified reference.
  local coord=RefCoordinate or self:GetCoordinate()

  -- Loop over all warehouses.
  local nearest=nil
  local distmin=nil
  for wid,warehouse in pairs(_WAREHOUSEDB.Warehouses) do
    local warehouse=warehouse --#WAREHOUSE

    -- Distance from this warehouse to the other warehouse.
    local dist=coord:Get2DDistance(warehouse:GetCoordinate())

    if dist>0 then

      -- Check if coalition is right.
      local samecoalition=anycoalition or Coalition==warehouse:GetCoalition()

      -- Check that warehouse is in service.
      if samecoalition and not (warehouse:IsNotReadyYet() or warehouse:IsStopped() or warehouse:IsDestroyed()) then

        -- Get number of assets. Whole stock is returned if no descriptor/value is given.
        local nassets=warehouse:GetNumberOfAssets(Descriptor, DescriptorValue)

        --env.info(string.format("FF warehouse %s nassets = %d  for %s=%s", warehouse.alias, nassets, tostring(Descriptor), tostring(DescriptorValue)))

        -- Assume we have enough.
        local enough=true
        -- If specifc assets need to be present...
        if Descriptor and DescriptorValue then
          -- Check that enough assets (default 1) are available.
          enough = nassets>=MinAssets
        end

        -- Check distance.
        if enough and (distmin==nil or dist<distmin) then
          distmin=dist
          nearest=warehouse
        end
      end
    end
  end

  return nearest, distmin
end


--- Find an asset in the the global warehouse data base. Parameter is the MOOSE group object.
-- Note that the group name must contain they "AID" keyword.
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP group The group from which it is assumed that it has a registered asset.
-- @return #WAREHOUSE.Assetitem The asset from the data base or nil if it could not be found.
function WAREHOUSE:FindAssetInDB(group)

  -- Get unique ids from group name.
  local wid,aid,rid=self:_GetIDsFromGroup(group)

  if aid~=nil then

    local asset=_WAREHOUSEDB.Assets[aid]
    self:T2({asset=asset})
    if asset==nil then
      self:_ErrorMessage(string.format("ERROR: Asset for group %s not found in the data base!", group:GetName()), 0)
    end
    return asset
  end

  self:_ErrorMessage(string.format("ERROR: Group %s does not contain an asset ID in its name!", group:GetName()), 0)
  return nil
end

--- Check if runway is operational.
-- @param #WAREHOUSE self
-- @return #boolean If `true`, runway is operational.
function WAREHOUSE:IsRunwayOperational()
  if self.airbase then
    if self.runwaydestroyed then
      return false
    else
      return true
    end
  end
  return nil
end

--- Set the time until the runway(s) of an airdrome are repaired after it has been destroyed.
-- Note that this is the time, the DCS engine uses not something we can control on a user level or we could get via scripting.
-- You need to input the value. On the DCS forum it was stated that this is currently one hour. Hence this is the default value.
-- @param #WAREHOUSE self
-- @param #number RepairTime Time in seconds until the runway is repaired. Default 3600 sec (one hour).
-- @return #WAREHOUSE self
function WAREHOUSE:SetRunwayRepairtime(RepairTime)
  self.runwayrepairtime=RepairTime or 3600
  return self
end

--- Check if runway is operational.
-- @param #WAREHOUSE self
-- @return #number Time in seconds until the runway is repaired. Will return 0 if runway is repaired.
function WAREHOUSE:GetRunwayRepairtime()
  if self.runwaydestroyed then
    local Tnow=timer.getAbsTime()
    local Tsince=Tnow-self.runwaydestroyed
    local Trepair=math.max(self.runwayrepairtime-Tsince, 0)
    return Trepair
  end
  return 0
end

--- Check if warehouse physical representation is a unit (not a static) object.
-- @param #WAREHOUSE self
-- @return #boolean If `true`, warehouse object is a unit.
function WAREHOUSE:IsUnit()
  return self.isUnit
end

--- Check if warehouse physical representation is a static (not a unit) object.
-- @param #WAREHOUSE self
-- @return #boolean If `true`, warehouse object is a static.
function WAREHOUSE:IsStatic()
  return not self.isUnit
end

--- Check if warehouse physical representation is a ship.
-- @param #WAREHOUSE self
-- @return #boolean If `true`, warehouse object is a ship.
function WAREHOUSE:IsShip()
  return self.isShip
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM states
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after Start event. Starts the warehouse. Adds event handlers and schedules status updates of reqests and queue.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function WAREHOUSE:onafterStart(From, Event, To)

  -- Short info.
  local text=string.format("Starting warehouse %s alias %s:\n",self.warehouse:GetName(), self.alias)
  text=text..string.format("Coalition = %s\n", self:GetCoalitionName())
  text=text..string.format("Country  = %s\n", self:GetCountryName())
  text=text..string.format("Airbase  = %s (category=%d)\n", self:GetAirbaseName(), self:GetAirbaseCategory())
  env.info(text)

  -- Save self in static object. Easier to retrieve later.
  self.warehouse:SetState(self.warehouse, "WAREHOUSE", self)

  -- Get the closest point on road wrt spawnzone of ground assets.
  local _road=self.spawnzone:GetCoordinate():GetClosestPointToRoad()
  if _road and self.road==nil then
    -- Set connection to road if distance is less than 3 km.
    local _Droad=_road:Get2DDistance(self.spawnzone:GetCoordinate())
    if _Droad < 3000 then
      self.road=_road
    end
  end
  -- Mark point at road connection.
  if self.road and self.markerOn then
    self.markroad=self.road:MarkToCoalition(string.format("%s road connection.",self.alias), self:GetCoalition(), true)
  end

  -- Get the closest point on railroad wrt spawnzone of ground assets.
  local _rail=self.spawnzone:GetCoordinate():GetClosestPointToRoad(true)
  if _rail and self.rail==nil then
    -- Set rail conection if it is less than 3 km away.
    local _Drail=_rail:Get2DDistance(self.spawnzone:GetCoordinate())
    if _Drail < 3000 then
      self.rail=_rail
    end
  end
  -- Mark point at rail connection.
  if self.rail and self.markerOn then
    self.markrail=self.rail:MarkToCoalition(string.format("%s rail connection.", self.alias), self:GetCoalition(), true)
  end

  -- Handle events:
  self:HandleEvent(EVENTS.Birth,          self._OnEventBirth)
  self:HandleEvent(EVENTS.EngineStartup,  self._OnEventEngineStartup)
  self:HandleEvent(EVENTS.Takeoff,        self._OnEventTakeOff)
  self:HandleEvent(EVENTS.Land,           self._OnEventLanding)
  self:HandleEvent(EVENTS.EngineShutdown, self._OnEventEngineShutdown)
  self:HandleEvent(EVENTS.Crash,          self._OnEventCrashOrDead)
  self:HandleEvent(EVENTS.Dead,           self._OnEventCrashOrDead)
  self:HandleEvent(EVENTS.BaseCaptured,   self._OnEventBaseCaptured)
  self:HandleEvent(EVENTS.MissionEnd,     self._OnEventMissionEnd)

  -- This event triggers the arrived event for air assets.
  -- TODO Might need to make this landing or optional!
  -- In fact, it would be better if the type could be defined for only for the warehouse which receives stuff,
  -- since there will be warehouses with small airbases and little space or other problems!
  self:HandleEvent(EVENTS.EngineShutdown, self._OnEventArrived)

  -- Start the status monitoring.
  self:__Status(-1)
end

--- On after "Restart" event. Restarts the warehouse when it was in stopped state by reactivating the event handlers *only*.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function WAREHOUSE:onafterRestart(From, Event, To)

  self:I(self.lid..string.format("Restarting Warehouse %s.", self.alias))

  -- Handle events:
  self:HandleEvent(EVENTS.Birth,          self._OnEventBirth)
  self:HandleEvent(EVENTS.EngineStartup,  self._OnEventEngineStartup)
  self:HandleEvent(EVENTS.Takeoff,        self._OnEventTakeOff)
  self:HandleEvent(EVENTS.Land,           self._OnEventLanding)
  self:HandleEvent(EVENTS.EngineShutdown, self._OnEventEngineShutdown)
  self:HandleEvent(EVENTS.Crash,          self._OnEventCrashOrDead)
  self:HandleEvent(EVENTS.Dead,           self._OnEventCrashOrDead)
  self:HandleEvent(EVENTS.BaseCaptured,   self._OnEventBaseCaptured)

  -- This event triggers the arrived event for air assets.
  -- TODO Might need to make this landing or optional!
  -- In fact, it would be better if the type could be defined for only for the warehouse which receives stuff,
  -- since there will be warehouses with small airbases and little space or other problems!
  self:HandleEvent(EVENTS.EngineShutdown, self._OnEventArrived)

  -- Start the status monitoring.
  self:__Status(-1)

end

--- On after "Stop" event. Stops the warehouse, unhandles all events.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function WAREHOUSE:onafterStop(From, Event, To)
  self:_InfoMessage(string.format("Warehouse %s stopped!", self.alias))

  -- Unhandle event.
  self:UnHandleEvent(EVENTS.Birth)
  self:UnHandleEvent(EVENTS.EngineStartup)
  self:UnHandleEvent(EVENTS.Takeoff)
  self:UnHandleEvent(EVENTS.Land)
  self:UnHandleEvent(EVENTS.EngineShutdown)
  self:UnHandleEvent(EVENTS.Crash)
  self:UnHandleEvent(EVENTS.Dead)
  self:UnHandleEvent(EVENTS.BaseCaptured)

  self.pending=nil
  self.pending={}

  self.queue=nil
  self.queue={}

  self.stock=nil
  self.stock={}

  self:_UpdateWarehouseMarkText()

  -- Clear all pending schedules.
  self.CallScheduler:Clear()
end

--- On after "Pause" event. Pauses the warehouse, i.e. no requests are processed. However, new requests and new assets can be added in this state.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function WAREHOUSE:onafterPause(From, Event, To)
  self:I(self.lid..string.format("Warehouse %s paused! Queued requests are not processed in this state.", self.alias))
end

--- On after "Unpause" event. Unpauses the warehouse, i.e. requests in queue are processed again.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function WAREHOUSE:onafterUnpause(From, Event, To)
  self:I(self.lid..string.format("Warehouse %s unpaused! Processing of requests is resumed.", self.alias))
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after Status event. Checks the queue and handles requests.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function WAREHOUSE:onafterStatus(From, Event, To)

  -- General info.
  if self.verbosity>=1 then
  
    local FSMstate=self:GetState()
  
    local coalition=self:GetCoalitionName()
    local country=self:GetCountryName()
  
    -- Info.
    self:I(self.lid..string.format("State=%s %s [%s]: Assets=%d,  Requests: waiting=%d, pending=%d", FSMstate, country, coalition, #self.stock, #self.queue, #self.pending))
  end

  -- Check if any pending jobs are done and can be deleted from the queue.
  self:_JobDone()

  -- Print status.
  self:_DisplayStatus()

  -- Check if warehouse is being attacked or has even been captured.
  self:_CheckConquered()
  
  if self:IsRunwayOperational()==false then
    local Trepair=self:GetRunwayRepairtime()
    self:I(self.lid..string.format("Runway destroyed! Will be repaired in %d sec", Trepair))
    if Trepair==0 then
      self.runwaydestroyed = nil
      self:RunwayRepaired()
    end
  end

  -- Check if requests are valid and remove invalid one.
  self:_CheckRequestConsistancy(self.queue)

  -- If warehouse is running than requests can be processed.
  if self:IsRunning() or self:IsAttacked() then

    -- Check queue and handle requests if possible.
    local request=self:_CheckQueue()

    -- Execute the request. If the request is really executed, it is also deleted from the queue.
    if request then
      self:Request(request)
    end

  end

  -- Print queue after processing requests.
  if self.verbosity > 2 then
    self:_PrintQueue(self.queue, "Queue waiting")
    self:_PrintQueue(self.pending, "Queue pending")
  end
  -- Check fuel for all assets.
  --self:_CheckFuel()

  -- Update warhouse marker on F10 map.
  self:_UpdateWarehouseMarkText()

  -- Display complete list of stock itmes.
  if self.Debug then
    self:_DisplayStockItems(self.stock)
  end

  -- Call status again in ~30 sec (user choice).
  self:__Status(-self.dTstatus)
end


--- Function that checks if a pending job is done and can be removed from queue.
-- @param #WAREHOUSE self
function WAREHOUSE:_JobDone()

  -- For jobs that are done, i.e. all cargo and transport assets are delivered, home or dead!
  local done={}

  -- Loop over all pending requests of this warehouse.
  for _,request in pairs(self.pending) do
    local request=request --#WAREHOUSE.Pendingitem
    
    if request.born then

      -- Count number of cargo groups.
      local ncargo=0
      if request.cargogroupset then
        ncargo=request.cargogroupset:Count()
      end
  
      -- Count number of transport groups (if any).
      local ntransport=0
      if request.transportgroupset then
        ntransport=request.transportgroupset:Count()
      end
  
      local ncargotot=request.nasset
      local ncargodelivered=request.ndelivered
  
      -- Dead cargo: Ndead=Ntot-Ndeliverd-Nalive,
      local ncargodead=ncargotot-ncargodelivered-ncargo
  
  
      local ntransporttot=request.ntransport
      local ntransporthome=request.ntransporthome
  
      -- Dead transport: Ndead=Ntot-Nhome-Nalive.
      local ntransportdead=ntransporttot-ntransporthome-ntransport
  
      local text=string.format("Request id=%d: Cargo: Ntot=%d, Nalive=%d, Ndelivered=%d, Ndead=%d  |  Transport: Ntot=%d, Nalive=%d, Nhome=%d, Ndead=%d",
      request.uid, ncargotot, ncargo, ncargodelivered, ncargodead, ntransporttot, ntransport, ntransporthome, ntransportdead)
      self:T(self.lid..text)
  
  
      -- Handle different cases depending on what asset are still around.
      if ncargo==0 then
        ---------------------
        -- Cargo delivered --
        ---------------------
  
        -- Trigger delivered event.
        if not self.delivered[request.uid] then
          self:Delivered(request)
        end
  
        -- Check if transports are back home?
        if ntransport==0 then
          ---------------
          -- Job done! --
          ---------------
  
          -- Info on job.
          if self.verbosity>=1 then
            local text=string.format("Warehouse %s: Job on request id=%d for warehouse %s done!\n", self.alias, request.uid, request.warehouse.alias)
            text=text..string.format("- %d of %d assets delivered. Casualties %d.", ncargodelivered, ncargotot, ncargodead)
            if request.ntransport>0 then
              text=text..string.format("\n- %d of %d transports returned home. Casualties %d.", ntransporthome, ntransporttot, ntransportdead)
            end
            self:_InfoMessage(text, 20)
          end
  
          -- Mark request for deletion.
          table.insert(done, request)
  
        else
          -----------------------------------
          -- No cargo but still transports --
          -----------------------------------
  
          -- This is difficult! How do I know if transports were unused? They could also be just on their way back home.
          -- ==> Need to do a lot of checks.
  
          -- All transports are dead but there is still cargo left ==> Put cargo back into stock.
          for _,_group in pairs(request.transportgroupset:GetSetObjects()) do
            local group=_group --Wrapper.Group#GROUP
  
            -- Check if group is alive.
            if group and group:IsAlive() then
  
              -- Check if group is in the spawn zone?
              local category=group:GetCategory()
  
              -- Get current speed.
              local speed=group:GetVelocityKMH()
              local notmoving=speed<1
  
              -- Closest airbase.
              local airbase=group:GetCoordinate():GetClosestAirbase():GetName()
              local athomebase=self.airbase and self.airbase:GetName()==airbase
  
              -- On ground
              local onground=not group:InAir()
  
              -- In spawn zone.
              local inspawnzone=group:IsPartlyOrCompletelyInZone(self.spawnzone)
  
              -- Check conditions for being back home.
              local ishome=false
              if category==Group.Category.GROUND or category==Group.Category.HELICOPTER then
                -- Units go back to the spawn zone, helicopters land and they should not move any more.
                ishome=inspawnzone and onground and notmoving
              elseif category==Group.Category.AIRPLANE then
                -- Planes need to be on ground at their home airbase and should not move any more.
                ishome=athomebase and onground and notmoving
              end
  
              -- Debug text.
              local text=string.format("Group %s: speed=%d km/h, onground=%s , airbase=%s, spawnzone=%s ==> ishome=%s", group:GetName(), speed, tostring(onground), airbase, tostring(inspawnzone), tostring(ishome))
              self:T(self.lid..text)
  
              if ishome then
  
                -- Info message.
                local text=string.format("Warehouse %s: Transport group arrived back home and no cargo left for request id=%d.\nSending transport group %s back to stock.", self.alias, request.uid, group:GetName())
                self:T(self.lid..text)
  
                -- Debug smoke.
                if self.Debug then
                  group:SmokeRed()
                end
  
                -- Group arrived.
                self:Arrived(group)
              end
            end
          end
  
        end
  
      else
  
        if ntransport==0 and request.ntransport>0 then
          -----------------------------------
          -- Still cargo but no transports --
          -----------------------------------
  
          local ncargoalive=0
  
          -- All transports are dead but there is still cargo left ==> Put cargo back into stock.
          for _,_group in pairs(request.cargogroupset:GetSetObjects()) do
            --local group=group --Wrapper.Group#GROUP
  
            -- These groups have been respawned as cargo, i.e. their name changed!
            local groupname=_group:GetName()
            local group=GROUP:FindByName(groupname.."#CARGO")
  
            -- Check if group is alive.
            if group and group:IsAlive() then
  
              -- Check if group is in spawn zone?
              if group:IsPartlyOrCompletelyInZone(self.spawnzone) then
                -- Debug smoke.
                if self.Debug then
                  group:SmokeBlue()
                end
                -- Add asset group back to stock.
                self:AddAsset(group)
                ncargoalive=ncargoalive+1
              end
            end
  
          end
  
          -- Info message.
          self:_InfoMessage(string.format("Warehouse %s: All transports of request id=%s dead! Putting remaining %s cargo assets back into warehouse!", self.alias, request.uid, ncargoalive))
        end
  
      end
    end -- born check
  end -- loop over requests

  -- Remove pending requests if done.
  for _,request in pairs(done) do
    self:_DeleteQueueItem(request, self.pending)
  end
end

--- Function that checks if an asset group is still okay.
-- @param #WAREHOUSE self
function WAREHOUSE:_CheckAssetStatus()

  -- Check if a unit of the group has problems.
  local function _CheckGroup(_request, _group)
    local request=_request --#WAREHOUSE.Pendingitem
    local group=_group     --Wrapper.Group#GROUP

    if group and group:IsAlive() then

      -- Category of group.
      local category=group:GetCategory()

      for _,_unit in pairs(group:GetUnits()) do
        local unit=_unit --Wrapper.Unit#UNIT

        if unit and unit:IsAlive() then
          local unitid=unit:GetID()
          local life9=unit:GetLife()
          local life0=unit:GetLife0()
          local life=life9/life0*100
          local speed=unit:GetVelocityMPS()
          local onground=unit:InAir()

          local problem=false
          if life<10 then
            self:T(string.format("Unit %s is heavily damaged!", unit:GetName()))
          end
          if speed<1 and unit:GetSpeedMax()>1 and onground then
            self:T(string.format("Unit %s is not moving!", unit:GetName()))
            problem=true
          end

          if problem then
            if request.assetproblem[unitid] then
              local deltaT=timer.getAbsTime()-request.assetproblem[unitid]
              if deltaT>300 then
                --Todo: which event to generate? Removeunit or Dead/Creash or both?
                unit:Destroy()
              end
            else
              request.assetproblem[unitid]=timer.getAbsTime()
            end
          end
        end

      end
    end
  end


  for _,request in pairs(self.pending) do
    local request=request --#WAREHOUSE.Pendingitem

    -- Cargo groups.
    if request.cargogroupset then
      for _,_group in pairs(request.cargogroupset:GetSet()) do
        local group=_group --Wrapper.Group#GROUP

        _CheckGroup(request, group)

      end
    end

    -- Transport groups.
    if request.transportgroupset then
      for _,group in pairs(request.transportgroupset:GetSet()) do

        _CheckGroup(request, group)
      end
    end

  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "AddAsset" event. Add a group to the warehouse stock. If the group is alive, it is destroyed.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Group#GROUP group Group or template group to be added to the warehouse stock.
-- @param #number ngroups Number of groups to add to the warehouse stock. Default is 1.
-- @param #WAREHOUSE.Attribute forceattribute (Optional) Explicitly force a generalized attribute for the asset. This has to be an @{#WAREHOUSE.Attribute}.
-- @param #number forcecargobay (Optional) Explicitly force cargobay weight limit in kg for cargo carriers. This is for each *unit* of the group.
-- @param #number forceweight (Optional) Explicitly force weight in kg of each unit in the group.
-- @param #number loadradius (Optional) Radius in meters when the cargo is loaded into the carrier.
-- @param DCS#AI.Skill skill Skill of the asset.
-- @param #table liveries Table of livery names. When the asset is spawned one livery is chosen randomly.
-- @param #string assignment A free to choose string specifying an assignment for the asset. This can be used with the @{#WAREHOUSE.OnAfterNewAsset} function.
-- @param #table other (Optional) Table of other useful data. Can be collected via WAREHOUSE.OnAfterNewAsset() function for example
function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, skill, liveries, assignment, other)
  self:T({group=group, ngroups=ngroups, forceattribute=forceattribute, forcecargobay=forcecargobay, forceweight=forceweight})

  -- Set default.
  local n=ngroups or 1

  -- Handle case where just a string is passed.
  if type(group)=="string" then
    group=GROUP:FindByName(group)
  end

  if liveries and type(liveries)=="string" then
    liveries={liveries}
  end

  if group then

    -- Try to get UIDs from group name. Is this group a known or a new asset?
    local wid,aid,rid=self:_GetIDsFromGroup(group)

    if wid and aid and rid then

      ---------------------------
      -- This is a KNOWN asset --
      ---------------------------

      -- Get the original warehouse this group belonged to.
      local warehouse=self:FindWarehouseInDB(wid)

      if warehouse then

        local request=warehouse:_GetRequestOfGroup(group, warehouse.pending)

        if request then

          -- Increase number of cargo delivered and transports home.
          local istransport=warehouse:_GroupIsTransport(group,request)

          if istransport==true then
            request.ntransporthome=request.ntransporthome+1
            request.transportgroupset:Remove(group:GetName(), true)
            local ntrans=request.transportgroupset:Count()
            self:T2(warehouse.lid..string.format("Transport %d of %s returned home. TransportSet=%d", request.ntransporthome, tostring(request.ntransport), ntrans))
          elseif istransport==false then
            request.ndelivered=request.ndelivered+1
            local namewo=self:_GetNameWithOut(group)
            request.cargogroupset:Remove(namewo, true)
            local ncargo=request.cargogroupset:Count()
            self:T2(warehouse.lid..string.format("Cargo %s: %d of %s delivered. CargoSet=%d", namewo, request.ndelivered, tostring(request.nasset), ncargo))
          else
            self:E(warehouse.lid..string.format("WARNING: Group %s is neither cargo nor transport! Need to investigate...", group:GetName()))
          end

          -- If no assignment was given we take the assignment of the request if there is any.
          if assignment==nil and request.assignment~=nil then
            assignment=request.assignment
          end

        end
      end

      -- Get the asset from the global DB.
      local asset=self:FindAssetInDB(group)

      -- Note the group is only added once, i.e. the ngroups parameter is ignored here.
      -- This is because usually these request comes from an asset that has been transfered from another warehouse and hence should only be added once.
      if asset~=nil then
        self:_DebugMessage(string.format("Warehouse %s: Adding KNOWN asset uid=%d with attribute=%s to stock.", self.alias, asset.uid, asset.attribute), 5)

        -- Set livery.
        if liveries then
          if type(liveries)=="table" then
            asset.livery=liveries[math.random(#liveries)]
          else
            asset.livery=liveries
          end
        end

        -- Set skill.
        asset.skill=skill or asset.skill

        -- Asset now belongs to this warehouse. Set warehouse ID.
        asset.wid=self.uid

        -- No request associated with this asset.
        asset.rid=nil

        -- Asset is not spawned.
        asset.spawned=false
        asset.requested=false
        asset.isReserved=false
        asset.iscargo=nil
        asset.arrived=nil
        
        -- Destroy group if it is alive.
        if group:IsAlive()==true then
          asset.damage=asset.life0-group:GetLife()
        end

        -- Add asset to stock.
        table.insert(self.stock, asset)

        -- Trigger New asset event.
        self:__NewAsset(0.1, asset, assignment or "")
      else
        self:_ErrorMessage(string.format("ERROR: Known asset could not be found in global warehouse db!"), 0)
      end

    else

      -------------------------
      -- This is a NEW asset --
      -------------------------

      -- Debug info.
      self:_DebugMessage(string.format("Warehouse %s: Adding %d NEW assets of group %s to stock", self.alias, n, tostring(group:GetName())), 5)

      -- This is a group that is not in the db yet. Add it n times.
      local assets=self:_RegisterAsset(group, n, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill, assignment)

      -- Add created assets to stock of this warehouse.
      for _,asset in pairs(assets) do

        -- Asset belongs to this warehouse. Set warehouse ID.
        asset.wid=self.uid

        -- No request associated with this asset.
        asset.rid=nil

        -- Add asset to stock.
        table.insert(self.stock, asset)

        -- Trigger NewAsset event. Delay a bit for OnAfterNewAsset functions to work properly.
        self:__NewAsset(0.1, asset, assignment or "")
      end

    end

    -- Destroy group if it is alive.
    if group:IsAlive()==true then
      self:_DebugMessage(string.format("Removing group %s", group:GetName()), 5)

      local opsgroup=_DATABASE:GetOpsGroup(group:GetName())
      if opsgroup then      
        opsgroup:Despawn(0, true)
        opsgroup:__Stop(-0.01)
      else
        -- Setting parameter to false, i.e. creating NO dead or remove unit event, seems to not confuse the dispatcher logic.
        -- TODO: It would be nice, however, to have the remove event.      
        group:Destroy() --(false)
      end
    else
      local opsgroup=_DATABASE:GetOpsGroup(group:GetName())
      if opsgroup then
        opsgroup:Stop()
      end
    end

  else
    self:E(self.lid.."ERROR: Unknown group added as asset!")
    self:E({unknowngroup=group})
  end

end

--- Register new asset in globase warehouse data base.
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP group The group that will be added to the warehouse stock.
-- @param #number ngroups Number of groups to be added.
-- @param #string forceattribute Forced generalized attribute.
-- @param #number forcecargobay Cargo bay weight limit in kg.
-- @param #number forceweight Weight of units in kg.
-- @param #number loadradius Radius in meters when cargo is loaded into the carrier.
-- @param #table liveries Table of liveries.
-- @param DCS#AI.Skill skill Skill of AI.
-- @param #string assignment Assignment attached to the asset item.
-- @return #table A table containing all registered assets.
function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill, assignment)
  self:F({groupname=group:GetName(), ngroups=ngroups, forceattribute=forceattribute, forcecargobay=forcecargobay, forceweight=forceweight})

  -- Set default.
  local n=ngroups or 1

  -- Get the size of an object.
  local function _GetObjectSize(DCSdesc)
    if DCSdesc.box then
      local x=DCSdesc.box.max.x-DCSdesc.box.min.x  --length
      local y=DCSdesc.box.max.y-DCSdesc.box.min.y  --height
      local z=DCSdesc.box.max.z-DCSdesc.box.min.z  --width
      return math.max(x,z), x , y, z
    end
    return 0,0,0,0
  end

  -- Get name of template group.
  local templategroupname=group:GetName()

  local Descriptors=group:GetUnit(1):GetDesc()
  local Category=group:GetCategory()
  local TypeName=group:GetTypeName()
  local SpeedMax=group:GetSpeedMax()
  local RangeMin=group:GetRange()
  local smax,sx,sy,sz=_GetObjectSize(Descriptors)

  --self:E(Descriptors)

  -- Get weight and cargo bay size in kg.
  local weight=0
  local cargobay={}
  local cargobaytot=0
  local cargobaymax=0
  local weights={}
  for _i,_unit in pairs(group:GetUnits()) do
    local unit=_unit --Wrapper.Unit#UNIT
    local Desc=unit:GetDesc()

    -- Weight. We sum up all units in the group.
    local unitweight=forceweight or Desc.massEmpty
    if unitweight then
      weight=weight+unitweight
      weights[_i]=unitweight
    end
    
    local cargomax=0
    local massfuel=Desc.fuelMassMax or 0
    local massempty=Desc.massEmpty or 0
    local massmax=Desc.massMax or 0

    -- Calcuate cargo bay limit value.
    cargomax=massmax-massfuel-massempty
    self:T3(self.lid..string.format("Unit name=%s: mass empty=%.1f kg, fuel=%.1f kg, max=%.1f kg ==> cargo=%.1f kg", unit:GetName(), unitweight, massfuel, massmax, cargomax))

    -- Cargo bay size.
    local bay=forcecargobay or unit:GetCargoBayFreeWeight()

    -- Add bay size to table.
    table.insert(cargobay, bay)

    -- Sum up total bay size.
    cargobaytot=cargobaytot+bay

    -- Get max bay size.
    if bay>cargobaymax then
      cargobaymax=bay
    end
  end

  -- Set/get the generalized attribute.
  local attribute=forceattribute or self:_GetAttribute(group)

  -- Table for returned assets.
  local assets={}

  -- Add this n times to the table.
  for i=1,n do
    local asset={} --#WAREHOUSE.Assetitem

    -- Increase asset unique id counter.
    _WAREHOUSEDB.AssetID=_WAREHOUSEDB.AssetID+1

    -- Set parameters.
    asset.uid=_WAREHOUSEDB.AssetID
    asset.templatename=templategroupname
    asset.template=UTILS.DeepCopy(_DATABASE.Templates.Groups[templategroupname].Template)
    asset.category=Category
    asset.unittype=TypeName
    asset.nunits=#asset.template.units
    asset.range=RangeMin
    asset.speedmax=SpeedMax
    asset.size=smax
    asset.weight=weight
    asset.weights=weights
    asset.DCSdesc=Descriptors
    asset.attribute=attribute
    asset.cargobay=cargobay
    asset.cargobaytot=cargobaytot
    asset.cargobaymax=cargobaymax
    asset.loadradius=loadradius
    if liveries then
      asset.livery=liveries[math.random(#liveries)]
    end
    asset.skill=skill
    asset.assignment=assignment
    asset.spawned=false
    asset.requested=false
    asset.isReserved=false
    asset.life0=group:GetLife0()
    asset.damage=0
    asset.spawngroupname=string.format("%s_AID-%d", templategroupname, asset.uid)

    if i==1 then
      self:_AssetItemInfo(asset)
    end

    -- Add asset to global db.
    _WAREHOUSEDB.Assets[asset.uid]=asset

    -- Add asset to the table that is retured.
    table.insert(assets,asset)
  end

  return assets
end

--- Asset item characteristics.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE.Assetitem asset The asset for which info in printed in trace mode.
function WAREHOUSE:_AssetItemInfo(asset)
  -- Info about asset:
  local text=string.format("\nNew asset with id=%d for warehouse %s:\n", asset.uid, self.alias)
  text=text..string.format("Spawngroup name= %s\n", asset.spawngroupname)
  text=text..string.format("Template name  = %s\n", asset.templatename)
  text=text..string.format("Unit type      = %s\n", asset.unittype)
  text=text..string.format("Attribute      = %s\n", asset.attribute)
  text=text..string.format("Category       = %d\n", asset.category)
  text=text..string.format("Units #        = %d\n", asset.nunits)
  text=text..string.format("Speed max      = %5.2f km/h\n", asset.speedmax)
  text=text..string.format("Range max      = %5.2f km\n", asset.range/1000)
  text=text..string.format("Size  max      = %5.2f m\n", asset.size)
  text=text..string.format("Weight total   = %5.2f kg\n", asset.weight)
  text=text..string.format("Cargo bay tot  = %5.2f kg\n", asset.cargobaytot)
  text=text..string.format("Cargo bay max  = %5.2f kg\n", asset.cargobaymax)
  text=text..string.format("Load radius    = %s m\n", tostring(asset.loadradius))
  text=text..string.format("Skill          = %s\n", tostring(asset.skill))
  text=text..string.format("Livery         = %s", tostring(asset.livery))  
  self:I(self.lid..text)
  self:T({DCSdesc=asset.DCSdesc})
  self:T3({Template=asset.template})
end

--- This function uses Disposition and other fallback logic to find better ground positions for ground units.
--- NOTE: This is not a spawn randomizer.
--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table.
--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit.
--- Uses UTILS.ValidateAndRepositionGroundUnits.
-- @param #boolean Enabled Enable/disable the feature.
function WAREHOUSE:SetValidateAndRepositionGroundUnits(Enabled)
    self.ValidateAndRepositionGroundUnits = Enabled
end

--- On after "NewAsset" event. A new asset has been added to the warehouse stock.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #WAREHOUSE.Assetitem asset The asset that has just been added.
-- @param #string assignment The (optional) assignment for the asset.
function WAREHOUSE:onafterNewAsset(From, Event, To, asset, assignment)
  self:T(self.lid..string.format("New asset %s id=%d with assignment %s.", tostring(asset.templatename), asset.uid, tostring(assignment)))
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On before "AddRequest" event. Checks some basic properties of the given parameters.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #WAREHOUSE warehouse The warehouse requesting supply.
-- @param #WAREHOUSE.Descriptor AssetDescriptor Descriptor describing the asset that is requested.
-- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc.
-- @param #number nAsset Number of groups requested that match the asset specification.
-- @param #WAREHOUSE.TransportType TransportType Type of transport.
-- @param #number nTransport Number of transport units requested.
-- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low.
-- @param #string Assignment A keyword or text that later be used to identify this request and postprocess the assets.
-- @return #boolean If true, request is okay at first glance.
function WAREHOUSE:onbeforeAddRequest(From, Event, To, warehouse, AssetDescriptor, AssetDescriptorValue, nAsset, TransportType, nTransport, Assignment, Prio)

  -- Request is okay.
  local okay=true

  if AssetDescriptor==WAREHOUSE.Descriptor.ATTRIBUTE then

    -- Check if a valid attibute was given.
    local gotit=false
    for _,attribute in pairs(WAREHOUSE.Attribute) do
      if AssetDescriptorValue==attribute then
        gotit=true
      end
    end
    if not gotit then
      self:_ErrorMessage("ERROR: Invalid request. Asset attribute is unknown!", 5)
      okay=false
    end

  elseif AssetDescriptor==WAREHOUSE.Descriptor.CATEGORY then

    -- Check if a valid category was given.
    local gotit=false
    for _,category in pairs(Group.Category) do
      if AssetDescriptorValue==category then
        gotit=true
      end
    end
    if not gotit then
      self:_ErrorMessage("ERROR: Invalid request. Asset category is unknown!", 5)
      okay=false
    end

  elseif AssetDescriptor==WAREHOUSE.Descriptor.GROUPNAME then

    if type(AssetDescriptorValue)~="string" then
      self:_ErrorMessage("ERROR: Invalid request. Asset template name must be passed as a string!", 5)
      okay=false
    end

  elseif AssetDescriptor==WAREHOUSE.Descriptor.UNITTYPE then

    if type(AssetDescriptorValue)~="string" then
      self:_ErrorMessage("ERROR: Invalid request. Asset unit type must be passed as a string!", 5)
      okay=false
    end

  elseif AssetDescriptor==WAREHOUSE.Descriptor.ASSIGNMENT then

    if type(AssetDescriptorValue)~="string" then
      self:_ErrorMessage("ERROR: Invalid request. Asset assignment type must be passed as a string!", 5)
      okay=false
    end

  elseif AssetDescriptor==WAREHOUSE.Descriptor.ASSETLIST then

    if type(AssetDescriptorValue)~="table" then
      self:_ErrorMessage("ERROR: Invalid request. Asset assignment type must be passed as a table!", 5)
      okay=false
    end

  else
    self:_ErrorMessage("ERROR: Invalid request. Asset descriptor is not ATTRIBUTE, CATEGORY, GROUPNAME, UNITTYPE or ASSIGNMENT!", 5)
    okay=false
  end

  -- Warehouse is stopped?
  if self:IsStopped() then
    self:_ErrorMessage("ERROR: Invalid request. Warehouse is stopped!", 0)
    okay=false
  end

  -- Warehouse is destroyed?
  if self:IsDestroyed() and not self.respawnafterdestroyed then
    self:_ErrorMessage("ERROR: Invalid request. Warehouse is destroyed!", 0)
    okay=false
  end

  return okay
end

--- On after "AddRequest" event. Add a request to the warehouse queue, which is processed when possible.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #WAREHOUSE warehouse The warehouse requesting supply.
-- @param #WAREHOUSE.Descriptor AssetDescriptor Descriptor describing the asset that is requested.
-- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc.
-- @param #number nAsset Number of groups requested that match the asset specification.
-- @param #WAREHOUSE.TransportType TransportType Type of transport.
-- @param #number nTransport Number of transport units requested.
-- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low.
-- @param #string Assignment A keyword or text that can later be used to identify this request and postprocess the assets.
function WAREHOUSE:onafterAddRequest(From, Event, To, warehouse, AssetDescriptor, AssetDescriptorValue, nAsset, TransportType, nTransport, Prio, Assignment)

  -- Defaults.
  nAsset=nAsset or 1
  TransportType=TransportType or WAREHOUSE.TransportType.SELFPROPELLED
  Prio=Prio or 50
  if nTransport==nil then
    if TransportType==WAREHOUSE.TransportType.SELFPROPELLED then
      nTransport=0
    else
      nTransport=1
    end
  end

  -- Self request?
  local toself=false
  if self.warehouse:GetName()==warehouse.warehouse:GetName() then
    toself=true
  end

  -- Increase id.
  self.queueid=self.queueid+1

  -- Request queue table item.
  local request={
  uid=self.queueid,
  prio=Prio,
  warehouse=warehouse,
  assetdesc=AssetDescriptor,
  assetdescval=AssetDescriptorValue,
  nasset=nAsset,
  transporttype=TransportType,
  ntransport=nTransport,
  assignment=tostring(Assignment),
  airbase=warehouse:GetAirbase(),
  category=warehouse:GetAirbaseCategory(),
  ndelivered=0,
  ntransporthome=0,
  assets={},
  toself=toself,
  } --#WAREHOUSE.Queueitem

  -- Add request to queue.
  table.insert(self.queue, request)
  
  local descval="assetlist"
  if request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST then
  
  else
    descval=tostring(request.assetdescval)
  end

  local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports=%s.",
  self.alias, warehouse.alias, request.assetdesc, descval, tostring(request.nasset), request.transporttype, tostring(request.ntransport))
  self:_DebugMessage(text, 5)

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On before "Request" event. Checks if the request can be fulfilled.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #WAREHOUSE.Queueitem Request Information table of the request.
-- @return #boolean If true, request is granted.
function WAREHOUSE:onbeforeRequest(From, Event, To, Request)
  self:T3({warehouse=self.alias, request=Request})

  -- Distance from warehouse to requesting warehouse.
  local distance=self:GetCoordinate():Get2DDistance(Request.warehouse:GetCoordinate())

  -- Shortcut to cargoassets.
  local _assets=Request.cargoassets

  if Request.nasset==0 then
    local text=string.format("Warehouse %s: Request denied! Zero assets were requested.", self.alias)
    self:_InfoMessage(text, 10)
    return false
  end

  -- Check if destination is in range for all requested assets.
  for _,_asset in pairs(_assets) do
    local asset=_asset --#WAREHOUSE.Assetitem

    -- Check if destination is in range.
    if asset.range<distance then
      local text=string.format("Request denied! Destination %s is out of range for asset %s.", Request.airbase:GetName(), asset.templatename)
      self:_InfoMessage(text, 10)

      -- Delete request from queue because it will never be possible.
      -- Unless(!) at least one is a moving warehouse, which could, e.g., be an aircraft carrier.
      if not (self.isUnit or Request.warehouse.isUnit) then
        self:_DeleteQueueItem(Request, self.queue)
      end

      return false
    end

  end

  return true
end


--- On after "Request" event. Spawns the necessary cargo and transport assets.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #WAREHOUSE.Queueitem Request Information table of the request.
function WAREHOUSE:onafterRequest(From, Event, To, Request)

  -- Info message.
  if self.verbosity>=1 then
    local text=string.format("Warehouse %s: Processing request id=%d from warehouse %s.\n", self.alias, Request.uid, Request.warehouse.alias)
    text=text..string.format("Requested %s assets of %s=%s.\n", tostring(Request.nasset), Request.assetdesc, Request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST and "Asset list" or Request.assetdescval)
    text=text..string.format("Transports %s of type %s.", tostring(Request.ntransport), tostring(Request.transporttype))
    self:_InfoMessage(text, 5)
  end

  ------------------------------------------------------------------------------------------------------------------------------------
  -- Cargo assets.
  ------------------------------------------------------------------------------------------------------------------------------------

  -- Set time stamp.
  Request.timestamp=timer.getAbsTime()

  -- Spawn assets of this request.
  self:_SpawnAssetRequest(Request)

  ------------------------------------------------------------------------------------------------------------------------------------
  -- Transport assets
  ------------------------------------------------------------------------------------------------------------------------------------

  -- Shortcut to transport assets.
  local _assetstock=Request.transportassets

  -- Now we try to find all parking spots for all cargo groups in advance. Due to the for loop, the parking spots do not get updated while spawning.
  local Parking={}
  if Request.transportcategory==Group.Category.AIRPLANE or Request.transportcategory==Group.Category.HELICOPTER then
    Parking=self:_FindParkingForAssets(self.airbase,_assetstock)
  end

  -- Transport assets table.
  local _transportassets={}

  ----------------------------
  -- Spawn Transport Groups --
  ----------------------------

  -- Spawn the transport groups.
  for i=1,Request.ntransport do

    -- Get stock item.
    local _assetitem=_assetstock[i] --#WAREHOUSE.Assetitem

    -- Spawn group name
    local _alias=_assetitem.spawngroupname

    -- Set Request ID.
    _assetitem.rid=Request.uid

    -- Asset is transport.
    _assetitem.spawned=false
    _assetitem.iscargo=false
    _assetitem.arrived=false

    local spawngroup=nil --Wrapper.Group#GROUP

    -- Add asset by id to all assets table.
    Request.assets[_assetitem.uid]=_assetitem

    -- Spawn assets depending on type.
    if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE then

      -- Spawn plane at airport in uncontrolled state. Will get activated when cargo is loaded.
      spawngroup=self:_SpawnAssetAircraft(_alias,_assetitem, Request, Parking[_assetitem.uid], true)

    elseif Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then

      -- Spawn helos at airport in controlled state. They need to fly to the spawn zone.
      spawngroup=self:_SpawnAssetAircraft(_alias,_assetitem, Request, Parking[_assetitem.uid], false)

    elseif Request.transporttype==WAREHOUSE.TransportType.APC then

      -- Spawn APCs in spawn zone.
      spawngroup=self:_SpawnAssetGroundNaval(_alias, _assetitem, Request, self.spawnzone)

    elseif Request.transporttype==WAREHOUSE.TransportType.TRAIN then

      self:_ErrorMessage("ERROR: Cargo transport by train not supported yet!")
      return

    elseif Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.NAVALCARRIER
      or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP then

      -- Spawn Ship in port zone
      spawngroup=self:_SpawnAssetGroundNaval(_alias, _assetitem, Request, self.portzone)

    elseif Request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then

      self:_ErrorMessage("ERROR: Transport type selfpropelled was already handled above. We should not get here!")
      return

    else
      self:_ErrorMessage("ERROR: Unknown transport type!")
      return
    end

    -- Trigger event.
    if spawngroup then
      self:__AssetSpawned(0.01, spawngroup, _assetitem, Request)
    end

  end

  -- Init problem table.
  Request.assetproblem={}

  -- Add request to pending queue.
  table.insert(self.pending, Request)

  -- Delete request from queue.
  self:_DeleteQueueItem(Request, self.queue)

end

--- On after "RequestSpawned" event. Initiates the transport of the assets to the requesting warehouse.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #WAREHOUSE.Pendingitem Request Information table of the request.
-- @param Core.Set#SET_GROUP CargoGroupSet Set of cargo groups.
-- @param Core.Set#SET_GROUP TransportGroupSet Set of transport groups if any.
function WAREHOUSE:onafterRequestSpawned(From, Event, To, Request, CargoGroupSet, TransportGroupSet)

  -- General type and category.
  local _cargotype=Request.cargoattribute    --#WAREHOUSE.Attribute
  local _cargocategory=Request.cargocategory --DCS#Group.Category

  -- Add groups to pending item.
  --Request.cargogroupset=CargoGroupSet

  ------------------------------------------------------------------------------------------------------------------------------------
  -- Self request: assets are spawned at warehouse but not transported anywhere.
  ------------------------------------------------------------------------------------------------------------------------------------

  -- Self request! Assets are only spawned but not routed or transported anywhere.
  if Request.toself then
    self:_DebugMessage(string.format("Selfrequest! Current status %s", self:GetState()))

    -- Start self request.
    self:__SelfRequest(1, CargoGroupSet, Request)

    return
  end

  ------------------------------------------------------------------------------------------------------------------------------------
  -- Self propelled: assets go to the requesting warehouse by themselfs.
  ------------------------------------------------------------------------------------------------------------------------------------

  -- No transport unit requested. Assets go by themselfes.
  if Request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then
    self:T2(self.lid..string.format("Got selfpropelled request for %d assets.", CargoGroupSet:Count()))

    for _,_group in pairs(CargoGroupSet:GetSetObjects()) do
      local group=_group --Wrapper.Group#GROUP

      -- Route cargo to their destination.
      if _cargocategory==Group.Category.GROUND then
        self:T2(self.lid..string.format("Route ground group %s.", group:GetName()))

        -- Random place in the spawn zone of the requesting warehouse.
        local ToCoordinate=Request.warehouse.spawnzone:GetRandomCoordinate()

        -- Debug marker.
        if self.Debug then
          ToCoordinate:MarkToAll(string.format("Destination of group %s", group:GetName()))
        end

        -- Route ground.
        self:_RouteGround(group, Request)

      elseif _cargocategory==Group.Category.AIRPLANE or _cargocategory==Group.Category.HELICOPTER then
        self:T2(self.lid..string.format("Route airborne group %s.", group:GetName()))

        -- Route plane to the requesting warehouses airbase.
        -- Actually, the route is already set. We only need to activate the uncontrolled group.
        self:_RouteAir(group)

      elseif _cargocategory==Group.Category.SHIP then
        self:T2(self.lid..string.format("Route naval group %s.", group:GetName()))

        -- Route plane to the requesting warehouses airbase.
        self:_RouteNaval(group, Request)

      elseif _cargocategory==Group.Category.TRAIN then
        self:T2(self.lid..string.format("Route train group %s.", group:GetName()))

        -- Route train to the rail connection of the requesting warehouse.
        self:_RouteTrain(group, Request.warehouse.rail)

      else
        self:E(self.lid..string.format("ERROR: unknown category %s for self propelled cargo %s!", tostring(_cargocategory), tostring(group:GetName())))
      end

    end

    -- Transport group set.
    Request.transportgroupset=TransportGroupSet

    -- No cargo transport necessary.
    return
  end

  ------------------------------------------------------------------------------------------------------------------------------------
  -- Prepare cargo groups for transport
  ------------------------------------------------------------------------------------------------------------------------------------

  -- Board radius, i.e. when the cargo will begin to board the carrier
  local _boardradius=500

  if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE then
    _boardradius=5000
  elseif Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then
    --_loadradius=1000
    --_boardradius=nil
  elseif Request.transporttype==WAREHOUSE.TransportType.APC then
    --_boardradius=nil
  elseif Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER 
      or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP then
    _boardradius=6000
  end

  -- Empty cargo group set.
  local CargoGroups=SET_CARGO:New()

  -- Add cargo groups to set.
  for _,_group in pairs(CargoGroupSet:GetSetObjects()) do

    -- Find asset belonging to this group.
    local asset=self:FindAssetInDB(_group)
    -- New cargo group object.
    local cargogroup=CARGO_GROUP:New(_group, _cargotype,_group:GetName(),_boardradius, asset.loadradius)

    -- Set weight for this group.
    cargogroup:SetWeight(asset.weight)

    -- Add group to group set.
    CargoGroups:AddCargo(cargogroup)

  end

  ------------------------
  -- Create Dispatchers --
  ------------------------

  -- Cargo dispatcher.
  local CargoTransport --AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER

  if Request.transporttype==WAREHOUSE.TransportType.AIRPLANE then

    -- Pickup and deploy zones.
    local PickupAirbaseSet = SET_ZONE:New():AddZone(ZONE_AIRBASE:New(self.airbase:GetName()))
    local DeployAirbaseSet = SET_ZONE:New():AddZone(ZONE_AIRBASE:New(Request.airbase:GetName()))

    -- Define dispatcher for this task.
    CargoTransport = AI_CARGO_DISPATCHER_AIRPLANE:New(TransportGroupSet, CargoGroups, PickupAirbaseSet, DeployAirbaseSet)

    -- Set home zone.
    CargoTransport:SetHomeZone(ZONE_AIRBASE:New(self.airbase:GetName()))

  elseif Request.transporttype==WAREHOUSE.TransportType.HELICOPTER then

    -- Pickup and deploy zones.
    local PickupZoneSet = SET_ZONE:New():AddZone(self.spawnzone)
    local DeployZoneSet = SET_ZONE:New():AddZone(Request.warehouse.spawnzone)

    -- Define dispatcher for this task.
    CargoTransport = AI_CARGO_DISPATCHER_HELICOPTER:New(TransportGroupSet, CargoGroups, PickupZoneSet, DeployZoneSet)

    -- Home zone.
    CargoTransport:SetHomeZone(self.spawnzone)

  elseif Request.transporttype==WAREHOUSE.TransportType.APC then

    -- Pickup and deploy zones.
    local PickupZoneSet = SET_ZONE:New():AddZone(self.spawnzone)
    local DeployZoneSet = SET_ZONE:New():AddZone(Request.warehouse.spawnzone)

    -- Define dispatcher for this task.
    CargoTransport = AI_CARGO_DISPATCHER_APC:New(TransportGroupSet, CargoGroups, PickupZoneSet, DeployZoneSet, 0)

    -- Set home zone.
    CargoTransport:SetHomeZone(self.spawnzone)

  elseif Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER 
      or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP then

    -- Pickup and deploy zones.
    local PickupZoneSet = SET_ZONE:New():AddZone(self.portzone)
    PickupZoneSet:AddZone(self.harborzone)
    local DeployZoneSet = SET_ZONE:New():AddZone(Request.warehouse.harborzone)


    -- Get the shipping lane to use and pass it to the Dispatcher
    local remotename = Request.warehouse.warehouse:GetName()
    local ShippingLane = self.shippinglanes[remotename][math.random(#self.shippinglanes[remotename])]

    -- Define dispatcher for this task.
    CargoTransport = AI_CARGO_DISPATCHER_SHIP:New(TransportGroupSet, CargoGroups, PickupZoneSet, DeployZoneSet, ShippingLane)

    -- Set home zone
    CargoTransport:SetHomeZone(self.portzone)

  else
    self:E(self.lid.."ERROR: Unknown transporttype!")
  end

  -- Set pickup and deploy radii.
  -- The 20 m inner radius are to ensure that the helo does not land on the warehouse itself in the middle of the default spawn zone.
  local pickupouter = 200
  local pickupinner = 0
  local deployouter = 200
  local deployinner = 0
  if Request.transporttype==WAREHOUSE.TransportType.SHIP or Request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER 
    or Request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or Request.transporttype==WAREHOUSE.TransportType.WARSHIP then
    pickupouter=1000
    pickupinner=20
    deployouter=1000
    deployinner=0
  else 
    pickupouter=200
    pickupinner=0
    if self.spawnzone.Radius~=nil then
      pickupouter=self.spawnzone.Radius
      pickupinner=20
    end
    deployouter=200
    deployinner=0
    if self.spawnzone.Radius~=nil then
      deployouter=Request.warehouse.spawnzone.Radius
      deployinner=20
    end
  end
  CargoTransport:SetPickupRadius(pickupouter, pickupinner)
  CargoTransport:SetDeployRadius(deployouter, deployinner)


  -- Adjust carrier units. This has to come AFTER the dispatchers have been defined because they set the cargobay free weight!
  Request.carriercargo={}
  for _,carriergroup in pairs(TransportGroupSet:GetSetObjects()) do
    local asset=self:FindAssetInDB(carriergroup)
    for _i,_carrierunit in pairs(carriergroup:GetUnits()) do
      local carrierunit=_carrierunit --Wrapper.Unit#UNIT

      -- Create empty tables which will be filled with the cargo groups of each carrier unit. Needed in case a carrier unit dies.
      Request.carriercargo[carrierunit:GetName()]={}

      -- Adjust cargo bay of carrier unit.
      local cargobay=asset.cargobay[_i]
      carrierunit:SetCargoBayWeightLimit(cargobay)

      -- Debug info.
      self:T2(self.lid..string.format("Cargo bay weight limit of carrier unit %s: %.1f kg.", carrierunit:GetName(), carrierunit:GetCargoBayFreeWeight()))
    end
  end

  --------------------------------
  -- Dispatcher Event Functions --
  --------------------------------

  --- Function called after carrier picked up something.
  function CargoTransport:OnAfterPickedUp(From, Event, To, Carrier, PickupZone)

    -- Get warehouse state.
    local warehouse=Carrier:GetState(Carrier, "WAREHOUSE") --#WAREHOUSE

    -- Debug message.
    local text=string.format("Carrier group %s picked up at pickup zone %s.", Carrier:GetName(), PickupZone:GetName())
    warehouse:T(warehouse.lid..text)

  end

  --- Function called if something was deployed.
  function CargoTransport:OnAfterDeployed(From, Event, To, Carrier, DeployZone)

    -- Get warehouse state.
    local warehouse=Carrier:GetState(Carrier, "WAREHOUSE") --#WAREHOUSE

    -- Debug message.
    -- TODO: Depoloy zone is nil!
    --local text=string.format("Carrier group %s deployed at deploy zone %s.", Carrier:GetName(), DeployZone:GetName())
    --warehouse:T(warehouse.lid..text)

  end

  --- Function called if carrier group is going home.
  function CargoTransport:OnAfterHome(From, Event, To, Carrier, Coordinate, Speed, Height, HomeZone)

    -- Get warehouse state.
    local warehouse=Carrier:GetState(Carrier, "WAREHOUSE") --#WAREHOUSE

    -- Debug message.
    local text=string.format("Carrier group %s going home to zone %s.", Carrier:GetName(), HomeZone:GetName())
    warehouse:T(warehouse.lid..text)

  end

  --- Function called when a carrier unit has loaded a cargo group.
  function CargoTransport:OnAfterLoaded(From, Event, To, Carrier, Cargo, CarrierUnit, PickupZone)

    -- Get warehouse state.
    local warehouse=Carrier:GetState(Carrier, "WAREHOUSE") --#WAREHOUSE

    -- Debug message.
    local text=string.format("Carrier group %s loaded cargo %s into unit %s in pickup zone %s", Carrier:GetName(), Cargo:GetName(), CarrierUnit:GetName(), PickupZone:GetName())
    warehouse:T(warehouse.lid..text)

    -- Get cargo group object.
    local group=Cargo:GetObject() --Wrapper.Group#GROUP

     -- Get request.
    local request=warehouse:_GetRequestOfGroup(group, warehouse.pending)

    -- Add cargo group to this carrier.
    table.insert(request.carriercargo[CarrierUnit:GetName()], warehouse:_GetNameWithOut(Cargo:GetName()))

  end

  --- Function called when cargo has arrived and was unloaded.
  function CargoTransport:OnAfterUnloaded(From, Event, To, Carrier, Cargo, CarrierUnit, DeployZone)

    -- Get warehouse state.
    local warehouse=Carrier:GetState(Carrier, "WAREHOUSE") --#WAREHOUSE

    -- Get group obejet.
    local group=Cargo:GetObject() --Wrapper.Group#GROUP

    -- Debug message.
    local text=string.format("Cargo group %s was unloaded from carrier unit %s.", tostring(group:GetName()), tostring(CarrierUnit:GetName()))
    warehouse:T(warehouse.lid..text)

    -- Load the cargo in the warehouse.
    --Cargo:Load(warehouse.warehouse)

    -- Trigger Arrived event.
    warehouse:Arrived(group)
  end

  --- On after BackHome event.
  function CargoTransport:OnAfterBackHome(From, Event, To, Carrier)

    -- Intellisense.
    local carrier=Carrier --Wrapper.Group#GROUP

    -- Get warehouse state.
    local warehouse=carrier:GetState(carrier, "WAREHOUSE") --#WAREHOUSE
    carrier:SmokeWhite()

    -- Debug info.
    local text=string.format("Carrier %s is back home at warehouse %s.", tostring(Carrier:GetName()), tostring(warehouse.warehouse:GetName()))
    MESSAGE:New(text, 5):ToAllIf(warehouse.Debug)
    warehouse:I(warehouse.lid..text)

    -- Call arrived event for carrier.
    warehouse:__Arrived(1, Carrier)

  end

  -- Start dispatcher.
  CargoTransport:__Start(5)

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "Unloaded" event. Triggered when a group was unloaded from the carrier.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Group#GROUP group The group that was delivered.
function WAREHOUSE:onafterUnloaded(From, Event, To, group)
  -- Debug info.
  self:_DebugMessage(string.format("Cargo %s unloaded!", tostring(group:GetName())), 5)

  if group and group:IsAlive() then

    -- Debug smoke.
    if self.Debug then
      group:SmokeWhite()
    end

    -- Get max speed of group.
    local speedmax=group:GetSpeedMax()

    if group:IsGround() then
      -- Route group to spawn zone.
      if speedmax>1 then
        group:RouteGroundTo(self.spawnzone:GetRandomCoordinate(), speedmax*0.5, AI.Task.VehicleFormation.RANK, 3)
      else
        -- Immobile ground unit ==> directly put it into the warehouse.
        self:Arrived(group)
      end
    elseif group:IsAir() then
      -- Not sure if air units will be allowed as cargo even though it might be possible. Best put them into warehouse immediately.
      self:Arrived(group)
    elseif group:IsShip() then
      -- Not sure if naval units will be allowed as cargo even though it might be possible. Best put them into warehouse immediately.
      self:Arrived(group)
    end

  else
    self:E(self.lid..string.format("ERROR unloaded Cargo group is not alive!"))
  end
end

--- On before "Arrived" event. Triggered when a group has arrived at its destination warehouse.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Group#GROUP group The group that was delivered.
function WAREHOUSE:onbeforeArrived(From, Event, To, group)

  local asset=self:FindAssetInDB(group)

  if asset then

    if asset.flightgroup and not asset.arrived then
      --env.info("FF asset has a flightgroup. arrival will be handled there!")
      asset.arrived=true
      return false
    end  
  
    if asset.arrived==true then
      -- Asset already arrived (e.g. if multiple units trigger the event via landing).
      return false
    else
      asset.arrived=true  --ensure this is not called again from the same asset group.
      return true
    end
    
  end

end

--- On after "Arrived" event. Triggered when a group has arrived at its destination warehouse.
-- The routine should be called by the warehouse sending this asset and not by the receiving warehouse.
-- It is checked if this asset is cargo (or self propelled) or transport. If it is cargo it is put into the stock of receiving warehouse.
-- If it is a transporter it is put back into the sending warehouse since transports are supposed to return their home warehouse.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Group#GROUP group The group that was delivered.
function WAREHOUSE:onafterArrived(From, Event, To, group)

  -- Debug message and smoke.
  if self.Debug then
    group:SmokeOrange()
  end

  -- Get pending request this group belongs to.
  local request=self:_GetRequestOfGroup(group, self.pending)

  if request then

    -- Get the right warehouse to put the asset into
    -- Transports go back to the warehouse which called this function while cargo goes into the receiving warehouse.
    local warehouse=request.warehouse
    local istransport=self:_GroupIsTransport(group,request)
    if istransport==true then
      warehouse=self
    elseif istransport==false then
      warehouse=request.warehouse
    else
      self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport", group:GetName()))
      return
    end

    -- Debug message.
    self:_DebugMessage(string.format("Group %s arrived at warehouse %s!", tostring(group:GetName()), warehouse.alias), 5)

    -- Route mobile ground group to the warehouse. Group has 60 seconds to get there or it is despawned and added as asset to the new warehouse regardless.
    if group:IsGround() and group:GetSpeedMax()>1 then
      group:RouteGroundTo(warehouse:GetCoordinate(), group:GetSpeedMax()*0.3, "Off Road")
    end

    -- Move asset from pending queue into new warehouse.
    self:T(self.lid.."Asset arrived at warehouse adding in 60 sec")
    warehouse:__AddAsset(60, group)
  end

end

--- On after "Delivered" event. Triggered when all asset groups have reached their destination. Corresponding request is deleted from the pending queue.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #WAREHOUSE.Pendingitem request The pending request that is finished and deleted from the pending queue.
function WAREHOUSE:onafterDelivered(From, Event, To, request)

  -- Debug info
  if self.verbosity>=1 then
    local text=string.format("Warehouse %s: All assets delivered to warehouse %s!", self.alias, request.warehouse.alias)
    self:_InfoMessage(text, 5)
  end

  -- Make some noise :)
  if self.Debug then
    self:_Fireworks(request.warehouse:GetCoordinate())
  end

  -- Set delivered status for this request uid.
  self.delivered[request.uid]=true

end


--- On after "SelfRequest" event. Request was initiated to the warehouse itself. Groups are just spawned at the warehouse or the associated airbase.
-- If the warehouse is currently under attack when the self request is made, the self request is added to the defending table. One the attack is defeated,
-- this request is used to put the groups back into the warehouse stock.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Set#SET_GROUP groupset The set of asset groups that was delivered to the warehouse itself.
-- @param #WAREHOUSE.Pendingitem request Pending self request.
function WAREHOUSE:onafterSelfRequest(From, Event, To, groupset, request)

  -- Debug info.
  self:_DebugMessage(string.format("Assets spawned at warehouse %s after self request!", self.alias))

  -- Debug info.
  for _,_group in pairs(groupset:GetSetObjects()) do
    local group=_group --Wrapper.Group#GROUP
    if self.Debug then
      group:FlareGreen()
    end
  end

  -- Add a "defender request" to be able to despawn all assets once defeated.
  if self:IsAttacked() then

    -- Route (mobile) ground troops to warehouse zone if they are not alreay there.
    if self.autodefence then
      for _,_group in pairs(groupset:GetSetObjects()) do
        local group=_group --Wrapper.Group#GROUP
        local speedmax=group:GetSpeedMax()
        if group:IsGround() and speedmax>1 and group:IsNotInZone(self.zone) then
          group:RouteGroundTo(self.zone:GetRandomCoordinate(), 0.8*speedmax, "Off Road")
        end
      end
    end

    -- Add request to defenders.
    table.insert(self.defending, request)
  end

end

--- On after "Attacked" event. Warehouse is under attack by an another coalition.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param DCS#coalition.side Coalition which is attacking the warehouse.
-- @param DCS#country.id Country which is attacking the warehouse.
function WAREHOUSE:onafterAttacked(From, Event, To, Coalition, Country)

  -- Warning.
  local text=string.format("Warehouse %s: We are under attack!", self.alias)
  self:_InfoMessage(text)

  -- Debug smoke.
  if self.Debug then
    self:GetCoordinate():SmokeOrange()
  end

  -- Spawn all ground units in the spawnzone?
  if self.autodefence then
    local nground=self:GetNumberOfAssets(WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND)
    local text=string.format("Warehouse auto defence activated.\n")

    if nground>0 then
      text=text..string.format("Deploying all %d ground assets.", nground)

      -- Add self request.
      self:AddRequest(self, WAREHOUSE.Descriptor.CATEGORY, Group.Category.GROUND, WAREHOUSE.Quantity.ALL, nil, nil , 0, "AutoDefence")
    else
      text=text..string.format("No ground assets currently available.")
    end
    self:_InfoMessage(text)
  else
    local text=string.format("Warehouse auto defence inactive.")
    self:I(self.lid..text)
  end
end

--- On after "Defeated" event. Warehouse defeated an attack by another coalition. Defender assets are added back to warehouse stock.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function WAREHOUSE:onafterDefeated(From, Event, To)

  -- Message.
  local text=string.format("Warehouse %s: Enemy attack was defeated!", self.alias)
  self:_InfoMessage(text)

  -- Debug smoke.
  if self.Debug then
    self:GetCoordinate():SmokeGreen()
  end

  -- Auto defence: put assets back into stock.
  if self.autodefence then
    for _,request in pairs(self.defending) do

      -- Route defenders back to warehoue (for visual reasons only) and put them back into stock.
      for _,_group in pairs(request.cargogroupset:GetSetObjects()) do
        local group=_group --Wrapper.Group#GROUP

        -- Get max speed of group and route it back slowly to the warehouse.
        local speed=group:GetSpeedMax()
        if group:IsGround() and speed>1 then
          group:RouteGroundTo(self:GetCoordinate(), speed*0.3)
        end

        -- Add asset group back to stock after 60 seconds.
        self:__AddAsset(60, group)
      end

    end

    self.defending=nil
    self.defending={}
  end
end

--- Respawn warehouse.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function WAREHOUSE:onafterRespawn(From, Event, To)

  -- Info message.
  local text=string.format("Respawning warehouse %s", self.alias)
  self:_InfoMessage(text)

  -- Respawn warehouse.
  self.warehouse:ReSpawn()

end

--- On before "ChangeCountry" event. Checks whether a change of country is necessary by comparing the actual country to the the requested one.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param DCS#country.id Country which has captured the warehouse.
function WAREHOUSE:onbeforeChangeCountry(From, Event, To, Country)

  local currentCountry=self:GetCountry()

  -- Message.
  local text=string.format("Warehouse %s: request to change country %d-->%d", self.alias, currentCountry, Country)
  self:_DebugMessage(text, 10)

  -- Check if current or requested coalition or country match.
  if currentCountry~=Country then
    return true
  end

  return false
end

--- On after "ChangeCountry" event. Warehouse is respawned with the specified country. All queued requests are deleted and the owned airbase is reset if the coalition is changed by changing the
-- country.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param DCS#country.id Country Country which has captured the warehouse.
function WAREHOUSE:onafterChangeCountry(From, Event, To, Country)

  local CoalitionOld=self:GetCoalition()

  self.warehouse:ReSpawn(Country)

  local CoalitionNew=self:GetCoalition()

  -- Delete all waiting requests because they are not valid any more.
  self.queue=nil
  self.queue={}

  if self.airbasename then

    -- Get airbase of this warehouse.
    local airbase=AIRBASE:FindByName(self.airbasename)

    -- Get coalition of the airbase.
    local airbaseCoalition=airbase:GetCoalition()

    if CoalitionNew==airbaseCoalition then
      -- Airbase already owned by the coalition that captured the warehouse. Airbase can be used by this warehouse.
      self.airbase=airbase
    else
      -- Airbase is owned by other coalition. So this warehouse does not have an airbase until it is captured.
      self.airbase=nil
    end

  end

  -- Debug smoke.
  if self.Debug then
    if CoalitionNew==coalition.side.RED then
      self:GetCoordinate():SmokeRed()
    elseif CoalitionNew==coalition.side.BLUE then
      self:GetCoordinate():SmokeBlue()
    end
  end

end

--- On before "Captured" event. Warehouse has been captured by another coalition.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param DCS#coalition.side Coalition which captured the warehouse.
-- @param DCS#country.id Country which has captured the warehouse.
function WAREHOUSE:onbeforeCaptured(From, Event, To, Coalition, Country)

  -- Warehouse respawned.
  self:ChangeCountry(Country)

end

--- On after "Captured" event. Warehouse has been captured by another coalition.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param DCS#coalition.side Coalition which captured the warehouse.
-- @param DCS#country.id Country which has captured the warehouse.
function WAREHOUSE:onafterCaptured(From, Event, To, Coalition, Country)

  -- Message.
  local text=string.format("Warehouse %s: We were captured by enemy coalition (side=%d)!", self.alias, Coalition)
  self:_InfoMessage(text)

end


--- On after "AirbaseCaptured" event. Airbase of warehouse has been captured by another coalition.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param DCS#coalition.side Coalition which captured the warehouse.
function WAREHOUSE:onafterAirbaseCaptured(From, Event, To, Coalition)

  -- Message.
  local text=string.format("Warehouse %s: Our airbase %s was captured by the enemy (coalition=%d)!", self.alias, self.airbasename, Coalition)
  self:_InfoMessage(text)

  -- Debug smoke.
  if self.Debug then
    if Coalition==coalition.side.RED then
      self.airbase:GetCoordinate():SmokeRed()
    elseif Coalition==coalition.side.BLUE then
      self.airbase:GetCoordinate():SmokeBlue()
    end
  end

  -- Set airbase to nil and category to no airbase.
  self.airbase=nil
end

--- On after "AirbaseRecaptured" event. Airbase of warehouse has been re-captured from other coalition.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param DCS#coalition.side Coalition Coalition side which originally captured the warehouse.
function WAREHOUSE:onafterAirbaseRecaptured(From, Event, To, Coalition)

  -- Message.
  local text=string.format("Warehouse %s: We recaptured our airbase %s from the enemy (coalition=%d)!", self.alias, self.airbasename, Coalition)
  self:_InfoMessage(text)

  -- Set airbase and category.
  self.airbase=AIRBASE:FindByName(self.airbasename)

  -- Debug smoke.
  if self.Debug then
    if Coalition==coalition.side.RED then
      self.airbase:GetCoordinate():SmokeRed()
    elseif Coalition==coalition.side.BLUE then
      self.airbase:GetCoordinate():SmokeBlue()
    end
  end

end

--- On after "RunwayDestroyed" event.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function WAREHOUSE:onafterRunwayDestroyed(From, Event, To)

  -- Message.
  local text=string.format("Warehouse %s: Runway %s destroyed!", self.alias, self.airbasename)
  self:_InfoMessage(text)

  self.runwaydestroyed=timer.getAbsTime()
  
  return self
end

--- On after "RunwayRepaired" event.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function WAREHOUSE:onafterRunwayRepaired(From, Event, To)

  -- Message.
  local text=string.format("Warehouse %s: Runway %s repaired!", self.alias, self.airbasename)
  self:_InfoMessage(text)

  self.runwaydestroyed=nil
  
  return self
end


--- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Group#GROUP group The group spawned.
-- @param #WAREHOUSE.Assetitem asset The asset that is dead.
-- @param #WAREHOUSE.Pendingitem request The request of the dead asset.
function WAREHOUSE:onafterAssetSpawned(From, Event, To, group, asset, request)
  local text=string.format("Asset %s from request id=%d was spawned!", asset.spawngroupname, request.uid)
  self:T(self.lid..text)

  -- Sete asset state to spawned.
  asset.spawned=true
  
  -- Set spawn group name.
  asset.spawngroupname=group:GetName()
  
  -- Remove asset from stock.
  self:_DeleteStockItem(asset)          

  -- Add group.
  if asset.iscargo==true then
    request.cargogroupset=request.cargogroupset or SET_GROUP:New()
    request.cargogroupset:AddGroup(group)
  else
    request.transportgroupset=request.transportgroupset or SET_GROUP:New()
    request.transportgroupset:AddGroup(group)
  end

  -- Set warehouse state.
  group:SetState(group, "WAREHOUSE", self)  

  -- Check if all assets groups are spawned and trigger events.
  local n=0
  for _,_asset in pairs(request.assets) do
    local assetitem=_asset --#WAREHOUSE.Assetitem

    -- Debug info.
    self:T(self.lid..string.format("Asset %s spawned %s as %s", assetitem.templatename, tostring(assetitem.spawned), tostring(assetitem.spawngroupname)))

    if assetitem.spawned then
      n=n+1
    else
      -- Now this can happend if multiple groups need to be spawned in one request.
      --self:I(self.lid.."FF What?! This should not happen!")
    end

  end

  -- Trigger event.
  if n==request.nasset+request.ntransport then
    self:T(self.lid..string.format("All assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned. Calling RequestSpawned", n, request.nasset, request.ntransport, request.uid))
    self:RequestSpawned(request, request.cargogroupset, request.transportgroupset)
  else
    self:T(self.lid..string.format("Not all assets %d (ncargo=%d + ntransport=%d) of request rid=%d spawned YET", n, request.nasset, request.ntransport, request.uid))
  end

end

--- On after "AssetDead" event triggered when an asset group died.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #WAREHOUSE.Assetitem asset The asset that is dead.
-- @param #WAREHOUSE.Pendingitem request The request of the dead asset.
function WAREHOUSE:onafterAssetDead(From, Event, To, asset, request)

  if asset and request then

    -- Debug message.
    local text=string.format("Asset %s from request id=%d is dead!", asset.templatename, request.uid)
    self:T(self.lid..text)
  
    -- Here I need to get rid of the #CARGO at the end to obtain the original name again!
    local groupname=asset.spawngroupname --self:_GetNameWithOut(group)
  
    -- Dont trigger a Remove event for the group sets.
    local NoTriggerEvent=true
  
    if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then
  
      ---
      -- Easy case: Group can simply be removed from the cargogroupset.
      ---
  
      -- Remove dead group from cargo group set.
      if request.cargogroupset then
        -- cargogroupset was nil for user case. Difficult to reproduce so we add a nil check.
        request.cargogroupset:Remove(groupname, NoTriggerEvent)
        self:T(self.lid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count()))
      else
        self:E(self.lid..string.format("ERROR: cargogroupset is nil for request ID=%s!", tostring(request.uid)))
      end
  
    else
  
      ---
      -- Complicated case: Dead unit could be:
      -- 1.) A Cargo unit (e.g. waiting to be picked up).
      -- 2.) A Transport unit which itself holds cargo groups.
      ---
  
      -- Check if this a cargo or transport group.
      local istransport=not asset.iscargo --self:_GroupIsTransport(group, request)
  
      if istransport==true then
  
        -- Whole carrier group is dead. Remove it from the carrier group set.
        request.transportgroupset:Remove(groupname, NoTriggerEvent)
        self:T(self.lid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count()))
  
      elseif istransport==false then
  
        -- This must have been an alive cargo group that was killed outside the carrier, e.g. waiting to be transported or waiting to be put back.
        -- Remove dead group from cargo group set.
        request.cargogroupset:Remove(groupname, NoTriggerEvent)
        self:T(self.lid..string.format("Removed transported cargo %s outside carrier: ncargo=%d", groupname, request.cargogroupset:Count()))
        -- This as well?
        --request.transportcargoset:RemoveCargosByName(RemoveCargoNames)
        
      else
        --self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName()))
      end
    end
    
  else
    self:E(self.lid.."ERROR: Asset and/or Request is nil in onafterAssetDead")
  
  end
  
end


--- On after "Destroyed" event. Warehouse was destroyed. All services are stopped. Warehouse is going to "Stopped" state in one minute.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function WAREHOUSE:onafterDestroyed(From, Event, To)

  -- Message.
  local text=string.format("Warehouse %s was destroyed! Assets lost %d. Respawn=%s", self.alias, #self.stock, tostring(self.respawnafterdestroyed))
  self:_InfoMessage(text)

  if self.respawnafterdestroyed then

    if self.respawndelay then
      self:Pause()
      self:__Respawn(self.respawndelay)
    else
      self:Respawn()
    end

  else

    -- Remove all table entries from waiting queue and stock.
    for k,_ in pairs(self.queue) do
      self.queue[k]=nil
    end

    for k,_ in pairs(self.stock) do
      --self.stock[k]=nil
    end

    for k=#self.stock,1,-1 do
      --local asset=self.stock[k] --#WAREHOUSE.Assetitem
      --self:AssetDead(asset, nil)
      self.stock[k]=nil
    end

    --self.queue=nil
    --self.queue={}

    --self.stock=nil
    --self.stock={}
  end

end


--- On after "Save" event. Warehouse assets are saved to file on disk.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory.
-- @param #string filename (Optional) Name of the file containing the asset data.
function WAREHOUSE:onafterSave(From, Event, To, path, filename)

  local function _savefile(filename, data)
    local f = assert(io.open(filename, "wb"))
    f:write(data)
    f:close()
  end

  -- Set file name.
  filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias)

  -- Set path.
  if path~=nil then
    filename=path.."\\"..filename
  end

  -- Info
  local text=string.format("Saving warehouse assets to file %s", filename)
  MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report)
  self:I(self.lid..text)

  local warehouseassets=""
  warehouseassets=warehouseassets..string.format("coalition=%d\n", self:GetCoalition())
  warehouseassets=warehouseassets..string.format("country=%d\n", self:GetCountry())

  -- Loop over all assets in stock.
  for _,_asset in pairs(self.stock) do
    local asset=_asset -- #WAREHOUSE.Assetitem

    -- Loop over asset parameters.
    local assetstring=""
    for key,value in pairs(asset) do

      -- Only save keys which are needed to restore the asset.
      if key=="templatename" or key=="attribute" or key=="cargobay" or key=="weight" or key=="loadradius" or key=="livery" or key=="skill" or key=="assignment" then
        local name
        if type(value)=="table" then
          name=string.format("%s=%s;", key, value[1])
        else
          name=string.format("%s=%s;", key, value)
        end
        assetstring=assetstring..name
      end
      self:I(string.format("Loaded asset: %s", assetstring))
    end

    -- Add asset string.
    warehouseassets=warehouseassets..assetstring.."\n"
  end

  -- Save file.
  _savefile(filename, warehouseassets)

end


--- On before "Load" event. Checks if the file the warehouse data should be loaded from exists.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string path Path where the file is loaded from.
-- @param #string filename (Optional) Name of the file containing the asset data.
function WAREHOUSE:onbeforeLoad(From, Event, To, path, filename)


  local function _fileexists(name)
     local f=io.open(name,"r")
     if f~=nil then
      io.close(f)
      return true
    else
      return false
    end
  end

  -- Set file name.
  filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias)

  -- Set path.
  if path~=nil then
    filename=path.."\\"..filename
  end

  -- Check if file exists.
  local exists=_fileexists(filename)

  if exists then
    return true
  else
    self:_ErrorMessage(string.format("ERROR: file %s does not exist! Cannot load assets.", filename), 60)
    return false
  end

end


--- On after "Load" event. Warehouse assets are loaded from file on disk.
-- @param #WAREHOUSE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string path Path where the file is loaded from.
-- @param #string filename (Optional) Name of the file containing the asset data.
function WAREHOUSE:onafterLoad(From, Event, To, path, filename)

  local function _loadfile(filename)
    local f = assert(io.open(filename, "rb"))
    local data = f:read("*all")
    f:close()
    return data
  end

  -- Set file name.
  filename=filename or string.format("WAREHOUSE-%d_%s.txt", self.uid, self.alias)

  -- Set path.
  if path~=nil then
    filename=path.."\\"..filename
  end

  -- Info
  local text=string.format("Loading warehouse assets from file %s", filename)
  MESSAGE:New(text,30):ToAllIf(self.Debug or self.Report)
  self:I(self.lid..text)

  -- Load asset data from file.
  local data=_loadfile(filename)

  -- Split by line break.
  local assetdata=UTILS.Split(data,"\n")

  -- Coalition and coutrny.
  local Coalition
  local Country

  -- Loop over asset lines.
  local assets={}
  for _,asset in pairs(assetdata) do

    -- Parameters are separated by semi-colons
    local descriptors=UTILS.Split(asset,";")

    local asset={}
    local isasset=false
    for _,descriptor in pairs(descriptors) do

      local keyval=UTILS.Split(descriptor,"=")

      if #keyval==2 then

        if keyval[1]=="coalition" then
          -- Get coalition side.
          Coalition=tonumber(keyval[2])
        elseif keyval[1]=="country" then
          -- Get country id.
          Country=tonumber(keyval[2])
        else

          -- This is an asset.
          isasset=true

          local key=keyval[1]
          local val=keyval[2]

          --env.info(string.format("FF asset key=%s val=%s", key, val))

          -- Livery or skill could be "nil".
          if val=="nil" then
            val=nil
          end

          -- Convert string to number where necessary.
          if key=="cargobay" or key=="weight" or key=="loadradius" then
            asset[key]=tonumber(val)
          else
            asset[key]=val
          end
        end

      end
    end

    -- Add to table.
    if isasset then
      table.insert(assets, asset)
    end
  end

  -- Respawn warehouse with prev coalition if necessary.
  if Country~=self:GetCountry() then
    self:T(self.lid..string.format("Changing warehouse country %d-->%d on loading assets.", self:GetCountry(), Country))
    self:ChangeCountry(Country)
  end

  for _,_asset in pairs(assets) do
    local asset=_asset --#WAREHOUSE.Assetitem

    local group=GROUP:FindByName(asset.templatename)
    if group then
      self:AddAsset(group, 1, asset.attribute, asset.cargobay, asset.weight, asset.loadradius, asset.skill, asset.livery, asset.assignment)
    else
      self:E(string.format("ERROR: Group %s doest not exit. Cannot be loaded as asset.", tostring(asset.templatename)))
    end
  end

end

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Spawn functions
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Spawns requested assets at warehouse or associated airbase.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE.Queueitem Request Information table of the request.
function WAREHOUSE:_SpawnAssetRequest(Request)
  self:F2({requestUID=Request.uid})

  -- Shortcut to cargo assets.
  local cargoassets=Request.cargoassets

  -- Now we try to find all parking spots for all cargo groups in advance. Due to the for loop, the parking spots do not get updated while spawning.
  local Parking={}
  if Request.cargocategory==Group.Category.AIRPLANE or Request.cargocategory==Group.Category.HELICOPTER then
    --TODO: Check for airstart. Should be a request property.
    Parking=self:_FindParkingForAssets(self.airbase, cargoassets) or {}
  end

  -- Spawn aircraft in uncontrolled state.
  local UnControlled=true

  -- Loop over cargo requests.
  for i=1,#cargoassets do

    -- Get stock item.
    local asset=cargoassets[i] --#WAREHOUSE.Assetitem
    
    if not asset.spawned then

      -- Set asset status to not spawned until we capture its birth event.
      asset.iscargo=true
  
      -- Set request ID.
      asset.rid=Request.uid
  
      -- Spawn group name.
      local _alias=asset.spawngroupname
  
      --Request add asset by id.
      Request.assets[asset.uid]=asset
  
      -- Spawn an asset group.
      local _group=nil --Wrapper.Group#GROUP
      if asset.category==Group.Category.GROUND then
  
        -- Spawn ground troops.
        _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone, Request.lateActivation)
  
      elseif asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then
  
        -- Spawn air units.
        if Parking[asset.uid] then
          _group=self:_SpawnAssetAircraft(_alias, asset, Request, Parking[asset.uid], UnControlled, Request.lateActivation)
        else
          _group=self:_SpawnAssetAircraft(_alias, asset, Request, nil, UnControlled, Request.lateActivation)
        end
  
      elseif asset.category==Group.Category.TRAIN then
  
        -- Spawn train.
        if self.rail then
          --TODO: Rail should only get one asset because they would spawn on top!
  
          -- Spawn naval assets.
          _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.spawnzone, Request.lateActivation)
        end
  
        --self:E(self.lid.."ERROR: Spawning of TRAIN assets not possible yet!")
  
      elseif asset.category==Group.Category.SHIP then
  
        -- Spawn naval assets.
        _group=self:_SpawnAssetGroundNaval(_alias, asset, Request, self.portzone, Request.lateActivation)
  
      else
        self:E(self.lid.."ERROR: Unknown asset category!")
      end
      
      -- Trigger event.
      if _group then
        self:__AssetSpawned(0.01, _group, asset, Request)
      end    
  
    end
    
  end

end


--- Spawn a ground or naval asset in the corresponding spawn zone of the warehouse.
-- @param #WAREHOUSE self
-- @param #string alias Alias name of the asset group.
-- @param #WAREHOUSE.Assetitem asset Ground asset that will be spawned.
-- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias.
-- @param Core.Zone#ZONE spawnzone Zone where the assets should be spawned.
-- @param #boolean lateactivated If true, groups are spawned late activated.
-- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned.
function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, lateactivated)

  if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP or asset.category==Group.Category.TRAIN) then

    -- Prepare spawn template.
    local template=self:_SpawnAssetPrepareTemplate(asset, alias)

    -- Initial spawn point.
    template.route.points[1]={}

    -- Get a random coordinate in the spawn zone.
    local coord=spawnzone:GetRandomCoordinate()

    -- For trains, we use the rail connection point.
    if asset.category==Group.Category.TRAIN then
      coord=self.rail
    end

    -- Translate the position of the units.
    for i=1,#template.units do

      -- Unit template.
      local unit = template.units[i]

      -- Translate position.
      local SX = unit.x or 0
      local SY = unit.y or 0
      local BX = asset.template.route.points[1].x
      local BY = asset.template.route.points[1].y
      local TX = coord.x + (SX-BX)
      local TY = coord.z + (SY-BY)

      template.units[i].x = TX
      template.units[i].y = TY

      if asset.livery then
        unit.livery_id = asset.livery
      end
      if asset.skill then
        unit.skill= asset.skill
      end

    end
    
    -- Late activation.
    template.lateActivation=lateactivated

    template.route.points[1].x = coord.x
    template.route.points[1].y = coord.z

    template.x   = coord.x
    template.y   = coord.z
    template.alt = coord.y

    if self.ValidateAndRepositionGroundUnits then
      UTILS.ValidateAndRepositionGroundUnits(template.units)
    end

    -- Spawn group.
    local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP

    return group
  end

  return nil
end

--- Spawn an aircraft asset (plane or helo) at the airbase associated with the warehouse.
-- @param #WAREHOUSE self
-- @param #string alias Alias name of the asset group.
-- @param #WAREHOUSE.Assetitem asset Ground asset that will be spawned.
-- @param #WAREHOUSE.Queueitem request Request belonging to this asset. Needed for the name/alias.
-- @param #table parking Parking data for this asset.
-- @param #boolean uncontrolled Spawn aircraft in uncontrolled state.
-- @param #boolean lateactivated If true, groups are spawned late activated.
-- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned.
function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled, lateactivated)

  if asset and asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then

    -- Prepare the spawn template.
    local template=self:_SpawnAssetPrepareTemplate(asset, alias)

    -- Cold start (default).
    local _type=COORDINATE.WaypointType.TakeOffParking
    local _action=COORDINATE.WaypointAction.FromParkingArea

    -- Hot start.
    if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then
      _type=COORDINATE.WaypointType.TakeOffParkingHot
      _action=COORDINATE.WaypointAction.FromParkingAreaHot
      uncontrolled=false
    end
    
    local airstart=asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TurningPoint or false
    
    if airstart then
      _type=COORDINATE.WaypointType.TurningPoint
      _action=COORDINATE.WaypointAction.TurningPoint
      uncontrolled=false    
    end


    -- Set route points.
    if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then

      -- Get flight path if the group goes to another warehouse by itself.
      if request.toself then
      
        local coord=self.airbase:GetCoordinate()
        
        if airstart then
          coord:SetAltitude(math.random(1000, 2000))
        end
        
        -- Single waypoint.
        local wp=coord:WaypointAir("RADIO", _type, _action, 0, false, self.airbase, {}, "Parking")
        template.route.points={wp}
      else
        template.route.points=self:_GetFlightplan(asset, self.airbase, request.warehouse.airbase)
      end

    else

      -- First route point is the warehouse airbase.
      template.route.points[1]=self.airbase:GetCoordinate():WaypointAir("BARO", _type, _action, 0, true, self.airbase, nil, "Spawnpoint")

    end

    -- Get airbase ID and category.
    local AirbaseID = self.airbase:GetID()
    local AirbaseCategory = self:GetAirbaseCategory()

    -- Check enough parking spots.
    if AirbaseCategory==Airbase.Category.HELIPAD or AirbaseCategory==Airbase.Category.SHIP then

      --TODO Figure out what's necessary in this case.

    else

      if parking and #parking<#template.units and not airstart then
        local text=string.format("ERROR: Not enough parking! Free parking = %d < %d aircraft to be spawned.", #parking, #template.units)
        self:_DebugMessage(text)
        return nil
      end

    end

    -- Position the units.
    for i=1,#template.units do

      -- Unit template.
      local unit = template.units[i]

      if AirbaseCategory == Airbase.Category.HELIPAD or AirbaseCategory == Airbase.Category.SHIP then

        -- Helipads we take the position of the airbase location, since the exact location of the spawn point does not make sense.
        local coord=self.airbase:GetCoordinate()

        unit.x=coord.x
        unit.y=coord.z
        unit.alt=coord.y
        
        if airstart then
          unit.alt=math.random(1000, 2000)
        end

        unit.parking_id = nil
        unit.parking    = nil

      else

        local coord=nil    --Core.Point#COORDINATE
        local terminal=nil --#number
        
        if airstart then
          coord=self.airbase:GetCoordinate():SetAltitude(math.random(1000, 2000))
        else
          coord=parking[i].Coordinate
          terminal=parking[i].TerminalID
        end

        if self.Debug and terminal then
          local text=string.format("Spawnplace unit %s terminal %d.", unit.name, terminal)
          coord:MarkToAll(text)
          env.info(text)
        end

        unit.x=coord.x
        unit.y=coord.z
        unit.alt=coord.y

        unit.parking_id = nil
        unit.parking    = terminal

      end

      if asset.livery then
        unit.livery_id = asset.livery
      end

      if asset.skill then
        unit.skill= asset.skill
      end

      if asset.payload then
        unit.payload=asset.payload.pylons
      end

      if asset.modex then
        unit.onboard_num=asset.modex[i]
      end
      if asset.callsign then
        unit.callsign=asset.callsign[i]
      end

    end

    -- And template position.
    template.x = template.units[1].x
    template.y = template.units[1].y

    -- DCS bug workaround. Spawning helos in uncontrolled state on carriers causes a big spash!
    -- See https://forums.eagle.ru/showthread.php?t=219550
    -- Should be solved in latest OB update 2.5.3.21708
    --if AirbaseCategory == Airbase.Category.SHIP and asset.category==Group.Category.HELICOPTER then
    --  uncontrolled=false
    --end

    -- Uncontrolled spawning.
    template.uncontrolled=uncontrolled

    -- Debug info.
    self:T2({airtemplate=template})

    -- Spawn group.
    local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP

    return group
  end

  return nil
end


--- Prepare a spawn template for the asset. Deep copy of asset template, adjusting template and unit names, nillifying group and unit ids.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE.Assetitem asset Ground asset that will be spawned.
-- @param #string alias Alias name of the group.
-- @return #table Prepared new spawn template.
function WAREHOUSE:_SpawnAssetPrepareTemplate(asset, alias)

  -- Create an own copy of the template!
  local template=UTILS.DeepCopy(asset.template)

  -- Set unique name.
  template.name=alias

  -- Set current(!) coalition and country.
  template.CoalitionID=self:GetCoalition()
  template.CountryID=self:GetCountry()

  -- Nillify the group ID.
  template.groupId=nil

  -- No late activation.
  template.lateActivation=false

  if asset.missionTask then
    self:T(self.lid..string.format("Setting mission task to %s", tostring(asset.missionTask)))
    template.task=asset.missionTask
  end

  -- No predefined task.
  --template.taskSelected=false

  -- Set and empty route.
  template.route = {}
  template.route.routeRelativeTOT=true
  template.route.points = {}

  -- Handle units.
  for i=1,#template.units do

    -- Unit template.
    local unit = template.units[i]

    -- Nillify the unit ID.
    unit.unitId=nil

    -- Set unit name: <alias>-01, <alias>-02, ...
    unit.name=string.format("%s-%02d", template.name , i)

  end

  return template
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Routing functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Route ground units to destination. ROE is set to return fire and alarm state to green.
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP group The ground group to be routed
-- @param #WAREHOUSE.Queueitem request The request for this group.
function WAREHOUSE:_RouteGround(group, request)

  if group and group:IsAlive() then

    -- Set speed to 70% of max possible.
    local _speed=group:GetSpeedMax()*0.7

    -- Route waypoints.
    local Waypoints={}

    -- Check if an off road path has been defined.
    local hasoffroad=self:HasConnectionOffRoad(request.warehouse, self.Debug)

    -- Check if any off road paths have be defined. They have priority!
    if hasoffroad then

      -- Get off road path to remote warehouse. If more have been defined, pick one randomly.
      local remotename=request.warehouse.warehouse:GetName()
      local path=self.offroadpaths[remotename][math.random(#self.offroadpaths[remotename])]

      -- Loop over user defined shipping lanes.
      for i=1,#path do

        -- Shortcut and coordinate intellisense.
        local coord=path[i] --Core.Point#COORDINATE

        -- Get waypoint for coordinate.
        local Waypoint=coord:WaypointGround(_speed, "Off Road")

        -- Add waypoint to route.
        table.insert(Waypoints, Waypoint)
      end

    else

      -- Waypoints for road-to-road connection.
      Waypoints = group:TaskGroundOnRoad(request.warehouse.road, _speed, "Off Road", false, self.road)

      -- First waypoint = current position of the group.
      local FromWP=group:GetCoordinate():WaypointGround(_speed, "Off Road")
      table.insert(Waypoints, 1, FromWP)

      -- Final coordinate. Note, this can lead to errors if the final WP is too close the the point on the road. The vehicle will stop driving and not reach the final WP!
      --local ToCO=request.warehouse.spawnzone:GetRandomCoordinate()
      --local ToWP=ToCO:WaypointGround(_speed, "Off Road")
      --table.insert(Waypoints, #Waypoints+1, ToWP)

    end

    for n,wp in ipairs(Waypoints) do
      local tf=self:_SimpleTaskFunctionWP("warehouse:_PassingWaypoint",group, n, #Waypoints)
      group:SetTaskWaypoint(wp, tf)
    end

    -- Route group to destination.
    group:Route(Waypoints, 1)

    -- Set ROE and alaram state.
    group:OptionROEReturnFire()
    group:OptionAlarmStateGreen()
  end
end

--- Route naval units along user defined shipping lanes to destination warehouse. ROE is set to return fire.
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP group The naval group to be routed
-- @param #WAREHOUSE.Queueitem request The request for this group.
function WAREHOUSE:_RouteNaval(group, request)

  -- Check if we have a group and it is alive.
  if group and group:IsAlive() then

    -- Set speed to 80% of max possible.
    local _speed=group:GetSpeedMax()*0.8

    -- Get shipping lane to remote warehouse. If more have been defined, pick one randomly.
    local remotename=request.warehouse.warehouse:GetName()
    local lane=self.shippinglanes[remotename][math.random(#self.shippinglanes[remotename])]

    if lane then

      -- Route waypoints.
      local Waypoints={}

      -- Loop over user defined shipping lanes.
      for i=1,#lane do

        -- Shortcut and coordinate intellisense.
        local coord=lane[i] --Core.Point#COORDINATE

        -- Get waypoint for coordinate.
        local Waypoint=coord:WaypointGround(_speed)

        -- Add waypoint to route.
        table.insert(Waypoints, Waypoint)
      end

      -- Task function triggering the arrived event at the last waypoint.
      local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", group)

      -- Put task function on last waypoint.
      local Waypoint = Waypoints[#Waypoints]
      group:SetTaskWaypoint(Waypoint, TaskFunction)

      -- Route group to destination.
      group:Route(Waypoints, 1)

      -- Set ROE (Naval units dont have and alaram state.)
      group:OptionROEReturnFire()

    else
      -- This should not happen! Existance of shipping lane was checked before executing this request.
      self:E(self.lid..string.format("ERROR: No shipping lane defined for Naval asset!"))
    end

  end
end


--- Route the airplane from one airbase another. Activates uncontrolled aircraft and sets ROE/ROT for ferry flights.
-- ROE is set to return fire and ROT to passive defence.
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP aircraft Airplane group to be routed.
function WAREHOUSE:_RouteAir(aircraft)

  if aircraft and aircraft:IsAlive()~=nil then

    -- Debug info.
    self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s", aircraft:GetName(), tostring(aircraft:IsAlive())))

    -- Give start command to activate uncontrolled aircraft within the next 60 seconds.
    if self.flightcontrol then
      local fg=FLIGHTGROUP:New(aircraft)
      fg:SetReadyForTakeoff(true)    
    else
      aircraft:StartUncontrolled(math.random(60))
    end

    -- Debug info.
    self:T2(self.lid..string.format("RouteAir aircraft group %s alive=%s (after start command)", aircraft:GetName(), tostring(aircraft:IsAlive())))

    -- Set ROE and alaram state.
    aircraft:OptionROEReturnFire()
    aircraft:OptionROTPassiveDefense()

  else
    self:E(string.format("ERROR: aircraft %s cannot be routed since it does not exist or is not alive %s!", tostring(aircraft:GetName()), tostring(aircraft:IsAlive())))
  end
end

--- Route trains to their destination - or at least to the closest point on rail of the desired final destination.
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP Group The train group.
-- @param Core.Point#COORDINATE Coordinate of the destination. Tail will be routed to the closest point
-- @param #number Speed Speed in km/h to drive to the destination coordinate. Default is 60% of max possible speed the unit can go.
function WAREHOUSE:_RouteTrain(Group, Coordinate, Speed)

  if Group and Group:IsAlive() then

    local _speed=Speed or Group:GetSpeedMax()*0.6

    -- Create a
    local Waypoints = Group:TaskGroundOnRailRoads(Coordinate, Speed)

    -- Task function triggering the arrived event at the last waypoint.
    local TaskFunction = self:_SimpleTaskFunction("warehouse:_Arrived", Group)

    -- Put task function on last waypoint.
    local Waypoint = Waypoints[#Waypoints]
    Group:SetTaskWaypoint( Waypoint, TaskFunction )

    -- Route group to destination.
    Group:Route(Waypoints, 1)
  end
end

--- Task function for last waypoint. Triggering the "Arrived" event.
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP group The group that arrived.
function WAREHOUSE:_Arrived(group)
  self:_DebugMessage(string.format("Group %s arrived!", tostring(group:GetName())))

  if group then
    --Trigger "Arrived event.
    self:__Arrived(1, group)
  end

end

--- Task function for when passing a waypoint.
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP group The group that arrived.
-- @param #number n Waypoint passed.
-- @param #number N Final waypoint.
function WAREHOUSE:_PassingWaypoint(group, n, N)
  self:T(self.lid..string.format("Group %s passing waypoint %d of %d!", tostring(group:GetName()), n, N))

  -- Final waypoint reached.
  if n==N then
    self:__Arrived(1, group)
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event handler functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get a warehouse asset from its unique id.
-- @param #WAREHOUSE self
-- @param #number id Asset ID.
-- @return #WAREHOUSE.Assetitem The warehouse asset.
function WAREHOUSE:GetAssetByID(id)
  if id then
    return _WAREHOUSEDB.Assets[id]
  else
    return nil
  end
end

--- Get a warehouse asset from its name.
-- @param #WAREHOUSE self
-- @param #string GroupName Spawn group name.
-- @return #WAREHOUSE.Assetitem The warehouse asset.
function WAREHOUSE:GetAssetByName(GroupName)

  local name=self:_GetNameWithOut(GroupName)
  local _,aid,_=self:_GetIDsFromGroup(GROUP:FindByName(name))

  if aid then
    return _WAREHOUSEDB.Assets[aid]
  else
    return nil
  end
end

--- Get a warehouse request from its unique id.
-- @param #WAREHOUSE self
-- @param #number id Request ID.
-- @return #WAREHOUSE.Pendingitem The warehouse requested - either queued or pending.
-- @return #boolean If *true*, request is queued, if *false*, request is pending, if *nil*, request could not be found.
function WAREHOUSE:GetRequestByID(id)

  if id then

    for _,_request in pairs(self.queue) do
      local request=_request --#WAREHOUSE.Queueitem
      if request.uid==id then
        return request, true
      end
    end

    for _,_request in pairs(self.pending) do
      local request=_request --#WAREHOUSE.Pendingitem
      if request.uid==id then
        return request, false
      end
    end

  end

  return nil,nil
end

--- Warehouse event function, handling the birth of a unit.
-- @param #WAREHOUSE self
-- @param Core.Event#EVENTDATA EventData Event data.
function WAREHOUSE:_OnEventBirth(EventData)
  self:T3(self.lid..string.format("Warehouse %s (id=%s) captured event birth!", self.alias, self.uid))

  if EventData and EventData.IniGroup then
    local group=EventData.IniGroup

    -- Note: Remember, group:IsAlive might(?) not return true here.
    local wid,aid,rid=self:_GetIDsFromGroup(group)

    if wid==self.uid then

      -- Get asset and request from id.
      local asset=self:GetAssetByID(aid)
      local request=self:GetRequestByID(rid)
            
      if asset and request then

        -- Debug message.
        self:T(self.lid..string.format("Warehouse %s captured event birth of request ID=%d, asset ID=%d, unit %s spawned=%s", self.alias, request.uid, asset.uid, EventData.IniUnitName, tostring(asset.spawned)))
        
        -- Set born to true.
        request.born=true
        
      else
        self:E(self.lid..string.format("ERROR: Either asset AID=%s or request RID=%s are nil in event birth of unit %s", tostring(aid), tostring(rid), tostring(EventData.IniUnitName)))
      end

    else
      --self:T3({wid=wid, uid=self.uid, match=(wid==self.uid), tw=type(wid), tu=type(self.uid)})
    end

  end
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function handling the event when a (warehouse) unit starts its engines.
-- @param #WAREHOUSE self
-- @param Core.Event#EVENTDATA EventData Event data.
function WAREHOUSE:_OnEventEngineStartup(EventData)
  self:T3(self.lid..string.format("Warehouse %s captured event engine startup!",self.alias))

  if EventData and EventData.IniGroup then
    local group=EventData.IniGroup
    local wid,aid,rid=self:_GetIDsFromGroup(group)
    if wid==self.uid then
      self:T(self.lid..string.format("Warehouse %s captured event engine startup of its asset unit %s.", self.alias, EventData.IniUnitName))
    end
  end
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function handling the event when a (warehouse) unit takes off.
-- @param #WAREHOUSE self
-- @param Core.Event#EVENTDATA EventData Event data.
function WAREHOUSE:_OnEventTakeOff(EventData)
  self:T3(self.lid..string.format("Warehouse %s captured event takeoff!",self.alias))

  if EventData and EventData.IniGroup then
    local group=EventData.IniGroup
    local wid,aid,rid=self:_GetIDsFromGroup(group)
    if wid==self.uid then
      self:T(self.lid..string.format("Warehouse %s captured event takeoff of its asset unit %s.", self.alias, EventData.IniUnitName))
    end
  end
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function handling the event when a (warehouse) unit lands.
-- @param #WAREHOUSE self
-- @param Core.Event#EVENTDATA EventData Event data.
function WAREHOUSE:_OnEventLanding(EventData)
  self:T3(self.lid..string.format("Warehouse %s captured event landing!", self.alias))

  if EventData and EventData.IniGroup then
    local group=EventData.IniGroup

    -- Try to get UIDs from group name.
    local wid,aid,rid=self:_GetIDsFromGroup(group)

    -- Check that this group belongs to this warehouse.
    if wid~=nil and wid==self.uid then

      -- Debug info.
      self:T(self.lid..string.format("Warehouse %s captured event landing of its asset unit %s.", self.alias, EventData.IniUnitName))

    end
  end
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function handling the event when a (warehouse) unit shuts down its engines.
-- @param #WAREHOUSE self
-- @param Core.Event#EVENTDATA EventData Event data.
function WAREHOUSE:_OnEventEngineShutdown(EventData)
  self:T3(self.lid..string.format("Warehouse %s captured event engine shutdown!", self.alias))

  if EventData and EventData.IniGroup then
    local group=EventData.IniGroup
    local wid,aid,rid=self:_GetIDsFromGroup(group)
    if wid==self.uid then
      self:T(self.lid..string.format("Warehouse %s captured event engine shutdown of its asset unit %s.", self.alias, EventData.IniUnitName))
    end
  end
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Arrived event if an air unit/group arrived at its destination. This can be an engine shutdown or a landing event.
-- @param #WAREHOUSE self
-- @param Core.Event#EVENTDATA EventData Event data table.
function WAREHOUSE:_OnEventArrived(EventData)

  if EventData and EventData.IniUnit then

    -- Unit that arrived.
    local unit=EventData.IniUnit

    -- Check if unit is alive and on the ground. Engine shutdown can also be triggered in other situations!
    if unit and unit:IsAlive()==true and unit:InAir()==false then

      -- Get group.
      local group=EventData.IniGroup

      -- Get unique IDs from group name.
      local wid,aid,rid=self:_GetIDsFromGroup(group)

      -- If all IDs are good we can assume it is a warehouse asset.
      if wid~=nil and aid~=nil and rid~=nil then

        -- Check that warehouse ID is right.
        if self.uid==wid then

          local request=self:_GetRequestOfGroup(group, self.pending)

          -- Better check that the request still exists, because for a group with more units, the
          if request then

            local istransport=self:_GroupIsTransport(group, request)

            -- Get closest airbase.
            local closest=group:GetCoordinate():GetClosestAirbase()

            -- Check if engine shutdown happend at right airbase because the event is also triggered in other situations.
            local rightairbase=closest:GetName()==request.warehouse:GetAirbase():GetName()

            -- Check that group is cargo and not transport.
            if istransport==false and rightairbase then

              -- Trigger arrived event for this group. Note that each unit of a group will trigger this event. So the onafterArrived function needs to take care of that.
              -- Actually, we only take the first unit of the group that arrives. If it does, we assume the whole group arrived, which might not be the case, since
              -- some units might still be taxiing or whatever. Therefore, we add 10 seconds for each additional unit of the group until the first arrived event is triggered.
              local nunits=#group:GetUnits()
              local dt=10*(nunits-1)+1  -- one unit = 1 sec, two units = 11 sec, three units = 21 sec before we call the group arrived.
              
              -- Debug info.
              if self.verbosity>=1 then
                local text=string.format("Air asset group %s from warehouse %s arrived at its destination. Trigger Arrived event in %d sec", group:GetName(), self.alias, dt)
                self:_InfoMessage(text)
              end              
              
              -- Arrived event.
              self:__Arrived(dt, group)
            end

          end
        end

      else
        self:T3(string.format("Group that arrived did not belong to a warehouse. Warehouse ID=%s, Asset ID=%s, Request ID=%s.", tostring(wid), tostring(aid), tostring(rid)))
      end
    end
  end

end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Warehouse event handling function.
-- @param #WAREHOUSE self
-- @param Core.Event#EVENTDATA EventData Event data.
function WAREHOUSE:_OnEventCrashOrDead(EventData)
  self:T3(self.lid..string.format("Warehouse %s captured event dead or crash!", self.alias))

  if EventData then

    -- Check if warehouse was destroyed. We compare the name of the destroyed unit.
    if EventData.IniUnitName then
      local warehousename=self.warehouse:GetName()
      if EventData.IniUnitName==warehousename then
        self:_DebugMessage(string.format("Warehouse %s alias %s was destroyed!", warehousename, self.alias))

        -- Trigger Destroyed event.
        self:Destroyed()
      end
      if self.airbase and self.airbasename and self.airbasename==EventData.IniUnitName then
        if self:IsRunwayOperational() then
          -- Trigger RunwayDestroyed event (only if it is not destroyed already)
          self:RunwayDestroyed()
        else
          -- Reset the time stamp.
          self.runwaydestroyed=timer.getAbsTime()
        end        
      end
    end

    -- Debug info.
    self:T2(self.lid..string.format("Warehouse %s captured event dead or crash or unit %s", self.alias, tostring(EventData.IniUnitName)))

    -- Check if an asset unit was destroyed.
    if EventData.IniGroup then

      -- Group initiating the event.
      local group=EventData.IniGroup

      -- Get warehouse, asset and request IDs from the group name.
      local wid,aid,rid=self:_GetIDsFromGroup(group)

      -- Check that we have the right warehouse.
      if wid==self.uid then

        -- Debug message.
        self:T(self.lid..string.format("Warehouse %s captured event dead or crash of its asset unit %s", self.alias, EventData.IniUnitName))

        -- Loop over all pending requests and get the one belonging to this unit.
        for _,request in pairs(self.pending) do
          local request=request --#WAREHOUSE.Pendingitem

          -- This is the right request.
          if request.uid==rid then

            -- Update cargo and transport group sets of this request. We need to know if this job is finished.
            self:_UnitDead(EventData.IniUnit, EventData.IniGroup, request)

          end
        end
      end
    end
  end
end

--- A unit of a group just died. Update group sets in request.
-- This is important in order to determine if a job is done and can be removed from the (pending) queue.
-- @param #WAREHOUSE self
-- @param Wrapper.Unit#UNIT deadunit Unit that died.
-- @param Wrapper.Group#GROUP deadgroup Group of unit that died.
-- @param #WAREHOUSE.Pendingitem request Request that needs to be updated.
function WAREHOUSE:_UnitDead(deadunit, deadgroup, request)
  --self:F(self.lid.."FF unit dead "..deadunit:GetName())

  -- Find opsgroup.
  local opsgroup=_DATABASE:FindOpsGroup(deadgroup)
  
  -- Check if we have an opsgroup.
  if opsgroup then  
    -- Handled in OPSGROUP:onafterDead() now.
    return nil  
  end

  -- Number of alive units in group.
  local nalive=deadgroup:CountAliveUnits()

  -- Whole group is dead?
  local groupdead=false
  if nalive>0 then
    groupdead=false
  else
    groupdead=true
  end
  
  -- Find asset.  
  local asset=self:FindAssetInDB(deadgroup)  

  -- Here I need to get rid of the #CARGO at the end to obtain the original name again!
  local unitname=self:_GetNameWithOut(deadunit)
  local groupname=self:_GetNameWithOut(deadgroup)

  -- Group is dead!
  if groupdead then
    -- Debug output.
    self:T(self.lid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(deadgroup,request))))
    if self.Debug then
      deadgroup:SmokeWhite()
    end
    -- Trigger AssetDead event.    
    self:AssetDead(asset, request)
  end


  -- Dont trigger a Remove event for the group sets.
  local NoTriggerEvent=true

  if request.transporttype~=WAREHOUSE.TransportType.SELFPROPELLED then

    ---
    -- Complicated case: Dead unit could be:
    -- 1.) A Cargo unit (e.g. waiting to be picked up).
    -- 2.) A Transport unit which itself holds cargo groups.
    ---

    if not asset.iscargo then

      -- Get the carrier unit table holding the cargo groups inside this carrier.
      local cargogroupnames=request.carriercargo[unitname]

      if cargogroupnames then

        -- Loop over all groups inside the destroyed carrier ==> all dead.
        for _,cargoname in pairs(cargogroupnames) do
          request.cargogroupset:Remove(cargoname, NoTriggerEvent)
          self:T(self.lid..string.format("Removed transported cargo %s inside dead carrier %s: ncargo=%d", cargoname, unitname, request.cargogroupset:Count()))
        end

      end

    else
      self:E(self.lid..string.format("ERROR: Group %s is neither cargo nor transport!", deadgroup:GetName()))
    end
  end

end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Warehouse event handling function.
-- Handles the case when the airbase associated with the warehous is captured.
-- @param #WAREHOUSE self
-- @param Core.Event#EVENTDATA EventData Event data.
function WAREHOUSE:_OnEventBaseCaptured(EventData)
  self:T3(self.lid..string.format("Warehouse %s captured event base captured!",self.alias))

  -- This warehouse does not have an airbase and never had one. So it could not have been captured.
  if self.airbasename==nil then
    return
  end

  if EventData and EventData.Place then

    -- Place is the airbase that was captured.
    local airbase=EventData.Place --Wrapper.Airbase#AIRBASE

    -- Check that this airbase belongs or did belong to this warehouse.
    if EventData.PlaceName==self.airbasename then

      -- New coalition of airbase after it was captured.
      local NewCoalitionAirbase=airbase:GetCoalition()

      -- Debug info
      self:T(self.lid..string.format("Airbase of warehouse %s (coalition ID=%d) was captured! New owner coalition ID=%d.",self.alias, self:GetCoalition(), NewCoalitionAirbase))

      -- So what can happen?
      -- Warehouse is blue, airbase is blue and belongs to warehouse and red captures it  ==> self.airbase=nil
      -- Warehouse is blue, airbase is blue self.airbase is nil and blue (re-)captures it ==> self.airbase=Event.Place
      if self.airbase==nil then
        -- New coalition is the same as of the warehouse ==> warehouse previously lost this airbase and now it was re-captured.
        if NewCoalitionAirbase == self:GetCoalition() then
          self:AirbaseRecaptured(NewCoalitionAirbase)
        end
      else
        -- Captured airbase belongs to this warehouse but was captured by other coalition.
        if NewCoalitionAirbase ~= self:GetCoalition() then
          self:AirbaseCaptured(NewCoalitionAirbase)
        end
      end

    end
  end
end

--- Warehouse event handling function.
-- Handles the case when the mission is ended.
-- @param #WAREHOUSE self
-- @param Core.Event#EVENTDATA EventData Event data.
function WAREHOUSE:_OnEventMissionEnd(EventData)
  self:T3(self.lid..string.format("Warehouse %s captured event mission end!",self.alias))

  if self.autosave then
    self:Save(self.autosavepath, self.autosavefile)
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Helper functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Checks if the warehouse zone was conquered by antoher coalition.
-- @param #WAREHOUSE self
function WAREHOUSE:_CheckConquered()

  -- Get coordinate and radius to check.
  local coord=self.zone:GetCoordinate()
  local radius=self.zone:GetRadius()

  -- Scan units in zone.
  local gotunits,_,_,units,_,_=coord:ScanObjects(radius, true, false, false)

  local Nblue=0
  local Nred=0
  local Nneutral=0

  local CountryBlue=nil
  local CountryRed=nil
  local CountryNeutral=nil

  if gotunits then
    -- Loop over all units.
    for _,_unit in pairs(units) do
      local unit=_unit --Wrapper.Unit#UNIT

      local distance=coord:Get2DDistance(unit:GetCoord())

      -- Filter only alive groud units. Also check distance again, because the scan routine might give some larger distances.
      if unit:IsGround() and unit:IsAlive() and distance <= radius then

        -- Get coalition and country.
        local _coalition=unit:GetCoalition()
        local _country=unit:GetCountry()

        -- Debug info.
        self:T2(self.lid..string.format("Unit %s in warehouse zone of radius=%d m. Coalition=%d, country=%d. Distance = %d m.",unit:GetName(), radius,_coalition,_country, distance))

        -- Add up units for each side.
        if _coalition==coalition.side.BLUE then
          Nblue=Nblue+1
          CountryBlue=_country
        elseif _coalition==coalition.side.RED then
          Nred=Nred+1
          CountryRed=_country
        else
          Nneutral=Nneutral+1
          CountryNeutral=_country
        end

      end
    end
  end

  -- Debug info.
  self:T(self.lid..string.format("Ground troops in warehouse zone: blue=%d, red=%d, neutral=%d", Nblue, Nred, Nneutral))


  -- Figure out the new coalition if any.
  -- Condition is that only units of one coalition are within the zone.
  local newcoalition=self:GetCoalition()
  local newcountry=self:GetCountry()
  if Nblue>0 and Nred==0 and Nneutral==0 then
    -- Only blue units in zone ==> Zone goes to blue.
    newcoalition=coalition.side.BLUE
    newcountry=CountryBlue
  elseif Nblue==0 and Nred>0 and Nneutral==0 then
    -- Only red units in zone ==> Zone goes to red.
    newcoalition=coalition.side.RED
    newcountry=CountryRed
  elseif Nblue==0 and Nred==0 and Nneutral>0 then
    -- Only neutral units in zone but neutrals do not attack or even capture!
    --newcoalition=coalition.side.NEUTRAL
    --newcountry=CountryNeutral
  end

  -- Coalition has changed ==> warehouse was captured! This should be before the attack check.
  if self:IsAttacked() and newcoalition ~= self:GetCoalition() then
    self:Captured(newcoalition, newcountry)
    return
  end

  -- Before a warehouse can be captured, it has to be attacked.
  -- That is, even if only enemy units are present it is not immediately captured in order to spawn all ground assets for defence.
  if self:GetCoalition()==coalition.side.BLUE then
    -- Blue warehouse is running and we have red units in the zone.
    if self:IsRunning() and Nred>0 then
      self:Attacked(coalition.side.RED, CountryRed)
    end
    -- Blue warehouse was under attack by blue but no more blue units in zone.
    if self:IsAttacked() and Nred==0 then
      self:Defeated()
    end
  elseif self:GetCoalition()==coalition.side.RED then
    -- Red Warehouse is running and we have blue units in the zone.
    if self:IsRunning() and Nblue>0 then
      self:Attacked(coalition.side.BLUE, CountryBlue)
    end
    -- Red warehouse was under attack by blue but no more blue units in zone.
    if self:IsAttacked() and Nblue==0 then
      self:Defeated()
    end
  elseif self:GetCoalition()==coalition.side.NEUTRAL then
    -- Neutrals dont attack!
    if self:IsRunning() and Nred>0 then
      self:Attacked(coalition.side.RED, CountryRed)
    elseif self:IsRunning() and Nblue>0 then
      self:Attacked(coalition.side.BLUE, CountryBlue)
    end
  end

end

--- Checks if the associated airbase still belongs to the warehouse.
-- @param #WAREHOUSE self
function WAREHOUSE:_CheckAirbaseOwner()
  -- The airbasename is set at start and not deleted if the airbase was captured.
  if self.airbasename then

    local airbase=AIRBASE:FindByName(self.airbasename)
    local airbasecurrentcoalition=airbase:GetCoalition()

    if self.airbase then

      -- Warehouse has lost its airbase.
      if self:GetCoalition()~=airbasecurrentcoalition then
        self.airbase=nil
      end

    else

      -- Warehouse has re-captured the airbase.
      if self:GetCoalition()==airbasecurrentcoalition then
        self.airbase=airbase
      end

    end

  end
end

--- Checks if the request can be fulfilled in general. If not, it is removed from the queue.
-- Check if departure and destination bases are of the right type.
-- @param #WAREHOUSE self
-- @param #table queue The queue which is holding the requests to check.
-- @return #boolean If true, request can be executed. If false, something is not right.
function WAREHOUSE:_CheckRequestConsistancy(queue)
  self:T3(self.lid..string.format("Number of queued requests = %d", #queue))

  -- Requests to delete.
  local invalid={}

  for _,_request in pairs(queue) do
    local request=_request --#WAREHOUSE.Queueitem

    -- Debug info.
    self:T2(self.lid..string.format("Checking request id=%d.", request.uid))

    -- Let's assume everything is fine.
    local valid=true

    -- Check if at least one asset was requested.
    if request.nasset==0 then
      self:E(self.lid..string.format("ERROR: INVALID request. Request for zero assets not possible. Can happen when, e.g. \"all\" ground assets are requests but none in stock."))
      valid=false
    end

    -- Request from enemy coalition?
    if self:GetCoalition()~=request.warehouse:GetCoalition() then
      self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is of wrong coalition! Own coalition %s != %s of requesting warehouse.", self:GetCoalitionName(), request.warehouse:GetCoalitionName()))
      valid=false
    end

    -- Is receiving warehouse stopped?
    if request.warehouse:IsStopped() then
      self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is stopped!"))
      valid=false
    end

    -- Is receiving warehouse destroyed?
    if request.warehouse:IsDestroyed() and not self.respawnafterdestroyed then
      self:E(self.lid..string.format("ERROR: INVALID request. Requesting warehouse is destroyed!"))
      valid=false
    end

    -- Add request as unvalid and delete it later.
    if valid==false then
      self:E(self.lid..string.format("Got invalid request id=%d.", request.uid))
      table.insert(invalid, request)
    else
      self:T3(self.lid..string.format("Got valid request id=%d.", request.uid))
    end
  end

  -- Delete invalid requests.
  for _,_request in pairs(invalid) do
    self:E(self.lid..string.format("Deleting INVALID request id=%d.",_request.uid))
    self:_DeleteQueueItem(_request, self.queue)
  end

end

--- Check if a request is valid in general. If not, it will be removed from the queue.
-- This routine needs to have at least one asset in stock that matches the request descriptor in order to determine whether the request category of troops.
-- If no asset is in stock, the request will remain in the queue but cannot be executed.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE.Queueitem request The request to be checked.
-- @return #boolean If true, request can be executed. If false, something is not right.
function WAREHOUSE:_CheckRequestValid(request)

  -- Check if number of requested assets is in stock.
  local _assets,_nassets,_enough=self:_FilterStock(self.stock, request.assetdesc, request.assetdescval, request.nasset)

  -- No assets in stock? Checks cannot be performed.
  if #_assets==0 then
    return true
  end

  -- Convert relative to absolute number if necessary.
  local nasset=request.nasset
  if type(request.nasset)=="string" then
    nasset=self:_QuantityRel2Abs(request.nasset,_nassets)
  end

  -- Debug check, request.nasset might be a string Quantity enumerator.
  local text=string.format("Request valid? Number of assets: requested=%s=%d, selected=%d, total=%d, enough=%s.", tostring(request.nasset), nasset,#_assets,_nassets, tostring(_enough))
  self:T(text)

  -- First asset. Is representative for all filtered items in stock.
  local asset=_assets[1] --#WAREHOUSE.Assetitem

  -- Asset is air, ground etc.
  local asset_plane  = asset.category==Group.Category.AIRPLANE
  local asset_helo   = asset.category==Group.Category.HELICOPTER
  local asset_ground = asset.category==Group.Category.GROUND
  local asset_train  = asset.category==Group.Category.TRAIN
  local asset_naval  = asset.category==Group.Category.SHIP

  -- General air request.
  local asset_air=asset_helo or asset_plane

  -- Assume everything is okay.
  local valid=true

  -- Category of the requesting warehouse airbase.
  local requestcategory=request.warehouse:GetAirbaseCategory()

  if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then
    -------------------------------------------
    -- Case where the units go my themselves --
    -------------------------------------------

    if asset_air then

      if asset_plane then

        -- No airplane to or from FARPS.
        if requestcategory==Airbase.Category.HELIPAD or self:GetAirbaseCategory()==Airbase.Category.HELIPAD then
          self:E("ERROR: Incorrect request. Asset airplane requested but warehouse or requestor is HELIPAD/FARP!")
          valid=false
        end

        -- Category SHIP is not general enough! Fighters can go to carriers. Which fighters, is there an attibute?
        -- Also for carriers, attibute?

      elseif asset_helo then

        -- Helos need a FARP or AIRBASE or SHIP for spawning. Also at the the receiving warehouse. So even if they could go there they "cannot" be spawned again.
        -- Unless I allow spawning of helos in the the spawn zone. But one should place at least a FARP there.
        if self:GetAirbaseCategory()==-1 or requestcategory==-1 then
          self:E("ERROR: Incorrect request. Helos need a AIRBASE/HELIPAD/SHIP as home/destination base!")
          valid=false
        end

      end

      -- All aircraft need an airbase of any type at depature and destination.
      if self.airbase==nil or request.airbase==nil then

        self:E("ERROR: Incorrect request. Either warehouse or requesting warehouse does not have any kind of airbase!")
        valid=false

      else

        -- Check if enough parking spots are available. This checks the spots available in general, i.e. not the free spots.
        -- TODO: For FARPS/ships, is it possible to send more assets than parking spots? E.g. a FARPS has only four (or even one).
        -- TODO: maybe only check if spots > 0 for the necessary terminal type? At least for FARPS.

        -- Get necessary terminal type.
        local termtype_dep=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory())
        local termtype_des=asset.terminalType or self:_GetTerminal(asset.attribute, request.warehouse:GetAirbaseCategory())

        -- Get number of parking spots.
        local np_departure=self.airbase:GetParkingSpotsNumber(termtype_dep)
        local np_destination=request.airbase:GetParkingSpotsNumber(termtype_des)

        -- Debug info.
        self:T(string.format("Asset attribute = %s, DEPARTURE: terminal type = %d, spots = %d, DESTINATION: terminal type = %d, spots = %d", asset.attribute, termtype_dep, np_departure, termtype_des, np_destination))

        -- Not enough parking at sending warehouse.
        --if (np_departure < request.nasset) and not (self.category==Airbase.Category.SHIP or self.category==Airbase.Category.HELIPAD) then
        if np_departure < nasset then
          self:E(string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype_dep, np_departure, nasset))
          valid=false
        end

        -- No parking at requesting warehouse.
        if np_destination == 0 then
          self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse. Available spots = %d!", termtype_des, np_destination))
          valid=false
        end

      end

    elseif asset_ground then

      -- Check that both spawn zones are not in water.
      local inwater=self.spawnzone:GetCoordinate():IsSurfaceTypeWater() or request.warehouse.spawnzone:GetCoordinate():IsSurfaceTypeWater()

      if inwater and not request.lateActivation then
        self:E("ERROR: Incorrect request. Ground asset requested but at least one spawn zone is in water!")
        return false
      end

      -- No ground assets directly to or from ships.
      -- TODO: May needs refinement if warehouse is on land and requestor is ship in harbour?!
      --if (requestcategory==Airbase.Category.SHIP or self:GetAirbaseCategory()==Airbase.Category.SHIP) then
      --  self:E("ERROR: Incorrect request. Ground asset requested but warehouse or requestor is SHIP!")
      --  valid=false
      --end

      if asset_train then

        -- Check if there is a valid path on rail.
        local hasrail=self:HasConnectionRail(request.warehouse)
        if not hasrail then
          self:E("ERROR: Incorrect request. No valid path on rail for train assets!")
          valid=false
        end

      else

        if self.warehouse:GetName()~=request.warehouse.warehouse:GetName() then

          -- Check if there is a valid path on road.
          local hasroad=self:HasConnectionRoad(request.warehouse)

          -- Check if there is a valid off road path.
          local hasoffroad=self:HasConnectionOffRoad(request.warehouse)

          if not (hasroad or hasoffroad) then
            self:E("ERROR: Incorrect request. No valid path on or off road for ground assets!")
            valid=false
          end

        end

      end

    elseif asset_naval then

        -- Check shipping lane.
        local shippinglane=self:HasConnectionNaval(request.warehouse)

        if not shippinglane then
          self:E("ERROR: Incorrect request. No shipping lane has been defined between warehouses!")
          valid=false
        end

    end

  else
    -------------------------------
    -- Assests need a transport ---
    -------------------------------

    if request.transporttype==WAREHOUSE.TransportType.AIRPLANE then

      -- Airplanes only to AND from airdromes.
      if self:GetAirbaseCategory()~=Airbase.Category.AIRDROME or requestcategory~=Airbase.Category.AIRDROME then
        self:E("ERROR: Incorrect request. Warehouse or requestor does not have an airdrome. No transport by plane possible!")
        valid=false
      end

      --TODO: Not sure if there are any transport planes that can land on a carrier?

    elseif request.transporttype==WAREHOUSE.TransportType.APC then

      -- Transport by ground units.

      -- No transport to or from ships
      if self:GetAirbaseCategory()==Airbase.Category.SHIP or requestcategory==Airbase.Category.SHIP then
        self:E("ERROR: Incorrect request. Warehouse or requestor is SHIP. No transport by APC possible!")
        valid=false
      end

      -- Check if there is a valid path on road.
      local hasroad=self:HasConnectionRoad(request.warehouse)
      if not hasroad then
        self:E("ERROR: Incorrect request. No valid path on road for ground transport assets!")
        valid=false
      end

    elseif request.transporttype==WAREHOUSE.TransportType.HELICOPTER then

      -- Transport by helicopters ==> need airbase for spawning but not for delivering to the spawn zone of the receiver.
      if self:GetAirbaseCategory()==-1 then
        self:E("ERROR: Incorrect request. Warehouse has no airbase. Transport by helicopter not possible!")
        valid=false
      end

    elseif request.transporttype==WAREHOUSE.TransportType.SHIP or request.transporttype==WAREHOUSE.TransportType.AIRCRAFTCARRIER 
        or request.transporttype==WAREHOUSE.TransportType.ARMEDSHIP or request.transporttype==WAREHOUSE.TransportType.WARSHIP then

      -- Transport by ship.
      local shippinglane=self:HasConnectionNaval(request.warehouse)

      if not shippinglane then
        self:E("ERROR: Incorrect request. No shipping lane has been defined between warehouses!")
        valid=false
      end

    elseif request.transporttype==WAREHOUSE.TransportType.TRAIN then

      -- Transport by train.
      self:E("ERROR: Incorrect request. Transport by TRAIN not implemented yet!")
      valid=false

    else
      -- No match.
      self:E("ERROR: Incorrect request. Transport type unknown!")
      valid=false
    end

    -- Airborne assets: check parking situation.
    if request.transporttype==WAREHOUSE.TransportType.AIRPLANE or request.transporttype==WAREHOUSE.TransportType.HELICOPTER then

      -- Check if number of requested assets is in stock.
      local _assets,_nassets,_enough=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype, request.ntransport, true)

      -- Convert relative to absolute number if necessary.
      local nasset=request.ntransport
      if type(request.ntransport)=="string" then
        nasset=self:_QuantityRel2Abs(request.ntransport,_nassets)
      end

      -- Debug check, request.nasset might be a string Quantity enumerator.
      local text=string.format("Request valid? Number of transports: requested=%s=%d, selected=%d, total=%d, enough=%s.", tostring(request.ntransport), nasset,#_assets,_nassets, tostring(_enough))
      self:T(text)

      -- Get necessary terminal type for helos or transport aircraft.
      local termtype=self:_GetTerminal(request.transporttype, self:GetAirbaseCategory())

      -- Get number of parking spots.
      local np_departure=self.airbase:GetParkingSpotsNumber(termtype)

      -- Debug info.
      self:T(self.lid..string.format("Transport attribute = %s, terminal type = %d, spots at departure = %d.", request.transporttype, termtype, np_departure))

      -- Not enough parking at sending warehouse.
      --if (np_departure < request.nasset) and not (self.category==Airbase.Category.SHIP or self.category==Airbase.Category.HELIPAD) then
      if np_departure < nasset then
        self:E(self.lid..string.format("ERROR: Incorrect request. Not enough parking spots of terminal type %d at warehouse. Available spots %d < %d necessary.", termtype, np_departure, nasset))
        valid=false
      end

      -- Planes also need parking at the receiving warehouse.
      if request.transporttype==WAREHOUSE.TransportType.AIRPLANE then

        -- Total number of parking spots for transport planes at destination.
        termtype=self:_GetTerminal(request.transporttype, request.warehouse:GetAirbaseCategory())
        local np_destination=request.airbase:GetParkingSpotsNumber(termtype)

        -- Debug info.
        self:T(self.lid..string.format("Transport attribute = %s: total # of spots (type=%d) at destination = %d.", asset.attribute, termtype, np_destination))

        -- No parking at requesting warehouse.
        if np_destination == 0 then
          self:E(string.format("ERROR: Incorrect request. No parking spots of terminal type %d at requesting warehouse for transports. Available spots = %d!", termtype, np_destination))
          valid=false
        end
      end

    end


  end

  -- Add request as unvalid and delete it later.
  if valid==false then
    self:E(self.lid..string.format("ERROR: Got invalid request id=%d.", request.uid))
  else
    self:T3(self.lid..string.format("Request id=%d valid :)", request.uid))
  end

  return valid
end


--- Checks if the request can be fulfilled right now.
-- Check for current parking situation, number of assets and transports currently in stock.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE.Queueitem request The request to be checked.
-- @return #boolean If true, request can be executed. If false, something is not right.
function WAREHOUSE:_CheckRequestNow(request)

  -- Check if receiving warehouse is running. We do allow self requests if the warehouse is under attack though!
  if (request.warehouse:IsRunning()==false) and not (request.toself and self:IsAttacked()) then
    local text=string.format("Warehouse %s: Request denied! Receiving warehouse %s is not running. Current state %s.", self.alias, request.warehouse.alias, request.warehouse:GetState())
    self:_InfoMessage(text, 5)

    return false
  end

  -- If no transport is requested, assets need to be mobile unless it is a self request.
  local onlymobile=false
  if type(request.ntransport)=="number" and request.ntransport==0 and not request.toself then
    onlymobile=true
  end

  -- Check if number of requested assets is in stock.
  local _assets,_nassets,_enough=self:_FilterStock(self.stock, request.assetdesc, request.assetdescval, request.nasset, onlymobile)


  -- Check if enough assets are in stock.
  if not _enough then
    local text=string.format("Warehouse %s: Request ID=%d denied! Not enough (cargo) assets currently available.", self.alias, request.uid)
    self:_InfoMessage(text, 5)
    text=string.format("Enough=%s, #assets=%d, nassets=%d, request.nasset=%s", tostring(_enough), #_assets,_nassets, tostring(request.nasset))
    self:T(self.lid..text)
    return false
  end

  local _transports
  local _assetattribute
  local _assetcategory
  local _assetairstart=false

  -- Check if at least one (cargo) asset is available.
  if _nassets>0 then
  
    local asset=_assets[1] --#WAREHOUSE.Assetitem

    -- Get the attibute of the requested asset.
    _assetattribute=_assets[1].attribute
    _assetcategory=_assets[1].category
    _assetairstart=_assets[1].takeoffType and _assets[1].takeoffType==COORDINATE.WaypointType.TurningPoint or false

    -- Check available parking for air asset units.
    if _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then
    
      if self.airbase and self.airbase:GetCoalition()==self:GetCoalition() then
      
        -- Check if DCS warehouse of airbase has enough assets        
        if self.airbase.storage then
          local nS=self.airbase.storage:GetAmount(asset.unittype)
          local nA=asset.nunits*request.nasset  -- Number of units requested
          if nS<nA then
            local text=string.format("Warehouse %s: Request denied! DCS Warehouse has only %d assets of type %s ==> NOT enough to spawn the requested %d asset units (%d groups)", 
            self.alias, nS, asset.unittype, nA, request.nasset)
            self:_InfoMessage(text, 5)            
            return false
          end
        end
        
    
        if self:IsRunwayOperational() or _assetairstart then
  
          if _assetairstart then
            -- Airstart no need to check parking
          else
          
            -- Check parking.
            local Parking=self:_FindParkingForAssets(self.airbase,_assets)
      
            -- No parking?
            if Parking==nil then
              local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all requested assets at the moment.", self.alias)
              self:_InfoMessage(text, 5)
              return false
            end
          end
          
        else
          -- Runway destroyed.
          local text=string.format("Warehouse %s: Request denied! Runway is still destroyed", self.alias)
          self:_InfoMessage(text, 5)
          return false                
        end
        
      else
      
        -- No airbase!
        local text=string.format("Warehouse %s: Request denied! No airbase", self.alias)
        self:_InfoMessage(text, 5)
        return false                
      
      end

    end

    -- Add this here or gettransport fails
    request.cargoassets=_assets

  end

  -- Check that a transport units.
  if request.transporttype ~= WAREHOUSE.TransportType.SELFPROPELLED then

    -- Get best transports for this asset pack.
    _transports=self:_GetTransportsForAssets(request)

    -- Check if at least one transport asset is available.
    if #_transports>0 then

      -- Get the attibute of the transport units.
      local _transportattribute=_transports[1].attribute
      local _transportcategory=_transports[1].category

      -- Check available parking for transport units.
      if _transportcategory==Group.Category.AIRPLANE or _transportcategory==Group.Category.HELICOPTER then
      
        if self.airbase and self.airbase:GetCoalition()==self:GetCoalition() then
        
          if self:IsRunwayOperational() then        
      
            local Parking=self:_FindParkingForAssets(self.airbase,_transports)
            
            -- No parking ==> return false
            if Parking==nil then           
              local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all transports at the moment.", self.alias)
              self:_InfoMessage(text, 5)  
              return false
            end
            
          else
          
             -- Runway destroyed.
            local text=string.format("Warehouse %s: Request denied! Runway is still destroyed", self.alias)
            self:_InfoMessage(text, 5)
            return false
                                     
          end
          
        else
          -- No airbase
          local text=string.format("Warehouse %s: Request denied! No airbase currently!", self.alias)
          self:_InfoMessage(text, 5)  
          return false        
        end
          
      end

    else

      -- Not enough or the right transport carriers.
      local text=string.format("Warehouse %s: Request denied! Not enough transport carriers available at the moment.", self.alias)
      self:_InfoMessage(text, 5)

      return false
    end

  else

    ---
    -- Self propelled case
    ---

    -- Ground asset checks.
    if _assetcategory==Group.Category.GROUND then

      -- Distance between warehouse and spawn zone.
      local dist=self.warehouse:GetCoordinate():Get2DDistance(self.spawnzone:GetCoordinate())

      -- Check min dist to spawn zone.
      if dist>self.spawnzonemaxdist then
        -- Not close enough to spawn zone.
        local text=string.format("Warehouse %s: Request denied! Not close enough to spawn zone. Distance = %d m. We need to be at least within %d m range to spawn.", self.alias, dist, self.spawnzonemaxdist)
        self:_InfoMessage(text, 5)
        return false
      end
      
    elseif _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then


    end

  end


  -- Set chosen cargo assets.
  request.cargoassets=_assets
  request.cargoattribute=_assets[1].attribute
  request.cargocategory=_assets[1].category
  request.nasset=#_assets

  -- Debug info:
  local text=string.format("Selected cargo assets, attibute=%s, category=%d:\n", request.cargoattribute, request.cargocategory)
  for _i,_asset in pairs(_assets) do
    local asset=_asset --#WAREHOUSE.Assetitem
    text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d",_i, asset.templatename, asset.unittype, asset.category, asset.nunits)
  end
  self:T(self.lid..text)

  if request.transporttype ~= WAREHOUSE.TransportType.SELFPROPELLED then

    -- Set chosen transport assets.
    request.transportassets=_transports
    request.transportattribute=_transports[1].attribute
    request.transportcategory=_transports[1].category
    request.ntransport=#_transports

    -- Debug info:
    local text=string.format("Selected transport assets, attibute=%s, category=%d:\n", request.transportattribute, request.transportcategory)
    for _i,_asset in pairs(_transports) do
      local asset=_asset --#WAREHOUSE.Assetitem
      text=text..string.format("%d) name=%s, type=%s, category=%d, #units=%d\n",_i, asset.templatename, asset.unittype, asset.category, asset.nunits)
    end
    self:T(self.lid..text)

  end

  return true
end

---Get (optimized) transport carriers for the given assets to be transported.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE.Pendingitem Chosen request.
function WAREHOUSE:_GetTransportsForAssets(request)

  -- Get all transports of the requested type in stock.
  local transports=self:_FilterStock(self.stock, WAREHOUSE.Descriptor.ATTRIBUTE, request.transporttype, nil, true)

  -- Copy asset.
  local cargoassets=UTILS.DeepCopy(request.cargoassets)
  local cargoset=request.transportcargoset

  -- TODO: Get weight and cargo bay from CARGO_GROUP
  --local cargogroup=CARGO_GROUP:New(CargoGroup,Type,Name,LoadRadius,NearRadius)
  --cargogroup:GetWeight()

  -- Sort transport carriers w.r.t. cargo bay size.
  local function sort_transports(a,b)
    return a.cargobaymax>b.cargobaymax
  end

  -- Sort cargo assets w.r.t. weight in assending order.
  local function sort_cargoassets(a,b)
    return a.weight>b.weight
  end

  -- Sort tables.
  table.sort(transports, sort_transports)
  table.sort(cargoassets, sort_cargoassets)

  -- Total cargo bay size of all groups.
  self:T2(self.lid.."Transport capability:")
  local totalbay=0
  for i=1,#transports do
    local transport=transports[i] --#WAREHOUSE.Assetitem
    for j=1,transport.nunits do
      totalbay=totalbay+transport.cargobay[j]
      self:T2(self.lid..string.format("Cargo bay = %d  (unit=%d)", transport.cargobay[j], j))
    end
  end
  self:T2(self.lid..string.format("Total capacity = %d", totalbay))

  -- Total cargo weight of all assets to transports.
  self:T2(self.lid.."Cargo weight:")
  local totalcargoweight=0
  for i=1,#cargoassets do
    local asset=cargoassets[i] --#WAREHOUSE.Assetitem
    totalcargoweight=totalcargoweight+asset.weight
    self:T2(self.lid..string.format("weight = %d", asset.weight))
  end
  self:T2(self.lid..string.format("Total weight = %d", totalcargoweight))

  -- Transports used.
  local used_transports={}

  -- Loop over all transport groups, largest cargobaymax to smallest.
  for i=1,#transports do

    -- Shortcut for carrier and cargo bay
    local transport=transports[i]

    -- Cargo put into carrier.
    local putintocarrier={}

    -- Cargo assigned to this transport group?
    local used=false

    -- Loop over all units
    for k=1,transport.nunits do

      -- Get cargo bay of this carrier.
      local cargobay=transport.cargobay[k]

      -- Loop over cargo assets.
      for j,asset in pairs(cargoassets) do
        local asset=asset --#WAREHOUSE.Assetitem

        -- How many times does the cargo fit into the carrier?
        local delta=cargobay-asset.weight
        --env.info(string.format("k=%d, j=%d delta=%d  cargobay=%d  weight=%d", k, j, delta, cargobay, asset.weight))

        --self:E(self.lid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight))

        -- Cargo fits into carrier
        if delta>=0 then
          -- Reduce remaining cargobay.
          cargobay=cargobay-asset.weight
          self:T3(self.lid..string.format("%s unit %d loads cargo uid=%d: bayempty=%02d, bayloaded = %02d - weight=%02d", transport.templatename, k, asset.uid, transport.cargobay[k], cargobay, asset.weight))

          -- Remember this cargo and remove it so it does not get loaded into other carriers.
          table.insert(putintocarrier, j)

          -- This transport group is used.
          used=true
        else
          self:T2(self.lid..string.format("Carrier unit %s too small for cargo asset %s ==> cannot be used! Cargo bay - asset weight = %d kg", transport.templatename, asset.templatename, delta))
        end

      end -- loop over assets
    end   -- loop over units

    -- Remove cargo assets from list. Needs to be done back-to-front in order not to confuse the loop.
    for j=#putintocarrier,1, -1 do

      local nput=putintocarrier[j]
      local cargo=cargoassets[nput]

      -- Need to check if multiple units in a group and the group has already been removed!
      -- TODO: This might need to be improved but is working okay so far.
      if cargo then
        -- Remove this group because it was used.
        self:T2(self.lid..string.format("Cargo id=%d assigned for carrier id=%d", cargo.uid, transport.uid))
        table.remove(cargoassets, nput)
      end
    end

    -- Cargo was assined for this carrier.
    if used then
      table.insert(used_transports, transport)
    end

    -- Convert relative quantity (all, half) to absolute number if necessary.
    local ntrans=self:_QuantityRel2Abs(request.ntransport, #transports)

    -- Max number of transport groups reached?
    if #used_transports >= ntrans then
      request.ntransport=#used_transports
      break
    end
  end

  -- Debug info.
  local text=string.format("Used Transports for request %d to warehouse %s:\n", request.uid, request.warehouse.alias)
  local totalcargobay=0
  for _i,_transport in pairs(used_transports) do
    local transport=_transport --#WAREHOUSE.Assetitem
    text=text..string.format("%d) %s: cargobay tot = %d kg, cargobay max = %d kg, nunits=%d\n", _i, transport.unittype, transport.cargobaytot, transport.cargobaymax, transport.nunits)
    totalcargobay=totalcargobay+transport.cargobaytot
    --for _,cargobay in pairs(transport.cargobay) do
    --  env.info(string.format("cargobay %d", cargobay))
    --end
  end
  text=text..string.format("Total cargo bay capacity = %.1f kg\n", totalcargobay)
  text=text..string.format("Total cargo weight       = %.1f kg\n", totalcargoweight)
  text=text..string.format("Minimum number of runs   = %.1f", totalcargoweight/totalcargobay)
  self:_DebugMessage(text)

  return used_transports
end

---Relative to absolute quantity.
-- @param #WAREHOUSE self
-- @param #string relative Relative number in terms of @{#WAREHOUSE.Quantity}.
-- @param #number ntot Total number.
-- @return #number Absolute number.
function WAREHOUSE:_QuantityRel2Abs(relative, ntot)

  local nabs=0

  -- Handle string input for nmax.
  if type(relative)=="string" then
    if relative==WAREHOUSE.Quantity.ALL then
      nabs=ntot
    elseif relative==WAREHOUSE.Quantity.THREEQUARTERS then
      nabs=UTILS.Round(ntot*3/4)
    elseif relative==WAREHOUSE.Quantity.HALF then
      nabs=UTILS.Round(ntot/2)
    elseif relative==WAREHOUSE.Quantity.THIRD then
      nabs=UTILS.Round(ntot/3)
    elseif relative==WAREHOUSE.Quantity.QUARTER then
      nabs=UTILS.Round(ntot/4)
    else
      nabs=math.min(1, ntot)
    end
  else
    nabs=relative
  end

  self:T2(self.lid..string.format("Relative %s: tot=%d, abs=%.2f", tostring(relative), ntot, nabs))

  return nabs
end

---Sorts the queue and checks if the request can be fulfilled.
-- @param #WAREHOUSE self
-- @return #WAREHOUSE.Queueitem Chosen request.
function WAREHOUSE:_CheckQueue()

  -- Sort queue wrt to first prio and then qid.
  self:_SortQueue()

  -- Search for a request we can execute.
  local request=nil --#WAREHOUSE.Queueitem

  local invalid={}
  local gotit=false
  for _,_qitem in ipairs(self.queue) do
    local qitem=_qitem --#WAREHOUSE.Queueitem

    -- Check if request is valid in general.
    local valid=self:_CheckRequestValid(qitem)

    -- Check if request is possible now.
    local okay=false
    if valid then    
      okay=self:_CheckRequestNow(qitem)
    else
      -- Remember invalid request and delete later in order not to confuse the loop.
      table.insert(invalid, qitem)
    end

    -- Get the first valid request that can be executed now.
    if okay and valid and not gotit then
      request=qitem
      gotit=true
      break
    end
  end

  -- Delete invalid requests.
  for _,_request in pairs(invalid) do
    self:T(self.lid..string.format("Deleting invalid request id=%d.",_request.uid))
    self:_DeleteQueueItem(_request, self.queue)
  end

  -- Execute request.
  return request
end

--- Simple task function. Can be used to call a function which has the warehouse and the executing group as parameters.
-- @param #WAREHOUSE self
-- @param #string Function The name of the function to call passed as string.
-- @param Wrapper.Group#GROUP group The group which is meant.
function WAREHOUSE:_SimpleTaskFunction(Function, group)
  self:F2({Function})

  -- Name of the warehouse (static) object.
  local warehouse=self.warehouse:GetName()
  local groupname=group:GetName()

  -- Task script.
  local DCSScript = {}

  DCSScript[#DCSScript+1]   = string.format('local mygroup     = GROUP:FindByName(\"%s\") ', groupname)               -- The group that executes the task function. Very handy with the "...".
  if self.isUnit then
    DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse)                -- The unit that holds the warehouse self object.
  else
    DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse)              -- The static that holds the warehouse self object.
  end
  DCSScript[#DCSScript+1]   = string.format('local warehouse   = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ')  -- Get the warehouse self object from the static.
  DCSScript[#DCSScript+1]   = string.format('%s(mygroup)', Function)                                                  -- Call the function, e.g. myfunction.(warehouse,mygroup)

  -- Create task.
  local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript)))

  return DCSTask
end

--- Simple task function. Can be used to call a function which has the warehouse and the executing group as parameters.
-- @param #WAREHOUSE self
-- @param #string Function The name of the function to call passed as string.
-- @param Wrapper.Group#GROUP group The group which is meant.
-- @param #number n Waypoint passed.
-- @param #number N Final waypoint number.
function WAREHOUSE:_SimpleTaskFunctionWP(Function, group, n, N)
  self:F2({Function})

  -- Name of the warehouse (static) object.
  local warehouse=self.warehouse:GetName()
  local groupname=group:GetName()

  -- Task script.
  local DCSScript = {}

  DCSScript[#DCSScript+1]   = string.format('local mygroup     = GROUP:FindByName(\"%s\") ', groupname)               -- The group that executes the task function. Very handy with the "...".
  if self.isUnit then
    DCSScript[#DCSScript+1] = string.format("local mywarehouse = UNIT:FindByName(\"%s\") ", warehouse)                -- The unit that holds the warehouse self object.
  else
    DCSScript[#DCSScript+1] = string.format("local mywarehouse = STATIC:FindByName(\"%s\") ", warehouse)              -- The static that holds the warehouse self object.
  end
  DCSScript[#DCSScript+1]   = string.format('local warehouse   = mywarehouse:GetState(mywarehouse, \"WAREHOUSE\") ')  -- Get the warehouse self object from the static.
  DCSScript[#DCSScript+1]   = string.format('%s(mygroup, %d, %d)', Function, n ,N)                                    -- Call the function, e.g. myfunction.(warehouse,mygroup)

  -- Create task.
  local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript)))

  return DCSTask
end

--- Get the proper terminal type based on generalized attribute of the group.
--@param #WAREHOUSE self
--@param #WAREHOUSE.Attribute _attribute Generlized attibute of unit.
--@param #number _category Airbase category.
--@return Wrapper.Airbase#AIRBASE.TerminalType Terminal type for this group.
function WAREHOUSE:_GetTerminal(_attribute, _category)

  -- Default terminal is "large".
  local _terminal=AIRBASE.TerminalType.OpenBig

  if _attribute==WAREHOUSE.Attribute.AIR_FIGHTER or _attribute==WAREHOUSE.Attribute.AIR_UAV then
    -- Fighter ==> small.
    _terminal=AIRBASE.TerminalType.FighterAircraft
  elseif _attribute==WAREHOUSE.Attribute.AIR_BOMBER or _attribute==WAREHOUSE.Attribute.AIR_TRANSPORTPLANE or _attribute==WAREHOUSE.Attribute.AIR_TANKER or _attribute==WAREHOUSE.Attribute.AIR_AWACS then
    -- Bigger aircraft.
    _terminal=AIRBASE.TerminalType.OpenBig
  elseif _attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO then
    -- Helicopter.
    _terminal=AIRBASE.TerminalType.HelicopterUsable
  else
    --_terminal=AIRBASE.TerminalType.OpenMedOrBig
  end

  -- For ships, we allow medium spots for all fixed wing aircraft. There are smaller tankers and AWACS aircraft that can use a carrier.
  if _category==Airbase.Category.SHIP then
    if not (_attribute==WAREHOUSE.Attribute.AIR_TRANSPORTHELO or _attribute==WAREHOUSE.Attribute.AIR_ATTACKHELO) then
      _terminal=AIRBASE.TerminalType.OpenMedOrBig
    end
  end

  return _terminal
end


--- Seach unoccupied parking spots at the airbase for a list of assets. For each asset group a list of parking spots is returned.
-- During the search also the not yet spawned asset aircraft are considered.
-- If not enough spots for all asset units could be found, the routine returns nil!
-- @param #WAREHOUSE self
-- @param Wrapper.Airbase#AIRBASE airbase The airbase where we search for parking spots.
-- @param #table assets A table of assets for which the parking spots are needed.
-- @return #table Table of coordinates and terminal IDs of free parking spots. Each table entry has the elements .Coordinate and .TerminalID.
function WAREHOUSE:_FindParkingForAssets(airbase, assets)

  -- Init default
  local scanradius=25
  local scanunits=true
  local scanstatics=true
  local scanscenery=false
  local verysafe=false

  -- Function calculating the overlap of two (square) objects.
  local function _overlap(l1,l2,dist)
    local safedist=(l1/2+l2/2)*1.05  -- 5% safety margine added to safe distance!
    local safe = (dist > safedist)
    self:T3(string.format("l1=%.1f l2=%.1f s=%.1f d=%.1f ==> safe=%s", l1,l2,safedist,dist,tostring(safe)))
    return safe
  end

  -- Get client coordinates.
  local function _clients()
    local coords={}
    if not self.allowSpawnOnClientSpots then  
      local clients=_DATABASE.CLIENTS
      for clientname, client in pairs(clients) do
        local template=_DATABASE:GetGroupTemplateFromUnitName(clientname)
        if template then
          local units=template.units
          for i,unit in pairs(units) do
            local coord=COORDINATE:New(unit.x, unit.alt, unit.y)
            coords[unit.name]=coord
          end
        end
      end
    end
    return coords
  end

  -- Get parking spot data table. This contains all free and "non-free" spots.
  local parkingdata=airbase.parking --airbase:GetParkingSpotsTable()
  
  ---
  -- Find all obstacles
  ---

  -- List of obstacles.
  local obstacles={}
  
  -- Check all clients. Clients dont change so we can put that out of the loop.
  self.clientcoords=self.clientcoords or _clients()
  for clientname,_coord in pairs(self.clientcoords) do
    table.insert(obstacles, {coord=_coord, size=15, name=clientname, type="client"})
  end

  -- Loop over all parking spots and get the currently present obstacles.
  -- How long does this take on very large airbases, i.e. those with hundereds of parking spots? Seems to be okay!
  for _,parkingspot in pairs(parkingdata) do

    -- Coordinate of the parking spot.
    local _spot=parkingspot.Coordinate   -- Core.Point#COORDINATE
    local _termid=parkingspot.TerminalID

    -- Scan a radius of 100 meters around the spot.
    local _,_,_,_units,_statics,_sceneries=_spot:ScanObjects(scanradius, scanunits, scanstatics, scanscenery)

    -- Check all units.
    for _,_unit in pairs(_units) do
      local unit=_unit --Wrapper.Unit#UNIT
      local _coord=unit:GetVec3()
      local _size=self:_GetObjectSize(unit:GetDCSObject())
      local _name=unit:GetName()
      if unit and unit:IsAlive() then
        table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"})
      end
    end

    -- Check all statics.
    for _,static in pairs(_statics) do
      local _coord=static:getPoint()
      --local _coord=COORDINATE:NewFromVec3(_vec3)
      local _name=static:getName()
      local _size=self:_GetObjectSize(static)
      table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="static"})
    end

    -- Check all scenery.
    for _,scenery in pairs(_sceneries) do
      local _coord=scenery:getPoint()
      --local _coord=COORDINATE:NewFromVec3(_vec3)
      local _name=scenery:getTypeName()
      local _size=self:_GetObjectSize(scenery)
      table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="scenery"})
    end

  end
  
  ---
  -- Get Parking Spots
  ---

  -- Parking data for all assets.
  local parking={}

  -- Loop over all assets that need a parking psot.
  for _,asset in pairs(assets) do
    local _asset=asset --#WAREHOUSE.Assetitem
    
    if not _asset.spawned then

      -- Get terminal type of this asset
      local terminaltype=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory())
  
      -- Asset specific parking.
      parking[_asset.uid]={}
  
      -- Loop over all units - each one needs a spot.
      for i=1,_asset.nunits do
      
        -- Asset name
        local assetname=_asset.spawngroupname.."-"..tostring(i)
  
        -- Loop over all parking spots.
        local gotit=false
        for _,_parkingspot in pairs(parkingdata) do
          local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot
          
          -- Parking valid?
          local valid=true
          
          if asset.parkingIDs then
            -- If asset has assigned parking spots, we take these no matter what.
            valid=self:_CheckParkingAsset(parkingspot, asset)
          else
  
            -- Valid terminal type depending on attribute.
            local validTerminal=AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype)
            
            -- Valid parking list.
            local validParking=self:_CheckParkingValid(parkingspot)
            
            -- Black and white list.
            local validBWlist=airbase:_CheckParkingLists(parkingspot.TerminalID)        
  
            -- Debug info.
            --env.info(string.format("FF validTerminal = %s", tostring(validTerminal)))
            --env.info(string.format("FF validParking  = %s", tostring(validParking)))
            --env.info(string.format("FF validBWlist   = %s", tostring(validBWlist)))
          
            -- Check if all are true
            valid=validTerminal and validParking and validBWlist
          end
          
  
          -- Check correct terminal type for asset. We don't want helos in shelters etc.
          if valid then
  
            -- Coordinate of the parking spot.
            local _spot=parkingspot.Coordinate   -- Core.Point#COORDINATE
            local _termid=parkingspot.TerminalID
            local free=true
            local problem=nil
  
            -- Loop over all obstacles.
            for _,obstacle in pairs(obstacles) do
  
              -- Check if aircraft overlaps with any obstacle.
              local dist=_spot:Get2DDistance(obstacle.coord)
              local safe=_overlap(_asset.size, obstacle.size, dist)
  
              -- Spot is blocked.
              if not safe then
                self:T3(self.lid..string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", assetname, _asset.uid, _termid, dist))
                free=false
                problem=obstacle
                problem.dist=dist
                break
              else
                --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", assetname, _asset.uid, _termid, dist))
              end
  
            end
  
            -- Check if spot is free
            if free then
  
              -- Add parkingspot for this asset unit.
              table.insert(parking[_asset.uid], parkingspot)
  
              -- Debug
              self:T(self.lid..string.format("Parking spot %d is free for asset %s [id=%d]!", _termid, assetname, _asset.uid))
  
              -- Add the unit as obstacle so that this spot will not be available for the next unit.
              table.insert(obstacles, {coord=_spot, size=_asset.size, name=assetname, type="asset"})
  
              gotit=true
              break
  
            else
  
              -- Debug output for occupied spots.            
              if self.Debug then
                local coord=problem.coord --Core.Point#COORDINATE
                if coord then
                    local text=string.format("Obstacle %s [type=%s] blocking spot=%d! Size=%.1f m and distance=%.1f m.", problem.name, problem.type, _termid, problem.size, problem.dist)
                    self:I(self.lid..text)
                    coord:MarkToAll(text)
                end
              else
                self:T(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid))
              end
  
            end
  
          else
            self:T2(self.lid..string.format("Terminal ID=%d: type=%s not supported", parkingspot.TerminalID, parkingspot.TerminalType))
          end -- check terminal type
        end -- loop over parking spots
  
        -- No parking spot for at least one asset :(
        if not gotit then
          self:I(self.lid..string.format("WARNING: No free parking spot for asset %s [id=%d]", assetname, _asset.uid))
          return nil
        end
      end -- loop over asset units
    end -- Asset spawned check
  end -- loop over asset groups

  return parking
end


--- Get the request belonging to a group.
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP group The group from which the info is gathered.
-- @param #table queue Queue holding all requests.
-- @return #WAREHOUSE.Pendingitem The request belonging to this group.
function WAREHOUSE:_GetRequestOfGroup(group, queue)

  -- Get warehouse, asset and request ID from group name.
  local wid,aid,rid=self:_GetIDsFromGroup(group)

  -- Find the request.
  for _,_request in pairs(queue) do
    local request=_request --#WAREHOUSE.Queueitem
    if request.uid==rid then
      return request
    end
  end

end

--- Is the group a used as transporter for a given request?
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP group The group from which the info is gathered.
-- @param #WAREHOUSE.Pendingitem request Request.
-- @return #boolean True if group is transport, false if group is cargo and nil otherwise.
function WAREHOUSE:_GroupIsTransport(group, request)

  local asset=self:FindAssetInDB(group)

  if asset and asset.iscargo~=nil then
    return not asset.iscargo
  else

    -- Name of the group under question.
    local groupname=self:_GetNameWithOut(group)

    if request.transportgroupset then
      local transporters=request.transportgroupset:GetSetObjects()

      for _,transport in pairs(transporters) do
        if transport:GetName()==groupname then
          return true
        end
      end
    end

    if request.cargogroupset then
      local cargos=request.cargogroupset:GetSetObjects()

      for _,cargo in pairs(cargos) do
        if self:_GetNameWithOut(cargo)==groupname then
          return false
        end
      end
    end
  end

  return nil
end


--- Get group name without any spawn or cargo suffix #CARGO etc.
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP group The group from which the info is gathered.
-- @return #string Name of the object without trailing #...
function WAREHOUSE:_GetNameWithOut(group)

  local groupname=type(group)=="string" and group or group:GetName()

  if groupname:find("CARGO") then
    local name=groupname:gsub("#CARGO", "")
    return name
  else
    return groupname
  end

end


--- Get warehouse id, asset id and request id from group name (alias).
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP group The group from which the info is gathered.
-- @return #number Warehouse ID.
-- @return #number Asset ID.
-- @return #number Request ID.
function WAREHOUSE:_GetIDsFromGroup(group)

  if group then
  
    -- Group name
    local groupname=group:GetName()
    
    local wid, aid, rid=self:_GetIDsFromGroupName(groupname)

    return wid,aid,rid
  else
    self:E("WARNING: Group not found in GetIDsFromGroup() function!")
  end

end

--- Get warehouse id, asset id and request id from group name (alias).
-- @param #WAREHOUSE self
-- @param #string groupname Name of the group from which the info is gathered.
-- @return #number Warehouse ID.
-- @return #number Asset ID.
-- @return #number Request ID.
function WAREHOUSE:_GetIDsFromGroupName(groupname)

  -- @param #string text The text to analyse.
  local function analyse(text)

    -- Get rid of #0001 tail from spawn.
    local unspawned=UTILS.Split(text, "#")[1]

    -- Split keywords.
    local keywords=UTILS.Split(unspawned, "_")
    local _wid=nil  -- warehouse UID
    local _aid=nil  -- asset UID
    local _rid=nil  -- request UID

    -- Loop over keys.
    for _,keys in pairs(keywords) do
      local str=UTILS.Split(keys, "-")
      local key=str[1]
      local val=str[2]
      if key:find("WID") then
        _wid=tonumber(val)
      elseif key:find("AID") then
        _aid=tonumber(val)
      elseif key:find("RID") then
        _rid=tonumber(val)
      end
    end

    return _wid,_aid,_rid
  end


  -- Get asset id from group name.
  local wid,aid,rid=analyse(groupname)

  -- Get Asset.
  local asset=self:GetAssetByID(aid)

  -- Get warehouse and request id from asset table.
  if asset then
    wid=asset.wid
    rid=asset.rid
  end

  -- Debug info
  self:T3(self.lid..string.format("Group Name   = %s", tostring(groupname)))
  self:T3(self.lid..string.format("Warehouse ID = %s", tostring(wid)))
  self:T3(self.lid..string.format("Asset     ID = %s", tostring(aid)))
  self:T3(self.lid..string.format("Request   ID = %s", tostring(rid)))

  return wid,aid,rid
end

--- Filter stock assets by descriptor and attribute.
-- @param #WAREHOUSE self
-- @param #string descriptor Descriptor describing the filtered assets.
-- @param attribute Value of the descriptor.
-- @param #number nmax (Optional) Maximum number of items that will be returned. Default nmax=nil is all matching items are returned.
-- @param #boolean mobile (Optional) If true, filter only mobile assets.
-- @return #table Filtered assets in stock with the specified descriptor value.
-- @return #number Total number of (requested) assets available.
-- @return #boolean If true, enough assets are available.
function WAREHOUSE:FilterStock(descriptor, attribute, nmax, mobile)
  return self:_FilterStock(self.stock, descriptor, attribute, nmax, mobile)
end

--- Filter stock assets by table entry.
-- @param #WAREHOUSE self
-- @param #table stock Table holding all assets in stock of the warehouse. Each entry is of type @{#WAREHOUSE.Assetitem}.
-- @param #string descriptor Descriptor describing the filtered assets.
-- @param attribute Value of the descriptor.
-- @param #number nmax (Optional) Maximum number of items that will be returned. Default nmax=nil is all matching items are returned.
-- @param #boolean mobile (Optional) If true, filter only mobile assets.
-- @return #table Filtered stock items table.
-- @return #number Total number of (requested) assets available.
-- @return #boolean If true, enough assets are available.
function WAREHOUSE:_FilterStock(stock, descriptor, attribute, nmax, mobile)

  -- Default all.
  nmax=nmax or WAREHOUSE.Quantity.ALL
  if mobile==nil then
    mobile=false
  end

  -- Filtered array.
  local filtered={}

  -- A specific list of assets was required.
  if descriptor==WAREHOUSE.Descriptor.ASSETLIST then

    -- Count total number in stock.
    local ntot=0
    for _,_rasset in pairs(attribute) do
      local rasset=_rasset --#WAREHOUSE.Assetitem
      for _,_asset in ipairs(stock) do
        local asset=_asset --#WAREHOUSE.Assetitem
        if rasset.uid==asset.uid then
          table.insert(filtered, asset)
          break
        end
      end
    end

    return filtered, #filtered, #filtered>=#attribute
  end

  -- Count total number in stock.
  local ntot=0
  for _,_asset in ipairs(stock) do
    local asset=_asset --#WAREHOUSE.Assetitem
    local ismobile=asset.speedmax>0
    if asset[descriptor]==attribute then
      if (mobile==true and ismobile) or mobile==false then
        ntot=ntot+1
      end
    end
  end

  -- Treat case where ntot=0, i.e. no assets at all.
  if ntot==0 then
    return filtered, ntot, false
  end

  -- Convert relative to absolute number if necessary.
  nmax=self:_QuantityRel2Abs(nmax,ntot)

  -- Loop over stock items.
  for _i,_asset in ipairs(stock) do
    local asset=_asset --#WAREHOUSE.Assetitem

    -- Check if asset has the right attribute.
    if asset[descriptor]==attribute then

      -- Check if asset has to be mobile.
      if (mobile and asset.speedmax>0) or (not mobile) then

        -- Add asset to filtered table.
        table.insert(filtered, asset)

        -- Break loop if nmax was reached.
        if nmax~=nil and #filtered>=nmax then
          return filtered, ntot, true
        end

      end
    end
  end

  return filtered, ntot, ntot>=nmax
end

--- Check if a group has a generalized attribute.
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP group MOOSE group object.
-- @param #WAREHOUSE.Attribute attribute Attribute to check.
-- @return #boolean True if group has the specified attribute.
function WAREHOUSE:_HasAttribute(group, attribute)

  if group then
    local groupattribute=self:_GetAttribute(group)
    return groupattribute==attribute
  end

  return false
end

--- Get the generalized attribute of a group.
-- Note that for a heterogenious group, the attribute is determined from the attribute of the first unit!
-- @param #WAREHOUSE self
-- @param Wrapper.Group#GROUP group MOOSE group object.
-- @return #WAREHOUSE.Attribute Generalized attribute of the group.
function WAREHOUSE:_GetAttribute(group)

  -- Default
  local attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN --#WAREHOUSE.Attribute

  if group then
  
    local groupCat=group:GetCategory()

    -----------
    --- Air ---
    -----------
    -- Planes
    local transportplane=group:HasAttribute("Transports") and group:HasAttribute("Planes") and groupCat==Group.Category.AIRPLANE
    local awacs=group:HasAttribute("AWACS")
    local fighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers"))
    local bomber=group:HasAttribute("Strategic bombers")
    local tanker=group:HasAttribute("Tankers")
    local uav=group:HasAttribute("UAVs")
    -- Helicopters
    local transporthelo=group:HasAttribute("Transport helicopters")
    local attackhelicopter=group:HasAttribute("Attack helicopters")

    --------------
    --- Ground ---
    --------------
    -- Ground
    local apc=group:HasAttribute("APC") --("Infantry carriers")
    local truck=group:HasAttribute("Trucks") and group:GetCategory()==Group.Category.GROUND
    local infantry=group:HasAttribute("Infantry")
    local ifv=group:HasAttribute("IFV")
    local artillery=group:HasAttribute("Artillery")
    local tank=group:HasAttribute("Old Tanks") or group:HasAttribute("Modern Tanks")
    local aaa=group:HasAttribute("AAA")
    local ewr=group:HasAttribute("EWR")
    local sam=group:HasAttribute("SAM elements") and (not group:HasAttribute("AAA"))
    -- Train
    local train=group:GetCategory()==Group.Category.TRAIN

    -------------
    --- Naval ---
    -------------
    -- Ships
    local aircraftcarrier=group:HasAttribute("Aircraft Carriers")
    local warship=group:HasAttribute("Heavy armed ships")
    local armedship=group:HasAttribute("Armed ships") or group:HasAttribute("Armed Ship")
    local unarmedship=group:HasAttribute("Unarmed ships")

    -- Define attribute. Order is important.
    if transportplane then
      attribute=WAREHOUSE.Attribute.AIR_TRANSPORTPLANE
    elseif awacs then
      attribute=WAREHOUSE.Attribute.AIR_AWACS
    elseif fighter then
      attribute=WAREHOUSE.Attribute.AIR_FIGHTER
    elseif bomber then
      attribute=WAREHOUSE.Attribute.AIR_BOMBER
    elseif tanker then
      attribute=WAREHOUSE.Attribute.AIR_TANKER
    elseif transporthelo then
      attribute=WAREHOUSE.Attribute.AIR_TRANSPORTHELO
    elseif attackhelicopter then
      attribute=WAREHOUSE.Attribute.AIR_ATTACKHELO
    elseif uav then
      attribute=WAREHOUSE.Attribute.AIR_UAV
    elseif apc then
      attribute=WAREHOUSE.Attribute.GROUND_APC
    elseif ifv then
      attribute=WAREHOUSE.Attribute.GROUND_IFV
    elseif infantry then
      attribute=WAREHOUSE.Attribute.GROUND_INFANTRY
    elseif artillery then
      attribute=WAREHOUSE.Attribute.GROUND_ARTILLERY
    elseif tank then
      attribute=WAREHOUSE.Attribute.GROUND_TANK
    elseif aaa then
      attribute=WAREHOUSE.Attribute.GROUND_AAA
    elseif ewr then
      attribute=WAREHOUSE.Attribute.GROUND_EWR
    elseif sam then
      attribute=WAREHOUSE.Attribute.GROUND_SAM
    elseif truck then
      attribute=WAREHOUSE.Attribute.GROUND_TRUCK
    elseif train then
      attribute=WAREHOUSE.Attribute.GROUND_TRAIN
    elseif aircraftcarrier then
      attribute=WAREHOUSE.Attribute.NAVAL_AIRCRAFTCARRIER
    elseif warship then
      attribute=WAREHOUSE.Attribute.NAVAL_WARSHIP
    elseif armedship then
      attribute=WAREHOUSE.Attribute.NAVAL_ARMEDSHIP
    elseif unarmedship then
      attribute=WAREHOUSE.Attribute.NAVAL_UNARMEDSHIP
    else
      if group:IsGround() then
        attribute=WAREHOUSE.Attribute.GROUND_OTHER
      elseif group:IsShip() then
        attribute=WAREHOUSE.Attribute.NAVAL_OTHER
      elseif group:IsAir() then
        attribute=WAREHOUSE.Attribute.AIR_OTHER
      else
        attribute=WAREHOUSE.Attribute.OTHER_UNKNOWN
      end
    end
  end

  return attribute
end

--- Size of the bounding box of a DCS object derived from the DCS descriptor table. If boundinb box is nil, a size of zero is returned.
-- @param #WAREHOUSE self
-- @param DCS#Object DCSobject The DCS object for which the size is needed.
-- @return #number Max size of object in meters (length (x) or width (z) components not including height (y)).
-- @return #number Length (x component) of size.
-- @return #number Height (y component) of size.
-- @return #number Width (z component) of size.
function WAREHOUSE:_GetObjectSize(DCSobject)
  local DCSdesc=DCSobject:getDesc()
  if DCSdesc.box then
    local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x)  --length
    local y=DCSdesc.box.max.y+math.abs(DCSdesc.box.min.y)  --height
    local z=DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z)  --width
    return math.max(x,z), x , y, z
  end
  return 0,0,0,0
end

--- Returns the number of assets for each generalized attribute.
-- @param #WAREHOUSE self
-- @param #table stock The stock of the warehouse.
-- @return #table Data table holding the numbers, i.e. data[attibute]=n.
function WAREHOUSE:GetStockInfo(stock)

  local _data={}
  for _j,_attribute in pairs(WAREHOUSE.Attribute) do

    local n=0
    for _i,_item in pairs(stock) do
      local _ite=_item --#WAREHOUSE.Assetitem
      if _ite.attribute==_attribute then
        n=n+1
      end
    end

    _data[_attribute]=n
  end

  return _data
end

--- Delete an asset item from stock.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE.Assetitem stockitem Asset item to delete from stock table.
function WAREHOUSE:_DeleteStockItem(stockitem)
  for i=1,#self.stock do
    local item=self.stock[i] --#WAREHOUSE.Assetitem
    if item.uid==stockitem.uid then
      table.remove(self.stock,i)
      break
    end
  end
end

--- Delete item from queue.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE.Queueitem qitem Item of queue to be removed.
-- @param #table queue The queue from which the item should be deleted.
function WAREHOUSE:_DeleteQueueItem(qitem, queue)

  for i=1,#queue do
    local _item=queue[i] --#WAREHOUSE.Queueitem
    if _item.uid==qitem.uid then
      self:T(self.lid..string.format("Deleting queue item id=%d.", qitem.uid))
      table.remove(queue,i)
      break
    end
  end
end

--- Delete item from queue.
-- @param #WAREHOUSE self
-- @param #number qitemID ID of queue item to be removed.
-- @param #table queue The queue from which the item should be deleted.
function WAREHOUSE:_DeleteQueueItemByID(qitemID, queue)

  for i=1,#queue do
    local _item=queue[i] --#WAREHOUSE.Queueitem
    if _item.uid==qitemID then
      self:T(self.lid..string.format("Deleting queue item id=%d.", qitemID))
      table.remove(queue,i)
      break
    end
  end
end

--- Sort requests queue wrt prio and request uid.
-- @param #WAREHOUSE self
function WAREHOUSE:_SortQueue()
  self:F3()
  -- Sort.
  local function _sort(a, b)
    return (a.prio < b.prio) or (a.prio==b.prio and a.uid < b.uid)
  end
  table.sort(self.queue, _sort)
end

--- Checks fuel on all pening assets.
-- @param #WAREHOUSE self
function WAREHOUSE:_CheckFuel()

  for i,qitem in ipairs(self.pending) do
    local qitem=qitem --#WAREHOUSE.Pendingitem

    if qitem.transportgroupset then
      for _,_group in pairs(qitem.transportgroupset:GetSet()) do
        local group=_group --Wrapper.Group#GROUP

        if group and group:IsAlive() then

          -- Get min fuel of group.
          local fuel=group:GetFuelMin()

          -- Debug info.
          self:T2(self.lid..string.format("Transport group %s min fuel state = %.2f", group:GetName(), fuel))

          -- Check if fuel is below threshold for first time.
          if fuel<self.lowfuelthresh and not qitem.lowfuel then

            -- Set low fuel flag.
            self:I(self.lid..string.format("Transport group %s is low on fuel! Min fuel state = %.2f", group:GetName(), fuel))
            qitem.lowfuel=true

            -- Trigger low fuel event.
            local asset=self:FindAssetInDB(group)
            self:AssetLowFuel(asset, qitem)
            break
          end

        end
      end
    end

    if qitem.cargogroupset then
      for _,_group in pairs(qitem.cargogroupset:GetSet()) do
        local group=_group --Wrapper.Group#GROUP

        if group and group:IsAlive() then

          -- Get min fuel of group.
          local fuel=group:GetFuelMin()

          -- Debug output.
          self:T2(self.lid..string.format("Cargo group %s min fuel state = %.2f. Threshold = %.2f", group:GetName(), fuel, self.lowfuelthresh))

          -- Check if fuel is below threshold for first time.
          if fuel<self.lowfuelthresh and not qitem.lowfuel then

            -- Set low fuel flag.
            self:I(self.lid..string.format("Cargo group %s is low on fuel! Min fuel state = %.2f", group:GetName(), fuel))
            qitem.lowfuel=true

            -- Trigger low fuel event.
            local asset=self:FindAssetInDB(group)
            self:AssetLowFuel(asset, qitem)
            break
          end

        end
      end
    end

  end
end


--- Prints the queue to DCS.log file.
-- @param #WAREHOUSE self
-- @param #table queue Queue to print.
-- @param #string name Name of the queue for info reasons.
function WAREHOUSE:_PrintQueue(queue, name)

  if self.verbosity>=2 then

    local total="Empty"
    if #queue>0 then
      total=string.format("Total = %d", #queue)
    end
  
    -- Init string.
    local text=string.format("%s at %s: %s",name, self.alias, total)
  
    for i,qitem in ipairs(queue) do
      local qitem=qitem --#WAREHOUSE.Pendingitem
  
      local uid=qitem.uid
      local prio=qitem.prio
      local clock="N/A"
      if qitem.timestamp then
        clock=tostring(UTILS.SecondsToClock(qitem.timestamp))
      end
      local assignment=tostring(qitem.assignment)
      local requestor=qitem.warehouse.alias
      local airbasename=qitem.warehouse:GetAirbaseName()
      local requestorAirbaseCat=qitem.warehouse:GetAirbaseCategory()
      local assetdesc=qitem.assetdesc
      local assetdescval=qitem.assetdescval
      if assetdesc==WAREHOUSE.Descriptor.ASSETLIST then
        assetdescval="Asset list"
      end
      local nasset=tostring(qitem.nasset)
      local ndelivered=tostring(qitem.ndelivered)
      local ncargogroupset="N/A"
      if qitem.cargogroupset then
        ncargogroupset=tostring(qitem.cargogroupset:Count())
      end
      local transporttype="N/A"
      if qitem.transporttype then
        transporttype=qitem.transporttype
      end
      local ntransport="N/A"
      if qitem.ntransport then
        ntransport=tostring(qitem.ntransport)
      end
      local ntransportalive="N/A"
      if qitem.transportgroupset then
        ntransportalive=tostring(qitem.transportgroupset:Count())
      end
      local ntransporthome="N/A"
      if qitem.ntransporthome then
        ntransporthome=tostring(qitem.ntransporthome)
      end
  
      -- Output text:
      text=text..string.format(
      "\n%d) UID=%d, Prio=%d, Clock=%s, Assignment=%s | Requestor=%s [Airbase=%s, category=%d] | Assets(%s)=%s: #requested=%s / #alive=%s / #delivered=%s | Transport=%s: #requested=%s / #alive=%s / #home=%s",
      i, uid, prio, clock, assignment, requestor, airbasename, requestorAirbaseCat, assetdesc, assetdescval, nasset, ncargogroupset, ndelivered, transporttype, ntransport, ntransportalive, ntransporthome)
  
    end
  
    if #queue==0 then
      self:I(self.lid..text)
    else
      if total~="Empty" then
        self:I(self.lid..text)
      end
    end
  end
end

--- Display status of warehouse.
-- @param #WAREHOUSE self
function WAREHOUSE:_DisplayStatus()
  if self.verbosity>=3 then
    local text=string.format("\n------------------------------------------------------\n")
    text=text..string.format("Warehouse %s status: %s\n", self.alias, self:GetState())
    text=text..string.format("------------------------------------------------------\n")
    text=text..string.format("Coalition name   = %s\n", self:GetCoalitionName())
    text=text..string.format("Country name     = %s\n", self:GetCountryName())
    text=text..string.format("Airbase name     = %s (category=%d)\n", self:GetAirbaseName(), self:GetAirbaseCategory())
    text=text..string.format("Queued requests  = %d\n", #self.queue)
    text=text..string.format("Pending requests = %d\n", #self.pending)
    text=text..string.format("------------------------------------------------------\n")
    text=text..self:_GetStockAssetsText()
    self:I(text)
  end
end

--- Get text about warehouse stock.
-- @param #WAREHOUSE self
-- @param #boolean messagetoall If true, send message to all.
-- @return #string Text about warehouse stock
function WAREHOUSE:_GetStockAssetsText(messagetoall)

  -- Get assets in stock.
  local _data=self:GetStockInfo(self.stock)

  -- Text.
  local text="Stock:\n"
  local total=0
  for _attribute,_count in pairs(_data) do
    if _count>0 then
      local attribute=tostring(UTILS.Split(_attribute, "_")[2])
      text=text..string.format("%s = %d\n", attribute,_count)
      total=total+_count
    end
  end
  text=text..string.format("===================\n")
  text=text..string.format("Total = %d\n", total)
  text=text..string.format("------------------------------------------------------\n")

  -- Send message?
  MESSAGE:New(text, 10):ToAllIf(messagetoall)

  return text
end

--- Create or update mark text at warehouse, which is displayed in F10 map showing how many assets of each type are in stock.
-- Only the coalition of the warehouse owner is able to see it.
-- @param #WAREHOUSE self
-- @return #string Text about warehouse stock
function WAREHOUSE:_UpdateWarehouseMarkText()

  if self.markerOn then

    -- Marker text.
    local text=string.format("Warehouse state: %s\nTotal assets in stock %d:\n", self:GetState(), #self.stock)
    for _attribute,_count in pairs(self:GetStockInfo(self.stock) or {}) do
      if _count>0 then
        local attribute=tostring(UTILS.Split(_attribute, "_")[2])
        text=text..string.format("%s=%d, ", attribute,_count)
      end
    end
    
    local coordinate=self:GetCoordinate()
    local coalition=self:GetCoalition()

    if not self.markerWarehouse then
    
      -- Create a new marker.
      self.markerWarehouse=MARKER:New(coordinate, text):ToCoalition(coalition)
          
    else
  
      local refresh=false
    
      if self.markerWarehouse.text~=text then
        self.markerWarehouse.text=text
        refresh=true
      end
      
      if self.markerWarehouse.coordinate~=coordinate then
        self.markerWarehouse.coordinate=coordinate
        refresh=true
      end
      
      if self.markerWarehouse.coalition~=coalition then
        self.markerWarehouse.coalition=coalition
        refresh=true
      end
      
      if refresh then
        self.markerWarehouse:Refresh()
      end
  
    end
  end

end

--- Display stock items of warehouse.
-- @param #WAREHOUSE self
-- @param #table stock Table holding all assets in stock of the warehouse. Each entry is of type @{#WAREHOUSE.Assetitem}.
function WAREHOUSE:_DisplayStockItems(stock)

  local text=self.lid..string.format("Warehouse %s stock assets:", self.alias)
  for _i,_stock in pairs(stock) do
    local mystock=_stock --#WAREHOUSE.Assetitem
    local name=mystock.templatename
    local category=mystock.category
    local cargobaymax=mystock.cargobaymax
    local cargobaytot=mystock.cargobaytot
    local nunits=mystock.nunits
    local range=mystock.range
    local size=mystock.size
    local speed=mystock.speedmax
    local uid=mystock.uid
    local unittype=mystock.unittype
    local weight=mystock.weight
    local attribute=mystock.attribute
    text=text..string.format("\n%02d) uid=%d, name=%s, unittype=%s, category=%d, attribute=%s, nunits=%d, speed=%.1f km/h, range=%.1f km, size=%.1f m, weight=%.1f kg, cargobax max=%.1f kg tot=%.1f kg",
    _i, uid, name, unittype, category, attribute, nunits, speed, range/1000, size, weight, cargobaymax, cargobaytot)
  end

  self:T3(text)
end

--- Fireworks!
-- @param #WAREHOUSE self
-- @param Core.Point#COORDINATE coord
function WAREHOUSE:_Fireworks(coord)

  -- Place.
  coord=coord or self:GetCoordinate()

  -- Fireworks!
  for i=1,91 do
    local color=math.random(0,3)
    coord:Flare(color, i-1)
  end
end

--- Info Message. Message send to coalition if reports or debug mode activated (and duration > 0). Text self:I(text) added to DCS.log file.
-- @param #WAREHOUSE self
-- @param #string text The text of the error message.
-- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed.
function WAREHOUSE:_InfoMessage(text, duration)
  duration=duration or 20
  if duration>0 and self.Debug or self.Report then
    MESSAGE:New(text, duration):ToCoalition(self:GetCoalition())
  end
  self:I(self.lid..text)
end


--- Debug message. Message send to all if debug mode is activated (and duration > 0). Text self:T(text) added to DCS.log file.
-- @param #WAREHOUSE self
-- @param #string text The text of the error message.
-- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed.
function WAREHOUSE:_DebugMessage(text, duration)
  duration=duration or 20
  if self.Debug and duration>0 then
    MESSAGE:New(text, duration):ToAllIf(self.Debug)
  end
  self:T(self.lid..text)
end

--- Error message. Message send to all (if duration > 0). Text self:E(text) added to DCS.log file.
-- @param #WAREHOUSE self
-- @param #string text The text of the error message.
-- @param #number duration Message display duration in seconds. Default 20 sec. If duration is zero, no message is displayed.
function WAREHOUSE:_ErrorMessage(text, duration)
  duration=duration or 20
  if duration>0 then
    MESSAGE:New(text, duration):ToAll()
  end
  self:E(self.lid..text)
end


--- Calculate the maximum height an aircraft can reach for the given parameters.
-- @param #WAREHOUSE self
-- @param #number D Total distance in meters from Departure to holding point at destination.
-- @param #number alphaC Climb angle in rad.
-- @param #number alphaD Descent angle in rad.
-- @param #number Hdep AGL altitude of departure point.
-- @param #number Hdest AGL altitude of destination point.
-- @param #number Deltahhold Relative altitude of holding point above destination.
-- @return #number Maximum height the aircraft can reach.
function WAREHOUSE:_GetMaxHeight(D, alphaC, alphaD, Hdep, Hdest, Deltahhold)

  local Hhold=Hdest+Deltahhold
  local hdest=Hdest-Hdep
  local hhold=hdest+Deltahhold

  local Dp=math.sqrt(D^2 + hhold^2)

  local alphaS=math.atan(hdest/D) -- slope angle
  local alphaH=math.atan(hhold/D) -- angle to holding point (could be necative!)

  local alphaCp=alphaC-alphaH  -- climb angle with slope
  local alphaDp=alphaD+alphaH  -- descent angle with slope

  -- ASA triangle.
  local gammap=math.pi-alphaCp-alphaDp
  local sCp=Dp*math.sin(alphaDp)/math.sin(gammap)
  local sDp=Dp*math.sin(alphaCp)/math.sin(gammap)

  -- Max height from departure.
  local hmax=sCp*math.sin(alphaC)

  -- Debug info.
  if self.Debug then
    env.info(string.format("Hdep    = %.3f km", Hdep/1000))
    env.info(string.format("Hdest   = %.3f km", Hdest/1000))
    env.info(string.format("DetaHold= %.3f km", Deltahhold/1000))
    env.info()
    env.info(string.format("D       = %.3f km", D/1000))
    env.info(string.format("Dp      = %.3f km", Dp/1000))
    env.info()
    env.info(string.format("alphaC  = %.3f Deg", math.deg(alphaC)))
    env.info(string.format("alphaCp = %.3f Deg", math.deg(alphaCp)))
    env.info()
    env.info(string.format("alphaD  = %.3f Deg", math.deg(alphaD)))
    env.info(string.format("alphaDp = %.3f Deg", math.deg(alphaDp)))
    env.info()
    env.info(string.format("alphaS  = %.3f Deg", math.deg(alphaS)))
    env.info(string.format("alphaH  = %.3f Deg", math.deg(alphaH)))
    env.info()
    env.info(string.format("sCp      = %.3f km", sCp/1000))
    env.info(string.format("sDp      = %.3f km", sDp/1000))
    env.info()
    env.info(string.format("hmax     = %.3f km", hmax/1000))
    env.info()

    -- Descent height
    local hdescent=hmax-hhold

    local dClimb   = hmax/math.tan(alphaC)
    local dDescent = (hmax-hhold)/math.tan(alphaD)
    local dCruise  = D-dClimb-dDescent

    env.info(string.format("hmax     = %.3f km", hmax/1000))
    env.info(string.format("hdescent = %.3f km", hdescent/1000))
    env.info(string.format("Dclimb   = %.3f km", dClimb/1000))
    env.info(string.format("Dcruise  = %.3f km", dCruise/1000))
    env.info(string.format("Ddescent = %.3f km", dDescent/1000))
    env.info()
  end

  return hmax
end


--- Make a flight plan from a departure to a destination airport.
-- @param #WAREHOUSE self
-- @param #WAREHOUSE.Assetitem asset
-- @param Wrapper.Airbase#AIRBASE departure Departure airbase.
-- @param Wrapper.Airbase#AIRBASE destination Destination airbase.
-- @return #table Table of flightplan waypoints.
-- @return #table Table of flightplan coordinates.
function WAREHOUSE:_GetFlightplan(asset, departure, destination)

  -- Parameters in SI units (m/s, m).
  local Vmax=asset.speedmax/3.6
  local Range=asset.range
  local category=asset.category
  local ceiling=asset.DCSdesc.Hmax
  local Vymax=asset.DCSdesc.VyMax

  -- Max cruise speed 90% of max speed.
  local VxCruiseMax=0.90*Vmax

  -- Min cruise speed 70% of max cruise or 600 km/h whichever is lower.
  local VxCruiseMin = math.min(VxCruiseMax*0.70, 166)

  -- Cruise speed (randomized). Expectation value at midpoint between min and max.
  local VxCruise = UTILS.RandomGaussian((VxCruiseMax-VxCruiseMin)/2+VxCruiseMin, (VxCruiseMax-VxCruiseMax)/4, VxCruiseMin, VxCruiseMax)

  -- Climb speed 90% ov Vmax but max 720 km/h.
  local VxClimb = math.min(Vmax*0.90, 200)

  -- Descent speed 60% of Vmax but max 500 km/h.
  local VxDescent = math.min(Vmax*0.60, 140)

  -- Holding speed is 90% of descent speed.
  local VxHolding = VxDescent*0.9

  -- Final leg is 90% of holding speed.
  local VxFinal = VxHolding*0.9

  -- Reasonably civil climb speed Vy=1500 ft/min = 7.6 m/s but max aircraft specific climb rate.
  local VyClimb=math.min(7.6, Vymax)

  -- Climb angle in rad.
  --local AlphaClimb=math.asin(VyClimb/VxClimb)
  local AlphaClimb=math.rad(4)

  -- Descent angle in rad. Moderate 4 degrees.
  local AlphaDescent=math.rad(4)

  -- Expected cruise level (peak of Gaussian distribution)
  local FLcruise_expect=150*RAT.unit.FL2m
  if category==Group.Category.HELICOPTER then
    FLcruise_expect=1000 -- 1000 m ASL
  end

  -------------------------
  --- DEPARTURE AIRPORT ---
  -------------------------

  -- Coordinates of departure point.
  local Pdeparture=departure:GetCoordinate()

  -- Height ASL of departure point.
  local H_departure=Pdeparture.y

  ---------------------------
  --- DESTINATION AIRPORT ---
  ---------------------------

  -- Position of destination airport.
  local Pdestination=destination:GetCoordinate()

  -- Height ASL of destination airport/zone.
  local H_destination=Pdestination.y

  -----------------------------
  --- DESCENT/HOLDING POINT ---
  -----------------------------

  -- Get a random point between 5 and 10 km away from the destination.
  local Rhmin=5000
  local Rhmax=10000

  -- For helos we set a distance between 500 to 1000 m.
  if category==Group.Category.HELICOPTER then
    Rhmin=500
    Rhmax=1000
  end

  -- Coordinates of the holding point. y is the land height at that point.
  local Pholding=Pdestination:GetRandomCoordinateInRadius(Rhmax, Rhmin)

  -- Distance from holding point to final destination (not used).
  local d_holding=Pholding:Get2DDistance(Pdestination)

  -- AGL height of holding point.
  local H_holding=Pholding.y

  ---------------
  --- GENERAL ---
  ---------------

  -- We go directly to the holding point not the destination airport. From there, planes are guided by DCS to final approach.
  local heading=Pdeparture:HeadingTo(Pholding)
  local d_total=Pdeparture:Get2DDistance(Pholding)

  ------------------------------
  --- Holding Point Altitude ---
  ------------------------------

  -- Holding point altitude. For planes between 1600 and 2400 m AGL. For helos 160 to 240 m AGL.
  local h_holding=1200
  if category==Group.Category.HELICOPTER then
    h_holding=150
  end
  h_holding=UTILS.Randomize(h_holding, 0.2)

  -- Max holding altitude.
  local DeltaholdingMax=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, 0)

  if h_holding>DeltaholdingMax then
    h_holding=math.abs(DeltaholdingMax)
  end

  -- This is the height ASL of the holding point we want to fly to.
  local Hh_holding=H_holding+h_holding

  ---------------------------
  --- Max Flight Altitude ---
  ---------------------------

  -- Get max flight altitude relative to H_departure.
  local h_max=self:_GetMaxHeight(d_total, AlphaClimb, AlphaDescent, H_departure, H_holding, h_holding)

  -- Max flight level ASL aircraft can reach for given angles and distance.
  local FLmax = h_max+H_departure

  --CRUISE
  -- Min cruise alt is just above holding point at destination or departure height, whatever is larger.
  local FLmin=math.max(H_departure, Hh_holding)

  -- Ensure that FLmax not above its service ceiling.
  FLmax=math.min(FLmax, ceiling)

  -- If the route is very short we set FLmin a bit lower than FLmax.
  if FLmin>FLmax then
    FLmin=FLmax
  end

  -- Expected cruise altitude - peak of gaussian distribution.
  if FLcruise_expect<FLmin then
    FLcruise_expect=FLmin
  end
  if FLcruise_expect>FLmax then
    FLcruise_expect=FLmax
  end

  -- Set cruise altitude. Selected from Gaussian distribution but limited to FLmin and FLmax.
  local FLcruise=UTILS.RandomGaussian(FLcruise_expect, math.abs(FLmax-FLmin)/4, FLmin, FLmax)

  -- Climb and descent heights.
  local h_climb   = FLcruise - H_departure
  local h_descent = FLcruise - Hh_holding

  -- Get distances.
  local d_climb   = h_climb/math.tan(AlphaClimb)
  local d_descent = h_descent/math.tan(AlphaDescent)
  local d_cruise  = d_total-d_climb-d_descent

  -- Debug.
  local text=string.format("Flight plan:\n")
  text=text..string.format("Vx max        = %.2f km/h\n", Vmax*3.6)
  text=text..string.format("Vx climb      = %.2f km/h\n", VxClimb*3.6)
  text=text..string.format("Vx cruise     = %.2f km/h\n", VxCruise*3.6)
  text=text..string.format("Vx descent    = %.2f km/h\n", VxDescent*3.6)
  text=text..string.format("Vx holding    = %.2f km/h\n", VxHolding*3.6)
  text=text..string.format("Vx final      = %.2f km/h\n", VxFinal*3.6)
  text=text..string.format("Vy max        = %.2f m/s\n",  Vymax)
  text=text..string.format("Vy climb      = %.2f m/s\n",  VyClimb)
  text=text..string.format("Alpha Climb   = %.2f Deg\n",  math.deg(AlphaClimb))
  text=text..string.format("Alpha Descent = %.2f Deg\n",  math.deg(AlphaDescent))
  text=text..string.format("Dist climb    = %.3f km\n",   d_climb/1000)
  text=text..string.format("Dist cruise   = %.3f km\n",   d_cruise/1000)
  text=text..string.format("Dist descent  = %.3f km\n",   d_descent/1000)
  text=text..string.format("Dist total    = %.3f km\n",   d_total/1000)
  text=text..string.format("h_climb       = %.3f km\n",   h_climb/1000)
  text=text..string.format("h_desc        = %.3f km\n",   h_descent/1000)
  text=text..string.format("h_holding     = %.3f km\n",   h_holding/1000)
  text=text..string.format("h_max         = %.3f km\n",   h_max/1000)
  text=text..string.format("FL min        = %.3f km\n",   FLmin/1000)
  text=text..string.format("FL expect     = %.3f km\n",   FLcruise_expect/1000)
  text=text..string.format("FL cruise *   = %.3f km\n",   FLcruise/1000)
  text=text..string.format("FL max        = %.3f km\n",   FLmax/1000)
  text=text..string.format("Ceiling       = %.3f km\n",   ceiling/1000)
  text=text..string.format("Max range     = %.3f km\n",   Range/1000)
  self:T(self.lid..text)

  -- Ensure that cruise distance is positve. Can be slightly negative in special cases. And we don't want to turn back.
  if d_cruise<0 then
    d_cruise=100
  end

  ------------------------
  --- Create Waypoints ---
  ------------------------

  -- Waypoints and coordinates
  local wp={}
  local c={}

  -- Cold start (default).
  local _type=COORDINATE.WaypointType.TakeOffParking
  local _action=COORDINATE.WaypointAction.FromParkingArea

  -- Hot start.
  if asset.takeoffType and asset.takeoffType==COORDINATE.WaypointType.TakeOffParkingHot then
    --env.info("FF hot")
    _type=COORDINATE.WaypointType.TakeOffParkingHot
    _action=COORDINATE.WaypointAction.FromParkingAreaHot
  else
    --env.info("FF cold")      
  end


  --- Departure/Take-off
  c[#c+1]=Pdeparture
  wp[#wp+1]=Pdeparture:WaypointAir("RADIO", _type, _action, VxClimb*3.6, true, departure, nil, "Departure")

  --- Begin of Cruise
  local Pcruise=Pdeparture:Translate(d_climb, heading)
  Pcruise.y=FLcruise
  c[#c+1]=Pcruise
  wp[#wp+1]=Pcruise:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise*3.6, true, nil, nil, "Cruise")

  --- Descent
  local Pdescent=Pcruise:Translate(d_cruise, heading)
  Pdescent.y=FLcruise
  c[#c+1]=Pdescent
  wp[#wp+1]=Pdescent:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent*3.6, true, nil, nil, "Descent")

  --- Holding point
  Pholding.y=H_holding+h_holding
  c[#c+1]=Pholding
  wp[#wp+1]=Pholding:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxHolding*3.6, true, nil, nil, "Holding")

  --- Final destination.
  c[#c+1]=Pdestination
  wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal*3.6, true,  destination, nil, "Final Destination")


  -- Mark points at waypoints for debugging.
  if self.Debug then
    for i,coord in pairs(c) do
      local coord=coord --Core.Point#COORDINATE
      local dist=0
      if i>1 then
        dist=coord:Get2DDistance(c[i-1])
      end
      coord:MarkToAll(string.format("Waypoint %i, distance = %.2f km",i, dist/1000))
    end
  end

  return wp,c
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Functional** - Yet Another Missile Trainer.
--
--
-- Practice to evade missiles without being destroyed.
--
--
-- ## Main Features:
--
--    * Handles air-to-air and surface-to-air missiles.
--    * Define your own training zones on the map. Players in this zone will be protected.
--    * Define launch zones. Only missiles launched in these zones are tracked.
--    * Define protected AI groups.
--    * F10 radio menu to adjust settings for each player.
--    * Alert on missile launch (optional).
--    * Marker of missile launch position (optional).
--    * Adaptive update of missile-to-player distance.
--    * Finite State Machine (FSM) implementation.
--    * Easy to use. See examples below.
--
-- ===
--
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/Functional/FOX)
--
-- ===
--
-- ### Author: **funkyfranky**
-- @module Functional.Fox
-- @image Functional_FOX.png

--- FOX class.
-- @type FOX
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #boolean Debug Debug mode. Messages to all about status.
-- @field #string lid Class id string for output to DCS log file.
-- @field #table menuadded Table of groups the menu was added for.
-- @field #boolean menudisabled If true, F10 menu for players is disabled.
-- @field #boolean destroy Default player setting for destroying missiles.
-- @field #boolean launchalert Default player setting for launch alerts.
-- @field #boolean marklaunch Default player setting for mark launch coordinates.
-- @field #table players Table of players.
-- @field #table missiles Table of tracked missiles.
-- @field #table safezones Table of practice zones.
-- @field #table launchzones Table of launch zones.
-- @field Core.Set#SET_GROUP protectedset Set of protected groups.
-- @field #number explosionpower Power of explostion when destroying the missile in kg TNT. Default 5 kg TNT.
-- @field #number explosiondist Missile player distance in meters for destroying smaller missiles. Default 200 m.
-- @field #number explosiondist2 Missile player distance in meters for destroying big missiles. Default 500 m.
-- @field #number bigmissilemass Explosion power of big missiles. Default 50 kg TNT. Big missiles will be destroyed earlier.
-- @field #number dt50 Time step [sec] for missile position updates if distance to target > 50 km. Default 5 sec.
-- @field #number dt10 Time step [sec] for missile position updates if distance to target > 10 km and < 50 km. Default 1 sec.
-- @field #number dt05 Time step [sec] for missile position updates if distance to target > 5 km and < 10 km. Default 0.5 sec.
-- @field #number dt01 Time step [sec] for missile position updates if distance to target > 1 km and < 5 km. Default 0.1 sec.
-- @field #number dt00 Time step [sec] for missile position updates if distance to target < 1 km. Default 0.01 sec.
-- @extends Core.Fsm#FSM

--- Fox 3!
--
-- ===
--
-- ![Banner Image](..\Presentations\FOX\FOX_Main.png)
--
-- # The FOX Concept
--
-- As you probably know [Fox](https://en.wikipedia.org/wiki/Fox_\(code_word\)) is a NATO brevity code for launching air-to-air munition. Therefore, the class name is not 100% accurate as this
-- script handles air-to-air but also surface-to-air missiles.
--
-- # Basic Script
--
--     -- Create a new missile trainer object.
--     fox=FOX:New()
--
--     -- Start missile trainer.
--     fox:Start()
--
-- # Training Zones
--
-- Players are only protected if they are inside one of the training zones.
--
--     -- Create a new missile trainer object.
--     fox=FOX:New()
--
--     -- Add training zones.
--     fox:AddSafeZone(ZONE:New("Training Zone Alpha"))
--     fox:AddSafeZone(ZONE:New("Training Zone Bravo"))
--
--     -- Start missile trainer.
--     fox:Start()
--
-- # Launch Zones
--
-- Missile launches are only monitored if the shooter is inside the defined launch zone.
--
--     -- Create a new missile trainer object.
--     fox=FOX:New()
--
--     -- Add training zones.
--     fox:AddLaunchZone(ZONE:New("Launch Zone SA-10 Krim"))
--     fox:AddLaunchZone(ZONE:New("Training Zone Bravo"))
--
--     -- Start missile trainer.
--     fox:Start()
--
-- # Protected AI Groups
--
-- Define AI protected groups. These groups cannot be harmed by missiles.
--
-- ## Add Individual Groups
--
--     -- Create a new missile trainer object.
--     fox=FOX:New()
--
--     -- Add single protected group(s).
--     fox:AddProtectedGroup(GROUP:FindByName("A-10 Protected"))
--     fox:AddProtectedGroup(GROUP:FindByName("Yak-40"))
--
--     -- Start missile trainer.
--     fox:Start()
--
-- # Notes
--
-- The script needs to be running before you enter an airplane slot. If FOX is not available to you, go back to observers and then join a slot again.
--
-- @field #FOX
FOX = {
  ClassName      = "FOX",
  verbose        =    0,
  Debug          = false,
  lid            =   nil,
  menuadded      =    {},
  menudisabled   =   nil,
  destroy        =   nil,
  launchalert    =   nil,
  marklaunch     =   nil,
  missiles       =    {},
  players        =    {},
  safezones      =    {},
  launchzones    =    {},
  protectedset   =   nil,
  explosionpower =   0.1,
  explosiondist  =   200,
  explosiondist2 =   500,
  bigmissilemass =    50,
  --destroy        =   nil,
  dt50           =     5,
  dt10           =     1,
  dt05           =   0.5,
  dt01           =   0.1,
  dt00           =  0.01,
}


--- Player data table holding all important parameters of each player.
-- @type FOX.PlayerData
-- @field Wrapper.Unit#UNIT unit Aircraft of the player.
-- @field #string unitname Name of the unit.
-- @field Wrapper.Client#CLIENT client Client object of player.
-- @field #string callsign Callsign of player.
-- @field Wrapper.Group#GROUP group Aircraft group of player.
-- @field #string groupname Name of the the player aircraft group.
-- @field #string name Player name.
-- @field #number coalition Coalition number of player.
-- @field #boolean destroy Destroy missile.
-- @field #boolean launchalert Alert player on detected missile launch.
-- @field #boolean marklaunch Mark position of launched missile on F10 map.
-- @field #number defeated Number of missiles defeated.
-- @field #number dead Number of missiles not defeated.
-- @field #boolean inzone Player is inside a protected zone.

--- Missile data table.
-- @type FOX.MissileData
-- @field DCS#Weapon weapon Missile weapon object.
-- @field #boolean active If true the missile is active.
-- @field #string missileType Type of missile.
-- @field #string missileName Name of missile.
-- @field #number missileRange Range of missile in meters.
-- @field #number fuseDist Fuse distance in meters.
-- @field #number explosive Explosive mass in kg TNT.
-- @field Wrapper.Unit#UNIT shooterUnit Unit that shot the missile.
-- @field Wrapper.Group#GROUP shooterGroup Group that shot the missile.
-- @field #number shooterCoalition Coalition side of the shooter.
-- @field #string shooterName Name of the shooter unit.
-- @field #number shotTime Abs. mission time in seconds the missile was fired.
-- @field Core.Point#COORDINATE shotCoord Coordinate where the missile was fired.
-- @field Wrapper.Unit#UNIT targetUnit Unit that was targeted.
-- @field #string targetName Name of the target unit or "unknown".
-- @field #string targetOrig Name of the "original" target, i.e. the one right after launched.
-- @field #FOX.PlayerData targetPlayer Player that was targeted or nil.
-- @field Core.Point#COORDINATE missileCoord Missile coordinate during tracking.
-- @field Wrapper.Weapon#WEAPON Weapon Weapon object.

--- Main radio menu on group level.
-- @field #table MenuF10 Root menu table on group level.
FOX.MenuF10={}

--- Main radio menu on mission level.
-- @field #table MenuF10Root Root menu on mission level.
FOX.MenuF10Root=nil

--- FOX class version.
-- @field #string version
FOX.version="0.8.0"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO list:
-- DONE: safe zones
-- DONE: mark shooter on F10

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new FOX class object.
-- @param #FOX self
-- @return #FOX self.
function FOX:New()

  self.lid="FOX | "

  -- Inherit everthing from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #FOX

  -- Defaults:
  self:SetDefaultMissileDestruction(true)
  self:SetDefaultLaunchAlerts(true)
  self:SetDefaultLaunchMarks(true)

  -- Explosion/destruction defaults.
  self:SetExplosionDistance()
  self:SetExplosionDistanceBigMissiles()
  self:SetExplosionPower()

  -- Start State.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  --                 From State   -->   Event        -->     To State
  self:AddTransition("Stopped",           "Start",          "Running")     -- Start FOX script.
  self:AddTransition("*",                "Status",          "*")           -- Status update.
  self:AddTransition("*",         "MissileLaunch",          "*")           -- Missile was launched.
  self:AddTransition("*",      "MissileDestroyed",          "*")           -- Missile was destroyed before impact.
  self:AddTransition("*",         "EnterSafeZone",          "*")           -- Player enters a safe zone.
  self:AddTransition("*",          "ExitSafeZone",          "*")           -- Player exists a safe zone.
  self:AddTransition("Running",            "Stop",          "Stopped")     -- Stop FOX script.

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the FOX. Initializes parameters and starts event handlers.
  -- @function [parent=#FOX] Start
  -- @param #FOX self

  --- Triggers the FSM event "Start" after a delay. Starts the FOX. Initializes parameters and starts event handlers.
  -- @function [parent=#FOX] __Start
  -- @param #FOX self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the FOX and all its event handlers.
  -- @param #FOX self

  --- Triggers the FSM event "Stop" after a delay. Stops the FOX and all its event handlers.
  -- @function [parent=#FOX] __Stop
  -- @param #FOX self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Status".
  -- @function [parent=#FOX] Status
  -- @param #FOX self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#FOX] __Status
  -- @param #FOX self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "MissileLaunch".
  -- @function [parent=#FOX] MissileLaunch
  -- @param #FOX self
  -- @param #FOX.MissileData missile Data of the fired missile.

  --- Triggers the FSM delayed event "MissileLaunch".
  -- @function [parent=#FOX] __MissileLaunch
  -- @param #FOX self
  -- @param #number delay Delay in seconds before the function is called.
  -- @param #FOX.MissileData missile Data of the fired missile.

  --- On after "MissileLaunch" event user function. Called when a missile was launched.
  -- @function [parent=#FOX] OnAfterMissileLaunch
  -- @param #FOX self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #FOX.MissileData missile Data of the fired missile.

  --- Triggers the FSM event "MissileDestroyed".
  -- @function [parent=#FOX] MissileDestroyed
  -- @param #FOX self
  -- @param #FOX.MissileData missile Data of the destroyed missile.

  --- Triggers the FSM delayed event "MissileDestroyed".
  -- @function [parent=#FOX] __MissileDestroyed
  -- @param #FOX self
  -- @param #number delay Delay in seconds before the function is called.
  -- @param #FOX.MissileData missile Data of the destroyed missile.

  --- On after "MissileDestroyed" event user function. Called when a missile was destroyed.
  -- @function [parent=#FOX] OnAfterMissileDestroyed
  -- @param #FOX self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #FOX.MissileData missile Data of the destroyed missile.


  --- Triggers the FSM event "EnterSafeZone".
  -- @function [parent=#FOX] EnterSafeZone
  -- @param #FOX self
  -- @param #FOX.PlayerData player Player data.

  --- Triggers the FSM delayed event "EnterSafeZone".
  -- @function [parent=#FOX] __EnterSafeZone
  -- @param #FOX self
  -- @param #number delay Delay in seconds before the function is called.
  -- @param #FOX.PlayerData player Player data.

  --- On after "EnterSafeZone" event user function. Called when a player enters a safe zone.
  -- @function [parent=#FOX] OnAfterEnterSafeZone
  -- @param #FOX self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #FOX.PlayerData player Player data.


  --- Triggers the FSM event "ExitSafeZone".
  -- @function [parent=#FOX] ExitSafeZone
  -- @param #FOX self
  -- @param #FOX.PlayerData player Player data.

  --- Triggers the FSM delayed event "ExitSafeZone".
  -- @function [parent=#FOX] __ExitSafeZone
  -- @param #FOX self
  -- @param #number delay Delay in seconds before the function is called.
  -- @param #FOX.PlayerData player Player data.

  --- On after "ExitSafeZone" event user function. Called when a player exists a safe zone.
  -- @function [parent=#FOX] OnAfterExitSafeZone
  -- @param #FOX self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #FOX.PlayerData player Player data.


  return self
end

--- On after Start event. Starts the missile trainer and adds event handlers.
-- @param #FOX self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FOX:onafterStart(From, Event, To)

  -- Short info.
  local text=string.format("Starting FOX Missile Trainer %s", FOX.version)
  env.info(text)

  -- Handle events:
  self:HandleEvent(EVENTS.Birth)
  self:HandleEvent(EVENTS.Shot)

  if self.Debug then
    self:HandleEvent(EVENTS.Hit)
  end

  if self.Debug then
    self:TraceClass(self.ClassName)
    self:TraceLevel(2)
  end

  self:__Status(-20)
end

--- On after Stop event. Stops the missile trainer and unhandles events.
-- @param #FOX self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FOX:onafterStop(From, Event, To)

  -- Short info.
  local text=string.format("Stopping FOX Missile Trainer %s", FOX.version)
  env.info(text)

  -- Handle events:
  self:UnHandleEvent(EVENTS.Birth)
  self:UnHandleEvent(EVENTS.Shot)

  if self.Debug then
    self:UnhandleEvent(EVENTS.Hit)
  end

end

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Add a training zone. Players in the zone are safe.
-- @param #FOX self
-- @param Core.Zone#ZONE zone Training zone.
-- @return #FOX self
function FOX:AddSafeZone(zone)

  table.insert(self.safezones, zone)

  return self
end

--- Add a launch zone. Only missiles launched within these zones will be tracked.
-- @param #FOX self
-- @param Core.Zone#ZONE zone Training zone.
-- @return #FOX self
function FOX:AddLaunchZone(zone)

  table.insert(self.launchzones, zone)

  return self
end

--- Add a protected set of groups.
-- @param #FOX self
-- @param Core.Set#SET_GROUP groupset The set of groups.
-- @return #FOX self
function FOX:SetProtectedGroupSet(groupset)
  self.protectedset=groupset
  return self
end

--- Add a group to the protected set. Works only with AI!
-- @param #FOX self
-- @param Wrapper.Group#GROUP group Protected group.
-- @return #FOX self
function FOX:AddProtectedGroup(group)

  if not self.protectedset then
    self.protectedset=SET_GROUP:New()
  end

  self.protectedset:AddGroup(group)

  return self
end

--- Set explosion power. This is an "artificial" explosion generated when the missile is destroyed. Just for the visual effect.
-- Don't set the explosion power too big or it will harm the aircraft in the vicinity.
-- @param #FOX self
-- @param #number power Explosion power in kg TNT. Default 0.1 kg.
-- @return #FOX self
function FOX:SetExplosionPower(power)

  self.explosionpower=power or 0.1

  return self
end

--- Set missile-player distance when missile is destroyed.
-- @param #FOX self
-- @param #number distance Distance in meters. Default 200 m.
-- @return #FOX self
function FOX:SetExplosionDistance(distance)

  self.explosiondist=distance or 200

  return self
end

--- Set missile-player distance when BIG missiles are destroyed.
-- @param #FOX self
-- @param #number distance Distance in meters. Default 500 m.
-- @param #number explosivemass Explosive mass of missile threshold in kg TNT. Default 50 kg.
-- @return #FOX self
function FOX:SetExplosionDistanceBigMissiles(distance, explosivemass)

  self.explosiondist2=distance or 500

  self.bigmissilemass=explosivemass or 50

  return self
end

--- Disable F10 menu for all players.
-- @param #FOX self
-- @return #FOX self
function FOX:SetDisableF10Menu()

  self.menudisabled=true

  return self
end


--- Enable F10 menu for all players.
-- @param #FOX self
-- @return #FOX self
function FOX:SetEnableF10Menu()

  self.menudisabled=false

  return self
end

--- Set verbosity level.
-- @param #FOX self
-- @param #number VerbosityLevel Level of output (higher=more). Default 0.
-- @return #FOX self
function FOX:SetVerbosity(VerbosityLevel)
  self.verbose=VerbosityLevel or 0
  return self
end

--- Set default player setting for missile destruction.
-- @param #FOX self
-- @param #boolean switch If true missiles are destroyed. If false/nil missiles are not destroyed.
-- @return #FOX self
function FOX:SetDefaultMissileDestruction(switch)

  if switch==nil then
    self.destroy=false
  else
    self.destroy=switch
  end

  return self
end

--- Set default player setting for launch alerts.
-- @param #FOX self
-- @param #boolean switch If true launch alerts to players are active. If false/nil no launch alerts are given.
-- @return #FOX self
function FOX:SetDefaultLaunchAlerts(switch)

  if switch==nil then
    self.launchalert=false
  else
    self.launchalert=switch
  end

  return self
end

--- Set default player setting for marking missile launch coordinates
-- @param #FOX self
-- @param #boolean switch If true missile launches are marked. If false/nil marks are disabled.
-- @return #FOX self
function FOX:SetDefaultLaunchMarks(switch)

  if switch==nil then
    self.marklaunch=false
  else
    self.marklaunch=switch
  end

  return self
end


--- Set debug mode on/off.
-- @param #FOX self
-- @param #boolean switch If true debug mode on. If false/nil debug mode off.
-- @return #FOX self
function FOX:SetDebugOnOff(switch)

  if switch==nil then
    self.Debug=false
  else
    self.Debug=switch
  end

  return self
end

--- Set debug mode on.
-- @param #FOX self
-- @return #FOX self
function FOX:SetDebugOn()
  self:SetDebugOnOff(true)
  return self
end

--- Set debug mode off.
-- @param #FOX self
-- @return #FOX self
function FOX:SetDebugOff()
  self:SetDebugOff(false)
  return self
end

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Status Functions
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check spawn queue and spawn aircraft if necessary.
-- @param #FOX self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FOX:onafterStatus(From, Event, To)

  -- Get FSM state.
  local fsmstate=self:GetState()

  local time=timer.getAbsTime()
  local clock=UTILS.SecondsToClock(time)

  -- Status.
  if self.verbose>=1 then
    self:I(self.lid..string.format("Missile trainer status %s: %s", clock, fsmstate))
  end

  -- Check missile status.
  self:_CheckMissileStatus()

  -- Check player status.
  self:_CheckPlayers()

  if fsmstate=="Running" then
    self:__Status(-10)
  end
end

--- Check status of players.
-- @param #FOX self
function FOX:_CheckPlayers()

  for playername,_playersettings in pairs(self.players) do
    local playersettings=_playersettings  --#FOX.PlayerData

    local unitname=playersettings.unitname
    local unit=UNIT:FindByName(unitname)

    if unit and unit:IsAlive() then

      local coord=unit:GetCoordinate()

      local issafe=self:_CheckCoordSafe(coord)


      if issafe then

        -----------------------------
        -- Player INSIDE Safe Zone --
        -----------------------------

        if not playersettings.inzone then
          self:EnterSafeZone(playersettings)
          playersettings.inzone=true
        end

      else

        ------------------------------
        -- Player OUTSIDE Safe Zone --
        ------------------------------

        if playersettings.inzone==true then
          self:ExitSafeZone(playersettings)
          playersettings.inzone=false
        end

      end
    end
  end

end

--- Remove missile.
-- @param #FOX self
-- @param #FOX.MissileData missile Missile data.
function FOX:_RemoveMissile(missile)

  if missile then
    for i,_missile in pairs(self.missiles) do
      local m=_missile --#FOX.MissileData
      if missile.missileName==m.missileName then
        table.remove(self.missiles, i)
        return
      end
    end
  end

end

--- Missile status.
-- @param #FOX self
function FOX:_CheckMissileStatus()

  local text="Missiles:"
  local inactive={}
  for i,_missile in pairs(self.missiles) do
    local missile=_missile --#FOX.MissileData

    local targetname="unkown"
    if missile.targetUnit then
      targetname=missile.targetUnit:GetName()
    end
    local playername="none"
    if missile.targetPlayer then
      playername=missile.targetPlayer.name
    end
    local active=tostring(missile.active)
    local mtype=missile.missileType
    local dtype=missile.missileType
    local range=UTILS.MetersToNM(missile.missileRange)

    if not active then
      table.insert(inactive,i)
    end
    local heading=self:_GetWeapongHeading(missile.weapon)

    text=text..string.format("\n[%d] %s: active=%s, range=%.1f NM, heading=%03d, target=%s, player=%s, missilename=%s", i, mtype, active, range, heading, targetname, playername, missile.missileName)

  end
  if #self.missiles==0 then
    text=text.." none"
  end
  if self.verbose>=2 then
    self:I(self.lid..text)
  end

  -- Remove inactive missiles.
  for i=#self.missiles,1,-1 do
    local missile=self.missiles[i] --#FOX.MissileData
    if missile and not missile.active then
      table.remove(self.missiles, i)
    end
  end

end

--- Check if missile target is protected.
-- @param #FOX self
-- @param Wrapper.Unit#UNIT targetunit Target unit.
-- @return #boolean If true, unit is protected.
function FOX:_IsProtected(targetunit)

  if not self.protectedset then
    return false
  end

  if targetunit and targetunit:IsAlive() then

    -- Get Group.
    local targetgroup=targetunit:GetGroup()

    if targetgroup then
      local targetname=targetgroup:GetName()

      for _,_group in pairs(self.protectedset:GetSet()) do
        local group=_group --Wrapper.Group#GROUP

        if group then
          local groupname=group:GetName()

          -- Target belongs to a protected set.
          if targetname==groupname then
            return true
          end
        end

      end
    end
  end

  return false
end


--- Function called from weapon tracking.
-- @param Wrapper.Weapon#WEAPON weapon Weapon object.
-- @param #FOX self FOX object.
-- @param #FOX.MissileData missile Fired missile
function FOX._FuncTrack(weapon, self, missile)

  -- Missile coordinate.
  local missileCoord= missile.missileCoord:UpdateFromVec3(weapon.vec3) --COORDINATE:NewFromVec3(_lastBombPos)

  -- Missile velocity in m/s.
  local missileVelocity=weapon:GetSpeed() --UTILS.VecNorm(_ordnance:getVelocity())

  -- Update missile target if necessary.
  self:GetMissileTarget(missile)

 -- Target unit of the missile.
  local target=nil --Wrapper.Unit#UNIT

  if missile.targetUnit then

    -----------------------------------
    -- Missile has a specific target --
    -----------------------------------

    if missile.targetPlayer then
      -- Target is a player.
      if missile.targetPlayer.destroy==true then
        target=missile.targetUnit
      end
    else
      -- Check if unit is protected.
      if self:_IsProtected(missile.targetUnit) then
        target=missile.targetUnit
      end
    end

  else

    ------------------------------------
    -- Missile has NO specific target --
    ------------------------------------

    -- TODO: This might cause a problem with wingman. Even if the shooter itself is excluded from the check, it's wingmen are not.
    --       That would trigger the distance check right after missile launch if things to wrong.
    --
    --       Possible solutions:
    --       * Time check: enable this check after X seconds after missile was fired. What is X?
    --       * Coalition check. But would not work in training situations where blue on blue is valid!
    --       * At least enable it for surface-to-air missiles.

    local function _GetTarget(_unit)
      local unit=_unit --Wrapper.Unit#UNIT

      -- Player position.
      local playerCoord=unit:GetCoordinate()

      -- Distance.
      local dist=missileCoord:Get3DDistance(playerCoord)

      -- Update mindist if necessary. Only include players in range of missile + 50% safety margin.
      if dist<=self.explosiondist then
        return unit
      end
    end

    -- Distance to closest player.
    local mindist=nil

    -- Loop over players.
    for _,_player in pairs(self.players) do
      local player=_player  --#FOX.PlayerData

      -- Check that player was not the one who launched the missile.
      if player.unitname~=missile.shooterName then

        -- Player position.
        local playerCoord=player.unit:GetCoordinate()

        -- Distance.
        local dist=missileCoord:Get3DDistance(playerCoord)

        -- Distance from shooter to player.
        local Dshooter2player=playerCoord:Get3DDistance(missile.shotCoord)

        -- Update mindist if necessary. Only include players in range of missile + 50% safety margin.
        if (mindist==nil or dist<mindist) and (Dshooter2player<=missile.missileRange*1.5 or dist<=self.explosiondist) then
          mindist=dist
          target=player.unit
        end
      end
    end

    if self.protectedset then

      -- Distance to closest protected unit.
      mindist=nil

      for _,_group in pairs(self.protectedset:GetSet()) do
        local group=_group --Wrapper.Group#GROUP
        for _,_unit in pairs(group:GetUnits()) do
          local unit=_unit --Wrapper.Unit#UNIT

          if unit and unit:IsAlive() then

            -- Check that player was not the one who launched the missile.
            if unit:GetName()~=missile.shooterName then

              -- Player position.
              local playerVec3=unit:GetVec3()

              -- Distance.
              local dist=missileCoord:Get3DDistance(playerVec3)

              -- Distance from shooter to player.
              local Dshooter2player=missile.shotCoord:Get3DDistance(playerVec3)

              -- Update mindist if necessary. Only include players in range of missile + 50% safety margin.
              if (mindist==nil or dist<mindist) and (Dshooter2player<=missile.missileRange*1.5 or dist<=self.explosiondist) then
                mindist=dist
                target=unit
              end
            end

          end
        end
      end
    end

    if target then
      self:T(self.lid..string.format("Missile %s with NO explicit target got closest unit to missile as target %s. Dist=%s m", missile.missileType, target:GetName(), tostring(mindist)))
    end

  end

  -- Check if missile has a valid target.
  if target then

    -- Target coordinate.
    local targetVec3=target:GetVec3() --target:GetCoordinate()

    -- Distance from missile to target.
    local distance=missileCoord:Get3DDistance(targetVec3)

    -- Distance missile to shooter.
    local distShooter=nil
    if missile.shooterUnit and missile.shooterUnit:IsAlive() then
      distShooter=missileCoord:Get3DDistance(missile.shooterUnit:GetVec3())
    end


    -- Debug output.
    if self.Debug then
      local bearing=missileCoord:HeadingTo(targetVec3)
      local eta=distance/missileVelocity
      -- Debug distance check.
      self:I(self.lid..string.format("Missile %s Target %s: Distance = %.1f m, v=%.1f m/s, bearing=%03d°, ETA=%.1f sec", missile.missileType, target:GetName(), distance, missileVelocity, bearing, eta))
    end

    -- Distroy missile if it's getting too close.
    local destroymissile=distance<=self.explosiondist

    -- Check BIG missiles.
    if self.explosiondist2 and distance<=self.explosiondist2 and not destroymissile then
       destroymissile=missile.explosive>=self.bigmissilemass
    end

    -- If missile is 150 m from target ==> destroy missile if in safe zone.
    if destroymissile and self:_CheckCoordSafe(targetVec3) then

      -- Destroy missile.
      self:I(self.lid..string.format("Destroying missile %s(%s) fired by %s aimed at %s [player=%s] at distance %.1f m",
      missile.missileType, missile.missileName, missile.shooterName, target:GetName(), tostring(missile.targetPlayer~=nil), distance))
      weapon:Destroy()

      -- Missile is not active any more.
      missile.active=false

      -- Debug smoke.
      if self.Debug then
        missileCoord:SmokeRed()
      end

      -- Create event.
      self:MissileDestroyed(missile)

      -- Little explosion for the visual effect.
      if self.explosionpower>0 and distance>50 and (distShooter==nil or (distShooter and distShooter>50)) then
        missileCoord:Explosion(self.explosionpower)
      end

      -- Target was a player.
      if missile.targetPlayer then

        -- Message to target.
        local text=string.format("Destroying missile. %s", self:_DeadText())
        MESSAGE:New(text, 10):ToGroup(target:GetGroup())

        -- Increase dead counter.
        missile.targetPlayer.dead=missile.targetPlayer.dead+1
      end

      -- We could disable the tracking here but then the impact function would not be called.
      --weapon.tracking=false

    else

      -- Time step.
      local dt=1.0
      if distance>50000 then
        -- > 50 km
        dt=self.dt50 --=5.0
      elseif distance>10000 then
        -- 10-50 km
        dt=self.dt10 --=1.0
      elseif distance>5000 then
        -- 5-10 km
        dt=self.dt05 --0.5
      elseif distance>1000 then
        -- 1-5 km
        dt=self.dt01 --0.1
      else
        -- < 1 km
        dt=self.dt00 --0.01
      end

      -- Set time step.
      weapon:SetTimeStepTrack(dt)
    end

  else

    -- No current target.
    self:T(self.lid..string.format("Missile %s(%s) fired by %s has no current target. Checking back in 0.1 sec.",  missile.missileType, missile.missileName, missile.shooterName))
    weapon:SetTimeStepTrack(0.1)

  end

end

--- Callback function on impact or destroy otherwise.
-- @param Wrapper.Weapon#WEAPON weapon Weapon object.
-- @param #FOX self FOX object.
-- @param #FOX.MissileData missile Fired missile.
function FOX._FuncImpact(weapon, self, missile)

  if missile.targetPlayer then

    -- Get human player.
    local player=missile.targetPlayer

    -- Check for player and distance < 10 km.
    if player and player.unit:IsAlive() then -- and missileCoord and player.unit:GetCoordinate():Get3DDistance(missileCoord)<10*1000 then
      local text=string.format("Missile defeated. Well done, %s!", player.name)
      MESSAGE:New(text, 10):ToClient(player.client)

      -- Increase defeated counter.
      player.defeated=player.defeated+1
    end

  end

  -- Missile is not active any more.
  missile.active=false

  --Terminate the timer.
  self:T(FOX.lid..string.format("Terminating missile track timer."))
  weapon.tracking=false

end

--- Missle launch event.
-- @param #FOX self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #FOX.MissileData missile Fired missile
function FOX:onafterMissileLaunch(From, Event, To, missile)

  -- Tracking info and init of last bomb position.
  local text=string.format("FOX: Tracking missile %s(%s) - target %s - shooter %s", missile.missileType, missile.missileName, tostring(missile.targetName), missile.shooterName)
  self:T(FOX.lid..text)
  MESSAGE:New(text, 10):ToAllIf(self.Debug)

  -- Loop over players.
  for _,_player in pairs(self.players) do
    local player=_player  --#FOX.PlayerData

    -- Player position.
    local playerUnit=player.unit

    -- Check that player is alive and of the opposite coalition.
    if playerUnit and playerUnit:IsAlive() and player.coalition~=missile.shooterCoalition then

      -- Player missile distance.
      local distance=playerUnit:GetCoordinate():Get3DDistance(missile.shotCoord)

      -- Player bearing to missile.
      local bearing=playerUnit:GetCoordinate():HeadingTo(missile.shotCoord)

      -- Alert that missile has been launched.
      if player.launchalert then

        -- Alert directly targeted players or players that are within missile max range.
        if (missile.targetPlayer and player.unitname==missile.targetPlayer.unitname) or (distance<missile.missileRange)  then

          -- Inform player.
          local text=string.format("Missile launch detected! Distance %.1f NM, bearing %03d°.", UTILS.MetersToNM(distance), bearing)

          -- Say notching headings.
          self:ScheduleOnce(5, FOX._SayNotchingHeadings, self, player, missile.weapon)

          --TODO: ALERT or INFO depending on whether this is a direct target.
          --TODO: lauchalertall option.
          MESSAGE:New(text, 5, "ALERT"):ToClient(player.client)

        end

      end

      -- Mark coordinate.
      if player.marklaunch then
        local text=string.format("Missile launch coordinates:\n%s\n%s", missile.shotCoord:ToStringLLDMS(), missile.shotCoord:ToStringBULLS(player.coalition))
        missile.shotCoord:MarkToGroup(text, player.group)
      end

    end
  end

  -- Set callback function for tracking.
  missile.Weapon:SetFuncTrack(FOX._FuncTrack, self, missile)

  -- Set callback function for impact.
  missile.Weapon:SetFuncImpact(FOX._FuncImpact, self, missile)


  -- Weapon is not yet "alife" just yet. Start timer with a little delay.
  self:T(FOX.lid..string.format("Tracking of missile starts in 0.0001 seconds."))
  --timer.scheduleFunction(trackMissile, missile.weapon, timer.getTime()+0.0001)
  missile.Weapon:StartTrack(0.0001)

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- FOX event handler for event birth.
-- @param #FOX self
-- @param Core.Event#EVENTDATA EventData
function FOX:OnEventPlayerEnterAircraft(EventData)

end

--- FOX event handler for event birth.
-- @param #FOX self
-- @param Core.Event#EVENTDATA EventData
function FOX:OnEventBirth(EventData)
  self:F3({eventbirth = EventData})

  -- Nil checks.
  if EventData==nil then
    self:E(self.lid.."ERROR: EventData=nil in event BIRTH!")
    self:E(EventData)
    return
  end
  if EventData.IniUnit==nil then
    self:E(self.lid.."ERROR: EventData.IniUnit=nil in event BIRTH!")
    self:E(EventData)
    return
  end

  -- Player unit and name.
  local _unitName=EventData.IniUnitName
  local playerunit, playername=self:_GetPlayerUnitAndName(_unitName)

  -- Debug info.
  self:T(self.lid.."BIRTH: unit   = "..tostring(EventData.IniUnitName))
  self:T(self.lid.."BIRTH: group  = "..tostring(EventData.IniGroupName))
  self:T(self.lid.."BIRTH: player = "..tostring(playername))

  -- Check if player entered.
  if playerunit and playername then

    local _uid=playerunit:GetID()
    local _group=playerunit:GetGroup()
    local _callsign=playerunit:GetCallsign()

    -- Debug output.
    local text=string.format("Pilot %s, callsign %s entered unit %s of group %s.", playername, _callsign, _unitName, _group:GetName())
    self:T(self.lid..text)
    MESSAGE:New(text, 5):ToAllIf(self.Debug)

    -- Add F10 radio menu for player.
    if not self.menudisabled then
      self:ScheduleOnce(0.1, self._AddF10Commands, self, _unitName)
    end

    -- Player data.
    local playerData={} --#FOX.PlayerData

    -- Player unit, client and callsign.
    playerData.unit      = playerunit
    playerData.unitname  = _unitName
    playerData.group     = _group
    playerData.groupname = _group:GetName()
    playerData.name      = playername
    playerData.callsign  = playerData.unit:GetCallsign()
    playerData.client    = CLIENT:FindByName(_unitName, nil, true)
    playerData.coalition = _group:GetCoalition()

    playerData.destroy=playerData.destroy or self.destroy
    playerData.launchalert=playerData.launchalert or self.launchalert
    playerData.marklaunch=playerData.marklaunch or self.marklaunch

    playerData.defeated=playerData.defeated or 0
    playerData.dead=playerData.dead or 0

    -- Init player data.
    self.players[playername]=playerData

  end
end

--- Get missile target.
-- @param #FOX self
-- @param #FOX.MissileData missile The missile data table.
function FOX:GetMissileTarget(missile)

  local target=nil
  local targetName="unknown"
  local targetUnit=nil --Wrapper.Unit#UNIT

  if missile.weapon and missile.weapon:isExist() then

    -- Get target of missile.
    target=missile.weapon:getTarget()

    -- Get the target unit. Note if if _target is not nil, the unit can sometimes not be found!
    if target then
      self:T2({missiletarget=target})

      -- Get target unit.
      targetUnit=UNIT:Find(target)

      if targetUnit then
        targetName=targetUnit:GetName()

        missile.targetUnit=targetUnit
        missile.targetPlayer=self:_GetPlayerFromUnit(missile.targetUnit)
      end

    end
  end

  -- Missile got new target.
  if missile.targetName and missile.targetName~=targetName then
    self:I(self.lid..string.format("Missile %s(%s) changed target to %s. Previous target was %s.", missile.missileType, missile.missileName, targetName, missile.targetName))
  end

  -- Set target name.
  missile.targetName=targetName

end

--- FOX event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun).
-- @param #FOX self
-- @param Core.Event#EVENTDATA EventData
function FOX:OnEventShot(EventData)
  self:T2({eventshot=EventData})

  -- Nil checks.
  if EventData.Weapon==nil or EventData.IniDCSUnit==nil or EventData.weapon==nil then
    return
  end

  -- Create a weapon object.
  local weapon=WEAPON:New(EventData.weapon)

  -- Weapon data.
  local _weapon     = weapon:GetTypeName()
  local _target     = EventData.Weapon:getTarget()
  local _targetName = "unknown"
  local _targetUnit = nil --Wrapper.Unit#UNIT

  -- Weapon descriptor.
  local desc=weapon.desc
  self:T2({desc=desc})

  -- Missile category: 1=AAM, 2=SAM, 6=OTHER
  local missilecategory=desc.missileCategory

  -- Missile range.
  local missilerange=nil
  if missilecategory then
    missilerange=desc.rangeMaxAltMax
  end

  -- Debug info.
  self:T2(FOX.lid.."EVENT SHOT: FOX")
  self:T2(FOX.lid..string.format("EVENT SHOT: Ini unit     = %s", tostring(EventData.IniUnitName)))
  self:T2(FOX.lid..string.format("EVENT SHOT: Ini group    = %s", tostring(EventData.IniGroupName)))
  self:T2(FOX.lid..string.format("EVENT SHOT: Weapon type  = %s", tostring(weapon:GetTypeName())))
  self:T2(FOX.lid..string.format("EVENT SHOT: Weapon categ = %s", tostring(weapon:GetCategory())))
  self:T2(FOX.lid..string.format("EVENT SHOT: Missil categ = %s", tostring(missilecategory)))
  self:T2(FOX.lid..string.format("EVENT SHOT: Missil range = %s", tostring(missilerange)))


  -- Check if fired in launch zone.
  if not self:_CheckCoordLaunch(EventData.IniUnit:GetCoordinate()) then
    self:T(self.lid.."Missile was not fired in launch zone. No tracking!")
    return
  end

  -- Track missiles of type AAM=1, SAM=2 or OTHER=6
  local _track = weapon:IsMissile() and missilecategory and (missilecategory==1 or missilecategory==2 or missilecategory==6)

  -- Only track missiles
  if _track then

    local missile={} --#FOX.MissileData

    missile.active=true
    missile.weapon=EventData.weapon
    missile.Weapon=weapon
    missile.missileType=_weapon
    missile.missileRange=missilerange
    missile.missileName=EventData.weapon:getName()
    missile.shooterUnit=EventData.IniUnit
    missile.shooterGroup=EventData.IniGroup
    missile.shooterCoalition=EventData.IniUnit:GetCoalition()
    missile.shooterName=EventData.IniUnitName
    missile.shotTime=timer.getAbsTime()
    missile.shotCoord=EventData.IniUnit:GetCoordinate()
    missile.fuseDist=desc.fuseDist
    missile.explosive=desc.warhead.explosiveMass or desc.warhead.shapedExplosiveMass
    missile.targetOrig=missile.targetName
    missile.missileCoord=COORDINATE:New(0,0,0)

    -- Set missile target name, unit and player.
    self:GetMissileTarget(missile)

    self:I(FOX.lid..string.format("EVENT SHOT: Shooter=%s %s(%s) ==> Target=%s, fuse dist=%s, explosive=%s",
    tostring(missile.shooterName), tostring(missile.missileType), tostring(missile.missileName), tostring(missile.targetName), tostring(missile.fuseDist), tostring(missile.explosive)))

    -- Only track if target was a player or target is protected. Saw the 9M311 missiles have no target!
    if missile.targetPlayer or self:_IsProtected(missile.targetUnit) or missile.targetName=="unknown" then

      -- Add missile table.
      table.insert(self.missiles, missile)

      -- Trigger MissileLaunch event.
      self:__MissileLaunch(0.1, missile)

    end

  end --if _track

end

--- FOX event handler for event hit.
-- @param #FOX self
-- @param Core.Event#EVENTDATA EventData
function FOX:OnEventHit(EventData)
  self:T({eventhit = EventData})

  -- Nil checks.
  if EventData.Weapon==nil then
    return
  end
  if EventData.IniUnit==nil then
    return
  end
  if EventData.TgtUnit==nil then
    return
  end

  local weapon=EventData.Weapon
  local weaponname=weapon:getName()

  for i,_missile in pairs(self.missiles) do
    local missile=_missile --#FOX.MissileData
    if missile.missileName==weaponname then
      self:I(self.lid..string.format("WARNING: Missile %s (%s) hit target %s. Missile trainer target was %s.", missile.missileType, missile.missileName, EventData.TgtUnitName, missile.targetName))
      self:I({missile=missile})
      return
    end
  end

end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- RADIO MENU Functions
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Add menu commands for player.
-- @param #FOX self
-- @param #string _unitName Name of player unit.
function FOX:_AddF10Commands(_unitName)
  self:F(_unitName)

  -- Get player unit and name.
  local _unit, playername = self:_GetPlayerUnitAndName(_unitName)

  -- Check for player unit.
  if _unit and playername then

    -- Get group and ID.
    local group=_unit:GetGroup()
    local gid=group:GetID()

    if group and gid then

      if not self.menuadded[gid] then

        -- Enable switch so we don't do this twice.
        self.menuadded[gid]=true

        -- Set menu root path.
        local _rootPath=nil
        if FOX.MenuF10Root then
          ------------------------
          -- MISSON LEVEL MENUE --
          ------------------------

          -- F10/FOX/...
          _rootPath=FOX.MenuF10Root

        else
          ------------------------
          -- GROUP LEVEL MENUES --
          ------------------------

          -- Main F10 menu: F10/FOX/
          if FOX.MenuF10[gid]==nil then
            FOX.MenuF10[gid]=missionCommands.addSubMenuForGroup(gid, "FOX")
          end

          -- F10/FOX/...
          _rootPath=FOX.MenuF10[gid]

        end


        --------------------------------
        -- F10/F<X> FOX/F1 Help
        --------------------------------
        --local _helpPath=missionCommands.addSubMenuForGroup(gid, "Help", _rootPath)
        -- F10/FOX/F1 Help/
        --missionCommands.addCommandForGroup(gid, "Subtitles On/Off",    _helpPath, self._SubtitlesOnOff,      self, _unitName)   -- F7
        --missionCommands.addCommandForGroup(gid, "Trapsheet On/Off",    _helpPath, self._TrapsheetOnOff,      self, _unitName)   -- F8

        -------------------------
        -- F10/F<X> FOX/
        -------------------------

        missionCommands.addCommandForGroup(gid, "Destroy Missiles On/Off", _rootPath, self._ToggleDestroyMissiles, self, _unitName) -- F1
        missionCommands.addCommandForGroup(gid, "Launch Alerts On/Off",    _rootPath, self._ToggleLaunchAlert,     self, _unitName) -- F2
        missionCommands.addCommandForGroup(gid, "Mark Launch On/Off",      _rootPath, self._ToggleLaunchMark,      self, _unitName) -- F3
        missionCommands.addCommandForGroup(gid, "My Status",               _rootPath, self._MyStatus,              self, _unitName) -- F4

      end
    else
      self:E(self.lid..string.format("ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName or "unknown"))
    end
  else
    self:E(self.lid..string.format("ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName or "unknown"))
  end

end


--- Turn player's launch alert on/off.
-- @param #FOX self
-- @param #string _unitname Name of the player unit.
function FOX:_MyStatus(_unitname)
  self:F2(_unitname)

  -- Get player unit and player name.
  local unit, playername = self:_GetPlayerUnitAndName(_unitname)

  -- Check if we have a player.
  if unit and playername then

    -- Player data.
    local playerData=self.players[playername]  --#FOX.PlayerData

    if playerData then

      local m,mtext=self:_GetTargetMissiles(playerData.name)

      local text=string.format("Status of player %s:\n", playerData.name)
      local safe=self:_CheckCoordSafe(playerData.unit:GetCoordinate())

      text=text..string.format("Destroy missiles? %s\n", tostring(playerData.destroy))
      text=text..string.format("Launch alert? %s\n", tostring(playerData.launchalert))
      text=text..string.format("Launch marks? %s\n", tostring(playerData.marklaunch))
      text=text..string.format("Am I safe? %s\n", tostring(safe))
      text=text..string.format("Missiles defeated: %d\n", playerData.defeated)
      text=text..string.format("Missiles destroyed: %d\n", playerData.dead)
      text=text..string.format("Me target: %d\n%s", m, mtext)

      MESSAGE:New(text, 10, nil, true):ToClient(playerData.client)

    end
  end
end

--- Turn player's launch alert on/off.
-- @param #FOX self
-- @param #string playername Name of the player.
-- @return #number Number of missiles targeting the player.
-- @return #string Missile info.
function FOX:_GetTargetMissiles(playername)

  local text=""
  local n=0
  for _,_missile in pairs(self.missiles) do
    local missile=_missile --#FOX.MissileData

    if missile.targetPlayer and missile.targetPlayer.name==playername then
      n=n+1
      text=text..string.format("Type %s: active %s\n", missile.missileType, tostring(missile.active))
    end

  end

  return n,text
end

--- Turn player's launch alert on/off.
-- @param #FOX self
-- @param #string _unitname Name of the player unit.
function FOX:_ToggleLaunchAlert(_unitname)
  self:F2(_unitname)

  -- Get player unit and player name.
  local unit, playername = self:_GetPlayerUnitAndName(_unitname)

  -- Check if we have a player.
  if unit and playername then

    -- Player data.
    local playerData=self.players[playername]  --#FOX.PlayerData

    if playerData then

      -- Invert state.
      playerData.launchalert=not playerData.launchalert

      -- Inform player.
      local text=""
      if playerData.launchalert==true then
        text=string.format("%s, missile launch alerts are now ENABLED.", playerData.name)
      else
        text=string.format("%s, missile launch alerts are now DISABLED.", playerData.name)
      end
      MESSAGE:New(text, 5):ToClient(playerData.client)

    end
  end
end

--- Turn player's launch marks on/off.
-- @param #FOX self
-- @param #string _unitname Name of the player unit.
function FOX:_ToggleLaunchMark(_unitname)
  self:F2(_unitname)

  -- Get player unit and player name.
  local unit, playername = self:_GetPlayerUnitAndName(_unitname)

  -- Check if we have a player.
  if unit and playername then

    -- Player data.
    local playerData=self.players[playername]  --#FOX.PlayerData

    if playerData then

      -- Invert state.
      playerData.marklaunch=not playerData.marklaunch

      -- Inform player.
      local text=""
      if playerData.marklaunch==true then
        text=string.format("%s, missile launch marks are now ENABLED.", playerData.name)
      else
        text=string.format("%s, missile launch marks are now DISABLED.", playerData.name)
      end
      MESSAGE:New(text, 5):ToClient(playerData.client)

    end
  end
end


--- Turn destruction of missiles on/off for player.
-- @param #FOX self
-- @param #string _unitname Name of the player unit.
function FOX:_ToggleDestroyMissiles(_unitname)
  self:F2(_unitname)

  -- Get player unit and player name.
  local unit, playername = self:_GetPlayerUnitAndName(_unitname)

  -- Check if we have a player.
  if unit and playername then

    -- Player data.
    local playerData=self.players[playername]  --#FOX.PlayerData

    if playerData then

      -- Invert state.
      playerData.destroy=not playerData.destroy

      -- Inform player.
      local text=""
      if playerData.destroy==true then
        text=string.format("%s, incoming missiles will be DESTROYED.", playerData.name)
      else
        text=string.format("%s, incoming missiles will NOT be DESTROYED.", playerData.name)
      end
      MESSAGE:New(text, 5):ToClient(playerData.client)

    end
  end
end


---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get a random text message in case you die.
-- @param #FOX self
-- @return #string Text in case you die.
function FOX:_DeadText()

  local texts={}
  texts[1]="You're dead!"
  texts[2]="Meet your maker!"
  texts[3]="Time to meet your maker!"
  texts[4]="Well, I guess that was it!"
  texts[5]="Bye, bye!"
  texts[6]="Cheers buddy, was nice knowing you!"

  local r=math.random(#texts)

  return texts[r]
end


--- Check if a coordinate lies within a safe training zone.
-- @param #FOX self
-- @param Core.Point#COORDINATE coord Coordinate to check. Can also be a DCS#Vec3.
-- @return #boolean True if safe.
function FOX:_CheckCoordSafe(coord)

  -- No safe zones defined ==> Everything is safe.
  if #self.safezones==0 then
    return true
  end

  -- Loop over all zones.
  for _,_zone in pairs(self.safezones) do
    local zone=_zone --Core.Zone#ZONE
    local Vec2={x=coord.x, y=coord.z}
    local inzone=zone:IsVec2InZone(Vec2)
    --local inzone=zone:IsCoordinateInZone(coord)
    if inzone then
      return true
    end
  end

  return false
end

--- Check if a coordinate lies within a launch zone.
-- @param #FOX self
-- @param Core.Point#COORDINATE coord Coordinate to check. Can also be a DCS#Vec2.
-- @return #boolean True if in launch zone.
function FOX:_CheckCoordLaunch(coord)

  -- No safe zones defined ==> Everything is safe.
  if #self.launchzones==0 then
    return true
  end

  -- Loop over all zones.
  for _,_zone in pairs(self.launchzones) do
    local zone=_zone --Core.Zone#ZONE
    local Vec2={x=coord.x, y=coord.z}
    local inzone=zone:IsVec2InZone(Vec2)
    --local inzone=zone:IsCoordinateInZone(coord)
    if inzone then
      return true
    end
  end

  return false
end

--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned.
-- @param #FOX self
-- @param DCS#Weapon weapon The weapon.
-- @return #number Heading of weapon in degrees or -1.
function FOX:_GetWeapongHeading(weapon)

  if weapon and weapon:isExist() then

    local wp=weapon:getPosition()

    local wph = math.atan2(wp.x.z, wp.x.x)

    if wph < 0 then
      wph=wph+2*math.pi
    end

    wph=math.deg(wph)

    return wph
  end

  return -1
end

--- Tell player notching headings.
-- @param #FOX self
-- @param #FOX.PlayerData playerData Player data.
-- @param DCS#Weapon weapon The weapon.
function FOX:_SayNotchingHeadings(playerData, weapon)

  if playerData and playerData.unit and playerData.unit:IsAlive() then

    local nr, nl=self:_GetNotchingHeadings(weapon)

    if nr and nl then
      local text=string.format("Notching heading %03d° or %03d°", nr, nl)
      MESSAGE:New(text, 5, "FOX"):ToClient(playerData.client)
    end

  end

end

--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned.
-- @param #FOX self
-- @param DCS#Weapon weapon The weapon.
-- @return #number Notching heading right, i.e. missile heading +90°.
-- @return #number Notching heading left, i.e. missile heading -90°.
function FOX:_GetNotchingHeadings(weapon)

  if weapon then

    local hdg=self:_GetWeapongHeading(weapon)

    local hdg1=hdg+90
    if hdg1>360 then
      hdg1=hdg1-360
    end

    local hdg2=hdg-90
    if hdg2<0 then
      hdg2=hdg2+360
    end

    return hdg1, hdg2
  end

  return nil, nil
end

--- Returns the player data from a unit name.
-- @param #FOX self
-- @param #string unitName Name of the unit.
-- @return #FOX.PlayerData Player data.
function FOX:_GetPlayerFromUnitname(unitName)

  for _,_player in pairs(self.players) do
    local player=_player --#FOX.PlayerData

    if player.unitname==unitName then
      return player
    end
  end

  return nil
end

--- Retruns the player data from a unit.
-- @param #FOX self
-- @param Wrapper.Unit#UNIT unit
-- @return #FOX.PlayerData Player data.
function FOX:_GetPlayerFromUnit(unit)

  if unit and unit:IsAlive() then

    -- Name of the unit
    local unitname=unit:GetName()

    for _,_player in pairs(self.players) do
      local player=_player --#FOX.PlayerData

      if player.unitname==unitname then
        return player
      end
    end

  end

  return nil
end

--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned.
-- @param #FOX self
-- @param #string _unitName Name of the player unit.
-- @return Wrapper.Unit#UNIT Unit of player or nil.
-- @return #string Name of the player or nil.
function FOX:_GetPlayerUnitAndName(_unitName)
  self:F2(_unitName)

  if _unitName ~= nil then

    -- Get DCS unit from its name.
    local DCSunit=Unit.getByName(_unitName)

    if DCSunit then

      -- Get player name if any.
      local playername=DCSunit:getPlayerName()

      -- Unit object.
      local unit=UNIT:Find(DCSunit)

      -- Debug.
      self:T2({DCSunit=DCSunit, unit=unit, playername=playername})

      -- Check if enverything is there.
      if DCSunit and unit and playername then
        self:T(self.lid..string.format("Found DCS unit %s with player %s.", tostring(_unitName), tostring(playername)))
        return unit, playername
      end

    end

  end

  -- Return nil if we could not find a player.
  return nil,nil
end


---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ **Functional** - Modular, Automatic and Network capable Targeting and Interception System for Air Defenses.
--
-- ===
--
-- ## Features:
--
--  * Moose derived  Modular, Automatic and Network capable Targeting and Interception System.
--  * Controls a network of SAM sites. Uses detection to switch on the AA site closest to the enemy.   
--  * Automatic mode (default since 0.8) can set-up your SAM site network automatically for you.   
--  * Leverage evasiveness from SEAD, leverage attack range setting.   
--
-- ===
--
-- ## Missions:
--
-- ### [MANTIS - Modular, Automatic and Network capable Targeting and Interception System](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Functional/Mantis)
--
-- ===
--
-- ### Author : **applevangelist **
--
-- @module Functional.Mantis
-- @image Functional.Mantis.jpg
--
-- Last Update: August 2025

-------------------------------------------------------------------------
--- **MANTIS** class, extends Core.Base#BASE
-- @type MANTIS
-- @field #string ClassName
-- @field #string name Name of this Mantis
-- @field #string SAM_Templates_Prefix Prefix to build the #SET_GROUP for SAM sites
-- @field Core.Set#SET_GROUP SAM_Group The SAM #SET_GROUP
-- @field #string EWR_Templates_Prefix Prefix to build the #SET_GROUP for EWR group
-- @field Core.Set#SET_GROUP EWR_Group The EWR #SET_GROUP
-- @field Core.Set#SET_GROUP Adv_EWR_Group The EWR #SET_GROUP used for advanced mode
-- @field #string HQ_Template_CC The ME name of the HQ object
-- @field Wrapper.Group#GROUP HQ_CC The #GROUP object of the HQ
-- @field #table SAM_Table Table of SAM sites
-- @field #string lid Prefix for logging
-- @field Functional.Detection#DETECTION_AREAS Detection The #DETECTION_AREAS object for EWR
-- @field Functional.Detection#DETECTION_AREAS AWACS_Detection The #DETECTION_AREAS object for AWACS
-- @field #boolean debug Switch on extra messages
-- @field #boolean verbose Switch on extra logging
-- @field #number checkradius Radius of the SAM sites
-- @field #number grouping Radius to group detected objects
-- @field #number acceptrange Radius of the EWR detection
-- @field #number detectinterval Interval in seconds for the target detection
-- @field #number engagerange Firing engage range of the SAMs, see [https://wiki.hoggitworld.com/view/DCS_option_engagementRange]
-- @field #boolean autorelocate Relocate HQ and EWR groups in random intervals. Note: You need to select units for this which are *actually mobile*
-- @field #boolean advanced Use advanced mode, will decrease reactivity of MANTIS, if HQ and/or EWR network dies. Set SAMs to RED state if both are dead. Requires usage of an HQ object
-- @field #number adv_ratio Percentage to use for advanced mode, defaults to 100%
-- @field #number adv_state Advanced mode state tracker
-- @field #boolean advAwacs Boolean switch to use Awacs as a separate detection stream
-- @field #number awacsrange Detection range of an optional Awacs unit
-- @field #boolean UseEmOnOff Decide if we are using Emissions on/off (true) or AlarmState red/green (default)
-- @field Functional.Shorad#SHORAD Shorad SHORAD Object, if available
-- @field #boolean ShoradLink If true, #MANTIS has #SHORAD enabled
-- @field #number ShoradTime Timer in seconds, how long #SHORAD will be active after a detection inside of the defense range
-- @field #number ShoradActDistance Distance of an attacker in meters from a Mantis SAM site, on which Shorad will be switched on. Useful to not give away Shorad sites too early. Default 15km. Should be smaller than checkradius.
-- @field #boolean checkforfriendlies If true, do not activate a SAM installation if a friendly aircraft is in firing range.
-- @field #table FilterZones Table of Core.Zone#ZONE Zones Consider SAM groups in this zone(s) only for this MANTIS instance, must be handed as #table of Zone objects.
-- @field #boolean SmokeDecoy If true, smoke short range SAM units as decoy if a plane is in firing range.
-- @field #number SmokeDecoyColor Color to use, defaults to SMOKECOLOR.White
-- @field #number checkcounter Counter for SAM Table refreshes.
-- @field #number DLinkCacheTime Seconds after which cached contacts in DLink will decay.
-- @field #boolean logsamstatus Log SAM status in dcs.log every cycle if true
-- @extends Core.Base#BASE


--- *The worst thing that can happen to a good cause is, not to be skillfully attacked, but to be ineptly defended.* - Frédéric Bastiat
--
-- Moose class for a more intelligent Air Defense System
--
-- # MANTIS
-- 
-- * Moose derived  Modular, Automatic and Network capable Targeting and Interception System.
-- * Controls a network of SAM sites. Uses detection to switch on the SAM site closest to the enemy.
-- * **Automatic mode** (default) will set-up your SAM site network automatically for you.
-- * Leverage evasiveness from SEAD, leverage attack range setting.
-- * Automatic setup of SHORAD based on groups of the class "short-range".
--
-- # 0. Base considerations and naming conventions
-- 
-- **Before** you start to set up your SAM sites in the mission editor, please think of naming conventions. This is especially critical to make
-- eveything work as intended, also if you have both a blue and a red installation!
-- 
-- You need three **non-overlapping** "name spaces" for everything to work properly:
-- 
-- * SAM sites, e.g. each **group name** begins with "Red SAM"
-- * EWR network and AWACS, e.g. each **group name** begins with "Red EWR" and *not* e.g. "Red SAM EWR" (overlap with  "Red SAM"), "Red EWR Awacs" will be found by "Red EWR"
-- * SHORAD, e.g. each **group name** begins with "Red SHORAD" and *not" e.g. just "SHORAD" because you might also have "Blue SHORAD"
-- * Point Defense, e.g. each **group name** begins with "Red AAA" and *not" e.g. just "AAA" because you might also have "Blue AAA"
-- 
-- It's important to get this right because of the nature of the filter-system in @{Core.Set#SET_GROUP}. Filters are "greedy", that is they
-- will match *any* string that contains the search string - hence we need to avoid that SAMs, EWR and SHORAD step on each other\'s toes.
-- 
-- Second, for auto-mode to work, the SAMs need the **SAM Type Name** in their group name, as MANTIS will determine their capabilities from this.
-- This is case-sensitive, so "sa-11" is not equal to "SA-11" is not equal to "Sa-11"!
-- 
-- Known SAM types at the time of writing are:
-- 
-- * Avenger
-- * Chaparral
-- * Hawk
-- * Linebacker
-- * NASAMS
-- * Patriot
-- * Rapier
-- * Roland
-- * IRIS-T SLM
-- * Pantsir S1
-- * TOR M2
-- * C-RAM
-- * Silkworm (though strictly speaking this is a surface to ship missile)
-- * SA-2, SA-3, SA-5, SA-6, SA-7, SA-8, SA-9, SA-10, SA-11, SA-13, SA-15, SA-19
-- * From IDF mod: STUNNER IDFA, TAMIR IDFA (Note all caps!)
-- * From HDS (see note on HDS below): SA-2, SA-3, SA-10B, SA-10C, SA-12, SA-17, SA-20A, SA-20B, SA-23, HQ-2, SAMP/T Block 1, SAMP/T Block 1INT,  SAMP/T Block2
-- * Other Mods: Nike
-- 
-- * From SMA: RBS98M, RBS70, RBS90, RBS90M, RBS103A, RBS103B, RBS103AM, RBS103BM, Lvkv9040M 
-- **NOTE** If you are using the Swedish Military Assets (SMA), please note that the **group name** for RBS-SAM types also needs to contain the keyword "SMA"
-- 
-- * From CH: 2S38, PantsirS1, PantsirS2, PGL-625, HQ-17A, M903PAC2, M903PAC3, TorM2, TorM2K, TorM2M, NASAMS3-AMRAAMER, NASAMS3-AIM9X2, C-RAM, PGZ-09, S350-9M100, S350-9M96D
-- **NOTE** If you are using the Military Assets by Currenthill (CH), please note that the **group name** for CH-SAM types also needs to contain the keyword "CHM"
-- 
-- Following the example started above, an SA-6 site group name should start with "Red SAM SA-6" then, or a blue Patriot installation with e.g. "Blue SAM Patriot". 
-- **NOTE** If you are using the High-Digit-Sam Mod, please note that the **group name** for the following SAM types also needs to contain the keyword "HDS":
-- 
-- * SA-2 (with V759 missile, e.g. "Red SAM SA-2 HDS")
-- * SA-2 (with HQ-2 launcher, use HQ-2 in the group name, e.g. "Red SAM HQ-2" )
-- * SA-3 (with V601P missile, e.g. "Red SAM SA-3 HDS")
-- * SA-10B (overlap with other SA-10 types, e.g. "Red SAM SA-10B HDS" with 5P85CE launcher)
-- * SA-10C (overlap with other SA-10 types, e.g. "Red SAM SA-10C HDS" with 5P85SE launcher)
-- * SA-12 (launcher dependent range, e.g. "Red SAM SA-12 HDS 2" for the 9A82 variant and "Red SAM SA-12 HDS 1" for the 9A83 variant)
-- * SA-23 (launcher dependent range, e.g. "Red SAM SA-23 HDS 2" for the 9A82ME variant and "Red SAM SA-23 HDS 1" for the 9A83ME variant)
-- * SAMP/T (launcher dependent range, e.g. "Blue SAM SAMPT Block 1 HDS" for Block 1, "Blue SAM SAMPT Block 1INT HDS", "Blue SAM SAMPT Block 2 HDS")
-- 
-- The other HDS types work like the rest of the known SAM systems.
-- 
-- # 0.1 Set-up in the mission editor
-- 
-- Set up your SAM sites in the mission editor. Name the groups using a systematic approach like above.Can be e.g. AWACS or a combination of AWACS and Search Radars like e.g. EWR 1L13 etc. 
-- Search Radars usually have "SR" or "STR" in their names. Use the encyclopedia in the mission editor to inform yourself.
-- Set up your SHORAD systems. They need to be **close** to (i.e. around) the SAM sites to be effective. Use **one unit ** per group (multiple groups) for the SAM location. 
-- Else, evasive manoevers might club up all defenders in one place. Red SA-15 TOR systems offer a good missile defense.
-- 
-- [optional] Set up your HQ. Can be any group, e.g. a command vehicle.
--
-- # 1. Basic tactical considerations when setting up your SAM sites
--
-- ## 1.1 Radar systems and AWACS
--
--  Typically, your setup should consist of EWR (early warning) radars to detect and track targets, accompanied by AWACS if your scenario forsees that. Ensure that your EWR radars have a good coverage of the area you want to track.
--  **Location** is of highest importance here. Whilst AWACS in DCS has almost the "all seeing eye", EWR don't have that. Choose your location wisely, against a mountain backdrop or inside a valley even the best EWR system
--  doesn't work well. Prefer higher-up locations with a good view; use F7 in-game to check where you actually placed your EWR and have a look around. Apart from the obvious choice, do also consider other radar units
--  for this role, most have "SR" (search radar) or "STR" (search and track radar) in their names, use the encyclopedia to see what they actually do.
--  **HINT** Set at least one EWR on invisible and immortal so MANTIS doesn't stop working.
--
-- ## 1.2 SAM sites
--
-- Typically your SAM should cover all attack ranges. The closer the enemy gets, the more systems you will need to deploy to defend your location. Use a combination of long-range systems like the SA-5/10/11, midrange like SA-6 and short-range like
-- SA-2 for defense (Patriot, Hawk, Gepard, Blindfire for the blue side). For close-up defense and defense against HARMs or low-flying aircraft, helicopters it is also advisable to deploy SA-15 TOR systems, Shilka, Strela and Tunguska units, as well as manpads (Think Gepard, Avenger, Chaparral,
-- Linebacker, Roland systems for the blue side). If possible, overlap ranges for mutual coverage.
--
-- ## 1.3 Typical problems
--
-- Often times, people complain because the detection cannot "see" oncoming targets and/or Mantis switches on too late. Three typial problems here are
--
--   * bad placement of radar units,
--   * overestimation how far units can "see" and
--   * not taking into account that a SAM site will take (e.g for a SA-6) 30-40 seconds between switching on, acquiring the target and firing.
--
-- An attacker doing 350knots will cover ca 180meters/second or thus more than 6km until the SA-6 fires. Use triggers zones and the ruler in the mission editor to understand distances and zones. Take into account that the ranges given by the circles
-- in the mission editor are absolute maximum ranges; in-game this is rather 50-75% of that depending on the system. Fiddle with placement and options to see what works best for your scenario, and remember **everything in here is in meters**.
--
-- # 2. Start up your MANTIS with a basic setting
--
--        myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false)
--        myredmantis:Start()
--
-- Use
--
--  *     MANTIS:SetEWRGrouping(radius) [classic mode]
--  *     MANTIS:SetSAMRadius(radius) [classic mode]
--  *     MANTIS:SetDetectInterval(interval) [classic & auto modes]
--  *     MANTIS:SetAutoRelocate(hq, ewr) [classic & auto modes]
--
-- before starting #MANTIS to fine-tune your setup.
--
-- If you want to use a separate AWACS unit to support your EWR system, use e.g. the following setup:
--
--        mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")
--        mybluemantis:Start()
-- 
-- ## 2.1 Auto mode features
-- 
-- ### 2.1.1 You can add Accept-, Reject- and Conflict-Zones to your setup, e.g. to consider borders or de-militarized zones:   
-- 
--        -- Parameters are tables of Core.Zone#ZONE objects!   
--        -- This is effectively a 3-stage filter allowing for zone overlap. A coordinate is accepted first when   
--        -- it is inside any AcceptZone. Then RejectZones are checked, which enforces both borders, but also overlaps of   
--        -- Accept- and RejectZones. Last, if it is inside a conflict zone, it is accepted.   
--        mybluemantis:AddZones(AcceptZones,RejectZones,ConflictZones)   
--        
--        
-- ### 2.1.2 Change the number of long-, mid- and short-range, point defense systems going live on a detected target:   
-- 
--        -- parameters are numbers. Defaults are 1,2,2,6,6 respectively
--        mybluemantis:SetMaxActiveSAMs(Short,Mid,Long,Classic,Point)
-- 
-- ### 2.1.3 SHORAD/Point defense will automatically be added from SAM sites of type "point" or if the range is less than 5km or if the type is AAA. 
--        
-- ### 2.1.4 Advanced features   
--        
--        -- Option to set the scale of the activation range, i.e. don't activate at the fringes of max range, defaults below.   
--        -- also see engagerange below.   
--            self.radiusscale[MANTIS.SamType.LONG] = 1.1   
--            self.radiusscale[MANTIS.SamType.MEDIUM] = 1.2   
--            self.radiusscale[MANTIS.SamType.SHORT] = 1.3 
--            self.radiusscale[MANTIS.SamType.POINT] = 1.4 
--        
-- ### 2.1.5 Friendlies check in firing range
-- 
--        -- For some scenarios, like Cold War, it might be useful not to activate SAMs if friendly aircraft are around to avoid death by friendly fire.
--        mybluemantis.checkforfriendlies = true  
--        
-- ### 2.1.6 Shoot & Scoot
-- 
--        -- Option to make the (driveable) SHORAD units drive around and shuffle positions
--        -- We use a SET_ZONE for that, number of zones to consider defaults to three, Random is true for random coordinates and Formation is e.g. "Vee".
--        mybluemantis:AddScootZones(ZoneSet, Number, Random, Formation)
-- 
-- # 3. Default settings [both modes unless stated otherwise]
--
-- By default, the following settings are active:
--
--  * SAM_Templates_Prefix = "Red SAM" - SAM site group names in the mission editor begin with "Red SAM"
--  * EWR_Templates_Prefix = "Red EWR" - EWR group names in the mission editor begin with "Red EWR" - can also be combined with an AWACS unit
--  * [classic mode] checkradius = 25000 (meters) - SAMs will engage enemy flights, if they are within a 25km around each SAM site - `MANTIS:SetSAMRadius(radius)`
--  * grouping = 5000 (meters) - Detection (EWR) will group enemy flights to areas of 5km for tracking - `MANTIS:SetEWRGrouping(radius)`
--  * detectinterval = 30 (seconds) - MANTIS will decide every 30 seconds which SAM to activate - `MANTIS:SetDetectInterval(interval)`
--  * engagerange = 95 (percent) - SAMs will only fire if flights are inside of a 95% radius of their max firerange - `MANTIS:SetSAMRange(range)`
--  * dynamic = false - Group filtering is set to once, i.e. newly added groups will not be part of the setup by default - `MANTIS:New(name,samprefix,ewrprefix,hq,coalition,dynamic)`
--  * autorelocate = false - HQ and (mobile) EWR system will not relocate in random intervals between 30mins and 1 hour - `MANTIS:SetAutoRelocate(hq, ewr)`
--  * debug = false - Debugging reports on screen are set to off - `MANTIS:Debug(onoff)`
--
-- # 4. Advanced Mode
--
--  Advanced mode will *decrease* reactivity of MANTIS, if HQ and/or EWR  network dies.  Awacs is counted as one EWR unit. It will set SAMs to RED state if both are dead.  Requires usage of an **HQ** object and the **dynamic** option.
--
--  E.g.        mymantis:SetAdvancedMode( true, 90 )
--
--  Use this option if you want to make use of or allow advanced SEAD tactics.
--  
-- # 5. Integrated SEAD
--  
--  MANTIS is using @{Functional.Sead#SEAD} internally to both detect and evade HARM attacks. No extra efforts needed to set this up! 
--  Once a HARM attack is detected, MANTIS (via SEAD) will shut down the radars of the attacked SAM site and take evasive action by moving the SAM
--  vehicles around (*if they are __drivable__*, that is). There's a component of randomness in detection and evasion, which is based on the
--  skill set of the SAM set (the higher the skill, the more likely). When a missile is fired from far away, the SAM will stay active for a 
--  period of time to stay defensive, before it takes evasive actions.
--  
--  You can link into the SEAD driven events of MANTIS like so:
--  
--        function mymantis:OnAfterSeadSuppressionPlanned(From, Event, To, Group, Name, SuppressionStartTime, SuppressionEndTime)
--          -- your code here - SAM site shutdown and evasion planned, but not yet executed
--          -- Time entries relate to timer.getTime() - see https://wiki.hoggitworld.com/view/DCS_func_getTime
--        end
--        
--        function mymantis:OnAfterSeadSuppressionStart(From, Event, To, Group, Name)
--          -- your code here - SAM site is emissions off and possibly moving
--        end
--        
--        function mymantis:OnAfterSeadSuppressionEnd(From, Event, To, Group, Name)
--          -- your code here - SAM site is back online
--        end
--  
-- @field #MANTIS
MANTIS = {
  ClassName             = "MANTIS",
  name                  = "mymantis",
  version               = "0.9.34",
  SAM_Templates_Prefix  = "",
  SAM_Group             = nil,
  EWR_Templates_Prefix  = "",
  EWR_Group             = nil,
  Adv_EWR_Group         = nil,
  HQ_Template_CC        = "",
  HQ_CC                 = nil,
  SAM_Table             = {},
  SAM_Table_Long        = {},
  SAM_Table_Medium      = {},
  SAM_Table_Short       = {},
  SAM_Table_PointDef    = {},
  lid                   = "",
  Detection             = nil,
  AWACS_Detection       = nil,
  debug                 = false,
  checkradius           = 25000,
  grouping              = 5000,
  acceptrange           = 80000,
  detectinterval        = 30,
  engagerange           = 95,
  autorelocate          = false,
  advanced              = false,
  adv_ratio             = 100,
  adv_state             = 0,
  AWACS_Prefix          = "",
  advAwacs              = false,
  verbose               = false,
  awacsrange            = 250000,
  Shorad                = nil,
  ShoradLink            = false,
  ShoradTime            = 600,
  ShoradActDistance     = 25000,
  UseEmOnOff            = false,
  TimeStamp             = 0,
  state2flag            = false,
  SamStateTracker       = {},
  DLink                 = false,
  DLTimeStamp           = 0,
  Padding               = 10,
  SuppressedGroups      = {},
  automode              = true,
  autoshorad            = true,
  ShoradGroupSet        = nil,
  checkforfriendlies    = false,
  SmokeDecoy            = false,
  SmokeDecoyColor       = SMOKECOLOR.White,
  checkcounter          = 1,
  DLinkCacheTime        = 120,
  logsamstatus          = false,
}

--- Advanced state enumerator
-- @type MANTIS.AdvancedState
MANTIS.AdvancedState = {
  GREEN = 0,
  AMBER = 1,
  RED = 2,
}

--- SAM Type
-- @type MANTIS.SamType
MANTIS.SamType = {
  SHORT = "Short",
  MEDIUM = "Medium",
  LONG = "Long",
  POINT = "Point",
}

--- SAM Radiusscale
-- @type MANTIS.radiusscale
MANTIS.radiusscale = {}
MANTIS.radiusscale[MANTIS.SamType.LONG] = 1.1
MANTIS.radiusscale[MANTIS.SamType.MEDIUM] = 1.2
MANTIS.radiusscale[MANTIS.SamType.SHORT] = 1.75
MANTIS.radiusscale[MANTIS.SamType.POINT] = 3

--- SAM data
-- @type MANTIS.SamData
-- @field #number Range Max firing range in km
-- @field #number Blindspot no-firing range (green circle)
-- @field #number Height Max firing height in km
-- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range)
-- @field #string Radar Radar typename on unit level (used as key)
-- @field #string Point Point defense capable
MANTIS.SamData = {
  ["Hawk"] = { Range=35, Blindspot=0, Height=12, Type="Medium", Radar="Hawk" }, -- measures in km
  ["NASAMS"] = { Range=14, Blindspot=0, Height=7, Type="Short", Radar="NSAMS" }, -- AIM 120B
  ["Patriot"] = { Range=99, Blindspot=0, Height=25, Type="Long", Radar="Patriot str" },
  ["Rapier"] = { Range=10, Blindspot=0, Height=3, Type="Short", Radar="rapier" },
  ["SA-2"] = { Range=40, Blindspot=7, Height=25, Type="Medium", Radar="S_75M_Volhov" },
  ["SA-3"] = { Range=18, Blindspot=6, Height=18, Type="Short", Radar="5p73 s-125 ln" },
  ["SA-5"] = { Range=250, Blindspot=7, Height=40, Type="Long", Radar="5N62V" },
  ["SA-6"] = { Range=25, Blindspot=0, Height=8, Type="Medium", Radar="1S91" },
  ["SA-10"] = { Range=119, Blindspot=0, Height=18, Type="Long" , Radar="S-300PS 4"},
  ["SA-11"] = { Range=35, Blindspot=0, Height=20, Type="Medium", Radar="SA-11" },
  ["Roland"] = { Range=6, Blindspot=0, Height=5, Type="Short", Radar="Roland" },
  ["Gepard"] = { Range=5, Blindspot=0, Height=4, Type="Point", Radar="Gepard" },
  ["HQ-7"] = { Range=12, Blindspot=0, Height=3, Type="Short", Radar="HQ-7" },
  ["SA-9"] = { Range=4, Blindspot=0, Height=3, Type="Point", Radar="Strela", Point="true" },
  ["SA-8"] = { Range=10, Blindspot=0, Height=5, Type="Short", Radar="Osa 9A33" },
  ["SA-19"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Tunguska" },
  ["SA-15"] = { Range=11, Blindspot=0, Height=6, Type="Point", Radar="Tor 9A331", Point="true" },
  ["SA-13"] = { Range=5, Blindspot=0, Height=3, Type="Point", Radar="Strela", Point="true" },
  ["Avenger"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="Avenger" },
  ["Chaparral"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Chaparral" },
  ["Linebacker"] = { Range=4, Blindspot=0, Height=3, Type="Point", Radar="Linebacker", Point="true" },
  ["Silkworm"] = { Range=90, Blindspot=1, Height=0.2, Type="Long", Radar="Silkworm" },
  ["C-RAM"] = { Range=2, Blindspot=0, Height=2, Type="Point", Radar="HEMTT_C-RAM_Phalanx", Point="true" },
  -- units from HDS Mod, multi launcher options is tricky
  ["SA-10B"] = { Range=75, Blindspot=0, Height=18, Type="Medium" , Radar="SA-10B"},
  ["SA-17"] = { Range=50, Blindspot=3, Height=50, Type="Medium", Radar="SA-17" },
  ["SA-20A"] = { Range=150, Blindspot=5, Height=27, Type="Long" , Radar="S-300PMU1"},
  ["SA-20B"] = { Range=200, Blindspot=4, Height=27, Type="Long" , Radar="S-300PMU2"},
  ["HQ-2"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" },
  ["TAMIR IDFA"] = { Range=20, Blindspot=0.6, Height=12.3, Type="Short", Radar="IRON_DOME_LN" },
  ["STUNNER IDFA"] = { Range=250, Blindspot=1, Height=45, Type="Long", Radar="DAVID_SLING_LN" },
  ["NIKE"] = { Range=155, Blindspot=6, Height=30, Type="Long", Radar="HIPAR" },
  ["Dog Ear"] = { Range=11, Blindspot=0, Height=9, Type="Point", Radar="Dog Ear", Point="true" },
  -- CH Added to DCS core 2.9.19.x
  ["Pantsir S1"] = { Range=20, Blindspot=1.2, Height=15, Type="Point", Radar="PantsirS1" , Point="true" }, 
  ["Tor M2"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2", Point="true"  },
  ["IRIS-T SLM"] = { Range=40, Blindspot=0.5, Height=20, Type="Medium", Radar="CH_IRIST_SLM" }, 
}

--- SAM data HDS
-- @type MANTIS.SamDataHDS
-- @field #number Range Max firing range in km
-- @field #number Blindspot no-firing range (green circle)
-- @field #number Height Max firing height in km
-- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range)
-- @field #string Radar Radar typename on unit level (used as key)
-- @field #string Point Point defense capable
MANTIS.SamDataHDS = {
  -- units from HDS Mod, multi launcher options is tricky
  -- group name MUST contain HDS to ID launcher type correctly!
  ["SA-2 HDS"] = { Range=56, Blindspot=7, Height=30, Type="Medium", Radar="V759" }, 
  ["SA-3 HDS"] = { Range=20, Blindspot=6, Height=30, Type="Short", Radar="V-601P" },  
  ["SA-10B HDS"] = { Range=90, Blindspot=5, Height=25, Type="Long" , Radar="5P85CE ln"}, -- V55RUD
  ["SA-10C HDS"] = { Range=75, Blindspot=5, Height=25, Type="Long" , Radar="5P85SE ln"}, -- V55RUD
  ["SA-17 HDS"] = { Range=50, Blindspot=3, Height=50, Type="Medium", Radar="SA-17 " },
  ["SA-12 HDS 2"] = { Range=100, Blindspot=13, Height=30, Type="Long" , Radar="S-300V 9A82 l"},
  ["SA-12 HDS 1"] = { Range=75, Blindspot=6, Height=25, Type="Long" , Radar="S-300V 9A83 l"},
  ["SA-23 HDS 2"] = { Range=200, Blindspot=5, Height=37, Type="Long", Radar="S-300VM 9A82ME" },
  ["SA-23 HDS 1"] = { Range=100, Blindspot=1, Height=50, Type="Long", Radar="S-300VM 9A83ME" },
  ["HQ-2 HDS"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" },
  ["SAMPT Block 1 HDS"] = { Range=120, Blindspot=1, Height=20, Type="long", Radar="SAMPT_MLT_Blk1" }, -- Block 1 Launcher
  ["SAMPT Block 1INT HDS"] = { Range=150, Blindspot=1, Height=25, Type="long", Radar="SAMPT_MLT_Blk1NT" }, -- Block 1-INT Launcher
  ["SAMPT Block 2 HDS"] = { Range=200, Blindspot=10, Height=70, Type="long", Radar="SAMPT_MLT_Blk2" }, -- Block 2 Launcher
}

--- SAM data SMA
-- @type MANTIS.SamDataSMA
-- @field #number Range Max firing range in km
-- @field #number Blindspot no-firing range (green circle)
-- @field #number Height Max firing height in km
-- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range)
-- @field #string Radar Radar typename on unit level (used as key)
-- @field #string Point Point defense capable
MANTIS.SamDataSMA = {
  -- units from SMA Mod (Sweedish Military Assets)
  -- https://forum.dcs.world/topic/295202-swedish-military-assets-for-dcs-by-currenthill/
  -- group name MUST contain SMA to ID launcher type correctly!
   ["RBS98M SMA"] = { Range=20, Blindspot=0.2, Height=8, Type="Short", Radar="RBS-98" },
   ["RBS70 SMA"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="RBS-70" },  
   ["RBS70M SMA"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="BV410_RBS70" }, 
   ["RBS90 SMA"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="RBS-90" }, 
   ["RBS90M SMA"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="BV410_RBS90" },  
   ["RBS103A SMA"] = { Range=160, Blindspot=1, Height=36, Type="Long", Radar="LvS-103_Lavett103_Rb103A" },
   ["RBS103B SMA"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="LvS-103_Lavett103_Rb103B" }, 
   ["RBS103AM SMA"] = { Range=160, Blindspot=1, Height=36, Type="Long", Radar="LvS-103_Lavett103_HX_Rb103A" },
   ["RBS103BM SMA"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="LvS-103_Lavett103_HX_Rb103B" },
   ["Lvkv9040M SMA"] = { Range=2, Blindspot=0.1, Height=1.2, Type="Point", Radar="LvKv9040",Point="true" },   
}

--- SAM data CH
-- @type MANTIS.SamDataCH
-- @field #number Range Max firing range in km
-- @field #number Blindspot no-firing range (green circle)
-- @field #number Height Max firing height in km
-- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range)
-- @field #string Radar Radar typename on unit level (used as key)
-- @field #string Point Point defense capable
MANTIS.SamDataCH = {
    -- units from CH (Military Assets by Currenthill)
    -- https://www.currenthill.com/
    -- group name MUST contain CHM to ID launcher type correctly!
   ["2S38 CHM"] = { Range=6, Blindspot=0.1, Height=4.5, Type="Short", Radar="2S38" },
   ["PantsirS1 CHM"] = { Range=20, Blindspot=1.2, Height=15, Type="Point", Radar="PantsirS1", Point="true"  }, 
   ["PantsirS2 CHM"] = { Range=30, Blindspot=1.2, Height=18, Type="Medium", Radar="PantsirS2" }, 
   ["PGL-625 CHM"] = { Range=10, Blindspot=1, Height=5, Type="Short", Radar="PGL_625" }, 
   ["HQ-17A CHM"] = { Range=15, Blindspot=1.5, Height=10, Type="Short", Radar="HQ17A" }, 
   ["M903PAC2 CHM"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="MIM104_M903_PAC2" },
   ["M903PAC3 CHM"] = { Range=160, Blindspot=1, Height=40, Type="Long", Radar="MIM104_M903_PAC3" }, 
   ["TorM2 CHM"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2", Point="true"  },
   ["TorM2K CHM"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2K", Point="true"  },
   ["TorM2M CHM"] = { Range=16, Blindspot=1, Height=10, Type="Point", Radar="TorM2M", Point="true"  }, 
   ["NASAMS3-AMRAAMER CHM"] = { Range=50, Blindspot=2, Height=35.7, Type="Medium", Radar="CH_NASAMS3_LN_AMRAAM_ER" }, 
   ["NASAMS3-AIM9X2 CHM"] = { Range=20, Blindspot=0.2, Height=18, Type="Short", Radar="CH_NASAMS3_LN_AIM9X2" },
   ["C-RAM CHM"] = { Range=2, Blindspot=0, Height=2, Type="Point", Radar="CH_Centurion_C_RAM", Point="true" }, 
   ["PGZ-09 CHM"] = { Range=4, Blindspot=0.5, Height=3, Type="Point", Radar="CH_PGZ09", Point="true" },
   ["S350-9M100 CHM"] = { Range=15, Blindspot=1, Height=8, Type="Short", Radar="CH_S350_50P6_9M100" },
   ["S350-9M96D CHM"] = { Range=150, Blindspot=2.5, Height=30, Type="Long", Radar="CH_S350_50P6_9M96D" },
   ["LAV-AD CHM"] = { Range=8, Blindspot=0.16, Height=4.8, Type="Short", Radar="CH_LAVAD" }, 
   ["HQ-22 CHM"] = { Range=170, Blindspot=5, Height=27, Type="Long", Radar="CH_HQ22_LN" }, 
   ["PGZ-95 CHM"] = { Range=2.5, Blindspot=0.5, Height=2, Type="Point", Radar="CH_PGZ95",Point="true" },
   ["LD-3000 CHM"] = { Range=2.5, Blindspot=0.1, Height=3, Type="Point", Radar="CH_LD3000_stationary", Point="true" }, 
   ["LD-3000M CHM"] = { Range=2.5, Blindspot=0.1, Height=3, Type="Point", Radar="CH_LD3000", Point="true" },  
   ["FlaRakRad CHM"] = { Range=8, Blindspot=1.5, Height=6, Type="Short", Radar="CH_FlaRakRad" },  
   ["IRIS-T SLM CHM"] = { Range=40, Blindspot=0.5, Height=20, Type="Medium", Radar="CH_IRIST_SLM" }, 
   ["M903PAC2KAT1 CHM"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="CH_MIM104_M903_PAC2_KAT1" }, 
   ["Skynex CHM"] = { Range=3.5, Blindspot=0.1, Height=3.5, Type="Point", Radar="CH_SkynexHX", Point="true" },
   ["Skyshield CHM"] = { Range=3.5, Blindspot=0.1, Height=3.5, Type="Point", Radar="CH_Skyshield_Gun", Point="true" },
   ["WieselOzelot CHM"] = { Range=8, Blindspot=0.16, Height=4.8, Type="Short", Radar="CH_Wiesel2Ozelot" }, 
   ["BukM3-9M317M CHM"] = { Range=70, Blindspot=0.25, Height=35, Type="Medium", Radar="CH_BukM3_9A317M" },  
   ["BukM3-9M317MA CHM"] = { Range=70, Blindspot=0.25, Height=35, Type="Medium", Radar="CH_BukM3_9A317MA" },  
   ["SkySabre CHM"] = { Range=30, Blindspot=0.5, Height=10, Type="Medium", Radar="CH_SkySabreLN" },  
   ["Stormer CHM"] = { Range=7.5, Blindspot=0.3, Height=7, Type="Short", Radar="CH_StormerHVM" },  
   ["THAAD CHM"] = { Range=200, Blindspot=40, Height=150, Type="Long", Radar="CH_THAAD_M1120" },  
   ["USInfantryFIM92K CHM"] = { Range=8, Blindspot=0.16, Height=4.8, Type="Short", Radar="CH_USInfantry_FIM92" },
   ["RBS98M CHM"] = { Range=20, Blindspot=0.2, Height=8, Type="Short", Radar="RBS-98" },
   ["RBS70 CHM"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="RBS-70" },  
   ["RBS70M CHM"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="BV410_RBS70" }, 
   ["RBS90 CHM"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="RBS-90" }, 
   ["RBS90M CHM"] = { Range=8, Blindspot=0.25, Height=6, Type="Short", Radar="BV410_RBS90" },  
   ["RBS103A CHM"] = { Range=160, Blindspot=1, Height=36, Type="Long", Radar="LvS-103_Lavett103_Rb103A" },
   ["RBS103B CHM"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="LvS-103_Lavett103_Rb103B" }, 
   ["RBS103AM CHM"] = { Range=160, Blindspot=1, Height=36, Type="Long", Radar="LvS-103_Lavett103_HX_Rb103A" },
   ["RBS103BM CHM"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="LvS-103_Lavett103_HX_Rb103B" },
   ["Lvkv9040M CHM"] = { Range=2, Blindspot=0.1, Height=1.2, Type="Point", Radar="LvKv9040",Point="true" },   
}

-----------------------------------------------------------------------
-- MANTIS System
-----------------------------------------------------------------------

do
  --- Function to instantiate a new object of class MANTIS
  --@param #MANTIS self
  --@param #string name Name of this MANTIS for reporting
  --@param #string samprefix Prefixes for the SAM groups from the ME, e.g. all groups starting with "Red Sam..."
  --@param #string ewrprefix Prefixes for the EWR groups from the ME, e.g. all groups starting with "Red EWR..."
  --@param #string hq Group name of your HQ (optional)
  --@param #string coalition Coalition side of your setup, e.g. "blue", "red" or "neutral"
  --@param #boolean dynamic Use constant (true) filtering or just filter once (false, default) (optional)
  --@param #string awacs Group name of your Awacs (optional)
  --@param #boolean EmOnOff Make MANTIS switch Emissions on and off instead of changing the alarm state between RED and GREEN (optional)
  --@param #number Padding For #SEAD - Extra number of seconds to add to radar switch-back-on time (optional)
  --@param #table Zones Table of Core.Zone#ZONE Zones Consider SAM groups in this zone(s) only for this MANTIS instance, must be handed as #table of Zone objects
  --@return #MANTIS self
  --@usage Start up your MANTIS with a basic setting
  --
  --        myredmantis = MANTIS:New("myredmantis","Red SAM","Red EWR",nil,"red",false)
  --        myredmantis:Start()
  --
  -- [optional] Use
  --
  --        myredmantis:SetDetectInterval(interval)
  --        myredmantis:SetAutoRelocate(hq, ewr)
  --
  -- before starting #MANTIS to fine-tune your setup.
  --
  -- If you want to use a separate AWACS unit (default detection range: 250km) to support your EWR system, use e.g. the following setup:
  --
  --        mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")
  --        mybluemantis:Start()
  --
  function MANTIS:New(name,samprefix,ewrprefix,hq,coalition,dynamic,awacs, EmOnOff, Padding, Zones)
    
    
    -- Inherit everything from BASE class.
    local self = BASE:Inherit(self, FSM:New()) -- #MANTIS
    
    -- DONE: Create some user functions for these
    -- DONE: Make HQ useful
    -- DONE: Set SAMs to auto if EWR dies
    -- DONE: Refresh SAM table in dynamic mode
    -- DONE: Treat Awacs separately, since they might be >80km off site
    -- DONE: Allow tables of prefixes for the setup
    -- DONE: Auto-Mode with range setups for various known SAM types.
    
    self.name = name or "mymantis"
    self.SAM_Templates_Prefix = samprefix or "Red SAM"
    self.EWR_Templates_Prefix = ewrprefix or "Red EWR"
    self.HQ_Template_CC = hq or nil
    self.Coalition = coalition or "red"
    self.SAM_Table = {}
    self.SAM_Table_Long = {}
    self.SAM_Table_Medium = {}
    self.SAM_Table_Short = {}
    self.SAM_Table_PointDef = {}
    self.dynamic = dynamic or false
    self.checkradius = 25000
    self.grouping = 5000
    self.acceptrange = 80000
    self.detectinterval = 30
    self.engagerange = 95
    self.autorelocate = false
    self.autorelocateunits = { HQ = false, EWR = false}
    self.advanced = false
    self.adv_ratio = 100
    self.adv_state = 0
    self.verbose = false
    self.Adv_EWR_Group = nil
    self.AWACS_Prefix = awacs or nil
    self.awacsrange = 250000      --DONE: 250km, User Function to change
    self.Shorad = nil
    self.ShoradLink = false
    self.ShoradTime = 600
    self.ShoradActDistance = 25000
    self.TimeStamp = timer.getAbsTime()
    self.relointerval = math.random(1800,3600) -- random between 30 and 60 mins
    self.state2flag = false
    self.SamStateTracker = {} -- table to hold alert states, so we don't trigger state changes twice in adv mode
    self.DLink = false
    self.Padding = Padding or 10
    self.SuppressedGroups = {}
    -- 0.8 additions
    self.automode = true
    --self.SAMCheckRanges = {}
    self.usezones = false
    self.AcceptZones = {}
    self.RejectZones = {}
    self.ConflictZones = {}
    self.maxlongrange = 1
    self.maxmidrange = 2
    self.maxshortrange = 2
    self.maxpointdefrange = 6 
    self.maxclassic = 6
    self.autoshorad = true
    self.ShoradGroupSet = SET_GROUP:New() -- Core.Set#SET_GROUP
    self.FilterZones = Zones
    
    self.SkateZones = nil
    self.SkateNumber =  3
    self.shootandscoot = false
    
    self.SmokeDecoy = false
    self.SmokeDecoyColor = SMOKECOLOR.White
    
    self.UseEmOnOff = true
    if EmOnOff == false then
      self.UseEmOnOff = false
    end

    if type(awacs) == "string" then
      self.advAwacs = true
    else
      self.advAwacs = false
    end
    
    self:SetDLinkCacheTime()
    
    -- Set the string id for output to DCS.log file.
    self.lid=string.format("MANTIS %s | ", self.name)

    -- Debug trace.
    if self.debug then
      BASE:TraceOnOff(true)
      BASE:TraceClass(self.ClassName)
      --BASE:TraceClass("SEAD")
      BASE:TraceLevel(1)
    end
    
    self.ewr_templates = {}
    if type(samprefix) ~= "table" then
      self.SAM_Templates_Prefix = {samprefix}
    end
    
    if type(ewrprefix) ~= "table" then
      self.EWR_Templates_Prefix = {ewrprefix}
    end
    
    for _,_group in pairs (self.SAM_Templates_Prefix) do
      table.insert(self.ewr_templates,_group)
    end
    
    for _,_group in pairs (self.EWR_Templates_Prefix) do
      table.insert(self.ewr_templates,_group)
    end
    
    if self.advAwacs then
      table.insert(self.ewr_templates,awacs)
    end
    
    self.logsamstatus = false
    
    self:T({self.ewr_templates})
    
    self.SAM_Group = SET_GROUP:New():FilterPrefixes(self.SAM_Templates_Prefix):FilterCoalitions(self.Coalition)
    self.EWR_Group = SET_GROUP:New():FilterPrefixes(self.ewr_templates):FilterCoalitions(self.Coalition)
    
    if self.FilterZones then
      self.SAM_Group:FilterZones(self.FilterZones)
    end
    
    if self.dynamic then
      -- Set SAM SET_GROUP
      self.SAM_Group:FilterStart()
      -- Set EWR SET_GROUP
      self.EWR_Group:FilterStart()
    else
      -- Set SAM SET_GROUP
      self.SAM_Group:FilterOnce()
      -- Set EWR SET_GROUP
      self.EWR_Group:FilterOnce()
    end

    -- set up CC
    if self.HQ_Template_CC then
      self.HQ_CC = GROUP:FindByName(self.HQ_Template_CC)
    end
    
    -- counter for SAM table updates
    self.checkcounter = 1
    
    self:I(string.format("***** Starting MANTIS Version %s *****", self.version))

    --- FSM Functions ---

      -- Start State.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  --                 From State  -->   Event            -->     To State
  self:AddTransition("Stopped",       "Start",                   "Running")     -- Start FSM.
  self:AddTransition("*",             "Status",                  "*")           -- MANTIS status update.
  self:AddTransition("*",             "Relocating",              "*")           -- MANTIS HQ and EWR are relocating.
  self:AddTransition("*",             "GreenState",              "*")           -- MANTIS A SAM switching to GREEN state.
  self:AddTransition("*",             "RedState",                "*")           -- MANTIS A SAM switching to RED state.
  self:AddTransition("*",             "AdvStateChange",          "*")           -- MANTIS advanced mode state change.
  self:AddTransition("*",             "ShoradActivated",         "*")           -- MANTIS woke up a connected SHORAD.
  self:AddTransition("*",             "SeadSuppressionStart",    "*")           -- SEAD has switched off one group.
  self:AddTransition("*",             "SeadSuppressionEnd",      "*")           -- SEAD has switched on one group.
  self:AddTransition("*",             "SeadSuppressionPlanned",  "*")           -- SEAD has planned a suppression.
  self:AddTransition("*",             "Stop",                    "Stopped")     -- Stop FSM.

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the MANTIS. Initializes parameters and starts event handlers.
  -- @function [parent=#MANTIS] Start
  -- @param #MANTIS self

  --- Triggers the FSM event "Start" after a delay. Starts the MANTIS. Initializes parameters and starts event handlers.
  -- @function [parent=#MANTIS] __Start
  -- @param #MANTIS self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the MANTIS and all its event handlers.
  -- @param #MANTIS self

  --- Triggers the FSM event "Stop" after a delay. Stops the MANTIS and all its event handlers.
  -- @function [parent=#MANTIS] __Stop
  -- @param #MANTIS self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Status".
  -- @function [parent=#MANTIS] Status
  -- @param #MANTIS self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#MANTIS] __Status
  -- @param #MANTIS self
  -- @param #number delay Delay in seconds.

  --- On After "Relocating" event. HQ and/or EWR moved.
  -- @function [parent=#MANTIS] OnAfterRelocating
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @return #MANTIS self

  --- On After "GreenState" event. A SAM group was switched to GREEN alert.
  -- @function [parent=#MANTIS] OnAfterGreenState
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed
  -- @return #MANTIS self

  --- On After "RedState" event. A SAM group was switched to RED alert.
  -- @function [parent=#MANTIS] OnAfterRedState
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed
  -- @return #MANTIS self

  --- On After "AdvStateChange" event. Advanced state changed, influencing detection speed.
  -- @function [parent=#MANTIS] OnAfterAdvStateChange
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param #number Oldstate Old state - 0 = green, 1 = amber, 2 = red
  -- @param #number Newstate New state - 0 = green, 1 = amber, 2 = red
  -- @param #number Interval Calculated detection interval based on state and advanced feature setting
  -- @return #MANTIS self

  --- On After "ShoradActivated" event. Mantis has activated a SHORAD.
  -- @function [parent=#MANTIS] OnAfterShoradActivated
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param #string Name Name of the GROUP which SHORAD shall protect
  -- @param #number Radius Radius around the named group to find SHORAD groups
  -- @param #number Ontime Seconds the SHORAD will stay active

  --- On After "SeadSuppressionPlanned" event. Mantis has planned to switch off a site to defend SEAD attack.
  -- @function [parent=#MANTIS] OnAfterSeadSuppressionPlanned
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param Wrapper.Group#GROUP Group The suppressed GROUP object
  -- @param #string Name Name of the suppressed group
  -- @param #number SuppressionStartTime Model start time of the suppression from `timer.getTime()`
  -- @param #number SuppressionEndTime Model end time of the suppression from `timer.getTime()`
  -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object

  --- On After "SeadSuppressionStart" event. Mantis has switched off a site to defend a SEAD attack.
  -- @function [parent=#MANTIS] OnAfterSeadSuppressionStart
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param Wrapper.Group#GROUP Group The suppressed GROUP object
  -- @param #string Name Name of the suppressed group
  -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object

  --- On After "SeadSuppressionEnd" event. Mantis has switched on a site after a SEAD attack.
  -- @function [parent=#MANTIS] OnAfterSeadSuppressionEnd
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param Wrapper.Group#GROUP Group The suppressed GROUP object
  -- @param #string Name Name of the suppressed group
  
  return self
 end

-----------------------------------------------------------------------
-- MANTIS helper functions
-----------------------------------------------------------------------

  --- [Internal] Function to get the self.SAM_Table
  -- @param #MANTIS self
  -- @return #table table
  function MANTIS:_GetSAMTable()
    self:T(self.lid .. "GetSAMTable")
    return self.SAM_Table
  end

  --- [Internal] Function to set the self.SAM_Table
  -- @param #MANTIS self
  -- @return #MANTIS self
  function MANTIS:_SetSAMTable(table)
    self:T(self.lid .. "SetSAMTable")
    self.SAM_Table = table
    return self
  end

  --- Function to set the grouping radius of the detection in meters
  -- @param #MANTIS self
  -- @param #number radius Radius upon which detected objects will be grouped
  function MANTIS:SetEWRGrouping(radius)
    self:T(self.lid .. "SetEWRGrouping")
    local radius = radius or 5000
    self.grouping = radius
    return self
  end
  
  --- Add a SET_ZONE of zones for Shoot&Scoot - SHORAD units will move around
  -- @param #MANTIS self
  -- @param Core.Set#SET_ZONE ZoneSet Set of zones to be used. Units will move around to the next (random) zone between 100m and 3000m away.
  -- @param #number Number Number of closest zones to be considered, defaults to 3.
  -- @param #boolean Random If true, use a random coordinate inside the next zone to scoot to.
  -- @param #string Formation Formation to use, defaults to "Cone". See mission editor dropdown for options.
  -- @return #MANTIS self
  function MANTIS:AddScootZones(ZoneSet, Number, Random, Formation)
    self:T(self.lid .. " AddScootZones")
    self.SkateZones = ZoneSet
    self.SkateNumber = Number or 3
    self.shootandscoot = true
    self.ScootRandom = Random
    self.ScootFormation = Formation or "Cone"    
    return self
  end
  
  --- Function to set accept and reject zones.
  -- @param #MANTIS self
  -- @param #table AcceptZones Table of @{Core.Zone#ZONE} objects
  -- @param #table RejectZones Table of @{Core.Zone#ZONE} objects
  -- @param #table ConflictZones Table of @{Core.Zone#ZONE} objects
  -- @return #MANTIS self
  -- @usage
  -- Parameters are **tables of Core.Zone#ZONE** objects!   
  -- This is effectively a 3-stage filter allowing for zone overlap. A coordinate is accepted first when   
  -- it is inside any AcceptZone. Then RejectZones are checked, which enforces both borders, but also overlaps of   
  -- Accept- and RejectZones. Last, if it is inside a conflict zone, it is accepted.   
  function MANTIS:AddZones(AcceptZones,RejectZones, ConflictZones)
    self:T(self.lid .. "AddZones")
    self.AcceptZones = AcceptZones or {}
    self.RejectZones = RejectZones or {}
    self.ConflictZones = ConflictZones or {}
    self.AcceptZonesNo = UTILS.TableLength(self.AcceptZones)
    self.RejectZonesNo = UTILS.TableLength(self.RejectZones)
    self.ConflictZonesNo = UTILS.TableLength(self.ConflictZones)
    self:T(string.format("AcceptZonesNo = %d | RejectZonesNo = %d | ConflictZonesNo = %d",self.AcceptZonesNo,self.RejectZonesNo,self.ConflictZonesNo))
    if self.AcceptZonesNo > 0 or self.RejectZonesNo > 0 or self.ConflictZonesNo > 0 then
      self.usezones = true
    end
    return self
  end
  
  --- Function to set the detection radius of the EWR in meters. (Deprecated, SAM range is used)
  -- @param #MANTIS self
  -- @param #number radius Radius of the EWR detection zone
  function MANTIS:SetEWRRange(radius)
    self:T(self.lid .. "SetEWRRange")
    --local radius = radius or 80000
    -- self.acceptrange = radius
    return self
  end

  --- Function to set switch-on/off zone for the SAM sites in meters. Overwritten per SAM in automode.
  -- @param #MANTIS self
  -- @param #number radius Radius of the firing zone in classic mode
  function MANTIS:SetSAMRadius(radius)
    self:T(self.lid .. "SetSAMRadius")
    local radius = radius or 25000
    self.checkradius = radius
    return self
  end

  --- Function to set SAM firing engage range, 0-100 percent, e.g. 85
  -- @param #MANTIS self
  -- @param #number range Percent of the max fire range
  function MANTIS:SetSAMRange(range)
    self:T(self.lid .. "SetSAMRange")
    local range = range or 95
    if range < 0 or range > 100 then
      range = 95
    end
    self.engagerange = range
    return self
  end
  
  --- Function to set Short Range SAMs to spit out smoke as decoy, if an enemy plane is in range.
  -- @param #MANTIS self
  -- @param #boolean Onoff Set to true for on and nil/false for off.
  -- @param #number Color (Optional) Color to use, defaults to `SMOKECOLOR.White`
  function MANTIS:SetSmokeDecoy(Onoff,Color)
    self.SmokeDecoy = Onoff
    self.SmokeDecoyColor = Color or SMOKECOLOR.White
    return self
  end
  
    --- Function to set number of SAMs going active on a valid, detected thread
    -- @param #MANTIS self
    -- @param #number Short Number of short-range systems activated, defaults to 1.
    -- @param #number Mid Number of mid-range systems activated, defaults to 2.
    -- @param #number Long Number of long-range systems activated, defaults to 2.
    -- @param #number Classic (non-automode) Number of overall systems activated, defaults to 6.
    -- @param #number Point Number of point defense and AAA systems activated, defaults to 6.
    -- @return #MANTIS self
  function MANTIS:SetMaxActiveSAMs(Short,Mid,Long,Classic,Point)
    self:T(self.lid .. "SetMaxActiveSAMs")
    self.maxclassic = Classic or 6
    self.maxlongrange = Long or 1
    self.maxmidrange = Mid or 2
    self.maxshortrange = Short or 2
    self.maxpointdefrange= Point or 6
    return self
  end

  --- Function to set a new SAM firing engage range, use this method to adjust range while running MANTIS, e.g. for different setups day and night
  -- @param #MANTIS self
  -- @param #number range Percent of the max fire range
  function MANTIS:SetNewSAMRangeWhileRunning(range)
    self:T(self.lid .. "SetNewSAMRangeWhileRunning")
    local range = range or 95
    if range < 0 or range > 100 then
      range = 95
    end
    self.engagerange = range
    self:_RefreshSAMTable()
    self.mysead.EngagementRange = range
    return self
  end

  --- Function to set switch-on/off the debug state
  -- @param #MANTIS self
  -- @param #boolean onoff Set true to switch on
  function MANTIS:Debug(onoff)
    self:T(self.lid .. "SetDebug")
    local onoff = onoff or false
    self.debug = onoff
    if onoff then
      -- Debug trace.
      BASE:TraceOn()
      BASE:TraceClass("MANTIS")
      BASE:TraceLevel(1)
    else
      BASE:TraceOff()
    end
    return self
  end

  --- Function to get the HQ object for further use
  -- @param #MANTIS self
  -- @return Wrapper.Group#GROUP The HQ #GROUP object or *nil* if it doesn't exist
  function MANTIS:GetCommandCenter()
    self:T(self.lid .. "GetCommandCenter")
    if self.HQ_CC then
      return self.HQ_CC
    else
      return nil
    end
  end

  --- Function to set separate AWACS detection instance
  -- @param #MANTIS self
  -- @param #string prefix Name of the AWACS group in the mission editor
  function MANTIS:SetAwacs(prefix)
    self:T(self.lid .. "SetAwacs")
    if prefix ~= nil then
      if type(prefix) == "string" then
        self.AWACS_Prefix = prefix
        self.advAwacs = true
      end
    end
    return self
  end

  --- Function to set AWACS detection range. Defaults to 250.000m (250km) - use **before** starting your Mantis!
  -- @param #MANTIS self
  -- @param #number range Detection range of the AWACS group
  function MANTIS:SetAwacsRange(range)
    self:T(self.lid .. "SetAwacsRange")
    local range = range or 250000
    self.awacsrange = range
    return self
  end

  --- Function to set the HQ object for further use
  -- @param #MANTIS self
  -- @param Wrapper.Group#GROUP group The #GROUP object to be set as HQ
  function MANTIS:SetCommandCenter(group)
    self:T(self.lid .. "SetCommandCenter")
    local group = group or nil
    if group ~= nil then
      if type(group) == "string" then
        self.HQ_CC = GROUP:FindByName(group)
        self.HQ_Template_CC = group
      else
        self.HQ_CC = group
        self.HQ_Template_CC = group:GetName()
      end
    end
    return self
  end
  
  --- Function to set how long INTEL DLINK remembers contacts.
  -- @param #MANTIS self
  -- @param #number seconds Remember this many seconds, at least 5 seconds.
  -- @return #MANTIS self
  function MANTIS:SetDLinkCacheTime(seconds)
    self.DLinkCacheTime = math.abs(seconds or 120)
    if self.DLinkCacheTime < 5 then self.DLinkCacheTime = 5 end
    return self
  end

  --- Function to set the detection interval
  -- @param #MANTIS self
  -- @param #number interval The interval in seconds
  function MANTIS:SetDetectInterval(interval)
    self:T(self.lid .. "SetDetectInterval")
    local interval = interval or 30
    self.detectinterval = interval
    return self
  end

  --- Function to set Advanded Mode
  -- @param #MANTIS self
  -- @param #boolean onoff If true, will activate Advanced Mode
  -- @param #number ratio [optional] Percentage to use for advanced mode, defaults to 100%
  -- @usage Advanced mode will *decrease* reactivity of MANTIS, if HQ and/or EWR network dies.  Set SAMs to RED state if both are dead.  Requires usage of an **HQ** object and the **dynamic** option.
  -- E.g. `mymantis:SetAdvancedMode(true, 90)`
  function MANTIS:SetAdvancedMode(onoff, ratio)
    self:T(self.lid .. "SetAdvancedMode")
    --self:T({onoff, ratio})
    local onoff = onoff or false
    local ratio = ratio or 100
    if (type(self.HQ_Template_CC) == "string") and onoff and self.dynamic then
      self.adv_ratio = ratio
      self.advanced = true
      self.adv_state = 0
      self.Adv_EWR_Group = SET_GROUP:New():FilterPrefixes(self.EWR_Templates_Prefix):FilterCoalitions(self.Coalition):FilterStart()
      self:I(string.format("***** Starting Advanced Mode MANTIS Version %s *****", self.version))
    else
      local text = self.lid.." Advanced Mode requires a HQ and dynamic to be set. Revisit your MANTIS:New() statement to add both."
      local m= MESSAGE:New(text,10,"MANTIS",true):ToAll()
      self:E(text)
    end
    return self
  end

  --- Set using Emissions on/off instead of changing alarm state
  -- @param #MANTIS self
  -- @param #boolean switch Decide if we are changing alarm state or Emission state
  function MANTIS:SetUsingEmOnOff(switch)
    self:T(self.lid .. "SetUsingEmOnOff")
    self.UseEmOnOff = switch or false
    return self
  end

  --- Set using your own #INTEL_DLINK object instead of #DETECTION
  -- @param #MANTIS self
  -- @param Ops.Intel#INTEL_DLINK DLink The data link object to be used.
  function MANTIS:SetUsingDLink(DLink)
    self:T(self.lid .. "SetUsingDLink")
    self.DLink = true
    self.Detection = DLink
    self.DLTimeStamp = timer.getAbsTime()
    return self
  end

  --- [Internal] Function to check if HQ is alive
  -- @param #MANTIS self
  -- @return #boolean True if HQ is alive, else false
  function MANTIS:_CheckHQState()
    self:T(self.lid .. "CheckHQState")
    local text = self.lid.." Checking HQ State"
    local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug)
    if self.verbose then self:I(text) end
    -- start check
    if self.advanced then
      local hq = self.HQ_Template_CC
      local hqgrp = GROUP:FindByName(hq)
      if hqgrp then
        if hqgrp:IsAlive() then -- ok we're on, hq exists and as alive
          --self:T(self.lid.." HQ is alive!")
          return true
        else
          --self:T(self.lid.." HQ is dead!")
          return false
        end
      end
    end
    return self
  end

  --- [Internal] Function to check if EWR is (at least partially) alive
  -- @param #MANTIS self
  -- @return #boolean True if EWR is alive, else false
  function MANTIS:_CheckEWRState()
    self:T(self.lid .. "CheckEWRState")
    local text = self.lid.." Checking EWR State"
    --self:T(text)
    local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug)
    if self.verbose then self:I(text) end
    -- start check
    if self.advanced then
      local EWR_Group = self.Adv_EWR_Group
      --local EWR_Set = EWR_Group.Set
      local nalive = EWR_Group:CountAlive()
      if self.advAwacs then
        local awacs = GROUP:FindByName(self.AWACS_Prefix)
        if awacs ~= nil then
          if awacs:IsAlive() then
            nalive = nalive+1
          end
        end
      end
      --self:T(self.lid..string.format(" No of EWR alive is %d", nalive))
      if nalive > 0 then
        return true
      else
        return false
      end
    end
    return self
  end
  
  --- [Internal] Check if any EWR or AWACS is still alive
  -- @param #MANTIS self
  -- @return #boolean outcome
  function MANTIS:_CheckAnyEWRAlive()
    self:T(self.lid .. "_CheckAnyEWRAlive")
    local alive = false
    if self.EWR_Group:CountAlive() > 0 then
      alive = true
    end
    if not alive and self.AWACS_Prefix then
      local awacs = GROUP:FindByName(self.AWACS_Prefix)
      if awacs and awacs:IsAlive() then
        alive = true
      end
    end
    return alive
  end

  --- [Internal] Function to determine state of the advanced mode
  -- @param #MANTIS self
  -- @return #number Newly calculated interval
  -- @return #number Previous state for tracking 0, 1, or 2
  function MANTIS:_CalcAdvState()
    self:T(self.lid .. "CalcAdvState")
    local m=MESSAGE:New(self.lid.." Calculating Advanced State",10,"MANTIS"):ToAllIf(self.debug)
    if self.verbose then self:I(self.lid.." Calculating Advanced State") end
    -- start check
    local currstate = self.adv_state -- save curr state for comparison later
    local EWR_State = self:_CheckEWRState()
    local HQ_State = self:_CheckHQState()
    -- set state
    if EWR_State and HQ_State then -- both alive
      self.adv_state = 0 --everything is fine
    elseif EWR_State or HQ_State then -- one alive
      self.adv_state = 1 --slow down level 1
    else -- none alive
      self.adv_state = 2 --slow down level 2
    end
    -- calculate new detectioninterval
    local interval = self.detectinterval -- e.g. 30
    local ratio = self.adv_ratio / 100 -- e.g. 80/100 = 0.8
    ratio = ratio * self.adv_state -- e.g 0.8*2 = 1.6
    local newinterval = interval + (interval * ratio) -- e.g. 30+(30*1.6) = 78
    if self.debug or self.verbose then
      local text = self.lid..string.format(" Calculated OldState/NewState/Interval: %d / %d / %d", currstate, self.adv_state, newinterval)
      --self:T(text)
      local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug)
      if self.verbose then self:I(text) end
    end
    return newinterval, currstate
  end

  --- Function to set autorelocation for HQ and EWR objects. Note: Units must be actually mobile in DCS!
  -- @param #MANTIS self
  -- @param #boolean hq If true, will relocate HQ object
  -- @param #boolean ewr If true, will relocate  EWR objects
  function MANTIS:SetAutoRelocate(hq, ewr)
    self:T(self.lid .. "SetAutoRelocate")
    --self:T({hq, ewr})
    local hqrel = hq or false
    local ewrel = ewr or false
    if hqrel or ewrel then
      self.autorelocate = true
      self.autorelocateunits = { HQ = hqrel, EWR = ewrel }
      --self:T({self.autorelocate, self.autorelocateunits})
    end
    return self
  end

  --- [Internal] Function to execute the relocation
  -- @param #MANTIS self
  -- @return #MANTIS self 
  function MANTIS:_RelocateGroups()
    self:T(self.lid .. "RelocateGroups")
    local text = self.lid.." Relocating Groups"
    local m= MESSAGE:New(text,10,"MANTIS",true):ToAllIf(self.debug)
    if self.verbose then self:I(text) end
    if self.autorelocate then
      -- relocate HQ
      local HQGroup = self.HQ_CC
      if self.autorelocateunits.HQ and self.HQ_CC and HQGroup:IsAlive() then --only relocate if HQ exists
        local _hqgrp = self.HQ_CC
        --self:T(self.lid.." Relocating HQ")
        local text = self.lid.." Relocating HQ"
        --local m= MESSAGE:New(text,10,"MANTIS"):ToAll()
          _hqgrp:RelocateGroundRandomInRadius(20,500,true,true,nil,true)
      end
      --relocate EWR
      -- TODO: maybe dependent on AlarmState? Observed: SA11 SR only relocates if no objects in reach
      if self.autorelocateunits.EWR then
         -- get EWR Group
         local EWR_GRP = SET_GROUP:New():FilterPrefixes(self.EWR_Templates_Prefix):FilterCoalitions(self.Coalition):FilterOnce()
         local EWR_Grps = EWR_GRP.Set --table of objects in SET_GROUP
         for _,_grp in pairs (EWR_Grps) do
             if _grp:IsAlive() and _grp:IsGround() then
              --self:T(self.lid.." Relocating EWR ".._grp:GetName())
              local text = self.lid.." Relocating EWR ".._grp:GetName()
              local m= MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug)
              if self.verbose then self:I(text) end
              _grp:RelocateGroundRandomInRadius(20,500,true,true,nil,true)
             end
         end
      end
    end
    return self
  end
  
  --- [Internal] Function to check accept and reject zones
  -- @param #MANTIS self
  -- @param Core.Point#COORDINATE coord The coordinate to check
  -- @return #boolean outcome
  function MANTIS:_CheckCoordinateInZones(coord)
    -- DEBUG
    self:T(self.lid.."_CheckCoordinateInZones")
    local inzone = false
    -- acceptzones
    self:T(string.format("AcceptZonesNo = %d | RejectZonesNo = %d | ConflictZonesNo = %d",self.AcceptZonesNo,self.RejectZonesNo,self.ConflictZonesNo))
    if self.AcceptZonesNo > 0 then
      for _,_zone in pairs(self.AcceptZones) do
        local zone = _zone -- Core.Zone#ZONE
        if zone:IsCoordinateInZone(coord) then
          inzone = true
          self:T(self.lid.."Target coord in Accept Zone!")
          break
        end
      end
    end
    -- rejectzones
    if self.RejectZonesNo > 0 then 
      for _,_zone in pairs(self.RejectZones) do
        local zone = _zone -- Core.Zone#ZONE
        if zone:IsCoordinateInZone(coord) then
          inzone = false
          self:T(self.lid.."Target coord in Reject Zone!")
          break
        end
      end
    end
    -- conflictzones
    if self.ConflictZonesNo > 0 then
      for _,_zone in pairs(self.ConflictZones) do
        local zone = _zone -- Core.Zone#ZONE
        if zone:IsCoordinateInZone(coord) then
          inzone = true
          self:T(self.lid.."Target coord in Conflict Zone!")
          break
        end
      end
    end   
    return inzone
  end
  
  --- [Internal] Function to prefilter height based
  -- @param #MANTIS self
  -- @param #number height
  -- @return #table set
  function MANTIS:_PreFilterHeight(height)
    self:T(self.lid.."_PreFilterHeight")   
    local set = {}
    local dlink = self.Detection -- Ops.Intel#INTEL_DLINK
    local detectedgroups = dlink:GetContactTable()
    for _,_contact in pairs(detectedgroups) do
      local contact = _contact -- Ops.Intel#INTEL.Contact
      local grp = contact.group -- Wrapper.Group#GROUP
      if grp:IsAlive() then
        if grp:GetHeight(true) < height then
          local coord = grp:GetCoordinate()
          table.insert(set,coord)
        end
      end
    end
    return set
  end
  
  --- [Internal] Function to check if any object is in the given SAM zone
  -- @param #MANTIS self
  -- @param #table dectset Table of coordinates of detected items
  -- @param Core.Point#COORDINATE samcoordinate Coordinate object.
  -- @param #number radius Radius to check.
  -- @param #number height Height to check.
  -- @param #boolean dlink Data from DLINK.
  -- @return #boolean True if in any zone, else false
  -- @return #number Distance Target distance in meters or zero when no object is in zone
  function MANTIS:_CheckObjectInZone(dectset, samcoordinate, radius, height, dlink)
    self:T(self.lid.."_CheckObjectInZone")
    -- check if non of the coordinate is in the given defense zone
    local rad = radius or self.checkradius
    local set = dectset
    if dlink then
      -- DEBUG
      set = self:_PreFilterHeight(height)
    end
    --self.friendlyset -- Core.Set#SET_GROUP
    if self.checkforfriendlies == true and self.friendlyset == nil then
      self.friendlyset = SET_GROUP:New():FilterCoalitions(self.Coalition):FilterCategories({"plane","helicopter"}):FilterFunction(function(grp) if grp and grp:InAir() then return true else return false end end):FilterStart()
    end
    for _,_coord in pairs (set) do
      local coord = _coord  -- get current coord to check
      -- output for cross-check
      local targetdistance = samcoordinate:DistanceFromPointVec2(coord)
      if not targetdistance then
        targetdistance = samcoordinate:Get2DDistance(coord)
      end
      -- check accept/reject zones
      local zonecheck = true
      self:T("self.usezones = "..tostring(self.usezones))
      if self.usezones then
        -- DONE
        zonecheck = self:_CheckCoordinateInZones(coord)
      end
      if self.verbose and self.debug then
        --local dectstring = coord:ToStringLLDMS()
        local samstring = samcoordinate:ToStringMGRS({MGRS_Accuracy=0})
        samstring = string.gsub(samstring,"%s","")
        local inrange = "false"
        if targetdistance <= rad then
          inrange = "true"
        end
        local text = string.format("Checking SAM at %s | Tgtdist %.1fkm | Rad %.1fkm | Inrange %s", samstring, targetdistance/1000, rad/1000, inrange)
        local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug)
        self:T(self.lid..text)
      end
      -- friendlies around?
      local nofriendlies = true
      if self.checkforfriendlies == true then
        local closestfriend, distance = self.friendlyset:GetClosestGroup(samcoordinate)
        if closestfriend and distance and distance < rad then
          nofriendlies = false
        end
      end
      -- end output to cross-check
      if targetdistance <= rad and zonecheck == true and nofriendlies == true then
        return true, targetdistance
      end
    end
    return false, 0
  end

  --- [Internal] Function to start the detection via EWR groups - if INTEL isn\'t available
  -- @param #MANTIS self
  -- @return Functional.Detection #DETECTION_AREAS The running detection set
  function MANTIS:StartDetection()
    self:T(self.lid.."Starting Detection")

    -- start detection
    local groupset = self.EWR_Group
    local grouping = self.grouping or 5000
    --local acceptrange = self.acceptrange or 80000
    local interval = self.detectinterval or 20
    
    local MANTISdetection = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones
    MANTISdetection:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER })
    --MANTISdetection:SetAcceptRange(acceptrange) -- deprecated - in range of SAMs is used anyway
    MANTISdetection:SetRefreshTimeInterval(interval)
    MANTISdetection:__Start(2)

    return MANTISdetection
  end
  
  --- [Internal] Function to start the detection with INTEL via EWR groups
  -- @param #MANTIS self
  -- @return Ops.Intel#INTEL_DLINK The running detection set
  function MANTIS:StartIntelDetection()
    self:T(self.lid.."Starting Intel Detection")
    -- DEBUG
    -- start detection
    local groupset = self.EWR_Group
    local samset = self.SAM_Group
    
    self.intelset = {}
    
    local IntelOne = INTEL:New(groupset,self.Coalition,self.name.." IntelOne")
    --IntelOne:SetClusterAnalysis(true,true)
    --IntelOne:SetClusterRadius(5000)
    IntelOne:Start()
    
    local IntelTwo = INTEL:New(samset,self.Coalition,self.name.." IntelTwo")
    --IntelTwo:SetClusterAnalysis(true,true)
    --IntelTwo:SetClusterRadius(5000)
    IntelTwo:Start()
    
    local CacheTime = self.DLinkCacheTime or 120
    local IntelDlink = INTEL_DLINK:New({IntelOne,IntelTwo},self.name.." DLINK",22,CacheTime)
    
    IntelDlink:__Start(1)
    
    self:SetUsingDLink(IntelDlink)
    
    table.insert(self.intelset, IntelOne)
    table.insert(self.intelset, IntelTwo)
    
    return IntelDlink
  end
  
  --- [Internal] Function to start the detection via AWACS if defined as separate (classic)
  -- @param #MANTIS self
  -- @return Functional.Detection #DETECTION_AREAS The running detection set
  function MANTIS:StartAwacsDetection()
    self:T(self.lid.."Starting Awacs Detection")

    -- start detection
    local group = self.AWACS_Prefix
    local groupset = SET_GROUP:New():FilterPrefixes(group):FilterCoalitions(self.Coalition):FilterStart()
    local grouping = self.grouping or 5000
    --local acceptrange = self.acceptrange or 80000
    local interval = self.detectinterval or 60

    --@param Functional.Detection #DETECTION_AREAS _MANTISdetection [Internal] The MANTIS detection object
    local MANTISAwacs = DETECTION_AREAS:New( groupset, grouping ) --[Internal] Grouping detected objects to 5000m zones
    MANTISAwacs:FilterCategories({ Unit.Category.AIRPLANE, Unit.Category.HELICOPTER })
    MANTISAwacs:SetAcceptRange(self.awacsrange)  --250km
    MANTISAwacs:SetRefreshTimeInterval(interval)
    MANTISAwacs:Start()

    return MANTISAwacs
  end
  
   --- [Internal] Function to get SAM firing data from units types.
  -- @param #MANTIS self
  -- @param #string grpname Name of the group
  -- @param #boolean mod HDS mod flag
  -- @param #boolean sma SMA mod flag
  -- @param #boolean chm CH mod flag
  -- @return #number range Max firing range
  -- @return #number height Max firing height
  -- @return #string type Long, medium or short range
  -- @return #number blind "blind" spot
  function MANTIS:_GetSAMDataFromUnits(grpname,mod,sma,chm)
    self:T(self.lid.."_GetSAMDataFromUnits")
    local found = false
    local range = self.checkradius
    local height = 3000
    local type = MANTIS.SamType.MEDIUM
    local radiusscale = self.radiusscale[type]
    local blind = 0
    local group = GROUP:FindByName(grpname) -- Wrapper.Group#GROUP
    local units = group:GetUnits()
    local SAMData = self.SamData
    if mod then
      SAMData = self.SamDataHDS
    elseif sma then
      SAMData = self.SamDataSMA
    elseif chm then
      SAMData = self.SamDataCH
    end
    --self:I("Looking to auto-match for "..grpname)
    for _,_unit in pairs(units) do
      local unit = _unit -- Wrapper.Unit#UNIT
      local type = string.lower(unit:GetTypeName())
      --self:I(string.format("Matching typename: %s",type))
      for idx,entry in pairs(SAMData) do
        local _entry = entry -- #MANTIS.SamData
        local _radar = string.lower(_entry.Radar)
        --self:I(string.format("Trying typename: %s",_radar))
        if string.find(type,_radar,1,true) then
          type = _entry.Type
          radiusscale = self.radiusscale[type]
          range = _entry.Range * 1000 * radiusscale -- max firing range used as switch-on
          height = _entry.Height * 1000 -- max firing height
          blind = _entry.Blindspot * 100 -- blind spot range 
          --self:I(string.format("Match: %s - %s",_radar,type))
          found = true
          break
        end
      end
      if found then break end
    end
    --- AAA or Point Defense
    if not found then
      local grp = GROUP:FindByName(grpname)
      if (grp and grp:IsAlive() and grp:IsAAA()) or string.find(grpname,"AAA",1,true) then
        range = 2000
        height = 2000
        blind = 50
        type = MANTIS.SamType.POINT
        found = true
      end
    end
    if not found then
      self:E(self.lid .. string.format("*****Could not match radar data for %s! Will default to midrange values!",grpname))
    end
    return range, height, type, blind
  end
  
  --- [Internal] Function to get SAM firing data
  -- @param #MANTIS self
  -- @param #string grpname Name of the group
  -- @return #number range Max firing range
  -- @return #number height Max firing height
  -- @return #string type Long, medium or short range
  -- @return #number blind "blind" spot
  function MANTIS:_GetSAMRange(grpname)
    self:T(self.lid.."_GetSAMRange for "..tostring(grpname))
    local range = self.checkradius
    local height = 3000
    local type = MANTIS.SamType.MEDIUM
    local radiusscale = self.radiusscale[type]
    local blind = 0
    local found = false
    local HDSmod = false
    local SMAMod = false
    local CHMod = false
    if string.find(grpname,"HDS",1,true) then
      HDSmod = true
    elseif string.find(grpname,"SMA",1,true) then
      SMAMod = true
    elseif string.find(grpname,"CHM",1,true) then
      CHMod = true
    end
    --if self.automode then
      for idx,entry in pairs(self.SamData) do
        self:T2("ID = " .. idx)
        if string.find(grpname,idx,1,true) then
          local _entry = entry -- #MANTIS.SamData
          type = _entry.Type
          radiusscale = self.radiusscale[type]
          range = _entry.Range * 1000 * radiusscale -- max firing range
          height = _entry.Height * 1000 -- max firing height        
          blind = _entry.Blindspot 
          self:T("Matching Groupname = " .. grpname .. " Range= " .. range)
          found = true
          break
        end
      end
    --end
    --- Secondary - AAA or Point Defense
    if not found then
      local grp = GROUP:FindByName(grpname)
      if (grp and grp:IsAlive() and grp:IsAAA()) or string.find(grpname,"AAA",1,true) then
        range = 2000
        height = 2000
        blind = 50
        type = MANTIS.SamType.POINT
        found = true
      end
    end
    --- Tertiary filter if not found
    if (not found) or HDSmod or SMAMod or CHMod then
      range, height, type = self:_GetSAMDataFromUnits(grpname,HDSmod,SMAMod,CHMod)
    elseif not found then
      self:E(self.lid .. string.format("*****Could not match radar data for %s! Will default to midrange values!",grpname))
    end
    if found and string.find(grpname,"SHORAD",1,true) then
      type = MANTIS.SamType.POINT -- force short on match
    end
    return range, height, type, blind
  end
  
  --- [Internal] Function to set the SAM start state
  -- @param #MANTIS self
  -- @return #MANTIS self
  function MANTIS:SetSAMStartState()
    -- DONE: if using dynamic filtering, update SAM_Table and the (active) SEAD groups, pull req #1405/#1406
    -- DONE: Auto mode
    self:T(self.lid.."Setting SAM Start States")
     -- get SAM Group
     local SAM_SET = self.SAM_Group
     local SAM_Grps = SAM_SET.Set --table of objects
     local SAM_Tbl = {} -- table of SAM defense zones
     local SAM_Tbl_lg = {} -- table of long range SAM defense zones
     local SAM_Tbl_md = {} -- table of mid range SAM defense zones
     local SAM_Tbl_sh = {} -- table of short range SAM defense zones
     local SAM_Tbl_pt = {} -- table of point defense/AAA
     local SEAD_Grps = {} -- table of SAM names to make evasive
     local engagerange = self.engagerange -- firing range in % of max
     --cycle through groups and set alarm state etc
     for _i,_group in pairs (SAM_Grps) do
      if _group:IsGround() and _group:IsAlive() then
        local group = _group -- Wrapper.Group#GROUP
        -- DONE: add emissions on/off
        if self.UseEmOnOff then
          group:OptionAlarmStateRed()
          group:EnableEmission(false)
          --group:SetAIOff()
        else
          group:OptionAlarmStateGreen() -- AI off
        end
        group:OptionEngageRange(engagerange)  --default engagement will be 95% of firing range
        local grpname = group:GetName()
        local grpcoord = group:GetCoordinate()
        local grprange,grpheight,type,blind  = self:_GetSAMRange(grpname)
        table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind, type})
        --table.insert( SEAD_Grps, grpname )
        if type == MANTIS.SamType.LONG then
          table.insert( SAM_Tbl_lg, {grpname, grpcoord, grprange, grpheight, blind, type})
          table.insert( SEAD_Grps, grpname )
          self:T("SAM "..grpname.." is type LONG")
        elseif type == MANTIS.SamType.MEDIUM then
         table.insert( SAM_Tbl_md, {grpname, grpcoord, grprange, grpheight, blind, type})
         table.insert( SEAD_Grps, grpname )
         self:T("SAM "..grpname.." is type MEDIUM")
        elseif type == MANTIS.SamType.SHORT then
          table.insert( SAM_Tbl_sh, {grpname, grpcoord, grprange, grpheight, blind, type})
          table.insert( SEAD_Grps, grpname )
          self:T("SAM "..grpname.." is type SHORT")
        elseif type == MANTIS.SamType.POINT then
          table.insert( SAM_Tbl_pt, {grpname, grpcoord, grprange, grpheight, blind, type})
          self:T("SAM "..grpname.." is type POINT")
          self.ShoradGroupSet:Add(grpname,group)
          if not self.autoshorad then
            table.insert( SEAD_Grps, grpname )
          end  
        end
        self.SamStateTracker[grpname] = "GREEN"
        end
     end
     self.SAM_Table = SAM_Tbl
     self.SAM_Table_Long = SAM_Tbl_lg
     self.SAM_Table_Medium = SAM_Tbl_md
     self.SAM_Table_Short = SAM_Tbl_sh
     self.SAM_Table_PointDef = SAM_Tbl_pt
     -- make SAMs evasive
     local mysead = SEAD:New( SEAD_Grps, self.Padding ) -- Functional.Sead#SEAD
     mysead:SetEngagementRange(engagerange)
     mysead:AddCallBack(self)
     if self.UseEmOnOff then
      mysead:SwitchEmissions(true)
     end
     self.mysead = mysead
     return self
  end

  --- [Internal] Function to update SAM table and SEAD state
  -- @param #MANTIS self
  -- @return #MANTIS self
  function MANTIS:_RefreshSAMTable()
    self:T(self.lid.."RefreshSAMTable")
    -- Requires SEAD 0.2.2 or better
     -- get SAM Group
     local SAM_SET = self.SAM_Group
     local SAM_Grps = SAM_SET.Set --table of objects
     local SAM_Tbl = {} -- table of SAM defense zones
     local SAM_Tbl_lg = {} -- table of long range SAM defense zones
     local SAM_Tbl_md = {} -- table of mid range SAM defense zones
     local SAM_Tbl_sh = {} -- table of short range SAM defense zones
     local SAM_Tbl_pt = {} -- table of point defense/AAA
     local SEAD_Grps = {} -- table of SAM names to make evasive
     local engagerange = self.engagerange -- firing range in % of max
     --cycle through groups and set alarm state etc
     for _i,_group in pairs (SAM_Grps) do
        local group = _group -- Wrapper.Group#GROUP
        group:OptionEngageRange(engagerange)  --engagement will be 95% of firing range
        if group:IsGround() and group:IsAlive() then
          local grpname = group:GetName()
          local grpcoord = group:GetCoordinate()
          local grprange, grpheight,type,blind  = self:_GetSAMRange(grpname)
          -- TODO the below might stop working at some point after some hours, needs testing
          --local radaralive = group:IsSAM()
          local radaralive = true
          table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind, type}) -- make the table lighter, as I don't really use the zone here
          table.insert( SEAD_Grps, grpname )
          if type == MANTIS.SamType.LONG and radaralive then
            table.insert( SAM_Tbl_lg, {grpname, grpcoord, grprange, grpheight, blind, type})
            self:T({grpname,grprange, grpheight})
          elseif type == MANTIS.SamType.MEDIUM and radaralive then
           table.insert( SAM_Tbl_md, {grpname, grpcoord, grprange, grpheight, blind, type})
           self:T({grpname,grprange, grpheight})
          elseif type == MANTIS.SamType.SHORT and radaralive then
           table.insert( SAM_Tbl_sh, {grpname, grpcoord, grprange, grpheight, blind, type})
           self:T({grpname,grprange, grpheight})
          elseif type == MANTIS.SamType.POINT or (not radaralive) then
            table.insert( SAM_Tbl_pt, {grpname, grpcoord, grprange, grpheight, blind, type})
            self:T({grpname,grprange, grpheight})
            self.ShoradGroupSet:Add(grpname,group)
            if self.autoshorad then
              self.Shorad.Groupset = self.ShoradGroupSet
            end
          end
        end
     end
     self.SAM_Table = SAM_Tbl
     self.SAM_Table_Long = SAM_Tbl_lg
     self.SAM_Table_Medium = SAM_Tbl_md
     self.SAM_Table_Short = SAM_Tbl_sh
     self.SAM_Table_PointDef = SAM_Tbl_pt
     -- make SAMs evasive
     if self.mysead ~= nil then
      local mysead = self.mysead
      mysead:UpdateSet( SEAD_Grps )
     end
     return self
  end

  --- Function to link up #MANTIS with a #SHORAD installation
  -- @param #MANTIS self
  -- @param Functional.Shorad#SHORAD Shorad The #SHORAD object
  -- @param #number Shoradtime Number of seconds #SHORAD stays active post wake-up
  function MANTIS:AddShorad(Shorad,Shoradtime)
    self:T(self.lid.."AddShorad")
    local Shorad = Shorad or nil
    local ShoradTime = Shoradtime or 600
    local ShoradLink = true
    if Shorad:IsInstanceOf("SHORAD") then
      self.ShoradLink = ShoradLink
      self.Shorad = Shorad --#SHORAD
      self.ShoradTime = Shoradtime -- #number
    end
    return self
  end

  --- Function to unlink #MANTIS from a #SHORAD installation
  -- @param #MANTIS self
  function MANTIS:RemoveShorad()
    self:T(self.lid.."RemoveShorad")
    self.ShoradLink = false
    return self
  end

-----------------------------------------------------------------------
-- MANTIS main functions
-----------------------------------------------------------------------
  
  --- [Internal] Check detection function
  -- @param #MANTIS self
  -- @param #table samset Table of SAM data
  -- @param #table detset Table of COORDINATES
  -- @param #boolean dlink Using DLINK
  -- @param #number limit of SAM sites to go active on a contact
  -- @return #number instatusred
  -- @return #number instatusgreen
  -- @return #number activeshorads
  function MANTIS:_CheckLoop(samset,detset,dlink,limit)
    self:T(self.lid .. "CheckLoop " .. #detset .. " Coordinates")
    local switchedon = 0
    local instatusred = 0
    local instatusgreen = 0
    local activeshorads = 0
    local SEADactive = 0
    for _,_data in pairs (samset) do
      local samcoordinate = _data[2]
      local name = _data[1]
      local radius = _data[3]
      local height = _data[4]
      local blind = _data[5] * 1.25 + 1
      local shortsam = (_data[6] == MANTIS.SamType.SHORT) and true or false
      if not shortsam then
        shortsam = (_data[6] == MANTIS.SamType.POINT) and true or false
      end
      local samgroup = GROUP:FindByName(name)
      local IsInZone, Distance = self:_CheckObjectInZone(detset, samcoordinate, radius, height, dlink)
      local suppressed = self.SuppressedGroups[name] or false
      local activeshorad = false
      if self.Shorad and self.Shorad.ActiveGroups and self.Shorad.ActiveGroups[name] then
       activeshorad = true
      end
      if IsInZone and (not suppressed) and (not activeshorad) then --check any target in zone and not currently managed by SEAD
        if samgroup:IsAlive() then
          -- switch on SAM
          local switch = false
          if self.UseEmOnOff and switchedon < limit then
            -- DONE: add emissions on/off
            samgroup:EnableEmission(true)
            switchedon = switchedon + 1
            switch = true
          elseif (not self.UseEmOnOff) and switchedon < limit then
            samgroup:OptionAlarmStateRed()
            switchedon = switchedon + 1
            switch = true           
          end
          if self.SamStateTracker[name] ~= "RED" and switch then
            self:__RedState(1,samgroup)
            self.SamStateTracker[name] = "RED"
          end
          -- TODO doesn't work
          if shortsam == true and self.SmokeDecoy == true then
            self:T("Smoking")
            local units = samgroup:GetUnits() or {}
            local smoke = self.SmokeDecoyColor or SMOKECOLOR.White
            for _,unit in pairs(units) do
              if unit and unit:IsAlive() then
                unit:GetCoordinate():Smoke(smoke)
              end
            end
          end
          -- link in to SHORAD if available
          -- DONE: Test integration fully
          if self.ShoradLink and (Distance < self.ShoradActDistance or Distance < blind ) then -- don't give SHORAD position away too early
            local Shorad = self.Shorad  --Functional.Shorad#SHORAD
            local radius = self.checkradius
            local ontime = self.ShoradTime
            Shorad:WakeUpShorad(name, radius, ontime)
            self:__ShoradActivated(1,name, radius, ontime)
          end
          -- debug output
          if (self.debug or self.verbose) and switch then
            local text = string.format("SAM %s in alarm state RED!", name)
            --local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug
            if self.verbose then self:I(self.lid..text) end
          end
        end --end alive
      else
        if samgroup:IsAlive() and not suppressed and not activeshorad then
          -- switch off SAM
          if self.UseEmOnOff  then
            samgroup:EnableEmission(false)
          else
            samgroup:OptionAlarmStateGreen()
          end
          if self.SamStateTracker[name] ~= "GREEN" then
            self:__GreenState(1,samgroup)
            self.SamStateTracker[name] = "GREEN"
          end
          if self.debug or self.verbose then
            local text = string.format("SAM %s in alarm state GREEN!", name)
            --local m=MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.debug)
            if self.verbose then self:I(self.lid..text) end
          end
        end --end alive
      end --end check     
    end --for loop
    if self.debug or self.verbose or self.logsamstatus then
      for _,_status in pairs(self.SamStateTracker) do
        if _status == "GREEN" then
          instatusgreen=instatusgreen+1
        elseif _status == "RED" then
          instatusred=instatusred+1
        end
      end
      if self.Shorad then
        for _,_name in pairs(self.Shorad.ActiveGroups or {}) do
          activeshorads=activeshorads+1
        end
      end
    end
    return instatusred, instatusgreen, activeshorads
  end
  
  --- [Internal] Check detection function
  -- @param #MANTIS self
  -- @param Functional.Detection#DETECTION_AREAS detection Detection object
  -- @param #boolean dlink
  -- @param #boolean reporttolog
  -- @return #MANTIS self
  function MANTIS:_Check(detection,dlink,reporttolog)
    self:T(self.lid .. "Check")
    --get detected set
    local detset = detection:GetDetectedItemCoordinates()
    --self:T("Check:", {detset})
    -- update SAM Table evey 3 runs
    if self.checkcounter%3 == 0 then
      self:_RefreshSAMTable()
    end
    self.checkcounter = self.checkcounter + 1
    local instatusred = 0
    local instatusgreen = 0
    local activeshorads = 0
    -- switch SAMs on/off if (n)one of the detected groups is inside their reach
    if self.automode then
      local samset = self.SAM_Table_Long -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height
      local instatusredl, instatusgreenl, activeshoradsl = self:_CheckLoop(samset,detset,dlink,self.maxlongrange)
      local samset = self.SAM_Table_Medium -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height
      local instatusredm, instatusgreenm, activeshoradsm = self:_CheckLoop(samset,detset,dlink,self.maxmidrange)
      local samset = self.SAM_Table_Short -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height
      local instatusreds, instatusgreens, activeshoradss = self:_CheckLoop(samset,detset,dlink,self.maxshortrange)
      local samset = self.SAM_Table_PointDef -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height
      instatusred, instatusgreen, activeshorads = self:_CheckLoop(samset,detset,dlink,self.maxpointdefrange)
    else
      local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height
      instatusred, instatusgreen, activeshorads = self:_CheckLoop(samset,detset,dlink,self.maxclassic)
    end
    
    local function GetReport()
      local statusreport = REPORT:New("\nMANTIS Status "..self.name)
      statusreport:Add("+-----------------------------+")
      statusreport:Add(string.format("+ SAM in RED State: %2d",instatusred))
      statusreport:Add(string.format("+ SAM in GREEN State: %2d",instatusgreen))
      if self.Shorad then
       statusreport:Add(string.format("+ SHORAD active: %2d",activeshorads))  
      end
      statusreport:Add("+-----------------------------+")
      return statusreport
    end
    
    if self.debug or self.verbose then
      local statusreport = GetReport()
      MESSAGE:New(statusreport:Text(),10):ToAll():ToLog()
    elseif reporttolog == true then
      local statusreport = GetReport()
      MESSAGE:New(statusreport:Text(),10):ToLog()
    end
    return self
  end

  --- [Internal] Relocation relay function
  -- @param #MANTIS self
  -- @return #MANTIS self
  function MANTIS:_Relocate()
    self:T(self.lid .. "Relocate")
    self:_RelocateGroups()
    return self
  end

  --- [Internal] Check advanced state
  -- @param #MANTIS self
  -- @return #MANTIS self
  function MANTIS:_CheckAdvState()
    self:T(self.lid .. "CheckAdvSate")
    local interval, oldstate = self:_CalcAdvState()
    local newstate = self.adv_state
    if newstate ~= oldstate then
      -- deal with new state
      self:__AdvStateChange(1,oldstate,newstate,interval)
      if newstate == 2 then
        -- switch alarm state RED
        self.state2flag = true
        local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates
        for _,_data in pairs (samset) do
          local name = _data[1]
          local samgroup = GROUP:FindByName(name)
          if samgroup:IsAlive() then
            if self.UseEmOnOff then
              -- DONE: add emissions on/off
              --samgroup:SetAIOn()
              samgroup:EnableEmission(true)
            else
              samgroup:OptionAlarmStateRed()
            end
          end -- end alive
        end -- end for loop
      elseif newstate <= 1 then
        -- change MantisTimer to slow down or speed up
        self.detectinterval = interval
        self.state2flag = false
      end
    end -- end newstate vs oldstate
    return self
  end

  --- [Internal] Check DLink state
  -- @param #MANTIS self
  -- @return #MANTIS self
  function MANTIS:_CheckDLinkState()
    self:T(self.lid .. "_CheckDLinkState")
    local dlink = self.Detection -- Ops.Intel#INTEL_DLINK
    local TS = timer.getAbsTime()
    if not dlink:Is("Running") and (TS - self.DLTimeStamp > 29) then
      self.DLink = false
      self.Detection = self:StartDetection() -- fall back
      self:I(self.lid .. "Intel DLink not running - switching back to single detection!")
    end
  end

  --- [Internal] Function to set start state
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @return #MANTIS self
  function MANTIS:onafterStart(From, Event, To)
    self:T({From, Event, To})
    self:T(self.lid.."Starting MANTIS")
    self:SetSAMStartState()
    if not INTEL then
      self.Detection = self:StartDetection()
    else
      self.Detection = self:StartIntelDetection()
    end
    --[[
    if self.advAwacs and not self.automode then
      self.AWACS_Detection = self:StartAwacsDetection()
    end
    --]]
    if self.autoshorad then
      self.Shorad = SHORAD:New(self.name.."-SHORAD","SHORAD",self.SAM_Group,self.ShoradActDistance,self.ShoradTime,self.coalition,self.UseEmOnOff)
      self.Shorad:SetDefenseLimits(80,95)
      self.ShoradLink = true
      self.Shorad.Groupset=self.ShoradGroupSet
      self.Shorad.debug = self.debug
    end
    if self.shootandscoot and self.SkateZones and self.Shorad then
      self.Shorad:AddScootZones(self.SkateZones,self.SkateNumber or 3,self.ScootRandom,self.ScootFormation)
    end
    self:__Status(-math.random(1,10))
    return self
  end

  --- [Internal] Before status function for MANTIS
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @return #MANTIS self
  function MANTIS:onbeforeStatus(From, Event, To)
    self:T({From, Event, To})
    -- check detection
    if not self.state2flag then
      self:_Check(self.Detection,self.DLink,self.logsamstatus)
    end
  
    local EWRAlive = self:_CheckAnyEWRAlive()
    
    local function FindSAMSRTR()
      for i=1,1000 do
        local randomsam = self.SAM_Group:GetRandom()
        if randomsam and randomsam:IsAlive() then
          if randomsam:IsSAM() then return randomsam end
        end
      end
    end
    
    -- Switch on a random SR/TR if no EWR left over
    if not EWRAlive then
      local randomsam = FindSAMSRTR() -- Wrapper.Group#GROUP
      if randomsam and randomsam:IsAlive() then
        if self.UseEmOnOff then
          randomsam:EnableEmission(true)
        else
          randomsam:OptionAlarmStateRed()
        end
        local name = randomsam:GetName()
        if self.SamStateTracker[name] ~= "RED" then
          self:__RedState(1,randomsam)
          self.SamStateTracker[name] = "RED"
        end
      end
    end
    
    -- relocate HQ and EWR
    if self.autorelocate then
      local relointerval = self.relointerval
      local thistime = timer.getAbsTime()
      local timepassed = thistime - self.TimeStamp

      local halfintv = math.floor(timepassed / relointerval)

      if halfintv >= 1 then
        self.TimeStamp = timer.getAbsTime()
        self:_Relocate()
        self:__Relocating(1)
      end
    end

    -- advanced state check
    if self.advanced then
      self:_CheckAdvState()
    end

    -- check DLink state
    if self.DLink then
      self:_CheckDLinkState()
    end

    return self
  end

  --- [Internal] Status function for MANTIS
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @return #MANTIS self
  function MANTIS:onafterStatus(From,Event,To)
    self:T({From, Event, To})
    -- Display some states
    if self.debug and self.verbose then
      self:I(self.lid .. "Status Report")
      for _name,_state in pairs(self.SamStateTracker) do
        self:I(string.format("Site %s | Status %s",_name,_state))
      end
    end
    local interval = self.detectinterval * -1
    self:__Status(interval)
    return self
  end

  --- [Internal] Function to stop MANTIS
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @return #MANTIS self
  function MANTIS:onafterStop(From, Event, To)
    self:T({From, Event, To})
    return self
  end

  --- [Internal] Function triggered by Event Relocating
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @return #MANTIS self
  function MANTIS:onafterRelocating(From, Event, To)
    self:T({From, Event, To})
    return self
  end

  --- [Internal] Function triggered by Event GreenState
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed
  -- @return #MANTIS self
  function MANTIS:onafterGreenState(From, Event, To, Group)
    self:T({From, Event, To, Group:GetName()})
    return self
  end

  --- [Internal] Function triggered by Event RedState
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param Wrapper.Group#GROUP Group The GROUP object whose state was changed
  -- @return #MANTIS self
  function MANTIS:onafterRedState(From, Event, To, Group)
    self:T({From, Event, To, Group:GetName()})
    return self
  end

  --- [Internal] Function triggered by Event AdvStateChange
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param #number Oldstate Old state - 0 = green, 1 = amber, 2 = red
  -- @param #number Newstate New state - 0 = green, 1 = amber, 2 = red
  -- @param #number Interval Calculated detection interval based on state and advanced feature setting
  -- @return #MANTIS self
  function MANTIS:onafterAdvStateChange(From, Event, To, Oldstate, Newstate, Interval)
    self:T({From, Event, To, Oldstate, Newstate, Interval})
    return self
  end

  --- [Internal] Function triggered by Event ShoradActivated
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param #string Name Name of the GROUP which SHORAD shall protect
  -- @param #number Radius Radius around the named group to find SHORAD groups
  -- @param #number Ontime Seconds the SHORAD will stay active
  function MANTIS:onafterShoradActivated(From, Event, To, Name, Radius, Ontime)
    self:T({From, Event, To, Name, Radius, Ontime})
    return self
  end
  
    --- [Internal] Function triggered by Event SeadSuppressionStart
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param Wrapper.Group#GROUP Group The suppressed GROUP object
  -- @param #string Name Name of the suppressed group
  -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object
  function MANTIS:onafterSeadSuppressionStart(From, Event, To, Group, Name, Attacker)
    self:T({From, Event, To, Name})
    self.SuppressedGroups[Name] = true
    if self.ShoradLink then
      local Shorad = self.Shorad
      local radius = self.checkradius
      local ontime = self.ShoradTime
      Shorad:WakeUpShorad(Name, radius, ontime, nil, true)
      self:__ShoradActivated(1,Name, radius, ontime)
    end
    return self
  end
  
    --- [Internal] Function triggered by Event SeadSuppressionEnd
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param Wrapper.Group#GROUP Group The suppressed GROUP object
  -- @param #string Name Name of the suppressed group
  function MANTIS:onafterSeadSuppressionEnd(From, Event, To, Group, Name)
    self:T({From, Event, To, Name})
    self.SuppressedGroups[Name] = false
    return self
  end
  
    --- [Internal] Function triggered by Event SeadSuppressionPlanned
  -- @param #MANTIS self
  -- @param #string From The From State
  -- @param #string Event The Event
  -- @param #string To The To State
  -- @param Wrapper.Group#GROUP Group The suppressed GROUP object
  -- @param #string Name Name of the suppressed group
  -- @param #number SuppressionStartTime Model start time of the suppression from `timer.getTime()`
  -- @param #number SuppressionEndTime Model end time of the suppression from `timer.getTime()`
  -- @param Wrapper.Group#GROUP Attacker The attacking GROUP object
  function MANTIS:onafterSeadSuppressionPlanned(From, Event, To, Group, Name, SuppressionStartTime, SuppressionEndTime, Attacker)
    self:T({From, Event, To, Name})
    return self
  end
  
end
-----------------------------------------------------------------------
-- MANTIS end
-----------------------------------------------------------------------
--- **Functional** - Short Range Air Defense System.
-- 
-- ===
--
-- ## Features: 
-- 
--   * Short Range Air Defense System
--   * Controls a network of short range air/missile defense groups.
-- 
-- ===
-- 
-- ## Missions:
--
-- ### [SHORAD - Short Range Air Defense](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Functional/Shorad)
-- 
-- ===
-- 
-- ### Author : **applevangelist **
-- 
-- @module Functional.Shorad
-- @image Functional.Shorad.jpg
--
-- Date: Nov 2021
-- Last Update: Jan 2025

-------------------------------------------------------------------------
--- **SHORAD** class, extends Core.Base#BASE
-- @type SHORAD
-- @field #string ClassName
-- @field #string name Name of this Shorad
-- @field #boolean debug Set the debug state
-- @field #string Prefixes String to be used to build the @{#Core.Set#SET_GROUP} 
-- @field #number Radius Shorad defense radius in meters
-- @field Core.Set#SET_GROUP Groupset The set of Shorad groups
-- @field Core.Set#SET_GROUP Samset The set of SAM groups to defend
-- @field #string Coalition The coalition of this Shorad
-- @field #number ActiveTimer How long a Shorad stays active after wake-up in seconds
-- @field #table ActiveGroups Table for the timer function
-- @field #string lid The log ID for the dcs.log
-- @field #boolean DefendHarms Default true, intercept incoming HARMS
-- @field #boolean DefendMavs Default true, intercept incoming AG-Missiles
-- @field #number DefenseLowProb Default 70, minimum detection limit
-- @field #number DefenseHighProb Default 90, maximum detection limit
-- @field #boolean UseEmOnOff Decide if we are using Emission on/off (default) or AlarmState red/green
-- @field #boolean shootandscoot If true, shoot and scoot between zones
-- @field #number SkateNumber Number of zones to consider
-- @field Core.Set#SET_ZONE SkateZones Zones in this set are considered
-- @field #number minscootdist Min distance of the next zone
-- @field #number maxscootdist Max distance of the next zone
-- @field #boolean scootrandomcoord If true, use a random coordinate in the zone and not the center
-- @field #string scootformation Formation to take for scooting, e.g. "Vee" or "Cone"  
-- @extends Core.Base#BASE


--- *Good friends are worth defending.* Mr Tushman, Wonder (the Movie) 
-- 
-- Simple Class for a more intelligent Short Range Air Defense System
-- 
-- #SHORAD
-- Moose derived missile intercepting short range defense system.
-- Protects a network of SAM sites. Uses events to switch on the defense groups closest to the enemy.
-- Easily integrated with @{Functional.Mantis#MANTIS} to complete the defensive system setup.
--
-- ## Usage
--
-- Set up a #SET_GROUP for the SAM sites to be protected:  
-- 
--        `local SamSet = SET_GROUP:New():FilterPrefixes("Red SAM"):FilterCoalitions("red"):FilterStart()`   
--    
-- By default, SHORAD will defense against both HARMs and AG-Missiles with short to medium range. The default defense probability is 70-90%.
-- When a missile is detected, SHORAD will activate defense groups in the given radius around the target for 10 minutes. It will *not* react to friendly fire.    
--        
-- ### Start a new SHORAD system, parameters are:
--   
--  * Name: Name of this SHORAD.  
--  * ShoradPrefix: Filter for the Shorad #SET_GROUP.  
--  * Samset: The #SET_GROUP of SAM sites to defend.  
--  * Radius: Defense radius in meters. 
--  * ActiveTimer: Determines how many seconds the systems stay on red alert after wake-up call.  
--  * Coalition: Coalition, i.e. "blue", "red", or "neutral".* 
--    
--        `myshorad = SHORAD:New("RedShorad", "Red SHORAD", SamSet, 25000, 600, "red")`       
--
-- ## Customization options   
--  
--  * myshorad:SwitchDebug(debug)
--  * myshorad:SwitchHARMDefense(onoff)
--  * myshorad:SwitchAGMDefense(onoff)
--  * myshorad:SetDefenseLimits(low,high)
--  * myshorad:SetActiveTimer(seconds)
--  * myshorad:SetDefenseRadius(meters)
--  * myshorad:AddScootZones(ZoneSet,Number,Random,Formation)
--
-- @field #SHORAD
SHORAD = {
  ClassName = "SHORAD",
  name = "MyShorad",
  debug = false,
  Prefixes = "",
  Radius = 20000,
  Groupset = nil,
  Samset = nil,
  Coalition = nil,
  ActiveTimer = 600, --stay on 10 mins
  ActiveGroups = {},
  lid = "",
  DefendHarms = true,
  DefendMavs = true,
  DefenseLowProb = 70,
  DefenseHighProb = 90,
  UseEmOnOff = true,
  shootandscoot = false,
  SkateNumber = 3,
  SkateZones = nil,
  minscootdist = 100,
  maxscootdist = 3000,
  scootrandomcoord = false,  
}

-----------------------------------------------------------------------
-- SHORAD System
-----------------------------------------------------------------------

do
  -- TODO Complete list?
  --- Missile enumerators
  -- @field Harms
  SHORAD.Harms = {
  ["AGM_88"] = "AGM_88",
  ["AGM_122"] = "AGM_122",
  ["AGM_84"] = "AGM_84",
  ["AGM_45"] = "AGM_45",
  ["ALARM"] = "ALARM",
  ["LD-10"] = "LD-10",
  ["X_58"] = "X_58",
  ["X_28"] = "X_28",
  ["X_25"] = "X_25",
  ["X_31"] = "X_31",
  ["Kh25"] = "Kh25",
  ["HY-2"] = "HY-2",
  ["ADM_141A"] = "ADM_141A",
  }
  
  --- TODO complete list?
  -- @field Mavs
  SHORAD.Mavs = {
  ["AGM"] = "AGM",
  ["C-701"] = "C-701",
  ["Kh25"] = "Kh25",
  ["Kh29"] = "Kh29",
  ["Kh31"] = "Kh31",
  ["Kh66"] = "Kh66",
  }
  
  --- Instantiates a new SHORAD object
  -- @param #SHORAD self
  -- @param #string Name Name of this SHORAD
  -- @param #string ShoradPrefix Filter for the Shorad #SET_GROUP
  -- @param Core.Set#SET_GROUP Samset The #SET_GROUP of SAM sites to defend
  -- @param #number Radius Defense radius in meters, used to switch on SHORAD groups **within** this radius
  -- @param #number ActiveTimer Determines how many seconds the systems stay on red alert after wake-up call
  -- @param #string Coalition Coalition, i.e. "blue", "red", or "neutral"
  -- @param #boolean UseEmOnOff Use Emissions On/Off rather than Alarm State Red/Green (default: use Emissions switch)
  -- @return #SHORAD self
  function SHORAD:New(Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition, UseEmOnOff) 
    local self = BASE:Inherit( self, FSM:New() )
    self:T({Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition})
    
    local GroupSet = SET_GROUP:New():FilterPrefixes(ShoradPrefix):FilterCoalitions(Coalition):FilterCategoryGround():FilterStart()

    self.name = Name or "MyShorad"
    self.Prefixes = ShoradPrefix or "SAM SHORAD"
    self.Radius = Radius or 20000
    self.Coalition = Coalition or "blue"
    self.Samset = Samset or GroupSet
    self.ActiveTimer = ActiveTimer or 600
    self.ActiveGroups = {}
    self.Groupset = GroupSet
    self.DefendHarms = true
    self.DefendMavs = true
    self.DefenseLowProb = 70 -- probability to detect a missile shot, low margin
    self.DefenseHighProb = 90  -- probability to detect a missile shot, high margin
    self.UseEmOnOff = true -- Decide if we are using Emission on/off (default) or AlarmState red/green
    if UseEmOnOff == false then self.UseEmOnOff = UseEmOnOff end
    self:I("*** SHORAD - Started Version 0.3.4")
    -- Set the string id for output to DCS.log file.
    self.lid=string.format("SHORAD %s | ", self.name)
    self:_InitState()
    self:HandleEvent(EVENTS.Shot, self.HandleEventShot)
    
    -- Start State.
    self:SetStartState("Running")
    self:AddTransition("*",             "WakeUpShorad",                 "*")
    self:AddTransition("*",             "CalculateHitZone",             "*")
    self:AddTransition("*",             "ShootAndScoot",                "*")
    
    return self
  end
  
  --- Initially set all groups to alarm state GREEN
  -- @param #SHORAD self
  -- @return #SHORAD self
  function SHORAD:_InitState()
    self:T(self.lid .. " _InitState")
    local table = {}
    local set = self.Groupset
    self:T({set = set})
    local aliveset = set:GetAliveSet() --#table
    for _,_group in pairs (aliveset) do
     if self.UseEmOnOff then
      --_group:SetAIOff()
      _group:EnableEmission(false)
      _group:OptionAlarmStateRed() --Wrapper.Group#GROUP
     else
      _group:OptionAlarmStateGreen() --Wrapper.Group#GROUP
     end
     _group:OptionDisperseOnAttack(30)
    end
    -- gather entropy
    for i=1,100 do
      math.random()
    end
    return self
  end
  
  --- Add a SET_ZONE of zones for Shoot&Scoot
  -- @param #SHORAD self
  -- @param Core.Set#SET_ZONE ZoneSet Set of zones to be used. Units will move around to the next (random) zone between 100m and 3000m away.
  -- @param #number Number Number of closest zones to be considered, defaults to 3.
  -- @param #boolean Random If true, use a random coordinate inside the next zone to scoot to.
  -- @param #string Formation Formation to use, defaults to "Cone". See mission editor dropdown for options.
  -- @return #SHORAD self
  function SHORAD:AddScootZones(ZoneSet, Number, Random, Formation)
    self:T(self.lid .. " AddScootZones")
    self.SkateZones = ZoneSet
    self.SkateNumber = Number or 3
    self.shootandscoot = true
    self.scootrandomcoord = Random
    self.scootformation = Formation or "Cone"   
    return self
  end
  
  --- Switch debug state on
  -- @param #SHORAD self
  -- @param #boolean debug Switch debug on (true) or off (false)
  -- @return #SHORAD self 
  function SHORAD:SwitchDebug(onoff)
    self:T( { onoff } )
    if onoff then
      self:SwitchDebugOn()
    else
      self:SwitchDebugOff()
    end
    return self
  end
  
  --- Switch debug state on
  -- @param #SHORAD self
  -- @return #SHORAD self 
  function SHORAD:SwitchDebugOn()
     self.debug = true
     --tracing
     BASE:TraceOn()
     BASE:TraceClass("SHORAD")
     return self
  end
  
  --- Switch debug state off
  -- @param #SHORAD self
  -- @return #SHORAD self 
  function SHORAD:SwitchDebugOff()
    self.debug = false
    BASE:TraceOff()
    return self
  end
  
  --- Switch defense for HARMs
  -- @param #SHORAD self
  -- @param #boolean onoff
  -- @return #SHORAD self 
  function SHORAD:SwitchHARMDefense(onoff)
    self:T( { onoff } )
    local onoff = onoff or true
    self.DefendHarms = onoff
    return self
  end
  
  --- Switch defense for AGMs
  -- @param #SHORAD self
  -- @param #boolean onoff
  -- @return #SHORAD self 
  function SHORAD:SwitchAGMDefense(onoff)
    self:T( { onoff } )
    local onoff = onoff or true
    self.DefendMavs = onoff
    return self
  end
  
  --- Set defense probability limits
  -- @param #SHORAD self
  -- @param #number low Minimum detection limit, integer 1-100
  -- @param #number high Maximum detection limit integer 1-100
  -- @return #SHORAD self 
  function SHORAD:SetDefenseLimits(low,high)
    self:T( { low, high } )
    local low = low or 70
    local high = high or 90
    if (low < 0) or (low > 100) or (low > high) then
      low = 70
    end
    if (high < 0) or (high > 100) or (high < low ) then
      high = 90
    end
    self.DefenseLowProb = low
    self.DefenseHighProb = high
    return self
  end
  
  --- Set the number of seconds a SHORAD site will stay active
  -- @param #SHORAD self
  -- @param #number seconds Number of seconds systems stay active
  -- @return #SHORAD self 
  function SHORAD:SetActiveTimer(seconds)
    self:T(self.lid .. " SetActiveTimer")
    local timer = seconds or 600
    if timer < 0 then
      timer = 600
    end
    self.ActiveTimer = timer
    return self
  end

  --- Set the number of meters for the SHORAD defense zone
  -- @param #SHORAD self
  -- @param #number meters Radius of the defense search zone in meters. #SHORADs in this range around a targeted group will go active 
  -- @return #SHORAD self 
  function SHORAD:SetDefenseRadius(meters)
  self:T(self.lid .. " SetDefenseRadius")
    local radius = meters or 20000
    if radius < 0 then
      radius = 20000
    end
    self.Radius = radius
    return self
  end
  
  --- Set using Emission on/off instead of changing alarm state
  -- @param #SHORAD self
  -- @param #boolean switch Decide if we are changing alarm state or AI state
  -- @return #SHORAD self 
  function SHORAD:SetUsingEmOnOff(switch)
  self:T(self.lid .. " SetUsingEmOnOff")
    self.UseEmOnOff = switch or false
    return self
  end
  
  --- Check if a HARM was fired
  -- @param #SHORAD self
  -- @param #string WeaponName
  -- @return #boolean Returns true for a match
  function SHORAD:_CheckHarms(WeaponName)
    self:T(self.lid .. " _CheckHarms")
    self:T( { WeaponName } )
    local hit = false
    if self.DefendHarms then
      for _,_name in pairs (SHORAD.Harms) do
        if string.find(WeaponName,_name,1,true) then hit = true end
      end
    end
    return hit
  end
  
  --- Check if an AGM was fired
  -- @param #SHORAD self
  -- @param #string WeaponName
  -- @return #boolean Returns true for a match
  function SHORAD:_CheckMavs(WeaponName)
    self:T(self.lid .. " _CheckMavs")
    self:T( { WeaponName } )
    local hit = false
    if self.DefendMavs then
      for _,_name in pairs (SHORAD.Mavs) do
        if string.find(WeaponName,_name,1,true) then hit = true end
      end
    end
    return hit
  end
  
  --- Check the coalition of the attacker
  -- @param #SHORAD self
  -- @param #string Coalition name
  -- @return #boolean Returns false for a match
  function SHORAD:_CheckCoalition(Coalition)
    self:T(self.lid .. " _CheckCoalition")
    local owncoalition = self.Coalition
    local othercoalition = ""
    if Coalition == 0 then 
      othercoalition = "neutral"
    elseif Coalition == 1 then
      othercoalition = "red"
    else
      othercoalition = "blue"
    end
    self:T({owncoalition = owncoalition, othercoalition = othercoalition})
    if owncoalition ~= othercoalition then
      return true
    else
      return false
    end
  end
  
  --- Check if the missile is aimed at a SHORAD
  -- @param #SHORAD self
  -- @param #string TargetGroupName Name of the target group
  -- @return #boolean Returns true for a match, else false
  function SHORAD:_CheckShotAtShorad(TargetGroupName)
    self:T(self.lid .. " _CheckShotAtShorad")
    local tgtgrp = TargetGroupName
    local shorad = self.Groupset
    local shoradset = shorad:GetAliveSet() --#table
    local returnname = false
    --local TDiff = 1
    for _,_groups in pairs (shoradset) do
      local groupname = _groups:GetName()
      if string.find(groupname, tgtgrp, 1, true) then
        returnname = true
      end
    end
    return returnname  
  end
  
  --- Check if the missile is aimed at a SAM site
  -- @param #SHORAD self
  -- @param #string TargetGroupName Name of the target group
  -- @return #boolean Returns true for a match, else false
  function SHORAD:_CheckShotAtSams(TargetGroupName)
    self:T(self.lid .. " _CheckShotAtSams")
    local tgtgrp = TargetGroupName
    local shorad = self.Samset
    --local shoradset = shorad:GetAliveSet() --#table
    local shoradset = shorad:GetSet() --#table
    local returnname = false
    for _,_groups in pairs (shoradset) do
      local groupname = _groups:GetName()
      if string.find(groupname, tgtgrp, 1, true) then
        if _groups:IsSAM() then
          returnname = true
        end
      end
    end
    return returnname
  end
  
  --- Calculate if the missile shot is detected
  -- @param #SHORAD self
  -- @return #boolean Returns true for a detection, else false
  function SHORAD:_ShotIsDetected()
    self:T(self.lid .. " _ShotIsDetected")
    if self.debug then return true end
    local IsDetected = false
    local DetectionProb = math.random(self.DefenseLowProb, self.DefenseHighProb)  -- reference value
    local ActualDetection = math.random(1,100) -- value for this shot
    if ActualDetection <= DetectionProb then
      IsDetected = true
    end
    return IsDetected
  end
  
  --- Wake up #SHORADs in a zone with diameter Radius for ActiveTimer seconds
  -- @param #SHORAD self
  -- @param #string TargetGroup Name of the target group used to build the #ZONE
  -- @param #number Radius Radius of the #ZONE
  -- @param #number ActiveTimer Number of seconds to stay active
  -- @param #number TargetCat (optional) Category, i.e. Object.Category.UNIT or Object.Category.STATIC
  -- @param #boolean ShotAt If true, function is called after a shot
  -- @return #SHORAD self 
  -- @usage Use this function to integrate with other systems, example   
  -- 
  -- local SamSet = SET_GROUP:New():FilterPrefixes("Blue SAM"):FilterCoalitions("blue"):FilterStart()
  -- myshorad = SHORAD:New("BlueShorad", "Blue SHORAD", SamSet, 22000, 600, "blue")
  -- myshorad:SwitchDebug(true)
  -- mymantis = MANTIS:New("BlueMantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")
  -- mymantis:AddShorad(myshorad,720)
  -- mymantis:Start()
  function SHORAD:onafterWakeUpShorad(From, Event, To, TargetGroup, Radius, ActiveTimer, TargetCat, ShotAt)
    self:T(self.lid .. " WakeUpShorad")
    self:T({TargetGroup, Radius, ActiveTimer, TargetCat})
    local targetcat = TargetCat or Object.Category.UNIT
    local targetgroup = TargetGroup
    local targetvec2 = nil
    if targetcat == Object.Category.UNIT then
      targetvec2 = GROUP:FindByName(targetgroup):GetVec2()
    elseif targetcat == Object.Category.STATIC then
      targetvec2 = STATIC:FindByName(targetgroup,false):GetVec2()
    else
      local samset = self.Samset
      local sam = samset:GetRandom()
      targetvec2 = sam:GetVec2()
    end
    local targetzone = ZONE_RADIUS:New("Shorad",targetvec2,Radius)  -- create a defense zone to check
    local groupset = self.Groupset --Core.Set#SET_GROUP
    local shoradset = groupset:GetAliveSet() --#table
    
    -- local function to switch off shorad again
    local function SleepShorad(group)
      if group and group:IsAlive() then
        local groupname = group:GetName()
        self.ActiveGroups[groupname] = nil
        if self.UseEmOnOff then
          group:EnableEmission(false)
        else
          group:OptionAlarmStateGreen()
        end
        local text = string.format("Sleeping SHORAD %s", group:GetName())
        self:T(text)
        local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug)
        --Shoot and Scoot
        if self.shootandscoot then
          self:__ShootAndScoot(1,group)
        end
      end
    end
    
    -- go through set and find the one(s) to activate
    local TDiff = 4
    for _,_group in pairs (shoradset) do
      
      local groupname = _group:GetName()
      
      if groupname == TargetGroup and ShotAt==true then
        -- Shot at a SHORAD group
        if self.UseEmOnOff then
          _group:EnableEmission(false)
        end
        _group:OptionAlarmStateGreen()
        self.ActiveGroups[groupname] = nil
        local text = string.format("Shot at SHORAD %s! Evading!", _group:GetName())
        self:T(text)
        local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug)
        
        --Shoot and Scoot
        if self.shootandscoot then
          self:__ShootAndScoot(1,_group)
        end
        
      elseif _group:IsAnyInZone(targetzone) or groupname == TargetGroup then
        -- shot at a group we protect
        local text = string.format("Waking up SHORAD %s", _group:GetName())
        self:T(text)
        local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug)
        if self.UseEmOnOff then
          _group:EnableEmission(true)
        end
        _group:OptionAlarmStateRed()
        if self.ActiveGroups[groupname] == nil then -- no timer yet for this group
          self.ActiveGroups[groupname] = { Timing = ActiveTimer }
          local endtime = timer.getTime() + (ActiveTimer * math.random(75,100) / 100 ) -- randomize wakeup a bit
          self.ActiveGroups[groupname].Timer = TIMER:New(SleepShorad,_group):Start(endtime)
          --Shoot and Scoot
          if self.shootandscoot then
            self:__ShootAndScoot(TDiff,_group)
            TDiff=TDiff+1
          end
        end
      end
    end
    return self
  end
  
  --- (Internal) Calculate hit zone of an AGM-88
  -- @param #SHORAD self
  -- @param #table SEADWeapon DCS.Weapon object
  -- @param Core.Point#COORDINATE pos0 Position of the plane when it fired
  -- @param #number height Height when the missile was fired
  -- @param Wrapper.Group#GROUP SEADGroup Attacker group
  -- @return #SHORAD self 
  function SHORAD:onafterCalculateHitZone(From,Event,To,SEADWeapon,pos0,height,SEADGroup)
    self:T("**** Calculating hit zone")
    if SEADWeapon and SEADWeapon:isExist() then
      --local pos = SEADWeapon:getPoint()
      
      -- postion and height
      local position = SEADWeapon:getPosition()
      local mheight = height
      -- heading
      local wph = math.atan2(position.x.z, position.x.x)      
      if wph < 0 then
        wph=wph+2*math.pi
      end   
      wph=math.deg(wph)
      
      -- velocity
      local wpndata = SEAD.HarmData["AGM_88"]
      local mveloc = math.floor(wpndata[2] * 340.29)
      local c1 = (2*mheight*9.81)/(mveloc^2)
      local c2 = (mveloc^2) / 9.81
      local Ropt = c2 * math.sqrt(c1+1) 
      if height <= 5000 then
        Ropt = Ropt * 0.72
      elseif height <= 7500 then
        Ropt = Ropt * 0.82  
      elseif height <= 10000 then
        Ropt = Ropt * 0.87
      elseif height <= 12500 then
        Ropt = Ropt * 0.98
      end
      
      -- look at a couple of zones across the trajectory
      for n=1,3 do
        local dist = Ropt - ((n-1)*20000)
        local predpos= pos0:Translate(dist,wph)
        if predpos then
    
          local targetzone = ZONE_RADIUS:New("Target Zone",predpos:GetVec2(),20000)
          
          if self.debug then
            predpos:MarkToAll(string.format("height=%dm | heading=%d | velocity=%ddeg | Ropt=%dm",mheight,wph,mveloc,Ropt),false)
            targetzone:DrawZone(coalition.side.BLUE,{0,0,1},0.2,nil,nil,3,true)
          end  
          
          local seadset = self.Groupset
          local tgtcoord = targetzone:GetRandomPointVec2()
          local tgtgrp = seadset:FindNearestGroupFromPointVec2(tgtcoord)
          local _targetgroup = nil
          local _targetgroupname = "none"
          local _targetskill = "Random"
          if tgtgrp and tgtgrp:IsAlive() then
            _targetgroup = tgtgrp
            _targetgroupname = tgtgrp:GetName() -- group name
            _targetskill = tgtgrp:GetUnit(1):GetSkill()
            self:T("*** Found Target = ".. _targetgroupname)
            self:WakeUpShorad(_targetgroupname, self.Radius, self.ActiveTimer, Object.Category.UNIT,true)
          end
        end
      end     
    end
    return self
  end
  
  --- (Internal) Shoot and Scoot
  -- @param #SHORAD self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Wrapper.Group#GROUP Shorad Shorad group
  -- @return #SHORAD self 
  function SHORAD:onafterShootAndScoot(From,Event,To,Shorad)
    self:T( { From,Event,To } )
    local possibleZones = {}
    local mindist = self.minscootdist or 100
    local maxdist = self.maxscootdist or 3000
    if Shorad and Shorad:IsAlive() then
      local NowCoord = Shorad:GetCoordinate()
      for _,_zone in pairs(self.SkateZones.Set) do
        local zone = _zone -- Core.Zone#ZONE_RADIUS
        local dist = NowCoord:Get2DDistance(zone:GetCoordinate())
        if dist >= mindist and dist <= maxdist then
          possibleZones[#possibleZones+1] = zone
          if #possibleZones == self.SkateNumber then break end
        end
      end
      if #possibleZones > 0 and Shorad:GetVelocityKMH() < 2 then
        local rand = math.floor(math.random(1,#possibleZones*1000)/1000+0.5)
        if rand == 0 then rand = 1 end
        self:T(self.lid .. " ShootAndScoot to zone "..rand)
        local ToCoordinate = possibleZones[rand]:GetCoordinate()
        if self.scootrandomcoord then
          ToCoordinate = possibleZones[rand]:GetRandomCoordinate(nil,nil,{land.SurfaceType.LAND,land.SurfaceType.ROAD})         
        end
        local formation = self.scootformation or "Cone"
        Shorad:RouteGroundTo(ToCoordinate,20,formation,1)
      end
    end
    return self
  end
  
  --- Main function - work on the EventData
  -- @param #SHORAD self
  -- @param Core.Event#EVENTDATA EventData The event details table data set
  -- @return #SHORAD self 
  function SHORAD:HandleEventShot( EventData )
    self:T( { EventData } )
    self:T(self.lid .. " HandleEventShot")
    local ShootingWeapon = EventData.Weapon -- Identify the weapon fired
    local ShootingWeaponName = EventData.WeaponName -- return weapon type
    -- get firing coalition
    local weaponcoalition = EventData.IniGroup:GetCoalition()
    -- get detection probability
    if self:_CheckCoalition(weaponcoalition) then --avoid overhead on friendly fire
      local IsDetected = self:_ShotIsDetected()
      -- convert to text
      local DetectedText = "false"
      if IsDetected then 
        DetectedText = "true"
      end
      local text = string.format("%s Missile Launched = %s | Detected probability state is %s", self.lid, ShootingWeaponName, DetectedText)
      self:T( text )
      local m = MESSAGE:New(text,10,"Info"):ToAllIf(self.debug)
      --
      if (self:_CheckHarms(ShootingWeaponName) or self:_CheckMavs(ShootingWeaponName)) and IsDetected then
        -- get target data
        local targetdata = EventData.Weapon:getTarget() -- Identify target
        -- Is there target data?
        if not targetdata or self.debug then 
          if string.find(ShootingWeaponName,"AGM_88",1,true) then
            self:I("**** Tracking AGM-88 with no target data.")
            local pos0 = EventData.IniUnit:GetCoordinate()
            local fheight = EventData.IniUnit:GetHeight()
            self:__CalculateHitZone(20,ShootingWeapon,pos0,fheight,EventData.IniGroup)
          end  
          return self
        end
        
        local targetcat = Object.getCategory(targetdata) -- Identify category
        self:T(string.format("Target Category (3=STATIC, 1=UNIT)= %s",tostring(targetcat)))
        self:T({targetdata})
        local targetunit = nil
        if targetcat == Object.Category.UNIT then -- UNIT
          targetunit = UNIT:Find(targetdata)
        elseif targetcat == Object.Category.STATIC then  -- STATIC
          local tgtcoord = COORDINATE:NewFromVec3(targetdata:getPoint())
         local tgtgrp1 = self.Samset:FindNearestGroupFromPointVec2(tgtcoord)
          local tgtcoord1 = tgtgrp1:GetCoordinate()
          local tgtgrp2 = self.Groupset:FindNearestGroupFromPointVec2(tgtcoord)
          local tgtcoord2 = tgtgrp2:GetCoordinate()
          local dist1 = tgtcoord:Get2DDistance(tgtcoord1)
          local dist2 = tgtcoord:Get2DDistance(tgtcoord2)
          
          if dist1 < dist2 then
            targetunit = tgtgrp1
            targetcat = Object.Category.UNIT
          else
            targetunit = tgtgrp2
            targetcat = Object.Category.UNIT
          end
        end   
        if targetunit and targetunit:IsAlive() then
          local targetunitname = targetunit:GetName()
          local targetgroup = nil
          local targetgroupname = "none"
          if targetcat == Object.Category.UNIT then
            if targetunit.ClassName == "UNIT" then
              targetgroup = targetunit:GetGroup()
            elseif targetunit.ClassName == "GROUP" then
              targetgroup = targetunit
            end
            targetgroupname = targetgroup:GetName() -- group name
          elseif targetcat == Object.Category.STATIC then
            targetgroup = targetunit
            targetgroupname = targetunitname
          end
          local text = string.format("%s Missile Target = %s", self.lid, tostring(targetgroupname))
          self:T( text )
          local m = MESSAGE:New(text,10,"Info"):ToAllIf(self.debug)
          -- check if we or a SAM site are the target 
          local shotatus = self:_CheckShotAtShorad(targetgroupname) --#boolean
          local shotatsams = self:_CheckShotAtSams(targetgroupname) --#boolean
          -- if being shot at, find closest SHORADs to activate
          if shotatsams or shotatus then
            self:T({shotatsams=shotatsams,shotatus=shotatus})
            self:WakeUpShorad(targetgroupname, self.Radius, self.ActiveTimer, targetcat, true)
          end
        end  
      end
    end
    return self
  end 
--
end
-----------------------------------------------------------------------
-- SHORAD end
-----------------------------------------------------------------------
--- **Functional** - AI CSAR system.
-- 
-- ===
--
-- ## Features:
--
--    * Send out helicopters to downed pilots
--    * Rescues players and AI alike
--    * Coalition specific
--    * Starting from a FARP or Airbase
--    * Dedicated MASH zone
--    * Some FSM functions to include in your mission scripts
--    * Limit number of available helos
--    * SRS voice output via TTS or soundfiles
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Functional/AICSAR).
--
-- ===
-- 
-- ### Author: **Applevangelist**
-- Last Update July 2025
--
-- ===
-- @module Functional.AICSAR
-- @image MOOSE.JPG



--- AI CSAR class.
-- @type AICSAR
-- @field #string ClassName Name of this class.
-- @field #string version Versioning.
-- @field #string lid LID for log entries.
-- @field #number coalition Colition side.
-- @field #string template Template for pilot.
-- @field #string helotemplate Template for CSAR helo.
-- @field #string alias Alias Name.
-- @field Wrapper.Airbase#AIRBASE farp FARP object from where to start.
-- @field Core.Zone#ZONE farpzone MASH zone to drop rescued pilots.
-- @field #number maxdistance Max distance to go for a rescue.
-- @field #table pilotqueue Queue of pilots to rescue.
-- @field #number pilotindex Table index to bind pilot to helo.
-- @field #table helos Table of Ops.FlightGroup#FLIGHTGROUP objects
-- @field #boolean verbose Switch more output.
-- @field #number rescuezoneradius Radius around downed pilot for the helo to land in.
-- @field #table rescued Track number of rescued pilot.
-- @field #boolean autoonoff Only send a helo when no human heli pilots are available.
-- @field Core.Set#SET_CLIENT playerset Track if alive heli pilots are available.
-- @field #boolean limithelos limit available number of helos going on mission (defaults to true)
-- @field #number helonumber number of helos available (default: 3)
-- @field Utilities.FiFo#FIFO PilotStore
-- @field #number Altitude Default altitude setting for the helicopter FLIGHTGROUP 1500ft.
-- @field #number Speed Default speed setting for the helicopter FLIGHTGROUP is 100kn.
-- @field #boolean UseEventEject In case Event LandingAfterEjection isn't working, use set this to true.
-- @field #number Delay In case of UseEventEject wait this long until we spawn a landed pilot.
-- @field #boolean UseRescueZone If true, use a rescue zone and not the max distance to FARP/MASH
-- @field Core.Zone#ZONE_RADIUS RescueZone Use this zone as operational area for the AICSAR instance.
-- @extends Core.Fsm#FSM


--- *I once donated a pint of my finest red corpuscles to the great American Red Cross and the doctor opined my blood was very helpful; contained so much alcohol they could use it to sterilize their instruments.*    
--  W.C.Fields
--
-- ===
--
-- # AICSAR Concept
-- 
-- For an AI or human pilot landing with a parachute, a rescue mission will be spawned. The helicopter will fly to the pilot, pick him or her up,
-- and fly back to a designated MASH (medical) zone, drop the pilot and then return to base.
-- Operational maxdistance can be set as well as the landing radius around the downed pilot.
-- Keep in mind that AI helicopters cannot hover-load at the time of writing, so rescue operations over water or in the mountains might not
-- work.
-- Optionally, if you have a CSAR operation with human pilots in your mission, you can set AICSAR to ignore missions when human helicopter
-- pilots are around.
--
-- ## Setup
-- 
-- Setup is a one-liner:
--            
--            -- @param #string Alias Name of this instance.
--            -- @param #number Coalition Coalition as in coalition.side.BLUE, can also be passed as "blue", "red" or "neutral"
--            -- @param #string Pilottemplate Pilot template name.
--            -- @param #string Helotemplate Helicopter template name. Set the template to "cold start". Hueys work best.
--            -- @param Wrapper.Airbase#AIRBASE FARP FARP object or Airbase from where to start.
--            -- @param Core.Zone#ZONE MASHZone Zone where to drop pilots after rescue.
--            local my_aicsar=AICSAR:New("Luftrettung",coalition.side.BLUE,"Downed Pilot","Rescue Helo",AIRBASE:FindByName("Test FARP"),ZONE:New("MASH"))
--
-- ## Options are
--  
--            my_aicsar.maxdistance -- maximum operational distance in meters. Defaults to 50NM or 92.6km
--            my_aicsar.rescuezoneradius -- landing zone around downed pilot. Defaults to 200m
--            my_aicsar.autoonoff -- stop operations when human helicopter pilots are around. Defaults to true.
--            my_aicsar.verbose -- text messages to own coalition about ongoing operations. Defaults to true.
--            my_aicsar.limithelos -- limit available number of helos going on mission (defaults to true)
--            my_aicsar.helonumber -- number of helos available (default: 3)
--            my_aicsar.verbose -- boolean, set to `true`for message output on-screen
-- 
-- ## Radio output options
-- 
-- Radio messages, soundfile names and (for SRS) lengths are defined in three enumerators, so you can customize, localize messages and soundfiles to your liking:
-- 
-- Defaults are:
-- 
--            AICSAR.Messages = {
--              EN = {
--              INITIALOK = "Roger, Pilot, we hear you. Stay where you are, a helo is on the way!",
--              INITIALNOTOK = "Sorry, Pilot. You're behind maximum operational distance! Good Luck!",
--              PILOTDOWN = "Mayday, mayday, mayday! Pilot down at ", -- note that this will be appended with the position in MGRS
--              PILOTKIA = "Pilot KIA!",
--              HELODOWN = "CSAR Helo Down!",
--              PILOTRESCUED = "Pilot rescued!",
--              PILOTINHELO = "Pilot picked up!",
--              },
--            }
-- 
-- Correspondingly, sound file names are defined as these defaults:
-- 
--            AICSAR.RadioMessages = {
--              EN = {
--              INITIALOK = "initialok.ogg",
--              INITIALNOTOK = "initialnotok.ogg",
--              PILOTDOWN = "pilotdown.ogg",
--              PILOTKIA = "pilotkia.ogg", 
--              HELODOWN = "helodown.ogg", 
--              PILOTRESCUED = "pilotrescued.ogg", 
--              PILOTINHELO = "pilotinhelo.ogg",
--              }, 
--            }
-- 
-- and these default transmission lengths in seconds:
-- 
--            AICSAR.RadioLength = {
--              EN = {
--              INITIALOK = 4.1,
--              INITIALNOTOK = 4.6, 
--              PILOTDOWN = 2.6,
--              PILOTKIA = 1.1,
--              HELODOWN = 2.1,
--              PILOTRESCUED = 3.5,
--              PILOTINHELO = 2.6,
--              },
--            }
--
-- ## Radio output via SRS and Text-To-Speech (TTS)
-- 
-- Radio output can be done via SRS and Text-To-Speech. No extra sound files required! 
-- [Initially, Have a look at the guide on setting up SRS TTS for Moose](https://github.com/FlightControl-Master/MOOSE_GUIDES/blob/master/documents/Moose%20TTS%20Setup%20Guide.pdf).
-- The text from the `AICSAR.Messages` table above is converted on the fly to an .ogg-file, which is then played back via SRS on the selected frequency and mdulation.
-- Hint - the small black window popping up shortly is visible in Single-Player only. 
-- 
-- To set up AICSAR for SRS TTS output, add e.g. the following to your script:
--              
--              -- setup for google TTS, radio 243 AM, SRS server port 5002 with a google standard-quality voice (google cloud account required)
--              my_aicsar:SetSRSTTSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio",243,radio.modulation.AM,5002,MSRS.Voices.Google.Standard.en_US_Standard_D,"en-US","female","C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio\\google.json")
--              
--              -- alternatively for MS Desktop TTS (voices need to be installed locally first!)
--              my_aicsar:SetSRSTTSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio",243,radio.modulation.AM,5002,MSRS.Voices.Microsoft.Hazel,"en-GB","female")
--              
--              -- define a different voice for the downed pilot(s)
--              my_aicsar:SetPilotTTSVoice(MSRS.Voices.Google.Standard.en_AU_Standard_D,"en-AU","male")
--              
--              -- define another voice for the operator
--              my_aicsar:SetOperatorTTSVoice(MSRS.Voices.Google.Standard.en_GB_Standard_A,"en-GB","female")
--
-- ## Radio output via preproduced soundfiles
--
-- The easiest way to add a soundfile to your mission is to use the "Sound to..." trigger in the mission editor. This will effectively 
-- save your sound file inside of the .miz mission file. [Example soundfiles are located on github](https://github.com/FlightControl-Master/MOOSE_SOUND/tree/master/AICSAR)
-- 
-- To customize or localize your texts and sounds, you can take e.g. the following approach to add a German language version:
-- 
--           -- parameters are: locale, ID, text, soundfilename, duration
--           my_aicsar.gettext:AddEntry("de","INITIALOK","Copy, Pilot, wir hören Sie. Bleiben Sie, wo Sie sind, ein Hubschrauber sammelt Sie auf!","okneu.ogg",5.0)
--           my_aicsar.locale = "de" -- plays and shows the defined German language texts and sound. Fallback is "en", if something is undefined.
--           
-- Switch on radio transmissions via **either** SRS **or** "normal" DCS radio e.g. like so:
-- 
--          my_aicsar:SetSRSRadio(true,"C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio",270,radio.modulation.AM,nil,5002)
--         
-- or         
--          
--          my_aicsar:SetDCSRadio(true,300,radio.modulation.AM,GROUP:FindByName("FARP-Radio"))
-- 
-- See the function documentation for parameter details.
--                    
-- ===
---
--
-- @field #AICSAR
AICSAR = {
  ClassName = "AICSAR",
  version = "0.1.18",
  lid = "",
  coalition = coalition.side.BLUE,
  template = "",
  helotemplate = "",
  alias = "",
  farp = nil,
  farpzone = nil,
  maxdistance = UTILS.NMToMeters(50),
  pilotqueue = {},
  pilotindex = 0,
  helos = {},
  verbose = false,
  rescuezoneradius = 200,
  rescued = {},
  autoonoff = true,
  playerset = nil,
  Messages = {},
  SRS = nil,
  SRSRadio = false,
  SRSFrequency = 243,
  SRSPath = "\\",
  SRSModulation = radio.modulation.AM,
  SRSSoundPath = nil, -- defaults to "l10n/DEFAULT/", i.e. add messages via "Sount to..." in the ME
  SRSPort = 5002,
  DCSRadio = false,
  DCSFrequency = 243,
  DCSModulation = radio.modulation.AM,
  DCSRadioGroup = nil,
  limithelos = true,
  helonumber = 3,
  gettext = nil,
  locale ="en", -- default text language
  SRSTTSRadio = false,
  SRSGoogle = false,
  SRSQ = nil,
  SRSPilot = nil,
  SRSPilotVoice = false,
  SRSOperator = nil,
  SRSOperatorVoice = false,
  PilotStore = nil,
  Speed = 100,
  Altitude = 1500,
  UseEventEject = false,
  Delay = 100,
  UseRescueZone = false,
  RescueZone = nil,
}

-- TODO Messages
--- Messages enum
-- @field Messages
AICSAR.Messages = {
  EN = {
  INITIALOK = "Roger, Pilot, we hear you. Stay where you are, a helo is on the way!",
  INITIALNOTOK = "Sorry, Pilot. You're behind maximum operational distance! Good Luck!",
  PILOTDOWN = "Mayday, mayday, mayday! Pilot down at ",
  PILOTKIA = "Pilot KIA!",
  HELODOWN = "CSAR Helo Down!",
  PILOTRESCUED = "Pilot rescued!",
  PILOTINHELO = "Pilot picked up!",
  },
  DE = {
  INITIALOK = "Copy, Pilot, wir hören Sie. Bleiben Sie, wo Sie sind!\nEin Hubschrauber sammelt Sie auf!",
  INITIALNOTOK = "Verstehe, Pilot. Sie sind zu weit weg von uns.\nViel Glück!",
  PILOTDOWN = "Mayday, mayday, mayday! Pilot abgestürzt: ",
  PILOTKIA = "Pilot gefallen!",
  HELODOWN = "CSAR Hubschrauber verloren!",
  PILOTRESCUED = "Pilot gerettet!",
  PILOTINHELO = "Pilot an Bord geholt!",
  },
}

-- TODO Radio Messages
--- Radio Messages enum for ogg files
-- @field RadioMessages
AICSAR.RadioMessages = {
  EN = {
  INITIALOK = "initialok.ogg", -- 4.1 secs
  INITIALNOTOK = "initialnotok.ogg", -- 4.6 secs
  PILOTDOWN = "pilotdown.ogg", -- 2.6 secs
  PILOTKIA = "pilotkia.ogg", -- 1.1 sec
  HELODOWN = "helodown.ogg", -- 2.1 secs
  PILOTRESCUED = "pilotrescued.ogg", -- 3.5 secs
  PILOTINHELO = "pilotinhelo.ogg", -- 2.6 secs
  },
}

-- TODO Radio Messages
--- Radio Messages enum for ogg files length in secs
-- @field RadioLength
AICSAR.RadioLength = {
  EN = {
  INITIALOK = 4.1,
  INITIALNOTOK = 4.6, 
  PILOTDOWN = 2.6,
  PILOTKIA = 1.1,
  HELODOWN = 2.1,
  PILOTRESCUED = 3.5,
  PILOTINHELO = 2.6,
  },
}

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function to create a new AICSAR object
-- @param #AICSAR self
-- @param #string Alias Name of this instance.
-- @param #number Coalition Coalition as in coalition.side.BLUE, can also be passed as "blue", "red" or "neutral"
-- @param #string Pilottemplate Pilot template name.
-- @param #string Helotemplate Helicopter template name.
-- @param Wrapper.Airbase#AIRBASE FARP FARP object or Airbase from where to start.
-- @param Core.Zone#ZONE MASHZone Zone where to drop pilots after rescue.
-- @param #number Helonumber Max number of alive Ai Helos at the same time. Defaults to three.
-- @return #AICSAR self
function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone,Helonumber)
  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New())
  
    --set Coalition
  if Coalition and type(Coalition)=="string" then
    if Coalition=="blue" then
      self.coalition=coalition.side.BLUE
      self.coalitiontxt = Coalition
    elseif Coalition=="red" then
      self.coalition=coalition.side.RED
      self.coalitiontxt = Coalition
    elseif Coalition=="neutral" then
      self.coalition=coalition.side.NEUTRAL
      self.coalitiontxt = Coalition
    else
      self:E("ERROR: Unknown coalition in AICSAR!")
    end
  else
    self.coalition = Coalition
    self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition))
  end
  
    -- Set alias.
  if Alias then
    self.alias=tostring(Alias)
  else
    self.alias="Red Cross"  
    if self.coalition then
      if self.coalition==coalition.side.RED then
        self.alias="IFRC"
      elseif self.coalition==coalition.side.BLUE then
        self.alias="CSAR"
      end
    end
  end
  
  self.template = Pilottemplate
  self.helotemplate = Helotemplate
  self.farp = FARP
  self.farpzone = MASHZone
  self.playerset = SET_CLIENT:New():FilterActive(true):FilterCategories("helicopter"):FilterStart()
  self.UseEventEject = false
  self.Delay = 300
  
  -- Radio
  self.SRS = nil
  self.SRSRadio = false
  self.SRSTTSRadio = false
  self.SRSGoogle = false
  self.SRSQ = nil
  self.SRSFrequency = 243
  self.SRSPath = "\\"
  self.SRSModulation = radio.modulation.AM
  self.SRSSoundPath = nil -- defaults to "l10n/DEFAULT/", i.e. add messages via "Sound to..." in the ME
  self.SRSPort = 5002
  
  -- DCS Radio - add messages via "Sound to..." in the ME
  self.DCSRadio = false
  self.DCSFrequency = 243
  self.DCSModulation = radio.modulation.AM
  self.DCSRadioGroup = nil
  self.DCSRadioQueue = nil
  
  self.MGRS_Accuracy = 2
  
  -- limit number of available helos at the same time
  self.limithelos = true
  self.helonumber = Helonumber or 3

  -- localization
  self:InitLocalization()
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown")
  
  --Pilot Store
  self.PilotStore = FIFO:New()
  
  -- Start State.
  self:SetStartState("Stopped")
  
    -- Add FSM transitions.
  --                 From State  -->   Event        -->     To State
  self:AddTransition("Stopped",       "Start",              "Running")     -- Start FSM.
  self:AddTransition("*",             "Status",             "*")           -- CSAR status update.
  self:AddTransition("*",             "PilotDown",          "*")           -- Pilot down
  self:AddTransition("*",             "PilotPickedUp",      "*")           -- Pilot in helo
  self:AddTransition("*",             "PilotUnloaded",      "*")           -- Pilot Unloaded from helo
  self:AddTransition("*",             "PilotRescued",       "*")           -- Pilot Rescued
  self:AddTransition("*",             "PilotKIA",           "*")           -- Pilot dead
  self:AddTransition("*",             "HeloDown",           "*")           -- Helo dead
  self:AddTransition("*",             "HeloOnDuty",         "*")           -- Helo spawnd
  self:AddTransition("*",             "Stop",               "Stopped")     -- Stop FSM.
  
  self:HandleEvent(EVENTS.LandingAfterEjection,self._EventHandler)
  self:HandleEvent(EVENTS.Ejection,self._EjectEventHandler)
  
  self:__Start(math.random(2,5))
  
  local text = string.format("%sAICSAR Version %s Starting",self.lid,self.version)
  
  self:I(text)
  
  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Status".
  -- @function [parent=#AICSAR] Status
  -- @param #AICSAR self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#AICSAR] __Status
  -- @param #AICSAR self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop".
  -- @function [parent=#AICSAR] Stop
  -- @param #AICSAR self

  --- Triggers the FSM event "Stop" after a delay.
  -- @function [parent=#AICSAR] __Stop
  -- @param #AICSAR self
  -- @param #number delay Delay in seconds.
  
  --- On after "PilotDown" event.
  -- @function [parent=#AICSAR] OnAfterPilotDown
  -- @param #AICSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Point#COORDINATE Coordinate Location of the pilot.
  -- @param #boolean InReach True if in maxdistance else false. 
  
  --- On after "PilotPickedUp" event.
  -- @function [parent=#AICSAR] OnAfterPilotPickedUp
  -- @param #AICSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.FlightGroup#FLIGHTGROUP Helo
  -- @param #table CargoTable of Ops.OpsGroup#OPSGROUP Cargo objects
  -- @param #number Index  
  
  --- On after "PilotRescued" event.
  -- @function [parent=#AICSAR] OnAfterPilotRescued
  -- @param #AICSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string PilotName 

  --- On after "PilotUnloaded" event.
  -- @function [parent=#AICSAR] OnAfterPilotUnloaded
  -- @param #AICSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.FlightGroup#FLIGHTGROUP Helo
  -- @param Ops.OpsGroup#OPSGROUP OpsGroup  

  --- On after "PilotKIA" event.
  -- @function [parent=#AICSAR] OnAfterPilotKIA
  -- @param #AICSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state. 

  --- On after "HeloOnDuty" event.
  -- @function [parent=#AICSAR] OnAfterHeloOnDuty
  -- @param #AICSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Group#GROUP Helo Helo group object

  --- On after "HeloDown" event.
  -- @function [parent=#AICSAR] OnAfterHeloDown
  -- @param #AICSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.FlightGroup#FLIGHTGROUP Helo
  -- @param #number Index
  
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- [Internal] Create the Moose TextAndSoundEntries
-- @param #AICSAR self
-- @return #AICSAR self
function AICSAR:InitLocalization()
  self:T(self.lid .. "InitLocalization")
  -- English standard localization
  self.gettext=TEXTANDSOUND:New(self.ClassName, "en")
  self.gettext:AddEntry("en","INITIALOK",AICSAR.Messages.EN.INITIALOK,AICSAR.RadioMessages.EN.INITIALOK,AICSAR.RadioLength.INITIALOK)
  self.gettext:AddEntry("en","INITIALNOTOK",AICSAR.Messages.EN.INITIALNOTOK,AICSAR.RadioMessages.EN.INITIALNOTOK,AICSAR.RadioLength.EN.INITIALNOTOK)
  self.gettext:AddEntry("en","HELODOWN",AICSAR.Messages.EN.HELODOWN,AICSAR.RadioMessages.EN.HELODOWN,AICSAR.RadioLength.EN.HELODOWN)
  self.gettext:AddEntry("en","PILOTDOWN",AICSAR.Messages.EN.PILOTDOWN,AICSAR.RadioMessages.EN.PILOTDOWN,AICSAR.RadioLength.EN.PILOTDOWN)
  self.gettext:AddEntry("en","PILOTINHELO",AICSAR.Messages.EN.PILOTINHELO,AICSAR.RadioMessages.EN.PILOTINHELO,AICSAR.RadioLength.EN.PILOTINHELO)
  self.gettext:AddEntry("en","PILOTKIA",AICSAR.Messages.EN.PILOTKIA,AICSAR.RadioMessages.EN.PILOTKIA,AICSAR.RadioLength.EN.PILOTKIA)
  self.gettext:AddEntry("en","PILOTRESCUED",AICSAR.Messages.EN.PILOTRESCUED,AICSAR.RadioMessages.EN.PILOTRESCUED,AICSAR.RadioLength.EN.PILOTRESCUED)
  -- German localization - we keep the sound files English
  self.gettext:AddEntry("de","INITIALOK",AICSAR.Messages.DE.INITIALOK,AICSAR.RadioMessages.EN.INITIALOK,AICSAR.RadioLength.INITIALOK)
  self.gettext:AddEntry("de","INITIALNOTOK",AICSAR.Messages.DE.INITIALNOTOK,AICSAR.RadioMessages.EN.INITIALNOTOK,AICSAR.RadioLength.EN.INITIALNOTOK)
  self.gettext:AddEntry("de","HELODOWN",AICSAR.Messages.DE.HELODOWN,AICSAR.RadioMessages.EN.HELODOWN,AICSAR.RadioLength.EN.HELODOWN)
  self.gettext:AddEntry("de","PILOTDOWN",AICSAR.Messages.DE.PILOTDOWN,AICSAR.RadioMessages.EN.PILOTDOWN,AICSAR.RadioLength.EN.PILOTDOWN)
  self.gettext:AddEntry("de","PILOTINHELO",AICSAR.Messages.DE.PILOTINHELO,AICSAR.RadioMessages.EN.PILOTINHELO,AICSAR.RadioLength.EN.PILOTINHELO)
  self.gettext:AddEntry("de","PILOTKIA",AICSAR.Messages.DE.PILOTKIA,AICSAR.RadioMessages.EN.PILOTKIA,AICSAR.RadioLength.EN.PILOTKIA)
  self.gettext:AddEntry("de","PILOTRESCUED",AICSAR.Messages.DE.PILOTRESCUED,AICSAR.RadioMessages.EN.PILOTRESCUED,AICSAR.RadioLength.EN.PILOTRESCUED)
  self.locale = "en"
  return self
end

--- [User] Use a defined zone as area of operation and not the distance to FARP.
-- @param #AICSAR self
-- @param Core.Zone#ZONE Zone The operational zone to use. Downed pilots in this area will be rescued. Can be any known #ZONE type.
-- @return #AICSAR self 
function AICSAR:SetUsingRescueZone(Zone)
  self.UseRescueZone = true
  self.RescueZone = Zone
  return self
end

--- [User] Switch sound output on and use SRS output for sound files.
-- @param #AICSAR self
-- @param #boolean OnOff Switch on (true) or off (false).
-- @param #string Path Path to your SRS Server External Audio Component, e.g. "C:\\\\Program Files\\\\DCS-SimpleRadio-Standalone\\\\ExternalAudio"
-- @param #number Frequency Defaults to 243 (guard)
-- @param #number Modulation Radio modulation. Defaults to radio.modulation.AM
-- @param #string SoundPath Where to find the audio files. Defaults to nil, i.e. add messages via "Sound to..." in the Mission Editor.
-- @param #number Port Port of the SRS, defaults to 5002.
-- @return #AICSAR self
function AICSAR:SetSRSRadio(OnOff,Path,Frequency,Modulation,SoundPath,Port)
  self:T(self.lid .. "SetSRSRadio")
  self.SRSRadio = OnOff and true
  self.SRSTTSRadio = false
  self.SRSFrequency = Frequency or 243
  self.SRSPath = Path or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio"
  self.SRS:SetLabel("ACSR")
  self.SRS:SetCoalition(self.coalition)
  self.SRSModulation = Modulation or radio.modulation.AM
  local soundpath = os.getenv('TMP') .. "\\DCS\\Mission\\l10n\\DEFAULT" -- defaults to "l10n/DEFAULT/", i.e. add messages by "Sound to..." in the ME
  self.SRSSoundPath = SoundPath or soundpath
  self.SRSPort = Port or MSRS.port or 5002
  if OnOff then
    self.SRS = MSRS:New(Path,Frequency,Modulation)
    self.SRS:SetPort(self.SRSPort)
  end
  return self
end

--- [User] Switch sound output on and use SRS-TTS output. The voice will be used across all outputs, unless you define an extra voice for downed pilots and/or the operator.
-- See `AICSAR:SetPilotTTSVoice()` and `AICSAR:SetOperatorTTSVoice()`
-- @param #AICSAR self
-- @param #boolean OnOff Switch on (true) or off (false).
-- @param #string Path Path to your SRS Server Component, e.g. "E:\\\\Program Files\\\\DCS-SimpleRadio-Standalone\\ExternalAudio"
-- @param #number Frequency (Optional) Defaults to 243 (guard)
-- @param #number Modulation (Optional) Radio modulation. Defaults to radio.modulation.AM
-- @param #number Port (Optional) Port of the SRS, defaults to 5002.
-- @param #string Voice (Optional) The voice to be used.
-- @param #string Culture (Optional) The culture to be used, defaults to "en-GB"
-- @param #string Gender (Optional)  The gender to be used, defaults to "male"
-- @param #string GoogleCredentials (Optional) Path to google credentials
-- @return #AICSAR self
function AICSAR:SetSRSTTSRadio(OnOff,Path,Frequency,Modulation,Port,Voice,Culture,Gender,GoogleCredentials)
  self:T(self.lid .. "SetSRSTTSRadio")
  self.SRSTTSRadio = OnOff and true
  self.SRSRadio = false
  self.SRSFrequency = Frequency or 243
  self.SRSPath = Path or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio"
  self.SRSModulation = Modulation or radio.modulation.AM
  self.SRSPort = Port or MSRS.port or 5002
  if OnOff then
    self.SRS = MSRS:New(self.SRSPath,Frequency,Modulation)
    self.SRS:SetPort(self.SRSPort)
    self.SRS:SetCoalition(self.coalition)
    self.SRS:SetLabel("ACSR")
    self.SRS:SetVoice(Voice)
    self.SRS:SetCulture(Culture)
    self.SRS:SetGender(Gender)
    if GoogleCredentials then
      self.SRS:SetProviderOptionsGoogle(GoogleCredentials,GoogleCredentials)
      self.SRS:SetProvider(MSRS.Provider.GOOGLE)
      self.SRSGoogle = true
    end
    self.SRSQ = MSRSQUEUE:New(self.alias)
  end
  return self
end

--- [User] Set SRS TTS Voice of downed pilot. `AICSAR:SetSRSTTSRadio()` needs to be set first!
-- @param #AICSAR self
-- @param #string Voice The voice to be used, e.g. `MSRS.Voices.Google.Standard.en_US_Standard_J` for Google or `MSRS.Voices.Microsoft.David` for Microsoft. 
-- Specific voices override culture and gender!
-- @param #string Culture (Optional) The culture to be used, defaults to "en-US"
-- @param #string Gender (Optional)  The gender to be used, defaults to "male"
-- @return #AICSAR self
function AICSAR:SetPilotTTSVoice(Voice,Culture,Gender)
 self:T(self.lid .. "SetPilotTTSVoice")
 self.SRSPilotVoice = true
 self.SRSPilot = MSRS:New(self.SRSPath,self.SRSFrequency,self.SRSModulation)
 self.SRSPilot:SetCoalition(self.coalition)
 self.SRSPilot:SetVoice(Voice)
 self.SRSPilot:SetCulture(Culture or "en-US")
 self.SRSPilot:SetGender(Gender or "male")
 self.SRSPilot:SetLabel("PILOT")
 if self.SRSGoogle then
  local poptions = self.SRS:GetProviderOptions(MSRS.Provider.GOOGLE) -- Sound.SRS#MSRS.ProviderOptions
  self.SRSPilot:SetProviderOptionsGoogle(poptions.credentials,poptions.key)
  self.SRSPilot:SetProvider(MSRS.Provider.GOOGLE)
 end
 return self
end

--- [User] Set SRS TTS Voice of the rescue operator. `AICSAR:SetSRSTTSRadio()` needs to be set first!
-- @param #AICSAR self
-- @param #string Voice The voice to be used, e.g. `MSRS.Voices.Google.Standard.en_US_Standard_J` for Google or `MSRS.Voices.Microsoft.David` for Microsoft.
-- Specific voices override culture and gender!
-- @param #string Culture (Optional) The culture to be used, defaults to "en-GB"
-- @param #string Gender (Optional)  The gender to be used, defaults to "female"
-- @return #AICSAR self
function AICSAR:SetOperatorTTSVoice(Voice,Culture,Gender)
 self:T(self.lid .. "SetOperatorTTSVoice")
 self.SRSOperatorVoice = true
 self.SRSOperator = MSRS:New(self.SRSPath,self.SRSFrequency,self.SRSModulation)
 self.SRSOperator:SetCoalition(self.coalition)
 self.SRSOperator:SetVoice(Voice)
 self.SRSOperator:SetCulture(Culture or "en-GB")
 self.SRSOperator:SetGender(Gender or "female")
 self.SRSOperator:SetLabel("RESCUE")
 if self.SRSGoogle then
  local poptions = self.SRS:GetProviderOptions(MSRS.Provider.GOOGLE) -- Sound.SRS#MSRS.ProviderOptions
  self.SRSOperator:SetProviderOptionsGoogle(poptions.credentials,poptions.key)
  self.SRSOperator:SetProvider(MSRS.Provider.GOOGLE)
 end
 return self
end

--- [User] Switch sound output on and use normale (DCS) radio
-- @param #AICSAR self
-- @param #boolean OnOff Switch on (true) or off (false).
-- @param #number Frequency Defaults to 243 (guard).
-- @param #number Modulation Radio modulation. Defaults to radio.modulation.AM.
-- @param Wrapper.Group#GROUP Group The group to use as sending station.
-- @return #AICSAR self
function AICSAR:SetDCSRadio(OnOff,Frequency,Modulation,Group)
  self:T(self.lid .. "SetDCSRadio")
  self:T(self.lid .. "SetDCSRadio to "..tostring(OnOff))
  self.DCSRadio = OnOff and true
  self.DCSFrequency = Frequency or 243
  self.DCSModulation = Modulation or radio.modulation.AM
  self.DCSRadioGroup = Group
  if self.DCSRadio then
    self.DCSRadioQueue = RADIOQUEUE:New(Frequency,Modulation,"AI-CSAR")
    self.DCSRadioQueue:Start(5,5)
    self.DCSRadioQueue:SetRadioPower(1000)
    self.DCSRadioQueue:SetSenderCoordinate(Group:GetCoordinate())
  else
    if self.DCSRadioQueue then
      self.DCSRadioQueue:Stop()
    end
  end
  return self
end

--- [Internal] Sound output via non-SRS Radio. Add message files (.ogg) via "Sound to..." in the ME.
-- @param #AICSAR self
-- @param #string Soundfile Name of the soundfile
-- @param #number Duration Duration of the sound
-- @param #string Subtitle Text to display
-- @return #AICSAR self
function AICSAR:DCSRadioBroadcast(Soundfile,Duration,Subtitle)
  self:T(self.lid .. "DCSRadioBroadcast")
  local radioqueue = self.DCSRadioQueue -- Sound.RadioQueue#RADIOQUEUE
  radioqueue:NewTransmission(Soundfile,Duration,nil,2,nil,Subtitle,10)
  return self
end

--- [Internal] Catch the ejection and save the pilot name
-- @param #AICSAR self
-- @param Core.Event#EVENTDATA EventData
-- @return #AICSAR self
function AICSAR:_EjectEventHandler(EventData)
  local _event = EventData -- Core.Event#EVENTDATA
  if _event.IniPlayerName then
    self.PilotStore:Push(_event.IniPlayerName)
    self:T(self.lid.."Pilot Ejected: ".._event.IniPlayerName)
    if self.UseEventEject then
      -- get position and spawn in a template pilot
      local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p)
      local _country = _event.initiator:getCountry()
      local _coalition = coalition.getCountryCoalition( _country )
      --local data = UTILS.DeepCopy(EventData)
      Unit.destroy(_event.initiator) -- shagrat remove static Pilot model
      self:ScheduleOnce(self.Delay,self._DelayedSpawnPilot,self,_LandingPos,_coalition)
    end
  end
  return self
end

--- [Internal] Spawn a pilot
-- @param #AICSAR self
-- @param Core.Point#COORDINATE _LandingPos Landing Postion
-- @param #number _coalition Coalition side
-- @return #AICSAR self
function AICSAR:_DelayedSpawnPilot(_LandingPos,_coalition)

  local distancetofarp = _LandingPos:Get2DDistance(self.farp:GetCoordinate())
  if self.UseRescueZone == true and self.RescueZone ~= nil then
    if self.RescueZone:IsCoordinateInZone(_LandingPos) then
      distancetofarp = self.maxdistance - 10
    else
      distancetofarp = self.maxdistance + 10
    end
  end 
  -- Mayday Message
  local Text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTDOWN",self.locale)
  local text = ""
  local setting = {}
  setting.MGRS_Accuracy = self.MGRS_Accuracy
  local location = _LandingPos:ToStringMGRS(setting)
  local msgtxt = Text..location.."!"
  location = string.gsub(location,"MGRS ","")
  location = string.gsub(location,"%s+","")
  location = string.gsub(location,"([%a%d])","%1;") -- "0 5 1 "
  location = string.gsub(location,"0","zero")
  location = string.gsub(location,"9","niner")
  location = "MGRS;"..location
  if self.SRSGoogle then
    location = string.format("<say-as interpret-as='characters'>%s</say-as>",location)
  end
  text = Text .. location .. "!"
  local ttstext = Text .. location .. "! Repeat! "..location
  if _coalition == self.coalition then
    if self.verbose then
      MESSAGE:New(msgtxt,15,"AICSAR"):ToCoalition(self.coalition)
     -- MESSAGE:New(msgtxt,15,"AICSAR"):ToLog()
    end
    if self.SRSRadio then
      local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength)
      sound:SetPlayWithSRS(true)
      self.SRS:PlaySoundFile(sound,2)
    elseif self.DCSRadio then
      self:DCSRadioBroadcast(Soundfile,Soundlength,text)
    elseif self.SRSTTSRadio then
      if self.SRSPilotVoice then
        self.SRSQ:NewTransmission(ttstext,nil,self.SRSPilot,nil,1)
      else
        self.SRSQ:NewTransmission(ttstext,nil,self.SRS,nil,1)
      end
    end   
  end
  
  -- further processing
  if _coalition == self.coalition and distancetofarp <= self.maxdistance then
    -- in reach
    self:T(self.lid .. "Spawning new Pilot")
    self.pilotindex = self.pilotindex + 1
    local newpilot = SPAWN:NewWithAlias(self.template,string.format("%s-AICSAR-%d",self.template, self.pilotindex))
    newpilot:InitDelayOff()
    newpilot:OnSpawnGroup(
      function (grp)
        self.pilotqueue[self.pilotindex] = grp
      end
    )
    newpilot:SpawnFromCoordinate(_LandingPos) 
    
    self:__PilotDown(2,_LandingPos,true)
  elseif _coalition == self.coalition and distancetofarp > self.maxdistance then
    -- out of reach, apologies, too far off   
    self:T(self.lid .. "Pilot out of reach")
    self:__PilotDown(2,_LandingPos,false)
  end 
  return self
end

--- [Internal] Catch the landing after ejection and spawn a pilot in situ.
-- @param #AICSAR self
-- @param Core.Event#EVENTDATA EventData
-- @param #boolean FromEject
-- @return #AICSAR self
function AICSAR:_EventHandler(EventData, FromEject)
  self:T(self.lid .. "OnEventLandingAfterEjection ID=" .. EventData.id)
  
  -- autorescue on off?
  if self.autoonoff then
    if self.playerset:CountAlive() > 0 then
      return self
    end
  end
  
  if self.UseEventEject and (not FromEject) then return self end
  
  local _event = EventData -- Core.Event#EVENTDATA
  -- get position and spawn in a template pilot
  local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p)
  local _country = _event.initiator:getCountry()
  local _coalition = coalition.getCountryCoalition( _country )

  -- DONE: add distance check
  local distancetofarp = _LandingPos:Get2DDistance(self.farp:GetCoordinate())
  if self.UseRescueZone == true and self.RescueZone ~= nil then
    if self.RescueZone:IsCoordinateInZone(_LandingPos) then
      distancetofarp = self.maxdistance - 10
    else
      distancetofarp = self.maxdistance + 10
    end
  end 
  -- Mayday Message
  local Text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTDOWN",self.locale)
  local text = ""
  local setting = {}
  setting.MGRS_Accuracy = self.MGRS_Accuracy
  local location = _LandingPos:ToStringMGRS(setting)
  local msgtxt = Text..location.."!"
  location = string.gsub(location,"MGRS ","")
  location = string.gsub(location,"%s+","")
  location = string.gsub(location,"([%a%d])","%1;") -- "0 5 1 "
  location = string.gsub(location,"0","zero")
  location = string.gsub(location,"9","niner")
  location = "MGRS;"..location
  if self.SRSGoogle then
    location = string.format("<say-as interpret-as='characters'>%s</say-as>",location)
  end
  text = Text .. location .. "!"
  local ttstext = Text .. location .. "! Repeat! "..location
  if _coalition == self.coalition then
    if self.verbose then
      MESSAGE:New(msgtxt,15,"AICSAR"):ToCoalition(self.coalition)
    end
    if self.SRSRadio then
      local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength)
      sound:SetPlayWithSRS(true)
      self.SRS:PlaySoundFile(sound,2)
    elseif self.DCSRadio then
      self:DCSRadioBroadcast(Soundfile,Soundlength,text)
    elseif self.SRSTTSRadio then
      if self.SRSPilotVoice then
        self.SRSQ:NewTransmission(ttstext,nil,self.SRSPilot,nil,1)
      else
        self.SRSQ:NewTransmission(ttstext,nil,self.SRS,nil,1)
      end
    end   
  end
  
  -- further processing
  if _coalition == self.coalition and distancetofarp <= self.maxdistance then
    -- in reach
    self:T(self.lid .. "Spawning new Pilot")
    self.pilotindex = self.pilotindex + 1
    local newpilot = SPAWN:NewWithAlias(self.template,string.format("%s-AICSAR-%d",self.template, self.pilotindex))
    newpilot:InitDelayOff()
    newpilot:OnSpawnGroup(
      function (grp)
        self.pilotqueue[self.pilotindex] = grp
      end
    )
    newpilot:SpawnFromCoordinate(_LandingPos) 
    
    Unit.destroy(_event.initiator) -- shagrat remove static Pilot model
    self:__PilotDown(2,_LandingPos,true)
  elseif _coalition == self.coalition and distancetofarp > self.maxdistance then
    -- out of reach, apologies, too far off   
    self:T(self.lid .. "Pilot out of reach")
    self:__PilotDown(2,_LandingPos,false)
  end 
  return self
end

--- [Internal] Get FlightGroup
-- @param #AICSAR self
-- @return Ops.FlightGroup#FLIGHTGROUP The FlightGroup
function AICSAR:_GetFlight()
  self:T(self.lid .. "_GetFlight")
  -- Helo Carrier.
  local newhelo = SPAWN:NewWithAlias(self.helotemplate,self.helotemplate..math.random(1,10000))
    :InitDelayOff()
    :InitUnControlled(true)
    :OnSpawnGroup(
      function(Group)
        Group:OptionPreferVerticalLanding()
        self:__HeloOnDuty(1,Group)
      end
    )
    :Spawn()
    
  local nhelo=FLIGHTGROUP:New(newhelo)
  nhelo:SetHomebase(self.farp)
  nhelo:Activate()
  return nhelo
end

--- [Internal] Create a new rescue mission
-- @param #AICSAR self
-- @param Wrapper.Group#GROUP Pilot The pilot to be rescued.
-- @param #number Index Index number of this pilot
-- @return #AICSAR self
function AICSAR:_InitMission(Pilot,Index)
  self:T(self.lid .. "_InitMission")
  
  local pickupzone = ZONE_GROUP:New(Pilot:GetName(),Pilot,self.rescuezoneradius)
  --local pilotset = SET_GROUP:New()
  --pilotset:AddGroup(Pilot)
  
  -- Cargo transport assignment.
  local opstransport=OPSTRANSPORT:New(Pilot, pickupzone, self.farpzone)
  --opstransport:SetVerbosity(3)
  
  local helo = self:_GetFlight()
  -- inject reservation
  helo.AICSARReserved = true
  
  helo:SetDefaultAltitude(self.Altitude or 1500)
  
  helo:SetDefaultSpeed(self.Speed or 100)
  
  -- Cargo transport assignment to first Huey group.
  helo:AddOpsTransport(opstransport)
  
  -- callback functions
  local function AICPickedUp(Helo,Cargo,Index)
    self:__PilotPickedUp(2,Helo,Cargo,Index)   
  end
  
  local function AICHeloDead(Helo,Index)
    self:__HeloDown(2,Helo,Index)   
  end
  
  local function AICHeloUnloaded(Helo,OpsGroup)
    self:__PilotUnloaded(2,Helo,OpsGroup)
  end
  
  function helo:OnAfterLoading(From,Event,To)
    AICPickedUp(helo,helo:GetCargoGroups(),Index) 
    helo:__LoadingDone(5)  
  end
  
  function helo:OnAfterDead(From,Event,To)
    AICHeloDead(helo,Index)
  end
  
  function helo:OnAfterUnloaded(From,Event,To,OpsGroupCargo)
    AICHeloUnloaded(helo,OpsGroupCargo)
    helo:__UnloadingDone(5)
  end
  
  function helo:OnAfterLandAtAirbase(From,Event,To,airbase)
    helo:Despawn(2)
  end
  
  self.helos[Index] = helo
  
  return self
end

--- [Internal] Check if pilot arrived in rescue zone (MASH)
-- @param #AICSAR self
-- @param Wrapper.Group#GROUP Pilot The pilot to be rescued.
-- @return #boolean outcome
function AICSAR:_CheckInMashZone(Pilot)
  self:T(self.lid .. "_CheckInMashZone")
  if Pilot:IsInZone(self.farpzone) then
    return true
  else
    return false
  end
end

--- [User] Set default helo speed. Note - AI might have other ideas. Defaults to 100kn.
-- @param #AICSAR self
-- @param #number Knots Speed in knots.
-- @return #AICSAR self
function AICSAR:SetDefaultSpeed(Knots)
  self:T(self.lid .. "SetDefaultSpeed")
  self.Speed = Knots or 100
  return self
end

--- [User] Set default helo altitudeAGL. Note - AI might have other ideas. Defaults to 1500ft.
-- @param #AICSAR self
-- @param #number Feet AGL set in feet.
-- @return #AICSAR self
function AICSAR:SetDefaultAltitude(Feet)
  self:T(self.lid .. "SetDefaultAltitude")
  self.Altitude = Feet or 1500
  return self
end

--- [Internal] Check helo queue 
-- @param #AICSAR self
-- @return #AICSAR self
function AICSAR:_CheckHelos()
  self:T(self.lid .. "_CheckHelos")
  for _index,_helo in pairs(self.helos) do
    local helo = _helo -- Ops.FlightGroup#FLIGHTGROUP
    if helo and helo.ClassName == "FLIGHTGROUP" then
      local state = helo:GetState()
      local name = helo:GetName()
      self:T("Helo group "..name.." in state "..state)
      if state == "Arrived" then
        --helo:__Stop(5)
        helo.OnAfterDead = nil
        helo:Despawn(35)
        self.helos[_index] = nil
      end
    else
      self.helos[_index] = nil
    end
  end
  return self
end

--- [Internal] Count helos queue 
-- @param #AICSAR self
-- @return #number Number of helos on mission
function AICSAR:_CountHelos()
  self:T(self.lid .. "_CountHelos")
  local count = 0
    for _index,_helo in pairs(self.helos) do
      count = count + 1
    end
  return count
end

--- [Internal] Check pilot queue for next mission
-- @param #AICSAR self
-- @param Ops.OpsGroup#OPSGROUP OpsGroup
-- @return #AICSAR self
function AICSAR:_CheckQueue(OpsGroup)
  self:T(self.lid .. "_CheckQueue")
  for _index, _pilot in pairs(self.pilotqueue) do
    local classname = _pilot.ClassName and _pilot.ClassName or "NONE"
    local name = _pilot.GroupName and _pilot.GroupName or "NONE"
    local playername = "John Doe"
    local helocount = self:_CountHelos()
    --self:T("Looking at " .. classname .. " " .. name)
    -- find one w/o mission
    if _pilot and _pilot.ClassName and _pilot.ClassName == "GROUP" then
     local flightgroup = self.helos[_index] -- Ops.FlightGroup#FLIGHTGROUP
     -- rescued?
     if self:_CheckInMashZone(_pilot) then
      self:T("Pilot" .. _pilot.GroupName .. " rescued!")
      if OpsGroup then
        --OpsGroup:Despawn(10)
      else 
        _pilot:Destroy(true,10)
      end
      self.pilotqueue[_index] = nil
      self.rescued[_index] = true
      if self.PilotStore:Count() > 0 then
        playername = self.PilotStore:Pull()
      end
      self:__PilotRescued(2,playername)
      if flightgroup then
        flightgroup.AICSARReserved = false
      end
     end -- end rescued
      -- has no mission assigned?
      if not _pilot.AICSAR then
        -- helo available?
        if self.limithelos and helocount >= self.helonumber then
            -- none free
            break
        end -- end limit
        _pilot.AICSAR = {}
        _pilot.AICSAR.Status = "Initiated"
        _pilot.AICSAR.Boarded = false
        self:_InitMission(_pilot,_index)
        break
      else 
       -- update status from OPSGROUP
       if flightgroup then
         local state = flightgroup:GetState()
         _pilot.AICSAR.Status = state
       end
       --self:T("Flight for " .. _pilot.GroupName .. " in state " .. state)
      end -- end has mission
    end -- end if pilot
  end -- end loop
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- [Internal] onafterStart
-- @param #AICSAR self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AICSAR self
function AICSAR:onafterStart(From, Event, To)
  self:T({From, Event, To})
  self:__Status(3)
  return self
end

--- [Internal] onafterStatus
-- @param #AICSAR self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AICSAR self
function AICSAR:onafterStatus(From, Event, To)
  self:T({From, Event, To})
  --self:_CheckQueue()
  self:_CheckHelos()
  self:__Status(30)
  return self
end

--- [Internal] onafterStop
-- @param #AICSAR self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AICSAR self
function AICSAR:onafterStop(From, Event, To)
  self:T({From, Event, To})
  self:UnHandleEvent(EVENTS.LandingAfterEjection)
  if self.DCSRadioQueue then
    self.DCSRadioQueue:Stop()
  end
  return self
end

--- [Internal] onafterPilotDown
-- @param #AICSAR self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param Core.Point#COORDINATE Coordinate Location of the pilot.
-- @param #boolean InReach True if in maxdistance else false.
-- @return #AICSAR self
function AICSAR:onafterPilotDown(From, Event, To, Coordinate, InReach)
  self:T({From, Event, To})
  local CoordinateText = Coordinate:ToStringMGRS()
  local inreach = tostring(InReach)
  --local text = string.format("Pilot down at %s. In reach = %s",CoordinateText,inreach)
  if InReach then
    local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("INITIALOK",self.locale)
    --local text = AICSAR.Messages.EN.INITIALOK
    self:T(text)
    if self.verbose then
      MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition)
    end
    if self.SRSRadio then
      local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength)
      sound:SetPlayWithSRS(true)
      self.SRS:PlaySoundFile(sound,2)
    elseif self.DCSRadio then
      self:DCSRadioBroadcast(Soundfile,Soundlength,text)
    elseif self.SRSTTSRadio then
      if self.SRSOperatorVoice then
       self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1)
      else
        self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1)
      end
    end
  else
    local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("INITIALNOTOK",self.locale)
    --local text = AICSAR.Messages.EN.INITIALNOTOK
    self:T(text)
    if self.verbose then
      MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition)
    end
    if self.SRSRadio then
      local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength)
      sound:SetPlayWithSRS(true)
      self.SRS:PlaySoundFile(sound,2)
    elseif self.DCSRadio then
      self:DCSRadioBroadcast(Soundfile,Soundlength,text)
    elseif self.SRSTTSRadio then
      if self.SRSOperatorVoice then
        self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1)
      else
        self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1)
      end
    end
  end
  self:_CheckQueue()
  return self
end

--- [Internal] onafterPilotKIA
-- @param #AICSAR self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AICSAR self
function AICSAR:onafterPilotKIA(From, Event, To)
  self:T({From, Event, To})
  local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTKIA",self.locale)
  if self.verbose then
    MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition)
  end
  if self.SRSRadio then
    local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength)
    sound:SetPlayWithSRS(true)
    self.SRS:PlaySoundFile(sound,2)
  elseif self.DCSRadio then
    self:DCSRadioBroadcast(Soundfile,Soundlength,text)
  elseif self.SRSTTSRadio then
      self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1)
    end
  return self
end

--- [Internal] onafterHeloDown
-- @param #AICSAR self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param Ops.FlightGroup#FLIGHTGROUP Helo
-- @param #number Index
-- @return #AICSAR self
function AICSAR:onafterHeloDown(From, Event, To, Helo, Index)
  self:T({From, Event, To})
  local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("HELODOWN",self.locale)
  if self.verbose then
    MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition)
  end
  if self.SRSRadio then
    local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength)
    sound:SetPlayWithSRS(true)
    self.SRS:PlaySoundFile(sound,2)
  elseif self.DCSRadio then
    self:DCSRadioBroadcast(Soundfile,Soundlength,text)
  elseif self.SRSTTSRadio then
    if self.SRSOperatorVoice then
      self.SRSQ:NewTransmission(text,nil,self.SRSOperator,nil,1)
    else
      self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1)
    end
  end
  local findex = 0
  local fhname = Helo:GetName()
  -- find index of Helo
  if Index and Index > 0 then
    findex=Index
  else
    for _index, _helo in pairs(self.helos) do
      local helo = _helo -- Ops.FlightGroup#FLIGHTGROUP
      local hname = helo:GetName()
      if fhname == hname then
        findex = _index
        break
      end
    end
  end
  -- find pilot
  if findex > 0  and not self.rescued[findex] then
    local pilot = self.pilotqueue[findex]
    self.helos[findex] = nil
    if pilot.AICSAR.Boarded then
      self:T("Helo Down: Found DEAD Pilot ID " .. findex .. " with name " .. pilot:GetName())
      -- pilot also dead
      self:__PilotKIA(2)
      self.pilotqueue[findex] = nil
    else
      -- initiate new mission
      self:T("Helo Down: Found ALIVE Pilot ID " .. findex .. " with name " .. pilot:GetName())
      self:_InitMission(pilot,findex)
    end
  end
  return self
end

--- [Internal] onafterPilotRescued
-- @param #AICSAR self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param #string PilotName
-- @return #AICSAR self
function AICSAR:onafterPilotRescued(From, Event, To, PilotName)
  self:T({From, Event, To})
  local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTRESCUED",self.locale)
  if self.verbose then
    MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition)
  end
  if self.SRSRadio then
    local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength)
    sound:SetPlayWithSRS(true)
    self.SRS:PlaySoundFile(sound,2)
  elseif self.DCSRadio then
    self:DCSRadioBroadcast(Soundfile,Soundlength,text)
  elseif self.SRSTTSRadio then
      self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1)
  end
  return self
end

--- [Internal] onafterPilotUnloaded
-- @param #AICSAR self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param Ops.FlightGroup#FLIGHTGROUP Helo
-- @param Ops.OpsGroup#OPSGROUP OpsGroup
-- @return #AICSAR self
function AICSAR:onafterPilotUnloaded(From, Event, To, Helo, OpsGroup)
  self:T({From, Event, To})
  self:_CheckQueue(OpsGroup)
  return self
end

--- [Internal] onafterPilotPickedUp
-- @param #AICSAR self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param Ops.FlightGroup#FLIGHTGROUP Helo
-- @param #table CargoTable of Ops.OpsGroup#OPSGROUP Cargo objects
-- @param #number Index 
-- @return #AICSAR self
function AICSAR:onafterPilotPickedUp(From, Event, To, Helo, CargoTable, Index)
  self:T({From, Event, To})
  local text,Soundfile,Soundlength,Subtitle = self.gettext:GetEntry("PILOTINHELO",self.locale)
  if self.verbose then
    MESSAGE:New(text,15,"AICSAR"):ToCoalition(self.coalition)
  end
  if self.SRSRadio then
    local sound = SOUNDFILE:New(Soundfile,self.SRSSoundPath,Soundlength)
    sound:SetPlayWithSRS(true)
    self.SRS:PlaySoundFile(sound,2)
  elseif self.DCSRadio then
    self:DCSRadioBroadcast(Soundfile,Soundlength,text)
  elseif self.SRSTTSRadio then
    self.SRSQ:NewTransmission(text,nil,self.SRS,nil,1)
  end
  local findex = 0
  local fhname = Helo:GetName()
  if Index and Index > 0 then
    findex = Index
  else
    -- find index of Helo
    for _index, _helo in pairs(self.helos) do
      local helo = _helo -- Ops.FlightGroup#FLIGHTGROUP
      local hname = helo:GetName()
      if fhname == hname then
        findex = _index
        break
      end
    end
  end
  -- find pilot
  if findex > 0  then
    local pilot = self.pilotqueue[findex]
    self:T("Boarded: Found Pilot ID " .. findex .. " with name " .. pilot:GetName())
    pilot.AICSAR.Boarded = true -- mark as boarded
  end
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- END AICSAR
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Functional** -- Send a truck to supply artillery groups.
--
-- ===
--
-- **AMMOTRUCK** - Send a truck to supply artillery groups.
--
-- ===
--
-- ## Missions:
--
-- Demo missions can be found on [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Functional/AmmoTruck)
--
-- ===
--
-- ### Author : **applevangelist**
--
-- @module Functional.AmmoTruck
-- @image Artillery.JPG
--
-- Last update: July 2023

-------------------------------------------------------------------------
--- **AMMOTRUCK** class, extends Core.Fsm#FSM
-- @type AMMOTRUCK
-- @field #string ClassName Class Name
-- @field #string lid Lid for log entries
-- @field #string version Version string
-- @field #string alias Alias name
-- @field #boolean debug Debug flag
-- @field #table trucklist List of (alive) #AMMOTRUCK.data trucks
-- @field #table targetlist  List of (alive) #AMMOTRUCK.data artillery
-- @field #number coalition Coalition this is for
-- @field Core.Set#SET_GROUP truckset SET of trucks
-- @field Core.Set#SET_GROUP targetset SET of artillery
-- @field #table remunitionqueue List of (alive) #AMMOTRUCK.data artillery to be reloaded
-- @field #table waitingtargets  List of (alive) #AMMOTRUCK.data artillery waiting
-- @field #number ammothreshold Threshold (min) ammo before sending a truck
-- @field #number remunidist Max distance trucks will go
-- @field #number monitor Monitor interval in seconds
-- @field #number unloadtime Unload time in seconds
-- @field #number waitingtime Max waiting time in seconds
-- @field #boolean routeonroad Route truck on road if true (default)
-- @field #number reloads Number of reloads a single truck can do before he must return home
-- @extends Core.Fsm#FSM

--- *Amateurs talk about tactics, but professionals study logistics.* - General Robert H Barrow, USMC
--
-- Simple Class to re-arm your artillery with trucks.
--
-- #AMMOTRUCK
-- 
-- * Controls a SET\_GROUP of trucks which will re-arm a SET\_GROUP of artillery groups when they run out of ammunition. 
-- 
-- ## 1 The AMMOTRUCK concept
-- 
-- A SET\_GROUP of trucks which will re-arm a SET\_GROUP of artillery groups when they run out of ammunition. They will be based on a
-- homebase and drive from there to the artillery groups and then back home.
-- Trucks are the **only known in-game mechanic** to re-arm artillery and other units in DCS. Working units are e.g.: M-939 (blue), Ural-375 and ZIL-135 (both red).
-- 
-- ## 2 Set-up
--
-- Define a set of trucks and a set of artillery:  
-- 
--            local truckset = SET_GROUP:New():FilterCoalitions("blue"):FilterActive(true):FilterCategoryGround():FilterPrefixes("Ammo Truck"):FilterStart()
--            local ariset = SET_GROUP:New():FilterCoalitions("blue"):FilterActive(true):FilterCategoryGround():FilterPrefixes("Artillery"):FilterStart()
--
-- Create an AMMOTRUCK object to take care of the artillery using the trucks, with a homezone:  
-- 
--            local ammotruck = AMMOTRUCK:New(truckset,ariset,coalition.side.BLUE,"Logistics",ZONE:FindByName("HomeZone") 
--            
-- ## 2 Options and their default values
-- 
--            ammotruck.ammothreshold = 5 -- send a truck when down to this many rounds
--            ammotruck.remunidist = 20000 -- 20km - send trucks max this far from home
--            ammotruck.unloadtime = 600 -- 10 minutes - min time to unload ammunition
--            ammotruck.waitingtime = 1800 -- 30 mintes - wait max this long until remunition is done
--            ammotruck.monitor = -60 -- 1 minute - AMMOTRUCK checks run every one minute
--            ammotruck.routeonroad = true -- Trucks will **try** to drive on roads
--            ammotruck.usearmygroup = false -- If true, will make use of ARMYGROUP in the background (if used in DEV branch)
--            ammotruck.reloads = 5 -- Maxn re-arms a truck can do before he needs to go home and restock. Set to -1 for unlimited
-- 
-- ## 3 FSM Events to shape mission
-- 
-- Truck has been sent off:
-- 
--            function ammotruck:OnAfterRouteTruck(From, Event, To, Truckdata, Aridata)
--              ...
--            end
--            
-- Truck has arrived:
-- 
--            function ammotruck:OnAfterTruckArrived(From, Event, To, Truckdata)
--              ...
--            end 
-- 
-- Truck is unloading:
-- 
--            function ammotruck:OnAfterTruckUnloading(From, Event, To, Truckdata)
--              ...
--            end 
-- 
-- Truck is returning home:
-- 
--            function ammotruck:OnAfterTruckReturning(From, Event, To, Truckdata)
--              ...
--            end  
-- 
-- Truck is arrived at home:
-- 
--            function ammotruck:OnAfterTruckHome(From, Event, To, Truckdata)
--              ...
--            end 
--                        
-- @field #AMMOTRUCK
AMMOTRUCK = {
  ClassName = "AMMOTRUCK",
  lid = "",
  version = "0.0.12",
  alias = "",
  debug = false,
  trucklist = {},
  targetlist = {},
  coalition = nil,
  truckset = nil,
  targetset = nil,
  remunitionqueue = {},
  waitingtargets = {},
  ammothreshold = 5,
  remunidist = 20000,
  monitor = -60,
  unloadtime = 600,
  waitingtime = 1800,
  routeonroad = true,
  reloads = 5,
}

---
-- @type AMMOTRUCK.State
AMMOTRUCK.State = {
  IDLE = "idle",
  DRIVING = "driving",
  ARRIVED = "arrived",
  UNLOADING = "unloading",
  RETURNING = "returning",
  WAITING = "waiting",
  RELOADING = "reloading",
  OUTOFAMMO = "outofammo",
  REQUESTED = "requested",
}

---
--@type AMMOTRUCK.data
--@field Wrapper.Group#GROUP group
--@field #string name
--@field #AMMOTRUCK.State statusquo
--@field #number timestamp
--@field #number ammo
--@field Core.Point#COORDINATE coordinate
--@field #string targetname
--@field Wrapper.Group#GROUP targetgroup
--@field Core.Point#COORDINATE targetcoordinate
--@field #number reloads

---
-- @param #AMMOTRUCK self
-- @param Core.Set#SET_GROUP Truckset Set of truck groups
-- @param Core.Set#SET_GROUP Targetset Set of artillery groups
-- @param #number Coalition Coalition
-- @param #string Alias Alias Name
-- @param Core.Zone#ZONE Homezone Home, return zone for trucks 
-- @return #AMMOTRUCK self
-- @usage
-- Define a set of trucks and a set of artillery:  
--            local truckset = SET_GROUP:New():FilterCoalitions("blue"):FilterActive(true):FilterCategoryGround():FilterPrefixes("Ammo Truck"):FilterStart()
--            local ariset = SET_GROUP:New():FilterCoalitions("blue"):FilterActive(true):FilterCategoryGround():FilterPrefixes("Artillery"):FilterStart()
--
-- Create an AMMOTRUCK object to take care of the artillery using the trucks, with a homezone:  
--            local ammotruck = AMMOTRUCK:New(truckset,ariset,coalition.side.BLUE,"Logistics",ZONE:FindByName("HomeZone") 
function AMMOTRUCK:New(Truckset,Targetset,Coalition,Alias,Homezone)
  
    -- Inherit everything from BASE class.
  local self=BASE:Inherit(self, FSM:New()) -- #AMMOTRUCK
  
  self.truckset = Truckset -- Core.Set#SET_GROUP
  self.targetset = Targetset -- Core.Set#SET_GROUP
  self.coalition = Coalition -- #number
  self.alias = Alias -- #string
  self.debug = false
  self.remunitionqueue = {}
  self.trucklist = {}
  self.targetlist = {}
  self.ammothreshold = 5
  self.remunidist = 20000
  self.homezone = Homezone -- Core.Zone#ZONE
  self.waitingtime = 1800
  self.usearmygroup = false
  self.hasarmygroup = false
  
  -- Log id.
  self.lid=string.format("AMMOTRUCK %s | %s | ", self.version, self.alias)
  
  self:SetStartState("Stopped")
  self:AddTransition("Stopped",     "Start",               "Running")
  self:AddTransition("*",           "Monitor",             "*")
  self:AddTransition("*",           "RouteTruck",          "*")
  self:AddTransition("*",           "TruckArrived",        "*")
  self:AddTransition("*",           "TruckUnloading",      "*")
  self:AddTransition("*",           "TruckReturning",      "*")
  self:AddTransition("*",           "TruckHome",           "*")
  self:AddTransition("*",           "Stop",                "Stopped")
  
  self:__Start(math.random(5,10))
  
  self:I(self.lid .. "Started")
  
  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Stop". Stops the AMMOTRUCK and all its event handlers.
  -- @function [parent=#AMMOTRUCK] Stop
  -- @param #AMMOTRUCK self

  --- Triggers the FSM event "Stop" after a delay. Stops the AMMOTRUCK and all its event handlers.
  -- @function [parent=#AMMOTRUCK] __Stop
  -- @param #AMMOTRUCK self
  -- @param #number delay Delay in seconds.
  
  --- On after "RouteTruck" event.
  -- @function [parent=#AMMOTRUCK] OnAfterRouteTruck
  -- @param #AMMOTRUCK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #AMMOTRUCK.data Truck
  -- @param #AMMOTRUCK.data Artillery
  
    --- On after "TruckUnloading" event.
  -- @function [parent=#AMMOTRUCK] OnAfterTruckUnloading
  -- @param #AMMOTRUCK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #AMMOTRUCK.data Truck
  
    --- On after "TruckReturning" event.
  -- @function [parent=#AMMOTRUCK] OnAfterTruckReturning
  -- @param #AMMOTRUCK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #AMMOTRUCK.data Truck
  
    --- On after "RouteTruck" event.
  -- @function [parent=#AMMOTRUCK] OnAfterRouteTruck
  -- @param #AMMOTRUCK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #AMMOTRUCK.data Truck
  
    --- On after "TruckHome" event.
  -- @function [parent=#AMMOTRUCK] OnAfterTruckHome
  -- @param #AMMOTRUCK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #AMMOTRUCK.data Truck
  
  return self
end

---
-- @param #AMMOTRUCK self
-- @param #table dataset table of #AMMOTRUCK.data entries
-- @return #AMMOTRUCK self 
function AMMOTRUCK:CheckDrivingTrucks(dataset)
  self:T(self.lid .. " CheckDrivingTrucks")
  local data = dataset 
  for _,_data in pairs (data) do
    local truck = _data -- #AMMOTRUCK.data
    -- see if we arrived at destination
    local coord = truck.group:GetCoordinate()
    local tgtcoord = truck.targetcoordinate
    local dist = coord:Get2DDistance(tgtcoord)
    if dist <= 150 then
      -- arrived
      truck.statusquo = AMMOTRUCK.State.ARRIVED
      truck.timestamp = timer.getAbsTime()
      truck.coordinate = coord
      self:__TruckArrived(1,truck)
    end
    -- still driving?
    local Tnow = timer.getAbsTime()
    if Tnow - truck.timestamp > 30 then
      local group = truck.group
      if self.usearmygroup then
        group = truck.group:GetGroup()
      end
      local currspeed = group:GetVelocityKMH()
      if truck.lastspeed then
        if truck.lastspeed == 0 and currspeed == 0 then
          self:T(truck.group:GetName().." Is not moving!")
          -- try and move it
          truck.timestamp = timer.getAbsTime()
          if self.routeonroad then
            group:RouteGroundOnRoad(truck.targetcoordinate,30,2,"Vee")
          else
            group:RouteGroundTo(truck.targetcoordinate,30,"Vee",2)
          end
        end
        truck.lastspeed = currspeed
      else
        truck.lastspeed = currspeed
        truck.timestamp = timer.getAbsTime()
      end
      self:I({truck=truck.group:GetName(),currspeed=currspeed,lastspeed=truck.lastspeed})
    end
  end
  return self
end

---
-- @param #AMMOTRUCK self
-- @param Wrapper.Group#GROUP Group
-- @return #AMMOTRUCK self 
function AMMOTRUCK:GetAmmoStatus(Group)
  local ammotot, shells, rockets, bombs, missiles, narti = Group:GetAmmunition()
  return rockets+missiles+narti
end

---
-- @param #AMMOTRUCK self
-- @param #table dataset table of #AMMOTRUCK.data entries
-- @return #AMMOTRUCK self 
function AMMOTRUCK:CheckWaitingTargets(dataset)
  self:T(self.lid .. " CheckWaitingTargets")
  local data = dataset 
  for _,_data in pairs (data) do
    local truck = _data -- #AMMOTRUCK.data
    -- see how long we're waiting - maybe ammo truck is dead?
    local Tnow = timer.getAbsTime()
    local Tdiff = Tnow - truck.timestamp
    if Tdiff > self.waitingtime then
      local hasammo = self:GetAmmoStatus(truck.group)
      if hasammo <= self.ammothreshold then
        truck.statusquo = AMMOTRUCK.State.OUTOFAMMO
      else
        truck.statusquo = AMMOTRUCK.State.IDLE
      end
    end
  end
  return self
end

---
-- @param #AMMOTRUCK self
-- @param #table dataset table of #AMMOTRUCK.data entries
-- @return #AMMOTRUCK self 
function AMMOTRUCK:CheckReturningTrucks(dataset)
  self:T(self.lid .. " CheckReturningTrucks")
  local data = dataset
  local tgtcoord = self.homezone:GetCoordinate()
  local radius = self.homezone:GetRadius()
  for _,_data in pairs (data) do
    local truck = _data -- #AMMOTRUCK.data
    -- see if we arrived at destination
    local coord = truck.group:GetCoordinate()
    local dist = coord:Get2DDistance(tgtcoord)
    self:T({name=truck.name,radius=radius,distance=dist})
    if dist <= radius then
      -- arrived
      truck.statusquo = AMMOTRUCK.State.IDLE
      truck.timestamp = timer.getAbsTime()
      truck.coordinate = coord
      truck.reloads = self.reloads or 5
      self:__TruckHome(1,truck)
    end
  end
  return self
end

---
-- @param #AMMOTRUCK self
-- @param #string name Artillery group name to find
-- @return #AMMOTRUCK.data Data
function AMMOTRUCK:FindTarget(name)
  self:T(self.lid .. " FindTarget")
  local data = nil
  local dataset = self.targetlist
  for _,_entry in pairs(dataset) do
    local entry = _entry -- #AMMOTRUCK.data
    if entry.name == name then
      data = entry
      break
    end
  end
  return data
end

---
-- @param #AMMOTRUCK self
-- @param #string name Truck group name to find
-- @return #AMMOTRUCK.data Data
function AMMOTRUCK:FindTruck(name)
  self:T(self.lid .. " FindTruck")
  local data = nil
  local dataset = self.trucklist
  for _,_entry in pairs(dataset) do
    local entry = _entry -- #AMMOTRUCK.data
    if entry.name == name then
      data = entry
      break
    end
  end
  return data
end

---
-- @param #AMMOTRUCK self
-- @param #table dataset table of #AMMOTRUCK.data entries
-- @return #AMMOTRUCK self 
function AMMOTRUCK:CheckArrivedTrucks(dataset)
  self:T(self.lid .. " CheckArrivedTrucks")
  local data = dataset 
  for _,_data in pairs (data) do
    -- set to unloading
    local truck = _data -- #AMMOTRUCK.data
    truck.statusquo = AMMOTRUCK.State.UNLOADING
    truck.timestamp = timer.getAbsTime()
    self:__TruckUnloading(2,truck)
    -- set target to reloading
    local aridata = self:FindTarget(truck.targetname) -- #AMMOTRUCK.data
    if aridata then
      aridata.statusquo = AMMOTRUCK.State.RELOADING
      aridata.timestamp = timer.getAbsTime()
    end
  end
  return self
end

---
-- @param #AMMOTRUCK self
-- @param #table dataset table of #AMMOTRUCK.data entries
-- @return #AMMOTRUCK self 
function AMMOTRUCK:CheckUnloadingTrucks(dataset)
  self:T(self.lid .. " CheckUnloadingTrucks")
  local data = dataset 
  for _,_data in pairs (data) do
    -- check timestamp
    local truck = _data -- #AMMOTRUCK.data
    local Tnow = timer.getAbsTime()
    local Tpassed = Tnow - truck.timestamp
    local hasammo = self:GetAmmoStatus(truck.targetgroup)
    if Tpassed > self.unloadtime and hasammo > self.ammothreshold then
      truck.statusquo = AMMOTRUCK.State.RETURNING
      truck.timestamp = timer.getAbsTime()
      self:__TruckReturning(2,truck)
      -- set target to reloaded     
      local aridata = self:FindTarget(truck.targetname) -- #AMMOTRUCK.data
      if aridata then
        aridata.statusquo = AMMOTRUCK.State.IDLE
        aridata.timestamp = timer.getAbsTime()
      end
    end
  end
  return self
end

---
-- @param #AMMOTRUCK self
-- @return #AMMOTRUCK self 
function AMMOTRUCK:CheckTargetsAlive()
  self:T(self.lid .. " CheckTargetsAlive")
  local arilist = self.targetlist
  for _,_ari in pairs(arilist) do
    local ari = _ari -- #AMMOTRUCK.data
    if ari.group and ari.group:IsAlive() then
      -- everything fine
    else
      -- ari dead
      self.targetlist[ari.name] = nil
    end
  end
  -- new arrivals?
  local aritable = self.targetset:GetSetObjects() --#table
  for _,_ari in pairs(aritable) do
    local ari = _ari -- Wrapper.Group#GROUP
    if ari and ari:IsAlive() and not self.targetlist[ari:GetName()] then
      local name = ari:GetName()
      local newari = {} -- #AMMOTRUCK.data
      newari.name = name
      newari.group = ari
      newari.statusquo = AMMOTRUCK.State.IDLE
      newari.timestamp = timer.getAbsTime()
      newari.coordinate = ari:GetCoordinate()
      local hasammo = self:GetAmmoStatus(ari)
      --newari.ammo = ari:GetAmmunition()
      newari.ammo = hasammo
      self.targetlist[name] = newari
    end 
  end
  return self
end

---
-- @param #AMMOTRUCK self
-- @return #AMMOTRUCK self 
function AMMOTRUCK:CheckTrucksAlive()
  self:T(self.lid .. " CheckTrucksAlive")
  local trucklist = self.trucklist
  for _,_truck in pairs(trucklist) do
    local truck = _truck -- #AMMOTRUCK.data
    if truck.group and truck.group:IsAlive() then
      -- everything fine
    else
      -- truck dead
      local tgtname = truck.targetname
      local targetdata = self:FindTarget(tgtname) -- #AMMOTRUCK.data
      if targetdata then
        if targetdata.statusquo ~= AMMOTRUCK.State.IDLE then
          targetdata.statusquo = AMMOTRUCK.State.IDLE
        end
      end
      self.trucklist[truck.name] = nil
    end
  end
  -- new arrivals?
  local trucktable = self.truckset:GetSetObjects() --#table
  for _,_truck in pairs(trucktable) do
    local truck = _truck -- Wrapper.Group#GROUP
    if truck and truck:IsAlive() and not self.trucklist[truck:GetName()] then
      local name = truck:GetName()
      local newtruck = {} -- #AMMOTRUCK.data
      newtruck.name = name
      newtruck.group = truck
      if self.hasarmygroup then
        -- is (not) already ARMYGROUP?
        if truck.ClassName and truck.ClassName == "GROUP" then
          local trucker = ARMYGROUP:New(truck)
          trucker:Activate()
          newtruck.group = trucker
        end
      end
      newtruck.statusquo = AMMOTRUCK.State.IDLE
      newtruck.timestamp = timer.getAbsTime()
      newtruck.coordinate = truck:GetCoordinate()
      newtruck.reloads = self.reloads or 5
      self.trucklist[name] = newtruck
    end 
  end
  return self
end

---
-- @param #AMMOTRUCK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #AMMOTRUCK self 
function AMMOTRUCK:onafterStart(From, Event, To)
  self:T({From, Event, To})
  if ARMYGROUP and self.usearmygroup then
    self.hasarmygroup = true
  else
   self.hasarmygroup = false
  end
  if self.debug then
    BASE:TraceOn()
    BASE:TraceClass("AMMOTRUCK")
  end
  self:CheckTargetsAlive()
  self:CheckTrucksAlive() 
  self:__Monitor(-30)
  return self
end

---
-- @param #AMMOTRUCK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #AMMOTRUCK self 
function AMMOTRUCK:onafterMonitor(From, Event, To)
  self:T({From, Event, To})
  self:CheckTargetsAlive()
  self:CheckTrucksAlive()
  -- update ammo state
  local remunition = false
  local remunitionqueue = {}
  local waitingtargets = {}
  for _,_ari in pairs(self.targetlist) do
    local data = _ari -- #AMMOTRUCK.data
    if data.group and data.group:IsAlive() then
      data.ammo = self:GetAmmoStatus(data.group)
      data.timestamp = timer.getAbsTime()
      local text = string.format("Ari %s | Ammo %d | State %s",data.name,data.ammo,data.statusquo)
      self:T(text)
      if data.ammo <= self.ammothreshold and (data.statusquo == AMMOTRUCK.State.IDLE or data.statusquo == AMMOTRUCK.State.OUTOFAMMO) then
        -- add to remu queue
        data.statusquo = AMMOTRUCK.State.OUTOFAMMO
        remunitionqueue[#remunitionqueue+1] = data
        remunition = true
      elseif data.statusquo == AMMOTRUCK.State.WAITING then
        waitingtargets[#waitingtargets+1] = data
      end
    else
      self.targetlist[data.name] = nil
    end
  end
  -- sort trucks in buckets
  local idletrucks = {}
  local drivingtrucks = {}
  local unloadingtrucks = {}
  local arrivedtrucks = {}
  local returningtrucks = {}
  local found = false
  for _,_truckdata in pairs(self.trucklist) do
    local data = _truckdata -- #AMMOTRUCK.data
    if data.group and data.group:IsAlive() then
        -- check state
        local text = string.format("Truck %s | State %s",data.name,data.statusquo)
        self:T(text)
        if data.statusquo == AMMOTRUCK.State.IDLE then
          idletrucks[#idletrucks+1] = data
          found = true
        elseif data.statusquo == AMMOTRUCK.State.DRIVING then
          drivingtrucks[#drivingtrucks+1] = data
        elseif data.statusquo == AMMOTRUCK.State.ARRIVED then
          arrivedtrucks[#arrivedtrucks+1] = data
        elseif data.statusquo == AMMOTRUCK.State.UNLOADING then
          unloadingtrucks[#unloadingtrucks+1] = data
        elseif data.statusquo == AMMOTRUCK.State.RETURNING then
          returningtrucks[#returningtrucks+1] = data
        if data.reloads > 0 or data.reloads == -1 then
          idletrucks[#idletrucks+1] = data
          found = true
        end         
        end
    else
      self.truckset[data.name] = nil
    end
  end
  -- see if we can/need route one
  local n=0
  if found and remunition then
    -- match
    --local match = false
    for _,_truckdata in pairs(idletrucks) do
      local truckdata = _truckdata -- #AMMOTRUCK.data
      local truckcoord = truckdata.group:GetCoordinate() -- Core.Point#COORDINATE
      for _,_aridata in pairs(remunitionqueue) do
        local aridata = _aridata -- #AMMOTRUCK.data
        local aricoord = aridata.coordinate
        local distance = truckcoord:Get2DDistance(aricoord)
        if distance <= self.remunidist and aridata.statusquo == AMMOTRUCK.State.OUTOFAMMO and n <= #idletrucks then
          n = n + 1
          aridata.statusquo = AMMOTRUCK.State.REQUESTED
          self:__RouteTruck(n*5,truckdata,aridata)
          break
        end
      end
    end
  end
  
  -- check driving trucks
  if #drivingtrucks > 0 then
    self:CheckDrivingTrucks(drivingtrucks)
  end
  
  -- check arrived trucks
  if #arrivedtrucks > 0 then
    self:CheckArrivedTrucks(arrivedtrucks)
  end
  
  -- check unloading trucks
  if #unloadingtrucks > 0 then
    self:CheckUnloadingTrucks(unloadingtrucks)
  end
  
  -- check returningtrucks trucks
  if #returningtrucks > 0 then
    self:CheckReturningTrucks(returningtrucks)
  end
  
  -- check waiting targets
  if #waitingtargets > 0 then
    self:CheckWaitingTargets(waitingtargets)
  end
  
  self:__Monitor(self.monitor)
  return self
end

---
-- @param #AMMOTRUCK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #AMMOTRUCK.data Truckdata
-- @param #AMMOTRUCK.data Aridata
-- @return #AMMOTRUCK self 
function AMMOTRUCK:onafterRouteTruck(From, Event, To, Truckdata, Aridata)
  self:T({From, Event, To, Truckdata.name, Aridata.name})
  local truckdata = Truckdata -- #AMMOTRUCK.data
  local aridata = Aridata -- #AMMOTRUCK.data
  local tgtgrp = aridata.group
  local tgtzone = ZONE_GROUP:New(aridata.name,tgtgrp,30)
  local tgtcoord = tgtzone:GetRandomCoordinate(15)
  if self.hasarmygroup then
    local mission = AUFTRAG:NewONGUARD(tgtcoord)
    local oldmission = truckdata.group:GetMissionCurrent()
    if oldmission then oldmission:Cancel() end
    mission:SetTime(5)
    mission:SetTeleport(false)
    truckdata.group:AddMission(mission)
  elseif self.routeonroad then
    truckdata.group:RouteGroundOnRoad(tgtcoord,30)
  else
    truckdata.group:RouteGroundTo(tgtcoord,30)
  end
  truckdata.statusquo = AMMOTRUCK.State.DRIVING
  truckdata.targetgroup = tgtgrp
  truckdata.targetname = aridata.name
  truckdata.targetcoordinate = tgtcoord
  aridata.statusquo = AMMOTRUCK.State.WAITING
  aridata.timestamp = timer.getAbsTime()
  return self
end

---
-- @param #AMMOTRUCK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #AMMOTRUCK.data Truckdata
-- @return #AMMOTRUCK self 
 function AMMOTRUCK:onafterTruckUnloading(From, Event, To, Truckdata)
   local m = MESSAGE:New("Truck "..Truckdata.name.." unloading!",15,"AmmoTruck"):ToCoalitionIf(self.coalition,self.debug)
   local truck = Truckdata -- Functional.AmmoTruck#AMMOTRUCK.data
   local coord = truck.group:GetCoordinate()
   local heading = truck.group:GetHeading()
   heading = heading < 180 and (360-heading) or (heading - 180)
   local cid = self.coalition == coalition.side.BLUE and country.id.USA or country.id.RUSSIA
   cid = self.coalition == coalition.side.NEUTRAL and country.id.UN_PEACEKEEPERS or cid
   
   local ammo = {}
   for i=1,5 do
     ammo[i] = SPAWNSTATIC:NewFromType("ammo_cargo","Cargos",cid)
      :InitCoordinate(coord:Translate((15+((i-1)*4)),heading))
      :Spawn(0,"AmmoCrate-"..math.random(1,10000))
    end
    
    local function destroyammo(ammo)
      for _,_crate in pairs(ammo) do
        _crate:Destroy(false)
      end
    end
    
    local scheduler = SCHEDULER:New(nil,destroyammo,{ammo},self.waitingtime)
  
  -- one reload less
  if truck.reloads ~= -1 then
    truck.reloads = truck.reloads - 1
  end
  return self
 end 

---
-- @param #AMMOTRUCK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #AMMOTRUCK.data Truck
-- @return #AMMOTRUCK self 
function AMMOTRUCK:onafterTruckReturning(From, Event, To, Truck)
  self:T({From, Event, To, Truck.name})
  -- route home
  local truckdata = Truck -- #AMMOTRUCK.data
  local tgtzone = self.homezone
  local tgtcoord = tgtzone:GetRandomCoordinate()
  if self.hasarmygroup then
    local mission = AUFTRAG:NewONGUARD(tgtcoord)
    local oldmission = truckdata.group:GetMissionCurrent()
    if oldmission then oldmission:Cancel() end
    mission:SetTime(5)
    mission:SetTeleport(false)
    truckdata.group:AddMission(mission)
  elseif self.routeonroad then
    truckdata.group:RouteGroundOnRoad(tgtcoord,30,1,"Cone")
  else
    truckdata.group:RouteGroundTo(tgtcoord,30,"Cone",1)
  end
  return self
end

---
-- @param #AMMOTRUCK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #AMMOTRUCK self 
function AMMOTRUCK:onafterStop(From, Event, To)
  self:T({From, Event, To})
  return self
end
--- **Functional** - Autolase targets in the field.
--
-- ===
-- 
-- **AUOTLASE** - Autolase targets in the field.
-- 
-- ===
--
-- ## Missions:
--
-- None yet.
--
-- ===
-- 
-- **Main Features:**
--
--    * Detect and lase contacts automatically
--    * Targets are lased by threat priority order
--    * Use FSM events to link functionality into your scripts
--    * Easy setup
--
-- ===
-- 
--- Spot on!
-- 
-- ===
-- 
-- # 1 Autolase concept
-- 
-- * Detect and lase contacts automatically
-- * Targets are lased by threat priority order
-- * Use FSM events to link functionality into your scripts
-- * Set laser codes and smoke colors per Recce unit
-- * Easy set-up
-- 
-- # 2 Basic usage
-- 
-- ## 2.2 Set up a group of Recce Units:
-- 
--            local FoxSet = SET_GROUP:New():FilterPrefixes("Recce"):FilterCoalitions("blue"):FilterStart()
--            
-- ## 2.3 (Optional) Set up a group of pilots, this will drive who sees the F10 menu entry:
-- 
--            local Pilotset = SET_CLIENT:New():FilterCoalitions("blue"):FilterActive(true):FilterStart()
--            
-- ## 2.4 Set up and start Autolase:
-- 
--            local autolaser = AUTOLASE:New(FoxSet,coalition.side.BLUE,"Wolfpack",Pilotset)
--            
-- ## 2.5 Example - Using a fixed laser code and color for a specific Recce unit:
-- 
--            local recce = SPAWN:New("Reaper")
--              :InitDelayOff()
--              :OnSpawnGroup(
--                function (group)
--                  local unit = group:GetUnit(1)
--                  local name = unit:GetName()
--                  autolaser:SetRecceLaserCode(name,1688)
--                  autolaser:SetRecceSmokeColor(name,SMOKECOLOR.Red)
--                end
--              )
--              :InitCleanUp(60)
--              :InitLimit(1,0)
--              :SpawnScheduled(30,0.5)
--              
-- ## 2.6 Example - Inform pilots about events:
-- 
--            autolaser:SetNotifyPilots(true) -- defaults to true, also shown if debug == true
--            -- Note - message are shown to pilots in the #SET_CLIENT only if using the pilotset option, else to the coalition.
--
--
-- ### Author: **applevangelist**
-- @module Functional.Autolase
-- @image Designation.JPG
--
-- Date: 24 Oct 2021
-- Last Update: Mar 2025
--
--- Class AUTOLASE
-- @type AUTOLASE
-- @field #string ClassName
-- @field #string lid
-- @field #number verbose
-- @field #string alias
-- @field #boolean debug
-- @field #string version
-- @field Core.Set#SET_GROUP RecceSet
-- @field #table LaserCodes
-- @field #table playermenus
-- @field #boolean smokemenu
-- @field #boolean threatmenu
-- @field #number RoundingPrecision
-- @field #table smokeoffset
-- @field #boolean increasegroundawareness
-- @field #number MonitorFrequency
-- @extends Ops.Intel#INTEL

---
-- @field #AUTOLASE
AUTOLASE = {
  ClassName = "AUTOLASE",
  lid = "",
  verbose = 0,
  alias = "",
  debug = false,
  smokemenu = true,
  RoundingPrecision = 0,
  increasegroundawareness = false,
  MonitorFrequency = 30,
}

--- Laser spot info
-- @type AUTOLASE.LaserSpot
-- @field Core.Spot#SPOT laserspot 
-- @field Wrapper.Unit#UNIT lasedunit
-- @field Wrapper.Unit#UNIT lasingunit
-- @field #number lasercode
-- @field #string location
-- @field #number timestamp
-- @field #string unitname
-- @field #string reccename
-- @field #string unittype
-- @field Core.Point#COORDINATE coordinate


--- AUTOLASE class version.
-- @field #string version
AUTOLASE.version = "0.1.31"

-------------------------------------------------------------------
-- Begin Functional.Autolase.lua
-------------------------------------------------------------------

--- Constructor for a new Autolase instance.
-- @param #AUTOLASE self
-- @param Core.Set#SET_GROUP RecceSet Set of detecting and lasing units
-- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral".
-- @param #string Alias (Optional) An alias how this object is called in the logs etc.
-- @param Core.Set#SET_CLIENT PilotSet (Optional) Set of clients for precision bombing, steering menu creation. Leave nil for a coalition-wide F10 entry and display.
-- @return #AUTOLASE self 
function AUTOLASE:New(RecceSet, Coalition, Alias, PilotSet)
  BASE:T({RecceSet, Coalition, Alias, PilotSet})
  
  -- Inherit everything from BASE class.
  local self=BASE:Inherit(self, BASE:New()) -- #AUTOLASE
  
  if Coalition and type(Coalition)=="string" then
    if Coalition=="blue" then
      self.coalition=coalition.side.BLUE
    elseif Coalition=="red" then
      self.coalition=coalition.side.RED
    elseif Coalition=="neutral" then
      self.coalition=coalition.side.NEUTRAL
    else
      self:E("ERROR: Unknown coalition in AUTOLASE!")
    end
  end
  
  -- Set alias.
  if Alias then
    self.alias=tostring(Alias)
  else
    self.alias="Lion"  
    if self.coalition then
      if self.coalition==coalition.side.RED then
        self.alias="Wolf"
      elseif self.coalition==coalition.side.BLUE then
        self.alias="Fox"
      end
    end
  end 
  
  -- inherit from INTEL
  local self=BASE:Inherit(self, INTEL:New(RecceSet, Coalition, Alias)) -- #AUTOLASE
  
  self.RecceSet = RecceSet
  self.DetectVisual = true
  self.DetectOptical = true
  self.DetectRadar = true
  self.DetectIRST = true
  self.DetectRWR = true
  self.DetectDLINK = true
  self.LaserCodes = UTILS.GenerateLaserCodes()
  self.LaseDistance = 5000
  self.LaseDuration = 300
  self.GroupsByThreat = {}
  self.UnitsByThreat = {}
  self.RecceNames = {}
  self.RecceLaserCode = {}
  self.RecceSmokeColor = {}
  self.RecceUnitNames= {}
  self.maxlasing = 4
  self.CurrentLasing = {}
  self.lasingindex = 0
  self.deadunitnotes = {}
  self.usepilotset = false
  self.reporttimeshort = 10
  self.reporttimelong = 30
  self.smoketargets = false
  self.smokecolor = SMOKECOLOR.Red
  self.smokeoffset = nil
  self.notifypilots = true
  self.targetsperrecce = {}
  self.RecceUnits = {}
  self.forcecooldown = true
  self.cooldowntime = 60
  self.useSRS = false
  self.SRSPath = ""
  self.SRSFreq = 251
  self.SRSMod = radio.modulation.AM
  self.NoMenus = false
  self.minthreatlevel = 0
  self.blacklistattributes = {}
  self:SetLaserCodes( { 1688, 1130, 4785, 6547, 1465, 4578 } ) -- set self.LaserCodes
  self.playermenus = {}
  self.smokemenu = true
  self.threatmenu = true
  self.RoundingPrecision = 0
  self.increasegroundawareness = false
  self.MonitorFrequency = 30
  
  self:EnableSmokeMenu({Angle=math.random(0,359),Distance=math.random(10,20)})
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("AUTOLASE %s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown")
  
  -- Add FSM transitions.
  --                 From State  -->   Event        -->     To State
  self:AddTransition("*",             "Monitor",              "*")     -- Start FSM
  self:AddTransition("*",             "Lasing",               "*")     -- Lasing target
  self:AddTransition("*",             "TargetLost",           "*")     -- Lost target
  self:AddTransition("*",             "TargetDestroyed",      "*")     -- Target destroyed
  self:AddTransition("*",             "RecceKIA",             "*")     -- Recce KIA
  self:AddTransition("*",             "LaserTimeout",         "*")     -- Laser timed out
  self:AddTransition("*",             "Cancel",               "*")     -- Stop Autolase
  
  -- Menu Entry
  if PilotSet then
    self.usepilotset = true
    self.pilotset = PilotSet
    self:HandleEvent(EVENTS.PlayerEnterAircraft,self._EventHandler)
    --self:SetPilotMenu()
  end
  --self.SetPilotMenu()
  
  
  self:SetClusterAnalysis(false, false)
  
  self:__Start(2)
  self:__Monitor(math.random(5,10))
  
  return self
  
  ------------------------
  --- Pseudo Functions ---
  ------------------------
  
  --- Triggers the FSM event "Monitor".
  -- @function [parent=#AUTOLASE] Status
  -- @param #AUTOLASE self

  --- Triggers the FSM event "Monitor" after a delay.
  -- @function [parent=#AUTOLASE] __Status
  -- @param #AUTOLASE self
  -- @param #number delay Delay in seconds.
  
  --- Triggers the FSM event "Cancel".
  -- @function [parent=#AUTOLASE] Cancel
  -- @param #AUTOLASE self

  --- Triggers the FSM event "Cancel" after a delay.
  -- @function [parent=#AUTOLASE] __Cancel
  -- @param #AUTOLASE self
  -- @param #number delay Delay in seconds.
  
  --- On After "RecceKIA" event.
  -- @function [parent=#AUTOLASE] OnAfterRecceKIA
  -- @param #AUTOLASE self
  -- @param #string From The from state
  -- @param #string Event The event
  -- @param #string To The to state
  -- @param #string RecceName The lost Recce
    
  --- On After "TargetDestroyed" event.
  -- @function [parent=#AUTOLASE] OnAfterTargetDestroyed
  -- @param #AUTOLASE self
  -- @param #string From The from state
  -- @param #string Event The event
  -- @param #string To The to state
  -- @param #string UnitName The destroyed unit\'s name
  -- @param #string RecceName The Recce name lasing
  
  --- On After "TargetLost" event.
  -- @function [parent=#AUTOLASE] OnAfterTargetLost
  -- @param #AUTOLASE self
  -- @param #string From The from state
  -- @param #string Event The event
  -- @param #string To The to state
  -- @param #string UnitName The lost unit\'s name
  -- @param #string RecceName The Recce name lasing
  
  --- On After "LaserTimeout" event.
  -- @function [parent=#AUTOLASE] OnAfterLaserTimeout
  -- @param #AUTOLASE self
  -- @param #string From The from state
  -- @param #string Event The event
  -- @param #string To The to state
  -- @param #string UnitName The lost unit\'s name
  -- @param #string RecceName The Recce name lasing
  
  --- On After "Lasing" event.
  -- @function [parent=#AUTOLASE] OnAfterLasing
  -- @param #AUTOLASE self
  -- @param #string From The from state
  -- @param #string Event The event
  -- @param #string To The to state
  -- @param Functional.Autolase#AUTOLASE.LaserSpot LaserSpot The LaserSpot data table
  
end

-------------------------------------------------------------------
-- Helper Functions
-------------------------------------------------------------------

--- [User] When using Monitor, set the frequency here in which the report will appear
-- @param #AUTOLASE self
-- @param #number Seconds Run the report loop every number of seconds defined here.
-- @return #AUTOLASE self
function AUTOLASE:SetMonitorFrequency(Seconds)
  self.MonitorFrequency = Seconds or 30
  return self
end

--- [User] Set a table of possible laser codes.
-- Each new RECCE can select a code from this table, default is { 1688, 1130, 4785, 6547, 1465, 4578 }.
-- @param #AUTOLASE self
-- @param #list<#number> LaserCodes
-- @return #AUTOLASE self
function AUTOLASE:SetLaserCodes( LaserCodes )
  self.LaserCodes = ( type( LaserCodes ) == "table" ) and LaserCodes or { LaserCodes }
  return self
end

--- [User] Improve ground unit detection by using a zone scan and LOS check.
-- @param #AUTOLASE self
-- @return #AUTOLASE self 
function AUTOLASE:EnableImproveGroundUnitsDetection()
  self.increasegroundawareness = true
  return self
end

--- [User] Do not improve ground unit detection by using a zone scan and LOS check.
-- @param #AUTOLASE self
-- @return #AUTOLASE self 
function AUTOLASE:DisableImproveGroundUnitsDetection()
  self.increasegroundawareness = false
  return self
end

--- (Internal) Function to set pilot menu.
-- @param #AUTOLASE self
-- @return #AUTOLASE self 
function AUTOLASE:SetPilotMenu()
  if self.usepilotset then
    local pilottable = self.pilotset:GetSetObjects() or {}
    local grouptable = {}
    for _,_unit in pairs (pilottable) do
      local Unit = _unit -- Wrapper.Unit#UNIT
      if Unit and Unit:IsAlive() then
        local Group = Unit:GetGroup()
        local GroupName = Group:GetName() or "none"
        local unitname = Unit:GetName()
        if not grouptable[GroupName] == true then
          if self.playermenus[unitname] then self.playermenus[unitname]:Remove() end -- menus
          local lasetopm = MENU_GROUP:New(Group,"Autolase",nil)
          self.playermenus[unitname] = lasetopm
          local lasemenu = MENU_GROUP_COMMAND:New(Group,"Status",lasetopm,self.ShowStatus,self,Group,Unit)
          if self.smokemenu then
            local smoke = (self.smoketargets == true) and "off" or "on"
            local smoketext = string.format("Switch smoke targets to %s",smoke)
            local smokemenu = MENU_GROUP_COMMAND:New(Group,smoketext,lasetopm,self.SetSmokeTargets,self,(not self.smoketargets))
          end -- smokement
         if self.threatmenu then
             local threatmenutop = MENU_GROUP:New(Group,"Set min lasing threat",lasetopm)
             for i=0,10,2 do
              local text = "Threatlevel "..tostring(i)
              local threatmenu = MENU_GROUP_COMMAND:New(Group,text,threatmenutop,self.SetMinThreatLevel,self,i)
             end -- threatlevel
         end -- threatmenu
          for _,_grp in pairs(self.RecceSet.Set) do
            local grp = _grp -- Wrapper.Group#GROUP
            local unit = grp:GetUnit(1)
            --local name = grp:GetName()
            if unit and unit:IsAlive() then
              local name = unit:GetName()
              local mname = string.gsub(name,".%d+.%d+$","")
              local code = self:GetLaserCode(name)
              local unittop = MENU_GROUP:New(Group,"Change laser code for "..mname,lasetopm)
              for _,_code in pairs(self.LaserCodes) do
                local text = tostring(_code)
                if _code == code then text = text.."(*)" end
                local changemenu = MENU_GROUP_COMMAND:New(Group,text,unittop,self.SetRecceLaserCode,self,name,_code,true)
              end -- Codes
            end -- unit alive
          end -- Recceset
          grouptable[GroupName] = true
        end -- grouptable[GroupName] 
        --lasemenu:Refresh()
      end -- unit alive
    end -- pilot loop
  else
    if not self.NoMenus then
      self.Menu = MENU_COALITION_COMMAND:New(self.coalition,"Autolase",nil,self.ShowStatus,self)
    end
  end
  return self
end

--- (Internal) Event function for new pilots.
-- @param #AUTOLASE self
-- @param Core.Event#EVENTDATA EventData
-- @return #AUTOLASE self 
function AUTOLASE:_EventHandler(EventData)
  self:SetPilotMenu()
  return self
end

--- (User) Set minimum threat level for target selection, can be 0 (lowest) to 10 (highest).
-- @param #AUTOLASE self
-- @param #number Level Level used for filtering, defaults to 0. SAM systems and manpads have level 7 to 10, AAA level 6, MTBs and armoured vehicles level 3 to 5, APC, Artillery, Infantry and EWR level 1 to 2.
-- @return #AUTOLASE self
-- @usage Filter for level 3 and above:
--            `myautolase:SetMinThreatLevel(3)`
function AUTOLASE:SetMinThreatLevel(Level)
  local level = Level or 0
  if level < 0 or level > 10 then level = 0 end
  self.minthreatlevel = level
  return self 
end

--- (User) Set list of #UNIT level attributes that won't be lased. For list of attributes see [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_attributes) and [GitHub](https://github.com/mrSkortch/DCS-miscScripts/tree/master/ObjectDB) 
-- @param #AUTOLASE self
-- @param #table Attributes Table of #string attributes to blacklist. Can be handed over as a single #string.
-- @return #AUTOLASE self
-- @usage To exclude e.g. manpads from being lased:
-- 
--            `myautolase:AddBlackListAttributes("MANPADS")`
-- 
-- To exclude trucks and artillery:
-- 
--            `myautolase:AddBlackListAttributes({"Trucks","Artillery"})`
--            
function AUTOLASE:AddBlackListAttributes(Attributes)
  local attributes = Attributes
  if type(attributes) ~= "table" then
    attributes = {attributes}
  end
  for _,_attr in pairs(attributes) do
    table.insert(self.blacklistattributes,_attr)
  end
  return self
end

--- (Internal) Function to get a laser code by recce name
-- @param #AUTOLASE self
-- @param #string RecceName Unit(!) name of the Recce
-- @return #AUTOLASE self 
function AUTOLASE:GetLaserCode(RecceName)
  local code = 1688
  if self.RecceLaserCode[RecceName] == nil then
    code = self.LaserCodes[math.random(#self.LaserCodes)]
    self.RecceLaserCode[RecceName] = code
  else
    code = self.RecceLaserCode[RecceName]
  end
  return code
end

--- (Internal) Function to get a smoke color by recce name
-- @param #AUTOLASE self
-- @param #string RecceName Unit(!) name of the Recce
-- @return #AUTOLASE self 
function AUTOLASE:GetSmokeColor(RecceName)
  local color = self.smokecolor
  if self.RecceSmokeColor[RecceName] == nil then
    self.RecceSmokeColor[RecceName] = color
  else
    color = self.RecceSmokeColor[RecceName]
  end
  return color
end

--- (User) Function enable sending messages via SRS.
-- @param #AUTOLASE self
-- @param #boolean OnOff Switch usage on and off
-- @param #string Path Path to SRS TTS directory, e.g. C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio
-- @param #number Frequency Frequency to send, e.g. 243
-- @param #number Modulation Modulation i.e. radio.modulation.AM or radio.modulation.FM
-- @param #string Label (Optional) Short label to be used on the SRS Client Overlay
-- @param #string Gender (Optional) Defaults to "male"
-- @param #string Culture (Optional) Defaults to "en-US"
-- @param #number Port (Optional) Defaults to 5002
-- @param #string Voice (Optional) Use a specifc voice with the @{Sound.SRS#SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`.
-- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS.
-- @param #number Volume (Optional) Volume - between 0.0 (silent) and 1.0 (loudest)
-- @param #string PathToGoogleKey (Optional) Path to your google key if you want to use google TTS
-- @return #AUTOLASE self 
function AUTOLASE:SetUsingSRS(OnOff,Path,Frequency,Modulation,Label,Gender,Culture,Port,Voice,Volume,PathToGoogleKey)
  if OnOff then
    self.useSRS = true
    self.SRSPath = Path or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio"
    self.SRSFreq = Frequency or 271
    self.SRSMod = Modulation or radio.modulation.AM
    self.Gender = Gender or MSRS.gender or "male"
    self.Culture = Culture or MSRS.culture or "en-US"
    self.Port = Port or MSRS.port or 5002
    self.Voice = Voice 
    self.PathToGoogleKey = PathToGoogleKey
    self.Volume = Volume or 1.0
    self.Label = Label
    -- set up SRS
    self.SRS = MSRS:New(self.SRSPath,self.SRSFreq,self.SRSMod)
    self.SRS:SetCoalition(self.coalition)
    self.SRS:SetLabel(self.MenuName or self.Name)
    self.SRS:SetGender(self.Gender)
    self.SRS:SetCulture(self.Culture)
    self.SRS:SetPort(self.Port)
    self.SRS:SetVoice(self.Voice)
    self.SRS:SetCoalition(self.coalition)
    self.SRS:SetVolume(self.Volume)
    if self.PathToGoogleKey then
      self.SRS:SetProviderOptionsGoogle(PathToGoogleKey,PathToGoogleKey)
      self.SRS:SetProvider(MSRS.Provider.GOOGLE)
    end
    self.SRSQueue = MSRSQUEUE:New(self.alias)
  else
    self.useSRS = false
    self.SRS= nil
    self.SRSQueue = nil
  end
  return self
end

--- (User) Function set max lasing targets
-- @param #AUTOLASE self
-- @param #number Number Max number of targets to lase at once
-- @return #AUTOLASE self 
function AUTOLASE:SetMaxLasingTargets(Number)
  self.maxlasing = Number or 4
  return self
end

--- (Internal) Function set notify pilots on events
-- @param #AUTOLASE self
-- @param #boolean OnOff Switch messaging on (true) or off (false)
-- @return #AUTOLASE self 
function AUTOLASE:SetNotifyPilots(OnOff)
  self.notifypilots = OnOff and true
  return self
end

--- (User) Function to set a specific code to a Recce.
-- @param #AUTOLASE self
-- @param #string RecceName (Unit!) Name of the Recce
-- @param #number Code The lase code
-- @param #boolean Refresh If true, refresh menu entries
-- @return #AUTOLASE self 
function AUTOLASE:SetRecceLaserCode(RecceName, Code, Refresh)
  local code = Code or 1688
  self.RecceLaserCode[RecceName] = code
  if Refresh then
    self:SetPilotMenu()
    if self.notifypilots then
      if string.find(RecceName,"#") then
        RecceName = string.match(RecceName,"^(.*)#")
      end
      self:NotifyPilots(string.format("Code for %s set to: %d",RecceName,Code),15)
    end
  end
  return self
end

--- (User) Function to set a specific smoke color for a Recce.
-- @param #AUTOLASE self
-- @param #string RecceName (Unit!) Name of the Recce
-- @param #number Color The color, e.g. SMOKECOLOR.Red, SMOKECOLOR.Green etc
-- @return #AUTOLASE self 
function AUTOLASE:SetRecceSmokeColor(RecceName, Color)
  local color = Color or self.smokecolor
  self.RecceSmokeColor[RecceName] = color
  return self
end

--- (User) Function to force laser cooldown and cool down time
-- @param #AUTOLASE self
-- @param #boolean OnOff Switch cool down on (true) or off (false) - defaults to true
-- @param #number Seconds Number of seconds for cooldown - dafaults to 60 seconds
-- @return #AUTOLASE self 
function AUTOLASE:SetLaserCoolDown(OnOff, Seconds)
  self.forcecooldown = OnOff and true
  self.cooldowntime = Seconds or 60
  return self
end
  
--- (User) Function to set message show times.
-- @param #AUTOLASE self
-- @param #number long Longer show time
-- @param #number short Shorter show time
-- @return #AUTOLASE self 
function AUTOLASE:SetReportingTimes(long, short)
  self.reporttimeshort = short or 10
  self.reporttimelong = long or 30
  return self
end

--- (User) Function to set lasing distance in meters and duration in seconds
-- @param #AUTOLASE self
-- @param #number Distance (Max) distance for lasing in meters - default 5000 meters
-- @param #number Duration (Max) duration for lasing in seconds - default 300 secs
-- @return #AUTOLASE self 
function AUTOLASE:SetLasingParameters(Distance, Duration)
  self.LaseDistance = Distance or 5000
  self.LaseDuration = Duration or 300
  return self
end

--- (User) Function to set smoking of targets.
-- @param #AUTOLASE self
-- @param #boolean OnOff Switch smoking on or off
-- @param #number Color Smokecolor, e.g. SMOKECOLOR.Red
-- @return #AUTOLASE self 
function AUTOLASE:SetSmokeTargets(OnOff,Color)
  self.smoketargets = OnOff
  self.smokecolor = Color or SMOKECOLOR.Red
  local smktxt = OnOff == true and "on" or "off"
  local Message = "Smoking targets is now "..smktxt.."!"
  self:NotifyPilots(Message,10)
  return self
end

--- (User) Function to set rounding precision for BR distance output.
-- @param #AUTOLASE self
-- @param #number IDP Rounding precision before/after the decimal sign. Defaults to zero. Positive values round right of the decimal sign, negative ones left of the decimal sign. 
-- @return #AUTOLASE self 
function AUTOLASE:SetRoundingPrecsion(IDP)
  self.RoundingPrecision = IDP or 0
  return self
end

--- (User) Show the "Switch smoke target..." menu entry for pilots. On by default.
-- @param #AUTOLASE self
-- @param #table Offset (Optional) Define an offset for the smoke, i.e. not directly on the unit itself, angle is degrees and distance is meters. E.g. `autolase:EnableSmokeMenu({Angle=30,Distance=20})`
-- @return #AUTOLASE self 
function AUTOLASE:EnableSmokeMenu(Offset)
  self.smokemenu = true
  if Offset then
    self.smokeoffset = {}
    self.smokeoffset.Distance = Offset.Distance or math.random(10,20)
    self.smokeoffset.Angle = Offset.Angle or math.random(0,359)
  end
  return self
end

--- (User) Do not show the "Switch smoke target..." menu entry for pilots.
-- @param #AUTOLASE self
-- @return #AUTOLASE self 
function AUTOLASE:DisableSmokeMenu()
  self.smokemenu = false
  self.smokeoffset = nil
  return self
end

--- (User) Show the "Switch min threat lasing..." menu entry for pilots. On by default.
-- @param #AUTOLASE self
-- @return #AUTOLASE self 
function AUTOLASE:EnableThreatLevelMenu()
  self.threatmenu = true
  return self
end

--- (User) Do not show the "Switch min threat lasing..." menu entry for pilots.
-- @param #AUTOLASE self
-- @return #AUTOLASE self 
function AUTOLASE:DisableThreatLevelMenu()
  self.threatmenu = false
  return self
end

--- (Internal) Function to calculate line of sight.
-- @param #AUTOLASE self
-- @param Wrapper.Unit#UNIT Unit 
-- @return #number LOS Line of sight in meters
function AUTOLASE:GetLosFromUnit(Unit)
  local lasedistance = self.LaseDistance
  local unitheight = Unit:GetHeight()
  local coord = Unit:GetCoordinate()
  local landheight = coord:GetLandHeight()
  local asl = unitheight - landheight
  if asl > 100 then
    local absquare = lasedistance^2+asl^2
    lasedistance = math.sqrt(absquare)
  end
  return lasedistance
end

--- (Internal) Function to check on lased targets.
-- @param #AUTOLASE self
-- @return #AUTOLASE self
function AUTOLASE:CleanCurrentLasing()
  local lasingtable = self.CurrentLasing
  local newtable = {}
  local newreccecount = {}
  local lasing = 0
  
  for _ind,_entry in pairs(lasingtable) do
    local entry = _entry -- #AUTOLASE.LaserSpot
    if not newreccecount[entry.reccename] then
      newreccecount[entry.reccename] = 0
    end
  end
  
  for _,_recce in pairs (self.RecceSet:GetSetObjects()) do
    local recce = _recce --Wrapper.Group#GROUP
    if recce and recce:IsAlive() then
      local unit = recce:GetUnit(1)
      local name = unit:GetName()
      if not self.RecceUnits[name] then
        local isground = (unit and unit.IsGround) and unit:IsGround() or false
        self.RecceUnits[name] = { name=name, unit=unit, cooldown = false, timestamp = timer.getAbsTime(), isground=isground }
      end
    end
  end
  
  for _ind,_entry in pairs(lasingtable) do
    local entry = _entry -- #AUTOLASE.LaserSpot
    local valid = 0
    local reccedead = false
    local unitdead = false
    local lostsight = false
    local timeout = false
    local Tnow = timer.getAbsTime()
    -- check recce dead
    local recce = entry.lasingunit
    if recce and recce:IsAlive() then
      valid = valid + 1
    else
      reccedead = true
      self:__RecceKIA(2,entry.reccename)
    end
    -- check entry dead
    local unit = entry.lasedunit
    if unit and unit:IsAlive() == true then
      valid = valid + 1
    else
      unitdead = true
      if not self.deadunitnotes[entry.unitname] then
        self.deadunitnotes[entry.unitname] = true
        self:__TargetDestroyed(2,entry.unitname,entry.reccename)
      end
    end
    -- check entry out of sight
    if not reccedead and not unitdead then
      if self:CanLase(recce,unit) then
        valid = valid + 1
      else
        lostsight = true
        entry.laserspot:LaseOff()
        self:__TargetLost(2,entry.unitname,entry.reccename)
      end
    end
    -- check timed out
    local timestamp = entry.timestamp
    if Tnow - timestamp < self.LaseDuration and not lostsight then
      valid = valid + 1
    else
      timeout = true
      entry.laserspot:LaseOff()
      
      self.RecceUnits[entry.reccename].cooldown = true
      self.RecceUnits[entry.reccename].timestamp = timer.getAbsTime()
      
      if not lostsight then
        self:__LaserTimeout(2,entry.unitname,entry.reccename)
      end
    end
    if valid == 4 then
     self.lasingindex = self.lasingindex + 1
     newtable[self.lasingindex] = entry
     newreccecount[entry.reccename] = newreccecount[entry.reccename] + 1
     lasing = lasing + 1
    end
  end
  self.CurrentLasing = newtable
  self.targetsperrecce = newreccecount
  return lasing
end

--- (Internal) Function to show status.
-- @param #AUTOLASE self
-- @param Wrapper.Group#GROUP Group (Optional) show to a certain group
-- @param Wrapper.Unit#UNIT Unit (Optional) show to a certain unit
-- @return #AUTOLASE self
function AUTOLASE:ShowStatus(Group,Unit)
  local report = REPORT:New("Autolase")
  local reccetable = self.RecceSet:GetSetObjects()
  for _,_recce in pairs(reccetable) do
    if _recce and _recce:IsAlive() then
      local unit = _recce:GetUnit(1)
      local name = unit:GetName()
      if string.find(name,"#") then
        name = string.match(name,"^(.*)#")
      end
      local code = self:GetLaserCode(unit:GetName())
      report:Add(string.format("Recce %s has code %d",name,code))
      report:Add("---------------")
    end
  end
  report:Add(string.format("Lasing min threat level %d",self.minthreatlevel))
  report:Add("---------------")
  local lines = 0
  for _ind,_entry in pairs(self.CurrentLasing) do
    local entry = _entry -- #AUTOLASE.LaserSpot
    local reccename = entry.reccename
    if string.find(reccename,"#") then
      reccename = string.match(reccename,"^(.*)#")
    end
    local typename = entry.unittype
    local code = entry.lasercode
    local locationstring = entry.location
    local playername = nil
    if Unit and Unit:IsAlive() then
      playername = Unit:GetPlayerName()
    elseif Group and Group:IsAlive() then
     playername = Group:GetPlayerName()
    end
    if playername then
      local settings = _DATABASE:GetPlayerSettings(playername)
      if settings then
        self:T("Get Settings ok!")
        if settings:IsA2G_MGRS() then
          locationstring = entry.coordinate:ToStringMGRS(settings)
        elseif settings:IsA2G_LL_DMS() then
          locationstring = entry.coordinate:ToStringLLDMS(settings)
        elseif settings:IsA2G_LL_DDM() then
         locationstring = entry.coordinate:ToStringLLDDM(settings)
        elseif settings:IsA2G_BR() then
          -- attention this is the distance from the ASKING unit to target, not from RECCE to target!
          local startcoordinate = Unit:GetCoordinate() or Group:GetCoordinate()
          locationstring = entry.coordinate:ToStringBR(startcoordinate,settings,false,self.RoundingPrecision)
        end
      end
    end
    local text = string.format("+ %s lasing %s code %d\nat %s",reccename,typename,code,locationstring)
    report:Add(text)
    report:Add("---------------")
    lines = lines + 1
  end
  if lines == 0 then
    report:Add("No targets!")
    report:Add("---------------")
  end
  local reporttime = self.reporttimelong
  if lines == 0 then reporttime = self.reporttimeshort end
  if Unit and Unit:IsAlive() then
    local m = MESSAGE:New(report:Text(),reporttime,"Info"):ToUnit(Unit)
  elseif Group and Group:IsAlive() then
    local m = MESSAGE:New(report:Text(),reporttime,"Info"):ToGroup(Group)
  else
    local m = MESSAGE:New(report:Text(),reporttime,"Info"):ToCoalition(self.coalition)
  end
  return self
end

--- (Internal) Function to show messages.
-- @param #AUTOLASE self
-- @param #string Message The message to be sent
-- @param #number Duration Duration in seconds
-- @return #AUTOLASE self
function AUTOLASE:NotifyPilots(Message,Duration)
  if self.usepilotset then
    local pilotset = self.pilotset:GetSetObjects() --#table
    for _,_pilot in pairs(pilotset) do
      local pilot = _pilot -- Wrapper.Unit#UNIT
      if pilot and pilot:IsAlive() then
       local Group = pilot:GetGroup()
       local m = MESSAGE:New(Message,Duration,"Autolase"):ToGroup(Group)
      end
    end
  elseif not self.debug then
    local m = MESSAGE:New(Message,Duration,"Autolase"):ToCoalition(self.coalition)
  else
    local m = MESSAGE:New(Message,Duration,"Autolase"):ToAll()
  end
  if self.debug then self:I(Message) end
  return self
end

--- (User) Send messages via SRS.
-- @param #AUTOLASE self
-- @param #string Message The (short!) message to be sent, e.g. "Lasing target!"
-- @return #AUTOLASE self
-- @usage Step 1 - set up the radio basics **once** with
--            my_autolase:SetUsingSRS(true,"C:\\path\\SRS-Folder",251,radio.modulation.AM)
-- Step 2 - send a message, e.g.
--            function my_autolase:OnAfterLasing(From, Event, To, LaserSpot)
--                my_autolase:NotifyPilotsWithSRS("Reaper lasing new target!")
--            end
function AUTOLASE:NotifyPilotsWithSRS(Message)
  if self.useSRS then
   self.SRSQueue:NewTransmission(Message,nil,self.SRS,nil,2)
  end
  if self.debug then self:I(Message) end
  return self
end

--- (Internal) Function to check if a unit is already lased.
-- @param #AUTOLASE self
-- @param #string unitname Name of the unit to check
-- @return #boolean outcome True or false
function AUTOLASE:CheckIsLased(unitname)
  local outcome = false
  for _,_laserspot in pairs(self.CurrentLasing) do
    local spot = _laserspot -- #AUTOLASE.LaserSpot
    if spot.unitname == unitname then
      outcome = true
      break
    end
  end
  return outcome
end

--- (Internal) Function to check if a unit can be lased.
-- @param #AUTOLASE self
-- @param Wrapper.Unit#UNIT Recce The Recce #UNIT
-- @param Wrapper.Unit#UNIT Unit The lased #UNIT
-- @return #boolean outcome True or false
function AUTOLASE:CanLase(Recce,Unit)
  
  local function HasNoBlackListAttribute(Unit)
    local nogos = self.blacklistattributes or {}
    local having = true
    local unit = Unit -- Wrapper.Unit#UNIT
    for _,_attribute in pairs (nogos) do
      if unit:HasAttribute(_attribute) then
        having = false
        break
      end
    end
    return having
  end

  local canlase = false
  -- cooldown?
  if Recce and Recce:IsAlive() == true then
    local name = Recce:GetName()
    local cooldown = self.RecceUnits[name].cooldown and self.forcecooldown
    if cooldown then
      local Tdiff = timer.getAbsTime() - self.RecceUnits[name].timestamp
      if Tdiff < self.cooldowntime then
        return false
      else
        self.RecceUnits[name].cooldown = false
      end
    end
    -- calculate LOS
    local reccecoord = Recce:GetCoordinate()
    local unitcoord = Unit:GetCoordinate()
    local islos = reccecoord:IsLOS(unitcoord,2.5)
    -- calculate distance
    local distance = math.floor(reccecoord:Get3DDistance(unitcoord))
    local lasedistance = self:GetLosFromUnit(Recce)
    if distance <= lasedistance and islos and HasNoBlackListAttribute(Unit) then
      canlase = true
    end
  end
  return canlase
end

--- (Internal) Function to do a zone check per ground Recce and make found units and statics "known".
-- @param #AUTOLASE self
-- @return #AUTOLASE self 
function AUTOLASE:_Prescient()
  -- self.RecceUnits[name] = { name=name, unit=unit, cooldown = false, timestamp = timer.getAbsTime(), isground=isground }
  for _,_data in pairs(self.RecceUnits) do
    -- ground units only
    if _data.isground and _data.unit and _data.unit:IsAlive() then
      local unit = _data.unit -- Wrapper.Unit#UNIT
      local position = unit:GetCoordinate() -- Core.Point#COORDINATE
      local needsinit = false
      if position then
        local lastposition = unit:GetProperty("lastposition")
        -- property initiated?
        if not lastposition then
          unit:SetProperty("lastposition",position)
          lastposition = position
          needsinit = true
        end
        -- has moved?
        local dist = position:Get2DDistance(lastposition)
        -- refresh?
        local TNow = timer.getAbsTime()
        -- check
        if dist > 10 or needsinit==true or TNow - _data.timestamp > 29 then
          -- init scan objects
          local hasunits,hasstatics,_,Units,Statics = position:ScanObjects(self.LaseDistance,true,true,false)
          -- loop found units
          if hasunits then
            self:T(self.lid.."Checking possibly visible UNITs for Recce "..unit:GetName())
            for _,_target in pairs(Units) do -- Wrapper.Unit#UNIT object here
              local target = _target -- Wrapper.Unit#UNIT
              if target and target:GetCoalition() ~= self.coalition then
                if unit:IsLOS(target) and (not target:IsUnitDetected(unit))then
                  unit:KnowUnit(target,true,true)
                end
              end
            end
          end
          -- loop found statics
          if hasstatics then
           self:T(self.lid.."Checking possibly visible STATICs for Recce "..unit:GetName())
            for _,_static in pairs(Statics) do -- DCS static object here
              local static = STATIC:Find(_static)
              if static and static:GetCoalition() ~= self.coalition and static:GetCoordinate() then
                local IsLOS = position:IsLOS(static:GetCoordinate())
                if IsLOS then
                  unit:KnowUnit(static,true,true)
                end
              end
            end
          end
        end
      end
    end
  end
  return self
end

-------------------------------------------------------------------
-- FSM Functions
-------------------------------------------------------------------

--- (Internal) FSM Function for monitoring
-- @param #AUTOLASE self
-- @param #string From The from state
-- @param #string Event The event
-- @param #string To The to state
-- @return #AUTOLASE self
function AUTOLASE:onbeforeMonitor(From, Event, To)
  self:T({From, Event, To})
  if self.increasegroundawareness then
    self:_Prescient()
  end
  -- Check if group has detected any units.
  self:UpdateIntel()
  return self
end

--- (Internal) FSM Function for monitoring
-- @param #AUTOLASE self
-- @param #string From The from state
-- @param #string Event The event
-- @param #string To The to state
-- @return #AUTOLASE self
function AUTOLASE:onafterMonitor(From, Event, To)
  self:T({From, Event, To})

  -- Housekeeping
  local countlases = self:CleanCurrentLasing()
  
  self:SetPilotMenu()
  
  local detecteditems = self.Contacts or {} -- #table of Ops.Intel#INTEL.Contact
  local groupsbythreat = {}
  local report = REPORT:New("Detections")
  local lines = 0
  for _,_contact in pairs(detecteditems) do
    local contact = _contact -- Ops.Intel#INTEL.Contact
    local grp = contact.group
    local coord = contact.position
    local reccename = contact.recce or "none"
    local threat = contact.threatlevel or 0
    local reccegrp = UNIT:FindByName(reccename)
    if reccegrp then
      local reccecoord = reccegrp:GetCoordinate()
      local distance = math.floor(reccecoord:Get3DDistance(coord))
      local text = string.format("%s of %s | Distance %d km | Threatlevel %d",contact.attribute, contact.groupname, math.floor(distance/1000), contact.threatlevel)
      report:Add(text)
      self:T(text)
      if self.debug then self:I(text) end
      lines = lines  +  1
      -- sort out groups beyond sight
      local lasedistance = self:GetLosFromUnit(reccegrp)
      if grp:IsGround() and lasedistance >= distance and threat >= self.minthreatlevel then
        table.insert(groupsbythreat,{contact.group,contact.threatlevel})
        self.RecceNames[contact.groupname] = contact.recce
      end
    end
  end
  
  self.GroupsByThreat = groupsbythreat
  
  if self.verbose > 2 and lines > 0 then
    local m=MESSAGE:New(report:Text(),self.reporttimeshort,"Autolase"):ToAll()
  end
  
  table.sort(self.GroupsByThreat, function(a,b)
      local aNum = a[2] -- Coin value of a
      local bNum = b[2] -- Coin value of b
      return aNum > bNum -- Return their comparisons, < for ascending, > for descending
    end)
  
  -- build table of Units
  local unitsbythreat = {}
  for _,_entry in pairs(self.GroupsByThreat) do
    local group = _entry[1] -- Wrapper.Group#GROUP
    if group and group:IsAlive() then
      local units = group:GetUnits()
      local reccename = self.RecceNames[group:GetName()]
      for _,_unit in pairs(units) do
        local unit = _unit -- Wrapper.Unit#UNIT
        if unit and unit:IsAlive() then
          local threat = unit:GetThreatLevel()
          local coord = unit:GetCoordinate()
          if threat >= self.minthreatlevel then
            local unitname = unit:GetName()
            -- prefer radar units
            if unit:HasAttribute("RADAR_BAND1_FOR_ARM") or unit:HasAttribute("RADAR_BAND2_FOR_ARM") or unit:HasAttribute("Optical Tracker") then
              threat = 11
            end
            table.insert(unitsbythreat,{unit,threat})
            self.RecceUnitNames[unitname] = reccename
          end
        end
      end
    end
  end
  
  self.UnitsByThreat = unitsbythreat
  
  table.sort(self.UnitsByThreat, function(a,b)
      local aNum = a[2] -- Coin value of a
      local bNum = b[2] -- Coin value of b
      return aNum > bNum -- Return their comparisons, < for ascending, > for descending
    end)
  
  local unitreport = REPORT:New("Detected Units")
  
  local lines = 0 
  for _,_entry in pairs(self.UnitsByThreat) do
    local threat = _entry[2]
    local unit = _entry[1]
    local unitname = unit:GetName()
    local text = string.format("Unit %s | Threatlevel %d | Detected by %s",unitname,threat,self.RecceUnitNames[unitname])
    unitreport:Add(text)
    lines = lines + 1
    self:T(text)
    if self.debug then self:I(text) end
  end
  
  if self.verbose > 2 and lines > 0 then
    local m=MESSAGE:New(unitreport:Text(),self.reporttimeshort,"Autolase"):ToAll()
  end
  
  for _,_detectingunit in pairs(self.RecceUnits) do
    
    local reccename = _detectingunit.name
    local recce = _detectingunit.unit
    local reccecount = self.targetsperrecce[reccename] or 0
    local targets = 0
    for _,_entry in pairs(self.UnitsByThreat) do
      local unit = _entry[1] -- Wrapper.Unit#UNIT
      local unitname = unit:GetName()
      local canlase = self:CanLase(recce,unit)
      if targets+reccecount < self.maxlasing and not self:CheckIsLased(unitname) and unit:IsAlive() and canlase then
        targets = targets + 1
        local code = self:GetLaserCode(reccename)
        local spot = SPOT:New(recce)
        spot:LaseOn(unit,code,self.LaseDuration)
        local locationstring = unit:GetCoordinate():ToStringLLDDM()    
        if _SETTINGS:IsA2G_MGRS() then
          local precision = _SETTINGS:GetMGRS_Accuracy()
          local settings = {}
          settings.MGRS_Accuracy = precision
          locationstring = unit:GetCoordinate():ToStringMGRS(settings)
        elseif _SETTINGS:IsA2G_LL_DMS() then
          locationstring = unit:GetCoordinate():ToStringLLDMS(_SETTINGS)
        elseif _SETTINGS:IsA2G_BR() then
          locationstring = unit:GetCoordinate():ToStringBULLS(self.coalition,_SETTINGS)
        end
  
        local laserspot = { -- #AUTOLASE.LaserSpot
          laserspot = spot,
          lasedunit = unit,
          lasingunit = recce,
          lasercode = code,
          location = locationstring,
          timestamp = timer.getAbsTime(),
          unitname = unitname,
          reccename = reccename,
          unittype = unit:GetTypeName(),
          coordinate = unit:GetCoordinate(),
          }
       if self.smoketargets then
          local coord = unit:GetCoordinate()
          if self.smokeoffset then
            coord:Translate(self.smokeoffset.Distance,self.smokeoffset.Angle,true,true)
          end
          local color = self:GetSmokeColor(reccename)
          coord:Smoke(color)
       end
       self.lasingindex = self.lasingindex + 1 
       self.CurrentLasing[self.lasingindex] = laserspot
       self:__Lasing(2,laserspot)  
      end
    end
  end
  
  local nextloop = -self.MonitorFrequency or -30
  self:__Monitor(nextloop)
  return self
end

--- (Internal) FSM Function onbeforeRecceKIA
-- @param #AUTOLASE self
-- @param #string From The from state
-- @param #string Event The event
-- @param #string To The to state
-- @param #string RecceName The lost Recce
-- @return #AUTOLASE self
function AUTOLASE:onbeforeRecceKIA(From,Event,To,RecceName)
  self:T({From, Event, To, RecceName})
  if self.notifypilots or self.debug then
    if string.find(RecceName,"#") then
      RecceName = string.match(RecceName,"^(.*)#")
    end
    local text = string.format("Recce %s KIA!",RecceName)
    self:NotifyPilots(text,self.reporttimeshort)
  end
  return self
end

--- (Internal) FSM Function onbeforeTargetDestroyed
-- @param #AUTOLASE self
-- @param #string From The from state
-- @param #string Event The event
-- @param #string To The to state
-- @param #string UnitName The destroyed unit\'s name
-- @param #string RecceName The Recce name lasing
-- @return #AUTOLASE self
function AUTOLASE:onbeforeTargetDestroyed(From,Event,To,UnitName,RecceName)
  self:T({From, Event, To, UnitName, RecceName})
  if self.notifypilots or self.debug then
    local text = string.format("Unit %s destroyed! Good job!",UnitName)
    self:NotifyPilots(text,self.reporttimeshort)
  end
  return self
end

--- (Internal) FSM Function onbeforeTargetLost
-- @param #AUTOLASE self
-- @param #string From The from state
-- @param #string Event The event
-- @param #string To The to state
-- @param #string UnitName The lost unit\'s name
-- @param #string RecceName The Recce name lasing
-- @return #AUTOLASE self
function AUTOLASE:onbeforeTargetLost(From,Event,To,UnitName,RecceName)
  self:T({From, Event, To, UnitName,RecceName})
  if self.notifypilots or self.debug then
    if string.find(RecceName,"#") then
      RecceName = string.match(RecceName,"^(.*)#")
    end
    local text = string.format("%s lost sight of unit %s.",RecceName,UnitName)
    self:NotifyPilots(text,self.reporttimeshort)
  end
  return self
end

--- (Internal) FSM Function onbeforeLaserTimeout
-- @param #AUTOLASE self
-- @param #string From The from state
-- @param #string Event The event
-- @param #string To The to state
-- @param #string UnitName The lost unit\'s name
-- @param #string RecceName The Recce name lasing
-- @return #AUTOLASE self
function AUTOLASE:onbeforeLaserTimeout(From,Event,To,UnitName,RecceName)
  self:T({From, Event, To, UnitName,RecceName})
  if self.notifypilots or self.debug then
    if string.find(RecceName,"#") then
      RecceName = string.match(RecceName,"^(.*)#")
    end
    local text = string.format("%s laser timeout on unit %s.",RecceName,UnitName)
    self:NotifyPilots(text,self.reporttimeshort)
  end
  return self
end

--- (Internal) FSM Function onbeforeLasing
-- @param #AUTOLASE self
-- @param #string From The from state
-- @param #string Event The event
-- @param #string To The to state
-- @param Functional.Autolase#AUTOLASE.LaserSpot LaserSpot The LaserSpot data table
-- @return #AUTOLASE self
function AUTOLASE:onbeforeLasing(From,Event,To,LaserSpot)
  self:T({From, Event, To, LaserSpot.unittype})
  if self.notifypilots or self.debug then
    local laserspot = LaserSpot -- #AUTOLASE.LaserSpot
    local name = laserspot.reccename    if string.find(name,"#") then
        name = string.match(name,"^(.*)#")
    end
    local text = string.format("%s is lasing %s code %d\nat %s",name,laserspot.unittype,laserspot.lasercode,laserspot.location)
    self:NotifyPilots(text,self.reporttimeshort+5)
  end
  return self
end

--- (Internal) FSM Function onbeforeCancel
-- @param #AUTOLASE self
-- @param #string From The from state
-- @param #string Event The event
-- @param #string To The to state
-- @return #AUTOLASE self
function AUTOLASE:onbeforeCancel(From,Event,To)
  self:UnHandleEvent(EVENTS.PlayerEnterAircraft)
  self:__Stop(2)
  return self
end

-------------------------------------------------------------------
-- End Functional.Autolase.lua
-------------------------------------------------------------------
--- **Functional** - Base class that models processes to achieve goals involving a Zone and Cargo.
--
-- ===
-- 
-- ZONE_GOAL_CARGO models processes that have a Goal with a defined achievement involving a Zone and Cargo.  
-- Derived classes implement the ways how the achievements can be realized.
--
-- # Developer Note
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===
-- 
-- @module Functional.ZoneGoalCargo
-- @image MOOSE.JPG

do -- ZoneGoal

  -- @type ZONE_GOAL_CARGO
  -- @extends Functional.ZoneGoal#ZONE_GOAL


  --- Models processes that have a Goal with a defined achievement involving a Zone and Cargo.  
  -- Derived classes implement the ways how the achievements can be realized.
  -- 
  -- ## 1. ZONE_GOAL_CARGO constructor
  --   
  --   * @{#ZONE_GOAL_CARGO.New}(): Creates a new ZONE_GOAL_CARGO object.
  -- 
  -- ## 2. ZONE_GOAL_CARGO is a finite state machine (FSM).
  -- 
  -- ### 2.1 ZONE_GOAL_CARGO States
  --  
  --   * **Deployed**: The Zone has been captured by an other coalition.
  --   * **Airborne**: The Zone is currently intruded by an other coalition. There are units of the owning coalition and an other coalition in the Zone.
  --   * **Loaded**: The Zone is guarded by the owning coalition. There is no other unit of an other coalition in the Zone.
  --   * **Empty**: The Zone is empty. There is not valid unit in the Zone.
  -- 
  -- ### 2.2 ZONE_GOAL_CARGO Events
  -- 
  --   * **Capture**: The Zone has been captured by an other coalition.
  --   * **Attack**: The Zone is currently intruded by an other coalition. There are units of the owning coalition and an other coalition in the Zone.
  --   * **Guard**: The Zone is guarded by the owning coalition. There is no other unit of an other coalition in the Zone.
  --   * **Empty**: The Zone is empty. There is not valid unit in the Zone.
  --   
  -- ### 2.3 ZONE_GOAL_CARGO State Machine
  -- 
  -- @field #ZONE_GOAL_CARGO
  ZONE_GOAL_CARGO = {
    ClassName = "ZONE_GOAL_CARGO",
  }
  
  -- @field #table ZONE_GOAL_CARGO.States
  ZONE_GOAL_CARGO.States = {}
  
  --- ZONE_GOAL_CARGO Constructor.
  -- @param #ZONE_GOAL_CARGO self
  -- @param Core.Zone#ZONE Zone A @{Core.Zone} object with the goal to be achieved.
  -- @param #number Coalition The initial coalition owning the zone.
  -- @return #ZONE_GOAL_CARGO
  function ZONE_GOAL_CARGO:New( Zone, Coalition )
  
    local self = BASE:Inherit( self, ZONE_GOAL:New( Zone ) ) -- #ZONE_GOAL_CARGO
    self:F( { Zone = Zone, Coalition  = Coalition  } )

    self:SetCoalition( Coalition )

    do 
    
      --- Captured State Handler OnLeave for ZONE_GOAL_CARGO
      -- @function [parent=#ZONE_GOAL_CARGO] OnLeaveCaptured
      -- @param #ZONE_GOAL_CARGO self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
      -- @return #boolean
  
      --- Captured State Handler OnEnter for ZONE_GOAL_CARGO
      -- @function [parent=#ZONE_GOAL_CARGO] OnEnterCaptured
      -- @param #ZONE_GOAL_CARGO self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
  
    end
  
  
    do 
    
      --- Attacked State Handler OnLeave for ZONE_GOAL_CARGO
      -- @function [parent=#ZONE_GOAL_CARGO] OnLeaveAttacked
      -- @param #ZONE_GOAL_CARGO self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
      -- @return #boolean
  
      --- Attacked State Handler OnEnter for ZONE_GOAL_CARGO
      -- @function [parent=#ZONE_GOAL_CARGO] OnEnterAttacked
      -- @param #ZONE_GOAL_CARGO self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
  
    end

    do 
    
      --- Guarded State Handler OnLeave for ZONE_GOAL_CARGO
      -- @function [parent=#ZONE_GOAL_CARGO] OnLeaveGuarded
      -- @param #ZONE_GOAL_CARGO self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
      -- @return #boolean
  
      --- Guarded State Handler OnEnter for ZONE_GOAL_CARGO
      -- @function [parent=#ZONE_GOAL_CARGO] OnEnterGuarded
      -- @param #ZONE_GOAL_CARGO self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
  
    end
  

    do 
    
      --- Empty State Handler OnLeave for ZONE_GOAL_CARGO
      -- @function [parent=#ZONE_GOAL_CARGO] OnLeaveEmpty
      -- @param #ZONE_GOAL_CARGO self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
      -- @return #boolean
  
      --- Empty State Handler OnEnter for ZONE_GOAL_CARGO
      -- @function [parent=#ZONE_GOAL_CARGO] OnEnterEmpty
      -- @param #ZONE_GOAL_CARGO self
      -- @param #string From
      -- @param #string Event
      -- @param #string To
  
    end
  
    self:AddTransition( "*", "Guard", "Guarded" )
    
    --- Guard Handler OnBefore for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] OnBeforeGuard
    -- @param #ZONE_GOAL_CARGO self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Guard Handler OnAfter for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] OnAfterGuard
    -- @param #ZONE_GOAL_CARGO self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Guard Trigger for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] Guard
    -- @param #ZONE_GOAL_CARGO self
    
    --- Guard Asynchronous Trigger for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] __Guard
    -- @param #ZONE_GOAL_CARGO self
    -- @param #number Delay
    
    self:AddTransition( "*", "Empty", "Empty" )
    
    --- Empty Handler OnBefore for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] OnBeforeEmpty
    -- @param #ZONE_GOAL_CARGO self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Empty Handler OnAfter for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] OnAfterEmpty
    -- @param #ZONE_GOAL_CARGO self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Empty Trigger for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] Empty
    -- @param #ZONE_GOAL_CARGO self
    
    --- Empty Asynchronous Trigger for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] __Empty
    -- @param #ZONE_GOAL_CARGO self
    -- @param #number Delay
    
    
    self:AddTransition( {  "Guarded", "Empty" }, "Attack", "Attacked" )
  
    --- Attack Handler OnBefore for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] OnBeforeAttack
    -- @param #ZONE_GOAL_CARGO self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Attack Handler OnAfter for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] OnAfterAttack
    -- @param #ZONE_GOAL_CARGO self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Attack Trigger for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] Attack
    -- @param #ZONE_GOAL_CARGO self
    
    --- Attack Asynchronous Trigger for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] __Attack
    -- @param #ZONE_GOAL_CARGO self
    -- @param #number Delay
    
    self:AddTransition( { "Guarded", "Attacked", "Empty" }, "Capture", "Captured" )
   
    --- Capture Handler OnBefore for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] OnBeforeCapture
    -- @param #ZONE_GOAL_CARGO self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Capture Handler OnAfter for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] OnAfterCapture
    -- @param #ZONE_GOAL_CARGO self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Capture Trigger for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] Capture
    -- @param #ZONE_GOAL_CARGO self
    
    --- Capture Asynchronous Trigger for ZONE_GOAL_CARGO
    -- @function [parent=#ZONE_GOAL_CARGO] __Capture
    -- @param #ZONE_GOAL_CARGO self
    -- @param #number Delay

    return self
  end
  

  --- Set the owning coalition of the zone.
  -- @param #ZONE_GOAL_CARGO self
  -- @param #number Coalition
  function ZONE_GOAL_CARGO:SetCoalition( Coalition )
    self.Coalition = Coalition
  end
  
  
  --- Get the owning coalition of the zone.
  -- @param #ZONE_GOAL_CARGO self
  -- @return #number Coalition.
  function ZONE_GOAL_CARGO:GetCoalition()
    return self.Coalition
  end

  
  --- Get the owning coalition name of the zone.
  -- @param #ZONE_GOAL_CARGO self
  -- @return #string Coalition name.
  function ZONE_GOAL_CARGO:GetCoalitionName()
  
    if self.Coalition == coalition.side.BLUE then
      return "Blue"
    end
    
    if self.Coalition == coalition.side.RED then
      return "Red"
    end
    
    if self.Coalition == coalition.side.NEUTRAL then
      return "Neutral"
    end
    
    return ""
  end


  function ZONE_GOAL_CARGO:IsGuarded()
  
    local IsGuarded = self.Zone:IsAllInZoneOfCoalition( self.Coalition )
    self:F( { IsGuarded = IsGuarded } )
    return IsGuarded
  end


  function ZONE_GOAL_CARGO:IsEmpty()
  
    local IsEmpty = self.Zone:IsNoneInZone()
    self:F( { IsEmpty = IsEmpty } )
    return IsEmpty
  end


  function ZONE_GOAL_CARGO:IsCaptured()
  
    local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition )
    self:F( { IsCaptured = IsCaptured } )
    return IsCaptured
  end
  
  
  function ZONE_GOAL_CARGO:IsAttacked()
  
    local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition )
    self:F( { IsAttacked = IsAttacked } )
    return IsAttacked
  end
  
  

  --- Mark.
  -- @param #ZONE_GOAL_CARGO self
  function ZONE_GOAL_CARGO:Mark()
  
    local Coord = self.Zone:GetCoordinate()
    local ZoneName = self:GetZoneName()
    local State = self:GetState()
    
    if self.MarkRed and self.MarkBlue then
      self:F( { MarkRed = self.MarkRed, MarkBlue = self.MarkBlue } )
      Coord:RemoveMark( self.MarkRed )
      Coord:RemoveMark( self.MarkBlue )
    end
    
    if self.Coalition == coalition.side.BLUE then
      self.MarkBlue = Coord:MarkToCoalitionBlue( "Guard Zone: " .. ZoneName .. "\nStatus: " .. State )  
      self.MarkRed = Coord:MarkToCoalitionRed( "Capture Zone: " .. ZoneName .. "\nStatus: " .. State )  
    else
      self.MarkRed = Coord:MarkToCoalitionRed( "Guard Zone: " .. ZoneName .. "\nStatus: " .. State )  
      self.MarkBlue = Coord:MarkToCoalitionBlue( "Capture Zone: " .. ZoneName .. "\nStatus: " .. State )  
    end
  end

  --- Bound.
  -- @param #ZONE_GOAL_CARGO self
  function ZONE_GOAL_CARGO:onenterGuarded()
  
    --self:GetParent( self ):onenterGuarded()
  
    if self.Coalition == coalition.side.BLUE then
      --elf.ProtectZone:BoundZone( 12, country.id.USA )
    else
      --self.ProtectZone:BoundZone( 12, country.id.RUSSIA )
    end
    
    self:Mark()
    
  end
  
  function ZONE_GOAL_CARGO:onenterCaptured()
  
    --self:GetParent( self ):onenterCaptured()

    local NewCoalition = self.Zone:GetCoalition()
    self:F( { NewCoalition = NewCoalition } )
    self:SetCoalition( NewCoalition )
  
    self:Mark()
  end
  
  
  function ZONE_GOAL_CARGO:onenterEmpty()

    --self:GetParent( self ):onenterEmpty()
  
    self:Mark()
  end
  
  
  function ZONE_GOAL_CARGO:onenterAttacked()
  
    --self:GetParent( self ):onenterAttacked()
  
    self:Mark()
  end


  --- When started, check the Coalition status.
  -- @param #ZONE_GOAL_CARGO self
  function ZONE_GOAL_CARGO:onafterGuard()
  
    --self:F({BASE:GetParent( self )})
    --BASE:GetParent( self ).onafterGuard( self )
  
    if not self.SmokeScheduler then
      self.SmokeScheduler = self:ScheduleRepeat( 1, 1, 0.1, nil, self.StatusSmoke, self )
    end
    if not self.ScheduleStatusZone then
      self.ScheduleStatusZone = self:ScheduleRepeat( 15, 15, 0.1, nil, self.StatusZone, self )
    end
  end


  function ZONE_GOAL_CARGO:IsCaptured()
  
    local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition )
    self:F( { IsCaptured = IsCaptured } )
    return IsCaptured
  end
  
  
  function ZONE_GOAL_CARGO:IsAttacked()
  
    local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition )
    self:F( { IsAttacked = IsAttacked } )
    return IsAttacked
  end
  
  --- Check status Coalition ownership.
  -- @param #ZONE_GOAL_CARGO self
  function ZONE_GOAL_CARGO:StatusZone()
  
    local State = self:GetState()
    self:F( { State = self:GetState() } )
  
    self.Zone:Scan()
  
    if State ~= "Guarded" and self:IsGuarded() then
      self:Guard()
    end
    
    if State ~= "Empty" and self:IsEmpty() then  
      self:Empty()
    end

    if State ~= "Attacked" and self:IsAttacked() then
      self:Attack()
    end
    
    if State ~= "Captured" and self:IsCaptured() then  
      self:Capture()
    end
    
  end
  
end

----- **Functional** - TIRESIAS - manages AI behaviour (OPTIMIZED VERSION).

----  ===

---  The @{#TIRESIAS} class is working in the back to keep your large-scale ground units in check.
--
-- -- Features:
--
-- * Designed to keep CPU and Network usage lower on missions with a lot of ground units.
-- * Does not affect ships to keep the Navy guys happy.
-- * Does not affect OpsGroup type groups.
-- * Distinguishes between SAM groups, AAA groups and other ground groups.
-- * Exceptions can be defined to keep certain actions going.
-- * Works coalition-independent in the back
-- * Easy setup.
--
-- ===
--
-- ## Optimizations Applied:
--
-- * Cached frequently used functions and constants
-- * Reduced string concatenations and formatting
-- * Optimized loop structures and conditions
-- * Pre-allocated tables where possible
-- * Reduced function call overhead
-- * Improved memory management
--
----  ===
--
----  #-- Author : **applevangelist ** (Optimized by AI)

---
-- @module Functional.Tiresias
-- @image Functional.Tiresias.jpg

--- Last Update: July 2025

--- **TIRESIAS** class, extends Core.Base#BASE
--  @type TIRESIAS
--  @field #string ClassName
--  @field #boolean debug
--  @field #string version
--  @field #number Interval
--  @field Core.Set#SET_GROUP GroundSet
--  @field #number Coalition
--  @field Core.Set#SET_GROUP VehicleSet
--  @field Core.Set#SET_GROUP AAASet
--  @field Core.Set#SET_GROUP SAMSet
--  @field Core.Set#SET_GROUP ExceptionSet
--  @field Core.Set#SET_OPSGROUP OpsGroupSet
--  @field #number AAARange
--  @field #number HeloSwitchRange
--  @field #number PlaneSwitchRange
--  @field Core.Set#SET_GROUP FlightSet
--  @field #boolean SwitchAAA
--  @field #string lid
--  @field #table _cached_zones
--  @field #table _cached_groupsets
--  @extends Core.Fsm#FSM

---
--  @type TIRESIAS.Data
--  @field #string type
--  @field #number range
--  @field #boolean invisible
--  @field #boolean AIOff
--  @field #boolean exception

---
-- *Tiresias, Greek demi-god and shapeshifter, blinded by the Gods, works as oracle for you.* (Wiki)
--
--  ===
--
--  ## TIRESIAS Concept
--
--   * Designed to keep CPU and Network usage lower on missions with a lot of ground units.
--   * Does not affect ships to keep the Navy guys happy.
--   * Does not affect OpsGroup type groups.
--   * Distinguishes between SAM groups, AAA groups and other ground groups.
--   * Exceptions can be defined in SET_GROUP objects to keep certain actions going.
--   * Works coalition-independent in the back
--   * Easy setup.
--
-- ## Setup  
-- --  Setup is a one-liner:
--
--            local blinder = TIRESIAS:New()
--
-- --  Optionally you can set up exceptions, e.g. for convoys driving around
--
--           local exceptionset = SET_GROUP:New():FilterCoalitions(" red" ):FilterPrefixes(" Convoy" ):FilterStart()
--           local blinder = TIRESIAS:New()
--           blinder:AddExceptionSet(exceptionset)
--
-- --  Options
--
--           --  Setup different radius for activation around helo and airplane groups (applies to AI and humans)
--           blinder:SetActivationRanges(10,25) --  defaults are 10, and 25
--
--           --  Setup engagement ranges for AAA (non-advanced SAM units like Flaks etc) and if you want them to be AIOff
--           blinder:SetAAARanges(60,true) --  defaults are 60, and true
--
---
--  @field #TIRESIAS
TIRESIAS = {
  ClassName         = "TIRESIAS",
  debug             = true,
  version           = " 0.0.7-OPT" ,
  Interval          = 20,
  GroundSet         = nil,
  VehicleSet        = nil,
  AAASet            = nil,
  SAMSet            = nil,
  ExceptionSet      = nil,
  AAARange          = 60, --  60%
  HeloSwitchRange   = 10, --  NM
  PlaneSwitchRange  = 25, --  NM
  SwitchAAA         = true,
  _cached_zones     = {}, --  Cache for zone objects
  _cached_groupsets = {}, --  Cache for group_set objects
  }

---
-- [USER] Create a new Tiresias object and start it up.
--  @param #TIRESIAS self
--  @return #TIRESIAS self
function TIRESIAS:New()
  
  -- Inherit everything from FSM class.
  local self = BASE:Inherit(self, FSM:New()) -- #TIRESIAS
  
  --- FSM Functions ---
  
  -- Start State.
  self:SetStartState("Stopped")
  
  -- Add FSM transitions.
  --                 From State  -->   Event            -->     To State
  self:AddTransition("Stopped",       "Start",                   "Running")     -- Start FSM.
  self:AddTransition("*",             "Status",                  "*")           -- TIRESIAS status update.
  self:AddTransition("*",             "Stop",                    "Stopped")     -- Stop FSM.
  
  self.ExceptionSet = nil --SET_GROUP:New():Clear(false)
  self._cached_zones = {} -- Initialize zone cache
  
  self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler)
  
  -- Cache the log identifier to avoid string concatenation in loops
  self.lid = "TIRESIAS " .. self.version .. " | "
  
  self:I(self.lid .. "Managing ground groups!")
  
  --- Triggers the FSM event "Stop". Stops TIRESIAS and all its event handlers.
  -- @function [parent=#TIRESIAS] Stop
  -- @param #TIRESIAS self
  
  --- Triggers the FSM event "Stop" after a delay. Stops TIRESIAS and all its event handlers.
  -- @function [parent=#TIRESIAS] __Stop
  -- @param #TIRESIAS self
  -- @param #number delay Delay in seconds.
  
  --- Triggers the FSM event "Start". Starts TIRESIAS and all its event handlers. Note - `:New()` already starts the instance.
  -- @function [parent=#TIRESIAS] Start
  -- @param #TIRESIAS self
  
  --- Triggers the FSM event "Start" after a delay. Starts TIRESIAS and all its event handlers. Note - `:New()` already starts the instance.
  -- @function [parent=#TIRESIAS] __Start
  -- @param #TIRESIAS self
  -- @param #number delay Delay in seconds.  
  
  self:__Start(1)
  
  return self
end

-----

--- 
--  Helper Functions
---

--- [USER] Set activation radius for Helos and Planes in Nautical Miles.
--  @param #TIRESIAS self
--  @param #number HeloMiles Radius around a Helicopter in which AI ground units will be activated. Defaults to 10NM.
--  @param #number PlaneMiles Radius around an Airplane in which AI ground units will be activated. Defaults to 25NM.
--  @return #TIRESIAS self
function TIRESIAS:SetActivationRanges(HeloMiles, PlaneMiles)
  self.HeloSwitchRange = HeloMiles or 10
  self.PlaneSwitchRange = PlaneMiles or 25
  --  Clear zone cache when ranges change
  self._cached_zones = {}
  return self
end

---[USER] Set AAA Ranges - AAA equals non-SAM systems which qualify as AAA in DCS world.
--  @param #TIRESIAS self
--  @param #number FiringRange The engagement range that AAA units will be set to. Can be 0 to 100 (percent). Defaults to 60.
--  @param #boolean SwitchAAA Decide if these system will have their AI switched off, too. Defaults to true.
--  @return #TIRESIAS self
function TIRESIAS:SetAAARanges(FiringRange, SwitchAAA)
  self.AAARange = FiringRange or 60
  self.SwitchAAA = (SwitchAAA == false) and false or true
  return self
end

--- [USER] Add a SET_GROUP of GROUP objects as exceptions. Can be done multiple times. Does **not** work work for GROUP objects spawned into the SET after start, i.e. the groups need to exist in the game already.
--  @param #TIRESIAS self
--  @param Core.Set#SET_GROUP Set to add to the exception list.
--  @return #TIRESIAS self
function TIRESIAS:AddExceptionSet(Set)
  self:T(self.lid .. " AddExceptionSet" )
  
  if not self.ExceptionSet then
   self.ExceptionSet = SET_GROUP:New()
  end
  
  local exceptions = self.ExceptionSet

  --  Cache the exception data structure for reuse
  local exception_data = {
    type = " Exception" ,
    exception = true,
    }

  Set:ForEachGroupAlive(
    function(grp)
      local inAAASet = self.AAASet:IsIncludeObject(grp)
      local inVehSet = self.VehicleSet:IsIncludeObject(grp)
      local inSAMSet = self.SAMSet:IsIncludeObject(grp)
      if grp:IsGround() and (not grp.Tiresias) and (not inAAASet) and (not inVehSet) and (not inSAMSet) then
        grp.Tiresias = exception_data
        exceptions:AddGroup(grp, true)
        BASE:T(" TIRESIAS: Added exception group: "  .. grp:GetName())
      end
    end
    )  
  return self
end

--- [INTERNAL] Filter Function - Optimized with cached calls
--  @param Wrapper.Group#GROUP Group
--  @return #boolean isin
function TIRESIAS._FilterNotAAA(Group)
  local grp = Group --  Wrapper.Group#GROUP
  --  Cache method calls to reduce overhead
  local is_air = grp:IsAir()
  local is_ship = grp:IsShip()
  local is_AAA = grp:IsAAA()
  if is_air or grp:IsShip() then --  air or ship - no AAA
    return true --  keep in SET
  end
  return not is_AAA --  remove AAA, keep others
end

--- [INTERNAL] Filter Function - Optimized with cached calls
--  @param Wrapper.Group#GROUP Group
--  @return #boolean isin
function TIRESIAS._FilterNotSAM(Group)
  local grp = Group --  Wrapper.Group#GROUP
  --  Cache method calls to reduce overhead
  local is_air = grp:IsGround()
  local is_ship = grp:IsShip()
  local is_SAM = grp:IsSAM()
  if is_air or grp:IsShip() then
    return true --  keep in SET
  end
  return not is_SAM --  remove SAM, keep others
end

--- [INTERNAL] Filter Function - Optimized with cached calls
--  @param Wrapper.Group#GROUP Group
--  @return #boolean isin
function TIRESIAS._FilterAAA(Group)
  local grp = Group --  Wrapper.Group#GROUP
  --  Cache method calls to reduce overhead
  local is_ground = grp:IsGround()
  if (not is_ground) or grp:IsShip() then
    return false --  not AAA
  end
  return grp:IsAAA() --  only AAA
end

--- [INTERNAL] Filter Function - Optimized with cached calls
--  @param Wrapper.Group#GROUP Group
--  @return #boolean isin
function TIRESIAS._FilterSAM(Group)
  local grp = Group --  Wrapper.Group#GROUP
  --  Cache method calls to reduce overhead
  local is_ground = grp:IsGround()
  if (not is_ground) or grp:IsShip() then
    return false --  not SAM
  end
  return grp:IsSAM() --  only SAM
end

--- [INTERNAL] Init Groups - Optimized with reduced function calls
--  @param #TIRESIAS self
--  @return #TIRESIAS self
function TIRESIAS:_InitGroups()
self:T(self.lid .. " _InitGroups" )

--  Cache frequently used values
local EngageRange = self.AAARange
local SwitchAAA = self.SwitchAAA

--  Pre-create data structures to avoid repeated table creation
local aaa_data_template = {
  type = " AAA" ,
  invisible = true,
  range = EngageRange,
  exception = false,
  AIOff = SwitchAAA,
  }

local vehicle_data_template = {
  type = " Vehicle" ,
  invisible = true,
  AIOff = true,
  exception = false,
  }

local sam_data_template = {
  type = " SAM" ,
  invisible = true,
  exception = false,
  }

--- AAA - Optimized loop
self.AAASet:ForEachGroupAlive(
  function(grp)
    local tiresias_data = grp.Tiresias
    if not tiresias_data then
      grp:OptionEngageRange(EngageRange)
      grp:SetCommandInvisible(true)
      if SwitchAAA then
        grp:SetAIOff()
        grp:EnableEmission(false)
      end
      grp.Tiresias = aaa_data_template
    elseif not tiresias_data.exception == true then
      if not tiresias_data.invisible == true then
        grp:SetCommandInvisible(true)
        tiresias_data.invisible = true
        if SwitchAAA == true then
          grp:SetAIOff()
          grp:EnableEmission(false)
          tiresias_data.AIOff = true
        end
      end
    end
  end
  )

--- Vehicles - Optimized loop
self.VehicleSet:ForEachGroupAlive(
  function(grp)
    local tiresias_data = grp.Tiresias
    if not tiresias_data then
      grp:SetAIOff()
      grp:SetCommandInvisible(true)
      grp.Tiresias = vehicle_data_template
    elseif not tiresias_data.exception == true then
      if not tiresias_data.invisible then
        grp:SetCommandInvisible(true)
        grp:SetAIOff()
        tiresias_data.invisible = true
        tiresias_data.AIOff = true
      end
    end  
  end
  )

--- SAM - Optimized loop
self.SAMSet:ForEachGroupAlive(
  function(grp)
    local tiresias_data = grp.Tiresias
    if not tiresias_data then
      grp:SetCommandInvisible(true)
      grp.Tiresias = sam_data_template
    elseif not tiresias_data.exception == true then
      if not tiresias_data.invisible then
        grp:SetCommandInvisible(true)
        tiresias_data.invisible = true
      end
    end
  end
  )

return self
end

--- [INTERNAL] Event handler function - Optimized
--  @param #TIRESIAS self
--  @param Core.Event#EVENTDATA EventData
--  @return #TIRESIAS self
function TIRESIAS:_EventHandler(EventData)
  self:T(string.format(" %s Event = %d" , self.lid, EventData.id))
  
  local event = EventData --  Core.Event#EVENTDATA
  if event.id == EVENTS.PlayerEnterAircraft or event.id == EVENTS.PlayerEnterUnit then
    local _group = event.IniGroup
    if _group and _group:IsAlive() then
      --  Cache the radius calculation
      local radius = _group:IsHelicopter() and self.HeloSwitchRange or self.PlaneSwitchRange
      self:_SwitchOnGroups(_group, radius)
    end
  end
  return self
end

--- [INTERNAL] Switch Groups Behaviour - Optimized with zone caching
--  @param #TIRESIAS self
--  @param Wrapper.Group#GROUP group
--  @param #number radius Radius in NM
--  @return #TIRESIAS self
function TIRESIAS:_SwitchOnGroups(group, radius)
  self:T(self.lid .. " _SwitchOnGroups "  .. group:GetName() .. "  Radius "  .. radius .. "  NM" )
  
  --  Use cached zones to reduce object creation
  local group_name = group:GetName()
  local cache_key = group_name .. " _"  .. radius
  local zone = self._cached_zones[cache_key]
  local ground = self._cached_groupsets[cache_key]
  
  if not zone then
    zone = ZONE_GROUP:New(" Zone-"  .. group_name, group, UTILS.NMToMeters(radius))
    self._cached_zones[cache_key] = zone
  else
    --  Update zone center to current group position
    zone:UpdateFromGroup(group)
  end
  
  if not ground then
    ground = SET_GROUP:New():FilterCategoryGround():FilterZones({zone}):FilterOnce()
    self._cached_groupsets[cache_key] = ground
  else
    ground:FilterZones({zone},true):FilterOnce()
  end
  
  local count = ground:CountAlive()
  
  if self.debug then
    self:I(string.format(" There are %d groups around this plane or helo!" , count))
  end
  
  if count > 0 then
  --  Cache values outside the loop
  local SwitchAAA = self.SwitchAAA
  local group_coalition = group:GetCoalition()
  
  ground:ForEachGroupAlive(
    function(grp)
      local tiresias_data = grp.Tiresias
      if grp:GetCoalition() ~= group_coalition 
         and tiresias_data 
         and tiresias_data.type 
         and not tiresias_data.exception == true then
        
        -- Make group visible if invisible
        if tiresias_data.invisible == true then
          grp:SetCommandInvisible(false)
          tiresias_data.invisible = false
        end
        
        -- Handle AI activation based on type
        local grp_type = tiresias_data.type
        if grp_type == "Vehicle" and tiresias_data.AIOff == true then
          grp:SetAIOn()
          tiresias_data.AIOff = false
        elseif SwitchAAA == true and grp_type == "AAA" and tiresias_data.AIOff == true then
          grp:SetAIOn()
          grp:EnableEmission(true)
          tiresias_data.AIOff = false
        end
      else
        BASE:T("TIRESIAS - This group " .. tostring(grp:GetName()) .. " has not been initialized or is an exception!")
      end
    end
  )
  
  end
  return self
end

-----

--- 
--  FSM Functions
----

--- [INTERNAL] FSM Function - Optimized initialization
--  @param #TIRESIAS self
--  @param #string From
--  @param #string Event
--  @param #string To
--  @return #TIRESIAS self
function TIRESIAS:onafterStart(From, Event, To)
self:T({From, Event, To})

--  Create sets with optimized filters
local VehicleSet = SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterNotAAA):FilterFunction(TIRESIAS._FilterNotSAM):FilterStart()
local AAASet = SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterAAA):FilterStart()
local SAMSet = SET_GROUP:New():FilterCategoryGround():FilterFunction(TIRESIAS._FilterSAM):FilterStart()
local OpsGroupSet = SET_OPSGROUP:New():FilterActive(true):FilterStart()
self.FlightSet = SET_GROUP:New():FilterCategories({" plane" ," helicopter" }):FilterStart()

--  Cache frequently used values
local EngageRange = self.AAARange
local SwitchAAA = self.SwitchAAA
local ExceptionSet = self.ExceptionSet

--  Pre-create data templates to reduce object creation
local exception_data = {
  type = " Exception" ,
  exception = true,
  }

local vehicle_data = {
  type = " Vehicle" ,
  invisible = true,
  AIOff = true,
  exception = false,
  }

local aaa_data = {
  type = " AAA" ,
  invisible = true,
  range = EngageRange,
  exception = false,
  AIOff = SwitchAAA,
  }

local sam_data = {
  type = " SAM" ,
  invisible = true,
  exception = false,
  }

if ExceptionSet then  
  function ExceptionSet:OnAfterAdded(From, Event, To, ObjectName, Object)
    BASE:I(" TIRESIAS: EXCEPTION Object Added: "  .. Object:GetName())
    if Object and Object:IsAlive() then
      Object.Tiresias = exception_data
      Object:SetAIOn()
      Object:SetCommandInvisible(false)
      Object:EnableEmission(true)
    end
  end

  -- Process existing OpsGroups more efficiently
  local OGS = OpsGroupSet:GetAliveSet()
  for _, _OG in pairs(OGS or {}) do
    local OG = _OG -- Ops.OpsGroup#OPSGROUP
    local grp = OG:GetGroup()  
    ExceptionSet:AddGroup(grp, true)
  end
  
  function OpsGroupSet:OnAfterAdded(From, Event, To, ObjectName, Object)
    local grp = Object:GetGroup()
    ExceptionSet:AddGroup(grp, true)
  end
end

--  Optimized event handlers with pre-created data objects
function VehicleSet:OnAfterAdded(From, Event, To, ObjectName, Object)
  BASE:T(" TIRESIAS: VEHICLE Object Added: "  .. Object:GetName())
  if Object and Object:IsAlive() then
    Object:SetAIOff()
    Object:SetCommandInvisible(true)
    Object.Tiresias = vehicle_data
  end
end
  
function AAASet:OnAfterAdded(From, Event, To, ObjectName, Object)
  if Object and Object:IsAlive() then
    BASE:I(" TIRESIAS: AAA Object Added: "  .. Object:GetName())
    Object:OptionEngageRange(EngageRange)
    Object:SetCommandInvisible(true)
    if SwitchAAA then
      Object:SetAIOff()
      Object:EnableEmission(false)
    end
    Object.Tiresias = aaa_data
  end
end
  
function SAMSet:OnAfterAdded(From, Event, To, ObjectName, Object)
  if Object and Object:IsAlive() then
    BASE:T(" TIRESIAS: SAM Object Added: "  .. Object:GetName())
    Object:SetCommandInvisible(true)
    Object.Tiresias = sam_data
  end
end
  
  --  Store references
  self.VehicleSet = VehicleSet
  self.AAASet = AAASet
  self.SAMSet = SAMSet
  self.OpsGroupSet = OpsGroupSet
  
  self:_InitGroups()
  
  self:__Status(1)  
  return self
end

--- [INTERNAL] FSM Function
--  @param #TIRESIAS self
--  @param #string From
--  @param #string Event
--  @param #string To
--  @return #TIRESIAS self
function TIRESIAS:onbeforeStatus(From, Event, To)
  self:T({From, Event, To})
  return self:GetState() ~= " Stopped" 
end

--- [INTERNAL] FSM Function - Optimized status processing
--  @param #TIRESIAS self
--  @param #string From
--  @param #string Event
--  @param #string To
--  @return #TIRESIAS self
function TIRESIAS:onafterStatus(From, Event, To)
  self:T({From, Event, To})
  
  if self.debug then
    local count = self.VehicleSet:CountAlive()
    local AAAcount = self.AAASet:CountAlive()
    local SAMcount = self.SAMSet:CountAlive()
    self:I(string.format(" Overall: %d | Vehicles: %d | AAA: %d | SAM: %d" ,
    count + AAAcount + SAMcount, count, AAAcount, SAMcount))
  end
  
  self:_InitGroups()
  
  --  Process flight groups more efficiently
  local flight_count = self.FlightSet:CountAlive()
  if flight_count > 0 then
    local Set = self.FlightSet:GetAliveSet()
    --  Cache range values outside loop
    local helo_range = self.HeloSwitchRange
    local plane_range = self.PlaneSwitchRange
    
    for _, _plane in pairs(Set or {}) do
      local plane = _plane -- Wrapper.Group#GROUP
      local radius = plane:IsHelicopter() and helo_range or plane_range
      self:_SwitchOnGroups(plane, radius)
    end
  end
  
  if self:GetState() ~= " Stopped"  then
    self:__Status(self.Interval)
  end
  
  return self
end

--- [INTERNAL] FSM Function
--  @param #TIRESIAS self
--  @param #string From
--  @param #string Event
--  @param #string To
--  @return #TIRESIAS self
function TIRESIAS:onafterStop(From, Event, To)
  self:T({From, Event, To})
  self:UnHandleEvent(EVENTS.PlayerEnterAircraft)
  --  Clear zone cache on stop to free memory
  self._cached_zones = {}
  return self
end

-----
----  End
-------- **Functional** - Stratego.
--
-- **Main Features:**
--
--    * Helper class for mission designers to support classic capture-the-base scenarios.
--    * Creates a network of possible connections between bases (airbases, FARPs, Ships), Ports (defined as zones) and POIs (defined as zones).
--    * Assigns a strategic value to each of the resulting nodes.
--    * Can create a list of targets for your next mission move, both strategic and consolidation targets.
--    * Can be used with budgets to limit the target selection.
--    * Highly configureable.
--
-- ===
--
-- ### Author: **applevangelist**
-- 
-- @module Functional.Stratego
-- @image Functional.Stratego.png
-- Last Update May 2024


---
--- **STRATEGO** class, extends Core.Base#BASE
-- @type STRATEGO
-- @field #string ClassName
-- @field #boolean debug
-- @field #string version
-- @field #number portweight
-- @field #number POIweight
-- @field #number maxrunways
-- @field #number coalition
-- @field #table colors
-- @field #table airbasetable
-- @field #table nonconnectedab
-- @field #table easynames
-- @field #number maxdist
-- @field #table disttable
-- @field #table routexists
-- @field #number routefactor
-- @field #table OpsZones
-- @field #number NeutralBenefit
-- @field #number Budget
-- @field #boolean usebudget
-- @field #number CaptureUnits
-- @field #number CaptureThreatlevel
-- @field #table CaptureObjectCategories
-- @field #boolean ExcludeShips
-- @field Core.Zone#ZONE StrategoZone
-- @extends Core.Base#BASE
-- @extends Core.Fsm#FSM


--- *If you see what is right and fail to act on it, you lack courage* --- Confucius
--
-- ===
--
-- # The STRATEGO Concept
--
-- STRATEGO is a helper class for mission designers. 
-- The basic idea is to create a network of nodes (bases) on the map, which each have a number of connections
-- to other nodes. The base value of each node is the number of runways of the base (the bigger the more important), or in the case of Ports and POIs, the assigned value points.
-- The strategic value of each base is determined by the number of routes going in and out of the node, where connections between more strategic nodes add a higher value to the
-- strategic value than connections to less valueable nodes.
-- 
-- ## Setup
-- 
-- Setup is map indepent and works automatically. All airbases, FARPS, and ships on the map are considered. **Note:** Later spawned objects are not considered at the moment.
--          
--          -- Setup and start STRATGEO for the blue side, maximal node distance is 100km
--          local Bluecher = STRATEGO:New("Bluecher",coalition.side.BLUE,100)
--          -- use budgets
--          Bluecher:SetUsingBudget(true,500)
--          -- draw on the map
--          Bluecher:SetDebug(true,true,true)
--          -- Start
--          Bluecher:Start()
-- 
-- ### Helper
-- 
-- @{#STRATEGO.SetWeights}(): Set weights for nodes and routes to determine their importance.
-- 
-- ### Hint
--          
-- Each node is its own @{Ops.OpsZone#OPSZONE} object to manage the coalition alignment of that node and how it can be conquered.
-- 
-- ### Distance
-- 
-- The node distance factor determines how many connections are there on the map. The smaller the lighter is the resulting net. The higher the thicker it gets, with more strategic options.
-- Play around with the distance to get an optimal map for your scenario.
-- 
-- One some maps, e.g. Syria, lower distance factors can create "islands" of unconnected network parts on the map. FARPs and POIs can bridge those gaps, or you can add routes manually.
-- 
-- @{#STRATEGO.AddRoutesManually}(): Add a route manually.
-- 
-- ## Ports and POIs
-- 
-- Ports and POIs are @{Core.Zone#ZONE} objects on the map with specfic values. Zones with the keywords "Port" or "POI" in the name are automatically considered at setup time.
--  
-- ## Get next possible targets
-- 
-- There are two types of possible target lists, strategic and consolidation. Targets closer to the start node are chosen as possible targets. 
-- 
-- 
--      * Strategic targets are of higher or equal base weight from a given start point. Can also be obtained for the whole net.
--      * Consoliation targets are of smaller or equal base weight from a given start point. Can also be obtained for the whole net.  
-- 
-- 
--  @{#STRATEGO.UpdateNodeCoalitions}(): Update alls node's coalition data before takign a decision.   
--  @{#STRATEGO.FindStrategicTargets}(): Find a list of possible strategic targets in the network of the enemy or neutral coalition.   
--  @{#STRATEGO.FindConsolidationTargets}(): Find a list of possible strategic targets in the network of the enemy or neutral coalition.     
--  @{#STRATEGO.FindAffordableStrategicTarget}(): When using budgets, find **one** strategic target you can afford.   
--  @{#STRATEGO.FindAffordableConsolidationTarget}(): When using budgets, find **one** consolidation target you can afford.   
--  @{#STRATEGO.FindClosestStrategicTarget}(): Find closest strategic target from a given start point.   
--  @{#STRATEGO.FindClosestConsolidationTarget}(): Find closest consolidation target from a given start point.   
--  @{#STRATEGO.GetHighestWeightNodes}(): Get a list of the nodes with the highest weight. Coalition independent.   
--  @{#STRATEGO.GetNextHighestWeightNodes}(): Get a list of the nodes a weight less than the give parameter. Coalition independent.   
-- 
--  
-- **How** you act on these suggestions is again totally up to your mission design.
--  
-- ## Using budgets
--  
--  Set up STRATEGO to use budgets to limit the target selection. **How** your side actually earns budgets is up to your mission design. However, when using budgets, a target will only be selected,
--  when you have more budget points available than the value points of the targeted base.
--  
--          -- use budgets
--          Bluecher:SetUsingBudget(true,500)
--  
-- ### Helpers:
-- 
--  
--  @{#STRATEGO.GetBudget}(): Get the current budget points.   
--  @{#STRATEGO.AddBudget}(): Add a number of budget points.   
--  @{#STRATEGO.SubtractBudget}(): Subtract a number of budget points.   
--  @{#STRATEGO.SetNeutralBenefit}(): Set neutral benefit, i.e. how many points it is cheaper to decide for a neutral vs an enemy node when taking decisions.   
-- 
--  
-- ## Functions to query a node's data
--  
--  
--  @{#STRATEGO.GetNodeBaseWeight}(): Get the base weight of a node by its name.   
--  @{#STRATEGO.GetNodeCoalition}(): Get the COALITION of a node by its name.   
--  @{#STRATEGO.GetNodeType}(): Get the TYPE of a node by its name.   
--  @{#STRATEGO.GetNodeZone}(): Get the ZONE of a node by its name.   
--  @{#STRATEGO.GetNodeOpsZone}(): Get the OPSZONE of a node by its name.   
--  @{#STRATEGO.GetNodeCoordinate}(): Get the COORDINATE of a node by its name.   
--  @{#STRATEGO.IsAirbase}(): Check if the TYPE of a node is AIRBASE.   
--  @{#STRATEGO.IsPort}(): Check if the TYPE of a node is PORT.   
--  @{#STRATEGO.IsPOI}(): Check if the TYPE of a node is POI.   
--  @{#STRATEGO.IsFARP}(): Check if the TYPE of a node is FARP.   
--  @{#STRATEGO.IsShip}(): Check if the TYPE of a node is SHIP.   
--  
--  
-- ## Various
--  
--  
--  @{#STRATEGO.FindNeighborNodes}(): Get neighbor nodes of a named node.   
--  @{#STRATEGO.FindRoute}(): Find a route between two nodes.   
--  @{#STRATEGO.SetCaptureOptions}(): Set how many units of which minimum threat level are needed to capture one node (i.e. the underlying OpsZone).   
--  @{#STRATEGO.SetDebug}(): Set debug and draw options.   
--  @{#STRATEGO.SetStrategoZone}(): Set a zone to restrict STRATEGO analytics to, can be any kind of ZONE Object.
--
--
-- ## Visualisation example code for the Syria map:
-- 
--            local Bluecher = STRATEGO:New("Bluecher",coalition.side.BLUE,100)
--            Bluecher:SetDebug(true,true,true)
--            Bluecher:Start()
-- 
--            Bluecher:AddRoutesManually(AIRBASE.Syria.Beirut_Rafic_Hariri,AIRBASE.Syria.Larnaca)
--            Bluecher:AddRoutesManually(AIRBASE.Syria.Incirlik,AIRBASE.Syria.Hatay)
--            Bluecher:AddRoutesManually(AIRBASE.Syria.Incirlik,AIRBASE.Syria.Minakh)
--            Bluecher:AddRoutesManually(AIRBASE.Syria.King_Hussein_Air_College,AIRBASE.Syria.H4)
--            Bluecher:AddRoutesManually(AIRBASE.Syria.Sayqal,AIRBASE.Syria.At_Tanf)
-- 
--            local route = Bluecher:FindRoute(AIRBASE.Syria.Rosh_Pina,AIRBASE.Syria.Incirlik,5,true)
--            UTILS.PrintTableToLog(route,1)  
--          
-- @field #STRATEGO
STRATEGO = {
  ClassName = "STRATEGO",
  debug = false,
  drawzone = false,
  markzone = false,
  version = "0.3.1",
  portweight = 3,
  POIweight = 1,
  maxrunways = 3,
  coalition = nil,
  colors = nil,
  airbasetable = {},
  nonconnectedab = {},
  easynames = {},
  maxdist = 150, -- km
  disttable = {},
  routexists = {},
  routefactor = 5,
  OpsZones = {},
  NeutralBenefit = 100,
  Budget = 0,
  usebudget = false,
  CaptureUnits = 3,
  CaptureThreatlevel = 1,
  CaptureObjectCategories = {Object.Category.UNIT},
  ExcludeShips = true,
}

---
-- @type STRATEGO.Data
-- @field #string name
-- @field #number baseweight
-- @field #number weight
-- @field #number coalition
-- @field #boolean port
-- @field Core.Zone#ZONE_RADIUS zone,
-- @field Core.Point#COORDINATE coord
-- @field #string type
-- @field Ops.OpsZone#OPSZONE opszone
-- @field #number connections

---
-- @type STRATEGO.DistData
-- @field #string start
-- @field #string target
-- @field #number dist 

---
-- @type STRATEGO.Target
-- @field #string name
-- @field #number dist
-- @field #number points
-- @field #number coalition
-- @field #string coalitionname  
-- @field Core.Point#COORDINATRE coordinate

---
-- @type STRATEGO.Type
-- @field #string AIRBASE
-- @field #string PORT
-- @field #string POI
-- @field #string FARP
-- @field #string SHIP
STRATEGO.Type = {
  AIRBASE = "AIRBASE",
  PORT = "PORT",
  POI = "POI",
  FARP = "FARP",
  SHIP = "SHIP",
}

--- [USER] Create a new STRATEGO object and start it up.
-- @param #STRATEGO self
-- @param #string Name Name of the Adviser.
-- @param #number Coalition Coalition, e.g. coalition.side.BLUE.
-- @param #number MaxDist Maximum distance of a single route in kilometers, defaults to 150.
-- @return #STRATEGO self 
function STRATEGO:New(Name,Coalition,MaxDist)
  -- Inherit everything from FSM class.
  local self = BASE:Inherit(self, FSM:New()) -- #STRATEGO
  
  self.coalition = Coalition
  self.coalitiontext = UTILS.GetCoalitionName(Coalition)
  self.name = Name or "Hannibal"
  
  self.maxdist = MaxDist or 150 -- km
  self.disttable = {}
  self.routexists = {}
  self.ExcludeShips = true
  
  self.lid = string.format("STRATEGO %s %s | ",self.name,self.version) 
  
  self.bases = SET_AIRBASE:New():FilterOnce()
  self.ports = SET_ZONE:New():FilterPrefixes("Port"):FilterOnce()
  self.POIs = SET_ZONE:New():FilterPrefixes("POI"):FilterOnce()
  
  self.colors = {
      [1] = {0,1,0}, -- green
      [2] = {1,0,0}, -- red
      [3] = {0,0,1}, -- blue
      [4] = {1,0.65,0}, -- orange
    }
  
    -- Start State.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  --                 From State  -->   Event        -->      To State
  self:AddTransition("Stopped",       "Start",               "Running")     -- Start FSM.
  self:AddTransition("*",             "Update",              "*")           -- Start FSM.
  self:AddTransition("*",             "NodeEvent",           "*")           -- Start FSM.
  self:AddTransition("Running",       "Stop",                "Stopped")     -- Start FSM.
  
  ------------------------
  --- Pseudo Functions ---
  ------------------------
  
  --- Triggers the FSM event "Start". Starts the STRATEGO. Initializes parameters and starts event handlers.
  -- @function [parent=#STRATEGO] Start
  -- @param #STRATEGO self

  --- Triggers the FSM event "Start" after a delay. Starts the STRATEGO. Initializes parameters and starts event handlers.
  -- @function [parent=#STRATEGO] __Start
  -- @param #STRATEGO self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the STRATEGO and all its event handlers.
  -- @function [parent=#STRATEGO] Stop
  -- @param #STRATEGO self

  --- Triggers the FSM event "Stop" after a delay. Stops the STRATEGO and all its event handlers.
  -- @function [parent=#STRATEGO] __Stop
  -- @param #STRATEGO self
  -- @param #number delay Delay in seconds.
  
  --- FSM Function OnAfterNodeEvent. A node changed coalition.
  -- @function [parent=#STRATEGO] OnAfterNodeEvent
  -- @param #STRATEGO self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Ops.OpsZone#OPSZONE OpsZone The OpsZone triggering the event.
  -- @param #number Coalition The coalition of the new owner.
  -- @return #STRATEGO self
  
  return self
end

--- [INTERNAL] FSM function for initial setup and getting ready.
-- @param #STRATEGO self
-- @return #STRATEGO self
function STRATEGO:onafterStart(From,Event,To)
  self:T(self.lid.."Start")
  self:AnalyseBases()
  self:AnalysePOIs(self.ports,self.portweight,"PORT")
  self:AnalysePOIs(self.POIs,self.POIweight,"POI")
  
  for i=self.maxrunways,1,-1 do
  self:AnalyseRoutes(i,i*self.routefactor,self.colors[(i%3)+1],i)
  end
  self:AnalyseUnconnected(self.colors[4])
      
  self:I(self.lid.."Advisory ready.")
  
  self:__Update(180)
  return self
end

--- [INTERNAL] Update knot association
-- @param #STRATEGO self
-- @return #STRATEGO self
function STRATEGO:onafterUpdate(From,Event,To)
  self:T(self.lid.."Update")
  
  self:UpdateNodeCoalitions()
  
  if self:GetState() == "Running" then
    self:__Update(180)
  end
  
  return self
end

--- [USER] Set up usage of budget and set an initial budget in points.
-- @param #STRATEGO self
-- @param #boolean Usebudget If true, use budget for advisory calculations.
-- @param #number StartBudget Initial budget to be used, defaults to 500.
function STRATEGO:SetUsingBudget(Usebudget,StartBudget)
  self:T(self.lid.."SetUsingBudget")
  self.usebudget = Usebudget
  self.Budget = StartBudget
  return self
end

--- [USER] Set debugging.
-- @param #STRATEGO self
-- @param #boolean Debug If true, switch on debugging.
-- @param #boolean DrawZones If true, draw the OpsZones on the F10 map.
-- @param #boolean MarkZones if true, mark the OpsZones on the F10 map (with further information).
function STRATEGO:SetDebug(Debug,DrawZones,MarkZones)
  self:T(self.lid.."SetDebug")
  self.debug = Debug
  self.drawzone = DrawZones
  self.markzone = MarkZones
  return self
end

--- [USER] Restrict Stratego to analyse this zone only.
-- @param #STRATEGO self
-- @param Core.Zone#ZONE Zone The Zone to restrict Stratego to, can be any kind of ZONE Object.
-- @return #STRATEGO self
function STRATEGO:SetStrategoZone(Zone)
  self.StrategoZone = Zone
  return self
end

--- [USER] Set weights for nodes and routes to determine their importance.
-- @param #STRATEGO self
-- @param #number MaxRunways Set the maximum number of runways the big (equals strategic) airbases on the map have. Defaults to 3. The weight of an airbase node hence equals the number of runways.
-- @param #number PortWeight Set what weight a port node has. Defaults to 3.
-- @param #number POIWeight Set what weight a POI node has. Defaults to 1.
-- @param #number RouteFactor Defines which weight each route between two defined nodes gets: Weight * RouteFactor.
-- @return #STRATEGO self
function STRATEGO:SetWeights(MaxRunways,PortWeight,POIWeight,RouteFactor)
  self:T(self.lid.."SetWeights")
  self.portweight = PortWeight or 3
  self.POIweight = POIWeight or 1
  self.maxrunways = MaxRunways or 3
  self.routefactor = RouteFactor or 5
  return self
end

--- [USER] Set neutral benefit, i.e. how many points it is cheaper to decide for a neutral vs an enemy node when taking decisions.
-- @param #STRATEGO self
-- @param #number NeutralBenefit Pointsm defaults to 100.
-- @return #STRATEGO self
function STRATEGO:SetNeutralBenefit(NeutralBenefit)
  self:T(self.lid.."SetNeutralBenefit")
  self.NeutralBenefit = NeutralBenefit or 100
  return self
end

--- [USER] Set how many units of which minimum threat level are needed to capture one node (i.e. the underlying OpsZone).
-- @param #STRATEGO self
-- @param #number CaptureUnits Number of units needed, defaults to three.
-- @param #number CaptureThreatlevel Threat level needed, can be 0..10, defaults to one.
-- @param #table CaptureCategories Table of object categories which can capture a node, defaults to `{Object.Category.UNIT}`.
-- @return #STRATEGO self
function STRATEGO:SetCaptureOptions(CaptureUnits,CaptureThreatlevel,CaptureCategories)
  self:T(self.lid.."SetCaptureOptions")
  self.CaptureUnits = CaptureUnits or 3
  self.CaptureThreatlevel = CaptureThreatlevel or 1
  self.CaptureObjectCategories = CaptureCategories or {Object.Category.UNIT}
  return self
end

--- [INTERNAL] Analyse airbase setups
-- @param #STRATEGO self
-- @return #STRATEGO self
function STRATEGO:AnalyseBases()
  self:T(self.lid.."AnalyseBases")
  local colors = self.colors
  local debug = self.debug
  local airbasetable = self.airbasetable
  local nonconnectedab = self.nonconnectedab
  local easynames = self.easynames
  local zone = self.StrategoZone -- Core.Zone#ZONE_POLYGON
  
  -- find bases with >= 1 runways
  self.bases:ForEach(
    function(afb)
      local ab = afb -- Wrapper.Airbase#AIRBASE
      local abvec2 = ab:GetVec2()
      if self.ExcludeShips and ab:IsShip() then return end
      if zone ~= nil then
        if not zone:IsVec2InZone(abvec2) then
          return
        end
      end
      local abname = ab:GetName()
      local runways = ab:GetRunways()
      local numrwys = #runways
      if numrwys >= 1 then numrwys = numrwys * 0.5 end    
      local abzone = ab:GetZone()
      if not abzone then 
        abzone = ZONE_RADIUS:New(abname,ab:GetVec2(),500)
      end
      local coa = ab:GetCoalition()
      if coa == nil then return end -- Spawned FARPS issue - these have no tangible data
      coa = coa+1
      local abtype = STRATEGO.Type.AIRBASE
      if ab:IsShip() then
        numrwys = 1
        abtype = STRATEGO.Type.SHIP
      end
      if ab:IsHelipad() then 
        numrwys = 1
        abtype = STRATEGO.Type.FARP
      end
      local coord = ab:GetCoordinate()
      if debug then
        abzone:DrawZone(-1,colors[coa],1,colors[coa],0.3,1)
        coord:TextToAll(tostring(numrwys),-1,{0,0,0},1,colors[coa],0.3,20)
      end
      local opszone = self:GetNewOpsZone(abname,coa-1)
      local tbl = {
        name = abname,
        baseweight = numrwys,
        weight = 0,
        coalition = coa-1,
        port = false,
        zone = abzone,
        coord = coord,
        type = abtype,
        opszone = opszone,
        connections = 0,
      }
      airbasetable[abname] = tbl
      nonconnectedab[abname] = true
      local name = string.gsub(abname,"[%p%s]",".")
      easynames[name]=abname
    end
  )
  return self
end

--- [INTERNAL] Update node coalitions
-- @param #STRATEGO self
-- @return #STRATEGO self
function STRATEGO:UpdateNodeCoalitions()
  self:T(self.lid.."UpdateNodeCoalitions")
  local newtable = {}
  for _id,_data in pairs(self.airbasetable) do
    local data = _data -- #STRATEGO.Data
    if data.type == STRATEGO.Type.AIRBASE or data.type == STRATEGO.Type.FARP or data.type == STRATEGO.Type.SHIP then
      data.coalition = AIRBASE:FindByName(data.name):GetCoalition() or 0
    else
      data.coalition = data.opszone:GetOwner() or 0
    end
    newtable[_id] = _data
  end
  self.airbasetable = nil
  self.airbasetable = newtable
  return self
end

--- [INTERNAL] Get an OpsZone from a Zone object.
-- @param #STRATEGO self
-- @param Core.Zone#ZONE Zone
-- @param #number Coalition
-- @return Ops.OpsZone#OPSZONE OpsZone
function STRATEGO:GetNewOpsZone(Zone,Coalition)
  self:T(self.lid.."GetNewOpsZone")
  local opszone = OPSZONE:New(Zone,Coalition or 0)
  opszone:SetCaptureNunits(self.CaptureUnits)
  opszone:SetCaptureThreatlevel(self.CaptureThreatlevel)
  opszone:SetObjectCategories(self.CaptureObjectCategories)
  opszone:SetDrawZone(self.drawzone)
  opszone:SetMarkZone(self.markzone)
  opszone:Start()
  
  local function Captured(opszone,coalition)
    self:__NodeEvent(1,opszone,coalition)
  end
  
  function opszone:OnBeforeCaptured(From,Event,To,Coalition)
    Captured(opszone,Coalition)
  end
  
  return opszone
end

--- [INTERNAL] Analyse POI setups
-- @param #STRATEGO self
-- @return #STRATEGO self
function STRATEGO:AnalysePOIs(Set,Weight,Key)
  self:T(self.lid.."AnalysePOIs")
  local colors = self.colors
  local debug = self.debug
  local airbasetable = self.airbasetable
  local nonconnectedab = self.nonconnectedab
  local easynames = self.easynames
  Set:ForEach(
    function(port)
      local zone = port -- Core.Zone#ZONE_RADIUS
      local zname = zone:GetName()
      local coord = zone:GetCoordinate()
      if debug then
        zone:DrawZone(-1,colors[1],1,colors[1],0.3,1)
        coord:TextToAll(tostring(Weight),-1,{0,0,0},1,colors[1],0.3,20)
      end
      local opszone = self:GetNewOpsZone(zone)
      local tbl = { -- #STRATEGO.Data
        name = zname,
        baseweight = Weight,
        weight = 0,
        coalition = coalition.side.NEUTRAL,
        port = true,
        zone = zone,
        coord = coord,
        type = Key,
        opszone = opszone,
        connections = 0,
      }
      airbasetable[zname] = tbl
      nonconnectedab[zname] = true
      local name = string.gsub(zname,"[%p%s]",".")
      --self:I({name=name,zone=zname})
      easynames[name]=zname
    end
  )
  return self
end

--- [INTERNAL] Get nice route text
-- @param #STRATEGO self
-- @return #STRATEGO self
function STRATEGO:GetToFrom(StartPoint,EndPoint)
  self:T(self.lid.."GetToFrom "..tostring(StartPoint).." "..tostring(EndPoint))
  local pstart = string.gsub(StartPoint,"[%p%s]",".")
  local pend = string.gsub(EndPoint,"[%p%s]",".")
  local fromto = pstart..";"..pend
  local tofrom = pend..";"..pstart
  return fromto, tofrom
end

--- [USER] Get available connecting nodes from one start node
-- @param #STRATEGO self
-- @param #string StartPoint The starting name
-- @return #boolean found
-- @return #table Nodes 
function STRATEGO:GetRoutesFromNode(StartPoint)
  self:T(self.lid.."GetRoutesFromNode")
  local pstart = string.gsub(StartPoint,"[%p%s]",".")
  local found = false
  pstart=pstart..";"
  local routes = {}
  local listed = {}
  for _,_data in pairs(self.routexists) do
    if string.find(_data,pstart,1,true) and not listed[_data] then
      local target = string.gsub(_data,pstart,"")
      local fname = self.easynames[target]
      table.insert(routes,fname)
      found = true
      listed[_data] = true
    end
  end
  return found,routes
end

--- [USER] Manually add a route, for e.g. Island hopping or to connect isolated networks. Use **after** STRATEGO has been started!
-- @param #STRATEGO self
-- @param #string Startpoint Starting Point, e.g. AIRBASE.Syria.Hatay
-- @param #string Endpoint End Point, e.g. AIRBASE.Syria.H4
-- @param #table Color (Optional) RGB color table {r, g, b}, e.g. {1,0,0} for red. Defaults to violet.
-- @param #number Linetype (Optional) Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 5.
-- @param #boolean Draw (Optional) If true, draw route on the F10 map. Defaukt false.
-- @return #STRATEGO self
function STRATEGO:AddRoutesManually(Startpoint,Endpoint,Color,Linetype,Draw)
  self:T(self.lid.."AddRoutesManually")
  local fromto,tofrom = self:GetToFrom(Startpoint,Endpoint)
  local startcoordinate = self.airbasetable[Startpoint].coord
  local targetcoordinate = self.airbasetable[Endpoint].coord
  local dist = UTILS.Round(targetcoordinate:Get2DDistance(startcoordinate),-2)/1000
  local color = Color or {136/255,0,1}
  local linetype = Linetype or 5
  local data = {
      start = Startpoint,
      target = Endpoint,
      dist = dist,
    }
  --table.insert(disttable,fromto,data)
  self.disttable[fromto] = data
  self.disttable[tofrom] = data
  --table.insert(disttable,tofrom,data)
  table.insert(self.routexists,fromto)
  table.insert(self.routexists,tofrom)
  self.nonconnectedab[Endpoint] = false
  self.nonconnectedab[Startpoint] = false
  local factor = self.airbasetable[Startpoint].baseweight*self.routefactor
  self.airbasetable[Startpoint].weight = self.airbasetable[Startpoint].weight+factor
  self.airbasetable[Endpoint].weight = self.airbasetable[Endpoint].weight+factor
  self.airbasetable[Endpoint].connections = self.airbasetable[Endpoint].connections + 2
  self.airbasetable[Startpoint].connections = self.airbasetable[Startpoint].connections+2
  if self.debug or Draw then
    startcoordinate:LineToAll(targetcoordinate,-1,color,1,linetype,nil,string.format("%dkm",dist))
  end
  return self
end

--- [INTERNAL] Analyse routes
-- @param #STRATEGO self
-- @return #STRATEGO self
function STRATEGO:AnalyseRoutes(tgtrwys,factor,color,linetype)
  self:T(self.lid.."AnalyseRoutes")
  for _,_ab in pairs(self.airbasetable) do
    if _ab.baseweight >= 1 then
      local startpoint = _ab.name
      local startcoord = _ab.coord
      for _,_data in pairs(self.airbasetable) do
        local fromto,tofrom = self:GetToFrom(startpoint,_data.name) 
        if _data.name == startpoint then
          -- same as we
        elseif _data.baseweight == tgtrwys and not (self.routexists[fromto] or self.routexists[tofrom]) then
          local tgtc = _data.coord
          local dist = UTILS.Round(tgtc:Get2DDistance(startcoord),-2)/1000
          if dist <= self.maxdist then
            --local text = string.format("Distance %s to %s is %dkm",startpoint,_data.name,dist)
            --MESSAGE:New(text,10):ToLog()
            local data = {
              start = startpoint,
              target = _data.name,
              dist = dist,
            }
            --table.insert(disttable,fromto,data)
            self.disttable[fromto] = data
            self.disttable[tofrom] = data
            --table.insert(disttable,tofrom,data)
            table.insert(self.routexists,fromto)
            table.insert(self.routexists,tofrom)
            self.nonconnectedab[_data.name] = false
            self.nonconnectedab[startpoint] = false
            self.airbasetable[startpoint].weight = self.airbasetable[startpoint].weight+factor
            self.airbasetable[_data.name].weight = self.airbasetable[_data.name].weight+factor
            self.airbasetable[startpoint].connections = self.airbasetable[startpoint].connections + 1
            self.airbasetable[_data.name].connections = self.airbasetable[_data.name].connections + 1
            if self.debug then
              startcoord:LineToAll(tgtc,-1,color,1,linetype,nil,string.format("%dkm",dist))
            end
          end
        end
      end
    end
  end
  return self
end

--- [INTERNAL] Analyse non-connected points.
-- @param #STRATEGO self
-- @param #table Color RGB color to be used.
-- @return #STRATEGO self
function STRATEGO:AnalyseUnconnected(Color)
  self:T(self.lid.."AnalyseUnconnected")
  -- Non connected ones
  for _name,_noconnect in pairs(self.nonconnectedab) do
    if _noconnect then
      -- Find closest connected airbase
      local startpoint = _name
      local startcoord = self.airbasetable[_name].coord
      local shortest = 1000*1000
      local closest = nil
      local closestcoord = nil
      for _,_data in pairs(self.airbasetable) do
        if _name ~= _data.name then
          --local tgt = AIRBASE:FindByName(_data.name)
          local tgtc = _data.coord
          local dist = UTILS.Round(tgtc:Get2DDistance(startcoord),-2)/1000
          if dist < shortest and self.nonconnectedab[_data.name] == false then
            --local text = string.format("Distance %s to %s is %dkm",startpoint,_data.name,dist)
            shortest = dist
            closest = _data.name
            closestcoord = tgtc
            --MESSAGE:New(text,10):ToLog():ToAll()
          end
        end
      end
      if closest then
       if self.debug then
        startcoord:LineToAll(closestcoord,-1,Color,1,3,nil,string.format("%dkm",shortest))
       end
       self.airbasetable[startpoint].weight = self.airbasetable[startpoint].weight+1
       self.airbasetable[closest].weight = self.airbasetable[closest].weight+1
       self.airbasetable[startpoint].connections = self.airbasetable[startpoint].connections+2
       self.airbasetable[closest].connections = self.airbasetable[closest].connections+2
       local data = {
          start = startpoint,
          target = closest,
          dist = shortest,
        }
        local fromto,tofrom = self:GetToFrom(startpoint,closest)    
        self.disttable[fromto] = data
        self.disttable[tofrom] = data
       table.insert(self.routexists,fromto)
       table.insert(self.routexists,tofrom)
      end
    end
  end
  return self
end

--[[
function STRATEGO:PruneDeadEnds(abtable)
  local found = false
  local newtable = {}
  for name, _data in pairs(abtable) do
    local data = _data -- #STRATEGO.Data
    if data.connections > 2 then
      newtable[name] = data     
    else
      -- dead end
      found = true
      local neighbors, nearest, distance  = self:FindNeighborNodes(name)
      --self:I("Pruning "..name)
      if nearest then
        for _name,_ in pairs(neighbors) do
          local abname = self.easynames[_name] or _name
          --self:I({easyname=_name,airbasename=abname})
          if abtable[abname] then
            abtable[abname].connections = abtable[abname].connections -1 
          end
        end
      end
      if self.debug then
        data.coord:CircleToAll(5000,-1,{1,1,1},1,{1,1,1},1,3,true,"Dead End")
      end
    end
  end
  abtable = nil
  return found,newtable
end
--]]

--- [USER] Get a list of the nodes with the highest weight.
-- @param #STRATEGO self
-- @param #number Coalition (Optional) Find for this coalition only. E.g. coalition.side.BLUE.
-- @return #table Table of nodes.
-- @return #number Weight The consolidated weight associated with the nodes.
-- @return #number Highest Highest weight found.
-- @return #string Name of the node with the highest weight.
function STRATEGO:GetHighestWeightNodes(Coalition)
  self:T(self.lid.."GetHighestWeightNodes")
  local weight = 0
  local highest = 0
  local highname = nil
  local airbases = {}
  for _name,_data in pairs(self.airbasetable) do
    local okay = true
    if Coalition then
      if _data.coalition ~= Coalition then
        okay = false
      end
    end
    if _data.weight >= weight and okay then
      weight = _data.weight
      if not airbases[weight] then airbases[weight]={} end
      table.insert(airbases[weight],_name)
    end
    if _data.weight > highest and okay then
      highest = _data.weight
      highname = _name
    end
  end
  return airbases[weight],weight,highest,highname
end

--- [USER] Get a list of the nodes a weight less than the given parameter.
-- @param #STRATEGO self
-- @param #number Weight Weight - nodes need to have less than this weight.
-- @param #number Coalition (Optional) Find for this coalition only. E.g. coalition.side.BLUE.
-- @return #table Table of nodes.
-- @return #number Weight The consolidated weight associated with the nodes.
function STRATEGO:GetNextHighestWeightNodes(Weight, Coalition)
  self:T(self.lid.."GetNextHighestWeightNodes")
  local weight = 0
  local airbases = {}
  for _name,_data in pairs(self.airbasetable) do
    local okay = true
    if Coalition then
      if _data.coalition ~= Coalition then
        okay = false
      end
    end
    if _data.weight >= weight and _data.weight < Weight and okay then
      weight = _data.weight
      if not airbases[weight] then airbases[weight]={} end
      table.insert(airbases[weight],_name)
    end
  end
  return airbases[weight],weight
end

--- [USER] Set the aggregated weight of a single node found by its name manually.
-- @param #STRATEGO self
-- @param #string Name The name to look for.
-- @param #number Weight The weight to be set.
-- @return #boolean success
function STRATEGO:SetNodeWeight(Name,Weight)
  self:T(self.lid.."SetNodeWeight")
  if Name and Weight and self.airbasetable[Name] then
    self.airbasetable[Name].weight = Weight or 0
    return true
  else
    return false
  end
end

--- [USER] Set the base weight of a single node found by its name manually.
-- @param #STRATEGO self
-- @param #string Name The name to look for.
-- @param #number Weight The weight to be set.
-- @return #boolean success
function STRATEGO:SetNodeBaseWeight(Name,Weight)
  self:T(self.lid.."SetNodeBaseWeight")
  if Name and Weight and self.airbasetable[Name] then
    self.airbasetable[Name].baseweight = Weight or 0
    return true
  else
    return false
  end
end

--- [USER] Get the aggregated weight of a node by its name.
-- @param #STRATEGO self
-- @param #string Name The name to look for.
-- @return #number Weight The weight or 0 if not found.
function STRATEGO:GetNodeWeight(Name)
  self:T(self.lid.."GetNodeWeight")
  if Name and self.airbasetable[Name] then
    return self.airbasetable[Name].weight or 0
  else
    return 0
  end
end

--- [USER] Get the base weight of a node by its name.
-- @param #STRATEGO self
-- @param #string Name The name to look for.
-- @return #number Weight The base weight or 0 if not found.
function STRATEGO:GetNodeBaseWeight(Name)
  self:T(self.lid.."GetNodeBaseWeight")
  if Name and self.airbasetable[Name] then
    return self.airbasetable[Name].baseweight or 0
  else
    return 0
  end
end

--- [USER] Get the COALITION of a node by its name.
-- @param #STRATEGO self
-- @param #string The name to look for.
-- @return #number Coalition The coalition.
function STRATEGO:GetNodeCoalition(Name)
  self:T(self.lid.."GetNodeCoalition")
  if Name and self.airbasetable[Name] then
    return self.airbasetable[Name].coalition or coalition.side.NEUTRAL
  else
    return coalition.side.NEUTRAL
  end
end

--- [USER] Get the TYPE of a node by its name.
-- @param #STRATEGO self
-- @param #string The name to look for.
-- @return #string Type Type of the node, e.g. STRATEGO.Type.AIRBASE or nil if not found.
function STRATEGO:GetNodeType(Name)
  self:T(self.lid.."GetNodeType")
  if Name and self.airbasetable[Name] then
    return self.airbasetable[Name].type
  else
    return nil
  end
end

--- [USER] Get the ZONE of a node by its name.
-- @param #STRATEGO self
-- @param #string The name to look for.
-- @return Core.Zone#ZONE Zone The Zone of the node or nil if not found.
function STRATEGO:GetNodeZone(Name)
  self:T(self.lid.."GetNodeZone")
  if Name and self.airbasetable[Name] then
    return self.airbasetable[Name].zone
  else
    return nil
  end
end

--- [USER] Get the OPSZONE of a node by its name.
-- @param #STRATEGO self
-- @param #string The name to look for.
-- @return Ops.OpsZone#OPSZONE OpsZone The OpsZone of the node or nil if not found.
function STRATEGO:GetNodeOpsZone(Name)
  self:T(self.lid.."GetNodeOpsZone")
  if Name and self.airbasetable[Name] then
    return self.airbasetable[Name].opszone
  else
    return nil
  end
end

--- [USER] Get the COORDINATE of a node by its name.
-- @param #STRATEGO self
-- @param #string The name to look for.
-- @return Core.Point#COORDINATE Coordinate The Coordinate of the node or nil if not found.
function STRATEGO:GetNodeCoordinate(Name)
  self:T(self.lid.."GetNodeCoordinate")
  if Name and self.airbasetable[Name] then
    return self.airbasetable[Name].coord
  else
    return nil
  end
end

--- [USER] Check if the TYPE of a node is AIRBASE.
-- @param #STRATEGO self
-- @param #string The name to look for.
-- @return #boolean Outcome
function STRATEGO:IsAirbase(Name)
  self:T(self.lid.."IsAirbase")
  if Name and self.airbasetable[Name] then
    return self.airbasetable[Name].type == STRATEGO.Type.AIRBASE
  else
    return false
  end
end

--- [USER] Check if the TYPE of a node is PORT.
-- @param #STRATEGO self
-- @param #string The name to look for.
-- @return #boolean Outcome
function STRATEGO:IsPort(Name)
  self:T(self.lid.."IsPort")
  if Name and self.airbasetable[Name] then
    return self.airbasetable[Name].type == STRATEGO.Type.PORT
  else
    return false
  end
end

--- [USER] Check if the TYPE of a node is POI.
-- @param #STRATEGO self
-- @param #string The name to look for.
-- @return #boolean Outcome
function STRATEGO:IsPOI(Name)
  self:T(self.lid.."IsPOI")
  if Name and self.airbasetable[Name] then
    return self.airbasetable[Name].type == STRATEGO.Type.POI
  else
    return false
  end
end

--- [USER] Check if the TYPE of a node is FARP.
-- @param #STRATEGO self
-- @param #string The name to look for.
-- @return #boolean Outcome
function STRATEGO:IsFARP(Name)
  self:T(self.lid.."IsFARP")
  if Name and self.airbasetable[Name] then
    return self.airbasetable[Name].type == STRATEGO.Type.FARP
  else
    return false
  end
end

--- [USER] Check if the TYPE of a node is SHIP.
-- @param #STRATEGO self
-- @param #string The name to look for.
-- @return #boolean Outcome
function STRATEGO:IsShip(Name)
  self:T(self.lid.."IsShip")
  if Name and self.airbasetable[Name] then
    return self.airbasetable[Name].type == STRATEGO.Type.SHIP
  else
    return false
  end
end

--- [USER] Get the next best consolidation target node with a lower BaseWeight.
-- @param #STRATEGO self
-- @param #string Startpoint Name of start point.
-- @param #number BaseWeight Base weight of the node, i.e. the number of runways of an airbase or the weight of ports or POIs.
-- @return #number ShortestDist Shortest distance found.
-- @return #string Name Name of the target node.
-- @return #number Weight Consolidated weight of the target node, zero if none found.
-- @return #number Coalition Coaltion of the target.
function STRATEGO:FindClosestConsolidationTarget(Startpoint,BaseWeight)
  self:T(self.lid.."FindClosestConsolidationTarget for "..Startpoint.." Weight "..BaseWeight or 0)
  -- find existing routes
  local shortest = 1000*1000
  local target = nil
  local weight = 0
  local coa = nil
  if not BaseWeight then BaseWeight = self.maxrunways-1 end
  local startpoint = string.gsub(Startpoint,"[%p%s]",".")
  for _,_route in pairs(self.routexists) do
    if string.find(_route,startpoint,1,true) then
      --BASE:I({_route,startpoint})
      local dist = self.disttable[_route].dist
      local tname = string.gsub(_route,startpoint,"")
      local tname = string.gsub(tname,";","")
      local cname = self.easynames[tname]
      local targetweight = self.airbasetable[cname].baseweight
      coa = self.airbasetable[cname].coalition
      --self:T("Start -> End: "..startpoint.." -> "..cname)
      if (dist < shortest) and  (coa ~= self.coalition) and (BaseWeight >= targetweight) then
        self:T("Found Consolidation Target: "..cname)
        shortest = dist
        target = cname
        weight = self.airbasetable[cname].weight
        coa = coa
      end
    end
  end
  return shortest,target, weight, coa
end

--- [USER] Get the next best strategic target node with same or higher Consolidated Weight.
-- @param #STRATEGO self
-- @param #string Startpoint Name of start point.
-- @param #number Weight Consolidated Weight of the node, i.e. the calculated weight of the node based on number of runways, connections and a weight factor.
-- @return #number ShortestDist Shortest distance found.
-- @return #string Name Name of the target node.
-- @return #number Weight Consolidated weight of the target node, zero if none found.
-- @return #number Coalition Coaltion of the target.
function STRATEGO:FindClosestStrategicTarget(Startpoint,Weight)
  self:T(self.lid.."FindClosestStrategicTarget for "..Startpoint.." Weight "..Weight or 0)
  -- find existing routes
  local shortest = 1000*1000
  local target = nil
  local weight = 0
  local coa = nil
  if not Weight then Weight = self.maxrunways end
  local startpoint = string.gsub(Startpoint,"[%p%s]",".")
  for _,_route in pairs(self.routexists) do 
    if string.find(_route,startpoint,1,true) then
      local dist = self.disttable[_route].dist
      local tname = string.gsub(_route,startpoint,"")
      local tname = string.gsub(tname,";","")
      local cname = self.easynames[tname]
      local coa = self.airbasetable[cname].coalition
      local tweight = self.airbasetable[cname].baseweight
      local ttweight = self.airbasetable[cname].weight
      --self:T("Start -> End: "..startpoint.." -> "..cname)
      if (dist < shortest) and (coa ~= self.coalition) and (tweight >=  Weight) then
        self:T("Found Strategic Target: "..cname)
        shortest = dist
        target = cname
        weight = self.airbasetable[cname].weight
        coa = self.airbasetable[cname].coalition
      end
    end
  end
  return shortest,target,weight, coa
end

--- [USER] Get the next best strategic target nodes in the network.
-- @param #STRATEGO self
-- @return #table of #STRATEGO.Target data points
function STRATEGO:FindStrategicTargets()
  self:T(self.lid.."FindStrategicTargets")
  local targets = {}
  for _,_data in pairs(self.airbasetable) do
    local data = _data -- #STRATEGO.Data
    if data.coalition == self.coalition then
      local dist, name, points, coa = self:FindClosestStrategicTarget(data.name,data.weight)
      if points > 0 then
        self:T({dist=dist, name=name, points=points, coa=coa})
      end
      if points ~= 0 then
        local enemycoa = self.coalition == coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE
        self:T("Enemycoa = "..enemycoa)
        if coa == coalition.side.NEUTRAL then
          local tdata = {}
          tdata.name = name
          tdata.dist = dist
          tdata.points = points + self.NeutralBenefit
          tdata.coalition = coa
          tdata.coalitionname = UTILS.GetCoalitionName(coa) 
          tdata.coordinate = self.airbasetable[name].coord 
          table.insert(targets,tdata)
        else
          local tdata = {}
          tdata.name = name
          tdata.dist = dist
          tdata.points = points
          tdata.coalition = coa
          tdata.coalitionname = UTILS.GetCoalitionName(coa) 
          tdata.coordinate = self.airbasetable[name].coord 
          table.insert(targets,tdata)
        end
      end
    end
  end
  return targets
end

--- [USER] Get the next best consolidation target nodes in the network.
-- @param #STRATEGO self
-- @return #table of #STRATEGO.Target data points
function STRATEGO:FindConsolidationTargets()
  self:T(self.lid.."FindConsolidationTargets")
  local targets = {}
  for _,_data in pairs(self.airbasetable) do
    local data = _data -- #STRATEGO.Data
    if data.coalition == self.coalition then
      local dist, name, points, coa = self:FindClosestConsolidationTarget(data.name,self.maxrunways-1)
      if points > 0 then
        self:T({dist=dist, name=name, points=points, coa=coa})
      end
      if points ~= 0 then
        local enemycoa = self.coalition == coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE
        self:T("Enemycoa = "..enemycoa)
        if coa == coalition.side.NEUTRAL then
          local tdata = {}
          tdata.name = name
          tdata.dist = dist
          tdata.points = points + self.NeutralBenefit
          tdata.coalition = coa
          tdata.coalitionname = UTILS.GetCoalitionName(coa) 
          tdata.coordinate = self.airbasetable[name].coord 
          table.insert(targets,tdata)
        else 
          local tdata = {}
          tdata.name = name
          tdata.dist = dist
          tdata.points = points
          tdata.coalition = coa
          tdata.coalitionname = UTILS.GetCoalitionName(coa) 
          tdata.coordinate = self.airbasetable[name].coord 
          table.insert(targets,tdata)
        end
      end
    end
  end
  return targets
end

--- [USER] Get neighbor nodes of a named node.
-- @param #STRATEGO self
-- @param #string Name The name to search the neighbors for.
-- @param #boolean Enemies (optional) If true, find only enemy neighbors.
-- @param #boolean Friends (optional) If true, find only friendly or neutral neighbors.
-- @return #table Neighbors Table of #STRATEGO.DistData entries indexed by neighbor node names.
-- @return #string Nearest Name of the nearest node.
-- @return #number Distance Distance of the nearest node.
function STRATEGO:FindNeighborNodes(Name,Enemies,Friends)
  self:T(self.lid.."FindNeighborNodes")
  local neighbors = {}
  local name = string.gsub(Name,"[%p%s]",".")
  --self:I({Name=Name,name=name})
  local shortestdist = 1000*1000
  local nearest = nil
  for _route,_data in pairs(self.disttable) do
    if string.find(_route,name,1,true) then
      local dist = self.disttable[_route] -- #STRATEGO.DistData
      --self:I({route=_route,name=name})
      local tname = string.gsub(_route,name,"")
      local tname = string.gsub(tname,";","")
      --self:I({tname=tname,cname=self.easynames[tname]})
      local cname = self.easynames[tname] -- name of target
      if cname then
        local encoa = self.coalition == coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE
        if Enemies == true then
          if self.airbasetable[cname].coalition == encoa then
           neighbors[cname] = dist
          end
        elseif Friends == true then
          if self.airbasetable[cname].coalition ~= encoa then
           neighbors[cname] = dist
          end
        else
          neighbors[cname] = dist
        end
        if neighbors[cname] and  dist.dist < shortestdist then 
          shortestdist = dist.dist
          nearest = cname
        end
      end
    end
  end
  return neighbors, nearest, shortestdist
end

--- [INTERNAL] Route Finding - Find the next hop towards an end node from a start node
-- @param #STRATEGO self
-- @param #string Start The name of the start node.
-- @param #string End The name of the end node.
-- @param #table InRoute Table of node names making up the route so far.
-- @return #string Name of the next closest node
function STRATEGO:_GetNextClosest(Start,End,InRoute)
  local ecoord = self.airbasetable[End].coord
  local nodes,nearest = self:FindNeighborNodes(Start)
  --self:I(tostring(nearest))
  local closest = nil
  local closedist = 1000*1000
  for _name,_dist in pairs(nodes) do
    local kcoord = self.airbasetable[_name].coord
    local nnodes = self.airbasetable[_name].connections > 2 and true or false
    if _name == End then nnodes = true end
    if kcoord ~= nil and ecoord ~= nil and nnodes == true and InRoute[_name] ~= true then
      local dist = math.floor((kcoord:Get2DDistance(ecoord)/1000)+0.5)
      if (dist < closedist ) then
        closedist = dist
        closest = _name
      end
    end
  end
  return closest
end

--- [USER] Find a route between two nodes.
-- @param #STRATEGO self
-- @param #string Start The name of the start node.
-- @param #string End The name of the end node.
-- @param #number Hops Max iterations to find a route.
-- @param #boolean Draw If true, draw the route on the map.
-- @param #table Color (Optional) RGB color table {r, g, b}, e.g. {1,0,0} for red. Defaults to black.
-- @param #number LineType (Optional) Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 6.
-- @param #boolean NoOptimize If set to true, do not optimize (shorten) the resulting route if possible.
-- @return #table Route Table of #string name entries of the route
-- @return #boolean Complete If true, the route was found end-to-end.
-- @return #boolean Reverse If true, the route was found with a reverse search, the route table will be from sorted from end point to start point.
function STRATEGO:FindRoute(Start,End,Hops,Draw,Color,LineType,NoOptimize)
  self:T(self.lid.."FindRoute")
  --self:I({Start,End,Hops})
  --local bases = UTILS.DeepCopy(self.airbasetable)
  local Route = {}
  local InRoute = {}  
  local hops = Hops or 4
  local routecomplete = false
  local reverse = false
  
  local function Checker(neighbors)
    for _name,_data in pairs(neighbors) do
      if _name == End then
        -- found it
        return End
      end
    end
    return nil
  end
 
  local function DrawRoute(Route)
    for i=1,#Route-1 do
      local p1=Route[i]
      local p2=Route[i+1]
      local c1 = self.airbasetable[p1].coord -- Core.Point#COORDINATE
      local c2 = self.airbasetable[p2].coord -- Core.Point#COORDINATE
      local line = LineType or 6
      local color = Color or {0,0,0}
      c1:LineToAll(c2,-1,color,1,line)
    end
  end
  
  -- One hop
  Route[#Route+1] = Start
  InRoute[Start] = true
  local nodes = self:FindNeighborNodes(Start)
  local endpoint = Checker(nodes)
  
  if endpoint then
    Route[#Route+1] = endpoint
    routecomplete = true
  else
    local spoint = Start
    for i=1,hops do
      --self:I("Start="..tostring(spoint))
      local Next = self:_GetNextClosest(spoint,End,InRoute)
      if Next ~= nil then
        Route[#Route+1] = Next
        InRoute[Next] = true
        local nodes = self:FindNeighborNodes(Next)
        local endpoint = Checker(nodes)
        if endpoint then
          Route[#Route+1] = endpoint
          routecomplete = true
          break
        else
          spoint = Next
        end
      else
       break
      end 
    end
  end
  
  -- optimize route
  local function OptimizeRoute(Route)
    local foundcut = false
    local largestcut = 0
    local cut = {}
    for i=1,#Route do
      --self:I({Start=Route[i]})
      local found,nodes = self:GetRoutesFromNode(Route[i])
      for _,_name in pairs(nodes or {}) do
        for j=i+2,#Route do      
          if _name == Route[j] then
            --self:I({"Shortcut",Route[i],Route[j]})          
            if j-i > largestcut then
              largestcut = j-i
              cut = {i=i,j=j}
              foundcut = true
            end
          end
        end
      end
    end
    if foundcut then
      local newroute = {}
      for i=1,#Route do
        if i<= cut.i or i>=cut.j then
          table.insert(newroute,Route[i])
        end
      end
      return newroute
    end
    return Route, foundcut
  end 
  
  if routecomplete == true and NoOptimize ~= true then
    local foundcut = true
    while foundcut ~= false do
      Route, foundcut = OptimizeRoute(Route)
    end
  else
    -- reverse search
    Route, routecomplete = self:FindRoute(End,Start,Hops,Draw,Color,LineType)
    reverse = true
  end
  
  if (self.debug or Draw) then DrawRoute(Route) end
  
  return Route, routecomplete, reverse
end

--- [USER] Add budget points.
-- @param #STRATEGO self
-- @param #number Number of points to add.
-- @return #STRATEGO self
function STRATEGO:AddBudget(Number)
  self:T(self.lid.."AddBudget")
  self.Budget = self.Budget + Number
  return self
end

--- [USER] Subtract budget points.
-- @param #STRATEGO self
-- @param #number Number of points to subtract.
-- @return #STRATEGO self
function STRATEGO:SubtractBudget(Number)
  self:T(self.lid.."SubtractBudget")
  self.Budget = self.Budget - Number
  return self
end

--- [USER] Get budget points.
-- @param #STRATEGO self
-- @return #number budget
function STRATEGO:GetBudget()
  self:T(self.lid.."GetBudget")
  return self.Budget
end

--- [USER] Find **one** affordable strategic target.
-- @param #STRATEGO self
-- @return #table Target Table with #STRATEGO.Target data or nil if none found.
function STRATEGO:FindAffordableStrategicTarget()
  self:T(self.lid.."FindAffordableStrategicTarget")
  local Stargets = self:FindStrategicTargets() -- #table of #STRATEGO.Target
  --UTILS.PrintTableToLog(Stargets,1)
  local budget = self.Budget
  --local leftover = self.Budget
  local ftarget = nil -- #STRATEGO.Target
  local Targets = {}
  for _,_data in pairs(Stargets) do
    local data = _data -- #STRATEGO.Target
    self:T("Considering Strategic Target "..data.name)
    --if data.points <= budget and budget-data.points < leftover then
    if data.points <= budget then
      --leftover = budget-data.points
      table.insert(Targets,data)
      self:T(self.lid.."Affordable strategic target: "..data.name)
    end
  end
  if #Targets == 0 then
    self:T(self.lid.."No suitable target found!")
    return nil
  end
  if #Targets > 1 then
    ftarget = Targets[math.random(1,#Targets)]
  else
    ftarget = Targets[1]
  end 
  if ftarget then
    self:T(self.lid.."Final affordable strategic target: "..ftarget.name)
    return ftarget
  else
   return nil
  end
end

--- [USER] Find **one** affordable consolidation target.
-- @param #STRATEGO self
-- @return #table Target Table with #STRATEGO.Target data or nil if none found.
function STRATEGO:FindAffordableConsolidationTarget()
  self:T(self.lid.."FindAffordableConsolidationTarget")
  local Ctargets = self:FindConsolidationTargets() -- #table of #STRATEGO.Target
  --UTILS.PrintTableToLog(Ctargets,1)
  local budget = self.Budget
  --local leftover = self.Budget
  local ftarget = nil -- #STRATEGO.Target
  local Targets = {}
  for _,_data in pairs(Ctargets) do
    local data = _data -- #STRATEGO.Target
    self:T("Considering Consolidation Target "..data.name)
    --if data.points <= budget and budget-data.points < leftover then
    if data.points <= budget then
      --leftover = budget-data.points
      table.insert(Targets,data)
      self:T(self.lid.."Affordable consolidation target: "..data.name)
    end
  end
  if #Targets == 0 then
    self:T(self.lid.."No suitable target found!")
    return nil
  end
  if #Targets > 1 then
    ftarget = Targets[math.random(1,#Targets)]
  else
    ftarget = Targets[1]
  end
  if ftarget then
    self:T(self.lid.."Final affordable consolidation target: "..ftarget.name)
    return ftarget
  else
    return nil
  end
end

--- [INTERNAL] Internal helper function to check for islands, aka Floodtest
-- @param #STRATEGO self
-- @param #string next Name of the start node
-- @param #table filled #table of visited nodes
-- @param #table unfilled #table if unvisited nodes
-- @return #STRATEGO self
function STRATEGO:_FloodNext(next,filled,unfilled)
  local start = self:FindNeighborNodes(next)
  for _name,_ in pairs (start) do
    if filled[_name] ~= true then
      self:T("Flooding ".._name)
      filled[_name] = true
      unfilled[_name] = nil
      self:_FloodNext(_name,filled,unfilled)
    end
  end
  return self
end

--- [INTERNAL] Internal helper function to check for islands, aka Floodtest
-- @param #STRATEGO self
-- @param #string Start Name of the start node
-- @param #table ABTable (Optional) #table of node names to check.
-- @return #STRATEGO self
function STRATEGO:_FloodFill(Start,ABTable)
  self:T("Start = "..tostring(Start))
  if Start == nil then return end
  local filled = {}
  local unfilled = {}
  if ABTable then
    unfilled = ABTable
  else
    for _name,_ in pairs(self.airbasetable) do
      unfilled[_name] = true
    end
  end
  filled[Start] = true
  unfilled[Start] = nil
  local start = self:FindNeighborNodes(Start)
  for _name,_ in pairs (start) do
    if filled[_name] ~= true then
      self:T("Flooding ".._name)
      filled[_name] = true
      unfilled[_name] = nil
      self:_FloodNext(_name,filled,unfilled)
    end
  end
  return filled, unfilled
end

--- [INTERNAL] Internal helper function to check for islands, aka Floodtest
-- @param #STRATEGO self
-- @param #boolen connect If true, connect the two resulting islands at the shortest distance if necessary
-- @param #boolen draw If true, draw outer vertices of found node networks
-- @return #boolean Connected If true, all nodes are in one network
-- @return #table Network #table of node names in the network
-- @return #table Unconnected #table of node names **not** in the network
function STRATEGO:_FloodTest(connect,draw)
  
  local function GetElastic(bases)
    local vec2table = {}
    for _name,_ in pairs(bases) do
      local coord = self.airbasetable[_name].coord
      local vec2 = coord:GetVec2()
      table.insert(vec2table,vec2)
    end
    local zone = ZONE_ELASTIC:New("STRATEGO-Floodtest-"..math.random(1,10000),vec2table)
    return zone
  end
  
  local function DrawElastic(filled,drawit)
    local zone = GetElastic(filled)
    if drawit then
      zone:SetColor({1,1,1},1)
      zone:SetDrawCoalition(-1)
      zone:Update(1,true) -- draw zone
    end
    return zone
  end

  local _,_,weight,name = self:GetHighestWeightNodes()
  local filled, unfilled = self:_FloodFill(name)
  local allin = true
  if table.length(unfilled) > 0 then
    MESSAGE:New("There is at least one node island!",15,"STRATEGO"):ToAllIf(self.debug):ToLog()
    allin = false
    if self.debug == true then
      local zone1 = DrawElastic(filled,draw)
      local zone2 = DrawElastic(unfilled,draw)
      local vertices1 = zone1:GetVerticiesVec2()
      local vertices2 = zone2:GetVerticiesVec2()
      -- get closest vertices
      local corner1 = nil
      local corner2 = nil
      local mindist = math.huge
      local found = false
      for _,_edge in pairs(vertices1) do
        for _,_edge2 in pairs(vertices2) do
          local dist=UTILS.VecDist2D(_edge,_edge2)
          if dist < mindist then
            mindist = dist
            corner1 = _edge
            corner2 = _edge2
            found = true
          end
        end  
      end
      if found then
        local Corner = COORDINATE:NewFromVec2(corner1)
        local Corner2 = COORDINATE:NewFromVec2(corner2)
        Corner:LineToAll(Corner2,-1,{1,1,1},1,1,true,"Island2Island")
        local cornername
        local cornername2
        for _name,_data in pairs(self.airbasetable) do
          local zone = _data.zone
          if zone:IsVec2InZone(corner1) then
            cornername = _name
            self:T("Corner1 = ".._name)
          end
          if zone:IsVec2InZone(corner2) then
            cornername2 = _name
            self:T("Corner2 = ".._name)
          end
          if cornername and cornername2 and connect == true then
            self:AddRoutesManually(cornername,cornername2,Color,Linetype,self.debug)
          end
        end
      end
    end
  end
  return allin, filled, unfilled
end

---------------------------------------------------------------------------------------------------------------
--
-- End
-- 
---------------------------------------------------------------------------------------------------------------
--- **Functional** - Manage and track client slots easily to add your own client-based menus and modules to.
-- 
-- The @{#CLIENTWATCH} class adds a simplified way to create scripts and menus for individual clients. Instead of creating large algorithms and juggling multiple event handlers, you can simply provide one or more prefixes to the class and use the callback functions on spawn, despawn, and any aircraft related events to script to your hearts content.
-- 
-- ===
-- 
-- ## Features:
-- 
--   * Find clients by prefixes or by providing a Wrapper.CLIENT object
--   * Trigger functions when the client spawns and despawns
--   * Create multiple client instances without overwriting event handlers between instances
--   * More reliable aircraft lost events for when DCS thinks the aircraft id dead but a dead event fails to trigger
--   * Easily manage clients spawned in dynamic slots
--
-- ====
-- 
-- ### Author: **Statua**
-- 
-- ### Contributions: **FlightControl**: Wrapper.CLIENT
-- 
-- ====
-- @module Functional.ClientWatch
-- @image clientwatch.jpg

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- CLIENTWATCH class
-- @type CLIENTWATCH
-- @field #string ClassName Name of the class.
-- @field #boolean Debug Write Debug messages to DCS log file and send Debug messages to all players.
-- @field #string lid String for DCS log file.
-- @field #number FilterCoalition If not nil, will only activate for aircraft of the given coalition value.
-- @field #number FilterCategory If not nil, will only activate for aircraft of the given category value.
-- @extends Core.Fsm#FSM_CONTROLLABLE

--- Manage and track client slots easily to add your own client-based menus and modules to.
-- 
-- ## Creating a new instance
-- 
-- To start, you must first create a new instance of the client manager and provide it with either a Wrapper.Client#CLIENT object, a string prefix of the unit name, or a table of string prefixes for unit names. These are used to capture the client unit when it spawns and apply your scripted functions to it. Only fixed wing and rotary wing aircraft controlled by players can be used by this class.
-- **This will not work if the client aircraft is alive!**
-- 
-- ### Examples
--
--          -- Create an instance with a Wrapper.Client#CLIENT object
--          local heliClient = CLIENT:FindByName('Rotary1-1')
--          local clientInstance = CLIENTWATCH:New(heliClient)
--  
--          -- Create an instance with part of the unit name in the Mission Editor
--          local clientInstance = CLIENTWATCH:New("Rotary")
--  
--          -- Create an instance using prefixes for a few units as well as a FARP name for any dynamic spawns coming out of it
--          local clientInstance = CLIENTWATCH:New({"Rescue","UH-1H","FARP ALPHA"})
--
-- ## Applying functions and methods to client aircraft when they spawn
-- 
-- Once the instance is created, it will watch for birth events. If the unit name of the client aircraft matches the one provided in the instance, the callback method @{#CLIENTWATCH:OnAfterSpawn}() can be used to apply functions and methods to the client object.
--
-- In the OnAfterSpawn() callback method are four values. From, Event, To, and ClientObject. From,Event,To are standard FSM strings for the state changes. ClientObject is where the magic happens. This is a special object which you can use to access all the data of the client aircraft. The following entries in ClientObject are available for you to use:
--
--   * **ClientObject.Unit**: The Moose @{Wrapper.Unit#UNIT} of the client aircraft
--   * **ClientObject.Group**: The Moose @{Wrapper.Group#GRUP} of the client aircraft
--   * **ClientObject.Client**: The Moose @{Wrapper.Client#CLIENT} of the client aircraft
--   * **ClientObject.PlayerName**: A #string of the player controlling the aircraft
--   * **ClientObject.UnitName**: A #string of the client aircraft unit.
--   * **ClientObject.GroupName**: A #string of the client aircraft group.
-- 
-- ### Examples
--
--          -- Create an instance with a client unit prefix and send them a message when they spawn
--          local clientInstance = CLIENTWATCH:New("Rotary")
--          function clientInstance:OnAfterSpawn(From,Event,To,ClientObject,EventData)
--              MESSAGE:New("Welcome to your aircraft!",10):ToUnit(ClientObject.Unit)
--          end
--
-- ## Using event callbacks
-- 
-- In a normal setting, you can only use a callback function for a specific option in one location. If you have multiple scripts that rely on the same callback from the same object, this can get quite messy. With the ClientWatch module, these callbacks are isolated t the instances and therefore open the possibility to use many instances with the same callback doing different things. ClientWatch instances subscribe to all events that are applicable to player controlled aircraft and provides callbacks for each, forwarding the EventData in the callback function.
--
-- The following event callbacks can be used inside the OnAfterSpawn() callback:
--
--   * **:OnAfterDespawn(From,Event,To)**: Triggers whenever DCS no longer sees the aircraft as 'alive'. No event data is given in this callback as it is derived from other events
--   * **:OnAfterHit(From,Event,To,EventData)**: Triggers every time the aircraft takes damage or is struck by a weapon/explosion
--   * **:OnAfterKill(From,Event,To,EventData)**: Triggers after the aircraft kills something with its weapons
--   * **:OnAfterScore(From,Event,To,EventData)**: Triggers after accumulating score
--   * **:OnAfterShot(From,Event,To,EventData)**: Triggers after a single-shot weapon is released
--   * **:OnAfterShootingStart(From,Event,To,EventData)**: Triggers when an automatic weapon begins firing
--   * **:OnAfterShootingEnd(From,Event,To,EventData)**: Triggers when an automatic weapon stops firing
--   * **:OnAfterLand(From,Event,To,EventData)**: Triggers when an aircraft transitions from being airborne to on the ground
--   * **:OnAfterTakeoff(From,Event,To,EventData)**: Triggers when an aircraft transitions from being on the ground to airborne
--   * **:OnAfterRunwayTakeoff(From,Event,To,EventData)**: Triggers after lifting off from a runway
--   * **:OnAfterRunwayTouch(From,Event,To,EventData)**: Triggers when an aircraft's gear makes contact with a runway
--   * **:OnAfterRefueling(From,Event,To,EventData)**: Triggers when an aircraft begins taking on fuel
--   * **:OnAfterRefuelingStop(From,Event,To,EventData)**: Triggers when an aircraft stops taking on fuel
--   * **:OnAfterPlayerLeaveUnit(From,Event,To,EventData)**: Triggers when a player leaves an operational aircraft
--   * **:OnAfterCrash(From,Event,To,EventData)**: Triggers when an aircraft is destroyed (may fail to trigger if the aircraft is only partially destroyed)
--   * **:OnAfterDead(From,Event,To,EventData)**: Triggers when an aircraft is considered dead (may fail to trigger if the aircraft was partially destroyed first)
--   * **:OnAfterPilotDead(From,Event,To,EventData)**: Triggers when the pilot is killed (may fail to trigger if the aircraft was partially destroyed first)
--   * **:OnAfterUnitLost(From,Event,To,EventData)**: Triggers when an aircraft is lost for any reason (may fail to trigger if the aircraft was partially destroyed first)
--   * **:OnAfterEjection(From,Event,To,EventData)**: Triggers when a pilot ejects from an aircraft
--   * **:OnAfterHumanFailure(From,Event,To,EventData)**: Triggers when an aircraft or system is damaged from any source or action by the player
--   * **:OnAfterHumanAircraftRepairStart(From,Event,To,EventData)**: Triggers when an aircraft repair is started
--   * **:OnAfterHumanAircraftRepairFinish(From,Event,To,EventData)**: Triggers when an aircraft repair is completed
--   * **:OnAfterEngineStartup(From,Event,To,EventData)**: Triggers when the engine enters what DCS considers to be a started state. Parameters vary by aircraft
--   * **:OnAfterEngineShutdown(From,Event,To,EventData)**: Triggers when the engine enters what DCS considers to be a stopped state. Parameters vary by aircraft
--   * **:OnAfterWeaponAdd(From,Event,To,EventData)**: Triggers when an item is added to an aircraft's payload
--   * **:OnAfterWeaponDrop(From,Event,To,EventData)**: Triggers when an item is jettisoned or dropped from an aircraft (unconfirmed)
--   * **:OnAfterWeaponRearm(From,Event,To,EventData)**: Triggers when an item with internal supply is restored (unconfirmed)
-- 
-- ### Examples
--
--          -- Show a message to player when they take damage from a weapon
--          local clientInstance = CLIENTWATCH:New("Rotary")
--              function clientInstance:OnAfterSpawn(From,Event,To,ClientObject,EventData)
--              function ClientObject:OnAfterHit(From,Event,To,EventData)
--                 local typeShooter = EventData.IniTypeName
--                 local nameWeapon = EventData.weapon_name
--                 MESSAGE:New("A "..typeShooter.." hit you with a "..nameWeapon,20):ToUnit(ClientObject.Unit)
--              end
--          end
-- 
-- @field #CLIENTWATCH
CLIENTWATCH = {}
CLIENTWATCH.ClassName = "CLIENTWATCH"
CLIENTWATCH.Debug = false
CLIENTWATCH.DebugEventData = false
CLIENTWATCH.lid = nil

-- @type CLIENTWATCHTools
-- @field #table Unit Wrapper.UNIT of the cient object
-- @field #table Group Wrapper.GROUP of the cient object
-- @field #table Client Wrapper.CLIENT of the cient object
-- @field #string PlayerName Name of the player controlling the client object
-- @field #string UnitName Name of the unit that is the client object
-- @field #string GroupName Name of the group the client object belongs to
CLIENTWATCHTools = {}
  
--- CLIENTWATCH version
-- @field #string version
CLIENTWATCH.version="1.0.1"

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Creates a new instance of CLIENTWATCH to add scripts to. Can be used multiple times with the same client/prefixes if you need it for multiple scripts.
-- @param #CLIENTWATCH self
-- @param #string Will watch for clients whos UNIT NAME or GROUP NAME matches part of the #string as a prefix.
-- @param #table Put strings in a table to use multiple prefixes for the above method.
-- @param Wrapper.Client#CLIENT Provide a Moose CLIENT object to apply to that specific aircraft slot (static slots only!)
-- @param #nil Leave blank to activate for ALL CLIENTS
-- @return #CLIENTWATCH self
function CLIENTWATCH:New(client)
    --Init FSM
    local self=BASE:Inherit(self, FSM:New())
    self:SetStartState( "Idle" )
    self:AddTransition( "*", "Spawn", "*" )

    self.FilterCoalition = nil
    self.FilterCategory = nil

    --- User function for OnAfter "Spawn" event.
  -- @function [parent=#CLIENTWATCH] OnAfterSpawn
  -- @param #CLIENTWATCH self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable Controllable of the group.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #table clientObject Custom object that handles events and stores Moose object data. See top documentation for more details.
  -- @param #table eventdata Data from EVENTS.Birth.

    --Set up spawn tracking
    if not client then
        if self.Debug then self:I({"New client instance created. ClientType = All clients"}) end
        self:HandleEvent(EVENTS.Birth)
        function self:OnEventBirth(eventdata)
            if (eventdata.IniCategory == 0 or eventdata.IniCategory == 1) and eventdata.IniPlayerName 
            and (not self.FilterCoalition or self.FilterCoalition == eventdata.IniCoalition) 
            and (not self.FilterCategory or self.FilterCategory == eventdata.IniCategory) then
                if self.Debug then 
                    self:I({"Client spawned in.",IniCategory = eventdata.IniCategory})
                end
                local clientWatchDebug = self.Debug
                local clientObject = CLIENTWATCHTools:_newClient(clientWatchDebug,eventdata)
                self:Spawn(clientObject,eventdata)
            end
        end
    elseif type(client) == "table" or type(client) == "string" then
        if type(client) == "table" then 

            --CLIENT TABLE
            if client.ClassName == "CLIENT" then
                if self.Debug then self:I({"New client instance created. ClientType = Wrapper.CLIENT",client}) end
                self.ClientName = client:GetName()
                self:HandleEvent(EVENTS.Birth)
                function self:OnEventBirth(eventdata)
                    if (eventdata.IniCategory == 0 or eventdata.IniCategory == 1) and eventdata.IniPlayerName 
                    and (not self.FilterCoalition or self.FilterCoalition == eventdata.IniCoalition) 
                    and (not self.FilterCategory or self.FilterCategory == eventdata.IniCategory) then
                        if self.ClientName == eventdata.IniUnitName then
                            if self.Debug then 
                                self:I({"Client spawned in.",IniCategory = eventdata.IniCategory})
                            end
                            local clientWatchDebug = self.Debug
                            local clientObject = CLIENTWATCHTools:_newClient(clientWatchDebug,eventdata)
                            self:Spawn(clientObject,eventdata)
                        end
                    end
                end

            --STRING TABLE
            else
                if self.Debug then self:I({"New client instance created. ClientType = Multiple Prefixes",client}) end
                local tableValid = true
                for _,entry in pairs(client) do
                    if type(entry) ~= "string" then
                        tableValid = false
                        self:E({"The base handler failed to start because at least one entry in param1's table is not a string!",InvalidEntry = entry})
                        return nil
                    end
                end
                if tableValid then
                    self:HandleEvent(EVENTS.Birth)
                    function self:OnEventBirth(eventdata)
                        for _,entry in pairs(client) do
                            if (eventdata.IniCategory == 0 or eventdata.IniCategory == 1) and eventdata.IniPlayerName 
                            and (not self.FilterCoalition or self.FilterCoalition == eventdata.IniCoalition) 
                            and (not self.FilterCategory or self.FilterCategory == eventdata.IniCategory) then
                                if string.match(eventdata.IniUnitName,entry) or string.match(eventdata.IniGroupName,entry) then
                                    if self.Debug then 
                                        self:I({"Client spawned in.",IniCategory = eventdata.IniCategory})
                                    end
                                    local clientWatchDebug = self.Debug
                                    local clientObject = CLIENTWATCHTools:_newClient(clientWatchDebug,eventdata)
                                    self:Spawn(clientObject,eventdata)
                                    break
                                end
                            end
                        end
                    end
                end
            end
        else
            if self.Debug then self:I({"New client instance created. ClientType = Single Prefix",client}) end

            --SOLO STRING
            self:HandleEvent(EVENTS.Birth)
            function self:OnEventBirth(eventdata)
                if (eventdata.IniCategory == 0 or eventdata.IniCategory == 1) and eventdata.IniPlayerName 
                and (not self.FilterCoalition or self.FilterCoalition == eventdata.IniCoalition) 
                and (not self.FilterCategory or self.FilterCategory == eventdata.IniCategory) then
                    if string.match(eventdata.IniUnitName,client) or string.match(eventdata.IniGroupName,client) then
                        if self.Debug then 
                            self:I({"Client spawned in.",IniCategory = eventdata.IniCategory})
                        end
                        local clientWatchDebug = self.Debug
                        local clientObject = CLIENTWATCHTools:_newClient(clientWatchDebug,eventdata)
                        self:Spawn(clientObject,eventdata)
                    end
                end
            end
        end
    else
        self:E({"The base handler failed to start because param1 is not a CLIENT object or a prefix string!",param1 = client})
        return nil
    end

    return self
end


--- Filter out all clients not belonging to the provided coalition
-- @param #CLIENTWATCH self
-- @param #number Coalition number (1 = red, 2 = blue)
-- @param #string Coalition string ('red' or 'blue')
function CLIENTWATCH:FilterByCoalition(value)
    if value == 1 or value == "red" then
        self.FilterCoalition = 1
    else
        self.FilterCoalition = 2
    end
    return self
end


--- Filter out all clients that are not of the given category
-- @param #CLIENTWATCH self
-- @param #number Category number (0 = airplane, 1 = helicopter)
-- @param #string Category string ('airplane' or 'helicopter')
function CLIENTWATCH:FilterByCategory(value)
    if value == 1 or value == "helicopter" then
        self.FilterCategory = 1
    else
        self.FilterCategory = 0
    end
    return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Internal function for creating a new client on birth. Do not use!!!.
-- @param #CLIENTWATCHTools self
-- @param #EVENTS.Birth EventData
-- @return #CLIENTWATCHTools self
function CLIENTWATCHTools:_newClient(clientWatchDebug,eventdata)
    --Init FSM
    local self=BASE:Inherit(self, FSM:New())
    self:SetStartState( "Alive" )
    self:AddTransition( "Alive", "Despawn", "Dead" )

    self.Unit = eventdata.IniUnit
    self.Group = self.Unit:GetGroup()
    self.Client = self.Unit:GetClient()
    self.PlayerName = self.Unit:GetPlayerName()
    self.UnitName = self.Unit:GetName()
    self.GroupName = self.Group:GetName()

    --Event events
    self:AddTransition( "*", "Hit", "*" )
    self:AddTransition( "*", "Kill", "*" )
    self:AddTransition( "*", "Score", "*" )
    self:AddTransition( "*", "Shot", "*" )
    self:AddTransition( "*", "ShootingStart", "*" )
    self:AddTransition( "*", "ShootingEnd", "*" )
    self:AddTransition( "*", "Land", "*" )
    self:AddTransition( "*", "Takeoff", "*" )
    self:AddTransition( "*", "RunwayTakeoff", "*" )
    self:AddTransition( "*", "RunwayTouch", "*" )
    self:AddTransition( "*", "Refueling", "*" )
    self:AddTransition( "*", "RefuelingStop", "*" )
    self:AddTransition( "*", "PlayerLeaveUnit", "*" )
    self:AddTransition( "*", "Crash", "*" )
    self:AddTransition( "*", "Dead", "*" )
    self:AddTransition( "*", "PilotDead", "*" )
    self:AddTransition( "*", "UnitLost", "*" )
    self:AddTransition( "*", "Ejection", "*" )
    self:AddTransition( "*", "HumanFailure", "*" )
    self:AddTransition( "*", "HumanAircraftRepairFinish", "*" )
    self:AddTransition( "*", "HumanAircraftRepairStart", "*" )
    self:AddTransition( "*", "EngineShutdown", "*" )
    self:AddTransition( "*", "EngineStartup", "*" )
    self:AddTransition( "*", "WeaponAdd", "*" )
    self:AddTransition( "*", "WeaponDrop", "*" )
    self:AddTransition( "*", "WeaponRearm", "*" )

    --Event Handlers
    self:HandleEvent( EVENTS.Hit )
    self:HandleEvent( EVENTS.Kill )
    self:HandleEvent( EVENTS.Score )
    self:HandleEvent( EVENTS.Shot )
    self:HandleEvent( EVENTS.ShootingStart )
    self:HandleEvent( EVENTS.ShootingEnd )
    self:HandleEvent( EVENTS.Land )
    self:HandleEvent( EVENTS.Takeoff )
    self:HandleEvent( EVENTS.RunwayTakeoff )
    self:HandleEvent( EVENTS.RunwayTouch )
    self:HandleEvent( EVENTS.Refueling )
    self:HandleEvent( EVENTS.RefuelingStop )
    self:HandleEvent( EVENTS.PlayerLeaveUnit )
    self:HandleEvent( EVENTS.Crash )
    self:HandleEvent( EVENTS.Dead )
    self:HandleEvent( EVENTS.PilotDead )
    self:HandleEvent( EVENTS.UnitLost )
    self:HandleEvent( EVENTS.Ejection )
    self:HandleEvent( EVENTS.HumanFailure )
    self:HandleEvent( EVENTS.HumanAircraftRepairFinish )
    self:HandleEvent( EVENTS.HumanAircraftRepairStart )
    self:HandleEvent( EVENTS.EngineShutdown )
    self:HandleEvent( EVENTS.EngineStartup )
    self:HandleEvent( EVENTS.WeaponAdd )
    self:HandleEvent( EVENTS.WeaponDrop )
    self:HandleEvent( EVENTS.WeaponRearm )

    function self:OnEventHit(EventData)
        if EventData.TgtUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered hit event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:Hit(EventData)
        end
    end

    function self:OnEventKill(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered kill event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:Kill(EventData)
        end
    end

    function self:OnEventScore(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered score event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:Score(EventData)
        end
    end

    function self:OnEventShot(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered shot event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:Shot(EventData)
        end
    end

    function self:OnEventShootingStart(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered shooting start event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:ShootingStart(EventData)
        end
    end

    function self:OnEventShootingEnd(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered shooting end event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:ShootingEnd(EventData)
        end
    end

    function self:OnEventLand(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered land event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:Land(EventData)
        end
    end

    function self:OnEventTakeoff(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered takeoff event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:Takeoff(EventData)
        end
    end

    function self:OnEventRunwayTakeoff(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered runway takeoff event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:RunwayTakeoff(EventData)
        end
    end

    function self:OnEventRunwayTouch(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered runway touch event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:RunwayTouch(EventData)
        end
    end

    function self:OnEventRefueling(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered refueling event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:Refueling(EventData)
        end
    end

    function self:OnEventRefuelingStop(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered refueling event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:RefuelingStop(EventData)
        end
    end

    function self:OnEventPlayerLeaveUnit(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered leave unit event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:PlayerLeaveUnit(EventData)
            self._deadRoutine()
        end
    end

    function self:OnEventCrash(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered crash event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:Crash(EventData)
            self._deadRoutine()
        end
    end

    function self:OnEventDead(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered dead event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:Dead(EventData)
            self._deadRoutine()
        end
    end

    function self:OnEventPilotDead(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered pilot dead event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:PilotDead(EventData)
            self._deadRoutine()
        end
    end

    function self:OnEventUnitLost(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered unit lost event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:UnitLost(EventData)
            self._deadRoutine()
        end
    end

    function self:OnEventEjection(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered ejection event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:Ejection(EventData)
            self._deadRoutine()
        end
    end

    function self:OnEventHumanFailure(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered human failure event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:HumanFailure(EventData)
            if not self.Unit:IsAlive() then
                self._deadRoutine()
            end
        end
    end

    function self:OnEventHumanAircraftRepairFinish(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered repair finished event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:HumanAircraftRepairFinish(EventData)
        end
    end

    function self:OnEventHumanAircraftRepairStart(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered repair start event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:HumanAircraftRepairStart(EventData)
        end
    end

    function self:OnEventEngineShutdown(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered engine shutdown event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:EngineShutdown(EventData)
        end
    end

    function self:OnEventEngineStartup(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered engine startup event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:EngineStartup(EventData)
        end
    end

    function self:OnEventWeaponAdd(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered weapon add event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:WeaponAdd(EventData)
        end
    end

    function self:OnEventWeaponDrop(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered weapon drop event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:WeaponDrop(EventData)
        end
    end

    function self:OnEventWeaponRearm(EventData)
        if EventData.IniUnitName == self.UnitName then
            if clientWatchDebug then
                self:I({"Client triggered weapon rearm event.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
                
            end
            self:WeaponRearm(EventData)
        end
    end


    --Fallback timer
    self.FallbackTimer = TIMER:New(function()
        if not self.Unit:IsAlive() then
            if clientWatchDebug then
                self:I({"Client is registered as dead without an event trigger. Running fallback dead routine.",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName})
            end
            self._deadRoutine()
        end
    end)
    self.FallbackTimer:Start(5,5)

    --Stop event handlers and trigger Despawn
    function self._deadRoutine()
        if clientWatchDebug then self:I({"Client dead routine triggered. Shutting down tracking...",Player = self.PlayerName,Group = self.GroupName,Unit = self.UnitName}) end
        self:UnHandleEvent( EVENTS.Hit )
        self:UnHandleEvent( EVENTS.Kill )
        self:UnHandleEvent( EVENTS.Score )
        self:UnHandleEvent( EVENTS.Shot )
        self:UnHandleEvent( EVENTS.ShootingStart )
        self:UnHandleEvent( EVENTS.ShootingEnd )
        self:UnHandleEvent( EVENTS.Land )
        self:UnHandleEvent( EVENTS.Takeoff )
        self:UnHandleEvent( EVENTS.RunwayTakeoff )
        self:UnHandleEvent( EVENTS.RunwayTouch )
        self:UnHandleEvent( EVENTS.Refueling )
        self:UnHandleEvent( EVENTS.RefuelingStop )
        self:UnHandleEvent( EVENTS.PlayerLeaveUnit )
        self:UnHandleEvent( EVENTS.Crash )
        self:UnHandleEvent( EVENTS.Dead )
        self:UnHandleEvent( EVENTS.PilotDead )
        self:UnHandleEvent( EVENTS.UnitLost )
        self:UnHandleEvent( EVENTS.Ejection )
        self:UnHandleEvent( EVENTS.HumanFailure )
        self:UnHandleEvent( EVENTS.HumanAircraftRepairFinish )
        self:UnHandleEvent( EVENTS.HumanAircraftRepairStart )
        self:UnHandleEvent( EVENTS.EngineShutdown )
        self:UnHandleEvent( EVENTS.EngineStartup )
        self:UnHandleEvent( EVENTS.WeaponAdd )
        self:UnHandleEvent( EVENTS.WeaponDrop )
        self:UnHandleEvent( EVENTS.WeaponRearm )
        self.FallbackTimer:Stop()
        self:Despawn()
    end
    
    self:I({"Detected client spawn and applied internal functions and events.", PlayerName = self.PlayerName, UnitName = self.UnitName, GroupName = self.GroupName})
    return self
end
--- **Ops** - Manages aircraft CASE X recoveries for carrier operations (X=I, II, III).
--
-- The AIRBOSS class manages recoveries of human pilots and AI aircraft on aircraft carriers.
--
-- **Main Features:**
--
--    * CASE I, II and III recoveries.
--    * Supports human pilots as well as AI flight groups.
--    * Automatic LSO grading including (optional) live grading while in the groove.
--    * Different skill levels from on-the-fly tips for flight students to *ziplip* for pros. Can be set for each player individually.
--    * Define recovery time windows with individual recovery cases in the same mission.
--    * Option to let the carrier steam into the wind automatically.
--    * Automatic TACAN and ICLS channel setting of carrier.
--    * Separate radio channels for LSO and Marshal transmissions.
--    * Voice over support for LSO and Marshal radio transmissions.
--    * Advanced F10 radio menu including carrier info, weather, radio frequencies, TACAN/ICLS channels, player LSO grades, marking of zones etc.
--    * Recovery tanker and refueling option via integration of @{Ops.RecoveryTanker} class.
--    * Rescue helicopter option via @{Ops.RescueHelo} class.
--    * Combine multiple human players to sections.
--    * Many parameters customizable by convenient user API functions.
--    * Multiple carrier support due to object oriented approach.
--    * Unlimited number of players.
--    * Persistence of player results (optional). LSO grading data is saved to csv file.
--    * Trap sheet (optional). 
--    * Finite State Machine (FSM) implementation.
--
-- **Supported Carriers:**
--
--    * [USS John C. Stennis](https://en.wikipedia.org/wiki/USS_John_C._Stennis) (CVN-74)
--    * [USS Theodore Roosevelt](https://en.wikipedia.org/wiki/USS_Theodore_Roosevelt_\(CVN-71\)) (CVN-71) [Super Carrier Module]
--    * [USS Abraham Lincoln](https://en.wikipedia.org/wiki/USS_Abraham_Lincoln_\(CVN-72\)) (CVN-72) [Super Carrier Module]
--    * [USS George Washington](https://en.wikipedia.org/wiki/USS_George_Washington_\(CVN-73\)) (CVN-73) [Super Carrier Module]
--    * [USS Harry S. Truman](https://en.wikipedia.org/wiki/USS_Harry_S._Truman) (CVN-75) [Super Carrier Module]
--    * [USS Forrestal](https://en.wikipedia.org/wiki/USS_Forrestal_\(CV-59\)) (CV-59) [Heatblur Carrier Module]
--    * [Essex Class](https://en.wikipedia.org/wiki/Essex-class_aircraft_carrier) (CV-11) [Magnitude 3 Carrier Module]
--    * [HMS Hermes](https://en.wikipedia.org/wiki/HMS_Hermes_\(R12\)) (R12)
--    * [HMS Invincible](https://en.wikipedia.org/wiki/HMS_Invincible_\(R05\)) (R05)
--    * [USS Tarawa](https://en.wikipedia.org/wiki/USS_Tarawa_\(LHA-1\)) (LHA-1)
--    * [USS America](https://en.wikipedia.org/wiki/USS_America_\(LHA-6\)) (LHA-6)
--    * [Juan Carlos I](https://en.wikipedia.org/wiki/Spanish_amphibious_assault_ship_Juan_Carlos_I) (L61)
--    * [HMAS Canberra](https://en.wikipedia.org/wiki/HMAS_Canberra_\(L02\)) (L02)
--    * BONHOMMERICHARD [VWV Mod]
--    * ENTERPRISE66 [VWV Mod]
--
-- **Supported Aircraft:**
--
--    * [F/A-18C Hornet Lot 20](https://forums.eagle.ru/forumdisplay.php?f=557) (Player & AI)
--    * [F-14A/B Tomcat](https://forums.eagle.ru/forumdisplay.php?f=395) (Player & AI)
--    * [A-4E Skyhawk Community Mod](https://forums.eagle.ru/showthread.php?t=224989) (Player & AI)
--    * [AV-8B N/A Harrier](https://forums.eagle.ru/forumdisplay.php?f=555) (Player & AI)
--    * [T-45C Goshawk](https://forum.dcs.world/topic/203816-vnao-t-45-goshawk/) (VNAO mod) (Player & AI)
--    * [FE/A-18E/F/G Superhornet](https://forum.dcs.world/topic/316971-cjs-super-hornet-community-mod-v20-official-thread/) (CJS mod) (Player & AI)
--    * [F4U-1D Corsair](https://forum.dcs.world/forum/781-f4u-1d/) (Player & AI)
--    * F/A-18C Hornet (AI)
--    * F-14A Tomcat (AI)
--    * E-2D Hawkeye (AI)
--    * S-3B Viking & tanker version (AI)
--    * [C-2A Greyhound](https://forums.eagle.ru/showthread.php?t=255641) (AI)
--
-- At the moment, optimized parameters are available for the F/A-18C Hornet (Lot 20) and A-4E community mod as aircraft and the USS John C. Stennis as carrier.
--
-- The AV-8B Harrier, HMS Hermes, HMS Invincible, the USS Tarawa, USS America, HMAS Canberra, and Juan Carlos I are WIP. The AV-8B harrier and the LHA's and LHD can only be used together, i.e. these ships are the only carriers the harrier is supposed to land on and
-- no other fixed wing aircraft (human or AI controlled) are supposed to land on these ships. Currently only Case I is supported. Case II/III take slightly different steps from the CVN carrier.
-- However, if no offset is used for the holding radial this provides a very close representation of the V/STOL Case III, allowing for an approach to over the deck and a vertical landing.
--
-- Heatblur's mighty F-14B Tomcat has been added (March 13th 2019) as well. Same goes for the A version.
--
-- The [DCS Supercarriers](https://www.digitalcombatsimulator.com/de/shop/modules/supercarrier/) are also supported.
--
-- ## Discussion
--
-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-airboss channel.
-- There you also find an example mission and the necessary voice over sound files. Check out the **pinned messages**.
--
-- ## Example Missions
--
-- Example missions can be found [here](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Ops/Airboss).
-- They contain the latest development Moose.lua file.
--
-- ## IMPORTANT
--
-- Some important restrictions (of DCS) you should be aware of:
--
--    * Each player slot (client) should be in a separate group as DCS does only allow for sending messages to groups and not individual units.
--    * Players are identified by their player name. Hence, ensure that no two player have the same name, e.g. "New Callsign", as this will lead to unexpected results.
--    * The modex (tail number) of an aircraft should **not** be changed dynamically in the mission by a player. Unfortunately, there is no way to get this information via scripting API functions.
--    * The A-4E-C mod needs *easy comms* activated to interact with the F10 radio menu.
--
-- ## Youtube Videos
--
-- ### AIRBOSS videos:
--
--    * [[MOOSE] Airboss - Groove Testing (WIP)](https://www.youtube.com/watch?v=94KHQxxX3UI)
--    * [[MOOSE] Airboss - Groove Test A-4E Community Mod](https://www.youtube.com/watch?v=ZbjD7FHiaHo)
--    * [[MOOSE] Airboss - Groove Test: On-the-fly LSO Grading](https://www.youtube.com/watch?v=Xgs1hwDcPyM)
--    * [[MOOSE] Airboss - Carrier Auto Steam Into Wind](https://www.youtube.com/watch?v=IsU8dYgsp90)
--    * [[MOOSE] Airboss - CASE I Walkthrough in the F/A-18C by TG](https://www.youtube.com/watch?v=o1UrP4Q6PMM)
--    * [[MOOSE] Airboss - New LSO/Marshal Voice Overs by Raynor](https://www.youtube.com/watch?v=_Suo68bRu8k)
--    * [[MOOSE] Airboss - CASE I, "Until We Go Down" featuring the F-14B by Pikes](https://www.youtube.com/watch?v=ojgHDSw3Doc)
--    * [[MOOSE] Airboss - Skipper Menu](https://youtu.be/awnecCxRoNQ)
--
-- ### Jabbers Case I and III Recovery Tutorials:
--
--    * [DCS World - F/A-18 - Case I Carrier Recovery Tutorial](https://www.youtube.com/watch?v=lm-M3VUy-_I)
--    * [DCS World - Case I Recovery Tutorial - Followup](https://www.youtube.com/watch?v=cW5R32Q6xC8)
--    * [DCS World - CASE III Recovery Tutorial](https://www.youtube.com/watch?v=Lnfug5CVAvo)
--
-- ### Wags DCS Hornet Videos:
--
--    * [DCS: F/A-18C Hornet - Episode 9: CASE I Carrier Landing](https://www.youtube.com/watch?v=TuigBLhtAH8)
--    * [DCS: F/A-18C Hornet â€“ Episode 16: CASE III Introduction](https://www.youtube.com/watch?v=DvlMHnLjbDQ)
--    * [DCS: F/A-18C Hornet Case I Carrier Landing Training Lesson Recording](https://www.youtube.com/watch?v=D33uM9q4xgA)
--
-- ### AV-8B Harrier and V/STOL Operations:
--
--    * [Harrier Ship Landing Mission with Auto LSO!](https://www.youtube.com/watch?v=lqmVvpunk2c)
--    * [Updated Airboss V/STOL Features USS Tarawa](https://youtu.be/K7I4pU6j718)
--    * [Harrier Practice pattern USS America](https://youtu.be/99NigITYmcI)
--    * [Harrier CASE III TACAN Approach USS Tarawa](https://www.youtube.com/watch?v=bTgJXZ9Mhdc&t=1s)
--    * [Harrier CASE III TACAN Approach USS Tarawa](https://www.youtube.com/watch?v=wWHag5WpNZ0)
--
-- ===
--
-- ### Author: **funkyfranky** LHA and LHD V/STOL additions by **Pene**
-- ### Special Thanks To **Bankler**
-- For his great [Recovery Trainer](https://forums.eagle.ru/showthread.php?t=221412) mission and script!
-- His work was the initial inspiration for this class. Also note that this implementation uses some routines for determining the player position in Case I recoveries he developed.
-- Bankler was kind enough to allow me to add this to the class - thanks again!
--
-- @module Ops.Airboss
-- @image Ops_Airboss.png

--- AIRBOSS class.
-- @type AIRBOSS
-- @field #string ClassName Name of the class.
-- @field #boolean Debug Debug mode. Messages to all about status.
-- @field #string lid Class id string for output to DCS log file.
-- @field #string theatre The DCS map used in the mission.
-- @field Wrapper.Unit#UNIT carrier Aircraft carrier unit on which we want to practice.
-- @field #string carriertype Type name of aircraft carrier.
-- @field #AIRBOSS.CarrierParameters carrierparam Carrier specific parameters.
-- @field #string alias Alias of the carrier.
-- @field Wrapper.Airbase#AIRBASE airbase Carrier airbase object.
-- @field #table waypoints Waypoint coordinates of carrier.
-- @field #number currentwp Current waypoint, i.e. the one that has been passed last.
-- @field Core.Beacon#BEACON beacon Carrier beacon for TACAN and ICLS.
-- @field #boolean TACANon Automatic TACAN is activated.
-- @field #number TACANchannel TACAN channel.
-- @field #string TACANmode TACAN mode, i.e. "X" or "Y".
-- @field #string TACANmorse TACAN morse code, e.g. "STN".
-- @field #boolean ICLSon Automatic ICLS is activated.
-- @field #number ICLSchannel ICLS channel.
-- @field #string ICLSmorse ICLS morse code, e.g. "STN".
-- @field #AIRBOSS.Radio PilotRadio Radio for Pilot calls.
-- @field #AIRBOSS.Radio LSORadio Radio for LSO calls.
-- @field #number LSOFreq LSO radio frequency in MHz.
-- @field #string LSOModu LSO radio modulation "AM" or "FM".
-- @field #AIRBOSS.Radio MarshalRadio Radio for carrier calls.
-- @field #number MarshalFreq Marshal radio frequency in MHz.
-- @field #string MarshalModu Marshal radio modulation "AM" or "FM".
-- @field #AIRBOSS.Radio AirbossRadio Radio for carrier calls.
-- @field #number AirbossFreq Airboss radio frequency in MHz.
-- @field #string AirbossModu Airboss radio modulation "AM" or "FM".
-- @field #number TowerFreq Tower radio frequency in MHz.
-- @field Core.Scheduler#SCHEDULER radiotimer Radio queue scheduler.
-- @field Core.Zone#ZONE_UNIT zoneCCA Carrier controlled area (CCA), i.e. a zone of 50 NM radius around the carrier.
-- @field Core.Zone#ZONE_UNIT zoneCCZ Carrier controlled zone (CCZ), i.e. a zone of 5 NM radius around the carrier.
-- @field #table players Table of players.
-- @field #table menuadded Table of units where the F10 radio menu was added.
-- @field #AIRBOSS.Checkpoint BreakEntry Break entry checkpoint.
-- @field #AIRBOSS.Checkpoint BreakEarly Early break checkpoint.
-- @field #AIRBOSS.Checkpoint BreakLate Late break checkpoint.
-- @field #AIRBOSS.Checkpoint Abeam Abeam checkpoint.
-- @field #AIRBOSS.Checkpoint Ninety At the ninety checkpoint.
-- @field #AIRBOSS.Checkpoint Wake Checkpoint right behind the carrier.
-- @field #AIRBOSS.Checkpoint Final Checkpoint when turning to final.
-- @field #AIRBOSS.Checkpoint Groove In the groove checkpoint.
-- @field #AIRBOSS.Checkpoint Platform Case II/III descent at 2000 ft/min at 5000 ft platform.
-- @field #AIRBOSS.Checkpoint DirtyUp Case II/III dirty up and on speed position at 1200 ft and 10-12 NM from the carrier.
-- @field #AIRBOSS.Checkpoint Bullseye Case III intercept glideslope and follow ICLS aka "bullseye".
-- @field #number defaultcase Default recovery case. This is the case used if not specified otherwise.
-- @field #number case Recovery case I, II or III currently in progress.
-- @field #table recoverytimes List of time windows when aircraft are recovered including the recovery case and holding offset.
-- @field #number defaultoffset Default holding pattern update if not specified otherwise.
-- @field #number holdingoffset Offset [degrees] of Case II/III holding pattern.
-- @field #table flights List of all flights in the CCA.
-- @field #table Qmarshal Queue of marshalling aircraft groups.
-- @field #table Qpattern Queue of aircraft groups in the landing pattern.
-- @field #table Qwaiting Queue of aircraft groups waiting outside 10 NM zone for the next free Marshal stack.
-- @field #table Qspinning Queue of aircraft currently spinning.
-- @field #table RQMarshal Radio queue of marshal.
-- @field #number TQMarshal Abs mission time, the last transmission ended.
-- @field #table RQLSO Radio queue of LSO.
-- @field #number TQLSO Abs mission time, the last transmission ended.
-- @field #number Nmaxpattern Max number of aircraft in landing pattern.
-- @field #number Nmaxmarshal Number of max Case I Marshal stacks available. Default 3, i.e. angels 2, 3 and 4.
-- @field #number NmaxSection Number of max section members (excluding the lead itself), i.e. NmaxSection=1 is a section of two.
-- @field #number NmaxStack Number of max flights per stack. Default 2.
-- @field #boolean handleai If true (default), handle AI aircraft.
-- @field Ops.RecoveryTanker#RECOVERYTANKER tanker Recovery tanker flying overhead of carrier.
-- @field DCS#Vec3 Corientation Carrier orientation in space.
-- @field DCS#Vec3 Corientlast Last known carrier orientation.
-- @field Core.Point#COORDINATE Cposition Carrier position.
-- @field #string defaultskill Default player skill @{#AIRBOSS.Difficulty}.
-- @field #boolean adinfinitum If true, carrier patrols ad infinitum, i.e. when reaching its last waypoint it starts at waypoint one again.
-- @field #number magvar Magnetic declination in degrees.
-- @field #number Tcollapse Last time timer.gettime() the stack collapsed.
-- @field #AIRBOSS.Recovery recoverywindow Current or next recovery window opened.
-- @field #boolean usersoundradio Use user sound output instead of radio transmissions.
-- @field #number Tqueue Last time in seconds of timer.getTime() the queue was updated.
-- @field #number dTqueue Time interval in seconds for updating the queues etc.
-- @field #number dTstatus Time interval for call FSM status updates.
-- @field #boolean menumarkzones If false, disables the option to mark zones via smoke or flares.
-- @field #boolean menusmokezones If false, disables the option to mark zones via smoke.
-- @field #table playerscores Table holding all player scores and grades.
-- @field #boolean autosave If true, all player grades are automatically saved to a file on disk.
-- @field #string autosavepath Path where the player grades file is saved on auto save.
-- @field #string autosavefilename File name of the auto player grades save file. Default is auto generated from carrier name/alias.
-- @field #number marshalradius Radius of the Marshal stack zone.
-- @field #boolean airbossnice Airboss is a nice guy.
-- @field #boolean staticweather Mission uses static rather than dynamic weather.
-- @field #number windowcount Running number counting the recovery windows.
-- @field #number LSOdT Time interval in seconds before the LSO will make its next call.
-- @field #string senderac Name of the aircraft acting as sender for broadcasting radio messages from the carrier. DCS shortcoming workaround.
-- @field #string radiorelayLSO Name of the aircraft acting as sender for broadcasting LSO radio messages from the carrier. DCS shortcoming workaround.
-- @field #string radiorelayMSH Name of the aircraft acting as sender for broadcasting Marhsal radio messages from the carrier. DCS shortcoming workaround.
-- @field #boolean turnintowind If true, carrier is currently turning into the wind.
-- @field #boolean detour If true, carrier is currently making a detour from its path along the ME waypoints.
-- @field Core.Point#COORDINATE Creturnto Position to return to after turn into the wind leg is over.
-- @field Core.Set#SET_GROUP squadsetAI AI groups in this set will be handled by the airboss.
-- @field Core.Set#SET_GROUP excludesetAI AI groups in this set will be explicitly excluded from handling by the airboss and not forced into the Marshal pattern.
-- @field #boolean menusingle If true, menu is optimized for a single carrier.
-- @field #number collisiondist Distance up to which collision checks are done.
-- @field #number holdtimestamp Timestamp when the carrier first came to an unexpected hold.
-- @field #number Tmessage Default duration in seconds messages are displayed to players.
-- @field #string soundfolder Folder within the mission (miz) file where airboss sound files are located.
-- @field #string soundfolderLSO Folder withing the mission (miz) file where LSO sound files are stored.
-- @field #string soundfolderMSH Folder withing the mission (miz) file where Marshal sound files are stored.
-- @field #boolean despawnshutdown Despawn group after engine shutdown.
-- @field #number Tbeacon Last time the beacons were refeshed.
-- @field #number dTbeacon Time interval to refresh the beacons. Default 5 minutes.
-- @field #AIRBOSS.LSOCalls LSOCall Radio voice overs of the LSO.
-- @field #AIRBOSS.MarshalCalls MarshalCall Radio voice over of the Marshal/Airboss.
-- @field #AIRBOSS.PilotCalls PilotCall Radio voice over from AI pilots.
-- @field #number lowfuelAI Low fuel threshold for AI groups in percent.
-- @field #boolean emergency If true (default), allow emergency landings, i.e. bypass any pattern and go for final approach.
-- @field #boolean respawnAI If true, respawn AI flights as they enter the CCA to detach and airfields from the mission plan. Default false.
-- @field #boolean turning If true, carrier is currently turning.
-- @field #AIRBOSS.GLE gle Glidesope error thresholds.
-- @field #AIRBOSS.LUE lue Lineup error thresholds.
-- @field #boolean trapsheet If true, players can save their trap sheets.
-- @field #string trappath Path where to save the trap sheets.
-- @field #string trapprefix File prefix for trap sheet files.
-- @field #number initialmaxalt Max altitude in meters to register in the inital zone.
-- @field #boolean welcome If true, display welcome message to player.
-- @field #boolean skipperMenu If true, add skipper menu.
-- @field #number skipperSpeed Speed in knots for manual recovery start.
-- @field #number skipperCase Manual recovery case.
-- @field #boolean skipperUturn U-turn on/off via menu.
-- @field #number skipperOffset Holding offset angle in degrees for Case II/III manual recoveries.
-- @field #number skipperTime Recovery time in min for manual recovery.
-- @field #boolean intowindold If true, use old into wind calculation.
-- @extends Core.Fsm#FSM

--- Be the boss!
--
-- ===
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Main.png)
--
-- # The AIRBOSS Concept
--
-- On a carrier, the AIRBOSS is guy who is really in charge - don't mess with him!
--
-- # Recovery Cases
--
-- The AIRBOSS class supports all three commonly used recovery cases, i.e.
--
--    * **CASE I** during daytime and good weather (ceiling > 3000 ft, visibility > 5 NM),
--    * **CASE II** during daytime but poor visibility conditions (ceiling > 1000 ft, visibility > 5NM),
--    * **CASE III** when below Case II conditions and during nighttime (ceiling < 1000 ft, visibility < 5 NM).
--
-- That being said, this script allows you to use any of the three cases to be used at any time. Or, in other words, *you* need to specify when which case is safe and appropriate.
--
-- This is a lot of responsibility. *You* are the boss, but *you* need to make the right decisions or things will go terribly wrong!
--
-- Recovery windows can be set up via the @{#AIRBOSS.AddRecoveryWindow} function as explained below. With this it is possible to seamlessly (within reason!) switch recovery cases in the same mission.
--
-- ## CASE I
--
-- As mentioned before, Case I recovery is the standard procedure during daytime and good visibility conditions.
--
-- ### Holding Pattern
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Holding.png)
--
-- The graphic depicts a the standard holding pattern during a Case I recovery. Incoming aircraft enter the holding pattern, which is a counter clockwise turn with a
-- diameter of 5 NM, at their assigned altitude. The holding altitude of the first stack is 2000 ft. The interval between stacks is 1000 ft.
--
-- Once a recovery window opens, the aircraft of the lowest stack commence their landing approach and the rest of the Marshal stack collapses, i.e. aircraft switch from
-- their current stack to the next lower stack.
--
-- The flight that transitions form the holding pattern to the landing approach, it should leave the Marshal stack at the 3 position and make a left hand turn to the *Initial*
-- position, which is 3 NM astern of the boat. Note that you need to be below 1300 feet to be registered in the initial zone.
-- The altitude can be set via the function @{#AIRBOSS.SetInitialMaxAlt}(*altitude*) function.
-- As described below, the initial zone can be smoked or flared via the AIRBOSS F10 Help radio menu.
--
-- ### Landing Pattern
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1_Landing.png)
--
-- Once the aircraft reaches the Initial, the landing pattern begins. The important steps of the pattern are shown in the image above.
-- The AV-8B Harrier pattern is very similar, the only differences are as there is no angled deck there is no wake check. from the ninety you wil fly a straight approach offset 26 ft to port (left) of the tram line.
-- The aim is to arrive abeam the landing spot in a stable hover at 120 ft with forward speed matched to the boat. From there the LSO will call "cleared to land". You then level cross to the tram line at the designated landing spot at land vertcally. When you stabalise over the landing spot LSO will call Stabalise to indicate you are centered at the correct spot.
--
-- ## CASE III
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3.png)
--
-- A Case III recovery is conducted during nighttime or when the visibility is below CASE II minima during the day. The holding position and the landing pattern are rather different from a Case I recovery as can be seen in the image above.
--
-- The first holding zone starts 21 NM astern the carrier at angels 6. The separation between the stacks is 1000 ft just like in Case I. However, the distance to the boat
-- increases by 1 NM with each stack. The general form can be written as D=15+6+(N-1), where D is the distance to the boat in NM and N the number of the stack starting at N=1.
--
-- Once the aircraft of the lowest stack is allowed to commence to the landing pattern, it starts a descent at 4000 ft/min until it reaches the "*Platform*" at 5000 ft and
-- ~19 NM DME. From there a shallower descent at 2000 ft/min should be performed. At an altitude of 1200 ft the aircraft should level out and "*Dirty Up*" (gear, flaps & hook down).
--
-- At 3 NM distance to the carrier, the aircraft should intercept the 3.5 degrees glideslope at the "*Bullseye*". From there the pilot should "follow the needles" of the ICLS.
--
-- ## CASE II
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case2.png)
--
-- Case II is the common recovery procedure at daytime if visibility conditions are poor. It can be viewed as hybrid between Case I and III.
-- The holding pattern is very similar to that of the Case III recovery with the difference the the radial is the inverse of the BRC instead of the FB.
-- From the holding zone aircraft are follow the Case III path until they reach the Initial position 3 NM astern the boat. From there a standard Case I recovery procedure is
-- in place.
--
-- Note that the image depicts the case, where the holding zone has an angle offset of 30 degrees with respect to the BRC. This is optional. Commonly used offset angels
-- are 0 (no offset), +-15 or +-30 degrees. The AIRBOSS class supports all these scenarios which are used during Case II and III recoveries.
--
-- ===
--
-- # The F10 Radio Menu
--
-- The F10 radio menu can be used to post requests to Marshal but also provides information about the player and carrier status. Additionally, helper functions
-- can be called.
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMain.png)
--
-- By default, the script creates a submenu "Airboss" in the "F10 Other ..." menu and each @{#AIRBOSS} carrier gets its own submenu.
-- If you intend to have only one carrier, you can simplify the menu structure using the @{#AIRBOSS.SetMenuSingleCarrier} function, which will create all carrier specific menu entries directly
-- in the "Airboss" submenu. (Needless to say, that if you enable this and define multiple carriers, the menu structure will get completely screwed up.)
--
-- ## Root Menu
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuRoot.png)
--
-- The general structure
--
--    * **F1 Help...**  (Help submenu, see below.)
--    * **F2 Kneeboard...** (Kneeboard submenu, see below. Carrier information, weather report, player status.)
--    * **F3 Request Marshal**
--    * **F4 Request Commence**
--    * **F5 Request Refueling**
--    * **F6 Spinning**
--    * **F7 Emergency Landing**
--    * **F8 [Reset My Status]**
--
-- ### Request Marshal
--
-- This radio command can be used to request a stack in the holding pattern from Marshal. Necessary conditions are that the flight is inside the Carrier Controlled Area (CCA)
-- (see @{#AIRBOSS.SetCarrierControlledArea}).
--
-- Marshal will assign an individual stack for each player group depending on the current or next open recovery case window.
-- If multiple players have registered as a section, the section lead will be assigned a stack and is responsible to guide his section to the assigned holding position.
--
-- ### Request Commence
--
-- This command can be used to request commencing from the marshal stack to the landing pattern. Necessary condition is that the player is in the lowest marshal stack
-- and that the number of aircraft in the landing pattern is smaller than four (or the number set by the mission designer).
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case1Pattern.png)
--
-- The image displays the standard Case I Marshal pattern recovery. Pilots are supposed to fly a clockwise circle and descent between the **3** and **1** positions.
--
-- Commence should be performed at around the **3** position. If the pilot is in the lowest Marshal stack, and flies through this area, he is automatically cleared for the
-- landing pattern. In other words, there is no need for the "Request Commence" radio command. The zone can be marked via smoke or flared using the player's F10 radio menu.
--
-- A player can also request commencing if he is not registered in a marshal stack yet. If the pattern is free, Marshal will allow him to directly enter the landing pattern.
-- However, this is only possible when the Airboss has a nice day - see @{#AIRBOSS.SetAirbossNiceGuy}.
--
-- ### Request Refueling
--
-- If a recovery tanker has been set up via the @{#AIRBOSS.SetRecoveryTanker}, the player can request refueling at any time. If currently in the marshal stack, the stack above will collapse.
-- The player will be informed if the tanker is currently busy or going RTB to refuel itself at its home base. Once the re-fueling is complete, the player has to re-register to the marshal stack.
--
-- ### Spinning
--
-- If the pattern is full, players can go into the spinning pattern. This step is only allowed, if the player is in the pattern and his next step
-- is initial, break entry, early/late break. At this point, the player should climb to 1200 ft a fly on the port side of the boat to go back to the initial again.
--
-- If a player is in the spin pattern, flights in the Marshal queue should hold their altitude and are not allowed into the pattern until the spinning aircraft
-- proceeds.
--
-- Once the player reaches a point 100 meters behind the boat and at least 1 NM port, his step is set to "Initial" and he can resume the normal pattern approach.
--
-- If necessary, the player can call "Spinning" again when in the above mentioned steps.
--
-- ### Emergency Landing
--
-- Request an emergency landing, i.e. bypass all pattern steps and go directly to the final approach.
--
-- All section members are supposed to follow. Player (or section lead) is removed from all other queues and automatically added to the landing pattern queue.
--
-- If this command is called while the player is currently on the carrier, he will be put in the bolter pattern. So the next expected step after take of
-- is the abeam position. This allows for quick landing training exercises without having to go through the whole pattern.
--
-- The mission designer can forbid this option my setting @{#AIRBOSS.SetEmergencyLandings}(false) in the script.
--
-- ### [Reset My Status]
--
-- This will reset the current player status. If player is currently in a marshal stack, he will be removed from the marshal queue and the stack above will collapse.
-- The player needs to re-register later if desired. If player is currently in the landing pattern, he will be removed from the pattern queue.
--
-- ## Help Menu
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuHelp.png)
--
-- This menu provides commands to help the player.
--
-- ### Mark Zones Submenu
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMarkZones.png)
--
-- These commands can be used to mark marshal or landing pattern zones.
--
--    * **Smoke Pattern Zones** Smoke is used to mark the landing pattern zone of the player depending on his recovery case.
--    For Case I this is the initial zone. For Case II/III and three these are the Platform, Arc turn, Dirty Up, Bullseye/Initial zones as well as the approach corridor.
--    * **Flare Pattern Zones** Similar to smoke but uses flares to mark the pattern zones.
--    * **Smoke Marshal Zone** This smokes the surrounding area of the currently assigned Marshal zone of the player. Player has to be registered in Marshal queue.
--    * **Flare Marshal Zone** Similar to smoke but uses flares to mark the Marshal zone.
--
-- Note that the smoke lasts ~5 minutes but the zones are moving along with the carrier. So after some time, the smoke gives shows you a picture of the past.
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_Case3_FlarePattern.png)
--
-- ### Skill Level Submenu
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuSkill.png)
--
-- The player can choose between three skill or difficulty levels.
--
--    * **Flight Student**: The player receives tips at certain stages of the pattern, e.g. if he is at the right altitude, speed, etc.
--    * **Naval Aviator**: Less tips are show. Player should be familiar with the procedures and its aircraft parameters.
--    * **TOPGUN Graduate**: Only very few information is provided to the player. This is for the pros.
--    * **Hints On/Off**: Toggle displaying hints.
--
-- ### My Status
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMyStatus.png)
--
-- This command provides information about the current player status. For example, his current step in the pattern.
--
-- ### Attitude Monitor
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuAttitudeMonitor.png)
--
-- This command displays the current aircraft attitude of the player aircraft in short intervals as message on the screen.
-- It provides information about current pitch, roll, yaw, orientation of the plane with respect to the carrier's orientation (*Gamma*) etc.
--
-- If you are in the groove, current lineup and glideslope errors are displayed and you get an on-the-fly LSO grade.
--
-- ### LSO Radio Check
--
-- LSO will transmit a short message on his radio frequency. See @{#AIRBOSS.SetLSORadio}. Note that in the A-4E you will not hear the message unless you are in the pattern.
--
-- ### Marshal Radio Check
--
-- Marshal will transmit a short message on his radio frequency. See @{#AIRBOSS.SetMarshalRadio}.
--
-- ### Subtitles On/Off
--
-- This command toggles the display of radio message subtitles if no radio relay unit is used. By default subtitles are on.
-- Note that subtitles for radio messages which do not have a complete voice over are always displayed.
--
-- ### Trapsheet On/Off
--
-- Each player can activated or deactivate the recording of his flight data (AoA, glideslope, lineup, etc.) during his landing approaches.
-- Note that this feature also has to be enabled by the mission designer.
--
-- ## Kneeboard Menu
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuKneeboard.png)
--
-- The Kneeboard menu provides information about the carrier, weather and player results.
--
-- ### Results Submenu
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuResults.png)
--
-- Here you find your LSO grading results as well as scores of other players.
--
--    * **Greenie Board** lists average scores of all players obtained during landing approaches.
--    * **My LSO Grades** lists all grades the player has received for his approaches in this mission.
--    * **Last Debrief** shows the detailed debriefing of the player's last approach.
--
-- ### Carrier Info
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuCarrierInfo.png)
--
-- Information about the current carrier status is displayed. This includes current BRC, FB, LSO and Marshal frequencies, list of next recovery windows.
--
-- ### Weather Report
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuWeatherReport.png)
--
-- Displays information about the current weather at the carrier such as QFE, wind and temperature.
--
-- For missions using static weather, more information such as cloud base, thickness, precipitation, visibility distance, fog and dust are displayed.
-- If your mission uses dynamic weather, you can disable this output via the @{#AIRBOSS.SetStaticWeather}(**false**) function.
--
-- ### Set Section
--
-- With this command, you can define a section of human flights. The player who issues the command becomes the section lead and all other human players
-- within a radius of 100 meters become members of the section.
--
-- The responsibilities of the section leader are:
--
--    * To request Marshal. The section members are not allowed to do this and have to follow the lead to his assigned stack.
--    * To lead the right way to the pattern if the flight is allowed to commence.
--    * The lead is also the only one who can request commence if the flight wants to bypass the Marshal stack.
--
-- Each time the command is issued by the lead, the complete section is set up from scratch. Members which are not inside the 100 m radius any more are
-- removed and/or new members which are now in range are added.
--
-- If a section member issues this command, it is removed from the section of his lead. All flights which are not yet in another section will become members.
--
-- The default maximum size of a section is two human players. This can be adjusted by the @{#AIRBOSS.SetMaxSectionSize}(*size*) function. The maximum allowed size
-- is four.
--
-- ### Marshal Queue
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuMarshalQueue.png)
--
-- Lists all flights currently in the Marshal queue including their assigned stack, recovery case and Charlie time estimate.
-- By default, the number of available Case I stacks is three, i.e. at angels 2, 3 and 4. Usually, the recovery thanker orbits at angels 6.
-- The number of available stacks can be set by the @{#AIRBOSS.SetMaxMarshalStack} function.
--
-- The default number of human players per stack is two. This can be set via the @{#AIRBOSS.SetMaxFlightsPerStack} function but has to be between one and four.
--
-- Due to technical reasons, each AI group always gets its own stack. DCS does not allow to control the AI in a manner that more than one group per stack would make sense unfortunately.
--
-- ### Pattern Queue
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_MenuPatternQueue.png)
--
-- Lists all flights currently in the landing pattern queue showing the time since they entered the pattern.
-- By default, a maximum of four flights is allowed to enter the pattern. This can be set via the @{#AIRBOSS.SetMaxLandingPattern} function.
--
-- ### Waiting Queue
--
-- Lists all flights currently waiting for a free Case I Marshal stack. Note, stacks are limited only for Case I recovery ops but not for Case II or III.
-- If the carrier is switches recovery ops form Case I to Case II or III, all waiting flights will be assigned a stack.
--
-- # Landing Signal Officer (LSO)
--
-- The LSO will first contact you on his radio channel when you are at the the abeam position (Case I) with the phrase "Paddles, contact.".
-- Once you are in the groove the LSO will ask you to "Call the ball." and then acknowledge your ball call by "Roger Ball."
--
-- During the groove the LSO will give you advice if you deviate from the correct landing path. These advices will be given when you are
--
--    * too low or too high with respect to the glideslope,
--    * too fast or too slow with respect to the optimal AoA,
--    * too far left or too far right with respect to the lineup of the (angled) runway.
--
-- ## LSO Grading
--
-- LSO grading starts when the player enters the groove. The flight path and aircraft attitude is evaluated at certain steps (distances measured from rundown):
--
--    * **X** At the Start (0.75 NM = 1390 m).
--    * **IM** In the Middle (0.5 NM = 926 m), middle one third of the glideslope.
--    * **IC** In Close (0.25 NM = 463 m), last one third of the glideslope.
--    * **AR** At the Ramp (0.027 NM = 50 m).
--    * **IW** In the Wires (at the landing position).
--
-- Grading at each step includes the above calls, i.e.
--
--    * **L**ined **U**p **L**eft or **R**ight: LUL, LUR
--    * Too **H**igh or too **LO**w: H, LO
--    * Too **F**ast or too **SLO**w: F, SLO
--    * **O**ver**S**hoot: OS, only referenced during **X**
--    * **Fly through** glideslope **down** or **up**: \\ , /, advisory only
--    * **D**rift **L**eft or **R**ight:DL, DR, advisory only
--    * **A**ngled **A**pproach: Angled approach (wings level and LUL): AA, advisory only
--
-- Each grading, x, is subdivided by
--
--    * (x): parenthesis, indicating "a little" for a minor deviation and
--    * \_x\_: underline, indicating "a lot" for major deviations.
--
-- The position at the landing event is analyzed and the corresponding trapped wire calculated. If no wire was caught, the LSO will give the bolter call.
--
-- If a player is significantly off from the ideal parameters from IC to AR, the LSO will wave the player off. Thresholds for wave off are
--
--    * Line up error > 3.0 degrees left or right and/or
--    * Glideslope error < -1.2 degrees or > 1.8 degrees and/or
--    * AOA depending on aircraft type and only applied if skill level is "TOPGUN graduate".
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_LSOPlatcam.png)
--
-- Line up and glideslope error thresholds were tested extensively using [VFA-113 Stingers LSO Mod](https://forums.eagle.ru/showthread.php?t=211557),
-- if the aircraft is outside the red box. In the picture above, **blue** numbers denote the line up thresholds while the **blacks** refer to the glideslope.
--
-- A wave off is called, when the aircraft is outside the red rectangle. The measurement stops already ~50 m before the rundown, since the error in the calculation
-- increases the closer the aircraft gets to the origin/reference point.
--
-- The optimal glideslope is assumed to be 3.5 degrees leading to a touch down point between the second and third wire.
-- The height of the carrier deck and the exact wire locations are taken into account in the calculations.
--
-- ## Pattern Waveoff
--
-- The player's aircraft position is evaluated at certain critical locations in the landing pattern. If the player is far off from the ideal approach, the LSO will
-- issue a pattern wave off. Currently, this is only implemented for Case I recoveries and the Case I part in the Case II recovery, i.e.
--
--    * Break Entry
--    * Early Break
--    * Late Break
--    * Abeam
--    * Ninety
--    * Wake
--    * Groove
--
-- At these points it is also checked if a player comes too close to another aircraft ahead of him in the pattern.
--
-- ## Grading Points
--
-- Currently grades are given by as follows
--
--    * 5.0 Points **\_OK\_**: "Okay underline", given only for a perfect pass, i.e. when no deviations at all were observed by the LSO. The unicorn!
--    * 4.0 Points **OK**: "Okay pass" when only minor () deviations happened.
--    * 3.0 Points **(OK)**: "Fair pass", when only "normal" deviations were detected.
--    * 2.0 Points **--**: "No grade", for larger deviations.
--
-- Furthermore, we have the cases:
--
--    * 2.5 Points **B**: "Bolter", when the player landed but did not catch a wire.
--    * 2.0 Points **WOP**: "Pattern Wave-Off", when pilot was far away from where he should be in the pattern.
--    * 2.0 Points **OWO**: "Own Wave-Off**, when pilot flies past the deck without touching it.
--    * 1.0 Points **WO**: "Technique Wave-Off": Player got waved off in the final parts of the groove.
--    * 1.0 Points **LIG**: "Long In the Groove", when pilot extents the downwind leg too far and screws up the timing for the following aircraft.
--    * 0.0 Points **CUT**: "Cut pass", when player was waved off but landed anyway. In addition if a V/STOL lands without having been Cleared to Land.
--
-- ## Foul Deck Waveoff
--
-- A foul deck waveoff is called by the LSO if an aircraft is detected within the landing area when an approaching aircraft is at position IM-IC during Case I/II operations,
-- or with an aircraft approaching the 3/4 NM during Case III operations.
--
-- The approaching aircraft will be notified via LSO radio comms and is supposed to overfly the landing area to enter the Bolter pattern. **This pass is not graded**.
--
-- ===
--
-- # Scripting
--
-- Writing a basic script is easy and can be done in two lines.
--
--     local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis")
--     airbossStennis:Start()
--
-- The **first line** creates and AIRBOSS object via the @{#AIRBOSS.New}(*carriername*, *alias*) constructor. The first parameter *carriername* is name of the carrier unit as
-- defined in the mission editor. The second parameter *alias* is optional. This name will, e.g., be used for the F10 radio menu entry. If not given, the alias is identical
-- to the *carriername* of the first parameter.
--
-- This simple script initializes a lot of parameters with default values:
--
--    * TACAN channel is set to 74X, see @{#AIRBOSS.SetTACAN},
--    * ICSL channel is set to 1, see @{#AIRBOSS.SetICLS},
--    * LSO radio is set to 264 MHz FM, see @{#AIRBOSS.SetLSORadio},
--    * Marshal radio is set to 305 MHz FM, see @{#AIRBOSS.SetMarshalRadio},
--    * Default recovery case is set to 1, see @{#AIRBOSS.SetRecoveryCase},
--    * Carrier Controlled Area (CCA) is set to 50 NM, see @{#AIRBOSS.SetCarrierControlledArea},
--    * Default player skill "Flight Student" (easy), see @{#AIRBOSS.SetDefaultPlayerSkill},
--    * Once the carrier reaches its final waypoint, it will restart its route, see @{#AIRBOSS.SetPatrolAdInfinitum}.
--
-- The **second line** starts the AIRBOSS class. If you set options this should happen after the @{#AIRBOSS.New} and before @{#AIRBOSS.Start} command.
--
-- However, good mission planning involves also planning when aircraft are supposed to be launched or recovered. The definition of *case specific* recovery ops within the same mission is described in
-- the next section.
--
-- ## Recovery Windows
--
-- Recovery of aircraft is only allowed during defined time slots. You can define these slots via the @{#AIRBOSS.AddRecoveryWindow}(*start*, *stop*, *case*, *holdingoffset*) function.
-- The parameters are:
--
--   * *start*: The start time as a string. For example "8:00" for a window opening at 8 am. Or "13:30+1" for half past one on the next day. Default (nil) is ASAP.
--   * *stop*: Time when the window closes as a string. Same format as *start*. Default is 90 minutes after start time.
--   * *case*: The recovery case during that window (1, 2 or 3). Default 1.
--   * *holdingoffset*: Holding offset angle in degrees. Only for Case II or III recoveries. Default 0 deg. Common +-15 deg or +-30 deg.
--
-- If recovery is closed, AI flights will be send to marshal stacks and orbit there until the next window opens.
-- Players can request marshal via the F10 menu and will also be given a marshal stack. Currently, human players can request commence via the F10 radio regardless of
-- whether a window is open or not and will be allowed to enter the pattern (if not already full). This will probably change in the future.
--
-- At the moment there is no automatic recovery case set depending on weather or daytime. So it is the AIRBOSS (i.e. you as mission designer) who needs to make that decision.
-- It is probably a good idea to synchronize the timing with the waypoints of the carrier. For example, setting up the waypoints such that the carrier
-- already has turning into the wind, when a recovery window opens.
--
-- The code for setting up multiple recovery windows could look like this
--     local airbossStennis=AIRBOSS:New("USS Stennis", "Stennis")
--     airbossStennis:AddRecoveryWindow("8:30", "9:30", 1)
--     airbossStennis:AddRecoveryWindow("12:00", "13:15", 2, 15)
--     airbossStennis:AddRecoveryWindow("23:30", "00:30+1", 3, -30)
--     airbossStennis:Start()
--
-- This will open a Case I recovery window from 8:30 to 9:30. Then a Case II recovery from 12:00 to 13:15, where the holing offset is +15 degrees wrt BRC.
-- Finally, a Case III window opens 23:30 on the day the mission starts and closes 0:30 on the following day. The holding offset is -30 degrees wrt FB.
--
-- Note that incoming flights will be assigned a holding pattern for the next opening window case if no window is open at the moment. So in the above example,
-- all flights incoming after 13:15 will be assigned to a Case III marshal stack. Therefore, you should make sure that no flights are incoming long before the
-- next window opens or adjust the recovery planning accordingly.
--
-- The following example shows how you set up a recovery window for the next week:
--
--     for i=0,7 do
--        airbossStennis:AddRecoveryWindow(string.format("08:05:00+%d", i), string.format("08:50:00+%d", i))
--     end
--
-- ### Turning into the Wind
--
-- For each recovery window, you can define if the carrier should automatically turn into the wind. This is done by passing one or two additional arguments to the @{#AIRBOSS.AddRecoveryWindow} function:
--
--     airbossStennis:AddRecoveryWindow("8:30", "9:30", 1, nil, true, 20)
--
-- Setting the fifth parameter to *true* enables the automatic turning into the wind. The sixth parameter (here 20) specifies the speed in knots the carrier will go so that to total wind above the deck
-- corresponds to this wind speed. For example, if the is blowing with 5 knots, the carrier will go 15 knots so that the total velocity adds up to the specified 20 knots for the pilot.
--
-- The carrier will steam into the wind for as long as the recovery window is open. The distance up to which possible collisions are detected can be set by the @{#AIRBOSS.SetCollisionDistance} function.
--
-- However, the AIRBOSS scans the type of the surface up to 5 NM in the direction of movement of the carrier. If he detects anything but deep water, he will stop the current course and head back to
-- the point where he initially turned into the wind.
--
-- The same holds true after the recovery window closes. The carrier will head back to the place where he left its assigned route and resume the path to the next waypoint defined in the mission editor.
--
-- Note that the carrier will only head into the wind, if the wind direction is different by more than 5° from the current heading of the carrier (the angled runway, if any, fis taken into account here).
--
-- ===
--
-- # Persistence of Player Results
--
-- LSO grades of players can be saved to disk and later reloaded when a new mission is started.
--
-- ## Prerequisites
--
-- **Important** By default, DCS does not allow for writing data to files. Therefore, one first has to comment out the line "sanitizeModule('io')" and "sanitizeModule('lfs')", i.e.
--
--     do
--       sanitizeModule('os')
--       --sanitizeModule('io')    -- required for saving files
--       --sanitizeModule('lfs')   -- optional for setting the default path to your "Saved Games\DCS" folder
--       require = nil
--       loadlib = nil
--     end
--
-- in the file "MissionScripting.lua", which is located in the subdirectory "Scripts" of your DCS installation root directory.
--
-- **WARNING** Desanitizing the "io" and "lfs" modules makes your machine or server vulnerable to attacks from the outside! Use this at your own risk.
--
-- ## Save Results
--
-- Saving asset data to file is achieved by the @{#AIRBOSS.Save}(*path*, *filename*) function.
--
-- The parameter *path* specifies the path on the file system where the
-- player grades are saved. If you do not specify a path, the file is saved your the DCS installation root directory if the **lfs** module is *not* desanizied or
-- your "Saved Games\\DCS" folder in case you did desanitize the **lfs** module.
--
-- The parameter *filename* is optional and defines the name of the saved file. By default this is automatically created from the AIRBOSS carrier name/alias, i.e.
-- "Airboss-USS Stennis_LSOgrades.csv", if the alias is "USS Stennis".
--
-- In the easiest case, you desanitize the **io** and **lfs** modules and just add the line
--
--     airbossStennis:Save()
--
-- If you want to specify an explicit path you can do this by
--
--     airbossStennis:Save("D:\\My Airboss Data\\")
--
-- This will save all player grades to in "D:\\My Airboss Data\\Airboss-USS Stennis_LSOgrades.csv".
--
-- ### Automatic Saving
--
-- The player grades can be saved automatically after each graded player pass via the @{#AIRBOSS.SetAutoSave}(*path*, *filename*) function. Again the parameters *path* and *filename* are optional.
-- In the simplest case, you desanitize the **lfs** module and just add
--
--     airbossStennis:SetAutoSave()
--
-- Note that the the stats are saved after the *final* grade has been given, i.e. the player has landed on the carrier. After intermediate results such as bolters or waveoffs the stats are not automatically saved.
--
-- In case you want to specify an explicit path, you can write
--
--     airbossStennis:SetAutoSave("D:\\My Airboss Data\\")
--
-- ## Results Output
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_PersistenceResultsTable.png)
--
-- The results file is stored as comma separated file. The columns are
--
--    * *Name*: The player name.
--    * *Pass*: A running number counting the passes of the player
--    * *Points Final*: The final points (i.e. when the player has landed). This is the average over all previous bolters or waveoffs, if any.
--    * *Points Pass*: The points of each pass including bolters and waveoffs.
--    * *Grade*: LSO grade.
--    * *Details*: Detailed analysis of deviations within the groove.
--    * *Wire*: Trapped wire, if any.
--    * *Tgroove*: Time in the groove in seconds (not applicable during Case III).
--    * *Case*: The recovery case operations in progress during the pass.
--    * *Wind*: Wind on deck in knots during approach.
--    * *Modex*: Tail number of the player.
--    * *Airframe*: Aircraft type used in the recovery.
--    * *Carrier Type*: Type name of the carrier.
--    * *Carrier Name*: Name/alias of the carrier.
--    * *Theatre*: DCS map.
--    * *Mission Time*: Mission time at the end of the approach.
--    * *Mission Date*: Mission date in yyyy/mm/dd format.
--    * *OS Date*: Real life date from os.date(). Needs **os** to be desanitized.
--
-- ## Load Results
--
-- Loading player grades from file is achieved by the @{#AIRBOSS.Load}(*path*, *filename*) function. The parameter *path* specifies the path on the file system where the
-- data is loaded from. If you do not specify a path, the file is loaded from your the DCS installation root directory or, if **lfs** was desanitized from you "Saved Games\DCS" directory.
-- The parameter *filename* is optional and defines the name of the file to load. By default this is automatically generated from the AIBOSS carrier name/alias, for example
-- "Airboss-USS Stennis_LSOgrades.csv".
--
-- Note that the AIRBOSS FSM **must not be started** in order to load the data. In other words, loading should happen **after** the
-- @{#AIRBOSS.New} command is specified in the code but **before** the @{#AIRBOSS.Start} command is given.
--
-- The easiest was to load player results is
--
--     airbossStennis:New("USS Stennis")
--     airbossStennis:Load()
--     airbossStennis:SetAutoSave()
--     -- Additional specification of parameters such as recovery windows etc, if required.
--     airbossStennis:Start()
--
-- This sequence loads all available player grades from the default file and automatically saved them when a player received a (final) grade. Again, if **lfs** was desanitized, the files are save to and loaded
-- from the "Saved Games\DCS" directory. If **lfs** was *not* desanitized, the DCS root installation folder is the default path.
--
-- # Trap Sheet
--
-- Important aircraft attitude parameters during the Groove can be saved to file for later analysis. This also requires the **io** and optionally **lfs** modules to be desanitized.
--
-- In the script you have to add the @{#AIRBOSS.SetTrapSheet}(*path*) function to activate this feature.
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetTable.png)
--
-- Data the is written to a file in csv format and contains the following information:
--
--    * *Time*: time in seconds since start.
--    * *Rho*: distance from rundown to player aircraft in NM.
--    * *X*: distance parallel to the carrier in meters.
--    * *Z*: distance perpendicular to the carrier in meters.
--    * *Alt*: altitude of player aircraft in feet.
--    * *AoA*: angle of attack in degrees.
--    * *GSE*: glideslope error in degrees.
--    * *LUE*: lineup error in degrees.
--    * *Vtot*: total velocity of player aircraft in knots.
--    * *Vy*: vertical (descent) velocity in ft/min.
--    * *Gamma*: angle between vector of aircraft nose and vector point in the direction of the carrier runway in degrees.
--    * *Pitch*: pitch angle of player aircraft in degrees.
--    * *Roll*: roll angle of player aircraft in degrees.
--    * *Yaw*: yaw angle of player aircraft in degrees.
--    * *Step*: Step in the groove.
--    * *Grade*: Current LSO grade.
--    * *Points*: Current points for the pass.
--    * *Details*: Detailed grading analysis.
--
-- ## Lineup Error
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetLUE.png)
--
-- The graph displays the lineup error (LUE) as a function of the distance to the carrier.
--
-- The pilot approaches the carrier from the port side, LUE>0°, at a distance of ~1 NM.
-- At the beginning of the groove (X), he significantly overshoots to the starboard side (LUE<5°).
-- In the middle (IM), he performs good corrections and smoothly reduces the lineup error.
-- Finally, at a distance of ~0.3 NM (IC) he has corrected his lineup with the runway to a reasonable level, |LUE|<0.5°.
--
-- ## Glideslope Error
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetGLE.png)
--
-- The graph displays the glideslope error (GSE) as a function of the distance to the carrier.
--
-- In this case the pilot already enters the groove (X) below the optimal glideslope. He is not able to correct his height in the IM part and
-- stays significantly too low. In close, he performs a harsh correction to gain altitude and ends up even slightly too high (GSE>0.5°).
-- At his point further corrections are necessary.
--
-- ## Angle of Attack
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_TrapSheetAoA.png)
--
-- The graph displays the angle of attack (AoA) as a function of the distance to the carrier.
--
-- The pilot starts off being on speed after the ball call. Then he get way to fast troughout the most part of the groove. He manages to correct
-- this somewhat short before touchdown.
--
-- ===
--
-- # Sound Files
--
-- An important aspect of the AIRBOSS is that it uses voice overs for greater immersion. The necessary sound files can be obtained from the
-- MOOSE Discord in the [#ops-airboss](https://discordapp.com/channels/378590350614462464/527363141185830915) channel. Check out the **pinned messages**.
--
-- However, including sound files into a new mission is tedious as these usually need to be included into the mission **miz** file via (unused) triggers.
--
-- The default location inside the miz file is "l10n/DEFAULT/". But simply opening the *miz* file with e.g. [7-zip](https://www.7-zip.org/) and copying the files into that folder does not work.
-- The next time the mission is saved, files not included via trigger are automatically removed by DCS.
--
-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used. The location of the sound files can be specified
-- via the @{#AIRBOSS.SetSoundfilesFolder}(*folderpath*) function. The parameter *folderpath* defines the location of the sound files folder within the mission *miz* file.
--
-- ![Banner Image](..\Presentations\AIRBOSS\Airboss_SoundfilesFolder.png)
--
-- For example as
--
--     airbossStennis:SetSoundfilesFolder("Airboss Soundfiles/")
--
-- ## Carrier Specific Voice Overs
--
-- It is possible to use different sound files for different carriers. If you have set up two (or more) AIRBOSS objects at different carriers - say Stennis and Tarawa - each
-- carrier would use the files in the specified directory, e.g.
--
--     airbossStennis:SetSoundfilesFolder("Airboss Soundfiles Stennis/")
--     airbossTarawa:SetSoundfilesFolder("Airboss Soundfiles Tarawa/")
--
-- ## Sound Packs
--
-- The AIRBOSS currently has two different "sound packs" for LSO and three different "sound Packs" for Marshal radios. These contain voice overs by different actors.
-- These can be set by @{#AIRBOSS.SetVoiceOversLSOByRaynor}() and @{#AIRBOSS.SetVoiceOversMarshalByRaynor}(). These are the default settings.
-- The other sound files can be set by @{#AIRBOSS.SetVoiceOversLSOByFF}(), @{#AIRBOSS.SetVoiceOversMarshalByGabriella}() and @{#AIRBOSS.SetVoiceOversMarshalByFF}().
-- Also combinations can be used, e.g.
--
--     airbossStennis:SetVoiceOversLSOByFF()
--     airbossStennis:SetVoiceOversMarshalByRaynor()
--
-- In this example LSO voice overs by FF and Marshal voice overs by Raynor are used.
--
-- **Note** that this only initializes the correct parameters parameters of sound files, i.e. the duration. The correct files have to be in the directory set by the
-- @{#AIRBOSS.SetSoundfilesFolder}(*folder*) function.
--
-- ## How To Use Your Own Voice Overs
--
-- If you have a set of AIRBOSS sound files recorded or got it from elsewhere it is possible to use those instead of the default ones.
-- I recommend to use exactly the same file names as the original sound files have.
--
-- However, the **timing is critical**! As sometimes sounds are played directly after one another, e.g. by saying the modex but also on other occations, the airboss
-- script has a radio queue implemented (actually two - one for the LSO and one for the Marshal/Airboss radio).
-- By this it is automatically taken care that played messages are not overlapping and played over each other. The disadvantage is, that the script needs to know
-- the exact duration of *each* voice over. For the default sounds this is hard coded in the source code. For your own files, you need to give that bit of information
-- to the script via the @{#AIRBOSS.SetVoiceOver}(**radiocall**, **duration**, **subtitle**, **subduration**, **filename**, **suffix**) function. Only the first two
-- parameters **radiocall** and **duration** are usually important to adjust here.
--
-- For example, if you want to change the LSO "Call the Ball" and "Roger Ball" calls:
--
--     airbossStennis:SetVoiceOver(airbossStennis.LSOCall.CALLTHEBALL, 0.6)
--     airbossStennis:SetVoiceOver(airbossStennis.LSOCall.ROGERBALL, 0.7)
--
-- Again, changing the file name, subtitle, subtitle duration is not required if you name the file exactly like the original one, which is this case would be "LSO-RogerBall.ogg".
--
-- ## The Radio Dilemma
--
-- DCS offers two (actually three) ways to send radio messages. Each one has its advantages and disadvantages and it is important to understand the differences.
--
-- ### Transmission via Command
--
-- *In principle*, the best way to transmit messages is via the [TransmitMessage](https://wiki.hoggitworld.com/view/DCS_command_transmitMessage) command.
-- This method has the advantage that subtitles can be used and these subtitles are only displayed to the players who dialed in the same radio frequency as
-- used for the transmission.
-- However, this method unfortunately only works if the sending unit is an **aircraft**. Therefore, it is not usable by the AIRBOSS per se as the transmission comes from
-- a naval unit (i.e. the carrier).
--
-- As a workaround, you can put an aircraft, e.g. a Helicopter on the deck of the carrier or another ship of the strike group. The aircraft should be set to
-- uncontrolled and maybe even to immortal. With the @{#AIRBOSS.SetRadioUnitName}(*unitname*) function you can use this unit as "radio repeater" for both Marshal and LSO
-- radio channels. However, this might lead to interruptions in the transmission if both channels transmit simultaniously. Therefore, it is better to assign a unit for
-- each radio via the @{#AIRBOSS.SetRadioRelayLSO}(unitname) and @{#AIRBOSS.SetRadioRelayMarshal}(unitname) functions.
--
-- Of course you can also use any other aircraft in the vicinity of the carrier, e.g. a rescue helo or a recovery tanker. It is just important that this
-- unit is and stays close the the boat as the distance from the sender to the receiver is modeled in DCS. So messages from too far away might not reach the players.
--
-- **Note** that not all radio messages the airboss sends have voice overs. Therefore, if you use a radio relay unit, users should *not* disable the
-- subtitles in the DCS game menu.
--
-- ### Transmission via Trigger
--
-- Another way to broadcast messages is via the [radio transmission trigger](https://wiki.hoggitworld.com/view/DCS_func_radioTransmission). This method can be used for all
-- units (land, air, naval). However, messages cannot be subtitled. Therefore, subtitles are displayed to the players via normal textout messages.
-- The disadvantage is that is is impossible to know which players have the right radio frequencies dialed in. Therefore, subtitles of the Marshal radio calls are displayed to all players
-- inside the CCA. Subtitles on the LSO radio frequency are displayed to all players in the pattern.
--
-- ### Sound to User
--
-- The third way to play sounds to the user via the [outsound trigger](https://wiki.hoggitworld.com/view/DCS_func_outSound).
-- These sounds are not coming from a radio station and therefore can be heard by players independent of their actual radio frequency setting.
-- The AIRBOSS class uses this method to play sounds to players which are of a more "private" nature - for example when a player has left his assigned altitude
-- in the Marshal stack. Often this is the modex of the player in combination with a textout messaged displayed on screen.
--
-- If you want to use this method for all radio messages you can enable it via the @{#AIRBOSS.SetUserSoundRadio}() function. This is the analogue of activating easy comms in DCS.
--
-- Note that this method is used for all players who are in the A-4E community mod as this mod does not have the ability to use radios due to current DCS restrictions.
-- Therefore, A-4E drivers will hear all radio transmissions from the Marshal/Airboss and all LSO messages as soon as their commence the pattern.
--
-- ===
--
-- # AI Handling
--
-- The @{#AIRBOSS} class allows to handle incoming AI units and integrate them into the marshal and landing pattern.
--
-- By default, incoming carrier capable aircraft which are detecting inside the Carrier Controlled Area (CCA) and approach the carrier by more than 5 NM are automatically guided to the holding zone.
-- Each AI group gets its own marshal stack in the holding pattern. Once a recovery window opens, the AI group of the lowest stack is transitioning to the landing pattern
-- and the Marshal stack collapses.
--
-- If no AI handling is desired, this can be turned off via the @{#AIRBOSS.SetHandleAIOFF} function.
--
-- In case only specifc AI groups shall be excluded, it can be done by adding the groups to a set, e.g.
--
--     -- AI groups explicitly excluded from handling by the Airboss
--     local CarrierExcludeSet=SET_GROUP:New():FilterPrefixes("E-2D Wizard Group"):FilterStart()
--     AirbossStennis:SetExcludeAI(CarrierExcludeSet)
--
-- Similarly, to the @{#AIRBOSS.SetExcludeAI} function, AI groups can be explicitly *included* via the @{#AIRBOSS.SetSquadronAI} function. If this is used, only the *included* groups are handled
-- by the AIRBOSS.
--
-- ## Keep the Deck Clean
--
-- Once the AI groups have landed on the carrier, they can be despawned automatically after they shut down their engines. This is achieved by the @{#AIRBOSS.SetDespawnOnEngineShutdown}() function.
--
-- ## Refueling
--
-- AI groups in the marshal pattern can be send to refuel at the recovery tanker or if none is defined to the nearest divert airfield. This can be enabled by the @{#AIRBOSS.SetRefuelAI}(*lowfuelthreshold*).
-- The parameter *lowfuelthreshold* is the threshold of fuel in percent. If the fuel drops below this value, the group will go for refueling. If refueling is performed at the recovery tanker,
-- the group will return to the marshal stack when done. The aircraft will not return from the divert airfield however.
--
-- Note that this feature is not enabled by default as there might be bugs in DCS that prevent a smooth refueling of the AI. Enable at your own risk.
--
-- ## Respawning - DCS Landing Bug
--
-- AI groups that enter the CCA are usually guided to Marshal stack. However, due to DCS limitations they might not obey the landing task if they have another airfield as departure and/or destination in
-- their mission task. Therefore, AI groups can be respawned when detected in the CCA. This should clear all other airfields and allow the aircraft to land on the carrier.
-- This is achieved by the @{#AIRBOSS.SetRespawnAI}() function.
--
-- ## Known Issues
--
-- Dealing with the DCS AI is a big challenge and there is only so much one can do. Please bear this in mind!
--
-- ### Pattern Updates
--
-- The holding position of the AI is updated regularly when the carrier has changed its position by more then 2.5 NM or changed its course significantly.
-- The patterns are realized by orbit or racetrack patterns of the DCS scripting API.
-- However, when the position is updated or the marshal stack collapses, it comes to disruptions of the regular orbit because a new waypoint with a new
-- orbit task needs to be created.
--
-- ### Recovery Cases
--
-- The AI performs a very realistic Case I recovery. Therefore, we already have a good Case I and II recovery simulation since the final part of Case II is a
-- Case I recovery. However, I don't think the AI can do a proper Case III recovery. If you give the AI the landing command, it is out of our hands and will
-- always go for a Case I in the final pattern part. Maybe this will improve in future DCS version but right now, there is not much we can do about it.
--
-- ===
--
-- # Finite State Machine (FSM)
--
-- The AIRBOSS class has a Finite State Machine (FSM) implementation for the carrier. This allows mission designers to hook into certain events and helps
-- simulate complex behaviour easier.
--
-- FSM events are:
--
--    * @{#AIRBOSS.Start}: Starts the AIRBOSS FSM.
--    * @{#AIRBOSS.Stop}: Stops the AIRBOSS FSM.
--    * @{#AIRBOSS.Idle}: Carrier is set to idle and not recovering.
--    * @{#AIRBOSS.RecoveryStart}: Starts the recovery ops.
--    * @{#AIRBOSS.RecoveryStop}: Stops the recovery ops.
--    * @{#AIRBOSS.RecoveryPause}: Pauses the recovery ops.
--    * @{#AIRBOSS.RecoveryUnpause}: Unpauses the recovery ops.
--    * @{#AIRBOSS.RecoveryCase}: Sets/switches the recovery case.
--    * @{#AIRBOSS.PassingWaypoint}: Carrier passes a waypoint defined in the mission editor.
--
-- These events can be used in the user script. When the event is triggered, it is automatically a function OnAfter*Eventname* called. For example
--
--     --- Carrier just passed waypoint *n*.
--     function AirbossStennis:OnAfterPassingWaypoint(From, Event, To, n)
--      -- Launch green flare.
--      self.carrier:FlareGreen()
--     end
--
-- In this example, we only launch a green flare every time the carrier passes a waypoint defined in the mission editor. But, of course, you can also use it to add new
-- recovery windows each time a carrier passes a waypoint. Therefore, you can create an "infinite" number of windows easily.
--
-- ===
--
-- # Examples
--
-- In this section a few simple examples are given to illustrate the scripting part.
--
-- ## Simple Case
--
--     -- Create AIRBOSS object.
--     local AirbossStennis=AIRBOSS:New("USS Stennis")
--
--     -- Add recovery windows:
--     -- Case I from 9 to 10 am. Carrier will turn into the wind 5 min before window opens and go at a speed so that wind over the deck is 25 knots.
--     local window1=AirbossStennis:AddRecoveryWindow("9:00",  "10:00", 1, nil, true, 25)
--     -- Case II with +15 degrees holding offset from 15:00 for 60 min.
--     local window2=AirbossStennis:AddRecoveryWindow("15:00", "16:00", 2, 15)
--     -- Case III with +30 degrees holding offset from 21:00 to 23:30.
--     local window3=AirbossStennis:AddRecoveryWindow("21:00", "23:30", 3, 30)
--
--     -- Load all saved player grades from your "Saved Games\DCS" folder (if lfs was desanitized).
--     AirbossStennis:Load()
--
--     -- Automatically save player results to your "Saved Games\DCS" folder each time a player get a final grade from the LSO.
--     AirbossStennis:SetAutoSave()
--
--     -- Start airboss class.
--     AirbossStennis:Start()
--
-- ===
--
-- # Debugging
--
-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in
--     C:\Users\<yourname>\Saved Games\DCS\Logs\dcs.log
-- All output concerning the @{#AIRBOSS} class should have the string "AIRBOSS" in the corresponding line.
-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong.
--
-- The verbosity of the output can be increased by adding the following lines to your script:
--
--     BASE:TraceOnOff(true)
--     BASE:TraceLevel(1)
--     BASE:TraceClass("AIRBOSS")
--
-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details.
--
-- ### Debug Mode
--
-- You have the option to enable the debug mode for this class via the @{#AIRBOSS.SetDebugModeON} function.
-- If enabled, status and debug text messages will be displayed on the screen. Also informative marks on the F10 map are created.
--
-- @field #AIRBOSS
AIRBOSS = {
  ClassName      = "AIRBOSS",
  Debug          = false,
  lid            = nil,
  theatre        = nil,
  carrier        = nil,
  carriertype    = nil,
  carrierparam   =  {},
  alias          = nil,
  airbase        = nil,
  waypoints      =  {},
  currentwp      = nil,
  beacon         = nil,
  TACANon        = nil,
  TACANchannel   = nil,
  TACANmode      = nil,
  TACANmorse     = nil,
  ICLSon         = nil,
  ICLSchannel    = nil,
  ICLSmorse      = nil,
  LSORadio       = nil,
  LSOFreq        = nil,
  LSOModu        = nil,
  MarshalRadio   = nil,
  MarshalFreq    = nil,
  MarshalModu    = nil,
  TowerFreq      = nil,
  radiotimer     = nil,
  zoneCCA        = nil,
  zoneCCZ        = nil,
  players        =  {},
  menuadded      =  {},
  BreakEntry     =  {},
  BreakEarly     =  {},
  BreakLate      =  {},
  Abeam          =  {},
  Ninety         =  {},
  Wake           =  {},
  Final          =  {},
  Groove         =  {},
  Platform       =  {},
  DirtyUp        =  {},
  Bullseye       =  {},
  defaultcase    = nil,
  case           = nil,
  defaultoffset  = nil,
  holdingoffset  = nil,
  recoverytimes  =  {},
  flights        =  {},
  Qpattern       =  {},
  Qmarshal       =  {},
  Qwaiting       =  {},
  Qspinning      =  {},
  RQMarshal      =  {},
  RQLSO          =  {},
  TQMarshal      =   0,
  TQLSO          =   0,
  Nmaxpattern    = nil,
  Nmaxmarshal    = nil,
  NmaxSection    = nil,
  NmaxStack      = nil,
  handleai       = nil,
  xtVoiceOvers   = nil,
  xtVoiceOversAI = nil,
  tanker         = nil,
  Corientation   = nil,
  Corientlast    = nil,
  Cposition      = nil,
  defaultskill   = nil,
  adinfinitum    = nil,
  magvar         = nil,
  Tcollapse      = nil,
  recoverywindow = nil,
  usersoundradio = nil,
  Tqueue         = nil,
  dTqueue        = nil,
  dTstatus       = nil,
  menumarkzones  = nil,
  menusmokezones = nil,
  playerscores   = nil,
  autosave       = nil,
  autosavefile   = nil,
  autosavepath   = nil,
  marshalradius  = nil,
  airbossnice    = nil,
  staticweather  = nil,
  windowcount    =   0,
  LSOdT          = nil,
  senderac       = nil,
  radiorelayLSO  = nil,
  radiorelayMSH  = nil,
  turnintowind   = nil,
  detour         = nil,
  squadsetAI     = nil,
  excludesetAI   = nil,
  menusingle     = nil,
  collisiondist  = nil,
  holdtimestamp  = nil,
  Tmessage       = nil,
  soundfolder    = nil,
  soundfolderLSO = nil,
  soundfolderMSH = nil,
  despawnshutdown= nil,
  dTbeacon       = nil,
  Tbeacon        = nil,
  LSOCall        = nil,
  MarshalCall    = nil,
  lowfuelAI      = nil,
  emergency      = nil,
  respawnAI      = nil,
  gle            =  {},
  lue            =  {},
  trapsheet      = nil,
  trappath       = nil,
  trapprefix     = nil,
  initialmaxalt  = nil,
  welcome        = nil,
  skipperMenu    = nil,
  skipperSpeed   = nil,
  skipperTime    = nil,
  skipperOffset  = nil,
  skipperUturn   = nil,
}

--- Aircraft types capable of landing on carrier (human+AI).
-- @type AIRBOSS.AircraftCarrier
-- @field #string AV8B AV-8B Night Harrier. Works only with the HMS Hermes, HMS Invincible, USS Tarawa, USS America, and Juan Carlos I.
-- @field #string A4EC A-4E Community mod.
-- @field #string HORNET F/A-18C Lot 20 Hornet by Eagle Dynamics.
-- @field #string F14A F-14A by Heatblur.
-- @field #string F14B F-14B by Heatblur.
-- @field #string F14A_AI F-14A Tomcat (AI).
-- @field #string FA18C F/A-18C Hornet (AI).
-- @field #string S3B Lockheed S-3B Viking.
-- @field #string S3BTANKER Lockheed S-3B Viking tanker.
-- @field #string E2D Grumman E-2D Hawkeye AWACS.
-- @field #string C2A Grumman C-2A Greyhound from Military Aircraft Mod.
-- @field #string T45C T-45C by VNAO.
-- @field #string RHINOE F/A-18E Superhornet (mod).
-- @field #string RHINOF F/A-18F Superhornet (mod).
-- @field #string GROWLER FEA-18G Superhornet (mod).
-- @field #string CORSAIR F4U-1D Corsair.
-- @field #string CORSAIR_CW F4U-1D Corsair Mk.4 (clipped wing).
AIRBOSS.AircraftCarrier={
  AV8B="AV8BNA",
  HORNET="FA-18C_hornet",
  A4EC="A-4E-C",
  F14A="F-14A-135-GR",
  F14B="F-14B",
  F14A_AI="F-14A",
  FA18C="F/A-18C",
  T45C="T-45",
  S3B="S-3B",
  S3BTANKER="S-3B Tanker",
  E2D="E-2C",
  C2A="C2A_Greyhound",
  RHINOE="FA-18E",
  RHINOF="FA-18F",
  GROWLER="EA-18G",
  CORSAIR="F4U-1D",
  CORSAIR_CW="F4U-1D CW",  
}

--- Carrier types.
-- @type AIRBOSS.CarrierType
-- @field #string ROOSEVELT USS Theodore Roosevelt (CVN-71) [Super Carrier Module]
-- @field #string LINCOLN USS Abraham Lincoln (CVN-72) [Super Carrier Module]
-- @field #string WASHINGTON USS George Washington (CVN-73) [Super Carrier Module]
-- @field #string STENNIS USS John C. Stennis (CVN-74)
-- @field #string TRUMAN USS Harry S. Truman (CVN-75) [Super Carrier Module]
-- @field #string FORRESTAL USS Forrestal (CV-59) [Heatblur Carrier Module]
-- @field #string VINSON USS Carl Vinson (CVN-70) [Deprecated!]
-- @field #string ESSEX Essex class carrier (e.g. USS Yorktown (CV-10)) [Magnitude 3 Carrier Module]
-- @field #string BONHOMMERICHARD USS Bon Homme Richard carrier [VWV Mod]
-- @field #string ESSEXSCB125 Generic Essex class carrier with angled deck (SCB-125 upgrade) [VWV Mod]
-- @field #string ENTERPRISE66 USS Enterprise in the 1966 configuration [VWV Mod]
-- @field #string ENTERPRISEMODERN USS Enterprise in a modern configuration [Derived VWV Mod]
-- @field #string HERMES HMS Hermes (R12) [V/STOL Carrier]
-- @field #string INVINCIBLE HMS Invincible (R05) [V/STOL Carrier]
-- @field #string TARAWA USS Tarawa (LHA-1) [V/STOL Carrier]
-- @field #string AMERICA USS America (LHA-6) [V/STOL Carrier]
-- @field #string JCARLOS Juan Carlos I (L61) [V/STOL Carrier]
-- @field #string HMAS Canberra (L02) [V/STOL Carrier]
-- @field #string KUZNETSOV Admiral Kuznetsov (CV 1143.5)
AIRBOSS.CarrierType = {
  ROOSEVELT = "CVN_71",
  LINCOLN = "CVN_72",
  WASHINGTON = "CVN_73",
  TRUMAN = "CVN_75",
  STENNIS = "Stennis",
  FORRESTAL = "Forrestal",
  ENTERPRISE66 = "USS Enterprise 1966",
  ENTERPRISEMODERN = "cvn-65",
  VINSON = "VINSON",
  ESSEX = "Essex",
  BONHOMMERICHARD = "USS Bon Homme Richard",
  ESSEXSCB125 = "essex_scb125",
  HERMES = "HERMES81",
  INVINCIBLE = "hms_invincible",
  TARAWA = "LHA_Tarawa",
  AMERICA = "USS America LHA-6",
  JCARLOS = "L61",
  CANBERRA = "L02",
  KUZNETSOV = "KUZNECOW",
}

--- Carrier specific parameters.
-- @type AIRBOSS.CarrierParameters
-- @field #number rwyangle Runway angle in degrees. for carriers with angled deck. For USS Stennis -9 degrees.
-- @field #number sterndist Distance in meters from carrier position to stern of carrier. For USS Stennis -150 meters.
-- @field #number deckheight Height of deck in meters. For USS Stennis ~63 ft = 19 meters.
-- @field #number wire1 Distance in meters from carrier position to first wire.
-- @field #number wire2 Distance in meters from carrier position to second wire.
-- @field #number wire3 Distance in meters from carrier position to third wire.
-- @field #number wire4 Distance in meters from carrier position to fourth wire.
-- @field #number landingdist Distance in meeters to the landing position.
-- @field #number rwylength Length of the landing runway in meters.
-- @field #number rwywidth Width of the landing runway in meters.
-- @field #number totlength Total length of carrier.
-- @field #number totwidthstarboard Total with of the carrier from stern position to starboard side (asymmetric carriers).
-- @field #number totwidthport Total with of the carrier from stern position to port side (asymmetric carriers).

--- Aircraft specific Angle of Attack (AoA) (or alpha) parameters.
-- @type AIRBOSS.AircraftAoA
-- @field #number OnSpeedMin Minimum on speed AoA. Values below are fast
-- @field #number OnSpeedMax Maximum on speed AoA. Values above are slow.
-- @field #number OnSpeed Optimal on-speed AoA.
-- @field #number Fast Fast AoA threshold. Smaller means faster.
-- @field #number Slow Slow AoA threshold. Larger means slower.
-- @field #number FAST Really fast AoA threshold.
-- @field #number SLOW Really slow AoA threshold.

--- Glideslope error thresholds in degrees.
-- @type AIRBOSS.GLE
-- @field #number _max Max _OK_ value. Default 0.4 deg.
-- @field #number _min Min _OK_ value. Default -0.3 deg.
-- @field #number High (H) threshold. Default 0.8 deg.
-- @field #number Low  (L) threshold. Default -0.6 deg.
-- @field #number HIGH  H  threshold. Default 1.5 deg.
-- @field #number LOW   L  threshold. Default -0.9 deg.

--- Lineup error thresholds in degrees.
-- @type AIRBOSS.LUE
-- @field #number _max Max _OK_ value. Default 0.5 deg.
-- @field #number _min Min _OK_ value. Default -0.5 deg.
-- @field #number Left  (LUR) threshold. Default -1.0 deg.
-- @field #number Right (LUL) threshold. Default 1.0 deg.
-- @field #number LeftMed threshold for AA/OS measuring. Default -2.0 deg.
-- @field #number RightMed threshold for AA/OS measuring. Default 2.0 deg.
-- @field #number LEFT   LUR  threshold. Default -3.0 deg.
-- @field #number RIGHT  LUL  threshold. Default 3.0 deg.

--- Pattern steps.
-- @type AIRBOSS.PatternStep
-- @field #string UNDEFINED "Undefined".
-- @field #string REFUELING "Refueling".
-- @field #string SPINNING "Spinning".
-- @field #string COMMENCING "Commencing".
-- @field #string HOLDING "Holding".
-- @field #string WAITING "Waiting for free Marshal stack".
-- @field #string PLATFORM "Platform".
-- @field #string ARCIN "Arc Turn In".
-- @field #string ARCOUT "Arc Turn Out".
-- @field #string DIRTYUP "Dirty Up".
-- @field #string BULLSEYE "Bullseye".
-- @field #string INITIAL "Initial".
-- @field #string BREAKENTRY "Break Entry".
-- @field #string EARLYBREAK "Early Break".
-- @field #string LATEBREAK "Late Break".
-- @field #string ABEAM "Abeam".
-- @field #string NINETY "Ninety".
-- @field #string WAKE "Wake".
-- @field #string FINAL "Final".
-- @field #string GROOVE_XX "Groove X".
-- @field #string GROOVE_IM "Groove In the Middle".
-- @field #string GROOVE_IC "Groove In Close".
-- @field #string GROOVE_AR "Groove At the Ramp".
-- @field #string GROOVE_AL "Groove Abeam Landing Spot".
-- @field #string GROOVE_LC "Groove Level Cross".
-- @field #string GROOVE_IW "Groove In the Wires".
-- @field #string BOLTER "Bolter Pattern".
-- @field #string EMERGENCY "Emergency Landing".
-- @field #string DEBRIEF "Debrief".
AIRBOSS.PatternStep = {
  UNDEFINED = "Undefined",
  REFUELING = "Refueling",
  SPINNING = "Spinning",
  COMMENCING = "Commencing",
  HOLDING = "Holding",
  WAITING = "Waiting for free Marshal stack",
  PLATFORM = "Platform",
  ARCIN = "Arc Turn In",
  ARCOUT = "Arc Turn Out",
  DIRTYUP = "Dirty Up",
  BULLSEYE = "Bullseye",
  INITIAL = "Initial",
  BREAKENTRY = "Break Entry",
  EARLYBREAK = "Early Break",
  LATEBREAK = "Late Break",
  ABEAM = "Abeam",
  NINETY = "Ninety",
  WAKE = "Wake",
  FINAL = "Turn Final",
  GROOVE_XX = "Groove X",
  GROOVE_IM = "Groove In the Middle",
  GROOVE_IC = "Groove In Close",
  GROOVE_AR = "Groove At the Ramp",
  GROOVE_IW = "Groove In the Wires",
  GROOVE_IWs = "Groove In the Wires stopped?", -- VNAO Edit - Added
  GROOVE_AL = "Groove Abeam Landing Spot",
  GROOVE_LC = "Groove Level Cross",
  BOLTER = "Bolter Pattern",
  EMERGENCY = "Emergency Landing",
  DEBRIEF = "Debrief",
}

--- Groove position.
-- @type AIRBOSS.GroovePos
-- @field #string X0 "X0": Entering the groove.
-- @field #string XX "XX": At the start, i.e. 3/4 from the run down.
-- @field #string IM "IM": In the middle.
-- @field #string IC "IC": In close.
-- @field #string AR "AR": At the ramp.
-- @field #string AL "AL": Abeam landing position (V/STOL).
-- @field #string LC "LC": Level crossing (V/STOL).
-- @field #string IW "IW": In the wires.
AIRBOSS.GroovePos = {
  X0 = "X0",
  XX = "XX",
  IM = "IM",
  IC = "IC",
  AR = "AR",
  AL = "AL",
  LC = "LC",
  IW = "IW",
}

--- Radio.
-- @type AIRBOSS.Radio
-- @field #number frequency Frequency in Hz.
-- @field #number modulation Band modulation.
-- @field #string alias Radio alias.

--- Radio sound file and subtitle.
-- @type AIRBOSS.RadioCall
-- @field #string file Sound file name without suffix.
-- @field #string suffix File suffix/extension, e.g. "ogg".
-- @field #boolean loud Loud version of sound file available.
-- @field #string subtitle Subtitle displayed during transmission.
-- @field #number duration Duration of the sound in seconds. This is also the duration the subtitle is displayed.
-- @field #number subduration Duration in seconds the subtitle is displayed.
-- @field #string modexsender Onboard number of the sender (optional).
-- @field #string modexreceiver Onboard number of the receiver (optional).
-- @field #string sender Sender of the message (optional). Default radio alias.

--- Pilot radio calls.
-- type AIRBOSS.PilotCalls
-- @field #AIRBOSS.RadioCall N0 "Zero" call.
-- @field #AIRBOSS.RadioCall N1 "One" call.
-- @field #AIRBOSS.RadioCall N2 "Two" call.
-- @field #AIRBOSS.RadioCall N3 "Three" call.
-- @field #AIRBOSS.RadioCall N4 "Four" call.
-- @field #AIRBOSS.RadioCall N5 "Five" call.
-- @field #AIRBOSS.RadioCall N6 "Six" call.
-- @field #AIRBOSS.RadioCall N7 "Seven" call.
-- @field #AIRBOSS.RadioCall N8 "Eight" call.
-- @field #AIRBOSS.RadioCall N9 "Nine" call.
-- @field #AIRBOSS.RadioCall POINT "Point" call.
-- @field #AIRBOSS.RadioCall BALL "Ball" call.
-- @field #AIRBOSS.RadioCall HARRIER "Harrier" call.
-- @field #AIRBOSS.RadioCall HAWKEYE "Hawkeye" call.
-- @field #AIRBOSS.RadioCall HORNET "Hornet" call.
-- @field #AIRBOSS.RadioCall SKYHAWK "Skyhawk" call.
-- @field #AIRBOSS.RadioCall TOMCAT "Tomcat" call.
-- @field #AIRBOSS.RadioCall VIKING "Viking" call.
-- @field #AIRBOSS.RadioCall BINGOFUEL "Bingo Fuel" call.
-- @field #AIRBOSS.RadioCall GASATDIVERT "Going for gas at the divert field" call.
-- @field #AIRBOSS.RadioCall GASATTANKER "Going for gas at the recovery tanker" call.

--- LSO radio calls.
-- @type AIRBOSS.LSOCalls
-- @field #AIRBOSS.RadioCall BOLTER "Bolter, Bolter" call.
-- @field #AIRBOSS.RadioCall CALLTHEBALL "Call the Ball" call.
-- @field #AIRBOSS.RadioCall CHECK "CHECK" call.
-- @field #AIRBOSS.RadioCall CLEAREDTOLAND "Cleared to land" call.
-- @field #AIRBOSS.RadioCall COMELEFT "Come left" call.
-- @field #AIRBOSS.RadioCall DEPARTANDREENTER "Depart and re-enter" call.
-- @field #AIRBOSS.RadioCall EXPECTHEAVYWAVEOFF "Expect heavy wavoff" call.
-- @field #AIRBOSS.RadioCall EXPECTSPOT75 "Expect spot 7.5" call.
-- @field #AIRBOSS.RadioCall EXPECTSPOT5 "Expect spot 5" call.
-- @field #AIRBOSS.RadioCall FAST "You're fast" call.
-- @field #AIRBOSS.RadioCall FOULDECK "Foul Deck" call.
-- @field #AIRBOSS.RadioCall HIGH "You're high" call.
-- @field #AIRBOSS.RadioCall IDLE "Idle" call.
-- @field #AIRBOSS.RadioCall LONGINGROOVE "You're long in the groove" call.
-- @field #AIRBOSS.RadioCall LOW "You're low" call.
-- @field #AIRBOSS.RadioCall N0 "Zero" call.
-- @field #AIRBOSS.RadioCall N1 "One" call.
-- @field #AIRBOSS.RadioCall N2 "Two" call.
-- @field #AIRBOSS.RadioCall N3 "Three" call.
-- @field #AIRBOSS.RadioCall N4 "Four" call.
-- @field #AIRBOSS.RadioCall N5 "Five" call.
-- @field #AIRBOSS.RadioCall N6 "Six" call.
-- @field #AIRBOSS.RadioCall N7 "Seven" call.
-- @field #AIRBOSS.RadioCall N8 "Eight" call.
-- @field #AIRBOSS.RadioCall N9 "Nine" call.
-- @field #AIRBOSS.RadioCall PADDLESCONTACT "Paddles, contact" call.
-- @field #AIRBOSS.RadioCall POWER "Power" call.
-- @field #AIRBOSS.RadioCall RADIOCHECK "Paddles, radio check" call.
-- @field #AIRBOSS.RadioCall RIGHTFORLINEUP "Right for line up" call.
-- @field #AIRBOSS.RadioCall ROGERBALL "Roger ball" call.
-- @field #AIRBOSS.RadioCall SLOW "You're slow" call.
-- @field #AIRBOSS.RadioCall STABILIZED "Stabilized" call.
-- @field #AIRBOSS.RadioCall WAVEOFF "Wave off" call.
-- @field #AIRBOSS.RadioCall WELCOMEABOARD "Welcome aboard" call.
-- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound.
-- @field #AIRBOSS.RadioCall NOISE Static noise sound.
-- @field #AIRBOSS.RadioCall SPINIT "Spin it" call.

--- Marshal radio calls.
-- @type AIRBOSS.MarshalCalls
-- @field #AIRBOSS.RadioCall AFFIRMATIVE "Affirmative" call.
-- @field #AIRBOSS.RadioCall ALTIMETER "Altimeter" call.
-- @field #AIRBOSS.RadioCall BRC "BRC" call.
-- @field #AIRBOSS.RadioCall CARRIERTURNTOHEADING "Turn to heading" call.
-- @field #AIRBOSS.RadioCall CASE "Case" call.
-- @field #AIRBOSS.RadioCall CHARLIETIME "Charlie Time" call.
-- @field #AIRBOSS.RadioCall CLEAREDFORRECOVERY "You're cleared for case" call.
-- @field #AIRBOSS.RadioCall DECKCLOSED "Deck closed" sound.
-- @field #AIRBOSS.RadioCall DEGREES "Degrees" call.
-- @field #AIRBOSS.RadioCall EXPECTED "Expected" call.
-- @field #AIRBOSS.RadioCall FLYNEEDLES "Fly your needles" call.
-- @field #AIRBOSS.RadioCall HOLDATANGELS "Hold at angels" call.
-- @field #AIRBOSS.RadioCall HOURS "Hours" sound.
-- @field #AIRBOSS.RadioCall MARSHALRADIAL "Marshal radial" call.
-- @field #AIRBOSS.RadioCall N0 "Zero" call.
-- @field #AIRBOSS.RadioCall N1 "One" call.
-- @field #AIRBOSS.RadioCall N2 "Two" call.
-- @field #AIRBOSS.RadioCall N3 "Three" call.
-- @field #AIRBOSS.RadioCall N4 "Four" call.
-- @field #AIRBOSS.RadioCall N5 "Five" call.
-- @field #AIRBOSS.RadioCall N6 "Six" call.
-- @field #AIRBOSS.RadioCall N7 "Seven" call.
-- @field #AIRBOSS.RadioCall N8 "Eight" call.
-- @field #AIRBOSS.RadioCall N9 "Nine" call.
-- @field #AIRBOSS.RadioCall NEGATIVE "Negative" sound.
-- @field #AIRBOSS.RadioCall NEWFB "New final bearing" call.
-- @field #AIRBOSS.RadioCall OBS "Obs" call.
-- @field #AIRBOSS.RadioCall POINT "Point" call.
-- @field #AIRBOSS.RadioCall RADIOCHECK "Radio check" call.
-- @field #AIRBOSS.RadioCall RECOVERY "Recovery" call.
-- @field #AIRBOSS.RadioCall RECOVERYOPSSTOPPED "Recovery ops stopped" sound.
-- @field #AIRBOSS.RadioCall RECOVERYPAUSEDNOTICE "Recovery paused until further notice" call.
-- @field #AIRBOSS.RadioCall RECOVERYPAUSEDRESUMED "Recovery paused and will be resumed at" call.
-- @field #AIRBOSS.RadioCall RESUMERECOVERY "Resuming aircraft recovery" call.
-- @field #AIRBOSS.RadioCall REPORTSEEME "Report see me" call.
-- @field #AIRBOSS.RadioCall ROGER "Roger" call.
-- @field #AIRBOSS.RadioCall SAYNEEDLES "Say needles" call.
-- @field #AIRBOSS.RadioCall STACKFULL "Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions" call.
-- @field #AIRBOSS.RadioCall STARTINGRECOVERY "Starting aircraft recovery" call.
-- @field #AIRBOSS.RadioCall CLICK Radio end transmission click sound.
-- @field #AIRBOSS.RadioCall NOISE Static noise sound.

--- Difficulty level.
-- @type AIRBOSS.Difficulty
-- @field #string EASY Flight Student. Shows tips and hints in important phases of the approach.
-- @field #string NORMAL Naval aviator. Moderate number of hints but not really zip lip.
-- @field #string HARD TOPGUN graduate. For people who know what they are doing. Nearly *ziplip*.
AIRBOSS.Difficulty = {
  EASY = "Flight Student",
  NORMAL = "Naval Aviator",
  HARD = "TOPGUN Graduate",
}

--- Recovery window parameters.
-- @type AIRBOSS.Recovery
-- @field #number START Start of recovery in seconds of abs mission time.
-- @field #number STOP End of recovery in seconds of abs mission time.
-- @field #number CASE Recovery case (1-3) of that time slot.
-- @field #number OFFSET Angle offset of the holding pattern in degrees. Usually 0, +-15, or +-30 degrees.
-- @field #boolean OPEN Recovery window is currently open.
-- @field #boolean OVER Recovery window is over and closed.
-- @field #boolean WIND Carrier will turn into the wind.
-- @field #number SPEED The speed in knots the carrier has during the recovery.
-- @field #boolean UTURN If true, carrier makes a U-turn to the point it came from before resuming its route to the next waypoint.
-- @field #number ID Recovery window ID.

--- Groove data.
-- @type AIRBOSS.GrooveData
-- @field #number Step Current step.
-- @field #number Time Time in seconds.
-- @field #number Rho Distance in meters.
-- @field #number X Distance in meters.
-- @field #number Z Distance in meters.
-- @field #number AoA Angle of Attack.
-- @field #number Alt Altitude in meters.
-- @field #number GSE Glideslope error in degrees.
-- @field #number LUE Lineup error in degrees.
-- @field #number Pitch Pitch angle in degrees.
-- @field #number Roll Roll angle in degrees.
-- @field #number Yaw Yaw angle in degrees.
-- @field #number Vel Total velocity in m/s.
-- @field #number Vy Vertical velocity in m/s.
-- @field #number Gamma Relative heading player to carrier's runway. 0=parallel, +-90=perpendicular.
-- @field #string Grade LSO grade.
-- @field #number GradePoints LSO grade points
-- @field #string GradeDetail LSO grade details.
-- @field #string FlyThrough Fly through up "/" or fly through down "\\".

--- LSO grade data.
-- @type AIRBOSS.LSOgrade
-- @field #string grade LSO grade, i.e. _OK_, OK, (OK), --, CUT
-- @field #number points Points received.
-- @field #number finalscore Points received after player has finally landed. This is the average over all incomplete passes (bolter, waveoff) before.
-- @field #string details Detailed flight analysis.
-- @field #number wire Wire caught.
-- @field #number Tgroove Time in the groove in seconds.
-- @field #number case Recovery case.
-- @field #string wind Wind speed on deck in knots.
-- @field #string modex Onboard number.
-- @field #string airframe Aircraft type name of player.
-- @field #string carriertype Carrier type name.
-- @field #string carriername Carrier name/alias.
-- @field #string theatre DCS map.
-- @field #string mitime Mission time in hh:mm:ss+d format
-- @field #string midate Mission date in yyyy/mm/dd format.
-- @field #string osdate Real live date. Needs **os** to be desanitized.

--- Checkpoint parameters triggering the next step in the pattern.
-- @type AIRBOSS.Checkpoint
-- @field #string name Name of checkpoint.
-- @field #number Xmin Minimum allowed longitual distance to carrier.
-- @field #number Xmax Maximum allowed longitual distance to carrier.
-- @field #number Zmin Minimum allowed latitudal distance to carrier.
-- @field #number Zmax Maximum allowed latitudal distance to carrier.
-- @field #number LimitXmin Latitudal threshold for triggering the next step if X<Xmin.
-- @field #number LimitXmax Latitudal threshold for triggering the next step if X>Xmax.
-- @field #number LimitZmin Latitudal threshold for triggering the next step if Z<Zmin.
-- @field #number LimitZmax Latitudal threshold for triggering the next step if Z>Zmax.

--- Parameters of a flight group.
-- @type AIRBOSS.FlightGroup
-- @field Wrapper.Group#GROUP group Flight group.
-- @field #string groupname Name of the group.
-- @field #number nunits Number of units in group.
-- @field #number dist0 Distance to carrier in meters when the group was first detected inside the CCA.
-- @field #number time Timestamp in seconds of timer.getAbsTime() of the last important event, e.g. added to the queue.
-- @field #number flag Flag value describing the current stack.
-- @field #boolean ai If true, flight is purly AI.
-- @field #string actype Aircraft type name.
-- @field #table onboardnumbers Onboard numbers of aircraft in the group.
-- @field #string onboard Onboard number of player or first unit in group.
-- @field #number case Recovery case of flight.
-- @field #string seclead Name of section lead.
-- @field #table section Other human flight groups belonging to this flight. This flight is the lead.
-- @field #boolean holding If true, flight is in holding zone.
-- @field #boolean ballcall If true, flight called the ball in the groove.
-- @field #table elements Flight group elements.
-- @field #number Tcharlie Charlie (abs) time in seconds.
-- @field #string name Player name or name of first AI unit.
-- @field #boolean refueling Flight is refueling.

--- Parameters of an element in a flight group.
-- @type AIRBOSS.FlightElement
-- @field Wrapper.Unit#UNIT unit Aircraft unit.
-- @field #string unitname Name of the unit.
-- @field #boolean ai If true, AI sits inside. If false, human player is flying.
-- @field #string onboard Onboard number of the aircraft.
-- @field #boolean ballcall If true, flight called the ball in the groove.
-- @field #boolean recovered If true, element was successfully recovered.

--- Player data table holding all important parameters of each player.
-- @type AIRBOSS.PlayerData
-- @field Wrapper.Unit#UNIT unit Aircraft of the player.
-- @field #string unitname Name of the unit.
-- @field Wrapper.Client#CLIENT client Client object of player.
-- @field #string callsign Callsign of player.
-- @field #string difficulty Difficulty level.
-- @field #string step Current/next pattern step.
-- @field #boolean warning Set true once the player got a warning.
-- @field #number passes Number of passes.
-- @field #boolean attitudemonitor If true, display aircraft attitude and other parameters constantly.
-- @field #table debrief Debrief analysis of the current step of this pass.
-- @field #table lastdebrief Debrief of player performance of last completed pass.
-- @field #boolean landed If true, player landed or attempted to land.
-- @field #boolean boltered If true, player boltered.
-- @field #boolean waveoff If true, player was waved off during final approach.
-- @field #boolean wop If true, player was waved off during the pattern.
-- @field #boolean lig If true, player was long in the groove.
-- @field #boolean owo If true, own waveoff by player.
-- @field #boolean wofd If true, player was waved off because of a foul deck.
-- @field #number Tlso Last time the LSO gave an advice.
-- @field #number Tgroove Time in the groove in seconds.
-- @field #number TIG0 Time in groove start timer.getTime().
-- @field #number wire Wire caught by player when trapped.
-- @field #AIRBOSS.GroovePos groove Data table at each position in the groove. Elements are of type @{#AIRBOSS.GrooveData}.
-- @field #table points Points of passes until finally landed.
-- @field #number finalscore Final score if points are averaged over multiple passes.
-- @field #boolean valid If true, player made a valid approach. Is set true on start of Groove X.
-- @field #boolean subtitles If true, display subtitles of radio messages.
-- @field #boolean showhints If true, show step hints.
-- @field #table trapsheet Groove data table recorded every 0.5 seconds.
-- @field #boolean trapon If true, save trap sheets.
-- @field #string debriefschedulerID Debrief scheduler ID.
--
-- @field Sound.SRS#MSRS SRS
-- @field Sound.SRS#MSRSQUEUE SRSQ
--
-- @extends #AIRBOSS.FlightGroup

--- Main group level radio menu: F10 Other/Airboss.
-- @field #table MenuF10
AIRBOSS.MenuF10 = {}

--- Airboss mission level F10 root menu.
-- @field #table MenuF10Root
AIRBOSS.MenuF10Root = nil

--- Airboss class version.
-- @field #string version
AIRBOSS.version = "1.4.2"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- DONE: Handle tanker and AWACS. Put them into pattern.
-- TODO: Handle cases where AI crashes on carrier deck ==> Clean up deck.
-- TODO: Player eject and crash debrief "gradings".
-- TODO: PWO during case 2/3.
-- TODO: PWO when player comes too close to other flight.
-- DONE: Spin pattern. Add radio menu entry. Not sure what to add though?!
-- DONE: Despawn AI after engine shutdown option.
-- DONE: What happens when section lead or member dies?
-- DONE: Do not remove recovered elements but only set switch. Remove only groups which are completely recovered.
-- DONE: Option to filter AI groups for recovery.
-- DONE: Rework radio messages. Better control over player board numbers.
-- DONE: Case I & II/III zone so that player gets into pattern automatically. Case I 3 position on the circle. Case II/III when the player enters the approach corridor maybe?
-- DONE: Add static weather information.
-- DONE: Allow up to two flights per Case I marshal stack.
-- DONE: Add max stack for Case I and define waiting queue outside CCZ.
-- DONE: Maybe do an additional step at the initial (Case II) or bullseye (Case III) and register player in case he missed some steps.
-- DONE: Subtitles off options on player level.
-- DONE: Persistence of results.
-- DONE: Foul deck waveoff.
-- DONE: Get Charlie time estimate function.
-- DONE: Average player grades until landing.
-- DONE: Check player heading at zones, e.g. initial.
-- DONE: Fix bug that player leaves the approach zone if he boltered or was waved off during Case II or III. NOTE: Partly due to increasing approach zone size.
-- DONE: Fix bug that player gets an altitude warning if stack collapses. NOTE: Would not work if two stacks Case I and II/III are used.
-- DONE: Improve radio messages. Maybe usersound for messages which are only meant for players?
-- DONE: Add voice over fly needs and welcome aboard.
-- DONE: Improve trapped wire calculation.
-- DONE: Carrier zone with dimensions of carrier. to check if landing happened on deck.
-- DONE: Carrier runway zone for fould deck check.
-- DONE: More Hints for Case II/III.
-- DONE: Set magnetic declination function.
-- DONE: First send AI to marshal and then allow them into the landing pattern ==> task function when reaching the waypoint.
-- DONE: Extract (static) weather from mission for cloud cover etc.
-- DONE: Check distance to players during approach.
-- DONE: Option to turn AI handling off.
-- DONE: Add user functions.
-- DONE: Update AI holding pattern wrt to moving carrier.
-- DONE: Generalize parameters for other carriers.
-- DONE: Generalize parameters for other aircraft.
-- DONE: Add radio check (LSO, AIRBOSS) to F10 radio menu.
-- DONE: Right pattern step after bolter/wo/patternWO? Guess so.
-- DONE: Set case II and III times (via recovery time).
-- DONE: Get correct wire when trapped. DONE but might need further tweaking.
-- DONE: Add radio transmission queue for LSO and airboss.
-- DONE: CASE II.
-- DONE: CASE III.
-- NOPE: Strike group with helo bringing cargo etc. Not yet.
-- DONE: Handle crash event. Delete A/C from queue, send rescue helo.
-- DONE: Get fuel state in pounds. (working for the hornet, did not check others)
-- DONE: Add aircraft numbers in queue to carrier info F10 radio output.
-- DONE: Monitor holding of players/AI in zoneHolding.
-- DONE: Transmission via radio.
-- DONE: Get board numbers.
-- DONE: Get an _OK_ pass if long in groove. Possible other pattern wave offs as well?!
-- DONE: Add scoring to radio menu.
-- DONE: Optimized debrief.
-- DONE: Add automatic grading.
-- DONE: Fix radio menu.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new AIRBOSS class object for a specific aircraft carrier unit.
-- @param #AIRBOSS self
-- @param carriername Name of the aircraft carrier unit as defined in the mission editor.
-- @param alias (Optional) Alias for the carrier. This will be used for radio messages and the F10 radius menu. Default is the carrier name as defined in the mission editor.
-- @return #AIRBOSS self or nil if carrier unit does not exist.
function AIRBOSS:New( carriername, alias )

  -- Inherit everthing from FSM class.
  local self = BASE:Inherit( self, FSM:New() ) -- #AIRBOSS

  -- Debug.
  self:F2( { carriername = carriername, alias = alias } )

  -- Set carrier unit.
  self.carrier = UNIT:FindByName( carriername )

  -- Check if carrier unit exists.
  if self.carrier == nil then
    -- Error message.
    local text = string.format( "ERROR: Carrier unit %s could not be found! Make sure this UNIT is defined in the mission editor and check the spelling of the unit name carefully.", carriername )
    MESSAGE:New( text, 120 ):ToAll()
    self:E( text )
    return nil
  end

  -- Set some string id for output to DCS.log file.
  self.lid = string.format( "AIRBOSS %s | ", carriername )

  -- Current map.
  self.theatre = env.mission.theatre
  self:T2( self.lid .. string.format( "Theatre = %s.", tostring( self.theatre ) ) )

  -- Get carrier type.
  self.carriertype = self.carrier:GetTypeName()

  -- Set alias.
  self.alias = alias or carriername

  -- Set carrier airbase object.
  self.airbase = AIRBASE:FindByName( carriername )

  -- Create carrier beacon.
  self.beacon = BEACON:New( self.carrier )

  -- Set Tower Frequency of carrier.
  self:_GetTowerFrequency()

  -- Init player scores table.
  self.playerscores = {}

  -- Initialize ME waypoints.
  self:_InitWaypoints()

  -- Current waypoint.
  self.currentwp = 1

  -- Patrol route.
  self:_PatrolRoute()

  -------------
  --- Defaults:
  -------------

  -- Set up Airboss radio.
  self:SetMarshalRadio()
  self:SetAirbossRadio()

  -- Set up LSO radio.
  self:SetLSORadio()

  -- Set LSO call interval. Default 4 sec.
  self:SetLSOCallInterval()

  -- Radio scheduler.
  self.radiotimer = SCHEDULER:New()

  -- Set magnetic declination.
  self:SetMagneticDeclination()

  -- Set ICSL to channel 1.
  self:SetICLS()

  -- Set TACAN to channel 74X.
  self:SetTACAN()

  -- Becons are reactivated very 5 min.
  self:SetBeaconRefresh()

  -- Set max aircraft in landing pattern. Default 4.
  self:SetMaxLandingPattern()

  -- Set max Case I Marshal stacks. Default 3.
  self:SetMaxMarshalStacks()

  -- Set max section members. Default 2.
  self:SetMaxSectionSize()

  -- Set max section distance. Default 100 meters.
  self:SetMaxSectionDistance()

  -- Set max flights per stack. Default is 2.
  self:SetMaxFlightsPerStack()

  -- Set AI handling On.
  self:SetHandleAION()

  -- No extra voiceover/calls from player by default
  self:SetExtraVoiceOvers(false)

  -- No extra voiceover/calls from AI by default
  self:SetExtraVoiceOversAI(false)

  -- Airboss is a nice guy.
  self:SetAirbossNiceGuy()

  -- Allow emergency landings.
  self:SetEmergencyLandings()

  -- No despawn after engine shutdown by default.
  self:SetDespawnOnEngineShutdown( false )

  -- No respawning of AI groups when entering the CCA.
  self:SetRespawnAI( false )

  -- Mission uses static weather by default.
  self:SetStaticWeather()

  -- Default recovery case. This sets self.defaultcase and self.case. Default Case I.
  self:SetRecoveryCase()

  -- Set time the turn starts before the window opens.
  self:SetRecoveryTurnTime()

  -- Set holding offset to 0 degrees. This set self.defaultoffset and self.holdingoffset.
  self:SetHoldingOffsetAngle()

  -- Set Marshal stack radius. Default 2.75 NM, which gives a diameter of 5.5 NM.
  self:SetMarshalRadius()

  -- Set max alt at initial. Default 1300 ft.
  self:SetInitialMaxAlt()

  -- Default player skill EASY.
  self:SetDefaultPlayerSkill( AIRBOSS.Difficulty.EASY )

  -- Default glideslope error thresholds.
  self:SetGlideslopeErrorThresholds()

  -- Default lineup error thresholds.
  self:SetLineupErrorThresholds()

  -- CCA 50 NM radius zone around the carrier.
  self:SetCarrierControlledArea()

  -- CCZ 5 NM radius zone around the carrier.
  self:SetCarrierControlledZone()

  -- Carrier patrols its waypoints until the end of time.
  self:SetPatrolAdInfinitum( true )

  -- Collision check distance. Default 5 NM.
  self:SetCollisionDistance()

  -- Set update time intervals.
  self:SetQueueUpdateTime()
  self:SetStatusUpdateTime()
  self:SetDefaultMessageDuration()

  -- Menu options.
  self:SetMenuMarkZones()
  self:SetMenuSmokeZones()
  self:SetMenuSingleCarrier( false )

  -- Welcome players.
  self:SetWelcomePlayers( true )

  -- Coordinates
  self.landingcoord = COORDINATE:New( 0, 0, 0 ) -- Core.Point#COORDINATE
  self.sterncoord = COORDINATE:New( 0, 0, 0 ) -- Core.Point#COORDINATE
  self.landingspotcoord = COORDINATE:New( 0, 0, 0 ) -- Core.Point#COORDINATE

  -- Init carrier parameters.
  if self.carriertype == AIRBOSS.CarrierType.STENNIS then
    -- Stennis parameters were updated to match the other Super Carriers.
    self:_InitNimitz()
  elseif self.carriertype == AIRBOSS.CarrierType.ROOSEVELT then
    self:_InitNimitz()
  elseif self.carriertype == AIRBOSS.CarrierType.LINCOLN then
    self:_InitNimitz()
  elseif self.carriertype == AIRBOSS.CarrierType.WASHINGTON then
    self:_InitNimitz()
  elseif self.carriertype == AIRBOSS.CarrierType.TRUMAN then
    self:_InitNimitz()
  elseif self.carriertype == AIRBOSS.CarrierType.FORRESTAL then
    self:_InitForrestal()
  elseif self.carriertype == AIRBOSS.CarrierType.ENTERPRISE66 then
    self:_InitEnterprise()
  elseif self.carriertype == AIRBOSS.CarrierType.ENTERPRISEMODERN then
    self:_InitEnterprise()
  elseif self.carriertype == AIRBOSS.CarrierType.VINSON then
    -- Carl Vinson is legacy now.
    self:_InitStennis()
  elseif self.carriertype == AIRBOSS.CarrierType.ESSEX then
    self:_InitEssex()
  elseif self.carriertype == AIRBOSS.CarrierType.BONHOMMERICHARD then
    self:_InitBonHommeRichard()
  elseif self.carriertype == AIRBOSS.CarrierType.ESSEXSCB125 then
    self:_InitEssexSCB125()
  elseif self.carriertype == AIRBOSS.CarrierType.HERMES then
    -- Hermes parameters.
    self:_InitHermes()
  elseif self.carriertype == AIRBOSS.CarrierType.INVINCIBLE then
    -- Invincible parameters.
    self:_InitInvincible()
  elseif self.carriertype == AIRBOSS.CarrierType.TARAWA then
    -- Tarawa parameters.
    self:_InitTarawa()
  elseif self.carriertype == AIRBOSS.CarrierType.AMERICA then
    -- Use America parameters.
    self:_InitAmerica()
  elseif self.carriertype == AIRBOSS.CarrierType.JCARLOS then
    -- Use Juan Carlos parameters.
    self:_InitJcarlos()
  elseif self.carriertype == AIRBOSS.CarrierType.CANBERRA then
    -- Use Juan Carlos parameters at this stage.
    self:_InitCanberra()
  elseif self.carriertype == AIRBOSS.CarrierType.KUZNETSOV then
    -- Kusnetsov parameters - maybe...
    self:_InitStennis()
  else
    self:E( self.lid .. string.format( "ERROR: Unknown carrier type %s!", tostring( self.carriertype ) ) )
    return nil
  end

  -- Init voice over files.
  self:_InitVoiceOvers()

  -------------------
  -- Debug Section --
  -------------------

  -- Debug trace.
  if false then
    self.Debug = true
    BASE:TraceOnOff( true )
    BASE:TraceClass( self.ClassName )
    BASE:TraceLevel( 3 )
    -- self.dTstatus=0.1
  end

  -- Smoke zones.
  if false then
    local case = 3
    self.holdingoffset = 30
    self:_GetZoneGroove():SmokeZone( SMOKECOLOR.Red, 5 )
    self:_GetZoneLineup():SmokeZone( SMOKECOLOR.Green, 5 )
    self:_GetZoneBullseye( case ):SmokeZone( SMOKECOLOR.White, 45 )
    self:_GetZoneDirtyUp( case ):SmokeZone( SMOKECOLOR.Orange, 45 )
    self:_GetZoneArcIn( case ):SmokeZone( SMOKECOLOR.Blue, 45 )
    self:_GetZoneArcOut( case ):SmokeZone( SMOKECOLOR.Blue, 45 )
    self:_GetZonePlatform( case ):SmokeZone( SMOKECOLOR.Blue, 45 )
    self:_GetZoneCorridor( case ):SmokeZone( SMOKECOLOR.Green, 45 )
    self:_GetZoneHolding( case, 1 ):SmokeZone( SMOKECOLOR.White, 45 )
    self:_GetZoneHolding( case, 2 ):SmokeZone( SMOKECOLOR.White, 45 )
    self:_GetZoneInitial( case ):SmokeZone( SMOKECOLOR.Orange, 45 )
    self:_GetZoneCommence( case, 1 ):SmokeZone( SMOKECOLOR.Red, 45 )
    self:_GetZoneCommence( case, 2 ):SmokeZone( SMOKECOLOR.Red, 45 )
    self:_GetZoneAbeamLandingSpot():SmokeZone( SMOKECOLOR.Red, 5 )
    self:_GetZoneLandingSpot():SmokeZone( SMOKECOLOR.Red, 5 )
  end

  -- Carrier parameter debug tests.
  if false then
    -- Stern coordinate.
    local FB = self:GetFinalBearing( false )
    local hdg = self:GetHeading( false )

    -- Stern pos.
    local stern = self:_GetSternCoord()

    -- Bow pos.
    local bow = stern:Translate( self.carrierparam.totlength, hdg, true )

    -- End of rwy.
    local rwy = stern:Translate( self.carrierparam.rwylength, FB, true )

    --- Flare points and zones.
    local function flareme()

      -- Carrier pos.
      self:GetCoordinate():FlareYellow()

      -- Stern
      stern:FlareYellow()

      -- Bow
      bow:FlareYellow()

      -- Runway half width = 10 m.
      local r1 = stern:Translate( self.carrierparam.rwywidth * 0.5, FB + 90, true )
      local r2 = stern:Translate( self.carrierparam.rwywidth * 0.5, FB - 90, true )
      -- r1:FlareWhite()
      -- r2:FlareWhite()

      -- End of runway.
      rwy:FlareRed()

      -- Right 30 meters from stern.
      local cR = stern:Translate( self.carrierparam.totwidthstarboard, hdg + 90, true )
      -- cR:FlareYellow()

      -- Left 40 meters from stern.
      local cL = stern:Translate( self.carrierparam.totwidthport, hdg - 90, true )
      -- cL:FlareYellow()

      -- Carrier specific.
      if self.carrier:GetTypeName() ~= AIRBOSS.CarrierType.INVINCIBLE or self.carrier:GetTypeName() ~= AIRBOSS.CarrierType.HERMES or self.carrier:GetTypeName() ~= AIRBOSS.CarrierType.TARAWA or self.carrier:GetTypeName() ~= AIRBOSS.CarrierType.AMERICA or self.carrier:GetTypeName() ~= AIRBOSS.CarrierType.JCARLOS or self.carrier:GetTypeName() ~= AIRBOSS.CarrierType.CANBERRA then

        -- Flare wires.
        local w1 = stern:Translate( self.carrierparam.wire1, FB, true )
        local w2 = stern:Translate( self.carrierparam.wire2, FB, true )
        local w3 = stern:Translate( self.carrierparam.wire3, FB, true )
        local w4 = stern:Translate( self.carrierparam.wire4, FB, true )
        w1:FlareWhite()
        w2:FlareYellow()
        w3:FlareWhite()
        w4:FlareYellow()

      else

        -- Abeam landing spot zone.
        local ALSPT = self:_GetZoneAbeamLandingSpot()
        ALSPT:FlareZone( FLARECOLOR.Red, 5, nil, UTILS.FeetToMeters( 120 ) )

        -- Primary landing spot zone.
        local LSPT = self:_GetZoneLandingSpot()
        LSPT:FlareZone( FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight )

        -- Landing spot coordinate.
        local PLSC = self:_GetLandingSpotCoordinate()
        PLSC:FlareWhite()
      end

      -- Flare carrier and landing runway.
      local cbox = self:_GetZoneCarrierBox()
      local rbox = self:_GetZoneRunwayBox()
      cbox:FlareZone( FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight )
      rbox:FlareZone( FLARECOLOR.White, 5, nil, self.carrierparam.deckheight )
    end

    -- Flare points every 3 seconds for 3 minutes.
    SCHEDULER:New( nil, flareme, {}, 1, 3, nil, 180 )
  end

  -----------------------
  --- FSM Transitions ---
  -----------------------

  -- Start State.
  self:SetStartState( "Stopped" )

  -- Add FSM transitions.
  --                 From State  -->   Event      -->     To State
  self:AddTransition("Stopped",       "Load",            "Stopped")     -- Load player scores from file.
  self:AddTransition("Stopped",       "Start",           "Idle")        -- Start AIRBOSS script.
  self:AddTransition("*",             "Idle",            "Idle")        -- Carrier is idling.
  self:AddTransition("Idle",          "RecoveryStart",   "Recovering")  -- Start recovering aircraft.
  self:AddTransition("Recovering",    "RecoveryStop",    "Idle")        -- Stop recovering aircraft.
  self:AddTransition("Recovering",    "RecoveryPause",   "Paused")      -- Pause recovering aircraft.
  self:AddTransition("Paused",        "RecoveryUnpause", "Recovering")  -- Unpause recovering aircraft.
  self:AddTransition("*",             "Status",          "*")           -- Update status of players and queues.
  self:AddTransition("*",             "RecoveryCase",    "*")           -- Switch to another case recovery.
  self:AddTransition("*",             "PassingWaypoint", "*")           -- Carrier is passing a waypoint.
  self:AddTransition("*",             "LSOGrade",        "*")           -- LSO grade.
  self:AddTransition("*",             "Marshal",         "*")           -- A flight was send into the marshal stack.
  self:AddTransition("*",             "Save",            "*")           -- Save player scores to file.
  self:AddTransition("*",             "Stop",            "Stopped")     -- Stop AIRBOSS FMS.


  --- Triggers the FSM event "Start" that starts the airboss. Initializes parameters and starts event handlers.
  -- @function [parent=#AIRBOSS] Start
  -- @param #AIRBOSS self

  --- Triggers the FSM event "Start" that starts the airboss after a delay. Initializes parameters and starts event handlers.
  -- @function [parent=#AIRBOSS] __Start
  -- @param #AIRBOSS self
  -- @param #number delay Delay in seconds.

  --- On after "Start" user function. Called when the AIRBOSS FSM is started.
  -- @function [parent=#AIRBOSS] OnAfterStart
  -- @param #AIRBOSS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- Triggers the FSM event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out.
  -- @function [parent=#AIRBOSS] Idle
  -- @param #AIRBOSS self

  --- Triggers the FSM delayed event "Idle" that puts the carrier into state "Idle" where no recoveries are carried out.
  -- @function [parent=#AIRBOSS] __Idle
  -- @param #AIRBOSS self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern.
  -- @function [parent=#AIRBOSS] RecoveryStart
  -- @param #AIRBOSS self
  -- @param #number Case Recovery case (1, 2 or 3) that is started.
  -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries.

  --- Triggers the FSM delayed event "RecoveryStart" that starts the recovery of aircraft. Marshalling aircraft are send to the landing pattern.
  -- @function [parent=#AIRBOSS] __RecoveryStart
  -- @param #AIRBOSS self
  -- @param #number delay Delay in seconds.
  -- @param #number Case Recovery case (1, 2 or 3) that is started.
  -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries.

  --- On after "RecoveryStart" user function. Called when recovery of aircraft is started and carrier switches to state "Recovering".
  -- @function [parent=#AIRBOSS] OnAfterRecoveryStart
  -- @param #AIRBOSS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number Case The recovery case (1, 2 or 3) to start.
  -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries.

  --- Triggers the FSM event "RecoveryStop" that stops the recovery of aircraft.
  -- @function [parent=#AIRBOSS] RecoveryStop
  -- @param #AIRBOSS self

  --- Triggers the FSM delayed event "RecoveryStop" that stops the recovery of aircraft.
  -- @function [parent=#AIRBOSS] __RecoveryStop
  -- @param #AIRBOSS self
  -- @param #number delay Delay in seconds.

  --- On after "RecoveryStop" user function. Called when recovery of aircraft is stopped.
  -- @function [parent=#AIRBOSS] OnAfterRecoveryStop
  -- @param #AIRBOSS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- Triggers the FSM event "RecoveryPause" that pauses the recovery of aircraft.
  -- @function [parent=#AIRBOSS] RecoveryPause
  -- @param #AIRBOSS self
  -- @param #number duration Duration of pause in seconds. After that recovery is automatically resumed.

  --- Triggers the FSM delayed event "RecoveryPause" that pauses the recovery of aircraft.
  -- @function [parent=#AIRBOSS] __RecoveryPause
  -- @param #AIRBOSS self
  -- @param #number delay Delay in seconds.
  -- @param #number duration Duration of pause in seconds. After that recovery is automatically resumed.

  --- Triggers the FSM event "RecoveryUnpause" that resumes the recovery of aircraft if it was paused.
  -- @function [parent=#AIRBOSS] RecoveryUnpause
  -- @param #AIRBOSS self

  --- Triggers the FSM delayed event "RecoveryUnpause" that resumes the recovery of aircraft if it was paused.
  -- @function [parent=#AIRBOSS] __RecoveryUnpause
  -- @param #AIRBOSS self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "RecoveryCase" that switches the aircraft recovery case.
  -- @function [parent=#AIRBOSS] RecoveryCase
  -- @param #AIRBOSS self
  -- @param #number Case The new recovery case (1, 2 or 3).
  -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries.

  --- Triggers the delayed FSM event "RecoveryCase" that sets the used aircraft recovery case.
  -- @function [parent=#AIRBOSS] __RecoveryCase
  -- @param #AIRBOSS self
  -- @param #number delay Delay in seconds.
  -- @param #number Case The new recovery case (1, 2 or 3).
  -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries.

  --- Triggers the FSM event "PassingWaypoint". Called when the carrier passes a waypoint.
  -- @function [parent=#AIRBOSS] PassingWaypoint
  -- @param #AIRBOSS self
  -- @param #number waypoint Number of waypoint.

  --- Triggers the FSM delayed event "PassingWaypoint". Called when the carrier passes a waypoint.
  -- @function [parent=#AIRBOSS] __PassingWaypoint
  -- @param #AIRBOSS self
  -- @param #number delay Delay in seconds.
  -- @param #number Case Recovery case (1, 2 or 3) that is started.
  -- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries.

  --- On after "PassingWaypoint" user function. Called when the carrier passes a waypoint of its route.
  -- @function [parent=#AIRBOSS] OnAfterPassingWaypoint
  -- @param #AIRBOSS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number waypoint Number of waypoint.

  --- Triggers the FSM event "Save" that saved the player scores to a file.
  -- @function [parent=#AIRBOSS] Save
  -- @param #AIRBOSS self
  -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized.
  -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv.

  --- Triggers the FSM delayed event "Save" that saved the player scores to a file.
  -- @function [parent=#AIRBOSS] __Save
  -- @param #AIRBOSS self
  -- @param #number delay Delay in seconds.
  -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized.
  -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv.

  --- On after "Save" event user function. Called when the player scores are saved to disk.
  -- @function [parent=#AIRBOSS] OnAfterSave
  -- @param #AIRBOSS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path Path where the file is saved. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized.
  -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv.

  --- Triggers the FSM event "Load" that loads the player scores from a file. AIRBOSS FSM must **not** be started at this point.
  -- @function [parent=#AIRBOSS] Load
  -- @param #AIRBOSS self
  -- @param #string path Path where the file is located. Default is the DCS installation root directory.
  -- @param #string filename (Optional) File name. Default is AIRBOSS-<ALIAS>_LSOgrades.csv.

  --- Triggers the FSM delayed event "Load" that loads the player scores from a file. AIRBOSS FSM must **not** be started at this point.
  -- @function [parent=#AIRBOSS] __Load
  -- @param #AIRBOSS self
  -- @param #number delay Delay in seconds.
  -- @param #string path Path where the file is located. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized.
  -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv.

  --- On after "Load" event user function. Called when the player scores are loaded from disk.
  -- @function [parent=#AIRBOSS] OnAfterLoad
  -- @param #AIRBOSS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path Path where the file is located. Default is the DCS installation root directory or your "Saved Games\DCS" folder if lfs was desanitized.
  -- @param #string filename (Optional) File name. Default is AIRBOSS-*ALIAS*_LSOgrades.csv.

  --- Triggers the FSM event "LSOGrade". Called when the LSO grades a player
  -- @function [parent=#AIRBOSS] LSOGrade
  -- @param #AIRBOSS self
  -- @param #AIRBOSS.PlayerData playerData Player Data.
  -- @param #AIRBOSS.LSOgrade grade LSO grade.

  --- Triggers the FSM event "LSOGrade". Delayed called when the LSO grades a player.
  -- @function [parent=#AIRBOSS] __LSOGrade
  -- @param #AIRBOSS self
  -- @param #number delay Delay in seconds.
  -- @param #AIRBOSS.PlayerData playerData Player Data.
  -- @param #AIRBOSS.LSOgrade grade LSO grade.

  --- On after "LSOGrade" user function. Called when the carrier passes a waypoint of its route.
  -- @function [parent=#AIRBOSS] OnAfterLSOGrade
  -- @param #AIRBOSS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #AIRBOSS.PlayerData playerData Player Data.
  -- @param #AIRBOSS.LSOgrade grade LSO grade.

  --- Triggers the FSM event "Marshal". Called when a flight is send to the Marshal stack.
  -- @function [parent=#AIRBOSS] Marshal
  -- @param #AIRBOSS self
  -- @param #AIRBOSS.FlightGroup flight The flight group data.

  --- Triggers the FSM event "Marshal". Delayed call when a flight is send to the Marshal stack.
  -- @function [parent=#AIRBOSS] __Marshal
  -- @param #AIRBOSS self
  -- @param #number delay Delay in seconds.
  -- @param #AIRBOSS.FlightGroup flight The flight group data.

  --- On after "Marshal" user function. Called when a flight is send to the Marshal stack.
  -- @function [parent=#AIRBOSS] OnAfterMarshal
  -- @param #AIRBOSS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #AIRBOSS.FlightGroup flight The flight group data.

  --- Triggers the FSM event "Stop" that stops the airboss. Event handlers are stopped.
  -- @function [parent=#AIRBOSS] Stop
  -- @param #AIRBOSS self

  --- Triggers the FSM event "Stop" that stops the airboss after a delay. Event handlers are stopped.
  -- @function [parent=#AIRBOSS] __Stop
  -- @param #AIRBOSS self
  -- @param #number delay Delay in seconds.

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- USER API Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set the carrier illumination mode.
-- @param #AIRBOSS self
-- @param #number Mode Options are: -2: OFF, -1: AUTO, 0: NAVIGATION, 1: AC LAUNCH, 2: AC RECOVERY
-- @return #AIRBOSS self
function AIRBOSS:SetCarrierIllumination(Mode)
  self.carrier:SetCarrierIlluminationMode(Mode)
  return self
end


--- Set welcome messages for players.
-- @param #AIRBOSS self
-- @param #boolean Switch If true, display welcome message to player.
-- @return #AIRBOSS self
function AIRBOSS:SetWelcomePlayers( Switch )

  self.welcome = Switch

  return self
end

--- Set carrier controlled area (CCA).
-- This is a large zone around the carrier, which is constantly updated wrt the carrier position.
-- @param #AIRBOSS self
-- @param #number Radius Radius of zone in nautical miles (NM). Default 50 NM.
-- @return #AIRBOSS self
function AIRBOSS:SetCarrierControlledArea( Radius )

  Radius = UTILS.NMToMeters( Radius or 50 )

  self.zoneCCA = ZONE_UNIT:New( "Carrier Controlled Area", self.carrier, Radius )

  return self
end

--- Set carrier controlled zone (CCZ).
-- This is a small zone (usually 5 NM radius) around the carrier, which is constantly updated wrt the carrier position.
-- @param #AIRBOSS self
-- @param #number Radius Radius of zone in nautical miles (NM). Default 5 NM.
-- @return #AIRBOSS self
function AIRBOSS:SetCarrierControlledZone( Radius )

  Radius = UTILS.NMToMeters( Radius or 5 )

  self.zoneCCZ = ZONE_UNIT:New( "Carrier Controlled Zone", self.carrier, Radius )

  return self
end

--- Set distance up to which water ahead is scanned for collisions.
-- @param #AIRBOSS self
-- @param #number Distance Distance in NM. Default 5 NM.
-- @return #AIRBOSS self
function AIRBOSS:SetCollisionDistance( Distance )
  self.collisiondist = UTILS.NMToMeters( Distance or 5 )
  return self
end

--- Set the default recovery case.
-- @param #AIRBOSS self
-- @param #number Case Case of recovery. Either 1, 2 or 3. Default 1.
-- @return #AIRBOSS self
function AIRBOSS:SetRecoveryCase( Case )

  -- Set default case or 1.
  self.defaultcase = Case or 1

  -- Current case init.
  self.case = self.defaultcase

  return self
end

--- Set holding pattern offset from final bearing for Case II/III recoveries.
-- Usually, this is +-15 or +-30 degrees. You should not use and offset angle >= 90 degrees, because this will cause a devision by zero in some of the equations used to calculate the approach corridor.
-- So best stick to the defaults up to 30 degrees.
-- @param #AIRBOSS self
-- @param #number Offset Offset angle in degrees. Default 0.
-- @return #AIRBOSS self
function AIRBOSS:SetHoldingOffsetAngle( Offset )

  -- Set default angle or 0.
  self.defaultoffset = Offset or 0

  -- Current offset init.
  self.holdingoffset = self.defaultoffset

  return self
end

--- Enable F10 menu to manually start recoveries.
-- @param #AIRBOSS self
-- @param #number Duration Default duration of the recovery in minutes. Default 30 min.
-- @param #number WindOnDeck Default wind on deck in knots. Default 25 knots.
-- @param #boolean Uturn U-turn after recovery window closes on=true or off=false/nil. Default off.
-- @param #number Offset Relative Marshal radial in degrees for Case II/III recoveries. Default 30°.
-- @return #AIRBOSS self
function AIRBOSS:SetMenuRecovery( Duration, WindOnDeck, Uturn, Offset )

  self.skipperMenu = true
  self.skipperTime = Duration or 30
  self.skipperSpeed = WindOnDeck or 25
  self.skipperOffset = Offset or 30

  if Uturn then
    self.skipperUturn = true
  else
    self.skipperUturn = false
  end

  return self
end

--- Add aircraft recovery time window and recovery case.
-- @param #AIRBOSS self
-- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now.
-- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time.
-- @param #number case Recovery case for that time slot. Number between one and three.
-- @param #number holdingoffset Only for CASE II/III: Angle in degrees the holding pattern is offset.
-- @param #boolean turnintowind If true, carrier will turn into the wind 5 minutes before the recovery window opens.
-- @param #number speed Speed in knots during turn into wind leg.
-- @param #boolean uturn If true (or nil), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint.
-- @return #AIRBOSS.Recovery Recovery window.
function AIRBOSS:AddRecoveryWindow( starttime, stoptime, case, holdingoffset, turnintowind, speed, uturn )

  -- Absolute mission time in seconds.
  local Tnow = timer.getAbsTime()

  if starttime and type( starttime ) == "number" then
    starttime = UTILS.SecondsToClock( Tnow + starttime )
  end

  if stoptime and type( stoptime ) == "number" then
    stoptime = UTILS.SecondsToClock( Tnow + stoptime )
  end

  -- Input or now.
  starttime = starttime or UTILS.SecondsToClock( Tnow )

  -- Set start time.
  local Tstart = UTILS.ClockToSeconds( starttime )

  -- Set stop time.
  local Tstop = stoptime and UTILS.ClockToSeconds( stoptime ) or Tstart + 90 * 60

  -- Consistancy check for timing.
  if Tstart > Tstop then
    self:E( string.format( "ERROR: Recovery stop time %s lies before recovery start time %s! Recovery window rejected.", UTILS.SecondsToClock( Tstart ), UTILS.SecondsToClock( Tstop ) ) )
    return self
  end
  if Tstop <= Tnow then
      string.format( "WARNING: Recovery stop time %s already over. Tnow=%s! Recovery window rejected.", UTILS.SecondsToClock( Tstop ), UTILS.SecondsToClock( Tnow ) )
    return self
  end

  -- Case or default value.
  case = case or self.defaultcase

  -- Holding offset or default value.
  holdingoffset = holdingoffset or self.defaultoffset

  -- Offset zero for case I.
  if case == 1 then
    holdingoffset = 0
  end

  -- Increase counter.
  self.windowcount = self.windowcount + 1

  -- Recovery window.
  local recovery = {} -- #AIRBOSS.Recovery
  recovery.START = Tstart
  recovery.STOP = Tstop
  recovery.CASE = case
  recovery.OFFSET = holdingoffset
  recovery.OPEN = false
  recovery.OVER = false
  recovery.WIND = turnintowind
  recovery.SPEED = speed or 20
  recovery.ID = self.windowcount

  if uturn == nil or uturn == true then
    recovery.UTURN = true
  else
    recovery.UTURN = false
  end

  -- Add to table
  table.insert( self.recoverytimes, recovery )

  return recovery
end

--- Define a set of AI groups that are handled by the airboss.
-- @param #AIRBOSS self
-- @param Core.Set#SET_GROUP SetGroup The set of AI groups which are handled by the airboss.
-- @return #AIRBOSS self
function AIRBOSS:SetSquadronAI( SetGroup )
  self.squadsetAI = SetGroup
  return self
end

--- Define a set of AI groups that excluded from AI handling. Members of this set will be left allone by the airboss and not forced into the Marshal pattern.
-- @param #AIRBOSS self
-- @param Core.Set#SET_GROUP SetGroup The set of AI groups which are excluded.
-- @return #AIRBOSS self
function AIRBOSS:SetExcludeAI( SetGroup )
  self.excludesetAI = SetGroup
  return self
end

--- Add a group to the exclude set. If no set exists, it is created.
-- @param #AIRBOSS self
-- @param Wrapper.Group#GROUP Group The group to be excluded.
-- @return #AIRBOSS self
function AIRBOSS:AddExcludeAI( Group )

  self.excludesetAI = self.excludesetAI or SET_GROUP:New()

  self.excludesetAI:AddGroup( Group )

  return self
end

--- Close currently running recovery window and stop recovery ops. Recovery window is deleted.
-- @param #AIRBOSS self
-- @param #number Delay (Optional) Delay in seconds before the window is deleted.
function AIRBOSS:CloseCurrentRecoveryWindow( Delay )

  if Delay and Delay > 0 then
    -- SCHEDULER:New(nil, self.CloseCurrentRecoveryWindow, {self}, delay)
    self:ScheduleOnce( Delay, self.CloseCurrentRecoveryWindow, self )
  else
    if self:IsRecovering() and self.recoverywindow and self.recoverywindow.OPEN then
      self:RecoveryStop()
      self.recoverywindow.OPEN = false
      self.recoverywindow.OVER = true
      self:DeleteRecoveryWindow( self.recoverywindow )
    end
  end
end

--- Delete all recovery windows.
-- @param #AIRBOSS self
-- @param #number Delay (Optional) Delay in seconds before the windows are deleted.
-- @return #AIRBOSS self
function AIRBOSS:DeleteAllRecoveryWindows( Delay )

  -- Loop over all recovery windows.
  for _, recovery in pairs( self.recoverytimes ) do
    self:I( self.lid .. string.format( "Deleting recovery window ID %s", tostring( recovery.ID ) ) )
    self:DeleteRecoveryWindow( recovery, Delay )
  end

  return self
end

--- Return the recovery window of the given ID.
-- @param #AIRBOSS self
-- @param #number id The ID of the recovery window.
-- @return #AIRBOSS.Recovery Recovery window with the right ID or nil if no such window exists.
function AIRBOSS:GetRecoveryWindowByID( id )
  if id then
    for _, _window in pairs( self.recoverytimes ) do
      local window = _window -- #AIRBOSS.Recovery
      if window and window.ID == id then
        return window
      end
    end
  end
  return nil
end

--- Delete a recovery window. If the window is currently open, it is closed and the recovery stopped.
-- @param #AIRBOSS self
-- @param #AIRBOSS.Recovery Window Recovery window.
-- @param #number Delay Delay in seconds, before the window is deleted.
function AIRBOSS:DeleteRecoveryWindow( Window, Delay )

  if Delay and Delay > 0 then
    -- Delayed call.
    -- SCHEDULER:New(nil, self.DeleteRecoveryWindow, {self, window}, delay)
    self:ScheduleOnce( Delay, self.DeleteRecoveryWindow, self, Window )
  else

    for i, _recovery in pairs( self.recoverytimes ) do
      local recovery = _recovery -- #AIRBOSS.Recovery

      if Window and Window.ID == recovery.ID then
        if Window.OPEN then
          -- Window is currently open.
          self:RecoveryStop()
        else
          table.remove( self.recoverytimes, i )
        end

      end
    end
  end
end

--- Set time before carrier turns and recovery window opens.
-- @param #AIRBOSS self
-- @param #number Interval Time interval in seconds. Default 300 sec.
-- @return #AIRBOSS self
function AIRBOSS:SetRecoveryTurnTime( Interval )
  self.dTturn = Interval or 300
  return self
end

--- Set multiplayer environment wire correction.
-- @param #AIRBOSS self
-- @param #number Dcorr Correction distance in meters. Default 12 m.
-- @return #AIRBOSS self
function AIRBOSS:SetMPWireCorrection( Dcorr )
  self.mpWireCorrection = Dcorr or 12
  return self
end

--- Set time interval for updating queues and other stuff.
-- @param #AIRBOSS self
-- @param #number TimeInterval Time interval in seconds. Default 30 sec.
-- @return #AIRBOSS self
function AIRBOSS:SetQueueUpdateTime( TimeInterval )
  self.dTqueue = TimeInterval or 30
  return self
end

--- Set time interval between LSO calls. Optimal time in the groove is ~16 seconds. So the default of 4 seconds gives around 3-4 correction calls in the groove.
-- @param #AIRBOSS self
-- @param #number TimeInterval Time interval in seconds between LSO calls. Default 4 sec.
-- @return #AIRBOSS self
function AIRBOSS:SetLSOCallInterval( TimeInterval )
  self.LSOdT = TimeInterval or 4
  return self
end

--- Set if old into wind calculation is used when carrier turns into the wind for a recovery.
-- @param #AIRBOSS self
-- @param #boolean SwitchOn If `true` or `nil`, use old into wind calculation.
-- @return #AIRBOSS self
function AIRBOSS:SetIntoWindLegacy( SwitchOn )
  if SwitchOn==nil then
    SwitchOn=true
  end
  self.intowindold=SwitchOn
  return self
end

--- Airboss is a rather nice guy and not strictly following the rules. Fore example, he does allow you into the landing pattern if you are not coming from the Marshal stack.
-- @param #AIRBOSS self
-- @param #boolean Switch If true or nil, Airboss bends the rules a bit.
-- @return #AIRBOSS self
function AIRBOSS:SetAirbossNiceGuy( Switch )
  if Switch == true or Switch == nil then
    self.airbossnice = true
  else
    self.airbossnice = false
  end
  return self
end

--- Allow emergency landings, i.e. bypassing any pattern and go directly to final approach.
-- @param #AIRBOSS self
-- @param #boolean Switch If true or nil, emergency landings are okay.
-- @return #AIRBOSS self
function AIRBOSS:SetEmergencyLandings( Switch )
  if Switch == true or Switch == nil then
    self.emergency = true
  else
    self.emergency = false
  end
  return self
end

--- Despawn AI groups after they they shut down their engines.
-- @param #AIRBOSS self
-- @param #boolean Switch If true or nil, AI groups are despawned.
-- @return #AIRBOSS self
function AIRBOSS:SetDespawnOnEngineShutdown( Switch )
  if Switch == true or Switch == nil then
    self.despawnshutdown = true
  else
    self.despawnshutdown = false
  end
  return self
end

--- Respawn AI groups once they reach the CCA. Clears any attached airbases and allows making them land on the carrier via script.
-- @param #AIRBOSS self
-- @param #boolean Switch If true or nil, AI groups are respawned.
-- @return #AIRBOSS self
function AIRBOSS:SetRespawnAI( Switch )
  if Switch == true or Switch == nil then
    self.respawnAI = true
  else
    self.respawnAI = false
  end
  return self
end

--- Give AI aircraft the refueling task if a recovery tanker is present or send them to the nearest divert airfield.
-- @param #AIRBOSS self
-- @param #number LowFuelThreshold Low fuel threshold in percent. AI will go refueling if their fuel level drops below this value. Default 10 %.
-- @return #AIRBOSS self
function AIRBOSS:SetRefuelAI( LowFuelThreshold )
  self.lowfuelAI = LowFuelThreshold or 10
  return self
end

--- Set max altitude to register flights in the initial zone. Aircraft above this altitude will not be registerered.
-- @param #AIRBOSS self
-- @param #number MaxAltitude Max altitude in feet. Default 1300 ft.
-- @return #AIRBOSS self
function AIRBOSS:SetInitialMaxAlt( MaxAltitude )
  self.initialmaxalt = UTILS.FeetToMeters( MaxAltitude or 1300 )
  return self
end

--- Set folder path where the airboss sound files are located **within you mission (miz) file**.
-- The default path is "l10n/DEFAULT/" but sound files simply copied there will be removed by DCS the next time you save the mission.
-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used.
-- @param #AIRBOSS self
-- @param #string FolderPath The path to the sound files, e.g. "Airboss Soundfiles/".
-- @return #AIRBOSS self
function AIRBOSS:SetSoundfilesFolder( FolderPath )

  -- Check that it ends with /
  if FolderPath then
    local lastchar = string.sub( FolderPath, -1 )
    if lastchar ~= "/" then
      FolderPath = FolderPath .. "/"
    end
  end

  -- Folderpath.
  self.soundfolder = FolderPath

  -- Info message.
  self:I( self.lid .. string.format( "Setting sound files folder to: %s", self.soundfolder ) )

  return self
end

--- Set time interval for updating player status and other things.
-- @param #AIRBOSS self
-- @param #number TimeInterval Time interval in seconds. Default 0.5 sec.
-- @return #AIRBOSS self
function AIRBOSS:SetStatusUpdateTime( TimeInterval )
  self.dTstatus = TimeInterval or 0.5
  return self
end

--- Set duration how long messages are displayed to players.
-- @param #AIRBOSS self
-- @param #number Duration Duration in seconds. Default 10 sec.
-- @return #AIRBOSS self
function AIRBOSS:SetDefaultMessageDuration( Duration )
  self.Tmessage = Duration or 10
  return self
end

--- Set glideslope error thresholds.
-- @param #AIRBOSS self
-- @param #number _max
-- @param #number _min
-- @param #number High
-- @param #number HIGH
-- @param #number Low
-- @param #number LOW
-- @return #AIRBOSS self

function AIRBOSS:SetGlideslopeErrorThresholds(_max,_min, High, HIGH, Low, LOW)

  --Check if V/STOL Carrier
  if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or 
    self.carriertype == AIRBOSS.CarrierType.HERMES or 
    self.carriertype == AIRBOSS.CarrierType.TARAWA or 
    self.carriertype == AIRBOSS.CarrierType.AMERICA or 
    self.carriertype == AIRBOSS.CarrierType.JCARLOS or 
    self.carriertype == AIRBOSS.CarrierType.CANBERRA then

    -- allow a larger GSE for V/STOL operations --Pene Testing
    self.gle._max=_max or  0.7
    self.gle.High=High or  1.4
    self.gle.HIGH=HIGH or  1.9
    self.gle._min=_min or -0.5
    self.gle.Low=Low   or -1.2
    self.gle.LOW=LOW   or -1.5
  else
    -- CVN values    
    self.gle._max=_max or  0.4
    self.gle.High=High or  0.8
    self.gle.HIGH=HIGH or  1.5
    self.gle._min=_min or -0.3
    self.gle.Low=Low   or -0.6
    self.gle.LOW=LOW   or -0.9
  end

  return self
end

--- Set lineup error thresholds.
-- @param #AIRBOSS self
-- @param #number _max
-- @param #number _min
-- @param #number Left
-- @param #number LeftMed
-- @param #number LEFT
-- @param #number Right
-- @param #number RightMed
-- @param #number RIGHT
-- @return #AIRBOSS self

function AIRBOSS:SetLineupErrorThresholds(_max,_min, Left, LeftMed, LEFT, Right, RightMed, RIGHT)

  --Check if V/STOL Carrier -- Pene testing
  if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then

  -- V/STOL Values -- allow a larger LUE for V/STOL operations
  self.lue._max=_max   or  1.8
  self.lue._min=_min   or -1.8
  self.lue.Left=Left   or -2.8
  self.lue.LeftMed=LeftMed   or -3.8
  self.lue.LEFT=LEFT   or -4.5
  self.lue.Right=Right or  2.8
  self.lue.RightMed=RightMed or  3.8
  self.lue.RIGHT=RIGHT or  4.5
  -- CVN Values
  else
  self.lue._max=_max   or  0.5
  self.lue._min=_min   or -0.5
  self.lue.Left=Left   or -1.0
  self.lue.LeftMed=LeftMed   or -2.0
  self.lue.LEFT=LEFT   or -3.0
  self.lue.Right=Right or  1.0
  self.lue.RightMed=RightMed or  2.0
  self.lue.RIGHT=RIGHT or  3.0
  end

  return self
end

--- Set Case I Marshal radius. This is the radius of the valid zone around "the post" aircraft are supposed to be holding in the Case I Marshal stack.
-- The post is 2.5 NM port of the carrier.
-- @param #AIRBOSS self
-- @param #number Radius Radius in NM. Default 2.8 NM, which gives a diameter of 5.6 NM.
-- @return #AIRBOSS self
function AIRBOSS:SetMarshalRadius( Radius )
  self.marshalradius = UTILS.NMToMeters( Radius or 2.8 )
  return self
end

--- Optimized F10 radio menu for a single carrier. The menu entries will be stored directly under F10 Other/Airboss/ and not F10 Other/Airboss/"Carrier Alias"/.
-- **WARNING**: If you use this with two airboss objects/carriers, the radio menu will be screwed up!
-- @param #AIRBOSS self
-- @param #boolean Switch If true or nil single menu is enabled. If false, menu is for multiple carriers in the mission.
-- @return #AIRBOSS self
function AIRBOSS:SetMenuSingleCarrier( Switch )
  if Switch == true or Switch == nil then
    self.menusingle = true
  else
    self.menusingle = false
  end
  return self
end

--- Enable or disable F10 radio menu for marking zones via smoke or flares.
-- @param #AIRBOSS self
-- @param #boolean Switch If true or nil, menu is enabled. If false, menu is not available to players.
-- @return #AIRBOSS self
function AIRBOSS:SetMenuMarkZones( Switch )
  if Switch == nil or Switch == true then
    self.menumarkzones = true
  else
    self.menumarkzones = false
  end
  return self
end

--- Enable or disable F10 radio menu for marking zones via smoke.
-- @param #AIRBOSS self
-- @param #boolean Switch If true or nil, menu is enabled. If false, menu is not available to players.
-- @return #AIRBOSS self
function AIRBOSS:SetMenuSmokeZones( Switch )
  if Switch == nil or Switch == true then
    self.menusmokezones = true
  else
    self.menusmokezones = false
  end
  return self
end

--- Enable saving of player's trap sheets and specify an optional directory path.
-- @param #AIRBOSS self
-- @param #string Path (Optional) Path where to save the trap sheets.
-- @param #string Prefix (Optional) Prefix for trap sheet files. File name will be saved as *prefix_aircrafttype-0001.csv*, *prefix_aircrafttype-0002.csv*, etc.
-- @return #AIRBOSS self
function AIRBOSS:SetTrapSheet( Path, Prefix )
  if io then
    self.trapsheet = true
    self.trappath = Path
    self.trapprefix = Prefix
  else
    self:E( self.lid .. "ERROR: io is not desanitized. Cannot save trap sheet." )
  end
  return self
end

--- Specify weather the mission has set static or dynamic weather.
-- @param #AIRBOSS self
-- @param #boolean Switch If true or nil, mission uses static weather. If false, dynamic weather is used in this mission.
-- @return #AIRBOSS self
function AIRBOSS:SetStaticWeather( Switch )
  if Switch == nil or Switch == true then
    self.staticweather = true
  else
    self.staticweather = false
  end
  return self
end

--- Disable automatic TACAN activation
-- @param #AIRBOSS self
-- @return #AIRBOSS self
function AIRBOSS:SetTACANoff()
  self.TACANon = false
  return self
end

--- Set TACAN channel of carrier and switches TACAN on.
-- @param #AIRBOSS self
-- @param #number Channel (Optional) TACAN channel. Default 74.
-- @param #string Mode (Optional) TACAN mode, i.e. "X" or "Y". Default "X".
-- @param #string MorseCode (Optional) Morse code identifier. Three letters, e.g. "STN". Default "STN".
-- @return #AIRBOSS self
function AIRBOSS:SetTACAN( Channel, Mode, MorseCode )

  self.TACANchannel = Channel or 74
  self.TACANmode = Mode or "X"
  self.TACANmorse = MorseCode or "STN"
  self.TACANon = true

  return self
end

--- Disable automatic ICLS activation.
-- @param #AIRBOSS self
-- @return #AIRBOSS self
function AIRBOSS:SetICLSoff()
  self.ICLSon = false
  return self
end

--- Set ICLS channel of carrier.
-- @param #AIRBOSS self
-- @param #number Channel (Optional) ICLS channel. Default 1.
-- @param #string MorseCode (Optional) Morse code identifier. Three letters, e.g. "STN". Default "STN".
-- @return #AIRBOSS self
function AIRBOSS:SetICLS( Channel, MorseCode )

  self.ICLSchannel = Channel or 1
  self.ICLSmorse = MorseCode or "STN"
  self.ICLSon = true

  return self
end

--- Set beacon (TACAN/ICLS) time refresh interfal in case the beacons die.
-- @param #AIRBOSS self
-- @param #number TimeInterval (Optional) Time interval in seconds. Default 1200 sec = 20 min.
-- @return #AIRBOSS self
function AIRBOSS:SetBeaconRefresh( TimeInterval )
  self.dTbeacon = TimeInterval or (20 * 60)
  return self
end

--- Set up SRS for usage without sound files
-- @param #AIRBOSS self
-- @param #string PathToSRS Path to SRS folder, e.g. "C:\\Program Files\\DCS-SimpleRadio\\ExternalAudio".
-- @param #number Port Port of the SRS server, defaults to 5002.
-- @param #string Culture (Optional, Airboss Culture)  Culture, defaults to "en-US".
-- @param #string Gender (Optional, Airboss Gender)  Gender, e.g. "male" or "female". Defaults to "male".
-- @param #string Voice (Optional, Airboss Voice) Set to use a specific voice. Will **override gender and culture** settings.
-- @param #string GoogleCreds (Optional) Path to Google credentials, e.g. "C:\\Program Files\\DCS-SimpleRadio-Standalone\\yourgooglekey.json".
-- @param #number Volume (Optional) E.g. 0.75. Defaults to 1.0 (loudest).
-- @param #table AltBackend (Optional) See MSRS for details.
-- @return #AIRBOSS self
function AIRBOSS:EnableSRS(PathToSRS,Port,Culture,Gender,Voice,GoogleCreds,Volume,AltBackend)
  -- SRS
  local Frequency = self.AirbossRadio.frequency
  local Modulation = self.AirbossRadio.modulation
  self.SRS = MSRS:New(PathToSRS,Frequency,Modulation,AltBackend)
  self.SRS:SetCoalition(self:GetCoalition())
  self.SRS:SetCoordinate(self:GetCoordinate())
  self.SRS:SetCulture(Culture or "en-US")
  --self.SRS:SetFrequencies(Frequencies)
  self.SRS:SetGender(Gender or "male")
  --self.SRS:SetPath(PathToSRS)
  self.SRS:SetPort(Port or MSRS.port or 5002)
  self.SRS:SetLabel(self.AirbossRadio.alias or "AIRBOSS")
  self.SRS:SetCoordinate(self.carrier:GetCoordinate())
  self.SRS:SetVolume(Volume or 1)
  --self.SRS:SetModulations(Modulations)
  if GoogleCreds then
    self.SRS:SetGoogle(GoogleCreds)
  end
  if Voice then
    self.SRS:SetVoice(Voice)
  end
  if (not Voice) and self.SRS and self.SRS:GetProvider() == MSRS.Provider.GOOGLE then
    self.SRS.voice = MSRS.poptions["gcloud"].voice or MSRS.Voices.Google.Standard.en_US_Standard_B
  end
  --self.SRS:SetVolume(Volume or 1.0)
  -- SRSQUEUE
  self.SRSQ = MSRSQUEUE:New("AIRBOSS")
  self.SRSQ:SetTransmitOnlyWithPlayers(true)
  if not self.PilotRadio then
    self:SetSRSPilotVoice()
  end
  return self
end

--- Set LSO radio frequency and modulation. Default frequency is 264 MHz AM.
-- @param #AIRBOSS self
-- @param #number Frequency (Optional) Frequency in MHz. Default 264 MHz.
-- @param #string Modulation (Optional) Modulation, "AM" or "FM". Default "AM".
-- @param #string Voice (Optional) SRS specific voice
-- @param #string Gender (Optional) SRS specific gender
-- @param #string Culture (Optional) SRS specific culture
-- @return #AIRBOSS self
function AIRBOSS:SetLSORadio( Frequency, Modulation, Voice, Gender, Culture  )

  self.LSOFreq = (Frequency or 264)
  Modulation = Modulation or "AM"

  if Modulation == "FM" then
    self.LSOModu = radio.modulation.FM
  else
    self.LSOModu = radio.modulation.AM
  end

  self.LSORadio = {} -- #AIRBOSS.Radio
  self.LSORadio.frequency = self.LSOFreq
  self.LSORadio.modulation = self.LSOModu
  self.LSORadio.alias = "LSO"
  self.LSORadio.voice = Voice
  self.LSORadio.gender = Gender or "male"
  self.LSORadio.culture = Culture or "en-US"

  return self
end

--- Set Airboss radio frequency and modulation. Default frequency is Tower frequency.
-- @param #AIRBOSS self
-- @param #number Frequency (Optional) Frequency in MHz. Default frequency is Tower frequency.
-- @param #string Modulation (Optional) Modulation, "AM" or "FM". Default "AM".
-- @param #string Voice (Optional) SRS specific voice
-- @param #string Gender (Optional) SRS specific gender
-- @param #string Culture (Optional) SRS specific culture
-- @return #AIRBOSS self
-- @usage
--    -- Set single frequency
--    myairboss:SetAirbossRadio(127.5,"AM",MSRS.Voices.Google.Standard.en_GB_Standard_F)
--
--    -- Set multiple frequencies, note you **need** to pass one modulation per frequency given!
--    myairboss:SetAirbossRadio({127.5,243},{radio.modulation.AM,radio.modulation.AM},MSRS.Voices.Google.Standard.en_GB_Standard_F)
function AIRBOSS:SetAirbossRadio( Frequency, Modulation, Voice, Gender, Culture )

  self.AirbossFreq = Frequency or self:_GetTowerFrequency() or 127.5
  Modulation = Modulation or "AM"

  if type(Modulation) == "table" then
    self.AirbossModu = Modulation
  else
    if Modulation == "FM" then
      self.AirbossModu = radio.modulation.FM
    else
      self.AirbossModu = radio.modulation.AM
    end
  end

  self.AirbossRadio = {} -- #AIRBOSS.Radio
  self.AirbossRadio.frequency = self.AirbossFreq
  self.AirbossRadio.modulation = self.AirbossModu
  self.AirbossRadio.alias = "AIRBOSS"
  self.AirbossRadio.voice = Voice
  self.AirbossRadio.gender = Gender or "male"
  self.AirbossRadio.culture = Culture or "en-US"

  return self
end

--- Set Marshal radio frequency and modulation. Default frequency is 305 MHz AM.
-- @param #AIRBOSS self
-- @param #number Frequency (Optional) Frequency in MHz. Default 305 MHz.
-- @param #string Modulation (Optional) Modulation, "AM" or "FM". Default "AM".
-- @param #string Voice (Optional) SRS specific voice
-- @param #string Gender (Optional) SRS specific gender
-- @param #string Culture (Optional) SRS specific culture
-- @return #AIRBOSS self
function AIRBOSS:SetMarshalRadio( Frequency, Modulation, Voice, Gender, Culture )

  self.MarshalFreq = Frequency or 305
  Modulation = Modulation or "AM"

  if Modulation == "FM" then
    self.MarshalModu = radio.modulation.FM
  else
    self.MarshalModu = radio.modulation.AM
  end

  self.MarshalRadio = {} -- #AIRBOSS.Radio
  self.MarshalRadio.frequency = self.MarshalFreq
  self.MarshalRadio.modulation = self.MarshalModu
  self.MarshalRadio.alias = "MARSHAL"
  self.MarshalRadio.voice = Voice
  self.MarshalRadio.gender = Gender or "male"
  self.MarshalRadio.culture = Culture or "en-US"

  return self
end

--- Set unit name for sending radio messages.
-- @param #AIRBOSS self
-- @param #string unitname Name of the unit.
-- @return #AIRBOSS self
function AIRBOSS:SetRadioUnitName( unitname )
  self.senderac = unitname
  return self
end

--- Set unit acting as radio relay for the LSO radio.
-- @param #AIRBOSS self
-- @param #string unitname Name of the unit.
-- @return #AIRBOSS self
function AIRBOSS:SetRadioRelayLSO( unitname )
  self.radiorelayLSO = unitname
  return self
end

--- Set unit acting as radio relay for the Marshal radio.
-- @param #AIRBOSS self
-- @param #string unitname Name of the unit.
-- @return #AIRBOSS self
function AIRBOSS:SetRadioRelayMarshal( unitname )
  self.radiorelayMSH = unitname
  return self
end

--- Use user sound output instead of radio transmission for messages. Might be handy if radio transmissions are broken.
-- @param #AIRBOSS self
-- @return #AIRBOSS self
function AIRBOSS:SetUserSoundRadio()
  self.usersoundradio = true
  return self
end

--- Test LSO radio sounds.
-- @param #AIRBOSS self
-- @param #number delay Delay in seconds be sound check starts.
-- @return #AIRBOSS self
function AIRBOSS:SoundCheckLSO( delay )

  if delay and delay > 0 then
    -- Delayed call.
    -- SCHEDULER:New(nil, AIRBOSS.SoundCheckLSO, {self}, delay)
    self:ScheduleOnce( delay, AIRBOSS.SoundCheckLSO, self )
  else

    local text = "Playing LSO sound files:"

    for _name, _call in pairs( self.LSOCall ) do
      local call = _call -- #AIRBOSS.RadioCall

      -- Debug text.
      text = text .. string.format( "\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring( call.loud ), call.subtitle )

      -- Radio transmission to queue.
      self:RadioTransmission( self.LSORadio, call, false )

      -- Also play the loud version.
      if call.loud then
        self:RadioTransmission( self.LSORadio, call, true )
      end
    end

    -- Debug message.
    self:T( self.lid .. text )

  end
end

--- Test Marshal radio sounds.
-- @param #AIRBOSS self
-- @param #number delay Delay in seconds be sound check starts.
-- @return #AIRBOSS self
function AIRBOSS:SoundCheckMarshal( delay )

  if delay and delay > 0 then
    -- Delayed call.
    -- SCHEDULER:New(nil, AIRBOSS.SoundCheckMarshal, {self}, delay)
    self:ScheduleOnce( delay, AIRBOSS.SoundCheckMarshal, self )
  else

    local text = "Playing Marshal sound files:"

    for _name, _call in pairs( self.MarshalCall ) do
      local call = _call -- #AIRBOSS.RadioCall

      -- Debug text.
      text = text .. string.format( "\nFile=%s.%s, duration=%.2f sec, loud=%s, subtitle=\"%s\".", call.file, call.suffix, call.duration, tostring( call.loud ), call.subtitle )

      -- Radio transmission to queue.
      self:RadioTransmission( self.MarshalRadio, call, false )

      -- Also play the loud version.
      if call.loud then
        self:RadioTransmission( self.MarshalRadio, call, true )
      end
    end

    -- Debug message.
    self:T( self.lid .. text )

  end
end

--- Set number of aircraft units, which can be in the landing pattern before the pattern is full.
-- @param #AIRBOSS self
-- @param #number nmax Max number. Default 4. Minimum is 1, maximum is 6.
-- @return #AIRBOSS self
function AIRBOSS:SetMaxLandingPattern( nmax )
  nmax = nmax or 4
  nmax = math.max( nmax, 1 )
  nmax = math.min( nmax, 6 )
  self.Nmaxpattern = nmax
  return self
end

--- Set number available Case I Marshal stacks. If Marshal stacks are full, flights requesting Marshal will be told to hold outside 10 NM zone until a stack becomes available again.
-- Marshal stacks for Case II/III are unlimited.
-- @param #AIRBOSS self
-- @param #number nmax Max number of stacks available to players and AI flights. Default 3, i.e. angels 2, 3, 4. Minimum is 1.
-- @return #AIRBOSS self
function AIRBOSS:SetMaxMarshalStacks( nmax )
  self.Nmaxmarshal = nmax or 3
  self.Nmaxmarshal = math.max( self.Nmaxmarshal, 1 )
  return self
end

--- Set max number of section members. Minimum is one, i.e. the section lead itself. Maximum number is four. Default is two, i.e. the lead and one other human flight.
-- @param #AIRBOSS self
-- @param #number nmax Number of max allowed members including the lead itself. For example, Nmax=2 means a section lead plus one member.
-- @return #AIRBOSS self
function AIRBOSS:SetMaxSectionSize( nmax )
  nmax = nmax or 2
  nmax = math.max( nmax, 1 )
  nmax = math.min( nmax, 4 )
  self.NmaxSection = nmax - 1 -- We substract one because internally the section lead is not counted!
  return self
end

--- Set maximum distance up to which section members are allowed (default: 100 meters).
-- @param #AIRBOSS self
-- @param #number dmax Max distance in meters (default 100 m). Minimum is 10 m, maximum is 5000 m.
-- @return #AIRBOSS self
function AIRBOSS:SetMaxSectionDistance( dmax )
    if dmax then
        if dmax < 10 then
            dmax = 10
        elseif dmax > 5000 then
            dmax = 5000
        end
    end
    self.maxsectiondistance = dmax or 100
    return self
end

--- Set max number of flights per stack. All members of a section count as one "flight".
-- @param #AIRBOSS self
-- @param #number nmax Number of max allowed flights per stack. Default is two. Minimum is one, maximum is 4.
-- @return #AIRBOSS self
function AIRBOSS:SetMaxFlightsPerStack( nmax )
  nmax = nmax or 2
  nmax = math.max( nmax, 1 )
  nmax = math.min( nmax, 4 )
  self.NmaxStack = nmax
  return self
end

--- Handle AI aircraft.
-- @param #AIRBOSS self
-- @return #AIRBOSS self
function AIRBOSS:SetHandleAION()
  self.handleai = true
  return self
end

--- Will play the inbound calls, commencing, initial, etc. from the player when requesteing marshal
-- @param #AIRBOSS self
-- @param #AIRBOSS status Boolean to activate (true) / deactivate (false) the radio inbound calls (default is ON)
-- @return #AIRBOSS self
function AIRBOSS:SetExtraVoiceOvers(status)
  self.xtVoiceOvers=status
  return self
end

--- Will simulate the inbound call, commencing, initial, etc from the AI when requested by Airboss
-- @param #AIRBOSS self
-- @param #AIRBOSS status Boolean to activate (true) / deactivate (false) the radio inbound calls (default is ON)
-- @return #AIRBOSS self
function AIRBOSS:SetExtraVoiceOversAI(status)
  self.xtVoiceOversAI=status
  return self
end

--- Do not handle AI aircraft.
-- @param #AIRBOSS self
-- @return #AIRBOSS self
function AIRBOSS:SetHandleAIOFF()
  self.handleai = false
  return self
end

--- Define recovery tanker associated with the carrier.
-- @param #AIRBOSS self
-- @param Ops.RecoveryTanker#RECOVERYTANKER recoverytanker Recovery tanker object.
-- @return #AIRBOSS self
function AIRBOSS:SetRecoveryTanker( recoverytanker )
  self.tanker = recoverytanker
  return self
end

--- Define an AWACS associated with the carrier.
-- @param #AIRBOSS self
-- @param Ops.RecoveryTanker#RECOVERYTANKER awacs AWACS (recovery tanker) object.
-- @return #AIRBOSS self
function AIRBOSS:SetAWACS( awacs )
  self.awacs = awacs
  return self
end

--- Set default player skill. New players will be initialized with this skill.
--
-- * "Flight Student" = @{#AIRBOSS.Difficulty.Easy}
-- * "Naval Aviator" = @{#AIRBOSS.Difficulty.Normal}
-- * "TOPGUN Graduate" = @{#AIRBOSS.Difficulty.Hard}
-- @param #AIRBOSS self
-- @param #string skill Player skill. Default "Naval Aviator".
-- @return #AIRBOSS self
function AIRBOSS:SetDefaultPlayerSkill( skill )

  -- Set skill or normal.
  self.defaultskill = skill or AIRBOSS.Difficulty.NORMAL

  -- Check that defualt skill is valid.
  local gotit = false
  for _, _skill in pairs( AIRBOSS.Difficulty ) do
    if _skill == self.defaultskill then
      gotit = true
    end
  end

  -- If invalid user input, fall back to normal.
  if not gotit then
    self.defaultskill = AIRBOSS.Difficulty.NORMAL
    self:E( self.lid .. string.format( "ERROR: Invalid default skill = %s. Resetting to Naval Aviator.", tostring( skill ) ) )
  end

  return self
end

--- Enable auto save of player results each time a player is *finally* graded. *Finally* means after the player landed on the carrier! After intermediate passes (bolter or waveoff) the stats are *not* saved.
-- @param #AIRBOSS self
-- @param #string path Path where to save the asset data file. Default is the DCS root installation directory or your "Saved Games\\DCS" folder if lfs was desanitized.
-- @param #string filename File name. Default is generated automatically from airboss carrier name/alias.
-- @return #AIRBOSS self
function AIRBOSS:SetAutoSave( path, filename )
  self.autosave = true
  self.autosavepath = path
  self.autosavefile = filename
  return self
end

--- Activate debug mode. Display debug messages on screen.
-- @param #AIRBOSS self
-- @return #AIRBOSS self
function AIRBOSS:SetDebugModeON()
  self.Debug = true
  return self
end

--- Carrier patrols ad inifintum. If the last waypoint is reached, it will go to waypoint one and repeat its route.
-- @param #AIRBOSS self
-- @param #boolean switch If true or nil, patrol until the end of time. If false, go along the waypoints once and stop.
-- @return #AIRBOSS self
function AIRBOSS:SetPatrolAdInfinitum( switch )
  if switch == false then
    self.adinfinitum = false
  else
    self.adinfinitum = true
  end
  return self
end

--- Set the magnetic declination (or variation). By default this is set to the standard declination of the map.
-- @param #AIRBOSS self
-- @param #number declination Declination in degrees or nil for default declination of the map.
-- @return #AIRBOSS self
function AIRBOSS:SetMagneticDeclination( declination )
  self.magvar = declination or UTILS.GetMagneticDeclination()
  return self
end

--- Deactivate debug mode. This is also the default setting.
-- @param #AIRBOSS self
-- @return #AIRBOSS self
function AIRBOSS:SetDebugModeOFF()
  self.Debug = false
  return self
end


--- Set FunkMan socket. LSO grades and trap sheets will be send to your Discord bot.
-- **Requires running FunkMan program**.
-- @param #AIRBOSS self
-- @param #number Port Port. Default `10042`.
-- @param #string Host Host. Default `"127.0.0.1"`.
-- @return #AIRBOSS self
function AIRBOSS:SetFunkManOn(Port, Host)

  self.funkmanSocket=SOCKET:New(Port, Host)

  return self
end

--- Get next time the carrier will start recovering aircraft.
-- @param #AIRBOSS self
-- @param #boolean InSeconds If true, abs. mission time seconds is returned. Default is a clock #string.
-- @return #string Clock start (or start time in abs. seconds).
-- @return #string Clock stop (or stop time in abs. seconds).
function AIRBOSS:GetNextRecoveryTime( InSeconds )
  if self.recoverywindow then
    if InSeconds then
      return self.recoverywindow.START, self.recoverywindow.STOP
    else
      return UTILS.SecondsToClock( self.recoverywindow.START ), UTILS.SecondsToClock( self.recoverywindow.STOP )
    end
  else
    if InSeconds then
      return -1, -1
    else
      return "?", "?"
    end
  end
end

--- Check if carrier is recovering aircraft.
-- @param #AIRBOSS self
-- @return #boolean If true, time slot for recovery is open.
function AIRBOSS:IsRecovering()
  return self:is( "Recovering" )
end

--- Check if carrier is idle, i.e. no operations are carried out.
-- @param #AIRBOSS self
-- @return #boolean If true, carrier is in idle state.
function AIRBOSS:IsIdle()
  return self:is( "Idle" )
end

--- Check if recovery of aircraft is paused.
-- @param #AIRBOSS self
-- @return #boolean If true, recovery is paused
function AIRBOSS:IsPaused()
  return self:is( "Paused" )
end

--- Activate TACAN and ICLS beacons.
-- @param #AIRBOSS self
function AIRBOSS:_ActivateBeacons()
  self:T( self.lid .. string.format( "Activating Beacons (TACAN=%s, ICLS=%s)", tostring( self.TACANon ), tostring( self.ICLSon ) ) )

  -- Activate TACAN.
  if self.TACANon then
    self:I( self.lid .. string.format( "Activating TACAN Channel %d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse ) )
    self.beacon:ActivateTACAN( self.TACANchannel, self.TACANmode, self.TACANmorse, true )
  end

  -- Activate ICLS.
  if self.ICLSon then
    self:I( self.lid .. string.format( "Activating ICLS Channel %d (%s)", self.ICLSchannel, self.ICLSmorse ) )
    self.beacon:ActivateICLS( self.ICLSchannel, self.ICLSmorse )
  end

  -- Set time stamp.
  self.Tbeacon = timer.getTime()
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM event functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after Start event. Starts the AIRBOSS. Adds event handlers and schedules status updates of requests and queue.
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AIRBOSS:onafterStart( From, Event, To )

  -- Events are handled my MOOSE.
  self:I( self.lid .. string.format( "Starting AIRBOSS v%s for carrier unit %s of type %s on map %s", AIRBOSS.version, self.carrier:GetName(), self.carriertype, self.theatre ) )

  -- Activate TACAN and ICLS if desired.
  self:_ActivateBeacons()

  -- Schedule radio queue checks.
  -- self.RQLid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQLSO,     "LSO"},     1, 0.1)
  -- self.RQMid=self.radiotimer:Schedule(nil, AIRBOSS._CheckRadioQueue, {self, self.RQMarshal, "MARSHAL"}, 1, 0.1)

  -- self:I("FF: starting timer.scheduleFunction")
  -- timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQLSO,     name="LSO"},     timer.getTime()+1)
  -- timer.scheduleFunction(AIRBOSS._CheckRadioQueueT, {airboss=self, radioqueue=self.RQMarshal, name="MARSHAL"}, timer.getTime()+1)

  -- Initial carrier position and orientation.
  self.Cposition = self:GetCoordinate()
  self.Corientation = self.carrier:GetOrientationX()
  self.Corientlast = self.Corientation
  self.Tpupdate = timer.getTime()

  -- Check if no recovery window is set. DISABLED!
  if #self.recoverytimes == 0 and false then

    -- Open window in 15 minutes for 3 hours.
    local Topen = timer.getAbsTime() + 15 * 60
    local Tclose = Topen + 3 * 60 * 60

    -- Add window.
    self:AddRecoveryWindow( UTILS.SecondsToClock( Topen ), UTILS.SecondsToClock( Tclose ) )
  end

  -- Check Recovery time.s
  self:_CheckRecoveryTimes()

  -- Time stamp for checking queues. We substract 60 seconds so the routine is called right after status is called the first time.
  self.Tqueue = timer.getTime() - 60

  -- Handle events.
  self:HandleEvent( EVENTS.Birth )
  self:HandleEvent( EVENTS.RunwayTouch )
  self:HandleEvent( EVENTS.EngineShutdown )
  self:HandleEvent( EVENTS.Takeoff )
  self:HandleEvent( EVENTS.Crash )
  self:HandleEvent( EVENTS.Ejection )
  self:HandleEvent( EVENTS.PlayerLeaveUnit, self._PlayerLeft )
  self:HandleEvent( EVENTS.MissionEnd )
  self:HandleEvent( EVENTS.RemoveUnit )

  -- self.StatusScheduler=SCHEDULER:New(self)
  -- self.StatusScheduler:Schedule(self, self._Status, {}, 1, 0.5)

  self.StatusTimer = TIMER:New( self._Status, self ):Start( 2, 0.5 )

  -- Start status check in 1 second.
  self:__Status( 1 )
end

--- On after Status event. Checks for new flights, updates queue and checks player status.
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AIRBOSS:onafterStatus( From, Event, To )

  -- Get current time.
  local time = timer.getTime()

  -- Update marshal and pattern queue every 30 seconds.
  if time - self.Tqueue > self.dTqueue then

    -- Get time.
    local clock = UTILS.SecondsToClock( timer.getAbsTime() )
    local eta = UTILS.SecondsToClock( self:_GetETAatNextWP() )

    -- Current heading and position of the carrier.
    local hdg = self:GetHeading()
    local pos = self:GetCoordinate()
    local speed = self.carrier:GetVelocityKNOTS()

    -- Update magnetic variation if we can get it from DCS.
    if require then
      self.magvar=pos:GetMagneticDeclination()
      --env.info(string.format("FF magvar=%.1f", self.magvar))
    end

    -- Check water is ahead.
    local collision = false -- self:_CheckCollisionCoord(pos:Translate(self.collisiondist, hdg))

    local holdtime = 0
    if self.holdtimestamp then
      holdtime = timer.getTime() - self.holdtimestamp
    end

    -- Check if carrier is stationary.
    local NextWP = self:_GetNextWaypoint()
    local ExpectedSpeed = UTILS.MpsToKnots( NextWP:GetVelocity() )
    if speed < 0.5 and ExpectedSpeed > 0 and not (self.detour or self.turnintowind) then
      if not self.holdtimestamp then
        self:E( self.lid .. string.format( "Carrier came to an unexpected standstill. Trying to re-route in 3 min. Speed=%.1f knots, expected=%.1f knots", speed, ExpectedSpeed ) )
        self.holdtimestamp = timer.getTime()
      else
        if holdtime > 3 * 60 then
          local coord = self:GetCoordinate():Translate( 500, hdg + 10 )
          -- coord:MarkToAll("Re-route after standstill.")
          self:CarrierResumeRoute( coord )
          self.holdtimestamp = nil
        end
      end
    end

    -- Debug info.
    local text = string.format( "Time %s - Status %s (case=%d) - Speed=%.1f kts - Heading=%d - WP=%d - ETA=%s - Turning=%s - Collision Warning=%s - Detour=%s - Turn Into Wind=%s - Holdtime=%d sec", clock, self:GetState(), self.case, speed, hdg, self.currentwp, eta, tostring( self.turning ), tostring( collision ), tostring( self.detour ), tostring( self.turnintowind ), holdtime )
    self:T( self.lid .. text )

    -- Players online:
    text = "Players:"
    local i = 0
    for _name, _player in pairs( self.players ) do
      i = i + 1
      local player = _player -- #AIRBOSS.FlightGroup
      text = text .. string.format( "\n%d.) %s: Step=%s, Unit=%s, Airframe=%s", i, tostring( player.name ), tostring( player.step ), tostring( player.unitname ), tostring( player.actype ) )
    end
    if i == 0 then
      text = text .. " none"
    end
    self:T( self.lid .. text )

    -- Check for collision.
    if collision then

      -- We are currently turning into the wind.
      if self.turnintowind then

        -- Carrier resumes its initial route. This disables turnintowind switch.
        self:CarrierResumeRoute( self.Creturnto )

        -- Since current window would stay open, we disable the WIND switch.
        if self:IsRecovering() and self.recoverywindow and self.recoverywindow.WIND then
          -- Disable turn into the wind for this window so that we do not do this all over again.
          self.recoverywindow.WIND = false
        end

      end

    end

    -- Check recovery times and start/stop recovery mode if necessary.
    self:_CheckRecoveryTimes()

    -- Remove dead/zombie flight groups. Player leaving the server whilst in pattern etc.
    -- self:_RemoveDeadFlightGroups()

    -- Scan carrier zone for new aircraft.
    self:_ScanCarrierZone()

    -- Check marshal and pattern queues.
    self:_CheckQueue()

    -- Check if carrier is currently turning.
    self:_CheckCarrierTurning()

    -- Check if marshal pattern of AI needs an update.
    self:_CheckPatternUpdate()

    -- Time stamp.
    self.Tqueue = time
  end

  -- (Re-)activate TACAN and ICLS channels.
  if time - self.Tbeacon > self.dTbeacon then
    self:_ActivateBeacons()
  end

  -- Call status every ~0.5 seconds.
  self:__Status( -30 )

end

--- Check AI status. Pattern queue AI in the groove? Marshal queue AI arrived in holding zone?
-- @param #AIRBOSS self
function AIRBOSS:_Status()

  -- Check player status.
  self:_CheckPlayerStatus()

  -- Check AI landing pattern status
  self:_CheckAIStatus()

end

--- Check AI status. Pattern queue AI in the groove? Marshal queue AI arrived in holding zone?
-- @param #AIRBOSS self
function AIRBOSS:_CheckAIStatus()

  -- Loop over all flights in Marshal stack.
  for _, _flight in pairs( self.Qmarshal ) do
    local flight = _flight -- #AIRBOSS.FlightGroup

    -- Only AI!
    if flight.ai then

      -- Get fuel amount in %.
      local fuel = flight.group:GetFuelMin() * 100

      -- Debug text.
      local text = string.format( "Group %s fuel=%.1f %%", flight.groupname, fuel )
      self:T3( self.lid .. text )

      -- Check if flight is low on fuel and not yet refueling.
      if self.lowfuelAI and fuel < self.lowfuelAI and not flight.refueling then

        -- Send AI for refueling at tanker or divert field.
        self:_RefuelAI( flight )

        -- Remove flight from marshal queue.
        self:_RemoveFlightFromMarshalQueue( flight, true )

      end

    end
  end

  -- Loop over all flights in landing pattern.
  for _, _flight in pairs( self.Qpattern ) do
    local flight = _flight -- #AIRBOSS.FlightGroup

    -- Only AI!
    if flight.ai then

      -- Loop over all units in AI flight.
      for _, _element in pairs( flight.elements ) do
        local element = _element -- #AIRBOSS.FlightElement

        -- Unit
        local unit = element.unit

        -- Get lineup and distance to carrier.
        local lineup = self:_Lineup( unit, true )

        local unitcoord = unit:GetCoord()

        local dist = unitcoord:Get2DDistance( self:GetCoord() )

        -- Distance in NM.
        local distance = UTILS.MetersToNM( dist )

        -- Altitude in ft.
        local alt = UTILS.MetersToFeet( unitcoord.y )

        -- Check if parameters are right and flight is in the groove.
        if lineup < 2 and distance <= 0.75 and alt < 500 and not element.ballcall then

          -- Paddles: Call the ball!
          self:RadioTransmission( self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true )

          -- Pilot: "405, Hornet Ball, 3.2"
          self:_LSOCallAircraftBall( element.onboard, self:_GetACNickname( unit:GetTypeName() ), self:_GetFuelState( unit ) / 1000 )

          -- Paddles: Roger ball after 0.5 seconds.
          self:RadioTransmission( self.LSORadio, self.LSOCall.ROGERBALL, nil, nil, 0.5, true )

          -- Flight element called the ball.
          element.ballcall = true

          -- This is for the whole flight. Maybe we need it.
          flight.ballcall = true
        end

      end
    end
  end

end

--- Check if player in the landing pattern is too close to another aircarft in the pattern.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData player Player data.
function AIRBOSS:_CheckPlayerPatternDistance( player )

  -- Check if player is too close to another aircraft in the pattern.
  -- TODO: At which steps is the really necessary. Case II/III?
  if  player.step==AIRBOSS.PatternStep.INITIAL    or
      player.step==AIRBOSS.PatternStep.BREAKENTRY or
      player.step==AIRBOSS.PatternStep.EARLYBREAK or
      player.step==AIRBOSS.PatternStep.LATEBREAK  or
      player.step==AIRBOSS.PatternStep.ABEAM      or
      player.step==AIRBOSS.PatternStep.GROOVE_XX  or
      player.step==AIRBOSS.PatternStep.GROOVE_IM  then

    -- Right step but not implemented.
    return

  else
    -- Wrong step - no check performed.
    return
  end

  -- Nothing to do since we check only in the pattern.
  if #self.Qpattern == 0 then
    return
  end

  --- Function that checks if unit1 is too close to unit2.
  local function _checkclose( _unit1, _unit2 )

    local unit1 = _unit1 -- Wrapper.Unit#UNIT
    local unit2 = _unit2 -- Wrapper.Unit#UNIT

    if (not unit1) or (not unit2) then
      return false
    end

    -- Check that this is not the same unit.
    if unit1:GetName() == unit2:GetName() then
      return false
    end

    -- Return false when unit2 is not in air? Could be on the carrier.
    if not unit2:InAir() then
      return false
    end

    -- Positions of units.
    local c1 = unit1:GetCoordinate()
    local c2 = unit2:GetCoordinate()

    -- Vector from unit1 to unit2
    local vec12 = { x = c2.x - c1.x, y = 0, z = c2.z - c1.z } -- DCS#Vec3

    -- Distance between units.
    local dist = UTILS.VecNorm( vec12 )

    -- Orientation of unit 1 in space.
    local vec1 = unit1:GetOrientationX()
    vec1.y = 0

    -- Get angle between the two orientation vectors. Does the player aircraft nose point into the direction of the other aircraft? (Could be behind him!)
    local rhdg = math.deg( math.acos( UTILS.VecDot( vec12, vec1 ) / UTILS.VecNorm( vec12 ) / UTILS.VecNorm( vec1 ) ) )

    -- Check altitude difference?
    local dalt = math.abs( c2.y - c1.y )

    -- 650 feet ~= 200 meters distance between flights
    local dcrit = UTILS.FeetToMeters( 650 )

    -- Direction in 30 degrees cone and distance < 200 meters and altitude difference <50
    -- TODO: Test parameter values.
    if math.abs( rhdg ) < 10 and dist < dcrit and dalt < 50 then
      return true
    else
      return false
    end
  end

  -- Loop over all other flights in pattern.
  for _, _flight in pairs( self.Qpattern ) do
    local flight = _flight -- #AIRBOSS.FlightGroup

    -- Now we still need to loop over all units in the flight.
    for _, _element in pairs( flight.elements ) do
      local element = _element -- #AIRBOSS.FlightElement

      -- Check if player is too close to another aircraft in the pattern.
      local tooclose = _checkclose( player.unit, element.unit )

      -- Check if we are too close.
      if tooclose then

        -- Debug message.
        local text = string.format( "Player %s too close (<200 meters) to aircraft %s!", player.name, element.unit:GetName() )
        self:T2( self.lid .. text )
        -- MESSAGE:New(text, 20, "DEBUG"):ToAllIf(self.Debug)

        -- Inform player that he is too close.
        -- TODO: Pattern wave off?
        -- TODO: This function needs a switch so that it is not called over and over again!
        -- local text=string.format("you're getting too close to the aircraft, %s, ahead of you!\nKeep a min distance of at least 650 ft.", element.onboard)
        -- self:MessageToPlayer(player, text, "LSO")
      end

    end
  end

end

--- Check recovery times and start/stop recovery mode of aircraft.
-- @param #AIRBOSS self
function AIRBOSS:_CheckRecoveryTimes()

  -- Get current abs time.
  local time = timer.getAbsTime()
  local Cnow = UTILS.SecondsToClock( time )

  -- Debug output:
  local text = string.format( self.lid .. "Recovery time windows:" )

  -- Handle case with no recoveries.
  if #self.recoverytimes == 0 then
    text = text .. " none!"
  end

  -- Sort windows wrt to start time.
  local _sort = function( a, b )
    return a.START < b.START
  end
  table.sort( self.recoverytimes, _sort )

  -- Next recovery case in the future.
  local nextwindow = nil -- #AIRBOSS.Recovery
  local currwindow = nil -- #AIRBOSS.Recovery

  -- Loop over all slots.
  for _, _recovery in pairs( self.recoverytimes ) do
    local recovery = _recovery -- #AIRBOSS.Recovery

    -- Get start/stop clock strings.
    local Cstart = UTILS.SecondsToClock( recovery.START )
    local Cstop = UTILS.SecondsToClock( recovery.STOP )

    -- Status info.
    local state = ""

    -- Check if start time passed.
    if time >= recovery.START then
      -- Start time has passed.

      if time < recovery.STOP then
        -- Stop time has NOT passed.

        if self:IsRecovering() then
          -- Carrier is already recovering.
          state = "in progress"
        else
          -- Start recovery.
          self:RecoveryStart( recovery.CASE, recovery.OFFSET )
          state = "starting now"
          recovery.OPEN = true
        end

        -- Set current recovery window.
        currwindow = recovery

      else -- Stop time HAS passed.

        if self:IsRecovering() and not recovery.OVER then

          -- Get number of airborne aircraft units(!) currently in pattern.
          local _, npattern = self:_GetQueueInfo( self.Qpattern )

          if npattern > 0 then

            -- Extend recovery time. 5 min per flight.
            local extmin = 5 * npattern
            recovery.STOP = recovery.STOP + extmin * 60

            local text = string.format( "We still got flights in the pattern.\nRecovery time prolonged by %d minutes.\nNow get your act together and no more bolters!", extmin )
            self:MessageToPattern( text, "AIRBOSS", "99", 10, false, nil )

          else

            -- Set carrier to idle.
            self:RecoveryStop()
            state = "closing now"

            -- Closed.
            recovery.OPEN = false

            -- Window just closed.
            recovery.OVER = true

          end
        else

          -- Carrier is already idle.
          state = "closed"
        end

      end

    else
      -- This recovery is in the future.
      state = "in the future"

      -- This is the next to come as we sorted by start time.
      if nextwindow == nil then
        nextwindow = recovery
        state = "next in line"
      end
    end

    -- Debug text.
    text = text .. string.format( "\n- Start=%s Stop=%s Case=%d Offset=%d Open=%s Closed=%s Status=\"%s\"", Cstart, Cstop, recovery.CASE, recovery.OFFSET, tostring( recovery.OPEN ), tostring( recovery.OVER ), state )
  end

  -- Debug output.
  self:T( self.lid .. text )

  -- Current recovery window.
  self.recoverywindow = nil

  if self:IsIdle() then
    -----------------------------------------------------------------------------------------------------------------
    -- Carrier is idle: We need to make sure that incoming flights get the correct recovery info of the next window.
    -----------------------------------------------------------------------------------------------------------------

    -- Check if there is a next windows defined.
    if nextwindow then

      -- Set case and offset of the next window.
      self:RecoveryCase( nextwindow.CASE, nextwindow.OFFSET )

      -- Check if time is less than 5 minutes.
      if nextwindow.WIND and nextwindow.START - time < self.dTturn and not self.turnintowind then

        -- Check that wind is blowing from a direction > 5° different from the current heading.
        local hdg = self:GetHeading()
        local wind = self:GetHeadingIntoWind(nextwindow.SPEED)
        local delta = self:_GetDeltaHeading( hdg, wind )
        local uturn = delta > 5

        -- Check if wind is actually blowing (0.1 m/s = 0.36 km/h = 0.2 knots)
        local _, vwind = self:GetWind()
        if vwind < 0.1 then
          uturn = false
        end

        -- U-turn disabled by user input.
        if not nextwindow.UTURN then
          uturn = false
        end

        -- Debug info
        self:T( self.lid .. string.format( "Heading=%03d°, Wind=%03d° %.1f kts, Delta=%03d° ==> U-turn=%s", hdg, wind, UTILS.MpsToKnots( vwind ), delta, tostring( uturn ) ) )

        -- Time into the wind 1 day or if longer recovery time + the 5 min early.
        local t = math.max( nextwindow.STOP - nextwindow.START + self.dTturn, 60 * 60 * 24 )

        -- Recovery wind on deck in knots.
        local v = UTILS.KnotsToMps( nextwindow.SPEED )

        -- Check that we do not go above max possible speed.
        local vmax = self.carrier:GetSpeedMax() / 3.6 -- convert to m/s
        v = math.min( v, vmax )

        -- Route carrier into the wind. Sets self.turnintowind=true
        self:CarrierTurnIntoWind( t, v, uturn )

      end

      -- Set current recovery window.
      self.recoverywindow = nextwindow

    else
      -- No next window. Set default values.
      self:RecoveryCase()
    end

  else
    -------------------------------------------------------------------------------------
    -- Carrier is recovering: We set the recovery window to the current one or next one.
    -------------------------------------------------------------------------------------

    if currwindow then
      self.recoverywindow = currwindow
    else
      self.recoverywindow = nextwindow
    end
  end

  self:T2( { "FF", recoverywindow = self.recoverywindow } )
end

--- Get section lead of a flight.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight
-- @return #AIRBOSS.FlightGroup The leader of the section. Could be the flight itself.
-- @return #boolean If true, flight is lead.
function AIRBOSS:_GetFlightLead( flight )

  if flight.name ~= flight.seclead then
    -- Section lead of flight.
    local lead = self.players[flight.seclead]
    return lead, false
  else
    -- Flight without section or section lead.
    return flight, true
  end

end

--- On before "RecoveryCase" event. Check if case or holding offset did change. If not transition is denied.
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number Case The recovery case (1, 2 or 3) to switch to.
-- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries.
function AIRBOSS:onbeforeRecoveryCase( From, Event, To, Case, Offset )

  -- Input or default value.
  Case = Case or self.defaultcase

  -- Input or default value
  Offset = Offset or self.defaultoffset

  if Case == self.case and Offset == self.holdingoffset then
    return false
  end

  return true
end

--- On after "RecoveryCase" event. Sets new aircraft recovery case. Updates
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number Case The recovery case (1, 2 or 3) to switch to.
-- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries.
function AIRBOSS:onafterRecoveryCase( From, Event, To, Case, Offset )

  -- Input or default value.
  Case = Case or self.defaultcase

  -- Input or default value
  Offset = Offset or self.defaultoffset

  -- Debug output.
  local text = string.format( "Switching recovery case %d ==> %d", self.case, Case )
  if Case > 1 then
    text = text .. string.format( " Holding offset angle %d degrees.", Offset )
  end
  MESSAGE:New( text, 20, self.alias ):ToAllIf( self.Debug )
  self:T( self.lid .. text )

  -- Set new recovery case.
  self.case = Case

  -- Set holding offset.
  self.holdingoffset = Offset

  -- Update case of all flights not in Marshal or Pattern queue.
  for _, _flight in pairs( self.flights ) do
    local flight = _flight -- #AIRBOSS.FlightGroup
    if not (self:_InQueue( self.Qmarshal, flight.group ) or self:_InQueue( self.Qpattern, flight.group )) then

      -- Also not for section members. These are not in the marshal or pattern queue if the lead is.
      if flight.name ~= flight.seclead then
        local lead = self.players[flight.seclead]

        if lead and not (self:_InQueue( self.Qmarshal, lead.group ) or self:_InQueue( self.Qpattern, lead.group )) then
          -- This is section member and the lead is not in the Marshal or Pattern queue.
          flight.case = self.case
        end

      else

        -- This is a flight without section or the section lead.
        flight.case = self.case

      end

    end
  end
end

--- On after "RecoveryStart" event. Recovery of aircraft is started and carrier switches to state "Recovering".
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number Case The recovery case (1, 2 or 3) to start.
-- @param #number Offset Holding pattern offset angle in degrees for CASE II/III recoveries.
function AIRBOSS:onafterRecoveryStart( From, Event, To, Case, Offset )

  -- Input or default value.
  Case = Case or self.defaultcase

  -- Input or default value.
  Offset = Offset or self.defaultoffset

  -- Radio message: "99, starting aircraft recovery case X ops. (Marshal radial XYZ degrees)"
  self:_MarshalCallRecoveryStart( Case )

  -- Switch to case.
  self:RecoveryCase( Case, Offset )
end

--- On after "RecoveryStop" event. Recovery of aircraft is stopped and carrier switches to state "Idle". Running recovery window is deleted.
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AIRBOSS:onafterRecoveryStop( From, Event, To )
  -- Debug output.
  self:T( self.lid .. string.format( "Stopping aircraft recovery." ) )

  -- Recovery ops stopped message.
  self:_MarshalCallRecoveryStopped( self.case )

  -- If carrier is currently heading into the wind, we resume the original route.
  if self.turnintowind then

    -- Coordinate to return to.
    local coord = self.Creturnto

    -- No U-turn.
    if self.recoverywindow and self.recoverywindow.UTURN == false then
      coord = nil
    end

    -- Carrier resumes route.
    self:CarrierResumeRoute( coord )
  end

  -- Delete current recovery window if open.
  if self.recoverywindow and self.recoverywindow.OPEN == true then
    self.recoverywindow.OPEN = false
    self.recoverywindow.OVER = true
    self:DeleteRecoveryWindow( self.recoverywindow )
  end

  -- Check recovery windows. This sets self.recoverywindow to the next window.
  self:_CheckRecoveryTimes()
end

--- On after "RecoveryPause" event. Recovery of aircraft is paused. Marshal queue stays intact.
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number duration Duration of pause in seconds. After that recovery is resumed automatically.
function AIRBOSS:onafterRecoveryPause( From, Event, To, duration )
  -- Debug output.
  self:T( self.lid .. string.format( "Pausing aircraft recovery." ) )

  -- Message text

  if duration then

    -- Auto resume.
    self:__RecoveryUnpause( duration )

    -- Time to resume.
    local clock = UTILS.SecondsToClock( timer.getAbsTime() + duration )

    -- Marshal call: "99, aircraft recovery paused and will be resume at XX:YY."
    self:_MarshalCallRecoveryPausedResumedAt( clock )
  else

    local text = string.format( "aircraft recovery is paused until further notice." )

    -- Marshal call: "99, aircraft recovery paused until further notice."
    self:_MarshalCallRecoveryPausedNotice()

  end

end

--- On after "RecoveryUnpause" event. Recovery of aircraft is resumed.
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AIRBOSS:onafterRecoveryUnpause( From, Event, To )
  -- Debug output.
  self:T( self.lid .. string.format( "Unpausing aircraft recovery." ) )

  -- Resume recovery.
  self:_MarshalCallResumeRecovery()

end

--- On after "PassingWaypoint" event. Carrier has just passed a waypoint
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number n Number of waypoint that was passed.
function AIRBOSS:onafterPassingWaypoint( From, Event, To, n )
  -- Debug output.
  self:I( self.lid .. string.format( "Carrier passed waypoint %d.", n ) )
end

--- On after "Idle" event. Carrier goes to state "Idle".
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AIRBOSS:onafterIdle( From, Event, To )
  -- Debug output.
  self:T( self.lid .. string.format( "Carrier goes to idle." ) )
end

--- On after Stop event. Unhandle events.
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AIRBOSS:onafterStop( From, Event, To )
  self:I( self.lid .. string.format( "Stopping airboss script." ) )

  -- Unhandle events.
  self:UnHandleEvent( EVENTS.Birth )
  self:UnHandleEvent( EVENTS.RunwayTouch )
  self:UnHandleEvent( EVENTS.EngineShutdown )
  self:UnHandleEvent( EVENTS.Takeoff )
  self:UnHandleEvent( EVENTS.Crash )
  self:UnHandleEvent( EVENTS.Ejection )
  self:UnHandleEvent( EVENTS.PlayerLeaveUnit )
  self:UnHandleEvent( EVENTS.MissionEnd )

  self.CallScheduler:Clear()
end

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Parameter initialization
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Init parameters for USS Stennis carrier.
-- @param #AIRBOSS self
function AIRBOSS:_InitStennis()

  -- Carrier Parameters.
  self.carrierparam.sterndist = -153
  self.carrierparam.deckheight = 18.30

  -- Total size of the carrier (approx as rectangle).
  self.carrierparam.totlength = 310 -- Wiki says 332.8 meters overall length.
  self.carrierparam.totwidthport = 40 -- Wiki says  76.8 meters overall beam.
  self.carrierparam.totwidthstarboard = 30

  -- Landing runway.
  self.carrierparam.rwyangle = -9.1359
  self.carrierparam.rwylength = 225
  self.carrierparam.rwywidth = 20

  -- Wires.
  self.carrierparam.wire1 = 46 -- Distance from stern to first wire.
  self.carrierparam.wire2 = 46 + 12
  self.carrierparam.wire3 = 46 + 24
  self.carrierparam.wire4 = 46 + 35 -- Last wire is strangely one meter closer.

  -- Landing distance.
  self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.wire3

  -- Platform at 5k. Reduce descent rate to 2000 ft/min to 1200 dirty up level flight.
  self.Platform.name = "Platform 5k"
  self.Platform.Xmin = -UTILS.NMToMeters( 22 ) -- Not more than 22 NM behind the boat. Last check was at 21 NM.
  self.Platform.Xmax = nil
  self.Platform.Zmin = -UTILS.NMToMeters( 30 ) -- Not more than 30 NM port of boat.
  self.Platform.Zmax = UTILS.NMToMeters( 30 ) -- Not more than 30 NM starboard of boat.
  self.Platform.LimitXmin = nil -- Limits via zone
  self.Platform.LimitXmax = nil
  self.Platform.LimitZmin = nil
  self.Platform.LimitZmax = nil

  -- Level out at 1200 ft and dirty up.
  self.DirtyUp.name = "Dirty Up"
  self.DirtyUp.Xmin = -UTILS.NMToMeters( 21 ) -- Not more than 21 NM behind the boat.
  self.DirtyUp.Xmax = nil
  self.DirtyUp.Zmin = -UTILS.NMToMeters( 30 ) -- Not more than 30 NM port of boat.
  self.DirtyUp.Zmax = UTILS.NMToMeters( 30 ) -- Not more than 30 NM starboard of boat.
  self.DirtyUp.LimitXmin = nil -- Limits via zone
  self.DirtyUp.LimitXmax = nil
  self.DirtyUp.LimitZmin = nil
  self.DirtyUp.LimitZmax = nil

  -- Intercept glide slope and follow bullseye.
  self.Bullseye.name = "Bullseye"
  self.Bullseye.Xmin = -UTILS.NMToMeters( 11 ) -- Not more than 11 NM behind the boat. Last check was at 10 NM.
  self.Bullseye.Xmax = nil
  self.Bullseye.Zmin = -UTILS.NMToMeters( 30 ) -- Not more than 30 NM port.
  self.Bullseye.Zmax = UTILS.NMToMeters( 30 ) -- Not more than 30 NM starboard.
  self.Bullseye.LimitXmin = nil -- Limits via zone.
  self.Bullseye.LimitXmax = nil
  self.Bullseye.LimitZmin = nil
  self.Bullseye.LimitZmax = nil

  -- Break entry.
  self.BreakEntry.name = "Break Entry"
  self.BreakEntry.Xmin = -UTILS.NMToMeters( 4 ) -- Not more than 4 NM behind the boat. Check for initial is at 3 NM with a radius of 500 m and 100 m starboard.
  self.BreakEntry.Xmax = nil
  self.BreakEntry.Zmin = -UTILS.NMToMeters( 0.5 ) -- Not more than 0.5 NM port of boat.
  self.BreakEntry.Zmax = UTILS.NMToMeters( 1.5 ) -- Not more than 1.5 NM starboard.
  self.BreakEntry.LimitXmin = 0 -- Check and next step when at carrier and starboard of carrier.
  self.BreakEntry.LimitXmax = nil
  self.BreakEntry.LimitZmin = nil
  self.BreakEntry.LimitZmax = nil

  -- Early break.
  self.BreakEarly.name = "Early Break"
  self.BreakEarly.Xmin = -UTILS.NMToMeters( 1 ) -- Not more than 1 NM behind the boat. Last check was at 0.
  self.BreakEarly.Xmax = UTILS.NMToMeters( 7 ) -- Not more than 5 NM in front of the boat. Enough for late breaks? -- VNAO EDIT - original value 5
  self.BreakEarly.Zmin = -UTILS.NMToMeters( 2 ) -- Not more than 2 NM port.
  self.BreakEarly.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard.
  self.BreakEarly.LimitXmin = 0 -- Check and next step 0.2 NM port and in front of boat.
  self.BreakEarly.LimitXmax = nil
  self.BreakEarly.LimitZmin = -UTILS.NMToMeters( 0.2 ) -- -370 m port
  self.BreakEarly.LimitZmax = nil

  -- Late break.
  self.BreakLate.name = "Late Break"
  self.BreakLate.Xmin = -UTILS.NMToMeters( 1 ) -- Not more than 1 NM behind the boat. Last check was at 0.
  self.BreakLate.Xmax = UTILS.NMToMeters( 7 ) -- Not more than 5 NM in front of the boat. Enough for late breaks? -- VNAO EDIT - original value 5
  self.BreakLate.Zmin = -UTILS.NMToMeters( 2 ) -- Not more than 2 NM port.
  self.BreakLate.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard.
  self.BreakLate.LimitXmin = 0 -- Check and next step 0.8 NM port and in front of boat.
  self.BreakLate.LimitXmax = nil
  self.BreakLate.LimitZmin = -UTILS.NMToMeters( 0.8 ) -- -1470 m port
  self.BreakLate.LimitZmax = nil

  -- Abeam position.
  self.Abeam.name = "Abeam Position"
  self.Abeam.Xmin = -UTILS.NMToMeters( 5 ) -- Not more then 5 NM astern of boat. Should be LIG call anyway.
    self.Abeam.Xmax = UTILS.NMToMeters( 7 ) -- Not more then 5 NM ahead of boat.    --VNAO EDIT  - original value 5
  self.Abeam.Zmin = -UTILS.NMToMeters( 2 ) -- Not more than 2 NM port.
  self.Abeam.Zmax = 500 -- Not more than 500 m starboard. Must be port!
  self.Abeam.LimitXmin = -200 -- Check and next step 200 meters behind the ship.
  self.Abeam.LimitXmax = nil
  self.Abeam.LimitZmin = nil
  self.Abeam.LimitZmax = nil

  -- At the Ninety.
  self.Ninety.name = "Ninety"
  self.Ninety.Xmin = -UTILS.NMToMeters( 4 ) -- Not more than 4 NM behind the boat. LIG check anyway.
  self.Ninety.Xmax = 0 -- Must be behind the boat.
  self.Ninety.Zmin = -UTILS.NMToMeters( 2 ) -- Not more than 2 NM port of boat.
  self.Ninety.Zmax = nil
  self.Ninety.LimitXmin = nil
  self.Ninety.LimitXmax = nil
  self.Ninety.LimitZmin = nil
  self.Ninety.LimitZmax = -UTILS.NMToMeters( 0.6 ) -- Check and next step when 0.6 NM port.

  -- At the Wake.
  self.Wake.name = "Wake"
  self.Wake.Xmin = -UTILS.NMToMeters( 4 ) -- Not more than 4 NM behind the boat.
  self.Wake.Xmax = 0 -- Must be behind the boat.
  self.Wake.Zmin = -2000 -- Not more than 2 km port of boat.
  self.Wake.Zmax = nil
  self.Wake.LimitXmin = nil
  self.Wake.LimitXmax = nil
  self.Wake.LimitZmin = 0 -- Check and next step when directly behind the boat.
  self.Wake.LimitZmax = nil

  -- Turn to final.
  self.Final.name = "Final"
  self.Final.Xmin = -UTILS.NMToMeters( 4 ) -- Not more than 4 NM behind the boat.
  self.Final.Xmax = 0 -- Must be behind the boat.
  self.Final.Zmin = -2000 -- Not more than 2 km port.
  self.Final.Zmax = nil
  self.Final.LimitXmin = nil -- No limits. Check is carried out differently.
  self.Final.LimitXmax = nil
  self.Final.LimitZmin = nil
  self.Final.LimitZmax = nil

  -- In the Groove.
  self.Groove.name = "Groove"
  self.Groove.Xmin = -UTILS.NMToMeters( 4 ) -- Not more than 4 NM behind the boat.
  self.Groove.Xmax = nil
  self.Groove.Zmin = -UTILS.NMToMeters( 2 ) -- Not more than 2 NM port
  self.Groove.Zmax = UTILS.NMToMeters( 2 ) -- Not more than 2 NM starboard.
  self.Groove.LimitXmin = nil -- No limits. Check is carried out differently.
  self.Groove.LimitXmax = nil
  self.Groove.LimitZmin = nil
  self.Groove.LimitZmax = nil

end

--- Init parameters for Nimitz class super carriers.
-- @param #AIRBOSS self
function AIRBOSS:_InitNimitz()

  -- Init Stennis as default.
  self:_InitStennis()

  -- Carrier Parameters.
  self.carrierparam.sterndist = -164
  self.carrierparam.deckheight = 20.1494 -- DCS World OpenBeta\CoreMods\tech\USS_Nimitz\Database\USS_CVN_7X.lua

  -- Total size of the carrier (approx as rectangle).
  self.carrierparam.totlength = 332.8 -- Wiki says 332.8 meters overall length.
  self.carrierparam.totwidthport = 45 -- Wiki says  76.8 meters overall beam.
  self.carrierparam.totwidthstarboard = 35

  -- Landing runway.
  self.carrierparam.rwyangle = -9.1359 -- DCS World OpenBeta\CoreMods\tech\USS_Nimitz\scripts\USS_Nimitz_RunwaysAndRoutes.lua
  self.carrierparam.rwylength = 250
  self.carrierparam.rwywidth = 25

  -- Wires.
  self.carrierparam.wire1 = 55 -- Distance from stern to first wire.
  self.carrierparam.wire2 = 67
  self.carrierparam.wire3 = 79
  self.carrierparam.wire4 = 96 -- VNAO Edit - original value was 92

  -- Landing distance.
  self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.wire3

end

--- Init parameters for Forrestal class super carriers.
-- @param #AIRBOSS self
function AIRBOSS:_InitForrestal()

  -- Init Nimitz as default.
  self:_InitNimitz()

  -- Carrier Parameters.
  self.carrierparam.sterndist = -135.5
  self.carrierparam.deckheight = 20 -- 20.1494  --DCS World OpenBeta\CoreMods\tech\USS_Nimitz\Database\USS_CVN_7X.lua

  -- Total size of the carrier (approx as rectangle).
  self.carrierparam.totlength = 315 -- Wiki says 325 meters overall length.
  self.carrierparam.totwidthport = 45 -- Wiki says  73 meters overall beam.
  self.carrierparam.totwidthstarboard = 35

  -- Landing runway.
  self.carrierparam.rwyangle = -9.1359 -- DCS World OpenBeta\CoreMods\tech\USS_Nimitz\scripts\USS_Nimitz_RunwaysAndRoutes.lua
  self.carrierparam.rwylength = 212
  self.carrierparam.rwywidth = 25

  -- Wires.
  self.carrierparam.wire1 = 44 -- Distance from stern to first wire. Original from Frank - 42
  self.carrierparam.wire2 = 54 -- 51.5
  self.carrierparam.wire3 = 64 -- 62
  self.carrierparam.wire4 = 74 -- 72.5

  -- Landing distance.
  self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.wire3

end

--- Init parameters for Enterprise carrier.
-- @param #AIRBOSS self
function AIRBOSS:_InitEnterprise()
  -- Using Forrestal as template
  self:_InitForrestal()

  self.carrierparam.sterndist = -164.30
  self.carrierparam.deckheight = 19.52

  self.carrierparam.totlength = 335
  self.carrierparam.rwylength = 223

  -- Wires.
  self.carrierparam.wire1 = 57.7
  self.carrierparam.wire2 = 69.6
  self.carrierparam.wire3 = 79.5
  self.carrierparam.wire4 = 90.0

end

--- Init parameters for Essec class carriers.
-- @param #AIRBOSS self
function AIRBOSS:_InitEssex()

  -- Init Nimitz as default.
  self:_InitNimitz()

  -- Carrier Parameters.
  self.carrierparam.sterndist = -126
  self.carrierparam.deckheight = 19.27 --DCS World\CoreMods\tech\M3 WWII PTO units\Database\Essex_Class_Carrier_1944.lua

  -- Total size of the carrier (approx as rectangle).
  self.carrierparam.totlength = 268
  self.carrierparam.totwidthport = 23
  self.carrierparam.totwidthstarboard = 23

  -- Landing runway.
  self.carrierparam.rwyangle = 0.0
  self.carrierparam.rwylength = 265
  self.carrierparam.rwywidth = 20

  -- Wires.
  self.carrierparam.wire1  = 21.9
  self.carrierparam.wire2  = 28.3
  self.carrierparam.wire3  = 34.7
  self.carrierparam.wire4  = 41.1
  self.carrierparam.wire5  = 47.4
  self.carrierparam.wire6  = 53.7
  self.carrierparam.wire7  = 59.0

  self.carrierparam.wire8  = 64.1
  self.carrierparam.wire9  = 72.7
  self.carrierparam.wire10 = 78.0
  self.carrierparam.wire11 = 85.5

  self.carrierparam.wire12 = 105.9
  self.carrierparam.wire13 = 113.3
  self.carrierparam.wire14 = 121.0
  self.carrierparam.wire15 = 128.5

  -- Landing distance.
  self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.wire3

end

--- Init parameters for CVA-31 Bon Homme Richard carriers.
-- @param #AIRBOSS self
function AIRBOSS:_InitBonHommeRichard()
  -- Init Essex as default
  self:_InitEssex()

  self.carrierparam.deckheight = 16.95

  -- Landing runway.
  -- from BHR EssexRunwayAndRoutes.lua
  self.carrierparam.rwyangle = -11.4
  self.carrierparam.rwylength = 97
  self.carrierparam.rwywidth = 20

  -- Wires.
  self.carrierparam.wire1 = 40.4 -- Distance from stern to first wire. Original from Frank - 42
  self.carrierparam.wire2 = 45
  self.carrierparam.wire3 = 51
  self.carrierparam.wire4 = 58.1
end

--- Init parameters for Generic Essex SC125 class carriers.
-- @param #AIRBOSS self
function AIRBOSS:_InitEssexSCB125()
  -- Init Bon Homme Richard as default
  self:_InitBonHommeRichard()

end

--- Init parameters for R12 HMS Hermes carrier.
-- @param #AIRBOSS self
function AIRBOSS:_InitHermes()

  -- Init Stennis as default.
  self:_InitStennis()

  -- Carrier Parameters.
  self.carrierparam.sterndist = -105
  self.carrierparam.deckheight = 12 -- From model viewer WL0.

  -- Total size of the carrier (approx as rectangle).
  self.carrierparam.totlength = 228.19
  self.carrierparam.totwidthport = 20.5
  self.carrierparam.totwidthstarboard = 24.5

  -- Landing runway.
  self.carrierparam.rwyangle = 0
  self.carrierparam.rwylength = 215
  self.carrierparam.rwywidth = 13

  -- Wires.
  self.carrierparam.wire1 = nil
  self.carrierparam.wire2 = nil
  self.carrierparam.wire3 = nil
  self.carrierparam.wire4 = nil

  -- Distance to landing spot.
  self.carrierparam.landingspot=69

  -- Landing distance.
  self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.landingspot

  -- Late break.
  self.BreakLate.name = "Late Break"
  self.BreakLate.Xmin = -UTILS.NMToMeters( 1 ) -- Not more than 1 NM behind the boat. Last check was at 0.
  self.BreakLate.Xmax = UTILS.NMToMeters( 5 ) -- Not more than 5 NM in front of the boat. Enough for late breaks?
  self.BreakLate.Zmin = -UTILS.NMToMeters( 1.6 ) -- Not more than 1.6 NM port.
  self.BreakLate.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard.
  self.BreakLate.LimitXmin = 0 -- Check and next step 0.8 NM port and in front of boat.
  self.BreakLate.LimitXmax = nil
  self.BreakLate.LimitZmin = -UTILS.NMToMeters( 0.5 ) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2
  self.BreakLate.LimitZmax = nil

end

--- Init parameters for R05 HMS Invincible carrier.
-- @param #AIRBOSS self
function AIRBOSS:_InitInvincible()

  -- Init Stennis as default.
  self:_InitStennis()

  -- Carrier Parameters.
  self.carrierparam.sterndist = -105
  self.carrierparam.deckheight = 12 -- From model viewer WL0.

  -- Total size of the carrier (approx as rectangle).
  self.carrierparam.totlength = 228.19
  self.carrierparam.totwidthport = 20.5
  self.carrierparam.totwidthstarboard = 24.5

  -- Landing runway.
  self.carrierparam.rwyangle = 0
  self.carrierparam.rwylength = 215
  self.carrierparam.rwywidth = 13

  -- Wires.
  self.carrierparam.wire1 = nil
  self.carrierparam.wire2 = nil
  self.carrierparam.wire3 = nil
  self.carrierparam.wire4 = nil

  -- Distance to landing spot.
  self.carrierparam.landingspot=69

  -- Landing distance.
  self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.landingspot

  -- Late break.
  self.BreakLate.name = "Late Break"
  self.BreakLate.Xmin = -UTILS.NMToMeters( 1 ) -- Not more than 1 NM behind the boat. Last check was at 0.
  self.BreakLate.Xmax = UTILS.NMToMeters( 5 ) -- Not more than 5 NM in front of the boat. Enough for late breaks?
  self.BreakLate.Zmin = -UTILS.NMToMeters( 1.6 ) -- Not more than 1.6 NM port.
  self.BreakLate.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard.
  self.BreakLate.LimitXmin = 0 -- Check and next step 0.8 NM port and in front of boat.
  self.BreakLate.LimitXmax = nil
  self.BreakLate.LimitZmin = -UTILS.NMToMeters( 0.5 ) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2
  self.BreakLate.LimitZmax = nil

end

--- Init parameters for LHA-1 Tarawa carrier.
-- @param #AIRBOSS self
function AIRBOSS:_InitTarawa()

  -- Init Stennis as default.
  self:_InitStennis()

  -- Carrier Parameters.
  self.carrierparam.sterndist = -125
  self.carrierparam.deckheight = 21 -- 69 ft

  -- Total size of the carrier (approx as rectangle).
  self.carrierparam.totlength = 245
  self.carrierparam.totwidthport = 10
  self.carrierparam.totwidthstarboard = 25

  -- Landing runway.
  self.carrierparam.rwyangle = 0
  self.carrierparam.rwylength = 225
  self.carrierparam.rwywidth = 15

  -- Wires.
  self.carrierparam.wire1 = nil
  self.carrierparam.wire2 = nil
  self.carrierparam.wire3 = nil
  self.carrierparam.wire4 = nil

  -- Distance to landing spot.
  self.carrierparam.landingspot=57

  -- Landing distance.
  self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.landingspot

  -- Late break.
  self.BreakLate.name = "Late Break"
  self.BreakLate.Xmin = -UTILS.NMToMeters( 1 ) -- Not more than 1 NM behind the boat. Last check was at 0.
  self.BreakLate.Xmax = UTILS.NMToMeters( 5 ) -- Not more than 5 NM in front of the boat. Enough for late breaks?
  self.BreakLate.Zmin = -UTILS.NMToMeters( 1.6 ) -- Not more than 1.6 NM port.
  self.BreakLate.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard.
  self.BreakLate.LimitXmin = 0 -- Check and next step 0.8 NM port and in front of boat.
  self.BreakLate.LimitXmax = nil
  self.BreakLate.LimitZmin = -UTILS.NMToMeters( 0.5 ) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2
  self.BreakLate.LimitZmax = nil

end

--- Init parameters for LHA-6 America carrier.
-- @param #AIRBOSS self
function AIRBOSS:_InitAmerica()

  -- Init Stennis as default.
  self:_InitStennis()

  -- Carrier Parameters.
  self.carrierparam.sterndist = -125
  self.carrierparam.deckheight = 20 -- 67 ft

  -- Total size of the carrier (approx as rectangle).
  self.carrierparam.totlength = 257
  self.carrierparam.totwidthport = 11
  self.carrierparam.totwidthstarboard = 25

  -- Landing runway.
  self.carrierparam.rwyangle = 0
  self.carrierparam.rwylength = 240
  self.carrierparam.rwywidth = 15

  -- Wires.
  self.carrierparam.wire1 = nil
  self.carrierparam.wire2 = nil
  self.carrierparam.wire3 = nil
  self.carrierparam.wire4 = nil

  -- Distance to landing spot.
  self.carrierparam.landingspot=59

  -- Landing distance.
  self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.landingspot

  -- Late break.
  self.BreakLate.name = "Late Break"
  self.BreakLate.Xmin = -UTILS.NMToMeters( 1 ) -- Not more than 1 NM behind the boat. Last check was at 0.
  self.BreakLate.Xmax = UTILS.NMToMeters( 5 ) -- Not more than 5 NM in front of the boat. Enough for late breaks?
  self.BreakLate.Zmin = -UTILS.NMToMeters( 1.6 ) -- Not more than 1.6 NM port.
  self.BreakLate.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard.
  self.BreakLate.LimitXmin = 0 -- Check and next step 0.8 NM port and in front of boat.
  self.BreakLate.LimitXmax = nil
  self.BreakLate.LimitZmin = -UTILS.NMToMeters( 0.5 ) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2
  self.BreakLate.LimitZmax = nil

end

--- Init parameters for L61 Juan Carlos carrier.
-- @param #AIRBOSS self
function AIRBOSS:_InitJcarlos()

  -- Init Stennis as default.
  self:_InitStennis()

  -- Carrier Parameters.
  self.carrierparam.sterndist = -125
  self.carrierparam.deckheight = 20 -- 67 ft

  -- Total size of the carrier (approx as rectangle).
  self.carrierparam.totlength = 231
  self.carrierparam.totwidthport = 10
  self.carrierparam.totwidthstarboard = 22

  -- Landing runway.
  self.carrierparam.rwyangle = 0
  self.carrierparam.rwylength = 202
  self.carrierparam.rwywidth = 14

  -- Wires.
  self.carrierparam.wire1 = nil
  self.carrierparam.wire2 = nil
  self.carrierparam.wire3 = nil
  self.carrierparam.wire4 = nil

  -- Distance to landing spot.
  self.carrierparam.landingspot=89

  -- Landing distance.
  self.carrierparam.landingdist = self.carrierparam.sterndist+self.carrierparam.landingspot

  -- Late break.
  self.BreakLate.name = "Late Break"
  self.BreakLate.Xmin = -UTILS.NMToMeters( 1 ) -- Not more than 1 NM behind the boat. Last check was at 0.
  self.BreakLate.Xmax = UTILS.NMToMeters( 5 ) -- Not more than 5 NM in front of the boat. Enough for late breaks?
  self.BreakLate.Zmin = -UTILS.NMToMeters( 1.6 ) -- Not more than 1.6 NM port.
  self.BreakLate.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard.
  self.BreakLate.LimitXmin = 0 -- Check and next step 0.8 NM port and in front of boat.
  self.BreakLate.LimitXmax = nil
  self.BreakLate.LimitZmin = -UTILS.NMToMeters( 0.5 ) -- 926 m port, closer than the stennis as abeam is 0.8-1.0 rather than 1.2
  self.BreakLate.LimitZmax = nil

end

--- Init parameters for L02 Canberra carrier.
-- @param #AIRBOSS self
function AIRBOSS:_InitCanberra()

    -- Init Juan Carlos as default.
    self:_InitJcarlos()

end

--- Init parameters for Marshal Voice overs *Gabriella* by HighwaymanEd.
-- @param #AIRBOSS self
-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located.
function AIRBOSS:SetVoiceOversMarshalByGabriella( mizfolder )

  -- Set sound files folder.
  if mizfolder then
    local lastchar = string.sub( mizfolder, -1 )
    if lastchar ~= "/" then
      mizfolder = mizfolder .. "/"
    end
    self.soundfolderMSH = mizfolder
  else
    -- Default is the general folder.
    self.soundfolderMSH = self.soundfolder
  end

  -- Report for duty.
  self:I( self.lid .. string.format( "Marshal Gabriella reporting for duty! Soundfolder=%s", tostring( self.soundfolderMSH ) ) )

  self.MarshalCall.AFFIRMATIVE.duration = 0.65
  self.MarshalCall.ALTIMETER.duration = 0.60
  self.MarshalCall.BRC.duration = 0.67
  self.MarshalCall.CARRIERTURNTOHEADING.duration = 1.62
  self.MarshalCall.CASE.duration = 0.30
  self.MarshalCall.CHARLIETIME.duration = 0.77
  self.MarshalCall.CLEAREDFORRECOVERY.duration = 0.93
  self.MarshalCall.DECKCLOSED.duration = 0.73
  self.MarshalCall.DEGREES.duration = 0.48
  self.MarshalCall.EXPECTED.duration = 0.50
  self.MarshalCall.FLYNEEDLES.duration = 0.89
  self.MarshalCall.HOLDATANGELS.duration = 0.81
  self.MarshalCall.HOURS.duration = 0.41
  self.MarshalCall.MARSHALRADIAL.duration = 0.95
  self.MarshalCall.N0.duration = 0.41
  self.MarshalCall.N1.duration = 0.30
  self.MarshalCall.N2.duration = 0.34
  self.MarshalCall.N3.duration = 0.31
  self.MarshalCall.N4.duration = 0.34
  self.MarshalCall.N5.duration = 0.30
  self.MarshalCall.N6.duration = 0.33
  self.MarshalCall.N7.duration = 0.38
  self.MarshalCall.N8.duration = 0.35
  self.MarshalCall.N9.duration = 0.35
  self.MarshalCall.NEGATIVE.duration = 0.60
  self.MarshalCall.NEWFB.duration = 0.95
  self.MarshalCall.OPS.duration = 0.23
  self.MarshalCall.POINT.duration = 0.38
  self.MarshalCall.RADIOCHECK.duration = 1.27
  self.MarshalCall.RECOVERY.duration = 0.60
  self.MarshalCall.RECOVERYOPSSTOPPED.duration = 1.25
  self.MarshalCall.RECOVERYPAUSEDNOTICE.duration = 2.55
  self.MarshalCall.RECOVERYPAUSEDRESUMED.duration = 2.55
  self.MarshalCall.REPORTSEEME.duration = 0.87
  self.MarshalCall.RESUMERECOVERY.duration = 1.55
  self.MarshalCall.ROGER.duration = 0.50
  self.MarshalCall.SAYNEEDLES.duration = 0.82
  self.MarshalCall.STACKFULL.duration = 5.70
  self.MarshalCall.STARTINGRECOVERY.duration = 1.61

end

--- Init parameters for Marshal Voice overs by *Raynor*.
-- @param #AIRBOSS self
-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located.
function AIRBOSS:SetVoiceOversMarshalByRaynor( mizfolder )

  -- Set sound files folder.
  if mizfolder then
    local lastchar = string.sub( mizfolder, -1 )
    if lastchar ~= "/" then
      mizfolder = mizfolder .. "/"
    end
    self.soundfolderMSH = mizfolder
  else
    -- Default is the general folder.
    self.soundfolderMSH = self.soundfolder
  end

  -- Report for duty.
  self:I( self.lid .. string.format( "Marshal Raynor reporting for duty! Soundfolder=%s", tostring( self.soundfolderMSH ) ) )

  self.MarshalCall.AFFIRMATIVE.duration = 0.70
  self.MarshalCall.ALTIMETER.duration = 0.60
  self.MarshalCall.BRC.duration = 0.60
  self.MarshalCall.CARRIERTURNTOHEADING.duration = 1.87
  self.MarshalCall.CASE.duration = 0.60
  self.MarshalCall.CHARLIETIME.duration = 0.81
  self.MarshalCall.CLEAREDFORRECOVERY.duration = 1.21
  self.MarshalCall.DECKCLOSED.duration = 0.86
  self.MarshalCall.DEGREES.duration = 0.55
  self.MarshalCall.EXPECTED.duration = 0.61
  self.MarshalCall.FLYNEEDLES.duration = 0.90
  self.MarshalCall.HOLDATANGELS.duration = 0.91
  self.MarshalCall.HOURS.duration = 0.54
  self.MarshalCall.MARSHALRADIAL.duration = 0.80
  self.MarshalCall.N0.duration = 0.38
  self.MarshalCall.N1.duration = 0.30
  self.MarshalCall.N2.duration = 0.30
  self.MarshalCall.N3.duration = 0.30
  self.MarshalCall.N4.duration = 0.32
  self.MarshalCall.N5.duration = 0.41
  self.MarshalCall.N6.duration = 0.48
  self.MarshalCall.N7.duration = 0.51
  self.MarshalCall.N8.duration = 0.38
  self.MarshalCall.N9.duration = 0.34
  self.MarshalCall.NEGATIVE.duration = 0.60
  self.MarshalCall.NEWFB.duration = 1.10
  self.MarshalCall.OPS.duration = 0.46
  self.MarshalCall.POINT.duration = 0.21
  self.MarshalCall.RADIOCHECK.duration = 0.95
  self.MarshalCall.RECOVERY.duration = 0.63
  self.MarshalCall.RECOVERYOPSSTOPPED.duration = 1.36
  self.MarshalCall.RECOVERYPAUSEDNOTICE.duration = 2.8 -- Strangely the file is actually a shorter ~2.4 sec.
  self.MarshalCall.RECOVERYPAUSEDRESUMED.duration = 2.75
  self.MarshalCall.REPORTSEEME.duration = 1.06 -- 0.96
  self.MarshalCall.RESUMERECOVERY.duration = 1.41
  self.MarshalCall.ROGER.duration = 0.41
  self.MarshalCall.SAYNEEDLES.duration = 0.79
  self.MarshalCall.STACKFULL.duration = 4.70
  self.MarshalCall.STARTINGRECOVERY.duration = 2.06

end

--- Set parameters for LSO Voice overs by *Raynor*.
-- @param #AIRBOSS self
-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located.
function AIRBOSS:SetVoiceOversLSOByRaynor( mizfolder )

  -- Set sound files folder.
  if mizfolder then
    local lastchar = string.sub( mizfolder, -1 )
    if lastchar ~= "/" then
      mizfolder = mizfolder .. "/"
    end
    self.soundfolderLSO = mizfolder
  else
    -- Default is the general folder.
    self.soundfolderLSO = self.soundfolder
  end

  -- Report for duty.
  self:I( self.lid .. string.format( "LSO Raynor reporting for duty! Soundfolder=%s", tostring( self.soundfolderLSO ) ) )

  self.LSOCall.BOLTER.duration = 0.75
  self.LSOCall.CALLTHEBALL.duration = 0.625
  self.LSOCall.CHECK.duration = 0.40
  self.LSOCall.CLEAREDTOLAND.duration = 0.85
  self.LSOCall.COMELEFT.duration = 0.60
  self.LSOCall.DEPARTANDREENTER.duration = 1.10
  self.LSOCall.EXPECTHEAVYWAVEOFF.duration = 1.30
  self.LSOCall.EXPECTSPOT75.duration = 1.85
  self.LSOCall.EXPECTSPOT5.duration = 1.3
  self.LSOCall.FAST.duration = 0.75
  self.LSOCall.FOULDECK.duration = 0.75
  self.LSOCall.HIGH.duration = 0.65
  self.LSOCall.IDLE.duration = 0.40
  self.LSOCall.LONGINGROOVE.duration = 1.25
  self.LSOCall.LOW.duration = 0.60
  self.LSOCall.N0.duration = 0.38
  self.LSOCall.N1.duration = 0.30
  self.LSOCall.N2.duration = 0.30
  self.LSOCall.N3.duration = 0.30
  self.LSOCall.N4.duration = 0.32
  self.LSOCall.N5.duration = 0.41
  self.LSOCall.N6.duration = 0.48
  self.LSOCall.N7.duration = 0.51
  self.LSOCall.N8.duration = 0.38
  self.LSOCall.N9.duration = 0.34
  self.LSOCall.PADDLESCONTACT.duration = 0.91
  self.LSOCall.POWERsoft.duration=0.9 -- VNAO Edit - Added
  self.LSOCall.POWER.duration = 0.45
  self.LSOCall.RADIOCHECK.duration = 0.90
  self.LSOCall.RIGHTFORLINEUP.duration = 0.70
  self.LSOCall.ROGERBALL.duration = 0.72
  self.LSOCall.SLOW.duration = 0.63
  -- self.LSOCall.SLOW.duration=0.59  --TODO
  self.LSOCall.STABILIZED.duration = 0.75
  self.LSOCall.WAVEOFF.duration = 0.55
  self.LSOCall.WELCOMEABOARD.duration = 0.80
end

--- Set parameters for LSO Voice overs by *funkyfranky*.
-- @param #AIRBOSS self
-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located.
function AIRBOSS:SetVoiceOversLSOByFF( mizfolder )

  -- Set sound files folder.
  if mizfolder then
    local lastchar = string.sub( mizfolder, -1 )
    if lastchar ~= "/" then
      mizfolder = mizfolder .. "/"
    end
    self.soundfolderLSO = mizfolder
  else
    -- Default is the general folder.
    self.soundfolderLSO = self.soundfolder
  end

  -- Report for duty.
  self:I( self.lid .. string.format( "LSO FF reporting for duty! Soundfolder=%s", tostring( self.soundfolderLSO ) ) )

  self.LSOCall.BOLTER.duration = 0.75
  self.LSOCall.CALLTHEBALL.duration = 0.60
  self.LSOCall.CHECK.duration = 0.45
  self.LSOCall.CLEAREDTOLAND.duration = 1.00
  self.LSOCall.COMELEFT.duration = 0.60
  self.LSOCall.DEPARTANDREENTER.duration = 1.10
  self.LSOCall.EXPECTHEAVYWAVEOFF.duration = 1.20
  self.LSOCall.EXPECTSPOT75.duration = 2.00
  self.LSOCall.EXPECTSPOT5.duration = 1.3
  self.LSOCall.FAST.duration = 0.70
  self.LSOCall.FOULDECK.duration = 0.62
  self.LSOCall.HIGH.duration = 0.65
  self.LSOCall.IDLE.duration = 0.45
  self.LSOCall.LONGINGROOVE.duration = 1.20
  self.LSOCall.LOW.duration = 0.50
  self.LSOCall.N0.duration = 0.40
  self.LSOCall.N1.duration = 0.25
  self.LSOCall.N2.duration = 0.37
  self.LSOCall.N3.duration = 0.37
  self.LSOCall.N4.duration = 0.39
  self.LSOCall.N5.duration = 0.39
  self.LSOCall.N6.duration = 0.40
  self.LSOCall.N7.duration = 0.40
  self.LSOCall.N8.duration = 0.37
  self.LSOCall.N9.duration = 0.40
  self.LSOCall.PADDLESCONTACT.duration = 1.00
  self.LSOCall.POWER.duration = 0.50
  self.LSOCall.POWERsoft.duration=0.9 -- VNAO Edit - Added
  self.LSOCall.RADIOCHECK.duration = 1.10
  self.LSOCall.RIGHTFORLINEUP.duration = 0.80
  self.LSOCall.ROGERBALL.duration = 1.00
  self.LSOCall.SLOW.duration = 0.65
  self.LSOCall.SLOW.duration = 0.59
  self.LSOCall.STABILIZED.duration = 0.90
  self.LSOCall.WAVEOFF.duration = 0.60
  self.LSOCall.WELCOMEABOARD.duration = 1.00
end

--- Intit parameters for Marshal Voice overs by *funkyfranky*.
-- @param #AIRBOSS self
-- @param #string mizfolder (Optional) Folder within miz file where the sound files are located.
function AIRBOSS:SetVoiceOversMarshalByFF( mizfolder )

  -- Set sound files folder.
  if mizfolder then
    local lastchar = string.sub( mizfolder, -1 )
    if lastchar ~= "/" then
      mizfolder = mizfolder .. "/"
    end
    self.soundfolderMSH = mizfolder
  else
    -- Default is the general folder.
    self.soundfolderMSH = self.soundfolder
  end

  -- Report for duty.
  self:I( self.lid .. string.format( "Marshal FF reporting for duty! Soundfolder=%s", tostring( self.soundfolderMSH ) ) )

  self.MarshalCall.AFFIRMATIVE.duration = 0.90
  self.MarshalCall.ALTIMETER.duration = 0.85
  self.MarshalCall.BRC.duration = 0.80
  self.MarshalCall.CARRIERTURNTOHEADING.duration = 2.48
  self.MarshalCall.CASE.duration = 0.40
  self.MarshalCall.CHARLIETIME.duration = 0.90
  self.MarshalCall.CLEAREDFORRECOVERY.duration = 1.25
  self.MarshalCall.DECKCLOSED.duration = 1.10
  self.MarshalCall.DEGREES.duration = 0.60
  self.MarshalCall.EXPECTED.duration = 0.55
  self.MarshalCall.FLYNEEDLES.duration = 0.90
  self.MarshalCall.HOLDATANGELS.duration = 1.10
  self.MarshalCall.HOURS.duration = 0.60
  self.MarshalCall.MARSHALRADIAL.duration = 1.10
  self.MarshalCall.N0.duration = 0.40
  self.MarshalCall.N1.duration = 0.25
  self.MarshalCall.N2.duration = 0.37
  self.MarshalCall.N3.duration = 0.37
  self.MarshalCall.N4.duration = 0.39
  self.MarshalCall.N5.duration = 0.39
  self.MarshalCall.N6.duration = 0.40
  self.MarshalCall.N7.duration = 0.40
  self.MarshalCall.N8.duration = 0.37
  self.MarshalCall.N9.duration = 0.40
  self.MarshalCall.NEGATIVE.duration = 0.80
  self.MarshalCall.NEWFB.duration = 1.35
  self.MarshalCall.OPS.duration = 0.48
  self.MarshalCall.POINT.duration = 0.33
  self.MarshalCall.RADIOCHECK.duration = 1.20
  self.MarshalCall.RECOVERY.duration = 0.70
  self.MarshalCall.RECOVERYOPSSTOPPED.duration = 1.65
  self.MarshalCall.RECOVERYPAUSEDNOTICE.duration = 2.9 -- Strangely the file is actually a shorter ~2.4 sec.
  self.MarshalCall.RECOVERYPAUSEDRESUMED.duration = 3.40
  self.MarshalCall.REPORTSEEME.duration = 0.95
  self.MarshalCall.RESUMERECOVERY.duration = 1.75
  self.MarshalCall.ROGER.duration = 0.53
  self.MarshalCall.SAYNEEDLES.duration = 0.90
  self.MarshalCall.STACKFULL.duration = 6.35
  self.MarshalCall.STARTINGRECOVERY.duration = 2.65

end

--- Init voice over radio transmission call.
-- @param #AIRBOSS self
function AIRBOSS:_InitVoiceOvers()

  ---------------
  -- LSO Radio --
  ---------------

  -- LSO Radio Calls.
  self.LSOCall = {
    BOLTER = { file = "LSO-BolterBolter", suffix = "ogg", loud = false, subtitle = "Bolter, Bolter", duration = 0.75, subduration = 5 },
    CALLTHEBALL = { file = "LSO-CallTheBall", suffix = "ogg", loud = false, subtitle = "Call the ball", duration = 0.6, subduration = 2 },
    CHECK = { file = "LSO-Check", suffix = "ogg", loud = false, subtitle = "Check", duration = 0.45, subduration = 2.5 },
    CLEAREDTOLAND = { file = "LSO-ClearedToLand", suffix = "ogg", loud = false, subtitle = "Cleared to land", duration = 1.0, subduration = 5 },
    COMELEFT = { file = "LSO-ComeLeft", suffix = "ogg", loud = true, subtitle = "Come left", duration = 0.60, subduration = 1 },
    RADIOCHECK = { file = "LSO-RadioCheck", suffix = "ogg", loud = false, subtitle = "Paddles, radio check", duration = 1.1, subduration = 5 },
    RIGHTFORLINEUP = { file = "LSO-RightForLineup", suffix = "ogg", loud = true, subtitle = "Right for line up", duration = 0.80, subduration = 1 },
    HIGH = { file = "LSO-High", suffix = "ogg", loud = true, subtitle = "You're high", duration = 0.65, subduration = 1 },
    LOW = { file = "LSO-Low", suffix = "ogg", loud = true, subtitle = "You're low", duration = 0.50, subduration = 1 },
    POWER = { file = "LSO-Power", suffix = "ogg", loud = true, subtitle = "Power", duration = 0.50, subduration = 1 }, -- duration 0.45 was too short
    POWERsoft={ file="LSO-Power-soft", suffix="ogg", loud=false, subtitle="Power-soft", duration=0.90, subduration=1 }, -- VNAO Edit - Added
    SLOW = { file = "LSO-Slow", suffix = "ogg", loud = true, subtitle = "You're slow", duration = 0.65, subduration = 1 },
    FAST = { file = "LSO-Fast", suffix = "ogg", loud = true, subtitle = "You're fast", duration = 0.70, subduration = 1 },
    ROGERBALL = { file = "LSO-RogerBall", suffix = "ogg", loud = false, subtitle = "Roger ball", duration = 1.00, subduration = 2 },
    WAVEOFF = { file = "LSO-WaveOff", suffix = "ogg", loud = false, subtitle = "Wave off", duration = 0.6, subduration = 5 },
    LONGINGROOVE = { file = "LSO-LongInTheGroove", suffix = "ogg", loud = false, subtitle = "You're long in the groove", duration = 1.2, subduration = 5 },
    FOULDECK = { file = "LSO-FoulDeck", suffix = "ogg", loud = false, subtitle = "Foul deck", duration = 0.62, subduration = 5 },
    DEPARTANDREENTER = { file = "LSO-DepartAndReenter", suffix = "ogg", loud = false, subtitle = "Depart and re-enter", duration = 1.1, subduration = 5 },
    PADDLESCONTACT = { file = "LSO-PaddlesContact", suffix = "ogg", loud = false, subtitle = "Paddles, contact", duration = 1.0, subduration = 5 },
    WELCOMEABOARD = { file = "LSO-WelcomeAboard", suffix = "ogg", loud = false, subtitle = "Welcome aboard", duration = 1.0, subduration = 5 },
    EXPECTHEAVYWAVEOFF = { file = "LSO-ExpectHeavyWaveoff", suffix = "ogg", loud = false, subtitle = "Expect heavy waveoff", duration = 1.2, subduration = 5 },
    EXPECTSPOT75 = { file = "LSO-ExpectSpot75", suffix = "ogg", loud = false, subtitle = "Expect spot 7.5", duration = 2.0, subduration = 5 },
    EXPECTSPOT5 = { file = "LSO-ExpectSpot5", suffix = "ogg", loud = false, subtitle = "Expect spot 5", duration = 1.3, subduration = 5 },
    STABILIZED = { file = "LSO-Stabilized", suffix = "ogg", loud = false, subtitle = "Stabilized", duration = 0.9, subduration = 5 },
    IDLE = { file = "LSO-Idle", suffix = "ogg", loud = false, subtitle = "Idle", duration = 0.45, subduration = 5 },
    N0 = { file = "LSO-N0", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 },
    N1 = { file = "LSO-N1", suffix = "ogg", loud = false, subtitle = "", duration = 0.25 },
    N2 = { file = "LSO-N2", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 },
    N3 = { file = "LSO-N3", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 },
    N4 = { file = "LSO-N4", suffix = "ogg", loud = false, subtitle = "", duration = 0.39 },
    N5 = { file = "LSO-N5", suffix = "ogg", loud = false, subtitle = "", duration = 0.39 },
    N6 = { file = "LSO-N6", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 },
    N7 = { file = "LSO-N7", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 },
    N8 = { file = "LSO-N8", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 },
    N9 = { file = "LSO-N9", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 },
    CLICK = { file = "AIRBOSS-RadioClick", suffix = "ogg", loud = false, subtitle = "", duration = 0.35 },
    NOISE = { file = "AIRBOSS-Noise", suffix = "ogg", loud = false, subtitle = "", duration = 3.6 },
    SPINIT = { file = "AIRBOSS-SpinIt", suffix = "ogg", loud = false, subtitle = "", duration = 0.73, subduration = 5 },
  }

  -----------------
  -- Pilot Calls --
  -----------------

  -- Pilot Radio Calls.
  self.PilotCall = {
    N0 = { file = "PILOT-N0", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 },
    N1 = { file = "PILOT-N1", suffix = "ogg", loud = false, subtitle = "", duration = 0.25 },
    N2 = { file = "PILOT-N2", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 },
    N3 = { file = "PILOT-N3", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 },
    N4 = { file = "PILOT-N4", suffix = "ogg", loud = false, subtitle = "", duration = 0.39 },
    N5 = { file = "PILOT-N5", suffix = "ogg", loud = false, subtitle = "", duration = 0.39 },
    N6 = { file = "PILOT-N6", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 },
    N7 = { file = "PILOT-N7", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 },
    N8 = { file = "PILOT-N8", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 },
    N9 = { file = "PILOT-N9", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 },
    POINT = { file = "PILOT-Point", suffix = "ogg", loud = false, subtitle = "", duration = 0.33 },
    SKYHAWK = { file = "PILOT-Skyhawk", suffix = "ogg", loud = false, subtitle = "", duration = 0.95, subduration = 5 },
    HARRIER = { file = "PILOT-Harrier", suffix = "ogg", loud = false, subtitle = "", duration = 0.58, subduration = 5 },
    HAWKEYE = { file = "PILOT-Hawkeye", suffix = "ogg", loud = false, subtitle = "", duration = 0.63, subduration = 5 },
    TOMCAT = { file = "PILOT-Tomcat", suffix = "ogg", loud = false, subtitle = "", duration = 0.66, subduration = 5 },
    HORNET = { file = "PILOT-Hornet", suffix = "ogg", loud = false, subtitle = "", duration = 0.56, subduration = 5 },
    VIKING = { file = "PILOT-Viking", suffix = "ogg", loud = false, subtitle = "", duration = 0.61, subduration = 5 },
    GREYHOUND = { file = "PILOT-Greyhound", suffix = "ogg", loud = false, subtitle = "", duration = 0.61, subduration = 5 },
    BALL = { file = "PILOT-Ball", suffix = "ogg", loud = false, subtitle = "", duration = 0.50, subduration = 5 },
    BINGOFUEL = { file = "PILOT-BingoFuel", suffix = "ogg", loud = false, subtitle = "", duration = 0.80 },
    GASATDIVERT = { file = "PILOT-GasAtDivert", suffix = "ogg", loud = false, subtitle = "", duration = 1.80 },
    GASATTANKER = { file = "PILOT-GasAtTanker", suffix = "ogg", loud = false, subtitle = "", duration = 1.95 },
  }

  -------------------
  -- MARSHAL Radio --
  -------------------

  -- MARSHAL Radio Calls.
  self.MarshalCall = {
    AFFIRMATIVE = { file = "MARSHAL-Affirmative", suffix = "ogg", loud = false, subtitle = "", duration = 0.90 },
    ALTIMETER = { file = "MARSHAL-Altimeter", suffix = "ogg", loud = false, subtitle = "", duration = 0.85 },
    BRC = { file = "MARSHAL-BRC", suffix = "ogg", loud = false, subtitle = "", duration = 0.80 },
    CARRIERTURNTOHEADING = { file = "MARSHAL-CarrierTurnToHeading", suffix = "ogg", loud = false, subtitle = "", duration = 2.48, subduration = 5 },
    CASE = { file = "MARSHAL-Case", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 },
    CHARLIETIME = { file = "MARSHAL-CharlieTime", suffix = "ogg", loud = false, subtitle = "", duration = 0.90 },
    CLEAREDFORRECOVERY = { file = "MARSHAL-ClearedForRecovery", suffix = "ogg", loud = false, subtitle = "", duration = 1.25 },
    DECKCLOSED = { file = "MARSHAL-DeckClosed", suffix = "ogg", loud = false, subtitle = "", duration = 1.10, subduration = 5 },
    DEGREES = { file = "MARSHAL-Degrees", suffix = "ogg", loud = false, subtitle = "", duration = 0.60 },
    EXPECTED = { file = "MARSHAL-Expected", suffix = "ogg", loud = false, subtitle = "", duration = 0.55 },
    FLYNEEDLES = { file = "MARSHAL-FlyYourNeedles", suffix = "ogg", loud = false, subtitle = "Fly your needles", duration = 0.9, subduration = 5 },
    HOLDATANGELS = { file = "MARSHAL-HoldAtAngels", suffix = "ogg", loud = false, subtitle = "", duration = 1.10 },
    HOURS = { file = "MARSHAL-Hours", suffix = "ogg", loud = false, subtitle = "", duration = 0.60, subduration = 5 },
    MARSHALRADIAL = { file = "MARSHAL-MarshalRadial", suffix = "ogg", loud = false, subtitle = "", duration = 1.10 },
    N0 = { file = "MARSHAL-N0", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 },
    N1 = { file = "MARSHAL-N1", suffix = "ogg", loud = false, subtitle = "", duration = 0.25 },
    N2 = { file = "MARSHAL-N2", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 },
    N3 = { file = "MARSHAL-N3", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 },
    N4 = { file = "MARSHAL-N4", suffix = "ogg", loud = false, subtitle = "", duration = 0.39 },
    N5 = { file = "MARSHAL-N5", suffix = "ogg", loud = false, subtitle = "", duration = 0.39 },
    N6 = { file = "MARSHAL-N6", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 },
    N7 = { file = "MARSHAL-N7", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 },
    N8 = { file = "MARSHAL-N8", suffix = "ogg", loud = false, subtitle = "", duration = 0.37 },
    N9 = { file = "MARSHAL-N9", suffix = "ogg", loud = false, subtitle = "", duration = 0.40 },
    NEGATIVE = { file = "MARSHAL-Negative", suffix = "ogg", loud = false, subtitle = "", duration = 0.80, subduration = 5 },
    NEWFB = { file = "MARSHAL-NewFB", suffix = "ogg", loud = false, subtitle = "", duration = 1.35 },
    OPS = { file = "MARSHAL-Ops", suffix = "ogg", loud = false, subtitle = "", duration = 0.48 },
    POINT = { file = "MARSHAL-Point", suffix = "ogg", loud = false, subtitle = "", duration = 0.33 },
    RADIOCHECK = { file = "MARSHAL-RadioCheck", suffix = "ogg", loud = false, subtitle = "Radio check", duration = 1.20, subduration = 5 },
    RECOVERY = { file = "MARSHAL-Recovery", suffix = "ogg", loud = false, subtitle = "", duration = 0.70, subduration = 5 },
    RECOVERYOPSSTOPPED = { file = "MARSHAL-RecoveryOpsStopped", suffix = "ogg", loud = false, subtitle = "", duration = 1.65, subduration = 5 },
    RECOVERYPAUSEDNOTICE = { file = "MARSHAL-RecoveryPausedNotice", suffix = "ogg", loud = false, subtitle = "aircraft recovery paused until further notice", duration = 2.90, subduration = 5 },
    RECOVERYPAUSEDRESUMED = { file = "MARSHAL-RecoveryPausedResumed", suffix = "ogg", loud = false, subtitle = "", duration = 3.40, subduration = 5 },
    REPORTSEEME = { file = "MARSHAL-ReportSeeMe", suffix = "ogg", loud = false, subtitle = "", duration = 0.95 },
    RESUMERECOVERY = { file = "MARSHAL-ResumeRecovery", suffix = "ogg", loud = false, subtitle = "resuming aircraft recovery", duration = 1.75, subduraction = 5 },
    ROGER = { file = "MARSHAL-Roger", suffix = "ogg", loud = false, subtitle = "", duration = 0.53, subduration = 5 },
    SAYNEEDLES = { file = "MARSHAL-SayNeedles", suffix = "ogg", loud = false, subtitle = "Say needles", duration = 0.90, subduration = 5 },
    STACKFULL = { file = "MARSHAL-StackFull", suffix = "ogg", loud = false, subtitle = "Marshal Stack is currently full. Hold outside 10 NM zone and wait for further instructions", duration = 6.35, subduration = 10 },
    STARTINGRECOVERY = { file = "MARSHAL-StartingRecovery", suffix = "ogg", loud = false, subtitle = "", duration = 2.65, subduration = 5 },
    CLICK = { file = "AIRBOSS-RadioClick", suffix = "ogg", loud = false, subtitle = "", duration = 0.35 },
    NOISE = { file = "AIRBOSS-Noise", suffix = "ogg", loud = false, subtitle = "", duration = 3.6 },
  }

  -- Default timings by Raynor
  self:SetVoiceOversLSOByRaynor()
  self:SetVoiceOversMarshalByRaynor()

end

--- Init voice over radio transmission call.
-- @param #AIRBOSS self
-- @param #AIRBOSS.RadioCall radiocall LSO or Marshal radio call object.
-- @param #number duration Duration of the voice over in seconds.
-- @param #string subtitle (Optional) Subtitle to be displayed along with voice over.
-- @param #number subduration (Optional) Duration how long the subtitle is displayed.
-- @param #string filename (Optional) Name of the voice over sound file.
-- @param #string suffix (Optional) Extention of file. Default ".ogg".
function AIRBOSS:SetVoiceOver( radiocall, duration, subtitle, subduration, filename, suffix )
  radiocall.duration = duration
  radiocall.subtitle = subtitle or radiocall.subtitle
  radiocall.file = filename
  radiocall.suffix = suffix or ".ogg"
end

--- Get optimal aircraft AoA parameters..
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @return #AIRBOSS.AircraftAoA AoA parameters for the given aircraft type.
function AIRBOSS:_GetAircraftAoA( playerData )

  -- Get AC type.
  local hornet =   playerData.actype == AIRBOSS.AircraftCarrier.HORNET
                or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE
                or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF
                or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER
  local goshawk = playerData.actype == AIRBOSS.AircraftCarrier.T45C
  local skyhawk = playerData.actype == AIRBOSS.AircraftCarrier.A4EC
  local harrier = playerData.actype == AIRBOSS.AircraftCarrier.AV8B
  local tomcat  = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B
  local corsair = playerData.actype == AIRBOSS.AircraftCarrier.CORSAIR or playerData.actype == AIRBOSS.AircraftCarrier.CORSAIR_CW

  -- Table with AoA values.
  local aoa = {} -- #AIRBOSS.AircraftAoA

  if hornet then
    -- F/A-18C Hornet parameters.
    aoa.SLOW = 9.8
    aoa.Slow = 9.3
    aoa.OnSpeedMax = 8.8
    aoa.OnSpeed = 8.1
    aoa.OnSpeedMin = 7.4
    aoa.Fast = 6.9
    aoa.FAST = 6.3
  elseif tomcat then
    -- F-14A/B Tomcat parameters (taken from NATOPS). Converted from units 0-30 to degrees.
    -- Currently assuming a linear relationship with 0=-10 degrees and 30=+40 degrees as stated in NATOPS.
    aoa.SLOW = self:_AoAUnit2Deg( playerData, 17.5 ) -- 18.33 --17.0 units -- VNAO Edit - Original value 17
    aoa.Slow = self:_AoAUnit2Deg( playerData, 16.5 ) -- 16.67 --16.0 units -- VNAO Edit - Original value 16
    aoa.OnSpeedMax = self:_AoAUnit2Deg( playerData, 16.0 ) -- 15.83 --15.5 units -- VNAO Edit - Original value 15.5 
    aoa.OnSpeed = self:_AoAUnit2Deg( playerData, 15.0 ) -- 15.0  --15.0 units
    aoa.OnSpeedMin = self:_AoAUnit2Deg( playerData, 14.0 ) -- 14.17 --14.5 units -- VNAO Edit - Original value 14.5
    aoa.Fast = self:_AoAUnit2Deg( playerData, 13.5 ) -- 13.33 --14.0 units -- VNAO Edit - Original value 14
    aoa.FAST = self:_AoAUnit2Deg( playerData, 12.5 ) -- 11.67 --13.0 units -- VNAO Edit - Original value 13
  elseif goshawk then
    -- T-45C Goshawk parameters.
    aoa.SLOW = 8.00 -- 19
    aoa.Slow = 7.75 -- 18
    aoa.OnSpeedMax = 7.25 -- 17.5
    aoa.OnSpeed = 7.00 -- 17
    aoa.OnSpeedMin = 6.75 -- 16.5
    aoa.Fast = 6.25 -- 16
    aoa.FAST = 6.00 -- 15
  elseif skyhawk then
    -- A-4E-C Skyhawk parameters from https://forums.eagle.ru/showpost.php?p=3703467&postcount=390
    -- Note that these are arbitrary UNITS and not degrees. We need a conversion formula!
    -- Github repo suggests they simply use a factor of two to get from degrees to units.
    aoa.SLOW = 10.50 -- =19.0/2 -- VNAO Edit - Original value 9.50
    aoa.Slow = 9.50 -- =18.5/2 -- VNAO Edit - Original value 9.25
    aoa.OnSpeedMax = 9.25 -- =18.0/2 -- VNAO Edit - Original value 9.00
    aoa.OnSpeed = 8.75 -- =17.5/2 8.1 -- VNAO Edit - Original value 8.75
    aoa.OnSpeedMin = 8.25 -- =17.0/2 -- VNAO Edit - Original value 8.50
    aoa.Fast = 8.00 -- =17.5/2 -- VNAO Edit - Original value 8.25
    aoa.FAST = 7.00 -- =16.5/2 -- VNAO Edit - Original value 8.0
  elseif harrier then
    -- AV-8B Harrier parameters. Tuning done on the Fast AoA to allow for abeam and ninety at Nozzles 55. Pene testing
    aoa.SLOW       = 16.0
    aoa.Slow       = 13.5
    aoa.OnSpeedMax = 12.5
    aoa.OnSpeed    = 10.0
    aoa.OnSpeedMin =  9.5
    aoa.Fast       =  8.0
    aoa.FAST       =  7.5
  elseif corsair then
    -- F4U-1D Corsair parameters.
    aoa.SLOW       = 16.0
    aoa.Slow       = 13.5
    aoa.OnSpeedMax = 12.5
    aoa.OnSpeed    = 10.0
    aoa.OnSpeedMin =  9.5
    aoa.Fast       =  8.0
    aoa.FAST       =  7.5
  end

  return aoa
end

--- Convert AoA from arbitrary units to degrees.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @param #number aoaunits AoA in arbitrary units.
-- @return #number AoA in degrees.
function AIRBOSS:_AoAUnit2Deg( playerData, aoaunits )

  -- Init.
  local degrees = aoaunits

  -- Check aircraft type of player.
  if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then

    -------------
    -- F-14A/B --
    -------------

    -- NATOPS:
    -- unit=0  ==> alpha=-10 degrees.
    -- unit=30 ==> alpha=+40 degrees.

    -- Assuming a linear relationship between these to points of the graph.
    -- However: AoA=15 Units ==> 15 degrees, which is too much.
    degrees = -10 + 50 / 30 * aoaunits

    -- HB Facebook page https://www.facebook.com/heatblur/photos/a.683612385159716/754368278084126
    -- AoA=15 Units <==> AoA=10.359 degrees.
    degrees = 0.918 * aoaunits - 3.411

  elseif playerData.actype == AIRBOSS.AircraftCarrier.A4EC then

    ----------
    -- A-4E --
    ----------

    -- A-4E-C source code suggests a simple factor of 1/2 for conversion.
    degrees = 0.5 * aoaunits

  end

  return degrees
end

--- Convert AoA from degrees to arbitrary units.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @param #number degrees AoA in degrees.
-- @return #number AoA in arbitrary units.
function AIRBOSS:_AoADeg2Units( playerData, degrees )

  -- Init.
  local aoaunits = degrees

  -- Check aircraft type of player.
  if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then

    -------------
    -- F-14A/B --
    -------------

    -- NATOPS:
    -- unit=0  ==> alpha=-10 degrees.
    -- unit=30 ==> alpha=+40 degrees.

    -- Assuming a linear relationship between these to points of the graph.
    aoaunits = (degrees + 10) * 30 / 50

    -- HB Facebook page https://www.facebook.com/heatblur/photos/a.683612385159716/754368278084126
    -- AoA=15 Units <==> AoA=10.359 degrees.
    aoaunits = 1.089 * degrees + 3.715

  elseif playerData.actype == AIRBOSS.AircraftCarrier.A4EC then

    ----------
    -- A-4E --
    ----------

    -- A-4E source code suggests a simple factor of two as conversion.
    aoaunits = 2 * degrees

  end

  return aoaunits
end

--- Get optimal aircraft flight parameters at checkpoint.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @param #string step Pattern step.
-- @return #number Altitude in meters or nil.
-- @return #number Angle of Attack or nil.
-- @return #number Distance to carrier in meters or nil.
-- @return #number Speed in m/s or nil.
function AIRBOSS:_GetAircraftParameters( playerData, step )

  -- Get parameters depended on step.
  step = step or playerData.step

  -- Get AC type.
  local hornet =    playerData.actype == AIRBOSS.AircraftCarrier.HORNET
                 or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE
                 or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF
                 or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER
  local skyhawk = playerData.actype == AIRBOSS.AircraftCarrier.A4EC
  local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B
  local harrier = playerData.actype == AIRBOSS.AircraftCarrier.AV8B
  local goshawk = playerData.actype == AIRBOSS.AircraftCarrier.T45C
  local corsair = playerData.actype == AIRBOSS.AircraftCarrier.CORSAIR or playerData.actype == AIRBOSS.AircraftCarrier.CORSAIR_CW

  -- Return values.
  local alt
  local aoa
  local dist
  local speed

  -- Aircraft specific AoA.
  local aoaac = self:_GetAircraftAoA( playerData )

  if step == AIRBOSS.PatternStep.PLATFORM then

    alt = UTILS.FeetToMeters( 5000 )

    -- dist=UTILS.NMToMeters(20)

    speed = UTILS.KnotsToMps( 250 )

  elseif step == AIRBOSS.PatternStep.ARCIN then

    if tomcat then
      speed = UTILS.KnotsToMps( 150 )
    else
      speed = UTILS.KnotsToMps( 250 )
    end

  elseif step == AIRBOSS.PatternStep.ARCOUT then

    if tomcat then
      speed = UTILS.KnotsToMps( 150 )
    else
      speed = UTILS.KnotsToMps( 250 )
    end

  elseif step == AIRBOSS.PatternStep.DIRTYUP then

    alt = UTILS.FeetToMeters( 1200 )

    -- speed=UTILS.KnotsToMps(250)

  elseif step == AIRBOSS.PatternStep.BULLSEYE then

    alt = UTILS.FeetToMeters( 1200 )

    dist = -UTILS.NMToMeters( 3 )

    aoa = aoaac.OnSpeed

  elseif step == AIRBOSS.PatternStep.INITIAL then

    if hornet or tomcat or harrier then
      alt = UTILS.FeetToMeters( 800 )
      speed = UTILS.KnotsToMps( 350 )
    elseif skyhawk then
      alt = UTILS.FeetToMeters( 600 )
      speed = UTILS.KnotsToMps( 250 )
    elseif goshawk then
      alt = UTILS.FeetToMeters( 800 )
      speed = UTILS.KnotsToMps( 300 )
    elseif corsair then
      alt = UTILS.FeetToMeters( 300 )
      speed = UTILS.KnotsToMps( 120 )
    end

  elseif step == AIRBOSS.PatternStep.BREAKENTRY then

    if hornet or tomcat or harrier then
      alt = UTILS.FeetToMeters( 800 )
      speed = UTILS.KnotsToMps( 350 )
    elseif skyhawk then
      alt = UTILS.FeetToMeters( 600 )
      speed = UTILS.KnotsToMps( 250 )
    elseif goshawk then
      alt = UTILS.FeetToMeters( 800 )
      speed = UTILS.KnotsToMps( 300 )
    elseif corsair then
      alt = UTILS.FeetToMeters( 200 )
      speed = UTILS.KnotsToMps( 110 )
    end

  elseif step == AIRBOSS.PatternStep.EARLYBREAK then

    if hornet or tomcat or harrier or goshawk then
      alt = UTILS.FeetToMeters( 800 )
    elseif skyhawk then
      alt = UTILS.FeetToMeters( 600 )
    elseif corsair then
      alt = UTILS.FeetToMeters( 200 )
      speed = UTILS.KnotsToMps( 100 )
    end

  elseif step == AIRBOSS.PatternStep.LATEBREAK then

    if hornet or tomcat or harrier or goshawk then
      alt = UTILS.FeetToMeters( 800 )
    elseif skyhawk then
      alt = UTILS.FeetToMeters( 600 )
    elseif corsair then
      alt = UTILS.FeetToMeters( 150 )
      speed = UTILS.KnotsToMps( 100 )
    end

  elseif step == AIRBOSS.PatternStep.ABEAM then

    if hornet or tomcat or harrier or goshawk then
      alt = UTILS.FeetToMeters( 600 )
    elseif skyhawk then
      alt = UTILS.FeetToMeters( 500 )
    elseif corsair then
      alt = UTILS.FeetToMeters( 150 )
      speed = UTILS.KnotsToMps( 90 )
    end

    aoa = aoaac.OnSpeed

    if goshawk then
      -- 0.9 to 1.1 NM per natops ch.4 page 48
      dist = UTILS.NMToMeters( 0.9 )
    elseif harrier then
      -- 0.8 to 1.0 NM
      dist = UTILS.NMToMeters( 0.9 )
    else
      dist = UTILS.NMToMeters( 1.1 )
    end

  elseif step == AIRBOSS.PatternStep.NINETY then

    if hornet or tomcat then
      alt = UTILS.FeetToMeters( 500 )
    elseif goshawk then
      alt = UTILS.FeetToMeters( 450 )
    elseif skyhawk then
      alt = UTILS.FeetToMeters( 500 )
    elseif harrier then
      alt = UTILS.FeetToMeters( 425 )
    elseif corsair then
      alt = UTILS.FeetToMeters( 90 )
      speed = UTILS.KnotsToMps( 90 )      
    end

    aoa = aoaac.OnSpeed

  elseif step == AIRBOSS.PatternStep.WAKE then

    if hornet or goshawk then
      alt = UTILS.FeetToMeters( 370 )
    elseif tomcat then
      alt = UTILS.FeetToMeters( 430 ) -- Tomcat should be a bit higher as it intercepts the GS a bit higher.
    elseif skyhawk then
      alt = UTILS.FeetToMeters( 370 ) -- ?
    elseif corsair then
      alt = UTILS.FeetToMeters( 80 )
    end
    -- Harrier wont get into wake pos. Runway is not angled and it stays port.

    aoa = aoaac.OnSpeed

  elseif step == AIRBOSS.PatternStep.FINAL then

    if hornet or goshawk then
      alt = UTILS.FeetToMeters( 300 )
    elseif tomcat then
      alt = UTILS.FeetToMeters( 360 )
    elseif skyhawk then
      alt = UTILS.FeetToMeters( 300 ) -- ?
    elseif harrier then
      alt=UTILS.FeetToMeters(312)-- 300-325 ft
    elseif corsair then
      alt = UTILS.FeetToMeters( 80 )      
    end

    aoa = aoaac.OnSpeed

  end

  return alt, aoa, dist, speed
end

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- QUEUE Functions
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get next marshal flight which is ready to enter the landing pattern.
-- @param #AIRBOSS self
-- @return #AIRBOSS.FlightGroup Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready.
function AIRBOSS:_GetNextMarshalFight()

  -- Loop over all marshal flights.
  for _, _flight in pairs( self.Qmarshal ) do
    local flight = _flight -- #AIRBOSS.FlightGroup

    -- Current stack.
    local stack = flight.flag

    -- Total marshal time in seconds.
    local Tmarshal = timer.getAbsTime() - flight.time

    -- Min time in marshal stack.
    local TmarshalMin = 2 * 60 -- Two minutes for human players.
    if flight.ai then
      TmarshalMin = 3 * 60 -- Three minutes for AI.
    end

    -- Check if conditions are right.
    if flight.holding ~= nil and Tmarshal >= TmarshalMin then
      if flight.case == 1 and stack == 1 or flight.case > 1 then
        if flight.ai then
          -- Return AI flight.
          return flight
        else
          -- Check for human player if they are already commencing.
          if flight.step ~= AIRBOSS.PatternStep.COMMENCING then
            return flight
          end
        end
      end
    end
  end

  return nil
end

--- Check marshal and pattern queues.
-- @param #AIRBOSS self
function AIRBOSS:_CheckQueue()

  -- Print queues.
  if self.Debug then
    self:_PrintQueue( self.flights, "All Flights" )
  end
  self:_PrintQueue( self.Qmarshal, "Marshal" )
  self:_PrintQueue( self.Qpattern, "Pattern" )
  self:_PrintQueue( self.Qwaiting, "Waiting" )
  self:_PrintQueue( self.Qspinning, "Spinning" )

  -- If flights are waiting outside 10 NM zone and carrier switches from Case I to Case II/III, they should be added to the Marshal stack as now there is no stack limit any more.
  if self.case > 1 then
    for _, _flight in pairs( self.Qwaiting ) do
      local flight = _flight -- #AIRBOSS.FlightGroup

      -- Remove flight from waiting queue.
      local removed = self:_RemoveFlightFromQueue( self.Qwaiting, flight )

      if removed then

        -- Get free stack
        local stack = self:_GetFreeStack( flight.ai )

        -- Debug info.
        self:T( self.lid .. string.format( "Moving flight %s onboard %s from Waiting queue to Case %d Marshal stack %d", flight.groupname, flight.onboard, self.case, stack ) )

        -- Send flight to marshal stack.
        if flight.ai then
          self:_MarshalAI( flight, stack )
        else
          self:_MarshalPlayer( flight, stack )
        end

        -- Break the loop so that only one flight per 30 seconds is removed.
        break
      end

    end
  end

  -- Check if carrier is currently in recovery mode.
  if not self:IsRecovering() then

    -----------------------------
    -- Switching Recovery Case --
    -----------------------------

    -- Loop over all flights currently in the marshal queue.
    for _, _flight in pairs( self.Qmarshal ) do
      local flight = _flight -- #AIRBOSS.FlightGroup

      -- TODO: In principle this should be done/necessary only if case 1-->2/3 or 2/3-->1, right?
      -- When recovery switches from 2->3 or 3-->2 nothing changes in the marshal stack.

      -- Check if a change of stack is necessary.
      if (flight.case == 1 and self.case > 1) or (flight.case > 1 and self.case == 1) then

        -- Remove flight from marshal queue.
        local removed = self:_RemoveFlightFromQueue( self.Qmarshal, flight )

        if removed then

          -- Get free stack
          local stack = self:_GetFreeStack( flight.ai )

          -- Debug output.
          self:T( self.lid .. string.format( "Moving flight %s onboard %s from Marshal Case %d ==> %d Marshal stack %d", flight.groupname, flight.onboard, flight.case, self.case, stack ) )

          -- Send flight to marshal queue.
          if flight.ai then
            self:_MarshalAI( flight, stack )
          else
            self:_MarshalPlayer( flight, stack )
          end

          -- Break the loop so that only one flight per 30 seconds is removed. No spam of messages, no conflict with the loop over queue entries.
          break

        elseif flight.case ~= self.case then

          -- This should handle 2-->3 or 3-->2
          flight.case = self.case

        end

      end
    end

    -- Not recovering ==> skip the rest!
    return
  end

  -- Get number of airborne aircraft units(!) currently in pattern.
  local _, npattern = self:_GetQueueInfo( self.Qpattern )

  -- Get number of aircraft units spinning.
  local _, nspinning = self:_GetQueueInfo( self.Qspinning )

  -- Get next marshal flight.
  local marshalflight = self:_GetNextMarshalFight()

  -- Check if there are flights waiting in the Marshal stack and if the pattern is free. No one should be spinning.
  if marshalflight and npattern < self.Nmaxpattern and nspinning == 0 then

    -- Time flight is marshaling.
    local Tmarshal = timer.getAbsTime() - marshalflight.time
    self:T( self.lid .. string.format( "Marshal time of next group %s = %d seconds", marshalflight.groupname, Tmarshal ) )

    -- Time (last) flight has entered landing pattern.
    local Tpattern = 9999
    local npunits = 1
    local pcase = 1
    if npattern > 0 then

      -- Last flight group send to pattern.
      local patternflight = self.Qpattern[#self.Qpattern] -- #AIRBOSS.FlightGroup

      -- Recovery case of pattern flight.
      pcase = patternflight.case

      -- Number of airborne aircraft in this group. Count includes section members.
      local npunits = self:_GetFlightUnits( patternflight, false )

      -- Get time in pattern.
      Tpattern = timer.getAbsTime() - patternflight.time
      self:T( self.lid .. string.format( "Pattern time of last group %s = %d seconds. # of units=%d.", patternflight.groupname, Tpattern, npunits ) )
    end

    -- Min time in pattern before next aircraft is allowed.
    local TpatternMin
    if pcase == 1 then
      TpatternMin = 2 * 60 * npunits -- 45*npunits   --  45 seconds interval per plane!
    else
      TpatternMin = 2 * 60 * npunits -- 120*npunits  -- 120 seconds interval per plane!
    end

    -- Check interval to last pattern flight.
    if Tpattern > TpatternMin then
      self:T( self.lid .. string.format( "Sending marshal flight %s to pattern.", marshalflight.groupname ) )
      self:_ClearForLanding( marshalflight )
    end

  end
end

--- Clear flight for landing. AI are removed from Marshal queue and the Marshal stack is collapsed.
-- If next in line is an AI flight, this is done. If human player is next, we wait for "Commence" via F10 radio menu command.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight Flight to go to pattern.
function AIRBOSS:_ClearForLanding( flight )

  -- Check if flight is AI or human. If AI, we collapse the stack and commence. If human, we suggest to commence.
  if flight.ai then

    -- Collapse stack and send AI to pattern.
    self:_RemoveFlightFromMarshalQueue( flight, false )
    self:_LandAI( flight )

    -- Cleared for Case X recovery.
    self:_MarshalCallClearedForRecovery( flight.onboard, flight.case )

    -- Voice over of the commencing simulated call from AI
    if self.xtVoiceOversAI then
      local leader = flight.group:GetUnits()[1]
      self:_CommencingCall(leader, flight.onboard)
    end

  else

    -- Cleared for Case X recovery.
    if flight.step ~= AIRBOSS.PatternStep.COMMENCING then
      self:_MarshalCallClearedForRecovery( flight.onboard, flight.case )
      flight.time = timer.getAbsTime()
    end

    -- Set step to commencing. This will trigger the zone check until the player is in the right place.
    self:_SetPlayerStep( flight, AIRBOSS.PatternStep.COMMENCING, 3 )

  end

end

--- Set player step. Any warning is erased and next step hint shown.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @param #string step Next step.
-- @param #number delay (Optional) Set set after a delay in seconds.
function AIRBOSS:_SetPlayerStep( playerData, step, delay )

  if delay and delay > 0 then
    -- Delayed call.
    -- SCHEDULER:New(nil, self._SetPlayerStep, {self, playerData, step}, delay)
    self:ScheduleOnce( delay, self._SetPlayerStep, self, playerData, step )
  else

    -- Check if player still exists after possible delay.
    if playerData then

      -- Set player step.
      playerData.step = step

      -- Erase warning.
      playerData.warning = nil

      -- Next step hint.
      self:_StepHint( playerData )
    end

  end

end

--- Scan carrier zone for (new) units.
-- @param #AIRBOSS self
function AIRBOSS:_ScanCarrierZone()

  -- Carrier position.
  local coord = self:GetCoordinate()

  -- Scan radius = radius of the CCA.
  local RCCZ = self.zoneCCA:GetRadius()

  -- Debug info.
  self:T( self.lid .. string.format( "Scanning Carrier Controlled Area. Radius=%.1f NM.", UTILS.MetersToNM( RCCZ ) ) )

  -- Scan units in carrier zone.
  local _, _, _, unitscan = coord:ScanObjects( RCCZ, true, false, false )

  -- Make a table with all groups currently in the CCA zone.
  local insideCCA = {}
  for _, _unit in pairs( unitscan ) do
    local unit = _unit -- Wrapper.Unit#UNIT

    -- Necessary conditions to be met:
    local airborne = unit:IsAir() -- and unit:InAir()
    local inzone = unit:IsInZone( self.zoneCCA )
    local friendly = self:GetCoalition() == unit:GetCoalition()
    local carrierac = self:_IsCarrierAircraft( unit )

    -- Check if this an aircraft and that it is airborne and closing in.
    if airborne and inzone and friendly and carrierac then

      local group = unit:GetGroup()
      local groupname = group:GetName()

      if insideCCA[groupname] == nil then
        insideCCA[groupname] = group
      end

    end
  end

  -- Find new flights that are inside CCA.
  for groupname, _group in pairs( insideCCA ) do
    local group = _group -- Wrapper.Group#GROUP

    -- Get flight group if possible.
    local knownflight = self:_GetFlightFromGroupInQueue( group, self.flights )

    -- Get aircraft type name.
    local actype = group:GetTypeName()

    -- Create a new flight group
    if knownflight then

      -- Debug output.
      self:T2(self.lid..string.format("Known flight group %s of type %s in CCA.", groupname, actype))

      -- Check if flight is AI and if we want to handle it at all.
      if knownflight.ai and knownflight.flag == -100 and self.handleai then

        local putintomarshal = false

        -- Get flight group.
        local flight = _DATABASE:GetOpsGroup( groupname )

        if flight and flight:IsInbound() and flight.destbase:GetName() == self.carrier:GetName() then
          if flight.ishelo then
          else
            putintomarshal = true
          end
          flight.airboss = self
        end

        -- Send AI flight to marshal stack if group closes in more than 5 and has initial flag value.
        if putintomarshal then


          -- Get the next free stack for current recovery case.
          local stack = self:_GetFreeStack( knownflight.ai )

          -- Repawn.
          local respawn = self.respawnAI

          if stack then

            -- Send AI to marshal stack. We respawn the group to clean possible departure and destination airbases.
            self:_MarshalAI( knownflight, stack, respawn )

          else

            -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available.
            if not self:_InQueue( self.Qwaiting, knownflight.group ) then
              self:_WaitAI( knownflight, respawn ) -- Group is respawned to clear any attached airfields.
            end

          end

          -- Break the loop to not have all flights at once! Spams the message screen.
          break

        end -- Closed in or tanker/AWACS

      end

    else

      -- Unknown new AI flight. Create a new flight group.
      if not self:_IsHuman( group ) then
        self:_CreateFlightGroup( group )
      end
    end

  end

  -- Find flights that are not in CCA.
  local remove = {}
  for _, _flight in pairs( self.flights ) do
    local flight = _flight -- #AIRBOSS.FlightGroup
    if insideCCA[flight.groupname] == nil then
      -- Do not remove flights in marshal pattern. At least for case 2 & 3. If zone is set small, they might be outside in the holding pattern.
      if flight.ai and not (self:_InQueue( self.Qmarshal, flight.group ) or self:_InQueue( self.Qpattern, flight.group )) then
        table.insert( remove, flight )
      end
    end
  end

  -- Remove flight groups outside CCA.
  for _, flight in pairs( remove ) do
    self:_RemoveFlightFromQueue( self.flights, flight )
  end

end

--- Tell player to wait outside the 10 NM zone until a Marshal stack is available.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
function AIRBOSS:_WaitPlayer( playerData )

  -- Check if flight is known to the airboss already.
  if playerData then

    -- Number of waiting flights
    local nwaiting = #self.Qwaiting

    -- Radio message: Stack is full.
    self:_MarshalCallStackFull( playerData.onboard, nwaiting )

    -- Add player flight to waiting queue.
    table.insert( self.Qwaiting, playerData )

    -- Set time stamp.
    playerData.time = timer.getAbsTime()

    -- Set step to waiting.
    playerData.step = AIRBOSS.PatternStep.WAITING
    playerData.warning = nil

    -- Set all flights in section to waiting.
    for _, _flight in pairs( playerData.section ) do
      local flight = _flight -- #AIRBOSS.PlayerData
      flight.step = AIRBOSS.PatternStep.WAITING
      flight.time = timer.getAbsTime()
      flight.warning = nil
    end

  end

end

--- Orbit at a specified position at a specified altitude with a specified speed.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @param #number stack The Marshal stack the player gets.
function AIRBOSS:_MarshalPlayer( playerData, stack )

  -- Check if flight is known to the airboss already.
  if playerData then

    -- Add group to marshal stack.
    self:_AddMarshalGroup( playerData, stack )

    -- Set step to holding.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.HOLDING )

    -- Holding switch to nil until player arrives in the holding zone.
    playerData.holding = nil

    -- Set same stack for all flights in section.
    for _, _flight in pairs( playerData.section ) do
      local flight = _flight -- #AIRBOSS.PlayerData

      -- XXX: Inform player? Should be done by lead via radio?

      -- Set step.
      self:_SetPlayerStep( flight, AIRBOSS.PatternStep.HOLDING )

      -- Holding to nil, until arrived.
      flight.holding = nil

      -- Set case to that of lead.
      flight.case = playerData.case

      -- Set stack flag.
      flight.flag = stack

      -- Trigger Marshal event.
      self:Marshal( flight )
    end

  else
    self:E( self.lid .. "ERROR: Could not add player to Marshal stack! playerData=nil" )
  end

end

--- Command AI flight to orbit outside the 10 NM zone and wait for a free Marshal stack.
-- If the flight is not already holding in the Marshal stack, it is guided there first.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight Flight group.
-- @param #boolean respawn If true respawn the group. Otherwise reset the mission task with new waypoints.
function AIRBOSS:_WaitAI( flight, respawn )

  -- Set flag to something other than -100 and <0
  flight.flag = -99

  -- Add AI flight to waiting queue.
  table.insert( self.Qwaiting, flight )

  -- Flight group name.
  local group = flight.group
  local groupname = flight.groupname

  -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.)
  local speedOrbitMps = UTILS.KnotsToMps( 274 )

  -- Orbit speed in km/h for waypoints.
  local speedOrbitKmh = UTILS.KnotsToKmph( 274 )

  -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.)
  local speedTransit = UTILS.KnotsToKmph( 370 )

  -- Carrier coordinate
  local cv = self:GetCoordinate()

  -- Coordinate of flight group
  local fc = group:GetCoordinate()

  -- Carrier heading
  local hdg = self:GetHeading( false )

  -- Heading from carrier to flight group
  local hdgto = cv:HeadingTo( fc )

  -- Holding altitude between angels 6 and 10 (random).
  local angels = math.random( 6, 10 )
  local altitude = UTILS.FeetToMeters( angels * 1000 )

  -- Point outsize 10 NM zone of the carrier.
  local p0 = cv:Translate( UTILS.NMToMeters( 11 ), hdgto ):Translate( UTILS.NMToMeters( 5 ), hdg ):SetAltitude( altitude )

  -- Waypoints array to be filled depending on case etc.
  local wp = {}

  -- Current position. Always good for as the first waypoint.
  wp[1] = group:GetCoordinate():WaypointAirTurningPoint( nil, speedTransit, {}, "Current Position" )

  -- Set orbit task.
  local taskorbit = group:TaskOrbit( p0, altitude, speedOrbitMps )

  -- Orbit at waypoint.
  wp[#wp + 1] = p0:WaypointAirTurningPoint( nil, speedOrbitKmh, { taskorbit }, string.format( "Waiting Orbit at Angels %d", angels ) )

  -- Debug markers.
  if self.Debug then
    p0:MarkToAll( string.format( "Waiting Orbit of flight %s at Angels %s", groupname, angels ) )
  end

  if respawn then

    -- This should clear the landing waypoints.
    -- Note: This resets the weapons and the fuel state. But not the units fortunately.

    -- Get group template.
    local Template = group:GetTemplate()

    -- Set route points.
    Template.route.points = wp

    -- Respawn the group.
    group = group:Respawn( Template, true )

  end

  -- Reinit waypoints.
  group:WayPointInitialize( wp )

  -- Route group.
  group:Route( wp, 1 )

end

--- Command AI flight to orbit at a specified position at a specified altitude with a specified speed. If flight is not in the Marshal queue yet, it is added. This fixes the recovery case.
-- If the flight is not already holding in the Marshal stack, it is guided there first.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight Flight group.
-- @param #number nstack Stack number of group. Can also be the current stack if AI position needs to be updated wrt to changed carrier position.
-- @param #boolean respawn If true, respawn the flight otherwise update mission task with new waypoints.
function AIRBOSS:_MarshalAI( flight, nstack, respawn )
  self:F2( { flight = flight, nstack = nstack, respawn = respawn } )

  -- Nil check.
  if flight == nil or flight.group == nil then
    self:E( self.lid .. "ERROR: flight or flight.group is nil." )
    return
  end

  -- Nil check.
  if flight.group:GetCoordinate() == nil then
    self:E( self.lid .. "ERROR: cannot get coordinate of flight group." )
    return
  end

  -- Check if flight is already in Marshal queue.
  if not self:_InQueue(self.Qmarshal,flight.group) then
    -- Simulate inbound call
    if self.xtVoiceOversAI then
      local leader = flight.group:GetUnits()[1]
      self:_MarshallInboundCall(leader, flight.onboard)
    end
    -- Add group to marshal stack queue.
    self:_AddMarshalGroup( flight, nstack )
  end

  -- Explode unit for testing. Worked!
  -- local u1=flight.group:GetUnit(1) --Wrapper.Unit#UNIT
  -- u1:Explode(500, 10)

  -- Recovery case.
  local case = flight.case

  -- Get old/current stack.
  local ostack = flight.flag

  -- Flight group name.
  local group = flight.group
  local groupname = flight.groupname

  -- Set new stack.
  flight.flag = nstack

  -- Current carrier position.
  local Carrier = self:GetCoordinate()

  -- Carrier heading.
  local hdg = self:GetHeading()

  -- Aircraft speed 274 knots TAS ~= 250 KIAS when orbiting the pattern. (Orbit expects m/s.)
  local speedOrbitMps = UTILS.KnotsToMps( 274 )

  -- Orbit speed in km/h for waypoints.
  local speedOrbitKmh = UTILS.KnotsToKmph( 274 )

  -- Aircraft speed 400 knots when transiting to holding zone. (Waypoint expects km/h.)
  local speedTransit = UTILS.KnotsToKmph( 370 )

  local altitude
  local p0 -- Core.Point#COORDINATE
  local p1 -- Core.Point#COORDINATE
  local p2 -- Core.Point#COORDINATE

  -- Get altitude and positions.
  altitude, p1, p2 = self:_GetMarshalAltitude( nstack, case )

  -- Waypoints array to be filled depending on case etc.
  local wp = {}

  -- If flight has not arrived in the holding zone, we guide it there.
  if not flight.holding then

    ----------------------
    -- Route to Holding --
    ----------------------

    -- Debug info.
    self:T( self.lid .. string.format( "Guiding AI flight %s to marshal stack %d-->%d.", groupname, ostack, nstack ) )

    -- Current position. Always good for as the first waypoint.
    wp[1] = group:GetCoordinate():WaypointAirTurningPoint( nil, speedTransit, {}, "Current Position" )

    -- Task function when arriving at the holding zone. This will set flight.holding=true.
    local TaskArrivedHolding = flight.group:TaskFunction( "AIRBOSS._ReachedHoldingZone", self, flight )

    -- Select case.
    if case == 1 then

      -- Initial point 7 NM and a bit port of carrier.
      local pE = Carrier:Translate( UTILS.NMToMeters( 7 ), hdg - 30 ):SetAltitude( altitude )

      -- Entry point 5 NM port and slightly astern the boat.
      p0 = Carrier:Translate( UTILS.NMToMeters( 5 ), hdg - 135 ):SetAltitude( altitude )

      -- Waypoint ahead of carrier's holding zone.
      wp[#wp + 1] = pE:WaypointAirTurningPoint( nil, speedTransit, { TaskArrivedHolding }, "Entering Case I Marshal Pattern" )

    else

      -- Get correct radial depending on recovery case including offset.
      local radial = self:GetRadial( case, false, true )

      -- Point in the middle of the race track and a 5 NM more port perpendicular.
      p0 = p2:Translate( UTILS.NMToMeters( 5 ), radial + 90, true ):Translate( UTILS.NMToMeters( 5 ), radial, true )

      -- Entering Case II/III marshal pattern waypoint.
      wp[#wp + 1] = p0:WaypointAirTurningPoint( nil, speedTransit, { TaskArrivedHolding }, "Entering Case II/III Marshal Pattern" )

    end

  else

    ------------------------
    -- In Marshal Pattern --
    ------------------------

    -- Debug info.
    self:T( self.lid .. string.format( "Updating AI flight %s at marshal stack %d-->%d.", groupname, ostack, nstack ) )

    -- Current position. Speed expected in km/h.
    wp[1] = group:GetCoordinate():WaypointAirTurningPoint( nil, speedOrbitKmh, {}, "Current Position" )

    -- Create new waypoint 0.2 Nm ahead of current positon.
    p0 = group:GetCoordinate():Translate( UTILS.NMToMeters( 0.2 ), group:GetHeading(), true )

  end

  -- Set orbit task.
  local taskorbit = group:TaskOrbit( p1, altitude, speedOrbitMps, p2 )

  -- Orbit at waypoint.
  wp[#wp + 1] = p0:WaypointAirTurningPoint( nil, speedOrbitKmh, { taskorbit }, string.format( "Marshal Orbit Stack %d", nstack ) )

  -- Debug markers.
  if self.Debug then
    p0:MarkToAll( "WP P0 " .. groupname )
    p1:MarkToAll( "RT P1 " .. groupname )
    p2:MarkToAll( "RT P2 " .. groupname )
  end

  if respawn then

    -- This should clear the landing waypoints.
    -- Note: This resets the weapons and the fuel state. But not the units fortunately.

    -- Get group template.
    local Template = group:GetTemplate()

    -- Set route points.
    Template.route.points = wp

    -- Respawn the group.
    flight.group = group:Respawn( Template, true )

  end

  -- Reinit waypoints.
  flight.group:WayPointInitialize( wp )

  -- Route group.
  flight.group:Route( wp, 1 )

  -- Trigger Marshal event.
  self:Marshal( flight )

end

--- Tell AI to refuel. Either at the recovery tanker or at the nearest divert airfield.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight Flight group.
function AIRBOSS:_RefuelAI( flight )

  -- Waypoints array.
  local wp = {}

  -- Current speed.
  local CurrentSpeed = flight.group:GetVelocityKMH()

  -- Current positon.
  wp[#wp + 1] = flight.group:GetCoordinate():WaypointAirTurningPoint( nil, CurrentSpeed, {}, "Current position" )

  -- Check if aircraft can be refueled.
  -- TODO: This should also depend on the tanker type AC.
  local refuelac=false
  local actype=flight.group:GetTypeName()
  if actype==AIRBOSS.AircraftCarrier.AV8B      or
     actype==AIRBOSS.AircraftCarrier.F14A      or
     actype==AIRBOSS.AircraftCarrier.F14B      or
     actype==AIRBOSS.AircraftCarrier.F14A_AI   or
     actype==AIRBOSS.AircraftCarrier.HORNET    or
     actype==AIRBOSS.AircraftCarrier.RHINOE    or
     actype==AIRBOSS.AircraftCarrier.RHINOF    or
     actype==AIRBOSS.AircraftCarrier.GROWLER   or
     actype==AIRBOSS.AircraftCarrier.FA18C     or
     actype==AIRBOSS.AircraftCarrier.S3B       or
     actype==AIRBOSS.AircraftCarrier.S3BTANKER then
     refuelac=true
  end

  -- Message.
  local text = ""

  -- Refuel or divert?
  if self.tanker and refuelac then

    -- Current Tanker position.
    local tankerpos = self.tanker.tanker:GetCoordinate()

    -- Task refueling.
    local TaskRefuel = flight.group:TaskRefueling()

    -- Task to go back to Marshal.
    local TaskMarshal = flight.group:TaskFunction( "AIRBOSS._TaskFunctionMarshalAI", self, flight )

    -- Waypoint with tasks.
    wp[#wp + 1] = tankerpos:WaypointAirTurningPoint( nil, CurrentSpeed, { TaskRefuel, TaskMarshal }, "Refueling" )

    -- Marshal Message.
    self:_MarshalCallGasAtTanker( flight.onboard )

  else

    ------------------------------
    -- Guide AI to divert field --
    ------------------------------

    -- Closest Airfield of the coalition.
    local divertfield = self:GetCoordinate():GetClosestAirbase( Airbase.Category.AIRDROME, self:GetCoalition() )

    -- Handle case where there is no divert field of the own coalition and try neutral instead.
    if divertfield == nil then
      divertfield = self:GetCoordinate():GetClosestAirbase( Airbase.Category.AIRDROME, 0 )
    end

    if divertfield then

      -- Coordinate.
      local divertcoord = divertfield:GetCoordinate()

      -- Landing waypoint.
      wp[#wp + 1] = divertcoord:WaypointAirLanding( UTILS.KnotsToKmph( 300 ), divertfield, {}, "Divert Field" )

      -- Marshal Message.
      self:_MarshalCallGasAtDivert( flight.onboard, divertfield:GetName() )

      -- Respawn!

      -- Get group template.
      local Template = flight.group:GetTemplate()

      -- Set route points.
      Template.route.points = wp

      -- Respawn the group.
      flight.group = flight.group:Respawn( Template, true )

    else
      -- Set flight to refueling so this is not called again.
      self:E( self.lid .. string.format( "WARNING: No recovery tanker or divert field available for group %s.", flight.groupname ) )
      flight.refueling = true
      return
    end

  end

  -- Reinit waypoints.
  flight.group:WayPointInitialize( wp )

  -- Route group.
  flight.group:Route( wp, 1 )

  -- Set refueling switch.
  flight.refueling = true

end

--- Tell AI to land on the carrier.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight Flight group.
function AIRBOSS:_LandAI( flight )

  -- Debug info.
  self:T( self.lid .. string.format( "Landing AI flight %s.", flight.groupname ) )

  -- NOTE: Looks like the AI needs to approach at the "correct" speed. If they are too fast, they fly an unnecessary circle to bleed of speed first.
  --       Unfortunately, the correct speed depends on the aircraft type!

  -- Aircraft speed when flying the pattern.
  local Speed = UTILS.KnotsToKmph( 200 )

  if   flight.actype == AIRBOSS.AircraftCarrier.HORNET
    or flight.actype == AIRBOSS.AircraftCarrier.FA18C
    or flight.actype == AIRBOSS.AircraftCarrier.RHINOE
    or flight.actype == AIRBOSS.AircraftCarrier.RHINOF
    or flight.actype == AIRBOSS.AircraftCarrier.GROWLER then
    Speed = UTILS.KnotsToKmph( 200 )
  elseif flight.actype == AIRBOSS.AircraftCarrier.E2D or flight.actype == AIRBOSS.AircraftCarrier.C2A then
    Speed = UTILS.KnotsToKmph( 150 )
  elseif flight.actype == AIRBOSS.AircraftCarrier.F14A_AI or flight.actype == AIRBOSS.AircraftCarrier.F14A or flight.actype == AIRBOSS.AircraftCarrier.F14B then
    Speed = UTILS.KnotsToKmph( 175 )
  elseif flight.actype == AIRBOSS.AircraftCarrier.S3B or flight.actype == AIRBOSS.AircraftCarrier.S3BTANKER then
    Speed = UTILS.KnotsToKmph( 140 )
  elseif flight.actype == AIRBOSS.AircraftCarrier.CORSAIR or flight.actype == AIRBOSS.AircraftCarrier.CORSAIR_CW then
    Speed = UTILS.KnotsToKmph( 100 )
  end

  -- Carrier position.
  local Carrier = self:GetCoordinate()

  -- Carrier heading.
  local hdg = self:GetHeading()

  -- Waypoints array.
  local wp = {}

  local CurrentSpeed = flight.group:GetVelocityKMH()

  -- Current positon.
  wp[#wp + 1] = flight.group:GetCoordinate():WaypointAirTurningPoint( nil, CurrentSpeed, {}, "Current position" )

  -- Altitude 800 ft. Looks like this works best.
  local alt = UTILS.FeetToMeters( 800 )

  -- Landing waypoint 5 NM behind carrier at 2000 ft = 610 meters ASL.
  wp[#wp + 1] = Carrier:Translate( UTILS.NMToMeters( 4 ), hdg - 160 ):SetAltitude( alt ):WaypointAirLanding( Speed, self.airbase, nil, "Landing" )
  -- wp[#wp+1]=Carrier:Translate(UTILS.NMToMeters(4), hdg-160):SetAltitude(alt):WaypointAirLandingReFu(Speed, self.airbase, nil, "Landing")

  -- wp[#wp+1]=self:GetCoordinate():Translate(UTILS.NMToMeters(3), hdg-160):SetAltitude(alt):WaypointAirTurningPoint(nil,Speed, {}, "Before Initial") ---WaypointAirLanding(Speed, self.airbase, nil, "Landing")
  -- wp[#wp+1]=self:GetCoordinate():WaypointAirLanding(Speed, self.airbase, nil, "Landing")

  -- Reinit waypoints.
  flight.group:WayPointInitialize( wp )

  -- Route group.
  flight.group:Route( wp, 0 )
end

--- Get marshal altitude and two positions of a counter-clockwise race track pattern.
-- @param #AIRBOSS self
-- @param #number stack Assigned stack number. Counting starts at one, i.e. stack=1 is the first stack.
-- @param #number case Recovery case. Default is self.case.
-- @return #number Holding altitude in meters.
-- @return Core.Point#COORDINATE First race track coordinate.
-- @return Core.Point#COORDINATE Second race track coordinate.
function AIRBOSS:_GetMarshalAltitude( stack, case )

  -- Stack <= 0.
  if stack <= 0 then
    return 0, nil, nil
  end

  -- Recovery case.
  case = case or self.case

  -- Carrier position.
  local Carrier = self:GetCoordinate()

  -- Altitude of first stack. Depends on recovery case.
  local angels0
  local Dist
  local p1 = nil -- Core.Point#COORDINATE
  local p2 = nil -- Core.Point#COORDINATE

  -- Stack number.
  local nstack = stack - 1

  if case == 1 then

    -- CASE I: Holding at 2000 ft on a circular pattern port of the carrier. Interval +1000 ft for next stack.
    angels0 = 2

    -- Get true heading of carrier.
    local hdg = self.carrier:GetHeading()

    -- For CCW pattern: First point astern, second ahead of the carrier.

    -- First point over carrier.
    p1 = Carrier

    -- Second point 1.5 NM ahead.
    p2 = Carrier:Translate( UTILS.NMToMeters( 1.5 ), hdg )

    -- Tarawa,LHA,LHD Delta patterns.
    if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then

      -- Pattern is directly overhead the carrier.
      p1 = Carrier:Translate( UTILS.NMToMeters( 1.0 ), hdg + 90 )
      p2 = p1:Translate( 2.5, hdg )

    end

  else

    -- CASE II/III: Holding at 6000 ft on a racetrack pattern astern the carrier.
    angels0 = 6

    -- Distance: d=n*angels0+15 NM, so first stack is at 15+6=21 NM
    Dist = UTILS.NMToMeters( nstack + angels0 + 15 )

    -- Get correct radial depending on recovery case including offset.
    local radial = self:GetRadial( case, false, true )

    -- For CCW pattern: p1 further astern than p2.

    -- Length of the race track pattern.
    local l = UTILS.NMToMeters( 10 )

    -- First point of race track pattern.
    p1 = Carrier:Translate( Dist + l, radial )

    -- Second point.
    p2 = Carrier:Translate( Dist, radial )

  end

  -- Pattern altitude.
  local altitude = UTILS.FeetToMeters( (nstack + angels0) * 1000 )

  -- Set altitude of coordinate.
  p1:SetAltitude( altitude, true )
  p2:SetAltitude( altitude, true )

  return altitude, p1, p2
end

--- Calculate an estimate of the charlie time of the player based on how many other aircraft are in the marshal or pattern queue before him.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flightgroup Flight data.
-- @return #number Charlie (abs) time in seconds. Or nil, if stack<0 or no recovery window will open.
function AIRBOSS:_GetCharlieTime( flightgroup )

  -- Get current stack of player.
  local stack = flightgroup.flag

  -- Flight is not in marshal stack.
  if stack <= 0 then
    return nil
  end

  -- Current abs time.
  local Tnow = timer.getAbsTime()

  -- Time the player has to spend in marshal stack until all lower stacks are emptied.
  local Tcharlie = 0

  local Trecovery = 0
  if self.recoverywindow then
    -- Time in seconds until the next recovery starts or 0 if window is already open.
    Trecovery = math.max( self.recoverywindow.START - Tnow, 0 )
  else
    -- Set ~7 min if no future recovery window is defined. Otherwise radio call function crashes.
    Trecovery = 7 * 60
  end

  -- Loop over flights currently in the marshal queue.
  for _, _flight in pairs( self.Qmarshal ) do
    local flight = _flight -- #AIRBOSS.FlightGroup

    -- Stack of marshal flight.
    local mstack = flight.flag

    -- Time to get to the marshal stack if not holding already.
    local Tarrive = 0

    -- Minimum holding time per stack.
    local Tholding = 3 * 60

    if stack > 0 and mstack > 0 and mstack <= stack then

      -- Check if flight is already holding or just on its way.
      if flight.holding == nil then
        -- Flight is on its way to the marshal stack.

        -- Coordinate of the holding zone.
        local holdingzone = self:_GetZoneHolding( flight.case, 1 ):GetCoordinate()

        -- Distance to holding zone.
        local d0 = holdingzone:Get2DDistance( flight.group:GetCoordinate() )

        -- Current velocity.
        local v0 = flight.group:GetVelocityMPS()

        -- Time to get to the carrier.
        Tarrive = d0 / v0

        self:T3( self.lid .. string.format( "Tarrive=%.1f seconds, Clock %s", Tarrive, UTILS.SecondsToClock( Tnow + Tarrive ) ) )

      else
        -- Flight is already holding.

        -- Next in line.
        if mstack == 1 then

          -- Current holding time. flight.time stamp should be when entering holding or last time the stack collapsed.
          local tholding = timer.getAbsTime() - flight.time

          -- Deduce current holding time. Ensure that is >=0.
          Tholding = math.max( 3 * 60 - tholding, 0 )
        end

      end

      -- This is the approx time needed to get to the pattern. If we are already there, it is the time until the recovery window opens or 0 if it is already open.
      local Tmin = math.max( Tarrive, Trecovery )

      -- Charlie time + 2 min holding in stack 1.
      Tcharlie = math.max( Tmin, Tcharlie ) + Tholding
    end

  end

  -- Convert to abs time.
  Tcharlie = Tcharlie + Tnow

  -- Debug info.
  local text = string.format( "Charlie time for flight %s (%s) %s", flightgroup.onboard, flightgroup.groupname, UTILS.SecondsToClock( Tcharlie ) )
  MESSAGE:New( text, 10, "DEBUG" ):ToAllIf( self.Debug )
  self:T( self.lid .. text )

  return Tcharlie
end

--- Add a flight group to the Marshal queue at a specific stack. Flight is informed via message. This fixes the recovery case to the current case ops in progress self.case).
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight Flight group.
-- @param #number stack Marshal stack. This (re-)sets the flag value.
function AIRBOSS:_AddMarshalGroup( flight, stack )

  -- Set flag value. This corresponds to the stack number which starts at 1.
  flight.flag = stack

  -- Set recovery case.
  flight.case = self.case

  -- Add to marshal queue.
  table.insert( self.Qmarshal, flight )

  -- Pressure.
  local P = UTILS.hPa2inHg( self:GetCoordinate():GetPressure() )

  -- Stack altitude.
  -- local alt=UTILS.MetersToFeet(self:_GetMarshalAltitude(stack, flight.case))
  local alt = self:_GetMarshalAltitude( stack, flight.case )

  -- Current BRC.
  local brc = self:GetBRC()

  -- If the carrier is supposed to turn into the wind, we take the wind coordinate.
  if self.recoverywindow and self.recoverywindow.WIND then
    brc = self:GetBRCintoWind(self.recoverywindow.SPEED)
  end

  -- Get charlie time estimate.
  flight.Tcharlie = self:_GetCharlieTime( flight )

  -- Convert to clock string.
  local Ccharlie = UTILS.SecondsToClock( flight.Tcharlie )

  -- Make sure brc is never above 360
  brc = brc % 360

  -- Combined marshal call.
  self:_MarshalCallArrived( flight.onboard, flight.case, brc, alt, Ccharlie, P )

  -- Hint about TACAN bearing.
  if self.TACANon and (not flight.ai) and flight.difficulty == AIRBOSS.Difficulty.EASY then
    -- Get inverse magnetic radial potential offset.
    local radial = self:GetRadial( flight.case, true, true, true )
    if flight.case == 1 then
      -- For case 1 we want the BRC but above routine return FB.
      radial = self:GetBRC()
    end
    local text = string.format( "Select TACAN %03d°, channel %d%s (%s)", radial, self.TACANchannel, self.TACANmode, self.TACANmorse )
    self:MessageToPlayer( flight, text, nil, "" )
  end

end

--- Collapse marshal stack.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight Flight that left the marshal stack.
-- @param #boolean nopattern If true, flight does not go to pattern.
function AIRBOSS:_CollapseMarshalStack( flight, nopattern )
  self:F2( { flight = flight, nopattern = nopattern } )

  -- Recovery case of flight.
  local case = flight.case

  -- Stack of flight.
  local stack = flight.flag

  -- Check that stack > 0.
  if stack <= 0 then
    self:E( self.lid .. string.format( "ERROR: Flight %s is has stack value %d<0. Cannot collapse stack!", flight.groupname, stack ) )
    return
  end

  -- Memorize time when stack collapsed. Should better depend on case but for now we assume there are no two different stacks Case I or II/III.
  self.Tcollapse = timer.getTime()

  -- Decrease flag values of all flight groups in marshal stack.
  for _, _flight in pairs( self.Qmarshal ) do
    local mflight = _flight -- #AIRBOSS.PlayerData

    -- Only collapse stack of which the flight left. CASE II/III stacks are not collapsed.
    if (case == 1 and mflight.case == 1) then -- or (case>1 and mflight.case>1) then

      -- Get current flag/stack value.
      local mstack = mflight.flag

      -- Only collapse stacks above the new pattern flight.
      if mstack > stack then

        -- TODO: Is this now right as we allow more flights per stack?
        -- Question is, does the stack collapse if the lower stack is completely empty or do aircraft descent if just one flight leaves.
        -- For now, assuming that the stack must be completely empty before the next higher AC are allowed to descent.
        local newstack = self:_GetFreeStack( mflight.ai, mflight.case, true )

        -- Free stack has to be below.
        if newstack and newstack < mstack then

          -- Debug info.
          self:T( self.lid .. string.format( "Collapse Marshal: Flight %s (case %d) is changing marshal stack %d --> %d.", mflight.groupname, mflight.case, mstack, newstack ) )

          if mflight.ai then

            -- Command AI to decrease stack. Flag is set in the routine.
            self:_MarshalAI( mflight, newstack )

          else

            -- Decrease stack/flag. Human player needs to take care himself.
            mflight.flag = newstack

            -- Angels of new stack.
            local angels = self:_GetAngels( self:_GetMarshalAltitude( newstack, case ) )

            -- Inform players.
            if mflight.difficulty ~= AIRBOSS.Difficulty.HARD then

              -- Send message to all non-pros that they can descent.
              local text = string.format( "descent to stack at Angels %d.", angels )
              self:MessageToPlayer( mflight, text, "MARSHAL" )

            end

            -- Set time stamp.
            mflight.time = timer.getAbsTime()

            -- Loop over section members.
            for _, _sec in pairs( mflight.section ) do
              local sec = _sec -- #AIRBOSS.PlayerData

              -- Also decrease flag for section members of flight.
              sec.flag = newstack

              -- Set new time stamp.
              sec.time = timer.getAbsTime()

              -- Inform section member.
              if sec.difficulty ~= AIRBOSS.Difficulty.HARD then
                local text = string.format( "descent to stack at Angels %d.", angels )
                self:MessageToPlayer( sec, text, "MARSHAL" )
              end

            end

          end

        end
      end
    end
  end

  if nopattern then

    -- Debug message.
    self:T( self.lid .. string.format( "Flight %s is leaving stack but not going to pattern.", flight.groupname ) )

  else

    -- Debug message.
    local Tmarshal = UTILS.SecondsToClock( timer.getAbsTime() - flight.time )
    self:T( self.lid .. string.format( "Flight %s is leaving marshal after %s and going pattern.", flight.groupname, Tmarshal ) )

    -- Add flight to pattern queue.
    self:_AddFlightToPatternQueue( flight )

  end

  -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42).
  flight.flag = -1

  -- New time stamp for time in pattern.
  flight.time = timer.getAbsTime()

end

--- Get next free Marshal stack. Depending on AI/human and recovery case.
-- @param #AIRBOSS self
-- @param #boolean ai If true, get a free stack for an AI flight group.
-- @param #number case Recovery case. Default current (self) case in progress.
-- @param #boolean empty Return lowest stack that is completely empty.
-- @return #number Lowest free stack available for the given case or nil if all Case I stacks are taken.
function AIRBOSS:_GetFreeStack( ai, case, empty )

  -- Recovery case.
  case = case or self.case

  if case == 1 then
    return self:_GetFreeStack_Old( ai, case, empty )
  end

  -- Max number of stacks available.
  local nmaxstacks = 100
  if case == 1 then
    nmaxstacks = self.Nmaxmarshal
  end

  -- Assume up to two (human) flights per stack. All are free.
  local stack = {}
  for i = 1, nmaxstacks do
    stack[i] = self.NmaxStack -- Number of human flights per stack.
  end

  local nmax = 1

  -- Loop over all flights in marshal stack.
  for _, _flight in pairs( self.Qmarshal ) do
    local flight = _flight -- #AIRBOSS.FlightGroup

    -- Check that the case is right.
    if flight.case == case then

      -- Get stack of flight.
      local n = flight.flag

      if n > nmax then
        nmax = n
      end

      if n > 0 then
        if flight.ai or flight.case > 1 then
          stack[n] = 0 -- AI get one stack on their own. Also CASE II/III get one stack each.
        else
          stack[n] = stack[n] - 1
        end
      else
        self:E( string.format( "ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.", flight.groupname, n ) )
      end

    end
  end

  local nfree = nil
  if stack[nmax] == 0 then
    -- Max occupied stack is completely full!
    if case == 1 then
      if nmax >= nmaxstacks then
        -- Already all Case I stacks are occupied ==> wait outside 10 NM zone.
        nfree = nil
      else
        -- Return next free stack.
        nfree = nmax + 1
      end
    else
      -- Case II/III return next stack
      nfree = nmax + 1
    end

  elseif stack[nmax] == self.NmaxStack then
    -- Max occupied stack is completely empty! This should happen only when there is no other flight in the marshal queue.
    self:E( self.lid .. string.format( "ERROR: Max occupied stack is empty. Should not happen! Nmax=%d, stack[nmax]=%d", nmax, stack[nmax] ) )
    nfree = nmax
  else
    -- Max occupied stack is partly full.
    if ai or empty or case > 1 then
      nfree = nmax + 1
    else
      nfree = nmax
    end

  end

  self:T( self.lid .. string.format( "Returning free stack %s", tostring( nfree ) ) )
  return nfree
end

--- Get next free Marshal stack. Depending on AI/human and recovery case.
-- @param #AIRBOSS self
-- @param #boolean ai If true, get a free stack for an AI flight group.
-- @param #number case Recovery case. Default current (self) case in progress.
-- @param #boolean empty Return lowest stack that is completely empty.
-- @return #number Lowest free stack available for the given case or nil if all Case I stacks are taken.
function AIRBOSS:_GetFreeStack_Old( ai, case, empty )

  -- Recovery case.
  case = case or self.case

  -- Max number of stacks available.
  local nmaxstacks = 100
  if case == 1 then
    nmaxstacks = self.Nmaxmarshal
  end

  -- Assume up to two (human) flights per stack. All are free.
  local stack = {}
  for i = 1, nmaxstacks do
    stack[i] = self.NmaxStack -- Number of human flights per stack.
  end

  -- Loop over all flights in marshal stack.
  for _, _flight in pairs( self.Qmarshal ) do
    local flight = _flight -- #AIRBOSS.FlightGroup

    -- Check that the case is right.
    if flight.case == case then

      -- Get stack of flight.
      local n = flight.flag

      if n > 0 then
        if flight.ai or flight.case > 1 then
          stack[n] = 0 -- AI get one stack on their own. Also CASE II/III get one stack each.
        else
          stack[n] = stack[n] - 1
        end
      else
        self:E( string.format( "ERROR: Flight %s in marshal stack has stack value <= 0. Stack value is %d.", flight.groupname, n ) )
      end

    end
  end

  -- Loop over stacks and check which one has a place left.
  local nfree = nil
  for i = 1, nmaxstacks do
    self:T2( self.lid .. string.format( "FF Stack[%d]=%d", i, stack[i] ) )
    if ai or empty or case > 1 then
      -- AI need the whole stack.
      if stack[i] == self.NmaxStack then
        nfree = i
        return i
      end
    else
      -- Human players only need one free spot.
      if stack[i] > 0 then
        nfree = i
        return i
      end
    end
  end

  return nfree
end

--- Get number of (airborne) units in a flight.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight The flight group.
-- @param #boolean onground If true, include units on the ground. By default only airborne units are counted.
-- @return #number Number of units in flight including section members.
-- @return #number Number of units in flight excluding section members.
-- @return #number Number of section members.
function AIRBOSS:_GetFlightUnits( flight, onground )

  -- Default is only airborne.
  local inair = true
  if onground == true then
    inair = false
  end

  --- Count units of a group which are alive and in the air.
  local function countunits( _group, inair )
    local group = _group -- Wrapper.Group#GROUP
    local units = group:GetUnits()
    local n = 0
    if units then
      for _, _unit in pairs( units ) do
        local unit = _unit -- Wrapper.Unit#UNIT
        if unit and unit:IsAlive() then
          if inair then
            -- Only count units in air.
            if unit:InAir() then
              self:T2( self.lid .. string.format( "Unit %s is in AIR", unit:GetName() ) )
              n = n + 1
            end
          else
            -- Count units in air or on the ground.
            n = n + 1
          end
        end
      end
    end
    return n
  end

  -- Count units of the group itself (alive units in air).
  local nunits = countunits( flight.group, inair )

  -- Count section members.
  local nsection = 0
  for _, sec in pairs( flight.section ) do
    local secflight = sec -- #AIRBOSS.PlayerData
    -- Count alive units in air.
    nsection = nsection + countunits( secflight.group, inair )
  end

  return nunits + nsection, nunits, nsection
end

--- Get number of groups and units in queue, which are alive and airborne. In units we count the section members as well.
-- @param #AIRBOSS self
-- @param #table queue The queue. Can be self.flights, self.Qmarshal or self.Qpattern.
-- @param #number case (Optional) Only count flights, which are in a specific recovery case. Note that you can use case=23 for flights that are either in Case II or III. By default all groups/units regardless of case are counted.
-- @return #number Total number of flight groups in queue.
-- @return #number Total number of aircraft in queue since each flight group can contain multiple aircraft.
function AIRBOSS:_GetQueueInfo( queue, case )

  local ngroup = 0
  local Nunits = 0

  -- Loop over flight groups.
  for _, _flight in pairs( queue ) do
    local flight = _flight -- #AIRBOSS.FlightGroup

    -- Check if a specific case was requested.
    if case then

      ------------------------------------------------------------------------
      -- Only count specific case with special 23 = CASE II and III combined.
      ------------------------------------------------------------------------

      if (flight.case == case) or (case == 23 and (flight.case == 2 or flight.case == 3)) then

        -- Number of total units, units in flight and section members ALIVE and AIRBORNE.
        local ntot, nunits, nsection = self:_GetFlightUnits( flight )

        -- Add up total unit number.
        Nunits = Nunits + ntot

        -- Increase group count.
        if ntot > 0 then
          ngroup = ngroup + 1
        end

      end

    else

      ---------------------------------------------------------------------------
      -- No specific case requested. Count all groups & units in selected queue.
      ---------------------------------------------------------------------------

      -- Number of total units, units in flight and section members ALIVE and AIRBORNE.
      local ntot, nunits, nsection = self:_GetFlightUnits( flight )

      -- Add up total unit number.
      Nunits = Nunits + ntot

      -- Increase group count.
      if ntot > 0 then
        ngroup = ngroup + 1
      end

    end

  end

  return ngroup, Nunits
end

--- Print holding queue.
-- @param #AIRBOSS self
-- @param #table queue Queue to print.
-- @param #string name Queue name.
function AIRBOSS:_PrintQueue( queue, name )

  -- local nqueue=#queue
  local Nqueue, nqueue = self:_GetQueueInfo( queue )

  local text = string.format( "%s Queue N=%d (#%d), n=%d:", name, Nqueue, #queue, nqueue )
  if #queue == 0 then
    text = text .. " empty."
  else
    for i, _flight in pairs( queue ) do
      local flight = _flight -- #AIRBOSS.FlightGroup

      local clock = UTILS.SecondsToClock( timer.getAbsTime() - flight.time )
      local case = flight.case
      local stack = flight.flag
      local fuel = flight.group:GetFuelMin() * 100
      local ai = tostring( flight.ai )
      local lead = flight.seclead
      local Nsec = #flight.section
      local actype = self:_GetACNickname( flight.actype )
      local onboard = flight.onboard
      local holding = tostring( flight.holding )

      -- Airborne units.
      local _, nunits, nsec = self:_GetFlightUnits( flight, false )

      -- Text.
      text = text .. string.format( "\n[%d] %s*%d (%s): lead=%s (%d/%d), onboard=%s, flag=%d, case=%d, time=%s, fuel=%d, ai=%s, holding=%s", i, flight.groupname, nunits, actype, lead, nsec, Nsec, onboard, stack, case, clock, fuel, ai, holding )
      if stack > 0 then
        local alt = UTILS.MetersToFeet( self:_GetMarshalAltitude( stack, case ) )
        text = text .. string.format( " stackalt=%d ft", alt )
      end
      for j, _element in pairs( flight.elements ) do
        local element = _element -- #AIRBOSS.FlightElement
        text = text .. string.format( "\n  (%d) %s (%s): ai=%s, ballcall=%s, recovered=%s", j, element.onboard, element.unitname, tostring( element.ai ), tostring( element.ballcall ), tostring( element.recovered ) )
      end
    end
  end
  self:T( self.lid .. text )
end

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FLIGHT & PLAYER functions
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new flight group. Usually when a flight appears in the CCA.
-- @param #AIRBOSS self
-- @param Wrapper.Group#GROUP group Aircraft group.
-- @return #AIRBOSS.FlightGroup Flight group.
function AIRBOSS:_CreateFlightGroup( group )

  -- Debug info.
  self:T( self.lid .. string.format( "Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName() ) )

  -- New flight.
  local flight = {} -- #AIRBOSS.FlightGroup

  -- Check if not already in flights
  if not self:_InQueue( self.flights, group ) then

    -- Flight group name
    local groupname = group:GetName()
    local human, playername = self:_IsHuman( group )

    -- Queue table item.
    flight.group = group
    flight.groupname = group:GetName()
    flight.nunits = #group:GetUnits()
    flight.time = timer.getAbsTime()
    flight.dist0 = group:GetCoordinate():Get2DDistance( self:GetCoordinate() )
    flight.flag = -100
    flight.ai = not human
    flight.actype = group:GetTypeName()
    flight.onboardnumbers = self:_GetOnboardNumbers( group )
    flight.seclead = flight.group:GetUnit( 1 ):GetName() -- Sec lead is first unitname of group but player name for players.
    flight.section = {}
    flight.ballcall = false
    flight.refueling = false
    flight.holding = nil
    flight.name = flight.group:GetUnit( 1 ):GetName() -- Will be overwritten in _Newplayer with player name if human player in the group.

    -- Note, this should be re-set elsewhere!
    flight.case = self.case

    -- Flight elements.
    local text = string.format( "Flight elements of group %s:", flight.groupname )
    flight.elements = {}
    local units = group:GetUnits()
    for i, _unit in pairs( units ) do
      local unit = _unit -- Wrapper.Unit#UNIT
      local element = {} -- #AIRBOSS.FlightElement
      element.unit = unit
      element.unitname = unit:GetName()
      element.onboard = flight.onboardnumbers[element.unitname]
      element.ballcall = false
      element.ai = not self:_IsHumanUnit( unit )
      element.recovered = nil
      text = text .. string.format( "\n[%d] %s onboard #%s, AI=%s", i, element.unitname, tostring( element.onboard ), tostring( element.ai ) )
      table.insert( flight.elements, element )
    end
    self:T( self.lid .. text )

    -- Onboard
    if flight.ai then
      local onboard = flight.onboardnumbers[flight.seclead]
      flight.onboard = onboard
    else
      flight.onboard = self:_GetOnboardNumberPlayer( group )
    end

    -- Add to known flights.
    table.insert( self.flights, flight )

  else
    self:E( self.lid .. string.format( "ERROR: Flight group %s already exists in self.flights!", group:GetName() ) )
    return nil
  end

  return flight
end

--- Initialize player data after birth event of player unit.
-- @param #AIRBOSS self
-- @param #string unitname Name of the player unit.
-- @return #AIRBOSS.PlayerData Player data.
function AIRBOSS:_NewPlayer( unitname )

  -- Get player unit and name.
  local playerunit, playername = self:_GetPlayerUnitAndName( unitname )

  if playerunit and playername then

    -- Get group.
    local group = playerunit:GetGroup()

    -- Player data.
    local playerData -- #AIRBOSS.PlayerData

    -- Create a flight group for the player.
    playerData = self:_CreateFlightGroup( group )

    -- Nil check.
    if playerData then

      -- Player unit, client and callsign.
      playerData.unit = playerunit
      playerData.unitname = unitname
      playerData.name = playername
      playerData.callsign = playerData.unit:GetCallsign()
      playerData.client = CLIENT:FindByName( unitname, nil, true )
      playerData.seclead = playername

      -- Number of passes done by player in this slot.
      playerData.passes = 0 -- playerData.passes or 0

      -- Messages for player.
      playerData.messages = {}

      -- Debriefing tables.
      playerData.lastdebrief = playerData.lastdebrief or {}

      -- Attitude monitor.
      playerData.attitudemonitor = false

      -- Trap sheet save.
      if playerData.trapon == nil then
        playerData.trapon = self.trapsheet
      end

      -- Set difficulty level.
      playerData.difficulty = playerData.difficulty or self.defaultskill

      -- Subtitles of player.
      if playerData.subtitles == nil then
        playerData.subtitles = true
      end

      -- Show step hints.
      if playerData.showhints == nil then
        if playerData.difficulty == AIRBOSS.Difficulty.HARD then
          playerData.showhints = false
        else
          playerData.showhints = true
        end
      end

      -- Points rewarded.
      playerData.points = {}

      -- Init stuff for this round.
      playerData = self:_InitPlayer( playerData )

      -- Init player data.
      self.players[playername] = playerData

      -- Init player grades table if necessary.
      self.playerscores[playername] = self.playerscores[playername] or {}

      -- Welcome player message.
      if self.welcome then
        self:MessageToPlayer( playerData, string.format( "Welcome, %s %s!", playerData.difficulty, playerData.name ), string.format( "AIRBOSS %s", self.alias ), "", 5 )
      end

    end

    -- Return player data table.
    return playerData
  end

  return nil
end

--- Initialize player data by (re-)setting parmeters to initial values.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @param #string step (Optional) New player step. Default UNDEFINED.
-- @return #AIRBOSS.PlayerData Initialized player data.
function AIRBOSS:_InitPlayer( playerData, step )
  self:T( self.lid .. string.format( "Initializing player data for %s callsign %s.", playerData.name, playerData.callsign ) )

  playerData.step = step or AIRBOSS.PatternStep.UNDEFINED
  playerData.groove = {}
  playerData.debrief = {}
  playerData.trapsheet = {}
  playerData.warning = nil
  playerData.holding = nil
  playerData.refueling = false
  playerData.valid = false
  playerData.lig = false
  playerData.wop = false
  playerData.waveoff = false
  playerData.wofd = false
  playerData.owo = false
  playerData.boltered = false
  playerData.hover = false
  playerData.stable = false
  playerData.landed = false
  playerData.Tlso = timer.getTime()
  playerData.Tgroove = nil
  playerData.TIG0 = 0 --changed to prevent errors in script when player is not in correct spot
  playerData.wire = nil
  playerData.flag = -100
  playerData.debriefschedulerID = nil

  -- Set us up on final if group name contains "Groove". But only for the first pass.
  if playerData.group:GetName():match( "Groove" ) and playerData.passes == 0 then
    self:MessageToPlayer( playerData, "Group name contains \"Groove\". Happy groove testing." )
    playerData.attitudemonitor = true
    playerData.step = AIRBOSS.PatternStep.FINAL
    self:_AddFlightToPatternQueue( playerData )
    self.dTstatus = 0.1
  end

  return playerData
end

--- Get flight from group in a queue.
-- @param #AIRBOSS self
-- @param Wrapper.Group#GROUP group Group that will be removed from queue.
-- @param #table queue The queue from which the group will be removed.
-- @return #AIRBOSS.FlightGroup Flight group or nil.
-- @return #number Queue index or nil.
function AIRBOSS:_GetFlightFromGroupInQueue( group, queue )

  if group then

    -- Group name
    local name = group:GetName()

    -- Loop over all flight groups in queue
    for i, _flight in pairs( queue ) do
      local flight = _flight -- #AIRBOSS.FlightGroup

      if flight.groupname == name then
        return flight, i
      end
    end

    self:T2( self.lid .. string.format( "WARNING: Flight group %s could not be found in queue.", name ) )
  end

  self:T2( self.lid .. string.format( "WARNING: Flight group could not be found in queue. Group is nil!" ) )
  return nil, nil
end

--- Get element in flight.
-- @param #AIRBOSS self
-- @param #string unitname Name of the unit.
-- @return #AIRBOSS.FlightElement Element of the flight or nil.
-- @return #number Element index or nil.
-- @return #AIRBOSS.FlightGroup The Flight group or nil
function AIRBOSS:_GetFlightElement( unitname )

  -- Get the unit.
  local unit = UNIT:FindByName( unitname )

  -- Check if unit exists.
  if unit then

    -- Get flight element from all flights.
    local flight = self:_GetFlightFromGroupInQueue( unit:GetGroup(), self.flights )

    -- Check if fight exists.
    if flight then

      -- Loop over all elements in flight group.
      for i, _element in pairs( flight.elements ) do
        local element = _element -- #AIRBOSS.FlightElement

        if element.unit:GetName() == unitname then
          return element, i, flight
        end
      end

      self:T2( self.lid .. string.format( "WARNING: Flight element %s could not be found in flight group.", unitname, flight.groupname ) )
    end
  end

  return nil, nil, nil
end

--- Get element in flight.
-- @param #AIRBOSS self
-- @param #string unitname Name of the unit.
-- @return #boolean If true, element could be removed or nil otherwise.
function AIRBOSS:_RemoveFlightElement( unitname )

  -- Get table index.
  local element, idx, flight = self:_GetFlightElement( unitname )

  if idx then
    table.remove( flight.elements, idx )
    return true
  else
    self:T( "WARNING: Flight element could not be removed from flight group. Index=nil!" )
    return nil
  end
end

--- Check if a group is in a queue.
-- @param #AIRBOSS self
-- @param #table queue The queue to check.
-- @param Wrapper.Group#GROUP group The group to be checked.
-- @return #boolean If true, group is in the queue. False otherwise.
function AIRBOSS:_InQueue( queue, group )
  local name = group:GetName()
  for _, _flight in pairs( queue ) do
    local flight = _flight -- #AIRBOSS.FlightGroup
    if name == flight.groupname then
      return true
    end
  end
  return false
end

--- Remove dead flight groups from all queues.
-- @param #AIRBOSS self
-- @param Wrapper.Group#GROUP group Aircraft group.
-- @return #AIRBOSS.FlightGroup Flight group.
function AIRBOSS:_RemoveDeadFlightGroups()

  -- Remove dead flights from all flights table.
  for i = #self.flight, 1, -1 do
    local flight = self.flights[i] -- #AIRBOSS.FlightGroup
    if not flight.group:IsAlive() then
      self:T( string.format( "Removing dead flight group %s from ALL flights table.", flight.groupname ) )
      table.remove( self.flights, i )
    end
  end

  -- Remove dead flights from Marhal queue table.
  for i = #self.Qmarshal, 1, -1 do
    local flight = self.Qmarshal[i] -- #AIRBOSS.FlightGroup
    if not flight.group:IsAlive() then
      self:T( string.format( "Removing dead flight group %s from Marshal Queue table.", flight.groupname ) )
      table.remove( self.Qmarshal, i )
    end
  end

  -- Remove dead flights from Pattern queue table.
  for i = #self.Qpattern, 1, -1 do
    local flight = self.Qpattern[i] -- #AIRBOSS.FlightGroup
    if not flight.group:IsAlive() then
      self:T( string.format( "Removing dead flight group %s from Pattern Queue table.", flight.groupname ) )
      table.remove( self.Qpattern, i )
    end
  end

end

--- Get the lead flight group of a flight group.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight Flight group to check.
-- @return #AIRBOSS.FlightGroup Flight group of the leader or flight itself if no other leader.
function AIRBOSS:_GetLeadFlight( flight )

  -- Init.
  local lead = flight

  -- Only human players can be section leads of other players.
  if flight.name ~= flight.seclead then
    lead = self.players[flight.seclead]
  end

  return lead
end

--- Check if all elements of a flight were recovered. This also checks potential section members.
-- If so, flight is removed from the queue.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight Flight group to check.
-- @return #boolean If true, all elements landed.
function AIRBOSS:_CheckSectionRecovered( flight )

  -- Nil check.
  if flight == nil then
    return true
  end

  -- Get the lead flight first, so that we can also check all section members.
  local lead = self:_GetLeadFlight( flight )

  -- Check all elements of the lead flight group.
  for _, _element in pairs( lead.elements ) do
    local element = _element -- #AIRBOSS.FlightElement
    if not element.recovered then
      return false
    end
  end

  -- Now check all section members, if any.
  for _, _section in pairs( lead.section ) do
    local sectionmember = _section -- #AIRBOSS.FlightGroup

    -- Check all elements of the secmember flight group.
    for _, _element in pairs( sectionmember.elements ) do
      local element = _element -- #AIRBOSS.FlightElement
      if not element.recovered then
        return false
      end
    end
  end

  -- Remove lead flight from pattern queue. It is this flight who is added to the queue.
  self:_RemoveFlightFromQueue( self.Qpattern, lead )

  -- Just for now, check if it is in other queues as well.
  if self:_InQueue( self.Qmarshal, lead.group ) then
    self:E( self.lid .. string.format( "ERROR: lead flight group %s should not be in marshal queue", lead.groupname ) )
    self:_RemoveFlightFromMarshalQueue( lead, true )
  end
  -- Just for now, check if it is in other queues as well.
  if self:_InQueue( self.Qwaiting, lead.group ) then
    self:E( self.lid .. string.format( "ERROR: lead flight group %s should not be in pattern queue", lead.groupname ) )
    self:_RemoveFlightFromQueue( self.Qwaiting, lead )
  end

  return true
end

--- Add flight to pattern queue and set recoverd to false for all elements of the flight and its section members.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup Flight group of element.
function AIRBOSS:_AddFlightToPatternQueue( flight )

  -- Add flight to table.
  table.insert( self.Qpattern, flight )

  -- Set flag to -1 (-1 is rather arbitrary but it should not be positive or -100 or -42).
  flight.flag = -1
  -- New time stamp for time in pattern.
  flight.time = timer.getAbsTime()

  -- Init recovered switch.
  flight.recovered = false
  for _, elem in pairs( flight.elements ) do
    elem.recoverd = false
  end

  -- Set recovered for all section members.
  for _, sec in pairs( flight.section ) do
    -- Set flag and timestamp for section members
    sec.flag = -1
    sec.time = timer.getAbsTime()
    for _, elem in pairs( sec.elements ) do
      elem.recoverd = false
    end
  end
end

--- Sets flag recovered=true for a flight element, which was successfully recovered (landed).
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit The aircraft unit that was recovered.
-- @return #AIRBOSS.FlightGroup Flight group of element.
function AIRBOSS:_RecoveredElement( unit )

  -- Get element of flight.
  local element, idx, flight = self:_GetFlightElement( unit:GetName() ) -- #AIRBOSS.FlightElement

  -- Nil check. Could be if a helo landed or something else we dont know!
  if element then
    element.recovered = true
  end

  return flight
end

--- Remove a flight group from the Marshal queue. Marshal stack is collapsed, too, if flight was in the queue. Waiting flights are send to marshal.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight Flight group that will be removed from queue.
-- @param #boolean nopattern If true, flight is NOT going to landing pattern.
-- @return #boolean True, flight was removed or false otherwise.
-- @return #number Table index of the flight in the Marshal queue.
function AIRBOSS:_RemoveFlightFromMarshalQueue( flight, nopattern )

  -- Remove flight from marshal queue if it is in.
  local removed, idx = self:_RemoveFlightFromQueue( self.Qmarshal, flight )

  -- Collapse marshal stack if flight was removed.
  if removed then

    -- Flight is not holding any more.
    flight.holding = nil

    -- Collapse marshal stack if flight was removed.
    self:_CollapseMarshalStack( flight, nopattern )

    -- Stacks are only limited for Case I.
    if flight.case == 1 and #self.Qwaiting > 0 then

      -- Next flight in line waiting.
      local nextflight = self.Qwaiting[1] -- #AIRBOSS.FlightGroup

      -- Get free stack.
      local freestack = self:_GetFreeStack( nextflight.ai )

      -- Send next flight to marshal stack.
      if nextflight.ai then

        -- Send AI to Marshal Stack.
        self:_MarshalAI( nextflight, freestack )

      else

        -- Send player to Marshal stack.
        self:_MarshalPlayer( nextflight, freestack )

      end

      -- Remove flight from waiting queue.
      self:_RemoveFlightFromQueue( self.Qwaiting, nextflight )

    end
  end

  return removed, idx
end

--- Remove a flight group from a queue.
-- @param #AIRBOSS self
-- @param #table queue The queue from which the group will be removed.
-- @param #AIRBOSS.FlightGroup flight Flight group that will be removed from queue.
-- @return #boolean True, flight was in Queue and removed. False otherwise.
-- @return #number Table index of removed queue element or nil.
function AIRBOSS:_RemoveFlightFromQueue( queue, flight )

  -- Loop over all flights in group.
  for i, _flight in pairs( queue ) do
    local qflight = _flight -- #AIRBOSS.FlightGroup

    -- Check for name.
    if qflight.groupname == flight.groupname then
      self:T( self.lid .. string.format( "Removing flight group %s from queue.", flight.groupname ) )
      table.remove( queue, i )
      return true, i
    end
  end

  return false, nil
end

--- Remove a unit and its element from a flight group (e.g. when landed) and update all queues if the whole flight group is gone.
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit The unit to be removed.
function AIRBOSS:_RemoveUnitFromFlight( unit )

  -- Check if unit exists.
  if unit and unit:IsInstanceOf( "UNIT" ) then

    -- Get group.
    local group = unit:GetGroup()

    -- Check if group exists.
    if group then

      -- Get flight.
      local flight = self:_GetFlightFromGroupInQueue( group, self.flights )

      -- Check if flight exists.
      if flight then

        -- Remove element from flight group.
        local removed = self:_RemoveFlightElement( unit:GetName() )

        if removed then

          -- Get number of units (excluding section members). For AI only those that are still in air as we assume once they landed, they are out of the game.
          local _, nunits = self:_GetFlightUnits( flight, not flight.ai )

          -- Number of flight elements still left.
          local nelements = #flight.elements

          -- Debug info.
          self:T( self.lid .. string.format( "Removed unit %s: nunits=%d, nelements=%d", unit:GetName(), nunits, nelements ) )

          -- Check if no units are left.
          if nunits == 0 or nelements == 0 then
            -- Remove flight from all queues.
            self:_RemoveFlight( flight )
          end

        end
      end
    end
  end

end

--- Remove a flight, which is a member of a section, from this section.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight The flight to be removed from the section
function AIRBOSS:_RemoveFlightFromSection( flight )

  -- First check if player is not the lead.
  if flight.name ~= flight.seclead then

    -- Remove this flight group from the section of the leader.
    local lead = self.players[flight.seclead] -- #AIRBOSS.FlightGroup
    if lead then
      for i, sec in pairs( lead.section ) do
        local sectionmember = sec -- #AIRBOSS.FlightGroup
        if sectionmember.name == flight.name then
          table.remove( lead.section, i )
          break
        end
      end
    end
  end

end

--- Update section if a flight is removed.
-- If removed flight is member of a section, he is removed for the leaders section.
-- If removed flight is the section lead, we try to find a new leader.
-- @param #AIRBOSS self
-- @param #AIRBOSS.FlightGroup flight The flight to be removed.
function AIRBOSS:_UpdateFlightSection( flight )

  -- Check if this player is the leader of a section.
  if flight.seclead == flight.name then

    --------------------
    -- Section Leader --
    --------------------

    -- This player is the leader ==> We need a new one.
    if #flight.section >= 1 then

      -- New leader.
      local newlead = flight.section[1] -- #AIRBOSS.FlightGroup
      newlead.seclead = newlead.name

      -- Adjust new section members.
      for i = 2, #flight.section do
        local member = flight.section[i] -- #AIRBOSS.FlightGroup

        -- Add remaining members new leaders table.
        table.insert( newlead.section, member )

        -- Set new section lead of member.
        member.seclead = newlead.name
      end

    end

    -- Flight section empty
    flight.section = {}

  else

    --------------------
    -- Section Member --
    --------------------

    -- Remove flight from its leaders section.
    self:_RemoveFlightFromSection( flight )

  end

end

--- Remove a flight from Marshal, Pattern and Waiting queues. If flight is in Marhal queue, the above stack is collapsed.
-- Also set player step to undefined if applicable or remove human flight if option *completely* is true.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData flight The flight to be removed.
-- @param #boolean completely If true, also remove human flight from all flights table.
function AIRBOSS:_RemoveFlight( flight, completely )
  self:F( self.lid .. string.format( "Removing flight %s, ai=%s completely=%s.", tostring( flight.groupname ), tostring( flight.ai ), tostring( completely ) ) )

  -- Remove flight from all queues.
  self:_RemoveFlightFromMarshalQueue( flight, true )
  self:_RemoveFlightFromQueue( self.Qpattern, flight )
  self:_RemoveFlightFromQueue( self.Qwaiting, flight )
  self:_RemoveFlightFromQueue( self.Qspinning, flight )

  -- Check if player or AI
  if flight.ai then

    -- Remove AI flight completely. Pure AI flights have no sections and cannot be members.
    self:_RemoveFlightFromQueue( self.flights, flight )

  else

    -- Remove all grades until a final grade is reached.
    local grades = self.playerscores[flight.name]
    if grades and #grades > 0 then
      while #grades > 0 and grades[#grades].finalscore == nil do
        table.remove( grades, #grades )
      end
    end

    -- Check if flight should be completely removed, e.g. after the player died or simply left the slot.
    if completely then

      -- Update flight section. Remove flight from section or find new section leader if flight was the lead.
      self:_UpdateFlightSection( flight )

      -- Remove completely.
      self:_RemoveFlightFromQueue( self.flights, flight )

      -- Remove player from players table.
      local playerdata = self.players[flight.name]
      if playerdata then
        self:T( self.lid .. string.format( "Removing player %s completely.", flight.name ) )
        self.players[flight.name] = nil
      end

      -- Remove flight.
      flight = nil

    else

      -- Set player step to undefined.
      self:_SetPlayerStep( flight, AIRBOSS.PatternStep.UNDEFINED )

      -- Also set this for the section members as they are in the same boat.
      for _, sectionmember in pairs( flight.section ) do
        self:_SetPlayerStep( sectionmember, AIRBOSS.PatternStep.UNDEFINED )
        -- Also remove section member in case they are in the spinning queue.
        self:_RemoveFlightFromQueue( self.Qspinning, sectionmember )
      end

      -- What if flight is member of a section. His status is now undefined. Should he be removed from the section?
      -- I think yes, if he pulls the trigger.
      self:_RemoveFlightFromSection( flight )

    end
  end

end

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Player Status
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check current player status.
-- @param #AIRBOSS self
function AIRBOSS:_CheckPlayerStatus()
    -- Loop over all players.
  for _playerName, _playerData in pairs( self.players ) do
    local playerData = _playerData -- #AIRBOSS.PlayerData

    if playerData then

      -- Player unit.
      local unit = playerData.unit

      -- Check if unit is alive.
      if unit and unit:IsAlive() then

        -- Check if player is in carrier controlled area (zone with R=50 NM around the carrier).
        -- TODO: This might cause problems if the CCA is set to be very small!
        if unit:IsInZone( self.zoneCCA ) then

          -- VNAO Edit - Added wrapped up call to LSO grading
          if playerData.step==AIRBOSS.PatternStep.WAKE then-- VNAO Edit - Added
            if math.abs(playerData.unit:GetRoll())>35 and math.abs(playerData.unit:GetRoll())<=40 then-- VNAO Edit - Added
              playerData.wrappedUpAtWakeLittle = true -- VNAO Edit - Added
            elseif math.abs(playerData.unit:GetRoll()) >40 and math.abs(playerData.unit:GetRoll())<=45 then-- VNAO Edit - Added
              playerData.wrappedUpAtWakeFull = true-- VNAO Edit - Added
            elseif math.abs(playerData.unit:GetRoll()) >45 then-- VNAO Edit - Added
              playerData.wrappedUpAtWakeUnderline = true -- VNAO Edit - Added
            elseif math.abs(playerData.unit:GetRoll()) <20 and math.abs(playerData.unit:GetRoll()) >=10 then  -- VNAO Edit - Added a new AA comment based on discussion with Lipps today, and going to replace the AA at the X with the original LUL comments
              playerData.AAatWakeLittle = true  -- VNAO Edit - Added
            elseif math.abs(playerData.unit:GetRoll()) <10 and math.abs(playerData.unit:GetRoll()) >=2 then  -- VNAO Edit - Added a new AA comment based on discussion with Lipps today, and going to replace the AA at the X with the original LUL comments
              playerData.AAatWakeFull = true  -- VNAO Edit - Added 
            elseif math.abs(playerData.unit:GetRoll()) <2 then  -- VNAO Edit - Added a new AA comment based on discussion with Lipps today, and going to replace the AA at the X with the original LUL comments
              playerData.AAatWakeUnderline = true  -- VNAO Edit - Added 
            else  -- VNAO Edit - Added 
            end -- VNAO Edit - Added

            if math.abs(playerData.unit:GetAoA())>= 15 then  -- VNAO Edit - Added 
              playerData.AFU = true  -- VNAO Edit - Added 
            elseif math.abs(playerData.unit:GetAoA())<= 5 then  -- VNAO Edit - Added 
              playerData.AFU = true  -- VNAO Edit - Added 
            else  -- VNAO Edit - Added 
            end  -- VNAO Edit - Added 
          end-- VNAO Edit - Added


          -- Display aircraft attitude and other parameters as message text.
          if playerData.attitudemonitor then
            self:_AttitudeMonitor( playerData )
          end

          -- Check distance to other flights.
          self:_CheckPlayerPatternDistance( playerData )

          -- Foul deck check.
          self:_CheckFoulDeck( playerData )

          -- Check current step.
          if playerData.step == AIRBOSS.PatternStep.UNDEFINED then

            -- Status undefined.
            -- local time=timer.getAbsTime()
            -- local clock=UTILS.SecondsToClock(time)
            -- self:T3(string.format("Player status undefined. Waiting for next step. Time %s", clock))

          elseif playerData.step == AIRBOSS.PatternStep.REFUELING then

            -- Nothing to do here at the moment.

          elseif playerData.step == AIRBOSS.PatternStep.SPINNING then

            -- Player is spinning.
            self:_Spinning( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.HOLDING then

            -- CASE I/II/III: In holding pattern.
            self:_Holding( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.WAITING then

            -- CASE I: Waiting outside 10 NM zone for next free Marshal stack.
            self:_Waiting( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.COMMENCING then

            -- CASE I/II/III: New approach.
            self:_Commencing( playerData, true )

          elseif playerData.step == AIRBOSS.PatternStep.BOLTER then

            -- CASE I/II/III: Bolter pattern.
            self:_BolterPattern( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.PLATFORM then

            -- CASE II/III: Player has reached 5k "Platform".
            self:_Platform( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.ARCIN then

            -- Case II/III if offset.
            self:_ArcInTurn( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.ARCOUT then

            -- Case II/III if offset.
            self:_ArcOutTurn( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.DIRTYUP then

            -- CASE III: Player has descended to 1200 ft and is going level from now on.
            self:_DirtyUp( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.BULLSEYE then

            -- CASE III: Player has intercepted the glide slope and should follow "Bullseye" (ICLS).
            self:_Bullseye( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.INITIAL then

            -- CASE I/II: Player is at the initial position entering the landing pattern.
            self:_Initial( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.BREAKENTRY then

            -- CASE I/II: Break entry.
            self:_BreakEntry( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.EARLYBREAK then

            -- CASE I/II: Early break.
            self:_Break( playerData, AIRBOSS.PatternStep.EARLYBREAK )

          elseif playerData.step == AIRBOSS.PatternStep.LATEBREAK then

            -- CASE I/II: Late break.
            self:_Break( playerData, AIRBOSS.PatternStep.LATEBREAK )

          elseif playerData.step == AIRBOSS.PatternStep.ABEAM then

            -- CASE I/II: Abeam position.
            self:_Abeam( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.NINETY then

            -- CASE:I/II: Check long down wind leg.
            self:_CheckForLongDownwind( playerData )

            -- At the ninety.
            self:_Ninety( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.WAKE then

            -- CASE I/II: In the wake.
            self:_Wake( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.EMERGENCY then

            -- Emergency landing. Player pos is not checked.
            self:_Final( playerData, true )

          elseif playerData.step == AIRBOSS.PatternStep.FINAL then

            -- CASE I/II: Turn to final and enter the groove.
            self:_Final( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.GROOVE_XX or
                 playerData.step == AIRBOSS.PatternStep.GROOVE_IM or
                 playerData.step == AIRBOSS.PatternStep.GROOVE_IC or
                 playerData.step == AIRBOSS.PatternStep.GROOVE_AR or
                 playerData.step == AIRBOSS.PatternStep.GROOVE_AL or
                 playerData.step == AIRBOSS.PatternStep.GROOVE_LC or
                 playerData.step == AIRBOSS.PatternStep.GROOVE_IW then

            -- CASE I/II: In the groove.
            self:_Groove( playerData )

          elseif playerData.step == AIRBOSS.PatternStep.DEBRIEF then

            -- Debriefing in 5 seconds.
            -- SCHEDULER:New(nil, self._Debrief, {self, playerData}, 5)
            playerData.debriefschedulerID = self:ScheduleOnce( 5, self._Debrief, self, playerData )

            -- Undefined status.
            playerData.step = AIRBOSS.PatternStep.UNDEFINED

          else

            -- Error, unknown step!
            self:E( self.lid .. string.format( "ERROR: Unknown player step %s. Please report!", tostring( playerData.step ) ) )

          end

          -- Check if player missed a step during Case II/III and allow him to enter the landing pattern.
          self:_CheckMissedStepOnEntry( playerData )

        else
          self:T2( self.lid .. "WARNING: Player unit not inside the CCA!" )
        end

      else
        -- Unit not alive.
        self:T( self.lid .. "WARNING: Player unit is not alive!" )
      end
    end
  end

end

--- Checks if a player is in the pattern queue and has missed a step in Case II/III approach.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
function AIRBOSS:_CheckMissedStepOnEntry( playerData )

  -- Conditions to be met: Case II/III, in pattern queue, flag!=42 (will be set to 42 at the end if player missed a step).
  local rightcase = playerData.case > 1
  local rightqueue = self:_InQueue( self.Qpattern, playerData.group )
  local rightflag = playerData.flag ~= -42

  -- Steps that the player could have missed during Case II/III.
  local step = playerData.step
  local missedstep = step == AIRBOSS.PatternStep.PLATFORM or step == AIRBOSS.PatternStep.ARCIN or step == AIRBOSS.PatternStep.ARCOUT or step == AIRBOSS.PatternStep.DIRTYUP

  -- Check if player is about to enter the initial or bullseye zones and maybe has missed a step in the pattern.
  if rightcase and rightqueue and rightflag then

    -- Get right zone.
    local zone = nil
    if playerData.case == 2 and missedstep then

      zone = self:_GetZoneInitial( playerData.case )

    elseif playerData.case == 3 and missedstep then

      zone = self:_GetZoneBullseye( playerData.case )

    end

    -- Zone only exists if player is not at the initial or bullseye step.
    if zone then

      -- Check if player is in initial or bullseye zone.
      local inzone = playerData.unit:IsInZone( zone )

      -- Relative heading to carrier direction.
      local relheading = self:_GetRelativeHeading( playerData.unit, false )

      -- Check if player is in zone and flying roughly in the right direction.
      if inzone and math.abs( relheading ) < 60 then

        -- Player is in one of the initial zones short before the landing pattern.
        local text = string.format( "you missed an important step in the pattern!\nYour next step would have been %s.", playerData.step )
        self:MessageToPlayer( playerData, text, "AIRBOSS", nil, 5 )

        if playerData.case == 2 then
          -- Set next step to initial.
          playerData.step = AIRBOSS.PatternStep.INITIAL
        elseif playerData.case == 3 then
          -- Set next step to bullseye.
          playerData.step = AIRBOSS.PatternStep.BULLSEYE
        end

        -- Set flag value to -42. This is the value to ensure that this routine is not called again!
        playerData.flag = -42
      end
    end
  end
end

--- Set time in the groove for player.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
function AIRBOSS:_SetTimeInGroove( playerData )

  -- Set time in the groove
  if playerData.TIG0 then 
    playerData.Tgroove = timer.getTime() - playerData.TIG0 - 1.5 -- VNAO Edit - Subtracting an extra 1.5
  else
    playerData.Tgroove = 999
  end

end

--- Get time in the groove of player.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @return #number Player's time in groove in seconds.
function AIRBOSS:_GetTimeInGroove( playerData )

  local Tgroove = 999

  -- Get time in the groove.
  if playerData.TIG0 then
    Tgroove = timer.getTime() - playerData.TIG0
  end

  return Tgroove
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- EVENT functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Airboss event handler for event birth.
-- @param #AIRBOSS self
-- @param Core.Event#EVENTDATA EventData
function AIRBOSS:OnEventBirth( EventData )
  self:F3( { eventbirth = EventData } )

  -- Nil checks.
  if EventData == nil then
    self:E( self.lid .. "ERROR: EventData=nil in event BIRTH!" )
    self:E( EventData )
    return
  end
  if EventData.IniUnit == nil and (not EventData.IniObjectCategory == Object.Category.STATIC) then
    self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event BIRTH!" )
    self:E( EventData )
    return
  end

  if EventData.IniObjectCategory ~= Object.Category.UNIT then return end

  local _unitName = EventData.IniUnitName
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  self:T( self.lid .. "BIRTH: unit   = " .. tostring( EventData.IniUnitName ) )
  self:T( self.lid .. "BIRTH: group  = " .. tostring( EventData.IniGroupName ) )
  self:T( self.lid .. "BIRTH: player = " .. tostring( _playername ) )

  if _unit and _playername then

    local _uid = _unit:GetID()
    local _group = _unit:GetGroup()
    local _callsign = _unit:GetCallsign()

    -- Debug output.
    local text = string.format( "Pilot %s, callsign %s entered unit %s of group %s.", _playername, _callsign, _unitName, _group:GetName() )
    self:T( self.lid .. text )
    MESSAGE:New( text, 5 ):ToAllIf( self.Debug )

    -- Check if aircraft type the player occupies is carrier capable.
    local rightaircraft = self:_IsCarrierAircraft( _unit )
    if rightaircraft == false then
      local text = string.format( "Player aircraft type %s not supported by AIRBOSS class.", _unit:GetTypeName() )
      MESSAGE:New( text, 30 ):ToAllIf( self.Debug )
      self:T2( self.lid .. text )
      return
    end

    -- Check that coalition of the carrier and aircraft match.
    if self:GetCoalition() ~= _unit:GetCoalition() then
      local text = string.format( "Player entered aircraft of other coalition." )
      MESSAGE:New( text, 30 ):ToAllIf( self.Debug )
      self:T( self.lid .. text )
      return
    end

    -- Add Menu commands.
    self:_AddF10Commands( _unitName )

    -- Delaying the new player for a second, because AI units of the flight would not be registered correctly.
    -- SCHEDULER:New(nil, self._NewPlayer, {self, _unitName}, 1)
    self:ScheduleOnce( 1, self._NewPlayer, self, _unitName )

  end
end

--- Airboss event handler for event land.
-- @param #AIRBOSS self
-- @param Core.Event#EVENTDATA EventData
function AIRBOSS:OnEventRunwayTouch( EventData )
  self:F3( { eventland = EventData } )

  -- Nil checks.
  if EventData == nil then
    self:E( self.lid .. "ERROR: EventData=nil in event LAND!" )
    self:E( EventData )
    return
  end
  if EventData.IniUnit == nil then
    self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event LAND!" )
    self:E( EventData )
    return
  end

  -- Get unit name that landed.
  local _unitName = EventData.IniUnitName

  -- Check if this was a player.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Debug output.
  self:T( self.lid .. "LAND: unit   = " .. tostring( EventData.IniUnitName ) )
  self:T( self.lid .. "LAND: group  = " .. tostring( EventData.IniGroupName ) )
  self:T( self.lid .. "LAND: player = " .. tostring( _playername ) )

  -- This would be the closest airbase.
  local airbase = EventData.Place

  -- Nil check for airbase. Crashed as player gave me no airbase.
  if airbase == nil then
    return
  end

  -- Get airbase name.
  local airbasename = tostring( airbase:GetName() )

  -- Check if aircraft landed on the right airbase.
  if airbasename == self.airbase:GetName() then

    -- Stern coordinate at the rundown.
    local stern = self:_GetSternCoord()

    -- Polygon zone close around the carrier.
    local zoneCarrier = self:_GetZoneCarrierBox()

    -- Check if player or AI landed.
    if _unit and _playername then

      -------------------------
      -- Human Player landed --
      -------------------------

      -- Get info.
      local _uid = _unit:GetID()
      local _group = _unit:GetGroup()
      local _callsign = _unit:GetCallsign()

      -- Debug output.
      local text = string.format( "Player %s, callsign %s unit %s (ID=%d) of group %s landed at airbase %s", _playername, _callsign, _unitName, _uid, _group:GetName(), airbasename )
      self:T( self.lid .. text )
      MESSAGE:New( text, 5, "DEBUG" ):ToAllIf( self.Debug )

      -- Player data.
      local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

      -- Check if playerData is okay.
      if playerData == nil then
        self:E( self.lid .. string.format( "ERROR: playerData nil in landing event. unit=%s player=%s", tostring( _unitName ), tostring( _playername ) ) )
        return
      end

      -- Check that player landed on the carrier.
      if _unit:IsInZone( zoneCarrier ) then

        -- Check if this was a valid approach.
        if not playerData.valid then
          -- Player missed at least one step in the pattern.
          local text = string.format( "you missed at least one important step in the pattern!\nYour next step would have been %s.\nThis pass is INVALID.", playerData.step )
          self:MessageToPlayer( playerData, text, "AIRBOSS", nil, 30, true, 5 )

          -- Clear queues just in case.
          self:_RemoveFlightFromMarshalQueue( playerData, true )
          self:_RemoveFlightFromQueue( self.Qpattern, playerData )
          self:_RemoveFlightFromQueue( self.Qwaiting, playerData )
          self:_RemoveFlightFromQueue( self.Qspinning, playerData )

          -- Reinitialize player data.
          self:_InitPlayer( playerData )

          return
        end

        -- Check if player already landed. We dont need a second time.
        if playerData.landed then

          self:E( self.lid .. string.format( "Player %s just landed a second time.", _playername ) )

        else

          -- We did land.
          playerData.landed = true

          -- Switch attitude monitor off if on.
          playerData.attitudemonitor = false

          -- Coordinate at landing event.
          local coord = playerData.unit:GetCoordinate()

          -- Get distances relative to
          local X, Z, rho, phi = self:_GetDistances( _unit )

          -- Landing distance wrt to stern position.
          local dist = coord:Get2DDistance( stern )

          -- Debug mark of player landing coord.
          if self.Debug and false then
            -- Debug mark of player landing coord.
            local lp = coord:MarkToAll( "Landing coord." )
            coord:SmokeGreen()
          end

          -- Set time in the groove of player.
          self:_SetTimeInGroove( playerData )

          -- Debug text.
          local text = string.format( "Player %s AC type %s landed at dist=%.1f m. Tgroove=%.1f sec.", playerData.name, playerData.actype, dist, self:_GetTimeInGroove( playerData ) )
          text = text .. string.format( " X=%.1f m, Z=%.1f m, rho=%.1f m.", X, Z, rho )
          self:T( self.lid .. text )

          -- Check carrier type.
          if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then

            -- Power "Idle".
            self:RadioTransmission( self.LSORadio, self.LSOCall.IDLE, false, 1, nil, true )

            -- Next step debrief.
            self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.DEBRIEF )

          else

            -- Next step undefined until we know more.
            self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.UNDEFINED )

            -- Call trapped function in 1 second to make sure we did not bolter.
            -- SCHEDULER:New(nil, self._Trapped, {self, playerData}, 1)
            self:ScheduleOnce( 1, self._Trapped, self, playerData )

          end

        end

      else
        -- Handle case where player did not land on the carrier.
        -- Well, I guess, he leaves the slot or ejects. Both should be handled.
        if playerData then
          self:E( self.lid .. string.format( "Player %s did not land in carrier box zone. Maybe in the water near the carrier?", playerData.name ) )
        end
      end

    else

      --------------------
      -- AI unit landed --
      --------------------

      if self.carriertype ~= AIRBOSS.CarrierType.INVINCIBLE or self.carriertype ~= AIRBOSS.CarrierType.HERMES or self.carriertype ~= AIRBOSS.CarrierType.TARAWA or self.carriertype ~= AIRBOSS.CarrierType.AMERICA or self.carriertype ~= AIRBOSS.CarrierType.JCARLOS or self.carriertype ~= AIRBOSS.CarrierType.CANBERRA then

        -- Coordinate at landing event
        local coord = EventData.IniUnit:GetCoordinate()

        -- Debug mark of player landing coord.
        local dist = coord:Get2DDistance( self:GetCoordinate() )

        -- Get wire
        local wire = self:_GetWire( coord, 0 )

        -- Aircraft type.
        local _type = EventData.IniUnit:GetTypeName()

        -- Debug text.
        local text = string.format( "AI unit %s of type %s landed at dist=%.1f m. Trapped wire=%d.", _unitName, _type, dist, wire )
        self:T( self.lid .. text )

      end

      -- AI always lands ==> remove unit from flight group and queues.
      local flight = self:_RecoveredElement( EventData.IniUnit )

      -- Check if all were recovered. If so update pattern queue.
      self:_CheckSectionRecovered( flight )

    end
  end
end

--- Airboss event handler for event that a unit shuts down its engines.
-- @param #AIRBOSS self
-- @param Core.Event#EVENTDATA EventData
function AIRBOSS:OnEventEngineShutdown( EventData )
  self:F3( { eventengineshutdown = EventData } )

  -- Nil checks.
  if EventData == nil then
    self:E( self.lid .. "ERROR: EventData=nil in event ENGINESHUTDOWN!" )
    self:E( EventData )
    return
  end
  if EventData.IniUnit == nil then
    self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event ENGINESHUTDOWN!" )
    self:E( EventData )
    return
  end

  local _unitName = EventData.IniUnitName
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  self:T3( self.lid .. "ENGINESHUTDOWN: unit   = " .. tostring( EventData.IniUnitName ) )
  self:T3( self.lid .. "ENGINESHUTDOWN: group  = " .. tostring( EventData.IniGroupName ) )
  self:T3( self.lid .. "ENGINESHUTDOWN: player = " .. tostring( _playername ) )

  if _unit and _playername then

    -- Debug message.
    self:T( self.lid .. string.format( "Player %s shut down its engines!", _playername ) )

  else

    -- Debug message.
    self:T( self.lid .. string.format( "AI unit %s shut down its engines!", _unitName ) )

    -- Get flight.
    local flight = self:_GetFlightFromGroupInQueue( EventData.IniGroup, self.flights )

    -- Only AI flights.
    if flight and flight.ai then

      -- Check if all elements were recovered.
      local recovered = self:_CheckSectionRecovered( flight )

      -- Despawn group and completely remove flight.
      if recovered then
        self:T( self.lid .. string.format( "AI group %s completely recovered. Despawning group after engine shutdown event as requested in 5 seconds.", tostring( EventData.IniGroupName ) ) )

        -- Remove flight.
        self:_RemoveFlight( flight )

        -- Check if this is a tanker or AWACS associated with the carrier.
        local istanker = self.tanker and self.tanker.tanker:GetName() == EventData.IniGroupName
        local isawacs = self.awacs and self.awacs.tanker:GetName() == EventData.IniGroupName

        -- Destroy group if desired. Recovery tankers have their own logic for despawning.
        if self.despawnshutdown and not (istanker or isawacs) then
          EventData.IniGroup:Destroy( nil, 5 )
        end

      end

    end
  end
end

--- Airboss event handler for event that a unit takes off.
-- @param #AIRBOSS self
-- @param Core.Event#EVENTDATA EventData
function AIRBOSS:OnEventTakeoff( EventData )
  self:F3( { eventtakeoff = EventData } )

  -- Nil checks.
  if EventData == nil then
    self:E( self.lid .. "ERROR: EventData=nil in event TAKEOFF!" )
    self:E( EventData )
    return
  end
  if EventData.IniUnit == nil then
    self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event TAKEOFF!" )
    self:E( EventData )
    return
  end

  local _unitName = EventData.IniUnitName
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  self:T3( self.lid .. "TAKEOFF: unit   = " .. tostring( EventData.IniUnitName ) )
  self:T3( self.lid .. "TAKEOFF: group  = " .. tostring( EventData.IniGroupName ) )
  self:T3( self.lid .. "TAKEOFF: player = " .. tostring( _playername ) )

  -- Airbase.
  local airbase = EventData.Place

  -- Airbase name.
  local airbasename = "unknown"
  if airbase then
    airbasename = airbase:GetName()
  end

  -- Check right airbase.
  if airbasename == self.airbase:GetName() then

    if _unit and _playername then

      -- Debug message.
      self:T( self.lid .. string.format( "Player %s took off at %s!", _playername, airbasename ) )

    else

      -- Debug message.
      self:T2( self.lid .. string.format( "AI unit %s took off at %s!", _unitName, airbasename ) )

      -- Get flight.
      local flight = self:_GetFlightFromGroupInQueue( EventData.IniGroup, self.flights )

      if flight then

        -- Set ballcall and recoverd status.
        for _, elem in pairs( flight.elements ) do
          local element = elem -- #AIRBOSS.FlightElement
          element.ballcall = false
          element.recovered = nil
        end
      end
    end

  end
end

--- Airboss event handler for event crash.
-- @param #AIRBOSS self
-- @param Core.Event#EVENTDATA EventData
function AIRBOSS:OnEventCrash( EventData )
  self:F3( { eventcrash = EventData } )

  -- Nil checks.
  if EventData == nil then
    self:E( self.lid .. "ERROR: EventData=nil in event CRASH!" )
    self:E( EventData )
    return
  end
  if EventData.IniUnit == nil then
    self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event CRASH!" )
    self:E( EventData )
    return
  end

  local _unitName = EventData.IniUnitName
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  self:T3( self.lid .. "CRASH: unit   = " .. tostring( EventData.IniUnitName ) )
  self:T3( self.lid .. "CRASH: group  = " .. tostring( EventData.IniGroupName ) )
  self:T3( self.lid .. "CARSH: player = " .. tostring( _playername ) )

  if _unit and _playername then
    -- Debug message.
    self:T( self.lid .. string.format( "Player %s crashed!", _playername ) )

    -- Get player flight.
    local flight = self.players[_playername]

    -- Remove flight completely from all queues and collapse marshal if necessary.
    -- This also updates the section, if any and removes any unfinished gradings of the player.
    if flight then
      self:_RemoveFlight( flight, true )
    end

  else
    -- Debug message.
    self:T2( self.lid .. string.format( "AI unit %s crashed!", EventData.IniUnitName ) )

    -- Remove unit from flight and queues.
    self:_RemoveUnitFromFlight( EventData.IniUnit )
  end

end

--- Airboss event handler for event Ejection.
-- @param #AIRBOSS self
-- @param Core.Event#EVENTDATA EventData
function AIRBOSS:OnEventEjection( EventData )
  self:F3( { eventland = EventData } )

  -- Nil checks.
  if EventData == nil then
    self:E( self.lid .. "ERROR: EventData=nil in event EJECTION!" )
    self:E( EventData )
    return
  end
  if EventData.IniUnit == nil then
    self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event EJECTION!" )
    self:E( EventData )
    return
  end

  local _unitName = EventData.IniUnitName
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  self:T3( self.lid .. "EJECT: unit   = " .. tostring( EventData.IniUnitName ) )
  self:T3( self.lid .. "EJECT: group  = " .. tostring( EventData.IniGroupName ) )
  self:T3( self.lid .. "EJECT: player = " .. tostring( _playername ) )

  if _unit and _playername then
    self:T( self.lid .. string.format( "Player %s ejected!", _playername ) )

    -- Get player flight.
    local flight = self.players[_playername]

    -- Remove flight completely from all queues and collapse marshal if necessary.
    if flight then
      self:_RemoveFlight( flight, true )
    end

  else
    -- Debug message.
    self:T( self.lid .. string.format( "AI unit %s ejected!", EventData.IniUnitName ) )

    -- Remove element/unit from flight group and from all queues if no elements alive.
    self:_RemoveUnitFromFlight( EventData.IniUnit )

    -- What could happen is, that another element has landed (recovered) already and this one crashes.
    -- This would mean that the flight would not be deleted from the queue ==> Check if section recovered.
    local flight = self:_GetFlightFromGroupInQueue( EventData.IniGroup, self.flights )
    self:_CheckSectionRecovered( flight )
  end

end

--- Airboss event handler for event REMOVEUNIT.
-- @param #AIRBOSS self
-- @param Core.Event#EVENTDATA EventData
function AIRBOSS:OnEventRemoveUnit( EventData )
  self:F3( { eventland = EventData } )

  -- Nil checks.
  if EventData == nil then
    self:T( self.lid .. "ERROR: EventData=nil in event REMOVEUNIT!" )
    self:T( EventData )
    return
  end
  if EventData.IniUnit == nil then
    self:T( self.lid .. "ERROR: EventData.IniUnit=nil in event REMOVEUNIT!" )
    self:T( EventData )
    return
  end

  local _unitName = EventData.IniUnitName
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  self:T3( self.lid .. "EJECT: unit   = " .. tostring( EventData.IniUnitName ) )
  self:T3( self.lid .. "EJECT: group  = " .. tostring( EventData.IniGroupName ) )
  self:T3( self.lid .. "EJECT: player = " .. tostring( _playername ) )

  if _unit and _playername then
    self:T( self.lid .. string.format( "Player %s removed!", _playername ) )

    -- Get player flight.
    local flight = self.players[_playername]

    -- Remove flight completely from all queues and collapse marshal if necessary.
    if flight then
      self:_RemoveFlight( flight, true )
    end

  else
    -- Debug message.
    self:T( self.lid .. string.format( "AI unit %s removed!", EventData.IniUnitName ) )

    -- Remove element/unit from flight group and from all queues if no elements alive.
    self:_RemoveUnitFromFlight( EventData.IniUnit )

    -- What could happen is, that another element has landed (recovered) already and this one crashes.
    -- This would mean that the flight would not be deleted from the queue ==> Check if section recovered.
    local flight = self:_GetFlightFromGroupInQueue( EventData.IniGroup, self.flights )
    self:_CheckSectionRecovered( flight )
  end

end

--- Airboss event handler for event player leave unit.
-- @param #AIRBOSS self
-- @param Core.Event#EVENTDATA EventData
-- function AIRBOSS:OnEventPlayerLeaveUnit(EventData)
function AIRBOSS:_PlayerLeft( EventData )
  self:F3( { eventleave = EventData } )

  -- Nil checks.
  if EventData == nil then
    self:E( self.lid .. "ERROR: EventData=nil in event PLAYERLEFTUNIT!" )
    self:E( EventData )
    return
  end
  if EventData.IniUnit == nil then
    self:E( self.lid .. "ERROR: EventData.IniUnit=nil in event PLAYERLEFTUNIT!" )
    self:E( EventData )
    return
  end

  local _unitName = EventData.IniUnitName
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  self:T3( self.lid .. "PLAYERLEAVEUNIT: unit   = " .. tostring( EventData.IniUnitName ) )
  self:T3( self.lid .. "PLAYERLEAVEUNIT: group  = " .. tostring( EventData.IniGroupName ) )
  self:T3( self.lid .. "PLAYERLEAVEUNIT: player = " .. tostring( _playername ) )

  if _unit and _playername then

    -- Debug info.
    self:T( self.lid .. string.format( "Player %s left unit %s!", _playername, _unitName ) )

    -- Get player flight.
    local flight = self.players[_playername]

    -- Remove flight completely from all queues and collapse marshal if necessary.
    if flight then
      self:_RemoveFlight( flight, true )
    end

  end

end

--- Airboss event function handling the mission end event.
-- Handles the case when the mission is ended.
-- @param #AIRBOSS self
-- @param Core.Event#EVENTDATA EventData Event data.
function AIRBOSS:OnEventMissionEnd( EventData )
  self:T3( self.lid .. "Mission Ended" )
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- PATTERN functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Spinning
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
function AIRBOSS:_Spinning( playerData )

  -- Early break.
  local SpinIt = {}
  SpinIt.name = "Spinning"
  SpinIt.Xmin = -UTILS.NMToMeters( 6 ) -- Not more than 5 NM behind the boat.
  SpinIt.Xmax = UTILS.NMToMeters( 5 ) -- Not more than 5 NM in front of the boat.
  SpinIt.Zmin = -UTILS.NMToMeters( 6 ) -- Not more than 5 NM port.
  SpinIt.Zmax = UTILS.NMToMeters( 2 ) -- Not more than 3 NM starboard.
  SpinIt.LimitXmin = -100 -- 100 meters behind the boat
  SpinIt.LimitXmax = nil
  SpinIt.LimitZmin = -UTILS.NMToMeters( 1 ) -- 1 NM port
  SpinIt.LimitZmax = nil

  -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier)
  local X, Z, rho, phi = self:_GetDistances( playerData.unit )

  -- Check if we are in front of the boat (diffX > 0).
  if self:_CheckLimits( X, Z, SpinIt ) then

    -- Player is "de-spinned". Should go to initial again.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.INITIAL )

    -- Remove player from spinning queue.
    self:_RemoveFlightFromQueue( self.Qspinning, playerData )

  end

end

--- Waiting outside 10 NM zone for free Marshal stack.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
function AIRBOSS:_Waiting( playerData )

  -- Create 10 NM zone around the carrier.
  local radius = UTILS.NMToMeters( 10 )
  local zone = ZONE_RADIUS:New( "Carrier 10 NM Zone", self.carrier:GetVec2(), radius )

  -- Check if player is inside 10 NM radius of the carrier.
  local inzone = playerData.unit:IsInZone( zone )

  -- Time player is waiting.
  local Twaiting = timer.getAbsTime() - playerData.time

  -- Warning if player is inside the zone.
  if inzone and Twaiting > 3 * 60 and not playerData.warning then
    local text = string.format( "You are supposed to wait outside the 10 NM zone." )
    self:MessageToPlayer( playerData, text, "AIRBOSS" )
    playerData.warning = true
  end

  -- Reset warning.
  if inzone == false and playerData.warning == true then
    playerData.warning = nil
  end

end

--- Holding.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
function AIRBOSS:_Holding( playerData )

  -- Player unit and flight.
  local unit = playerData.unit

  -- Current stack.
  local stack = playerData.flag

  -- Check for reported error.
  if stack <= 0 then
    local text = string.format( "ERROR: player %s in step %s is holding but has stack=%s (<=0)", playerData.name, playerData.step, tostring( stack ) )
    self:E( self.lid .. text )
  end

  ---------------------------
  -- Holding Pattern Check --
  ---------------------------

  -- Pattern altitude.
  local patternalt = self:_GetMarshalAltitude( stack, playerData.case )

  -- Player altitude.
  local playeralt = unit:GetAltitude()

  -- Get holding zone of player.
  local zoneHolding = self:_GetZoneHolding( playerData.case, stack )

  -- Nil check.
  if zoneHolding == nil then
    self:E( self.lid .. "ERROR: zoneHolding is nil!" )
    self:E( { playerData = playerData } )
    return
  end

  -- Check if player is in holding zone.
  local inholdingzone = unit:IsInZone( zoneHolding )

  -- Altitude difference between player and assigned stack.
  local altdiff = playeralt - patternalt

  -- Acceptable altitude depending on player skill.
  local altgood = UTILS.FeetToMeters( 500 )
  if playerData.difficulty == AIRBOSS.Difficulty.HARD then
    -- Pros can be expected to be within +-200 ft.
    altgood = UTILS.FeetToMeters( 200 )
  elseif playerData.difficulty == AIRBOSS.Difficulty.NORMAL then
    -- Normal guys should be within +-350 ft.
    altgood = UTILS.FeetToMeters( 350 )
  elseif playerData.difficulty == AIRBOSS.Difficulty.EASY then
    -- Students should be within +-500 ft.
    altgood = UTILS.FeetToMeters( 500 )
  end

  -- When back to good altitude = 50%.
  local altback = altgood * 0.5

  -- Check if stack just collapsed and give the player one minute to change the altitude.
  local justcollapsed = false
  if self.Tcollapse then
    -- Time since last stack change.
    local dT = timer.getTime() - self.Tcollapse

    -- TODO: check if this works.
    -- local dT=timer.getAbsTime()-playerData.time

    -- Check if less then 90 seconds.
    if dT <= 90 then
      justcollapsed = true
    end
  end

  -- Check if altitude is acceptable.
  local goodalt = math.abs( altdiff ) < altgood

  -- Angels.
  local angels = self:_GetAngels( patternalt )

  -- XXX: Check if player is flying counter clockwise. AOB<0.

  -- Message text.
  local text = ""

  -- Different cases
  if playerData.holding == true then
    -- Player was in holding zone last time we checked.

    if inholdingzone then
      -- Player is still in holding zone.
      self:T3( "Player is still in the holding zone. Good job." )
    else
      -- Player left the holding zone.
      text = text .. string.format( "You just left the holding zone. Watch your numbers!" )
      playerData.holding = false
    end

    -- Altitude check if stack not just collapsed.
    if not justcollapsed then

      if altdiff > altgood then

        -- Issue warning for being too high.
        if not playerData.warning then
          text = text .. string.format( "You left your assigned altitude. Descent to angels %d.", angels )
          playerData.warning = true
        end

      elseif altdiff < -altgood then

        -- Issue warning for being too low.
        if not playerData.warning then
          text = text .. string.format( "You left your assigned altitude. Climb to angels %d.", angels )
          playerData.warning = true
        end

      end

    end

    -- Back to assigned altitude.
    if playerData.warning and math.abs( altdiff ) <= altback then
      text = text .. string.format( "Altitude is looking good again." )
      playerData.warning = nil
    end

  elseif playerData.holding == false then

    -- Player left holding zone
    if inholdingzone then
      -- Player is back in the holding zone.
      text = text .. string.format( "You are back in the holding zone. Now stay there!" )
      playerData.holding = true
    else
      -- Player is still outside the holding zone.
      self:T3( "Player still outside the holding zone. What are you doing man?!" )
    end

  elseif playerData.holding == nil then
    -- Player did not entered the holding zone yet.

    if inholdingzone then

      -- Player arrived in holding zone.
      playerData.holding = true

      -- Inform player.
      text = text .. string.format( "You arrived at the holding zone." )

      -- Feedback on altitude.
      if goodalt then
        text = text .. string.format( " Altitude is good." )
      else
        if altdiff < 0 then
          text = text .. string.format( " But you're too low." )
        else
          text = text .. string.format( " But you're too high." )
        end
        text = text .. string.format( "\nCurrently assigned altitude is %d ft.", UTILS.MetersToFeet( patternalt ) )
        playerData.warning = true
      end

    else
      -- Player did not yet arrive in holding zone.
      self:T3( "Waiting for player to arrive in the holding zone." )
    end

  end

  -- Send message.
  if playerData.showhints then
    self:MessageToPlayer( playerData, text, "MARSHAL" )
  end

end

--- Commence approach. This step initializes the player data. Section members are also set to commence. Next step depends on recovery case:
--
-- * Case 1: Initial
-- * Case 2/3: Platform
--
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @param #boolean zonecheck If true, zone is checked before player is released.
function AIRBOSS:_Commencing( playerData, zonecheck )

  -- Check for auto commence
  if zonecheck then

    -- Get auto commence zone.
    local zoneCommence = self:_GetZoneCommence( playerData.case, playerData.flag )

    -- Check if unit is in the zone.
    local inzone = playerData.unit:IsInZone( zoneCommence )

    -- Skip the rest if not in the zone yet.
    if not inzone then

      -- Friendly reminder.
      if timer.getAbsTime() - playerData.time > 180 then
        self:_MarshalCallClearedForRecovery( playerData.onboard, playerData.case )
        playerData.time = timer.getAbsTime()
      end

      -- Skip the rest.
      return
    end

  end

  -- Remove flight from Marshal queue. If flight was in queue, stack is collapsed and flight added to the pattern queue.
  self:_RemoveFlightFromMarshalQueue( playerData )

  -- Initialize player data for new approach.
  self:_InitPlayer( playerData )

  -- Commencing message to player only.
  if playerData.difficulty ~= AIRBOSS.Difficulty.HARD then

    -- Text
    local text = ""

    -- Positive response.
    if playerData.case == 1 then
      text = text .. "Proceed to initial."
    else
      text = text .. "Descent to platform."
      if playerData.difficulty == AIRBOSS.Difficulty.EASY and playerData.showhints then
        text = text .. " VSI 4000 ft/min until you reach 5000 ft."
      end
    end

    -- Message to player.
    self:MessageToPlayer( playerData, text, "MARSHAL" )
  end

  -- Next step: depends on case recovery.
  local nextstep
  if playerData.case == 1 then
    -- CASE I: Player has to fly to the initial which is 3 NM DME astern of the boat.
    nextstep = AIRBOSS.PatternStep.INITIAL
  else
    -- CASE II/III: Player has to start the descent at 4000 ft/min to the platform at 5k ft.
    nextstep = AIRBOSS.PatternStep.PLATFORM
  end

  -- Next step hint.
  self:_SetPlayerStep( playerData, nextstep )

  -- Commence section members as well but dont check the zone.
  for i, _flight in pairs( playerData.section ) do
    local flight = _flight -- #AIRBOSS.PlayerData
    self:_Commencing( flight, false )
  end

end

--- Start pattern when player enters the initial zone in case I/II recoveries.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @return #boolean True if player is in the initial zone.
function AIRBOSS:_Initial( playerData )

  -- Check if player is in initial zone and entering the CASE I pattern.
  local inzone = playerData.unit:IsInZone( self:_GetZoneInitial( playerData.case ) )

  -- Relative heading to carrier direction.
  local relheading = self:_GetRelativeHeading( playerData.unit, false )

  -- altitude of player in feet.
  local altitude = playerData.unit:GetAltitude()

  -- Check if player is in zone and flying roughly in the right direction.
  if inzone and math.abs( relheading ) < 60 and altitude <= self.initialmaxalt then

    -- Send message for normal and easy difficulty.
    if playerData.showhints then

      -- Inform player.
      local hint = string.format( "Initial" )

      -- Hook down for students.
      if playerData.difficulty == AIRBOSS.Difficulty.EASY and playerData.actype ~= AIRBOSS.AircraftCarrier.AV8B then
        if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then
          hint = hint .. " - Hook down, SAS on, Wing Sweep 68°!"
        else
          hint = hint .. " - Hook down!"
        end
      end

      self:MessageToPlayer( playerData, hint, "MARSHAL" )
    end

    -- Next step: Break entry.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.BREAKENTRY )

    return true
  end

  return false
end

--- Check if player is in CASE II/III approach corridor.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
function AIRBOSS:_CheckCorridor( playerData )

  -- Check if player is in valid zone
  local validzone = self:_GetZoneCorridor( playerData.case )

  -- Check if we are inside the moving zone.
  local invalid = playerData.unit:IsNotInZone( validzone )

  -- Issue warning.
  if invalid and (not playerData.warning) then
    self:MessageToPlayer( playerData, "you left the approach corridor!", "AIRBOSS" )
    playerData.warning = true
  end

  -- Back in zone.
  if (not invalid) and playerData.warning then
    self:MessageToPlayer( playerData, "you're back in the approach corridor.", "AIRBOSS" )
    playerData.warning = false
  end

end

--- Platform at 5k ft for case II/III recoveries. Descent at 2000 ft/min.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
function AIRBOSS:_Platform( playerData )

  -- Check if player left or got back to the approach corridor.
  self:_CheckCorridor( playerData )

  -- Check if we are inside the moving zone.
  local inzone = playerData.unit:IsInZone( self:_GetZonePlatform( playerData.case ) )

  -- Check if we are in zone.
  if inzone then

    -- Hint for player about altitude, AoA etc.
    self:_PlayerHint( playerData )

    -- Next step: depends.
    local nextstep
    if math.abs( self.holdingoffset ) > 0 and playerData.case > 1 then
      -- Turn to BRC (case II) or FB (case III).
      nextstep = AIRBOSS.PatternStep.ARCIN
    else
      if playerData.case == 2 then
        -- Case II: Initial zone then Case I recovery.
        nextstep = AIRBOSS.PatternStep.INITIAL
      elseif playerData.case == 3 then
        -- CASE III: Dirty up.
        nextstep = AIRBOSS.PatternStep.DIRTYUP
      end
    end

    -- Next step hint.
    self:_SetPlayerStep( playerData, nextstep )

  end
end

--- Arc in turn for case II/III recoveries.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
function AIRBOSS:_ArcInTurn( playerData )

  -- Check if player left or got back to the approach corridor.
  self:_CheckCorridor( playerData )

  -- Check if we are inside the moving zone.
  local inzone = playerData.unit:IsInZone( self:_GetZoneArcIn( playerData.case ) )

  if inzone then

    -- Hint for player about altitude, AoA etc.
    self:_PlayerHint( playerData )

    -- Next step: Arc Out Turn.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.ARCOUT )

  end
end

--- Arc out turn for case II/III recoveries.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
function AIRBOSS:_ArcOutTurn( playerData )

  -- Check if player left or got back to the approach corridor.
  self:_CheckCorridor( playerData )

  -- Check if we are inside the moving zone.
  local inzone = playerData.unit:IsInZone( self:_GetZoneArcOut( playerData.case ) )

  if inzone then

    -- Hint for player about altitude, AoA etc.
    self:_PlayerHint( playerData )

    -- Next step:
    local nextstep
    if playerData.case == 3 then
      -- Case III: Dirty up.
      nextstep = AIRBOSS.PatternStep.DIRTYUP
    else
      -- Case II: Initial.
      nextstep = AIRBOSS.PatternStep.INITIAL
    end

    -- Next step hint.
    self:_SetPlayerStep( playerData, nextstep )
  end
end

--- Dirty up and level out at 1200 ft for case III recovery.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
function AIRBOSS:_DirtyUp( playerData )

  -- Check if player left or got back to the approach corridor.
  self:_CheckCorridor( playerData )

  -- Check if we are inside the moving zone.
  local inzone = playerData.unit:IsInZone( self:_GetZoneDirtyUp( playerData.case ) )

  if inzone then

    -- Hint for player about altitude, AoA etc.
    playerData.Tgroove = timer.getTime() - playerData.TIG0 - 1.5 -- VNAO Edit - Subtracting an extra 1.5

    -- VNAO Edit - Added wrapped up call to LSO grading
    playerData.wrappedUpAtWakeLittle = false -- VNAO Edit - Added
    playerData.wrappedUpAtWakeFull = false -- VNAO Edit - Added
    playerData.wrappedUpAtWakeUnderline = false -- VNAO Edit - Added
    playerData.wrappedUpAtStartLittle = false -- VNAO Edit - Added 
    playerData.wrappedUpAtStartFull = false -- VNAO Edit - Added
    playerData.wrappedUpAtStartUnderline = false -- VNAO Edit - Added
    playerData.AAatWakeLittle = false -- VNAO Edit - Added
    playerData.AAatWakeFull = false -- VNAO Edit - Added
    playerData.AAatWakeUnderline = false -- VNAO Edit - Added
    playerData.AFU = false -- VNAO Edit - Added

    -- Radio call "Say/Fly needles". Delayed by 10/15 seconds.
    if   playerData.actype == AIRBOSS.AircraftCarrier.HORNET
      or playerData.actype == AIRBOSS.AircraftCarrier.F14A
      or playerData.actype == AIRBOSS.AircraftCarrier.F14B
      or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE
      or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF
      or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER
    then
      local callsay = self:_NewRadioCall( self.MarshalCall.SAYNEEDLES, nil, nil, 5, playerData.onboard )
      local callfly = self:_NewRadioCall( self.MarshalCall.FLYNEEDLES, nil, nil, 5, playerData.onboard )
      self:RadioTransmission( self.MarshalRadio, callsay, false, 55, nil, true )
      self:RadioTransmission( self.MarshalRadio, callfly, false, 60, nil, true )
    end

    -- TODO: Make Fly Bullseye call if no automatic ICLS is active.

    -- Next step: CASE III: Intercept glide slope and follow bullseye (ICLS).
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.BULLSEYE )

  end
end

--- Intercept glide slop and follow ICLS, aka Bullseye for case III recovery.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @return #boolean If true, player is in bullseye zone.
function AIRBOSS:_Bullseye( playerData )

  -- Check if player left or got back to the approach corridor.
  self:_CheckCorridor( playerData )

  -- Check if we are inside the moving zone.
  local inzone = playerData.unit:IsInZone( self:_GetZoneBullseye( playerData.case ) )

  -- Relative heading to carrier direction of the runway.
  local relheading = self:_GetRelativeHeading( playerData.unit, true )

  -- Check if player is in zone and flying roughly in the right direction.
  if inzone and math.abs( relheading ) < 60 then

    -- Hint for player about altitude, AoA etc.
    self:_PlayerHint( playerData )

    -- LSO expect spot 5 or 7.5 call
    if playerData.actype == AIRBOSS.AircraftCarrier.AV8B and self.carriertype == AIRBOSS.CarrierType.JCARLOS then
      self:RadioTransmission( self.LSORadio, self.LSOCall.EXPECTSPOT5, nil, nil, nil, true )
    elseif playerData.actype == AIRBOSS.AircraftCarrier.AV8B and self.carriertype == AIRBOSS.CarrierType.CANBERRA then
      self:RadioTransmission( self.LSORadio, self.LSOCall.EXPECTSPOT5, nil, nil, nil, true )
    elseif playerData.actype == AIRBOSS.AircraftCarrier.AV8B then
      self:RadioTransmission( self.LSORadio, self.LSOCall.EXPECTSPOT75, nil, nil, nil, true )
    end

    -- Next step: Groove Call the ball.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_XX )

  end
end

--- Bolter pattern. Sends player to abeam for Case I/II or Bullseye for Case III ops.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
function AIRBOSS:_BolterPattern( playerData )

  -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier)
  local X, Z, rho, phi = self:_GetDistances( playerData.unit )

  -- Bolter Pattern thresholds.
  local Bolter = {}
  Bolter.name = "Bolter Pattern"
  Bolter.Xmin = -UTILS.NMToMeters( 5 ) -- Not more then 5 NM astern of boat.
  Bolter.Xmax = UTILS.NMToMeters( 3 ) -- Not more then 3 NM ahead of boat.
  Bolter.Zmin = -UTILS.NMToMeters( 5 ) -- Not more than 2 NM port.
  Bolter.Zmax = UTILS.NMToMeters( 1 ) -- Not more than 1 NM starboard.
  Bolter.LimitXmin = 100 -- Check that 100 meter ahead and port
  Bolter.LimitXmax = nil
  Bolter.LimitZmin = nil
  Bolter.LimitZmax = nil

  -- Check if we are in front of the boat (diffX > 0).
  if self:_CheckLimits( X, Z, Bolter ) then
    local nextstep
    if playerData.case < 3 then
      nextstep = AIRBOSS.PatternStep.ABEAM
    else
      nextstep = AIRBOSS.PatternStep.BULLSEYE
    end
    self:_SetPlayerStep( playerData, nextstep )
  end
end

--- Break entry for case I/II recoveries.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
function AIRBOSS:_BreakEntry( playerData )

  -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier)
  local X, Z = self:_GetDistances( playerData.unit )

  -- Abort condition check.
  if self:_CheckAbort( X, Z, self.BreakEntry ) then
    self:_AbortPattern( playerData, X, Z, self.BreakEntry, true )
    return
  end

  -- Check if we are in front of the boat (diffX > 0).
  if self:_CheckLimits( X, Z, self.BreakEntry ) then

    -- Hint for player about altitude, AoA etc.
    self:_PlayerHint( playerData )
    -- Next step: Early Break.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.EARLYBREAK )

  end
end

--- Break.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @param #string part Part of the break.
function AIRBOSS:_Break( playerData, part )

  -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier)
  local X, Z = self:_GetDistances( playerData.unit )

  -- Early or late break.
  local breakpoint = self.BreakEarly
  if part == AIRBOSS.PatternStep.LATEBREAK then
    breakpoint = self.BreakLate
  end

  -- Check abort conditions.
  if self:_CheckAbort( X, Z, breakpoint ) then
    self:_AbortPattern( playerData, X, Z, breakpoint, true )
    return
  end

  -- Player made a very tight turn and did not trigger the latebreak threshold at 0.8 NM.
  local tooclose = false
  if part == AIRBOSS.PatternStep.LATEBREAK then
    local close = 0.8
    if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then
      close = 0.5
    end
    if X < 0 and Z < UTILS.NMToMeters( close ) then
      if playerData.difficulty == AIRBOSS.Difficulty.EASY and playerData.showhints then
        self:MessageToPlayer( playerData, "your turn was too tight! Allow for more distance to the boat next time.", "LSO" )
      end
      tooclose = true
    end
  end

  -- Check limits.
  if self:_CheckLimits( X, Z, breakpoint ) or tooclose then

    -- Hint for player about altitude, AoA etc.
    self:_PlayerHint( playerData )

    -- Next step: Late Break or Abeam.
    local nextstep
    if part == AIRBOSS.PatternStep.EARLYBREAK then
      nextstep = AIRBOSS.PatternStep.LATEBREAK
    else
      nextstep = AIRBOSS.PatternStep.ABEAM
    end

    self:_SetPlayerStep( playerData, nextstep )
  end
end

--- Long downwind leg check.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
function AIRBOSS:_CheckForLongDownwind( playerData )

  -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier)
  local X, Z = self:_GetDistances( playerData.unit )

  -- 1.6 NM from carrier is too far.
  local limit = UTILS.NMToMeters( -1.6 )

  -- For the tarawa, other LHA and LHD we give a bit more space.
  if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then
    limit = UTILS.NMToMeters( -2.0 )
  end

  -- Check we are not too far out w.r.t back of the boat.
  if X < limit then -- and relhead<45 then

    -- Sound output.
    self:RadioTransmission( self.LSORadio, self.LSOCall.LONGINGROOVE )
    self:RadioTransmission( self.LSORadio, self.LSOCall.DEPARTANDREENTER, nil, nil, nil, true )

    -- Debrief.
    self:_AddToDebrief( playerData, "Long in the groove - Pattern Waveoff!" )

    -- grade="LIG PATTERN WAVE OFF - CUT 1 PT"
    playerData.lig = true
    playerData.wop = true

    -- Next step: Debriefing.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.DEBRIEF )

  end

end

--- Abeam position.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
function AIRBOSS:_Abeam( playerData )

  -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier)
  local X, Z = self:_GetDistances( playerData.unit )

  -- Check abort conditions.
  if self:_CheckAbort( X, Z, self.Abeam ) then
    self:_AbortPattern( playerData, X, Z, self.Abeam, true )
    return
  end

  -- Check nest step threshold.
  if self:_CheckLimits( X, Z, self.Abeam ) then

    -- Paddles contact.
    self:RadioTransmission( self.LSORadio, self.LSOCall.PADDLESCONTACT, nil, nil, nil, true )

    -- LSO expect spot 5 or 7.5 call
    if playerData.actype == AIRBOSS.AircraftCarrier.AV8B and self.carriertype == AIRBOSS.CarrierType.JCARLOS then
      self:RadioTransmission( self.LSORadio, self.LSOCall.EXPECTSPOT5, false, 5, nil, true )
    elseif playerData.actype == AIRBOSS.AircraftCarrier.AV8B and self.carriertype == AIRBOSS.CarrierType.CANBERRA then
      self:RadioTransmission( self.LSORadio, self.LSOCall.EXPECTSPOT5, false, 5, nil, true )
    elseif playerData.actype == AIRBOSS.AircraftCarrier.AV8B then
      self:RadioTransmission( self.LSORadio, self.LSOCall.EXPECTSPOT75, false, 5, nil, true )
    end

    -- Hint for player about altitude, AoA etc.
    self:_PlayerHint( playerData, 3 )

    -- Next step: ninety.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.NINETY )

  end
end

--- At the Ninety.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
function AIRBOSS:_Ninety( playerData )

  -- VNAO Edit - Added wrapped up call to LSO grading
  playerData.wrappedUpAtWakeLittle = false -- VNAO Edit - Added
  playerData.wrappedUpAtWakeFull = false -- VNAO Edit - Added 
  playerData.wrappedUpAtWakeUnderline = false -- VNAO Edit - Added
  playerData.wrappedUpAtStartLittle = false -- VNAO Edit - Added
  playerData.wrappedUpAtStartFull = false  -- VNAO Edit - Added
  playerData.wrappedUpAtStartUnderline = false -- VNAO Edit - Added
  playerData.AAatWakeLittle = false -- VNAO Edit - Added
  playerData.AAatWakeFull = false -- VNAO Edit - Added
  playerData.AAatWakeUnderline = false -- VNAO Edit - Added
  playerData.AFU = false -- VNAO Edit - Added

  -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier)
  local X, Z = self:_GetDistances( playerData.unit )

  -- Check abort conditions.
  if self:_CheckAbort( X, Z, self.Ninety ) then
    self:_AbortPattern( playerData, X, Z, self.Ninety, true )
    return
  end

  -- Get Realtive heading player to carrier.
  local relheading = self:_GetRelativeHeading( playerData.unit, false )

  -- At the 90, i.e. 90 degrees between player heading and BRC of carrier.
  if relheading <= 90 then

    -- Hint for player about altitude, AoA etc.
    self:_PlayerHint( playerData )

    -- Next step: wake.
    if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then
      -- Harrier has no wake stop. It stays port of the boat.
      self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.FINAL )
    else
      self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.WAKE )
    end

  elseif relheading > 90 and self:_CheckLimits( X, Z, self.Wake ) then
    -- Message to player.
    self:MessageToPlayer( playerData, "you are already at the wake and have not passed the 90. Turn faster next time!", "LSO" )
    self:RadioTransmission( self.LSORadio, self.LSOCall.DEPARTANDREENTER, nil, nil, nil, true )
    playerData.wop = true
    -- Debrief.
    self:_AddToDebrief( playerData, "Overshoot at wake - Pattern Waveoff!" )
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.DEBRIEF )
  end
end

--- At the Wake.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
function AIRBOSS:_Wake( playerData )

  -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier)
  local X, Z = self:_GetDistances( playerData.unit )

  -- Check abort conditions.
  if self:_CheckAbort( X, Z, self.Wake ) then
    self:_AbortPattern( playerData, X, Z, self.Wake, true )
    return
  end

  -- Right behind the wake of the carrier dZ>0.
  if self:_CheckLimits( X, Z, self.Wake ) then

    -- Hint for player about altitude, AoA etc.
    self:_PlayerHint( playerData )

    -- Next step: Final.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.FINAL )

  end
end

--- Get groove data.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @return #AIRBOSS.GrooveData Groove data table.
function AIRBOSS:_GetGrooveData( playerData )

  -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier).
  local X, Z = self:_GetDistances( playerData.unit )

  -- Stern position at the rundown.
  local stern = self:_GetSternCoord()

  -- Distance from rundown to player aircraft.
  local rho = stern:Get2DDistance( playerData.unit:GetCoordinate() )

  -- Aircraft is behind the carrier.
  local astern = X < self.carrierparam.sterndist

  -- Correct sign. Negative if passed rundown.
  if astern == false then
    rho = -rho
  end

  -- Velocity vector.
  local vel = playerData.unit:GetVelocityVec3()

  -- Grade, points, details
  local Gg, Gp, Gd = self:_LSOgrade( playerData )

  -- Gather pilot data.
  local groovedata = {} -- #AIRBOSS.GrooveData
  groovedata.Step = playerData.step
  groovedata.Time = timer.getTime()
  groovedata.Rho = rho
  groovedata.X = X
  groovedata.Z = Z
  groovedata.Alt = self:_GetAltCarrier( playerData.unit )
  groovedata.AoA = playerData.unit:GetAoA()
  groovedata.GSE = self:_Glideslope( playerData.unit )
  groovedata.LUE = self:_Lineup( playerData.unit, true )
  groovedata.LUEwire = self:_LineupWIRE( playerData.unit, true ) -- VNAO Edit - Added
  groovedata.LeftNozzle = self:_NozzleArgumentLeft( playerData.unit ) -- VNAO Edit - Added
  groovedata.RightNozzle = self:_NozzleArgumentRight( playerData.unit ) -- VNAO Edit - Added
  groovedata.Roll = playerData.unit:GetRoll()
  groovedata.Pitch = playerData.unit:GetPitch()
  groovedata.Yaw = playerData.unit:GetYaw()
  groovedata.Vel = UTILS.VecNorm( vel )
  groovedata.Vy = vel.y
  groovedata.Gamma = self:_GetRelativeHeading( playerData.unit, true )
  groovedata.Grade = Gg
  groovedata.GradePoints = Gp
  groovedata.GradeDetail = Gd

  -- env.info(string.format(", %.6f, %.6f, %.6f, %.6f, %.6f, %.6f, %.6f", groovedata.Time, groovedata.Rho, groovedata.X, groovedata.Alt, groovedata.GSE, groovedata.LUE, groovedata.AoA))

  return groovedata
end

--- Turn to final.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @param #boolean nocheck If true, player is not checked to be in the right position.
function AIRBOSS:_Final( playerData, nocheck )

  -- Get distances between carrier and player unit (parallel and perpendicular to direction of movement of carrier)
  local X, Z, rho, phi = self:_GetDistances( playerData.unit )

  -- In front of carrier or more than 4 km behind carrier.
  if not nocheck then
    if self:_CheckAbort( X, Z, self.Final ) then
      self:_AbortPattern( playerData, X, Z, self.Final, true )
      return
    end
  end

  -- Get Groove data
  local groovedata = self:_GetGrooveData( playerData )

  -- Trap sheet data.
  table.insert( playerData.trapsheet, groovedata )

  -- Get groove zone.
  local zone = self:_GetZoneGroove()

  -- Check if player is in zone.
  local inzone = playerData.unit:IsInZone( zone )

  -- Check.
  if inzone then -- and math.abs(groovedata.Roll)<5 then

    -- Hint for player about altitude, AoA etc. Sound is off.
    self:_PlayerHint( playerData, nil, true )

    -- Init FlyThrough.
    groovedata.FlyThrough = nil

    -- TODO: could add angled approach if lineup<5 and relhead>5. This would mean the player has not turned in correctly!

    -- Groove data.
    playerData.groove.X0 = UTILS.DeepCopy( groovedata )

    -- Set time stamp. Next call in 4 seconds.
    playerData.Tlso = timer.getTime()

    -- Next step: X start.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_XX )
  end

  -- Groovedata step.
  groovedata.Step = playerData.step

end

--- In the groove.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
function AIRBOSS:_Groove( playerData )

  -- Ranges in the groove.
  local RX0 = UTILS.NMToMeters( 1.000 ) -- Everything before X    1.00  = 1852 m
  local RXX = UTILS.NMToMeters( 0.750 ) -- Start of groove.       0.75  = 1389 m
  local RIM = UTILS.NMToMeters( 0.500 ) -- In the Middle          0.50  =  926 m (middle one third of the glideslope)
  local RIC = UTILS.NMToMeters( 0.250 ) -- In Close               0.25  =  463 m (last one third of the glideslope)
  local RAR = UTILS.NMToMeters( 0.040 ) -- At the Ramp.           0.04  =   75 m
  local RIW = UTILS.NMToMeters( -0.020 ) -- In the wires.           0.04  =   75 m -- VNAO Edit - Added

  -- Groove data.
  local groovedata = self:_GetGrooveData( playerData )

  -- Add data to trapsheet.
  table.insert( playerData.trapsheet, groovedata )

  -- Coords.
  local X = groovedata.X
  local Z = groovedata.Z

  -- Check abort conditions.
  if self:_CheckAbort( groovedata.X, groovedata.Z, self.Groove ) then
    self:_AbortPattern( playerData, groovedata.X, groovedata.Z, self.Groove, true )
    return
  end

  -- Shortcuts.
  local rho = groovedata.Rho
  local lineupError = groovedata.LUE
  local glideslopeError = groovedata.GSE
  local AoA = groovedata.AoA

  -- if rho <= RXX and playerData.step == AIRBOSS.PatternStep.GROOVE_XX and (math.abs( groovedata.Roll ) <= 4.0 or playerData.unit:IsInZone( self:_GetZoneLineup() )) then -- VNAO Edit - Commented out
  if rho <= RXX and playerData.step == AIRBOSS.PatternStep.GROOVE_XX and (math.abs( groovedata.Roll ) <= 3.5 or playerData.unit:IsInZone( self:_GetZoneLineup() )) then -- VNAO Edit - Added

    -- Start time in groove
    playerData.TIG0 = timer.getTime()

    -- LSO "Call the ball" call.
    self:RadioTransmission( self.LSORadio, self.LSOCall.CALLTHEBALL, nil, nil, nil, true )
    playerData.Tlso = timer.getTime()

    -- Pilot "405, Hornet Ball, 3.2".

    -- LSO "Roger ball" call in three seconds.
    self:RadioTransmission( self.LSORadio, self.LSOCall.ROGERBALL, false, nil, 2, true )

    -- Store data.
    playerData.groove.XX = UTILS.DeepCopy( groovedata )

    -- This is a valid approach and player did not miss any important steps in the pattern.
    playerData.valid = true

    -- Next step: in the middle.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_IM )

  elseif rho <= RIM and playerData.step == AIRBOSS.PatternStep.GROOVE_IM then

    -- Store data.
    playerData.groove.IM = UTILS.DeepCopy( groovedata )

    -- Next step: in close.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_IC )

  elseif rho <= RIC and playerData.step == AIRBOSS.PatternStep.GROOVE_IC then

    -- Store data.
    playerData.groove.IC = UTILS.DeepCopy( groovedata )

    -- Next step: AR at the ramp.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_AR )

  elseif rho <= RAR and playerData.step == AIRBOSS.PatternStep.GROOVE_AR then

    -- Store data.
    playerData.groove.AR = UTILS.DeepCopy( groovedata )

    -- Next step: in the wires.
    if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then
      self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_AL )
    else
      self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_IW )
    end
  elseif rho <= RIW and playerData.step == AIRBOSS.PatternStep.GROOVE_IW then -- VNAO Edit - Added
    playerData.groove.IW = UTILS.DeepCopy( groovedata ) -- VNAO Edit - Added
  elseif rho <= RAR and playerData.step == AIRBOSS.PatternStep.GROOVE_AL then

    -- Store data.
    playerData.groove.AL = UTILS.DeepCopy( groovedata )

    -- Get zone abeam LDG spot.
    local ZoneALS = self:_GetZoneAbeamLandingSpot()

    -- Get player velocity in km/h.
    local vplayer = playerData.unit:GetVelocityKMH()

    -- Get carrier velocity in km/h.
    local vcarrier = self.carrier:GetVelocityKMH()

    -- Speed difference.
    local dv = math.abs( vplayer - vcarrier )


    -- Stable when speed difference < 30 km/h.(16 Kts)Pene Testing
    local stable=dv<30

    -- Check if player is inside the zone.
    if playerData.unit:IsInZone( ZoneALS ) and stable then

      -- Radio Transmission "Cleared to land" once the aircraft is inside the zone.
      self:RadioTransmission( self.LSORadio, self.LSOCall.CLEAREDTOLAND, nil, nil, nil, true )

      -- Next step: Level cross.
      self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.GROOVE_LC )
      -- Set Stable Hover
      playerData.stable = true
      playerData.hover = true
    end

  elseif rho <= RAR and playerData.step == AIRBOSS.PatternStep.GROOVE_LC then

    -- Store data.
    playerData.groove.LC = UTILS.DeepCopy( groovedata )

    -- Get zone primary LDG spot.
    local ZoneLS = self:_GetZoneLandingSpot()

    -- Get player velocity in km/h.
    local vplayer = playerData.unit:GetVelocityKMH()

    -- Get carrier velocity in km/h.
    local vcarrier = self.carrier:GetVelocityKMH()

    -- Speed difference.
    local dv = math.abs( vplayer - vcarrier )

    -- Stable when v<15 km/h.
    local stable=dv<15

    -- Radio Transmission "Stabilized" once the aircraft has been cleared to cross and is over the Landing Spot and stable.
    if playerData.unit:IsInZone( ZoneLS ) and stable and playerData.stable == true then
      self:RadioTransmission( self.LSORadio, self.LSOCall.STABILIZED, nil, nil, nil, false )
      playerData.stable = false
      playerData.warning = true
    end

    -- We keep it in this step until landed.

  end

  --------------
  -- Wave Off --
  --------------

  -- Between IC and AR check for wave off.
  if rho >= RAR and rho <= RIC and not playerData.waveoff then

    -- Check if player should wave off.
    local waveoff = self:_CheckWaveOff( glideslopeError, lineupError, AoA, playerData )

    -- Let's see..
    if waveoff then

      -- Debug info.
      self:T3( self.lid .. string.format( "Waveoff distance rho=%.1f m", rho ) )

      -- LSO Wave off!
      self:RadioTransmission( self.LSORadio, self.LSOCall.WAVEOFF, nil, nil, nil, true )
      playerData.Tlso = timer.getTime()

      -- Player was waved off!
      playerData.waveoff = true

      -- Nothing else necessary.
      return
    end

   end

    -- Long V/STOL groove time Wave Off over 75 seconds to IC - TOPGUN level Only. --pene testing (WIP)--- Need to think more about this.

  --if rho>=RAR and rho<=RIC and not playerData.waveoff and playerData.difficulty==AIRBOSS.Difficulty.HARD and  playerData.actype==      AIRBOSS.AircraftCarrier.AV8B then
   -- Get groove time
   --local vSlow=groovedata.time
   -- If too slow wave off.
   --if vSlow >75 then

   -- LSO Wave off!
     --self:RadioTransmission(self.LSORadio, self.LSOCall.WAVEOFF, nil, nil, nil, true)
     --playerData.Tlso=timer.getTime()

   -- Player was waved Off
     --playerData.waveoff=true
     --return
  --end
 --end

  -- Groovedata step.
  groovedata.Step = playerData.step

  -----------------
  -- Groove Data --
  -----------------

  -- Check if we are beween 3/4 NM and end of ship.
  if rho >= RAR and rho < RX0 and playerData.waveoff == false then

    -- Get groove step short hand of the previous step.
    local gs = self:_GS( playerData.step, -1 )

    -- Get current groove data.
    local gd = playerData.groove[gs] -- #AIRBOSS.GrooveData

    if gd then
      self:T3( gd )

      -- Distance in NM.
      local d = UTILS.MetersToNM( rho )

      -- VNAO Edit - Added wrapped up call to LSO grading
      if playerData.case ~=3 then -- VNAO Edit - Added
        -- env.info("ADAM AIRBOSS EDIT- function AIRBOSS:_Groove(playerData) - playerData.case ~= 3, proceeding with checks") -- VNAO Edit - Added
        if playerData.wrappedUpAtWakeUnderline == true or playerData.wrappedUpAtStartUnderline == true then -- VNAO Edit - Added
          gd.WrappedUp="_WU_" -- VNAO Edit - Added
          -- env.info("ADAM AIRBOSS EDIT- function AIRBOSS:_Groove(playerData) - UNDERLINE WU comment should be added for player: "..playerData.name) -- VNAO Edit - Added
        elseif playerData.wrappedUpAtWakeUnderline == false and playerData.wrappedUpAtStartUnderline == false then -- VNAO Edit - Added
          if playerData.wrappedUpAtWakeFull == true or playerData.wrappedUpAtStartFull == true then -- VNAO Edit - Added
            gd.WrappedUp="WU" -- VNAO Edit - Added
            -- env.info("ADAM AIRBOSS EDIT- function AIRBOSS:_Groove(playerData) - FULL WU comment should be added for player: "..playerData.name) -- VNAO Edit - Added
          elseif playerData.wrappedUpAtStartFull == false then -- VNAO Edit - Added
            if playerData.wrappedUpAtWakeLittle == true or playerData.wrappedUpAtStartLittle == true then -- VNAO Edit - Added
              gd.WrappedUp="(WU)" -- VNAO Edit - Added
              -- env.info("ADAM AIRBOSS EDIT- function AIRBOSS:_Groove(playerData) -  little WU comment should be added for player: "..playerData.name) -- VNAO Edit - Added
            end -- VNAO Edit - Added
          end -- VNAO Edit - Added
        else -- VNAO Edit - Added
          -- gd.WrappedUp="" -- VNAO Edit - Added
          -- env.info("ADAM AIRBOSS EDIT- function AIRBOSS:_Groove(playerData) -  NO WU comment should be added for player: "..playerData.name) -- VNAO Edit - Added
        end -- VNAO Edit - Added

        if playerData.AAatWakeUnderline == true then -- VNAO Edit - Added
          gd.AngledApch="_AA_" -- VNAO Edit - Added
          -- env.info("ADAM AIRBOSS EDIT- function AIRBOSS:_Groove(playerData) - UNDERLINE AA comment should be added for player: "..playerData.name)  -- VNAO Edit - Added
        elseif playerData.AAatWakeUnderline == false then -- VNAO Edit - Added
          if playerData.AAatWakeFull == true then  -- VNAO Edit - Added
            gd.AngledApch="AA" -- VNAO Edit - Added
            -- env.info("ADAM AIRBOSS EDIT- function AIRBOSS:_Groove(playerData) - FULL AA comment should be added for player: "..playerData.name)  -- VNAO Edit - Added
          elseif playerData.AAatWakeFull == false then  -- VNAO Edit - Added
            if playerData.AAatWakeLittle == true then  -- VNAO Edit - Added
              gd.AngledApch="(AA)" -- VNAO Edit - Added
              -- env.info("ADAM AIRBOSS EDIT- function AIRBOSS:_Groove(playerData) -  little AA comment should be added for player: "..playerData.name)  -- VNAO Edit - Added
            end -- VNAO Edit - Added
          end -- VNAO Edit - Added
        else -- VNAO Edit - Added
          -- gd.AngledApch="" -- VNAO Edit - Added
          -- env.info("ADAM AIRBOSS EDIT- function AIRBOSS:_Groove(playerData) -  NO AA comment should be added for player: "..playerData.name)  -- VNAO Edit - Added
        end -- VNAO Edit - Added

        if playerData.AFU == true then -- VNAO Edit - Added
          gd.AFU="AFU" -- VNAO Edit - Added
          -- env.info("ADAM AIRBOSS EDIT- function AIRBOSS:_Groove(playerData) -  AFU comment should be added for player: "..playerData.name)  -- VNAO Edit - Added
        else  -- VNAO Edit - Added
          -- env.info("ADAM AIRBOSS EDIT- function AIRBOSS:_Groove(playerData) -  NO AFU comment should be added for player: "..playerData.name)  -- VNAO Edit - Added
        end -- VNAO Edit - Added
      else  -- VNAO Edit - Added
        -- gd.WrappedUp="" -- VNAO Edit - Added
        -- gd.AngledApch="" -- VNAO Edit - Added
        --gd.AFU="" -- VNAO Edit - Added
        -- env.info("ADAM AIRBOSS EDIT- function AIRBOSS:_Groove(playerData) -  CASE 3, don't add WU, AA or AFU comments at all for player: "..playerData.name)  -- VNAO Edit - Added
      end

      -- Drift on lineup.
      if rho >= RAR and rho <= RIM then
        if gd.LUE > 0.22 and lineupError < -0.22 then
          env.info " Drift Right across centre ==> DR-"
          gd.Drift = "DR"
          self:T( self.lid .. string.format( "Got Drift Right across centre step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError ) )
        elseif gd.LUE < -0.22 and lineupError > 0.22 then
          env.info " Drift Left ==> DL-"
          gd.Drift = "DL"
          self:T( self.lid .. string.format( "Got Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError ) )
        elseif gd.LUE > 0.13 and lineupError < -0.14 then
          env.info " Little Drift Right across centre ==> (DR-)"
          gd.Drift = "(DR)"
          self:T( self.lid .. string.format( "Got Little Drift Right across centre at step %s, d=%.3f: Max LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError ) )
        elseif gd.LUE < -0.13 and lineupError > 0.14 then
          env.info " Little Drift Left across centre ==> (DL-)"
          gd.Drift = "(DL)"
          self:E( self.lid .. string.format( "Got Little Drift Left across centre at step %s, d=%.3f: Min LUE=%.3f, lower LUE=%.3f", gs, d, gd.LUE, lineupError ) )
        end
      end

      -- Update max deviation of line up error.
      if math.abs( lineupError ) > math.abs( gd.LUE ) then
        self:T( self.lid .. string.format( "Got bigger LUE at step %s, d=%.3f: LUE %.3f>%.3f", gs, d, lineupError, gd.LUE ) )
        gd.LUE = lineupError
      end

      -- Fly through good window of glideslope.
      if gd.GSE > 0.4 and glideslopeError < -0.3 then
        -- Fly through down ==> "\"
        gd.FlyThrough = "\\"
        self:T( self.lid .. string.format( "Got Fly through DOWN at step %s, d=%.3f: Max GSE=%.3f, lower GSE=%.3f", gs, d, gd.GSE, glideslopeError ) )
      elseif gd.GSE < -0.3 and glideslopeError > 0.4 then
        -- Fly through up ==> "/"
        gd.FlyThrough = "/"
        self:E( self.lid .. string.format( "Got Fly through UP at step %s, d=%.3f: Min GSE=%.3f, lower GSE=%.3f", gs, d, gd.GSE, glideslopeError ) )
      end

      -- Update max deviation of glideslope error.
      if math.abs( glideslopeError ) > math.abs( gd.GSE ) then
        self:T( self.lid .. string.format( "Got bigger GSE at step %s, d=%.3f: GSE |%.3f|>|%.3f|", gs, d, glideslopeError, gd.GSE ) )
        gd.GSE = glideslopeError
      end

      -- Get aircraft AoA parameters.
      local aircraftaoa = self:_GetAircraftAoA( playerData )

      -- On Speed AoA.
      local aoaopt = aircraftaoa.OnSpeed

      -- Compare AoAs wrt on speed AoA and update max deviation.
      if math.abs( AoA - aoaopt ) > math.abs( gd.AoA - aoaopt ) then
        self:T( self.lid .. string.format( "Got bigger AoA error at step %s, d=%.3f: AoA %.3f>%.3f.", gs, d, AoA, gd.AoA ) )
        gd.AoA = AoA
      end

      -- local gs2=self:_GS(groovedata.Step, -1)
      -- env.info(string.format("groovestep %s %s d=%.3f NM: GSE=%.3f %.3f, LUE=%.3f %.3f, AoA=%.3f %.3f", gs, gs2, d, groovedata.GSE, gd.GSE, groovedata.LUE, gd.LUE, groovedata.AoA, gd.AoA))

    end

    ---------------
    -- LSO Calls --
    ---------------

    -- Time since last LSO call.
    local deltaT = timer.getTime() - playerData.Tlso

    -- Wait until player passed the 0.75 NM distance.
    local _advice = true
    if playerData.TIG0 == nil and playerData.difficulty ~= AIRBOSS.Difficulty.EASY then -- rho>RXX
      _advice = false
    end

    -- LSO call if necessary.
    if deltaT >= self.LSOdT and _advice then
      self:_LSOadvice( playerData, glideslopeError, lineupError )
    end

  end

  ----------------------------------------------------------
  --- Some time here the landing event MIGHT be triggered --
  ----------------------------------------------------------

  -- Player infront of the carrier X>~77 m.
  if X > self.carrierparam.totlength + self.carrierparam.sterndist then

    if playerData.waveoff then

      if playerData.landed then
        -- This should not happen because landing event was triggered.
        self:_AddToDebrief( playerData, "You were waved off but landed anyway. Airboss wants to talk to you!" )
      else
        self:_AddToDebrief( playerData, "You were waved off." )
      end

    elseif playerData.boltered then

      -- This should not happen because landing event was triggered.
      self:_AddToDebrief( playerData, "You boltered." )

    else

      -- This should not happen.
      self:T( "Player was not waved off but flew past the carrier without landing ==> Own wave off!" )

      -- We count this as OWO.
      self:_AddToDebrief( playerData, "Own waveoff." )

      -- Set Owo
      playerData.owo = true

    end

    -- Next step: debrief.
    self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.DEBRIEF )

  end

end

--- LSO check if player needs to wave off.
-- Wave off conditions are:
--
-- * Glideslope error <1.2 or >1.8 degrees.
-- * |Line up error| > 3 degrees.
-- * AoA check but only for TOPGUN graduates.
-- @param #AIRBOSS self
-- @param #number glideslopeError Glideslope error in degrees.
-- @param #number lineupError Line up error in degrees.
-- @param #number AoA Angle of attack of player aircraft.
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @return #boolean If true, player should wave off!
function AIRBOSS:_CheckWaveOff( glideslopeError, lineupError, AoA, playerData )

  -- Assume we're all good.
  local waveoff = false

  -- Parameters
  local glMax = 1.8
  local glMin = -1.2
  local luAbs = 3.0

  -- For the harrier, we allow a bit more room.
  if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then
    glMax = 2.6
    glMin = -2.2 -- Testing, @Engines may be just dragging it in on Hermes, or the carrier parameters need adjusting.
    luAbs = 4.1 -- Testing Pene.

  end

  -- Too high or too low?
  if glideslopeError > glMax then
    local text = string.format( "\n- Waveoff due to glideslope error %.2f > %.1f degrees!", glideslopeError, glMax )
    self:T( self.lid .. string.format( "%s: %s", playerData.name, text ) )
    self:_AddToDebrief( playerData, text )
    waveoff = true
  elseif glideslopeError < glMin then
    local text = string.format( "\n- Waveoff due to glideslope error %.2f < %.1f degrees!", glideslopeError, glMin )
    self:T( self.lid .. string.format( "%s: %s", playerData.name, text ) )
    self:_AddToDebrief( playerData, text )
    waveoff = true
  end

  -- Too far from centerline?
  if math.abs( lineupError ) > luAbs then
    local text = string.format( "\n- Waveoff due to line up error |%.1f| > %.1f degrees!", lineupError, luAbs )
    self:T( self.lid .. string.format( "%s: %s", playerData.name, text ) )
    self:_AddToDebrief( playerData, text )
    waveoff = true
  end

  -- Too slow or too fast? Only for pros.

  if playerData.difficulty == AIRBOSS.Difficulty.HARD and playerData.actype ~= AIRBOSS.AircraftCarrier.AV8B then
    -- Get aircraft specific AoA values. Not for AV-8B due to transition to Stable Hover.
    local aoaac = self:_GetAircraftAoA( playerData )
    -- Check too slow or too fast.
    if AoA < aoaac.FAST then
      local text = string.format( "\n- Waveoff due to AoA %.1f < %.1f!", AoA, aoaac.FAST )
      self:T( self.lid .. string.format( "%s: %s", playerData.name, text ) )
      self:_AddToDebrief( playerData, text )
      waveoff = true
    elseif AoA > aoaac.SLOW then
      local text = string.format( "\n- Waveoff due to AoA %.1f > %.1f!", AoA, aoaac.SLOW )
      self:T( self.lid .. string.format( "%s: %s", playerData.name, text ) )
      self:_AddToDebrief( playerData, text )
      waveoff = true

    end
  end

  return waveoff
end

--- Check if other aircraft are currently on the landing runway.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @return boolean If true, we have a foul deck.
function AIRBOSS:_CheckFoulDeck( playerData )

  -- Assume no check necessary.
  local check = false

  -- CVN: Check at IM and IC.
  if playerData.step == AIRBOSS.PatternStep.GROOVE_IM or playerData.step == AIRBOSS.PatternStep.GROOVE_IC then
    check = true
  end

  -- AV-8B check until
  if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then
    if playerData.step == AIRBOSS.PatternStep.GROOVE_AR or playerData.step == AIRBOSS.PatternStep.GROOVE_AL then
      check = true
    end
  end

  -- Check if player was already waved off. Should not be necessary as player step is set to debrief afterwards!
  if playerData.wofd == true or check == false then
    -- Player was already waved off.
    return
  end

  -- Landing runway zone.
  local runway = self:_GetZoneRunwayBox()

  -- For AB-8B we just check the primary landing spot.
  if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then
    runway = self:_GetZoneLandingSpot()
  end

  -- Scan radius.
  local R = 250

  -- Debug info.
  self:T( self.lid .. string.format( "Foul deck check: Scanning Carrier Runway Area. Radius=%.1f m.", R ) )

  -- Scan units in carrier zone.
  local _, _, _, unitscan = self:GetCoordinate():ScanObjects( R, true, false, false )

  -- Loop over all scanned units and check if they are on the runway.
  local fouldeck = false
  local foulunit = nil -- Wrapper.Unit#UNIT
  for _, _unit in pairs( unitscan ) do
    local unit = _unit -- Wrapper.Unit#UNIT

    -- Check if unit is in zone.
    local inzone = unit:IsInZone( runway )

    -- Check if aircraft and in air.
    local isaircraft = unit:IsAir()
    local isairborn = unit:InAir()

    if inzone and isaircraft and not isairborn then
      local text = string.format( "Unit %s on landing runway ==> Foul deck!", unit:GetName() )
      self:T( self.lid .. text )
      MESSAGE:New( text, 10 ):ToAllIf( self.Debug )
      if self.Debug then
        runway:FlareZone( FLARECOLOR.Red, 30 )
      end
      fouldeck = true
      foulunit = unit
    end
  end

  -- Add to debrief and
  if playerData and fouldeck then

    -- Debrief text.
    local text = string.format( "Foul deck waveoff due to aircraft %s!", foulunit:GetName() )
    self:T( self.lid .. string.format( "%s: %s", playerData.name, text ) )
    self:_AddToDebrief( playerData, text )

    -- Foul deck + wave off radio message.
    self:RadioTransmission( self.LSORadio, self.LSOCall.FOULDECK, false, 1 )
    self:RadioTransmission( self.LSORadio, self.LSOCall.WAVEOFF, false, 1.2, nil, true )

    -- Player hint for flight students.
    if playerData.showhints then
      local text = string.format( "overfly landing area and enter bolter pattern." )
      self:MessageToPlayer( playerData, text, "LSO", nil, nil, false, 3 )
    end

    -- Set player parameters for foul deck.
    playerData.wofd = true

    -- Debrief.
    playerData.step = AIRBOSS.PatternStep.DEBRIEF
    playerData.warning = nil

    -- Pass would be invalid if the player lands.
    playerData.valid = false

    -- Send a message to the player that blocks the runway.
    if foulunit then
      local foulflight = self:_GetFlightFromGroupInQueue( foulunit:GetGroup(), self.flights )
      if foulflight and not foulflight.ai then
        self:MessageToPlayer( foulflight, "move your ass from my runway. NOW!", "AIRBOSS" )
      end
    end
  end

  return fouldeck
end

--- Get "stern" coordinate.
-- @param #AIRBOSS self
-- @return Core.Point#COORDINATE Coordinate at the rundown of the carrier.
function AIRBOSS:_GetSternCoord()

  -- Heading of carrier (true).
  local hdg = self.carrier:GetHeading()

  -- Final bearing (true).
  local FB=self:GetFinalBearing()
  local case=self.case

  -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing.
  self.sterncoord:UpdateFromCoordinate( self:GetCoordinate() )
  -- local stern=self:GetCoordinate()

  -- Stern coordinate (sterndist<0). --Pene testing Case III
  if self.carriertype==AIRBOSS.CarrierType.INVINCIBLE or self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA then
    if case==3 then
    -- CASE III V/STOL translation Due over deck approach if needed.
    self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(8, FB-90, true, true)
    elseif case==2 or case==1 then
    -- V/Stol: Translate 8 meters port.
    self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(8, FB-90, true, true)
  end
  elseif self.carriertype==AIRBOSS.CarrierType.STENNIS then
    -- Stennis: translate 7 meters starboard wrt Final bearing.
    self.sterncoord:Translate( self.carrierparam.sterndist, hdg, true, true ):Translate( 7, FB + 90, true, true )
  elseif self.carriertype == AIRBOSS.CarrierType.FORRESTAL then
    -- Forrestal
    self.sterncoord:Translate( self.carrierparam.sterndist, hdg, true, true ):Translate( 7.5, FB + 90, true, true )
  elseif self.carriertype == AIRBOSS.CarrierType.ESSEX then
    -- Forrestal
    self.sterncoord:Translate( self.carrierparam.sterndist, hdg, true, true ):Translate( -1, FB + 90, true, true )
  else
    -- Nimitz SC: translate 8 meters starboard wrt Final bearing.
    self.sterncoord:Translate( self.carrierparam.sterndist, hdg, true, true ):Translate( 9.5, FB + 90, true, true )
  end

  -- Set altitude.
  self.sterncoord:SetAltitude( self.carrierparam.deckheight )

  return self.sterncoord
end

--- Get wire from draw argument.
-- @param #AIRBOSS self
-- @param Core.Point#COORDINATE Lcoord Landing position.
-- @return #number Trapped wire (1-4) or 99 if no wire was trapped.
function AIRBOSS:_GetWireFromDrawArg()

  local wireArgs={}
  wireArgs[1]=141
  wireArgs[2]=142
  wireArgs[3]=143
  wireArgs[4]=144

  for wire,drawArg in pairs(wireArgs) do
    local value=self.carrier:GetDrawArgumentValue(drawArg)
    if math.abs(value)>0.001 then
      return wire
    end
  end

  return 99
end

--- Get wire from landing position.
-- @param #AIRBOSS self
-- @param Core.Point#COORDINATE Lcoord Landing position.
-- @param #number dc Distance correction. Shift the landing coord back if dc>0 and forward if dc<0.
-- @return #number Trapped wire (1-4) or 99 if no wire was trapped.
function AIRBOSS:_GetWire( Lcoord, dc )

  -- Final bearing (true).
  local FB = self:GetFinalBearing()

  -- Stern coordinate (sterndist<0). Also translate 10 meters starboard wrt Final bearing.
  local Scoord = self:_GetSternCoord()

  -- Distance to landing coord.
  local Ldist = Lcoord:Get2DDistance( Scoord )

  -- For human (not AI) the lading event is delayed unfortunately. Therefore, we need another correction factor.
  dc = dc or 65

  -- Corrected landing distance wrt to stern. Landing distance needs to be reduced due to delayed landing event for human players.
  local d = Ldist - dc

  -- Multiplayer wire correction.
  if self.mpWireCorrection then
    d = d - self.mpWireCorrection
  end

  -- Shift wires from stern to their correct position.
  local w1 = self.carrierparam.wire1
  local w2 = self.carrierparam.wire2
  local w3 = self.carrierparam.wire3
  local w4 = self.carrierparam.wire4

  -- Which wire was caught?
  local wire
  if d < w1 then -- 46
    wire = 1
  elseif d < w2 then -- 46+12
    wire = 2
  elseif d < w3 then -- 46+24
    wire = 3
  elseif d < w4 then -- 46+35
    wire = 4
  else
    wire = 99
  end

  if self.Debug and false then

    -- Wire position coordinates.
    local wp1 = Scoord:Translate( w1, FB )
    local wp2 = Scoord:Translate( w2, FB )
    local wp3 = Scoord:Translate( w3, FB )
    local wp4 = Scoord:Translate( w4, FB )

    -- Debug marks.
    wp1:MarkToAll( "Wire 1" )
    wp2:MarkToAll( "Wire 2" )
    wp3:MarkToAll( "Wire 3" )
    wp4:MarkToAll( "Wire 4" )

    -- Mark stern.
    Scoord:MarkToAll( "Stern" )

    -- Mark at landing position.
    Lcoord:MarkToAll( string.format( "Landing Point wire=%s", wire ) )

    -- Smoke landing position.
    Lcoord:SmokeGreen()

    -- Corrected landing position.
    local Dcoord = Lcoord:Translate( -dc, FB )

    -- Smoke corrected landing pos red.
    Dcoord:SmokeRed()

  end

  -- Debug output.
  self:T( string.format( "GetWire: L=%.1f, L-dc=%.1f ==> wire=%d (dc=%.1f)", Ldist, Ldist - dc, wire, dc ) )

  return wire
end

--- Trapped? Check if in air or not after landing event.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
function AIRBOSS:_Trapped( playerData )

  if playerData.unit:InAir() == false then
    -- Seems we have successfully landed.

    -- Lets see if we can get a good wire.
    local unit = playerData.unit

    -- Coordinate of player aircraft.
    local coord = unit:GetCoordinate()

    -- Get velocity in km/h. We need to substrackt the carrier velocity.
    local v = unit:GetVelocityKMH() - self.carrier:GetVelocityKMH()

    -- Stern coordinate.
    local stern = self:_GetSternCoord()

    -- Distance to stern pos.
    local s = stern:Get2DDistance( coord )

    -- Get current wire (estimate). This now based on the position where the player comes to a standstill which should reflect the trapped wire better.
    local dcorr = 100
    if   playerData.actype == AIRBOSS.AircraftCarrier.HORNET
      or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE
      or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF
      or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER then
      dcorr = 100
    elseif playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then
      -- TODO: Check Tomcat.
      dcorr = 100
    elseif playerData.actype == AIRBOSS.AircraftCarrier.A4EC then
      -- A-4E gets slowed down much faster the the F/A-18C!
      dcorr = 56
    elseif playerData.actype == AIRBOSS.AircraftCarrier.T45C then
      -- T-45 also gets slowed down much faster the the F/A-18C.
      dcorr = 56
    end

    -- Get wire.
    local wire = self:_GetWire( coord, dcorr )

    -- Debug.
    local text = string.format( "Player %s _Trapped: v=%.1f km/h, s-dcorr=%.1f m ==> wire=%d (dcorr=%d)", playerData.name, v, s - dcorr, wire, dcorr )
    self:T( self.lid .. text )

    -- Call this function again until v < threshold. Player comes to a standstill ==> Get wire!
    if v > 5 then

      -- Check if we passed all wires.
      if wire > 4 and v > 10 and not playerData.warning then
        -- Looks like we missed the wires ==> Bolter!
        self:RadioTransmission( self.LSORadio, self.LSOCall.BOLTER, nil, nil, nil, true )
        playerData.warning = true
      end

      -- Call function again and check if converged or back in air.
      -- SCHEDULER:New(nil, self._Trapped, {self, playerData}, 0.1)
      self:ScheduleOnce( 0.1, self._Trapped, self, playerData )
      return
    end

    ----------------------------------------
    --- Form this point on we have converged
    ----------------------------------------

    -- Put some smoke and a mark.
    if self.Debug then
      coord:SmokeBlue()
      coord:MarkToAll( text )
      stern:MarkToAll( "Stern" )
    end

    -- Set player wire.
    playerData.wire = wire

    -- Message to player.
    local text = string.format( "Trapped %d-wire.", wire )
    if wire == 3 then
      text = text .. " " -- VNAO Edit - Removed comment text
    elseif wire == 2 then
      text = text .. " " -- VNAO Edit - Removed comment text
    elseif wire == 4 then
      text = text .. " " -- VNAO Edit - Removed comment text
    elseif wire == 1 then
      text = text .. " " -- VNAO Edit - Removed comment text
    end

    -- Message to player.
    self:MessageToPlayer( playerData, text, "LSO", "" )

    -- Debrief.
    local hint = string.format( "Trapped %d-wire.", wire )
    self:_AddToDebrief( playerData, hint, "Groove: IW" )

  else

    -- Again in air ==> Boltered!
    local text = string.format( "Player %s boltered in trapped function.", playerData.name )
    self:T( self.lid .. text )
    MESSAGE:New( text, 5, "DEBUG" ):ToAllIf( self.debug )

    -- Bolter switch on.
    playerData.boltered = true

  end

  -- Next step: debriefing.
  playerData.step = AIRBOSS.PatternStep.DEBRIEF
  playerData.warning = nil
end

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ZONE functions
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get Initial zone for Case I or II.
-- @param #AIRBOSS self
-- @param #number case Recovery Case.
-- @return Core.Zone#ZONE_POLYGON_BASE Initial zone.
function AIRBOSS:_GetZoneInitial( case )

  self.zoneInitial = self.zoneInitial or ZONE_POLYGON_BASE:New( "Zone CASE I/II Initial" )

  -- Get radial, i.e. inverse of BRC.
  local radial = self:GetRadial( 2, false, false )

  -- Carrier coordinate.
  local cv = self:GetCoordinate()

  -- Vec2 array.
  local vec2 = {}

  if case == 1 then
    -- Case I

    local c1 = cv:Translate( UTILS.NMToMeters( 0.5 ), radial - 90 ) --  0.0  0.5 starboard
    local c2 = cv:Translate( UTILS.NMToMeters( 1.3 ), radial - 90 ):Translate( UTILS.NMToMeters( 3 ), radial ) -- -3.0  1.3 starboard, astern
    local c3 = cv:Translate( UTILS.NMToMeters( 0.4 ), radial + 90 ):Translate( UTILS.NMToMeters( 3 ), radial ) -- -3.0 -0.4 port, astern
    local c4 = cv:Translate( UTILS.NMToMeters( 1.0 ), radial )
    local c5 = cv

    -- Vec2 array.
    vec2 = { c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2() }

  else
    -- Case II

    -- Funnel.
    local c1 = cv:Translate( UTILS.NMToMeters( 0.5 ), radial - 90 ) -- 0.0, 0.5
    local c2 = c1:Translate( UTILS.NMToMeters( 0.5 ), radial ) -- 0.5, 0.5
    local c3 = cv:Translate( UTILS.NMToMeters( 1.2 ), radial - 90 ):Translate( UTILS.NMToMeters( 3 ), radial ) -- 3.0, 1.2
    local c4 = cv:Translate( UTILS.NMToMeters( 1.2 ), radial + 90 ):Translate( UTILS.NMToMeters( 3 ), radial ) -- 3.0,-1.2
    local c5 = cv:Translate( UTILS.NMToMeters( 0.5 ), radial )
    local c6 = cv

    -- Vec2 array.
    vec2 = { c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2() }

  end

  -- Polygon zone.
  -- local zone=ZONE_POLYGON_BASE:New("Zone CASE I/II Initial", vec2)

  self.zoneInitial:UpdateFromVec2( vec2 )

  -- return zone
  return self.zoneInitial
end

--- Get lineup groove zone.
-- @param #AIRBOSS self
-- @return Core.Zone#ZONE_POLYGON_BASE Lineup zone.
function AIRBOSS:_GetZoneLineup()

  self.zoneLineup = self.zoneLineup or ZONE_POLYGON_BASE:New( "Zone Lineup" )

  -- Get radial, i.e. inverse of BRC.
  local fbi = self:GetRadial( 1, false, false )

  -- Stern coordinate.
  local st = self:_GetOptLandingCoordinate()

  -- Zone points.
  local c1 = st
  local c2 = st:Translate( UTILS.NMToMeters( 0.50 ), fbi + 15 )
  local c3 = st:Translate( UTILS.NMToMeters( 0.50 ), fbi + self.lue._max - 0.05 )
  local c4 = st:Translate( UTILS.NMToMeters( 0.77 ), fbi + self.lue._max - 0.05 )
  local c5 = c4:Translate( UTILS.NMToMeters( 0.25 ), fbi - 90 )

  -- Vec2 array.
  local vec2 = { c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2() }

  self.zoneLineup:UpdateFromVec2( vec2 )

  -- Polygon zone.
  -- local zone=ZONE_POLYGON_BASE:New("Zone Lineup", vec2)
  -- return zone

  return self.zoneLineup
end

--- Get groove zone.
-- @param #AIRBOSS self
-- @param #number l Length of the groove in NM. Default 1.5 NM.
-- @param #number w Width of the groove in NM. Default 0.25 NM.
-- @param #number b Width of the beginning in NM. Default 0.10 NM.
-- @return Core.Zone#ZONE_POLYGON_BASE Groove zone.
function AIRBOSS:_GetZoneGroove( l, w, b )

  self.zoneGroove = self.zoneGroove or ZONE_POLYGON_BASE:New( "Zone Groove" )

  l = l or 1.50
  w = w or 0.25
  b = b or 0.10

  -- Get radial, i.e. inverse of BRC.
  local fbi = self:GetRadial( 1, false, false )

  -- Stern coordinate.
  local st = self:_GetSternCoord()

  -- Zone points.
  local c1 = st:Translate( self.carrierparam.totwidthstarboard, fbi - 90 )
  local c2 = st:Translate( UTILS.NMToMeters( 0.10 ), fbi - 90 ):Translate( UTILS.NMToMeters( 0.3 ), fbi )
  local c3 = st:Translate( UTILS.NMToMeters( 0.25 ), fbi - 90 ):Translate( UTILS.NMToMeters( l ), fbi )
  local c4 = st:Translate( UTILS.NMToMeters( w / 2 ), fbi + 90 ):Translate( UTILS.NMToMeters( l ), fbi )
  local c5 = st:Translate( UTILS.NMToMeters( b ), fbi + 90 ):Translate( UTILS.NMToMeters( 0.3 ), fbi )
  local c6 = st:Translate( self.carrierparam.totwidthport, fbi + 90 )

  -- Vec2 array.
  local vec2 = { c1:GetVec2(), c2:GetVec2(), c3:GetVec2(), c4:GetVec2(), c5:GetVec2(), c6:GetVec2() }

  self.zoneGroove:UpdateFromVec2( vec2 )

  -- Polygon zone.
  -- local zone=ZONE_POLYGON_BASE:New("Zone Groove", vec2)
  -- return zone

  return self.zoneGroove
end

--- Get Bullseye zone with radius 1 NM and DME 3 NM from the carrier. Radial depends on recovery case.
-- @param #AIRBOSS self
-- @param #number case Recovery case.
-- @return Core.Zone#ZONE_RADIUS Arc in zone.
function AIRBOSS:_GetZoneBullseye( case )

  -- Radius = 1 NM.
  local radius = UTILS.NMToMeters( 1 )

  -- Distance = 3 NM
  local distance = UTILS.NMToMeters( 3 )

  -- Zone depends on Case recovery.
  local radial = self:GetRadial( case, false, false )

  -- Get coordinate and vec2.
  local coord = self:GetCoordinate():Translate( distance, radial )
  local vec2 = coord:GetVec2()

  -- Create zone.
  local zone = ZONE_RADIUS:New( "Zone Bullseye", vec2, radius )
  return zone

  -- self.zoneBullseye=self.zoneBullseye or ZONE_RADIUS:New("Zone Bullseye", vec2, radius)
end

--- Get dirty up zone with radius 1 NM and DME 9 NM from the carrier. Radial depends on recovery case.
-- @param #AIRBOSS self
-- @param #number case Recovery case.
-- @return Core.Zone#ZONE_RADIUS Dirty up zone.
function AIRBOSS:_GetZoneDirtyUp( case )

  -- Radius = 1 NM.
  local radius = UTILS.NMToMeters( 1 )

  -- Distance = 9 NM
  local distance = UTILS.NMToMeters( 9 )

  -- Zone depends on Case recovery.
  local radial = self:GetRadial( case, false, false )

  -- Get coordinate and vec2.
  local coord = self:GetCoordinate():Translate( distance, radial )
  local vec2 = coord:GetVec2()

  -- Create zone.
  local zone = ZONE_RADIUS:New( "Zone Dirty Up", vec2, radius )

  return zone
end

--- Get arc out zone with radius 1 NM and DME 12 NM from the carrier. Radial depends on recovery case.
-- @param #AIRBOSS self
-- @param #number case Recovery case.
-- @return Core.Zone#ZONE_RADIUS Arc in zone.
function AIRBOSS:_GetZoneArcOut( case )

  -- Radius = 1.25 NM.
  local radius = UTILS.NMToMeters( 1.25 )

  -- Distance = 12 NM
  local distance = UTILS.NMToMeters( 11.75 )

  -- Zone depends on Case recovery.
  local radial = self:GetRadial( case, false, false )

  -- Get coordinate of carrier and translate.
  local coord = self:GetCoordinate():Translate( distance, radial )

  -- Create zone.
  local zone = ZONE_RADIUS:New( "Zone Arc Out", coord:GetVec2(), radius )

  return zone
end

--- Get arc in zone with radius 1 NM and DME 14 NM from the carrier. Radial depends on recovery case.
-- @param #AIRBOSS self
-- @param #number case Recovery case.
-- @return Core.Zone#ZONE_RADIUS Arc in zone.
function AIRBOSS:_GetZoneArcIn( case )

  -- Radius = 1.25 NM.
  local radius = UTILS.NMToMeters( 1.25 )

  -- Zone depends on Case recovery.
  local radial = self:GetRadial( case, false, true )

  -- Angle between FB/BRC and holding zone.
  local alpha = math.rad( self.holdingoffset )

  -- 14+x NM from carrier
  local x = 14 -- /math.cos(alpha)

  -- Distance = 14 NM
  local distance = UTILS.NMToMeters( x )

  -- Get coordinate.
  local coord = self:GetCoordinate():Translate( distance, radial )

  -- Create zone.
  local zone = ZONE_RADIUS:New( "Zone Arc In", coord:GetVec2(), radius )

  return zone
end

--- Get platform zone with radius 1 NM and DME 19 NM from the carrier. Radial depends on recovery case.
-- @param #AIRBOSS self
-- @param #number case Recovery case.
-- @return Core.Zone#ZONE_RADIUS Circular platform zone.
function AIRBOSS:_GetZonePlatform( case )

  -- Radius = 1 NM.
  local radius = UTILS.NMToMeters( 1 )

  -- Zone depends on Case recovery.
  local radial = self:GetRadial( case, false, true )

  -- Angle between FB/BRC and holding zone.
  local alpha = math.rad( self.holdingoffset )

  -- Distance = 19 NM
  local distance = UTILS.NMToMeters( 19 ) -- /math.cos(alpha)

  -- Get coordinate.
  local coord = self:GetCoordinate():Translate( distance, radial )

  -- Create zone.
  local zone = ZONE_RADIUS:New( "Zone Platform", coord:GetVec2(), radius )

  return zone
end

--- Get approach corridor zone. Shape depends on recovery case.
-- @param #AIRBOSS self
-- @param #number case Recovery case.
-- @param #number l Length of the zone in NM. Default 31 (=21+10) NM.
-- @return Core.Zone#ZONE_POLYGON_BASE Box zone.
function AIRBOSS:_GetZoneCorridor( case, l )

  -- Total length.
  l = l or 31

  -- Radial and offset.
  local radial = self:GetRadial( case, false, false )
  local offset = self:GetRadial( case, false, true )

  -- Distance shift ahead of carrier to allow for some space to bolter.
  local dx = 5

  -- Width of the box in NM.
  local w = 2
  local w2 = w / 2

  -- Distance from carrier to arc out zone.
  local d = 12

  -- Carrier position.
  local cv = self:GetCoordinate()

  -- Polygon points.
  local c = {}

  -- First point. Carrier coordinate translated 5 NM in direction of travel to allow for bolter space.
  c[1] = cv:Translate( -UTILS.NMToMeters( dx ), radial )

  if math.abs( self.holdingoffset ) >= 5 then

    -----------------
    -- Angled Case --
    -----------------

    c[2] = c[1]:Translate( UTILS.NMToMeters( w2 ), radial - 90 ) -- 1 Right of carrier, dx ahead.
    c[3] = c[2]:Translate( UTILS.NMToMeters( d + dx + w2 ), radial ) -- 13 "south" @ 1 right

    c[4] = cv:Translate( UTILS.NMToMeters( 15 ), offset ):Translate( UTILS.NMToMeters( 1 ), offset - 90 )
    c[5] = cv:Translate( UTILS.NMToMeters( l ), offset ):Translate( UTILS.NMToMeters( 1 ), offset - 90 )
    c[6] = cv:Translate( UTILS.NMToMeters( l ), offset ):Translate( UTILS.NMToMeters( 1 ), offset + 90 )
    c[7] = cv:Translate( UTILS.NMToMeters( 13 ), offset ):Translate( UTILS.NMToMeters( 1 ), offset + 90 )
    c[8] = cv:Translate( UTILS.NMToMeters( 11 ), radial ):Translate( UTILS.NMToMeters( 1 ), radial + 90 )

    c[9] = c[1]:Translate( UTILS.NMToMeters( w2 ), radial + 90 )

  else

    -----------------------------
    -- Easy case of a long box --
    -----------------------------

    c[2] = c[1]:Translate( UTILS.NMToMeters( w2 ), radial - 90 )
    c[3] = c[2]:Translate( UTILS.NMToMeters( dx + l ), radial ) -- Stack 1 starts at 21 and is 7 NM.
    c[4] = c[3]:Translate( UTILS.NMToMeters( w ), radial + 90 )
    c[5] = c[1]:Translate( UTILS.NMToMeters( w2 ), radial + 90 )

  end

  -- Create an array of a square!
  local p = {}
  for _i, _c in ipairs( c ) do
    if self.Debug then
      -- _c:SmokeBlue()
    end
    p[_i] = _c:GetVec2()
  end

  -- Square zone length=10NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier.
  -- So stay 0-5 NM (+1 NM error margin) port of carrier.
  local zone = ZONE_POLYGON_BASE:New( "CASE II/III Approach Corridor", p )

  return zone
end

--- Get zone of carrier. Carrier is approximated as rectangle.
-- @param #AIRBOSS self
-- @return Core.Zone#ZONE Zone surrounding the carrier.
function AIRBOSS:_GetZoneCarrierBox()

  self.zoneCarrierbox = self.zoneCarrierbox or ZONE_POLYGON_BASE:New( "Carrier Box Zone" )

  -- Stern coordinate.
  local S = self:_GetSternCoord()

  -- Current carrier heading.
  local hdg = self:GetHeading( false )

  -- Coordinate array.
  local p = {}

  -- Starboard stern point.
  p[1] = S:Translate( self.carrierparam.totwidthstarboard, hdg + 90 )

  -- Starboard bow point.
  p[2] = p[1]:Translate( self.carrierparam.totlength, hdg )

  -- Port bow point.
  p[3] = p[2]:Translate( self.carrierparam.totwidthstarboard + self.carrierparam.totwidthport, hdg - 90 )

  -- Port stern point.
  p[4] = p[3]:Translate( self.carrierparam.totlength, hdg - 180 )

  -- Convert to vec2.
  local vec2 = {}
  for _, coord in ipairs( p ) do
    table.insert( vec2, coord:GetVec2() )
  end

  -- Create polygon zone.
  -- local zone=ZONE_POLYGON_BASE:New("Carrier Box Zone", vec2)
  -- return zone

  self.zoneCarrierbox:UpdateFromVec2( vec2 )

  return self.zoneCarrierbox
end

--- Get zone of landing runway.
-- @param #AIRBOSS self
-- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway.
function AIRBOSS:_GetZoneRunwayBox()

  self.zoneRunwaybox = self.zoneRunwaybox or ZONE_POLYGON_BASE:New( "Landing Runway Zone" )

  -- Stern coordinate.
  local S = self:_GetSternCoord()

  -- Current carrier heading.
  local FB = self:GetFinalBearing( false )

  -- Coordinate array.
  local p = {}

  -- Points.
  p[1] = S:Translate( self.carrierparam.rwywidth * 0.5, FB + 90 )
  p[2] = p[1]:Translate( self.carrierparam.rwylength, FB )
  p[3] = p[2]:Translate( self.carrierparam.rwywidth, FB - 90 )
  p[4] = p[3]:Translate( self.carrierparam.rwylength, FB - 180 )

  -- Convert to vec2.
  local vec2 = {}
  for _, coord in ipairs( p ) do
    table.insert( vec2, coord:GetVec2() )
  end

  -- Create polygon zone.
  -- local zone=ZONE_POLYGON_BASE:New("Landing Runway Zone", vec2)
  -- return zone

  self.zoneRunwaybox:UpdateFromVec2( vec2 )

  return self.zoneRunwaybox
end

--- Get zone of primary abeam landing position of HMS Hermes, HMS Invincible, USS Tarawa, USS America and Juan Carlos. Box length 50 meters and width 30 meters.

--- Allow for Clear to land call from LSO approaching abeam the landing spot if stable as per NATOPS 00-80T
-- @param #AIRBOSS self
-- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway.
function AIRBOSS:_GetZoneAbeamLandingSpot()

  -- Primary landing Spot coordinate.
  local S = self:_GetOptLandingCoordinate()

  -- Current carrier heading.
  local FB = self:GetFinalBearing( false )

  -- Coordinate array. Pene Testing extended Abeam landing spot V/STOL.
  local p={}

  -- Points.
  p[1] = S:Translate( 15, FB ):Translate( 15, FB + 90 ) -- Top-Right
  p[2] = S:Translate( -45, FB ):Translate( 15, FB + 90 ) -- Bottom-Right
  p[3] = S:Translate( -45, FB ):Translate( 15, FB - 90 ) -- Bottom-Left
  p[4] = S:Translate( 15, FB ):Translate( 15, FB - 90 ) -- Top-Left

  -- Convert to vec2.
  local vec2 = {}
  for _, coord in ipairs( p ) do
    table.insert( vec2, coord:GetVec2() )
  end

  -- Create polygon zone.
  local zone = ZONE_POLYGON_BASE:New( "Abeam Landing Spot Zone", vec2 )

  return zone
end

--- Get zone of the primary landing spot of the USS Tarawa.
-- @param #AIRBOSS self
-- @return Core.Zone#ZONE_POLYGON Zone surrounding landing runway.
function AIRBOSS:_GetZoneLandingSpot()

  -- Primary landing Spot coordinate.
  local S = self:_GetLandingSpotCoordinate()

  -- Current carrier heading.
  local FB = self:GetFinalBearing( false )

  -- Coordinate array.
  local p = {}

  -- Points.
  p[1] = S:Translate( 10, FB ):Translate( 10, FB + 90 ) -- Top-Right
  p[2] = S:Translate( -10, FB ):Translate( 10, FB + 90 ) -- Bottom-Right
  p[3] = S:Translate( -10, FB ):Translate( 10, FB - 90 ) -- Bottom-Left
  p[4] = S:Translate( 10, FB ):Translate( 10, FB - 90 ) -- Top-left

  -- Convert to vec2.
  local vec2 = {}
  for _, coord in ipairs( p ) do
    table.insert( vec2, coord:GetVec2() )
  end

  -- Create polygon zone.
  local zone = ZONE_POLYGON_BASE:New( "Landing Spot Zone", vec2 )

  return zone
end

--- Get holding zone of player.
-- @param #AIRBOSS self
-- @param #number case Recovery case.
-- @param #number stack Marshal stack number.
-- @return Core.Zone#ZONE Holding zone.
function AIRBOSS:_GetZoneHolding( case, stack )

  -- Holding zone.
  local zoneHolding = nil -- Core.Zone#ZONE

  -- Stack is <= 0 ==> no marshal zone.
  if stack <= 0 then
    self:E( self.lid .. "ERROR: Stack <= 0 in _GetZoneHolding!" )
    self:E( { case = case, stack = stack } )
    return nil
  end

  -- Pattern altitude.
  local patternalt, c1, c2 = self:_GetMarshalAltitude( stack, case )

  -- Select case.
  if case == 1 then
    -- CASE I

    -- Get current carrier heading.
    local hdg = self:GetHeading()

    -- Distance to the post.
    local D = UTILS.NMToMeters( 2.5 )

    -- Post 2.5 NM port of carrier.
    local Post = self:GetCoordinate():Translate( D, hdg + 270 )

    -- TODO: update zone not creating a new one.

    -- Create holding zone.
    self.zoneHolding = ZONE_RADIUS:New( "CASE I Holding Zone", Post:GetVec2(), self.marshalradius )

    -- Delta pattern.
    if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then
      self.zoneHolding = ZONE_RADIUS:New( "CASE I Holding Zone", self.carrier:GetVec2(), UTILS.NMToMeters( 5 ) )
    end

  else
    -- CASE II/II

    -- Get radial.
    local radial = self:GetRadial( case, false, true )

    -- Create an array of a rectangle. Length is 7 NM, width is 8 NM. One NM starboard to line up with the approach corridor.
    local p = {}
    p[1] = c2:Translate( UTILS.NMToMeters( 1 ), radial - 90 ):GetVec2() -- c2 is at (angels+15) NM directly behind the carrier. We translate it 1 NM starboard.
    p[2] = c1:Translate( UTILS.NMToMeters( 1 ), radial - 90 ):GetVec2() -- c1 is 7 NM further behind. Also translated 1 NM starboard.
    p[3] = c1:Translate( UTILS.NMToMeters( 7 ), radial + 90 ):GetVec2() -- p3 7 NM port of carrier.
    p[4] = c2:Translate( UTILS.NMToMeters( 7 ), radial + 90 ):GetVec2() -- p4 7 NM port of carrier.

    -- Square zone length=7NM width=6 NM behind the carrier starting at angels+15 NM behind the carrier.
    -- So stay 0-5 NM (+1 NM error margin) port of carrier.
    self.zoneHolding = self.zoneHolding or ZONE_POLYGON_BASE:New( "CASE II/III Holding Zone" )

    self.zoneHolding:UpdateFromVec2( p )
  end

  return self.zoneHolding
end

--- Get zone where player are automatically commence when enter.
-- @param #AIRBOSS self
-- @param #number case Recovery case.
-- @param #number stack Stack for Case II/III as we commence from stack>=1.
-- @return Core.Zone#ZONE Holding zone.
function AIRBOSS:_GetZoneCommence( case, stack )

  -- Commence zone.
  local zone

  if case == 1 then
    -- Case I

    -- Get current carrier heading.
    local hdg = self:GetHeading()

    -- Distance to the zone.
    local D = UTILS.NMToMeters( 4.75 )

    -- Zone radius.
    local R = UTILS.NMToMeters( 1 )

    -- Three position
    local Three = self:GetCoordinate():Translate( D, hdg + 275 )

    if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then
      local Dx = UTILS.NMToMeters( 2.25 )

      local Dz = UTILS.NMToMeters( 2.25 )

      R = UTILS.NMToMeters( 1 )

      Three = self:GetCoordinate():Translate( Dz, hdg - 90 ):Translate( Dx, hdg - 180 )

    end

    -- Create holding zone.
    self.zoneCommence = self.zoneCommence or ZONE_RADIUS:New( "CASE I Commence Zone" )

    self.zoneCommence:UpdateFromVec2( Three:GetVec2(), R )

  else
    -- Case II/III

    stack = stack or 1

    -- Start point at 21 NM for stack=1.
    local l = 20 + stack

    -- Offset angle
    local offset = self:GetRadial( case, false, true )

    -- Carrier position.
    local cv = self:GetCoordinate()

    -- Polygon points.
    local c = {}

    c[1] = cv:Translate( UTILS.NMToMeters( l ), offset ):Translate( UTILS.NMToMeters( 1 ), offset - 90 )
    c[2] = cv:Translate( UTILS.NMToMeters( l + 2.5 ), offset ):Translate( UTILS.NMToMeters( 1 ), offset - 90 )
    c[3] = cv:Translate( UTILS.NMToMeters( l + 2.5 ), offset ):Translate( UTILS.NMToMeters( 1 ), offset + 90 )
    c[4] = cv:Translate( UTILS.NMToMeters( l ), offset ):Translate( UTILS.NMToMeters( 1 ), offset + 90 )

    -- Create an array of a square!
    local p = {}
    for _i, _c in ipairs( c ) do
      p[_i] = _c:GetVec2()
    end

    -- Zone polygon.
    self.zoneCommence = self.zoneCommence or ZONE_POLYGON_BASE:New( "CASE II/III Commence Zone" )

    self.zoneCommence:UpdateFromVec2( p )

  end

  return self.zoneCommence
end

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ORIENTATION functions
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Provide info about player status on the fly.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
function AIRBOSS:_AttitudeMonitor( playerData )

  -- Player unit.
  local unit = playerData.unit

  -- Aircraft attitude.
  local aoa = unit:GetAoA()
  local yaw = unit:GetYaw()
  local roll = unit:GetRoll()
  local pitch = unit:GetPitch()

  -- Distance to the boat.
  local dist = playerData.unit:GetCoordinate():Get2DDistance( self:GetCoordinate() )
  local dx, dz, rho, phi = self:_GetDistances( unit )

  -- Wind vector.
  local wind = unit:GetCoordinate():GetWindWithTurbulenceVec3()

  -- Aircraft veloecity vector.
  local velo = unit:GetVelocityVec3()
  local vabs = UTILS.VecNorm( velo )

  local rwy = false
  local step = playerData.step
  if playerData.step == AIRBOSS.PatternStep.FINAL or
     playerData.step == AIRBOSS.PatternStep.GROOVE_XX or
     playerData.step == AIRBOSS.PatternStep.GROOVE_IM or
     playerData.step == AIRBOSS.PatternStep.GROOVE_IC or
     playerData.step == AIRBOSS.PatternStep.GROOVE_AR or
     playerData.step == AIRBOSS.PatternStep.GROOVE_AL or
     playerData.step == AIRBOSS.PatternStep.GROOVE_LC or
     playerData.step == AIRBOSS.PatternStep.GROOVE_IW then
    step = self:_GS( step, -1 )
    rwy = true
  end

  -- Relative heading Aircraft to Carrier.
  local relhead = self:_GetRelativeHeading( playerData.unit, rwy )

  -- local lc=self:_GetOptLandingCoordinate()
  -- lc:FlareRed()

  -- Output
  local text = string.format( "Pattern step: %s", step )
  text = text .. string.format( "\nAoA=%.1f° = %.1f Units | |V|=%.1f knots", aoa, self:_AoADeg2Units( playerData, aoa ), UTILS.MpsToKnots( vabs ) )
  if self.Debug then
    -- Velocity vector.
    text = text .. string.format( "\nVx=%.1f Vy=%.1f Vz=%.1f m/s", velo.x, velo.y, velo.z )
    -- Wind vector.
    text = text .. string.format( "\nWind Vx=%.1f Vy=%.1f Vz=%.1f m/s", wind.x, wind.y, wind.z )
  end
  text = text .. string.format( "\nPitch=%.1f° | Roll=%.1f° | Yaw=%.1f°", pitch, roll, yaw )
  text = text .. string.format( "\nClimb Angle=%.1f° | Rate=%d ft/min", unit:GetClimbAngle(), velo.y * 196.85 )
  local dist = self:_GetOptLandingCoordinate():Get3DDistance( playerData.unit:GetVec3() )
  -- Get player velocity in km/h.
  local vplayer = playerData.unit:GetVelocityKMH()
  -- Get carrier velocity in km/h.
  local vcarrier = self.carrier:GetVelocityKMH()
  -- Speed difference.
  local dv = math.abs( vplayer - vcarrier )
  local alt = self:_GetAltCarrier( playerData.unit )
  text = text .. string.format( "\nDist=%.1f m Alt=%.1f m delta|V|=%.1f km/h", dist, alt, dv )
  -- If in the groove, provide line up and glide slope error.
  if playerData.step == AIRBOSS.PatternStep.FINAL or
     playerData.step == AIRBOSS.PatternStep.GROOVE_XX or
     playerData.step == AIRBOSS.PatternStep.GROOVE_IM or
     playerData.step == AIRBOSS.PatternStep.GROOVE_IC or
     playerData.step == AIRBOSS.PatternStep.GROOVE_AR or
     playerData.step == AIRBOSS.PatternStep.GROOVE_AL or
     playerData.step == AIRBOSS.PatternStep.GROOVE_LC or
     playerData.step == AIRBOSS.PatternStep.GROOVE_IW then
    local lue = self:_Lineup( playerData.unit, true )
    local gle = self:_Glideslope( playerData.unit )
    text = text .. string.format( "\nGamma=%.1f° | Rho=%.1f°", relhead, phi )
    text = text .. string.format( "\nLineUp=%.2f° | GlideSlope=%.2f° | AoA=%.1f Units", lue, gle, self:_AoADeg2Units( playerData, aoa ) )
    local grade, points, analysis = self:_LSOgrade( playerData )
    text = text .. string.format( "\nTgroove=%.1f sec", self:_GetTimeInGroove( playerData ) )
    text = text .. string.format( "\nGrade: %s %.1f PT - %s", grade, points, analysis )
  else
    text = text .. string.format( "\nR=%.2f NM | X=%d Z=%d m", UTILS.MetersToNM( rho ), dx, dz )
    text = text .. string.format( "\nGamma=%.1f° | Rho=%.1f°", relhead, phi )
  end

  -- VNAO Edit: for testing the damn line up in the wires!
  -- VNAO Edit: F-14A Nozzle: R-433, L-434, R Burner- 435, L Burner - 436
  local lueWire = self:_LineupWIRE( playerData.unit, true )
  text = text .. string.format( "\nLineUpForWireCalls=%.2f° | lineup for Groove calls=%.2f°", lueWire or 0, lue or 0)-- VNAO Edit - Added

  local unitClient = Unit.getByName(unit:GetName()) -- VNAO Edit - Added
  local hornet = playerData.actype == AIRBOSS.AircraftCarrier.HORNET -- VNAO Edit - Added
  local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B -- VNAO Edit - Added

  if hornet then -- VNAO Edit - Added
    local nozzlePosL = 0  -- VNAO Edit - Added
    local burnerPosL = unitClient:getDrawArgumentValue(28)  -- VNAO Edit - Added
      if burnerPosL < 0.2 then -- VNAO Edit - Added
        nozzlePosL = unitClient:getDrawArgumentValue(89) -- VNAO Edit - Added
      else -- VNAO Edit - Added
        nozzlePosL = 0 -- VNAO Edit - Added
      end -- VNAO Edit - Added

    local nozzlePosR = 0 -- VNAO Edit - Added
    local burnerPosR = unitClient:getDrawArgumentValue(29) -- VNAO Edit - Added
    if burnerPosR < 0.2 then -- VNAO Edit - Added
      nozzlePosR = unitClient:getDrawArgumentValue(90) -- VNAO Edit - Added
    else -- VNAO Edit - Added
      nozzlePosR = 0 -- VNAO Edit - Added
    end -- VNAO Edit - Added

    text = text .. string.format( "\n Left Nozzle position=%.2f | Right Nozzle position=%.2f ", nozzlePosL, nozzlePosR ) -- VNAO Edit - Added
  end -- VNAO Edit - Added

  if tomcat then -- VNAO Edit - Added
    local nozzlePosL = unitClient:getDrawArgumentValue(434) -- VNAO Edit - Added
    local nozzlePosR = unitClient:getDrawArgumentValue(433) -- VNAO Edit - Added
    text = text .. string.format( "\n Left Nozzle position=%.2f | Right Nozzle position=%.2f ", nozzlePosL, nozzlePosR ) -- VNAO Edit - Added
  end -- VNAO Edit - Added

  MESSAGE:New( text, 1, nil, true ):ToClient( playerData.client )
end

--- Get glide slope of aircraft unit.
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit Aircraft unit.
-- @param #number optangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope ~3.5 degrees.
-- @return #number Glide slope angle in degrees measured from the deck of the carrier and third wire.
function AIRBOSS:_Glideslope( unit, optangle )

  if optangle == nil then
    if unit:GetTypeName() == AIRBOSS.AircraftCarrier.AV8B then
      optangle = 3.0
    else
      optangle = 3.5
    end
  end
  -- Landing coordinate
  local landingcoord = self:_GetOptLandingCoordinate()

  -- Distance from stern to aircraft.
  local x = unit:GetCoordinate():Get2DDistance( landingcoord )

  -- Altitude of unit corrected by the deck height of the carrier.
  local h = self:_GetAltCarrier( unit )

  -- Harrier should be 40-50 ft above the deck.
  if unit:GetTypeName() == AIRBOSS.AircraftCarrier.AV8B then
    h = unit:GetAltitude() - (UTILS.FeetToMeters( 50 ) + self.carrierparam.deckheight + 2)
  end

  -- Glide slope.
  local glideslope = math.atan( h / x )

  -- Glide slope (error) in degrees.
  local gs = math.deg( glideslope ) - optangle

  return gs
end

--- Get glide slope of aircraft unit.
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit Aircraft unit.
-- @param #number optangle (Optional) Return glide slope relative to this angle, i.e. the error from the optimal glide slope ~3.5 degrees.
-- @return #number Glide slope angle in degrees measured from the deck of the carrier and third wire.
function AIRBOSS:_Glideslope2( unit, optangle )

  if optangle == nil then
    if unit:GetTypeName() == AIRBOSS.AircraftCarrier.AV8B then
      optangle = 3.0
    else
      optangle = 3.5
    end
  end
  -- Landing coordinate
  local landingcoord = self:_GetOptLandingCoordinate()

  -- Distance from stern to aircraft.
  local x = unit:GetCoordinate():Get3DDistance( landingcoord )

  -- Altitude of unit corrected by the deck height of the carrier.
  local h = self:_GetAltCarrier( unit )

  -- Harrier should be 40-50 ft above the deck.
  if unit:GetTypeName() == AIRBOSS.AircraftCarrier.AV8B then
    h = unit:GetAltitude() - (UTILS.FeetToMeters( 50 ) + self.carrierparam.deckheight + 2)
  end

  -- Glide slope.
  local glideslope = math.asin( h / x )

  -- Glide slope (error) in degrees.
  local gs = math.deg( glideslope ) - optangle

  -- Debug.
  self:T3( self.lid .. string.format( "Glide slope error = %.1f, x=%.1f h=%.1f", gs, x, h ) )

  return gs
end

--- Get line up of player wrt to carrier.
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit Aircraft unit.
-- @param #boolean runway If true, include angled runway.
-- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right.
function AIRBOSS:_Lineup( unit, runway )

  -- Landing coordinate
  local landingcoord = self:_GetOptLandingCoordinate()

  -- Vector to landing coord.
  local A = landingcoord:GetVec3()

  -- Vector to player.
  local B = unit:GetVec3()

  -- Vector from player to carrier.
  local C = UTILS.VecSubstract( A, B )

  -- Only in 2D plane.
  C.y = 0.0

  -- Orientation of carrier.
  local X = self.carrier:GetOrientationX()
  X.y = 0.0

  -- Rotate orientation to angled runway.
  if runway then
    X = UTILS.Rotate2D( X, -self.carrierparam.rwyangle )
  end

  -- Projection of player pos on x component.
  local x = UTILS.VecDot( X, C )

  -- Orientation of carrier.
  local Z = self.carrier:GetOrientationZ()
  Z.y = 0.0

  -- Rotate orientation to angled runway.
  if runway then
    Z = UTILS.Rotate2D( Z, -self.carrierparam.rwyangle )
  end

  -- Projection of player pos on z component.
  local z = UTILS.VecDot( Z, C )

  ---
  local lineup = math.deg( math.atan2( z, x ) )

  return lineup
end

-- VNAO Edit - Added this function
--- Get line up of player wrt to carrier.
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit Aircraft unit.
-- @param #boolean runway If true, include angled runway.
-- @return #number Line up with runway heading in degrees. 0 degrees = perfect line up. +1 too far left. -1 too far right.
function AIRBOSS:_LineupWIRE( unit, runway ) -- VNAO Edit - Added

  -- Landing coordinate
  local landingcoord = self:_GetOptLandingCoordinateWIRE() -- VNAO Edit - Added

  -- Vector to landing coord.
  local A = landingcoord:GetVec3() -- VNAO Edit - Added

  -- Vector to player.
  local B = unit:GetVec3() -- VNAO Edit - Added

  -- Vector from player to carrier.
  local C = UTILS.VecSubstract( A, B ) -- VNAO Edit - Added

  -- Only in 2D plane.
  C.y = 0.0 -- VNAO Edit - Added

  -- Orientation of carrier.
  local X = self.carrier:GetOrientationX() -- VNAO Edit - Added
  X.y = 0.0 -- VNAO Edit - Added

  -- Rotate orientation to angled runway.
  if runway then -- VNAO Edit - Added
    X = UTILS.Rotate2D( X, -self.carrierparam.rwyangle ) -- VNAO Edit - Added
  end -- VNAO Edit - Added

  -- Projection of player pos on x component.
  local x = UTILS.VecDot( X, C ) -- VNAO Edit - Added

  -- Orientation of carrier.
  local Z = self.carrier:GetOrientationZ() -- VNAO Edit - Added
  Z.y = 0.0 -- VNAO Edit - Added

  -- Rotate orientation to angled runway.
  if runway then -- VNAO Edit - Added
    Z = UTILS.Rotate2D( Z, -self.carrierparam.rwyangle ) -- VNAO Edit - Added
  end -- VNAO Edit - Added

  -- Projection of player pos on z component.
  local z = UTILS.VecDot( Z, C ) -- VNAO Edit - Added

  ---
  local lineup = math.deg( math.atan2( z, x ) ) -- VNAO Edit - Added

  return lineup -- VNAO Edit - Added
end -- VNAO Edit - Added

-- VNAO Edit - Added this function
--- Get L/R Nozzle Position from Argument and L/R Burner Argument
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit Aircraft unit.
-- @return #number Left Nozzle position argument
function AIRBOSS:_NozzleArgumentLeft( unit ) -- VNAO Edit - Added
  --Arguments for HORNET L burner and R burner are 28 and 29 respectively and >.2 indicates burner active
  --Nozzle position greater than 0.3 for the hornet indicates either idle or burner
  --if Lnoz > 0.6 and Rnoz > 0.6  is current check for EG
  local unitClient = Unit.getByName(unit:GetName()) -- VNAO Edit - Added
  local typeName = unit:GetTypeName() -- VNAO Edit - Added
  local nozzlePosL = 0 -- VNAO Edit - Added
  local burnerPosL = 0 -- VNAO Edit - Added
  if typeName == "FA-18C_hornet" then -- VNAO Edit - Added
    burnerPosL = unitClient:getDrawArgumentValue(28) -- VNAO Edit - Added
    if burnerPosL < 0.2 then -- VNAO Edit - Added
      nozzlePosL = unitClient:getDrawArgumentValue(89) -- VNAO Edit - Added
    else -- VNAO Edit - Added
      nozzlePosL = 0 -- VNAO Edit - Added
    end -- VNAO Edit - Added
  elseif typeName == "F-14A-135-GR" or typeName == "F-14B" then -- VNAO Edit - Added
    nozzlePosL = unitClient:getDrawArgumentValue(434) -- VNAO Edit - Added
  end -- VNAO Edit - Added
  
  return nozzlePosL -- VNAO Edit - Added
end -- VNAO Edit - Added

-- VNAO Edit - Added this function
--- Get L/R Nozzle Position from Argument and L/R Burner Argument
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit Aircraft unit.
-- @return #number Right Nozzle position argument
function AIRBOSS:_NozzleArgumentRight( unit ) -- VNAO Edit - Added
  local unitClient = Unit.getByName(unit:GetName()) -- VNAO Edit - Added
  local typeName = unit:GetTypeName() -- VNAO Edit - Added
  local nozzlePosR = 0 -- VNAO Edit - Added
  local burnerPosR = 0 -- VNAO Edit - Added

  if typeName == "FA-18C_hornet" then -- VNAO Edit - Added
    burnerPosR = unitClient:getDrawArgumentValue(29) -- VNAO Edit - Added
    if burnerPosR < 0.2 then -- VNAO Edit - Added
      nozzlePosR = unitClient:getDrawArgumentValue(90) -- VNAO Edit - Added
    else -- VNAO Edit - Added
      nozzlePosR = 0 -- VNAO Edit - Added
    end -- VNAO Edit - Added
  elseif typeName == "F-14A-135-GR" or typeName == "F-14B" then -- VNAO Edit - Added
    nozzlePosR = unitClient:getDrawArgumentValue(433) -- VNAO Edit - Added
  end -- VNAO Edit - Added
  return nozzlePosR -- VNAO Edit - Added
end -- VNAO Edit - Added


--- Get altitude of aircraft wrt carrier deck. Should give zero when the aircraft touched down.
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit Aircraft unit.
-- @return #number Altitude in meters wrt carrier height.
function AIRBOSS:_GetAltCarrier( unit )

  -- TODO: Value 4 meters is for the Hornet. Adjust for Harrier, A4E and

  -- Altitude of unit corrected by the deck height of the carrier.
  local h = unit:GetAltitude() - self.carrierparam.deckheight - 2

  return h
end

--- Get optimal landing position of the aircraft. Usually between second and third wire. In case of Tarawa, Canberrra, Juan Carlos and America we take the abeam landing spot 120 ft above and 21 ft abeam the 7.5 position, for the Juan Carlos I, HMS Invincible, and HMS Hermes and Invincible it is 120 ft above and 21 ft abeam the 5 position. For CASE III it is 120ft directly above the landing spot.
-- @param #AIRBOSS self
-- @return Core.Point#COORDINATE Optimal landing coordinate.
function AIRBOSS:_GetOptLandingCoordinate()

  -- Start with stern coordiante.
  self.landingcoord:UpdateFromCoordinate( self:_GetSternCoord() )

  -- Final bearing.
  local FB=self:GetFinalBearing(false)

  -- Cse
  local case=self.case

  -- set Case III V/STOL abeam landing spot over deck -- Pene Testing
  if self.carriertype==AIRBOSS.CarrierType.INVINCIBLE or self.carriertype==AIRBOSS.CarrierType.HERMES or self.carriertype==AIRBOSS.CarrierType.TARAWA or self.carriertype==AIRBOSS.CarrierType.AMERICA or self.carriertype==AIRBOSS.CarrierType.JCARLOS or self.carriertype==AIRBOSS.CarrierType.CANBERRA then

    if case==3 then

      -- Landing coordinate.
      self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate())

      -- Altitude 120ft -- is this corect for Case III?
      self.landingcoord:SetAltitude(UTILS.FeetToMeters(120))

    elseif case==2 or case==1 then

      -- Landing 100 ft abeam, 120 ft alt.
      self.landingcoord:UpdateFromCoordinate(self:_GetLandingSpotCoordinate()):Translate(35, FB-90, true, true)

      -- Alitude 120 ft.
      self.landingcoord:SetAltitude(UTILS.FeetToMeters(120))

    end

  else

    -- Ideally we want to land between 2nd and 3rd wire.
    if self.carrierparam.wire3 then
      -- We take the position of the 3rd wire to approximately account for the length of the aircraft.
      self.landingcoord:Translate( self.carrierparam.wire3, FB, true, true )
    end

    -- Add 2 meters to account for aircraft height.
    self.landingcoord.y = self.landingcoord.y + 2

  end

  return self.landingcoord
end

-- VNAO Edit - dded this whole function
--- Get optimal landing position of the aircraft. Usually between second and third wire. In case of Tarawa, Canberrra, Juan Carlos and America we take the abeam landing spot 120 ft above and 21 ft abeam the 7.5 position, for the Juan Carlos I, HMS Invincible, and HMS Hermes and Invincible it is 120 ft above and 21 ft abeam the 5 position. For CASE III it is 120ft directly above the landing spot.
-- @param #AIRBOSS self
-- @return Core.Point#COORDINATE Optimal landing coordinate.
function AIRBOSS:_GetOptLandingCoordinateWIRE()

  -- Start with stern coordiante.
  self.landingcoord:UpdateFromCoordinate( self:_GetSternCoord() )

  -- Final bearing.
  local FB=self:GetFinalBearing(false)

  -- Cse
  local case=self.case

  -- set Case III V/STOL abeam landing spot over deck -- Pene Testing
  
  

  -- Ideally we want to land between 2nd and 3rd wire.
  if self.carrierparam.wire3 then
    -- We take the position of the 3rd wire to approximately account for the length of the aircraft.
    self.landingcoord:Translate( self.carrierparam.wire3 + 500, FB, true, true )-- adding 80 meter to wire to see if this is far enough to keep tracking a good lineup error (50 meters not enough), went from 250 meters to 500 meters 12/23/23, the more distance out front the less the error sensitivity down deck. I've checked the location of the landing spot 500 meters out front of 3 wire on CL with flares and smoke, appears to be dead on. Not sure why there's still this small difference between lined up left and right differences. 
  end

  -- Add 2 meters to account for aircraft height.
  self.landingcoord.y = self.landingcoord.y + 2

  
  --self.landingcoord:FlareGreen() --for testing the lineup spot translated out in front of the carrier landing area by 500 meters. Appears good.
  return self.landingcoord
end

--- Get landing spot on Tarawa and others.
-- @param #AIRBOSS self
-- @return Core.Point#COORDINATE Primary landing spot coordinate.
function AIRBOSS:_GetLandingSpotCoordinate()

  -- Start at stern coordinate.
  self.landingspotcoord:UpdateFromCoordinate( self:_GetSternCoord() )

  -- Landing 100 ft abeam, 100 alt.
  local hdg = self:GetHeading()

  -- Primary landing spot. Different carriers handled via carrier parameter landingspot now.
  self.landingspotcoord:Translate( self.carrierparam.landingspot, hdg, true, true ):SetAltitude( self.carrierparam.deckheight )

  return self.landingspotcoord
end

--- Get true (or magnetic) heading of carrier.
-- @param #AIRBOSS self
-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned.
-- @return #number Carrier heading in degrees.
function AIRBOSS:GetHeading( magnetic )
  self:F3( { magnetic = magnetic } )

  -- Carrier heading
  local hdg = self.carrier:GetHeading()

  -- Include magnetic declination.
  if magnetic then
    hdg = hdg - self.magvar
  end

  -- -- Adjust negative values.
  -- if hdg < 0 then
  --   hdg = hdg + 360
  -- end

  hdg = hdg % 360  -- using this to replace the above function to prevent negative values and BRC higher than 360

  return hdg
end

--- Get base recovery course (BRC) of carrier.
-- The is the magnetic heading of the carrier.
-- @param #AIRBOSS self
-- @return #number BRC in degrees.
function AIRBOSS:GetBRC()
  return self:GetHeading( true )
end

--- Get wind direction and speed at carrier position.
-- @param #AIRBOSS self
-- @param #number alt Altitude ASL in meters. Default 18 m.
-- @param #boolean magnetic Direction including magnetic declination.
-- @param Core.Point#COORDINATE coord (Optional) Coordinate at which to get the wind. Default is current carrier position.
-- @return #number Direction the wind is blowing **from** in degrees.
-- @return #number Wind speed in m/s.
function AIRBOSS:GetWind( alt, magnetic, coord )

  -- Current position of the carrier or input.
  local cv = coord or self:GetCoordinate()

  -- Wind direction and speed. By default at 18 meters ASL.
  local Wdir, Wspeed = cv:GetWind( alt or 18 )

  -- Include magnetic declination.
  if magnetic then
    Wdir = Wdir - self.magvar
    -- Adjust negative values.
    if Wdir < 0 then
      Wdir = Wdir + 360
    end
  end

  return Wdir, Wspeed
end

--- Get wind speed on carrier deck parallel and perpendicular to runway.
-- @param #AIRBOSS self
-- @param #number alt Altitude in meters. Default 18 m.
-- @return #number Wind component parallel to runway im m/s.
-- @return #number Wind component perpendicular to runway in m/s.
-- @return #number Total wind strength in m/s.
function AIRBOSS:GetWindOnDeck( alt )

  -- Position of carrier.
  local cv = self:GetCoordinate()

  -- Velocity vector of carrier.
  local vc = self.carrier:GetVelocityVec3()

  -- Carrier orientation X.
  local xc = self.carrier:GetOrientationX()

  -- Carrier orientation Z.
  local zc = self.carrier:GetOrientationZ()

  -- Rotate back so that angled deck points to wind.
  xc = UTILS.Rotate2D( xc, -self.carrierparam.rwyangle )
  zc = UTILS.Rotate2D( zc, -self.carrierparam.rwyangle )

  -- Wind (from) vector
  local vw = cv:GetWindWithTurbulenceVec3( alt or 18 ) --(change made from 50m to 15m from Discord discussion from Sickdog, next change to 18m due to SC higher deck discord)

  -- Total wind velocity vector.
  -- Carrier velocity has to be negative. If carrier drives in the direction the wind is blowing from, we have less wind in total.
  local vT = UTILS.VecSubstract( vw, vc )

  -- || Parallel component.
  local vpa = UTILS.VecDot( vT, xc )

  -- == Perpendicular component.
  local vpp = UTILS.VecDot( vT, zc )

  -- Strength.
  local vabs = UTILS.VecNorm( vT )

  -- We return positive values as head wind and negative values as tail wind.
  -- TODO: Check minus sign.
  return -vpa, vpp, vabs
end

--- Get true (or magnetic) heading of carrier into the wind. This accounts for the angled runway.
-- @param #AIRBOSS self
-- @param #number vdeck Desired wind velocity over deck in knots.
-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned.
-- @param Core.Point#COORDINATE coord (Optional) Coordinate from which heading is calculated. Default is current carrier position.
-- @return #number Carrier heading in degrees.
-- @return #number Carrier speed in knots to reach desired wind speed on deck.
function AIRBOSS:GetHeadingIntoWind(vdeck, magnetic, coord )

  if self.intowindold then
    --env.info("FF use OLD into wind")
    return self:GetHeadingIntoWind_old(vdeck, magnetic, coord)
  else
    --env.info("FF use NEW into wind")
    return self:GetHeadingIntoWind_new(vdeck, magnetic, coord)
  end

end


--- Get true (or magnetic) heading of carrier into the wind. This accounts for the angled runway.
-- @param #AIRBOSS self
-- @param #number vdeck Desired wind velocity over deck in knots.
-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned.
-- @param Core.Point#COORDINATE coord (Optional) Coordinate from which heading is calculated. Default is current carrier position.
-- @return #number Carrier heading in degrees.
function AIRBOSS:GetHeadingIntoWind_old( vdeck, magnetic, coord )

  local function adjustDegreesForWindSpeed(windSpeed)
    local degreesAdjustment = 0
  -- the windspeeds are in m/s

  -- +0 degrees at 15m/s = 37kts
  -- +0 degrees at 14m/s = 35kts
  -- +0 degrees at 13m/s = 33kts
  -- +4 degrees at 12m/s = 31kts
  -- +4 degrees at 11m/s = 29kts
  -- +4 degrees at 10m/s = 27kts
  -- +4 degrees at 9m/s = 27kts
  -- +4 degrees at 8m/s = 27kts
  -- +8 degrees at 7m/s = 27kts
  -- +8 degrees at 6m/s = 27kts
  -- +8 degrees at 5m/s = 26kts
  -- +20 degrees at 4m/s = 26kts
  -- +20 degrees at 3m/s = 26kts
  -- +30 degrees at 2m/s = 26kts 1s

    if windSpeed > 0 and windSpeed < 3 then
      degreesAdjustment = 30
    elseif windSpeed >= 3 and windSpeed < 5 then
      degreesAdjustment = 20
    elseif windSpeed >= 5 and windSpeed < 8 then
      degreesAdjustment = 8
    elseif windSpeed >= 8 and windSpeed < 13 then
      degreesAdjustment = 4
    elseif windSpeed >= 13 then
      degreesAdjustment = 0
    end

    return degreesAdjustment
  end

  -- Get direction the wind is blowing from. This is where we want to go.
  local windfrom, vwind = self:GetWind( nil, nil, coord )

  -- Actually, we want the runway in the wind.
  local intowind = windfrom - self.carrierparam.rwyangle + adjustDegreesForWindSpeed(vwind)

  -- If no wind, take current heading.
  if vwind < 0.1 then
    intowind = self:GetHeading()
  end

  -- Magnetic heading.
  if magnetic then
    intowind = intowind - self.magvar
  end

  -- Adjust negative values.
  if intowind < 0 then
    intowind = intowind + 360
  end

  -- Wind speed.
  --local _, vwind = self:GetWind()

  -- Speed of carrier in m/s but at least 4 knots.
  local vtot = math.max(vdeck-UTILS.MpsToKnots(vwind), 4)

  return intowind, vtot
end

--- Get true (or magnetic) heading of carrier into the wind. This accounts for the angled runway.
-- Implementation based on [Mags & Bambi](https://magwo.github.io/carrier-cruise/).
-- @param #AIRBOSS self
-- @param #number vdeck Desired wind velocity over deck in knots.
-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned.
-- @param Core.Point#COORDINATE coord (Optional) Coordinate from which heading is calculated. Default is current carrier position.
-- @return #number Carrier heading in degrees.
-- @return #number Carrier speed in knots to reach desired wind speed on deck.
function AIRBOSS:GetHeadingIntoWind_new( vdeck, magnetic, coord )

  -- Default offset angle.
  local Offset=self.carrierparam.rwyangle or 0

  -- Get direction the wind is blowing from.
  local windfrom, vwind=self:GetWind(18, nil ,coord)

  -- Ships min/max speed.
  local Vmin=4
  local Vmax=UTILS.KmphToKnots(self.carrier:GetSpeedMax())

  -- No wind. will stay on current heading.
  if vwind<0.1 then
    local h=self:GetHeading(magnetic)
    return h, math.min(vdeck, Vmax)
  end

  -- Convert wind speed to knots.
  vwind=UTILS.MpsToKnots(vwind)

  -- Wind to in knots.
  local windto=(windfrom+180)%360

  -- Offset angle in rad. We also define the rotation to be clock-wise, which requires a minus sign.
  local alpha=math.rad(-Offset)

  -- Constant.
  local C = math.sqrt(math.cos(alpha)^2 / math.sin(alpha)^2 + 1)


  -- Upper limit of desired speed due to max boat speed.
  local vdeckMax=vwind + math.cos(alpha) * Vmax

  -- Lower limit of desired speed due to min boat speed.
  local vdeckMin=vwind + math.cos(alpha) * Vmin


  -- Speed of ship so it matches the desired speed.
  local v=0

  -- Angle wrt. to wind TO-direction
  local theta=0

  if vdeck>vdeckMax then
    -- Boat cannot go fast enough

    -- Set max speed.
    v=Vmax

    -- Calculate theta.
    theta = math.asin(v/(vwind*C)) - math.asin(-1/C)

  elseif vdeck<vdeckMin then
    -- Boat cannot go slow enought

    -- Set min speed.
    v=Vmin

    -- Calculatge theta.
    theta = math.asin(v/(vwind*C)) - math.asin(-1/C)

  elseif vdeck*math.sin(alpha)>vwind then
    -- Too little wind

    -- Set theta to 90°
    theta=math.pi/2

    -- Set speed.
    v = math.sqrt(vdeck^2 - vwind^2)

  else
    -- Normal case
    theta = math.asin(vdeck * math.sin(alpha) / vwind)
    v = vdeck * math.cos(alpha) - vwind * math.cos(theta)
  end

  -- Magnetic heading.
  local magvar= magnetic and self.magvar or 0

  -- Ship heading so cross wind is min for the given wind.
  -- local intowind = (540 + (windto - magvar + math.deg(theta) )) % 360 -- VNAO Edit: Using old heading into wind algorithm
  local intowind = self:GetHeadingIntoWind_old(vdeck) -- VNAO Edit: Using old heading into wind algorithm

  return intowind, v
end

--- Get base recovery course (BRC) when the carrier would head into the wind.
-- This includes the current wind direction and accounts for the angled runway.
-- @param #AIRBOSS self
-- @param #number vdeck Desired wind velocity over deck in knots.
-- @return #number BRC into the wind in degrees.
function AIRBOSS:GetBRCintoWind(vdeck)
  -- BRC is the magnetic heading.
  return self:GetHeadingIntoWind(vdeck, true )
end

--- Get final bearing (FB) of carrier.
-- By default, the routine returns the magnetic FB depending on the current map (Caucasus, NTTR, Normandy, Persion Gulf etc).
-- The true bearing can be obtained by setting the *TrueNorth* parameter to true.
-- @param #AIRBOSS self
-- @param #boolean magnetic If true, magnetic FB is returned.
-- @return #number FB in degrees.
function AIRBOSS:GetFinalBearing( magnetic )

  -- First get the heading.
  local fb = self:GetHeading( magnetic )

  -- Final baring = BRC including angled deck.
  fb = fb + self.carrierparam.rwyangle

  -- Adjust negative values.
  if fb < 0 then
    fb = fb + 360
  end

  return fb
end

--- Get radial with respect to carrier BRC or FB and (optionally) holding offset.
--
-- * case=1: radial=FB-180
-- * case=2: radial=HDG-180 (+offset)
-- * case=3: radial=FB-180 (+offset)
--
-- @param #AIRBOSS self
-- @param #number case Recovery case.
-- @param #boolean magnetic If true, magnetic radial is returned. Default is true radial.
-- @param #boolean offset If true, inlcude holding offset.
-- @param #boolean inverse Return inverse, i.e. radial-180 degrees.
-- @return #number Radial in degrees.
function AIRBOSS:GetRadial( case, magnetic, offset, inverse )

  -- Case or current case.
  case = case or self.case

  -- Radial.
  local radial

  -- Select case.
  if case == 1 then

    -- Get radial.
    radial = self:GetFinalBearing( magnetic ) - 180

  elseif case == 2 then

    -- Radial wrt to heading of carrier.
    radial = self:GetHeading( magnetic ) - 180

    -- Holding offset angle (+-15 or 30 degrees usually)
    if offset then
      radial = radial + self.holdingoffset
    end

  elseif case == 3 then

    -- Radial wrt angled runway.
    radial = self:GetFinalBearing( magnetic ) - 180

    -- Holding offset angle (+-15 or 30 degrees usually)
    if offset then
      radial = radial + self.holdingoffset
    end

  end

  -- Adjust for negative values.
  if radial < 0 then
    radial = radial + 360
  end

  -- Inverse?
  if inverse then

    -- Inverse radial
    radial = radial - 180

    -- Adjust for negative values.
    if radial < 0 then
      radial = radial + 360
    end

  end

  return radial
end

--- Get difference between to headings in degrees taking into accound the [0,360) periodocity.
-- @param #AIRBOSS self
-- @param #number hdg1 Heading one.
-- @param #number hdg2 Heading two.
-- @return #number Difference between the two headings in degrees.
function AIRBOSS:_GetDeltaHeading( hdg1, hdg2 )

  local V = {} -- DCS#Vec3
  V.x = math.cos( math.rad( hdg1 ) )
  V.y = 0
  V.z = math.sin( math.rad( hdg1 ) )

  local W = {} -- DCS#Vec3
  W.x = math.cos( math.rad( hdg2 ) )
  W.y = 0
  W.z = math.sin( math.rad( hdg2 ) )

  local alpha = UTILS.VecAngle( V, W )

  return alpha
end

--- Get relative heading of player wrt carrier.
-- This is the angle between the direction/orientation vector of the carrier and the direction/orientation vector of the provided unit.
-- Note that this is calculated in the X-Z plane, i.e. the altitude Y is not taken into account.
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit Player unit.
-- @param #boolean runway (Optional) If true, return relative heading of unit wrt to angled runway of the carrier.
-- @return #number Relative heading in degrees. An angle of 0 means, unit fly parallel to carrier. An angle of + or - 90 degrees means, unit flies perpendicular to carrier.
function AIRBOSS:_GetRelativeHeading( unit, runway )

  -- Direction vector of the carrier.
  local vC = self.carrier:GetOrientationX()

  -- Include runway angle.
  if runway then
    vC = UTILS.Rotate2D( vC, -self.carrierparam.rwyangle )
  end

  -- Direction vector of the unit.
  local vP = unit:GetOrientationX()

  -- We only want the X-Z plane. Aircraft could fly parallel but ballistic and we dont want the "pitch" angle.
  vC.y = 0;
  vP.y = 0

  -- Get angle between the two orientation vectors in degrees.
  local rhdg = UTILS.VecAngle( vC, vP )

  -- Return heading in degrees.
  return rhdg
end

--- Get relative velocity of player unit wrt to carrier
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit Player unit.
-- @return #number Relative velocity in m/s.
function AIRBOSS:_GetRelativeVelocity( unit )

  local vC = self.carrier:GetVelocityVec3()
  local vP = unit:GetVelocityVec3()

  -- Only X-Z plane is necessary here.
  vC.y = 0;
  vP.y = 0

  local v = UTILS.VecSubstract( vP, vC )

  return UTILS.VecNorm( v ), v
end

--- Calculate distances between carrier and aircraft unit.
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit Aircraft unit.
-- @return #number Distance [m] in the direction of the orientation of the carrier.
-- @return #number Distance [m] perpendicular to the orientation of the carrier.
-- @return #number Distance [m] to the carrier.
-- @return #number Angle [Deg] from carrier to plane. Phi=0 if the plane is directly behind the carrier, phi=90 if the plane is starboard, phi=180 if the plane is in front of the carrier.
function AIRBOSS:_GetDistances( unit )

  -- Vector to carrier
  local a = self.carrier:GetVec3()

  -- Vector to player
  local b = unit:GetVec3()

  -- Vector from carrier to player.
  local c = { x = b.x - a.x, y = 0, z = b.z - a.z }

  -- Orientation of carrier.
  local x = self.carrier:GetOrientationX()

  -- Projection of player pos on x component.
  local dx = UTILS.VecDot( x, c )

  -- Orientation of carrier.
  local z = self.carrier:GetOrientationZ()

  -- Projection of player pos on z component.
  local dz = UTILS.VecDot( z, c )

  -- Polar coordinates.
  local rho = math.sqrt( dx * dx + dz * dz )

  -- Not exactly sure any more what I wanted to calculate here.
  local phi = math.deg( math.atan2( dz, dx ) )

  -- Correct for negative values.
  if phi < 0 then
    phi = phi + 360
  end

  return dx, dz, rho, phi
end

--- Check limits for reaching next step.
-- @param #AIRBOSS self
-- @param #number X X position of player unit.
-- @param #number Z Z position of player unit.
-- @param #AIRBOSS.Checkpoint check Checkpoint.
-- @return #boolean If true, checkpoint condition for next step was reached.
function AIRBOSS:_CheckLimits( X, Z, check )

  -- Limits
  local nextXmin = check.LimitXmin == nil or (check.LimitXmin and (check.LimitXmin < 0 and X <= check.LimitXmin or check.LimitXmin >= 0 and X >= check.LimitXmin))
  local nextXmax = check.LimitXmax == nil or (check.LimitXmax and (check.LimitXmax < 0 and X >= check.LimitXmax or check.LimitXmax >= 0 and X <= check.LimitXmax))
  local nextZmin = check.LimitZmin == nil or (check.LimitZmin and (check.LimitZmin < 0 and Z <= check.LimitZmin or check.LimitZmin >= 0 and Z >= check.LimitZmin))
  local nextZmax = check.LimitZmax == nil or (check.LimitZmax and (check.LimitZmax < 0 and Z >= check.LimitZmax or check.LimitZmax >= 0 and Z <= check.LimitZmax))

  -- Proceed to next step if all conditions are fullfilled.
  local next = nextXmin and nextXmax and nextZmin and nextZmax

  -- Debug info.
  local text = string.format( "step=%s: next=%s: X=%d Xmin=%s Xmax=%s | Z=%d Zmin=%s Zmax=%s", check.name, tostring( next ), X, tostring( check.LimitXmin ), tostring( check.LimitXmax ), Z, tostring( check.LimitZmin ), tostring( check.LimitZmax ) )
  self:T3( self.lid .. text )

  return next
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- LSO functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- LSO advice radio call.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @param #number glideslopeError Error in degrees.
-- @param #number lineupError Error in degrees.
function AIRBOSS:_LSOadvice( playerData, glideslopeError, lineupError )

  -- Advice time.
  local advice = 0

  -- Glideslope high/low calls.
  if glideslopeError > self.gle.HIGH then -- 1.5 then
    -- "You're high!"
    self:RadioTransmission( self.LSORadio, self.LSOCall.HIGH, true, nil, nil, true )
    advice = advice + self.LSOCall.HIGH.duration
  elseif glideslopeError > self.gle.High then -- 0.8 then
    -- "You're high."
    self:RadioTransmission( self.LSORadio, self.LSOCall.HIGH, false, nil, nil, true )
    advice = advice + self.LSOCall.HIGH.duration
  elseif glideslopeError < self.gle.LOW then -- -0.9 then
    -- "Power!"
    self:RadioTransmission( self.LSORadio, self.LSOCall.POWER, true, nil, nil, true )
    advice = advice + self.LSOCall.POWER.duration
  elseif glideslopeError < self.gle.Low then -- -0.6 then
    -- "Power."
    self:RadioTransmission( self.LSORadio, self.LSOCall.POWER, false, nil, nil, true )
    advice = advice + self.LSOCall.POWER.duration
  elseif glideslopeError<self.gle._min then  -- -0.3 then -- VNAO Edit - Added
    -- "power." -- VNAO Edit - Added
    self:RadioTransmission(self.LSORadio, self.LSOCall.POWER, false, nil, nil, true) -- VNAO Edit - Added
    advice=advice+self.LSOCall.POWERsoft.duration -- VNAO Edit - Added
  else
    -- "Good altitude."
  end

  -- Lineup left/right calls.
  if lineupError < self.lue.LEFT then
    -- "Come left!"
    self:RadioTransmission( self.LSORadio, self.LSOCall.COMELEFT, true, nil, nil, true )
    advice = advice + self.LSOCall.COMELEFT.duration
  elseif lineupError < self.lue.Left then
    -- "Come left."
    self:RadioTransmission( self.LSORadio, self.LSOCall.COMELEFT, false, nil, nil, true )
    advice = advice + self.LSOCall.COMELEFT.duration
  elseif lineupError > self.lue.RIGHT then -- 3 then
    -- "Right for lineup!"
    self:RadioTransmission( self.LSORadio, self.LSOCall.RIGHTFORLINEUP, true, nil, nil, true )
    advice = advice + self.LSOCall.RIGHTFORLINEUP.duration
  elseif lineupError > self.lue.Right then -- 1 then
    -- "Right for lineup."
    self:RadioTransmission( self.LSORadio, self.LSOCall.RIGHTFORLINEUP, false, nil, nil, true )
    advice = advice + self.LSOCall.RIGHTFORLINEUP.duration
  else
    -- "Good lineup."
  end

  -- Get current AoA.
  local AOA = playerData.unit:GetAoA()

  -- Get aircraft AoA parameters.
  local acaoa = self:_GetAircraftAoA( playerData )

  -- Speed via AoA - not for the Harrier.
  if playerData.actype ~= AIRBOSS.AircraftCarrier.AV8B then
    if AOA > acaoa.SLOW then
      -- "Your're slow!"
      self:RadioTransmission( self.LSORadio, self.LSOCall.SLOW, true, nil, nil, true )
      advice = advice + self.LSOCall.SLOW.duration
      -- S=underline("SLO")
    elseif AOA > acaoa.Slow then
      -- "Your're slow."
      self:RadioTransmission( self.LSORadio, self.LSOCall.SLOW, false, nil, nil, true )
      advice = advice + self.LSOCall.SLOW.duration
      -- S="SLO"
    elseif AOA > acaoa.OnSpeedMax then
      -- No call.
      -- S=little("SLO")
    elseif AOA < acaoa.FAST then
      -- "You're fast!"
      self:RadioTransmission( self.LSORadio, self.LSOCall.FAST, true, nil, nil, true )
      advice = advice + self.LSOCall.FAST.duration
      -- S=underline("F")
    elseif AOA < acaoa.Fast then
      -- "You're fast."
      self:RadioTransmission( self.LSORadio, self.LSOCall.FAST, false, nil, nil, true )
      advice = advice + self.LSOCall.FAST.duration
      -- S="F"
    elseif AOA < acaoa.OnSpeedMin then
      -- No Call.
      -- S=little("F")
    end
  end

  -- Set last time.
  playerData.Tlso = timer.getTime()
end

--- Grade player time in the groove - from turning to final until touchdown.
--
-- If time
--
-- * < 9 seconds: No Grade "--"
-- * 9-11 seconds: Fair "(OK)"
-- * 12-21 seconds: OK (15-18 is ideal)
-- * 22-24 seconds: Fair "(OK)
-- * > 24 seconds: No Grade "--"
--
-- If you manage to be between 16.4 and and 16.6 seconds, you will even get and okay underline "\_OK\_".
-- No groove time for Harrier on LHA, LHD set to Tgroove Unicorn as starting point to allow possible _OK_ 5.0.
--
-- If time in the AV-8B
--
-- * < 55 seconds: Fast V/STOL
-- * < 75 seconds: OK V/STOL
-- * > 76 Seconds: SLOW V/STOL (Early hover stop selection)
--
-- If you manage to be between 60.0 and 65.0 seconds in the AV-8B, you will even get and okay underline "\_OK\_"
--
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @return #string LSO grade for time in groove, i.e. \_OK\_, OK, (OK), --.
function AIRBOSS:_EvalGrooveTime( playerData )

  -- Time in groove.
  local t = playerData.Tgroove
  local grade = ""

  if t < 9 then      --Circuit edit was 15
    grade = "_NESA_"  --Circuit edit was (NESA)
  elseif t < 12 then  --Circuit added
    grade = "NESA"    --Circuit added
  elseif t < 15 then  --Circuit added
    grade = "(NESA)"  --Circuit added
  elseif t < 19  then 
    grade = ""
  elseif t < 22 then    --Circuit added
    grade = "(LIG)"     --Circuit added
  elseif t < 25 then    --Circuit added
    grade = "LIG"       --Circuit added
  elseif t >= 25 then   --Circuit added
    grade = "_LIG_"     --Circuit added

    -- Time in groove for AV-8B
  elseif playerData.actype == AIRBOSS.AircraftCarrier.AV8B and t < 55 then -- VSTOL Late Hover stop selection too fast to Abeam LDG Spot AV-8B.
    grade = "FAST V/STOL Groove"
  elseif playerData.actype == AIRBOSS.AircraftCarrier.AV8B and t < 75 then -- VSTOL Operations with AV-8B.
    grade = "OK V/STOL Groove"
  elseif playerData.actype == AIRBOSS.AircraftCarrier.AV8B and t >= 76 then -- VSTOL Early Hover stop selection slow to Abeam LDG Spot AV-8B.
    grade = "SLOW V/STOL Groove"
  else
    grade = "LIG"
  end

  -- The unicorn!
  if t >= 16.49 and t <= 16.6 then 
    grade = "_OK_"
  end

  -- V/STOL Unicorn!
  if playerData.actype == AIRBOSS.AircraftCarrier.AV8B and (t >= 60.0 and t <= 65.0) then
    grade = "_OK_ V/STOL"
  end

  return grade
end

--- Grade approach.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @return #string LSO grade, i.g. _OK_, OK, (OK), --, etc.
-- @return #number Points.
-- @return #string LSO analysis of flight path.
function AIRBOSS:_LSOgrade( playerData )

  --- Count deviations.
  local function count( base, pattern )
    return select( 2, string.gsub( base, pattern, "" ) )
  end
  local TIG =  ""
  -- Analyse flight data and convert to LSO text.
  if playerData.Tgroove and playerData.Tgroove <= 360 and playerData.case < 3 then --Circuit Added
   TIG = self:_EvalGrooveTime( playerData ) or "N/A" --Circuit Added
  end                                               --Circuit Added
  local GXX, nXX = self:_Flightdata2Text( playerData, AIRBOSS.GroovePos.XX )
  local GIM, nIM = self:_Flightdata2Text( playerData, AIRBOSS.GroovePos.IM )
  local GIC, nIC = self:_Flightdata2Text( playerData, AIRBOSS.GroovePos.IC )
  local GAR, nAR = self:_Flightdata2Text( playerData, AIRBOSS.GroovePos.AR )
  local GIW, nIW = self:_Flightdata2Text( playerData, AIRBOSS.GroovePos.IW ) -- VNAO Edit - Added

  -- VTOL approach, which is graded differently (currently only Harrier).
  local vtol=playerData.actype==AIRBOSS.AircraftCarrier.AV8B

  -- Put everything together.
  local G = GXX .. " " .. GIM .. " " .. " " .. GIC .. " " .. GAR .. " " .. GIW .. " " .. TIG -- VNAO Edit - Added .. " " .. GIW Circuit added TIG

  local gradeWithDeviations = GXX .. "[" .. nXX .. "] " .. GIM .. "[" .. nIM .. "] " .. GIC .. "[" .. nIC .. "] " .. GAR .. "[" .. nAR .. "] " .. GIW .. "[" .. nIW .. "]" -- VNAO Edit - Added
  env.info("LSO Grade [with deviation count]: " .. gradeWithDeviations) -- VNAO Edit - Added

  -- Count number of minor/small nS, normal nN and major/large deviations nL.
  local N=nXX+nIM+nIC+nAR+nIW -- VNAO Edit - Added +nIW
  local nL=count(G, '_')/2
  local nS=count(G, '%(')
  local nN=N-nS-nL


  -- Groove time 15-18.99 sec for a unicorn. Or 60-65 for V/STOL unicorn.
  local Tgroove=playerData.Tgroove
  local TgrooveUnicorn=Tgroove and (Tgroove>=16.49 and Tgroove<=16.59) or false -- VNAO Edit - Original values 15.0/18.99
  local TgrooveVstolUnicorn=Tgroove and (Tgroove>=60.0 and Tgroove<=65.0)and playerData.actype==AIRBOSS.AircraftCarrier.AV8B or false

  local grade
  local points
  -- if N == 0 and (TgrooveUnicorn or TgrooveVstolUnicorn or playerData.case==3) then -- VNAO Edit - Removed TgrooveUnicorn and case 3 as a factor
    if N == 0 and TgrooveVstolUnicorn then -- VNAO Edit - Removed TgrooveUnicorn and case 3 as a factor
      -- No deviations, should be REALLY RARE!
      grade = "_OK_"
      points = 5.0
      G = "Unicorn"
    end -- VNAO Edit - Added
    if N==0 and TgrooveUnicorn then  -- VNAO Edit - Added
      -- No deviations, should be REALLY RARE! -- VNAO Edit - Added
      if playerData.wire == 3 then -- VNAO Edit - Added
        grade="_OK_" -- VNAO Edit - Added
        points=5.0 -- VNAO Edit - Added
        G="Unicorn" -- VNAO Edit - Added
      else -- VNAO Edit - Added
        grade="OK" -- VNAO Edit - Added
        points=4.0 -- VNAO Edit - Added
      end -- VNAO Edit - Added
  
    else

    if vtol then

      -- Add AV-8B Harrier devation allowances due to lower groundspeed and 3x conventional groove time, this allows to maintain LSO tolerances while respecting the deviations are not unsafe.--Pene testing
      -- Large devaitions still result in a No Grade, A Unicorn still requires a clean pass with no deviation.

      -- Normal laning part at the beginning
      local Gb = GXX .. " " .. GIM

      -- Number of deviations that occurred at the the beginning of the landing (XX or IM). These are graded like in non-VTOL landings, i.e. on deviations is
      local N=nXX+nIM
      local nL=count(Gb, '_')/2
      local nS=count(Gb, '%(')
      local nN=N-nS-nL


      -- VTOL part of the landing
      local Gv = GIC .. " " .. GAR

      -- Number of deviations that occurred at the the end (VTOL part) of the landing (IC or AR).
      local Nv=nIC+nAR
      local nLv=count(Gv, '_')/2
      local nSv=count(Gv, '%(')
      local nNv=Nv-nSv-nLv

      if nL>0 or nLv>1 then
          -- Larger deviations at XX or IM or at least one larger deviation IC or AR==> "No grade" 2.0 points.
          -- In other words, we allow one larger deviation at IC+AR
          grade="--"
          points=2.0
      elseif nN>0 or nNv>1 or nLv==1 then
          -- Average deviations at XX+IM or more than one normal deviation IC or AR ==>  "Fair Pass" Pass with average deviations and corrections.
          grade="(OK)"
          points=3.0
      else
          -- Only minor corrections
          grade="OK"
          points=4.0
      end

    else

      -- This is a normal (non-VTOL) landing.

      if nL > 0 then
          -- Larger deviations ==> "No grade" 2.0 points.
          grade="--"
          points=2.0
      elseif nN> 0 then
          -- No larger but average/normal deviations ==>  "Fair Pass" Pass with average deviations and corrections.
          grade="(OK)"
          points=3.0
      else
          -- Only minor corrections ==> "Okay pass" 4.0 points.
          grade="OK"
          points=4.0
      end

    end
  end

  -- Replace" )"( and "__"
  G = G:gsub( "%)%(", "" )
  G = G:gsub( "__", "" )

  -- Debug info
  local text = "LSO grade:\n"
  text = text .. G .. "\n"
  text = text .. "Grade = " .. grade .. " points = " .. points .. "\n"
  text = text .. "# of total deviations   = " .. N .. "\n"
  text = text .. "# of large deviations _ = " .. nL .. "\n"
  text = text .. "# of normal deviations  = " .. nN .. "\n"
  text = text .. "# of small deviations ( = " .. nS .. "\n"
  self:T2( self.lid .. text )
  env.info(text)-- VNAO Edit - Added

  -- Special cases.
  if playerData.wop then
    ---------------------
    -- Pattern Waveoff --
    ---------------------
    if playerData.lig then
      -- Long In the Groove (LIG).
      -- According to Stingers this is a CUT pass and gives 1.0 points.
      grade = "WO"
      points = 1.0
      G = "LIG"
    else
      -- Other pattern WO
      grade = "WOP"
      points = 2.0
      G = "n/a"
    end
  elseif playerData.wofd then
    -----------------------
    -- Foul Deck Waveoff --
    -----------------------
    if playerData.landed then
      -- AIRBOSS wants to talk to you!
      grade = "CUT"
      points = 0.0
    else
      grade = "WOFD"
      points = -1.0
    end
    G = "n/a"
  elseif playerData.owo then
    -----------------
    -- Own Waveoff --
    -----------------
    grade = "OWO"
    points = 2.0
    if N == 0 then
      G = "n/a"
    end
  elseif playerData.waveoff then
    -------------
    -- Waveoff --
    -------------
    if playerData.landed then
      -- AIRBOSS wants to talk to you!
      grade = "CUT"
      points = 0.0
    else
      grade = "WO"
      points = 1.0
    end
  elseif playerData.boltered then
    -- Bolter
    grade = "-- (BOLTER)"
    points = 2.5

  elseif not playerData.hover and playerData.actype == AIRBOSS.AircraftCarrier.AV8B then
    -------------------------------
    -- AV-8B not cleared to land -- -- Landing clearence is carrier from LC to Landing
    -------------------------------
    if playerData.landed then
      -- AIRBOSS wants your balls!
      grade = "CUT"
      points = 0.0
    end

  end

  -- -- VNAO EDIT: Subtract 1pt from overall grade if it is a 1 wire.  If it's already a 1pt pass, ignore.
  -- if playerData.wire == 1 and points > 1 then -- VNAO EDIT: added
  --   if points == 4 then -- VNAO EDIT: added
  --       points = 3 -- VNAO EDIT: added
  --       grade = "(OK)" -- VNAO EDIT: added
  --   elseif points == 3 then -- VNAO EDIT: added
  --       points = 2 -- VNAO EDIT: added
  --       grade = "--" -- VNAO EDIT: added
  --   end -- VNAO EDIT: added
  -- end -- VNAO EDIT: added

    -- Circuit edit only take points awary from a 1 wire if there are more than 4 other deviations
  if playerData.wire == 1 and points >= 3 and N > 4 then
    points = points -1
  end

  env.info("Returning: " .. grade .. "  " .. points .. "  " .. G)

  return grade, points, G
end

--- Grade flight data.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @param #string groovestep Step in the groove.
-- @param #AIRBOSS.GrooveData fdata Flight data in the groove.
-- @return #string LSO grade or empty string if flight data table is nil.
-- @return #number Number of deviations from perfect flight path.
function AIRBOSS:_Flightdata2Text( playerData, groovestep )

  local function little( text )
    return string.format( "(%s)", text )
  end
  local function underline( text )
    return string.format( "_%s_", text )
  end

  -- Groove Data.
  local fdata = playerData.groove[groovestep] -- #AIRBOSS.GrooveData

  -- No flight data ==> return empty string.
  if fdata == nil then
    self:T3( self.lid .. "Flight data is nil." )
    return "", 0
  end

  -- Flight data.
  local step = fdata.Step
  local AOA = fdata.AoA
  local GSE = fdata.GSE
  local LUE = fdata.LUE
  local LUEwire = fdata.LUEwire -- VNAO Edit - Added
  local Lnoz = fdata.LeftNozzle -- VNAO Edit - Added
  local Rnoz = fdata.RightNozzle -- VNAO Edit - Added
  local ROL = fdata.Roll
  local GT = fdata.GT -- Circuit Added

  -- Aircraft specific AoA values.
  local acaoa = self:_GetAircraftAoA( playerData )

  -- Angled Approach.
  -- VNAO Edit - changed this to regualr LUL at X, made my own Angled Approach check starting at the wake based on angle of bank less than 15 degrees/10 degrees/2 degrees
  local P = nil -- VNAO Edit - Added
  if step == AIRBOSS.PatternStep.GROOVE_XX and ROL <= 3.5 and playerData.case < 3 then -- VNAO Edit - Changed, original ROL val 4.0
    if LUE > 3.2 then -- VNAO Edit - Original value self.lue.RIGHT
      -- P = underline( "AA" )
      P = underline( "LUL") -- VNAO Edit - Added
    elseif LUE > 2.2 then -- VNAO Edit - Original value self.lue.RightMed
      -- P = "AA "
      P="LUL" -- VNAO Edit - Added
    elseif LUE > 1.2 then -- VNAO Edit - Original value self.lue.Right
      -- P = little( "AA" )
      P= little( "LUL") -- VNAO Edit - Added
    end
  end

  -- Overshoot Start.
  local O = nil
  if step == AIRBOSS.PatternStep.GROOVE_XX and playerData.case < 3 then -- VNAO Edit - Added case 3 check
    if LUE < self.lue.LEFT then
      O = underline( "OS" )
    elseif LUE < self.lue.Left then
      O = "OS"
    elseif LUE < self.lue._min then
      O = little( "OS" )
    end
  end

  -- Speed via AoA. Depends on aircraft type.
  local S = nil
  local A = nil --circuit moved this line to be seen outside of this scope
  if step~=AIRBOSS.PatternStep.GROOVE_IW then -- VNAO Edit - Added To avoid getting an AOA or GS grade in the wires... let's just check left or right in the wires
    if  AIRBOSS.PatternStep.GROOVE_AR and playerData.waveoff == true and playerData.owo == true then -- VNAO Edit - Added
      -- env.info('Adam MOOSE Edit -AR and waved off so do not add AOA or GS errors to comments ') -- VNAO Edit - Added
    else -- VNAO Edit - Added
      -- Speed via AoA. Depends on aircraft type.
      if AOA > acaoa.SLOW then
        S = underline( "SLO" )
      elseif AOA > acaoa.Slow then
        S = "SLO"
      elseif AOA > acaoa.OnSpeedMax then
        S = little( "SLO" )
      elseif AOA < acaoa.FAST then
        S = underline( "F" )
      elseif AOA < acaoa.Fast then
        S = "F"
      elseif AOA < acaoa.OnSpeedMin then
        S = little( "F" )
      end

      -- Glideslope/altitude. Good [-0.3, 0.4] asymmetric!

      if GSE > self.gle.HIGH then
        A = underline( "H" )
      elseif GSE > self.gle.High then
        A = "H"
      elseif GSE > self.gle._max then
        A = little( "H" )
      elseif GSE < self.gle.LOW then
        A = underline( "LO" )
      elseif GSE < self.gle.Low then
        A = "LO"
      elseif GSE < self.gle._min then
        A = little( "LO" )
      end
    end -- VNAO Edit - Added
  end -- VNAO Edit - Added

  local stepMod=self:_GS(step) -- VNAO Edit - Added

  local D = nil
  local DW = nil
  local Rol = nil -- VNAO Edit - Added
  local Noz = nil -- VNAO Edit - Added
  -- VNAO Edit - Now replacing LUL with AA at Groove start for case 1 and 2, to fix problem with getting a full LUL with an (AA) and still getting a 4.0 OK
  -- if playerData.case < 3 then -- VNAO Edit - Added
  if AIRBOSS.PatternStep.GROOVE_AR and playerData.waveoff == true and playerData.owo == true then
    -- env.info('Adam MOOSE Edit -AR and waved off so do not add LU errors to comments ')
  else
    if LUE > self.lue.RIGHT and step ~= AIRBOSS.PatternStep.GROOVE_XX and step~=AIRBOSS.PatternStep.GROOVE_IW then -- VNAO Edit - Changed
      D = underline( "LUL" ) -- VNAO Edit - Changed
    elseif LUE > self.lue.Right and step ~= AIRBOSS.PatternStep.GROOVE_XX and step~=AIRBOSS.PatternStep.GROOVE_IW then -- VNAO Edit - Changed
      D = "LUL" -- VNAO Edit - Changed
    elseif LUE > self.lue._max and step ~= AIRBOSS.PatternStep.GROOVE_XX and step~=AIRBOSS.PatternStep.GROOVE_IW then -- VNAO Edit - Changed
      D = little( "LUL" ) -- VNAO Edit - Changed
    elseif LUE < self.lue.LEFT and step ~= AIRBOSS.PatternStep.GROOVE_XX and step~=AIRBOSS.PatternStep.GROOVE_IW then -- VNAO Edit - Changed
      D = underline( "LUR" ) -- VNAO Edit - Changed
    elseif LUE < self.lue.Left and step ~= AIRBOSS.PatternStep.GROOVE_XX and step~=AIRBOSS.PatternStep.GROOVE_IW then -- VNAO Edit - Changed
      D = "LUR" -- VNAO Edit - Changed
    elseif LUE < self.lue._min and step ~= AIRBOSS.PatternStep.GROOVE_XX and step~=AIRBOSS.PatternStep.GROOVE_IW then -- VNAO Edit - Changed
      D = little( "LUR" ) -- VNAO Edit - Changed
    end -- VNAO Edit - Changed
  end
  --[[
  elseif playerData.case == 3 then -- VNAO Edit - Changed
    if LUE>self.lue.RIGHT then -- VNAO Edit - Changed
      D=underline("LUL") -- VNAO Edit - Changed
    elseif LUE>self.lue.Right then -- VNAO Edit - Changed
      D="LUL" -- VNAO Edit - Changed
    elseif LUE>self.lue._max then -- VNAO Edit - Changed
      D=little("LUL") -- VNAO Edit - Changed
    elseif LUE < self.lue.LEFT then -- VNAO Edit - Changed
      D = underline( "LUR" ) -- VNAO Edit - Changed
    elseif LUE < self.lue.Left then -- VNAO Edit - Changed
      D = "LUR" -- VNAO Edit - Changed
    elseif LUE < self.lue._min then -- VNAO Edit - Changed
      D = little( "LUR" ) -- VNAO Edit - Changed
    end -- VNAO Edit - Changed
  end -- VNAO Edit - Changed 
--]]
  if step == AIRBOSS.PatternStep.GROOVE_IW and playerData.waveoff == false and playerData.owo == false then -- VNAO Edit - Added check for waveoff so we don't get any IN THE WIRE comments if its a WO
    -- env.info('Adam MOOSE Edit -IW code: checking for Landing Left or Landing Right, lue: '..LUEwire)
    -- env.info("Adam MOOSE Edit -IW code: ROLL IN THE WIRES: "..ROL)

    if LUEwire>1.2 then -- VNAO Edit - Added by using real time attitude monitor and using the ladders as a reference for distance down the deck, focusing on ladder rungs 4-8 but using specifiically values at ((rung 4 12/24/23) 12/24/23), average rung 3 and 4 after testing 12/27
      -- env.info("Adam MOOSE Edit -IW code: Line up > 1.2, _LL_")
      DW=underline("LL")-- VNAO Edit - Added 
    elseif LUEwire>0.4 then -- VNAO Edit - Added  by using real time attitude monitor and using the ladders as a reference for distance down the deck, focusing on ladder rungs 4-8 but using specifiically values at (rung 4 12/24/23), average rung 3 and 4 after testing 12/27
      -- env.info("Adam MOOSE Edit -IW code: Line up > 0.4, LL")
      DW="LL"-- VNAO Edit - Added 
    elseif LUEwire>0.25 then -- VNAO Edit - Added  by using real time attitude monitor and using the ladders as a reference for distance down the deck, focusing on ladder rungs 4-8 but using specifiically values at (rung 4 12/24/23), average rung 3 and 4 after testing 12/27
      -- env.info("Adam MOOSE Edit -IW code: Line up > 0.25, (LL)")
      DW=little("LL")-- VNAO Edit - Added 
    elseif LUEwire<-1.17 then -- VNAO Edit - Added  by using real time attitude monitor and using the ladders as a reference for distance down the deck, focusing on ladder rungs 4-8 but using specifiically values at (rung 4 12/24/23), average rung 3 and 4 after testing 12/27
      -- env.info("Adam MOOSE Edit -IW code: Line up < -1.17, _LR_")
      DW=underline("LR")-- VNAO Edit - Added 
    elseif LUEwire<-0.46 then -- VNAO Edit - Added  by using real time attitude monitor and using the ladders as a reference for distance down the deck, focusing on ladder rungs 4-8 but using specifiically values at (rung 4 12/24/23), average rung 3 and 4 after testing 12/27
      -- env.info("Adam MOOSE Edit -IW code: Line up < -0.46, LR")
      DW="LR"-- VNAO Edit - Added 
    elseif LUEwire<-0.25 then -- VNAO Edit - Added  by using real time attitude monitor and using the ladders as a reference for distance down the deck, focusing on ladder rungs 4-8 but using specifiically values at (rung 4 12/24/23), average rung 3 and 4 after testing 12/27
      -- env.info("Adam MOOSE Edit -IW code: Line up < -0.25, (LR)")
      DW=little("LR")-- VNAO Edit - Added 
    else-- VNAO Edit - Added 
      -- env.info('Adam MOOSE Edit -IW code: checking for Landing Left or Landing Right: NO LINEUP ERROR DECTECTED ')
      -- env.info("Adam MOOSE Edit -IW code: NO LINEUP ERORR DECTECTED ")
    end-- VNAO Edit - Added 

    if ROL > 5 and ROL <= 10 then-- VNAO Edit - Added 
      -- env.info("Adam MOOSE Edit -IW code: ROLL GREATER THAN 5 DEGREES, maybe a (LRWD) ")
      Rol=little("LRWD")-- VNAO Edit - Added 
    elseif ROL > 10 and ROL <= 15 then-- VNAO Edit - Added 
      -- env.info("Adam MOOSE Edit -IW code: ROLL GREATER THAN 10 DEGREES, maybe a LRWD ")
      Rol=("LRWD")-- VNAO Edit - Added 
    elseif ROL > 15 then-- VNAO Edit - Added 
      -- env.info("Adam MOOSE Edit -IW code: ROLL GREATER THAN 20 DEGREES, maybe a _LRWD_ ")
      Rol=underline("LRWD")-- VNAO Edit - Added 
    elseif ROL < -5 and ROL >= -10 then-- VNAO Edit - Added 
      -- env.info("Adam MOOSE Edit -IW code: ROLL GREATER THAN 5 DEGREES, maybe a (LLWD) ")
      Rol=little("LLWD")-- VNAO Edit - Added 
    elseif ROL < -10 and ROL >= -15 then-- VNAO Edit - Added 
      -- env.info("Adam MOOSE Edit -IW code: ROLL GREATER THAN 10 DEGREES, maybe a LLWD ")
      Rol=("LLWD")-- VNAO Edit - Added 
    elseif ROL < -15 then-- VNAO Edit - Added 
      -- env.info("Adam MOOSE Edit -IW code: ROLL GREATER THAN 20 DEGREES, maybe a _LLWD_ ")
      Rol=underline("LLWD")-- VNAO Edit - Added 
    else-- VNAO Edit - Added 
      -- env.info("Adam MOOSE Edit -IW code: ROLL is acceptable, less than 5 degrees left or right ")
    end-- VNAO Edit - Added 



    local hornet =   playerData.actype == AIRBOSS.AircraftCarrier.HORNET-- VNAO Edit - Added 
    local tomcat = playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B-- VNAO Edit - Added 

    if hornet then-- VNAO Edit - Added 
      if Lnoz > 0.6 and Rnoz > 0.6 then -- VNAO Edit - Added check them both, it's possilbe there could be a single engine landing and one is in idle perhaps?
        -- env.info("Adam MOOSE Edit -IW code: Throttles maybe close to idle? EGIW?  L:"..Lnoz.." R: "..Rnoz)
        Noz = underline("EG")-- VNAO Edit - Added 
      else-- VNAO Edit - Added 
        -- env.info("Adam MOOSE Edit -IW code: Throttles ok position- L:"..Lnoz.." R: "..Rnoz)
      end-- VNAO Edit - Added 
    end-- VNAO Edit - Added 

   --[[ if tomcat then-- VNAO Edit - Added 
      if Lnoz > 0.9 and Rnoz > 0.9 then -- VNAO Edit - Added Appears that when engines are idle nozzle arguments are Zero. Anything more than idle is close to 1 (0.95 to be exact) and Burner reduces to 0.3
        -- env.info("Adam MOOSE Edit -IW code: Throttles maybe close to idle? EGIW?  L:"..Lnoz.." R: "..Rnoz)
        Noz = underline("EG")-- VNAO Edit - Added 
      else-- VNAO Edit - Added 
        -- env.info("Adam MOOSE Edit -IW code: Throttles ok position- L:"..Lnoz.." R: "..Rnoz)
      end-- VNAO Edit - Added 
    end-- VNAO Edit - Added ]]

    if playerData.Tgroove and playerData.Tgroove <= 360 and playerData.case < 3 then  --Circuit Added
      local grooveTime = playerData.Tgroove --Circuit Added
      if grooveTime > 19 or grooveTime < 15 then --Circuit Added
        GT = "" --Circuit Added
      end --Circuit Added
    end --Circuit Added

  end-- VNAO Edit - Added 

  -- Compile.
  local G = ""
  local n = 0

  -- VNAO Edit - Added WU, AA and AFU calls to LSO grading
  if stepMod == "XX" then -- VNAO Edit - Added
    if playerData.case < 3 then -- VNAO Edit - Added
      if fdata.WrappedUp then -- VNAO Edit - Added
        env.info("Adding WrappedUp deviation.")
        G=G..fdata.WrappedUp -- VNAO Edit - Added
        n=n + 1 -- VNAO Edit - Added
        -- env.info('MOOSE/AIRBOSS- MOD - GRADE.... you`RE GETTING WU`D')
      end -- VNAO Edit - Added
      if fdata.AngledApch then  -- VNAO Edit - Added
        env.info("Adding AngledApch deviation.")
        G=G..fdata.AngledApch  -- VNAO Edit - Added
        n=n+1   -- VNAO Edit - Added
        -- env.info('Adam MOOSE Edit -AA code: Function- _Flightdata2Text: Trying to add AA comment only to the "Start" in comments for player: '..playerData.name)
        -- env.info('MOOSE/AIRBOSS- MOD - GRADE.... you`RE GETTING AA`D')
      end  -- VNAO Edit - Added
      if fdata.AFU then  -- VNAO Edit - Added
        env.info("Adding AFU deviation.")
        G=G..fdata.AFU  -- VNAO Edit - Added
        n=n+1   -- VNAO Edit - Added
        -- env.info('Adam MOOSE Edit -AFU code: Function- _Flightdata2Text: Trying to add AFU comment only to the "Start" in comments for player: '..playerData.name)
        -- env.info('MOOSE/AIRBOSS- MOD - GRADE....you`RE GETTING AFU`D')
      end
    end -- VNAO Edit - Added
  end  -- VNAO Edit - Added

  -- Fly through.
  if fdata.FlyThrough then
    G = G .. fdata.FlyThrough
  end
  -- Angled Approach - doesn't affect score, advisory only.
  -- if P then -- VNAO Edit - Commented out
    -- G = G .. P -- VNAO Edit - Commented out
  --   n = n + 1 -- VNAO Edit - Added + 1 -- VNAO Edit - Commented out
  -- end -- VNAO Edit - Commented out
  -- Speed.
  if S then
    env.info("Adding speed deviation.")--VNAO Added
    G = G .. S
    n = n + 1
  end
  -- Glide slope.
  if A then
    env.info("Adding altitude deviation.")--VNAO Added
    G = G .. A
    n = n + 1
  end
  -- Line up.
  if D then
    env.info("Adding line up deviation.")--VNAO Added
    G = G .. D
    n = n + 1
  end
  -- Drift in Lineup
  if fdata.Drift then
    env.info("Adding drift deviation.")--VNAO Added
    G = G .. fdata.Drift
    n = n -- Drift doesn't affect score, advisory only.
  end
  -- Overshoot.
  if O then
    env.info("Adding overshoot deviation.")--VNAO Added
    G = G .. O
    n = n + 1
  end

  if DW then  -- VNAO Edit - Added
    -- env.info("Adam MOOSE Edit -IW code:TRYING TO ADD COMMENT LANDED LEFT/RIGHT OF CENTER LINE ")  -- VNAO Edit - Added
    env.info("Adding landed L/R deviation.")
    G = G .. DW  -- VNAO Edit - Added
    n = n + 1  -- VNAO Edit - Added
  end  -- VNAO Edit - Added

  if Rol then  -- VNAO Edit - Added
    -- env.info("Adam MOOSE Edit -IW code:TRYING TO ADD COMMENT LANDED LEFT/RIGHT WING DOWN ")  -- VNAO Edit - Added
    env.info("Adding landed rol deviation.")
    G = G .. Rol  -- VNAO Edit - Added
    n = n + 1  -- VNAO Edit - Added
  end  -- VNAO Edit - Added

  if Noz then  -- VNAO Edit - Added
    -- env.info("Adam MOOSE Edit -IW code:TRYING TO ADD COMMENT EG ")  -- VNAO Edit - Added
    env.info("Adding eased guns deviation.")
    G = G .. Noz  -- VNAO Edit - Added
    n = n + 1-- try to add 3 to get an automatic no grade.  -- VNAO Edit - Added
  end  -- VNAO Edit - Added

  if GT then  --Circuit Added
    G = G .. GT --Circuit Added
    n = n + 1 --Circuit Added
  end --Circuit Added

  -- Add current step.
  local step = self:_GS( step )
  step = step:gsub( "XX", "X" )
  if G ~= "" then
    G = G .. step
  end

  -- Debug info.
  local text = string.format( "LSO Grade at %s:\n", step )
  text = text .. string.format( "AOA=%.1f\n", AOA )
  text = text .. string.format( "GSE=%.1f\n", GSE )
  text = text .. string.format( "LUE=%.1f\n", LUE )
  text = text .. string.format( "ROL=%.1f\n", ROL )
  text = text .. G
  self:T3( self.lid .. text )

  return G, n
end

--- Get short name of the grove step.
-- @param #AIRBOSS self
-- @param #string step Player step.
-- @param #number n Use -1 for previous or +1 for next. Default 0.
-- @return #string Shortcut name "X", "RB", "IM", "AR", "IW".
function AIRBOSS:_GS( step, n )
  local gp
  n = n or 0

  if step == AIRBOSS.PatternStep.FINAL then
    gp = AIRBOSS.GroovePos.X0 -- "X0"  -- Entering the groove.
    if n == -1 then
      gp = AIRBOSS.GroovePos.X0 -- There is no previous step.
    elseif n == 1 then
      gp = AIRBOSS.GroovePos.XX
    end
  elseif step == AIRBOSS.PatternStep.GROOVE_XX then
    gp = AIRBOSS.GroovePos.XX -- "XX"  -- Starting the groove.
    if n == -1 then
      gp = AIRBOSS.GroovePos.X0
    elseif n == 1 then
      gp = AIRBOSS.GroovePos.IM
    end
  elseif step == AIRBOSS.PatternStep.GROOVE_IM then
    gp = AIRBOSS.GroovePos.IM -- "IM"  -- In the middle.
    if n == -1 then
      gp = AIRBOSS.GroovePos.XX
    elseif n == 1 then
      gp = AIRBOSS.GroovePos.IC
    end
  elseif step == AIRBOSS.PatternStep.GROOVE_IC then
    gp = AIRBOSS.GroovePos.IC -- "IC"  -- In close.
    if n == -1 then
      gp = AIRBOSS.GroovePos.IM
    elseif n == 1 then
      gp = AIRBOSS.GroovePos.AR
    end
  elseif step == AIRBOSS.PatternStep.GROOVE_AR then
    gp = AIRBOSS.GroovePos.AR -- "AR"  -- At the ramp.
    if n == -1 then
      gp = AIRBOSS.GroovePos.IC
    elseif n == 1 then
      if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then
        gp = AIRBOSS.GroovePos.AL
      else
        gp = AIRBOSS.GroovePos.IW
      end
    end
  elseif step == AIRBOSS.PatternStep.GROOVE_AL then
    gp = AIRBOSS.GroovePos.AL -- "AL"  -- Abeam landing spot.
    if n == -1 then
      gp = AIRBOSS.GroovePos.AR
    elseif n == 1 then
      gp = AIRBOSS.GroovePos.LC
    end
  elseif step == AIRBOSS.PatternStep.GROOVE_LC then
    gp = AIRBOSS.GroovePos.LC -- "LC"  -- Level crossing.
    if n == -1 then
      gp = AIRBOSS.GroovePos.AL
    elseif n == 1 then
      gp = AIRBOSS.GroovePos.LC
    end
  elseif step == AIRBOSS.PatternStep.GROOVE_IW then
    gp = AIRBOSS.GroovePos.IW -- "IW"  -- In the wires.
    if n == -1 then
      gp = AIRBOSS.GroovePos.AR
    elseif n == 1 then
      gp = AIRBOSS.GroovePos.IW -- There is no next step.
    end
  end
  return gp
end

--- Check if a player is within the right area.
-- @param #AIRBOSS self
-- @param #number X X distance player to carrier.
-- @param #number Z Z distance player to carrier.
-- @param #AIRBOSS.Checkpoint pos Position data limits.
-- @return #boolean If true, approach should be aborted.
function AIRBOSS:_CheckAbort( X, Z, pos )

  local abort = false
  if pos.Xmin and X < pos.Xmin then
    self:T( string.format( "Xmin: X=%d < %d=Xmin", X, pos.Xmin ) )
    abort = true
  elseif pos.Xmax and X > pos.Xmax then
    self:T( string.format( "Xmax: X=%d > %d=Xmax", X, pos.Xmax ) )
    abort = true
  elseif pos.Zmin and Z < pos.Zmin then
    self:T( string.format( "Zmin: Z=%d < %d=Zmin", Z, pos.Zmin ) )
    abort = true
  elseif pos.Zmax and Z > pos.Zmax then
    self:T( string.format( "Zmax: Z=%d > %d=Zmax", Z, pos.Zmax ) )
    abort = true
  end

  return abort
end

--- Generate a text if a player is too far from where he should be.
-- @param #AIRBOSS self
-- @param #number X X distance player to carrier.
-- @param #number Z Z distance player to carrier.
-- @param #AIRBOSS.Checkpoint posData Checkpoint data.
function AIRBOSS:_TooFarOutText( X, Z, posData )

  -- Intro.
  local text = "you are too "

  -- X text.
  local xtext = nil
  if posData.Xmin and X < posData.Xmin then
    if posData.Xmin <= 0 then
      xtext = "far behind "
    else
      xtext = "close to "
    end
  elseif posData.Xmax and X > posData.Xmax then
    if posData.Xmax >= 0 then
      xtext = "far ahead of "
    else
      xtext = "close to "
    end
  end

  -- Z text.
  local ztext = nil
  if posData.Zmin and Z < posData.Zmin then
    if posData.Zmin <= 0 then
      ztext = "far port of "
    else
      ztext = "close to "
    end
  elseif posData.Zmax and Z > posData.Zmax then
    if posData.Zmax >= 0 then
      ztext = "far starboard of "
    else
      ztext = "too close to "
    end
  end

  -- Combine X-Z text.
  if xtext and ztext then
    text = text .. xtext .. " and " .. ztext
  elseif xtext then
    text = text .. xtext
  elseif ztext then
    text = text .. ztext
  end

  -- Complete the sentence
  text = text .. "the carrier."

  -- If no case could be identified.
  if xtext == nil and ztext == nil then
    text = "you are too far from where you should be!"
  end

  return text
end

--- Pattern aborted.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @param #number X X distance player to carrier.
-- @param #number Z Z distance player to carrier.
-- @param #AIRBOSS.Checkpoint posData Checkpoint data.
-- @param #boolean patternwo (Optional) Pattern wave off.
function AIRBOSS:_AbortPattern( playerData, X, Z, posData, patternwo )

  -- Text where we are wrong.
  local text = self:_TooFarOutText( X, Z, posData )

  -- Debug.
  local dtext = string.format( "Abort: X=%d Xmin=%s, Xmax=%s | Z=%d Zmin=%s Zmax=%s", X, tostring( posData.Xmin ), tostring( posData.Xmax ), Z, tostring( posData.Zmin ), tostring( posData.Zmax ) )
  self:T( self.lid .. dtext )

  -- Message to player.
  self:MessageToPlayer( playerData, text, "LSO" )

  if patternwo then

    -- Pattern wave off!
    playerData.wop = true

    -- Add to debrief.
    self:_AddToDebrief( playerData, string.format( "Pattern wave off: %s", text ) )

    -- Depart and re-enter radio message.
    -- TODO: Radio should depend on player step.
    self:RadioTransmission( self.LSORadio, self.LSOCall.DEPARTANDREENTER, false, 3, nil, nil, true )

    -- Next step debrief.
    playerData.step = AIRBOSS.PatternStep.DEBRIEF
    playerData.warning = nil
  end

end

--- Display hint to player.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @param #number delay Delay before playing sound messages. Default 0 sec.
-- @param #boolean soundoff If true, don't play and sound hint.
function AIRBOSS:_PlayerHint( playerData, delay, soundoff )

  -- No hint for the pros.
  if not playerData.showhints then
    return
  end

  -- Get optimal altitude, distance and speed.
  local alt, aoa, dist, speed = self:_GetAircraftParameters( playerData )

  -- Get altitude hint.
  local hintAlt, debriefAlt, callAlt = self:_AltitudeCheck( playerData, alt )

  -- Get speed hint.
  local hintSpeed, debriefSpeed, callSpeed = self:_SpeedCheck( playerData, speed )

  -- Get AoA hint.
  local hintAoA, debriefAoA, callAoA = self:_AoACheck( playerData, aoa )

  -- Get distance to the boat hint.
  local hintDist, debriefDist, callDist = self:_DistanceCheck( playerData, dist )

  -- Message to player.
  local hint = ""
  if hintAlt and hintAlt ~= "" then
    hint = hint .. "\n" .. hintAlt
  end
  if hintSpeed and hintSpeed ~= "" then
    hint = hint .. "\n" .. hintSpeed
  end
  if hintAoA and hintAoA ~= "" then
    hint = hint .. "\n" .. hintAoA
  end
  if hintDist and hintDist ~= "" then
    hint = hint .. "\n" .. hintDist
  end

  -- Debriefing text.
  local debrief = ""
  if debriefAlt and debriefAlt ~= "" then
    debrief = debrief .. "\n- " .. debriefAlt
  end
  if debriefSpeed and debriefSpeed ~= "" then
    debrief = debrief .. "\n- " .. debriefSpeed
  end
  if debriefAoA and debriefAoA ~= "" then
    debrief = debrief .. "\n- " .. debriefAoA
  end
  if debriefDist and debriefDist ~= "" then
    debrief = debrief .. "\n- " .. debriefDist
  end

  -- Add step to debriefing.
  if debrief ~= "" then
    self:_AddToDebrief( playerData, debrief )
  end

  -- Voice hint.
  delay = delay or 0
  if not soundoff then
    if callAlt then
      self:Sound2Player( playerData, self.LSORadio, callAlt, false, delay )
      delay = delay + callAlt.duration + 0.5
    end
    if callSpeed then
      self:Sound2Player( playerData, self.LSORadio, callSpeed, false, delay )
      delay = delay + callSpeed.duration + 0.5
    end
    if callAoA then
      self:Sound2Player( playerData, self.LSORadio, callAoA, false, delay )
      delay = delay + callAoA.duration + 0.5
    end
    if callDist then
      self:Sound2Player( playerData, self.LSORadio, callDist, false, delay )
      delay = delay + callDist.duration + 0.5
    end
  end

  -- ARC IN info.
  if playerData.step == AIRBOSS.PatternStep.ARCIN then

    -- Hint turn and set TACAN.
    if playerData.difficulty == AIRBOSS.Difficulty.EASY then
      -- Get inverse magnetic radial without offset ==> FB for Case II or BRC for Case III.
      local radial = self:GetRadial( playerData.case, true, false, true )
      local turn = "right"
      if self.holdingoffset < 0 then
        turn = "left"
      end
      hint = hint .. string.format( "\nTurn %s and select TACAN %03d°.", turn, radial )
    end

  end

  -- DIRTUP additonal info.
  if playerData.step == AIRBOSS.PatternStep.DIRTYUP then
    if playerData.difficulty == AIRBOSS.Difficulty.EASY then
      if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then
        hint = hint .. "\nFAF! Checks completed. Nozzles 50°."
      else
        -- TODO: Tomcat?
        hint = hint .. "\nDirty up! Hook, gear and flaps down."
      end
    end
  end

  -- BULLSEYE additonal info.
  if playerData.step == AIRBOSS.PatternStep.BULLSEYE then
    -- Hint follow the needles.
    if playerData.difficulty == AIRBOSS.Difficulty.EASY then
      if   playerData.actype == AIRBOSS.AircraftCarrier.HORNET
        or playerData.actype == AIRBOSS.AircraftCarrier.RHINOE
        or playerData.actype == AIRBOSS.AircraftCarrier.RHINOF
        or playerData.actype == AIRBOSS.AircraftCarrier.GROWLER then
        hint = hint .. string.format( "\nIntercept glideslope and follow the needles." )
      else
        hint = hint .. string.format( "\nIntercept glideslope." )
      end
    end
  end

  -- Message to player.
  if hint ~= "" then
    local text = string.format( "%s%s", playerData.step, hint )
    self:MessageToPlayer( playerData, hint, "AIRBOSS", "" )
  end
end

--- Display hint for flight students about the (next) step. Message is displayed after one second.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @param #string step Step for which hint is given.
function AIRBOSS:_StepHint( playerData, step )

  -- Set step.
  step = step or playerData.step

  -- Message is only for "Flight Students".
  if playerData.difficulty == AIRBOSS.Difficulty.EASY and playerData.showhints then

    -- Get optimal parameters at step.
    local alt, aoa, dist, speed = self:_GetAircraftParameters( playerData, step )

    -- Hint:
    local hint = ""

    -- Altitude.
    if alt then
      hint = hint .. string.format( "\nAltitude %d ft", UTILS.MetersToFeet( alt ) )
    end

    -- AoA.
    if aoa then
      hint = hint .. string.format( "\nAoA %.1f", self:_AoADeg2Units( playerData, aoa ) )
    end

    -- Speed.
    if speed then
      hint = hint .. string.format( "\nSpeed %d knots", UTILS.MpsToKnots( speed ) )
    end

    -- Distance to the boat.
    if dist then
      hint = hint .. string.format( "\nDistance to the boat %.1f NM", UTILS.MetersToNM( dist ) )
    end

    -- Late break.
    if step == AIRBOSS.PatternStep.LATEBREAK then
      if playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then
        hint = hint .. "\nWing Sweep 20°, Gear DOWN < 280 KIAS."
      end
    end

    -- Abeam.
    if step == AIRBOSS.PatternStep.ABEAM then
      if playerData.actype == AIRBOSS.AircraftCarrier.AV8B then
        hint = hint .. "\nNozzles 50°-60°. Antiskid OFF. Lights OFF."
      elseif playerData.actype == AIRBOSS.AircraftCarrier.F14A or playerData.actype == AIRBOSS.AircraftCarrier.F14B then
        hint = hint .. "\nSlats/Flaps EXTENDED < 225 KIAS. DLC SELECTED. Auto Throttle IF DESIRED."
      else
        hint = hint .. "\nDirty up! Gear DOWN, flaps DOWN. Check hook down."
      end
    end

    -- Check if there was actually anything to tell.
    if hint ~= "" then

      -- Compile text if any.
      local text = string.format( "Optimal setup at next step %s:%s", step, hint )

      -- Send hint to player.
      self:MessageToPlayer( playerData, text, "AIRBOSS", "", nil, false, 1 )

    end

  end
end

--- Evaluate player's altitude at checkpoint.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @param #number altopt Optimal altitude in meters.
-- @return #string Feedback text.
-- @return #string Debriefing text.
-- @return #AIRBOSS.RadioCall Radio call.
function AIRBOSS:_AltitudeCheck( playerData, altopt )

  if altopt == nil then
    return nil, nil
  end

  -- Player altitude.
  local altitude = playerData.unit:GetAltitude()

  -- Get relative score.
  local lowscore, badscore = self:_GetGoodBadScore( playerData )

  -- Altitude error +-X%
  local _error = (altitude - altopt) / altopt * 100

  -- Radio call for flight students.
  local radiocall = nil -- #AIRBOSS.RadioCall

  local hint = ""
  if _error > badscore then
    -- hint=string.format("You're high.")
    radiocall = self:_NewRadioCall( self.LSOCall.HIGH, "Paddles", "" )
  elseif _error > lowscore then
    -- hint= string.format("You're slightly high.")
    radiocall = self:_NewRadioCall( self.LSOCall.HIGH, "Paddles", "" )
  elseif _error < -badscore then
    -- hint=string.format("You're low. ")
    radiocall = self:_NewRadioCall( self.LSOCall.LOW, "Paddles", "" )
  elseif _error < -lowscore then
    -- hint=string.format("You're slightly low.")
    radiocall = self:_NewRadioCall( self.LSOCall.LOW, "Paddles", "" )
  else
    hint = string.format( "Good altitude. " )
  end

  -- Extend or decrease depending on skill.
  if playerData.difficulty == AIRBOSS.Difficulty.EASY then
    -- Also inform students about the optimal altitude.
    hint = hint .. string.format( "Optimal altitude is %d ft.", UTILS.MetersToFeet( altopt ) )
  elseif playerData.difficulty == AIRBOSS.Difficulty.NORMAL then
    -- We keep it short normally.
    hint = ""
  elseif playerData.difficulty == AIRBOSS.Difficulty.HARD then
    -- No hint at all for the pros.
    hint = ""
  end

  -- Debrief text.
  local debrief = string.format( "Altitude %d ft = %d%% deviation from %d ft.", UTILS.MetersToFeet( altitude ), _error, UTILS.MetersToFeet( altopt ) )

  return hint, debrief, radiocall
end

--- Score for correct AoA.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @param #number optaoa Optimal AoA.
-- @return #string Feedback message text or easy and normal difficulty level or nil for hard.
-- @return #string Debriefing text.
-- @return #AIRBOSS.RadioCall Radio call.
function AIRBOSS:_AoACheck( playerData, optaoa )

  if optaoa == nil then
    return nil, nil
  end

  -- Get relative score.
  local lowscore, badscore = self:_GetGoodBadScore( playerData )

  -- Player AoA
  local aoa = playerData.unit:GetAoA()

  -- Altitude error +-X%
  local _error = (aoa - optaoa) / optaoa * 100

  -- Get aircraft AoA parameters.
  local aircraftaoa = self:_GetAircraftAoA( playerData )

  -- Radio call for flight students.
  local radiocall = nil -- #AIRBOSS.RadioCall

  -- Rate aoa.
  local hint = ""
  if aoa >= aircraftaoa.SLOW then
    -- hint="Your're slow!"
    radiocall = self:_NewRadioCall( self.LSOCall.SLOW, "Paddles", "" )
  elseif aoa >= aircraftaoa.Slow then
    -- hint="Your're slow."
    radiocall = self:_NewRadioCall( self.LSOCall.SLOW, "Paddles", "" )
  elseif aoa >= aircraftaoa.OnSpeedMax then
    hint = "Your're a little slow. "
  elseif aoa >= aircraftaoa.OnSpeedMin then
    hint = "You're on speed. "
  elseif aoa >= aircraftaoa.Fast then
    hint = "You're a little fast. "
  elseif aoa >= aircraftaoa.FAST then
    -- hint="Your're fast."
    radiocall = self:_NewRadioCall( self.LSOCall.FAST, "Paddles", "" )
  else
    -- hint="You're fast!"
    radiocall = self:_NewRadioCall( self.LSOCall.FAST, "Paddles", "" )
  end

  -- Extend or decrease depending on skill.
  if playerData.difficulty == AIRBOSS.Difficulty.EASY then
    -- Also inform students about optimal value.
    hint = hint .. string.format( "Optimal AoA is %.1f.", self:_AoADeg2Units( playerData, optaoa ) )
  elseif playerData.difficulty == AIRBOSS.Difficulty.NORMAL then
    -- We keep is short normally.
    hint = ""
  elseif playerData.difficulty == AIRBOSS.Difficulty.HARD then
    -- No hint at all for the pros.
    hint = ""
  end

  -- Debriefing text.
  local debrief = string.format( "AoA %.1f = %d%% deviation from %.1f.", self:_AoADeg2Units( playerData, aoa ), _error, self:_AoADeg2Units( playerData, optaoa ) )

  return hint, debrief, radiocall
end

--- Evaluate player's speed.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @param #number speedopt Optimal speed in m/s.
-- @return #string Feedback text.
-- @return #string Debriefing text.
-- @return #AIRBOSS.RadioCall Radio call.
function AIRBOSS:_SpeedCheck( playerData, speedopt )

  if speedopt == nil then
    return nil, nil
  end

  -- Player altitude.
  local speed = playerData.unit:GetVelocityMPS()

  -- Get relative score.
  local lowscore, badscore = self:_GetGoodBadScore( playerData )

  -- Altitude error +-X%
  local _error = (speed - speedopt) / speedopt * 100

  -- Radio call for flight students.
  local radiocall = nil -- #AIRBOSS.RadioCall

  local hint = ""
  if _error > badscore then
    -- hint=string.format("You're fast.")
    radiocall = self:_NewRadioCall( self.LSOCall.FAST, "AIRBOSS", "" )
  elseif _error > lowscore then
    -- hint= string.format("You're slightly fast.")
    radiocall = self:_NewRadioCall( self.LSOCall.FAST, "AIRBOSS", "" )
  elseif _error < -badscore then
    -- hint=string.format("You're slow.")
    radiocall = self:_NewRadioCall( self.LSOCall.SLOW, "AIRBOSS", "" )
  elseif _error < -lowscore then
    -- hint=string.format("You're slightly slow.")
    radiocall = self:_NewRadioCall( self.LSOCall.SLOW, "AIRBOSS", "" )
  else
    hint = string.format( "Good speed. " )
  end

  -- Extend or decrease depending on skill.
  if playerData.difficulty == AIRBOSS.Difficulty.EASY then
    hint = hint .. string.format( "Optimal speed is %d knots.", UTILS.MpsToKnots( speedopt ) )
  elseif playerData.difficulty == AIRBOSS.Difficulty.NORMAL then
    -- We keep is short normally.
    hint = ""
  elseif playerData.difficulty == AIRBOSS.Difficulty.HARD then
    -- No hint at all for pros.
    hint = ""
  end

  -- Debrief text.
  local debrief = string.format( "Speed %d knots = %d%% deviation from %d knots.", UTILS.MpsToKnots( speed ), _error, UTILS.MpsToKnots( speedopt ) )

  return hint, debrief, radiocall
end

--- Evaluate player's distance to the boat at checkpoint.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @param #number optdist Optimal distance in meters.
-- @return #string Feedback message text.
-- @return #string Debriefing text.
-- @return #AIRBOSS.RadioCall Distance radio call. Not implemented yet.
function AIRBOSS:_DistanceCheck( playerData, optdist )

  if optdist == nil then
    return nil, nil
  end

  -- Distance to carrier.
  local distance = playerData.unit:GetCoordinate():Get2DDistance( self:GetCoordinate() )

  -- Get relative score.
  local lowscore, badscore = self:_GetGoodBadScore( playerData )

  -- Altitude error +-X%
  local _error = (distance - optdist) / optdist * 100

  local hint
  if _error > badscore then
    hint = string.format( "You're too far from the boat!" )
  elseif _error > lowscore then
    hint = string.format( "You're slightly too far from the boat." )
  elseif _error < -badscore then
    hint = string.format( "You're too close to the boat!" )
  elseif _error < -lowscore then
    hint = string.format( "You're slightly too far from the boat." )
  else
    hint = string.format( "Good distance to the boat." )
  end

  -- Extend or decrease depending on skill.
  if playerData.difficulty == AIRBOSS.Difficulty.EASY then
    -- Also inform students about optimal value.
    hint = hint .. string.format( " Optimal distance is %.1f NM.", UTILS.MetersToNM( optdist ) )
  elseif playerData.difficulty == AIRBOSS.Difficulty.NORMAL then
    -- We keep it short normally.
    hint = ""
  elseif playerData.difficulty == AIRBOSS.Difficulty.HARD then
    -- No hint at all for the pros.
    hint = ""
  end

  -- Debriefing text.
  local debrief = string.format( "Distance %.1f NM = %d%% deviation from %.1f NM.", UTILS.MetersToNM( distance ), _error, UTILS.MetersToNM( optdist ) )

  return hint, debrief, nil
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- DEBRIEFING
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Append text to debriefing.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @param #string hint Debrief text of this step.
-- @param #string step (Optional) Current step in the pattern. Default from playerData.
function AIRBOSS:_AddToDebrief( playerData, hint, step )
  step = step or playerData.step
  table.insert( playerData.debrief, { step = step, hint = hint } )
end

--- Debrief player and set next step.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
function AIRBOSS:_Debrief( playerData )
  self:F( self.lid .. string.format( "Debriefing of player %s.", playerData.name ) )

  -- Delete scheduler ID.
  playerData.debriefschedulerID = nil

  -- Switch attitude monitor off if on.
  playerData.attitudemonitor = false

  -- LSO grade, points, and flight data analyis.
  local grade, points, analysis = self:_LSOgrade( playerData )

  -- Insert points to table of all points until player landed.
  if points and points >= 0 then
    table.insert( playerData.points, points )
  end

  -- Player has landed and is not airborne any more.
  local Points = 0
  if playerData.landed and not playerData.unit:InAir() then

    -- Average over all points received so far.
    for _, _points in pairs( playerData.points ) do
      Points = Points + _points
    end

    -- This is the final points.
    Points = Points / #playerData.points

    -- Reset points array.
    playerData.points = {}
  else
    -- Player boltered or was waved off ==> We display the normal points.
    Points = points
  end


  -- My LSO grade.
  local mygrade = {} -- #AIRBOSS.LSOgrade
  mygrade.grade = grade
  mygrade.points = points
  mygrade.details = analysis
  mygrade.wire = playerData.wire
  mygrade.Tgroove = playerData.Tgroove
  if playerData.landed and not playerData.unit:InAir() then
    mygrade.finalscore = Points
  end
  mygrade.case = playerData.case
  local windondeck = self:GetWindOnDeck()
  mygrade.wind = UTILS.Round( UTILS.MpsToKnots( windondeck ), 1 )
  mygrade.modex = playerData.onboard
  mygrade.airframe = playerData.actype
  mygrade.carriertype = self.carriertype
  mygrade.carriername = self.alias
  mygrade.carrierrwy  = self.carrierparam.rwyangle
  mygrade.theatre = self.theatre
  mygrade.mitime = UTILS.SecondsToClock( timer.getAbsTime(), true )
  mygrade.midate = UTILS.GetDCSMissionDate()
  mygrade.osdate = "n/a"
  if os then
    mygrade.osdate = os.date() -- os.date("%d.%m.%Y")
  end

  -- Add last grade to playerdata for FunkMan.
  playerData.grade=mygrade

  -- Save trap sheet.
  if playerData.trapon and self.trapsheet then
    self:_SaveTrapSheet( playerData, mygrade )
  end

  -- Add LSO grade to player grades table.
  table.insert( self.playerscores[playerData.name], mygrade )

  -- Trigger grading event.
  self:LSOGrade( playerData, mygrade )

  -- LSO grade: (OK) 3.0 PT - LURIM
  local text = string.format( "%s %.1f PT - %s", grade, Points, analysis )
  if Points == -1 then
    text = string.format( "%s n/a PT - Foul deck", grade, Points, analysis )
  end

  -- Wire and Groove time only if not pattern WO.
  if not (playerData.wop or playerData.wofd) then

    -- Wire trapped. Not if pattern WI.
    if playerData.wire and playerData.wire <= 4 then
      text = text .. string.format( " %d-wire", playerData.wire )
    end

    -- Time in the groove. Only Case I/II and not pattern WO.
    if playerData.Tgroove and playerData.Tgroove <= 360 and playerData.case < 3 then
      -- text = text .. string.format( "\nTime in the groove %.1f seconds: %s", playerData.Tgroove, self:_EvalGrooveTime( playerData ) ) --Circuit changed removed groove comment
      text = text .. string.format( "\nTime in the groove %.1f seconds.", playerData.Tgroove ) 
    end

  end

  -- Copy debriefing text.
  playerData.lastdebrief = UTILS.DeepCopy( playerData.debrief )

  -- Info text.
  if playerData.difficulty == AIRBOSS.Difficulty.EASY then
    text = text .. string.format( "\nYour detailed debriefing can be found via the F10 radio menu." )
  end

  -- Message.
  self:MessageToPlayer( playerData, text, "LSO", "", 30, true )

  -- Set step to undefined and check if other cases apply.
  playerData.step = AIRBOSS.PatternStep.UNDEFINED

  -- Check what happened?
  if playerData.wop then

    ----------------------
    -- Pattern Wave Off --
    ----------------------

    -- Next step?
    -- TODO: CASE I: After bolter/wo turn left and climb to 600 ft and re-enter the pattern. But do not go to initial but reenter earlier?
    -- TODO: CASE I: After pattern wo? go back to initial, I guess?
    -- TODO: CASE III: After bolter/wo turn left and climb to 1200 ft and re-enter pattern?
    -- TODO: CASE III: After pattern wo? No idea...

    -- Can become nil when I crashed and changed to observer. Which events are captured? Nil check for unit?
    if playerData.unit:IsAlive() then

      -- Heading and distance tip.
      local heading, distance

      if playerData.case == 1 or playerData.case == 2 then

        -- Next step: Initial again.
        playerData.step = AIRBOSS.PatternStep.INITIAL

        -- Create a point 3.0 NM astern for re-entry.
        local initial = self:GetCoordinate():Translate( UTILS.NMToMeters( 3.5 ), self:GetRadial( 2, false, false, false ) )

        -- Get heading and distance to initial zone ~3 NM astern.
        heading = playerData.unit:GetCoordinate():HeadingTo( initial )
        distance = playerData.unit:GetCoordinate():Get2DDistance( initial )

      elseif playerData.case == 3 then

        -- Next step? Bullseye for now.
        -- TODO: Could be DIRTY UP or PLATFORM or even back to MARSHAL STACK?
        playerData.step = AIRBOSS.PatternStep.BULLSEYE

        -- Get heading and distance to bullseye zone ~3 NM astern.
        local zone = self:_GetZoneBullseye( playerData.case )

        heading = playerData.unit:GetCoordinate():HeadingTo( zone:GetCoordinate() )
        distance = playerData.unit:GetCoordinate():Get2DDistance( zone:GetCoordinate() )

      end

      -- Re-enter message.
      local text = string.format( "fly heading %03d° for %d NM to re-enter the pattern.", heading, UTILS.MetersToNM( distance ) )
      self:MessageToPlayer( playerData, text, "LSO", nil, nil, false, 5 )

    else

      -- Unit does not seem to be alive!
      -- TODO: What now?
      self:E( self.lid .. string.format( "ERROR: Player unit not alive!" ) )

    end

  elseif playerData.wofd then

    ---------------
    -- Foul Deck --
    ---------------

    if playerData.unit:InAir() then

      -- Bolter pattern. Then Abeam or bullseye.
      playerData.step = AIRBOSS.PatternStep.BOLTER

    else

      -- Welcome aboard!
      self:Sound2Player( playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD )

      -- Airboss talkto!
      local text = string.format( "deck was fouled but you landed anyway. Airboss wants to talk to you!" )
      self:MessageToPlayer( playerData, text, "LSO", nil, nil, false, 3 )

    end

  elseif playerData.owo then

    ------------------
    -- Own Wave Off --
    ------------------

    if playerData.unit:InAir() then

      -- Bolter pattern. Then Abeam or bullseye.
      playerData.step = AIRBOSS.PatternStep.BOLTER

    else

      -- Welcome aboard!
      -- NOTE: This should not happen as owo is only triggered if player flew past the carrier.
      self:E( self.lid .. "ERROR: player landed when OWO was issues. This should not happen. Please report!" )
      self:Sound2Player( playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD )

    end

  elseif playerData.waveoff then

    --------------
    -- Wave Off --
    --------------

    if playerData.unit:InAir() then

      -- Bolter pattern. Then Abeam or bullseye.
      playerData.step = AIRBOSS.PatternStep.BOLTER

    else

      -- Welcome aboard!
      self:Sound2Player( playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD )

      -- Airboss talkto!
      local text = string.format( "you were waved off but landed anyway. Airboss wants to talk to you!" )
      self:MessageToPlayer( playerData, text, "LSO", nil, nil, false, 3 )

    end

  elseif playerData.boltered then

    --------------
    -- Boltered --
    --------------

    if playerData.unit:InAir() then

      -- Bolter pattern. Then Abeam or bullseye.
      playerData.step = AIRBOSS.PatternStep.BOLTER

    end

  elseif playerData.landed then

    ------------
    -- Landed --
    ------------

    if not playerData.unit:InAir() then

      -- Welcome aboard!
      self:Sound2Player( playerData, self.LSORadio, self.LSOCall.WELCOMEABOARD )

    end

  else

    -- Message to player.
    self:MessageToPlayer( playerData, "Undefined state after landing! Please report.", "ERROR", nil, 20 )

    -- Next step.
    playerData.step = AIRBOSS.PatternStep.UNDEFINED

  end

  -- Player landed and is not in air anymore.
  if playerData.landed and not playerData.unit:InAir() then
    -- Set recovered flag.
    self:_RecoveredElement( playerData.unit )

    -- Check if all elements
    self:_CheckSectionRecovered( playerData )
  end

  -- Increase number of passes.
  playerData.passes = playerData.passes + 1

  -- Next step hint for students if any.
  self:_StepHint( playerData )

  -- Reinitialize player data for new approach.
  self:_InitPlayer( playerData, playerData.step )

  -- Debug message.
  MESSAGE:New( string.format( "Player step %s.", playerData.step ), 5, "DEBUG" ):ToAllIf( self.Debug )

  -- Auto save player results.
  if self.autosave and mygrade.finalscore then
    self:Save( self.autosavepath, self.autosavefile )
  end
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- CARRIER ROUTING Functions
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check for possible collisions between two coordinates.
-- @param #AIRBOSS self
-- @param Core.Point#COORDINATE coordto Coordinate to which the collision is check.
-- @param Core.Point#COORDINATE coordfrom Coordinate from which the collision is check.
-- @return #boolean If true, surface type ahead is not deep water.
-- @return #number Max free distance in meters.
function AIRBOSS:_CheckCollisionCoord( coordto, coordfrom )

  -- Increment in meters.
  local dx = 100

  -- From coordinate. Default 500 in front of the carrier.
  local d = 0
  if coordfrom then
    d = 0
  else
    d = 250
    coordfrom = self:GetCoordinate():Translate( d, self:GetHeading() )
  end

  -- Distance between the two coordinates.
  local dmax = coordfrom:Get2DDistance( coordto )

  -- Direction.
  local direction = coordfrom:HeadingTo( coordto )

  -- Scan path between the two coordinates.
  local clear = true
  while d <= dmax do

    -- Check point.
    local cp = coordfrom:Translate( d, direction )

    -- Check if surface type is water.
    if not cp:IsSurfaceTypeWater() then

      -- Debug mark points.
      if self.Debug then
        local st = cp:GetSurfaceType()
        cp:MarkToAll( string.format( "Collision check surface type %d", st ) )
      end

      -- Collision WARNING!
      clear = false
      break
    end

    -- Increase distance.
    d = d + dx
  end

  local text = ""
  if clear then
    text = string.format( "Path into direction %03d° is clear for the next %.1f NM.", direction, UTILS.MetersToNM( d ) )
  else
    text = string.format( "Detected obstacle at distance %.1f NM into direction %03d°.", UTILS.MetersToNM( d ), direction )
  end
  self:T2( self.lid .. text )

  return not clear, d
end

--- Check Collision.
-- @param #AIRBOSS self
-- @param Core.Point#COORDINATE fromcoord Coordinate from which the path to the next WP is calculated. Default current carrier position.
-- @return #boolean If true, surface type ahead is not deep water.
function AIRBOSS:_CheckFreePathToNextWP( fromcoord )

  -- Position.
  fromcoord = fromcoord or self:GetCoordinate():Translate( 250, self:GetHeading() )

  -- Next wp = current+1 (or last)
  local Nnextwp = math.min( self.currentwp + 1, #self.waypoints )

  -- Next waypoint.
  local nextwp = self.waypoints[Nnextwp] -- Core.Point#COORDINATE

  -- Check for collision.
  local collision = self:_CheckCollisionCoord( nextwp, fromcoord )

  return collision
end

--- Find free path to the next waypoint.
-- @param #AIRBOSS self
function AIRBOSS:_Pathfinder()

  -- Heading and current coordiante.
  local hdg = self:GetHeading()
  local cv = self:GetCoordinate()

  -- Possible directions.
  local directions = { -20, 20, -30, 30, -40, 40, -50, 50, -60, 60, -70, 70, -80, 80, -90, 90, -100, 100 }

  -- Starboard turns up to 90 degrees.
  for _, _direction in pairs( directions ) do

    -- New direction.
    local direction = hdg + _direction

    -- Check for collisions in the next 20 NM of the current direction.
    local _, dfree = self:_CheckCollisionCoord( cv:Translate( UTILS.NMToMeters( 20 ), direction ), cv )

    -- Loop over distances and find the first one which gives a clear path to the next waypoint.
    local distance = 500
    while distance <= dfree do

      -- Coordinate from which we calculate the path.
      local fromcoord = cv:Translate( distance, direction )

      -- Check for collision between point and next waypoint.
      local collision = self:_CheckFreePathToNextWP( fromcoord )

      -- Debug info.
      self:T2( self.lid .. string.format( "Pathfinder d=%.1f m, direction=%03d°, collision=%s", distance, direction, tostring( collision ) ) )

      -- If path is clear, we start a little detour.
      if not collision then
        self:CarrierDetour( fromcoord )
        return
      end

      distance = distance + 500
    end
  end
end

--- Carrier resumes the route at its next waypoint.
-- @param #AIRBOSS self
-- @param Core.Point#COORDINATE gotocoord (Optional) First goto this coordinate before resuming route.
-- @return #AIRBOSS self
function AIRBOSS:CarrierResumeRoute( gotocoord )

  -- Make carrier resume its route.
  AIRBOSS._ResumeRoute( self.carrier:GetGroup(), self, gotocoord )

  return self
end

--- Let the carrier make a detour to a given point. When it reaches the point, it will resume its normal route.
-- @param #AIRBOSS self
-- @param Core.Point#COORDINATE coord Coordinate of the detour.
-- @param #number speed Speed in knots. Default is current carrier velocity.
-- @param #boolean uturn (Optional) If true, carrier will go back to where it came from before it resumes its route to the next waypoint.
-- @param #number uspeed Speed in knots after U-turn. Default is same as before.
-- @param Core.Point#COORDINATE tcoord Additional coordinate to make turn smoother.
-- @return #AIRBOSS self
function AIRBOSS:CarrierDetour( coord, speed, uturn, uspeed, tcoord )

  -- Current coordinate of the carrier.
  local pos0 = self:GetCoordinate()

  -- Current speed in knots.
  local vel0 = self.carrier:GetVelocityKNOTS()

  -- Default. If speed is not given we take the current speed but at least 5 knots.
  speed = speed or math.max( vel0, 5 )

  -- Speed in km/h. At least 2 knots.
  local speedkmh = math.max( UTILS.KnotsToKmph( speed ), UTILS.KnotsToKmph( 2 ) )

  -- Turn speed in km/h. At least 10 knots.
  local cspeedkmh = math.max( self.carrier:GetVelocityKMH(), UTILS.KnotsToKmph( 10 ) )

  -- U-turn speed in km/h.
  local uspeedkmh = UTILS.KnotsToKmph( uspeed or speed )

  -- Waypoint table.
  local wp = {}

  -- Waypoint at current position.
  table.insert( wp, pos0:WaypointGround( cspeedkmh ) )

  -- Waypooint to help the turn.
  if tcoord then
    table.insert( wp, tcoord:WaypointGround( cspeedkmh ) )
  end

  -- Detour waypoint.
  table.insert( wp, coord:WaypointGround( speedkmh ) )

  -- U-turn waypoint. If enabled, go back to where you came from.
  if uturn then
    table.insert( wp, pos0:WaypointGround( uspeedkmh ) )
  end

  -- Get carrier group.
  local group = self.carrier:GetGroup()

  -- Passing waypoint taskfunction
  local TaskResumeRoute = group:TaskFunction( "AIRBOSS._ResumeRoute", self )

  -- Set task to restart route at the last point.
  group:SetTaskWaypoint( wp[#wp], TaskResumeRoute )

  -- Debug mark.
  if self.Debug then
    if tcoord then
      tcoord:MarkToAll( string.format( "Detour Turn Help WP. Speed %.1f knots", UTILS.KmphToKnots( cspeedkmh ) ) )
    end
    coord:MarkToAll( string.format( "Detour Waypoint. Speed %.1f knots", UTILS.KmphToKnots( speedkmh ) ) )
    if uturn then
      pos0:MarkToAll( string.format( "Detour U-turn WP. Speed %.1f knots", UTILS.KmphToKnots( uspeedkmh ) ) )
    end
  end

  -- Detour switch true.
  self.detour = true

  -- Route carrier into the wind.
  self.carrier:Route( wp )
end

--- Let the carrier turn into the wind.
-- @param #AIRBOSS self
-- @param #number time Time in seconds.
-- @param #number vdeck Speed on deck m/s. Carrier will
-- @param #boolean uturn Make U-turn and go back to initial after downwind leg.
-- @return #AIRBOSS self
function AIRBOSS:CarrierTurnIntoWind( time, vdeck, uturn )

  -- Wind speed.
  local _, vwind = self:GetWind()

  -- Desired wind on deck in knots.
  local vdeck=UTILS.MpsToKnots(vdeck)

  -- Get heading into the wind accounting for angled runway.
  local hiw, speedknots = self:GetHeadingIntoWind(vdeck)

  -- Speed of carrier in m/s but at least 4 knots.
  local vtot = UTILS.KnotsToMps(speedknots)

  -- Distance to travel
  local dist = vtot * time

  -- Distance in NM.
  local distNM = UTILS.MetersToNM( dist )

  -- Current heading.
  local hdg = self:GetHeading()

  -- Heading difference.
  local deltaH = self:_GetDeltaHeading( hdg, hiw )

  -- Debug output
  self:I( self.lid .. string.format( "Carrier steaming into the wind (%.1f kts). Heading=%03d-->%03d (Delta=%.1f), Speed=%.1f knots, Distance=%.1f NM, Time=%d sec",
  UTILS.MpsToKnots( vwind ), hdg, hiw, deltaH, speedknots, distNM, speedknots, time ) )

  -- Current coordinate.
  local Cv = self:GetCoordinate()

  local Ctiw = nil -- Core.Point#COORDINATE
  local Csoo = nil -- Core.Point#COORDINATE

  -- Define path depending on turn angle.
  if deltaH < 45 then
    -- Small turn.

    -- Point in the right direction to help turning.
    Csoo = Cv:Translate( 750, hdg ):Translate( 750, hiw )

    -- Heading into wind from Csoo.
    local hsw = self:GetHeadingIntoWind(vdeck, false, Csoo )

    -- Into the wind coord.
    Ctiw = Csoo:Translate( dist, hsw )

  elseif deltaH < 90 then
    -- Medium turn.

    -- Point in the right direction to help turning.
    Csoo = Cv:Translate( 900, hdg ):Translate( 900, hiw )

    -- Heading into wind from Csoo.
    local hsw = self:GetHeadingIntoWind(vdeck, false, Csoo )

    -- Into the wind coord.
    Ctiw = Csoo:Translate( dist, hsw )

  elseif deltaH < 135 then
    -- Large turn backwards.

    -- Point in the right direction to help turning.
    Csoo = Cv:Translate( 1100, hdg - 90 ):Translate( 1000, hiw )

    -- Heading into wind from Csoo.
    local hsw = self:GetHeadingIntoWind(vdeck, false, Csoo )

    -- Into the wind coord.
    Ctiw = Csoo:Translate( dist, hsw )

  else
    -- Huge turn backwards.

    -- Point in the right direction to help turning.
    Csoo = Cv:Translate( 1200, hdg - 90 ):Translate( 1000, hiw )

    -- Heading into wind from Csoo.
    local hsw = self:GetHeadingIntoWind(vdeck, false, Csoo )

    -- Into the wind coord.
    Ctiw = Csoo:Translate( dist, hsw )

  end

  -- Return to coordinate if collision is detected.
  self.Creturnto = self:GetCoordinate()

  -- Next waypoint.
  local nextwp = self:_GetNextWaypoint()

  -- For downwind, we take the velocity at the next WP.
  local vdownwind = UTILS.MpsToKnots( nextwp:GetVelocity() )

  -- Make sure we move at all in case the speed at the waypoint is zero.
  if vdownwind < 1 then
    vdownwind = 10
  end

  -- Let the carrier make a detour from its route but return to its current position.
  self:CarrierDetour( Ctiw, speedknots, uturn, vdownwind, Csoo )

  -- Set switch that we are currently turning into the wind.
  self.turnintowind = true

  return self
end

--- Get next waypoint of the carrier.
-- @param #AIRBOSS self
-- @return Core.Point#COORDINATE Coordinate of the next waypoint.
-- @return #number Number of waypoint.
function AIRBOSS:_GetNextWaypoint()

  -- Next waypoint.
  local Nextwp = nil
  if self.currentwp == #self.waypoints then
    Nextwp = 1
  else
    Nextwp = self.currentwp + 1
  end

  -- Debug output
  local text = string.format( "Current WP=%d/%d, next WP=%d", self.currentwp, #self.waypoints, Nextwp )
  self:T2( self.lid .. text )

  -- Next waypoint.
  local nextwp = self.waypoints[Nextwp] -- Core.Point#COORDINATE

  return nextwp, Nextwp
end

--- Initialize Mission Editor waypoints.
-- @param #AIRBOSS self
-- @return #AIRBOSS self
function AIRBOSS:_InitWaypoints()

  -- Waypoints of group as defined in the ME.
  local Waypoints = self.carrier:GetGroup():GetTemplateRoutePoints()

  -- Init array.
  self.waypoints = {}

  -- Set waypoint table.
  for i, point in ipairs( Waypoints ) do

    -- Coordinate of the waypoint
    local coord = COORDINATE:New( point.x, point.alt, point.y )

    -- Set velocity of the coordinate.
    coord:SetVelocity( point.speed )

    -- Add to table.
    table.insert( self.waypoints, coord )

    -- Debug info.
    if self.Debug then
      coord:MarkToAll( string.format( "Carrier Waypoint %d, Speed=%.1f knots", i, UTILS.MpsToKnots( point.speed ) ) )
    end

  end

  return self
end

--- Patrol carrier.
-- @param #AIRBOSS self
-- @param #number n Next waypoint number.
-- @return #AIRBOSS self
function AIRBOSS:_PatrolRoute( n )

  -- Get next waypoint coordinate and number.
  local nextWP, N = self:_GetNextWaypoint()

  -- Default resume is to next waypoint.
  n = n or N

  -- Get carrier group.
  local CarrierGroup = self.carrier:GetGroup()

  -- Waypoints table.
  local Waypoints = {}

  -- Create a waypoint from the current coordinate.
  local wp = self:GetCoordinate():WaypointGround( CarrierGroup:GetVelocityKMH() )

  -- Add current position as first waypoint.
  table.insert( Waypoints, wp )

  -- Loop over waypoints.
  for i = n, #self.waypoints do
    local coord = self.waypoints[i] -- Core.Point#COORDINATE

    -- Create a waypoint from the coordinate.
    local wp = coord:WaypointGround( UTILS.MpsToKmph( coord.Velocity ) )

    -- Passing waypoint taskfunction
    local TaskPassingWP = CarrierGroup:TaskFunction( "AIRBOSS._PassingWaypoint", self, i, #self.waypoints )

    -- Call task function when carrier arrives at waypoint.
    CarrierGroup:SetTaskWaypoint( wp, TaskPassingWP )

    -- Add waypoint to table.
    table.insert( Waypoints, wp )
  end

  -- Route carrier group.
  CarrierGroup:Route( Waypoints )

  return self
end

--- Estimated the carrier position at some point in the future given the current waypoints and speeds.
-- @param #AIRBOSS self
-- @return DCS#time ETA abs. time in seconds.
function AIRBOSS:_GetETAatNextWP()

  -- Current waypoint
  local cwp = self.currentwp

  -- Current abs. time.
  local tnow = timer.getAbsTime()

  -- Current position.
  local p = self:GetCoordinate()

  -- Current velocity [m/s].
  local v = self.carrier:GetVelocityMPS()

  -- Next waypoint.
  local nextWP = self:_GetNextWaypoint()

  -- Distance to next waypoint.
  local s = p:Get2DDistance( nextWP )

  -- Distance to next waypoint.
  -- local s=0
  -- if #self.waypoints>cwp then
  --  s=p:Get2DDistance(self.waypoints[cwp+1])
  -- end

  -- v=s/t <==> t=s/v
  local t = s / v

  -- ETA
  local eta = t + tnow

  return eta
end

--- Check if carrier is turning. If turning started or stopped, we inform the players via radio message.
-- @param #AIRBOSS self
function AIRBOSS:_CheckCarrierTurning()

  -- Current orientation of carrier.
  local vNew = self.carrier:GetOrientationX()

  -- Last orientation from 30 seconds ago.
  local vLast = self.Corientlast

  -- We only need the X-Z plane.
  vNew.y = 0;
  vLast.y = 0

  -- Angle between current heading and last time we checked ~30 seconds ago.
  local deltaLast = math.deg( math.acos( UTILS.VecDot( vNew, vLast ) / UTILS.VecNorm( vNew ) / UTILS.VecNorm( vLast ) ) )

  -- Last orientation becomes new orientation
  self.Corientlast = vNew

  -- Carrier is turning when its heading changed by at least one degree since last check.
  local turning = math.abs( deltaLast ) >= 1

  -- Check if turning stopped. (Carrier was turning but is not any more.)
  if self.turning and not turning then

    -- Get final bearing.
    local FB = self:GetFinalBearing( true )

    -- Marshal radio call: "99, new final bearing XYZ degrees."
    self:_MarshalCallNewFinalBearing( FB )

  end

  -- Check if turning started. (Carrier was not turning and is now.)
  if turning and not self.turning then

    -- Get heading.
    local hdg
    if self.turnintowind then
      -- We are now steaming into the wind.
      local vdeck=self.recoverywindow and self.recoverywindow.SPEED or 20
      hdg = self:GetHeadingIntoWind(vdeck, false)
    else
      -- We turn towards the next waypoint.
      hdg = self:GetCoordinate():HeadingTo( self:_GetNextWaypoint() )
    end

    -- Magnetic!
    hdg = hdg - self.magvar
    if hdg < 0 then
      hdg = 360 + hdg
    end

    -- Radio call: "99, Carrier starting turn to heading XYZ degrees".
    self:_MarshalCallCarrierTurnTo( hdg )
  end

  -- Update turning.
  self.turning = turning
end

--- Check if heading or position of carrier have changed significantly.
-- @param #AIRBOSS self
function AIRBOSS:_CheckPatternUpdate()

  ----------------------------------------
  -- TODO: Make parameters input values --
  ----------------------------------------

  -- Min 10 min between pattern updates.
  local dTPupdate = 10 * 60

  -- Update if carrier moves by more than 2.5 NM.
  local Dupdate = UTILS.NMToMeters( 2.5 )

  -- Update if carrier turned by more than 5°.
  local Hupdate = 5

  -----------------------
  -- Time Update Check --
  -----------------------

  -- Time since last pattern update
  local dt = timer.getTime() - self.Tpupdate

  -- Check whether at least 10 min between updates and not turning currently.
  if dt < dTPupdate or self.turning then
    return
  end

  --------------------------
  -- Heading Update Check --
  --------------------------

  -- Current orientation of carrier.
  local vNew = self.carrier:GetOrientationX()

  -- Reference orientation of carrier after the last update.
  local vOld = self.Corientation

  -- We only need the X-Z plane.
  vNew.y = 0;
  vOld.y = 0

  -- Get angle between old and new orientation vectors in rad and convert to degrees.
  local deltaHeading = math.deg( math.acos( UTILS.VecDot( vNew, vOld ) / UTILS.VecNorm( vNew ) / UTILS.VecNorm( vOld ) ) )

  -- Check if orientation changed.
  local Hchange = false
  if math.abs( deltaHeading ) >= Hupdate then
    self:T( self.lid .. string.format( "Carrier heading changed by %d°.", deltaHeading ) )
    Hchange = true
  end

  ---------------------------
  -- Distance Update Check --
  ---------------------------

  -- Get current position and orientation of carrier.
  local pos = self:GetCoordinate()

  -- Get distance to saved position.
  local dist = pos:Get2DDistance( self.Cposition )

  -- Check if carrier moved more than ~10 km.
  local Dchange = false
  if dist >= Dupdate then
    self:T( self.lid .. string.format( "Carrier position changed by %.1f NM.", UTILS.MetersToNM( dist ) ) )
    Dchange = true
  end

  ----------------------------
  -- Update Marshal Flights --
  ----------------------------

  -- If heading or distance changed ==> update marshal AI patterns.
  if Hchange or Dchange then

    -- Loop over all marshal flights
    for _, _flight in pairs( self.Qmarshal ) do
      local flight = _flight -- #AIRBOSS.FlightGroup

      -- Update marshal pattern of AI keeping the same stack.
      if flight.ai then
        self:_MarshalAI( flight, flight.flag )
      end

    end

    -- Reset parameters for next update check.
    self.Corientation = vNew
    self.Cposition = pos
    self.Tpupdate = timer.getTime()
  end

end

--- Function called when a group is passing a waypoint.
-- @param Wrapper.Group#GROUP group Group that passed the waypoint
-- @param #AIRBOSS airboss Airboss object.
-- @param #number i Waypoint number that has been reached.
-- @param #number final Final waypoint number.
function AIRBOSS._PassingWaypoint( group, airboss, i, final )

  -- Debug message.
  local text = string.format( "Group %s passing waypoint %d of %d.", group:GetName(), i, final )

  -- Debug smoke and marker.
  if airboss.Debug and false then
    local pos = group:GetCoordinate()
    pos:SmokeRed()
    local MarkerID = pos:MarkToAll( string.format( "Group %s reached waypoint %d", group:GetName(), i ) )
  end

  -- Debug message.
  MESSAGE:New( text, 10 ):ToAllIf( airboss.Debug )
  airboss:T( airboss.lid .. text )

  -- Set current waypoint.
  airboss.currentwp = i

  -- Passing Waypoint event.
  airboss:PassingWaypoint( i )

  -- Reactivate beacons.
  -- airboss:_ActivateBeacons()

  -- If final waypoint reached, do route all over again.
  if i == final and final > 1 and airboss.adinfinitum then
    airboss:_PatrolRoute()
  end
end

--- Carrier Strike Group resumes the route of the waypoints defined in the mission editor.
-- @param Wrapper.Group#GROUP group Carrier Strike Group that passed the waypoint.
-- @param #AIRBOSS airboss Airboss object.
-- @param Core.Point#COORDINATE gotocoord Go to coordinate before route is resumed.
function AIRBOSS._ResumeRoute( group, airboss, gotocoord )

  -- Get next waypoint
  local nextwp, Nextwp = airboss:_GetNextWaypoint()

  -- Speed set at waypoint.
  local speedkmh = nextwp.Velocity * 3.6

  -- If speed at waypoint is zero, we set it to 10 knots.
  if speedkmh < 1 then
    speedkmh = UTILS.KnotsToKmph( 10 )
  end

  -- Waypoints array.
  local waypoints = {}

  -- Current position.
  local c0 = group:GetCoordinate()

  -- Current positon as first waypoint.
  local wp0 = c0:WaypointGround( speedkmh )
  table.insert( waypoints, wp0 )

  -- First goto this coordinate.
  if gotocoord then

    -- gotocoord:MarkToAll(string.format("Goto waypoint speed=%.1f km/h", speedkmh))

    local headingto = c0:HeadingTo( gotocoord )

    local hdg1 = airboss:GetHeading()
    local hdg2 = c0:HeadingTo( gotocoord )
    local delta = airboss:_GetDeltaHeading( hdg1, hdg2 )

    -- env.info(string.format("FF hdg1=%d, hdg2=%d, delta=%d", hdg1, hdg2, delta))

    -- Add additional turn points
    if delta > 90 then

      -- Turn radius 3 NM.
      local turnradius = UTILS.NMToMeters( 3 )

      local gotocoordh = c0:Translate( turnradius, hdg1 + 45 )
      -- gotocoordh:MarkToAll(string.format("Goto help waypoint 1 speed=%.1f km/h", speedkmh))

      local wp = gotocoordh:WaypointGround( speedkmh )
      table.insert( waypoints, wp )

      gotocoordh = c0:Translate( turnradius, hdg1 + 90 )
      -- gotocoordh:MarkToAll(string.format("Goto help waypoint 2 speed=%.1f km/h", speedkmh))

      wp = gotocoordh:WaypointGround( speedkmh )
      table.insert( waypoints, wp )

    end

    local wp1 = gotocoord:WaypointGround( speedkmh )
    table.insert( waypoints, wp1 )

  end

  -- Debug message.
  local text = string.format( "Carrier is resuming route. Next waypoint %d, Speed=%.1f knots.", Nextwp, UTILS.KmphToKnots( speedkmh ) )

  -- Debug message.
  MESSAGE:New( text, 10 ):ToAllIf( airboss.Debug )
  airboss:I( airboss.lid .. text )

  -- Loop over all remaining waypoints.
  for i = Nextwp, #airboss.waypoints do

    -- Coordinate of the next WP.
    local coord = airboss.waypoints[i] -- Core.Point#COORDINATE

    -- Speed in km/h of that WP. Velocity is in m/s.
    local speed = coord.Velocity * 3.6

    -- If speed is zero we set it to 10 knots.
    if speed < 1 then
      speed = UTILS.KnotsToKmph( 10 )
    end

    -- coord:MarkToAll(string.format("Resume route WP %d, speed=%.1f km/h", i, speed))

    -- Create waypoint.
    local wp = coord:WaypointGround( speed )

    -- Passing waypoint task function.
    local TaskPassingWP = group:TaskFunction( "AIRBOSS._PassingWaypoint", airboss, i, #airboss.waypoints )

    -- Call task function when carrier arrives at waypoint.
    group:SetTaskWaypoint( wp, TaskPassingWP )

    -- Add waypoints to table.
    table.insert( waypoints, wp )
  end

  -- Set turn into wind switch false.
  airboss.turnintowind = false
  airboss.detour = false

  -- Route group.
  group:Route( waypoints )
end

--- Function called when a group has reached the holding zone.
-- @param Wrapper.Group#GROUP group Group that reached the holding zone.
-- @param #AIRBOSS airboss Airboss object.
-- @param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone.
function AIRBOSS._ReachedHoldingZone( group, airboss, flight )

  -- Debug message.
  local text = string.format( "Flight %s reached holding zone.", group:GetName() )
  MESSAGE:New( text, 10 ):ToAllIf( airboss.Debug )
  airboss:T( airboss.lid .. text )

  -- Debug mark.
  if airboss.Debug then
    group:GetCoordinate():MarkToAll( text )
  end

  -- Set holding flag true and set timestamp for marshal time check.
  if flight then
    flight.holding = true
    flight.time = timer.getAbsTime()
  end
end

--- Function called when a group should be send to the Marshal stack. If stack is full, it is send to wait.
-- @param Wrapper.Group#GROUP group Group that reached the holding zone.
-- @param #AIRBOSS airboss Airboss object.
-- @param #AIRBOSS.FlightGroup flight Flight group that has reached the holding zone.
function AIRBOSS._TaskFunctionMarshalAI( group, airboss, flight )

  -- Debug message.
  local text = string.format( "Flight %s is send to marshal.", group:GetName() )
  MESSAGE:New( text, 10 ):ToAllIf( airboss.Debug )
  airboss:T( airboss.lid .. text )

  -- Get the next free stack for current recovery case.
  local stack = airboss:_GetFreeStack( flight.ai )

  if stack then

    -- Send AI to marshal stack.
    airboss:_MarshalAI( flight, stack )

  else

    -- Send AI to orbit outside 10 NM zone and wait until the next Marshal stack is available.
    if not airboss:_InQueue( airboss.Qwaiting, flight.group ) then
      airboss:_WaitAI( flight )
    end

  end

  -- If it came from refueling.
  if flight.refueling == true then
    airboss:I( airboss.lid .. string.format( "Flight group %s finished refueling task.", flight.groupname ) )
  end

  -- Not refueling any more in case it was.
  flight.refueling = false

end

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- MISC functions
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get aircraft nickname.
-- @param #AIRBOSS self
-- @param #string actype Aircraft type name.
-- @return #string Aircraft nickname. E.g. "Hornet" for the F/A-18C or "Tomcat" For the F-14A.
function AIRBOSS:_GetACNickname( actype )

  local nickname = "unknown"
  if actype == AIRBOSS.AircraftCarrier.A4EC then
    nickname = "Skyhawk"
  elseif actype == AIRBOSS.AircraftCarrier.T45C then
    nickname = "Goshawk"
  elseif actype == AIRBOSS.AircraftCarrier.AV8B then
    nickname = "Harrier"
  elseif actype == AIRBOSS.AircraftCarrier.E2D then
    nickname = "Hawkeye"
  elseif actype == AIRBOSS.AircraftCarrier.C2A then
    nickname = "Greyhound"
  elseif actype == AIRBOSS.AircraftCarrier.F14A_AI or actype == AIRBOSS.AircraftCarrier.F14A or actype == AIRBOSS.AircraftCarrier.F14B then
    nickname = "Tomcat"
  elseif actype == AIRBOSS.AircraftCarrier.FA18C or actype == AIRBOSS.AircraftCarrier.HORNET then
    nickname = "Hornet"
  elseif actype == AIRBOSS.AircraftCarrier.RHINOE or actype == AIRBOSS.AircraftCarrier.RHINOF then
    nickname = "Rhino"
  elseif actype == AIRBOSS.AircraftCarrier.GROWLER then
    nickname = "Growler"
  elseif actype == AIRBOSS.AircraftCarrier.S3B or actype == AIRBOSS.AircraftCarrier.S3BTANKER then
    nickname = "Viking"
  end

  return nickname
end

--- Get onboard number of player or client.
-- @param #AIRBOSS self
-- @param Wrapper.Group#GROUP group Aircraft group.
-- @return #string Onboard number as string.
function AIRBOSS:_GetOnboardNumberPlayer( group )
  return self:_GetOnboardNumbers( group, true )
end

--- Get onboard numbers of all units in a group.
-- @param #AIRBOSS self
-- @param Wrapper.Group#GROUP group Aircraft group.
-- @param #boolean playeronly If true, return the onboard number for player or client skill units.
-- @return #table Table of onboard numbers.
function AIRBOSS:_GetOnboardNumbers( group, playeronly )
  -- self:F({groupname=group:GetName})

  -- Get group name.
  local groupname = group:GetName()

  -- Debug text.
  local text = string.format( "Onboard numbers of group %s:", groupname )

  local template=group:GetTemplate()

  local numbers = {}
  if template then

    -- Units of template group.
    local units = template.units

    -- Get numbers.
    for _, unit in pairs( units ) do

      -- Onboard number and unit name.
      local n = tostring( unit.onboard_num )
      local name = unit.name
      local skill = unit.skill or "Unknown"

      -- Debug text.
      text = text .. string.format( "\n- unit %s: onboard #=%s  skill=%s", name, n, tostring( skill ) )

      if playeronly and skill == "Client" or skill == "Player" then
        -- There can be only one player in the group, so we skip everything else.
        return n
      end

      -- Table entry.
      numbers[name] = n
    end

    -- Debug info.
    self:T2( self.lid .. text )

  else

    if playeronly then
      return 101
    else

      local units=group:GetUnits()

      for i,_unit in pairs(units) do
        local name=_unit:GetName()

        numbers[name]=100+i

      end

    end

  end

  return numbers
end

--- Get Tower frequency of carrier.
-- @param #AIRBOSS self
function AIRBOSS:_GetTowerFrequency()

  -- Tower frequency in MHz
  self.TowerFreq = 0

  -- Get Template of Strike Group
  local striketemplate = self.carrier:GetGroup():GetTemplate()

  -- Find the carrier unit.
  for _, unit in pairs( striketemplate.units ) do
    if self.carrier:GetName() == unit.name then
      self.TowerFreq = unit.frequency / 1000000
      return
    end
  end
end

--- Get error margin depending on player skill.
--
-- * Flight students: 10% and 20%
-- * Naval Aviators: 5% and 10%
-- * TOPGUN Graduates: 2.5% and 5%
--
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @return #number Error margin for still being okay.
-- @return #number Error margin for really sucking.
function AIRBOSS:_GetGoodBadScore( playerData )

  local lowscore
  local badscore
  if playerData.difficulty == AIRBOSS.Difficulty.EASY then
    lowscore = 10
    badscore = 20
  elseif playerData.difficulty == AIRBOSS.Difficulty.NORMAL then
    lowscore = 5
    badscore = 10
  elseif playerData.difficulty == AIRBOSS.Difficulty.HARD then
    lowscore = 2.5
    badscore = 5
  end

  return lowscore, badscore
end

--- Check if aircraft is capable of landing on this aircraft carrier.
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit Aircraft unit. (Will also work with groups as given parameter.)
-- @return #boolean If true, aircraft can land on a carrier.
function AIRBOSS:_IsCarrierAircraft( unit )

  -- Get aircraft type name
  local aircrafttype = unit:GetTypeName()

  -- Special case for Harrier which can only land on Tarawa, LHA and LHD.
  if aircrafttype == AIRBOSS.AircraftCarrier.AV8B then
    if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then
      return true
    else
      return false
    end
  end

  -- Also only Harriers can land on the Tarawa, LHA and LHD.
  if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then
    if aircrafttype ~= AIRBOSS.AircraftCarrier.AV8B then
      return false
    end
  end

  -- Loop over all other known carrier capable aircraft.
  for _, actype in pairs( AIRBOSS.AircraftCarrier ) do

    -- Check if this is a carrier capable aircraft type.
    if actype == aircrafttype then
      return true
    end
  end

  -- No carrier carrier aircraft.
  return false
end

--- Checks if a human player sits in the unit.
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit Aircraft unit.
-- @return #boolean If true, human player inside the unit.
function AIRBOSS:_IsHumanUnit( unit )

  -- Get player unit or nil if no player unit.
  local playerunit = self:_GetPlayerUnitAndName( unit:GetName() )

  if playerunit then
    return true
  else
    return false
  end
end

--- Checks if a group has a human player.
-- @param #AIRBOSS self
-- @param Wrapper.Group#GROUP group Aircraft group.
-- @return #boolean If true, human player inside group.
function AIRBOSS:_IsHuman( group )

  -- Get all units of the group.
  local units = group:GetUnits()

  -- Loop over all units.
  for _, _unit in pairs( units ) do
    -- Check if unit is human.
    local human = self:_IsHumanUnit( _unit )
    if human then
      return true
    end
  end

  return false
end

--- Get fuel state in pounds.
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined.
-- @return #number Fuel state in pounds.
function AIRBOSS:_GetFuelState( unit )

  -- Get relative fuel [0,1].
  local fuel = unit:GetFuel()

  -- Get max weight of fuel in kg.
  local maxfuel = self:_GetUnitMasses( unit )

  -- Fuel state, i.e. what let's
  local fuelstate = fuel * maxfuel

  -- Debug info.
  self:T2( self.lid .. string.format( "Unit %s fuel state = %.1f kg = %.1f lbs", unit:GetName(), fuelstate, UTILS.kg2lbs( fuelstate ) ) )

  return UTILS.kg2lbs( fuelstate )
end

--- Convert altitude from meters to angels (thousands of feet).
-- @param #AIRBOSS self
-- @param alt altitude in meters.
-- @return #number Altitude in Anglels = thousands of feet using math.floor().
function AIRBOSS:_GetAngels( alt )

  if alt then
    local angels = UTILS.Round( UTILS.MetersToFeet( alt ) / 1000, 0 )
    return angels
  else
    return 0
  end

end

--- Get unit masses especially fuel from DCS descriptor values.
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit The unit for which the mass is determined.
-- @return #number Mass of fuel in kg.
-- @return #number Empty weight of unit in kg.
-- @return #number Max weight of unit in kg.
-- @return #number Max cargo weight in kg.
function AIRBOSS:_GetUnitMasses( unit )

  -- Get DCS descriptors table.
  local Desc = unit:GetDesc()

  -- Mass of fuel in kg.
  local massfuel = Desc.fuelMassMax or 0

  -- Mass of empty unit in km.
  local massempty = Desc.massEmpty or 0

  -- Max weight of unit in kg.
  local massmax = Desc.massMax or 0

  -- Rest is cargo.
  local masscargo = massmax - massfuel - massempty

  -- Debug info.
  self:T2( self.lid .. string.format( "Unit %s mass fuel=%.1f kg, empty=%.1f kg, max=%.1f kg, cargo=%.1f kg", unit:GetName(), massfuel, massempty, massmax, masscargo ) )

  return massfuel, massempty, massmax, masscargo
end

--- Get player data from unit object
-- @param #AIRBOSS self
-- @param Wrapper.Unit#UNIT unit Unit in question.
-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists.
function AIRBOSS:_GetPlayerDataUnit( unit )
  if unit:IsAlive() then
    local unitname = unit:GetName()
    local playerunit, playername = self:_GetPlayerUnitAndName( unitname )
    if playerunit and playername then
      return self.players[playername]
    end
  end
  return nil
end

--- Get player data from group object.
-- @param #AIRBOSS self
-- @param Wrapper.Group#GROUP group Group in question.
-- @return #AIRBOSS.PlayerData Player data or nil if not player with this name or unit exists.
function AIRBOSS:_GetPlayerDataGroup( group )
  local units = group:GetUnits()
  for _, unit in pairs( units ) do
    local playerdata = self:_GetPlayerDataUnit( unit )
    if playerdata then
      return playerdata
    end
  end
  return nil
end

--- Returns the unit of a player and the player name from the self.players table if it exists.
-- @param #AIRBOSS self
-- @param #string _unitName Name of the player unit.
-- @return Wrapper.Unit#UNIT Unit of player or nil.
-- @return #string Name of player or nil.
function AIRBOSS:_GetPlayerUnit( _unitName )

  for _, _player in pairs( self.players ) do

    local player = _player -- #AIRBOSS.PlayerData

    if player.unit and player.unit:GetName() == _unitName then
      self:T( self.lid .. string.format( "Found player=%s unit=%s in players table.", tostring( player.name ), tostring( _unitName ) ) )
      return player.unit, player.name
    end

  end

  return nil, nil
end

--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned.
-- @param #AIRBOSS self
-- @param #string _unitName Name of the player unit.
-- @return Wrapper.Unit#UNIT Unit of player or nil.
-- @return #string Name of the player or nil.
function AIRBOSS:_GetPlayerUnitAndName( _unitName )
  self:F2( _unitName )

  if _unitName ~= nil then

    -- First, let's look up all current players.
    local u, pn = self:_GetPlayerUnit( _unitName )

    -- Return
    if u and pn then
      return u, pn
    end

    -- Get DCS unit from its name.
    local DCSunit = Unit.getByName( _unitName )

    if DCSunit and DCSunit.getPlayerName then

      -- Get player name if any.
      local playername = DCSunit:getPlayerName()

      -- Unit object.
      local unit = UNIT:Find( DCSunit )

      -- Debug.
      self:T2( { DCSunit = DCSunit, unit = unit, playername = playername } )

      -- Check if enverything is there.
      if DCSunit and unit and playername then
        self:T( self.lid .. string.format( "Found DCS unit %s with player %s.", tostring( _unitName ), tostring( playername ) ) )
        return unit, playername
      end

    end

  end

  -- Return nil if we could not find a player.
  return nil, nil
end

--- Get carrier coalition.
-- @param #AIRBOSS self
-- @return #number Coalition side of carrier.
function AIRBOSS:GetCoalition()
  return self.carrier:GetCoalition()
end

--- Get carrier coordinate.
-- @param #AIRBOSS self
-- @return Core.Point#COORDINATE Carrier coordinate.
function AIRBOSS:GetCoordinate()
  return self.carrier:GetCoord()
end

--- Get carrier coordinate.
-- @param #AIRBOSS self
-- @return Core.Point#COORDINATE Carrier coordinate.
function AIRBOSS:GetCoord()
  return self.carrier:GetCoord()
end

--- Get static weather of this mission from env.mission.weather.
-- @param #AIRBOSS self
-- @param #table Clouds table which has entries "thickness", "density", "base", "iprecptns".
-- @param #number Visibility distance in meters.
-- @param #table Fog table, which has entries "thickness", "visibility" or nil if fog is disabled in the mission.
-- @param #number Dust density or nil if dust is disabled in the mission.
function AIRBOSS:_GetStaticWeather()

  -- Weather data from mission file.
  local weather = env.mission.weather

  -- Clouds
  --[[
  ["clouds"] =
  {
      ["thickness"] = 430,
      ["density"] = 7,
      ["base"] = 0,
      ["iprecptns"] = 1,
  }, -- end of ["clouds"]
  ]]
  local clouds = weather.clouds

  -- Visibilty distance in meters.
  local visibility = weather.visibility.distance

  -- Dust
  --[[
  ["enable_dust"] = false,
  ["dust_density"] = 0,
  ]]
  local dust = nil
  if weather.enable_dust == true then
    dust = weather.dust_density
  end

  -- Fog
  --[[
  ["enable_fog"] = false,
  ["fog"] =
  {
      ["thickness"] = 0,
      ["visibility"] = 25,
  }, -- end of ["fog"]
  ]]
  local fog = nil
  if weather.enable_fog == true then
    fog = weather.fog
  end

  return clouds, visibility, fog, dust
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- RADIO MESSAGE Functions
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function called by DCS timer. Unused.
-- @param #table param Parameters.
-- @param #number time Time.
function AIRBOSS._CheckRadioQueueT( param, time )
  AIRBOSS._CheckRadioQueue( param.airboss, param.radioqueue, param.name )
  return time + 0.05
end

--- Radio queue item.
-- @type AIRBOSS.Radioitem
-- @field #number Tplay Abs time when transmission should be played.
-- @field #number Tstarted Abs time when transmission began to play.
-- @field #boolean isplaying Currently playing.
-- @field #AIRBOSS.Radio radio Radio object.
-- @field #AIRBOSS.RadioCall call Radio call.
-- @field #boolean loud If true, play loud version of file.
-- @field #number interval Interval in seconds after the last sound was played.

--- Check radio queue for transmissions to be broadcasted.
-- @param #AIRBOSS self
-- @param #table radioqueue The radio queue.
-- @param #string name Name of the queue.
function AIRBOSS:_CheckRadioQueue( radioqueue, name )

  -- env.info(string.format("FF %s #radioqueue %d", name, #radioqueue))

  -- Check if queue is empty.
  if #radioqueue == 0 then

    if name == "LSO" then
      self:T( self.lid .. string.format( "Stopping LSO radio queue." ) )
      self.radiotimer:Stop( self.RQLid )
      self.RQLid = nil
    elseif name == "MARSHAL" then
      self:T( self.lid .. string.format( "Stopping Marshal radio queue." ) )
      self.radiotimer:Stop( self.RQMid )
      self.RQMid = nil
    end

    return
  end

  -- Get current abs time.
  local _time = timer.getAbsTime()

  local playing = false
  local next = nil -- #AIRBOSS.Radioitem
  local _remove = nil
  for i, _transmission in ipairs( radioqueue ) do
    local transmission = _transmission -- #AIRBOSS.Radioitem

    -- Check if transmission time has passed.
    if _time >= transmission.Tplay then

      -- Check if transmission is currently playing.
      if transmission.isplaying then

        -- Check if transmission is finished.
        if _time >= transmission.Tstarted + transmission.call.duration then

          -- Transmission over.
          transmission.isplaying = false
          _remove = i

          if transmission.radio.alias == "LSO" then
            self.TQLSO = _time
          elseif transmission.radio.alias == "MARSHAL" then
            self.TQMarshal = _time
          end

        else -- still playing

          -- Transmission is still playing.
          playing = true

        end

      else -- not playing yet

        local Tlast = nil
        if transmission.interval then
          if transmission.radio.alias == "LSO" then
            Tlast = self.TQLSO
          elseif transmission.radio.alias == "MARSHAL" then
            Tlast = self.TQMarshal
          end
        end

        if transmission.interval == nil then

          -- Not playing ==> this will be next.
          if next == nil then
            next = transmission
          end

        else

          if _time - Tlast >= transmission.interval then
            next = transmission
          else

          end
        end

        -- We got a transmission or one with an interval that is not due yet. No need for anything else.
        if next or Tlast then
          break
        end

      end

    else

      -- Transmission not due yet.

    end
  end

  -- Found a new transmission.
  if next ~= nil and not playing then
    self:Broadcast( next.radio, next.call, next.loud )
    next.isplaying = true
    next.Tstarted = _time
  end

  -- Remove completed calls from queue.
  if _remove then
    table.remove( radioqueue, _remove )
  end

  return
end

--- Add Radio transmission to radio queue.
-- @param #AIRBOSS self
-- @param #AIRBOSS.Radio radio Radio sending the transmission.
-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles.
-- @param #boolean loud If true, play loud sound file version.
-- @param #number delay Delay in seconds, before the message is broadcasted.
-- @param #number interval Interval in seconds after the last sound has been played.
-- @param #boolean click If true, play radio click at the end.
-- @param #boolean pilotcall If true, it's a pilot call.
function AIRBOSS:RadioTransmission( radio, call, loud, delay, interval, click, pilotcall )
  self:F2( { radio = radio, call = call, loud = loud, delay = delay, interval = interval, click = click } )

  -- Nil check.
  if radio == nil or call == nil then
    return
  end

  if not self.SRS then

    -- Create a new radio transmission item.
    local transmission = {} -- #AIRBOSS.Radioitem

    transmission.radio = radio
    transmission.call = call
    transmission.Tplay = timer.getAbsTime() + (delay or 0)
    transmission.interval = interval
    transmission.isplaying = false
    transmission.Tstarted = nil
    transmission.loud = loud and call.loud

    -- Player onboard number if sender has one.
    if self:_IsOnboard( call.modexsender ) then
      self:_Number2Radio( radio, call.modexsender, delay, 0.3, pilotcall )
    end

    -- Play onboard number if receiver has one.
    if self:_IsOnboard( call.modexreceiver ) then
      self:_Number2Radio( radio, call.modexreceiver, delay, 0.3, pilotcall )
    end

    -- Add transmission to the right queue.
    local caller = ""
    if radio.alias == "LSO" then

      table.insert( self.RQLSO, transmission )

      caller = "LSOCall"

      -- Schedule radio queue checks.
      if not self.RQLid then
        self:T( self.lid .. string.format( "Starting LSO radio queue." ) )
        self.RQLid = self.radiotimer:Schedule( nil, AIRBOSS._CheckRadioQueue, { self, self.RQLSO, "LSO" }, 0.02, 0.05 )
      end

    elseif radio.alias == "MARSHAL" then

      table.insert( self.RQMarshal, transmission )

      caller = "MarshalCall"

      if not self.RQMid then
        self:T( self.lid .. string.format( "Starting Marhal radio queue." ) )
        self.RQMid = self.radiotimer:Schedule( nil, AIRBOSS._CheckRadioQueue, { self, self.RQMarshal, "MARSHAL" }, 0.02, 0.05 )
      end

    end

    -- Append radio click sound at the end of the transmission.
    if click then
      self:RadioTransmission( radio, self[caller].CLICK, false, delay )
    end

  else

    -- SRS transmission
    if call.subtitle ~= nil and string.len(call.subtitle) > 1  then

      local frequency = self.MarshalRadio.frequency
      local modulation = self.MarshalRadio.modulation
      local voice = nil
      local gender = nil
      local culture = nil

      if radio.alias == "AIRBOSS" then
        frequency = self.AirbossRadio.frequency
        modulation = self.AirbossRadio.modulation
        voice = self.AirbossRadio.voice
        gender = self.AirbossRadio.gender
        culture = self.AirbossRadio.culture
      end

      if radio.alias == "MARSHAL" then
        voice = self.MarshalRadio.voice
        gender = self.MarshalRadio.gender
        culture = self.MarshalRadio.culture
      end

      if radio.alias == "LSO" then
        frequency = self.LSORadio.frequency
        modulation = self.LSORadio.modulation
        voice = self.LSORadio.voice
        gender = self.LSORadio.gender
        culture = self.LSORadio.culture
      end

      if pilotcall then
        voice = self.PilotRadio.voice
        gender = self.PilotRadio.gender
        culture = self.PilotRadio.culture
        radio.alias = "PILOT"
      end

      if not radio.alias then
        -- TODO - what freq to use here?
        frequency = self.AirbossRadio.frequency
        modulation = self.AirbossRadio.modulation
        radio.alias = "AIRBOSS"
      end

      local volume = nil

      if loud then
        volume = 1.0
      end

      --local text = tostring(call.modexreceiver).."; "..radio.alias.."; "..call.subtitle
      local text = call.subtitle
      self:T(self.lid..text)
      local srstext = self:_GetNiceSRSText(text)
      self.SRSQ:NewTransmission(srstext, call.duration, self.SRS, nil, 0.1, nil, call.subtitle, call.subduration, frequency, modulation, gender, culture, voice, volume, radio.alias)
    end
  end
end

--- Set SRS voice for the pilot calls.
-- @param #AIRBOSS self
-- @param #string Voice (Optional) SRS specific voice
-- @param #string Gender (Optional) SRS specific gender
-- @param #string Culture (Optional) SRS specific culture
-- @return #AIRBOSS self
function AIRBOSS:SetSRSPilotVoice( Voice, Gender, Culture )

  self.PilotRadio = {} -- #AIRBOSS.Radio
  self.PilotRadio.alias = "PILOT"
  self.PilotRadio.voice = Voice or MSRS.Voices.Microsoft.David
  self.PilotRadio.gender = Gender or "male"
  self.PilotRadio.culture = Culture or "en-US"

  if (not Voice) and self.SRS and self.SRS:GetProvider() == MSRS.Provider.GOOGLE then
    self.PilotRadio.voice = MSRS.Voices.Google.Standard.en_US_Standard_J
  end

  return self
end

--- Check if a call needs a subtitle because the complete voice overs are not available.
-- @param #AIRBOSS self
-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles.
-- @return #boolean If true, call needs a subtitle.
function AIRBOSS:_NeedsSubtitle( call )
  -- Currently we play the noise file.
  if call.file == self.MarshalCall.NOISE.file or call.file == self.LSOCall.NOISE.file then
    return true
  else
    return false
  end
end

--- Broadcast radio message.
-- @param #AIRBOSS self
-- @param #AIRBOSS.Radio radio Radio sending transmission.
-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles.
-- @param #boolean loud Play loud version of file.
function AIRBOSS:Broadcast( radio, call, loud )
  self:F( call )

  -- Check which sound output method to use.
  if not self.usersoundradio then

    ----------------------------
    -- Transmission via Radio --
    ----------------------------

    -- Get unit sending the transmission.
    local sender = self:_GetRadioSender( radio )

    -- Construct file name and subtitle.
    local filename = self:_RadioFilename( call, loud, radio.alias )

    -- Create subtitle for transmission.
    local subtitle = self:_RadioSubtitle( radio, call, loud )

    -- Debug.
    self:T( { filename = filename, subtitle = subtitle } )

    if sender then

      -- Broadcasting from aircraft. Only players tuned in to the right frequency will see the message.
      self:T( self.lid .. string.format( "Broadcasting from aircraft %s", sender:GetName() ) )

      -- Command to set the Frequency for the transmission.
      local commandFrequency = {
        id = "SetFrequency",
        params = {
          frequency = radio.frequency * 1000000, -- Frequency in Hz.
          modulation = radio.modulation,
        },
      }

      -- Command to tranmit the call.
      local commandTransmit = {
        id = "TransmitMessage",
        params = {
          file = filename,
          duration = call.subduration or 5,
          subtitle = subtitle,
          loop = false,
        },
      }

      -- Set commend for frequency
      sender:SetCommand( commandFrequency )

      -- Set command for radio transmission.
      sender:SetCommand( commandTransmit )

    else

      -- Broadcasting from carrier. No subtitle possible. Need to send messages to players.
      self:T( self.lid .. string.format( "Broadcasting from carrier via trigger.action.radioTransmission()." ) )

      -- Transmit from carrier position.
      local vec3 = self.carrier:GetPositionVec3()

      -- Transmit via trigger.
      trigger.action.radioTransmission( filename, vec3, radio.modulation, false, radio.frequency * 1000000, 100 )

      -- Display subtitle of message to players.
      for _, _player in pairs( self.players ) do
        local playerData = _player -- #AIRBOSS.PlayerData

        -- Message to all players in CCA that have subtites on.
        if playerData.unit:IsInZone( self.zoneCCA ) and playerData.actype ~= AIRBOSS.AircraftCarrier.A4EC then

          -- Only to players with subtitle on or if noise is played.
          if playerData.subtitles or self:_NeedsSubtitle( call ) then

            -- Messages to marshal to everyone. Messages on LSO radio only to those in the pattern.
            if radio.alias == "MARSHAL" or (radio.alias == "LSO" and self:_InQueue( self.Qpattern, playerData.group )) then

              -- Message to player.
              self:MessageToPlayer( playerData, subtitle, nil, "", call.subduration or 5 )

            end

          end

        end
      end
    end
  end

  ----------------
  -- Easy Comms --
  ----------------

  -- Workaround for the community A-4E-C as long as their radios are not functioning properly.
  for _, _player in pairs( self.players ) do
    local playerData = _player -- #AIRBOSS.PlayerData

    -- Easy comms if globally activated but definitly for all player in the community A-4E.
    if self.usersoundradio or playerData.actype == AIRBOSS.AircraftCarrier.A4EC then

      -- Messages to marshal to everyone. Messages on LSO radio only to those in the pattern.
      if radio.alias == "MARSHAL" or (radio.alias == "LSO" and self:_InQueue( self.Qpattern, playerData.group )) then

        -- User sound to players (inside CCA).
        self:Sound2Player( playerData, radio, call, loud )
      end

    end
  end

end

--- Player user sound to player if he is inside the CCA.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @param #AIRBOSS.Radio radio The radio used for transmission.
-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles.
-- @param #boolean loud If true, play loud sound file version.
-- @param #number delay Delay in seconds, before the message is broadcasted.
function AIRBOSS:Sound2Player( playerData, radio, call, loud, delay )

  -- Only to players inside the CCA.
  if playerData.unit:IsInZone( self.zoneCCA ) and call then

    -- Construct file name.
    local filename = self:_RadioFilename( call, loud, radio.alias )

    -- Get Subtitle
    local subtitle = self:_RadioSubtitle( radio, call, loud )

    -- Play sound file via usersound trigger.
    USERSOUND:New( filename ):ToGroup( playerData.group, delay )

    -- Only to players with subtitle on or if noise is played.
    if playerData.subtitles or self:_NeedsSubtitle( call ) then
      self:MessageToPlayer( playerData, subtitle, nil, "", call.subduration, false, delay )
    end

  end
end

--- Create radio subtitle from radio call.
-- @param #AIRBOSS self
-- @param #AIRBOSS.Radio radio The radio used for transmission.
-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles.
-- @param #boolean loud If true, append "!" else ".".
-- @return #string Subtitle to be displayed.
function AIRBOSS:_RadioSubtitle( radio, call, loud )

  -- No subtitle if call is nil, or subtitle is nil or subtitle is empty.
  if call == nil or call.subtitle == nil or call.subtitle == "" then
    return ""
  end

  -- Sender
  local sender = call.sender or radio.alias
  if call.modexsender then
    sender = call.modexsender
  end

  -- Modex of receiver.
  local receiver = call.modexreceiver or ""

  -- Init subtitle.
  local subtitle = string.format( "%s: %s", sender, call.subtitle )
  if receiver and receiver ~= "" then
    subtitle = string.format( "%s: %s, %s", sender, receiver, call.subtitle )
  end

  -- Last character of the string.
  local lastchar = string.sub( subtitle, -1 )

  -- Append ! or .
  if loud then
    if lastchar == "." or lastchar == "!" then
      subtitle = string.sub( subtitle, 1, -1 )
    end
    subtitle = subtitle .. "!"
  else
    if lastchar == "!" then
      -- This also okay.
    elseif lastchar == "." then
      -- Nothing to do.
    else
      subtitle = subtitle .. "."
    end
  end

  return subtitle
end

--- Get full file name for radio call.
-- @param #AIRBOSS self
-- @param #AIRBOSS.RadioCall call Radio sound files and subtitles.
-- @param #boolean loud Use loud version of file if available.
-- @param #string channel Radio channel alias "LSO" or "LSOCall", "MARSHAL" or "MarshalCall".
-- @return #string The file name of the radio sound.
function AIRBOSS:_RadioFilename( call, loud, channel )

  -- Construct file name and subtitle.
  local prefix = call.file or ""
  local suffix = call.suffix or "ogg"

  -- Path to sound files. Default is in the ME
  local path = self.soundfolder or "l10n/DEFAULT/"

  -- Check for special LSO and Marshal sound folders.
  if string.find( call.file, "LSO-" ) and channel and (channel == "LSO" or channel == "LSOCall") then
    path = self.soundfolderLSO or path
  end
  if string.find( call.file, "MARSHAL-" ) and channel and (channel == "MARSHAL" or channel == "MarshalCall") then
    path = self.soundfolderMSH or path
  end

  -- Loud version.
  if loud then
    prefix = prefix .. "_Loud"
  end

  -- File name inclusing path in miz file.
  local filename = string.format( "%s%s.%s", path, prefix, suffix )

  return filename
end

--- Format text into SRS friendly string
-- @param #AIRBOSS self
-- @param #string text
-- @return #string text
function AIRBOSS:_GetNiceSRSText(text)
  text = string.gsub(text,"================================\n","")
  text = string.gsub(text,"||","parallel")
  text = string.gsub(text,"==","perpendicular")
  text = string.gsub(text,"BRC","Base recovery")
  --text = string.gsub(text,"#","Number")
  text = string.gsub(text,"%((%a+)%)","Morse %1")
  text = string.gsub(text,"°C","° Celsius")
  text = string.gsub(text,"°"," degrees")
  text = string.gsub(text," FB "," Final bearing ")
  text = string.gsub(text," ops"," operations ")
  text = string.gsub(text," kts"," knots")
  text = string.gsub(text,"TACAN","Tackan")
  text = string.gsub(text,"ICLS","I.C.L.S.")
  text = string.gsub(text,"LSO","L.S.O.")
  text = string.gsub(text,"inHg","inches of Mercury")
  text = string.gsub(text,"QFE","Q.F.E.")
  text = string.gsub(text,"hPa","hecto pascal")
  text = string.gsub(text," NM"," nautical miles")
  text = string.gsub(text," ft"," feet")
  text = string.gsub(text,"A/C","aircraft")
  text = string.gsub(text,"(#[%a%d%p%s]+)\n","")
  text = string.gsub(text,"%.000"," dot zero")
  text = string.gsub(text,"00"," double zero")
  text = string.gsub(text," 0 "," zero " )
  text = string.gsub(text,"\n","; ")
  return text
end

--- Send text message to player client.
-- Message format will be "SENDER: RECCEIVER, MESSAGE".
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @param #string message The message to send.
-- @param #string sender The person who sends the message or nil.
-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver.
-- @param #number duration Display message duration. Default 10 seconds.
-- @param #boolean clear If true, clear screen from previous messages.
-- @param #number delay Delay in seconds, before the message is displayed.
function AIRBOSS:MessageToPlayer( playerData, message, sender, receiver, duration, clear, delay )
  self:T({sender,receiver,message})
  if playerData and message and message ~= "" then

    -- Default duration.
    duration = duration or self.Tmessage

    -- Format message.
    local text
    if receiver and receiver == "" then
      -- No (blank) receiver.
      text = string.format( "%s", message )
    else
      -- Default "receiver" is onboard number of player.
      receiver = receiver or playerData.onboard
      text = string.format( "%s, %s", receiver, message )
    end
    self:T( self.lid .. text )

    if delay and delay > 0 then
      -- Delayed call.
      -- SCHEDULER:New(nil, self.MessageToPlayer, {self, playerData, message, sender, receiver, duration, clear}, delay)
      self:ScheduleOnce( delay, self.MessageToPlayer, self, playerData, message, sender, receiver, duration, clear )
    else

      if not self.SRS then
        -- Wait until previous sound finished.
        local wait = 0

        -- Onboard number to get the attention.
        if receiver == playerData.onboard then

          -- Which voice over number to use.
          if sender and (sender == "LSO" or sender == "MARSHAL" or sender == "AIRBOSS") then

            -- User sound of board number.
            wait = wait + self:_Number2Sound( playerData, sender, receiver )

          end
        end

        -- Negative.
        if string.find( text:lower(), "negative" ) then
          local filename = self:_RadioFilename( self.MarshalCall.NEGATIVE, false, "MARSHAL" )
          USERSOUND:New( filename ):ToGroup( playerData.group, wait )
          wait = wait + self.MarshalCall.NEGATIVE.duration
        end

        -- Affirm.
        if string.find( text:lower(), "affirm" ) then
          local filename = self:_RadioFilename( self.MarshalCall.AFFIRMATIVE, false, "MARSHAL" )
          USERSOUND:New( filename ):ToGroup( playerData.group, wait )
          wait = wait + self.MarshalCall.AFFIRMATIVE.duration
        end

        -- Roger.
        if string.find( text:lower(), "roger" ) then
          local filename = self:_RadioFilename( self.MarshalCall.ROGER, false, "MARSHAL" )
          USERSOUND:New( filename ):ToGroup( playerData.group, wait )
          wait = wait + self.MarshalCall.ROGER.duration
        end

        -- Play click sound to end message.
        if wait > 0 then
          local filename = self:_RadioFilename( self.MarshalCall.CLICK )
          USERSOUND:New( filename ):ToGroup( playerData.group, wait )
        end
      else
        -- SRS transmission
        local frequency = self.MarshalRadio.frequency
        local modulation = self.MarshalRadio.modulation
        local voice = self.MarshalRadio.voice
        local gender = self.MarshalRadio.gender
        local culture = self.MarshalRadio.culture

        if not sender then sender = "AIRBOSS" end

        if string.find(sender,"AIRBOSS" ) then
          frequency = self.AirbossRadio.frequency
          modulation = self.AirbossRadio.modulation
          voice = self.AirbossRadio.voice
          gender = self.AirbossRadio.gender
          culture = self.AirbossRadio.culture
        end

        --if sender == "MARSHAL" then
          --voice = self.MarshalRadio.voice
          --gender = self.MarshalRadio.gender
          --culture = self.MarshalRadio.culture
        --end

        if sender == "LSO" then
          frequency = self.LSORadio.frequency
          modulation = self.LSORadio.modulation
          voice = self.LSORadio.voice
          gender = self.LSORadio.gender
          culture = self.LSORadio.culture
        --elseif not sender then
          -- TODO - what freq to use here?
          --frequency = self.AirbossRadio.frequency
          --modulation = self.AirbossRadio.modulation
          --sender = "AIRBOSS"
        end

        self:T(self.lid..text)
        self:T({sender,frequency,modulation,voice})
        local srstext = self:_GetNiceSRSText(text)
        self.SRSQ:NewTransmission(srstext,duration,self.SRS,nil,0.1,nil,nil,nil,frequency,modulation,gender,culture,voice,nil,sender)
      end
      -- Text message to player client.
      if playerData.client then
        MESSAGE:New( text, duration, sender, clear ):ToClient( playerData.client )
      end

    end

  end
end

--- Send text message to all players in the pattern queue.
-- Message format will be "SENDER: RECCEIVER, MESSAGE".
-- @param #AIRBOSS self
-- @param #string message The message to send.
-- @param #string sender The person who sends the message or nil.
-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver.
-- @param #number duration Display message duration. Default 10 seconds.
-- @param #boolean clear If true, clear screen from previous messages.
-- @param #number delay Delay in seconds, before the message is displayed.
function AIRBOSS:MessageToPattern( message, sender, receiver, duration, clear, delay )

  -- Create new (fake) radio call to show the subtitile.
  local call = self:_NewRadioCall( self.LSOCall.NOISE, sender or "LSO", message, duration, receiver, sender )

  -- Dummy radio transmission to display subtitle only to those who tuned in.
  self:RadioTransmission( self.LSORadio, call, false, delay, nil, true )

end

--- Send text message to all players in the marshal queue.
-- Message format will be "SENDER: RECCEIVER, MESSAGE".
-- @param #AIRBOSS self
-- @param #string message The message to send.
-- @param #string sender The person who sends the message or nil.
-- @param #string receiver The person who receives the message. Default player's onboard number. Set to "" for no receiver.
-- @param #number duration Display message duration. Default 10 seconds.
-- @param #boolean clear If true, clear screen from previous messages.
-- @param #number delay Delay in seconds, before the message is displayed.
function AIRBOSS:MessageToMarshal( message, sender, receiver, duration, clear, delay )

  -- Create new (fake) radio call to show the subtitile.
  local call = self:_NewRadioCall( self.MarshalCall.NOISE, sender or "MARSHAL", message, duration, receiver, sender )

  -- Dummy radio transmission to display subtitle only to those who tuned in.
  self:RadioTransmission( self.MarshalRadio, call, false, delay, nil, true )

end

--- Generate a new radio call (deepcopy) from an existing default call.
-- @param #AIRBOSS self
-- @param #AIRBOSS.RadioCall call Radio call to be enhanced.
-- @param #string sender Sender of the message. Default is the radio alias.
-- @param #string subtitle Subtitle of the message. Default from original radio call. Use "" for no subtitle.
-- @param #number subduration Time in seconds the subtitle is displayed. Default 10 seconds.
-- @param #string modexreceiver Onboard number of the receiver or nil.
-- @param #string modexsender Onboard number of the sender or nil.
function AIRBOSS:_NewRadioCall( call, sender, subtitle, subduration, modexreceiver, modexsender )

  -- Create a new call
  local newcall = UTILS.DeepCopy( call ) -- #AIRBOSS.RadioCall

  -- Sender for displaying the subtitle.
  newcall.sender = sender

  -- Subtitle of the message.
  newcall.subtitle = subtitle or call.subtitle

  -- Duration of subtitle display.
  newcall.subduration = subduration or self.Tmessage

  -- Tail number of the receiver.
  if self:_IsOnboard( modexreceiver ) then
    newcall.modexreceiver = modexreceiver
  end

  -- Tail number of the sender.
  if self:_IsOnboard( modexsender ) then
    newcall.modexsender = modexsender
  end

  return newcall
end

--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work.
-- @param #AIRBOSS self
-- @param #AIRBOSS.Radio radio Airboss radio data.
-- @return Wrapper.Unit#UNIT Sending aircraft unit or nil if was not setup, is not an aircraft or is not alive.
function AIRBOSS:_GetRadioSender( radio )

  -- Check if we have a sending aircraft.
  local sender = nil -- Wrapper.Unit#UNIT

  -- Try the general default.
  if self.senderac then
    sender = UNIT:FindByName( self.senderac )
  end

  -- Try the specific marshal unit.
  if radio.alias == "MARSHAL" then
    if self.radiorelayMSH then
      sender = UNIT:FindByName( self.radiorelayMSH )
    end
  end

  -- Try the specific LSO unit.
  if radio.alias == "LSO" then
    if self.radiorelayLSO then
      sender = UNIT:FindByName( self.radiorelayLSO )
    end
  end

  -- Check that sender is alive and an aircraft.
  if sender and sender:IsAlive() and sender:IsAir() then
    return sender
  end

  return nil
end

--- Check if text is an onboard number of a flight.
-- @param #AIRBOSS self
-- @param #string text Text to check.
-- @return #boolean If true, text is an onboard number of a flight.
function AIRBOSS:_IsOnboard( text )

  -- Nil check.
  if text == nil then
    return false
  end

  -- Message to all.
  if text == "99" then
    return true
  end

  -- Loop over all flights.
  for _, _flight in pairs( self.flights ) do
    local flight = _flight -- #AIRBOSS.FlightGroup

    -- Loop over all onboard number of that flight.
    for _, onboard in pairs( flight.onboardnumbers ) do
      if text == onboard then
        return true
      end
    end

  end

  return false
end

--- Convert a number (as string) into an outsound and play it to a player group. E.g. for board number or headings.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data.
-- @param #string sender Who is sending the call, either "LSO" or "MARSHAL".
-- @param #string number Number string, e.g. "032" or "183".
-- @param #number delay Delay before transmission in seconds.
-- @return #number Duration of the call in seconds.
function AIRBOSS:_Number2Sound( playerData, sender, number, delay )

  -- Default.
  delay = delay or 0

  --- Split string into characters.
  local function _split( str )
    local chars = {}
    for i = 1, #str do
      local c = str:sub( i, i )
      table.insert( chars, c )
    end
    return chars
  end

  -- Sender
  local Sender
  if sender == "LSO" then
    Sender = "LSOCall"
  elseif sender == "MARSHAL" or sender == "AIRBOSS" then
    Sender = "MarshalCall"
  else
    self:E( self.lid .. string.format( "ERROR: Unknown radio sender %s!", tostring( sender ) ) )
    return
  end

  -- Split string into characters.
  local numbers = _split( tostring(number) )

  local wait = 0
  for i = 1, #numbers do

    -- Current number
    local n = numbers[i]

    -- Convert to N0, N1, ...
    local N = string.format( "N%s", n )

    -- Radio call.
    local call = self[Sender][N] -- #AIRBOSS.RadioCall

    -- Create file name.
    local filename = self:_RadioFilename( call, false, Sender )

    -- Play sound.
    USERSOUND:New( filename ):ToGroup( playerData.group, delay + wait )

    -- Wait until this call is over before playing the next.
    wait = wait + call.duration
  end

  return wait
end

--- Convert a number (as string) into a radio message.
-- E.g. for board number or headings.
-- @param #AIRBOSS self
-- @param #AIRBOSS.Radio radio Radio used for transmission.
-- @param #string number Number string, e.g. "032" or "183".
-- @param #number delay Delay before transmission in seconds.
-- @param #number interval Interval between the next call.
-- @param #boolean pilotcall If true, use pilot sound files.
-- @return #number Duration of the call in seconds.
function AIRBOSS:_Number2Radio( radio, number, delay, interval, pilotcall )

  --- Split string into characters.
  local function _split( str )
    local chars = {}
    for i = 1, #str do
      local c = str:sub( i, i )
      table.insert( chars, c )
    end
    return chars
  end

  -- Sender.
  local Sender = ""
  if radio.alias == "LSO" then
    Sender = "LSOCall"
  elseif radio.alias == "MARSHAL" then
    Sender = "MarshalCall"
  else
    self:E( self.lid .. string.format( "ERROR: Unknown radio alias %s!", tostring( radio.alias ) ) )
  end

  if pilotcall then
    Sender = "PilotCall"
  end

  if Sender=="" then
    self:E( self.lid .. string.format( "ERROR: Sender unknown!") )
    return
  end

  -- Split string into characters.
  local numbers = _split( tostring(number) )

  local wait = 0
  for i = 1, #numbers do

    -- Current number
    local n = numbers[i]

    -- Convert to N0, N1, ...
    local N = string.format( "N%s", n )

    -- Radio call.
    local call = self[Sender][N] -- #AIRBOSS.RadioCall

    if interval and i == 1 then
      -- Transmit.
      self:RadioTransmission( radio, call, false, delay, interval )
    else
      self:RadioTransmission( radio, call, false, delay )
    end

    -- Add up duration of the number.
    wait = wait + call.duration
  end

  -- Return the total duration of the call.
  return wait
end

--- Aircraft request marshal (Inbound call both for players and AI).
-- @param #AIRBOSS self
-- @return Wrapper.Unit#UNIT Unit of player or nil.
-- @param #string modex Tail number.
function AIRBOSS:_MarshallInboundCall(unit, modex)

  -- Calculate
  local vectorCarrier = self:GetCoordinate():GetDirectionVec3(unit:GetCoordinate())
  local bearing =  UTILS.Round(unit:GetCoordinate():GetAngleDegrees( vectorCarrier ), 0)
  local distance = UTILS.Round(UTILS.MetersToNM(unit:GetCoordinate():Get2DDistance(self:GetCoordinate())),0)
  local angels = UTILS.Round(UTILS.MetersToFeet(unit:GetHeight()/1000),0)
  local state = UTILS.Round(self:_GetFuelState(unit)/1000,1)

  -- Pilot: "Marshall, [modex], marking mom's [bearing] for [distance], angels [XX], state [X.X]"
  local text=string.format("Marshal, %s, marking mom's %d for %d, angels %d, state %.1f", modex, bearing, distance, angels, state)
  -- Debug message.
  self:T(self.lid..text)

  -- Fuel state.
  local FS=UTILS.Split(string.format("%.1f", state), ".")

  -- Create new call to display complete subtitle.
  local inboundcall=self:_NewRadioCall(self.MarshalCall.CLICK,  unit.UnitName:upper() , text, self.Tmessage, nil, unit.UnitName:upper())

  -- CLICK!
  self:RadioTransmission(self.MarshalRadio, inboundcall)
  -- Marshal ..
  self:RadioTransmission(self.MarshalRadio, self.PilotCall.MARSHAL, nil, nil, nil, nil, true)
  -- Modex..
  self:_Number2Radio(self.MarshalRadio, modex, nil, nil, true)
  -- Marking Mom's,
  self:RadioTransmission(self.MarshalRadio, self.PilotCall.MARKINGMOMS, nil, nil, nil, nil, true)
  -- Bearing ..
  self:_Number2Radio(self.MarshalRadio, tostring(bearing), nil, nil, true)
  -- For ..
  self:RadioTransmission(self.MarshalRadio, self.PilotCall.FOR, nil, nil, nil, nil, true)
  -- Distance ..
  self:_Number2Radio(self.MarshalRadio, tostring(distance), nil, nil, true)
  -- Angels ..
  self:RadioTransmission(self.MarshalRadio, self.PilotCall.ANGELS, nil, nil, nil, nil, true)
  -- Angels Number ..
  self:_Number2Radio(self.MarshalRadio, tostring(angels), nil, nil, true)
  -- State ..
  self:RadioTransmission(self.MarshalRadio, self.PilotCall.STATE, nil, nil, nil, nil, true)
  -- X..
  self:_Number2Radio(self.MarshalRadio, FS[1], nil, nil, true)
  -- Point..
  self:RadioTransmission(self.MarshalRadio, self.PilotCall.POINT, nil, nil, nil, nil, true)
  -- Y.
  self:_Number2Radio(self.MarshalRadio, FS[2], nil, nil, true)
  -- CLICK!
  self:RadioTransmission(self.MarshalRadio, self.MarshalRadio.CLICK, nil, nil, nil, nil, true)

end

--- Aircraft commencing call (both for players and AI).
-- @param #AIRBOSS self
-- @return Wrapper.Unit#UNIT Unit of player or nil.
-- @param #string modex Tail number.
function AIRBOSS:_CommencingCall(unit, modex)

  -- Pilot: "[modex], commencing"
  local text=string.format("%s, commencing", modex)
  -- Debug message.
  self:T(self.lid..text)

  -- Create new call to display complete subtitle.
  local commencingCall=self:_NewRadioCall(self.MarshalCall.CLICK,  unit.UnitName:upper() , text, self.Tmessage, nil, unit.UnitName:upper())

  -- Click
  self:RadioTransmission(self.MarshalRadio, commencingCall)
  -- Modex..
  self:_Number2Radio(self.MarshalRadio, modex, nil, nil, true)
  -- Commencing
  self:RadioTransmission(self.MarshalRadio, self.PilotCall.COMMENCING, nil, nil, nil, nil, true)
  -- CLICK!
  self:RadioTransmission(self.MarshalRadio, self.MarshalRadio.CLICK, nil, nil, nil, nil, true)

end

--- AI aircraft calls the ball.
-- @param #AIRBOSS self
-- @param #string modex Tail number.
-- @param #string nickname Aircraft nickname.
-- @param #number fuelstate Aircraft fuel state in thouthands of pounds.
function AIRBOSS:_LSOCallAircraftBall( modex, nickname, fuelstate )

  -- Pilot: "405, Hornet Ball, 3.2"
  local text = string.format( "%s Ball, %.1f.", nickname, fuelstate )

  -- Debug message.
  self:T( self.lid .. text )

  -- Nickname UPPERCASE.
  local NICKNAME = nickname:upper()

  -- Fuel state.
  local FS = UTILS.Split( string.format( "%.1f", fuelstate ), "." )

  -- Create new call to display complete subtitle.
  local call = self:_NewRadioCall( self.PilotCall[NICKNAME], modex, text, self.Tmessage, nil, modex )

  -- Hornet ..
  self:RadioTransmission( self.LSORadio, call, nil, nil, nil, nil, true )
  -- Ball,
  self:RadioTransmission( self.LSORadio, self.PilotCall.BALL, nil, nil, nil, nil, true )
  -- X..
  self:_Number2Radio( self.LSORadio, FS[1], nil, nil, true )
  -- Point..
  self:RadioTransmission( self.LSORadio, self.PilotCall.POINT, nil, nil, nil, nil, true )
  -- Y.
  self:_Number2Radio( self.LSORadio, FS[2], nil, nil, true )

  -- CLICK!
  self:RadioTransmission( self.LSORadio, self.LSOCall.CLICK )

end

--- AI is bingo and goes to the recovery tanker.
-- @param #AIRBOSS self
-- @param #string modex Tail number.
function AIRBOSS:_MarshalCallGasAtTanker( modex )

  -- Subtitle.
  local text = string.format( "Bingo fuel! Going for gas at the recovery tanker." )

  -- Debug message.
  self:T( self.lid .. text )


  -- Create new call to display complete subtitle.
  local call = self:_NewRadioCall( self.PilotCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex )

  -- MODEX, bingo fuel!
  self:RadioTransmission( self.MarshalRadio, call, nil, nil, nil, nil, true )

  -- Going for fuel at the recovery tanker. Click!
  self:RadioTransmission( self.MarshalRadio, self.PilotCall.GASATTANKER, nil, nil, nil, true, true )

end

--- AI is bingo and goes to the divert field.
-- @param #AIRBOSS self
-- @param #string modex Tail number.
-- @param #string divertname Name of the divert field.
function AIRBOSS:_MarshalCallGasAtDivert( modex, divertname )

  -- Subtitle.
  local text = string.format( "Bingo fuel! Going for gas at divert field %s.", divertname )

  -- Debug message.
  self:T( self.lid .. text )

  -- Create new call to display complete subtitle.
  local call = self:_NewRadioCall( self.PilotCall.BINGOFUEL, modex, text, self.Tmessage, nil, modex )

  -- MODEX, bingo fuel!
  self:RadioTransmission( self.MarshalRadio, call, nil, nil, nil, nil, true )

  -- Going for fuel at the divert field. Click!
  self:RadioTransmission( self.MarshalRadio, self.PilotCall.GASATDIVERT, nil, nil, nil, true, true )

end

--- Inform everyone that recovery ops are stopped and deck is closed.
-- @param #AIRBOSS self
-- @param #number case Recovery case.
function AIRBOSS:_MarshalCallRecoveryStopped( case )

  -- Subtitle.
  local text = string.format( "Case %d recovery ops are stopped. Deck is closed.", case )

  -- Debug message.
  self:T( self.lid .. text )

  -- Create new call to display complete subtitle.
  local call = self:_NewRadioCall( self.MarshalCall.CASE, "AIRBOSS", text, self.Tmessage, "99" )

  -- 99, Case..
  self:RadioTransmission( self.MarshalRadio, call )
  -- X.
  self:_Number2Radio( self.MarshalRadio, tostring( case ) )
  -- recovery ops are stopped.
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.RECOVERYOPSSTOPPED, nil, nil, 0.2 )
  -- Deck is closed. Click!
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.DECKCLOSED, nil, nil, nil, true )

end

--- Inform everyone that recovery is paused and will resume at a certain time.
-- @param #AIRBOSS self
function AIRBOSS:_MarshalCallRecoveryPausedUntilFurtherNotice()

  -- Create new call. Subtitle already set.
  local call = self:_NewRadioCall( self.MarshalCall.RECOVERYPAUSEDNOTICE, "AIRBOSS", nil, self.Tmessage, "99" )

  -- 99, aircraft recovery is paused until further notice.
  self:RadioTransmission( self.MarshalRadio, call, nil, nil, nil, true )

end

--- Inform everyone that recovery is paused and will resume at a certain time.
-- @param #AIRBOSS self
-- @param #string clock Time.
function AIRBOSS:_MarshalCallRecoveryPausedResumedAt( clock )

  -- Get relevant part of clock.
  local _clock = UTILS.Split( clock, "+" )
  local CT = UTILS.Split( _clock[1], ":" )

  -- Subtitle.
  local text = string.format( "aircraft recovery is paused and will be resumed at %s.", clock )

  -- Debug message.
  self:T( self.lid .. text )

  -- Create new call with full subtitle.
  local call = self:_NewRadioCall( self.MarshalCall.RECOVERYPAUSEDRESUMED, "AIRBOSS", text, self.Tmessage, "99" )

  -- 99, aircraft recovery is paused and will resume at...
  self:RadioTransmission( self.MarshalRadio, call )

  -- XY.. (hours)
  self:_Number2Radio( self.MarshalRadio, CT[1] )
  -- XY (minutes)..
  self:_Number2Radio( self.MarshalRadio, CT[2] )
  -- hours. Click!
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.HOURS, nil, nil, nil, true )

end

--- Inform flight that he is cleared for recovery.
-- @param #AIRBOSS self
-- @param #string modex Tail number.
-- @param #number case Recovery case.
function AIRBOSS:_MarshalCallClearedForRecovery( modex, case )

  -- Subtitle.
  local text = string.format( "you're cleared for Case %d recovery.", case )

  -- Debug message.
  self:T( self.lid .. text )

  -- Create new call with full subtitle.
  local call = self:_NewRadioCall( self.MarshalCall.CLEAREDFORRECOVERY, "MARSHAL", text, self.Tmessage, modex )

  -- Two second delay.
  local delay = 2

  -- XYZ, you're cleared for case..
  self:RadioTransmission( self.MarshalRadio, call, nil, delay )
  -- X..
  self:_Number2Radio( self.MarshalRadio, tostring( case ), delay )
  -- recovery. Click!
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.RECOVERY, nil, delay, nil, true )

end

--- Inform everyone that recovery is resumed after pause.
-- @param #AIRBOSS self
function AIRBOSS:_MarshalCallResumeRecovery()

  -- Create new call with full subtitle.
  local call = self:_NewRadioCall( self.MarshalCall.RESUMERECOVERY, "AIRBOSS", nil, self.Tmessage, "99" )

  -- 99, aircraft recovery resumed. Click!
  self:RadioTransmission( self.MarshalRadio, call, nil, nil, nil, true )

end

--- Inform everyone about new final bearing.
-- @param #AIRBOSS self
-- @param #number FB Final Bearing in degrees.
function AIRBOSS:_MarshalCallNewFinalBearing( FB )

  -- Subtitle.
  local text = string.format( "new final bearing %03d°.", FB )

  -- Debug message.
  self:T( self.lid .. text )

  -- Create new call with full subtitle.
  local call = self:_NewRadioCall( self.MarshalCall.NEWFB, "AIRBOSS", text, self.Tmessage, "99" )

  -- 99, new final bearing..
  self:RadioTransmission( self.MarshalRadio, call )
  -- XYZ..
  self:_Number2Radio( self.MarshalRadio, string.format( "%03d", FB ), nil, 0.2 )
  -- Degrees. Click!
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true )

end

--- Compile a radio call when Marshal tells a flight the holding altitude.
-- @param #AIRBOSS self
-- @param #number hdg Heading in degrees.
function AIRBOSS:_MarshalCallCarrierTurnTo( hdg )

  -- Subtitle.
  local text = string.format( "carrier is now starting turn to heading %03d°.", hdg )

  -- Debug message.
  self:T( self.lid .. text )

  -- Create new call with full subtitle.
  local call = self:_NewRadioCall( self.MarshalCall.CARRIERTURNTOHEADING, "AIRBOSS", text, self.Tmessage, "99" )

  -- 99, turning to heading...
  self:RadioTransmission( self.MarshalRadio, call )
  -- XYZ..
  self:_Number2Radio( self.MarshalRadio, string.format( "%03d", hdg ), nil, 0.2 )
  -- Degrees. Click!
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true )

end

--- Compile a radio call when Marshal tells a flight the holding altitude.
-- @param #AIRBOSS self
-- @param #string modex Tail number.
-- @param #number nwaiting Number of flights already waiting.
function AIRBOSS:_MarshalCallStackFull( modex, nwaiting )

  -- Subtitle.
  local text = string.format( "Marshal stack is currently full. Hold outside 10 NM zone and wait for further instructions. " )
  if nwaiting == 1 then
    text = text .. string.format( "There is one flight ahead of you." )
  elseif nwaiting > 1 then
    text = text .. string.format( "There are %d flights ahead of you.", nwaiting )
  else
    text = text .. string.format( "You are next in line." )
  end

  -- Debug message.
  self:T( self.lid .. text )

  -- Create new call with full subtitle.
  local call = self:_NewRadioCall( self.MarshalCall.STACKFULL, "AIRBOSS", text, self.Tmessage, modex )

  -- XYZ, Marshal stack is currently full.
  self:RadioTransmission( self.MarshalRadio, call, nil, nil, nil, true )
end

--- Compile a radio call when Marshal tells a flight the holding altitude.
-- @param #AIRBOSS self
function AIRBOSS:_MarshalCallRecoveryStart( case )

  -- Marshal radial.
  local radial = self:GetRadial( case, true, true, false )

  -- Debug output.
  local text = string.format( "Starting aircraft recovery Case %d ops.", case )
  if case == 1 then
    text = text .. string.format( " BRC %03d°.", self:GetBRC() )
  elseif case == 2 then
    text = text .. string.format( " Marshal radial %03d°. BRC %03d°.", radial, self:GetBRC() )
  elseif case == 3 then
    text = text .. string.format( " Marshal radial %03d°. Final heading %03d°.", radial, self:GetFinalBearing( false ) )
  end
  self:T( self.lid .. text )

  -- New call including the subtitle.
  local call = self:_NewRadioCall( self.MarshalCall.STARTINGRECOVERY, "AIRBOSS", text, self.Tmessage, "99" )

  -- 99, Starting aircraft recovery case..
  self:RadioTransmission( self.MarshalRadio, call )
  -- X..
  self:_Number2Radio( self.MarshalRadio, tostring( case ), nil, 0.1 )
  -- ops.
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.OPS )

  -- Marshal Radial
  if case > 1 then
    -- Marshal radial..
    self:RadioTransmission( self.MarshalRadio, self.MarshalCall.MARSHALRADIAL )
    -- XYZ..
    self:_Number2Radio( self.MarshalRadio, string.format( "%03d", radial ), nil, 0.2 )
    -- Degrees.
    self:RadioTransmission( self.MarshalRadio, self.MarshalCall.DEGREES, nil, nil, nil, true )
  end

end

--- Compile a radio call when Marshal tells a flight the holding altitude.
-- @param #AIRBOSS self
-- @param #string modex Tail number.
-- @param #number case Recovery case.
-- @param #number brc Base recovery course.
-- @param #number altitude Holding altitude.
-- @param #string charlie Charlie Time estimate.
-- @param #number qfe Alitmeter inHg.
function AIRBOSS:_MarshalCallArrived( modex, case, brc, altitude, charlie, qfe )
  self:F( { modex = modex, case = case, brc = brc, altitude = altitude, charlie = charlie, qfe = qfe } )

  -- Split strings etc.
  local angels = self:_GetAngels( altitude )
  -- local QFE=UTILS.Split(tostring(UTILS.Round(qfe,2)), ".")
  local QFE = UTILS.Split( string.format( "%.2f", qfe ), "." )
  local clock = UTILS.Split( charlie, "+" )
  local CT = UTILS.Split( clock[1], ":" )

  -- Subtitle text.
  local text = string.format( "Case %d, expected BRC %03d°, hold at angels %d. Expected Charlie Time %s. Altimeter %.2f. Report see me.", case, brc, angels, charlie, qfe )

  -- Debug message.
  self:T( self.lid .. text )

  -- Create new call to display complete subtitle.
  local casecall = self:_NewRadioCall( self.MarshalCall.CASE, "MARSHAL", text, self.Tmessage, modex )

  -- Case..
  self:RadioTransmission( self.MarshalRadio, casecall )
  -- X.
  self:_Number2Radio( self.MarshalRadio, tostring( case ) )

  -- Expected..
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5 )
  -- BRC..
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.BRC )
  -- XYZ...
  self:_Number2Radio( self.MarshalRadio, string.format( "%03d", brc ) )
  -- Degrees.
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.DEGREES )

  -- Hold at..
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.HOLDATANGELS, nil, nil, 0.5 )
  -- X.
  self:_Number2Radio( self.MarshalRadio, tostring( angels ) )

  -- Expected..
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.EXPECTED, nil, nil, 0.5 )
  -- Charlie time..
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.CHARLIETIME )
  -- XY.. (hours)
  self:_Number2Radio( self.MarshalRadio, CT[1] )
  -- XY (minutes).
  self:_Number2Radio( self.MarshalRadio, CT[2] )
  -- hours.
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.HOURS )

  -- Altimeter..
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.ALTIMETER, nil, nil, 0.5 )
  -- XY..
  self:_Number2Radio( self.MarshalRadio, QFE[1] )
  -- Point..
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.POINT )
  -- XY.
  self:_Number2Radio( self.MarshalRadio, QFE[2] )

  -- Report see me. Click!
  self:RadioTransmission( self.MarshalRadio, self.MarshalCall.REPORTSEEME, nil, nil, 0.5, true )

end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- RADIO MENU Functions
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Add menu commands for player.
-- @param #AIRBOSS self
-- @param #string _unitName Name of player unit.
function AIRBOSS:_AddF10Commands( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check for player unit.
  if _unit and playername then

    -- Get group and ID.
    local group = _unit:GetGroup()
    local gid = group:GetID()

    if group and gid then

      if not self.menuadded[gid] then

        -- Enable switch so we don't do this twice.
        self.menuadded[gid] = true

        -- Set menu root path.
        local _rootPath = nil
        if AIRBOSS.MenuF10Root then
          ------------------------
          -- MISSON LEVEL MENUE --
          ------------------------

          if self.menusingle then
            -- F10/Airboss/...
            _rootPath = AIRBOSS.MenuF10Root
          else
            -- F10/Airboss/<Carrier Alias>/...
            _rootPath = missionCommands.addSubMenuForGroup( gid, self.alias, AIRBOSS.MenuF10Root )
          end

        else
          ------------------------
          -- GROUP LEVEL MENUES --
          ------------------------

          -- Main F10 menu: F10/Airboss/
          if AIRBOSS.MenuF10[gid] == nil then
            AIRBOSS.MenuF10[gid] = missionCommands.addSubMenuForGroup( gid, "Airboss" )
          end

          if self.menusingle then
            -- F10/Airboss/...
            _rootPath = AIRBOSS.MenuF10[gid]
          else
            -- F10/Airboss/<Carrier Alias>/...
            _rootPath = missionCommands.addSubMenuForGroup( gid, self.alias, AIRBOSS.MenuF10[gid] )
          end

        end

        --------------------------------
        -- F10/Airboss/<Carrier>/F1 Help
        --------------------------------
        local _helpPath = missionCommands.addSubMenuForGroup( gid, "Help", _rootPath )
        -- F10/Airboss/<Carrier>/F1 Help/F1 Mark Zones
        if self.menumarkzones then
          local _markPath = missionCommands.addSubMenuForGroup( gid, "Mark Zones", _helpPath )
          -- F10/Airboss/<Carrier>/F1 Help/F1 Mark Zones/
          if self.menusmokezones then
            missionCommands.addCommandForGroup( gid, "Smoke Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, false ) -- F1
          end
          missionCommands.addCommandForGroup( gid, "Flare Pattern Zones", _markPath, self._MarkCaseZones, self, _unitName, true ) -- F2
          if self.menusmokezones then
            missionCommands.addCommandForGroup( gid, "Smoke Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, false ) -- F3
          end
          missionCommands.addCommandForGroup( gid, "Flare Marshal Zone", _markPath, self._MarkMarshalZone, self, _unitName, true ) -- F4
        end
        -- F10/Airboss/<Carrier>/F1 Help/F2 Skill Level
        local _skillPath = missionCommands.addSubMenuForGroup( gid, "Skill Level", _helpPath )
        -- F10/Airboss/<Carrier>/F1 Help/F2 Skill Level/
        missionCommands.addCommandForGroup( gid, "Flight Student", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.EASY ) -- F1
        missionCommands.addCommandForGroup( gid, "Naval Aviator", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.NORMAL ) -- F2
        missionCommands.addCommandForGroup( gid, "TOPGUN Graduate", _skillPath, self._SetDifficulty, self, _unitName, AIRBOSS.Difficulty.HARD ) -- F3
        missionCommands.addCommandForGroup( gid, "Hints On/Off", _skillPath, self._SetHintsOnOff, self, _unitName ) -- F4
        -- F10/Airboss/<Carrier>/F1 Help/
        missionCommands.addCommandForGroup( gid, "My Status", _helpPath, self._DisplayPlayerStatus, self, _unitName ) -- F3
        missionCommands.addCommandForGroup( gid, "Attitude Monitor", _helpPath, self._DisplayAttitude, self, _unitName ) -- F4
        missionCommands.addCommandForGroup( gid, "Radio Check LSO", _helpPath, self._LSORadioCheck, self, _unitName ) -- F5
        missionCommands.addCommandForGroup( gid, "Radio Check Marshal", _helpPath, self._MarshalRadioCheck, self, _unitName ) -- F6
        missionCommands.addCommandForGroup( gid, "Subtitles On/Off", _helpPath, self._SubtitlesOnOff, self, _unitName ) -- F7
        missionCommands.addCommandForGroup( gid, "Trapsheet On/Off", _helpPath, self._TrapsheetOnOff, self, _unitName ) -- F8

        -------------------------------------
        -- F10/Airboss/<Carrier>/F2 Kneeboard
        -------------------------------------
        local _kneeboardPath = missionCommands.addSubMenuForGroup( gid, "Kneeboard", _rootPath )
        -- F10/Airboss/<Carrier>/F2 Kneeboard/F1 Results
        local _resultsPath = missionCommands.addSubMenuForGroup( gid, "Results", _kneeboardPath )
        -- F10/Airboss/<Carrier>/F2 Kneeboard/F1 Results/
        missionCommands.addCommandForGroup( gid, "Greenie Board", _resultsPath, self._DisplayScoreBoard, self, _unitName ) -- F1
        missionCommands.addCommandForGroup( gid, "My LSO Grades", _resultsPath, self._DisplayPlayerGrades, self, _unitName ) -- F2
        missionCommands.addCommandForGroup( gid, "Last Debrief", _resultsPath, self._DisplayDebriefing, self, _unitName ) -- F3

        -- F10/Airboss/<Carrier>/F2 Kneeboard/F2 Skipper/
        if self.skipperMenu then
          local _skipperPath = missionCommands.addSubMenuForGroup( gid, "Skipper", _kneeboardPath )
          local _menusetspeed = missionCommands.addSubMenuForGroup( gid, "Set Speed", _skipperPath )
          missionCommands.addCommandForGroup( gid, "10 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 10 )
          missionCommands.addCommandForGroup( gid, "15 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 15 )
          missionCommands.addCommandForGroup( gid, "20 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 20 )
          missionCommands.addCommandForGroup( gid, "25 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 25 )
          missionCommands.addCommandForGroup( gid, "30 knots", _menusetspeed, self._SkipperRecoverySpeed, self, _unitName, 30 )
          local _menusetrtime = missionCommands.addSubMenuForGroup( gid, "Set Time", _skipperPath )
          missionCommands.addCommandForGroup( gid, "15 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 15 )
          missionCommands.addCommandForGroup( gid, "30 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 30 )
          missionCommands.addCommandForGroup( gid, "45 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 45 )
          missionCommands.addCommandForGroup( gid, "60 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 60 )
          missionCommands.addCommandForGroup( gid, "90 min", _menusetrtime, self._SkipperRecoveryTime, self, _unitName, 90 )
          local _menusetrtime = missionCommands.addSubMenuForGroup( gid, "Set Marshal Radial", _skipperPath )
          missionCommands.addCommandForGroup( gid, "+30°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 30 )
          missionCommands.addCommandForGroup( gid, "+15°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 15 )
          missionCommands.addCommandForGroup( gid, "0°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, 0 )
          missionCommands.addCommandForGroup( gid, "-15°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, -15 )
          missionCommands.addCommandForGroup( gid, "-30°", _menusetrtime, self._SkipperRecoveryOffset, self, _unitName, -30 )
          missionCommands.addCommandForGroup( gid, "U-turn On/Off", _skipperPath, self._SkipperRecoveryUturn, self, _unitName )
          missionCommands.addCommandForGroup( gid, "Start CASE I", _skipperPath, self._SkipperStartRecovery, self, _unitName, 1 )
          missionCommands.addCommandForGroup( gid, "Start CASE II", _skipperPath, self._SkipperStartRecovery, self, _unitName, 2 )
          missionCommands.addCommandForGroup( gid, "Start CASE III", _skipperPath, self._SkipperStartRecovery, self, _unitName, 3 )
          missionCommands.addCommandForGroup( gid, "Stop Recovery", _skipperPath, self._SkipperStopRecovery, self, _unitName )
        end

        -- F10/Airboss/<Carrier/F2 Kneeboard/
        missionCommands.addCommandForGroup( gid, "Carrier Info", _kneeboardPath, self._DisplayCarrierInfo, self, _unitName ) -- F2
        missionCommands.addCommandForGroup( gid, "Weather Report", _kneeboardPath, self._DisplayCarrierWeather, self, _unitName ) -- F3
        missionCommands.addCommandForGroup( gid, "Set Section", _kneeboardPath, self._SetSection, self, _unitName ) -- F4
        missionCommands.addCommandForGroup( gid, "Marshal Queue", _kneeboardPath, self._DisplayQueue, self, _unitName, "Marshal" ) -- F5
        missionCommands.addCommandForGroup( gid, "Pattern Queue", _kneeboardPath, self._DisplayQueue, self, _unitName, "Pattern" ) -- F6
        missionCommands.addCommandForGroup( gid, "Waiting Queue", _kneeboardPath, self._DisplayQueue, self, _unitName, "Waiting" ) -- F7

        -------------------------
        -- F10/Airboss/<Carrier>/
        -------------------------
        missionCommands.addCommandForGroup( gid, "Request Marshal", _rootPath, self._RequestMarshal, self, _unitName ) -- F3
        missionCommands.addCommandForGroup( gid, "Request Commence", _rootPath, self._RequestCommence, self, _unitName ) -- F4
        missionCommands.addCommandForGroup( gid, "Request Refueling", _rootPath, self._RequestRefueling, self, _unitName ) -- F5
        missionCommands.addCommandForGroup( gid, "Spinning", _rootPath, self._RequestSpinning, self, _unitName ) -- F6
        missionCommands.addCommandForGroup( gid, "Emergency Landing", _rootPath, self._RequestEmergency, self, _unitName ) -- F7
        missionCommands.addCommandForGroup( gid, "[Reset My Status]", _rootPath, self._ResetPlayerStatus, self, _unitName ) -- F8
      end
    else
      self:E( self.lid .. string.format( "ERROR: Could not find group or group ID in AddF10Menu() function. Unit name: %s.", _unitName ) )
    end
  else
    self:E( self.lid .. string.format( "ERROR: Player unit does not exist in AddF10Menu() function. Unit name: %s.", _unitName ) )
  end

end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- SKIPPER MENU
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Reset player status. Player is removed from all queues and its status is set to undefined.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
-- @param #number case Recovery case.
function AIRBOSS:_SkipperStartRecovery( _unitName, case )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Inform player.
      local text = string.format( "affirm, Case %d recovery will start in 5 min for %d min. Wind on deck %d knots. U-turn=%s.", case, self.skipperTime, self.skipperSpeed, tostring( self.skipperUturn ) )
      if case > 1 then
        text = text .. string.format( " Marshal radial %d°.", self.skipperOffset )
      end
      if self:IsRecovering() then
        text = "negative, carrier is already recovering."
        self:MessageToPlayer( playerData, text, "AIRBOSS" )
        return
      end
      self:MessageToPlayer( playerData, text, "AIRBOSS" )

      -- Recovery staring in 5 min for 30 min.
      local t0 = timer.getAbsTime() + 5 * 60
      local t9 = t0 + self.skipperTime * 60
      local C0 = UTILS.SecondsToClock( t0 )
      local C9 = UTILS.SecondsToClock( t9 )

      -- Carrier will turn into the wind. Wind on deck 25 knots. U-turn on.
      self:AddRecoveryWindow( C0, C9, case, self.skipperOffset, true, self.skipperSpeed, self.skipperUturn )

    end
  end
end

--- Skipper Stop recovery function.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
function AIRBOSS:_SkipperStopRecovery( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Inform player.
      local text = "roger, stopping recovery right away."
      if not self:IsRecovering() then
        text = "negative, carrier is currently not recovering."
        self:MessageToPlayer( playerData, text, "AIRBOSS" )
        return
      end
      self:MessageToPlayer( playerData, text, "AIRBOSS" )

      self:RecoveryStop()
    end
  end
end

--- Skipper set recovery offset angle.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
-- @param #number offset Recovery holding offset angle in degrees for Case II/III.
function AIRBOSS:_SkipperRecoveryOffset( _unitName, offset )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Inform player.
      local text = string.format( "roger, relative CASE II/III Marshal radial set to %d°.", offset )
      self:MessageToPlayer( playerData, text, "AIRBOSS" )

      self.skipperOffset = offset
    end
  end
end

--- Skipper set recovery time.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
-- @param #number time Recovery time in minutes.
function AIRBOSS:_SkipperRecoveryTime( _unitName, time )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Inform player.
      local text = string.format( "roger, manual recovery time set to %d min.", time )
      self:MessageToPlayer( playerData, text, "AIRBOSS" )

      self.skipperTime = time

    end
  end
end

--- Skipper set recovery speed.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
-- @param #number speed Recovery speed in knots.
function AIRBOSS:_SkipperRecoverySpeed( _unitName, speed )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Inform player.
      local text = string.format( "roger, wind on deck set to %d knots.", speed )
      self:MessageToPlayer( playerData, text, "AIRBOSS" )

      self.skipperSpeed = speed
    end
  end
end

--- Skipper set recovery speed.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
function AIRBOSS:_SkipperRecoveryUturn( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      self.skipperUturn = not self.skipperUturn

      -- Inform player.
      local text = string.format( "roger, U-turn is now %s.", tostring( self.skipperUturn ) )
      self:MessageToPlayer( playerData, text, "AIRBOSS" )

    end
  end
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ROOT MENU
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Reset player status. Player is removed from all queues and its status is set to undefined.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
function AIRBOSS:_ResetPlayerStatus( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Inform player.
      local text = "roger, status reset executed! You have been removed from all queues."
      self:MessageToPlayer( playerData, text, "AIRBOSS" )

      -- Remove flight from queues. Collapse marshal stack if necessary.
      -- Section members are removed from the Spinning queue. If flight is member, he is removed from the section.
      self:_RemoveFlight( playerData )

      -- Stop pending debrief scheduler.
      if playerData.debriefschedulerID and self.Scheduler then
        self.Scheduler:Stop( playerData.debriefschedulerID )
      end

      -- Initialize player data.
      self:_InitPlayer( playerData )

    end
  end
end

--- Request marshal.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
function AIRBOSS:_RequestMarshal( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Voice over of inbound call (regardless of airboss rejecting it or not)
      if self.xtVoiceOvers then
        self:_MarshallInboundCall(_unit, playerData.onboard)
      end

      -- Check if player is in CCA
      local inCCA = playerData.unit:IsInZone( self.zoneCCA )

      if inCCA then

        if self:_InQueue( self.Qmarshal, playerData.group ) then

          -- Flight group is already in marhal queue.
          local text = string.format( "negative, you are already in the Marshal queue. New marshal request denied!" )
          self:MessageToPlayer( playerData, text, "MARSHAL" )

        elseif self:_InQueue( self.Qpattern, playerData.group ) then

          -- Flight group is already in pattern queue.
          local text = string.format( "negative, you are already in the Pattern queue. Marshal request denied!" )
          self:MessageToPlayer( playerData, text, "MARSHAL" )

        elseif self:_InQueue( self.Qwaiting, playerData.group ) then

          -- Flight group is already in pattern queue.
          local text = string.format( "negative, you are in the Waiting queue with %d flights ahead of you. Marshal request denied!", #self.Qwaiting )
          self:MessageToPlayer( playerData, text, "MARSHAL" )

        elseif not _unit:InAir() then

          -- Flight group is already in pattern queue.
          local text = string.format( "negative, you are not airborne. Marshal request denied!" )
          self:MessageToPlayer( playerData, text, "MARSHAL" )

        elseif playerData.name ~= playerData.seclead then

          -- Flight group is already in pattern queue.
          local text = string.format( "negative, your section lead %s needs to request Marshal.", playerData.seclead )
          self:MessageToPlayer( playerData, text, "MARSHAL" )

        else

          -- Get next free Marshal stack.
          local freestack = self:_GetFreeStack( playerData.ai )

          -- Check if stack is available. For Case I the number is limited.
          if freestack then

            -- Add flight to marshal stack.
            self:_MarshalPlayer( playerData, freestack )

          else

            -- Add flight to waiting queue.
            self:_WaitPlayer( playerData )

          end

        end

      else

        -- Flight group is not in CCA yet.
        local text = string.format( "negative, you are not inside CCA. Marshal request denied!" )
        self:MessageToPlayer( playerData, text, "MARSHAL" )

      end
    end
  end
end

--- Request emergency landing.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
function AIRBOSS:_RequestEmergency( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      local text = ""
      if not self.emergency then

        -- Mission designer did not allow emergency landing.
        text = "negative, no emergency landings on my carrier. We are currently busy. See how you get along!"

      elseif not _unit:InAir() then

        -- Carrier zone.
        local zone = self:_GetZoneCarrierBox()

        -- Check if player is on the carrier.
        if playerData.unit:IsInZone( zone ) then

          -- Bolter pattern.
          text = "roger, you are now technically in the bolter pattern. Your next step after takeoff is abeam!"

          -- Get flight lead.
          local lead = self:_GetFlightLead( playerData )

          -- Set set for lead.
          self:_SetPlayerStep( lead, AIRBOSS.PatternStep.BOLTER )

          -- Also set bolter pattern for all members.
          for _, sec in pairs( lead.section ) do
            local sectionmember = sec -- #AIRBOSS.PlayerData
            self:_SetPlayerStep( sectionmember, AIRBOSS.PatternStep.BOLTER )
          end

          -- Remove flight from waiting queue just in case.
          self:_RemoveFlightFromQueue( self.Qwaiting, lead )

          if self:_InQueue( self.Qmarshal, lead.group ) then
            -- Remove flight from Marshal queue and add to pattern.
            self:_RemoveFlightFromMarshalQueue( lead )
          else
            -- Add flight to pattern if he was not.
            if not self:_InQueue( self.Qpattern, lead.group ) then
              self:_AddFlightToPatternQueue( lead )
            end
          end

        else
          -- Flight group is not in air.
          text = string.format( "negative, you are not airborne. Request denied!" )
        end

      else

        -- Cleared.
        text = "affirmative, you can bypass the pattern and are cleared for final approach!"

        -- VNAO Edit - Added wrapped up call to LSO grading
        playerData.wrappedUpAtWakeLittle = false -- VNAO Edit - Added
        playerData.wrappedUpAtWakeFull = false -- VNAO Edit - Added
        playerData.wrappedUpAtWakeUnderline = false -- VNAO Edit - Added
        playerData.wrappedUpAtStartLittle = false -- VNAO Edit - Added
        playerData.wrappedUpAtStartFull = false -- VNAO Edit - Added
        playerData.wrappedUpAtStartUnderline = false -- VNAO Edit - Added
        playerData.AAatWakeLittle = false -- VNAO Edit - Added
        playerData.AAatWakeFull = false -- VNAO Edit - Added
        playerData.AAatWakeUnderline = false -- VNAO Edit - Added

        -- Now, if player is in the marshal or waiting queue he will be removed. But the new leader should stay in or not.
        local lead = self:_GetFlightLead( playerData )

        -- Set set for lead.
        self:_SetPlayerStep( lead, AIRBOSS.PatternStep.EMERGENCY )

        -- Also set emergency landing for all members.
        for _, sec in pairs( lead.section ) do
          local sectionmember = sec -- #AIRBOSS.PlayerData
          self:_SetPlayerStep( sectionmember, AIRBOSS.PatternStep.EMERGENCY )

          -- Remove flight from spinning queue just in case (everone can spin on his own).
          self:_RemoveFlightFromQueue( self.Qspinning, sectionmember )
        end

        -- Remove flight from waiting queue just in case.
        self:_RemoveFlightFromQueue( self.Qwaiting, lead )

        if self:_InQueue( self.Qmarshal, lead.group ) then
          -- Remove flight from Marshal queue and add to pattern.
          self:_RemoveFlightFromMarshalQueue( lead )
        else
          -- Add flight to pattern if he was not.
          if not self:_InQueue( self.Qpattern, lead.group ) then
            self:_AddFlightToPatternQueue( lead )
          end
        end

      end

      -- Send message.
      self:MessageToPlayer( playerData, text, "AIRBOSS" )

    end

  end
end

--- Request spinning.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
function AIRBOSS:_RequestSpinning( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      local text = ""
      if not self:_InQueue( self.Qpattern, playerData.group ) then

        -- Player not in pattern queue.
        text = "negative, you have to be in the pattern to spin it!"

      elseif playerData.step == AIRBOSS.PatternStep.SPINNING then

        -- Player is already spinning.
        text = "negative, you are already spinning."

        -- Check if player is in the right step.
      elseif not (playerData.step == AIRBOSS.PatternStep.BREAKENTRY or
                  playerData.step == AIRBOSS.PatternStep.EARLYBREAK or
                  playerData.step == AIRBOSS.PatternStep.LATEBREAK) then

        -- Player is not in the right step.
        text = "negative, you have to be in the right step to spin it!"

      else

        -- Set player step.
        self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.SPINNING )

        -- Add player to spinning queue.
        table.insert( self.Qspinning, playerData )

        -- 405, Spin it! Click.
        local call = self:_NewRadioCall( self.LSOCall.SPINIT, "AIRBOSS", "Spin it!", self.Tmessage, playerData.onboard )
        self:RadioTransmission( self.LSORadio, call, nil, nil, nil, true )

        -- Some advice.
        if playerData.difficulty == AIRBOSS.Difficulty.EASY then
          local text = "Climb to 1200 feet and proceed to the initial again."
          self:MessageToPlayer( playerData, text, "AIRBOSS", "" )
        end

        return
      end

      -- Send message.
      self:MessageToPlayer( playerData, text, "AIRBOSS" )

    end
  end
end

--- Request to commence landing approach.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
function AIRBOSS:_RequestCommence( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Voice over of Commencing call (regardless of Airboss will rejected or not)
      if self.xtVoiceOvers then
        self:_CommencingCall(_unit, playerData.onboard)
      end

      -- Check if unit is in CCA.
      local text = ""
      local cleared = false
      if _unit:IsInZone( self.zoneCCA ) then

        -- Get stack value.
        local stack = playerData.flag

        -- Number of airborne aircraft currently in pattern.
        local _, npattern = self:_GetQueueInfo( self.Qpattern )

        -- TODO: Check distance to initial or platform. Only allow commence if < max distance. Otherwise say bearing.

        if self:_InQueue( self.Qpattern, playerData.group ) then

          -- Flight group is already in pattern queue.
          text = string.format( "negative, %s, you are already in the Pattern queue.", playerData.name )

        elseif not _unit:InAir() then

          -- Flight group is already in pattern queue.
          text = string.format( "negative, %s, you are not airborne.", playerData.name )

        elseif playerData.seclead ~= playerData.name then

          -- Flight group is already in pattern queue.
          text = string.format( "negative, %s, your section leader %s has to request commence!", playerData.name, playerData.seclead )

        elseif stack > 1 then

          -- We are in a higher stack.
          text = string.format( "negative, %s, it's not your turn yet! You are in stack no. %s.", playerData.name, stack )

        elseif npattern >= self.Nmaxpattern then

          -- Patern is full!
          text = string.format( "negative ghostrider, pattern is full!\nThere are %d aircraft currently in the pattern.", npattern )

        elseif self:IsRecovering() == false and not self.airbossnice then

          -- Carrier is not recovering right now.
          if self.recoverywindow then
            local clock = UTILS.SecondsToClock( self.recoverywindow.START )
            text = string.format( "negative, carrier is currently not recovery. Next window will open at %s.", clock )
          else
            text = string.format( "negative, carrier is not recovering. No future windows planned." )
          end

        elseif not self:_InQueue( self.Qmarshal, playerData.group ) and not self.airbossnice then

          text = "negative, you have to request Marshal before you can commence."

        else

          -----------------------
          -- Positive Response --
          -----------------------

          text = text .. "roger."

          -- Carrier is not recovering but Airboss has a good day.
          if not self:IsRecovering() then
            text = text .. " Carrier is not recovering currently! However, you are cleared anyway as I have a nice day."
          end

          -- If player is not in the Marshal queue set player case to current case.
          if not self:_InQueue( self.Qmarshal, playerData.group ) then

            -- Set current case.
            playerData.case = self.case

            -- Hint about TACAN bearing.
            if self.TACANon and playerData.difficulty ~= AIRBOSS.Difficulty.HARD then
              -- Get inverse magnetic radial potential offset.
              local radial = self:GetRadial( playerData.case, true, true, true )
              if playerData.case == 1 then
                -- For case 1 we want the BRC but above routine return FB.
                radial = self:GetBRC()
              end
              text = text .. string.format( "\nSelect TACAN %03d°, Channel %d%s (%s).\n", radial, self.TACANchannel, self.TACANmode, self.TACANmorse )
            end

            -- TODO: Inform section members.

            -- Set case of section members as well. Not sure if necessary any more since it is set as soon as the recovery case is changed.
            for _, flight in pairs( playerData.section ) do
              flight.case = playerData.case
            end

            -- Add player to pattern queue. Usually this is done when the stack is collapsed but this player is not in the Marshal queue.
            self:_AddFlightToPatternQueue( playerData )
          end

          -- Clear player for commence.
          cleared = true
        end

      else
        -- This flight is not yet registered!
        text = string.format( "negative, %s, you are not inside the CCA!", playerData.name )
      end

      -- Debug
      self:T( self.lid .. text )

      -- Send message.
      self:MessageToPlayer( playerData, text, "MARSHAL" )

      -- Check if player was cleard. Need to do this after the message above is displayed.
      if cleared then
        -- Call commence routine. No zone check. NOTE: Commencing will set step for all section members as well.
        self:_Commencing( playerData, false )
      end
    end
  end
end

--- Player requests refueling.
-- @param #AIRBOSS self
-- @param #string _unitName Name of the player unit.
function AIRBOSS:_RequestRefueling( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Check if there is a recovery tanker defined.
      local text
      if self.tanker then

        -- Check if player is in CCA.
        if _unit:IsInZone( self.zoneCCA ) then

          -- Check if tanker is running or refueling or returning.
          if self.tanker:IsRunning() or self.tanker:IsRefueling() then

            -- Get alt of tanker in angels.
            -- local angels=UTILS.Round(UTILS.MetersToFeet(self.tanker.altitude)/1000, 0)
            local angels = self:_GetAngels( self.tanker.altitude )

            -- Tanker is up and running.
            text = string.format( "affirmative, proceed to tanker at angels %d.", angels )

            -- State TACAN channel of tanker if defined.
            if self.tanker.TACANon then
              text = text .. string.format( "\nTanker TACAN channel %d%s (%s).", self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse )
              text = text .. string.format( "\nRadio frequency %.3f MHz AM.", self.tanker.RadioFreq )
            end

            -- Tanker is currently refueling. Inform player.
            if self.tanker:IsRefueling() then
              text = text .. "\nTanker is currently refueling. You might have to queue up."
            end

            -- Collapse marshal stack if player is in queue.
            self:_RemoveFlightFromMarshalQueue( playerData, true )

            -- Set step to refueling.
            self:_SetPlayerStep( playerData, AIRBOSS.PatternStep.REFUELING )

            -- Inform section and set step.
            for _, sec in pairs( playerData.section ) do
              local sectext = "follow your section leader to the tanker."
              self:MessageToPlayer( sec, sectext, "MARSHAL" )
              self:_SetPlayerStep( sec, AIRBOSS.PatternStep.REFUELING )
            end

          elseif self.tanker:IsReturning() then
            -- Tanker is RTB.
            text = "negative, tanker is RTB. Request denied!\nWait for the tanker to be back on station if you can."
          end

        else
          text = "negative, you are not inside the CCA yet."
        end
      else
        text = "negative, no refueling tanker available."
      end

      -- Send message.
      self:MessageToPlayer( playerData, text, "MARSHAL" )
    end
  end
end

--- Remove a member from the player's section.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player
-- @param #AIRBOSS.PlayerData sectionmember The section member to be removed.
-- @return #boolean If true, flight was a section member and could be removed. False otherwise.
function AIRBOSS:_RemoveSectionMember( playerData, sectionmember )
  -- Loop over all flights in player's section
  for i, _flight in pairs( playerData.section ) do
    local flight = _flight -- #AIRBOSS.PlayerData
    if flight.name == sectionmember.name then
      table.remove( playerData.section, i )
      return true
    end
  end
  return false
end

--- Set all flights within maxsectiondistance meters to be part of my section (default: 100 meters).
-- @param #AIRBOSS self
-- @param #string _unitName Name of the player unit.
function AIRBOSS:_SetSection( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Coordinate of flight lead.
      local mycoord = _unit:GetCoordinate()

      -- Max distance up to which section members are allowed.
      local dmax = self.maxsectiondistance

      -- Check if player is in Marshal or pattern queue already.
      local text
      if self.NmaxSection == 0 then
        text = string.format( "negative, setting sections is disabled in this mission. You stay alone." )
      elseif self:_InQueue( self.Qmarshal, playerData.group ) then
        text = string.format( "negative, you are already in the Marshal queue. Setting section not possible any more!" )
      elseif self:_InQueue( self.Qpattern, playerData.group ) then
        text = string.format( "negative, you are already in the Pattern queue. Setting section not possible any more!" )
      else

        -- Check if player is member of another section already. If so, remove him from his current section.
        if playerData.seclead ~= playerData.name then
          local lead = self.players[playerData.seclead] -- #AIRBOSS.PlayerData
          if lead then

            -- Remove player from his old section lead.
            local removed = self:_RemoveSectionMember( lead, playerData )
            if removed then
              self:MessageToPlayer( lead, string.format( "Flight %s has been removed from your section.", playerData.name ), "AIRBOSS", "", 5 )
              self:MessageToPlayer( playerData, string.format( "You have been removed from %s's section.", lead.name ), "AIRBOSS", "", 5 )
            end

          end
        end

        -- Potential section members.
        local section = {}

        -- Loop over all registered flights.
        for _, _flight in pairs( self.flights ) do
          local flight = _flight -- #AIRBOSS.FlightGroup

          -- Only human flight groups excluding myself. Also only flights that dont have a section itself (would get messy) or are part of another section (no double membership).
          if flight.ai == false and flight.groupname ~= playerData.groupname and #flight.section == 0 and flight.seclead == flight.name then

            -- Distance (3D) to other flight group.
            local distance = flight.group:GetCoordinate():Get3DDistance( mycoord )

            -- Check distance.
            if distance < dmax then
              self:T( self.lid .. string.format( "Found potential section member %s for lead %s at distance %.1f m.", flight.name, playerData.name, distance ) )
              table.insert( section, { flight = flight, distance = distance } )
            end

          end
        end

        -- Sort potential section members wrt to distance to lead.
        table.sort( section, function( a, b )
          return a.distance < b.distance
        end )

        -- Make player section lead if he was not before.
        playerData.seclead = playerData.name

        -- Loop over all flights in player's current section and inform those members that will be removed because they are not in range any more.
        for _, _flight in pairs( playerData.section ) do
          local flight = _flight -- #AIRBOSS.PlayerData

          -- Loop over all potential new members and check if they were already part of the player's section.
          local gotit = false
          for _, _new in pairs( section ) do
            local newflight = _new.flight -- #AIRBOSS.PlayerData
            if newflight.name == flight.name then
              gotit = true -- This is an old one that stays.
            end
          end

          -- Flight is not a member any more ==> remove it.
          if not gotit then
            self:MessageToPlayer( flight, string.format( "you were removed from %s's section and are on your own now.", playerData.name ), "AIRBOSS", "", 5 )
            flight.seclead = flight.name
            self:_RemoveSectionMember( playerData, flight )
          end
        end

        -- Remove all flights that are currently in the player's section already from scanned potential new section members.
        for i, _new in pairs( section ) do
          local newflight = _new.flight -- #AIRBOSS.PlayerData
          for _, _flight in pairs( playerData.section ) do
            local currentflight = _flight -- #AIRBOSS.PlayerData
            if newflight.name == currentflight.name then
              table.remove( section, i )
            end
          end
        end

        -- Init section table. Should not be necessary as all members are removed anyhow above.
        -- playerData.section={}

        -- Output text.
        text = string.format( "Registered flight section:" )
        text = text .. string.format( "\n- %s (lead)", playerData.seclead )
        -- Old members that stay (if any).
        for _, _flight in pairs( playerData.section ) do
          local flight = _flight -- #AIRBOSS.PlayerData
          text = text .. string.format( "\n- %s", flight.name )
        end
        -- New members (if any).
        for i = 1, math.min( self.NmaxSection - #playerData.section, #section ) do
          local flight = section[i].flight -- #AIRBOSS.PlayerData

          -- New flight members.
          text = text .. string.format( "\n- %s", flight.name )

          -- Set section lead of player flight.
          flight.seclead = playerData.name

          -- Set case of f
          flight.case = playerData.case

          -- Inform player that he is now part of a section.
          self:MessageToPlayer( flight, string.format( "your section lead is now %s.", playerData.name ), "AIRBOSS" )

          -- Add flight to section table.
          table.insert( playerData.section, flight )
        end

        -- Section is empty.
        if #playerData.section == 0 then
          text = text .. string.format( "\n- No other human flights found within radius of %.1f meters!", dmax )
        end

      end

      -- Message to section lead.
      self:MessageToPlayer( playerData, text, "MARSHAL" )
    end
  end
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- RESULTS MENU
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Display top 10 player scores.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
function AIRBOSS:_DisplayScoreBoard( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then

    -- Results table.
    local _playerResults = {}

    -- Calculate average points for all players.
    for playerName, playerGrades in pairs( self.playerscores ) do

      if playerGrades then

        -- Loop over all grades
        local Paverage = 0
        local n = 0
        for _, _grade in pairs( playerGrades ) do
          local grade = _grade -- #AIRBOSS.LSOgrade

          -- Add up only final scores for the average.
          if grade.finalscore then -- grade.points>=0 then
            Paverage = Paverage + grade.finalscore
            n = n + 1
          else
            -- Case when the player just leaves after an unfinished pass, e.g bolter, without landing.
            -- But this should now be solved by deleteing all unfinished results.
          end
        end

        -- We dont want to devide by zero.
        if n > 0 then
          _playerResults[playerName] = Paverage / n
        end

      end
    end

    -- Message text.
    local text = string.format( "Greenie Board (top ten):" )
    local i = 1
    for _playerName, _points in UTILS.spairs( _playerResults, function( t, a, b )
      return t[b] < t[a]
    end ) do

      -- Text.
      text = text .. string.format( "\n[%d] %s %.1f||", i, _playerName, _points )

      -- All player grades.
      local playerGrades = self.playerscores[_playerName]

      -- Add grades of passes. We use the actual grade of each pass here and not the average after player has landed.
      for _, _grade in pairs( playerGrades ) do
        local grade = _grade -- #AIRBOSS.LSOgrade
        if grade.finalscore then
          text = text .. string.format( "%.1f|", grade.points )
        elseif grade.points >= 0 then -- Only points >=0 as foul deck gives -1.
          text = text .. string.format( "(%.1f)", grade.points )
        end
      end

      -- Display only the top ten.
      i = i + 1
      if i > 10 then
        break
      end
    end

    -- If no results yet.
    if i == 1 then
      text = text .. "\nNo results yet."
    end

    -- Send message.
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData
    if playerData.client then
      MESSAGE:New( text, 30, nil, true ):ToClient( playerData.client )
    end

  end
end

--- Display top 10 player scores.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
function AIRBOSS:_DisplayPlayerGrades( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Grades of player:
      local text = string.format( "Your last 10 grades, %s:", _playername )

      -- All player grades.
      local playerGrades = self.playerscores[_playername] or {}

      local p = 0 -- Average points.
      local n = 0 -- Number of final passes.
      local m = 0 -- Number of total passes.
      -- for i,_grade in pairs(playerGrades) do
      for i = #playerGrades, 1, -1 do
        -- local grade=_grade --#AIRBOSS.LSOgrade
        local grade = playerGrades[i] -- #AIRBOSS.LSOgrade

        -- Check if points >=0. For foul deck WO we give -1 and pass is not counted.
        if grade.points >= 0 then

          -- Show final points or points of pass.
          local points = grade.finalscore or grade.points

          -- Display max 10 results.
          if m < 10 then
            text = text .. string.format( "\n[%d] %s %.1f PT - %s", i, grade.grade, points, grade.details )

            -- Wire trapped if any.
            if grade.wire and grade.wire <= 4 then
              text = text .. string.format( " %d-wire", grade.wire )
            end

            -- Time in the groove if any.
            if grade.Tgroove and grade.Tgroove <= 360 then
              text = text .. string.format( " Tgroove=%.1f s", grade.Tgroove )
            end
          end

          -- Add up final points.
          if grade.finalscore then
            p = p + grade.finalscore
            n = n + 1
          end

          -- Total passes
          m = m + 1
        end
      end

      if n > 0 then
        text = text .. string.format( "\nAverage points = %.1f", p / n )
      else
        text = text .. string.format( "\nNo data available." )
      end

      -- Send message.
      if playerData.client then
        MESSAGE:New( text, 30, nil, true ):ToClient( playerData.client )
      end
    end
  end
end

--- Display last debriefing.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
function AIRBOSS:_DisplayDebriefing( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Debriefing text.
      local text = string.format( "Debriefing:" )

      -- Check if data is present.
      if #playerData.lastdebrief > 0 then
        text = text .. string.format( "\n================================\n" )
        for _, _data in pairs( playerData.lastdebrief ) do
          local step = _data.step
          local comment = _data.hint
          text = text .. string.format( "* %s:", step )
          text = text .. string.format( "%s\n", comment )
        end
      else
        text = text .. " Nothing to show yet."
      end

      -- Send debrief message to player
      self:MessageToPlayer( playerData, text, nil, "", 30, true )

    end
  end
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- KNEEBOARD MENU
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Display marshal or pattern queue.
-- @param #AIRBOSS self
-- @param #string _unitname Name of the player unit.
-- @param #string qname Name of the queue.
function AIRBOSS:_DisplayQueue( _unitname, qname )

  -- Get player unit and player name.
  local unit, playername = self:_GetPlayerUnitAndName( _unitname )

  -- Check if we have a player.
  if unit and playername then

    -- Player data.
    local playerData = self.players[playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Queue to display.
      local queue = nil
      if qname == "Marshal" then
        queue = self.Qmarshal
      elseif qname == "Pattern" then
        queue = self.Qpattern
      elseif qname == "Waiting" then
        queue = self.Qwaiting
      end

      -- Number of group and units in queue
      local Nqueue, nqueue = self:_GetQueueInfo( queue, playerData.case )

      local text = string.format( "%s Queue:", qname )
      if #queue == 0 then
        text = text .. " empty"
      else
        local N = 0
        if qname == "Marshal" then
          for i, _flight in pairs( queue ) do
            local flight = _flight -- #AIRBOSS.FlightGroup
            local charlie = self:_GetCharlieTime( flight )
            local Charlie = UTILS.SecondsToClock( charlie )
            local stack = flight.flag
            local angels = self:_GetAngels( self:_GetMarshalAltitude( stack, flight.case ) )
            local _, nunit, nsec = self:_GetFlightUnits( flight, true )
            local nick = self:_GetACNickname( flight.actype )
            N = N + nunit
            text = text .. string.format( "\n[Stack %d] %s (%s*%d+%d): Case %d, Angels %d, Charlie %s", stack, flight.onboard, nick, nunit, nsec, flight.case, angels, tostring( Charlie ) )
          end
        elseif qname == "Pattern" or qname == "Waiting" then
          for i, _flight in pairs( queue ) do
            local flight = _flight -- #AIRBOSS.FlightGroup
            local _, nunit, nsec = self:_GetFlightUnits( flight, true )
            local nick = self:_GetACNickname( flight.actype )
            local ptime = UTILS.SecondsToClock( timer.getAbsTime() - flight.time )
            N = N + nunit
            text = text .. string.format( "\n[%d] %s (%s*%d+%d): Case %d, T=%s", i, flight.onboard, nick, nunit, nsec, flight.case, ptime )
          end
        end
        text = text .. string.format( "\nTotal AC: %d (airborne %d)", N, nqueue )
      end

      -- Send message.
      self:MessageToPlayer( playerData, text, nil, "", nil, true )
    end
  end
end

--- Report information about carrier.
-- @param #AIRBOSS self
-- @param #string _unitname Name of the player unit.
function AIRBOSS:_DisplayCarrierInfo( _unitname )
  self:F2( _unitname )

  -- Get player unit and player name.
  local unit, playername = self:_GetPlayerUnitAndName( _unitname )

  -- Check if we have a player.
  if unit and playername then

    -- Player data.
    local playerData = self.players[playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Current coordinates.
      local coord = self:GetCoordinate()

      -- Carrier speed and heading.
      local carrierheading = self.carrier:GetHeading()
      local carrierspeed = UTILS.MpsToKnots( self.carrier:GetVelocityMPS() )

      -- TACAN/ICLS.
      local tacan = "unknown"
      local icls = "unknown"
      if self.TACANon and self.TACANchannel ~= nil then
        tacan = string.format( "%d%s (%s)", self.TACANchannel, self.TACANmode, self.TACANmorse )
      end
      if self.ICLSon and self.ICLSchannel ~= nil then
        icls = string.format( "%d (%s)", self.ICLSchannel, self.ICLSmorse )
      end

      -- Wind on flight deck
      local wind = UTILS.MpsToKnots( select( 1, self:GetWindOnDeck() ) )

      -- Get groups, units in queues.
      local Nmarshal, nmarshal = self:_GetQueueInfo( self.Qmarshal, playerData.case )
      local Npattern, npattern = self:_GetQueueInfo( self.Qpattern )
      local Nspinning, nspinning = self:_GetQueueInfo( self.Qspinning )
      local Nwaiting, nwaiting = self:_GetQueueInfo( self.Qwaiting )
      local Ntotal, ntotal = self:_GetQueueInfo( self.flights )

      -- Current abs time.
      local Tabs = timer.getAbsTime()

      -- Get recovery times of carrier.
      local recoverytext = "Recovery time windows (max 5):"
      if #self.recoverytimes == 0 then
        recoverytext = recoverytext .. " none."
      else
        -- Loop over recovery windows.
        local rw = 0
        for _, _recovery in pairs( self.recoverytimes ) do
          local recovery = _recovery -- #AIRBOSS.Recovery
          -- Only include current and future recovery windows.
          if Tabs < recovery.STOP then
            -- Output text.
            recoverytext = recoverytext .. string.format( "\n* %s - %s: Case %d (%d°)", UTILS.SecondsToClock( recovery.START ), UTILS.SecondsToClock( recovery.STOP ), recovery.CASE, recovery.OFFSET )
            if recovery.WIND then
              recoverytext = recoverytext .. string.format( " @ %.1f kts wind", recovery.SPEED )
            end
            rw = rw + 1
            if rw >= 5 then
              -- Break the loop after 5 recovery times.
              break
            end
          end
        end
      end

      -- Recovery tanker TACAN text.
      local tankertext = nil
      if self.tanker then
        tankertext = string.format( "Recovery tanker frequency %.3f MHz\n", self.tanker.RadioFreq )
        if self.tanker.TACANon then
          tankertext = tankertext .. string.format( "Recovery tanker TACAN %d%s (%s)", self.tanker.TACANchannel, self.tanker.TACANmode, self.tanker.TACANmorse )
        else
          tankertext = tankertext .. "Recovery tanker TACAN n/a"
        end
      end

      -- Carrier FSM state. Idle is not clear enough.
      local state = self:GetState()
      if state == "Idle" then
        state = "Deck closed"
      end
      if self.turning then
        state = state .. " (currently turning)"
      end

      -- Message text.
      local text = string.format( "%s info:\n", self.alias )
      text = text .. string.format( "================================\n" )
      text = text .. string.format( "Carrier state: %s\n", state )
      if self.case == 1 then
        text = text .. string.format( "Case %d recovery ops\n", self.case )
      else
        local radial = self:GetRadial( self.case, true, true, false )
        text = text .. string.format( "Case %d recovery ops\nMarshal radial %03d°\n", self.case, radial )
      end
      text = text .. string.format( "BRC %03d° - FB %03d°\n", self:GetBRC(), self:GetFinalBearing( true ) )
      text = text .. string.format( "Speed %.1f kts - Wind on deck %.1f kts\n", carrierspeed, wind )
      text = text .. string.format( "Tower frequency %.3f MHz\n", self.TowerFreq )
      text = text .. string.format( "Marshal radio %.3f MHz\n", self.MarshalFreq )
      text = text .. string.format( "LSO radio %.3f MHz\n", self.LSOFreq )
      text = text .. string.format( "TACAN Channel %s\n", tacan )
      text = text .. string.format( "ICLS Channel %s\n", icls )
      if tankertext then
        text = text .. tankertext .. "\n"
      end
      text = text .. string.format( "# A/C total %d (%d)\n", Ntotal, ntotal )
      text = text .. string.format( "# A/C marshal %d (%d)\n", Nmarshal, nmarshal )
      text = text .. string.format( "# A/C pattern %d (%d) - spinning %d (%d)\n", Npattern, npattern, Nspinning, nspinning )
      text = text .. string.format( "# A/C waiting %d (%d)\n", Nwaiting, nwaiting )
      text = text .. string.format( recoverytext )
      self:T2( self.lid .. text )

      -- Send message.
      self:MessageToPlayer( playerData, text, nil, "", 30, true )

    else
      self:E( self.lid .. string.format( "ERROR: Could not get player data for player %s.", playername ) )
    end
  end

end

--- Report weather conditions at the carrier location. Temperature, QFE pressure and wind data.
-- @param #AIRBOSS self
-- @param #string _unitname Name of the player unit.
function AIRBOSS:_DisplayCarrierWeather( _unitname )
  self:F2( _unitname )

  -- Get player unit and player name.
  local unit, playername = self:_GetPlayerUnitAndName( _unitname )

  -- Check if we have a player.
  if unit and playername then

    -- Message text.
    local text = ""

    -- Current coordinates.
    local coord = self:GetCoordinate()

    -- Get atmospheric data at carrier location.
    local T = coord:GetTemperature()
    local P = coord:GetPressure()

    -- Get wind direction (magnetic) and strength.
    local Wd, Ws = self:GetWind( nil, true )

    -- Get Beaufort wind scale.
    local Bn, Bd = UTILS.BeaufortScale( Ws )

    -- Wind on flight deck.
    local WodPA, WodPP = self:GetWindOnDeck()
    local WodPA = UTILS.MpsToKnots( WodPA )
    local WodPP = UTILS.MpsToKnots( WodPP )

    local WD = string.format( '%03d°', Wd )
    local Ts = string.format( "%d°C", T )

    local tT = string.format( "%d°C", T )
    local tW = string.format( "%.1f knots", UTILS.MpsToKnots( Ws ) )
    local tP = string.format( "%.2f inHg", UTILS.hPa2inHg( P ) )

    -- Report text.
    text = text .. string.format( "Weather Report at Carrier %s:\n", self.alias )
    text = text .. string.format( "================================\n" )
    text = text .. string.format( "Temperature %s\n", tT )
    text = text .. string.format( "Wind from %s at %s (%s)\n", WD, tW, Bd )
    text = text .. string.format( "Wind on deck || %.1f kts, == %.1f kts\n", WodPA, WodPP )
    text = text .. string.format( "QFE %.1f hPa = %s", P, tP )

    -- More info only reliable if Mission uses static weather.
    if self.staticweather then
      local clouds, visibility, fog, dust = self:_GetStaticWeather()
      text = text .. string.format( "\nVisibility %.1f NM", UTILS.MetersToNM( visibility ) )
      text = text .. string.format( "\nCloud base %d ft", UTILS.MetersToFeet( clouds.base ) )
      text = text .. string.format( "\nCloud thickness %d ft", UTILS.MetersToFeet( clouds.thickness ) )
      text = text .. string.format( "\nCloud density %d", clouds.density )
      text = text .. string.format( "\nPrecipitation %d", clouds.iprecptns )
      if fog then
        text = text .. string.format( "\nFog thickness %d ft", UTILS.MetersToFeet( fog.thickness ) )
        text = text .. string.format( "\nFog visibility %d ft", UTILS.MetersToFeet( fog.visibility ) )
      else
        text = text .. string.format( "\nNo fog" )
      end
      if dust then
        text = text .. string.format( "\nDust density %d", dust )
      else
        text = text .. string.format( "\nNo dust" )
      end
    end

    -- Debug output.
    self:T2( self.lid .. text )

    -- Send message to player group.
    self:MessageToPlayer( self.players[playername], text, nil, "", 30, true )

  else
    self:E( self.lid .. string.format( "ERROR! Could not find player unit in CarrierWeather! Unit name = %s", _unitname ) )
  end
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- HELP MENU
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set difficulty level.
-- @param #AIRBOSS self
-- @param #string _unitname Name of the player unit.
-- @param #AIRBOSS.Difficulty difficulty Difficulty level.
function AIRBOSS:_SetDifficulty( _unitname, difficulty )
  self:T2( { difficulty = difficulty, unitname = _unitname } )

  -- Get player unit and player name.
  local unit, playername = self:_GetPlayerUnitAndName( _unitname )

  -- Check if we have a player.
  if unit and playername then

    -- Player data.
    local playerData = self.players[playername] -- #AIRBOSS.PlayerData

    if playerData then
      playerData.difficulty = difficulty
      local text = string.format( "roger, your skill level is now: %s.", difficulty )
      self:MessageToPlayer( playerData, text, nil, playerData.name, 5 )
    else
      self:E( self.lid .. string.format( "ERROR: Could not get player data for player %s.", playername ) )
    end

    -- Set hints as well.
    if playerData.difficulty == AIRBOSS.Difficulty.HARD then
      playerData.showhints = false
    else
      playerData.showhints = true
    end

  end
end

--- Turn player's aircraft attitude display on or off.
-- @param #AIRBOSS self
-- @param #string _unitname Name of the player unit.
function AIRBOSS:_SetHintsOnOff( _unitname )
  self:F2( _unitname )

  -- Get player unit and player name.
  local unit, playername = self:_GetPlayerUnitAndName( _unitname )

  -- Check if we have a player.
  if unit and playername then

    -- Player data.
    local playerData = self.players[playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Invert hints.
      playerData.showhints = not playerData.showhints

      -- Inform player.
      local text = ""
      if playerData.showhints == true then
        text = string.format( "roger, hints are now ON." )
      else
        text = string.format( "affirm, hints are now OFF." )
      end
      self:MessageToPlayer( playerData, text, nil, playerData.name, 5 )

    end
  end
end

--- Turn player's aircraft attitude display on or off.
-- @param #AIRBOSS self
-- @param #string _unitname Name of the player unit.
function AIRBOSS:_DisplayAttitude( _unitname )
  self:F2( _unitname )

  -- Get player unit and player name.
  local unit, playername = self:_GetPlayerUnitAndName( _unitname )

  -- Check if we have a player.
  if unit and playername then

    -- Player data.
    local playerData = self.players[playername] -- #AIRBOSS.PlayerData

    if playerData then
      playerData.attitudemonitor = not playerData.attitudemonitor
    end
  end

end

--- Turn radio subtitles of player on or off.
-- @param #AIRBOSS self
-- @param #string _unitname Name of the player unit.
function AIRBOSS:_SubtitlesOnOff( _unitname )
  self:F2( _unitname )

  -- Get player unit and player name.
  local unit, playername = self:_GetPlayerUnitAndName( _unitname )

  -- Check if we have a player.
  if unit and playername then

    -- Player data.
    local playerData = self.players[playername] -- #AIRBOSS.PlayerData

    if playerData then
      playerData.subtitles = not playerData.subtitles
      -- Inform player.
      local text = ""
      if playerData.subtitles == true then
        text = string.format( "roger, subtitiles are now ON." )
      elseif playerData.subtitles == false then
        text = string.format( "affirm, subtitiles are now OFF." )
      end
      self:MessageToPlayer( playerData, text, nil, playerData.name, 5 )
    end
  end

end

--- Turn radio subtitles of player on or off.
-- @param #AIRBOSS self
-- @param #string _unitname Name of the player unit.
function AIRBOSS:_TrapsheetOnOff( _unitname )
  self:F2( _unitname )

  -- Get player unit and player name.
  local unit, playername = self:_GetPlayerUnitAndName( _unitname )

  -- Check if we have a player.
  if unit and playername then

    -- Player data.
    local playerData = self.players[playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Check if option is enabled at all.
      local text = ""
      if self.trapsheet then

        -- Invert current setting.
        playerData.trapon = not playerData.trapon

        -- Inform player.
        if playerData.trapon == true then
          text = string.format( "roger, your trapsheets are now SAVED." )
        else
          text = string.format( "affirm, your trapsheets are NOT SAVED." )
        end

      else
        text = "negative, trap sheet data recorder is broken on this carrier."
      end

      -- Message to player.
      self:MessageToPlayer( playerData, text, nil, playerData.name, 5 )
    end
  end

end

--- Display player status.
-- @param #AIRBOSS self
-- @param #string _unitName Name of the player unit.
function AIRBOSS:_DisplayPlayerStatus( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Pattern step text.
      local steptext = playerData.step
      if playerData.step == AIRBOSS.PatternStep.HOLDING then
        if playerData.holding == nil then
          steptext = "Transit to Marshal"
        elseif playerData.holding == false then
          steptext = "Marshal (outside zone)"
        elseif playerData.holding == true then
          steptext = "Marshal Stack Holding"
        end
      end

      -- Stack.
      local stack = playerData.flag

      -- Stack text.
      local stacktext = nil
      if stack > 0 then
        local stackalt = self:_GetMarshalAltitude( stack )
        local angels = self:_GetAngels( stackalt )
        stacktext = string.format( "Marshal Stack %d, Angels %d\n", stack, angels )

        -- Hint about TACAN bearing.
        if playerData.step == AIRBOSS.PatternStep.HOLDING and playerData.case > 1 then
          -- Get inverse magnetic radial potential offset.
          local radial = self:GetRadial( playerData.case, true, true, true )
          stacktext = stacktext .. string.format( "Select TACAN %03d°, %d DME\n", radial, angels + 15 )
        end
      end

      -- Fuel and fuel state.
      local fuel = playerData.unit:GetFuel() * 100
      local fuelstate = self:_GetFuelState( playerData.unit )

      -- Number of units in group.
      local _, nunitsGround = self:_GetFlightUnits( playerData, true )
      local _, nunitsAirborne = self:_GetFlightUnits( playerData, false )

      -- Player data.
      local text = string.format( "Status of player %s (%s)\n", playerData.name, playerData.callsign )
      text = text .. string.format( "================================\n" )
      text = text .. string.format( "Step: %s\n", steptext )
      if stacktext then
        text = text .. stacktext
      end
      text = text .. string.format( "Recovery Case: %d\n", playerData.case )
      text = text .. string.format( "Skill Level: %s\n", playerData.difficulty )
      text = text .. string.format( "Modex: %s (%s)\n", playerData.onboard, self:_GetACNickname( playerData.actype ) )
      text = text .. string.format( "Fuel State: %.1f lbs/1000 (%.1f %%)\n", fuelstate / 1000, fuel )
      text = text .. string.format( "# units: %d (%d airborne)\n", nunitsGround, nunitsAirborne )
      text = text .. string.format( "Section Lead: %s (%d/%d)", tostring( playerData.seclead ), #playerData.section + 1, self.NmaxSection + 1 )
      for _, _sec in pairs( playerData.section ) do
        local sec = _sec -- #AIRBOSS.PlayerData
        text = text .. string.format( "\n- %s", sec.name )
      end

      if playerData.step == AIRBOSS.PatternStep.INITIAL then

        -- Create a point 3.0 NM astern for re-entry.
        local zoneinitial = self:GetCoordinate():Translate( UTILS.NMToMeters( 3.5 ), self:GetRadial( 2, false, false, false ) )

        -- Heading and distance to initial zone.
        local flyhdg = playerData.unit:GetCoordinate():HeadingTo( zoneinitial )
        local flydist = UTILS.MetersToNM( playerData.unit:GetCoordinate():Get2DDistance( zoneinitial ) )
        local brc = self:GetBRC()

        -- Help player to find its way to the initial zone.
        text = text .. string.format( "\nTo Initial: Fly heading %03d° for %.1f NM and turn to BRC %03d°", flyhdg, flydist, brc )

      elseif playerData.step == AIRBOSS.PatternStep.PLATFORM then

        -- Coordinate of the platform zone.
        local zoneplatform = self:_GetZonePlatform( playerData.case ):GetCoordinate()

        -- Heading and distance to platform zone.
        local flyhdg = playerData.unit:GetCoordinate():HeadingTo( zoneplatform )
        local flydist = UTILS.MetersToNM( playerData.unit:GetCoordinate():Get2DDistance( zoneplatform ) )

        -- Get heading.
        local hdg = self:GetRadial( playerData.case, true, true, true )

        -- Help player to find its way to the initial zone.
        text = text .. string.format( "\nTo Platform: Fly heading %03d° for %.1f NM and turn to %03d°", flyhdg, flydist, hdg )

      end

      -- Send message.
      self:MessageToPlayer( playerData, text, nil, "", 30, true )
    else
      self:E( self.lid .. string.format( "ERROR: playerData=nil. Unit name=%s, player name=%s", _unitName, _playername ) )
    end
  else
    self:E( self.lid .. string.format( "ERROR: could not find player for unit %s", _unitName ) )
  end

end

--- Mark current marshal zone of player by either smoke or flares.
-- @param #AIRBOSS self
-- @param #string _unitName Name of the player unit.
-- @param #boolean flare If true, flare the zone. If false, smoke the zone.
function AIRBOSS:_MarkMarshalZone( _unitName, flare )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Get player stack and recovery case.
      local stack = playerData.flag
      local case = playerData.case

      local text = ""
      if stack > 0 then

        -- Get current holding zone.
        local zoneHolding = self:_GetZoneHolding( case, stack )

        -- Get Case I commence zone at three position.
        local zoneThree = self:_GetZoneCommence( case, stack )

        -- Pattern altitude.
        local patternalt = self:_GetMarshalAltitude( stack, case )

        -- Flare and smoke at the ground.
        patternalt = 5

        -- Roger!
        text = "roger, marking"
        if flare then

          -- Marshal WHITE flares.
          text = text .. string.format( "\n* Marshal zone stack %d with WHITE flares.", stack )
          zoneHolding:FlareZone( FLARECOLOR.White, 45, nil, patternalt )

          -- Commence RED flares.
          text = text .. "\n* Commence zone with RED flares."
          zoneThree:FlareZone( FLARECOLOR.Red, 45, nil, patternalt )

        else

          -- Marshal WHITE smoke.
          text = text .. string.format( "\n* Marshal zone stack %d with WHITE smoke.", stack )
          zoneHolding:SmokeZone( SMOKECOLOR.White, 45, patternalt )

          -- Commence RED smoke
          text = text .. "\n* Commence zone with RED smoke."
          zoneThree:SmokeZone( SMOKECOLOR.Red, 45, patternalt )

        end

      else
        text = "negative, you are currently not in a Marshal stack. No zones will be marked!"
      end

      -- Send message to player.
      self:MessageToPlayer( playerData, text, "MARSHAL", playerData.name )
    end
  end

end

--- Mark CASE I or II/II zones by either smoke or flares.
-- @param #AIRBOSS self
-- @param #string _unitName Name of the player unit.
-- @param #boolean flare If true, flare the zone. If false, smoke the zone.
function AIRBOSS:_MarkCaseZones( _unitName, flare )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData

    if playerData then

      -- Player's recovery case.
      local case = playerData.case

      -- Initial
      local text = string.format( "affirm, marking CASE %d zones", case )

      -- Flare or smoke?
      if flare then

        -----------
        -- Flare --
        -----------

        -- Case I/II: Initial
        if case == 1 or case == 2 then
          text = text .. "\n* initial with GREEN flares"
          self:_GetZoneInitial( case ):FlareZone( FLARECOLOR.Green, 45 )
        end

        -- Case II/III: approach corridor
        if case == 2 or case == 3 then
          text = text .. "\n* approach corridor with GREEN flares"
          self:_GetZoneCorridor( case ):FlareZone( FLARECOLOR.Green, 45 )
        end

        -- Case II/III: platform
        if case == 2 or case == 3 then
          text = text .. "\n* platform with RED flares"
          self:_GetZonePlatform( case ):FlareZone( FLARECOLOR.Red, 45 )
        end

        -- Case III: dirty up
        if case == 3 then
          text = text .. "\n* dirty up with YELLOW flares"
          self:_GetZoneDirtyUp( case ):FlareZone( FLARECOLOR.Yellow, 45 )
        end

        -- Case II/III: arc in/out
        if case == 2 or case == 3 then
          if math.abs( self.holdingoffset ) > 0 then
            self:_GetZoneArcIn( case ):FlareZone( FLARECOLOR.White, 45 )
            text = text .. "\n* arc turn in with WHITE flares"
            self:_GetZoneArcOut( case ):FlareZone( FLARECOLOR.White, 45 )
            text = text .. "\n* arc turn out with WHITE flares"
          end
        end

        -- Case III: bullseye
        if case == 3 then
          text = text .. "\n* bullseye with GREEN flares"
          self:_GetZoneBullseye( case ):FlareZone( FLARECOLOR.Green, 45 )
        end

        -- Tarawa, LHA and LHD landing spots.
        if self.carriertype == AIRBOSS.CarrierType.INVINCIBLE or self.carriertype == AIRBOSS.CarrierType.HERMES or self.carriertype == AIRBOSS.CarrierType.TARAWA or self.carriertype == AIRBOSS.CarrierType.AMERICA or self.carriertype == AIRBOSS.CarrierType.JCARLOS or self.carriertype == AIRBOSS.CarrierType.CANBERRA then
          text = text .. "\n* abeam landing stop with RED flares"
          -- Abeam landing spot zone.
          local ALSPT = self:_GetZoneAbeamLandingSpot()
          ALSPT:FlareZone( FLARECOLOR.Red, 5, nil, UTILS.FeetToMeters( 110 ) )
          -- Primary landing spot zone.
          text = text .. "\n* primary landing spot with GREEN flares"
          local LSPT = self:_GetZoneLandingSpot()
          LSPT:FlareZone( FLARECOLOR.Green, 5, nil, self.carrierparam.deckheight )
        end

      else

        -----------
        -- Smoke --
        -----------

        -- Case I/II: Initial
        if case == 1 or case == 2 then
          text = text .. "\n* initial with GREEN smoke"
          self:_GetZoneInitial( case ):SmokeZone( SMOKECOLOR.Green, 45 )
        end

        -- Case II/III: Approach Corridor
        if case == 2 or case == 3 then
          text = text .. "\n* approach corridor with GREEN smoke"
          self:_GetZoneCorridor( case ):SmokeZone( SMOKECOLOR.Green, 45 )
        end

        -- Case II/III: platform
        if case == 2 or case == 3 then
          text = text .. "\n* platform with RED smoke"
          self:_GetZonePlatform( case ):SmokeZone( SMOKECOLOR.Red, 45 )
        end

        -- Case II/III: arc in/out if offset>0.
        if case == 2 or case == 3 then
          if math.abs( self.holdingoffset ) > 0 then
            self:_GetZoneArcIn( case ):SmokeZone( SMOKECOLOR.Blue, 45 )
            text = text .. "\n* arc turn in with BLUE smoke"
            self:_GetZoneArcOut( case ):SmokeZone( SMOKECOLOR.Blue, 45 )
            text = text .. "\n* arc turn out with BLUE smoke"
          end
        end

        -- Case III: dirty up
        if case == 3 then
          text = text .. "\n* dirty up with ORANGE smoke"
          self:_GetZoneDirtyUp( case ):SmokeZone( SMOKECOLOR.Orange, 45 )
        end

        -- Case III: bullseye
        if case == 3 then
          text = text .. "\n* bullseye with GREEN smoke"
          self:_GetZoneBullseye( case ):SmokeZone( SMOKECOLOR.Green, 45 )
        end

      end

      -- Send message to player.
      self:MessageToPlayer( playerData, text, "MARSHAL", playerData.name )
    end
  end

end

--- LSO radio check. Will broadcase LSO message at given LSO frequency.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
function AIRBOSS:_LSORadioCheck( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData
    if playerData then
      -- Broadcase LSO radio check message on LSO radio.
      self:RadioTransmission( self.LSORadio, self.LSOCall.RADIOCHECK, nil, nil, nil, true )
    end
  end
end

--- Marshal radio check. Will broadcase Marshal message at given Marshal frequency.
-- @param #AIRBOSS self
-- @param #string _unitName Name fo the player unit.
function AIRBOSS:_MarshalRadioCheck( _unitName )
  self:F( _unitName )

  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )

  -- Check if we have a unit which is a player.
  if _unit and _playername then
    local playerData = self.players[_playername] -- #AIRBOSS.PlayerData
    if playerData then
      -- Broadcase Marshal radio check message on Marshal radio.
      self:RadioTransmission( self.MarshalRadio, self.MarshalCall.RADIOCHECK, nil, nil, nil, true )
    end
  end
end

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Persistence Functions
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Save trapsheet data.
-- @param #AIRBOSS self
-- @param #AIRBOSS.PlayerData playerData Player data table.
-- @param #AIRBOSS.LSOgrade grade LSO grad data.
function AIRBOSS:_SaveTrapSheet( playerData, grade )

  -- Nothing to save.
  if playerData.trapsheet == nil or #playerData.trapsheet == 0 or not io then
    return
  end

  --- Function that saves data to file
  local function _savefile( filename, data )
    local f = io.open( filename, "wb" )
    if f then
      f:write( data )
      f:close()
    else
      self:E( self.lid .. string.format( "ERROR: could not save trap sheet to file %s.\nFile may contain invalid characters.", tostring( filename ) ) )
    end
  end

  -- Set path or default.
  local path = self.trappath
  if lfs then
    path = path or lfs.writedir()
  end

  -- Create unused file name.
  local filename = nil
  for i = 1, 9999 do

    -- Create file name
    if self.trapprefix then
      filename = string.format( "%s_%s-%04d.csv", self.trapprefix, playerData.actype, i )
    else
      local name = UTILS.ReplaceIllegalCharacters( playerData.name, "_" )
      filename = string.format( "AIRBOSS-%s_Trapsheet-%s_%s-%04d.csv", self.alias, name, playerData.actype, i )
    end

    -- Set path.
    if path ~= nil then
      filename = path .. "\\" .. filename
    end

    -- Check if file exists.
    local _exists = UTILS.FileExists( filename )
    if not _exists then
      break
    end
  end

  -- Info
  local text = string.format( "Saving player %s trapsheet to file %s", playerData.name, filename )
  self:I( self.lid .. text )

  -- Header line
  local data = "#Time,Rho,X,Z,Alt,AoA,GSE,LUE,Vtot,Vy,Gamma,Pitch,Roll,Yaw,Step,Grade,Points,Details\n"

  local g0 = playerData.trapsheet[1] -- #AIRBOSS.GrooveData
  local T0 = g0.Time

  -- for _,_groove in ipairs(playerData.trapsheet) do
  for i = 1, #playerData.trapsheet do
    -- local groove=_groove --#AIRBOSS.GrooveData
    local groove = playerData.trapsheet[i]
    local t = groove.Time - T0
    local a = UTILS.MetersToNM( groove.Rho or 0 )
    local b = -groove.X or 0
    local c = groove.Z or 0
    local d = UTILS.MetersToFeet( groove.Alt or 0 )
    local e = groove.AoA or 0
    local f = groove.GSE or 0
    local g = -groove.LUE or 0
    local h = UTILS.MpsToKnots( groove.Vel or 0 )
    local i = (groove.Vy or 0) * 196.85
    local j = groove.Gamma or 0
    local k = groove.Pitch or 0
    local l = groove.Roll or 0
    local m = groove.Yaw or 0
    local n = self:_GS( groove.Step, -1 ) or "n/a"
    local o = groove.Grade or "n/a"
    local p = groove.GradePoints or 0
    local q = groove.GradeDetail or "n/a"
    --                              t    a    b    c    d    e    f    g    h    i    j    k    l    m   n  o   p   q
    data = data .. string.format( "%.2f,%.3f,%.1f,%.1f,%.1f,%.2f,%.2f,%.2f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%s,%s,%.1f,%s\n", t, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q )
  end

  -- Save file.
  _savefile( filename, data )
end

--- On before "Save" event. Checks if io and lfs are available.
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-<ALIAS>_LSOgrades.csv".
function AIRBOSS:onbeforeSave( From, Event, To, path, filename )

  -- Check io module is available.
  if not io then
    self:E( self.lid .. "ERROR: io not desanitized. Can't save player grades." )
    return false
  end

  -- Check default path.
  if path == nil and not lfs then
    self:E( self.lid .. "WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder." )
  end

  return true
end

--- On after "Save" event. Player data is saved to file.
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory or your "Saved Games" folder if lfs was desanitized.
-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-<ALIAS>_LSOgrades.csv".
function AIRBOSS:onafterSave( From, Event, To, path, filename )

  --- Function that saves data to file
  local function _savefile( filename, data )
    local f = assert( io.open( filename, "wb" ) )
    f:write( data )
    f:close()
  end

  -- Set path or default.
  if lfs then
    path = path or lfs.writedir()
  end

  -- Set file name.
  filename = filename or string.format( "AIRBOSS-%s_LSOgrades.csv", self.alias )

  -- Set path.
  if path ~= nil then
    filename = path .. "\\" .. filename
  end

  -- Header line
  local scores = "Name,Pass,Points Final,Points Pass,Grade,Details,Wire,Tgroove,Case,Wind,Modex,Airframe,Carrier Type,Carrier Name,Theatre,Mission Time,Mission Date,OS Date\n"

  -- Loop over all players.
  local n = 0
  for playername, grades in pairs( self.playerscores ) do

    -- Loop over player grades table.
    for i, _grade in pairs( grades ) do
      local grade = _grade -- #AIRBOSS.LSOgrade

      -- Check some stuff that could be nil.
      local wire = "n/a"
      if grade.wire and grade.wire <= 4 then
        wire = tostring( grade.wire )
      end

      local Tgroove = "n/a"
      if grade.Tgroove and grade.Tgroove <= 360 and grade.case < 3 then
        Tgroove = tostring( UTILS.Round( grade.Tgroove, 1 ) )
      end

      local finalscore = "n/a"
      if grade.finalscore then
        finalscore = tostring( UTILS.Round( grade.finalscore, 1 ) )
      end

      -- Compile grade line.
      scores = scores .. string.format( "%s,%d,%s,%.1f,%s,%s,%s,%s,%d,%s,%s,%s,%s,%s,%s,%s,%s,%s\n", playername, i, finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, grade.case, grade.wind, grade.modex, grade.airframe, grade.carriertype, grade.carriername, grade.theatre, grade.mitime, grade.midate, grade.osdate )
      n = n + 1
    end
  end

  -- Info
  local text = string.format( "Saving %d player LSO grades to file %s", n, filename )
  self:I( self.lid .. text )

  -- Save file.
  _savefile( filename, scores )
end

--- On before "Load" event. Checks if the file that the player grades from exists.
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string path (Optional) Path where the file is loaded from. Default is the DCS installation root directory or your "Saved Games\\DCS" folder if lfs was desanizized.
-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-<ALIAS>_LSOgrades.csv".
function AIRBOSS:onbeforeLoad( From, Event, To, path, filename )

  --- Function that check if a file exists.
  local function _fileexists( name )
    local f = io.open( name, "r" )
    if f ~= nil then
      io.close( f )
      return true
    else
      return false
    end
  end

  -- Check io module is available.
  if not io then
    self:E( self.lid .. "WARNING: io not desanitized. Can't load player grades." )
    return false
  end

  -- Check default path.
  if path == nil and not lfs then
    self:E( self.lid .. "WARNING: lfs not desanitized. Results will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder." )
  end

  -- Set path or default.
  if lfs then
    path = path or lfs.writedir()
  end

  -- Set file name.
  filename = filename or string.format( "AIRBOSS-%s_LSOgrades.csv", self.alias )

  -- Set path.
  if path ~= nil then
    filename = path .. "\\" .. filename
  end

  -- Check if file exists.
  local exists = _fileexists( filename )

  if exists then
    return true
  else
    self:E( self.lid .. string.format( "WARNING: Player LSO grades file %s does not exist.", filename ) )
    return false
  end

end

--- On after "Load" event. Loads grades of all players from file.
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string path Path where the file is loaded from. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if lfs was desanizied.
-- @param #string filename (Optional) File name for saving the player grades. Default is "AIRBOSS-<ALIAS>_LSOgrades.csv".
function AIRBOSS:onafterLoad( From, Event, To, path, filename )

  --- Function that load data from a file.
  local function _loadfile( filename )
    local f = assert( io.open( filename, "rb" ) )
    local data = f:read( "*all" )
    f:close()
    return data
  end

  -- Set path or default.
  if lfs then
    path = path or lfs.writedir()
  end

  -- Set file name.
  filename = filename or string.format( "AIRBOSS-%s_LSOgrades.csv", self.alias )

  -- Set path.
  if path ~= nil then
    filename = path .. "\\" .. filename
  end

  -- Info message.
  local text = string.format( "Loading player LSO grades from file %s", filename )
  MESSAGE:New( text, 10 ):ToAllIf( self.Debug )
  self:I( self.lid .. text )

  -- Load asset data from file.
  local data = _loadfile( filename )

  -- Split by line break.
  local playergrades = UTILS.Split( data, "\n" )

  -- Remove first header line.
  table.remove( playergrades, 1 )

  -- Init player scores table.
  self.playerscores = {}

  -- Loop over all lines.
  local n = 0
  for _, gradeline in pairs( playergrades ) do

    -- Parameters are separated by commata.
    local gradedata = UTILS.Split( gradeline, "," )

    -- Debug info.
    self:T2( gradedata )

    -- Grade table
    local grade = {} -- #AIRBOSS.LSOgrade

    --- Line format:
    -- playername, i, grade.finalscore, grade.points, grade.grade, grade.details, wire, Tgroove, case,
    -- time, wind, airframe, modex, carriertype, carriername, theatre, date
    local playername = gradedata[1]
    if gradedata[3] ~= nil and gradedata[3] ~= "n/a" then
      grade.finalscore = tonumber( gradedata[3] )
    end
    grade.points = tonumber( gradedata[4] )
    grade.grade = tostring( gradedata[5] )
    grade.details = tostring( gradedata[6] )
    if gradedata[7] ~= nil and gradedata[7] ~= "n/a" then
      grade.wire = tonumber( gradedata[7] )
    end
    if gradedata[8] ~= nil and gradedata[8] ~= "n/a" then
      grade.Tgroove = tonumber( gradedata[8] )
    end
    grade.case = tonumber( gradedata[9] )
    -- new
    grade.wind = gradedata[10] or "n/a"
    grade.modex = gradedata[11] or "n/a"
    grade.airframe = gradedata[12] or "n/a"
    grade.carriertype = gradedata[13] or "n/a"
    grade.carriername = gradedata[14] or "n/a"
    grade.theatre = gradedata[15] or "n/a"
    grade.mitime = gradedata[16] or "n/a"
    grade.midate = gradedata[17] or "n/a"
    grade.osdate = gradedata[18] or "n/a"

    -- Init player table if necessary.
    self.playerscores[playername] = self.playerscores[playername] or {}

    -- Add grade to table.
    table.insert( self.playerscores[playername], grade )

    n = n + 1

    -- Debug info.
    self:T2( { playername, self.playerscores[playername] } )
  end

  -- Info message.
  local text = string.format( "Loaded %d player LSO grades from file %s", n, filename )
  self:I( self.lid .. text )

end

--- On after "LSOGrade" event.
-- @param #AIRBOSS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #AIRBOSS.PlayerData playerData Player Data.
-- @param #AIRBOSS.LSOgrade grade LSO grade.
function AIRBOSS:onafterLSOGrade(From, Event, To, playerData, grade)

  if self.funkmanSocket then

    -- Extract used info for FunkMan. We need to be careful with the amount of data send via UDP socket.
    local trapsheet={} ; trapsheet.X={} ; trapsheet.Z={} ; trapsheet.AoA={} ; trapsheet.Alt={}

    -- Loop over trapsheet and extract used values.
    for i = 1, #playerData.trapsheet do
      local ts=playerData.trapsheet[i] --#AIRBOSS.GrooveData
      table.insert(trapsheet.X, UTILS.Round(ts.X, 1))
      table.insert(trapsheet.Z, UTILS.Round(ts.Z, 1))
      table.insert(trapsheet.AoA, UTILS.Round(ts.AoA, 2))
      table.insert(trapsheet.Alt, UTILS.Round(ts.Alt, 1))
    end

    local result={}
    result.command=SOCKET.DataType.LSOGRADE
    result.name=playerData.name
    result.trapsheet=trapsheet
    result.airframe=grade.airframe
    result.mitime=grade.mitime
    result.midate=grade.midate
    result.wind=grade.wind
    result.carriertype=grade.carriertype
    result.carriername=grade.carriername
    result.carrierrwy=grade.carrierrwy
    result.landingdist=self.carrierparam.landingdist
    result.theatre=grade.theatre
    result.case=playerData.case
    result.Tgroove=grade.Tgroove
    result.wire=grade.wire
    result.grade=grade.grade
    result.points=grade.points
    result.details=grade.details

    -- Debug info.
    self:T(self.lid.."Result onafterLSOGrade")
    self:T(result)

    -- Send result.
    self.funkmanSocket:SendTable(result)
  end

end--- **Ops** - Recovery tanker for carrier operations.
-- 
-- Tanker aircraft flying a racetrack pattern overhead an aircraft carrier.
--
-- **Main Features:**
--
--    * Regular pattern update with respect to carrier position.
--    * No restrictions regarding carrier waypoints and heading.
--    * Automatic respawning when tanker runs out of fuel for 24/7 operations.
--    * Tanker can be spawned cold or hot on the carrier or at any other airbase or directly in air.
--    * Automatic AA TACAN beacon setting.
--    * Multiple tankers at the same carrier.
--    * Multiple carriers due to object oriented approach.
--    * Finite State Machine (FSM) implementation, which allows the mission designer to hook into certain events.
--
-- ===
--
-- ### Author: **funkyfranky** 
-- ### Special thanks to **HighwaymanEd** for testing and suggesting improvements!
--
-- @module Ops.RecoveryTanker
-- @image Ops_RecoveryTanker.png

--- RECOVERYTANKER class.
-- @type RECOVERYTANKER
-- @field #string ClassName Name of the class.
-- @field #boolean Debug Debug mode.
-- @field #string lid Log debug id text.
-- @field Wrapper.Unit#UNIT carrier The carrier the tanker is attached to.
-- @field #string carriertype Carrier type.
-- @field #string tankergroupname Name of the late activated tanker template group.
-- @field Wrapper.Group#GROUP tanker Tanker group.
-- @field Wrapper.Airbase#AIRBASE airbase The home airbase object of the tanker. Normally the aircraft carrier.
-- @field Core.Beacon#BEACON beacon Tanker TACAN beacon.
-- @field #number TACANchannel TACAN channel. Default 1.
-- @field #string TACANmode TACAN mode, i.e. "X" or "Y". Default "Y". Use only "Y" for AA TACAN stations!
-- @field #string TACANmorse TACAN morse code. Three letters identifying the TACAN station. Default "TKR".
-- @field #boolean TACANon If true, TACAN is automatically activated. If false, TACAN is disabled.
-- @field #number RadioFreq Radio frequency in MHz of the tanker. Default 251 MHz.
-- @field #string RadioModu Radio modulation "AM" or "FM". Default "AM".
-- @field #number speed Tanker speed when flying pattern.
-- @field #number altitude Tanker orbit pattern altitude.
-- @field #number distStern Race-track distance astern. distStern is <0.
-- @field #number distBow Race-track distance bow. distBow is >0.
-- @field #number Dupdate Pattern update when carrier changes its position by more than this distance (meters).
-- @field #number Hupdate Pattern update when carrier changes its heading by more than this number (degrees).
-- @field #number dTupdate Minimum time interval in seconds before the next pattern update can happen.
-- @field #number Tupdate Last time the pattern was updated.
-- @field #number takeoff Takeoff type (cold, hot, air).
-- @field #number lowfuel Low fuel threshold in percent.
-- @field #boolean respawn If true, tanker be respawned (default). If false, no respawning will happen.
-- @field #boolean respawninair If true, tanker will always be respawned in air. This has no impact on the initial spawn setting.
-- @field #boolean uncontrolledac If true, use and uncontrolled tanker group already present in the mission.
-- @field DCS#Vec3 orientation Orientation of the carrier. Used to monitor changes and update the pattern if heading changes significantly.
-- @field DCS#Vec3 orientlast Orientation of the carrier for checking if carrier is currently turning.
-- @field Core.Point#COORDINATE position Position of carrier. Used to monitor if carrier significantly changed its position and then update the tanker pattern.
-- @field #string alias Alias of the spawn group.
-- @field #number uid Unique ID of this tanker.
-- @field #boolean awacs If true, the groups gets the enroute task AWACS instead of tanker.
-- @field #number callsignname Number for the callsign name.
-- @field #number callsignnumber Number of the callsign name.
-- @field #string modex Tail number of the tanker.
-- @field #boolean eplrs If true, enable data link, e.g. if used as AWACS.
-- @field #boolean recovery If true, tanker will recover using the AIRBOSS marshal pattern.
-- @field #number terminaltype Terminal type of used parking spots on airbases.
-- @field #boolean unlimitedfuel If true, the tanker will have unlimited fuel.
-- @extends Core.Fsm#FSM

--- Recovery Tanker.
--
-- ===
--
-- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Main.png)
--
-- # Recovery Tanker
--
-- A recovery tanker acts as refueling unit flying overhead an aircraft carrier in order to supply incoming flights with gas if they go "*Bingo on the Ball*".
-- 
-- # Simple Script
-- 
-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named **"USS Stennis"**.
-- 
-- Secondly, you need to define a recovery tanker group in the mission editor and set it to **"LATE ACTIVATED"**. The name of the group we'll use is **"Texaco"**.
-- 
-- The basic script is very simple and consists of only two lines: 
-- 
--     TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco")
--     TexacoStennis:Start()
--
-- The first line will create a new RECOVERYTANKER object and the second line starts the process.
-- 
-- With this setup, the tanker will be spawned on the USS Stennis with running engines. After it takes off, it will fly a position ~10 NM astern of the boat and from there start its
-- pattern. This is a counter clockwise racetrack pattern at angels 6.
-- 
-- A TACAN beacon will be automatically activated at channel 1Y with morse code "TKR". See below how to change this setting.
-- 
-- Note that the Tanker entry in the F10 radio menu will appear once the tanker is on station and not before. If you spawn the tanker cold or hot on the carrier, this will take ~10 minutes.
-- 
-- Also note, that currently the only carrier capable aircraft in DCS is the S-3B Viking (tanker version). If you want to use another refueling aircraft, you need to activate air spawn
-- or set a different land based airport of the map. This will be explained below.
-- 
-- ![Banner Image](..\Presentations\RECOVERYTANKER\RecoveryTanker_Pattern.jpg)
-- 
-- The "downwind" leg of the pattern is normally used for refueling.
-- 
-- Once the tanker runs out of fuel itself, it will return to the carrier, respawn with full fuel and take up its pattern again.
-- 
-- # Options and Fine Tuning
-- 
-- Several parameters can be customized by the mission designer via user API functions.
-- 
-- ## Takeoff Type
-- 
-- By default, the tanker is spawned with running engines on the carrier. The mission designer has set option to set the take off type via the @{#RECOVERYTANKER.SetTakeoff} function.
-- Or via shortcuts
-- 
--    * @{#RECOVERYTANKER.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default.
--    * @{#RECOVERYTANKER.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off.
--    * @{#RECOVERYTANKER.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the tanker will be spawned in air ~10 NM astern the carrier.  
-- 
-- For example,
--     TexacoStennis=RECOVERYTANKER:New(UNIT:FindByName("USS Stennis"), "Texaco")
--     TexacoStennis:SetTakeoffAir()
--     TexacoStennis:Start()
-- will spawn the tanker several nautical miles astern the carrier. From there it will start its pattern.
-- 
-- Spawning in air is not as realistic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck.
-- 
-- **Note** that when spawning in air is set, the tanker will also not return to the boat, once it is out of fuel. Instead it will be respawned directly in air.
-- 
-- If only the first spawning should happen on the carrier, one use the @{#RECOVERYTANKER.SetRespawnInAir}() function to command that all subsequent spawning
-- will happen in air.
-- 
-- If the tanker should not be respawned at all, one can set @{#RECOVERYTANKER.SetRespawnOff}().
-- 
-- ## Pattern Parameters
-- 
-- The racetrack pattern parameters can be fine tuned via the following functions:
-- 
--    * @{#RECOVERYTANKER.SetAltitude}(*altitude*), where *altitude* is the pattern altitude in feet. Default 6000 ft.
--    * @{#RECOVERYTANKER.SetSpeed}(*speed*), where *speed* is the pattern speed in knots. Default is 274 knots TAS which results in ~250 KIAS.
--    * @{#RECOVERYTANKER.SetRacetrackDistances}(*distbow*, *diststern*), where *distbow* and *diststern* are the distances ahead and astern the boat (default 10 and 4 NM), respectively.
--    In principle, these number should be more like 8 and 6 NM but since the carrier is moving, we give translate the pattern points a bit forward.
--    
-- ## Home Base
-- 
-- The home base is the airbase where the tanker is spawned (if not in air) and where it will go once it is running out of fuel. The default home base is the carrier itself.
-- The home base can be changed via the @{#RECOVERYTANKER.SetHomeBase}(*airbase*) function, where *airbase* can be a MOOSE @{Wrapper.Airbase#AIRBASE} object or simply the 
-- name of the airbase passed as string.
-- 
-- Note that only the S3B Viking is a refueling aircraft that is carrier capable. You can use other tanker aircraft types, e.g. the KC-130, but in this case you must either 
-- set an airport of the map as home base or activate spawning in air via @{#RECOVERYTANKER.SetTakeoffAir}.
--
-- ## TACAN
-- 
-- A TACAN beacon for the tanker can be activated via scripting, i.e. no need to do this within the mission editor.
-- 
-- The beacon is create with the @{#RECOVERYTANKER.SetTACAN}(*channel*, *morse*) function, where *channel* is the TACAN channel (a number), 
-- and *morse* a three letter string that is send as morse code to identify the tanker:
-- 
--     TexacoStennis:SetTACAN(10, "TKR")
--     
-- will activate a TACAN beacon 10Y with more code "TKR".
-- 
-- If you do not set a TACAN beacon explicitly, it is automatically create on channel 1Y and morse code "TKR".
-- The mode is *always* "Y" for AA TACAN stations since mode "X" does not work!
-- 
-- In order to completely disable the TACAN beacon, you can use the @{#RECOVERYTANKER.SetTACANoff}() function in your script.
-- 
-- ## Radio
-- 
-- The radio frequency on optionally modulation can be set via the @{#RECOVERYTANKER.SetRadio}(*frequency*, *modulation*) function. The first parameter denotes the radio frequency the tanker uses in MHz.
-- The second parameter is *optional* and sets the modulation to either AM (default) or FM.
-- 
-- For example,
-- 
--     TexacoStennis:SetRadio(260)
--
-- will set the frequency of the tanker to 260 MHz AM.
-- 
-- **Note** that if this is not set, the tanker frequency will be automatically set to **251 MHz AM**.
--
-- ## Pattern Update
-- 
-- The pattern of the tanker is updated if at least one of the two following conditions apply:
-- 
--    * The aircraft carrier changes its position by more than 5 NM (see @{#RECOVERYTANKER.SetPatternUpdateDistance}) and/or
--    * The aircraft carrier changes its heading by more than 5 degrees (see @{#RECOVERYTANKER.SetPatternUpdateHeading})
-- 
-- **Note** that updating the pattern often leads to a more or less small disruption of the perfect racetrack pattern of the tanker. This is because a new waypoint and new racetrack points
-- need to be set as DCS task. This is the reason why the pattern is not constantly updated but rather when the position or heading of the carrier changes significantly.
--
-- The maximum update frequency is set to 10 minutes. You can adjust this by @{#RECOVERYTANKER.SetPatternUpdateInterval}.
-- Also the pattern will not be updated whilst the carrier is turning or the tanker is currently refueling another unit.
-- 
-- ## Callsign
-- 
-- The callsign of the tanker can be set via the @{#RECOVERYTANKER.SetCallsign}(*callsignname*, *callsignnumber*) function. Both parameters are *numbers*.
-- The first parameter *callsignname* defines the name (1=Texaco, 2=Arco, 3=Shell). The second (optional) parameter specifies the first number and has to be between 1-9.
-- Also see [DCS_enum_callsigns](https://wiki.hoggitworld.com/view/DCS_enum_callsigns) and [DCS_command_setCallsign](https://wiki.hoggitworld.com/view/DCS_command_setCallsign).
-- 
--     TexacoStennis:SetCallsign(CALLSIGN.Tanker.Arco)
--
-- For convenience, MOOSE has a CALLSIGN enumerator introduced.
-- 
-- ## AWACS
-- 
-- You can use the class also to have an AWACS orbiting overhead the carrier. This requires to add the @{#RECOVERYTANKER.SetAWACS}(*switch*, *eplrs*) function to the script, which sets the enroute tasks AWACS 
-- as soon as the aircraft enters its pattern. Note that the EPLRS data link is enabled by default. To disable it, the second parameter *eplrs* must be set to *false*.
-- 
-- A simple script could look like this:
-- 
--     -- E-2D at USS Stennis spawning in air.
--     local awacsStennis=RECOVERYTANKER:New("USS Stennis", "E2D Group")
--     
--     -- Custom settings:
--     awacsStennis:SetAWACS()
--     awacsStennis:SetCallsign(CALLSIGN.AWACS.Wizard, 1)
--     awacsStennis:SetTakeoffAir()
--     awacsStennis:SetAltitude(20000)
--     awacsStennis:SetRadio(262)
--     awacsStennis:SetTACAN(2, "WIZ")
--     
--     -- Start AWACS.
--     awacsStennis:Start()
--
-- # Finite State Machine
-- 
-- The implementation uses a Finite State Machine (FSM). This allows the mission designer to hook in to certain events.
-- 
--    * @{#RECOVERYTANKER.Start}: This event starts the FMS process and initialized parameters and spawns the tanker. DCS event handling is started.
--    * @{#RECOVERYTANKER.Status}: This event is called in regular intervals (~60 seconds) and checks the status of the tanker and carrier. It triggers other events if necessary.
--    * @{#RECOVERYTANKER.PatternUpdate}: This event commands the tanker to update its pattern
--    * @{#RECOVERYTANKER.RTB}: This events sends the tanker to its home base (usually the carrier). This is called once the tanker runs low on gas.
--    * @{#RECOVERYTANKER.RefuelStart}: This event is called when a tanker starts to refuel another unit.
--    * @{#RECOVERYTANKER.RefuelStop}: This event is called when a tanker stopped to refuel another unit.
--    * @{#RECOVERYTANKER.Run}: This event is called when the tanker resumes normal operations, e.g. after refueling stopped or tanker finished refueling.
--    * @{#RECOVERYTANKER.Stop}: This event stops the FSM by unhandling DCS events.
--
-- The mission designer can capture these events by RECOVERYTANKER.OnAfter*Eventname* functions, e.g. @{#RECOVERYTANKER.OnAfterPatternUpdate}.
-- 
-- # Debugging
-- 
-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in
--     C:\Users\<yourname>\Saved Games\DCS\Logs\dcs.log
-- All output concerning the @{#RECOVERYTANKER} class should have the string "RECOVERYTANKER" in the corresponding line.
-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong.
-- 
-- The verbosity of the output can be increased by adding the following lines to your script:
-- 
--     BASE:TraceOnOff(true)
--     BASE:TraceLevel(1)
--     BASE:TraceClass("RECOVERYTANKER")
-- 
-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details.
-- 
-- ## Debug Mode
-- 
-- You have the option to enable the debug mode for this class via the @{#RECOVERYTANKER.SetDebugModeON} function.
-- If enabled, text messages about the tanker status will be displayed on screen and marks of the pattern created on the F10 map.
--
-- @field #RECOVERYTANKER
RECOVERYTANKER = {
  ClassName       = "RECOVERYTANKER",
  Debug           = false,
  lid             = nil,
  carrier         = nil,
  carriertype     = nil,
  tankergroupname = nil,
  tanker          = nil,
  airbase         = nil,
  beacon          = nil,
  TACANchannel    = nil,
  TACANmode       = nil,
  TACANmorse      = nil,
  TACANon         = nil,
  RadioFreq       = nil,
  RadioModu       = nil,
  altitude        = nil,
  speed           = nil,
  distStern       = nil,
  distBow         = nil,
  dTupdate        = nil,
  Dupdate         = nil,
  Hupdate         = nil,
  Tupdate         = nil,
  takeoff         = nil,
  lowfuel         = nil,
  respawn         = nil,
  respawninair    = nil,
  uncontrolledac  = nil,
  orientation     = nil,
  orientlast      = nil,
  position        = nil,
  alias           = nil,
  uid             =   0,
  awacs           = nil,
  callsignname    = nil,
  callsignnumber  = nil,
  modex           = nil,
  eplrs           = nil,
  recovery        = nil,
  terminaltype    = nil,
  unlimitedfuel   = false,
}

--- Unique ID (global).
-- @field #number UID Unique ID (global).
_RECOVERYTANKERID=0

--- Class version.
-- @field #string version
RECOVERYTANKER.version="1.0.10"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- DONE: Is alive check for tanker necessary?
-- DONE: Seamless change of position update. Get good updated waypoint and update position if tanker position is right. Not really possiple atm.
-- DONE: Check if TACAN mode "X" is allowed for AA TACAN stations. Nope
-- DONE: Check if tanker is going back to "Running" state after RTB and respawn.
-- DONE: Write documentation.
-- DONE: Trace functions self:T instead of self:I for less output.
-- DONE: Make pattern update parameters (distance, orientation) input parameters.
-- DONE: Add FSM event for pattern update.
-- DONE: Smarter pattern update function. E.g. (small) zone around carrier. Only update position when carrier leaves zone or changes heading?
-- DONE: Set AA TACAN.
-- DONE: Add refueling event/state.
-- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse.
-- DONE: Add unlimited fuel

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create new RECOVERYTANKER object.
-- @param #RECOVERYTANKER self
-- @param Wrapper.Unit#UNIT carrierunit Carrier unit.
-- @param #string tankergroupname Name of the late activated tanker aircraft template group.
-- @return #RECOVERYTANKER RECOVERYTANKER object.
function RECOVERYTANKER:New(carrierunit, tankergroupname)

  -- Inherit everthing from FSM class.
  local self = BASE:Inherit(self, FSM:New()) -- #RECOVERYTANKER
  
  if type(carrierunit)=="string" then
    self.carrier=UNIT:FindByName(carrierunit)
  else
    self.carrier=carrierunit
  end
  
  -- Carrier type.
  self.carriertype=self.carrier:GetTypeName()
  
  -- Tanker group name.
  self.tankergroupname=tankergroupname
  
  -- Increase unique ID.
  _RECOVERYTANKERID=_RECOVERYTANKERID+1
  
  -- Unique ID of this tanker.
  self.uid=_RECOVERYTANKERID

  -- Save self in static object. Easier to retrieve later.
  self.carrier:SetState(self.carrier, string.format("RECOVERYTANKER_%d", self.uid) , self)    
  
  -- Set unique spawn alias.
  self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.tankergroupname, _RECOVERYTANKERID)
  
  -- Log ID.
  self.lid=string.format("RECOVERYTANKER %s | ", self.alias)
  
  -- Init default parameters.
  self:SetAltitude()
  self:SetSpeed()
  self:SetRacetrackDistances()
  self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName()))
  self:SetTakeoffHot()
  self:SetLowFuelThreshold()
  self:SetRespawnOnOff()
  self:SetTACAN()
  self:SetRadio()
  self:SetPatternUpdateDistance()
  self:SetPatternUpdateHeading()
  self:SetPatternUpdateInterval()
  self:SetAWACS(false)
  self:SetRecoveryAirboss(false)
  self.terminaltype=AIRBASE.TerminalType.OpenMedOrBig
  
  -- Debug trace.
  if false then
    BASE:TraceOnOff(true)
    BASE:TraceClass(self.ClassName)
    BASE:TraceLevel(1)
  end
  
  -----------------------
  --- FSM Transitions ---
  -----------------------
  
  -- Start State.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  --                 From State  -->   Event   -->   To State
  self:AddTransition("Stopped",       "Start",         "Running")        -- Start the FSM.
  self:AddTransition("*",             "RefuelStart",   "Refueling")      -- Tanker has started to refuel another unit.
  self:AddTransition("*",             "RefuelStop",    "Running")        -- Tanker starts to refuel.
  self:AddTransition("*",             "Run",           "Running")        -- Tanker starts normal operation again.
  self:AddTransition("Running",       "RTB",           "Returning")      -- Tanker is returning to base (for fuel).
  self:AddTransition("Returning",     "Returned",      "Returned")       -- Tanker has returned to its airbase (i.e. landed).
  self:AddTransition("*",             "Status",        "*")              -- Status update.
  self:AddTransition("Running",       "PatternUpdate", "*")              -- Update pattern wrt to carrier.
  self:AddTransition("*",             "Stop",          "Stopped")        -- Stop the FSM.


  --- Triggers the FSM event "Start" that starts the recovery tanker. Initializes parameters and starts event handlers.
  -- @function [parent=#RECOVERYTANKER] Start
  -- @param #RECOVERYTANKER self

  --- Triggers the FSM event "Start" that starts the recovery tanker after a delay. Initializes parameters and starts event handlers.
  -- @function [parent=#RECOVERYTANKER] __Start
  -- @param #RECOVERYTANKER self
  -- @param #number delay Delay in seconds.

  --- On after "Start" event function. Called when FSM is started.
  -- @function [parent=#RECOVERYTANKER] OnAfterStart
  -- @param #RECOVERYTANKER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "RefuelStart" when the tanker starts refueling another aircraft.
  -- @function [parent=#RECOVERYTANKER] RefuelStart
  -- @param #RECOVERYTANKER self
  -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker.

  --- On after "RefuelStart" event user function. Called when a the the tanker started to refuel another unit.
  -- @function [parent=#RECOVERYTANKER] OnAfterRefuelStart
  -- @param #RECOVERYTANKER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Unit#UNIT receiver Unit receiving fuel from the tanker.


  --- Triggers the FSM event "RefuelStop" when the tanker stops refueling another aircraft.
  -- @function [parent=#RECOVERYTANKER] RefuelStop
  -- @param #RECOVERYTANKER self
  -- @param Wrapper.Unit#UNIT receiver Unit stoped receiving fuel from the tanker.

  --- On after "RefuelStop" event user function. Called when a the the tanker stopped to refuel another unit.
  -- @function [parent=#RECOVERYTANKER] OnAfterRefuelStop
  -- @param #RECOVERYTANKER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Unit#UNIT receiver Unit that received fuel from the tanker.


  --- Triggers the FSM event "Run". Simply puts the group into "Running" state.
  -- @function [parent=#RECOVERYTANKER] Run
  -- @param #RECOVERYTANKER self

  --- Triggers delayed the FSM event "Run". Simply puts the group into "Running" state.
  -- @function [parent=#RECOVERYTANKER] __Run
  -- @param #RECOVERYTANKER self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "RTB" that sends the tanker home.
  -- @function [parent=#RECOVERYTANKER] RTB
  -- @param #RECOVERYTANKER self
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to.

  --- Triggers the FSM event "RTB" that sends the tanker home after a delay.
  -- @function [parent=#RECOVERYTANKER] __RTB
  -- @param #RECOVERYTANKER self
  -- @param #number delay Delay in seconds.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to.

  --- On after "RTB" event user function. Called when a the the tanker returns to its home base.
  -- @function [parent=#RECOVERYTANKER] OnAfterRTB
  -- @param #RECOVERYTANKER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to.


  --- Triggers the FSM event "Returned" after the tanker has landed.
  -- @function [parent=#RECOVERYTANKER] Returned
  -- @param #RECOVERYTANKER self
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed.

  --- Triggers the delayed FSM event "Returned" after the tanker has landed.
  -- @function [parent=#RECOVERYTANKER] __Returned
  -- @param #RECOVERYTANKER self
  -- @param #number delay Delay in seconds.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed.

  --- On after "Returned" event user function. Called when a the the tanker has landed at an airbase.
  -- @function [parent=#RECOVERYTANKER] OnAfterReturned
  -- @param #RECOVERYTANKER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase the tanker has landed.


  --- Triggers the FSM event "Status" that updates the tanker status.
  -- @function [parent=#RECOVERYTANKER] Status
  -- @param #RECOVERYTANKER self

  --- Triggers the delayed FSM event "Status" that updates the tanker status.
  -- @function [parent=#RECOVERYTANKER] __Status
  -- @param #RECOVERYTANKER self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "PatternUpdate" that updates the pattern of the tanker wrt to the carrier position.
  -- @function [parent=#RECOVERYTANKER] PatternUpdate
  -- @param #RECOVERYTANKER self

  --- Triggers the delayed FSM event "PatternUpdate" that updates the pattern of the tanker wrt to the carrier position.
  -- @function [parent=#RECOVERYTANKER] __PatternUpdate
  -- @param #RECOVERYTANKER self
  -- @param #number delay Delay in seconds.

  --- On after "PatternEvent" event user function. Called when a the pattern of the tanker is updated.
  -- @function [parent=#RECOVERYTANKER] OnAfterPatternUpdate
  -- @param #RECOVERYTANKER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Stop" that stops the recovery tanker. Event handlers are stopped.
  -- @function [parent=#RECOVERYTANKER] Stop
  -- @param #RECOVERYTANKER self

  --- Triggers the FSM event "Stop" that stops the recovery tanker after a delay. Event handlers are stopped.
  -- @function [parent=#RECOVERYTANKER] __Stop
  -- @param #RECOVERYTANKER self
  -- @param #number delay Delay in seconds.
  
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set the tanker to have unlimited fuel.
-- @param #RECOVERYTANKER self
-- @param #boolean OnOff If true, the tanker will have unlimited fuel.
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetUnlimitedFuel(OnOff)
  self.unlimitedfuel = OnOff
  return self
end

--- Set the speed the tanker flys in its orbit pattern.
-- @param #RECOVERYTANKER self
-- @param #number speed True air speed (TAS) in knots. Default 274 knots, which results in ~250 KIAS.
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetSpeed(speed)
  self.speed=UTILS.KnotsToMps(speed or 274)
  return self
end

--- Set orbit pattern altitude of the tanker.
-- @param #RECOVERYTANKER self
-- @param #number altitude Tanker altitude in feet. Default 6000 ft.
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetAltitude(altitude)
  self.altitude=UTILS.FeetToMeters(altitude or 6000)
  return self
end

--- Set race-track distances.
-- @param #RECOVERYTANKER self
-- @param #number distbow Distance [NM] in front of the carrier. Default 10 NM.
-- @param #number diststern Distance [NM] behind the carrier. Default 4 NM.
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetRacetrackDistances(distbow, diststern)
  self.distBow=UTILS.NMToMeters(distbow or 10)
  self.distStern=-UTILS.NMToMeters(diststern or 4)
  return self
end

--- Set minimum pattern update interval. After a pattern update this time interval has to pass before the next update is allowed.
-- @param #RECOVERYTANKER self
-- @param #number interval Min interval in minutes. Default is 10 minutes.
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetPatternUpdateInterval(interval)
  self.dTupdate=(interval or 10)*60
  return self
end

--- Set pattern update distance threshold. Tanker will update its pattern when the carrier changes its position by more than this distance.
-- @param #RECOVERYTANKER self
-- @param #number distancechange Distance threshold in NM. Default 5 NM (=9.62 km).
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetPatternUpdateDistance(distancechange)
  self.Dupdate=UTILS.NMToMeters(distancechange or 5)
  return self
end

--- Set pattern update heading threshold. Tanker will update its pattern when the carrier changes its heading by more than this value.
-- @param #RECOVERYTANKER self
-- @param #number headingchange Heading threshold in degrees. Default 5 degrees.
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetPatternUpdateHeading(headingchange)
  self.Hupdate=headingchange or 5
  return self
end

--- Set low fuel state of tanker. When fuel is below this threshold, the tanker will RTB or be respawned if takeoff type is in air.
-- @param #RECOVERYTANKER self
-- @param #number fuelthreshold Low fuel threshold in percent. Default 10 % of max fuel.
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetLowFuelThreshold(fuelthreshold)
  self.lowfuel=fuelthreshold or 10
  return self
end

--- Set home airbase of the tanker. This is the airbase where the tanker will go when it is out of fuel.
-- @param #RECOVERYTANKER self
-- @param Wrapper.Airbase#AIRBASE airbase The home airbase. Can be the airbase name or a Moose AIRBASE object.
-- @param #number terminaltype (Optional) The terminal type of parking spots used for spawning at airbases. Default AIRBASE.TerminalType.OpenMedOrBig.
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetHomeBase(airbase, terminaltype)
  if type(airbase)=="string" then
    self.airbase=AIRBASE:FindByName(airbase)
  else
    self.airbase=airbase
  end
  if not self.airbase then
    self:E(self.lid.."ERROR: Airbase is nil!")
  end
  if terminaltype then
    self.terminaltype=terminaltype
  end
  return self
end

--- Activate recovery by the AIRBOSS class. Tanker will get a Marshal stack and perform a CASE I, II or III recovery when RTB.
-- @param #RECOVERYTANKER self
-- @param #boolean switch If true or nil, recovery is done by AIRBOSS.
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetRecoveryAirboss(switch)
  if switch==true or switch==nil then
    self.recovery=true
  else
    self.recovery=false
  end
  return self
end

--- Set that the group takes the role of an AWACS instead of a refueling tanker.
-- @param #RECOVERYTANKER self
-- @param #boolean switch If true or nil, set role AWACS.
-- @param #boolean eplrs If true or nil, enable EPLRS. If false, EPLRS will be off.
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetAWACS(switch, eplrs)
  if switch==nil or switch==true then
    self.awacs=true
  else
    self.awacs=false
  end
  if eplrs==nil or eplrs==true then
    self.eplrs=true
  else
    self.eplrs=false
  end
  
  return self
end


--- Set callsign of the tanker group.
-- @param #RECOVERYTANKER self
-- @param #number callsignname Number
-- @param #number callsignnumber Number
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetCallsign(callsignname, callsignnumber)
  self.callsignname=callsignname
  self.callsignnumber=callsignnumber
  return self
end

--- Set modex (tail number) of the tanker.
-- @param #RECOVERYTANKER self
-- @param #number modex Tail number.
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetModex(modex)
  self.modex=modex
  return self
end

--- Set takeoff type.
-- @param #RECOVERYTANKER self
-- @param #number takeofftype Takeoff type.
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetTakeoff(takeofftype)
  self.takeoff=takeofftype
  return self
end

--- Set takeoff with engines running (hot).
-- @param #RECOVERYTANKER self
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetTakeoffHot()
  self:SetTakeoff(SPAWN.Takeoff.Hot)
  return self
end

--- Set takeoff with engines off (cold).
-- @param #RECOVERYTANKER self
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetTakeoffCold()
  self:SetTakeoff(SPAWN.Takeoff.Cold)
  return self
end

--- Set takeoff in air at the defined pattern altitude and ~10 NM astern the carrier.
-- @param #RECOVERYTANKER self
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetTakeoffAir()
  self:SetTakeoff(SPAWN.Takeoff.Air)
  return self
end

--- Enable respawning of tanker. Note that this is the default behaviour. 
-- @param #RECOVERYTANKER self 
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetRespawnOn()
  self.respawn=true
  return self
end

--- Disable respawning of tanker.
-- @param #RECOVERYTANKER self 
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetRespawnOff()
  self.respawn=false
  return self
end

--- Set whether tanker shall be respawned or not.
-- @param #RECOVERYTANKER self
-- @param #boolean switch If true (or nil), tanker will be respawned. If false, tanker will not be respawned. 
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetRespawnOnOff(switch)
  if switch==nil or switch==true then
    self.respawn=true
  else
    self.respawn=false
  end
  return self
end

--- Tanker will be respawned in air, even it was initially spawned on the carrier.
-- So only the first spawn will be on the carrier while all subsequent spawns will happen in air.
-- This allows for undisrupted operations and less problems on the carrier deck.
-- @param #RECOVERYTANKER self
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetRespawnInAir()
  self.respawninair=true
  return self
end

--- Use an uncontrolled aircraft already present in the mission rather than spawning a new tanker as initial recovery thanker.
-- This can be useful when interfaced with, e.g., a MOOSE @{Functional.Warehouse#WAREHOUSE}.
-- The group name is the one specified in the @{#RECOVERYTANKER.New} function.
-- @param #RECOVERYTANKER self
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetUseUncontrolledAircraft()
  self.uncontrolledac=true
  return self
end


--- Disable automatic TACAN activation.
-- @param #RECOVERYTANKER self
-- @return #RECOVERYTANKER self 
function RECOVERYTANKER:SetTACANoff()
  self.TACANon=false
  return self
end

--- Set TACAN channel of tanker. Note that mode is automatically set to "Y" for AA TACAN since only that works.
-- @param #RECOVERYTANKER self
-- @param #number channel TACAN channel. Default 1.
-- @param #string morse TACAN morse code identifier. Three letters. Default "TKR".
-- @param #string mode TACAN mode, which can be either "Y" (default) or "X".
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetTACAN(channel, morse, mode)
  self.TACANchannel=channel or 1
  self.TACANmode=mode or "Y"
  self.TACANmorse=morse or "TKR"
  self.TACANon=true
  return self
end

--- Set radio frequency and optionally modulation of the tanker.
-- @param #RECOVERYTANKER self
-- @param #number frequency Radio frequency in MHz. Default 251 MHz.
-- @param #string modulation Radio modulation, either "AM" or "FM". Default "AM".
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetRadio(frequency, modulation)
  self.RadioFreq=frequency or 251
  self.RadioModu=modulation or "AM"
  return self
end

--- Activate debug mode. Marks of pattern on F10 map and debug messages displayed on screen.
-- @param #RECOVERYTANKER self
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetDebugModeON()
  self.Debug=true
  return self
end

--- Deactivate debug mode. This is also the default setting.
-- @param #RECOVERYTANKER self
-- @return #RECOVERYTANKER self
function RECOVERYTANKER:SetDebugModeOFF()
  self.Debug=false
  return self
end

--- Check if tanker is currently returning to base.
-- @param #RECOVERYTANKER self
-- @return #boolean If true, tanker is returning to base. 
function RECOVERYTANKER:IsReturning()
  return self:is("Returning")
end

--- Check if tanker has returned to base.
-- @param #RECOVERYTANKER self
-- @return #boolean If true, tanker has returned to base. 
function RECOVERYTANKER:IsReturned()
  return self:is("Returned")
end

--- Check if tanker is currently operating.
-- @param #RECOVERYTANKER self
-- @return #boolean If true, tanker is operating. 
function RECOVERYTANKER:IsRunning()
  return self:is("Running")
end

--- Check if tanker is currently refueling another aircraft.
-- @param #RECOVERYTANKER self
-- @return #boolean If true, tanker is refueling. 
function RECOVERYTANKER:IsRefueling()
  return self:is("Refueling")
end

--- Check if FMS was stopped.
-- @param #RECOVERYTANKER self
-- @return #boolean If true, is stopped. 
function RECOVERYTANKER:IsStopped()
  return self:is("Stopped")
end

--- Alias of tanker spawn group.
-- @param #RECOVERYTANKER self
-- @return #string Alias of the tanker. 
function RECOVERYTANKER:GetAlias()
  return self.alias
end

--- Get unit name of the spawned tanker.
-- @param #RECOVERYTANKER self
-- @return #string Name of the tanker unit or nil if it does not exist. 
function RECOVERYTANKER:GetUnitName()
  local unit=self.tanker:GetUnit(1)
  if unit then
    return unit:GetName()
  end
  return nil
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM states
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue.
-- @param #RECOVERYTANKER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RECOVERYTANKER:onafterStart(From, Event, To)

  -- Info on start.
  self:I(string.format("Starting Recovery Tanker v%s for carrier unit %s of type %s for tanker group %s.", RECOVERYTANKER.version, self.carrier:GetName(), self.carriertype, self.tankergroupname))
  
  -- Handle events.
  self:HandleEvent(EVENTS.EngineShutdown)
  self:HandleEvent(EVENTS.Land)
  self:HandleEvent(EVENTS.Refueling,     self._RefuelingStart)      --Need explicit functions since OnEventRefueling and OnEventRefuelingStop did not hook!
  self:HandleEvent(EVENTS.RefuelingStop, self._RefuelingStop)
  self:HandleEvent(EVENTS.Crash,         self._OnEventCrashOrDead)
  self:HandleEvent(EVENTS.Dead,          self._OnEventCrashOrDead)
  
  -- Spawn tanker. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine.
  local Spawn=SPAWN:NewWithAlias(self.tankergroupname, self.alias)
  
  if self.unlimitedfuel then
    Spawn:OnSpawnGroup(
      function (grp)
        grp:CommandSetUnlimitedFuel(self.unlimitedfuel)
      end
    )
  end
  
  -- Set radio frequency and modulation.
  Spawn:InitRadioCommsOnOff(true)
  Spawn:InitRadioFrequency(self.RadioFreq)
  Spawn:InitRadioModulation(self.RadioModu)
  Spawn:InitModex(self.modex)
  
  -- Spawn on carrier.
  if self.takeoff==SPAWN.Takeoff.Air then
  
    -- Carrier heading
    local hdg=self.carrier:GetHeading()
    
    -- Spawn distance behind the carrier.
    local dist=-self.distStern+UTILS.NMToMeters(4)
    
    -- Coordinate behind the carrier and slightly port.
    local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg+190):SetAltitude(self.altitude)
    
    -- Orientation of spawned group.
    Spawn:InitHeading(hdg+10)
    
    -- Spawn at coordinate.
    self.tanker=Spawn:SpawnFromCoordinate(Carrier)
    
  else
  
    -- Check if an uncontrolled tanker group was requested.
    if self.uncontrolledac then
    
      -- Use an uncontrolled aircraft group.
      self.tanker=GROUP:FindByName(self.tankergroupname)
      
      if self.tanker:IsAlive() then
      
        -- Start uncontrolled group.
        self.tanker:StartUncontrolled()
        
      else
        -- No group by that name!
        self:E(string.format("ERROR: No uncontrolled (alive) tanker group with name %s could be found!", self.tankergroupname))
        return
      end
      
    else
    
      -- Spawn tanker at airbase.
      self.tanker=Spawn:SpawnAtAirbase(self.airbase, self.takeoff, nil, self.terminaltype)
      
    end
    
  end

  -- Initialize route. self.distStern<0!
  self:ScheduleOnce(1, self._InitRoute, self, -self.distStern+UTILS.NMToMeters(3))
  
  -- Create tanker beacon.
  if self.TACANon then
    self:_ActivateTACAN(2)
  end
  
  -- Set callsign.
  if self.callsignname then
    self.tanker:CommandSetCallsign(self.callsignname, self.callsignnumber, 2)
  end
  
  -- Turn EPLRS datalink on.
  if self.eplrs then
    self.tanker:CommandEPLRS(true, 3)
  end
  
  
  -- Get initial orientation and position of carrier.
  self.orientation=self.carrier:GetOrientationX()
  self.orientlast=self.carrier:GetOrientationX()
  self.position=self.carrier:GetCoordinate()

  -- Init status updates in 10 seconds.
  self:__Status(10)
  
  return self
end


--- On after Status event. Checks player status.
-- @param #RECOVERYTANKER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RECOVERYTANKER:onafterStatus(From, Event, To)

  -- Get current time.
  local time=timer.getTime()
  
  if self.tanker and self.tanker:IsAlive() then
  
    ---------------------
    -- TANKER is ALIVE --
    --------------------- 
  
    -- Get fuel of tanker.
    local fuel=self.tanker:GetFuel()*100
    local life=self.tanker:GetUnit(1):GetLife()
    local life0=self.tanker:GetUnit(1):GetLife0()
    local lifeR=self.tanker:GetUnit(1):GetLifeRelative()    
    
    -- Report fuel and life.
    local text=string.format("Recovery tanker %s: state=%s fuel=%.1f, life=%.1f/%.1f=%d", self.tanker:GetName(), self:GetState(), fuel, life, life0, lifeR*100)
    self:T(self.lid..text)
    MESSAGE:New(text, 10):ToAllIf(self.Debug)
    
    -- Check if tanker is running and not RTBing or refueling.
    if self:IsRunning() then
    
      -- Check fuel.
      if fuel<self.lowfuel then
      
        -- Check if spawn in air is activated.
        if (self.takeoff==SPAWN.Takeoff.Air or self.respawninair) and not self.recovery then
        
          -- Check that respawn should happen.
          if self.respawn then
        
            -- Debug message.
            local text=string.format("Respawning recovery tanker %s in air.", self.tanker:GetName())
            MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
            self:T(self.lid..text)  
            
            -- Set heading for respawn template.
            self.tanker:InitHeading(self.tanker:GetHeading())
            
            -- Set radio for respawn template.
            self.tanker:InitRadioCommsOnOff(true)
            self.tanker:InitRadioFrequency(self.RadioFreq)
            self.tanker:InitRadioModulation(self.RadioModu)
            self.tanker:InitModex(self.modex)
            
            -- Respawn tanker.
            self.tanker=self.tanker:Respawn(nil, true)
            
            -- Update Pattern in 2 seconds. Need to give a bit time so that the respawned group is in the game.
            self:__PatternUpdate(2)
            
            -- Create tanker beacon and activate TACAN.
            if self.TACANon then
              self:_ActivateTACAN(3)
            end
            
            -- Set callsign.
            if self.callsignname then
              self.tanker:CommandSetCallsign(self.callsignname, self.callsignnumber, 3)
            end
            
            -- Turn EPLRS datalink on.
            if self.eplrs then
              self.tanker:CommandEPLRS(true, 4)
            end                        
          end
          
        else
  
          -- Send tanker home if fuel runs low.
          self:RTB(self.airbase)
                  
        end
                  
      else
      
        if self.Tupdate then
        
          --Time since last pattern update.
          local dt=time-self.Tupdate
          
          -- Check if pattern needs to be updated.
          local updatepattern=self:_CheckPatternUpdate(dt)
          
          -- Update pattern.
          if updatepattern then
            self:PatternUpdate()
          end
          
        end
      end
      
    elseif self:IsReturning() then
    
      -- Tanker is returning to its base.
      self:T2(self.lid.."Tanker is returning.")
      
    elseif self:IsReturned() then
    
      -- Tanker landed. Waiting for engine shutdown...
      self:T2(self.lid.."Tanker returned. waiting for engine shutdown.")
      
    end
    
    -- Call status again in 30 seconds.
    if not self:IsStopped() then
      self:__Status(-30)
    end
    
  else
  
    --------------------
    -- TANKER is DEAD --
    --------------------

    if not self:IsStopped() then
    
      self:E(self.lid.."Recovery tanker is NOT alive (and not stopped)!")
    
      -- Stop FSM.
      self:Stop()
    
      -- Restart FSM after 5 seconds.
      if self.respawn then
        self:__Start(5)
      end
      
    end
    
  end

end

--- On after "PatternUpdate" event. Updates the racetrack pattern of the tanker wrt the carrier position.
-- @param #RECOVERYTANKER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RECOVERYTANKER:onafterPatternUpdate(From, Event, To)
  -- Debug message.
  local text=string.format("Updating recovery tanker %s racetrack pattern.", self.tanker:GetName())
  MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
  self:T(self.lid..text)
    
  -- Carrier heading.
  local hdg=self.carrier:GetHeading()
  
  -- Carrier position.
  local Carrier=self.carrier:GetCoordinate()
  
  -- Define race-track pattern.
  local p0=self.tanker:GetCoordinate():Translate(UTILS.NMToMeters(1), self.tanker:GetHeading(), true)
  
  -- Racetrack pattern points.
  local p1=Carrier:Translate(self.distStern, hdg):SetAltitude(self.altitude)
  local p2=Carrier:Translate(self.distBow, hdg):SetAltitude(self.altitude)
  
  -- Set orbit task.
  local taskorbit=self.tanker:TaskOrbit(p1, self.altitude, self.speed, p2)
  
  -- Debug markers.
  if self.Debug then
    p0:MarkToAll("Waypoint P0 " ..self.tanker:GetName())
    p1:MarkToAll("Racetrack P1 "..self.tanker:GetName())
    p2:MarkToAll("Racetrack P2 "..self.tanker:GetName())
  end
    
  -- Waypoints array.
  local wp={}
    
  -- New waypoint with orbit pattern task. Speed expected in km/h.
  wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil , UTILS.MpsToKmph(self.speed), {}, "Current Position")
  wp[2]=p0:WaypointAirTurningPoint(nil, UTILS.MpsToKmph(self.speed), {taskorbit}, "Tanker Orbit")
  
  -- Initialize WP and route tanker.
  self.tanker:WayPointInitialize(wp)
  
  -- Task combo.
  
  -- Be a tanker or be an AWACS.
  local taskroll = self.tanker:EnRouteTaskTanker()
  if self.awacs then
    taskroll=self.tanker:EnRouteTaskAWACS()
  end
  
  --local taskeplrs=self.tanker:TaskEPLRS(true, 2)
  
  -- Route task.
  local taskroute  = self.tanker:TaskRoute(wp)
  
  -- Note that the order is important here! tasktanker has to come first. Otherwise it does not work.
  local taskcombo  = self.tanker:TaskCombo({taskroll, taskroute})

  -- Set task.
  self.tanker:SetTask(taskcombo, 1)
  
  -- Set update time.
  self.Tupdate=timer.getTime()

end

--- On after "RTB" event. Send tanker back to carrier.
-- @param #RECOVERYTANKER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Airbase#AIRBASE airbase The airbase where the tanker should return to.
function RECOVERYTANKER:onafterRTB(From, Event, To, airbase)

  -- Default is the home base.
  airbase=airbase or self.airbase

  -- Debug message.
  local text=string.format("Recovery tanker %s returning to airbase %s.", self.tanker:GetName(), airbase:GetName())
  MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
  self:T(self.lid..text)
  
  -- Waypoint array.
  local wp={}
  
  -- Set speed ot 75% max.
  local speed=self.tanker:GetSpeedMax()*0.75
  
  -- Set landing waypoint.
  wp[1]=self.tanker:GetCoordinate():WaypointAirTurningPoint(nil, speed, {}, "Current Position")
  
  
  if self.recovery then
    -- Fly a bit until the airboss takes over.
    wp[2]=self.tanker:GetCoordinate():Translate(UTILS.NMToMeters(10), self.tanker:GetHeading(), true):WaypointAirTurningPoint(nil, speed, {}, "WP")
  else  
    wp[2]=airbase:GetCoordinate():SetAltitude(500):WaypointAirLanding(speed, airbase, nil, "Land at airbase")
  end
  
  -- Initialize WP and route tanker.
  self.tanker:WayPointInitialize(wp)
  
  -- Set task.
  self.tanker:Route(wp, 1)
end


--- On after Returned event. The tanker has landed.
-- @param #RECOVERYTANKER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Airbase#AIRBASE airbase The base at which the tanker landed.
function RECOVERYTANKER:onafterReturned(From, Event, To, airbase)

  if airbase then
    local airbasename=airbase:GetName()
    self:I(self.lid..string.format("Tanker returned to airbase %s", tostring(airbasename)))
  else
    self:E(self.lid..string.format("WARNING: Tanker landed but airbase (EventData.Place) is nil!"))
  end
  
end


--- On after Stop event. Unhandle events and stop status updates. 
-- @param #RECOVERYTANKER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RECOVERYTANKER:onafterStop(From, Event, To)

  -- Unhandle events.
  self:UnHandleEvent(EVENTS.Land)
  self:UnHandleEvent(EVENTS.EngineShutdown)
  self:UnHandleEvent(EVENTS.Refueling)
  self:UnHandleEvent(EVENTS.RefuelingStop)
  self:UnHandleEvent(EVENTS.Dead)
  self:UnHandleEvent(EVENTS.Crash)
  
  -- Clear all pending FSM events.
  self.CallScheduler:Clear()
  
  -- If tanker is alive, despawn it.
  if self.tanker and self.tanker:IsAlive() then
    self:I(self.lid.."Stopping FSM and despawning tanker.")
    self.tanker:Destroy()
  else
    self:I(self.lid.."Stopping FSM. Tanker was not alive.")
  end
    
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- EVENT functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Event handler for landing of recovery tanker.
-- @param #RECOVERYTANKER self
-- @param Core.Event#EVENTDATA EventData Event data.
function RECOVERYTANKER:OnEventLand(EventData)

  -- Group that shut down the engine.
  local group=EventData.IniGroup --Wrapper.Group#GROUP
  
  -- Check if group is alive.
  if group and group:IsAlive() then
  
    -- Group name. When spawning it will have #001 attached.
    local groupname=group:GetName()
    
    -- Check that we have the right group and that it should be respawned.
    if groupname==self.tanker:GetName() then
    
      local airbase=nil --Wrapper.Airbase#AIRBASE
      local airbasename="unknown"
      if EventData.Place then
        airbase=EventData.Place
        airbasename=airbase:GetName()
      end
  
      -- Debug info.
      local text=string.format("Recovery tanker group %s landed at airbase %s.", group:GetName(), airbasename)
      MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
      self:T(self.lid..text)
      
      -- Trigger returned event.
      self:__Returned(1, airbase)      
    end
  end
end


--- Event handler for engine shutdown of recovery tanker.
-- Respawn tanker group once it landed because it was out of fuel.
-- @param #RECOVERYTANKER self
-- @param Core.Event#EVENTDATA EventData Event data.
function RECOVERYTANKER:OnEventEngineShutdown(EventData)

  -- Group that shut down the engine.
  local group=EventData.IniGroup --Wrapper.Group#GROUP
  
  -- Check if group is alive.
  if group and group:IsAlive() then
  
    -- Group name. When spawning it will have #001 attached.
    local groupname=group:GetName()
    
    -- Check that we have the right group and that it should be respawned.
    if groupname==self.tanker:GetName() and self.respawn then
  
      -- Debug info.
      local text=string.format("Respawning recovery tanker group %s.", group:GetName())
      MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
      self:T(self.lid..text)
      
      -- Set radio for respawn template.
      group:InitRadioCommsOnOff(true)
      group:InitRadioFrequency(self.RadioFreq)
      group:InitRadioModulation(self.RadioModu)
      group:InitModex(self.modex)
           
      -- Respawn tanker. Delaying respawn due to DCS bug https://github.com/FlightControl-Master/MOOSE/issues/1076
      self:ScheduleOnce(1, GROUP.RespawnAtCurrentAirbase, group)
      
      -- Create tanker beacon and activate TACAN.
      if self.TACANon then
        self:_ActivateTACAN(3)
      end
      
      -- Set callsign.
      if self.callsignname then
        self.tanker:CommandSetCallsign(self.callsignname, self.callsignnumber, 3)
      end
      
      -- Turn EPLRS datalink on.
      if self.eplrs then
        self.tanker:CommandEPLRS(true, 4)
      end       

      -- Initial route.
      self:ScheduleOnce(2, RECOVERYTANKER._InitRoute, self, -self.distStern+UTILS.NMToMeters(3))     
    end
    
  end
end

--- Event handler for refueling started.
-- @param #RECOVERYTANKER self
-- @param Core.Event#EVENTDATA EventData Event data.
function RECOVERYTANKER:_RefuelingStart(EventData)

  if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() then
  
    -- Unit receiving fuel.
    local receiver=EventData.IniUnit
    
    -- Get distance to tanker to check that unit is receiving fuel from this tanker.
    local dist=receiver:GetCoordinate():Get2DDistance(self.tanker:GetCoordinate())
    
    -- If distance > 100 meters, this should be another tanker.
    if dist>100 then
      return
    end
  
    -- Info message.
    local text=string.format("Recovery tanker %s started refueling unit %s", self.tanker:GetName(), receiver:GetName())
    MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
    self:T(self.lid..text)    
    
    -- FMS state "Refueling".
    self:RefuelStart(receiver) 
  end

end

--- Event handler for refueling stopped.
-- @param #RECOVERYTANKER self
-- @param Core.Event#EVENTDATA EventData Event data.
function RECOVERYTANKER:_RefuelingStop(EventData)

  if EventData and EventData.IniUnit and EventData.IniUnit:IsAlive() then
  
    -- Unit receiving fuel.
    local receiver=EventData.IniUnit
    
    -- Get distance to tanker to check that unit is receiving fuel from this tanker.
    local dist=receiver:GetCoordinate():Get2DDistance(self.tanker:GetCoordinate())
    
    -- If distance > 100 meters, this should be another tanker.
    if dist>100 then
      return
    end
  
    -- Info message.
    local text=string.format("Recovery tanker %s stopped refueling unit %s", self.tanker:GetName(), receiver:GetName())
    MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
    self:T(self.lid..text)
        
    -- FSM state "Running".
    self:RefuelStop(receiver)
  end

end

--- A unit crashed or died.
-- @param #RECOVERYTANKER self
-- @param Core.Event#EVENTDATA EventData Event data.
function RECOVERYTANKER:_OnEventCrashOrDead(EventData)
  self:F2({eventdata=EventData})
  
  -- Check that there is an initiating unit in the event data.
  if EventData and EventData.IniUnit then

    -- Crashed or dead unit.
    local unit=EventData.IniUnit  
    local unitname=tostring(EventData.IniUnitName)
    
    -- Check that it was the tanker that crashed.
    if EventData.IniGroupName==self.tanker:GetName() then
    
      -- Error message.
      self:E(self.lid..string.format("Recovery tanker %s crashed!", unitname))
      
      -- Stop FSM.
      self:Stop()
      
      -- Restart.
      if self.respawn then
        self:__Start(5)
      end
    
    end
    
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- MISC functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Task function to
-- @param #RECOVERYTANKER self
function RECOVERYTANKER:_InitPatternTaskFunction()

  -- Name of the warehouse (static) object.
  local carriername=self.carrier:GetName()

  -- Task script.
  local DCSScript = {}
  DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername)                           -- The carrier unit that holds the self object.
  DCSScript[#DCSScript+1] = string.format('local mytanker  = mycarrier:GetState(mycarrier, \"RECOVERYTANKER_%d\") ', self.uid) -- Get the RECOVERYTANKER self object.
  DCSScript[#DCSScript+1] = string.format('mytanker:PatternUpdate()')                                                          -- Call the function, e.g. mytanker.(self)

  -- Create task.
  local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript)))

  return DCSTask
end

--- Init waypoint after spawn. Tanker is first guided to a position astern the carrier and starts its racetrack pattern from there.
-- @param #RECOVERYTANKER self
-- @param #number dist Distance [NM] of initial waypoint astern carrier. Default 8 NM.
-- @param #number delay Delay before routing in seconds. Default 1 second.
function RECOVERYTANKER:_InitRoute(dist, delay)

  -- Defaults.
  dist=dist or UTILS.NMToMeters(8)
  delay=delay or 1
  
  -- Debug message.
  self:T(self.lid..string.format("Initializing route of recovery tanker %s.", self.tanker:GetName()))
  
  -- Carrier position.
  local Carrier=self.carrier:GetCoordinate()
  
  -- Carrier heading.
  local hdg=self.carrier:GetHeading()
  
  -- First waypoint is ~10 NM behind and slightly port the boat.
  local p=Carrier:Translate(dist, hdg+190):SetAltitude(self.altitude)
  
  -- Speed for waypoints in km/h.
  -- This causes a problem, because the tanker might not be alive yet ==> We schedule the call of _InitRoute
  local speed=self.tanker:GetSpeedMax()*0.8
  
  -- Set to 280 knots and convert to km/h.
  --local speed=280/0.539957
  
  -- Debug mark.
  if self.Debug then
    p:MarkToAll(string.format("Enter Pattern WP: alt=%d ft, speed=%d kts", UTILS.MetersToFeet(self.altitude), speed*0.539957))
  end
  
  -- Task to update pattern when wp 2 is reached.
  local task=self:_InitPatternTaskFunction()

  -- Waypoints.
  local wp={}
  if self.takeoff==SPAWN.Takeoff.Air then
    wp[#wp+1]=self.tanker:GetCoordinate():SetAltitude(self.altitude):WaypointAirTurningPoint(nil, speed, {}, "Spawn Position")
  else
    wp[#wp+1]=Carrier:WaypointAirTakeOffParking()
  end
  wp[#wp+1]=p:WaypointAirTurningPoint(nil, speed, {task}, "Enter Pattern")
    
  -- Set route.
  self.tanker:Route(wp, delay)
  
  -- Set state to Running. Necessary when tanker was RTB and respawned since it is probably in state "Returning".
  self:__Run(1)
  
  -- No update yet, wait until the function is called (avoids checks if pattern update is needed).
  self.Tupdate=nil
end

--- Check if heading or position have changed significantly.
-- @param #RECOVERYTANKER self
-- @param #number dt Time since last update in seconds.
-- @return #boolean If true, heading and/or position have changed more than 5 degrees or 10 km, respectively.
function RECOVERYTANKER:_CheckPatternUpdate(dt)

  -- Get current position and orientation of carrier.
  local pos=self.carrier:GetCoordinate()
  
  -- Current orientation of carrier.
  local vNew=self.carrier:GetOrientationX()
  
  -- Reference orientation of carrier after the last update
  local vOld=self.orientation
  
  -- Last orientation from 30 seconds ago.
  local vLast=self.orientlast
  
  -- We only need the X-Z plane.
  vNew.y=0 ; vOld.y=0 ; vLast.y=0
  
  -- Get angle between old and new orientation vectors in rad and convert to degrees.
  local deltaHeading=math.deg(math.acos(UTILS.VecDot(vNew,vOld)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vOld)))
  
  -- Angle between current heading and last time we checked ~30 seconds ago.
  local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast)))
  
  -- Last orientation becomes new orientation
  self.orientlast=vNew
  
  -- Carrier is turning when its heading changed by at least one degree since last check.
  local turning=deltaLast>=1
  
  -- Debug output if turning
  if turning then
    self:T2(self.lid..string.format("Carrier is turning. Delta Heading = %.1f", deltaLast))
  end

  -- Check if orientation changed.
  local Hchange=false
  if math.abs(deltaHeading)>=self.Hupdate then
    self:T(self.lid..string.format("Carrier heading changed by %d degrees. Turning=%s.", deltaHeading, tostring(turning)))
    Hchange=true
  end
  
  -- Get distance to saved position.
  local dist=pos:Get2DDistance(self.position)
  
  -- Check if carrier moved more than ~5 NM.
  local Dchange=false
  if dist>self.Dupdate then
    self:T(self.lid..string.format("Carrier position changed by %.1f NM. Turning=%s.", UTILS.MetersToNM(dist), tostring(turning)))
    Dchange=true
  end
  
  -- Assume no update necessary.
  local update=false
  
  -- No update if currently turning! Also must be running (not RTB or refueling) and T>~10 min since last position update.
  if self:IsRunning() and dt>self.dTupdate and not turning then
  
    -- Update if heading or distance changed.
    if Hchange or Dchange then
      -- Debug message.
      local text=string.format("Updating tanker %s pattern due to carrier position=%s or heading=%s change.", self.tanker:GetName(), tostring(Dchange), tostring(Hchange))
      MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
      self:T(self.lid..text)
      
      -- Update pos and orientation.
      self.orientation=vNew
      self.position=pos
      update=true
    end
    
  end
    
  return update
end

--- Activate TACAN of tanker.
-- @param #RECOVERYTANKER self
-- @param #number delay Delay in seconds.
function RECOVERYTANKER:_ActivateTACAN(delay)

  if delay and delay>0 then
  
    -- Schedule TACAN activation.
    self:ScheduleOnce(delay, RECOVERYTANKER._ActivateTACAN, self)
    
  else

    -- Get tanker unit.
    local unit=self.tanker:GetUnit(1)
    
    -- Check if unit is alive.
    if unit and unit:IsAlive() then
    
      -- Debug message.
      local text=string.format("Activating TACAN beacon: channel=%d mode=%s, morse=%s.", self.TACANchannel, self.TACANmode, self.TACANmorse)
      MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
      self:T(self.lid..text)      
    
      -- Create a new beacon and activate TACAN.
      self.beacon=BEACON:New(unit)
      self.beacon:ActivateTACAN(self.TACANchannel, self.TACANmode, self.TACANmorse, true)
            
    else
      self:E(self.lid.."ERROR: Recovery tanker is not alive!")
    end
    
  end

end

--- Self made race track pattern. Not working as desired, since tanker changes course too rapidly after each waypoint.
-- @param #RECOVERYTANKER self
-- @return #table Table of pattern waypoints.
function RECOVERYTANKER:_Pattern()

  -- Carrier heading.
  local hdg=self.carrier:GetHeading()
  
  -- Pattern altitude
  local alt=self.altitude
  
  -- Carrier position.
  local Carrier=self.carrier:GetCoordinate()
  
  local width=UTILS.NMToMeters(8)
  
  -- Define race-track pattern.
  local p={}
  p[1]=self.tanker:GetCoordinate()                      -- Tanker position
  p[2]=Carrier:SetAltitude(alt)                         -- Carrier position
  p[3]=p[2]:Translate(self.distBow, hdg)                -- In front of carrier
  p[4]=p[3]:Translate(width/math.sqrt(2), hdg-45)       -- Middle front for smoother curve
  -- Probably need one more to make it go -hdg at the waypoint.
  p[5]=p[3]:Translate(width, hdg-90)                    -- In front on port
  p[6]=p[5]:Translate(self.distStern-self.distBow, hdg) -- Behind on port (sterndist<0!)
  p[7]=p[2]:Translate(self.distStern, hdg)              -- Behind carrier
  
  local wp={}
  for i=1,#p do
    local coord=p[i] --Core.Point#COORDINATE
    coord:MarkToAll(string.format("Waypoint %d", i))
    --table.insert(wp, coord:WaypointAirFlyOverPoint(nil , self.speed))
    table.insert(wp, coord:WaypointAirTurningPoint(nil , UTILS.MpsToKmph(self.speed)))
  end

  return wp
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Rescue helicopter for carrier operations.
-- 
-- Recue helicopter for carrier operations.
--
-- **Main Features:**
--
--    * Close formation with carrier.
--    * No restrictions regarding carrier waypoints and heading.
--    * Automatic respawning on empty fuel for 24/7 operations.
--    * Automatic rescuing of crashed or ejected pilots in the vicinity of the carrier.
--    * Multiple helos at different carriers due to object oriented approach.
--    * Finite State Machine (FSM) implementation.
--    
-- ## Known (DCS) Issues
-- 
--    * CH-53E does only report 27.5% fuel even if fuel is set to 100% in the ME. See [bug report](https://forums.eagle.ru/showthread.php?t=223712)
--    * CH-53E does not accept USS Tarawa as landing airbase (even it can be spawned on it).
--    * Helos dont move away from their landing position on carriers.
--
-- ===
--
-- ### Author: **funkyfranky**
-- ### Contributions: Flightcontrol (@{AI.AI_Formation} class being used here)
--
-- @module Ops.RescueHelo
-- @image Ops_RescueHelo.png

--- RESCUEHELO class.
-- @type RESCUEHELO
-- @field #string ClassName Name of the class.
-- @field #boolean Debug Debug mode on/off.
-- @field #string lid Log debug id text.
-- @field Wrapper.Unit#UNIT carrier The carrier the helo is attached to.
-- @field #string carriertype Carrier type.
-- @field #string helogroupname Name of the late activated helo template group.
-- @field Wrapper.Group#GROUP helo Helo group.
-- @field #number takeoff Takeoff type.
-- @field Wrapper.Airbase#AIRBASE airbase The airbase object acting as home base of the helo.
-- @field Core.Set#SET_GROUP followset Follow group set.
-- @field AI.AI_Formation#AI_FORMATION formation AI_FORMATION object.
-- @field #number lowfuel Low fuel threshold of helo in percent.
-- @field #number altitude Altitude of helo in meters.
-- @field #number offsetX Offset in meters to carrier in longitudinal direction.
-- @field #number offsetZ Offset in meters to carrier in latitudinal direction.
-- @field Core.Zone#ZONE_RADIUS rescuezone Zone around the carrier in which helo will rescue crashed or ejected units.
-- @field #boolean respawn If true, helo be respawned (default). If false, no respawning will happen.
-- @field #boolean respawninair If true, helo will always be respawned in air. This has no impact on the initial spawn setting.
-- @field #boolean uncontrolledac If true, use and uncontrolled helo group already present in the mission.
-- @field #boolean rescueon If true, helo will rescue crashed pilots. If false, no recuing will happen.
-- @field #number rescueduration Time the rescue helicopter hovers over the crash site in seconds.
-- @field #number rescuespeed Speed in m/s the rescue helicopter hovers at over the crash site.
-- @field #boolean rescuestopboat If true, stop carrier during rescue operations.
-- @field #boolean carrierstop If true, route of carrier was stopped.
-- @field #number HeloFuel0 Initial fuel of helo in percent. Necessary due to DCS bug that helo with full tank does not return fuel via API function.
-- @field #boolean rtb If true, Helo will be return to base on the next status check.
-- @field #number hid Unit ID of the helo group. (Global) Running number.
-- @field #string alias Alias of the spawn group.
-- @field #number uid Unique ID of this helo.
-- @field #number modex Tail number of the helo.
-- @field #number dtFollow Follow time update interval in seconds. Default 1.0 sec.
-- @extends Core.Fsm#FSM

--- Rescue Helo
--
-- ===
--
-- # Recue Helo
--
-- The rescue helo will fly in close formation with another unit, which is typically an aircraft carrier.
-- It's mission is to rescue crashed or ejected pilots. Well, and to look cool...
-- 
-- # Simple Script
-- 
-- In the mission editor you have to set up a carrier unit, which will act as "mother". In the following, this unit will be named "*USS Stennis*".
-- 
-- Secondly, you need to define a rescue helicopter group in the mission editor and set it to "**LATE ACTIVATED**". The name of the group we'll use is "*Recue Helo*".
-- 
-- The basic script is very simple and consists of only two lines. 
-- 
--      RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo")
--      RescueheloStennis:Start()
--
-- The first line will create a new @{#RESCUEHELO} object via @{#RESCUEHELO.New} and the second line starts the process by calling @{#RESCUEHELO.Start}.
-- 
-- **NOTE** that it is *very important* to define the RESCUEHELO object as **global** variable. Otherwise, the lua garbage collector will kill the formation for unknown reasons!
-- 
-- By default, the helo will be spawned on the *USS Stennis* with hot engines. Then it will take off and go on station on the starboard side of the boat.
-- 
-- Once the helo is out of fuel, it will return to the carrier. When the helo lands, it will be respawned immidiately and go back on station.
-- 
-- If a unit crashes or a pilot ejects within a radius of 30 km from the USS Stennis, the helo will automatically fly to the crash side and 
-- rescue to pilot. This will take around 5 minutes. After that, the helo will return to the Stennis, land there and bring back the poor guy.
-- When this is done, the helo will go back on station.
-- 
-- # Fine Tuning
-- 
-- The implementation allows to customize quite a few settings easily via user API functions.
-- 
-- ## Takeoff Type
-- 
-- By default, the helo is spawned with running engines on the carrier. The mission designer has set option to set the take off type via the @{#RESCUEHELO.SetTakeoff} function.
-- Or via shortcuts
-- 
--    * @{#RESCUEHELO.SetTakeoffHot}(): Will set the takeoff to hot, which is also the default.
--    * @{#RESCUEHELO.SetTakeoffCold}(): Will set the takeoff type to cold, i.e. with engines off.
--    * @{#RESCUEHELO.SetTakeoffAir}(): Will set the takeoff type to air, i.e. the helo will be spawned in air near the unit which he follows.  
-- 
-- For example,
--      RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo")
--      RescueheloStennis:SetTakeoffAir()
--      RescueheloStennis:Start()
-- will spawn the helo near the USS Stennis in air.
-- 
-- Spawning in air is not as realistic but can be useful do avoid DCS bugs and shortcomings like aircraft crashing into each other on the flight deck.
-- 
-- **Note** that when spawning in air is set, the helo will also not return to the boat, once it is out of fuel. Instead it will be respawned in air.
-- 
-- If only the first spawning should happen on the carrier, one use the @{#RESCUEHELO.SetRespawnInAir}() function to command that all subsequent spawning
-- will happen in air.
-- 
-- If the helo should no be respawned at all, one can set @{#RESCUEHELO.SetRespawnOff}(). 
-- 
-- ## Home Base
-- 
-- It is possible to define a "home base" other than the aircraft carrier using the @{#RESCUEHELO.SetHomeBase}(*airbase*) function, where *airbase* is
-- a @{Wrapper.Airbase#AIRBASE} object or simply the name of the airbase.
-- 
-- For example, one could imagine a strike group, and the helo will be spawned from another ship which has a helo pad.
-- 
--      RescueheloStennis=RESCUEHELO:New(UNIT:FindByName("USS Stennis"), "Rescue Helo")
--      RescueheloStennis:SetHomeBase(AIRBASE:FindByName("USS Normandy"))
--      RescueheloStennis:Start()
-- 
-- In this case, the helo will be spawned on the USS Normandy and then make its way to the USS Stennis to establish the formation.
-- Note that the distance to the mother ship should be rather small since the helo will go there very slowly.
-- 
-- Once the helo runs out of fuel, it will return to the USS Normandy and not the Stennis for respawning.
-- 
-- ## Formation Position
-- 
-- The position of the helo relative to the mother ship can be tuned via the functions
-- 
--    * @{#RESCUEHELO.SetAltitude}(*altitude*), where *altitude* is the altitude the helo flies at in meters. Default is 70 meters.
--    * @{#RESCUEHELO.SetOffsetX}(*distance*), where *distance is the distance in the direction of movement of the carrier. Default is 200 meters.
--    * @{#RESCUEHELO.SetOffsetZ}(*distance*), where *distance is the distance on the starboard side. Default is 100 meters.
--
-- ## Rescue Operations
--
-- By default the rescue helo will start a rescue operation if an aircraft crashes or a pilot ejects in the vicinity of the carrier.
-- This is restricted to aircraft of the same coalition as the rescue helo. Enemy (or neutral) pilots will be left on their own.
-- 
-- The standard "rescue zone" has a radius of 15 NM (~28 km) around the carrier. The radius can be adjusted via the @{#RESCUEHELO.SetRescueZone}(*radius*) functions,
-- where *radius* is the radius of the zone in nautical miles. If you use multiple rescue helos in the same mission, you might want to ensure that the radii
-- are not overlapping so that two helos try to rescue the same pilot. But it should not hurt either way.
-- 
-- Once the helo reaches the crash site, the rescue operation will last 5 minutes. This time can be changed by @{#RESCUEHELO.SetRescueDuration(*time*),
-- where *time* is the duration in minutes.
-- 
-- During the rescue operation, the helo will hover (orbit) over the crash site at a speed of 5 knots. The speed can be set by @{#RESCUEHELO.SetRescueHoverSpeed}(*speed*),
-- where the *speed* is given in knots.
-- 
-- If no rescue operations should be carried out by the helo, this option can be completely disabled by using @{#RESCUEHELO.SetRescueOff}().
--
-- # Finite State Machine
-- 
-- The implementation uses a Finite State Machine (FSM). This allows the mission designer to hook in to certain events.
-- 
--    * @{#RESCUEHELO.Start}: This eventfunction starts the FMS process and initialized parameters and spawns the helo. DCS event handling is started.
--    * @{#RESCUEHELO.Status}: This eventfunction is called in regular intervals (~60 seconds) and checks the status of the helo and carrier. It triggers other events if necessary.
--    * @{#RESCUEHELO.Rescue}: This eventfunction commands the helo to go on a rescue operation at a certain coordinate.
--    * @{#RESCUEHELO.RTB}: This eventsfunction sends the helo to its home base (usually the carrier). This is called once the helo runs low on gas.
--    * @{#RESCUEHELO.Run}: This eventfunction is called when the helo resumes normal operations and goes back on station.  
--    * @{#RESCUEHELO.Stop}: This eventfunction stops the FSM by unhandling DCS events.
--
-- The mission designer can capture these events by RESCUEHELO.OnAfter*Eventname* functions, e.g. @{#RESCUEHELO.OnAfterRescue}.
--
-- # Debugging
-- 
-- In case you have problems, it is always a good idea to have a look at your DCS log file. You find it in your "Saved Games" folder, so for example in
--     C:\Users\<yourname>\Saved Games\DCS\Logs\dcs.log
-- All output concerning the @{#RESCUEHELO} class should have the string "RESCUEHELO" in the corresponding line.
-- Searching for lines that contain the string "error" or "nil" can also give you a hint what's wrong.
-- 
-- The verbosity of the output can be increased by adding the following lines to your script:
-- 
--     BASE:TraceOnOff(true)
--     BASE:TraceLevel(1)
--     BASE:TraceClass("RESCUEHELO")
-- 
-- To get even more output you can increase the trace level to 2 or even 3, c.f. @{Core.Base#BASE} for more details.
-- 
-- ## Debug Mode
-- 
-- You have the option to enable the debug mode for this class via the @{#RESCUEHELO.SetDebugModeON} function.
-- If enabled, text messages about the helo status will be displayed on screen and marks of the pattern created on the F10 map.
--
--
-- @field #RESCUEHELO
RESCUEHELO = {
  ClassName      = "RESCUEHELO",
  Debug          = false,
  lid            = nil,
  carrier        = nil,
  carriertype    = nil,
  helogroupname  = nil,
  helo           = nil,
  airbase        = nil,
  takeoff        = nil,
  followset      = nil,
  formation      = nil,
  lowfuel        = nil,
  altitude       = nil,
  offsetX        = nil,
  offsetZ        = nil,
  rescuezone     = nil,
  respawn        = nil,
  respawninair   = nil,
  uncontrolledac = nil,
  rescueon       = nil,
  rescueduration = nil,
  rescuespeed    = nil,
  rescuestopboat = nil,
  HeloFuel0      = nil,
  rtb            = nil,
  carrierstop    = nil,
  alias          = nil,
  uid            =   0,
  modex          = nil,
  dtFollow       = nil,
}

--- Unique ID (global).
-- @field #number uid Unique ID (global).
_RESCUEHELOID=0

--- Class version.
-- @field #string version
RESCUEHELO.version="1.1.0"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- NOPE: Add messages for rescue mission.
-- NOPE: Add option to stop carrier while rescue operation is in progress? Done but NOT working. Postponed...
-- DONE: Write documentation.
-- DONE: Add option to deactivate the rescuing.
-- DONE: Possibility to add already present/spawned aircraft, e.g. for warehouse.
-- DONE: Add rescue event when aircraft crashes.
-- DONE: Make offset input parameter.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new RESCUEHELO object. 
-- @param #RESCUEHELO self
-- @param Wrapper.Unit#UNIT carrierunit Carrier unit object or simply the unit name.
-- @param #string helogroupname Name of the late activated rescue helo template group.
-- @return #RESCUEHELO RESCUEHELO object.
function RESCUEHELO:New(carrierunit, helogroupname)

  -- Inherit everthing from FSM class.
  local self = BASE:Inherit(self, FSM:New()) -- #RESCUEHELO
  
  -- Catch case when just the unit name is passed.
  if type(carrierunit)=="string" then
    self.carrier=UNIT:FindByName(carrierunit)
  else
    self.carrier=carrierunit
  end
  
  -- Carrier type.
  self.carriertype=self.carrier:GetTypeName()
  
  -- Helo group name.
  self.helogroupname=helogroupname
  
  -- Increase ID.
  _RESCUEHELOID=_RESCUEHELOID+1
  
  -- Unique ID of this helo.
  self.uid=_RESCUEHELOID
  
  -- Save self in static object. Easier to retrieve later.
  self.carrier:SetState(self.carrier, string.format("RESCUEHELO_%d", self.uid) , self)
  
  -- Set unique spawn alias.
  self.alias=string.format("%s_%s_%02d", self.carrier:GetName(), self.helogroupname, _RESCUEHELOID)
  
  -- Log ID.
  self.lid=string.format("RESCUEHELO %s | ", self.alias)
    
  -- Init defaults.  
  self:SetHomeBase(AIRBASE:FindByName(self.carrier:GetName()))
  self:SetTakeoffHot()
  self:SetLowFuelThreshold()
  self:SetAltitude()
  self:SetOffsetX()
  self:SetOffsetZ()
  self:SetRespawnOn()
  self:SetRescueOn()
  self:SetRescueZone()
  self:SetRescueHoverSpeed()
  self:SetRescueDuration()
  self:SetFollowTimeInterval()
  self:SetRescueStopBoatOff()
  
  -- Some more.
  self.rtb=false
  self.carrierstop=false
  
  -- Debug trace.
  if false then
    self.Debug=true
    BASE:TraceOnOff(true)
    BASE:TraceClass(self.ClassName)
    BASE:TraceLevel(1)
  end
  
  -----------------------
  --- FSM Transitions ---
  -----------------------
  
  -- Start State.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  --                 From State  -->  Event    -->  To State
  self:AddTransition("Stopped",       "Start",      "Running")
  self:AddTransition("Running",       "Rescue",     "Rescuing")
  self:AddTransition("Running",       "RTB",        "Returning")
  self:AddTransition("Rescuing",      "RTB",        "Returning")
  self:AddTransition("Returning",     "Returned",   "Returned")
  self:AddTransition("Running",       "Run",        "Running")
  self:AddTransition("Returned",      "Run",        "Running")
  self:AddTransition("*",             "Status",     "*")
  self:AddTransition("*",             "Stop",       "Stopped")


  --- Triggers the FSM event "Start" that starts the rescue helo. Initializes parameters and starts event handlers.
  -- @function [parent=#RESCUEHELO] Start
  -- @param #RESCUEHELO self

  --- Triggers the FSM event "Start" that starts the rescue helo after a delay. Initializes parameters and starts event handlers.
  -- @function [parent=#RESCUEHELO] __Start
  -- @param #RESCUEHELO self
  -- @param #number delay Delay in seconds.

  --- On after "Start" event function. Called when FSM is started.
  -- @function [parent=#RESCUEHELO] OnAfterStart
  -- @param #RECOVERYTANKER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- Triggers the FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate.
  -- @function [parent=#RESCUEHELO] Rescue
  -- @param #RESCUEHELO self
  -- @param Core.Point#COORDINATE RescueCoord Coordinate where the resue mission takes place.

  --- Triggers the delayed FSM event "Rescue" that sends the helo on a rescue mission to a specifc coordinate.
  -- @function [parent=#RESCUEHELO] __Rescue
  -- @param #RESCUEHELO self
  -- @param #number delay Delay in seconds.
  -- @param Core.Point#COORDINATE RescueCoord Coordinate where the resue mission takes place.

  --- On after "Rescue" event user function. Called when a the the helo goes on a rescue mission.
  -- @function [parent=#RESCUEHELO] OnAfterRescue
  -- @param #RESCUEHELO self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Point#COORDINATE RescueCoord Crash site where the rescue operation takes place.


  --- Triggers the FSM event "RTB" that sends the helo home.
  -- @function [parent=#RESCUEHELO] RTB
  -- @param #RESCUEHELO self
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base.

  --- Triggers the FSM event "RTB" that sends the helo home after a delay.
  -- @function [parent=#RESCUEHELO] __RTB
  -- @param #RESCUEHELO self
  -- @param #number delay Delay in seconds.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base.

  --- On after "RTB" event user function. Called when a the the helo returns to its home base.
  -- @function [parent=#RESCUEHELO] OnAfterRTB
  -- @param #RESCUEHELO self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase to return to. Default is the home base.


  --- Triggers the FSM event "Returned" after the helo has landed.
  -- @function [parent=#RESCUEHELO] Returned
  -- @param #RESCUEHELO self
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed.

  --- Triggers the delayed FSM event "Returned" after the helo has landed.
  -- @function [parent=#RESCUEHELO] __Returned
  -- @param #RESCUEHELO self
  -- @param #number delay Delay in seconds.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed.

  --- On after "Returned" event user function. Called when a the the helo has landed at an airbase.
  -- @function [parent=#RESCUEHELO] OnAfterReturned
  -- @param #RESCUEHELO self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase the helo has landed.


  --- Triggers the FSM event "Run".
  -- @function [parent=#RESCUEHELO] Run
  -- @param #RESCUEHELO self

  --- Triggers the delayed FSM event "Run".
  -- @function [parent=#RESCUEHELO] __Run
  -- @param #RESCUEHELO self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Status" that updates the helo status.
  -- @function [parent=#RESCUEHELO] Status
  -- @param #RESCUEHELO self

  --- Triggers the delayed FSM event "Status" that updates the helo status.
  -- @function [parent=#RESCUEHELO] __Status
  -- @param #RESCUEHELO self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Stop" that stops the rescue helo. Event handlers are stopped.
  -- @function [parent=#RESCUEHELO] Stop
  -- @param #RESCUEHELO self

  --- Triggers the FSM event "Stop" that stops the rescue helo after a delay. Event handlers are stopped.
  -- @function [parent=#RESCUEHELO] __Stop
  -- @param #RESCUEHELO self
  -- @param #number delay Delay in seconds.
  
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set low fuel state of helo. When fuel is below this threshold, the helo will RTB or be respawned if takeoff type is in air.
-- @param #RESCUEHELO self
-- @param #number threshold Low fuel threshold in percent. Default 5%.
-- @return #RESCUEHELO self
function RESCUEHELO:SetLowFuelThreshold(threshold)
  self.lowfuel=threshold or 5
  return self
end

--- Set home airbase of the helo. This is the airbase where the helo is spawned (if not in air) and will go when it is out of fuel.
-- @param #RESCUEHELO self
-- @param Wrapper.Airbase#AIRBASE airbase The home airbase. Can be the airbase name (passed as a string) or a Moose AIRBASE object.
-- @return #RESCUEHELO self
function RESCUEHELO:SetHomeBase(airbase)
  if type(airbase)=="string" then
    self.airbase=AIRBASE:FindByName(airbase)
  else
    self.airbase=airbase
  end
  if not self.airbase then
    self:E(self.lid.."ERROR: Airbase is nil!")
  end
  return self
end

--- Set rescue zone radius. Crashed or ejected units inside this radius of the carrier will be rescued if possible.
-- @param #RESCUEHELO self
-- @param #number radius Radius of rescue zone in nautical miles. Default is 15 NM.
-- @return #RESCUEHELO self
function RESCUEHELO:SetRescueZone(radius)
  radius=UTILS.NMToMeters(radius or 15)
  self.rescuezone=ZONE_UNIT:New("Rescue Zone", self.carrier, radius)
  return self
end

--- Set rescue hover speed.
-- @param #RESCUEHELO self
-- @param #number speed Speed in knots. Default 5 kts.
-- @return #RESCUEHELO self
function RESCUEHELO:SetRescueHoverSpeed(speed)
  self.rescuespeed=UTILS.KnotsToMps(speed or 5)
  return self
end

--- Set rescue duration. This is the time it takes to rescue a pilot at the crash site.
-- @param #RESCUEHELO self
-- @param #number duration Duration in minutes. Default 5 min.
-- @return #RESCUEHELO self
function RESCUEHELO:SetRescueDuration(duration)
  self.rescueduration=(duration or 5)*60
  return self
end

--- Activate rescue option. Crashed and ejected pilots will be rescued. This is the default setting.
-- @param #RESCUEHELO self
-- @return #RESCUEHELO self
function RESCUEHELO:SetRescueOn()
  self.rescueon=true
  return self
end

--- Deactivate rescue option. Crashed and ejected pilots will not be rescued.
-- @param #RESCUEHELO self
-- @return #RESCUEHELO self
function RESCUEHELO:SetRescueOff()
  self.rescueon=false
  return self
end

--- Stop carrier during rescue operations. NOT WORKING!
-- @param #RESCUEHELO self
-- @return #RESCUEHELO self
function RESCUEHELO:SetRescueStopBoatOn()
  self.rescuestopboat=true
  return self
end

--- Do not stop carrier during rescue operations. This is the default setting.
-- @param #RESCUEHELO self
-- @return #RESCUEHELO self
function RESCUEHELO:SetRescueStopBoatOff()
  self.rescuestopboat=false
  return self
end


--- Set takeoff type.
-- @param #RESCUEHELO self
-- @param #number takeofftype Takeoff type. Default SPAWN.Takeoff.Hot.
-- @return #RESCUEHELO self
function RESCUEHELO:SetTakeoff(takeofftype)
  self.takeoff=takeofftype or SPAWN.Takeoff.Hot
  return self
end

--- Set takeoff with engines running (hot).
-- @param #RESCUEHELO self
-- @return #RESCUEHELO self
function RESCUEHELO:SetTakeoffHot()
  self:SetTakeoff(SPAWN.Takeoff.Hot)
  return self
end

--- Set takeoff with engines off (cold).
-- @param #RESCUEHELO self
-- @return #RESCUEHELO self
function RESCUEHELO:SetTakeoffCold()
  self:SetTakeoff(SPAWN.Takeoff.Cold)
  return self
end

--- Set takeoff in air near the carrier.
-- @param #RESCUEHELO self
-- @return #RESCUEHELO self
function RESCUEHELO:SetTakeoffAir()
  self:SetTakeoff(SPAWN.Takeoff.Air)
  return self
end

--- Set altitude of helo.
-- @param #RESCUEHELO self
-- @param #number alt Altitude in meters. Default 70 m.
-- @return #RESCUEHELO self
function RESCUEHELO:SetAltitude(alt)
  self.altitude=alt or 70
  return self
end

--- Set offset parallel to orientation of carrier.
-- @param #RESCUEHELO self
-- @param #number distance Offset distance in meters. Default 200 m (~660 ft).
-- @return #RESCUEHELO self
function RESCUEHELO:SetOffsetX(distance)
  self.offsetX=distance or 200
  return self
end

--- Set offset perpendicular to orientation to carrier.
-- @param #RESCUEHELO self
-- @param #number distance Offset distance in meters. Default 240 m (~780 ft).
-- @return #RESCUEHELO self
function RESCUEHELO:SetOffsetZ(distance)
  self.offsetZ=distance or 240
  return self
end


--- Enable respawning of helo. Note that this is the default behaviour. 
-- @param #RESCUEHELO self 
-- @return #RESCUEHELO self
function RESCUEHELO:SetRespawnOn()
  self.respawn=true
  return self
end

--- Disable respawning of helo.
-- @param #RESCUEHELO self 
-- @return #RESCUEHELO self
function RESCUEHELO:SetRespawnOff()
  self.respawn=false
  return self
end

--- Set whether helo shall be respawned or not.
-- @param #RESCUEHELO self
-- @param #boolean switch If true (or nil), helo will be respawned. If false, helo will not be respawned. 
-- @return #RESCUEHELO self
function RESCUEHELO:SetRespawnOnOff(switch)
  if switch==nil or switch==true then
    self.respawn=true
  else
    self.respawn=false
  end
  return self
end

--- Helo will be respawned in air, even it was initially spawned on the carrier.
-- So only the first spawn will be on the carrier while all subsequent spawns will happen in air.
-- This allows for undisrupted operations and less problems on the carrier deck.
-- @param #RESCUEHELO self
-- @return #RESCUEHELO self
function RESCUEHELO:SetRespawnInAir()
  self.respawninair=true
  return self
end

--- Set modex (tail number) of the helo.
-- @param #RESCUEHELO self
-- @param #number modex Tail number.
-- @return #RESCUEHELO self
function RESCUEHELO:SetModex(modex)
  self.modex=modex
  return self
end

--- Set follow time update interval.
-- @param #RESCUEHELO self
-- @param #number dt Time interval in seconds. Default 1.0 sec.
-- @return #RESCUEHELO self
function RESCUEHELO:SetFollowTimeInterval(dt)
  self.dtFollow=dt or 1.0
  return self
end

--- Use an uncontrolled aircraft already present in the mission rather than spawning a new helo as initial rescue helo.
-- This can be useful when interfaced with, e.g., a warehouse.
-- The group name is the one specified in the @{#RESCUEHELO.New} function.
-- @param #RESCUEHELO self
-- @return #RESCUEHELO self
function RESCUEHELO:SetUseUncontrolledAircraft()
  self.uncontrolledac=true
  return self
end

--- Activate debug mode. Display debug messages on screen.
-- @param #RESCUEHELO self
-- @return #RESCUEHELO self
function RESCUEHELO:SetDebugModeON()
  self.Debug=true
  return self
end

--- Deactivate debug mode. This is also the default setting.
-- @param #RESCUEHELO self
-- @return #RESCUEHELO self
function RESCUEHELO:SetDebugModeOFF()
  self.Debug=false
  return self
end

--- Check if helo is returning to base.
-- @param #RESCUEHELO self
-- @return #boolean If true, helo is returning to base. 
function RESCUEHELO:IsReturning()
  return self:is("Returning")
end

--- Check if helo is operating.
-- @param #RESCUEHELO self
-- @return #boolean If true, helo is operating. 
function RESCUEHELO:IsRunning()
  return self:is("Running")
end

--- Check if helo is on a rescue mission.
-- @param #RESCUEHELO self
-- @return #boolean If true, helo is rescuing somebody. 
function RESCUEHELO:IsRescuing()
  return self:is("Rescuing")
end

--- Check if FMS was stopped.
-- @param #RESCUEHELO self
-- @return #boolean If true, is stopped. 
function RESCUEHELO:IsStopped()
  return self:is("Stopped")
end

--- Alias of helo spawn group.
-- @param #RESCUEHELO self
-- @return #string Alias of the helo. 
function RESCUEHELO:GetAlias()
  return self.alias
end

--- Get unit name of the spawned helo.
-- @param #RESCUEHELO self
-- @return #string Name of the helo unit or nil if it does not exist. 
function RESCUEHELO:GetUnitName()
  local unit=self.helo:GetUnit(1)
  if unit then
    return unit:GetName()
  end
  return nil
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- EVENT functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Handle landing event of rescue helo.
-- @param #RESCUEHELO self
-- @param Core.Event#EVENTDATA EventData Event data.
function RESCUEHELO:OnEventLand(EventData)
  local group=EventData.IniGroup --Wrapper.Group#GROUP
  
  if group and group:IsAlive() then
  
    -- Group name that landed.
    local groupname=group:GetName()
  
    -- Check that it was our helo that landed.
    if groupname==self.helo:GetName() then
    
      local airbase=nil --Wrapper.Airbase#AIRBASE
      local airbasename="unknown"
      if EventData.Place then
        airbase=EventData.Place
        airbasename=airbase:GetName()
      end
    
      -- Respawn the Helo.
      local text=string.format("Rescue helo group %s landed at airbase %s.", groupname, airbasename)
      MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
      self:T(self.lid..text)
      
      -- Helo has rescued someone.
      -- TODO: Add "Rescued" event.
      if self:IsRescuing() then        
        self:T(self.lid..string.format("Rescue helo %s returned from rescue operation.", groupname))
      end
      
      -- Check if takeoff air or respawn in air is set. Landing event should not happen unless the helo was on a rescue mission.
      if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then
        
        if not self:IsRescuing() then

          self:E(self.lid..string.format("WARNING: Rescue helo %s landed. This should not happen for Takeoff=Air or respawninair=true and no rescue operation in progress.", groupname))
          
        end              
      end
      
      -- Trigger returned event. Respawn at current airbase.
      self:__Returned(3, airbase)
            
    end
  end
end

--- A unit crashed or a player ejected.
-- @param #RESCUEHELO self
-- @param Core.Event#EVENTDATA EventData Event data.
function RESCUEHELO:_OnEventCrashOrEject(EventData)
  self:F2({eventdata=EventData})
  
  -- NOTE: Careful here. Eject and crash events will probably happen for the same unit!
  
  -- Check that there is an initiating unit in the event data.
  if EventData and EventData.IniUnit then

    -- Crashed or ejected unit.
    local unit=EventData.IniUnit  
    local unitname=tostring(EventData.IniUnitName)
    
    -- Check that it was not the rescue helo itself that crashed.
    if EventData.IniGroupName~=self.helo:GetName() then
    
      -- Debug.
      local text=string.format("Unit %s crashed or ejected.", unitname)
      MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
      self:T(self.lid..text)

      -- Get coordinate of unit.      
      --local coord=unit:GetCoordinate()
      local Vec3 = EventData.IniDCSUnit:getPoint() -- Vec3
      local coord = COORDINATE:NewFromVec3(Vec3)
      
      if coord and self.rescuezone:IsCoordinateInZone(coord) then
      
      -- This does not seem to work any more. Is:Alive returns flase on ejection.
      -- Unit "alive" and in our rescue zone.
      --if unit:IsAlive() and unit:IsInZone(self.rescuezone) then
        -- Get coordinate of crashed unit.
        --local coord=unit:GetCoordinate()
        
        -- Debug mark on map.
        if self.Debug then
          coord:MarkToCoalition(self.lid..string.format("Crash site of unit %s.", unitname), self.helo:GetCoalition())
        end
        
        -- Check that coalition is the same.
        local rightcoalition=EventData.IniGroup:GetCoalition()==self.helo:GetCoalition()
      
        -- Only rescue if helo is "running" and not, e.g., rescuing already.
        if self:IsRunning() and self.rescueon and rightcoalition then 
          self:Rescue(coord)
        end
      
      end
      
    else
    
      -- Error message.
      self:E(self.lid..string.format("Rescue helo %s crashed!", unitname))
      
      -- Stop FSM.
      self:Stop()
      
      -- Restart.
      if self.respawn then
        self:__Start(5)
      end
    
    end
    
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM states
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after Start event. Starts the warehouse. Addes event handlers and schedules status updates of reqests and queue.
-- @param #RESCUEHELO self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RESCUEHELO:onafterStart(From, Event, To)

  -- Events are handled my MOOSE.
  local text=string.format("Starting Rescue Helo Formation v%s for carrier unit %s of type %s.", RESCUEHELO.version, self.carrier:GetName(), self.carriertype)
  self:I(self.lid..text)
  
  -- Handle events.
  self:HandleEvent(EVENTS.Land)
  self:HandleEvent(EVENTS.Crash,    self._OnEventCrashOrEject)
  self:HandleEvent(EVENTS.Ejection, self._OnEventCrashOrEject)
  
  -- Delay before formation is started.
  local delay=120
    
  -- Spawn helo. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine.
  local Spawn=SPAWN:NewWithAlias(self.helogroupname, self.alias)
  
  -- Set modex for spawn.
  Spawn:InitModex(self.modex)

  -- Spawn in air or at airbase.
  if self.takeoff==SPAWN.Takeoff.Air then
  
    -- Carrier heading
    local hdg=self.carrier:GetHeading()
    
    -- Spawn distance in front of carrier.
    local dist=UTILS.NMToMeters(0.2)
    
    -- Coordinate behind the carrier. Altitude at least 100 meters for spawning because it drops down a bit.
    local Carrier=self.carrier:GetCoordinate():Translate(dist, hdg):SetAltitude(math.max(100, self.altitude))
    
    -- Orientation of spawned group.
    Spawn:InitHeading(hdg)
    
    -- Spawn at coordinate.
    self.helo=Spawn:SpawnFromCoordinate(Carrier)
    
    -- Start formation in 1 seconds
    delay=1
    
  else  
 
    -- Check if an uncontrolled helo group was requested.
    if self.uncontrolledac then
    
      -- Use an uncontrolled aircraft group.
      self.helo=GROUP:FindByName(self.helogroupname)
      
      if self.helo and self.helo:IsAlive() then
      
        -- Start uncontrolled group.
        self.helo:StartUncontrolled()
        
        -- Delay before formation is started.
        delay=60
        
      else
        -- No group of that name!
        self:E(string.format("ERROR: No uncontrolled (alive) rescue helo group with name %s could be found!", self.helogroupname))
        return
      end
       
    else    

      -- Spawn at airbase.
      self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff, nil, AIRBASE.TerminalType.HelicopterUsable)
      
      -- Delay before formation is started.
      if self.takeoff==SPAWN.Takeoff.Runway then
        delay=5
      elseif self.takeoff==SPAWN.Takeoff.Hot then
        delay=30
      elseif self.takeoff==SPAWN.Takeoff.Cold then
        delay=60
      end
      
    end
    
  end
  
  -- Set of group(s) to follow Mother.
  self.followset=SET_GROUP:New()
  self.followset:AddGroup(self.helo)
  
  -- Get initial fuel.
  self.HeloFuel0=self.helo:GetFuel()
  
  -- Define AI Formation object.
  self.formation=AI_FORMATION:New(self.carrier, self.followset, "Helo Formation with Carrier", "Follow Carrier at given parameters.")
  
  -- Formation parameters.
  self.formation:FormationCenterWing(-self.offsetX, 50, math.abs(self.altitude), 50, self.offsetZ, 50)
  
  -- Set follow time interval.
  self.formation:SetFollowTimeInterval(self.dtFollow)
  
  -- Formation mode.
  self.formation:SetFlightModeFormation(self.helo)
  
  -- Start formation FSM.
  self.formation:__Start(delay)
  
  -- Init status check
  self:__Status(1)
  
  return self
end

--- On after Status event. Checks player status.
-- @param #RESCUEHELO self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RESCUEHELO:onafterStatus(From, Event, To)

  -- Get current time.
  local time=timer.getTime()

  -- Check if helo is running and not RTBing already or rescuing.
  if self.helo and self.helo:IsAlive() then

    -------------------
    -- HELO is ALIVE --
    ------------------- 

    -- Get (relative) fuel wrt to initial fuel of helo (DCS bug https://forums.eagle.ru/showthread.php?t=223712)
    local fuel=self.helo:GetFuel()*100
    local fuelrel=fuel/self.HeloFuel0
    local life=self.helo:GetUnit(1):GetLife()
    local life0=self.helo:GetUnit(1):GetLife0()
    local lifeR=self.helo:GetUnit(1):GetLifeRelative()
  
    -- Report current fuel.
    local text=string.format("Rescue Helo %s: state=%s fuel=%.1f, rel.fuel=%.1f, life=%.1f/%.1f=%d", self.helo:GetName(), self:GetState(), fuel, fuelrel, life, life0, lifeR*100)
    MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
    self:T(self.lid..text)
  
    if self:IsRunning() then
    
      -- Check if fuel is low.
      if fuel<self.lowfuel then
      
        -- Check if spawn in air is activated.
        if self.takeoff==SPAWN.Takeoff.Air or self.respawninair then
        
          -- Check if respawn is enabled.
          if self.respawn then
          
            -- Set modex for respawn.
            self.helo:InitModex(self.modex)
            
            -- Respawn helo in air.
            self.helo=self.helo:Respawn(nil, true)
            
            -- XXX: ATTENTION: if helo automatically RTBs on low fuel, it goes a bit crazy. The formation is not stopped and he partially dives into the water.
            -- Also trying to find a ship to land on he flies right through it.            
            --self.helo:OptionRTBBingoFuel(false)
            
          end
          
        else
        
          -- Send helo back to base.
          self:RTB(self.airbase)
        
        end
      
      end
      
    elseif self:IsRescuing() then
    
      -- Helo is on a rescue mission.
    
    end

    -- Call status again in 30 seconds.
    if not self:IsStopped() then
      self:__Status(-30)
    end

  else
  
    ------------------
    -- HELO is DEAD --
    ------------------
    
    if not self:IsStopped() then
    
      self:E(self.lid.."Rescue helo is NOT alive (and not stopped)!")
    
      -- Stop FSM.
      self:Stop()
      
      -- Restart FSM after 5 seconds.
      if self.respawn then
        self:__Start(5)
      end
      
    end
  end
    
end

--- On after "Run" event. FSM will go to "Running" state. If formation is stopped, it will be started again.
-- @param #RESCUEHELO self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RESCUEHELO:onafterRun(From, Event, To)
  
  -- Restart formation if stopped.
  if self.formation:Is("Stopped") then
    -- Debug info.
    local text=string.format("Restarting formation of rescue helo %s.", self.helo:GetName())
    MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
    self:T(self.lid..text)
    
    -- Start formation.
    self.formation:Start()
  end
  
end


--- Task to send the helo RTB.
-- @param #RESCUEHELO self
-- @return DCS#Task DCS Task table.
function RESCUEHELO:_TaskRTB()
  
  -- Set RTB switch so on next status update, the helo is respawned with RTB waypoints.
  --rescuehelo.rtb=true

  -- Name of the warehouse (static) object.
  local carriername=self.carrier:GetName()

  -- Task script.
  local DCSScript = {}
  DCSScript[#DCSScript+1] = string.format('local mycarrier = UNIT:FindByName(\"%s\") ', carriername)                       -- The carrier unit that holds the self object.
  DCSScript[#DCSScript+1] = string.format('local myhelo    = mycarrier:GetState(mycarrier, \"RESCUEHELO_%d\") ', self.uid) -- Get the RESCUEHELO self object.
  DCSScript[#DCSScript+1] = string.format('myhelo:RTB()')                                                                  -- Call the function, e.g. myhelo.(self)

  -- Create task.
  local DCSTask = CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript)))

  return DCSTask
  
  -- This made DCS crash to desktop!
  --rescuehelo:RTB()
end

--- On after "Rescue" event. Helo will fly to the given coordinate, orbit there for 5 minutes and then return to the carrier.
-- @param #RESCUEHELO self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE RescueCoord Coordinate where the rescue should happen.
function RESCUEHELO:onafterRescue(From, Event, To, RescueCoord)

  -- Debug message.
  local text=string.format("Helo %s is send to rescue mission.", self.helo:GetName())
  MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
  self:I(self.lid..text)
  
  -- Waypoint array.
  local wp={}
  
  --local RescueTask=self.helo:TaskControlled(self.helo:TaskOrbitCircle(20, 2, RescueCoord), self.helo:TaskCondition(nil, nil, nil, nil, 5*60, nil))
   
  -- Rescue task: Orbit at crash site for ~5 minutes at 20 meters altitude and ~10 km/h.
  local RescueTask={}
  RescueTask.id="ControlledTask"
  RescueTask.params={}
  RescueTask.params.task=self.helo:TaskOrbit(RescueCoord, 20, self.rescuespeed)
  RescueTask.params.stopCondition={duration=self.rescueduration}
  
  -- Passing waypoint taskfunction
  --local TaskRTB=self.helo:TaskFunction("RESCUEHELO._TaskRTB", self)
  local TaskRTB=self:_TaskRTB()
  
  -- Rescue speed 90% of max possible.
  local speed=self.helo:GetSpeedMax()*0.9
  
  -- Set Waypoints:
  
  -- Current position.
  wp[1]=self.helo:GetCoordinate():WaypointAirTurningPoint(nil, speed, {}, "Current Position")
  
  -- Go to crash site and hover a bit.
  wp[2]=RescueCoord:SetAltitude(50):WaypointAirTurningPoint(nil, speed, {RescueTask, TaskRTB}, "Crash Site")
  
  -- Route helo back home and respawn next status update.
  wp[3]=self.airbase:GetCoordinate():SetAltitude(50):WaypointAirTurningPoint(nil, speed, {}, "RTB")
  
  -- Unfortunately not possible reliably due to DCS bug that units dont land or just land on any nearby base.
  --wp[3]=self.airbase:GetCoordinate():SetAltitude(70):WaypointAirLanding(200, self.airbase, {}, "Land at Home Base")

  -- Initialize WP and route helo.
  self.helo:WayPointInitialize(wp)

  -- Set task.
  self.helo:Route(wp, 1)
  
  -- Stop formation.
  self.formation:Stop()
  
end

--- On after RTB event. Send helo back to carrier.
-- @param #RESCUEHELO self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Airbase#AIRBASE airbase The base to return to. Default is the home base.
function RESCUEHELO:onafterRTB(From, Event, To, airbase)

  -- Set airbase.
  airbase=airbase or self.airbase

  -- Debug message.
  local text=string.format("Rescue helo %s is returning to airbase %s.", self.helo:GetName(), airbase:GetName())
  MESSAGE:New(text, 10, "DEBUG"):ToAllIf(self.Debug)
  self:T(self.lid..text)
  
  -- Stop formation.
  if From=="Running" then
    self.formation:Stop()
  end
    
  -- Route helo back home. It is respawned! But this is the only way to ensure that it actually lands at the airbase.
  self:RouteRTB(airbase)
end

--- On after Returned event. Helo has landed.
-- @param #RESCUEHELO self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Airbase#AIRBASE airbase The base to which the helo has returned.
function RESCUEHELO:onafterReturned(From, Event, To, airbase)

  if airbase then
    local airbasename=airbase:GetName()
    self:I(self.lid..string.format("Helo returned to airbase %s", tostring(airbasename)))
  else
    self:E(self.lid..string.format("WARNING: Helo landed but airbase (EventData.Place) is nil!"))
  end
  
  -- Respawn helo at current airbase.
  if self.respawn then
  
    -- Set modex for respawn.
    self.helo:InitModex(self.modex)
  
    -- Respawn helo at current airbase.
    if self.helo and self.helo:IsAlive() then
      self:ScheduleOnce(5, self.helo.RespawnAtCurrentAirbase, self.helo)
    end
    
    -- Restart the formation.
    self:__Run(10)
  end  

end

--- On after Stop event. Unhandle events and stop status updates. If helo is alive, it is despawned.
-- @param #RESCUEHELO self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function RESCUEHELO:onafterStop(From, Event, To)

  -- Stop formation
  self.formation:Stop()
    
  -- Unhandle events.
  self:UnHandleEvent(EVENTS.Land)
  self:UnHandleEvent(EVENTS.Crash)
  self:UnHandleEvent(EVENTS.Ejection)
  
  -- Clear all pending FSM events.
  self.CallScheduler:Clear()

  -- If helo is alive, despawn it.
  if self.helo and self.helo:IsAlive() then
    self:I(self.lid.."Stopping FSM and despawning helo.")
    self.helo:Destroy()
  else
    self:I(self.lid.."Stopping FSM. Helo was not alive.")
  end
  
end


--- Route helo back to its home base.
-- @param #RESCUEHELO self
-- @param Wrapper.Airbase#AIRBASE RTBAirbase
-- @param #number Speed Speed.
function RESCUEHELO:RouteRTB(RTBAirbase, Speed)

  -- If speed is not given take 80% of max speed.
  local Speed=Speed or self.helo:GetSpeedMax()*0.8
  
  -- Curent (from) waypoint.
  local coord=self.helo:GetCoordinate()
  local PointFrom=coord:WaypointAirTurningPoint(nil, Speed, {}, "Current")
  
  -- Airbase coordinate.
  --local PointAirbase=RTBAirbase:GetCoordinate():SetAltitude(100):WaypointAirTurningPoint(nil ,Speed)
  
  -- Landing waypoint. More general than prev version since it should also work with FAPRS and ships.
  local PointLanding=RTBAirbase:GetCoordinate():SetAltitude(20):WaypointAirLanding(Speed, RTBAirbase, {}, "Landing")
  
  -- Waypoint table.
  local Points={PointFrom, PointLanding}
  
  -- Get group template.
  local Template=self.helo:GetTemplate()
  
  -- Set route points.
  Template.route.points=Points
  
  -- Set modex for respawn.
  self.helo:InitModex(self.modex)          
    
  -- Respawn the group.
  self.helo=self.helo:Respawn(Template, true)
  
  -- Route the group or this will not work.
  self.helo:Route(Points, 1)
  
  return self
end


------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---@diagnostic disable: cast-local-type
--- **Ops** - Automatic Terminal Information Service (ATIS).
--
-- ===
--
-- **Main Features:**
--
--    * Wind direction and speed
--    * Visibility
--    * Cloud coverage, base and ceiling
--    * Temperature
--    * Dew point (approximate as there is no relative humidity in DCS yet)
--    * Pressure QNH/QFE
--    * Weather phenomena: rain, thunderstorm, fog, dust
--    * Active runway based on wind direction
--    * Tower frequencies
--    * More than 180 voice overs
--    * Airbase names pronounced in locale accent (russian, US, french, arabic)
--    * Option to present information in imperial or metric units
--    * Runway length and airfield elevation (optional)
--    * Frequencies/channels of nav aids (ILS, VOR, NDB, TACAN, PRMG, RSBN) (optional)
--    * SRS Simple-Text-To-Speech (MSRS) integration (no sound files necessary)
--
-- ===
--
-- ## Youtube Videos:
--
--    * [ATIS v0.1 Caucasus - Batumi (WIP)](https://youtu.be/MdH9FmbNabo)
--    * [ATIS v0.2 Nevada - Nellis AFB (WIP)](https://youtu.be/8CT_9AoPrTk)
--    * [ATIS v0.3 Persion Gulf - Abu Dhabi/Dubai International](https://youtu.be/NjkKvPz6ovM)
--    * [ATIS Airport Names Sound Check](https://youtu.be/qIE_OUQNAc0)
--
-- ===
--
-- ## Missions: Example missions can be found [here](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20ATIS)
--
-- ===
--
-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases)
--
-- ===
--
-- Automatic terminal information service, or ATIS, is a continuous broadcast of recorded aeronautical information in busier terminal areas, *i.e.* airports and their immediate surroundings.
-- ATIS broadcasts contain essential information, such as current weather information, active runways, and any other information required by the pilots.
--
-- ===
--
-- ### Author: **funkyfranky**
-- ### Additions for SRS and FARP: **applevangelist**
--
-- @module Ops.ATIS
-- @image OPS_ATIS.png

--- ATIS class.
-- @type ATIS
-- @field #string ClassName Name of the class.
-- @field #string lid Class id string for output to DCS log file.
-- @field #string theatre DCS map name.
-- @field #string airbasename The name of the airbase.
-- @field Wrapper.Airbase#AIRBASE airbase The airbase object.
-- @field #number frequency Radio frequency in MHz.
-- @field #number modulation Radio modulation 0=AM or 1=FM.
-- @field #number power Radio power in Watts. Default 100 W.
-- @field Sound.RadioQueue#RADIOQUEUE radioqueue Radio queue for broadcasing messages.
-- @field #string soundpath Path to sound files.
-- @field #string soundpathAirports Path to airport names sound files.
-- @field #string soundpathNato Path to NATO alphabet sound files.
-- @field #string relayunitname Name of the radio relay unit.
-- @field #table towerfrequency Table with tower frequencies.
-- @field #string activerunway The active runway specified by the user.
-- @field #number subduration Duration how long subtitles are displayed in seconds.
-- @field #boolean metric If true, use metric units. If false, use imperial (default).
-- @field #boolean PmmHg If true, give pressure in millimeters of Mercury. Default is inHg for imperial and hectopascal (hPa, which is the same as millibar - mbar) for metric units.
-- @field #boolean qnhonly If true, suppresses reporting QFE. Default is to report both QNH and QFE.
-- @field #boolean TDegF If true, give temperature in degrees Fahrenheit. Default is in degrees Celsius independent of chosen unit system.
-- @field #number zuludiff Time difference local vs. zulu in hours.
-- @field #boolean zulutimeonly If true, suppresses report of local time, sunrise, and sunset.
-- @field #number magvar Magnetic declination/variation at the airport in degrees.
-- @field #table ils Table of ILS frequencies (can be runway specific).
-- @field #table ndbinner Table of inner NDB frequencies (can be runway specific).
-- @field #table ndbouter Table of outer NDB frequencies (can be runway specific).
-- @field #number tacan TACAN channel.
-- @field #number vor VOR frequency.
-- @field #number rsbn RSBN channel.
-- @field #table prmg PRMG channels (can be runway specific).
-- @field #boolean rwylength If true, give info on runway length.
-- @field #boolean elevation If true, give info on airfield elevation.
-- @field #table runwaymag Table of magnetic runway headings.
-- @field #number runwaym2t Optional correction for magnetic to true runway heading conversion (and vice versa) in degrees.
-- @field #boolean windtrue Report true (from) heading of wind. Default is magnetic.
-- @field #boolean altimeterQNH Report altimeter QNH.
-- @field #boolean usemarker Use mark on the F10 map.
-- @field #number markerid Numerical ID of the F10 map mark point.
-- @field #number relHumidity Relative humidity (used to approximately calculate the dew point).
-- @field #boolean useSRS If true, use SRS for transmission.
-- @field Sound.SRS#MSRS msrs Moose SRS object.
-- @field #number dTQueueCheck Time interval to check the radio queue. Default 5 sec or 90 sec if SRS is used.
-- @field #boolean ReportmBar Report mBar/hpa even if not metric, i.e. for Mirage flights
-- @field #boolean TransmitOnlyWithPlayers For SRS - If true, only transmit if there are alive Players.
-- @field #string SRSText Text of the complete SRS message (if done at least once, else nil)
-- @field #boolean ATISforFARPs Will be set to true if the base given is a FARP/Helipad
-- @field Core.TextAndSound#TEXTANDSOUND gettext Gettext for localization
-- @field #string locale Current locale
-- @extends Core.Fsm#FSM

--- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde
--
-- ===
--
-- ![Banner Image](..\Presentations\ATIS\ATIS_Main.png)
--
-- # The ATIS Concept
--
-- Automatic terminal information service, or ATIS, is a continuous broadcast of recorded aeronautical information in busier terminal areas, *i.e.* airports and their immediate surroundings.
-- ATIS broadcasts contain essential information, such as current weather information, active runways, and any other information required by the pilots.
--
-- # DCS Limitations
--
-- Unfortunately, the DCS API only allow to get the temperature, pressure as well as wind direction and speed. Therefore, some other information such as cloud coverage, base and ceiling are not available
-- when dynamic weather is used.
--
-- # Scripting
--
-- The lua script to create an ATIS at an airport is pretty easy:
--
--     -- ATIS at Batumi Airport on 143.00 MHz AM.
--     atisBatumi=ATIS:New("Batumi", 143.00)
--     atisBatumi:Start()
--
-- The @{#ATIS.New}(*airbasename*, *frequency*) creates a new ATIS object. The parameter *airbasename* is the name of the airbase or airport. Note that this has to be spelled exactly as in the DCS mission editor.
-- The parameter *frequency* is the frequency the ATIS broadcasts in MHz.
--
-- Broadcasting is started via the @{#ATIS.Start}() function. The start can be delayed by using @{#ATIS.__Start}(*delay*), where *delay* is the delay in seconds.
--
-- ## Subtitles
--
-- **Note** Subtitles are not displayed when using SRS. The DCS mechanic to show subtitles (top left screen), is via the function call that plays a sound file from a UNIT, hence this doesn't work here.
--
-- Currently, DCS allows for displaying subtitles of radio transmissions only from airborne units, *i.e.* airplanes and helicopters. Therefore, if you want to have subtitles, it is necessary to place an
-- additional aircraft on the ATIS airport and set it to uncontrolled. This unit can then function as a radio relay to transmit messages with subtitles. These subtitles will only be displayed, if the
-- player has tuned in the correct ATIS frequency.
--
-- Radio transmissions via an airborne unit can be set via the @{#ATIS.SetRadioRelayUnitName}(*unitname*) function, where the parameter *unitname* is the name of the unit passed as string, *e.g.*
--
--     atisBatumi:SetRadioRelayUnitName("Radio Relay Batumi")
--
-- With a unit set in the mission editor with name "Radio Relay Batumi".
--
-- **Note** that you should use a different relay unit for each ATIS!
--
-- By default, subtitles are displayed for 10 seconds. This can be changed using @{#ATIS.SetSubtitleDuration}(*duration*) with *duration* being the duration in seconds.
-- Setting a *duration* of 0 will completely disable all subtitles.
--
-- ## Active Runway
--
-- By default, the currently active runway is determined automatically by analyzing the wind direction. Therefore, you should obviously set the wind speed to be greater zero in your mission.
--
-- Note however, there are a few special cases, where automatic detection does not yield the correct or desired result.
-- For example, there are airports with more than one runway facing in the same direction (usually denoted left and right). In this case, there is obviously no *unique* result depending on the wind vector.
--
-- If the automatic runway detection fails, the active runway can be specified manually in the script via the @{#ATIS.SetActiveRunway}(*runway*) function.
-- The parameter *runway* is a string which can be used to specify the runway heading and, if applicable, whether the left or right runway is in use.
--
-- For example, setting runway 21L would be
--
--     atisNellis:SetActiveRunway("21L")
--
-- The script will examine the string and search for the characters "L" (left) and "R" (right).
--
-- If only left or right should be set and the direction determined by the wind vector, the runway heading can be left out, *e.g.*
--
--     atisAbuDhabi:SetActiveRunway("L")
--
-- The first two digits of the runway are determined by converting the *true* runway heading into its magnetic heading. The magnetic declination (or variation) is assumed to be constant on the given map.
-- An explicit correction factor can be set via @{#ATIS.SetRunwayCorrectionMagnetic2True}.
--
-- ## Tower Frequencies
--
-- The tower frequency (or frequencies) can also be included in the ATIS information. However, there is no way to get these automatically. Therefore, it is necessary to manually specify them in the script via the
-- @{#ATIS.SetTowerFrequencies}(*frequencies*) function. The parameter *frequencies* can be a plain number if only one frequency is necessary or it can be a table of frequencies.
--
-- ## Nav Aids
--
-- Frequencies or channels of navigation aids can be specified by the user and are then provided as additional information. Unfortunately, it is **not possible** to acquire this information via the DCS API
-- we have access to.
--
-- As they say, all road lead to Rome but (for me) the easiest way to obtain the available nav aids data of an airport, is to start a mission and click on an airport symbol.
--
-- For example, the *AIRDROME DATA* for **Batumi** reads:
--
--    * **TACAN** *16X* - set via @{#ATIS.SetTACAN}
--    * **VOR** *N/A* - set via @{#ATIS.SetVOR}
--    * **RSBN** *N/A* - set via @{#ATIS.SetRSBN}
--    * **ATC** *260.000*, *131.000*, *40.400*, *4.250* - set via @{#ATIS.SetTowerFrequencies}
--    * **Runways** *31* and *13* - automatic but can be set manually via @{#ATIS.SetRunwayHeadingsMagnetic}
--    * **ILS** *110.30* for runway *13* - set via @{#ATIS.AddILS}
--    * **PRMG** *N/A* - set via @{#ATIS.AddPRMG}
--    * **OUTER NDB** *N/A* - set via @{#ATIS.AddNDBouter}
--    * **INNER NDB** *N/A* - set via @{#ATIS.AddNDBinner}
--
-- ![Banner Image](..\Presentations\ATIS\NavAid_Batumi.png)
--
-- And the *AIRDROME DATA* for **Kobuleti** reads:
--
--    * **TACAN** *67X* - set via @{#ATIS.SetTACAN}
--    * **VOR** *N/A* - set via @{#ATIS.SetVOR}
--    * **RSBN** *N/A* - set via @{#ATIS.SetRSBN}
--    * **ATC** *262.000*, *133.000*, *40.800*, *4.350* - set via @{#ATIS.SetTowerFrequencies}
--    * **Runways** *25* and *07* - automatic but can be set manually via @{#ATIS.SetRunwayHeadingsMagnetic}
--    * **ILS** *111.50* for runway *07* - set via @{#ATIS.AddILS}
--    * **PRMG** *N/A* - set via @{#ATIS.AddPRMG}
--    * **OUTER NDB** *870.00* - set via @{#ATIS.AddNDBouter}
--    * **INNER NDB** *490.00* - set via @{#ATIS.AddNDBinner}
--
-- ![Banner Image](..\Presentations\ATIS\NavAid_Kobuleti.png)
--
-- ### TACAN
--
-- The TACtical Air Navigation system [(TACAN)](https://en.wikipedia.org/wiki/Tactical_air_navigation_system) channel can be set via the @{#ATIS.SetTACAN}(*channel*) function, where *channel* is the TACAN channel. Band is always assumed to be X-ray.
--
-- ### VOR
--
-- The Very high frequency Omni-directional Range [(VOR)](https://en.wikipedia.org/wiki/VHF_omnidirectional_range) frequency can be set via the @{#ATIS.SetVOR}(*frequency*) function, where *frequency* is the VOR frequency.
--
-- ### ILS
--
-- The Instrument Landing System [(ILS)](https://en.wikipedia.org/wiki/Instrument_landing_system) frequency can be set via the @{#ATIS.AddILS}(*frequency*, *runway*) function, where *frequency* is the ILS frequency and *runway* the two letter string of the corresponding runway, *e.g.* "31".
-- If the parameter *runway* is omitted (nil) then the frequency is supposed to be valid for all runways of the airport.
--
-- ### NDB
--
-- Inner and outer Non-Directional (radio) Beacons [NDBs](https://en.wikipedia.org/wiki/Non-directional_beacon) can be set via the @{#ATIS.AddNDBinner}(*frequency*, *runway*) and @{#ATIS.AddNDBouter}(*frequency*, *runway*) functions, respectively.
--
-- In both cases, the parameter *frequency* is the NDB frequency and *runway* the two letter string of the corresponding runway, *e.g.* "31".
-- If the parameter *runway* is omitted (nil) then the frequency is supposed to be valid for all runways of the airport.
--
-- ## RSBN
--
-- The RSBN channel can be set via the @{#ATIS.SetRSBN}(*channel*) function.
--
-- ## PRMG
--
-- The PRMG channel can be set via the @{#ATIS.AddPRMG}(*channel*, *runway*) function for each *runway*.
--
-- ## Unit System
--
-- By default, information is given in imperial units, *i.e.* wind speed in knots, pressure in inches of mercury, visibility in Nautical miles, etc.
--
-- If you prefer metric units, you can enable this via the @{#ATIS.SetMetricUnits}() function,
--
--     atisBatumi:SetMetricUnits()
--
-- With this, wind speed is given in meters per second, pressure in hectopascal (hPa, which is the same as millibar - mbar), visibility in kilometers etc.
--
-- # Sound Files
--
-- More than 180 individual sound files have been created using a text-to-speech program. All ATIS information is given with en-US accent.
-- You can find the sound files [here](https://github.com/FlightControl-Master/MOOSE_SOUND/releases). Also check out the pinned messages in the Moose discord #ops-atis channel.
--
-- To include the files, open the mission (.miz) file with, *e.g.*, 7-zip. Then just drag-n-drop the file into the miz.
--
-- ![Banner Image](..\Presentations\ATIS\ATIS_SoundFolder.png)
--
-- **Note** that the default folder name is *ATIS Soundfiles/*. If you want to change it, you can use the @{#ATIS.SetSoundfilesPath}(*path*), where *path* is the path of the directory. This must end with a slash "/"!
--
-- # Marks on the F10 Map
--
-- You can place marks on the F10 map via the @{#ATIS.SetMapMarks}() function. These will contain info about the ATIS frequency, the currently active runway and some basic info about the weather (wind, pressure and temperature).
--
-- # Text-To-Speech
--
-- You can enable text-to-speech ATIS information with the @{#ATIS.SetSRS}() function. This uses [SRS](http://dcssimpleradio.com/) (Version >= 1.9.6.0) for broadcasting.
-- Advantages are that **no sound files** or radio relay units are necessary. Also the issue that FC3 aircraft hear all transmissions will be circumvented.
--
-- The @{#ATIS.SetSRS}() requires you to specify the path to the SRS install directory or more specifically the path to the DCS-SR-ExternalAudio.exe file.
--
-- Unfortunately, it is not possible to determine the duration of the complete transmission. So once the transmission is finished, there might be some radio silence before
-- the next iteration begins. You can fine tune the time interval between transmissions with the @{#ATIS.SetQueueUpdateTime}() function. The default interval is 90 seconds.
--
-- An SRS Setup-Guide can be found here: [Moose TTS Setup Guide](https://github.com/FlightControl-Master/MOOSE_GUIDES/blob/master/documents/Moose%20TTS%20Setup%20Guide.pdf)
--
-- # Examples
--
-- ## Caucasus: Batumi
--
--     -- ATIS Batumi Airport on 143.00 MHz AM.
--     atisBatumi=ATIS:New(AIRBASE.Caucasus.Batumi, 143.00)
--     atisBatumi:SetRadioRelayUnitName("Radio Relay Batumi")
--     atisBatumi:Start()
--
-- ## Nevada: Nellis AFB
--
--     -- ATIS Nellis AFB on 270.10 MHz AM.
--     atisNellis=ATIS:New(AIRBASE.Nevada.Nellis, 270.1)
--     atisNellis:SetRadioRelayUnitName("Radio Relay Nellis")
--     atisNellis:SetActiveRunway("21L")
--     atisNellis:SetTowerFrequencies({327.000, 132.550})
--     atisNellis:SetTACAN(12)
--     atisNellis:AddILS(109.1, "21")
--     atisNellis:Start()
--
-- ## Persian Gulf: Abu Dhabi International Airport
--
--     -- ATIS Abu Dhabi International on 125.1 MHz AM.
--     atisAbuDhabi=ATIS:New(AIRBASE.PersianGulf.Abu_Dhabi_Intl, 125.1)
--     atisAbuDhabi:SetRadioRelayUnitName("Radio Relay Abu Dhabi International Airport")
--     atisAbuDhabi:SetMetricUnits()
--     atisAbuDhabi:SetActiveRunway("L")
--     atisAbuDhabi:SetTowerFrequencies({250.5, 119.2})
--     atisAbuDhabi:SetVOR(114.25)
--     atisAbuDhabi:Start()
--
-- ## SRS
--
--     atis=ATIS:New("Batumi", 305, radio.modulation.AM)
--     atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US")
--     atis:Start()
--
-- This uses a male voice with US accent. It requires SRS to be installed in the `D:\DCS\_SRS\` directory. Note that backslashes need to be escaped or simply use slashes (as in linux).
--
-- ### SRS can use multiple frequencies:
--
--     atis=ATIS:New("Batumi", {305,103.85}, {radio.modulation.AM,radio.modulation.FM})
--     atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US")
--     atis:Start()
--
-- ### SRS Localization
--
--  You can localize the SRS output, all you need is to provide a table of translations and set the `locale` of your instance. You need to provide the translations in your script **before you instantiate your ATIS**.
--  The German localization (already provided in the code) e.g. looks like follows:
--
--          ATIS.Messages.DE =
--            {
--              HOURS = "Uhr",
--              TIME = "Zeit",
--              NOCLOUDINFO = "Informationen über Wolken nicht verfuegbar",
--              OVERCAST = "Geschlossene Wolkendecke",
--              BROKEN = "Stark bewoelkt",
--              SCATTERED = "Bewoelkt",
--              FEWCLOUDS = "Leicht bewoelkt",
--              NOCLOUDS = "Klar",
--              AIRPORT = "Flughafen",
--              INFORMATION ="Information",
--              SUNRISEAT = "Sonnenaufgang um %s lokaler Zeit",
--              SUNSETAT = "Sonnenuntergang um %s lokaler Zeit",
--              WINDFROMMS = "Wind aus %s mit %s m/s",
--              WINDFROMKNOTS = "Wind aus %s mit %s Knoten",
--              GUSTING = "boeig",
--              VISIKM = "Sichtweite %s km",
--              VISISM = "Sichtweite %s Meilen",
--              RAIN = "Regen",
--              TSTORM = "Gewitter",
--              SNOW = "Schnee",
--              SSTROM = "Schneesturm",
--              FOG = "Nebel",
--              DUST = "Staub",
--              PHENOMENA = "Wetter Phaenomene",
--              CLOUDBASEM = "Wolkendecke von %s bis %s Meter",
--              CLOUDBASEFT = "Wolkendecke von %s bis %s Fuß",
--              TEMPERATURE = "Temperatur",
--              DEWPOINT = "Taupunkt",
--              ALTIMETER = "Hoehenmesser",
--              ACTIVERUN = "Aktive Startbahn",
--              ACTIVELANDING = "Aktive Landebahn",
--              LEFT = "Links",
--              RIGHT = "Rechts",
--              RWYLENGTH = "Startbahn",
--              METERS = "Meter",
--              FEET = "Fuß",
--              ELEVATION = "Hoehe",
--              TOWERFREQ = "Kontrollturm Frequenz",
--              ILSFREQ = "ILS Frequenz",
--              OUTERNDB = "Aeussere NDB Frequenz",
--              INNERNDB = "Innere NDB Frequenz",
--              VORFREQ = "VOR Frequenz",
--              VORFREQTTS = "V O R Frequenz",
--              TACANCH = "TACAN Kanal %d Xaver",
--              RSBNCH = "RSBN Kanal",
--              PRMGCH = "PRMG Kanal",
--              ADVISE = "Hinweis bei Erstkontakt, Sie haben Informationen",
--              STATUTE = "englische Meilen",
--              DEGREES = "Grad Celsius",
--              FAHRENHEIT = "Grad Fahrenheit",
--              INCHHG = "Inches H G",
--              MMHG = "Millimeter H G",
--              HECTO = "Hektopascal",
--              METERSPER = "Meter pro Sekunde",
--              TACAN = "Tackan",
--              FARP = "Farp",
--              DELIMITER = "Komma", -- decimal delimiter
--            }
--
-- Then set up your ATIS and set the locale:
--
--          atis=ATIS:New("Batumi", 305, radio.modulation.AM)
--          atis:SetSRS("D:\\DCS\\_SRS\\", "female", "de_DE")
--          atis:SetLocale("de") -- available locales from source are "en", "de" and "es"
--          atis:Start()
--
-- ## FARPS
--
-- ATIS is working with FARPS, but this requires the usage of SRS. The airbase name for the `New()-method` is the UNIT name of the FARP:
--
--      atis = ATIS:New("FARP Gold",119,radio.modulation.AM)
--      atis:SetMetricUnits()
--      atis:SetTransmitOnlyWithPlayers(true)
--      atis:SetReportmBar(true)
--      atis:SetTowerFrequencies(127.50)
--      atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US",nil,5002)
--      atis:SetAdditionalInformation("Welcome to the Jungle!")
--      atis:__Start(3)
--
-- @field #ATIS
ATIS = {
  ClassName      = "ATIS",
  lid            =   nil,
  theatre        =   nil,
  airbasename    =   nil,
  airbase        =   nil,
  frequency      =   nil,
  modulation     =   nil,
  power          =   nil,
  radioqueue     =   nil,
  soundpath      =   nil,
  relayunitname  =   nil,
  towerfrequency =   nil,
  activerunway   =   nil,
  subduration    =   nil,
  metric         =   nil,
  PmmHg          =   nil,
  qnhonly        =   false,
  TDegF          =   nil,
  zuludiff       =   nil,
  zulutimeonly   =   false,
  magvar         =   nil,
  ils            =    {},
  ndbinner       =    {},
  ndbouter       =    {},
  vor            =   nil,
  tacan          =   nil,
  rsbn           =   nil,
  prmg           =    {},
  rwylength      =   nil,
  elevation      =   nil,
  runwaymag      =    {},
  runwaym2t      =   nil,
  windtrue       =   nil,
  altimeterQNH   =   nil,
  usemarker      =   nil,
  markerid       =   nil,
  relHumidity    =   nil,
  ReportmBar     =   false,
  TransmitOnlyWithPlayers = false,
  ATISforFARPs   =   false,
  locale         =   "en",
}

--- NATO alphabet.
-- @type ATIS.Alphabet
ATIS.Alphabet = {
  [1]  = "Alfa",
  [2]  = "Bravo",
  [3]  = "Charlie",
  [4]  = "Delta",
  [5]  = "Echo",
  [6]  = "Delta",
  [7]  = "Echo",
  [8]  = "Foxtrot",
  [9]  = "Golf",
  [10] = "Hotel",
  [11] = "India",
  [12] = "Juliett",
  [13] = "Kilo",
  [14] = "Lima",
  [15] = "Mike",
  [16] = "November",
  [17] = "Oscar",
  [18] = "Papa",
  [19] = "Quebec",
  [20] = "Romeo",
  [21] = "Sierra",
  [22] = "Tango",
  [23] = "Uniform",
  [24] = "Victor",
  [25] = "Whiskey",
  [26] = "Xray",
  [27] = "Yankee",
  [28] = "Zulu",
}

--- Runway correction for converting true to magnetic heading.
-- @type ATIS.RunwayM2T
-- @field #number Caucasus 0° (East).
-- @field #number Nevada +12° (East).
-- @field #number Normandy -10° (West).
-- @field #number PersianGulf +2° (East).
-- @field #number TheChannel -10° (West).
-- @field #number Syria +5° (East).
-- @field #number MarianaIslands +2° (East).
-- @field #number SinaiMap +5° (East).
-- @field #number Kola +15° (East).
-- @field #number Afghanistan +3° (East).
-- @field #number Iraq +4.4° (East).
-- @field #number GermanyCW +0.1° (East).
ATIS.RunwayM2T = {
  Caucasus = 0,
  Nevada = 12,
  Normandy = -10,
  PersianGulf = 2,
  TheChannel = -10,
  Syria = 5,
  MarianaIslands = 2,
  Falklands = 12,
  SinaiMap = 5,
  Kola = 15,
  Afghanistan = 3,
  Iraq=4.4,
  GermanyCW=0.1,
}

--- Whether ICAO phraseology is used for ATIS broadcasts.
-- @type ATIS.ICAOPhraseology
-- @field #boolean Caucasus true.
-- @field #boolean Nevada false.
-- @field #boolean Normandy true.
-- @field #boolean PersianGulf true.
-- @field #boolean TheChannel true.
-- @field #boolean Syria true.
-- @field #boolean MarianaIslands true.
-- @field #boolean Falklands true.
-- @field #boolean SinaiMap true.
-- @field #boolean Kola true.
-- @field #boolean Afghanistan true.
-- @field #boolean Iraq true.
-- @field #boolean GermanyCW true.
ATIS.ICAOPhraseology = {
  Caucasus = true,
  Nevada = false,
  Normandy = true,
  PersianGulf = true,
  TheChannel = true,
  Syria = true,
  MarianaIslands = true,
  Falklands = true,
  SinaiMap = true,
  Kola = true,
  Afghanistan = true,
  Iraq = true,
  GermanyCW = true,
}

--- Nav point data.
-- @type ATIS.NavPoint
-- @field #number frequency Nav point frequency.
-- @field #string runway Runway, *e.g.* "21".
-- @field #boolean leftright If true, runway has left "L" and right "R" runways.

--- Sound file data.
-- @type ATIS.Soundfile
-- @field #string filename Name of the file
-- @field #number duration Duration in seconds.

--- Sound files.
-- @type ATIS.Sound
-- @field #ATIS.Soundfile ActiveRunway
-- @field #ATIS.Soundfile AdviceOnInitial
-- @field #ATIS.Soundfile Airport
-- @field #ATIS.Soundfile Altimeter
-- @field #ATIS.Soundfile At
-- @field #ATIS.Soundfile CloudBase
-- @field #ATIS.Soundfile CloudCeiling
-- @field #ATIS.Soundfile CloudsBroken
-- @field #ATIS.Soundfile CloudsFew
-- @field #ATIS.Soundfile CloudsNo
-- @field #ATIS.Soundfile CloudsNotAvailable
-- @field #ATIS.Soundfile CloudsOvercast
-- @field #ATIS.Soundfile CloudsScattered
-- @field #ATIS.Soundfile Decimal
-- @field #ATIS.Soundfile DegreesCelsius
-- @field #ATIS.Soundfile DegreesFahrenheit
-- @field #ATIS.Soundfile DewPoint
-- @field #ATIS.Soundfile Dust
-- @field #ATIS.Soundfile Elevation
-- @field #ATIS.Soundfile EndOfInformation
-- @field #ATIS.Soundfile Feet
-- @field #ATIS.Soundfile Fog
-- @field #ATIS.Soundfile Gusting
-- @field #ATIS.Soundfile HectoPascal
-- @field #ATIS.Soundfile Hundred
-- @field #ATIS.Soundfile InchesOfMercury
-- @field #ATIS.Soundfile Information
-- @field #ATIS.Soundfile Kilometers
-- @field #ATIS.Soundfile Knots
-- @field #ATIS.Soundfile Left
-- @field #ATIS.Soundfile MegaHertz
-- @field #ATIS.Soundfile Meters
-- @field #ATIS.Soundfile MetersPerSecond
-- @field #ATIS.Soundfile Miles
-- @field #ATIS.Soundfile MillimetersOfMercury
-- @field #ATIS.Soundfile N0
-- @field #ATIS.Soundfile N1
-- @field #ATIS.Soundfile N2
-- @field #ATIS.Soundfile N3
-- @field #ATIS.Soundfile N4
-- @field #ATIS.Soundfile N5
-- @field #ATIS.Soundfile N6
-- @field #ATIS.Soundfile N7
-- @field #ATIS.Soundfile N8
-- @field #ATIS.Soundfile N9
-- @field #ATIS.Soundfile NauticalMiles
-- @field #ATIS.Soundfile None
-- @field #ATIS.Soundfile QFE
-- @field #ATIS.Soundfile QNH
-- @field #ATIS.Soundfile Rain
-- @field #ATIS.Soundfile Right
-- @field #ATIS.Soundfile Snow
-- @field #ATIS.Soundfile SnowStorm
-- @field #ATIS.Soundfile SunriseAt
-- @field #ATIS.Soundfile SunsetAt
-- @field #ATIS.Soundfile Temperature
-- @field #ATIS.Soundfile Thousand
-- @field #ATIS.Soundfile ThunderStorm
-- @field #ATIS.Soundfile TimeLocal
-- @field #ATIS.Soundfile TimeZulu
-- @field #ATIS.Soundfile TowerFrequency
-- @field #ATIS.Soundfile Visibilty
-- @field #ATIS.Soundfile WeatherPhenomena
-- @field #ATIS.Soundfile WindFrom
-- @field #ATIS.Soundfile ILSFrequency
-- @field #ATIS.Soundfile InnerNDBFrequency
-- @field #ATIS.Soundfile OuterNDBFrequency
-- @field #ATIS.Soundfile PRMGChannel
-- @field #ATIS.Soundfile RSBNChannel
-- @field #ATIS.Soundfile RunwayLength
-- @field #ATIS.Soundfile TACANChannel
-- @field #ATIS.Soundfile VORFrequency
ATIS.Sound = {
  ActiveRunway = { filename = "ActiveRunway.ogg", duration = 0.85 },
  ActiveRunwayDeparture = { filename = "ActiveRunwayDeparture.ogg", duration = 1.50 },
  ActiveRunwayArrival = { filename = "ActiveRunwayArrival.ogg", duration = 1.38 },
  AdviceOnInitial = { filename = "AdviceOnInitial.ogg", duration = 2.98 },
  Airport = { filename = "Airport.ogg", duration = 0.55 },
  Altimeter = { filename = "Altimeter.ogg", duration = 0.91 },
  At = { filename = "At.ogg", duration = 0.32 },
  CloudBase = { filename = "CloudBase.ogg", duration = 0.69 },
  CloudCeiling = { filename = "CloudCeiling.ogg", duration = 0.53 },
  CloudsBroken = { filename = "CloudsBroken.ogg", duration = 0.81 },
  CloudsFew = { filename = "CloudsFew.ogg", duration = 0.74 },
  CloudsNo = { filename = "CloudsNo.ogg", duration =  0.69},
  CloudsNotAvailable = { filename = "CloudsNotAvailable.ogg", duration = 2.64 },
  CloudsOvercast = { filename = "CloudsOvercast.ogg", duration = 0.82 },
  CloudsScattered = { filename = "CloudsScattered.ogg", duration = 0.89 },
  Decimal = { filename = "Decimal.ogg", duration = 0.71 },
  DegreesCelsius = { filename = "DegreesCelsius.ogg", duration = 1.08 },
  DegreesFahrenheit = { filename = "DegreesFahrenheit.ogg", duration = 1.07 },
  DewPoint = { filename = "DewPoint.ogg", duration = 0.59 },
  Dust = { filename = "Dust.ogg", duration = 0.37 },
  Elevation = { filename = "Elevation.ogg", duration = 0.92 },
  EndOfInformation = { filename = "EndOfInformation.ogg", duration = 1.24 },
  Feet = { filename = "Feet.ogg", duration = 0.34 },
  Fog = { filename = "Fog.ogg", duration = 0.41 },
  Gusting = { filename = "Gusting.ogg", duration = 0.58 },
  HectoPascal = { filename = "HectoPascal.ogg", duration = 0.92 },
  Hundred = { filename = "Hundred.ogg", duration = 0.53 },
  ILSFrequency = { filename = "ILSFrequency.ogg", duration = 1.30 },
  InchesOfMercury = { filename = "InchesOfMercury.ogg", duration = 1.26 },
  Information = { filename = "Information.ogg", duration = 0.99 },  
  InnerNDBFrequency = { filename = "InnerNDBFrequency.ogg", duration = 1.69 },
  Kilometers = { filename = "Kilometers.ogg", duration = 0.93 },
  Knots = { filename = "Knots.ogg", duration = 0.46 },
  Left = { filename = "Left.ogg", duration = 0.41 },
  MegaHertz = { filename = "MegaHertz.ogg", duration = 0.83 },
  Meters = { filename = "Meters.ogg", duration = 0.55 },
  MetersPerSecond = { filename = "MetersPerSecond.ogg", duration = 1.03 },
  Miles = { filename = "Miles.ogg", duration = 0.44 },
  MillimetersOfMercury = { filename = "MillimetersOfMercury.ogg", duration = 1.59 },
  Minus = { filename = "Minus.ogg", duration = 0.55 },
  N0 = { filename = "N-0.ogg", duration = 0.52 },
  N1 = { filename = "N-1.ogg", duration = 0.35 },
  N2 = { filename = "N-2.ogg", duration = 0.41 },
  N3 = { filename = "N-3.ogg", duration = 0.34 },
  N4 = { filename = "N-4.ogg", duration = 0.37 },
  N5 = { filename = "N-5.ogg", duration = 0.40 },
  N6 = { filename = "N-6.ogg", duration = 0.46 },
  N7 = { filename = "N-7.ogg", duration = 0.52 },
  N8 = { filename = "N-8.ogg", duration = 0.36 },
  N9 = { filename = "N-9.ogg", duration = 0.51 },
  NauticalMiles = { filename = "NauticalMiles.ogg", duration = 0.93 },
  None = { filename = "None.ogg", duration = 0.33 },
  OuterNDBFrequency = { filename = "OuterNDBFrequency.ogg", duration = 1.70 },
  PRMGChannel = { filename = "PRMGChannel.ogg", duration = 1.27 },  
  QFE = { filename = "QFE.ogg", duration = 0.90 },
  QNH = { filename = "QNH.ogg", duration = 0.94 },
  Rain = { filename = "Rain.ogg", duration = 0.35 },
  Right = { filename = "Right.ogg", duration = 0.31 },
  RSBNChannel = { filename = "RSBNChannel.ogg", duration = 1.26 },
  RunwayLength = { filename = "RunwayLength.ogg", duration = 0.81 },    
  Snow = { filename = "Snow.ogg", duration = 0.40 },
  SnowStorm = { filename = "SnowStorm.ogg", duration = 0.73 },
  StatuteMiles = { filename = "StatuteMiles.ogg", duration = 0.90 },
  SunriseAt = { filename = "SunriseAt.ogg", duration = 0.82 },
  SunsetAt = { filename = "SunsetAt.ogg", duration = 0.87 },
  TACANChannel = { filename = "TACANChannel.ogg", duration = 0.81 },  
  Temperature = { filename = "Temperature.ogg", duration = 0.70 },
  Thousand = { filename = "Thousand.ogg", duration = 0.58 },
  ThunderStorm = { filename = "ThunderStorm.ogg", duration = 0.79 },
  TimeLocal = { filename = "TimeLocal.ogg", duration = 0.83 },
  TimeZulu = { filename = "TimeZulu.ogg", duration = 0.83 },
  TowerFrequency = { filename = "TowerFrequency.ogg", duration = 1.05 },
  Visibilty = { filename = "Visibility.ogg", duration = 1.16 },
  VORFrequency = { filename = "VORFrequency.ogg", duration = 1.28 },  
  WeatherPhenomena = { filename = "WeatherPhenomena.ogg", duration = 1.09 },
  WindFrom = { filename = "WindFrom.ogg", duration = 0.63 },
  Zulu = { filename = "Zulu.ogg", duration = 0.51 },
}

---
-- @field Messages
ATIS.Messages = {
  EN =
  {
    HOURS = "hours",
    TIME = "Hours",
    NOCLOUDINFO = "Cloud coverage information not available",
    OVERCAST = "Overcast",
    BROKEN = "Broken clouds",
    SCATTERED = "Scattered clouds",
    FEWCLOUDS = "Few clouds",
    NOCLOUDS = "No clouds",
    AIRPORT = "Airport",
    INFORMATION ="Information",
    SUNRISEAT = "Sunrise at %s local time",
    SUNSETAT = "Sunset at %s local time",
    WINDFROMMS = "Wind from %s at %s m/s",
    WINDFROMKNOTS = "Wind from %s at %s knots",
    GUSTING = "gusting",
    VISIKM = "Visibility %s km",
    VISISM = "Visibility %s SM",
    RAIN = "rain",
    TSTORM = "thunderstorm",
    SNOW = "snow",
    SSTROM = "snowstorm",
    FOG = "fog",
    DUST = "dust",
    PHENOMENA = "Weather phenomena",
    CLOUDBASEM = "Cloud base %s, ceiling %s meters",
    CLOUDBASEFT = "Cloud base %s, ceiling %s feet",
    TEMPERATURE = "Temperature",
    DEWPOINT = "Dew point",
    ALTIMETER = "Altimeter",
    ACTIVERUN = "Active runway departure",
    ACTIVELANDING = "Active runway arrival",
    LEFT = "Left",
    RIGHT = "Right",
    RWYLENGTH = "Runway length",
    METERS = "meters",
    FEET = "feet",
    ELEVATION = "Elevation",
    TOWERFREQ = "Tower frequency",
    ILSFREQ = "ILS frequency",
    OUTERNDB = "Outer NDB frequency",
    INNERNDB = "Inner NDB frequency",
    VORFREQ = "VOR frequency",
    VORFREQTTS = "V O R frequency",
    TACANCH = "TACAN channel %dX Ray",
    RSBNCH = "RSBN channel",
    PRMGCH = "PRMG channel",
    ADVISE = "Advise on initial contact, you have information",
    STATUTE = "statute miles",
    DEGREES = "degrees Celsius",
    FAHRENHEIT = "degrees Fahrenheit",
    INCHHG = "inches of Mercury",
    MMHG = "millimeters of Mercury",
    HECTO = "hectopascals",
    METERSPER = "meters per second",
    TACAN = "tackan",
    FARP = "farp",
    DELIMITER = "point", -- decimal delimiter
  },
  DE =
  {
    HOURS = "Uhr",
    TIME = "Zeit",
    NOCLOUDINFO = "Informationen über Wolken nicht verfuegbar",
    OVERCAST = "Geschlossene Wolkendecke",
    BROKEN = "Stark bewoelkt",
    SCATTERED = "Bewoelkt",
    FEWCLOUDS = "Leicht bewoelkt",
    NOCLOUDS = "Klar",
    AIRPORT = "Flughafen",
    INFORMATION ="Information",
    SUNRISEAT = "Sonnenaufgang um %s lokaler Zeit",
    SUNSETAT = "Sonnenuntergang um %s lokaler Zeit",
    WINDFROMMS = "Wind aus %s mit %s m/s",
    WINDFROMKNOTS = "Wind aus %s mit %s Knoten",
    GUSTING = "boeig",
    VISIKM = "Sichtweite %s km",
    VISISM = "Sichtweite %s Meilen",
    RAIN = "Regen",
    TSTORM = "Gewitter",
    SNOW = "Schnee",
    SSTROM = "Schneesturm",
    FOG = "Nebel",
    DUST = "Staub",
    PHENOMENA = "Wetter Phaenomene",
    CLOUDBASEM = "Wolkendecke von %s bis %s Meter",
    CLOUDBASEFT = "Wolkendecke von %s bis %s Fuß",
    TEMPERATURE = "Temperatur",
    DEWPOINT = "Taupunkt",
    ALTIMETER = "Hoehenmesser",
    ACTIVERUN = "Aktive Startbahn",
    ACTIVELANDING = "Aktive Landebahn",
    LEFT = "Links",
    RIGHT = "Rechts",
    RWYLENGTH = "Startbahn",
    METERS = "Meter",
    FEET = "Fuß",
    ELEVATION = "Hoehe",
    TOWERFREQ = "Kontrollturm Frequenz",
    ILSFREQ = "ILS Frequenz",
    OUTERNDB = "Aeussere NDB Frequenz",
    INNERNDB = "Innere NDB Frequenz",
    VORFREQ = "VOR Frequenz",
    VORFREQTTS = "V O R Frequenz",
    TACANCH = "TACAN Kanal %d Xaver",
    RSBNCH = "RSBN Kanal",
    PRMGCH = "PRMG Kanal",
    ADVISE = "Hinweis bei Erstkontakt, Sie haben Informationen",
    STATUTE = "englische Meilen",
    DEGREES = "Grad Celsius",
    FAHRENHEIT = "Grad Fahrenheit",
    INCHHG = "Inches H G",
    MMHG = "Millimeter H G",
    HECTO = "Hektopascal",
    METERSPER = "Meter pro Sekunde",
    TACAN = "Tackan",
    FARP = "Farp",
    DELIMITER = "Komma", -- decimal delimiter
  },
  -- Set ES Locale translations for ATIS thanks to @Ritu
  ES =
  {
    HOURS = "horas",
    TIME = "horas",
    NOCLOUDINFO = "Información sobre capa de nubes no disponible",
    OVERCAST = "Nublado",
    BROKEN = "Nubes rotas",
    SCATTERED = "Nubes dispersas",
    FEWCLOUDS = "Ligeramente nublado",
    NOCLOUDS = "Despejado",
    AIRPORT = "Aeropuerto",
    INFORMATION ="Informacion",
    SUNRISEAT = "Amanecer a las %s hora local",
    SUNSETAT = "Puesta de sol a las %s hora local",
    WINDFROMMS = "Viento procedente de %s con %s m/s",
    WINDFROMKNOTS = "Viento de %s con %s nudos",
    GUSTING = "ráfagas",
    VISIKM = "Visibilidad %s km",
    VISISM = "Visibilidad %s millas",
    RAIN = "Lluvia",
    TSTORM = "Tormenta",
    SNOW = "Nieve",
    SSTROM = "Tormenta de nieve",
    FOG = "Niebla",
    DUST = "Polvo",
    PHENOMENA = "Fenómenos meteorológicos",
    CLOUDBASEM = "Capa de nubes de %s a %s metros",
    CLOUDBASEFT = "Capa de nubes de %s a %s pies",
    TEMPERATURE = "Temperatura",
    DEWPOINT = "Punto de rocio",
    ALTIMETER = "Altímetro",
    ACTIVERUN = "Pista activa",
    ACTIVELANDING = "Pista de aterrizaje activa",
    LEFT = "Izquierda",
    RIGHT = "Derecha",
    RWYLENGTH = "Longitud de pista",
    METERS = "Metro",
    FEET = "Pie",
    ELEVATION = "Elevación",
    TOWERFREQ = "Frecuencias de la torre de control",
    ILSFREQ = "Fecuencia ILS",
    OUTERNDB = "Frecuencia NDB externa",
    INNERNDB = "Frecuencia NDB interior",
    VORFREQ = "Frecuencia VOR",
    VORFREQTTS = "Frecuencia V O R",
    TACANCH = "Canal TACAN %d Xaver",
    RSBNCH = "Canal RSBN",
    PRMGCH = "Canal PRMG",
    ADVISE = "Avise en el contacto inicial a torre de que tiene la informacion",
    STATUTE = "Millas inglesas",
    DEGREES = "Grados Celsius",
    FAHRENHEIT = "Grados Fahrenheit",
    INCHHG = "Pulgadas de mercurio",
    MMHG = "Milímeteros de Mercurio",
    HECTO = "Hectopascales",
    METERSPER = "Metros por segundo",
    TACAN = "Tacan",
    FARP = "Farp",
    DELIMITER = "Punto", -- decimal delimiter
  },
  -- French messages thanks to @Wojtech and Bing
  FR = {
    HOURS = "Heures",
    TIME = "Temps",
    NOCLOUDINFO = "Informations sur la couverture nuageuse non disponibles",
    OVERCAST = "Ciel couvert",
    BROKEN = "Nuages fragmentés",
    SCATTERED = "Nuages épars",
    FEWCLOUDS = "Nuages rares",
    NOCLOUDS = "Clair",
    AIRPORT = "Aéroport",
    INFORMATION ="Information",
    SUNRISEAT = "Levé du soleil à %s heure locale",
    SUNSETAT = "Couché du soleil à %s heure locale",
    WINDFROMMS = "Vent du %s pour %s mètres par seconde",
    WINDFROMKNOTS = "Vent du %s pour %s noeuds",
    GUSTING = "Rafale de vent",
    VISIKM = "Visibilité %s kilomètres",
    VISISM = "Visibilité %s Miles",
    RAIN = "Pluie",
    TSTORM = "Orage",
    SNOW = "Neige",
    SSTROM = "Tempête de neige",
    FOG = "Brouillard",
    DUST = "Poussière",
    PHENOMENA = "Phénomène météorologique",
    CLOUDBASEM = "Couverture nuageuse de %s à %s mètres",
    CLOUDBASEFT = "Couverture nuageuse de %s à %s pieds",
    TEMPERATURE = "Température",
    DEWPOINT = "Point de rosée",
    ALTIMETER = "Altimètre",
    ACTIVERUN = "Décollages piste",
    ACTIVELANDING = "Atterrissages piste",
    LEFT = "Gauche",
    RIGHT = "Droite",
    RWYLENGTH = "Longueur de piste",
    METERS = "Mètre",
    FEET = "Pieds",
    ELEVATION = "Hauteur",
    TOWERFREQ = "Fréquences de la tour",
    ILSFREQ = "Fréquences ILS",
    OUTERNDB = "Fréquences Outer NDB",
    INNERNDB = "Fréquences Inner NDB",
    VORFREQ = "Fréquences VOR",
    VORFREQTTS = "Fréquences V O R",
    TACANCH = "Canal TACAN %d",
    RSBNCH = "Canal RSBN",
    PRMGCH = "Canal PRMG",
    ADVISE = "Informez le contrôle que vous avez copié l'information",
    STATUTE = "Statute Miles",
    DEGREES = "Degré celcius",
    FAHRENHEIT = "Degré Fahrenheit",
    INCHHG = "Pouces de mercure",
    MMHG = "Millimètres de mercure",
    HECTO = "Hectopascals",
    METERSPER = "Mètres par seconde",
    TACAN = "TAKAN",
    FARP = "FARPE",
    DELIMITER = "Décimal", -- decimal delimiter
  }
}

---
-- @field locale
ATIS.locale = "en"

--- ATIS table containing all defined ATISes.
-- @field #table _ATIS
_ATIS = {}

--- ATIS class version.
-- @field #string version
ATIS.version = "1.0.1"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Correct fog for elevation.
-- TODO: Generalize sound files input to be able to use custom made sounds.
-- DONE: Option to add multiple frequencies for SRS
-- DONE: Zulu time --> Zulu in output.
-- DONE: Fix for AB not having a runway - Helopost like Naqoura
-- DONE: Add new Normandy airfields.
-- DONE: Use new AIRBASE system to set start/landing runway
-- DONE: SetILS doesn't work
-- DONE: Visibility reported twice over SRS
-- DONE: Add text report for output.
-- DONE: Add stop FMS functions.
-- NOGO: Use local time. Not realistic!
-- DONE: Dew point. Approx. done.
-- DONE: Metric units.
-- DONE: Set UTC correction.
-- DONE: Set magnetic variation.
-- DONE: New DCS 2.7 weather presets.
-- DONE: Added TextAndSound localization
-- DONE: Added SRS spelling out both take off and landing runway

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new ATIS class object for a specific airbase.
-- @param #ATIS self
-- @param #string AirbaseName Name of the airbase.
-- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. When using **SRS** this can be passed as a table of multiple frequencies.
-- @param #number Modulation Radio modulation: 0=AM, 1=FM. Default 0=AM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. When using **SRS** this can be passed as a table of multiple modulations.
-- @return #ATIS self
function ATIS:New(AirbaseName, Frequency, Modulation)

  -- Inherit everything from FSM class.
  local self = BASE:Inherit( self, FSM:New() ) -- #ATIS

  self.airbasename=AirbaseName
  self.airbase=AIRBASE:FindByName(AirbaseName)

  if self.airbase==nil then
    self:E("ERROR: Airbase %s for ATIS could not be found!", tostring(AirbaseName))
    return nil
  end

  -- Default freq and modulation.
  self.frequency=Frequency or 143.00
  self.modulation=Modulation or 0

  -- Get map.
  self.theatre = env.mission.theatre

  -- Set some string id for output to DCS.log file.
  self.lid = string.format( "ATIS %s | ", self.airbasename )

  -- This is just to hinder the garbage collector deallocating the ATIS object.
  _ATIS[#_ATIS + 1] = self

  -- Defaults:
  self:SetSoundfilesPath()
  self:SetSubtitleDuration()
  self:SetMagneticDeclination()
  self:SetRunwayCorrectionMagnetic2True()
  self:SetRadioPower()
  self:SetAltimeterQNH( true )
  self:SetMapMarks( false )
  self:SetRelativeHumidity()
  self:SetQueueUpdateTime()
  self:SetReportmBar(false)
  self:_InitLocalization()

  -- Start State.
  self:SetStartState( "Stopped" )

  -- Add FSM transitions.
  --                 From State  -->   Event      -->     To State
  self:AddTransition("Stopped",       "Start",           "Running") -- Start FSM.
  self:AddTransition("*",             "Status",          "*")       -- Update status.
  self:AddTransition("*",             "Broadcast",       "*")       -- Broadcast ATIS message.
  self:AddTransition("*",             "CheckQueue",      "*")       -- Check if radio queue is empty.
  self:AddTransition("*",             "Report",          "*")       -- Report ATIS text.
  self:AddTransition("*",             "Stop",            "Stopped") -- Stop.

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the ATIS.
  -- @function [parent=#ATIS] Start
  -- @param #ATIS self

  --- Triggers the FSM event "Start" after a delay.
  -- @function [parent=#ATIS] __Start
  -- @param #ATIS self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the ATIS.
  -- @function [parent=#ATIS] Stop
  -- @param #ATIS self

  --- Triggers the FSM event "Stop" after a delay.
  -- @function [parent=#ATIS] __Stop
  -- @param #ATIS self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Status".
  -- @function [parent=#ATIS] Status
  -- @param #ATIS self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#ATIS] __Status
  -- @param #ATIS self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Broadcast".
  -- @function [parent=#ATIS] Broadcast
  -- @param #ATIS self

  --- Triggers the FSM event "Broadcast" after a delay.
  -- @function [parent=#ATIS] __Broadcast
  -- @param #ATIS self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "CheckQueue".
  -- @function [parent=#ATIS] CheckQueue
  -- @param #ATIS self

  --- Triggers the FSM event "CheckQueue" after a delay.
  -- @function [parent=#ATIS] __CheckQueue
  -- @param #ATIS self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Report".
  -- @function [parent=#ATIS] Report
  -- @param #ATIS self
  -- @param #string Text Report text.

  --- Triggers the FSM event "Report" after a delay.
  -- @function [parent=#ATIS] __Report
  -- @param #ATIS self
  -- @param #number delay Delay in seconds.
  -- @param #string Text Report text.

  --- On after "Report" event user function.
  -- @function [parent=#ATIS] OnAfterReport
  -- @param #ATIS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string Text Report text.

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- [Internal] Init localization
-- @param #ATIS self
-- @return #ATIS self
function ATIS:_InitLocalization()
  self:T(self.lid.."_InitLocalization")
  self.gettext = TEXTANDSOUND:New("ATIS","en") -- Core.TextAndSound#TEXTANDSOUND
  self.locale = "en"
  for locale,table in pairs(self.Messages) do
    local Locale = string.lower(tostring(locale))
    self:T("**** Adding locale: "..Locale)
    for ID,Text in pairs(table) do
      self:T(string.format('Adding ID %s',tostring(ID)))
      self.gettext:AddEntry(Locale,tostring(ID),Text)
    end
  end
  return self
end

--- Set locale for localized text-to-sound output via SRS, defaults to "en".
-- @param #ATIS self
-- @param #string locale Locale for localized text-to-sound output via SRS, defaults to "en".
-- @return #ATIS self
function ATIS:SetLocale(locale)
  self.locale = string.lower(locale)
  return self
end

--- Set sound files folder within miz file (not your local hard drive!).
-- @param #ATIS self
-- @param #string pathMain Path to folder containing main sound files. Default "ATIS Soundfiles/". Mind the slash "/" at the end!
-- @param #string pathAirports Path folder containing the airport names sound files. Default is `"ATIS Soundfiles/<Map Name>"`, *e.g.* `"ATIS Soundfiles/Caucasus/"`.
-- @param #string pathNato Path folder containing the NATO alphabet sound files. Default is "ATIS Soundfiles/NATO Alphabet/".
-- @return #ATIS self
function ATIS:SetSoundfilesPath( pathMain, pathAirports, pathNato )
  self.soundpath = tostring( pathMain or "ATIS Soundfiles/" )
  if pathAirports==nil then
    self.soundpathAirports=self.soundpath..env.mission.theatre.."/"
  else
    self.soundpathAirports=pathAirports
  end
  if pathNato==nil then
    self.soundpathNato=self.soundpath.."NATO Alphabet/"
  else
    self.soundpathNato=pathNato
  end
  self:T( self.lid .. string.format( "Setting sound files path to %s", self.soundpath ) )
  return self
end

--- Set the path to the csv file that contains information about the used sound files.
-- The parameter file has to be located on your local disk (**not** inside the miz file).
-- @param #ATIS self
-- @param #string csvfile Full path to the csv file on your local disk.
-- @return #ATIS self
function ATIS:SetSoundfilesInfo( csvfile )

  --- Local function to return the ATIS.Soundfile for a given file name
  local function getSound(filename)
    for key,_soundfile in pairs(self.Sound) do
      local soundfile=_soundfile --#ATIS.Soundfile
      if filename==soundfile.filename then
        return soundfile
      end
    end
    return nil
  end

  -- Read csv file
  local data=UTILS.ReadCSV(csvfile)

  if data then

    for i,sound in pairs(data) do

      -- Get the ATIS.Soundfile
      local soundfile=getSound(sound.filename..".ogg") --#ATIS.Soundfile

      if soundfile then

        -- Set duration
        soundfile.duration=tonumber(sound.duration)

      else
        self:E(string.format("ERROR: Could not get info for sound file %s", sound.filename))
      end

    end
  else
    self:E(string.format("ERROR: Could not read sound csv file!"))
  end


  return self
end

--- Set airborne unit (airplane or helicopter), used to transmit radio messages including subtitles.
-- Best is to place the unit on a parking spot of the airbase and set it to *uncontrolled* in the mission editor.
-- @param #ATIS self
-- @param #string unitname Name of the unit.
-- @return #ATIS self
function ATIS:SetRadioRelayUnitName( unitname )
  self.relayunitname = unitname
  self:T( self.lid .. string.format( "Setting radio relay unit to %s", self.relayunitname ) )
  return self
end

--- Set tower frequencies.
-- @param #ATIS self
-- @param #table freqs Table of frequencies in MHz. A single frequency can be given as a plain number (*i.e.* must not be table).
-- @return #ATIS self
function ATIS:SetTowerFrequencies( freqs )
  if type( freqs ) == "table" then
  -- nothing to do
  else
    freqs = { freqs }
  end
  self.towerfrequency = freqs
  return self
end

--- For SRS - Switch to only transmit if there are players on the server.
-- @param #ATIS self
-- @param #boolean Switch If true, only send SRS if there are alive Players.
-- @return #ATIS self
function ATIS:SetTransmitOnlyWithPlayers(Switch)
  self.TransmitOnlyWithPlayers = Switch
  if self.msrsQ then
    self.msrsQ:SetTransmitOnlyWithPlayers(Switch)
  end
  return self
end

--- Set active runway for **landing** operations. This can be used if the automatic runway determination via the wind direction gives incorrect results.
-- For example, use this if there are two runways with the same directions.
-- @param #ATIS self
-- @param #string runway Active runway, *e.g.* "31L".
-- @return #ATIS self
function ATIS:SetActiveRunway( runway )
  self.activerunway = tostring( runway )
  local prefer = nil
  if string.find(string.lower(runway),"l") then
    prefer = true
  elseif string.find(string.lower(runway),"r") then
    prefer = false
  end
  self.airbase:SetActiveRunway(runway,prefer)
  return self
end

--- Set the active runway for landing.
-- @param #ATIS self
-- @param #string runway : Name of the runway, e.g. "31" or "02L" or "90R". If not given, the runway is determined from the wind direction.
-- @param #boolean preferleft : If true, perfer the left runway. If false, prefer the right runway. If nil (default), do not care about left or right.
-- @return #ATIS self
function ATIS:SetActiveRunwayLanding(runway, preferleft)
  self.airbase:SetActiveRunwayLanding(runway,preferleft)
  return self
end

--- Set the active runway for take-off.
-- @param #ATIS self
-- @param #string runway : Name of the runway, e.g. "31" or "02L" or "90R". If not given, the runway is determined from the wind direction.
-- @param #boolean preferleft : If true, perfer the left runway. If false, prefer the right runway. If nil (default), do not care about left or right.
-- @return #ATIS self
function ATIS:SetActiveRunwayTakeoff(runway,preferleft)
  self.airbase:SetActiveRunwayTakeoff(runway,preferleft)
  return self
end

--- Give information on runway length.
-- @param #ATIS self
-- @return #ATIS self
function ATIS:SetRunwayLength()
  self.rwylength = true
  return self
end

--- Give information on runway length.
-- @param #ATIS self
-- @return #ATIS self
function ATIS:SetRunwayLength()
  self.rwylength=true
  return self
end


--- Give information on airfield elevation
-- @param #ATIS self
-- @return #ATIS self
function ATIS:SetElevation()
  self.elevation = true
  return self
end

--- Set radio power. Note that this only applies if no relay unit is used.
-- @param #ATIS self
-- @param #number power Radio power in Watts. Default 100 W.
-- @return #ATIS self
function ATIS:SetRadioPower( power )
  self.power = power or 100
  return self
end

--- Use F10 map mark points.
-- @param #ATIS self
-- @param #boolean switch If *true* or *nil*, marks are placed on F10 map. If *false* this feature is set to off (default).
-- @return #ATIS self
function ATIS:SetMapMarks( switch )
  if switch == nil or switch == true then
    self.usemarker = true
  else
    self.usemarker = false
  end
  return self
end

--- Return the complete SRS Text block, if at least generated once. Else nil.
-- @param #ATIS self
-- @return #string SRSText
function ATIS:GetSRSText()
  return self.SRSText
end

--- Set magnetic runway headings as depicted on the runway, *e.g.* "13" for 130° or "25L" for the left runway with magnetic heading 250°.
-- @param #ATIS self
-- @param #table headings Magnetic headings. Inverse (-180°) headings are added automatically. You only need to specify one heading per runway direction. "L"eft and "R" right can also be appended.
-- @return #ATIS self
function ATIS:SetRunwayHeadingsMagnetic( headings )

  -- First make sure, we have a table.
  if type( headings ) == "table" then
  -- nothing to do
  else
    headings = { headings }
  end

  for _, heading in pairs( headings ) do

    if type( heading ) == "number" then
      heading = string.format( "%02d", heading )
    end

    -- Add runway heading to table.
    self:T( self.lid .. string.format( "Adding user specified magnetic runway heading %s", heading ) )
    table.insert( self.runwaymag, heading )

    local h = self:GetRunwayWithoutLR( heading )

    local head2 = tonumber( h ) - 18
    if head2 < 0 then
      head2 = head2 + 36
    end

    -- Convert to string.
    head2 = string.format( "%02d", head2 )

    -- Append "L" or "R" if necessary.
    local left = self:GetRunwayLR( heading )
    if left == true then
      head2 = head2 .. "L"
    elseif left == false then
      head2 = head2 .. "R"
    end

    -- Add inverse runway heading to table.
    self:T( self.lid .. string.format( "Adding user specified magnetic runway heading %s (inverse)", head2 ) )
    table.insert( self.runwaymag, head2 )
  end

  return self
end

--- Set duration how long subtitles are displayed.
-- @param #ATIS self
-- @param #number duration Duration in seconds. Default 10 seconds.
-- @return #ATIS self
function ATIS:SetSubtitleDuration( duration )
  self.subduration = tonumber( duration or 10 )
  return self
end

--- Set unit system to metric units.
-- @param #ATIS self
-- @return #ATIS self
function ATIS:SetMetricUnits()
  self.metric = true
  return self
end

--- Set unit system to imperial units.
-- @param #ATIS self
-- @return #ATIS self
function ATIS:SetImperialUnits()
  self.metric = false
  return self
end

--- Set pressure unit to millimeters of mercury (mmHg).
-- Default is inHg for imperial and hPa (=mBar) for metric units.
-- @param #ATIS self
-- @return #ATIS self
function ATIS:SetPressureMillimetersMercury()
  self.PmmHg = true
  return self
end

--- Set temperature to be given in degrees Fahrenheit.
-- @param #ATIS self
-- @return #ATIS self
function ATIS:SetTemperatureFahrenheit()
  self.TDegF = true
  return self
end

--- Set relative humidity. This is used to approximately calculate the dew point.
-- Note that the dew point is only an artificial information as DCS does not have an atmospheric model that includes humidity (yet).
-- @param #ATIS self
-- @param #number Humidity Relative Humidity, i.e. a number between 0 and 100 %. Default is 50 %.
-- @return #ATIS self
function ATIS:SetRelativeHumidity( Humidity )
  self.relHumidity = Humidity or 50
  return self
end

--- Report altimeter QNH.
-- @param #ATIS self
-- @param #boolean switch If true or nil, report altimeter QHN. If false, report QFF.
-- @return #ATIS self
function ATIS:SetAltimeterQNH( switch )

  if switch == true or switch == nil then
    self.altimeterQNH = true
  else
    self.altimeterQNH = false
  end

  return self
end

--- Additionally report altimeter QNH/QFE in hPa, even if not set to metric.
-- @param #ATIS self
-- @param #boolean switch If true or nil, report mBar/hPa in addition.
-- @return #ATIS self
function ATIS:SetReportmBar(switch)
  if switch == true or switch == nil then
    self.ReportmBar = true
  else
    self.ReportmBar = false
  end
  return self
end

--- Additionally report free text, only working with SRS(!)
-- @param #ATIS self
-- @param #string text The text to report at the end of the ATIS message, e.g. runway closure, warnings, etc.
-- @return #ATIS self
function ATIS:SetAdditionalInformation(text)
  self.AdditionalInformation = text
  return self
end

--- Suppresses QFE readout. Default is to report both QNH and QFE.
-- @param #ATIS self
-- @return #ATIS self
function ATIS:ReportQNHOnly()
  self.qnhonly = true
  return self
end

--- Set magnetic declination/variation at the airport.
--
-- Default is per map:
--
-- * Caucasus +6 (East), year ~ 2011
-- * NTTR +12 (East), year ~ 2011
-- * Normandy -10 (West), year ~ 1944
-- * Persian Gulf +2 (East), year ~ 2011
--
-- To get *true* from *magnetic* heading one has to add easterly or substract westerly variation, e.g
--
-- A magnetic heading of 180° corresponds to a true heading of
--
--   * 186° on the Caucaus map
--   * 192° on the Nevada map
--   * 170° on the Normandy map
--   * 182° on the Persian Gulf map
--
-- Likewise, to convert *true* into *magnetic* heading, one has to substract easterly and add westerly variation.
--
-- Or you make your life simple and just include the sign so you don't have to bother about East/West.
--
-- @param #ATIS self
-- @param #number magvar Magnetic variation in degrees. Positive for easterly and negative for westerly variation. Default is magnatic declinaton of the used map, c.f. @{Utilities.Utils#UTILS.GetMagneticDeclination}.
-- @return #ATIS self
function ATIS:SetMagneticDeclination( magvar )
  self.magvar = magvar or UTILS.GetMagneticDeclination()
  return self
end

--- Explicitly set correction of magnetic to true heading for runways.
-- @param #ATIS self
-- @param #number correction Correction of magnetic to true heading for runways in degrees.
-- @return #ATIS self
function ATIS:SetRunwayCorrectionMagnetic2True( correction )
  self.runwaym2t = correction or ATIS.RunwayM2T[UTILS.GetDCSMap()]
  return self
end

--- Set wind direction (from) to be reported as *true* heading. Default is magnetic.
-- @param #ATIS self
-- @return #ATIS self
function ATIS:SetReportWindTrue()
  self.windtrue = true
  return self
end

--- Set time local difference with respect to Zulu time.
-- Default is per map:
--
--    * Caucasus +4
--    * Nevada -8
--    * Normandy 0
--    * Persian Gulf +4
--    * The Channel +2 (should be 0)
--
-- @param #ATIS self
-- @param #number delta Time difference in hours.
-- @return #ATIS self
function ATIS:SetZuluTimeDifference( delta )
  self.zuludiff = delta
  return self
end

--- Suppresses local time, sunrise, and sunset. Default is to report all these times.
-- @param #ATIS self
-- @return #ATIS self
function ATIS:ReportZuluTimeOnly()
  self.zulutimeonly = true
  return self
end

--- Add ILS station. Note that this can be runway specific.
-- @param #ATIS self
-- @param #number frequency ILS frequency in MHz.
-- @param #string runway (Optional) Runway for which the given ILS frequency applies. Default all (*nil*).
-- @return #ATIS self
function ATIS:AddILS( frequency, runway )
  local ils = {} -- #ATIS.NavPoint
  ils.frequency = tonumber( frequency )
  ils.runway = runway and tostring( runway ) or nil
  table.insert( self.ils, ils )
  return self
end

--- Set VOR station.
-- @param #ATIS self
-- @param #number frequency VOR frequency.
-- @return #ATIS self
function ATIS:SetVOR( frequency )
  self.vor = frequency
  return self
end

--- Add outer NDB. Note that this can be runway specific.
-- @param #ATIS self
-- @param #number frequency NDB frequency in MHz.
-- @param #string runway (Optional) Runway for which the given NDB frequency applies. Default all (*nil*).
-- @return #ATIS self
function ATIS:AddNDBouter( frequency, runway )
  local ndb = {} -- #ATIS.NavPoint
  ndb.frequency = tonumber( frequency )
  ndb.runway = runway and tostring( runway ) or nil
  table.insert( self.ndbouter, ndb )
  return self
end

--- Add inner NDB. Note that this can be runway specific.
-- @param #ATIS self
-- @param #number frequency NDB frequency in MHz.
-- @param #string runway (Optional) Runway for which the given NDB frequency applies. Default all (*nil*).
-- @return #ATIS self
function ATIS:AddNDBinner( frequency, runway )
  local ndb = {} -- #ATIS.NavPoint
  ndb.frequency = tonumber( frequency )
  ndb.runway = runway and tostring( runway ) or nil
  table.insert( self.ndbinner, ndb )
  return self
end

--- Set TACAN channel.
-- @param #ATIS self
-- @param #number channel TACAN channel.
-- @return #ATIS self
function ATIS:SetTACAN( channel )
  self.tacan = channel
  return self
end

--- Set RSBN channel.
-- @param #ATIS self
-- @param #number channel RSBN channel.
-- @return #ATIS self
function ATIS:SetRSBN( channel )
  self.rsbn = channel
  return self
end

--- Add PRMG channel. Note that this can be runway specific.
-- @param #ATIS self
-- @param #number channel PRMG channel.
-- @param #string runway (Optional) Runway for which the given PRMG channel applies. Default all (*nil*).
-- @return #ATIS self
function ATIS:AddPRMG( channel, runway )
  local ndb = {} -- #ATIS.NavPoint
  ndb.frequency = tonumber( channel )
  ndb.runway = runway and tostring( runway ) or nil
  table.insert( self.prmg, ndb )
  return self
end

--- Place marks with runway data on the F10 map.
-- @param #ATIS self
-- @param #boolean markall If true, mark all runways of the map. By default only the current ATIS runways are marked.
function ATIS:MarkRunways( markall )
  local airbases = AIRBASE.GetAllAirbases()
  for _, _airbase in pairs( airbases ) do
    local airbase = _airbase -- Wrapper.Airbase#AIRBASE
    if (not markall and airbase:GetName() == self.airbasename) or markall == true then
      airbase:GetRunwayData( self.runwaym2t, true )
    end
  end
end

--- Use SRS Simple-Text-To-Speech for transmissions. No sound files necessary.`SetSRS()` will try to use as many attributes configured with @{Sound.SRS#MSRS.LoadConfigFile}() as possible.
-- @param #ATIS self
-- @param #string PathToSRS Path to SRS directory (only necessary if SRS exe backend is used).
-- @param #string Gender Gender: "male" or "female" (default).
-- @param #string Culture Culture, e.g. "en-GB" (default).
-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`.
-- @param #number Port SRS port. Default 5002.
-- @param #string GoogleKey Path to Google JSON-Key (SRS exe backend) or Google API key (DCS-gRPC backend).
-- @return #ATIS self
function ATIS:SetSRS(PathToSRS, Gender, Culture, Voice, Port, GoogleKey)
  --if PathToSRS or MSRS.path then
    self.useSRS=true

    local path = PathToSRS or MSRS.path
    local gender = Gender or MSRS.gender
    local culture = Culture or MSRS.culture
    local voice = Voice or MSRS.voice
    local port = Port or MSRS.port or 5002

    self.msrs=MSRS:New(path, self.frequency, self.modulation)
    self.msrs:SetGender(gender)
    self.msrs:SetCulture(culture)
    self.msrs:SetPort(port)
    self.msrs:SetCoalition(self:GetCoalition())
    self.msrs:SetLabel("ATIS")
    if GoogleKey then
      self.msrs:SetProviderOptionsGoogle(GoogleKey,GoogleKey)
      self.msrs:SetProvider(MSRS.Provider.GOOGLE)
    end
   -- Pre-configured Google?
    if (not GoogleKey) and self.msrs:GetProvider() == MSRS.Provider.GOOGLE then
      voice = Voice or MSRS.poptions.gcloud.voice
    end
    self.msrs:SetVoice(voice)
    self.msrs:SetCoordinate(self.airbase:GetCoordinate())
    self.msrsQ = MSRSQUEUE:New("ATIS")
    self.msrsQ:SetTransmitOnlyWithPlayers(self.TransmitOnlyWithPlayers)
    if self.dTQueueCheck<=10 then
      self:SetQueueUpdateTime(90)
    end
  --else
    --self:E(self.lid..string.format("ERROR: No SRS path specified!"))
  --end
  return self
end

--- Set an alternative provider to the one set in your MSRS configuration file.
-- @param #ATIS self
-- @param #string Provider The provider to use. Known providers are: `MSRS.Provider.WINDOWS` and `MSRS.Provider.GOOGLE`
-- @return #ATIS self
function ATIS:SetSRSProvider(Provider)
  self:T(self.lid.."SetSRSProvider")
  if self.msrs then
    self.msrs:SetProvider(Provider)
  else
    MESSAGE:New(self.lid.."Set up SRS first before trying to change the provider!",30,"ATIS"):ToAll():ToLog()
  end
  return self
end

--- Set the time interval between radio queue updates.
-- @param #ATIS self
-- @param #number TimeInterval Interval in seconds. Default 5 sec.
-- @return #ATIS self
function ATIS:SetQueueUpdateTime( TimeInterval )
  self.dTQueueCheck = TimeInterval or 5
end

--- Get the coalition of the associated airbase.
-- @param #ATIS self
-- @return #number Coalition of the associated airbase.
function ATIS:GetCoalition()
  local coal = self.airbase and self.airbase:GetCoalition() or nil
  return coal
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start & Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Start ATIS FSM.
-- @param #ATIS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ATIS:onafterStart( From, Event, To )
  self:T({From, Event, To})
  self:T("Airbase category is "..self.airbase:GetAirbaseCategory())

  -- Check that this is an airdrome.
  if self.airbase:GetAirbaseCategory() == Airbase.Category.SHIP then
    self:E( self.lid .. string.format( "ERROR: Cannot start ATIS for airbase %s! Only AIRDROMES are supported but NOT SHIPS.", self.airbasename ) )
    return
  end

  -- Check that if is a Helipad.
  if self.airbase:GetAirbaseCategory() == Airbase.Category.HELIPAD then
    self:E( self.lid .. string.format( "EXPERIMENTAL: Starting ATIS for Helipad %s! SRS must be ON", self.airbasename ) )
    self.ATISforFARPs = true
    self.useSRS = true
  end

  -- Info.
  if type(self.frequency) == "table" then
    local frequency = table.concat(self.frequency,"/")
    local modulation = self.modulation
    if type(self.modulation) == "table" then
      modulation = table.concat(self.modulation,"/")
    end
    self:I( self.lid .. string.format( "Starting ATIS v%s for airbase %s on %s MHz Modulation=%s", ATIS.version, self.airbasename, frequency, modulation ) )
  else
    self:I( self.lid .. string.format( "Starting ATIS v%s for airbase %s on %.3f MHz Modulation=%d", ATIS.version, self.airbasename, self.frequency, self.modulation ) )
  end
  -- Start radio queue.
  if not self.useSRS then
    self.radioqueue = RADIOQUEUE:New( self.frequency, self.modulation, string.format( "ATIS %s", self.airbasename ) )

    -- Send coordinate is airbase coord.
    self.radioqueue:SetSenderCoordinate( self.airbase:GetCoordinate() )

    -- Set relay unit if we have one.
    self.radioqueue:SetSenderUnitName( self.relayunitname )

    -- Set radio power.
    self.radioqueue:SetRadioPower( self.power )

    -- Init numbers.
    self.radioqueue:SetDigit( 0, self.Sound.N0.filename, self.Sound.N0.duration, self.soundpath )
    self.radioqueue:SetDigit( 1, self.Sound.N1.filename, self.Sound.N1.duration, self.soundpath )
    self.radioqueue:SetDigit( 2, self.Sound.N2.filename, self.Sound.N2.duration, self.soundpath )
    self.radioqueue:SetDigit( 3, self.Sound.N3.filename, self.Sound.N3.duration, self.soundpath )
    self.radioqueue:SetDigit( 4, self.Sound.N4.filename, self.Sound.N4.duration, self.soundpath )
    self.radioqueue:SetDigit( 5, self.Sound.N5.filename, self.Sound.N5.duration, self.soundpath )
    self.radioqueue:SetDigit( 6, self.Sound.N6.filename, self.Sound.N6.duration, self.soundpath )
    self.radioqueue:SetDigit( 7, self.Sound.N7.filename, self.Sound.N7.duration, self.soundpath )
    self.radioqueue:SetDigit( 8, self.Sound.N8.filename, self.Sound.N8.duration, self.soundpath )
    self.radioqueue:SetDigit( 9, self.Sound.N9.filename, self.Sound.N9.duration, self.soundpath )

    -- Start radio queue.
    self.radioqueue:Start( 1, 0.1 )
  end

  -- Handle airbase capture
  -- Handle events.
  self:HandleEvent( EVENTS.BaseCaptured )

  -- Init status updates.
  self:__Status( -2 )
  self:__CheckQueue( -3 )
end

--- Update status.
-- @param #ATIS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ATIS:onafterStatus( From, Event, To )
  self:T({From, Event, To})
  -- Get FSM state.
  local fsmstate = self:GetState()

  local relayunitstatus = "N/A"
  if self.relayunitname then
    local ru = UNIT:FindByName( self.relayunitname )
    if ru then
      relayunitstatus = tostring( ru:IsAlive() )
    end
  end

  -- Info text.
  local text = ""
  if type(self.frequency) == "table" then
    local frequency = table.concat(self.frequency,"/")
    local modulation = self.modulation
    if type(self.modulation) == "table" then
      modulation = table.concat(self.modulation,"/")
    end
    text = string.format( "State %s: Freq=%s MHz %s", fsmstate, frequency, modulation )
  else
    text = string.format( "State %s: Freq=%.3f MHz %s", fsmstate, self.frequency, UTILS.GetModulationName( self.modulation ) )
  end
  if self.useSRS then
    text = text .. string.format( ", SRS path=%s (%s), gender=%s, culture=%s, voice=%s", tostring( self.msrs.path ), tostring( self.msrs.port ), tostring( self.msrs.gender ), tostring( self.msrs.culture ), tostring( self.msrs.voice ) )
  else
    text = text .. string.format( ", Relay unit=%s (alive=%s)", tostring( self.relayunitname ), relayunitstatus )
  end
  self:T( self.lid .. text )

  if not self:Is("Stopped") then
    self:__Status( 60 )
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check if radio queue is empty. If so, start broadcasting the message again.
-- @param #ATIS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ATIS:onafterCheckQueue( From, Event, To )
  self:T({From, Event, To})
  if not self:Is("Stopped") then
    if self.useSRS then

      self:Broadcast()

    else

      if #self.radioqueue.queue == 0 then
        self:T( self.lid .. string.format( "Radio queue empty. Repeating message." ) )
        self:Broadcast()
      else
        self:T2( self.lid .. string.format( "Radio queue %d transmissions queued.", #self.radioqueue.queue ) )
      end

    end


    -- Check back in 5 seconds.
    self:__CheckQueue( math.abs( self.dTQueueCheck ) )
  end
end

--- Broadcast ATIS radio message.
-- @param #ATIS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ATIS:onafterBroadcast( From, Event, To )
  self:T({From, Event, To})
  -- Get current coordinate.
  local coord = self.airbase:GetCoordinate()

  -- Get elevation.
  local height = coord:GetLandHeight()

  ----------------
  --- Pressure ---
  ----------------

  -- Pressure in hPa.
  local qfe = coord:GetPressure( height )
  local qnh = coord:GetPressure( 0 )

  if self.altimeterQNH then

    -- Some constants.
    local L = -0.0065 -- [K/m]
    local R = 8.31446 -- [J/mol/K]
    local g = 9.80665 -- [m/s^2]
    local M = 0.0289644 -- [kg/mol]
    local T0 = coord:GetTemperature( 0 ) + 273.15 -- [K] Temp at sea level.
    local TS = 288.15 -- Standard Temperature assumed by Altimeter is 15°C
    local q = qnh * 100

    -- Calculate Pressure.
    local P = q * (1 + L * height / T0) ^ (-g * M / (R * L)) -- Pressure at sea level
    local Q = P / (1 + L * height / TS) ^ (-g * M / (R * L)) -- Altimeter QNH
    local A = (T0 / L) * ((P / q) ^ (((-R * L) / (g * M))) - 1) -- Altitude check

    -- Debug aoutput
    self:T2( self.lid .. string.format( "height=%.1f, A=%.1f, T0=%.1f, QFE=%.1f, QNH=%.1f, P=%.1f, Q=%.1f hPa = %.2f", height, A, T0 - 273.15, qfe, qnh, P / 100, Q / 100, UTILS.hPa2inHg( Q / 100 ) ) )

    -- Set QNH value in hPa.
    qnh = Q / 100

  end

  local mBarqnh = qnh
  local mBarqfe = qfe

  -- Convert to inHg.
  if self.PmmHg then
    qfe = UTILS.hPa2mmHg( qfe )
    qnh = UTILS.hPa2mmHg( qnh )
  else
    if not self.metric then
      qfe = UTILS.hPa2inHg( qfe )
      qnh = UTILS.hPa2inHg( qnh )
    end
  end

  local QFE = UTILS.Split( string.format( "%.2f", qfe ), "." )
  local QNH = UTILS.Split( string.format( "%.2f", qnh ), "." )

  if self.PmmHg then
    QFE = UTILS.Split( string.format( "%.1f", qfe ), "." )
    QNH = UTILS.Split( string.format( "%.1f", qnh ), "." )
  else
    if self.metric then
      QFE = UTILS.Split( string.format( "%.1f", qfe ), "." )
      QNH = UTILS.Split( string.format( "%.1f", qnh ), "." )
    end
  end

  ------------
  --- Wind ---
  ------------

  -- Get wind direction and speed in m/s.
  local windFrom, windSpeed = coord:GetWind( height + 10 )

  -- Wind in magnetic or true.
  local magvar = self.magvar
  if self.windtrue then
    magvar = 0
  end
  windFrom = windFrom - magvar

  -- Correct negative values.
  if windFrom < 0 then
    windFrom = windFrom + 360
  end

  local WINDFROM = string.format( "%03d", windFrom )
  local WINDSPEED = string.format( "%d", UTILS.MpsToKnots( windSpeed ) )

  -- Report North as 0.
  if WINDFROM == "000" then
    WINDFROM = "360"
  end

  if self.metric then
    WINDSPEED = string.format( "%d", windSpeed )
  end

  --------------
  --- Runway ---
  --------------


  local runwayLanding, rwyLandingLeft
  local runwayTakeoff, rwyTakeoffLeft

  if self.airbase:GetAirbaseCategory() == Airbase.Category.HELIPAD then
    runwayLanding, rwyLandingLeft="PAD 01",false
    runwayTakeoff, rwyTakeoffLeft="PAD 02",false
  else
    runwayLanding, rwyLandingLeft=self:GetActiveRunway()
    runwayTakeoff, rwyTakeoffLeft=self:GetActiveRunway(true)
  end

  ------------
  --- Time ---
  ------------
  local time = timer.getAbsTime()

  -- Conversion to Zulu time.
  if self.zuludiff then
    -- User specified.
    time = time - self.zuludiff * 60 * 60
  else
    time = time - UTILS.GMTToLocalTimeDifference() * 60 * 60
  end

  if time < 0 then
    time = 24 * 60 * 60 + time -- avoid negative time around midnight
  end

  local clock = UTILS.SecondsToClock( time )
  local zulu = UTILS.Split( clock, ":" )
  local ZULU = string.format( "%s%s", zulu[1], zulu[2] )
  local hours = self.gettext:GetEntry("TIME",self.locale)
  if self.useSRS then
    ZULU = string.format( "%s %s", hours, zulu[1] )
  end

  -- NATO time stamp. 0=Alfa, 1=Bravo, 2=Charlie, etc.
  local NATO = ATIS.Alphabet[tonumber( zulu[1] ) + 1]

  -- Debug.
  self:T3( string.format( "clock=%s", tostring( clock ) ) )
  self:T3( string.format( "zulu1=%s", tostring( zulu[1] ) ) )
  self:T3( string.format( "zulu2=%s", tostring( zulu[2] ) ) )
  self:T3( string.format( "ZULU =%s", tostring( ZULU ) ) )
  self:T3( string.format( "NATO =%s", tostring( NATO ) ) )

  --------------------------
  --- Sunrise and Sunset ---
  --------------------------

  local hours = self.gettext:GetEntry("HOURS",self.locale)
  local sunrise = coord:GetSunrise()
  --self:I(sunrise)
  local SUNRISE = "no time"
  local NorthPolar = true
  if tostring(sunrise) ~= "N/S" and tostring(sunrise) ~= "N/R" then
    sunrise = UTILS.Split( sunrise, ":" )
    SUNRISE = string.format( "%s%s", sunrise[1], sunrise[2] )
    if self.useSRS then
      SUNRISE = string.format( "%s %s %s", sunrise[1], sunrise[2], hours )
    end
    NorthPolar = false
  end
  
  local sunset = coord:GetSunset()
  --self:I(sunset)
  local SUNSET = "no time"
  if tostring(sunset) ~= "N/S" and tostring(sunset) ~= "N/R" then
    sunset = UTILS.Split( sunset, ":" )
    SUNSET = string.format( "%s%s", sunset[1], sunset[2] )
    if self.useSRS then
      SUNSET = string.format( "%s %s %s", sunset[1], sunset[2], hours )
    end
    NorthPolar = false
  end

  ---------------------------------
  --- Temperature and Dew Point ---
  ---------------------------------

  -- Temperature in °C.
  local temperature = coord:GetTemperature( height + 5 )

  -- Dew point in °C.
  local dewpoint = temperature - (100 - self.relHumidity) / 5

  -- Convert to °F.
  if self.TDegF then
    temperature=UTILS.CelsiusToFahrenheit(temperature)
    dewpoint=UTILS.CelsiusToFahrenheit(dewpoint)
  end

  local TEMPERATURE = string.format( "%d", math.abs( temperature ) )
  local DEWPOINT = string.format( "%d", math.abs( dewpoint ) )

  ---------------
  --- Weather ---
  ---------------

  -- Get mission weather info. Most of this is static.
  local clouds, visibility, turbulence, dustdens, static = self:GetMissionWeather()
  
  local dust=false
  local fog=false

  ------------------
  --- Visibility ---
  ------------------

  if dustdens then
  
    -- Dust only up to 1500 ft = 457 m ASL.
    if UTILS.FeetToMeters( 1500 )> height+25 then
      dust=true
      visibility=math.min(visibility, dustdens)
    end
    
  else -- As of DCS 2.9.10.3948 (December 2024), fog and dust are mutually exclusive!
  
    -- Get current fog visibility and thickness
    local fvis=world.weather.getFogVisibilityDistance()
    local fheight=world.weather.getFogThickness()
    
    if fvis>0 and fheight>height+25 then
      fog=true
      visibility=math.min(visibility, fvis)
    end
  end

  local VISIBILITY = ""

  if self.metric then
    -- Visibility in km.
    local reportedviz = UTILS.Round( visibility / 1000 )
    -- max reported visibility 9999 m
    if reportedviz > 10 then
      reportedviz = 10
    end
    VISIBILITY = string.format( "%d", reportedviz )
  else
    -- max reported visibility 10 NM
    local reportedviz = UTILS.Round( UTILS.MetersToSM( visibility ) )
    if reportedviz > 10 then
      reportedviz = 10
    end
    VISIBILITY = string.format( "%d", reportedviz )
  end

  --------------
  --- Clouds ---
  --------------

  local cloudbase = clouds.base
  local cloudceil = clouds.base + clouds.thickness
  local clouddens = clouds.density
    
  -- Cloud preset (DCS 2.7)
  local cloudspreset = clouds.preset or "Nothing"

  -- Precepitation: 0=None, 1=Rain, 2=Thunderstorm, 3=Snow, 4=Snowstorm.
  local precepitation = 0

  if cloudspreset:find( "RainyPreset1" ) then
    -- Overcast + Rain
    clouddens = 9
    if temperature > 5 then
      precepitation = 1 -- rain
    else
      precepitation = 3 -- snow
    end
  elseif cloudspreset:find( "RainyPreset2" ) then
    -- Overcast + Rain
    clouddens = 9
    if temperature > 5 then
      precepitation = 1 -- rain
    else
      precepitation = 3 -- snow
    end
  elseif cloudspreset:find( "RainyPreset3" ) then
    -- Overcast + Rain
    clouddens = 9
    if temperature > 5 then
      precepitation = 1 -- rain
    else
      precepitation = 3 -- snow
    end
  elseif cloudspreset:find( "RainyPreset4" ) then
    -- Overcast + Rain
    clouddens = 5
    if temperature > 5 then
      precepitation = 1 -- rain
    else
      precepitation = 3 -- snow
    end
  elseif cloudspreset:find( "RainyPreset5" ) then
    -- Overcast + Rain
    clouddens = 5
    if temperature > 5 then
      precepitation = 1 -- rain
    else
      precepitation = 3 -- snow
    end
  elseif cloudspreset:find( "RainyPreset6" ) then
    -- Overcast + Rain
    clouddens = 5
    if temperature > 5 then
      precepitation = 1 -- rain
    else
      precepitation = 3 -- snow
    end
    -- NEWRAINPRESET4
  elseif cloudspreset:find( "NEWRAINPRESET4" ) then
    -- Overcast + Rain
    clouddens = 5
    if temperature > 5 then
      precepitation = 1 -- rain
    else
      precepitation = 3 -- snow
    end
  elseif cloudspreset:find( "RainyPreset" ) then
    -- Overcast + Rain
    clouddens = 9
    if temperature > 5 then
      precepitation = 1 -- rain
    else
      precepitation = 3 -- snow
    end
  elseif cloudspreset:find( "Preset10" ) then
    -- Scattered 5
    clouddens = 4
  elseif cloudspreset:find( "Preset11" ) then
    -- Scattered 6
    clouddens = 4
  elseif cloudspreset:find( "Preset12" ) then
    -- Scattered 7
    clouddens = 4
  elseif cloudspreset:find( "Preset13" ) then
    -- Broken 1
    clouddens = 7
  elseif cloudspreset:find( "Preset14" ) then
    -- Broken 2
    clouddens = 7
  elseif cloudspreset:find( "Preset15" ) then
    -- Broken 3
    clouddens = 7
  elseif cloudspreset:find( "Preset16" ) then
    -- Broken 4
    clouddens = 7
  elseif cloudspreset:find( "Preset17" ) then
    -- Broken 5
    clouddens = 7
  elseif cloudspreset:find( "Preset18" ) then
    -- Broken 6
    clouddens = 7
  elseif cloudspreset:find( "Preset19" ) then
    -- Broken 7
    clouddens = 7
  elseif cloudspreset:find( "Preset20" ) then
    -- Broken 8
    clouddens = 7
  elseif cloudspreset:find( "Preset21" ) then
    -- Overcast 1
    clouddens = 9
  elseif cloudspreset:find( "Preset22" ) then
    -- Overcast 2
    clouddens = 9
  elseif cloudspreset:find( "Preset23" ) then
    -- Overcast 3
    clouddens = 9
  elseif cloudspreset:find( "Preset24" ) then
    -- Overcast 4
    clouddens = 9
  elseif cloudspreset:find( "Preset25" ) then
    -- Overcast 5
    clouddens = 9
  elseif cloudspreset:find( "Preset26" ) then
    -- Overcast 6
    clouddens = 9
  elseif cloudspreset:find( "Preset27" ) then
    -- Overcast 7
    clouddens = 9
  elseif cloudspreset:find( "Preset1" ) then
    -- Light Scattered 1
    clouddens = 1
  elseif cloudspreset:find( "Preset2" ) then
    -- Light Scattered 2
    clouddens = 1
  elseif cloudspreset:find( "Preset3" ) then
    -- High Scattered 1
    clouddens = 4
  elseif cloudspreset:find( "Preset4" ) then
    -- High Scattered 2
    clouddens = 4
  elseif cloudspreset:find( "Preset5" ) then
    -- Scattered 1
    clouddens = 4
  elseif cloudspreset:find( "Preset6" ) then
    -- Scattered 2
    clouddens = 4
  elseif cloudspreset:find( "Preset7" ) then
    -- Scattered 3
    clouddens = 4
  elseif cloudspreset:find( "Preset8" ) then
    -- High Scattered 3
    clouddens = 4
  elseif cloudspreset:find( "Preset9" ) then
    -- Scattered 4
    clouddens = 4
  else
    self:E(string.format("WARNING! Unknown weather preset: %s", tostring(cloudspreset)))
  end

  local CLOUDBASE = string.format( "%d", UTILS.MetersToFeet( cloudbase ) )
  local CLOUDCEIL = string.format( "%d", UTILS.MetersToFeet( cloudceil ) )

  if self.metric then
    CLOUDBASE = string.format( "%d", cloudbase )
    CLOUDCEIL = string.format( "%d", cloudceil )
  end

  -- Cloud base/ceiling in thousands and hundrets of ft/meters.
  local CLOUDBASE1000, CLOUDBASE0100 = self:_GetThousandsAndHundreds( UTILS.MetersToFeet( cloudbase ) )
  local CLOUDCEIL1000, CLOUDCEIL0100 = self:_GetThousandsAndHundreds( UTILS.MetersToFeet( cloudceil ) )

  if self.metric then
    CLOUDBASE1000, CLOUDBASE0100 = self:_GetThousandsAndHundreds( cloudbase )
    CLOUDCEIL1000, CLOUDCEIL0100 = self:_GetThousandsAndHundreds( cloudceil )
  end

  -- No cloud info for dynamic weather.
  local CloudCover = {} -- #ATIS.Soundfile
  CloudCover = self.Sound.CloudsNotAvailable
  --local CLOUDSsub = "Cloud coverage information not available"
  local CLOUDSsub = self.gettext:GetEntry("NOCLOUDINFO",self.locale)
  -- Only valid for static weather.
  if static then
    if clouddens >= 9 then
      -- Overcast 9,10
      CloudCover = self.Sound.CloudsOvercast
      --CLOUDSsub = "Overcast"
      CLOUDSsub = self.gettext:GetEntry("OVERCAST",self.locale)
    elseif clouddens >= 7 then
      -- Broken 7,8
      CloudCover = self.Sound.CloudsBroken
      --CLOUDSsub = "Broken clouds"
      CLOUDSsub = self.gettext:GetEntry("BROKEN",self.locale)
    elseif clouddens >= 4 then
      -- Scattered 4,5,6
      CloudCover = self.Sound.CloudsScattered
      --CLOUDSsub = "Scattered clouds"
      CLOUDSsub = self.gettext:GetEntry("SCATTERED",self.locale)
    elseif clouddens >= 1 then
      -- Few 1,2,3
      CloudCover = self.Sound.CloudsFew
      --CLOUDSsub = "Few clouds"
      CLOUDSsub = self.gettext:GetEntry("FEWCLOUDS",self.locale)
    else
      -- No clouds
      CLOUDBASE = nil
      CLOUDCEIL = nil
      CloudCover = self.Sound.CloudsNo
      --CLOUDSsub = "No clouds"
      CLOUDSsub = self.gettext:GetEntry("NOCLOUDS",self.locale)
    end
  end

  --------------------
  --- Transmission ---
  --------------------

  -- Subtitle
  local subtitle = ""

  -- Airbase name
  subtitle = string.format( "%s", self.airbasename )
  if (not self.ATISforFARPs) and self.airbasename:find( "AFB" ) == nil and self.airbasename:find( "Airport" ) == nil
    and self.airbasename:find( "Airstrip" ) == nil and self.airbasename:find( "airfield" ) == nil and self.airbasename:find( "AB" ) == nil
    and self.airbasename:find( "Field" ) == nil
  then
    --subtitle = subtitle .. " Airport"
    subtitle = subtitle .. " "..self.gettext:GetEntry("AIRPORT",self.locale)
  end
  if not self.useSRS then
    --self:I(string.format( "%s/%s.ogg", self.theatre, self.airbasename ))
    self.radioqueue:NewTransmission( string.format( "%s.ogg", self.airbasename ), 3.0, self.soundpathAirports, nil, nil, subtitle, self.subduration )
  end
  local alltext = subtitle

  -- Information tag
  local information = self.gettext:GetEntry("INFORMATION",self.locale)
  --subtitle = string.format( "Information %s", NATO )
  subtitle = string.format( "%s %s", information, NATO )
  local _INFORMATION = subtitle
  if not self.useSRS then
    self:Transmission( self.Sound.Information, 0.5, subtitle )
    self.radioqueue:NewTransmission( string.format( "%s.ogg", NATO ), 0.75, self.soundpathNato )
  end
  alltext = alltext .. ";\n" .. subtitle

  -- Zulu Time
  subtitle = string.format( "%s Zulu", ZULU )
  if not self.useSRS then
    self.radioqueue:Number2Transmission( ZULU, nil, 0.5 )
    self:Transmission( self.Sound.Zulu, 0.2, subtitle )
  end
  alltext = alltext .. ";\n" .. subtitle

  if not self.zulutimeonly then

    -- Sunrise Time
    local sunrise = self.gettext:GetEntry("SUNRISEAT",self.locale)
    --subtitle = string.format( "Sunrise at %s local time", SUNRISE )
    subtitle = string.format( sunrise, SUNRISE )
    if not self.useSRS and NorthPolar == false then
      self:Transmission( self.Sound.SunriseAt, 0.5, subtitle )
      self.radioqueue:Number2Transmission( SUNRISE, nil, 0.2 )
      self:Transmission( self.Sound.TimeLocal, 0.2 )
    end
    alltext = alltext .. ";\n" .. subtitle

    -- Sunset Time
    local sunset = self.gettext:GetEntry("SUNSETAT",self.locale)
    --subtitle = string.format( "Sunset at %s local time", SUNSET )
    subtitle = string.format( sunset, SUNSET )
    if not self.useSRS and NorthPolar == false then
      self:Transmission( self.Sound.SunsetAt, 0.5, subtitle )
      self.radioqueue:Number2Transmission( SUNSET, nil, 0.5 )
      self:Transmission( self.Sound.TimeLocal, 0.2 )
    end
    alltext = alltext .. ";\n" .. subtitle
  end

  -- Wind
  -- Adding a space after each digit of WINDFROM to convert this to aviation-speak for TTS via SRS
  if self.useSRS then
    WINDFROM = string.gsub(WINDFROM,".", "%1 ")
  end
  if self.metric then
    local windfrom = self.gettext:GetEntry("WINDFROMMS",self.locale)
    --subtitle = string.format( "Wind from %s at %s m/s", WINDFROM, WINDSPEED )
    subtitle = string.format( windfrom, WINDFROM, WINDSPEED )
  else
    local windfrom = self.gettext:GetEntry("WINDFROMKNOTS",self.locale)
    --subtitle = string.format( "Wind from %s at %s m/s", WINDFROM, WINDSPEED )
    subtitle = string.format( windfrom, WINDFROM, WINDSPEED )
  end
  if turbulence > 0 then
    --subtitle = subtitle .. ", gusting"
    subtitle = subtitle .. ", "..self.gettext:GetEntry("GUSTING",self.locale)
  end
  local _WIND = subtitle
  if not self.useSRS then
    self:Transmission( self.Sound.WindFrom, 1.0, subtitle )
    self.radioqueue:Number2Transmission( WINDFROM )
    self:Transmission( self.Sound.At, 0.2 )
    self.radioqueue:Number2Transmission( WINDSPEED )
    if self.metric then
      self:Transmission( self.Sound.MetersPerSecond, 0.2 )
    else
      self:Transmission( self.Sound.Knots, 0.2 )
    end
    if turbulence > 0 then
      self:Transmission( self.Sound.Gusting, 0.2 )
    end
  end
  alltext = alltext .. ";\n" .. subtitle

  -- Visibility
  if self.metric then
    local visi = self.gettext:GetEntry("VISIKM",self.locale)
    --subtitle = string.format( "Visibility %s km", VISIBILITY )
    subtitle = string.format( visi, VISIBILITY )
  else
    local visi = self.gettext:GetEntry("VISISM",self.locale)
    --subtitle = string.format( "Visibility %s SM", VISIBILITY )
    subtitle = string.format( visi, VISIBILITY )
  end
  if not self.useSRS then
    self:Transmission( self.Sound.Visibilty, 1.0, subtitle )
    self.radioqueue:Number2Transmission( VISIBILITY )
    if self.metric then
      self:Transmission( self.Sound.Kilometers, 0.2 )
    else
      self:Transmission( self.Sound.StatuteMiles, 0.2 )
    end
  end
  alltext = alltext .. ";\n" .. subtitle

  subtitle = ""
  -- Weather phenomena
  local wp = false
  local wpsub = ""
  if precepitation == 1 then
    wp = true
    --wpsub = wpsub .. " rain"
    wpsub = wpsub .. " "..self.gettext:GetEntry("RAIN",self.locale)
  elseif precepitation == 2 then
    if wp then
      wpsub = wpsub .. ","
    end
    --wpsub = wpsub .. " thunderstorm"
    wpsub = wpsub .. " "..self.gettext:GetEntry("TSTORM",self.locale)
    wp = true
  elseif precepitation == 3 then
    --wpsub = wpsub .. " snow"
    wpsub = wpsub .. " "..self.gettext:GetEntry("SNOW",self.locale)
    wp = true
  elseif precepitation == 4 then
    --wpsub = wpsub .. " snowstorm"
    wpsub = wpsub .. " "..self.gettext:GetEntry("SSTROM",self.locale)
    wp = true
  end
  if fog then
    if wp then
      wpsub = wpsub .. ","
    end
    --wpsub = wpsub .. " fog"
    wpsub = wpsub .. " "..self.gettext:GetEntry("FOG",self.locale)
    wp = true
  end
  if dust then
    if wp then
      wpsub = wpsub .. ","
    end
    --wpsub = wpsub .. " dust"
    wpsub = wpsub .. " "..self.gettext:GetEntry("DUST",self.locale)
    wp = true
  end
  -- Actual output
  if wp then
    local phenos = self.gettext:GetEntry("PHENOMENA",self.locale)
    --subtitle = string.format( "Weather phenomena: %s", wpsub )
    subtitle = string.format( "%s: %s", phenos, wpsub )
    if not self.useSRS then
      self:Transmission( self.Sound.WeatherPhenomena, 1.0, subtitle )
      if precepitation == 1 then
        self:Transmission( self.Sound.Rain, 0.5 )
      elseif precepitation == 2 then
        self:Transmission( self.Sound.ThunderStorm, 0.5 )
      elseif precepitation == 3 then
        self:Transmission( self.Sound.Snow, 0.5 )
      elseif precepitation == 4 then
        self:Transmission( self.Sound.SnowStorm, 0.5 )
      end
      if fog then
        self:Transmission( self.Sound.Fog, 0.5 )
      end
      if dust then
        self:Transmission( self.Sound.Dust, 0.5 )
      end
    end
    alltext = alltext .. ";\n" .. subtitle
  end

  -- Cloud base
  if not self.useSRS then
    self:Transmission( CloudCover, 1.0, CLOUDSsub )
  end
  if CLOUDBASE and static then
    -- Base
    local cbase = tostring( tonumber( CLOUDBASE1000 ) * 1000 + tonumber( CLOUDBASE0100 ) * 100 )
    local cceil = tostring( tonumber( CLOUDCEIL1000 ) * 1000 + tonumber( CLOUDCEIL0100 ) * 100 )
    if self.metric then
      -- subtitle=string.format("Cloud base %s, ceiling %s meters", CLOUDBASE, CLOUDCEIL)
      local cloudbase = self.gettext:GetEntry("CLOUDBASEM",self.locale)
      --subtitle = string.format( "Cloud base %s, ceiling %s meters", cbase, cceil )
      subtitle = string.format( cloudbase, cbase, cceil )
    else
      -- subtitle=string.format("Cloud base %s, ceiling %s feet", CLOUDBASE, CLOUDCEIL)
      local cloudbase = self.gettext:GetEntry("CLOUDBASEFT",self.locale)
      --subtitle = string.format( "Cloud base %s, ceiling %s feet", cbase, cceil )
      subtitle = string.format( cloudbase, cbase, cceil )
    end
    if not self.useSRS then
      self:Transmission( self.Sound.CloudBase, 1.0, subtitle )
      if tonumber( CLOUDBASE1000 ) > 0 then
        self.radioqueue:Number2Transmission( CLOUDBASE1000 )
        self:Transmission( self.Sound.Thousand, 0.1 )
      end
      if tonumber( CLOUDBASE0100 ) > 0 then
        self.radioqueue:Number2Transmission( CLOUDBASE0100 )
        self:Transmission( self.Sound.Hundred, 0.1 )
      end
      -- Ceiling
      self:Transmission( self.Sound.CloudCeiling, 0.5 )
      if tonumber( CLOUDCEIL1000 ) > 0 then
        self.radioqueue:Number2Transmission( CLOUDCEIL1000 )
        self:Transmission( self.Sound.Thousand, 0.1 )
      end
      if tonumber( CLOUDCEIL0100 ) > 0 then
        self.radioqueue:Number2Transmission( CLOUDCEIL0100 )
        self:Transmission( self.Sound.Hundred, 0.1 )
      end
      if self.metric then
        self:Transmission( self.Sound.Meters, 0.1 )
      else
        self:Transmission( self.Sound.Feet, 0.1 )
      end
    end
  end

  alltext = alltext .. ";\n" .. subtitle
  subtitle = ""
  -- Temperature
  local temptext = self.gettext:GetEntry("TEMPERATURE",self.locale)
  if self.TDegF then
    if temperature < 0 then
      --subtitle = string.format( "Temperature -%s °F", TEMPERATURE )
      subtitle = string.format( "%s -%s °F", temptext, TEMPERATURE )
    else
      --subtitle = string.format( "Temperature %s °F", TEMPERATURE )
      subtitle = string.format( "%s %s °F", temptext, TEMPERATURE )
    end
  else
    if temperature < 0 then
      --subtitle = string.format( "Temperature -%s °C", TEMPERATURE )
      subtitle = string.format( "%s -%s °C", temptext, TEMPERATURE )
    else
      --subtitle = string.format( "Temperature %s °C", TEMPERATURE )
      subtitle = string.format( "%s %s °C", temptext, TEMPERATURE )
    end
  end
  local _TEMPERATURE = subtitle
  if not self.useSRS then
    self:Transmission( self.Sound.Temperature, 1.0, subtitle )
    if temperature < 0 then
      self:Transmission( self.Sound.Minus, 0.2 )
    end
    self.radioqueue:Number2Transmission( TEMPERATURE )
    if self.TDegF then
      self:Transmission( self.Sound.DegreesFahrenheit, 0.2 )
    else
      self:Transmission( self.Sound.DegreesCelsius, 0.2 )
    end
  end
  alltext = alltext .. ";\n" .. subtitle

  -- Dew point
  local dewtext = self.gettext:GetEntry("DEWPOINT",self.locale)
  if self.TDegF then
    if dewpoint < 0 then
      --subtitle = string.format( "Dew point -%s °F", DEWPOINT )
      subtitle = string.format( "%s -%s °F", dewtext, DEWPOINT )
    else
      --subtitle = string.format( "Dew point %s °F", DEWPOINT )
      subtitle = string.format( "%s %s °F", dewtext, DEWPOINT )
    end
  else
    if dewpoint < 0 then
      --subtitle = string.format( "Dew point -%s °C", DEWPOINT )
      subtitle = string.format( "%s -%s °C", dewtext, DEWPOINT )
    else
      --subtitle = string.format( "Dew point %s °C", DEWPOINT )
      subtitle = string.format( "%s %s °C", dewtext, DEWPOINT )
    end
  end
  local _DEWPOINT = subtitle
  if not self.useSRS then
    self:Transmission( self.Sound.DewPoint, 1.0, subtitle )
    if dewpoint < 0 then
      self:Transmission( self.Sound.Minus, 0.2 )
    end
    self.radioqueue:Number2Transmission( DEWPOINT )
    if self.TDegF then
      self:Transmission( self.Sound.DegreesFahrenheit, 0.2 )
    else
      self:Transmission( self.Sound.DegreesCelsius, 0.2 )
    end
  end
  alltext = alltext .. ";\n" .. subtitle

  -- Altimeter QNH/QFE.
  local altim = self.gettext:GetEntry("ALTIMETER",self.locale)
  if self.PmmHg then
    if self.qnhonly then
      --subtitle = string.format( "Altimeter %s.%s mmHg", QNH[1], QNH[2] )
      subtitle = string.format( "%s %s.%s mmHg", altim, QNH[1], QNH[2] )
    else
      --subtitle = string.format( "Altimeter: QNH %s.%s, QFE %s.%s mmHg", QNH[1], QNH[2], QFE[1], QFE[2] )
      subtitle = string.format( "%s: QNH %s.%s, QFE %s.%s mmHg", altim, QNH[1], QNH[2], QFE[1], QFE[2] )
    end
  else
    if self.metric then
      if self.qnhonly then
        --subtitle = string.format( "Altimeter %s.%s hPa", QNH[1], QNH[2] )
        subtitle = string.format( "%s %s.%s hPa", altim, QNH[1], QNH[2] )
      else
        --subtitle = string.format( "Altimeter: QNH %s.%s, QFE %s.%s hPa", QNH[1], QNH[2], QFE[1], QFE[2] )
        subtitle = string.format( "%s: QNH %s.%s, QFE %s.%s hPa", altim, QNH[1], QNH[2], QFE[1], QFE[2] )
      end
    else
      if self.qnhonly then
        --subtitle = string.format( "Altimeter %s.%s inHg", QNH[1], QNH[2] )
        subtitle = string.format( "%s %s.%s inHg", altim, QNH[1], QNH[2] )
      else
        --subtitle = string.format( "Altimeter: QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2] )
        subtitle = string.format( "%s: QNH %s.%s, QFE %s.%s inHg", altim, QNH[1], QNH[2], QFE[1], QFE[2] )
      end
    end
  end

  if self.ReportmBar and not self.metric then
    if self.qnhonly then
      --subtitle = string.format( "%s;\nAltimeter %d hPa", subtitle, mBarqnh )
      subtitle = string.format( "%s;\n%s %d hPa", subtitle, altim, mBarqnh )
    else
      --subtitle = string.format( "%s;\nAltimeter: QNH %d, QFE %d hPa", subtitle, mBarqnh, mBarqfe)
      subtitle = string.format( "%s;\n%s: QNH %d, QFE %d hPa", subtitle, altim, mBarqnh, mBarqfe)
    end
  end

  local _ALTIMETER = subtitle
  if not self.useSRS then
    self:Transmission( self.Sound.Altimeter, 1.0, subtitle )
    if not self.qnhonly then
      self:Transmission( self.Sound.QNH, 0.5 )
    end
    self.radioqueue:Number2Transmission( QNH[1] )

    if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then
      self:Transmission( self.Sound.Decimal, 0.2 )
    end
    self.radioqueue:Number2Transmission( QNH[2] )

    if not self.qnhonly then
      self:Transmission( self.Sound.QFE, 0.75 )
      self.radioqueue:Number2Transmission( QFE[1] )
      if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then
        self:Transmission( self.Sound.Decimal, 0.2 )
      end
      self.radioqueue:Number2Transmission( QFE[2] )
    end

    if self.PmmHg then
      self:Transmission( self.Sound.MillimetersOfMercury, 0.1 )
    else
      if self.metric then
        self:Transmission( self.Sound.HectoPascal, 0.1 )
      else
        self:Transmission( self.Sound.InchesOfMercury, 0.1 )
      end
    end
  end
  alltext = alltext .. ";\n" .. subtitle

  local _RUNACT

  if not self.ATISforFARPs then
    -- Active runway.
    local subtitle = ""
    if runwayLanding and runwayLanding ~= runwayTakeoff then

      local actrun = self.gettext:GetEntry("ACTIVELANDING",self.locale)

      subtitle=string.format("%s %s", actrun, runwayLanding)
      if rwyLandingLeft==true then
        subtitle=subtitle.." "..self.gettext:GetEntry("LEFT",self.locale)
      elseif rwyLandingLeft==false then
        subtitle=subtitle.." "..self.gettext:GetEntry("RIGHT",self.locale)
      end
      alltext = alltext .. ";\n" .. subtitle

      if not self.useSRS then
        self:Transmission(self.Sound.ActiveRunwayArrival, 1.0, subtitle)
        self.radioqueue:Number2Transmission(runwayLanding)
        if rwyLandingLeft==true then
          self:Transmission(self.Sound.Left, 0.2)
        elseif rwyLandingLeft==false then
          self:Transmission(self.Sound.Right, 0.2)
        end
      end

    end

    if runwayTakeoff then

      local actrun = self.gettext:GetEntry("ACTIVERUN",self.locale)

      subtitle=string.format("%s %s", actrun, runwayTakeoff)
      if rwyTakeoffLeft==true then
        subtitle=subtitle.." "..self.gettext:GetEntry("LEFT",self.locale)
      elseif rwyTakeoffLeft==false then
        subtitle=subtitle.." "..self.gettext:GetEntry("RIGHT",self.locale)
      end
      alltext = alltext .. ";\n" .. subtitle

      if not self.useSRS then
        self:Transmission(self.Sound.ActiveRunwayDeparture, 1.0, subtitle)
        self.radioqueue:Number2Transmission(runwayTakeoff)
        if rwyTakeoffLeft==true then
          self:Transmission(self.Sound.Left, 0.2)
        elseif rwyTakeoffLeft==false then
          self:Transmission(self.Sound.Right, 0.2)
        end
      end


    end
    _RUNACT = subtitle
    --alltext = alltext .. ";\n" .. subtitle

    -- Runway length.
    if self.rwylength then

      local runact = self.airbase:GetActiveRunway( self.runwaym2t )
      local length = runact.length
      if not self.metric then
        length = UTILS.MetersToFeet( length )
      end

      -- Length in thousands and hundrets of ft/meters.
      local L1000, L0100 = self:_GetThousandsAndHundreds( length )

      -- Subtitle.
      local rwyl = self.gettext:GetEntry("RWYLENGTH",self.locale)
      local meters = self.gettext:GetEntry("METERS",self.locale)
      local feet = self.gettext:GetEntry("FEET",self.locale)
      --local subtitle = string.format( "Runway length %d", length )
      local subtitle = string.format( "%s %d", rwyl, length )
      if self.metric then
        subtitle = subtitle .. " "..meters
      else
        subtitle = subtitle .. " "..feet
      end

      -- Transmit.
      if not self.useSRS then
        self:Transmission( self.Sound.RunwayLength, 1.0, subtitle )
        if tonumber( L1000 ) > 0 then
          self.radioqueue:Number2Transmission( L1000 )
          self:Transmission( self.Sound.Thousand, 0.1 )
        end
        if tonumber( L0100 ) > 0 then
          self.radioqueue:Number2Transmission( L0100 )
          self:Transmission( self.Sound.Hundred, 0.1 )
        end
        if self.metric then
          self:Transmission( self.Sound.Meters, 0.1 )
        else
          self:Transmission( self.Sound.Feet, 0.1 )
        end
      end
      alltext = alltext .. ";\n" .. subtitle
    end
  end

  -- Airfield elevation
  if self.elevation then

    local elev = self.gettext:GetEntry("ELEVATION",self.locale)
    local meters = self.gettext:GetEntry("METERS",self.locale)
    local feet = self.gettext:GetEntry("FEET",self.locale)

    local elevation = self.airbase:GetHeight()
    if not self.metric then
      elevation = UTILS.MetersToFeet( elevation )
    end

    -- Length in thousands and hundrets of ft/meters.
    local L1000, L0100 = self:_GetThousandsAndHundreds( elevation )

    -- Subtitle.
    --local subtitle = string.format( "Elevation %d", elevation )
    local subtitle = string.format( "%s %d", elev, elevation )
    if self.metric then
      subtitle = subtitle .. " "..meters
    else
      subtitle = subtitle .. " "..feet
    end

    -- Transmit.
    if not self.useSRS then
      self:Transmission( self.Sound.Elevation, 1.0, subtitle )
      if tonumber( L1000 ) > 0 then
        self.radioqueue:Number2Transmission( L1000 )
        self:Transmission( self.Sound.Thousand, 0.1 )
      end
      if tonumber( L0100 ) > 0 then
        self.radioqueue:Number2Transmission( L0100 )
        self:Transmission( self.Sound.Hundred, 0.1 )
      end
      if self.metric then
        self:Transmission( self.Sound.Meters, 0.1 )
      else
        self:Transmission( self.Sound.Feet, 0.1 )
      end
    end
    alltext = alltext .. ";\n" .. subtitle
  end

  -- Tower frequency.
  if self.towerfrequency then
    local freqs = ""
    for i, freq in pairs( self.towerfrequency ) do
      freqs = freqs .. string.format( "%.3f MHz", freq )
      if i < #self.towerfrequency then
        freqs = freqs .. ", "
      end
    end
    local twrfrq = self.gettext:GetEntry("TOWERFREQ",self.locale)
    --subtitle = string.format( "Tower frequency %s", freqs )
    subtitle = string.format( "%s %s", twrfrq, freqs )
    if not self.useSRS then
      self:Transmission( self.Sound.TowerFrequency, 1.0, subtitle )
      for _, freq in pairs( self.towerfrequency ) do
        local f = string.format( "%.3f", freq )
        f = UTILS.Split( f, "." )
        self.radioqueue:Number2Transmission( f[1], nil, 0.5 )
        if tonumber( f[2] ) > 0 then
          self:Transmission( self.Sound.Decimal, 0.2 )
          self.radioqueue:Number2Transmission( f[2] )
        end
        self:Transmission( self.Sound.MegaHertz, 0.2 )
      end
    end
    alltext = alltext .. ";\n" .. subtitle
  end

  -- ILS
  local ils=self:GetNavPoint(self.ils, runwayLanding, rwyLandingLeft)
  if ils then
    local ilstxt = self.gettext:GetEntry("ILSFREQ",self.locale)
    --subtitle = string.format( "ILS frequency %.2f MHz", ils.frequency )
    subtitle = string.format( "%s %.2f MHz", ilstxt, ils.frequency )
    if not self.useSRS then
      self:Transmission( self.Sound.ILSFrequency, 1.0, subtitle )
      local f = string.format( "%.2f", ils.frequency )
      f = UTILS.Split( f, "." )
      self.radioqueue:Number2Transmission( f[1], nil, 0.5 )
      if tonumber( f[2] ) > 0 then
        self:Transmission( self.Sound.Decimal, 0.2 )
        self.radioqueue:Number2Transmission( f[2] )
      end
      self:Transmission( self.Sound.MegaHertz, 0.2 )
    end
    alltext = alltext .. ";\n" .. subtitle
  end

  -- Outer NDB
  local ndb=self:GetNavPoint(self.ndbouter, runwayLanding, rwyLandingLeft)
  if ndb then
    local ndbtxt = self.gettext:GetEntry("OUTERNDB",self.locale)
    --subtitle = string.format( "Outer NDB frequency %.2f MHz", ndb.frequency )
    subtitle = string.format( "%s %.2f MHz", ndbtxt, ndb.frequency )
    if not self.useSRS then
      self:Transmission( self.Sound.OuterNDBFrequency, 1.0, subtitle )
      local f = string.format( "%.2f", ndb.frequency )
      f = UTILS.Split( f, "." )
      self.radioqueue:Number2Transmission( f[1], nil, 0.5 )
      if tonumber( f[2] ) > 0 then
        self:Transmission( self.Sound.Decimal, 0.2 )
        self.radioqueue:Number2Transmission( f[2] )
      end
      self:Transmission( self.Sound.MegaHertz, 0.2 )
    end
    alltext = alltext .. ";\n" .. subtitle
  end

  -- Inner NDB
  local ndb=self:GetNavPoint(self.ndbinner, runwayLanding, rwyLandingLeft)
  if ndb then
    local ndbtxt = self.gettext:GetEntry("INNERNDB",self.locale)
    --subtitle = string.format( "Inner NDB frequency %.2f MHz", ndb.frequency )
    subtitle = string.format( "%s %.2f MHz", ndbtxt, ndb.frequency )
    if not self.useSRS then
      self:Transmission( self.Sound.InnerNDBFrequency, 1.0, subtitle )
      local f = string.format( "%.2f", ndb.frequency )
      f = UTILS.Split( f, "." )
      self.radioqueue:Number2Transmission( f[1], nil, 0.5 )
      if tonumber( f[2] ) > 0 then
        self:Transmission( self.Sound.Decimal, 0.2 )
        self.radioqueue:Number2Transmission( f[2] )
      end
      self:Transmission( self.Sound.MegaHertz, 0.2 )
    end
    alltext = alltext .. ";\n" .. subtitle
  end

  -- VOR
  if self.vor then
    local vortxt = self.gettext:GetEntry("VORFREQ",self.locale)
    local vorttstxt = self.gettext:GetEntry("VORFREQTTS",self.locale)
    --subtitle = string.format( "VOR frequency %.2f MHz", self.vor )
    subtitle = string.format( "%s %.2f MHz", vortxt, self.vor )
    if self.useSRS then
      --subtitle = string.format( "V O R frequency %.2f MHz", self.vor )
      subtitle = string.format( "%s %.2f MHz", vorttstxt, self.vor )
    end
    if not self.useSRS then
      self:Transmission( self.Sound.VORFrequency, 1.0, subtitle )
      local f = string.format( "%.2f", self.vor )
      f = UTILS.Split( f, "." )
      self.radioqueue:Number2Transmission( f[1], nil, 0.5 )
      if tonumber( f[2] ) > 0 then
        self:Transmission( self.Sound.Decimal, 0.2 )
        self.radioqueue:Number2Transmission( f[2] )
      end
      self:Transmission( self.Sound.MegaHertz, 0.2 )
    end
    alltext = alltext .. ";\n" .. subtitle
  end

  -- TACAN
  if self.tacan then
    local tactxt = self.gettext:GetEntry("TACANCH",self.locale)
    --subtitle=string.format("TACAN channel %dX Ray", self.tacan)
    subtitle=string.format(tactxt, self.tacan)
    if not self.useSRS then
      self:Transmission( self.Sound.TACANChannel, 1.0, subtitle )
      self.radioqueue:Number2Transmission( tostring( self.tacan ), nil, 0.2 )
      self.radioqueue:NewTransmission( "Xray.ogg", 0.75, self.soundpathNato, nil, 0.2 )
    end
    alltext = alltext .. ";\n" .. subtitle
  end

  -- RSBN
  if self.rsbn then
    local rsbntxt = self.gettext:GetEntry("RSBNCH",self.locale)
    --subtitle = string.format( "RSBN channel %d", self.rsbn )
    subtitle = string.format( "%s %d", rsbntxt, self.rsbn )
    if not self.useSRS then
      self:Transmission( self.Sound.RSBNChannel, 1.0, subtitle )
      self.radioqueue:Number2Transmission( tostring( self.rsbn ), nil, 0.2 )
    end
    alltext = alltext .. ";\n" .. subtitle
  end

  -- PRMG
  local ndb=self:GetNavPoint(self.prmg, runwayLanding, rwyLandingLeft)
  if ndb then
    local prmtxt = self.gettext:GetEntry("PRMGCH",self.locale)
    --subtitle = string.format( "PRMG channel %d", ndb.frequency )
    subtitle = string.format( "%s %d", prmtxt, ndb.frequency )
    if not self.useSRS then
      self:Transmission( self.Sound.PRMGChannel, 1.0, subtitle )
      self.radioqueue:Number2Transmission( tostring( ndb.frequency ), nil, 0.5 )
    end
    alltext = alltext .. ";\n" .. subtitle
  end

  -- additional info, if any
  if self.useSRS and self.AdditionalInformation then
    alltext = alltext .. ";\n"..self.AdditionalInformation
  end

  -- Advice on initial...
  local advtxt = self.gettext:GetEntry("ADVISE",self.locale)
  --subtitle = string.format( "Advise on initial contact, you have information %s", NATO )
  subtitle = string.format( "%s %s", advtxt, NATO )
  if not self.useSRS then
    self:Transmission( self.Sound.AdviceOnInitial, 0.5, subtitle )
    self.radioqueue:NewTransmission( string.format( "%s.ogg", NATO ), 0.75, self.soundpathNato )
  end
  alltext = alltext .. ";\n" .. subtitle

  -- Report ATIS text.
  self:Report( alltext )

  -- Update F10 marker.
  if self.usemarker then
    self:UpdateMarker( _INFORMATION, _RUNACT, _WIND, _ALTIMETER, _TEMPERATURE )
  end

end

--- Text report of ATIS information. Information delimitor is a semicolon ";" and a line break "\n".
-- @param #ATIS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string Text Report text.
function ATIS:onafterReport( From, Event, To, Text )
  self:T({From, Event, To})
  self:T( self.lid .. string.format( "Report:\n%s", Text ) )

  if self.useSRS and self.msrs then

    -- Remove line breaks
    local text = string.gsub( Text, "[\r\n]", "" )

    -- Replace other stuff.
    local statute = self.gettext:GetEntry("STATUTE",self.locale)
    local degc = self.gettext:GetEntry("DEGREES",self.locale)
    local degf = self.gettext:GetEntry("FAHRENHEIT",self.locale)
    local inhg = self.gettext:GetEntry("INCHHG",self.locale)
    local mmhg = self.gettext:GetEntry("MMHG",self.locale)
    local hpa = self.gettext:GetEntry("HECTO",self.locale)
    local emes = self.gettext:GetEntry("METERSPER",self.locale)
    local tacan = self.gettext:GetEntry("TACAN",self.locale)
    local farp = self.gettext:GetEntry("FARP",self.locale)


    local text = string.gsub( text, "SM", statute )
    text = string.gsub( text, "°C", degc )
    text = string.gsub( text, "°F", degf )
    text = string.gsub( text, "inHg", inhg )
    text = string.gsub( text, "mmHg", mmhg )
    text = string.gsub( text, "hPa", hpa )
    text = string.gsub( text, "m/s", emes )
    text = string.gsub( text, "TACAN", tacan )
    text = string.gsub( text, "FARP", farp )

    local delimiter = self.gettext:GetEntry("DELIMITER",self.locale)

    if string.lower(self.locale) ~= "en" then
      text = string.gsub(text,"(%d+)(%.)(%d+)","%1 "..delimiter.." %3")
    end

    -- Replace ";" by "."
    local text = string.gsub( text, ";", " . " )

    -- Debug output.
    self:T( "SRS TTS: " .. text )

    -- Play text-to-speech report.
    local duration = MSRS.getSpeechTime(text,0.95)
    self.msrsQ:NewTransmission(text,duration,self.msrs,nil,2)
    --self.msrs:PlayText( text )
    self.SRSText = text

  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Base captured
-- @param #ATIS self
-- @param Core.Event#EVENTDATA EventData Event data.
function ATIS:OnEventBaseCaptured( EventData )

  if EventData and EventData.Place then

    -- Place is the airbase that was captured.
    local airbase = EventData.Place -- Wrapper.Airbase#AIRBASE

    -- Check that this airbase belongs or did belong to this warehouse.
    if EventData.PlaceName == self.airbasename then

      -- New coalition of airbase after it was captured.
      local NewCoalitionAirbase = airbase:GetCoalition()

      if self.useSRS and self.msrs and self.msrs.coalition ~= NewCoalitionAirbase then
        self.msrs:SetCoalition( NewCoalitionAirbase )
      end

    end

  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Update F10 map marker.
-- @param #ATIS self
-- @param #string information Information tag text.
-- @param #string runact Active runway text.
-- @param #string wind Wind text.
-- @param #string altimeter Altimeter text.
-- @param #string temperature Temperature text.
-- @return #number Marker ID.
function ATIS:UpdateMarker( information, runact, wind, altimeter, temperature )

  if self.markerid then
    self.airbase:GetCoordinate():RemoveMark( self.markerid )
  end
  local text = ""
  if type(self.frequency) == "table" then
    local frequency = table.concat(self.frequency,"/")
    local modulation = self.modulation
    if type(modulation) == "table" then
      modulation = table.concat(self.modulation,"/")
    end
    text = string.format( "ATIS on %s %s, %s:\n", tostring(frequency), tostring(modulation), tostring( information ) )
  else
    text = string.format( "ATIS on %.3f %s, %s:\n", self.frequency, UTILS.GetModulationName( self.modulation ), tostring( information ) )
  end
  text = text .. string.format( "%s\n", tostring( runact ) )
  text = text .. string.format( "%s\n", tostring( wind ) )
  text = text .. string.format( "%s\n", tostring( altimeter ) )
  text = text .. string.format( "%s", tostring( temperature ) )
  -- More info is not displayed on the marker!

  -- Place new mark
  self.markerid = self.airbase:GetCoordinate():MarkToAll( text, true )

  return self.markerid
end

--- Get active runway runway.
-- @param #ATIS self
-- @param #boolean Takeoff If `true`, get runway for takeoff. Default is for landing.
-- @return #string Active runway, e.g. "31" for 310 deg.
-- @return #boolean Use Left=true, Right=false, or nil.
function ATIS:GetActiveRunway(Takeoff)

  local runway=nil --Wrapper.Airbase#AIRBASE.Runway
  if Takeoff then
    runway=self.airbase:GetActiveRunwayTakeoff()
  else
    runway=self.airbase:GetActiveRunwayLanding()
  end
  if runway then -- some ABs have NO runways, e.g. Syria Naqoura
    return runway.name, runway.isLeft
  else
    return nil, nil
  end
end

--- Get runway from user supplied magnetic heading.
-- @param #ATIS self
-- @param #number windfrom Wind direction (from) in degrees.
-- @return #string Runway magnetic heading divided by ten (and rounded). Eg, "13" for 130°.
function ATIS:GetMagneticRunway( windfrom )

  local diffmin = nil
  local runway = nil
  for _, heading in pairs( self.runwaymag ) do

    local hdg = self:GetRunwayWithoutLR( heading )

    local diff = UTILS.HdgDiff( windfrom, tonumber( hdg ) * 10 )
    if diffmin == nil or diff < diffmin then
      diffmin = diff
      runway = hdg
    end

  end

  return runway
end

--- Get nav aid data.
-- @param #ATIS self
-- @param #table navpoints Nav points data table.
-- @param #string runway (Active) runway, *e.g.* "31".
-- @param #boolean left If *true*, left runway, if *false, right, else does not matter.
-- @return #ATIS.NavPoint Nav point data table.
function ATIS:GetNavPoint( navpoints, runway, left )

  -- Loop over all defined nav aids.
  for _, _nav in pairs( navpoints or {} ) do
    local nav = _nav -- #ATIS.NavPoint

    if nav.runway == nil then
      -- No explicit runway data specified ==> data is valid for all runways.
      return nav
    else

      local navy = tonumber( self:GetRunwayWithoutLR( nav.runway ) ) * 10
      local rwyy = tonumber( self:GetRunwayWithoutLR( runway ) ) * 10

      local navL = self:GetRunwayLR( nav.runway )
      local hdgD = UTILS.HdgDiff( navy, rwyy )

      if hdgD <= 15 then -- We allow an error of +-15° here.
        if navL == nil or (navL == true and left == true) or (navL == false and left == false) then
          return nav
      end
      end
    end
  end

  return nil
end

--- Get runway heading without left or right info.
-- @param #ATIS self
-- @param #string runway Runway heading, *e.g.* "31L".
-- @return #string Runway heading without left or right, *e.g.* "31".
function ATIS:GetRunwayWithoutLR( runway )
  local rwywo = runway:gsub( "%D+", "" )
  -- self:T(string.format("FF runway=%s ==> rwywo=%s", runway, rwywo))
  return rwywo
end

--- Get info if left or right runway is active.
-- @param #ATIS self
-- @param #string runway Runway heading, *e.g.* "31L".
-- @return #boolean If *true*, left runway is active. If *false*, right runway. If *nil*, neither applies.
function ATIS:GetRunwayLR( runway )

  -- Get left/right if specified.
  local rwyL = runway:lower():find( "l" )
  local rwyR = runway:lower():find( "r" )

  if rwyL then
    return true
  elseif rwyR then
    return false
  else
    return nil
  end

end

--- Transmission via RADIOQUEUE.
-- @param #ATIS self
-- @param #ATIS.Soundfile sound ATIS sound object.
-- @param #number interval Interval in seconds after the last transmission finished.
-- @param #string subtitle Subtitle of the transmission.
-- @param #string path Path to sound file. Default `self.soundpath`.
function ATIS:Transmission( sound, interval, subtitle, path )
  self.radioqueue:NewTransmission( sound.filename, sound.duration, path or self.soundpath, nil, interval, subtitle, self.subduration )
end

--- Play all audio files.
-- @param #ATIS self
function ATIS:SoundCheck()

  for _, _sound in pairs( ATIS.Sound ) do
    local sound = _sound -- #ATIS.Soundfile
    local subtitle = string.format( "Playing sound file %s, duration %.2f sec", sound.filename, sound.duration )
    self:Transmission( sound, nil, subtitle )
    MESSAGE:New( subtitle, 5, "ATIS" ):ToAll()
  end

end

--- Get weather of this mission from env.mission.weather variable.
-- @param #ATIS self
-- @return #table Clouds table which has entries "thickness", "density", "base", "iprecptns".
-- @return #number Visibility distance in meters.
-- @return #number Ground turbulence in m/s.
-- @return #table Fog table, which has entries "thickness", "visibility" or nil if fog is disabled in the mission.
-- @return #number Dust density or nil if dust is disabled in the mission.
-- @return #boolean static If true, static weather is used. If false, dynamic weather is used.
function ATIS:GetMissionWeather()

  -- Weather data from mission file.
  local weather = env.mission.weather

  -- Clouds
  --[[
  ["clouds"] =
  {
      ["thickness"] = 430,
      ["density"] = 7,
      ["base"] = 0,
      ["iprecptns"] = 1,
  }, -- end of ["clouds"]
  ]]
  local clouds = weather.clouds

  -- 0=static, 1=dynamic
  local static = weather.atmosphere_type == 0

  -- Visibilty distance in meters.
  local visibility = weather.visibility.distance

  -- Ground turbulence.
  local turbulence = weather.groundTurbulence

  -- Dust
  --[[
  ["enable_dust"] = false,
  ["dust_density"] = 0,
  ]]
  local dust = nil
  if weather.enable_dust == true then
    dust = weather.dust_density
  end

  self:T( "FF weather:" )
  self:T( { clouds = clouds } )
  self:T( { visibility = visibility } )
  self:T( { turbulence = turbulence } )
  self:T( { dust = dust } )
  self:T( { static = static } )
  return clouds, visibility, turbulence, dust, static
end

--- Get thousands of a number.
-- @param #ATIS self
-- @param #number n Number, *e.g.* 4359.
-- @return #string Thousands of n, *e.g.* "4" for 4359.
-- @return #string Hundreds of n, *e.g.* "4" for 4359 because its rounded.
function ATIS:_GetThousandsAndHundreds( n )

  local N = UTILS.Round( n / 1000, 1 )

  local S = UTILS.Split( string.format( "%.1f", N ), "." )

  local t = S[1]
  local h = S[2]

  return t, h
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Combat Troops & Logistics Department.
--
-- ===
-- 
-- **CTLD** - MOOSE based Helicopter CTLD Operations.
-- 
-- ===
-- 
-- ## Missions:
--
-- ### [CTLD - Combat Troop & Logistics Deployment](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/CTLD)
-- 
-- ===
-- 
-- **Main Features:**
--
--    * MOOSE-based Helicopter CTLD Operations for Players.
--
-- ===
--
-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing), bbirchnz (additional code!!)
-- ### Repack addition for crates: **Raiden**
-- ### Additional cool features: **Lekaa**
-- 
-- @module Ops.CTLD
-- @image OPS_CTLD.jpg

-- Last Update Oct 2025


------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO CTLD_CARGO
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

do 

------------------------------------------------------
--- **CTLD_CARGO** class, extends Core.Base#BASE
-- @type CTLD_CARGO
-- @field #string ClassName Class name.
-- @field #number ID ID of this cargo.
-- @field #string Name Name for menu.
-- @field #table Templates Table of #POSITIONABLE objects.
-- @field #string CargoType Enumerator of Type.
-- @field #boolean HasBeenMoved Flag for moving.
-- @field #boolean LoadDirectly Flag for direct loading.
-- @field #number CratesNeeded Crates needed to build.
-- @field Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission.
-- @field #boolean HasBeenDropped True if dropped from heli.
-- @field #number PerCrateMass Mass in kg.
-- @field #number Stock Number of builds available, -1 for unlimited.
-- @field #string Subcategory Sub-category name.
-- @field #boolean DontShowInMenu Show this item in menu or not.
-- @field Core.Zone#ZONE Location Location (if set) where to get this cargo item.
-- @field #table ResourceMap Resource Map information table if it has been set for static cargo items.
-- @field #string StaticShape Individual shape if set.
-- @field #string StaticType Individual type if set.
-- @field #string StaticCategory Individual static category if set.
-- @field #list<#string> TypeNames Table of unit types able to pick this cargo up.
-- @field #number Stock0 Initial stock, if any given.
-- @extends Core.Base#BASE

---
-- @field #CTLD_CARGO CTLD_CARGO
CTLD_CARGO = {
  ClassName = "CTLD_CARGO",
  ID = 0,
  Name = "none",
  Templates = {},
  CargoType = "none",
  HasBeenMoved = false,
  LoadDirectly = false,
  CratesNeeded = 0,
  Positionable = nil,
  HasBeenDropped = false,
  PerCrateMass = 0,
  Stock = nil,
  Stock0 = nil,
  Mark = nil,
  DontShowInMenu = false,
  Location = nil,
  }
  
  --- Define cargo types.
  -- @type CTLD_CARGO.Enum
  -- @field #string VEHICLE
  -- @field #string TROOPS
  -- @field #string FOB
  -- @field #string CRATE
  -- @field #string REPAIR
  -- @field #string ENGINEERS
  -- @field #string STATIC
  -- @field #string GCLOADABLE
  CTLD_CARGO.Enum = {
    VEHICLE = "Vehicle", -- #string vehicles
    TROOPS = "Troops", -- #string troops
    FOB = "FOB", -- #string FOB
    CRATE = "Crate", -- #string crate
    REPAIR = "Repair", -- #string repair
    ENGINEERS = "Engineers", -- #string engineers
    STATIC = "Static", -- #string statics
    GCLOADABLE = "GC_Loadable", -- #string dynamiccargo
  }
  
  --- Function to create new CTLD_CARGO object.
  -- @param #CTLD_CARGO self
  -- @param #number ID ID of this #CTLD_CARGO
  -- @param #string Name Name for menu.
  -- @param #table Templates Table of #POSITIONABLE objects.
  -- @param #CTLD_CARGO.Enum Sorte Enumerator of Type.
  -- @param #boolean HasBeenMoved Flag for moving.
  -- @param #boolean LoadDirectly Flag for direct loading.
  -- @param #number CratesNeeded Crates needed to build.
  -- @param Wrapper.Positionable#POSITIONABLE Positionable Representation of cargo in the mission.
  -- @param #boolean Dropped Cargo/Troops have been unloaded from a chopper.
  -- @param #number PerCrateMass Mass in kg
  -- @param #number Stock Number of builds available, nil for unlimited
  -- @param #string Subcategory Name of subcategory, handy if using > 10 types to load.
  -- @param #boolean DontShowInMenu Show this item in menu or not (default: false == show it).
  -- @param Core.Zone#ZONE Location (optional) Where the cargo is available (one location only).
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock, Subcategory, DontShowInMenu, Location)
    -- Inherit everything from BASE class.
    local self=BASE:Inherit(self, BASE:New()) -- #CTLD_CARGO
    self:T({ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped})
    self.ID = ID or math.random(100000,1000000)
    self.Name = Name or "none" -- #string
    self.Templates = Templates or {} -- #table
    self.CargoType = Sorte or "type" -- #CTLD_CARGO.Enum
    self.HasBeenMoved = HasBeenMoved or false -- #boolean
    self.LoadDirectly = LoadDirectly or false -- #boolean
    self.CratesNeeded = CratesNeeded or 0 -- #number
    self.Positionable = Positionable or nil -- Wrapper.Positionable#POSITIONABLE
    self.HasBeenDropped = Dropped or false --#boolean
    self.PerCrateMass = PerCrateMass or 0 -- #number
    self.Stock = Stock or nil --#number
    self.Stock0 = Stock or nil --#number 
    self.Mark = nil
    self.Subcategory = Subcategory or "Other"
    self.DontShowInMenu = DontShowInMenu or false
    self.ResourceMap = nil
    self.StaticType = "container_cargo" -- "container_cargo"
    self.StaticShape = nil
    self.TypeNames = nil
    self.StaticCategory = "Cargos"
    if type(Location) == "string" then
      Location = ZONE:New(Location)
    end
    self.Location = Location
    return self
  end
  
  --- Add specific static type and shape to this CARGO.
  -- @param #CTLD_CARGO self
  -- @param #string TypeName
  -- @param #string ShapeName
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:SetStaticTypeAndShape(Category,TypeName,ShapeName)
    self.StaticCategory = Category or "Cargos"
    self.StaticType = TypeName or "container_cargo"
    self.StaticShape = ShapeName
    return self
  end
  
  --- Get the specific static type and shape from this CARGO if set.
  -- @param #CTLD_CARGO self
  -- @return #string Category
  -- @return #string TypeName
  -- @return #string ShapeName
  function CTLD_CARGO:GetStaticTypeAndShape()
    return self.StaticCategory, self.StaticType, self.StaticShape
  end
  
  --- Add specific unit types to this CARGO (restrict what types can pick this up).
  -- @param #CTLD_CARGO self
  -- @param #string UnitTypes Unit type name, can also be a #list<#string> table of unit type names.
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:AddUnitTypeName(UnitTypes)
    if not self.TypeNames then self.TypeNames = {} end
    if type(UnitTypes) ~= "table" then UnitTypes = {UnitTypes} end
    for _,_singletype in pairs(UnitTypes or {}) do
      self.TypeNames[_singletype]=_singletype
    end
    return self
  end
  
  --- Check if a specific unit can carry this CARGO (restrict what types can pick this up).
  -- @param #CTLD_CARGO self
  -- @param Wrapper.Unit#UNIT Unit
  -- @return #boolean Outcome
  function CTLD_CARGO:UnitCanCarry(Unit)
    if self.TypeNames == nil then return true end
    local typename = Unit:GetTypeName() or "none"
    if self.TypeNames[typename] then
      return true
    else
      return false
    end
  end
  
  --- Add Resource Map information table
  -- @param #CTLD_CARGO self
  -- @param #table ResourceMap
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:SetStaticResourceMap(ResourceMap)
    self.ResourceMap = ResourceMap
    return self
  end
  
  --- Get Resource Map information table
  -- @param #CTLD_CARGO self
  -- @return #table ResourceMap
  function CTLD_CARGO:GetStaticResourceMap()
    return self.ResourceMap
  end
  
  --- Query Location.
  -- @param #CTLD_CARGO self
  -- @return Core.Zone#ZONE location or `nil` if not set
  function CTLD_CARGO:GetLocation()
    return self.Location
  end
  
  --- Query ID.
  -- @param #CTLD_CARGO self
  -- @return #number ID
  function CTLD_CARGO:GetID()
    return self.ID
  end
  
  --- Query Subcategory
  -- @param #CTLD_CARGO self
  -- @return #string SubCategory
  function CTLD_CARGO:GetSubCat()
    return self.Subcategory
  end
  
  --- Query Mass.
  -- @param #CTLD_CARGO self
  -- @return #number Mass in kg
  function CTLD_CARGO:GetMass()
    return self.PerCrateMass
  end  
  
  --- Query Name.
  -- @param #CTLD_CARGO self
  -- @return #string Name
  function CTLD_CARGO:GetName()
    return self.Name
  end
  
  --- Query Templates.
  -- @param #CTLD_CARGO self
  -- @return #table Templates
  function CTLD_CARGO:GetTemplates()
    return self.Templates
  end
  
  --- Query has moved.
  -- @param #CTLD_CARGO self
  -- @return #boolean Has moved
  function CTLD_CARGO:HasMoved()
    return self.HasBeenMoved
  end
  
  --- Query was dropped.
  -- @param #CTLD_CARGO self
  -- @return #boolean Has been dropped.
  function CTLD_CARGO:WasDropped()
    return self.HasBeenDropped
  end
  
  --- Query directly loadable.
  -- @param #CTLD_CARGO self
  -- @return #boolean loadable
  function CTLD_CARGO:CanLoadDirectly()
    return self.LoadDirectly
  end
  
  --- Query number of crates or troopsize.
  -- @param #CTLD_CARGO self
  -- @return #number Crates or size of troops.
  function CTLD_CARGO:GetCratesNeeded()
    return self.CratesNeeded
  end
  
  --- Query type.
  -- @param #CTLD_CARGO self
  -- @return #CTLD_CARGO.Enum Type
  function CTLD_CARGO:GetType()
    return self.CargoType
  end
  
  --- Query type.
  -- @param #CTLD_CARGO self
  -- @return Wrapper.Positionable#POSITIONABLE Positionable
  function CTLD_CARGO:GetPositionable()
    return self.Positionable
  end
  
  --- Set HasMoved.
  -- @param #CTLD_CARGO self
  -- @param #boolean moved
  function CTLD_CARGO:SetHasMoved(moved)
    self.HasBeenMoved = moved or false
  end
  
   --- Query if cargo has been loaded.
  -- @param #CTLD_CARGO self
  -- @param #boolean loaded
  function CTLD_CARGO:Isloaded()
    if self.HasBeenMoved and not self:WasDropped() then
      return true
    else
     return false
    end 
  end
  
  --- Set WasDropped.
  -- @param #CTLD_CARGO self
  -- @param #boolean dropped
  function CTLD_CARGO:SetWasDropped(dropped)
    self.HasBeenDropped = dropped or false
  end
  
  --- Get Stock.
  -- @param #CTLD_CARGO self
  -- @return #number Stock or -1 if unlimited.
  function CTLD_CARGO:GetStock()
    if self.Stock then
      return self.Stock
    else
      return -1
    end
  end
  
  --- Get Stock0.
  -- @param #CTLD_CARGO self
  -- @return #number Stock0 or -1 if unlimited.
  function CTLD_CARGO:GetStock0()
    if self.Stock0 then
      return self.Stock0
    else
      return -1
    end
  end
  
    --- Get relative Stock.
  -- @param #CTLD_CARGO self
  -- @return #number Stock Percentage like 75, or -1 if unlimited.
  function CTLD_CARGO:GetRelativeStock()
    if self.Stock and self.Stock0 then
      return math.floor((self.Stock/self.Stock0)*100)
    else
      return -1
    end
  end
  
  --- Add Stock.
  -- @param #CTLD_CARGO self
  -- @param #number Number to add, none if nil.
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:AddStock(Number)
    if self.Stock then -- Stock nil?
      local number = Number or 1
      self.Stock = self.Stock + number
    end
    return self
  end
  
  --- Remove Stock.
  -- @param #CTLD_CARGO self
  -- @param #number Number to reduce, none if nil.
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:RemoveStock(Number)
    if self.Stock then -- Stock nil?
      local number = Number or 1
      self.Stock = self.Stock - number
      if self.Stock < 0 then self.Stock = 0 end
    end
    return self
  end
  
  --- Set Stock.
  -- @param #CTLD_CARGO self
  -- @param #number Number to set, nil means unlimited.
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:SetStock(Number)
    self.Stock = Number
    return self
  end
  
  --- Query crate type for REPAIR
  -- @param #CTLD_CARGO self
  -- @param #boolean 
  function CTLD_CARGO:IsRepair()
   if self.CargoType == "Repair" then
    return true
   else
    return false
   end
  end
  
  --- Query crate type for STATIC
  -- @param #CTLD_CARGO self
  -- @return #boolean 
  function CTLD_CARGO:IsStatic()
   if self.CargoType == "Static" then
    return true
   else
    return false
   end
  end
  
  --- Add mark
  -- @param #CTLD_CARGO self
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:AddMark(Mark)
    self.Mark = Mark
    return self
  end
  
  --- Get mark
  -- @param #CTLD_CARGO self
  -- @return #string Mark
  function CTLD_CARGO:GetMark(Mark)
    return self.Mark
  end
  
  --- Wipe mark
  -- @param #CTLD_CARGO self
  -- @return #CTLD_CARGO self
  function CTLD_CARGO:WipeMark()
    self.Mark = nil
    return self
  end
  
  --- Get overall mass of a cargo object, i.e. crates needed x mass per crate
  -- @param #CTLD_CARGO self
  -- @return #number mass
  function CTLD_CARGO:GetNetMass()
    return self.CratesNeeded * self.PerCrateMass
  end
   
end

do

------------------------------------------------------
--- **CTLD_ENGINEERING** class, extends Core.Base#BASE
-- @type CTLD_ENGINEERING
-- @field #string ClassName
-- @field #string lid
-- @field #string Name
-- @field Wrapper.Group#GROUP Group
-- @field Wrapper.Unit#UNIT Unit
-- @field Wrapper.Group#GROUP HeliGroup
-- @field Wrapper.Unit#UNIT HeliUnit
-- @field #string State
-- @extends Core.Base#BASE

---
-- @field #CTLD_ENGINEERING CTLD_ENGINEERING
CTLD_ENGINEERING = {
  ClassName = "CTLD_ENGINEERING",
  lid = "",
  Name = "none",
  Group = nil,
  Unit = nil,
  --C_Ops = nil,
  HeliGroup = nil,
  HeliUnit = nil,
  State = "",
  }
  
  --- CTLD_ENGINEERING class version.
  -- @field #string version
  CTLD_ENGINEERING.Version = "0.0.3"
  
  --- Create a new instance.
  -- @param #CTLD_ENGINEERING self
  -- @param #string Name
  -- @param #string GroupName Name of Engineering #GROUP object
  -- @param Wrapper.Group#GROUP HeliGroup HeliGroup
  -- @param Wrapper.Unit#UNIT HeliUnit HeliUnit
  -- @return #CTLD_ENGINEERING self 
  function CTLD_ENGINEERING:New(Name, GroupName, HeliGroup, HeliUnit)
  
      -- Inherit everything from BASE class.
    local self=BASE:Inherit(self, BASE:New()) -- #CTLD_ENGINEERING
    
   --BASE:I({Name, GroupName})
    
    self.Name = Name or "Engineer Squad" -- #string
    self.Group = GROUP:FindByName(GroupName) -- Wrapper.Group#GROUP
    self.Unit = self.Group:GetUnit(1) -- Wrapper.Unit#UNIT
    self.HeliGroup = HeliGroup -- Wrapper.Group#GROUP
    self.HeliUnit = HeliUnit -- Wrapper.Unit#UNIT
    self.currwpt = nil -- Core.Point#COORDINATE
    self.lid = string.format("%s (%s) | ",self.Name, self.Version)
      -- Start State.
    self.State = "Stopped"
    self.marktimer = 300 -- wait this many secs before trying a crate again
    self:Start()
    local parent = self:GetParent(self)
    return self
  end
  
  --- (Internal) Set the status
  -- @param #CTLD_ENGINEERING self
  -- @param #string State
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:SetStatus(State)
    self.State = State
    return self
  end
  
  --- (Internal) Get the status
  -- @param #CTLD_ENGINEERING self
  -- @return #string State
  function CTLD_ENGINEERING:GetStatus()
    return self.State
  end
  
  --- (Internal) Check the status
  -- @param #CTLD_ENGINEERING self
  -- @param #string State
  -- @return #boolean Outcome
  function CTLD_ENGINEERING:IsStatus(State)
    return self.State == State
  end
  
  --- (Internal) Check the negative status
  -- @param #CTLD_ENGINEERING self
  -- @param #string State
  -- @return #boolean Outcome
  function CTLD_ENGINEERING:IsNotStatus(State)
    return self.State ~= State
  end
  
  --- (Internal) Set start status.
  -- @param #CTLD_ENGINEERING self
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Start()
    self:T(self.lid.."Start")
    self:SetStatus("Running")
    return self
  end
  
  --- (Internal) Set stop status.
  -- @param #CTLD_ENGINEERING self
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Stop()
    self:T(self.lid.."Stop")
    self:SetStatus("Stopped")
    return self
  end
  
  --- (Internal) Set build status.
  -- @param #CTLD_ENGINEERING self
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Build()
    self:T(self.lid.."Build")
    self:SetStatus("Building")
    return self
  end
  
  --- (Internal) Set done status.
  -- @param #CTLD_ENGINEERING self
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Done()
    self:T(self.lid.."Done")
    local grp = self.Group -- Wrapper.Group#GROUP
    grp:RelocateGroundRandomInRadius(7,100,false,false,"Diamond")
    self:SetStatus("Running")
    return self
  end
  
  --- (Internal) Search for crates in reach.
  -- @param #CTLD_ENGINEERING self
  -- @param #table crates Table of found crate Ops.CTLD#CTLD_CARGO objects.
  -- @param #number number Number of crates found.
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Search(crates,number)
    self:T(self.lid.."Search")
    self:SetStatus("Searching")
    -- find crates close by
    --local COps = self.C_Ops -- Ops.CTLD#CTLD
    local dist = self.distance -- #number
    local group = self.Group -- Wrapper.Group#GROUP
    --local crates,number = COps:_FindCratesNearby(group,nil, dist) -- #table
    local ctable = {}
    local ind = 0
    if number > 0 then
      -- get set of dropped only
      for _,_cargo in pairs (crates) do
       local cgotype = _cargo:GetType()
       if _cargo:WasDropped() and cgotype ~= CTLD_CARGO.Enum.STATIC then
        local ok = false
        local chalk = _cargo:GetMark()
        if chalk == nil then
          ok = true
        else
         -- have we tried this cargo recently?
         local tag = chalk.tag or "none"
         local timestamp = chalk.timestamp or 0
         -- enough time gone?
         local gone = timer.getAbsTime() - timestamp
         if gone >= self.marktimer then
            ok = true
            _cargo:WipeMark()
         end -- end time check
        end -- end chalk
        if ok then
          local chalk = {}
          chalk.tag = "Engineers"
          chalk.timestamp = timer.getAbsTime()
          _cargo:AddMark(chalk)
          ind = ind + 1
          table.insert(ctable,ind,_cargo)
        end     
       end -- end dropped
      end -- end for
    end -- end number
    
    if ind > 0 then
      local crate = ctable[1] -- Ops.CTLD#CTLD_CARGO
      local static = crate:GetPositionable() -- Wrapper.Static#STATIC
      local crate_pos = static:GetCoordinate() -- Core.Point#COORDINATE
      local gpos = group:GetCoordinate() -- Core.Point#COORDINATE
      -- see how far we are from the crate
      local distance = self:_GetDistance(gpos,crate_pos)
      self:T(string.format("%s Distance to crate: %d", self.lid, distance))
      -- move there
      if distance > 30 and distance ~= -1 and self:IsStatus("Searching") then
        group:RouteGroundTo(crate_pos,15,"Line abreast",1)
        self.currwpt = crate_pos -- Core.Point#COORDINATE
        self:Move()
      elseif distance <= 30 and distance ~= -1 then
        -- arrived
        self:Arrive()
      end
    else
      self:T(self.lid.."No crates in reach!")
    end
    return self
  end
  
  --- (Internal) Move towards crates in reach.
  -- @param #CTLD_ENGINEERING self
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Move()
    self:T(self.lid.."Move")
    self:SetStatus("Moving")
    -- check if we arrived on target
    --local COps = self.C_Ops -- Ops.CTLD#CTLD
    local group = self.Group -- Wrapper.Group#GROUP
    local tgtpos = self.currwpt -- Core.Point#COORDINATE
    local gpos = group:GetCoordinate() -- Core.Point#COORDINATE
    -- see how far we are from the crate
    local distance = self:_GetDistance(gpos,tgtpos)
    self:T(string.format("%s Distance remaining: %d", self.lid, distance))
    if distance <= 30 and distance ~= -1 then
        -- arrived
        self:Arrive()
    end
    return self
  end
  
  --- (Internal) Arrived at crates in reach. Stop group.
  -- @param #CTLD_ENGINEERING self
  -- @return #CTLD_ENGINEERING self
  function CTLD_ENGINEERING:Arrive()
    self:T(self.lid.."Arrive")
    self:SetStatus("Arrived")
    self.currwpt = nil
    local Grp = self.Group -- Wrapper.Group#GROUP
    Grp:RouteStop()
    return self
  end
  
  --- (Internal) Return distance in meters between two coordinates.
  -- @param #CTLD_ENGINEERING self
  -- @param Core.Point#COORDINATE _point1 Coordinate one
  -- @param Core.Point#COORDINATE _point2 Coordinate two
  -- @return #number Distance in meters or -1
  function CTLD_ENGINEERING:_GetDistance(_point1, _point2)
    self:T(self.lid .. " _GetDistance")
    if _point1 and _point2 then
      local distance1 = _point1:Get2DDistance(_point2)
      local distance2 = _point1:DistanceFromPointVec2(_point2)
      if distance1 and type(distance1) == "number" then
        return distance1
      elseif distance2 and type(distance2) == "number" then
        return distance2
      else
        self:E("*****Cannot calculate distance!")
        self:E({_point1,_point2})
        return -1
      end
    else
      self:E("******Cannot calculate distance!")
      self:E({_point1,_point2})
      return -1
    end
  end

end

do
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO CTLD
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------
--- **CTLD** class, extends Core.Base#BASE, Core.Fsm#FSM
-- @type CTLD
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field #number coalition Coalition side number, e.g. `coalition.side.RED`.
-- @field #boolean debug
-- @extends Core.Fsm#FSM

--- *Combat Troop & Logistics Deployment (CTLD): Everyone wants to be a POG, until there\'s POG stuff to be done.* (Mil Saying)
--
-- ===
--
-- ![Banner Image](../Images/OPS_CTLD.jpg)
--
-- # CTLD Concept
-- 
--  * MOOSE-based CTLD for Players.
--  * Object oriented refactoring of Ciribob\'s fantastic CTLD script.
--  * No need for extra MIST loading. 
--  * Additional events to tailor your mission.
--  * ANY late activated group can serve as cargo, either as troops, crates, which have to be build on-location, or static like ammo chests.
--  * Option to persist (save&load) your dropped troops, crates and vehicles.
--  * Weight checks on loaded cargo.
-- 
-- ## 0. Prerequisites
-- 
-- You need to load an .ogg soundfile for the pilot\'s beacons into the mission, e.g. "beacon.ogg", use a once trigger, "sound to country" for that.
-- Create the late-activated troops, vehicles, that will make up your deployable forces.
-- 
-- Example sound files are here: [Moose Sound](https://github.com/FlightControl-Master/MOOSE_SOUND/tree/master/CTLD%20CSAR)
-- 
-- ## 1. Basic Setup
-- 
-- ## 1.1 Create and start a CTLD instance
-- 
-- A basic setup example is the following:
--        
--        -- Instantiate and start a CTLD for the blue side, using helicopter groups named "Helicargo" and alias "Lufttransportbrigade I"
--        local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo"},"Lufttransportbrigade I")
--        my_ctld:__Start(5)
--
-- ## 1.2 Add cargo types available
--        
-- Add *generic* cargo types that you need for your missions, here infantry units, vehicles and a FOB. These need to be late-activated Wrapper.Group#GROUP objects:
--        
--        -- add infantry unit called "Anti-Tank Small" using template "ATS", of type TROOP with size 3
--        -- infantry units will be loaded directly from LOAD zones into the heli (matching number of free seats needed)
--        my_ctld:AddTroopsCargo("Anti-Tank Small",{"ATS"},CTLD_CARGO.Enum.TROOPS,3)
--        -- if you want to add weight to your Heli, troops can have a weight in kg **per person**. Currently no max weight checked. Fly carefully.
--        my_ctld:AddTroopsCargo("Anti-Tank Small",{"ATS"},CTLD_CARGO.Enum.TROOPS,3,80)
--        
--        -- add infantry unit called "Anti-Tank" using templates "AA" and "AA"", of type TROOP with size 4. No weight. We only have 2 in stock:
--        my_ctld:AddTroopsCargo("Anti-Air",{"AA","AA2"},CTLD_CARGO.Enum.TROOPS,4,nil,2)
--        
--        -- add an engineers unit called "Wrenches" using template "Engineers", of type ENGINEERS with size 2. Engineers can be loaded, dropped,
--        -- and extracted like troops. However, they will seek to build and/or repair crates found in a given radius. Handy if you can\'t stay
--        -- to build or repair or under fire.
--        my_ctld:AddTroopsCargo("Wrenches",{"Engineers"},CTLD_CARGO.Enum.ENGINEERS,4)
--        myctld.EngineerSearch = 2000 -- teams will search for crates in this radius.
--        
--        -- add vehicle called "Humvee" using template "Humvee", of type VEHICLE, size 2, i.e. needs two crates to be build
--        -- vehicles and FOB will be spawned as crates in a LOAD zone first. Once transported to DROP zones, they can be build into the objects
--        my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2)
--        -- if you want to add weight to your Heli, crates can have a weight in kg **per crate**. Fly carefully.
--        my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775)
--        -- if you want to limit your stock, add a number (here: 10) as parameter after weight. No parameter / nil means unlimited stock.
--        my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10)
--        -- additionally, you can limit **where** the stock is available (one location only!) - this one is available in a zone called "Vehicle Store".
--        my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10,nil,nil,"Vehicle Store")
--        
--        -- add infantry unit called "Forward Ops Base" using template "FOB", of type FOB, size 4, i.e. needs four crates to be build:
--        my_ctld:AddCratesCargo("Forward Ops Base",{"FOB"},CTLD_CARGO.Enum.FOB,4)
--        
--        -- add crates to repair FOB or VEHICLE type units - the 2nd parameter needs to match the template you want to repair,
--        -- e.g. the "Humvee" here refers back to the "Humvee" crates cargo added above (same template!)
--        my_ctld:AddCratesRepair("Humvee Repair","Humvee",CTLD_CARGO.Enum.REPAIR,1)
--        my_ctld.repairtime = 300 -- takes 300 seconds to repair something
-- 
--        -- add static cargo objects, e.g ammo chests - the name needs to refer to a STATIC object in the mission editor, 
--        -- here: it\'s the UNIT name (not the GROUP name!), the second parameter is the weight in kg.
--        my_ctld:AddStaticsCargo("Ammunition",500)
--        
-- ## 1.3 Add logistics zones
--  
--  Add (normal, round!)  zones for loading troops and crates and dropping, building crates
--  
--        -- Add a zone of type LOAD to our setup. Players can load any troops and crates here as defined in 1.2 above.
--        -- "Loadzone" is the name of the zone from the ME. Players can load, if they are inside the zone.
--        -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon.
--        my_ctld:AddCTLDZone("Loadzone",CTLD.CargoZoneType.LOAD,SMOKECOLOR.Blue,true,true)
--        
--        -- Add a zone of type DROP. Players can drop crates here.
--        -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon.
--        -- NOTE: Troops can be unloaded anywhere, also when hovering in parameters.
--        my_ctld:AddCTLDZone("Dropzone",CTLD.CargoZoneType.DROP,SMOKECOLOR.Red,true,true)
--        
--        -- Add two zones of type MOVE. Dropped troops and vehicles will move to the nearest one. See options.
--        -- Smoke and Flare color for this zone is blue, it is active (can be used) and has a radio beacon.
--        my_ctld:AddCTLDZone("Movezone",CTLD.CargoZoneType.MOVE,SMOKECOLOR.Orange,false,false)
--        
--        my_ctld:AddCTLDZone("Movezone2",CTLD.CargoZoneType.MOVE,SMOKECOLOR.White,true,true)
--        
--        -- Add a zone of type SHIP to our setup. Players can load troops and crates from this ship
--        -- "Tarawa" is the unitname (callsign) of the ship from the ME. Players can load, if they are inside the zone.
--        -- The ship is 240 meters long and 20 meters wide.
--        -- Note that you need to adjust the max hover height to deck height plus 5 meters or so for loading to work.
--        -- When the ship is moving, avoid forcing hoverload.
--        my_ctld:AddCTLDZone("Tarawa",CTLD.CargoZoneType.SHIP,SMOKECOLOR.Blue,true,true,240,20)
-- 
-- ## 2. Options
-- 
-- The following options are available (with their defaults). Only set the ones you want changed:
--
--          my_ctld.useprefix = true -- (DO NOT SWITCH THIS OFF UNLESS YOU KNOW WHAT YOU ARE DOING!) Adjust **before** starting CTLD. If set to false, *all* choppers of the coalition side will be enabled for CTLD.
--          my_ctld.CrateDistance = 35 -- List and Load crates in this radius only.
--          my_ctld.PackDistance = 35 -- Pack crates in this radius only
--          my_ctld.dropcratesanywhere = false -- Option to allow crates to be dropped anywhere.
--          my_ctld.dropAsCargoCrate = false -- Hercules only: Parachuted herc cargo is not unpacked automatically but placed as crate to be unpacked. Needs a cargo with the same name defined like the cargo that was dropped.
--          my_ctld.maximumHoverHeight = 15 -- Hover max this high to load in meters.
--          my_ctld.minimumHoverHeight = 4 -- Hover min this low to load in meters.
--          my_ctld.forcehoverload = true -- Crates (not: troops) can **only** be loaded while hovering.
--          my_ctld.hoverautoloading = true -- Crates in CrateDistance in a LOAD zone will be loaded automatically if space allows.
--          my_ctld.smokedistance = 2000 -- Smoke or flares can be request for zones this far away (in meters).
--          my_ctld.movetroopstowpzone = true -- Troops and vehicles will move to the nearest MOVE zone...
--          my_ctld.movetroopsdistance = 5000 -- .. but only if this far away (in meters)
--          my_ctld.smokedistance = 2000 -- Only smoke or flare zones if requesting player unit is this far away (in meters)
--          my_ctld.suppressmessages = false -- Set to true if you want to script your own messages.
--          my_ctld.repairtime = 300 -- Number of seconds it takes to repair a unit.
--          my_ctld.buildtime = 300 -- Number of seconds it takes to build a unit. Set to zero or nil to build instantly.
--          my_ctld.cratecountry = country.id.GERMANY -- ID of crates. Will default to country.id.RUSSIA for RED coalition setups.
--          my_ctld.allowcratepickupagain = true  -- allow re-pickup crates that were dropped.
--          my_ctld.enableslingload = false -- allow cargos to be slingloaded - might not work for all cargo types
--          my_ctld.pilotmustopendoors = false -- force opening of doors
--          my_ctld.SmokeColor = SMOKECOLOR.Red -- default color to use when dropping smoke from heli 
--          my_ctld.FlareColor = FLARECOLOR.Red -- color to use when flaring from heli
--          my_ctld.basetype = "container_cargo" -- default shape of the cargo container
--          my_ctld.droppedbeacontimeout = 600 -- dropped beacon lasts 10 minutes
--          my_ctld.usesubcats = false -- use sub-category names for crates, adds an extra menu layer in "Get Crates", useful if you have > 10 crate types.
--          my_ctld.placeCratesAhead = false -- place crates straight ahead of the helicopter, in a random way. If true, crates are more neatly sorted.
--          my_ctld.nobuildinloadzones = true -- forbid players to build stuff in LOAD zones if set to `true`
--          my_ctld.movecratesbeforebuild = true -- crates must be moved once before they can be build. Set to false for direct builds.
--          my_ctld.surfacetypes = {land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.RUNWAY,land.SurfaceType.SHALLOW_WATER} -- surfaces for loading back objects.
--          my_ctld.nobuildmenu = false -- if set to true effectively enforces to have engineers build/repair stuff for you.
--          my_ctld.RadioSound = "beacon.ogg" -- -- this sound will be hearable if you tune in the beacon frequency. Add the sound file to your miz.
--          my_ctld.RadioSoundFC3 = "beacon.ogg" -- this sound will be hearable by FC3 users (actually all UHF radios); change to something like "beaconsilent.ogg" and add the sound file to your miz if you don't want to annoy FC3 pilots.
--          my_ctld.enableChinookGCLoading = true -- this will effectively suppress the crate load and drop for CTLD_CARGO.Enum.STATIC types for CTLD for the Chinook
--          my_ctld.TroopUnloadDistGround = 5 -- If hovering, spawn dropped troops this far away in meters from the helo
--          my_ctld.TroopUnloadDistHover = 1.5 -- If grounded, spawn dropped troops this far away in meters from the helo
--          my_ctld.TroopUnloadDistGroundHerc = 25 -- On the ground, unload troops this far behind the Hercules
--          my_ctld.TroopUnloadDistGroundHook = 15 -- On the ground, unload troops this far behind the Chinook
--          my_ctld.TroopUnloadDistHoverHook = 5 -- When hovering, unload troops this far behind the Chinook
--          my_ctld.showstockinmenuitems = false -- When set to true, the menu lines will also show the remaining items in stock (that is, if you set any), downside is that the menu for all will be build every 30 seconds anew.
--          my_ctld.onestepmenu = false -- When set to true, the menu will create Drop and build, Get and load, Pack and remove, Pack and load, Pack. it will be a 1 step solution.
--          my_ctld.VehicleMoveFormation = AI.Task.VehicleFormation.VEE -- When a group moves to a MOVE zone, then it takes this formation. Can be a table of formations, which are then randomly chosen. Defaults to "Vee".
--          my_ctld.validateAndRepositionUnits = false -- Uses Disposition and other logic to find better ground positions for ground units avoiding trees, water, roads, runways, map scenery, statics and other units in the area. (Default is false)
--
-- ## 2.1 CH-47 Chinook support
-- 
-- The Chinook comes with the option to use the ground crew menu to load and unload cargo into the Helicopter itself for better immersion. As well, it can sling-load cargo from ground. The cargo you can actually **create**
-- from this menu is limited to contain items from the airbase or FARP's resources warehouse and can take a number of shapes (static shapes in the category of cargo) independent of their contents. If you unload this
-- kind of cargo with the ground crew, the contents will be "absorbed" into the airbase or FARP you landed at, and the cargo static will be removed after ca 2 mins. 
-- 
-- ## 2.1.1 Moose CTLD created crate cargo
-- 
-- Given the correct shape, Moose created cargo can theoretically be either loaded with the ground crew or via the F10 CTLD menu. **It is strongly stated to avoid using shapes with 
-- CTLD which can be Ground Crew loaded.**
-- Static shapes loadable *into* the Chinook and thus to **be avoided for CTLD** are at the time of writing:
-- 
--      * Ammo box (type "ammo_crate")
--      * M117 bomb crate (type name "m117_cargo")
--      * Dual shell fuel barrels (type name "barrels")
--      * UH-1H net (type name "uh1h_cargo")
--      
-- All other kinds of cargo can be sling-loaded.
--      
-- ## 2.1.2 Recommended settings
--          
--          my_ctld.basetype = "container_cargo" -- **DO NOT** change this to a base type which could also be loaded by F8/GC to avoid logic problems!
--          my_ctld.forcehoverload = false -- no hover autoload, leads to cargo complications with ground crew created cargo items
--          my_ctld.pilotmustopendoors = true -- crew must open back loading door 50% (horizontal) or more - watch out for NOT adding a back door gunner!
--          my_ctld.enableslingload = true -- will set cargo items as sling-loadable.
--          my_ctld.enableChinookGCLoading = true -- this will effectively suppress the crate load and drop for CTLD_CARGO.Enum.STATIC types for CTLD for the Chinook.
--          my_ctld.movecratesbeforebuild = true -- leave as is at the pain of building crate still **inside** of the Hook.
--          my_ctld.nobuildinloadzones = true -- don't build where you load.
--          my_ctld.ChinookTroopCircleRadius = 5 -- Radius for troops dropping in a nice circle. Adjust to your planned squad size for the Chinook.
--          
-- ## 2.2 User functions
-- 
-- ### 2.2.1 Adjust or add chopper unit-type capabilities
--  
-- Use this function to adjust what a heli type can or cannot do:
-- 
--        -- E.g. update unit capabilities for testing. Please stay realistic in your mission design.
--        -- Make a Gazelle into a heavy truck, this type can load both crates and troops and eight of each type, up to 4000 kgs:
--        my_ctld:SetUnitCapabilities("SA342L", true, true, 8, 8, 12, 4000)
--        
--        -- Default unit type capabilities are: 
--        ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400},
--        ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400},
--        ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400},
--        ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400},
--        ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15, cargoweightlimit = 700},
--        ["Mi-8MT"] = {type="Mi-8MT", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000},
--        ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000},
--        ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15, cargoweightlimit = 0},
--        ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700},
--        ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700},
--        ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25, cargoweightlimit = 19000},
--        ["UH-60L"] = {type="UH-60L", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500},
--        ["AH-64D_BLK_II"] = {type="AH-64D_BLK_II", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 17, cargoweightlimit = 200}, 
--        ["MH-60R"] = {type="MH-60R", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
--        ["SH-60B"] = {type="SH-60B", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
--        ["Bronco-OV-10A"] = {type="Bronco-OV-10A", crates= false, troops=true, cratelimit = 0, trooplimit = 5, length = 13, cargoweightlimit = 1450},
--        ["OH-6A"] = {type="OH-6A", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 7, cargoweightlimit = 550},
--        ["OH58D"] = {type="OH58D", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 14, cargoweightlimit = 400},
--        ["CH-47Fbl1"] = {type="CH-47Fbl1", crates=true, troops=true, cratelimit = 4, trooplimit = 31, length = 20, cargoweightlimit = 8000},
--        
-- ### 2.2.2 Activate and deactivate zones
-- 
-- Activate a zone:
-- 
--        -- Activate zone called Name of type #CTLD.CargoZoneType ZoneType:
--        my_ctld:ActivateZone(Name,CTLD.CargoZoneType.MOVE)
-- 
-- Deactivate a zone:
-- 
--        -- Deactivate zone called Name of type #CTLD.CargoZoneType ZoneType:
--        my_ctld:DeactivateZone(Name,CTLD.CargoZoneType.DROP)
-- 
-- ## 2.2.3 Limit and manage available resources
--  
--  When adding generic cargo types, you can effectively limit how many units can be dropped/build by the players, e.g.
--  
--              -- if you want to limit your stock, add a number (here: 10) as parameter after weight. No parameter / nil means unlimited stock.
--              my_ctld:AddCratesCargo("Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,2,2775,10)
--  
--  You can manually add or remove the available stock like so:
--            
--              -- Crates
--              my_ctld:AddStockCrates("Humvee", 2)
--              my_ctld:RemoveStockCrates("Humvee", 2)
--              
--              -- Troops
--              my_ctld:AddStockTroops("Anti-Air", 2)
--              my_ctld:RemoveStockTroops("Anti-Air", 2)
--  
--  Notes:
--  Troops dropped back into a LOAD zone will effectively be added to the stock. Crates lost in e.g. a heli crash are just that - lost.
--  
-- ## 2.2.4 Create own SET_GROUP to manage CTLD Pilot groups
-- 
--              -- Parameter: Set The SET_GROUP object created by the mission designer/user to represent the CTLD pilot groups.
--              -- Needs to be set before starting the CTLD instance.
--              local myset = SET_GROUP:New():FilterPrefixes("Helikopter"):FilterCoalitions("red"):FilterStart()
--              my_ctld:SetOwnSetPilotGroups(myset)
-- 
-- ## 3. Events
--
--  The class comes with a number of FSM-based events that missions designers can use to shape their mission.
--  These are:
-- 
-- ## 3.1 OnAfterTroopsPickedUp
-- 
--   This function is called when a player has loaded Troops:
--
--        function my_ctld:OnAfterTroopsPickedUp(From, Event, To, Group, Unit, Cargo)
--          ... your code here ...
--        end
-- 
-- ## 3.2 OnAfterCratesPickedUp
-- 
--    This function is called when a player has picked up crates:
--
--        function my_ctld:OnAfterCratesPickedUp(From, Event, To, Group, Unit, Cargo)
--          ... your code here ...
--        end
--  
-- ## 3.3 OnAfterTroopsDeployed
--  
--    This function is called when a player has deployed troops into the field:
--
--        function my_ctld:OnAfterTroopsDeployed(From, Event, To, Group, Unit, Troops)
--          ... your code here ...
--        end
--        
-- ## 3.4 OnAfterTroopsExtracted
--  
--    This function is called when a player has re-boarded already deployed troops from the field:
--
--        function my_ctld:OnAfterTroopsExtracted(From, Event, To, Group, Unit, Troops, Troopname)
--          ... your code here ...
--        end
--  
-- ## 3.5 OnAfterCratesDropped
--  
--    This function is called when a player has deployed crates:
--
--        function my_ctld:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable)
--          ... your code here ...
--        end
--
--- ## 3.6 OnAfterHelicopterLost
--  
--    This function is called when a player has deployed left a unit or crashed/died:
--
--        function my_ctld:OnAfterHelicopterLost(From, Event, To, Unitname, Cargotable)
--          ... your code here ...
--        end  
--  
-- ## 3.6 OnAfterCratesBuild, OnAfterCratesRepaired
--  
--    This function is called when a player has build a vehicle or FOB:
--
--        function my_ctld:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle)
--          ... your code here ...
--        end
--        
--        function my_ctld:OnAfterCratesRepaired(From, Event, To, Group, Unit, Vehicle)
--          ... your code here ...
--        end
 --  
-- ## 3.7 A simple SCORING example:
--  
--    To award player with points, using the SCORING Class (SCORING: my_Scoring, CTLD: CTLD_Cargotransport)
--
--        my_scoring = SCORING:New("Combat Transport")
--
--        function CTLD_Cargotransport:OnAfterCratesDropped(From, Event, To, Group, Unit, Cargotable)
--            local points = 10
--            if Unit then
--              local PlayerName = Unit:GetPlayerName()
--              my_scoring:_AddPlayerFromUnit( Unit )
--              my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for transporting cargo crates!", PlayerName, points), points)
--            end
--        end
--        
--        function CTLD_Cargotransport:OnAfterCratesBuild(From, Event, To, Group, Unit, Vehicle)
--          local points = 5
--          if Unit then
  --          local PlayerName = Unit:GetPlayerName()
  --          my_scoring:_AddPlayerFromUnit( Unit )
  --          my_scoring:AddGoalScore(Unit, "CTLD", string.format("Pilot %s has been awarded %d points for the construction of Units!", PlayerName, points), points)
--          end
--         end
--  
-- ## 4. F10 Menu structure
-- 
-- CTLD management menu is under the F10 top menu and called "CTLD"
-- 
-- ## 4.1 Manage Crates
-- 
-- Use this entry to get, load, list nearby, drop, build and repair crates. Also see options.
-- 
-- ## 4.2 Manage Troops
-- 
-- Use this entry to load, drop and extract troops. NOTE - with extract you can only load troops from the field that were deployed prior. 
-- Currently limited CTLD_CARGO troops, which are build from **one** template. Also, this will heal/complete your units as they are respawned.
-- 
-- ## 4.3 List boarded cargo
-- 
-- Lists what you have loaded. Shows load capabilities for number of crates and number of seats for troops.
-- 
-- ## 4.4 Smoke & Flare zones nearby or drop smoke, beacon or flare from Heli
-- 
-- Does what it says.
-- 
-- ## 4.5 List active zone beacons
-- 
-- Lists active radio beacons for all zones, where zones are both active and have a beacon. @see `CTLD:AddCTLDZone()`
-- 
-- ## 4.6 Show hover parameters
-- 
-- Lists hover parameters and indicates if these are curently fulfilled. Also @see options on hover heights.
-- 
-- ## 4.7 List Inventory
-- 
-- Lists inventory of available units to drop or build.
-- 
-- ## 5. Support for fixed wings
-- 
-- Basic support for the Hercules mod By Anubis has been build into CTLD, as well as Bronco and Mosquito - that is you can load/drop/build the same way and for the same objects as 
-- the helicopters (main method). 
-- To cover objects and troops which can be loaded from the groud crew Rearm/Refuel menu (F8), you need to use @{#CTLD_HERCULES.New}() and link
-- this object to your CTLD setup (alternative method). In this case, do **not** use the `Hercules_Cargo.lua` or `Hercules_Cargo_CTLD.lua` which are part of the mod 
-- in your mission!
-- 
-- ### 5.1 Create an own CTLD instance and allow the usage of the Hercules mod (main method)
--
--              local my_ctld = CTLD:New(coalition.side.BLUE,{"Helicargo", "Hercules"},"Lufttransportbrigade I")
-- 
-- Enable these options for Hercules support:
--  
--              my_ctld.enableFixedWing = true
--              my_ctld.FixedMinAngels = 155 -- for troop/cargo drop via chute in meters, ca 470 ft
--              my_ctld.FixedMaxAngels = 2000 -- for troop/cargo drop via chute in meters, ca 6000 ft
--              my_ctld.FixedMaxSpeed = 77 -- 77mps or 270kph or 150kn
-- 
-- Hint: you can **only** airdrop from the Hercules if you are "in parameters", i.e. at or below `FixedMaxSpeed` and in the AGLFixedMinAngelseen
-- `FixedMinAngels` and `FixedMaxAngels`!
-- 
-- Also, the following options need to be set to `true`:
-- 
--              my_ctld.useprefix = true -- this is true by default and MUST BE ON. 
-- 
-- ### 5.2 Integrate Hercules ground crew (F8 Menu) loadable objects (alternative method, use either the above OR this method, NOT both!)
-- 
-- Taking another approach, integrate to your CTLD instance like so, where `my_ctld` is a previously created CTLD instance:
--            
--            my_ctld.enableFixedWing = false -- avoid dual loading via CTLD F10 and F8 ground crew
--            local herccargo = CTLD_HERCULES:New("blue", "Hercules Test", my_ctld)
--            
-- You also need: 
-- 
-- * A template called "Infantry" for 10 Paratroopers (as set via herccargo.infantrytemplate).   
-- * Depending on what you are loading with the help of the ground crew, there are 42 more templates for the various vehicles that are loadable.   
-- 
-- There's a **quick check output in the `dcs.log`** which tells you what's there and what not.
-- E.g.:   
-- 
--            ...Checking template for APC BTR-82A Air [24998lb] (BTR-82A) ... MISSING)   
--            ...Checking template for ART 2S9 NONA Skid [19030lb] (SAU 2-C9) ... MISSING)   
--            ...Checking template for EWR SBORKA Air [21624lb] (Dog Ear radar) ... MISSING)   
--            ...Checking template for Transport Tigr Air [15900lb] (Tigr_233036) ... OK)   
--
-- Expected template names are the ones in the rounded brackets.
--
-- ### 5.2.1 Hints
-- 
-- The script works on the EVENTS.Shot trigger, which is used by the mod when you **drop cargo from the Hercules while flying**. Unloading on the ground does
-- not achieve anything here. If you just want to unload on the ground, use the normal Moose CTLD (see 5.1).
-- 
-- DO NOT use the "splash damage" script together with this method! Your cargo will explode on the ground!
-- 
-- There are two ways of airdropping: 
--   
-- 1) Very low and very slow (>5m and <10m AGL) - here you can drop stuff which has "Skid" at the end of the cargo name (loaded via F8 Ground Crew menu)   
-- 2) Higher up and slow (>100m AGL) - here you can drop paratroopers and cargo which has "Air" at the end of the cargo name (loaded via F8 Ground Crew menu)   
-- 
-- Standard transport capabilities as per the real Hercules are:
-- 
--               ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64}, -- 19t cargo, 64 paratroopers
--  
-- ### 5.3 Don't automatically unpack dropped cargo but drop as CTLD_CARGO
-- 
-- Cargo can be defined to be automatically dropped as crates.
--              my_ctld.dropAsCargoCrate = true -- default is false
--
-- The idea is, to have those crate behave like brought in with a helo. So any unpack restictions apply.
-- To enable those cargo drops, the cargo types must be added manually in the CTLD configuration. So when the above defined template for "Vulcan" should be used
-- as CTLD_Cargo, the following line has to be added. NoCrates, PerCrateMass, Stock, SubCategory can be configured freely.
--              my_ctld:AddCratesCargo("Vulcan",      {"Vulcan"}, CTLD_CARGO.Enum.VEHICLE, 6, 2000, nil, "SAM/AAA")
--
-- So if the Vulcan in the example now needs six crates to complete, you have to bring two Hercs with three Vulcan crates each and drop them very close together...
--
-- ## 6. Save and load back units - persistance
-- 
-- You can save and later load back units dropped or build to make your mission persistent.
-- For this to work, you need to de-sanitize **io** and **lfs** in your MissionScripting.lua, which is located in your DCS installtion folder under Scripts.
-- There is a risk involved in doing that; if you do not know what that means, this is possibly not for you.
-- 
-- Use the following options to manage your saves:
-- 
--              my_ctld.enableLoadSave = true -- allow auto-saving and loading of files
--              my_ctld.saveinterval = 600 -- save every 10 minutes
--              my_ctld.filename = "missionsave.csv" -- example filename
--              my_ctld.filepath = "C:\\Users\\myname\\Saved Games\\DCS\Missions\\MyMission" -- example path
--              my_ctld.eventoninject = true -- fire OnAfterCratesBuild and OnAfterTroopsDeployed events when loading (uses Inject functions)
--              my_ctld.useprecisecoordloads = true -- Instead if slightly varyiing the group position, try to maintain it as is
--  
--  Then use an initial load at the beginning of your mission:
--  
--            my_ctld:__Load(10)
--            
-- **Caveat:**
-- If you use units build by multiple templates, they will effectively double on loading. Dropped crates are not saved. Current stock is not saved.
-- 
-- ## 7. Complex example - Build a complete FARP from a CTLD crate drop
-- 
-- Prerequisites - you need to add a cargo of type FOB to your CTLD instance, for simplification reasons we call it FOB:
-- 
--            my_ctld:AddCratesCargo("FARP",{"FOB"},CTLD_CARGO.Enum.FOB,2)
--            
-- The following code will build a FARP at the coordinate the FOB was dropped and built (the UTILS function used below **does not** need a template for the statics):
--            
--          -- FARP Radio. First one has 130AM name London, next 131 name Dallas, and so forth. 
--          local FARPFreq = 129
--          local FARPName = 1  --numbers 1..10
-- 
--          local FARPClearnames = {
--            [1]="London",
--            [2]="Dallas",
--            [3]="Paris",
--            [4]="Moscow",
--            [5]="Berlin",
--            [6]="Rome",
--            [7]="Madrid",
--            [8]="Warsaw",
--            [9]="Dublin",
--            [10]="Perth",
--            }
-- 
--          function BuildAFARP(Coordinate)
--            local coord = Coordinate  --Core.Point#COORDINATE
--
--            local FarpNameNumber = ((FARPName-1)%10)+1 -- make sure 11 becomes 1 etc
--            local FName = FARPClearnames[FarpNameNumber] -- get clear namee
--  
--            FARPFreq = FARPFreq + 1
--            FARPName = FARPName + 1
--  
--            FName = FName .. " FAT COW "..tostring(FARPFreq).."AM" -- make name unique
--  
--            -- Get a Zone for loading 
--            local ZoneSpawn = ZONE_RADIUS:New("FARP "..FName,Coordinate:GetVec2(),150,false)
--  
--            -- Spawn a FARP with our little helper and fill it up with resources (10t fuel each type, 10 pieces of each known equipment)
--            UTILS.SpawnFARPAndFunctionalStatics(FName,Coordinate,ENUMS.FARPType.INVISIBLE,my_ctld.coalition,country.id.USA,FarpNameNumber,FARPFreq,radio.modulation.AM,nil,nil,nil,10,10)
-- 
--            -- add a loadzone to CTLD
--            my_ctld:AddCTLDZone("FARP "..FName,CTLD.CargoZoneType.LOAD,SMOKECOLOR.Blue,true,true)
--            local m  = MESSAGE:New(string.format("FARP %s in operation!",FName),15,"CTLD"):ToBlue() 
--          end
--
--          function my_ctld:OnAfterCratesBuild(From,Event,To,Group,Unit,Vehicle)
--            local name = Vehicle:GetName()
--            if string.find(name,"FOB",1,true) then
--              local Coord = Vehicle:GetCoordinate()
--              Vehicle:Destroy(false)
--              BuildAFARP(Coord) 
--            end
--          end
-- 
-- ## 8. Transport crates and troops with CA (Combined Arms) trucks
-- 
-- You can optionally also allow to CTLD with CA trucks and other vehicles:
-- 
--          -- Create a SET_CLIENT to capture CA vehicles steered by players
--          local truckers = SET_CLIENT:New():HandleCASlots():FilterCoalitions("blue"):FilterPrefixes("Truck"):FilterStart()
--          -- Allow CA transport
--          my_ctld:AllowCATransport(true,truckers)
--          -- Set truck capability by typename
--          my_ctld:SetUnitCapabilities("M 818", true, true, 2, 12, 9, 4500)
--          -- Alternatively set truck capability with a UNIT object
--          local GazTruck = UNIT:FindByName("GazTruck-1-1")
--          my_ctld:SetUnitCapabilities(GazTruck, true, true, 2, 12, 9, 4500)
-- 
--
-- @field #CTLD
CTLD = {
  ClassName       = "CTLD",
  verbose         = 0,
  lid             = "",
  coalition       = 1,
  coalitiontxt    = "blue",
  PilotGroups = {}, -- #GROUP_SET of heli pilots
  CtldUnits = {},   -- Table of helicopter #GROUPs
  FreeVHFFrequencies = {}, -- Table of VHF
  FreeUHFFrequencies = {}, -- Table of UHF
  FreeFMFrequencies = {}, -- Table of FM
  CargoCounter = 0,
  Cargo_Troops = {}, -- generic troops objects
  Cargo_Crates = {}, -- generic crate objects
  Loaded_Cargo = {}, -- cargo aboard units
  Spawned_Crates = {}, -- Holds objects for crates spawned generally
  Spawned_Cargo = {}, -- Binds together spawned_crates and their CTLD_CARGO objects
  CrateDistance = 35, -- list crates in this radius
  PackDistance = 35,  -- pack crates in this radius
  debug = false,
  wpZones = {},
  dropOffZones = {},
  pickupZones  = {},
  DynamicCargo = {},
  ChinookTroopCircleRadius = 5,
  TroopUnloadDistGround = 5,
  TroopUnloadDistGroundHerc = 25,
  TroopUnloadDistGroundHook = 15,
  TroopUnloadDistHoverHook = 5,
  TroopUnloadDistHover = 1.5,
  UserSetGroup = nil,
  LoadedGroupsTable = {},
  keeploadtable = true,
  allowCATransport = false,
  VehicleMoveFormation = AI.Task.VehicleFormation.VEE,
}

------------------------------
-- DONE: Zone Checks
-- DONE: TEST Hover load and unload
-- DONE: Crate unload
-- DONE: Hover (auto-)load
-- DONE: (More) Housekeeping
-- DONE: Troops running to WP Zone
-- DONE: Zone Radio Beacons
-- DONE: Stats Running
-- DONE: Added support for Hercules
-- TODO: Possibly - either/or loading crates and troops
-- DONE: Make inject respect existing cargo types
-- DONE: Drop beacons or flares/smoke
-- DONE: Add statics as cargo
-- DONE: List cargo in stock
-- DONE: Limit of troops, crates buildable?
-- DONE: Allow saving of Troops & Vehicles
-- DONE: Adding re-packing dropped units
------------------------------

--- Radio Beacons
-- @type CTLD.ZoneBeacon
-- @field #string name -- Name of zone for the coordinate
-- @field #number frequency -- in mHz
-- @field #number modulation -- i.e.CTLD.RadioModulation.FM or CTLD.RadioModulation.AM

--- Radio Modulation
-- @type CTLD.RadioModulation
-- @field #number AM
-- @field #number FM
CTLD.RadioModulation = {
  AM = 0,
  FM = 1,
}

--- Loaded Cargo
-- @type CTLD.LoadedCargo
-- @field #number Troopsloaded
-- @field #number Cratesloaded
-- @field #table Cargo Table of #CTLD_CARGO objects

--- Zone Info.
-- @type CTLD.CargoZone
-- @field #string name Name of Zone.
-- @field #string color Smoke color for zone, e.g. SMOKECOLOR.Red.
-- @field #boolean active Active or not.
-- @field #string type Type of zone, i.e. load,drop,move,ship
-- @field #boolean hasbeacon Create and run radio beacons if active.
-- @field #table fmbeacon Beacon info as #CTLD.ZoneBeacon
-- @field #table uhfbeacon Beacon info as #CTLD.ZoneBeacon
-- @field #table vhfbeacon Beacon info as #CTLD.ZoneBeacon
-- @field #number shiplength For ships - length of ship
-- @field #number shipwidth For ships - width of ship
-- @field #number timestamp For dropped beacons - time this was created

--- Zone Type Info.
-- @type CTLD.CargoZoneType
CTLD.CargoZoneType = {
  LOAD = "load",
  DROP = "drop",
  MOVE = "move",
  SHIP = "ship",
  BEACON = "beacon",
}

--- Buildable table info.
-- @type CTLD.Buildable
-- @field #string Name Name of the object.
-- @field #number Required Required crates.
-- @field #number Found Found crates.
-- @field #table Template Template names for this build.
-- @field #boolean CanBuild Is buildable or not.
-- @field #CTLD_CARGO.Enum Type Type enumerator (for moves).

--- Unit capabilities.
-- @type CTLD.UnitTypeCapabilities
-- @field #string type Unit type.
-- @field #boolean crates Can transport crate.
-- @field #boolean troops Can transport troops.
-- @field #number cratelimit Number of crates transportable.
-- @field #number trooplimit Number of troop units transportable.
-- @field #number cargoweightlimit Max loadable kgs of cargo.
CTLD.UnitTypeCapabilities = {
    ["SA342Mistral"] = {type="SA342Mistral", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400},
    ["SA342L"] = {type="SA342L", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400},
    ["SA342M"] = {type="SA342M", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 12, cargoweightlimit = 400},
    ["SA342Minigun"] = {type="SA342Minigun", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 12, cargoweightlimit = 400},
    ["UH-1H"] = {type="UH-1H", crates=true, troops=true, cratelimit = 1, trooplimit = 8, length = 15, cargoweightlimit = 700},
    ["Mi-8MTV2"] = {type="Mi-8MTV2", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000},
    ["Mi-8MT"] = {type="Mi-8MT", crates=true, troops=true, cratelimit = 2, trooplimit = 12, length = 15, cargoweightlimit = 3000},
    ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15, cargoweightlimit = 0},
    ["Ka-50_3"] = {type="Ka-50_3", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 15, cargoweightlimit = 0},
    ["Mi-24P"] = {type="Mi-24P", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700},
    ["Mi-24V"] = {type="Mi-24V", crates=true, troops=true, cratelimit = 2, trooplimit = 8, length = 18, cargoweightlimit = 700},
    ["Hercules"] = {type="Hercules", crates=true, troops=true, cratelimit = 7, trooplimit = 64, length = 25, cargoweightlimit = 19000}, -- 19t cargo, 64 paratroopers. 
    --Actually it's longer, but the center coord is off-center of the model.
    ["UH-60L"] = {type="UH-60L", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
    ["UH-60L_DAP"] = {type="UH-60L_DAP", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 16, cargoweightlimit = 500}, -- UH-60L DAP is an attack helo but can do limited CSAR and CTLD
    ["MH-60R"] = {type="MH-60R", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
    ["SH-60B"] = {type="SH-60B", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
    ["AH-64D_BLK_II"] = {type="AH-64D_BLK_II", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 17, cargoweightlimit = 200}, -- 2 ppl **outside** the helo
    ["Bronco-OV-10A"] = {type="Bronco-OV-10A", crates= false, troops=true, cratelimit = 0, trooplimit = 5, length = 13, cargoweightlimit = 1450},
    ["OH-6A"] = {type="OH-6A", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 7, cargoweightlimit = 550},
    ["OH58D"] = {type="OH58D", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 14, cargoweightlimit = 400},
    ["CH-47Fbl1"] = {type="CH-47Fbl1", crates=true, troops=true, cratelimit = 4, trooplimit = 31, length = 20, cargoweightlimit = 10800},
    ["MosquitoFBMkVI"] = {type="MosquitoFBMkVI", crates= true, troops=false, cratelimit = 2, trooplimit = 0, length = 13, cargoweightlimit = 1800},
    ["M 818"] = {type="M 818", crates= true, troops=true, cratelimit = 4, trooplimit = 12, length = 9, cargoweightlimit = 4500},
}

--- Allowed Fixed Wing Types
-- @type CTLD.FixedWingTypes
CTLD.FixedWingTypes = {
  ["Hercules"] = "Hercules",
  ["Bronco"] = "Bronco",
  ["Mosquito"] = "Mosquito",
}

--- CTLD class version.
-- @field #string version
CTLD.version="1.3.38"

--- Instantiate a new CTLD.
-- @param #CTLD self
-- @param #string Coalition Coalition of this CTLD. I.e. coalition.side.BLUE or coalition.side.RED or coalition.side.NEUTRAL
-- @param #table Prefixes Table of pilot prefixes.
-- @param #string Alias Alias of this CTLD for logging.
-- @return #CTLD self
function CTLD:New(Coalition, Prefixes, Alias)
  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #CTLD
  
  BASE:T({Coalition, Prefixes, Alias})
  
  --set Coalition
  if Coalition and type(Coalition)=="string" then
    if Coalition=="blue" then
      self.coalition=coalition.side.BLUE
      self.coalitiontxt = Coalition
    elseif Coalition=="red" then
      self.coalition=coalition.side.RED
      self.coalitiontxt = Coalition
    elseif Coalition=="neutral" then
      self.coalition=coalition.side.NEUTRAL
      self.coalitiontxt = Coalition
    else
      self:E("ERROR: Unknown coalition in CTLD!")
    end
  else
    self.coalition = Coalition
    self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition))
  end
  
  -- Set alias.
  if Alias then
    self.alias=tostring(Alias)
  else
    self.alias="UNHCR"  
    if self.coalition then
      if self.coalition==coalition.side.RED then
        self.alias="Red CTLD"
      elseif self.coalition==coalition.side.BLUE then
        self.alias="Blue CTLD"
      end
    end
  end
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown")
  
  -- Start State.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  --                 From State  -->   Event        -->      To State
  self:AddTransition("Stopped",       "Start",               "Running")     -- Start FSM.
  self:AddTransition("*",             "Status",              "*")           -- CTLD status update.
  self:AddTransition("*",             "TroopsPickedUp",      "*")           -- CTLD pickup  event. 
  self:AddTransition("*",             "TroopsExtracted",     "*")           -- CTLD extract  event. 
  self:AddTransition("*",             "CratesPickedUp",      "*")           -- CTLD pickup  event.  
  self:AddTransition("*",             "TroopsDeployed",      "*")           -- CTLD deploy  event. 
  self:AddTransition("*",             "TroopsRTB",           "*")           -- CTLD deploy  event.   
  self:AddTransition("*",             "CratesDropped",       "*")           -- CTLD deploy  event.  
  self:AddTransition("*",             "CratesBuild",         "*")           -- CTLD build  event.
  self:AddTransition("*",             "CratesRepaired",      "*")           -- CTLD repair  event.
  self:AddTransition("*",             "CratesBuildStarted",  "*")           -- CTLD build  event.
  self:AddTransition("*",             "CratesRepairStarted", "*")           -- CTLD repair  event.
  self:AddTransition("*",             "CratesPacked",        "*")           -- CTLD repack  event.
  self:AddTransition("*",             "HelicopterLost",      "*")           -- CTLD lost  event.
  self:AddTransition("*",             "Load",                "*")           -- CTLD load  event.
  self:AddTransition("*",             "Loaded",              "*")           -- CTLD load  event.   
  self:AddTransition("*",             "Save",                "*")           -- CTLD save  event.      
  self:AddTransition("*",             "Stop",                "Stopped")     -- Stop FSM.
  
  -- tables
  self.PilotGroups ={}
  self.CtldUnits = {}
  
  -- Beacons
  self.FreeVHFFrequencies = {}
  self.FreeUHFFrequencies = {}
  self.FreeFMFrequencies = {}
  self.UsedVHFFrequencies = {}
  self.UsedUHFFrequencies = {}
  self.UsedFMFrequencies = {}
  
  -- radio beacons
  self.RadioSound = "beacon.ogg"
  self.RadioSoundFC3 = "beacon.ogg"
  self.RadioPath = "l10n/DEFAULT/"
  
  -- zones stuff
  self.pickupZones  = {}
  self.dropOffZones = {}
  self.wpZones = {}
  self.shipZones = {}
  self.droppedBeacons = {}
  self.droppedbeaconref = {}
  self.droppedbeacontimeout = 600
  self.useprecisecoordloads = true
  
  -- Cargo
  self.Cargo_Crates = {}
  self.Cargo_Troops = {}
  self.Cargo_Statics = {}
  self.Loaded_Cargo = {}
  self.Spawned_Crates = {}
  self.Spawned_Cargo = {}
  self.MenusDone = {}
  self.DroppedTroops = {}
  self.DroppedCrates = {}
  self.CargoCounter = 0
  self.CrateCounter = 0
  self.TroopCounter = 0
  
  -- added engineering
  self.Engineers = 0 -- #number use as counter
  self.EngineersInField = {} -- #table holds #CTLD_ENGINEERING objects
  self.EngineerSearch = 2000 -- #number search distance for crates to build or repair
  self.nobuildmenu = false -- enfore engineer build only?
  
  -- setup
  self.CrateDistance = 35 -- list/load crates in this radius
  self.PackDistance = 35 -- pack objects in this radius
  self.ExtractFactor = 3.33 -- factor for troops extraction, i.e. CrateDistance * Extractfactor
  self.prefixes = Prefixes or {"Cargoheli"}
  self.useprefix = true
  
  self.maximumHoverHeight = 15
  self.minimumHoverHeight = 4
  self.forcehoverload = true
  self.hoverautoloading = true
  self.dropcratesanywhere = false -- #1570
  self.dropAsCargoCrate = false -- Parachuted herc cargo is not unpacked automatically but placed as crate to be unpacked

  self.smokedistance = 2000
  self.movetroopstowpzone = true
  self.movetroopsdistance = 5000
  self.returntroopstobase = false -- if set to false, troops would stay after deployment inside a load zone.
  self.troopdropzoneradius = 100
  
  self.VehicleMoveFormation = AI.Task.VehicleFormation.VEE
  
  -- added support Hercules Mod
  self.enableHercules = false -- deprecated
  self.enableFixedWing = false
  self.FixedMinAngels = 165 -- for troop/cargo drop via chute
  self.FixedMaxAngels = 2000 -- for troop/cargo drop via chute
  self.FixedMaxSpeed = 77 -- 280 kph or 150kn eq 77 mps

  self.validateAndRepositionUnits = false -- 280 kph or 150kn eq 77 mps

  -- message suppression
  self.suppressmessages = false
  
  -- time to repairor build a unit/group
  self.repairtime = 300
  self.buildtime = 300
  
  -- place spawned crates in front of aircraft
  self.placeCratesAhead = false
  
  -- country of crates spawned
  self.cratecountry = country.id.GERMANY
  
  -- for opening doors
  self.pilotmustopendoors = false
  
  if self.coalition == coalition.side.RED then
     self.cratecountry = country.id.RUSSIA
  end
  
  -- load and save dropped TROOPS
  self.enableLoadSave = false
  self.filepath = nil
  self.saveinterval = 600
  self.eventoninject = true
  self.keeploadtable = true
  self.LoadedGroupsTable = {}
  
  -- sub categories
  self.usesubcats = false
  self.subcats = {}
  self.subcatsTroop = {}
  self.showstockinmenuitems = false
  self.onestepmenu = false
  
  -- disallow building in loadzones
  self.nobuildinloadzones = true
  self.movecratesbeforebuild = true
  self.surfacetypes = {land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.RUNWAY,land.SurfaceType.SHALLOW_WATER}
  
  -- Chinook
  self.enableChinookGCLoading = true
  self.ChinookTroopCircleRadius = 5
  
  -- User SET_GROUP
  self.UserSetGroup = nil
  
  local AliaS = string.gsub(self.alias," ","_")
  self.filename = string.format("CTLD_%s_Persist.csv",AliaS)
  
  -- allow re-pickup crates
  self.allowcratepickupagain = true
  
  -- slingload
  self.enableslingload = false
  self.basetype = "container_cargo" -- shape of the container
  
  -- Smokes and Flares
  self.SmokeColor = SMOKECOLOR.Red
  self.FlareColor = FLARECOLOR.Red
  
  for i=1,100 do
    math.random()
  end
  
  -- CA Transport
  self.allowCATransport = false -- #boolean
  self.CATransportSet = nil -- Core.Set#SET_CLIENT
  
  self:_GenerateVHFrequencies()
  self:_GenerateUHFrequencies()
  self:_GenerateFMFrequencies()
  
  ------------------------
  --- Pseudo Functions ---
  ------------------------
  
  --- Triggers the FSM event "Start". Starts the CTLD. Initializes parameters and starts event handlers.
  -- @function [parent=#CTLD] Start
  -- @param #CTLD self

  --- Triggers the FSM event "Start" after a delay. Starts the CTLD. Initializes parameters and starts event handlers.
  -- @function [parent=#CTLD] __Start
  -- @param #CTLD self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the CTLD and all its event handlers.
  -- @function [parent=#CTLD] Stop
  -- @param #CTLD self

  --- Triggers the FSM event "Stop" after a delay. Stops the CTLD and all its event handlers.
  -- @function [parent=#CTLD] __Stop
  -- @param #CTLD self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Status".
  -- @function [parent=#CTLD] Status
  -- @param #CTLD self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#CTLD] __Status
  -- @param #CTLD self
  -- @param #number delay Delay in seconds.
  
  --- Triggers the FSM event "Load".
  -- @function [parent=#CTLD] Load
  -- @param #CTLD self

  --- Triggers the FSM event "Load" after a delay.
  -- @function [parent=#CTLD] __Load
  -- @param #CTLD self
  -- @param #number delay Delay in seconds.
  
  --- Triggers the FSM event "Save".
  -- @function [parent=#CTLD] Load
  -- @param #CTLD self

  --- Triggers the FSM event "Save" after a delay.
  -- @function [parent=#CTLD] __Save
  -- @param #CTLD self
  -- @param #number delay Delay in seconds.
  
  --- FSM Function OnBeforeTroopsPickedUp.
  -- @function [parent=#CTLD] OnBeforeTroopsPickedUp
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo troops.
  -- @return #CTLD self
  
  --- FSM Function OnBeforeTroopsExtracted.
  -- @function [parent=#CTLD] OnBeforeTroopsExtracted
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops extracted.
  -- @param #string Troopname Name of the extracted group.
  -- @return #CTLD self
    
  --- FSM Function OnBeforeCratesPickedUp.
  -- @function [parent=#CTLD] OnBeforeCratesPickedUp
  -- @param #CTLD self
  -- @param #string From State .
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo crate. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew loaded!
  -- @return #CTLD self
  
   --- FSM Function OnBeforeTroopsDeployed.
  -- @function [parent=#CTLD] OnBeforeTroopsDeployed
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object.
  -- @return #CTLD self
  
  --- FSM Function OnBeforeCratesDropped.
  -- @function [parent=#CTLD] OnBeforeCratesDropped
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew unloaded!
  -- @return #CTLD self
  
  --- FSM Function OnBeforeCratesBuild.
  -- @function [parent=#CTLD] OnBeforeCratesBuild
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build.
  -- @return #CTLD self

  --- FSM Function OnBeforeCratesRepaired.
  -- @function [parent=#CTLD] OnBeforeCratesRepaired
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB repaired.
  -- @return #CTLD self
        
  --- FSM Function OnBeforeCratesPacked.
  -- @function [parent=#CTLD] OnBeforeCratesPacked
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo crate that was repacked.
  -- @return #CTLD self
    
  --- FSM Function OnBeforeTroopsRTB.
  -- @function [parent=#CTLD] OnBeforeTroopsRTB
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #string ZoneName Name of the Zone where the Troops have been RTB'd.
  -- @param Core.Zone#ZONE_Radius ZoneObject of the Zone where the Troops have been RTB'd.
  
  --- FSM Function OnAfterTroopsPickedUp.
  -- @function [parent=#CTLD] OnAfterTroopsPickedUp
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo troops.
  -- @return #CTLD self
  
  --- FSM Function OnAfterTroopsExtracted.
  -- @function [parent=#CTLD] OnAfterTroopsExtracted
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops extracted.
  -- @param #string Troopname Name of the extracted group.
  -- @return #CTLD self
    
  --- FSM Function OnAfterCratesPickedUp.
  -- @function [parent=#CTLD] OnAfterCratesPickedUp
  -- @param #CTLD self
  -- @param #string From State .
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo crate. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew loaded!
  -- @return #CTLD self
  
   --- FSM Function OnAfterTroopsDeployed.
  -- @function [parent=#CTLD] OnAfterTroopsDeployed
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object.
  -- @return #CTLD self
  
  --- FSM Function OnAfterCratesDropped.
  -- @function [parent=#CTLD] OnAfterCratesDropped
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew unloaded!
  -- @return #CTLD self
  
  --- FSM Function OnAfterCratesBuild.
  -- @function [parent=#CTLD] OnAfterCratesBuild
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build.
  -- @return #CTLD self

  --- FSM Function OnAfterCratesBuildStarted. Info event that a build has been started.
  -- @function [parent=#CTLD] OnAfterCratesBuildStarted
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param CargoName The name of the cargo being built.
  -- @return #CTLD self

  --- FSM Function OnAfterCratesRepairStarted. Info event that a repair has been started.
  -- @function [parent=#CTLD] OnAfterCratesRepairStarted
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @return #CTLD self

  --- FSM Function OnBeforeCratesBuildStarted. Info event that a build has been started.
  -- @function [parent=#CTLD] OnBeforeCratesBuildStarted
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @return #CTLD self

  --- FSM Function OnBeforeCratesRepairStarted. Info event that a repair has been started.
  -- @function [parent=#CTLD] OnBeforeCratesRepairStarted
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @return #CTLD self

  --- FSM Function OnAfterCratesRepaired.
  -- @function [parent=#CTLD] OnAfterCratesRepaired
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB repaired.
  -- @return #CTLD self
  
  --- FSM Function OnAfterCratesPacked.
  -- @function [parent=#CTLD] OnAfterCratesPacked
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo crate that was repacked.
  -- @return #CTLD self
    
  --- FSM Function OnAfterTroopsRTB.
  -- @function [parent=#CTLD] OnAfterTroopsRTB
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
        
  --- FSM Function OnBeforeHelicopterLost.
  -- @function [parent=#CTLD] OnBeforeHelicopterLost
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param #string Unitname The name of the unit lost.
  -- @param #table LostCargo Table of #CTLD_CARGO object which were aboard the helicopter/transportplane lost. Can be an empty table!

  --- FSM Function OnAfterHelicopterLost.
  -- @function [parent=#CTLD] OnAfterHelicopterLost
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param #string Unitname The name of the unit lost.
  -- @param #table LostCargo Table of #CTLD_CARGO object which were aboard the helicopter/transportplane lost. Can be an empty table!
  
  --- FSM Function OnAfterLoad.
  -- @function [parent=#CTLD] OnAfterLoad
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
  -- @param #string filename (Optional) File name for loading. Default is "CTLD_<alias>_Persist.csv".
  
  --- FSM Function OnAfterLoaded.
  -- @function [parent=#CTLD] OnAfterLoaded
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #table LoadedGroups Table of loaded groups, each entry is a table with three values: Group, TimeStamp and CargoType.
  
  --- FSM Function OnAfterSave.
  -- @function [parent=#CTLD] OnAfterSave
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
  -- @param #string filename (Optional) File name for saving. Default is "CTLD_<alias>_Persist.csv".
  
  return self
end

------------------------------------------------------------------- 
-- Helper and User Functions
------------------------------------------------------------------- 

--- (Internal) Function to get capabilities of a chopper
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit The unit
-- @return #table Capabilities Table of caps
function CTLD:_GetUnitCapabilities(Unit)
  self:T(self.lid .. " _GetUnitCapabilities")
  local _unit = Unit -- Wrapper.Unit#UNIT
  local unittype = _unit:GetTypeName()
  local capabilities = self.UnitTypeCapabilities[unittype] -- #CTLD.UnitTypeCapabilities
  if not capabilities or capabilities == {} then
    -- e.g. ["Ka-50"] = {type="Ka-50", crates=false, troops=false, cratelimit = 0, trooplimit = 0},
    capabilities = {}
    capabilities.troops = false
    capabilities.crates = false
    capabilities.cratelimit = 0
    capabilities.trooplimit = 0
    capabilities.type = "generic"
    capabilities.length = 20
    capabilities.cargoweightlimit = 0
  end
  return capabilities
end

--- (User) Function to allow transport via Combined Arms Trucks.
-- @param #CTLD self
-- @param #boolean OnOff Switch on (true) or off (false).
-- @param Core.Set#SET_CLIENT ClientSet The CA handling client set for ground transport.
-- @return #CTLD self
function CTLD:AllowCATransport(OnOff,ClientSet)
  self.allowCATransport = OnOff -- #boolean
  self.CATransportSet = ClientSet -- Core.Set#SET_CLIENT
  return self
end

--- (Internal) Function to generate valid UHF Frequencies
-- @param #CTLD self
function CTLD:_GenerateUHFrequencies()
  self:T(self.lid .. " _GenerateUHFrequencies")
    self.FreeUHFFrequencies = {}
    self.FreeUHFFrequencies = UTILS.GenerateUHFrequencies(243,320)    
    return self
end

--- (Internal) Function to generate valid FM Frequencies
-- @param #CTLD self
function CTLD:_GenerateFMFrequencies()
  self:T(self.lid .. " _GenerateFMrequencies")
    self.FreeFMFrequencies = {}
    self.FreeFMFrequencies = UTILS.GenerateFMFrequencies()
    return self
end

--- (Internal) Populate table with available VHF beacon frequencies.
-- @param #CTLD self
function CTLD:_GenerateVHFrequencies()
  self:T(self.lid .. " _GenerateVHFrequencies")
  self.FreeVHFFrequencies = {}
  self.UsedVHFFrequencies = {}
  self.FreeVHFFrequencies = UTILS.GenerateVHFrequencies()
  return self
end

--- (User) Set drop zone radius for troop drops in meters. Minimum distance is 25m for security reasons.
-- @param #CTLD self
-- @param #number Radius The radius to use.
function CTLD:SetTroopDropZoneRadius(Radius)
  self:T(self.lid .. " SetTroopDropZoneRadius")
  local tradius = Radius or 100
  if tradius < 25 then tradius = 25 end
  self.troopdropzoneradius = tradius
  return self
end

--- (User) Add a PLAYERTASK - FSM events will check success
-- @param #CTLD self
-- @param Ops.PlayerTask#PLAYERTASK PlayerTask
-- @return #CTLD self
function CTLD:AddPlayerTask(PlayerTask)
  self:T(self.lid .. " AddPlayerTask")
  if not self.PlayerTaskQueue then
    self.PlayerTaskQueue = FIFO:New()
  end
  self.PlayerTaskQueue:Push(PlayerTask,PlayerTask.PlayerTaskNr)
  return self
end

--- (Internal) Event handler function
-- @param #CTLD self
-- @param Core.Event#EVENTDATA EventData
function CTLD:_EventHandler(EventData)
  self:T(string.format("%s Event = %d",self.lid, EventData.id))
  local event = EventData -- Core.Event#EVENTDATA
  if event.id == EVENTS.PlayerEnterAircraft or event.id == EVENTS.PlayerEnterUnit then
    local _coalition = event.IniCoalition
    if _coalition ~= self.coalition then
        return --ignore!
    end
    local unitname = event.IniUnitName or "none"
    self.MenusDone[unitname] = nil
    -- check is Helicopter
    local _unit = event.IniUnit
    local _group = event.IniGroup
    if _unit:IsHelicopter() or _group:IsHelicopter() then
      local unitname = event.IniUnitName or "none"
      self.Loaded_Cargo[unitname] = nil
      self:_RefreshF10Menus()
    end
    -- Herc support
    if self:IsFixedWing(_unit) and self.enableFixedWing then
      local unitname = event.IniUnitName or "none"
      self.Loaded_Cargo[unitname] = nil
      self:_RefreshF10Menus()
    end
    -- CA support
    if _unit:IsGround() and self.allowCATransport then
      local unitname = event.IniUnitName or "none"
      self.Loaded_Cargo[unitname] = nil
      self:_RefreshF10Menus()
    end
    return
  elseif event.id == EVENTS.Land or event.id == EVENTS.Takeoff then
    local unitname = event.IniUnitName
    if self.CtldUnits[unitname] then
      local _group = event.IniGroup
      local _unit = event.IniUnit
      self:_RefreshLoadCratesMenu(_group, _unit)
    if self:IsFixedWing(_unit) and self.enableFixedWing then
      self:_RefreshDropCratesMenu(_group, _unit)
    end
    end
  elseif event.id == EVENTS.PlayerLeaveUnit or event.id == EVENTS.UnitLost then
    -- remove from pilot table
    local unitname = event.IniUnitName or "none"
    if self.CtldUnits[unitname] then
        local lostcargo = UTILS.DeepCopy(self.Loaded_Cargo[unitname] or {})
        self:__HelicopterLost(1,unitname,lostcargo)    
    end
    self.CtldUnits[unitname] = nil
    self.Loaded_Cargo[unitname] = nil
    self.MenusDone[unitname] = nil
  --elseif event.id == EVENTS.NewDynamicCargo and event.IniObjectCategory == 6 and string.match(event.IniUnitName,".+|%d%d:%d%d|PKG%d+") then
  elseif event.id == EVENTS.NewDynamicCargo then
    self:T(self.lid.."GC New Event "..event.IniDynamicCargoName)
    ---------------
    -- New dynamic cargo system Handling NEW
    --------------
    self.DynamicCargo[event.IniDynamicCargoName] = event.IniDynamicCargo
    ---------------
    -- End new dynamic cargo system Handling
    --------------
  elseif event.id == EVENTS.DynamicCargoLoaded then
    self:T(self.lid.."GC Loaded Event "..event.IniDynamicCargoName)
    ---------------
    -- New dynamic cargo system Handling LOADING
    --------------
    local dcargo = event.IniDynamicCargo -- Wrapper.DynamicCargo#DYNAMICCARGO
    -- get client/unit object
    local client = CLIENT:FindByPlayerName(dcargo.Owner)
    if client and client:IsAlive() then
      -- add to unit load list
      local unitname = client:GetName() or "none"
      local loaded = {}
      if self.Loaded_Cargo[unitname] then
        loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
      else
        loaded = {} -- #CTLD.LoadedCargo
        loaded.Troopsloaded = 0
        loaded.Cratesloaded = 0
        loaded.Cargo = {}
      end
      loaded.Cratesloaded = loaded.Cratesloaded+1
      table.insert(loaded.Cargo,dcargo)
      self.Loaded_Cargo[unitname] = nil
      self.Loaded_Cargo[unitname] = loaded
      local Group = client:GetGroup()
      self:_SendMessage(string.format("Crate %s loaded by ground crew!",event.IniDynamicCargoName), 10, false, Group)
      self:__CratesPickedUp(1, Group, client, dcargo)
    end
    ---------------
    -- End new dynamic cargo system Handling
    --------------
  elseif event.id == EVENTS.DynamicCargoUnloaded then
    self:T(self.lid.."GC Unload Event "..event.IniDynamicCargoName)
    ---------------
    -- New dynamic cargo system Handling UNLOADING
    --------------
    local dcargo = event.IniDynamicCargo -- Wrapper.DynamicCargo#DYNAMICCARGO
    -- get client/unit object
    local client = CLIENT:FindByPlayerName(dcargo.Owner)
    if client and client:IsAlive() then
      -- add to unit load list
      local unitname = client:GetName() or "none"
      local loaded = {}
      if self.Loaded_Cargo[unitname] then
        loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
        loaded.Cratesloaded = loaded.Cratesloaded - 1
        if loaded.Cratesloaded < 0  then loaded.Cratesloaded = 0 end
        -- TODO zap cargo from list
        local Loaded = {}
        for _,_item in pairs (loaded.Cargo or {}) do
          self:T(self.lid.."UNLOAD checking: ".._item:GetName())
          self:T(self.lid.."UNLOAD state: ".. tostring(_item:WasDropped()))
          if _item and _item:GetType() == CTLD_CARGO.Enum.GCLOADABLE and event.IniDynamicCargoName and event.IniDynamicCargoName ~= _item:GetName() and not _item:WasDropped() then
            table.insert(Loaded,_item)
          else
            table.insert(Loaded,_item)
          end 
        end
        loaded.Cargo = nil
        loaded.Cargo = Loaded
        self.Loaded_Cargo[unitname] = nil
        self.Loaded_Cargo[unitname] = loaded
      else
        loaded = {} -- #CTLD.LoadedCargo
        loaded.Troopsloaded = 0
        loaded.Cratesloaded = 0
        loaded.Cargo = {}
        self.Loaded_Cargo[unitname] = loaded
      end
      local Group = client:GetGroup()
      self:_SendMessage(string.format("Crate %s unloaded by ground crew!",event.IniDynamicCargoName), 10, false, Group) 
      self:__CratesDropped(1,Group,client,{dcargo})
    end
    ---------------
    -- End new dynamic cargo system Handling
    --------------
  elseif event.id == EVENTS.DynamicCargoRemoved then
    self:T(self.lid.."GC Remove Event "..event.IniDynamicCargoName)
    ---------------
    -- New dynamic cargo system Handling REMOVE
    --------------
    self.DynamicCargo[event.IniDynamicCargoName] = nil
    ---------------
    -- End new dynamic cargo system Handling
    --------------
  end
  return self
end

--- (Internal) Function to message a group.
-- @param #CTLD self
-- @param #string Text The text to display.
-- @param #number Time Number of seconds to display the message.
-- @param #boolean Clearscreen Clear screen or not.
-- @param Wrapper.Group#GROUP Group The group receiving the message.
function CTLD:_SendMessage(Text, Time, Clearscreen, Group)
  self:T(self.lid .. " _SendMessage")
  if not self.suppressmessages then
    local m = MESSAGE:New(Text,Time,"CTLD",Clearscreen):ToGroup(Group)
  end 
  return self
end

--- (Internal) Find a troops CTLD_CARGO object in stock
-- @param #CTLD self
-- @param #string Name of the object
-- @return #CTLD_CARGO Cargo object, nil if it cannot be found
function CTLD:_FindTroopsCargoObject(Name)
  self:T(self.lid .. " _FindTroopsCargoObject")
  local cargo = nil
  for _,_cargo in pairs(self.Cargo_Troops)do
    local cargo = _cargo -- #CTLD_CARGO
    if cargo.Name == Name then
      return cargo
    end
  end
  return nil
end

--- (Internal) Find a crates CTLD_CARGO object in stock
-- @param #CTLD self
-- @param #string Name of the object
-- @return #CTLD_CARGO Cargo object, nil if it cannot be found
function CTLD:_FindCratesCargoObject(Name)
  self:T(self.lid .. " _FindCratesCargoObject")
  local cargo = nil
  for _,_cargo in pairs(self.Cargo_Crates)do
    local cargo = _cargo -- #CTLD_CARGO
    if cargo.Name == Name then
      return cargo
    end
  end
  return nil
end

--- (User) Add a new fixed wing type to the list of allowed types.
-- @param #CTLD self
-- @param #string typename The typename to add. Can be handed as Wrapper.Unit#UNIT object. Do NOT forget to `myctld:SetUnitCapabilities()` for this type!
-- @return #CTLD self
function CTLD:AddAllowedFixedWingType(typename)
  if type(typename) == "string" then
    self.FixedWingTypes[typename] = typename
  elseif typename and typename.ClassName and typename:IsInstanceOf("UNIT") then
    local TypeName = typename:GetTypeName() or "none"
    self.FixedWingTypes[TypeName] = TypeName
  else
    self:E(self.lid.."No valid typename or no UNIT handed!")
  end
  return self
end

--- (User) Pre-load troops into a helo, e.g. for airstart. Unit **must** be alive in-game, i.e. player has taken the slot!
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit The unit to load into, can be handed as Wrapper.Client#CLIENT object
-- @param #string Troopname The name of the Troops to be loaded. Must be created prior in the CTLD setup!
-- @return #CTLD self
-- @usage
--          local client = UNIT:FindByName("Helo-1-1")
--          if client and client:IsAlive() then
--            myctld:PreloadTroops(client,"Infantry")
--          end
function CTLD:PreloadTroops(Unit,Troopname)
  self:T(self.lid .. " PreloadTroops")
  local name = Troopname or "Unknown"
  if Unit and Unit:IsAlive() then
    local cargo = self:_FindTroopsCargoObject(name)
    local group = Unit:GetGroup()
    if cargo then
      self:_LoadTroops(group,Unit,cargo,true)
    else
      self:E(self.lid.." Troops preload - Cargo Object "..name.." not found!")
    end
  end
  return self
end

--- (Internal) Pre-load crates into a helo. Do not use standalone!
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The group to load into, can be handed as Wrapper.Client#CLIENT object
-- @param Wrapper.Unit#UNIT Unit The unit to load into, can be handed as Wrapper.Client#CLIENT object
-- @param #CTLD_CARGO Cargo The Cargo crate object to load
-- @param #number NumberOfCrates (Optional) Number of crates to be loaded. Default - all necessary to build this object. Might overload the helo!
-- @return #CTLD self
function CTLD:_PreloadCrates(Group, Unit, Cargo, NumberOfCrates)
    -- load crate into heli
  local group = Group -- Wrapper.Group#GROUP
  local unit = Unit -- Wrapper.Unit#UNIT
  local unitname = unit:GetName()
  -- see if this heli can load crates
  local unittype = unit:GetTypeName()
  local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
  local cancrates = capabilities.crates -- #boolean
  local cratelimit = capabilities.cratelimit -- #number
  if not cancrates then
    self:_SendMessage("Sorry this chopper cannot carry crates!", 10, false, Group) 
    return self
  else
    -- have we loaded stuff already?
    local numberonboard = 0
    local massonboard = 0
    local loaded = {}
    if self.Loaded_Cargo[unitname] then
      loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
      numberonboard = loaded.Cratesloaded or 0
      massonboard = self:_GetUnitCargoMass(Unit)
    else
      loaded = {} -- #CTLD.LoadedCargo
      loaded.Troopsloaded = 0
      loaded.Cratesloaded = 0
      loaded.Cargo = {}
    end
    local crate = Cargo -- #CTLD_CARGO
    local numbercrates = NumberOfCrates or crate:GetCratesNeeded()
    for i=1,numbercrates do
      loaded.Cratesloaded = loaded.Cratesloaded + 1
      crate:SetHasMoved(true)
      crate:SetWasDropped(false)
      table.insert(loaded.Cargo, crate)
      crate.Positionable = nil
      self:_SendMessage(string.format("Crate ID %d for %s loaded!",crate:GetID(),crate:GetName()), 10, false, Group)
      --self:__CratesPickedUp(1, Group, Unit, crate)
      self.Loaded_Cargo[unitname] = loaded
      self:_UpdateUnitCargoMass(Unit)
    end 
  end
  return self
end

--- (User) Pre-load crates into a helo, e.g. for airstart. Unit **must** be alive in-game, i.e. player has taken the slot!
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit The unit to load into, can be handed as Wrapper.Client#CLIENT object
-- @param #string Cratesname The name of the cargo to be loaded. Must be created prior in the CTLD setup!
-- @param #number NumberOfCrates (Optional) Number of crates to be loaded. Default - all necessary to build this object. Might overload the helo!
-- @return #CTLD self
-- @usage
--          local client = UNIT:FindByName("Helo-1-1")
--          if client and client:IsAlive() then
--            myctld:PreloadCrates(client,"Humvee")
--          end
function CTLD:PreloadCrates(Unit,Cratesname,NumberOfCrates)
  self:T(self.lid .. " PreloadCrates")
  local name = Cratesname or "Unknown"
  if Unit and Unit:IsAlive() then
    local cargo = self:_FindCratesCargoObject(name)
    local group = Unit:GetGroup()
    if cargo then
      self:_PreloadCrates(group,Unit,cargo,NumberOfCrates)
    else
      self:E(self.lid.." Crates preload - Cargo Object "..name.." not found!")
    end
  end
  return self
end

--- (Internal) Function to load troops into a heli.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @param #CTLD_CARGO Cargotype
-- @param #boolean Inject
function CTLD:_LoadTroops(Group, Unit, Cargotype, Inject)
  self:T(self.lid .. " _LoadTroops")
  -- check if we have stock
  local instock = Cargotype:GetStock()
  local cgoname = Cargotype:GetName()
  local cgotype = Cargotype:GetType()
  local cgonetmass = Cargotype:GetNetMass()
  local maxloadable = self:_GetMaxLoadableMass(Unit)
  if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 and not Inject then
    -- nothing left over
    self:_SendMessage(string.format("Sorry, all %s are gone!", cgoname), 10, false, Group)
    return self
  end
  -- landed or hovering over load zone?
  local grounded = not self:IsUnitInAir(Unit)
  local hoverload = self:CanHoverLoad(Unit)
  -- check if we are in LOAD zone
  local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD)
  if not inzone then
    inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP)
  end
  if not Inject then
    if not inzone then
      self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group)
      if not self.debug then return self end
    elseif not grounded and not hoverload then
      self:_SendMessage("You need to land or hover in position to load!", 10, false, Group)
      if not self.debug then return self end
    elseif self.pilotmustopendoors and not  UTILS.IsLoadingDoorOpen(Unit:GetName()) then
      self:_SendMessage("You need to open the door(s) to load troops!", 10, false, Group)
      if not self.debug then return self end  
    end
  end
  -- load troops into heli
  local group = Group -- Wrapper.Group#GROUP
  local unit = Unit -- Wrapper.Unit#UNIT
  local unitname = unit:GetName()
  local cargotype = Cargotype -- #CTLD_CARGO
  local cratename = cargotype:GetName() -- #string
  -- see if this heli can load troops
  local unittype = unit:GetTypeName()
  local capabilities = self:_GetUnitCapabilities(Unit)
  local cantroops = capabilities.troops -- #boolean
  local trooplimit = capabilities.trooplimit -- #number
  local troopsize = cargotype:GetCratesNeeded() -- #number
  -- have we loaded stuff already?
  local numberonboard = 0
  local loaded = {}
  if self.Loaded_Cargo[unitname] then
    loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
    numberonboard = loaded.Troopsloaded or 0
  else
    loaded = {} -- #CTLD.LoadedCargo
    loaded.Troopsloaded = 0
    loaded.Cratesloaded = 0
    loaded.Cargo = {}
  end
  if troopsize + numberonboard > trooplimit then
    self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group)
    return
  elseif maxloadable < cgonetmass then
    self:_SendMessage("Sorry, that\'s too heavy to load!", 10, false, Group)
    return
  else
    self.CargoCounter = self.CargoCounter + 1
    local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, cgotype, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass)
    self:T({cargotype=loadcargotype})
    loaded.Troopsloaded = loaded.Troopsloaded + troopsize
    table.insert(loaded.Cargo,loadcargotype)
    self.Loaded_Cargo[unitname] = loaded
    self:_SendMessage(string.format("%s boarded!", cgoname), 10, false, Group)
    self:_RefreshDropTroopsMenu(Group,Unit)
    self:__TroopsPickedUp(1,Group, Unit, Cargotype)
    self:_UpdateUnitCargoMass(Unit)
    Cargotype:RemoveStock()
  end
  return self
end

function CTLD:_FindRepairNearby(Group, Unit, Repairtype)
    self:T(self.lid .. " _FindRepairNearby")
    --self:T({Group:GetName(),Unit:GetName(),Repairtype})
    local unitcoord = Unit:GetCoordinate()
    
    -- find nearest group of deployed groups
    local nearestGroup = nil
    local nearestGroupIndex = -1
    local nearestDistance = 10000
    for k,v in pairs(self.DroppedTroops) do
      local distance = self:_GetDistance(v:GetCoordinate(),unitcoord)
      local unit = v:GetUnit(1) -- Wrapper.Unit#UNIT
      local desc = unit:GetDesc() or nil
      if distance < nearestDistance and distance ~= -1 and not desc.attributes.Infantry then
        nearestGroup = v
        nearestGroupIndex = k
        nearestDistance = distance
      end
    end
    
    --self:T("Distance: ".. nearestDistance)
    
    -- found one and matching distance?  
    if nearestGroup == nil or nearestDistance > self.EngineerSearch then
      self:_SendMessage("No unit close enough to repair!", 10, false, Group)
      return nil, nil
    end
    
    local groupname = nearestGroup:GetName()
    
    -- helper to find matching template
    local function matchstring(String,Table)
      local match = false
      String = string.gsub(String,"-"," ")
      if type(Table) == "table" then       
        for _,_name in pairs (Table) do
          _name = string.gsub(_name,"-"," ")
          if string.find(String,_name) then
            match = true
            break
          end
        end
      else
        if type(String) == "string" then
          Table = string.gsub(Table,"-"," ")
          if string.find(String,Table) then match = true end
        end
      end 
      return match
    end
    
    -- walk through generics and find matching type
    local Cargotype = nil
    for k,v in pairs(self.Cargo_Crates) do
      --self:T({groupname,v.Templates,Repairtype})
      if matchstring(groupname,v.Templates) and matchstring(groupname,Repairtype) then
        Cargotype = v -- #CTLD_CARGO
        break
      end
    end

    if Cargotype == nil then
      return nil, nil
    else
      --self:T({groupname,Cargotype})
      return nearestGroup, Cargotype
    end
    
end

--- (Internal) Function to repair an object.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @param #table Crates Table of #CTLD_CARGO objects near the unit.
-- @param #CTLD.Buildable Build Table build object.
-- @param #number Number Number of objects in Crates (found) to limit search.
-- @param #boolean Engineering If true it is an Engineering repair.
function CTLD:_RepairObjectFromCrates(Group,Unit,Crates,Build,Number,Engineering)
  self:T(self.lid .. " _RepairObjectFromCrates") 
  local build = Build -- -- #CTLD.Buildable
  local Repairtype = build.Template -- #string
  local NearestGroup, CargoType = self:_FindRepairNearby(Group,Unit,Repairtype) -- Wrapper.Group#GROUP, #CTLD_CARGO
  if NearestGroup ~= nil then
    if self.repairtime < 2 then self.repairtime = 30 end -- noob catch
    if not Engineering then
      self:_SendMessage(string.format("Repair started using %s taking %d secs", build.Name, self.repairtime), 10, false, Group)
    end
    -- now we can build ....
    local name = CargoType:GetName()
    local required = CargoType:GetCratesNeeded()
    local template = CargoType:GetTemplates()
    local ctype = CargoType:GetType()
    local object = {} -- #CTLD.Buildable
    object.Name = CargoType:GetName()
    object.Required = required
    object.Found = required
    object.Template = template
    object.CanBuild = true
    object.Type = ctype -- #CTLD_CARGO.Enum
    self:_CleanUpCrates(Crates,Build,Number)
    local desttimer = TIMER:New(function() NearestGroup:Destroy(false) end, self)
    desttimer:Start(self.repairtime - 1)
    local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,object,true,NearestGroup:GetCoordinate())
    buildtimer:Start(self.repairtime)
    self:__CratesRepairStarted(1,Group,Unit)
  else
    if not Engineering then
      self:_SendMessage("Can't repair this unit with " .. build.Name, 10, false, Group)
    else
      self:T("Can't repair this unit with " .. build.Name)
    end
  end
  return self
end

  --- (Internal) Function to extract (load from the field) troops into a heli.
  -- @param #CTLD self
  -- @param Wrapper.Group#GROUP Group
  -- @param Wrapper.Unit#UNIT Unit
  function CTLD:_ExtractTroops(Group, Unit) -- #1574 thanks to @bbirchnz!
    self:T(self.lid .. " _ExtractTroops")
    -- landed or hovering over load zone?
    local grounded = not self:IsUnitInAir(Unit)
    local hoverload = self:CanHoverLoad(Unit)
    local hassecondaries = false
    
    if not grounded and not hoverload then
      self:_SendMessage("You need to land or hover in position to load!", 10, false, Group)
      if not self.debug then return self end
    end
    if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
      self:_SendMessage("You need to open the door(s) to extract troops!", 10, false, Group)
      if not self.debug then return self end 
    end
    -- load troops into heli
    local unit = Unit -- Wrapper.Unit#UNIT
    local unitname = unit:GetName()
    -- see if this heli can load troops
    local unittype = unit:GetTypeName()
    local capabilities = self:_GetUnitCapabilities(Unit)
    local cantroops = capabilities.troops -- #boolean
    local trooplimit = capabilities.trooplimit -- #number
    local unitcoord = unit:GetCoordinate()
    
    -- find nearest group of deployed troops
    local nearestGroup = nil
    local nearestGroupIndex = -1
    local nearestDistance = 10000000
    local maxdistance = 0
    local nearestList = {}
    local distancekeys = {}
    local extractdistance = self.CrateDistance * self.ExtractFactor
    for k,v in pairs(self.DroppedTroops) do
      local distance = self:_GetDistance(v:GetCoordinate(),unitcoord)
      local TNow = timer.getTime()
      local vtime = v.ExtractTime or TNow-310
      if distance <= extractdistance and distance ~= -1 and (TNow - vtime > 300) then
        nearestGroup = v
        nearestGroupIndex = k
        nearestDistance = distance
        if math.floor(distance) > maxdistance then maxdistance = math.floor(distance) end
        if nearestList[math.floor(distance)] then 
          distance = maxdistance+1
          maxdistance = distance 
        end
        table.insert(nearestList, math.floor(distance), v)
        distancekeys[#distancekeys+1] = math.floor(distance)
        --self:I(string.format("Adding group %s distance %dm",nearestGroup:GetName(),distance))
      end
    end
    
    if nearestGroup == nil or nearestDistance > extractdistance then
      self:_SendMessage("No units close enough to extract!", 10, false, Group)
      return self
    end
    
    -- sort reference keys
    table.sort(distancekeys)
    
    local secondarygroups = {}
    
    for i=1,#distancekeys do
      local nearestGroup = nearestList[distancekeys[i]] -- Wrapper.Group#GROUP
          -- find matching cargo type
      local groupType = string.match(nearestGroup:GetName(), "(.+)-(.+)$")
      local Cargotype = nil
      for k,v in pairs(self.Cargo_Troops) do
        local comparison = ""
        if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end
        if comparison == groupType then
          Cargotype = v
          break
        end
      end
      if Cargotype == nil then
        self:_SendMessage("Can't onboard " .. groupType, 10, false, Group)
      else
      
        local troopsize = Cargotype:GetCratesNeeded() -- #number
        -- have we loaded stuff already?
        local numberonboard = 0
        local loaded = {}
        if self.Loaded_Cargo[unitname] then
          loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
          numberonboard = loaded.Troopsloaded or 0
        else
          loaded = {} -- #CTLD.LoadedCargo
          loaded.Troopsloaded = 0
          loaded.Cratesloaded = 0
          loaded.Cargo = {}
        end
        if troopsize + numberonboard > trooplimit then
          self:_SendMessage("Sorry, we\'re crammed already!", 10, false, Group)
          nearestGroup.ExtractTime = 0
          --return self
        else
          self.CargoCounter = self.CargoCounter + 1
          nearestGroup.ExtractTime = timer.getTime()
          local loadcargotype = CTLD_CARGO:New(self.CargoCounter, Cargotype.Name, Cargotype.Templates, Cargotype.CargoType, true, true, Cargotype.CratesNeeded,nil,nil,Cargotype.PerCrateMass)
          self:T({cargotype=loadcargotype})
          local running = math.floor(nearestDistance / 4)+20 -- time run to helo plus boarding
          loaded.Troopsloaded = loaded.Troopsloaded + troopsize
          table.insert(loaded.Cargo,loadcargotype)
          self.Loaded_Cargo[unitname] = loaded
          self:ScheduleOnce(running, self._SendMessage, self, string.format("%s boarded!", Cargotype.Name), 10, false, Group)
          self:_SendMessage(string.format("%s boarding!", Cargotype.Name), 10, false, Group)
          self:_RefreshDropTroopsMenu(Group,Unit)
          self:_UpdateUnitCargoMass(Unit)
          local groupname = nearestGroup:GetName()
          self:__TroopsExtracted(running,Group, Unit, nearestGroup, groupname)
          local coord = Unit:GetCoordinate() or Group:GetCoordinate() -- Core.Point#COORDINATE
          local Point
          if coord then
            local heading = unit:GetHeading() or 0
            local Angle = math.floor((heading+160)%360)
            Point = coord:Translate(8,Angle):GetVec2()
            if Point then
              nearestGroup:RouteToVec2(Point,5)
            end
          end
          -- clean up:
          hassecondaries = false
          if type(Cargotype.Templates) == "table" and Cargotype.Templates[2] then
            for _,_key in pairs (Cargotype.Templates) do
              table.insert(secondarygroups,_key)
              hassecondaries = true
            end
          end
          local destroytimer = math.random(10,20)
          --self:I("Destroying Group "..nearestGroup:GetName().." in "..destroytimer.." seconds!")
          nearestGroup:Destroy(false,destroytimer)
        end
      end
    end
    -- clean up secondary groups
    if hassecondaries == true then
      for _,_name in pairs(secondarygroups) do
        for _,_group in pairs(nearestList) do
          if _group and _group:IsAlive() then
            local groupname = string.match(_group:GetName(), "(.+)-(.+)$")
            if _name == groupname then
              _group:Destroy(false,15)
            end
          end
        end
      end
    end
    self:CleanDroppedTroops()
    return self
  end
  
--- (Internal) Function to spawn crates in front of the heli.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @param #CTLD_CARGO Cargo
-- @param #number number Number of crates to generate (for dropping)
-- @param #boolean drop If true we\'re dropping from heli rather than loading.
-- @param #boolean pack If true we\'re packing crates from a template rather than loading or dropping
function CTLD:_GetCrates(Group, Unit, Cargo, number, drop, pack)
  self:T(self.lid .. " _GetCrates")
  if not drop and not pack then
    local cgoname = Cargo:GetName()
    -- check if we have stock
    local instock = Cargo:GetStock()
    if type(instock) == "number" and tonumber(instock) <= 0 and tonumber(instock) ~= -1 then
      -- nothing left over
      self:_SendMessage(string.format("Sorry, we ran out of %s", cgoname), 10, false, Group)
      return self
    end
  end
  -- check if we are in LOAD zone
  local inzone = false 
  local drop = drop or false
  local ship = nil
  local width = 20
  local distance = nil
  local zone = nil
  if not drop and not pack then 
    inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD)
    if not inzone then
---@diagnostic disable-next-line: cast-local-type
      inzone, ship, zone, distance, width  = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP)
    end
  elseif drop and not pack then
    if self.dropcratesanywhere then -- #1570
      inzone = true
    else
      inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP)
    end
  elseif pack and not drop then
    inzone = true
  end
  
  if not inzone then
    self:_SendMessage("You are not close enough to a logistics zone!", 10, false, Group)
    if not self.debug then return self end
  end
  
  -- Check cargo location if available
  local location = Cargo:GetLocation()
  
  if location then
    local unitcoord = Unit:GetCoordinate() or Group:GetCoordinate()
    if unitcoord then
      if not location:IsCoordinateInZone(unitcoord) then
        -- no we're not at the right spot
        self:_SendMessage("The requested cargo is not available in this zone!", 10, false, Group)
        if not self.debug then return self end
      end
    end
  end
  
  -- avoid crate spam
  local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
  local canloadcratesno = capabilities.cratelimit
  local loaddist = self.CrateDistance or 35
  local nearcrates, numbernearby = self:_FindCratesNearby(Group,Unit,loaddist,true,true)
  if numbernearby >= canloadcratesno and not drop then
    self:_SendMessage("There are enough crates nearby already! Take care of those first!", 10, false, Group)
    return self
  end
  -- spawn crates in front of helicopter
  local IsHerc = self:IsFixedWing(Unit) -- Herc, Bronco and Hook load from behind
  local IsHook = self:IsHook(Unit) -- Herc, Bronco and Hook load from behind
  local IsTruck = Unit:IsGround()
  local cargotype = Cargo -- Ops.CTLD#CTLD_CARGO
  local number = number or cargotype:GetCratesNeeded() --#number
  local cratesneeded = cargotype:GetCratesNeeded() --#number
  local cratename = cargotype:GetName()
  local cratetemplate = "Container"-- #string
  local cgotype = cargotype:GetType()
  local cgomass = cargotype:GetMass()
  local isstatic = false
  if cgotype == CTLD_CARGO.Enum.STATIC then
    cratetemplate = cargotype:GetTemplates()
    isstatic = true
  end
  -- get position and heading of heli
  local position = Unit:GetCoordinate()
  local heading = Unit:GetHeading() + 1
  local height = Unit:GetHeight()
  local droppedcargo = {}
  local cratedistance = 0
  local rheading = 0
  local angleOffNose = 0
  local addon = 0
  if IsHerc or IsHook or IsTruck then 
    -- spawn behind the Herc
    addon = 180
  end
  heading = (heading+addon)%360
  local row = 1
  local column = 1
  local initialdist = IsHerc and 16 or (capabilities.length+2) -- initial spacing of the first crates
  local startpos = position:Translate(initialdist,heading)
  if self.placeCratesAhead == true then
    cratedistance = initialdist
  end
  -- loop crates needed
  local cratecoord = nil -- Core.Point#COORDINATE
  for i=1,number do
    local cratealias = string.format("%s-%s-%d", cratename, cratetemplate, math.random(1,100000))
    if not self.placeCratesAhead or drop == true then
      cratedistance = (i-1)*2.5 + capabilities.length
      if cratedistance > self.CrateDistance then cratedistance = self.CrateDistance end
      -- altered heading logic
      -- DONE: right standard deviation?
        if self:IsUnitInAir(Unit) and self:IsFixedWing(Unit) then
          rheading = math.random(20,60)
        else
          rheading = UTILS.RandomGaussian(0, 30, -90, 90, 100)
        end
      rheading=math.fmod((heading+rheading),360)
      cratecoord = position:Translate(cratedistance,rheading)
    else
      cratedistance = (row-1)*6
      rheading = 90
      row = row+1
      cratecoord = startpos:Translate(cratedistance,rheading)
      if row > 4 then
        row = 1
        startpos:Translate(6,heading,nil,true)
      end
    end
    
    --local cratevec2 = cratecoord:GetVec2()
    self.CrateCounter = self.CrateCounter + 1
    local CCat, CType, CShape = Cargo:GetStaticTypeAndShape()
    local basetype = CType or self.basetype or "container_cargo"
    CCat = CCat or "Cargos"
    if isstatic then
      basetype = cratetemplate
    end
    if type(ship) == "string" then
      self:T("Spawning on ship "..ship)
      local Ship = UNIT:FindByName(ship)
      local shipcoord = Ship:GetCoordinate()
      local unitcoord = Unit:GetCoordinate()
      local dist = shipcoord:Get2DDistance(unitcoord)
      dist = dist - (20 + math.random(1,10))
      local width = width / 2
      local Offy = math.random(-width,width)
      local spawnstatic = SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry)
      :InitCargoMass(cgomass)
      :InitCargo(self.enableslingload)
      :InitLinkToUnit(Ship,dist,Offy,0)
      if CShape then
        spawnstatic:InitShape(CShape)
      end 
      if isstatic then
        local map=cargotype:GetStaticResourceMap()
        spawnstatic.TemplateStaticUnit.resourcePayload = map
      end
      self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(270,cratealias)
    else   
      local spawnstatic = SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry)
        :InitCoordinate(cratecoord)
        :InitCargoMass(cgomass)
        :InitCargo(self.enableslingload)
      if CShape then
        spawnstatic:InitShape(CShape)
      end 
      if isstatic then
        local map=cargotype:GetStaticResourceMap()
        spawnstatic.TemplateStaticUnit.resourcePayload = map
      end
      self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(270,cratealias)
    end
    local templ = cargotype:GetTemplates()
    local sorte = cargotype:GetType()
    local subcat = cargotype.Subcategory
    self.CargoCounter = self.CargoCounter + 1
    local realcargo = nil
    if drop then
                --CTLD_CARGO:New(ID, Name, Templates, Sorte, HasBeenMoved, LoadDirectly, CratesNeeded, Positionable, Dropped, PerCrateMass, Stock, Subcategory)
      realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,true,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],true,cargotype.PerCrateMass,nil,subcat) -- #CTLD_CARGO
      local map=cargotype:GetStaticResourceMap()
      realcargo:SetStaticResourceMap(map)
      local CCat, CType, CShape = cargotype:GetStaticTypeAndShape()
      realcargo:SetStaticTypeAndShape(CCat,CType,CShape)
      if cargotype.TypeNames then
        realcargo.TypeNames = UTILS.DeepCopy(cargotype.TypeNames)
      end
      table.insert(droppedcargo,realcargo)
    else
      realcargo = CTLD_CARGO:New(self.CargoCounter,cratename,templ,sorte,false,false,cratesneeded,self.Spawned_Crates[self.CrateCounter],false,cargotype.PerCrateMass,nil,subcat)
      local map=cargotype:GetStaticResourceMap()
      realcargo:SetStaticResourceMap(map) 
      if cargotype.TypeNames then
        realcargo.TypeNames = UTILS.DeepCopy(cargotype.TypeNames)
      end   
    end
    local CCat, CType, CShape = cargotype:GetStaticTypeAndShape()
    realcargo:SetStaticTypeAndShape(CCat,CType,CShape)
    table.insert(self.Spawned_Cargo, realcargo)
  end
  if not (drop or pack) then
    Cargo:RemoveStock()
  end
  local text = string.format("Crates for %s have been positioned near you!",cratename)
  if drop then
    text = string.format("Crates for %s have been dropped!",cratename)
    self:__CratesDropped(1, Group, Unit, droppedcargo)
  else
    self:_SendMessage(text, 10, false, Group)
  end
  self:_RefreshLoadCratesMenu(Group, Unit)
  return self
end

--- (Internal) Inject crates and static cargo objects.
-- @param #CTLD self
-- @param Core.Zone#ZONE Zone Zone to spawn in.
-- @param #CTLD_CARGO Cargo The cargo type to spawn.
-- @param #boolean RandomCoord Randomize coordinate.
-- @param #boolean FromLoad Create only **one** crate per cargo type, as we are re-creating dropped crates that CTLD has saved prior.
-- @return #CTLD self
function CTLD:InjectStatics(Zone, Cargo, RandomCoord, FromLoad)
  self:T(self.lid .. " InjectStatics")
  local cratecoord = Zone:GetCoordinate()
  if RandomCoord then
    cratecoord = Zone:GetRandomCoordinate(5,20)
  end
  local surface = cratecoord:GetSurfaceType()
  if surface == land.SurfaceType.WATER then
    return self
  end
  local cargotype = Cargo -- #CTLD_CARGO
  --local number = 1
  local cratesneeded = cargotype:GetCratesNeeded() --#number
  local cratetemplate = "Container"-- #string
  local cratename = cargotype:GetName()
  local cgotype = cargotype:GetType()
  local cgomass = cargotype:GetMass()
  local cratenumber = cargotype:GetCratesNeeded() or 1
  if FromLoad == true then cratenumber=1 end
  for i=1,cratenumber do
    local cratealias = string.format("%s-%s-%d", cratename, cratetemplate, math.random(1,100000))
    local isstatic = false
    if cgotype == CTLD_CARGO.Enum.STATIC then
      cratetemplate = cargotype:GetTemplates()
      isstatic = true
    end
    local CCat,CType,CShape = cargotype:GetStaticTypeAndShape()
    local basetype = CType or self.basetype or "container_cargo"
    CCat = CCat or "Cargos"
    if isstatic then
      basetype = cratetemplate
    end
    self.CrateCounter = self.CrateCounter + 1
    local spawnstatic = SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry)
      :InitCargoMass(cgomass)
      :InitCargo(self.enableslingload)
      :InitCoordinate(cratecoord)
    if CShape then
      spawnstatic:InitShape(CShape)
    end
    if isstatic then
      local map = cargotype:GetStaticResourceMap()
      spawnstatic.TemplateStaticUnit.resourcePayload = map
    end
    self.Spawned_Crates[self.CrateCounter] = spawnstatic:Spawn(270,cratealias)
    local templ = cargotype:GetTemplates()
    local sorte = cargotype:GetType()
    cargotype.Positionable = self.Spawned_Crates[self.CrateCounter]
    table.insert(self.Spawned_Cargo, cargotype)
  end
  return self
end

--- (User) Inject static cargo objects.
-- @param #CTLD self
-- @param Core.Zone#ZONE Zone Zone to spawn in. Will be a somewhat random coordinate.
-- @param #string Template Unit(!) name of the static cargo object to be used as template.
-- @param #number Mass Mass of the static in kg.
-- @return #CTLD self
function CTLD:InjectStaticFromTemplate(Zone, Template, Mass)
  self:T(self.lid .. " InjectStaticFromTemplate")
  local cargotype = self:GetStaticsCargoFromTemplate(Template,Mass) -- #CTLD_CARGO
  self:InjectStatics(Zone,cargotype,true,true)
  return self
end

--- (Internal) Function to find and list nearby crates.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @return #CTLD self
function CTLD:_ListCratesNearby( _group, _unit)
  self:T(self.lid .. " _ListCratesNearby")
  local finddist = self.CrateDistance or 35
  local crates,number,loadedbygc,indexgc = self:_FindCratesNearby(_group,_unit, finddist,true,true) -- #table
  if number > 0 or indexgc > 0 then
    local text = REPORT:New("Crates Found Nearby:")
    text:Add("------------------------------------------------------------")
    for _,_entry in pairs (crates) do
      local entry = _entry -- #CTLD_CARGO
      local name = entry:GetName() --#string
      local dropped = entry:WasDropped()
      if dropped then
        text:Add(string.format("Dropped crate for %s, %dkg",name, entry.PerCrateMass))
      else
        text:Add(string.format("Crate for %s, %dkg",name, entry.PerCrateMass))
      end
    end
    if text:GetCount() == 1 then
    text:Add("        N O N E")
    end
    text:Add("------------------------------------------------------------")
    if indexgc > 0 then
      text:Add("Probably ground crew loadable (F8)")
      for _,_entry in pairs (loadedbygc) do
        local entry = _entry -- #CTLD_CARGO
        local name = entry:GetName() --#string
        local dropped = entry:WasDropped()
        if dropped then
          text:Add(string.format("Dropped crate for %s, %dkg",name, entry.PerCrateMass))
        else
          text:Add(string.format("Crate for %s, %dkg",name, entry.PerCrateMass))
        end
      end
    end
    self:_SendMessage(text:Text(), 30, true, _group) 
  else
    self:_SendMessage(string.format("No (loadable) crates within %d meters!",finddist), 10, false, _group) 
  end
  return self
end

-- (Internal) Function to find and Remove nearby crates.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @return #CTLD self
function CTLD:_RemoveCratesNearby(_group, _unit)
  self:T(self.lid.." _RemoveCratesNearby")
  local finddist=self.CrateDistance or 35
  local crates,number=self:_FindCratesNearby(_group,_unit,finddist,true,true)
  if number>0 then
    local removedIDs={}
    local text=REPORT:New("Removing Crates Found Nearby:")
    text:Add("------------------------------------------------------------")
    for _,_entry in pairs(crates)do
      local entry=_entry
      local name=entry:GetName()or"none"
      text:Add(string.format("Crate for %s, %dkg removed",name,entry.PerCrateMass))
      if entry:GetPositionable()then
        entry:GetPositionable():Destroy(false)
      end
      table.insert(removedIDs,entry:GetID())
    end
    if text:GetCount()==1 then
      text:Add("        N O N E")
    end
    text:Add("------------------------------------------------------------")
    self:_SendMessage(text:Text(),30,true,_group)
    self:_CleanupTrackedCrates(removedIDs)
    self:_RefreshLoadCratesMenu(_group,_unit)
  else
    self:_SendMessage(string.format("No (loadable) crates within %d meters!",finddist),10,false,_group)
  end
  return self
end

--- (Internal) Return distance in meters between two coordinates.
-- @param #CTLD self
-- @param Core.Point#COORDINATE _point1 Coordinate one
-- @param Core.Point#COORDINATE _point2 Coordinate two
-- @return #number Distance in meters
function CTLD:_GetDistance(_point1, _point2)
  self:T(self.lid .. " _GetDistance")
  if _point1 and _point2 then
    local distance1 = _point1:Get2DDistance(_point2)
    local distance2 = _point1:DistanceFromPointVec2(_point2)
    if distance1 and type(distance1) == "number" then
      return distance1
    elseif distance2 and type(distance2) == "number" then
      return distance2
    else
      self:E("*****Cannot calculate distance!")
      self:E({_point1,_point2})
      return -1
    end
  else
    self:E("******Cannot calculate distance!")
    self:E({_point1,_point2})
    return -1
  end
end

--- (Internal) Function to find and return nearby crates.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP _group Group
-- @param Wrapper.Unit#UNIT _unit Unit
-- @param #number _dist Distance
-- @param #boolean _ignoreweight Find everything in range, ignore loadable weight
-- @param #boolean ignoretype Find everything in range, ignore loadable type name
-- @return #table Crates Table of crates
-- @return #number Number Number of crates found
-- @return #table CratesGC Table of crates possibly loaded by GC
-- @return #number NumberGC Number of crates possibly loaded by GC
function CTLD:_FindCratesNearby( _group, _unit, _dist, _ignoreweight, ignoretype)
  self:T(self.lid .. " _FindCratesNearby")
  local finddist = _dist
  local location = _group:GetCoordinate()
  local existingcrates = self.Spawned_Cargo -- #table
  -- cycle
  local index = 0
  local indexg = 0
  local found = {}
  local LoadedbyGC = {}
  local loadedmass = 0
  local unittype = "none"
  local capabilities = {}
  --local maxmass = 2000
  local maxloadable = 2000
  local IsHook = self:IsHook(_unit)
  if not _ignoreweight then
    maxloadable = self:_GetMaxLoadableMass(_unit)
  end
  self:T(self.lid .. " Max loadable mass: " .. maxloadable)
  for _,_cargoobject in pairs (existingcrates) do
    local cargo = _cargoobject -- #CTLD_CARGO
    local static = cargo:GetPositionable() -- Wrapper.Static#STATIC -- crates
    local weight = cargo:GetMass() -- weight in kgs of this cargo
    local staticid = cargo:GetID()
    self:T(self.lid .. " Found cargo mass: " .. weight)
    if static and static:IsAlive() then --or cargoalive) then
      local restricthooktononstatics = self.enableChinookGCLoading and IsHook
      self:T(self.lid .. " restricthooktononstatics: " .. tostring(restricthooktononstatics))
      local cargoisstatic = cargo:GetType() == CTLD_CARGO.Enum.STATIC and true or false
      self:T(self.lid .. " Cargo is static: " .. tostring(cargoisstatic))
      local restricted = cargoisstatic and restricthooktononstatics
      self:T(self.lid .. " Loading restricted: " .. tostring(restricted))
      local staticpos = static:GetCoordinate() --or dcsunitpos
      local cando = cargo:UnitCanCarry(_unit)
      if ignoretype == true then cando = true end
      self:T(self.lid .. " Unit can carry: " .. tostring(cando))
      --- Testing
      local distance = self:_GetDistance(location,staticpos)
      self:T(self.lid .. string.format("Dist %dm/%dm | weight %dkg | maxloadable %dkg",distance,finddist,weight,maxloadable))
      if distance <= finddist and (weight <= maxloadable or _ignoreweight) and restricted == false and cando == true then 
        index = index + 1
        table.insert(found, staticid, cargo)
        maxloadable = maxloadable - weight
      end
      
    end
  end
  return found, index, LoadedbyGC, indexg
end

--- (Internal) Function to get and load nearby crates.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @return #CTLD self
function CTLD:_LoadCratesNearby(Group, Unit)
  self:T(self.lid .. " _LoadCratesNearby")
    -- load crates into heli
  local group = Group -- Wrapper.Group#GROUP
  local unit = Unit  -- Wrapper.Unit#UNIT
  local unitname = unit:GetName()
    -- see if this heli can load crates
  local unittype = unit:GetTypeName()
  local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
  local cancrates = capabilities.crates -- #boolean
  local cratelimit = capabilities.cratelimit -- #number
  local grounded = not self:IsUnitInAir(Unit)
  local canhoverload = self:CanHoverLoad(Unit)

  -- Door check
  if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
    self:_SendMessage("You need to open the door(s) to load cargo!", 10, false, Group)
    if not self.debug then return self end
  end
  --- cases -------------------------------
  -- Chopper can\'t do crates - bark & return
  -- Chopper can do crates -
  -- --> hover if forcedhover or bark and return
  -- --> hover or land if not forcedhover
  -----------------------------------------
  if not cancrates then
    self:_SendMessage("Sorry this chopper cannot carry crates!", 10, false, Group)
  elseif self.forcehoverload and not canhoverload then
    self:_SendMessage("Hover over the crates to pick them up!", 10, false, Group)
  elseif not grounded and not canhoverload then
    self:_SendMessage("Land or hover over the crates to pick them up!", 10, false, Group)
  else
    -- have we loaded stuff already?
    local numberonboard = 0
    local loaded        = {}
    if self.Loaded_Cargo[unitname] then
      loaded         = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
      numberonboard  = loaded.Cratesloaded or 0
    else
      loaded = {}
      loaded.Troopsloaded = 0
      loaded.Cratesloaded = 0
      loaded.Cargo = {}
    end

    -- get nearby crates
    local finddist           = self.CrateDistance or 35
    local nearcrates, number = self:_FindCratesNearby(Group,Unit,finddist,false,false)
    self:T(self.lid .. " Crates found: " .. number)

    if number == 0 and self.hoverautoloading then
      return self
    elseif number == 0 then
      self:_SendMessage("Sorry, no loadable crates nearby or max cargo weight reached!", 10, false, Group)
      return self
    elseif numberonboard == cratelimit then
      self:_SendMessage("Sorry, we are fully loaded!", 10, false, Group)
      return self
    else
      local capacity = cratelimit - numberonboard
      local crateidsloaded = {}
      local crateMap = {}

      for _, cObj in pairs(nearcrates) do
        if not cObj:HasMoved() or self.allowcratepickupagain then
          local cName = cObj:GetName() or "Unknown"
          crateMap[cName] = crateMap[cName] or {}
          table.insert(crateMap[cName], cObj)
        end
      end
      for cName, crateList in pairs(crateMap) do
        if capacity <= 0 then break end

        table.sort(crateList, function(a, b) return a:GetID() > b:GetID() end)
        local needed = crateList[1]:GetCratesNeeded() or 1
        local totalFound = #crateList
        local loadedHere = 0

        while loaded.Cratesloaded < cratelimit and loadedHere < totalFound do
          loadedHere = loadedHere + 1
          local crate = crateList[loadedHere]
          if crate and crate.Positionable then
            loaded.Cratesloaded = loaded.Cratesloaded + 1
            crate:SetHasMoved(true)
            crate:SetWasDropped(false)
            table.insert(loaded.Cargo, crate)
            table.insert(crateidsloaded, crate:GetID())
            -- destroy crate
            crate:GetPositionable():Destroy(false)
            crate.Positionable = nil
          else
            loadedHere = loadedHere - 1
            break
          end
        end

        capacity = cratelimit - loaded.Cratesloaded
        if loadedHere > 0 then
          local fullSets = math.floor(loadedHere / needed)
          local leftover = loadedHere % needed

          if needed > 1 then
            if fullSets > 0 and leftover == 0 then
              self:_SendMessage(string.format("Loaded %d %s.", fullSets, cName), 10, false, Group)
            elseif fullSets > 0 and leftover > 0 then
              self:_SendMessage(string.format("Loaded %d %s(s), with %d leftover crate(s).", fullSets, cName, leftover), 10, false, Group)
            else
              self:_SendMessage(string.format("Loaded only %d/%d crate(s) of %s.", loadedHere, needed, cName), 15, false, Group)
            end
          else
            self:_SendMessage(string.format("Loaded %d %s(s).", loadedHere, cName), 10, false, Group)
          end
        end
      end
      self.Loaded_Cargo[unitname] = loaded
      self:_UpdateUnitCargoMass(Unit)
      self:_RefreshDropCratesMenu(Group, Unit)
      self:_RefreshLoadCratesMenu(Group, Unit)
      -- clean up real world crates
      self:_CleanupTrackedCrates(crateidsloaded)
      self:__CratesPickedUp(1, Group, Unit, loaded.Cargo) 
    end
  end
  return self
end


--- (Internal) Function to clean up tracked cargo crates
-- @param #CTLD self
-- @param #list crateIdsToRemove Table of IDs
-- @return self
function CTLD:_CleanupTrackedCrates(crateIdsToRemove)
  local existingcrates = self.Spawned_Cargo -- #table
  local newexcrates = {}
  for _,_crate in pairs(existingcrates) do
    local excrate = _crate -- #CTLD_CARGO
    local ID = excrate:GetID()
    local keep = true
    for _,_ID in pairs(crateIdsToRemove) do
      if ID == _ID then
        keep = false
      end
    end
    -- remove destroyed crates here too
    local static = _crate:GetPositionable() -- Wrapper.Static#STATIC -- crates
    if not static or not static:IsAlive() then
      keep = false
    end
    if keep then
      table.insert(newexcrates,_crate)
    end
  end
  self.Spawned_Cargo = nil
  self.Spawned_Cargo = newexcrates
  return self
end

--- (Internal) Function to get current loaded mass
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit
-- @return #number mass in kgs
function CTLD:_GetUnitCargoMass(Unit) 
  self:T(self.lid .. " _GetUnitCargoMass")
  if not Unit then return 0 end
  local unitname = Unit:GetName()
  local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo
  local loadedmass = 0 -- #number
  if self.Loaded_Cargo[unitname] then
    local cargotable = loadedcargo.Cargo or {} -- #table
    for _,_cargo in pairs(cargotable) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then
        loadedmass = loadedmass + (cargo.PerCrateMass * cargo:GetCratesNeeded())
      end
      if type ~= CTLD_CARGO.Enum.TROOPS and type ~=  CTLD_CARGO.Enum.ENGINEERS and type ~= CTLD_CARGO.Enum.GCLOADABLE and not cargo:WasDropped() then
        loadedmass = loadedmass + cargo.PerCrateMass
      end
      if type == CTLD_CARGO.Enum.GCLOADABLE then
        local mass = cargo:GetCargoWeight()
        loadedmass = loadedmass+mass
      end
    end
  end
  return loadedmass
end

--- (Internal) Function to calculate max loadable mass left over.
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit
-- @return #number maxloadable Max loadable mass in kg
function CTLD:_GetMaxLoadableMass(Unit)
  self:T(self.lid .. " _GetMaxLoadableMass")
  if not Unit then return 0 end
  local loadable = 0
  local loadedmass = self:_GetUnitCargoMass(Unit)
  local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
  local maxmass = capabilities.cargoweightlimit or 2000 -- max 2 tons
  loadable = maxmass - loadedmass
  return loadable
end

--- (Internal) Function to calculate and set Unit internal cargo mass
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit
function CTLD:_UpdateUnitCargoMass(Unit)
  self:T(self.lid .. " _UpdateUnitCargoMass")
  local calculatedMass = self:_GetUnitCargoMass(Unit)
  Unit:SetUnitInternalCargo(calculatedMass)
  return self
end

--- (Internal) Function to list loaded cargo.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @return #CTLD self
function CTLD:_ListCargo(Group, Unit)
  self:T(self.lid .. " _ListCargo")
  local unitname = Unit:GetName()
  local unittype = Unit:GetTypeName()
  local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
  local trooplimit = capabilities.trooplimit -- #boolean
  local cratelimit = capabilities.cratelimit -- #number
  local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo
  local loadedmass = self:_GetUnitCargoMass(Unit) -- #number
  local maxloadable = self:_GetMaxLoadableMass(Unit)
  local finddist = self.CrateDistance or 35
  --local _,_,loadedgc,loadedno = self:_FindCratesNearby(Group,Unit,finddist,true)

  if self.Loaded_Cargo[unitname] then
    local no_troops = loadedcargo.Troopsloaded or 0
    local no_crates = loadedcargo.Cratesloaded or 0
    local cargotable = loadedcargo.Cargo or {} -- #table
    local report = REPORT:New("Transport Checkout Sheet")
    report:Add("------------------------------------------------------------")
    report:Add(string.format("Troops: %d(%d), Crates: %d(%d)",no_troops,trooplimit,no_crates,cratelimit))
    report:Add("------------------------------------------------------------")
    report:Add("        -- TROOPS --")
    for _,_cargo in pairs(cargotable) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and (not cargo:WasDropped() or self.allowcratepickupagain) then
        report:Add(string.format("Troop: %s size %d", cargo:GetName(), cargo:GetCratesNeeded()))
      end
    end
    if report:GetCount() == 4 then
      report:Add("        N O N E")
    end
    report:Add("------------------------------------------------------------")
    report:Add("       -- CRATES --")
    local cratecount = 0
    local accumCrates = {}
    for _,_cargo in pairs(cargotable or {}) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and type ~= CTLD_CARGO.Enum.GCLOADABLE) and (not cargo:WasDropped() or self.allowcratepickupagain) then
        local cName = cargo:GetName()
        local needed = cargo:GetCratesNeeded() or 1
        accumCrates[cName] = accumCrates[cName] or {count=0, needed=needed}
        accumCrates[cName].count = accumCrates[cName].count + 1
      end
      if type == CTLD_CARGO.Enum.GCLOADABLE and not cargo:WasDropped() then
        report:Add(string.format("GC loaded Crate: %s size 1", cargo:GetName()))
        cratecount = cratecount + 1
      end
    end
    for cName, data in pairs(accumCrates) do
      cratecount = cratecount + data.count
      report:Add(string.format("Crate: %s %d/%d", cName, data.count, data.needed))
    end
    if cratecount == 0 then
      report:Add("        N O N E")
    end
    --[[
    if loadedno > 0 then
      report:Add("------------------------------------------------------------")
      report:Add("       -- CRATES loaded via Ground Crew --")
      for _,_cargo in pairs(loadedgc or {}) do
        local cargo = _cargo -- #CTLD_CARGO
        local type = cargo:GetType() -- #CTLD_CARGO.Enum
        if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS) then
          report:Add(string.format("Crate: %s size 1",cargo:GetName()))
          loadedmass = loadedmass + cargo:GetMass()
        end
      end
    end
    --]]
    report:Add("------------------------------------------------------------")
    report:Add("Total Mass: ".. loadedmass .. " kg. Loadable: "..maxloadable.." kg.")
    local text = report:Text()
    self:_SendMessage(text, 30, true, Group)
  else
    self:_SendMessage(string.format("Nothing loaded!\nTroop limit: %d | Crate limit %d | Weight limit %d kgs", trooplimit, cratelimit, maxloadable), 10, false, Group)
  end
  return self
end
--- (Internal) Function to list loaded cargo.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @return #CTLD self
function CTLD:_ListInventory(Group, Unit)
  self:T(self.lid .. " _ListInventory")
  local unitname = Unit:GetName()
  local unittype = Unit:GetTypeName()
  local cgotypes = self.Cargo_Crates
  local trptypes = self.Cargo_Troops
  local stctypes = self.Cargo_Statics
  
  local function countcargo(cgotable)
    local counter = 0
    for _,_cgo in pairs(cgotable) do
      counter = counter + 1
    end
    return counter
  end
  
  local crateno = countcargo(cgotypes)
  local troopno = countcargo(trptypes)
  local staticno = countcargo(stctypes)
  
  if (crateno > 0 or troopno > 0 or staticno > 0) then

    local report = REPORT:New("Inventory Sheet")
    report:Add("------------------------------------------------------------")
    report:Add(string.format("Troops: %d, Cratetypes: %d",troopno,crateno+staticno))
    report:Add("------------------------------------------------------------")
    report:Add("        -- TROOPS --")
    for _,_cargo in pairs(trptypes) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then
        local stockn = cargo:GetStock()
        local stock = "none"
        if stockn == -1 then 
          stock = "unlimited"
        elseif stockn > 0 then
          stock = tostring(stockn)
        end
        report:Add(string.format("Unit: %s | Soldiers: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock))
      end
    end
    if report:GetCount() == 4 then
      report:Add("        N O N E")
    end
    report:Add("------------------------------------------------------------")
    report:Add("       -- CRATES --")
    local cratecount = 0
    for _,_cargo in pairs(cgotypes) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      if (type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then
        local stockn = cargo:GetStock()
        local stock = "none"
        if stockn == -1 then 
          stock = "unlimited"
        elseif stockn > 0 then
          stock = tostring(stockn)
        end
        report:Add(string.format("Type: %s | Crates per Set: %d | Stock: %s",cargo:GetName(),cargo:GetCratesNeeded(),stock))
        cratecount = cratecount + 1
      end
    end
    -- Statics
    for _,_cargo in pairs(stctypes) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      if (type == CTLD_CARGO.Enum.STATIC) and not cargo:WasDropped() then
        local stockn = cargo:GetStock()
        local stock = "none"
        if stockn == -1 then 
          stock = "unlimited"
        elseif stockn > 0 then
          stock = tostring(stockn)
        end
        report:Add(string.format("Type: %s | Stock: %s",cargo:GetName(),stock))
        cratecount = cratecount + 1
      end
    end
    if cratecount == 0 then
      report:Add("        N O N E")
    end
    local text = report:Text()
    self:_SendMessage(text, 30, true, Group) 
  else
    self:_SendMessage(string.format("Nothing in stock!"), 10, false, Group) 
  end
  return self
end

--- (Internal) Function to check if a unit is an allowed fixed wing.
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit
-- @return #boolean Outcome
function CTLD:IsFixedWing(Unit)
  local typename = Unit:GetTypeName() or "none"  
  for _,_name in pairs(self.FixedWingTypes or {}) do
    if _name and (typename==_name or string.find(typename,_name,1,true))then
      return true
    end
  end
  return false
end

--- (Internal) Function to check if a unit is a CH-47
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit
-- @return #boolean Outcome
function CTLD:IsHook(Unit)
    if not Unit then return false end
    local typeName = Unit:GetTypeName()
    if not typeName then return false end
    if string.find(typeName, "CH.47") then
        return true
    else
        return false
    end
end

--- (Internal) Function to set troops positions of a template to a nice circle
-- @param #CTLD self
-- @param Core.Point#COORDINATE Coordinate Start coordinate to use
-- @param #number Radius Radius to be used
-- @param #number Heading Heading starting with
-- @param #string Template The group template name
-- @return #table Positions The positions table
function CTLD:_GetUnitPositions(Coordinate,Radius,Heading,Template)
  local Positions = {}
  local template = _DATABASE:GetGroupTemplate(Template)
  --UTILS.PrintTableToLog(template)
  local numbertroops = #template.units
  local slightshift = math.abs(math.random(1,500)/100)
  local newcenter = Coordinate:Translate(Radius+slightshift,((Heading+270+math.random(1,10))%360))
  for i=1,360,math.floor(360/numbertroops) do
    local phead = ((Heading+270+i)%360)
    local post = newcenter:Translate(Radius,phead)
    local pos1 = post:GetVec2()
    local p1t = {
    x = pos1.x,
    y = pos1.y,
    heading = phead,
    }
    table.insert(Positions,p1t)
  end
  --UTILS.PrintTableToLog(Positions)
  return Positions
end

--- (Internal) Function to unload troops from heli.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
function CTLD:_UnloadTroops(Group, Unit)
  self:T(self.lid .. " _UnloadTroops")
  -- check if we are in LOAD zone
  local droppingatbase = false
  local canunload = true
  if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
    self:_SendMessage("You need to open the door(s) to unload troops!", 10, false, Group)
    if not self.debug then return self end 
  end
  local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD)
  if not inzone then
    inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP)
  end
  if inzone then
    droppingatbase = self.returntroopstobase
  end
  -- check for hover unload
  local hoverunload = self:IsCorrectHover(Unit) --if true we\'re hovering in parameters
  local IsHerc = self:IsFixedWing(Unit)
  local IsHook = self:IsHook(Unit) 
  if IsHerc and (not IsHook) then
    -- no hover but airdrop here
    hoverunload = self:IsCorrectFlightParameters(Unit)
  end
  -- check if we\'re landed
  local grounded = not self:IsUnitInAir(Unit)
  -- Get what we have loaded
  local unitname = Unit:GetName()
  if self.Loaded_Cargo[unitname] and (grounded or hoverunload) then
    if not droppingatbase or self.debug then
      local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo
      -- looking for troops
      local cargotable = loadedcargo.Cargo
      for _,_cargo in pairs (cargotable) do
        local cargo = _cargo -- #CTLD_CARGO
        local type = cargo:GetType() -- #CTLD_CARGO.Enum
        if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and not cargo:WasDropped() then
          -- unload troops
          local name = cargo:GetName() or "none"
          local temptable = cargo:GetTemplates() or {}
          local position = Group:GetCoordinate()
          local zoneradius = self.troopdropzoneradius or 100 -- drop zone radius
          local factor = 1
          if IsHerc then
            factor = cargo:GetCratesNeeded() or 1 -- spread a bit more if airdropping
            zoneradius = Unit:GetVelocityMPS() or 100
          end
          local zone = ZONE_GROUP:New(string.format("Unload zone-%s",unitname),Group,zoneradius*factor)
          local randomcoord = zone:GetRandomCoordinate(10,30*factor) --:GetVec2()
          local heading = Group:GetHeading() or 0
          -- Spawn troops left from us, closer when hovering, further off when landed
          if hoverunload or grounded then
            randomcoord = Group:GetCoordinate()
            -- slightly left from us           
            local Angle = (heading+270)%360
            if IsHerc or IsHook then Angle = (heading+180)%360 end
            local offset = hoverunload and self.TroopUnloadDistHover or self.TroopUnloadDistGround
            if IsHerc then offset = self.TroopUnloadDistGroundHerc or 25 end
            if IsHook then  
              offset = self.TroopUnloadDistGroundHook or 15 
              if hoverunload and self.TroopUnloadDistHoverHook then
                offset = self.TroopUnloadDistHoverHook or 5
              end
            end
            randomcoord:Translate(offset,Angle,nil,true)
          end
          local tempcount = 0
          local ishook = self:IsHook(Unit)
          if ishook then tempcount = self.ChinookTroopCircleRadius or 5 end -- 10m circle for the Chinook
          for _,_template in pairs(temptable) do
            self.TroopCounter = self.TroopCounter + 1
            tempcount = tempcount+1
            local alias = string.format("%s-%d", _template, math.random(1,100000))
            local rad = 2.5+(tempcount*2)
            local Positions = self:_GetUnitPositions(randomcoord,rad,heading,_template)
            self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
              :InitDelayOff()
              :InitSetUnitAbsolutePositions(Positions)
              :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
              :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
              :SpawnFromVec2(randomcoord:GetVec2())
            self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter],type)
          end -- template loop
          cargo:SetWasDropped(true)
          -- engineering group?
          if type == CTLD_CARGO.Enum.ENGINEERS then
            self.Engineers = self.Engineers + 1
            local grpname = self.DroppedTroops[self.TroopCounter]:GetName()
            self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname)
            self:_SendMessage(string.format("Dropped Engineers %s into action!",name), 10, false, Group)
          else
            self:_SendMessage(string.format("Dropped Troops %s into action!",name), 10, false, Group)
          end
        end -- if type end
      end  -- cargotable loop
    else -- droppingatbase
        self:_SendMessage("Troops have returned to base!", 10, false, Group) 
        self:__TroopsRTB(1, Group, Unit, zonename, zone)
    end
    -- cleanup load list
    local    loaded = {} -- #CTLD.LoadedCargo
    loaded.Troopsloaded = 0
    loaded.Cratesloaded = 0
    loaded.Cargo = {}
    local loadedcargo = self.Loaded_Cargo[unitname] or {} -- #CTLD.LoadedCargo
    local cargotable = loadedcargo.Cargo or {}
    for _,_cargo in pairs (cargotable) do
      local cargo = _cargo -- #CTLD_CARGO
      local type = cargo:GetType() -- #CTLD_CARGO.Enum
      local dropped = cargo:WasDropped()
      if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and not dropped then
        table.insert(loaded.Cargo,_cargo)
        loaded.Cratesloaded = loaded.Cratesloaded + 1
      else
        -- add troops back to stock
        if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) and droppingatbase then
          -- find right generic type
          local name = cargo:GetName()
          local gentroops = self.Cargo_Troops
          for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
            if _troop.Name == name then
              local stock = _troop:GetStock()
              -- avoid making unlimited stock limited
              if stock and tonumber(stock) >= 0 then _troop:AddStock() end
            end
          end
        end
      end
    end
    self.Loaded_Cargo[unitname] = nil
    self.Loaded_Cargo[unitname] = loaded
    self:_RefreshDropTroopsMenu(Group,Unit)
    self:_UpdateUnitCargoMass(Unit)
  else
   if IsHerc then
    self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) 
   else
    self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) 
   end
  end
  return self
end

--- (Internal) Function to unload crates from heli.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
function CTLD:_UnloadCrates(Group, Unit)
    self:T(self.lid .. " _UnloadCrates")
    
    if not self.dropcratesanywhere then -- #1570
      local inzone, zonename, zone, distance = self:IsUnitInZone(Unit,CTLD.CargoZoneType.DROP)
      if not inzone then
        self:_SendMessage("You are not close enough to a drop zone!", 10, false, Group) 
        if not self.debug then 
          return self 
        end
      end
    end
    if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
      self:_SendMessage("You need to open the door(s) to drop cargo!", 10, false, Group)
      if not self.debug then return self end 
    end
    local hoverunload = self:IsCorrectHover(Unit)
    local IsHerc = self:IsFixedWing(Unit)
    local IsHook = self:IsHook(Unit)
    if IsHerc and (not IsHook) then
      hoverunload = self:IsCorrectFlightParameters(Unit)
    end
    local grounded = not self:IsUnitInAir(Unit)
    local unitname = Unit:GetName()
    if self.Loaded_Cargo[unitname] and (grounded or hoverunload) then
      local loadedcargo = self.Loaded_Cargo[unitname] or {}
      local cargotable = loadedcargo.Cargo
      local droppedCount = {}
      local neededMap = {}
      for _,_cargo in pairs (cargotable) do
        local cargo = _cargo
        local type = cargo:GetType()
        if type ~= CTLD_CARGO.Enum.TROOPS and type ~= CTLD_CARGO.Enum.ENGINEERS and type ~= CTLD_CARGO.Enum.GCLOADABLE and (not cargo:WasDropped() or self.allowcratepickupagain) then
          self:_GetCrates(Group, Unit, cargo, 1, true)
          cargo:SetWasDropped(true)
          cargo:SetHasMoved(true)
          local cname = cargo:GetName() or "Unknown"
          droppedCount[cname] = (droppedCount[cname] or 0) + 1
          if not neededMap[cname] then
            neededMap[cname] = cargo:GetCratesNeeded() or 1
          end
        end
      end
      for cname,count in pairs(droppedCount) do
        local needed = neededMap[cname] or 1
        if needed > 1 then
          local full = math.floor(count/needed)
          local left = count % needed
          if full > 0 and left == 0 then
            self:_SendMessage(string.format("Dropped %d %s.",full,cname),10,false,Group)
          elseif full > 0 and left > 0 then
            self:_SendMessage(string.format("Dropped %d %s(s), with %d leftover crate(s).",full,cname,left),10,false,Group)
          else
            self:_SendMessage(string.format("Dropped %d/%d crate(s) of %s.",count,needed,cname),15,false,Group)
          end
        else
          self:_SendMessage(string.format("Dropped %d %s(s).",count,cname),10,false,Group)
        end
      end
      local loaded = {}
      loaded.Troopsloaded = 0
      loaded.Cratesloaded = 0
      loaded.Cargo = {}
      for _,_cargo in pairs (cargotable) do
        local cargo = _cargo
        local type = cargo:GetType()
        local size = cargo:GetCratesNeeded()
        if type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS then
          table.insert(loaded.Cargo,_cargo)
          loaded.Troopsloaded = loaded.Troopsloaded + size
        end
        if type == CTLD_CARGO.Enum.GCLOADABLE and not cargo:WasDropped() then
          table.insert(loaded.Cargo,_cargo)
          loaded.Cratesloaded = loaded.Cratesloaded + size
        end
      end
      self.Loaded_Cargo[unitname] = nil
      self.Loaded_Cargo[unitname] = loaded
      
      self:_UpdateUnitCargoMass(Unit)
      self:_RefreshDropCratesMenu(Group,Unit)
    else
      if IsHerc then
          self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group) 
      else
          self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group) 
       end
    end
    return self
  end

--- (Internal) Function to build nearby crates.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @param #boolean Engineering If true build is by an engineering team.
-- @param #boolean MultiDrop If true and not engineering or FOB, vary position a bit.
function CTLD:_BuildCrates(Group, Unit,Engineering,MultiDrop)
  self:T(self.lid .. " _BuildCrates")
  -- avoid users trying to build from flying Hercs
  if self:IsFixedWing(Unit) and self.enableFixedWing and not Engineering then
    local speed = Unit:GetVelocityKMH()
    if speed > 1 then
      self:_SendMessage("You need to land / stop to build something, Pilot!", 10, false, Group) 
      return self
    end
  end
  if not Engineering and self.nobuildinloadzones then
    -- are we in a load zone?
    local inloadzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD)
    if inloadzone then
      self:_SendMessage("You cannot build in a loading area, Pilot!", 10, false, Group) 
      return self
    end
  end
  -- get nearby crates
  local finddist = self.CrateDistance or 35
  local crates,number = self:_FindCratesNearby(Group,Unit, finddist,true,true) -- #table
  local buildables = {}
  local foundbuilds = false
  local canbuild = false
  if number > 0 then
    -- get dropped crates
    for _,_crate in pairs(crates) do
      local Crate = _crate -- #CTLD_CARGO
      if (Crate:WasDropped() or not self.movecratesbeforebuild) and not Crate:IsRepair() and not Crate:IsStatic() then
        -- we can build these - maybe
        local name = Crate:GetName()
        local required = Crate:GetCratesNeeded()
        local template = Crate:GetTemplates()
        local ctype = Crate:GetType()
        local ccoord = Crate:GetPositionable():GetCoordinate() -- Core.Point#COORDINATE
        --local testmarker = ccoord:MarkToAll("Crate found",true,"Build Position")
        if not buildables[name] then
          local object = {} -- #CTLD.Buildable
          object.Name = name
          object.Required = required
          object.Found = 1
          object.Template = template
          object.CanBuild = false
          object.Type = ctype -- #CTLD_CARGO.Enum
          object.Coord = ccoord:GetVec2()
          buildables[name] = object
          foundbuilds = true
        else
         buildables[name].Found = buildables[name].Found + 1
         foundbuilds = true
        end
        if buildables[name].Found >= buildables[name].Required then 
           buildables[name].CanBuild = true
           canbuild = true
        end
        self:T({buildables = buildables})
      end -- end dropped
    end -- end crate loop
    -- ok let\'s list what we have
    local report = REPORT:New("Checklist Buildable Crates")
    report:Add("------------------------------------------------------------")
    for _,_build in pairs(buildables) do
      local build = _build -- Object table from above
      local name = build.Name
      local needed = build.Required
      local found = build.Found
      local txtok = "NO"
      if build.CanBuild then 
        txtok = "YES" 
      end
      local text = string.format("Type: %s | Required %d | Found %d | Can Build %s", name, needed, found, txtok)
      report:Add(text)
    end -- end list buildables
    if not foundbuilds then 
      report:Add("     --- None found! ---")
      if self.movecratesbeforebuild then
        report:Add("*** Crates need to be moved before building!")
      end
    end
    report:Add("------------------------------------------------------------")
    local text = report:Text()
    if not Engineering then
      self:_SendMessage(text, 30, true, Group) 
    else
      self:T(text)
    end
    -- let\'s get going
    if canbuild then
      -- loop again
      for _,_build in pairs(buildables) do
        local build = _build -- #CTLD.Buildable
        if build.CanBuild then
          self:_CleanUpCrates(crates,build,number)
          if self.buildtime and self.buildtime > 0 then
              local buildtimer = TIMER:New(self._BuildObjectFromCrates,self,Group,Unit,build,false,Group:GetCoordinate(),MultiDrop)
              buildtimer:Start(self.buildtime)
              self:_SendMessage(string.format("Build started, ready in %d seconds!",self.buildtime),15,false,Group)
              self:__CratesBuildStarted(1,Group,Unit,build.Name) 
              self:_RefreshDropTroopsMenu(Group,Unit)
          else
            self:_BuildObjectFromCrates(Group,Unit,build,false,nil,MultiDrop)
          end
        end
      end
    end
  else
    if not Engineering then self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) end
  end -- number > 0
  return self
end

--- (Internal) Function to repair nearby vehicles / FOBs
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit

function CTLD:_PackCratesNearby(Group, Unit)
  self:T(self.lid .. " _PackCratesNearby")
  -----------------------------------------
  -- search for nearest group to player
  -- determine if group is packable
  -- generate crates and destroy group
  -----------------------------------------

  -- get nearby vehicles
  local location = Group:GetCoordinate() -- get coordinate of group using function
  local nearestGroups = SET_GROUP:New():FilterCoalitions("blue"):FilterZones({ZONE_RADIUS:New("TempZone", location:GetVec2(), self.PackDistance, false)}):FilterOnce() -- get all groups withing PackDistance from group using function
  -- get template name of all vehicles in zone

  -- determine if group is packable
  for _, _Group in pairs(nearestGroups.Set) do -- convert #SET_GROUP to a list of Wrapper.Group#GROUP
    for _, _Template in pairs(_DATABASE.Templates.Groups) do -- iterate through the database of templates
      if (string.match(_Group:GetName(), _Template.GroupName)) then -- check if the Wrapper.Group#GROUP near the player is in the list of templates by name
        -- generate crates and destroy group
        for _, _entry in pairs(self.Cargo_Crates) do -- iterate through #CTLD_CARGO
          if (_entry.Templates[1] == _Template.GroupName) then -- check if the #CTLD_CARGO matches the template name
            _Group:Destroy() -- if a match is found destroy the Wrapper.Group#GROUP near the player
            self:_GetCrates(Group, Unit, _entry, nil, false, true) -- spawn the appropriate crates near the player
            self:_RefreshLoadCratesMenu(Group,Unit) -- call the refresher to show the crates in the menu
            self:__CratesPacked(1,Group,Unit,_entry)
            return true
          end
        end
      end
    end
  end
    self:_SendMessage("Nothing to pack at this distance pilot!",10,false,Group)
    return false
end

--- (Internal) Function to repair nearby vehicles / FOBs
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @param #boolean Engineering If true, this is an engineering role
function CTLD:_RepairCrates(Group, Unit, Engineering)
  self:T(self.lid .. " _RepairCrates")
  -- get nearby crates
  local finddist = self.CrateDistance or 35
  local crates,number = self:_FindCratesNearby(Group,Unit,finddist,true,true) -- #table
  local buildables = {}
  local foundbuilds = false
  local canbuild = false
  if number > 0 then
    -- get dropped crates
    for _,_crate in pairs(crates) do
      local Crate = _crate -- #CTLD_CARGO
      if Crate:WasDropped() and Crate:IsRepair() and not Crate:IsStatic() then
        -- we can build these - maybe
        local name = Crate:GetName()
        local required = Crate:GetCratesNeeded()
        local template = Crate:GetTemplates()
        local ctype = Crate:GetType()
        if not buildables[name] then
          local object = {} -- #CTLD.Buildable
          object.Name = name
          object.Required = required
          object.Found = 1
          object.Template = template
          object.CanBuild = false
          object.Type = ctype -- #CTLD_CARGO.Enum
          buildables[name] = object
          foundbuilds = true
        else
         buildables[name].Found = buildables[name].Found + 1
         foundbuilds = true
        end
        if buildables[name].Found >= buildables[name].Required then 
           buildables[name].CanBuild = true
           canbuild = true
        end
        self:T({repair = buildables})
      end -- end dropped
    end -- end crate loop
    -- ok let\'s list what we have
    local report = REPORT:New("Checklist Repairs")
    report:Add("------------------------------------------------------------")
    for _,_build in pairs(buildables) do
      local build = _build -- Object table from above
      local name = build.Name
      local needed = build.Required
      local found = build.Found
      local txtok = "NO"
      if build.CanBuild then 
        txtok = "YES" 
      end
      local text = string.format("Type: %s | Required %d | Found %d | Can Repair %s", name, needed, found, txtok)
      report:Add(text)
    end -- end list buildables
    if not foundbuilds then report:Add("     --- None Found ---") end
    report:Add("------------------------------------------------------------")
    local text = report:Text()
    if not Engineering then
      self:_SendMessage(text, 30, true, Group) 
    else
      self:T(text)
    end
    -- let\'s get going
    if canbuild then
      -- loop again
      for _,_build in pairs(buildables) do
        local build = _build -- #CTLD.Buildable
        if build.CanBuild then
          self:_RepairObjectFromCrates(Group,Unit,crates,build,number,Engineering)
        end
      end
    end
  else
    if not Engineering then self:_SendMessage(string.format("No crates within %d meters!",finddist), 10, false, Group) end 
  end -- number > 0
  return self
end

--- (Internal) Function to actually SPAWN buildables in the mission.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Group#UNIT Unit
-- @param #CTLD.Buildable Build
-- @param #boolean Repair If true this is a repair and not a new build
-- @param Core.Point#COORDINATE RepairLocation Location for repair (e.g. where the destroyed unit was)
-- @param #boolean MultiDrop if true and not a repair, vary location a bit if not a FOB
function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,MultiDrop)
  self:T(self.lid .. " _BuildObjectFromCrates")
  -- Spawn-a-crate-content
  if Group and Group:IsAlive() or (RepairLocation and not Repair) then
    --local position = Unit:GetCoordinate() or Group:GetCoordinate()
    --local unitname = Unit:GetName() or Group:GetName() or "Unknown"
    local name = Build.Name
    local ctype = Build.Type -- #CTLD_CARGO.Enum
    local canmove = false
    if ctype == CTLD_CARGO.Enum.VEHICLE then canmove = true end
    if ctype == CTLD_CARGO.Enum.STATIC then 
      return self 
    end
    local temptable = Build.Template or {}
    if type(temptable) == "string" then 
      temptable = {temptable}
    end
    local zone = nil -- Core.Zone#ZONE_RADIUS
    if RepairLocation and not Repair then
      -- timed build
      zone = ZONE_RADIUS:New(string.format("Build zone-%d",math.random(1,10000)),RepairLocation:GetVec2(),100)
    else
      zone = ZONE_GROUP:New(string.format("Unload zone-%d",math.random(1,10000)),Group,100)
    end
    --local randomcoord = zone:GetRandomCoordinate(35):GetVec2()
    local randomcoord = Build.Coord or zone:GetRandomCoordinate(35):GetVec2()
    if MultiDrop and (not Repair) and canmove then
      -- coordinate may be the same, avoid
      local randomcoord = zone:GetRandomCoordinate(35):GetVec2()
    end
    if Repair then
      randomcoord = RepairLocation:GetVec2()
    end
    for _,_template in pairs(temptable) do
      self.TroopCounter = self.TroopCounter + 1
      local alias = string.format("%s-%d", _template, math.random(1,100000))
      if canmove then
        self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
          --:InitRandomizeUnits(true,20,2)
          :InitDelayOff()
          :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
          :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
          :SpawnFromVec2(randomcoord)
      else -- don't random position of e.g. SAM units build as FOB
        self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
          :InitDelayOff()
          :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
          :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
          :SpawnFromVec2(randomcoord)
      end
      if Repair then
        self:__CratesRepaired(1,Group,Unit,self.DroppedTroops[self.TroopCounter])
      else
        self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter])
      end
    end -- template loop
  else
    self:T(self.lid.."Group KIA while building!")
  end
  return self
end

--- (Internal) Function to get a vehicle formation for a moving group
-- @param #CTLD self
-- @return #string Formation
function CTLD:_GetVehicleFormation()
  local VehicleMoveFormation = self.VehicleMoveFormation or AI.Task.VehicleFormation.VEE
  if type(self.VehicleMoveFormation)=="table" then
    VehicleMoveFormation = self.VehicleMoveFormation[math.random(1,#self.VehicleMoveFormation)]
  end
  return VehicleMoveFormation
end

--- (Internal) Function to move group to WP zone.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The Group to move.
function CTLD:_MoveGroupToZone(Group)
  self:T(self.lid .. " _MoveGroupToZone")
  local groupname = Group:GetName() or "none"
  local groupcoord = Group:GetCoordinate()
  -- Get closest zone of type
  local outcome, name, zone, distance  = self:IsUnitInZone(Group,CTLD.CargoZoneType.MOVE)
  self:T({canmove=outcome, name=name, zone=zone, dist=distance,max=self.movetroopsdistance})
  if (distance <= self.movetroopsdistance) and outcome == true and zone~= nil then
    -- yes, we can ;)
    local groupname = Group:GetName()
    local zonecoord = zone:GetRandomCoordinate(20,125) -- Core.Point#COORDINATE
    local formation = self:_GetVehicleFormation()
    --local coordinate = zonecoord:GetVec2()
    Group:SetAIOn()
    Group:OptionAlarmStateAuto()
    Group:OptionDisperseOnAttack(30)
    Group:OptionROEOpenFire()
    Group:RouteGroundTo(zonecoord,25,formation)
    end
  return self
end

--- (Internal) Housekeeping - Cleanup crates when build
-- @param #CTLD self
-- 
-- @param #table Crates Table of #CTLD_CARGO objects near the unit.
-- @param #CTLD.Buildable Build Table build object.
-- @param #number Number Number of objects in Crates (found) to limit search.
function CTLD:_CleanUpCrates(Crates,Build,Number)
  self:T(self.lid .. " _CleanUpCrates")
  -- clean up real world crates
  local build = Build -- #CTLD.Buildable
  local existingcrates = self.Spawned_Cargo -- #table of exising crates
  local newexcrates = {}
  -- get right number of crates to destroy
  local numberdest = Build.Required
  local nametype = Build.Name
  local found = 0
  local rounds = Number
  local destIDs = {}
  
  -- loop and find matching IDs in the set
  for _,_crate in pairs(Crates) do
    local nowcrate = _crate -- #CTLD_CARGO
    local name = nowcrate:GetName()
    local thisID = nowcrate:GetID()
    if name == nametype then -- matching crate type
      table.insert(destIDs,thisID)
      found = found + 1
      nowcrate:GetPositionable():Destroy(false)
      nowcrate.Positionable = nil
      nowcrate.HasBeenDropped = false
    end
    if found == numberdest then break end -- got enough
  end
  -- loop and remove from real world representation
  self:_CleanupTrackedCrates(destIDs)
  return self
end

--- (Internal) Helper - Drop **all** loaded crates nearby and build them.
-- @param Wrapper.Group#GROUP Group The calling group
-- @param Wrapper.Unit#UNIT  Unit  The calling unit
function CTLD:_DropAndBuild(Group,Unit)
    if self.nobuildinloadzones then
      if self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) then
        self:_SendMessage("You cannot build in a loading area, Pilot!",10,false,Group)
        return self
      end
    end
    self:_UnloadCrates(Group,Unit)
    timer.scheduleFunction(function() self:_BuildCrates(Group,Unit,false,true) end,{},timer.getTime()+1)
  end
  
  --- (Internal) Helper - Drop a **single** crate set and build it.
-- @param Wrapper.Group#GROUP Group     The calling group
-- @param Wrapper.Unit#UNIT  Unit       The calling unit
-- @param number             setIndex   Index of the crate-set to drop
  function CTLD:_DropSingleAndBuild(Group,Unit,setIndex)
    if self.nobuildinloadzones then
      if self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) then
        self:_SendMessage("You cannot build in a loading area, Pilot!",10,false,Group)
        return self
      end
    end
    self:_UnloadSingleCrateSet(Group,Unit,setIndex)
    timer.scheduleFunction(function() self:_BuildCrates(Group,Unit,false) end,{},timer.getTime()+1)
  end

--- (Internal) Helper - Pack crates near the unit and load them.
-- @param Wrapper.Group#GROUP Group  The calling group
-- @param Wrapper.Unit#UNIT  Unit    The calling unit
function CTLD:_PackAndLoad(Group,Unit)
    if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
      self:_SendMessage("You need to open the door(s) to load cargo!",10,false,Group)
      return self
    end
    if not self:_PackCratesNearby(Group,Unit) then
        return self
      end
    timer.scheduleFunction(function() self:_LoadCratesNearby(Group,Unit) end,{},timer.getTime()+1)
    return self
  end

--- (Internal) Helper - Pack crates near the unit and then remove them.
-- @param Wrapper.Group#GROUP Group  The calling group
-- @param Wrapper.Unit#UNIT  Unit    The calling unit
function CTLD:_PackAndRemove(Group,Unit)
    if not self:_PackCratesNearby(Group,Unit) then
        return self
      end
        timer.scheduleFunction(function() self:_RemoveCratesNearby(Group,Unit) end,{},timer.getTime()+1)
    return self
end

--- (Internal) Helper - get and load in one step
-- @param Wrapper.Group#GROUP Group  The calling group
-- @param Wrapper.Unit#UNIT  Unit    The calling unit
function CTLD:_GetAndLoad(Group,Unit,cargoObj)
    if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
        self:_SendMessage("You need to open the door(s) to load cargo!",10,false,Group)
        return self
    end
    self:_GetCrates(Group,Unit,cargoObj)

    timer.scheduleFunction(function() self:_LoadSingleCrateSet(Group,Unit,cargoObj.Name) end,{},timer.getTime()+1)
end

-- @param Wrapper.Group#GROUP Group The player’s group that triggered the action
-- @param Wrapper.Unit#UNIT  Unit The unit performing the pack-and-load  
function CTLD:_GetAllAndLoad(Group,Unit)
    if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
        self:_SendMessage("You need to open the door(s) to load cargo!",10,false,Group)
        return self
    end

    timer.scheduleFunction(function() self:_LoadCratesNearby(Group,Unit) end,{},timer.getTime()+1)
end

--- (Internal) Housekeeping - Function to refresh F10 menus.
-- @param #CTLD self
-- @return #CTLD self
function CTLD:_RefreshF10Menus()
    self:T(self.lid .. " _RefreshF10Menus")
    self.onestepmenu = self.onestepmenu or false                                            -- hybrid toggle (default = false)
  
    -- 1) Gather all the pilot groups from our Set
    local PlayerSet   = self.PilotGroups
    local PlayerTable = PlayerSet:GetSetObjects()
  
    -- 2) Rebuild the self.CtldUnits table
    local _UnitList = {}
    for _, groupObj in pairs(PlayerTable) do
      local firstUnit = groupObj:GetFirstUnitAlive()
      if firstUnit then
        if firstUnit:IsPlayer() then
          if firstUnit:IsHelicopter() or (self.enableFixedWing and self:IsFixedWing(firstUnit)) then
            local _unit = firstUnit:GetName()
            _UnitList[_unit] = _unit
          end
        end
      end
    end
    
    -- 3) CA Units
    if self.allowCATransport and self.CATransportSet then
      for _,_clientobj in pairs(self.CATransportSet.Set) do
        local client = _clientobj -- Wrapper.Client#CLIENT
        if client:IsGround() then
          local cname = client:GetName()
          self:T(self.lid.."Adding: "..cname)
          _UnitList[cname] = cname
        end
      end
    end
    
    self.CtldUnits = _UnitList
  
    -- subcats?
    if self.usesubcats then
     for _id,_cargo in pairs(self.Cargo_Crates) do
      local entry = _cargo -- #CTLD_CARGO
      if not self.subcats[entry.Subcategory] then
        self.subcats[entry.Subcategory] = entry.Subcategory
      end
     end
     for _id,_cargo in pairs(self.Cargo_Statics) do
      local entry = _cargo -- #CTLD_CARGO
      if not self.subcats[entry.Subcategory] then
        self.subcats[entry.Subcategory] = entry.Subcategory
      end
     end
     for _id,_cargo in pairs(self.Cargo_Troops) do
      local entry = _cargo -- #CTLD_CARGO
      if not self.subcatsTroop[entry.Subcategory] then
        self.subcatsTroop[entry.Subcategory] = entry.Subcategory
      end
     end
    end
  
    local menucount = 0
    local menus = {}
    for _, _unitName in pairs(self.CtldUnits) do
      if (not self.MenusDone[_unitName]) or (self.showstockinmenuitems == true) then
        self:T(self.lid.."Menu not done yet for ".._unitName)
        local _unit  = UNIT:FindByName(_unitName)
        if not _unit and self.allowCATransport then
          _unit = CLIENT:FindByName(_unitName)
        end
        if _unit and _unit:IsAlive() then
          local _group = _unit:GetGroup()
          if _group then
            self:T(self.lid.."Unit and Group exist")
            local capabilities = self:_GetUnitCapabilities(_unit)
            local cantroops  = capabilities.troops
            local cancrates  = capabilities.crates
            local unittype   = _unit:GetTypeName()
            local isHook     = self:IsHook(_unit)
            local nohookswitch = true
            --local nohookswitch = not (isHook and self.enableChinookGCLoading)
            -- Clear old topmenu if it existed
            if _group.CTLDTopmenu then
              _group.CTLDTopmenu:Remove()
              _group.CTLDTopmenu = nil
            end
            local toptroops = nil
            local topcrates = nil
            local topmenu = MENU_GROUP:New(_group, "CTLD", nil)
            _group.CTLDTopmenu = topmenu
  
            if cantroops then
              local toptroops  = MENU_GROUP:New(_group, "Manage Troops", topmenu)
              local troopsmenu = MENU_GROUP:New(_group, "Load troops", toptroops)
              _group.MyTopTroopsMenu = toptroops
              
              if self.usesubcats then
                local subcatmenus = {}
                for catName, _ in pairs(self.subcatsTroop) do
                  subcatmenus[catName] = MENU_GROUP:New(_group, catName, troopsmenu)
                end
                for _, cargoObj in pairs(self.Cargo_Troops) do
                  if not cargoObj.DontShowInMenu then
                    local stock = cargoObj:GetStock()
                    local menutext = cargoObj.Name
                    if (stock >= 0) and (self.showstockinmenuitems == true) then menutext = menutext.." ["..stock.."]" end
                    MENU_GROUP_COMMAND:New(_group, menutext, subcatmenus[cargoObj.Subcategory], self._LoadTroops, self, _group, _unit, cargoObj)
                  end
                end
              else
                for _, cargoObj in pairs(self.Cargo_Troops) do
                  if not cargoObj.DontShowInMenu then
                    local stock = cargoObj:GetStock()
                    local menutext = cargoObj.Name
                    if (stock >= 0) and (self.showstockinmenuitems == true) then menutext = menutext.." ["..stock.."]" end
                    MENU_GROUP_COMMAND:New(_group, menutext, troopsmenu, self._LoadTroops, self, _group, _unit, cargoObj)
                  end
                end
              end
              local dropTroopsMenu=MENU_GROUP:New(_group,"Drop Troops",toptroops):Refresh()
              MENU_GROUP_COMMAND:New(_group,"Drop ALL troops",dropTroopsMenu,self._UnloadTroops,self,_group,_unit):Refresh()
              MENU_GROUP_COMMAND:New(_group,"Extract troops",toptroops,self._ExtractTroops,self,_group,_unit):Refresh()
              local uName=_unit:GetName()
              local loadedData=self.Loaded_Cargo[uName]
              if loadedData and loadedData.Cargo then
                for i,cargoObj in ipairs(loadedData.Cargo) do
                  if cargoObj and (cargoObj:GetType()==CTLD_CARGO.Enum.TROOPS or cargoObj:GetType()==CTLD_CARGO.Enum.ENGINEERS) and not cargoObj:WasDropped() then
                    local name=cargoObj:GetName() or "Unknown"
                    local needed=cargoObj:GetCratesNeeded() or 1
                    local cID=cargoObj:GetID()
                    local line=string.format("Drop: %s",name,needed,cID)
                    MENU_GROUP_COMMAND:New(_group,line,dropTroopsMenu,self._UnloadSingleTroopByID,self,_group,_unit,cID):Refresh()
                  end
                end
              end
            end
            if cancrates then
              local topcrates  = MENU_GROUP:New(_group, "Manage Crates", topmenu)
              _group.MyTopCratesMenu = topcrates
  
              -- Build the “Get Crates” sub-menu items
              local cratesmenu = MENU_GROUP:New(_group,"Get Crates",topcrates)
  
              if self.onestepmenu then
                if self.usesubcats then
                  local subcatmenus = {}
                  for catName,_ in pairs(self.subcats) do
                    subcatmenus[catName] = MENU_GROUP:New(_group,catName,cratesmenu)
                  end
                  for _,cargoObj in pairs(self.Cargo_Crates) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock>=0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      local mSet = MENU_GROUP:New(_group,txt,subcatmenus[cargoObj.Subcategory])
                      MENU_GROUP_COMMAND:New(_group,"Get",mSet,self._GetCrates,self,_group,_unit,cargoObj)
                      MENU_GROUP_COMMAND:New(_group,"Get and Load",mSet,self._GetAndLoad,self,_group,_unit,cargoObj)
                    end
                  end
                  for _,cargoObj in pairs(self.Cargo_Statics) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock>=0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      local mSet = MENU_GROUP:New(_group,txt,subcatmenus[cargoObj.Subcategory])
                      MENU_GROUP_COMMAND:New(_group,"Get",mSet,self._GetCrates,self,_group,_unit,cargoObj)
                      MENU_GROUP_COMMAND:New(_group,"Get and Load",mSet,self._GetAndLoad,self,_group,_unit,cargoObj)
                    end
                  end
                else
                  for _,cargoObj in pairs(self.Cargo_Crates) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock>=0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      local mSet = MENU_GROUP:New(_group,txt,cratesmenu)
                      MENU_GROUP_COMMAND:New(_group,"Get",mSet,self._GetCrates,self,_group,_unit,cargoObj)                      
                      MENU_GROUP_COMMAND:New(_group,"Get and Load",mSet,self._GetAndLoad,self,_group,_unit,cargoObj)
                    end
                  end
                  for _,cargoObj in pairs(self.Cargo_Statics) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)",cargoObj.Name,cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock>=0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      local mSet = MENU_GROUP:New(_group,txt,cratesmenu)
                      MENU_GROUP_COMMAND:New(_group,"Get",mSet,self._GetCrates,self,_group,_unit,cargoObj)
                      MENU_GROUP_COMMAND:New(_group,"Get and Load",mSet,self._GetAndLoad,self,_group,_unit,cargoObj)
                      
                    end
                  end
                end
              else
                if self.usesubcats == true then
                  local subcatmenus = {}
                  for catName, _ in pairs(self.subcats) do
                    subcatmenus[catName] = MENU_GROUP:New(_group, catName, cratesmenu)         -- fixed variable case
                  end
                  for _, cargoObj in pairs(self.Cargo_Crates) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)", cargoObj.Name, cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock >= 0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      MENU_GROUP_COMMAND:New(_group, txt, subcatmenus[cargoObj.Subcategory], self._GetCrates, self, _group, _unit, cargoObj)
                    end
                  end
                  for _, cargoObj in pairs(self.Cargo_Statics) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)", cargoObj.Name, cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock >= 0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      MENU_GROUP_COMMAND:New(_group, txt, subcatmenus[cargoObj.Subcategory], self._GetCrates, self, _group, _unit, cargoObj)
                    end
                  end
                else
                  for _, cargoObj in pairs(self.Cargo_Crates) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)", cargoObj.Name, cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock >= 0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      MENU_GROUP_COMMAND:New(_group, txt, cratesmenu, self._GetCrates, self, _group, _unit, cargoObj)
                    end
                  end
                  for _, cargoObj in pairs(self.Cargo_Statics) do
                    if not cargoObj.DontShowInMenu then
                      local txt = string.format("Crate %s (%dkg)", cargoObj.Name, cargoObj.PerCrateMass or 0)
                      if cargoObj.Location then txt = txt.."[R]" end
                      local stock = cargoObj:GetStock()
                      if stock >= 0 and self.showstockinmenuitems then txt = txt.."["..stock.."]" end
                      MENU_GROUP_COMMAND:New(_group, txt, cratesmenu, self._GetCrates, self, _group, _unit, cargoObj)
                    end
                  end
                end
              end
  
              local loadCratesMenu=MENU_GROUP:New(_group,"Load Crates",topcrates)
              _group.MyLoadCratesMenu=loadCratesMenu
              MENU_GROUP_COMMAND:New(_group,"Load ALL",loadCratesMenu,self._LoadCratesNearby,self,_group,_unit)
              MENU_GROUP_COMMAND:New(_group,"Show loadable crates",loadCratesMenu,self._RefreshLoadCratesMenu,self,_group,_unit)
  
              local dropCratesMenu = MENU_GROUP:New(_group,"Drop Crates",topcrates)
              topcrates.DropCratesMenu = dropCratesMenu
  
              if not self.nobuildmenu then
                MENU_GROUP_COMMAND:New(_group, "Build crates", topcrates, self._BuildCrates, self, _group, _unit)
                MENU_GROUP_COMMAND:New(_group, "Repair", topcrates, self._RepairCrates, self, _group, _unit):Refresh()
              end
  
              local removecratesmenu = MENU_GROUP:New(_group, "Remove crates", topcrates)
              MENU_GROUP_COMMAND:New(_group, "Remove crates nearby", removecratesmenu, self._RemoveCratesNearby, self, _group, _unit)
  
              if self.onestepmenu then
                local mPack=MENU_GROUP:New(_group,"Pack crates",topcrates)
                MENU_GROUP_COMMAND:New(_group,"Pack",mPack,self._PackCratesNearby,self,_group,_unit)
                MENU_GROUP_COMMAND:New(_group,"Pack and Load",mPack,self._PackAndLoad,self,_group,_unit)
                MENU_GROUP_COMMAND:New(_group,"Pack and Remove",mPack,self._PackAndRemove,self,_group,_unit)
                MENU_GROUP_COMMAND:New(_group, "List crates nearby", topcrates, self._ListCratesNearby, self, _group, _unit)
              else
                MENU_GROUP_COMMAND:New(_group, "Pack crates", topcrates, self._PackCratesNearby, self, _group, _unit)
                MENU_GROUP_COMMAND:New(_group, "List crates nearby", topcrates, self._ListCratesNearby, self, _group, _unit)
              end
  
              local uName = _unit:GetName()
              local loadedData = self.Loaded_Cargo[uName]
              if loadedData and loadedData.Cargo then
                local cargoByName = {}
                for _, cgo in pairs(loadedData.Cargo) do
                  if cgo and (not cgo:WasDropped()) then
                    local cname   = cgo:GetName()
                    local cneeded = cgo:GetCratesNeeded()
                    cargoByName[cname] = cargoByName[cname] or { count=0, needed=cneeded }
                    cargoByName[cname].count = cargoByName[cname].count + 1
                  end
                end
                for name, info in pairs(cargoByName) do
                  local line = string.format("Drop %s (%d/%d)", name, info.count, info.needed)
                  MENU_GROUP_COMMAND:New(_group, line, dropCratesMenu, self._UnloadSingleCrateSet, self, _group, _unit, name)
                end
              end
            end
  
            -----------------------------------------------------
            -- Misc sub‐menus
            -----------------------------------------------------
            MENU_GROUP_COMMAND:New(_group, "List boarded cargo", topmenu, self._ListCargo, self, _group, _unit)
            MENU_GROUP_COMMAND:New(_group, "Inventory", topmenu, self._ListInventory, self, _group, _unit)
            MENU_GROUP_COMMAND:New(_group, "List active zone beacons", topmenu, self._ListRadioBeacons, self, _group, _unit)
  
            local smoketopmenu = MENU_GROUP:New(_group, "Smokes, Flares, Beacons", topmenu)
            MENU_GROUP_COMMAND:New(_group, "Smoke zones nearby", smoketopmenu, self.SmokeZoneNearBy, self, _unit, false)
            local smokeself = MENU_GROUP:New(_group, "Drop smoke now", smoketopmenu)
            MENU_GROUP_COMMAND:New(_group, "Red smoke", smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.Red)
            MENU_GROUP_COMMAND:New(_group, "Blue smoke", smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.Blue)
            MENU_GROUP_COMMAND:New(_group, "Green smoke", smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.Green)
            MENU_GROUP_COMMAND:New(_group, "Orange smoke", smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.Orange)
            MENU_GROUP_COMMAND:New(_group, "White smoke", smokeself, self.SmokePositionNow, self, _unit, false, SMOKECOLOR.White)
  
            MENU_GROUP_COMMAND:New(_group, "Flare zones nearby", smoketopmenu, self.SmokeZoneNearBy, self, _unit, true)
            MENU_GROUP_COMMAND:New(_group, "Fire flare now", smoketopmenu, self.SmokePositionNow, self, _unit, true)
            MENU_GROUP_COMMAND:New(_group, "Drop beacon now", smoketopmenu, self.DropBeaconNow, self, _unit):Refresh()
  
            if self:IsFixedWing(_unit) then
              MENU_GROUP_COMMAND:New(_group, "Show flight parameters", topmenu, self._ShowFlightParams, self, _group, _unit):Refresh()
            else
              MENU_GROUP_COMMAND:New(_group, "Show hover parameters", topmenu, self._ShowHoverParams, self, _group, _unit):Refresh()
            end
  
            -- Mark we built the menu
            self.MenusDone[_unitName] = true
            self:_RefreshLoadCratesMenu(_group,_unit)
            self:_RefreshDropCratesMenu(_group,_unit)
  
          end -- if _group
        end -- if _unit
      else
        self:T(self.lid .. " Menus already done for this group!")
      end
    end -- for all pilot units
  
    return self
  end
  
--- (Internal) Function to refresh the menu for load crates. Triggered from land/getcrate/pack and more
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The calling group.
-- @param Wrapper.Unit#UNIT Unit The calling unit.
-- @return #CTLD self
function CTLD:_RefreshLoadCratesMenu(Group,Unit)
    if not Group.MyLoadCratesMenu then return end
    Group.MyLoadCratesMenu:RemoveSubMenus()
  
    local d=self.CrateDistance or 35
    local nearby,n=self:_FindCratesNearby(Group,Unit,d,true,true)
    if n==0 then
      MENU_GROUP_COMMAND:New(Group,"No crates found! Rescan?",Group.MyLoadCratesMenu,function() self:_RefreshLoadCratesMenu(Group,Unit) end)
      return
    end
    MENU_GROUP_COMMAND:New(Group,"Load ALL",Group.MyLoadCratesMenu,self._LoadCratesNearby,self,Group,Unit)
  
    local cargoByName={}
    for _,crate in pairs(nearby) do
      local name=crate:GetName()
      cargoByName[name]=cargoByName[name] or{}
      table.insert(cargoByName[name],crate)
    end
  
    local lineIndex=1
    for cName,list in pairs(cargoByName) do
      local needed=list[1]:GetCratesNeeded() or 1
      table.sort(list,function(a,b)return a:GetID()<b:GetID() end)
      local i=1
      while i<=#list do
        local left=#list-i+1
        local label
        if left>=needed then
          label=string.format("%d. Load %s",lineIndex,cName)
          i=i+needed
        else
          label=string.format("%d. Load %s (%d/%d)",lineIndex,cName,left,needed)
          i=#list+1
        end
        MENU_GROUP_COMMAND:New(Group,label,Group.MyLoadCratesMenu,self._LoadSingleCrateSet,self,Group,Unit,cName)
        lineIndex=lineIndex+1
      end
    end
  end
  

---
-- Loads exactly `CratesNeeded` crates for one cargoName in range.
-- If "Ammo Truck" needs 2 crates, we pick up 2 if available.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
-- @param #string cargoName The cargo name, e.g. "Ammo Truck"
function CTLD:_LoadSingleCrateSet(Group, Unit, cargoName)
  self:T(self.lid .. " _LoadSingleCrateSet cargoName=" .. (cargoName or "nil"))

  -- 1) Must be landed or hovering
  local grounded = not self:IsUnitInAir(Unit)
  local hover    = self:CanHoverLoad(Unit)
  if not grounded and not hover then
    self:_SendMessage("You must land or hover to load crates!", 10, false, Group)
    return self
  end

  -- 2) Check door if required
  if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
    self:_SendMessage("You need to open the door(s) to load cargo!", 10, false, Group)
    return self
  end

  -- 3) Find crates with `cargoName` in range
  local finddist = self.CrateDistance or 35
  local cratesNearby, number = self:_FindCratesNearby(Group, Unit, finddist, false, false)
  if number == 0 then
    self:_SendMessage("No crates found in range!", 10, false, Group)
    return self
  end

  local matchingCrates = {}
  local needed = nil
  for _, crateObj in pairs(cratesNearby) do
    if crateObj:GetName() == cargoName then
      needed = needed or crateObj:GetCratesNeeded()
      table.insert(matchingCrates, crateObj)
    end
  end
  if not needed then
    self:_SendMessage(string.format("No \"%s\" crates found in range!", cargoName), 10, false, Group)
    return self
  end

  local found = #matchingCrates

  -- 4) Check capacity
  local unitName = Unit:GetName()
  local loadedData = self.Loaded_Cargo[unitName] or { Troopsloaded=0, Cratesloaded=0, Cargo={} }
  local capabilities = self:_GetUnitCapabilities(Unit)
  local capacity = capabilities.cratelimit or 0
  if loadedData.Cratesloaded >= capacity then
    self:_SendMessage("No more capacity to load crates!", 10, false, Group)
    return self
  end

  -- decide how many we can actually load
  local spaceLeft = capacity - loadedData.Cratesloaded
  local toLoad = math.min(found, needed, spaceLeft)
  if toLoad < 1 then
    self:_SendMessage("Cannot load crates: either none found or no capacity left.", 10, false, Group)
    return self
  end

  -- 5) Load exactly `toLoad` crates
  local crateIDsLoaded = {}
  for i = 1, toLoad do
    local crate = matchingCrates[i]
    crate:SetHasMoved(true)
    crate:SetWasDropped(false)
    table.insert(loadedData.Cargo, crate)
    loadedData.Cratesloaded = loadedData.Cratesloaded + 1
    local stObj = crate:GetPositionable()
    if stObj and stObj:IsAlive() then
      stObj:Destroy(false)
    end
    table.insert(crateIDsLoaded, crate:GetID())
  end
  self.Loaded_Cargo[unitName] = loadedData
  self:_UpdateUnitCargoMass(Unit)

  -- 6) Remove them from self.Spawned_Cargo
  local newSpawned = {}
  for _, cObj in ipairs(self.Spawned_Cargo) do
    local keep = true
    for i=1, toLoad do
      if matchingCrates[i] and cObj:GetID() == matchingCrates[i]:GetID() then
        keep = false
        break
      end
    end
    if keep then
      table.insert(newSpawned, cObj)
    end
  end
  self.Spawned_Cargo = newSpawned

  -- 7) Show final message, including a special note if capacity is now reached
  local loadedHere = toLoad
  if loadedHere < needed and loadedData.Cratesloaded >= capacity then
    self:_SendMessage(string.format("Loaded only %d/%d crate(s) of %s. Cargo limit is now reached!", loadedHere, needed, cargoName), 10, false, Group)
  else
    local fullSets = math.floor(loadedHere / needed)
    local leftover = loadedHere % needed
    if needed > 1 then
      if fullSets > 0 and leftover == 0 then
        self:_SendMessage(string.format("Loaded %d %s.", fullSets, cargoName), 10, false, Group)
      elseif fullSets > 0 and leftover > 0 then
        self:_SendMessage(string.format("Loaded %d %s(s), with %d leftover crate(s).", fullSets, cargoName, leftover), 10, false, Group)
      else
        self:_SendMessage(string.format("Loaded only %d/%d crate(s) of %s.", loadedHere, needed, cargoName), 15, false, Group)
      end
    else
      self:_SendMessage(string.format("Loaded %d %s(s).", loadedHere, cargoName), 10, false, Group)
    end
  end

  self:_RefreshLoadCratesMenu(Group, Unit)
  self:_RefreshDropCratesMenu(Group, Unit)
  return self
end


--- (Internal) Function to unload a single crate
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The calling group.
-- @param Wrapper.Unit#UNIT Unit The calling unit.
-- @param #string setIndex The name of the crate to unload
-- @return #CTLD self
function CTLD:_UnloadSingleCrateSet(Group, Unit, setIndex)
  self:T(self.lid .. " _UnloadSingleCrateSet")

  -- Check if we are in a drop zone (unless we drop anywhere)
  if not self.dropcratesanywhere then
    local inzone, zoneName, zone, distance = self:IsUnitInZone(Unit, CTLD.CargoZoneType.DROP)
    if not inzone then
      self:_SendMessage("You are not close enough to a drop zone!", 10, false, Group)
      if not self.debug then 
        return self 
      end
    end
  end

  -- Check if doors must be open
  if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
    self:_SendMessage("You need to open the door(s) to drop cargo!", 10, false, Group)
    if not self.debug then return self end
  end

  -- Check if the crate grouping data is available
  local unitName = Unit:GetName()
  if not self.CrateGroupList or not self.CrateGroupList[unitName] then
    self:_SendMessage("No crate groups found for this unit!", 10, false, Group)
    if not self.debug then return self end
    return self
  end

  -- Find the selected chunk/set by index
  local chunk = self.CrateGroupList[unitName][setIndex]
  if not chunk then
    self:_SendMessage("No crate set found or index invalid!", 10, false, Group)
    if not self.debug then return self end
    return self
  end

  -- Check if the chunk is empty
  if #chunk == 0 then
    self:_SendMessage("No crate found in that set!", 10, false, Group)
    if not self.debug then return self end
    return self
  end

  -- Check hover/airdrop/landed logic
  local grounded = not self:IsUnitInAir(Unit)
  local hoverunload = self:IsCorrectHover(Unit)
  local isHerc = self:IsFixedWing(Unit)
  local isHook = self:IsHook(Unit)
  if isHerc and not isHook then
    hoverunload = self:IsCorrectFlightParameters(Unit)
  end
  if not grounded and not hoverunload then
    if isHerc then
      self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group)
    else
      self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group)
    end
    if not self.debug then return self end
    return self
  end

  -- Get the first crate from this set
  local crateObj = chunk[1]
  if not crateObj then
    self:_SendMessage("No crate found in that set!", 10, false, Group)
    if not self.debug then return self end
    return self
  end

  -- Perform the actual "drop" spawn
  local needed = crateObj:GetCratesNeeded() or 1
  self:_GetCrates(Group, Unit, crateObj, #chunk, true)

  -- Mark all crates in the chunk as dropped
  for _, cObj in ipairs(chunk) do
    cObj:SetWasDropped(true)
    cObj:SetHasMoved(true)
  end
local cname  = crateObj:GetName() or "Unknown"
local count  = #chunk
if needed > 1 then
if count == needed then
    self:_SendMessage(string.format("Dropped %d %s.", 1, cname), 10, false, Group)
else
    self:_SendMessage(string.format("Dropped %d/%d crate(s) of %s.", count, needed, cname), 15, false, Group)
end
else
self:_SendMessage(string.format("Dropped %d %s(s).", count, cname), 10, false, Group)
end
  -- Rebuild the cargo list to remove the dropped crates
  local loadedData = self.Loaded_Cargo[unitName]
  if loadedData and loadedData.Cargo then
    local newList = {}
    local newCratesCount = 0
    for _, cObj in ipairs(loadedData.Cargo) do
      if not cObj:WasDropped() then
        table.insert(newList, cObj)
        local ct = cObj:GetType()
        if ct ~= CTLD_CARGO.Enum.TROOPS and ct ~= CTLD_CARGO.Enum.ENGINEERS then
          newCratesCount = newCratesCount + 1
        end
      end
    end
    loadedData.Cargo = newList
    loadedData.Cratesloaded = newCratesCount
    self.Loaded_Cargo[unitName] = loadedData
  end

  -- Update cargo mass, refresh menu
  self:_UpdateUnitCargoMass(Unit)
  self:_RefreshDropCratesMenu(Group, Unit)
  self:_RefreshLoadCratesMenu(Group, Unit)
  return self
end

--- (Internal) Function to refresh the menu for a single unit after crates dropped.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The calling group.
-- @param Wrapper.Unit#UNIT Unit The calling unit.
-- @return #CTLD self
function CTLD:_RefreshDropCratesMenu(Group, Unit)

    if not Group.CTLDTopmenu then return end
    local topCrates = Group.MyTopCratesMenu
    if not topCrates then return end
    if topCrates.DropCratesMenu then
      topCrates.DropCratesMenu:RemoveSubMenus()
    else
      topCrates.DropCratesMenu = MENU_GROUP:New(Group, "Drop Crates", topCrates)
    end
  
    local dropCratesMenu = topCrates.DropCratesMenu
    local loadedData = self.Loaded_Cargo[Unit:GetName()]
    if not loadedData or not loadedData.Cargo then
      MENU_GROUP_COMMAND:New(Group,"No crates to drop!",dropCratesMenu,function() end)
      return
    end
  
    local cargoByName={}
    local dropableCrates=0
    for _,cObj in ipairs(loadedData.Cargo) do
      if cObj and not cObj:WasDropped() then
        local cType=cObj:GetType()
        if cType~=CTLD_CARGO.Enum.TROOPS and cType~=CTLD_CARGO.Enum.ENGINEERS and cType~=CTLD_CARGO.Enum.GCLOADABLE then
          local name=cObj:GetName()or"Unknown"
          cargoByName[name]=cargoByName[name]or{}
          table.insert(cargoByName[name],cObj)
          dropableCrates=dropableCrates+1
        end
      end
    end
  
    if dropableCrates==0 then
      MENU_GROUP_COMMAND:New(Group,"No crates to drop!",dropCratesMenu,function() end)
      return
    end
  
    ----------------------------------------------------------------------
    -- DEFAULT (“classic”) versus ONE-STEP behaviour
    ----------------------------------------------------------------------
    if not self.onestepmenu then
      --------------------------------------------------------------------
      -- classic menu
      --------------------------------------------------------------------
      MENU_GROUP_COMMAND:New(Group,"Drop ALL crates",dropCratesMenu,self._UnloadCrates,self,Group,Unit)
  
      self.CrateGroupList=self.CrateGroupList or{}
      self.CrateGroupList[Unit:GetName()]={}
  
      local lineIndex=1
      for cName,list in pairs(cargoByName) do
        local needed=list[1]:GetCratesNeeded() or 1
        table.sort(list,function(a,b)return a:GetID()<b:GetID()end)
        local i=1
        while i<=#list do
          local left=(#list-i+1)
          if left>=needed then
            local chunk={}
            for n=i,i+needed-1 do
              table.insert(chunk,list[n])
            end
            local label=string.format("%d. %s",lineIndex,cName)
            table.insert(self.CrateGroupList[Unit:GetName()],chunk)
            local setIndex=#self.CrateGroupList[Unit:GetName()]
            MENU_GROUP_COMMAND:New(Group,label,dropCratesMenu,self._UnloadSingleCrateSet,self,Group,Unit,setIndex)
            i=i+needed
          else
            local chunk={}
            for n=i,#list do
              table.insert(chunk,list[n])
            end
            local label=string.format("%d. %s %d/%d",lineIndex,cName,left,needed)
            table.insert(self.CrateGroupList[Unit:GetName()],chunk)
            local setIndex=#self.CrateGroupList[Unit:GetName()]
            MENU_GROUP_COMMAND:New(Group,label,dropCratesMenu,self._UnloadSingleCrateSet,self,Group,Unit,setIndex)
            i=#list+1
          end
          lineIndex=lineIndex+1
        end
      end
  
    else
      --------------------------------------------------------------------
      -- one-step (enhanced) menu
      --------------------------------------------------------------------
      local mAll=MENU_GROUP:New(Group,"Drop ALL crates",dropCratesMenu)
      MENU_GROUP_COMMAND:New(Group,"Drop",mAll,self._UnloadCrates,self,Group,Unit)
      if not ( self:IsUnitInAir(Unit) and self:IsFixedWing(Unit) ) then
        MENU_GROUP_COMMAND:New(Group,"Drop and build",mAll,self._DropAndBuild,self,Group,Unit)
      end

      self.CrateGroupList=self.CrateGroupList or{}
      self.CrateGroupList[Unit:GetName()]={}
  
      local lineIndex=1
      for cName,list in pairs(cargoByName) do
        local needed=list[1]:GetCratesNeeded() or 1
        table.sort(list,function(a,b)return a:GetID()<b:GetID()end)
        local i=1
        while i<=#list do
          local left=(#list-i+1)
          if left>=needed then
            local chunk={}
            for n=i,i+needed-1 do
              table.insert(chunk,list[n])
            end
            local label=string.format("%d. %s",lineIndex,cName)
            table.insert(self.CrateGroupList[Unit:GetName()],chunk)
            local setIndex=#self.CrateGroupList[Unit:GetName()]
            local mSet=MENU_GROUP:New(Group,label,dropCratesMenu)
            MENU_GROUP_COMMAND:New(Group,"Drop",mSet,self._UnloadSingleCrateSet,self,Group,Unit,setIndex)
            if not ( self:IsUnitInAir(Unit) and self:IsFixedWing(Unit) ) then
            MENU_GROUP_COMMAND:New(Group,"Drop and build",mSet,self._DropSingleAndBuild,self,Group,Unit,setIndex)
            end
            i=i+needed
          else
            local chunk={}
            for n=i,#list do
              table.insert(chunk,list[n])
            end
            local label=string.format("%d. %s %d/%d",lineIndex,cName,left,needed)
            table.insert(self.CrateGroupList[Unit:GetName()],chunk)
            local setIndex=#self.CrateGroupList[Unit:GetName()]
            MENU_GROUP_COMMAND:New(Group,label,dropCratesMenu,self._UnloadSingleCrateSet,self,Group,Unit,setIndex)
            i=#list+1
          end
          lineIndex=lineIndex+1
        end
      end
    end
  end

--- (Internal) Function to unload a single Troop group by ID.
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The calling group.
-- @param Wrapper.Unit#UNIT Unit The calling unit.
-- @param #number chunkID the Cargo ID
-- @return #CTLD self
function CTLD:_UnloadSingleTroopByID(Group, Unit, chunkID)
  self:T(self.lid .. " _UnloadSingleTroopByID chunkID=" .. tostring(chunkID))

  local droppingatbase = false
  local inzone, zonename, zone, distance = self:IsUnitInZone(Unit, CTLD.CargoZoneType.LOAD)
  if not inzone then
    inzone, zonename, zone, distance = self:IsUnitInZone(Unit, CTLD.CargoZoneType.SHIP)
  end
  if inzone then
    droppingatbase = self.returntroopstobase
  end

  if self.pilotmustopendoors and not UTILS.IsLoadingDoorOpen(Unit:GetName()) then
    self:_SendMessage("You need to open the door(s) to unload troops!", 10, false, Group)
    if not self.debug then return self end 
  end

  local hoverunload = self:IsCorrectHover(Unit)
  local isHerc = self:IsFixedWing(Unit)
  local isHook = self:IsHook(Unit)
  if isHerc and not isHook then
    hoverunload = self:IsCorrectFlightParameters(Unit)
  end
  local grounded = not self:IsUnitInAir(Unit)
  local unitName = Unit:GetName()

  if self.Loaded_Cargo[unitName] and (grounded or hoverunload) then
    if not droppingatbase or self.debug then
      if not self.TroopsIDToChunk or not self.TroopsIDToChunk[chunkID] then
        self:_SendMessage(string.format("No troop cargo chunk found for ID %d!", chunkID), 10, false, Group)
        if not self.debug then return self end
        return self
      end

      local chunk = self.TroopsIDToChunk[chunkID]
      if not chunk or #chunk == 0 then
        self:_SendMessage(string.format("Troop chunk is empty for ID %d!", chunkID), 10, false, Group)
        if not self.debug then return self end
        return self
      end

      -- Drop the FIRST cargo in that chunk
      local foundCargo = chunk[1]
      if not foundCargo then
        self:_SendMessage(string.format("No troop cargo at chunk %d!", chunkID), 10, false, Group)
        if not self.debug then return self end
        return self
      end

      local cType = foundCargo:GetType()
      local name  = foundCargo:GetName() or "none"
      local tmpl  = foundCargo:GetTemplates() or {}
      local zoneradius = self.troopdropzoneradius or 100
      local factor = 1
      if isHerc then
        factor = foundCargo:GetCratesNeeded() or 1
        zoneradius = Unit:GetVelocityMPS() or 100
      end
      local zone = ZONE_GROUP:New(string.format("Unload zone-%s", unitName), Group, zoneradius * factor)
      local randomcoord = zone:GetRandomCoordinate(10, 30 * factor)
      local heading = Group:GetHeading() or 0

      if grounded or hoverunload then
        randomcoord = Group:GetCoordinate()
        local Angle = (heading + 270) % 360
        if isHerc or isHook then
          Angle = (heading + 180) % 360
        end
        local offset = hoverunload and self.TroopUnloadDistHover or self.TroopUnloadDistGround
        if isHerc then
          offset = self.TroopUnloadDistGroundHerc or 25
        end
        if isHook then
          offset = self.TroopUnloadDistGroundHook or 15
          if hoverunload and self.TroopUnloadDistHoverHook then
            offset = self.TroopUnloadDistHoverHook or 5
          end
        end
        randomcoord:Translate(offset, Angle, nil, true)
      end

      local tempcount = 0
      if isHook then
        tempcount = self.ChinookTroopCircleRadius or 5
      end
      for _, _template in pairs(tmpl) do
        self.TroopCounter = self.TroopCounter + 1
        tempcount = tempcount + 1
        local alias = string.format("%s-%d", _template, math.random(1,100000))
        local rad   = 2.5 + (tempcount * 2)
        local Positions = self:_GetUnitPositions(randomcoord, rad, heading, _template)
        self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template, alias)
          :InitDelayOff()
          :InitSetUnitAbsolutePositions(Positions)
          :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
          :OnSpawnGroup(function(grp) grp.spawntime = timer.getTime() end)
          :SpawnFromVec2(randomcoord:GetVec2())
        self:__TroopsDeployed(1, Group, Unit, self.DroppedTroops[self.TroopCounter], cType)
      end
      
      foundCargo:SetWasDropped(true)
      if cType == CTLD_CARGO.Enum.ENGINEERS then
        self.Engineers = self.Engineers + 1
          local grpname = self.DroppedTroops[self.TroopCounter]:GetName()
        self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname)
        self:_SendMessage(string.format("Dropped Engineers %s into action!", name), 10, false, Group)
      else
        self:_SendMessage(string.format("Dropped Troops %s into action!", name), 10, false, Group)
      end

      table.remove(chunk, 1)
      if #chunk == 0 then
        self.TroopsIDToChunk[chunkID] = nil
      end

    else
      -- Return to base logic, remove ONLY the first cargo
      self:_SendMessage("Troops have returned to base!", 10, false, Group)
      self:__TroopsRTB(1, Group, Unit, zonename, zone)

      if self.TroopsIDToChunk and self.TroopsIDToChunk[chunkID] then
        local chunk = self.TroopsIDToChunk[chunkID]
        if #chunk > 0 then
          local firstObj = chunk[1]
          local cName = firstObj:GetName()
          local gentroops = self.Cargo_Troops
          for _id, _troop in pairs(gentroops) do
            if _troop.Name == cName then
              local st = _troop:GetStock()
              if st and tonumber(st) >= 0 then
                _troop:AddStock()
              end
            end
          end
          firstObj:SetWasDropped(true)
          table.remove(chunk, 1)
          if #chunk == 0 then
            self.TroopsIDToChunk[chunkID] = nil
          end
        end
      end
    end

    local cargoList = self.Loaded_Cargo[unitName].Cargo
    for i = #cargoList, 1, -1 do
      if cargoList[i]:WasDropped() then
        table.remove(cargoList, i)
      end
    end
    local troopsLoaded = 0
    local cratesLoaded = 0
    for _, cargo in ipairs(cargoList) do
      local cT = cargo:GetType()
      if cT == CTLD_CARGO.Enum.TROOPS or cT == CTLD_CARGO.Enum.ENGINEERS then
        troopsLoaded = troopsLoaded + 1
      else
        cratesLoaded = cratesLoaded + 1
      end
    end
    self.Loaded_Cargo[unitName].Troopsloaded = troopsLoaded
    self.Loaded_Cargo[unitName].Cratesloaded = cratesLoaded
    self:_RefreshDropTroopsMenu(Group, Unit)
  else
    local isHerc = self:IsFixedWing(Unit)
    if isHerc then
      self:_SendMessage("Nothing loaded or not within airdrop parameters!", 10, false, Group)
    else
      self:_SendMessage("Nothing loaded or not hovering within parameters!", 10, false, Group)
    end
  end
  return self
end

--- (Internal) Function to refresh menu for troops on drop for a specific unit
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group The requesting group.
-- @param Wrapper.Unit#UNIT Unit The requesting unit.
-- @return #CTLD self
function CTLD:_RefreshDropTroopsMenu(Group, Unit)
  local theGroup = Group
  local theUnit  = Unit
  if not theGroup.CTLDTopmenu then return end
  local topTroops = theGroup.MyTopTroopsMenu
  if not topTroops then return end
  if topTroops.DropTroopsMenu then
    topTroops.DropTroopsMenu:Remove()
  end
  local dropTroopsMenu = MENU_GROUP:New(theGroup, "Drop Troops", topTroops)
  topTroops.DropTroopsMenu = dropTroopsMenu
  MENU_GROUP_COMMAND:New(theGroup, "Drop ALL troops", dropTroopsMenu, self._UnloadTroops, self, theGroup, theUnit)

  local loadedData = self.Loaded_Cargo[theUnit:GetName()]
  if not loadedData or not loadedData.Cargo then return end

  -- Gather troop cargo by name
  local troopsByName = {}
  for _, cargoObj in ipairs(loadedData.Cargo) do
    if cargoObj
       and (cargoObj:GetType() == CTLD_CARGO.Enum.TROOPS or cargoObj:GetType() == CTLD_CARGO.Enum.ENGINEERS)
       and not cargoObj:WasDropped()
    then
      local name = cargoObj:GetName() or "Unknown"
      troopsByName[name] = troopsByName[name] or {}
      table.insert(troopsByName[name], cargoObj)
    end
  end

  self.TroopsIDToChunk = self.TroopsIDToChunk or {}

  for tName, objList in pairs(troopsByName) do
    table.sort(objList, function(a,b) return a:GetID() < b:GetID() end)
    local count = #objList

    local chunkID = objList[1]:GetID()
    self.TroopsIDToChunk[chunkID] = objList

    local label = string.format("Drop %s (%d)", tName, count)
    MENU_GROUP_COMMAND:New(theGroup, label, dropTroopsMenu, self._UnloadSingleTroopByID, self, theGroup, theUnit, chunkID)
  end
end

--- [Internal] Function to check if a template exists in the mission.
-- @param #CTLD self
-- @param #table temptable Table of string names
-- @return #boolean outcome
function CTLD:_CheckTemplates(temptable)
  self:T(self.lid .. " _CheckTemplates")
  local outcome = true
  if type(temptable) ~= "table" then
    temptable = {temptable}
  end
  for _,_name in pairs(temptable) do
    if not _DATABASE.Templates.Groups[_name] then
      outcome = false
      self:E(self.lid .. "ERROR: Template name " .. _name ..  " is missing!")
    end
  end
  return outcome
end

--- User function - Add *generic* troop type loadable as cargo. This type will load directly into the heli without crates.
-- @param #CTLD self
-- @param #string Name Unique name of this type of troop. E.g. "Anti-Air Small".
-- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP making up this troop.
-- @param #CTLD_CARGO.Enum Type Type of cargo, here TROOPS - these will move to a nearby destination zone when dropped/build.
-- @param #number NoTroops Size of the group in number of Units across combined templates (for loading).
-- @param #number PerTroopMass Mass in kg of each soldier
-- @param #number Stock Number of groups in stock. Nil for unlimited.
-- @param #string SubCategory Name of sub-category (optional).
function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock,SubCategory) 
  self:T(self.lid .. " AddTroopsCargo")
  self:T({Name,Templates,Type,NoTroops,PerTroopMass,Stock})
  if not self:_CheckTemplates(Templates) then
    self:E(self.lid .. "Troops Cargo for " .. Name .. " has missing template(s)!" )
    return self
  end
  self.CargoCounter = self.CargoCounter + 1
  -- Troops are directly loadable
  local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock, SubCategory)
  table.insert(self.Cargo_Troops,cargo)
  if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
  return self
end

--- User function - Add *generic* crate-type loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built.
-- @param #CTLD self
-- @param #string Name Unique name of this type of cargo. E.g. "Humvee".
-- @param #table Templates Table of #string names of late activated Wrapper.Group#GROUP building this cargo.
-- @param #CTLD_CARGO.Enum Type Type of cargo. I.e. VEHICLE or FOB. VEHICLE will move to destination zones when dropped/build, FOB stays put.
-- @param #number NoCrates Number of crates needed to build this cargo.
-- @param #number PerCrateMass Mass in kg of each crate
-- @param #number Stock Number of buildable groups in stock. Nil for unlimited.
-- @param #string SubCategory Name of sub-category (optional).
-- @param #boolean DontShowInMenu (optional) If set to "true" this won't show up in the menu.
-- @param Core.Zone#ZONE Location (optional) If set, the cargo item is **only** available here. Can be a #ZONE object or the name of a zone as #string.
-- @param #string UnitTypes Unit type names (optional). If set, only these unit types can pick up the cargo, e.g. "UH-1H" or {"UH-1H","OH58D"}.
-- @param #string Category Static category name (optional). If set, spawn cargo crate with an alternate category type, e.g. "Cargos".
-- @param #string TypeName Static type name (optional). If set, spawn cargo crate with an alternate type shape, e.g. "iso_container".
-- @param #string ShapeName Static shape name (optional). If set, spawn cargo crate with an alternate type sub-shape, e.g. "iso_container_cargo".
-- @return #CTLD self
function CTLD:AddCratesCargo(Name,Templates,Type,NoCrates,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,Category,TypeName,ShapeName)
  self:T(self.lid .. " AddCratesCargo")
  if not self:_CheckTemplates(Templates) then
    self:E(self.lid .. "Crates Cargo for " .. Name .. " has missing template(s)!" )
    return self
  end
  self.CargoCounter = self.CargoCounter + 1
  -- Crates are not directly loadable
  local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location)
  if UnitTypes then
    cargo:AddUnitTypeName(UnitTypes)
  end
  cargo:SetStaticTypeAndShape("Cargos",self.basetype)
  if TypeName then
    cargo:SetStaticTypeAndShape(Category,TypeName,ShapeName)
  end
  table.insert(self.Cargo_Crates,cargo)
  if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
  return self
end

--- User function - Add *generic* static-type loadable as cargo. This type will create cargo that needs to be loaded, moved and dropped.
-- @param #CTLD self
-- @param #string Name Unique name of this type of cargo as set in the mission editor (note: UNIT name!), e.g. "Ammunition-1".
-- @param #number Mass Mass in kg of each static in kg, e.g. 100.
-- @param #number Stock Number of groups in stock. Nil for unlimited.
-- @param #string SubCategory Name of sub-category (optional).
-- @param #boolean DontShowInMenu (optional) If set to "true" this won't show up in the menu.
-- @param Core.Zone#ZONE Location (optional) If set, the cargo item is **only** available here. Can be a #ZONE object or the name of a zone as #string.
-- @return #CTLD_CARGO CargoObject
function CTLD:AddStaticsCargo(Name,Mass,Stock,SubCategory,DontShowInMenu,Location)
  self:T(self.lid .. " AddStaticsCargo")
  self.CargoCounter = self.CargoCounter + 1
  local type = CTLD_CARGO.Enum.STATIC
  local template = STATIC:FindByName(Name,true):GetTypeName()
  local unittemplate = _DATABASE:GetStaticUnitTemplate(Name)
  local ResourceMap = nil
  if unittemplate and unittemplate.resourcePayload then
    ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload)
  end
  -- Crates are not directly loadable
  local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,Stock,SubCategory,DontShowInMenu,Location)
  cargo:SetStaticResourceMap(ResourceMap)
  table.insert(self.Cargo_Statics,cargo)
  if SubCategory and self.usesubcats ~= true then self.usesubcats=true end
  return cargo
end

--- User function - Get a *generic* static-type loadable as #CTLD_CARGO object.
-- @param #CTLD self
-- @param #string Name Unique Unit(!) name of this type of cargo as set in the mission editor (not: GROUP name!), e.g. "Ammunition-1".
-- @param #number Mass Mass in kg of each static in kg, e.g. 100.
-- @return #CTLD_CARGO Cargo object
function CTLD:GetStaticsCargoFromTemplate(Name,Mass)
  self:T(self.lid .. " GetStaticsCargoFromTemplate")
  self.CargoCounter = self.CargoCounter + 1
  local type = CTLD_CARGO.Enum.STATIC
  local template = STATIC:FindByName(Name,true):GetTypeName()
  local unittemplate = _DATABASE:GetStaticUnitTemplate(Name)
  local ResourceMap = nil
  if unittemplate and unittemplate.resourcePayload then
    ResourceMap = UTILS.DeepCopy(unittemplate.resourcePayload)
  end
  -- Crates are not directly loadable
  local cargo = CTLD_CARGO:New(self.CargoCounter,Name,template,type,false,false,1,nil,nil,Mass,1)
  cargo:SetStaticResourceMap(ResourceMap)
  --table.insert(self.Cargo_Statics,cargo)
  return cargo
end

--- User function - Add *generic* repair crates loadable as cargo. This type will create crates that need to be loaded, moved, dropped and built.
-- @param #CTLD self
-- @param #string Name Unique name of this type of cargo. E.g. "Humvee".
-- @param #string Template Template of VEHICLE or FOB cargo that this can repair. MUST be the same as given in `AddCratesCargo(..)`!
-- @param #CTLD_CARGO.Enum Type Type of cargo, here REPAIR.
-- @param #number NoCrates Number of crates needed to build this cargo.
-- @param #number PerCrateMass Mass in kg of each crate
-- @param #number Stock Number of groups in stock. Nil for unlimited.
-- @param #string SubCategory Name of the sub-category (optional).
-- @param #boolean DontShowInMenu (optional) If set to "true" this won't show up in the menu.
-- @param Core.Zone#ZONE Location (optional) If set, the cargo item is **only** available here. Can be a #ZONE object or the name of a zone as #string.
-- @param #string UnitTypes Unit type names (optional). If set, only these unit types can pick up the cargo, e.g. "UH-1H" or {"UH-1H","OH58D"}
-- @param #string Category Static category name (optional). If set, spawn cargo crate with an alternate category type, e.g. "Cargos".
-- @param #string TypeName Static type name (optional). If set, spawn cargo crate with an alternate type shape, e.g. "iso_container".
-- @param #string ShapeName Static shape name (optional). If set, spawn cargo crate with an alternate type sub-shape, e.g. "iso_container_cargo".
-- @return #CTLD self
function CTLD:AddCratesRepair(Name,Template,Type,NoCrates, PerCrateMass,Stock,SubCategory,DontShowInMenu,Location,UnitTypes,Category,TypeName,ShapeName)
  self:T(self.lid .. " AddCratesRepair")
  if not self:_CheckTemplates(Template) then
    self:E(self.lid .. "Repair Cargo for " .. Name .. " has a missing template!" )
    return self
  end
  self.CargoCounter = self.CargoCounter + 1
  -- Crates are not directly loadable
  local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Template,Type,false,false,NoCrates,nil,nil,PerCrateMass,Stock,SubCategory,DontShowInMenu,Location)
  if UnitTypes then
    cargo:AddUnitTypeName(UnitTypes)
  end
  cargo:SetStaticTypeAndShape("cargos",self.basetype)
  if TypeName then
    cargo:SetStaticTypeAndShape(Category,TypeName,ShapeName)
  end
  table.insert(self.Cargo_Crates,cargo)
  return self
end

--- User function - Add a #CTLD.CargoZoneType zone for this CTLD instance.
-- @param #CTLD self
-- @param #CTLD.CargoZone Zone Zone #CTLD.CargoZone describing the zone.
function CTLD:AddZone(Zone)
  self:T(self.lid .. " AddZone")
  local zone = Zone -- #CTLD.CargoZone
  if zone.type == CTLD.CargoZoneType.LOAD then
    table.insert(self.pickupZones,zone)
  elseif zone.type == CTLD.CargoZoneType.DROP then
    table.insert(self.dropOffZones,zone)
  elseif zone.type == CTLD.CargoZoneType.SHIP then
    table.insert(self.shipZones,zone)
  elseif zone.type == CTLD.CargoZoneType.BEACON then
    table.insert(self.droppedBeacons,zone)   
  else
    table.insert(self.wpZones,zone)
  end
  return self
end

--- User function - Activate Name #CTLD.CargoZone.Type ZoneType for this CTLD instance.
-- @param #CTLD self
-- @param #string Name Name of the zone to change in the ME.
-- @param #CTLD.CargoZoneType ZoneType Type of zone this belongs to.
-- @param #boolean NewState (Optional) Set to true to activate, false to switch off.
function CTLD:ActivateZone(Name,ZoneType,NewState)
  self:T(self.lid .. " ActivateZone")
  local newstate = true
  -- set optional in case we\'re deactivating
  if NewState ~= nil then
    newstate = NewState
  end  
  
  -- get correct table
  local table = {}
  if ZoneType == CTLD.CargoZoneType.LOAD then
    table = self.pickupZones
  elseif ZoneType == CTLD.CargoZoneType.DROP then
    table = self.dropOffZones
  elseif ZoneType == CTLD.CargoZoneType.SHIP then
    table = self.shipZones
  elseif ZoneType == CTLD.CargoZoneType.BEACON then
    table = self.droppedBeacons
  else
    table = self.wpZones
  end
  -- loop table
  for _,_zone in pairs(table) do
    local thiszone = _zone --#CTLD.CargoZone
    if thiszone.name == Name then
      thiszone.active = newstate
      break
    end
  end
  return self
end


--- User function - Deactivate Name #CTLD.CargoZoneType ZoneType for this CTLD instance.
-- @param #CTLD self
-- @param #string Name Name of the zone to change in the ME.
-- @param #CTLD.CargoZoneType ZoneType Type of zone this belongs to.
function CTLD:DeactivateZone(Name,ZoneType)
  self:T(self.lid .. " DeactivateZone")
  self:ActivateZone(Name,ZoneType,false)
  return self
end

--- (Internal) Function to obtain a valid FM frequency.
-- @param #CTLD self
-- @param #string Name Name of zone.
-- @return #CTLD.ZoneBeacon Beacon Beacon table.
function CTLD:_GetFMBeacon(Name)
  self:T(self.lid .. " _GetFMBeacon")
  local beacon = {} -- #CTLD.ZoneBeacon  
  if #self.FreeFMFrequencies <= 1 then
      self.FreeFMFrequencies = self.UsedFMFrequencies
      self.UsedFMFrequencies = {}
  end 
  --random
  local FM = table.remove(self.FreeFMFrequencies, math.random(#self.FreeFMFrequencies))
  table.insert(self.UsedFMFrequencies, FM)  
  beacon.name = Name
  beacon.frequency = FM / 1000000
  beacon.modulation = CTLD.RadioModulation.FM
  return beacon
end

--- (Internal) Function to obtain a valid UHF frequency.
-- @param #CTLD self
-- @param #string Name Name of zone.
-- @return #CTLD.ZoneBeacon Beacon Beacon table.
function CTLD:_GetUHFBeacon(Name)
  self:T(self.lid .. " _GetUHFBeacon")
  local beacon = {} -- #CTLD.ZoneBeacon  
  if #self.FreeUHFFrequencies <= 1 then
      self.FreeUHFFrequencies = self.UsedUHFFrequencies
      self.UsedUHFFrequencies = {}
  end 
  --random
  local UHF = table.remove(self.FreeUHFFrequencies, math.random(#self.FreeUHFFrequencies))
  table.insert(self.UsedUHFFrequencies, UHF)
  beacon.name = Name
  beacon.frequency = UHF / 1000000
  beacon.modulation = CTLD.RadioModulation.AM

  return beacon
end

--- (Internal) Function to obtain a valid VHF frequency.
-- @param #CTLD self
-- @param #string Name Name of zone.
-- @return #CTLD.ZoneBeacon Beacon Beacon table.
function CTLD:_GetVHFBeacon(Name)
  self:T(self.lid .. " _GetVHFBeacon")
  local beacon = {} -- #CTLD.ZoneBeacon
  if #self.FreeVHFFrequencies <= 3 then
      self.FreeVHFFrequencies = self.UsedVHFFrequencies
      self.UsedVHFFrequencies = {}
  end
  --get random
  local VHF = table.remove(self.FreeVHFFrequencies, math.random(#self.FreeVHFFrequencies))
  table.insert(self.UsedVHFFrequencies, VHF)
  beacon.name = Name
  beacon.frequency = VHF / 1000000
  beacon.modulation = CTLD.RadioModulation.FM
  return beacon
end


--- User function - Creates and adds a #CTLD.CargoZone zone for this CTLD instance.
--  Zones of type LOAD: Players load crates and troops here.  
--  Zones of type DROP: Players can drop crates here. Note that troops can be unloaded anywhere.  
--  Zone of type MOVE: Dropped troops and vehicles will start moving to the nearest zone of this type (also see options).  
-- @param #CTLD self
-- @param #string Name Name of this zone, as in Mission Editor.
-- @param #string Type Type of this zone, #CTLD.CargoZoneType
-- @param #number Color Smoke/Flare color e.g. #SMOKECOLOR.Red
-- @param #string Active Is this zone currently active?
-- @param #string HasBeacon Does this zone have a beacon if it is active?
-- @param #number Shiplength Length of Ship for shipzones
-- @param #number Shipwidth Width of Ship for shipzones
-- @return #CTLD self
function CTLD:AddCTLDZone(Name, Type, Color, Active, HasBeacon, Shiplength, Shipwidth)
  self:T(self.lid .. " AddCTLDZone")
  
  local zone = ZONE:FindByName(Name)
  if not zone and  Type ~= CTLD.CargoZoneType.SHIP then
    self:E(self.lid.."**** Zone does not exist: "..Name)
    return self
  end
  
  if Type == CTLD.CargoZoneType.SHIP then
  local Ship = UNIT:FindByName(Name)
  if not Ship then
      self:E(self.lid.."**** Ship does not exist: "..Name)
    return self
  end
  end

  local exists = true
  local ctldzone = self:GetCTLDZone(Name, Type) -- #CTLD.CargoZone
  if not ctldzone then
    exists = false
    ctldzone = {}
  end

  ctldzone.active = Active or false
  ctldzone.color = Color or SMOKECOLOR.Red
  ctldzone.name = Name or "NONE"
  ctldzone.type = Type or CTLD.CargoZoneType.MOVE -- #CTLD.CargoZoneType
  ctldzone.hasbeacon = HasBeacon or false
  
  if Type == CTLD.CargoZoneType.BEACON then
    self.droppedbeaconref[ctldzone.name] = zone:GetCoordinate()
    ctldzone.timestamp = timer.getTime()
  end
     
  if HasBeacon then
    ctldzone.fmbeacon = self:_GetFMBeacon(Name)
    ctldzone.uhfbeacon = self:_GetUHFBeacon(Name)
    ctldzone.vhfbeacon = self:_GetVHFBeacon(Name)
  else
    ctldzone.fmbeacon = nil
    ctldzone.uhfbeacon = nil
    ctldzone.vhfbeacon = nil
  end
  
  if Type == CTLD.CargoZoneType.SHIP then
   ctldzone.shiplength = Shiplength or 100
   ctldzone.shipwidth = Shipwidth or 10
  end

  if not exists then
    self:AddZone(ctldzone)
  end
  return self
end


--- User function - find #CTLD.CargoZone zone by name.
-- @param #CTLD self
-- @param #string Name Name of this zone.
-- @param #string Type Type of this zone, #CTLD.CargoZoneType
-- @return #CTLD.CargoZone self
function CTLD:GetCTLDZone(Name, Type)

  if Type == CTLD.CargoZoneType.LOAD then
    for _, z in pairs(self.pickupZones) do
        if z.name == Name then
            return z
        end
    end
  elseif Type == CTLD.CargoZoneType.DROP then
    for _, z in pairs(self.dropOffZones) do
        if z.name == Name then
            return z
        end
    end
  elseif Type == CTLD.CargoZoneType.SHIP then
    for _, z in pairs(self.shipZones) do
        if z.name == Name then
            return z
        end
    end
  elseif Type == CTLD.CargoZoneType.BEACON then
    for _, z in pairs(self.droppedBeacons) do
        if z.name == Name then
            return z
        end
    end
  else
    for _, z in pairs(self.wpZones) do
        if z.name == Name then
            return z
        end
    end
  end

  return nil
end

--- User function - Creates and adds a #CTLD.CargoZone zone for this CTLD instance from an Airbase or FARP name.
--  Zones of type LOAD: Players load crates and troops here.  
--  Zones of type DROP: Players can drop crates here. Note that troops can be unloaded anywhere.  
--  Zone of type MOVE: Dropped troops and vehicles will start moving to the nearest zone of this type (also see options).  
-- @param #CTLD self
-- @param #string AirbaseName Name of the Airbase, can be e.g. AIRBASE.Caucasus.Beslan or "Beslan". For FARPs, this will be the UNIT name.
-- @param #string Type Type of this zone, #CTLD.CargoZoneType
-- @param #number Color Smoke/Flare color e.g. #SMOKECOLOR.Red
-- @param #string Active Is this zone currently active?
-- @param #string HasBeacon Does this zone have a beacon if it is active?
-- @return #CTLD self
function CTLD:AddCTLDZoneFromAirbase(AirbaseName, Type, Color, Active, HasBeacon)
  self:T(self.lid .. " AddCTLDZoneFromAirbase")
  local AFB = AIRBASE:FindByName(AirbaseName)
  local name = AFB:GetZone():GetName()
  self:T(self.lid .. "AFB " .. AirbaseName .. " ZoneName " .. name)
  self:AddCTLDZone(name, Type, Color, Active, HasBeacon)
  return self
end

--- (Internal) Function to create a dropped beacon
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit
-- @return #CTLD self
function CTLD:DropBeaconNow(Unit)
  self:T(self.lid .. " DropBeaconNow")
  
  local ctldzone = {} -- #CTLD.CargoZone
  ctldzone.active = true
  ctldzone.color = math.random(0,4) -- random color
  ctldzone.name = "Beacon " .. math.random(1,10000)
  ctldzone.type = CTLD.CargoZoneType.BEACON -- #CTLD.CargoZoneType
  ctldzone.hasbeacon = true
   
  ctldzone.fmbeacon = self:_GetFMBeacon(ctldzone.name)
  ctldzone.uhfbeacon = self:_GetUHFBeacon(ctldzone.name)
  ctldzone.vhfbeacon = self:_GetVHFBeacon(ctldzone.name)
  ctldzone.timestamp = timer.getTime()
  
  self.droppedbeaconref[ctldzone.name] = Unit:GetCoordinate()
  
  self:AddZone(ctldzone)
  
  local FMbeacon = ctldzone.fmbeacon -- #CTLD.ZoneBeacon
  local VHFbeacon = ctldzone.vhfbeacon -- #CTLD.ZoneBeacon
  local UHFbeacon = ctldzone.uhfbeacon -- #CTLD.ZoneBeacon
  local Name = ctldzone.name
  local FM = FMbeacon.frequency  -- MHz
  local VHF = VHFbeacon.frequency * 1000 -- KHz
  local UHF = UHFbeacon.frequency  -- MHz
  local text = string.format("Dropped %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", Name, FM, VHF, UHF)
  
  self:_SendMessage(text,15,false,Unit:GetGroup())
  
  return self
end

--- (Internal) Housekeeping dropped beacons.
-- @param #CTLD self
-- @return #CTLD self
function CTLD:CheckDroppedBeacons()
  self:T(self.lid .. " CheckDroppedBeacons")
  
  -- check for timeout
  local timeout = self.droppedbeacontimeout or 600
  local livebeacontable = {}
  
  for _,_beacon in pairs (self.droppedBeacons) do
    local beacon = _beacon -- #CTLD.CargoZone
    if not beacon.timestamp then beacon.timestamp = timer.getTime() + timeout end
    local T0 = beacon.timestamp 
    if timer.getTime() - T0 > timeout then
      local name = beacon.name
      self.droppedbeaconref[name] = nil
      _beacon = nil
    else
      table.insert(livebeacontable,beacon)
    end
  end
  
  self.droppedBeacons = nil
  self.droppedBeacons = livebeacontable
  
  return self
end

--- (Internal) Function to show list of radio beacons
-- @param #CTLD self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Unit#UNIT Unit
function CTLD:_ListRadioBeacons(Group, Unit)
  self:T(self.lid .. " _ListRadioBeacons")
  local report = REPORT:New("Active Zone Beacons")
  report:Add("------------------------------------------------------------")
  local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones, [5] = self.droppedBeacons}
  for i=1,5 do
    for index,cargozone in pairs(zones[i]) do
      -- Get Beacon object from zone
      local czone = cargozone -- #CTLD.CargoZone
      if czone.active and czone.hasbeacon then
        local FMbeacon = czone.fmbeacon -- #CTLD.ZoneBeacon
        local VHFbeacon = czone.vhfbeacon -- #CTLD.ZoneBeacon
        local UHFbeacon = czone.uhfbeacon -- #CTLD.ZoneBeacon
        local Name = czone.name
        local FM = FMbeacon.frequency  -- MHz
        local VHF = VHFbeacon.frequency * 1000 -- KHz
        local UHF = UHFbeacon.frequency  -- MHz
        report:AddIndent(string.format(" %s | FM %s Mhz | VHF %s KHz | UHF %s Mhz ", Name, FM, VHF, UHF),"|")
      end
    end
  end
  if report:GetCount() == 1 then
    report:Add("        N O N E")
  end
  report:Add("------------------------------------------------------------")
  self:_SendMessage(report:Text(), 30, true, Group) 
  return self
end

--- (Internal) Add radio beacon to zone. Runs 30 secs.
-- @param #CTLD self
-- @param #string Name Name of zone.
-- @param #string Sound Name of soundfile.
-- @param #number Mhz Frequency in Mhz.
-- @param #number Modulation Modulation AM or FM.
-- @param #boolean IsShip If true zone is a ship.
-- @param #boolean IsDropped If true, this isn't a zone but a dropped beacon
function CTLD:_AddRadioBeacon(Name, Sound, Mhz, Modulation, IsShip, IsDropped)
  self:T(self.lid .. " _AddRadioBeacon")
  local Zone = nil
  if IsShip then
    Zone = UNIT:FindByName(Name)
  elseif IsDropped then
    Zone = self.droppedbeaconref[Name]
  else
    Zone = ZONE:FindByName(Name)
    if not Zone then
      Zone = AIRBASE:FindByName(Name):GetZone()
    end
  end
  local Sound = Sound or "beacon.ogg"
  if Zone then
  if IsDropped then
    local ZoneCoord = Zone
    local ZoneVec3 = ZoneCoord:GetVec3() or {x=0,y=0,z=0}
    local Frequency = Mhz * 1000000 -- Freq in Hertz
    local Sound =  self.RadioPath..Sound
    trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000, Name..math.random(1,10000)) -- Beacon in MP only runs for 30secs straight
    self:T2(string.format("Beacon added | Name = %s | Sound = %s | Vec3 = %d %d %d | Freq = %f | Modulation = %d (0=AM/1=FM)",Name,Sound,ZoneVec3.x,ZoneVec3.y,ZoneVec3.z,Mhz,Modulation))
  else
    local ZoneCoord = Zone:GetCoordinate()
    local ZoneVec3 = ZoneCoord:GetVec3() or {x=0,y=0,z=0}
    local Frequency = Mhz * 1000000 -- Freq in Hert
    local Sound =  self.RadioPath..Sound
    trigger.action.radioTransmission(Sound, ZoneVec3, Modulation, false, Frequency, 1000, Name..math.random(1,10000)) -- Beacon in MP only runs for 30secs straightt
    self:T2(string.format("Beacon added | Name = %s | Sound = %s | Vec3 = {x=%d, y=%d, z=%d} | Freq = %f | Modulation = %d (0=AM/1=FM)",Name,Sound,ZoneVec3.x,ZoneVec3.y,ZoneVec3.z,Mhz,Modulation))
    end
  else
  self:E(self.lid.."***** _AddRadioBeacon: Zone does not exist: "..Name)
  end
  return self
end

--- Set folder path where the CTLD sound files are located **within you mission (miz) file**.
-- The default path is "l10n/DEFAULT/" but sound files simply copied there will be removed by DCS the next time you save the mission.
-- However, if you create a new folder inside the miz file, which contains the sounds, it will not be deleted and can be used.
-- @param #CTLD self
-- @param #string FolderPath The path to the sound files, e.g. "CTLD_Soundfiles/".
-- @return #CTLD self
function CTLD:SetSoundfilesFolder( FolderPath )
  self:T(self.lid .. " SetSoundfilesFolder")
  -- Check that it ends with /
  if FolderPath then
      local lastchar = string.sub( FolderPath, -1 )
      if lastchar ~= "/" then
          FolderPath = FolderPath .. "/"
      end
  end

  -- Folderpath.
  self.RadioPath = FolderPath

  -- Info message.
  self:I( self.lid .. string.format( "Setting sound files folder to: %s", self.RadioPath ) )

  return self
end

--- (Internal) Function to refresh radio beacons
-- @param #CTLD self
function CTLD:_RefreshRadioBeacons()
  self:T(self.lid .. " _RefreshRadioBeacons")

  local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones, [5] = self.droppedBeacons}
  for i=1,5 do
    local IsShip = false
    if i == 4 then IsShip = true end
    local IsDropped = false
    if i == 5 then IsDropped = true end
    for index,cargozone in pairs(zones[i]) do
      -- Get Beacon object from zone
      local czone = cargozone -- #CTLD.CargoZone
      local Sound = self.RadioSound
      local Silent = self.RadioSoundFC3 or self.RadioSound
      if czone.active and czone.hasbeacon then
        local FMbeacon = czone.fmbeacon -- #CTLD.ZoneBeacon
        local VHFbeacon = czone.vhfbeacon -- #CTLD.ZoneBeacon
        local UHFbeacon = czone.uhfbeacon -- #CTLD.ZoneBeacon
        local Name = czone.name
        local FM = FMbeacon.frequency  -- MHz
        local VHF = VHFbeacon.frequency -- KHz
        local UHF = UHFbeacon.frequency  -- MHz   
        self:_AddRadioBeacon(Name,Sound,FM, CTLD.RadioModulation.FM, IsShip, IsDropped)
        self:_AddRadioBeacon(Name,Sound,VHF,CTLD.RadioModulation.AM, IsShip, IsDropped)
        self:_AddRadioBeacon(Name,Silent,UHF,CTLD.RadioModulation.AM, IsShip, IsDropped)
      end
    end
  end
  return self
end

--- (Internal) Function to see if a unit is in a specific zone type.
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit Unit
-- @param #CTLD.CargoZoneType Zonetype Zonetype
-- @return #boolean Outcome Is in zone or not
-- @return #string name Closest zone name
-- @return Core.Zone#ZONE zone Closest Core.Zone#ZONE object
-- @return #number distance Distance to closest zone
-- @return #number width Radius of zone or width of ship
function CTLD:IsUnitInZone(Unit,Zonetype)
  self:T(self.lid .. " IsUnitInZone")
  self:T(Zonetype)
  local unitname = Unit:GetName()
  local zonetable = {}
  local outcome = false
  if Zonetype == CTLD.CargoZoneType.LOAD then
    zonetable = self.pickupZones -- #table
  elseif Zonetype == CTLD.CargoZoneType.DROP then
    zonetable = self.dropOffZones -- #table
  elseif Zonetype == CTLD.CargoZoneType.SHIP then
    zonetable = self.shipZones -- #table
  else 
   zonetable = self.wpZones -- #table
  end
  --- now see if we\'re in
  local zonecoord = nil
  local colorret = nil
  local maxdist = 1000000 -- 100km
  local zoneret = nil
  local zonewret = nil
  local zonenameret = nil
  local unitcoord = Unit:GetCoordinate()
  local unitVec2 = unitcoord:GetVec2()
  for _,_cargozone in pairs(zonetable) do
    local czone = _cargozone -- #CTLD.CargoZone
    local zonename = czone.name
    local active = czone.active
    local color = czone.color
    local zone = nil
    local zoneradius = 100
    local zonewidth = 20
    if Zonetype == CTLD.CargoZoneType.SHIP then
      self:T("Checking Type Ship: "..zonename)
      local ZoneUNIT = UNIT:FindByName(zonename)
      if not ZoneUNIT then return false end
      zonecoord = ZoneUNIT:GetCoordinate()
      zoneradius = czone.shiplength
      zonewidth = czone.shipwidth
      zone = ZONE_UNIT:New( ZoneUNIT:GetName(), ZoneUNIT, zoneradius/2)
    elseif ZONE:FindByName(zonename) then
      zone = ZONE:FindByName(zonename)
      self:T("Checking Zone: "..zonename)
      zonecoord = zone:GetCoordinate()
      --zoneradius = 1500
      zonewidth = zoneradius
    elseif AIRBASE:FindByName(zonename) then
      zone = AIRBASE:FindByName(zonename):GetZone()
      self:T("Checking Zone: "..zonename)
      zonecoord = zone:GetCoordinate()
      zoneradius = 2000
      zonewidth = zoneradius
    end
    local distance = self:_GetDistance(zonecoord,unitcoord)
    self:T("Distance Zone: "..distance)
    self:T("Zone Active: "..tostring(active))
    if (zone:IsVec2InZone(unitVec2) or Zonetype == CTLD.CargoZoneType.MOVE) and active == true and distance < maxdist then 
      outcome = true
      maxdist = distance
      zoneret = zone 
      zonenameret = zonename
      zonewret = zonewidth
      colorret = color 
    end
  end
  if Zonetype == CTLD.CargoZoneType.SHIP then
    return outcome, zonenameret, zoneret, maxdist, zonewret
  else
    return outcome, zonenameret, zoneret, maxdist
  end
end

--- User function - Drop a smoke or flare at current location.
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit The Unit.
-- @param #boolean Flare If true, flare instead.
-- @param #number SmokeColor Color enumerator for smoke, e.g. SMOKECOLOR.Red
function CTLD:SmokePositionNow(Unit, Flare, SmokeColor)
  self:T(self.lid .. " SmokePositionNow")
  local Smokecolor = self.SmokeColor or SMOKECOLOR.Red
  if SmokeColor then 
    Smokecolor = SmokeColor
  end
  local FlareColor = self.FlareColor or FLARECOLOR.Red
  -- table of #CTLD.CargoZone table
  local unitcoord = Unit:GetCoordinate() -- Core.Point#COORDINATE
  local Group = Unit:GetGroup()
  if Flare then
    unitcoord:Flare(FlareColor, 90)
  else
    local height = unitcoord:GetLandHeight() + 2
    unitcoord.y = height
    unitcoord:Smoke(Smokecolor)
  end
  return self
end

--- User function - Start smoke/flare in a zone close to the Unit.
-- @param #CTLD self
-- @param Wrapper.Unit#UNIT Unit The Unit.
-- @param #boolean Flare If true, flare instead.
function CTLD:SmokeZoneNearBy(Unit, Flare)
  self:T(self.lid .. " SmokeZoneNearBy")
  -- table of #CTLD.CargoZone table
  local unitcoord = Unit:GetCoordinate()
  local Group = Unit:GetGroup()
  local smokedistance = self.smokedistance
  local smoked = false
  local zones = {[1] = self.pickupZones, [2] = self.wpZones, [3] = self.dropOffZones, [4] = self.shipZones}
  for i=1,4 do
    for index,cargozone in pairs(zones[i]) do
      local CZone = cargozone --#CTLD.CargoZone
      local zonename = CZone.name
      local zone = nil -- Core.Zone#ZONE_RADIUS
      local airbasezone = false
      if i == 4 then
        zone = UNIT:FindByName(zonename)
      else
        zone = ZONE:FindByName(zonename)
        if not zone then
          zone = AIRBASE:FindByName(zonename):GetZone()
          airbasezone = true
        end
      end
      local zonecoord = zone:GetCoordinate()
      -- Avoid smoke/flares on runways
      if (i==1 or 1==3) and airbasezone==true and zone:IsInstanceOf("ZONE_BASE") then
        zonecoord = zone:GetRandomCoordinate(inner,outer,{land.SurfaceType.LAND})
      end
    if zonecoord then
      local active = CZone.active
      local color = CZone.color
      local distance = self:_GetDistance(zonecoord,unitcoord)
      if distance < smokedistance and active then
        -- smoke zone since we\'re nearby
        if not Flare then 
          zonecoord:Smoke(color or SMOKECOLOR.White)
        else
          if color == SMOKECOLOR.Blue then color = FLARECOLOR.White end
          zonecoord:Flare(color or FLARECOLOR.White)
        end
        local txt = "smoking"
        if Flare then txt = "flaring" end
        self:_SendMessage(string.format("Roger, %s zone %s!",txt, zonename), 10, false, Group)
        smoked = true
      end
     end
    end
  end
  if not smoked then
    local distance = UTILS.MetersToNM(self.smokedistance)
    self:_SendMessage(string.format("Negative, need to be closer than %dnm to a zone!",distance), 10, false, Group)
  end
    return self 
end

  --- User - Function to add/adjust unittype capabilities.
  -- @param #CTLD self
  -- @param #string Unittype The unittype to adjust. If passed as Wrapper.Unit#UNIT, it will search for the unit in the mission.
  -- @param #boolean Cancrates Unit can load crates. Default false.
  -- @param #boolean Cantroops Unit can load troops. Default false.
  -- @param #number Cratelimit Unit can carry number of crates. Default 0.
  -- @param #number Trooplimit Unit can carry number of troops. Default 0.
  -- @param #number Length Unit lenght (in metres) for the load radius. Default 20.
  -- @param #number Maxcargoweight Maxmimum weight in kgs this helo can carry. Default 500.
  function CTLD:SetUnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight)
    self:T(self.lid .. " UnitCapabilities")
    local unittype =  nil
    local unit = nil
    if type(Unittype) == "string" then
      unittype = Unittype
    elseif type(Unittype) == "table" and Unittype.ClassName and Unittype:IsInstanceOf("UNIT") then
      unittype = Unittype:GetTypeName()
    else
      return self
    end
    local length = 20
    local maxcargo = 500
    local existingcaps = self.UnitTypeCapabilities[unittype] -- #CTLD.UnitTypeCapabilities
    if existingcaps then
      length = existingcaps.length or 20
      maxcargo = existingcaps.cargoweightlimit or 500
    end
    -- set capabilities
    local capabilities = {} -- #CTLD.UnitTypeCapabilities
    capabilities.type = unittype
    capabilities.crates = Cancrates or false
    capabilities.troops = Cantroops or false
    capabilities.cratelimit = Cratelimit or  0
    capabilities.trooplimit = Trooplimit or 0
    capabilities.length = Length or length
    capabilities.cargoweightlimit = Maxcargoweight or maxcargo
    self.UnitTypeCapabilities[unittype] = capabilities
    return self
  end
  
  --- User - Function to add onw SET_GROUP Set-up for pilot filtering and assignment.
  -- Needs to be set before starting the CTLD instance.
  -- @param #CTLD self
  -- @param Core.Set#SET_GROUP Set The SET_GROUP object created by the mission designer/user to represent the CTLD pilot groups.
  -- @return #CTLD self 
  function CTLD:SetOwnSetPilotGroups(Set)
    self.UserSetGroup = Set
    return self
  end
  
  --- [Deprecated] - Function to add/adjust unittype capabilities. Has been replaced with `SetUnitCapabilities()` - pls use the new one going forward!
  -- @param #CTLD self
  -- @param #string Unittype The unittype to adjust. If passed as Wrapper.Unit#UNIT, it will search for the unit in the mission.
  -- @param #boolean Cancrates Unit can load crates. Default false.
  -- @param #boolean Cantroops Unit can load troops. Default false.
  -- @param #number Cratelimit Unit can carry number of crates. Default 0.
  -- @param #number Trooplimit Unit can carry number of troops. Default 0.
  -- @param #number Length Unit lenght (in metres) for the load radius. Default 20.
  -- @param #number Maxcargoweight Maxmimum weight in kgs this helo can carry. Default 500.
  function CTLD:UnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight)
    self:I(self.lid.."This function been replaced with `SetUnitCapabilities()` - pls use the new one going forward!")
    self:SetUnitCapabilities(Unittype, Cancrates, Cantroops, Cratelimit, Trooplimit, Length, Maxcargoweight)
    return self
  end
  
  
  --- (Internal) Check if a unit is hovering *in parameters*.
  -- @param #CTLD self
  -- @param Wrapper.Unit#UNIT Unit
  -- @return #boolean Outcome
  function CTLD:IsCorrectHover(Unit)
    self:T(self.lid .. " IsCorrectHover")
    local outcome = false
    -- see if we are in air and within parameters.
    if self:IsUnitInAir(Unit) then
      -- get speed and height
      local uspeed = Unit:GetVelocityMPS()
      local uheight = Unit:GetHeight()
      local ucoord = Unit:GetCoordinate()
      if not ucoord then
        return false
      end
      local gheight = ucoord:GetLandHeight()
      local aheight = uheight - gheight -- height above ground
      local maxh = self.maximumHoverHeight -- 15
      local minh =  self.minimumHoverHeight -- 5
      local mspeed = 2 -- 2 m/s
      if (uspeed <= mspeed) and (aheight <= maxh) and (aheight >= minh)  then 
        -- yep within parameters
        outcome = true
      end
    end
    return outcome
  end
  
    --- (Internal) Check if a Hercules is flying *in parameters* for air drops.
  -- @param #CTLD self
  -- @param Wrapper.Unit#UNIT Unit
  -- @return #boolean Outcome
  function CTLD:IsCorrectFlightParameters(Unit)
    self:T(self.lid .. " IsCorrectFlightParameters")
    local outcome = false
    -- see if we are in air and within parameters.
    if self:IsUnitInAir(Unit) then
      -- get speed and height
      local uspeed = Unit:GetVelocityMPS()
      local uheight = Unit:GetHeight()
      local ucoord = Unit:GetCoordinate()
      if not ucoord then
        return false
      end
      local gheight = ucoord:GetLandHeight()
      local aheight = uheight - gheight -- height above ground
      local minh = self.FixedMinAngels-- 1500m
      local maxh =  self.FixedMaxAngels -- 5000m
      local maxspeed =  self.FixedMaxSpeed -- 77 mps
      -- DONE: TEST - Speed test for Herc, should not be above 280kph/150kn
      local kmspeed = uspeed * 3.6
      local knspeed = kmspeed / 1.86
      self:T(string.format("%s Unit parameters: at %dm AGL with %dmps | %dkph | %dkn",self.lid,aheight,uspeed,kmspeed,knspeed))
      if (aheight <= maxh) and (aheight >= minh) and (uspeed <= maxspeed) then 
        -- yep within parameters
        outcome = true
      end
    end
    return outcome
  end
  
  --- (Internal) List if a unit is hovering *in parameters*.
  -- @param #CTLD self
  -- @param Wrapper.Group#GROUP Group
  -- @param Wrapper.Unit#UNIT Unit
  function CTLD:_ShowHoverParams(Group,Unit)
    local inhover = self:IsCorrectHover(Unit)
    local htxt = "true"
    if not inhover then htxt = "false" end
    local text = ""
    if _SETTINGS:IsMetric() then
      text = string.format("Hover parameters (autoload/drop):\n - Min height %dm \n - Max height %dm \n - Max speed 2mps \n - In parameter: %s", self.minimumHoverHeight, self.maximumHoverHeight, htxt)
    else
      local minheight = UTILS.MetersToFeet(self.minimumHoverHeight)
      local maxheight = UTILS.MetersToFeet(self.maximumHoverHeight)
      text = string.format("Hover parameters (autoload/drop):\n - Min height %dft \n - Max height %dft \n - Max speed 6ftps \n - In parameter: %s", minheight, maxheight, htxt)
    end
    self:_SendMessage(text, 10, false, Group)
    return self
  end
  
    --- (Internal) List if a Herc unit is flying *in parameters*.
  -- @param #CTLD self
  -- @param Wrapper.Group#GROUP Group
  -- @param Wrapper.Unit#UNIT Unit
  function CTLD:_ShowFlightParams(Group,Unit)
    local inhover = self:IsCorrectFlightParameters(Unit)
    local htxt = "true"
    if not inhover then htxt = "false" end
    local text = ""
    if _SETTINGS:IsImperial() then
      local minheight = UTILS.MetersToFeet(self.FixedMinAngels)
      local maxheight = UTILS.MetersToFeet(self.FixedMaxAngels)
      text = string.format("Flight parameters (airdrop):\n - Min height %dft \n - Max height %dft \n - In parameter: %s", minheight, maxheight, htxt)
    else
      local minheight = self.FixedMinAngels
      local maxheight = self.FixedMaxAngels
      text = string.format("Flight parameters (airdrop):\n - Min height %dm \n - Max height %dm \n - In parameter: %s", minheight, maxheight, htxt)
    end
    self:_SendMessage(text, 10, false, Group)
    return self
  end
    
  --- (Internal) Check if a unit is in a load zone and is hovering in parameters.
  -- @param #CTLD self
  -- @param Wrapper.Unit#UNIT Unit
  -- @return #boolean Outcome
  function CTLD:CanHoverLoad(Unit)
    self:T(self.lid .. " CanHoverLoad")
    if self:IsFixedWing(Unit) then return false end
    local outcome = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) and self:IsCorrectHover(Unit)
    if not outcome then
      outcome = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) --and self:IsCorrectHover(Unit)
    end
    return outcome
  end
  
  --- (Internal) Check if a unit is above ground.
  -- @param #CTLD self
  -- @param Wrapper.Unit#UNIT Unit
  -- @return #boolean Outcome
  function CTLD:IsUnitInAir(Unit)
    -- get speed and height
    local minheight = self.minimumHoverHeight
    if self.enableFixedWing and self:IsFixedWing(Unit) then
      minheight = 5.1 -- herc is 5m AGL on the ground
    end
    local uheight = Unit:GetHeight()
    local ucoord = Unit:GetCoordinate()
    if not ucoord then
      return false
    end
    local gheight = ucoord:GetLandHeight()
    local aheight = uheight - gheight -- height above ground
    if aheight >= minheight then
      return true
    else
      return false
    end
  end
  
  --- (Internal) Autoload if we can do crates, have capacity free and are in a load zone.
  -- @param #CTLD self
  -- @param Wrapper.Unit#UNIT Unit
  -- @return #CTLD self
  function CTLD:AutoHoverLoad(Unit)
    self:T(self.lid .. " AutoHoverLoad")
    -- get capabilities and current load
    local unittype = Unit:GetTypeName()
    local unitname = Unit:GetName()
    local Group = Unit:GetGroup()
    local capabilities = self:_GetUnitCapabilities(Unit) -- #CTLD.UnitTypeCapabilities
    local cancrates = capabilities.crates -- #boolean
    local cratelimit = capabilities.cratelimit -- #number
    if cancrates then
      -- get load
      local numberonboard = 0
      local loaded = {}
      if self.Loaded_Cargo[unitname] then
        loaded = self.Loaded_Cargo[unitname] -- #CTLD.LoadedCargo
        numberonboard = loaded.Cratesloaded or 0
      end
      local load = cratelimit - numberonboard
      local canload = self:CanHoverLoad(Unit)
      if canload and load > 0 then
        self:_LoadCratesNearby(Group,Unit)
      end
    end
    return self
  end
  
  --- (Internal) Run through all pilots and see if we autoload.
  -- @param #CTLD self
  -- @return #CTLD self
  function CTLD:CheckAutoHoverload()
    if self.hoverautoloading then
      for _,_pilot in pairs (self.CtldUnits) do
        local Unit = UNIT:FindByName(_pilot)
        if self:CanHoverLoad(Unit) then self:AutoHoverLoad(Unit) end
      end
    end
    return self
  end
  
  --- (Internal) Run through DroppedTroops and capture alive units
  -- @param #CTLD self
  -- @return #CTLD self
  function CTLD:CleanDroppedTroops()
    -- Troops
    local troops = self.DroppedTroops
    local newtable = {}
    for _index, _group in pairs (troops) do
      self:T({_group.ClassName})
      if _group and _group.ClassName == "GROUP" then
        if _group:IsAlive() then
          newtable[_index] = _group
        end
      end
    end
    self.DroppedTroops = newtable
    -- Engineers
    local engineers = self.EngineersInField
    local engtable = {}
    for _index, _group in pairs (engineers) do
      self:T({_group.ClassName})
      if _group and _group:IsNotStatus("Stopped") then
        engtable[_index] = _group
      end
    end
    self.EngineersInField = engtable
    return self
  end
  
  --- User - Count both the stock and groups in the field for available cargo types. Counts only limited cargo items and only troops and vehicle/FOB crates!
  -- @param #CTLD self
  -- @param #boolean Restock If true, restock the cargo and troop items.
  -- @param #number Threshold Percentage below which to restock, used in conjunction with Restock (must be true). Defaults to 75 (percent).
  -- @return #table Table A table of contents with numbers.
  -- @usage
  --      The index is the unique cargo name.
  --      Each entry in the returned table contains a table with the following entries:
  --      
  --      {
  --          Stock0 -- number of original stock when the cargo entry was created.
  --          Stock -- number of currently available stock.
  --          StockR -- relative number of available stock, e.g. 75 (percent).
  --          Infield -- number of groups alive in the field of this kind.
  --          Inhelo -- number of troops/crates in any helo alive. Can be with decimals < 1 if e.g. you have cargo that need 4 crates, but you have 2 loaded.
  --          Sum -- sum is stock + infield + inhelo.
  --          GenericCargo -- this filed holds the generic CTLD_CARGO object which drives the available stock. Only populated if Restock is true.
  --        }
  function CTLD:_CountStockPlusInHeloPlusAliveGroups(Restock,Threshold)
    local Troopstable = {}
    for _id, _cargo in pairs(self.Cargo_Crates) do
      local generic = _cargo
      local genname = generic:GetName()
      if generic and generic:GetStock0() > 0 and not Troopstable[genname] then 
        Troopstable[genname] = {
          Stock0 = generic:GetStock0(),
          Stock = generic:GetStock(),
          StockR = generic:GetRelativeStock(),
          Infield = 0,
          Inhelo = 0,
          CratesInfield = 0,
          Sum = generic:GetStock(),
        }
        if Restock == true then
          Troopstable[genname].GenericCargo = generic
        end
      end
    end
    for _id, _cargo in pairs(self.Cargo_Troops) do
      local generic = _cargo
      local genname = generic:GetName()
      if generic and generic:GetStock0() > 0 and not Troopstable[genname] then        
        Troopstable[genname] = {
          Stock0 = generic:GetStock0(),
          Stock = generic:GetStock(),
          StockR = generic:GetRelativeStock(),
          Infield = 0,
          Inhelo = 0,
          CratesInfield = 0,
          Sum = generic:GetStock(),
        }
        if Restock == true then
          Troopstable[genname].GenericCargo = generic
        end
      end
    end   
    for _index, _group in pairs(self.DroppedTroops) do
      if _group and _group:IsAlive() then
        self:T("Looking at " .. _group:GetName() .. " in the field")
        local generic = self:GetGenericCargoObjectFromGroupName(_group:GetName())
        if generic then 
          local genname = generic:GetName()
          self:T("Found Generic " .. genname .. " in the field. Adding.")
          if generic:GetStock0() > 0 then
            Troopstable[genname].Infield = Troopstable[genname].Infield + 1
            Troopstable[genname].Sum = Troopstable[genname].Infield + Troopstable[genname].Stock + Troopstable[genname].Inhelo
          end
        else
          self:E(self.lid .. "Group without Cargo Generic: " .. _group:GetName())
        end
      end
    end
    for _unitname, _loaded in pairs(self.Loaded_Cargo) do
      local _unit = UNIT:FindByName(_unitname)
      if _unit and _unit:IsAlive() then
        local unitname = _unit:GetName()
        local loadedcargo = self.Loaded_Cargo[unitname].Cargo or {}
        for _, _cgo in pairs(loadedcargo) do
          local cargo = _cgo
          local type = cargo.CargoType
          local gname = cargo.Name
          local gcargo = self:_FindCratesCargoObject(gname) or self:_FindTroopsCargoObject(gname)
          self:T("Looking at " .. gname .. " in the helo - type = " .. type)
          if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS or type == CTLD_CARGO.Enum.VEHICLE or type == CTLD_CARGO.Enum.FOB) then
            if gcargo and gcargo:GetStock0() > 0 then
              self:T("Adding " .. gname .. " in the helo - type = " .. type)
              if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) then
                Troopstable[gname].Inhelo = Troopstable[gname].Inhelo + 1
              end
              if (type == CTLD_CARGO.Enum.VEHICLE or type == CTLD_CARGO.Enum.FOB) then
                local counting = gcargo.CratesNeeded
                local added = 1
                if counting > 1 then
                  added = added / counting
                end
                Troopstable[gname].Inhelo = Troopstable[gname].Inhelo + added
              end
              Troopstable[gname].Sum = Troopstable[gname].Infield + Troopstable[gname].Stock + Troopstable[gname].Inhelo + Troopstable[gname].CratesInfield
            end
          end
        end
      end
    end 
    if self.Spawned_Cargo then
      -- First pass: just add fractional amounts for each crate on the ground
      for i = #self.Spawned_Cargo, 1, -1 do
        local cargo = self.Spawned_Cargo[i]
        if cargo and cargo:GetPositionable() and cargo:GetPositionable():IsAlive() then
          local genname = cargo:GetName()
          local gcargo  = self:_FindCratesCargoObject(genname)
          if Troopstable[genname] and gcargo and gcargo:GetStock0() > 0 then
            local needed = gcargo.CratesNeeded or 1
            local added  = 1
            if needed > 1 then
              added = added / needed
            end
            Troopstable[genname].CratesInfield = Troopstable[genname].CratesInfield + added
            Troopstable[genname].Sum = Troopstable[genname].Infield + Troopstable[genname].Stock
                                     + Troopstable[genname].Inhelo + Troopstable[genname].CratesInfield
          end
        end
      end
      for i = #self.Spawned_Cargo, 1, -1 do
        local cargo = self.Spawned_Cargo[i]
        if cargo and cargo:GetPositionable() and cargo:GetPositionable():IsAlive() then
          local genname = cargo:GetName()
          if Troopstable[genname] then
            if Troopstable[genname].Inhelo == 0 and Troopstable[genname].CratesInfield < 1 then
              Troopstable[genname].CratesInfield = 0
              Troopstable[genname].Sum = Troopstable[genname].Stock
              cargo:GetPositionable():Destroy(false)
              table.remove(self.Spawned_Cargo, i)
              local leftover = Troopstable[genname].Stock0 - (Troopstable[genname].Infield + Troopstable[genname].Inhelo + Troopstable[genname].CratesInfield)
              if leftover < Troopstable[genname].Stock then
                Troopstable[genname].Stock = leftover
              end
              Troopstable[genname].Sum = Troopstable[genname].Stock + Troopstable[genname].Infield + Troopstable[genname].Inhelo + Troopstable[genname].CratesInfield
            end
          end
        end
      end
    end
      if Restock == true then
        local threshold = Threshold or 75
        for _name,_data in pairs(Troopstable) do
          if _data.StockR and _data.StockR < threshold then
            if _data.GenericCargo then
              _data.GenericCargo:SetStock(_data.Stock0) -- refill to start level
            end
          end
        end
      end
    return Troopstable
  end
  

  --- User - function to add stock of a certain troops type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to add.
  -- @return #CTLD self
  function CTLD:AddStockTroops(Name, Number)
    local name = Name or "none"
    local number = Number or 1
    -- find right generic type
    local gentroops = self.Cargo_Troops
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:AddStock(number)
        break
      end
    end
    return self
  end
  
  --- User - function to add stock of a certain crates type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to add.
  -- @return #CTLD self
  function CTLD:AddStockCrates(Name, Number)
    local name = Name or "none"
    local number = Number or 1
    -- find right generic type
    local gentroops = self.Cargo_Crates
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:AddStock(number)
        break
      end
    end
    return self
  end
  
  --- User - function to add stock of a certain crates type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to add.
  -- @return #CTLD self
  function CTLD:AddStockStatics(Name, Number)
    local name = Name or "none"
    local number = Number or 1
    -- find right generic type
    local gentroops = self.Cargo_Statics
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:AddStock(number)
        break
      end
    end
    return self
  end
  
  --- User - function to set the stock of a certain crates type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to be available. Nil equals unlimited
  -- @return #CTLD self
  function CTLD:SetStockCrates(Name, Number)
    local name = Name or "none"
    local number = Number
    -- find right generic type
    local gentroops = self.Cargo_Crates
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:SetStock(number)
        break
      end
    end
    return self
  end
  
  --- User - function to set the stock of a certain troops type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to be available. Nil equals unlimited
  -- @return #CTLD self
  function CTLD:SetStockTroops(Name, Number)
    local name = Name or "none"
    local number = Number
    -- find right generic type
    local gentroops = self.Cargo_Troops
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:SetStock(number)
        break
      end
    end
    return self
  end
  
  --- User - function to set the stock of a certain statics type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to be available. Nil equals unlimited
  -- @return #CTLD self
  function CTLD:SetStockStatics(Name, Number)
    local name = Name or "none"
    local number = Number
    -- find right generic type
    local gentroops = self.Cargo_Statics
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:SetStock(number)
        break
      end
    end
    return self
  end
  
  --- User - function to get a table of crates in stock
  -- @param #CTLD self
  -- @return #table Table Table of Stock, indexed by cargo type name
  function CTLD:GetStockCrates()
    local Stock = {}
    local gentroops = self.Cargo_Crates
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      Stock[_troop.Name] = _troop.Stock or -1
      --table.insert(Stock,_troop.Name,_troop.Stock or -1)
    end
    return Stock
  end  
  
  --- User - function to get a table of troops in stock
  -- @param #CTLD self
  -- @return #table Table Table of Stock, indexed by cargo type name
  function CTLD:GetStockTroops()
    local Stock = {}
    local gentroops = self.Cargo_Troops
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      Stock[_troop.Name] = _troop.Stock or -1
      --table.insert(Stock,_troop.Name,_troop.Stock or -1)
    end
    return Stock
  end
  
  --- User - Query the cargo loaded from a specific unit
  -- @param #CTLD self
  -- @param Wrapper.Unit#UNIT Unit The (client) unit to query.
  -- @return #number Troopsloaded
  -- @return #number Cratesloaded
  -- @return #table Cargo Table of #CTLD_CARGO objects
  function CTLD:GetLoadedCargo(Unit)
    local Troops = 0
    local Crates = 0
    local Cargo = {}
    if Unit and Unit:IsAlive() then
      local name = Unit:GetName()
      if self.Loaded_Cargo[name] then
        Troops = self.Loaded_Cargo[name].Troopsloaded or 0
        Crates = self.Loaded_Cargo[name].Cratesloaded or 0
        Cargo = self.Loaded_Cargo[name].Cargo or {}
      end
    end
    return Troops, Crates, Cargo
  end
  
  --- User - function to get a table of statics cargo in stock
  -- @param #CTLD self
  -- @return #table Table Table of Stock, indexed by cargo type name
  function CTLD:GetStockStatics()
    local Stock = {}
    local gentroops = self.Cargo_Statics
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      Stock[_troop.Name] = _troop.Stock or -1
      -- table.insert(Stock,_troop.Name,_troop.Stock or -1)
    end
    return Stock
  end
  
  --- User - function to remove stock of a certain troops type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to add.
  -- @return #CTLD self
  function CTLD:RemoveStockTroops(Name, Number)
    local name = Name or "none"
    local number = Number or 1
    -- find right generic type
    local gentroops = self.Cargo_Troops
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:RemoveStock(number)
      end
    end
    return self
  end
  
  --- User - function to remove stock of a certain crates type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to add.
  -- @return #CTLD self
  function CTLD:RemoveStockCrates(Name, Number)
    local name = Name or "none"
    local number = Number or 1
    -- find right generic type
    local gentroops = self.Cargo_Crates
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:RemoveStock(number)
      end
    end
    return self
  end
  
  --- User - function to remove stock of a certain statics type
  -- @param #CTLD self
  -- @param #string Name Name as defined in the generic cargo.
  -- @param #number Number Number of units/groups to add.
  -- @return #CTLD self
  function CTLD:RemoveStockStatics(Name, Number)
    local name = Name or "none"
    local number = Number or 1
    -- find right generic type
    local gentroops = self.Cargo_Statics
    for _id,_troop in pairs (gentroops) do -- #number, #CTLD_CARGO
      if _troop.Name == name then
        _troop:RemoveStock(number)
      end
    end
    return self
  end
  
  --- (User) Get a generic #CTLD_CARGO entry from a group name, works for Troops and Vehicles, FOB, i.e. everything that is spawned as a GROUP object.
  -- @param #CTLD self
  -- @param #string GroupName The name to use for the search
  -- @return #CTLD_CARGO The cargo object or nil if not found
  function CTLD:GetGenericCargoObjectFromGroupName(GroupName)
    local Cargotype = nil
    local template = GroupName
    if string.find(template,"#") then
      template = string.gsub(GroupName,"#(%d+)$","")
    end   
    template = string.gsub(template,"-(%d+)$","")
    for k,v in pairs(self.Cargo_Troops) do
    local comparison = ""
    if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end
      if comparison == template then
        Cargotype = v
        break
      end
    end
    if not Cargotype then
      for k,v in pairs(self.Cargo_Crates) do -- #number, #CTLD_CARGO
      local comparison = ""
      if type(v.Templates) == "string" then comparison = v.Templates else comparison = v.Templates[1] end
        if comparison == template and v.CargoType ~= CTLD_CARGO.Enum.REPAIR then
          Cargotype = v
          break
        end
      end
    end
    return Cargotype
  end
  
  --- (Internal) Check on engineering teams
  -- @param #CTLD self
  -- @return #CTLD self
  function CTLD:_CheckEngineers()
    self:T(self.lid.." CheckEngineers")
    local engtable = self.EngineersInField
    for _ind,_engineers in pairs (engtable) do
      local engineers = _engineers -- #CTLD_ENGINEERING
      local wrenches = engineers.Group -- Wrapper.Group#GROUP
      self:T(_engineers.lid .. _engineers:GetStatus())
      if wrenches and wrenches:IsAlive() then
        if engineers:IsStatus("Running") or engineers:IsStatus("Searching") then
          local crates,number = self:_FindCratesNearby(wrenches,nil, self.EngineerSearch,true,true) -- #table
          engineers:Search(crates,number)
        elseif engineers:IsStatus("Moving") then
          engineers:Move()
        elseif engineers:IsStatus("Arrived") then
          engineers:Build()
          local unit = wrenches:GetUnit(1)
          self:_BuildCrates(wrenches,unit,true)
          self:_RepairCrates(wrenches,unit,true)
          engineers:Done()
        end
      else
        engineers:Stop()
      end
    end
    return self
  end
  
  --- (User) Pre-populate troops in the field.
  -- @param #CTLD self
  -- @param Core.Zone#ZONE Zone The zone where to drop the troops.
  -- @param Ops.CTLD#CTLD_CARGO Cargo The #CTLD_CARGO object to spawn.
  -- @param #table Surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 1000 times to find the right type!
  -- @param #boolean PreciseLocation (Optional) Don't try to get a random position in the zone but use the dead center. Caution not to stack up stuff on another!
  -- @param #string Structure (Optional) String object describing the current structure of the injected group; mainly for load/save to keep current state setup.
  -- @param #number TimeStamp (Optional) Timestamp used internally on re-loading from disk.
  -- @return #CTLD self
  -- @usage Use this function to pre-populate the field with Troops or Engineers at a random coordinate in a zone:
  --            -- create a matching #CTLD_CARGO type
  --            local InjectTroopsType = CTLD_CARGO:New(nil,"Infantry",{"Inf12"},CTLD_CARGO.Enum.TROOPS,true,true,12,nil,false,80)
  --            -- get a #ZONE object
  --            local dropzone = ZONE:New("InjectZone") -- Core.Zone#ZONE
  --            -- and go:
  --            my_ctld:InjectTroops(dropzone,InjectTroopsType,{land.SurfaceType.LAND})
  function CTLD:InjectTroops(Zone,Cargo,Surfacetypes,PreciseLocation,Structure,TimeStamp)
    self:T(self.lid.." InjectTroops")
    local cargo = Cargo -- #CTLD_CARGO
    
    local function IsTroopsMatch(cargo)
      local match = false
      local cgotbl = self.Cargo_Troops
      local name = cargo:GetName()
      local CargoObject
      local CargoName
      for _,_cgo in pairs (cgotbl) do
        local cname = _cgo:GetName()
        if name == cname then
          match = true
          CargoObject = _cgo
          CargoName = cname
          break
        end
      end
      return match, CargoObject, CargoName
    end
    
    local function Cruncher(group,typename,anzahl)
      local units = group:GetUnits()
      local reduced = 0
      for _,_unit in pairs (units) do
        local typo = _unit:GetTypeName()
        if typename == typo then
          _unit:Destroy(false)
          reduced = reduced + 1
          if reduced == anzahl then break end
        end
      end
    end
    
    local function PostSpawn(args)
      local group = args[1]
      local structure = args[2]
      if structure then
  
        local loadedstructure = {}
        local strcset = UTILS.Split(structure,";")
        for _,_data in pairs(strcset) do
          local datasplit = UTILS.Split(_data,"==")
          loadedstructure[datasplit[1]] = tonumber(datasplit[2])
        end
  
        local originalstructure = UTILS.GetCountPerTypeName(group)
  
        for _name,_number in pairs(originalstructure) do
          local loadednumber = 0
          if loadedstructure[_name] then
            loadednumber = loadedstructure[_name]
          end
          local reduce = false
          if loadednumber < _number then reduce = true end
          
          if reduce then
            Cruncher(group,_name,_number-loadednumber)  
          end
                    
        end
     end
    end
    
    local match,CargoObject,CargoName = IsTroopsMatch(cargo)
    
    if not match then
      self.CargoCounter = self.CargoCounter + 1
      cargo.ID = self.CargoCounter
      --cargo.Stock = 1
      table.insert(self.Cargo_Troops,cargo)
    end
    
    if match and CargoObject then
      local stock = CargoObject:GetStock()
      if stock ~= -1 and stock ~= nil and stock == 0 then
       -- stock empty
       self:T(self.lid.."Stock of "..CargoName.." is empty. Cannot inject.")
       return
      else
        CargoObject:RemoveStock(1)
      end
    end
    
    local type = cargo:GetType() -- #CTLD_CARGO.Enum
    if (type == CTLD_CARGO.Enum.TROOPS or type == CTLD_CARGO.Enum.ENGINEERS) then
      -- unload 
      local name = cargo:GetName() or "none"
      local temptable = cargo:GetTemplates() or {}
      local factor = 1.5
      local zone = Zone
      local randomcoord = zone:GetRandomCoordinate(10,30*factor,Surfacetypes):GetVec2()
      if PreciseLocation then
        randomcoord = zone:GetCoordinate():GetVec2()
      end
      local randompositions = not PreciseLocation
      for _,_template in pairs(temptable) do
        self.TroopCounter = self.TroopCounter + 1
        local alias = string.format("%s-%d", _template, math.random(1,100000))
        self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
          :InitRandomizeUnits(randompositions,20,2)
          :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
          :InitDelayOff()
          :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp)
          :SpawnFromVec2(randomcoord)
        if self.movetroopstowpzone and type ~= CTLD_CARGO.Enum.ENGINEERS then
          self:_MoveGroupToZone(self.DroppedTroops[self.TroopCounter])
        end
      end -- template loop
      cargo:SetWasDropped(true)
      -- engineering group?
      if type == CTLD_CARGO.Enum.ENGINEERS then
        self.Engineers = self.Engineers + 1
        local grpname = self.DroppedTroops[self.TroopCounter]:GetName()
        self.EngineersInField[self.Engineers] = CTLD_ENGINEERING:New(name, grpname)
      end
      
      if Structure then
        BASE:ScheduleOnce(0.5,PostSpawn,{self.DroppedTroops[self.TroopCounter],Structure})
      end
      
      if self.keeploadtable and TimeStamp ~= nil then
        self:T2("Inserting: "..cargo.CargoType)
        local cargotype = type
        table.insert(self.LoadedGroupsTable,{Group=self.DroppedTroops[self.TroopCounter], TimeStamp=TimeStamp, CargoType=cargotype, CargoName=name})
      end
      
      if self.eventoninject then
         self:__TroopsDeployed(1,nil,nil,self.DroppedTroops[self.TroopCounter],type)
      end
    end -- if type end
    return self
  end
  
  --- (User) Pre-populate vehicles in the field.
  -- @param #CTLD self
  -- @param Core.Zone#ZONE Zone The zone where to drop the troops.
  -- @param Ops.CTLD#CTLD_CARGO Cargo The #CTLD_CARGO object to spawn.
  -- @param #table Surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 1000 times to find the right type!
  -- @param #boolean PreciseLocation (Optional) Don't try to get a random position in the zone but use the dead center. Caution not to stack up stuff on another!
  -- @param #string Structure (Optional) String object describing the current structure of the injected group; mainly for load/save to keep current state setup.
  -- @param #number TimeStamp (Optional) Timestamp used internally on re-loading from disk.
  -- @return #CTLD self
  -- @usage Use this function to pre-populate the field with Vehicles or FOB at a random coordinate in a zone:
  --            -- create a matching #CTLD_CARGO type
  --            local InjectVehicleType = CTLD_CARGO:New(nil,"Humvee",{"Humvee"},CTLD_CARGO.Enum.VEHICLE,true,true,1,nil,false,1000)
  --            -- get a #ZONE object
  --            local dropzone = ZONE:New("InjectZone") -- Core.Zone#ZONE
  --            -- and go:
  --            my_ctld:InjectVehicles(dropzone,InjectVehicleType)
  function CTLD:InjectVehicles(Zone,Cargo,Surfacetypes,PreciseLocation,Structure,TimeStamp)
    self:T(self.lid.." InjectVehicles")
    local cargo = Cargo -- #CTLD_CARGO
    
    local function IsVehicMatch(cargo)
      local match = false
      local cgotbl = self.Cargo_Crates
      local name = cargo:GetName()
      local CargoObject
      local CargoName
      for _,_cgo in pairs (cgotbl) do
        local cname = _cgo:GetName()
        if name == cname then
          match = true
          CargoObject = _cgo
          CargoName = cname
          break
        end
      end
      return match,CargoObject,CargoName
    end
    
    local function Cruncher(group,typename,anzahl)
      local units = group:GetUnits()
      local reduced = 0
      for _,_unit in pairs (units) do
        local typo = _unit:GetTypeName()
        if typename == typo then
          _unit:Destroy(false)
          reduced = reduced + 1
          if reduced == anzahl then break end
        end
      end
    end
    
    local function PostSpawn(args)
      local group = args[1]
      local structure = args[2]
      if structure then
  
        local loadedstructure = {}
        local strcset = UTILS.Split(structure,";")
        for _,_data in pairs(strcset) do
          local datasplit = UTILS.Split(_data,"==")
          loadedstructure[datasplit[1]] = tonumber(datasplit[2])
        end
  
        local originalstructure = UTILS.GetCountPerTypeName(group)
  
        for _name,_number in pairs(originalstructure) do
          local loadednumber = 0
          if loadedstructure[_name] then
            loadednumber = loadedstructure[_name]
          end
          local reduce = false
          if loadednumber < _number then reduce = true end
          
          if reduce then
            Cruncher(group,_name,_number-loadednumber)  
          end
                    
        end
     end
    end
    
    local match,CargoObject,CargoName = IsVehicMatch(cargo)
    
    if not match then
      self.CargoCounter = self.CargoCounter + 1
      cargo.ID = self.CargoCounter
      --cargo.Stock = 1
      table.insert(self.Cargo_Crates,cargo)
    end
    
    if match and CargoObject then
      local stock = CargoObject:GetStock()
      if stock ~= -1 and stock ~= nil and stock == 0 then
       -- stock empty
       self:T(self.lid.."Stock of "..CargoName.." is empty. Cannot inject.")
       return
      else
        CargoObject:RemoveStock(1)
      end
    end
    
    local type = cargo:GetType() -- #CTLD_CARGO.Enum
    if (type == CTLD_CARGO.Enum.VEHICLE or type == CTLD_CARGO.Enum.FOB) then
      -- unload 
      local name = cargo:GetName() or "none"
      local temptable = cargo:GetTemplates() or {}
      local factor = 1.5
      local zone = Zone
      local randomcoord = zone:GetRandomCoordinate(10,30*factor,Surfacetypes):GetVec2()
      if PreciseLocation then
        randomcoord = zone:GetCoordinate():GetVec2()
      end
      cargo:SetWasDropped(true)
      local canmove = false
      if type == CTLD_CARGO.Enum.VEHICLE then canmove = true end
      for _,_template in pairs(temptable) do
        self.TroopCounter = self.TroopCounter + 1
        local alias = string.format("%s-%d", _template, math.random(1,100000))
        if canmove then
          self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
            :InitRandomizeUnits(true,20,2)
            :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
            :InitDelayOff()
            :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp)
            :SpawnFromVec2(randomcoord)
        else -- don't random position of e.g. SAM units build as FOB
          self.DroppedTroops[self.TroopCounter] = SPAWN:NewWithAlias(_template,alias)
            :InitDelayOff()
            :InitValidateAndRepositionGroundUnits(self.validateAndRepositionUnits)
            :OnSpawnGroup(function(grp,TimeStamp) grp.spawntime = TimeStamp or timer.getTime() end,TimeStamp)
            :SpawnFromVec2(randomcoord)
        end
        
        if Structure then
          BASE:ScheduleOnce(0.5,PostSpawn,{self.DroppedTroops[self.TroopCounter],Structure})
        end
        
        if self.keeploadtable and TimeStamp ~= nil then
          self:T2("Inserting: "..cargo.CargoType)
          local cargotype = type
          table.insert(self.LoadedGroupsTable,{Group=self.DroppedTroops[self.TroopCounter], TimeStamp=TimeStamp, CargoType=cargotype, CargoName=name})
        end
        
        if self.eventoninject then
          self:__CratesBuild(1,nil,nil,self.DroppedTroops[self.TroopCounter])
        end
      end -- end loop
    end -- if type end
    return self
  end
  
------------------------------------------------------------------- 
-- TODO FSM functions
------------------------------------------------------------------- 

  --- (Internal) FSM Function onafterStart.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @return #CTLD self
  function CTLD:onafterStart(From, Event, To)
    self:T({From, Event, To})
    self:I(self.lid .. "Started ("..self.version..")")
    if self.enableHercules then self.enableFixedWing = true end
    if self.UserSetGroup then
      self.PilotGroups  = self.UserSetGroup
    elseif self.useprefix or self.enableFixedWing then
      local prefix = self.prefixes
      if self.enableFixedWing then
        self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterStart()
      else
        self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefix):FilterCategories("helicopter"):FilterStart()
      end
    else
      self.PilotGroups = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategories("helicopter"):FilterStart()
    end
    -- Events
    self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler)
    self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler)
    self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler)
    self:HandleEvent(EVENTS.UnitLost, self._EventHandler)  
    --self:HandleEvent(EVENTS.Birth, self._EventHandler)
    self:HandleEvent(EVENTS.NewDynamicCargo, self._EventHandler)
    self:HandleEvent(EVENTS.DynamicCargoLoaded, self._EventHandler)  
    self:HandleEvent(EVENTS.DynamicCargoUnloaded, self._EventHandler)  
    self:HandleEvent(EVENTS.DynamicCargoRemoved, self._EventHandler)     
    self:HandleEvent(EVENTS.Land, self._EventHandler)
    self:HandleEvent(EVENTS.Takeoff, self._EventHandler)
    self:__Status(-5)
    
    -- AutoSave
    if self.enableLoadSave then
      local interval = self.saveinterval
      local filename = self.filename
      local filepath = self.filepath
      self:__Save(interval,filepath,filename)
    end
    
    if type(self.VehicleMoveFormation) == "table" then
      local Formations = {}
      for _,_formation in pairs(self.VehicleMoveFormation) do
        table.insert(Formations,_formation)
      end
      self.VehicleMoveFormation = nil
      self.VehicleMoveFormation = Formations
    end
  
    return self
  end

  --- (Internal) FSM Function onbeforeStatus.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @return #CTLD self
  function CTLD:onbeforeStatus(From, Event, To)
    self:T({From, Event, To})
    self:CleanDroppedTroops()
    self:_RefreshF10Menus()
    self:CheckDroppedBeacons()
    self:_RefreshRadioBeacons()
    self:CheckAutoHoverload()
    self:_CheckEngineers()
    return self
  end
  
  --- (Internal) FSM Function onafterStatus.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @return #CTLD self
  function CTLD:onafterStatus(From, Event, To)
    self:T({From, Event, To})
     -- gather some stats
    -- pilots
    local pilots = 0
    for _,_pilot in pairs (self.CtldUnits) do   
     pilots = pilots + 1
    end
     
    -- spawned cargo boxes curr in field
    local boxes = 0
    for _,_pilot in pairs (self.Spawned_Cargo) do
     boxes = boxes + 1
    end
    
    local cc =  self.CargoCounter
    local tc = self.TroopCounter
    
    if self.debug or self.verbose > 0 then 
      local text = string.format("%s Pilots %d | Live Crates %d |\nCargo Counter %d | Troop Counter %d", self.lid, pilots, boxes, cc, tc)
      local m = MESSAGE:New(text,10,"CTLD"):ToAll()
      if self.verbose > 0 then
        self:I(self.lid.."Cargo and Troops in Stock:")
        for _,_troop in pairs (self.Cargo_Crates) do
          local name = _troop:GetName()
          local stock = _troop:GetStock()
          self:I(string.format("-- %s \t\t\t %d", name, stock))
        end
        for _,_troop in pairs (self.Cargo_Statics) do
          local name = _troop:GetName()
          local stock = _troop:GetStock()
          self:I(string.format("-- %s \t\t\t %d", name, stock))
        end
        for _,_troop in pairs (self.Cargo_Troops) do
          local name = _troop:GetName()
          local stock = _troop:GetStock()
          self:I(string.format("-- %s \t\t %d", name, stock))
        end
      end
    end
    self:__Status(-30)
    return self
  end
  
  --- (Internal) FSM Function onafterStop.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @return #CTLD self
  function CTLD:onafterStop(From, Event, To)
    self:T({From, Event, To})
    self:UnHandleEvent(EVENTS.PlayerEnterAircraft)
    self:UnHandleEvent(EVENTS.PlayerEnterUnit)
    self:UnHandleEvent(EVENTS.PlayerLeaveUnit)
    self:UnHandleEvent(EVENTS.UnitLost)  
    self:UnHandleEvent(EVENTS.Shot) 
    return self
  end
  
  --- (Internal) FSM Function onbeforeTroopsPickedUp.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo crate.
  -- @return #CTLD self
  function CTLD:onbeforeTroopsPickedUp(From, Event, To, Group, Unit, Cargo)
    self:T({From, Event, To})
    return self
  end
  
    --- (Internal) FSM Function onbeforeCratesPickedUp.
  -- @param #CTLD self
  -- @param #string From State .
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #CTLD_CARGO Cargo Cargo crate. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew loaded!
  -- @return #CTLD self
  function CTLD:onbeforeCratesPickedUp(From, Event, To, Group, Unit, Cargo)
    self:T({From, Event, To})
    return self
  end
  
  --- (Internal) FSM Function onbeforeTroopsExtracted.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object.
  -- @param #string Groupname Name of the extracted #GROUP.
  -- @return #CTLD self
  function CTLD:onbeforeTroopsExtracted(From, Event, To, Group, Unit, Troops, Groupname)
    self:T({From, Event, To})
    if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then
      local playername = Unit:GetPlayerName()
      --local dropcoord = Troops:GetCoordinate() or COORDINATE:New(0,0,0)
      --local dropvec2 = dropcoord:GetVec2()
      self.PlayerTaskQueue:ForEach(
        function (Task)
          local task = Task -- Ops.PlayerTask#PLAYERTASK
          local subtype = task:GetSubType()
          -- right subtype?
          if Event == subtype and not task:IsDone() then
            local targetzone = task.Target:GetObject() -- Core.Zone#ZONE should be a zone in this case ....
            self:T2({Name=Groupname,Property=task:GetProperty("ExtractName")})
            if task:GetProperty("ExtractName") then
              local okaygroup = string.find(Groupname,task:GetProperty("ExtractName"),1,true)
              if targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and okaygroup then
                if task.Clients:HasUniqueID(playername) then
                  -- success
                  task:__Success(-1)
                end
              end
            else
              self:T({Text="'ExtractName' Property not set",Name=Groupname,Property=task.Type})
            end
          end
        end
      )
    end
    return self
  end
    
    
  --- (Internal) FSM Function onbeforeTroopsDeployed.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object.
  -- @return #CTLD self
  function CTLD:onbeforeTroopsDeployed(From, Event, To, Group, Unit, Troops)
    self:T({From, Event, To})
    if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then
      local playername = Unit:GetPlayerName()
      local dropcoord = Troops:GetCoordinate() or COORDINATE:New(0,0,0)
      local dropvec2 = dropcoord:GetVec2()
      self.PlayerTaskQueue:ForEach(
        function (Task)
          local task = Task -- Ops.PlayerTask#PLAYERTASK
          local subtype = task:GetSubType()
          -- right subtype?
          if Event == subtype and not task:IsDone() then
            local targetzone = task.Target:GetObject() -- Core.Zone#ZONE should be a zone in this case ....
            if targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and targetzone:IsVec2InZone(dropvec2) then
              if task.Clients:HasUniqueID(playername) then
                -- success
                task:__Success(-1)
              end
            end
          end
        end
      )
    end
    return self
  end
  
  --- (Internal) FSM Function onafterTroopsDeployed.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Troops Troops #GROUP Object.
  -- @param #CTLD.CargoZoneType Type Type of Cargo deployed
  -- @return #CTLD self
  function CTLD:onafterTroopsDeployed(From, Event, To, Group, Unit, Troops, Type)
    self:T({From, Event, To})
    if self.movetroopstowpzone and Type ~= CTLD_CARGO.Enum.ENGINEERS then
      self:_MoveGroupToZone(Troops)
    end
    return self
  end
  
  --- (Internal) FSM Function onbeforeCratesDropped.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #table Cargotable Table of #CTLD_CARGO objects dropped. Can be a Wrapper.DynamicCargo#DYNAMICCARGO object, if ground crew unloaded!
  -- @return #CTLD self
  function CTLD:onbeforeCratesDropped(From, Event, To, Group, Unit, Cargotable)
    self:T({From, Event, To})
    return self
  end
  
  --- (Internal) FSM Function onbeforeCratesBuild.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build.
  -- @return #CTLD self
  function CTLD:onbeforeCratesBuild(From, Event, To, Group, Unit, Vehicle)
    self:T({From, Event, To})
    if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then
      local playername = Unit:GetPlayerName()
      local dropcoord = Vehicle:GetCoordinate() or COORDINATE:New(0,0,0)
      local dropvec2 = dropcoord:GetVec2()
      self.PlayerTaskQueue:ForEach(
        function (Task)
          local task = Task -- Ops.PlayerTask#PLAYERTASK
          local subtype = task:GetSubType()
          -- right subtype?
          if Event == subtype and not task:IsDone() then
            local targetzone = task.Target:GetObject() -- Core.Zone#ZONE should be a zone in this case ....
            if targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and targetzone:IsVec2InZone(dropvec2) then
              if task.Clients:HasUniqueID(playername) then
                -- success
                task:__Success(-1)
              end
            end
          end
        end
      )
    end
    return self
  end

  --- (Internal) FSM Function onafterCratesBuild.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param Wrapper.Group#GROUP Vehicle The #GROUP object of the vehicle or FOB build.
  -- @return #CTLD self
  function CTLD:onafterCratesBuild(From, Event, To, Group, Unit, Vehicle)
    self:T({From, Event, To})
    if self.movetroopstowpzone then
      self:_MoveGroupToZone(Vehicle)
    end
    return self
  end
  
  --- (Internal) FSM Function onbeforeTroopsRTB.
  -- @param #CTLD self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Group#GROUP Group Group Object.
  -- @param Wrapper.Unit#UNIT Unit Unit Object.
  -- @param #string ZoneName Name of the Zone where the Troops have been RTB'd.
  -- @param Core.Zone#ZONE_Radius ZoneObject of the Zone where the Troops have been RTB'd.
  -- @return #CTLD self
  function CTLD:onbeforeTroopsRTB(From, Event, To, Group, Unit, ZoneName, ZoneObject)
    self:T({From, Event, To})
    return self
  end
  
  --- On before "Save" event. Checks if io and lfs are available.
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
  -- @param #string filename (Optional) File name for saving. Default is "CTLD_<alias>_Persist.csv".
  function CTLD:onbeforeSave(From, Event, To, path, filename)
    self:T({From, Event, To, path, filename})
    if not self.enableLoadSave then
      return self
    end
    -- Thanks to @FunkyFranky 
    -- Check io module is available.
    if not io then
      self:E(self.lid.."ERROR: io not desanitized. Can't save current state.")
      return false
    end
  
    -- Check default path.
    if path==nil and not lfs then
      self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
    end
  
    return true
  end
  
  --- On after "Save" event. Player data is saved to file.
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory or your "Saved Games" folder if lfs was desanitized.
  -- @param #string filename (Optional) File name for saving. Default is Default is "CTLD_<alias>_Persist.csv".
  function CTLD:onafterSave(From, Event, To, path, filename)
    self:T({From, Event, To, path, filename})
    -- Thanks to @FunkyFranky 
    if not self.enableLoadSave then
      return self
    end
    --- Function that saves data to file
    local function _savefile(filename, data)
      local f = assert(io.open(filename, "wb"))
      f:write(data)
      f:close()
    end
  
    -- Set path or default.
    if lfs then
      path=self.filepath or lfs.writedir()
    end
    
    -- Set file name.
    filename=filename or self.filename
  
    -- Set path.
    if path~=nil then
      filename=path.."\\"..filename
    end
    
    local grouptable = self.DroppedTroops -- #table
    local cgovehic = self.Cargo_Crates
    local cgotable = self.Cargo_Troops
    local stcstable = self.Spawned_Cargo
    
    local statics = nil
    local statics = {}
    self:T(self.lid.."Building Statics Table for Saving")
    for _,_cargo in pairs (stcstable) do     
      local cargo = _cargo -- #CTLD_CARGO
      local object = cargo:GetPositionable() -- Wrapper.Static#STATIC
      if object and object:IsAlive() and (cargo:WasDropped() or not cargo:HasMoved()) then
        statics[#statics+1] = cargo
      end
    end
    
    -- find matching cargo
    local function FindCargoType(name,table)
      -- name matching a template in the table
      local match = false
      local cargo = nil
      name = string.gsub(name,"-"," ")
      for _ind,_cargo in pairs (table) do
        local thiscargo = _cargo -- #CTLD_CARGO
        local template = thiscargo:GetTemplates()
        if type(template) == "string" then
          template = { template }
        end
        for _,_name in pairs (template) do
          _name = string.gsub(_name,"-"," ")
          if string.find(name,_name) and _cargo:GetType() ~= CTLD_CARGO.Enum.REPAIR then
            match = true
            cargo = thiscargo
          end
        end
        if match then break end
      end
      return match, cargo
    end
    
      
    --local data = "LoadedData = {\n"
    local data = "Group,x,y,z,CargoName,CargoTemplates,CargoType,CratesNeeded,CrateMass,Structure,StaticCategory,StaticType,StaticShape,SpawnTime\n"
    local n = 0
    for _,_grp in pairs(grouptable) do
      local group = _grp -- Wrapper.Group#GROUP
      if group and group:IsAlive() then
        -- get template name
        local name = group:GetName()
        local template = name
        
        if string.find(template,"#") then
          template = string.gsub(name,"#(%d+)$","")
        end
        
        local template = string.gsub(name,"-(%d+)$","")
        
        local match, cargo = FindCargoType(template,cgotable)
        if not match then
          match, cargo = FindCargoType(template,cgovehic)
        end
        if match then
          n = n + 1
          local cargo = cargo -- #CTLD_CARGO
          local cgoname = cargo.Name
          local cgotemp = cargo.Templates
          local cgotype = cargo.CargoType
          local cgoneed = cargo.CratesNeeded
          local cgomass = cargo.PerCrateMass
          local scat,stype,sshape = cargo:GetStaticTypeAndShape()
          local structure = UTILS.GetCountPerTypeName(group)
          local strucdata =  ""
          for typen,anzahl in pairs (structure) do
            strucdata = strucdata .. typen .. "=="..anzahl..";"
          end
          local spawntime = group.spawntime or timer.getTime()+n
          
          if type(cgotemp) == "table" then       
            local templates = "{"
            for _,_tmpl in pairs(cgotemp) do
              templates = templates .. _tmpl .. ";"
            end
            templates = templates .. "}"
            cgotemp = templates
          end
          
          local location = group:GetVec3()
          local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d,%s,%s,%s,%s,%f\n"
              ,template,location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass,strucdata,scat,stype,sshape or "none",spawntime)             
          data = data .. txt
        end
      end
    end
    
    for _,_cgo in pairs(statics) do
      local object = _cgo -- #CTLD_CARGO
      local cgoname = object.Name
      local cgotemp = object.Templates
      
      if type(cgotemp) == "table" then       
        local templates = "{"
        for _,_tmpl in pairs(cgotemp) do
          templates = templates .. _tmpl .. ";"
        end
        templates = templates .. "}"
        cgotemp = templates
      end
      
      local cgotype = object.CargoType
      local cgoneed = object.CratesNeeded
      local cgomass = object.PerCrateMass
      local crateobj = object.Positionable
      local location = crateobj:GetVec3()
      local scat,stype,sshape = object:GetStaticTypeAndShape()
      local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%d,%d,'none',%s,%s,%s\n"
          ,"STATIC",location.x,location.y,location.z,cgoname,cgotemp,cgotype,cgoneed,cgomass,scat,stype,sshape or "none")
      data = data .. txt
    end
    
    _savefile(filename, data)
     
    -- AutoSave
    if self.enableLoadSave then
      local interval = self.saveinterval
      local filename = self.filename
      local filepath = self.filepath
      self:__Save(interval,filepath,filename)
    end
    return self
  end

  --- On before "Load" event. Checks if io and lfs and the file are available.
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
  -- @param #string filename (Optional) File name for loading. Default is "CTLD_<alias>_Persist.csv".
  function CTLD:onbeforeLoad(From, Event, To, path, filename)
    self:T({From, Event, To, path, filename})
    if not self.enableLoadSave then
      return self
    end
    --- Function that check if a file exists.
    local function _fileexists(name)
       local f=io.open(name,"r")
       if f~=nil then
        io.close(f)
        return true
      else
        return false
      end
    end
    
    -- Set file name and path
    filename=filename or self.filename
    path = path or self.filepath
    
    -- Check io module is available.
    if not io then
      self:E(self.lid.."WARNING: io not desanitized. Cannot load file.")
      return false
    end
  
    -- Check default path.
    if path==nil and not lfs then
      self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
    end
  
    -- Set path or default.
    if lfs then
      path=path or lfs.writedir()
    end
  
    -- Set path.
    if path~=nil then
      filename=path.."\\"..filename
    end
  
    -- Check if file exists.
    local exists=_fileexists(filename)
  
    if exists then
      return true
    else
      self:E(self.lid..string.format("WARNING: State file %s might not exist.", filename))
      return false
      --return self
    end
  
  end

  --- On after "Load" event. Loads dropped units from file.
  -- @param #CTLD self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
  -- @param #string filename (Optional) File name for loading. Default is "CTLD_<alias>_Persist.csv".
  function CTLD:onafterLoad(From, Event, To, path, filename)
    self:T({From, Event, To, path, filename})
    if not self.enableLoadSave then
      return self
    end
    --- Function that loads data from a file.
    local function _loadfile(filename)
      local f=assert(io.open(filename, "rb"))
      local data=f:read("*all")
      f:close()
      return data
    end
    
    -- Set file name and path
    filename=filename or self.filename
    path = path or self.filepath
    
    -- Set path or default.
    if lfs then
      path=path or lfs.writedir()
    end
  
    -- Set path.
    if path~=nil then
      filename=path.."\\"..filename
    end
  
    -- Info message.
    local text=string.format("Loading CTLD state from file %s", filename)
    MESSAGE:New(text,10):ToAllIf(self.Debug)
    self:I(self.lid..text)
    
    local file=assert(io.open(filename, "rb"))
    
    local loadeddata = {}
    for line in file:lines() do
        loadeddata[#loadeddata+1] = line
    end
    file:close()
    
    -- remove header
    table.remove(loadeddata, 1)
    local n=0
    for _id,_entry in pairs (loadeddata) do
      local dataset = UTILS.Split(_entry,",")     
      -- 1=Group,2=x,3=y,4=z,5=CargoName,6=CargoTemplates,7=CargoType,8=CratesNeeded,9=CrateMass,10=Structure,11=StaticCategory,12=StaticType,13=StaticShape,14=Timestamp
      local groupname = dataset[1]
      local vec2 = {}
      vec2.x = tonumber(dataset[2])
      vec2.y = tonumber(dataset[4])
      local cargoname = dataset[5]
      local cargotemplates = dataset[6]
      local cargotype = dataset[7]
      local size = tonumber(dataset[8])
      local mass = tonumber(dataset[9])
      local StaticCategory = dataset[11]
      local StaticType = dataset[12]
      local StaticShape = dataset[13]
      n=n+1
      local timestamp = tonumber(dataset[14]) or (timer.getTime()+n)
      self:T2("TimeStamp = "..timestamp)
      if type(groupname) == "string" and groupname ~= "STATIC" then
        cargotemplates = string.gsub(cargotemplates,"{","")
        cargotemplates = string.gsub(cargotemplates,"}","")
        cargotemplates = UTILS.Split(cargotemplates,";")
        local structure = nil
        if dataset[10] and dataset[10] ~= "none" then
          structure = dataset[10]
          structure = string.gsub(structure,",","")
        end
        -- inject at Vec2
        local dropzone = ZONE_RADIUS:New("DropZone",vec2,20)
        if cargotype == CTLD_CARGO.Enum.VEHICLE or cargotype == CTLD_CARGO.Enum.FOB then
          local injectvehicle = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass)
          injectvehicle:SetStaticTypeAndShape(StaticCategory,StaticType,StaticShape)      
          self:InjectVehicles(dropzone,injectvehicle,self.surfacetypes,self.useprecisecoordloads,structure,timestamp)
        elseif cargotype == CTLD_CARGO.Enum.TROOPS or cargotype == CTLD_CARGO.Enum.ENGINEERS then
          local injecttroops = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass)      
          self:InjectTroops(dropzone,injecttroops,self.surfacetypes,self.useprecisecoordloads,structure,timestamp)
        end
      elseif (type(groupname) == "string" and groupname == "STATIC") or cargotype == CTLD_CARGO.Enum.REPAIR then
        local dropzone = ZONE_RADIUS:New("DropZone",vec2,20)
        local injectstatic = nil
        if cargotype == CTLD_CARGO.Enum.VEHICLE or cargotype == CTLD_CARGO.Enum.FOB then
          cargotemplates = string.gsub(cargotemplates,"{","")
          cargotemplates = string.gsub(cargotemplates,"}","")
          cargotemplates = UTILS.Split(cargotemplates,";")
          injectstatic = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass) 
          injectstatic:SetStaticTypeAndShape(StaticCategory,StaticType,StaticShape)     
        elseif cargotype == CTLD_CARGO.Enum.STATIC or cargotype == CTLD_CARGO.Enum.REPAIR then
          injectstatic = CTLD_CARGO:New(nil,cargoname,cargotemplates,cargotype,true,true,size,nil,true,mass)
          injectstatic:SetStaticTypeAndShape(StaticCategory,StaticType,StaticShape)
          local map=cargotype:GetStaticResourceMap()
          injectstatic:SetStaticResourceMap(map) 
        end
        if injectstatic then
          self:InjectStatics(dropzone,injectstatic,false,true)
        end
      end    
    end
    if self.keeploadtable then -- keeploadtables
      self:__Loaded(1,self.LoadedGroupsTable)
    end
    return self
  end
end -- end do

do 
--- **Hercules Cargo AIR Drop Events** by Anubis Yinepu
-- Moose CTLD OO refactoring by Applevangelist
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO CTLD_HERCULES
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- This script will only work for the Herculus mod by Anubis, and only for **Air Dropping** cargo from the Hercules. 
-- Use the standard Moose CTLD if you want to unload on the ground.
-- Payloads carried by pylons 11, 12 and 13 need to be declared in the Herculus_Loadout.lua file
-- Except for Ammo pallets, this script will spawn whatever payload gets launched from pylons 11, 12 and 13
-- Pylons 11, 12 and 13 are moveable within the Herculus cargobay area
-- Ammo pallets can only be jettisoned from these pylons with no benefit to DCS world
-- To benefit DCS world, Ammo pallets need to be off/on loaded using DCS arming and refueling window
-- Cargo_Container_Enclosed = true: Cargo enclosed in container with parachute, need to be dropped from 100m (300ft) or more, except when parked on ground
-- Cargo_Container_Enclosed = false: Open cargo with no parachute, need to be dropped from 10m (30ft) or less

------------------------------------------------------
--- **CTLD_HERCULES** class, extends Core.Base#BASE
-- @type CTLD_HERCULES
-- @field #string ClassName
-- @field #string lid
-- @field #string Name
-- @field #string Version
-- @extends Core.Base#BASE
CTLD_HERCULES = {
  ClassName = "CTLD_HERCULES",
  lid = "",
  Name = "",
  Version = "0.0.3",
}

--- Define cargo types.
-- @type CTLD_HERCULES.Types
-- @field #table Type Name of cargo type, container (boolean) in container or not.
CTLD_HERCULES.Types = {
  ["ATGM M1045 HMMWV TOW Air [7183lb]"] = {['name'] = "M1045 HMMWV TOW", ['container'] = true},
  ["ATGM M1045 HMMWV TOW Skid [7073lb]"] = {['name'] = "M1045 HMMWV TOW", ['container'] = false},
  ["APC M1043 HMMWV Armament Air [7023lb]"] = {['name'] = "M1043 HMMWV Armament", ['container'] = true},
  ["APC M1043 HMMWV Armament Skid [6912lb]"] = {['name'] = "M1043 HMMWV Armament", ['container'] = false},
  ["SAM Avenger M1097 Air [7200lb]"] = {['name'] = "M1097 Avenger", ['container'] = true},
  ["SAM Avenger M1097 Skid [7090lb]"] = {['name'] = "M1097 Avenger", ['container'] = false},
  ["APC Cobra Air [10912lb]"] = {['name'] = "Cobra", ['container'] = true},
  ["APC Cobra Skid [10802lb]"] = {['name'] = "Cobra", ['container'] = false},
  ["APC M113 Air [21624lb]"] = {['name'] = "M-113", ['container'] = true},
  ["APC M113 Skid [21494lb]"] = {['name'] = "M-113", ['container'] = false},
  ["Tanker M978 HEMTT [34000lb]"] = {['name'] = "M978 HEMTT Tanker", ['container'] = false},
  ["HEMTT TFFT [34400lb]"] = {['name'] = "HEMTT TFFT", ['container'] = false},
  ["SPG M1128 Stryker MGS [33036lb]"] = {['name'] = "M1128 Stryker MGS", ['container'] = false},
  ["AAA Vulcan M163 Air [21666lb]"] = {['name'] = "Vulcan", ['container'] = true},
  ["AAA Vulcan M163 Skid [21577lb]"] = {['name'] = "Vulcan", ['container'] = false},
  ["APC M1126 Stryker ICV [29542lb]"] = {['name'] = "M1126 Stryker ICV", ['container'] = false},
  ["ATGM M1134 Stryker [30337lb]"] = {['name'] = "M1134 Stryker ATGM", ['container'] = false},
  ["APC LAV-25 Air [22520lb]"] = {['name'] = "LAV-25", ['container'] = true},
  ["APC LAV-25 Skid [22514lb]"] = {['name'] = "LAV-25", ['container'] = false},
  ["M1025 HMMWV Air [6160lb]"] = {['name'] = "Hummer", ['container'] = true},
  ["M1025 HMMWV Skid [6050lb]"] = {['name'] = "Hummer", ['container'] = false},
  ["IFV M2A2 Bradley [34720lb]"] = {['name'] = "M-2 Bradley", ['container'] = false},
  ["IFV MCV-80 [34720lb]"] = {['name'] = "MCV-80", ['container'] = false},
  ["IFV BMP-1 [23232lb]"] = {['name'] = "BMP-1", ['container'] = false},
  ["IFV BMP-2 [25168lb]"] = {['name'] = "BMP-2", ['container'] = false},
  ["IFV BMP-3 [32912lb]"] = {['name'] = "BMP-3", ['container'] = false},
  ["ARV BRDM-2 Air [12320lb]"] = {['name'] = "BRDM-2", ['container'] = true},
  ["ARV BRDM-2 Skid [12210lb]"] = {['name'] = "BRDM-2", ['container'] = false},
  ["APC BTR-80 Air [23936lb]"] = {['name'] = "BTR-80", ['container'] = true},
  ["APC BTR-80 Skid [23826lb]"] = {['name'] = "BTR-80", ['container'] = false},
  ["APC BTR-82A Air [24998lb]"] = {['name'] = "BTR-82A", ['container'] = true},
  ["APC BTR-82A Skid [24888lb]"] = {['name'] = "BTR-82A", ['container'] = false},
  ["SAM ROLAND ADS [34720lb]"] = {['name'] = "Roland Radar", ['container'] = false},
  ["SAM ROLAND LN [34720b]"] = {['name'] = "Roland ADS", ['container'] = false},
  ["SAM SA-13 STRELA [21624lb]"] = {['name'] = "Strela-10M3", ['container'] = false},
  ["AAA ZSU-23-4 Shilka [32912lb]"] = {['name'] = "ZSU-23-4 Shilka", ['container'] = false},
  ["SAM SA-19 Tunguska 2S6 [34720lb]"] = {['name'] = "2S6 Tunguska", ['container'] = false},
  ["Transport UAZ-469 Air [3747lb]"] = {['name'] = "UAZ-469", ['container'] = true},
  ["Transport UAZ-469 Skid [3630lb]"] = {['name'] = "UAZ-469", ['container'] = false},
  ["AAA GEPARD [34720lb]"] = {['name'] = "Gepard", ['container'] = false},
  ["SAM CHAPARRAL Air [21624lb]"] = {['name'] = "M48 Chaparral", ['container'] = true},
  ["SAM CHAPARRAL Skid [21516lb]"] = {['name'] = "M48 Chaparral", ['container'] = false},
  ["SAM LINEBACKER [34720lb]"] = {['name'] = "M6 Linebacker", ['container'] = false},
  ["Transport URAL-375 [14815lb]"] = {['name'] = "Ural-375", ['container'] = false},
  ["Transport M818 [16000lb]"] = {['name'] = "M 818", ['container'] = false},
  ["IFV MARDER [34720lb]"] = {['name'] = "Marder", ['container'] = false},
  ["Transport Tigr Air [15900lb]"] = {['name'] = "Tigr_233036", ['container'] = true},
  ["Transport Tigr Skid [15730lb]"] = {['name'] = "Tigr_233036", ['container'] = false},
  ["IFV TPZ FUCH [33440lb]"] = {['name'] = "TPZ", ['container'] = false},
  ["IFV BMD-1 Air [18040lb]"] = {['name'] = "BMD-1", ['container'] = true},
  ["IFV BMD-1 Skid [17930lb]"] = {['name'] = "BMD-1", ['container'] = false},
  ["IFV BTR-D Air [18040lb]"] = {['name'] = "BTR_D", ['container'] = true},
  ["IFV BTR-D Skid [17930lb]"] = {['name'] = "BTR_D", ['container'] = false},
  ["EWR SBORKA Air [21624lb]"] = {['name'] = "Dog Ear radar", ['container'] = true},
  ["EWR SBORKA Skid [21624lb]"] = {['name'] = "Dog Ear radar", ['container'] = false},
  ["ART 2S9 NONA Air [19140lb]"] = {['name'] = "SAU 2-C9", ['container'] = true},
  ["ART 2S9 NONA Skid [19030lb]"] = {['name'] = "SAU 2-C9", ['container'] = false},
  ["ART GVOZDIKA [34720lb]"] = {['name'] = "SAU Gvozdika", ['container'] = false},
  ["APC MTLB Air [26400lb]"] = {['name'] = "MTLB", ['container'] = true},
  ["APC MTLB Skid [26290lb]"] = {['name'] = "MTLB", ['container'] = false},
}

--- Cargo Object
-- @type CTLD_HERCULES.CargoObject
-- @field #number Cargo_Drop_Direction
-- @field #table Cargo_Contents
-- @field #string Cargo_Type_name
-- @field #boolean Container_Enclosed
-- @field #boolean ParatrooperGroupSpawn
-- @field #number Cargo_Country
-- @field #boolean offload_cargo
-- @field #boolean all_cargo_survive_to_the_ground
-- @field #boolean all_cargo_gets_destroyed
-- @field #boolean destroy_cargo_dropped_without_parachute
-- @field Core.Timer#TIMER scheduleFunctionID

--- [User] Instantiate a new object
-- @param #CTLD_HERCULES self
-- @param #string Coalition Coalition side, "red", "blue" or "neutral"
-- @param #string Alias Name of this instance
-- @param Ops.CTLD#CTLD CtldObject CTLD instance to link into
-- @return #CTLD_HERCULES self
-- @usage
-- Integrate to your CTLD instance like so, where `my_ctld` is a previously created CTLD instance:
--            
--            my_ctld.enableFixedWing = false -- avoid dual loading via CTLD F10 and F8 ground crew
--            local herccargo = CTLD_HERCULES:New("blue", "Hercules Test", my_ctld)
--            
-- You also need: 
-- * A template called "Infantry" for 10 Paratroopers (as set via herccargo.infantrytemplate). 
-- * Depending on what you are loading with the help of the ground crew, there are 42 more templates for the various vehicles that are loadable. 
-- There's a **quick check output in the `dcs.log`** which tells you what's there and what not.
-- E.g.:
--            ...Checking template for APC BTR-82A Air [24998lb] (BTR-82A) ... MISSING)
--            ...Checking template for ART 2S9 NONA Skid [19030lb] (SAU 2-C9) ... MISSING)
--            ...Checking template for EWR SBORKA Air [21624lb] (Dog Ear radar) ... MISSING)
--            ...Checking template for Transport Tigr Air [15900lb] (Tigr_233036) ... OK)
--            
-- Expected template names are the ones in the rounded brackets.
-- 
-- ### HINTS
-- 
-- The script works on the EVENTS.Shot trigger, which is used by the mod when you **drop cargo from the Hercules while flying**. Unloading on the ground does
-- not achieve anything here. If you just want to unload on the ground, use the normal Moose CTLD.
-- **Do not use** the **splash damage** script together with this, your cargo will just explode when reaching the ground!
-- 
-- ### Airdrops
-- 
-- There are two ways of airdropping:   
-- 1) Very low and very slow (>5m and <10m AGL) - here you can drop stuff which has "Skid" at the end of the cargo name (loaded via F8 Ground Crew menu)
-- 2) Higher up and slow (>100m AGL) - here you can drop paratroopers and cargo which has "Air" at the end of the cargo name (loaded via F8 Ground Crew menu)
-- 
-- ### General
-- 
-- Use either this method to integrate the Hercules **or** the one from the "normal" CTLD. Never both!
function CTLD_HERCULES:New(Coalition, Alias, CtldObject)
  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #CTLD_HERCULES
  
  --set Coalition
  if Coalition and type(Coalition)=="string" then
    if Coalition=="blue" then
      self.coalition=coalition.side.BLUE
      self.coalitiontxt = Coalition
    elseif Coalition=="red" then
      self.coalition=coalition.side.RED
      self.coalitiontxt = Coalition
    elseif Coalition=="neutral" then
      self.coalition=coalition.side.NEUTRAL
      self.coalitiontxt = Coalition
    else
      self:E("ERROR: Unknown coalition in CTLD!")
    end
  else
    self.coalition = Coalition
    self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition))
  end
  
  -- Set alias.
  if Alias then
    self.alias=tostring(Alias)
  else
    self.alias="UNHCR"  
    if self.coalition then
      if self.coalition==coalition.side.RED then
        self.alias="Red CTLD Hercules"
      elseif self.coalition==coalition.side.BLUE then
        self.alias="Blue CTLD Hercules"
      end
    end
  end
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", self.alias, self.coalitiontxt)
  
  self.infantrytemplate = "Infantry" -- template for a group of 10 paratroopers
  self.CTLD = CtldObject -- Ops.CTLD#CTLD
  
  self.verbose = true
  
  self.j = 0
  self.carrierGroups = {}
  self.Cargo = {}
  self.ParatrooperCount = {}
  
  self.ObjectTracker = {}
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown")

  self:HandleEvent(EVENTS.Shot, self._HandleShot)
  
  self:I(self.lid .. "Started")
  
  self:CheckTemplates()
    
  return self
end

--- [Internal] Function to check availability of templates
-- @param #CTLD_HERCULES self
-- @return #CTLD_HERCULES self 
function CTLD_HERCULES:CheckTemplates()
  self:T(self.lid .. 'CheckTemplates')
  -- inject Paratroopers
  self.Types["Paratroopers 10"] = {
    name = self.infantrytemplate,
    container = false,
    available = false,
  }
  local missing = {}
  local nomissing = 0
  local found = {}
  local nofound = 0
  
  -- list of groundcrew loadables
  for _index,_tab in pairs (self.Types) do
    local outcometxt = "MISSING"
    if _DATABASE.Templates.Groups[_tab.name] then
      outcometxt = "OK"
      self.Types[_index].available= true
      found[_tab.name] = true
    else
      self.Types[_index].available = false
      missing[_tab.name] = true
    end
    if self.verbose then
      self:I(string.format(self.lid .. "Checking template for %s (%s) ... %s", _index,_tab.name,outcometxt))
    end
  end
  for _,_name in pairs(found) do
    nofound = nofound + 1
  end
  for _,_name in pairs(missing) do
    nomissing = nomissing + 1
  end
  self:I(string.format(self.lid .. "Template Check Summary: Found %d, Missing %d, Total %d",nofound,nomissing,nofound+nomissing))
  return self
end

--- [Internal] Function to spawn a soldier group of 10 units
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP Cargo_Drop_initiator
-- @param Core.Point#COORDINATE Cargo_Drop_Position
-- @param #string Cargo_Type_name
-- @param #number CargoHeading
-- @param #number Cargo_Country
-- @param #number GroupSpacing
-- @return #CTLD_HERCULES self 
function CTLD_HERCULES:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Drop_Position, Cargo_Type_name, CargoHeading, Cargo_Country, GroupSpacing)
  --- TODO: Rework into Moose Spawns
  self:T(self.lid .. 'Soldier_SpawnGroup')
  self:T(Cargo_Drop_Position)
   -- create a matching #CTLD_CARGO type
   local InjectTroopsType = CTLD_CARGO:New(nil,self.infantrytemplate,{self.infantrytemplate},CTLD_CARGO.Enum.TROOPS,true,true,10,nil,false,80)
   -- get a #ZONE object
   local position = Cargo_Drop_Position:GetVec2()
   local dropzone = ZONE_RADIUS:New("Infantry " .. math.random(1,10000),position,100)
   -- and go:
   self.CTLD:InjectTroops(dropzone,InjectTroopsType)
  return self
end

--- [Internal] Function to spawn a group
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP Cargo_Drop_initiator
-- @param Core.Point#COORDINATE Cargo_Drop_Position
-- @param #string Cargo_Type_name
-- @param #number CargoHeading
-- @param #number Cargo_Country
-- @return #CTLD_HERCULES self 
function CTLD_HERCULES:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Drop_Position, Cargo_Type_name, CargoHeading, Cargo_Country)
  --- TODO: Rework into Moose Spawns
  self:T(self.lid .. "Cargo_SpawnGroup")
  self:T(Cargo_Type_name)   
  if Cargo_Type_name ~= 'Container red 1' then
   -- create a matching #CTLD_CARGO type
   local InjectVehicleType = CTLD_CARGO:New(nil,Cargo_Type_name,{Cargo_Type_name},CTLD_CARGO.Enum.VEHICLE,true,true,1,nil,false,1000)
   -- get a #ZONE object
   local position = Cargo_Drop_Position:GetVec2()
   local dropzone = ZONE_RADIUS:New("Vehicle " .. math.random(1,10000),position,100)
   -- and go:
   self.CTLD:InjectVehicles(dropzone,InjectVehicleType)
  end
  return self
end

--- [Internal] Function to spawn static cargo
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP Cargo_Drop_initiator
-- @param Core.Point#COORDINATE Cargo_Drop_Position
-- @param #string Cargo_Type_name
-- @param #number CargoHeading
-- @param #boolean dead
-- @param #number Cargo_Country
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Drop_Position, Cargo_Type_name, CargoHeading, dead, Cargo_Country)
  --- TODO: Rework into Moose Static Spawns
  self:T(self.lid .. "Cargo_SpawnStatic")
  self:T("Static " .. Cargo_Type_name .. " Dead " .. tostring(dead))
  local position = Cargo_Drop_Position:GetVec2()
  local Zone = ZONE_RADIUS:New("Cargo Static " .. math.random(1,10000),position,100)
  if not dead then
    local injectstatic = CTLD_CARGO:New(nil,"Cargo Static Group "..math.random(1,10000),"iso_container",CTLD_CARGO.Enum.STATIC,true,false,1,nil,true,4500,1) 
    self.CTLD:InjectStatics(Zone,injectstatic,true,true)
  end
  return self
end

--- [Internal] Function to spawn cargo by type at position
-- @param #CTLD_HERCULES self
-- @param #string Cargo_Type_name
-- @param Core.Point#COORDINATE Cargo_Drop_Position
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:Cargo_SpawnDroppedAsCargo(_name, _pos)
  local theCargo = self.CTLD:_FindCratesCargoObject(_name) -- #CTLD_CARGO
  if theCargo then
    self.CTLD.CrateCounter = self.CTLD.CrateCounter + 1
    local CCat, CType, CShape = theCargo:GetStaticTypeAndShape()
    local basetype = CType or self.CTLD.basetype or "container_cargo"
    CCat = CCat or "Cargos"
    local theStatic = SPAWNSTATIC:NewFromType(basetype,CCat,self.cratecountry)    
    :InitCargoMass(theCargo.PerCrateMass)
    :InitCargo(self.CTLD.enableslingload)
    :InitCoordinate(_pos)
    if CShape then
      theStatic:InitShape(CShape)
    end
    theStatic:Spawn(270,_name .. "-Container-".. math.random(1,100000))
  
    self.CTLD.Spawned_Crates[self.CTLD.CrateCounter] = theStatic
    local newCargo = CTLD_CARGO:New(self.CTLD.CargoCounter, theCargo.Name, theCargo.Templates, theCargo.CargoType, true, false, theCargo.CratesNeeded, self.CTLD.Spawned_Crates[self.CTLD.CrateCounter], true, theCargo.PerCrateMass, nil, theCargo.Subcategory)
    local map=theCargo:GetStaticResourceMap()
    newCargo:SetStaticResourceMap(map)
    table.insert(self.CTLD.Spawned_Cargo, newCargo)

    newCargo:SetWasDropped(true)
    newCargo:SetHasMoved(true)
  end
  return self
end

--- [Internal] Spawn cargo objects
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP Cargo_Drop_initiator
-- @param #number Cargo_Drop_Direction
-- @param Core.Point#COORDINATE Cargo_Content_position
-- @param #string Cargo_Type_name
-- @param #boolean Cargo_over_water
-- @param #boolean Container_Enclosed
-- @param #boolean ParatrooperGroupSpawn
-- @param #boolean offload_cargo
-- @param #boolean all_cargo_survive_to_the_ground
-- @param #boolean all_cargo_gets_destroyed
-- @param #boolean destroy_cargo_dropped_without_parachute
-- @param #number Cargo_Country
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:Cargo_SpawnObjects(Cargo_Drop_initiator,Cargo_Drop_Direction, Cargo_Content_position, Cargo_Type_name, Cargo_over_water, Container_Enclosed, ParatrooperGroupSpawn, offload_cargo, all_cargo_survive_to_the_ground, all_cargo_gets_destroyed, destroy_cargo_dropped_without_parachute, Cargo_Country)
  self:T(self.lid .. 'Cargo_SpawnObjects')
  
  local CargoHeading = self.CargoHeading
  
  if offload_cargo == true or ParatrooperGroupSpawn == true then  
    if ParatrooperGroupSpawn == true then
      self:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country, 10)
    else
      self:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country)
    end
  else
    if all_cargo_gets_destroyed == true or Cargo_over_water == true then

    else
      if all_cargo_survive_to_the_ground == true then
        if ParatrooperGroupSpawn == true then
          self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, true, Cargo_Country)
        else
          self:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country)
        end
        if Container_Enclosed == true then
          if ParatrooperGroupSpawn == false then
            self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, "Hercules_Container_Parachute_Static", CargoHeading, false, Cargo_Country)
          end
        end
      end
      if destroy_cargo_dropped_without_parachute == true then
        if Container_Enclosed == true then
          if ParatrooperGroupSpawn == true then
            self:Soldier_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country, 0)
          else
            if self.CTLD.dropAsCargoCrate then
              self:Cargo_SpawnDroppedAsCargo(Cargo_Type_name, Cargo_Content_position)
            else
              self:Cargo_SpawnGroup(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, Cargo_Country)
              self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, "Hercules_Container_Parachute_Static", CargoHeading, false, Cargo_Country)
            end
          end
        else
          self:Cargo_SpawnStatic(Cargo_Drop_initiator,Cargo_Content_position, Cargo_Type_name, CargoHeading, true, Cargo_Country)
        end
      end
    end
  end
  return self
end

--- [Internal] Function to calculate object height
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP group The group for which to calculate the height
-- @return #number height over ground
function CTLD_HERCULES:Calculate_Object_Height_AGL(group)
  self:T(self.lid .. "Calculate_Object_Height_AGL")
  if group.ClassName and group.ClassName == "GROUP" then
    local gcoord = group:GetCoordinate()
    local height = group:GetHeight()
    local lheight = gcoord:GetLandHeight()
    self:T(self.lid .. "Height " .. height - lheight)
    return height - lheight
  else
    -- DCS object
    if group:isExist() then
      local dcsposition = group:getPosition().p
      local dcsvec2 = {x = dcsposition.x, y = dcsposition.z} -- Vec2
      local height = math.floor(group:getPosition().p.y - land.getHeight(dcsvec2))
      self.ObjectTracker[group.id_] = dcsposition --  Vec3
      self:T(self.lid .. "Height " .. height)
      return height
    else
      return 0
    end
  end
end

--- [Internal] Function to check surface type
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP group The group for which to calculate the height
-- @return #number height over ground
function CTLD_HERCULES:Check_SurfaceType(object)
  self:T(self.lid .. "Check_SurfaceType")
   -- LAND,--1 SHALLOW_WATER,--2 WATER,--3 ROAD,--4 RUNWAY--5
  if object:isExist() then
   return land.getSurfaceType({x = object:getPosition().p.x, y = object:getPosition().p.z})
  else
   return 1
  end
end

--- [Internal] Function to track cargo objects
-- @param #CTLD_HERCULES self
-- @param #CTLD_HERCULES.CargoObject cargo
-- @param Wrapper.Group#GROUP initiator
-- @return #number height over ground
function CTLD_HERCULES:Cargo_Track(cargo, initiator)
  self:T(self.lid .. "Cargo_Track")
  local Cargo_Drop_initiator = initiator
  if cargo.Cargo_Contents ~= nil then
    if self:Calculate_Object_Height_AGL(cargo.Cargo_Contents) < 10 then --pallet less than 5m above ground before spawning
      if self:Check_SurfaceType(cargo.Cargo_Contents) == 2 or self:Check_SurfaceType(cargo.Cargo_Contents) == 3 then
        cargo.Cargo_over_water = true--pallets gets destroyed in water
      end
      local dcsvec3 = self.ObjectTracker[cargo.Cargo_Contents.id_] or initiator:GetVec3() -- last known position
      self:T("SPAWNPOSITION: ")
      self:T({dcsvec3})
      local Vec2 = {
         x=dcsvec3.x,
         y=dcsvec3.z,
      }
      local vec3 = COORDINATE:NewFromVec2(Vec2)
      self.ObjectTracker[cargo.Cargo_Contents.id_] =  nil
      self:Cargo_SpawnObjects(Cargo_Drop_initiator,cargo.Cargo_Drop_Direction, vec3, cargo.Cargo_Type_name, cargo.Cargo_over_water, cargo.Container_Enclosed, cargo.ParatrooperGroupSpawn, cargo.offload_cargo, cargo.all_cargo_survive_to_the_ground, cargo.all_cargo_gets_destroyed, cargo.destroy_cargo_dropped_without_parachute, cargo.Cargo_Country)
      if cargo.Cargo_Contents:isExist() then
       cargo.Cargo_Contents:destroy()--remove pallet+parachute before hitting ground and replace with Cargo_SpawnContents
      end
      --timer.removeFunction(cargo.scheduleFunctionID)
      cargo.scheduleFunctionID:Stop()
      cargo = {}
    end
  end
  return self
end

--- [Internal] Function to calc north correction
-- @param #CTLD_HERCULES self
-- @param Core.Point#POINT_Vec3 point Position Vec3
-- @return #number north correction
function CTLD_HERCULES:Calculate_Cargo_Drop_initiator_NorthCorrection(point)
  self:T(self.lid .. "Calculate_Cargo_Drop_initiator_NorthCorrection")
  if not point.z then --Vec2; convert to Vec3
    point.z = point.y
    point.y = 0
  end
  local lat, lon = coord.LOtoLL(point)
  local north_posit = coord.LLtoLO(lat + 1, lon)
  return math.atan2(north_posit.z - point.z, north_posit.x - point.x)
end

--- [Internal] Function to calc initiator heading
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP Cargo_Drop_initiator
-- @return #number north corrected heading
function CTLD_HERCULES:Calculate_Cargo_Drop_initiator_Heading(Cargo_Drop_initiator)
  self:T(self.lid .. "Calculate_Cargo_Drop_initiator_Heading") 
  local Heading = Cargo_Drop_initiator:GetHeading()
  Heading = Heading + self:Calculate_Cargo_Drop_initiator_NorthCorrection(Cargo_Drop_initiator:GetVec3())
  if Heading < 0 then
    Heading = Heading + (2 * math.pi)-- put heading in range of 0 to 2*pi
  end
  return Heading + 0.06 -- rad
end

--- [Internal] Function to initialize dropped cargo
-- @param #CTLD_HERCULES self
-- @param Wrapper.Group#GROUP Initiator
-- @param #table Cargo_Contents Table 'weapon' from event data
-- @param #string Cargo_Type_name Name of this cargo
-- @param #boolean Container_Enclosed Is container?
-- @param #boolean SoldierGroup Is soldier group?
-- @param #boolean ParatrooperGroupSpawnInit Is paratroopers?
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:Cargo_Initialize(Initiator, Cargo_Contents, Cargo_Type_name, Container_Enclosed, SoldierGroup, ParatrooperGroupSpawnInit)
  self:T(self.lid .. "Cargo_Initialize") 
    local Cargo_Drop_initiator = Initiator:GetName()
    if Cargo_Drop_initiator ~= nil then
      if ParatrooperGroupSpawnInit == true then
         self:T("Paratrooper Drop")
         -- Paratroopers
        if not self.ParatrooperCount[Cargo_Drop_initiator] then
         self.ParatrooperCount[Cargo_Drop_initiator] = 1
        else
         self.ParatrooperCount[Cargo_Drop_initiator] = self.ParatrooperCount[Cargo_Drop_initiator] + 1
        end
        
        local Paratroopers = self.ParatrooperCount[Cargo_Drop_initiator]
        
        self:T("Paratrooper Drop Number " .. self.ParatrooperCount[Cargo_Drop_initiator])
        
        local SpawnParas = false
        
        if math.fmod(Paratroopers,10) == 0 then
          SpawnParas = true
        end
        
        self.j = self.j + 1
        self.Cargo[self.j] = {}
        self.Cargo[self.j].Cargo_Drop_Direction = self:Calculate_Cargo_Drop_initiator_Heading(Initiator)
        self.Cargo[self.j].Cargo_Contents = Cargo_Contents
        self.Cargo[self.j].Cargo_Type_name = Cargo_Type_name
        self.Cargo[self.j].Container_Enclosed = Container_Enclosed
        self.Cargo[self.j].ParatrooperGroupSpawn = SpawnParas
        self.Cargo[self.j].Cargo_Country = Initiator:GetCountry()

        if self:Calculate_Object_Height_AGL(Initiator) < 5.0 then --aircraft on ground
          self.Cargo[self.j].offload_cargo = true
        elseif self:Calculate_Object_Height_AGL(Initiator) < 10.0 then --aircraft less than 10m above ground
          self.Cargo[self.j].all_cargo_survive_to_the_ground = true
        elseif self:Calculate_Object_Height_AGL(Initiator) < 100.0 then --aircraft more than 10m but less than 100m above ground
          self.Cargo[self.j].all_cargo_gets_destroyed = true
        else
         self.Cargo[self.j].all_cargo_gets_destroyed = false  
        end

        local timer = TIMER:New(self.Cargo_Track,self,self.Cargo[self.j],Initiator)
        self.Cargo[self.j].scheduleFunctionID  = timer
        timer:Start(1,1,600)  
        
      else
       -- no paras
        self.j = self.j + 1
        self.Cargo[self.j] = {}
        self.Cargo[self.j].Cargo_Drop_Direction = self:Calculate_Cargo_Drop_initiator_Heading(Initiator)
        self.Cargo[self.j].Cargo_Contents = Cargo_Contents
        self.Cargo[self.j].Cargo_Type_name = Cargo_Type_name
        self.Cargo[self.j].Container_Enclosed = Container_Enclosed
        self.Cargo[self.j].ParatrooperGroupSpawn = false
        self.Cargo[self.j].Cargo_Country = Initiator:GetCountry()

        if self:Calculate_Object_Height_AGL(Initiator) < 5.0 then--aircraft on ground
          self.Cargo[self.j].offload_cargo = true
        elseif self:Calculate_Object_Height_AGL(Initiator) < 10.0 then--aircraft less than 10m above ground
          self.Cargo[self.j].all_cargo_survive_to_the_ground = true
        elseif self:Calculate_Object_Height_AGL(Initiator) < 100.0 then--aircraft more than 10m but less than 100m above ground
          self.Cargo[self.j].all_cargo_gets_destroyed = true
        else
          self.Cargo[self.j].destroy_cargo_dropped_without_parachute = true --aircraft more than 100m above ground
        end
        
        local timer = TIMER:New(self.Cargo_Track,self,self.Cargo[self.j],Initiator)
        self.Cargo[self.j].scheduleFunctionID  = timer
        timer:Start(1,1,600)
      end
    end
  return self 
end

--- [Internal] Function to change cargotype per group (Wrench)
-- @param #CTLD_HERCULES self
-- @param #number key Carrier key id
-- @param #string cargoType Type of cargo
-- @param #number cargoNum Number of cargo objects
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:SetType(key,cargoType,cargoNum)
  self:T(self.lid .. "SetType") 
  self.carrierGroups[key]['cargoType'] = cargoType
  self.carrierGroups[key]['cargoNum'] = cargoNum
  return self
end

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- EventHandlers
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- [Internal] Function to capture SHOT event
-- @param #CTLD_HERCULES self
-- @param Core.Event#EVENTDATA Cargo_Drop_Event The event data
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:_HandleShot(Cargo_Drop_Event)
  self:T(self.lid .. "Shot Event ID:" .. Cargo_Drop_Event.id) 
  if Cargo_Drop_Event.id == EVENTS.Shot then
    
    local GT_Name = ""
    local SoldierGroup = false
    local ParatrooperGroupSpawnInit = false
    
    local GT_DisplayName = Weapon.getDesc(Cargo_Drop_Event.weapon).typeName:sub(15, -1)--Remove "weapons.bombs." from string
    self:T(string.format("%sCargo_Drop_Event: %s", self.lid, Weapon.getDesc(Cargo_Drop_Event.weapon).typeName))
 
    if (GT_DisplayName == "Squad 30 x Soldier [7950lb]") then
      self:Cargo_Initialize(Cargo_Drop_Event.IniGroup, Cargo_Drop_Event.weapon, "Soldier M4 GRG", false, true, true)
    end
 
    if self.Types[GT_DisplayName] then
      local GT_Name = self.Types[GT_DisplayName]['name']
    local Cargo_Container_Enclosed = self.Types[GT_DisplayName]['container']
        self:Cargo_Initialize(Cargo_Drop_Event.IniGroup, Cargo_Drop_Event.weapon, GT_Name, Cargo_Container_Enclosed)
      end
  end
  return self
end

--- [Internal] Function to capture BIRTH event
-- @param #CTLD_HERCULES self
-- @param Core.Event#EVENTDATA event The event data
-- @return #CTLD_HERCULES self
function CTLD_HERCULES:_HandleBirth(event)
  -- not sure what this is needed for? I think this for setting generic crates "content" setting.
  self:T(self.lid .. "Birth Event ID:" .. event.id)
  return self
end

end

-------------------------------------------------------------------
-- End Ops.CTLD.lua
-------------------------------------------------------------------
--- **Ops** - Combat Search and Rescue.
--
-- ===
--
-- **CSAR** - MOOSE based Helicopter CSAR Operations.
--
-- ===
--
-- ## Missions:--- **Ops** -- Combat Search and Rescue.
--
-- ===
--
-- **CSAR** - MOOSE based Helicopter CSAR Operations.
--
-- ===
--
-- ## Missions:
--
-- ### [CSAR - Combat Search & Rescue](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/CSAR)
--
-- ===
--
-- **Main Features:**
--
--    * MOOSE-based Helicopter CSAR Operations for Players.
--
-- ===
--
-- ### Author: **Applevangelist** (Moose Version), ***Ciribob*** (original), Thanks to: Shadowze, Cammel (testing), The Chosen One (Persistence)
-- @module Ops.CSAR
-- @image OPS_CSAR.jpg

---
-- Last Update Oct 2025

-------------------------------------------------------------------------
--- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM
-- @type CSAR
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field #number coalition Coalition side number, e.g. `coalition.side.RED`.
-- @field Core.Set#SET_GROUP allheligroupset Set of CSAR heli groups.
-- @field Core.Set#SET_GROUP UserSetGroup Set of CSAR heli groups as designed by the mission designer (if any set).
-- @extends Core.Fsm#FSM

--- *Combat search and rescue (CSAR) are search and rescue operations that are carried out during war that are within or near combat zones.* (Wikipedia)
--
-- ===
--
-- # CSAR Concept
--
--  * MOOSE-based Helicopter CSAR Operations for Players.
--  * Object oriented refactoring of Ciribob\'s fantastic CSAR script.
--  * No need for extra MIST loading.
--  * Additional events to tailor your mission.
--  * Optional SpawnCASEVAC to create casualties without beacon (e.g. handling dead ground vehicles and create CASVAC requests).
--
-- ## 0. Prerequisites
--
-- You need to load an .ogg soundfile for the pilot\'s beacons into the mission, e.g. "beacon.ogg", use a once trigger, "sound to country" for that.
-- Create a late-activated single infantry unit as template in the mission editor and name it e.g. "Downed Pilot".
--
-- Example sound files are here: [Moose Sound](https://github.com/FlightControl-Master/MOOSE_SOUND/tree/master/CTLD%20CSAR)
--
-- ## 1. Basic Setup
--
-- A basic setup example is the following:
--
--        -- Instantiate and start a CSAR for the blue side, with template "Downed Pilot" and alias "Luftrettung"
--        local my_csar = CSAR:New(coalition.side.BLUE,"Downed Pilot","Luftrettung")
--        -- options
--        my_csar.immortalcrew = true -- downed pilot spawn is immortal
--        my_csar.invisiblecrew = false -- downed pilot spawn is visible
--        -- start the FSM
--        my_csar:__Start(5)
--
-- ## 2. Options
--
-- The following options are available (with their defaults). Only set the ones you want changed:
--
--         mycsar.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined Arms.
--         mycsar.allowFARPRescue = true -- allows pilots to be rescued by landing at a FARP or Airbase. Else MASH only!
--         mycsar.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued.
--         mycsar.autosmoke = false -- automatically smoke a downed pilot\'s location when a heli is near.
--         mycsar.autosmokedistance = 1000 -- distance for autosmoke
--         mycsar.coordtype = 1 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates.
--         mycsar.csarOncrash = false -- (WIP) If set to true, will generate a downed pilot when a plane crashes as well.
--         mycsar.enableForAI = false -- set to false to disable AI pilots from being rescued.
--         mycsar.pilotRuntoExtractPoint = true -- Downed pilot will run to the rescue helicopter up to mycsar.extractDistance in meters.
--         mycsar.extractDistance = 500 -- Distance the downed pilot will start to run to the rescue helicopter.
--         mycsar.immortalcrew = true -- Set to true to make wounded crew immortal.
--         mycsar.invisiblecrew = false -- Set to true to make wounded crew insvisible.
--         mycsar.loadDistance = 75 -- configure distance for pilots to get into helicopter in meters.
--         mycsar.mashprefix = {"MASH"} -- prefixes of #GROUP objects used as MASHes. Will also try to add ZONE and STATIC objects with this prefix once at startup.
--         mycsar.max_units = 6 -- max number of pilots that can be carried if #CSAR.AircraftType is undefined.
--         mycsar.messageTime = 15 -- Time to show messages for in seconds. Doubled for long messages.
--         mycsar.radioSound = "beacon.ogg" -- the name of the sound file to use for the pilots\' radio beacons.
--         mycsar.smokecolor = 4 -- Color of smokemarker, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue.
--         mycsar.useprefix = true  -- Requires CSAR helicopter #GROUP names to have the prefix(es) defined below.
--         mycsar.csarPrefix = { "helicargo", "MEDEVAC"} -- #GROUP name prefixes used for useprefix=true - DO NOT use # in helicopter names in the Mission Editor!
--         mycsar.verbose = 0 -- set to > 1 for stats output for debugging.
--         -- limit amount of downed pilots spawned by **ejection** events
--         mycsar.limitmaxdownedpilots = true
--         mycsar.maxdownedpilots = 10
--         -- allow to set far/near distance for approach and optionally pilot must open doors
--         mycsar.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters
--         mycsar.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters
--         mycsar.pilotmustopendoors = false -- switch to true to enable check of open doors
--         mycsar.suppressmessages = false -- switch off all messaging if you want to do your own
--         mycsar.rescuehoverheight = 20 -- max height for a hovering rescue in meters
--         mycsar.rescuehoverdistance = 10 -- max distance for a hovering rescue in meters
--         -- Country codes for spawned pilots
--         mycsar.countryblue= country.id.USA
--         mycsar.countryred = country.id.RUSSIA
--         mycsar.countryneutral = country.id.UN_PEACEKEEPERS
--         mycsar.topmenuname = "CSAR" -- set the menu entry name
--         mycsar.ADFRadioPwr = 1000 -- ADF Beacons sending with 1KW as default
--         mycsar.PilotWeight = 80 --  Loaded pilots weigh 80kgs each
--         mycsar.AllowIRStrobe = false -- Allow a menu item to request an IR strobe to find a downed pilot at night (requires NVGs to see it).
--         mycsar.IRStrobeRuntime = 300 -- If an IR Strobe is activated, it runs for 300 seconds (5 mins).
--
-- ## 2.1 Create own SET_GROUP to manage CTLD Pilot groups
--
--         -- Parameter: Set The SET_GROUP object created by the mission designer/user to represent the CSAR pilot groups.
--         -- Needs to be set before starting the CSAR instance.
--         local myset = SET_GROUP:New():FilterPrefixes("Helikopter"):FilterCoalitions("red"):FilterStart()
--         mycsar:SetOwnSetPilotGroups(myset)
--
-- ## 2.2 SRS Features and Other Features
--
--       mycsar.useSRS = false -- Set true to use FF\'s SRS integration
--       mycsar.SRSPath = "C:\\Progra~1\\DCS-SimpleRadio-Standalone\\ExternalAudio\\" -- adjust your own path in your SRS installation -- server(!)
--       mycsar.SRSchannel = 300 -- radio channel
--       mycsar.SRSModulation = radio.modulation.AM -- modulation
--       mycsar.SRSport = 5002  -- and SRS Server port
--       mycsar.SRSCulture = "en-GB" -- SRS voice culture
--       mycsar.SRSVoice = nil -- SRS voice for downed pilot, relevant for Google TTS
--       mycsar.SRSGPathToCredentials = nil -- Path to your Google credentials json file, set this if you want to use Google TTS
--       mycsar.SRSVolume = 1 -- Volume, between 0 and 1
--       mycsar.SRSGender = "male" -- male or female voice
--       mycsar.CSARVoice = MSRS.Voices.Google.Standard.en_US_Standard_A -- SRS voice for CSAR Controller, relevant for Google TTS
--       mycsar.CSARVoiceMS = MSRS.Voices.Microsoft.Hedda -- SRS voice for CSAR Controller, relevant for MS Desktop TTS
--       mycsar.coordinate -- Coordinate from which CSAR TTS is sending. Defaults to a random MASH object position
--       --
--       mycsar.csarUsePara = false -- If set to true, will use the LandingAfterEjection Event instead of Ejection. Requires mycsar.enableForAI to be set to true. --shagrat
--       mycsar.wetfeettemplate = "man in floating thingy" -- if you use a mod to have a pilot in a rescue float, put the template name in here for wet feet spawns. Note: in conjunction with csarUsePara this might create dual ejected pilots in edge cases.
--       mycsar.allowbronco = false  -- set to true to use the Bronco mod as a CSAR plane
--       mycsar.CreateRadioBeacons = true -- set to false to disallow creating ADF radio beacons.
--
-- ## 3. Results
--
-- Number of successful landings with save pilots and aggregated number of saved pilots is stored in these variables in the object:
--
--        mycsar.rescues -- number of successful landings *with* saved pilots
--        mycsar.rescuedpilots -- aggregated number of pilots rescued from the field (of *all* players)
--
-- ## 4. Events
--
--  The class comes with a number of FSM-based events that missions designers can use to shape their mission.
--  These are:
--
-- ### 4.1. PilotDown.
--
--      The event is triggered when a new downed pilot is detected. Use e.g. `function my_csar:OnAfterPilotDown(...)` to link into this event:
--
--          function my_csar:OnAfterPilotDown(from, event, to, spawnedgroup, frequency, groupname, coordinates_text)
--            ... your code here ...
--          end
--
-- ### 4.2. Approach.
--
--      A CSAR helicpoter is closing in on a downed pilot. Use e.g. `function my_csar:OnAfterApproach(...)` to link into this event:
--
--          function my_csar:OnAfterApproach(from, event, to, heliname, groupname)
--            ... your code here ...
--          end
--
-- ### 4.3. Boarded.
--
--      The pilot has been boarded to the helicopter. Use e.g. `function my_csar:OnAfterBoarded(...)` to link into this event:
--
--          function my_csar:OnAfterBoarded(from, event, to, heliname, groupname, description)
--            ... your code here ...
--          end
--
-- ### 4.4. Returning.
--
--       The CSAR helicopter is ready to return to an Airbase, FARP or MASH. Use e.g. `function my_csar:OnAfterReturning(...)` to link into this event:
--
--          function my_csar:OnAfterReturning(from, event, to, heliname, groupname)
--            ... your code here ...
--          end
--
-- ### 4.5. Rescued.
--
--      The CSAR helicopter has landed close to an Airbase/MASH/FARP and the pilots are safe. Use e.g. `function my_csar:OnAfterRescued(...)` to link into this event:
--
--          function my_csar:OnAfterRescued(from, event, to, heliunit, heliname, pilotssaved)
--            ... your code here ...
--          end
--
-- ## 5. Spawn downed pilots at location to be picked up.
--
--      If missions designers want to spawn downed pilots into the field, e.g. at mission begin to give the helicopter guys works, they can do this like so:
--
--        -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition
--        my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Pilot Wagner", true )
--
--      --Create a casualty and CASEVAC request from a "Point" (VEC2) for the blue coalition --shagrat
--      my_csar:SpawnCASEVAC(Point, coalition.side.BLUE)
--
-- ## 6. Save and load downed pilots - Persistance
--
-- You can save and later load back downed pilots to make your mission persistent.
-- For this to work, you need to de-sanitize **io** and **lfs** in your MissionScripting.lua, which is located in your DCS installtion folder under Scripts.
-- There is a risk involved in doing that; if you do not know what that means, this is possibly not for you.
--
-- Use the following options to manage your saves:
--
--              mycsar.enableLoadSave = true -- allow auto-saving and loading of files
--              mycsar.saveinterval = 600 -- save every 10 minutes
--              mycsar.filename = "missionsave.csv" -- example filename
--              mycsar.filepath = "C:\\Users\\myname\\Saved Games\\DCS\Missions\\MyMission" -- example path
--
--  Then use an initial load at the beginning of your mission:
--
--            mycsar:__Load(10)
--
--  **Caveat:**
-- Dropped troop noMessage and forcedesc parameters aren't saved.
--
-- @field #CSAR
CSAR = {
  ClassName       = "CSAR",
  verbose         =     0,
  lid             =   "",
  coalition       = 1,
  coalitiontxt    = "blue",
  FreeVHFFrequencies = {},
  UsedVHFFrequencies = {},
  takenOff = {},
  csarUnits = {},  -- table of unit names
  downedPilots = {},
  -- = {},
  landedStatus = {},
  addedTo = {},
  woundedGroups = {}, -- contains the new group of units
  inTransitGroups = {}, -- contain a table for each SAR with all units he has with the original names
  smokeMarkers = {}, -- tracks smoke markers for groups
  heliVisibleMessage = {}, -- tracks if the first message has been sent of the heli being visible
  heliCloseMessage = {}, -- tracks heli close message  ie heli < 500m distance
  max_units = 6, -- number of pilots that can be carried
  hoverStatus = {}, -- tracks status of a helis hover above a downed pilot
  pilotDisabled = {}, -- tracks what aircraft a pilot is disabled for
  pilotLives = {}, -- tracks how many lives a pilot has
  useprefix    = true,  -- Use the Prefixed defined below, Requires Unit have the Prefix defined below
  csarPrefix = {},
  template = nil,
  mash = {},
  smokecolor = 4,
  rescues = 0,
  rescuedpilots = 0,
  limitmaxdownedpilots = true,
  maxdownedpilots = 10,
  useFIFOLimitReplacement = false, -- If true, it will remove the oldest downed pilot when a new one is added, if the limit is reached.
  allheligroupset = nil,
  topmenuname = "CSAR",
  ADFRadioPwr = 1000,
  PilotWeight = 80,
  CreateRadioBeacons = true,
  UserSetGroup = nil,
  AllowIRStrobe = false,
  IRStrobeRuntime = 300,
}

--- Downed pilots info.
-- @type CSAR.DownedPilot
-- @field #number index Pilot index.
-- @field #string name Name of the spawned group.
-- @field #number side Coalition.
-- @field #string originalUnit Name of the original unit.
-- @field #string desc Description.
-- @field #string typename Typename of Unit.
-- @field #number frequency Frequency of the NDB.
-- @field #string player Player name if applicable.
-- @field Wrapper.Group#GROUP group Spawned group object.
-- @field #number timestamp Timestamp for approach process.
-- @field #boolean alive Group is alive or dead/rescued.
-- @field #boolean wetfeet Group is spawned over (deep) water.
-- @field #string BeaconName Name of radio beacon - if any.

--- All slot / Limit settings
-- @type CSAR.AircraftType
-- @field #string typename Unit type name.
CSAR.AircraftType = {} -- Type and limit
CSAR.AircraftType["SA342Mistral"] = 2
CSAR.AircraftType["SA342Minigun"] = 2
CSAR.AircraftType["SA342L"] = 4
CSAR.AircraftType["SA342M"] = 4
CSAR.AircraftType["UH-1H"] = 8
CSAR.AircraftType["Mi-8MTV2"] = 12
CSAR.AircraftType["Mi-8MT"] = 12
CSAR.AircraftType["Mi-24P"] = 8
CSAR.AircraftType["Mi-24V"] = 8
CSAR.AircraftType["Bell-47"] = 2
CSAR.AircraftType["UH-60L"] = 10
CSAR.AircraftType["UH-60L_DAP"] = 2
CSAR.AircraftType["AH-64D_BLK_II"] = 2
CSAR.AircraftType["Bronco-OV-10A"] = 2
CSAR.AircraftType["MH-60R"] = 10
CSAR.AircraftType["OH-6A"] = 2
CSAR.AircraftType["OH58D"] = 2
CSAR.AircraftType["CH-47Fbl1"] = 31

--- CSAR class version.
-- @field #string version
CSAR.version="1.0.34"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- DONE: SRS Integration (to be tested)
-- TODO: Maybe - add option to smoke/flare closest MASH
-- DONE: shagrat Add cargoWeight to helicopter when pilot boarded
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new CSAR object and start the FSM.
-- @param #CSAR self
-- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral".
-- @param #string Template Name of the late activated infantry unit standing in for the downed pilot.
-- @param #string Alias An *optional* alias how this object is called in the logs etc.
-- @return #CSAR self
function CSAR:New(Coalition, Template, Alias)

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #CSAR

  BASE:T({Coalition, Template, Alias})

  --set Coalition
  if Coalition and type(Coalition)=="string" then
    if Coalition=="blue" then
      self.coalition=coalition.side.BLUE
      self.coalitiontxt = Coalition
    elseif Coalition=="red" then
      self.coalition=coalition.side.RED
      self.coalitiontxt = Coalition
    elseif Coalition=="neutral" then
      self.coalition=coalition.side.NEUTRAL
      self.coalitiontxt = Coalition
    else
      self:E("ERROR: Unknown coalition in CSAR!")
    end
  else
    self.coalition = Coalition
    self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition))
  end

  -- Set alias.
  if Alias then
    self.alias=tostring(Alias)
  else
    self.alias="Red Cross"
    if self.coalition then
      if self.coalition==coalition.side.RED then
        self.alias="IFRC"
      elseif self.coalition==coalition.side.BLUE then
        self.alias="CSAR"
      end
    end
  end

  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown")

  -- Start State.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  --                 From State  -->   Event        -->     To State
  self:AddTransition("Stopped",       "Start",              "Running")     -- Start FSM.
  self:AddTransition("*",             "Status",             "*")           -- CSAR status update.
  self:AddTransition("*",             "PilotDown",          "*")          -- Downed Pilot added
  self:AddTransition("*",             "Approach",           "*")         -- CSAR heli closing in.
  self:AddTransition("*",             "Landed",             "*")         -- CSAR heli landed
  self:AddTransition("*",             "Boarded",            "*")          -- Pilot boarded.
  self:AddTransition("*",             "Returning",          "*")        -- CSAR able to return to base.
  self:AddTransition("*",             "Rescued",            "*")          -- Pilot at MASH.
  self:AddTransition("*",             "KIA",                "*")          -- Pilot killed in action.
  self:AddTransition("*",             "Load",                "*")           -- CSAR load  event.
  self:AddTransition("*",             "Save",                "*")           -- CSAR save  event.
  self:AddTransition("*",             "Stop",               "Stopped")     -- Stop FSM.

  -- tables, mainly for tracking actions
  self.addedTo = {}
  self.allheligroupset = {} -- GROUP_SET of all helis
  self.csarUnits = {} -- table of CSAR unit names
  self.FreeVHFFrequencies = {}
  self.heliVisibleMessage = {} -- tracks if the first message has been sent of the heli being visible
  self.heliCloseMessage = {} -- tracks heli close message  ie heli < 500m distance
  self.hoverStatus = {} -- tracks status of a helis hover above a downed pilot
  self.inTransitGroups = {} -- contain a table for each SAR with all units he has with the original names
  self.landedStatus = {}
  self.lastCrash = {}
  self.takenOff = {}
  self.smokeMarkers = {} -- tracks smoke markers for groups
  self.UsedVHFFrequencies = {}
  self.woundedGroups = {} -- contains the new group of units
  self.downedPilots = {} -- Replacement woundedGroups
  self.downedpilotcounter = 1

  -- settings, counters etc
  self.rescues = 0 -- counter for successful rescue landings at FARP/AFB/MASH
  self.rescuedpilots = 0 -- counter for saved pilots
  self.csarOncrash = false -- If set to true, will generate a csar when a plane crashes as well.
  self.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined arms.
  self.enableForAI = false -- set to false to disable AI units from being rescued.
  self.smokecolor = 4 -- Color of smokemarker for blue side, 0 is green, 1 is red, 2 is white, 3 is orange and 4 is blue
  self.coordtype = 2 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates.
  self.immortalcrew = true -- Set to true to make wounded crew immortal
  self.invisiblecrew = false -- Set to true to make wounded crew insvisible
  self.messageTime = 15 -- Time to show longer messages for in seconds
  self.pilotRuntoExtractPoint = true -- Downed Pilot will run to the rescue helicopter up to self.extractDistance METERS
  self.loadDistance = 75 -- configure distance for pilot to get in helicopter in meters.
  self.extractDistance = 500 -- Distance the Downed pilot will run to the rescue helicopter
  self.loadtimemax = 135 -- seconds
  self.radioSound = "beacon.ogg" -- the name of the sound file to use for the Pilot radio beacons. If this isnt added to the mission BEACONS WONT WORK!
  self.beaconRefresher = 29 -- seconds
  self.allowFARPRescue = true --allows pilot to be rescued by landing at a FARP or Airbase
  self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued.
  self.max_units = 6 --max number of pilots that can be carried
  self.useprefix = true  -- Use the Prefixed defined below, Requires Unit have the Prefix defined below
  self.csarPrefix = { "helicargo", "MEDEVAC"} -- prefixes used for useprefix=true - DON\'T use # in names!
  self.template = Template or "generic" -- template for downed pilot
  self.mashprefix = {"MASH"} -- prefixes used to find MASHes

  self.autosmoke = false -- automatically smoke location when heli is near
  self.autosmokedistance = 2000 -- distance for autosmoke
  -- added 0.1.4
  self.limitmaxdownedpilots = true
  self.maxdownedpilots = 25
  -- generate Frequencies
  self:_GenerateVHFrequencies()
  -- added 0.1.8
  self.approachdist_far = 5000 -- switch do 10 sec interval approach mode, meters
  self.approachdist_near = 3000 -- switch to 5 sec interval approach mode, meters
  self.pilotmustopendoors = false -- switch to true to enable check on open doors
  self.suppressmessages = false

  -- added 0.1.11r1
  self.rescuehoverheight = 20
  self.rescuehoverdistance = 10

  -- added 0.1.12
  self.countryblue= country.id.USA
  self.countryred = country.id.RUSSIA
  self.countryneutral = country.id.UN_PEACEKEEPERS

  -- added 0.1.3
  self.csarUsePara = false -- shagrat set to true, will use the LandingAfterEjection Event instead of Ejection

  -- added 0.1.4
  self.wetfeettemplate = nil
  self.usewetfeet = false

  -- added 1.0.15
  self.allowbronco = false  -- set to true to use the Bronco mod as a CSAR plane

  self.ADFRadioPwr = 500

  -- added 1.0.16
  self.PilotWeight = 80

  -- Own SET_GROUP if any
  self.UserSetGroup = nil

  -- WARNING - here\'ll be dragons
  -- for this to work you need to de-sanitize your mission environment in <DCS root>\Scripts\MissionScripting.lua
  -- needs SRS => 1.9.6 to work (works on the *server* side)
  self.useSRS = false -- Use FF\'s SRS integration
  self.SRSPath = "E:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" -- adjust your own path in your server(!)
  self.SRSchannel = 300 -- radio channel
  self.SRSModulation = radio.modulation.AM -- modulation
  self.SRSport = 5002 -- port
  self.SRSCulture = "en-GB"
  self.SRSVoice = MSRS.Voices.Google.Standard.en_GB_Standard_B
  self.SRSGPathToCredentials = nil
  self.SRSVolume = 1.0 -- volume 0.0 to 1.0
  self.SRSGender = "male" -- male or female
  self.CSARVoice = MSRS.Voices.Google.Standard.en_US_Standard_A
  self.CSARVoiceMS = MSRS.Voices.Microsoft.Hedda
  self.coordinate = nil -- Core.Point#COORDINATE

  local AliaS = string.gsub(self.alias," ","_")
  self.filename = string.format("CSAR_%s_Persist.csv",AliaS)

  -- load and save downed pilots
  self.enableLoadSave = false
  self.filepath = nil
  self.saveinterval = 600

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the CSAR. Initializes parameters and starts event handlers.
  -- @function [parent=#CSAR] Start
  -- @param #CSAR self

  --- Triggers the FSM event "Start" after a delay. Starts the CSAR. Initializes parameters and starts event handlers.
  -- @function [parent=#CSAR] __Start
  -- @param #CSAR self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the CSAR and all its event handlers.
  -- @param #CSAR self

  --- Triggers the FSM event "Stop" after a delay. Stops the CSAR and all its event handlers.
  -- @function [parent=#CSAR] __Stop
  -- @param #CSAR self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Status".
  -- @function [parent=#CSAR] Status
  -- @param #CSAR self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#CSAR] __Status
  -- @param #CSAR self
  -- @param #number delay Delay in seconds.
  --
  --   --- Triggers the FSM event "Load".
  -- @function [parent=#CSAR] Load
  -- @param #CSAR self

  --- Triggers the FSM event "Load" after a delay.
  -- @function [parent=#CSAR] __Load
  -- @param #CSAR self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Save".
  -- @function [parent=#CSAR] Load
  -- @param #CSAR self

  --- Triggers the FSM event "Save" after a delay.
  -- @function [parent=#CSAR] __Save
  -- @param #CSAR self
  -- @param #number delay Delay in seconds.

  --- On After "PilotDown" event. Downed Pilot detected.
  -- @function [parent=#CSAR] OnAfterPilotDown
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Group#GROUP Group Group object of the downed pilot.
  -- @param #number Frequency Beacon frequency in kHz.
  -- @param #string Leadername Name of the #UNIT of the downed pilot.
  -- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype.
  -- @param #string Playername Player name if any given. Might be nil!

  --- On After "Aproach" event. Heli close to downed Pilot.
  -- @function [parent=#CSAR] OnAfterApproach
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string Heliname Name of the helicopter group.
  -- @param #string Woundedgroupname Name of the downed pilot\'s group.

  --- On After "Landed" event. Heli landed at an airbase.
  -- @function [parent=#CSAR] OnAfterLanded
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string HeliName Name of the #UNIT which has landed.
  -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where the heli landed.

  --- On After "Boarded" event. Downed pilot boarded heli.
  -- @function [parent=#CSAR] OnAfterBoarded
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string Heliname Name of the helicopter group.
  -- @param #string Woundedgroupname Name of the downed pilot\'s group.
  -- @param #string Description Descriptive name of the group.

  --- On After "Returning" event. Heli can return home with downed pilot(s).
  -- @function [parent=#CSAR] OnAfterReturning
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string Heliname Name of the helicopter group.
  -- @param #string Woundedgroupname Name of the downed pilot\'s group.

  --- On After "Rescued" event. Pilot(s) have been brought to the MASH/FARP/AFB.
  -- @function [parent=#CSAR] OnAfterRescued
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter.
  -- @param #string HeliName Name of the helicopter group.
  -- @param #number PilotsSaved Number of the saved pilots on board when landing.

  --- On After "KIA" event. Pilot is dead.
  -- @function [parent=#CSAR] OnAfterKIA
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string Pilotname Name of the pilot KIA.

  --- FSM Function OnAfterLoad.
  -- @function [parent=#CSAR] OnAfterLoad
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
  -- @param #string filename (Optional) File name for loading. Default is "CSAR_<alias>_Persist.csv".

  --- FSM Function OnAfterSave.
  -- @function [parent=#CSAR] OnAfterSave
  -- @param #CSAR self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
  -- @param #string filename (Optional) File name for saving. Default is "CSAR_<alias>_Persist.csv".

  return self
end

------------------------
--- Helper Functions ---
------------------------

--- (Internal) Function to insert downed pilot tracker object.
-- @param #CSAR self
-- @param Wrapper.Group#GROUP Group The #GROUP object
-- @param #string Groupname Name of the spawned group.
-- @param #number Side Coalition.
-- @param #string OriginalUnit Name of original Unit.
-- @param #string Description Descriptive text.
-- @param #string Typename Typename of unit.
-- @param #number Frequency Frequency of the NDB in Hz
-- @param #string Playername Name of Player (if applicable)
-- @param #boolean Wetfeet Ejected over water
-- @return #CSAR self.
function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername,Wetfeet,BeaconName)
  self:T({"_CreateDownedPilotTrack",Groupname,Side,OriginalUnit,Description,Typename,Frequency,Playername})

  -- create new entry
  local DownedPilot = {} -- #CSAR.DownedPilot
  DownedPilot.desc = Description or ""
  DownedPilot.frequency = Frequency or 0
  DownedPilot.index = self.downedpilotcounter
  DownedPilot.name = Groupname or Playername or ""
  DownedPilot.originalUnit = OriginalUnit or ""
  DownedPilot.player = Playername or ""
  DownedPilot.side = Side or 0
  DownedPilot.typename = Typename or ""
  DownedPilot.group = Group
  DownedPilot.timestamp = 0
  DownedPilot.alive = true
  DownedPilot.wetfeet = Wetfeet or false
  DownedPilot.BeaconName = BeaconName

  -- Add Pilot
  local PilotTable = self.downedPilots
  local counter = self.downedpilotcounter
  PilotTable[counter] = {}
  PilotTable[counter] = DownedPilot
  self:T({Table=PilotTable})
  self.downedPilots = PilotTable
  -- Increase counter
  self.downedpilotcounter = self.downedpilotcounter+1
  return self
end

--- (Internal) Count pilots on board.
-- @param #CSAR self
-- @param #string _heliName
-- @return #number count
function CSAR:_PilotsOnboard(_heliName)
  self:T(self.lid .. " _PilotsOnboard")
  local count = 0
  if self.inTransitGroups[_heliName] then
    for _, _group in pairs(self.inTransitGroups[_heliName]) do
      count = count + 1
    end
  end
  return count
end

--- (Internal) Function to check for dupe eject events.
-- @param #CSAR self
-- @param #string _unitname Name of unit.
-- @return #boolean Outcome
function CSAR:_DoubleEjection(_unitname)
  if self.lastCrash[_unitname] then
    local _time = self.lastCrash[_unitname]
    if timer.getTime() - _time < 10 then
      self:E(self.lid.."Caught double ejection!")
      return true
    end
  end
  self.lastCrash[_unitname] = timer.getTime()
  return false
end

--- (User) Add a PLAYERTASK - FSM events will check success
-- @param #CSAR self
-- @param Ops.PlayerTask#PLAYERTASK PlayerTask
-- @return #CSAR self
function CSAR:AddPlayerTask(PlayerTask)
  self:T(self.lid .. " AddPlayerTask")
  if not self.PlayerTaskQueue then
    self.PlayerTaskQueue = FIFO:New()
  end
  self.PlayerTaskQueue:Push(PlayerTask,PlayerTask.PlayerTaskNr)
  return self
end

--- (Internal) Spawn a downed pilot
-- @param #CSAR self
-- @param #number country Country for template.
-- @param Core.Point#COORDINATE point Coordinate to spawn at.
-- @param #number frequency Frequency of the pilot's beacon
-- @param #boolean wetfeet Spawn is over water
-- @return Wrapper.Group#GROUP group The #GROUP object.
-- @return #string alias The alias name.
function CSAR:_SpawnPilotInField(country,point,frequency,wetfeet)
  self:T({country,point,frequency,tostring(wetfeet)})
  local freq = frequency or 1000
  local freq = freq / 1000 -- kHz
  for i=1,10 do
    math.random(i,10000)
  end
  if point:IsSurfaceTypeWater() or wetfeet then
    point.y = 0
  end
  local template = self.template
  if self.usewetfeet and wetfeet then
    template = self.wetfeettemplate
  end
  local alias = string.format("Pilot %.2fkHz-%d", freq, math.random(1,99))
  local coalition = self.coalition
  local pilotcacontrol = self.allowDownedPilotCAcontrol -- Switch AI on/oof - is this really correct for CA?
  local _spawnedGroup = SPAWN
    :NewWithAlias(template,alias)
    :InitCoalition(coalition)
    :InitCountry(country)
    :InitDelayOff()
    :SpawnFromCoordinate(point)

  return _spawnedGroup, alias -- Wrapper.Group#GROUP object
end

--- (Internal) Add options to a downed pilot
-- @param #CSAR self
-- @param Wrapper.Group#GROUP group Group to use.
function CSAR:_AddSpecialOptions(group)
  self:T(self.lid.." _AddSpecialOptions")
  self:T({group})

  local immortalcrew = self.immortalcrew
  local invisiblecrew = self.invisiblecrew
  if immortalcrew then
    local _setImmortal = {
      id = 'SetImmortal',
      params = {
        value = true
      }
    }
    group:SetCommand(_setImmortal)
  end

  if invisiblecrew then
    local _setInvisible = {
      id = 'SetInvisible',
      params = {
        value = true
      }
    }
    group:SetCommand(_setInvisible)
  end

  group:OptionAlarmStateGreen()
  group:OptionROEHoldFire()
  return self
end

--- (Internal) Function to spawn a CSAR object into the scene.
-- @param #CSAR self
-- @param #number _coalition Coalition
-- @param DCS#country.id _country Country ID
-- @param Core.Point#COORDINATE _point Coordinate
-- @param #string _typeName Typename
-- @param #string _unitName Unitname
-- @param #string _playerName Playername
-- @param #number _freq Frequency
-- @param #boolean noMessage
-- @param #string _description Description
-- @param #boolean forcedesc Use the description only for the pilot track entry
-- @return Wrapper.Group#GROUP PilotInField Pilot GROUP object
-- @return #string AliasName Alias display name
function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description, forcedesc )
  self:T(self.lid .. " _AddCsar")
  self:T({_coalition , _country, _point, _typeName, _unitName, _playerName, _freq, noMessage, _description})

  local template = self.template
  local wetfeet = false

  local surface = _point:GetSurfaceType()
  if surface == land.SurfaceType.WATER then
    wetfeet = true
  end

  if not _freq then
    _freq = self:_GenerateADFFrequency()
    if not _freq then _freq = 333000 end --noob catch
  end

  local _spawnedGroup, _alias = self:_SpawnPilotInField(_country,_point,_freq,wetfeet)

  local _typeName = _typeName or "Pilot"

  if not noMessage then
    if _freq ~= 0 then --shagrat different CASEVAC msg
     -- self:_DisplayToAllSAR("MAYDAY MAYDAY! " .. _typeName .. " is down. ", self.coalition, self.messageTime)
    else
      self:_DisplayToAllSAR("Troops In Contact. " .. _typeName .. " requests CASEVAC. ", self.coalition, self.messageTime)
    end
  end

  local BeaconName

  if _playerName then
    BeaconName = _playerName..math.random(1,10000)
  elseif _unitName then
    BeaconName = _unitName..math.random(1,10000)
  else
    BeaconName = "Ghost-1-1"..math.random(1,10000)
  end

  if (_freq and _freq ~= 0) then --shagrat only add beacon if _freq is NOT 0
    self:_AddBeaconToGroup(_spawnedGroup, _freq, BeaconName)
  end

  self:_AddSpecialOptions(_spawnedGroup)

  local _text = _description
  if not forcedesc then
    if _playerName ~= nil then
      if _freq ~= 0 then --shagrat
        _text = "Pilot " .. _playerName
      else
        _text = "TIC - " .. _playerName
      end
    elseif _unitName ~= nil then
      if _freq ~= 0 then --shagrat
        _text = "AI Pilot of " .. _unitName
      else
        _text = "TIC - " .. _unitName
      end
    end
  end
  self:T({_spawnedGroup, _alias})

  local _GroupName = _spawnedGroup:GetName() or _alias

  self:_CreateDownedPilotTrack(_spawnedGroup,_GroupName,_coalition,_unitName,_text,_typeName,_freq,_playerName,wetfeet,BeaconName)

  self:_InitSARForPilot(_spawnedGroup, _unitName, _freq, noMessage, _playerName) --shagrat use unitName to have the aircraft callsign / descriptive "name" etc.

  return _spawnedGroup, _alias
end

--- (Internal) Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene.
-- @param #CSAR self
-- @param #string _zone Name of the zone. Can also be passed as a (normal, round) ZONE object.
-- @param #number _coalition Coalition.
-- @param #string _description (optional) Description.
-- @param #boolean _randomPoint (optional) Random yes or no.
-- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR.
-- @param #string unitname (optional) Name of the lost unit.
-- @param #string typename (optional) Type of plane.
-- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names.
function CSAR:_SpawnCsarAtZone( _zone, _coalition, _description, _randomPoint, _nomessage, unitname, typename, forcedesc)
  self:T(self.lid .. " _SpawnCsarAtZone")
  local freq = self:_GenerateADFFrequency()

  local _triggerZone = nil
  if type(_zone) == "string" then
    _triggerZone = ZONE:New(_zone) -- trigger to use as reference position
  elseif type(_zone) == "table" and _zone.ClassName then
    if string.find(_zone.ClassName, "ZONE",1) then
      _triggerZone = _zone -- is already a zone
    end
  end

  if _triggerZone == nil then
    self:E(self.lid.."ERROR: Can\'t find zone called " .. _zone, 10)
    return
  end

  local _description = _description or "PoW"
  local unitname = unitname or "Old Rusty"
  local typename = typename or "Phantom II"

  local pos = {}
  if _randomPoint then
    local _pos =  _triggerZone:GetRandomPointVec3()
    pos = COORDINATE:NewFromVec3(_pos)
  else
    pos  = _triggerZone:GetCoordinate()
  end

  local _country = 0
  if _coalition == coalition.side.BLUE then
    _country = self.countryblue
  elseif _coalition == coalition.side.RED then
    _country = self.countryred
  else
    _country = self.countryneutral
  end

  self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, freq, _nomessage, _description, forcedesc)

  return self
end

--- Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene.
-- @param #CSAR self
-- @param #string Zone Name of the zone. Can also be passed as a (normal, round) ZONE object.
-- @param #number Coalition Coalition.
-- @param #string Description (optional) Description.
-- @param #boolean RandomPoint (optional) Random yes or no.
-- @param #boolean Nomessage (optional) If true, don\'t send a message to SAR.
-- @param #string Unitname (optional) Name of the lost unit.
-- @param #string Typename (optional) Type of plane.
-- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names.
-- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so:
--
--        -- Create downed "Pilot Wagner" in #ZONE "CSAR_Start_1" at a random point for the blue coalition
--        my_csar:SpawnCSARAtZone( "CSAR_Start_1", coalition.side.BLUE, "Wagner", true, false, "Charly-1-1", "F5E" )
function CSAR:SpawnCSARAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc)
  self:_SpawnCsarAtZone(Zone, Coalition, Description, RandomPoint, Nomessage, Unitname, Typename, Forcedesc)
  return self
end

--- (Internal) Function to add a CSAR object into the scene at a Point coordinate (VEC_2). For mission designers wanting to add e.g. casualties to the scene, that don't use beacons.
-- @param #CSAR self
-- @param Core.Point#COORDINATE _Point
-- @param #number _coalition Coalition.
-- @param #string _description (optional) Description.
-- @param #boolean _nomessage (optional) If true, don\'t send a message to SAR.
-- @param #string unitname (optional) Name of the lost unit.
-- @param #string typename (optional) Type of plane.
-- @param #boolean forcedesc (optional) Force to use the description passed only for the pilot track entry. Use to have fully custom names.
function CSAR:_SpawnCASEVAC( _Point, _coalition, _description, _nomessage, unitname, typename, forcedesc) --shagrat added internal Function _SpawnCASEVAC
  self:T(self.lid .. " _SpawnCASEVAC")

  local _description = _description or "CASEVAC"
  local unitname = unitname or "CASEVAC"
  local typename = typename or "Ground Commander"

  local pos = {}
  pos  = _Point

  local _country = 0
  if _coalition == coalition.side.BLUE then
    _country = self.countryblue
  elseif _coalition == coalition.side.RED then
    _country = self.countryred
  else
    _country = self.countryneutral
  end
  --shagrat set frequency to 0 as "flag" for no beacon
  self:_AddCsar(_coalition, _country, pos, typename, unitname, _description, 0, _nomessage, _description, forcedesc)

  return self
end

--- Function to add a CSAR object into the scene at a zone coordinate. For mission designers wanting to add e.g. PoWs to the scene.
-- @param #CSAR self
-- @param Core.Point#COORDINATE Point
-- @param #number Coalition Coalition.
-- @param #string Description (optional) Description.
-- @param #boolean Nomessage (optional) If true, don\'t send a message to SAR.
-- @param #string Unitname (optional) Name of the lost unit.
-- @param #string Typename (optional) Type of plane.
-- @param #boolean Forcedesc (optional) Force to use the **description passed only** for the pilot track entry. Use to have fully custom names.
-- @usage If missions designers want to spawn downed pilots into the field, e.g. at mission begin, to give the helicopter guys work, they can do this like so:
--
--        -- Create casualty  "CASEVAC" at coordinate Core.Point#COORDINATE for the blue coalition.
--        my_csar:SpawnCASEVAC( coordinate, coalition.side.BLUE )
function CSAR:SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc)
  self:_SpawnCASEVAC(Point, Coalition, Description, Nomessage, Unitname, Typename, Forcedesc)
  return self
end --shagrat end added CASEVAC

--- (Internal) Event handler.
-- @param #CSAR self
function CSAR:_EventHandler(EventData)
  self:T(self.lid .. " _EventHandler")
  self:T({Event = EventData.id})

  local _event = EventData -- Core.Event#EVENTDATA

  -- no Player
  if self.enableForAI == false and _event.IniPlayerName == nil then
    return self
  end

  -- no event
  if _event == nil or _event.initiator == nil then
    return self

      -- take off
  elseif _event.id == EVENTS.Takeoff then -- taken off
    self:T(self.lid .. " Event unit - Takeoff")

    local _coalition = _event.IniCoalition
    if _coalition ~= self.coalition then
      return self --ignore!
    end

    if _event.IniGroupName then
      self.takenOff[_event.IniUnitName] = true
    end

    return self

      -- player enter unit
  elseif _event.id == EVENTS.PlayerEnterAircraft or _event.id == EVENTS.PlayerEnterUnit then --player entered unit
    self:T(self.lid .. " Event unit - Player Enter")

    local _coalition = _event.IniCoalition
    self:T("Coalition = "..UTILS.GetCoalitionName(_coalition))
    if _coalition ~= self.coalition then
      return self --ignore!
    end

    if _event.IniPlayerName then
      self.takenOff[_event.IniPlayerName] = nil
    end

    -- jumped into flying plane?
    self:T("Taken Off: "..tostring(_event.IniUnit:InAir(true)))

    if _event.IniUnit:InAir(true) then
      self.takenOff[_event.IniPlayerName] = true
    end

    local _unit = _event.IniUnit
    local _group = _event.IniGroup

    local function IsBronco(Group)
      local grp = Group -- Wrapper.Group#GROUP
      local typename = grp:GetTypeName()
      self:T(typename)
      if typename == "Bronco-OV-10A" then return true end
      return false
    end

    if _unit:IsHelicopter() or _group:IsHelicopter() or IsBronco(_group) then
      self:_AddMedevacMenuItem()
    end

    return self

  elseif (_event.id == EVENTS.PilotDead and self.csarOncrash == false) then
    -- Pilot dead

    self:T(self.lid .. " Event unit - Pilot Dead")

    local _unit = _event.IniUnit
    local _unitname = _event.IniUnitName
    local _group = _event.IniGroup

    if _unit == nil then
      return self -- error!
    end

    local _coalition = _event.IniCoalition
    if _coalition ~= self.coalition then
      return self --ignore!
    end

    -- Catch multiple events here?
    if self.takenOff[_event.IniUnitName] == true or _group:IsAirborne() then
      if self:_DoubleEjection(_unitname) then
        return self
      end

    else
      self:T(self.lid .. " Pilot has not taken off, ignore")
    end

    return self

  elseif _event.id == EVENTS.PilotDead or _event.id == EVENTS.Ejection then
    if _event.id == EVENTS.PilotDead and self.csarOncrash == false then
      return self
    end
    self:T(self.lid .. " Event unit - Pilot Ejected")

    local _unit = _event.IniUnit
    local _unitname = _event.IniUnitName
    local _group = _event.IniGroup

    self:T({_unit.UnitName, _unitname, _group.GroupName})

    if _unit == nil then
      self:T("Unit NIL!")
      return self -- error!
    end

    --local _coalition = _unit:GetCoalition() -- nil now for some reason
    local _coalition = _group:GetCoalition()
    if _coalition ~= self.coalition then
      self:T("Wrong coalition! Coalition = "..UTILS.GetCoalitionName(_coalition))
      return self --ignore!
    end


    self:T("Airborne: "..tostring(_group:IsAirborne()))
    self:T("Taken Off: "..tostring(self.takenOff[_event.IniUnitName]))

    if not self.takenOff[_event.IniUnitName] and not _group:IsAirborne() then
      self:T(self.lid .. " Pilot has not taken off, ignore")
      -- return self -- give up, pilot hasnt taken off
    end

    if self:_DoubleEjection(_unitname) then
      self:T("Double Ejection!")
      return self
    end


    local initdcscoord = nil
    local initcoord = nil
    if _event.id == EVENTS.Ejection and _event.TgtDCSUnit then
      initdcscoord = _event.TgtDCSUnit:getPoint()
      initcoord = COORDINATE:NewFromVec3(initdcscoord)
      self:T({initdcscoord})
    elseif _event.IniDCSUnit then
      initdcscoord = _event.IniDCSUnit:getPoint()
      initcoord = COORDINATE:NewFromVec3(initdcscoord)
      self:T({initdcscoord})
    end

    -- Remove downed pilot if already exists to replace with new one.
    if _event.IniPlayerName then
      local PilotTable = self.downedPilots --#CSAR.DownedPilot
      local _foundPilot = nil
      for _,_pilot in pairs(PilotTable) do
        if _pilot.player == _event.IniPlayerName and _pilot.alive == true then
          _foundPilot = _pilot
          break
        end
      end
      if _foundPilot then
        self:T("Downed pilot already exists!")
        _foundPilot.group:Destroy(false)
        self:_RemoveNameFromDownedPilots(_foundPilot.name)
        self:_CheckDownedPilotTable()
      end
    end

    -- limit no of pilots in the field.
    if self.limitmaxdownedpilots and self:_ReachedPilotLimit() then
      self:T("Maxed Downed Pilot!")
      return self
    end


    -- TODO: Over water check --- EVENTS.LandingAfterEjection NOT triggered by DCS, so handle csarUsePara = true case
    -- might create dual pilots in edge cases

    local wetfeet = false

    --local surface = _unit:GetCoordinate():GetSurfaceType()
    local surface = initcoord:GetSurfaceType()

    if surface == land.SurfaceType.WATER then
      self:T("Wet feet!")
      wetfeet = true
    end
    -- all checks passed, get going.
    if self.csarUsePara == false or (self.csarUsePara and wetfeet ) then --shagrat check parameter LandingAfterEjection, if true don't spawn a Pilot from EJECTION event, wait for the Chute to land
      local _freq = self:_GenerateADFFrequency()
       self:_AddCsar(_coalition, _unit:GetCountry(), initcoord , _unit:GetTypeName(),  _unit:GetName(), _event.IniPlayerName, _freq, self.suppressmessages, "none")
      return self
    end

  elseif _event.id == EVENTS.Land then
    self:T(self.lid .. " Landing")

    if _event.IniUnitName then
      self.takenOff[_event.IniUnitName] = nil
    end

    if self.allowFARPRescue then

      local _unit = _event.IniUnit  -- Wrapper.Unit#UNIT

      if _unit == nil then
        self:T(self.lid .. " Unit nil on landing")
        return self -- error!
      end

      --local _coalition = _event.IniCoalition
      local _coalition = _event.IniGroup:GetCoalition()
      if _coalition ~= self.coalition then
        self:T(self.lid .. " Wrong coalition")
        return self --ignore!
      end

      self.takenOff[_event.IniUnitName] = nil

      local _place = _event.Place -- Wrapper.Airbase#AIRBASE

      if _place == nil then
        self:T(self.lid .. " Landing Place Nil")
        return self -- error!
      end

      -- anyone on board?
      if self.inTransitGroups[_event.IniUnitName] == nil then
        -- ignore
        return self
      end

      if _place:GetCoalition() == self.coalition or _place:GetCoalition() == coalition.side.NEUTRAL then
        self:__Landed(2,_event.IniUnitName, _place)
        local IsHeloBase = false
        local ABName = _place:GetName()
        if ABName and string.find(ABName,"^H") then IsHeloBase = true end -- if name starts with an H it's an (possibly elevated) helo base on current maps
        self:_ScheduledSARFlight(_event.IniUnitName,_event.IniGroupName,true,true,IsHeloBase)
      else
        self:T(string.format("Airfield %d, Unit %d", _place:GetCoalition(), _unit:GetCoalition()))
      end
    end

    return self
  end

  ---- shagrat on event LANDING_AFTER_EJECTION spawn pilot at parachute location
  if (_event.id == EVENTS.LandingAfterEjection and self.csarUsePara == true) then
    self:T("LANDING_AFTER_EJECTION")
    local _LandingPos = COORDINATE:NewFromVec3(_event.initiator:getPosition().p)
    local _unitname = "Aircraft" --_event.initiator:getName() or "Aircraft" --shagrat Optional use of Object name which is unfortunately 'f15_Pilot_Parachute'
    local _typename = "Ejected Pilot" --_event.Initiator.getTypeName() or "Ejected Pilot"
    local _country = _event.initiator:getCountry()
    local _coalition = coalition.getCountryCoalition( _country )
    self:T("Country = ".._country.." Coalition = ".._coalition)
    if _coalition == self.coalition then
      local _freq = self:_GenerateADFFrequency()
      self:I({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq})
      self:_AddCsar(_coalition, _country, _LandingPos, nil, _unitname, _event.IniPlayerName, _freq, self.suppressmessages, "none")--shagrat add CSAR at Parachute location.
    
      Unit.destroy(_event.initiator) -- shagrat remove static Pilot model
    end
  end

  return self
end

--- (Internal)  Initialize the action for a pilot.
-- @param #CSAR self
-- @param Wrapper.Group#GROUP _downedGroup The group to rescue.
-- @param #string _GroupName Name of the Group
-- @param #number _freq Beacon frequency.
-- @param #boolean _nomessage Send message true or false.
-- @param #string _playername Name of the downed pilot if any
function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage, _playername)
  self:T(self.lid .. " _InitSARForPilot")
  local _leader = _downedGroup:GetUnit(1)
  local _groupName = _GroupName
  local _freqk = _freq / 1000
  local _coordinatesText = self:_GetPositionOfWounded(_downedGroup)
  local _leadername = _leader:GetName()

  if not _nomessage then
    if _freq ~= 0 then --shagrat
     -- local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _groupName, _coordinatesText, _freqk)--shagrat _groupName to prevent 'f15_Pilot_Parachute'
      if self.coordtype ~= 2 then --not MGRS
       -- self:_DisplayToAllSAR(_text,self.coalition,self.messageTime)
      else
        self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,false,true)
        local coordtext = UTILS.MGRSStringToSRSFriendly(_coordinatesText,true)
        local _text = string.format("%s requests SAR at %s, beacon at %.2f kilo hertz", _groupName, coordtext, _freqk)
        self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,true,false)
      end
    else --shagrat CASEVAC msg
      local _text = string.format("Pickup Zone at %s.", _coordinatesText )
      if self.coordtype ~= 2 then --not MGRS
        self:_DisplayToAllSAR(_text,self.coalition,self.messageTime)
      else
        self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,false,true)
        local coordtext = UTILS.MGRSStringToSRSFriendly(_coordinatesText,true)
        local _text = string.format("Pickup Zone at %s.", coordtext )
        self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,true,false)
      end
    end
  end

  for _,_heliName in pairs(self.csarUnits) do
    self:_CheckWoundedGroupStatus(_heliName, _groupName)
  end

  -- trigger FSM event
  self:__PilotDown(2,_downedGroup, _freqk, _groupName, _coordinatesText, _playername)

  return self
end

--- (Internal) Check if a name is in downed pilot table
-- @param #CSAR self
-- @param #string name Name to search for.
-- @return #boolean Outcome.
-- @return #CSAR.DownedPilot Table if found else nil.
function CSAR:_CheckNameInDownedPilots(name)
  local PilotTable = self.downedPilots --#CSAR.DownedPilot
  local found = false
  local table = nil
  for _,_pilot in pairs(PilotTable) do
    if _pilot.name == name and _pilot.alive == true then
      found = true
      table = _pilot
      break
    end
  end
  return found, table
end

--- (Internal) Check if a name is in downed pilot table and remove it.
-- @param #CSAR self
-- @param #string name Name to search for.
-- @param #boolean force Force removal.
-- @return #boolean Outcome.
function CSAR:_RemoveNameFromDownedPilots(name,force)
  local PilotTable = self.downedPilots --#CSAR.DownedPilot
  local found = false
  for _index,_pilot in pairs(PilotTable) do
    if _pilot.name == name then
      self.downedPilots[_index].alive = false
    end
  end
  return found
end

--- [User] Set callsign options for TTS output. See @{Wrapper.Group#GROUP.GetCustomCallSign}() on how to set customized callsigns.
-- @param #CSAR self
-- @param #boolean ShortCallsign If true, only call out the major flight number
-- @param #boolean Keepnumber If true, keep the **customized callsign** in the #GROUP name for players as-is, no amendments or numbers.
-- @param #table CallsignTranslations (optional) Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized
-- callsigns from playername or group name.
-- @return #CSAR self
function CSAR:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations)
  if not ShortCallsign or ShortCallsign == false then
    self.ShortCallsign = false
  else
    self.ShortCallsign = true
  end
  self.Keepnumber = Keepnumber or false
  self.CallsignTranslations = CallsignTranslations
  return self
end

--- (Internal) Check if a name is in downed pilot table and remove it.
-- @param #CSAR self
-- @param #string UnitName
-- @return #string CallSign
function CSAR:_GetCustomCallSign(UnitName)
  local callsign = UnitName
  local unit = UNIT:FindByName(UnitName)
  if unit and unit:IsAlive() then
    local group = unit:GetGroup()
    callsign = group:GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations)
  end
  return callsign
end

--- (Internal) Check state of wounded group.
-- @param #CSAR self
-- @param #string heliname heliname
-- @param #string woundedgroupname woundedgroupname
function CSAR:_CheckWoundedGroupStatus(heliname,woundedgroupname)
  self:T(self.lid .. " _CheckWoundedGroupStatus")
  local _heliName = heliname
  local _woundedGroupName = woundedgroupname
  self:T({Heli = _heliName, Downed  = _woundedGroupName})
  -- if wounded group is not here then message already been sent to SARs
  -- stop processing any further
  local _found, _downedpilot = self:_CheckNameInDownedPilots(_woundedGroupName)
  if not _found then
    self:T("...not found in list!")
    return
  end

  local _woundedGroup = _downedpilot.group
  if _woundedGroup ~= nil and _woundedGroup:IsAlive() then
    local _heliUnit = self:_GetSARHeli(_heliName) -- Wrapper.Unit#UNIT

    local _lookupKeyHeli = _heliName .. "_" .. _woundedGroupName --lookup key for message state tracking

    if _heliUnit == nil then
      self.heliVisibleMessage[_lookupKeyHeli] = nil
      self.heliCloseMessage[_lookupKeyHeli] = nil
      self.landedStatus[_lookupKeyHeli] = nil
      self:T("...heliunit nil!")
      return
    end

    local _heliCoord = _heliUnit:GetCoordinate()
    local _leaderCoord = _woundedGroup:GetCoordinate()
    local _distance = self:_GetDistance(_heliCoord,_leaderCoord)
    -- autosmoke
    if (self.autosmoke == true) and (_distance < self.autosmokedistance) and (_distance ~= -1) then
      self:_PopSmokeForGroup(_woundedGroupName, _woundedGroup)
    end

    if _distance < self.approachdist_near and _distance > 0 then
      if self:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName) == true then
        -- we\'re close, reschedule
        _downedpilot.timestamp = timer.getAbsTime()
        self:__Approach(-5,heliname,woundedgroupname)
      end
    elseif _distance >= self.approachdist_near and _distance < self.approachdist_far then
      -- message once
      if self.heliVisibleMessage[_lookupKeyHeli] == nil then
        local _pilotName = _downedpilot.desc
        if self.autosmoke == true then
          local dist = self.autosmokedistance / 1000
          local disttext = string.format("%.0fkm",dist)
          if _SETTINGS:IsImperial() then
            local dist = UTILS.MetersToNM(self.autosmokedistance)
            disttext = string.format("%.0fnm",dist)
          end
          self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nI'll pop a smoke when you are %s away.\nLand or hover by the smoke.", self:_GetCustomCallSign(_heliName), _pilotName, disttext), self.messageTime,false,true)
        else
          self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. I hear you! Finally, that is music in my ears!\nRequest a flare or smoke if you need.", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true)
        end
        --mark as shown for THIS heli and THIS group
        self.heliVisibleMessage[_lookupKeyHeli] = true
      end
      self.heliCloseMessage[_lookupKeyHeli] = nil
      self.landedStatus[_lookupKeyHeli] = nil
      --reschedule as units aren\'t dead yet , schedule for a bit slower though as we\'re far away
      _downedpilot.timestamp = timer.getAbsTime()
      self:__Approach(-10,heliname,woundedgroupname)
    end
  else
    self:T("...Downed Pilot KIA?!")
    if not _downedpilot.alive then
      --self:__KIA(1,_downedpilot.name)
      self:_RemoveNameFromDownedPilots(_downedpilot.name, true)
    end
  end
  return self
end

--- (Internal) Function to pop a smoke at a wounded pilot\'s positions.
-- @param #CSAR self
-- @param #string _woundedGroupName Name of the group.
-- @param Wrapper.Group#GROUP _woundedLeader Object of the group.
function CSAR:_PopSmokeForGroup(_woundedGroupName, _woundedLeader)
  self:T(self.lid .. " _PopSmokeForGroup")
  -- have we popped smoke already in the last 5 mins
  local _lastSmoke = self.smokeMarkers[_woundedGroupName]
  if _lastSmoke == nil or timer.getTime() > _lastSmoke then

    local _smokecolor = self.smokecolor
    local _smokecoord = _woundedLeader:GetCoordinate():Translate( 6, math.random( 1, 360) ) --shagrat place smoke at a random 6 m distance, so smoke does not obscure the pilot
    _smokecoord:Smoke(_smokecolor)
    self.smokeMarkers[_woundedGroupName] = timer.getTime() + 300 -- next smoke time
  end
  return self
end

--- (Internal) Function to pickup the wounded pilot from the ground.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _heliUnit Object of the group.
-- @param #string _pilotName Name of the pilot.
-- @param Wrapper.Group#GROUP _woundedGroup Object of the group.
-- @param #string _woundedGroupName Name of the group.
function CSAR:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName)
  self:T(self.lid .. " _PickupUnit")
  -- board
  local _heliName = _heliUnit:GetName()
  local _groups = self.inTransitGroups[_heliName]
  local _unitsInHelicopter = self:_PilotsOnboard(_heliName)

  -- init table if there is none for this helicopter
  if not _groups then
    self.inTransitGroups[_heliName] = {}
    _groups = self.inTransitGroups[_heliName]
  end

  -- if the heli can\'t pick them up, show a message and return
  local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()]
  if _maxUnits == nil then
    _maxUnits = self.max_units
  end
  if _unitsInHelicopter + 1 > _maxUnits then
    self:_DisplayMessageToSAR(_heliUnit, string.format("%s, %s. We\'re already crammed with %d guys! Sorry!", _pilotName, self:_GetCustomCallSign(_heliName), _unitsInHelicopter, _unitsInHelicopter), self.messageTime,false,false,true)
    return self
  end

  local found,downedgrouptable = self:_CheckNameInDownedPilots(_woundedGroupName)
  local grouptable = downedgrouptable --#CSAR.DownedPilot
  self.inTransitGroups[_heliName][_woundedGroupName] =
    {
      originalUnit = grouptable.originalUnit,
      woundedGroup = _woundedGroupName,
      side = self.coalition,
      desc = grouptable.desc,
      player = grouptable.player,
    }

  _woundedGroup:Destroy(false)
  self:_RemoveNameFromDownedPilots(_woundedGroupName,true)

  self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s I\'m in! Get to the MASH ASAP! ", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,true,true)

  self:_UpdateUnitCargoMass(_heliName)

  self:__Boarded(5,_heliName,_woundedGroupName,grouptable.desc)

  return self
end

--- (Internal) Function to calculate and set Unit internal cargo mass
-- @param #CSAR self
-- @param #string _heliName Unit name
-- @return #CSAR self
function CSAR:_UpdateUnitCargoMass(_heliName)
  self:T(self.lid .. " _UpdateUnitCargoMass")
  local calculatedMass = self:_PilotsOnboard(_heliName)*(self.PilotWeight or 80)
  local Unit = UNIT:FindByName(_heliName)
  if Unit then
    Unit:SetUnitInternalCargo(calculatedMass)
  end
  return self
end

--- (Internal) Move group to destination.
-- @param #CSAR self
-- @param Wrapper.Group#GROUP _leader
-- @param Core.Point#COORDINATE _destination
function CSAR:_OrderGroupToMoveToPoint(_leader, _destination)
  self:T(self.lid .. " _OrderGroupToMoveToPoint")
  local group = _leader
  local coordinate = _destination:GetVec2()

  group:SetAIOn()
  group:RouteToVec2(coordinate,5)
  return self
end

--- (internal) Function to check if the heli door(s) are open. Thanks to Shadowze.
-- @param #CSAR self
-- @param #string unit_name Name of unit.
-- @return #boolean outcome The outcome.
function CSAR:_IsLoadingDoorOpen( unit_name )
  self:T(self.lid .. " _IsLoadingDoorOpen")
  return UTILS.IsLoadingDoorOpen(unit_name)
end

--- (Internal) Function to check if heli is close to group.
-- @param #CSAR self
-- @param #number _distance
-- @param Wrapper.Unit#UNIT _heliUnit
-- @param #string _heliName
-- @param Wrapper.Group#GROUP _woundedGroup
-- @param #string _woundedGroupName
-- @return #boolean Outcome
function CSAR:_CheckCloseWoundedGroup(_distance, _heliUnit, _heliName, _woundedGroup, _woundedGroupName)
  self:T(self.lid .. " _CheckCloseWoundedGroup")

  local _woundedLeader = _woundedGroup
  local _lookupKeyHeli = _heliUnit:GetName() .. "_" .. _woundedGroupName --lookup key for message state tracking

  local _found, _pilotable = self:_CheckNameInDownedPilots(_woundedGroupName) -- #boolean, #CSAR.DownedPilot
  local _pilotName = _pilotable.desc


  local _reset = true

  if (_distance < 500) then
    self:T(self.lid .. "[Pickup Debug] Helo closer than 500m: ".._lookupKeyHeli)
    if self.heliCloseMessage[_lookupKeyHeli] == nil then
      if self.autosmoke == true then
        self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land or hover at the smoke.", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true)
      else
        self:_DisplayMessageToSAR(_heliUnit, string.format("%s: %s. You\'re close now! Land in a safe place, I will go there ", self:_GetCustomCallSign(_heliName), _pilotName), self.messageTime,false,true)
      end
      self.heliCloseMessage[_lookupKeyHeli] = true
    end
    self:T(self.lid .. "[Pickup Debug] Checking landed vs Hover for ".._lookupKeyHeli)
    -- have we landed close enough?
    if not _heliUnit:InAir() then
      self:T(self.lid .. "[Pickup Debug] Helo landed: ".._lookupKeyHeli)
      if self.pilotRuntoExtractPoint == true then
        if (_distance < self.extractDistance) then
          local _time = self.landedStatus[_lookupKeyHeli]
          self:T(self.lid .. "[Pickup Debug] Check pilot running or arrived ".._lookupKeyHeli)
          if _time == nil then
            self:T(self.lid .. "[Pickup Debug] Pilot running not arrived yet ".._lookupKeyHeli)
            self.landedStatus[_lookupKeyHeli] = math.floor( (_distance - self.loadDistance) / 3.6 )
            _time = self.landedStatus[_lookupKeyHeli]
            _woundedGroup:OptionAlarmStateGreen()
            self:_OrderGroupToMoveToPoint(_woundedGroup, _heliUnit:GetCoordinate())
            self:_DisplayMessageToSAR(_heliUnit, "Wait till " .. _pilotName .. " gets in. \nETA " .. _time .. " more seconds.", self.messageTime, false)
          else
            _time = self.landedStatus[_lookupKeyHeli] - 10
            self.landedStatus[_lookupKeyHeli] = _time
          end
          --if _time <= 0 or _distance < self.loadDistance then
          self:T(self.lid .. "[Pickup Debug] Pilot close enough? ".._lookupKeyHeli)
          if _distance < self.loadDistance + 5 or _distance <= 13 then
            self:T(self.lid .. "[Pickup Debug] Pilot close enough - YES ".._lookupKeyHeli)
            if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then
              self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true)
              self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli)
              return false
            else
              self:T(self.lid .. "[Pickup Debug] Pick up Pilot ".._lookupKeyHeli)
              self.landedStatus[_lookupKeyHeli] = nil
              self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName)
              return true
            end
          end
        end
      else
        self:T(self.lid .. "[Pickup Debug] Helo landed, pilot NOT set to run to helo ".._lookupKeyHeli)
        if (_distance < self.loadDistance) then
          self:T(self.lid .. "[Pickup Debug] Helo close enough, door check ".._lookupKeyHeli)
          if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then
            self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli)
            self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true)
            return false
          else
            self:T(self.lid .. "[Pickup Debug] Pick up Pilot ".._lookupKeyHeli)
            self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName)
            return true
          end
        end
      end
    else
      self:T(self.lid .. "[Pickup Debug] Helo hovering".._lookupKeyHeli)
      local _unitsInHelicopter = self:_PilotsOnboard(_heliName)
      local _maxUnits = self.AircraftType[_heliUnit:GetTypeName()]
      if _maxUnits == nil then
        _maxUnits = self.max_units
      end
      self:T(self.lid .. "[Pickup Debug] Check capacity and close enough for winching ".._lookupKeyHeli)
      if _heliUnit:InAir() and _unitsInHelicopter + 1 <= _maxUnits then
        -- DONE - make variable
        if _distance < self.rescuehoverdistance then
          self:T(self.lid .. "[Pickup Debug] Helo hovering close enough ".._lookupKeyHeli)
          --check height!
          local leaderheight = _woundedLeader:GetHeight()
          if leaderheight < 0 then leaderheight = 0 end
          local _height = _heliUnit:GetHeight() - leaderheight

          -- DONE - make variable
          if _height <= self.rescuehoverheight then
            self:T(self.lid .. "[Pickup Debug] Helo hovering low enough ".._lookupKeyHeli)
            local _time = self.hoverStatus[_lookupKeyHeli]

            if _time == nil then
              self.hoverStatus[_lookupKeyHeli] = 10
              _time = 10
            else
              _time = self.hoverStatus[_lookupKeyHeli] - 10
              self.hoverStatus[_lookupKeyHeli] = _time
            end
            self:T(self.lid .. "[Pickup Debug] Check hover timer ".._lookupKeyHeli)
            if _time > 0 then
              self:T(self.lid .. "[Pickup Debug] Helo hovering not long enough ".._lookupKeyHeli)
              self:_DisplayMessageToSAR(_heliUnit, "Hovering above " .. _pilotName .. ". \n\nHold hover for " .. _time .. " seconds to winch them up. \n\nIf the countdown stops you\'re too far away!", self.messageTime, true)
            else
              self:T(self.lid .. "[Pickup Debug] Helo hovering long enough - door check ".._lookupKeyHeli)
              if self.pilotmustopendoors and (self:_IsLoadingDoorOpen(_heliName) == false) then
                self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me in!", self.messageTime, true, true)
                self:T(self.lid .. "[Pickup Debug] Door closed, try again next loop ".._lookupKeyHeli)
                return false
              else
                self.hoverStatus[_lookupKeyHeli] = nil
                self:_PickupUnit(_heliUnit, _pilotName, _woundedGroup, _woundedGroupName)
                self:T(self.lid .. "[Pickup Debug] Pilot picked up ".._lookupKeyHeli)
                return true
              end
            end
            _reset = false
          else
            self:T(self.lid .. "[Pickup Debug] Helo hovering too high ".._lookupKeyHeli)
            self:_DisplayMessageToSAR(_heliUnit, "Too high to winch " .. _pilotName .. " \nReduce height and hover for 10 seconds!", self.messageTime, true,true)
            self:T(self.lid .. "[Pickup Debug] Hovering too high, try again next loop ".._lookupKeyHeli)
            return false
          end
        end

      end
    end
  end

  if _reset then
    self.hoverStatus[_lookupKeyHeli] = nil
  end

  if _distance < 500 then
    return true
  else
    return false
  end
end

--- (Internal) Monitor in-flight returning groups.
-- @param #CSAR self
-- @param #string heliname Heli name
-- @param #string groupname Group name
-- @param #boolean isairport If true, EVENT.Landing took place at an airport or FARP
-- @param #boolean noreschedule If true, do not try to reschedule this if distances are not ok (coming from landing event)
-- @param #boolean IsHeloBase If true, landing took place at a Helo Base  (name "H ..." on current maps)
function CSAR:_ScheduledSARFlight(heliname,groupname, isairport, noreschedule, IsHeloBase)
  self:T(self.lid .. " _ScheduledSARFlight")
  self:T({heliname,groupname})
  local _heliUnit = self:_GetSARHeli(heliname)
  local _woundedGroupName = groupname

  if (_heliUnit == nil) then
    --helicopter crashed?
    self.inTransitGroups[heliname] = nil
    return
  end

  if self.inTransitGroups[heliname] == nil or self.inTransitGroups[heliname][_woundedGroupName] == nil then
    -- Groups already rescued
    return
  end

  local _dist = self:_GetClosestMASH(_heliUnit)

  if _dist == -1 then
    self:T(self.lid.."[Drop off debug] Check distance to MASH for "..heliname.." Distance can not be determined!")
    return
  end

  self:T(self.lid.."[Drop off debug] Check distance to MASH for "..heliname.." Distance km: "..math.floor(_dist/1000))

  if ( _dist < self.FARPRescueDistance or isairport ) and ((_heliUnit:InAir() == false) or (IsHeloBase == true)) then
    self:T(self.lid.."[Drop off debug] Distance ok, door check")
    if self.pilotmustopendoors and self:_IsLoadingDoorOpen(heliname) == false then
      self:_DisplayMessageToSAR(_heliUnit, "Open the door to let me out!", self.messageTime, true, true)
      self:T(self.lid.."[Drop off debug] Door closed, try again next loop")
    else
      self:T(self.lid.."[Drop off debug] Rescued!")
      self:_RescuePilots(_heliUnit)
      return
    end
  end

  --queue up
  if not noreschedule then
    self:__Returning(5,heliname,_woundedGroupName, isairport)
    self:ScheduleOnce(5,self._ScheduledSARFlight,self,heliname,groupname, isairport, noreschedule, IsHeloBase)
  end
  return self
end

--- (Internal) Mark pilot as rescued and remove from tables.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _heliUnit
function CSAR:_RescuePilots(_heliUnit)
  self:T(self.lid .. " _RescuePilots")
  local _heliName = _heliUnit:GetName()
  local _rescuedGroups = self.inTransitGroups[_heliName]

  if _rescuedGroups == nil then
    -- Groups already rescued
    return
  end

  local PilotsSaved = self:_PilotsOnboard(_heliName)

  self.inTransitGroups[_heliName] = nil

  local _txt = string.format("%s: The %d pilot(s) have been taken to the\nmedical clinic. Good job!", self:_GetCustomCallSign(_heliName), PilotsSaved)

  self:_DisplayMessageToSAR(_heliUnit, _txt, self.messageTime)

  self:_UpdateUnitCargoMass(_heliName)

  -- trigger event
  self:__Rescued(-1,_heliUnit,_heliName, PilotsSaved)
  return self
end

--- (Internal) Check and return Wrappe.Unit#UNIT based on the name if alive.
-- @param #CSAR self
-- @param #string _unitname Name of Unit
-- @return Wrapper.Unit#UNIT The unit or nil
function CSAR:_GetSARHeli(_unitName)
  self:T(self.lid .. " _GetSARHeli")
  local unit = UNIT:FindByName(_unitName)
  if unit and unit:IsAlive() then
    return unit
  else
    return nil
  end
end

--- (Internal) Display message to single Unit.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _unit Unit #UNIT to display to.
-- @param #string _text Text of message.
-- @param #number _time Message show duration.
-- @param #boolean _clear (optional) Clear screen.
-- @param #boolean _speak (optional) Speak message via SRS.
-- @param #boolean _override (optional) Override message suppression
function CSAR:_DisplayMessageToSAR(_unit, _text, _time, _clear, _speak, _override)
  self:T(self.lid .. " _DisplayMessageToSAR")
  local group = _unit:GetGroup()
  local _clear = _clear or nil
  local _time = _time or self.messageTime
  if _override or not self.suppressmessages then
    local m = MESSAGE:New(_text,_time,"CSAR",_clear):ToGroup(group)
  end
  -- integrate SRS
  if _speak and self.useSRS then
    local coord = _unit:GetCoordinate()
    if coord then
      self.msrs:SetCoordinate(coord)
    end
    _text = string.gsub(_text,"km"," kilometer")
    _text = string.gsub(_text,"nm"," nautical miles")
    self.SRSQueue:NewTransmission(_text,duration,self.msrs,tstart,2,subgroups,subtitle,subduration,self.SRSchannel,self.SRSModulation,gender,culture,self.SRSVoice,volume,label,coord)
  end
  return self
end

--- (Internal) Function to get string of a group\'s position.
-- @param #CSAR self
-- @param Wrapper.Controllable#CONTROLLABLE _woundedGroup Group or Unit object.
-- @param Wrapper.Unit#UNIT _Unit Requesting helo pilot unit
-- @return #string Coordinates as Text
function CSAR:_GetPositionOfWounded(_woundedGroup,_Unit)
  self:T(self.lid .. " _GetPositionOfWounded")
  local _coordinate = _woundedGroup:GetCoordinate()
  local _coordinatesText = "None"
  if _coordinate then
    if self.coordtype == 0 then -- Lat/Long DMTM
      _coordinatesText = _coordinate:ToStringLLDDM()
    elseif self.coordtype == 1 then -- Lat/Long DMS
      _coordinatesText = _coordinate:ToStringLLDMS()
    elseif self.coordtype == 2 then -- MGRS
      _coordinatesText = _coordinate:ToStringMGRS()
    else -- Bullseye Metric --(medevac.coordtype == 4 or 3)
      _coordinatesText = _coordinate:ToStringBULLS(self.coalition)
    end
  end
  if _Unit and _Unit:GetPlayerName() then
    local playername = _Unit:GetPlayerName()
    if playername then
      local settings = _DATABASE:GetPlayerSettings(playername) or _SETTINGS
      if settings then
        self:T("Get Settings ok!")
        if settings:IsA2G_MGRS() then
          _coordinatesText = _coordinate:ToStringMGRS(settings)
        elseif settings:IsA2G_LL_DMS() then
          _coordinatesText = _coordinate:ToStringLLDMS(settings)
        elseif settings:IsA2G_LL_DDM() then
          _coordinatesText = _coordinate:ToStringLLDDM(settings)
        elseif settings:IsA2G_BR() then
          -- attention this is the distance from the ASKING unit to target, not from RECCE to target!
          local startcoordinate = _Unit:GetCoordinate()
          _coordinatesText = _coordinate:ToStringBR(startcoordinate,settings)
        end
      end
    end
  end
  return _coordinatesText
end

--- (Internal) Display active SAR tasks to player.
-- @param #CSAR self
-- @param #string _unitName Unit to display to
function CSAR:_DisplayActiveSAR(_unitName)
  self:T(self.lid .. " _DisplayActiveSAR")
  local _msg = "Active MEDEVAC/SAR:"
  local _heli = self:_GetSARHeli(_unitName) -- Wrapper.Unit#UNIT
  if _heli == nil then
    return
  end

  local _heliSide = self.coalition
  local _csarList = {}

  local _DownedPilotTable = self.downedPilots
  self:T({Table=_DownedPilotTable})
  for _, _value in pairs(_DownedPilotTable) do
    local _groupName = _value.name
    self:T(string.format("Display Active Pilot: %s", tostring(_groupName)))
    self:T({Table=_value})
    local _woundedGroup = _value.group
    if _woundedGroup and _value.alive then
      local _coordinatesText = self:_GetPositionOfWounded(_woundedGroup,_heli)
      local _helicoord =  _heli:GetCoordinate()
      local _woundcoord = _woundedGroup:GetCoordinate()
      local _distance = self:_GetDistance(_helicoord, _woundcoord)
      self:T({_distance = _distance})
      local distancetext = ""
      local settings = _SETTINGS
      if _heli:GetPlayerName() then
        settings = _DATABASE:GetPlayerSettings(_heli:GetPlayerName()) or _SETTINGS
      end
      if settings:IsImperial() then
        distancetext = string.format("%.1fnm",UTILS.MetersToNM(_distance))
      else
        distancetext = string.format("%.1fkm", _distance/1000.0)
      end
      if _value.frequency == 0 or self.CreateRadioBeacons == false then--shagrat insert CASEVAC without Frequency
        table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %s ", _value.desc, _coordinatesText, distancetext) })
      else
        table.insert(_csarList, { dist = _distance, msg = string.format("%s at %s - %.2f KHz ADF - %s ", _value.desc, _coordinatesText, _value.frequency / 1000, distancetext) })
      end
    end
  end

  local function sortDistance(a, b)
    return a.dist < b.dist
  end

  table.sort(_csarList, sortDistance)

  for _, _line in pairs(_csarList) do
    _msg = _msg .. "\n" .. _line.msg
  end

  self:_DisplayMessageToSAR(_heli, _msg, self.messageTime*2, false, false, true)
  return self
end

--- (Internal) Find the closest downed pilot to a heli.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT
-- @return #table Table of results
function CSAR:_GetClosestDownedPilot(_heli)
  self:T(self.lid .. " _GetClosestDownedPilot")
  local _side = self.coalition
  local _closestGroup = nil
  local _shortestDistance = -1
  local _distance = 0
  local _closestGroupInfo = nil
  local _heliCoord = _heli:GetCoordinate() or _heli:GetCoordinate()

  if _heliCoord == nil then
    self:E("****Error obtaining coordinate!")
    return nil
  end

  local DownedPilotsTable = self.downedPilots

  for _, _groupInfo in UTILS.spairs(DownedPilotsTable) do
    --for _, _groupInfo in pairs(DownedPilotsTable) do
    local _woundedName = _groupInfo.name
    local _tempWounded = _groupInfo.group

    -- check group exists and not moving to someone else
    if _tempWounded then
      local _tempCoord = _tempWounded:GetCoordinate()
      _distance = self:_GetDistance(_heliCoord, _tempCoord)

      if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then
        _shortestDistance = _distance
        _closestGroup = _tempWounded
        _closestGroupInfo = _groupInfo
      end
    end
  end

  return { pilot = _closestGroup, distance = _shortestDistance, groupInfo = _closestGroupInfo }
end

--- (Internal) Fire a flare at the point of a downed pilot.
-- @param #CSAR self
-- @param #string _unitName Name of the unit.
function CSAR:_SignalFlare(_unitName)
  self:T(self.lid .. " _SignalFlare")
  local _heli = self:_GetSARHeli(_unitName)
  if _heli == nil then
    return
  end

  local _closest = self:_GetClosestDownedPilot(_heli)
  local smokedist = 8000
  if self.approachdist_far > smokedist then smokedist = self.approachdist_far end
  if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then

    local _clockDir = self:_GetClockDirection(_heli, _closest.pilot)
    local _distance = ""
    if _SETTINGS:IsImperial() then
      _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance))
    else
      _distance = string.format("%.1fkm",_closest.distance/1000)
    end
    local _msg = string.format("%s - Firing signal flare at your %s o\'clock. Distance %s", self:_GetCustomCallSign(_unitName), _clockDir, _distance)
    self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true)

    local _coord = _closest.pilot:GetCoordinate()
    _coord:FlareRed(_clockDir)
  else
    local _distance = smokedist
    local dtext = ""
    if _SETTINGS:IsImperial() then
      dtext = string.format("%.1fnm",UTILS.MetersToNM(smokedist))
    else
      dtext = string.format("%.1fkm",smokedist/1000)
    end
    self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",dtext), self.messageTime, false, false, true)
  end
  return self
end

--- (Internal) Display info to all SAR groups.
-- @param #CSAR self
-- @param #string _message Message to display.
-- @param #number _side Coalition of message.
-- @param #number _messagetime How long to show.
-- @param #boolean ToSRS If true or nil, send to SRS TTS
-- @param #boolean ToScreen If true or nil, send to Screen
function CSAR:_DisplayToAllSAR(_message, _side, _messagetime,ToSRS,ToScreen)
  self:T(self.lid .. " _DisplayToAllSAR")
  local messagetime = _messagetime or self.messageTime
  self:T({_message,ToSRS=ToSRS,ToScreen=ToScreen})
  if self.msrs and (ToSRS == true or ToSRS == nil) then
    local voice = self.CSARVoice or MSRS.Voices.Google.Standard.en_GB_Standard_F
    if self.msrs:GetProvider() == MSRS.Provider.WINDOWS then
      voice = self.CSARVoiceMS or MSRS.Voices.Microsoft.Hedda
    end
    --self:F("Voice = "..voice)
    self.SRSQueue:NewTransmission(_message,duration,self.msrs,tstart,2,subgroups,subtitle,subduration,self.SRSchannel,self.SRSModulation,gender,culture,voice,volume,label,self.coordinate)
  end
  if ToScreen == true or ToScreen == nil then
    for _, _unitName in pairs(self.csarUnits) do
      local _unit = self:_GetSARHeli(_unitName)
      if _unit and not self.suppressmessages then
        self:_DisplayMessageToSAR(_unit, _message, _messagetime)
      end
    end
  end
  return self
end

---(Internal) Request IR Strobe at closest downed pilot.
--@param #CSAR self
--@param #string _unitName Name of the helicopter
function CSAR:_ReqIRStrobe( _unitName )
  self:T(self.lid .. " _ReqIRStrobe")
  local _heli = self:_GetSARHeli(_unitName)
  if _heli == nil then
    return
  end
  local smokedist = 8000
  if smokedist < self.approachdist_far then smokedist = self.approachdist_far end
  local _closest = self:_GetClosestDownedPilot(_heli)
  if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then
    local _clockDir = self:_GetClockDirection(_heli, _closest.pilot)
    local _distance = string.format("%.1fkm",_closest.distance/1000)
    if _SETTINGS:IsImperial() then
      _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance))
    else
      _distance = string.format("%.1fkm",_closest.distance/1000)
    end
    local _msg = string.format("%s - IR Strobe active at your %s o\'clock. Distance %s", self:_GetCustomCallSign(_unitName), _clockDir, _distance)
    self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true)
    _closest.pilot:NewIRMarker(true,self.IRStrobeRuntime or 300)
  else
    local _distance = string.format("%.1fkm",smokedist/1000)
    if _SETTINGS:IsImperial() then
      _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist))
    else
      _distance = string.format("%.1fkm",smokedist/1000)
    end
    self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true)
  end
  return self
end

---(Internal) Request smoke at closest downed pilot.
--@param #CSAR self
--@param #string _unitName Name of the helicopter
function CSAR:_Reqsmoke( _unitName )
  self:T(self.lid .. " _Reqsmoke")
  local _heli = self:_GetSARHeli(_unitName)
  if _heli == nil then
    return
  end
  local smokedist = 8000
  if smokedist < self.approachdist_far then smokedist = self.approachdist_far end
  local _closest = self:_GetClosestDownedPilot(_heli)
  if _closest ~= nil and _closest.pilot ~= nil and _closest.distance > 0 and _closest.distance < smokedist then
    local _clockDir = self:_GetClockDirection(_heli, _closest.pilot)
    local _distance = string.format("%.1fkm",_closest.distance/1000)
    if _SETTINGS:IsImperial() then
      _distance = string.format("%.1fnm",UTILS.MetersToNM(_closest.distance))
    else
      _distance = string.format("%.1fkm",_closest.distance/1000)
    end
    local _msg = string.format("%s - Popping smoke at your %s o\'clock. Distance %s", self:_GetCustomCallSign(_unitName), _clockDir, _distance)
    self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true)
    local _coord = _closest.pilot:GetCoordinate()
    local color = self.smokecolor
    _coord:Smoke(color)
  else
    local _distance = string.format("%.1fkm",smokedist/1000)
    if _SETTINGS:IsImperial() then
      _distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist))
    else
      _distance = string.format("%.1fkm",smokedist/1000)
    end
    self:_DisplayMessageToSAR(_heli, string.format("No Pilots within %s",_distance), self.messageTime, false, false, true)
  end
  return self
end

--- (Internal) Determine distance to closest MASH.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT
-- @return #number Distance in meters
-- @return #string MASH Name as string
function CSAR:_GetClosestMASH(_heli)
  self:T(self.lid .. " _GetClosestMASH")
  local _mashset = self.mash -- Core.Set#SET_GROUP
  local MashSets = {}
  --local _mashes = _mashset.Set-- #table
  table.insert(MashSets,_mashset.Set)
  table.insert(MashSets,self.zonemashes.Set)
  table.insert(MashSets,self.staticmashes.Set)
  local _shortestDistance = -1
  local _distance = 0
  local _helicoord = _heli:GetCoordinate()
  local MashName = nil

  if self.allowFARPRescue then
    local position = _heli:GetCoordinate()
    local afb,distance = position:GetClosestAirbase(nil,self.coalition)
    _shortestDistance = distance
    MashName = (afb ~= nil) and afb:GetName() or "Unknown"
  end

  for _,_mashes in pairs(MashSets)  do
    for _, _mashUnit in pairs(_mashes or {}) do
      local _mashcoord
      if _mashUnit and (not _mashUnit:IsInstanceOf("ZONE_BASE")) and _mashUnit:IsAlive() then
        _mashcoord = _mashUnit:GetCoordinate()
      elseif _mashUnit and _mashUnit:IsInstanceOf("ZONE_BASE") then
        _mashcoord = _mashUnit:GetCoordinate()
      end
      _distance = self:_GetDistance(_helicoord, _mashcoord)
      if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then
        _shortestDistance = _distance
        MashName = _mashUnit:GetName() or "Unknown"
      end
    end
  end

  if _shortestDistance ~= -1 then
    return _shortestDistance, MashName
  else
    return -1
  end

end

--- (Internal) Display onboarded rescued pilots.
-- @param #CSAR self
-- @param #string _unitName Name of the chopper
function CSAR:_CheckOnboard(_unitName)
  self:T(self.lid .. " _CheckOnboard")
  local _unit = self:_GetSARHeli(_unitName)
  if _unit == nil then
    return
  end
  --list onboard pilots
  local _inTransit = self.inTransitGroups[_unitName]
  if _inTransit == nil then
    self:_DisplayMessageToSAR(_unit, "No Rescued Pilots onboard", self.messageTime, false, false, true)
  else
    local _text = "Onboard - RTB to FARP/Airfield or MASH: "
    for _, _onboard in pairs(self.inTransitGroups[_unitName]) do
      _text = _text .. "\n" .. _onboard.desc
    end
    self:_DisplayMessageToSAR(_unit, _text, self.messageTime*2, false, false, true)
  end
  return self
end

--- (Internal) Populate F10 menu for CSAR players.
-- @param #CSAR self
function CSAR:_AddMedevacMenuItem()
  self:T(self.lid .. " _AddMedevacMenuItem")

  local coalition = self.coalition
  local allheligroupset = self.allheligroupset -- Core.Set#SET_GROUP
  local _allHeliGroups = allheligroupset:GetSetObjects()
  -- rebuild units table
  local _UnitList = {}
  for _key, _group in pairs (_allHeliGroups) do
    local _unit = _group:GetFirstUnitAlive() -- Asume that there is only one unit in the flight for players
    if _unit then
      --self:T("Unitname ".._unit:GetName().." IsAlive "..tostring(_unit:IsAlive()).." IsPlayer "..tostring(_unit:IsPlayer()))
      if _unit:IsAlive() and _unit:IsPlayer() then
        local unitName = _unit:GetName()
        _UnitList[unitName] = unitName
      end -- end isAlive
    end -- end if _unit
  end -- end for
  self.csarUnits = _UnitList

  -- build unit menus
  for _, _unitName in pairs(self.csarUnits) do
    local _unit = self:_GetSARHeli(_unitName) -- Wrapper.Unit#UNIT
    if _unit then
      local _group = _unit:GetGroup() -- Wrapper.Group#GROUP
      if _group then
        local groupname = _group:GetName()
        if self.addedTo[groupname] == nil then
          self.addedTo[groupname] = true
          local menuname = self.topmenuname or "CSAR"
          local _rootPath = MENU_GROUP:New(_group,menuname)
          local _rootMenu1 = MENU_GROUP_COMMAND:New(_group,"List Active CSAR",_rootPath, self._DisplayActiveSAR,self,_unitName)
          local _rootMenu2 = MENU_GROUP_COMMAND:New(_group,"Check Onboard",_rootPath, self._CheckOnboard,self,_unitName)
          local _rootMenu3 = MENU_GROUP_COMMAND:New(_group,"Request Signal Flare",_rootPath, self._SignalFlare,self,_unitName)
          local _rootMenu4 = MENU_GROUP_COMMAND:New(_group,"Request Smoke",_rootPath, self._Reqsmoke,self,_unitName)
          if self.AllowIRStrobe then
            local _rootMenu5 = MENU_GROUP_COMMAND:New(_group,"Request IR Strobe",_rootPath, self._ReqIRStrobe,self,_unitName):Refresh()
          else
            _rootMenu4:Refresh()
          end
        end
      end
    end
  end
  return self
end

--- (Internal) Return distance in meters between two coordinates.
-- @param #CSAR self
-- @param Core.Point#COORDINATE _point1 Coordinate one
-- @param Core.Point#COORDINATE _point2 Coordinate two
-- @return #number Distance in meters
function CSAR:_GetDistance(_point1, _point2)
  self:T(self.lid .. " _GetDistance")
  if _point1 and _point2 then
    local distance1 = _point1:Get2DDistance(_point2)
    local distance2 = _point1:DistanceFromPointVec2(_point2)
    if distance1 and type(distance1) == "number" then
      return distance1
    elseif distance2 and type(distance2) == "number" then
      return distance2
    else
      self:E("*****Cannot calculate distance!")
      self:E({_point1,_point2})
      return -1
    end
  else
    self:E("******Cannot calculate distance!")
    self:E({_point1,_point2})
    return -1
  end
end

--- (Internal) Populate table with available beacon frequencies.
-- @param #CSAR self
function CSAR:_GenerateVHFrequencies()
  self:T(self.lid .. " _GenerateVHFrequencies")

  local FreeVHFFrequencies = {}
  FreeVHFFrequencies = UTILS.GenerateVHFrequencies()
  self.FreeVHFFrequencies = FreeVHFFrequencies
  return self
end

--- (Internal) Pop frequency from prepopulated table.
-- @param #CSAR self
-- @return #number frequency
function CSAR:_GenerateADFFrequency()
  self:T(self.lid .. " _GenerateADFFrequency")
  -- get a free freq for a beacon
  if #self.FreeVHFFrequencies <= 3 then
    self.FreeVHFFrequencies = self.UsedVHFFrequencies
    self.UsedVHFFrequencies = {}
  end
  local _vhf = table.remove(self.FreeVHFFrequencies, math.random(#self.FreeVHFFrequencies))
  return _vhf
end

--- (Internal) Function to determine clockwise direction for flares.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _heli The Helicopter
-- @param Wrapper.Group#GROUP _group The downed Group
-- @return #number direction
function CSAR:_GetClockDirection(_heli, _group)
  self:T(self.lid .. " _GetClockDirection")

  local _playerPosition = _heli:GetCoordinate() -- get position of helicopter
  local _targetpostions = _group:GetCoordinate() -- get position of downed pilot
  local _heading = _heli:GetHeading() -- heading
  local DirectionVec3 = _playerPosition:GetDirectionVec3( _targetpostions )
  local Angle = _playerPosition:GetAngleDegrees( DirectionVec3 )
  self:T(self.lid .. " _GetClockDirection"..tostring(Angle).." "..tostring(_heading))
  local hours = 0
  local clock = 12
  if _heading and Angle then
    clock = 12
    --if angle == 0 then angle = 360 end
    clock = _heading-Angle
    hours = (clock/30)*-1
    clock = 12+hours
    clock = UTILS.Round(clock,0)
    if clock > 12 then clock = clock-12 end
  end
  return clock
end

--- (Internal) Function to add beacon to downed pilot.
-- @param #CSAR self
-- @param Wrapper.Group#GROUP _group Group #GROUP object.
-- @param #number _freq Frequency to use
-- @param #string BeaconName Beacon Name to use
-- @return #CSAR self
function CSAR:_AddBeaconToGroup(_group, _freq, BeaconName)
  self:T(self.lid .. " _AddBeaconToGroup")
  if self.CreateRadioBeacons == false then return end
  local _group = _group

  if _group == nil then
    --return frequency to pool of available
    for _i, _current in ipairs(self.UsedVHFFrequencies) do
      if _current == _freq then
        table.insert(self.FreeVHFFrequencies, _freq)
        table.remove(self.UsedVHFFrequencies, _i)
      end
    end
    return
  end

  if _group:IsAlive() then
    local _radioUnit = _group:GetUnit(1)
    if _radioUnit then
      local name = _radioUnit:GetName()
      local Frequency = _freq -- Freq in Hertz
      --local name = _radioUnit:GetName()
      local Sound =  "l10n/DEFAULT/"..self.radioSound
      local vec3 = _radioUnit:GetVec3() or _radioUnit:GetPositionVec3() or {x=0,y=0,z=0}
      self:I(self.lid..string.format("Added Radio Beacon %d Hertz | Name %s | Position {%d,%d,%d}",Frequency,BeaconName,vec3.x,vec3.y,vec3.z))
      trigger.action.radioTransmission(Sound, vec3, 0, true, Frequency, self.ADFRadioPwr or 500,BeaconName) -- Beacon in MP only runs for exactly 30secs straight
    end
  end

  return self
end

--- (Internal) Helper function to (re-)add beacon to downed pilot.
-- @param #CSAR self
-- @return #CSAR self
function CSAR:_RefreshRadioBeacons()
  self:T(self.lid .. " _RefreshRadioBeacons")
  if self.CreateRadioBeacons == false then return end
  if self:_CountActiveDownedPilots() > 0 then
    local PilotTable = self.downedPilots
    for _,_pilot in pairs (PilotTable) do
      self:T({_pilot.name})
      local pilot = _pilot -- #CSAR.DownedPilot
      local group = pilot.group
      local frequency = pilot.frequency or 0 -- thanks to @Thrud
      local bname = pilot.BeaconName or pilot.name..math.random(1,100000)
      --trigger.action.stopRadioTransmission(bname)
      if group and group:IsAlive() and frequency > 0 then
      --self:_AddBeaconToGroup(group,frequency,bname)
      else
        if frequency > 0 then
          trigger.action.stopRadioTransmission(bname)
        end
      end
    end
  end
  return self
end

--- (Internal) Helper function to count active downed pilots.
-- @param #CSAR self
-- @return #number Number of pilots in the field.
function CSAR:_CountActiveDownedPilots()
  self:T(self.lid .. " _CountActiveDownedPilots")
  local PilotsInFieldN = 0
  for _, _unitName in pairs(self.downedPilots) do
    self:T({_unitName.desc})
    if _unitName.alive == true then
      PilotsInFieldN = PilotsInFieldN + 1
    end
  end
  return PilotsInFieldN
end

--- (Internal) Helper to decide if we're over max limit.
-- @param #CSAR self
-- @return #boolean True or false.
function CSAR:_ReachedPilotLimit()
  self:T(self.lid .. " _ReachedPilotLimit")
  local limit = self.maxdownedpilots
  local islimited = self.limitmaxdownedpilots
  local count = self:_CountActiveDownedPilots()
  if islimited and (count >= limit) then
    if self.useFIFOLimitReplacement then
      local oldIndex  = -1
      local oldDownedPilot = nil
      for _index, _downedpilot in pairs(self.downedPilots) do
        oldIndex = _index
        oldDownedPilot = _downedpilot
        break
      end
      if oldDownedPilot then
        oldDownedPilot.group:Destroy(false)
        oldDownedPilot.alive = false
        self:_CheckDownedPilotTable()
        return false
      end
    end
    return true
  else
    return false
  end
end

--- User - Function to add onw SET_GROUP Set-up for pilot filtering and assignment.
-- Needs to be set before starting the CSAR instance.
-- @param #CSAR self
-- @param Core.Set#SET_GROUP Set The SET_GROUP object created by the mission designer/user to represent the CSAR pilot groups.
-- @return #CSAR self
function CSAR:SetOwnSetPilotGroups(Set)
  self.UserSetGroup = Set
  return self
end

------------------------------
--- FSM internal Functions ---
------------------------------

--- (Internal) Function called after Start() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
function CSAR:onafterStart(From, Event, To)
  self:T({From, Event, To})
  self:I(self.lid .. "Started ("..self.version..")")
  -- event handler
  self:HandleEvent(EVENTS.Takeoff, self._EventHandler)
  self:HandleEvent(EVENTS.Land, self._EventHandler)
  self:HandleEvent(EVENTS.Ejection, self._EventHandler)
  self:HandleEvent(EVENTS.LandingAfterEjection, self._EventHandler) --shagrat
  self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler)
  self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler)
  self:HandleEvent(EVENTS.PilotDead, self._EventHandler)

  if self.UserSetGroup then
    self.allheligroupset  = self.UserSetGroup
  elseif self.allowbronco then
    local prefixes = self.csarPrefix or {}
    self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefixes):FilterStart()
  elseif self.useprefix then
    local prefixes = self.csarPrefix or {}
    self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(prefixes):FilterCategoryHelicopter():FilterStart()
  else
    self.allheligroupset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryHelicopter():FilterStart()
  end

  self.mash = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart()

  self.staticmashes = SET_STATIC:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart()
  self.zonemashes = SET_ZONE:New():FilterPrefixes(self.mashprefix):FilterStart()

  --[[
  if staticmashes:Count() > 0  then
    for _,_mash in pairs(staticmashes.Set) do
      self.mash:AddObject(_mash)
    end
  end
  
  if zonemashes:Count() > 0  then
    self:T("Adding zones to self.mash SET")
    for _,_mash in pairs(zonemashes.Set) do
      self.mash:AddObject(_mash)
    end
    self:T("Objects in SET: "..self.mash:Count())
  end
  --]]

  if not self.coordinate then
    local csarhq = self.mash:GetRandom()
    if csarhq then
      self.coordinate = csarhq:GetCoordinate()
    end
  end
  if self.wetfeettemplate then
    self.usewetfeet = true
  end
  if self.useSRS then
    local path = self.SRSPath
    local modulation = self.SRSModulation
    local channel = self.SRSchannel
    self.msrs = MSRS:New(path,channel,modulation) -- Sound.SRS#MSRS
    self.msrs:SetPort(self.SRSport)
    self.msrs:SetLabel("CSAR")
    self.msrs:SetCulture(self.SRSCulture)
    self.msrs:SetCoalition(self.coalition)
    self.msrs:SetVoice(self.SRSVoice)
    self.msrs:SetGender(self.SRSGender)
    if self.SRSGPathToCredentials then
      self.msrs:SetProviderOptionsGoogle(self.SRSGPathToCredentials,self.SRSGPathToCredentials)
      self.msrs:SetProvider(MSRS.Provider.GOOGLE)
    end
    self.msrs:SetVolume(self.SRSVolume)
    self.msrs:SetLabel("CSAR")
    self.SRSQueue = MSRSQUEUE:New("CSAR") -- Sound.SRS#MSRSQUEUE
  end

  self:__Status(-10)

  if self.enableLoadSave then
    local interval = self.saveinterval
    local filename = self.filename
    local filepath = self.filepath
    self:__Save(interval,filepath,filename)
  end

  return self
end

--- (Internal) Function called before Status() event.
-- @param #CSAR self
function CSAR:_CheckDownedPilotTable()
  local pilots = self.downedPilots
  local npilots = {}

  for _ind,_entry in pairs(pilots) do
    local _group = _entry.group
    if _group:IsAlive() then
      npilots[_ind] = _entry
    else
      if _entry.alive then
        self:__KIA(1,_entry.desc)
      end
    end
  end
  self.downedPilots = npilots
  return self
end

--- (Internal) Function called before Status() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
function CSAR:onbeforeStatus(From, Event, To)
  self:T({From, Event, To})
  -- housekeeping
  self:_AddMedevacMenuItem()

  if not self.BeaconTimer or (self.BeaconTimer and not self.BeaconTimer:IsRunning()) then
    self.BeaconTimer = TIMER:New(self._RefreshRadioBeacons,self)
    self.BeaconTimer:Start(2,self.beaconRefresher)
  end

  self:_CheckDownedPilotTable()
  for _,_sar in pairs (self.csarUnits) do
    local PilotTable = self.downedPilots
    for _,_entry in pairs (PilotTable) do
      if _entry.alive then
        local entry = _entry -- #CSAR.DownedPilot
        local name = entry.name
        local timestamp = entry.timestamp or 0
        local now = timer.getAbsTime()
        if now - timestamp > 17 then -- only check if we\'re not in approach mode, which is iterations of 5 and 10.
          self:_CheckWoundedGroupStatus(_sar,name)
        end
      end
    end
  end
  return self
end

--- (Internal) Function called after Status() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
function CSAR:onafterStatus(From, Event, To)
  self:T({From, Event, To})
  -- collect some stats
  local NumberOfSARPilots = 0
  for _, _unitName in pairs(self.csarUnits) do
    NumberOfSARPilots = NumberOfSARPilots + 1
  end

  local PilotsInFieldN = self:_CountActiveDownedPilots()

  local PilotsBoarded = 0
  for _, _unitName in pairs(self.inTransitGroups) do
    for _,_units in pairs(_unitName) do
      PilotsBoarded = PilotsBoarded + 1
    end
  end

  if self.verbose > 0 then
    local text = string.format("%s Active SAR: %d | Downed Pilots in field: %d (max %d) | Pilots boarded: %d | Landings: %d | Pilots rescued: %d",
      self.lid,NumberOfSARPilots,PilotsInFieldN,self.maxdownedpilots,PilotsBoarded,self.rescues,self.rescuedpilots)
    self:T(text)
    if self.verbose < 2 then
      self:I(text)
    elseif self.verbose > 1 then
      self:I(text)
      local m = MESSAGE:New(text,"10","Status",true):ToCoalition(self.coalition)
    end
  end
  self:__Status(-20)
  return self
end

--- (Internal) Function called after Stop() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
function CSAR:onafterStop(From, Event, To)
  self:T({From, Event, To})
  -- event handler
  self:UnHandleEvent(EVENTS.Takeoff)
  self:UnHandleEvent(EVENTS.Land)
  self:UnHandleEvent(EVENTS.Ejection)
  self:UnHandleEvent(EVENTS.LandingAfterEjection) -- shagrat
  self:UnHandleEvent(EVENTS.PlayerEnterUnit)
  self:UnHandleEvent(EVENTS.PlayerEnterAircraft)
  self:UnHandleEvent(EVENTS.PilotDead)
  self:T(self.lid .. "Stopped.")
  return self
end

--- (Internal) Function called before Approach() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
-- @param #string Heliname Name of the helicopter group.
-- @param #string Woundedgroupname Name of the downed pilot\'s group.
function CSAR:onbeforeApproach(From, Event, To, Heliname, Woundedgroupname)
  self:T({From, Event, To, Heliname, Woundedgroupname})
  self:_CheckWoundedGroupStatus(Heliname,Woundedgroupname)
  return self
end

--- (Internal) Function called before Boarded() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
-- @param #string Heliname Name of the helicopter group.
-- @param #string Woundedgroupname Name of the downed pilot\'s group.
function CSAR:onbeforeBoarded(From, Event, To, Heliname, Woundedgroupname)
  self:T({From, Event, To, Heliname, Woundedgroupname})
  self:_ScheduledSARFlight(Heliname,Woundedgroupname)
  local Unit = UNIT:FindByName(Heliname)
  if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then
    local playername = Unit:GetPlayerName()
    local dropcoord = Unit:GetCoordinate() or COORDINATE:New(0,0,0)
    local dropvec2 = dropcoord:GetVec2()
    self.PlayerTaskQueue:ForEach(
      function (Task)
        local task = Task -- Ops.PlayerTask#PLAYERTASK
        local subtype = task:GetSubType()
        -- right subtype?
        if Event == subtype and not task:IsDone() then
          local targetzone = task.Target:GetObject() -- Core.Zone#ZONE should be a zone in this case ....
          if (targetzone and targetzone.ClassName and string.match(targetzone.ClassName,"ZONE") and targetzone:IsVec2InZone(dropvec2))
            or (string.find(task.CSARPilotName,Woundedgroupname)) then
            if task.Clients:HasUniqueID(playername) then
              -- success
              task:__Success(-1)
            end
          end
        end
      end
    )
  end
  return self
end

--- (Internal) Function called before Returning() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
-- @param #string Heliname Name of the helicopter group.
-- @param #string Woundedgroupname Name of the downed pilot\'s group.
-- @param #boolean IsAirport True if heli has landed on an AFB (from event land).
function CSAR:onbeforeReturning(From, Event, To, Heliname, Woundedgroupname, IsAirPort)
  self:T({From, Event, To, Heliname, Woundedgroupname})
  --self:_ScheduledSARFlight(Heliname,Woundedgroupname, IsAirPort)
  return self
end

--- (Internal) Function called before Rescued() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
-- @param Wrapper.Unit#UNIT HeliUnit Unit of the helicopter.
-- @param #string HeliName Name of the helicopter group.
-- @param #number PilotsSaved Number of the saved pilots on board when landing.
function CSAR:onbeforeRescued(From, Event, To, HeliUnit, HeliName, PilotsSaved)
  self:T({From, Event, To, HeliName, HeliUnit})
  self.rescues = self.rescues + 1
  self.rescuedpilots = self.rescuedpilots + PilotsSaved
  local Unit = HeliUnit or UNIT:FindByName(HeliName)
  if Unit and Unit:IsPlayer() and self.PlayerTaskQueue then
    local playername = Unit:GetPlayerName()
    self.PlayerTaskQueue:ForEach(
      function (Task)
        local task = Task -- Ops.PlayerTask#PLAYERTASK
        local subtype = task:GetSubType()
        -- right subtype?
        if Event == subtype and not task:IsDone() then
          if task.Clients:HasUniqueID(playername) then
            -- success
            task:__Success(-1)
          end
        end
      end
    )
  end
  return self
end

--- (Internal) Function called before PilotDown() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
-- @param Wrapper.Group#GROUP Group Group object of the downed pilot.
-- @param #number Frequency Beacon frequency in kHz.
-- @param #string Leadername Name of the #UNIT of the downed pilot.
-- @param #string CoordinatesText String of the position of the pilot. Format determined by self.coordtype.
-- @param #string Playername Player name if any given. Might be nil!
function CSAR:onbeforePilotDown(From, Event, To, Group, Frequency, Leadername, CoordinatesText, Playername)
  self:T({From, Event, To, Group, Frequency, Leadername, CoordinatesText, tostring(Playername)})
  return self
end

--- (Internal) Function called before Landed() event.
-- @param #CSAR self.
-- @param #string From From state.
-- @param #string Event Event triggered.
-- @param #string To To state.
-- @param #string HeliName Name of the #UNIT which has landed.
-- @param Wrapper.Airbase#AIRBASE Airbase Airbase where the heli landed.
function CSAR:onbeforeLanded(From, Event, To, HeliName, Airbase)
  self:T({From, Event, To, HeliName, Airbase})
  return self
end

--- On before "Save" event. Checks if io and lfs are available.
-- @param #CSAR self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string path (Optional) Path where the file is saved. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
-- @param #string filename (Optional) File name for saving. Default is "CSAR_<alias>_Persist.csv".
function CSAR:onbeforeSave(From, Event, To, path, filename)
  self:T({From, Event, To, path, filename})
  if not self.enableLoadSave then
    return self
  end
  -- Thanks to @FunkyFranky
  -- Check io module is available.
  if not io then
    self:E(self.lid.."ERROR: io not desanitized. Can't save current state.")
    return false
  end

  -- Check default path.
  if path==nil and not lfs then
    self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
  end

  return true
end

--- On after "Save" event. Player data is saved to file.
-- @param #CSAR self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string path Path where the file is saved. If nil, file is saved in the DCS root installtion directory or your "Saved Games" folder if lfs was desanitized.
-- @param #string filename (Optional) File name for saving. Default is Default is "CSAR_<alias>_Persist.csv".
function CSAR:onafterSave(From, Event, To, path, filename)
  self:T({From, Event, To, path, filename})
  -- Thanks to @FunkyFranky
  if not self.enableLoadSave then
    return self
  end
  --- Function that saves data to file
  local function _savefile(filename, data)
    local f = assert(io.open(filename, "wb"))
    f:write(data)
    f:close()
  end

  -- Set path or default.
  if lfs then
    path=self.filepath or lfs.writedir()
  end

  -- Set file name.
  filename=filename or self.filename

  -- Set path.
  if path~=nil then
    filename=path.."\\"..filename
  end

  local pilots = self.downedPilots

  --local data = "LoadedData = {\n"
  local data = "playerName,x,y,z,coalition,country,description,typeName,unitName,freq\n"
  local n = 0
  for _,_grp in pairs(pilots) do
    local DownedPilot = _grp -- Wrapper.Group#GROUP
    if DownedPilot and DownedPilot.alive then
      -- get downed pilot data for saving
      local playerName = DownedPilot.player
      local group = DownedPilot.group
      local coalition = group:GetCoalition()
      local country = group:GetCountry()
      local description = DownedPilot.desc
      local typeName = DownedPilot.typename
      local freq = DownedPilot.frequency
      local location = group:GetVec3()
      local unitName = DownedPilot.originalUnit
      local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%s,%s,%d\n",playerName,location.x,location.y,location.z,coalition,country,description,typeName,unitName,freq)

      self:I(self.lid.."Saving to CSAR File: " .. txt)

      data = data .. txt
    end
  end

  _savefile(filename, data)

  -- AutoSave
  if self.enableLoadSave then
    local interval = self.saveinterval
    local filename = self.filename
    local filepath = self.filepath
    self:__Save(interval,filepath,filename)
  end
  return self
end

--- On before "Load" event. Checks if io and lfs and the file are available.
-- @param #CSAR self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
-- @param #string filename (Optional) File name for loading. Default is "CSAR_<alias>_Persist.csv".
function CSAR:onbeforeLoad(From, Event, To, path, filename)
  self:T({From, Event, To, path, filename})
  if not self.enableLoadSave then
    return self
  end
  --- Function that check if a file exists.
  local function _fileexists(name)
    local f=io.open(name,"r")
    if f~=nil then
      io.close(f)
      return true
    else
      return false
    end
  end

  -- Set file name and path
  filename=filename or self.filename
  path = path or self.filepath

  -- Check io module is available.
  if not io then
    self:E(self.lid.."WARNING: io not desanitized. Cannot load file.")
    return false
  end

  -- Check default path.
  if path==nil and not lfs then
    self:E(self.lid.."WARNING: lfs not desanitized. State will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
  end

  -- Set path or default.
  if lfs then
    path=path or lfs.writedir()
  end

  -- Set path.
  if path~=nil then
    filename=path.."\\"..filename
  end

  -- Check if file exists.
  local exists=_fileexists(filename)

  if exists then
    return true
  else
    self:E(self.lid..string.format("WARNING: State file %s might not exist.", filename))
    return false
      --return self
  end

end

--- On after "Load" event. Loads dropped units from file.
-- @param #CSAR self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string path (Optional) Path where the file is located. Default is the DCS root installation folder or your "Saved Games\\DCS" folder if the lfs module is desanitized.
-- @param #string filename (Optional) File name for loading. Default is "CSAR_<alias>_Persist.csv".
function CSAR:onafterLoad(From, Event, To, path, filename)
  self:T({From, Event, To, path, filename})
  if not self.enableLoadSave then
    return self
  end
  --- Function that loads data from a file.
  local function _loadfile(filename)
    local f=assert(io.open(filename, "rb"))
    local data=f:read("*all")
    f:close()
    return data
  end

  -- Set file name and path
  filename=filename or self.filename
  path = path or self.filepath

  -- Set path or default.
  if lfs then
    path=path or lfs.writedir()
  end

  -- Set path.
  if path~=nil then
    filename=path.."\\"..filename
  end

  -- Info message.
  local text=string.format("Loading CSAR state from file %s", filename)
  MESSAGE:New(text,10):ToAllIf(self.Debug)
  self:I(self.lid..text)

  local file=assert(io.open(filename, "rb"))

  local loadeddata = {}
  for line in file:lines() do
    loadeddata[#loadeddata+1] = line
  end
  file:close()

  -- remove header
  table.remove(loadeddata, 1)

  for _id,_entry in pairs (loadeddata) do
    local dataset = UTILS.Split(_entry,",")
    -- 1=playerName,2=x,3=y,4=z,5=coalition,6=country,7=description,8=typeName,9=unitName,10=freq\n
    local playerName = dataset[1]

    local vec3 = {}
    vec3.x = tonumber(dataset[2])
    vec3.y = tonumber(dataset[3])
    vec3.z = tonumber(dataset[4])
    local point = COORDINATE:NewFromVec3(vec3)

    local coalition = tonumber(dataset[5])
    local country = tonumber(dataset[6])
    local description = dataset[7]
    local typeName = dataset[8]
    local unitName = dataset[9]
    local freq = tonumber(dataset[10])
    
    self:_AddCsar(coalition, country, point, typeName, unitName, playerName, freq, false, description, nil)    
  end

  return self
end

--------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- End Ops.CSAR
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Airwing Warehouse.
--
-- **Main Features:**
--
--    * Manage squadrons.
--    * Launch A2A and A2G missions (AUFTRAG)
-- 
-- 
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/Airwing).
--
-- ===
--
-- ### Author: **funkyfranky**
-- 
-- ===
-- @module Ops.Airwing
-- @image OPS_AirWing.png


--- AIRWING class.
-- @type AIRWING
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity of output.
-- @field #string lid Class id string for output to DCS log file.
-- @field #table menu Table of menu items.
-- @field #table squadrons Table of squadrons.
-- @field #table missionqueue Mission queue table.
-- @field #table payloads Playloads for specific aircraft and mission types.
-- @field #number payloadcounter Running index of payloads.
-- @field Core.Set#SET_ZONE zonesetCAP Set of CAP zones.
-- @field Core.Set#SET_ZONE zonesetTANKER Set of TANKER zones.
-- @field Core.Set#SET_ZONE zonesetAWACS Set of AWACS zones.
-- @field Core.Set#SET_ZONE zonesetRECON Set of RECON zones. 
-- @field #number nflightsCAP Number of CAP flights constantly in the air.
-- @field #number nflightsAWACS Number of AWACS flights constantly in the air.
-- @field #number nflightsTANKERboom Number of TANKER flights with BOOM constantly in the air.
-- @field #number nflightsTANKERprobe Number of TANKER flights with PROBE constantly in the air.
-- @field #number nflightsRescueHelo Number of Rescue helo flights constantly in the air.
-- @field #number nflightsRecon Number of Recon flights constantly in the air.
-- @field #table pointsCAP Table of CAP points.
-- @field #table pointsTANKER Table of Tanker points.
-- @field #table pointsAWACS Table of AWACS points.
-- @field #table pointsRecon Table of RECON points.
-- @field #boolean markpoints Display markers on the F10 map.
-- @field Ops.Airboss#AIRBOSS airboss Airboss attached to this wing.
--
-- @field Ops.RescueHelo#RESCUEHELO rescuehelo The rescue helo.
-- @field Ops.RecoveryTanker#RECOVERYTANKER recoverytanker The recoverytanker.
-- 
-- @field #string takeoffType Take of type.
-- @field #boolean despawnAfterLanding Aircraft are despawned after landing.
-- @field #boolean despawnAfterHolding Aircraft are despawned after holding.
-- @field #boolean capOptionPatrolRaceTrack Use closer patrol race track or standard orbit auftrag.
-- @field #number capFormation If capOptionPatrolRaceTrack is true, set the formation, also.
-- @field #number capOptionVaryStartTime If set, vary mission start time for CAP missions generated random between capOptionVaryStartTime and capOptionVaryEndTime
-- @field #number capOptionVaryEndTime If set, vary mission start time for CAP missions generated random between capOptionVaryStartTime and capOptionVaryEndTime
-- 
-- @extends Ops.Legion#LEGION

--- *I fly because it releases my mind from the tyranny of petty things.* -- Antoine de Saint-Exupery
--
-- ===
--
-- # The AIRWING Concept
--
-- An AIRWING consists of multiple SQUADRONS. These squadrons "live" in a WAREHOUSE, i.e. a physical structure that is connected to an airbase (airdrome, FRAP or ship).
-- For an airwing to be operational, it needs airframes, weapons/fuel and an airbase.
--
-- # Create an Airwing
--
-- ## Constructing the Airwing
--
--     airwing=AIRWING:New("Warehouse Batumi", "8th Fighter Wing")
--     airwing:Start()
--
-- The first parameter specified the warehouse, i.e. the static building housing the airwing (or the name of the aircraft carrier). The second parameter is optional
-- and sets an alias.
--
-- ## Adding Squadrons
--
-- At this point the airwing does not have any assets (aircraft). In order to add these, one needs to first define SQUADRONS.
--
--     VFA151=SQUADRON:New("F-14 Group", 8, "VFA-151 (Vigilantes)")
--     VFA151:AddMissionCapability({AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT})
--
--     airwing:AddSquadron(VFA151)
--
-- This adds eight Tomcat groups beloning to VFA-151 to the airwing. This squadron has the ability to perform combat air patrols and intercepts.
--
-- ## Adding Payloads
--
-- Adding pure airframes is not enough. The aircraft also need weapons (and fuel) for certain missions. These must be given to the airwing from template groups
-- defined in the Mission Editor.
--
--     -- F-14 payloads for CAP and INTERCEPT. Phoenix are first, sparrows are second choice.
--     airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-54C"), 2, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP}, 80)
--     airwing:NewPayload(GROUP:FindByName("F-14 Payload AIM-7M"), 20, {AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.GCICAP})
--
-- This will add two AIM-54C and 20 AIM-7M payloads.
--
-- If the airwing gets an intercept or patrol mission assigned, it will first use the AIM-54s. Once these are consumed, the AIM-7s are attached to the aircraft.
--
-- When an airwing does not have a payload for a certain mission type, the mission cannot be carried out.
--
-- You can set the number of payloads to "unlimited" by setting its quantity to -1.
--
-- # Adding Missions
--
-- Various mission types can be added easily via the AUFTRAG class.
--
-- Once you created an AUFTRAG you can add it to the AIRWING with the :AddMission(mission) function.
--
-- This mission will be put into the AIRWING queue. Once the mission start time is reached and all resources (airframes and payloads) are available, the mission is started.
-- If the mission stop time is over (and the mission is not finished), it will be cancelled and removed from the queue. This applies also to mission that were not even
-- started.
--
--
-- @field #AIRWING
AIRWING = {
  ClassName      = "AIRWING",
  verbose        =     0,
  lid            =   nil,
  menu           =   nil,
  payloads       =    {},
  payloadcounter =     0,
  pointsCAP      =    {},
  pointsTANKER   =    {},
  pointsAWACS    =    {},
  pointsRecon    =    {},
  markpoints     =   false,
  capOptionPatrolRaceTrack = false,
  capFormation   =    nil,
  capOptionVaryStartTime =    nil,
  capOptionVaryEndTime =    nil,
}

--- Payload data.
-- @type AIRWING.Payload
-- @field #number uid Unique payload ID.
-- @field #string unitname Name of the unit this pylon was extracted from.
-- @field #string aircrafttype Type of aircraft, which can use this payload.
-- @field #table capabilities Mission types and performances for which this payload can be used.
-- @field #table pylons Pylon data extracted for the unit template.
-- @field #number navail Number of available payloads of this type.
-- @field #boolean unlimited If true, this payload is unlimited and does not get consumed.

--- Patrol data.
-- @type AIRWING.PatrolData
-- @field #string type Type name.
-- @field Core.Point#COORDINATE coord Patrol coordinate.
-- @field #number altitude Altitude in feet.
-- @field #number heading Heading in degrees.
-- @field #number leg Leg length in NM.
-- @field #number speed Speed in knots.
-- @field #number refuelsystem Refueling system type: `0=Unit.RefuelingSystem.BOOM_AND_RECEPTACLE`, `1=Unit.RefuelingSystem.PROBE_AND_DROGUE`.
-- @field #number noccupied Number of flights on this patrol point.
-- @field Wrapper.Marker#MARKER marker F10 marker.
-- @field #boolean IsZonePoint flag for using a (moving) zone as point for patrol etc.
-- @field Core.Zone#ZONE_BASE patrolzone in case Patrol coordinate was handed as zone, store here.

--- Patrol zone.
-- @type AIRWING.PatrolZone
-- @field Core.Zone#ZONE zone Zone.
-- @field #number altitude Altitude in feet.
-- @field #number heading Heading in degrees.
-- @field #number leg Leg length in NM.
-- @field #number speed Speed in knots.
-- @field Ops.Auftrag#AUFTRAG mission Mission assigned.
-- @field Wrapper.Marker#MARKER marker F10 marker.

--- AWACS zone.
-- @type AIRWING.AwacsZone
-- @field Core.Zone#ZONE zone Zone.
-- @field #number altitude Altitude in feet.
-- @field #number heading Heading in degrees.
-- @field #number leg Leg length in NM.
-- @field #number speed Speed in knots.
-- @field Ops.Auftrag#AUFTRAG mission Mission assigned.
-- @field Wrapper.Marker#MARKER marker F10 marker.

--- Tanker zone.
-- @type AIRWING.TankerZone
-- @field #number refuelsystem Refueling system type: `0=Unit.RefuelingSystem.BOOM_AND_RECEPTACLE`, `1=Unit.RefuelingSystem.PROBE_AND_DROGUE`.
-- @extends #AIRWING.PatrolZone

--- AIRWING class version.
-- @field #string version
AIRWING.version="0.9.7"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Check that airbase has enough parking spots if a request is BIG.
-- DONE: Allow (moving) zones as base for patrol points.
-- DONE: Spawn in air ==> Needs WAREHOUSE update.
-- DONE: Spawn hot.
-- DONE: Make special request to transfer squadrons to anther airwing (or warehouse).
-- DONE: Add squadrons to warehouse.
-- DONE: Build mission queue.
-- DONE: Find way to start missions.
-- DONE: Check if missions are done/cancelled.
-- DONE: Payloads as resources.
-- DONE: Define CAP zones.
-- DONE: Define TANKER zones for refuelling.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new AIRWING class object for a specific aircraft carrier unit.
-- @param #AIRWING self
-- @param #string warehousename Name of the warehouse static or unit object representing the warehouse.
-- @param #string airwingname Name of the air wing, e.g. "AIRWING-8".
-- @return #AIRWING self
function AIRWING:New(warehousename, airwingname)

  -- Inherit everything from LEGION class.
  local self=BASE:Inherit(self, LEGION:New(warehousename, airwingname)) -- #AIRWING

  -- Nil check.
  if not self then
    BASE:E(string.format("ERROR: Could not find warehouse %s!", warehousename))
    return nil
  end

  -- Set some string id for output to DCS.log file.
  self.lid=string.format("AIRWING %s | ", self.alias)
  
  -- Defaults:
  self.nflightsCAP=0
  self.nflightsAWACS=0
  self.nflightsRecon=0
  self.nflightsTANKERboom=0
  self.nflightsTANKERprobe=0
  self.nflightsRecoveryTanker=0
  self.nflightsRescueHelo=0
  self.markpoints=false  

  -- Add FSM transitions.
  --                 From State  -->   Event         -->      To State
  self:AddTransition("*",             "FlightOnMission",       "*")           -- A FLIGHTGROUP was send on a Mission (AUFTRAG).

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the AIRWING. Initializes parameters and starts event handlers.
  -- @function [parent=#AIRWING] Start
  -- @param #AIRWING self

  --- Triggers the FSM event "Start" after a delay. Starts the AIRWING. Initializes parameters and starts event handlers.
  -- @function [parent=#AIRWING] __Start
  -- @param #AIRWING self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Stop". Stops the AIRWING and all its event handlers.
  -- @param #AIRWING self

  --- Triggers the FSM event "Stop" after a delay. Stops the AIRWING and all its event handlers.
  -- @function [parent=#AIRWING] __Stop
  -- @param #AIRWING self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "FlightOnMission".
  -- @function [parent=#AIRWING] FlightOnMission
  -- @param #AIRWING self
  -- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FLIGHTGROUP on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- Triggers the FSM event "FlightOnMission" after a delay.
  -- @function [parent=#AIRWING] __FlightOnMission
  -- @param #AIRWING self
  -- @param #number delay Delay in seconds.
  -- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FLIGHTGROUP on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "FlightOnMission" event.
  -- @function [parent=#AIRWING] OnAfterFlightOnMission
  -- @param #AIRWING self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup  The FLIGHTGROUP on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Add a squadron to the air wing.
-- @param #AIRWING self
-- @param Ops.Squadron#SQUADRON Squadron The squadron object.
-- @return #AIRWING self
function AIRWING:AddSquadron(Squadron)

  -- Add squadron to airwing.
  table.insert(self.cohorts, Squadron)

  -- Add assets to squadron.
  self:AddAssetToSquadron(Squadron, Squadron.Ngroups)

  -- Tanker and AWACS get unlimited payloads.
  if Squadron.attribute==GROUP.Attribute.AIR_AWACS then
    self:NewPayload(Squadron.templategroup, -1, AUFTRAG.Type.AWACS)
  elseif Squadron.attribute==GROUP.Attribute.AIR_TANKER then
    self:NewPayload(Squadron.templategroup, -1, AUFTRAG.Type.TANKER)
  end
  
  -- Relocate mission.
  self:NewPayload(Squadron.templategroup, -1, AUFTRAG.Type.RELOCATECOHORT, 0)

  -- Set airwing to squadron.
  Squadron:SetAirwing(self)

  -- Start squadron.
  if Squadron:IsStopped() then
    Squadron:Start()
  end

  return self
end

--- Add a **new** payload to the airwing resources.
-- @param #AIRWING self
-- @param Wrapper.Unit#UNIT Unit The unit, the payload is extracted from. Can also be given as *#string* name of the unit.
-- @param #number Npayloads Number of payloads to add to the airwing resources. Default 99 (which should be enough for most scenarios). Set to -1 for unlimited.
-- @param #table MissionTypes Mission types this payload can be used for.
-- @param #number Performance A number between 0 (worst) and 100 (best) to describe the performance of the loadout for the given mission types. Default is 50.
-- @return #AIRWING.Payload The payload table or nil if the unit does not exist.
function AIRWING:NewPayload(Unit, Npayloads, MissionTypes,  Performance)

  -- Default performance.
  Performance=Performance or 50

  if type(Unit)=="string" then
    local name=Unit
    Unit=UNIT:FindByName(name)
    if not Unit then
      Unit=GROUP:FindByName(name)
    end
  end

  if Unit then

    -- If a GROUP object was given, get the first unit.
    if Unit:IsInstanceOf("GROUP") then
      Unit=Unit:GetUnit(1)
    end

    -- Ensure Missiontypes is a table.
    if MissionTypes and type(MissionTypes)~="table" then
      MissionTypes={MissionTypes}
    end

    -- Create payload.
    local payload={} --#AIRWING.Payload
    payload.uid=self.payloadcounter
    payload.unitname=Unit:GetName()
    payload.aircrafttype=Unit:GetTypeName()
    payload.pylons=Unit:GetTemplatePayload()
    
    -- Set the number of available payloads.
    self:SetPayloadAmount(payload, Npayloads)

    -- Payload capabilities.
    payload.capabilities={}
    for _,missiontype in pairs(MissionTypes) do
      local capability={} --Ops.Auftrag#AUFTRAG.Capability
      capability.MissionType=missiontype
      capability.Performance=Performance
      table.insert(payload.capabilities, capability)
    end

    -- Add ORBIT for all.
    if not AUFTRAG.CheckMissionType(AUFTRAG.Type.ORBIT, MissionTypes) then
      local capability={}  --Ops.Auftrag#AUFTRAG.Capability
      capability.MissionType=AUFTRAG.Type.ORBIT
      capability.Performance=50
      table.insert(payload.capabilities, capability)
    end
    
    -- Add RELOCATION for all.
    if not AUFTRAG.CheckMissionType(AUFTRAG.Type.RELOCATECOHORT, MissionTypes) then
      local capability={}  --Ops.Auftrag#AUFTRAG.Capability
      capability.MissionType=AUFTRAG.Type.RELOCATECOHORT
      capability.Performance=50
      table.insert(payload.capabilities, capability)
    end    

    -- Info
    self:T(self.lid..string.format("Adding new payload from unit %s for aircraft type %s: ID=%d, N=%d (unlimited=%s), performance=%d, missions: %s",
    payload.unitname, payload.aircrafttype, payload.uid, payload.navail, tostring(payload.unlimited), Performance, table.concat(MissionTypes, ", ")))

    -- Add payload
    table.insert(self.payloads, payload)

    -- Increase counter
    self.payloadcounter=self.payloadcounter+1

    return payload

  end

  self:E(self.lid.."ERROR: No UNIT found to create PAYLOAD!")
  return nil
end

--- Set the number of payload available.
-- @param #AIRWING self
-- @param #AIRWING.Payload Payload The payload table created by the `:NewPayload` function.
-- @param #number Navailable Number of payloads available to the airwing resources. Default 99 (which should be enough for most scenarios). Set to -1 for unlimited.
-- @return #AIRWING self
function AIRWING:SetPayloadAmount(Payload, Navailable)

  Navailable=Navailable or 99

  if Payload then

    Payload.unlimited=Navailable<0
    if Payload.unlimited then
      Payload.navail=1
    else
      Payload.navail=Navailable
    end
    
  end

  return self
end

--- Increase or decrease the amount of available payloads. Unlimited playloads first need to be set to a limited number with the `SetPayloadAmount` function.
-- @param #AIRWING self
-- @param #AIRWING.Payload Payload The payload table created by the `:NewPayload` function.
-- @param #number N Number of payloads to be added. Use negative number to decrease amount. Default 1.
-- @return #AIRWING self
function AIRWING:IncreasePayloadAmount(Payload, N)

  N=N or 1
  
  if Payload and Payload.navail>=0 then
  
    -- Increase/decrease amount.
    Payload.navail=Payload.navail+N
  
    -- Ensure playload does not drop below 0.
    Payload.navail=math.max(Payload.navail, 0) 
  
  end
  
  return self
end

--- Get amount of payloads available for a given playload.
-- @param #AIRWING self
-- @param #AIRWING.Payload Payload The payload table created by the `:NewPayload` function.
-- @return #number Number of payloads available. Unlimited payloads will return -1.
function AIRWING:GetPayloadAmount(Payload)
  return Payload.navail
end

--- Get capabilities of a given playload.
-- @param #AIRWING self
-- @param #AIRWING.Payload Payload The payload data table.
-- @return #table Capabilities.
function AIRWING:GetPayloadCapabilities(Payload)
  return Payload.capabilities
end

--- Add a mission capability to an existing payload.
-- @param #AIRWING self
-- @param #AIRWING.Payload Payload The payload table to which the capability should be added.
-- @param #table MissionTypes Mission types to be added.
-- @param #number Performance A number between 0 (worst) and 100 (best) to describe the performance of the loadout for the given mission types. Default is 50.
-- @return #AIRWING self
function AIRWING:AddPayloadCapability(Payload, MissionTypes, Performance)

  -- Ensure Missiontypes is a table.
  if MissionTypes and type(MissionTypes)~="table" then
    MissionTypes={MissionTypes}
  end

  Payload.capabilities=Payload.capabilities or {}

  for _,missiontype in pairs(MissionTypes) do

    local capability={} --Ops.Auftrag#AUFTRAG.Capability
    capability.MissionType=missiontype
    capability.Performance=Performance

    --TODO: check that capability does not already exist!

    table.insert(Payload.capabilities, capability)
  end

  return self
end

--- Fetch a payload from the airwing resources for a given unit and mission type.
-- The payload with the highest priority is preferred.
-- @param #AIRWING self
-- @param #string UnitType The type of the unit.
-- @param #string MissionType The mission type.
-- @param #table Payloads Specific payloads only to be considered.
-- @return #AIRWING.Payload Payload table or *nil*.
function AIRWING:FetchPayloadFromStock(UnitType, MissionType, Payloads)

  -- Quick check if we have any payloads.
  if not self.payloads or #self.payloads==0 then
    self:T(self.lid.."WARNING: No payloads in stock!")
    return nil
  end

  -- Debug.
  if self.verbose>=4 then
    self:I(self.lid..string.format("Looking for payload for unit type=%s and mission type=%s", UnitType, MissionType))
    for i,_payload in pairs(self.payloads) do
      local payload=_payload --#AIRWING.Payload
      local performance=self:GetPayloadPeformance(payload, MissionType)
      self:I(self.lid..string.format("[%d] Payload type=%s navail=%d unlimited=%s", i, payload.aircrafttype, payload.navail, tostring(payload.unlimited)))
    end
  end

  --- Sort payload wrt the following criteria:
  -- 1) Highest performance is the main selection criterion.
  -- 2) If payloads have the same performance, unlimited payloads are preferred over limited ones.
  -- 3) If payloads have the same performance _and_ are limited, the more abundant one is preferred.
  local function sortpayloads(a,b)
    local pA=a --#AIRWING.Payload
    local pB=b --#AIRWING.Payload
    if a and b then  -- I had the case that a or b were nil even though the self.payloads table was looking okay. Very strange! Seems to be solved by pre-selecting valid payloads.
      local performanceA=self:GetPayloadPeformance(a, MissionType)
      local performanceB=self:GetPayloadPeformance(b, MissionType)
      return (performanceA>performanceB) or (performanceA==performanceB and a.unlimited==true and b.unlimited~=true) or (performanceA==performanceB and a.unlimited==true and b.unlimited==true and a.navail>b.navail)
    elseif not a then
      self:I(self.lid..string.format("FF ERROR in sortpayloads: a is nil"))
      return false
    elseif not b then
      self:I(self.lid..string.format("FF ERROR in sortpayloads: b is nil"))
      return true
    else
      self:I(self.lid..string.format("FF ERROR in sortpayloads: a and b are nil"))
      return false
    end
  end

  local function _checkPayloads(payload)
    if Payloads then
      for _,Payload in pairs(Payloads) do
        if Payload.uid==payload.uid then
          return true
        end
      end
    else
      -- Payload was not specified.
      return nil
    end
    return false
  end

  -- Pre-selection: filter out only those payloads that are valid for the airframe and mission type and are available.
  local payloads={}
  for _,_payload in pairs(self.payloads) do
    local payload=_payload --#AIRWING.Payload

    local specialpayload=_checkPayloads(payload)
    local compatible=AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities)

    local goforit = specialpayload or (specialpayload==nil and compatible)

    if payload.aircrafttype==UnitType and payload.navail>0 and goforit then
      table.insert(payloads, payload)
    end
  end

  -- Debug.
  if self.verbose>=4 then
    self:I(self.lid..string.format("Sorted payloads for mission type %s and aircraft type=%s:", MissionType, UnitType))
    for _,_payload in ipairs(self.payloads) do
      local payload=_payload --#AIRWING.Payload
      if payload.aircrafttype==UnitType and AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities) then
        local performace=self:GetPayloadPeformance(payload, MissionType)
        self:I(self.lid..string.format("- %s payload for %s: avail=%d performace=%d", MissionType, payload.aircrafttype, payload.navail, performace))
      end
    end
  end

  -- Cases:
  if #payloads==0 then
    -- No payload available.
    self:T(self.lid..string.format("WARNING: Could not find a payload for airframe %s mission type %s!", UnitType, MissionType))
    return nil
  elseif #payloads==1 then
    -- Only one payload anyway.
    local payload=payloads[1] --#AIRWING.Payload
    if not payload.unlimited then
      payload.navail=payload.navail-1
    end
    return payload
  else
    -- Sort payloads.
    table.sort(payloads, sortpayloads)
    local payload=payloads[1] --#AIRWING.Payload
    if not payload.unlimited then
      payload.navail=payload.navail-1
    end
    return payload
  end

end

--- Return payload from asset back to stock.
-- @param #AIRWING self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The squadron asset.
function AIRWING:ReturnPayloadFromAsset(asset)

  local payload=asset.payload

  if payload then

    -- Increase count if not unlimited.
    if not payload.unlimited then
      payload.navail=payload.navail+1
    end

    -- Remove asset payload.
    asset.payload=nil

  else
    self:E(self.lid.."ERROR: asset had no payload attached!")
  end

end


--- Add asset group(s) to squadron.
-- @param #AIRWING self
-- @param Ops.Squadron#SQUADRON Squadron The squadron object.
-- @param #number Nassets Number of asset groups to add.
-- @return #AIRWING self
function AIRWING:AddAssetToSquadron(Squadron, Nassets)

  if Squadron then

    -- Get the template group of the squadron.
    local Group=GROUP:FindByName(Squadron.templatename)

    if Group then

      -- Debug text.
      local text=string.format("Adding asset %s to squadron %s", Group:GetName(), Squadron.name)
      self:T(self.lid..text)

      -- Add assets to airwing warehouse.
      self:AddAsset(Group, Nassets, nil, nil, nil, nil, Squadron.skill, Squadron.livery, Squadron.name)

    else
      self:E(self.lid.."ERROR: Group does not exist!")
    end

  else
    self:E(self.lid.."ERROR: Squadron does not exit!")
  end

  return self
end

--- Get squadron by name.
-- @param #AIRWING self
-- @param #string SquadronName Name of the squadron, e.g. "VFA-37".
-- @return Ops.Squadron#SQUADRON The squadron object.
function AIRWING:GetSquadron(SquadronName)
  local squad=self:_GetCohort(SquadronName)
  return squad
end

--- Get squadron of an asset.
-- @param #AIRWING self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The squadron asset.
-- @return Ops.Squadron#SQUADRON The squadron object.
function AIRWING:GetSquadronOfAsset(Asset)
  return self:GetSquadron(Asset.squadname)
end

--- Remove asset from squadron.
-- @param #AIRWING self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The squad asset.
function AIRWING:RemoveAssetFromSquadron(Asset)
  local squad=self:GetSquadronOfAsset(Asset)
  if squad then
    squad:DelAsset(Asset)
  end
end

--- Set number of CAP flights constantly carried out.
-- @param #AIRWING self
-- @param #number n Number of flights. Default 1.
-- @return #AIRWING self
function AIRWING:SetNumberCAP(n)
  self.nflightsCAP=n or 1
  return self
end

--- Set CAP flight formation.
-- @param #AIRWING self
-- @param #number Formation Formation to take, e.g. ENUMS.Formation.FixedWing.Trail.Close, also see [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_option_formation).
-- @return #AIRWING self
function AIRWING:SetCAPFormation(Formation)
  self.capFormation = Formation
  return self
end

--- Set CAP close race track.We'll utilize the AUFTRAG PatrolRaceTrack instead of a standard race track orbit task.
-- @param #AIRWING self
-- @param #boolean OnOff If true, switch this on, else switch off. Off by default.
-- @return #AIRWING self
function AIRWING:SetCapCloseRaceTrack(OnOff)
  self.capOptionPatrolRaceTrack = OnOff
  return self
end

--- Set CAP mission start to vary randomly between Start end End seconds.
-- @param #AIRWING self
-- @param #number Start
-- @param #number End 
-- @return #AIRWING self
function AIRWING:SetCapStartTimeVariation(Start, End)
  self.capOptionVaryStartTime = Start or 5
  self.capOptionVaryEndTime = End or 60
  return self
end

--- Set number of TANKER flights with Boom constantly in the air.
-- @param #AIRWING self
-- @param #number Nboom Number of flights. Default 1.
-- @return #AIRWING self
function AIRWING:SetNumberTankerBoom(Nboom)
  self.nflightsTANKERboom=Nboom or 1
  return self
end

--- Set markers on the map for Patrol Points.
-- @param #AIRWING self
-- @param #boolean onoff Set to true to switch markers on.
-- @return #AIRWING self
function AIRWING:ShowPatrolPointMarkers(onoff)
  if onoff then
    self.markpoints = true
  else
    self.markpoints = false
  end
  return self
end

--- Set number of TANKER flights with Probe constantly in the air.
-- @param #AIRWING self
-- @param #number Nprobe Number of flights. Default 1.
-- @return #AIRWING self
function AIRWING:SetNumberTankerProbe(Nprobe)
  self.nflightsTANKERprobe=Nprobe or 1
  return self
end

--- Set number of AWACS flights constantly in the air.
-- @param #AIRWING self
-- @param #number n Number of flights. Default 1.
-- @return #AIRWING self
function AIRWING:SetNumberAWACS(n)
  self.nflightsAWACS=n or 1
  return self
end

--- Set number of RECON flights constantly in the air.
-- @param #AIRWING self
-- @param #number n Number of flights. Default 1.
-- @return #AIRWING self
function AIRWING:SetNumberRecon(n)
  self.nflightsRecon=n or 1
  return self
end

--- Set number of Rescue helo flights constantly in the air.
-- @param #AIRWING self
-- @param #number n Number of flights. Default 1.
-- @return #AIRWING self
function AIRWING:SetNumberRescuehelo(n)
  self.nflightsRescueHelo=n or 1
  return self
end

---
-- @param #AIRWING self
-- @param #AIRWING.PatrolData point Patrol point table.
-- @return #string Marker text.
function AIRWING:_PatrolPointMarkerText(point)

  local text=string.format("%s Occupied=%d, \nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts",
  point.type, point.noccupied, point.heading, point.leg, point.altitude, point.speed)

  return text
end

--- Update marker of the patrol point.
-- @param #AIRWING self
-- @param #AIRWING.PatrolData point Patrol point table.
function AIRWING:UpdatePatrolPointMarker(point)

  if self and self.markpoints then -- sometimes there's a direct call from #OPSGROUP
    local text=string.format("%s Occupied=%d\nheading=%03d, leg=%d NM, alt=%d ft, speed=%d kts",
    point.type, point.noccupied, point.heading, point.leg, point.altitude, point.speed)
 
    if point.IsZonePoint and point.IsZonePoint == true and point.patrolzone then
      -- update position
      local Coordinate = point.patrolzone:GetCoordinate()
      point.marker:UpdateCoordinate(Coordinate)
      point.marker:UpdateText(text, 1.5)
    else
      point.marker:UpdateText(text, 1)
    end
  end
end


--- Create a new generic patrol point.
-- @param #AIRWING self
-- @param #string Type Patrol point type, e.g. "CAP" or "AWACS". Default "Unknown".
-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. Default 10-15 NM away from the location of the airwing. Can be handed as a Core.Zone#ZONE object (e.g. in case you want  the point to align with a moving zone).
-- @param #number Altitude Orbit altitude in feet. Default random between Angels 10 and 20.
-- @param #number Heading Heading in degrees. Default random (0, 360] degrees.
-- @param #number LegLength Length of race-track orbit in NM. Default 15 NM.
-- @param #number Speed Orbit speed in knots. Default 350 knots.
-- @param #number RefuelSystem Refueling system: 0=Boom, 1=Probe. Default nil=any.
-- @return #AIRWING.PatrolData Patrol point table.
function AIRWING:NewPatrolPoint(Type, Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem)

  local patrolpoint={}  --#AIRWING.PatrolData
  patrolpoint.type=Type or "Unknown"
  patrolpoint.coord=Coordinate or self:GetCoordinate():Translate(UTILS.NMToMeters(math.random(10, 15)), math.random(360))
  if Coordinate and Coordinate:IsInstanceOf("ZONE_BASE") then
    patrolpoint.IsZonePoint = true
    patrolpoint.patrolzone = Coordinate
    patrolpoint.coord = patrolpoint.patrolzone:GetCoordinate()
  else
    patrolpoint.IsZonePoint = false
  end
  patrolpoint.heading=Heading or math.random(360)
  patrolpoint.leg=LegLength or 15
  patrolpoint.altitude=Altitude or math.random(10,20)*1000
  patrolpoint.speed=Speed or 350
  patrolpoint.noccupied=0
  patrolpoint.refuelsystem=RefuelSystem

  if self.markpoints then
    patrolpoint.marker=MARKER:New(Coordinate, "New Patrol Point"):ToAll()
    self:UpdatePatrolPointMarker(patrolpoint)
  end

  return patrolpoint
end

--- Add a patrol Point for CAP missions.
-- @param #AIRWING self
-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. Can be handed as a Core.Zone#ZONE object (e.g. in case you want  the point to align with a moving zone).
-- @param #number Altitude Orbit altitude in feet.
-- @param #number Speed Orbit speed in knots.
-- @param #number Heading Heading in degrees.
-- @param #number LegLength Length of race-track orbit in NM.
-- @return #AIRWING self
function AIRWING:AddPatrolPointCAP(Coordinate, Altitude, Speed, Heading, LegLength)

  local patrolpoint=self:NewPatrolPoint("CAP", Coordinate, Altitude, Speed, Heading, LegLength)

  table.insert(self.pointsCAP, patrolpoint)

  return self
end

--- Add a patrol Point for RECON missions.
-- @param #AIRWING self
-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. Can be handed as a Core.Zone#ZONE object (e.g. in case you want  the point to align with a moving zone).
-- @param #number Altitude Orbit altitude in feet.
-- @param #number Speed Orbit speed in knots.
-- @param #number Heading Heading in degrees.
-- @param #number LegLength Length of race-track orbit in NM.
-- @return #AIRWING self
function AIRWING:AddPatrolPointRecon(Coordinate, Altitude, Speed, Heading, LegLength)

  local patrolpoint=self:NewPatrolPoint("RECON", Coordinate, Altitude, Speed, Heading, LegLength)

  table.insert(self.pointsRecon, patrolpoint)

  return self
end

--- Add a patrol Point for TANKER missions.
-- @param #AIRWING self
-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. Can be handed as a Core.Zone#ZONE object (e.g. in case you want  the point to align with a moving zone).
-- @param #number Altitude Orbit altitude in feet.
-- @param #number Speed Orbit speed in knots.
-- @param #number Heading Heading in degrees.
-- @param #number LegLength Length of race-track orbit in NM.
-- @param #number RefuelSystem Set refueling system of tanker: 0=boom, 1=probe. Default any (=nil).
-- @return #AIRWING self
function AIRWING:AddPatrolPointTANKER(Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem)

  local patrolpoint=self:NewPatrolPoint("Tanker", Coordinate, Altitude, Speed, Heading, LegLength, RefuelSystem)

  table.insert(self.pointsTANKER, patrolpoint)

  return self
end

--- Add a patrol Point for AWACS missions.
-- @param #AIRWING self
-- @param Core.Point#COORDINATE Coordinate Coordinate of the patrol point. Can be handed as a Core.Zone#ZONE object (e.g. in case you want  the point to align with a moving zone).
-- @param #number Altitude Orbit altitude in feet.
-- @param #number Speed Orbit speed in knots.
-- @param #number Heading Heading in degrees.
-- @param #number LegLength Length of race-track orbit in NM.
-- @return #AIRWING self
function AIRWING:AddPatrolPointAWACS(Coordinate, Altitude, Speed, Heading, LegLength)

  local patrolpoint=self:NewPatrolPoint("AWACS", Coordinate, Altitude, Speed, Heading, LegLength)

  table.insert(self.pointsAWACS, patrolpoint)

  return self
end

--- Set airboss of this wing. He/she will take care that no missions are launched if the carrier is recovering.
-- @param #AIRWING self
-- @param Ops.Airboss#AIRBOSS airboss The AIRBOSS object.
-- @return #AIRWING self
function AIRWING:SetAirboss(airboss)
  self.airboss=airboss
  return self
end

--- Set takeoff type. All assets of this airwing will be spawned with this takeoff type.
-- Spawning on runways is not supported.
-- @param #AIRWING self
-- @param #string TakeoffType Take off type: "Cold" (default) or "Hot" with engines on or "Air" for spawning in air.
-- @return #AIRWING self
function AIRWING:SetTakeoffType(TakeoffType)
  TakeoffType=TakeoffType or "Cold"
  if TakeoffType:lower()=="hot" then
    self.takeoffType=COORDINATE.WaypointType.TakeOffParkingHot
  elseif TakeoffType:lower()=="cold" then
    self.takeoffType=COORDINATE.WaypointType.TakeOffParking
  elseif TakeoffType:lower()=="air" then
    self.takeoffType=COORDINATE.WaypointType.TurningPoint    
  else
    self.takeoffType=COORDINATE.WaypointType.TakeOffParking
  end
  return self
end

--- Set takeoff type cold (default). All assets of this squadron will be spawned with engines off (cold).
-- @param #AIRWING self
-- @return #AIRWING self
function AIRWING:SetTakeoffCold()
  self:SetTakeoffType("Cold")
  return self
end

--- Set takeoff type hot. All assets of this squadron will be spawned with engines on (hot).
-- @param #AIRWING self
-- @return #AIRWING self
function AIRWING:SetTakeoffHot()
  self:SetTakeoffType("Hot")
  return self
end

--- Set takeoff type air. All assets of this squadron will be spawned in air above the airbase.
-- @param #AIRWING self
-- @return #AIRWING self
function AIRWING:SetTakeoffAir()
  self:SetTakeoffType("Air")
  return self
end

--- Set the aircraft of the AirWing to land straight in.
-- @param #AIRWING self
-- @return #FLIGHTGROUP self
function AIRWING:SetLandingStraightIn()
  self.OptionLandingStraightIn = true
  return self
end

--- Set the aircraft of the AirWing to land in pairs for groups > 1 aircraft.
-- @param #AIRWING self
-- @return #AIRWING self
function AIRWING:SetLandingForcePair()
  self.OptionLandingForcePair = true
  return self
end

--- Set the aircraft of the AirWing to NOT land in pairs.
-- @param #AIRWING self
-- @return #AIRWING self
function AIRWING:SetLandingRestrictPair()
  self.OptionLandingRestrictPair = true
  return self
end

--- Set the aircraft of the AirWing to land after overhead break.
-- @param #AIRWING self
-- @return #AIRWING self
function AIRWING:SetLandingOverheadBreak()
  self.OptionLandingOverheadBreak = true
  return self
end

--- [Helicopter] Set the aircraft of the AirWing to prefer vertical takeoff and landing.
-- @param #AIRWING self
-- @return #AIRWING self
function AIRWING:SetOptionPreferVerticalLanding()
  self.OptionPreferVerticalLanding = true
  return self
end

--- Set despawn after landing. Aircraft will be despawned after the landing event.
-- Can help to avoid DCS AI taxiing issues.
-- @param #AIRWING self
-- @param #boolean Switch If `true` (default), activate despawn after landing.
-- @return #AIRWING self
function AIRWING:SetDespawnAfterLanding(Switch)
  if Switch then
    self.despawnAfterLanding=Switch
  else
    self.despawnAfterLanding=true
  end
  return self
end

--- Set despawn after holding. Aircraft will be despawned when they arrive at their holding position at the airbase.
-- Can help to avoid DCS AI taxiing issues.
-- @param #AIRWING self
-- @param #boolean Switch If `true` (default), activate despawn after landing.
-- @return #AIRWING self
function AIRWING:SetDespawnAfterHolding(Switch)
  if Switch then
    self.despawnAfterHolding=Switch
  else
    self.despawnAfterHolding=true
  end
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start & Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Start AIRWING FSM.
-- @param #AIRWING self
function AIRWING:onafterStart(From, Event, To)

  -- Start parent Warehouse.
  self:GetParent(self, AIRWING).onafterStart(self, From, Event, To)

  -- Info.
  self:I(self.lid..string.format("Starting AIRWING v%s", AIRWING.version))

end

--- Update status.
-- @param #AIRWING self
function AIRWING:onafterStatus(From, Event, To)

  -- Status of parent Warehouse.
  self:GetParent(self).onafterStatus(self, From, Event, To)

  local fsmstate=self:GetState()

  -- Check CAP missions.
  self:CheckCAP()

  -- Check TANKER missions.
  self:CheckTANKER()

  -- Check AWACS missions.
  self:CheckAWACS()

  -- Check Rescue Helo missions.
  self:CheckRescuhelo()

  -- Check Recon missions.
  self:CheckRECON()
  
  -- Display tactival overview.
  self:_TacticalOverview()  

  ----------------
  -- Transport ---
  ----------------

  -- Check transport queue.
  self:CheckTransportQueue()

  --------------
  -- Mission ---
  --------------

  -- Check mission queue.
  self:CheckMissionQueue()


  -- General info:
  if self.verbose>=1 then

    -- Count missions not over yet.
    local Nmissions=self:CountMissionsInQueue()

    -- Count ALL payloads in stock. If any payload is unlimited, this gives 999.
    local Npayloads=self:CountPayloadsInStock(AUFTRAG.Type)

    -- Assets tot
    local Npq, Np, Nq=self:CountAssetsOnMission()

    local assets=string.format("%d (OnMission: Total=%d, Active=%d, Queued=%d)", self:CountAssets(), Npq, Np, Nq)

    -- Output.
    local text=string.format("%s: Missions=%d, Payloads=%d (%d), Squads=%d, Assets=%s", fsmstate, Nmissions, Npayloads, #self.payloads, #self.cohorts, assets)
    self:I(self.lid..text)
  end

  ------------------
  -- Mission Info --
  ------------------
  if self.verbose>=2 then
    local text=string.format("Missions Total=%d:", #self.missionqueue)
    for i,_mission in pairs(self.missionqueue) do
      local mission=_mission --Ops.Auftrag#AUFTRAG

      local prio=string.format("%d/%s", mission.prio, tostring(mission.importance)) ; if mission.urgent then prio=prio.." (!)" end
      local assets=string.format("%d/%d", mission:CountOpsGroups(), mission:GetNumberOfRequiredAssets())
      local target=string.format("%d/%d Damage=%.1f", mission:CountMissionTargets(), mission:GetTargetInitialNumber(), mission:GetTargetDamage())
      local mystatus=mission:GetLegionStatus(self)

      text=text..string.format("\n[%d] %s %s: Status=%s [%s], Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mystatus, mission.status, prio, assets, target)
    end
    self:I(self.lid..text)
  end

  -------------------
  -- Squadron Info --
  -------------------
  if self.verbose>=3 then
    local text="Squadrons:"
    for i,_squadron in pairs(self.cohorts) do
      local squadron=_squadron --Ops.Squadron#SQUADRON

      local callsign=squadron.callsignName and UTILS.GetCallsignName(squadron.callsignName) or "N/A"
      local modex=squadron.modex and squadron.modex or -1
      local skill=squadron.skill and tostring(squadron.skill) or "N/A"

      -- Squadron text
      text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", squadron.name, squadron:GetState(), squadron.aircrafttype, squadron:CountAssets(true), #squadron.assets, callsign, modex, skill)
    end
    self:I(self.lid..text)
  end

end

--- Get patrol data.
-- @param #AIRWING self
-- @param #table PatrolPoints Patrol data points.
-- @param #number RefuelSystem If provided, only return points with the specific refueling system.
-- @return #AIRWING.PatrolData Patrol point data table.
function AIRWING:_GetPatrolData(PatrolPoints, RefuelSystem)

  -- Sort wrt lowest number of flights on this point.
  local function sort(a,b)
    return a.noccupied<b.noccupied
  end

  if PatrolPoints and #PatrolPoints>0 then

    -- Sort data wrt number of flights at that point.
    table.sort(PatrolPoints, sort)

    for _,_patrolpoint in pairs(PatrolPoints) do
      local patrolpoint=_patrolpoint --#AIRWING.PatrolData
      if patrolpoint.IsZonePoint and patrolpoint.IsZonePoint == true and patrolpoint.patrolzone then
        -- update
        patrolpoint.coord = patrolpoint.patrolzone:GetCoordinate()
      end
      if (RefuelSystem and patrolpoint.refuelsystem and RefuelSystem==patrolpoint.refuelsystem) or RefuelSystem==nil or patrolpoint.refuelsystem==nil then
        return patrolpoint
      end
    end

  end

  -- Return a new point.
  return self:NewPatrolPoint()
end

--- Check how many CAP missions are assigned and add number of missing missions.
-- @param #AIRWING self
-- @return #AIRWING self
function AIRWING:CheckCAP()

  local Ncap=0 


  -- Count CAP missions.
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    if mission:IsNotOver() and (mission.type==AUFTRAG.Type.GCICAP or mission.type == AUFTRAG.Type.PATROLRACETRACK) and mission.patroldata then
      Ncap=Ncap+1
    end

  end

  for i=1,self.nflightsCAP-Ncap do

    local patrol=self:_GetPatrolData(self.pointsCAP)

    local altitude=patrol.altitude+1000*patrol.noccupied

    local missionCAP = nil -- Ops.Auftrag#AUFTRAG
    
    if self.capOptionPatrolRaceTrack then
      
      missionCAP=AUFTRAG:NewPATROL_RACETRACK(patrol.coord,altitude,patrol.speed,patrol.heading,patrol.leg, self.capFormation)
      
    else
        
      missionCAP=AUFTRAG:NewGCICAP(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg)
    
    end
    
    if self.capOptionVaryStartTime then
    
      local ClockStart = math.random(self.capOptionVaryStartTime, self.capOptionVaryEndTime)
    
      missionCAP:SetTime(ClockStart)
    
    end
    
    missionCAP.patroldata=patrol

    patrol.noccupied=patrol.noccupied+1

    if self.markpoints then self:UpdatePatrolPointMarker(patrol) end

    self:AddMission(missionCAP)

  end

  return self
end

--- Check how many RECON missions are assigned and add number of missing missions.
-- @param #AIRWING self
-- @return #AIRWING self
function AIRWING:CheckRECON()

  local Ncap=0

  -- Count CAP missions.
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    if mission:IsNotOver() and mission.type==AUFTRAG.Type.RECON and mission.patroldata then
      Ncap=Ncap+1
    end

  end
  
  --self:I(self.lid.."Number of active RECON Missions: "..Ncap)
  
  for i=1,self.nflightsRecon-Ncap do
    
    --self:I(self.lid.."Creating RECON Missions: "..i)
    
    local patrol=self:_GetPatrolData(self.pointsRecon)

    local altitude=patrol.altitude  --+1000*patrol.noccupied
    
    local ZoneSet = SET_ZONE:New()
    local Zone = ZONE_RADIUS:New(self.alias.." Recon "..math.random(1,10000),patrol.coord:GetVec2(),UTILS.NMToMeters(patrol.leg/2))
    
    ZoneSet:AddZone(Zone)
    
    if self.Debug then
      Zone:DrawZone(self.coalition,{0,0,1},Alpha,FillColor,FillAlpha,2,true)
    end
    
    local missionRECON=AUFTRAG:NewRECON(ZoneSet,patrol.speed,patrol.altitude,true)
    
    missionRECON.patroldata=patrol
    missionRECON.categories={AUFTRAG.Category.AIRCRAFT}

    patrol.noccupied=patrol.noccupied+1

    if self.markpoints then self:UpdatePatrolPointMarker(patrol) end

    self:AddMission(missionRECON)

  end

  return self
end

--- Check how many TANKER missions are assigned and add number of missing missions.
-- @param #AIRWING self
-- @return #AIRWING self
function AIRWING:CheckTANKER()

  local Nboom=0
  local Nprob=0

  -- Count tanker missions.
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    if mission:IsNotOver() and mission.type==AUFTRAG.Type.TANKER and mission.patroldata then
      if mission.refuelSystem==Unit.RefuelingSystem.BOOM_AND_RECEPTACLE then
        Nboom=Nboom+1
      elseif mission.refuelSystem==Unit.RefuelingSystem.PROBE_AND_DROGUE then
        Nprob=Nprob+1
      end

    end

  end

  -- Check missing boom tankers.
  for i=1,self.nflightsTANKERboom-Nboom do

    local patrol=self:_GetPatrolData(self.pointsTANKER)

    local altitude=patrol.altitude+1000*patrol.noccupied

    local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, Unit.RefuelingSystem.BOOM_AND_RECEPTACLE)

    mission.patroldata=patrol

    patrol.noccupied=patrol.noccupied+1

    if self.markpoints then self:UpdatePatrolPointMarker(patrol) end

    self:AddMission(mission)

  end

  -- Check missing probe tankers.
  for i=1,self.nflightsTANKERprobe-Nprob do

    local patrol=self:_GetPatrolData(self.pointsTANKER)

    local altitude=patrol.altitude+1000*patrol.noccupied

    local mission=AUFTRAG:NewTANKER(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg, Unit.RefuelingSystem.PROBE_AND_DROGUE)

    mission.patroldata=patrol

    patrol.noccupied=patrol.noccupied+1

    if self.markpoints then self:UpdatePatrolPointMarker(patrol) end

    self:AddMission(mission)

  end

  return self
end

--- Check how many AWACS missions are assigned and add number of missing missions.
-- @param #AIRWING self
-- @return #AIRWING self
function AIRWING:CheckAWACS()

  local N=0 --self:CountMissionsInQueue({AUFTRAG.Type.AWACS})

  -- Count AWACS missions.
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    if mission:IsNotOver() and mission.type==AUFTRAG.Type.AWACS and mission.patroldata then
      N=N+1
    end

  end

  for i=1,self.nflightsAWACS-N do

    local patrol=self:_GetPatrolData(self.pointsAWACS)

    local altitude=patrol.altitude+1000*patrol.noccupied

    local mission=AUFTRAG:NewAWACS(patrol.coord, altitude, patrol.speed, patrol.heading, patrol.leg)

    mission.patroldata=patrol

    patrol.noccupied=patrol.noccupied+1

    if self.markpoints then self:UpdatePatrolPointMarker(patrol) end

    self:AddMission(mission)

  end

  return self
end

--- Check how many Rescue helos are currently in the air.
-- @param #AIRWING self
-- @return #AIRWING self
function AIRWING:CheckRescuhelo()

  local N=self:CountMissionsInQueue({AUFTRAG.Type.RESCUEHELO})

  if self.airbase then
  
    local name=self.airbase:GetName()
  
    local carrier=UNIT:FindByName(name)
  
    for i=1,self.nflightsRescueHelo-N do
  
      local mission=AUFTRAG:NewRESCUEHELO(carrier)
  
      self:AddMission(mission)
  
    end
    
  end

  return self
end

--- Check how many AWACS missions are assigned and add number of missing missions.
-- @param #AIRWING self
-- @param Ops.FlightGroup#FLIGHTGROUP flightgroup The flightgroup.
-- @return Functional.Warehouse#WAREHOUSE.Assetitem The tanker asset.
function AIRWING:GetTankerForFlight(flightgroup)

  local tankers=self:GetAssetsOnMission(AUFTRAG.Type.TANKER)

  if #tankers>0 then

    local tankeropt={}
    for _,_tanker in pairs(tankers) do
      local tanker=_tanker --Functional.Warehouse#WAREHOUSE.Assetitem

      -- Check that donor and acceptor use the same refuelling system.
      if flightgroup.refueltype and flightgroup.refueltype==tanker.flightgroup.tankertype then

        local tankercoord=tanker.flightgroup.group:GetCoordinate()
        local assetcoord=flightgroup.group:GetCoordinate()

        local dist=assetcoord:Get2DDistance(tankercoord)

        -- Ensure that the flight does not find itself. Asset could be a tanker!
        if dist>5 then
          table.insert(tankeropt, {tanker=tanker, dist=dist})
        end

      end
    end

    -- Sort tankers wrt to distance.
    table.sort(tankeropt, function(a,b) return a.dist<b.dist end)

    -- Return tanker asset.
    if #tankeropt>0 then
      return tankeropt[1].tanker
    else
      return nil
    end
  end

  return nil
end

--- Add the ability to call back an Ops.AWACS#AWACS object with an FSM call "FlightOnMission(FlightGroup, Mission)".
-- @param #AIRWING self
-- @param Ops.AWACS#AWACS ConnectecdAwacs
-- @return #AIRWING self
function AIRWING:SetUsingOpsAwacs(ConnectecdAwacs)
  self:I(self.lid .. "Added AWACS Object: "..ConnectecdAwacs:GetName() or "unknown")
  self.UseConnectedOpsAwacs = true
  self.ConnectedOpsAwacs = ConnectecdAwacs
  return self
end

--- Remove the ability to call back an Ops.AWACS#AWACS object with an FSM call "FlightOnMission(FlightGroup, Mission)".
-- @param #AIRWING self
-- @return #AIRWING self
function AIRWING:RemoveUsingOpsAwacs()
  self:I(self.lid .. "Reomve AWACS Object: "..self.ConnectedOpsAwacs:GetName() or "unknown")
  self.UseConnectedOpsAwacs = false
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "FlightOnMission".
-- @param #AIRWING self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup Ops flight group on mission.
-- @param Ops.Auftrag#AUFTRAG Mission The requested mission.
function AIRWING:onafterFlightOnMission(From, Event, To, FlightGroup, Mission)
  -- Debug info.
  self:T(self.lid..string.format("Group %s on %s mission %s", FlightGroup:GetName(), Mission:GetType(), Mission:GetName()))
  if self.UseConnectedOpsAwacs and self.ConnectedOpsAwacs then
    self.ConnectedOpsAwacs:__FlightOnMission(2,FlightGroup,Mission)
  end
  -- Landing Options  
  if self.OptionLandingForcePair then
    FlightGroup:SetOptionLandingForcePair()
  elseif self.OptionLandingOverheadBreak then
   FlightGroup:SetOptionLandingOverheadBreak()
  elseif self.OptionLandingRestrictPair then
   FlightGroup:SetOptionLandingRestrictPair()
  elseif self.OptionLandingStraightIn then
    FlightGroup:SetOptionLandingStraightIn()
  end
  -- Landing Options Helo
  if self.OptionPreferVerticalLanding then
    FlightGroup:SetOptionPreferVertical()
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Count payloads in stock.
-- @param #AIRWING self
-- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`.
-- @param #table UnitTypes Types of units.
-- @param #table Payloads Specific payloads to be counted only.
-- @return #number Count of available payloads in stock.
function AIRWING:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads)

  if MissionTypes then
    if type(MissionTypes)=="string" then
      MissionTypes={MissionTypes}
    end
  end

  if UnitTypes then
    if type(UnitTypes)=="string" then
      UnitTypes={UnitTypes}
    end
  end

  local function _checkUnitTypes(payload)
    if UnitTypes then
      for _,unittype in pairs(UnitTypes) do
        if unittype==payload.aircrafttype then
          return true
        end
      end
    else
      -- Unit type was not specified.
      return true
    end
    return false
  end

  local function _checkPayloads(payload)
    if Payloads then
      for _,Payload in pairs(Payloads) do
        if Payload.uid==payload.uid then
          return true
        end
      end
    else
      -- Payload was not specified.
      return nil
    end
    return false
  end

  local n=0
  for _,_payload in pairs(self.payloads) do
    local payload=_payload --#AIRWING.Payload

    for _,MissionType in pairs(MissionTypes) do

      local specialpayload=_checkPayloads(payload)
      local compatible=AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities)

      local goforit = specialpayload or (specialpayload==nil and compatible)

      if goforit and _checkUnitTypes(payload) then

        if payload.unlimited then
          -- Payload is unlimited. Return a BIG number.
          return 999
        else
          n=n+payload.navail
        end

      end

    end
  end

  return n
end

--- Get payload performance for a given type of misson type.
-- @param #AIRWING self
-- @param #AIRWING.Payload Payload The payload table.
-- @param #string MissionType Type of mission.
-- @return #number Performance or -1.
function AIRWING:GetPayloadPeformance(Payload, MissionType)

  if Payload then

    for _,Capability in pairs(Payload.capabilities) do
      local capability=Capability --Ops.Auftrag#AUFTRAG.Capability
      if capability.MissionType==MissionType then
        return capability.Performance
      end
    end

  else
    self:E(self.lid.."ERROR: Payload is nil!")
  end

  return -1
end

--- Get mission types a payload can perform.
-- @param #AIRWING self
-- @param #AIRWING.Payload Payload The payload table.
-- @return #table Mission types.
function AIRWING:GetPayloadMissionTypes(Payload)

  local missiontypes={}

  for _,Capability in pairs(Payload.capabilities) do
    local capability=Capability --Ops.Auftrag#AUFTRAG.Capability
    table.insert(missiontypes, capability.MissionType)
  end

  return missiontypes
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Enhanced Ground Group.
--
-- ## Main Features:
--
--    * Patrol waypoints *ad infinitum*
--    * Easy change of ROE and alarm state, formation and other settings
--    * Dynamically add and remove waypoints
--    * Sophisticated task queueing system  (know when DCS tasks start and end)
--    * Convenient checks when the group enters or leaves a zone
--    * Detection events for new, known and lost units
--    * Simple LASER and IR-pointer setup
--    * Compatible with AUFTRAG class
--    * Many additional events that the mission designer can hook into
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/Armygroup).
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ==
-- @module Ops.ArmyGroup
-- @image OPS_ArmyGroup.png


--- ARMYGROUP class.
-- @type ARMYGROUP
-- @field #boolean adinfinitum Resume route at first waypoint when final waypoint is reached.
-- @field #boolean formationPerma Formation that is used permanently and overrules waypoint formations.
-- @field #boolean isMobile If true, group is mobile.
-- @field #ARMYGROUP.Target engage Engage target.
-- @field Core.Set#SET_ZONE retreatZones Set of retreat zones.
-- @field #boolean suppressOn Bla
-- @field #boolean isSuppressed Bla
-- @field #number TsuppressMin Bla
-- @field #number TsuppressMax Bla
-- @field #number TsuppressAve Bla
-- @extends Ops.OpsGroup#OPSGROUP

--- *Your soul may belong to Jesus, but your ass belongs to the marines.* -- Eugene B Sledge
--
-- ===
--
-- # The ARMYGROUP Concept
--
-- This class enhances ground groups.
--
-- @field #ARMYGROUP
ARMYGROUP = {
  ClassName       = "ARMYGROUP",
  formationPerma  = nil,
  engage          = {},
}

--- Engage Target.
-- @type ARMYGROUP.Target
-- @field Ops.Target#TARGET Target The target.
-- @field Core.Point#COORDINATE Coordinate Last known coordinate of the target.
-- @field Ops.OpsGroup#OPSGROUP.Waypoint Waypoint the waypoint created to go to the target.
-- @field #number Speed Speed in knots.
-- @field #string Formation Formation used in the engagement.
-- @field #number roe ROE backup.
-- @field #number alarmstate Alarm state backup.

--- Army Group version.
-- @field #string version
ARMYGROUP.version="1.0.3"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Suppression of fire.
-- TODO: Check if group is mobile.
-- TODO: F10 menu.
-- DONE: Retreat.
-- DONE: Rearm. Specify a point where to go and wait until ammo is full.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new ARMYGROUP class object.
-- @param #ARMYGROUP self
-- @param Wrapper.Group#GROUP group The GROUP object. Can also be given by its group name as `#string`.
-- @return #ARMYGROUP self
function ARMYGROUP:New(group)

  -- First check if we already have an OPS group for this group.
  local og=_DATABASE:GetOpsGroup(group)
  if og then
    og:I(og.lid..string.format("WARNING: OPS group already exists in data base!"))
    return og
  end

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, OPSGROUP:New(group)) -- #ARMYGROUP

  -- Set some string id for output to DCS.log file.
  self.lid=string.format("ARMYGROUP %s | ", self.groupname)

  -- Defaults
  self:SetDefaultROE()
  self:SetDefaultAlarmstate()
  self:SetDefaultEPLRS(self.isEPLRS)
  self:SetDefaultEmission()
  self:SetDetection()
  self:SetPatrolAdInfinitum(false)
  self:SetRetreatZones()

  -- Add FSM transitions.
  --                 From State  -->   Event      -->     To State
  self:AddTransition("*",             "FullStop",         "Holding")     -- Hold position.
  self:AddTransition("*",             "Cruise",           "Cruising")    -- Cruise along the given route of waypoints.

  self:AddTransition("*",             "RTZ",              "Returning")   -- Group is returning to (home) zone.
  self:AddTransition("Holding",       "Returned",         "Returned")    -- Group is returned to (home) zone, e.g. when unloaded from carrier.
  self:AddTransition("Returning",     "Returned",         "Returned")    -- Group is returned to (home) zone.

  self:AddTransition("*",             "Detour",           "OnDetour")    -- Make a detour to a coordinate and resume route afterwards.
  self:AddTransition("OnDetour",      "DetourReached",    "Cruising")    -- Group reached the detour coordinate.

  self:AddTransition("*",             "Retreat",          "Retreating")  -- Order a retreat.
  self:AddTransition("Retreating",    "Retreated",        "Retreated")   -- Group retreated.

  self:AddTransition("*",             "Suppressed",       "*")           -- Group is suppressed
  self:AddTransition("*",             "Unsuppressed",     "*")           -- Group is unsuppressed.

  self:AddTransition("Cruising",      "EngageTarget",     "Engaging")    -- Engage a target from Cruising state
  self:AddTransition("Holding",       "EngageTarget",     "Engaging")    -- Engage a target from Holding state
  self:AddTransition("OnDetour",      "EngageTarget",     "Engaging")    -- Engage a target from OnDetour state
  self:AddTransition("Engaging",      "Disengage",        "Cruising")    -- Disengage and back to cruising.

  self:AddTransition("*",             "Rearm",            "Rearm")       -- Group is send to a coordinate and waits until ammo is refilled.
  self:AddTransition("Rearm",         "Rearming",         "Rearming")    -- Group has arrived at the rearming coodinate and is waiting to be fully rearmed.
  self:AddTransition("*",             "Rearmed",          "Cruising")    -- Group was rearmed.

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Cruise".
  -- @function [parent=#ARMYGROUP] Cruise
  -- @param #ARMYGROUP self
  -- @param #number Speed Speed in knots until next waypoint is reached.
  -- @param #number Formation Formation.

  --- Triggers the FSM event "Cruise" after a delay.
  -- @function [parent=#ARMYGROUP] __Cruise
  -- @param #ARMYGROUP self
  -- @param #number delay Delay in seconds.
  -- @param #number Speed Speed in knots until next waypoint is reached.
  -- @param #number Formation Formation.

  --- On after "Cruise" event.
  -- @function [parent=#ARMYGROUP] OnAfterCruise
  -- @param #ARMYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number Speed Speed in knots until next waypoint is reached.
  -- @param #number Formation Formation.


  --- Triggers the FSM event "FullStop".
  -- @function [parent=#ARMYGROUP] FullStop
  -- @param #ARMYGROUP self

  --- Triggers the FSM event "FullStop" after a delay.
  -- @function [parent=#ARMYGROUP] __FullStop
  -- @param #ARMYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "FullStop" event.
  -- @function [parent=#ARMYGROUP] OnAfterFullStop
  -- @param #ARMYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "RTZ".
  -- @function [parent=#ARMYGROUP] RTZ
  -- @param #ARMYGROUP self

  --- Triggers the FSM event "RTZ" after a delay.
  -- @function [parent=#ARMYGROUP] __RTZ
  -- @param #ARMYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "RTZ" event.
  -- @function [parent=#ARMYGROUP] OnAfterRTZ
  -- @param #ARMYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Returned".
  -- @function [parent=#ARMYGROUP] Returned
  -- @param #ARMYGROUP self

  --- Triggers the FSM event "Returned" after a delay.
  -- @function [parent=#ARMYGROUP] __Returned
  -- @param #ARMYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "Returned" event.
  -- @function [parent=#ARMYGROUP] OnAfterReturned
  -- @param #ARMYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Detour".
  -- @function [parent=#ARMYGROUP] Detour
  -- @param #ARMYGROUP self

  --- Triggers the FSM event "Detour" after a delay.
  -- @function [parent=#ARMYGROUP] __Detour
  -- @param #ARMYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "Detour" event.
  -- @function [parent=#ARMYGROUP] OnAfterDetour
  -- @param #ARMYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "DetourReached".
  -- @function [parent=#ARMYGROUP] DetourReached
  -- @param #ARMYGROUP self

  --- Triggers the FSM event "DetourReached" after a delay.
  -- @function [parent=#ARMYGROUP] __DetourReached
  -- @param #ARMYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "DetourReached" event.
  -- @function [parent=#ARMYGROUP] OnAfterDetourReached
  -- @param #ARMYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Retreat".
  -- @function [parent=#ARMYGROUP] Retreat
  -- @param #ARMYGROUP self
  -- @param Core.Zone#ZONE_BASE Zone (Optional) Zone where to retreat. Default is the closest retreat zone.
  -- @param #number Formation (Optional) Formation of the group.

  --- Triggers the FSM event "Retreat" after a delay.
  -- @function [parent=#ARMYGROUP] __Retreat
  -- @param #ARMYGROUP self
  -- @param Core.Zone#ZONE_BASE Zone (Optional) Zone where to retreat. Default is the closest retreat zone.
  -- @param #number Formation (Optional) Formation of the group.
  -- @param #number delay Delay in seconds.

  --- On after "Retreat" event.
  -- @function [parent=#ARMYGROUP] OnAfterRetreat
  -- @param #ARMYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Zone#ZONE_BASE Zone Zone where to retreat.
  -- @param #number Formation Formation of the group. Can be #nil.

  --- Triggers the FSM event "Retreated".
  -- @function [parent=#ARMYGROUP] Retreated
  -- @param #ARMYGROUP self

  --- Triggers the FSM event "Retreated" after a delay.
  -- @function [parent=#ARMYGROUP] __Retreated
  -- @param #ARMYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "Retreated" event.
  -- @function [parent=#ARMYGROUP] OnAfterRetreated
  -- @param #ARMYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "EngageTarget".
  -- @function [parent=#ARMYGROUP] EngageTarget
  -- @param #ARMYGROUP self
  -- @param Ops.Target#TARGET Target The target to be engaged. Can also be a GROUP or UNIT object.
  -- @param #number Speed Speed in knots.
  -- @param #string Formation Formation used in the engagement.

  --- Triggers the FSM event "EngageTarget" after a delay.
  -- @function [parent=#ARMYGROUP] __EngageTarget
  -- @param #ARMYGROUP self
  -- @param #number delay Delay in seconds.
  -- @param Wrapper.Group#GROUP Group the group to be engaged.
  -- @param #number Speed Speed in knots.
  -- @param #string Formation Formation used in the engagement.


  --- On after "EngageTarget" event.
  -- @function [parent=#ARMYGROUP] OnAfterEngageTarget
  -- @param #ARMYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Group#GROUP Group the group to be engaged.
  -- @param #number Speed Speed in knots.
  -- @param #string Formation Formation used in the engagement.


  --- Triggers the FSM event "Disengage".
  -- @function [parent=#ARMYGROUP] Disengage
  -- @param #ARMYGROUP self

  --- Triggers the FSM event "Disengage" after a delay.
  -- @function [parent=#ARMYGROUP] __Disengage
  -- @param #ARMYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "Disengage" event.
  -- @function [parent=#ARMYGROUP] OnAfterDisengage
  -- @param #ARMYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Rearm".
  -- @function [parent=#ARMYGROUP] Rearm
  -- @param #ARMYGROUP self
  -- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm.
  -- @param #number Formation Formation of the group.

  --- Triggers the FSM event "Rearm" after a delay.
  -- @function [parent=#ARMYGROUP] __Rearm
  -- @param #ARMYGROUP self
  -- @param #number delay Delay in seconds.
  -- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm.
  -- @param #number Formation Formation of the group.

  --- On after "Rearm" event.
  -- @function [parent=#ARMYGROUP] OnAfterRearm
  -- @param #ARMYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm.
  -- @param #number Formation Formation of the group.


  --- Triggers the FSM event "Rearming".
  -- @function [parent=#ARMYGROUP] Rearming
  -- @param #ARMYGROUP self

  --- Triggers the FSM event "Rearming" after a delay.
  -- @function [parent=#ARMYGROUP] __Rearming
  -- @param #ARMYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "Rearming" event.
  -- @function [parent=#ARMYGROUP] OnAfterRearming
  -- @param #ARMYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Rearmed".
  -- @function [parent=#ARMYGROUP] Rearmed
  -- @param #ARMYGROUP self

  --- Triggers the FSM event "Rearmed" after a delay.
  -- @function [parent=#ARMYGROUP] __Rearmed
  -- @param #ARMYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "Rearmed" event.
  -- @function [parent=#ARMYGROUP] OnAfterRearmed
  -- @param #ARMYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  -- TODO: Add pseudo functions.

  -- Init waypoints.
  self:_InitWaypoints()

  -- Initialize the group.
  self:_InitGroup()

  -- Handle events:
  self:HandleEvent(EVENTS.Birth,      self.OnEventBirth)
  self:HandleEvent(EVENTS.Dead,       self.OnEventDead)
  self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit)
  self:HandleEvent(EVENTS.UnitLost,   self.OnEventRemoveUnit)
  self:HandleEvent(EVENTS.Hit,        self.OnEventHit)

  -- Start the status monitoring.
  self.timerStatus=TIMER:New(self.Status, self):Start(1, 30)

  -- Start queue update timer.
  self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5)

  -- Start check zone timer.
  self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(2, 30)

  -- Add OPSGROUP to _DATABASE.
  _DATABASE:AddOpsGroup(self)

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Group patrols ad inifintum. If the last waypoint is reached, it will go to waypoint one and repeat its route.
-- @param #ARMYGROUP self
-- @param #boolean switch If true or nil, patrol until the end of time. If false, go along the waypoints once and stop.
-- @return #ARMYGROUP self
function ARMYGROUP:SetPatrolAdInfinitum(switch)
  if switch==false then
    self.adinfinitum=false
  else
    self.adinfinitum=true
  end
  return self
end

--- Get coordinate of the closest road.
-- @param #ARMYGROUP self
-- @return Core.Point#COORDINATE Coordinate of a road closest to the group.
function ARMYGROUP:GetClosestRoad()
  local coord=self:GetCoordinate():GetClosestPointToRoad()
  return coord
end

--- Get 2D distance to the closest road.
-- @param #ARMYGROUP self
-- @return #number Distance in meters to the closest road.
function ARMYGROUP:GetClosestRoadDist()
  local road=self:GetClosestRoad()
  if road then
    local dist=road:Get2DDistance(self:GetCoordinate())
    return dist
  end
  return math.huge
end


--- Add a *scheduled* task to fire at a given coordinate.
-- @param #ARMYGROUP self
-- @param Core.Point#COORDINATE Coordinate Coordinate of the target.
-- @param #string Clock Time when to start the attack.
-- @param #number Radius Radius in meters. Default 100 m.
-- @param #number Nshots Number of shots to fire. Default 3.
-- @param #number WeaponType Type of weapon. Default auto.
-- @param #number Prio Priority of the task.
-- @return Ops.OpsGroup#OPSGROUP.Task The task table.
function ARMYGROUP:AddTaskFireAtPoint(Coordinate, Clock, Radius, Nshots, WeaponType, Prio)

  Coordinate=self:_CoordinateFromObject(Coordinate)

  local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType)

  local task=self:AddTask(DCStask, Clock, nil, Prio)

  return task
end

--- Add a *scheduled* task to fire at a given coordinate.
-- @param #ARMYGROUP self
-- @param #string Clock Time when to start the attack.
-- @param #number Heading Heading min in Degrees.
-- @param #number Alpha Shooting angle in Degrees.
-- @param #number Altitude Altitude in meters.
-- @param #number Radius Radius in meters. Default 100 m.
-- @param #number Nshots Number of shots to fire. Default nil.
-- @param #number WeaponType Type of weapon. Default auto.
-- @param #number Prio Priority of the task.
-- @return Ops.OpsGroup#OPSGROUP.Task The task table.
function ARMYGROUP:AddTaskBarrage(Clock, Heading, Alpha, Altitude, Radius, Nshots, WeaponType, Prio)

  Heading=Heading or 0

  Alpha=Alpha or 60

  Altitude=Altitude or 100

  local distance=Altitude/math.tan(math.rad(Alpha))

  local a=self:GetVec2()

  local vec2=UTILS.Vec2Translate(a, distance, Heading)

  --local coord=COORDINATE:NewFromVec2(vec2):MarkToAll("Fire At Point",ReadOnly,Text)

  local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, vec2, Radius, Nshots, WeaponType, Altitude)

  local task=self:AddTask(DCStask, Clock, nil, Prio)

  return task
end

--- Add a *waypoint* task to fire at a given coordinate.
-- @param #ARMYGROUP self
-- @param Core.Point#COORDINATE Coordinate Coordinate of the target.
-- @param Ops.OpsGroup#OPSGROUP.Waypoint Waypoint Where the task is executed. Default is next waypoint.
-- @param #number Radius Radius in meters. Default 100 m.
-- @param #number Nshots Number of shots to fire. Default 3.
-- @param #number WeaponType Type of weapon. Default auto.
-- @param #number Prio Priority of the task.
-- @return Ops.OpsGroup#OPSGROUP.Task The task table.
function ARMYGROUP:AddTaskWaypointFireAtPoint(Coordinate, Waypoint, Radius, Nshots, WeaponType, Prio)

  Coordinate=self:_CoordinateFromObject(Coordinate)

  Waypoint=Waypoint or self:GetWaypointNext()

  local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType)

  local task=self:AddTaskWaypoint(DCStask, Waypoint, nil, Prio)

  return task
end

--- Add a *scheduled* task.
-- @param #ARMYGROUP self
-- @param Wrapper.Group#GROUP TargetGroup Target group.
-- @param #number WeaponExpend How much weapons does are used.
-- @param #number WeaponType Type of weapon. Default auto.
-- @param #string Clock Time when to start the attack.
-- @param #number Prio Priority of the task.
-- @return Ops.OpsGroup#OPSGROUP.Task The task table.
function ARMYGROUP:AddTaskAttackGroup(TargetGroup, WeaponExpend, WeaponType, Clock, Prio)

  local DCStask=CONTROLLABLE.TaskAttackGroup(nil, TargetGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit, GroupAttack)

  local task=self:AddTask(DCStask, Clock, nil, Prio)

  return task
end

--- Add a *scheduled* task to transport group(s).
-- @param #ARMYGROUP self
-- @param Core.Set#SET_GROUP GroupSet Set of cargo groups. Can also be a singe @{Wrapper.Group#GROUP} object.
-- @param Core.Zone#ZONE PickupZone Zone where the cargo is picked up.
-- @param Core.Zone#ZONE DeployZone Zone where the cargo is delivered to.
-- @param #string Clock Time when to start the attack.
-- @param #number Prio Priority of the task.
-- @return Ops.OpsGroup#OPSGROUP.Task The task table.
function ARMYGROUP:AddTaskCargoGroup(GroupSet, PickupZone, DeployZone, Clock, Prio)

  local DCStask={}
  DCStask.id="CargoTransport"
  DCStask.params={}
  DCStask.params.cargoqueu=1

  local task=self:AddTask(DCStask, Clock, nil, Prio)

  return task
end

--- Define a set of possible retreat zones.
-- @param #ARMYGROUP self
-- @param Core.Set#SET_ZONE RetreatZoneSet The retreat zone set. Default is an empty set.
-- @return #ARMYGROUP self
function ARMYGROUP:SetRetreatZones(RetreatZoneSet)
  self.retreatZones=RetreatZoneSet or SET_ZONE:New()
  return self
end

--- Add a zone to the retreat zone set.
-- @param #ARMYGROUP self
-- @param Core.Zone#ZONE_BASE RetreatZone The retreat zone.
-- @return #ARMYGROUP self
function ARMYGROUP:AddRetreatZone(RetreatZone)
  self.retreatZones:AddZone(RetreatZone)
  return self
end

--- Set suppression on. average, minimum and maximum time a unit is suppressed each time it gets hit.
-- @param #ARMYGROUP self
-- @param #number Tave Average time [seconds] a group will be suppressed. Default is 15 seconds.
-- @param #number Tmin (Optional) Minimum time [seconds] a group will be suppressed. Default is 5 seconds.
-- @param #number Tmax (Optional) Maximum time a group will be suppressed. Default is 25 seconds.
-- @return #ARMYGROUP self
function ARMYGROUP:SetSuppressionOn(Tave, Tmin, Tmax)

  -- Activate suppression.
  self.suppressionOn=true

  -- Minimum suppression time is input or default 5 sec (but at least 1 second).
  self.TsuppressMin=Tmin or 1
  self.TsuppressMin=math.max(self.TsuppressMin, 1)

  -- Maximum suppression time is input or default but at least Tmin.
  self.TsuppressMax=Tmax or 15
  self.TsuppressMax=math.max(self.TsuppressMax, self.TsuppressMin)

  -- Expected suppression time is input or default but at leat Tmin and at most Tmax.
  self.TsuppressAve=Tave or 10
  self.TsuppressAve=math.max(self.TsuppressMin)
  self.TsuppressAve=math.min(self.TsuppressMax)

  -- Debug Info
  self:T(self.lid..string.format("Set ave suppression time to %d seconds.", self.TsuppressAve))
  self:T(self.lid..string.format("Set min suppression time to %d seconds.", self.TsuppressMin))
  self:T(self.lid..string.format("Set max suppression time to %d seconds.", self.TsuppressMax))

  return self
end

--- Set suppression off.
-- @param #ARMYGROUP self
-- @return #ARMYGROUP self
function ARMYGROUP:SetSuppressionOff()
  -- Activate suppression.
  self.suppressionOn=false
end


--- Check if the group is currently holding its positon.
-- @param #ARMYGROUP self
-- @return #boolean If true, group was ordered to hold.
function ARMYGROUP:IsHolding()
  return self:Is("Holding")
end

--- Check if the group is currently cruising.
-- @param #ARMYGROUP self
-- @return #boolean If true, group cruising.
function ARMYGROUP:IsCruising()
  return self:Is("Cruising")
end

--- Check if the group is currently on a detour.
-- @param #ARMYGROUP self
-- @return #boolean If true, group is on a detour.
function ARMYGROUP:IsOnDetour()
  return self:Is("OnDetour")
end

--- Check if the group is ready for combat. I.e. not reaming, retreating, retreated, out of ammo or engaging.
-- @param #ARMYGROUP self
-- @return #boolean If true, group is on a combat ready.
function ARMYGROUP:IsCombatReady()
  local combatready=true

  if self:IsRearming() or self:IsRetreating() or self:IsOutOfAmmo() or self:IsEngaging() or self:IsDead() or self:IsStopped() or self:IsInUtero() then
    combatready=false
  end

  if self:IsPickingup() or self:IsLoading() or self:IsTransporting() or self:IsLoaded() or self:IsCargo() or self:IsCarrier() then
    combatready=false
  end

  return combatready
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Update status.
-- @param #ARMYGROUP self
function ARMYGROUP:Status()

  -- FSM state.
  local fsmstate=self:GetState()

  -- Is group alive?
  local alive=self:IsAlive()

  -- Check that group EXISTS and is ACTIVE.
  if alive then

    -- Update position etc.
    self:_UpdatePosition()

    -- Check if group has detected any units.
    self:_CheckDetectedUnits()

    -- Check ammo status.
    self:_CheckAmmoStatus()

    -- Check damage of elements and group.
    self:_CheckDamage()

    -- Check if group got stuck.
    self:_CheckStuck()

    -- Update engagement.
    if self:IsEngaging() then
      self:_UpdateEngageTarget()
    end

    -- Check if group is waiting.
    if self:IsWaiting() then
      if self.Twaiting and self.dTwait then
        if timer.getAbsTime()>self.Twaiting+self.dTwait then
          self.Twaiting=nil
          self.dTwait=nil
          if self:_CountPausedMissions()>0 then
            self:UnpauseMission()
          else
            self:Cruise()
          end
        end
      end
    end


    -- Get current mission (if any).
    local mission=self:GetMissionCurrent()

    -- If mission, check if DCS task needs to be updated.
    if mission and mission.updateDCSTask  then

      if mission.type==AUFTRAG.Type.CAPTUREZONE then

        -- Get task.
        local Task=mission:GetGroupWaypointTask(self)

        -- Update task: Engage or get new zone.
        if mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING or  mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.STARTED then
          self:_UpdateTask(Task, mission)
        end

      end

    end

  else
    -- Check damage of elements and group.
    self:_CheckDamage()
  end

  -- Check that group EXISTS.
  if alive~=nil then

    if self.verbose>=1 then

      -- Number of elements.
      local nelem=self:CountElements()
      local Nelem=#self.elements

      -- Get number of tasks and missions.
      local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks()
      local nMissions=self:CountRemainingMissison()

      -- ROE and Alarm State.
      local roe=self:GetROE() or -1
      local als=self:GetAlarmstate() or -1

      -- Waypoint stuff.
      local wpidxCurr=self.currentwp
      local wpuidCurr=self:GetWaypointUIDFromIndex(wpidxCurr) or 0
      local wpidxNext=self:GetWaypointIndexNext() or 0
      local wpuidNext=self:GetWaypointUIDFromIndex(wpidxNext) or 0
      local wpN=#self.waypoints or 0
      local wpF=tostring(self.passedfinalwp)

      -- Speed.
      local speed=UTILS.MpsToKnots(self.velocity or 0)
      local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed())

      -- Altitude.
      local alt=self.position and self.position.y or 0

      -- Heading in degrees.
      local hdg=self.heading or 0

      -- TODO: GetFormation function.
      local formation=self.option.Formation or "unknown"

      -- Life points.
      local life=self.life or 0

      -- Total ammo.
      local ammo=self:GetAmmoTot().Total

      -- Detected units.
      local ndetected=self.detectionOn and tostring(self.detectedunits:Count()) or "Off"

      -- Get cargo weight.
      local cargo=0
      for _,_element in pairs(self.elements) do
        local element=_element --Ops.OpsGroup#OPSGROUP.Element
        cargo=cargo+element.weightCargo
      end

      -- Info text.
      local text=string.format("%s [%d/%d]: ROE/AS=%d/%d | T/M=%d/%d | Wp=%d[%d]-->%d[%d]/%d [%s] | Life=%.1f | v=%.1f (%d) [%s] | Hdg=%03d | Ammo=%d | Detect=%s | Cargo=%.1f",
      fsmstate, nelem, Nelem, roe, als, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, wpN, wpF, life, speed, speedEx, formation, hdg, ammo, ndetected, cargo)
      self:I(self.lid..text)

    end

  else

    -- Info text.
    if self.verbose>=1 then
      local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive()))
      self:I(self.lid..text)
    end

  end

  ---
  -- Elements
  ---

  if self.verbose>=2 then
    local text="Elements:"
    for i,_element in pairs(self.elements) do
      local element=_element --Ops.OpsGroup#OPSGROUP.Element

      local name=element.name
      local status=element.status
      local unit=element.unit
      local life,life0=self:GetLifePoints(element)

      local life0=element.life0

      -- Get ammo.
      local ammo=self:GetAmmoElement(element)

      -- Output text for element.
      text=text..string.format("\n[%d] %s: status=%s, life=%.1f/%.1f, guns=%d, rockets=%d, bombs=%d, missiles=%d, cargo=%d/%d kg",
      i, name, status, life, life0, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, element.weightCargo, element.weightMaxCargo)
    end
    if #self.elements==0 then
      text=text.." none!"
    end
    self:T(self.lid..text)
  end

  ---
  -- Engage Detected Targets
  ---
  if self:IsCruising() and self.detectionOn and self.engagedetectedOn then

    local targetgroup, targetdist=self:_GetDetectedTarget()

    -- If we found a group, we engage it.
    if targetgroup then
      self:T(self.lid..string.format("Engaging target group %s at distance %d meters", targetgroup:GetName(), targetdist))
      self:EngageTarget(targetgroup)
    end

  end


  ---
  -- Cargo
  ---

  self:_CheckCargoTransport()


  ---
  -- Tasks & Missions
  ---

  self:_PrintTaskAndMissionStatus()

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- DCS Events ==> See OPSGROUP
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "ElementSpawned" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The group element.
function ARMYGROUP:onafterElementSpawned(From, Event, To, Element)
  self:T(self.lid..string.format("Element spawned %s", Element.name))

  -- Set element status.
  self:_UpdateStatus(Element, OPSGROUP.ElementStatus.SPAWNED)

end

--- On after "Spawned" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARMYGROUP:onafterSpawned(From, Event, To)
  self:T(self.lid..string.format("Group spawned!"))

  -- Debug info.
  if self.verbose>=1 then
    local text=string.format("Initialized Army Group %s:\n", self.groupname)
    text=text..string.format("Unit type    = %s\n", self.actype)
    text=text..string.format("Speed max    = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax))
    text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise))
    text=text..string.format("Weight       = %.1f kg\n", self:GetWeightTotal())
    text=text..string.format("Cargo bay    = %.1f kg\n", self:GetFreeCargobay())
    text=text..string.format("Has EPLRS    = %s\n", tostring(self.isEPLRS))
    text=text..string.format("Elements     = %d\n", #self.elements)
    text=text..string.format("Waypoints    = %d\n", #self.waypoints)
    text=text..string.format("Radio        = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On))
    text=text..string.format("Ammo         = %d (G=%d/R=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles)
    text=text..string.format("FSM state    = %s\n", self:GetState())
    text=text..string.format("Is alive     = %s\n", tostring(self:IsAlive()))
    text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated()))
    self:I(self.lid..text)
  end

  -- Update position.
  self:_UpdatePosition()

  -- Not dead or destroyed yet.
  self.isDead=false
  self.isDestroyed=false

  if self.isAI then

    -- Set default ROE.
    self:SwitchROE(self.option.ROE)

    -- Set default Alarm State.
    self:SwitchAlarmstate(self.option.Alarm)

    -- Set emission.
    self:SwitchEmission(self.option.Emission)

    -- Set default EPLRS.
    self:SwitchEPLRS(self.option.EPLRS)

    -- Set default Invisible.
    self:SwitchInvisible(self.option.Invisible)

    -- Set default Immortal.
    self:SwitchImmortal(self.option.Immortal)

    -- Set TACAN to default.
    self:_SwitchTACAN()

    -- Turn on the radio.
    if self.radioDefault then
      self:SwitchRadio(self.radioDefault.Freq, self.radioDefault.Modu)
    else
      self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, true)
    end

    -- Formation
    if not self.option.Formation then
      -- Will be set in update route.
      --self.option.Formation=self.optionDefault.Formation
    end

    -- Number of waypoints.
    local Nwp=#self.waypoints

    -- Update route.
    if Nwp>1 and self.isMobile then
      self:T(self.lid..string.format("Got %d waypoints on spawn ==> Cruise in -1.0 sec!", Nwp))
      local wp=self:GetWaypointNext()
      self.option.Formation=wp.action
      --self:__Cruise(-1, nil, self.option.Formation)
      self:__Cruise(-1)
    else
      self:T(self.lid.."No waypoints on spawn ==> Full Stop!")
      self:FullStop()
    end

  end

end

--- On before "UpdateRoute" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last.
-- @param #number N Waypoint  Max waypoint index to be included in the route. Default is the final waypoint.
-- @param #number Speed Speed in knots. Default cruise speed.
-- @param #number Formation Formation of the group.
function ARMYGROUP:onbeforeUpdateRoute(From, Event, To, n, N, Speed, Formation)

  -- Is transition allowed? We assume yes until proven otherwise.
  local allowed=true
  local trepeat=nil

  if self:IsWaiting() then
    self:T(self.lid.."Update route denied. Group is WAITING!")
    return false
  elseif self:IsInUtero() then
    self:T(self.lid.."Update route denied. Group is INUTERO!")
    return false
  elseif self:IsDead() then
    self:T(self.lid.."Update route denied. Group is DEAD!")
    return false
  elseif self:IsStopped() then
    self:T(self.lid.."Update route denied. Group is STOPPED!")
    return false
  elseif self:IsHolding() then
    self:T(self.lid.."Update route denied. Group is holding position!")
    return false
  elseif self:IsEngaging() then
    self:T(self.lid.."Update route allowed. Group is engaging!")
    return true
  end

  -- Check for a current task.
  if self.taskcurrent>0 then

    -- Get the current task. Must not be executing already.
    local task=self:GetTaskByID(self.taskcurrent)

    if task then
      if task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then
        -- For patrol zone, we need to allow the update as we insert new waypoints.
        self:T2(self.lid.."Allowing update route for Task: PatrolZone")
      elseif task.dcstask.id==AUFTRAG.SpecialTask.RECON then
        -- For recon missions, we need to allow the update as we insert new waypoints.
        self:T2(self.lid.."Allowing update route for Task: ReconMission")
      elseif task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then
        -- For relocate
        self:T2(self.lid.."Allowing update route for Task: Relocate Cohort")
      elseif task.dcstask.id==AUFTRAG.SpecialTask.REARMING then
        -- For relocate
        self:T2(self.lid.."Allowing update route for Task: Rearming")
      else
        local taskname=task and task.description or "No description"
        self:T(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s", self.taskcurrent, tostring(taskname)))
        allowed=false
      end
    else
      -- Now this can happen, if we directly use TaskExecute as the task is not in the task queue and cannot be removed. Therefore, also directly executed tasks should be added to the queue!
      self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d (>0!) but no task?!", self.taskcurrent))
      -- Anyhow, a task is running so we do not allow to update the route!
      allowed=false
    end
  end

  -- Not good, because mission will never start. Better only check if there is a current task!
  --if self.currentmission then
  --end

  -- Only AI flights.
  if not self.isAI then
    allowed=false
  end

  -- Debug info.
  self:T2(self.lid..string.format("Onbefore Updateroute in state %s: allowed=%s (repeat in %s)", self:GetState(), tostring(allowed), tostring(trepeat)))

  -- Try again?
  if trepeat then
    self:__UpdateRoute(trepeat, n)
  end

  return allowed
end

--- On after "UpdateRoute" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last.
-- @param #number N Waypoint  Max waypoint index to be included in the route. Default is the final waypoint.
-- @param #number Speed Speed in knots. Default cruise speed.
-- @param #number Formation Formation of the group.
function ARMYGROUP:onafterUpdateRoute(From, Event, To, n, N, Speed, Formation)

  -- Update route from this waypoint number onwards.
  n=n or self:GetWaypointIndexNext(self.adinfinitum)

  -- Max index.
  N=N or #self.waypoints
  N=math.min(N, #self.waypoints)

  -- Debug info.
  local text=string.format("Update route state=%s: n=%s, N=%s, Speed=%s, Formation=%s", self:GetState(), tostring(n), tostring(N), tostring(Speed), tostring(Formation))
  self:T(self.lid..text)

  -- Waypoints including addtional wp onroad.
  local waypoints={}

  -- Next waypoint.
  local wp=self.waypoints[n] --Ops.OpsGroup#OPSGROUP.Waypoint

  -- Current position.
  local coordinate=self:GetCoordinate()

  -- Road coordinate.
  local coordRoad=coordinate:GetClosestPointToRoad()

  -- Road distance.
  local roaddist=coordinate:Get2DDistance(coordRoad)

  -- Formation at the current position.
  local formation0=wp.action
  if formation0==ENUMS.Formation.Vehicle.OnRoad then
    -- Next waypoint is on road. Check if we are already on road.
    if roaddist>10 then
      -- Currently off road ==> we add an on road WP later.
      formation0=ENUMS.Formation.Vehicle.OffRoad
    else
      -- Already on road. We won't add an extra on road WP.
      formation0=ENUMS.Formation.Vehicle.OnRoad
    end
  end

  -- Debug
  --env.info(self.lid.."FF formation0="..tostring(formation0))

  -- Current point.
  local current=coordinate:WaypointGround(UTILS.MpsToKmph(self.speedWp), formation0)
  table.insert(waypoints, 1, current)

  -- Check if route consists of more than one waypoint (otherwise we have no previous waypoint)
  if N-n>0 then

    -- Loop over waypoints.
    for j=n, N do

      -- Index of previous waypoint.
      local i=j-1

      -- If we go to the first waypoint j=1 ==> i=0, so we take the last waypoint passed. E.g. when adinfinitum and passed final waypoint.
      if i==0 then
        i=self.currentwp
      end

      -- Next waypoint. We create a copy because we need to modify it.
      local wp=UTILS.DeepCopy(self.waypoints[j]) --Ops.OpsGroup#OPSGROUP.Waypoint

      -- Previous waypoint. Index is i and not i-1 because we added the current position.
      local wp0=self.waypoints[i] --Ops.OpsGroup#OPSGROUP.Waypoint

      -- Debug
      if false and self.attribute==GROUP.Attribute.GROUND_APC then
        local text=string.format("FF Update: i=%d, wp[i]=%s, wp[i-1]=%s", i, wp.action, wp0.action)
        env.info(text)
      end

      -- Speed.
      if Speed then
        wp.speed=UTILS.KnotsToMps(tonumber(Speed))
      else
        -- Take default waypoint speed. But make sure speed>0 if patrol ad infinitum.
        if wp.speed<0.1 then
          wp.speed=UTILS.KmphToMps(self.speedCruise)
        end
      end

      -- Formation.
      if self.formationPerma then
        wp.action=self.formationPerma
      elseif Formation then
        wp.action=Formation
      end

      -- Add waypoint in between because this waypoint is "On Road" but lies "Off Road".
      if wp.action==ENUMS.Formation.Vehicle.OnRoad and wp0.roaddist>=0 then

        -- Add "On Road" waypoint in between.
        local wproad=wp0.roadcoord:WaypointGround(UTILS.MpsToKmph(wp.speed), ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint

        -- Debug
        --wp0.roadcoord:MarkToAll(self.lid.." Added road wp near "..tostring(wproad.action))

        -- Insert road waypoint.
        table.insert(waypoints, wproad)
      end

      -- Add waypoint in between because this waypoint is "On Road" but lies "Off Road".
      if wp.action==ENUMS.Formation.Vehicle.OnRoad and wp.roaddist>=0 then

        -- The real waypoint is actually off road.
        wp.action=ENUMS.Formation.Vehicle.OffRoad

        -- Add "On Road" waypoint in between.
        local wproad=wp.roadcoord:WaypointGround(UTILS.MpsToKmph(wp.speed), ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint

        -- Debug
        --wp.roadcoord:MarkToAll(self.lid.." Added road wp far "..tostring(wproad.action))

        -- Insert road waypoint.
        table.insert(waypoints, wproad)
      end

      -- Debug
      --wp.coordinate:MarkToAll(self.lid.." Added wp actual"..tostring(wp.action))

      -- Add waypoint.
      table.insert(waypoints, wp)
    end

  else

    ---
    -- This is the case, where we have only one WP left.
    -- Could be because we had only one WP and did a detour (temp waypoint, which was deleted).
    ---

    -- Next waypoint.
    local wp=UTILS.DeepCopy(self.waypoints[n]) --Ops.OpsGroup#OPSGROUP.Waypoint

    -- Speed.
    if wp.speed<0.1 then
      wp.speed=UTILS.KmphToMps(self.speedCruise)
    end

    -- Formation.
    local formation=wp.action
    if self.formationPerma then
      formation=self.formationPerma
    elseif Formation then
      formation=Formation
    end

    -- Debug
    --env.info(self.lid..string.format("FF Formation %s", formation))

    -- Add road waypoint.
    if formation==ENUMS.Formation.Vehicle.OnRoad then

      if roaddist>10 then

        -- Add "On Road" waypoint in between.
        local wproad=coordRoad:WaypointGround(UTILS.MpsToKmph(wp.speed), ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint

        -- Debug
        --coordRoad:MarkToAll(self.lid.." Added road wp near "..tostring(wp.action))

        -- Insert road waypoint.
        table.insert(waypoints, wproad)

      end

      if wp.roaddist>10 then

        -- Add "On Road" waypoint in between.
        local wproad=wp.roadcoord:WaypointGround(UTILS.MpsToKmph(wp.speed), ENUMS.Formation.Vehicle.OnRoad) --Ops.OpsGroup#OPSGROUP.Waypoint

        -- Debug
        --wp.roadcoord:MarkToAll(self.lid.." Added road wp far "..tostring(wp.action))

        -- Insert road waypoint.
        table.insert(waypoints, wproad)

      end

    end

    -- Waypoint set set to on-road but lies off-road. We set it to off-road. the on-road wp has been inserted.
    if wp.action==ENUMS.Formation.Vehicle.OnRoad and wp.roaddist>10 then
      wp.action=ENUMS.Formation.Vehicle.OffRoad
    end

    -- Debug
    --wp.coordinate:MarkToAll(self.lid.." Added coord "..tostring(wp.action))

    -- Add actual waypoint.
    table.insert(waypoints, wp)

  end

  -- First (next wp).
  local wp=waypoints[1] --Ops.OpsGroup#OPSGROUP.Waypoint

  -- Current set formation.
  self.option.Formation=wp.action

  -- Current set speed in m/s.
  self.speedWp=wp.speed
  self:T(self.lid..string.format("Expected/waypoint speed=%.1f m/s", self.speedWp))

  -- Debug output.
  if self.verbose>=10 then --or self.attribute==GROUP.Attribute.GROUND_APC then
    for i,_wp in pairs(waypoints) do
      local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint

      local text=string.format("WP #%d UID=%d Formation=%s: Speed=%d m/s, Alt=%d m, Type=%s", i, wp.uid and wp.uid or -1, wp.action, wp.speed, wp.alt, wp.type)

      local coord=COORDINATE:NewFromWaypoint(wp):MarkToAll(text)
      self:I(text)

    end
  end

  if self:IsEngaging() or not self.passedfinalwp then

    -- Debug info.
    self:T(self.lid..string.format("Updateing route: WP %d-->%d (%d/%d), Speed=%.1f knots, Formation=%s",
    self.currentwp, n, #waypoints, #self.waypoints, UTILS.MpsToKnots(self.speedWp), tostring(self.option.Formation)))

    -- Route group to all defined waypoints remaining.
    self:Route(waypoints)

  else

    ---
    -- Passed final WP ==> Full Stop
    ---

    self:T(self.lid..string.format("WARNING: Passed final WP when UpdateRoute() ==> Full Stop!"))
    self:FullStop()

  end

end

--- On after "GotoWaypoint" event. Group will got to the given waypoint and execute its route from there.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number UID The goto waypoint unique ID.
-- @param #number Speed (Optional) Speed to waypoint in knots.
-- @param #number Formation (Optional) Formation to waypoint.
function ARMYGROUP:onafterGotoWaypoint(From, Event, To, UID, Speed, Formation)

  local n=self:GetWaypointIndex(UID)

  if n then

    -- Speed to waypoint.
    Speed=Speed or self:GetSpeedToWaypoint(n)

    -- Update the route.
    self:__UpdateRoute(-0.01, n, nil, Speed, Formation)

  end

end

--- On after "Detour" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Coordinate Coordinate where to go.
-- @param #number Speed Speed in knots. Default cruise speed.
-- @param #number Formation Formation of the group.
-- @param #number ResumeRoute If true, resume route after detour point was reached. If false, the group will stop at the detour point and wait for futher commands.
function ARMYGROUP:onafterDetour(From, Event, To, Coordinate, Speed, Formation, ResumeRoute)

  for _,_wp in pairs(self.waypoints) do
    local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint
    if wp.detour then
      self:RemoveWaypointByID(wp.uid)
    end
  end

  -- Speed in knots.
  Speed=Speed or self:GetSpeedCruise()

  -- ID of current waypoint.
  local uid=self:GetWaypointCurrentUID()

  -- Add waypoint after current.
  local wp=self:AddWaypoint(Coordinate, Speed, uid, Formation, true)

  -- Set if we want to resume route after reaching the detour waypoint.
  if ResumeRoute then
    wp.detour=1
  else
    wp.detour=0
  end

end

--- On after "OutOfAmmo" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARMYGROUP:onafterOutOfAmmo(From, Event, To)
  self:T(self.lid..string.format("Group is out of ammo at t=%.3f", timer.getTime()))

  -- Get current task.
  local task=self:GetTaskCurrent()

  if task then
    if task.dcstask.id=="FireAtPoint" or task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then
      self:T(self.lid..string.format("Cancelling current %s task because out of ammo!", task.dcstask.id))
      self:TaskCancel(task)
    end
  end

  -- Fist, check if we want to rearm once out-of-ammo.
  --TODO: IsMobile() check
  if self.rearmOnOutOfAmmo then
    local truck, dist=self:FindNearestAmmoSupply(30)
    if truck then
      self:T(self.lid..string.format("Found Ammo Truck %s [%s]", truck:GetName(), truck:GetTypeName()))
      local Coordinate=truck:GetCoordinate()
      self:__Rearm(-1, Coordinate)
      return
    end
  end

  -- Second, check if we want to retreat once out of ammo.
  if self.retreatOnOutOfAmmo then
    self:T(self.lid.."Retreat on out of ammo")
    self:__Retreat(-1)
    return
  end

  -- Third, check if we want to RTZ once out of ammo (unless we have a rearming mission in the queue).
  if self.rtzOnOutOfAmmo and not self:IsMissionTypeInQueue(AUFTRAG.Type.REARMING) then
    self:T(self.lid.."RTZ on out of ammo")
    self:__RTZ(-1)
  end

end


--- On before "Rearm" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm.
-- @param #number Formation Formation of the group.
function ARMYGROUP:onbeforeRearm(From, Event, To, Coordinate, Formation)

  local dt=nil
  local allowed=true

  -- Pause current mission.
  if self:IsOnMission() then
    local mission=self:GetMissionCurrent()
    if mission and mission.type~=AUFTRAG.Type.REARMING then
      self:T(self.lid.."Rearm command but have current mission ==> Pausing mission!")
      self:PauseMission()
      dt=-0.1
      allowed=false
    else
      self:T(self.lid.."Rearm command and current mission is REARMING ==> Transition ALLOWED!")
    end
  end

  -- Disengage.
  if self:IsEngaging() then
    self:T(self.lid.."Rearm command but currently engaging ==> Disengage!")
    self:Disengage()
    dt=-0.1
    allowed=false
  end

  -- Check if coordinate is provided.
  if allowed and not Coordinate then
    local truck=self:FindNearestAmmoSupply()
    if truck and truck:IsAlive() then
      self:__Rearm(-0.1, truck:GetCoordinate(), Formation)
    end
    return false
  end

  -- Try again...
  if dt then
    self:T(self.lid..string.format("Trying Rearm again in %.2f sec", dt))
    self:__Rearm(dt, Coordinate, Formation)
    allowed=false
  end

  return allowed
end

--- On after "Rearm" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Coordinate Coordinate where to rearm.
-- @param #number Formation Formation of the group.
function ARMYGROUP:onafterRearm(From, Event, To, Coordinate, Formation)

  -- Debug info.
  self:T(self.lid..string.format("Group send to rearm"))

  -- ID of current waypoint.
  local uid=self:GetWaypointCurrentUID()

  -- Add waypoint after current.
  local wp=self:AddWaypoint(Coordinate, nil, uid, Formation, true)

  -- Set if we want to resume route after reaching the detour waypoint.
  wp.detour=0

end

--- On after "Rearmed" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARMYGROUP:onafterRearmed(From, Event, To)
  self:T(self.lid.."Group rearmed")

  -- Get Current mission.
  local mission=self:GetMissionCurrent()

  -- Check if this is a rearming mission.
  if mission and mission.type==AUFTRAG.Type.REARMING then

    -- Rearmed ==> Mission Done! This also checks if the group is done.
    self:MissionDone(mission)

  else

    -- Check group done.
    self:_CheckGroupDone(1)

  end

end

--- On before "RTZ" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE Zone The zone to return to.
-- @param #number Formation Formation of the group.
function ARMYGROUP:onbeforeRTZ(From, Event, To, Zone, Formation)
  self:T2(self.lid.."onbeforeRTZ")

  -- Zone.
  local zone=Zone or self.homezone

  if zone then

    if (not self.isMobile) and (not self:IsInZone(zone)) then
      self:Teleport(zone:GetCoordinate(), 0, true)
      self:__RTZ(-1, Zone, Formation)
      return false
    end

  else
    return false
  end

  return true
end

--- On after "RTZ" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE Zone The zone to return to.
-- @param #number Formation Formation of the group.
function ARMYGROUP:onafterRTZ(From, Event, To, Zone, Formation)
  self:T2(self.lid.."onafterRTZ")

  -- Zone.
  local zone=Zone or self.homezone

  -- Cancel all missions in the queue.
  self:CancelAllMissions()

  if zone then

    if self:IsInZone(zone) then
      self:Returned()
    else

      -- Debug info.
      self:T(self.lid..string.format("RTZ to Zone %s", zone:GetName()))

      local Coordinate=zone:GetRandomCoordinate()

      -- ID of current waypoint.
      local uid=self:GetWaypointCurrentUID()

      -- Add waypoint after current.
      local wp=self:AddWaypoint(Coordinate, nil, uid, Formation, true)

      -- Set if we want to resume route after reaching the detour waypoint.
      wp.detour=0

    end

  else
    self:T(self.lid.."ERROR: No RTZ zone given!")
  end

end

--- On after "Returned" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARMYGROUP:onafterReturned(From, Event, To)

  -- Debug info.
  self:T(self.lid..string.format("Group returned"))

  if self.legion then
    -- Debug info.
    self:T(self.lid..string.format("Adding group back to warehouse stock"))

    -- Add asset back in 10 seconds.
    self.legion:__AddAsset(10, self.group, 1)
  end

end

--- On after "Rearming" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARMYGROUP:onafterRearming(From, Event, To)

  -- Get current position.
  local pos=self:GetCoordinate()

  -- Create a new waypoint.
  local wp=pos:WaypointGround(0)

  -- Create new route consisting of only this position ==> Stop!
  self:Route({wp})

end

--- On before "Retreat" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE_BASE Zone (Optional) Zone where to retreat. Default is the closest retreat zone.
-- @param #number Formation (Optional) Formation of the group.
function ARMYGROUP:onbeforeRetreat(From, Event, To, Zone, Formation)

  if not Zone then

    local a=self:GetVec2()

    local distmin=math.huge
    local zonemin=nil
    for _,_zone in pairs(self.retreatZones:GetSet()) do
      local zone=_zone --Core.Zone#ZONE_BASE

      local b=zone:GetVec2()

      local dist=UTILS.VecDist2D(a, b)

      if dist<distmin then
        distmin=dist
        zonemin=zone
      end

    end

    if zonemin then
      self:__Retreat(0.1, zonemin, Formation)
    end

    return false
  end

  return true
end

--- On after "Retreat" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE_BASE Zone (Optional) Zone where to retreat. Default is the closest retreat zone.
-- @param #number Formation (Optional) Formation of the group.
function ARMYGROUP:onafterRetreat(From, Event, To, Zone, Formation)

  -- ID of current waypoint.
  local uid=self:GetWaypointCurrentUID()

  -- Get random coordinate of the zone.
  local Coordinate=Zone:GetRandomCoordinate()

  -- Debug info.
  self:T(self.lid..string.format("Retreating to zone %s", Zone:GetName()))

  -- Add waypoint after current.
  local wp=self:AddWaypoint(Coordinate, nil, uid, Formation, true)

  -- Set if we want to resume route after reaching the detour waypoint.
  wp.detour=0

  -- Cancel all missions.
  self:CancelAllMissions()

end

--- On after "Retreated" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARMYGROUP:onafterRetreated(From, Event, To)

  -- Get current position.
  local pos=self:GetCoordinate()

  -- Create a new waypoint.
  local wp=pos:WaypointGround(0)

  -- Create new route consisting of only this position ==> Stop!
  self:Route({wp})

end

--- On after "EngageTarget" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Group#GROUP Group the group to be engaged.
-- @param #number Speed Speed in knots.
-- @param #string Formation Formation used in the engagement. Default `ENUMS.Formation.Vehicle.Vee`.
function ARMYGROUP:onbeforeEngageTarget(From, Event, To, Target, Speed, Formation)

  local dt=nil
  local allowed=true

  local ammo=self:GetAmmoTot()

  if ammo.Total==0 then
    self:T(self.lid.."WARNING: Cannot engage TARGET because no ammo left!")
    return false
  end

  -- Get current mission.
  local mission=self:GetMissionCurrent()

  -- Pause current mission unless it uses the EngageTarget command.
  if mission and mission.type~=AUFTRAG.Type.GROUNDATTACK and mission.type~=AUFTRAG.Type.CAPTUREZONE then
    self:T(self.lid.."Engage command but have current mission ==> Pausing mission!")
    self:PauseMission()
    dt=-0.1
    allowed=false
  end

  -- Try again...
  if dt then
    self:T(self.lid..string.format("Trying Engage again in %.2f sec", dt))
    self:__EngageTarget(dt, Target)
    allowed=false
  end

  return allowed
end

--- On after "EngageTarget" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Target#TARGET Target The target to be engaged. Can also be a group or unit.
-- @param #number Speed Attack speed in knots.
-- @param #string Formation Formation used in the engagement. Default `ENUMS.Formation.Vehicle.Vee`.
function ARMYGROUP:onafterEngageTarget(From, Event, To, Target, Speed, Formation)
  self:T(self.lid.."Engaging Target")

  -- Make sure this is a target.
  if Target:IsInstanceOf("TARGET") then
    self.engage.Target=Target
  else
    self.engage.Target=TARGET:New(Target)
  end

  -- Target coordinate.
  self.engage.Coordinate=UTILS.DeepCopy(self.engage.Target:GetCoordinate())

  -- Get a coordinate close to the target.
  local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.95)

  -- Backup ROE and alarm state.
  self.engage.roe=self:GetROE()
  self.engage.alarmstate=self:GetAlarmstate()

  -- Switch ROE and alarm state.
  self:SwitchAlarmstate(ENUMS.AlarmState.Auto)
  self:SwitchROE(ENUMS.ROE.OpenFire)

  -- ID of current waypoint.
  local uid=self:GetWaypointCurrentUID()

  -- Set formation.
  self.engage.Formation=Formation or ENUMS.Formation.Vehicle.Vee

  -- Set speed.
  self.engage.Speed=Speed

  -- Add waypoint after current.
  self.engage.Waypoint=self:AddWaypoint(intercoord, self.engage.Speed, uid, self.engage.Formation, true)

  -- Set if we want to resume route after reaching the detour waypoint.
  self.engage.Waypoint.detour=1

end

--- Update engage target.
-- @param #ARMYGROUP self
function ARMYGROUP:_UpdateEngageTarget()

  if self.engage.Target and self.engage.Target:IsAlive() then

    -- Get current position vector.
    local vec3=self.engage.Target:GetVec3()

    if vec3 then

      -- Distance to last known position of target.
      local dist=UTILS.VecDist3D(vec3, self.engage.Coordinate:GetVec3())

      -- Check line of sight to target.
      local los=self:HasLoS(vec3)

      -- Check if target moved more than 100 meters or we do not have line of sight.
      if dist>100 or los==false then

        --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName())

        -- Update new position.
        self.engage.Coordinate:UpdateFromVec3(vec3)

        -- ID of current waypoint.
        local uid=self:GetWaypointCurrentUID()

        -- Remove current waypoint
        self:RemoveWaypointByID(self.engage.Waypoint.uid)

        local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9)

          -- Add waypoint after current.
        self.engage.Waypoint=self:AddWaypoint(intercoord, self.engage.Speed, uid, self.engage.Formation, true)

        -- Set if we want to resume route after reaching the detour waypoint.
        self.engage.Waypoint.detour=0

      end

    else

      -- Could not get position of target (not alive any more?) ==> Disengage.
      self:T(self.lid.."Could not get position of target ==> Disengage!")
      self:Disengage()

    end

  else

    -- Target not alive any more ==> Disengage.
    self:T(self.lid.."Target not ALIVE ==> Disengage!")
    self:Disengage()

  end

end

--- On after "Disengage" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARMYGROUP:onafterDisengage(From, Event, To)
  self:T(self.lid.."Disengage Target")

  -- Restore previous ROE and alarm state.
  self:SwitchROE(self.engage.roe)
  self:SwitchAlarmstate(self.engage.alarmstate)

  -- Get current task
  local task=self:GetTaskCurrent()

  -- Get if current task is ground attack.
  if task and task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK then
    self:T(self.lid.."Disengage with current task GROUNDATTACK ==> Task Done!")
    self:TaskDone(task)
  end

  -- Remove current waypoint
  if self.engage.Waypoint then
    self:RemoveWaypointByID(self.engage.Waypoint.uid)
  end

  -- Check group is done
  self:_CheckGroupDone(1)
end

--- On after "DetourReached" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARMYGROUP:onafterDetourReached(From, Event, To)
  self:T(self.lid.."Group reached detour coordinate")
end


--- On after "FullStop" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARMYGROUP:onafterFullStop(From, Event, To)

  -- Debug info.
  self:T(self.lid..string.format("Full stop!"))

  -- Get current position.
  local pos=self:GetCoordinate()

  -- Create a new waypoint.
  local wp=pos:WaypointGround(0)

  -- Create new route consisting of only this position ==> Stop!
  self:Route({wp})

end

--- On after "Cruise" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number Speed Speed in knots.
-- @param #number Formation Formation.
function ARMYGROUP:onafterCruise(From, Event, To, Speed, Formation)

  -- Not waiting anymore.
  self.Twaiting=nil
  self.dTwait=nil

  -- Debug info.
  self:T(self.lid..string.format("Cruise ==> Update route in 0.01 sec (speed=%s, formation=%s)", tostring(Speed), tostring(Formation)))

  -- Update route.
  self:__UpdateRoute(-0.01, nil, nil, Speed, Formation)

end

--- On after "Hit" event.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Unit#UNIT Enemy Unit that hit the element or `nil`.
function ARMYGROUP:onafterHit(From, Event, To, Enemy)
  self:T(self.lid..string.format("ArmyGroup hit by %s", Enemy and Enemy:GetName() or "unknown"))

  if self.suppressionOn then
    env.info(self.lid.."FF suppress")
    self:_Suppress()
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Routing
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Add an a waypoint to the route.
-- @param #ARMYGROUP self
-- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint.
-- @param #number Speed Speed in knots. Default is default cruise speed or 70% of max speed.
-- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint.
-- @param #string Formation Formation the group will use.
-- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call.
-- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table.
function ARMYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Formation, Updateroute)

  -- Debug info.
  self:T(self.lid..string.format("AddWaypoint Formation = %s", tostring(Formation)))

  -- Create coordinate.
  local coordinate=self:_CoordinateFromObject(Coordinate)

  -- Set waypoint index.
  local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID)

  -- Speed in knots.
  Speed=Speed or self:GetSpeedCruise()

  -- Formation.
  if not Formation then
    if self.formationPerma then
      Formation = self.formationPerma
    elseif self.optionDefault.Formation then
      Formation = self.optionDefault.Formation
    elseif self.option.Formation then
      Formation = self.option.Formation
    else
      -- Default formation is on road.
      Formation = ENUMS.Formation.Vehicle.OnRoad
    end
    self:T2(self.lid..string.format("Formation set to = %s", tostring(Formation)))
  end

  -- Create a Ground waypoint.
  local wp=coordinate:WaypointGround(UTILS.KnotsToKmph(Speed), Formation)

  -- Create waypoint data table.
  local waypoint=self:_CreateWaypoint(wp)

  -- Add waypoint to table.
  self:_AddWaypoint(waypoint, wpnumber)

  -- Get closest point to road.
  waypoint.roadcoord=coordinate:GetClosestPointToRoad(false)
  if waypoint.roadcoord then
    waypoint.roaddist=coordinate:Get2DDistance(waypoint.roadcoord)
  else
    waypoint.roaddist=1000*1000 --1000 km.
  end

  -- Debug info.
  self:T(self.lid..string.format("Adding waypoint UID=%d (index=%d), Speed=%.1f knots, Dist2Road=%d m, Action=%s", waypoint.uid, wpnumber, Speed, waypoint.roaddist, waypoint.action))

  -- Update route.
  if Updateroute==nil or Updateroute==true then
    self:__UpdateRoute(-0.01)
  end

  return waypoint
end

--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil.
-- @param #ARMYGROUP self
-- @param #table Template Template used to init the group. Default is `self.template`.
-- @param #number Delay Delay in seconds before group is initialized. Default `nil`, *i.e.* instantaneous.
-- @return #ARMYGROUP self
function ARMYGROUP:_InitGroup(Template, Delay)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, ARMYGROUP._InitGroup, self, Template, 0)
  else
  
    -- First check if group was already initialized.
    if self.groupinitialized then
      self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!")
      return
    end
  
    -- Get template of group.
    local template=Template or self:_GetTemplate()
  
    -- Ground are always AI.
    self.isAI=true
  
    -- Is (template) group late activated.
    self.isLateActivated=template.lateActivation
  
    -- Ground groups cannot be uncontrolled.
    self.isUncontrolled=false
  
    -- Max speed in km/h.
    self.speedMax=self.group:GetSpeedMax()
  
    -- Is group mobile?
    if self.speedMax and self.speedMax>3.6 then
      self.isMobile=true
    else
      self.isMobile=false
      self.speedMax = 0
    end
  
    -- Cruise speed in km/h
    self.speedCruise=self.speedMax*0.7
  
    -- Group ammo.
    self.ammo=self:GetAmmoTot()
  
    -- Radio parameters from template.
    self.radio.On=false  -- Radio is always OFF for ground.
    self.radio.Freq=133
    self.radio.Modu=radio.modulation.AM
  
    -- Set default radio.
    self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, self.radio.On)
  
    -- Get current formation from first waypoint.
    self.option.Formation=template.route.points[1].action
  
    -- Set default formation to "on road".
    self.optionDefault.Formation=ENUMS.Formation.Vehicle.OnRoad

    -- Default TACAN off.
    if not self.tacanDefault then
      self:SetDefaultTACAN(nil, nil, nil, nil, true)
    end
    if not self.tacan then
      self.tacan=UTILS.DeepCopy(self.tacanDefault)
    end

    -- Units of the group.
    local units=self.group:GetUnits()

    -- DCS group.
    local dcsgroup=Group.getByName(self.groupname)
    local size0=dcsgroup:getInitialSize()
    local u=dcsgroup:getUnits()

    -- Quick check.
    if #units~=size0 then
      self:T(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units! u=%d", #units, size0, #u))
    end

    -- Add elemets.
    for _,unit in pairs(units) do
      local unitname=unit:GetName()
      self:_AddElementByName(unitname)
    end

    -- Init done.
    self.groupinitialized=true
  end

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Option Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Switch to a specific formation.
-- @param #ARMYGROUP self
-- @param #number Formation New formation the group will fly in. Default is the setting of `SetDefaultFormation()`.
-- @param #boolean Permanently If true, formation always used from now on.
-- @param #boolean NoRouteUpdate If true, route is not updated.
-- @return #ARMYGROUP self
function ARMYGROUP:SwitchFormation(Formation, Permanently, NoRouteUpdate)

  if self:IsAlive() or self:IsInUtero() then

    Formation=Formation or (self.optionDefault.Formation or "Off road")
    Permanently = Permanently or false

    if Permanently then
      self.formationPerma=Formation
    else
      self.formationPerma=nil
    end

    -- Set current formation.
    self.option.Formation=Formation or "Off road"

    if self:IsInUtero() then
        self:T(self.lid..string.format("Will switch formation to %s (permanently=%s) when group is spawned", tostring(self.option.Formation), tostring(Permanently)))
    else

      -- Update route with the new formation.
      if NoRouteUpdate then
      else
        self:__UpdateRoute(-1, nil, nil, Formation)
      end

      -- Debug info.
      self:T(self.lid..string.format("Switching formation to %s (permanently=%s)", tostring(self.option.Formation), tostring(Permanently)))

    end

  end

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Find the neares ammo supply group within a given radius.
-- @param #ARMYGROUP self
-- @param #number Radius Search radius in NM. Default 30 NM.
-- @return Wrapper.Group#GROUP Closest ammo supplying group or `nil` if no group is in the given radius.
-- @return #number Distance to closest group in meters.
function ARMYGROUP:FindNearestAmmoSupply(Radius)

  -- Radius in meters.
  Radius=UTILS.NMToMeters(Radius or 30)

  -- Current positon.
  local coord=self:GetCoordinate()

  -- Get my coalition.
  local myCoalition=self:GetCoalition()

  -- Scanned units.
  local units=coord:ScanUnits(Radius)

  -- Find closest
  local dmin=math.huge
  local truck=nil --Wrapper.Unit#UNIT
  for _,_unit in pairs(units.Set) do
    local unit=_unit --Wrapper.Unit#UNIT

    -- Check coaliton and if unit can supply ammo.
    if unit:IsAlive() and unit:GetCoalition()==myCoalition and unit:IsAmmoSupply() and unit:GetVelocityKMH()<1 then

      -- Distance.
      local d=coord:Get2DDistance(unit:GetCoord())

      -- Check if distance is smaller.
      if d<dmin then
        dmin=d
        truck=unit
        -- Debug message.
        self:T(self.lid..string.format("Ammo truck %s [%s] at dist=%d meters", unit:GetName(), unit:GetTypeName(), d))
      end

    end
  end

  if truck then
    return truck:GetGroup(), dmin
  end

  return nil, nil
end

--- Suppress fire of the group by setting its ROE to weapon hold.
-- @param #ARMYGROUP self
function ARMYGROUP:_Suppress()

  -- Current time.
  local Tnow=timer.getTime()

  -- Current ROE
  local currROE=self:GetROE()


  -- Get randomized time the unit is suppressed.
  local sigma=(self.TsuppressMax-self.TsuppressMin)/4

  -- Gaussian distribution.
  local Tsuppress=UTILS.RandomGaussian(self.TsuppressAve,sigma,self.TsuppressMin, self.TsuppressMax)

  -- Time at which the suppression is over.
  local renew=true
  if not self.TsuppressionOver then

    -- Group is not suppressed currently.
    self.TsuppressionOver=Tnow+Tsuppress

    -- Group will hold their weapons.
    self:SwitchROE(ENUMS.ROE.WeaponHold)

    -- Backup ROE.
    self.suppressionROE=currROE

  else
    -- Check if suppression is longer than current time.
    if Tsuppress+Tnow > self.TsuppressionOver then
      self.TsuppressionOver=Tnow+Tsuppress
    else
      renew=false
    end
  end

  -- Recovery event will be called in Tsuppress seconds.
  if renew then
    self:__Unsuppressed(self.TsuppressionOver-Tnow)
  end

  -- Debug message.
  self:T(self.lid..string.format("Suppressed for %d sec", Tsuppress))

end

--- Before "Recovered" event. Check if suppression time is over.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @return #boolean
function ARMYGROUP:onbeforeUnsuppressed(From, Event, To)

  -- Current time.
  local Tnow=timer.getTime()

  -- Debug info
  self:T(self.lid..string.format("onbeforeRecovered: Time now: %d  - Time over: %d", Tnow, self.TsuppressionOver))

  -- Recovery is only possible if enough time since the last hit has passed.
  if Tnow >= self.TsuppressionOver then
    return true
  else
    return false
  end

end

--- After "Recovered" event. Group has recovered and its ROE is set back to the "normal" unsuppressed state. Optionally the group is flared green.
-- @param #ARMYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function ARMYGROUP:onafterUnsuppressed(From, Event, To)

  -- Debug message.
  local text=string.format("Group %s has recovered!", self:GetName())
  MESSAGE:New(text, 10):ToAll()
  self:T(self.lid..text)

  -- Set ROE back to default.
  self:SwitchROE(self.suppressionROE)

  -- Flare unit green.
  if true then
    self.group:FlareGreen()
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
---@diagnostic disable: undefined-global
--- **Ops** - Auftrag (mission) for Ops.
--
-- ## Main Features:
--
--    * Simplifies defining and executing DCS tasks
--    * Additional useful events
--    * Set mission start/stop times
--    * Set mission priority and urgency (can cancel running missions)
--    * Specific mission options for ROE, ROT, formation, etc.
--    * Compatible with OPS classes like FLIGHTGROUP, NAVYGROUP, ARMYGROUP, AIRWING, etc.
--    * FSM events when a mission is done, successful or failed
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/OPS%20-%20Auftrag).
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ===
-- @module Ops.Auftrag
-- @image OPS_Auftrag.png


--- AUFTRAG class.
-- @type AUFTRAG
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field #number auftragsnummer Auftragsnummer.
-- @field #string type Mission type.
-- @field #table categories Mission categories.
-- @field #string status Mission status.
-- @field #table legions Assigned legions.
-- @field #table statusLegion Mission status of all assigned LEGIONs.
-- @field #string statusCommander Mission status of the COMMANDER.
-- @field #string statusChief Mission status of the CHIEF.
-- @field #table groupdata Group specific data.
-- @field #string name Mission name.
-- @field #number prio Mission priority.
-- @field #boolean urgent Mission is urgent. Running missions with lower prio might be cancelled.
-- @field #number importance Importance.
-- @field #number Tstart Mission start time in abs. seconds.
-- @field #number Tstop Mission stop time in abs. seconds.
-- @field #number duration Mission duration in seconds.
-- @field #number durationExe Mission execution time in seconds.
-- @field #number Texecuting Time stamp (abs) when mission is executing. Is `#nil` on start.
-- @field #number Tpush Mission push/execute time in abs. seconds.
-- @field #number Tstarted Time stamp (abs) when mission is started.
-- @field Wrapper.Marker#MARKER marker F10 map marker.
-- @field #boolean markerOn If true, display marker on F10 map with the AUFTRAG status.
-- @field #number markerCoaliton Coalition to which the marker is dispayed.
-- @field #table DCStask DCS task structure.
-- @field #number Ncasualties Number of own casualties during mission.
-- @field #number Nkills Number of (enemy) units killed by assets of this mission.
-- @field #number Ndead Number of assigned groups that are dead.
-- @field #number Nassigned Number of assigned groups.
-- @field #number Nelements Number of elements (units) assigned to mission.
-- @field #number dTevaluate Time interval in seconds before the mission result is evaluated after mission is over.
-- @field #number Tover Mission abs. time stamp, when mission was over.
-- @field #boolean updateDCSTask If `true`, DCS task is updated at every status update of the assigned groups.
-- @field #table conditionStart Condition(s) that have to be true, before the mission will be started.
-- @field #table conditionSuccess If all conditions are true, the mission is cancelled.
-- @field #table conditionFailure If all conditions are true, the mission is cancelled.
-- @field #table conditionPush If all conditions are true, the mission is executed. Before, the group(s) wait at the mission execution waypoint.
-- @field #boolean conditionSuccessSet
-- @field #boolean conditionFailureSet
--
-- @field #number orbitSpeed Orbit speed in m/s.
-- @field #number orbitAltitude Orbit altitude in meters.
-- @field #number orbitHeading Orbit heading in degrees.
-- @field #number orbitLeg Length of orbit leg in meters.
-- @field DCS#Vec2 orbitOffsetVec2 2D offset vector.
-- @field DCS#Vec2 orbitVec2 2D orbit vector.
-- @field #number orbitDeltaR Distance threshold in meters for moving orbit targets.
--
-- @field Ops.Target#TARGET engageTarget Target data to engage.
-- @field #number targetHeading Heading of target in degrees.
-- 
-- @field Ops.Operation#OPERATION operation Operation this mission is part of.
-- 
-- @field #boolean teleport Groups are teleported to the mission ingress waypoint.
--
-- @field Core.Zone#ZONE_RADIUS engageZone *Circular* engagement zone.
-- @field #table engageTargetTypes Table of target types that are engaged in the engagement zone.
-- @field #number engageAltitude Engagement altitude in meters.
-- @field #number engageDirection Engagement direction in degrees.
-- @field #number engageQuantity Number of times a target is engaged.
-- @field #number engageWeaponType Weapon type used.
-- @field #number engageWeaponExpend How many weapons are used.
-- @field #boolean engageAsGroup Group attack.
-- @field #number engageLength Length of engage (carpet or strafing) in meters.
-- @field #number engageMaxDistance Max engage distance.
-- @field #number refuelSystem Refuel type (boom or probe) for TANKER missions.
--
-- @field Wrapper.Group#GROUP escortGroup The group to be escorted.
-- @field #string escortGroupName Name of the escorted group.
-- @field DCS#Vec3 escortVec3 The 3D offset vector from the escorted group to the escort group.
--
-- @field #number facDesignation FAC designation type.
-- @field #boolean facDatalink FAC datalink enabled.
-- @field #number facFreq FAC radio frequency in MHz.
-- @field #number facModu FAC radio modulation 0=AM 1=FM.
--
-- @field Core.Set#SET_GROUP transportGroupSet Groups to be transported.
-- @field Core.Point#COORDINATE transportPickup Coordinate where to pickup the cargo.
-- @field Core.Point#COORDINATE transportDropoff Coordinate where to drop off the cargo.
-- @field #number transportPickupRadius Radius in meters for pickup zone. Default 500 m.
--
-- @field Ops.OpsTransport#OPSTRANSPORT opstransport OPS transport assignment.
-- @field #number NcarriersMin Min number of required carrier assets.
-- @field #number NcarriersMax Max number of required carrier assets.
-- @field Core.Zone#ZONE transportDeployZone Deploy zone of an OPSTRANSPORT.
-- @field Core.Zone#ZONE transportDisembarkZone Disembark zone of an OPSTRANSPORT.
-- @param #table carrierCategories Transport group categories.
-- @field #table carrierAttributes Generalized attribute(s) of transport assets.
-- @field #table carrierProperties DCS attribute(s) of transport assets.
--
-- @field #number artyRadius Radius in meters.
-- @field #number artyShots Number of shots fired.
-- @field #number artyAltitude Altitude in meters. Can be used for a Barrage.
-- @field #number artyHeading Heading in degrees (for Barrage).
-- @field #number artyAngle Shooting angle in degrees (for Barrage).
--
-- @field #string alert5MissionType Alert 5 mission type. This is the mission type, the alerted assets will be able to carry out.
-- 
-- @field #table attributes Generalized attribute(s) of assets.
-- @field #table properties DCS attribute(s) of assets.
--
-- @field Ops.Chief#CHIEF chief The CHIEF managing this mission.
-- @field Ops.Commander#COMMANDER commander The COMMANDER managing this mission.
-- @field #table assets Warehouse assets assigned for this mission.
-- @field #number NassetsMin Min. number of required warehouse assets.
-- @field #number NassetsMax Max. number of required warehouse assets.
-- @field #number NescortMin Min. number of required escort assets for each group the mission is assigned to.
-- @field #number NescortMax Max. number of required escort assets for each group the mission is assigned to.
-- @field #string escortMissionType Escort mission type.
-- @field #table escortTargetTypes Target types that will be engaged.
-- @field #number escortEngageRange Engage range in nautical miles (NM).
-- @field #number Nassets Number of requested warehouse assets.
-- @field #table NassetsLegMin Number of required warehouse assets for each assigned legion.
-- @field #table NassetsLegMax Number of required warehouse assets for each assigned legion.
-- @field #table requestID The ID of the queued warehouse request. Necessary to cancel the request if the mission was cancelled before the request is processed.
-- @field #table payloads User specified airwing payloads for this mission. Only these will be considered for the job!
-- @field Ops.Airwing#AIRWING.PatrolData patroldata Patrol data.
--
-- @field #table specialLegions User specified legions assigned for this mission. Only these will be considered for the job!
-- @field #table specialCohorts User specified cohorts assigned for this mission. Only these will be considered for the job!
-- @field #table transportLegions Legions explicitly requested for providing transport carrier assets.
-- @field #table transportCohorts Cohorts explicitly requested for providing transport carrier assets.
-- @field #table escortLegions Legions explicitly requested for providing escorting assets.
-- @field #table escortCohorts Cohorts explicitly requested for providing escorting assets.
--
-- @field #string missionTask Mission task. See `ENUMS.MissionTask`.
-- @field #number missionAltitude Mission altitude in meters.
-- @field #number missionSpeed Mission speed in km/h.
-- @field #number missionFraction Mission coordiante fraction. Default is 0.5.
-- @field #number missionRange Mission range in meters. Used by LEGION classes (AIRWING, BRIGADE, ...).
-- @field Core.Point#COORDINATE missionWaypointCoord Mission waypoint coordinate.
-- @field Core.Point#COORDINATE missionEgressCoord Mission egress waypoint coordinate.
-- @field Core.Point#COORDINATE missionIngressCoord Mission Ingress waypoint coordinate.
-- @field #number missionWaypointRadius Random radius in meters.
-- @field #boolean legionReturn If `true`, assets return to their legion (default). If `false`, they will stay alive. 
--
-- @field #table enrouteTasks Mission enroute tasks.
--
-- @field #number repeated Number of times mission was repeated.
-- @field #number repeatedSuccess Number of times mission was repeated after a success.
-- @field #number repeatedFailure Number of times mission was repeated after a failure.
-- @field #number Nrepeat Number of times the mission is repeated.
-- @field #number NrepeatFailure Number of times mission is repeated if failed.
-- @field #number NrepeatSuccess Number of times mission is repeated if successful.
--
-- @field Ops.OpsGroup#OPSGROUP.Radio radio Radio freq and modulation.
-- @field Ops.OpsGroup#OPSGROUP.Beacon tacan TACAN setting.
-- @field Ops.OpsGroup#OPSGROUP.Beacon icls ICLS setting.
--
-- @field #number optionROE ROE.
-- @field #number optionROT ROT.
-- @field #number optionAlarm Alarm state.
-- @field #number optionFormation Formation.
-- @field #boolean optionEPLRS EPLRS datalink.
-- @field #number optionCM Counter measures.
-- @field #number optionRTBammo RTB on out-of-ammo.
-- @field #number optionRTBfuel RTB on out-of-fuel.
-- @field #number optionECM ECM.
-- @field #boolean optionEmission Emission is on or off.
-- @field #boolean optionInvisible Invisible is on/off.
-- @field #boolean optionImmortal Immortal is on/off.
--
-- @extends Core.Fsm#FSM

--- *A warrior's mission is to foster the success of others.* -- Morihei Ueshiba
--
-- ===
--
-- # The AUFTRAG Concept
--
-- The AUFTRAG class significantly simplifies the workflow of using DCS tasks.
--
-- You can think of an AUFTRAG as document, which contains the mission briefing, i.e. information about the target location, mission altitude, speed and various other parameters.
-- This document can be handed over directly to a pilot (or multiple pilots) via the @{Ops.FlightGroup#FLIGHTGROUP} class. The pilots will then execute the mission.
--
-- The AUFTRAG document can also be given to an AIRWING. The airwing will then determine the best assets (pilots and payloads) available for the job.
--
-- Similarly, an AUFTRAG can be given to ground or navel groups via the @{Ops.ArmyGroup#ARMYGROUP} or @{Ops.NavyGroup#NAVYGROUP} classes, respectively. These classes have also
-- AIRWING analouges, which are called BRIGADE and FLEET. Brigades and fleets will likewise select the best assets they have available and pass on the AUFTRAG to them.
--
--
-- One more up the food chain, an AUFTRAG can be passed to a COMMANDER. The commander will recruit the best assets of AIRWINGs, BRIGADEs and/or FLEETs and pass the job over to it.
--
--
-- # Airborne Missions
--
-- Several mission types are supported by this class.
--
-- ## Anti-Ship
--
-- An anti-ship mission can be created with the @{#AUFTRAG.NewANTISHIP}() function.
--
-- ## AWACS
--
-- An AWACS mission can be created with the @{#AUFTRAG.NewAWACS}() function.
--
-- ## BAI
--
-- A BAI mission can be created with the @{#AUFTRAG.NewBAI}() function.
--
-- ## Bombing
--
-- A bombing mission can be created with the @{#AUFTRAG.NewBOMBING}() function.
--
-- ## Bombing Runway
--
-- A bombing runway mission can be created with the @{#AUFTRAG.NewBOMBRUNWAY}() function.
--
-- ## Bombing Carpet
--
-- A carpet bombing mission can be created with the @{#AUFTRAG.NewBOMBCARPET}() function.
-- 
-- ## Strafing
--
-- A strafing mission can be created with the @{#AUFTRAG.NewSTRAFING}() function.
--
-- ## CAP
--
-- A CAP mission can be created with the @{#AUFTRAG.NewCAP}() function.
--
-- ## CAS
--
-- A CAS mission can be created with the @{#AUFTRAG.NewCAS}() function.
--
-- ## Escort
--
-- An escort mission can be created with the @{#AUFTRAG.NewESCORT}() function.
--
-- ## FACA
--
-- An FACA mission can be created with the @{#AUFTRAG.NewFACA}() function.
--
-- ## Ferry
--
-- Not implemented yet.
--
-- ## Ground Escort
--
-- An escort mission can be created with the @{#AUFTRAG.NewGROUNDESCORT}() function.
-- 
-- ## Intercept
--
-- An intercept mission can be created with the @{#AUFTRAG.NewINTERCEPT}() function.
--
-- ## Orbit
--
-- An orbit mission can be created with the @{#AUFTRAG.NewORBIT}() function.
--
-- ## GCICAP
--
-- An patrol mission can be created with the @{#AUFTRAG.NewGCICAP}() function.
--
-- ## RECON
--
-- An reconnaissance mission can be created with the @{#AUFTRAG.NewRECON}() function.
--
-- ## RESCUE HELO
--
-- An rescue helo mission can be created with the @{#AUFTRAG.NewRESCUEHELO}() function.
--
-- ## SEAD
--
-- An SEAD mission can be created with the @{#AUFTRAG.NewSEAD}() function.
--
-- ## STRIKE
--
-- An strike mission can be created with the @{#AUFTRAG.NewSTRIKE}() function.
--
-- ## Tanker
--
-- A refueling tanker mission can be created with the @{#AUFTRAG.NewTANKER}() function.
--
-- ## TROOPTRANSPORT
--
-- A troop transport mission can be created with the @{#AUFTRAG.NewTROOPTRANSPORT}() function.
-- 
-- ## CARGOTRANSPORT
-- 
-- A cargo transport mission can be created with the @{#AUFTRAG.NewCARGOTRANSPORT}() function.
--
-- ## HOVER
--
-- A mission for a helicoptre or VSTOL plane to Hover at a point for a certain amount of time can be created with the @{#AUFTRAG.NewHOVER}() function.
--
-- # Ground Missions
--
-- ## ARTY
--
-- An arty mission can be created with the @{#AUFTRAG.NewARTY}() function.
--
-- ## GROUNDATTACK
--
-- A ground attack mission can be created with the @{#AUFTRAG.NewGROUNDATTACK}() function.
--
-- # Assigning Missions
--
-- An AUFTRAG can be assigned to groups (FLIGHTGROUP, ARMYGROUP, NAVYGROUP), legions (AIRWING, BRIGADE, FLEET) or to a COMMANDER.
--
-- ## Group Level
--
-- ### Flight Group
--
-- Assigning an AUFTRAG to a flight group is done via the @{Ops.FlightGroup#FLIGHTGROUP.AddMission} function. See FLIGHTGROUP docs for details.
--
-- ### Army Group
--
-- Assigning an AUFTRAG to an army group is done via the @{Ops.ArmyGroup#ARMYGROUP.AddMission} function. See ARMYGROUP docs for details.
--
-- ### Navy Group
--
-- Assigning an AUFTRAG to a navy group is done via the @{Ops.NavyGroup#NAVYGROUP.AddMission} function. See NAVYGROUP docs for details.
--
-- ## Legion Level
--
-- Adding an AUFTRAG to an airwing is done via the @{Ops.Airwing#AIRWING.AddMission} function. See AIRWING docs for further details.
-- Similarly, an AUFTRAG can be added to a brigade via the @{Ops.Brigade#BRIGADE.AddMission} function.
--
-- ## Commander Level
--
-- Assigning an AUFTRAG to a commander is done via the @{Ops.Commander#COMMANDER.AddMission} function.
-- The commander will select the best assets available from all the legions under his command. See COMMANDER docs for details.
-- 
-- ## Chief Level
-- 
--  Assigning an AUFTRAG to a commander is done via the @{Ops.Chief#CHIEF.AddMission} function. The chief will simply pass on the mission to his/her commander.
--  
-- # Transportation
-- 
-- TODO
--
--
-- # Events
--
-- The AUFTRAG class creates many useful (FSM) events, which can be used in the mission designers script.
--
-- TODO
--
--
-- # Examples
--
-- TODO
--
--
-- @field #AUFTRAG
AUFTRAG = {
  ClassName          = "AUFTRAG",
  verbose            =     0,
  lid                =   nil,
  auftragsnummer     =   nil,
  groupdata          =    {},
  legions            =    {},
  statusLegion       =    {},
  requestID          =    {},
  assets             =    {},
  NassetsLegMin      =    {},
  NassetsLegMax      =    {},
  missionFraction    =   0.5,
  enrouteTasks       =    {},
  marker             =   nil,
  markerOn           =   nil,
  markerCoalition    =   nil,
  conditionStart     =    {},
  conditionSuccess   =    {},
  conditionFailure   =    {},
  conditionPush      =    {},
  conditionSuccessSet = false,
  conditionFailureSet = false,
  repeatDelay = 1,
}

--- Global mission counter.
_AUFTRAGSNR=0


--- Mission types.
-- @type AUFTRAG.Type
-- @field #string ANTISHIP Anti-ship mission.
-- @field #string AWACS AWACS mission.
-- @field #string BAI Battlefield Air Interdiction.
-- @field #string BOMBING Bombing mission.
-- @field #string BOMBRUNWAY Bomb runway of an airbase.
-- @field #string BOMBCARPET Carpet bombing.
-- @field #string CAP Combat Air Patrol.
-- @field #string CAS Close Air Support.
-- @field #string ESCORT Escort mission.
-- @field #string FAC Forward AirController mission.
-- @field #string FACA Forward AirController airborne mission.
-- @field #string FERRY Ferry mission.
-- @field #string GROUNDESCORT Ground escort mission.
-- @field #string INTERCEPT Intercept mission.
-- @field #string ORBIT Orbit mission.
-- @field #string GCICAP Similar to CAP but no auto engage targets.
-- @field #string RECON Recon mission.
-- @field #string RECOVERYTANKER Recovery tanker mission. Not implemented yet.
-- @field #string RESCUEHELO Rescue helo.
-- @field #string SEAD Suppression/destruction of enemy air defences.
-- @field #string STRIKE Strike mission.
-- @field #string TANKER Tanker mission.
-- @field #string TROOPTRANSPORT Troop transport mission.
-- @field #string ARTY Fire at point.
-- @field #string PATROLZONE Patrol a zone.
-- @field #string OPSTRANSPORT Ops transport.
-- @field #string AMMOSUPPLY Ammo supply.
-- @field #string FUELSUPPLY Fuel supply.
-- @field #string ALERT5 Alert 5.
-- @field #string ONGUARD On guard.
-- @field #string ARMOREDGUARD On guard - with armored groups.
-- @field #string BARRAGE Barrage.
-- @field #string ARMORATTACK Armor attack.
-- @field #string CASENHANCED Enhanced CAS.
-- @field #string HOVER Hover.
-- @field #string LANDATCOORDINATE Land at coordinate.
-- @field #string GROUNDATTACK Ground attack.
-- @field #string CARGOTRANSPORT Cargo transport.
-- @field #string RELOCATECOHORT Relocate a cohort from one legion to another.
-- @field #string AIRDEFENSE Air defense.
-- @field #string EWR Early Warning Radar.
-- @field #string RECOVERYTANKER Recovery tanker.
-- @field #string REARMING Rearming mission.
-- @field #string CAPTUREZONE Capture zone mission.
-- @field #string NOTHING Nothing.
-- @field #string PATROLRACETRACK Patrol Racetrack.
-- @field #string STRAFING Strafing run.
AUFTRAG.Type={
  ANTISHIP="Anti Ship",
  AWACS="AWACS",
  BAI="BAI",
  BOMBING="Bombing",
  BOMBRUNWAY="Bomb Runway",
  BOMBCARPET="Carpet Bombing",
  CAP="CAP",
  CAS="CAS",
  ESCORT="Escort",
  FAC="FAC",
  FACA="FAC-A",
  FERRY="Ferry Flight",
  GROUNDESCORT="Ground Escort",
  INTERCEPT="Intercept",
  ORBIT="Orbit",
  GCICAP="Ground Controlled CAP",
  RECON="Recon",
  RECOVERYTANKER="Recovery Tanker",
  RESCUEHELO="Rescue Helo",
  SEAD="SEAD",
  STRIKE="Strike",
  TANKER="Tanker",
  TROOPTRANSPORT="Troop Transport",
  ARTY="Fire At Point",
  PATROLZONE="Patrol Zone",
  OPSTRANSPORT="Ops Transport",
  AMMOSUPPLY="Ammo Supply",
  FUELSUPPLY="Fuel Supply",
  ALERT5="Alert5",
  ONGUARD="On Guard",
  ARMOREDGUARD="Armored Guard",
  BARRAGE="Barrage",
  ARMORATTACK="Armor Attack",
  CASENHANCED="CAS Enhanced",
  HOVER="Hover",
  LANDATCOORDINATE="Land at Coordinate",
  GROUNDATTACK="Ground Attack",
  CARGOTRANSPORT="Cargo Transport",
  RELOCATECOHORT="Relocate Cohort",
  AIRDEFENSE="Air Defence",
  EWR="Early Warning Radar",
  REARMING="Rearming",
  CAPTUREZONE="Capture Zone",
  NOTHING="Nothing",
  PATROLRACETRACK="Patrol Racetrack",
  STRAFING="Strafing",
}

--- Special task description.
-- @type AUFTRAG.SpecialTask
-- @field #string FORMATION AI formation task.
-- @field #string PATROLZONE Patrol zone task.
-- @field #string RECON Recon task.
-- @field #string AMMOSUPPLY Ammo Supply.
-- @field #string FUELSUPPLY Fuel Supply.
-- @field #string ALERT5 Alert 5 task.
-- @field #string ONGUARD On guard.
-- @field #string ARMOREDGUARD On guard with armor.
-- @field #string BARRAGE Barrage.
-- @field #string HOVER Hover.
-- @field #string GROUNDATTACK Ground attack.
-- @field #string FERRY Ferry mission.
-- @field #string RELOCATECOHORT Relocate cohort.
-- @field #string AIRDEFENSE Air defense.
-- @field #string EWR Early Warning Radar.
-- @field #string RECOVERYTANKER Recovery tanker.
-- @field #string REARMING Rearming.
-- @field #string CAPTUREZONE Capture OPS zone.
-- @field #string NOTHING Nothing.
-- @field #string PATROLRACETRACK Patrol Racetrack.
AUFTRAG.SpecialTask={
  FORMATION="Formation",
  PATROLZONE="PatrolZone",
  RECON="ReconMission",
  AMMOSUPPLY="Ammo Supply",
  FUELSUPPLY="Fuel Supply",
  ALERT5="Alert5",
  ONGUARD="On Guard",
  ARMOREDGUARD="ArmoredGuard",
  BARRAGE="Barrage",
  ARMORATTACK="AmorAttack",
  HOVER="Hover",
  GROUNDATTACK="Ground Attack",
  FERRY="Ferry",
  RELOCATECOHORT="Relocate Cohort",
  AIRDEFENSE="Air Defense",
  EWR="Early Warning Radar",
  RECOVERYTANKER="Recovery Tanker",
  REARMING="Rearming",
  CAPTUREZONE="Capture Zone",  
  NOTHING="Nothing",
  PATROLRACETRACK="Patrol Racetrack",
}

--- Mission status.
-- @type AUFTRAG.Status
-- @field #string PLANNED Mission is at the early planning stage and has not been added to any queue.
-- @field #string QUEUED Mission is queued at a LEGION.
-- @field #string REQUESTED Mission assets were requested from the warehouse.
-- @field #string SCHEDULED Mission is scheduled in an OPSGROUP queue waiting to be started.
-- @field #string STARTED Mission has started but is not executed yet.
-- @field #string EXECUTING Mission is being executed.
-- @field #string DONE Mission is over.
-- @field #string CANCELLED Mission was cancelled.
-- @field #string SUCCESS Mission was a success.
-- @field #string FAILED Mission failed.
AUFTRAG.Status={
  PLANNED="planned",
  QUEUED="queued",
  REQUESTED="requested",
  SCHEDULED="scheduled",
  STARTED="started",
  EXECUTING="executing",
  DONE="done",
  CANCELLED="cancelled",
  SUCCESS="success",
  FAILED="failed",
}

--- Mission status of an assigned group.
-- @type AUFTRAG.GroupStatus
-- @field #string SCHEDULED Mission is scheduled in a FLIGHGROUP queue waiting to be started.
-- @field #string STARTED Ops group started this mission but it is not executed yet.
-- @field #string EXECUTING Ops group is executing this mission.
-- @field #string PAUSED Ops group has paused this mission, e.g. for refuelling.
-- @field #string DONE Mission task of the Ops group is done.
-- @field #string CANCELLED Mission was cancelled.
AUFTRAG.GroupStatus={
  SCHEDULED="scheduled",
  STARTED="started",
  EXECUTING="executing",
  PAUSED="paused",
  DONE="done",
  CANCELLED="cancelled",
}

--- Target type.
-- @type AUFTRAG.TargetType
-- @field #string GROUP Target is a GROUP object.
-- @field #string UNIT Target is a UNIT object.
-- @field #string STATIC Target is a STATIC object.
-- @field #string COORDINATE Target is a COORDINATE.
-- @field #string AIRBASE Target is an AIRBASE.
-- @field #string SETGROUP Target is a SET of GROUPs.
-- @field #string SETUNIT Target is a SET of UNITs.
AUFTRAG.TargetType={
  GROUP="Group",
  UNIT="Unit",
  STATIC="Static",
  COORDINATE="Coordinate",
  AIRBASE="Airbase",
  SETGROUP="SetGroup",
  SETUNIT="SetUnit",
}

--- Mission category.
-- @type AUFTRAG.Category
-- @field #string AIRCRAFT Airplanes and helicopters.
-- @field #string AIRPLANE Airplanes.
-- @field #string HELICOPTER Helicopter.
-- @field #string GROUND Ground troops.
-- @field #string NAVAL Naval grous.
AUFTRAG.Category={
  ALL="All",
  AIRCRAFT="Aircraft",
  AIRPLANE="Airplane",
  HELICOPTER="Helicopter",
  GROUND="Ground",
  NAVAL="Naval",
}

--- Target data.
-- @type AUFTRAG.TargetData
-- @field Wrapper.Positionable#POSITIONABLE Target Target Object.
-- @field #string Type Target type: "Group", "Unit", "Static", "Coordinate", "Airbase", "SetGroup", "SetUnit".
-- @field #string Name Target name.
-- @field #number Ninital Number of initial targets.
-- @field #number Lifepoints Total life points.
-- @field #number Lifepoints0 Inital life points.

--- Mission capability.
-- @type AUFTRAG.Capability
-- @field #string MissionType Type of mission.
-- @field #number Performance Number describing the performance level. The higher the better.

--- Mission success.
-- @type AUFTRAG.Success
-- @field #string SURVIVED Group did survive.
-- @field #string ENGAGED Target was engaged.
-- @field #string DAMAGED Target was damaged.
-- @field #string DESTROYED Target was destroyed.

--- Generic mission condition.
-- @type AUFTRAG.Condition
-- @field #function func Callback function to check for a condition. Should return a #boolean.
-- @field #table arg Optional arguments passed to the condition callback function.

--- Group specific data. Each ops group subscribed to this mission has different data for this.
-- @type AUFTRAG.GroupData
-- @field Ops.OpsGroup#OPSGROUP opsgroup The OPS group.
-- @field Core.Point#COORDINATE waypointcoordinate Ingress waypoint coordinate.
-- @field #number waypointindex Mission (ingress) Waypoint UID.
-- @field #number waypointEgressUID Egress Waypoint UID.
-- @field Core.Point#COORDINATE wpegresscoordinate Egress waypoint coordinate.
--
-- @field Ops.OpsGroup#OPSGROUP.Task waypointtask Waypoint task.
-- @field #string status Group mission status.
-- @field Functional.Warehouse#WAREHOUSE.Assetitem asset The warehouse asset.


--- AUFTRAG class version.
-- @field #string version
AUFTRAG.version="1.2.1"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Replace engageRange by missionRange. Here and in other classes. CTRL+H is your friend!
-- TODO: Mission success options damaged, destroyed.
-- TODO: F10 marker to create new missions.
-- DONE: Add option that assets do not return to their legion.
-- DONE: Add Capture zone task.
-- DONE: Add orbit mission for moving anker points.
-- DONE: Add recovery tanker mission for boat ops.
-- DONE: Added auftrag category.
-- DONE: Missions can be assigned to multiple legions.
-- DONE: Option to assign a specific payload for the mission (requires an AIRWING).
-- NOPE: Clone mission. How? Deepcopy? ==> Create a new auftrag.
-- DONE: Recon mission. What input? Set of coordinates?
-- DONE: Option to assign mission to specific squadrons (requires an AIRWING).
-- DONE: Add mission start conditions.
-- DONE: Add rescue helo mission for boat ops.
-- DONE: Mission ROE and ROT.
-- DONE: Mission frequency and TACAN.
-- DONE: Mission formation, etc.
-- DONE: FSM events.
-- DONE: F10 marker functions that are updated on Status event.
-- DONE: Evaluate mission result ==> SUCCESS/FAILURE
-- DONE: NewAUTO() NewA2G NewA2A
-- DONE: Transport mission.
-- DONE: Set mission coalition, e.g. for F10 markers. Could be derived from target if target has a coalition.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new generic AUFTRAG object.
-- @param #AUFTRAG self
-- @param #string Type Mission type.
-- @return #AUFTRAG self
function AUFTRAG:New(Type)

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #AUFTRAG

  -- Increase global counter.
  _AUFTRAGSNR=_AUFTRAGSNR+1

  -- Mission type.
  self.type=Type

  -- Auftragsnummer.
  self.auftragsnummer=_AUFTRAGSNR

  -- Log ID.
  self:_SetLogID()

  -- State is planned.
  self.status=AUFTRAG.Status.PLANNED

  -- Defaults  .
  self:SetName()
  self:SetPriority()
  self:SetTime()
  self:SetRequiredAssets()
  self.engageAsGroup=true
  self.dTevaluate=5

  -- Init counters and stuff.
  self.repeated=0
  self.repeatedSuccess=0
  self.repeatedFailure=0
  self.Nrepeat=0
  self.NrepeatFailure=0
  self.NrepeatSuccess=0
  self.Ncasualties=0
  self.Nkills=0
  self.Nelements=0
  self.Ngroups=0
  self.Nassigned=nil
  self.Ndead=0

  -- FMS start state is PLANNED.
  self:SetStartState(self.status)

  -- PLANNED --> (QUEUED) --> (REQUESTED) --> SCHEDULED --> STARTED --> EXECUTING --> DONE
  self:AddTransition("*",                      "Planned",          AUFTRAG.Status.PLANNED)     -- Mission is in planning stage. Could be in the queue of a COMMANDER or CHIEF.
  self:AddTransition(AUFTRAG.Status.PLANNED,   "Queued",           AUFTRAG.Status.QUEUED)      -- Mission is in queue of a LEGION.
  self:AddTransition(AUFTRAG.Status.QUEUED,    "Requested",        AUFTRAG.Status.REQUESTED)   -- Mission assets have been requested from the warehouse.
  self:AddTransition(AUFTRAG.Status.REQUESTED, "Scheduled",        AUFTRAG.Status.SCHEDULED)   -- Mission added to the first ops group queue.
  
  self:AddTransition(AUFTRAG.Status.PLANNED,   "Scheduled",        AUFTRAG.Status.SCHEDULED)   -- From planned directly to scheduled.

  self:AddTransition(AUFTRAG.Status.SCHEDULED, "Started",          AUFTRAG.Status.STARTED)     -- First asset has started the mission.
  self:AddTransition(AUFTRAG.Status.STARTED,   "Executing",        AUFTRAG.Status.EXECUTING)   -- First asset is executing the mission.
  

  self:AddTransition("*",                      "Done",             AUFTRAG.Status.DONE)        -- All assets have reported that mission is done.

  self:AddTransition("*",                      "Cancel",           AUFTRAG.Status.CANCELLED)   -- Command to cancel the mission.

  self:AddTransition("*",                      "Success",          AUFTRAG.Status.SUCCESS)
  self:AddTransition("*",                      "Failed",           AUFTRAG.Status.FAILED)

  self:AddTransition("*",                      "Status",           "*")
  self:AddTransition("*",                      "Stop",             "*")

  self:AddTransition("*",                      "Repeat",           AUFTRAG.Status.PLANNED)

  self:AddTransition("*",                      "ElementDestroyed", "*")
  self:AddTransition("*",                      "GroupDead",        "*")
  self:AddTransition("*",                      "AssetDead",        "*")

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Status".
  -- @function [parent=#AUFTRAG] Status
  -- @param #AUFTRAG self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#AUFTRAG] __Status
  -- @param #AUFTRAG self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop".
  -- @function [parent=#AUFTRAG] Stop
  -- @param #AUFTRAG self

  --- Triggers the FSM event "Stop" after a delay.
  -- @function [parent=#AUFTRAG] __Stop
  -- @param #AUFTRAG self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Planned".
  -- @function [parent=#AUFTRAG] Planned
  -- @param #AUFTRAG self

  --- Triggers the FSM event "Planned" after a delay.
  -- @function [parent=#AUFTRAG] __Planned
  -- @param #AUFTRAG self
  -- @param #number delay Delay in seconds.

  --- On after "Planned" event.
  -- @function [parent=#AUFTRAG] OnAfterPlanned
  -- @param #AUFTRAG self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Queued".
  -- @function [parent=#AUFTRAG] Queued
  -- @param #AUFTRAG self

  --- Triggers the FSM event "Queued" after a delay.
  -- @function [parent=#AUFTRAG] __Queued
  -- @param #AUFTRAG self
  -- @param #number delay Delay in seconds.

  --- On after "Queued" event.
  -- @function [parent=#AUFTRAG] OnAfterQueued
  -- @param #AUFTRAG self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Requested".
  -- @function [parent=#AUFTRAG] Requested
  -- @param #AUFTRAG self

  --- Triggers the FSM event "Requested" after a delay.
  -- @function [parent=#AUFTRAG] __Requested
  -- @param #AUFTRAG self
  -- @param #number delay Delay in seconds.

  --- On after "Requested" event.
  -- @function [parent=#AUFTRAG] OnAfterRequested
  -- @param #AUFTRAG self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Scheduled".
  -- @function [parent=#AUFTRAG] Scheduled
  -- @param #AUFTRAG self

  --- Triggers the FSM event "Scheduled" after a delay.
  -- @function [parent=#AUFTRAG] __Scheduled
  -- @param #AUFTRAG self
  -- @param #number delay Delay in seconds.

  --- On after "Scheduled" event.
  -- @function [parent=#AUFTRAG] OnAfterScheduled
  -- @param #AUFTRAG self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Started".
  -- @function [parent=#AUFTRAG] Started
  -- @param #AUFTRAG self

  --- Triggers the FSM event "Started" after a delay.
  -- @function [parent=#AUFTRAG] __Started
  -- @param #AUFTRAG self
  -- @param #number delay Delay in seconds.

  --- On after "Started" event.
  -- @function [parent=#AUFTRAG] OnAfterStarted
  -- @param #AUFTRAG self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Executing".
  -- @function [parent=#AUFTRAG] Executing
  -- @param #AUFTRAG self

  --- Triggers the FSM event "Executing" after a delay.
  -- @function [parent=#AUFTRAG] __Executing
  -- @param #AUFTRAG self
  -- @param #number delay Delay in seconds.

  --- On after "Executing" event.
  -- @function [parent=#AUFTRAG] OnAfterExecuting
  -- @param #AUFTRAG self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Cancel".
  -- @function [parent=#AUFTRAG] Cancel
  -- @param #AUFTRAG self

  --- Triggers the FSM event "Cancel" after a delay.
  -- @function [parent=#AUFTRAG] __Cancel
  -- @param #AUFTRAG self
  -- @param #number delay Delay in seconds.

  --- On after "Cancel" event.
  -- @function [parent=#AUFTRAG] OnAfterCancel
  -- @param #AUFTRAG self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Done".
  -- @function [parent=#AUFTRAG] Done
  -- @param #AUFTRAG self

  --- Triggers the FSM event "Done" after a delay.
  -- @function [parent=#AUFTRAG] __Done
  -- @param #AUFTRAG self
  -- @param #number delay Delay in seconds.

  --- On after "Done" event.
  -- @function [parent=#AUFTRAG] OnAfterDone
  -- @param #AUFTRAG self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Success".
  -- @function [parent=#AUFTRAG] Success
  -- @param #AUFTRAG self

  --- Triggers the FSM event "Success" after a delay.
  -- @function [parent=#AUFTRAG] __Success
  -- @param #AUFTRAG self
  -- @param #number delay Delay in seconds.

  --- On after "Success" event.
  -- @function [parent=#AUFTRAG] OnAfterSuccess
  -- @param #AUFTRAG self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- Triggers the FSM event "Failed".
  -- @function [parent=#AUFTRAG] Failed
  -- @param #AUFTRAG self

  --- Triggers the FSM event "Failed" after a delay.
  -- @function [parent=#AUFTRAG] __Failed
  -- @param #AUFTRAG self
  -- @param #number delay Delay in seconds.

  --- On after "Failed" event.
  -- @function [parent=#AUFTRAG] OnAfterFailed
  -- @param #AUFTRAG self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- Triggers the FSM event "Repeat".
  -- @function [parent=#AUFTRAG] Repeat
  -- @param #AUFTRAG self

  --- Triggers the FSM event "Repeat" after a delay.
  -- @function [parent=#AUFTRAG] __Repeat
  -- @param #AUFTRAG self
  -- @param #number delay Delay in seconds.

  --- On after "Repeat" event.
  -- @function [parent=#AUFTRAG] OnAfterRepeat
  -- @param #AUFTRAG self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  -- Init status update.
  self:__Status(-1)

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Create Missions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- **[AIR]** Create an ANTI-SHIP mission.
-- @param #AUFTRAG self
-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be passed as a @{Wrapper.Group#GROUP} or @{Wrapper.Unit#UNIT} object.
-- @param #number Altitude Engage altitude in feet. Default 2000 ft.
-- @return #AUFTRAG self
function AUFTRAG:NewANTISHIP(Target, Altitude)

  local mission=AUFTRAG:New(AUFTRAG.Type.ANTISHIP)

  mission:_TargetFromObject(Target)

  -- DCS task parameters:
  mission.engageWeaponType=ENUMS.WeaponFlag.Auto
  mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL
  mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000)

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.ANTISHIPSTRIKE
  mission.missionAltitude=mission.engageAltitude
  mission.missionFraction=0.4
  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.EvadeFire

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR ROTARY]** Create an HOVER mission.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Where to hover.
-- @param #number Altitude Hover altitude in feet AGL. Default is 50 feet above ground.
-- @param #number Time Time in seconds to hold the hover. Default 300 seconds.
-- @param #number Speed Speed in knots to fly to the target coordinate. Default 150kn.
-- @param #number MissionAlt Altitude to fly towards the mission in feet AGL. Default 1000ft.
-- @return #AUFTRAG self
function AUFTRAG:NewHOVER(Coordinate, Altitude, Time, Speed, MissionAlt)

  local mission=AUFTRAG:New(AUFTRAG.Type.HOVER)

  -- Altitude.
  if Altitude then
    mission.hoverAltitude=Coordinate:GetLandHeight()+UTILS.FeetToMeters(Altitude)
  else
    mission.hoverAltitude=Coordinate:GetLandHeight()+UTILS.FeetToMeters(50)
  end

  mission:_TargetFromObject(Coordinate)

  mission.hoverSpeed = 0.1 -- the DCS Task itself will shortly be build with this so MPS
  mission.hoverTime = Time or 300
  self:SetMissionSpeed(Speed or 150)
  self:SetMissionAltitude(MissionAlt or 1000)
  
  -- Mission options:
  mission.missionFraction=0.9
  mission.optionROE=ENUMS.ROE.ReturnFire
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.HELICOPTER}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR ROTARY]** Create an LANDATCOORDINATE mission.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Where to land.
-- @param #number OuterRadius (Optional) Vary the coordinate by this many feet, e.g. get a new random coordinate between OuterRadius and (optionally) avoiding InnerRadius of the coordinate.
-- @param #number InnerRadius (Optional) Vary the coordinate by this many feet, e.g. get a new random coordinate between OuterRadius and (optionally) avoiding InnerRadius of the coordinate.
-- @param #number Time Time in seconds to stay. Default 300 seconds.
-- @param #number Speed Speed in knots to fly to the target coordinate. Default 150kn.
-- @param #number MissionAlt Altitude to fly towards the mission in feet AGL. Default 1000ft.
-- @param #boolean CombatLanding (Optional) If true, set the Combat Landing option.
-- @param #number DirectionAfterLand (Optional) Heading after landing in degrees.
-- @return #AUFTRAG self
function AUFTRAG:NewLANDATCOORDINATE(Coordinate, OuterRadius, InnerRadius, Time, Speed, MissionAlt, CombatLanding, DirectionAfterLand)

  local mission=AUFTRAG:New(AUFTRAG.Type.LANDATCOORDINATE)

  mission:_TargetFromObject(Coordinate)

  mission.stayTime = Time or 300
  mission.stayAt = Coordinate
  mission.combatLand = CombatLanding
  mission.directionAfter = DirectionAfterLand
  self:SetMissionSpeed(Speed or 150)
  self:SetMissionAltitude(MissionAlt or 1000)
  
  if OuterRadius then
    mission.stayAt = Coordinate:GetRandomCoordinateInRadius(UTILS.FeetToMeters(OuterRadius),UTILS.FeetToMeters(InnerRadius or 0))  
  end
  
  -- Mission options:
  mission.missionFraction=0.9
  mission.optionROE=ENUMS.ROE.ReturnFire
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.HELICOPTER}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR]** Create an enhanced orbit race track mission. Planes will keep closer to the track.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Where to start the race track.
-- @param #number Altitude (Optional) Altitude in feet. Defaults to 20,000ft ASL.
-- @param #number Speed (Optional) Speed in knots. Defaults to 300kn TAS.
-- @param #number Heading (Optional) Heading in degrees, 0 to 360. Defaults to 90 degree (East).
-- @param #number Leg (Optional) Leg of the race track in NM. Defaults to 10nm.
-- @param #number Formation (Optional) Formation to take, e.g. ENUMS.Formation.FixedWing.Trail.Close, also see [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_option_formation).
-- @return #AUFTRAG self
function AUFTRAG:NewPATROL_RACETRACK(Coordinate,Altitude,Speed,Heading,Leg,Formation)
  
  local mission = AUFTRAG:New(AUFTRAG.Type.PATROLRACETRACK)
  
    -- Target.
  mission:_TargetFromObject(Coordinate)
  
  -- Set Altitude.  
  if Altitude then
    mission.TrackAltitude=UTILS.FeetToMeters(Altitude)
  else
    mission.TrackAltitude=UTILS.FeetToMeters(20000)
  end
  
  -- Points
  mission.TrackPoint1 = Coordinate
  
  local leg = UTILS.NMToMeters(Leg) or UTILS.NMToMeters(10)
  
  local heading = Heading or 90
  
  if heading < 0 or heading > 360 then heading = 90 end
  
  mission.TrackPoint2 = Coordinate:Translate(leg,heading,true)
  
  -- Orbit speed in m/s TAS.
  mission.TrackSpeed = UTILS.IasToTas(UTILS.KnotsToKmph(Speed or 300), mission.TrackAltitude)
  
  -- Mission speed in km/h and altitude
  mission.missionSpeed = UTILS.KnotsToKmph(Speed or 300)
  mission.missionAltitude = mission.TrackAltitude * 0.9
  mission.missionTask=ENUMS.MissionTask.CAP
  mission.optionROE=ENUMS.ROE.ReturnFire
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()
  
  return mission
end


--- **[AIR]** Create an ORBIT mission, which can be either a circular orbit or a race-track pattern.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Where to orbit.
-- @param #number Altitude Orbit altitude in feet above sea level. Default is y component of `Coordinate`.
-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. Default 350 KIAS.
-- @param #number Heading Heading of race-track pattern in degrees. If not specified, a circular orbit is performed.
-- @param #number Leg Length of race-track in NM. If not specified, a circular orbit is performed.
-- @return #AUFTRAG self
function AUFTRAG:NewORBIT(Coordinate, Altitude, Speed, Heading, Leg)

  local mission=AUFTRAG:New(AUFTRAG.Type.ORBIT)
  
  -- Target.
  mission:_TargetFromObject(Coordinate)
  
  -- Set Altitude.  
  if Altitude then
    mission.orbitAltitude=UTILS.FeetToMeters(Altitude)
  else
    mission.orbitAltitude=Coordinate.y
  end
    
  -- Orbit speed in m/s TAS.
  mission.orbitSpeed = UTILS.IasToTas(UTILS.KnotsToMps(Speed or 350), mission.orbitAltitude)
  
  -- Mission speed in km/h.
  mission.missionSpeed = UTILS.KnotsToKmph(Speed or 350)
  
  if Leg then
    mission.orbitLeg=UTILS.NMToMeters(Leg)

    -- Relative heading
    if Heading and Heading<0 then
      mission.orbitHeadingRel=true
      Heading=-Heading
    end      

    -- Heading if given.
    mission.orbitHeading=Heading    
  end

  -- Mission options:
  mission.missionAltitude=mission.orbitAltitude*0.9
  mission.missionFraction=0.9
  mission.optionROE=ENUMS.ROE.ReturnFire
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR]** Create an ORBIT mission, where the aircraft will go in a circle around the specified coordinate.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Position where to orbit around.
-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`.
-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. Default 350 KIAS.
-- @return #AUFTRAG self
function AUFTRAG:NewORBIT_CIRCLE(Coordinate, Altitude, Speed)

  local mission=AUFTRAG:NewORBIT(Coordinate, Altitude, Speed)

  return mission
end

--- **[AIR]** Create an ORBIT mission, where the aircraft will fly a race-track pattern.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Where to orbit.
-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`.
-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. Default 350 KIAS.
-- @param #number Heading Heading of race-track pattern in degrees. Default random in [0, 360) degrees.
-- @param #number Leg Length of race-track in NM. Default 10 NM.
-- @return #AUFTRAG self
function AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg)

  Heading = Heading or math.random(360)
  Leg     = Leg or 10

  local mission=AUFTRAG:NewORBIT(Coordinate, Altitude, Speed, Heading, Leg)

  return mission
end

--- **[AIR]** Create an ORBIT mission, where the aircraft will fly a circular or race-track pattern over a given group or unit.
-- @param #AUFTRAG self
-- @param Wrapper.Group#GROUP Group Group where to orbit around. Can also be a UNIT object.
-- @param #number Altitude Orbit altitude in feet. Default is 6,000 ft.
-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. Default 350 KIAS.
-- @param #number Leg Length of race-track in NM. Default nil.
-- @param #number Heading Heading of race-track pattern in degrees. Default is heading of the group.
-- @param DCS#Vec2 OffsetVec2 Offset 2D-vector {x=0, y=0} in NM with respect to the group. Default directly overhead. Can also be given in polar coordinates `{r=5, phi=45}`.
-- @param #number Distance Threshold distance in NM before orbit pattern is updated. Default 5 NM.
-- @return #AUFTRAG self
function AUFTRAG:NewORBIT_GROUP(Group, Altitude, Speed, Leg, Heading, OffsetVec2, Distance)

  -- Set default altitude.
  Altitude = Altitude or 6000

  -- Create orbit mission.
  local mission=AUFTRAG:NewORBIT(Group, Altitude, Speed, Heading, Leg)

  -- DCS tasks needs to be updated from time to time.  
  mission.updateDCSTask=true
  
  -- Convert offset vector to meters.
  if OffsetVec2 then
    if OffsetVec2.x then
      OffsetVec2.x=UTILS.NMToMeters(OffsetVec2.x)
    end
    if OffsetVec2.y then
      OffsetVec2.y=UTILS.NMToMeters(OffsetVec2.y)
    end
    if OffsetVec2.r then
      OffsetVec2.r=UTILS.NMToMeters(OffsetVec2.r)
    end    
  end
  
  -- Offset vector.
  mission.orbitOffsetVec2=OffsetVec2
  
  -- Pattern update distance.
  mission.orbitDeltaR=UTILS.NMToMeters(Distance or 5)

  -- Update task with offset etc.
  mission:GetDCSMissionTask()

  return mission
end


--- **[AIR]** Create a Ground Controlled CAP (GCICAP) mission. Flights with this task are considered for A2A INTERCEPT missions by the CHIEF class. They will perform a combat air patrol but not engage by
-- themselfs. They wait for the CHIEF to tell them whom to engage.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Where to orbit.
-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`.
-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. Default 350 KIAS.
-- @param #number Heading Heading of race-track pattern in degrees. Default random in [0, 360) degrees.
-- @param #number Leg Length of race-track in NM. Default 10 NM.
-- @return #AUFTRAG self
function AUFTRAG:NewGCICAP(Coordinate, Altitude, Speed, Heading, Leg)

  -- Create ORBIT first.
  local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg)

  -- Mission type GCICAP.
  mission.type=AUFTRAG.Type.GCICAP

  mission:_SetLogID()

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.INTERCEPT
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  return mission
end

--- **[AIR]** Create a TANKER mission.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Where to orbit.
-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`.
-- @param #number Speed Orbit indicated airspeed in knots at the set altitude ASL. Default 350 KIAS.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 10 NM. Set to 0 for a simple circular orbit.
-- @param #number RefuelSystem Refueling system (0=boom, 1=probe). This info is *only* for AIRWINGs so they launch the right tanker type.
-- @return #AUFTRAG self
function AUFTRAG:NewTANKER(Coordinate, Altitude, Speed, Heading, Leg, RefuelSystem)
  
  local mission
  if Leg == 0 then
    mission=AUFTRAG:NewORBIT_CIRCLE(Coordinate,Altitude,Speed)
  else
    mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate,Altitude,Speed,Heading,Leg)
  end
  -- Create ORBIT first.
  --local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg)

  -- Mission type TANKER.
  mission.type=AUFTRAG.Type.TANKER

  mission:_SetLogID()

  mission.refuelSystem=RefuelSystem

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.REFUELING
  mission.optionROE=ENUMS.ROE.WeaponHold
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR]** Create a AWACS mission.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Where to orbit. Altitude is also taken from the coordinate.
-- @param #number Altitude Orbit altitude in feet. Default is y component of `Coordinate`.
-- @param #number Speed Orbit speed in knots. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 10 NM.
-- @return #AUFTRAG self
function AUFTRAG:NewAWACS(Coordinate, Altitude, Speed, Heading, Leg)

  -- Create ORBIT first.
  local mission=AUFTRAG:NewORBIT_RACETRACK(Coordinate, Altitude, Speed, Heading, Leg)

  -- Mission type AWACS.
  mission.type=AUFTRAG.Type.AWACS

  mission:_SetLogID()

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.AWACS
  mission.optionROE=ENUMS.ROE.WeaponHold
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end



--- **[AIR]** Create an INTERCEPT mission.
-- @param #AUFTRAG self
-- @param Wrapper.Positionable#POSITIONABLE Target The target to intercept. Can also be passed as simple @{Wrapper.Group#GROUP} or @{Wrapper.Unit#UNIT} object.
-- @return #AUFTRAG self
function AUFTRAG:NewINTERCEPT(Target)

  local mission=AUFTRAG:New(AUFTRAG.Type.INTERCEPT)

  mission:_TargetFromObject(Target)

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.INTERCEPT
  mission.missionFraction=0.1
  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.EvadeFire

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR]** Create a CAP mission.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE_RADIUS ZoneCAP Circular CAP zone. Detected targets in this zone will be engaged.
-- @param #number Altitude Altitude at which to orbit in feet. Default is 10,000 ft.
-- @param #number Speed Orbit speed in knots. Default 350 kts.
-- @param Core.Point#COORDINATE Coordinate Where to orbit. Default is the center of the CAP zone.
-- @param #number Heading Heading of race-track pattern in degrees. If not specified, a simple circular orbit is performed.
-- @param #number Leg Length of race-track in NM. If not specified, a simple circular orbit is performed.
-- @param #table TargetTypes Table of target types. Default {"Air"}.
-- @return #AUFTRAG self
function AUFTRAG:NewCAP(ZoneCAP, Altitude, Speed, Coordinate, Heading, Leg, TargetTypes)

  -- Ensure given TargetTypes parameter is a table.
  TargetTypes=UTILS.EnsureTable(TargetTypes, true)

  -- Set default altitude if not specified.
  Altitude = Altitude or 10000

  -- Create ORBIT first.
  local mission=AUFTRAG:NewORBIT(Coordinate or ZoneCAP:GetCoordinate(), Altitude, Speed or 350, Heading, Leg)

  -- Mission type CAP.
  mission.type=AUFTRAG.Type.CAP
  mission:_SetLogID()

  -- DCS task parameters:
  mission.engageZone=ZoneCAP or Coordinate
  mission.engageTargetTypes=TargetTypes or {"Air"}

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.CAP
  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.EvadeFire
  mission.missionSpeed = UTILS.KnotsToKmph(UTILS.KnotsToAltKIAS(Speed or 350, Altitude))

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR]** Create a CAP mission over a (moving) group.
-- @param #AUFTRAG self
-- @param Wrapper.Group#GROUP Grp The grp to perform the CAP over.
-- @param #number Altitude Orbit altitude in feet. Default is 6,000 ft.
-- @param #number Speed Orbit speed in knots. Default 250 KIAS.
-- @param #number RelHeading Relative heading [0, 360) of race-track pattern in degrees wrt heading of the carrier. Default is heading of the carrier.
-- @param #number Leg Length of race-track in NM. Default 14 NM.
-- @param #number OffsetDist Relative distance of the first race-track point wrt to the carrier. Default 6 NM.
-- @param #number OffsetAngle Relative angle of the first race-track point wrt. to the carrier. Default 180 (behind the boat).
-- @param #number UpdateDistance Threshold distance in NM before orbit pattern is updated. Default 5 NM.
-- @param #table TargetTypes (Optional) Table of target types. Default `{"Air"}`.
-- @param #number EngageRange Max range in nautical miles that the escort group(s) will engage enemies. Default 32 NM (60 km).
-- @return #AUFTRAG self
function AUFTRAG:NewCAPGROUP(Grp, Altitude, Speed, RelHeading, Leg, OffsetDist, OffsetAngle, UpdateDistance, TargetTypes, EngageRange)

  -- Ensure given TargetTypes parameter is a table.
  TargetTypes=UTILS.EnsureTable(TargetTypes, true)

  -- Six NM astern.
 local OffsetVec2={r=OffsetDist or 6, phi=OffsetAngle or 180}

 -- Default leg.
 Leg=Leg or 14

 local Heading=nil
 if RelHeading then  
   Heading=-math.abs(RelHeading)
 end  

 -- Create orbit mission. 
 local mission=AUFTRAG:NewORBIT_GROUP(Grp, Altitude, Speed, Leg, Heading, OffsetVec2, UpdateDistance)
 -- Mission type CAP.
 mission.type=AUFTRAG.Type.CAP
 mission:_SetLogID()

 -- DCS task parameters:
 local engage = EngageRange or 32
 local zoneCAPGroup = ZONE_GROUP:New("CAPGroup", Grp, UTILS.NMToMeters(engage))
 mission.engageZone=zoneCAPGroup
 mission.engageTargetTypes=TargetTypes or {"Air"}

 -- Mission options:
 mission.missionTask=ENUMS.MissionTask.CAP
 mission.optionROE=ENUMS.ROE.OpenFire
 mission.optionROT=ENUMS.ROT.EvadeFire

 mission.categories={AUFTRAG.Category.AIRCRAFT}

 mission.DCStask=mission:GetDCSMissionTask()

 return mission
end

--- **[AIR]** Create a CAS mission.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE_RADIUS ZoneCAS Circular CAS zone. Detected targets in this zone will be engaged.
-- @param #number Altitude Altitude at which to orbit. Default is 10,000 ft.
-- @param #number Speed Orbit speed in knots. Default 350 KIAS.
-- @param Core.Point#COORDINATE Coordinate Where to orbit. Default is the center of the CAS zone.
-- @param #number Heading Heading of race-track pattern in degrees. If not specified, a simple circular orbit is performed.
-- @param #number Leg Length of race-track in NM. If not specified, a simple circular orbit is performed.
-- @param #table TargetTypes (Optional) Table of target types. Default `{"Helicopters", "Ground Units", "Light armed ships"}`.
-- @return #AUFTRAG self
function AUFTRAG:NewCAS(ZoneCAS, Altitude, Speed, Coordinate, Heading, Leg, TargetTypes)

  -- Ensure given TargetTypes parameter is a table.
  TargetTypes=UTILS.EnsureTable(TargetTypes, true)

  -- Create ORBIT first.
  local mission=AUFTRAG:NewORBIT(Coordinate or ZoneCAS:GetCoordinate(), Altitude or 10000, Speed, Heading, Leg)

  -- Mission type CAS.
  mission.type=AUFTRAG.Type.CAS
  mission:_SetLogID()

  -- DCS Task options:
  mission.engageZone=ZoneCAS
  mission.engageTargetTypes=TargetTypes or {"Helicopters", "Ground Units", "Light armed ships"}

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.CAS
  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.EvadeFire

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR]** Create a CASENHANCED mission. Group(s) will go to the zone and patrol it randomly.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE CasZone The CAS zone.
-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL.
-- @param #number Speed Speed in knots.
-- @param #number RangeMax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM.
-- @param Core.Set#SET_ZONE NoEngageZoneSet Set of zones in which targets are *not* engaged. Default is nowhere.
-- @param #table TargetTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default `{"Helicopters", "Ground Units", "Light armed ships"}`.
-- @return #AUFTRAG self
function AUFTRAG:NewCASENHANCED(CasZone, Altitude, Speed, RangeMax, NoEngageZoneSet, TargetTypes)

  local mission=AUFTRAG:New(AUFTRAG.Type.CASENHANCED)

  -- Ensure we got a ZONE and not just the zone name.
  if type(CasZone)=="string" then
    CasZone=ZONE:New(CasZone)
  end

  mission:_TargetFromObject(CasZone)

  mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.CASENHANCED)

  mission:SetEngageDetected(RangeMax, TargetTypes or {"Helicopters", "Ground Units", "Light armed ships"}, CasZone, NoEngageZoneSet)

  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.EvadeFire

  mission.missionFraction=0.5
  mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil
  mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or nil
  
  -- Evaluate result after x secs. We might need time until targets have been detroyed.
  mission.dTevaluate=15
  
  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()
  

  return mission
end

--- **[AIR, GROUND]** Create a FAC mission. Group(s) will go to the zone and patrol it randomly and act as FAC for detected units.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE FacZone The FAC zone (or name of zone) where to patrol.
-- @param #number Speed Speed in knots.
-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL. 
-- @param #number Frequency Frequency in MHz.
-- @param #number Modulation Modulation.
-- @return #AUFTRAG self
function AUFTRAG:NewFAC(FacZone, Speed, Altitude, Frequency, Modulation)

  local mission=AUFTRAG:New(AUFTRAG.Type.FAC)

  -- Ensure we got a ZONE and not just the zone name.
  if type(FacZone)=="string" then
    FacZone=ZONE:FindByName(FacZone)
  end

  mission:_TargetFromObject(FacZone)

  mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.FAC)
  
  mission.facFreq=Frequency or 133
  mission.facModu=Modulation or radio.modulation.AM

  mission.optionROE=ENUMS.ROE.ReturnFire
  mission.optionROT=ENUMS.ROT.EvadeFire
  mission.optionAlarm=ENUMS.AlarmState.Auto

  mission.missionFraction=1.0
  mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil
  mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or nil

  mission.categories={AUFTRAG.Category.AIRCRAFT, AUFTRAG.Category.GROUND}

  mission.DCStask=mission:GetDCSMissionTask()
  

  return mission
end


--- **[AIR]** Create a FACA mission.
-- @param #AUFTRAG self
-- @param Wrapper.Group#GROUP Target Target group. Must be a GROUP object.
-- @param #string Designation Designation of target. See `AI.Task.Designation`. Default `AI.Task.Designation.AUTO`.
-- @param #boolean DataLink Enable data link. Default `true`.
-- @param #number Frequency Radio frequency in MHz the FAC uses for communication. Default is 133 MHz.
-- @param #number Modulation Radio modulation band. Default 0=AM. Use 1 for FM. See radio.modulation.AM or radio.modulaton.FM.
-- @return #AUFTRAG self
function AUFTRAG:NewFACA(Target, Designation, DataLink, Frequency, Modulation)

  local mission=AUFTRAG:New(AUFTRAG.Type.FACA)

  mission:_TargetFromObject(Target)

  -- TODO: check that target is really a group object!

  -- DCS Task options:
  mission.facDesignation=Designation --or AI.Task.Designation.AUTO
  mission.facDatalink=true
  mission.facFreq=Frequency or 133
  mission.facModu=Modulation or radio.modulation.AM

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.AFAC
  mission.missionAltitude=nil
  mission.missionFraction=0.5
  mission.optionROE=ENUMS.ROE.ReturnFire
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end


--- **[AIR]** Create a BAI mission.
-- @param #AUFTRAG self
-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP, UNIT or STATIC object.
-- @param #number Altitude Engage altitude in feet. Default 5000 ft.
-- @return #AUFTRAG self
function AUFTRAG:NewBAI(Target, Altitude)

  local mission=AUFTRAG:New(AUFTRAG.Type.BAI)

  mission:_TargetFromObject(Target)

  -- DCS Task options:
  mission.engageWeaponType=ENUMS.WeaponFlag.Auto
  mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL
  mission.engageAltitude=UTILS.FeetToMeters(Altitude or 5000)

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.GROUNDATTACK
  mission.missionAltitude=mission.engageAltitude
  mission.missionFraction=0.75
  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR]** Create a SEAD mission.
-- @param #AUFTRAG self
-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP or UNIT object.
-- @param #number Altitude Engage altitude in feet. Default 25000 ft.
-- @return #AUFTRAG self
function AUFTRAG:NewSEAD(Target, Altitude)

  local mission=AUFTRAG:New(AUFTRAG.Type.SEAD)

  mission:_TargetFromObject(Target)

  -- DCS Task options:
  mission.engageWeaponType=ENUMS.WeaponFlag.Auto
  mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL
  mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000)

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.SEAD
  mission.missionAltitude=mission.engageAltitude
  mission.missionFraction=0.2
  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.EvadeFire

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR]** Create a SEAD in Zone mission.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE TargetZone The target zone to attack.
-- @param #number Altitude Engage altitude in feet. Default 25000 ft.
-- @param #table TargetTypes Table of string of DCS known target types, defaults to {"Air Defence"}. See [DCS Target Attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes)
-- @param #number Duration Engage this much time when the AUFTRAG starts executing.
-- @return #AUFTRAG self
function AUFTRAG:NewSEADInZone(TargetZone, Altitude, TargetTypes, Duration)

  local mission=AUFTRAG:New(AUFTRAG.Type.SEAD)

  --mission:_TargetFromObject(TargetZone)

  -- DCS Task options:
  mission.engageWeaponType=ENUMS.WeaponFlag.Auto
  mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL
  mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000)
  mission.engageZone = TargetZone
  mission.engageTargetTypes = TargetTypes or {"Air Defence"}

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.SEAD
  mission.missionAltitude=mission.engageAltitude
  mission.missionFraction=0.2
  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.EvadeFire

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()
  
  mission:SetDuration(Duration or 1800)

  return mission
end

--- **[AIR]** Create a STRIKE mission. Flight will attack the closest map object to the specified coordinate.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Target The target coordinate. Can also be given as a GROUP, UNIT, STATIC, SET_GROUP, SET_UNIT, SET_STATIC or TARGET object.
-- @param #number Altitude Engage altitude in feet. Default 2000 ft.
-- @param #number EngageWeaponType Which weapon to use. Defaults to auto, ie ENUMS.WeaponFlag.Auto. See ENUMS.WeaponFlag for options.
-- @return #AUFTRAG self
function AUFTRAG:NewSTRIKE(Target, Altitude, EngageWeaponType)

  local mission=AUFTRAG:New(AUFTRAG.Type.STRIKE)

  mission:_TargetFromObject(Target)

  -- DCS Task options:
  mission.engageWeaponType=EngageWeaponType or ENUMS.WeaponFlag.Auto
  mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL
  mission.engageAltitude=UTILS.FeetToMeters(Altitude or 2000)

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.GROUNDATTACK
  mission.missionAltitude=mission.engageAltitude
  mission.missionFraction=0.75
  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR]** Create a BOMBING mission. Flight will drop bombs a specified coordinate.
-- See [DCS task bombing](https://wiki.hoggitworld.com/view/DCS_task_bombing).
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT, STATIC, SET_GROUP, SET_UNIT, SET_STATIC or TARGET object.
-- @param #number Altitude Engage altitude in feet. Default 25000 ft.
-- @param #number EngageWeaponType Which weapon to use. Defaults to auto, ie ENUMS.WeaponFlag.Auto. See ENUMS.WeaponFlag for options.
-- @param #boolean Divebomb If true, use a dive bombing attack approach.
-- @return #AUFTRAG self
function AUFTRAG:NewBOMBING(Target, Altitude, EngageWeaponType, Divebomb)

  local mission=AUFTRAG:New(AUFTRAG.Type.BOMBING)

  mission:_TargetFromObject(Target)

  -- DCS task options:
  mission.engageWeaponType=EngageWeaponType or ENUMS.WeaponFlag.Auto
  mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL
  mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000)

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.GROUNDATTACK
  mission.missionAltitude=mission.engageAltitude*0.8
  mission.missionFraction=0.5
  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.NoReaction   -- No reaction is better.
  mission.optionDivebomb = Divebomb or nil

  -- Evaluate result after 5 min. We might need time until the bombs have dropped and targets have been detroyed.
  mission.dTevaluate=5*60

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  -- Get DCS task.
  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR]** Create a STRAFING mission. Assigns a point on the ground for which the AI will do a strafing run with guns or rockets.
-- See [DCS task strafing](https://wiki.hoggitworld.com/view/DCS_task_strafing).
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT, STATIC or TARGET object.
-- @param #number Altitude Engage altitude in feet. Default 1000 ft.
-- @param #number Length The total length of the strafing target in meters. Default `nil`.
-- @return #AUFTRAG self
function AUFTRAG:NewSTRAFING(Target, Altitude, Length)

  local mission=AUFTRAG:New(AUFTRAG.Type.STRAFING)

  mission:_TargetFromObject(Target)

  -- DCS task options:
  mission.engageWeaponType=805337088 -- Corresponds to guns/cannons (805306368) + any rocket (30720). This is the default when selecting this task in the ME.
  mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL
  mission.engageAltitude=UTILS.FeetToMeters(Altitude or 1000)
  mission.engageLength=Length

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.GROUNDATTACK
  mission.missionAltitude=mission.engageAltitude*0.8
  mission.missionFraction=0.5
  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.NoReaction   -- No reaction is better.

  -- Evaluate result after 5 min. We might need time until the bombs have dropped and targets have been detroyed.
  mission.dTevaluate=5*60

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  -- Get DCS task.
  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end


--- **[AIR]** Create a BOMBRUNWAY mission.
-- @param #AUFTRAG self
-- @param Wrapper.Airbase#AIRBASE Airdrome The airbase to bomb. This must be an airdrome (not a FARP or ship) as these do not have a runway.
-- @param #number Altitude Engage altitude in feet. Default 25000 ft.
-- @return #AUFTRAG self
function AUFTRAG:NewBOMBRUNWAY(Airdrome, Altitude)

  if type(Airdrome)=="string" then
    Airdrome=AIRBASE:FindByName(Airdrome)
  end

  local mission=AUFTRAG:New(AUFTRAG.Type.BOMBRUNWAY)

  mission:_TargetFromObject(Airdrome)

  -- DCS task options:
  mission.engageWeaponType=ENUMS.WeaponFlag.Auto
  mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL
  mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000)

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.RUNWAYATTACK
  mission.missionAltitude=mission.engageAltitude*0.8
  mission.missionFraction=0.75
  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.PassiveDefense

  -- Evaluate result after 5 min.
  mission.dTevaluate=5*60

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  -- Get DCS task.
  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR]** Create a CARPET BOMBING mission.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Target Target coordinate. Can also be specified as a GROUP, UNIT or STATIC object.
-- @param #number Altitude Engage altitude in feet. Default 25000 ft.
-- @param #number CarpetLength Length of bombing carpet in meters. Default 500 m.
-- @return #AUFTRAG self
function AUFTRAG:NewBOMBCARPET(Target, Altitude, CarpetLength)

  local mission=AUFTRAG:New(AUFTRAG.Type.BOMBCARPET)

  mission:_TargetFromObject(Target)

  -- DCS task options:
  mission.engageWeaponType=ENUMS.WeaponFlag.Auto
  mission.engageWeaponExpend=AI.Task.WeaponExpend.ALL
  mission.engageAltitude=UTILS.FeetToMeters(Altitude or 25000)
  mission.engageLength=CarpetLength or 500
  mission.engageAsGroup=false  -- Looks like this must be false or the task is not executed. It is not available in the ME anyway but in the task of the mission file.
  mission.engageDirection=nil  -- This is also not available in the ME.

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.GROUNDATTACK
  mission.missionAltitude=mission.engageAltitude*0.8
  mission.missionFraction=0.5
  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.NoReaction

  -- Evaluate result after 5 min.
  mission.dTevaluate=5*60

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  -- Get DCS task.
  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR/HELO]** Create a GROUNDESCORT (or FOLLOW) mission. Helo will escort a **ground** group and automatically engage certain target types.
-- @param #AUFTRAG self
-- @param Wrapper.Group#GROUP EscortGroup The ground group to escort.
-- @param #number OrbitDistance Orbit to/from the lead unit this many NM. Defaults to 1.5 NM.
-- @param #table TargetTypes Types of targets to engage automatically. Default is {"Ground vehicles"}, i.e. all enemy ground units. Use an empty set {} for a simple "FOLLOW" mission.
-- @return #AUFTRAG self
function AUFTRAG:NewGROUNDESCORT(EscortGroup, OrbitDistance, TargetTypes)

  local mission=AUFTRAG:New(AUFTRAG.Type.GROUNDESCORT)

  -- If only a string is passed we set a variable and check later if the group exists.
  if type(EscortGroup)=="string" then
    mission.escortGroupName=EscortGroup
    mission:_TargetFromObject()
  else
    mission:_TargetFromObject(EscortGroup)
  end

  -- DCS task parameters:
  mission.orbitDistance=OrbitDistance and UTILS.NMToMeters(OrbitDistance) or UTILS.NMToMeters(1.5)
  --mission.engageMaxDistance=EngageMaxDistance and UTILS.NMToMeters(EngageMaxDistance) or UTILS.NMToMeters(5)
  mission.engageTargetTypes=TargetTypes or {"Ground vehicles"}

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.GROUNDESCORT
  mission.missionFraction=0.1
  mission.missionAltitude=100
  mission.optionROE=ENUMS.ROE.OpenFire       -- TODO: what's the best ROE here? Make dependent on ESCORT or FOLLOW!
  mission.optionROT=ENUMS.ROT.EvadeFire

  mission.categories={AUFTRAG.Category.HELICOPTER}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end


--- **[AIR]** Create an ESCORT (or FOLLOW) mission. Flight will escort another group and automatically engage certain target types.
-- @param #AUFTRAG self
-- @param Wrapper.Group#GROUP EscortGroup The group to escort.
-- @param DCS#Vec3 OffsetVector A table with x, y and z components specifying the offset of the flight to the escorted group. Default {x=-100, y=0, z=200} for z=200 meters to the right, same alitude (y=0), x=-100 meters behind.
-- @param #number EngageMaxDistance Max engage distance of targets in nautical miles. Default auto 32 NM.
-- @param #table TargetTypes Types of targets to engage automatically. Default is {"Air"}, i.e. all enemy airborne units. Use an empty set {} for a simple "FOLLOW" mission.
-- @return #AUFTRAG self
function AUFTRAG:NewESCORT(EscortGroup, OffsetVector, EngageMaxDistance, TargetTypes)

  local mission=AUFTRAG:New(AUFTRAG.Type.ESCORT)

  -- If only a string is passed we set a variable and check later if the group exists.
  if type(EscortGroup)=="string" then
    mission.escortGroupName=EscortGroup
    mission:_TargetFromObject()
  else
    mission:_TargetFromObject(EscortGroup)
  end

  -- DCS task parameters:
  mission.escortVec3=OffsetVector or {x=-100, y=0, z=200}
  mission.engageMaxDistance=EngageMaxDistance and UTILS.NMToMeters(EngageMaxDistance) or UTILS.NMToMeters(32)
  mission.engageTargetTypes=TargetTypes or {"Air"}

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.ESCORT
  mission.missionFraction=0.1
  mission.missionAltitude=1000
  mission.optionROE=ENUMS.ROE.OpenFire       -- TODO: what's the best ROE here? Make dependent on ESCORT or FOLLOW!
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR ROTARY]** Create a RESCUE HELO mission.
-- @param #AUFTRAG self
-- @param Wrapper.Unit#UNIT Carrier The carrier unit.
-- @return #AUFTRAG self
function AUFTRAG:NewRESCUEHELO(Carrier)

  local mission=AUFTRAG:New(AUFTRAG.Type.RESCUEHELO)

  mission:_TargetFromObject(Carrier)

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.NOTHING
  mission.missionFraction=0.5
  mission.optionROE=ENUMS.ROE.WeaponHold
  mission.optionROT=ENUMS.ROT.NoReaction

  mission.categories={AUFTRAG.Category.HELICOPTER}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIRPANE]** Create a RECOVERY TANKER mission.
-- @param #AUFTRAG self
-- @param Wrapper.Unit#UNIT Carrier The carrier unit.
-- @param #number Altitude Orbit altitude in feet. Default is 6,000 ft.
-- @param #number Speed Orbit speed in knots. Default 250 KIAS.
-- @param #number Leg Length of race-track in NM. Default 14 NM.
-- @param #number RelHeading Relative heading [0, 360) of race-track pattern in degrees wrt heading of the carrier. Default is heading of the carrier.
-- @param #number OffsetDist Relative distance of the first race-track point wrt to the carrier. Default 6 NM.
-- @param #number OffsetAngle Relative angle of the first race-track point wrt. to the carrier. Default 180 (behind the boat).
-- @param #number UpdateDistance Threshold distance in NM before orbit pattern is updated. Default 5 NM.
-- @return #AUFTRAG self
function AUFTRAG:NewRECOVERYTANKER(Carrier, Altitude, Speed, Leg, RelHeading, OffsetDist, OffsetAngle, UpdateDistance)
 
   -- Six NM astern.
  local OffsetVec2={r=OffsetDist or 6, phi=OffsetAngle or 180}
  
  -- Default leg.
  Leg=Leg or 14
  
  -- Default Speed.
  Speed=Speed or 250
  
  local Heading=nil
  if RelHeading then  
    Heading=-math.abs(RelHeading)
  end  
 
  -- Create orbit mission. 
  local mission=AUFTRAG:NewORBIT_GROUP(Carrier, Altitude, Speed, Leg, Heading, OffsetVec2, UpdateDistance)

  -- Set the type.  
  mission.type=AUFTRAG.Type.RECOVERYTANKER

  -- Mission options:
  mission.missionTask=ENUMS.MissionTask.REFUELING
  mission.missionFraction=0.9
  mission.optionROE=ENUMS.ROE.WeaponHold
  mission.optionROT=ENUMS.ROT.NoReaction

  mission.categories={AUFTRAG.Category.AIRPLANE}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end


--- **[AIR ROTARY, GROUND]** Create a TROOP TRANSPORT mission.
-- @param #AUFTRAG self
-- @param Core.Set#SET_GROUP TransportGroupSet The set group(s) to be transported.
-- @param Core.Point#COORDINATE DropoffCoordinate Coordinate where the helo will land drop off the the troops.
-- @param Core.Point#COORDINATE PickupCoordinate Coordinate where the helo will land to pick up the the cargo. Default is the first transport group.
-- @param #number PickupRadius Radius around the pickup coordinate in meters. Default 100 m.
-- @return #AUFTRAG self
function AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet, DropoffCoordinate, PickupCoordinate, PickupRadius)

  local mission=AUFTRAG:New(AUFTRAG.Type.TROOPTRANSPORT)

  if TransportGroupSet:IsInstanceOf("GROUP") then
    mission.transportGroupSet=SET_GROUP:New()
    mission.transportGroupSet:AddGroup(TransportGroupSet)
  elseif TransportGroupSet:IsInstanceOf("SET_GROUP") then
    mission.transportGroupSet=TransportGroupSet
  else
    mission:E(mission.lid.."ERROR: TransportGroupSet must be a GROUP or SET_GROUP object!")
    return nil
  end

  mission:_TargetFromObject(mission.transportGroupSet)

  mission.transportPickup=PickupCoordinate or mission:GetTargetCoordinate()
  mission.transportDropoff=DropoffCoordinate

  mission.transportPickupRadius=PickupRadius or 100

  mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.TROOPTRANSPORT)

  -- Debug.
  --mission.transportPickup:MarkToAll("Pickup Transport")
  --mission.transportDropoff:MarkToAll("Drop off")

  -- TODO: what's the best ROE here?
  mission.optionROE=ENUMS.ROE.ReturnFire
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.HELICOPTER, AUFTRAG.Category.GROUND}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR ROTARY]** Create a CARGO TRANSPORT mission.
-- **Important Note:**
-- The dropoff zone has to be a zone defined in the Mission Editor. This is due to a restriction in the used DCS task, which takes the zone ID as input.
-- Only ME zones have an ID that can be referenced.
-- @param #AUFTRAG self
-- @param Wrapper.Static#STATIC StaticCargo Static cargo object.
-- @param Core.Zone#ZONE DropZone Zone where to drop off the cargo. **Has to be a zone defined in the ME!**
-- @return #AUFTRAG self
function AUFTRAG:NewCARGOTRANSPORT(StaticCargo, DropZone)

  local mission=AUFTRAG:New(AUFTRAG.Type.CARGOTRANSPORT)

  mission:_TargetFromObject(StaticCargo)

  mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.CARGOTRANSPORT)

  -- Set ROE and ROT.
  mission.optionROE=ENUMS.ROE.ReturnFire
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.HELICOPTER}

  mission.DCStask=mission:GetDCSMissionTask()
  
  mission.DCStask.params.groupId=StaticCargo:GetID()
  mission.DCStask.params.zoneId=DropZone.ZoneID
  mission.DCStask.params.zone=DropZone
  mission.DCStask.params.cargo=StaticCargo
  
  return mission
end

--[[

--- **[AIR, GROUND, NAVAL]** Create a OPS TRANSPORT mission.
-- @param #AUFTRAG self
-- @param Core.Set#SET_GROUP CargoGroupSet The set group(s) to be transported.
-- @param Core.Zone#ZONE PickupZone Pick up zone
-- @param Core.Zone#ZONE DeployZone Deploy zone
-- @return #AUFTRAG self
function AUFTRAG:NewOPSTRANSPORT(CargoGroupSet, PickupZone, DeployZone)

  local mission=AUFTRAG:New(AUFTRAG.Type.OPSTRANSPORT)

  mission.transportGroupSet=CargoGroupSet

  mission:_TargetFromObject(mission.transportGroupSet)

  mission.opstransport=OPSTRANSPORT:New(CargoGroupSet, PickupZone, DeployZone)

  function mission.opstransport:OnAfterExecuting(From, Event, To)
    mission:Executing()
  end

  function mission.opstransport:OnAfterDelivered(From, Event, To)
    mission:Done()
  end

  -- TODO: what's the best ROE here?
  mission.optionROE=ENUMS.ROE.ReturnFire
  mission.optionROT=ENUMS.ROT.PassiveDefense

  mission.categories={AUFTRAG.Category.ALL}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

]]

--- **[GROUND, NAVAL]** Create an ARTY mission ("Fire at point" task).
-- 
-- If the group has more than one weapon type supporting the "Fire at point" task, the employed weapon type can be set via the `AUFTRAG:SetWeaponType()` function.
-- 
-- **Note** that it is recommended to set the weapon range via the `OPSGROUP:AddWeaponRange()` function as this cannot be retrieved from the DCS API.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Target Center of the firing solution.
-- @param #number Nshots Number of shots to be fired. Default `#nil`.
-- @param #number Radius Radius of the shells in meters. Default 100 meters.
-- @param #number Altitude Altitude in meters. Can be used to setup a Barrage. Default `#nil`.
-- @return #AUFTRAG self
function AUFTRAG:NewARTY(Target, Nshots, Radius, Altitude)

  local mission=AUFTRAG:New(AUFTRAG.Type.ARTY)

  mission:_TargetFromObject(Target)

  mission.artyShots=Nshots or nil
  mission.artyRadius=Radius or 100
  mission.artyAltitude=Altitude

  mission.engageWeaponType=ENUMS.WeaponFlag.Auto

  mission.optionROE=ENUMS.ROE.OpenFire   -- Ground/naval need open fire!
  mission.optionAlarm=0

  mission.missionFraction=0.0
  mission.missionWaypointRadius=0.0

  -- Evaluate after 8 min.
  mission.dTevaluate=8*60

  mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[GROUND, NAVAL]** Create an BARRAGE mission. Assigned groups will move to a random coordinate within a given zone and start firing into the air.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE Zone The zone where the unit will go.
-- @param #number Heading Heading in degrees. Default random heading [0, 360).
-- @param #number Angle Shooting angle in degrees. Default random [45, 85].
-- @param #number Radius Radius of the shells in meters. Default 100 meters.
-- @param #number Altitude Altitude in meters. Default 500 m.
-- @param #number Nshots Number of shots to be fired. Default is until ammo is empty (`#nil`).
-- @return #AUFTRAG self
function AUFTRAG:NewBARRAGE(Zone, Heading, Angle, Radius, Altitude, Nshots)

  local mission=AUFTRAG:New(AUFTRAG.Type.BARRAGE)

  mission:_TargetFromObject(Zone)

  mission.artyShots=Nshots
  mission.artyRadius=Radius or 100
  mission.artyAltitude=Altitude
  mission.artyHeading=Heading
  mission.artyAngle=Angle

  mission.engageWeaponType=ENUMS.WeaponFlag.Auto

  mission.optionROE=ENUMS.ROE.OpenFire   -- Ground/naval need open fire!
  mission.optionAlarm=0

  mission.missionFraction=0.0

  -- Evaluate after instantly.
  mission.dTevaluate=10

  mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[AIR, GROUND, NAVAL]** Create a PATROLZONE mission. Group(s) will go to the zone and patrol it randomly.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE Zone The patrol zone.
-- @param #number Speed Speed in knots.
-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL.
-- @param #string Formation Formation used by ground units during patrol. Default "Off Road".
-- @return #AUFTRAG self
function AUFTRAG:NewPATROLZONE(Zone, Speed, Altitude, Formation)

  local mission=AUFTRAG:New(AUFTRAG.Type.PATROLZONE)

  -- Ensure we got a ZONE and not just the zone name.
  if type(Zone)=="string" then
    Zone=ZONE:New(Zone)
  end

  mission:_TargetFromObject(Zone)

  mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.PATROLZONE)

  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionROT=ENUMS.ROT.PassiveDefense
  mission.optionAlarm=ENUMS.AlarmState.Auto

  mission.missionFraction=1.0
  mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil
  mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or nil

  mission.categories={AUFTRAG.Category.ALL}

  mission.DCStask=mission:GetDCSMissionTask()

  mission.DCStask.params.formation=Formation or "Off Road"

  return mission
end

--- **[AIR, GROUND, NAVAL]** Create a CAPTUREZONE mission. Group(s) will go to the zone and patrol it randomly.
-- @param #AUFTRAG self
-- @param Ops.OpsZone#OPSZONE OpsZone The OPS zone to capture.
-- @param #number Coalition The coalition which should capture the zone for the mission to be successful.
-- @param #number Speed Speed in knots.
-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL.
-- @param #string Formation Formation used by ground units during patrol. Default "Off Road".
-- @return #AUFTRAG self
function AUFTRAG:NewCAPTUREZONE(OpsZone, Coalition, Speed, Altitude, Formation)

  local mission=AUFTRAG:New(AUFTRAG.Type.CAPTUREZONE)


  mission:_TargetFromObject(OpsZone)
  
  mission.coalition=Coalition

  mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.CAPTUREZONE)

  mission.optionROE=ENUMS.ROE.ReturnFire
  mission.optionROT=ENUMS.ROT.PassiveDefense
  mission.optionAlarm=ENUMS.AlarmState.Auto

  mission.missionFraction=0.1
  mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil
  mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or nil

  mission.categories={AUFTRAG.Category.ALL}

  mission.DCStask=mission:GetDCSMissionTask()
  
  mission.updateDCSTask=true

  local params={}
  
  params.formation=Formation or "Off Road"  
  params.zone=mission:GetObjective()
  params.altitude=mission.missionAltitude
  params.speed=mission.missionSpeed and UTILS.KmphToMps(mission.missionSpeed) or nil

  mission.DCStask.params=params

  return mission
end


--- **[OBSOLETE]** Create a ARMORATTACK mission.
-- ** Note that this is actually creating a GROUNDATTACK mission!** 
-- @param #AUFTRAG self
-- @param Ops.Target#TARGET Target The target to attack. Can be a GROUP, UNIT or STATIC object.
-- @param #number Speed Speed in knots.
-- @param #string Formation The attack formation, e.g. "Wedge", "Vee" etc.
-- @return #AUFTRAG self
function AUFTRAG:NewARMORATTACK(Target, Speed, Formation)

  local mission=AUFTRAG:NewGROUNDATTACK(Target, Speed, Formation)
  
  -- Mission type.
  mission.type=AUFTRAG.Type.ARMORATTACK  

  return mission
end

--- **[GROUND]** Create a GROUNDATTACK mission. Ground group(s) will go to a target object and attack.
-- @param #AUFTRAG self
-- @param Wrapper.Positionable#POSITIONABLE Target The target to attack. Can be a GROUP, UNIT or STATIC object.
-- @param #number Speed Speed in knots. Default max.
-- @param #string Formation The attack formation, e.g. "Wedge", "Vee" etc. Default `ENUMS.Formation.Vehicle.Vee`.
-- @return #AUFTRAG self
function AUFTRAG:NewGROUNDATTACK(Target, Speed, Formation)

  local mission=AUFTRAG:New(AUFTRAG.Type.GROUNDATTACK)

  mission:_TargetFromObject(Target)

  mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.GROUNDATTACK)

  -- Defaults.
  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionAlarm=ENUMS.AlarmState.Auto  
  mission.optionFormation="On Road"
  mission.missionFraction=0.70
  mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil

  mission.categories={AUFTRAG.Category.GROUND}

  mission.DCStask=mission:GetDCSMissionTask()
  
  mission.DCStask.params.speed=mission.missionSpeed and UTILS.KmphToMps(mission.missionSpeed) or nil
  mission.DCStask.params.formation=Formation or ENUMS.Formation.Vehicle.Vee
  
  return mission
end

--- **[AIR, GROUND, NAVAL]** Create a RECON mission.
-- @param #AUFTRAG self
-- @param Core.Set#SET_ZONE ZoneSet The recon zones.
-- @param #number Speed Speed in knots.
-- @param #number Altitude Altitude in feet. Only for airborne units. Default 2000 feet ASL.
-- @param #boolean Adinfinitum If `true`, the group will start over again after reaching the final zone.
-- @param #boolean Randomly If `true`, the group will select a random zone.
-- @param #string Formation Formation used during recon route.
-- @return #AUFTRAG self
function AUFTRAG:NewRECON(ZoneSet, Speed, Altitude, Adinfinitum, Randomly, Formation)

  local mission=AUFTRAG:New(AUFTRAG.Type.RECON)

  mission:_TargetFromObject(ZoneSet)
  
  if ZoneSet:IsInstanceOf("SET_ZONE") then
    mission.missionZoneSet = ZoneSet
  elseif ZoneSet:IsInstanceOf("ZONE_BASE") then
   mission.missionZoneSet = SET_ZONE:New()
   mission.missionZoneSet:AddZone(ZoneSet)
  end
  
  
  mission.missionTask=mission:GetMissionTaskforMissionType(AUFTRAG.Type.RECON)

  mission.optionROE=ENUMS.ROE.WeaponHold
  mission.optionROT=ENUMS.ROT.PassiveDefense
  mission.optionAlarm=ENUMS.AlarmState.Auto

  mission.missionFraction=0.5
  mission.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil
  mission.missionAltitude=Altitude and UTILS.FeetToMeters(Altitude) or UTILS.FeetToMeters(2000)

  mission.categories={AUFTRAG.Category.ALL}

  mission.DCStask=mission:GetDCSMissionTask()
  mission.DCStask.params.adinfinitum=Adinfinitum
  mission.DCStask.params.randomly=Randomly
  mission.DCStask.params.formation=Formation

  return mission
end

--- **[GROUND]** Create a AMMO SUPPLY mission.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE Zone The zone, where supply units go.
-- @return #AUFTRAG self
function AUFTRAG:NewAMMOSUPPLY(Zone)

  local mission=AUFTRAG:New(AUFTRAG.Type.AMMOSUPPLY)

  mission:_TargetFromObject(Zone)

  mission.optionROE=ENUMS.ROE.WeaponHold
  mission.optionAlarm=ENUMS.AlarmState.Auto

  mission.missionFraction=1.0

  mission.missionWaypointRadius=0

  mission.categories={AUFTRAG.Category.GROUND}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[GROUND]** Create a FUEL SUPPLY mission.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE Zone The zone, where supply units go.
-- @return #AUFTRAG self
function AUFTRAG:NewFUELSUPPLY(Zone)

  local mission=AUFTRAG:New(AUFTRAG.Type.FUELSUPPLY)

  mission:_TargetFromObject(Zone)

  mission.optionROE=ENUMS.ROE.WeaponHold
  mission.optionAlarm=ENUMS.AlarmState.Auto

  mission.missionFraction=1.0

  mission.categories={AUFTRAG.Category.GROUND}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[GROUND]** Create a REARMING mission.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE Zone The zone, where units go and look for ammo supply.
-- @return #AUFTRAG self
function AUFTRAG:NewREARMING(Zone)

  local mission=AUFTRAG:New(AUFTRAG.Type.REARMING)

  mission:_TargetFromObject(Zone)

  mission.optionROE=ENUMS.ROE.WeaponHold
  mission.optionAlarm=ENUMS.AlarmState.Auto

  mission.missionFraction=1.0

  mission.missionWaypointRadius=0

  mission.categories={AUFTRAG.Category.GROUND}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end


--- **[AIR]** Create an ALERT 5 mission. Aircraft will be spawned uncontrolled and wait for an assignment. You must specify **one** mission type which is performed.
-- This determines the payload and the DCS mission task which are used when the aircraft is spawned.
-- @param #AUFTRAG self
-- @param #string MissionType Mission type `AUFTRAG.Type.XXX`. Determines payload and mission task (intercept, ground attack, etc.).
-- @return #AUFTRAG self
function AUFTRAG:NewALERT5(MissionType)

  local mission=AUFTRAG:New(AUFTRAG.Type.ALERT5)

  mission.missionTask=self:GetMissionTaskforMissionType(MissionType)

  mission.optionROE=ENUMS.ROE.WeaponHold
  mission.optionROT=ENUMS.ROT.NoReaction

  mission.alert5MissionType=MissionType

  mission.missionFraction=1.0

  mission.categories={AUFTRAG.Category.AIRCRAFT}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[GROUND, NAVAL]** Create an ON GUARD mission.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Coordinate, where to stand guard.
-- @return #AUFTRAG self
function AUFTRAG:NewONGUARD(Coordinate)

  local mission=AUFTRAG:New(AUFTRAG.Type.ONGUARD)

  mission:_TargetFromObject(Coordinate)

  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionAlarm=ENUMS.AlarmState.Auto

  mission.missionFraction=1.0

  mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[GROUND, NAVAL]** Create an AIRDEFENSE mission.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE Zone Zone where the air defense group(s) should be stationed.
-- @return #AUFTRAG self
function AUFTRAG:NewAIRDEFENSE(Zone)

  local mission=AUFTRAG:New(AUFTRAG.Type.AIRDEFENSE)

  mission:_TargetFromObject(Zone)

  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionAlarm=ENUMS.AlarmState.Auto

  mission.missionFraction=1.0

  mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- **[GROUND]** Create an EWR mission.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE Zone Zone where the Early Warning Radar group(s) should be stationed.
-- @return #AUFTRAG self
function AUFTRAG:NewEWR(Zone)

  local mission=AUFTRAG:New(AUFTRAG.Type.EWR)

  mission:_TargetFromObject(Zone)

  mission.optionROE=ENUMS.ROE.WeaponHold
  mission.optionAlarm=ENUMS.AlarmState.Auto

  mission.missionFraction=1.0

  mission.categories={AUFTRAG.Category.GROUND}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end


--- **[PRIVATE, AIR, GROUND, NAVAL]** Create a mission to relocate all cohort assets to another LEGION.
-- @param #AUFTRAG self
-- @param Ops.Legion#LEGION Legion The new legion.
-- @param Ops.Cohort#COHORT Cohort The cohort to be relocated.
-- @return #AUFTRAG self
function AUFTRAG:_NewRELOCATECOHORT(Legion, Cohort)

  local mission=AUFTRAG:New(AUFTRAG.Type.RELOCATECOHORT)
  
  mission:_TargetFromObject(Legion.spawnzone)

  mission.optionROE=ENUMS.ROE.ReturnFire
  mission.optionAlarm=ENUMS.AlarmState.Auto

  mission.missionFraction=0.0

  mission.categories={AUFTRAG.Category.ALL}

  mission.DCStask=mission:GetDCSMissionTask()
  
  if Cohort.isGround then
    mission.optionFormation=ENUMS.Formation.Vehicle.OnRoad
  end
    
  mission.DCStask.params.legion=Legion
  mission.DCStask.params.cohort=Cohort

  return mission
end

--- **[GROUND, NAVAL]** Create a mission to do NOTHING.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE RelaxZone Zone where the assets are supposed to do nothing.
-- @return #AUFTRAG self
function AUFTRAG:NewNOTHING(RelaxZone)

  local mission=AUFTRAG:New(AUFTRAG.Type.NOTHING)

  mission:_TargetFromObject(RelaxZone)

  mission.optionROE=ENUMS.ROE.WeaponHold
  mission.optionAlarm=ENUMS.AlarmState.Auto

  mission.missionFraction=1.0

  mission.categories={AUFTRAG.Category.GROUND, AUFTRAG.Category.NAVAL}

  mission.DCStask=mission:GetDCSMissionTask()
    
  return mission
end

--- **[GROUND]** Create an ARMORED ON GUARD mission.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Coordinate, where to stand guard.
-- @param #string Formation Formation to take, e.g. "On Road", "Vee" etc.
-- @return #AUFTRAG self
function AUFTRAG:NewARMOREDGUARD(Coordinate,Formation)

  local mission=AUFTRAG:New(AUFTRAG.Type.ARMOREDGUARD)

  mission:_TargetFromObject(Coordinate)

  mission.optionROE=ENUMS.ROE.OpenFire
  mission.optionAlarm=ENUMS.AlarmState.Auto
  mission.optionFormation=Formation or "On Road"

  mission.missionFraction=1.0

  mission.categories={AUFTRAG.Category.GROUND}

  mission.DCStask=mission:GetDCSMissionTask()

  return mission
end

--- Create a mission to attack a TARGET object.
-- @param #AUFTRAG self
-- @param Ops.Target#TARGET Target The target.
-- @param #string MissionType The mission type.
-- @return #AUFTRAG self
function AUFTRAG:NewFromTarget(Target, MissionType)

  local mission=nil --#AUFTRAG

  if MissionType==AUFTRAG.Type.ANTISHIP then
    mission=self:NewANTISHIP(Target, Altitude)
  elseif MissionType==AUFTRAG.Type.ARTY then
    mission=self:NewARTY(Target, Nshots, Radius)
  elseif MissionType==AUFTRAG.Type.BAI then
    mission=self:NewBAI(Target, Altitude)
  elseif MissionType==AUFTRAG.Type.BOMBCARPET then
    mission=self:NewBOMBCARPET(Target, Altitude, CarpetLength)
  elseif MissionType==AUFTRAG.Type.BOMBING then
    mission=self:NewBOMBING(Target, Altitude)
  elseif MissionType==AUFTRAG.Type.BOMBRUNWAY then
    mission=self:NewBOMBRUNWAY(Target, Altitude)
  elseif MissionType==AUFTRAG.Type.STRAFING then
    mission=self:NewSTRAFING(Target, Altitude)    
  elseif MissionType==AUFTRAG.Type.CAS then
    mission=self:NewCAS(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000), Altitude, Speed, Target:GetAverageCoordinate(), Heading, Leg, TargetTypes)
  elseif MissionType==AUFTRAG.Type.CASENHANCED then
    mission=self:NewCASENHANCED(ZONE_RADIUS:New(Target:GetName(),Target:GetVec2(),1000), Altitude, Speed, RangeMax, NoEngageZoneSet, TargetTypes)
  elseif MissionType==AUFTRAG.Type.INTERCEPT then
    mission=self:NewINTERCEPT(Target)
  elseif MissionType==AUFTRAG.Type.SEAD then
    mission=self:NewSEAD(Target, Altitude)
  elseif MissionType==AUFTRAG.Type.STRIKE then
    mission=self:NewSTRIKE(Target, Altitude)
  elseif MissionType==AUFTRAG.Type.ARMORATTACK then
    mission=self:NewARMORATTACK(Target, Speed)
  elseif MissionType==AUFTRAG.Type.GROUNDATTACK then
    mission=self:NewGROUNDATTACK(Target, Speed, Formation)
  else
    return nil
  end

  return mission
end


--- Create a mission to attack a group. Mission type is automatically chosen from the group category.
-- @param #AUFTRAG self
-- @param Wrapper.Positionable#POSITIONABLE Target Target object.
-- @return #string Auftrag type, e.g. `AUFTRAG.Type.BAI` (="BAI").
function AUFTRAG:_DetermineAuftragType(Target)

  local group=nil      --Wrapper.Group#GROUP
  local airbase=nil    --Wrapper.Airbase#AIRBASE
  local scenery=nil    --Wrapper.Scenery#SCENERY
  local coordinate=nil --Core.Point#COORDINATE
  local auftrag=nil

  if Target:IsInstanceOf("GROUP") then
    group=Target --Target is already a group.
  elseif Target:IsInstanceOf("UNIT") then
    group=Target:GetGroup()
  elseif Target:IsInstanceOf("AIRBASE") then
    airbase=Target
  elseif Target:IsInstanceOf("SCENERY") then
    scenery=Target
  end

  if group then

    local category=group:GetCategory()
    local attribute=group:GetAttribute()

    if category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER then

      ---
      -- A2A: Intercept
      ---

      auftrag=AUFTRAG.Type.INTERCEPT

    elseif category==Group.Category.GROUND or category==Group.Category.TRAIN then

      ---
      -- GROUND
      ---

      if attribute==GROUP.Attribute.GROUND_SAM then

        -- SEAD/DEAD

        auftrag=AUFTRAG.Type.SEAD

      elseif attribute==GROUP.Attribute.GROUND_AAA then

        auftrag=AUFTRAG.Type.BAI

      elseif attribute==GROUP.Attribute.GROUND_ARTILLERY then

        auftrag=AUFTRAG.Type.BAI

      elseif attribute==GROUP.Attribute.GROUND_INFANTRY then

        auftrag=AUFTRAG.Type.CAS

      elseif attribute==GROUP.Attribute.GROUND_TANK then

        auftrag=AUFTRAG.Type.BAI

      else

        auftrag=AUFTRAG.Type.BAI

      end


    elseif category==Group.Category.SHIP then

      ---
      -- NAVAL
      ---

      auftrag=AUFTRAG.Type.ANTISHIP

    else
      self:T(self.lid.."ERROR: Unknown Group category!")
    end

  elseif airbase then
    auftrag=AUFTRAG.Type.BOMBRUNWAY
  elseif scenery then
    auftrag=AUFTRAG.Type.STRIKE
  elseif coordinate then
    auftrag=AUFTRAG.Type.BOMBING
  end

  return auftrag
end

--- Create a mission to attack a group. Mission type is automatically chosen from the group category.
-- @param #AUFTRAG self
-- @param Wrapper.Group#GROUP EngageGroup Group to be engaged.
-- @return #AUFTRAG self
function AUFTRAG:NewAUTO(EngageGroup)

  local mission=nil --#AUFTRAG

  local Target=EngageGroup

  local auftrag=self:_DetermineAuftragType(EngageGroup)

  if auftrag==AUFTRAG.Type.ANTISHIP then
    mission=AUFTRAG:NewANTISHIP(Target)
  elseif auftrag==AUFTRAG.Type.ARTY then
    mission=AUFTRAG:NewARTY(Target)
  elseif auftrag==AUFTRAG.Type.AWACS then
    mission=AUFTRAG:NewAWACS(Coordinate, Altitude,Speed,Heading,Leg)
  elseif auftrag==AUFTRAG.Type.BAI then
    mission=AUFTRAG:NewBAI(Target,Altitude)
  elseif auftrag==AUFTRAG.Type.BOMBING then
    mission=AUFTRAG:NewBOMBING(Target,Altitude)
  elseif auftrag==AUFTRAG.Type.BOMBRUNWAY then
    mission=AUFTRAG:NewBOMBRUNWAY(Airdrome,Altitude)
  elseif auftrag==AUFTRAG.Type.BOMBCARPET then
    mission=AUFTRAG:NewBOMBCARPET(Target,Altitude,CarpetLength)
  elseif auftrag==AUFTRAG.Type.CAP then
    mission=AUFTRAG:NewCAP(ZoneCAP,Altitude,Speed,Coordinate,Heading,Leg,TargetTypes)
  elseif auftrag==AUFTRAG.Type.CAS then
  mission=AUFTRAG:NewCAS(ZoneCAS,Altitude,Speed,Coordinate,Heading,Leg,TargetTypes)
  elseif auftrag==AUFTRAG.Type.ESCORT then
    mission=AUFTRAG:NewESCORT(EscortGroup,OffsetVector,EngageMaxDistance,TargetTypes)
  elseif auftrag==AUFTRAG.Type.FACA then
    mission=AUFTRAG:NewFACA(Target,Designation,DataLink,Frequency,Modulation)
  
  elseif auftrag==AUFTRAG.Type.FERRY then
    -- Not implemented yet.
  elseif auftrag==AUFTRAG.Type.GCICAP then
    mission=AUFTRAG:NewGCICAP(Coordinate,Altitude,Speed,Heading,Leg)
  elseif auftrag==AUFTRAG.Type.INTERCEPT then
    mission=AUFTRAG:NewINTERCEPT(Target)
  elseif auftrag==AUFTRAG.Type.ORBIT then
    mission=AUFTRAG:NewORBIT(Coordinate,Altitude,Speed,Heading,Leg)
  elseif auftrag==AUFTRAG.Type.RECON then
    mission=AUFTRAG:NewRECON(ZoneSet,Speed,Altitude,Adinfinitum,Randomly,Formation)
  elseif auftrag==AUFTRAG.Type.RESCUEHELO then
    mission=AUFTRAG:NewRESCUEHELO(Carrier)
  elseif auftrag==AUFTRAG.Type.SEAD then
    mission=AUFTRAG:NewSEAD(Target,Altitude)
  elseif auftrag==AUFTRAG.Type.STRIKE then
    mission=AUFTRAG:NewSTRIKE(Target,Altitude)
  elseif auftrag==AUFTRAG.Type.TANKER then
    mission=AUFTRAG:NewTANKER(Coordinate,Altitude,Speed,Heading,Leg,RefuelSystem)
  elseif auftrag==AUFTRAG.Type.TROOPTRANSPORT then
    mission=AUFTRAG:NewTROOPTRANSPORT(TransportGroupSet,DropoffCoordinate,PickupCoordinate)
  elseif auftrag==AUFTRAG.Type.PATROLRACETRACK then
    mission=AUFTRAG:NewPATROL_RACETRACK(Coordinate,Altitude,Speed,Heading,Leg,Formation)
  else

  end

  if mission then
    mission:SetPriority(10, true)
  end

  return mission
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User API Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set mission start and stop time.
-- @param #AUFTRAG self
-- @param #string ClockStart Time the mission is started, e.g. "05:00" for 5 am. If specified as a #number, it will be relative (in seconds) to the current mission time. Default is 5 seconds after mission was added.
-- @param #string ClockStop (Optional) Time the mission is stopped, e.g. "13:00" for 1 pm. If mission could not be started at that time, it will be removed from the queue. If specified as a #number it will be relative (in seconds) to the current mission time.
-- @return #AUFTRAG self
function AUFTRAG:SetTime(ClockStart, ClockStop)

  -- Current mission time.
  local Tnow=timer.getAbsTime()

  -- Set start time. Default in 5 sec.
  local Tstart=Tnow+5
  if ClockStart and type(ClockStart)=="number" then
    Tstart=Tnow+ClockStart
  elseif ClockStart and type(ClockStart)=="string" then
    Tstart=UTILS.ClockToSeconds(ClockStart)
  end

  -- Set stop time. Default nil.
  local Tstop=nil
  if ClockStop and type(ClockStop)=="number" then
    Tstop=Tnow+ClockStop
  elseif ClockStop and type(ClockStop)=="string" then
    Tstop=UTILS.ClockToSeconds(ClockStop)
  end

  self.Tstart=Tstart
  self.Tstop=Tstop

  if Tstop then
    self.duration=self.Tstop-self.Tstart
  end

  return self
end

--- Set time how long the mission is executed. Once this time limit has passed, the mission is cancelled.
-- @param #AUFTRAG self
-- @param #number Duration Duration in seconds.
-- @return #AUFTRAG self
function AUFTRAG:SetDuration(Duration)
  self.durationExe=Duration
  return self
end

--- Set that mission assets are teleported to the mission execution waypoint.
-- @param #AUFTRAG self
-- @param #boolean Switch If `true` or `nil`, teleporting is on. If `false`, teleporting is off.
-- @return #AUFTRAG self
function AUFTRAG:SetTeleport(Switch)
  if Switch==nil then
    Switch=true
  end
  self.teleport=Switch
  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Set whether assigned assets return to their legion once the mission is over. This is only applicable to **army** and **navy** groups, *i.e.* aircraft 
-- will always return.
-- @param #AUFTRAG self
-- @param #boolean Switch If `true`, assets will return. If `false`, assets will not return and stay where it finishes its last mission. If `nil`, let asset decide.
-- @return #AUFTRAG self
function AUFTRAG:SetReturnToLegion(Switch)
  self.legionReturn=Switch
  self:T(self.lid..string.format("Setting ReturnToLetion=%s", tostring(self.legionReturn)))
  return self
end


--- Set mission push time. This is the time the mission is executed. If the push time is not passed, the group will wait at the mission execution waypoint.
-- @param #AUFTRAG self
-- @param #string ClockPush Time the mission is executed, e.g. "05:00" for 5 am. Can also be given as a `#number`, where it is interpreted as relative push time in seconds.
-- @return #AUFTRAG self
function AUFTRAG:SetPushTime(ClockPush)

  if ClockPush then
    if type(ClockPush)=="string" then
      self.Tpush=UTILS.ClockToSeconds(ClockPush)
    elseif type(ClockPush)=="number" then
      self.Tpush=timer.getAbsTime()+ClockPush
    end
  end

  return self
end

--- Set mission priority and (optional) urgency. Urgent missions can cancel other running missions.
-- @param #AUFTRAG self
-- @param #number Prio Priority 1=high, 100=low. Default 50.
-- @param #boolean Urgent If *true*, another running mission might be cancelled if it has a lower priority.
-- @param #number Importance Number 1-10. If missions with lower value are in the queue, these have to be finished first. Default is `nil`.
-- @return #AUFTRAG self
function AUFTRAG:SetPriority(Prio, Urgent, Importance)
  self.prio=Prio or 50
  self.urgent=Urgent
  self.importance=Importance
  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Set how many times the mission is repeated. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, FLEET) or higher level.
-- @param #AUFTRAG self
-- @param #number Nrepeat Number of repeats. Default 0.
-- @return #AUFTRAG self
function AUFTRAG:SetRepeat(Nrepeat)
  self.Nrepeat=Nrepeat or 0
  return self
end


--- **[LEGION, COMMANDER, CHIEF]** Set the repeat delay in seconds after a mission is successful/failed. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, FLEET) or higher level.
-- @param #AUFTRAG self
-- @param #number Nrepeat Repeat delay in seconds. Default 1.
-- @return #AUFTRAG self
function AUFTRAG:SetRepeatDelay(RepeatDelay)
  self.repeatDelay = RepeatDelay
  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Set how many times the mission is repeated if it fails. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, FLEET) or higher level.
-- @param #AUFTRAG self
-- @param #number Nrepeat Number of repeats. Default 0.
-- @return #AUFTRAG self
function AUFTRAG:SetRepeatOnFailure(Nrepeat)
  self.NrepeatFailure=Nrepeat or 0
  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Set how many times the mission is repeated if it was successful. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, FLEET) or higher level.
-- @param #AUFTRAG self
-- @param #number Nrepeat Number of repeats. Default 0.
-- @return #AUFTRAG self
function AUFTRAG:SetRepeatOnSuccess(Nrepeat)
  self.NrepeatSuccess=Nrepeat or 0
  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Set that mission assets get reinforced if their number drops below the minimum number of required assets of the mission (*c.f.* SetRequiredAssets() function).
-- 
-- **Note** that reinforcement groups are only recruited from the legion (airwing, brigade, fleet) the mission was assigned to. If the legion does not have any more of these assets, 
-- no reinforcement can take place, even if the mission is submitted to a COMMANDER or CHIEF. 
-- @param #AUFTRAG self
-- @param #number Nreinforce Number of max asset groups used to reinforce.
-- @return #AUFTRAG self
function AUFTRAG:SetReinforce(Nreinforce)
  self.reinforce=Nreinforce
  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Define how many assets are required to do the job. Only used if the mission is handled by a **LEGION** (AIRWING, BRIGADE, ...) or higher level.
-- @param #AUFTRAG self
-- @param #number NassetsMin Minimum number of asset groups. Default 1.
-- @param #number NassetsMax Maximum Number of asset groups. Default is same as `NassetsMin`.
-- @return #AUFTRAG self
function AUFTRAG:SetRequiredAssets(NassetsMin, NassetsMax)

  self.NassetsMin=NassetsMin or 1

  self.NassetsMax=NassetsMax or self.NassetsMin

  -- Ensure that max is at least equal to min.
  if self.NassetsMax<self.NassetsMin then
    self.NassetsMax=self.NassetsMin
  end

  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Get number of required assets.
-- @param #AUFTRAG self
-- @return #number Min. number of required assets.
-- @return #number Max. number of required assets.
function AUFTRAG:GetRequiredAssets()
  
  local Nmin=self.NassetsMin
  local Nmax=self.NassetsMax
    
  if self.type==AUFTRAG.Type.RELOCATECOHORT then

    -- Relocation gets all the assets.
    local cohort=self.DCStask.params.cohort --Ops.Cohort#COHORT
    Nmin=#cohort.assets
    Nmax=Nmin
    
  else

    -- Check if this is an reinforcement.
    if self:IsExecuting() and self.reinforce and self.reinforce>0 then
      local N=self:CountOpsGroups()
      if N<Nmin then
        Nmin=math.min(Nmin-N, self.reinforce)
        Nmax=Nmin
        self:T(self.lid..string.format("FF Executing Nmin=%d, N=%d, Nreinfoce=%d ==> Nmin=%d", self.NassetsMin, N, self.reinforce, Nmin))
      end
    end
  
  end

  return Nmin, Nmax
end

--- **[LEGION, COMMANDER, CHIEF]** Set that only alive (spawned) assets are considered.
-- @param #AUFTRAG self
-- @param #boolean Switch If true or nil, only active assets. If false
-- @return #AUFTRAG self
function AUFTRAG:SetAssetsStayAlive(Switch)

  if Switch==nil then
    Switch=true
  end

  self.assetStayAlive=Switch

  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Define how many assets are required that escort the mission assets. 
-- Only used if the mission is handled by a **LEGION** (AIRWING, BRIGADE, FLEET) or higher level.
-- @param #AUFTRAG self
-- @param #number NescortMin Minimum number of asset groups. Default 1.
-- @param #number NescortMax Maximum Number of asset groups. Default is same as `NassetsMin`.
-- @param #string MissionType Mission type assets will be optimized for and payload selected, *e.g.* `AUFTRAG.Type.SEAD`. Default nil.
-- @param #table TargetTypes Target Types that will be engaged by the escort group(s). Default `{"Air"}` for aircraft and `{"Ground Units"}` for helos. Set, *e.g.*, `{"Air Defence"}` for SEAD.
-- @param #number EngageRange Max range in nautical miles that the escort group(s) will engage enemies. Default 32 NM (60 km).
-- @return #AUFTRAG self
function AUFTRAG:SetRequiredEscorts(NescortMin, NescortMax, MissionType, TargetTypes, EngageRange)

  -- Set number of escort assets.
  self.NescortMin=NescortMin or 1
  self.NescortMax=NescortMax or self.NescortMin

  -- Ensure that max is at least equal to min.
  if self.NescortMax<self.NescortMin then
    self.NescortMax=self.NescortMin
  end
  
  -- Set parameters.
  self.escortMissionType=MissionType
  self.escortTargetTypes=TargetTypes
  self.escortEngageRange=EngageRange or 32

  -- Debug info.
  self:T(self.lid..string.format("NescortMin=%s, NescortMax=%s", tostring(self.NescortMin), tostring(self.NescortMax)))

  return self
end

--- Set mission name.
-- @param #AUFTRAG self
-- @param #string Name Name of the mission. Default is "Auftrag Nr. X", where X is a running number, which is automatically increased.
-- @return #AUFTRAG self
function AUFTRAG:SetName(Name)
  self.name=Name or string.format("Auftrag Nr. %d", self.auftragsnummer)
  return self
end

--- Enable markers, which dispay the mission status on the F10 map.
-- @param #AUFTRAG self
-- @param #number Coalition The coaliton side to which the markers are dispayed. Default is to all.
-- @return #AUFTRAG self
function AUFTRAG:SetEnableMarkers(Coalition)
  self.markerOn=true
  self.markerCoaliton=Coalition or -1
  return self
end

--- Set verbosity level.
-- @param #AUFTRAG self
-- @param #number VerbosityLevel Level of output (higher=more). Default 0.
-- @return #AUFTRAG self
function AUFTRAG:SetVerbosity(VerbosityLevel)
  self.verbose=VerbosityLevel or 0
  return self
end

--- Set weapon type used for the engagement.
-- @param #AUFTRAG self
-- @param #number WeaponType Weapon type. Default is `ENUMS.WeaponFlag.Auto`.
-- @return #AUFTRAG self
function AUFTRAG:SetWeaponType(WeaponType)

  self.engageWeaponType=WeaponType or ENUMS.WeaponFlag.Auto

  -- Update the DCS task parameter.
  self.DCStask=self:GetDCSMissionTask()

  return self
end

--- Set number of weapons to expend.
-- @param #AUFTRAG self
-- @param #number WeaponExpend How much of the weapon load is expended during the attack, e.g. `AI.Task.WeaponExpend.ALL`. Default "Auto".
-- @return #AUFTRAG self
function AUFTRAG:SetWeaponExpend(WeaponExpend)

  self.engageWeaponExpend=WeaponExpend or "Auto"

  -- Update the DCS task parameter.
  self.DCStask=self:GetDCSMissionTask()

  return self
end

--- Set whether target will be attack as group.
-- @param #AUFTRAG self
-- @param #boolean Switch If true or nil, engage as group. If false, not.
-- @return #AUFTRAG self
function AUFTRAG:SetEngageAsGroup(Switch)

  if Switch==nil then
    Switch=true
  end

  self.engageAsGroup=Switch

  -- Update the DCS task parameter.
  self.DCStask=self:GetDCSMissionTask()

  return self
end

--- Set engage altitude. This is the altitude passed to the DCS task. In the ME it is the tickbox ALTITUDE ABOVE.
-- @param #AUFTRAG self
-- @param #string Altitude Altitude in feet. Default 6000 ft.
-- @return #AUFTRAG self
function AUFTRAG:SetEngageAltitude(Altitude)

  self.engageAltitude=UTILS.FeetToMeters(Altitude or 6000)

   -- Update the DCS task parameter.
  self.DCStask=self:GetDCSMissionTask()

  return self
end

--- Enable to automatically engage detected targets.
-- @param #AUFTRAG self
-- @param #number RangeMax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM.
-- @param #table TargetTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default "All".
-- @param Core.Set#SET_ZONE EngageZoneSet Set of zones in which targets are engaged. Default is anywhere.
-- @param Core.Set#SET_ZONE NoEngageZoneSet Set of zones in which targets are *not* engaged. Default is nowhere.
-- @return #AUFTRAG self
function AUFTRAG:SetEngageDetected(RangeMax, TargetTypes, EngageZoneSet, NoEngageZoneSet)

  -- Ensure table.
  if TargetTypes then
    if type(TargetTypes)~="table" then
      TargetTypes={TargetTypes}
    end
  else
    TargetTypes={"All"}
  end

  -- Ensure SET_ZONE if ZONE is provided.
  if EngageZoneSet and EngageZoneSet:IsInstanceOf("ZONE_BASE") then
    local zoneset=SET_ZONE:New():AddZone(EngageZoneSet)
    EngageZoneSet=zoneset
  end
  if NoEngageZoneSet and NoEngageZoneSet:IsInstanceOf("ZONE_BASE") then
    local zoneset=SET_ZONE:New():AddZone(NoEngageZoneSet)
    NoEngageZoneSet=zoneset
  end

  -- Set parameters.
  self.engagedetectedOn=true
  self.engagedetectedRmax=UTILS.NMToMeters(RangeMax or 25)
  self.engagedetectedTypes=TargetTypes
  self.engagedetectedEngageZones=EngageZoneSet
  self.engagedetectedNoEngageZones=NoEngageZoneSet

  return self
end

--- Set mission altitude. This is the altitude of the waypoint create where the DCS task is executed.
-- @param #AUFTRAG self
-- @param #string Altitude Altitude in feet.
-- @return #AUFTRAG self
function AUFTRAG:SetMissionAltitude(Altitude)
  self.missionAltitude=UTILS.FeetToMeters(Altitude)
  return self
end

--- Set mission speed. That is the speed the group uses to get to the mission waypoint.
-- @param #AUFTRAG self
-- @param #string Speed Mission speed in knots.
-- @return #AUFTRAG self
function AUFTRAG:SetMissionSpeed(Speed)
  self.missionSpeed=Speed and UTILS.KnotsToKmph(Speed) or nil
  return self
end

--- Set max mission range. Only applies if the AUFTRAG is handled by an AIRWING or CHIEF. This is the max allowed distance from the airbase to the target.
-- @param #AUFTRAG self
-- @param #number Range Max range in NM. Default 100 NM.
-- @return #AUFTRAG self
function AUFTRAG:SetMissionRange(Range)
  self.engageRange=UTILS.NMToMeters(Range or 100)
  return self
end

--- Attach OPS transport to the mission. Mission assets will be transported before the mission is started at the OPSGROUP level.
-- @param #AUFTRAG self
-- @param Ops.OpsTransport#OPSTRANSPORT OpsTransport The OPS transport assignment attached to the mission.
-- @return #AUFTRAG self
function AUFTRAG:SetOpsTransport(OpsTransport)
  self.opstransport=OpsTransport
  return self
end

--- Get the attach OPS transport of the mission.
-- @param #AUFTRAG self
-- @return Ops.OpsTransport#OPSTRANSPORT The OPS transport assignment attached to the mission.
function AUFTRAG:GetOpsTransport()
  return self.opstransport
end

--- **[LEGION, COMMANDER, CHIEF]** Attach OPS transport to the mission. Mission assets will be transported before the mission is started at the OPSGROUP level.
-- @param #AUFTRAG self
-- @param Core.Zone#ZONE DeployZone Zone where assets are deployed.
-- @param #number NcarriersMin Number of carriers *at least* required. Default 1.
-- @param #number NcarriersMax Number of carriers *at most* used for transportation. Default is same as `NcarriersMin`.
-- @param Core.Zone#ZONE DisembarkZone Zone where assets are disembarked to.
-- @param #table Categories Group categories.
-- @param #table Attributes Generalizes group attributes.
-- @param #table Properties DCS attributes.
-- @return #AUFTRAG self
function AUFTRAG:SetRequiredTransport(DeployZone, NcarriersMin, NcarriersMax, DisembarkZone, Categories, Attributes, Properties)

  -- OPS transport from pickup to deploy zone.
  self.transportDeployZone=DeployZone
  self.transportDisembarkZone=DisembarkZone

  -- Set required carriers.
  self:SetRequiredCarriers(NcarriersMin, NcarriersMax, Categories, Attributes, Properties)

  return self
end

--- Add carriers for a transport of mission assets.
-- @param #AUFTRAG self
-- @param Core.Set#SET_OPSGROUP Carriers Set of carriers. Can also be a single group.
-- @return #AUFTRAG self
function AUFTRAG:AddTransportCarriers(Carriers)

  if self.opstransport then
    if Carriers:IsInstanceOf("SET_OPSGROUP") then

      for _,_carrier in pairs(Carriers.Set) do
        local carrier=_carrier --Ops.OpsGroup#OPSGROUP
        carrier:AddOpsTransport(self.opstransport)
      end

    elseif Carriers:IsInstanceOf("OPSGROUP") then
      Carriers:AddOpsTransport(self.opstransport)
    end

  end

  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Set required attribute(s) the assets must have.
-- @param #AUFTRAG self
-- @param #table Attributes Generalized attribute(s).
-- @return #AUFTRAG self
function AUFTRAG:SetRequiredAttribute(Attributes)
  self.attributes=UTILS.EnsureTable(Attributes, true)
  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Set required property or properties the assets must have.
-- These are [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes).
-- @param #AUFTRAG self
-- @param #table Properties Property or table of properties.
-- @return #AUFTRAG self
function AUFTRAG:SetRequiredProperty(Properties)
  self.properties=UTILS.EnsureTable(Properties, true)
  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Set number of required carrier groups if an OPSTRANSPORT assignment is required.
-- @param #AUFTRAG self
-- @param #number NcarriersMin Number of carriers *at least* required. Default 1.
-- @param #number NcarriersMax Number of carriers *at most* used for transportation. Default is same as `NcarriersMin`.
-- @param #table Categories Group categories.
-- @param #table Attributes Group attributes. See `GROUP.Attribute.`
-- @param #table Properties DCS attributes.
-- @return #AUFTRAG self
function AUFTRAG:SetRequiredCarriers(NcarriersMin, NcarriersMax, Categories, Attributes, Properties)

  self.NcarriersMin=NcarriersMin or 1

  self.NcarriersMax=NcarriersMax or self.NcarriersMin

  -- Ensure that max is at least equal to min.
  if self.NcarriersMax<self.NcarriersMin then
    self.NcarriersMax=self.NcarriersMin
  end

  self.carrierCategories = UTILS.EnsureTable(Categories, true)
  self.carrierAttributes = UTILS.EnsureTable(Attributes, true)
  self.carrierProperties = UTILS.EnsureTable(Properties, true)

  return self
end

--- Set that (jet) aircraft are generally **not** allowed to use afterburner. Default is use of afterburner is allowed.
-- @param #AUFTRAG self
-- @return #AUFTRAG self
function AUFTRAG:SetProhibitAfterburner()
  self.prohibitAB = true
  return self 
end

--- Set that (jet) aircraft are generally allowed to use afterburner. Default is use of afterburner is allowed.
-- @param #AUFTRAG self
-- @return #AUFTRAG self
function AUFTRAG:SetAllowAfterburner()
  self.prohibitAB = false
  return self 
end

--- Set that (jet) aircraft are **not** allowed to use afterburner in mission execution phase. Default is use of afterburner is allowed.
-- @param #AUFTRAG self
-- @return #AUFTRAG self
function AUFTRAG:SetProhibitAfterburnerExecutePhase()
  self.prohibitABExecute = true
  return self 
end

--- Set that (jet) aircraft are allowed to use afterburner in mission execution phase. Default is use of afterburner is allowed.
-- @param #AUFTRAG self
-- @return #AUFTRAG self
function AUFTRAG:SetAllowAfterburnerExecutePhase()
  self.prohibitABExecute = false
  return self 
end

-- prohibitABExecute

--- **[LEGION, COMMANDER, CHIEF]** Assign a legion cohort to the mission. Only these cohorts will be considered for the job.
-- @param #AUFTRAG self
-- @param Ops.Cohort#COHORT Cohort The cohort.
-- @return #AUFTRAG self
function AUFTRAG:AssignCohort(Cohort)

  self.specialCohorts=self.specialCohorts or {}

  self:T3(self.lid..string.format("Assigning cohort %s", tostring(Cohort.name)))

  -- Add cohort to table.
  table.insert(self.specialCohorts, Cohort)

  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Assign a legion to the mission. Only cohorts of this legion will be considered for the job. You can assign multiple legions.
-- @param #AUFTRAG self
-- @param Ops.Legion#LEGION Legion The legion.
-- @return #AUFTRAG self
function AUFTRAG:AssignLegion(Legion)

  self.specialLegions=self.specialLegions or {}

  self:T3(self.lid..string.format("Assigning Legion %s", tostring(Legion.alias)))

  -- Add Legion to table.
  table.insert(self.specialLegions, Legion)

  return self
end


--- **[LEGION, COMMANDER, CHIEF]** Assign airwing squadron(s) to the mission. Only these squads will be considered for the job.
-- @param #AUFTRAG self
-- @param #table Squadrons A table of SQUADRON(s). **Has to be a table {}** even if a single squad is given.
-- @return #AUFTRAG self
function AUFTRAG:AssignSquadrons(Squadrons)

  for _,_squad in pairs(Squadrons) do
    local squadron=_squad --Ops.Squadron#SQUADRON
    self:T(self.lid..string.format("Assigning squadron %s", tostring(squadron.name)))
    self:AssignCohort(squadron)
  end

  return self
end


--- **[LEGION, COMMANDER, CHIEF]** Assign a transport Legion.
-- @param #AUFTRAG self
-- @param Ops.Legion#LEGION Legion The legion.
function AUFTRAG:AssignTransportLegion(Legion)

  self.transportLegions=self.transportLegions or {}

  table.insert(self.transportLegions, Legion)

  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Assign a transport cohort.
-- @param #AUFTRAG self
-- @param Ops.Cohort#Cohort Cohort The cohort.
function AUFTRAG:AssignTransportCohort(Cohort)

  self.transportCohorts=self.transportCohorts or {}

  table.insert(self.transportCohorts, Cohort)

  return self
end


--- **[LEGION, COMMANDER, CHIEF]** Add an escort Legion.
-- @param #AUFTRAG self
-- @param Ops.Legion#LEGION Legion The legion.
function AUFTRAG:AssignEscortLegion(Legion)

  self.escortLegions=self.escortLegions or {}

  table.insert(self.escortLegions, Legion)

  return self
end

--- **[LEGION, COMMANDER, CHIEF]** Assign an escort cohort.
-- @param #AUFTRAG self
-- @param Ops.Cohort#Cohort Cohort The cohort.
function AUFTRAG:AssignEscortCohort(Cohort)

  self.escortCohorts=self.escortCohorts or {}

  table.insert(self.escortCohorts, Cohort)

  return self
end



--- Set Rules of Engagement (ROE) for this mission.
-- @param #AUFTRAG self
-- @param #number roe Mission ROE, e.g. `ENUMS.ROE.ReturnFire` (whiche equals 3)
-- @return #AUFTRAG self
function AUFTRAG:SetROE(roe)

  self.optionROE=roe

  return self
end


--- Set Reaction on Threat (ROT) for this mission.
-- @param #AUFTRAG self
-- @param #number rot Mission ROT, e.g. `ENUMS.ROT.NoReaction` (whiche equals 0)
-- @return #AUFTRAG self
function AUFTRAG:SetROT(rot)

  self.optionROT=rot

  return self
end

--- Set alarm state for this mission.
-- @param #AUFTRAG self
-- @param #number Alarmstate Alarm state 0=Auto, 1=Green, 2=Red.
-- @return #AUFTRAG self
function AUFTRAG:SetAlarmstate(Alarmstate)

  self.optionAlarm=Alarmstate

  return self
end

--- Set EPLRS datalink setting for this mission.
-- @param #AUFTRAG self
-- @param #boolean OnOffSwitch If `true` or `nil`, EPLRS is on. If `false`, EPLRS is off.
-- @return #AUFTRAG self
function AUFTRAG:SetEPLRS(OnOffSwitch)

  if OnOffSwitch==nil then
    self.optionEPLRS=true
  else
    self.optionEPLRS=OnOffSwitch
  end

  return self
end

--- Set emission setting for this mission.
-- @param #AUFTRAG self
-- @param #boolean OnOffSwitch If `true` or `nil`, emission is on. If `false`, emission is off.
-- @return #AUFTRAG self
function AUFTRAG:SetEmission(OnOffSwitch)

  if OnOffSwitch==nil then
    self.optionEmission=true
  else
    self.optionEmission=OnOffSwitch
  end

  return self
end

--- Set invisibility setting for this mission.
-- @param #AUFTRAG self
-- @param #boolean OnOffSwitch If `true` or `nil`, invisible is on. If `false`, invisible is off.
-- @return #AUFTRAG self
function AUFTRAG:SetInvisible(OnOffSwitch)

  if OnOffSwitch==nil then
    self.optionInvisible=true
  else
    self.optionInvisible=OnOffSwitch
  end

  return self
end

--- Set immortality setting for this mission.
-- @param #AUFTRAG self
-- @param #boolean OnOffSwitch If `true` or `nil`, immortal is on. If `false`, immortal is off.
-- @return #AUFTRAG self
function AUFTRAG:SetImmortal(OnOffSwitch)

  if OnOffSwitch==nil then
    self.optionImmortal=true
  else
    self.optionImmortal=OnOffSwitch
  end

  return self
end

--- Set formation for this mission.
-- @param #AUFTRAG self
-- @param #number Formation Formation.
-- @return #AUFTRAG self
function AUFTRAG:SetFormation(Formation)

  self.optionFormation=Formation

  return self
end

--- Set radio frequency and modulation for this mission.
-- @param #AUFTRAG self
-- @param #number Frequency Frequency in MHz.
-- @param #number Modulation Radio modulation. Default 0=AM.
-- @return #AUFTRAG self
function AUFTRAG:SetRadio(Frequency, Modulation)

  self.radio={}
  self.radio.Freq=Frequency
  self.radio.Modu=Modulation

  return self
end

--- Set TACAN beacon channel and Morse code for this mission.
-- @param #AUFTRAG self
-- @param #number Channel TACAN channel.
-- @param #string Morse Morse code. Default "XXX".
-- @param #string UnitName Name of the unit in the group for which acts as TACAN beacon. Default is the first unit in the group.
-- @param #string Band Tacan channel mode ("X" or "Y"). Default is "X" for ground/naval and "Y" for aircraft.
-- @return #AUFTRAG self
function AUFTRAG:SetTACAN(Channel, Morse, UnitName, Band)

  self.tacan={}
  self.tacan.Channel=Channel
  self.tacan.Morse=Morse or "XXX"
  self.tacan.UnitName=UnitName
  self.tacan.Band=Band

  return self
end

--- Set ICLS beacon channel and Morse code for this mission.
-- @param #AUFTRAG self
-- @param #number Channel ICLS channel.
-- @param #string Morse Morse code. Default "XXX".
-- @param #string UnitName Name of the unit in the group for which acts as ICLS beacon. Default is the first unit in the group.
-- @return #AUFTRAG self
function AUFTRAG:SetICLS(Channel, Morse, UnitName)

  self.icls={}
  self.icls.Channel=Channel
  self.icls.Morse=Morse or "XXX"
  self.icls.UnitName=UnitName

  return self
end

--- Set time interval between mission done and success/failure evaluation.
-- @param #AUFTRAG self
-- @param #number Teval Time in seconds before the mission result is evaluated. Default depends on mission type.
-- @return #AUFTRAG self
function AUFTRAG:SetEvaluationTime(Teval)

  self.dTevaluate=Teval or 60

  return self
end

--- Get mission type.
-- @param #AUFTRAG self
-- @return #string Mission type, e.g. "BAI".
function AUFTRAG:GetType()
  return self.type
end

--- Get mission name.
-- @param #AUFTRAG self
-- @return #string Mission name, e.g. "Auftrag Nr.1".
function AUFTRAG:GetName()
  return self.name
end

--- Get number of required assets.
-- @param #AUFTRAG self
-- @return #number Numer of required assets.
function AUFTRAG:GetNumberOfRequiredAssets()
  return self.Nassets or self.NassetsMin
end

--- Get mission priority.
-- @param #AUFTRAG self
-- @return #number Priority. Smaller is higher.
function AUFTRAG:GetPriority()
  return self.prio
end

--- Get casualties, *i.e.* number of own units that died during this mission.
-- @param #AUFTRAG self
-- @return #number Number of dead units.
function AUFTRAG:GetCasualties()
  return self.Ncasualties or 0
end

--- Get kills, i.e. number of units that were destroyed by assets of this mission.
-- @param #AUFTRAG self
-- @return #number Number of units destroyed.
function AUFTRAG:GetKills()
  return self.Nkills or 0
end


--- Check if mission is "urgent".
-- @param #AUFTRAG self
-- @return #boolean If `true`, mission is "urgent".
function AUFTRAG:IsUrgent()
  return self.urgent
end

--- Get mission importance.
-- @param #AUFTRAG self
-- @return #number Importance. Smaller is higher.
function AUFTRAG:GetImportance()
  return self.importance
end

--- Add start condition.
-- @param #AUFTRAG self
-- @param #function ConditionFunction Function that needs to be true before the mission can be started. Must return a #boolean.
-- @param ... Condition function arguments if any.
-- @return #AUFTRAG self
function AUFTRAG:AddConditionStart(ConditionFunction, ...)

  local condition={} --#AUFTRAG.Condition

  condition.func=ConditionFunction
  condition.arg={}
  if arg then
    condition.arg=arg
  end

  table.insert(self.conditionStart, condition)

  return self
end

--- Add success condition.
-- @param #AUFTRAG self
-- @param #function ConditionFunction If this function returns `true`, the mission is cancelled.
-- @param ... Condition function arguments if any.
-- @return #AUFTRAG self
function AUFTRAG:AddConditionSuccess(ConditionFunction, ...)

  local condition={} --#AUFTRAG.Condition

  condition.func=ConditionFunction
  condition.arg={}
  if arg then
    condition.arg=arg
  end

  table.insert(self.conditionSuccess, condition)
  
  self.conditionSuccessSet = true
  
  return self
end

--- Add failure condition.
-- @param #AUFTRAG self
-- @param #function ConditionFunction If this function returns `true`, the mission is cancelled.
-- @param ... Condition function arguments if any.
-- @return #AUFTRAG self
function AUFTRAG:AddConditionFailure(ConditionFunction, ...)

  local condition={} --#AUFTRAG.Condition

  condition.func=ConditionFunction
  condition.arg={}
  if arg then
    condition.arg=arg
  end

  table.insert(self.conditionFailure, condition)
  
  self.conditionFailureSet = true
  
  return self
end

--- Add push condition.
-- @param #AUFTRAG self
-- @param #function ConditionFunction If this function returns `true`, the mission is executed.
-- @param ... Condition function arguments if any.
-- @return #AUFTRAG self
function AUFTRAG:AddConditionPush(ConditionFunction, ...)

  local condition={} --#AUFTRAG.Condition

  condition.func=ConditionFunction
  condition.arg={}
  if arg then
    condition.arg=arg
  end

  table.insert(self.conditionPush, condition)

  return self
end

--- Add a required payload for this mission. Only these payloads will be used for this mission. If they are not available, the mission cannot start. Only available for use with an AIRWING.
-- @param #AUFTRAG self
-- @param Ops.Airwing#AIRWING.Payload Payload Required payload.
-- @return #AUFTRAG self
function AUFTRAG:AddRequiredPayload(Payload)

  self.payloads=self.payloads or {}

  table.insert(self.payloads, Payload)

  return self
end


--- Add a Ops group to the mission.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPSGROUP object.
-- @return #AUFTRAG self
function AUFTRAG:AddOpsGroup(OpsGroup)
  self:T(self.lid..string.format("Adding Ops group %s", OpsGroup.groupname))

  local groupdata={} --#AUFTRAG.GroupData
  groupdata.opsgroup=OpsGroup
  groupdata.status=AUFTRAG.GroupStatus.SCHEDULED
  groupdata.waypointcoordinate=nil
  groupdata.waypointindex=nil
  groupdata.waypointtask=nil

  self.groupdata[OpsGroup.groupname]=groupdata

  -- Add ops transport to new group.
  if self.opstransport then
    for _,_tzc in pairs(self.opstransport.tzCombos) do
      local tzc=_tzc --Ops.OpsTransport#OPSTRANSPORT.TransportZoneCombo

      if tzc.assetsCargo and tzc.assetsCargo[OpsGroup:GetName()] then
        self.opstransport:AddCargoGroups(OpsGroup, tzc)
      end

    end
  end

  return self
end

--- Remove an Ops group from the mission.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPSGROUP object.
-- @return #AUFTRAG self
function AUFTRAG:DelOpsGroup(OpsGroup)
  self:T(self.lid..string.format("Removing OPS group %s", OpsGroup and OpsGroup.groupname or "nil (ERROR)!"))

  if OpsGroup then

    -- Remove mission form queue.
    OpsGroup:RemoveMission(self)

    self.groupdata[OpsGroup.groupname]=nil

  end

  return self
end

--- Check if mission is PLANNED.
-- @param #AUFTRAG self
-- @return #boolean If true, mission is in the planning state.
function AUFTRAG:IsPlanned()
  return self.status==AUFTRAG.Status.PLANNED
end

--- Check if mission is QUEUED at a LEGION mission queue.
-- @param #AUFTRAG self
-- @param Ops.Legion#LEGION Legion (Optional) Check if mission is queued at this legion.
-- @return #boolean If true, mission is queued.
function AUFTRAG:IsQueued(Legion)
  local is=self.status==AUFTRAG.Status.QUEUED
  if Legion then
    is=self:GetLegionStatus(Legion)==AUFTRAG.Status.QUEUED
  end
  return is
end

--- Check if mission is REQUESTED. The mission request out to the WAREHOUSE.
-- @param #AUFTRAG self
-- @param Ops.Legion#LEGION Legion (Optional) Check if mission is requested at this legion.
-- @return #boolean If true, mission is requested.
function AUFTRAG:IsRequested(Legion)
  local is=self.status==AUFTRAG.Status.REQUESTED
  if Legion then
    is=self:GetLegionStatus(Legion)==AUFTRAG.Status.REQUESTED
  end
  return is
end

--- Check if mission is SCHEDULED. The first OPSGROUP has been assigned.
-- @param #AUFTRAG self
-- @return #boolean If true, mission is queued.
function AUFTRAG:IsScheduled()
  return self.status==AUFTRAG.Status.SCHEDULED
end

--- Check if mission is STARTED. The first OPSGROUP is on its way to the mission execution waypoint.
-- @param #AUFTRAG self
-- @return #boolean If true, mission is started.
function AUFTRAG:IsStarted()
  return self.status==AUFTRAG.Status.STARTED
end

--- Check if mission is EXECUTING. The first OPSGROUP has reached the mission execution waypoint and is not executing the mission task.
-- @param #AUFTRAG self
-- @param #boolean AllGroups (Optional) Check that all groups are currently executing the mission.
-- @return #boolean If true, mission is currently executing.
function AUFTRAG:IsExecuting(AllGroups)

  local isExecuting=self.status==AUFTRAG.Status.EXECUTING

  if AllGroups and isExecuting then
  
    -- Number of groups executing.
    local n=self:CountOpsGroupsInStatus(AUFTRAG.GroupStatus.EXECUTING)

    local N    
    if self.Nassigned then
      N=self.Nassigned-self.Ndead
    else
      N=self:CountOpsGroups()   
    end
    
    if n==N then
      return true
    else
      return false
    end
         
  end

  return isExecuting
end

--- Check if mission was cancelled.
-- @param #AUFTRAG self
-- @return #boolean If true, mission was cancelled.
function AUFTRAG:IsCancelled()
  return self.status==AUFTRAG.Status.CANCELLED
end

--- Check if mission is done.
-- @param #AUFTRAG self
-- @return #boolean If true, mission is done.
function AUFTRAG:IsDone()
  return self.status==AUFTRAG.Status.DONE
end

--- Check if mission was a success.
-- @param #AUFTRAG self
-- @return #boolean If true, mission was successful.
function AUFTRAG:IsSuccess()
  return self.status==AUFTRAG.Status.SUCCESS
end

--- Check if mission is over. This could be state DONE, CANCELLED, SUCCESS, FAILED.
-- @param #AUFTRAG self
-- @return #boolean If true, mission is over.
function AUFTRAG:IsOver()
  local over = self.status==AUFTRAG.Status.DONE or self.status==AUFTRAG.Status.CANCELLED or self.status==AUFTRAG.Status.SUCCESS or self.status==AUFTRAG.Status.FAILED
  return over
end

--- Check if mission is repeatable.
-- @param #AUFTRAG self
-- @return #boolean If true, mission is repeatable.
function AUFTRAG:IsRepeatable()
  local repeatmeS=self.repeatedSuccess<self.NrepeatSuccess or self.repeated<self.Nrepeat
  local repeatmeF=self.repeatedFailure<self.NrepeatFailure or self.repeated<self.Nrepeat
  if repeatmeS==true or repeatmeF==true then return true else return false end
  return false
end

--- Check if mission is NOT repeatable.
-- @param #AUFTRAG self
-- @return #boolean If true, mission is NOT repeatable.
function AUFTRAG:IsNotRepeatable()
  return not self:IsRepeatable()
end

--- Check if mission is NOT over.
-- @param #AUFTRAG self
-- @return #boolean If true, mission is NOT over yet.
function AUFTRAG:IsNotOver()
  return not self:IsOver()
end

--- Check if mission is for aircarft (airplanes and/or helicopters).
-- @param #AUFTRAG self
-- @return #boolean If `true`, mission is for aircraft.
function AUFTRAG:IsAircraft()
  for _,category in pairs(self.categories) do
    if category==AUFTRAG.Category.AIRCRAFT or category==AUFTRAG.Category.AIRPLANE or category==AUFTRAG.Category.HELICOPTER then
      return true
    end
  end
  return false
end

--- Check if mission is for airplanes.
-- @param #AUFTRAG self
-- @return #boolean If `true`, mission is for airplanes.
function AUFTRAG:IsAirplane()
  for _,category in pairs(self.categories) do
    if category==AUFTRAG.Category.AIRPLANE then
      return true
    end
  end
  return false
end

--- Check if mission is for helicopters.
-- @param #AUFTRAG self
-- @return #boolean If `true`, mission is for helicopters.
function AUFTRAG:IsHelicopters()
  for _,category in pairs(self.categories) do
    if category==AUFTRAG.Category.HELICOPTER then
      return true
    end
  end
  return false
end

--- Check if mission is for ground units.
-- @param #AUFTRAG self
-- @return #boolean If `true`, mission is for ground units.
function AUFTRAG:IsGround()
  for _,category in pairs(self.categories) do
    if category==AUFTRAG.Category.GROUND then
      return true
    end
  end
  return false
end

--- Check if mission is for naval units.
-- @param #AUFTRAG self
-- @return #boolean If `true`, mission is for naval units.
function AUFTRAG:IsNaval()
  for _,category in pairs(self.categories) do
    if category==AUFTRAG.Category.NAVAL then
      return true
    end
  end
  return false
end


--- Check if mission is ready to be started.
-- * Mission start time passed.
-- * Mission stop time did not pass already.
-- * All start conditions are true.
-- @param #AUFTRAG self
-- @return #boolean If true, mission can be started.
function AUFTRAG:IsReadyToGo()

  local Tnow=timer.getAbsTime()

  -- Start time did not pass yet.
  if self.Tstart and Tnow<self.Tstart then
    return false
  end

  -- Stop time already passed.
  if self.Tstop and Tnow>self.Tstop then
    return false
  end

  -- All start conditions true?
  local startme=self:EvalConditionsAll(self.conditionStart)

  if not startme then
    return false
  end


  -- We're good to go!
  return true
end

--- Check if mission is ready to be cancelled.
-- * Mission stop already passed.
-- * Any stop condition is true.
-- @param #AUFTRAG self
-- @return #boolean If true, mission should be cancelled.
function AUFTRAG:IsReadyToCancel()

  local Tnow=timer.getAbsTime()

  -- Stop time already passed.
  if self.Tstop and Tnow>=self.Tstop then
    return true
  end

  -- Evaluate failure condition. One is enough.
  local failure=self:EvalConditionsAny(self.conditionFailure)

  if failure then
    self.failurecondition=true
    return true
  end

  -- Evaluate success consitions. One is enough.
  local success=self:EvalConditionsAny(self.conditionSuccess)

  if success then
    self.successcondition=true
    return true
  end

  -- No criterion matched.
  return false
end

--- Check if mission is ready to be pushed.
-- * Mission push time already passed.
-- * **All** push conditions are true.
-- @param #AUFTRAG self
-- @return #boolean If true, mission groups can push.
function AUFTRAG:IsReadyToPush()

  local Tnow=timer.getAbsTime()

  -- Push time passed?
  if self.Tpush and Tnow<=self.Tpush then
    return false
  end

  -- Evaluate push condition(s) if any. All need to be true.
  local push=self:EvalConditionsAll(self.conditionPush)

  return push
end

--- Check if all given condition are true.
-- @param #AUFTRAG self
-- @param #table Conditions Table of conditions.
-- @return #boolean If true, all conditions were true. Returns false if at least one condition returned false.
function AUFTRAG:EvalConditionsAll(Conditions)

  -- Any stop condition must be true.
  for _,_condition in pairs(Conditions or {}) do
    local condition=_condition --#AUFTRAG.Condition

    -- Call function.
    local istrue=condition.func(unpack(condition.arg))

    -- Any false will return false.
    if not istrue then
      return false
    end

  end

  -- All conditions were true.
  return true
end


--- Check if any of the given conditions is true.
-- @param #AUFTRAG self
-- @param #table Conditions Table of conditions.
-- @return #boolean If true, at least one condition is true.
function AUFTRAG:EvalConditionsAny(Conditions)

  -- Any stop condition must be true.
  for _,_condition in pairs(Conditions or {}) do
    local condition=_condition --#AUFTRAG.Condition

    -- Call function.
    local istrue=condition.func(unpack(condition.arg))

    -- Any true will return true.
    if istrue then
      return true
    end

  end

  -- No condition was true.
  return false
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Mission Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "Status" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterStatus(From, Event, To)

  -- Current abs. mission time.
  local Tnow=timer.getAbsTime()

  -- ESCORT: Check if only the group NAME of an escort had been specified.
  if self.escortGroupName then
    -- Try to find the group.
    local group=GROUP:FindByName(self.escortGroupName)
    if group and group:IsAlive() then

      -- Debug info.
      self:T(self.lid..string.format("ESCORT group %s is now alive. Updating DCS task and adding group to TARGET", tostring(self.escortGroupName)))

      -- Add TARGET object.
      self.engageTarget:AddObject(group)

      -- Update DCS task with the known group ID.
      self.DCStask=self:GetDCSMissionTask()

      -- Set value to nil so we do not do this again in the next cycle.
      self.escortGroupName=nil
    end
  end

  -- Number of alive mission targets.
  local Ntargets=self:CountMissionTargets()
  local Ntargets0=self:GetTargetInitialNumber()

  -- Number of alive groups attached to this mission.
  local Ngroups=self:CountOpsGroups()
  
  local Nassigned=self.Nassigned and self.Nassigned-self.Ndead or 0
  
  -- check conditions if set
  local conditionDone=false
  if self.conditionFailureSet then
    conditionDone = self:EvalConditionsAny(self.conditionFailure)
  end  
  if self.conditionSuccessSet and not conditionDone then
    conditionDone = self:EvalConditionsAny(self.conditionSuccess)
  end  

  -- Check if mission is not OVER yet.
  if self:IsNotOver() then

    if self:CheckGroupsDone() then

      -- All groups have reported MISSON DONE.
      self:Done()

    elseif (self.Tstop and Tnow>self.Tstop+10) then

      -- Cancel mission if stop time passed.
      self:Cancel()
      
    elseif conditionDone then
    
      -- Cancel mission if conditions were met.
      self:Cancel()    

    elseif self.durationExe and self.Texecuting and Tnow-self.Texecuting>self.durationExe then

      -- Backup repeat values
      local Nrepeat=self.Nrepeat
      local NrepeatS=self.NrepeatSuccess
      local NrepeatF=self.NrepeatFailure

      -- Cancel mission if stop time passed.
      self:Cancel()

      self.Nrepeat=Nrepeat
      self.NrepeatSuccess=NrepeatS
      self.NrepeatFailure=NrepeatF

    elseif (Ntargets0>0 and Ntargets==0) then

      -- Cancel mission if mission targets are gone (if there were any in the beginning).
      -- TODO: I commented this out for some reason but I forgot why...
      self:T(self.lid.."No targets left cancelling mission!")
      self:Cancel()

    elseif self:IsExecuting() and self:_IsNotReinforcing() then
    
--      env.info("Mission Done:")
--      env.info(string.format("Nreinforce= %d", self.reinforce or 0))      
--      env.info(string.format("Nassigned = %d", self.Nassigned))
--      env.info(string.format("Ndead     = %d", self.Ndead))
--      env.info(string.format("Nass-Ndead= %d", Nassigned))

      -- Had the case that mission was in state Executing but all assigned groups were dead.
      -- TODO: might need to loop over all assigned groups
      if Ngroups==0 then
        self:Done()
      else
        local done=true
        for groupname,data in pairs(self.groupdata or {}) do
          local groupdata=data --#AUFTRAG.GroupData
          local opsgroup=groupdata.opsgroup
          if opsgroup:IsAlive() then
            done=false
          end
        end
        if done then
          self:Done()
        end
      end

    end

  end

  -- Current FSM state.
  local fsmstate=self:GetState()

  -- Check for error.
  if fsmstate~=self.status then
    self:T(self.lid..string.format("ERROR: FSM state %s != %s mission status!", fsmstate, self.status))
  end

  -- General info.
  if self.verbose>=1 then

    -- Mission start stop time.
    local Cstart=UTILS.SecondsToClock(self.Tstart, true)
    local Cstop=self.Tstop and UTILS.SecondsToClock(self.Tstop, true) or "INF"

    local targetname=self:GetTargetName() or "unknown"

    local Nlegions=#self.legions
    local commander=self.commander and self.statusCommander or "N/A"
    local chief=self.chief and self.statusChief or "N/A"

    -- Info message.
    self:T(self.lid..string.format("Status %s: Target=%s, T=%s-%s, assets=%d, groups=%d, targets=%d, legions=%d, commander=%s, chief=%s",
    self.status, targetname, Cstart, Cstop, #self.assets, Ngroups, Ntargets, Nlegions, commander, chief))
  end

  -- Group info.
  if self.verbose>=2 then
    -- Data on assigned groups.
    local text="Group data:"
    for groupname,_groupdata in pairs(self.groupdata) do
      local groupdata=_groupdata --#AUFTRAG.GroupData
      text=text..string.format("\n- %s: status mission=%s opsgroup=%s", groupname, groupdata.status, groupdata.opsgroup and groupdata.opsgroup:GetState() or "N/A")
    end
    self:I(self.lid..text)
  end
  
  -- Group info.
  if self.verbose>=3 then
    -- Data on assigned groups.
    local text=string.format("Assets [N=%d,Nassigned=%s, Ndead=%s]:", self.Nassets or 0, self.Nassigned or 0, self.Ndead or 0)
    for i,_asset in pairs(self.assets or {}) do
      local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
      text=text..string.format("\n[%d] %s: spawned=%s, requested=%s, reserved=%s", i, asset.spawngroupname, tostring(asset.spawned), tostring(asset.requested), tostring(asset.reserved))
    end
    self:I(self.lid..text)
  end  
    
  -- Ready to evaluate mission outcome?
  local ready2evaluate=self.Tover and Tnow-self.Tover>=self.dTevaluate or false

  -- Check if mission is OVER (done or cancelled) and enough time passed to evaluate the result.
  if self:IsOver() and ready2evaluate then
    -- Evaluate success or failure of the mission.
    self:Evaluate()
  else
    self:__Status(-30)
  end

  -- Update F10 marker.
  if self.markerOn then
    self:UpdateMarker()
  end

end

--- Evaluate mission outcome - success or failure.
-- @param #AUFTRAG self
-- @return #AUFTRAG self
function AUFTRAG:Evaluate()

  -- Assume success and check if any failed condition applies.
  local failed=false

  -- Target damage in %.
  local targetdamage=self:GetTargetDamage()

  -- Own damage in %.
  local owndamage=self.Ncasualties/self.Nelements*100

  -- Current number of mission targets.
  local Ntargets=self:CountMissionTargets()
  local Ntargets0=self:GetTargetInitialNumber()

  local Life=self:GetTargetLife()
  local Life0=self:GetTargetInitialLife()


  if Ntargets0>0 then

    ---
    -- Mission had targets
    ---

    -- Check if failed.
    if self.type==AUFTRAG.Type.TROOPTRANSPORT or self.type==AUFTRAG.Type.ESCORT then

      -- Transported or escorted groups have to survive.
      if Ntargets<Ntargets0 then
        failed=true
      end
      
    elseif self.type==AUFTRAG.Type.CARGOTRANSPORT then
    
      -- Get zone and cargo.
      local zone=self.DCStask.params.zone   --Core.Zone#ZONE
      local cargo=self.DCStask.params.cargo --Wrapper.Static#STATIC
      
      --Check that cargo is in drop zone.
      if cargo and zone then
        failed=not cargo:IsInZone(zone)
      else
        failed=true
      end

    elseif self.type==AUFTRAG.Type.RESCUEHELO then

      -- Rescue helo has to survive.
      if self.Nelements==self.Ncasualties then
        failed=true
      end

    else

      -- Still targets left.
      if Ntargets>0 then
        failed=true
      end

    end

  else

    ---
    -- Mission had NO targets
    ---

      -- No targets and everybody died ==> mission failed. Well, unless success condition is true.
      if self.Nelements==self.Ncasualties then
        failed=true
      end

  end


  -- Any success condition true?
  local successCondition=self:EvalConditionsAny(self.conditionSuccess)

  -- Any failure condition true?
  local failureCondition=self:EvalConditionsAny(self.conditionFailure)

  if failureCondition then
    failed=true
  elseif successCondition then
    failed=false
  end

  -- Debug text.
  if self.verbose > 0 then
    local text=string.format("Evaluating mission:\n")
    text=text..string.format("Own casualties = %d/%d\n", self.Ncasualties, self.Nelements)
    text=text..string.format("Own losses     = %.1f %%\n", owndamage)
    text=text..string.format("Killed units   = %d\n", self.Nkills)
    text=text..string.format("--------------------------\n")
    text=text..string.format("Targets left   = %d/%d\n", Ntargets, Ntargets0)
    text=text..string.format("Targets life   = %.1f/%.1f\n", Life, Life0)
    text=text..string.format("Enemy losses   = %.1f %%\n", targetdamage)
    text=text..string.format("--------------------------\n")
    text=text..string.format("Success Cond   = %s\n", tostring(successCondition))
    text=text..string.format("Failure Cond   = %s\n", tostring(failureCondition))
    text=text..string.format("--------------------------\n")
    text=text..string.format("Final Success  = %s\n", tostring(not failed))
    text=text..string.format("=========================")
    self:I(self.lid..text)
  end

  -- Trigger events.
  if failed then
    self:I(self.lid..string.format("Mission %d [%s] failed!", self.auftragsnummer, self.type))
    if self.chief then
      self.chief.Nfailure=self.chief.Nfailure+1
    end
    self:Failed()
  else
    self:I(self.lid..string.format("Mission %d [%s] success!", self.auftragsnummer, self.type))
    if self.chief then
      self.chief.Nsuccess=self.chief.Nsuccess+1
    end
    self:Success()
  end

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Asset Data
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get all OPS groups.
-- @param #AUFTRAG self
-- @return #table Table of Ops.OpsGroup#OPSGROUP or {}.
function AUFTRAG:GetOpsGroups()
  local opsgroups={}
  for _,_groupdata in pairs(self.groupdata or {}) do
    local groupdata=_groupdata --#AUFTRAG.GroupData
    table.insert(opsgroups, groupdata.opsgroup)
  end
  return opsgroups
end

--- Get asset data table.
-- @param #AUFTRAG self
-- @param #string AssetName Name of the asset.
-- @return #AUFTRAG.GroupData Group data or *nil* if OPS group does not exist.
function AUFTRAG:GetAssetDataByName(AssetName)
  return self.groupdata[tostring(AssetName)]
end

--- Get flight data table.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group.
-- @return #AUFTRAG.GroupData Flight data or nil if opsgroup does not exist.
function AUFTRAG:GetGroupData(opsgroup)
  if opsgroup and self.groupdata then
    return self.groupdata[opsgroup.groupname]
  end
  return nil
end

--- Set opsgroup mission status.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP opsgroup The flight group.
-- @param #string status New status.
-- @return #AUFTRAG self
function AUFTRAG:SetGroupStatus(opsgroup, status)

  -- Current status.
  local oldstatus=self:GetGroupStatus(opsgroup)

  -- Debug info.
  self:T(self.lid..string.format("Setting OPSGROUP %s to status %s-->%s", opsgroup and opsgroup.groupname or "nil", tostring(oldstatus), tostring(status)))

  if oldstatus==AUFTRAG.GroupStatus.CANCELLED and status==AUFTRAG.GroupStatus.DONE then
    -- Do not overwrite a CANCELLED status with a DONE status.
  else
    local groupdata=self:GetGroupData(opsgroup)
    if groupdata then
      groupdata.status=status
    else
      self:T(self.lid.."WARNING: Could not SET flight data for flight group. Setting status to DONE")
    end
  end

  -- Check if mission is NOT over.
  local isNotOver=self:IsNotOver()

  -- Check if all assigned groups are done.
  local groupsDone=self:CheckGroupsDone()

  -- Debug info.
  self:T2(self.lid..string.format("Setting OPSGROUP %s status to %s. IsNotOver=%s  CheckGroupsDone=%s", opsgroup.groupname, self:GetGroupStatus(opsgroup), tostring(self:IsNotOver()), tostring(groupsDone)))

  -- Check if ALL flights are done with their mission.
  if isNotOver and groupsDone then
    self:T3(self.lid.."All assigned OPSGROUPs done ==> mission DONE!")
    self:Done()
  else
    self:T3(self.lid.."Mission NOT DONE yet!")
  end

  return self
end

--- Get ops group mission status.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group.
-- @return #string The group status.
function AUFTRAG:GetGroupStatus(opsgroup)
  self:T3(self.lid..string.format("Trying to get Flight status for flight group %s", opsgroup and opsgroup.groupname or "nil"))

  local groupdata=self:GetGroupData(opsgroup)

  if groupdata then
    return groupdata.status
  else

    self:T(self.lid..string.format("WARNING: Could not GET groupdata for opsgroup %s. Returning status DONE.", opsgroup and opsgroup.groupname or "nil"))
    return AUFTRAG.GroupStatus.DONE

  end
end

--- Add LEGION to mission.
-- @param #AUFTRAG self
-- @param Ops.Legion#LEGION Legion The legion.
-- @return #AUFTRAG self
function AUFTRAG:AddLegion(Legion)

  -- Debug info.
  self:T(self.lid..string.format("Adding legion %s", Legion.alias))

  -- Add legion to table.
  table.insert(self.legions, Legion)

  return self
end

--- Remove LEGION from mission.
-- @param #AUFTRAG self
-- @param Ops.Legion#LEGION Legion The legion.
-- @return #AUFTRAG self
function AUFTRAG:RemoveLegion(Legion)

  -- Loop over legions
  for i=#self.legions,1,-1 do
    local legion=self.legions[i] --Ops.Legion#LEGION
    
    if legion.alias==Legion.alias then
    
      -- Debug info.
      self:T(self.lid..string.format("Removing legion %s", Legion.alias))
      table.remove(self.legions, i)
      
      -- Set legion status to nil.
      self.statusLegion[Legion.alias]=nil
      
      return self
    end
    
  end

  self:T(self.lid..string.format("ERROR: Legion %s not found and could not be removed!", Legion.alias))
  return self
end

--- Set LEGION mission status.
-- @param #AUFTRAG self
-- @param Ops.Legion#LEGION Legion The legion.
-- @param #string Status New status.
-- @return #AUFTRAG self
function AUFTRAG:SetLegionStatus(Legion, Status)

  -- Old status
  local status=self:GetLegionStatus(Legion)

  -- Debug info.
  self:T(self.lid..string.format("Setting LEGION %s to status %s-->%s", Legion.alias, tostring(status), tostring(Status)))

  -- New status.
  self.statusLegion[Legion.alias]=Status

  return self
end

--- Get LEGION mission status.
-- @param #AUFTRAG self
-- @param Ops.Legion#LEGION Legion The legion.
-- @return #string status Current status.
function AUFTRAG:GetLegionStatus(Legion)

  -- New status.
  local status=self.statusLegion[Legion.alias] or "unknown"

  return status
end


--- Set mission (ingress) waypoint coordinate for OPS group.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group.
-- @param Core.Point#COORDINATE coordinate Waypoint Coordinate.
-- @return #AUFTRAG self
function AUFTRAG:SetGroupWaypointCoordinate(opsgroup, coordinate)
  local groupdata=self:GetGroupData(opsgroup)
  if groupdata then
    groupdata.waypointcoordinate=coordinate
  end
  return self
end

--- [Air] Set mission (ingress) waypoint coordinate for FLIGHT group.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE coordinate Waypoint Coordinate.
-- @return #AUFTRAG self
function AUFTRAG:SetIngressCoordinate(coordinate)
  self.missionIngressCoord = coordinate
  self.missionIngressCoordAlt = UTILS.MetersToFeet(coordinate.y) or 10000
  return self
end

--- Get mission (ingress) waypoint coordinate of OPS group
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group.
-- @return Core.Point#COORDINATE Waypoint Coordinate.
function AUFTRAG:GetGroupWaypointCoordinate(opsgroup)
  local groupdata=self:GetGroupData(opsgroup)
  if groupdata then
    return groupdata.waypointcoordinate
  end
end


--- Set mission waypoint task for OPS group.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group.
-- @param Ops.OpsGroup#OPSGROUP.Task task Waypoint task.
function AUFTRAG:SetGroupWaypointTask(opsgroup, task)
  self:T2(self.lid..string.format("Setting waypoint task %s", task and task.description or "WTF"))
  local groupdata=self:GetGroupData(opsgroup)
  if groupdata then
    groupdata.waypointtask=task
  end
end

--- Get mission waypoint task of OPS group.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group.
-- @return Ops.OpsGroup#OPSGROUP.Task task Waypoint task. Waypoint task.
function AUFTRAG:GetGroupWaypointTask(opsgroup)
  local groupdata=self:GetGroupData(opsgroup)
  if groupdata then
    return groupdata.waypointtask
  end
end

--- Set mission (ingress) waypoint UID for OPS group.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group.
-- @param #number waypointindex Waypoint UID.
-- @return #AUFTRAG self
function AUFTRAG:SetGroupWaypointIndex(opsgroup, waypointindex)
  self:T2(self.lid..string.format("Setting Mission waypoint UID=%d", waypointindex))
  local groupdata=self:GetGroupData(opsgroup)
  if groupdata then
    groupdata.waypointindex=waypointindex
  end
  return self
end

--- Get mission (ingress) waypoint UID of OPS group.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group.
-- @return #number Waypoint UID.
function AUFTRAG:GetGroupWaypointIndex(opsgroup)
  local groupdata=self:GetGroupData(opsgroup)
  if groupdata then
    return groupdata.waypointindex
  end
end

--- Set Egress waypoint UID for OPS group.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group.
-- @param #number waypointindex Waypoint UID.
-- @return #AUFTRAG self
function AUFTRAG:SetGroupEgressWaypointUID(opsgroup, waypointindex)
  self:T2(self.lid..string.format("Setting Egress waypoint UID=%d", waypointindex))
  local groupdata=self:GetGroupData(opsgroup)
  if groupdata then
    groupdata.waypointEgressUID=waypointindex
  end
  return self
end

--- Get Egress waypoint UID of OPS group.
-- @param #AUFTRAG self
-- @param Ops.OpsGroup#OPSGROUP opsgroup The OPS group.
-- @return #number Waypoint UID.
function AUFTRAG:GetGroupEgressWaypointUID(opsgroup)
  local groupdata=self:GetGroupData(opsgroup)
  if groupdata then
    return groupdata.waypointEgressUID
  end
end

--- Check if all groups are done with their mission (or dead).
-- @param #AUFTRAG self
-- @return #boolean If `true`, all groups are done with the mission.
function AUFTRAG:CheckGroupsDone()

  local fsmState = self:GetState()

  -- Check status of all OPS groups.
  for groupname,data in pairs(self.groupdata) do
    local groupdata=data --#AUFTRAG.GroupData
    if groupdata then
      if not (groupdata.status==AUFTRAG.GroupStatus.DONE or groupdata.status==AUFTRAG.GroupStatus.CANCELLED) then
        -- At least this group is not DONE or CANCELLED.
        self:T2(self.lid..string.format("CheckGroupsDone: OPSGROUP %s is not DONE or CANCELLED but in state %s. Mission NOT DONE!", groupdata.opsgroup.groupname, groupdata.status:upper()))
        return false
      end
    end
  end

  -- Check status of all LEGIONs.
  for _,_legion in pairs(self.legions) do
    local legion=_legion --Ops.Legion#LEGION
    local status=self:GetLegionStatus(legion)
    if not status==AUFTRAG.Status.CANCELLED then
      -- At least one LEGION has not CANCELLED.
      self:T2(self.lid..string.format("CheckGroupsDone: LEGION %s is not CANCELLED but in state %s. Mission NOT DONE!", legion.alias, status))
      return false
    end
  end

  -- Check commander status.
  if self.commander then
    if not self.statusCommander==AUFTRAG.Status.CANCELLED then
      self:T2(self.lid..string.format("CheckGroupsDone: COMMANDER is not CANCELLED but in state %s. Mission NOT DONE!", self.statusCommander))
      return false
    end
  end

  -- Check chief status.
  if self.chief then
    if not self.statusChief==AUFTRAG.Status.CANCELLED then
      self:T2(self.lid..string.format("CheckGroupsDone: CHIEF is not CANCELLED but in state %s. Mission NOT DONE!", self.statusChief))
      return false
    end
  end

  -- These are early stages, where we might not even have a opsgroup defined to be checked. If there were any groups, we checked above.
  if self:IsPlanned() or self:IsQueued() or self:IsRequested() then
    self:T2(self.lid..string.format("CheckGroupsDone: Mission is still in state %s [FSM=%s] (PLANNED or QUEUED or REQUESTED). Mission NOT DONE!", self.status, self:GetState()))
    return false
  end
  
  -- Check if there is still reinforcement to be expected.
  if self:IsExecuting() and self:_IsReinforcing() then
    self:T2(self.lid..string.format("CheckGroupsDone: Mission is still in state %s [FSM=%s] and reinfoce=%d. Mission NOT DONE!", self.status, self:GetState(), self.reinforce))
    return false  
  end

  -- It could be that all groups were destroyed on the way to the mission execution waypoint.
  -- TODO: would be better to check if everybody is dead by now.
  if self:IsStarted() and self:CountOpsGroups()==0 then
    self:T(self.lid..string.format("CheckGroupsDone: Mission is STARTED state %s [FSM=%s] but count of alive OPSGROUP is zero. Mission DONE!", self.status, self:GetState()))
    return true
  end
  
  if (self:IsStarted() or self:IsExecuting()) and (fsmState == AUFTRAG.Status.STARTED or fsmState == AUFTRAG.Status.EXECUTING) and self:CountOpsGroups()>0 then
    self:T(self.lid..string.format("CheckGroupsDone: Mission is STARTED state %s [FSM=%s] and count of alive OPSGROUP > zero. Mission NOT DONE!", self.status, self:GetState()))
    return false
  end

  return true
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- EVENT Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Unit lost event.
-- @param #AUFTRAG self
-- @param Core.Event#EVENTDATA EventData Event data.
function AUFTRAG:OnEventUnitLost(EventData)

  -- Check that this is the right group.
  if EventData and EventData.IniGroup and EventData.IniUnit then
    local unit=EventData.IniUnit
    local group=EventData.IniGroup
    local unitname=EventData.IniUnitName

    for _,_groupdata in pairs(self.groupdata) do
      local groupdata=_groupdata --#AUFTRAG.GroupData
      if groupdata and groupdata.opsgroup and groupdata.opsgroup.groupname==EventData.IniGroupName then
        self:T(self.lid..string.format("UNIT LOST event for opsgroup %s unit %s", groupdata.opsgroup.groupname, EventData.IniUnitName))
      end
    end

  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


--- On after "Planned" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterPlanned(From, Event, To)
  self.status=AUFTRAG.Status.PLANNED
  self:T(self.lid..string.format("New mission status=%s", self.status))
end

--- On after "Queue" event. Mission is added to the mission queue of a LEGION.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterQueued(From, Event, To, Airwing)
  self.status=AUFTRAG.Status.QUEUED
  self:T(self.lid..string.format("New mission status=%s", self.status))
end


--- On after "Requested" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterRequested(From, Event, To)
  self.status=AUFTRAG.Status.REQUESTED
  self:T(self.lid..string.format("New mission status=%s", self.status))
end

--- On after "Assign" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterAssign(From, Event, To)
  self.status=AUFTRAG.Status.ASSIGNED
  self:T(self.lid..string.format("New mission status=%s", self.status))
end

--- On after "Schedule" event. Mission is added to the mission queue of an OPSGROUP.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterScheduled(From, Event, To)
  self.status=AUFTRAG.Status.SCHEDULED
  self:T(self.lid..string.format("New mission status=%s", self.status))
end

--- On after "Start" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterStarted(From, Event, To)
  self.status=AUFTRAG.Status.STARTED
  self.Tstarted=timer.getAbsTime()
  self:T(self.lid..string.format("New mission status=%s", self.status))
end

--- On after "Execute" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterExecuting(From, Event, To)
  self.status=AUFTRAG.Status.EXECUTING
  self.Texecuting=timer.getAbsTime()
  self:T(self.lid..string.format("New mission status=%s", self.status))
end

--- On after "ElementDestroyed" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP OpsGroup The ops group to which the element belongs.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The element that got destroyed.
function AUFTRAG:onafterElementDestroyed(From, Event, To, OpsGroup, Element)
  -- Increase number of own casualties.
  self.Ncasualties=self.Ncasualties+1
end

--- On after "GroupDead" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP OpsGroup The ops group that is dead now.
function AUFTRAG:onafterGroupDead(From, Event, To, OpsGroup)

  local asset=self:GetAssetByName(OpsGroup.groupname)
  
  if asset then
    self:AssetDead(asset)
  end
  
  -- Number of dead groups.
  self.Ndead=self.Ndead+1

end

--- On after "AssetDead" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset.
function AUFTRAG:onafterAssetDead(From, Event, To, Asset)

  -- Number of groups alive.
  local N=self:CountOpsGroups()
  
  local notreinforcing=self:_IsNotReinforcing()

  self:T(self.lid..string.format("Asset %s dead! Number of ops groups remaining %d (reinforcing=%s)", tostring(Asset.spawngroupname), N, tostring(not notreinforcing)))

  -- All assets dead?
  if N==0 and notreinforcing then

    if self:IsNotOver() then

      -- Cancel mission. Wait for next mission update to evaluate SUCCESS or FAILURE.
      self:Cancel()

    else

      --self:E(self.lid.."ERROR: All assets are dead not but mission was already over... Investigate!")
      -- Now this can happen, because when a opsgroup dies (sometimes!), the mission is DONE

    end
  end

  -- Delete asset from mission.
  self:DelAsset(Asset)

end

--- On after "Cancel" event. Cancells the mission.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterCancel(From, Event, To)

  -- Number of OPSGROUPS assigned and alive.
  local Ngroups = self:CountOpsGroups()

  -- Debug info.
  self:T(self.lid..string.format("CANCELLING mission in status %s. Will wait for %d groups to report mission DONE before evaluation", self.status, Ngroups))

  -- Time stamp.
  self.Tover=timer.getAbsTime()

  -- No more repeats.
  self.Nrepeat=self.repeated
  self.NrepeatFailure=self.repeatedFailure
  self.NrepeatSuccess=self.repeatedSuccess

  -- Not necessary to delay the evaluaton?!
  self.dTevaluate=0

  if self.chief then

    -- Debug info.
    self:T(self.lid..string.format("CHIEF will cancel the mission. Will wait for mission DONE before evaluation!"))

    -- CHIEF will cancel the mission.
    self.chief:MissionCancel(self)

  elseif self.commander then

    -- Debug info.
    self:T(self.lid..string.format("COMMANDER will cancel the mission. Will wait for mission DONE before evaluation!"))

    -- COMMANDER will cancel the mission.
    self.commander:MissionCancel(self)

  elseif self.legions and #self.legions>0 then

    -- Loop over all LEGIONs.
    for _,_legion in pairs(self.legions or {}) do
      local legion=_legion --Ops.Legion#LEGION

      -- Debug info.
      self:T(self.lid..string.format("LEGION %s will cancel the mission. Will wait for mission DONE before evaluation!", legion.alias))

      -- Legion will cancel all group's missions and remove queued request from warehouse queue.
      legion:MissionCancel(self)

    end

  else

    -- Debug info.
    self:T(self.lid..string.format("No legion, commander or chief. Attached groups will cancel the mission on their own. Will wait for mission DONE before evaluation!"))

    -- Loop over all groups.
    for _,_groupdata in pairs(self.groupdata or {}) do
      local groupdata=_groupdata --#AUFTRAG.GroupData
      groupdata.opsgroup:MissionCancel(self)
    end

  end

  -- Special mission states.
  if self:IsPlanned() or self:IsQueued() or self:IsRequested() or Ngroups==0 then
    self:T(self.lid..string.format("Cancelled mission was in %s stage with %d groups assigned and alive. Call it done!", self.status, Ngroups))
    self:Done()
  end

end

--- On after "Done" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterDone(From, Event, To)
  self.status=AUFTRAG.Status.DONE
  self:T(self.lid..string.format("New mission status=%s", self.status))

  -- Set time stamp.
  self.Tover=timer.getAbsTime()

  -- Not executing any more.
  self.Texecuting=nil
  
  -- Set status for CHIEF.
  self.statusChief=AUFTRAG.Status.DONE
  
  -- Set status for COMMANDER.
  self.statusCommander=AUFTRAG.Status.DONE
  
  -- Set status for LEGIONs.
  for _,_legion in pairs(self.legions) do
    local Legion=_legion --Ops.Legion#LEGION
    
    self:SetLegionStatus(Legion, AUFTRAG.Status.DONE)
    
    -- Remove pending request from legion queue.
    if self.type==AUFTRAG.Type.RELOCATECOHORT then
    
      -- Get request ID
      local requestid=self.requestID[Legion.alias]
      
      if requestid then
      
        -- Debug info.
        self:T(self.lid.."Removing request from pending queue")
        
        -- Remove request from pending queue.
        Legion:_DeleteQueueItemByID(requestid, Legion.pending)
        
        -- Remove cohort from old legion.
        local Cohort=self.DCStask.params.cohort --Ops.Cohort#COHORT
        Legion:DelCohort(Cohort)
        
      else
        self:E(self.lid.."WARNING: Could NOT remove relocation request from from pending queue (all assets were spawned?)")
      end
    end
  end
  
  -- Trigger relocated event.
  if self.type==AUFTRAG.Type.RELOCATECOHORT then
    local cohort=self.DCStask.params.cohort --Ops.Cohort#COHORT
    cohort:Relocated()
  end
end

--- On after "Success" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterSuccess(From, Event, To)

  self.status=AUFTRAG.Status.SUCCESS
  self:T(self.lid..string.format("New mission status=%s", self.status))

  -- Set status for CHIEF, COMMANDER and LEGIONs
  self.statusChief=self.status
  self.statusCommander=self.status
  for _,_legion in pairs(self.legions) do
    local Legion=_legion --Ops.Legion#LEGION
    self:SetLegionStatus(Legion, self.status)
  end

  local repeatme=self.repeatedSuccess<self.NrepeatSuccess or self.repeated<self.Nrepeat

  if repeatme then

    -- Increase counter.
    self.repeatedSuccess=self.repeatedSuccess+1

    -- Number of repeats.
    local N=math.max(self.NrepeatSuccess, self.Nrepeat)

    -- Repeat mission.
    self:T(self.lid..string.format("Mission SUCCESS! Repeating mission for the %d time (max %d times) ==> Repeat mission!", self.repeated+1, N))
    self:__Repeat(self.repeatDelay)

  else

    -- Stop mission.
    self:T(self.lid..string.format("Mission SUCCESS! Number of max repeats %d reached  ==> Stopping mission!", self.repeated+1))
    self:Stop()

  end

end

--- On after "Failed" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterFailed(From, Event, To)

  self.status=AUFTRAG.Status.FAILED
  self:T(self.lid..string.format("New mission status=%s", self.status))

  -- Set status for CHIEF, COMMANDER and LEGIONs
  self.statusChief=self.status
  self.statusCommander=self.status
  for _,_legion in pairs(self.legions) do
    local Legion=_legion --Ops.Legion#LEGION
    self:SetLegionStatus(Legion, self.status)
  end

  local repeatme=self.repeatedFailure<self.NrepeatFailure or self.repeated<self.Nrepeat

  if repeatme then

    -- Increase counter.
    self.repeatedFailure=self.repeatedFailure+1

    -- Number of repeats.
    local N=math.max(self.NrepeatFailure, self.Nrepeat)

    -- Repeat mission.
    self:T(self.lid..string.format("Mission FAILED! Repeating mission for the %d time (max %d times) ==> Repeat mission!", self.repeated+1, N))
    self:__Repeat(self.repeatDelay)

  else

    -- Stop mission.
    self:T(self.lid..string.format("Mission FAILED! Number of max repeats %d reached ==> Stopping mission!", self.repeated+1))
    self:Stop()

  end

end

--- On before "Repeat" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onbeforeRepeat(From, Event, To)

  if not (self.chief or self.commander or #self.legions>0) then
    self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, COMMANDER or LEGION! Stopping AUFTRAG")
    self:Stop()
    return false
  end

  return true
end

--- On after "Repeat" event.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterRepeat(From, Event, To)

  -- Set mission status to PLANNED.
  self.status=AUFTRAG.Status.PLANNED

  -- Debug info.
  self:T(self.lid..string.format("New mission status=%s (on Repeat)", self.status))

  -- Set status for CHIEF, COMMANDER and LEGIONs
  self.statusChief=self.status
  self.statusCommander=self.status
  for _,_legion in pairs(self.legions) do
    local Legion=_legion --Ops.Legion#LEGION
    self:SetLegionStatus(Legion, self.status)
  end

  -- Increase repeat counter.
  self.repeated=self.repeated+1

  if self.chief then

    -- Set status for chief.
    self.statusChief=AUFTRAG.Status.PLANNED

    -- Remove mission from wingcommander because Chief will assign it again.
    if self.commander then
      self.statusCommander=AUFTRAG.Status.PLANNED
    end

    -- Remove mission from legions because commander will assign it again but maybe to different legion(s).
    for _,_legion in pairs(self.legions) do
      local legion=_legion --Ops.Legion#LEGION
      legion:RemoveMission(self)
    end

  elseif self.commander then

    -- Set status for commander.
    self.statusCommander=AUFTRAG.Status.PLANNED

    -- Remove mission from legion(s) because commander will assign it again but maybe to different legion(s).
    for _,_legion in pairs(self.legions) do
      local legion=_legion --Ops.Legion#LEGION
      legion:RemoveMission(self)
      self:SetLegionStatus(legion, AUFTRAG.Status.PLANNED)
    end

  elseif #self.legions>0 then

    -- Remove mission from airwing because WC will assign it again but maybe to a different wing.
    for _,_legion in pairs(self.legions) do
      local legion=_legion --Ops.Legion#LEGION
      legion:RemoveMission(self)
      self:SetLegionStatus(legion, AUFTRAG.Status.PLANNED)
      legion:AddMission(self)
    end

  else
    self:E(self.lid.."ERROR: Mission can only be repeated by a CHIEF, COMMANDER or LEGION! Stopping AUFTRAG")
    self:Stop()
    return
  end


  -- No mission assets.
  self.assets={}


  -- Remove OPS groups. This also removes the mission from the OPSGROUP mission queue.
  for groupname,_groupdata in pairs(self.groupdata) do
    local groupdata=_groupdata --#AUFTRAG.GroupData
    local opsgroup=groupdata.opsgroup
    if opsgroup then
      self:DelOpsGroup(opsgroup)
    end

  end
  -- No group data.
  self.groupdata={}

  -- Reset casualties and units assigned.
  self.Ncasualties=0
  self.Nelements=0
  self.Ngroups=0
  self.Nassigned=nil
  self.Ndead=0

  -- Update DCS mission task. Could be that the initial task (e.g. for bombing) was destroyed. Then we need to update the coordinate.
  self.DCStask=self:GetDCSMissionTask()

  -- Call status again.
  self:__Status(-30)

end

--- On after "Stop" event. Remove mission from LEGION and OPSGROUP mission queues.
-- @param #AUFTRAG self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AUFTRAG:onafterStop(From, Event, To)

  -- Debug info.
  self:T(self.lid..string.format("STOPPED mission in status=%s. Removing missions from queues. Stopping CallScheduler!", self.status))

  -- TODO: Mission should be OVER! we dont want to remove running missions from any queues.

  -- Remove mission from CHIEF queue.
  if self.chief then
    self.chief:RemoveMission(self)
  end

  -- Remove mission from WINGCOMMANDER queue.
  if self.commander then
    self.commander:RemoveMission(self)
  end

  -- Remove mission from LEGION queues.
  if #self.legions>0 then
    for _,_legion in pairs(self.legions) do
      local legion=_legion --Ops.Legion#LEGION
      legion:RemoveMission(self)
    end
  end

  -- Remove mission from OPSGROUP queue
  for _,_groupdata in pairs(self.groupdata) do
    local groupdata=_groupdata --#AUFTRAG.GroupData
    groupdata.opsgroup:RemoveMission(self)
  end

  -- No mission assets.
  self.assets={}

  -- No group data.
  self.groupdata={}

  -- Clear pending scheduler calls.
  self.CallScheduler:Clear()

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Target Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create target data from a given object.
-- @param #AUFTRAG self
-- @param Wrapper.Positionable#POSITIONABLE Object The target GROUP, UNIT, STATIC.
function AUFTRAG:_TargetFromObject(Object)

  if not self.engageTarget then

    if Object and Object:IsInstanceOf("TARGET") then

      self.engageTarget=Object

    else --if Object then

      self.engageTarget=TARGET:New(Object)

    end

  else

    -- Target was already specified elsewhere.

  end

  -- Debug info.
  --self:T2(self.lid..string.format("Mission Target %s Type=%s, Ntargets=%d, Lifepoints=%d", self.engageTarget.lid, self.engageTarget.lid, self.engageTarget.N0, self.engageTarget:GetLife()))

  return self
end


--- Count alive mission targets.
-- @param #AUFTRAG self
-- @return #number Number of alive target units.
function AUFTRAG:CountMissionTargets()

  local N=0

  -- Count specific coalitions.  
  local Coalitions=self.coalition and UTILS.GetCoalitionEnemy(self.coalition, true) or nil

  if self.engageTarget then
    N=self.engageTarget:CountTargets(Coalitions)
  end

  return N
end

--- Get initial number of targets.
-- @param #AUFTRAG self
-- @return #number Number of initial life points when mission was planned.
function AUFTRAG:GetTargetInitialNumber()
  local target=self:GetTargetData()
  if target then
    return target.N0
  else
    return 0
  end
end


--- Get target life points.
-- @param #AUFTRAG self
-- @return #number Number of initial life points when mission was planned.
function AUFTRAG:GetTargetInitialLife()
  local target=self:GetTargetData()
  if target then
    return target.life0
  else
    return 0
  end
end

--- Get target damage.
-- @param #AUFTRAG self
-- @return #number Damage in percent.
function AUFTRAG:GetTargetDamage()
  local target=self:GetTargetData()
  if target then
    return target:GetDamage()
  else
    return 0
  end
end


--- Get target life points.
-- @param #AUFTRAG self
-- @return #number Life points of target.
function AUFTRAG:GetTargetLife()
  local target=self:GetTargetData()
  if target then
    return target:GetLife()
  else
    return 0
  end
end

--- Get target.
-- @param #AUFTRAG self
-- @return Ops.Target#TARGET The target object. Could be many things.
function AUFTRAG:GetTargetData()
  return self.engageTarget
end

--- Get mission objective object. Could be many things depending on the mission type.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE RefCoordinate (Optional) Reference coordinate from which the closest target is determined.
-- @param #table Coalitions (Optional) Only consider targets of the given coalition(s). 
-- @return Wrapper.Positionable#POSITIONABLE The target object. Could be many things.
function AUFTRAG:GetObjective(RefCoordinate, Coalitions)
  
  local objective=self:GetTargetData():GetObject(RefCoordinate, Coalitions)
  
  return objective
end

--- Get type of target.
-- @param #AUFTRAG self
-- @return #string The target type.
function AUFTRAG:GetTargetType()
  local target=self.engageTarget
  if target then
    local to=target:GetObjective()
    if to then
      return to.Type
    else
      return "Unknown"
    end
  else
    return "Unknown"
  end
end

--- Get 2D vector of target.
-- @param #AUFTRAG self
-- @return DCS#VEC2 The target 2D vector or *nil*.
function AUFTRAG:GetTargetVec2()
  local coord=self:GetTargetCoordinate()
  if coord then
    local vec2=coord:GetVec2()
    return vec2
  end
  return nil
end

--- Get coordinate of target.
-- @param #AUFTRAG self
-- @return Core.Point#COORDINATE The target coordinate or *nil*.
function AUFTRAG:GetTargetCoordinate()

  if self.transportPickup then

    -- Special case where we defined a
    return self.transportPickup
    
  elseif self.missionZoneSet and self.type == AUFTRAG.Type.RECON then
  
    return self.missionZoneSet:GetAverageCoordinate()

  elseif self.engageTarget then

    local coord=self.engageTarget:GetCoordinate()
    return coord

  elseif self.type==AUFTRAG.Type.ALERT5 then

    -- For example, COMMANDER will not assign a coordiante. This will be done later, when the mission is assigned to an airwing.
    return nil

  else
    self:T(self.lid.."ERROR: Cannot get target coordinate!")
  end

  return nil
end

--- Get heading of target.
-- @param #AUFTRAG self
-- @return #number Heading of target in degrees.
function AUFTRAG:GetTargetHeading()
  if self.engageTarget then
    local heading=self.engageTarget:GetHeading()
    return heading
  end
  return nil
end

--- Get name of the target.
-- @param #AUFTRAG self
-- @return #string Name of the target or "N/A".
function AUFTRAG:GetTargetName()

  if self.engageTarget then
    local name=self.engageTarget:GetName()
    return name
  end

  return "N/A"
end


--- Get distance to target.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE FromCoord The coordinate from which the distance is measured.
-- @return #number Distance in meters or 0.
function AUFTRAG:GetTargetDistance(FromCoord)

  local TargetCoord=self:GetTargetCoordinate()

  if TargetCoord and FromCoord then
    return TargetCoord:Get2DDistance(FromCoord)
  else
    self:T(self.lid.."ERROR: TargetCoord or FromCoord does not exist in AUFTRAG:GetTargetDistance() function! Returning 0")
  end

  return 0
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Add asset to mission.
-- @param #AUFTRAG self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be added to the mission.
-- @return #AUFTRAG self
function AUFTRAG:AddAsset(Asset)

  -- Debug info
  self:T(self.lid..string.format("Adding asset \"%s\" to mission", tostring(Asset.spawngroupname)))

  -- Add to table.
  self.assets=self.assets or {}
  
  -- Get asset if it was already added.
  local asset=self:GetAssetByName(Asset.spawngroupname)
  
  -- Only add an asset is not already in.
  if not asset then
  
    -- Add to table.
    table.insert(self.assets, Asset)
    
    self.Nassigned=self.Nassigned or 0
    
    self.Nassigned=self.Nassigned+1
    
  end

  return self
end

--- Add assets to mission.
-- @param #AUFTRAG self
-- @param #table Assets List of assets.
-- @return #AUFTRAG self
function AUFTRAG:_AddAssets(Assets)

  for _,asset in pairs(Assets) do
    self:AddAsset(asset)
  end

  return self
end

--- Delete asset from mission.
-- @param #AUFTRAG self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset  The asset to be removed.
-- @return #AUFTRAG self
function AUFTRAG:DelAsset(Asset)

  for i,_asset in pairs(self.assets or {}) do
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem

    if asset.uid==Asset.uid then
      self:T(self.lid..string.format("Removing asset \"%s\" from mission", tostring(Asset.spawngroupname)))
      table.remove(self.assets, i)
      return self
    end

  end

  return self
end

--- Get asset by its spawn group name.
-- @param #AUFTRAG self
-- @param #string Name Asset spawn group name.
-- @return Functional.Warehouse#WAREHOUSE.Assetitem Asset.
function AUFTRAG:GetAssetByName(Name)

  for i,_asset in pairs(self.assets or {}) do
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem

    if asset.spawngroupname==Name then
      return asset
    end

  end

  return nil
end

--- Count alive OPS groups assigned for this mission.
-- @param #AUFTRAG self
-- @return #number Number of alive OPS groups.
function AUFTRAG:CountOpsGroups()
  local N=0
  for _,_groupdata in pairs(self.groupdata) do
    local groupdata=_groupdata --#AUFTRAG.GroupData
    if groupdata and groupdata.opsgroup and groupdata.opsgroup:IsAlive() and not groupdata.opsgroup:IsDead() then
      N=N+1
    end
  end
  return N
end

--- Count OPS groups in a certain status.
-- @param #AUFTRAG self
-- @param #string Status Status of group, e.g. `AUFTRAG.GroupStatus.EXECUTING`.
-- @return #number Number of alive OPS groups.
function AUFTRAG:CountOpsGroupsInStatus(Status)
  local N=0
  for _,_groupdata in pairs(self.groupdata) do
    local groupdata=_groupdata --#AUFTRAG.GroupData
    if groupdata and groupdata.status==Status then
      N=N+1
    end
  end
  return N
end



--- Get coordinate of target. First unit/group of the set is used.
-- @param #AUFTRAG self
-- @param #table MissionTypes A table of mission types.
-- @return #string Comma separated list of mission types.
function AUFTRAG:GetMissionTypesText(MissionTypes)

  local text=""
  for _,missiontype in pairs(MissionTypes) do
    text=text..string.format("%s, ", missiontype)
  end

  return text
end

--- [NON-AIR] Set the mission waypoint coordinate from where the mission is executed. Note that altitude is set via `:SetMissionAltitude`.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Coordinate where the mission is executed.
-- @return #AUFTRAG self
function AUFTRAG:SetMissionWaypointCoord(Coordinate)

  -- Obviously a zone was passed. We get the coordinate.
  if Coordinate:IsInstanceOf("ZONE_BASE") then
    Coordinate=Coordinate:GetCoordinate()
  end

  self.missionWaypointCoord=Coordinate
  return self
end

--- Set randomization of the mission waypoint coordinate. Each assigned group will get a random ingress coordinate, where the mission is executed.
-- @param #AUFTRAG self
-- @param #number Radius Distance in meters. Default `#nil`.
-- @return #AUFTRAG self
function AUFTRAG:SetMissionWaypointRandomization(Radius)
  self.missionWaypointRadius=Radius
  return self
end

--- Set the mission egress coordinate. This is the coordinate where the assigned group will go once the mission is finished.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Egrees coordinate.
-- @param #number Altitude (Optional) Altitude in feet. Default is y component of coordinate.
-- @param #number Speed (Optional) Speed in knots to reach this waypoint. Defaults to mission speed.
-- @return #AUFTRAG self
function AUFTRAG:SetMissionEgressCoord(Coordinate, Altitude, Speed)

  -- Obviously a zone was passed. We get the coordinate.
  if Coordinate:IsInstanceOf("ZONE_BASE") then
    Coordinate=Coordinate:GetCoordinate()
  end

  self.missionEgressCoord=Coordinate

  if Altitude then
    self.missionEgressCoord.y=UTILS.FeetToMeters(Altitude)
    self.missionEgressCoordAlt = UTILS.FeetToMeters(Altitude)
  end
  
  self.missionEgressCoordSpeed=Speed and Speed or nil
  
  return self
end

--- [Air] Set the mission ingress coordinate. This is the coordinate where the assigned group will fly before the actual mission coordinate.
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Ingrees coordinate.
-- @param #number Altitude (Optional) Altitude in feet. Default is y component of coordinate.
-- @param #number Speed (Optional) Speed in knots to reach this waypoint. Defaults to mission speed.
-- @return #AUFTRAG self
function AUFTRAG:SetMissionIngressCoord(Coordinate, Altitude, Speed)

  -- Obviously a zone was passed. We get the coordinate.
  if Coordinate:IsInstanceOf("ZONE_BASE") then
    Coordinate=Coordinate:GetCoordinate()
  end

  self.missionIngressCoord=Coordinate

  if Altitude then
    self.missionIngressCoord.y=UTILS.FeetToMeters(Altitude)
    self.missionIngressCoordAlt = UTILS.FeetToMeters(Altitude or 10000)
  end
  
  self.missionIngressCoordSpeed=Speed and Speed or nil
  
  return self
end

--- [Air] Set the mission holding coordinate. This is the coordinate where the assigned group will fly before the actual mission execution starts. Do not forget to add a push condition, too!
-- @param #AUFTRAG self
-- @param Core.Point#COORDINATE Coordinate Holding coordinate.
-- @param #number Altitude (Optional) Altitude in feet. Default is y component of coordinate.
-- @param #number Speed (Optional) Speed in knots to reach this waypoint and hold there. Defaults to mission speed.
-- @param #number Duration (Optional) Duration in seconds on how long to hold, defaults to 15 minutes. Mission continues if either a push condition is met or the time is up.
-- @return #AUFTRAG self
function AUFTRAG:SetMissionHoldingCoord(Coordinate, Altitude, Speed, Duration)

  -- Obviously a zone was passed. We get the coordinate.
  if Coordinate:IsInstanceOf("ZONE_BASE") then
    Coordinate=Coordinate:GetCoordinate()
  end
  
  self.missionHoldingCoord=Coordinate
  self.missionHoldingDuration=Duration or 900

  if Altitude then
    self.missionHoldingCoord.y=UTILS.FeetToMeters(Altitude)
    self.missionHoldingCoordAlt = UTILS.FeetToMeters(Altitude or 10000)
  end
  
  self.missionHoldingCoordSpeed=Speed and Speed or nil

  return self
end

--- Get the mission egress coordinate if this was defined.
-- @param #AUFTRAG self
-- @return Core.Point#COORDINATE Coordinate Coordinate or nil.
function AUFTRAG:GetMissionEgressCoord()
  return self.missionEgressCoord
end

--- Get the mission ingress coordinate if this was defined.
-- @param #AUFTRAG self
-- @return Core.Point#COORDINATE Coordinate Coordinate or nil.
function AUFTRAG:GetMissionIngressCoord()
  return self.missionIngressCoord
end

--- Get the mission holding coordinate if this was defined.
-- @param #AUFTRAG self
-- @return Core.Point#COORDINATE Coordinate Coordinate or nil.
function AUFTRAG:GetMissionHoldingCoord()
  return self.missionHoldingCoord
end

--- Get coordinate which was set as mission waypoint coordinate.
-- @param #AUFTRAG self
-- @return Core.Point#COORDINATE Coordinate where the mission is executed or `#nil`.
function AUFTRAG:_GetMissionWaypointCoordSet()

  -- Check if a coord has been explicitly set.
  if self.missionWaypointCoord then
    local coord=self.missionWaypointCoord
    if self.missionAltitude then
      coord.y=self.missionAltitude
    end
    
    
    return coord
  end

end

--- Get coordinate of target. First unit/group of the set is used.
-- @param #AUFTRAG self
-- @param Wrapper.Group#GROUP group Group.
-- @param #number randomradius Random radius in meters.
-- @param #table surfacetypes Surface types of random zone.
-- @return Core.Point#COORDINATE Coordinate where the mission is executed.
function AUFTRAG:GetMissionWaypointCoord(group, randomradius, surfacetypes)

  -- Check if a coord has been explicitly set.
  if self.missionWaypointCoord then
    local coord=self.missionWaypointCoord
    if self.missionAltitude then
      coord.y=self.missionAltitude
    end
    return coord
  end
  
  local coord=group:GetCoordinate()
  
  -- Check if an ingress or holding coord has been explicitly set.
  if self.missionHoldingCoord then
    coord=self.missionHoldingCoord
    if self.missionHoldingCoorddAlt then
      coord:SetAltitude(self.missionHoldingCoordAlt, true)
    end
  end
  
  if self.missionIngressCoord then
    coord=self.missionIngressCoord
    if self.missionIngressCoordAlt then
      coord:SetAltitude(self.missionIngressCoordAlt, true)
    end
  end

  -- Create waypoint coordinate half way between us and the target.
  local waypointcoord=COORDINATE:New(0,0,0)
  
  if coord then
    waypointcoord=coord:GetIntermediateCoordinate(self:GetTargetCoordinate(), self.missionFraction)
  else
    self:E(self.lid..string.format("ERROR: Cannot get coordinate of group %s (alive=%s)!", tostring(group:GetName()), tostring(group:IsAlive())))
  end
  local alt=waypointcoord.y

  -- Add some randomization.
  if randomradius then
    waypointcoord=ZONE_RADIUS:New("Temp", waypointcoord:GetVec2(), randomradius):GetRandomCoordinate(nil, nil, surfacetypes):SetAltitude(alt, false)
  end

  -- Set altitude of mission waypoint.
  if self.missionAltitude then
    waypointcoord:SetAltitude(self.missionAltitude, true)
  end

  return waypointcoord
end


--- Set log ID string.
-- @param #AUFTRAG self
-- @return #AUFTRAG self
function AUFTRAG:_SetLogID()
  self.lid=string.format("Auftrag #%d %s | ", self.auftragsnummer, tostring(self.type))
  return self
end


--- Get request ID from legion this mission requested assets from
-- @param #AUFTRAG self
-- @param Ops.Legion#LEGION Legion The legion from which to get the request ID.
-- @return #number Request ID (if any).
function AUFTRAG:_GetRequestID(Legion)

  local requestid=nil
  local name=nil
  
  if type(Legion)=="string" then
    name=Legion
  else
    name=Legion.alias
  end

  if name then
    requestid=self.requestID[name]
  end  

  return nil
end


--- Get request from legion this mission requested assets from.
-- @param #AUFTRAG self
-- @param Ops.Legion#LEGION Legion The legion from which to get the request ID.
-- @return Functional.Warehouse#WAREHOUSE.PendingItem Request.
function AUFTRAG:_GetRequest(Legion)

  local request=nil
  
  local requestID=self:_GetRequestID(Legion)
  
  if requestID then
    request=Legion:GetRequestByID(requestID)
  end

  return request
end

--- Set request ID from legion this mission requested assets from
-- @param #AUFTRAG self
-- @param Ops.Legion#LEGION Legion The legion from which to get the request ID.
-- @param #number RequestID Request ID.
-- @return #AUFTRAG self
function AUFTRAG:_SetRequestID(Legion, RequestID)

  local requestid=nil
  local name=nil
  
  if type(Legion)=="string" then
    name=Legion
  else
    name=Legion.alias
  end

  if name then
    if self.requestID[name] then
      self:I(self.lid..string.format("WARNING: Mission already has a request ID=%d!", self.requestID[name]))
    end
    self.requestID[name]=RequestID
  end  

  return self
end

--- Check if reinforcement is done.
-- @param #AUFTRAG self
-- @return #boolean If `true`, reinforcing is over.
function AUFTRAG:_IsNotReinforcing()

  -- Number of assigned assets that are still alive.
  local Nassigned=self.Nassigned and self.Nassigned-self.Ndead or 0
  
  -- Not reinforcing?
  local notreinforcing=((not self.reinforce) or (self.reinforce==0 and Nassigned<=0))
  
  return notreinforcing
end

--- Check if reinforcement is still ongoing.
-- @param #AUFTRAG self
-- @return #boolean If `true`, reinforcing is ongoing.
function AUFTRAG:_IsReinforcing()

  local reinforcing=not self:_IsNotReinforcing()
  
  return reinforcing
end



--- Update mission F10 map marker.
-- @param #AUFTRAG self
-- @return #AUFTRAG self
function AUFTRAG:UpdateMarker()

  -- Marker text.
  local text=string.format("%s %s: %s", self.name, self.type:upper(), self.status:upper())
  text=text..string.format("\n%s", self:GetTargetName())
  text=text..string.format("\nTargets %d/%d, Life Points=%d/%d", self:CountMissionTargets(), self:GetTargetInitialNumber(), self:GetTargetLife(), self:GetTargetInitialLife())
  text=text..string.format("\nOpsGroups %d/%d", self:CountOpsGroups(), self:GetNumberOfRequiredAssets())

  if not self.marker then

    -- Get target coordinates. Can be nil!
    local targetcoord=self:GetTargetCoordinate()

    if targetcoord then
        if self.markerCoaliton and self.markerCoaliton>=0 then
        self.marker=MARKER:New(targetcoord, text):ReadOnly():ToCoalition(self.markerCoaliton)
        else
        self.marker=MARKER:New(targetcoord, text):ReadOnly():ToAll()
        end
    end

  else

    if self.marker:GetText()~=text then
      self.marker:UpdateText(text)
    end

  end

  return self
end

--- Get DCS task table for the given mission.
-- @param #AUFTRAG self
-- @return DCS#Task The DCS task table. If multiple tasks are necessary, this is returned as a combo task.
function AUFTRAG:GetDCSMissionTask()

  local DCStasks={}

  -- Create DCS task based on current self.
  if self.type==AUFTRAG.Type.ANTISHIP then

    ----------------------
    -- ANTISHIP Mission --
    ----------------------

    -- Add enroute anti-ship task.
    local DCStask=CONTROLLABLE.EnRouteTaskAntiShip(nil)
    table.insert(self.enrouteTasks, DCStask)

    self:_GetDCSAttackTask(self.engageTarget, DCStasks)

  elseif self.type==AUFTRAG.Type.AWACS then

    -------------------
    -- AWACS Mission --
    -------------------

    local DCStask=CONTROLLABLE.EnRouteTaskAWACS(nil)

    table.insert(self.enrouteTasks, DCStask)

  elseif self.type==AUFTRAG.Type.BAI then

    -----------------
    -- BAI Mission --
    -----------------

    self:_GetDCSAttackTask(self.engageTarget, DCStasks)

  elseif self.type==AUFTRAG.Type.BOMBING then

    ---------------------
    -- BOMBING Mission --
    ---------------------

    local coords = self.engageTarget:GetCoordinates()
    for _, coord in pairs(coords) do
        local DCStask = CONTROLLABLE.TaskBombing(nil, coord:GetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType, self.optionDivebomb)

        table.insert(DCStasks, DCStask)
    end

  elseif self.type==AUFTRAG.Type.STRAFING then

    ----------------------
    -- STRAFING Mission --
    ----------------------

    local DCStask=CONTROLLABLE.TaskStrafing(nil,self:GetTargetVec2(), self.engageQuantity, self.engageLength,self.engageWeaponType,self.engageWeaponExpend,self.engageDirection,self.engageAsGroup)

    table.insert(DCStasks, DCStask)    

  elseif self.type==AUFTRAG.Type.BOMBRUNWAY then

    ------------------------
    -- BOMBRUNWAY Mission --
    ------------------------

    local DCStask=CONTROLLABLE.TaskBombingRunway(nil, self.engageTarget:GetObject(), self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAsGroup)

    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.BOMBCARPET then

    ------------------------
    -- BOMBCARPET Mission --
    ------------------------

    local DCStask=CONTROLLABLE.TaskCarpetBombing(nil, self:GetTargetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType, self.engageLength)

    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.CAP then

    -----------------
    -- CAP Mission --
    -----------------
    
    local Vec2 = self.engageZone:GetVec2()
    local Radius
    if self.engageZone:IsInstanceOf("COORDINATE") then
      Radius = UTILS.NMToMeters(20)
    else
      Radius = self.engageZone:GetRadius()
    end
        
    local DCStask=CONTROLLABLE.EnRouteTaskEngageTargetsInZone(nil, Vec2, Radius, self.engageTargetTypes, Priority)

    table.insert(self.enrouteTasks, DCStask)

  elseif self.type==AUFTRAG.Type.CAS then

    -----------------
    -- CAS Mission --
    -----------------

    local DCStask=CONTROLLABLE.EnRouteTaskEngageTargetsInZone(nil, self.engageZone:GetVec2(), self.engageZone:GetRadius(), self.engageTargetTypes, Priority)

    table.insert(self.enrouteTasks, DCStask)

  elseif self.type==AUFTRAG.Type.ESCORT then

    --------------------
    -- ESCORT Mission --
    --------------------

    local DCStask=CONTROLLABLE.TaskEscort(nil, self.engageTarget:GetObject(), self.escortVec3, nil, self.engageMaxDistance, self.engageTargetTypes)

    table.insert(DCStasks, DCStask)
    
  elseif self.type==AUFTRAG.Type.GROUNDESCORT then

    --------------------
    -- GROUNDESCORT Mission --
    --------------------

    local DCSTask=CONTROLLABLE.TaskGroundEscort(nil,self.engageTarget:GetObject(),nil,self.orbitDistance,self.engageTargetTypes)
    
    table.insert(DCStasks, DCSTask)
    
  elseif self.type==AUFTRAG.Type.FACA then

    ------------------
    -- AFAC Mission --
    ------------------

    local DCStask=CONTROLLABLE.TaskFAC_AttackGroup(nil, self.engageTarget:GetObject(), self.engageWeaponType, self.facDesignation, self.facDatalink, self.facFreq, self.facModu, CallsignName, CallsignNumber)

    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.FAC then

    -----------------
    -- FAC Mission --
    -----------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.PATROLZONE

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    param.zone=self:GetObjective()
    param.altitude=self.missionAltitude
    param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed) or nil

    DCStask.params=param

    table.insert(DCStasks, DCStask)

    -- Enroute task FAC
    local DCSenroute=CONTROLLABLE.EnRouteTaskFAC(self, self.facFreq, self.facModu)
    table.insert(self.enrouteTasks, DCSenroute)

  elseif self.type==AUFTRAG.Type.FERRY then

    -------------------
    -- FERRY Mission --
    -------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.FERRY

    -- We create a "fake" DCS task.
    local param={}
    DCStask.params=param

    table.insert(DCStasks, DCStask)
    
  elseif self.type==AUFTRAG.Type.RELOCATECOHORT then

    ----------------------
    -- RELOCATE Mission --
    ----------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.RELOCATECOHORT

    -- We create a "fake" DCS task.
    local param={}
    DCStask.params=param

    table.insert(DCStasks, DCStask)
     
  elseif self.type==AUFTRAG.Type.INTERCEPT then

    -----------------------
    -- INTERCEPT Mission --
    -----------------------

    self:_GetDCSAttackTask(self.engageTarget, DCStasks)

  elseif self.type==AUFTRAG.Type.ORBIT then

    -------------------
    -- ORBIT Mission --
    -------------------

    -- Done below as also other mission types use the orbit task.

  elseif self.type==AUFTRAG.Type.GCICAP then

    --------------------
    -- GCICAP Mission --
    --------------------

    -- Done below as also other mission types use the orbit task.

  elseif self.type==AUFTRAG.Type.RECON then

    -------------------
    -- RECON Mission --
    -------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.RECON

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    param.target=self.engageTarget
    param.altitude=self.missionAltitude
    param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed) or nil
    param.lastindex=nil

    DCStask.params=param

    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.SEAD then

    ------------------
    -- SEAD Mission --
    ------------------

    -- Add enroute task SEAD. Disabled that here because the group enganges everything on its route.
    --local DCStask=CONTROLLABLE.EnRouteTaskSEAD(nil, self.TargetType)
    --table.insert(self.enrouteTasks, DCStask)
    
    if self.engageZone then
    
      --local DCStask=CONTROLLABLE.EnRouteTaskSEAD(nil, self.engageTargetTypes)
      --table.insert(self.enrouteTasks, DCStask)
      self.engageZone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT})
      local ScanUnitSet = self.engageZone:GetScannedSetUnit()
      local SeadUnitSet = SET_UNIT:New()
      for _,_unit in pairs (ScanUnitSet.Set) do
        local unit = _unit -- Wrapper.Unit#UNTI
        if unit and unit:IsAlive() and unit:HasSEAD() then
          self:T("Adding UNIT for SEAD: "..unit:GetName())
          local task = CONTROLLABLE.TaskAttackUnit(nil,unit,GroupAttack,AI.Task.WeaponExpend.ALL,1,Direction,self.engageAltitude,2956984318)
          table.insert(DCStasks, task)
          SeadUnitSet:AddUnit(unit)
        end
      end
      self.engageTarget = TARGET:New(SeadUnitSet)
      --local OrbitTask = CONTROLLABLE.TaskOrbitCircle(nil,self.engageAltitude,self.missionSpeed,self.engageZone:GetCoordinate())
      --local Point = self.engageZone:GetVec2()
      --local OrbitTask = CONTROLLABLE.TaskOrbitCircleAtVec2(nil,Point,self.engageAltitude,self.missionSpeed)
      --table.insert(DCStasks, OrbitTask)
     
    else
    
      self:_GetDCSAttackTask(self.engageTarget, DCStasks)
    
    end
    
  elseif self.type==AUFTRAG.Type.STRIKE then

    --------------------
    -- STRIKE Mission --
    --------------------

    local coords = self.engageTarget:GetCoordinates()
    for _, coord in pairs(coords) do
        local DCStask=CONTROLLABLE.TaskAttackMapObject(nil, coord:GetVec2(), self.engageAsGroup, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType)

        table.insert(DCStasks, DCStask)
    end

  elseif self.type==AUFTRAG.Type.TANKER or self.type==AUFTRAG.Type.RECOVERYTANKER then

    --------------------
    -- TANKER Mission --
    --------------------

    local DCStask=CONTROLLABLE.EnRouteTaskTanker(nil)

    table.insert(self.enrouteTasks, DCStask)

  elseif self.type==AUFTRAG.Type.TROOPTRANSPORT then

    ----------------------------
    -- TROOPTRANSPORT Mission --
    ----------------------------

    -- Task to embark the troops at the pick up point.
    local TaskEmbark=CONTROLLABLE.TaskEmbarking(TaskControllable, self.transportPickup,  self.transportGroupSet, self.transportWaitForCargo)

    -- Task to disembark the troops at the drop off point.
    local TaskDisEmbark=CONTROLLABLE.TaskDisembarking(TaskControllable, self.transportDropoff, self.transportGroupSet)

    table.insert(DCStasks, TaskEmbark)
    table.insert(DCStasks, TaskDisEmbark)

  elseif self.type==AUFTRAG.Type.OPSTRANSPORT then

    --------------------------
    -- OPSTRANSPORT Mission --
    --------------------------

    local DCStask={}

    DCStask.id="OpsTransport"

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    DCStask.params=param

    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.CARGOTRANSPORT then

    ----------------------------
    -- CARGOTRANSPORT Mission --
    ----------------------------

    -- Task to transport cargo.
    local TaskCargoTransportation={
      id = "CargoTransportation",
      params = {}
    }
    
    table.insert(DCStasks, TaskCargoTransportation)

  elseif self.type==AUFTRAG.Type.RESCUEHELO then

    -------------------------
    -- RESCUE HELO Mission --
    -------------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.FORMATION

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    param.unitname=self:GetTargetName()
    param.offsetX=200
    param.offsetZ=240
    param.altitude=70
    param.dtFollow=1.0

    DCStask.params=param

    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.ARTY then

    ------------------
    -- ARTY Mission --
    ------------------

    
    if self.artyShots==1 or self.artyRadius<10 or true then
    
      local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, self:GetTargetVec2(), self.artyRadius, self.artyShots, self.engageWeaponType, self.artyAltitude)
      table.insert(DCStasks, DCStask)
    
    else
    
      local Vec2=self:GetTargetVec2()
      
      local zone=ZONE_RADIUS:New("temp", Vec2, self.artyRadius)
      
      for i=1,self.artyShots do
      
        local vec2=zone:GetRandomVec2()

        local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, vec2, 0, 1, self.engageWeaponType, self.artyAltitude)
        table.insert(DCStasks, DCStask)
  
      end
    
    end

  elseif self.type==AUFTRAG.Type.BARRAGE then

    ---------------------
    -- BARRAGE Mission --
    ---------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.BARRAGE

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    param.zone=self:GetObjective()
    param.altitude=self.artyAltitude
    param.radius=self.artyRadius
    param.heading=self.artyHeading
    param.angle=self.artyAngle
    param.shots=self.artyShots
    param.weaponTypoe=self.engageWeaponType

    DCStask.params=param

    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.PATROLZONE then

    -------------------------
    -- PATROL ZONE Mission --
    -------------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.PATROLZONE

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    param.zone=self:GetObjective()
    param.altitude=self.missionAltitude
    param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed) or nil

    DCStask.params=param

    table.insert(DCStasks, DCStask)
    
  elseif self.type==AUFTRAG.Type.CAPTUREZONE then

    --------------------------
    -- CAPTURE ZONE Mission --
    --------------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.CAPTUREZONE

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    DCStask.params=param

    table.insert(DCStasks, DCStask)    

  elseif self.type==AUFTRAG.Type.CASENHANCED then

    -------------------------
    -- CAS ENHANCED Mission --
    -------------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.PATROLZONE

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    param.zone=self:GetObjective()
    param.altitude=self.missionAltitude
    param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed) or nil

    DCStask.params=param

    table.insert(DCStasks, DCStask)

   elseif self.type==AUFTRAG.Type.GROUNDATTACK then

    ---------------------------
    -- GROUND ATTACK Mission --
    ---------------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.GROUNDATTACK

    -- We create a "fake" DCS task and pass the parameters to the ARMYGROUP.
    local param={}
    param.target=self:GetTargetData()
    param.action="Wedge"
    param.speed=self.missionSpeed and UTILS.KmphToMps(self.missionSpeed) or nil

    DCStask.params=param

    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.AMMOSUPPLY then

    -------------------------
    -- AMMO SUPPLY Mission --
    -------------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.AMMOSUPPLY

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    param.zone=self:GetObjective()

    DCStask.params=param

    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.FUELSUPPLY then

    -------------------------
    -- FUEL SUPPLY Mission --
    -------------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.FUELSUPPLY

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    param.zone=self:GetObjective()

    DCStask.params=param

    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.REARMING then

    ----------------------
    -- REARMING Mission --
    ----------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.REARMING

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    param.zone=self:GetObjective()

    DCStask.params=param

    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.ALERT5 then

    ---------------------
    -- ALERT 5 Mission --
    ---------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.ALERT5

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}

    DCStask.params=param

    table.insert(DCStasks, DCStask)
 
   elseif self.type==AUFTRAG.Type.NOTHING then

    ---------------------
    -- NOTHING Mission --
    ---------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.NOTHING

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}

    DCStask.params=param

    table.insert(DCStasks, DCStask)
       
  elseif self.type==AUFTRAG.Type.PATROLRACETRACK then

    ---------------------
    -- Enhanced Orbit Racetrack --
    ---------------------

    local DCStask={}
    DCStask.id=AUFTRAG.SpecialTask.PATROLRACETRACK

    local param={}
    -- ONTROLLABLE:PatrolRaceTrack(Point1, Point2, Altitude, Speed, Formation, Delay)
    
    param.TrackAltitude = self.TrackAltitude
    param.TrackSpeed = self.TrackSpeed
    param.TrackPoint1 = self.TrackPoint1
    param.TrackPoint2 = self.TrackPoint2
    param.missionSpeed = self.missionSpeed
    param.missionAltitude = self.missionAltitude
    param.TrackFormation = self.TrackFormation

    DCStask.params=param

    table.insert(DCStasks, DCStask)
    
  elseif self.type==AUFTRAG.Type.HOVER then

    ---------------------
    -- HOVER Mission --
    ---------------------

    local DCStask={}
    DCStask.id=AUFTRAG.SpecialTask.HOVER

    local param={}

    param.hoverAltitude=self.hoverAltitude
    param.hoverTime = self.hoverTime
    param.missionSpeed = self.missionSpeed
    param.missionAltitude = self.missionAltitude

    DCStask.params=param
  
    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.LANDATCOORDINATE then

    ---------------------
    -- LANDATCOORDINATE Mission
    ---------------------

    local DCStask={}
    local Vec2 = self.stayAt:GetVec2()  
    local DCStask = CONTROLLABLE.TaskLandAtVec2(nil,Vec2,self.stayTime, self.combatLand, self.directionAfter)
    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.ONGUARD or self.type==AUFTRAG.Type.ARMOREDGUARD then

    ----------------------
    -- ON GUARD Mission --
    ----------------------

    local DCStask={}

    DCStask.id= self.type==AUFTRAG.Type.ONGUARD and AUFTRAG.SpecialTask.ONGUARD or AUFTRAG.SpecialTask.ARMOREDGUARD

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    param.coordinate=self:GetObjective()

    DCStask.params=param

    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.AIRDEFENSE then

    ------------------------
    -- AIRDEFENSE Mission --
    ------------------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.AIRDEFENSE

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    param.zone=self:GetObjective()

    DCStask.params=param

    table.insert(DCStasks, DCStask)

  elseif self.type==AUFTRAG.Type.EWR then

    -----------------
    -- EWR Mission --
    -----------------

    local DCStask={}

    DCStask.id=AUFTRAG.SpecialTask.EWR

    -- We create a "fake" DCS task and pass the parameters to the OPSGROUP.
    local param={}
    param.zone=self:GetObjective()

    DCStask.params=param

    table.insert(DCStasks, DCStask)

    -- EWR is an enroute task
    local Enroutetask=CONTROLLABLE.EnRouteTaskEWR()
    table.insert(self.enrouteTasks, Enroutetask)

  else
    self:T(self.lid..string.format("ERROR: Unknown mission task!"))
    return nil
  end


  -- Set ORBIT task. Also applies to other missions: AWACS, TANKER, CAP, CAS.
  if self.type==AUFTRAG.Type.ORBIT  or
     self.type==AUFTRAG.Type.CAP    or
     self.type==AUFTRAG.Type.CAS    or
     self.type==AUFTRAG.Type.GCICAP or
     self.type==AUFTRAG.Type.AWACS  or
     self.type==AUFTRAG.Type.TANKER or
     self.type==AUFTRAG.Type.RECOVERYTANKER then

    -------------------
    -- ORBIT Mission --
    -------------------

    -- Get/update orbit vector.
    self.orbitVec2=self:GetTargetVec2()
    
    if self.orbitVec2 then
    
      -- Heading of the target.
      self.targetHeading=self:GetTargetHeading()

      local OffsetVec2=nil --DCS#Vec2
      if (self.orbitOffsetVec2~=nil) then
        OffsetVec2=UTILS.DeepCopy(self.orbitOffsetVec2)
      end
      
      if OffsetVec2 then
        
        if self.orbitOffsetVec2.r then
          -- Polar coordinates
          local r=self.orbitOffsetVec2.r
          local phi=(self.orbitOffsetVec2.phi or 0) + self.targetHeading
          
          OffsetVec2.x=r*math.cos(math.rad(phi))
          OffsetVec2.y=r*math.sin(math.rad(phi))
        else
          -- Cartesian coordinates
          OffsetVec2.x=self.orbitOffsetVec2.x
          OffsetVec2.y=self.orbitOffsetVec2.y
        end
        
      end
      
      -- Actual orbit position with possible offset.
      local orbitVec2=OffsetVec2 and UTILS.Vec2Add(self.orbitVec2, OffsetVec2) or self.orbitVec2      
      
      -- Check for race-track pattern.
      local orbitRaceTrack=nil --DCS#Vec2
      if self.orbitLeg then
      
        -- Default heading is due North. 
        local heading=0
        
        -- Check if specific heading was specified.
        if self.orbitHeading then
        
          -- Is heading realtive to target?
          if self.orbitHeadingRel then
            -- Relative heading wrt target.
            heading=self.targetHeading+self.orbitHeading
          else
            -- Take given heading.
            heading=self.orbitHeading
          end
          
        else
          -- Not specific heading specified ==> Take heading of target.
          heading=self.targetHeading or 0        
        end
                
        -- Race-track vector.
        orbitRaceTrack=UTILS.Vec2Translate(orbitVec2, self.orbitLeg, heading)
      end      
              
      local orbitRaceTrackCoord = nil    
      if orbitRaceTrack then
        orbitRaceTrackCoord = COORDINATE:NewFromVec2(orbitRaceTrack)
      end
      
      -- Create orbit task.
      local DCStask=CONTROLLABLE.TaskOrbit(nil, COORDINATE:NewFromVec2(orbitVec2), self.orbitAltitude, self.orbitSpeed, orbitRaceTrackCoord)
            
      -- Add DCS task.
      table.insert(DCStasks, DCStask)
      
    end

  end

  -- Debug info.
  self:T3({missiontask=DCStasks})

  -- Return the task.
  if #DCStasks==1 then
    return DCStasks[1]
  else
    return CONTROLLABLE.TaskCombo(nil, DCStasks)
  end

end

--- Get DCS task table for an attack group or unit task.
-- @param #AUFTRAG self
-- @param Ops.Target#TARGET Target Target data.
-- @param #table DCStasks DCS DCS tasks table to which the task is added.
-- @return DCS#Task The DCS task table.
function AUFTRAG:_GetDCSAttackTask(Target, DCStasks)

  DCStasks=DCStasks or {}

  for _,_target in pairs(Target.targets) do
    local target=_target --Ops.Target#TARGET.Object

    if target.Type==TARGET.ObjectType.GROUP then

      local DCStask=CONTROLLABLE.TaskAttackGroup(nil, target.Object, self.engageWeaponType, self.engageWeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageAsGroup)

      table.insert(DCStasks, DCStask)

    elseif target.Type==TARGET.ObjectType.UNIT or target.Type==TARGET.ObjectType.STATIC then

      local DCStask=CONTROLLABLE.TaskAttackUnit(nil, target.Object, self.engageAsGroup, self.WeaponExpend, self.engageQuantity, self.engageDirection, self.engageAltitude, self.engageWeaponType)

      table.insert(DCStasks, DCStask)

    end

  end

  return DCStasks
end

--- Get DCS task table for an attack group or unit task.
-- @param #AUFTRAG self
-- @param #string MissionType Mission (AUFTAG) type.
-- @return #string DCS mission task for the auftrag type.
function AUFTRAG:GetMissionTaskforMissionType(MissionType)

  local mtask=ENUMS.MissionTask.NOTHING

  if MissionType==AUFTRAG.Type.ANTISHIP then
    mtask=ENUMS.MissionTask.ANTISHIPSTRIKE
  elseif MissionType==AUFTRAG.Type.AWACS then
    mtask=ENUMS.MissionTask.AWACS
  elseif MissionType==AUFTRAG.Type.BAI then
    mtask=ENUMS.MissionTask.GROUNDATTACK
  elseif MissionType==AUFTRAG.Type.BOMBCARPET then
    mtask=ENUMS.MissionTask.GROUNDATTACK
  elseif MissionType==AUFTRAG.Type.BOMBING then
    mtask=ENUMS.MissionTask.GROUNDATTACK
  elseif MissionType==AUFTRAG.Type.BOMBRUNWAY then
    mtask=ENUMS.MissionTask.RUNWAYATTACK
  elseif MissionType==AUFTRAG.Type.CAP then
    mtask=ENUMS.MissionTask.CAP
  elseif MissionType==AUFTRAG.Type.GCICAP then
    mtask=ENUMS.MissionTask.CAP
  elseif MissionType==AUFTRAG.Type.CAS then
    mtask=ENUMS.MissionTask.CAS
  elseif MissionType==AUFTRAG.Type.PATROLZONE then
    mtask=ENUMS.MissionTask.CAS
  elseif MissionType==AUFTRAG.Type.CASENHANCED then
    mtask=ENUMS.MissionTask.CAS
  elseif MissionType==AUFTRAG.Type.ESCORT then
    mtask=ENUMS.MissionTask.ESCORT
  elseif MissionType==AUFTRAG.Type.FACA then
    mtask=ENUMS.MissionTask.AFAC
  elseif MissionType==AUFTRAG.Type.FAC then
    mtask=ENUMS.MissionTask.AFAC    
  elseif MissionType==AUFTRAG.Type.FERRY then
    mtask=ENUMS.MissionTask.NOTHING
  elseif MissionType==AUFTRAG.Type.GROUNDESCORT then
    mtask=ENUMS.MissionTask.GROUNDESCORT
  elseif MissionType==AUFTRAG.Type.INTERCEPT then
    mtask=ENUMS.MissionTask.INTERCEPT
  elseif MissionType==AUFTRAG.Type.RECON then
    mtask=ENUMS.MissionTask.RECONNAISSANCE
  elseif MissionType==AUFTRAG.Type.SEAD then
    mtask=ENUMS.MissionTask.SEAD
  elseif MissionType==AUFTRAG.Type.STRIKE then
    mtask=ENUMS.MissionTask.GROUNDATTACK
  elseif MissionType==AUFTRAG.Type.TANKER then
    mtask=ENUMS.MissionTask.REFUELING
  elseif MissionType==AUFTRAG.Type.TROOPTRANSPORT then
    mtask=ENUMS.MissionTask.TRANSPORT
  elseif MissionType==AUFTRAG.Type.CARGOTRANSPORT then
    mtask=ENUMS.MissionTask.TRANSPORT
  elseif MissionType==AUFTRAG.Type.ARMORATTACK then
    mtask=ENUMS.MissionTask.NOTHING
  elseif MissionType==AUFTRAG.Type.HOVER then
    mtask=ENUMS.MissionTask.NOTHING
  elseif MissionType==AUFTRAG.Type.PATROLRACETRACK then
    mtask=ENUMS.MissionTask.CAP
  end

  return mtask
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Global Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Checks if a mission type is contained in a table of possible types.
-- @param #string MissionType The requested mission type.
-- @param #table PossibleTypes A table with possible mission types.
-- @return #boolean If true, the requested mission type is part of the possible mission types.
function AUFTRAG.CheckMissionType(MissionType, PossibleTypes)

  if type(PossibleTypes)=="string" then
    PossibleTypes={PossibleTypes}
  end

  for _,canmission in pairs(PossibleTypes) do
    if canmission==MissionType then
      return true
    end
  end

  return false
end

--- Check if a mission type is contained in a list of possible capabilities.
-- @param #table MissionTypes The requested mission type. Can also be passed as a single mission type `#string`.
-- @param #table Capabilities A table with possible capabilities `Ops.Auftrag#AUFTRAG.Capability`.
-- @param #boolean All If `true`, given mission type must be includedin ALL capabilities. If `false` or `nil`, it must only match one.
-- @return #boolean If true, the requested mission type is part of the possible mission types.
function AUFTRAG.CheckMissionCapability(MissionTypes, Capabilities, All)

  -- Ensure table.
  if type(MissionTypes)~="table" then
    MissionTypes={MissionTypes}
  end

  for _,cap in pairs(Capabilities) do
    local capability=cap --Ops.Auftrag#AUFTRAG.Capability
    for _,MissionType in pairs(MissionTypes) do
      if All==true then
        if capability.MissionType~=MissionType then
          return false
        end
      else
        if capability.MissionType==MissionType then
          return true
        end
      end
    end
  end

  if All==true then
    return true
  else
    return false
  end
end


--- Check if a mission type is contained in a list of possible capabilities.
-- @param #table MissionTypes The requested mission type. Can also be passed as a single mission type `#string`.
-- @param #table Capabilities A table with possible capabilities `Ops.Auftrag#AUFTRAG.Capability`.
-- @return #boolean If true, the requested mission type is part of the possible mission types.
function AUFTRAG.CheckMissionCapabilityAny(MissionTypes, Capabilities)

  local res=AUFTRAG.CheckMissionCapability(MissionTypes, Capabilities, false)

  return res
end


--- Check if a mission type is contained in a list of possible capabilities.
-- @param #table MissionTypes The requested mission type. Can also be passed as a single mission type `#string`.
-- @param #table Capabilities A table with possible capabilities `Ops.Auftrag#AUFTRAG.Capability`.
-- @return #boolean If true, the requested mission type is part of the possible mission types.
function AUFTRAG.CheckMissionCapabilityAll(MissionTypes, Capabilities)

  local res=AUFTRAG.CheckMissionCapability(MissionTypes, Capabilities, true)

  return res
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - MOOSE AI AWACS Operations using text-to-speech.
-- 
-- ===
-- 
-- **AWACS** - MOOSE AI AWACS Operations using text-to-speech.
-- 
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/Awacs/).
--
-- ## Videos:
-- 
-- Demo videos can be found on [Youtube](https://www.youtube.com/watch?v=ocdy8QzTNN4&list=PLFxp425SeXnq-oS0DSjam1HtddywH8i_k)
--    
-- ===
--
-- ### Author: **applevangelist**
-- @date Last Update July 2025
-- @module Ops.AWACS
-- @image OPS_AWACS.jpg

do
--- Ops AWACS Class
-- @type AWACS
-- @field #string ClassName Name of this class.
-- @field #string version Versioning.
-- @field #string lid LID for log entries.
-- @field #number coalition Coalition side.
-- @field #string coalitiontxt e.g."blue"
-- @field Core.Zone#ZONE OpsZone,
-- @field Core.Zone#ZONE StationZone,
-- @field Core.Zone#ZONE BorderZone,
-- @field Core.Zone#ZONE RejectZone,
-- @field #number Frequency
-- @field #number Modulation
-- @field Wrapper.Airbase#AIRBASE Airbase
-- @field Ops.Airwing#AIRWING AirWing
-- @field #number AwacsAngels
-- @field Core.Zone#ZONE OrbitZone
-- @field #number CallSign
-- @field #number CallSignNo
-- @field #boolean debug
-- @field #number verbose
-- @field #table ManagedGrps
-- @field #number ManagedGrpID
-- @field #number ManagedTaskID
-- @field Utilities.FiFo#FIFO AnchorStacks
-- @field Utilities.FiFo#FIFO CAPIdleAI
-- @field Utilities.FiFo#FIFO CAPIdleHuman
-- @field Utilities.FiFo#FIFO TaskedCAPAI
-- @field Utilities.FiFo#FIFO TaskedCAPHuman
-- @field Utilities.FiFo#FIFO OpenTasks
-- @field Utilities.FiFo#FIFO ManagedTasks
-- @field Utilities.FiFo#FIFO PictureAO
-- @field Utilities.FiFo#FIFO PictureEWR
-- @field Utilities.FiFo#FIFO Contacts
-- @field #table CatchAllMissions
-- @field #table CatchAllFGs
-- @field #number Countactcounter
-- @field Utilities.FiFo#FIFO ContactsAO
-- @field Utilities.FiFo#FIFO RadioQueue
-- @field Utilities.FiFo#FIFO PrioRadioQueue
-- @field Utilities.FiFo#FIFO CAPAirwings
-- @field Utilities.FiFo#FIFO TacticalQueue
-- @field #number AwacsTimeOnStation
-- @field #number AwacsTimeStamp
-- @field #number EscortsTimeOnStation
-- @field #number EscortsTimeStamp
-- @field #string AwacsROE
-- @field #string AwacsROT
-- @field Ops.Auftrag#AUFTRAG AwacsMission
-- @field Ops.Auftrag#AUFTRAG EscortMission
-- @field Ops.Auftrag#AUFTRAG AwacsMissionReplacement
-- @field Ops.Auftrag#AUFTRAG EscortMissionReplacement
-- @field Utilities.FiFo#FIFO AICAPMissions FIFO for Ops.Auftrag#AUFTRAG for AI CAP
-- @field #boolean MenuStrict
-- @field #number MaxAIonCAP
-- @field #number AIonCAP
-- @field #boolean ShiftChangeAwacsFlag
-- @field #boolean ShiftChangeEscortsFlag
-- @field #boolean ShiftChangeAwacsRequested
-- @field #boolean ShiftChangeEscortsRequested
-- @field #AWACS.MonitoringData MonitoringData
-- @field #boolean MonitoringOn
-- @field Core.Set#SET_CLIENT clientset
-- @field Utilities.FiFo#FIFO FlightGroups
-- @field #number PictureInterval Interval in seconds for general picture
-- @field #number PictureTimeStamp Interval timestamp
-- @field #number maxassigndistance Only assing AI/Pilots to targets max this far away
-- @field #boolean PlayerGuidance if true additional callouts to guide/warn players
-- @field #boolean ModernEra if true we get more intel on targets, and EPLR on the AIC
-- @field #boolean callsignshort if true use short (group) callsigns, e.g. "Ghost 1", else "Ghost 1 1"
-- @field #boolean keepnumber if true, use the full string after # for a player custom callsign
-- @field #table callsignTranslations optional translations for callsigns
-- @field #number MeldDistance 25nm - distance for "Meld" Call , usually shortly before the actual engagement 
-- @field #number TacDistance 30nm - distance for "TAC" Call
-- @field #number ThreatDistance 15nm - distance to declare untargeted (new) threats
-- @field #string AOName name of the FEZ, e.g. Rock
-- @field Core.Point#COORDINATE AOCoordinate Coordinate of bulls eye
-- @field Utilities.FiFo#FIFO clientmenus
-- @field #number RadarBlur Radar blur in %
-- @field #number ReassignmentPause Wait this many seconds before re-assignment of a player
-- @field #boolean NoGroupTags Set to true if you don't want group tags.
-- @field #boolean SuppressScreenOutput Set to true to suppress all screen output.
-- @field #boolean NoMissileCalls Suppress missile callouts
-- @field #boolean PlayerCapAssignment Assign players to CAP tasks when they are logged on
-- @field #number GoogleTTSPadding
-- @field #number WindowsTTSPadding
-- @field #boolean AllowMarkers
-- @field #string PlayerStationName
-- @field #boolean GCI Act as GCI
-- @field Wrapper.Group#GROUP GCIGroup EWR group object for GCI ops
-- @field #string locale Localization
-- @field #boolean IncludeHelicopters
-- @field #boolean TacticalMenu
-- @field #table TacticalFrequencies
-- @field #table TacticalSubscribers
-- @field #number TacticalBaseFreq
-- @field #number TacticalIncrFreq
-- @field #number TacticalModulation
-- @field #number TacticalInterval
-- @field Core.Set#SET_GROUP DetectionSet
-- @field #number MaxMissionRange
-- @extends Core.Fsm#FSM


---
--
-- *Of all men\'s miseries the bitterest is this: to know so much and to have control over nothing.* (Herodotus)
--
-- ===
-- 
-- # AWACS AI Air Controller
-- 
--  * WIP (beta)
--  * AWACS replacement for the in-game AWACS
--  * Will control a fighter engagement zone and assign tasks to AI and human CAP flights
--  * Callouts referenced from:   
--  ** References from ARN33396 ATP 3-52.4 (Sep 2021) (Combined Forces)   
--  ** References from CNATRA P-877 (Rev 12-20) (NAVY)   
--  * FSM events that the mission designer can hook into
--  * Can also be used as GCI Controller
-- 
-- ## 0 Note for Multiplayer Setup
-- 
-- Due to DCS limitations you need to set up a second, "normal" AWACS plane in multi-player/server environments to keep the EPLRS/DataLink going in these environments.
-- Though working in single player, the situational awareness screens of the e.g. F14/16/18 will else not receive datalink targets.
-- 
-- ## 1 Prerequisites
-- 
-- The radio callouts in this class are ***exclusively*** created with Text-To-Speech (TTS), based on the Moose @{Sound.SRS} Class, and output is via [Ciribob's SRS system](https://github.com/ciribob/DCS-SimpleRadioStandalone/releases)
-- Ensure you have this covered and working before tackling this class. TTS generation can thus be done via the Windows built-in system or via Google TTS; 
-- the latter offers a wider range of voices and options, but you need to set up your own Google product account for this to work correctly.
-- 
-- ## 2 Mission Design - Operational Priorities
-- 
-- Basic operational target of the AWACS is to control a Fighter Engagement Zone, or FEZ, and defend itself.
-- 
-- ## 3 Airwing(s)
-- 
-- The AWACS plane, the optional escort planes, and the AI CAP planes work based on the @{Ops.Airwing} class. Read and understand the manual for this class in 
-- order to set everything up correctly. You will at least need one Squadron containing the AWACS plane itself.
-- 
-- Set up the Airwing
-- 
--            local AwacsAW = AIRWING:New("AirForce WH-1","AirForce One")
--            AwacsAW:SetMarker(false)
--            AwacsAW:SetAirbase(AIRBASE:FindByName(AIRBASE.Caucasus.Kutaisi))
--            AwacsAW:SetRespawnAfterDestroyed(900)
--            AwacsAW:SetTakeoffAir()
--            AwacsAW:__Start(2)
-- 
-- Add the AWACS template Squadron - **Note**: remove the task AWACS in the mission editor under "Advanced Waypoint Actions" from the template to remove the DCS F10 AWACS menu
-- 
--            local Squad_One = SQUADRON:New("Awacs One",2,"Awacs North")
--            Squad_One:AddMissionCapability({AUFTRAG.Type.ORBIT},100)
--            Squad_One:SetFuelLowRefuel(true)
--            Squad_One:SetFuelLowThreshold(0.2)
--            Squad_One:SetTurnoverTime(10,20)
--            AwacsAW:AddSquadron(Squad_One)
--            AwacsAW:NewPayload("Awacs One One",-1,{AUFTRAG.Type.ORBIT},100)
--            
-- Add Escorts Squad (recommended, optional)
-- 
--            local Squad_Two = SQUADRON:New("Escorts",4,"Escorts North") -- taking a template with 2 planes here, will result in a group of 2 escorts which can fly in formation escorting the AWACS.
--            Squad_Two:AddMissionCapability({AUFTRAG.Type.ESCORT})
--            Squad_Two:SetFuelLowRefuel(true)
--            Squad_Two:SetFuelLowThreshold(0.3)
--            Squad_Two:SetTurnoverTime(10,20)
--            Squad_Two:SetTakeoffAir()
--            Squad_Two:SetRadio(255,radio.modulation.AM)
--            AwacsAW:AddSquadron(Squad_Two)
--            AwacsAW:NewPayload("Escorts",-1,{AUFTRAG.Type.ESCORT},100)
--            
-- Add CAP Squad (recommended, optional)
-- 
--            local Squad_Three = SQUADRON:New("CAP",10,"CAP North")
--            Squad_Three:AddMissionCapability({AUFTRAG.Type.ALERT5, AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT},80)
--            Squad_Three:SetFuelLowRefuel(true)
--            Squad_Three:SetFuelLowThreshold(0.3)
--            Squad_Three:SetTurnoverTime(10,20)
--            Squad_Three:SetTakeoffAir()
--            Squad_Two:SetRadio(255,radio.modulation.AM)
--            AwacsAW:AddSquadron(Squad_Three)
--            AwacsAW:NewPayload("Aerial-1-2",-1,{AUFTRAG.Type.ALERT5,AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT},100)
-- 
-- ## 4 Zones
-- 
-- For the setup, you need to set up a couple of zones:
-- 
-- * An Orbit Zone, where your AWACS will orbit
-- * A Fighter Engagement Zone or FEZ
-- * A zone where your CAP flights will be stationed, waiting for assignments
-- * Optionally, an additional zone you wish to defend
-- * Optionally, a border of the opposing party
-- * Also, and move your BullsEye in the mission accordingly - this will be the key reference point for most AWACS callouts
-- 
-- ### 4.1 Strategic considerations
-- 
-- Your AWACS is an HVT or high-value-target. Thus it makes sense to position the Orbit Zone in a way that your FEZ and thus your CAP flights defend it. 
-- It should hence be positioned behind the FEZ, away from the direction of enemy engagement.
-- The zone for CAP stations should be close to the FEZ, but not inside it.
-- The optional additional defense zone can be anywhere, but keep an eye on the location so your CAP flights don't take ages to get there. 
-- The optional border is useful for e.g. "cold war" scenarios - planes across the border will not be considered as targets by AWACS.
-- 
-- ## 5 Set up AWACS
-- 
--            -- Set up AWACS called "AWACS North". It will use the AwacsAW Airwing set up above and be of the "blue" coalition. Homebase is Kutaisi.
--            -- The AWACS Orbit Zone is a round zone set in the mission editor named "Awacs Orbit", the FEZ is a Polygon-Zone called "Rock" we have also
--            -- set up in the mission editor with a late activated helo named "Rock#ZONE_POLYGON". Note this also sets the BullsEye to be referenced as "Rock".
--            -- The CAP station zone is called "Fremont". We will be on 255 AM.
--            local testawacs = AWACS:New("AWACS North",AwacsAW,"blue",AIRBASE.Caucasus.Kutaisi,"Awacs Orbit",ZONE:FindByName("Rock"),"Fremont",255,radio.modulation.AM )
--            -- set one escort group; this example has two units in the template group, so they can fly a nice formation.
--            testawacs:SetEscort(1,ENUMS.Formation.FixedWing.FingerFour.Group,{x=-500,y=50,z=500},45)
--            -- Callsign will be "Focus". We'll be a Angels 30, doing 300 knots, orbit leg to 88deg with a length of 25nm.
--            testawacs:SetAwacsDetails(CALLSIGN.AWACS.Focus,1,30,300,88,25)
--            -- Set up SRS on port 5010 - change the below to your path and port
--            testawacs:SetSRS("C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio","female","en-GB",5010)
--            -- Add a "red" border we don't want to cross, set up in the mission editor with a late activated helo named "Red Border#ZONE_POLYGON"
--            testawacs:SetRejectionZone(ZONE:FindByName("Red Border"))
--            -- Our CAP flight will have the callsign "Ford", we want 4 AI planes, Time-On-Station is four hours, doing 300 kn IAS.
--            testawacs:SetAICAPDetails(CALLSIGN.Aircraft.Ford,4,4,300)
--            -- We're modern (default), e.g. we have EPLRS and get more fill-in information on detections
--            testawacs:SetModernEra()
--            -- And start
--            testawacs:__Start(5)
--            
-- ### 5.1 Alternative - Set up as GCI (no AWACS plane needed) Theater Air Control System (TACS)
-- 
--            -- Set up as TACS called "GCI Senaki". It will use the AwacsAW Airwing set up above and be of the "blue" coalition. Homebase is Senaki.
--            -- No need to set the AWACS Orbit Zone; the FEZ is still a Polygon-Zone called "Rock" we have also
--            -- set up in the mission editor with a late activated helo named "Rock#ZONE_POLYGON". Note this also sets the BullsEye to be referenced as "Rock".
--            -- The CAP station zone is called "Fremont". We will be on 255 AM. Note the Orbit Zone is given as *nil* in the `New()`-Statement
--            local testawacs = AWACS:New("GCI Senaki",AwacsAW,"blue",AIRBASE.Caucasus.Senaki_Kolkhi,nil,ZONE:FindByName("Rock"),"Fremont",255,radio.modulation.AM )
--            -- Set up SRS on port 5010 - change the below to your path and port
--            testawacs:SetSRS("C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio","female","en-GB",5010)
--            -- Add a "red" border we don't want to cross, set up in the mission editor with a late activated helo named "Red Border#ZONE_POLYGON"
--            testawacs:SetRejectionZone(ZONE:FindByName("Red Border"))
--            -- Our CAP flight will have the callsign "Ford", we want 4 AI planes, Time-On-Station is four hours, doing 300 kn IAS.
--            testawacs:SetAICAPDetails(CALLSIGN.Aircraft.Ford,4,4,300)
--            -- We're modern (default), e.g. we have EPLRS and get more fill-in information on detections
--            testawacs:SetModernEra()
--            -- Give it a fancy callsign
--            testawacs:SetAwacsDetails(CALLSIGN.AWACS.Wizard)
--            -- And start as GCI using a group name "Blue EWR" as main EWR station
--            testawacs:SetAsGCI(GROUP:FindByName("Blue EWR"),2)
--            -- Set Custom CAP Flight Callsigns for use with TTS
--            testawacs:SetCustomCallsigns({
--              Devil = 'Bengal',
--              Snake = 'Winder',
--              Colt = 'Camelot',
--              Enfield = 'Victory',
--              Uzi = 'Evil Eye'
--            })
--            testawacs:__Start(4)
--            
-- ## 6 Menu entries
-- 
-- **Note on Radio Menu entries**: Due to a DCS limitation, these are on GROUP level and not individual (UNIT level). Hence, either put each player in his/her own group,
-- or ensure that only the flight lead will use the menu. Recommend the 1st option, unless you have a disciplined team.
-- 
-- ### 6.1 Check-in
-- 
-- In the base setup, you need to check in to the AWACS to get the full menu. This can be done once the AWACS is airborne. You will get an Alpha Check callout 
-- and be assigned a CAP station.
-- 
-- ### 6.2 Check-out
-- 
-- You can check-out anytime, of course.
-- 
-- ### 6.3 Picture
-- 
-- Get a picture from the AWACS. It will call out the three most important groups. References are **always** to the (named) BullsEye position.
-- **Note** that AWACS will anyway do a regular picture call to all stations every five minutes.
-- 
-- ### 6.4 Bogey Dope
-- 
-- Get bogey dope from the AWACS. It will call out the closest bogey group, if any. Reference is BRAA to the Player position.
-- 
-- ### 6.5 Declare
-- 
-- AWACS will declare, if the bogey closest to the calling player in a 3nm circle is hostile, friendly or neutral.
-- 
-- ### 6.6 Tasking
-- 
-- Tasking will show you the current task with "Showtask". Updated directions are shown, also.
-- You can decline a **requested** task with "unable", and abort **any task but CAP station** with "abort".
-- You can "commit" to a requested task within 3 minutes.
-- "VID" - if AWACS is set to Visial ID or VID oncoming planes first, there will also be an "VID" entry. Similar to "Declare" you can declare the requested contact 
-- to be hostile, friendly or neutral if you are close enough to it (3nm). If hostile, at the time of writing, an engagement task will be assigned to you (not: requested).
-- If neutral/friendly, contact will be excluded from further tasking.
-- 
-- ## 7 Air-to-Air Timeline Support
-- 
-- To support your engagement timeline, AWACS will make Tac-Range, Meld, Merge and Threat call-outs to the player/group (Figure 7-3, CNATRA P-877). Default settings in NM are 
-- 
--            Tac Distance = 45
--            Meld Distance = 35
--            Threat Distance = 25
--            Merge Distance = 5 
-- 
-- ## 8 Bespoke Player CallSigns
-- 
-- Append the GROUP name of your client slots with "#CallSign" to use bespoke callsigns in AWACS callouts. E.g. "Player F14#Ghostrider" will be refered to 
-- as "Ghostrider" plus group number, e.g. "Ghostrider 9". Alternatively, if you have set up your Player name in the "Logbook" in the mission editor main screen
-- as e.g. "Pikes | Goose", you will be addressed as "Goose" by the AWACS callouts.
--
-- ## 9 Options
-- 
-- There's a number of functions available, to set various options for the setup.
-- 
-- * @{#AWACS.SetBullsEyeAlias}() : Set the alias name of the Bulls Eye.
-- * @{#AWACS.SetTOS}() : Set time on station for AWACS and CAP.
-- * @{#AWACS.SetReassignmentPause}() : Pause this number of seconds before re-assigning a Player to a task.
-- * @{#AWACS.SuppressScreenMessages}() : Suppress message output on screen.
-- * @{#AWACS.SetRadarBlur}() : Set the radar blur faktor in percent.
-- * @{#AWACS.SetColdWar}() : Set to cold war - no fill-ins, no EPLRS, VID as standard.
-- * @{#AWACS.SetModernEraDefensive}() : Set to modern, EPLRS, BVR/IFF engagement, fill-ins.
-- * @{#AWACS.SetModernEraAggressive}() : Set to modern, EPLRS, BVR/IFF engagement, fill-ins.
-- * @{#AWACS.SetPolicingModern}() : Set to modern, EPLRS, VID engagement, fill-ins.
-- * @{#AWACS.SetPolicingColdWar}() : Set to cold war, no EPLRS, VID engagement, no fill-ins.
-- * @{#AWACS.SetInterceptTimeline}() : Set distances for TAC, Meld and Threat range calls.
-- * @{#AWACS.SetAdditionalZone}() : Add one additional defense zone, e.g. own border.
-- * @{#AWACS.SetRejectionZone}() : Add one foreign border. Targets beyond will be ignored for tasking.
-- * @{#AWACS.DrawFEZ}() : Show the FEZ on the F10 map.
-- * @{#AWACS.SetAWACSDetails}() : Set AWACS details.
-- * @{#AWACS.AddGroupToDetection}() : Add a GROUP or SET_GROUP object to INTEL detection, e.g. EWR.
-- * @{#AWACS.SetSRS}() : Set SRS details.
-- * @{#AWACS.SetSRSVoiceCAP}() : Set voice details for AI CAP planes, using Windows dektop TTS.
-- * @{#AWACS.SetAICAPDetails}() : Set AI CAP details.
-- * @{#AWACS.SetEscort}() : Set number of escorting planes for AWACS.
-- * @{#AWACS.AddCAPAirWing}() : Add an additional @{Ops.Airwing#AIRWING} for CAP flights.
-- * @{#AWACS.ZipLip}() : Do not show messages on screen, no extra calls for player guidance, use short callsigns, no group tags.
-- * @{#AWACS.AddFrequencyAndModulation}() : Add additional frequencies with modulation which will receive AWACS SRS messages.
-- 
-- ## 9.1 Single Options
-- 
-- Further single options (set before starting your AWACS instance, but after `:New()`)
-- 
--            testawacs.PlayerGuidance = true -- allow missile warning call-outs.
--            testawacs.NoGroupTags = false -- use group tags like Alpha, Bravo .. etc in call outs.
--            testawacs.callsignshort = true -- use short callsigns, e.g. "Moose 1", not "Moose 1-1".
--            testawacs.DeclareRadius = 5 -- you need to be this close to the lead unit for declare/VID to work, in NM.
--            testawacs.MenuStrict = true -- Players need to check-in to see the menu; check-in still require to use the menu.
--            testawacs.maxassigndistance = 100 -- Don't assign targets further out than this, in NM.
--            testawacs.debug = false -- set to true to produce more log output.
--            testawacs.NoMissileCalls = true -- suppress missile callouts
--            testawacs.PlayerCapAssignment = true -- no intercept task assignments for players
--            testawacs.invisible = false -- set AWACS to be invisible to hostiles
--            testawacs.immortal = false -- set AWACS to be immortal
--            -- By default, the radio queue is checked every 10 secs. This is altered by the calculated length of the sentence to speak
--            -- over the radio. Google and Windows speech speed is different. Use the below to fine-tune the setup in case of overlapping
--            -- messages or too long pauses
--            testawacs.GoogleTTSPadding = 1 -- seconds
--            testawacs.WindowsTTSPadding = 2.5 -- seconds
--            testawacs.PikesSpecialSwitch = false -- if set to true, AWACS will omit the "doing xy knots" on the station assignement callout
--            testawacs.IncludeHelicopters = false -- if set to true, Helicopter pilots will also get the AWACS Menu and options
-- 
-- ## 9.2 Bespoke random voices for AI CAP (Google TTS only)
-- 
-- Currently there are 10 voices defined which are randomly assigned to the AI CAP flights:
-- 
-- Defaults are:
-- 
--          testawacs.CapVoices = {
--            [1] = "de-DE-Wavenet-A",
--            [2] = "de-DE-Wavenet-B",
--            [3] = "fr-FR-Wavenet-A",
--            [4] = "fr-FR-Wavenet-B",
--            [5] = "en-GB-Wavenet-A",
--            [6] = "en-GB-Wavenet-B",
--            [7] = "en-GB-Wavenet-D",
--            [8] = "en-AU-Wavenet-B",
--            [9] = "en-US-Wavenet-J",
--            [10] = "en-US-Wavenet-H",
--           }
-- 
-- ## 10 Using F10 map markers to create new player station points
-- 
-- You can use F10 map markers to create new station points for human CAP flights. The latest created station will take priority for (new) station assignments for humans.
-- Enable this option with
-- 
--            testawacs.AllowMarkers = true
--            
-- Set a marker on the map and add the following text to create a station: "AWACS Station London" - "AWACS Station" are the necessary keywords, "London" 
-- in this example will be the name of the new station point. The user marker can then be deleted, an info marker point at the same place will remain.
-- You can delete a player station point the same way: "AWACS Delete London"; note this will only work if currently there are no assigned flights on this station. 
-- Lastly, you can move the station around with keyword "Move": "AWACS Move London".
-- 
-- ## 11 Localization
-- 
-- Localization for English text is build-in. Default setting is English. Change with @{#AWACS.SetLocale}()
-- 
-- ### 11.1 Adding Localization
-- 
-- A list of fields to be defined follows below. **Note** that in some cases `string.format()` is used to format texts for screen and SRS. 
-- Hence, the `%d`, `%s` and `%f` special characters need to appear in the exact same amount and order of appearance in the localized text or it will create errors.
-- To add a localization, the following texts need to be translated and set in your mission script **before** @{#AWACS.Start}():   
-- 
--      AWACS.Messages = {
--        EN =
--          {
--          DEFEND = "%s, %s! %s! %s! Defend!",
--          VECTORTO = "%s, %s. Vector%s %s",
--          VECTORTOTTS = "%s, %s, Vector%s %s",
--          ANGELS = ". Angels ",
--          ZERO = "zero",
--          VANISHED = "%s, %s Group. Vanished.",
--          VANISHEDTTS = "%s, %s group vanished.",
--          SHIFTCHANGE = "%s shift change for %s control.",
--          GROUPCAP = "Group",
--          GROUP = "group",
--          MILES = "miles",
--          THOUSAND = "thousand",
--          BOGEY = "Bogey",
--          ALLSTATIONS = "All Stations",
--          PICCLEAN = "%s. %s. Picture Clean.",
--          PICTURE = "Picture",
--          ONE = "One",
--          GROUPMULTI = "groups",
--          NOTCHECKEDIN = "%s. %s. Negative. You are not checked in.",
--          CLEAN = "%s. %s. Clean.",
--          DOPE = "%s. %s. Bogey Dope. ",
--          VIDPOS = "%s. %s. Copy, target identified as %s.",
--          VIDNEG = "%s. %s. Negative, get closer to target.",
--          FFNEUTRAL = "Neutral",
--          FFFRIEND = "Friendly",
--          FFHOSTILE = "Hostile",
--          FFSPADES = "Spades",
--          FFCLEAN = "Clean",
--          COPY = "%s. %s. Copy.",
--          TARGETEDBY = "Targeted by %s.",
--          STATUS = "Status",
--          ALREADYCHECKEDIN = "%s. %s. Negative. You are already checked in.",
--          ALPHACHECK = "Alpha Check",
--          CHECKINAI = "%s. %s. Checking in as fragged. Expected playtime %d hours. Request Alpha Check %s.",
--          SAFEFLIGHT = "%s. %s. Copy. Have a safe flight home.",
--          VERYLOW = "very low",
--          AIONSTATION = "%s. %s. On station over anchor %d at angels  %d. Ready for tasking.",
--          POPUP = "Pop-up",
--          NEWGROUP = "New group",
--          HIGH= " High.",
--          VERYFAST = " Very fast.",
--          FAST = " Fast.",
--          THREAT = "Threat",
--          MERGED = "Merged",
--          SCREENVID = "Intercept and VID %s group.",
--          SCREENINTER = "Intercept %s group.",
--          ENGAGETAG = "Targeted by %s.",
--          REQCOMMIT = "%s. %s group. %s. %s, request commit.",
--          AICOMMIT = "%s. %s group. %s. %s, commit.",
--          COMMIT = "Commit",
--          SUNRISE = "%s. All stations, SUNRISE SUNRISE SUNRISE, %s.",
--          AWONSTATION = "%s on station for %s control.",
--          STATIONAT = "%s. %s. Station at %s at angels %d.",
--          STATIONATLONG = "%s. %s. Station at %s at angels %d doing %d knots.",
--          STATIONSCREEN = "%s. %s.\nStation at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s.",
--          STATIONTASK = "Station at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s",
--          VECTORSTATION = " to Station",
--          TEXTOPTIONS1 = "Lost friendly flight",
--          TEXTOPTIONS2 =  "Vanished friendly flight",
--          TEXTOPTIONS3 =  "Faded friendly contact",
--          TEXTOPTIONS4 =  "Lost contact with",
--          },
--         } 
-- 
-- e.g.
-- 
--            testawacs.Messages = {
--              DE = {
--                ...
--                FFNEUTRAL = "Neutral",
--                FFFRIEND = "Freund",
--                FFHOSTILE = "Feind",
--                FFSPADES = "Uneindeutig",
--                FFCLEAN = "Sauber",
--                ...
--              },
--             
-- ## 12 Discussion
--
-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-awacs channel.
-- 
-- 
-- 
-- 
-- @field #AWACS
AWACS = {
  ClassName = "AWACS", -- #string
  version = "0.2.73", -- #string
  lid = "", -- #string
  coalition = coalition.side.BLUE, -- #number
  coalitiontxt = "blue", -- #string
  OpsZone = nil,
  StationZone = nil,
  AirWing = nil,
  Frequency = 271, -- #number
  Modulation = radio.modulation.AM, -- #number
  Airbase = nil,
  AwacsAngels = 25, -- orbit at 25'000 ft
  OrbitZone = nil,
  CallSign = CALLSIGN.AWACS.Magic, -- #number
  CallSignNo = 1, -- #number
  debug = false,
  verbose = false,
  ManagedGrps = {},
  ManagedGrpID = 0, -- #number
  ManagedTaskID = 0, -- #number
  AnchorStacks = {}, -- Utilities.FiFo#FIFO
  CAPIdleAI = {},
  CAPIdleHuman = {},
  TaskedCAPAI = {},
  TaskedCAPHuman = {},
  OpenTasks = {}, -- Utilities.FiFo#FIFO
  ManagedTasks = {}, -- Utilities.FiFo#FIFO
  PictureAO = {}, -- Utilities.FiFo#FIFO
  PictureEWR = {}, -- Utilities.FiFo#FIFO
  Contacts = {}, -- Utilities.FiFo#FIFO
  Countactcounter = 0,
  ContactsAO = {}, -- Utilities.FiFo#FIFO
  RadioQueue = {}, -- Utilities.FiFo#FIFO
  PrioRadioQueue = {}, -- Utilities.FiFo#FIFO
  TacticalQueue = {}, -- Utilities.FiFo#FIFO
  AwacsTimeOnStation = 4,
  AwacsTimeStamp = 0,
  EscortsTimeOnStation = 4,
  EscortsTimeStamp = 0,
  CAPTimeOnStation = 4,
  AwacsROE = "",
  AwacsROT = "",
  MenuStrict = true,
  MaxAIonCAP = 3,
  AIonCAP = 0,
  AICAPMissions = {}, -- Utilities.FiFo#FIFO
  ShiftChangeAwacsFlag = false,
  ShiftChangeEscortsFlag = false,
  ShiftChangeAwacsRequested = false,
  ShiftChangeEscortsRequested = false,
  CAPAirwings = {},  -- Utilities.FiFo#FIFO
  MonitoringData = {},
  MonitoringOn = false,
  FlightGroups = {},
  AwacsMission = nil,
  AwacsInZone = false, -- not yet arrived or gone again
  AwacsReady = false,
  CatchAllMissions = {},
  CatchAllFGs = {},
  PictureInterval = 300,
  ReassignTime = 120,
  PictureTimeStamp = 0,
  BorderZone = nil,
  RejectZone = nil,
  maxassigndistance = 100,
  PlayerGuidance = true,
  ModernEra = true,
  callsignshort = true,
  keepnumber = true,
  callsignTranslations = nil,
  TacDistance = 45,
  MeldDistance = 35,
  ThreatDistance = 25,
  AOName = "Rock",
  AOCoordinate = nil,
  clientmenus = nil,
  RadarBlur = 15,
  ReassignmentPause = 180,
  NoGroupTags = false,
  SuppressScreenOutput = false,
  NoMissileCalls = true,
  GoogleTTSPadding = 1,
  WindowsTTSPadding = 2.5,
  PlayerCapAssignment = true,
  AllowMarkers = false,
  PlayerStationName = nil,
  GCI = false,
  GCIGroup = nil,
  locale = "en",
  IncludeHelicopters = false,
  TacticalMenu = false,
  TacticalFrequencies = {},
  TacticalSubscribers = {},
  TacticalBaseFreq = 130,
  TacticalIncrFreq = 0.5,
  TacticalModulation = radio.modulation.AM,
  TacticalInterval = 120,
  DetectionSet = nil,
  MaxMissionRange = 125,
}

---
--@field CallSignClear
AWACS.CallSignClear = {
    [1]="Overlord",
    [2]="Magic",
    [3]="Wizard",
    [4]="Focus",
    [5]="Darkstar",
}

---
-- @field AnchorNames
AWACS.AnchorNames = {
  [1] = "One",
  [2] = "Two",
  [3] = "Three",
  [4] = "Four",
  [5] = "Five",
  [6] = "Six",
  [7] = "Seven",
  [8] = "Eight",
  [9] = "Nine",
  [10] = "Ten",
}

---
-- @field IFF
AWACS.IFF =
{
  SPADES = "Spades",
  NEUTRAL = "Neutral",
  FRIENDLY = "Friendly",
  ENEMY = "Hostile",
  BOGEY = "Bogey",
}

---
-- @field Phonetic
AWACS.Phonetic =
{
  [1] = 'Alpha',
  [2] = 'Bravo',
  [3] = 'Charlie',
  [4] = 'Delta',
  [5] = 'Echo',
  [6] = 'Foxtrot',
  [7] = 'Golf',
  [8] = 'Hotel',
  [9] = 'India',
  [10] = 'Juliett',
  [11] = 'Kilo',
  [12] = 'Lima',
  [13] = 'Mike',
  [14] = 'November',
  [15] = 'Oscar',
  [16] = 'Papa',
  [17] = 'Quebec',
  [18] = 'Romeo',
  [19] = 'Sierra',
  [20] = 'Tango',
  [21] = 'Uniform',
  [22] = 'Victor',
  [23] = 'Whiskey',
  [24] = 'Xray',
  [25] = 'Yankee',
  [26] = 'Zulu',
}

---
-- @field Shipsize
AWACS.Shipsize =
{
  [1] = "Singleton",
  [2] = "Two-Ship",
  [3] = "Heavy",
  [4] = "Gorilla",
}

---
-- @field ROE
AWACS.ROE = {
  POLICE = "Police",
  VID = "Visual ID",
  IFF = "IFF",
  BVR = "Beyond Visual Range",
}

---
-- @field AWACS.ROT
AWACS.ROT = {
    BYPASSESCAPE = "Bypass and Escape",
    EVADE = "Evade Fire",
    PASSIVE = "Passive Defense",
    RETURNFIRE = "Return Fire",
    OPENFIRE = "Open Fire",
 }
 
---
--@field THREATLEVEL -- can be 1-10, thresholds
AWACS.THREATLEVEL = {
  GREEN = 3,
  AMBER = 7,
  RED = 10,
}

---
--@field CapVoices -- Random CAP voices
AWACS.CapVoices = {
  [1] = "de-DE-Wavenet-A",
  [2] = "de-DE-Wavenet-B",
  [3] = "fr-FR-Wavenet-A",
  [4] = "fr-FR-Wavenet-B",
  [5] = "en-GB-Wavenet-A",
  [6] = "en-GB-Wavenet-B",
  [7] = "en-GB-Wavenet-D",
  [8] = "en-AU-Wavenet-B",
  [9] = "en-US-Wavenet-J",
  [10] = "en-US-Wavenet-H",
}

---
-- @field Messages 
AWACS.Messages = {
  EN =
    {
    DEFEND = "%s, %s! %s! %s! Defend!",
    VECTORTO = "%s, %s. Vector%s %s",
    VECTORTOTTS = "%s, %s, Vector%s %s",
    ANGELS = ". Angels ",
    ZERO = "zero",
    VANISHED = "%s, %s Group. Vanished.",
    VANISHEDTTS = "%s, %s group vanished.",
    SHIFTCHANGE = "%s shift change for %s control.",
    GROUPCAP = "Group",
    GROUP = "group",
    MILES = "miles",
    THOUSAND = "thousand",
    BOGEY = "Bogey",
    ALLSTATIONS = "All Stations",
    PICCLEAN = "%s. %s. Picture Clean.",
    PICTURE = "Picture",
    ONE = "One",
    GROUPMULTI = "groups",
    NOTCHECKEDIN = "%s. %s. Negative. You are not checked in.",
    CLEAN = "%s. %s. Clean.",
    DOPE = "%s. %s. Bogey Dope. ",
    VIDPOS = "%s. %s. Copy, target identified as %s.",
    VIDNEG = "%s. %s. Negative, get closer to target.",
    FFNEUTRAL = "Neutral",
    FFFRIEND = "Friendly",
    FFHOSTILE = "Hostile",
    FFSPADES = "Spades",
    FFCLEAN = "Clean",
    COPY = "%s. %s. Copy.",
    TARGETEDBY = "Targeted by %s.",
    STATUS = "Status",
    ALREADYCHECKEDIN = "%s. %s. Negative. You are already checked in.",
    ALPHACHECK = "Alpha Check",
    CHECKINAI = "%s. %s. Checking in as fragged. Expected playtime %d hours. Request Alpha Check %s.",
    SAFEFLIGHT = "%s. %s. Copy. Have a safe flight home.",
    VERYLOW = "very low",
    AIONSTATION = "%s. %s. On station over anchor %d at angels  %d. Ready for tasking.",
    POPUP = "Pop-up",
    NEWGROUP = "New group",
    HIGH= " High.",
    VERYFAST = " Very fast.",
    FAST = " Fast.",
    THREAT = "Threat",
    MERGED = "Merged",
    SCREENVID = "Intercept and VID %s group.",
    SCREENINTER = "Intercept %s group.",
    ENGAGETAG = "Targeted by %s.",
    REQCOMMIT = "%s. %s group. %s. %s, request commit.",
    AICOMMIT = "%s. %s group. %s. %s, commit.",
    COMMIT = "Commit",
    SUNRISE = "%s. All stations, SUNRISE SUNRISE SUNRISE, %s.",
    AWONSTATION = "%s on station for %s control.",
    STATIONAT = "%s. %s. Station at %s at angels %d.",
    STATIONATLONG = "%s. %s. Station at %s at angels %d doing %d knots.",
    STATIONSCREEN = "%s. %s.\nStation at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s.",
    STATIONTASK = "Station at %s\nAngels %d\nSpeed %d knots\nCoord %s\nROE %s",
    VECTORSTATION = " to Station",
    TEXTOPTIONS1 = "Lost friendly flight",
    TEXTOPTIONS2 =  "Vanished friendly flight",
    TEXTOPTIONS3 =  "Faded friendly contact",
    TEXTOPTIONS4 =  "Lost contact with",
    },
} 

---
-- @type AWACS.MonitoringData
-- @field #string AwacsStateMission
-- @field #string AwacsStateFG
-- @field #boolean AwacsShiftChange 
-- @field #table EscortsStateMission
-- @field #table EscortsStateFG
-- @field #boolean EscortsShiftChange
-- @field #number AICAPMax
-- @field #number AICAPCurrent
-- @field #number Airwings
-- @field #number Players
-- @field #number PlayersCheckedin

---
-- @type AWACS.MenuStructure
-- @field #boolean menuset
-- @field #string groupname
-- @field Core.Menu#MENU_GROUP basemenu
-- @field Core.Menu#MENU_GROUP_COMMAND checkin
-- @field Core.Menu#MENU_GROUP_COMMAND checkout
-- @field Core.Menu#MENU_GROUP_COMMAND picture
-- @field Core.Menu#MENU_GROUP_COMMAND bogeydope
-- @field Core.Menu#MENU_GROUP_COMMAND declare
-- @field Core.Menu#MENU_GROUP tasking
-- @field Core.Menu#MENU_GROUP_COMMAND showtask
-- @field Core.Menu#MENU_GROUP_COMMAND judy
-- @field Core.Menu#MENU_GROUP_COMMAND unable
-- @field Core.Menu#MENU_GROUP_COMMAND abort
-- @field Core.Menu#MENU_GROUP_COMMAND commit
-- @field Core.Menu#MENU_GROUP vid
-- @field Core.Menu#MENU_GROUP_COMMAND neutral
-- @field Core.Menu#MENU_GROUP_COMMAND hostile
-- @field Core.Menu#MENU_GROUP_COMMAND friendly

--- Group Data
-- @type AWACS.ManagedGroup
-- @field Wrapper.Group#GROUP Group
-- @field #string GroupName
-- @field Ops.FlightGroup#FLIGHTGROUP FlightGroup for AI
-- @field #boolean IsPlayer
-- @field #boolean IsAI
-- @field #string CallSign
-- @field #number CurrentAuftrag -- Auftragsnummer for AI
-- @field #number CurrentTask -- ManagedTask ID
-- @field #boolean HasAssignedTask
-- @field #number GID
-- @field #number AnchorStackNo
-- @field #number AnchorStackAngels
-- @field #number ContactCID
-- @field Core.Point#COORDINATE LastKnownPosition
-- @field #number LastTasking TimeStamp

--- Contact Data
-- @type AWACS.ManagedContact
-- @field #number CID
-- @field Ops.Intel#INTEL.Contact Contact
-- @field Ops.Intel#INTEL.Cluster Cluster
-- @field #string IFF -- ID'ed or not (yet)
-- @field Ops.Target#TARGET Target
-- @field #number LinkedTask --> TID
-- @field #number LinkedGroup --> GID
-- @field #string Status - #AWACS.TaskStatus
-- @field #string TargetGroupNaming -- Alpha, Charlie
-- @field #string ReportingName -- NATO platform name
-- @field #string EngagementTag
-- @field #boolean TACCallDone
-- @field #boolean MeldCallDone
-- @field #boolean MergeCallDone

---
-- @type AWACS.TaskDescription
AWACS.TaskDescription = {
  ANCHOR = "Anchor",
  REANCHOR = "Re-Anchor",
  VID = "VID",
  IFF = "IFF",
  INTERCEPT = "Intercept",
  SWEEP = "Sweep",
  RTB = "RTB",
}

---
-- @type AWACS.TaskStatus
AWACS.TaskStatus = {
  IDLE = "Idle",
  UNASSIGNED = "Unassigned",
  REQUESTED = "Requested",
  ASSIGNED = "Assigned",
  EXECUTING = "Executing",
  SUCCESS = "Success",
  FAILED = "Failed",
  DEAD = "Dead",
}

---
-- @type AWACS.ManagedTask
-- @field #number TID
-- @field #number AssignedGroupID
-- @field #boolean IsPlayerTask
-- @field #boolean IsUnassigned
-- @field Ops.Target#TARGET Target
-- @field Ops.Auftrag#AUFTRAG Auftrag
-- @field #AWACS.TaskStatus Status
-- @field #AWACS.TaskDescription ToDo
-- @field #string ScreenText Long descrition
-- @field Ops.Intel#INTEL.Contact Contact
-- @field Ops.Intel#INTEL.Cluster Cluster
-- @field #number CurrentAuftrag
-- @field #number RequestedTimestamp

---
-- @type AWACS.AnchorAssignedEntry
-- @field #number ID
-- @field #number Angels

---
-- @type AWACS.AnchorData
-- @field #number AnchorBaseAngels
-- @field Core.Zone#ZONE_RADIUS StationZone
-- @field Core.Point#COORDINATE StationZoneCoordinate
-- @field #string StationZoneCoordinateText
-- @field #string StationName
-- @field Utilities.FiFo#FIFO AnchorAssignedID FiFo of #AWACS.AnchorAssignedEntry
-- @field Utilities.FiFo#FIFO Anchors FiFo of available stacks
-- @field Wrapper.Marker#MARKER AnchorMarker Tag for this station

---
--@type RadioEntry
--@field #string TextTTS
--@field #string TextScreen
--@field #boolean IsNew
--@field #boolean IsGroup
--@field #boolean GroupID
--@field #number Duration
--@field #boolean ToScreen
--@field #boolean FromAI

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO-List 0.2.54
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--
-- DONE - WIP - Player tasking, VID
-- DONE - Localization (sensible?)
-- TODO - (LOW) LotATC
-- DONE - SW Optimization
-- WONTDO - Maybe check in AI only when airborne
-- DONE - remove SSML tag when not on google (currently sometimes spoken)
-- DONE - Maybe - Assign specific number of AI CAP to a station
-- DONE - Multiple AIRWING connection? Can't really get recruit to work, switched to random round robin
-- DONE - System for Players to VID contacts?
-- DONE - Task reassignment - if a player reject a task, don't choose him again for 3 minutes
-- DONE - added SSML tags to make google readouts nicer
-- DONE - 2nd audio queue for priority messages
-- DONE - (WIP) Missile launch callout
-- DONE - Event detection, Player joining, eject, crash, dead, leaving; AI shot -> DEFEND
-- DONE - AI Tasking
-- DONE - Shift Change, Change on asset RTB or dead or mission done (done for AWACS and Escorts)
-- DONE - TripWire - WIP - Threat (35nm), Meld (45nm, on mission), Merged (<3nm)
-- 
-- DONE - Escorts via Airwing not staying on
-- DONE - Borders for INTEL. Optional, i.e. land based defense within borders
-- DONE - Use AO as Anchor of Bulls, AO as default
-- DONE - SRS TTS output
-- DONE - Check-In/Out Humans
-- DONE - Check-In/Out AI
-- DONE - Picture
-- DONE - Declare
-- DONE - Bogey Dope
-- DONE - Radio Menu
-- DONE - Intel Detection
-- DONE - ROE
-- DONE - Anchor Stack Management
-- DONE - Shift Length AWACS/AI
-- DONE - (WIP) Reporting
-- DONE - Do not report non-airborne groups
-- DONE - Added option for helos
-- DONE - Added setting a coordinate for SRS

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO Constructor

--- Set up a new AI AWACS.
-- @param #AWACS self
-- @param #string Name Name of this AWACS for the radio menu.
-- @param #string AirWing The core Ops.Airwing#AIRWING managing the AWACS, Escort and (optionally) AI CAP planes for us.
-- @param #number Coalition Coalition, e.g. coalition.side.BLUE. Can also be passed as "blue", "red" or "neutral".
-- @param #string AirbaseName Name of the home airbase.
-- @param #string AwacsOrbit Name of the round, mission editor created zone where this AWACS orbits.
-- @param #string OpsZone Name of the round, mission editor created Fighter Engagement operations zone (FEZ) this AWACS controls. Can be passed as #ZONE_POLYGON. 
-- The name of the zone will be used in reference calls as bulls eye name, so ensure a radio friendly name that does not collide with NATOPS keywords.
-- @param #string StationZone Name of the round, mission editor created anchor zone where CAP groups will be stationed. Usually a short city name.
-- @param #number Frequency Radio frequency, e.g. 271.
-- @param #number Modulation Radio modulation, e.g. radio.modulation.AM or radio.modulation.FM.
-- @return #AWACS self
-- @usage
-- You can set up the OpsZone/FEZ in a number of ways:
-- * As a string denominating a normal, round zone you have created and named in the mission editor, e.g. "Rock".
-- * As a polygon zone, defined e.g. like `ZONE_POLYGON:New("Rock",GROUP:FindByName("RockZone"))` where "RockZone" is the name of a late activated helo, and it\'s waypoints (not more than 10) describe a closed polygon zone in the mission editor.
-- * As a string denominating a polygon zone from the mission editor (same late activated helo, but named "Rock#ZONE_POLYGON" in the mission editor. Here, Moose will auto-create a polygon zone when loading, and name it "Rock". Pass as `ZONE:FindByName("Rock")`.
function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,StationZone,Frequency,Modulation)
    -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New())
  
  --set Coalition
  if Coalition and type(Coalition)=="string" then
    if Coalition=="blue" then
      self.coalition=coalition.side.BLUE
      self.coalitiontxt = Coalition
    elseif Coalition=="red" then
      self.coalition=coalition.side.RED
      self.coalitiontxt = Coalition
    elseif Coalition=="neutral" then
      self.coalition=coalition.side.NEUTRAL
      self.coalitiontxt = Coalition
    else
      self:E("ERROR: Unknown coalition in AWACS!")
    end
  else
    self.coalition = Coalition
    self.coalitiontxt = string.lower(UTILS.GetCoalitionName(self.coalition))
  end
  
  -- base setup
  self.Name = Name -- #string
  self.AirWing = AirWing -- Ops.Airwing#AIRWING object
  
  AirWing:SetUsingOpsAwacs(self)
  
  self.CAPAirwings = FIFO:New() -- Utilities.FiFo#FIFO
  self.CAPAirwings:Push(AirWing,1)
  
  self.AwacsFG = nil
  --self.AwacsPayload = PayLoad -- Ops.Airwing#AIRWING.Payload
  --self.ModernEra = true -- use of EPLRS
  self.RadarBlur = 15 -- +/-15% detection precision i.e. 85-115 reported group size
  if type(OpsZone) == "string" then
    self.OpsZone = ZONE:New(OpsZone) -- Core.Zone#ZONE
  elseif type(OpsZone) == "table" and OpsZone.ClassName and string.find(OpsZone.ClassName,"ZONE") then
    self.OpsZone = OpsZone
  else
    self:E("AWACS - Invalid Zone passed!")
    return
  end
  
  --self.AOCoordinate = self.OpsZone:GetCoordinate()
  self.AOCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( self.coalition ) ) -- bulls eye from ME
  self.AOName = self.OpsZone:GetName()
  self.UseBullsAO = true -- as per NATOPS
  self.ControlZoneRadius = 100 -- nm
  self.StationZone = ZONE:New(StationZone) -- Core.Zone#ZONE
  self.StationZoneName = StationZone
  
  self.Frequency = Frequency or 271 -- #number
  self.Modulation = Modulation or radio.modulation.AM
  self.MultiFrequency = {self.Frequency}
  self.MultiModulation = {self.Modulation}

  self.Airbase = AIRBASE:FindByName(AirbaseName)
  self.AwacsAngels = 25 -- orbit at 25'000 ft
  if AwacsOrbit then
    self.OrbitZone = ZONE:New(AwacsOrbit) -- Core.Zone#ZONE
  end
  self.BorderZone = nil
  self.CallSign = CALLSIGN.AWACS.Magic -- #number
  self.CallSignNo = 1 -- #number
  self.NoHelos = true
  self.AIRequested = 0
  self.AIonCAP = 0
  self.AICAPMissions = FIFO:New() -- Utilities.FiFo#FIFO
  self.FlightGroups = FIFO:New() -- Utilities.FiFo#FIFO
  self.Countactcounter = 0
  
  self.PictureInterval = 300 -- picture every 5s mins
  self.PictureTimeStamp = 0 -- timestamp
  self.ReassignTime = 120 -- time for player re-assignment
  
  self.intelstarted = false
  self.sunrisedone = false
  
  local speed = 250
  self.SpeedBase = speed
  --self.Speed = UTILS.KnotsToAltKIAS(speed,self.AwacsAngels*1000)
  self.Speed = speed
  
  self.Heading = 0 -- north
  self.Leg = 50 -- nm
  self.invisible = false
  self.immortal = false
  self.callsigntxt = "AWACS"
  
  self.AwacsTimeOnStation = 4
  self.AwacsTimeStamp = 0
  self.EscortsTimeOnStation = 4
  self.EscortsTimeStamp = 0
  self.ShiftChangeTime = 0.25 -- 15mins
  self.ShiftChangeAwacsFlag = false
  self.ShiftChangeEscortsFlag = false
  
  self.CapSpeedBase = 270
  self.CAPTimeOnStation = 4
  self.MaxAIonCAP = 4
  self.AICAPCAllName = CALLSIGN.Aircraft.Colt
  self.AICAPCAllNumber = 0
  self.CAPGender = "male"
  self.CAPCulture = "en-US"
  self.CAPVoice = nil
  
  self.AwacsMission = nil
  self.AwacsInZone = false -- not yet arrived or gone again
  self.AwacsReady = false
  
  self.AwacsROE = AWACS.ROE.IFF
  self.AwacsROT = AWACS.ROT.BYPASSESCAPE
  
  -- Escorts
  self.HasEscorts = false
  self.EscortTemplate = ""
  self.EscortMission = {}
  self.EscortMissionReplacement = {}
  
  -- SRS
  self.PathToSRS = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio"
  self.Gender = "female"
  self.Culture = "en-GB"
  self.Voice = nil
  self.Port = 5002
  self.Volume = 1.0
  self.RadioQueue = FIFO:New() -- Utilities.FiFo#FIFO
  self.PrioRadioQueue = FIFO:New() -- Utilities.FiFo#FIFO
  self.TacticalQueue = FIFO:New() -- Utilities.FiFo#FIFO
  self.maxspeakentries = 3
  self.GoogleTTSPadding = 1
  self.WindowsTTSPadding = 2.5
  
  -- Client SET  
  self.clientset = SET_CLIENT:New():FilterActive(true):FilterCoalitions(self.coalitiontxt):FilterCategories("plane"):FilterStart()
  
  -- Player options
  self.PlayerGuidance = true
  self.ModernEra = true
  self.NoGroupTags = false
  self.SuppressScreenOutput = false  
  self.ReassignmentPause = 180
  self.callsignshort = true
  self.DeclareRadius = 5 -- NM
  self.MenuStrict = true
  self.maxassigndistance = 100 --nm
  self.NoMissileCalls = true
  self.PlayerCapAssignment = true
    
  -- managed groups
  self.ManagedGrps = {} -- #table of #AWACS.ManagedGroup entries
  self.ManagedGrpID = 0  
  self.callsignTranslations = nil
  
  -- Anchor stacks init
  self.AnchorStacks = FIFO:New() -- Utilities.FiFo#FIFO
  self.AnchorBaseAngels = 22
  self.AnchorStackDistance = 2
  self.AnchorMaxStacks = 4
  self.AnchorMaxAnchors = 2
  self.AnchorMaxZones = 6
  self.AnchorCurrZones = 1
  self.AnchorTurn = -(360/self.AnchorMaxZones)

  self:_CreateAnchorStack()

  -- Task lists
  self.ManagedTasks = FIFO:New() -- Utilities.FiFo#FIFO
  --self.OpenTasks = FIFO:New() -- Utilities.FiFo#FIFO

  -- Monitoring, init
  local MonitoringData = {} -- #AWACS.MonitoringData
  MonitoringData.AICAPCurrent = 0
  MonitoringData.AICAPMax = self.MaxAIonCAP
  MonitoringData.Airwings = 1
  MonitoringData.PlayersCheckedin = 0
  MonitoringData.Players = 0 
  MonitoringData.AwacsShiftChange = false
  MonitoringData.AwacsStateFG = "unknown"
  MonitoringData.AwacsStateMission = "unknown"
  MonitoringData.EscortsShiftChange = false
  MonitoringData.EscortsStateFG = {}
  MonitoringData.EscortsStateMission = {}
  self.MonitoringOn = false -- #boolean
  self.MonitoringData = MonitoringData
  
  self.CatchAllMissions = {}
  self.CatchAllFGs = {}
  
  -- Picture, Contacts, Bogeys
  self.PictureAO = FIFO:New() -- Utilities.FiFo#FIFO
  self.PictureEWR = FIFO:New() -- Utilities.FiFo#FIFO
  self.Contacts = FIFO:New() -- Utilities.FiFo#FIFO
  --self.ManagedContacts = FIFO:New()
  self.CID = 0
  self.ContactsAO = FIFO:New() -- Utilities.FiFo#FIFO
  
  self.clientmenus = FIFO:New() -- Utilities.FiFo#FIFO
  
  -- Tactical Menu
  self.TacticalMenu = false
  self.TacticalBaseFreq = 130
  self.TacticalIncrFreq = 0.5
  self.TacticalModulation = radio.modulation.AM
  self.acticalFrequencies = {}
  self.TacticalSubscribers = {}
  self.TacticalInterval = 120
  
  -- SET for Intel Detection
  self.DetectionSet=SET_GROUP:New()
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", self.Name, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown")
  
    -- Start State.
  self:SetStartState("Stopped")
  
  -- Add FSM transitions.
  --                 From State  -->   Event        -->     To State
  self:AddTransition("Stopped",       "Start",              "StartUp")     -- Start FSM.
  self:AddTransition("StartUp",       "Started",            "Running")    
  self:AddTransition("*",             "Status",             "*")           -- Status update.
  self:AddTransition("*",             "CheckedIn",          "*") 
  self:AddTransition("*",             "CheckedOut",         "*") 
  self:AddTransition("*",             "AssignAnchor",       "*") 
  self:AddTransition("*",             "AssignedAnchor",     "*")
  self:AddTransition("*",             "ReAnchor",           "*")
  self:AddTransition("*",             "NewCluster",         "*")
  self:AddTransition("*",             "NewContact",         "*")
  self:AddTransition("*",             "LostCluster",        "*")
  self:AddTransition("*",             "LostContact",        "*")
  self:AddTransition("*",             "CheckRadioQueue",    "*")
  self:AddTransition("*",             "CheckTacticalQueue", "*")  
  self:AddTransition("*",             "EscortShiftChange",  "*")
  self:AddTransition("*",             "AwacsShiftChange",   "*")
  self:AddTransition("*",             "FlightOnMission",    "*")
  self:AddTransition("*",             "Intercept",          "*")
  self:AddTransition("*",             "InterceptSuccess",   "*")
  self:AddTransition("*",             "InterceptFailure",   "*")
  self:AddTransition("*",             "VIDSuccess",   "*")
  self:AddTransition("*",             "VIDFailure",   "*")
  self:AddTransition("*",             "Stop",               "Stopped")     -- Stop FSM.
  
  
  local text = string.format("%sAWACS Version %s Initiated",self.lid,self.version)
  
  self:I(text)
  
  -- Events
  -- Player joins
  self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler)
  self:HandleEvent(EVENTS.PlayerEnterUnit, self._EventHandler)
    -- Player leaves
  self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler)
  self:HandleEvent(EVENTS.Ejection, self._EventHandler)
  self:HandleEvent(EVENTS.Crash, self._EventHandler)
  self:HandleEvent(EVENTS.Dead, self._EventHandler)
  self:HandleEvent(EVENTS.UnitLost, self._EventHandler)
  self:HandleEvent(EVENTS.BDA, self._EventHandler)
  self:HandleEvent(EVENTS.PilotDead, self._EventHandler)
  -- Missile warning
  self:HandleEvent(EVENTS.Shot, self._EventHandler)
  
  self:_InitLocalization()
  
  ------------------------
  --- Pseudo Functions ---
  ------------------------
  
    --- Triggers the FSM event "Start". Starts the AWACS. Initializes parameters and starts event handlers.
  -- @function [parent=#AWACS] Start
  -- @param #AWACS self

  --- Triggers the FSM event "Start" after a delay. Starts the AWACS. Initializes parameters and starts event handlers.
  -- @function [parent=#AWACS] __Start
  -- @param #AWACS self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the AWACS and all its event handlers.
  -- @param #AWACS self

  --- Triggers the FSM event "Stop" after a delay. Stops the AWACS and all its event handlers.
  -- @function [parent=#AWACS] __Stop
  -- @param #AWACS self
  -- @param #number delay Delay in seconds.
  
  --- On After "CheckedIn" event. AI or Player checked in.
  -- @function [parent=#AWACS] OnAfterCheckedIn
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
    --- On After "CheckedOut" event. AI or Player checked out.
  -- @function [parent=#AWACS] OnAfterCheckedOut
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
    --- On After "AssignedAnchor" event. AI or Player has been assigned a CAP station.
  -- @function [parent=#AWACS] OnAfterAssignedAnchor
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
    --- On After "ReAnchor" event. AI or Player has been send back to station.
  -- @function [parent=#AWACS] OnAfterReAnchor
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
  --- On After "NewCluster" event. AWACS detected a cluster.
  -- @function [parent=#AWACS] OnAfterNewCluster
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
  --- On After "NewContact" event. AWACS detected a contact.
  -- @function [parent=#AWACS] OnAfterNewContact
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
  --- On After "LostCluster" event. AWACS lost a radar cluster.
  -- @function [parent=#AWACS] OnAfterLostCluster
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
  --- On After "LostContact" event. AWACS lost a radar contact.
  -- @function [parent=#AWACS] OnAfterLostContact
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
    --- On After "EscortShiftChange" event. AWACS escorts shift change.
  -- @function [parent=#AWACS] OnAfterEscortShiftChange
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
  --- On After "AwacsShiftChange" event. AWACS shift change.
  -- @function [parent=#AWACS] OnAfterAwacsShiftChange
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
  --- On After "Intercept" event. CAP send on intercept.
  -- @function [parent=#AWACS] OnAfterIntercept
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
  --- On After "InterceptSuccess" event. Intercept successful.
  -- @function [parent=#AWACS] OnAfterInterceptSuccess
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
  --- On After "InterceptFailure" event. Intercept failure.
  -- @function [parent=#AWACS] OnAfterInterceptFailure
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- On After "VIDSuccess" event. Intercept successful.
  -- @function [parent=#AWACS] OnAfterVIDSuccess
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number GID Managed group ID (Player)
  -- @param Wrapper.Group#GROUP Group (Player) Group done the VID
  -- @param #AWACS.ManagedContact Contact The contact that was VID'd 
  
  --- On After "VIDFailure" event. Intercept failure.
  -- @function [parent=#AWACS] OnAfterVIDFailure
  -- @param #AWACS self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number GID Managed group ID (Player)
  -- @param Wrapper.Group#GROUP Group (Player) Group done the VID
  -- @param #AWACS.ManagedContact Contact The contact that was VID'd 
  
  return self
end

-- TODO Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- [User] Set the tactical information option, create 10 radio channels groups can subscribe and get Bogey Dope on a specific frequency automatically. You **need** to set up SRS first before using this!
-- @param #AWACS self
-- @param #number BaseFreq Base Frequency to use, defaults to 130.
-- @param #number Increase Increase to use, defaults to 0.5, thus channels created are 130, 130.5, 131 .. etc.
-- @param #number Modulation Modulation to use, defaults to radio.modulation.AM.
-- @param #number Interval Seconds between each update call.
-- @param #number Number Number of Frequencies to create, can be 1..10.
-- @return #AWACS self
function AWACS:SetTacticalRadios(BaseFreq,Increase,Modulation,Interval,Number)
  self:T(self.lid.."SetTacticalRadios")
  if not self.AwacsSRS then
    MESSAGE:New("AWACS: Setup SRS in your code BEFORE trying to add tac radios please!",30,"ERROR",true):ToLog():ToAll()
    return self
  end
  self.TacticalMenu = true
  self.TacticalBaseFreq = BaseFreq or 130
  self.TacticalIncrFreq = Increase or 0.5
  self.TacticalModulation = Modulation or radio.modulation.AM
  self.TacticalInterval = Interval or 120
  local number = Number or 10
  if number < 1 then number = 1 end
  if number > 10 then number = 10 end
  for i=1,number do
    local freq = self.TacticalBaseFreq + ((i-1)*self.TacticalIncrFreq)
    self.TacticalFrequencies[freq] = freq
  end
  if self.AwacsSRS then
    self.TacticalSRS = MSRS:New(self.PathToSRS,self.TacticalBaseFreq,self.TacticalModulation,self.Backend)
    self.TacticalSRS:SetCoalition(self.coalition)
    self.TacticalSRS:SetGender(self.Gender)
    self.TacticalSRS:SetCulture(self.Culture)
    self.TacticalSRS:SetVoice(self.Voice)
    self.TacticalSRS:SetPort(self.Port)
    self.TacticalSRS:SetLabel("AWACS")
    self.TacticalSRS:SetVolume(self.Volume)
    if self.PathToGoogleKey then
      --self.TacticalSRS:SetGoogle(self.PathToGoogleKey)
      self.TacticalSRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.AccessKey)
      self.TacticalSRS:SetProvider(MSRS.Provider.GOOGLE)
    end
    self.TacticalSRSQ = MSRSQUEUE:New("Tactical AWACS")
  end
  return self
end

--- TODO
-- [Internal] _RefreshMenuNonSubscribed
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_RefreshMenuNonSubscribed()
  self:T(self.lid.."_RefreshMenuNonSubscribed")
  local aliveset = self.clientset:GetAliveSet()
  
  for _,_group in pairs(aliveset) do
    -- go through set and re-build the sub-menu
    local grp = _group -- Wrapper.Client#CLIENT
    local Group = grp:GetGroup()
    local gname = nil
    if Group and Group:IsAlive() then
      gname = Group:GetName()
      self:T(gname)
    end
    local menustr = self.clientmenus:ReadByID(gname)
    local menu = menustr.tactical -- Core.Menu#MENU_GROUP
    if not self.TacticalSubscribers[gname] and menu then
      menu:RemoveSubMenus()
      for _,_freq in UTILS.spairs(self.TacticalFrequencies) do
        local modu = UTILS.GetModulationName(self.TacticalModulation)
        local text = string.format("Subscribe to %.3f %s",_freq,modu)
        local entry = MENU_GROUP_COMMAND:New(Group,text,menu,self._SubScribeTactRadio,self,Group,_freq) 
      end
    end
  end
  return self
end

--- [Internal] _UnsubScribeTactRadio
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group
-- @return #AWACS self
function AWACS:_UnsubScribeTactRadio(Group)
  self:T(self.lid.."_UnsubScribeTactRadio")
  local text = ""
  local textScreen = ""
  local GID, Outcome = self:_GetManagedGrpID(Group)
  local gcallsign = self:_GetCallSign(Group,GID) or "Ghost 1"
  local gname = Group:GetName() or "unknown"
    
  if Outcome and self.TacticalSubscribers[gname] then
    -- Pilot is checked in
    local Freq = self.TacticalSubscribers[gname]
    self.TacticalFrequencies[Freq] = Freq
    self.TacticalSubscribers[gname] = nil
    local modu = self.TacticalModulation == 0 and "AM" or "FM"
    text = string.format("%s, %s, switch back to AWACS main frequency!",gcallsign,self.callsigntxt)
    self:_NewRadioEntry(text,text,GID,true,true,true,false,true)
    self:_RefreshMenuNonSubscribed()
  elseif self.AwacsFG then
    -- no, unknown
    local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale)
    text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) 
    self:_NewRadioEntry(text,text,GID,Outcome,true,true,false)
  end
  return self
end

--- [Internal] _SubScribeTactRadio
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group
-- @param #number Frequency
-- @return #AWACS self
function AWACS:_SubScribeTactRadio(Group,Frequency)
  self:T(self.lid.."_SubScribeTactRadio")
  local text = ""
  local textScreen = ""
  local GID, Outcome = self:_GetManagedGrpID(Group)
  local gcallsign = self:_GetCallSign(Group,GID) or "Ghost 1"
  local gname = Group:GetName() or "unknown"
    
  if Outcome then
    -- Pilot is checked in
    self.TacticalSubscribers[gname] = Frequency
    self.TacticalFrequencies[Frequency] = nil
    local modu = self.TacticalModulation == 0 and "AM" or "FM"
    text = string.format("%s, %s, switch to %.3f %s for tactical information!",gcallsign,self.callsigntxt,Frequency,modu)
    self:_NewRadioEntry(text,text,GID,true,true,true,false,true)
    local menustr = self.clientmenus:ReadByID(gname)
    local menu = menustr.tactical -- Core.Menu#MENU_GROUP
    if menu then
      menu:RemoveSubMenus()
      local text = string.format("Unsubscribe %.3f %s",Frequency,modu)
      local entry = MENU_GROUP_COMMAND:New(Group,text,menu,self._UnsubScribeTactRadio,self,Group)
    end 
  elseif self.AwacsFG then
    -- no, unknown
    local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale)
    text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) 
    self:_NewRadioEntry(text,text,GID,Outcome,true,true,false)
  end
  
  return self
end

--- [Internal] _CheckSubscribers
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_CheckSubscribers()
   self:T(self.lid.."_InitLocalization")
   
   for _name,_freq in pairs(self.TacticalSubscribers or {}) do
    local grp = GROUP:FindByName(_name)
    if (not grp) or (not grp:IsAlive()) then
      self.TacticalFrequencies[_freq] = _freq
      self.TacticalSubscribers[_name] = nil
    end
   end
   
   return self
end

--- [Internal] Init localization
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_InitLocalization()
  self:T(self.lid.."_InitLocalization")
  self.gettext = TEXTANDSOUND:New("AWACS","en") -- Core.TextAndSound#TEXTANDSOUND
  self.locale = "en"
  for locale,table in pairs(self.Messages) do
    local Locale = string.lower(tostring(locale))
    self:T("**** Adding locale: "..Locale)
    for ID,Text in pairs(table) do
      self:T(string.format('Adding ID %s',tostring(ID)))
      self.gettext:AddEntry(Locale,tostring(ID),Text)
    end
  end
  return self
end

--- [User] Set locale for localization. Defaults to "en"
-- @param #AWACS self
-- @param #string Locale The locale to use
-- @return #AWACS self
function AWACS:SetLocale(Locale)
  self:T(self.lid.."SetLocale")
  self.locale = Locale or "en"
  return self
end

--- [User] Set own coordinate for BullsEye.
-- @param #AWACS self
-- @param Core.Point#COORDINATE
-- @return #AWACS self
function AWACS:SetBullsCoordinate(Coordinate)
  self:T(self.lid.."SetBullsCoordinate")
  self.AOCoordinate = Coordinate
  return self
end

--- [User] Set the max mission range flights can be away from their home base.
-- @param #AWACS self
-- @param #number NM Distance in nautical miles
-- @return #AWACS self
function AWACS:SetMaxMissionRange(NM)
  self.MaxMissionRange = NM or 125
  return self
end

--- [User] Add additional frequency and modulation for AWACS SRS output.
-- @param #AWACS self
-- @param #number Frequency The frequency to add, e.g. 132.5
-- @param #number Modulation The modulation to add for the frequency, e.g. radio.modulation.AM
-- @return #AWACS self
function AWACS:AddFrequencyAndModulation(Frequency,Modulation)
  self:T(self.lid.."AddFrequencyAndModulation")
  table.insert(self.MultiFrequency,Frequency)
  table.insert(self.MultiModulation,Modulation)
  if self.AwacsSRS then
    self.AwacsSRS:SetFrequencies(self.MultiFrequency)
    self.AwacsSRS:SetModulations(self.MultiModulation)
  end
  return self
end

--- [User] Set this instance to act as GCI TACS Theater Air Control System
-- @param #AWACS self
-- @param Wrapper.Group#GROUP EWR The **main** Early Warning Radar (EWR) GROUP object for GCI.
-- @param #number Delay (option) Start after this many seconds (optional).
-- @return #AWACS self
function AWACS:SetAsGCI(EWR,Delay)
  self:T(self.lid.."SetGCI")
  local delay = Delay or -5
  if type(EWR) == "string" then
    self.GCIGroup = GROUP:FindByName(EWR)
  else
    self.GCIGroup = EWR
  end
  self.GCI = true
  self:SetEscort(0)
  return self
end

--- [Internal] Create a AIC-TTS message entry
-- @param #AWACS self
-- @param #string TextTTS Text to speak
-- @param #string TextScreen Text for screen
-- @param #number GID Group ID #AWACS.ManagedGroup GID
-- @param #boolean IsGroup Has a group
-- @param #boolean ToScreen Show on screen
-- @param #boolean IsNew New
-- @param #boolean FromAI From AI
-- @param #boolean IsPrio Priority entry
-- @param #boolean Tactical Is for tactical info
-- @return #AWACS self
function AWACS:_NewRadioEntry(TextTTS, TextScreen,GID,IsGroup,ToScreen,IsNew,FromAI,IsPrio,Tactical)
  self:T(self.lid.."_NewRadioEntry")
  local RadioEntry = {} -- #AWACS.RadioEntry
  RadioEntry.IsNew = IsNew
  RadioEntry.TextTTS = TextTTS
  RadioEntry.TextScreen = TextScreen or TextTTS
  RadioEntry.GroupID = GID
  RadioEntry.ToScreen = ToScreen 
  RadioEntry.Duration = MSRS.getSpeechTime(TextTTS,0.95,false) or 8
  RadioEntry.FromAI = FromAI
  RadioEntry.IsGroup = IsGroup
  if Tactical then
    self.TacticalQueue:Push(RadioEntry)
  elseif IsPrio then
    self.PrioRadioQueue:Push(RadioEntry)
  else
    self.RadioQueue:Push(RadioEntry)
  end
  return self
end

--- [User] Change the bulls eye alias for AWACS callout. Defaults to "Rock"
-- @param #AWACS self
-- @param #string Name
-- @return #AWACS self
function AWACS:SetBullsEyeAlias(Name)
  self:T(self.lid.."_SetBullsEyeAlias")
  self.AOName = Name or "Rock"
  return self
end

--- [User] Set TOS Time-on-Station in Hours
-- @param #AWACS self
-- @param #number AICHours AWACS stays this number of hours on station before shift change, default is 4.
-- @param #number CapHours (optional) CAP stays this number of hours on station before shift change, default is 4.
-- @return #AWACS self
function AWACS:SetTOS(AICHours,CapHours)
  self:T(self.lid.."SetTOS")
  self.AwacsTimeOnStation = AICHours or 4
  self.CAPTimeOnStation = CapHours or 4
  return self
end

--- [User] Change number of seconds AWACS waits until a Player is re-assigned a different task. Defaults to 180.
-- @param #AWACS self
-- @param #number Seconds
-- @return #AWACS self
function AWACS:SetReassignmentPause(Seconds)
  self.ReassignmentPause = Seconds or 180
  return self
end

--- [User] Do not show messages on screen
-- @param #AWACS self
-- @param #boolean Switch If true, no messages will be shown on screen.
-- @return #AWACS self
function AWACS:SuppressScreenMessages(Switch)
  self:T(self.lid.."_SetBullsEyeAlias")
  self.SuppressScreenOutput = Switch or false
  return self
end

--- [User] Do not show messages on screen, no extra calls for player guidance, use short callsigns etc.
-- @param #AWACS self
-- @return #AWACS self
function AWACS:ZipLip()
  self:T(self.lid.."ZipLip")
  self:SuppressScreenMessages(true)
  self.PlayerGuidance = false
  self.callsignshort = true
  --self.NoGroupTags = true
  self.NoMissileCalls = true
  return self
end

--- [User] For CAP flights: Replace ME callsigns with user-defined callsigns for use with TTS and on-screen messaging
-- @param #AWACS self
-- @param #table translationTable with DCS callsigns as keys and replacements as values
-- @return #AWACS self
-- @usage
--            -- Set Custom CAP Flight Callsigns for use with TTS
--            testawacs:SetCustomCallsigns({
--              Devil = 'Bengal',
--              Snake = 'Winder',
--              Colt = 'Camelot',
--              Enfield = 'Victory',
--              Uzi = 'Evil Eye'
--            })
function AWACS:SetCustomCallsigns(translationTable) 
  self.callsignTranslations = translationTable
end

--- [Internal] Event handler
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group, can also be passed as #string group name
-- @return #boolean found
-- @return #number GID
-- @return #string CallSign
function AWACS:_GetGIDFromGroupOrName(Group)
  self:T(self.lid.."_GetGIDFromGroupOrName")
  self:T({Group})
  local GID = 0
  local Outcome = false
  local CallSign = "Ghost 1"
  local nametocheck = CallSign
  if Group and type(Group) == "string" then
    nametocheck = Group
  elseif Group and Group:IsInstanceOf("GROUP") then
    nametocheck = Group:GetName()
  else
    return false, 0, CallSign
  end

  local managedgrps = self.ManagedGrps or {}
  for _,_managed in pairs (managedgrps) do
    local managed = _managed -- #AWACS.ManagedGroup
    if managed.GroupName == nametocheck then
      GID = managed.GID
      Outcome = true
      CallSign = managed.CallSign
    end
  end
  self:T({Outcome, GID, CallSign})
  return Outcome, GID, CallSign
end

--- [Internal] Event handler
-- @param #AWACS self
-- @param Core.Event#EVENTDATA EventData
-- @return #AWACS self
function AWACS:_EventHandler(EventData)
  self:T(self.lid.."_EventHandler")
  self:T({Event = EventData.id})
  
  local Event = EventData -- Core.Event#EVENTDATA
  
  if Event.id == EVENTS.PlayerEnterAircraft or Event.id == EVENTS.PlayerEnterUnit then --player entered unit
    --self:T("Player enter unit: " .. Event.IniPlayerName)
    --self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition))
    if Event.IniCoalition == self.coalition then
      self:_SetClientMenus()
    end
  end
  
  if Event.id == EVENTS.PlayerLeaveUnit and Event.IniGroupName then --player left unit
    -- check known player?
    self:T("Player group left  unit: " .. Event.IniGroupName)
    self:T("Player name left: " .. Event.IniPlayerName)
    self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition))
    if Event.IniCoalition == self.coalition then
      local Outcome, GID, CallSign = self:_GetGIDFromGroupOrName(Event.IniGroupName)
      if Outcome and GID > 0 then
        self:T("Task Abort and Checkout Called")
        self:_TaskAbort(Event.IniGroupName)
        self:_CheckOut(nil,GID,true)
      end
    end
  end
  
  if Event.id == EVENTS.Ejection or Event.id == EVENTS.Crash or Event.id == EVENTS.Dead or Event.id == EVENTS.PilotDead then --unit or player dead
    -- check known group?
    if Event.IniCoalition == self.coalition then
      --self:T("Ejection/Crash/Dead/PilotDead Group: " .. Event.IniGroupName)
      --self:T("Coalition = " .. UTILS.GetCoalitionName(Event.IniCoalition))
      local Outcome, GID, CallSign = self:_GetGIDFromGroupOrName(Event.IniGroupName)
      if Outcome and GID > 0 then
        self:_TaskAbort(Event.IniGroupName)
        self:_CheckOut(nil,GID,true)
      end
    end
  end
  
  if Event.id == EVENTS.Shot and self.PlayerGuidance and not self.NoMissileCalls then
    if Event.IniCoalition ~= self.coalition then
      self:T("Shot from: " .. Event.IniGroupName)
      
      local position = Event.IniGroup:GetCoordinate()
      if not position then return self end
      
      -- Check missile type
      local Category = Event.WeaponCategory
      local WeaponDesc = EventData.Weapon:getDesc() -- https://wiki.hoggitworld.com/view/DCS_enum_weapon
      self:T({WeaponDesc})
      
      if WeaponDesc.category == 1 and (WeaponDesc.missileCategory == 1 or WeaponDesc.missileCategory == 2) then
        self:T("AAM or SAM Missile fired")
        -- Missile fired
        -- WIP Missile Callouts
        local warndist = 25
        local Type = "SAM"
        if WeaponDesc.category == 1 then
          Type = "Missile"
          -- AAM  
          local guidance = WeaponDesc.guidance or 4 -- IR=2, Radar Active=3, Radar Semi Active=4, Radar Passive = 5
          if guidance == 2 then
            warndist = 10
          elseif guidance == 3 then
            warndist = 25
          elseif guidance == 4 then
            warndist = 15
          elseif guidance == 5 then
            warndist = 10
          end -- guidance
        end -- cat 1
        self:_MissileWarning(position,Type,warndist)
      end -- cat 1 or 2
      
    end -- end coalition
  end -- end shot
  
  return self
end

--- [Internal] Missile Warning Callout
-- @param #AWACS self
-- @param Core.Point#COORDINATE Coordinate Where the shot happened
-- @param #string Type Type to call out, e.i. "SAM" or "Missile"
-- @param #number Warndist Distance in NM to find friendly planes
-- @return #AWACS self
function AWACS:_MissileWarning(Coordinate,Type,Warndist)
  self:T(self.lid.."_MissileWarning Type="..Type.." WarnDist="..Warndist)
  
  if not Coordinate then return self end
  local shotzone = ZONE_RADIUS:New("WarningZone",Coordinate:GetVec2(),UTILS.NMToMeters(Warndist))
  local targetgrpset = SET_GROUP:New():FilterCoalitions(self.coalitiontxt):FilterCategoryAirplane():FilterActive():FilterZones({shotzone}):FilterOnce()
  if targetgrpset:Count() > 0 then
    local targets = targetgrpset:GetSetObjects()
    for _,_grp in pairs (targets) do
      -- DONE -- player callouts only
      if _grp and _grp:IsAlive() then
        local isPlayer = _grp:IsPlayer()
        
        if isPlayer then
          local callsign = self:_GetCallSign(_grp)
          local defend = self.gettext:GetEntry("DEFEND",self.locale)
          --local text = string.format("%s, %s! %s! %s! Defend!",callsign,Type,Type,Type)
          local text = string.format(defend,callsign,Type,Type,Type)
          self:_NewRadioEntry(text, text,0,false,self.debug,true,false,true)
        end
      end
    end
  end
  return self
end

--- [User] Set AWACS Radar Blur - the radar contact count per group/cluster will be distored up or down by this number percent. Defaults to 15 in Modern Era and 25 in Cold War.
-- @param #AWACS self
-- @param #number Percent
-- @return #AWACS self
function AWACS:SetRadarBlur(Percent)
  local percent = Percent or 15
  if percent < 0  then percent = 0 end
  if percent > 100 then percent = 100 end
  self.RadarBlur = Percent
  return self
end

--- [User] Set AWACS to Cold War standards - ROE to VID, ROT to Passive (bypass and escape). Radar blur 25%.
-- Sets TAC/Meld/Threat call distances to 35, 25 and 15 nm.
-- @param #AWACS self
-- @return #AWACS self
function AWACS:SetColdWar()
  self.ModernEra = false
  self.AwacsROT = AWACS.ROT.PASSIVE
  self.AwacsROE = AWACS.ROE.VID
  self.RadarBlur = 25
  self:SetInterceptTimeline(35, 25, 15)
  return self
end

--- [User] Set AWACS to Modern Era standards - ROE to BVR, ROT to defensive (evade fire). Radar blur 15%.
-- @param #AWACS self
-- @return #AWACS self
function AWACS:SetModernEra()
  self.ModernEra = true
  self.AwacsROT = AWACS.ROT.EVADE
  self.AwacsROE = AWACS.ROE.BVR
  self.RadarBlur = 15
  return self
end

--- [User] Set AWACS to Modern Era standards - ROE to IFF, ROT to defensive (evade fire). Radar blur 15%.
-- @param #AWACS self
-- @return #AWACS self
function AWACS:SetModernEraDefensive()
  self.ModernEra = true
  self.AwacsROT = AWACS.ROT.EVADE
  self.AwacsROE = AWACS.ROE.IFF
  self.RadarBlur = 15
  return self
end

--- [User] Set AWACS to Modern Era standards - ROE to BVR, ROT to return fire. Radar blur 15%.
-- @param #AWACS self
-- @return #AWACS self
function AWACS:SetModernEraAggressive()
  self.ModernEra = true
  self.AwacsROT = AWACS.ROT.RETURNFIRE
  self.AwacsROE = AWACS.ROE.BVR
  self.RadarBlur = 15
  return self
end

--- [User] Set AWACS to Policing standards - ROE to VID, ROT to Lock (bypass and escape). Radar blur 15%.
-- @param #AWACS self
-- @return #AWACS self
function AWACS:SetPolicingModern()
  self.ModernEra = true
  self.AwacsROT = AWACS.ROT.BYPASSESCAPE
  self.AwacsROE = AWACS.ROE.VID
  self.RadarBlur = 15
  return self
end

--- [User] Set AWACS to Policing standards - ROE to VID, ROT to Lock (bypass and escape). Radar blur 25%.
-- Sets TAC/Meld/Threat call distances to 35, 25 and 15 nm.
-- @param #AWACS self
-- @return #AWACS self
function AWACS:SetPolicingColdWar()
  self.ModernEra = false
  self.AwacsROT = AWACS.ROT.BYPASSESCAPE
  self.AwacsROE = AWACS.ROE.VID
  self.RadarBlur = 25
  self:SetInterceptTimeline(35, 25, 15)
  return self
end

--- [User] Set AWACS Player Guidance - influences missile callout and the "New" label in group callouts. 
-- @param #AWACS self
-- @param #boolean Switch If true (default) it is on, if false, it is off.
-- @return #AWACS self
function AWACS:SetPlayerGuidance(Switch)
  if (Switch == nil) or (Switch == true) then
   self.PlayerGuidance = true
  else
    self.PlayerGuidance = false
  end
  return self
end

--- [User] Get AWACS Name
-- @param #AWACS self
-- @return #string Name of this instance
function AWACS:GetName()
  return self.Name or "not set"
end

--- [User] Set AWACS intercept timeline support distance.
-- @param #AWACS self
-- @param #number TacDistance Distance for TAC call, default 45nm
-- @param #number MeldDistance Distance for Meld call, default 35nm
-- @param #number ThreatDistance Distance for Threat call, default 25nm
-- @return #AWACS self
function AWACS:SetInterceptTimeline(TacDistance, MeldDistance, ThreatDistance)
  self.TacDistance = TacDistance or 45
  self.MeldDistance = MeldDistance or 35
  self.ThreatDistance = ThreatDistance or 25
  return self
end

--- [User] Set additional defensive zone, e.g. the zone behind the FEZ to also be defended
-- @param #AWACS self
-- @param Core.Zone#ZONE Zone
-- @param #boolean Draw Draw lines around this zone if true
-- @return #AWACS self
function AWACS:SetAdditionalZone(Zone, Draw)
  self:T(self.lid.."SetAdditionalZone")
  self.BorderZone = Zone
  if self.debug then
    Zone:DrawZone(self.coalition,{1,0.64,0},1,{1,0.64,0},0.2,1,true)
    if self.AllowMarkers then
      MARKER:New(Zone:GetCoordinate(),"Defensive Zone"):ToCoalition(self.coalition)
    end
  elseif Draw then
    Zone:DrawZone(self.coalition,{1,0.64,0},1,{1,0.64,0},0.2,1,true)
  end
  return self
end

--- [User] Set rejection zone, e.g. a border of a foreign country. Detected bogeys in here won't be engaged.
-- @param #AWACS self
-- @param Core.Zone#ZONE Zone
-- @param #boolean Draw Draw lines around this zone if true
-- @return #AWACS self
function AWACS:SetRejectionZone(Zone,Draw)
  self:T(self.lid.."SetRejectionZone")
  self.RejectZone = Zone
  if Draw then
    Zone:DrawZone(self.coalition,{1,0.64,0},1,{1,0.64,0},0.2,1,true)
    --MARKER:New(Zone:GetCoordinate(),"Rejection Zone"):ToAll()
  elseif self.debug then
    Zone:DrawZone(self.coalition,{1,0.64,0},1,{1,0.64,0},0.2,1,true)
    if self.AllowMarkers then
      MARKER:New(Zone:GetCoordinate(),"Rejection Zone"):ToCoalition(self.coalition)
    end
  end
  return self
end

--- [User] Draw a line around the FEZ on the F10 map.
-- @param #AWACS self
-- @return #AWACS self
function AWACS:DrawFEZ()
  self.OpsZone:DrawZone(self.coalition,{1,0,0},1,{1,0,0},0.2,5,true)
  return self
end

--- [User] Set AWACS flight details
-- @param #AWACS self
-- @param #number CallSign Defaults to CALLSIGN.AWACS.Magic
-- @param #number CallSignNo Defaults to 1
-- @param #number Angels Defaults to 25 (i.e. 25000 ft)
-- @param #number Speed Defaults to 250kn
-- @param #number Heading Defaults to 0 (North)
-- @param #number Leg Defaults to 25nm
-- @return #AWACS self
function AWACS:SetAwacsDetails(CallSign,CallSignNo,Angels,Speed,Heading,Leg)
  self:T(self.lid.."SetAwacsDetails")
  self.CallSign = CallSign or CALLSIGN.AWACS.Magic
  self.CallSignNo = CallSignNo or 1
  self.AwacsAngels = Angels or 25
  local speed = Speed or 250
  self.SpeedBase = speed
  --self.Speed = UTILS.KnotsToAltKIAS(speed,self.AwacsAngels*1000)
  self.Speed = speed
  self.Heading = Heading or 0
  self.Leg = Leg or 25
  return self
end

--- [User] Set AWACS custom callsigns for TTS
-- @param #AWACS self
-- @param #table CallsignTable Table of custom callsigns to use with TTS
-- @return #AWACS self
-- @usage
-- You can overwrite the standard AWACS callsign for TTS usage with your own naming, e.g. like so:
--              testawacs:SetCustomAWACSCallSign({
--                [1]="Overlord", -- Overlord
--                [2]="Bookshelf", -- Magic
--                [3]="Wizard", -- Wizard
--                [4]="Focus", -- Focus
--                [5]="Darkstar", -- Darkstar
--                })
-- The default callsign used in AWACS is "Magic". With the above change, the AWACS will call itself "Bookshelf" over TTS instead.
function AWACS:SetCustomAWACSCallSign(CallsignTable)
  self:T(self.lid.."SetCustomAWACSCallSign")
  self.CallSignClear = CallsignTable
  return self
end

--- [User] Add a radar GROUP object to the INTEL detection SET_GROUP
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group The GROUP to be added. Can be passed as SET_GROUP.
-- @return #AWACS self
function AWACS:AddGroupToDetection(Group)
  self:T(self.lid.."AddGroupToDetection")
  if Group and Group.ClassName and Group.ClassName == "GROUP" then
    self.DetectionSet:AddGroup(Group)
  elseif Group and Group.ClassName and Group.ClassName == "SET_GROUP" then
    self.DetectionSet:AddSet(Group)
  end
  return self
end

--- [User] Set AWACS SRS TTS details - see @{Sound.SRS} for details. `SetSRS()` will try to use as many attributes configured with @{Sound.SRS#MSRS.LoadConfigFile}() as possible.
-- @param #AWACS self
-- @param #string PathToSRS Defaults to "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio"
-- @param #string Gender Defaults to "male"
-- @param #string Culture Defaults to "en-US"
-- @param #number Port Defaults to 5002
-- @param #string Voice (Optional) Use a specifc voice with the @{Sound.SRS#SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`.
-- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS.
-- @param #number Volume Volume - between 0.0 (silent) and 1.0 (loudest)
-- @param #string PathToGoogleKey (Optional) Path to your google key if you want to use google TTS; if you use a config file for MSRS, hand in nil here.
-- @param #string AccessKey (Optional) Your Google API access key. This is necessary if DCS-gRPC is used as backend; if you use a config file for MSRS, hand in nil here.
-- @param #string Backend (Optional) Your MSRS Backend if different from your config file settings, e.g. MSRS.Backend.SRSEXE or MSRS.Backend.GRPC
-- @return #AWACS self
function AWACS:SetSRS(PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey,AccessKey,Backend)
  self:T(self.lid.."SetSRS")
  self.PathToSRS = PathToSRS or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" 
  self.Gender = Gender or MSRS.gender or "male"
  self.Culture = Culture or MSRS.culture or "en-US"
  self.Port = Port or MSRS.port or 5002
  self.Voice = Voice or MSRS.voice
  self.PathToGoogleKey = PathToGoogleKey
  self.AccessKey = AccessKey
  self.Volume = Volume or 1.0
  self.Backend = Backend or MSRS.backend
  BASE:I({backend = self.Backend})
  self.AwacsSRS = MSRS:New(self.PathToSRS,self.MultiFrequency,self.MultiModulation,self.Backend)
  self.AwacsSRS:SetCoalition(self.coalition)
  self.AwacsSRS:SetGender(self.Gender)
  self.AwacsSRS:SetCulture(self.Culture)
  self.AwacsSRS:SetPort(self.Port)
  self.AwacsSRS:SetLabel("AWACS")
  self.AwacsSRS:SetVolume(Volume)
  if self.PathToGoogleKey then
    --self.AwacsSRS:SetGoogle(self.PathToGoogleKey)
    self.AwacsSRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.AccessKey)
    self.AwacsSRS:SetProvider(MSRS.Provider.GOOGLE)
  end
   -- Pre-configured Google?
  if (not PathToGoogleKey) and self.AwacsSRS:GetProvider() == MSRS.Provider.GOOGLE then
    self.PathToGoogleKey = MSRS.poptions.gcloud.credentials
    self.Voice = Voice or MSRS.poptions.gcloud.voice
    self.AccessKey = AccessKey or MSRS.poptions.gcloud.key
  end
  self.AwacsSRS:SetVoice(self.Voice)
  return self
end

--- [User] Set AWACS Voice Details for AI CAP Planes  - SRS TTS - see @{Sound.SRS} for details
-- @param #AWACS self
-- @param #string Gender Defaults to "male"
-- @param #string Culture Defaults to "en-US"
-- @param #string Voice (Optional) Use a specifc voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`.
-- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS.
-- @return #AWACS self
function AWACS:SetSRSVoiceCAP(Gender, Culture, Voice)
  self:T(self.lid.."SetSRSVoiceCAP")
  self.CAPGender = Gender or "male"
  self.CAPCulture = Culture or "en-US"
  self.CAPVoice = Voice or "en-GB-Standard-B"
  return self
end

--- [User] Set AI CAP Plane Details
-- @param #AWACS self
-- @param #number Callsign Callsign name of AI CAP, e.g. CALLSIGN.Aircraft.Dodge. Defaults to CALLSIGN.Aircraft.Colt. Note that not all available callsigns work for all plane types.
-- @param #number MaxAICap Maximum number of AI CAP planes on station that AWACS will set up automatically. Default to 4.
-- @param #number TOS Time on station, in  hours. AI planes might go back to base earlier if they run out of fuel or missiles.
-- @param #number Speed Airspeed to be used in knots. Will be adjusted to flight height automatically. Defaults to 270.
-- @return #AWACS self
function AWACS:SetAICAPDetails(Callsign,MaxAICap,TOS,Speed)
  self:T(self.lid.."SetAICAPDetails")
  self.CapSpeedBase = Speed or 270
  self.CAPTimeOnStation = TOS or 4
  self.MaxAIonCAP = MaxAICap or 4
  self.AICAPCAllName = Callsign or CALLSIGN.Aircraft.Colt
  return self
end

--- [User] Set AWACS Escorts Template
-- @param #AWACS self
-- @param #number EscortNumber Number of fighther plane GROUPs to accompany this AWACS. 0 or nil means no escorts. If you want >1 plane in an escort group, you can either set the respective squadron grouping to the desired number, or use a template for escorts with >1 unit.
-- @param #number Formation Formation the escort should take (if more than one plane), e.g. `ENUMS.Formation.FixedWing.FingerFour.Group`. Formation is used on GROUP level, multiple groups of one unit will NOT conform to this formation.
-- @param #table OffsetVector Offset the escorts should fly behind the AWACS, given as table, distance in meters, e.g. `{x=-500,y=0,z=500}` - 500m behind (negative value) and to the right (negative for left), no vertical separation (positive over, negative under the AWACS flight). For multiple groups, the vectors will be slightly changed to avoid collisions.
-- @param #number EscortEngageMaxDistance Escorts engage air targets max this NM away, defaults to 45NM.
-- @return #AWACS self
function AWACS:SetEscort(EscortNumber,Formation,OffsetVector,EscortEngageMaxDistance)
  self:T(self.lid.."SetEscort")
  if EscortNumber and EscortNumber > 0 then
    self.HasEscorts = true
    self.EscortNumber = EscortNumber
  else
    self.HasEscorts = false
    self.EscortNumber = 0
  end
  self.EscortFormation = Formation
  self.OffsetVec = OffsetVector or {x=500,y=100,z=500}
  self.EscortEngageMaxDistance = EscortEngageMaxDistance or 45
  return self
end

--- [Internal] Message a vector BR to a position
-- @param #AWACS self
-- @param #number GID Group GID
-- @param #string Tag (optional) Text to add after Vector, e.g. " to Anchor" - NOTE the leading space
-- @param Core.Point#COORDINATE Coordinate The Coordinate to use
-- @param #number Angels (Optional) Add Angels 
-- @return #AWACS self
function AWACS:_MessageVector(GID,Tag,Coordinate,Angels)
  self:T(self.lid.."_MessageVector")
  
  local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
  local Tag = Tag or ""
  
  if managedgroup and Coordinate then
    
    local tocallsign = managedgroup.CallSign or "Ghost 1"
    local group = managedgroup.Group
    local groupposition = group:GetCoordinate()
    
    local BRtext,BRtextTTS = self:_ToStringBR(groupposition,Coordinate)
    
    local vector = self.gettext:GetEntry("VECTORTO",self.locale)
    local vectortts = self.gettext:GetEntry("VECTORTOTTS",self.locale)
    local angelstxt = self.gettext:GetEntry("ANGELS",self.locale)
    
    local text = string.format(vectortts,tocallsign, self.callsigntxt,Tag,BRtextTTS)
    local textScreen = string.format(vector,tocallsign, self.callsigntxt,Tag,BRtext)
    
    if Angels then
      text = text .. angelstxt ..tostring(Angels).."."
      textScreen = textScreen ..angelstxt..tostring(Angels).."."
    end
    
    self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false)
  
  end
  
  return self
end

--- [Internal] Start AWACS Escorts FlightGroup
-- @param #AWACS self
-- @param #boolean Shiftchange This is a shift change call
-- @return #AWACS self
function AWACS:_StartEscorts(Shiftchange)
  self:T(self.lid.."_StartEscorts")
  
  local AwacsFG = self.AwacsFG -- Ops.FlightGroup#FLIGHTGROUP
  local group = AwacsFG:GetGroup()

  local timeonstation = (self.EscortsTimeOnStation + self.ShiftChangeTime) * 3600 -- hours to seconds
  local OffsetX = 500
  local OffsetY = 500
  local OffsetZ = 500
  if self.OffsetVec then
    OffsetX = self.OffsetVec.x or 500
    OffsetY = self.OffsetVec.y or 500
    OffsetZ = self.OffsetVec.z or 500
  end
  
  for i=1,self.EscortNumber do
    -- every
    local escort = AUFTRAG:NewESCORT(group, {x= OffsetX*((i + (i%2))/2), y=OffsetY*((i + (i%2))/2), z=(OffsetZ + OffsetZ*((i + (i%2))/2))*(-1)^i},self.EscortEngageMaxDistance,{"Air"})
    --local escort = AUFTRAG:NewESCORT(group,self.OffsetVec,self.EscortEngageMaxDistance,{"Air"})
    --escort:SetRequiredAssets(self.EscortNumber)
    escort:SetTime(nil,timeonstation)
    if self.Escortformation then
      escort:SetFormation(self.Escortformation)
    end
    escort:SetMissionRange(self.MaxMissionRange)
    
    self.AirWing:AddMission(escort)
    self.CatchAllMissions[#self.CatchAllMissions+1] = escort

    if Shiftchange then
      self.EscortMissionReplacement[i] = escort
    else
      self.EscortMission[i] = escort
    end
  end
  
  return self
end

--- [Internal] AWACS further Start Settings
-- @param #AWACS self
-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup
-- @param Ops.Auftrag#AUFTRAG Mission
-- @return #AWACS self
function AWACS:_StartSettings(FlightGroup,Mission)
  self:T(self.lid.."_StartSettings")
  
  local Mission = Mission -- Ops.Auftrag#AUFTRAG
  local AwacsFG = FlightGroup -- Ops.FlightGroup#FLIGHTGROUP
  
  -- Is this our Awacs mission?
  if self.AwacsMission:GetName() == Mission:GetName() then
    self:T("Setting up Awacs")
    AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation,false)
    AwacsFG:SwitchRadio(self.Frequency,self.Modulation)
    AwacsFG:SetDefaultAltitude(self.AwacsAngels*1000)
    AwacsFG:SetHomebase(self.Airbase)
    AwacsFG:SetDefaultCallsign(self.CallSign,self.CallSignNo)
    AwacsFG:SetDefaultROE(ENUMS.ROE.WeaponHold)
    AwacsFG:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN)
    AwacsFG:SetDefaultEPLRS(self.ModernEra)
    AwacsFG:SetDespawnAfterLanding()
    AwacsFG:SetFuelLowRTB(true)
    AwacsFG:SetFuelLowThreshold(20)
    
    local group = AwacsFG:GetGroup() -- Wrapper.Group#GROUP
    
    group:SetCommandInvisible(self.invisible)
    group:SetCommandImmortal(self.immortal)
    group:CommandSetCallsign(self.CallSign,self.CallSignNo,2)
    group:CommandEPLRS(self.ModernEra,5)
    -- Non AWACS does not seem take AWACS CS in DCS Group

    self.AwacsFG = AwacsFG 
    
    --self.AwacsFG:SetSRS(self.PathToSRS,self.Gender,self.Culture,self.Voice,self.Port,self.PathToGoogleKey,"AWACS",self.Volume)
     
    self.callsigntxt = string.format("%s",self.CallSignClear[self.CallSign])
    
    self:__CheckRadioQueue(10)
    
    if self.HasEscorts then
      --mission:SetRequiredEscorts(self.EscortNumber)
      self:_StartEscorts()
    end
    
    self.AwacsTimeStamp = timer.getTime()
    self.EscortsTimeStamp = timer.getTime()

    self.PictureTimeStamp = timer.getTime() + 10*60
    
    self.AwacsReady = true
    -- set FSM to started
    self:Started()
    
  elseif self.ShiftChangeAwacsRequested and self.AwacsMissionReplacement and self.AwacsMissionReplacement:GetName() == Mission:GetName() then
    self:T("Setting up Awacs Replacement")
    -- manage AWACS Replacement
    AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation,false)
    AwacsFG:SwitchRadio(self.Frequency,self.Modulation)
    AwacsFG:SetDefaultAltitude(self.AwacsAngels*1000)
    AwacsFG:SetHomebase(self.Airbase)
    self.CallSignNo = self.CallSignNo+1
    AwacsFG:SetDefaultCallsign(self.CallSign,self.CallSignNo)
    AwacsFG:SetDefaultROE(ENUMS.ROE.WeaponHold)
    AwacsFG:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN)
    AwacsFG:SetDefaultEPLRS(self.ModernEra)
    AwacsFG:SetDespawnAfterLanding()
    AwacsFG:SetFuelLowRTB(true)
    AwacsFG:SetFuelLowThreshold(20)
    
    local group = AwacsFG:GetGroup() -- Wrapper.Group#GROUP
    
    group:SetCommandInvisible(self.invisible)
    group:SetCommandImmortal(self.immortal)
    group:CommandSetCallsign(self.CallSign,self.CallSignNo,2)
    
    --AwacsFG:SetSRS(self.PathToSRS,self.Gender,self.Culture,self.Voice,self.Port,nil,"AWACS")

    self.callsigntxt = string.format("%s",self.CallSignClear[self.CallSign])
    
    local shifting = self.gettext:GetEntry("SHIFTCHANGE",self.locale)
    
    local text = string.format(shifting,self.callsigntxt,self.AOName or "Rock")
    
    self:T(self.lid..text)
    
    AwacsFG:RadioTransmission(text,1,false)
    
    self.AwacsFG = AwacsFG 
    
    if self.HasEscorts then
      self:_StartEscorts(true)
    end
    
    self.AwacsTimeStamp = timer.getTime()
    self.EscortsTimeStamp = timer.getTime()
    
    self.AwacsReady = true
    
  end
  return self
end

--- [Internal] Return Bullseye BR for Alpha Check etc, returns e.g. "Rock 021, 16" ("Rock" being the set BE name)
-- @param #AWACS self
-- @param Core.Point#COORDINATE Coordinate
-- @param #boolean ssml Add SSML tag
-- @param #boolean TTS For non-Alpha checks, hand back in format "Rock 0 2 1, 16"
-- @return #string BullseyeBR
function AWACS:_ToStringBULLS( Coordinate, ssml, TTS )
  self:T(self.lid.."_ToStringBULLS")
  local bullseyename = self.AOName or "Rock"
  local BullsCoordinate = self.AOCoordinate
  local DirectionVec3 = BullsCoordinate:GetDirectionVec3( Coordinate )
  local AngleRadians =  Coordinate:GetAngleRadians( DirectionVec3 )
  local Distance = Coordinate:Get2DDistance( BullsCoordinate )
  local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 )
  local Bearing = string.format( '%03d', AngleDegrees )
  local Distance = UTILS.Round( UTILS.MetersToNM( Distance ), 0 )
  if ssml then
    return string.format("%s <say-as interpret-as='characters'>%03d</say-as>, %d",bullseyename,Bearing,Distance)
  end
  if TTS then
    Bearing = self:_ToStringBullsTTS(Bearing)
    local zero = self.gettext:GetEntry("ZERO",self.locale)
    local BearingTTS = string.gsub(Bearing,"0",zero)
    return string.format("%s %s, %d",bullseyename,BearingTTS,Distance)
  else
    return string.format("%s %s, %d",bullseyename,Bearing,Distance)
  end
end

--- [Internal] Change Bullseye string to be TTS friendly,  "Bullseye 021, 16" returns e.g. "Bulls eye 0 2 1. 1 6"
-- @param #AWACS self
-- @param #string Text Input text
-- @return #string BullseyeBRTTS
function AWACS:_ToStringBullsTTS(Text)
  local text = Text
  text=string.gsub(text,"Bullseye","Bulls eye")
  text=string.gsub(text,"%d","%1 ")
  text=string.gsub(text," ," ,".")
  text=string.gsub(text," $","")
  return text
end


--- [Internal] Check if a group has checked in
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to check
-- @return #number ID
-- @return #boolean CheckedIn
-- @return #string CallSign
function AWACS:_GetManagedGrpID(Group)
  if not Group or not Group:IsAlive() then
    self:T(self.lid.."_GetManagedGrpID - Requested Group is not alive!")
    return 0,false,""
  end
  self:T(self.lid.."_GetManagedGrpID for "..Group:GetName())
  local GID = 0
  local Outcome = false
  local CallSign = "Ghost 1"
  local nametocheck = Group:GetName()
  local managedgrps = self.ManagedGrps or {}
  for _,_managed in pairs (managedgrps) do
    local managed = _managed -- #AWACS.ManagedGroup
    if managed.GroupName == nametocheck then
      GID = managed.GID
      Outcome = true
      CallSign = managed.CallSign
    end
  end
  return GID, Outcome, CallSign
end

--- [Internal] AWACS Get TTS compatible callsign
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @param #number GID GID to use
-- @param #boolean IsPlayer Check in player if true
-- @return #string Callsign
function AWACS:_GetCallSign(Group,GID, IsPlayer)
  self:T(self.lid.."_GetCallSign - GID "..tostring(GID))
  
  if GID and type(GID) == "number" and GID > 0 then
    local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    self:T("Saved Callsign for TTS = " .. tostring(managedgroup.CallSign))
    return managedgroup.CallSign
  end
  
  local callsign = "Ghost 1"
  if Group and Group:IsAlive() then
    callsign = Group:GetCustomCallSign(self.callsignshort,self.keepnumber,self.callsignTranslations,self.callsignCustomFunc,self.callsignCustomArgs)
  end  
  return callsign
end

--- [User] Set player callsign options for TTS output. See @{Wrapper.Group#GROUP.GetCustomCallSign}() on how to set customized callsigns.
-- @param #AWACS self
-- @param #boolean ShortCallsign If true, only call out the major flight number
-- @param #boolean Keepnumber If true, keep the **customized callsign** in the #GROUP name as-is, no amendments or numbers.
-- @param #table CallsignTranslations (Optional) Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized.
-- callsigns from playername or group name.
-- @param #func CallsignCustomFunc (Optional) For player names only(!). If given, this function will return the callsign. Needs to take the groupname and the playername as first two arguments.
-- @param #arg ... (Optional) Comma separated arguments to add to the custom function call after groupname and playername.
-- @return #AWACS self
function AWACS:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations,CallsignCustomFunc,...)
  if not ShortCallsign or ShortCallsign == false then
   self.callsignshort = false
  else
   self.callsignshort = true
  end
  self.keepnumber = Keepnumber or false
  self.callsignTranslations = CallsignTranslations
  self.callsignCustomFunc = CallsignCustomFunc
  self.callsignCustomArgs = arg or {}
  return self  
end

--- [Internal] Update contact from cluster data
-- @param #AWACS self
-- @param #number CID Contact ID
-- @return #AWACS self
function AWACS:_UpdateContactFromCluster(CID)
  self:T(self.lid.."_UpdateContactFromCluster CID="..CID)
  
  local existingcontact = self.Contacts:PullByID(CID) -- #AWACS.ManagedContact
  local ContactTable = existingcontact.Cluster.Contacts or {}
  
  local function GetFirstAliveContact(table)
    for _,_contact in pairs (table) do
      local contact = _contact -- Ops.Intel#INTEL.Contact
      if contact and contact.group and contact.group:IsAlive() then
        return contact
      end
    end
    return nil
  end
  
  local NewContact = GetFirstAliveContact(ContactTable)
  
  if NewContact then
    existingcontact.Contact = NewContact
    self.Contacts:Push(existingcontact,existingcontact.CID)
  end
  
  return self
end

--- [Internal] Check merges for Players
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_CheckMerges()
  self:T(self.lid.."_CheckMerges") 
  for _id,_pilot in pairs (self.ManagedGrps) do
    local pilot = _pilot -- #AWACS.ManagedGroup
    if pilot.Group and pilot.Group:IsAlive() then
      local ppos = pilot.Group:GetCoordinate()
      local pcallsign = pilot.CallSign
      self:T(self.lid.."Checking for "..pcallsign)
      if ppos then
        self.Contacts:ForEach(
          function (Contact)
            local contact = Contact -- #AWACS.ManagedContact
            local cpos = contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate()
            local dist = ppos:Get2DDistance(cpos)
            local distnm = UTILS.Round(UTILS.MetersToNM(dist),0)
            if (pilot.IsPlayer or self.debug) and distnm <= 5 then --and ((not contact.MergeCallDone) or (timer.getTime() - contact.MergeCallDone > 30)) then
              --local label = contact.EngagementTag or ""
              --if not contact.MergeCallDone or not string.find(label,pcallsign) then
                self:T(self.lid.."Merged")
                self:_MergedCall(_id)
                --contact.MergeCallDone = true
              --end
            end
            if (pilot.IsPlayer or self.debug) and distnm >5 and distnm <= self.ThreatDistance then 
              self:_ThreatRangeCall(_id,Contact)
            end
            if (pilot.IsPlayer or self.debug) and distnm > self.ThreatDistance and distnm <= self.MeldDistance then 
              self:_MeldRangeCall(_id,Contact)
            end
            if (pilot.IsPlayer or self.debug) and distnm > self.MeldDistance and distnm <= self.TacDistance then 
              self:_TACRangeCall(_id,Contact)
            end
          end
        )      
      end
    end
  end  
  return self
end

--- [Internal] Clean up contacts list
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_CleanUpContacts()
  self:T(self.lid.."_CleanUpContacts")
  
  if self.Contacts:Count() >  0 then
    local deadcontacts = FIFO:New()   
    self.Contacts:ForEach(
      function (Contact)
        local contact = Contact -- #AWACS.ManagedContact
        if not contact.Contact.group:IsAlive() or contact.Target:IsDead() or contact.Target:IsDestroyed() or contact.Target:CountTargets() == 0 then
          deadcontacts:Push(contact,contact.CID)
          self:T("DEAD contact CID="..contact.CID)
        end
      end
    )

    -- announce VANISHED
    if deadcontacts:Count() > 0 and (not self.NoGroupTags) then
    
      self:T("DEAD count="..deadcontacts:Count())
      deadcontacts:ForEach(
      function (Contact) 
        local contact = Contact -- #AWACS.ManagedContact
          local vanished = self.gettext:GetEntry("VANISHED",self.locale)
          local vanishedtts = self.gettext:GetEntry("VANISHEDTTS",self.locale)
          local text = string.format(vanishedtts,self.callsigntxt, contact.TargetGroupNaming)
          local textScreen = string.format(vanished, self.callsigntxt, contact.TargetGroupNaming)
          self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true)
          self.Contacts:PullByID(contact.CID)
       -- end        
      end
      )
      
    end
    
    if self.Contacts:Count() > 0 then
      self.Contacts:ForEach(
        function (Contact)
          local contact = Contact -- #AWACS.ManagedContact 
          self:_UpdateContactFromCluster(contact.CID)
        end
        )
    end
    
    -- cleanup
    deadcontacts:Clear()
   -- aliveclusters:Clear()
    
  end
  return self
end

--- [Internal] Select pilots available for tasking, return AI and Human
-- @param #AWACS self
-- @return #table AIPilots Table of #AWACS.ManagedGroup
-- @return #table HumanPilots Table of #AWACS.ManagedGroup
function AWACS:_GetIdlePilots()
  self:T(self.lid.."_GetIdlePilots")
  local AIPilots = {}
  local HumanPilots = {}
  
  for _name,_entry in pairs (self.ManagedGrps) do
    local entry = _entry -- #AWACS.ManagedGroup
    self:T("Looking at entry "..entry.GID.." Name "..entry.GroupName)
    local managedtask = self:_ReadAssignedTaskFromGID(entry.GID) -- #AWACS.ManagedTask
    local overridetask = false
    if managedtask then
      self:T("Current task = "..(managedtask.ToDo or "Unknown"))
      if managedtask.ToDo == AWACS.TaskDescription.ANCHOR then
        overridetask = true
      end
    end
    if entry.IsAI then
      if entry.FlightGroup:IsAirborne() and ((not entry.HasAssignedTask) or overridetask) then -- must be idle, or?
        self:T("Adding AI with Callsign: "..entry.CallSign)
        AIPilots[#AIPilots+1] = _entry
      end
    elseif entry.IsPlayer and (not entry.Blocked) and (not entry.Group:IsHelicopter()) then
      if (not entry.HasAssignedTask) or overridetask then -- must be idle, or?
        -- check last assignment
        local TNow = timer.getTime()
        if entry.LastTasking and (TNow-entry.LastTasking > self.ReassignTime) then
          self:T("Adding Human with Callsign: "..entry.CallSign)
          HumanPilots[#HumanPilots+1] = _entry
        end
      end
    end
  end
  
  return AIPilots, HumanPilots

end

--- [Internal] Select max 3 targets for picture, bogey dope etc
-- @param #AWACS self
-- @param #boolean Untargeted Return not yet targeted contacts only
-- @return #boolean HaveTargets True if targets could be found, else false
-- @return Utilities.FiFo#FIFO Targetselection
function AWACS:_TargetSelectionProcess(Untargeted)
  self:T(self.lid.."_TargetSelectionProcess")
  
  local maxtargets = 3 -- handleable number of callouts
  local contactstable = self.Contacts:GetDataTable()
  local targettable = FIFO:New()
  local sortedtargets = FIFO:New()
  local prefiltered = FIFO:New() 
  local HaveTargets = false
  
  self:T(self.lid.."Initial count: "..self.Contacts:Count())
  
  -- Bucket sort
   
  if Untargeted then
    -- pre-filter
    self.Contacts:ForEach(
      function (Contact)
        local contact = Contact -- #AWACS.ManagedContact
        if contact.Contact.group:IsAlive() and (contact.Status == AWACS.TaskStatus.IDLE or contact.Status == AWACS.TaskStatus.UNASSIGNED) then
          if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then
            -- filter out VID'd non-hostiles
            if not (contact.IFF == AWACS.IFF.FRIENDLY or contact.IFF == AWACS.IFF.NEUTRAL) then
              prefiltered:Push(contact,contact.CID)
            end
          else
            prefiltered:Push(contact,contact.CID)
          end
        end
      end
    )
    contactstable = prefiltered:GetDataTable()
    self:T(self.lid.."Untargeted: "..prefiltered:Count())
  end
 
  -- Loop through 
  for _,_contact in pairs(contactstable) do
    local contact = _contact -- #AWACS.ManagedContact
    local checked = false
    local contactname = contact.TargetGroupNaming or "ZETA"
    local typename = contact.ReportingName or "Unknown"
    self:T(self.lid..string.format("Looking at group %s type %s",contactname,typename))
    local contactcoord = contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate()
    local contactvec2 = contactcoord:GetVec2()
   
    -- Bucket 0 - NOT in Rejection Zone :)
    if self.RejectZone then
      local isinrejzone = self.RejectZone:IsVec2InZone(contactvec2)
      if isinrejzone then
        self:T(self.lid.."Across Border = YES - ignore")
        checked = true
      end
    end
    -- Bucket 1 - close to AIC (HVT) ca ~45nm
    if not self.GCI then
      local HVTCoordinate = self.OrbitZone:GetCoordinate()
      local distance = UTILS.NMToMeters(200)
      if contactcoord then
        distance = HVTCoordinate:Get2DDistance(contactcoord)
      end
      self:T(self.lid.."HVT Distance = "..UTILS.Round(UTILS.MetersToNM(distance),0))
      if UTILS.MetersToNM(distance) <= 45 and not checked then
        self:T(self.lid.."In HVT Distance = YES")
        targettable:Push(contact,distance)
        checked = true
      end
    end
    
    -- Bucket 2 - in AO/FEZ   
    local isinopszone = self.OpsZone:IsVec2InZone(contactvec2)
    local distance = self.OpsZone:Get2DDistance(contactcoord)
    if isinopszone and not checked then
      self:T(self.lid.."In FEZ = YES")
      targettable:Push(contact,distance)
      checked = true
    end
    
    -- Bucket 3 - in Radar(Control)Zone, < 100nm to AO, Aspect HOT on AO
    local isinopszone = self.ControlZone:IsVec2InZone(contactvec2)
    if isinopszone and not checked then
      self:T(self.lid.."In Radar Zone = YES")
      -- Close to Bulls Eye?
      local distance = self.AOCoordinate:Get2DDistance(contactcoord) -- m
      local AOdist = UTILS.Round(UTILS.MetersToNM(distance),0) -- NM
      if not contactcoord.Heading then
        contactcoord.Heading = self.intel:CalcClusterDirection(contact.Cluster)
      end -- end heading
      local aspect = contactcoord:ToStringAspect(self.ControlZone:GetCoordinate())
      local sizing = contact.Cluster.size or self.intel:ClusterCountUnits(contact.Cluster) or 1
      -- prefer heavy groups
      sizing = math.fmod((sizing * 0.1),1)
      local AOdist2 = (AOdist / 2) * sizing
      AOdist2 = UTILS.Round((AOdist/2)+((AOdist/2)-AOdist2), 0)
      self:T(self.lid.."Aspect = "..aspect.." | Size = "..sizing )
      if (AOdist2 < 75) or (aspect == "Hot") then
        local text = string.format("In AO(Adj) dist = %d(%d) NM",AOdist,AOdist2)
        self:T(self.lid..text)
        targettable:Push(contact,distance)
        checked = true
      end
    end
    
    -- Bucket 4 (if set) within the border polyzone to be defended
    if self.BorderZone then
      local isinborderzone = self.BorderZone:IsVec2InZone(contactvec2)
      if isinborderzone and not checked then
        self:T(self.lid.."In BorderZone = YES")
        targettable:Push(contact,distance)
        checked = true
      end
    end
  end 
  
  self:T(self.lid.."Post filter count: "..targettable:Count())
  
  if targettable:Count() > maxtargets then
    local targets = targettable:GetSortedDataTable()
    targettable:Clear()
    for i=1,maxtargets do
      targettable:Push(targets[i])
    end
  end
  
  sortedtargets:Clear()
  prefiltered:Clear()
  
  if targettable:Count() > 0 then
    HaveTargets = true
  end
  
  return HaveTargets, targettable
end

--- [Internal] AWACS Speak Picture AO/EWR entries
-- @param #AWACS self
-- @param #boolean AO If true this is for AO, else EWR
-- @param #string Callsign Callsign to address
-- @param #number GID GroupID for comms
-- @param #number MaxEntries Max entries to show
-- @param #boolean IsGeneral Is a general picture, address all stations
-- @return #AWACS self
function AWACS:_CreatePicture(AO,Callsign,GID,MaxEntries,IsGeneral)
  self:T(self.lid.."_CreatePicture AO="..tostring(AO).." for "..Callsign.." GID "..GID)
  
  local managedgroup = nil
  local group = nil
  local groupcoord = nil
  
  if not IsGeneral then
    managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    group = managedgroup.Group -- Wrapper.Group#GROUP
    groupcoord = group:GetCoordinate()
  end
  
  local fifo = self.PictureAO -- Utilities.FiFo#FIFO
  
  local maxentries = self.maxspeakentries or 3
  
  if MaxEntries and MaxEntries>0  and MaxEntries <= 3 then
   maxentries = MaxEntries
  end
  
  local counter = 0
  
  if not AO then 
   -- fifo = self.PictureEWR 
  end
  
  local entries = fifo:GetSize()
  
  if entries < maxentries then maxentries = entries end
  
  local text = ""
  local textScreen = ""
  
  -- "<tag> group, BRA <bearing> for <range> at angels <alt/1000>, <aspect>, <shipsize>"
  while counter < maxentries do
    counter = counter + 1
    local contact = fifo:Pull() -- #AWACS.ManagedContact
    self:T({contact})
    if contact and contact.Contact.group and contact.Contact.group:IsAlive() then

      local coordinate = contact.Cluster.coordinate or contact.Contact.position or contact.Contact.group:GetCoordinate() -- Core.Point#COORDINATE
      if not coordinate then
        self:E(self.lid.."NO Coordinate for this cluster! CID="..contact.CID)
        self:E({contact})
        break
      end
      if not coordinate.Heading then
        coordinate.Heading = contact.Contact.heading or contact.Contact.group:GetHeading()
      end
      local refBRAA = ""
      local refBRAATTS = ""
      
      if self.NoGroupTags then
        local grouptxt = self.gettext:GetEntry("GROUPCAP",self.locale)
        text = grouptxt .. "." -- Alpha Group.
        textScreen = grouptxt ..","
      else
        local grouptxt = self.gettext:GetEntry("GROUP",self.locale)
        text = contact.TargetGroupNaming.." "..grouptxt.."." -- Alpha Group.
        textScreen = contact.TargetGroupNaming.." "..grouptxt..","
      end
      
      if IsGeneral or not self.PlayerGuidance then
        local milestxt = self.gettext:GetEntry("MILES",self.locale)
        local thsdtxt = self.gettext:GetEntry("THOUSAND",self.locale)
        refBRAA=self:_ToStringBULLS(coordinate)
        refBRAATTS = self:_ToStringBULLS(coordinate, false, true)
        local alt = contact.Contact.group:GetAltitude() or 8000
        alt = UTILS.Round(UTILS.MetersToFeet(alt)/1000,0)
        -- Alpha Group. Bulls eye 0 2 1, 16 miles, 25 thousand. 
        text = string.format("%s %s %s, %d %s.",text,refBRAATTS,milestxt,alt,thsdtxt) 
        textScreen = string.format("%s %s %s, %d %s.",textScreen,refBRAA,milestxt,alt,thsdtxt)      
      else
        -- pilot reference
        refBRAA = coordinate:ToStringBRAANATO(groupcoord,true,true)
        refBRAATTS = string.gsub(refBRAA,"BRAA","brah")
        refBRAATTS = string.gsub(refBRAATTS,"BRA","brah")
         -- Charlie group, BRAA 045, 105 miles, Angels 41, Flanking, Track North-East, Bogey, Spades.
        if self.PathToGoogleKey then
          refBRAATTS = coordinate:ToStringBRAANATO(groupcoord,true,true,true,false,true)
        end
        if contact.IFF ~= AWACS.IFF.BOGEY then
          local bogey = self.gettext:GetEntry("BOGEY",self.locale)
          refBRAA = string.gsub(refBRAA,bogey, contact.IFF)
          refBRAATTS = string.gsub(refBRAATTS,bogey, contact.IFF)
        end
        text = text .. " "..refBRAATTS
        textScreen = textScreen .." "..refBRAA
      end
      
      -- Aspect
      local aspect = ""
      
      -- sizing
      local size = contact.Contact.group:CountAliveUnits()
      local threatsize, threatsizetext = self:_GetBlurredSize(size)
      
      if threatsize > 1 then  
        text = text.." "..threatsizetext.."." -- Alpha Group. Heavy.
        textScreen = textScreen.." "..threatsizetext.."."
      end 
      
      -- engagement tag?
      if contact.EngagementTag then
        text = text .. " "..contact.EngagementTag -- Alpha Group. Bulls eye 0 2 1, 16. Heavy. Targeted by Jazz 1 1.
        textScreen = textScreen .. " "..contact.EngagementTag -- Alpha Group, Bullseye 021, 16, Flanking. Targeted by Jazz 1 1.
      end
      
      -- Transmit Radio
      local RadioEntry_IsGroup = false
      local RadioEntry_ToScreen = self.debug
      if managedgroup and not IsGeneral then
        RadioEntry_IsGroup = managedgroup.IsPlayer
        RadioEntry_ToScreen = managedgroup.IsPlayer
      end 
      
      self:_NewRadioEntry(text,textScreen,GID,RadioEntry_IsGroup,RadioEntry_ToScreen,true,false)

    end
  end
  
  -- empty queue from leftovers
  fifo:Clear()
 
  return self
end

--- [Internal] AWACS Speak Bogey Dope entries
-- @param #AWACS self
-- @param #string Callsign Callsign to address
-- @param #number GID GroupID for comms
-- @param #boolean Tactical Is for tactical info
-- @return #AWACS self
function AWACS:_CreateBogeyDope(Callsign,GID,Tactical)
  self:T(self.lid.."_CreateBogeyDope for "..Callsign.." GID "..GID)
  
  local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
  local group = managedgroup.Group -- Wrapper.Group#GROUP
  local groupcoord = group:GetCoordinate()
  
  local fifo = self.ContactsAO -- Utilities.FiFo#FIFO
  local maxentries = 1
  local counter = 0
  
  local entries = fifo:GetSize()
  
  if entries < maxentries then maxentries = entries end
  
  local sortedIDs = fifo:GetIDStackSorted() -- sort by distance
  
  while counter < maxentries do
    counter = counter + 1
    local contact = fifo:PullByID(sortedIDs[counter]) -- #AWACS.ManagedContact
    self:T({contact})
    local position = contact.Cluster.coordinate or contact.Contact.position
    if contact and position then
      local tag =  contact.TargetGroupNaming
      local reportingname = contact.ReportingName
      -- DONE - add tag
      self:_AnnounceContact(contact,false,group,true,tag,false,reportingname,Tactical)
    end
  end
  
  -- empty queue from leftovers
  fifo:Clear()
  
  return self
end

--- [Internal] AWACS Menu for Picture
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @param #boolean IsGeneral General picture if true, address no-one specific
-- @return #AWACS self
function AWACS:_Picture(Group,IsGeneral)
  self:T(self.lid.."_Picture")
  local text = ""
  local textScreen = text
  local general = IsGeneral
  local GID, Outcome, gcallsign = self:_GetManagedGrpID(Group) 
  
  if general then
    local allst = self.gettext:GetEntry("ALLSTATIONS",self.locale)
    gcallsign = allst
  end
  
  if Group and Outcome then
    general = false
  end
    
  if not self.intel then
    -- no intel yet!
    local picclean = self.gettext:GetEntry("PICCLEAN",self.locale)
    text = string.format(picclean,gcallsign,self.callsigntxt)
    textScreen = text
    
    self:_NewRadioEntry(text,text,GID,false,true,true,false)

    return self 
  end

  if Outcome or general then
    -- Pilot is checked in
    -- get clusters from Intel  
    local contactstable = self.Contacts:GetDataTable()
    
    -- sort into buckets
    for _,_contact in pairs(contactstable) do
      
      local contact  = _contact -- #AWACS.ManagedContact

      local coordVec2 = contact.Contact.position:GetVec2()        
      
      if self.OpsZone:IsVec2InZone(coordVec2) then
        self.PictureAO:Push(contact)
      elseif self.OrbitZone and self.OrbitZone:IsVec2InZone(coordVec2) then
        self.PictureAO:Push(contact)
      elseif self.ControlZone:IsVec2InZone(coordVec2) then
        local distance = math.floor((contact.Contact.position:Get2DDistance(self.ControlZone:GetCoordinate()) / 1000) + 1) -- km
        self.PictureEWR:Push(contact,distance)
      end
      
    end
    
    local clustersAO = self.PictureAO:GetSize()
    local clustersEWR = self.PictureEWR:GetSize()
    
    if clustersAO < 3 and clustersEWR > 0 then
      -- make sure we have 3, can only add 1, 2 or 3
      local IDstack = self.PictureEWR:GetSortedDataTable()
      -- how many do we need?
      local weneed = 3-clustersAO
      -- do we have enough?
      self:T(string.format("Picture - adding %d/%d contacts from EWR",weneed,clustersEWR))
      if weneed > clustersEWR then
        weneed = clustersEWR
      end
      for i=1,weneed do
        self.PictureAO:Push(IDstack[i])
      end
    end
    
    clustersAO = self.PictureAO:GetSize()
    
    if clustersAO == 0 and clustersEWR == 0 then
      -- clean
      local picclean = self.gettext:GetEntry("PICCLEAN",self.locale)
      text = string.format(picclean,gcallsign,self.callsigntxt)
      textScreen = text
      self:_NewRadioEntry(text,text,GID,Outcome,true,true,false)
    else
    
      if clustersAO > 0 then
        local picture = self.gettext:GetEntry("PICTURE",self.locale)
        text = string.format("%s, %s. %s. ",gcallsign, self.callsigntxt,picture)
        textScreen = string.format("%s, %s. %s. ",gcallsign, self.callsigntxt,picture)
        local onetxt = self.gettext:GetEntry("ONE",self.locale)
        local grptxt = self.gettext:GetEntry("GROUP",self.locale)
        local groupstxt = self.gettext:GetEntry("GROUPMULTI",self.locale)  
        if clustersAO == 1 then
          text = string.format("%s%s %s. ",text,onetxt,grptxt)
          textScreen = string.format("%s%s %s.\n",textScreen,onetxt,grptxt)
        else
          text = string.format("%s%d %s. ",text,clustersAO,groupstxt)
          textScreen = string.format("%s%d %s.\n",textScreen,clustersAO,groupstxt)
        end
        self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false)
        
        self:_CreatePicture(true,gcallsign,GID,3,general)
        
        self.PictureAO:Clear()
        self.PictureEWR:Clear()
      end
    end
    
  elseif self.AwacsFG then
    -- no, unknown
    local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale)
    text = string.format(nocheckin,gcallsign, self.callsigntxt) 
    self:_NewRadioEntry(text,text,GID,Outcome,true,true,false)
  end
  return self
end

--- [Internal] AWACS Menu for Bogey Dope
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @param #boolean Tactical Check for tactical info
-- @return #AWACS self
function AWACS:_BogeyDope(Group,Tactical)
  self:T(self.lid.."_BogeyDope")
  local text = ""
  local textScreen = ""
  local GID, Outcome = self:_GetManagedGrpID(Group)
  local gcallsign = self:_GetCallSign(Group,GID) or "Ghost 1"
    
  if not self.intel then
    -- no intel yet!
    local clean = self.gettext:GetEntry("CLEAN",self.locale)
    text = string.format(clean,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt)
    self:_NewRadioEntry(text,text,0,false,true,true,false,true,Tactical)
    return self 
  end

  if Outcome then
    -- Pilot is checked in
    
    local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    local pilotgroup = managedgroup.Group
    local pilotcoord = managedgroup.Group:GetCoordinate()
    
    local contactstable = self.Contacts:GetDataTable()
    
    -- sort into buckets - AO only for bogey dope!
    for _,_contact in pairs(contactstable) do
      local managedcontact = _contact -- #AWACS.ManagedContact
      local contactposition = managedcontact.Cluster.coordinate or managedcontact.Contact.position -- Core.Point#COORDINATE
      local coordVec2 = contactposition:GetVec2()
      -- Get distance for sorting
      local dist = pilotcoord:Get2DDistance(contactposition)

      if self.ControlZone:IsVec2InZone(coordVec2) then
        self.ContactsAO:Push(managedcontact,dist)
      elseif self.BorderZone and self.BorderZone:IsVec2InZone(coordVec2) then 
       self.ContactsAO:Push(managedcontact,dist)
      else
        if self.OrbitZone then
          local distance = contactposition:Get2DDistance(self.OrbitZone:GetCoordinate())
          if (distance <= UTILS.NMToMeters(45)) then
            self.ContactsAO:Push(managedcontact,distance)
          end
        end
      end     
    end
    
    local contactsAO = self.ContactsAO:GetSize()
    
    if contactsAO == 0 then
      -- clean
      local clean = self.gettext:GetEntry("CLEAN",self.locale)
      text = string.format(clean,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt)
      
      self:_NewRadioEntry(text,text,GID,Outcome,Outcome,true,false,true,Tactical)

    else
    
      if contactsAO > 0 then
        local dope = self.gettext:GetEntry("DOPE",self.locale)
        text = string.format(dope,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt)
        textScreen = string.format(dope,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt)
        local onetxt = self.gettext:GetEntry("ONE",self.locale)
        local grptxt = self.gettext:GetEntry("GROUP",self.locale)
        local groupstxt = self.gettext:GetEntry("GROUPMULTI",self.locale)  
        if contactsAO == 1 then
          text = string.format("%s%s %s. ",text,onetxt,grptxt)
          textScreen = string.format("%s%s %s.\n",textScreen,onetxt,grptxt)
        else
          text = string.format("%s%d %s. ",text,contactsAO,groupstxt)
          textScreen = string.format("%s%d %s.\n",textScreen,contactsAO,groupstxt)
        end
                
        self:_NewRadioEntry(text,textScreen,GID,Outcome,true,true,false,true,Tactical)
        
        self:_CreateBogeyDope(self:_GetCallSign(Group,GID) or "Ghost 1",GID,Tactical)
      end
    end
    
  elseif self.AwacsFG then
    -- no, unknown
    local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale)
    text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) 
    self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,Tactical)

  end
  return self
end

--- [Internal] AWACS Menu for Show Info
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_ShowAwacsInfo(Group)
  self:T(self.lid.."_ShowAwacsInfo")
  local report = REPORT:New("Info")
  local STN = self.STN 
  report:Add("====================")
  report:Add(string.format("AWACS %s",self.callsigntxt))
  report:Add(string.format("Radio: %.3f %s",self.Frequency,UTILS.GetModulationName(self.Modulation)))
  if STN then
    report:Add(string.format("Link-16 STN: %s",STN))
  end
  report:Add(string.format("Bulls Alias: %s",self.AOName))
  report:Add(string.format("Coordinate: %s",self.AOCoordinate:ToStringLLDDM()))
  report:Add("====================")
  report:Add(string.format("Assignment Distance: %d NM",self.maxassigndistance))
  report:Add(string.format("TAC Distance: %d NM",self.TacDistance))
  report:Add(string.format("MELD Distance: %d NM",self.MeldDistance))
  report:Add(string.format("THREAT Distance: %d NM",self.ThreatDistance))
  report:Add("====================")
  report:Add(string.format("ROE/ROT: %s, %s",self.AwacsROE,self.AwacsROT))
  MESSAGE:New(report:Text(),45,"AWACS"):ToGroup(Group)
  return self
end

--- [Internal] AWACS Menu for VID
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @param #string Declaration Text declaration the player used
-- @return #AWACS self
function AWACS:_VID(Group,Declaration)
  self:T(self.lid.."_VID")

  local GID, Outcome, Callsign = self:_GetManagedGrpID(Group)
  local text = ""
  local TextTTS = ""
  
  if Outcome then
    --yes, known
    local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    local group = managedgroup.Group
    local position = group:GetCoordinate()
    local radius = UTILS.NMToMeters(self.DeclareRadius) or UTILS.NMToMeters(5)
    
    -- find tasked contact
    local TID = managedgroup.CurrentTask or 0
    if TID > 0 then
      local task = self.ManagedTasks:ReadByID(TID) -- #AWACS.ManagedTask
      -- correct task?
      if task.ToDo ~= AWACS.TaskDescription.VID then
        return self
      end
      -- already done?
      if task.Status ~= AWACS.TaskStatus.ASSIGNED then
        return self
      end 
      local CID = task.Cluster.CID
      local cluster = self.Contacts:ReadByID(CID) -- #AWACS.ManagedContact
      if cluster then
        local gposition = cluster.Contact.group:GetCoordinate() 
        local cposition = gposition or cluster.Cluster.coordinate or cluster.Contact.position
        local distance = cposition:Get2DDistance(position)
        distance = UTILS.Round(distance,0) + 1
        if distance <= radius or self.debug then
          -- we can VID
          self:T("Contact VID as "..Declaration)
          -- update
          cluster.IFF = Declaration
          task.Status = AWACS.TaskStatus.SUCCESS
          self.ManagedTasks:PullByID(TID)
          self.ManagedTasks:Push(task,TID)
          self.Contacts:PullByID(CID)
          self.Contacts:Push(cluster,CID)
          local vidpos = self.gettext:GetEntry("VIDPOS",self.locale)
          text = string.format(vidpos,Callsign,self.callsigntxt, Declaration)
          self:T(text)
          self:__VIDSuccess(3,GID,group,cluster)
        else
          -- too far away
          self:T("Contact VID not close enough")
          local vidneg = self.gettext:GetEntry("VIDNEG",self.locale)
          text = string.format(vidneg,Callsign,self.callsigntxt)
          self:T(text)
          self:__VIDFailure(3,GID,group,cluster)
        end
        self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true)
      end
    end 
    --
  elseif self.AwacsFG then
    -- no, unknown
    local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale)
    text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) 
    self:_NewRadioEntry(text,text,GID,Outcome,true,true,false)
  end
  return self
end

--- [Internal] AWACS Menu for Declare
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_Declare(Group)
  self:T(self.lid.."_Declare")

  local GID, Outcome, Callsign = self:_GetManagedGrpID(Group)
  local text = ""
  local TextTTS = ""
  
  if Outcome then
    --yes, known
    local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    local group = managedgroup.Group
    local position = group:GetCoordinate()
    local radius = UTILS.NMToMeters(self.DeclareRadius) or UTILS.NMToMeters(5)
    -- find contacts nearby
    local groupzone = ZONE_GROUP:New(group:GetName(),group, radius)
    local Coalitions = {"red","neutral"}
    if self.coalition == coalition.side.NEUTRAL then
      Coalitions = {"red","blue"}
    elseif self.coalition == coalition.side.RED then
      Coalitions = {"blue","neutral"}
    end
    local contactset = SET_GROUP:New():FilterCategoryAirplane():FilterCoalitions(Coalitions):FilterZones({groupzone}):FilterOnce()
    local numbercontacts = contactset:CountAlive() or 0
    local foundcontacts = {}
    if numbercontacts > 0 then
      -- we have some around
      -- sort by distance
      contactset:ForEach(
        function (airpl)
          local distance = position:Get2DDistance(airpl:GetCoordinate())
          distance = UTILS.Round(distance,0) + 1
          foundcontacts[distance] = airpl
        end
      ,{}
      )
      for _dist,_contact in UTILS.spairs(foundcontacts) do
        local distanz = _dist
        local contact = _contact -- Wrapper.Group#GROUP
        local ccoalition = contact:GetCoalition()
        local ctypename = contact:GetTypeName()
        
        local ffneutral = self.gettext:GetEntry("FFNEUTRAL",self.locale)
        local fffriend = self.gettext:GetEntry("FFFRIEND",self.locale)
        local ffhostile = self.gettext:GetEntry("FFHOSTILE",self.locale)
        local ffspades = self.gettext:GetEntry("FFSPADES",self.locale)
        
        local friendorfoe = ffneutral
        if self.self.ModernEra then
          if ccoalition == self.coalition then
            friendorfoe = fffriend
          elseif ccoalition == coalition.side.NEUTRAL then
            friendorfoe = ffneutral
          elseif ccoalition ~= self.coalition then 
            friendorfoe = ffhostile
          end
        else
          friendorfoe = ffspades
        end
        -- see if that works
        self:T(string.format("Distance %d ContactName %s Coalition %d (%s) TypeName %s",distanz,contact:GetName(),ccoalition,friendorfoe,ctypename))
        
        text = string.format("%s. %s. %s.",Callsign,self.callsigntxt,friendorfoe)
        TextTTS = text
        if self.ModernEra then
          text = string.format("%s %s.",text,ctypename)
        end
        break 
      end
    else
      -- clean
      local ffclean = self.gettext:GetEntry("FFCLEAN",self.locale)
      text = string.format("%s. %s. %s.",Callsign,self.callsigntxt,ffclean)
      TextTTS = text
    end   
    self:_NewRadioEntry(TextTTS,text,GID,Outcome,true,true,false,true) 
    --
  elseif self.AwacsFG then
    -- no, unknown
    local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale)
    text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) 
    self:_NewRadioEntry(text,text,GID,Outcome,true,true,false)
  end
  return self
end

--- [Internal] AWACS Menu for Commit
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_Commit(Group)
  self:T(self.lid.."_Commit") 
  local GID, Outcome = self:_GetManagedGrpID(Group)
  local text = ""
  if Outcome then 
    local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    -- Get current task from the group
    local currtaskid = Pilot.CurrentTask
    local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask
    self:T(string.format("TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status))
    if managedtask then
      -- got a task, status?
      if managedtask.Status == AWACS.TaskStatus.REQUESTED then
        -- ok let's commit this one
        managedtask = self.ManagedTasks:PullByID(currtaskid)
        managedtask.Status = AWACS.TaskStatus.ASSIGNED
        self.ManagedTasks:Push(managedtask,currtaskid)
        self:T(string.format("COMMITTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status))
        -- link to Pilot
        Pilot.HasAssignedTask = true
        Pilot.CurrentTask = currtaskid
        self.ManagedGrps[GID] = Pilot
        local copy = self.gettext:GetEntry("COPY",self.locale)
        local targetedby = self.gettext:GetEntry("TARGETEDBY",self.locale)
        text = string.format(copy,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt)
        local EngagementTag = string.format(targetedby,Pilot.CallSign)
        self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.ASSIGNED)
        self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true)
      else
        self:E(self.lid.."Cannot find REQUESTED managed task with TID="..currtaskid.." for GID="..GID)
      end
    else
      self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID)
    end
  elseif self.AwacsFG then
    -- no, unknown
    local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale)
    text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) 
    self:_NewRadioEntry(text,text,GID,Outcome,true,true,false)
  end
  return self
end

--- [Internal] AWACS Menu for Judy
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_Judy(Group)
  self:T(self.lid.."_Judy")
  local GID, Outcome = self:_GetManagedGrpID(Group)
  local text = ""
  if Outcome then 
    local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    -- Get current task from the group
    local currtaskid = Pilot.CurrentTask
    local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask
    if managedtask then
      -- got a task, status?
      if managedtask.Status == AWACS.TaskStatus.REQUESTED or managedtask.Status == AWACS.TaskStatus.UNASSIGNED then
        -- ok let's commit this one
        managedtask = self.ManagedTasks:PullByID(currtaskid)
        managedtask.Status = AWACS.TaskStatus.ASSIGNED
        self.ManagedTasks:Push(managedtask,currtaskid)
        local copy = self.gettext:GetEntry("COPY",self.locale)
        local targetedby = self.gettext:GetEntry("TARGETEDBY",self.locale)
        text = string.format(copy,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt)
        local EngagementTag = string.format(targetedby,Pilot.CallSign)
        self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.ASSIGNED)
        self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true)
      else
        self:E(self.lid.."Cannot find REQUESTED or UNASSIGNED managed task with TID="..currtaskid.." for GID="..GID)
      end
    else
      self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID)
    end
  elseif self.AwacsFG then
    -- no, unknown
    local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale)
    text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) 
     self:_NewRadioEntry(text,text,GID,Outcome,true,true,false)
  end
  return self
end

--- [Internal] AWACS Menu for Unable
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_Unable(Group)
  self:T(self.lid.."_Unable")
  local GID, Outcome = self:_GetManagedGrpID(Group)
  local text = ""
  if Outcome then 
    local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    -- Get current task from the group
    local currtaskid = Pilot.CurrentTask
    local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask
    self:T(string.format("UNABLE for TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status))
    if managedtask then
      -- got a task, status?
      if managedtask.Status == AWACS.TaskStatus.REQUESTED then
        -- ok let's commit this one
        managedtask = self.ManagedTasks:PullByID(currtaskid)
        managedtask.IsUnassigned = true
        managedtask.Status = AWACS.TaskStatus.FAILED
        self.ManagedTasks:Push(managedtask,currtaskid)
        self:T(string.format("REJECTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status))
        -- unlink group from task
        Pilot.HasAssignedTask = false
        Pilot.CurrentTask = 0
        Pilot.LastTasking = timer.getTime()
        self.ManagedGrps[GID] = Pilot
        local copy = self.gettext:GetEntry("COPY",self.locale)
        text = string.format(copy,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt)
        local EngagementTag = ""
        self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.UNASSIGNED)
        self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true)
      else
        self:E(self.lid.."Cannot find REQUESTED managed task with TID="..currtaskid.." for GID="..GID)
      end
    else
      self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID)
    end
  elseif self.AwacsFG then
    -- no, unknown
    local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale)
    text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) 
    self:_NewRadioEntry(text,text,GID,Outcome,true,true,false)
  end
  return self
end

--- [Internal] AWACS Menu for Abort
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_TaskAbort(Group)
  self:T(self.lid.."_TaskAbort")
  local Outcome,GID = self:_GetGIDFromGroupOrName(Group)
  local text = ""
  if Outcome then 
    local Pilot = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    self:T({Pilot})
    -- Get current task from the group
    local currtaskid = Pilot.CurrentTask
    local managedtask = self.ManagedTasks:ReadByID(currtaskid) -- #AWACS.ManagedTask
    if managedtask then
      -- got a task, status?
      self:T(string.format("ABORT for TID %d(%d) | ToDo %s | Status %s",currtaskid,managedtask.TID,managedtask.ToDo,managedtask.Status))
      if managedtask.Status == AWACS.TaskStatus.ASSIGNED then
        -- ok let's un-commit this one
        managedtask = self.ManagedTasks:PullByID(currtaskid)
        managedtask.Status = AWACS.TaskStatus.FAILED
        managedtask.IsUnassigned = true
        self.ManagedTasks:Push(managedtask,currtaskid)
        -- unlink group
        self:T(string.format("ABORTED - TID %d(%d) for GID %d | ToDo %s | Status %s",currtaskid,GID,managedtask.TID,managedtask.ToDo,managedtask.Status))
        -- unlink group from task
        Pilot.HasAssignedTask = false
        Pilot.CurrentTask = 0
        Pilot.LastTasking = timer.getTime()
        self.ManagedGrps[GID] = Pilot
        local copy = self.gettext:GetEntry("COPY",self.locale)
        text = string.format(copy,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt)
        local EngagementTag = ""
        self:_UpdateContactEngagementTag(Pilot.ContactCID,EngagementTag,false,false,AWACS.TaskStatus.UNASSIGNED)
        self:_NewRadioEntry(text,text,GID,Outcome,true,true,false,true)
      else
        self:E(self.lid.."Cannot find ASSIGNED managed task with TID="..currtaskid.." for GID="..GID)
      end
    else
      self:E(self.lid.."Cannot find managed task with TID="..currtaskid.." for GID="..GID)
    end
  elseif self.AwacsFG then
    -- no, unknown
    local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale)
    text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) 
    self:_NewRadioEntry(text,text,GID,Outcome,true,true,false)
  end

  return self
end

--- [Internal] AWACS Menu for Showtask
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_Showtask(Group)
  self:T(self.lid.."_Showtask")

  local GID, Outcome, Callsign = self:_GetManagedGrpID(Group)
  local text = ""
  
  if Outcome then
   -- known group
   
   -- Do we have a task?
   local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
   
   if managedgroup.IsPlayer then

    if managedgroup.CurrentTask >0 and self.ManagedTasks:HasUniqueID(managedgroup.CurrentTask) then
      -- get task structure
      local currenttask = self.ManagedTasks:ReadByID(managedgroup.CurrentTask) -- #AWACS.ManagedTask
      if currenttask then
        local status = currenttask.Status
        local targettype = currenttask.Target:GetCategory()
        local targetstatus = currenttask.Target:GetState()
        local ToDo = currenttask.ToDo
        local description = currenttask.ScreenText
        local descTTS = currenttask.ScreenText
        local callsign = Callsign
        
        if self.debug then
          local taskreport = REPORT:New("AWACS Tasking Display")
          taskreport:Add("===============")
          taskreport:Add(string.format("Task for Callsign: %s",Callsign))
          taskreport:Add(string.format("Task: %s with Status: %s",ToDo,status))
          taskreport:Add(string.format("Target of Type: %s",targettype))
          taskreport:Add(string.format("Target in State: %s",targetstatus))
          taskreport:Add("===============")
          self:I(taskreport:Text())
        end
        
        local pposition = managedgroup.Group:GetCoordinate() or managedgroup.LastKnownPosition
        if currenttask.ToDo == AWACS.TaskDescription.INTERCEPT or currenttask.ToDo == AWACS.TaskDescription.VID then
          local targetpos = currenttask.Target:GetCoordinate()
          if pposition and targetpos then
            local alti = currenttask.Cluster.altitude or currenttask.Contact.altitude or currenttask.Contact.group:GetAltitude()
            local direction, direcTTS = self:_ToStringBRA(pposition,targetpos,alti)
            description = description .. "\nBRA "..direction
            descTTS = descTTS ..";BRA "..direcTTS
          end
        elseif currenttask.ToDo == AWACS.TaskDescription.ANCHOR or currenttask.ToDo == AWACS.TaskDescription.REANCHOR then
          local targetpos = currenttask.Target:GetCoordinate()
          local direction, direcTTS = self:_ToStringBR(pposition,targetpos)
          description = description .. "\nBR "..direction
          descTTS = descTTS .. ";BR "..direcTTS
        end
        local statustxt = self.gettext:GetEntry("STATUS",self.locale)  
        --MESSAGE:New(string.format("%s\n%s %s",description,statustxt,status),30,"AWACS",true):ToGroup(Group)
        local text = string.format("%s\n%s %s",description,statustxt,status)
        local ttstext = string.format("%s. %s. %s",managedgroup.CallSign,self.callsigntxt,descTTS)
        ttstext = string.gsub(ttstext,"\\n",";")
        ttstext = string.gsub(ttstext,"VID","V I D")
        self:_NewRadioEntry(ttstext,text,GID,true,true,false,false,true)
      end
    end
   end
   
  elseif self.AwacsFG then
    -- no, unknown
    local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale)
    text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt) 
    self:_NewRadioEntry(text,text,GID,Outcome,true,true,false)  
  end
  return self
end

--- [Internal] AWACS Menu for Check in
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @return #AWACS self
function AWACS:_CheckIn(Group)
  self:T(self.lid.."_CheckIn "..Group:GetName())
  -- check if already known
  local GID, Outcome = self:_GetManagedGrpID(Group)
  local text = ""
  local textTTS = ""
  if not Outcome then
    self.ManagedGrpID = self.ManagedGrpID + 1
    local managedgroup = {} -- #AWACS.ManagedGroup
      managedgroup.Group = Group
      managedgroup.GroupName = Group:GetName()
      managedgroup.IsPlayer = true
      managedgroup.IsAI = false
      managedgroup.CallSign = self:_GetCallSign(Group,GID,true) or "Ghost 1"
      managedgroup.CurrentAuftrag = 0
      managedgroup.CurrentTask = 0
      managedgroup.HasAssignedTask = true
      managedgroup.Blocked = true
      managedgroup.GID = self.ManagedGrpID
      managedgroup.LastKnownPosition = Group:GetCoordinate()
      managedgroup.LastTasking = timer.getTime()
      
      GID = managedgroup.GID
      self.ManagedGrps[self.ManagedGrpID]=managedgroup
    
    local alphacheckbulls = self:_ToStringBULLS(Group:GetCoordinate())
    local alphacheckbullstts = self:_ToStringBULLS(Group:GetCoordinate(),false,true)
    local alpha = self.gettext:GetEntry("ALPHACHECK",self.locale)
    text = string.format("%s. %s. %s. %s",managedgroup.CallSign,self.callsigntxt,alpha,alphacheckbulls)
    textTTS = string.format("%s. %s. %s. %s",managedgroup.CallSign,self.callsigntxt,alpha,alphacheckbullstts)
   
    self:__CheckedIn(1,managedgroup.GID)
    
    if self.PlayerStationName then
       self:__AssignAnchor(5,managedgroup.GID,true,self.PlayerStationName)
    else    
      self:__AssignAnchor(5,managedgroup.GID)
    end
    
  elseif self.AwacsFG then
    local nocheckin = self.gettext:GetEntry("ALREADYCHECKEDIN",self.locale)
    text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt)
    textTTS = text
  end
  
  self:_NewRadioEntry(textTTS,text,GID,Outcome,true,true,false)
  
  return self
end

--- [Internal] AWACS Menu for CheckInAI
-- @param #AWACS self
-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup to use
-- @param Wrapper.Group#GROUP Group Group to use
-- @param #number AuftragsNr Ops.Auftrag#AUFTRAG.auftragsnummer
-- @return #AWACS self
function AWACS:_CheckInAI(FlightGroup,Group,AuftragsNr)
  self:T(self.lid.."_CheckInAI "..Group:GetName() .. " to Auftrag Nr "..AuftragsNr)
  -- check if already known
  local GID, Outcome = self:_GetManagedGrpID(Group)
  local text = ""
  if not Outcome then
    self.ManagedGrpID = self.ManagedGrpID + 1
    local managedgroup = {} -- #AWACS.ManagedGroup
      managedgroup.Group = Group
      managedgroup.GroupName = Group:GetName()
      managedgroup.FlightGroup = FlightGroup
      managedgroup.IsPlayer = false
      managedgroup.IsAI = true
      local callsignstring = UTILS.GetCallsignName(self.AICAPCAllName)
      if self.callsignTranslations and self.callsignTranslations[callsignstring] then
        callsignstring = self.callsignTranslations[callsignstring]
      end
      local callsignmajor = math.fmod(self.AICAPCAllNumber,9)
      local callsign = string.format("%s %d 1",callsignstring,callsignmajor)
      if self.callsignshort then
        callsign = string.format("%s %d",callsignstring,callsignmajor)
      end
      self:T("Assigned Callsign: ".. callsign)
      managedgroup.CallSign =  callsign
      managedgroup.CurrentAuftrag = AuftragsNr
      managedgroup.HasAssignedTask = false
      managedgroup.GID = self.ManagedGrpID
      managedgroup.LastKnownPosition = Group:GetCoordinate()
    
    self.ManagedGrps[self.ManagedGrpID]=managedgroup
    
    -- SRS voice for CAP   
    FlightGroup:SetDefaultRadio(self.Frequency,self.Modulation,false)
    FlightGroup:SwitchRadio(self.Frequency,self.Modulation)
    
    local CAPVoice = self.CAPVoice
    
    if self.PathToGoogleKey then
      CAPVoice = self.CapVoices[math.floor(math.random(1,10))]
    end
    
    FlightGroup:SetSRS(self.PathToSRS,self.CAPGender,self.CAPCulture,CAPVoice,self.Port,self.PathToGoogleKey,"FLIGHT",1)
    
    local checkai = self.gettext:GetEntry("CHECKINAI",self.locale)
    text = string.format(checkai,self.callsigntxt, managedgroup.CallSign, self.CAPTimeOnStation, self.AOName)
    
    self:_NewRadioEntry(text,text,managedgroup.GID,Outcome,false,true,true)
    
    local alphacheckbulls = self:_ToStringBULLS(Group:GetCoordinate(),false,true)

    local alpha = self.gettext:GetEntry("ALPHACHECK",self.locale)
    text = string.format("%s. %s. %s. %s",managedgroup.CallSign,self.callsigntxt,alpha,alphacheckbulls)
    self:__CheckedIn(1,managedgroup.GID)

    local AW = FlightGroup.legion
    if AW.HasOwnStation then
      self:__AssignAnchor(5,managedgroup.GID,AW.HasOwnStation,AW.StationName)
    else
      self:__AssignAnchor(5,managedgroup.GID)
    end
  else
    local nocheckin = self.gettext:GetEntry("ALREADYCHECKEDIN",self.locale)
    text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt)
  end
  
  self:_NewRadioEntry(text,text,GID,Outcome,false,true,false)
  
  return self
end

--- [Internal] AWACS Menu for Check Out
-- @param #AWACS self
-- @param Wrapper.Group#GROUP Group Group to use
-- @param #number GID GroupID
-- @param #boolean dead If true, group is dead crashed or otherwise n/a
-- @return #AWACS self
function AWACS:_CheckOut(Group,GID,dead)
  self:T(self.lid.."_CheckOut")

  -- check if already known
  local GID, Outcome = self:_GetManagedGrpID(Group)
  local text = ""
  if Outcome then
    -- yes, known
    local safeflight = self.gettext:GetEntry("SAFEFLIGHT",self.locale)
    text = string.format(safeflight,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt)
    self:T(text)
    -- grab some data before we nil the entry
    local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    local Stack = managedgroup.AnchorStackNo
    local Angels = managedgroup.AnchorStackAngels
    local GroupName = managedgroup.GroupName
    -- remove menus
    if managedgroup.IsPlayer then
      if self.clientmenus:HasUniqueID(GroupName) then
        local menus = self.clientmenus:PullByID(GroupName) --#AWACS.MenuStructure
        menus.basemenu:Remove()
        if self.TacticalSubscribers[GroupName] then
          local Freq = self.TacticalSubscribers[GroupName]
          self.TacticalFrequencies[Freq] = Freq
          self.TacticalSubscribers[GroupName] = nil
        end
      end
    end
    -- delete open tasks
    if managedgroup.CurrentTask and managedgroup.CurrentTask > 0 then
      self.ManagedTasks:PullByID(managedgroup.CurrentTask )
      self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false)
    end
    self.ManagedGrps[GID] = nil
    self:__CheckedOut(1,GID,Stack,Angels)
  else
    -- no, unknown
    if not dead then
      local nocheckin = self.gettext:GetEntry("NOTCHECKEDIN",self.locale)
      text = string.format(nocheckin,self:_GetCallSign(Group,GID) or "Ghost 1", self.callsigntxt)
    end
  end
  
  if not dead then
    self:_NewRadioEntry(text,text,GID,Outcome,false,true,false)
  end
  
  return self
end

--- [Internal] AWACS set client menus
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_SetClientMenus()
  self:T(self.lid.."_SetClientMenus")
  local clientset = self.clientset -- Core.Set#SET_CLIENT
  local aliveset = clientset:GetSetObjects() or {}-- #table of #CLIENT objects
  local clientcount = 0
  local clientcheckedin = 0 
  for _,_group in pairs(aliveset) do
    -- go through set and build the menu
    local grp = _group -- Wrapper.Client#CLIENT
    local cgrp = grp:GetGroup()
    local cgrpname = nil
    if cgrp and cgrp:IsAlive() then
      cgrpname = cgrp:GetName()
      self:T(cgrpname)
    end
    if self.MenuStrict then
      -- check if pilot has checked in
      if cgrp and cgrp:IsAlive() then
        clientcount = clientcount + 1
        local GID, checkedin = self:_GetManagedGrpID(cgrp)
        if checkedin then
          -- full menu minus checkin
          clientcheckedin = clientcheckedin + 1
          local hasclientmenu = self.clientmenus:ReadByID(cgrpname) -- #AWACS.MenuStructure
          local basemenu = hasclientmenu.basemenu -- Core.Menu#MENU_GROUP
          
          if hasclientmenu and (not hasclientmenu.menuset) then
          
            self:T(self.lid.."Setting Menus for "..cgrpname)
          
            basemenu:RemoveSubMenus()
            local bogeydope = MENU_GROUP_COMMAND:New(cgrp,"Bogey Dope",basemenu,self._BogeyDope,self,cgrp)
            local picture = MENU_GROUP_COMMAND:New(cgrp,"Picture",basemenu,self._Picture,self,cgrp)
            local declare = MENU_GROUP_COMMAND:New(cgrp,"Declare",basemenu,self._Declare,self,cgrp)
            local tasking = MENU_GROUP:New(cgrp,"Tasking",basemenu)
            local showtask = MENU_GROUP_COMMAND:New(cgrp,"Showtask",tasking,self._Showtask,self,cgrp)
            
            local commit
            local unable
            local abort
            if self.PlayerCapAssignment then
              commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp)
              unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp)
              abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp)
              --local judy = MENU_GROUP_COMMAND:New(cgrp,"Judy",tasking,self._Judy,self,cgrp)
            end
            
            if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then
              local vid = MENU_GROUP:New(cgrp,"VID as",tasking)
              local hostile = MENU_GROUP_COMMAND:New(cgrp,"Hostile",vid,self._VID,self,cgrp,AWACS.IFF.ENEMY)
              local neutral = MENU_GROUP_COMMAND:New(cgrp,"Neutral",vid,self._VID,self,cgrp,AWACS.IFF.NEUTRAL)
              local friendly = MENU_GROUP_COMMAND:New(cgrp,"Friendly",vid,self._VID,self,cgrp,AWACS.IFF.FRIENDLY)
            end
            
            local tactical
            if self.TacticalMenu then
              tactical = MENU_GROUP:New(cgrp,"Tactical Radio",basemenu)
              if self.TacticalSubscribers[cgrpname] then
                -- unsubscribe
                local entry = MENU_GROUP_COMMAND:New(cgrp,"Unsubscribe",tactical,self._UnsubScribeTactRadio,self,cgrp) 
              else
                -- subscribe
                for _,_freq in UTILS.spairs(self.TacticalFrequencies) do
                  local modu = UTILS.GetModulationName(self.TacticalModulation)
                  local text = string.format("Subscribe to %.3f %s",_freq,modu)
                  local entry = MENU_GROUP_COMMAND:New(cgrp,text,tactical,self._SubScribeTactRadio,self,cgrp,_freq) 
                end
              end
            end

            local ainfo = MENU_GROUP_COMMAND:New(cgrp,"Awacs Info",basemenu,self._ShowAwacsInfo,self,cgrp)                
            local checkout = MENU_GROUP_COMMAND:New(cgrp,"Check Out",basemenu,self._CheckOut,self,cgrp)
            
            local menus = { -- #AWACS.MenuStructure
              groupname =  cgrpname,
              menuset = true,
              basemenu = basemenu,
              checkout= checkout,
              picture = picture,
              bogeydope = bogeydope,
              declare = declare,
              tasking = tasking,
              showtask = showtask,
              --judy = judy,
              unable = unable,
              abort = abort,
              commit=commit,
              tactical=tactical,
            }
            self.clientmenus:PullByID(cgrpname)
            self.clientmenus:Push(menus,cgrpname)
          end
        elseif not self.clientmenus:HasUniqueID(cgrpname) then
          -- check in only
          local basemenu = MENU_GROUP:New(cgrp,self.Name,nil)
          local checkin = MENU_GROUP_COMMAND:New(cgrp,"Check In",basemenu,self._CheckIn,self,cgrp)
          checkin:SetTag(cgrp:GetName())
          basemenu:Refresh()         
          local menus = { -- #AWACS.MenuStructure
            groupname =  cgrpname,
            menuset = false,
            basemenu = basemenu,
            checkin = checkin,
          }
          self.clientmenus:Push(menus,cgrpname)
          -- catch errors - when this entry is built we should NOT have a managed entry
          local GID,hasentry = self:_GetManagedGrpID(cgrp)
          if hasentry then
            -- this user is checked in but has the check in entry ... not good.
            self:_CheckOut(cgrp,GID,true)
          end
        end
      end
    else
      if cgrp and cgrp:IsAlive() and not self.clientmenus:HasUniqueID(cgrpname) then
        local basemenu = MENU_GROUP:New(cgrp,self.Name,nil)
        local picture = MENU_GROUP_COMMAND:New(cgrp,"Picture",basemenu,self._Picture,self,cgrp)
        local bogeydope = MENU_GROUP_COMMAND:New(cgrp,"Bogey Dope",basemenu,self._BogeyDope,self,cgrp)
        local declare = MENU_GROUP_COMMAND:New(cgrp,"Declare",basemenu,self._Declare,self,cgrp)
        
        local tasking = MENU_GROUP:New(cgrp,"Tasking",basemenu)
        local showtask = MENU_GROUP_COMMAND:New(cgrp,"Showtask",tasking,self._Showtask,self,cgrp)
        local commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp)
        local unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp)
        local abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp)
        --local judy = MENU_GROUP_COMMAND:New(cgrp,"Judy",tasking,self._Judy,self,cgrp)
        
        if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then
          local vid = MENU_GROUP:New(cgrp,"VID as",tasking)
          local hostile = MENU_GROUP_COMMAND:New(cgrp,"Hostile",vid,self._VID,self,cgrp,AWACS.IFF.ENEMY)
          local neutral = MENU_GROUP_COMMAND:New(cgrp,"Neutral",vid,self._VID,self,cgrp,AWACS.IFF.NEUTRAL)
          local friendly = MENU_GROUP_COMMAND:New(cgrp,"Friendly",vid,self._VID,self,cgrp,AWACS.IFF.FRIENDLY)
        end
        
        local ainfo = MENU_GROUP_COMMAND:New(cgrp,"Awacs Info",basemenu,self._ShowAwacsInfo,self,cgrp)  
        local checkin = MENU_GROUP_COMMAND:New(cgrp,"Check In",basemenu,self._CheckIn,self,cgrp)
        local checkout = MENU_GROUP_COMMAND:New(cgrp,"Check Out",basemenu,self._CheckOut,self,cgrp)
        
        basemenu:Refresh()
        
        local menus = { -- #AWACS.MenuStructure
          groupname =  cgrpname,
          menuset = true,
          basemenu = basemenu,
          checkin = checkin,
          checkout= checkout,
          picture = picture,
          bogeydope = bogeydope,
          declare = declare,
          showtask = showtask,
          tasking = tasking,
          --judy = judy,
          unable = unable,
          abort = abort,
          commit = commit,
        }
        self.clientmenus:Push(menus,cgrpname)
      end
    end
  end

  self.MonitoringData.Players = clientcount or 0
  self.MonitoringData.PlayersCheckedin = clientcheckedin or 0
    
  return self
end

--- [Internal] AWACS Delete a new Anchor Stack from a Marker - only works if no assignments are on the station
-- @param #AWACS self
-- @return #AWACS self 
function AWACS:_DeleteAnchorStackFromMarker(Name,Coord)
  self:T(self.lid.."_DeleteAnchorStackFromMarker")
  if self.AnchorStacks:HasUniqueID(Name) and self.PlayerStationName == Name then
    local stack = self.AnchorStacks:ReadByID(Name) -- #AWACS.AnchorData
    local marker = stack.AnchorMarker
    if stack.AnchorAssignedID:Count() == 0 then
      marker:Remove()
      if self.debug then
        stack.StationZone:UndrawZone()
      end
      self.AnchorStacks:PullByID(Name)
      self.PlayerStationName = nil
    else
      if self.debug then
        self:I(self.lid.."**** Cannot delete station, there are CAPs assigned!")
        local text = marker:GetText()
        marker:TextUpdate(text.."\nMarked for deletion")
      end
    end
  end
  return self
end

--- [Internal] AWACS Move a new Anchor Stack from a Marker
-- @param #AWACS self
-- @return #AWACS self 
function AWACS:_MoveAnchorStackFromMarker(Name,Coord)
  self:T(self.lid.."_MoveAnchorStackFromMarker")
  if self.AnchorStacks:HasUniqueID(Name) and self.PlayerStationName == Name then
    local station = self.AnchorStacks:PullByID(Name) -- #AWACS.AnchorData
    local stationtag = string.format("Station: %s\nCoordinate: %s",Name,Coord:ToStringLLDDM())
    local marker = station.AnchorMarker
    local zone = station.StationZone
    if self.debug then
      zone:UndrawZone()
    end
    local radius = self.StationZone:GetRadius()
    if radius < 10000 then radius = 10000 end
    station.StationZone = ZONE_RADIUS:New(Name, Coord:GetVec2(), radius)
    marker:UpdateCoordinate(Coord)
    marker:UpdateText(stationtag)
    station.AnchorMarker = marker
    if self.debug then
      station.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true)
    end
    self.AnchorStacks:Push(station,Name)
  end
  return self
end

--- [Internal] AWACS Create a new Anchor Stack from a Marker - this then is the preferred station for players
-- @param #AWACS self
-- @return #AWACS self 
function AWACS:_CreateAnchorStackFromMarker(Name,Coord)
  self:T(self.lid.."_CreateAnchorStackFromMarker")
  local AnchorStackOne = {} -- #AWACS.AnchorData
  AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels
  AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO
  AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO
  
  local newname = Name
  
  for i=1,self.AnchorMaxStacks do
    AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels)
  end
  local radius = self.StationZone:GetRadius()
  if radius < 10000 then radius = 10000 end
  AnchorStackOne.StationZone = ZONE_RADIUS:New(newname, Coord:GetVec2(), radius)
  AnchorStackOne.StationZoneCoordinate = Coord
  AnchorStackOne.StationZoneCoordinateText = Coord:ToStringLLDDM()
  AnchorStackOne.StationName = newname
  
  --push to AnchorStacks
  if self.debug then
    AnchorStackOne.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true)
    local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM())
    if self.AllowMarkers then
      AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition)
    end
  else
    local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM())
    if self.AllowMarkers then
      AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition)
    end
  end
  
  self.AnchorStacks:Push(AnchorStackOne,newname)
  self.PlayerStationName = newname
  
  return self
end

--- [Internal] AWACS Create a new Anchor Stack
-- @param #AWACS self
-- @return #boolean success
-- @return #number AnchorStackNo
function AWACS:_CreateAnchorStack()
  self:T(self.lid.."_CreateAnchorStack")
  local stackscreated = self.AnchorStacks:GetSize()
  if stackscreated == self.AnchorMaxAnchors  then
    -- only create self.AnchorMaxAnchors Anchors
    return false, 0
  end
  local AnchorStackOne = {} -- #AWACS.AnchorData
  AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels
  AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO
  AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO
  
  local newname = self.StationZone:GetName()
  
  for i=1,self.AnchorMaxStacks do
    AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels)
  end
  
  if stackscreated == 0 then
    local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1)
    newname = self.StationZone:GetName() .. "-"..newsubname
    AnchorStackOne.StationZone = self.StationZone
    AnchorStackOne.StationZoneCoordinate = self.StationZone:GetCoordinate()
    AnchorStackOne.StationZoneCoordinateText = self.StationZone:GetCoordinate():ToStringLLDDM()
    AnchorStackOne.StationName = newname
    --push to AnchorStacks
    if self.debug then
      --self.AnchorStacks:Flush()
      AnchorStackOne.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true)
      local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM())
      if self.AllowMarkers then
        AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition)
      end
    else
      local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM())
      if self.AllowMarkers then
        AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition)
      end
    end
    self.AnchorStacks:Push(AnchorStackOne,newname)
  else
    local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1)
    newname = self.StationZone:GetName() .. "-"..newsubname
    local anchorbasecoord = self.OpsZone:GetCoordinate() -- Core.Point#COORDINATE
    -- OpsZone can be Polygon, so use distance to StationZone as radius
    local anchorradius = anchorbasecoord:Get2DDistance(self.StationZone:GetCoordinate())
    local angel = self.StationZone:GetCoordinate():GetAngleDegrees(self.OpsZone:GetVec3())
    self:T("Angel Radians= " .. angel)
    local turn = math.fmod(self.AnchorTurn*stackscreated,360) -- #number
    if self.AnchorTurn < 0 then turn = -turn end
    local newanchorbasecoord = anchorbasecoord:Translate(anchorradius,turn+angel) -- Core.Point#COORDINATE
    local radius = self.StationZone:GetRadius()
    if radius < 10000 then radius = 10000 end
    AnchorStackOne.StationZone = ZONE_RADIUS:New(newname, newanchorbasecoord:GetVec2(), radius)
    AnchorStackOne.StationZoneCoordinate = newanchorbasecoord
    AnchorStackOne.StationZoneCoordinateText = newanchorbasecoord:ToStringLLDDM()
    AnchorStackOne.StationName = newname
    --push to AnchorStacks
    if self.debug then
      AnchorStackOne.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true)
      local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM())
      if self.AllowMarkers then
        AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition)
      end
    else
      local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM())
      if self.AllowMarkers then
        AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition)
      end
    end
    self.AnchorStacks:Push(AnchorStackOne,newname)
  end

  return true,self.AnchorStacks:GetSize()
  
end

--- [Internal] AWACS get free anchor stack for managed groups
-- @param #AWACS self
-- @return #number AnchorStackNo
-- @return #boolean free 
function AWACS:_GetFreeAnchorStack()
  self:T(self.lid.."_GetFreeAnchorStack")
  local AnchorStackNo, Free = 0, false
  --return AnchorStackNo, Free
  local availablestacks = self.AnchorStacks:GetPointerStack() or {} -- #table
  for _id,_entry in pairs(availablestacks) do
    local entry = _entry -- Utilities.FiFo#FIFO.IDEntry
    local data = entry.data -- #AWACS.AnchorData
    if data.Anchors:IsNotEmpty() then
      AnchorStackNo = _id
      Free = true
      break
    end
  end
  -- TODO - if extension of anchor stacks to max, send AI home
  if not Free then
    -- try to create another stack
    local created, number = self:_CreateAnchorStack()
    if created then
      -- we could create a new one - phew!
      self:_GetFreeAnchorStack()
    end
  end
  return AnchorStackNo, Free
end

--- [Internal] AWACS Assign Anchor Position to a Group
-- @param #AWACS self
-- @param #number GID Managed Group ID
-- @param #boolean HasOwnStation
-- @param #string StationName
-- @return #AWACS self
function AWACS:_AssignAnchorToID(GID, HasOwnStation, StationName)
  self:T(self.lid.."_AssignAnchorToID")
  if not HasOwnStation then
    local AnchorStackNo, Free = self:_GetFreeAnchorStack()
    if Free then
      -- get the Anchor from the stack
      local Anchor = self.AnchorStacks:PullByPointer(AnchorStackNo) -- #AWACS.AnchorData
      -- pull one free angels
      local freeangels = Anchor.Anchors:Pull()
      -- push GID on anchor
      Anchor.AnchorAssignedID:Push(GID)
      -- push back to AnchorStacks
      self.AnchorStacks:Push(Anchor,Anchor.StationName)
      self:T({Anchor,freeangels})
      self:__AssignedAnchor(5,GID,Anchor,AnchorStackNo,freeangels)
    else
      self:E(self.lid .. "Cannot assign free anchor stack to GID ".. GID .. " Trying again in 10secs.")
      -- try again ...
      self:__AssignAnchor(10,GID)
    end
  else
    local Anchor = self.AnchorStacks:PullByID(StationName) -- #AWACS.AnchorData
    -- pull one free angels
    local freeangels = Anchor.Anchors:Pull() or 25
    -- push GID on anchor
    Anchor.AnchorAssignedID:Push(GID)
    -- push back to AnchorStacks
    self.AnchorStacks:Push(Anchor,StationName)
    self:T({Anchor,freeangels})
    local StackNo = self.AnchorStacks.stackbyid[StationName].pointer
    self:__AssignedAnchor(5,GID,Anchor,StackNo,freeangels)
  end
  return self
end

--- [Internal] Remove GID (group) from Anchor Stack
-- @param #AWACS self
-- @param #AWACS.ManagedGroup.GID ID
-- @param #number AnchorStackNo
-- @param #number Angels
-- @return #AWACS self
function AWACS:_RemoveIDFromAnchor(GID,AnchorStackNo,Angels)
  local gid = GID or 0
  local stack = AnchorStackNo or 0
  local angels = Angels or 0
  local debugstring = string.format("%s_RemoveIDFromAnchor for GID=%d Stack=%d Angels=%d",self.lid,gid,stack,angels)
  self:T(debugstring)
  -- pull correct anchor
  if stack > 0 and angels > 0 then
    local AnchorStackNo = AnchorStackNo or 1
    local Anchor = self.AnchorStacks:ReadByPointer(AnchorStackNo) -- #AWACS.AnchorData
    -- pull GID from stack
    local removedID = Anchor.AnchorAssignedID:PullByID(GID)
    -- push free angels to stack
    Anchor.Anchors:Push(Angels)
  end
  return self
end

--- [Internal] Start INTEL detection when we reach the AWACS Orbit Zone
-- @param #AWACS self
-- @param Wrapper.Group#GROUP awacs
-- @return #AWACS self
function AWACS:_StartIntel(awacs)
  self:T(self.lid.."_StartIntel")
  
  if self.intelstarted then return self end
  
  self.DetectionSet:AddGroup(awacs)

  local intel = INTEL:New(self.DetectionSet,self.coalition,self.callsigntxt)

  intel:SetClusterAnalysis(true,false,false)
  
  local acceptzoneset = SET_ZONE:New()
  acceptzoneset:AddZone(self.ControlZone)
  acceptzoneset:AddZone(self.OpsZone)
  
  if not self.GCI then
    self.OrbitZone:SetRadius(UTILS.NMToMeters(55))
    acceptzoneset:AddZone(self.OrbitZone)
  end
  
  if self.BorderZone then
    acceptzoneset:AddZone(self.BorderZone)
  end
  
  intel:SetAcceptZones(acceptzoneset)
  
  if self.NoHelos then
    intel:SetFilterCategory({Unit.Category.AIRPLANE})
  else
    intel:SetFilterCategory({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER})
  end
  
  -- Callbacks
  local function NewCluster(Cluster)
    self:__NewCluster(5,Cluster)
  end 
  function intel:OnAfterNewCluster(From,Event,To,Cluster)
    NewCluster(Cluster)
  end
  
  local function NewContact(Contact)
    self:__NewContact(5,Contact)
  end 
  function intel:OnAfterNewContact(From,Event,To,Contact)
   NewContact(Contact)
  end
  
  local function LostContact(Contact)
    self:__LostContact(5,Contact)
  end 
  function intel:OnAfterLostContact(From,Event,To,Contact)
    LostContact(Contact)
  end
  
  local function LostCluster(Cluster,Mission)
    self:__LostCluster(5,Cluster,Mission)
  end
  function intel:OnAfterLostCluster(From,Event,To,Cluster,Mission)
    LostCluster(Cluster,Mission)
  end
  
  self.intelstarted = true
  
  intel.statusupdate = -30
  
  intel:__Start(5)
  
  self.intel = intel -- Ops.Intel#INTEL
  return self
end

--- [Internal] Get blurred size of group or cluster
-- @param #AWACS self
-- @param #number size
-- @return #number adjusted size
-- @return #string AWACS.Shipsize entry for size 1..4
function AWACS:_GetBlurredSize(size)
  self:T(self.lid.."_GetBlurredSize")
  local threatsize = 0
  local blur = self.RadarBlur
  local blurmin = 100 - blur
  local blurmax = 100 + blur
  local actblur = math.random(blurmin,blurmax) / 100
  threatsize = math.floor(size * actblur)
  if threatsize == 0 then threatsize = 1 end
  if threatsize then end
  local threatsizetext = AWACS.Shipsize[1]
  if threatsize == 2  then 
    threatsizetext = AWACS.Shipsize[2]
  elseif threatsize == 3 then 
    threatsizetext = AWACS.Shipsize[3]
  elseif threatsize > 3 then
    threatsizetext = AWACS.Shipsize[4] 
  end
  return threatsize, threatsizetext
end

--- [Internal] Get threat level as clear test
-- @param #AWACS self
-- @param #number threatlevel
-- @return #string threattext
function AWACS:_GetThreatLevelText(threatlevel)
  self:T(self.lid.."_GetThreatLevelText")
  local threattext = "GREEN"
  if threatlevel <= AWACS.THREATLEVEL.GREEN then
   threattext = "GREEN"
  elseif threatlevel <= AWACS.THREATLEVEL.AMBER then
   threattext = "AMBER"
  else
    threattext = "RED"
  end
  return threattext
end


--- [Internal] Get BR text for TTS
-- @param #AWACS self
-- @param Core.Point#COORDINATE FromCoordinate
-- @param Core.Point#COORDINATE ToCoordinate
-- @return #string BRText Desired Output (BR) "214, 35 miles"
-- @return #string BRTextTTS Desired Output (BR) "2 1 4, 35 miles"
function AWACS:_ToStringBR(FromCoordinate,ToCoordinate)
  self:T(self.lid.."_ToStringBR")
  local BRText = ""
  local BRTextTTS = ""
  local DirectionVec3 = FromCoordinate:GetDirectionVec3( ToCoordinate )
  local AngleRadians =  FromCoordinate:GetAngleRadians( DirectionVec3 )
  local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) -- degrees
  
  local AngleDegText = string.format("%03d",AngleDegrees) -- 051
  local AngleDegTextTTS = ""
  
  local zero = self.gettext:GetEntry("ZERO",self.locale)
  local miles = self.gettext:GetEntry("MILES",self.locale)
  
  AngleDegText = string.gsub(AngleDegText,"%d","%1 ") -- "0 5 1 "
  AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1"
  
  AngleDegTextTTS = string.gsub(AngleDegText,"0",zero)
  
  local Distance = ToCoordinate:Get2DDistance( FromCoordinate ) --meters
  local distancenm = UTILS.Round(UTILS.MetersToNM(Distance),0)
  
  BRText = string.format("%03d, %d %s",AngleDegrees,distancenm,miles)
  BRTextTTS = string.format("%s, %d %s",AngleDegText,distancenm,miles)
  
  if self.PathToGoogleKey then
    BRTextTTS = string.format("%s, %d %s",AngleDegTextTTS,distancenm,miles)
  end
  
  self:T(BRText,BRTextTTS)
  return BRText,BRTextTTS
end

--- [Internal] Get BRA text for TTS
-- @param #AWACS self
-- @param Core.Point#COORDINATE FromCoordinate
-- @param Core.Point#COORDINATE ToCoordinate
-- @param #number Altitude Altitude in meters
-- @return #string BRText Desired Output (BRA) "214, 35 miles, 20 thousand"
-- @return #string BRTextTTS Desired Output (BRA) "2 1 4, 35 miles, 20 thousand"
function AWACS:_ToStringBRA(FromCoordinate,ToCoordinate,Altitude)
  self:T(self.lid.."_ToStringBRA")
  local BRText = ""
  local BRTextTTS = ""
  local altitude = UTILS.Round(UTILS.MetersToFeet(Altitude)/1000,0)
  local DirectionVec3 = FromCoordinate:GetDirectionVec3( ToCoordinate )
  local AngleRadians =  FromCoordinate:GetAngleRadians( DirectionVec3 )
  local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), 0 ) -- degrees
  
  local AngleDegText = string.format("%03d",AngleDegrees) -- 051
  
  AngleDegText = string.gsub(AngleDegText,"%d","%1 ") -- "0 5 1 "
  AngleDegText = string.gsub(AngleDegText," $","") -- "0 5 1"
  local AngleDegTextTTS = string.gsub(AngleDegText,"0","zero")
  local Distance = ToCoordinate:Get2DDistance( FromCoordinate ) --meters
  local distancenm = UTILS.Round(UTILS.MetersToNM(Distance),0)
  
  local zero = self.gettext:GetEntry("ZERO",self.locale)
  local miles = self.gettext:GetEntry("MILES",self.locale)
  local thsd = self.gettext:GetEntry("THOUSAND",self.locale)
  local vlow = self.gettext:GetEntry("VERYLOW",self.locale)
  
  if altitude >= 1 then
    BRText = string.format("%03d, %d %s, %d %s",AngleDegrees,distancenm,miles,altitude,thsd)
    BRTextTTS = string.format("%s, %d %s, %d %s",AngleDegText,distancenm,miles,altitude,thsd)
    if self.PathToGoogleKey then
      BRTextTTS = string.format("%s, %d %s, %d %s",AngleDegTextTTS,distancenm,miles,altitude,thsd)
    end
  else
    BRText = string.format("%03d, %d %s, %s",AngleDegrees,distancenm,miles,vlow)
    BRTextTTS = string.format("%s, %d %s, %s",AngleDegText,distancenm,miles,vlow)
    if self.PathToGoogleKey then
      BRTextTTS = string.format("%s, %d %s, %s",AngleDegTextTTS,distancenm,miles,vlow)
    end
  end
  self:T(BRText,BRTextTTS)
  return BRText,BRTextTTS
end

--- [Internal] Get BR text for TTS - ie "Rock 214, 24 miles" and TTS "Rock 2 1 4, 24 miles"
-- @param #AWACS self
-- @param Core.Point#COORDINATE clustercoordinate
-- @return #string BRAText
-- @return #string BRATextTTS
function AWACS:_GetBRAfromBullsOrAO(clustercoordinate)
  self:T(self.lid.."_GetBRAfromBullsOrAO")
  local refcoord = self.AOCoordinate -- Core.Point#COORDINATE
  local BRAText = ""
  local BRATextTTS = ""
  -- get BR from AO
  local bullsname = self.AOName or "Rock"
  local stringbr, stringbrtts = self:_ToStringBR(refcoord,clustercoordinate)
  BRAText = string.format("%s %s",bullsname,stringbr)
  BRATextTTS = string.format("%s %s",bullsname,stringbrtts)
  self:T(BRAText,BRATextTTS)
  return BRAText,BRATextTTS
end

--- [Internal] Register Task for Group by GID
-- @param #AWACS self
-- @param #number GroupID ManagedGroup ID
-- @param #AWACS.TaskDescription Description Short Description Task Type
-- @param #string ScreenText Long task description for screen output
-- @param #table Object Object for Ops.Target#TARGET assignment
-- @param #AWACS.TaskStatus TaskStatus Status of this task
-- @param Ops.Auftrag#AUFTRAG Auftrag The Auftrag for this task if any
-- @param Ops.Intel#INTEL.Cluster Cluster Intel Cluster for this task
-- @param Ops.Intel#INTEL.Contact Contact Intel Contact for this task
-- @return #number TID Task ID created
function AWACS:_CreateTaskForGroup(GroupID,Description,ScreenText,Object,TaskStatus,Auftrag,Cluster,Contact)
   self:T(self.lid.."_CreateTaskForGroup "..GroupID .." Description: "..Description)
   
   local managedgroup = self.ManagedGrps[GroupID] -- #AWACS.ManagedGroup
   local task = {} -- #AWACS.ManagedTask
   self.ManagedTaskID = self.ManagedTaskID + 1
   task.TID = self.ManagedTaskID
   task.AssignedGroupID = GroupID
   task.Status = TaskStatus or AWACS.TaskStatus.ASSIGNED
   task.ToDo = Description
   task.Auftrag = Auftrag
   task.Cluster = Cluster
   task.Contact = Contact
   task.IsPlayerTask = managedgroup.IsPlayer
   task.IsUnassigned = TaskStatus == AWACS.TaskStatus.UNASSIGNED and false or true
  -- task.
   if Object and Object:IsInstanceOf("TARGET") then
    task.Target = Object
   else
    task.Target = TARGET:New(Object)
   end
   task.ScreenText = ScreenText
   if Description == AWACS.TaskDescription.ANCHOR or Description == AWACS.TaskDescription.REANCHOR then
    task.Target.Type = TARGET.ObjectType.ZONE
   end
   task.RequestedTimestamp = timer.getTime()
   
   self.ManagedTasks:Push(task,task.TID)

   managedgroup.HasAssignedTask = true
   managedgroup.CurrentTask = task.TID
   --managedgroup.TaskQueue:Push(task.TID)
  
   self:T({managedgroup})
   self.ManagedGrps[GroupID] = managedgroup

   return task.TID 
end 

--- [Internal] Read registered Task for Group by its ID
-- @param #AWACS self
-- @param #number GroupID ManagedGroup ID
-- @return #AWACS.ManagedTask Task or nil if n/e
function AWACS:_ReadAssignedTaskFromGID(GroupID)
   self:T(self.lid.."_GetAssignedTaskFromGID "..GroupID)
   local managedgroup = self.ManagedGrps[GroupID] -- #AWACS.ManagedGroup
   if managedgroup and managedgroup.HasAssignedTask and managedgroup.CurrentTask ~= 0 then
     local TaskID = managedgroup.CurrentTask
     if self.ManagedTasks:HasUniqueID(TaskID) then
      return self.ManagedTasks:ReadByID(TaskID)
     end
   end
   return nil
end

--- [Internal] Read assigned Group from a TaskID
-- @param #AWACS self
-- @param #number TaskID ManagedTask ID
-- @return #AWACS.ManagedGroup Group structure or nil if n/e
function AWACS:_ReadAssignedGroupFromTID(TaskID)
   self:T(self.lid.."_ReadAssignedGroupFromTID "..TaskID)
   if self.ManagedTasks:HasUniqueID(TaskID) then
    local task = self.ManagedTasks:ReadByID(TaskID) -- #AWACS.ManagedTask
    if task and task.AssignedGroupID and task.AssignedGroupID > 0 then
      return self.ManagedGrps[task.AssignedGroupID]
    end
   end
   return nil
end
 
--- [Internal] Create radio entry to tell players that CAP is on station in Anchor
-- @param #AWACS self
-- @param #number GID Group ID 
-- @return #AWACS self
function AWACS:_MessageAIReadyForTasking(GID)
  self:T(self.lid.."_MessageAIReadyForTasking")
  -- obtain group details
  if GID >0  and self.ManagedGrps[GID] then
    local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    local GFCallsign = self:_GetCallSign(managedgroup.Group)
    local aionst = self.gettext:GetEntry("AIONSTATION",self.locale)
    local TextTTS = string.format(aionst,GFCallsign,self.callsigntxt,managedgroup.AnchorStackNo or 1,managedgroup.AnchorStackAngels or 25)
    self:_NewRadioEntry(TextTTS,TextTTS,GID,false,false,true,true)
  end
  return self
end

--- [Internal] Update Contact Tag
-- @param #AWACS self
-- @param #number CID Contact ID
-- @param #string Text Text to be used
-- @param #boolean TAC TAC Call done
-- @param #boolean MELD MELD Call done
-- @param #string TaskStatus Overwrite status with #AWACS.TaskStatus  Status
-- @return #AWACS self
function AWACS:_UpdateContactEngagementTag(CID,Text,TAC,MELD,TaskStatus)
  self:T(self.lid.."_UpdateContactEngagementTag")
  local text = Text or ""
  -- get contact
  local contact = self.Contacts:PullByID(CID) -- #AWACS.ManagedContact
  if contact then
    contact.EngagementTag = text
    contact.TACCallDone = TAC or false
    contact.MeldCallDone = MELD or false
    contact.Status = TaskStatus or AWACS.TaskStatus.UNASSIGNED
    self.Contacts:Push(contact,CID)
  end
  return self
end

--- [Internal] Check available tasks and status
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_CheckTaskQueue()
  self:T(self.lid.."_CheckTaskQueue")
  local opentasks = 0
  local assignedtasks = 0
  
  -- update last known positions
  for _id,_managedgroup in pairs(self.ManagedGrps) do
    local group = _managedgroup -- #AWACS.ManagedGroup
    if group.Group and group.Group:IsAlive() then
      local coordinate = group.Group:GetCoordinate()
      if coordinate then
        local NewCoordinate = COORDINATE:New(0,0,0)
        group.LastKnownPosition = group.LastKnownPosition:UpdateFromCoordinate(coordinate)
        self.ManagedGrps[_id] = group
      end
    end
  end
  
  ----------------------------------------
  -- ANCHOR
  ----------------------------------------
  
  if self.ManagedTasks:IsNotEmpty() then
    opentasks = self.ManagedTasks:GetSize()
    self:T("Assigned Tasks: " .. opentasks)
    local taskstack = self.ManagedTasks:GetPointerStack()
    for _id,_entry in pairs(taskstack) do
      local data = _entry -- Utilities.FiFo#FIFO.IDEntry
      local entry = data.data -- #AWACS.ManagedTask
      local target = entry.Target -- Ops.Target#TARGET
      local description = entry.ToDo
      if description == AWACS.TaskDescription.ANCHOR or description == AWACS.TaskDescription.REANCHOR then
        self:T("Open Task ANCHOR/REANCHOR")
        -- see if we have reached the anchor zone
        local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup
        if managedgroup then
          local group = managedgroup.Group
          if group and group:IsAlive() then
            local groupcoord = group:GetCoordinate()
            local zone = target:GetObject() -- Core.Zone#ZONE
            self:T({zone})
            if group:IsInZone(zone) then
              self:T("Open Task ANCHOR/REANCHOR success for GroupID "..entry.AssignedGroupID)
              -- made it
              target:Stop()
              -- add group to idle stack
              if managedgroup.IsAI then
                -- message AI on station
                self:_MessageAIReadyForTasking(managedgroup.GID)
              end -- end isAI
              managedgroup.HasAssignedTask = false
              self.ManagedGrps[entry.AssignedGroupID] = managedgroup
              -- pull task from OpenTasks
              self.ManagedTasks:PullByID(entry.TID)
            else --inzone
              -- not there yet
              self:T("Open Task ANCHOR/REANCHOR executing for GroupID "..entry.AssignedGroupID)
            end
          else
            -- group dead, pull task
            self.ManagedTasks:PullByID(entry.TID)
          end
        end
      
      ----------------------------------------
      -- INTERCEPT
      ----------------------------------------
        
      elseif description == AWACS.TaskDescription.INTERCEPT then
        -- DONE
        self:T("Open Tasks INTERCEPT")
        local taskstatus = entry.Status
        local targetstatus = entry.Target:GetState()
        
        if taskstatus == AWACS.TaskStatus.UNASSIGNED then
          -- thou shallst not be in this list!      
          self.ManagedTasks:PullByID(entry.TID)
          break
        end
        
        local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup
        
        -- Check ranges for TAC and MELD
        -- postions relative to CAP position
        --[[
        local targetgrp = entry.Contact.group
        local position = entry.Contact.position or entry.Cluster.coordinate
        if targetgrp and targetgrp:IsAlive() and managedgroup then
          if position and managedgroup.Group and managedgroup.Group:IsAlive() then
            local grouposition = managedgroup.Group:GetCoordinate() or managedgroup.Group:GetCoordinate()
            local distance = 1000
            if grouposition then
              distance = grouposition:Get2DDistance(position)
              distance = UTILS.Round(UTILS.MetersToNM(distance),0)
            end        
            self:T("TAC/MELD distance check: "..distance.."NM!")
            if distance <= self.TacDistance and distance >= self.MeldDistance then
              -- TAC distance
              self:T("TAC distance: "..distance.."NM!")
              local Contact = self.Contacts:ReadByID(entry.Contact.CID)
              self:_TACRangeCall(entry.AssignedGroupID,Contact)
            elseif distance <= self.MeldDistance and distance >= self.ThreatDistance then
              -- MELD distance
              self:T("MELD distance: "..distance.."NM!")
              local Contact = self.Contacts:ReadByID(entry.Contact.CID)
              self:_MeldRangeCall(entry.AssignedGroupID,Contact)
            end
          end
        end
        --]]
        
        local auftrag = entry.Auftrag -- Ops.Auftrag#AUFTRAG
        local auftragstatus = "Not Known"
        if auftrag then
          auftragstatus = auftrag:GetState()
        end 
        local text = string.format("ID=%d | Status=%s | TargetState=%s | AuftragState=%s",entry.TID,taskstatus,targetstatus,auftragstatus)
        self:T(text)
        if auftrag then
          if auftrag:IsExecuting() then
            entry.Status = AWACS.TaskStatus.EXECUTING
          elseif auftrag:IsSuccess() then
            entry.Status = AWACS.TaskStatus.SUCCESS
          elseif auftrag:GetState() == AUFTRAG.Status.FAILED then 
            entry.Status = AWACS.TaskStatus.FAILED
          end 
          if targetstatus == "Dead" then
            entry.Status = AWACS.TaskStatus.SUCCESS
          elseif targetstatus == "Alive" and auftrag:IsOver() then
            entry.Status = AWACS.TaskStatus.FAILED
          end
        elseif entry.IsPlayerTask then
          -- Player task
          -- DONE
          if entry.Target:IsDead() or entry.Target:IsDestroyed() or entry.Target:CountTargets() == 0 then
            -- success!
            entry.Status = AWACS.TaskStatus.SUCCESS
          elseif entry.Target:IsAlive() then
            -- still alive
            -- out of zones?
            local targetpos = entry.Target:GetCoordinate()
            -- success == out of our controlled zones
            local outofzones = false
            self.RejectZoneSet:ForEachZone(
              function(Zone,Position)
                local zone = Zone -- Core.Zone#ZONE
                local pos = Position -- Core.Point#VEC2
                if pos and zone:IsVec2InZone(pos) then
                  -- crossed the border
                  outofzones = true
                end
              end,
              targetpos:GetVec2()
            )
            if not outofzones then
              outofzones = true
              self.ZoneSet:ForEachZone(
                function(Zone,Position)
                  local zone = Zone -- Core.Zone#ZONE
                  local pos = Position -- Core.Point#VEC2
                  if pos and zone:IsVec2InZone(pos) then
                    -- in any zone
                    outofzones = false
                  end
                end,
                targetpos:GetVec2()
              )
            end
            if outofzones then
              entry.Status = AWACS.TaskStatus.SUCCESS
            end
          end
        end
        
        if entry.Status == AWACS.TaskStatus.SUCCESS then
          self:T("Open Tasks INTERCEPT success for GroupID "..entry.AssignedGroupID)
          if managedgroup then
          
            self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",true,true,AWACS.TaskStatus.SUCCESS)
            
            managedgroup.HasAssignedTask = false
            managedgroup.ContactCID = 0
            managedgroup.LastTasking = timer.getTime()
            
            if managedgroup.IsAI then
              managedgroup.CurrentAuftrag = 0
            else
              managedgroup.CurrentTask = 0
            end
            
            self.ManagedGrps[entry.AssignedGroupID] = managedgroup
            self.ManagedTasks:PullByID(entry.TID)
            
            self:__InterceptSuccess(1)
            self:__ReAnchor(5,managedgroup.GID)
          end
         
        elseif entry.Status == AWACS.TaskStatus.FAILED then
          self:T("Open Tasks INTERCEPT failed for GroupID "..entry.AssignedGroupID)
          if managedgroup then
            managedgroup.HasAssignedTask = false
            self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED)
            managedgroup.ContactCID = 0
            managedgroup.LastTasking = timer.getTime()
            if managedgroup.IsAI then
              managedgroup.CurrentAuftrag = 0
            else
              managedgroup.CurrentTask = 0
            end
            if managedgroup.IsPlayer then
              entry.IsPlayerTask = false
            end 
            self.ManagedGrps[entry.AssignedGroupID] = managedgroup
            if managedgroup.Group:IsAlive() or (managedgroup.FlightGroup and managedgroup.FlightGroup:IsAlive()) then
              self:__ReAnchor(5,managedgroup.GID)
            end
          end
          -- remove         
          self.ManagedTasks:PullByID(entry.TID)
          self:__InterceptFailure(1)
        
        elseif entry.Status == AWACS.TaskStatus.REQUESTED then
          -- requested - player tasks only!
          self:T("Open Tasks INTERCEPT REQUESTED for GroupID "..entry.AssignedGroupID)
          local created = entry.RequestedTimestamp or timer.getTime() - 120
          local Tnow = timer.getTime()
          local Trunning = (Tnow-created) / 60 -- mins
          local text = string.format("Task TID %s Requested %d minutes ago.",entry.TID,Trunning)
          if Trunning > self.ReassignmentPause then
            -- reassign if player didn't react within 3 mins
            entry.Status = AWACS.TaskStatus.UNASSIGNED
            self.ManagedTasks:PullByID(entry.TID)
          end
          self:T(text)
        end
        
      ----------------------------------------
      -- VID/POLICE
      ----------------------------------------
        
      elseif description == AWACS.TaskDescription.VID then
       -- TODO - how to do this with AI?
       -- humans only ATM
          local managedgroup = self.ManagedGrps[entry.AssignedGroupID] -- #AWACS.ManagedGroup
          -- check we're alive
          if (not managedgroup) or (not managedgroup.Group:IsAlive()) then
            self.ManagedTasks:PullByID(entry.TID)
            return self
          end
          
          -- target dead or out of bounds?
          if entry.Target:IsDead() or entry.Target:IsDestroyed() or entry.Target:CountTargets() == 0 then
            -- success!
            entry.Status = AWACS.TaskStatus.SUCCESS
          elseif entry.Target:IsAlive() then
            -- still alive
            -- out of zones?
            self:T("Checking VID target out of bounds")
            local targetpos = entry.Target:GetCoordinate()
            -- success == out of our controlled zones
            local outofzones = false
            self.RejectZoneSet:ForEachZone(
              function(Zone,Position)
                local zone = Zone -- Core.Zone#ZONE
                local pos = Position -- Core.Point#VEC2
                if pos and zone:IsVec2InZone(pos) then
                  -- crossed the border
                  outofzones = true
                end
              end,
              targetpos:GetVec2()
            )
            if not outofzones then
              outofzones = true
              self.ZoneSet:ForEachZone(
                function(Zone,Position)
                  local zone = Zone -- Core.Zone#ZONE
                  local pos = Position -- Core.Point#VEC2
                  if pos and zone:IsVec2InZone(pos) then
                    -- in any zone
                    outofzones = false
                  end
                end,
                targetpos:GetVec2()
              )
            end
            if outofzones then
              entry.Status = AWACS.TaskStatus.SUCCESS
              self:T("Out of bounds - SUCCESS")
            end
          end
          
          if entry.Status == AWACS.TaskStatus.REQUESTED then
             -- requested - player tasks only!
            self:T("Open Tasks VID REQUESTED for GroupID "..entry.AssignedGroupID)
            local created = entry.RequestedTimestamp or timer.getTime() - 120
            local Tnow = timer.getTime()
            local Trunning = (Tnow-created) / 60 -- mins
            local text = string.format("Task TID %s Requested %d minutes ago.",entry.TID,Trunning)
            if Trunning > self.ReassignmentPause then
              -- reassign if player didn't react within 3 mins
              entry.Status = AWACS.TaskStatus.UNASSIGNED
              self.ManagedTasks:PullByID(entry.TID)
            end
            self:T(text)
          elseif entry.Status == AWACS.TaskStatus.ASSIGNED then
            self:T("Open Tasks VID ASSIGNED for GroupID "..entry.AssignedGroupID)
            -- check TAC/MELD ranges
            --[[
            local targetgrp = entry.Contact.group
            local position = entry.Contact.position or entry.Cluster.coordinate
            if targetgrp and targetgrp:IsAlive() and managedgroup then
              if position and managedgroup.Group and managedgroup.Group:IsAlive() then
                local grouposition = managedgroup.Group:GetCoordinate() or managedgroup.Group:GetCoordinate()
                local distance = 1000
                if grouposition then
                  distance = grouposition:Get2DDistance(position)
                  distance = UTILS.Round(UTILS.MetersToNM(distance),0)
                end        
                self:T("TAC/MELD distance check: "..distance.."NM!")
                if distance <= self.TacDistance and distance >= self.MeldDistance then
                  -- TAC distance
                  self:T("TAC distance: "..distance.."NM!")
                  local Contact = self.Contacts:ReadByID(entry.Contact.CID)
                  self:_TACRangeCall(entry.AssignedGroupID,Contact)
                elseif distance <= self.MeldDistance and distance >= self.ThreatDistance then
                  -- MELD distance
                  self:T("MELD distance: "..distance.."NM!")
                  local Contact = self.Contacts:ReadByID(entry.Contact.CID)
                  self:_MeldRangeCall(entry.AssignedGroupID,Contact)
                end
              end
            end
            --]]
          elseif entry.Status == AWACS.TaskStatus.SUCCESS then
            self:T("Open Tasks VID success for GroupID "..entry.AssignedGroupID)
            -- outcomes - player ID'd
            -- target dead or left zones handled above
            -- target ID'd --> if hostile, assign INTERCEPT TASK
            self.ManagedTasks:PullByID(entry.TID)
            local Contact = self.Contacts:ReadByID(entry.Contact.CID) -- #AWACS.ManagedContact
            if Contact and (Contact.IFF == AWACS.IFF.FRIENDLY or Contact.IFF == AWACS.IFF.NEUTRAL) then
              self:T("IFF outcome friendly/neutral for GroupID "..entry.AssignedGroupID)
              -- nothing todo, re-anchor
              if managedgroup then
                managedgroup.HasAssignedTask = false
                self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED)
                managedgroup.ContactCID = 0
                managedgroup.LastTasking = timer.getTime()
                if managedgroup.IsAI then
                  managedgroup.CurrentAuftrag = 0
                else
                  managedgroup.CurrentTask = 0
                end
                if managedgroup.IsPlayer then
                  entry.IsPlayerTask = false
                end
                self.ManagedGrps[entry.AssignedGroupID] = managedgroup
                self:__ReAnchor(5,managedgroup.GID)
              end
            elseif Contact and Contact.IFF == AWACS.IFF.ENEMY then
              self:T("IFF outcome hostile for GroupID "..entry.AssignedGroupID)
              -- change to intercept
              entry.ToDo = AWACS.TaskDescription.INTERCEPT
              entry.Status = AWACS.TaskStatus.ASSIGNED
              local cname = Contact.TargetGroupNaming
              entry.ScreenText = string.format("Engage hostile %s group.",cname)
              self.ManagedTasks:Push(entry,entry.TID)
              local TextTTS = string.format("%s, %s. Engage hostile target!",managedgroup.CallSign,self.callsigntxt)
              self:_NewRadioEntry(TextTTS,TextTTS,managedgroup.GID,true,self.debug,true,false,true)
            elseif not Contact then
              self:T("IFF outcome target DEAD for GroupID "..entry.AssignedGroupID)
              -- nothing todo, re-anchor
              if managedgroup then
                managedgroup.HasAssignedTask = false
                self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED)
                managedgroup.ContactCID = 0
                managedgroup.LastTasking = timer.getTime()
                if managedgroup.IsAI then
                  managedgroup.CurrentAuftrag = 0
                else
                  managedgroup.CurrentTask = 0
                end
                if managedgroup.IsPlayer then
                  entry.IsPlayerTask = false
                end
                self.ManagedGrps[entry.AssignedGroupID] = managedgroup
                if managedgroup.Group:IsAlive() or managedgroup.FlightGroup:IsAlive() then
                  self:__ReAnchor(5,managedgroup.GID)
                end
              end
            end
          elseif entry.Status == AWACS.TaskStatus.FAILED then
            -- outcomes - player unable/abort
            -- Player dead managed above
            -- Remove task
            self:T("Open Tasks VID failed for GroupID "..entry.AssignedGroupID)
            if managedgroup then
              managedgroup.HasAssignedTask = false
              self:_UpdateContactEngagementTag(managedgroup.ContactCID,"",false,false,AWACS.TaskStatus.UNASSIGNED)
              managedgroup.ContactCID = 0
              managedgroup.LastTasking = timer.getTime()
              if managedgroup.IsAI then
                managedgroup.CurrentAuftrag = 0
              else
                managedgroup.CurrentTask = 0
              end
              if managedgroup.IsPlayer then
                entry.IsPlayerTask = false
              end 
              self.ManagedGrps[entry.AssignedGroupID] = managedgroup
              if managedgroup.Group:IsAlive() or managedgroup.FlightGroup:IsAlive() then
                self:__ReAnchor(5,managedgroup.GID)
              end
            end
            -- remove          
            self.ManagedTasks:PullByID(entry.TID)
            self:__InterceptFailure(1)
          end
      end
      
    end
  end
  
  return self
end

--- [Internal] Write stats to log
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_LogStatistics()
  self:T(self.lid.."_LogStatistics")
  local text = string.gsub(UTILS.OneLineSerialize(self.MonitoringData),",","\n")
  local text = string.gsub(text,"{","\n")
  local text = string.gsub(text,"}","")
  local text = string.gsub(text,"="," = ")
  self:T(text)
  if self.MonitoringOn then
    MESSAGE:New(text,20,"AWACS",false):ToAll()
  end
  return self 
end

--- [User] Add another AirWing for AI CAP Flights under management
-- @param #AWACS self
-- @param Ops.Airwing#AIRWING AirWing The AirWing to (also) obtain CAP flights from
-- @param Core.Zone#ZONE_RADIUS Zone (optional) This AirWing has it's own station zone, AI CAP will be send there
-- @return #AWACS self
function AWACS:AddCAPAirWing(AirWing,Zone)
  self:T(self.lid.."AddCAPAirWing")
  if AirWing then
    AirWing:SetUsingOpsAwacs(self)
    local distance = self.AOCoordinate:Get2DDistance(AirWing:GetCoordinate())
    if Zone then
      -- create AnchorStack
        local stackscreated = self.AnchorStacks:GetSize()
        if stackscreated == self.AnchorMaxAnchors  then
          -- only create self.AnchorMaxAnchors Anchors
          self:E(self.lid.."Max number of stacks already created!")
        else
          local AnchorStackOne = {} -- #AWACS.AnchorData
          AnchorStackOne.AnchorBaseAngels = self.AnchorBaseAngels
          AnchorStackOne.Anchors = FIFO:New() -- Utilities.FiFo#FIFO
          AnchorStackOne.AnchorAssignedID = FIFO:New() -- Utilities.FiFo#FIFO
          
          local newname = Zone:GetName()
                    
          for i=1,self.AnchorMaxStacks do
            AnchorStackOne.Anchors:Push((i-1)*self.AnchorStackDistance+self.AnchorBaseAngels)
          end
          
          local newsubname = AWACS.AnchorNames[stackscreated+1] or tostring(stackscreated+1)
          newname = Zone:GetName() .. "-"..newsubname
          AnchorStackOne.StationZone = Zone
          AnchorStackOne.StationZoneCoordinate = Zone:GetCoordinate()
          AnchorStackOne.StationZoneCoordinateText = Zone:GetCoordinate():ToStringLLDDM()
          AnchorStackOne.StationName = newname
          --push to AnchorStacks
          if self.debug then
            AnchorStackOne.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true)
            local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM())
            if self.AllowMarkers then
              AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition)
            end
          else
            local stationtag = string.format("Station: %s\nCoordinate: %s",newname,self.StationZone:GetCoordinate():ToStringLLDDM())
            if self.AllowMarkers then
              AnchorStackOne.AnchorMarker=MARKER:New(AnchorStackOne.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition)
            end
          end
          self.AnchorStacks:Push(AnchorStackOne,newname)
          AirWing.HasOwnStation = true
          AirWing.StationName = newname
        end
    end
    self.CAPAirwings:Push(AirWing,distance)
  end
  return self
end

--- [Internal] Announce a new contact
-- @param #AWACS self
-- @param #AWACS.ManagedContact Contact
-- @param #boolean IsNew Is a new contact
-- @param Wrapper.Group#GROUP Group Announce to Group if not nil
-- @param #boolean IsBogeyDope If true, this is a bogey dope announcement
-- @param #string Tag Tag name for this contact. Alpha, Brave, Charlie ... 
-- @param #boolean IsPopup This is a pop-up group
-- @param #string ReportingName The NATO code reporting name for the contact, e.g. "Foxbat". "Bogey" if unknown.
-- @param #boolean Tactical
-- @return #AWACS self
function AWACS:_AnnounceContact(Contact,IsNew,Group,IsBogeyDope,Tag,IsPopup,ReportingName,Tactical)
  self:T(self.lid.."_AnnounceContact")
  -- do we have a group to talk to?
  local tag = ""
  local Tag = Tag
  local CID = 0
  if not Tag then
    -- injected data available?
    CID = Contact.CID or 0
    Tag = Contact.TargetGroupNaming or ""
  end
  if self.NoGroupTags then
    Tag = nil
  end
  local isGroup = false
  local GID = 0
  local grpcallsign = "Ghost 1"
  if Group and Group:IsAlive() then
    GID, isGroup,grpcallsign = self:_GetManagedGrpID(Group)
    self:T("GID="..GID.." CheckedIn = "..tostring(isGroup))
  end

  local cluster = Contact.Cluster
  local intel = self.intel -- Ops.Intel#INTEL
  
  local size = self.intel:ClusterCountUnits(cluster)
  local threatsize, threatsizetext = self:_GetBlurredSize(size)

  local clustercoordinate = Contact.Cluster.coordinate or Contact.Contact.position
  
  local heading = Contact.Contact.group:GetHeading() or self.intel:CalcClusterDirection(cluster)
  
  clustercoordinate:SetHeading(Contact.Contact.group:GetHeading())
  
  local BRAfromBulls, BRAfromBullsTTS = self:_GetBRAfromBullsOrAO(clustercoordinate)
   
  self:T(BRAfromBulls)
  self:T(BRAfromBullsTTS)
  BRAfromBulls=BRAfromBulls.."."
  BRAfromBullsTTS=BRAfromBullsTTS.."."

  if isGroup then
    BRAfromBulls = clustercoordinate:ToStringBRAANATO(Group:GetCoordinate(),true,true)
    BRAfromBullsTTS = string.gsub(BRAfromBulls,"BRAA","brah")
    BRAfromBullsTTS = string.gsub(BRAfromBullsTTS,"BRA","brah")
    if self.PathToGoogleKey then
      BRAfromBullsTTS = clustercoordinate:ToStringBRAANATO(Group:GetCoordinate(),true,true,true,false,true)
    end
  end
  
  local BRAText = ""
  local TextScreen = ""
  
  if isGroup then
    BRAText = string.format("%s, %s.",grpcallsign,self.callsigntxt)
    TextScreen = string.format("%s, %s.",grpcallsign,self.callsigntxt)
  else
    BRAText = string.format("%s.",self.callsigntxt)
    TextScreen = string.format("%s.",self.callsigntxt)
  end
  
  local newgrp = self.gettext:GetEntry("NEWGROUP",self.locale)
  local grptxt = self.gettext:GetEntry("GROUP",self.locale)
  local GRPtxt = self.gettext:GetEntry("GROUPCAP",self.locale)
  local popup = self.gettext:GetEntry("POPUP",self.locale)
  
  if IsNew and self.PlayerGuidance then
    BRAText = string.format("%s %s.",BRAText,newgrp)
    TextScreen = string.format("%s %s.",TextScreen,newgrp)
  elseif IsPopup then
    BRAText = string.format("%s %s %s.",BRAText,popup,grptxt)
    TextScreen = string.format("%s %s %s.",TextScreen,popup,grptxt)
  elseif IsBogeyDope and Tag and Tag ~= "" then
    BRAText = string.format("%s %s %s.",BRAText,Tag,grptxt)
    TextScreen = string.format("%s %s %s.",TextScreen,Tag,grptxt)
  else
    BRAText = string.format("%s %s.",BRAText,GRPtxt)
    TextScreen = string.format("%s %s.",TextScreen,GRPtxt)
  end
  
  if not IsBogeyDope then
    if Tag and Tag ~= "" then
      BRAText = BRAText .. " "..Tag.."."
      TextScreen = TextScreen .. " "..Tag.."."
    end
  end
  
  if threatsize > 1 then
    BRAText = BRAText .. " "..BRAfromBullsTTS.." "..threatsizetext.."."
    TextScreen = TextScreen .. " "..BRAfromBulls.." "..threatsizetext.."."
  else
    BRAText = BRAText .. " "..BRAfromBullsTTS
    TextScreen = TextScreen .. " "..BRAfromBulls
  end
  
  if self.ModernEra then
    local high = self.gettext:GetEntry("HIGH",self.locale)
    local vfast = self.gettext:GetEntry("VERYFAST",self.locale)
    local fast = self.gettext:GetEntry("FAST",self.locale)
    -- Platform
    if ReportingName and ReportingName ~= "Bogey" then
      ReportingName = string.gsub(ReportingName,"_"," ")
      BRAText = BRAText .. " "..ReportingName.."."
      TextScreen = TextScreen .. " "..ReportingName.."."
    end
    -- High - > 40k feet
    local height = Contact.Contact.group:GetHeight()
    local height = UTILS.Round(UTILS.MetersToFeet(height)/1000,0) -- e.g, 25
    if height >= 40 then
      BRAText = BRAText .. high
      TextScreen = TextScreen .. high
    end
    -- Fast (>600kn) or Very fast (>900kn)
    local speed = Contact.Contact.group:GetVelocityKNOTS()
    if speed > 900 then
      BRAText = BRAText .. vfast
      TextScreen = TextScreen .. vfast
    elseif speed >= 600 and speed <= 900 then
      BRAText = BRAText .. fast
      TextScreen = TextScreen .. fast
    end
  end
  
  BRAText = string.gsub(BRAText,"BRAA","brah")
  BRAText = string.gsub(BRAText,"BRA","brah")
  
  local prio = IsNew or IsBogeyDope
  self:_NewRadioEntry(BRAText,TextScreen,GID,isGroup,true,IsNew,false,prio,Tactical)

  return self
end

--- [Internal] Check for alive OpsGroup from Mission OpsGroups table
-- @param #AWACS self
-- @param #table OpsGroups
-- @return Ops.OpsGroup#OPSGROUP or nil
function AWACS:_GetAliveOpsGroupFromTable(OpsGroups)
  self:T(self.lid.."_GetAliveOpsGroupFromTable")
  local handback = nil 
  for _,_OG in pairs(OpsGroups or {}) do
    local OG = _OG -- Ops.OpsGroup#OPSGROUP
    if OG and OG:IsAlive() then
      handback = OG
      break
    end
  end 
  return handback
end

--- [Internal] Clean up mission stack
-- @param #AWACS self
-- @return #number CAPMissions
-- @return #number Alert5Missions
-- @return #number InterceptMissions
function AWACS:_CleanUpAIMissionStack()
  self:T(self.lid.."_CleanUpAIMissionStack")
  
  local CAPMissions = 0
  local Alert5Missions = 0
  local InterceptMissions = 0
  
  local MissionStack = FIFO:New()
  
  self:T("Checking MissionStack")
  for _,_mission in pairs(self.CatchAllMissions) do
    -- looking for missions of type CAP and ALERT5
    local mission = _mission -- Ops.Auftrag#AUFTRAG
    local type = mission:GetType()
    if type == AUFTRAG.Type.ALERT5 and mission:IsNotOver() then
      MissionStack:Push(mission,mission.auftragsnummer)
      Alert5Missions = Alert5Missions + 1
    elseif type == AUFTRAG.Type.CAP and mission:IsNotOver() then
      MissionStack:Push(mission,mission.auftragsnummer)
      CAPMissions = CAPMissions + 1
    elseif type == AUFTRAG.Type.INTERCEPT and mission:IsNotOver() then
      MissionStack:Push(mission,mission.auftragsnummer)
      InterceptMissions = InterceptMissions + 1
    end
  end
  
  self.AICAPMissions = nil
  self.AICAPMissions = MissionStack
  
  return CAPMissions, Alert5Missions, InterceptMissions
  
end

function AWACS:_ConsistencyCheck()
  self:T(self.lid.."_ConsistencyCheck")
  if self.debug then
    self:T("CatchAllMissions")
    local catchallm = {}
    local report1 = REPORT:New("CatchAll")
    report1:Add("====================")
    report1:Add("CatchAllMissions")
    report1:Add("====================")
    for _,_mission in pairs(self.CatchAllMissions) do
      local mission = _mission -- Ops.Auftrag#AUFTRAG
      local nummer = mission.auftragsnummer or 0
      local type = mission:GetType()
      local state = mission:GetState()
      local FG = mission:GetOpsGroups()
      local OG = self:_GetAliveOpsGroupFromTable(FG)
      local OGName = "UnknownFromMission"
      if OG then
        OGName=OG:GetName()
      end
      report1:Add(string.format("Auftrag Nr %d Type %s State %s FlightGroup %s",nummer,type,state,OGName))
      if mission:IsNotOver() then
        catchallm[#catchallm+1] = mission
      end
    end
    
    self.CatchAllMissions = nil
    self.CatchAllMissions = catchallm
    
    local catchallfg = {}
    
    self:T("CatchAllFGs")
    report1:Add("====================")
    report1:Add("CatchAllFGs")
    report1:Add("====================")
    for _,_fg in pairs(self.CatchAllFGs) do
      local FG = _fg -- Ops.FlightGroup#FLIGHTGROUP
      local mission = FG:GetMissionCurrent()
      local OGName = FG:GetName() or "UnknownFromFG"
      local nummer = 0
      local type = "No Type"
      local state = "None"
      if mission then
        type = mission:GetType()
        nummer = mission.auftragsnummer or 0
        state = mission:GetState()
      end
      report1:Add(string.format("Auftrag Nr %d Type %s State %s FlightGroup %s",nummer,type,state,OGName))
      if FG:IsAlive() then
        catchallfg[#catchallfg+1] = FG
      end
    end
    report1:Add("====================")
    self:T(report1:Text())
    
    self.CatchAllFGs = nil
    self.CatchAllFGs = catchallfg
    
  end
  return self
end

--- [Internal] Check Enough AI CAP on Station
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_CheckAICAPOnStation()
  self:T(self.lid.."_CheckAICAPOnStation")
  
  self:_ConsistencyCheck()
  
  local capmissions, alert5missions, interceptmissions = self:_CleanUpAIMissionStack()
  self:T("CAP="..capmissions.." ALERT5="..alert5missions.." Requested="..self.AIRequested)
  
  if self.MaxAIonCAP > 0 then
    
    local onstation = capmissions + alert5missions
    
    if capmissions > self.MaxAIonCAP then
      -- too many, send one home
      self:T(string.format("*** Onstation %d > MaxAIOnCAP %d",onstation,self.MaxAIonCAP))
      local mission = self.AICAPMissions:Pull() -- Ops.Auftrag#AUFTRAG
      local Groups = mission:GetOpsGroups()
      local OpsGroup = self:_GetAliveOpsGroupFromTable(Groups)
      local GID,checkedin = self:_GetManagedGrpID(OpsGroup)
      mission:__Cancel(30)
      self.AIRequested = self.AIRequested - 1
      if checkedin then
        self:_CheckOut(OpsGroup,GID)
      end
    end
    
    -- control number of AI CAP Flights
    if capmissions < self.MaxAIonCAP and alert5missions < self.MaxAIonCAP+2 then
      -- not enough
      local AnchorStackNo,free = self:_GetFreeAnchorStack()
      if free then
        -- create Alert5 and assign to ONE of our AWs
        -- TODO better selection due to resource shortage?
        local mission = AUFTRAG:NewALERT5(AUFTRAG.Type.CAP)
        self.CatchAllMissions[#self.CatchAllMissions+1] = mission
        local availableAWS = self.CAPAirwings:Count()
        local AWS = self.CAPAirwings:GetDataTable()
        -- round robin
        self.AIRequested = self.AIRequested + 1
        local selectedAW = AWS[(((self.AIRequested-1) % availableAWS)+1)]
        selectedAW:AddMission(mission)    
        self:T("CAP="..capmissions.." ALERT5="..alert5missions.." Requested="..self.AIRequested)
      end
    end
    
    -- Check CAP Mission states
    if onstation > 0 and capmissions < self.MaxAIonCAP then
      local missions = self.AICAPMissions:GetDataTable()
      -- get mission type and state
      for _,_Mission in pairs(missions) do

        local mission = _Mission -- Ops.Auftrag#AUFTRAG
        self:T("Looking at AuftragsNr " .. mission.auftragsnummer)
        local type = mission:GetType()
        local state = mission:GetState()

        if type == AUFTRAG.Type.ALERT5 then
          -- parked up for CAP
          local OpsGroups = mission:GetOpsGroups()
          local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups)
          local FGstate = mission:GetGroupStatus(OpsGroup)
          if OpsGroup then
             FGstate = OpsGroup:GetState()
             self:T("FG Object in state: " .. FGstate)
          end
          -- FG ready?

          if OpsGroup and (FGstate == "Parking" or FGstate == "Cruising") then
            -- has this group checked in already? Avoid double tasking
            local GID, CheckedInAlready = self:_GetManagedGrpID(OpsGroup:GetGroup())
            if not CheckedInAlready then
              self:_SetAIROE(OpsGroup,OpsGroup:GetGroup())
              self:_CheckInAI(OpsGroup,OpsGroup:GetGroup(),mission.auftragsnummer)
            end
          end
        end
      end
    end
    
    -- cycle mission status
    if onstation > 0 then
      local report = REPORT:New("CAP Mission Status")
      report:Add("===============")
      --local missionIDs = self.AICAPMissions:GetIDStackSorted()
      local missions = self.AICAPMissions:GetDataTable()
      local i = 1
      for _,_Mission in pairs(missions) do 
        local mission = _Mission -- Ops.Auftrag#AUFTRAG
        if mission then
          i = i + 1
          report:Add(string.format("Entry %d",i))
          report:Add(string.format("Mission No %d",mission.auftragsnummer))
          report:Add(string.format("Mission Type %s",mission:GetType()))
          report:Add(string.format("Mission State %s",mission:GetState()))
          local OpsGroups = mission:GetOpsGroups()
          local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP
          if OpsGroup then
            local OpsName = OpsGroup:GetName() or "Unknown"
            local found,GID,OpsCallSign = self:_GetGIDFromGroupOrName(OpsGroup)
            report:Add(string.format("Mission FG %s",OpsName))
            report:Add(string.format("Callsign %s",OpsCallSign))
            report:Add(string.format("Mission FG State %s",OpsGroup:GetState()))
          else
            report:Add("***** Cannot obtain (yet) this missions OpsGroup!")
          end
          report:Add(string.format("Target Type %s",mission:GetTargetType()))
        end
        report:Add("===============") 
      end
      if self.debug then
        self:I(report:Text())
      end    
    end
  end
  return self
end

--- [Internal] Set ROE for AI CAP
-- @param #AWACS self
-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup
-- @param Wrapper.Group#GROUP Group
-- @return #AWACS self
function AWACS:_SetAIROE(FlightGroup,Group)
  self:T(self.lid.."_SetAIROE")
  local ROE = self.AwacsROE or AWACS.ROE.POLICE
  local ROT = self.AwacsROT or AWACS.ROT.PASSIVE
  
  -- TODO adjust to AWACS set ROE
  -- for the time being set to be defensive
  Group:OptionAlarmStateGreen()
  Group:OptionECM_OnlyLockByRadar()
  Group:OptionROEHoldFire()
  Group:OptionROTEvadeFire()
  Group:OptionRTBBingoFuel(true)
  Group:OptionKeepWeaponsOnThreat()
  local callname = self.AICAPCAllName or CALLSIGN.Aircraft.Colt
  self.AICAPCAllNumber = self.AICAPCAllNumber + 1 
  Group:CommandSetCallsign(callname,math.fmod(self.AICAPCAllNumber,9))
  -- FG level
  FlightGroup:SetDefaultAlarmstate(AI.Option.Ground.val.ALARM_STATE.GREEN)
  FlightGroup:SetDefaultCallsign(callname,math.fmod(self.AICAPCAllNumber,9))
  if ROE == AWACS.ROE.POLICE or ROE == AWACS.ROE.VID then
    FlightGroup:SetDefaultROE(ENUMS.ROE.WeaponHold)
  elseif ROE == AWACS.ROE.IFF then
    FlightGroup:SetDefaultROE(ENUMS.ROE.ReturnFire)
  elseif ROE == AWACS.ROE.BVR then
    FlightGroup:SetDefaultROE(ENUMS.ROE.OpenFire)
  end
  if ROT == AWACS.ROT.BYPASSESCAPE or ROT == AWACS.ROT.PASSIVE then
    FlightGroup:SetDefaultROT(ENUMS.ROT.PassiveDefense)
  elseif ROT == AWACS.ROT.OPENFIRE or ROT == AWACS.ROT.RETURNFIRE then
    FlightGroup:SetDefaultROT(ENUMS.ROT.BypassAndEscape)
  elseif ROT == AWACS.ROT.EVADE then
    FlightGroup:SetDefaultROT(ENUMS.ROT.EvadeFire)
  end
  FlightGroup:SetFuelLowRTB(true)
  FlightGroup:SetFuelLowThreshold(0.2)
  FlightGroup:SetEngageDetectedOff()
  FlightGroup:SetOutOfAAMRTB(true)
  return self
end

--- [Internal] TAC Range Call to Pilot
-- @param #AWACS self
-- @param #number GID GID
-- @param #AWACS.ManagedContact Contact
-- @return #AWACS self
function AWACS:_TACRangeCall(GID,Contact)
  self:T(self.lid.."_TACRangeCall")
  -- AIC: “Enforcer 11, single group, 30 miles.”
  if not Contact then return self end
  local pilotcallsign = self:_GetCallSign(nil,GID) 
  local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
  local contact = Contact.Contact -- Ops.Intel#INTEL.Contact
  local contacttag = Contact.TargetGroupNaming
  local name = managedgroup.GroupName
  if contact then --and not Contact.TACCallDone then
    local position = contact.position -- Core.Point#COORDINATE
    if position then     
      local distance = position:Get2DDistance(managedgroup.Group:GetCoordinate())
      distance = UTILS.Round(UTILS.MetersToNM(distance)) -- 30nm - hopefully
      local grptxt = self.gettext:GetEntry("GROUP",self.locale)
      local miles = self.gettext:GetEntry("MILES",self.locale)
      local text = string.format("%s. %s. %s %s, %d %s.",self.callsigntxt,pilotcallsign,contacttag,grptxt,distance,miles)
      if not self.TacticalSubscribers[name] then
        self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true)
      end
      self:_UpdateContactEngagementTag(Contact.CID,Contact.EngagementTag,true,false,AWACS.TaskStatus.EXECUTING)
      if GID and GID ~= 0 then
        --local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
        if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then
          if self.TacticalSubscribers[name] then
            self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true,true)
          end
        end
      end
    end
  end
  return self
end

--- [Internal] Meld Range Call to Pilot
-- @param #AWACS self
-- @param #number GID GID
-- @param #AWACS.ManagedContact Contact
-- @return #AWACS self
function AWACS:_MeldRangeCall(GID,Contact)
  self:T(self.lid.."_MeldRangeCall")
  if not Contact then return self end
  -- AIC: “Heat 11, single group, BRAA 089/28, 32 thousand, hot, hostile, crow.”
  local pilotcallsign = self:_GetCallSign(nil,GID) 
  local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
  local flightpos = managedgroup.Group:GetCoordinate()
  local contact = Contact.Contact -- Ops.Intel#INTEL.Contact
  local contacttag = Contact.TargetGroupNaming or "Bogey"
  local name = managedgroup.GroupName
  if contact then --and not Contact.MeldCallDone then
    local position = contact.position -- Core.Point#COORDINATE
    if position then
      local BRATExt = ""
      if self.PathToGoogleKey then     
        BRATExt = position:ToStringBRAANATO(flightpos,false,false,true,false,true)
      else
        BRATExt = position:ToStringBRAANATO(flightpos,false,false)
      end
      local grptxt = self.gettext:GetEntry("GROUP",self.locale)
      local text = string.format("%s. %s. %s %s, %s",self.callsigntxt,pilotcallsign,contacttag,grptxt,BRATExt)
      if not self.TacticalSubscribers[name] then
        self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true)
      end
      self:_UpdateContactEngagementTag(Contact.CID,Contact.EngagementTag,true,true,AWACS.TaskStatus.EXECUTING)
      if GID and GID ~= 0 then
        --local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
        if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then
          local name = managedgroup.GroupName
          if self.TacticalSubscribers[name] then
            self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true,true)
          end
        end
      end
    end
  end
  return self
end

--- [Internal] Threat Range Call to Pilot
-- @param #AWACS self
-- @return #AWACS self
function AWACS:_ThreatRangeCall(GID,Contact)
  self:T(self.lid.."_ThreatRangeCall")
  if not Contact then return self end
  -- AIC: “Enforcer 11 12, east group, THREAT, BRAA 260/15, 29 thousand, hot, hostile, robin.”
  local pilotcallsign = self:_GetCallSign(nil,GID) 
  local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
  local flightpos = managedgroup.Group:GetCoordinate() or managedgroup.LastKnownPosition
  local contact = Contact.Contact -- Ops.Intel#INTEL.Contact
  local contacttag = Contact.TargetGroupNaming or "Bogey"
  local name = managedgroup.GroupName
  local IsSub = self.TacticalSubscribers[name] and true or false
  if contact then
    local position = contact.position or contact.group:GetCoordinate() -- Core.Point#COORDINATE
    if position then     
      local BRATExt = ""
      if self.PathToGoogleKey then     
        BRATExt = position:ToStringBRAANATO(flightpos,false,false,true,false,true)
      else
        BRATExt = position:ToStringBRAANATO(flightpos,false,false)
      end
      local grptxt = self.gettext:GetEntry("GROUP",self.locale)
      local thrt = self.gettext:GetEntry("THREAT",self.locale)
      local text = string.format("%s. %s. %s %s, %s. %s",self.callsigntxt,pilotcallsign,contacttag,grptxt, thrt, BRATExt)
      -- DONE MS TTS - fix spelling out B-R-A in this case
      if string.find(text,"BRAA",1,true) then
        text = string.gsub(text,"BRAA","brah")
      elseif string.find(text,"BRA",1,true) then
       text = string.gsub(text,"BRA","brah")
      end
      if IsSub == false then
        self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true)
      end
      if GID and GID ~= 0 then
        --local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
        if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then
          local name = managedgroup.GroupName
          if self.TacticalSubscribers[name] then
            self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true,true)
          end
        end
      end
    end
  end
  return self
end

--- [Internal] Merged Call to Pilot
-- @param #AWACS self
-- @param #number GID
-- @return #AWACS self
function AWACS:_MergedCall(GID)
  self:T(self.lid.."_MergedCall")
  -- AIC: “Enforcer, mergedb”
  local pilotcallsign = self:_GetCallSign(nil,GID) 
  local merge = self.gettext:GetEntry("MERGED",self.locale)  
  local text = string.format("%s. %s. %s.",self.callsigntxt,pilotcallsign,merge)
  local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
  local name
  if managedgroup then
    name = managedgroup.GroupName or "none"
  end
  if not self.TacticalSubscribers[name] then
    self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true)
  end
  if GID and GID ~= 0 then
    local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
    if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then    
      if self.TacticalSubscribers[name] then
        self:_NewRadioEntry(text,text,GID,true,self.debug,true,false,true,true)
      end
    end
  end
  return self
end

--- [Internal] Assign a Pilot to a target
-- @param #AWACS self
-- @param #table Pilots Table of #AWACS.ManagedGroup Pilot 
-- @param Utilities.FiFo#FIFO Targets FiFo of #AWACS.ManagedContact Targets
-- @return #AWACS self 
function AWACS:_AssignPilotToTarget(Pilots,Targets)
  self:T(self.lid.."_AssignPilotToTarget")
  
  local inreach = false
  local Pilot = nil -- #AWACS.ManagedGroup
  

  local closest = UTILS.NMToMeters(self.maxassigndistance+1)
  local targets = Targets:GetDataTable()
  local Target = nil
  
  for _,_target in pairs(targets) do
      -- Check Distance
    local targetgroupcoord = _target.Contact.position
    -- get closest pilot from target    
    for _,_Pilot in pairs(Pilots) do
      local pilotcoord = _Pilot.Group:GetCoordinate()
      local targetdist = targetgroupcoord:Get2DDistance(pilotcoord)
      if UTILS.MetersToNM(targetdist) < self.maxassigndistance and targetdist < closest then
        self:T(string.format("%sTarget distance %d! Assignment %s!",self.lid,UTILS.Round(UTILS.MetersToNM(targetdist),0),_Pilot.CallSign))
        inreach = true
        closest = targetdist
        Pilot = _Pilot
        Target = _target
        Targets:PullByID(_target.CID)
        break
      else
        self:T(self.lid .. "Target distance > "..self.maxassigndistance.."NM! No Assignment!")
      end
    end
  end
  
  -- DONE Check Human assignment working
  if inreach and Pilot and Pilot.IsPlayer then
     local callsign = Pilot.CallSign
     -- update pilot TaskSheet
    self.ManagedTasks:PullByID(Pilot.CurrentTask)
    
    Pilot.HasAssignedTask = true
    local TargetPosition = Target.Target:GetCoordinate()
    local PlayerPositon = Pilot.LastKnownPosition
    local TargetAlt = Target.Contact.altitude or Target.Cluster.altitude or Target.Contact.group:GetAltitude()
    local TargetDirections, TargetDirectionsTTS = self:_ToStringBRA(PlayerPositon,TargetPosition,TargetAlt)
    local ScreenText = ""
    local TaskType = AWACS.TaskDescription.INTERCEPT
    if self.AwacsROE == AWACS.ROE.POLICE or self.AwacsROE == AWACS.ROE.VID then
      local interc = self.gettext:GetEntry("SCREENVID",self.locale) 
      ScreenText = string.format(interc,Target.TargetGroupNaming)
      TaskType = AWACS.TaskDescription.VID
    else
      local interc = self.gettext:GetEntry("SCREENINTER",self.locale) 
      ScreenText = string.format(interc,Target.TargetGroupNaming)
    end
    Pilot.CurrentTask = self:_CreateTaskForGroup(Pilot.GID,TaskType,ScreenText,Target.Target,AWACS.TaskStatus.REQUESTED,nil,Target.Cluster,Target.Contact)

    Pilot.ContactCID = Target.CID
    
    -- update managed group
    self.ManagedGrps[Pilot.GID] = Pilot
    
    -- Update Contact Status
    Target.LinkedTask = Pilot.CurrentTask
    Target.LinkedGroup = Pilot.GID
    Target.Status = AWACS.TaskStatus.REQUESTED
    local targeted = self.gettext:GetEntry("ENGAGETAG",self.locale) 
    Target.EngagementTag = string.format(targeted,Pilot.CallSign)
    
    self.Contacts:PullByID(Target.CID)
    self.Contacts:Push(Target,Target.CID)
    
    local reqcomm = self.gettext:GetEntry("REQCOMMIT",self.locale)
    local text = string.format(reqcomm, self.callsigntxt,Target.TargetGroupNaming,TargetDirectionsTTS,Pilot.CallSign)
    local textScreen = string.format(reqcomm, self.callsigntxt,Target.TargetGroupNaming,TargetDirections,Pilot.CallSign)
    
    self:_NewRadioEntry(text,textScreen,Pilot.GID,true,self.debug,true,false,true)
    
  elseif inreach and Pilot and Pilot.IsAI then
    -- Target information
    local callsign = Pilot.CallSign
    local FGStatus = Pilot.FlightGroup:GetState()
    self:T("Pilot AI Callsign: " .. callsign)
    self:T("Pilot FG State: " .. FGStatus)
    local targetstatus = Target.Target:GetState()
    self:T("Target State: " .. targetstatus)
 
    --
    local currmission = Pilot.FlightGroup:GetMissionCurrent()
    if currmission then
      self:T("Current Mission: " .. currmission:GetType())
    end
    -- create one intercept Auftrag and one to return to CAP post this one
    local ZoneSet = self.ZoneSet
    local RejectZoneSet = self.RejectZoneSet
    
    local intercept = AUFTRAG:NewINTERCEPT(Target.Target)
    intercept:SetWeaponExpend(AI.Task.WeaponExpend.ALL)
    intercept:SetWeaponType(ENUMS.WeaponFlag.Auto)
    intercept:SetMissionRange(self.MaxMissionRange)
    -- TODO 
    -- now this is going to be interesting...
    -- Check if the target left the "hot" area or is dead already
    intercept:AddConditionSuccess(
      function(target,zoneset,rzoneset)
       -- BASE:I("AUFTRAG Condition Succes Eval Running")
        local success = true
        local target = target -- Ops.Target#TARGET
        if target:IsDestroyed() or target:IsDead() or target:CountTargets() == 0 then return true end
        local tgtcoord = target:GetCoordinate()
        local tgtvec2 = nil
        if tgtcoord then
          tgtvec2 = tgtcoord:GetVec2()
        end
        local zones = zoneset -- Core.Set#SET_ZONE
        local rzones = rzoneset -- Core.Set#SET_ZONE
        if tgtvec2 then
          zones:ForEachZone(
            function(zone)
              if zone:IsVec2InZone(tgtvec2) then
                success = false
              end
            end
          )
          rzones:ForEachZone(
            function(zone)
              if zone:IsVec2InZone(tgtvec2) then
                success = true
              end
            end
          )
        end
        return success
      end,
      Target.Target,
      ZoneSet,
      RejectZoneSet
    )
    
    Pilot.FlightGroup:AddMission(intercept)    
    
    local Angels = Pilot.AnchorStackAngels or 25
    Angels = Angels * 1000
    local AnchorSpeed = self.CapSpeedBase or 270
    AnchorSpeed = UTILS.KnotsToAltKIAS(AnchorSpeed,Angels)
    local Anchor = self.AnchorStacks:ReadByPointer(Pilot.AnchorStackNo) -- #AWACS.AnchorData
    local capauftrag = AUFTRAG:NewCAP(Anchor.StationZone,Angels,AnchorSpeed,Anchor.StationZoneCoordinate,0,15,{})
    capauftrag:SetMissionRange(self.MaxMissionRange)
    capauftrag:SetTime(nil,((self.CAPTimeOnStation*3600)+(15*60)))
    Pilot.FlightGroup:AddMission(capauftrag) 
    
    -- cancel current mission
    if currmission then
      currmission:__Cancel(3)
    end
    
    -- update known mission list
    self.CatchAllMissions[#self.CatchAllMissions+1] = intercept
    self.CatchAllMissions[#self.CatchAllMissions+1] = capauftrag
    
    -- update pilot TaskSheet
    self.ManagedTasks:PullByID(Pilot.CurrentTask)
    
    Pilot.HasAssignedTask = true
    Pilot.CurrentTask = self:_CreateTaskForGroup(Pilot.GID,AWACS.TaskDescription.INTERCEPT,"Intercept Task",Target.Target,AWACS.TaskStatus.ASSIGNED,intercept,Target.Cluster,Target.Contact)
    Pilot.CurrentAuftrag = intercept.auftragsnummer
    Pilot.ContactCID = Target.CID
    
    -- update managed group
    self.ManagedGrps[Pilot.GID] = Pilot
    
    -- Update Contact Status
    Target.LinkedTask = Pilot.CurrentTask
    Target.LinkedGroup = Pilot.GID
    Target.Status = AWACS.TaskStatus.ASSIGNED
    local targeted = self.gettext:GetEntry("ENGAGETAG",self.locale) 
    Target.EngagementTag = string.format(targeted,Pilot.CallSign)
    
    self.Contacts:PullByID(Target.CID)
    self.Contacts:Push(Target,Target.CID)
    
    local altitude = Target.Contact.altitude or Target.Contact.group:GetAltitude()
    local position = Target.Cluster.coordinate or Target.Contact.position
    if not position then
      self.intel:GetClusterCoordinate(Target.Cluster,true)
    end
    local bratext, bratexttts = self:_ToStringBRA(Pilot.Group:GetCoordinate(),position,altitude or 8000)
    
    local aicomm = self.gettext:GetEntry("AICOMMIT",self.locale) 
    local text = string.format(aicomm, self.callsigntxt,Target.TargetGroupNaming,bratexttts,Pilot.CallSign)
    local textScreen = string.format(aicomm, self.callsigntxt,Target.TargetGroupNaming,bratext,Pilot.CallSign)
    
    self:_NewRadioEntry(text,textScreen,Pilot.GID,true,self.debug,true,false,true)
    
    local comm = self.gettext:GetEntry("COMMIT",self.locale) 
    local text = string.format("%s. %s.",Pilot.CallSign,comm)
    
    self:_NewRadioEntry(text,text,Pilot.GID,true,self.debug,true,true,true)
    
    self:__Intercept(2)
    
  end
  
  return self
end

-- TODO FSMs
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- [Internal] onbeforeStart
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onbeforeStart(From,Event,To)
  self:T({From, Event, To})
  if self.IncludeHelicopters then
    self.clientset:FilterCategories("helicopter")
  end
  return self
end

--- [Internal] onafterStart
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterStart(From, Event, To)
  self:T({From, Event, To})
  
  -- Set up control zone
  local controlzonename = "FEZ-"..self.AOName
  self.ControlZone = ZONE_RADIUS:New(controlzonename,self.OpsZone:GetVec2(),UTILS.NMToMeters(self.ControlZoneRadius))
  if self.debug then
    self.ControlZone:DrawZone(self.coalition,{0,1,0},1,{1,0,0},0.05,3,true)
    self.OpsZone:DrawZone(self.coalition,{1,0,0},1,{1,0,0},0.2,5,true)
    local AOCoordString = self.AOCoordinate:ToStringLLDDM()
    local Rocktag = string.format("FEZ: %s\nBulls Coordinate: %s",self.AOName,AOCoordString)
    if self.AllowMarkers then
      MARKER:New(self.AOCoordinate,Rocktag):ToCoalition(self.coalition)
    end
    self.StationZone:DrawZone(self.coalition,{0,0,1},1,{0,0,1},0.2,5,true)
    local stationtag = string.format("Station: %s\nCoordinate: %s",self.StationZoneName,self.StationZone:GetCoordinate():ToStringLLDDM())
    if not self.GCI then
      if self.AllowMarkers then
        MARKER:New(self.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition)
      end
      self.OrbitZone:DrawZone(self.coalition,{0,1,0},1,{0,1,0},0.2,5,true)
      if self.AllowMarkers then
        MARKER:New(self.OrbitZone:GetCoordinate(),"AIC Orbit Zone"):ToCoalition(self.coalition)
      end
    end
  else
    local AOCoordString = self.AOCoordinate:ToStringLLDDM()
    local Rocktag = string.format("FEZ: %s\nBulls Coordinate: %s",self.AOName,AOCoordString)
    if self.AllowMarkers then
      MARKER:New(self.AOCoordinate,Rocktag):ToCoalition(self.coalition)
    end
    if not self.GCI then
      if self.AllowMarkers then
        MARKER:New(self.OrbitZone:GetCoordinate(),"AIC Orbit Zone"):ToCoalition(self.coalition)
      end
    end
    local stationtag = string.format("Station: %s\nCoordinate: %s",self.StationZoneName,self.StationZone:GetCoordinate():ToStringLLDDM())
    if self.AllowMarkers then
      MARKER:New(self.StationZone:GetCoordinate(),stationtag):ToCoalition(self.coalition)
    end
  end
  
  if not self.GCI then
    -- set up the AWACS and let it orbit
    local AwacsAW = self.AirWing -- Ops.Airwing#AIRWING
    local mission = AUFTRAG:NewORBIT_RACETRACK(self.OrbitZone:GetCoordinate(),self.AwacsAngels*1000,self.Speed,self.Heading,self.Leg)
    mission:SetMissionRange(self.MaxMissionRange)
    mission:SetRequiredAttribute({ GROUP.Attribute.AIR_AWACS }) -- prefered plane type, thanks to Heart8reaker
    local timeonstation = (self.AwacsTimeOnStation + self.ShiftChangeTime) * 3600
    mission:SetTime(nil,timeonstation)
    self.CatchAllMissions[#self.CatchAllMissions+1] = mission
    
    AwacsAW:AddMission(mission)
    
    self.AwacsMission = mission
    self.AwacsInZone = false -- not yet arrived or gone again
    self.AwacsReady = false
  else
    self.AwacsInZone = true -- for GCI - arrived
    self.AwacsReady = true
    self:_StartIntel(self.GCIGroup)
    
    if self.GCIGroup:IsGround() then
      self.AwacsFG = ARMYGROUP:New(self.GCIGroup)
      self.AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation)
      self.AwacsFG:SwitchRadio(self.Frequency,self.Modulation)
    elseif self.GCIGroup:IsShip() then
      self.AwacsFG = NAVYGROUP:New(self.GCIGroup)
      self.AwacsFG:SetDefaultRadio(self.Frequency,self.Modulation)
      self.AwacsFG:SwitchRadio(self.Frequency,self.Modulation)
    else
      self:E(self.lid.."**** Group unsuitable for GCI ops! Needs to be a GROUND or SHIP type group!")
      self:Stop()
      return self
    end
    
    self.callsigntxt = string.format("%s",self.CallSignClear[self.CallSign])  
    self:__CheckRadioQueue(-10)
    
    local sunrise = self.gettext:GetEntry("SUNRISE",self.locale)
    local text = string.format(sunrise,self.callsigntxt,self.callsigntxt)
    self:_NewRadioEntry(text,text,0,false,false,false,false,true)
    self:T(self.lid..text)
    self.sunrisedone = true
  end
  
  local ZoneSet = SET_ZONE:New()
  ZoneSet:AddZone(self.ControlZone)
  
  if not self.GCI then
    ZoneSet:AddZone(self.OrbitZone)
  end
  
  if self.BorderZone then
    ZoneSet:AddZone(self.BorderZone)
  end
  
  local RejectZoneSet = SET_ZONE:New()
  if self.RejectZone then
    RejectZoneSet:AddZone(self.RejectZone)
  end
  
  self.ZoneSet = ZoneSet
  self.RejectZoneSet = RejectZoneSet
  
  if self.AllowMarkers then
    -- Add MarkerOps
    
    local MarkerOps = MARKEROPS_BASE:New("AWACS",{"Station","Delete","Move"})
    
    local function Handler(Keywords,Coord,Text)
      self:T(Text)
      for _,_word in pairs (Keywords) do
        if string.lower(_word) == "station" then
          -- get the station name from the text field
          local Name = string.match(Text," ([%a]+)$")
          self:_CreateAnchorStackFromMarker(Name,Coord)
          break
        elseif string.lower(_word) == "delete" then
          -- get the station name from the text field
          local Name = string.match(Text," ([%a]+)$")
          self:_DeleteAnchorStackFromMarker(Name,Coord)
          break
        elseif string.lower(_word) == "move" then
          -- get the station name from the text field
          local Name = string.match(Text," ([%a]+)$")
          self:_MoveAnchorStackFromMarker(Name,Coord)
          break  
        end
      end
    end
    
    -- Event functions
    function MarkerOps:OnAfterMarkAdded(From,Event,To,Text,Keywords,Coord)
      Handler(Keywords,Coord,Text)
    end
    
    function MarkerOps:OnAfterMarkChanged(From,Event,To,Text,Keywords,Coord)
      Handler(Keywords,Coord,Text)
    end
    
    function MarkerOps:OnAfterMarkDeleted(From,Event,To)
    end
    
    self.MarkerOps = MarkerOps
    
  end
  
  if self.GCI then
    -- set FSM to started
    self:__Started(-5)
  end
  
  if self.TacticalMenu then
    self:__CheckTacticalQueue(55)
  end
  
  self:__Status(-30)
  return self
end

function AWACS:_CheckAwacsStatus()
  self:T(self.lid.."_CheckAwacsStatus")
  
  local awacs = nil -- Wrapper.Group#GROUP
  if self.AwacsFG then
    awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP
    local unit = awacs:GetUnit(1)
    if unit then
      self.STN = tostring(unit:GetSTN())
    end
  end
  
  local monitoringdata = self.MonitoringData -- #AWACS.MonitoringData
  
  if not self.GCI then
    if awacs and awacs:IsAlive() and not self.AwacsInZone then
      -- check if we arrived
      local orbitzone = self.OrbitZone -- Core.Zone#ZONE
      if awacs:IsInZone(orbitzone) then
        -- arrived
        self.AwacsInZone = true
        self:T(self.lid.."Arrived in Orbit Zone: " .. orbitzone:GetName())
        local onstationtxt = self.gettext:GetEntry("AWONSTATION",self.locale)
        local text = string.format(onstationtxt,self.callsigntxt,self.AOName or "Rock")
        local textScreen = text    
        self:_NewRadioEntry(text,textScreen,0,false,true,true,false,true)
      end
    end 
  end
  
  --------------------------------
  --     AWACS
  --------------------------------
   
  if (awacs and awacs:IsAlive()) then
    
    if not self.intelstarted then
      local alt = UTILS.Round(UTILS.MetersToFeet(awacs:GetAltitude())/1000,0)
      if alt >= 10 then 
        self:_StartIntel(awacs)
      end
    end
    
    if self.intelstarted  and not self.sunrisedone then
      -- TODO Sunrise call on after airborne at ca 10k feet
      local alt = UTILS.Round(UTILS.MetersToFeet(awacs:GetAltitude())/1000,0)
      if alt >= 10 then
        local sunrise = self.gettext:GetEntry("SUNRISE",self.locale)      
        local text = string.format(sunrise,self.callsigntxt,self.callsigntxt)
        self:_NewRadioEntry(text,text,0,false,false,false,false,true)
        self:T(self.lid..text)
        self.sunrisedone = true
      end
    end
    
    -- Check on Awacs Mission Status
    local AWmission = self.AwacsMission -- Ops.Auftrag#AUFTRAG
    local awstatus = AWmission:GetState()
    local AWmissiontime = (timer.getTime() - self.AwacsTimeStamp)
    
    local AWTOSLeft = UTILS.Round((((self.AwacsTimeOnStation+self.ShiftChangeTime)*3600) - AWmissiontime),0) -- seconds
    
    AWTOSLeft = UTILS.Round(AWTOSLeft/60,0) -- minutes
    
    local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0)
    
    local Changedue = "No"
    
    if not self.ShiftChangeAwacsFlag and (AWTOSLeft <= ChangeTime or AWmission:IsOver()) then 
      Changedue = "Yes"
      self.ShiftChangeAwacsFlag = true
      self:__AwacsShiftChange(2) 
    end
    
    local report = REPORT:New("AWACS:")
    report:Add("====================")
    report:Add("AWACS:")
    report:Add(string.format("Auftrag Status: %s",awstatus))
    report:Add(string.format("TOS Left: %d min",AWTOSLeft))
    report:Add(string.format("Needs ShiftChange: %s",Changedue))
    
    local OpsGroups = AWmission:GetOpsGroups()
    local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP
    if OpsGroup then
      local OpsName = OpsGroup:GetName() or "Unknown"
      local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown"
      report:Add(string.format("Mission FG %s",OpsName))
      report:Add(string.format("Callsign %s",OpsCallSign))
      report:Add(string.format("Mission FG State %s",OpsGroup:GetState()))
    else
      report:Add("***** Cannot obtain (yet) this missions OpsGroup!")
    end
    
    -- Check for replacement mission - if any
    if self.ShiftChangeAwacsFlag and self.ShiftChangeAwacsRequested then -- Ops.Auftrag#AUFTRAG
      AWmission = self.AwacsMissionReplacement
      local esstatus = AWmission:GetState()
      local ESmissiontime = (timer.getTime() - self.AwacsTimeStamp)
      local ESTOSLeft = UTILS.Round((((self.AwacsTimeOnStation+self.ShiftChangeTime)*3600) - ESmissiontime),0) -- seconds
      ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes
      local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0)
      
      report:Add("AWACS REPLACEMENT:")
      report:Add(string.format("Auftrag Status: %s",esstatus))
      report:Add(string.format("TOS Left: %d min",ESTOSLeft))
      
      local OpsGroups = AWmission:GetOpsGroups()
      local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP
      if OpsGroup then
        local OpsName = OpsGroup:GetName() or "Unknown"
        local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown"
        report:Add(string.format("Mission FG %s",OpsName))
        report:Add(string.format("Callsign %s",OpsCallSign))
        report:Add(string.format("Mission FG State %s",OpsGroup:GetState()))
      else
        report:Add("***** Cannot obtain (yet) this missions OpsGroup!")
      end
      
      if AWmission:IsExecuting() then
        -- make the actual change in the queue
        self.ShiftChangeAwacsFlag = false
        self.ShiftChangeAwacsRequested = false
        self.sunrisedone = false
        -- cancel old mission
        if self.AwacsMission and self.AwacsMission:IsNotOver() then
            self.AwacsMission:Cancel()
        end
        self.AwacsMission = self.AwacsMissionReplacement
        self.AwacsMissionReplacement = nil
        self.AwacsTimeStamp = timer.getTime()
        report:Add("*** Replacement DONE ***")
      end
      report:Add("====================")
    end
    
    --------------------------------
    --     ESCORTS
    --------------------------------
                       
    if self.HasEscorts then
      for i=1, self.EscortNumber do
        local ESmission = self.EscortMission[i] -- Ops.Auftrag#AUFTRAG
        if not ESmission then break end
        local esstatus = ESmission:GetState()
        local ESmissiontime = (timer.getTime() - self.EscortsTimeStamp)
        local ESTOSLeft = UTILS.Round((((self.EscortsTimeOnStation+self.ShiftChangeTime)*3600) - ESmissiontime),0) -- seconds
        ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes
        local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0)
        local Changedue = "No"
        
        if (ESTOSLeft <= ChangeTime and not self.ShiftChangeEscortsFlag) or (ESmission:IsOver() and not self.ShiftChangeEscortsFlag) then 
          Changedue = "Yes" 
          self.ShiftChangeEscortsFlag = true -- set this back when new Escorts arrived
          self:__EscortShiftChange(2)
        end
        
        report:Add("====================")
        report:Add("ESCORTS:")
        report:Add(string.format("Auftrag Status: %s",esstatus))
        report:Add(string.format("TOS Left: %d min",ESTOSLeft))
        report:Add(string.format("Needs ShiftChange: %s",Changedue))
        
        local OpsGroups = ESmission:GetOpsGroups()
        local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP
        if OpsGroup then
          local OpsName = OpsGroup:GetName() or "Unknown"
          local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown"
          report:Add(string.format("Mission FG %s",OpsName))
          report:Add(string.format("Callsign %s",OpsCallSign))
          report:Add(string.format("Mission FG State %s",OpsGroup:GetState()))
          monitoringdata.EscortsStateMission[i] = esstatus
          monitoringdata.EscortsStateFG[i] = OpsGroup:GetState()
        else
          report:Add("***** Cannot obtain (yet) this missions OpsGroup!")
        end
        
        report:Add("====================")
        
        local RESMission
        -- Check for replacement mission - if any
        if self.ShiftChangeEscortsFlag and self.ShiftChangeEscortsRequested then -- Ops.Auftrag#AUFTRAG
          RESMission = self.EscortMissionReplacement[i]
          local esstatus = RESMission:GetState()
          local RESMissiontime = (timer.getTime() - self.EscortsTimeStamp)
          local ESTOSLeft = UTILS.Round((((self.EscortsTimeOnStation+self.ShiftChangeTime)*3600) - RESMissiontime),0) -- seconds
          ESTOSLeft = UTILS.Round(ESTOSLeft/60,0) -- minutes
          local ChangeTime = UTILS.Round(((self.ShiftChangeTime * 3600)/60),0)

          report:Add("ESCORTS REPLACEMENT:")
          report:Add(string.format("Auftrag Status: %s",esstatus))
          report:Add(string.format("TOS Left: %d min",ESTOSLeft))
          
          local OpsGroups = RESMission:GetOpsGroups()
          local OpsGroup = self:_GetAliveOpsGroupFromTable(OpsGroups) -- Ops.OpsGroup#OPSGROUP
          if OpsGroup then
            local OpsName = OpsGroup:GetName() or "Unknown"
            local OpsCallSign = OpsGroup:GetCallsignName() or "Unknown"
            report:Add(string.format("Mission FG %s",OpsName))
            report:Add(string.format("Callsign %s",OpsCallSign))
            report:Add(string.format("Mission FG State %s",OpsGroup:GetState()))
          else
            report:Add("***** Cannot obtain (yet) this missions OpsGroup!")
          end
          
          if RESMission and RESMission:IsExecuting() then
            -- make the actual change in the queue
            self.ShiftChangeEscortsFlag = false
            self.ShiftChangeEscortsRequested = false
            -- cancel old mission
            if ESmission and ESmission:IsNotOver() then
              ESmission:__Cancel(1)
            end
            self.EscortMission[i] = self.EscortMissionReplacement[i]
              self.EscortMissionReplacement[i] = nil
            self.EscortsTimeStamp = timer.getTime()
            report:Add("*** Replacement DONE ***")
          end
          report:Add("====================")
        end
      end
    end
      
    if self.debug then  
      self:T(report:Text())
    end
  
  else
       -- Check on Awacs Mission Status
    local AWmission = self.AwacsMission -- Ops.Auftrag#AUFTRAG
    local awstatus = AWmission:GetState()
    if AWmission:IsOver() then
      -- yup we're dead
      self:I(self.lid.."*****AWACS is dead!*****")
      self.ShiftChangeAwacsFlag = true
      self:__AwacsShiftChange(2)
    end 
  end
  
  return monitoringdata
end

--- [Internal] onafterStatus
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterStatus(From, Event, To)
  self:T({From, Event, To})
  
  self:_SetClientMenus()
  
  local monitoringdata = self.MonitoringData -- #AWACS.MonitoringData
  
  if not self.GCI then
    monitoringdata = self:_CheckAwacsStatus()
  end
  
  local awacsalive = false
  if self.AwacsFG then
    local awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP
    if awacs and awacs:IsAlive() then
      awacsalive= true
    end
  end
  
  -- Check on AUFTRAG status for CAP AI
  if self:Is("Running") and (awacsalive or self.AwacsInZone) then
    
      
    -- update coord for SRS
    
    if self.AwacsSRS then
      self.AwacsSRS:SetCoordinate(self.AwacsFG:GetCoordinate())
      if self.TacticalSRS then
        self.TacticalSRS:SetCoordinate(self.AwacsFG:GetCoordinate())
      end
    end
    
    self:_CheckAICAPOnStation()
    
    self:_CleanUpContacts()
    
    self:_CheckMerges()
    
    self:_CheckSubscribers()
    
    local outcome, targets = self:_TargetSelectionProcess(true)
    
    self:_CheckTaskQueue()
    
    local AI, Humans = self:_GetIdlePilots()
    -- assign Pilot if there are targets and available Pilots, prefer Humans to AI
    -- DONE - Implemented AI First, Humans laters - need to work out how to loop the targets to assign a pilot
    if outcome and #Humans > 0 and self.PlayerCapAssignment then
      -- add a task for AI
      self:_AssignPilotToTarget(Humans,targets)
    end
    if outcome and #AI > 0 then
      -- add a task for AI
      self:_AssignPilotToTarget(AI,targets)
    end
  end
  
  if not self.GCI then
    monitoringdata.AwacsShiftChange = self.ShiftChangeAwacsFlag
    
    if self.AwacsFG then
     monitoringdata.AwacsStateFG = self.AwacsFG:GetState()
    end
    
    monitoringdata.AwacsStateMission = self.AwacsMission:GetState()
    monitoringdata.EscortsShiftChange = self.ShiftChangeEscortsFlag
  end
  
  monitoringdata.AICAPCurrent = self.AICAPMissions:Count()
  monitoringdata.AICAPMax = self.MaxAIonCAP
  monitoringdata.Airwings = self.CAPAirwings:Count()
  
  self.MonitoringData = monitoringdata
  
  if self.debug then
    self:_LogStatistics()
  end
  
  local picturetime = timer.getTime() - self.PictureTimeStamp
  
  if self.AwacsInZone and picturetime > self.PictureInterval then
    -- reset timer
    self.PictureTimeStamp = timer.getTime()
    self:_Picture(nil,true)
  end
  
  self:__Status(30)
  
  return self
end

--- [Internal] onafterStop
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterStop(From, Event, To)
  self:T({From, Event, To})
  -- unhandle stuff, exit intel
  
  self.intel:Stop()
  
  local AWFiFo = self.CAPAirwings -- Utilities.FiFo#FIFO
  local AWStack = AWFiFo:GetPointerStack()
  for _ID,_AWID in pairs(AWStack) do
    local SubAW = self.CAPAirwings:ReadByPointer(_ID)
    if SubAW then
      SubAW:RemoveUsingOpsAwacs()
    end
  end
    -- Events
  -- Player joins
  self:UnHandleEvent(EVENTS.PlayerEnterAircraft)
  self:UnHandleEvent(EVENTS.PlayerEnterUnit)
    -- Player leaves
  self:UnHandleEvent(EVENTS.PlayerLeaveUnit)
  self:UnHandleEvent(EVENTS.Ejection)
  self:UnHandleEvent(EVENTS.Crash)
  self:UnHandleEvent(EVENTS.Dead)
  self:UnHandleEvent(EVENTS.UnitLost)
  self:UnHandleEvent(EVENTS.BDA)
  self:UnHandleEvent(EVENTS.PilotDead)
  -- Missile warning
  self:UnHandleEvent(EVENTS.Shot)
  
  return self
end

--- [Internal] onafterAssignAnchor
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param #number GID Group ID
-- @param #boolean HasOwnStation
-- @param #string HasOwnStation
-- @return #AWACS self
function AWACS:onafterAssignAnchor(From, Event, To, GID, HasOwnStation, StationName)
  self:T({From, Event, To, "GID = " .. GID})
  self:_AssignAnchorToID(GID, HasOwnStation, StationName)
  return self
end

--- [Internal] onafterCheckedOut
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param #AWACS.ManagedGroup.GID Group ID 
-- @param #number AnchorStackNo
-- @param #number Angels
-- @return #AWACS self
function AWACS:onafterCheckedOut(From, Event, To, GID, AnchorStackNo, Angels)
  self:T({From, Event, To, "GID = " .. GID})
  self:_RemoveIDFromAnchor(GID,AnchorStackNo,Angels)
  return self
end

--- [Internal] onafterAssignedAnchor
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param #number GID Managed Group ID
-- @param #AWACS.AnchorData Anchor
-- @param #number AnchorStackNo
-- @return #AWACS self
function AWACS:onafterAssignedAnchor(From, Event, To, GID, Anchor, AnchorStackNo, AnchorAngels)
  self:T({From, Event, To, "GID=" .. GID, "Stack=" .. AnchorStackNo})
  -- TODO
  local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
  if not managedgroup then
    self:E(self.lid .. "**** GID "..GID.." Not Registered!")
    return self
  end
  managedgroup.AnchorStackNo = AnchorStackNo
  managedgroup.AnchorStackAngels = AnchorAngels
  managedgroup.Blocked = false
  local isPlayer = managedgroup.IsPlayer
  local isAI = managedgroup.IsAI
  local Group = managedgroup.Group
  local CallSign = managedgroup.CallSign or "Ghost 1"
  local AnchorName = Anchor.StationName or "unknown"
  local AnchorCoordTxt = Anchor.StationZoneCoordinateText or "unknown"
  local Angels = AnchorAngels or 25
  local AnchorSpeed = self.CapSpeedBase or 270
  local AuftragsNr = managedgroup.CurrentAuftrag
  
  local textTTS = ""
  if self.PikesSpecialSwitch then
    local stationtxt = self.gettext:GetEntry("STATIONAT",self.locale) 
    textTTS = string.format(stationtxt,CallSign,self.callsigntxt,AnchorName,Angels)
  else
    local stationtxt = self.gettext:GetEntry("STATIONATLONG",self.locale) 
    textTTS = string.format(stationtxt,CallSign,self.callsigntxt,AnchorName,Angels,AnchorSpeed)
  end
  local ROEROT = self.AwacsROE..", "..self.AwacsROT
  local stationtxtsc = self.gettext:GetEntry("STATIONSCREEN",self.locale) 
  local stationtxtta = self.gettext:GetEntry("STATIONTASK",self.locale) 
  local textScreen = string.format(stationtxtsc,CallSign,self.callsigntxt,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT)
  local TextTasking = string.format(stationtxtta,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT)
  
  self:_NewRadioEntry(textTTS,textScreen,GID,isPlayer,isPlayer,true,false)
      
  managedgroup.CurrentTask = self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,TextTasking,Anchor.StationZone)

  -- if it's a Alert5, we want to push CAP instead
  if isAI then
    local auftrag = managedgroup.FlightGroup:GetMissionCurrent() -- Ops.Auftrag#AUFTRAG
    if auftrag then
      local auftragtype = auftrag:GetType()
      if auftragtype == AUFTRAG.Type.ALERT5 then
        -- all correct
        local capauftrag = AUFTRAG:NewCAP(Anchor.StationZone,Angels*1000,AnchorSpeed,Anchor.StationZone:GetCoordinate(),0,15,{})
        capauftrag:SetMissionRange(self.MaxMissionRange)
        capauftrag:SetTime(nil,((self.CAPTimeOnStation*3600)+(15*60)))
        capauftrag:AddAsset(managedgroup.FlightGroup)
        self.CatchAllMissions[#self.CatchAllMissions+1] = capauftrag
        managedgroup.FlightGroup:AddMission(capauftrag)
        auftrag:Cancel()
      else
       self:E("**** AssignedAnchor but Auftrag NOT ALERT5!")
      end
    else
      self:E("**** AssignedAnchor but NO Auftrag!")
    end 
  end
  
  self.ManagedGrps[GID] = managedgroup
    
  return self
end

--- [Internal] onafterNewCluster
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param Ops.Intel#INTEL.Cluster Cluster
-- @return #AWACS self
function AWACS:onafterNewCluster(From,Event,To,Cluster)
  self:T({From, Event, To, Cluster.index})
  
  self.CID = self.CID + 1
  self.Countactcounter = self.Countactcounter + 1
  
  local ContactTable = Cluster.Contacts or {}
  
  local function GetFirstAliveContact(table)
    for _,_contact in pairs (table) do
      local contact = _contact -- Ops.Intel#INTEL.Contact
      if contact and contact.group and contact.group:IsAlive() then
        return contact, contact.group
      end
    end
    return nil
  end
  
  local Contact, Group = GetFirstAliveContact(ContactTable) -- Ops.Intel#INTEL.Contact
  
  if not Contact then return self end
  
  if Group and not Group:IsAirborne() then
    return self
  end
  
  local targetset = SET_GROUP:New()
  -- SET for TARGET
  for _,_grp in pairs(ContactTable) do
    local grp = _grp -- Ops.Intel#INTEL.Contact
    targetset:AddGroup(grp.group, true)
  end
  local managedcontact = {} -- #AWACS.ManagedContact
  managedcontact.CID = self.CID
  managedcontact.Contact = Contact
  managedcontact.Cluster = Cluster
  -- TODO set as per tech / engagement / alarm level age...
  managedcontact.IFF = AWACS.IFF.BOGEY -- no IFF yet
  managedcontact.Target = TARGET:New(targetset)
  managedcontact.LinkedGroup = 0
  managedcontact.LinkedTask = 0
  managedcontact.Status = AWACS.TaskStatus.IDLE
  local phoneid = math.fmod(self.Countactcounter,27)
  if phoneid == 0 then phoneid = 1 end
  managedcontact.TargetGroupNaming = AWACS.Phonetic[phoneid]
  managedcontact.ReportingName = Contact.group:GetNatoReportingName() -- e.g. Foxbat. Bogey if unknown
  managedcontact.TACCallDone = false
  managedcontact.MeldCallDone = false
  managedcontact.EngagementTag = ""
  
  local IsPopup = false
  -- is this a pop-up group? i.e. appeared inside AO
  if self.OpsZone:IsVec2InZone(Contact.position:GetVec2()) then
   IsPopup = true
  end
  
  -- let's see if we can inject some info into Contact
  Contact.CID = managedcontact.CID
  Contact.TargetGroupNaming = managedcontact.TargetGroupNaming
  Cluster.CID = managedcontact.CID
  Cluster.TargetGroupNaming = managedcontact.TargetGroupNaming
  
  self.Contacts:Push(managedcontact,self.CID)
  
  -- only announce if in right distance to HVT/AIC or in ControlZone or in BorderZone
  local ContactCoordinate = Contact.position:GetVec2()
  local incontrolzone = self.ControlZone:IsVec2InZone(ContactCoordinate)
  
  -- distance check to HVT
  local distance = 1000000
  if not self.GCI then
    distance = Contact.position:Get2DDistance(self.OrbitZone:GetCoordinate())
  end
  
  local inborderzone = false
  if self.BorderZone then
    inborderzone = self.BorderZone:IsVec2InZone(ContactCoordinate)
  end
  
  if incontrolzone or inborderzone or (distance <= UTILS.NMToMeters(55)) or IsPopup then
    self:_AnnounceContact(managedcontact,true,nil,false,managedcontact.TargetGroupNaming,IsPopup,managedcontact.ReportingName)
  end
  
  return self
end
  
--- [Internal] onafterNewContact
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param Ops.Intel#INTEL.Contact Contact
-- @return #AWACS self 
function AWACS:onafterNewContact(From,Event,To,Contact)
  self:T({From, Event, To, Contact})
  local tdist = self.ThreatDistance -- NM 
  -- is any plane near-by? 
  for _gid,_mgroup in pairs(self.ManagedGrps) do
    local managedgroup = _mgroup -- #AWACS.ManagedGroup
    local group = managedgroup.Group
    if group and group:IsAlive() and group:IsAirborne() then
       -- contact distance
       local cpos = Contact.position or Contact.group:GetCoordinate() -- Core.Point#COORDINATE
       local mpos = group:GetCoordinate()
       local dist = cpos:Get2DDistance(mpos) -- meter
       dist = UTILS.Round(UTILS.MetersToNM(dist),0)
       if dist <= tdist then
        -- threat call
        self:_ThreatRangeCall(_gid,Contact)
       end
    end
  end
  return self
end
  
--- [Internal] onafterLostContact
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param Ops.Intel#INTEL.Contact Contact
-- @return #AWACS self
function AWACS:onafterLostContact(From,Event,To,Contact)
  self:T({From, Event, To, Contact})
  return self
end
  
--- [Internal] onafterLostCluster
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @param Ops.Intel#INTEL.Cluster Cluster
-- @param Ops.Auftrag#AUFTRAG Mission
-- @return #AWACS self
function AWACS:onafterLostCluster(From,Event,To,Cluster,Mission)
  self:T({From, Event, To})
  return self
end

--- [Internal] onafterCheckTacticalQueue
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterCheckTacticalQueue(From,Event,To)
 self:T({From, Event, To})
 -- do we have messages queued?
 
 if self.clientset:CountAlive() ==  0 then 
  self:T(self.lid.."No player connected.")
  self:__CheckTacticalQueue(-5)
  return self 
 end
 
 for _name,_freq in pairs(self.TacticalSubscribers) do
  local Group = nil
  if _name then
    Group = GROUP:FindByName(_name)
  end
  if Group and Group:IsAlive() then
    self:_BogeyDope(Group,true)
  end
 end
 
 if (self.TacticalQueue:IsNotEmpty()) then
  
  while self.TacticalQueue:Count() > 0 do
  
    local RadioEntry = self.TacticalQueue:Pull() -- #AWACS.RadioEntry 
    self:T({RadioEntry})
    local frequency = self.TacticalBaseFreq
    if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then
      local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup
      if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then
        local name = managedgroup.GroupName
        frequency = self.TacticalSubscribers[name]
      end
    end
    -- AI AWACS Speaking
    local gtext = RadioEntry.TextTTS
    if self.PathToGoogleKey then
      gtext = string.format("<speak><prosody rate='medium'>%s</prosody></speak>",gtext)
    end
    self.TacticalSRSQ:NewTransmission(gtext,nil,self.TacticalSRS,nil,0.5,nil,nil,nil,frequency,self.TacticalModulation)
  
    self:T(RadioEntry.TextTTS)
    
    if RadioEntry.ToScreen and RadioEntry.TextScreen and (not self.SuppressScreenOutput) then
      if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then
        local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup
        if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then
          MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToGroup(managedgroup.Group)
          self:T(RadioEntry.TextScreen)
        end
      else
        MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToCoalition(self.coalition)
      end
    end
   end
 
 end -- end while
 
 if not self:Is("Stopped") then
  self:__CheckTacticalQueue(-self.TacticalInterval)
 end
 return self
end


--- [Internal] onafterCheckRadioQueue
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterCheckRadioQueue(From,Event,To)
 self:T({From, Event, To})
 -- do we have messages queued?
 
 local nextcall = 10
 if (self.RadioQueue:IsNotEmpty() or self.PrioRadioQueue:IsNotEmpty()) then
  
  local RadioEntry = nil
  
  if self.PrioRadioQueue:IsNotEmpty() then
    RadioEntry = self.PrioRadioQueue:Pull() -- #AWACS.RadioEntry
  else
    RadioEntry = self.RadioQueue:Pull() -- #AWACS.RadioEntry
  end
  self:T({RadioEntry})
  
  if self.clientset:CountAlive() ==  0 then 
    self:T(self.lid.."No player connected.")
    self:__CheckRadioQueue(-5)
    return self 
  end
  
  if not RadioEntry.FromAI then
    -- AI AWACS Speaking
    if self.PathToGoogleKey then
      local gtext = RadioEntry.TextTTS
      gtext = string.format("<speak><prosody rate='medium'>%s</prosody></speak>",gtext)
      self.AwacsSRS:PlayTextExt(gtext,nil,self.MultiFrequency,self.MultiModulation,self.Gender,self.Culture,self.Voice,self.Volume,"AWACS")
    else
      self.AwacsSRS:PlayTextExt(RadioEntry.TextTTS,nil,self.MultiFrequency,self.MultiModulation,self.Gender,self.Culture,self.Voice,self.Volume,"AWACS")
    end
    self:T(RadioEntry.TextTTS)
  else
    -- CAP AI speaking
    if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then
      local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup
      if managedgroup and managedgroup.FlightGroup and managedgroup.FlightGroup:IsAlive() then
        if self.PathToGoogleKey then
          local gtext = RadioEntry.TextTTS
          gtext = string.format("<speak><prosody rate='medium'>%s</prosody></speak>",gtext)
          managedgroup.FlightGroup:RadioTransmission(gtext,1,false)
        else
          managedgroup.FlightGroup:RadioTransmission(RadioEntry.TextTTS,1,false)
        end
        self:T(RadioEntry.TextTTS)
      end
    end
  end
  
  if RadioEntry.Duration then nextcall = RadioEntry.Duration end
  
  if RadioEntry.ToScreen and RadioEntry.TextScreen and (not self.SuppressScreenOutput) then
    if RadioEntry.GroupID and RadioEntry.GroupID ~= 0 then
      local managedgroup = self.ManagedGrps[RadioEntry.GroupID] -- #AWACS.ManagedGroup
      if managedgroup and managedgroup.Group and managedgroup.Group:IsAlive() then
        MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToGroup(managedgroup.Group)
        self:T(RadioEntry.TextScreen)
      end
    else
      MESSAGE:New(RadioEntry.TextScreen,20,"AWACS"):ToCoalition(self.coalition)
    end
  end
 end
 
 if self:Is("Running") then
  -- exit if stopped
  if self.PathToGoogleKey then
    nextcall = nextcall + self.GoogleTTSPadding
  else
    nextcall = nextcall + self.WindowsTTSPadding
  end
  self:__CheckRadioQueue(-nextcall)
 end
 return self
end

--- [Internal] onafterEscortShiftChange
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterEscortShiftChange(From,Event,To)
  self:T({From, Event, To})
  -- request new Escorts, check if AWACS-FG still alive first!
  if self.AwacsFG and self.ShiftChangeEscortsFlag and not self.ShiftChangeEscortsRequested then
    local awacs = self.AwacsFG:GetGroup() -- Wrapper.Group#GROUP
    if awacs and awacs:IsAlive() then
      -- ok we're good to re-request
      self.ShiftChangeEscortsRequested = true
      self.EscortsTimeStamp = timer.getTime()
      self:_StartEscorts(true)
    else
      -- should not happen
      self:E("**** AWACS group dead at onafterEscortShiftChange!")
    end
  end
  return self
end

--- [Internal] onafterAwacsShiftChange
-- @param #AWACS self
-- @param #string From 
-- @param #string Event
-- @param #string To
-- @return #AWACS self
function AWACS:onafterAwacsShiftChange(From,Event,To)
  self:T({From, Event, To})
  -- request new AWACS
  if self.AwacsFG and self.ShiftChangeAwacsFlag and not self.ShiftChangeAwacsRequested then
    
    -- ok we're good to re-request
    self.ShiftChangeAwacsRequested = true
    self.AwacsTimeStamp = timer.getTime()
    
    -- set up the AWACS and let it orbit
    local AwacsAW = self.AirWing -- Ops.Airwing#AIRWING
    local mission = AUFTRAG:NewORBIT_RACETRACK(self.OrbitZone:GetCoordinate(),self.AwacsAngels*1000,self.Speed,self.Heading,self.Leg)
    self.CatchAllMissions[#self.CatchAllMissions+1] = mission
    local timeonstation = (self.AwacsTimeOnStation + self.ShiftChangeTime) * 3600
    mission:SetTime(nil,timeonstation)
    mission:SetMissionRange(self.MaxMissionRange)
    
    AwacsAW:AddMission(mission)
    
    self.AwacsMissionReplacement = mission
    
  end
  return self
end

--- On after "FlightOnMission".
-- @param #AWACS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup on mission.
-- @param Ops.Auftrag#AUFTRAG Mission The requested mission.
-- @return #AWACS self
function AWACS:onafterFlightOnMission(From, Event, To, FlightGroup, Mission)
  self:T({From, Event, To})
  -- coming back from AW, set up the flight
  self:T("FlightGroup " .. FlightGroup:GetName() .. " Mission " .. Mission:GetName() .. " Type "..Mission:GetType())
  self.CatchAllFGs[#self.CatchAllFGs+1] = FlightGroup
  if not self:Is("Stopped") then
    if not self.AwacsReady or self.ShiftChangeAwacsFlag or self.ShiftChangeEscortsFlag then
     self:_StartSettings(FlightGroup,Mission)
    elseif Mission and (Mission:GetType() == AUFTRAG.Type.CAP or Mission:GetType() == AUFTRAG.Type.ALERT5 or Mission:GetType() == AUFTRAG.Type.ORBIT) then
        if not self.FlightGroups:HasUniqueID(FlightGroup:GetName()) then
          self:T("Pushing FG " .. FlightGroup:GetName() .. " to stack!")
          self.FlightGroups:Push(FlightGroup,FlightGroup:GetName())
        end
    end
  end
  return self
end

--- On after "ReAnchor".
-- @param #AWACS self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number GID Group ID to check and re-anchor if possible
-- @return #AWACS self
function AWACS:onafterReAnchor(From, Event, To, GID)
  self:T({From, Event, To, GID})
  -- get managedgroup, heck AI FG state, heck weapon state, check fuel state, vector back to anchor or RTB
  local managedgroup = self.ManagedGrps[GID] -- #AWACS.ManagedGroup
  if managedgroup then
    if managedgroup.IsAI then
      -- AI will now have a new CAP AUFTRAG and head back to the stack anyway
      local AIFG = managedgroup.FlightGroup -- Ops.FlightGroup#FLIGHTGROUP
      if AIFG and AIFG:IsAlive() then
        -- check state
        if AIFG:IsFuelLow() or AIFG:IsOutOfMissiles() or AIFG:IsOutOfAmmo() then
          local destbase = AIFG.homebase
          if not destbase then destbase = self.Airbase end
          -- RTB call needs an AIRBASE
          AIFG:RTB(destbase)
          -- Check out
          self:_CheckOut(AIFG:GetGroup(),GID)
          self.AIRequested = self.AIRequested - 1
        else
          -- re-establish anchor task, get anchor zone data
          local Anchor = self.AnchorStacks:ReadByPointer(managedgroup.AnchorStackNo) -- #AWACS.AnchorData
          local StationZone = Anchor.StationZone -- Core.Zone#ZONE
          managedgroup.CurrentTask = self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,"Re-Station AI",StationZone)
          managedgroup.HasAssignedTask = true
          local mission = AIFG:GetMissionCurrent() -- Ops.Auftrag#AUFTRAG
          if mission then
            managedgroup.CurrentAuftrag = mission.auftragsnummer or 0
          else
            managedgroup.CurrentAuftrag = 0
          end
          managedgroup.ContactCID = 0
          self.ManagedGrps[GID] = managedgroup 
          local tostation = self.gettext:GetEntry("VECTORSTATION",self.locale)       
          self:_MessageVector(GID,tostation,Anchor.StationZoneCoordinate,managedgroup.AnchorStackAngels)
        end
      else
        -- lost group, remove from known groups, declare vanished
        -- AI - remove from known FGs! -- done in status loop
        -- ALL remove from managedgrps
        
        -- message loss
        local savedcallsign = managedgroup.CallSign
          --vanished/friendly flight faded/lost contact with C/S/CSAR Scramble
           -- Magic, RIGHTGUARD, RIGHTGUARD, Dodge 41, Bullseye X/Y
       local textoptions = {}    
       textoptions[1] = self.gettext:GetEntry("TEXTOPTIONS1",self.locale)  
       textoptions[2] = self.gettext:GetEntry("TEXTOPTIONS2",self.locale)  
       textoptions[3] = self.gettext:GetEntry("TEXTOPTIONS3",self.locale)  
       textoptions[4] = self.gettext:GetEntry("TEXTOPTIONS4",self.locale)
       local allstations = self.gettext:GetEntry("ALLSTATIONS",self.locale)
       local milestxt = self.gettext:GetEntry("MILES",self.locale)  
        -- DONE - need to save last known coordinate
        
        if managedgroup.LastKnownPosition then
          local lastknown = UTILS.DeepCopy(managedgroup.LastKnownPosition)
          local faded = textoptions[math.random(1,4)]
          local text = string.format("%s. %s. %s %s.",allstations,self.callsigntxt, faded, savedcallsign)
          local textScreen = string.format("%s, %s. %s %s.",allstations, self.callsigntxt, faded, savedcallsign)
          
          local brtext = self:_ToStringBULLS(lastknown)
          local brtexttts = self:_ToStringBULLS(lastknown,false,true)

          text = text .. " "..brtexttts.." "..milestxt.."."
          textScreen = textScreen .. " "..brtext.." "..milestxt.."."
          
          self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true)
        end
        self.ManagedGrps[GID] = nil
      end 
    elseif managedgroup.IsPlayer then
      -- TODO
      local PLFG = managedgroup.Group -- Wrapper.Group#GROUP
      if PLFG and PLFG:IsAlive() then
          -- re-establish anchor task
          -- get anchor zone data
          local Anchor = self.AnchorStacks:ReadByPointer(managedgroup.AnchorStackNo) -- #AWACS.AnchorData
          local AnchorName = Anchor.StationName or "unknown"
          local AnchorCoordTxt = Anchor.StationZoneCoordinateText or "unknown"
          local Angels = managedgroup.AnchorStackAngels or 25
          local AnchorSpeed = self.CapSpeedBase or 270
          local StationZone = Anchor.StationZone -- Core.Zone#ZONE
          local ROEROT = self.AwacsROE.." "..self.AwacsROT
          local stationtxt = self.gettext:GetEntry("STATIONTASK",self.locale)
          local TextTasking = string.format(stationtxt,AnchorName,Angels,AnchorSpeed,AnchorCoordTxt,ROEROT)
          managedgroup.CurrentTask = self:_CreateTaskForGroup(GID,AWACS.TaskDescription.ANCHOR,TextTasking,StationZone)
          managedgroup.HasAssignedTask = true
          managedgroup.ContactCID = 0
          self.ManagedGrps[GID] = managedgroup
          local vectortxt = self.gettext:GetEntry("VECTORSTATION",self.locale)        
          self:_MessageVector(GID,vectortxt,Anchor.StationZoneCoordinate,managedgroup.AnchorStackAngels)
      else
        -- lost group, remove from known groups, declare vanished
        -- ALL remove from managedgrps       
        -- message loss
        local savedcallsign = managedgroup.CallSign
          --vanished/friendly flight faded/lost contact with C/S/CSAR Scramble
           -- Magic, RIGHTGUARD, RIGHTGUARD, Dodge 41, Bullseye X/Y
         local textoptions = {}    
         textoptions[1] = self.gettext:GetEntry("TEXTOPTIONS1",self.locale)  
         textoptions[2] = self.gettext:GetEntry("TEXTOPTIONS2",self.locale)  
         textoptions[3] = self.gettext:GetEntry("TEXTOPTIONS3",self.locale)  
         textoptions[4] = self.gettext:GetEntry("TEXTOPTIONS4",self.locale)
         local allstations = self.gettext:GetEntry("ALLSTATIONS",self.locale)
         local milestxt = self.gettext:GetEntry("MILES",self.locale)  
        
        -- DONE - need to save last known coordinate
        local faded = textoptions[math.random(1,4)]
        local text = string.format("%s. %s. %s %s.",allstations, self.callsigntxt, faded, savedcallsign)
        local textScreen = string.format("%s, %s. %s %s.", allstations,self.callsigntxt, faded, savedcallsign)
        if managedgroup.LastKnownPosition then
          local lastknown = UTILS.DeepCopy(managedgroup.LastKnownPosition)
          local brtext = self:_ToStringBULLS(lastknown)
          local brtexttts = self:_ToStringBULLS(lastknown,false,true)
          text = text .. " "..brtexttts.." "..milestxt.."."
          textScreen = textScreen .. " "..brtext.." "..milestxt.."."
        
          self:_NewRadioEntry(text,textScreen,0,false,self.debug,true,false,true)
        end
        self.ManagedGrps[GID] = nil
      end 
    end
  end
end

end -- end do
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- END AWACS
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Brigade Warehouse.
--
-- **Main Features:**
--
--    * Manage platoons
--    * Carry out ARTY and PATROLZONE missions (AUFTRAG)
--    * Define rearming zones
-- 
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/Brigade).
-- 
-- ===
--
-- ### Author: **funkyfranky**
-- 
-- ===
-- @module Ops.Brigade
-- @image OPS_Brigade_.png


--- BRIGADE class.
-- @type BRIGADE
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity of output.
-- @field #table rearmingZones Rearming zones. Each element is of type `#BRIGADE.SupplyZone`.
-- @field #table refuellingZones Refuelling zones. Each element is of type `#BRIGADE.SupplyZone`.
-- @field Core.Set#SET_ZONE retreatZones Retreat zone set.
-- @extends Ops.Legion#LEGION

--- *I am not afraid of an Army of lions lead by a sheep; I am afraid of sheep lead by a lion* -- Alexander the Great
--
-- ===
--
-- # The BRIGADE Concept
--
-- A BRIGADE consists of one or multiple PLATOONs. These platoons "live" in a WAREHOUSE that has a phyiscal struction (STATIC or UNIT) and can be captured or destroyed.
--
--
-- @field #BRIGADE
BRIGADE = {
  ClassName       = "BRIGADE",
  verbose         =     0,
  rearmingZones   =    {},
  refuellingZones =    {}, 
}

--- Supply Zone.
-- @type BRIGADE.SupplyZone
-- @field Core.Zone#ZONE zone The zone.
-- @field Ops.Auftrag#AUFTRAG mission Mission assigned to supply ammo or fuel.
-- @field #boolean markerOn If `true`, marker is on.
-- @field Wrapper.Marker#MARKER marker F10 marker.

--- BRIGADE class version.
-- @field #string version
BRIGADE.version="0.1.1"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Spawn when hosting warehouse is a ship or oil rig or gas platform.
-- TODO: Rearming zones.
-- TODO: Retreat zones.
-- DONE: Add weapon range.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new BRIGADE class object.
-- @param #BRIGADE self
-- @param #string WarehouseName Name of the warehouse STATIC or UNIT object representing the warehouse.
-- @param #string BrigadeName Name of the brigade.
-- @return #BRIGADE self
function BRIGADE:New(WarehouseName, BrigadeName)

  -- Inherit everything from LEGION class.
  local self=BASE:Inherit(self, LEGION:New(WarehouseName, BrigadeName)) -- #BRIGADE

  -- Nil check.
  if not self then
    BASE:E(string.format("ERROR: Could not find warehouse %s!", WarehouseName))
    return nil
  end

  -- Set some string id for output to DCS.log file.
  self.lid=string.format("BRIGADE %s | ", self.alias)
  
  -- Defaults
  self:SetRetreatZones()
  
  -- Turn ship into NAVYGROUP.
  if self:IsShip() then
    local wh=self.warehouse --Wrapper.Unit#UNIT
    local group=wh:GetGroup()
    self.warehouseOpsGroup=NAVYGROUP:New(group) --Ops.NavyGroup#NAVYGROUP
    self.warehouseOpsElement=self.warehouseOpsGroup:GetElementByName(wh:GetName())
  end
  
  -- Add FSM transitions.
  --                 From State  -->   Event         -->      To State
  self:AddTransition("*",             "ArmyOnMission",       "*")           -- An ARMYGROUP was send on a Mission (AUFTRAG).

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the BRIGADE. Initializes parameters and starts event handlers.
  -- @function [parent=#BRIGADE] Start
  -- @param #BRIGADE self

  --- Triggers the FSM event "Start" after a delay. Starts the BRIGADE. Initializes parameters and starts event handlers.
  -- @function [parent=#BRIGADE] __Start
  -- @param #BRIGADE self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Stop". Stops the BRIGADE and all its event handlers.
  -- @param #BRIGADE self

  --- Triggers the FSM event "Stop" after a delay. Stops the BRIGADE and all its event handlers.
  -- @function [parent=#BRIGADE] __Stop
  -- @param #BRIGADE self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "ArmyOnMission".
  -- @function [parent=#BRIGADE] ArmyOnMission
  -- @param #BRIGADE self
  -- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup The ARMYGROUP on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- Triggers the FSM event "ArmyOnMission" after a delay.
  -- @function [parent=#BRIGADE] __ArmyOnMission
  -- @param #BRIGADE self
  -- @param #number delay Delay in seconds.
  -- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup The ARMYGROUP on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "ArmyOnMission" event.
  -- @function [parent=#BRIGADE] OnAfterArmyOnMission
  -- @param #BRIGADE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup The ARMYGROUP on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Add a platoon to the brigade.
-- @param #BRIGADE self
-- @param Ops.Platoon#PLATOON Platoon The platoon object.
-- @return #BRIGADE self
function BRIGADE:AddPlatoon(Platoon)

  -- Add platoon to brigade.
  table.insert(self.cohorts, Platoon)

  -- Add assets to platoon.
  self:AddAssetToPlatoon(Platoon, Platoon.Ngroups)

  -- Set brigade of platoon.
  Platoon:SetBrigade(self)

  -- Start platoon.
  if Platoon:IsStopped() then
    Platoon:Start()
  end

  return self
end

--- Add asset group(s) to platoon.
-- @param #BRIGADE self
-- @param Ops.Platoon#PLATOON Platoon The platoon object.
-- @param #number Nassets Number of asset groups to add.
-- @return #BRIGADE self
function BRIGADE:AddAssetToPlatoon(Platoon, Nassets)

  if Platoon then

    -- Get the template group of the platoon.
    local Group=GROUP:FindByName(Platoon.templatename)

    if Group then

      -- Debug text.
      local text=string.format("Adding asset %s to platoon %s", Group:GetName(), Platoon.name)
      self:T(self.lid..text)

      -- Add assets to airwing warehouse.
      self:AddAsset(Group, Nassets, nil, nil, nil, nil, Platoon.skill, Platoon.livery, Platoon.name)

    else
      self:E(self.lid.."ERROR: Group does not exist!")
    end

  else
    self:E(self.lid.."ERROR: Platoon does not exit!")
  end

  return self
end

--- Define a set of retreat zones.
-- @param #BRIGADE self
-- @param Core.Set#SET_ZONE RetreatZoneSet Set of retreat zones.
-- @return #BRIGADE self
function BRIGADE:SetRetreatZones(RetreatZoneSet)
  self.retreatZones=RetreatZoneSet or SET_ZONE:New()
  return self
end

--- Add a retreat zone.
-- @param #BRIGADE self
-- @param Core.Zone#ZONE RetreatZone Retreat zone.
-- @return #BRIGADE self
function BRIGADE:AddRetreatZone(RetreatZone)
  self.retreatZones:AddZone(RetreatZone)
  return self
end

--- Get retreat zones.
-- @param #BRIGADE self
-- @return Core.Set#SET_ZONE Set of retreat zones.
function BRIGADE:GetRetreatZones()
  return self.retreatZones
end

--- Add a rearming zone.
-- @param #BRIGADE self
-- @param Core.Zone#ZONE RearmingZone Rearming zone.
-- @return #BRIGADE.SupplyZone The rearming zone data.
function BRIGADE:AddRearmingZone(RearmingZone)

  local rearmingzone={} --#BRIGADE.SupplyZone
  
  rearmingzone.zone=RearmingZone
  rearmingzone.mission=nil
  rearmingzone.marker=MARKER:New(rearmingzone.zone:GetCoordinate(), "Rearming Zone"):ToCoalition(self:GetCoalition())

  table.insert(self.rearmingZones, rearmingzone)

  return rearmingzone
end


--- Add a refuelling zone.
-- @param #BRIGADE self
-- @param Core.Zone#ZONE RefuellingZone Refuelling zone.
-- @return #BRIGADE.SupplyZone The refuelling zone data.
function BRIGADE:AddRefuellingZone(RefuellingZone)

  local supplyzone={} --#BRIGADE.SupplyZone
  
  supplyzone.zone=RefuellingZone
  supplyzone.mission=nil
  supplyzone.marker=MARKER:New(supplyzone.zone:GetCoordinate(), "Refuelling Zone"):ToCoalition(self:GetCoalition())

  table.insert(self.refuellingZones, supplyzone)

  return supplyzone
end


--- Get platoon by name.
-- @param #BRIGADE self
-- @param #string PlatoonName Name of the platoon.
-- @return Ops.Platoon#PLATOON The Platoon object.
function BRIGADE:GetPlatoon(PlatoonName)
  local platoon=self:_GetCohort(PlatoonName)
  return platoon
end

--- Get platoon of an asset.
-- @param #BRIGADE self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The platoon asset.
-- @return Ops.Platoon#PLATOON The platoon object.
function BRIGADE:GetPlatoonOfAsset(Asset)
  local platoon=self:GetPlatoon(Asset.squadname)
  return platoon
end

--- Remove asset from platoon.
-- @param #BRIGADE self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The platoon asset.
function BRIGADE:RemoveAssetFromPlatoon(Asset)
  local platoon=self:GetPlatoonOfAsset(Asset)
  if platoon then
    platoon:DelAsset(Asset)
  end
end


--- [ GROUND ] Function to load back an asset in the field that has been filed before.
-- @param #BRIGADE self
-- @param #string Templatename e.g."1 PzDv LogRg I\_AID-976" - that's the alias (name) of an platoon spawned as `"platoon - alias"_AID-"asset-ID"`
-- @param Core.Point#COORDINATE Position where to spawn the platoon
-- @return #BRIGADE self
-- @usage
-- Prerequisites:
-- Save the assets spawned by BRIGADE/CHIEF regularly (~every 5 mins) into a file, e.g. like this: 
--   
--              local Path = FilePath or "C:\\Users\\<yourname>\\Saved Games\\DCS\\Missions\\" -- example path
--              local BlueOpsFilename = BlueFileName or "ExamplePlatoonSave.csv" -- example filename 
--              local BlueSaveOps = SET_OPSGROUP:New():FilterCoalitions("blue"):FilterCategoryGround():FilterOnce()
--              UTILS.SaveSetOfOpsGroups(BlueSaveOps,Path,BlueOpsFilename)
--          
-- where Path and Filename are strings, as chosen by you.
-- You can then load back the assets at the start of your next mission run. Be aware that it takes a couple of seconds for the 
-- platoon data to arrive in brigade, so make this an action after ~20 seconds, e.g. like so:
-- 
--            function LoadBackAssets()
--              local Path = FilePath or "C:\\Users\\<yourname>\\Saved Games\\DCS\\Missions\\" -- example path
--              local BlueOpsFilename = BlueFileName or "ExamplePlatoonSave.csv" -- example filename  
--              if UTILS.CheckFileExists(Path,BlueOpsFilename) then
--                local loadback = UTILS.LoadSetOfOpsGroups(Path,BlueOpsFilename,false)
--                for _,_platoondata in pairs (loadback) do
--                  local groupname = _platoondata.groupname -- #string
--                  local coordinate = _platoondata.coordinate -- Core.Point#COORDINATE
--                  Your_Brigade:LoadBackAssetInPosition(groupname,coordinate)
--                end
--              end
--            end
--  
--            local AssetLoader = TIMER:New(LoadBackAssets)
--            AssetLoader:Start(20)
-- 
-- The assets loaded back into the mission will be considered for AUFTRAG type missions from CHIEF and BRIGADE.
function BRIGADE:LoadBackAssetInPosition(Templatename,Position)
  self:T(self.lid .. "LoadBackAssetInPosition: " .. tostring(Templatename))
  
  -- get Platoon alias from Templatename
  local nametbl = UTILS.Split(Templatename,"_")

  local name = nametbl[1]
  
  self:T(string.format("*** Target Platoon = %s ***",name))
  
  -- find a matching asset table from BRIGADE
  local cohorts = self.cohorts or {}
  local thisasset = nil --Functional.Warehouse#WAREHOUSE.Assetitem
  local found = false
  
  for _,_cohort in pairs(cohorts) do
    local asset = _cohort:GetName()
    self:T(string.format("*** Looking at Platoon = %s ***",asset))
    if asset == name then
      self:T("**** Found Platoon ****")
      local cohassets = _cohort.assets or {}
      for _,_zug in pairs (cohassets) do
        local zug = _zug -- Functional.Warehouse#WAREHOUSE.Assetitem   
        if zug.assignment == name and zug.requested == false then
          self:T("**** Found Asset ****")
          found = true
          thisasset = zug --Functional.Warehouse#WAREHOUSE.Assetitem         
        break
        end
      end
    end
  end
  
  if found then
  
    -- prep asset
    thisasset.rid = thisasset.uid
    thisasset.requested = false
    thisasset.score=100
    thisasset.missionTask="CAS"
    thisasset.spawned = true
    local template = thisasset.templatename
    local alias = thisasset.spawngroupname
            
    -- Spawn group
    local spawnasset = SPAWN:NewWithAlias(template,alias)
      :InitDelayOff()
      :SpawnFromCoordinate(Position)
    
    -- build a new self request
    local request = {} --Functional.Warehouse#WAREHOUSE.Pendingitem
    request.assignment = name
    request.warehouse = self
    request.assets = {thisasset}
    request.ntransporthome = 0
    request.ndelivered = 0
    request.ntransport = 0
    request.cargoattribute = thisasset.attribute
    request.category = thisasset.category
    request.cargoassets = {thisasset}
    request.assetdesc = WAREHOUSE.Descriptor.ASSETLIST
    request.cargocategory = thisasset.category
    request.toself = true
    request.transporttype = WAREHOUSE.TransportType.SELFPROPELLED
    request.assetproblem = {}
    request.born = true
    request.prio = 50
    request.uid = thisasset.uid
    request.airbase = nil
    request.timestamp = timer.getAbsTime()
    request.assetdescval = {thisasset}
    request.nasset = 1
    request.cargogroupset = SET_GROUP:New()
    request.cargogroupset:AddGroup(spawnasset)
    request.iscargo = true
    
    -- Call Brigade self
    self:__AssetSpawned(2, spawnasset, thisasset, request) 
     
  end
  return self
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Start BRIGADE FSM.
-- @param #BRIGADE self
function BRIGADE:onafterStart(From, Event, To)

  -- Start parent Warehouse.
  self:GetParent(self, BRIGADE).onafterStart(self, From, Event, To)

  -- Info.
  self:I(self.lid..string.format("Starting BRIGADE v%s", BRIGADE.version))

end

--- Update status.
-- @param #BRIGADE self
function BRIGADE:onafterStatus(From, Event, To)

  -- Status of parent Warehouse.
  self:GetParent(self).onafterStatus(self, From, Event, To)

  -- FSM state.
  local fsmstate=self:GetState()
  
  ----------------
  -- Transport ---
  ----------------
  
  self:CheckTransportQueue()

  --------------
  -- Mission ---
  --------------

  -- Check if any missions should be cancelled.
  self:CheckMissionQueue()

  ---------------------
  -- Rearming Zones ---
  ---------------------

  for _,_rearmingzone in pairs(self.rearmingZones) do
    local rearmingzone=_rearmingzone --#BRIGADE.SupplyZone
    if (not rearmingzone.mission) or rearmingzone.mission:IsOver() then
      rearmingzone.mission=AUFTRAG:NewAMMOSUPPLY(rearmingzone.zone)
      self:AddMission(rearmingzone.mission)
    end
  end

  -----------------------
  -- Refuelling Zones ---
  -----------------------

  -- Check refuelling zones.
  for _,_supplyzone in pairs(self.refuellingZones) do
    local supplyzone=_supplyzone --#BRIGADE.SupplyZone
    -- Check if mission is nil or over.      
    if (not supplyzone.mission) or supplyzone.mission:IsOver() then
      supplyzone.mission=AUFTRAG:NewFUELSUPPLY(supplyzone.zone)
      self:AddMission(supplyzone.mission)
    end
  end    


  -----------
  -- Info ---
  -----------    

  -- Display tactival overview.
  self:_TacticalOverview()

  -- General info:
  if self.verbose>=1 then

    -- Count missions not over yet.
    local Nmissions=self:CountMissionsInQueue()

    -- Asset count.
    local Npq, Np, Nq=self:CountAssetsOnMission()

    -- Asset string.
    local assets=string.format("%d [OnMission: Total=%d, Active=%d, Queued=%d]", self:CountAssets(), Npq, Np, Nq)

    -- Output.
    local text=string.format("%s: Missions=%d, Platoons=%d, Assets=%s", fsmstate, Nmissions, #self.cohorts, assets)
    self:I(self.lid..text)
  end

  ------------------
  -- Mission Info --
  ------------------
  if self.verbose>=2 then
    local text=string.format("Missions Total=%d:", #self.missionqueue)
    for i,_mission in pairs(self.missionqueue) do
      local mission=_mission --Ops.Auftrag#AUFTRAG

      local prio=string.format("%d/%s", mission.prio, tostring(mission.importance)) ; if mission.urgent then prio=prio.." (!)" end
      local assets=string.format("%d/%d", mission:CountOpsGroups(), mission.Nassets or 0)
      local target=string.format("%d/%d Damage=%.1f", mission:CountMissionTargets(), mission:GetTargetInitialNumber(), mission:GetTargetDamage())

      text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mission.status, prio, assets, target)
    end
    self:I(self.lid..text)
  end

  --------------------
  -- Transport Info --
  --------------------
  if self.verbose>=2 then
    local text=string.format("Transports Total=%d:", #self.transportqueue)
    for i,_transport in pairs(self.transportqueue) do
      local transport=_transport --Ops.OpsTransport#OPSTRANSPORT

      local prio=string.format("%d/%s", transport.prio, tostring(transport.importance)) ; if transport.urgent then prio=prio.." (!)" end
      local carriers=string.format("Ncargo=%d/%d, Ncarriers=%d", transport.Ncargo, transport.Ndelivered, transport.Ncarrier)

      text=text..string.format("\n[%d] UID=%d: Status=%s, Prio=%s, Cargo: %s", i, transport.uid, transport:GetState(), prio, carriers)
    end
    self:I(self.lid..text)
  end

  -------------------
  -- Platoon Info --
  -------------------
  if self.verbose>=3 then
    local text="Platoons:"
    for i,_platoon in pairs(self.cohorts) do
      local platoon=_platoon --Ops.Platoon#PLATOON

      local callsign=platoon.callsignName and UTILS.GetCallsignName(platoon.callsignName) or "N/A"
      local modex=platoon.modex and platoon.modex or -1
      local skill=platoon.skill and tostring(platoon.skill) or "N/A"

      -- Platoon text.
      text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", platoon.name, platoon:GetState(), platoon.aircrafttype, platoon:CountAssets(true), #platoon.assets, callsign, modex, skill)
    end
    self:I(self.lid..text)
  end

  -------------------
  -- Rearming Info --
  -------------------
  if self.verbose>=4 then
    local text="Rearming Zones:"
    for i,_rearmingzone in pairs(self.rearmingZones) do
      local rearmingzone=_rearmingzone --#BRIGADE.SupplyZone
      -- Info text.
      text=text..string.format("\n* %s: Mission status=%s, suppliers=%d", rearmingzone.zone:GetName(), rearmingzone.mission:GetState(), rearmingzone.mission:CountOpsGroups())      
    end
    self:I(self.lid..text)
  end
  
  ---------------------
  -- Refuelling Info --
  ---------------------
  if self.verbose>=4 then
    local text="Refuelling Zones:"
    for i,_refuellingzone in pairs(self.refuellingZones) do
      local refuellingzone=_refuellingzone --#BRIGADE.SupplyZone
      -- Info text.
      text=text..string.format("\n* %s: Mission status=%s, suppliers=%d", refuellingzone.zone:GetName(), refuellingzone.mission:GetState(), refuellingzone.mission:CountOpsGroups())      
    end
    self:I(self.lid..text)
  end
  
  ----------------
  -- Asset Info --
  ----------------
  if self.verbose>=5 then
    local text="Assets in stock:"
    for i,_asset in pairs(self.stock) do
      local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
      -- Info text.
      text=text..string.format("\n* %s: spawned=%s", asset.spawngroupname, tostring(asset.spawned))      
    end
    self:I(self.lid..text)
  end    

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "ArmyOnMission".
-- @param #BRIGADE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup Ops army group on mission.
-- @param Ops.Auftrag#AUFTRAG Mission The requested mission.
function BRIGADE:onafterArmyOnMission(From, Event, To, ArmyGroup, Mission)
  -- Debug info.
  self:T(self.lid..string.format("Group %s on %s mission %s", ArmyGroup:GetName(), Mission:GetType(), Mission:GetName()))  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Chief of Staff.
--
-- **Main Features:**
--
--    * Automatic target engagement based on detection network
--    * Define multiple border, conflict and attack zones
--    * Define strategic "capture" zones 
--    * Set strategy of chief from passive to agressive
--    * Manual target engagement via AUFTRAG and TARGET classes
--    * Add AIRWINGS, BRIGADES and FLEETS as resources
--    * Seamless air-to-air, air-to-ground, ground-to-ground dispatching
--
-- ===
--
-- ### Author: **funkyfranky**
-- @module Ops.Chief
-- @image OPS_Chief.png


--- CHIEF class.
-- @type CHIEF
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field #table targetqueue Target queue.
-- @field #table zonequeue Strategic zone queue.
-- @field Core.Set#SET_ZONE borderzoneset Set of zones defining the border of our territory.
-- @field Core.Set#SET_ZONE yellowzoneset Set of zones defining the extended border. Defcon is set to YELLOW if enemy activity is detected.
-- @field Core.Set#SET_ZONE engagezoneset Set of zones where enemies are actively engaged.
-- @field #number threatLevelMin Lowest threat level of targets to attack.
-- @field #number threatLevelMax Highest threat level of targets to attack.
-- @field #string Defcon Defence condition.
-- @field #string strategy Strategy of the CHIEF.
-- @field Ops.Commander#COMMANDER commander Commander of assigned legions.
-- @field #number Nsuccess Number of successful missions.
-- @field #number Nfailure Number of failed mission.
-- @field #table assetNumbers Asset numbers. Each entry is a table of data type `#CHIEF.AssetNumber`.
-- @extends Ops.Intel#INTEL

--- *In preparing for battle I have always found that plans are useless, but planning is indispensable* -- Dwight D Eisenhower
--
-- ===
--
-- # The CHIEF Concept
-- 
-- The Chief of staff gathers INTEL and assigns missions (AUFTRAG) to the airforce, army and/or navy. The distinguished feature here is that this class combines all three
-- forces under one hood. Therefore, this class be used as an air-to-air, air-to-ground, ground-to-ground, air-to-sea, sea-to-ground, etc. dispachter.
-- 
-- # Territory
-- 
-- The chief class allows you to define boarder zones, conflict zones and attack zones.
-- 
-- ## Border Zones
-- 
-- Border zones define your own territory.
-- They can be set via the @{#CHIEF.SetBorderZones}() function as a set or added zone by zone via the @{#CHIEF.AddBorderZone}() function.
-- 
-- ## Conflict Zones
-- 
-- Conflict zones define areas, which usually are under dispute of different coalitions.
-- They can be set via the @{#CHIEF.SetConflictZones}() function as a set or added zone by zone via the @{#CHIEF.AddConflictZone}() function.
-- 
-- ## Attack Zones
-- 
-- Attack zones are zones that usually lie within the enemy territory. They are only enganged with an agressive strategy.
-- They can be set via the @{#CHIEF.SetAttackZones}() function as a set or added zone by zone via the @{#CHIEF.AddAttackZone}() function.
--
-- # Defense Condition
-- 
-- The defence condition (DEFCON) depends on enemy activity detected in the different zone types and is set automatically.
-- 
-- * `CHIEF.Defcon.GREEN`: No enemy activities detected.
-- * `CHIEF.Defcon.YELLOW`: Enemy activity detected in conflict zones.
-- * `CHIEF.Defcon.RED`: Enemy activity detected in border zones.
-- 
-- The current DEFCON can be retrieved with the @(#CHIEF.GetDefcon)() function.
-- 
-- When the DEFCON changed, an FSM event @{#CHIEF.DefconChange} is triggered. Mission designers can hook into this event via the @{#CHIEF.OnAfterDefconChange}() function:
--
--     --- Function called when the DEFCON changes.
--     function myChief:OnAfterDefconChange(From, Event, To, Defcon)
--       local text=string.format("Changed DEFCON to %s", Defcon)
--       MESSAGE:New(text, 120):ToAll()    
--     end
--
-- # Strategy
-- 
-- The strategy of the chief determines, in which areas targets are engaged automatically.
-- 
-- * `CHIEF.Strategy.PASSIVE`: Chief is completely passive. No targets at all are engaged automatically.
-- * `CHIEF.Strategy.DEFENSIVE`: Chief acts defensively. Only targets in his own territory are engaged.
-- * `CHIEF.Strategy.OFFENSIVE`: Chief behaves offensively. Targets in his own territory and in conflict zones are enganged.
-- * `CHIEF.Strategy.AGGRESSIVE`: Chief is aggressive. Targets in his own territory, in conflict zones and in attack zones are enganged.
-- * `CHIEF.Strategy.TOTALWAR`: Anything anywhere is enganged.
-- 
-- The strategy can be set by the @(#CHIEF.SetStrategy)() and retrieved with the @(#CHIEF.GetStrategy)() function.
-- 
-- When the strategy is changed, the FSM event @{#CHIEF.StrategyChange} is triggered and customized code can be added to the @{#CHIEF.OnAfterStrategyChange}() function:
-- 
--     --- Function called when the STRATEGY changes.
--     function myChief:OnAfterStrategyChange(From, Event, To, Strategy)
--       local text=string.format("Strategy changd to %s", Strategy)
--       MESSAGE:New(text, 120):ToAll()
--     end
--     
-- # Resources
-- 
-- A chief needs resources such as air, ground and naval assets. These can be added in form of AIRWINGs, BRIGADEs and FLEETs.
-- 
-- Whenever the chief detects a target or receives a mission, he will select the best available assets and assign them to the mission.
-- The best assets are determined by their mission performance, payload performance (in case of air), distance to the target, skill level, etc.
-- 
-- ## Adding Airwings 
-- 
-- Airwings can be added via the @{#CHIEF.AddAirwing}() function.
-- 
-- ## Adding Brigades
-- 
-- Brigades can be added via the @{#CHIEF.AddBrigade}() function.
-- 
-- ## Adding Fleets
-- 
-- Fleets can be added via the @{#CHIEF.AddFleet}() function.
-- 
-- ## Response on Target
-- 
-- When the chief detects a valid target, he will launch a certain number of selected assets. Only whole groups from SQUADRONs, PLATOONs or FLOTILLAs can be selected.
-- In other words, it is not possible to specify the abount of individual *units*.
-- 
-- By default, one group is selected for any detected target. This can, however, be customized with the @{#CHIEF.SetResponseOnTarget}() function. The number of min and max
-- asset groups can be specified depending on threatlevel, category, mission type, number of units, defcon and strategy.
-- 
-- For example:
--
--     -- One group for aircraft targets of threat level 0 or higher.
--     myChief:SetResponseOnTarget(1, 1, 0, TARGET.Category.AIRCRAFT)
--     -- At least one and up to two groups for aircraft targets of threat level 8 or higher. This will overrule the previous response!
--     myChief:SetResponseOnTarget(1, 2, 8, TARGET.Category.AIRCRAFT)
--     
--     -- At least one and up to three groups for ground targets of threat level 0 or higher if current strategy is aggressive.  
--     myChief:SetResponseOnTarget(1, 1, 0, TARGET.Category.GROUND, nil ,nil, nil, CHIEF.Strategy.DEFENSIVE)
--     
--     -- One group for BAI missions if current defcon is green.
--     myChief:SetResponseOnTarget(1, 1, 0, nil, AUFTRAG.Type.BAI, nil, CHIEF.DEFCON.GREEN)
--     
--     -- At least one and up to four groups for BAI missions if current defcon is red.
--     myChief:SetResponseOnTarget(1, 2, 0, nil, AUFTRAG.Type.BAI, nil, CHIEF.DEFCON.YELLOW)
--     
--     -- At least one and up to four groups for BAI missions if current defcon is red.
--     myChief:SetResponseOnTarget(1, 3, 0, nil, AUFTRAG.Type.BAI, nil, CHIEF.DEFCON.RED)
-- 
--  
-- # Strategic (Capture) Zones
-- 
-- Strategically important zones, which should be captured can be added via the @{#CHIEF.AddStrategicZone}(*OpsZone, Prio, Importance*) function.
-- The first parameter *OpsZone* is an @{Ops.OpsZone#OPSZONE} specifying the zone. This has to be a **circular zone** due to DCS API restrictions.
-- The second parameter *Prio* is the priority. The zone queue is sorted wrt to lower prio values. By default this is set to 50.
-- The third parameter *Importance* is the importance of the zone. By default this is `nil`. If you specify one zone with importance 2 and a second zone with
-- importance 3, then the zone of importance 2 is attacked first and only if that zone has been captured, zones that have importances with higher values are attacked.
-- 
-- For example:
-- 
--     local myStratZone=myChief:AddStrategicZone(myOpsZone, nil , 2)
-- 
-- Will at a strategic zone with importance 2.
-- 
-- If the zone is currently owned by another coalition and enemy ground troops are present in the zone, a CAS and an ARTY mission are launched:
-- 
-- * A mission of type `AUFTRAG.Type.CASENHANCED` is started if assets are available that can carry out this mission type.
-- * A mission of type `AUFTRAG.Type.ARTY` is started provided assets are available.
-- 
-- The CAS flight(s) will patrol the zone randomly and take out enemy ground units they detect. It can always be possible that the enemies cannot be detected however.
-- The assets will shell the zone. However, it is unlikely that they hit anything as they do not have any information about the location of the enemies.
-- 
-- Once the zone is cleaned of enemy forces, ground troops are send there. By default, two missions are launched:
-- 
-- * First mission is of type `AUFTRAG.Type.ONGUARD` and will send infantry groups. These are transported by helicopters. Therefore, helo assets with `AUFTRAG.Type.OPSTRANSPORT` need to be available.
-- * The second mission is also of type `AUFTRAG.Type.ONGUARD` but will send tanks if these are available.
-- 
-- ## Customized Reaction
-- 
-- The default mission types and number of assets can be customized for the two scenarious (zone empty or zone occupied by the enemy).
-- 
-- In order to do this, you need to create resource lists (one for each scenario) via the @{#CHIEF.CreateResource}() function.
-- These lists can than passed as additional parameters to the @{#CHIEF.AddStrategicZone} function.
-- 
-- For example:
--     
--     --- Create a resource list of mission types and required assets for the case that the zone is OCCUPIED.
--     --
--     -- Here, we create an enhanced CAS mission and employ at least on and at most two asset groups.
--     -- NOTE that two objects are returned, the resource list (ResourceOccupied) and the first resource of that list (resourceCAS).
--     local ResourceOccupied, resourceCAS=myChief:CreateResource(AUFTRAG.Type.CASENHANCED, 1, 2)
--     -- We also add ARTY missions with at least one and at most two assets. We additionally require these to be MLRS groups (and not howitzers).
--     myChief:AddToResource(ResourceOccupied, AUFTRAG.Type.ARTY, 1, 2, nil, "MLRS")
--     -- Add at least one RECON mission that uses UAV type assets.
--     myChief:AddToResource(ResourceOccupied, AUFTRAG.Type.RECON, 1, nil, GROUP.Attribute.AIR_UAV)
--     -- Add at least one but at most two BOMBCARPET missions.
--     myChief:AddToResource(ResourceOccupied, AUFTRAG.Type.BOMBCARPET, 1, 2)
--     
--     --- Create a resource list of mission types and required assets for the case that the zone is EMPTY.
--     -- NOTE that two objects are returned, the resource list (ResourceEmpty) and the first resource of that list (resourceInf).
--     -- Here, we create an ONGUARD mission and employ at least on and at most five infantry assets.
--     local ResourceEmpty, resourceInf=myChief:CreateResource(AUFTRAG.Type.ONGUARD, 1, 5, GROUP.Attribute.GROUND_INFANTRY)
--     -- Additionally, we send up to three tank groups.
--     myChief:AddToResource(ResourceEmpty, AUFTRAG.Type.ONGUARD, 1, 3, GROUP.Attribute.GROUND_TANK)
--     -- Finally, we send two groups that patrol the zone.
--     myChief:AddToResource(ResourceEmpty, AUFTRAG.Type.PATROLZONE, 2)
--     
--     -- Add a transport to the infantry resource. We want at least one and up to two transport helicopters.
--     myChief:AddTransportToResource(resourceInf, 1, 2, GROUP.Attribute.AIR_TRANSPORTHELO)
--     
--     -- Add stratetic zone with customized reaction.
--     myChief:AddStrategicZone(myOpsZone, nil , 2, ResourceOccupied, ResourceEmpty)
-- 
-- As the location of the enemies is not known, only mission types that don't require an explicit target group are possible. These are
-- 
-- * `AUFTRAG.Type.CASENHANCED`
-- * `AUFTRAG.Type.ARTY`
-- * `AUFTRAG.Type.PATROLZONE`
-- * `AUFTRAG.Type.ONGUARD`
-- * `AUFTRAG.Type.CAPTUREZONE`
-- * `AUFTRAG.Type.RECON`
-- * `AUFTRAG.Type.AMMOSUPPLY`
-- * `AUFTRAG.Type.BOMBING`
-- * `AUFTRAG.Type.BOMBCARPET`
-- * `AUFTRAG.Type.BARRAGE`
-- 
-- ## Events
-- 
-- Whenever a strategic zone is captured by us the FSM event @{#CHIEF.ZoneCaptured} is triggered and customized further actions can be executed 
-- with the @{#CHIEF.OnAfterZoneCaptured}() function.
-- 
-- Whenever a strategic zone is lost (captured by the enemy), the FSM event @{#CHIEF.ZoneLost} is triggered and customized further actions can be executed 
-- with the @{#CHIEF.OnAfterZoneLost}() function.
-- 
-- Further events are 
-- 
-- * @{#CHIEF.ZoneEmpty}, once the zone is completely empty of ground troops. Code can be added to the  @{#CHIEF.OnAfterZoneEmpty}() function.
-- * @{#CHIEF.ZoneAttacked}, once the zone is under attack. Code can be added to the  @{#CHIEF.OnAfterZoneAttacked}() function.
-- 
-- Note that the ownership of a zone is determined via zone scans, i.e. not via the detection network. In other words, there is an all knowing eye.
-- Think of it as the local population providing the intel. It's not totally realistic but the best compromise within the limits of DCS.
-- 
-- 
--
-- @field #CHIEF
CHIEF = {
  ClassName      = "CHIEF",
  verbose        =     0,
  lid            =   nil,
  targetqueue    =    {},
  zonequeue      =    {},
  borderzoneset  =   nil,
  yellowzoneset  =   nil,
  engagezoneset  =   nil,
  tacview        = false,
  Nsuccess       =     0,
  Nfailure       =     0,
}

--- Defence condition.
-- @type CHIEF.DEFCON
-- @field #string GREEN No enemy activities detected in our terretory or conflict zones.
-- @field #string YELLOW Enemy in conflict zones.
-- @field #string RED Enemy within our border.
CHIEF.DEFCON = {
  GREEN="Green",
  YELLOW="Yellow",
  RED="Red",
}

--- Strategy.
-- @type CHIEF.Strategy
-- @field #string PASSIVE No targets at all are engaged.
-- @field #string DEFENSIVE Only target in our own terretory are engaged.
-- @field #string OFFENSIVE Targets in own terretory and yellow zones are engaged.
-- @field #string AGGRESSIVE Targets in own terretory, conflict zones and attack zones are engaged.
-- @field #string TOTALWAR Anything is engaged anywhere.
CHIEF.Strategy = {
  PASSIVE="Passive",
  DEFENSIVE="Defensive",
  OFFENSIVE="Offensive",
  AGGRESSIVE="Aggressive",
  TOTALWAR="Total War"
}

--- Mission performance.
-- @type CHIEF.MissionPerformance
-- @field #string MissionType Mission Type.
-- @field #number Performance Performance: a number between 0 and 100, where 100 is best performance.

--- Asset numbers for detected targets.
-- @type CHIEF.AssetNumber
-- @field #number nAssetMin Min number of assets.
-- @field #number nAssetMax Max number of assets.
-- @field #number threatlevel Threat level.
-- @field #string targetCategory Target category.
-- @field #string missionType Mission type.
-- @field #number nUnits Number of enemy units.
-- @field #string defcon Defense condition.
-- @field #string strategy Strategy.

--- Strategic zone.
-- @type CHIEF.StrategicZone
-- @field Ops.OpsZone#OPSZONE opszone OPS zone.
-- @field #number prio Priority.
-- @field #number importance Importance.
-- @field #CHIEF.Resources resourceEmpty List of resources employed when the zone is empty.
-- @field #CHIEF.Resources resourceOccup List of resources employed when the zone is occupied by an enemy.
-- @field #table missions Mission.

--- Resource list.
-- @type CHIEF.Resources
-- @field #CHIEF.Resource List of resources.

--- Resource.
-- @type CHIEF.Resource
-- @field #string MissionType Mission type, e.g. `AUFTRAG.Type.BAI`.
-- @field #number Nmin Min number of assets.
-- @field #number Nmax Max number of assets.
-- @field #table Attributes Generalized attribute, e.g. `{GROUP.Attribute.GROUND_INFANTRY}`.
-- @field #table Properties Properties ([DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes)), e.g. `"Attack helicopters"` or `"Mobile AAA"`.
-- @field #table Categories Categories Group categories.
-- @field Ops.Auftrag#AUFTRAG mission Attached mission.
-- @field #number carrierNmin Min number of assets.
-- @field #number carrierNmax Max number of assets.
-- @field #table carrierCategories Group categories.
-- @field #table carrierAttributes Generalized attribute, e.g. `{GROUP.Attribute.GROUND_INFANTRY}`.
-- @field #table carrierProperties Properties ([DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes)), e.g. `"Attack helicopters"` or `"Mobile AAA"`.


--- CHIEF class version.
-- @field #string version
CHIEF.version="0.6.1"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Event when asset groups die.
-- TODO: PLAYERTASK integration.
-- DONE: Let user specify amount of resources.
-- DONE: Tactical overview.
-- DONE: Add event for opsgroups on mission.
-- DONE: Add event for zone captured. 
-- DONE: Limits of missions?
-- DONE: Create a good mission, which can be passed on to the COMMANDER.
-- DONE: Capture OPSZONEs.
-- DONE: Get list of own assets and capabilities.
-- DONE: Get list/overview of enemy assets etc.
-- DONE: Put all contacts into target list. Then make missions from them.
-- DONE: Set of interesting zones.
-- DONE: Add/remove spawned flightgroups to detection set.
-- DONE: Borderzones.
-- NOGO: Maybe it's possible to preselect the assets for the mission.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new CHIEF object and start the FSM.
-- @param #CHIEF self
-- @param #number Coalition Coalition side, e.g. `coaliton.side.BLUE`. Can also be passed as a string "red", "blue" or "neutral".
-- @param Core.Set#SET_GROUP AgentSet Set of agents (groups) providing intel. Default is an empty set.
-- @param #string Alias An *optional* alias how this object is called in the logs etc.
-- @return #CHIEF self
function CHIEF:New(Coalition, AgentSet, Alias)

  -- Set alias.
  Alias=Alias or "CHIEF"
  
  -- coalition
  if type(Coalition) == "string" then
    if string.lower(Coalition) == "blue" then
      Coalition = coalition.side.BLUE
    elseif string.lower(Coalition) == "red" then
      Coalition = coalition.side.RED
    else
      Coalition = coalition.side.NEUTRAL
    end
  end
  
  -- Inherit everything from INTEL class.
  local self=BASE:Inherit(self, INTEL:New(AgentSet, Coalition, Alias)) --#CHIEF

  -- Defaults.
  self:SetBorderZones()
  self:SetConflictZones()
  self:SetAttackZones()
  self:SetThreatLevelRange()
  
  -- Init stuff.
  self.Defcon=CHIEF.DEFCON.GREEN
  self.strategy=CHIEF.Strategy.DEFENSIVE
  self.TransportCategories = {Group.Category.HELICOPTER}
  
  -- Create a new COMMANDER.
  self.commander=COMMANDER:New(Coalition)  

  -- Add FSM transitions.
  --                 From State   -->    Event                     -->    To State
  self:AddTransition("*",                "MissionAssign",                 "*")   -- Assign mission to a COMMANDER.  
  self:AddTransition("*",                "MissionCancel",                 "*")   -- Cancel mission.
  
  self:AddTransition("*",                "TransportCancel",               "*")   -- Cancel transport.
  
  self:AddTransition("*",                "OpsOnMission",                  "*")   -- An OPSGROUP was send on a Mission (AUFTRAG).
  
  self:AddTransition("*",                "ZoneCaptured",                  "*")   -- 
  self:AddTransition("*",                "ZoneLost",                      "*")   --
  self:AddTransition("*",                "ZoneEmpty",                     "*")   --
  self:AddTransition("*",                "ZoneAttacked",                  "*")   --
  
  self:AddTransition("*",                "DefconChange",                  "*")   -- Change defence condition. 
  self:AddTransition("*",                "StrategyChange",                "*")   -- Change strategy condition.

  self:AddTransition("*",                "LegionLost",                    "*")   -- Out of our legions was lost to the enemy.

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start".
  -- @function [parent=#CHIEF] Start
  -- @param #CHIEF self

  --- Triggers the FSM event "Start" after a delay.
  -- @function [parent=#CHIEF] __Start
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Stop".
  -- @param #CHIEF self

  --- Triggers the FSM event "Stop" after a delay.
  -- @function [parent=#CHIEF] __Stop
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Status".
  -- @function [parent=#CHIEF] Status
  -- @param #CHIEF self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#CHIEF] __Status
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "DefconChange".
  -- @function [parent=#CHIEF] DefconChange
  -- @param #CHIEF self
  -- @param #string Defcon New Defence Condition.

  --- Triggers the FSM event "DefconChange" after a delay.
  -- @function [parent=#CHIEF] __DefconChange
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.
  -- @param #string Defcon New Defence Condition.

  --- On after "DefconChange" event.
  -- @function [parent=#CHIEF] OnAfterDefconChange
  -- @param #CHIEF self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string Defcon New Defence Condition.


  --- Triggers the FSM event "StrategyChange".
  -- @function [parent=#CHIEF] StrategyChange
  -- @param #CHIEF self
  -- @param #string Strategy New strategy.

  --- Triggers the FSM event "StrategyChange" after a delay.
  -- @function [parent=#CHIEF] __StrategyChange
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.
  -- @param #string Strategy New strategy.

  --- On after "StrategyChange" event.
  -- @function [parent=#CHIEF] OnAfterStrategyChange
  -- @param #CHIEF self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string Strategy New strategy.


  --- Triggers the FSM event "MissionAssign".
  -- @function [parent=#CHIEF] MissionAssign
  -- @param #CHIEF self
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.
  -- @param #table Legions The Legion(s) to which the mission is assigned.

  --- Triggers the FSM event "MissionAssign" after a delay.
  -- @function [parent=#CHIEF] __MissionAssign
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.
  -- @param #table Legions The Legion(s) to which the mission is assigned.

  --- On after "MissionAssign" event.
  -- @function [parent=#CHIEF] OnAfterMissionAssign
  -- @param #CHIEF self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.
  -- @param #table Legions The Legion(s) to which the mission is assigned.

  --- Triggers the FSM event "MissionCancel".
  -- @function [parent=#CHIEF] MissionCancel
  -- @param #CHIEF self
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- Triggers the FSM event "MissionCancel" after a delay.
  -- @function [parent=#CHIEF] __MissionCancel
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "MissionCancel" event.
  -- @function [parent=#CHIEF] OnAfterMissionCancel
  -- @param #CHIEF self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.


  --- Triggers the FSM event "TransportCancel".
  -- @function [parent=#CHIEF] TransportCancel
  -- @param #CHIEF self
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.

  --- Triggers the FSM event "TransportCancel" after a delay.
  -- @function [parent=#CHIEF] __TransportCancel
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.

  --- On after "TransportCancel" event.
  -- @function [parent=#CHIEF] OnAfterTransportCancel
  -- @param #CHIEF self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.


  --- Triggers the FSM event "OpsOnMission".
  -- @function [parent=#CHIEF] OpsOnMission
  -- @param #CHIEF self
  -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- Triggers the FSM event "OpsOnMission" after a delay.
  -- @function [parent=#CHIEF] __OpsOnMission
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "OpsOnMission" event.
  -- @function [parent=#CHIEF] OnAfterOpsOnMission
  -- @param #CHIEF self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.


  --- Triggers the FSM event "ZoneCaptured".
  -- @function [parent=#CHIEF] ZoneCaptured
  -- @param #CHIEF self
  -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was captured.

  --- Triggers the FSM event "ZoneCaptured" after a delay.
  -- @function [parent=#CHIEF] __ZoneCaptured
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was captured.

  --- On after "ZoneCaptured" event.
  -- @function [parent=#CHIEF] OnAfterZoneCaptured
  -- @param #CHIEF self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was captured. 

  --- Triggers the FSM event "ZoneLost".
  -- @function [parent=#CHIEF] ZoneLost
  -- @param #CHIEF self
  -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was lost.

  --- Triggers the FSM event "ZoneLost" after a delay.
  -- @function [parent=#CHIEF] __ZoneLost
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was lost.

  --- On after "ZoneLost" event.
  -- @function [parent=#CHIEF] OnAfterZoneLost
  -- @param #CHIEF self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsZone#OPSZONE OpsZone Zone that was lost. 

  --- Triggers the FSM event "ZoneEmpty".
  -- @function [parent=#CHIEF] ZoneEmpty
  -- @param #CHIEF self
  -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is empty now.

  --- Triggers the FSM event "ZoneEmpty" after a delay.
  -- @function [parent=#CHIEF] __ZoneEmpty
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is empty now.

  --- On after "ZoneEmpty" event.
  -- @function [parent=#CHIEF] OnAfterZoneEmpty
  -- @param #CHIEF self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is empty now.

  --- Triggers the FSM event "ZoneAttacked".
  -- @function [parent=#CHIEF] ZoneAttacked
  -- @param #CHIEF self
  -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is being attacked.

  --- Triggers the FSM event "ZoneAttacked" after a delay.
  -- @function [parent=#CHIEF] __ZoneAttacked
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is being attacked.

  --- On after "ZoneAttacked" event.
  -- @function [parent=#CHIEF] OnAfterZoneAttacked
  -- @param #CHIEF self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsZone#OPSZONE OpsZone Zone that is being attacked. 


  --- Triggers the FSM event "LegionLost".
  -- @function [parent=#CHIEF] LegionLost
  -- @param #CHIEF self
  -- @param Ops.Legion#LEGION Legion The legion that was lost.
  -- @param DCS#coalition.side Coalition which captured the warehouse.
  -- @param DCS#country.id Country which has captured the warehouse.

  --- Triggers the FSM event "LegionLost".
  -- @function [parent=#CHIEF] __LegionLost
  -- @param #CHIEF self
  -- @param #number delay Delay in seconds.
  -- @param Ops.Legion#LEGION Legion The legion that was lost.
  -- @param DCS#coalition.side Coalition which captured the warehouse.
  -- @param DCS#country.id Country which has captured the warehouse.

  --- On after "LegionLost" event.
  -- @function [parent=#CHIEF] OnAfterLegionLost
  -- @param #CHIEF self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Legion#LEGION Legion The legion that was lost.
  -- @param DCS#coalition.side Coalition which captured the warehouse.
  -- @param DCS#country.id Country which has captured the warehouse.


  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set this to be an air-to-any dispatcher, i.e. engaging air, ground and naval targets. This is the default anyway.
-- @param #CHIEF self
-- @return #CHIEF self
function CHIEF:SetAirToAny()

  self:SetFilterCategory({})
  
  return self
end

--- Set this to be an air-to-air dispatcher.
-- @param #CHIEF self
-- @return #CHIEF self
function CHIEF:SetAirToAir()

  self:SetFilterCategory({Unit.Category.AIRPLANE, Unit.Category.HELICOPTER})
  
  return self
end

--- Set this to be an air-to-ground dispatcher, i.e. engage only ground units
-- @param #CHIEF self
-- @return #CHIEF self
function CHIEF:SetAirToGround()

  self:SetFilterCategory({Unit.Category.GROUND_UNIT})
  
  return self
end

--- Set this to be an air-to-sea dispatcher, i.e. engage only naval units.
-- @param #CHIEF self
-- @return #CHIEF self
function CHIEF:SetAirToSea()

  self:SetFilterCategory({Unit.Category.SHIP})
  
  return self
end

--- Set this to be an air-to-surface dispatcher, i.e. engaging ground and naval groups.
-- @param #CHIEF self
-- @return #CHIEF self
function CHIEF:SetAirToSurface()

  self:SetFilterCategory({Unit.Category.GROUND_UNIT, Unit.Category.SHIP})
  
  return self
end

--- Set a threat level range that will be engaged. Threat level is a number between 0 and 10, where 10 is a very dangerous threat.
-- Targets with threat level 0 are usually harmless.
-- @param #CHIEF self
-- @param #number ThreatLevelMin Min threat level. Default 1.
-- @param #number ThreatLevelMax Max threat level. Default 10.
-- @return #CHIEF self
function CHIEF:SetThreatLevelRange(ThreatLevelMin, ThreatLevelMax)

  self.threatLevelMin=ThreatLevelMin or 1
  self.threatLevelMax=ThreatLevelMax or 10
  
  return self
end

--- Set defence condition.
-- @param #CHIEF self
-- @param #string Defcon Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`.
-- @return #CHIEF self
function CHIEF:SetDefcon(Defcon)

  -- Check if valid string was passed.
  local gotit=false
  for _,defcon in pairs(CHIEF.DEFCON) do
    if defcon==Defcon then
      gotit=true
    end
  end  
  if not gotit then
    self:E(self.lid..string.format("ERROR: Unknown DEFCON specified! Dont know defcon=%s", tostring(Defcon)))
    return self
  end
  
  -- Trigger event if defcon changed.
  if Defcon~=self.Defcon then
    self:DefconChange(Defcon)
  end

  -- Set new DEFCON.
  self.Defcon=Defcon
  
  return self
end

--- Create a new resource list of required assets.
-- @param #CHIEF self
-- @param #string MissionType The mission type.
-- @param #number Nmin Min number of required assets. Default 1.
-- @param #number Nmax Max number of requried assets. Default 1.
-- @param #table Attributes Generalized attribute(s). Default `nil`.
-- @param #table Properties DCS attribute(s). Default `nil`.
-- @param #table Categories Group categories.
-- @return #CHIEF.Resources The newly created resource list table.
-- @return #CHIEF.Resource The resource object that was added.
function CHIEF:CreateResource(MissionType, Nmin, Nmax, Attributes, Properties, Categories)

  local resources={}
  
  local resource=self:AddToResource(resources, MissionType, Nmin, Nmax, Attributes, Properties, Categories)
    
  return resources, resource
end

--- Add mission type and number of required assets to resource list.
-- @param #CHIEF self
-- @param #CHIEF.Resources Resource List of resources.
-- @param #string MissionType Mission Type.
-- @param #number Nmin Min number of required assets. Default 1.
-- @param #number Nmax Max number of requried assets. Default equal `Nmin`.
-- @param #table Attributes Generalized attribute(s).
-- @param #table Properties DCS attribute(s). Default `nil`.
-- @param #table Categories Group categories.
-- @return #CHIEF.Resource Resource table.
function CHIEF:AddToResource(Resource, MissionType, Nmin, Nmax, Attributes, Properties, Categories)
    
  -- Create new resource table.
  local resource={} --#CHIEF.Resource
  resource.MissionType=MissionType
  resource.Nmin=Nmin or 1
  resource.Nmax=Nmax or Nmin
  resource.Attributes=UTILS.EnsureTable(Attributes, true)
  resource.Properties=UTILS.EnsureTable(Properties, true)
  resource.Categories=UTILS.EnsureTable(Categories, true)
  
  -- Transport carrier parameters.
  resource.carrierNmin=nil
  resource.carrierNmax=nil
  resource.carrierAttributes=nil
  resource.carrierProperties=nil
  resource.carrierCategories=nil
  
  -- Add to table.
  table.insert(Resource, resource)
  
  -- Debug output.
  if self.verbose>10 then
    local text="Resource:"
    for _,_r in pairs(Resource) do
      local r=_r --#CHIEF.Resource
      text=text..string.format("\nmission=%s, Nmin=%d, Nmax=%d, attribute=%s, properties=%s", r.MissionType, r.Nmin, r.Nmax, tostring(r.Attributes[1]), tostring(r.Properties[1]))
    end
    self:I(self.lid..text)
  end
    
  return resource
end

--- Define which assets will be transported and define the number and attributes/properties of the cargo carrier assets.
-- @param #CHIEF self
-- @param #CHIEF.Resource Resource Resource table.
-- @param #number Nmin Min number of required assets. Default 1.
-- @param #number Nmax Max number of requried assets. Default is equal to `Nmin`.
-- @param #table CarrierAttributes Generalized attribute(s) of the carrier assets.
-- @param #table CarrierProperties DCS attribute(s) of the carrier assets.
-- @param #table CarrierCategories Group categories of the carrier assets.
-- @return #CHIEF self
function CHIEF:AddTransportToResource(Resource, Nmin, Nmax, CarrierAttributes, CarrierProperties, CarrierCategories)

  Resource.carrierNmin=Nmin or 1
  Resource.carrierNmax=Nmax or Nmin
  Resource.carrierCategories=UTILS.EnsureTable(CarrierCategories, true)
  Resource.carrierAttributes=UTILS.EnsureTable(CarrierAttributes, true)
  Resource.carrierProperties=UTILS.EnsureTable(CarrierProperties, true)

  return self
end

--- Delete mission type from resource list. All running missions are cancelled.
-- @param #CHIEF self
-- @param #table Resource Resource table.
-- @param #string MissionType Mission Type.
-- @return #CHIEF self
function CHIEF:DeleteFromResource(Resource, MissionType)
  
  for i=#Resource,1,-1 do
    local resource=Resource[i] --#CHIEF.Resource
    if resource.MissionType==MissionType then
      if resource.mission and resource.mission:IsNotOver() then
        resource.mission:Cancel()
      end
      table.remove(Resource, i)
    end    
  end
  
  return self
end

--- Set number of assets requested for detected targets.
-- @param #CHIEF self
-- @param #number NassetsMin Min number of assets. Should be at least 1. Default 1.
-- @param #number NassetsMax Max number of assets. Default is same as `NassetsMin`.
-- @param #number ThreatLevel Only apply this setting if the target threat level is greater or equal this number. Default 0.
-- @param #string TargetCategory Only apply this setting if the target is of this category, e.g. `TARGET.Category.AIRCRAFT`.
-- @param #string MissionType Only apply this setting for this mission type, e.g. `AUFTRAG.Type.INTERCEPT`.
-- @param #string Nunits Only apply this setting if the number of enemy units is greater or equal this number.
-- @param #string Defcon Only apply this setting if this defense condition is in place.
-- @param #string Strategy Only apply this setting if this strategy is in currently. place.
-- @return #CHIEF self
function CHIEF:SetResponseOnTarget(NassetsMin, NassetsMax, ThreatLevel, TargetCategory, MissionType, Nunits, Defcon, Strategy)
  
  local bla={} --#CHIEF.AssetNumber
  
  bla.nAssetMin=NassetsMin or 1
  bla.nAssetMax=NassetsMax or bla.nAssetMin
  bla.threatlevel=ThreatLevel or 0
  bla.targetCategory=TargetCategory
  bla.missionType=MissionType
  bla.nUnits=Nunits or 1
  bla.defcon=Defcon
  bla.strategy=Strategy
  
  self.assetNumbers=self.assetNumbers or {}
  
  -- Add to table.
  table.insert(self.assetNumbers, bla)
  
end

--- Add mission type and number of required assets to resource.
-- @param #CHIEF self
-- @param Ops.Target#TARGET Target The target.
-- @param #string MissionType Mission type.
-- @return #number Number of min assets.
-- @return #number Number of max assets.
function CHIEF:_GetAssetsForTarget(Target, MissionType)

  -- Threat level.
  local threatlevel=Target:GetThreatLevelMax()
  
  -- Number of units.
  local nUnits=Target.N0
  
  -- Target category.
  local targetcategory=Target:GetCategory()
  
  -- Debug info.
  self:T(self.lid..string.format("Getting number of assets for target with TL=%d, Category=%s, nUnits=%s, MissionType=%s", threatlevel, targetcategory, nUnits, tostring(MissionType)))
  
  -- Candidates.
  local candidates={}
  
  local threatlevelMatch=nil
  for _,_assetnumber in pairs(self.assetNumbers or {}) do
    local assetnumber=_assetnumber --#CHIEF.AssetNumber
    
    if (threatlevelMatch==nil and threatlevel>=assetnumber.threatlevel) or (threatlevelMatch~=nil and threatlevelMatch==threatlevel) then
            
      if threatlevelMatch==nil then
        threatlevelMatch=threatlevel
      end
      
      -- Number of other parameters matching.
      local nMatch=0
      
      -- Assume cand.
      local cand=true
      
      if assetnumber.targetCategory~=nil then
        if assetnumber.targetCategory==targetcategory then
          nMatch=nMatch+1
        else
          cand=false
        end
      end
      
      if MissionType and assetnumber.missionType~=nil then 
        if assetnumber.missionType==MissionType then
          nMatch=nMatch+1
        else
          cand=false
        end
      end
          
      if assetnumber.nUnits~=nil then 
        if assetnumber.nUnits>=nUnits then
          nMatch=nMatch+1
        else
          cand=false
        end
      end
            
      if assetnumber.defcon~=nil then 
        if assetnumber.defcon==self.Defcon then
          nMatch=nMatch+1
        else
          cand=false
        end
      end
      
      if assetnumber.strategy~=nil then
        if assetnumber.strategy==self.strategy then
          nMatch=nMatch+1
        else
          cand=false
        end
      end
    
      -- Add to candidates.
      if cand then
        table.insert(candidates, {assetnumber=assetnumber, nMatch=nMatch})
      end
    
    end
    
  end
  
  if #candidates>0 then
  
    -- Return greater match.
    local function _sort(a,b)
      return a.nMatch>b.nMatch
    end
    
    -- Sort table by matches.
    table.sort(candidates, _sort)
    
    -- Pick the candidate with most matches.
    local candidate=candidates[1]
    
    -- Asset number.
    local an=candidate.assetnumber --#CHIEF.AssetNumber
    
    -- Debug message.
    self:T(self.lid..string.format("Picking candidate with %d matches: NassetsMin=%d, NassetsMax=%d, ThreatLevel=%d, TargetCategory=%s, MissionType=%s, Defcon=%s, Strategy=%s", 
    candidate.nMatch, an.nAssetMin, an.nAssetMax, an.threatlevel, tostring(an.targetCategory), tostring(an.missionType), tostring(an.defcon), tostring(an.strategy)))
     
    -- Return number of assetes.
    return an.nAssetMin, an.nAssetMax
  else
    return 1, 1
  end

end

--- Get defence condition.
-- @param #CHIEF self
-- @param #string Current Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`.
function CHIEF:GetDefcon(Defcon)  
  return self.Defcon
end

--- Set limit for number of total or specific missions to be executed simultaniously.
-- @param #CHIEF self
-- @param #number Limit Number of max. mission of this type. Default 10.
-- @param #string MissionType Type of mission, e.g. `AUFTRAG.Type.BAI`. Default `"Total"` for total number of missions.
-- @return #CHIEF self
function CHIEF:SetLimitMission(Limit, MissionType)
  self.commander:SetLimitMission(Limit, MissionType)
  return self
end

--- Set tactical overview on.
-- @param #CHIEF self
-- @return #CHIEF self
function CHIEF:SetTacticalOverviewOn()  
  self.tacview=true
  return self
end

--- Set tactical overview off.
-- @param #CHIEF self
-- @return #CHIEF self
function CHIEF:SetTacticalOverviewOff()  
  self.tacview=false
  return self
end


--- Set strategy.
-- @param #CHIEF self
-- @param #string Strategy Strategy. See @{#CHIEF.strategy}, e.g. `CHIEF.Strategy.DEFENSIVE` (default).
-- @return #CHIEF self
function CHIEF:SetStrategy(Strategy)

  -- Trigger event if Strategy changed.
  if Strategy~=self.strategy then
    self:StrategyChange(Strategy)
  end

  -- Set new Strategy.
  self.strategy=Strategy
  
  return self
end

--- Get current strategy.
-- @param #CHIEF self
-- @return #string Strategy.
function CHIEF:GetStrategy()
  return self.strategy
end

--- Get defence condition.
-- @param #CHIEF self
-- @param #string Current Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`.
function CHIEF:GetDefcon(Defcon)  
  return self.Defcon
end


--- Get the commander.
-- @param #CHIEF self
-- @return Ops.Commander#COMMANDER The commander.
function CHIEF:GetCommander()
  return self.commander
end


--- Add an AIRWING to the chief's commander.
-- @param #CHIEF self
-- @param Ops.Airwing#AIRWING Airwing The airwing to add.
-- @return #CHIEF self
function CHIEF:AddAirwing(Airwing)

  -- Add airwing to the commander.
  self:AddLegion(Airwing)
  
  return self
end

--- Add a BRIGADE to the chief's commander.
-- @param #CHIEF self
-- @param Ops.Brigade#BRIGADE Brigade The brigade to add.
-- @return #CHIEF self
function CHIEF:AddBrigade(Brigade)

  -- Add brigade to the commander.
  self:AddLegion(Brigade)
  
  return self
end

--- Add a FLEET to the chief's commander.
-- @param #CHIEF self
-- @param Ops.Fleet#FLEET Fleet The fleet to add.
-- @return #CHIEF self
function CHIEF:AddFleet(Fleet)

  -- Add fleet to the commander.
  self:AddLegion(Fleet)
  
  return self
end

--- Add a LEGION to the chief's commander.
-- @param #CHIEF self
-- @param Ops.Legion#LEGION Legion The legion to add.
-- @return #CHIEF self
function CHIEF:AddLegion(Legion)

  -- Set chief of the legion.
  Legion.chief=self

  -- Add legion to the commander.
  self.commander:AddLegion(Legion)
  
  return self
end

--- Remove a LEGION to the chief's commander.
-- @param #CHIEF self
-- @param Ops.Legion#LEGION Legion The legion to add.
-- @return #CHIEF self
function CHIEF:RemoveLegion(Legion)

  -- Set chief of the legion.
  Legion.chief=nil

  -- Add legion to the commander.
  self.commander:RemoveLegion(Legion)
  
  return self
end


--- Add mission to mission queue of the COMMANDER.
-- @param #CHIEF self
-- @param Ops.Auftrag#AUFTRAG Mission Mission to be added.
-- @return #CHIEF self
function CHIEF:AddMission(Mission)

  Mission.chief=self
  
  Mission.statusChief=AUFTRAG.Status.PLANNED
  
  self:I(self.lid..string.format("Adding mission #%d", Mission.auftragsnummer))
  
  self.commander:AddMission(Mission)
  
  return self
end

--- Remove mission from queue.
-- @param #CHIEF self
-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed.
-- @return #CHIEF self
function CHIEF:RemoveMission(Mission)

  Mission.chief=nil
  
  self.commander:RemoveMission(Mission)

  return self
end

--- Add transport to transport queue of the COMMANDER.
-- @param #CHIEF self
-- @param Ops.OpsTransport#OPSTRANSPORT Transport Transport to be added.
-- @return #CHIEF self
function CHIEF:AddOpsTransport(Transport)

  Transport.chief=self
  
  self.commander:AddOpsTransport(Transport)
  
  return self
end

--- Remove transport from queue.
-- @param #CHIEF self
-- @param Ops.OpsTransport#OPSTRANSPORT Transport Transport to be removed.
-- @return #CHIEF self
function CHIEF:RemoveTransport(Transport)

  Transport.chief=nil
  
  self.commander:RemoveTransport(Transport)

  return self
end

--- Add target.
-- @param #CHIEF self
-- @param Ops.Target#TARGET Target Target object to be added.
-- @return #CHIEF self
function CHIEF:AddTarget(Target)

  if not self:IsTarget(Target) then
    Target.chief=self
    table.insert(self.targetqueue, Target)
  end

  return self
end

--- Check if a TARGET is already in the queue. 
-- @param #CHIEF self
-- @param Ops.Target#TARGET Target Target object to be added.
-- @return #boolean If `true`, target exists in the target queue.
function CHIEF:IsTarget(Target)

  for _,_target in pairs(self.targetqueue) do
    local target=_target --Ops.Target#TARGET
    if target.uid==Target.uid or target:GetName()==Target:GetName() then
      return true
    end
  end

  return false
end

--- Remove target from queue.
-- @param #CHIEF self
-- @param Ops.Target#TARGET Target The target.
-- @return #CHIEF self
function CHIEF:RemoveTarget(Target)

  for i,_target in pairs(self.targetqueue) do
    local target=_target --Ops.Target#TARGET
    
    if target.uid==Target.uid then
      self:T(self.lid..string.format("Removing target %s from queue", Target.name))
      table.remove(self.targetqueue, i)
      break
    end
    
  end

  return self
end

--- Add strategically important zone.
-- By default two resource lists are created. One for the case that the zone is empty and the other for the case that the zone is occupied.
-- 
-- Occupied:
-- 
-- * `AUFTRAG.Type.ARTY` with Nmin=1, Nmax=2
-- * `AUFTRAG.Type.CASENHANCED` with Nmin=1, Nmax=2
-- 
-- Empty:
-- 
-- * `AUFTRAG.Type.ONGURAD` with Nmin=0 and Nmax=1 assets, Attribute=`GROUP.Attribute.GROUND_TANK`.
-- * `AUFTRAG.Type.ONGURAD` with Nmin=0 and Nmax=1 assets, Attribute=`GROUP.Attribute.GROUND_IFV`.
-- * `AUFTRAG.Type.ONGUARD` with Nmin=1 and Nmax=3 assets, Attribute=`GROUP.Attribute.GROUND_INFANTRY`.
-- * `AUFTRAG.Type.OPSTRANSPORT` with Nmin=0 and Nmax=1 assets, Attribute=`GROUP.Attribute.AIR_TRANSPORTHELO` or `GROUP.Attribute.GROUND_APC`. This asset is used to transport the infantry groups.
-- 
-- Resources can be created with the @{#CHIEF.CreateResource} and @{#CHIEF.AddToResource} functions.
-- 
-- @param #CHIEF self
-- @param Ops.OpsZone#OPSZONE OpsZone OPS zone object.
-- @param #number Priority Priority. Default 50.
-- @param #number Importance Importance. Default `#nil`.
-- @param #CHIEF.Resources ResourceOccupied (Optional) Resources used then zone is occupied by the enemy.
-- @param #CHIEF.Resources ResourceEmpty (Optional) Resources used then zone is empty.
-- @return #CHIEF.StrategicZone The strategic zone.
function CHIEF:AddStrategicZone(OpsZone, Priority, Importance, ResourceOccupied, ResourceEmpty)

  local stratzone={} --#CHIEF.StrategicZone
  
  stratzone.opszone=OpsZone
  stratzone.prio=Priority or 50
  stratzone.importance=Importance
  
  stratzone.missions={}

  -- Start ops zone.
  if OpsZone:IsStopped() then
    OpsZone:Start()
  end
  
  -- Add resources if zone is occupied.
  if ResourceOccupied then
    stratzone.resourceOccup=UTILS.DeepCopy(ResourceOccupied)
  else
    stratzone.resourceOccup=self:CreateResource(AUFTRAG.Type.ARTY, 1, 2)
    self:AddToResource(stratzone.resourceOccup, AUFTRAG.Type.CASENHANCED, 1, 2)
  end
  
  -- Add resources if zone is empty
  if ResourceEmpty then
    stratzone.resourceEmpty=UTILS.DeepCopy(ResourceEmpty)
  else
    local resourceEmpty, resourceInfantry=self:CreateResource(AUFTRAG.Type.ONGUARD, 1, 3, GROUP.Attribute.GROUND_INFANTRY)
    self:AddToResource(resourceEmpty, AUFTRAG.Type.ONGUARD, 0, 1, GROUP.Attribute.GROUND_TANK)
    self:AddToResource(resourceEmpty, AUFTRAG.Type.ONGUARD, 0, 1, GROUP.Attribute.GROUND_IFV)
    self:AddTransportToResource(resourceInfantry, 0, 1, {GROUP.Attribute.AIR_TRANSPORTHELO, GROUP.Attribute.GROUND_APC})
    stratzone.resourceEmpty=resourceEmpty
  end

  -- Add to table.
  table.insert(self.zonequeue, stratzone)
  
  -- Add chief so we get informed when something happens.
  OpsZone:_AddChief(self)

  return stratzone
end

--- Set the resource list of missions and assets employed when the zone is empty.
-- @param #CHIEF self
-- @param #CHIEF.StrategicZone StrategicZone The strategic zone.
-- @param #CHIEF.Resource Resource Resource list of missions and assets.
-- @param #boolean NoCopy If `true`, do **not** create a deep copy of the resource.
-- @return #CHIEF self
function CHIEF:SetStrategicZoneResourceEmpty(StrategicZone, Resource, NoCopy)
  if NoCopy then
    StrategicZone.resourceEmpty=Resource
  else
    StrategicZone.resourceEmpty=UTILS.DeepCopy(Resource)
  end
  return self
end

--- Set the resource list of missions and assets employed when the zone is occupied by the enemy.
-- @param #CHIEF self
-- @param #CHIEF.StrategicZone StrategicZone The strategic zone.
-- @param #CHIEF.Resource Resource Resource list of missions and assets.
-- @param #boolean NoCopy If `true`, do **not** create a deep copy of the resource.
-- @return #CHIEF self
function CHIEF:SetStrategicZoneResourceOccupied(StrategicZone, Resource, NoCopy)
  if NoCopy then
    StrategicZone.resourceOccup=Resource
  else
    StrategicZone.resourceOccup=UTILS.DeepCopy(Resource)
  end
  return self
end

--- Get the resource list of missions and assets employed when the zone is empty.
-- @param #CHIEF self
-- @param #CHIEF.StrategicZone StrategicZone The strategic zone.
-- @return #CHIEF.Resource Resource list of missions and assets.
function CHIEF:GetStrategicZoneResourceEmpty(StrategicZone)
  return StrategicZone.resourceEmpty
end

--- Get the resource list of missions and assets employed when the zone is occupied by the enemy.
-- @param #CHIEF self
-- @param #CHIEF.StrategicZone StrategicZone The strategic zone.
-- @return #CHIEF.Resource Resource list of missions and assets.
function CHIEF:GetStrategicZoneResourceOccupied(StrategicZone)
  return StrategicZone.resourceOccup
end


--- Remove strategically important zone. All runing missions are cancelled.
-- @param #CHIEF self
-- @param Ops.OpsZone#OPSZONE OpsZone OPS zone object.
-- @param #number Delay Delay in seconds before the zone is removed. Default immidiately.
-- @return #CHIEF self
function CHIEF:RemoveStrategicZone(OpsZone, Delay)

  if Delay and Delay>0 then
    -- Delayed call.
    self:ScheduleOnce(Delay, CHIEF.RemoveStrategicZone, self, OpsZone)  
  else
  
    -- Loop over all zones in the queue.
    for i=#self.zonequeue,1,-1 do
      local stratzone=self.zonequeue[i] --#CHIEF.StrategicZone
      
      if OpsZone.zoneName==stratzone.opszone.zoneName then
      
        -- Debug info.
        self:T(self.lid..string.format("Removing OPS zone \"%s\" from queue! All running missions will be cancelled", OpsZone.zoneName))

        -- Cancel running missions.
        for _,_resource in pairs(stratzone.resourceEmpty) do
          local resource=_resource --#CHIEF.Resource
          if resource.mission and resource.mission:IsNotOver() then
            resource.mission:Cancel()
          end
        end
        
        -- Cancel running missions.
        for _,_resource in pairs(stratzone.resourceOccup) do
          local resource=_resource --#CHIEF.Resource
          if resource.mission and resource.mission:IsNotOver() then
            resource.mission:Cancel()
          end
        end
                
        -- Remove from table.    
        table.remove(self.zonequeue, i)
        
        -- Done!
        return self
      end
    end
  
  end

  return self
end

--- Add a rearming zone.
-- @param #CHIEF self
-- @param Core.Zone#ZONE RearmingZone Rearming zone.
-- @return Ops.Brigade#BRIGADE.SupplyZone The rearming zone data.
function CHIEF:AddRearmingZone(RearmingZone)

  -- Hand over to commander.
  local supplyzone=self.commander:AddRearmingZone(RearmingZone)

  return supplyzone
end

--- Add a refuelling zone.
-- @param #CHIEF self
-- @param Core.Zone#ZONE RefuellingZone Refuelling zone.
-- @return Ops.Brigade#BRIGADE.SupplyZone The refuelling zone data.
function CHIEF:AddRefuellingZone(RefuellingZone)

  -- Hand over to commander.
  local supplyzone=self.commander:AddRefuellingZone(RefuellingZone)

  return supplyzone
end

--- Add a CAP zone. Flights will engage detected targets inside this zone. 
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone CAP Zone. Has to be a circular zone.
-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
-- @return Ops.Airwing#AIRWING.PatrolZone The CAP zone data.
function CHIEF:AddCapZone(Zone, Altitude, Speed, Heading, Leg)

  -- Hand over to commander.
  local zone=self.commander:AddCapZone(Zone, Altitude, Speed, Heading, Leg)

  return zone
end

--- Add a GCI CAP.
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone Zone, where the flight orbits.
-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
-- @return Ops.Airwing#AIRWING.PatrolZone The CAP zone data.
function CHIEF:AddGciCapZone(Zone, Altitude, Speed, Heading, Leg)

  -- Hand over to commander.
  local zone=self.commander:AddGciCapZone(Zone, Altitude, Speed, Heading, Leg)

  return zone
end

--- Remove a GCI CAP
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone Zone, where the flight orbits.
function CHIEF:RemoveGciCapZone(Zone)

  -- Hand over to commander.
  local zone=self.commander:RemoveGciCapZone(Zone)

  return zone
end

--- Add an AWACS zone.
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone Zone.
-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
-- @return Ops.Airwing#AIRWING.PatrolZone The AWACS zone data.
function CHIEF:AddAwacsZone(Zone, Altitude, Speed, Heading, Leg)

  -- Hand over to commander.
  local zone=self.commander:AddAwacsZone(Zone, Altitude, Speed, Heading, Leg)

  return zone
end

--- Remove a AWACS zone.
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone Zone, where the flight orbits.
function CHIEF:RemoveAwacsZone(Zone)

  -- Hand over to commander.
  local zone=self.commander:RemoveAwacsZone(Zone)

  return zone
end

--- Add a refuelling tanker zone.
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone Zone.
-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
-- @param #number RefuelSystem Refuelling system.
-- @return Ops.Airwing#AIRWING.TankerZone The tanker zone data.
function CHIEF:AddTankerZone(Zone, Altitude, Speed, Heading, Leg, RefuelSystem)

  -- Hand over to commander.
  local zone=self.commander:AddTankerZone(Zone, Altitude, Speed, Heading, Leg, RefuelSystem)

  return zone
end

--- Remove a refuelling tanker zone.
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone Zone, where the flight orbits.
function CHIEF:RemoveTankerZone(Zone)

  -- Hand over to commander.
  local zone=self.commander:RemoveTankerZone(Zone)

  return zone
end

--- Set border zone set, defining your territory.
-- 
-- * Detected enemy troops in these zones will trigger defence condition `RED`.
-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.DEFENSIVE`.
-- 
-- @param #CHIEF self
-- @param Core.Set#SET_ZONE BorderZoneSet Set of zones defining our borders.
-- @return #CHIEF self
function CHIEF:SetBorderZones(BorderZoneSet)

  -- Border zones.
  self.borderzoneset=BorderZoneSet or SET_ZONE:New()
  
  return self
end

--- Add a zone defining your territory.
-- 
-- * Detected enemy troops in these zones will trigger defence condition `RED`.
-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.DEFENSIVE`.
-- 
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone The zone to be added.
-- @return #CHIEF self
function CHIEF:AddBorderZone(Zone)

  -- Add a border zone.
  self.borderzoneset:AddZone(Zone)
  
  return self
end

--- Remove a border zone defining your territory. 
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone The zone to be removed.
-- @return #CHIEF self
function CHIEF:RemoveBorderZone(Zone)

  -- Add a border zone.
  self.borderzoneset:Remove(Zone:GetName())
  
  return self
end

--- Set conflict zone set.
-- 
-- * Detected enemy troops in these zones will trigger defence condition `YELLOW`.
-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.OFFENSIVE`.
-- 
-- @param #CHIEF self
-- @param Core.Set#SET_ZONE ZoneSet Set of zones.
-- @return #CHIEF self
function CHIEF:SetConflictZones(ZoneSet)

  -- Conflict zones.
  self.yellowzoneset=ZoneSet or SET_ZONE:New()
  
  return self
end

--- Add a conflict zone.
-- 
-- * Detected enemy troops in these zones will trigger defence condition `YELLOW`.
-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.OFFENSIVE`.
-- 
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone The zone to be added.
-- @return #CHIEF self
function CHIEF:AddConflictZone(Zone)

  -- Add a conflict zone.
  self.yellowzoneset:AddZone(Zone)
  
  return self
end

--- Remove a conflict zone.
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone The zone to be removed.
-- @return #CHIEF self
function CHIEF:RemoveConflictZone(Zone)

  -- Add a conflict zone.
  self.yellowzoneset:Remove(Zone:GetName())
  
  return self
end


--- Set attack zone set.
-- 
-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.AGGRESSIVE`.
-- 
-- @param #CHIEF self
-- @param Core.Set#SET_ZONE ZoneSet Set of zones.
-- @return #CHIEF self
function CHIEF:SetAttackZones(ZoneSet)

  -- Attacak zones.
  self.engagezoneset=ZoneSet or SET_ZONE:New()
  
  return self
end

--- Add an attack zone.
-- 
-- * Enemies in these zones will only be engaged if strategy is at least `CHIEF.STRATEGY.AGGRESSIVE`.
-- 
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone The zone to add.
-- @return #CHIEF self
function CHIEF:AddAttackZone(Zone)

  -- Add an attack zone.
  self.engagezoneset:AddZone(Zone)
  
  return self
end

--- Remove an attack zone.
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone The zone to be removed.
-- @return #CHIEF self
function CHIEF:RemoveAttackZone(Zone)

  -- Add an attack zone.
  self.engagezoneset:Remove(Zone:GetName())
  
  return self
end

--- Allow chief to use GROUND units for transport tasks. Helicopters are still preferred, and be aware there's no check as of now
-- if a destination can be reached on land.
-- @param #CHIEF self
-- @return #CHIEF self
function CHIEF:AllowGroundTransport()
  env.warning("WARNING: CHIEF:AllowGroundTransport() is deprecated and will be removed in the future!")
  self.TransportCategories = {Group.Category.GROUND, Group.Category.HELICOPTER}
  return self
end

--- Forbid chief to use GROUND units for transport tasks. Restrict to Helicopters. This is the default
-- @param #CHIEF self
-- @return #CHIEF self
function CHIEF:ForbidGroundTransport()
  env.warning("WARNING: CHIEF:ForbidGroundTransport() is deprecated and will be removed in the future!")
  self.TransportCategories = {Group.Category.HELICOPTER}
  return self
end

--- Check if current strategy is passive.
-- @param #CHIEF self
-- @return #boolean If `true`, strategy is passive.
function CHIEF:IsPassive()
  return self.strategy==CHIEF.Strategy.PASSIVE
end

--- Check if current strategy is defensive.
-- @param #CHIEF self
-- @return #boolean If `true`, strategy is defensive.
function CHIEF:IsDefensive()
  return self.strategy==CHIEF.Strategy.DEFENSIVE
end

--- Check if current strategy is offensive.
-- @param #CHIEF self
-- @return #boolean If `true`, strategy is offensive.
function CHIEF:IsOffensive()
  return self.strategy==CHIEF.Strategy.OFFENSIVE
end

--- Check if current strategy is aggressive.
-- @param #CHIEF self
-- @return #boolean If `true`, strategy is agressive.
function CHIEF:IsAgressive()
  return self.strategy==CHIEF.Strategy.AGGRESSIVE
end

--- Check if current strategy is total war.
-- @param #CHIEF self
-- @return #boolean If `true`, strategy is total war.
function CHIEF:IsTotalWar()
  return self.strategy==CHIEF.Strategy.TOTALWAR
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start & Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after Start event.
-- @param #CHIEF self
-- @param Wrapper.Group#GROUP Group Flight group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function CHIEF:onafterStart(From, Event, To)

  -- Short info.
  local text=string.format("Starting Chief of Staff")
  self:I(self.lid..text)

  -- Start parent INTEL.
  self:GetParent(self).onafterStart(self, From, Event, To)
  
  -- Start commander.
  if self.commander then
    if self.commander:GetState()=="NotReadyYet" then
      self.commander:Start()
    end
  end

end

--- On after "Status" event.
-- @param #CHIEF self
-- @param Wrapper.Group#GROUP Group Flight group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function CHIEF:onafterStatus(From, Event, To)

  -- Start parent INTEL.
  self:GetParent(self).onafterStatus(self, From, Event, To)

  -- FSM state.
  local fsmstate=self:GetState()

  ---
  -- CONTACTS: Mission Cleanup
  ---

  -- Clean up missions where the contact was lost.
  for _,_contact in pairs(self.ContactsLost) do
    local contact=_contact --Ops.Intel#INTEL.Contact
    
    if contact.mission and contact.mission:IsNotOver() then
    
      -- Debug info.
      local text=string.format("Lost contact to target %s! %s mission %s will be cancelled.", contact.groupname, contact.mission.type:upper(), contact.mission.name)
      MESSAGE:New(text, 120, "CHIEF"):ToAll()
      self:T(self.lid..text)
    
      -- Cancel this mission.
      contact.mission:Cancel()
          
    end
    
    -- Remove a target from the queue.
    if contact.target then
      self:RemoveTarget(contact.target)
    end    
    
  end

  ---
  -- CONTACTS: Create new TARGETS
  ---
  
  -- Create TARGETs for all new contacts.
  self.Nborder=0 ; self.Nconflict=0 ; self.Nattack=0
  for _,_contact in pairs(self.Contacts) do
    local contact=_contact    --Ops.Intel#INTEL.Contact
    local group=contact.group --Wrapper.Group#GROUP
    
    -- Check if contact inside of our borders.
    local inred=self:CheckGroupInBorder(group)
    if inred then
      self.Nborder=self.Nborder+1
    end
    
    -- Check if contact is in the conflict zones.
    local inyellow=self:CheckGroupInConflict(group)
    if inyellow then
      self.Nconflict=self.Nconflict+1
    end
    
    -- Check if contact is in the attack zones.
    local inattack=self:CheckGroupInAttack(group)
    if inattack then
      self.Nattack=self.Nattack+1
    end
    

    -- Check if this is not already a target.
    if not contact.target then

      -- Create a new TARGET of the contact group.
      local Target=TARGET:New(contact.group)
      
      -- Set to contact.
      contact.target=Target
      
      -- Set contact to target. Might be handy.
      Target.contact=contact
      
      -- Add target to queue.
      self:AddTarget(Target)

    end

  end
  
  
  
  ---
  -- Defcon
  ---
  
  -- TODO: Need to introduce time check to avoid fast oscillation between different defcon states in case groups move in and out of the zones.
  if self.Nborder>0 then
    self:SetDefcon(CHIEF.DEFCON.RED)
  elseif self.Nconflict>0 then
    self:SetDefcon(CHIEF.DEFCON.YELLOW)
  else
    self:SetDefcon(CHIEF.DEFCON.GREEN)
  end
  
  ---
  -- Check Target Queue
  ---
    
  -- Check target queue and assign missions to new targets.
  self:CheckTargetQueue()
  
  -- Loop over targets.
  for _,_target in pairs(self.targetqueue) do
    local target=_target --Ops.Target#TARGET
    
    if target and target:IsAlive() and target.chief and target.mission and target.mission:IsNotOver() then
    
      local inborder=self:CheckTargetInZones(target, self.borderzoneset)
      
      local inyellow=self:CheckTargetInZones(target, self.yellowzoneset)
      
      local inattack=self:CheckTargetInZones(target, self.engagezoneset)
      
      if self.strategy==CHIEF.Strategy.PASSIVE then
      
        -- Passive: No targets are engaged at all.
        self:T(self.lid..string.format("Cancelling mission for target %s as strategy is PASSIVE", target:GetName()))
        target.mission:Cancel()
        
      elseif self.strategy==CHIEF.Strategy.DEFENSIVE then
      
        -- Defensive: Cancel if not in border.
        if not inborder then
          self:T(self.lid..string.format("Cancelling mission for target %s as strategy is DEFENSIVE and not inside border", target:GetName()))
          target.mission:Cancel()
        end
      
      elseif self.strategy==CHIEF.Strategy.OFFENSIVE then

        -- Offensive: Cancel if not in border or conflict.
        if not (inborder or inyellow) then
          self:T(self.lid..string.format("Cancelling mission for target %s as strategy is OFFENSIVE and not inside border or conflict", target:GetName()))
          target.mission:Cancel()
        end
      
      elseif self.strategy==CHIEF.Strategy.AGGRESSIVE then

        -- Aggessive: Cancel if not in border, conflict or attack.
        if not (inborder or inyellow or inattack) then
          self:T(self.lid..string.format("Cancelling mission for target %s as strategy is AGGRESSIVE and not inside border, conflict or attack", target:GetName()))
          target.mission:Cancel()
        end
      
      elseif self.strategy==CHIEF.Strategy.TOTALWAR then
        
        -- Total War: No missions are cancelled.
        
      end
    
    end
    
  end  
  
  ---
  -- Check Strategic Zone Queue
  ---
    
  -- Check target queue and assign missions to new targets.
  self:CheckOpsZoneQueue()
  
  
  -- Display tactival overview.
  self:_TacticalOverview()
  
  ---
  -- Info General
  ---  
  
  if self.verbose>=1 then
    local Nassets=self.commander:CountAssets()
    local Ncontacts=#self.Contacts
    local Nmissions=#self.commander.missionqueue
    local Ntargets=#self.targetqueue
    
    -- Info message
    local text=string.format("Defcon=%s Strategy=%s: Assets=%d, Contacts=%d [Border=%d, Conflict=%d, Attack=%d], Targets=%d, Missions=%d", 
    self.Defcon, self.strategy, Nassets, Ncontacts, self.Nborder, self.Nconflict, self.Nattack, Ntargets, Nmissions)
    self:I(self.lid..text)
    
  end
  
  ---
  -- Info Contacts
  ---
  
  -- Info about contacts.
  if self.verbose>=2 and #self.Contacts>0 then
    local text="Contacts:"
    for i,_contact in pairs(self.Contacts) do
      local contact=_contact --Ops.Intel#INTEL.Contact
      
      local mtext="N/A"
      if contact.mission then
        mtext=string.format("\"%s\" [%s] %s", contact.mission:GetName(), contact.mission:GetType(), contact.mission.status:upper())
      end
      text=text..string.format("\n[%d] %s Type=%s (%s): Threat=%d Mission=%s", i, contact.groupname, contact.categoryname, contact.typename, contact.threatlevel, mtext)
    end
    self:I(self.lid..text)
  end

  ---
  -- Info Targets
  ---

  if self.verbose>=3 and #self.targetqueue>0 then
    local text="Targets:"
    for i,_target in pairs(self.targetqueue) do
      local target=_target --Ops.Target#TARGET

      local mtext="N/A"
      if target.mission then
        mtext=string.format("\"%s\" [%s] %s", target.mission:GetName(), target.mission:GetType(), target.mission.status:upper())
      end      
      text=text..string.format("\n[%d] %s: Category=%s, prio=%d, importance=%d, alive=%s [%.1f/%.1f], Mission=%s",
      i, target:GetName(), target.category, target.prio, target.importance or -1, tostring(target:IsAlive()), target:GetLife(), target:GetLife0(), mtext)          
    end
    self:I(self.lid..text)
  end

  ---
  -- Info Missions
  ---
  
  -- Mission queue.
  if self.verbose>=4 and #self.commander.missionqueue>0 then
    local text="Mission queue:"
    for i,_mission in pairs(self.commander.missionqueue) do
      local mission=_mission --Ops.Auftrag#AUFTRAG
      
      local target=mission:GetTargetName() or "unknown"
      
      text=text..string.format("\n[%d] %s (%s): status=%s, target=%s", i, mission.name, mission.type, mission.status, target)
    end
    self:I(self.lid..text)
  end
  
  ---
  -- Info Strategic Zones
  ---

  -- Loop over targets.
  if self.verbose>=4 and #self.zonequeue>0 then
    local text="Zone queue:"  
    for i,_stratzone in pairs(self.zonequeue) do
      local stratzone=_stratzone --#CHIEF.StrategicZone
      
      -- OPS zone object.
      local opszone=stratzone.opszone
      
      local owner=UTILS.GetCoalitionName(opszone.ownerCurrent)
      local prevowner=UTILS.GetCoalitionName(opszone.ownerPrevious)
      
      text=text..string.format("\n[%d] %s [%s]: owner=%s [%s] (prio=%d, importance=%s): Blue=%d, Red=%d, Neutral=%d", 
      i, opszone.zone:GetName(), opszone:GetState(), owner, prevowner, stratzone.prio, tostring(stratzone.importance), opszone.Nblu, opszone.Nred, opszone.Nnut)
            
    end
    self:I(self.lid..text)
  end
  

  ---
  -- Info Assets
  ---

  if self.verbose>=5 then
    local text="Assets:"
    for _,missiontype in pairs(AUFTRAG.Type) do
      local N=self.commander:CountAssets(nil, missiontype)
      if N>0 then
        text=text..string.format("\n- %s: %d", missiontype, N)
      end
    end
    self:I(self.lid..text)
    
    local text="Assets:"
    for _,attribute in pairs(WAREHOUSE.Attribute) do
      local N=self.commander:CountAssets(nil, nil, attribute)
      if N>0 or self.verbose>=10 then
        text=text..string.format("\n- %s: %d", attribute, N)
      end
    end
    self:T(self.lid..text)
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "MissionAssignToAny" event.
-- @param #CHIEF self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
  -- @param #table Legions The Legion(s) to which the mission is assigned.
function CHIEF:onafterMissionAssign(From, Event, To, Mission, Legions)

  if self.commander then
    self:T(self.lid..string.format("Assigning mission %s (%s) to COMMANDER", Mission.name, Mission.type))
    Mission.chief=self
    Mission.statusChief=AUFTRAG.Status.QUEUED
    self.commander:MissionAssign(Mission, Legions)
  else
    self:E(self.lid..string.format("Mission cannot be assigned as no COMMANDER is defined!"))
  end

end

--- On after "MissionCancel" event.
-- @param #CHIEF self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
function CHIEF:onafterMissionCancel(From, Event, To, Mission)

  -- Debug info.
  self:T(self.lid..string.format("Cancelling mission %s (%s) in status %s", Mission.name, Mission.type, Mission.status))
  
  -- Set status to CANCELLED.
  Mission.statusChief=AUFTRAG.Status.CANCELLED  
  
  if Mission:IsPlanned() then
  
    -- Mission is still in planning stage. Should not have any LEGIONS assigned ==> Just remove it form the COMMANDER queue.
    self:RemoveMission(Mission)
    
  else
  
    -- COMMANDER will cancel mission.
    if Mission.commander then
      Mission.commander:MissionCancel(Mission)
    end
    
  end

end

--- On after "TransportCancel" event.
-- @param #CHIEF self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.
function CHIEF:onafterTransportCancel(From, Event, To, Transport)

  -- Debug info.
  self:T(self.lid..string.format("Cancelling transport UID=%d in status %s", Transport.uid, Transport:GetState()))
  
  if Transport:IsPlanned() then
  
    -- Mission is still in planning stage. Should not have any LEGIONS assigned ==> Just remove it form the COMMANDER queue.
    self:RemoveTransport(Transport)
    
  else
  
    -- COMMANDER will cancel mission.
    if Transport.commander then
      Transport.commander:TransportCancel(Transport)
    end
    
  end

end

--- On after "DefconChange" event.
-- @param #CHIEF self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string Defcon New defence condition.
function CHIEF:onafterDefconChange(From, Event, To, Defcon)
  self:T(self.lid..string.format("Changing Defcon from %s --> %s", self.Defcon, Defcon))
end

--- On after "StrategyChange" event.
-- @param #CHIEF self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string Strategy
function CHIEF:onafterStrategyChange(From, Event, To, Strategy)
  self:T(self.lid..string.format("Changing Strategy from %s --> %s", self.strategy, Strategy))
end

--- On after "OpsOnMission".
-- @param #CHIEF self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP OpsGroup Ops group on mission
-- @param Ops.Auftrag#AUFTRAG Mission The requested mission.
function CHIEF:onafterOpsOnMission(From, Event, To, OpsGroup, Mission)
  -- Debug info.
  self:T(self.lid..string.format("Group %s on mission %s [%s]", OpsGroup:GetName(), Mission:GetName(), Mission:GetType()))
end


--- On after "ZoneCaptured".
-- @param #CHIEF self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsZone#OPSZONE OpsZone The zone that was captured by us.
function CHIEF:onafterZoneCaptured(From, Event, To, OpsZone)
  -- Debug info.
  self:T(self.lid..string.format("Zone %s captured!", OpsZone:GetName()))
end


--- On after "ZoneLost".
-- @param #CHIEF self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsZone#OPSZONE OpsZone The zone that was lost.
function CHIEF:onafterZoneLost(From, Event, To, OpsZone)
  -- Debug info.
  self:T(self.lid..string.format("Zone %s lost!", OpsZone:GetName()))
end

--- On after "ZoneEmpty".
-- @param #CHIEF self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsZone#OPSZONE OpsZone The zone that is empty now.
function CHIEF:onafterZoneEmpty(From, Event, To, OpsZone)
  -- Debug info.
  self:T(self.lid..string.format("Zone %s empty!", OpsZone:GetName()))
end

--- On after "ZoneAttacked".
-- @param #CHIEF self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsZone#OPSZONE OpsZone The zone that being attacked.
function CHIEF:onafterZoneAttacked(From, Event, To, OpsZone)
  -- Debug info.
  self:T(self.lid..string.format("Zone %s attacked!", OpsZone:GetName()))
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Target Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Display tactical overview.
-- @param #CHIEF self 
function CHIEF:_TacticalOverview()

  if self.tacview then

    local NassetsTotal=self.commander:CountAssets()
    local NassetsStock=self.commander:CountAssets(true)
    local Ncontacts=#self.Contacts
    local NmissionsTotal=#self.commander.missionqueue
    local NmissionsRunni=self.commander:CountMissions(AUFTRAG.Type, true)
    local Ntargets=#self.targetqueue
    local Nzones=#self.zonequeue
    local Nagents=self.detectionset:CountAlive()
    
    -- Info message
    local text=string.format("Tactical Overview\n")
    text=text..string.format("=================\n")
    
    -- Strategy and defcon info.
    text=text..string.format("Strategy: %s - Defcon: %s - Agents=%s\n", self.strategy, self.Defcon, Nagents)
    
    -- Contact info.
    text=text..string.format("Contacts: %d [Border=%d, Conflict=%d, Attack=%d]\n", Ncontacts, self.Nborder, self.Nconflict, self.Nattack)
    
    -- Asset info.
    text=text..string.format("Assets: %d [Active=%d, Stock=%d]\n", NassetsTotal, NassetsTotal-NassetsStock, NassetsStock)

    -- Target info.    
    text=text..string.format("Targets: %d\n", Ntargets)
    
    -- Mission info.
    text=text..string.format("Missions: %d [Running=%d/%d - Success=%d, Failure=%d]\n", NmissionsTotal, NmissionsRunni, self:GetMissionLimit("Total"), self.Nsuccess, self.Nfailure)
    for _,mtype in pairs(AUFTRAG.Type) do
      local n=self.commander:CountMissions(mtype)
      if n>0 then
        local N=self.commander:CountMissions(mtype, true)
        local limit=self:GetMissionLimit(mtype)
        text=text..string.format("  - %s: %d [Running=%d/%d]\n", mtype, n, N, limit)
      end
    end
    
    -- Strategic zone info.
    text=text..string.format("Strategic Zones: %d\n", Nzones)
    for _,_stratzone in pairs(self.zonequeue) do
      local stratzone=_stratzone --#CHIEF.StrategicZone
      local owner=stratzone.opszone:GetOwnerName()
      text=text..string.format("  - %s: %s - %s [I=%d, P=%d]\n", stratzone.opszone:GetName(), owner, stratzone.opszone:GetState(), stratzone.importance or 0, stratzone.prio or 0)
    end
    
    local Ntransports=#self.commander.transportqueue
    if Ntransports>0 then
      text=text..string.format("Transports: %d\n", Ntransports)
      for _,_transport in pairs(self.commander.transportqueue) do
        local transport=_transport --Ops.OpsTransport#OPSTRANSPORT
        text=text..string.format(" - %s", transport:GetState())
      end
    end
    
    -- Message to coalition.
    MESSAGE:New(text, 60, nil, true):ToCoalition(self.coalition)
    
    -- Output to log.
    if self.verbose>=4 then
      self:I(self.lid..text)
    end
    
  end

end


--- Check target queue and assign ONE valid target by adding it to the mission queue of the COMMANDER.
-- @param #CHIEF self 
function CHIEF:CheckTargetQueue()

  -- Number of missions.
  local Ntargets=#self.targetqueue

  -- Treat special cases.
  if Ntargets==0 then
    return nil
  end
  
  -- Check if total number of missions is reached.
  local NoLimit=self:_CheckMissionLimit("Total")
  --env.info("FF chief total nolimit="..tostring(NoLimit))
  if NoLimit==false then
    return nil
  end  

  -- Sort results table wrt prio and threatlevel.
  local function _sort(a, b)
    local taskA=a --Ops.Target#TARGET
    local taskB=b --Ops.Target#TARGET
    return (taskA.prio<taskB.prio) or (taskA.prio==taskB.prio and taskA.threatlevel0>taskB.threatlevel0)
  end
  table.sort(self.targetqueue, _sort)

  -- Get the lowest importance value (lower means more important).
  -- If a target with importance 1 exists, targets with importance 2 will not be assigned. Targets with no importance (nil) can still be selected. 
  local vip=math.huge
  for _,_target in pairs(self.targetqueue) do
    local target=_target --Ops.Target#TARGET
    if target:IsAlive() and target.importance and target.importance<vip then
      vip=target.importance
    end
  end

  -- Loop over targets.
  for _,_target in pairs(self.targetqueue) do
    local target=_target --Ops.Target#TARGET
    
    -- Is target still alive.
    local isAlive=target:IsAlive()
    
    -- Is this target important enough.
    local isImportant=(target.importance==nil or target.importance<=vip)
    
    -- Get threat level of target.
    local threatlevel=target:GetThreatLevelMax()
    
    -- Is this a threat?
    local isThreat=threatlevel>=self.threatLevelMin and threatlevel<=self.threatLevelMax
    
    -- Airbases, Zones and Coordinates have threat level 0. We consider them threads independent of min/max threat level set.
    if target.category==TARGET.Category.AIRBASE or target.category==TARGET.Category.ZONE or target.Category==TARGET.Category.COORDINATE then
      isThreat=true
    end
    
    -- Debug message.
    local text=string.format("Target %s: Alive=%s, Threat=%s, Important=%s", target:GetName(), tostring(isAlive), tostring(isThreat), tostring(isImportant))
        
    -- Check if mission is done.
    if target.mission then
      text=text..string.format(", Mission \"%s\" (%s) [%s]", target.mission:GetName(), target.mission:GetState(), target.mission:GetType())
      if target.mission:IsOver() then
        text=text..string.format(" - DONE ==> removing mission")
        target.mission=nil
      end
    else
      text=text..string.format(", NO mission yet")
    end
    self:T2(self.lid..text)

    -- Check that target is alive and not already a mission has been assigned.
    if isAlive and isThreat and isImportant and not target.mission then
    
      -- Check if this target is "valid", i.e. fits with the current strategy.
      local valid=false
      if self.strategy==CHIEF.Strategy.PASSIVE then

        ---
        -- PASSIVE: No targets at all are attacked.
        ---

        valid=false
      
      elseif self.strategy==CHIEF.Strategy.DEFENSIVE then

        ---
        -- DEFENSIVE: Attack inside borders only.
        ---
      
        if self:CheckTargetInZones(target, self.borderzoneset) then
          valid=true
        end
      
      elseif self.strategy==CHIEF.Strategy.OFFENSIVE then

        ---
        -- OFFENSIVE: Attack inside borders and in yellow zones.
        ---
      
        if self:CheckTargetInZones(target, self.borderzoneset) or self:CheckTargetInZones(target, self.yellowzoneset) then
          valid=true
        end
      
      elseif self.strategy==CHIEF.Strategy.AGGRESSIVE then
      
        ---
        -- AGGRESSIVE: Attack in all zone sets.
        ---

        if self:CheckTargetInZones(target, self.borderzoneset) or self:CheckTargetInZones(target, self.yellowzoneset) or self:CheckTargetInZones(target, self.engagezoneset) then
          valid=true
        end
      
      elseif self.strategy==CHIEF.Strategy.TOTALWAR then
      
        ---
        -- TOTAL WAR: We attack anything we find.
        ---
      
        valid=true
      end 
      
      -- Valid target?
      if valid then
      
        -- Debug info.
        self:T(self.lid..string.format("Got valid target %s: category=%s, threatlevel=%d", target:GetName(), target.category, threatlevel))
              
        -- Get mission performances for the given target.  
        local MissionPerformances=self:_GetMissionPerformanceFromTarget(target)
        
        -- Mission.
        local mission=nil --Ops.Auftrag#AUFTRAG
        local Legions=nil
        
        if #MissionPerformances>0 then
          
          for _,_mp in pairs(MissionPerformances) do
            local mp=_mp --#CHIEF.MissionPerformance
            
            -- Check mission type limit.
            local notlimited=self:_CheckMissionLimit(mp.MissionType)
            
            --env.info(string.format("FF chief %s nolimit=%s", mp.MissionType, tostring(NoLimit)))
            
            if notlimited then
            
              -- Get min/max number of assets.          
              local NassetsMin, NassetsMax=self:_GetAssetsForTarget(target, mp.MissionType)            

              -- Debug info.
              self:T2(self.lid..string.format("Recruiting assets for mission type %s [performance=%d] of target %s", mp.MissionType, mp.Performance, target:GetName()))
              
              -- Recruit assets.
              local recruited, assets, legions=self.commander:RecruitAssetsForTarget(target, mp.MissionType, NassetsMin, NassetsMax)
              
              if recruited then
              
                self:T(self.lid..string.format("Recruited %d assets for mission type %s [performance=%d] of target %s", #assets, mp.MissionType, mp.Performance, target:GetName()))
              
                -- Create a mission.
                mission=AUFTRAG:NewFromTarget(target, mp.MissionType)
                              
                -- Add asset to mission.
                if mission then
                  
                  mission:_AddAssets(assets)
                  Legions=legions
                  
                  -- We got what we wanted ==> leave loop.
                  break
                end
              else
                self:T(self.lid..string.format("Could NOT recruit assets for mission type %s [performance=%d] of target %s", mp.MissionType, mp.Performance, target:GetName()))
              end
            end
          end
        end
        
        -- Check if mission could be defined.
        if mission and Legions then
        
          -- Set target mission entry.
          target.mission=mission
          
          -- Mission parameters.
          mission.prio=target.prio
          mission.importance=target.importance

          -- Assign mission to legions.                    
          self:MissionAssign(mission, Legions)
          
          -- Only ONE target is assigned per check.
          return
        end
                
      end
          
    end
  end
  
end

--- Check if limit of missions has been reached.
-- @param #CHIEF self 
-- @param #string MissionType Type of mission.
-- @return #boolean If `true`, mission limit has **not** been reached. If `false`, limit has been reached.
function CHIEF:_CheckMissionLimit(MissionType)
  return self.commander:_CheckMissionLimit(MissionType)
end

--- Get mission limit.
-- @param #CHIEF self 
-- @param #string MissionType Type of mission.
-- @return #number Limit. Unlimited mission types are returned as 999.
function CHIEF:GetMissionLimit(MissionType)
  local l=self.commander.limitMission[MissionType]
  if not l then
    l=999
  end
  return l
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Strategic Zone Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check strategic zone queue.
-- @param #CHIEF self 
function CHIEF:CheckOpsZoneQueue()

  -- Number of zones.
  local Nzones=#self.zonequeue

  -- Treat special cases.
  if Nzones==0 then
    return nil
  end

  -- Loop over strategic zone and remove stopped zones.
  for i=Nzones, 1, -1 do
    local stratzone=self.zonequeue[i] --#CHIEF.StrategicZone
    if stratzone.opszone:IsStopped() then
      self:RemoveStrategicZone(stratzone.opszone)
    end
  end
  
  -- Loop over strategic zones and cancel missions for occupied zones if zone is not occupied any more.
  for _,_startzone in pairs(self.zonequeue) do
    local stratzone=_startzone --#CHIEF.StrategicZone
    
    -- Current owner of the zone.
    local ownercoalition=stratzone.opszone:GetOwner()

    -- Check if we own the zone or it is empty.
    if ownercoalition==self.coalition or stratzone.opszone:IsEmpty() then

      -- Loop over resources.
      for _,_resource in pairs(stratzone.resourceOccup or {}) do
        local resource=_resource --#CHIEF.Resource
        
        -- Cancel running missions.
        if resource.mission then
          resource.mission:Cancel()
        end
        
      end
    end
  end  

  -- Passive strategy ==> Do not act.
  if self:IsPassive() then
    return
  end
  
  -- Check if total number of missions is reached.
  local NoLimit=self:_CheckMissionLimit("Total")
  if NoLimit==false then
    return nil
  end  

  -- Sort results table wrt prio.
  local function _sort(a, b)
    local taskA=a --#CHIEF.StrategicZone
    local taskB=b --#CHIEF.StrategicZone
    return (taskA.prio<taskB.prio)
  end
  table.sort(self.zonequeue, _sort)

  -- Get the lowest importance value (lower means more important).
  -- If a zone with importance 1 exists that is not owned by us, zones with importance 2 will not be assigned. Zone with no importance (nil) can still be selected. 
  local vip=math.huge
  for _,_stratzone in pairs(self.zonequeue) do
    local stratzone=_stratzone --#CHIEF.StrategicZone
    if stratzone.importance and stratzone.importance<vip and stratzone.opszone:GetOwner()~=self.coalition then
      -- Most important zone that is NOT owned by us.
      vip=stratzone.importance
    end
  end

  -- Loop over strategic zones.
  for _,_startzone in pairs(self.zonequeue) do
    local stratzone=_startzone --#CHIEF.StrategicZone
    
    -- Current owner of the zone.
    local ownercoalition=stratzone.opszone:GetOwner()
    
    -- Name of the zone.
    local zoneName=stratzone.opszone.zone:GetName()
        
    -- Check coalition and importance.
    if (ownercoalition~=self.coalition or (ownercoalition==self.coalition and stratzone.opszone:IsEmpty())) 
      and (stratzone.importance==nil or stratzone.importance<=vip) 
      and (not stratzone.opszone:IsStopped()) then

      -- Debug info.
      self:T(self.lid..string.format("Zone %s [%s] is owned by coalition %d", zoneName, stratzone.opszone:GetState(), ownercoalition))
    
      if stratzone.opszone:IsEmpty() then
      
        ---
        -- Zone is EMPTY
        -- 
        -- We send ground troops to capture the zone.
        ---

        for _,_resource in pairs(stratzone.resourceEmpty or {}) do
          local resource=_resource --#CHIEF.Resource
          
          -- Mission type.
          local missionType=resource.MissionType

          if (not resource.mission) or resource.mission:IsOver() then

            -- Debug info.
            self:T2(self.lid..string.format("Zone \"%s\" is empty ==> Recruiting for mission type %s: Nmin=%d, Nmax=%d", zoneName, missionType, resource.Nmin, resource.Nmax))
          
            -- Recruit assets.
            local recruited=self:RecruitAssetsForZone(stratzone, resource)
            
            if recruited then
              self:T(self.lid..string.format("Successfully recruited assets for empty zone \"%s\" [mission type=%s]", zoneName, missionType))
            else
              self:T(self.lid..string.format("Could not recruited assets for empty zone \"%s\" [mission type=%s]", zoneName, missionType))
            end
            
          end
        
        end
        
      else

        ---
        -- Zone is NOT EMPTY
        -- 
        -- We first send a CAS flight to eliminate enemy activity.
        ---

        for _,_resource in pairs(stratzone.resourceOccup or {}) do
          local resource=_resource --#CHIEF.Resource
          
          -- Mission type.
          local missionType=resource.MissionType
          
          if (not resource.mission) or resource.mission:IsOver() then

            -- Debug info.
            self:T2(self.lid..string.format("Zone %s is NOT empty ==> Recruiting for mission type %s: Nmin=%d, Nmax=%d", zoneName, missionType, resource.Nmin, resource.Nmax))
          
            -- Recruit assets.
            local recruited=self:RecruitAssetsForZone(stratzone, resource)
            
            if recruited then
              self:T(self.lid..string.format("Successfully recruited assets for occupied zone %s, mission type=%s", zoneName, missionType))
            else
              self:T(self.lid..string.format("Could not recruited assets for occupied zone %s, mission type=%s", zoneName, missionType))
            end            
            
          end
        
        end

      end
          
    end    
  end
    
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Zone Check Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check if group is inside our border.
-- @param #CHIEF self
-- @param Wrapper.Group#GROUP group The group.
-- @return #boolean If true, group is in any border zone.
function CHIEF:CheckGroupInBorder(group)

  local inside=self:CheckGroupInZones(group, self.borderzoneset)

  return inside
end

--- Check if group is in a conflict zone.
-- @param #CHIEF self
-- @param Wrapper.Group#GROUP group The group.
-- @return #boolean If true, group is in any conflict zone.
function CHIEF:CheckGroupInConflict(group)

  -- Check inside yellow but not inside our border.
  local inside=self:CheckGroupInZones(group, self.yellowzoneset) --and not self:CheckGroupInZones(group, self.borderzoneset)

  return inside
end

--- Check if group is in a attack zone.
-- @param #CHIEF self
-- @param Wrapper.Group#GROUP group The group.
-- @return #boolean If true, group is in any attack zone.
function CHIEF:CheckGroupInAttack(group)

  -- Check inside yellow but not inside our border.
  local inside=self:CheckGroupInZones(group, self.engagezoneset) --and not self:CheckGroupInZones(group, self.borderzoneset)

  return inside
end

--- Check if group is inside a zone.
-- @param #CHIEF self
-- @param Wrapper.Group#GROUP group The group.
-- @param Core.Set#SET_ZONE zoneset Set of zones.
-- @return #boolean If true, group is in any zone.
function CHIEF:CheckGroupInZones(group, zoneset)

  for _,_zone in pairs(zoneset.Set or {}) do
    local zone=_zone --Core.Zone#ZONE
    
    if group:IsInZone(zone) then
      return true
    end
  end

  return false
end

--- Check if group is inside a zone.
-- @param #CHIEF self
-- @param Ops.Target#TARGET target The target.
-- @param Core.Set#SET_ZONE zoneset Set of zones.
-- @return #boolean If true, group is in any zone.
function CHIEF:CheckTargetInZones(target, zoneset)

  for _,_zone in pairs(zoneset.Set or {}) do
    local zone=_zone --Core.Zone#ZONE
    
    if zone:IsCoordinateInZone(target:GetCoordinate()) then
      return true
    end
  end

  return false
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Resources
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a mission performance table.
-- @param #CHIEF self
-- @param #string MissionType Mission type.
-- @param #number Performance Performance.
-- @return #CHIEF.MissionPerformance Mission performance.
function CHIEF:_CreateMissionPerformance(MissionType, Performance)
  local mp={} --#CHIEF.MissionPerformance
  mp.MissionType=MissionType
  mp.Performance=Performance
  return mp
end

--- Get mission performance for a given TARGET.
-- @param #CHIEF self
-- @param Ops.Target#TARGET Target The target.
-- @return #table Mission performances of type `#CHIEF.MissionPerformance`.
function CHIEF:_GetMissionPerformanceFromTarget(Target)

  -- Possible target objects.
  local group=nil      --Wrapper.Group#GROUP
  local airbase=nil    --Wrapper.Airbase#AIRBASE
  local scenery=nil    --Wrapper.Scenery#SCENERY
  local static=nil     --Wrapper.Static#STATIC
  local coordinate=nil --Core.Point#COORDINATE
  
  -- Get target objective.
  local target=Target:GetObject()

  if target:IsInstanceOf("GROUP") then
    group=target --Target is already a group.  
  elseif target:IsInstanceOf("UNIT") then
    group=target:GetGroup()
  elseif target:IsInstanceOf("AIRBASE") then
    airbase=target
  elseif target:IsInstanceOf("STATIC") then
    static=target
  elseif target:IsInstanceOf("SCENERY") then
    scenery=target
  end
 
  -- Target category.
  local TargetCategory=Target:GetCategory()
    
  -- Mission performances.
  local missionperf={} --#CHIEF.MissionPerformance
  
  if group then

    local category=group:GetCategory()
    local attribute=group:GetAttribute()

    if category==Group.Category.AIRPLANE or category==Group.Category.HELICOPTER then
    
      ---
      -- A2A: Intercept
      ---
    
      table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.INTERCEPT, 100))
    
    elseif category==Group.Category.GROUND or category==Group.Category.TRAIN then
    
      ---
      -- GROUND
      ---

      if attribute==GROUP.Attribute.GROUND_SAM then
          
        -- SEAD/DEAD
          
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD, 100))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30))
        
      elseif attribute==GROUP.Attribute.GROUND_EWR then

        -- EWR
        
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI,  100))  
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30))
        
      elseif attribute==GROUP.Attribute.GROUND_AAA then
      
        -- AAA
      
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK, 40))
        
      elseif attribute==GROUP.Attribute.GROUND_ARTILLERY then
      
        -- ARTY
      
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 75))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK, 70))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING, 70))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30))        
      
      elseif attribute==GROUP.Attribute.GROUND_INFANTRY then
      
        -- Infantry
                
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK, 40))
          
      elseif attribute==GROUP.Attribute.GROUND_TANK then
      
        -- Tanks
        
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.CAS, 90))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.CASENHANCED, 90))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARMORATTACK, 40))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30))
        
      else

        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI, 100))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.GROUNDATTACK, 50))
        table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY, 30))
      
      end

    
    elseif category==Group.Category.SHIP then
    
      ---
      -- NAVAL
      ---
    
      table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ANTISHIP, 100))
  
    else
      self:E(self.lid.."ERROR: Unknown Group category!")
    end
    
  elseif airbase then
  
    ---
    -- AIRBASE
    ---
  
    -- Bomb runway.
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBRUNWAY, 100))
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,        30))

  elseif static then
  
    ---
    -- STATIC
    ---

    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI,       100))
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING,    70))
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET, 50))
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,       30))
    
  elseif scenery then
  
    ---
    -- SCENERY
    ---
  
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.STRIKE,    100))
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING,    70))
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET, 50))
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,       30))
    
  elseif coordinate then
  
    ---
    -- COORDINATE
    ---
  
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING,   100))
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET, 50))
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY,       30))
    
  end

  return missionperf
end

--- Get mission performances for a given Group Attribute.
-- @param #CHIEF self
-- @param #string Attribute Group attibute.
-- @return #table Mission performances of type `#CHIEF.MissionPerformance`.
function CHIEF:_GetMissionTypeForGroupAttribute(Attribute)

  local missionperf={} --#CHIEF.MissionPerformance

  if Attribute==GROUP.Attribute.AIR_ATTACKHELO then
  
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.INTERCEPT), 100)
    
  elseif Attribute==GROUP.Attribute.GROUND_AAA then
  
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI),       100)
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBING),    80)
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BOMBCARPET), 70)
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY),       30)

  elseif Attribute==GROUP.Attribute.GROUND_SAM then
  
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD), 100)
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI),   90)
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY),  50)

  elseif Attribute==GROUP.Attribute.GROUND_EWR then
  
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.SEAD), 100)
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.BAI),  100)
    table.insert(missionperf, self:_CreateMissionPerformance(AUFTRAG.Type.ARTY),  50)
    
  end

  return missionperf
end


--- Recruit assets for a given OPS zone.
-- @param #CHIEF self
-- @param #CHIEF.StrategicZone StratZone The strategic zone.
-- @param #CHIEF.Resource Resource The required resources.
-- @return #boolean If `true` enough assets could be recruited.
function CHIEF:RecruitAssetsForZone(StratZone, Resource)

  -- Cohorts.
  local Cohorts=self.commander:_GetCohorts()
  
  -- Shortcuts.
  local MissionType=Resource.MissionType
  local NassetsMin=Resource.Nmin
  local NassetsMax=Resource.Nmax
  local Categories=Resource.Categories
  local Attributes=Resource.Attributes
  local Properties=Resource.Properties

  -- Target position.
  local TargetVec2=StratZone.opszone.zone:GetVec2()
  
  -- Max range in meters.
  local RangeMax=nil
  
  -- Set max range to 250 NM because we use helos as transport for the infantry.
  if MissionType==AUFTRAG.Type.PATROLZONE or MissionType==AUFTRAG.Type.ONGUARD then
    RangeMax=UTILS.NMToMeters(250)
  end
  
  -- Set max range to 50 NM because we use armor.
  if MissionType==AUFTRAG.Type.ARMOREDGUARD then
    RangeMax=UTILS.NMToMeters(50)
  end

  -- Recruite infantry assets.
  self:T(self.lid..string.format("Recruiting assets for zone %s", StratZone.opszone:GetName()))
  self:T(self.lid.."Missiontype="..MissionType)
  self:T({categories=Categories})
  self:T({attributes=Attributes})
  self:T({properties=Properties})
  
  
  local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, MissionType, nil, NassetsMin, NassetsMax, TargetVec2, nil, RangeMax, nil, nil, nil, nil, Categories, Attributes, Properties)
  
  if recruited then
  
    -- Mission for zone.
    local mission=nil  --Ops.Auftrag#AUFTRAG  
  
    -- Debug messgage.
    self:T2(self.lid..string.format("Recruited %d assets for %s mission STRATEGIC zone %s", #assets, MissionType, tostring(StratZone.opszone.zoneName)))
    
    -- Short cuts.
    local TargetZone  = StratZone.opszone.zone
    local TargetCoord = TargetZone:GetCoordinate()

    -- First check if we need a transportation.
    local transport=nil --Ops.OpsTransport#OPSTRANSPORT
    local Ntransports=0
    if Resource.carrierNmin and Resource.carrierNmax and Resource.carrierNmax>0 then
    
      self:T(self.lid..string.format("Recruiting carrier assets: Nmin=%s, Nmax=%s", tostring(Resource.carrierNmin), tostring(Resource.carrierNmax)))
    
      -- Filter only those assets that shall be transported.
      local cargoassets=CHIEF._FilterAssets(assets, Resource.Categories, Resource.Attributes, Resource.Properties)
      
      if #cargoassets>0 then

        -- Recruit transport carrier assets.
        recruited, transport=LEGION.AssignAssetsForTransport(self.commander, self.commander.legions, cargoassets, 
        Resource.carrierNmin, Resource.carrierNmax, TargetZone, nil, Resource.carrierCategories, Resource.carrierAttributes, Resource.carrierProperties)
        
        Ntransports=transport~=nil and #transport.assets or 0
        
        self:T(self.lid..string.format("Recruited %d transport carrier assets success=%s", Ntransports, tostring(recruited)))
        
      end
    
    end

    -- Check if everything was recruited.
    if not recruited then  
      -- No (transport) assets ==> no mission!
      self:T(self.lid..string.format("Could not allocate assets or transport of OPSZONE!"))
      LEGION.UnRecruitAssets(assets)
      return false
    end

    -- Debug message
    self:T2(self.lid..string.format("Recruited %d assets for mission %s", #assets, MissionType))
    
  
    if MissionType==AUFTRAG.Type.PATROLZONE or MissionType==AUFTRAG.Type.ONGUARD then
    
      ---
      -- PATROLZONE or ONGUARD
      ---
             
      if MissionType==AUFTRAG.Type.PATROLZONE then
        mission=AUFTRAG:NewPATROLZONE(TargetZone)
        
      elseif MissionType==AUFTRAG.Type.ONGUARD then 
        mission=AUFTRAG:NewONGUARD(TargetZone:GetRandomCoordinate(nil, nil, {land.SurfaceType.LAND}))
      end
      
      -- Engage detected targets.
      mission:SetEngageDetected(25, {"Ground Units", "Light armed ships", "Helicopters"})            
      
    elseif MissionType==AUFTRAG.Type.CAPTUREZONE then

      ---
      -- CAPTUREZONE
      ---
      
      mission=AUFTRAG:NewCAPTUREZONE(StratZone.opszone, self.coalition)
      
    elseif MissionType==AUFTRAG.Type.CASENHANCED then
    
      ---
      -- CAS ENHANCED
      ---

      -- Create Patrol zone mission.
      local height = UTILS.MetersToFeet(TargetCoord:GetLandHeight())+2500
      
      local Speed=200
      if assets[1] then
        if assets[1].speedmax then
          Speed = UTILS.KmphToKnots(assets[1].speedmax * 0.7) or 200
        end
      end

      -- CAS mission.
      mission=AUFTRAG:NewCASENHANCED(TargetZone, height, Speed)

    elseif MissionType==AUFTRAG.Type.CAS then
    
      ---
      -- CAS
      ---

      -- Create Patrol zone mission.
      local height = UTILS.MetersToFeet(TargetCoord:GetLandHeight())+2500
      
      local Speed = 200      
      if assets[1] then
        if assets[1].speedmax then
          Speed = UTILS.KmphToKnots(assets[1].speedmax * 0.7) or 200
        end
      end
      
      -- Here we need a circular zone.
      TargetZone=StratZone.opszone.zoneCircular
      
      -- Leg length.
      local Leg = TargetZone:GetRadius() <= 10000 and 5 or UTILS.MetersToNM(TargetZone:GetRadius())
      
      -- CAS mission.
      mission=AUFTRAG:NewCAS(TargetZone, height, Speed, TargetCoord, math.random(0,359), Leg)

    elseif MissionType==AUFTRAG.Type.ARTY then
    
      ---
      -- ARTY
      ---
      
      -- Create ARTY zone mission.
      local Radius = TargetZone:GetRadius()
      
      mission=AUFTRAG:NewARTY(TargetCoord, 120, Radius)

    elseif MissionType==AUFTRAG.Type.ARMOREDGUARD then
    
      --- 
      -- ARMORGUARD
      ---
      
      -- Create Armored on guard mission
      mission=AUFTRAG:NewARMOREDGUARD(TargetCoord)
      
    elseif MissionType==AUFTRAG.Type.BOMBCARPET then
    
      ---
      -- BOMB CARPET
      ---
    
      -- Create ARTY zone mission.
      mission=AUFTRAG:NewBOMBCARPET(TargetCoord, nil, 1000)

    elseif MissionType==AUFTRAG.Type.BOMBING then

      ---
      -- BOMBING
      ---
    
      local coord=TargetZone:GetRandomCoordinate()
    
      mission=AUFTRAG:NewBOMBING(TargetCoord)
      
    elseif MissionType==AUFTRAG.Type.RECON then
    
      ---
      -- RECON
      ---    
    
      mission=AUFTRAG:NewRECON(TargetZone, nil, 5000)
      
    elseif MissionType==AUFTRAG.Type.BARRAGE then

      ---
      -- BARRAGE
      ---    
      
      mission=AUFTRAG:NewBARRAGE(TargetZone)
      
    elseif MissionType==AUFTRAG.Type.AMMOSUPPLY then

      ---
      -- AMMO SUPPLY
      ---    
    
      mission=AUFTRAG:NewAMMOSUPPLY(TargetZone)

    end
    
    if mission then

      -- Add assets to mission.
      mission:_AddAssets(assets)
          
      -- Assign mission to legions.
      self:MissionAssign(mission, legions)
    
      -- Attach mission to ops zone.
      StratZone.opszone:_AddMission(self.coalition, MissionType, mission)
      
      mission:SetName(string.format("Stratzone %s-%d", StratZone.opszone:GetName(), mission.auftragsnummer))
      
      -- Attach mission to resource.
      Resource.mission=mission
      
      -- Check if transport assets could be allocated. If carrier Nmin=0 and 0 assets could be allocated, transport would still be created but not usefull obviously
      if transport and Ntransports>0 then
        -- Attach OPS transport to mission.
        mission.opstransport=transport
        -- Set ops zone to transport.
        transport.opszone=StratZone.opszone
        transport.chief=self
        transport.commander=self.commander  
      end      
    
      return true      
    else
      
      -- Mission not supported.
      self:E(self.lid..string.format("ERROR: Mission type %s not supported for OPSZONE! Unrecruiting assets...", tostring(MissionType)))
      LEGION.UnRecruitAssets(assets)
      
      return false  
    end
    
  end

    -- Debug messgage.
    self:T2(self.lid..string.format("Could NOT recruit assets for %s mission of STRATEGIC zone %s", MissionType, tostring(StratZone.opszone.zoneName)))  

  return false
end

--- Filter assets, which have certain categories, attributes and/or properties.
-- @param #table Assets The assets to be filtered.
-- @param #table Categories Group categories.
-- @param #table Attributes Generalized attributes.
-- @param #table Properties DCS attributes
-- @return #table Table of filtered assets.
function CHIEF._FilterAssets(Assets, Categories, Attributes, Properties)

  local filtered={}

  for _,_asset in pairs(Assets) do
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
    
    local hasCat=CHIEF._CheckAssetCategories(asset, Categories)    
    local hasAtt=CHIEF._CheckAssetAttributes(asset, Attributes)
    local hasPro=CHIEF._CheckAssetProperties(asset, Properties)
  
    if hasAtt and hasCat and hasPro then
      table.insert(filtered, asset)
    end
  
  end

  return filtered
end

--- Check if a given asset has certain attribute(s).
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset item.
-- @param #table Attributes The required attributes. See `WAREHOUSE.Attribute` enum. Can also be passed as a single attribute `#string`.
-- @return #boolean Returns `true`, the asset has at least one requested attribute.
function CHIEF._CheckAssetAttributes(Asset, Attributes)

  if not Attributes then
    return true
  end

  for _,attribute in pairs(UTILS.EnsureTable(Attributes)) do
    if attribute==Asset.attribute then
      return true
    end
  end

  return false
end

--- Check if a given asset has certain categories.
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset item.
-- @param #table Categories DCS group categories.
-- @return #boolean Returns `true`, the asset has at least one requested category.
function CHIEF._CheckAssetCategories(Asset, Categories)

  if not Categories then
    return true
  end

  for _,attribute in pairs(UTILS.EnsureTable(Categories)) do
    if attribute==Asset.category then
      return true
    end
  end

  return false
end

--- Check if a given asset has certain properties.
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset item.
-- @param #table Categories DCS group categories.
-- @return #boolean Returns `true`, the asset has at least one requested property.
function CHIEF._CheckAssetProperties(Asset, Properties)

  if not Properties then
    return true
  end

  for _,attribute in pairs(UTILS.EnsureTable(Properties)) do
    if attribute==Asset.DCSdesc then
      return true
    end
  end

  return false
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Cohort encompassed all characteristics of SQUADRONs, PLATOONs and FLOTILLAs.
--
-- **Main Features:**
--
--    * Set parameters like livery, skill valid for all cohort members.
--    * Define modex and callsigns.
--    * Define mission types, this cohort can perform (see Ops.Auftrag#AUFTRAG).
--    * Pause/unpause cohort operations.
--
-- ===
--
-- ### Author: **funkyfranky**
-- 
-- ===
-- @module Ops.Cohort
-- @image OPS_Cohort.png


--- COHORT class.
-- @type COHORT
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field #string name Name of the cohort.
-- @field #string templatename Name of the template group.
-- @field #string aircrafttype Type of the units the cohort is using.
-- @field #number category Group category of the assets: `Group.Category.AIRPLANE`, `Group.Category.HELICOPTER`, `Group.Category.GROUND`, `Group.Category.SHIP`, `Group.Category.TRAIN`.
-- @field Wrapper.Group#GROUP templategroup Template group.
-- @field #boolean isAir
-- @field #boolean isGround Is ground.
-- @field #boolean isNaval Is naval. 
-- @field #table assets Cohort assets.
-- @field #table missiontypes Capabilities (mission types and performances) of the cohort.
-- @field #number maintenancetime Time in seconds needed for maintenance of a returned flight.
-- @field #number repairtime Time in seconds for each
-- @field #string livery Livery of the cohort.
-- @field #number skill Skill of cohort members.
-- @field Ops.Legion#LEGION legion The LEGION object the cohort belongs to.
-- @field #number Ngroups Number of asset OPS groups this cohort has.
-- @field #number Nkilled Number of destroyed asset groups.
-- @field #number engageRange Mission range in meters.
-- @field #string attribute Generalized attribute of the cohort template group.
-- @field #table descriptors DCS descriptors.
-- @field #table properties DCS attributes.
-- @field #table tacanChannel List of TACAN channels available to the cohort.
-- @field #number radioFreq Radio frequency in MHz the cohort uses.
-- @field #number radioModu Radio modulation the cohort uses.
-- @field #table tacanChannel List of TACAN channels available to the cohort.
-- @field #number weightAsset Weight of one assets group in kg.
-- @field #number cargobayLimit Cargo bay capacity in kg.
-- @field #table operations Operations this cohort is part of.
-- @extends Core.Fsm#FSM

--- *I came, I saw, I conquered.* -- Julius Caesar
--
-- ===
--
-- # The COHORT Concept
-- 
-- A COHORT is essential part of a LEGION and consists of **one**  unit type. 
--
--
--
-- @field #COHORT
COHORT = {
  ClassName      = "COHORT",
  verbose        =     0,
  lid            =   nil,
  name           =   nil,
  templatename   =   nil,
  assets         =    {},
  missiontypes   =    {},
  repairtime     =     0,
  maintenancetime=     0,
  livery         =   nil,
  skill          =   nil,
  legion         =   nil,
  --Ngroups        =   nil,
  Ngroups        =     0,
  engageRange    =   nil,
  tacanChannel   =    {},
  weightAsset    = 99999,
  cargobayLimit  =     0,
  descriptors    =    {},
  properties     =    {},
  operations     =    {},
}

--- COHORT class version.
-- @field #string version
COHORT.version="0.3.7"

--- Global variable to store the unique(!) cohort names
_COHORTNAMES={}

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- DONE: Create FLOTILLA class.
-- DONE: Added check for properties.
-- DONE: Make general so that PLATOON and SQUADRON can inherit this class.
-- DONE: Better setting of call signs.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new COHORT object and start the FSM.
-- @param #COHORT self
-- @param #string TemplateGroupName Name of the template group.
-- @param #number Ngroups Number of asset groups of this Cohort. Default 3.
-- @param #string CohortName Name of the cohort.
-- @return #COHORT self
function COHORT:New(TemplateGroupName, Ngroups, CohortName)

  -- Name of the cohort.
  local name=tostring(CohortName or TemplateGroupName)
  
  -- Cohort name has to be unique or we will get serious problems!
  if UTILS.IsAnyInTable(_COHORTNAMES, name) then
    env.error(string.format('ERROR: cannot create cohort "%s" because another cohort with that name already exists. Names must be unique!', name))
    return nil
  else
    table.insert(_COHORTNAMES, name)
  end

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #COHORT

  -- Name of the template group.
  self.templatename=TemplateGroupName

  -- Cohort name.
  self.name=name
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("COHORT %s | ", self.name)
  
  -- Template group.
  self.templategroup=GROUP:FindByName(self.templatename)
  
  -- Check if template group exists.
  if not self.templategroup then
    self:E(self.lid..string.format("ERROR: Template group %s does not exist!", tostring(self.templatename)))
    return nil
  end
    
  -- Generalized attribute.
  self.attribute=self.templategroup:GetAttribute()
  
  -- Group category.
  self.category=self.templategroup:GetCategory()
  
  -- Aircraft type.
  self.aircrafttype=self.templategroup:GetTypeName()
  
  -- Get descriptors.
  self.descriptors=self.templategroup:GetUnit(1):GetDesc()
  
  -- Properties (DCS attributes).
  self.properties=self.descriptors.attributes
  
  -- Print properties.
  --self:I(self.properties)

  -- Defaults.
  self.Ngroups=Ngroups or 3  
  self:SetSkill(AI.Skill.GOOD)
  
  -- Mission range depends on 
  if self.category==Group.Category.AIRPLANE then
    self:SetMissionRange(200)
  elseif self.category==Group.Category.HELICOPTER then
    self:SetMissionRange(150)
  elseif self.category==Group.Category.GROUND then
    self:SetMissionRange(75)
  elseif self.category==Group.Category.SHIP then
    self:SetMissionRange(100)
  elseif self.category==Group.Category.TRAIN then
    self:SetMissionRange(100)
  else
   self:SetMissionRange(150)
  end    
  
  -- Units.
  local units=self.templategroup:GetUnits()
  
  -- Weight of the whole group.
  self.weightAsset=0
  for i,_unit in pairs(units) do
    local unit=_unit --Wrapper.Unit#UNIT
    local desc=unit:GetDesc()
    local mass=666
    if desc then
      mass=desc.massMax or desc.massEmpty
    end
    self.weightAsset=self.weightAsset + (mass or 666)
    if i==1 then
      self.cargobayLimit=unit:GetCargoBayFreeWeight()  
    end
  end
  
  -- Start State.
  self:SetStartState("Stopped")
  
  -- Add FSM transitions.
  --                 From State  -->   Event        -->     To State
  self:AddTransition("Stopped",       "Start",              "OnDuty")      -- Start FSM.
  self:AddTransition("*",             "Status",             "*")           -- Status update.
  
  self:AddTransition("OnDuty",        "Pause",              "Paused")      -- Pause cohort.
  self:AddTransition("Paused",        "Unpause",            "OnDuty")      -- Unpause cohort.
  
  self:AddTransition("OnDuty",        "Relocate",           "Relocating")  -- Relocate.
  self:AddTransition("Relocating",    "Relocated",          "OnDuty")      -- Relocated.
  
  self:AddTransition("*",             "Stop",               "Stopped")     -- Stop cohort.


  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the COHORT.
  -- @function [parent=#COHORT] Start
  -- @param #COHORT self

  --- Triggers the FSM event "Start" after a delay. Starts the COHORT.
  -- @function [parent=#COHORT] __Start
  -- @param #COHORT self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Stop".
  -- @param #COHORT self

  --- Triggers the FSM event "Stop" after a delay. Stops the COHORT and all its event handlers.
  -- @function [parent=#COHORT] __Stop
  -- @param #COHORT self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Status".
  -- @function [parent=#COHORT] Status
  -- @param #COHORT self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#COHORT] __Status
  -- @param #COHORT self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Pause".
  -- @function [parent=#COHORT] Pause
  -- @param #COHORT self

  --- Triggers the FSM event "Pause" after a delay.
  -- @function [parent=#COHORT] __Pause
  -- @param #COHORT self
  -- @param #number delay Delay in seconds.

  --- On after "Pause" event.
  -- @function [parent=#COHORT] OnAfterPause
  -- @param #COHORT self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Unpause".
  -- @function [parent=#COHORT] Unpause
  -- @param #COHORT self

  --- Triggers the FSM event "Unpause" after a delay.
  -- @function [parent=#COHORT] __Unpause
  -- @param #COHORT self
  -- @param #number delay Delay in seconds.

  --- On after "Unpause" event.
  -- @function [parent=#COHORT] OnAfterUnpause
  -- @param #COHORT self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Relocate".
  -- @function [parent=#COHORT] Relocate
  -- @param #COHORT self

  --- Triggers the FSM event "Relocate" after a delay.
  -- @function [parent=#COHORT] __Relocate
  -- @param #COHORT self
  -- @param #number delay Delay in seconds.

  --- On after "Relocate" event.
  -- @function [parent=#COHORT] OnAfterRelocate
  -- @param #COHORT self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Relocated".
  -- @function [parent=#COHORT] Relocated
  -- @param #COHORT self

  --- Triggers the FSM event "Relocated" after a delay.
  -- @function [parent=#COHORT] __Relocated
  -- @param #COHORT self
  -- @param #number delay Delay in seconds.

  --- On after "Relocated" event.
  -- @function [parent=#COHORT] OnAfterRelocated
  -- @param #COHORT self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set livery painted on all cohort units.
-- Note that the livery name in general is different from the name shown in the mission editor.
-- 
-- Valid names are the names of the **livery directories**. Check out the folder in your DCS installation for:
-- 
-- * Full modules: `DCS World OpenBeta\CoreMods\aircraft\<Aircraft Type>\Liveries\<Aircraft Type>\<Livery Name>`
-- * AI units: `DCS World OpenBeta\Bazar\Liveries\<Aircraft Type>\<Livery Name>`
-- 
-- The folder name `<Livery Name>` is the string you want.
-- 
-- Or personal liveries you have installed somewhere in your saved games folder.
--  
-- @param #COHORT self
-- @param #string LiveryName Name of the livery.
-- @return #COHORT self
function COHORT:SetLivery(LiveryName)
  self.livery=LiveryName
  return self
end

--- Set skill level of all cohort team members.
-- @param #COHORT self
-- @param #string Skill Skill of all flights.
-- @usage mycohort:SetSkill(AI.Skill.EXCELLENT)
-- @return #COHORT self
function COHORT:SetSkill(Skill)
  self.skill=Skill
  return self
end

--- Set verbosity level.
-- @param #COHORT self
-- @param #number VerbosityLevel Level of output (higher=more). Default 0.
-- @return #COHORT self
function COHORT:SetVerbosity(VerbosityLevel)
  self.verbose=VerbosityLevel or 0
  return self
end

--- Set turnover and repair time. If an asset returns from a mission, it will need some time until the asset is available for further missions.
-- @param #COHORT self
-- @param #number MaintenanceTime Time in minutes it takes until a flight is combat ready again. Default is 0 min.
-- @param #number RepairTime Time in minutes it takes to repair a flight for each life point taken. Default is 0 min.
-- @return #COHORT self
function COHORT:SetTurnoverTime(MaintenanceTime, RepairTime)
  self.maintenancetime=MaintenanceTime and MaintenanceTime*60 or 0
  self.repairtime=RepairTime and RepairTime*60 or 0
  return self
end

--- Set radio frequency and modulation the cohort uses.
-- @param #COHORT self
-- @param #number Frequency Radio frequency in MHz. Default 251 MHz.
-- @param #number Modulation Radio modulation. Default 0=AM.
-- @return #COHORT self
function COHORT:SetRadio(Frequency, Modulation)
  self.radioFreq=Frequency or 251
  self.radioModu=Modulation or radio.modulation.AM
  return self
end

--- Set number of units in groups.
-- @param #COHORT self
-- @param #number nunits Number of units. Default 2.
-- @return #COHORT self
function COHORT:SetGrouping(nunits)
  self.ngrouping=nunits or 2
  return self
end

--- Set mission types this cohort is able to perform.
-- @param #COHORT self
-- @param #table MissionTypes Table of mission types. Can also be passed as a #string if only one type.
-- @param #number Performance Performance describing how good this mission can be performed. Higher is better. Default 50. Max 100.
-- @return #COHORT self
function COHORT:AddMissionCapability(MissionTypes, Performance)

  -- Ensure Missiontypes is a table.
  if MissionTypes and type(MissionTypes)~="table" then
    MissionTypes={MissionTypes}
  end
  
  -- Set table.
  self.missiontypes=self.missiontypes or {}
  
  for _,missiontype in pairs(MissionTypes) do
  
    local Capability=self:GetMissionCapability(missiontype)
  
    -- Check not to add the same twice.  
    if Capability then
      self:E(self.lid.."WARNING: Mission capability already present! No need to add it twice. Will update the performance though!")
      Capability.Performance=Performance or 50
    else
  
      local capability={} --Ops.Auftrag#AUFTRAG.Capability
      capability.MissionType=missiontype
      capability.Performance=Performance or 50
      table.insert(self.missiontypes, capability)
      self:T(self.lid..string.format("Adding mission capability %s, performance=%d", tostring(capability.MissionType), capability.Performance))
    end
  end
  
  -- Debug info.
  self:T2(self.missiontypes)
  
  return self
end

--- Get missin capability for a given mission type.
-- @param #COHORT self
-- @param #string MissionType Mission type, e.g. `AUFTRAG.Type.BAI`.
-- @return Ops.Auftrag#AUFTRAG.Capability Capability table or `nil` if the capability does not exist.
function COHORT:GetMissionCapability(MissionType)
  
  for _,_capability in pairs(self.missiontypes) do
    local capability=_capability --Ops.Auftrag#AUFTRAG.Capability
    if capability.MissionType==MissionType then
      return capability
    end
  end
  
  return nil
end

--- Check if cohort assets have a given property (DCS attribute).
-- @param #COHORT self
-- @param #string Property The property.
-- @return #boolean If `true`, cohort assets have the attribute.
function COHORT:HasProperty(Property)

  for _,property in pairs(self.properties) do
    if Property==property then
      return true
    end
  end

  return false
end

--- Get mission types this cohort is able to perform.
-- @param #COHORT self
-- @return #table Table of mission types. Could be empty {}.
function COHORT:GetMissionTypes()

  local missiontypes={}
  
  for _,Capability in pairs(self.missiontypes) do
    local capability=Capability --Ops.Auftrag#AUFTRAG.Capability
    table.insert(missiontypes, capability.MissionType)  
  end

  return missiontypes
end

--- Get mission capabilities of this cohort.
-- @param #COHORT self
-- @return #table Table of mission capabilities.
function COHORT:GetMissionCapabilities()
  return self.missiontypes
end

--- Get mission performance for a given type of misson.
-- @param #COHORT self
-- @param #string MissionType Type of mission.
-- @return #number Performance or -1.
function COHORT:GetMissionPeformance(MissionType)

  for _,Capability in pairs(self.missiontypes) do
    local capability=Capability --Ops.Auftrag#AUFTRAG.Capability
    if capability.MissionType==MissionType then
      return capability.Performance
    end
  end

  return -1
end

--- Set max mission range. Only missions in a circle of this radius around the cohort base are executed.
-- @param #COHORT self
-- @param #number Range Range in NM. Default 150 NM.
-- @return #COHORT self
function COHORT:SetMissionRange(Range)
  self.engageRange=UTILS.NMToMeters(Range or 150)
  return self
end

--- Set call sign.
-- @param #COHORT self
-- @param #number Callsign Callsign from CALLSIGN.Aircraft, e.g. "Chevy" for CALLSIGN.Aircraft.CHEVY.
-- @param #number Index Callsign index, Chevy-**1**.
-- @param #string CallsignString (optional) Set this for tasks like TANKER, AWACS or KIOWA and the like, which have special names. E.g. "Darkstar" or "Roughneck".
-- @return #COHORT self
function COHORT:SetCallsign(Callsign, Index, CallsignString)
  self.callsignName=Callsign
  self.callsignIndex=Index
  self.callsignClearName=CallsignString
  self.callsign={}
  self.callsign.NumberSquad=Callsign
  self.callsign.NumberGroup=Index
  return self
end

--- Set generalized attribute.
-- @param #COHORT self
-- @param #string Attribute Generalized attribute, e.g. `GROUP.Attribute.Ground_Infantry`.
-- @return #COHORT self
function COHORT:SetAttribute(Attribute)
  self.attribute=Attribute
  return self
end

--- Get generalized attribute.
-- @param #COHORT self
-- @return #string Generalized attribute, e.g. `GROUP.Attribute.Ground_Infantry`.
function COHORT:GetAttribute()
  return self.attribute
end

--- Get group category.
-- @param #COHORT self
-- @return #string Group category
function COHORT:GetCategory()
  return self.category
end

--- Get properties, *i.e.* DCS attributes.
-- @param #COHORT self
-- @return #table Properties table.
function COHORT:GetProperties()
  return self.properties
end


--- Set modex.
-- @param #COHORT self
-- @param #number Modex A number like 100.
-- @param #string Prefix A prefix string, which is put before the `Modex` number.
-- @param #string Suffix A suffix string, which is put after the `Modex` number. 
-- @return #COHORT self
function COHORT:SetModex(Modex, Prefix, Suffix)
  self.modex=Modex
  self.modexPrefix=Prefix
  self.modexSuffix=Suffix
  return self
end

--- Set Legion.
-- @param #COHORT self
-- @param Ops.Legion#LEGION Legion The Legion.
-- @return #COHORT self
function COHORT:SetLegion(Legion)
  self.legion=Legion
  return self
end

--- Add asset to cohort.
-- @param #COHORT self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The warehouse asset.
-- @return #COHORT self
function COHORT:AddAsset(Asset)
  self:T(self.lid..string.format("Adding asset %s of type %s", Asset.spawngroupname, Asset.unittype))
  Asset.squadname=self.name
  Asset.legion=self.legion
  Asset.cohort=self
  table.insert(self.assets, Asset)
  return self
end

--- Remove specific asset from chort.
-- @param #COHORT self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset.
-- @return #COHORT self
function COHORT:DelAsset(Asset)
  for i,_asset in pairs(self.assets) do
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
    if Asset.uid==asset.uid then
      self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname))
      table.remove(self.assets, i)
      break
    end
  end
  return self
end

--- Remove asset group from cohort.
-- @param #COHORT self
-- @param #string GroupName Name of the asset group.
-- @return #COHORT self
function COHORT:DelGroup(GroupName)
  for i,_asset in pairs(self.assets) do
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
    if GroupName==asset.spawngroupname then
      self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname))
      table.remove(self.assets, i)
      break
    end
  end
  return self
end

--- Remove assets from pool. Not that assets must not be spawned or already reserved or requested.
-- @param #COHORT self
-- @param #number N Number of assets to be removed. Default 1.
-- @return #COHORT self
function COHORT:RemoveAssets(N)
  self:T2(self.lid..string.format("Remove %d assets of Cohort", N))

  N=N or 1
  
  local n=0
  for i=#self.assets,1,-1 do
    local asset=self.assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem
  
    self:T2(self.lid..string.format("Checking removing asset %s", asset.spawngroupname))
    if not (asset.requested or asset.spawned or asset.isReserved) then
      self:T2(self.lid..string.format("Removing asset %s", asset.spawngroupname))
      table.remove(self.assets, i)
      n=n+1
    else
      self:T2(self.lid..string.format("Could NOT Remove asset %s", asset.spawngroupname))
    end
    
    if n>=N then
      break
    end
  
  end
  
  self:T(self.lid..string.format("Removed %d/%d assets. New asset count=%d", n, N, #self.assets))

  return self
end


--- Get name of the cohort.
-- @param #COHORT self
-- @return #string Name of the cohort.
function COHORT:GetName()
  return self.name
end

--- Get radio frequency and modulation.
-- @param #COHORT self
-- @return #number Radio frequency in MHz.
-- @return #number Radio Modulation (0=AM, 1=FM).
function COHORT:GetRadio()
  return self.radioFreq, self.radioModu
end

--- Create a callsign for the asset.
-- @param #COHORT self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The warehouse asset.
-- @return #COHORT self
function COHORT:GetCallsign(Asset)

  if self.callsignName then
    --[[
                    ["callsign"] = 
                    {
                      [2] = 1,
                      ["name"] = "Darkstar11",
                      [3] = 1,
                      [1] = 5,
                      [4] = "Darkstar11",
                    }, -- end of ["callsign"]
    ]]
    Asset.callsign={}
  
    for i=1,Asset.nunits do
    
      local callsign={}
      
      callsign[1]=self.callsignName
      callsign[2]=math.floor(self.callsigncounter / 10)
      callsign[3]=self.callsigncounter % 10
      if callsign[3]==0 then
        callsign[3]=1
        self.callsigncounter=self.callsigncounter+2
      else
        self.callsigncounter=self.callsigncounter+1
      end
      callsign["name"] = self.callsignClearName or UTILS.GetCallsignName(self.callsignName) or "None"
      callsign["name"] = string.format("%s%d%d",callsign["name"],callsign[2],callsign[3])
      callsign[4] = callsign["name"] 
    
      Asset.callsign[i]=callsign
      
      self:T3({callsign=callsign})
    
      --DONE: there is also a table entry .name, which is a string.
      --UTILS.PrintTableToLog(callsign)
    end
  
  
  end

end

--- Create a modex for the asset.
-- @param #COHORT self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The warehouse asset.
-- @return #COHORT self
function COHORT:GetModex(Asset)

  if self.modex then
  
    Asset.modex={}
  
    for i=1,Asset.nunits do
    
      Asset.modex[i]=string.format("%03d", self.modex+self.modexcounter)
      
      self.modexcounter=self.modexcounter+1
      
      self:T3({modex=Asset.modex[i]})
    
    end
    
  end
  
end


--- Add TACAN channels to the cohort. Note that channels can only range from 1 to 126.
-- @param #COHORT self
-- @param #number ChannelMin Channel.
-- @param #number ChannelMax Channel.
-- @return #COHORT self
-- @usage mysquad:AddTacanChannel(64,69)  -- adds channels 64, 65, 66, 67, 68, 69
function COHORT:AddTacanChannel(ChannelMin, ChannelMax)

  ChannelMax=ChannelMax or ChannelMin
  
  if ChannelMin>126 then
    self:E(self.lid.."ERROR: TACAN Channel must be <= 126! Will not add to available channels")
    return self
  end
  if ChannelMax>126 then
    self:E(self.lid.."WARNING: TACAN Channel must be <= 126! Adjusting ChannelMax to 126")
    ChannelMax=126
  end

  for i=ChannelMin,ChannelMax do
    self.tacanChannel[i]=true
  end

  return self
end

--- Get an unused TACAN channel.
-- @param #COHORT self
-- @return #number TACAN channel or *nil* if no channel is free.
function COHORT:FetchTacan()

  -- Get the smallest free channel if there is one.
  local freechannel=nil  
  for channel,free in pairs(self.tacanChannel) do
    if free then      
      if freechannel==nil or channel<freechannel then
        freechannel=channel
      end      
    end
  end
  
  if freechannel then
    self:T(self.lid..string.format("Checking out Tacan channel %d", freechannel))
    self.tacanChannel[freechannel]=false
  end

  return freechannel
end

--- "Return" a used TACAN channel.
-- @param #COHORT self
-- @param #number channel The channel that is available again.
function COHORT:ReturnTacan(channel)
  self:T(self.lid..string.format("Returning Tacan channel %d", channel))
  self.tacanChannel[channel]=true
end

--- Add a weapon range for ARTY missions (@{Ops.Auftrag#AUFTRAG}).
-- @param #COHORT self
-- @param #number RangeMin Minimum range in nautical miles. Default 0 NM.
-- @param #number RangeMax Maximum range in nautical miles. Default 10 NM.
-- @param #number BitType Bit mask of weapon type for which the given min/max ranges apply. Default is `ENUMS.WeaponFlag.Auto`, i.e. for all weapon types.
-- @return #COHORT self
function COHORT:AddWeaponRange(RangeMin, RangeMax, BitType)

  RangeMin=UTILS.NMToMeters(RangeMin or 0)
  RangeMax=UTILS.NMToMeters(RangeMax or 10)

  local weapon={} --Ops.OpsGroup#OPSGROUP.WeaponData

  weapon.BitType=BitType or ENUMS.WeaponFlag.Auto
  weapon.RangeMax=RangeMax
  weapon.RangeMin=RangeMin

  self.weaponData=self.weaponData or {}
  self.weaponData[tostring(weapon.BitType)]=weapon
  
  -- Debug info.
  self:T(self.lid..string.format("Adding weapon data: Bit=%s, Rmin=%d m, Rmax=%d m", tostring(weapon.BitType), weapon.RangeMin, weapon.RangeMax))
  
  if self.verbose>=2 then
    local text="Weapon data:"
    for _,_weapondata in pairs(self.weaponData) do
      local weapondata=_weapondata
      text=text..string.format("\n- Bit=%s, Rmin=%d m, Rmax=%d m", tostring(weapondata.BitType), weapondata.RangeMin, weapondata.RangeMax)
    end
    self:I(self.lid..text)
  end

  return self
end

--- Get weapon range for given bit type.
-- @param #COHORT self
-- @param #number BitType Bit mask of weapon type.
-- @return Ops.OpsGroup#OPSGROUP.WeaponData Weapon data.
function COHORT:GetWeaponData(BitType)
  return self.weaponData[tostring(BitType)]
end

--- Check if cohort is "OnDuty".
-- @param #COHORT self
-- @return #boolean If true, cohort is in state "OnDuty".
function COHORT:IsOnDuty()
  return self:Is("OnDuty")
end

--- Check if cohort is "Stopped".
-- @param #COHORT self
-- @return #boolean If true, cohort is in state "Stopped".
function COHORT:IsStopped()
  return self:Is("Stopped")
end

--- Check if cohort is "Paused".
-- @param #COHORT self
-- @return #boolean If true, cohort is in state "Paused".
function COHORT:IsPaused()
  return self:Is("Paused")
end

--- Check if cohort is "Relocating".
-- @param #COHORT self
-- @return #boolean If true, cohort is relocating.
function COHORT:IsRelocating()
  return self:Is("Relocating")
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start & Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers.
-- @param #COHORT self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function COHORT:onafterStart(From, Event, To)

  -- Short info.
  local text=string.format("Starting %s v%s %s [%s]", self.ClassName, self.version, self.name, self.attribute)
  self:I(self.lid..text)

  -- Start the status monitoring.
  self:__Status(-1)
end

--- Check asset status.
-- @param #COHORT self
function COHORT:_CheckAssetStatus()

  if self.verbose>=2 and #self.assets>0 then
  
    local text=""
    for j,_asset in pairs(self.assets) do
      local asset=_asset  --Functional.Warehouse#WAREHOUSE.Assetitem
  
      -- Text.
      text=text..string.format("\n[%d] %s (%s*%d): ", j, asset.spawngroupname, asset.unittype, asset.nunits)
      
      if asset.spawned then
      
        ---
        -- Spawned
        ---
  
        -- Mission info.
        local mission=self.legion and self.legion:GetAssetCurrentMission(asset) or false
        if mission then
          local distance=asset.flightgroup and UTILS.MetersToNM(mission:GetTargetDistance(asset.flightgroup.group:GetCoordinate())) or 0
          text=text..string.format("Mission %s - %s: Status=%s, Dist=%.1f NM", mission.name, mission.type, mission.status, distance)
        else
          text=text.."Mission None"
        end
          
        -- Flight status.
        text=text..", Flight: "
        if asset.flightgroup and asset.flightgroup:IsAlive() then
          local status=asset.flightgroup:GetState()
          text=text..string.format("%s", status)
          
          if asset.flightgroup:IsFlightgroup() then
            local fuelmin=asset.flightgroup:GetFuelMin()
            local fuellow=asset.flightgroup:IsFuelLow()
            local fuelcri=asset.flightgroup:IsFuelCritical()            
            text=text..string.format("Fuel=%d", fuelmin)
            if fuelcri then
              text=text.." (Critical!)"
            elseif fuellow then
              text=text.." (Low)"
            end
          end
          
          local lifept, lifept0=asset.flightgroup:GetLifePoints()
          text=text..string.format(", Life=%d/%d", lifept, lifept0)
          
          local ammo=asset.flightgroup:GetAmmoTot()
          text=text..string.format(", Ammo=%d [G=%d, R=%d, B=%d, M=%d]", ammo.Total,ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles)
        else
          text=text.."N/A"
        end

        -- Payload info.
        if asset.flightgroup:IsFlightgroup() then
          local payload=asset.payload and table.concat(self.legion:GetPayloadMissionTypes(asset.payload), ", ") or "None"
          text=text..", Payload={"..payload.."}"
        end
     
      else
  
        ---
        -- In Stock
        ---
        
        text=text..string.format("In Stock")
        
        if self:IsRepaired(asset) then
          text=text..", Combat Ready"
        else
        
          text=text..string.format(", Repaired in %d sec", self:GetRepairTime(asset))

          if asset.damage then
            text=text..string.format(" (Damage=%.1f)", asset.damage)
          end
        end
  
        if asset.Treturned then
          local T=timer.getAbsTime()-asset.Treturned
          text=text..string.format(", Returned for %d sec", T)
        end
      
      end
    end
    self:T(self.lid..text)
  end

end

--- On after "Stop" event.
-- @param #COHORT self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function COHORT:onafterStop(From, Event, To)

  -- Debug info.
  self:T(self.lid.."STOPPING Cohort and removing all assets!")

  -- Remove all assets.
  for i=#self.assets,1,-1 do
    local asset=self.assets[i]
    self:DelAsset(asset)
  end

  -- Clear call scheduler.
  self.CallScheduler:Clear()
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check if there is a cohort that can execute a given mission.
-- We check the mission type, the refuelling system, mission range.
-- @param #COHORT self
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
-- @return #boolean If true, Cohort can do that type of mission.
function COHORT:CanMission(Mission)
  
  local cando=true
  
  -- On duty?=  
  if not self:IsOnDuty() then
    self:T(self.lid..string.format("Cohort in not OnDuty but in state %s. Cannot do mission %s with target %s", self:GetState(), Mission.name, Mission:GetTargetName()))
    return false
  end

  -- Check mission type. WARNING: This assumes that all assets of the cohort can do the same mission types!
  if not AUFTRAG.CheckMissionType(Mission.type, self:GetMissionTypes()) then
    self:T(self.lid..string.format("INFO: Cohort cannot do mission type %s (%s, %s)", Mission.type, Mission.name, Mission:GetTargetName()))
    return false
  end
  
  -- Check that tanker mission has the correct refuelling system.
  if Mission.type==AUFTRAG.Type.TANKER then
  
    if Mission.refuelSystem and Mission.refuelSystem==self.tankerSystem then
      -- Correct refueling system.
      self:T(self.lid..string.format("INFO: Correct refueling system requested=%s != %s=available", tostring(Mission.refuelSystem), tostring(self.tankerSystem)))
    else
      self:T(self.lid..string.format("INFO: Wrong refueling system requested=%s != %s=available", tostring(Mission.refuelSystem), tostring(self.tankerSystem)))
      return false
    end
  
  end
  
  -- Distance to target.
  local TargetDistance=Mission:GetTargetDistance(self.legion:GetCoordinate())
  
  -- Max engage range.
  local engagerange=Mission.engageRange and math.max(self.engageRange, Mission.engageRange) or self.engageRange
      
  -- Set range is valid. Mission engage distance can overrule the cohort engage range.
  if TargetDistance>engagerange then
    self:T(self.lid..string.format("INFO: Cohort is not in range. Target dist=%d > %d NM max mission Range", UTILS.MetersToNM(TargetDistance), UTILS.MetersToNM(engagerange)))
    return false
  end
  
  return true
end

--- Count assets in legion warehouse stock.
-- @param #COHORT self
-- @param #boolean InStock If `true`, only assets that are in the warehouse stock/inventory are counted. If `false`, only assets that are NOT in stock (i.e. spawned) are counted. If `nil`, all assets are counted.
-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types.
-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`.
-- @return #number Number of assets.
function COHORT:CountAssets(InStock, MissionTypes, Attributes)

  local N=0
  for _,_asset in pairs(self.assets) do
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
    
    if MissionTypes==nil or AUFTRAG.CheckMissionCapability(MissionTypes, self.missiontypes) then
      if Attributes==nil or self:CheckAttribute(Attributes) then
        if asset.spawned then
          if InStock==false or InStock==nil then
            N=N+1 --Spawned but we also count the spawned ones.
          end      
        else
          if InStock==true or InStock==nil then
            N=N+1 --This is in stock.
          end
        end
      end
    end
  end

  return N
end

--- Get OPSGROUPs.
-- @param #COHORT self
-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types.
-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`.
-- @return Core.Set#SET_OPSGROUP Ops groups set.
function COHORT:GetOpsGroups(MissionTypes, Attributes)

  local set=SET_OPSGROUP:New()

  for _,_asset in pairs(self.assets) do
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
    
    if MissionTypes==nil or AUFTRAG.CheckMissionCapability(MissionTypes, self.missiontypes) then
      if Attributes==nil or self:CheckAttribute(Attributes) then
        if asset.flightgroup and asset.flightgroup:IsAlive() then
          set:AddGroup(asset.flightgroup)
        end
      end
    end
  end

  return set
end

--- Get assets for a mission.
-- @param #COHORT self
-- @param #string MissionType Mission type.
-- @param #number Npayloads Number of payloads available.
-- @return #table Assets that can do the required mission.
-- @return #number Number of payloads still available after recruiting the assets.
function COHORT:RecruitAssets(MissionType, Npayloads)

  -- Debug info.
  self:T2(self.lid..string.format("Recruiting asset for Mission type=%s", MissionType))

  -- Recruited assets.
  local assets={}

  -- Loop over assets.
  for _,_asset in pairs(self.assets) do  
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
    
    -- Get info.
    local isRequested=asset.requested
    local isReserved=asset.isReserved
    local isSpawned=asset.spawned
    local isOnMission=self.legion:IsAssetOnMission(asset)
    
    local opsgroup=asset.flightgroup
    
    -- Debug info.
    self:T(self.lid..string.format("Asset %s: requested=%s, reserved=%s, spawned=%s,  onmission=%s", 
    asset.spawngroupname, tostring(isRequested), tostring(isReserved), tostring(isSpawned), tostring(isOnMission)))
    
    -- First check that asset is not requested or reserved. This could happen if multiple requests are processed simultaniously.
    if not (isRequested or isReserved) then
    
      -- Check if asset is currently on a mission (STARTED or QUEUED).
      if self.legion:IsAssetOnMission(asset) then
        ---
        -- Asset is already on a mission.
        ---
 
        -- Check if this asset is currently on a mission (STARTED or EXECUTING).
        if MissionType==AUFTRAG.Type.RELOCATECOHORT then
        
          -- Relocation: Take all assets. Mission will be cancelled.
          table.insert(assets, asset)
          
        elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.NOTHING) then

          -- Assets on mission NOTHING are considered.
          table.insert(assets, asset)
          
        elseif self.legion:IsAssetOnMission(asset, {AUFTRAG.Type.GCICAP, AUFTRAG.Type.PATROLRACETRACK}) and MissionType==AUFTRAG.Type.INTERCEPT then
  
          -- Check if the payload of this asset is compatible with the mission.
          -- Note: we do not check the payload as an asset that is on a GCICAP mission should be able to do an INTERCEPT as well!
          self:T(self.lid..string.format("Adding asset on GCICAP mission for an INTERCEPT mission"))
          table.insert(assets, asset)
          
        elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.ONGUARD) and (MissionType==AUFTRAG.Type.ARTY or MissionType==AUFTRAG.Type.GROUNDATTACK) then
        
          if not opsgroup:IsOutOfAmmo() then
            self:T(self.lid..string.format("Adding asset on ONGUARD mission for an XXX mission"))
            table.insert(assets, asset)
          end        

        elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.PATROLZONE) and (MissionType==AUFTRAG.Type.ARTY or MissionType==AUFTRAG.Type.GROUNDATTACK) then
        
          if not opsgroup:IsOutOfAmmo() then
            self:T(self.lid..string.format("Adding asset on PATROLZONE mission for an XXX mission"))
            table.insert(assets, asset)
          end        
          
        elseif self.legion:IsAssetOnMission(asset, AUFTRAG.Type.ALERT5) and AUFTRAG.CheckMissionCapability(MissionType, asset.payload.capabilities) and MissionType~=AUFTRAG.Type.ALERT5 then
                  
          -- Check if the payload of this asset is compatible with the mission.
          self:T(self.lid..string.format("Adding asset on ALERT 5 mission for %s mission", MissionType))
          table.insert(assets, asset)
                            
        end
      
      else
        ---
        -- Asset as NO current mission
        ---
  
        if asset.spawned then
          ---
          -- Asset is already SPAWNED (could be uncontrolled on the airfield or inbound after another mission)
          ---
        
          -- Opsgroup.
          local flightgroup=asset.flightgroup
          
          
          if flightgroup and flightgroup:IsAlive() and not (flightgroup:IsDead() or flightgroup:IsStopped()) then
            
            --self:I("OpsGroup is alive")
            
            -- Assume we are ready and check if any condition tells us we are not.
            local combatready=true
                
            -- Check if in a state where we really do not want to fight any more.
            if flightgroup:IsFlightgroup() then
            
              ---
              -- FLIGHTGROUP combat ready?
              ---
            
              -- No more attacks if fuel is already low. Safety first!
              if flightgroup:IsFuelLow() then
                combatready=false
              end
                        
              if MissionType==AUFTRAG.Type.INTERCEPT and not flightgroup:CanAirToAir() then
                combatready=false
              else
                local excludeguns=MissionType==AUFTRAG.Type.BOMBING or MissionType==AUFTRAG.Type.BOMBRUNWAY or MissionType==AUFTRAG.Type.BOMBCARPET or MissionType==AUFTRAG.Type.SEAD or MissionType==AUFTRAG.Type.ANTISHIP
                if excludeguns and not flightgroup:CanAirToGround(excludeguns) then
                  combatready=false
                end 
              end
                        
              if flightgroup:IsHolding() or flightgroup:IsLanding() or flightgroup:IsLanded() or flightgroup:IsArrived() then
                combatready=false
              end
              if asset.payload and not AUFTRAG.CheckMissionCapability(MissionType, asset.payload.capabilities) then
                combatready=false
              end
              
            else

              ---
              -- ARMY/NAVYGROUP combat ready?
              ---
            
              -- Disable this for now as it can cause problems - at least with transport and cargo assets.
              --self:I("Attribute is: "..asset.attribute)
              if flightgroup:IsArmygroup() then
                -- check for fighting assets
                if asset.attribute == WAREHOUSE.Attribute.GROUND_ARTILLERY or 
                      asset.attribute == WAREHOUSE.Attribute.GROUND_TANK or 
                      asset.attribute == WAREHOUSE.Attribute.GROUND_INFANTRY or 
                      asset.attribute == WAREHOUSE.Attribute.GROUND_AAA or 
                      asset.attribute == WAREHOUSE.Attribute.GROUND_SAM                
                then
                   combatready=true 
                end  
              else
                combatready=false
              end

              -- Not ready when rearming, retreating or returning!
              if flightgroup:IsRearming() or flightgroup:IsRetreating() or flightgroup:IsReturning() then
                combatready=false
              end
              
            end
            
            -- Not ready when currently acting as ops transport carrier.
            if flightgroup:IsLoading() or flightgroup:IsTransporting() or flightgroup:IsUnloading() or flightgroup:IsPickingup() or flightgroup:IsCarrier() then
              combatready=false
            end
            -- Not ready when currently acting as ops transport cargo.
            if flightgroup:IsCargo() or flightgroup:IsBoarding() or flightgroup:IsAwaitingLift() then
              combatready=false
            end
                                
            -- This asset is "combatready".
            if combatready then
              self:T(self.lid.."Adding SPAWNED asset to ANOTHER mission as it is COMBATREADY")
              table.insert(assets, asset)
            end
          
          end
          
        else
        
          ---
          -- Asset is still in STOCK
          ---          
        
          -- Check that we have payloads and asset is repaired.
          if Npayloads>0 and self:IsRepaired(asset) then
                      
            -- Add this asset to the selection.
            table.insert(assets, asset)
            
            -- Reduce number of payloads so we only return the number of assets that could do the job.
            Npayloads=Npayloads-1
            
          end
          
        end      
      end
      
    end -- not requested check
  end -- loop over assets

  self:T2(self.lid..string.format("Recruited %d assets for Mission type=%s", #assets, MissionType))

  return assets, Npayloads
end


--- Get the time an asset needs to be repaired.
-- @param #COHORT self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset.
-- @return #number Time in seconds until asset is repaired.
function COHORT:GetRepairTime(Asset)

  if Asset.Treturned then
  
    local t=self.maintenancetime    
    t=t+Asset.damage*self.repairtime
    
    -- Seconds after returned.
    local dt=timer.getAbsTime()-Asset.Treturned
  
    local T=t-dt
    
    return T
  else
    return 0
  end

end

--- Get max mission range. We add the largest weapon range, e.g. for arty or naval if weapon data is available.
-- @param #COHORT self
-- @param #table WeaponTypes (Optional) Weapon bit type(s) to add to the total range. Default is the max weapon type available.  
-- @return #number Range in meters.
function COHORT:GetMissionRange(WeaponTypes)

  if WeaponTypes and type(WeaponTypes)~="table" then
    WeaponTypes={WeaponTypes}
  end
  
  local function checkWeaponType(Weapon)
    local weapon=Weapon --Ops.OpsGroup#OPSGROUP.WeaponData
    if WeaponTypes and #WeaponTypes>0 then
      for _,weapontype in pairs(WeaponTypes) do
        if weapontype==weapon.BitType then
          return true
        end
      end
      return false
    end
    return true
  end

  -- Get max weapon range.  
  local WeaponRange=0
  for _,_weapon in pairs(self.weaponData or {}) do
    local weapon=_weapon --Ops.OpsGroup#OPSGROUP.WeaponData
    
    if weapon.RangeMax>WeaponRange and checkWeaponType(weapon) then
      WeaponRange=weapon.RangeMax
    end
  end

  return self.engageRange+WeaponRange
end

--- Checks if a mission type is contained in a table of possible types.
-- @param #COHORT self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset.
-- @return #boolean If true, the requested mission type is part of the possible mission types.
function COHORT:IsRepaired(Asset)

  if Asset.Treturned then
    local Tnow=timer.getAbsTime()
    local Trepaired=Asset.Treturned+self.maintenancetime
    if Tnow>=Trepaired then
      return true
    else
      return false
    end
  
  else
    return true
  end

end

--- Check if the cohort attribute matches the given attribute(s).
-- @param #COHORT self
-- @param #table Attributes The requested attributes. See `WAREHOUSE.Attribute` enum. Can also be passed as a single attribute `#string`.
-- @return #boolean If true, the cohort has the requested attribute.
function COHORT:CheckAttribute(Attributes)

  if type(Attributes)~="table" then
    Attributes={Attributes}
  end

  for _,attribute in pairs(Attributes) do
    if attribute==self.attribute then
      return true
    end
  end

  return false
end

--- Check ammo.
-- @param #COHORT self
-- @return Ops.OpsGroup#OPSGROUP.Ammo Ammo.
function COHORT:_CheckAmmo()

  -- Get units of group.
  local units=self.templategroup:GetUnits()

  -- Init counter.
  local nammo=0
  local nguns=0
  local nshells=0
  local nrockets=0
  local nmissiles=0
  local nmissilesAA=0
  local nmissilesAG=0
  local nmissilesAS=0
  local nmissilesSA=0
  local nmissilesBM=0
  local nmissilesCR=0
  local ntorps=0
  local nbombs=0

  
  for _,_unit in pairs(units) do
    local unit=_unit --Wrapper.Unit#UNIT
    
    -- Output.
    local text=string.format("Unit %s:\n", unit:GetName())
  
    -- Get ammo table.
    local ammotable=unit:GetAmmo()
  
    if ammotable then
    
      -- Debug info.
      self:T3(ammotable)
  
      -- Loop over all weapons.
      for w=1,#ammotable do
      
        -- Weapon table.
        local weapon=ammotable[w]
        
        -- Descriptors.
        local Desc=weapon["desc"]
        
        -- Warhead.
        local Warhead=Desc["warhead"]
  
        -- Number of current weapon.
        local Nammo=weapon["count"]
        
        -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3, torpedo=4
        local Category=Desc["category"]
  
        -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6
        local MissileCategory = (Category==Weapon.Category.MISSILE) and Desc.missileCategory or nil

        -- Type name of current weapon.
        local TypeName=Desc["typeName"]
  
        -- WeaponName
        local weaponString = UTILS.Split(TypeName,"%.")
        local WeaponName   = weaponString[#weaponString]
  
             
        -- Range in meters. Seems only to exist for missiles (not shells).
        local Rmin=Desc["rangeMin"] or 0
        local Rmax=Desc["rangeMaxAltMin"] or 0
        
        -- Caliber in mm.
        local Caliber=Warhead and Warhead["caliber"] or 0
        
      
        -- We are specifically looking for shells or rockets here.
        if Category==Weapon.Category.SHELL then
          ---
          -- SHELL
          ---
  
          -- Add up all shells.
          if Caliber<70 then
            nguns=nguns+Nammo
          else
            nshells=nshells+Nammo            
          end
  
          -- Debug info.
          text=text..string.format("- %d shells [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, WeaponName, Caliber, Rmin, Rmax)
  
        elseif Category==Weapon.Category.ROCKET then
          ---
          -- ROCKET
          ---
        
          -- Add up all rockets.
          nrockets=nrockets+Nammo
  
          -- Debug info.
          text=text..string.format("- %d rockets [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, WeaponName, Caliber, Rmin, Rmax)
  
        elseif Category==Weapon.Category.BOMB then
          ---
          -- BOMB
          ---
  
          -- Add up all rockets.
          nbombs=nbombs+Nammo
  
          -- Debug info.
          text=text..string.format("- %d bombs [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, WeaponName, Caliber, Rmin, Rmax)
  
        elseif Category==Weapon.Category.MISSILE then
          ---
          -- MISSILE
          ---
  
          -- Add up all cruise missiles (category 5)
          if MissileCategory==Weapon.MissileCategory.AAM then
            nmissiles=nmissiles+Nammo
            nmissilesAA=nmissilesAA+Nammo
            -- Auto add range for AA missles. Useless here as this is not an aircraft.
            if Rmax>0 then
              self:AddWeaponRange(UTILS.MetersToNM(Rmin), UTILS.MetersToNM(Rmax), ENUMS.WeaponFlag.AnyAA)
            end           
          elseif MissileCategory==Weapon.MissileCategory.SAM then
            nmissiles=nmissiles+Nammo
            nmissilesSA=nmissilesSA+Nammo
            -- Dont think there is a bit type for SAM.
            if Rmax>0 then
              --self:AddWeaponRange(Rmin, Rmax, ENUMS.WeaponFlag.AnyASM)
            end                        
          elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then
            nmissiles=nmissiles+Nammo
            nmissilesAS=nmissilesAS+Nammo
            -- Auto add weapon range for anti-ship missile.
            if Rmax>0 then
              self:AddWeaponRange(UTILS.MetersToNM(Rmin), UTILS.MetersToNM(Rmax), ENUMS.WeaponFlag.AntiShipMissile)
            end                                    
          elseif MissileCategory==Weapon.MissileCategory.BM then
            nmissiles=nmissiles+Nammo
            nmissilesBM=nmissilesBM+Nammo
            -- Don't think there is a good bit type for ballistic missiles.
            if Rmax>0 then
              --self:AddWeaponRange(Rmin, Rmax, ENUMS.WeaponFlag.AnyASM)
            end                                    
          elseif MissileCategory==Weapon.MissileCategory.CRUISE then
            nmissiles=nmissiles+Nammo
            nmissilesCR=nmissilesCR+Nammo            
            -- Auto add weapon range for cruise missile.
            if Rmax>0 then
              self:AddWeaponRange(UTILS.MetersToNM(Rmin), UTILS.MetersToNM(Rmax), ENUMS.WeaponFlag.CruiseMissile)
            end                                    
          elseif MissileCategory==Weapon.MissileCategory.OTHER then
            nmissiles=nmissiles+Nammo
            nmissilesAG=nmissilesAG+Nammo
          end
  
          -- Debug info.
          text=text..string.format("- %d %s missiles [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, self:_MissileCategoryName(MissileCategory), WeaponName, Caliber, Rmin, Rmax)
  
        elseif Category==Weapon.Category.TORPEDO then
  
          -- Add up all rockets.
          ntorps=ntorps+Nammo
  
          -- Debug info.
          text=text..string.format("- %d torpedos [%s]: caliber=%d mm, range=%d - %d meters\n", Nammo, WeaponName, Caliber, Rmin, Rmax)
  
        else
  
          -- Debug info.
          text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n", Nammo, TypeName, Category, tostring(MissileCategory))
  
        end
  
      end
    end
      
    -- Debug text and send message.
    if self.verbose>=5 then
      self:I(self.lid..text)
    else
      self:T2(self.lid..text)
    end
    
  end

  -- Total amount of ammunition.
  nammo=nguns+nshells+nrockets+nmissiles+nbombs+ntorps

  local ammo={} --Ops.OpsGroup#OPSGROUP.Ammo
  ammo.Total=nammo
  ammo.Guns=nguns
  ammo.Shells=nshells
  ammo.Rockets=nrockets
  ammo.Bombs=nbombs
  ammo.Torpedos=ntorps
  ammo.Missiles=nmissiles
  ammo.MissilesAA=nmissilesAA
  ammo.MissilesAG=nmissilesAG
  ammo.MissilesAS=nmissilesAS
  ammo.MissilesCR=nmissilesCR
  ammo.MissilesBM=nmissilesBM
  ammo.MissilesSA=nmissilesSA

  return ammo
end

--- Returns a name of a missile category.
-- @param #COHORT self
-- @param #number categorynumber Number of missile category from weapon missile category enumerator. See https://wiki.hoggitworld.com/view/DCS_Class_Weapon
-- @return #string Missile category name.
function COHORT:_MissileCategoryName(categorynumber)
  local cat="unknown"
  if categorynumber==Weapon.MissileCategory.AAM then
    cat="air-to-air"
  elseif categorynumber==Weapon.MissileCategory.SAM then
    cat="surface-to-air"
  elseif categorynumber==Weapon.MissileCategory.BM then
    cat="ballistic"
  elseif categorynumber==Weapon.MissileCategory.ANTI_SHIP then
    cat="anti-ship"
  elseif categorynumber==Weapon.MissileCategory.CRUISE then
    cat="cruise"
  elseif categorynumber==Weapon.MissileCategory.OTHER then
    cat="other"
  end
  return cat
end

--- Add an OPERATION.
-- @param #COHORT self
-- @param Ops.Operation#OPERATION Operation The operation this cohort is part of.
-- @return #COHORT self
function COHORT:_AddOperation(Operation)

  self.operations[Operation.name]=Operation

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Commander of Airwings, Brigades and Fleets.
--
-- **Main Features:**
--
--    * Manages AIRWINGS, BRIGADEs and FLEETs
--    * Handles missions (AUFTRAG) and finds the best assets for the job 
--
-- ===
--
-- ### Author: **funkyfranky**
-- 
-- ===
-- @module Ops.Commander
-- @image OPS_Commander.png


--- COMMANDER class.
-- @type COMMANDER
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field #number coalition Coalition side of the commander.
-- @field #string alias Alias name.
-- @field #table legions Table of legions which are commanded.
-- @field #table missionqueue Mission queue.
-- @field #table transportqueue Transport queue.
-- @field #table targetqueue Target queue.
-- @field #table opsqueue Operations queue.
-- @field #table rearmingZones Rearming zones. Each element is of type `#BRIGADE.SupplyZone`.
-- @field #table refuellingZones Refuelling zones. Each element is of type `#BRIGADE.SupplyZone`.
-- @field #table capZones CAP zones. Each element is of type `#AIRWING.PatrolZone`.
-- @field #table gcicapZones GCICAP zones. Each element is of type `#AIRWING.PatrolZone`.
-- @field #table awacsZones AWACS zones. Each element is of type `#AIRWING.PatrolZone`.
-- @field #table tankerZones Tanker zones. Each element is of type `#AIRWING.TankerZone`.
-- @field Ops.Chief#CHIEF chief Chief of staff.
-- @field #table limitMission Table of limits for mission types.
-- @extends Core.Fsm#FSM

--- *He who has never leared to obey cannot be a good commander.* -- Aristotle
--
-- ===
--
-- # The COMMANDER Concept
-- 
-- A commander is the head of legions. He/she will find the best LEGIONs to perform an assigned AUFTRAG (mission) or OPSTRANSPORT.
-- A legion can be an AIRWING, BRIGADE or FLEET.
--
-- # Constructor
-- 
-- A new COMMANDER object is created with the @{#COMMANDER.New}(*Coalition, Alias*) function, where the parameter *Coalition* is the coalition side.
-- It can be `coalition.side.RED`, `coalition.side.BLUE` or `coalition.side.NEUTRAL`. This parameter is mandatory!
-- 
-- The second parameter *Alias* is optional and can be used to give the COMMANDER a "name", which is used for output in the dcs.log file.
-- 
--     local myCommander=COMANDER:New(coalition.side.BLUE, "General Patton")
-- 
-- # Adding Legions
-- 
-- Legions, i.e. AIRWINGS, BRIGADES and FLEETS can be added via the @{#COMMANDER.AddLegion}(*Legion*) command:
-- 
--     myCommander:AddLegion(myLegion)
-- 
-- ## Adding Airwings
-- 
-- It is also possible to use @{#COMMANDER.AddAirwing}(*myAirwing*) function. This does the same as the `AddLegion` function but might be a bit more intuitive.
-- 
-- ## Adding Brigades
-- 
-- It is also possible to use @{#COMMANDER.AddBrigade}(*myBrigade*) function. This does the same as the `AddLegion` function but might be a bit more intuitive.
-- 
-- ## Adding Fleets
-- 
-- It is also possible to use @{#COMMANDER.AddFleet}(*myFleet*) function. This does the same as the `AddLegion` function but might be a bit more intuitive.
-- 
-- # Adding Missions
-- 
-- Mission can be added via the @{#COMMANDER.AddMission}(*myMission*) function.
-- 
-- # Adding OPS Transports
-- 
-- Transportation assignments can be added via the @{#COMMANDER.AddOpsTransport}(*myTransport*) function.
-- 
-- # Adding CAP Zones
-- 
-- A CAP zone can be added via the @{#COMMANDER.AddCapZone}() function.
-- 
-- # Adding Rearming Zones
-- 
-- A rearming zone can be added via the @{#COMMANDER.AddRearmingZone}() function.
-- 
-- # Adding Refuelling Zones
-- 
-- A refuelling zone can be added via the @{#COMMANDER.AddRefuellingZone}() function.
-- 
-- 
-- # FSM Events
-- 
-- The COMMANDER will 
-- 
-- ## OPSGROUP on Mission
-- 
-- Whenever an OPSGROUP (FLIGHTGROUP, ARMYGROUP or NAVYGROUP) is send on a mission, the `OnAfterOpsOnMission()` event is triggered.
-- Mission designers can hook into the event with the @{#COMMANDER.OnAfterOpsOnMission}() function
-- 
--     function myCommander:OnAfterOpsOnMission(From, Event, To, OpsGroup, Mission)
--       -- Your code
--     end
--     
-- ## Canceling a Mission
-- 
-- A mission can be cancelled with the @{#COMMMANDER.MissionCancel}() function
-- 
--     myCommander:MissionCancel(myMission)
--     
-- or
--     myCommander:__MissionCancel(5*60, myMission)
--     
-- The last commander cancels the mission after 5 minutes (300 seconds).
-- 
-- The cancel command will be forwarded to all assigned legions and OPS groups, which will abort their mission or remove it from their queue.
--
-- @field #COMMANDER
COMMANDER = {
  ClassName       = "COMMANDER",
  verbose         =     0,
  coalition       =   nil,
  legions         =    {},
  missionqueue    =    {},
  transportqueue  =    {},
  targetqueue     =    {},
  opsqueue        =    {},
  rearmingZones   =    {},
  refuellingZones =    {},
  capZones        =    {},
  gcicapZones     =    {},
  awacsZones      =    {},
  tankerZones     =    {},
  limitMission    =    {},
  maxMissionsAssignPerCycle = 1,
}

--- COMMANDER class version.
-- @field #string version
COMMANDER.version="0.1.4"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- DONE: Add CAP zones.
-- DONE: Add tanker zones.
-- DONE: Improve legion selection. Mostly done!
-- DONE: Find solution for missions, which require a transport. This is not as easy as it sounds since the selected mission assets restrict the possible transport assets.
-- DONE: Add ops transports.
-- DONE: Allow multiple Legions for one mission.
-- NOGO: Maybe it's possible to preselect the assets for the mission.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new COMMANDER object and start the FSM.
-- @param #COMMANDER self
-- @param #number Coalition Coaliton of the commander.
-- @param #string Alias Some name you want the commander to be called.
-- @return #COMMANDER self
function COMMANDER:New(Coalition, Alias)

  -- Inherit everything from INTEL class.
  local self=BASE:Inherit(self, FSM:New()) --#COMMANDER

  if Coalition==nil then
    env.error("ERROR: Coalition parameter is nil in COMMANDER:New() call!")
    return nil
  end
  
  -- Set coaliton.
  self.coalition=Coalition
  
  -- Alias name.
  self.alias=Alias
  
  -- Choose a name for red or blue.
  if self.alias==nil then
    if Coalition==coalition.side.BLUE then
      self.alias="George S. Patton"
    elseif Coalition==coalition.side.RED then
      self.alias="Georgy Zhukov"
    elseif Coalition==coalition.side.NEUTRAL then
      self.alias="Mahatma Gandhi"
    end
  end
  
  -- Log ID.
  self.lid=string.format("COMMANDER %s [%s] | ", self.alias, UTILS.GetCoalitionName(self.coalition))

  -- Start state.
  self:SetStartState("NotReadyYet")

  -- Add FSM transitions.
  --                 From State     -->      Event        -->     To State
  self:AddTransition("NotReadyYet",        "Start",               "OnDuty")      -- Start COMMANDER.
  self:AddTransition("*",                  "Status",              "*")           -- Status report.
  self:AddTransition("*",                  "Stop",                "Stopped")     -- Stop COMMANDER.
  
  self:AddTransition("*",                  "MissionAssign",       "*")           -- Mission is assigned to a or multiple LEGIONs.
  self:AddTransition("*",                  "MissionCancel",       "*")           -- COMMANDER cancels a mission.

  self:AddTransition("*",                  "TransportAssign",     "*")           -- Transport is assigned to a or multiple LEGIONs.
  self:AddTransition("*",                  "TransportCancel",     "*")           -- COMMANDER cancels a Transport.

  self:AddTransition("*",                  "OpsOnMission",        "*")           -- An OPSGROUP was send on a Mission (AUFTRAG).
  
  self:AddTransition("*",                  "LegionLost",          "*")           -- Out of our legions was lost to the enemy.

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the COMMANDER.
  -- @function [parent=#COMMANDER] Start
  -- @param #COMMANDER self

  --- Triggers the FSM event "Start" after a delay. Starts the COMMANDER.
  -- @function [parent=#COMMANDER] __Start
  -- @param #COMMANDER self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Stop". Stops the COMMANDER.
  -- @param #COMMANDER self

  --- Triggers the FSM event "Stop" after a delay. Stops the COMMANDER.
  -- @function [parent=#COMMANDER] __Stop
  -- @param #COMMANDER self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Status".
  -- @function [parent=#COMMANDER] Status
  -- @param #COMMANDER self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#COMMANDER] __Status
  -- @param #COMMANDER self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "MissionAssign". Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission!
  -- @function [parent=#COMMANDER] MissionAssign
  -- @param #COMMANDER self
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.
  -- @param #table Legions The Legion(s) to which the mission is assigned.

  --- Triggers the FSM event "MissionAssign" after a delay. Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission!
  -- @function [parent=#COMMANDER] __MissionAssign
  -- @param #COMMANDER self
  -- @param #number delay Delay in seconds.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.
  -- @param #table Legions The Legion(s) to which the mission is assigned.

  --- On after "MissionAssign" event.
  -- @function [parent=#COMMANDER] OnAfterMissionAssign
  -- @param #COMMANDER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.
  -- @param #table Legions The Legion(s) to which the mission is assigned.


  --- Triggers the FSM event "MissionCancel".
  -- @function [parent=#COMMANDER] MissionCancel
  -- @param #COMMANDER self
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- Triggers the FSM event "MissionCancel" after a delay.
  -- @function [parent=#COMMANDER] __MissionCancel
  -- @param #COMMANDER self
  -- @param #number delay Delay in seconds.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "MissionCancel" event.
  -- @function [parent=#COMMANDER] OnAfterMissionCancel
  -- @param #COMMANDER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.


  --- Triggers the FSM event "TransportAssign".
  -- @function [parent=#COMMANDER] TransportAssign
  -- @param #COMMANDER self
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.
  -- @param #table Legions The legion(s) to which this transport is assigned.

  --- Triggers the FSM event "TransportAssign" after a delay.
  -- @function [parent=#COMMANDER] __TransportAssign
  -- @param #COMMANDER self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.
  -- @param #table Legions The legion(s) to which this transport is assigned.

  --- On after "TransportAssign" event.
  -- @function [parent=#COMMANDER] OnAfterTransportAssign
  -- @param #COMMANDER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.
  -- @param #table Legions The legion(s) to which this transport is assigned.


  --- Triggers the FSM event "TransportCancel".
  -- @function [parent=#COMMANDER] TransportCancel
  -- @param #COMMANDER self
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.

  --- Triggers the FSM event "TransportCancel" after a delay.
  -- @function [parent=#COMMANDER] __TransportCancel
  -- @param #COMMANDER self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.

  --- On after "TransportCancel" event.
  -- @function [parent=#COMMANDER] OnAfterTransportCancel
  -- @param #COMMANDER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.


  --- Triggers the FSM event "OpsOnMission".
  -- @function [parent=#COMMANDER] OpsOnMission
  -- @param #COMMANDER self
  -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- Triggers the FSM event "OpsOnMission" after a delay.
  -- @function [parent=#COMMANDER] __OpsOnMission
  -- @param #COMMANDER self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "OpsOnMission" event.
  -- @function [parent=#COMMANDER] OnAfterOpsOnMission
  -- @param #COMMANDER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.


  --- Triggers the FSM event "LegionLost".
  -- @function [parent=#COMMANDER] LegionLost
  -- @param #COMMANDER self
  -- @param Ops.Legion#LEGION Legion The legion that was lost.
  -- @param DCS#coalition.side Coalition which captured the warehouse.
  -- @param DCS#country.id Country which has captured the warehouse.

  --- Triggers the FSM event "LegionLost".
  -- @function [parent=#COMMANDER] __LegionLost
  -- @param #COMMANDER self
  -- @param #number delay Delay in seconds.
  -- @param Ops.Legion#LEGION Legion The legion that was lost.
  -- @param DCS#coalition.side Coalition which captured the warehouse.
  -- @param DCS#country.id Country which has captured the warehouse.

  --- On after "LegionLost" event.
  -- @function [parent=#COMMANDER] OnAfterLegionLost
  -- @param #COMMANDER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Legion#LEGION Legion The legion that was lost.
  -- @param DCS#coalition.side Coalition which captured the warehouse.
  -- @param DCS#country.id Country which has captured the warehouse.

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set verbosity level.
-- @param #COMMANDER self
-- @param #number VerbosityLevel Level of output (higher=more). Default 0.
-- @return #COMMANDER self
function COMMANDER:SetVerbosity(VerbosityLevel)
  self.verbose=VerbosityLevel or 0
  return self
end

--- Set limit for number of total or specific missions to be executed simultaniously.
-- @param #COMMANDER self
-- @param #number Limit Number of max. mission of this type. Default 10.
-- @param #string MissionType Type of mission, e.g. `AUFTRAG.Type.BAI`. Default `"Total"` for total number of missions.
-- @return #COMMANDER self
function COMMANDER:SetLimitMission(Limit, MissionType)
  MissionType=MissionType or "Total"
  if MissionType then
    self.limitMission[MissionType]=Limit or 10
  else
    self:E(self.lid.."ERROR: No mission type given for setting limit!")
  end
  return self
end


--- Get coalition.
-- @param #COMMANDER self
-- @return #number Coalition.
function COMMANDER:GetCoalition()
  return self.coalition
end

--- Add an AIRWING to the commander.
-- @param #COMMANDER self
-- @param Ops.Airwing#AIRWING Airwing The airwing to add.
-- @return #COMMANDER self
function COMMANDER:AddAirwing(Airwing)

  -- Add legion.
  self:AddLegion(Airwing)
  
  return self
end

--- Add a BRIGADE to the commander.
-- @param #COMMANDER self
-- @param Ops.Brigade#BRIGADE Brigade The brigade to add.
-- @return #COMMANDER self
function COMMANDER:AddBrigade(Brigade)

  -- Add legion.
  self:AddLegion(Brigade)
  
  return self
end

--- Add a FLEET to the commander.
-- @param #COMMANDER self
-- @param Ops.Fleet#FLEET Fleet The fleet to add.
-- @return #COMMANDER self
function COMMANDER:AddFleet(Fleet)

  -- Add legion.
  self:AddLegion(Fleet)
  
  return self
end


--- Add a LEGION to the commander.
-- @param #COMMANDER self
-- @param Ops.Legion#LEGION Legion The legion to add.
-- @return #COMMANDER self
function COMMANDER:AddLegion(Legion)

  -- This legion is managed by the commander. 
  Legion.commander=self

  -- Add to legions.
  table.insert(self.legions, Legion)  
  
  return self
end

--- Remove a LEGION to the commander.
-- @param #COMMANDER self
-- @param Ops.Legion#LEGION Legion The legion to be removed.
-- @return #COMMANDER self
function COMMANDER:RemoveLegion(Legion)
    
  for i,_legion in pairs(self.legions) do
    local legion=_legion --Ops.Legion#LEGION
    if legion.alias==Legion.alias then
      table.remove(self.legions, i)
      Legion.commander=nil
    end
  end
  
  return self
end

--- Add mission to mission queue.
-- @param #COMMANDER self
-- @param Ops.Auftrag#AUFTRAG Mission Mission to be added.
-- @return #COMMANDER self
function COMMANDER:AddMission(Mission)

  if not self:IsMission(Mission) then

    Mission.commander=self
    
    Mission.statusCommander=AUFTRAG.Status.PLANNED
  
    table.insert(self.missionqueue, Mission)
    
  end

  return self
end

--- Add transport to queue.
-- @param #COMMANDER self
-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport to be added.
-- @return #COMMANDER self
function COMMANDER:AddOpsTransport(Transport)

  Transport.commander=self
  
  Transport.statusCommander=OPSTRANSPORT.Status.PLANNED

  table.insert(self.transportqueue, Transport)

  return self
end

--- Remove mission from queue.
-- @param #COMMANDER self
-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed.
-- @return #COMMANDER self
function COMMANDER:RemoveMission(Mission)

  for i,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG
    
    if mission.auftragsnummer==Mission.auftragsnummer then
      self:T(self.lid..string.format("Removing mission %s (%s) status=%s from queue", Mission.name, Mission.type, Mission.status))
      mission.commander=nil
      table.remove(self.missionqueue, i)
      break
    end
    
  end

  return self
end

--- Remove transport from queue.
-- @param #COMMANDER self
-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport to be removed.
-- @return #COMMANDER self
function COMMANDER:RemoveTransport(Transport)

  for i,_transport in pairs(self.transportqueue) do
    local transport=_transport --Ops.OpsTransport#OPSTRANSPORT
    
    if transport.uid==Transport.uid then
      self:T(self.lid..string.format("Removing transport UID=%d status=%s from queue", transport.uid, transport:GetState()))
      transport.commander=nil
      table.remove(self.transportqueue, i)
      break
    end
    
  end

  return self
end

--- Add target.
-- @param #COMMANDER self
-- @param Ops.Target#TARGET Target Target object to be added.
-- @return #COMMANDER self
function COMMANDER:AddTarget(Target)

  if not self:IsTarget(Target) then
    table.insert(self.targetqueue, Target)
  end

  return self
end

--- Add operation.
-- @param #COMMANDER self
-- @param Ops.Operation#OPERATION Operation The operation to be added.
-- @return #COMMANDER self
function COMMANDER:AddOperation(Operation)

  -- TODO: Check that is not already added.
  
  -- Add operation to table.
  table.insert(self.opsqueue, Operation)
  
  return self
end

--- Check if a TARGET is already in the queue. 
-- @param #COMMANDER self
-- @param Ops.Target#TARGET Target Target object to be added.
-- @return #boolean If `true`, target exists in the target queue.
function COMMANDER:IsTarget(Target)

  for _,_target in pairs(self.targetqueue) do
    local target=_target --Ops.Target#TARGET
    if target.uid==Target.uid or target:GetName()==Target:GetName() then
      return true
    end
  end

  return false
end

--- Remove target from queue.
-- @param #COMMANDER self
-- @param Ops.Target#TARGET Target The target.
-- @return #COMMANDER self
function COMMANDER:RemoveTarget(Target)

  for i,_target in pairs(self.targetqueue) do
    local target=_target --Ops.Target#TARGET
    
    if target.uid==Target.uid then
      self:T(self.lid..string.format("Removing target %s from queue", Target.name))
      table.remove(self.targetqueue, i)
      break
    end
    
  end

  return self
end

--- Add a rearming zone.
-- @param #COMMANDER self
-- @param Core.Zone#ZONE RearmingZone Rearming zone.
-- @return Ops.Brigade#BRIGADE.SupplyZone The rearming zone data.
function COMMANDER:AddRearmingZone(RearmingZone)

  local rearmingzone={} --Ops.Brigade#BRIGADE.SupplyZone
  
  rearmingzone.zone=RearmingZone
  rearmingzone.mission=nil
  --rearmingzone.marker=MARKER:New(rearmingzone.zone:GetCoordinate(), "Rearming Zone"):ToCoalition(self:GetCoalition())

  table.insert(self.rearmingZones, rearmingzone)

  return rearmingzone
end

--- Add a refuelling zone.
-- @param #COMMANDER self
-- @param Core.Zone#ZONE RefuellingZone Refuelling zone.
-- @return Ops.Brigade#BRIGADE.SupplyZone The refuelling zone data.
function COMMANDER:AddRefuellingZone(RefuellingZone)

  local rearmingzone={} --Ops.Brigade#BRIGADE.SupplyZone
  
  rearmingzone.zone=RefuellingZone
  rearmingzone.mission=nil
  --rearmingzone.marker=MARKER:New(rearmingzone.zone:GetCoordinate(), "Refuelling Zone"):ToCoalition(self:GetCoalition())

  table.insert(self.refuellingZones, rearmingzone)

  return rearmingzone
end

--- Add a CAP zone.
-- @param #COMMANDER self
-- @param Core.Zone#ZONE Zone CapZone Zone.
-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
-- @return Ops.Airwing#AIRWING.PatrolZone The CAP zone data.
function COMMANDER:AddCapZone(Zone, Altitude, Speed, Heading, Leg)

  local patrolzone={} --Ops.Airwing#AIRWING.PatrolZone
  
  patrolzone.zone=Zone
  patrolzone.altitude=Altitude or 12000
  patrolzone.heading=Heading or 270
  --patrolzone.speed=UTILS.KnotsToAltKIAS(Speed or 350, patrolzone.altitude)
  patrolzone.speed=Speed or 350
  patrolzone.leg=Leg or 30
  patrolzone.mission=nil
  --patrolzone.marker=MARKER:New(patrolzone.zone:GetCoordinate(), "CAP Zone"):ToCoalition(self:GetCoalition())

  table.insert(self.capZones, patrolzone)

  return patrolzone
end

--- Add a GCICAP zone.
-- @param #COMMANDER self
-- @param Core.Zone#ZONE Zone CapZone Zone.
-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
-- @return Ops.Airwing#AIRWING.PatrolZone The CAP zone data.
function COMMANDER:AddGciCapZone(Zone, Altitude, Speed, Heading, Leg)

  local patrolzone={} --Ops.Airwing#AIRWING.PatrolZone
  
  patrolzone.zone=Zone
  patrolzone.altitude=Altitude or 12000
  patrolzone.heading=Heading or 270
  --patrolzone.speed=UTILS.KnotsToAltKIAS(Speed or 350, patrolzone.altitude)
  patrolzone.speed=Speed or 350
  patrolzone.leg=Leg or 30
  patrolzone.mission=nil
  --patrolzone.marker=MARKER:New(patrolzone.zone:GetCoordinate(), "GCICAP Zone"):ToCoalition(self:GetCoalition())

  table.insert(self.gcicapZones, patrolzone)

  return patrolzone
end

--- Remove a GCI CAP.
-- @param #COMMANDER self
-- @param Core.Zone#ZONE Zone Zone, where the flight orbits.
function COMMANDER:RemoveGciCapZone(Zone)

  local patrolzone={} --Ops.Airwing#AIRWING.PatrolZone
  
  patrolzone.zone=Zone
  for i,_patrolzone in pairs(self.gcicapZones) do
    if _patrolzone.zone == patrolzone.zone then
      if _patrolzone.mission and _patrolzone.mission:IsNotOver() then
          _patrolzone.mission:Cancel()
      end
      table.remove(self.gcicapZones, i)
      break
    end
  end
  return patrolzone
end

--- Add an AWACS zone.
-- @param #COMMANDER self
-- @param Core.Zone#ZONE Zone Zone.
-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
-- @return Ops.Airwing#AIRWING.PatrolZone The AWACS zone data.
function COMMANDER:AddAwacsZone(Zone, Altitude, Speed, Heading, Leg)

  local awacszone={} --Ops.Airwing#AIRWING.PatrolZone
  
  awacszone.zone=Zone
  awacszone.altitude=Altitude or 12000
  awacszone.heading=Heading or 270
  --awacszone.speed=UTILS.KnotsToAltKIAS(Speed or 350, awacszone.altitude)
  awacszone.speed=Speed or 350
  awacszone.speed=Speed or 350
  awacszone.leg=Leg or 30
  awacszone.mission=nil
  --awacszone.marker=MARKER:New(awacszone.zone:GetCoordinate(), "AWACS Zone"):ToCoalition(self:GetCoalition())

  table.insert(self.awacsZones, awacszone)

  return awacszone
end

--- Remove a AWACS zone.
-- @param #COMMANDER self
-- @param Core.Zone#ZONE Zone Zone, where the flight orbits.
function COMMANDER:RemoveAwacsZone(Zone)

  local awacszone={} --Ops.Airwing#AIRWING.PatrolZone
  
  awacszone.zone=Zone
  for i,_awacszone in pairs(self.awacsZones) do
    if _awacszone.zone == awacszone.zone then
      if _awacszone.mission and _awacszone.mission:IsNotOver() then
          _awacszone.mission:Cancel()
      end
      table.remove(self.awacsZones, i)
      break
    end
  end
  return awacszone
end

--- Add a refuelling tanker zone.
-- @param #COMMANDER self
-- @param Core.Zone#ZONE Zone Zone.
-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
-- @param #number RefuelSystem Refuelling system.
-- @return Ops.Airwing#AIRWING.TankerZone The tanker zone data.
function COMMANDER:AddTankerZone(Zone, Altitude, Speed, Heading, Leg, RefuelSystem)

  local tankerzone={} --Ops.Airwing#AIRWING.TankerZone
  
  tankerzone.zone=Zone
  tankerzone.altitude=Altitude or 12000
  tankerzone.heading=Heading or 270
  --tankerzone.speed=UTILS.KnotsToAltKIAS(Speed or 350, tankerzone.altitude) -- speed translation to alt will be done by AUFTRAG anyhow
  tankerzone.speed = Speed or 350
  tankerzone.leg=Leg or 30
  tankerzone.refuelsystem=RefuelSystem
  tankerzone.mission=nil
  tankerzone.marker=MARKER:New(tankerzone.zone:GetCoordinate(), "Tanker Zone"):ToCoalition(self:GetCoalition())

  table.insert(self.tankerZones, tankerzone)

  return tankerzone
end

--- Remove a refuelling tanker zone.
-- @param #COMMANDER self
-- @param Core.Zone#ZONE Zone Zone, where the flight orbits.
function COMMANDER:RemoveTankerZone(Zone)

  local tankerzone={} --Ops.Airwing#AIRWING.PatrolZone
  
  tankerzone.zone=Zone
  for i,_tankerzone in pairs(self.tankerZones) do
    if _tankerzone.zone == tankerzone.zone then
      if _tankerzone.mission and _tankerzone.mission:IsNotOver() then
          _tankerzone.mission:Cancel()
      end
      table.remove(self.tankerZones, i)
      break
    end
  end
  return tankerzone
end

--- Check if this mission is already in the queue.
-- @param #COMMANDER self
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
-- @return #boolean If `true`, this mission is in the queue.
function COMMANDER:IsMission(Mission)

  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG
    if mission.auftragsnummer==Mission.auftragsnummer then
      return true
    end 
  end

  return false
end

--- Relocate a cohort to another legion.
-- Assets in stock are spawned and routed to the new legion.
-- If assets are spawned, running missions will be cancelled.
-- Cohort assets will not be available until relocation is finished.
-- @param #COMMANDER self
-- @param Ops.Cohort#COHORT Cohort The cohort to be relocated.
-- @param Ops.Legion#LEGION Legion The legion where the cohort is relocated to.
-- @param #number Delay Delay in seconds before relocation takes place. Default `nil`, *i.e.* ASAP.
-- @param #number NcarriersMin Min number of transport carriers in case the troops should be transported. Default `nil` for no transport.
-- @param #number NcarriersMax Max number of transport carriers.
-- @param #table TransportLegions Legion(s) assigned for transportation. Default is all legions of the commander.
-- @return #COMMANDER self
function COMMANDER:RelocateCohort(Cohort, Legion, Delay, NcarriersMin, NcarriersMax, TransportLegions)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, COMMANDER.RelocateCohort, self, Cohort, Legion, 0, NcarriersMin, NcarriersMax, TransportLegions)
  else
  
    -- Add cohort to legion.
    if Legion:IsCohort(Cohort.name) then
      self:E(self.lid..string.format("ERROR: Cohort %s is already part of new legion %s ==> CANNOT Relocate!", Cohort.name, Legion.alias))
      return self
    else
      table.insert(Legion.cohorts, Cohort)      
    end
    
    -- Old legion.
    local LegionOld=Cohort.legion
    
    -- Check that cohort is part of this legion
    if not LegionOld:IsCohort(Cohort.name) then
      self:E(self.lid..string.format("ERROR: Cohort %s is NOT part of this legion %s ==> CANNOT Relocate!", Cohort.name, self.alias))
      return self    
    end
    
    -- Check that legions are different.
    if LegionOld.alias==Legion.alias then
      self:E(self.lid..string.format("ERROR: old legion %s is same as new legion %s ==> CANNOT Relocate!", LegionOld.alias, Legion.alias))
      return self    
    end
    
    -- Trigger Relocate event.
    Cohort:Relocate()
    
    -- Create a relocation mission.
    local mission=AUFTRAG:_NewRELOCATECOHORT(Legion, Cohort)
   
    -- Assign cohort to mission.
    mission:AssignCohort(Cohort)
    
    -- All assets required.
    mission:SetRequiredAssets(#Cohort.assets)
    
    -- Set transportation.
    if NcarriersMin and NcarriersMin>0 then
      mission:SetRequiredTransport(Legion.spawnzone, NcarriersMin, NcarriersMax)
    end
    
    -- Assign transport legions.
    if TransportLegions then
      for _,legion in pairs(TransportLegions) do
        mission:AssignTransportLegion(legion)
      end
    else
      for _,legion in pairs(self.legions) do
        mission:AssignTransportLegion(legion)
      end 
    end
    
      -- Set mission range very large. Mission designer should know...
      mission:SetMissionRange(10000)    
    
    -- Add mission.
    self:AddMission(mission)
      
  end

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start & Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers.
-- @param #COMMANDER self
-- @param Wrapper.Group#GROUP Group Flight group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function COMMANDER:onafterStart(From, Event, To)

  -- Short info.
  local text=string.format("Starting Commander")
  self:I(self.lid..text)
  
  -- Start attached legions.
  for _,_legion in pairs(self.legions) do
    local legion=_legion --Ops.Legion#LEGION
    if legion:GetState()=="NotReadyYet" then
      legion:Start()
    end
  end

  self:__Status(-1)
end

--- On after "Status" event.
-- @param #COMMANDER self
-- @param Wrapper.Group#GROUP Group Flight group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function COMMANDER:onafterStatus(From, Event, To)

  -- FSM state.
  local fsmstate=self:GetState()

  -- Status.
  if self.verbose>=1 then
    local text=string.format("Status %s: Legions=%d, Missions=%d, Targets=%d, Transports=%d", fsmstate, #self.legions, #self.missionqueue, #self.targetqueue, #self.transportqueue)
    self:T(self.lid..text)
  end
  
  -- Check Operations queue.
  self:CheckOpsQueue()    
  
  -- Check target queue and add missions.
  self:CheckTargetQueue()  

  -- Check mission queue and assign one PLANNED mission.
  self:CheckMissionQueue()
  
  -- Check transport queue and assign one PLANNED transport.
  self:CheckTransportQueue()
  
  -- Check rearming zones.
  for _,_rearmingzone in pairs(self.rearmingZones) do
    local rearmingzone=_rearmingzone --Ops.Brigade#BRIGADE.SupplyZone
    -- Check if mission is nil or over.      
    if (not rearmingzone.mission) or rearmingzone.mission:IsOver() then
      rearmingzone.mission=AUFTRAG:NewAMMOSUPPLY(rearmingzone.zone)
      self:AddMission(rearmingzone.mission)
    end
  end
  
  -- Check refuelling zones.
  for _,_supplyzone in pairs(self.refuellingZones) do
    local supplyzone=_supplyzone --Ops.Brigade#BRIGADE.SupplyZone
    -- Check if mission is nil or over.      
    if (not supplyzone.mission) or supplyzone.mission:IsOver() then
      supplyzone.mission=AUFTRAG:NewFUELSUPPLY(supplyzone.zone)
      self:AddMission(supplyzone.mission)
    end
  end


  -- Check CAP zones.
  for _,_patrolzone in pairs(self.capZones) do
    local patrolzone=_patrolzone --Ops.Airwing#AIRWING.PatrolZone
    -- Check if mission is nil or over.
    if (not patrolzone.mission) or patrolzone.mission:IsOver() then
      local Coordinate=patrolzone.zone:GetCoordinate()
      patrolzone.mission=AUFTRAG:NewCAP(patrolzone.zone, patrolzone.altitude, patrolzone.speed, Coordinate, patrolzone.heading, patrolzone.leg)
      self:AddMission(patrolzone.mission)
    end
  end

  -- Check GCICAP zones.
  for _,_patrolzone in pairs(self.gcicapZones) do
    local patrolzone=_patrolzone --Ops.Airwing#AIRWING.PatrolZone
    -- Check if mission is nil or over.
    if (not patrolzone.mission) or patrolzone.mission:IsOver() then
      local Coordinate=patrolzone.zone:GetCoordinate()
      patrolzone.mission=AUFTRAG:NewGCICAP(Coordinate, patrolzone.altitude, patrolzone.speed, patrolzone.heading, patrolzone.leg)
      self:AddMission(patrolzone.mission)
    end
  end
  
  -- Check AWACS zones.
  for _,_awacszone in pairs(self.awacsZones) do
    local awacszone=_awacszone --Ops.Airwing#AIRWING.Patrol
    -- Check if mission is nil or over.
    if (not awacszone.mission) or awacszone.mission:IsOver() then
      local Coordinate=awacszone.zone:GetCoordinate()
      awacszone.mission=AUFTRAG:NewAWACS(Coordinate, awacszone.altitude, awacszone.speed, awacszone.heading, awacszone.leg)
      self:AddMission(awacszone.mission)
    end
  end 

  -- Check Tanker zones.
  for _,_tankerzone in pairs(self.tankerZones) do
    local tankerzone=_tankerzone --Ops.Airwing#AIRWING.TankerZone
    -- Check if mission is nil or over.
    if (not tankerzone.mission) or tankerzone.mission:IsOver() then
      local Coordinate=tankerzone.zone:GetCoordinate()
      tankerzone.mission=AUFTRAG:NewTANKER(Coordinate, tankerzone.altitude, tankerzone.speed, tankerzone.heading, tankerzone.leg, tankerzone.refuelsystem)
      self:AddMission(tankerzone.mission)
    end
  end      
    
  ---
  -- LEGIONS
  ---
 
  if self.verbose>=2 and #self.legions>0 then
  
    local text="Legions:"
    for _,_legion in pairs(self.legions) do
      local legion=_legion --Ops.Legion#LEGION
      local Nassets=legion:CountAssets()
      local Nastock=legion:CountAssets(true)
      text=text..string.format("\n* %s [%s]: Assets=%s stock=%s", legion.alias, legion:GetState(), Nassets, Nastock)
      for _,aname in pairs(AUFTRAG.Type) do
        local na=legion:CountAssets(true, {aname})
        local np=legion:CountPayloadsInStock({aname})
        local nm=legion:CountAssetsOnMission({aname})
        if na>0 or np>0 then
          text=text..string.format("\n   - %s: assets=%d, payloads=%d, on mission=%d", aname, na, np, nm)
        end
      end            
    end
    self:T(self.lid..text)
    
    
    if self.verbose>=3 then
    
      -- Count numbers
      local Ntotal=0
      local Nspawned=0
      local Nrequested=0
      local Nreserved=0
      local Nstock=0
      
      local text="\n===========================================\n"
      text=text.."Assets:"
      for _,_legion in pairs(self.legions) do
        local legion=_legion --Ops.Legion#LEGION
  
        for _,_cohort in pairs(legion.cohorts) do
          local cohort=_cohort --Ops.Cohort#COHORT
          
          for _,_asset in pairs(cohort.assets) do
            local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem

            local state="In Stock"
            if asset.flightgroup then
              state=asset.flightgroup:GetState()
              local mission=legion:GetAssetCurrentMission(asset)
              if mission then
                state=state..string.format(", Mission \"%s\" [%s]", mission:GetName(), mission:GetType())
              end
            else
              if asset.spawned then
                env.info("FF ERROR: asset has opsgroup but is NOT spawned!")
              end
              if asset.requested and asset.isReserved then
                env.info("FF ERROR: asset is requested and reserved. Should not be both!")
                state="Reserved+Requested!"
              elseif asset.isReserved then
                state="Reserved"
              elseif asset.requested then
                state="Requested"
              end
            end
                        
            -- Text.
            text=text..string.format("\n[UID=%03d] %s Legion=%s [%s]: State=%s [RID=%s]", 
            asset.uid, asset.spawngroupname, legion.alias, cohort.name, state, tostring(asset.rid))
            
            
            if asset.spawned then
              Nspawned=Nspawned+1
            end            
            if asset.requested then
              Nrequested=Nrequested+1
            end  
            if asset.isReserved then
              Nreserved=Nreserved+1
            end                      
            if not (asset.spawned or asset.requested or asset.isReserved) then
              Nstock=Nstock+1
            end
            
            Ntotal=Ntotal+1
            
          end
          
        end
  
      end
      text=text.."\n-------------------------------------------"
      text=text..string.format("\nNstock     = %d", Nstock)
      text=text..string.format("\nNreserved  = %d", Nreserved)
      text=text..string.format("\nNrequested = %d", Nrequested)
      text=text..string.format("\nNspawned   = %d", Nspawned)
      text=text..string.format("\nNtotal     = %d (=%d)", Ntotal, Nstock+Nspawned+Nrequested+Nreserved)
      text=text.."\n==========================================="
      self:I(self.lid..text)
    end
    
  end
  
  ---
  -- MISSIONS
  ---
    
  -- Mission queue.
  if self.verbose>=2 and #self.missionqueue>0 then
    local text="Mission queue:"
    for i,_mission in pairs(self.missionqueue) do
      local mission=_mission --Ops.Auftrag#AUFTRAG      
      local target=mission:GetTargetName() or "unknown"      
      text=text..string.format("\n[%d] %s (%s): status=%s, target=%s", i, mission.name, mission.type, mission.status, target)
    end
    self:I(self.lid..text)    
  end


  ---
  -- TARGETS
  ---
    
  -- Target queue.
  if self.verbose>=2 and #self.targetqueue>0 then
    local text="Target queue:"
    for i,_target in pairs(self.targetqueue) do      
      local target=_target --Ops.Target#TARGET      
      text=text..string.format("\n[%d] %s: status=%s, life=%d", i, target:GetName(), target:GetState(), target:GetLife())
    end
    self:I(self.lid..text)    
  end
  
  ---
  -- TRANSPORTS
  ---
    
  -- Transport queue.
  if self.verbose>=2 and #self.transportqueue>0 then
    local text="Transport queue:"
    for i,_transport in pairs(self.transportqueue) do
      local transport=_transport --Ops.OpsTransport#OPSTRANSPORT            
      text=text..string.format("\n[%d] UID=%d: status=%s", i, transport.uid, transport:GetState())
    end
    self:I(self.lid..text)    
  end  

  self:__Status(-30)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "MissionAssign" event. Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission already.
-- @param #COMMANDER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
-- @param #table Legions The Legion(s) to which the mission is assigned.
function COMMANDER:onafterMissionAssign(From, Event, To, Mission, Legions)
    
  -- Add mission to queue.
  self:AddMission(Mission)

  -- Set mission commander status to QUEUED as it is now queued at a legion.
  Mission.statusCommander=AUFTRAG.Status.QUEUED

  for _,_Legion in pairs(Legions) do
    local Legion=_Legion --Ops.Legion#LEGION

    -- Debug info.
    self:T(self.lid..string.format("Assigning mission \"%s\" [%s] to legion \"%s\"", Mission.name, Mission.type, Legion.alias))

    -- Add mission to legion.
    Legion:AddMission(Mission)
    
    -- Directly request the mission as the assets have already been selected.
    Legion:MissionRequest(Mission)
    
  end

end

--- On after "MissionCancel" event.
-- @param #COMMANDER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
function COMMANDER:onafterMissionCancel(From, Event, To, Mission)

  -- Debug info.
  self:T(self.lid..string.format("Cancelling mission \"%s\" [%s] in status %s", Mission.name, Mission.type, Mission.status))
  
  -- Set commander status.
  Mission.statusCommander=AUFTRAG.Status.CANCELLED
  
  if Mission:IsPlanned() then
  
    -- Mission is still in planning stage. Should not have a legion assigned ==> Just remove it form the queue.
    self:RemoveMission(Mission)
    
  else
  
    -- Legion will cancel mission.
    if #Mission.legions>0 then
      for _,_legion in pairs(Mission.legions) do
        local legion=_legion --Ops.Legion#LEGION
        
        -- TODO: Should check that this legions actually belongs to this commander.
        
        -- Legion will cancel the mission.
        legion:MissionCancel(Mission)
      end
    end
    
  end

end

--- On after "TransportAssign" event. Transport is added to a LEGION transport queue.
-- @param #COMMANDER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.
  -- @param #table Legions The legion(s) to which this transport is assigned.
function COMMANDER:onafterTransportAssign(From, Event, To, Transport, Legions)
  
  -- Set mission commander status to QUEUED as it is now queued at a legion.
  Transport.statusCommander=OPSTRANSPORT.Status.QUEUED
  
  for _,_Legion in pairs(Legions) do
    local Legion=_Legion --Ops.Legion#LEGION

    -- Debug info.
    self:T(self.lid..string.format("Assigning transport UID=%d to legion \"%s\"", Transport.uid, Legion.alias))
  
    -- Add mission to legion.
    Legion:AddOpsTransport(Transport)
    
    -- Directly request the mission as the assets have already been selected.
    Legion:TransportRequest(Transport)
    
  end

end

--- On after "TransportCancel" event.
-- @param #COMMANDER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.
function COMMANDER:onafterTransportCancel(From, Event, To, Transport)

  -- Debug info.
  self:T(self.lid..string.format("Cancelling Transport UID=%d in status %s", Transport.uid, Transport:GetState()))
  
  -- Set commander status.
  Transport.statusCommander=OPSTRANSPORT.Status.CANCELLED
  
  if Transport:IsPlanned() then
  
    -- Transport is still in planning stage. Should not have a legion assigned ==> Just remove it form the queue.
    self:RemoveTransport(Transport)
    
  else
  
    -- Legion will cancel mission.
    if #Transport.legions>0 then
      for _,_legion in pairs(Transport.legions) do
        local legion=_legion --Ops.Legion#LEGION
        
        -- TODO: Should check that this legions actually belongs to this commander.
        
        -- Legion will cancel the mission.
        legion:TransportCancel(Transport)
      end
    end
    
  end

end

--- On after "OpsOnMission".
-- @param #COMMANDER self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP OpsGroup Ops group on mission
-- @param Ops.Auftrag#AUFTRAG Mission The requested mission.
function COMMANDER:onafterOpsOnMission(From, Event, To, OpsGroup, Mission)
  -- Debug info.
  self:T2(self.lid..string.format("Group \"%s\" on mission \"%s\" [%s]", OpsGroup:GetName(), Mission:GetName(), Mission:GetType()))
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Mission Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check OPERATIONs queue.
-- @param #COMMANDER self 
function COMMANDER:CheckOpsQueue()

  -- Number of missions.
  local Nops=#self.opsqueue

  -- Treat special cases.
  if Nops==0 then
    return nil
  end
  
  -- Loop over operations.
  for _,_ops in pairs(self.opsqueue) do
    local operation=_ops --Ops.Operation#OPERATION
    
    if operation:IsRunning() then
    
      -- Loop over missions.
      for _,_mission in pairs(operation.missions or {}) do
        local mission=_mission --Ops.Auftrag#AUFTRAG
        
        if mission.phase==nil or (mission.phase and mission.phase==operation.phase) and mission:IsPlanned() then
          self:AddMission(mission)
        end
      end
      
      -- Loop over targets.
      for _,_target in pairs(operation.targets or {}) do
        local target=_target --Ops.Target#TARGET
        
        if (target.phase==nil or (target.phase and target.phase==operation.phase)) and (not self:IsTarget(target)) then
          self:AddTarget(target)
        end
      end    
    
    end
    
  end
  
end

--- Check target queue and assign ONE valid target by adding it to the mission queue of the COMMANDER.
-- @param #COMMANDER self 
function COMMANDER:CheckTargetQueue()

  -- Number of missions.
  local Ntargets=#self.targetqueue

  -- Treat special cases.
  if Ntargets==0 then
    return nil
  end
  
  -- Remove done targets.
  for i=#self.targetqueue,1,-1 do
    local target=self.targetqueue[i] --Ops.Target#TARGET
    if (not target:IsAlive()) or target:EvalConditionsAny(target.conditionStop) then   
      for _,_resource in pairs(target.resources) do
        local resource=_resource --Ops.Target#TARGET.Resource
        if resource.mission and resource.mission:IsNotOver() then
          self:MissionCancel(resource.mission)
        end
      end
      table.remove(self.targetqueue, i)
    end
  end
  
  -- Check if total number of missions is reached.
  local NoLimit=self:_CheckMissionLimit("Total")
  if NoLimit==false then
    return nil
  end

  -- Sort results table wrt prio and threatlevel.
  local function _sort(a, b)
    local taskA=a --Ops.Target#TARGET
    local taskB=b --Ops.Target#TARGET
    return (taskA.prio<taskB.prio) or (taskA.prio==taskB.prio and taskA.threatlevel0>taskB.threatlevel0)
  end
  table.sort(self.targetqueue, _sort)

  -- Get the lowest importance value (lower means more important).
  -- If a target with importance 1 exists, targets with importance 2 will not be assigned. Targets with no importance (nil) can still be selected. 
  local vip=math.huge
  for _,_target in pairs(self.targetqueue) do
    local target=_target --Ops.Target#TARGET
    if target:IsAlive() and target.importance and target.importance<vip then
      vip=target.importance
    end
  end

  -- Loop over targets.
  for _,_target in pairs(self.targetqueue) do
    local target=_target --Ops.Target#TARGET
    
    -- Is target still alive.
    local isAlive=target:IsAlive()
    
    -- Is this target important enough.
    local isImportant=(target.importance==nil or target.importance<=vip)
    
    -- Check ALL start conditions are true.
    local isReadyStart=target:EvalConditionsAll(target.conditionStart)
    
    -- Debug message.
    local text=string.format("Target %s: Alive=%s, Important=%s", target:GetName(), tostring(isAlive), tostring(isImportant))
    self:T2(self.lid..text)

    -- Check that target is alive and not already a mission has been assigned.
    if isAlive and isImportant then

      for _,_resource in pairs(target.resources or {}) do
        local resource=_resource --Ops.Target#TARGET.Resource
        
        -- Mission type.
        local missionType=resource.MissionType

        if (not resource.mission) or resource.mission:IsOver() then

          -- Debug info.
          self:T2(self.lid..string.format("Target \"%s\" ==> Creating mission type %s: Nmin=%d, Nmax=%d", target:GetName(), missionType, resource.Nmin, resource.Nmax))          
          
          -- Create a mission.
          local mission=AUFTRAG:NewFromTarget(target, missionType)
          
          if mission then
          
            -- Set mission parameters.
            mission:SetRequiredAssets(resource.Nmin, resource.Nmax)
            mission:SetRequiredAttribute(resource.Attributes)
            mission:SetRequiredProperty(resource.Properties)
            
            -- Set operation (if any).
            mission.operation=target.operation
            
            -- Set resource mission.
            resource.mission=mission
            
            -- Add mission to queue.
            self:AddMission(resource.mission)
            
          end
         
        end
      
      end
              
    end
  end
  
end


--- Check mission queue and assign ONE planned mission.
-- @param #COMMANDER self 
function COMMANDER:CheckMissionQueue()

  -- Number of missions.
  local Nmissions=#self.missionqueue

  -- Treat special cases.
  if Nmissions==0 then
    return nil
  end
  
  local NoLimit=self:_CheckMissionLimit("Total")  
  if NoLimit==false then
    return nil
  end

  -- Sort results table wrt prio and start time.
  local function _sort(a, b)
    local taskA=a --Ops.Auftrag#AUFTRAG
    local taskB=b --Ops.Auftrag#AUFTRAG
    return (taskA.prio<taskB.prio) or (taskA.prio==taskB.prio and taskA.Tstart<taskB.Tstart)
  end
  table.sort(self.missionqueue, _sort)

  -- Get the lowest importance value (lower means more important).
  -- If a mission with importance 1 exists, mission with importance 2 will not be assigned. Missions with no importance (nil) can still be selected. 
  local vip=math.huge
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG
    if mission.importance and mission.importance<vip then
      vip=mission.importance
    end
  end

  local missionsAssigned = 0

  -- Loop over missions in queue.
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG
    
    -- We look for PLANNED missions.
    if mission:IsPlanned() and mission:IsReadyToGo() and (mission.importance==nil or mission.importance<=vip) and self:_CheckMissionLimit(mission.type) then
    
      ---
      -- PLANNNED Mission
      -- 
      -- 1. Select best assets from legions
      -- 2. Assign mission to legions that have the best assets.
      ---    
    
      -- Recruite assets from legions.      
      local recruited, assets, legions=self:RecruitAssetsForMission(mission)
      
      if recruited then
      
        -- Add asset to mission.
        mission:_AddAssets(assets)
        
        -- Recruit asset for escorting recruited mission assets.
        local EscortAvail=self:RecruitAssetsForEscort(mission, assets)
        
        -- Transport available (or not required).
        local TransportAvail=true
        
        -- Escort requested and available.
        if EscortAvail then

          -- Check if mission assets need a transport.
          if mission.NcarriersMin then
          
            -- Recruit carrier assets for transport.
            local Transport=nil
            local Legions=mission.transportLegions or self.legions
            
            TransportAvail, Transport=LEGION.AssignAssetsForTransport(self, Legions, assets, mission.NcarriersMin, mission.NcarriersMax, mission.transportDeployZone, mission.transportDisembarkZone, mission.carrierCategories, mission.carrierAttributes, mission.carrierProperties)
            
            -- Add opstransport to mission.
            if TransportAvail and Transport then
              mission.opstransport=Transport
            end
          
          end
          
        end
        
        -- Escort and transport must be available (or not required).
        if EscortAvail and TransportAvail then
        
          -- Add mission to legion.
          self:MissionAssign(mission, legions)
        
        else
          -- Recruited assets but no requested escort available. Unrecruit assets!
          LEGION.UnRecruitAssets(assets, mission)
        end        

        missionsAssigned = missionsAssigned + 1
        if missionsAssigned >= (self.maxMissionsAssignPerCycle or 1) then
            return
        end

      end
      
    else

      ---
      -- Missions NOT in PLANNED state
      ---    
    
    end
  
  end
  
end

--- Set how many missions can be assigned in a single status iteration. (eg. This is useful for persistent missions where you need to load all AUFTRAGs on mission start and then change it back to default)
--- Warning: Increasing this value will increase the number of missions started per iteration and thus may lead to performance issues if too many missions are started at once.
-- @param #COMMANDER self
-- @param #number Number of missions assigned per status iteration. Default is 1.
-- @return #COMMANDER self.
function COMMANDER:SetMaxMissionsAssignPerCycle(MaxMissionsAssignPerCycle)
  self.maxMissionsAssignPerCycle = MaxMissionsAssignPerCycle or 1
  return self
end

--- Get cohorts.
-- @param #COMMANDER self
-- @param #table Legions Special legions.
-- @param #table Cohorts Special cohorts.
-- @param Ops.Operation#OPERATION Operation Operation.
-- @return #table Cohorts.
function COMMANDER:_GetCohorts(Legions, Cohorts, Operation)
  
  --- Function that check if a legion or cohort is part of an operation.
  local function CheckOperation(LegionOrCohort)
    -- No operations ==> no problem!
    if #self.opsqueue==0 then
      return true
    end
    
    -- Cohort is not dedicated to a running(!) operation. We assume so.
    local isAvail=true
    
    -- Only available...
    if Operation then
      isAvail=false
    end
        
    for _,_operation in pairs(self.opsqueue) do
      local operation=_operation --Ops.Operation#OPERATION
      
      -- Legion is assigned to this operation.
      local isOps=operation:IsAssignedCohortOrLegion(LegionOrCohort)
      
      if isOps and operation:IsRunning() then
        
        -- Is dedicated.
        isAvail=false
      
        if Operation==nil then
          -- No Operation given and this is dedicated to at least one operation.
          return false
        else
          if Operation.uid==operation.uid then
            -- Operation given and is part of it.
            return true
          end        
        end
      end
    end
    
    return isAvail
  end    

  -- Chosen cohorts.
  local cohorts={}
  
  -- Check if there are any special legions and/or cohorts.
  if (Legions and #Legions>0) or (Cohorts and #Cohorts>0) then
  
    -- Add cohorts of special legions.
    for _,_legion in pairs(Legions or {}) do
      local legion=_legion --Ops.Legion#LEGION
  
      -- Check that runway is operational.
      local Runway=true
      if legion:IsAirwing() then
        Runway=legion:IsRunwayOperational() and legion.airbase and legion.airbase:GetCoalition() == legion:GetCoalition()
      end

      -- Legion has to be running.
      if legion:IsRunning() and Runway then
      
        -- Add cohorts of legion.
        for _,_cohort in pairs(legion.cohorts) do
          local cohort=_cohort --Ops.Cohort#COHORT

          if CheckOperation(cohort.legion) or CheckOperation(cohort) then
            table.insert(cohorts, cohort)
          end
        end
        
      end
    end
    
    -- Add special cohorts.
    for _,_cohort in pairs(Cohorts or {}) do
      local cohort=_cohort --Ops.Cohort#COHORT
      
      if CheckOperation(cohort) then
        table.insert(cohorts, cohort)
      end
    end
    
  else

    -- No special mission legions/cohorts found ==> take own legions.
    for _,_legion in pairs(self.legions) do
      local legion=_legion --Ops.Legion#LEGION
      
      -- Check that runway is operational.
      local Runway=true
      if legion:IsAirwing() then
        Runway=legion:IsRunwayOperational() and legion.airbase and legion.airbase:GetCoalition() == legion:GetCoalition()
      end

      -- Legion has to be running.
      if legion:IsRunning() and Runway then
      
        -- Add cohorts of legion.
        for _,_cohort in pairs(legion.cohorts) do
          local cohort=_cohort --Ops.Cohort#COHORT
          
          if CheckOperation(cohort.legion) or CheckOperation(cohort) then
            table.insert(cohorts, cohort)
          end
        end
        
      end
    end
    
  end

  return cohorts
end

--- Recruit assets for a given mission.
-- @param #COMMANDER self
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
-- @return #boolean If `true` enough assets could be recruited.
-- @return #table Recruited assets.
-- @return #table Legions that have recruited assets.
function COMMANDER:RecruitAssetsForMission(Mission)

  -- Debug info.
  self:T2(self.lid..string.format("Recruiting assets for mission \"%s\" [%s]", Mission:GetName(), Mission:GetType()))

  -- Number of required assets.
  local NreqMin, NreqMax=Mission:GetRequiredAssets()

  -- Target position.
  local TargetVec2=Mission:GetTargetVec2()
  
  -- Special payloads.
  local Payloads=Mission.payloads  
  
  -- Largest cargo bay available of available carrier assets if mission assets need to be transported.
  local MaxWeight=nil
  
  if Mission.NcarriersMin then
  
    local legions=self.legions
    local cohorts=nil
    if Mission.transportLegions or Mission.transportCohorts then
      legions=Mission.transportLegions
      cohorts=Mission.transportCohorts
    end  
  
    -- Get transport cohorts.
    local Cohorts=LEGION._GetCohorts(legions, cohorts)
    
    -- Filter cohorts that can actually perform transport missions.    
    local transportcohorts={}
    for _,_cohort in pairs(Cohorts) do
      local cohort=_cohort --Ops.Cohort#COHORT
      
      -- Check if cohort can perform transport to target.
      local can=LEGION._CohortCan(cohort, AUFTRAG.Type.OPSTRANSPORT, Mission.carrierCategories, Mission.carrierAttributes, Mission.carrierProperties, nil, TargetVec2)
      
      -- MaxWeight of cargo assets is limited by the largets available cargo bay. We don't want to select, e.g., tanks that cannot be transported by APCs or helos.
      if can and (MaxWeight==nil or cohort.cargobayLimit>MaxWeight) then
        MaxWeight=cohort.cargobayLimit
      end
    end

    if MaxWeight then
        self:T(self.lid..string.format("Largest cargo bay available=%.1f", MaxWeight))
    end
  end
  
  local legions=self.legions
  local cohorts=nil
  if Mission.specialLegions or Mission.specialCohorts then
    legions=Mission.specialLegions
    cohorts=Mission.specialCohorts
  end    
  
  -- Get cohorts.
  local Cohorts=LEGION._GetCohorts(legions, cohorts, Mission.operation, self.opsqueue)
  
  -- Debug info.
  self:T(self.lid..string.format("Found %d cohort candidates for mission", #Cohorts))
  
  -- Recruite assets.
  local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads,
   Mission.engageRange, Mission.refuelSystem, nil, nil, MaxWeight, nil, Mission.attributes, Mission.properties, {Mission.engageWeaponType})

  return recruited, assets, legions
end

--- Recruit assets performing an escort mission for a given asset.
-- @param #COMMANDER self
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
-- @param #table Assets Table of assets to be escorted.
-- @return #boolean If `true`, enough assets could be recruited or no escort was required in the first place.
function COMMANDER:RecruitAssetsForEscort(Mission, Assets)

  -- Is an escort requested in the first place?
  if Mission.NescortMin and Mission.NescortMax and (Mission.NescortMin>0 or Mission.NescortMax>0) then

    -- Cohorts.
    local Cohorts=self:_GetCohorts(Mission.escortLegions, Mission.escortCohorts, Mission.operation)
    
    -- Call LEGION function but provide COMMANDER as self.
    local assigned=LEGION.AssignAssetsForEscort(self, Cohorts, Assets, Mission.NescortMin, Mission.NescortMax, Mission.escortMissionType, Mission.escortTargetTypes, Mission.escortEngageRange)
    
    return assigned    
  end

  return true
end

--- Recruit assets for a given TARGET.
-- @param #COMMANDER self
-- @param Ops.Target#TARGET Target The target.
-- @param #string MissionType Mission Type.
-- @param #number NassetsMin Min number of required assets.
-- @param #number NassetsMax Max number of required assets.
-- @return #boolean If `true` enough assets could be recruited.
-- @return #table Assets that have been recruited from all legions.
-- @return #table Legions that have recruited assets.
function COMMANDER:RecruitAssetsForTarget(Target, MissionType, NassetsMin, NassetsMax)

  -- Cohorts.
  local Cohorts=self:_GetCohorts()

  -- Target position.
  local TargetVec2=Target:GetVec2()
  
  -- Recruite assets.
  local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, MissionType, nil, NassetsMin, NassetsMax, TargetVec2)


  return recruited, assets, legions
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Transport Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check transport queue and assign ONE planned transport.
-- @param #COMMANDER self 
function COMMANDER:CheckTransportQueue()

  -- Number of missions.
  local Ntransports=#self.transportqueue

  -- Treat special cases.
  if Ntransports==0 then
    return nil
  end

  -- Sort results table wrt prio and start time.
  local function _sort(a, b)
    local taskA=a --Ops.Auftrag#AUFTRAG
    local taskB=b --Ops.Auftrag#AUFTRAG
    return (taskA.prio<taskB.prio) or (taskA.prio==taskB.prio and taskA.Tstart<taskB.Tstart)
  end
  table.sort(self.transportqueue, _sort)

  -- Get the lowest importance value (lower means more important).
  -- If a mission with importance 1 exists, mission with importance 2 will not be assigned. Missions with no importance (nil) can still be selected. 
  local vip=math.huge
  for _,_transport in pairs(self.transportqueue) do
    local transport=_transport --Ops.OpsTransport#OPSTRANSPORT
    if transport.importance and transport.importance<vip then
      vip=transport.importance
    end
  end

  for _,_transport in pairs(self.transportqueue) do
    local transport=_transport --Ops.OpsTransport#OPSTRANSPORT
    
    -- We look for PLANNED missions.
    if transport:IsPlanned() and transport:IsReadyToGo() and (transport.importance==nil or transport.importance<=vip) then
    
      ---
      -- PLANNNED Mission
      -- 
      -- 1. Select best assets from legions
      -- 2. Assign mission to legions that have the best assets.
      ---    

      -- Get all undelivered cargo ops groups.
      local cargoOpsGroups=transport:GetCargoOpsGroups(false)
      
      -- Weight of the heaviest cargo group. Necessary condition that this fits into on carrier unit!
      local weightGroup=0
      local TotalWeight=0
      
      -- Calculate the max weight so we know which cohorts can provide carriers.
      if #cargoOpsGroups>0 then  
        for _,_opsgroup in pairs(cargoOpsGroups) do
          local opsgroup=_opsgroup --Ops.OpsGroup#OPSGROUP
          local weight=opsgroup:GetWeightTotal()
          if weight>weightGroup then
            weightGroup=weight
          end
          TotalWeight=TotalWeight+weight
        end    
      end
      
      if weightGroup>0 then
    
        -- Recruite assets from legions.      
        local recruited, assets, legions=self:RecruitAssetsForTransport(transport, weightGroup, TotalWeight)
        
        if recruited then
        
          -- Add asset to transport.
          for _,_asset in pairs(assets) do
            local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
            transport:AddAsset(asset)
          end        
  
          -- Assign transport to legion(s).
          self:TransportAssign(transport, legions)
      
          -- Only ONE transport is assigned.
          return
        else
          -- Not recruited.
          LEGION.UnRecruitAssets(assets)
        end
        
      end
      
    else

      ---
      -- Missions NOT in PLANNED state
      ---    
    
    end
  
  end
  
end

--- Recruit assets for a given OPS transport.
-- @param #COMMANDER self
-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport.
-- @param #number CargoWeight Weight of the heaviest cargo group.
-- @param #number TotalWeight Total weight of all cargo groups.
-- @return #boolean If `true`, enough assets could be recruited.
-- @return #table Recruited assets.
-- @return #table Legions that have recruited assets.
function COMMANDER:RecruitAssetsForTransport(Transport, CargoWeight, TotalWeight)
  
  if CargoWeight==0 then
    -- No cargo groups!
    return false, {}, {}
  end
  
  -- Cohorts.
  local Cohorts=self:_GetCohorts()

  -- Target is the deploy zone.
  local TargetVec2=Transport:GetDeployZone():GetVec2()

  -- Number of required carriers.
  local NreqMin,NreqMax=Transport:GetRequiredCarriers()
  
  -- Recruit assets and legions.
  local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NreqMin, NreqMax, TargetVec2, nil, nil, nil, CargoWeight, TotalWeight)

  return recruited, assets, legions  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Resources
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check if limit of missions has been reached.
-- @param #COMMANDER self 
-- @param #string MissionType Type of mission.
-- @return #boolean If `true`, mission limit has **not** been reached. If `false`, limit has been reached.
function COMMANDER:_CheckMissionLimit(MissionType)

  local limit=self.limitMission[MissionType]
  
  if limit then
  
    if MissionType=="Total" then
      MissionType=AUFTRAG.Type    
    end
  
    local N=self:CountMissions(MissionType, true)
    
    if N>=limit then
      return false
    end
  end
  
  return true
end


--- Count assets of all assigned legions.
-- @param #COMMANDER self
-- @param #boolean InStock If true, only assets that are in the warehouse stock/inventory are counted.
-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types.
-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`.
-- @return #number Amount of asset groups.
function COMMANDER:CountAssets(InStock, MissionTypes, Attributes)

  local N=0
  for _,_legion in pairs(self.legions) do
    local legion=_legion --Ops.Legion#LEGION
    N=N+legion:CountAssets(InStock, MissionTypes, Attributes)
  end

  return N
end

--- Count assets of all assigned legions.
-- @param #COMMANDER self
-- @param #table MissionTypes (Optional) Count only missions of these types. Default is all types.
-- @param #boolean OnlyRunning If `true`, only count running missions.
-- @return #number Amount missions.
function COMMANDER:CountMissions(MissionTypes, OnlyRunning)

  local N=0
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG
    
    if (not OnlyRunning) or (mission.statusCommander~=AUFTRAG.Status.PLANNED) then

      -- Check if this mission type is requested.
      if AUFTRAG.CheckMissionType(mission.type, MissionTypes) then
        N=N+1
      end
      
    end
  end


  return N
end

--- Count assets of all assigned legions.
-- @param #COMMANDER self
-- @param #boolean InStock If true, only assets that are in the warehouse stock/inventory are counted.
-- @param #table Legions (Optional) Table of legions. Default is all legions.
-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types.
-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`.
-- @return #number Amount of asset groups.
function COMMANDER:GetAssets(InStock, Legions, MissionTypes, Attributes)

  -- Selected assets.
  local assets={}

  for _,_legion in pairs(Legions or self.legions) do
    local legion=_legion --Ops.Legion#LEGION
    
    --TODO Check if legion is running and maybe if runway is operational if air assets are requested.

    for _,_cohort in pairs(legion.cohorts) do
      local cohort=_cohort --Ops.Cohort#COHORT
      
      for _,_asset in pairs(cohort.assets) do
        local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
        
        -- TODO: Check if repaired.
        -- TODO: currently we take only unspawned assets.
        if not (asset.spawned or asset.isReserved or asset.requested) then
          table.insert(assets, asset)
        end
        
      end
    end
  end
  
  return assets
end

--- Check all legions if they are able to do a specific mission type at a certain location with a given number of assets.
-- @param #COMMANDER self
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
-- @return #table Table of LEGIONs that can do the mission and have at least one asset available right now.
function COMMANDER:GetLegionsForMission(Mission)

  -- Table of legions that can do the mission.
  local legions={}
  
  -- Loop over all legions.
  for _,_legion in pairs(self.legions) do
    local legion=_legion --Ops.Legion#LEGION
    
    -- Count number of assets in stock.
    local Nassets=0    
    if legion:IsAirwing() then
      Nassets=legion:CountAssetsWithPayloadsInStock(Mission.payloads, {Mission.type}, Attributes)
    else    
      Nassets=legion:CountAssets(true, {Mission.type}, Attributes) --Could also specify the attribute if Air or Ground mission.
    end    
    
    -- Has it assets that can?
    if Nassets>0  and false then        
      
      -- Get coordinate of the target.
      local coord=Mission:GetTargetCoordinate()
      
      if coord then
      
        -- Distance from legion to target.
        local distance=UTILS.MetersToNM(coord:Get2DDistance(legion:GetCoordinate()))
        
        -- Round: 55 NM ==> 5.5 ==> 6, 63 NM ==> 6.3 ==> 6
        local dist=UTILS.Round(distance/10, 0)
        
        -- Debug info.
        self:T(self.lid..string.format("Got legion %s with Nassets=%d and dist=%.1f NM, rounded=%.1f", legion.alias, Nassets, distance, dist))
      
        -- Add legion to table of legions that can.
        table.insert(legions, {airwing=legion, distance=distance, dist=dist, targetcoord=coord, nassets=Nassets})
        
      end
      
    end
    
    -- Add legion if it can provide at least 1 asset.    
    if Nassets>0 then
      table.insert(legions, legion)
    end
            
  end
  
  return legions
end

--- Get assets on given mission or missions.
-- @param #COMMANDER self
-- @param #table MissionTypes Types on mission to be checked. Default all.
-- @return #table Assets on pending requests.
function COMMANDER:GetAssetsOnMission(MissionTypes)

  local assets={}

  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    -- Check if this mission type is requested.
    if AUFTRAG.CheckMissionType(mission.type, MissionTypes) then

      for _,_asset in pairs(mission.assets or {}) do
        local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
          table.insert(assets, asset)
       end
    end
  end

  return assets
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Fleet Warehouse.
--
-- **Main Features:**
--
--    * Manage flotillas
--    * Carry out ARTY and PATROLZONE missions (AUFTRAG)
-- 
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/Fleet).
-- 
-- ===
--
-- ### Author: **funkyfranky**
-- 
-- ===
-- @module Ops.Fleet
-- @image OPS_Fleet.png


--- FLEET class.
-- @type FLEET
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity of output.
-- @field Core.Set#SET_ZONE retreatZones Retreat zone set.
-- @field #boolean pathfinding Set pathfinding on for all spawned navy groups.
-- @extends Ops.Legion#LEGION

--- *A fleet of British ships at war are the best negotiators.* -- Horatio Nelson
--
-- ===
--
-- # The FLEET Concept
--
-- A FLEET consists of one or multiple FLOTILLAs. These flotillas "live" in a WAREHOUSE that has a phyiscal struction (STATIC or UNIT) and can be captured or destroyed.
-- 
-- # Basic Setup
-- 
-- A new `FLEET` object can be created with the @{#FLEET.New}(`WarehouseName`, `FleetName`) function, where `WarehouseName` is the name of the static or unit object hosting the fleet
-- and `FleetName` is the name you want to give the fleet. This must be *unique*!
-- 
--     myFleet=FLEET:New("myWarehouseName", "1st Fleet")
--     myFleet:SetPortZone(ZonePort1stFleet)
--     myFleet:Start()
--     
-- A fleet needs a *port zone*, which is set via the @{#FLEET.SetPortZone}(`PortZone`) function. This is the zone where the naval assets are spawned and return to.
-- 
-- Finally, the fleet needs to be started using the @{#FLEET.Start}() function. If the fleet is not started, it will not process any requests.
-- 
-- ## Adding Flotillas
-- 
-- Flotillas can be added via the @{#FLEET.AddFlotilla}(`Flotilla`) function. See @{Ops.Flotilla#FLOTILLA} for how to create a flotilla.
-- 
--     myFleet:AddFlotilla(FlotillaTiconderoga)
--     myFleet:AddFlotilla(FlotillaPerry)
--     
--
--
-- @field #FLEET
FLEET = {
  ClassName       = "FLEET",
  verbose         =       0,
  pathfinding     =   false,
}

--- Supply Zone.
-- @type FLEET.SupplyZone
-- @field Core.Zone#ZONE zone The zone.
-- @field Ops.Auftrag#AUFTRAG mission Mission assigned to supply ammo or fuel.
-- @field #boolean markerOn If `true`, marker is on.
-- @field Wrapper.Marker#MARKER marker F10 marker.

--- FLEET class version.
-- @field #string version
FLEET.version="0.0.1"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Add routes?
-- DONE: Add weapon range.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new FLEET class object.
-- @param #FLEET self
-- @param #string WarehouseName Name of the warehouse STATIC or UNIT object representing the warehouse.
-- @param #string FleetName Name of the fleet.
-- @return #FLEET self
function FLEET:New(WarehouseName, FleetName)

  -- Inherit everything from LEGION class.
  local self=BASE:Inherit(self, LEGION:New(WarehouseName, FleetName)) -- #FLEET

  -- Nil check.
  if not self then
    BASE:E(string.format("ERROR: Could not find warehouse %s!", WarehouseName))
    return nil
  end

  -- Set some string id for output to DCS.log file.
  self.lid=string.format("FLEET %s | ", self.alias)
  
  -- Defaults
  self:SetRetreatZones()
  
  -- Turn ship into NAVYGROUP.
  if self:IsShip() then
    local wh=self.warehouse --Wrapper.Unit#UNIT
    local group=wh:GetGroup()
    self.warehouseOpsGroup=NAVYGROUP:New(group) --Ops.NavyGroup#NAVYGROUP
    self.warehouseOpsElement=self.warehouseOpsGroup:GetElementByName(wh:GetName())
  end


  -- Add FSM transitions.
  --                 From State  -->   Event         -->      To State
  self:AddTransition("*",             "NavyOnMission",        "*")           -- An NAVYGROUP was send on a Mission (AUFTRAG).

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the FLEET. Initializes parameters and starts event handlers.
  -- @function [parent=#FLEET] Start
  -- @param #FLEET self

  --- Triggers the FSM event "Start" after a delay. Starts the FLEET. Initializes parameters and starts event handlers.
  -- @function [parent=#FLEET] __Start
  -- @param #FLEET self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Stop". Stops the FLEET and all its event handlers.
  -- @param #FLEET self

  --- Triggers the FSM event "Stop" after a delay. Stops the FLEET and all its event handlers.
  -- @function [parent=#FLEET] __Stop
  -- @param #FLEET self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "NavyOnMission".
  -- @function [parent=#FLEET] NavyOnMission
  -- @param #FLEET self
  -- @param Ops.NavyGroup#NAVYGROUP ArmyGroup The NAVYGROUP on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- Triggers the FSM event "NavyOnMission" after a delay.
  -- @function [parent=#FLEET] __NavyOnMission
  -- @param #FLEET self
  -- @param #number delay Delay in seconds.
  -- @param Ops.NavyGroup#NAVYGROUP ArmyGroup The NAVYGROUP on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "NavyOnMission" event.
  -- @function [parent=#FLEET] OnAfterNavyOnMission
  -- @param #FLEET self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.NavyGroup#NAVYGROUP NavyGroup The NAVYGROUP on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Add a flotilla to the fleet.
-- @param #FLEET self
-- @param Ops.Flotilla#FLOTILLA Flotilla The flotilla object.
-- @return #FLEET self
function FLEET:AddFlotilla(Flotilla)

  -- Add flotilla to fleet.
  table.insert(self.cohorts, Flotilla)

  -- Add assets to flotilla.
  self:AddAssetToFlotilla(Flotilla, Flotilla.Ngroups)

  -- Set fleet of flotilla.
  Flotilla:SetFleet(self)

  -- Start flotilla.
  if Flotilla:IsStopped() then
    Flotilla:Start()
  end

  return self
end

--- Add asset group(s) to flotilla.
-- @param #FLEET self
-- @param Ops.Flotilla#FLOTILLA Flotilla The flotilla object.
-- @param #number Nassets Number of asset groups to add.
-- @return #FLEET self
function FLEET:AddAssetToFlotilla(Flotilla, Nassets)

  if Flotilla then

    -- Get the template group of the flotilla.
    local Group=GROUP:FindByName(Flotilla.templatename)

    if Group then

      -- Debug text.
      local text=string.format("Adding asset %s to flotilla %s", Group:GetName(), Flotilla.name)
      self:T(self.lid..text)

      -- Add assets to airwing warehouse.
      self:AddAsset(Group, Nassets, nil, nil, nil, nil, Flotilla.skill, Flotilla.livery, Flotilla.name)

    else
      self:E(self.lid.."ERROR: Group does not exist!")
    end

  else
    self:E(self.lid.."ERROR: Flotilla does not exit!")
  end

  return self
end

--- Set pathfinding for all spawned naval groups.
-- @param #FLEET self
-- @param #boolean Switch If `true`, pathfinding is used.
-- @return #FLEET self
function FLEET:SetPathfinding(Switch)
  self.pathfinding=Switch
  return self
end

--- Define a set of retreat zones.
-- @param #FLEET self
-- @param Core.Set#SET_ZONE RetreatZoneSet Set of retreat zones.
-- @return #FLEET self
function FLEET:SetRetreatZones(RetreatZoneSet)
  self.retreatZones=RetreatZoneSet or SET_ZONE:New()
  return self
end

--- Add a retreat zone.
-- @param #FLEET self
-- @param Core.Zone#ZONE RetreatZone Retreat zone.
-- @return #FLEET self
function FLEET:AddRetreatZone(RetreatZone)
  self.retreatZones:AddZone(RetreatZone)
  return self
end

--- Get retreat zones.
-- @param #FLEET self
-- @return Core.Set#SET_ZONE Set of retreat zones.
function FLEET:GetRetreatZones()
  return self.retreatZones
end

--- Get flotilla by name.
-- @param #FLEET self
-- @param #string FlotillaName Name of the flotilla.
-- @return Ops.Flotilla#FLOTILLA The Flotilla object.
function FLEET:GetFlotilla(FlotillaName)
  local flotilla=self:_GetCohort(FlotillaName)
  return flotilla
end

--- Get flotilla of an asset.
-- @param #FLEET self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The flotilla asset.
-- @return Ops.Flotilla#FLOTILLA The flotilla object.
function FLEET:GetFlotillaOfAsset(Asset)
  local flotilla=self:GetFlotilla(Asset.squadname)
  return flotilla
end

--- Remove asset from flotilla.
-- @param #FLEET self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The flotilla asset.
function FLEET:RemoveAssetFromFlotilla(Asset)
  local flotilla=self:GetFlotillaOfAsset(Asset)
  if flotilla then
    flotilla:DelAsset(Asset)
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Start FLEET FSM.
-- @param #FLEET self
function FLEET:onafterStart(From, Event, To)

  -- Start parent Warehouse.
  self:GetParent(self, FLEET).onafterStart(self, From, Event, To)

  -- Info.
  self:I(self.lid..string.format("Starting FLEET v%s", FLEET.version))

end

--- Update status.
-- @param #FLEET self
function FLEET:onafterStatus(From, Event, To)

  -- Status of parent Warehouse.
  self:GetParent(self).onafterStatus(self, From, Event, To)

  -- FSM state.
  local fsmstate=self:GetState()
  
  ----------------
  -- Transport ---
  ----------------
  
  self:CheckTransportQueue()

  --------------
  -- Mission ---
  --------------

  -- Check if any missions should be cancelled.
  self:CheckMissionQueue()

  -----------
  -- Info ---
  -----------    

  -- Display tactival overview.
  self:_TacticalOverview()

  -- General info:
  if self.verbose>=1 then

    -- Count missions not over yet.
    local Nmissions=self:CountMissionsInQueue()

    -- Asset count.
    local Npq, Np, Nq=self:CountAssetsOnMission()

    -- Asset string.
    local assets=string.format("%d [OnMission: Total=%d, Active=%d, Queued=%d]", self:CountAssets(), Npq, Np, Nq)

    -- Output.
    local text=string.format("%s: Missions=%d, Flotillas=%d, Assets=%s", fsmstate, Nmissions, #self.cohorts, assets)
    self:I(self.lid..text)
  end

  ------------------
  -- Mission Info --
  ------------------
  if self.verbose>=2 then
    local text=string.format("Missions Total=%d:", #self.missionqueue)
    for i,_mission in pairs(self.missionqueue) do
      local mission=_mission --Ops.Auftrag#AUFTRAG

      local prio=string.format("%d/%s", mission.prio, tostring(mission.importance)) ; if mission.urgent then prio=prio.." (!)" end
      local assets=string.format("%d/%d", mission:CountOpsGroups(), mission.Nassets or 0)
      local target=string.format("%d/%d Damage=%.1f", mission:CountMissionTargets(), mission:GetTargetInitialNumber(), mission:GetTargetDamage())

      text=text..string.format("\n[%d] %s %s: Status=%s, Prio=%s, Assets=%s, Targets=%s", i, mission.name, mission.type, mission.status, prio, assets, target)
    end
    self:I(self.lid..text)
  end

  --------------------
  -- Transport Info --
  --------------------
  if self.verbose>=2 then
    local text=string.format("Transports Total=%d:", #self.transportqueue)
    for i,_transport in pairs(self.transportqueue) do
      local transport=_transport --Ops.OpsTransport#OPSTRANSPORT

      local prio=string.format("%d/%s", transport.prio, tostring(transport.importance)) ; if transport.urgent then prio=prio.." (!)" end
      local carriers=string.format("Ncargo=%d/%d, Ncarriers=%d", transport.Ncargo, transport.Ndelivered, transport.Ncarrier)

      text=text..string.format("\n[%d] UID=%d: Status=%s, Prio=%s, Cargo: %s", i, transport.uid, transport:GetState(), prio, carriers)
    end
    self:I(self.lid..text)
  end

  -------------------
  -- Flotilla Info --
  -------------------
  if self.verbose>=3 then
    local text="Flotillas:"
    for i,_flotilla in pairs(self.cohorts) do
      local flotilla=_flotilla --Ops.Flotilla#FLOTILLA

      local callsign=flotilla.callsignName and UTILS.GetCallsignName(flotilla.callsignName) or "N/A"
      local modex=flotilla.modex and flotilla.modex or -1
      local skill=flotilla.skill and tostring(flotilla.skill) or "N/A"

      -- Flotilla text.
      text=text..string.format("\n* %s %s: %s*%d/%d, Callsign=%s, Modex=%d, Skill=%s", flotilla.name, flotilla:GetState(), flotilla.aircrafttype, flotilla:CountAssets(true), #flotilla.assets, callsign, modex, skill)
    end
    self:I(self.lid..text)
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "NavyOnMission".
-- @param #FLEET self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.ArmyGroup#ARMYGROUP ArmyGroup Ops army group on mission.
-- @param Ops.Auftrag#AUFTRAG Mission The requested mission.
function FLEET:onafterNavyOnMission(From, Event, To, NavyGroup, Mission)
  -- Debug info.
  self:T(self.lid..string.format("Group %s on %s mission %s", NavyGroup:GetName(), Mission:GetType(), Mission:GetName()))  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **OPS** - Air Traffic Control for AI and human players.
-- 
-- **Main Features:**
--
--    * Manage aircraft departure and arrival
--    * Handles AI and human players
--    * Limit number of AI groups taxiing, taking off and landing simultaneously
--    * Immersive voice overs via SRS text-to-speech
--    * Define holding patterns for airdromes
--     
-- ===
--
-- ## Example Missions:
--
-- Demo missions: None
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ===
-- @module OPS.FlightControl
-- @image OPS_FlightControl.png


--- FLIGHTCONTROL class.
-- @type FLIGHTCONTROL
-- @field #string ClassName Name of the class.
-- @field #boolean verbose Verbosity level.
-- @field #string theatre The DCS map used in the mission.
-- @field #string lid Class id string for output to DCS log file.
-- @field #string airbasename Name of airbase.
-- @field #string alias Radio alias, e.g. "Batumi Tower".
-- @field #number airbasetype Type of airbase.
-- @field Wrapper.Airbase#AIRBASE airbase Airbase object.
-- @field Core.Zone#ZONE zoneAirbase Zone around the airbase.
-- @field #table parking Parking spots table.
-- @field #table flights All flights table.
-- @field #table clients Table with all clients spawning at this airbase.
-- @field Ops.ATIS#ATIS atis ATIS object.
-- @field #number frequency ATC radio frequency in MHz.
-- @field #number modulation ATC radio modulation, *e.g.* `radio.modulation.AM`.
-- @field #number NlandingTot Max number of aircraft groups in the landing pattern.
-- @field #number NlandingTakeoff Max number of groups taking off to allow landing clearance.
-- @field #number NtaxiTot Max number of aircraft groups taxiing to runway for takeoff.
-- @field #boolean NtaxiInbound Include inbound taxiing groups.
-- @field #number NtaxiLanding Max number of aircraft landing for groups taxiing to runway for takeoff.
-- @field #number dTlanding Time interval in seconds between landing clearance.
-- @field #number Tlanding Time stamp (abs.) when last flight got landing clearance.
-- @field #number Nparkingspots Total number of parking spots.
-- @field Core.Spawn#SPAWN parkingGuard Parking guard spawner.
-- @field #table holdingpatterns Holding points.
-- @field #number hpcounter Counter for holding zones.
-- @field Sound.SRS#MSRSQUEUE msrsqueue Queue for TTS transmissions using MSRS class.
-- @field Sound.SRS#MSRS msrsTower Moose SRS wrapper.
-- @field Sound.SRS#MSRS msrsPilot Moose SRS wrapper.
-- @field #number Tlastmessage Time stamp (abs.) of last radio transmission.
-- @field #number dTmessage Time interval between messages.
-- @field #boolean markPatterns If `true`, park holding pattern.
-- @field #number speedLimitTaxi Taxi speed limit in m/s.
-- @field #number runwaydestroyed Time stamp (abs), when runway was destroyed. If `nil`, runway is operational.
-- @field #number runwayrepairtime Time in seconds until runway will be repaired after it was destroyed. Default is 3600 sec (one hour).
-- @field #boolean markerParking If `true`, occupied parking spots are marked.
-- @field #boolean nosubs If `true`, SRS TTS is without subtitles.
-- @field #number Nplayers Number of human players. Updated at each StatusUpdate call.
-- @field #boolean radioOnlyIfPlayers Activate to limit transmissions only if players are active at the airbase.
-- @extends Core.Fsm#FSM

--- **Ground Control**: Airliner X, Good news, you are clear to taxi to the active.
--  **Pilot**: Roger, What's the bad news?
--  **Ground Control**: No bad news at the moment, but you probably want to get gone before I find any.
--
-- ===
--
-- # The FLIGHTCONTROL Concept
-- 
-- This class implements an ATC for human and AI controlled aircraft. It gives permission for take-off and landing based on a sophisticated queueing system.
-- Therefore, it solves (or reduces) a lot of common problems with the DCS implementation.
-- 
-- You might be familiar with the `AIRBOSS` class. This class is the analogue for land based airfields. One major difference is that no pre-recorded sound files are 
-- necessary. The radio transmissions use the SRS text-to-speech feature.
-- 
-- ## Prerequisites
-- 
-- * SRS is used for radio communications
-- 
-- ## Limitations
-- 
-- Some (DCS) limitations you should be aware of:
-- 
-- * As soon as AI aircraft taxi or land, we completely loose control. All is governed by the internal DCS AI logic.
-- * We have no control over the active runway or which runway is used by the AI if there are multiple.
-- * Only one player/client per group as we can create menus only for a group and not for a specific unit.
-- * Only FLIGHTGROUPS are controlled. This means some older classes, *e.g.* RAT are not supported (yet).
-- * So far only airdromes are handled, *i.e.* no FARPs or ships.
-- * Helicopters are not treated differently from fixed wing aircraft until now.
-- * The active runway can only be determined by the wind direction. So at least set a very light wind speed in your mission.
-- 
-- # Basic Usage
-- 
-- A flight control for a given airdrome can be created with the @{#FLIGHTCONTROL.New}(*AirbaseName, Frequency, Modulation, PathToSRS*) function. You need to specify the name of the airbase, the 
-- tower radio frequency, its modulation and the path, where SRS is located on the machine that is running this mission.
-- 
-- For the FC to be operating, it needs to be started with the @{#FLIGHTCONTROL.Start}() function.
-- 
-- ## Simple Script
-- 
-- The simplest script looks like
-- 
--      local FC_BATUMI=FLIGHTCONTROL:New(AIRBASE.Caucasus.Batumi, 251, nil, "D:\\SomeDirectory\\_SRS")
--      FC_BATUMI:Start()
-- 
-- This will start the FC for at the Batumi airbase with tower frequency 251 MHz AM. SRS needs to be in the given directory.
-- 
-- Like this, a default holding pattern (see below) is parallel to the direction of the active runway.
-- 
-- # Holding Patterns
-- 
-- Holding pattern are air spaces where incoming aircraft are guided to and have to hold until they get landing clearance.
-- 
-- You can add a holding pattern with the @{#FLIGHTCONTROL.AddHoldingPattern}(*ArrivalZone, Heading, Length, FlightlevelMin, FlightlevelMax, Prio*) function, where
-- 
-- * `ArrivalZone` is the zone where the aircraft enter the pattern.
-- * `Heading` is the direction into which the aircraft have to fly from the arrival zone.
-- * `Length` is the length of the pattern.
-- * `FlightLevelMin` is the lowest altitude at which aircraft can hold.
-- * `FlightLevelMax` is the highest altitude at which aircraft can hold.
-- * `Prio` is the priority of this holding stacks. If multiple patterns are defined, patterns with higher prio will be filled first.
-- 
-- # Parking Guard
-- 
-- A "parking guard" is a group or static object, that is spawned in front of parking aircraft. This is useful to stop AI groups from taxiing if they are spawned with hot engines.
-- It is also handy to forbid human players to taxi until they ask for clearance.
-- 
-- You can activate the parking guard with the @{#FLIGHTCONTROL.SetParkingGuard}(*GroupName*) function, where the parameter `GroupName` is the name of a late activated template group.
-- This should consist of only *one* unit, *e.g.* a single infantry soldier.
-- 
-- You can also use static objects as parking guards with the @{#FLIGHTCONTROL.SetParkingGuardStatic}(*StaticName*), where the parameter `StaticName` is the name of a static object placed
-- somewhere in the mission editor.
-- 
-- # Limits for Inbound and Outbound Flights
-- 
-- You can define limits on how many aircraft are simultaneously landing and taking off. This avoids (DCS) problems where taxiing aircraft cause a "traffic jam" on the taxi way(s)
-- and bring the whole airbase effectively to a stand still.
-- 
-- ## Landing Limits
-- 
-- The number of groups getting landing clearance can be set with the @{#FLIGHTCONTROL.SetLimitLanding}(*Nlanding, Ntakeoff*) function. 
-- The first parameter, `Nlanding`, defines how many groups get clearance simultaneously.
-- 
-- The second parameter, `Ntakeoff`, sets a limit on how many flights can take off whilst inbound flights still get clearance. By default, this is set to zero because the runway can only be used for takeoff *or*
-- landing. So if you have a flight taking off, inbound fights will have to wait until the runway is clear. 
-- If you have an airport with more than one runway, *e.g.* Nellis AFB, you can allow simultanious landings and takeoffs by setting this number greater zero.
-- 
-- The time interval between clerances can be set with the @{#FLIGHTCONTROL.SetLandingInterval}(`dt`) function, where the parameter `dt` specifies the time interval in seconds before 
-- the next flight get clearance. This only has an effect if `Nlanding` is greater than one.
-- 
-- ## Taxiing/Takeoff Limits
-- 
-- The number of AI flight groups getting clearance to taxi to the runway can be set with the @{#FLIGHTCONTROL.SetLimitTaxi}(*Nlanding, Ntakeoff*) function. 
-- The first parameter, `Ntaxi`, defines how many groups are allowed to taxi to the runway simultaneously. Note that once the AI starts to taxi, we loose complete control over it.
-- They will follow their internal logic to get the the runway and take off. Therefore, giving clearance to taxi is equivalent to giving them clearance for takeoff.
-- 
-- By default, the parameter only counts the number of flights taxiing *to* the runway. If you set the second parameter, `IncludeInbound`, to `true`, this will also count the flights
-- that are taxiing to their parking spot(s) after they landed.
-- 
-- The third parameter, `Nlanding`, defines how many aircraft can land whilst outbound fights still get taxi/takeoff clearance. By default, this is set to zero because the runway
-- can only be used for takeoff *or* landing. If you have an airport with more than one runway, *e.g.* Nellis AFB, you can allow aircraft to taxi to the runway while other flights are landing
-- by setting this number greater zero.  
--
-- Note that the limits here are only affecting **AI** aircraft groups. *Human players* are assumed to be a lot more well behaved and capable as they are able to taxi around obstacles, *e.g.*
-- other aircraft etc. Therefore, players will get taxi clearance independent of the number of inbound and/or outbound flights. Players will, however, still need to ask for takeoff clearance once
-- they are holding short of the runway.
-- 
-- # Speeding Violations
-- 
-- You can set a speed limit for taxiing players with the @{#FLIGHTCONTROL.SetSpeedLimitTaxi}(*SpeedLimit*) function, where the parameter `SpeedLimit` is the max allowed speed in knots.
-- If players taxi faster, they will get a radio message. Additionally, the FSM event `PlayerSpeeding` is triggered and can be captured with the `OnAfterPlayerSpeeding` function.
-- For example, this can be used to kick players that do not behave well.
-- 
-- # Runway Destroyed
-- 
-- Once a runway is damaged, DCS AI will stop taxiing. Therefore, this class monitors if a runway is destroyed. If this is the case, all AI taxi and landing clearances will be suspended for
-- one hour. This is the hard coded time in DCS until the runway becomes operational again. If that ever changes, you can manually set the repair time with the 
-- @{#FLIGHTCONTROL.SetRunwayRepairtime} function.
-- 
-- Note that human players we still get taxi, takeoff and landing clearances.
-- 
-- If the runway is destroyed, the FSM event `RunwayDestroyed` is triggered and can be captured with the @{#FLIGHTCONTROL.OnAfterRunwayDestroyed} function.
-- 
-- If the runway is repaired, the FSM event `RunwayRepaired` is triggered and can be captured with the @{#FLIGHTCONTROL.OnAfterRunwayRepaired} function.
-- 
-- # SRS
-- 
-- SRS text-to-speech is used to send radio messages from the tower and pilots.
-- 
-- ## Tower
-- 
-- You can set the options for the tower SRS voice with the @{#FLIGHTCONTROL.SetSRSTower}() function.
-- 
-- ## Pilot
-- 
-- You can set the options for the pilot SRS voice with the @{#FLIGHTCONTROL.SetSRSPilot}() function.
-- 
-- # Runways
-- 
-- First note, that we have extremely limited control over which runway the DCS AI groups use. The only parameter we can adjust is the direction of the wind. In many cases, the AI will try to takeoff and land
-- against the wind, which therefore determines the active runway. There are, however, cases where this does not hold true. For example, at Nellis AFB the runway for takeoff is `03L` while the runway for
-- landing is `21L`.
-- 
-- By default, the runways for landing and takeoff are determined from the wind direction as described above. For cases where this gives wrong results, you can set the active runways manually. This is
-- done via @{Wrapper.Airbase#AIRBASE} class.
-- 
-- More specifically, you can use the @{Wrapper.Airbase#AIRBASE.SetActiveRunwayLanding} function to set the landing runway and the @{Wrapper.Airbase#AIRBASE.SetActiveRunwayTakeoff} function to set
-- the runway for takeoff.
-- 
-- ## Example for Nellis AFB
-- 
-- For Nellis, you can use:
-- 
--     -- Nellis AFB.
--     local Nellis=AIRBASE:FindByName(AIRBASE.Nevada.Nellis_AFB)
--     Nellis:SetActiveRunwayLanding("21L")
--     Nellis:SetActiveRunwayTakeoff("03L")
-- 
-- # DCS ATC
-- 
-- You can disable the DCS ATC with the @{Wrapper.Airbase#AIRBASE.SetRadioSilentMode}(*true*). This does not remove the DCS ATC airbase from the F10 menu but makes the ATC unresponsive.
-- 
-- 
-- # Examples
-- 
-- In this section, you find examples for different airdromes.
-- 
-- ## Nellis AFB
--     
--     -- Create a new FLIGHTCONTROL object at Nellis AFB. The tower frequency is 251 MHz AM. Path to SRS has to be adjusted. 
--     local atcNellis=FLIGHTCONTROL:New(AIRBASE.Nevada.Nellis_AFB, 251, nil, "D:\\My SRS Directory")
--     -- Set a parking guard from a static named "Static Generator F Template".
--     atcNellis:SetParkingGuardStatic("Static Generator F Template")
--     -- Set taxi speed limit to 25 knots.
--     atcNellis:SetSpeedLimitTaxi(25)
--     -- Set that max 3 groups are allowed to taxi simultaneously.
--     atcNellis:SetLimitTaxi(3, false, 1)
--     -- Set that max 2 groups are allowd to land simultaneously and unlimited number (99) groups can land, while other groups are taking off.
--     atcNellis:SetLimitLanding(2, 99)
--     -- Use Google for text-to-speech.
--     atcNellis:SetSRSTower(nil, nil, "en-AU-Standard-A", nil, nil, "D:\\Path To Google\\GoogleCredentials.json")
--     atcNellis:SetSRSPilot(nil, nil, "en-US-Wavenet-I",  nil, nil, "D:\\Path To Google\\GoogleCredentials.json")
--     -- Define two holding zones.
--     atcNellis:AddHoldingPattern(ZONE:New("Nellis Holding Alpha"), 030, 15, 6, 10, 10)
--     atcNellis:AddHoldingPattern(ZONE:New("Nellis Holding Bravo"), 090, 15, 6, 10, 20)
--     -- Start the ATC.
--     atcNellis:Start()
-- 
-- @field #FLIGHTCONTROL
FLIGHTCONTROL = {
  ClassName      = "FLIGHTCONTROL",
  verbose        =     0,
  lid            =   nil,
  theatre        =   nil,
  airbasename    =   nil,
  airbase        =   nil,
  airbasetype    =   nil,
  zoneAirbase    =   nil,
  parking        =    {},
  runways        =    {},
  flights        =    {},
  clients        =    {},
  atis           =   nil,
  Nlanding         = nil,
  dTlanding        = nil,
  Nparkingspots    = nil,
  holdingpatterns  =  {},
  hpcounter        =   0,
  nosubs         =  false,
  Nplayers       =     0,
}

--- Holding point. Contains holding stacks.
-- @type FLIGHTCONTROL.HoldingPattern
-- @field Core.Zone#ZONE arrivalzone Zone where aircraft should arrive.
-- @field #number uid Unique ID.
-- @field #string name Name of the zone, which is <zonename>-<uid>.
-- @field Core.Point#COORDINATE pos0 First position of racetrack holding pattern.
-- @field Core.Point#COORDINATE pos1 Second position of racetrack holding pattern.
-- @field #number angelsmin Smallest holding altitude in angels.
-- @field #number angelsmax Largest holding alitude in angels.
-- @field #table stacks Holding stacks.
-- @field #number markArrival Marker ID of the arrival zone.
-- @field #number markArrow Marker ID of the direction.

--- Holding stack.
-- @type FLIGHTCONTROL.HoldingStack
-- @field Ops.FlightGroup#FLIGHTGROUP flightgroup Flight group of this stack.
-- @field #number angels Holding altitude in Angels.
-- @field Core.Point#COORDINATE pos0 First position of racetrack holding pattern.
-- @field Core.Point#COORDINATE pos1 Second position of racetrack holding pattern.
-- @field #number heading Heading.


--- Parking spot data.
-- @type FLIGHTCONTROL.ParkingSpot
-- @field Wrapper.Group#GROUP ParkingGuard Parking guard for this spot.
-- @extends Wrapper.Airbase#AIRBASE.ParkingSpot

--- Flight status.
-- @type FLIGHTCONTROL.FlightStatus
-- @field #string UNKNOWN Flight is unknown.
-- @field #string INBOUND Flight is inbound.
-- @field #string HOLDING Flight is holding.
-- @field #string LANDING Flight is landing.
-- @field #string TAXIINB Flight is taxiing to parking area.
-- @field #string ARRIVED Flight arrived at parking spot.
-- @field #string TAXIOUT Flight is taxiing to runway for takeoff.
-- @field #string READYTX Flight is ready to taxi.
-- @field #string READYTO Flight is ready for takeoff.
-- @field #string TAKEOFF Flight is taking off.
FLIGHTCONTROL.FlightStatus={
  UNKNOWN="Unknown",
  PARKING="Parking",
  READYTX="Ready To Taxi",
  TAXIOUT="Taxi To Runway",
  READYTO="Ready For Takeoff",
  TAKEOFF="Takeoff",          
  INBOUND="Inbound",
  HOLDING="Holding",
  LANDING="Landing",
  TAXIINB="Taxi To Parking",
  ARRIVED="Arrived",
}

--- FlightControl class version.
-- @field #string version
FLIGHTCONTROL.version="0.7.7"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list

-- TODO: Switch to enable/disable AI messages.
-- TODO: Talk me down option.
-- TODO: Check runways and clean up.
-- TODO: Add FARPS?
-- DONE: Improve ATC TTS messages.
-- DONE: ATIS option.
-- DONE: Runway destroyed.
-- DONE: Accept and forbit parking spots. DONE via AIRBASE black/white lists and airwing features.
-- DONE: Support airwings. Dont give clearance for Alert5 or if mission has not started.
-- DONE: Define holding zone.
-- DONE: Basic ATC voice overs.
-- DONE: Add SRS TTS.
-- DONE: Add parking guard.
-- DONE: Interface with FLIGHTGROUP.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new FLIGHTCONTROL class object for an associated airbase.
-- @param #FLIGHTCONTROL self
-- @param #string AirbaseName Name of the airbase.
-- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. Can also be given as a `#table` of multiple frequencies.
-- @param #number Modulation Radio modulation: 0=AM (default), 1=FM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. Can also be given as a `#table` of multiple modulations.
-- @param #string PathToSRS Path to the directory, where SRS is located.
-- @param #number Port Port of SRS Server, defaults to 5002
-- @param #string GoogleKey Path to the Google JSON-Key.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS, Port, GoogleKey)

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #FLIGHTCONTROL
  
  -- Try to get the airbase.
  self.airbase=AIRBASE:FindByName(AirbaseName)
  
  -- Name of the airbase.
  self.airbasename=AirbaseName  
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("FLIGHTCONTROL %s | ", AirbaseName)  
  
  -- Check if the airbase exists.
  if not self.airbase then
    self:E(string.format("ERROR: Could not find airbase %s!", tostring(AirbaseName)))
    return nil
  end
  -- Check if airbase is an airdrome.
  if self.airbase:GetAirbaseCategory()~=Airbase.Category.AIRDROME then
    self:E(string.format("ERROR: Airbase %s is not an AIRDROME! Script does not handle FARPS or ships.", tostring(AirbaseName)))
    return nil
  end
  
  -- Airbase category airdrome, FARP, SHIP.
  self.airbasetype=self.airbase:GetAirbaseCategory()
  
  -- Current map.
  self.theatre=env.mission.theatre    
  
  -- 5 NM zone around the airbase.
  self.zoneAirbase=ZONE_RADIUS:New("FC", self:GetCoordinate():GetVec2(), UTILS.NMToMeters(5))
  
  -- Add backup holding pattern.
  self:_AddHoldingPatternBackup()

  -- Set alias.
  self.alias=self.airbasename.." Tower"
  
  -- Defaults:
  self:SetLimitLanding(2, 0)
  self:SetLimitTaxi(2, false, 0)
  self:SetLandingInterval()
  self:SetFrequency(Frequency, Modulation)
  self:SetMarkHoldingPattern(true)
  self:SetRunwayRepairtime()
  self.nosubs = false
  
  -- Set Callsign Options
  self:SetCallSignOptions(true,true)
  
  -- Init msrs queue.
  self.msrsqueue=MSRSQUEUE:New(self.alias)
  
  -- Set that transmission is only if alive players on the server.
  self:SetTransmitOnlyWithPlayers(true)
  
  -- Init msrs bases
  local path = PathToSRS or MSRS.path
  local port = Port or MSRS.port or 5002
  
  -- Set SRS Port
  self:SetSRSPort(port)
  
  -- SRS for Tower.
  self.msrsTower=MSRS:New(path, Frequency, Modulation)
  self.msrsTower:SetPort(port)
  if GoogleKey then
    self.msrsTower:SetProviderOptionsGoogle(GoogleKey,GoogleKey)
    self.msrsTower:SetProvider(MSRS.Provider.GOOGLE)
  end  
  self.msrsTower:SetCoordinate(self:GetCoordinate())
  self:SetSRSTower()
  
  -- SRS for Pilot.
  self.msrsPilot=MSRS:New(PathToSRS, Frequency, Modulation)
  self.msrsPilot:SetPort(self.Port)
  if GoogleKey then
    self.msrsPilot:SetProviderOptionsGoogle(GoogleKey,GoogleKey)
    self.msrsPilot:SetProvider(MSRS.Provider.GOOGLE)
  end  
  self.msrsTower:SetCoordinate(self:GetCoordinate())
  self:SetSRSPilot()
  
  -- Wait at least 10 seconds after last radio message before calling the next status update.
  self.dTmessage=10
  
  -- Start State.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  --                 From State  -->   Event         -->     To State
  self:AddTransition("Stopped",       "Start",              "Running")     -- Start FSM.
  self:AddTransition("*",             "StatusUpdate",       "*")           -- Update status.
  
  self:AddTransition("*",             "PlayerKilledGuard",  "*")           -- Player killed parking guard
  self:AddTransition("*",             "PlayerSpeeding",     "*")           -- Player speeding on taxi way.

  self:AddTransition("*",             "RunwayDestroyed",    "*")           -- Runway of the airbase was destroyed.
  self:AddTransition("*",             "RunwayRepaired",     "*")           -- Runway of the airbase was repaired.
  
  self:AddTransition("*",             "Stop",               "Stopped")     -- Stop FSM.
  
  -- Add to data base.
  _DATABASE:AddFlightControl(self)


  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start".
  -- @function [parent=#FLIGHTCONTROL] Start
  -- @param #FLIGHTCONTROL self

  --- Triggers the FSM event "Start" after a delay.
  -- @function [parent=#FLIGHTCONTROL] __Start
  -- @param #FLIGHTCONTROL self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Stop".
  -- @function [parent=#FLIGHTCONTROL] Stop
  -- @param #FLIGHTCONTROL self

  --- Triggers the FSM event "Stop" after a delay.
  -- @function [parent=#FLIGHTCONTROL] __Stop
  -- @param #FLIGHTCONTROL self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "StatusUpdate".
  -- @function [parent=#FLIGHTCONTROL] StatusUpdate
  -- @param #FLIGHTCONTROL self

  --- Triggers the FSM event "StatusUpdate" after a delay.
  -- @function [parent=#FLIGHTCONTROL] __StatusUpdate
  -- @param #FLIGHTCONTROL self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "RunwayDestroyed".
  -- @function [parent=#FLIGHTCONTROL] RunwayDestroyed
  -- @param #FLIGHTCONTROL self

  --- Triggers the FSM event "RunwayDestroyed" after a delay.
  -- @function [parent=#FLIGHTCONTROL] __RunwayDestroyed
  -- @param #FLIGHTCONTROL self
  -- @param #number delay Delay in seconds.

  --- On after "RunwayDestroyed" event.
  -- @function [parent=#FLIGHTCONTROL] OnAfterRunwayDestroyed
  -- @param #FLIGHTCONTROL self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "RunwayRepaired".
  -- @function [parent=#FLIGHTCONTROL] RunwayRepaired
  -- @param #FLIGHTCONTROL self

  --- Triggers the FSM event "RunwayRepaired" after a delay.
  -- @function [parent=#FLIGHTCONTROL] __RunwayRepaired
  -- @param #FLIGHTCONTROL self
  -- @param #number delay Delay in seconds.

  --- On after "RunwayRepaired" event.
  -- @function [parent=#FLIGHTCONTROL] OnAfterRunwayRepaired
  -- @param #FLIGHTCONTROL self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "PlayerSpeeding".
  -- @function [parent=#FLIGHTCONTROL] PlayerSpeeding
  -- @param #FLIGHTCONTROL self
  -- @param Ops.FlightGroup#FLIGHTGROUP.PlayerData Player data.

  --- Triggers the FSM event "PlayerSpeeding" after a delay.
  -- @function [parent=#FLIGHTCONTROL] __PlayerSpeeding
  -- @param #FLIGHTCONTROL self
  -- @param #number delay Delay in seconds.
  -- @param Ops.FlightGroup#FLIGHTGROUP.PlayerData Player data.

  --- On after "PlayerSpeeding" event.
  -- @function [parent=#FLIGHTCONTROL] OnAfterPlayerSpeeding
  -- @param #FLIGHTCONTROL self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.FlightGroup#FLIGHTGROUP.PlayerData Player data.


  return self  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User API Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set verbosity level.
-- @param #FLIGHTCONTROL self
-- @param #number VerbosityLevel Level of output (higher=more). Default 0.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetVerbosity(VerbosityLevel)
  self.verbose=VerbosityLevel or 0
  return self
end

--- Limit radio transmissions only if human players are registered at the airbase.
-- This can be used to reduce TTS messages on heavy missions.
-- @param #FLIGHTCONTROL self
-- @param #boolean Switch If `true` or `nil` no transmission if there are no players. Use `false` enable TTS with no players.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetRadioOnlyIfPlayers(Switch)
  if Switch==nil or Switch==true then
    self.radioOnlyIfPlayers=true
  else
    self.radioOnlyIfPlayers=false
  end
  return self
end


--- Set whether to only transmit TTS messages if there are players on the server.
-- @param #FLIGHTCONTROL self
-- @param #boolean Switch If `true`, only send TTS messages if there are alive Players. If `false` or `nil`, transmission are done also if no players are on the server.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetTransmitOnlyWithPlayers(Switch)
  self.msrsqueue:SetTransmitOnlyWithPlayers(Switch)
  return self
end


--- Set subtitles to appear on SRS TTS messages.
-- @param #FLIGHTCONTROL self
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SwitchSubtitlesOn()
  self.nosubs = false
  return self
end

--- Set subtitles to appear on SRS TTS messages.
-- @param #FLIGHTCONTROL self
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SwitchSubtitlesOff()
  self.nosubs = true
  return self
end

--- Set the tower frequency.
-- @param #FLIGHTCONTROL self
-- @param #number Frequency Frequency in MHz. Default 305 MHz.
-- @param #number Modulation Modulation `radio.modulation.AM`=0, `radio.modulation.FM`=1. Default `radio.modulation.AM`.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetFrequency(Frequency, Modulation)

  self.frequency=Frequency or 305
  self.modulation=Modulation or radio.modulation.AM
  
  if self.msrsPilot then
    self.msrsPilot:SetFrequencies(Frequency)
    self.msrsPilot:SetModulations(Modulation)
  end

  if self.msrsTower then
    self.msrsTower:SetFrequencies(Frequency)
    self.msrsTower:SetModulations(Modulation)
  end
  
  return self
end

--- Set the SRS server port.
-- @param #FLIGHTCONTROL self
-- @param #number Port Port to be used. Defaults to 5002.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetSRSPort(Port)
  self.Port = Port or 5002
  return self
end

--- Set SRS options for a given MSRS object.
-- @param #FLIGHTCONTROL self
-- @param Sound.SRS#MSRS msrs Moose SRS object.
-- @param #string Gender Gender: "male" or "female" (default).
-- @param #string Culture Culture, e.g. "en-GB" (default).
-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`.
-- @param #number Volume Volume. Default 1.0.
-- @param #string Label Name under which SRS transmits.
-- @param #string PathToGoogleCredentials Path to google credentials json file.
-- @param #number Port Server port for SRS
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:_SetSRSOptions(msrs, Gender, Culture, Voice, Volume, Label, PathToGoogleCredentials, Port)

  -- Defaults:
  Gender=Gender or "female"
  Culture=Culture or "en-GB"
  Volume=Volume or 1.0

  if msrs then
    msrs:SetGender(Gender)
    msrs:SetCulture(Culture)
    msrs:SetVoice(Voice)
    msrs:SetVolume(Volume)
    msrs:SetLabel(Label)
    msrs:SetCoalition(self:GetCoalition())
    msrs:SetPort(Port or self.Port or 5002)
  end

  return self
end

--- Set SRS options for tower voice.
-- @param #FLIGHTCONTROL self
-- @param #string Gender Gender: "male" or "female" (default).
-- @param #string Culture Culture, e.g. "en-GB" (default).
-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`. See [Google Voices](https://cloud.google.com/text-to-speech/docs/voices).
-- @param #number Volume Volume. Default 1.0.
-- @param #string Label Name under which SRS transmits. Default `self.alias`.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetSRSTower(Gender, Culture, Voice, Volume, Label)

  if self.msrsTower then
    self:_SetSRSOptions(self.msrsTower, Gender or "female", Culture or "en-GB", Voice, Volume, Label or self.alias)
  end

  return self
end

--- Set SRS options for pilot voice.
-- @param #FLIGHTCONTROL self
-- @param #string Gender Gender: "male" (default) or "female".
-- @param #string Culture Culture, e.g. "en-US" (default).
-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`.
-- @param #number Volume Volume. Default 1.0.
-- @param #string Label Name under which SRS transmits. Default "Pilot".
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetSRSPilot(Gender, Culture, Voice, Volume, Label)

  if self.msrsPilot then
    self:_SetSRSOptions(self.msrsPilot, Gender or "male", Culture or "en-US", Voice, Volume, Label or "Pilot")
  end

  return self
end


--- Set the number of aircraft groups, that are allowed to land simultaneously.
-- Note that this restricts AI and human players.
-- 
-- By default, up to two groups get landing clearance. They are spaced out in time, i.e. after the first one got cleared, the second has to wait a bit.
-- This
-- 
-- By default, landing clearance is only given when **no** other flight is taking off. You can adjust this for airports with more than one runway or 
-- in cases where simultaneous takeoffs and landings are unproblematic. Note that only because there are multiple runways, it does not mean the AI uses them.
--  
-- @param #FLIGHTCONTROL self
-- @param #number Nlanding Max number of aircraft landing simultaneously. Default 2.
-- @param #number Ntakeoff Allowed number of aircraft taking off for groups to get landing clearance. Default 0. 
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetLimitLanding(Nlanding, Ntakeoff)

  self.NlandingTot=Nlanding or 2
  
  self.NlandingTakeoff=Ntakeoff or 0

  return self
end

--- Set time interval between landing clearance of groups.
-- @param #FLIGHTCONTROL self
-- @param #number dt Time interval in seconds. Default 180 sec (3 min).
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetLandingInterval(dt)

  self.dTlanding=dt or 180

  return self
end


--- Set the number of **AI** aircraft groups, that are allowed to taxi simultaneously.
-- If the limit is reached, other AI groups not get taxi clearance to taxi to the runway.
-- 
-- By default, this only counts the number of AI that taxi from their parking position to the runway.
-- You can also include inbound AI that taxi from the runway to their parking position.
-- This can be handy for problematic (usually smaller) airdromes, where there is only one taxiway inbound and outbound flights.
-- 
-- By default, AI will not get cleared for taxiing if at least one other flight is currently landing. If this is an unproblematic airdrome, you can 
-- also allow groups to taxi if planes are landing, *e.g.* if there are two separate runways.
-- 
-- NOTE that human players are *not* restricted as they should behave better (hopefully) than the AI.
-- 
-- @param #FLIGHTCONTROL self
-- @param #number Ntaxi Max number of groups allowed to taxi. Default 2.
-- @param #boolean IncludeInbound If `true`, the above
-- @param #number Nlanding Max number of landing flights. Default 0.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetLimitTaxi(Ntaxi, IncludeInbound, Nlanding)

  self.NtaxiTot=Ntaxi or 2
  
  self.NtaxiInbound=IncludeInbound
  
  self.NtaxiLanding=Nlanding or 0

  return self
end

--- Add a holding pattern.
-- This is a zone where the aircraft...
-- @param #FLIGHTCONTROL self
-- @param Core.Zone#ZONE ArrivalZone Zone where planes arrive.
-- @param #number Heading Heading in degrees.
-- @param #number Length Length in nautical miles. Default 15 NM.
-- @param #number FlightlevelMin Min flight level. Default 5.
-- @param #number FlightlevelMax Max flight level. Default 15.
-- @param #number Prio Priority. Lower is higher. Default 50.
-- @return #FLIGHTCONTROL.HoldingPattern Holding pattern table.
function FLIGHTCONTROL:AddHoldingPattern(ArrivalZone, Heading, Length, FlightlevelMin, FlightlevelMax, Prio)

  -- Get ZONE if passed as string.
  if type(ArrivalZone)=="string" then
    ArrivalZone=ZONE:New(ArrivalZone)
  end
  
  -- Increase counter.
  self.hpcounter=self.hpcounter+1

  local hp={} --#FLIGHTCONTROL.HoldingPattern
  hp.uid=self.hpcounter
  hp.arrivalzone=ArrivalZone  
  hp.name=string.format("%s-%d", ArrivalZone:GetName(), hp.uid)
  hp.pos0=ArrivalZone:GetCoordinate()
  hp.pos1=hp.pos0:Translate(UTILS.NMToMeters(Length or 15), Heading)
  hp.angelsmin=FlightlevelMin or 5
  hp.angelsmax=FlightlevelMax or 15
  hp.prio=Prio or 50
  
  hp.stacks={}
  for i=hp.angelsmin, hp.angelsmax do
    local stack={} --#FLIGHTCONTROL.HoldingStack
    stack.angels=i
    stack.flightgroup=nil
    stack.pos0=UTILS.DeepCopy(hp.pos0)
    stack.pos0:SetAltitude(UTILS.FeetToMeters(i*1000))
    stack.pos1=UTILS.DeepCopy(hp.pos1)
    stack.pos1:SetAltitude(UTILS.FeetToMeters(i*1000))
    stack.heading=Heading
    table.insert(hp.stacks, stack)
  end
  
  -- Add to table.
  table.insert(self.holdingpatterns, hp)

  -- Sort holding patterns wrt to prio.  
  local function _sort(a,b)
    return a.prio<b.prio
  end
  table.sort(self.holdingpatterns, _sort)
  
  return self
end

--- Remove a holding pattern.
-- @param #FLIGHTCONTROL self
-- @param #FLIGHTCONTROL.HoldingPattern HoldingPattern Holding pattern to be removed.
-- @param #FLIGHTCONTROL self
function FLIGHTCONTROL:RemoveHoldingPattern(HoldingPattern)

  for i,_holdingpattern in pairs(self.holdingpatterns) do
    local hp=_holdingpattern --#FLIGHTCONTROL.HoldingPattern
    
    if hp.uid==HoldingPattern.uid then
      self:_UnMarkHoldingPattern(HoldingPattern)
      table.remove(self.holdingpatterns, i)
      return self
    end
  end

  return self
end


--- Set to mark the holding patterns on the F10 map.
-- @param #FLIGHTCONTROL self
-- @param #boolean Switch If `true` (or `nil`), mark holding patterns.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetMarkHoldingPattern(Switch)

  if Switch==nil then
    Switch=true
  end
  
  self.markPatterns=Switch

  return self
end

--- Set speed limit for taxiing.
-- @param #FLIGHTCONTROL self
-- @param #number SpeedLimit Speed limit in knots. If `nil`, no speed limit.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetSpeedLimitTaxi(SpeedLimit)

  if SpeedLimit then
    self.speedLimitTaxi=UTILS.KnotsToMps(SpeedLimit)
  else
    self.speedLimitTaxi=nil
  end

  return self
end


--- Set the parking guard group. This group is used to block (AI) aircraft from taxiing until they get clearance. It should contain of only one unit, *e.g.* a simple soldier.
-- @param #FLIGHTCONTROL self
-- @param #string TemplateGroupName Name of the template group.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetParkingGuard(TemplateGroupName)

  local alias=string.format("Parking Guard %s", self.airbasename)

  -- Need spawn with alias for multiple FCs.  
  self.parkingGuard=SPAWN:NewWithAlias(TemplateGroupName, alias)

  return self
end

--- Set the parking guard static. This static is used to block (AI) aircraft from taxiing until they get clearance.
-- @param #FLIGHTCONTROL self
-- @param #string TemplateStaticName Name of the template static.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetParkingGuardStatic(TemplateStaticName)

  local alias=string.format("Parking Guard %s", self.airbasename)

  -- Need spawn with alias for multiple FCs.    
  self.parkingGuard=SPAWNSTATIC:NewFromStatic(TemplateStaticName):InitNamePrefix(alias)

  return self
end

--- Set ATIS.
-- @param #FLIGHTCONTROL self
-- @param Ops.ATIS#ATIS Atis ATIS.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetATIS(Atis)
  self.atis=Atis
  return self
end

--- Get coordinate of the airbase.
-- @param #FLIGHTCONTROL self
-- @return Core.Point#COORDINATE Coordinate of the airbase.
function FLIGHTCONTROL:GetCoordinate()
  return self.airbase:GetCoordinate()
end

--- Get coalition of the airbase.
-- @param #FLIGHTCONTROL self
-- @return #number Coalition ID.
function FLIGHTCONTROL:GetCoalition()
  return self.airbase:GetCoalition()
end

--- Get country of the airbase.
-- @param #FLIGHTCONTROL self
-- @return #number Country ID.
function FLIGHTCONTROL:GetCountry()
  return self.airbase:GetCountry()
end

--- Set the time until the runway(s) of an airdrome are repaired after it has been destroyed.
-- Note that this is the time, the DCS engine uses not something we can control on a user level or we could get via scripting.
-- You need to input the value. On the DCS forum it was stated that this is currently one hour. Hence this is the default value.
-- @param #FLIGHTCONTROL self
-- @param #number RepairTime Time in seconds until the runway is repaired. Default 3600sec (one hour).
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetRunwayRepairtime(RepairTime)
  self.runwayrepairtime=RepairTime or 3600
  return self
end

--- Check if runway is operational.
-- @param #FLIGHTCONTROL self
-- @return #number Time in seconds until the runway is repaired. Will return 0 if runway is repaired.
function FLIGHTCONTROL:GetRunwayRepairtime()
  if self.runwaydestroyed then
    local Tnow=timer.getAbsTime()
    local Tsince=Tnow-self.runwaydestroyed
    local Trepair=math.max(self.runwayrepairtime-Tsince, 0)
    return Trepair
  end
  return 0
end

--- Check if runway is operational.
-- @param #FLIGHTCONTROL self
-- @return #boolean If `true`, runway is operational.
function FLIGHTCONTROL:IsRunwayOperational()
  if self.airbase then
    if self.runwaydestroyed then
      return false
    else
      return true
    end
  end
  return nil
end

--- Check if runway is destroyed.
-- @param #FLIGHTCONTROL self
-- @return #boolean If `true`, runway is destroyed.
function FLIGHTCONTROL:IsRunwayDestroyed()
  if self.airbase then
    if self.runwaydestroyed then
      return true
    else
      return false
    end
  end
  return nil
end

--- Is flight in queue of this flightcontrol.
-- @param #FLIGHTCONTROL self
-- @param Ops.FlightGroup#FLIGHTGROUP Flight Flight group.
-- @return #boolean If `true`, flight is in queue.
function FLIGHTCONTROL:IsFlight(Flight)

  for _,_flight in pairs(self.flights) do
    local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
    if flight.groupname==Flight.groupname then
      return true
    end
  end

  return false
end

--- Check if coordinate is on runway.
-- @param #FLIGHTCONTROL self
-- @param Core.Point#COORDINATE Coordinate
-- @return #boolean If `true`, coordinate is on a runway.
function FLIGHTCONTROL:IsCoordinateRunway(Coordinate)

  -- Get runways.
  local runways=self.airbase:GetRunways()
  
  -- Check all runways.
  for _,_runway in pairs(runways) do
    local runway=_runway --Wrapper.Airbase#AIRBASE.Runway
    
    -- Check if coordinate is in zone.
    if runway.zone:IsCoordinateInZone(Coordinate) then
      return true
    end
  end

  return false
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Start FLIGHTCONTROL FSM. Handle events.
-- @param #FLIGHTCONTROL self
function FLIGHTCONTROL:onafterStart()

  -- Events are handled my MOOSE.
  self:I(self.lid..string.format("Starting FLIGHTCONTROL v%s for airbase %s of type %d on map %s", FLIGHTCONTROL.version, self.airbasename, self.airbasetype, self.theatre))
  
  -- Init parking spots.
  self:_InitParkingSpots()  

  -- Handle events.
  self:HandleEvent(EVENTS.Birth)
  self:HandleEvent(EVENTS.EngineStartup)
  self:HandleEvent(EVENTS.Takeoff)
  self:HandleEvent(EVENTS.Land)
  self:HandleEvent(EVENTS.EngineShutdown)
  self:HandleEvent(EVENTS.Crash, FLIGHTCONTROL.OnEventCrashOrDead)
  self:HandleEvent(EVENTS.Dead,  FLIGHTCONTROL.OnEventCrashOrDead)
  self:HandleEvent(EVENTS.Kill)
 
  -- Init status updates.
  self:__StatusUpdate(-1)
end

--- On Before Update status.
-- @param #FLIGHTCONTROL self
function FLIGHTCONTROL:onbeforeStatusUpdate()
  
  local Tqueue=self.msrsqueue:CalcTransmisstionDuration()
  
  if Tqueue>0 then
      -- Debug info.
      local text=string.format("Still got %d messages in the radio queue. Will call status again in %.1f sec", #self.msrsqueue, Tqueue)
      self:T(self.lid..text)
        
      -- Call status again in dt seconds.
      self:__StatusUpdate(-Tqueue)

      -- Deny transition.
      return false  
  end

  return true
end

--- Update status.
-- @param #FLIGHTCONTROL self
function FLIGHTCONTROL:onafterStatusUpdate()

  -- Debug message.
  self:T2(self.lid.."Status update")
  
  -- Check markers of holding patterns.
  self:_CheckMarkHoldingPatterns()
  
  -- Check if runway was repaired.
  if self:IsRunwayOperational()==false then
    local Trepair=self:GetRunwayRepairtime()
    if Trepair==0 then
      self:RunwayRepaired()
    else
      self:I(self.lid..string.format("Runway still destroyed! Will be repaired in %d sec", Trepair))
    end
  end  

  -- Check status of all registered flights.
  self:_CheckFlights()
  
  -- Check parking spots.
  --self:_CheckParking()
  
  -- Check waiting and landing queue.
  self:_CheckQueues()
  
  -- Get runway.
  local rwyLanding=self:GetActiveRunwayText()
  local rwyTakeoff=self:GetActiveRunwayText(true)
    
  -- Count flights.
  local Nflights= self:CountFlights()
  local NQparking=self:CountFlights(FLIGHTCONTROL.FlightStatus.PARKING)
  local NQreadytx=self:CountFlights(FLIGHTCONTROL.FlightStatus.READYTX)
  local NQtaxiout=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIOUT)
  local NQreadyto=self:CountFlights(FLIGHTCONTROL.FlightStatus.READYTO)
  local NQtakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF)
  local NQinbound=self:CountFlights(FLIGHTCONTROL.FlightStatus.INBOUND)
  local NQholding=self:CountFlights(FLIGHTCONTROL.FlightStatus.HOLDING)
  local NQlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING)
  local NQtaxiinb=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIINB)
  local NQarrived=self:CountFlights(FLIGHTCONTROL.FlightStatus.ARRIVED)
  -- =========================================================================================================
  local Nqueues = (NQparking+NQreadytx+NQtaxiout+NQreadyto+NQtakeoff) + (NQinbound+NQholding+NQlanding+NQtaxiinb+NQarrived)

  -- Count free parking spots.
  --TODO: get and substract number of reserved parking spots.
  local nfree=self.Nparkingspots-NQarrived-NQparking

  local Nfree=self:CountParking(AIRBASE.SpotStatus.FREE)
  local Noccu=self:CountParking(AIRBASE.SpotStatus.OCCUPIED)
  local Nresv=self:CountParking(AIRBASE.SpotStatus.RESERVED)
  
  if Nfree+Noccu+Nresv~=self.Nparkingspots then
    self:E(self.lid..string.format("WARNING: Number of parking spots does not match! Nfree=%d, Noccu=%d, Nreserved=%d != %d total", Nfree, Noccu, Nresv, self.Nparkingspots))
  end

  -- Info text.
  if self.verbose>=1 then
    local text=string.format("State %s - Runway Landing=%s, Takeoff=%s  - Parking F=%d/O=%d/R=%d of %d - Flights=%s: Qpark=%d Qtxout=%d Qready=%d Qto=%d | Qinbound=%d Qhold=%d Qland=%d Qtxinb=%d Qarr=%d", 
    self:GetState(), rwyLanding, rwyTakeoff, Nfree, Noccu, Nresv, self.Nparkingspots, Nflights, NQparking, NQtaxiout, NQreadyto, NQtakeoff, NQinbound, NQholding, NQlanding, NQtaxiinb, NQarrived)
    self:I(self.lid..text)
  end
  
  if Nflights==Nqueues then
    --Check!
  else
    self:E(string.format("WARNING: Number of total flights %d!=%d number of flights in all queues!", Nflights, Nqueues))
  end
  
  if self.verbose>=2 then
    local text="Holding Patterns:"
    for i,_pattern in pairs(self.holdingpatterns) do
      local pattern=_pattern --#FLIGHTCONTROL.HoldingPattern
      
      -- Pattern info.
      text=text..string.format("\n[%d] Pattern %s [Prio=%d, UID=%d]: Stacks=%d, Angels %d - %d", i, pattern.name, pattern.prio, pattern.uid, #pattern.stacks, pattern.angelsmin, pattern.angelsmax)
      
      if self.verbose>=4 then
        -- Explicit stack info.
        for _,_stack in pairs(pattern.stacks) do
          local stack=_stack --#FLIGHTCONTROL.HoldingStack
          local text=string.format("", stack.angels, stack)
        end
      end
    end
    self:I(self.lid..text)
  end

  -- Next status update in ~30 seconds.
  self:__StatusUpdate(-30)
end

--- Stop FLIGHTCONTROL FSM.
-- @param #FLIGHTCONTROL self
function FLIGHTCONTROL:onafterStop()

  -- Unhandle events.
  self:UnHandleEvent(EVENTS.Birth)
  self:UnHandleEvent(EVENTS.EngineStartup)
  self:UnHandleEvent(EVENTS.Takeoff)
  self:UnHandleEvent(EVENTS.Land)
  self:UnHandleEvent(EVENTS.EngineShutdown)
  self:UnHandleEvent(EVENTS.Crash)
  self:UnHandleEvent(EVENTS.Kill)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Event handler for event birth.
-- @param #FLIGHTCONTROL self
-- @param Core.Event#EVENTDATA EventData
function FLIGHTCONTROL:OnEventBirth(EventData)
  self:F3({EvendData=EventData})
    
  if EventData and EventData.IniGroupName and EventData.IniUnit then
  
    self:T3(self.lid..string.format("BIRTH: unit  = %s", tostring(EventData.IniUnitName)))
    self:T3(self.lid..string.format("BIRTH: group = %s", tostring(EventData.IniGroupName)))

    -- Unit that was born.
    local unit=EventData.IniUnit    
  
    -- We delay this, to have all elements of the group in the game.
    if unit:IsAir() then
    
      local bornhere=EventData.Place and EventData.Place:GetName()==self.airbasename or false
      --env.info("FF born here ".. tostring(bornhere))
    
      -- We got a player?
      local playerunit, playername=self:_GetPlayerUnitAndName(EventData.IniUnitName)
      
      if playername or bornhere then
    
        -- Create player menu.
        self:ScheduleOnce(0.5, self._CreateFlightGroup, self, EventData.IniGroup)
        
      end
      
      -- Spawn parking guard.
      if bornhere then
        self:SpawnParkingGuard(unit)
      end

    end
      
  end
  
end

--- Event handling function.
-- @param #FLIGHTCONTROL self
-- @param Core.Event#EVENTDATA EventData Event data.
function FLIGHTCONTROL:OnEventCrashOrDead(EventData)

  if EventData then

    -- Check if out runway was destroyed.
    if EventData.IniUnitName then
      if self.airbase and self.airbasename and self.airbasename==EventData.IniUnitName then
        self:RunwayDestroyed()      
      end
    end
    
  end
  
end

--- Event handler for event land.
-- @param #FLIGHTCONTROL self
-- @param Core.Event#EVENTDATA EventData
function FLIGHTCONTROL:OnEventLand(EventData)
  self:F3({EvendData=EventData})
  
  self:T2(self.lid..string.format("LAND: unit  = %s", tostring(EventData.IniUnitName)))
  self:T3(self.lid..string.format("LAND: group = %s", tostring(EventData.IniGroupName)))
  
end

--- Event handler for event takeoff.
-- @param #FLIGHTCONTROL self
-- @param Core.Event#EVENTDATA EventData
function FLIGHTCONTROL:OnEventTakeoff(EventData)
  self:F3({EvendData=EventData})
  
  self:T2(self.lid..string.format("TAKEOFF: unit  = %s", tostring(EventData.IniUnitName)))
  self:T3(self.lid..string.format("TAKEOFF: group = %s", tostring(EventData.IniGroupName)))
  
  -- This would be the closest airbase.
  local airbase=EventData.Place
  
  -- Unit that took off.
  local unit=EventData.IniUnit
  
  -- Nil check for airbase. Crashed as player gave me no airbase.
  if not (airbase or unit) then
    self:E(self.lid.."WARNING: Airbase or IniUnit is nil in takeoff event!")
    return
  end
  
end

--- Event handler for event engine startup.
-- @param #FLIGHTCONTROL self
-- @param Core.Event#EVENTDATA EventData
function FLIGHTCONTROL:OnEventEngineStartup(EventData)
  self:F3({EvendData=EventData})
  
  self:T2(self.lid..string.format("ENGINESTARTUP: unit  = %s", tostring(EventData.IniUnitName)))
  self:T3(self.lid..string.format("ENGINESTARTUP: group = %s", tostring(EventData.IniGroupName)))
  
end

--- Event handler for event engine shutdown.
-- @param #FLIGHTCONTROL self
-- @param Core.Event#EVENTDATA EventData
function FLIGHTCONTROL:OnEventEngineShutdown(EventData)
  self:F3({EvendData=EventData})
  
  self:T2(self.lid..string.format("ENGINESHUTDOWN: unit  = %s", tostring(EventData.IniUnitName)))
  self:T3(self.lid..string.format("ENGINESHUTDOWN: group = %s", tostring(EventData.IniGroupName)))
  
end

--- Event handler for event kill.
-- @param #FLIGHTCONTROL self
-- @param Core.Event#EVENTDATA EventData
function FLIGHTCONTROL:OnEventKill(EventData)
  self:F3({EvendData=EventData})
  
  -- Debug info.
  self:T2(self.lid..string.format("KILL: ini unit  = %s", tostring(EventData.IniUnitName)))
  self:T3(self.lid..string.format("KILL: ini group = %s", tostring(EventData.IniGroupName)))
  self:T2(self.lid..string.format("KILL: tgt unit  = %s", tostring(EventData.TgtUnitName)))
  self:T3(self.lid..string.format("KILL: tgt group = %s", tostring(EventData.TgtGroupName)))
  
  -- Parking guard name prefix.
  local guardPrefix=string.format("Parking Guard %s", self.airbasename)
  
  local victimName=EventData.IniUnitName
  local killerName=EventData.TgtUnitName
  
  if victimName and victimName:find(guardPrefix) then
    
    env.info(string.format("Parking guard %s killed!", victimName))
    
    for _,_flight in pairs(self.flights) do
      local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
      local element=flight:GetElementByName(killerName)
      if element then
        env.info(string.format("Parking guard %s killed by %s!", victimName, killerName))
        return
      end
    end
    
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "RunwayDestroyed" event.
-- @param #FLIGHTCONTROL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTCONTROL:onafterRunwayDestroyed(From, Event, To)

  -- Debug Message.
  self:T(self.lid..string.format("Runway destoyed!"))

  -- Set time stamp.
  self.runwaydestroyed=timer.getAbsTime()
  
  self:TransmissionTower("All flights, our runway was destroyed. All operations are suspended for one hour.",Flight,Delay)
  
end

--- On after "RunwayRepaired" event.
-- @param #FLIGHTCONTROL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTCONTROL:onafterRunwayRepaired(From, Event, To)

  -- Debug Message.
  self:T(self.lid..string.format("Runway repaired!"))

  -- Set parameter.
  self.runwaydestroyed=nil

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Queue Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check takeoff and landing queues.
-- @param #FLIGHTCONTROL self
function FLIGHTCONTROL:_CheckQueues()

  -- Print queue.
  if self.verbose>=2 then
    self:_PrintQueue(self.flights,  "All flights")
  end

  -- Get next flight in line: either holding or parking.
  local flight, isholding, parking=self:_GetNextFlight()
  

  -- Check if somebody wants something.
  if flight then
      
    if isholding then

      --------------------
      -- Holding flight --
      --------------------

      -- No other flight is taking off and number of landing flights is below threshold.      
      if self:_CheckFlightLanding(flight) then

        -- Get interval to last flight that got landing clearance.
        local dTlanding=99999
        if self.Tlanding then
          dTlanding=timer.getAbsTime()-self.Tlanding
        end
      
        if parking and dTlanding>=self.dTlanding then
        
          -- Get callsign.
          local callsign=self:_GetCallsignName(flight)
          
          -- Runway.
          local runway=self:GetActiveRunwayText()
                
          -- Message.
          local text=string.format("%s, %s, you are cleared to land, runway %s", callsign, self.alias, runway)
                    
          -- Transmit message.
          self:TransmissionTower(text, flight)
              
          -- Give AI the landing signal.
          if flight.isAI then
          
            -- Message.
            local text=string.format("Runway %s, cleared to land, %s", runway, callsign)
                      
            -- Transmit message.
            self:TransmissionPilot(text, flight, 10)
          
            -- Land AI.          
            self:_LandAI(flight, parking)
          else
          
            -- We set this flight to landing. With this he is allowed to leave the pattern.
            self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.LANDING)
                        
          end
        
          -- Set time last flight got landing clearance.  
          self.Tlanding=timer.getAbsTime()
          
        end
      else
        self:T3(self.lid..string.format("FYI: Landing clearance for flight %s denied", flight.groupname))
      end
    
    else
    
      --------------------
      -- Takeoff flight --
      --------------------
     
      -- No other flight is taking off or landing.
      if self:_CheckFlightTakeoff(flight) then
      
        -- Get callsign.
        local callsign=self:_GetCallsignName(flight)
          
        -- Runway.
        local runway=self:GetActiveRunwayText(true)
      
        -- Message.
        local text=string.format("%s, %s, taxi to runway %s, hold short", callsign, self.alias, runway)
        
        if self:GetFlightStatus(flight)==FLIGHTCONTROL.FlightStatus.READYTO then
          text=string.format("%s, %s, cleared for take-off, runway %s", callsign, self.alias, runway)
        end
          
        -- Transmit message.
        self:TransmissionTower(text, flight)
     
        -- Check if flight is AI. Humans have to request taxi via F10 menu.
        if flight.isAI then
        
          ---
          -- AI
          ---
                  
          -- Message.
          local text="Wilco, "
          
          -- Start uncontrolled aircraft.
          if flight:IsUncontrolled() then

          -- Message.
            text=text..string.format("starting engines, ")
            
            -- Start uncontrolled aircraft.          
            flight:StartUncontrolled()
          end
          
          -- Message.
          text=text..string.format("runway %s, %s", runway, callsign)
          
          -- Transmit message.
          self:TransmissionPilot(text, flight, 10)             
          
          -- Remove parking guards.
          for _,_element in pairs(flight.elements) do
            local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element
            if element and element.parking then
              local spot=self:GetParkingSpotByID(element.parking.TerminalID)
              self:RemoveParkingGuard(spot)
            end
          end
          
          -- Set flight to takeoff. No way we can stop the AI now.
          self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF)
          
        else

          ---
          -- PLAYER
          ---

          if self:GetFlightStatus(flight)==FLIGHTCONTROL.FlightStatus.READYTO then

            -- Player is ready for takeoff
            self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF)
            
          else

            -- Remove parking guards.
            for _,_element in pairs(flight.elements) do
              local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element
              if element.parking then
                local spot=self:GetParkingSpotByID(element.parking.TerminalID)
                if element.ai then
                  self:RemoveParkingGuard(spot, 15)
                else
                  self:RemoveParkingGuard(spot, 10)
                end
              end        
            end
            
          end
          
        end
        
      else
        -- Debug message.
        self:T3(self.lid..string.format("FYI: Take off for flight %s denied", flight.groupname))
      end
    end
  else
    -- Debug message.
    self:T2(self.lid..string.format("FYI: No flight in queue for takeoff or landing"))
  end
  
end

--- Check if a flight can get clearance for taxi/takeoff.
-- @param #FLIGHTCONTROL self
-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight..
-- @return #boolean If true, flight can.
function FLIGHTCONTROL:_CheckFlightTakeoff(flight)

  -- Number of groups landing.
  local nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING)
    
  -- Number of groups taking off.
  local ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF, nil, true)
  
  -- Current status.
  local status=self:GetFlightStatus(flight)
  
  if flight.isAI then
    ---
    -- AI
    ---
      
    if nlanding>self.NtaxiLanding then
      self:T(self.lid..string.format("AI flight %s [status=%s] NOT cleared for taxi/takeoff as %d>%d flight(s) landing", flight.groupname, status, nlanding, self.NtaxiLanding))
      return false
    end
    
    local ninbound=0
    if self.NtaxiInbound then
      ninbound=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIINB, nil, true)
    end

    if ntakeoff+ninbound>=self.NtaxiTot then
      self:T(self.lid..string.format("AI flight %s [status=%s] NOT cleared for taxi/takeoff as %d>=%d flight(s) taxi/takeoff", flight.groupname, status, ntakeoff, self.NtaxiTot))
      return false
    end
  
    self:T(self.lid..string.format("AI flight %s [status=%s] cleared for taxi/takeoff! nLanding=%d, nTakeoff=%d", flight.groupname, status, nlanding, ntakeoff))
    return true
  else
    ---
    -- Player
    -- 
    -- We allow unlimited number of players to taxi to runway.
    -- We do not allow takeoff if at least one flight is landing.
    ---
  
    if status==FLIGHTCONTROL.FlightStatus.READYTO then
    
      if nlanding>self.NtaxiLanding then
        -- Traffic landing. No permission to
        self:T(self.lid..string.format("Player flight %s [status=%s] not cleared for taxi/takeoff as %d>%d flight(s) landing", flight.groupname, status, nlanding, self.NtaxiLanding))
        return false
      end      
    
    end
  
    self:T(self.lid..string.format("Player flight %s [status=%s] cleared for taxi/takeoff", flight.groupname, status))
    return true  
  end
  

end

--- Check if a flight can get clearance for taxi/takeoff.
-- @param #FLIGHTCONTROL self
-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight..
-- @return #boolean If true, flight can.
function FLIGHTCONTROL:_CheckFlightLanding(flight)

  -- Number of groups landing.
  local nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING)

  -- Number of groups taking off.
  local ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF, nil, true)
  
  -- Current status.
  local status=self:GetFlightStatus(flight)
  
  if flight.isAi then
    ---
    -- AI
    ---

    if ntakeoff<=self.NlandingTakeoff and nlanding<self.NlandingTot then
      return true
    end

    return false
  else
    ---
    -- Player
    ---


    if ntakeoff<=self.NlandingTakeoff and nlanding<self.NlandingTot then
      return true
    end

    return false      
  end
  
end

--- Get next flight in line, either waiting for landing or waiting for takeoff.
-- @param #FLIGHTCONTROL self
-- @return Ops.FlightGroup#FLIGHTGROUP Flight next in line and ready to enter the pattern. Or nil if no flight is ready.
-- @return #boolean If true, flight is holding and waiting for landing, if false, flight is parking and waiting for takeoff.
-- @return #table Parking data for holding flights or nil.
function FLIGHTCONTROL:_GetNextFlight()

  -- Get flight that is holding.
  local flightholding=self:_GetNextFightHolding()
  
  -- Get flight that is parking.
  local flightparking=self:_GetNextFightParking()
  
  -- If no flight is waiting for landing just return the takeoff flight or nil.
  if not flightholding then
    --self:T(self.lid..string.format("Next flight that is not holding"))
    return flightparking, false, nil
  end
  
  -- Get number of alive elements of the holding flight.
  local nH=flightholding:GetNelements()  
  
  -- Free parking spots.
  local parking=flightholding:GetParking(self.airbase)      
    
  -- If no flight is waiting for takeoff return the holding flight or nil.
  if not flightparking then
    if parking then
      return flightholding, true, parking
    else
      self:E(self.lid..string.format("WARNING: No flight parking but not enough parking spots for holding flight nH=%d!", nH))
      return nil, nil, nil
    end
  end
   
  -- We got flights waiting for landing and for takeoff.
  if flightholding and flightparking then
  
    local text=string.format("We got a flight holding %s [%s] and parking %s [%s]", flightholding:GetName(), flightholding:GetState(), flightparking:GetName(), flightparking:GetState())
    self:T(self.lid..text)
  
    -- Return holding flight if fuel is low.
    if flightholding.fuellow then
      if parking then
        -- Enough parking ==> land
        return flightholding, true, parking
      else
        -- Not enough parking ==> take off
        return flightparking, false, nil
      end
    end
    
    local text=string.format("Flight holding for %d sec, flight parking for %d sec", flightholding:GetHoldingTime(), flightparking:GetParkingTime())
    self:T(self.lid..text)
       
    -- Return the flight which is waiting longer. NOTE that Tholding and Tparking are abs. mission time. So a smaller value means waiting longer.
    if flightholding.Tholding and flightparking.Tparking and flightholding.Tholding<flightparking.Tparking and parking then
      return flightholding, true, parking
    else
      return flightparking, false, nil
    end
    
  end

  return nil, nil, nil
end


--- Get next flight waiting for landing clearance.
-- @param #FLIGHTCONTROL self
-- @return Ops.FlightGroup#FLIGHTGROUP Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready.
function FLIGHTCONTROL:_GetNextFightHolding()

  -- Return only AI or human player flights.
  local OnlyAI=nil  
  if self:IsRunwayDestroyed() then
    OnlyAI=false -- If false, we return only player flights.
  end

  -- Get all flights holding.
  local Qholding=self:GetFlights(FLIGHTCONTROL.FlightStatus.HOLDING, nil, OnlyAI)
  
  -- Min holding time in seconds.
  local TholdingMin=30

  if #Qholding==0 then
    return nil
  elseif #Qholding==1 then
    local fg=Qholding[1] --Ops.FlightGroup#FLIGHTGROUP
    local T=fg:GetHoldingTime()
    if T>TholdingMin then
      return fg
    end
  end

  -- Sort flights by low fuel.
  local function _sortByFuel(a, b)
    local flightA=a --Ops.FlightGroup#FLIGHTGROUP
    local flightB=b --Ops.FlightGroup#FLIGHTGROUP
    local fuelA=flightA.group:GetFuelMin()
    local fuelB=flightB.group:GetFuelMin()
    return fuelA<fuelB
  end

  -- Sort flights by holding time.
  local function _sortByTholding(a, b)
    local flightA=a --Ops.FlightGroup#FLIGHTGROUP
    local flightB=b --Ops.FlightGroup#FLIGHTGROUP
    return flightA.Tholding<flightB.Tholding  -- Tholding is the abs. timestamp. So the one with the smallest time is holding the longest.
  end


  -- Sort flights by fuel.
  table.sort(Qholding, _sortByFuel)
  
  -- Loop over all holding flights.
  for _,_flight in pairs(Qholding) do
    local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
    
    -- Return flight that is lowest on fuel.
    if flight.fuellow then  
      return flight
    end
    
  end
  

  -- Return flight waiting longest.
  table.sort(Qholding, _sortByTholding)
  
  -- First flight in line.
  local fg=Qholding[1] --Ops.FlightGroup#FLIGHTGROUP
  
  -- Check holding time.
  local T=fg:GetHoldingTime()
  if T>TholdingMin then
    return fg
  end
  
  return nil
end


--- Get next flight waiting for taxi and takeoff clearance.
-- @param #FLIGHTCONTROL self
-- @return Ops.FlightGroup#FLIGHTGROUP Marshal flight next in line and ready to enter the pattern. Or nil if no flight is ready.
function FLIGHTCONTROL:_GetNextFightParking()

  -- Return only AI or human player flights.
  local OnlyAI=nil  
  if self:IsRunwayDestroyed() then
    OnlyAI=false -- If false, we return only player flights.
  end

  -- Get flights ready for take off.
  local QreadyTO=self:GetFlights(FLIGHTCONTROL.FlightStatus.READYTO, OPSGROUP.GroupStatus.TAXIING, OnlyAI)

  -- First check human players.
  if #QreadyTO>0 then    
    -- First come, first serve.
    return QreadyTO[1]
  end
  
  -- Get flights ready to taxi.
  local QreadyTX=self:GetFlights(FLIGHTCONTROL.FlightStatus.READYTX, OPSGROUP.GroupStatus.PARKING, OnlyAI)

  -- First check human players.
  if #QreadyTX>0 then
    -- First come, first serve.
    return QreadyTX[1]
  end
  
  -- Check if runway is destroyed.
  if self:IsRunwayDestroyed() then
    -- Runway destroyed. As we only look for AI later on, we return nil here.
    return nil
  end
  
  -- Get AI flights parking.
  local Qparking=self:GetFlights(FLIGHTCONTROL.FlightStatus.PARKING, nil, true)
  
  -- Number of flights parking.
  local Nparking=#Qparking

  -- Check special cases where only up to one flight is waiting for takeoff.
  if Nparking==0 then
    return nil
  end

  -- Sort flights parking time.
  local function _sortByTparking(a, b)
    local flightA=a --Ops.FlightGroup#FLIGHTGROUP
    local flightB=b --Ops.FlightGroup#FLIGHTGROUP
    return flightA.Tparking<flightB.Tparking -- Tparking is the abs. timestamp. So the one with the smallest time is parking the longest.
  end

  -- Return flight waiting longest.
  table.sort(Qparking, _sortByTparking)
  
  -- Debug.
  if self.verbose>=2 then
    local text="Parking flights:"
    for i,_flight in pairs(Qparking) do
      local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
      text=text..string.format("\n[%d] %s [%s], state=%s [%s]: Tparking=%.1f sec", i, flight.groupname, tostring(flight.actype), flight:GetState(), self:GetFlightStatus(flight), flight:GetParkingTime())
    end
    self:I(self.lid..text)
  end

  -- Get the first AI flight.
  for i,_flight in pairs(Qparking) do
    local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
    if flight.isAI and flight.isReadyTO then
      return flight
    end
  end
  
  return nil
end

--- Print queue.
-- @param #FLIGHTCONTROL self
-- @param #table queue Queue to print.
-- @param #string name Queue name.
-- @return #string Queue text.
function FLIGHTCONTROL:_PrintQueue(queue, name)

  local text=string.format("%s Queue N=%d:", name, #queue)
  if #queue==0 then
    -- Queue is empty.
    text=text.." empty."
  else
    
    local time=timer.getAbsTime()
  
    -- Loop over all flights in queue.
    for i,_flight in ipairs(queue) do
      local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
      
      -- Gather info.
      local fuel=flight.group:GetFuelMin()*100
      local ai=tostring(flight.isAI)
      local actype=tostring(flight.actype)
      
      -- Holding and parking time.
      local holding=flight.Tholding and UTILS.SecondsToClock(time-flight.Tholding, true) or "X"
      local parking=flight.Tparking and UTILS.SecondsToClock(time-flight.Tparking, true) or "X"
      
      local holding=flight:GetHoldingTime()
      if holding>=0 then
        holding=UTILS.SecondsToClock(holding, true)
      else
        holding="X"
      end
      local parking=flight:GetParkingTime()
      if parking>=0 then
        parking=UTILS.SecondsToClock(parking, true)
      else
        parking="X"
      end      
      
      -- Number of elements.
      local nunits=flight:CountElements()
      
      -- Status.
      local state=flight:GetState()
      local status=self:GetFlightStatus(flight)
      
      -- Main info.
      text=text..string.format("\n[%d] %s (%s*%d): status=%s | %s, ai=%s, fuel=%d, holding=%s, parking=%s",
                                 i, flight.groupname, actype, nunits, state, status, ai, fuel, holding, parking)

      -- Elements info.                                 
      for j,_element in pairs(flight.elements) do
        local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element
        local life=element.unit:GetLife()
        local life0=element.unit:GetLife0()
        local park=element.parking and tostring(element.parking.TerminalID) or "N/A"
        text=text..string.format("\n  (%d) %s (%s): status=%s, ai=%s, airborne=%s life=%d/%d spot=%s",
        j, tostring(element.modex), element.name, tostring(element.status), tostring(element.ai), tostring(element.unit:InAir()), life, life0, park)
      end
    end
  end
  
  -- Display text.
  self:I(self.lid..text)
  
  return text
end

--- Set flight status.
-- @param #FLIGHTCONTROL self
-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group.
-- @param #string status New status.
function FLIGHTCONTROL:SetFlightStatus(flight, status)
  
  -- Debug message.
  self:T(self.lid..string.format("New status %s-->%s for flight %s", flight.controlstatus or "unknown", status, flight:GetName()))
  
  -- Update menu when flight status changed.
  if flight.controlstatus~=status and not flight.isAI then
    self:T(self.lid.."Updating menu in 0.2 sec after flight status change")
    flight:_UpdateMenu(0.2)
  end
  
  -- Set new status
  flight.controlstatus=status

end

--- Get flight status.
-- @param #FLIGHTCONTROL self
-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group.
-- @return #string Flight status
function FLIGHTCONTROL:GetFlightStatus(flight)

  if flight then
    return flight.controlstatus or "unkonwn"
  end
  
  return "unknown"
end

--- Check if FC has control over this flight.
-- @param #FLIGHTCONTROL self
-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group.
-- @return #boolean 
function FLIGHTCONTROL:IsControlling(flight)

  -- Check that we are controlling this flight.
  local is=flight.flightcontrol and flight.flightcontrol.airbasename==self.airbasename or false

  return is
end

--- Check if a group is in a queue.
-- @param #FLIGHTCONTROL self
-- @param #table queue The queue to check.
-- @param Wrapper.Group#GROUP group The group to be checked.
-- @return #boolean If true, group is in the queue. False otherwise.
function FLIGHTCONTROL:_InQueue(queue, group)
  local name=group:GetName()
  
  for _,_flight in pairs(queue) do
    local flight=_flight  --Ops.FlightGroup#FLIGHTGROUP
    if name==flight.groupname then
      return true
    end
  end
  
  return false
end

--- Get flights.
-- @param #FLIGHTCONTROL self
-- @param #string Status Return only flights in this flightcontrol status, e.g. `FLIGHTCONTROL.Status.XXX`.
-- @param #string GroupStatus Return only flights in this FSM status, e.g. `OPSGROUP.GroupStatus.TAXIING`.
-- @param #boolean AI If `true` only AI flights are returned. If `false`, only flights with clients are returned. If `nil` (default), all flights are returned.
-- @return #table Table of flights.
function FLIGHTCONTROL:GetFlights(Status, GroupStatus, AI)

  if Status~=nil or GroupStatus~=nil or AI~=nil then
  
    local flights={}
  
    for _,_flight in pairs(self.flights) do
      local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
      
      local status=self:GetFlightStatus(flight, Status)
      
      if status==Status then
        if AI==nil or AI==flight.isAI then
          if GroupStatus==nil or GroupStatus==flight:GetState() then
            table.insert(flights, flight)
          end
        end
      end
    
    end
    
    return flights
  else
    return self.flights
  end

end

--- Count flights in a given status.
-- @param #FLIGHTCONTROL self
-- @param #string Status Return only flights in this status.
-- @param #string GroupStatus Count only flights in this FSM status, e.g. `OPSGROUP.GroupStatus.TAXIING`.
-- @param #boolean AI If `true` only AI flights are counted. If `false`, only flights with clients are counted. If `nil` (default), all flights are counted.
-- @return #number Number of flights.
function FLIGHTCONTROL:CountFlights(Status, GroupStatus, AI)
  
  if Status~=nil or GroupStatus~=nil or AI~=nil then
  
    local flights=self:GetFlights(Status, GroupStatus, AI)
    
    return #flights
  
  else
    return #self.flights
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Runway Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get the active runway based on current wind direction.
-- @param #FLIGHTCONTROL self
-- @return Wrapper.Airbase#AIRBASE.Runway Active runway.
function FLIGHTCONTROL:GetActiveRunway()
  local rwy=self.airbase:GetActiveRunway()
  return rwy
end

--- Get the active runway for landing.
-- @param #FLIGHTCONTROL self
-- @return Wrapper.Airbase#AIRBASE.Runway Active runway.
function FLIGHTCONTROL:GetActiveRunwayLanding()
  local rwy=self.airbase:GetActiveRunwayLanding()
  return rwy
end

--- Get the active runway for takeoff.
-- @param #FLIGHTCONTROL self
-- @return Wrapper.Airbase#AIRBASE.Runway Active runway.
function FLIGHTCONTROL:GetActiveRunwayTakeoff()
  local rwy=self.airbase:GetActiveRunwayTakeoff()
  return rwy
end


--- Get the name of the active runway.
-- @param #FLIGHTCONTROL self
-- @param #boolean Takeoff If true, return takeoff runway name. Default is landing.
-- @return #string Runway text, e.g. "31L" or "09".
function FLIGHTCONTROL:GetActiveRunwayText(Takeoff)

  local runway
  if Takeoff then
    runway=self:GetActiveRunwayTakeoff()
  else
    runway=self:GetActiveRunwayLanding()
  end

  local name=self.airbase:GetRunwayName(runway, true)
  
  return name or "XX"
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Parking Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Init parking spots.
-- @param #FLIGHTCONTROL self
function FLIGHTCONTROL:_InitParkingSpots()

  -- Parking spots of airbase.
  local parkingdata=self.airbase:GetParkingSpotsTable()
  
  -- Init parking spots table.
  self.parking={}
  
  self.Nparkingspots=0
  for _,_spot in pairs(parkingdata) do
    local spot=_spot --Wrapper.Airbase#AIRBASE.ParkingSpot
       
    -- Mark position.
    local text=string.format("Parking ID=%d, Terminal=%d: Free=%s, Client=%s, Dist=%.1f", spot.TerminalID, spot.TerminalType, tostring(spot.Free), tostring(spot.ClientName), spot.DistToRwy)
    self:T3(self.lid..text)

    -- Add to table.
    self.parking[spot.TerminalID]=spot
    
    -- Marker.
    --spot.Marker=MARKER:New(spot.Coordinate, "Spot"):ReadOnly():ToCoalition(self:GetCoalition())
    
    -- Check if spot is initially free or occupied.
    if spot.Free then
    
      -- Parking spot is free.
      self:SetParkingFree(spot)
      
    else
    
      -- Scan for the unit sitting here.
      local unit=spot.Coordinate:FindClosestUnit(20)
      
      
      if unit then

        local unitname=unit and unit:GetName() or "unknown"
      
        local isalive=unit:IsAlive()
      
        self:T2(self.lid..string.format("FF parking spot %d is occupied by unit %s alive=%s", spot.TerminalID, unitname, tostring(isalive)))
      
        if isalive then
      
          -- Set parking occupied.
          self:SetParkingOccupied(spot, unitname)
          
          -- Spawn parking guard.
          self:SpawnParkingGuard(unit)
        
        else
        
          -- TODO
          --env.info(string.format("FF parking spot %d is occupied by NOT ALIVE unit %s", spot.TerminalID, unitname))
          
          -- Parking spot is free.
          self:SetParkingFree(spot)
          
        end
        
      else
        self:E(self.lid..string.format("ERROR: Parking spot is NOT FREE but no unit could be found there!"))
      end
    end

    -- Increase counter
    self.Nparkingspots=self.Nparkingspots+1
  end
  
end

--- Get parking spot by its Terminal ID.
-- @param #FLIGHTCONTROL self
-- @param #number TerminalID
-- @return #FLIGHTCONTROL.ParkingSpot Parking spot data table.
function FLIGHTCONTROL:GetParkingSpotByID(TerminalID)
  return self.parking[TerminalID]
end

--- Set parking spot to FREE and update F10 marker.
-- @param #FLIGHTCONTROL self
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table.
-- @param #string status New status.
-- @param #string unitname Name of the unit.
function FLIGHTCONTROL:_UpdateSpotStatus(spot, status, unitname)

  -- Debug message.
  self:T2(self.lid..string.format("Updating parking spot %d status: %s --> %s (unit=%s)", spot.TerminalID, tostring(spot.Status), status, tostring(unitname)))
  
  -- Set new status.
  spot.Status=status

end

--- Set parking spot to FREE and update F10 marker.
-- @param #FLIGHTCONTROL self
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table.
function FLIGHTCONTROL:SetParkingFree(spot)

  -- Get spot.
  local spot=self:GetParkingSpotByID(spot.TerminalID)
  
  -- Update spot status.
  self:_UpdateSpotStatus(spot, AIRBASE.SpotStatus.FREE, spot.OccupiedBy or spot.ReservedBy)
  
  -- Not occupied or reserved.
  spot.OccupiedBy=nil
  spot.ReservedBy=nil
  
  -- Remove parking guard.
  self:RemoveParkingGuard(spot)
  
  -- Update marker.
  self:UpdateParkingMarker(spot)

end

--- Set parking spot to RESERVED and update F10 marker.
-- @param #FLIGHTCONTROL self
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table.
-- @param #string unitname Name of the unit occupying the spot. Default "unknown". 
function FLIGHTCONTROL:SetParkingReserved(spot, unitname)

  -- Get spot.
  local spot=self:GetParkingSpotByID(spot.TerminalID)
  
  -- Update spot status.
  self:_UpdateSpotStatus(spot, AIRBASE.SpotStatus.RESERVED, unitname)
  
  -- Reserved.
  spot.ReservedBy=unitname or "unknown"
  
  -- Update marker.
  self:UpdateParkingMarker(spot)

end

--- Set parking spot to OCCUPIED and update F10 marker.
-- @param #FLIGHTCONTROL self
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table.
-- @param #string unitname Name of the unit occupying the spot. Default "unknown".
function FLIGHTCONTROL:SetParkingOccupied(spot, unitname)

  -- Get spot.
  local spot=self:GetParkingSpotByID(spot.TerminalID)

  -- Update spot status.
  self:_UpdateSpotStatus(spot, AIRBASE.SpotStatus.OCCUPIED, unitname)
  
  -- Occupied.
  spot.OccupiedBy=unitname or "unknown"
  
  -- Update marker.
  self:UpdateParkingMarker(spot)

end

--- Update parking markers.
-- @param #FLIGHTCONTROL self
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot The parking spot data table.
function FLIGHTCONTROL:UpdateParkingMarker(spot)

  if self.markerParking then

    -- Get spot.
    local spot=self:GetParkingSpotByID(spot.TerminalID)
    
    -- Only mark OCCUPIED and RESERVED spots.
    if spot.Status==AIRBASE.SpotStatus.FREE then
    
      if spot.Marker then
        spot.Marker:Remove()
      end
    
    else
    
      local text=string.format("Spot %d (type %d): %s", spot.TerminalID, spot.TerminalType, spot.Status:upper())
      if spot.OccupiedBy then
        text=text..string.format("\nOccupied by %s", tostring(spot.OccupiedBy))
      end
      if spot.ReservedBy then
        text=text..string.format("\nReserved for %s", tostring(spot.ReservedBy))
      end  
      if spot.ClientSpot then
        text=text..string.format("\nClient %s", tostring(spot.ClientName))
      end
      
      if spot.Marker then
      
        if text~=spot.Marker.text or not spot.Marker.shown then
          spot.Marker:UpdateText(text)
        end
        
      else
      
        spot.Marker=MARKER:New(spot.Coordinate, text):ToAll()
      
      end
      
    end
  end
  
end

--- Check if parking spot is free.
-- @param #FLIGHTCONTROL self
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot data.
-- @return #boolean If true, parking spot is free.
function FLIGHTCONTROL:IsParkingFree(spot)
  return spot.Status==AIRBASE.SpotStatus.FREE
end

--- Check if a parking spot is reserved by a flight group.
-- @param #FLIGHTCONTROL self
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot to check.
-- @return #string Name of element or nil.
function FLIGHTCONTROL:IsParkingOccupied(spot)

  if spot.Status==AIRBASE.SpotStatus.OCCUPIED then
    return tostring(spot.OccupiedBy)
  else
    return false
  end
end

--- Check if a parking spot is reserved by a flight group.
-- @param #FLIGHTCONTROL self
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot spot Parking spot to check.
-- @return #string Name of element or *nil*.
function FLIGHTCONTROL:IsParkingReserved(spot)

  if spot.Status==AIRBASE.SpotStatus.RESERVED then
    return tostring(spot.ReservedBy)
  else
    return false
  end
end

--- Get free parking spots.
-- @param #FLIGHTCONTROL self
-- @param #number terminal Terminal type or nil.
-- @return #number Number of free spots. Total if terminal=nil or of the requested terminal type.
-- @return #table Table of free parking spots of data type #FLIGHCONTROL.ParkingSpot.
function FLIGHTCONTROL:_GetFreeParkingSpots(terminal)
  
  local freespots={}
  
  local n=0
  for _,_parking in pairs(self.parking) do
    local parking=_parking --Wrapper.Airbase#AIRBASE.ParkingSpot
    
    if self:IsParkingFree(parking) then
      if terminal==nil or terminal==parking.terminal then
        n=n+1
        table.insert(freespots, parking)
      end
    end
  end
  
  return n,freespots
end

--- Get closest parking spot.
-- @param #FLIGHTCONTROL self
-- @param Core.Point#COORDINATE Coordinate Reference coordinate.
-- @param #number TerminalType (Optional) Check only this terminal type.
-- @param #boolean Status (Optional) Only consider spots that have this status.
-- @return #FLIGHTCONTROL.ParkingSpot Closest parking spot.
function FLIGHTCONTROL:GetClosestParkingSpot(Coordinate, TerminalType, Status)

  local distmin=math.huge
  local spotmin=nil
  
  for TerminalID, Spot in pairs(self.parking) do
    local spot=Spot --Wrapper.Airbase#AIRBASE.ParkingSpot
    
    --env.info(self.lid..string.format("FF Spot %d: %s", spot.TerminalID, spot.Status))
    
    if (Status==nil or Status==spot.Status) and AIRBASE._CheckTerminalType(spot.TerminalType, TerminalType) then
      
      -- Get distance from coordinate to spot.
      local dist=Coordinate:Get2DDistance(spot.Coordinate)
      
      -- Check if distance is smaller.
      if dist<distmin then
        distmin=dist
        spotmin=spot
      end
      
    end
  end
  
  return spotmin
end


--- Get parking spot this player was initially spawned on.
-- @param #FLIGHTCONTROL self
-- @param #string UnitName Name of the player unit.
-- @return #FLIGHTCONTROL.ParkingSpot Player spot or nil.
function FLIGHTCONTROL:_GetPlayerSpot(UnitName)

  for TerminalID, Spot in pairs(self.parking) do
    local spot=Spot --Wrapper.Airbase#AIRBASE.ParkingSpot
    
    if spot.ClientName and spot.ClientName==UnitName and (spot.Status==AIRBASE.SpotStatus.FREE or spot.ReservedBy==UnitName) then
      return spot
    end
    
  end
  
  return nil
end

--- Count number of parking spots.
-- @param #FLIGHTCONTROL self
-- @param #string SpotStatus (Optional) Status of spot.
-- @return #number Number of parking spots.
function FLIGHTCONTROL:CountParking(SpotStatus)

  local n=0
  for _,_spot in pairs(self.parking) do
    local spot=_spot --Wrapper.Airbase#AIRBASE.ParkingSpot
    if SpotStatus==nil or SpotStatus==spot.Status then
      n=n+1
    end
  
  end

  return n
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ATIS Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ATC Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Player Menu
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create player menu.
-- @param #FLIGHTCONTROL self
-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group.
-- @param Core.Menu#MENU_GROUP mainmenu ATC root menu table.
function FLIGHTCONTROL:_CreatePlayerMenu(flight, mainmenu)
  
  -- Group info.
  local group=flight.group
  local groupname=flight.groupname
  local gid=group:GetID()
  
  -- Flight status.
  local flightstatus=self:GetFlightStatus(flight)
  
  -- Are we controlling this flight.
  local gotcontrol=self:IsControlling(flight)
  
  -- Get player element.
  local player=flight:GetPlayerElement()
  
  -- Debug info.
  local text=string.format("Creating ATC player menu for flight %s: in state=%s status=%s, gotcontrol=%s, player=%s", 
  tostring(flight.groupname), flight:GetState(), flightstatus, tostring(gotcontrol), player.status)
  self:T(self.lid..text)


  -- Airbase root menu.  
  local rootmenu=MENU_GROUP:New(group, self.airbasename, mainmenu)
  
  ---
  -- Help Menu
  ---
  local helpmenu=MENU_GROUP:New(group, "Help",  rootmenu)
  MENU_GROUP_COMMAND:New(group, "Radio Check",     helpmenu, self._PlayerRadioCheck,     self, groupname)
  MENU_GROUP_COMMAND:New(group, "Confirm Status",  helpmenu, self._PlayerConfirmStatus,  self, groupname)
  if gotcontrol and flight:IsInbound() and flight.stack then
  MENU_GROUP_COMMAND:New(group, "Vector Holding",  helpmenu, self._PlayerVectorInbound,  self, groupname)
  end  

  ---
  -- Info Menu
  ---
  local infomenu=MENU_GROUP:New(group, "Info",  rootmenu)
  MENU_GROUP_COMMAND:New(group, "Airbase", infomenu, self._PlayerInfoAirbase, self, groupname)
  MENU_GROUP_COMMAND:New(group, "Traffic", infomenu, self._PlayerInfoTraffic, self, groupname)
  MENU_GROUP_COMMAND:New(group, "ATIS",    infomenu, self._PlayerInfoATIS,    self, groupname)

  ---
  -- Root Menu
  ---
  if gotcontrol then
  
    local status=self:GetFlightStatus(flight)

    ---
    -- FC is controlling this flight
    ---
  
    if flight:IsParking(player) or player.status==OPSGROUP.ElementStatus.ENGINEON then
      ---
      -- Parking
      ---
      
      if status==FLIGHTCONTROL.FlightStatus.READYTX then
        MENU_GROUP_COMMAND:New(group, "Cancel Taxi",    rootmenu, self._PlayerAbortTaxi,   self, groupname)
      else
        MENU_GROUP_COMMAND:New(group, "Request Taxi",  rootmenu, self._PlayerRequestTaxi, self, groupname)
      end
      
    elseif flight:IsTaxiing(player) then
      ---
      -- Taxiing
      ---

      if status==FLIGHTCONTROL.FlightStatus.READYTX or status==FLIGHTCONTROL.FlightStatus.TAXIOUT then
        -- Flight is "ready to taxi" (awaiting clearance) or "taxiing to runway".
        MENU_GROUP_COMMAND:New(group, "Request Takeoff", rootmenu, self._PlayerRequestTakeoff, self, groupname)
        MENU_GROUP_COMMAND:New(group, "Abort Taxi",      rootmenu, self._PlayerAbortTaxi,      self, groupname)              
      elseif status==FLIGHTCONTROL.FlightStatus.READYTO then
        -- Flight is ready for take off.
        MENU_GROUP_COMMAND:New(group, "Abort Takeoff",   rootmenu, self._PlayerAbortTakeoff,   self, groupname)
      elseif status==FLIGHTCONTROL.FlightStatus.TAKEOFF then
        -- Flight is taking off.
        MENU_GROUP_COMMAND:New(group, "Abort Takeoff",   rootmenu, self._PlayerAbortTakeoff,   self, groupname)
      elseif status==FLIGHTCONTROL.FlightStatus.TAXIINB then
        -- Could be after "abort taxi" call and we changed our mind (again)
        MENU_GROUP_COMMAND:New(group, "Request Taxi",       rootmenu, self._PlayerRequestTaxi,    self, groupname)
        if player.parking then
          MENU_GROUP_COMMAND:New(group, "Cancel Parking",    rootmenu, self._PlayerCancelParking, self, groupname)          
        else
          MENU_GROUP_COMMAND:New(group, "Reserve Parking",    rootmenu, self._PlayerRequestParking, self, groupname)
        end
        MENU_GROUP_COMMAND:New(group, "Arrived at Parking", rootmenu, self._PlayerArrived,        self, groupname)          
      end
      
    elseif flight:IsInbound() then
      ---
      -- Inbound
      ---

      if status==FLIGHTCONTROL.FlightStatus.LANDING then
        -- After direct approach.
        MENU_GROUP_COMMAND:New(group, "Confirm Landing!", rootmenu, self._PlayerConfirmLanding, self, groupname)
        MENU_GROUP_COMMAND:New(group, "Abort Landing",   rootmenu, self._PlayerAbortLanding,   self, groupname)
      else
        MENU_GROUP_COMMAND:New(group, "Holding!",        rootmenu, self._PlayerHolding,        self, groupname)
        MENU_GROUP_COMMAND:New(group, "Direct Approach", rootmenu, self._PlayerRequestDirectLanding, self, groupname)      
        MENU_GROUP_COMMAND:New(group, "Abort Inbound",   rootmenu, self._PlayerAbortInbound,   self, groupname)
      end
      if player.parking then
        MENU_GROUP_COMMAND:New(group, "Cancel Parking",    rootmenu, self._PlayerCancelParking, self, groupname)
      else
        MENU_GROUP_COMMAND:New(group, "Reserve Parking", rootmenu, self._PlayerRequestParking, self, groupname)
      end

      
    elseif flight:IsHolding() then
      ---
      -- Holding
      ---

      MENU_GROUP_COMMAND:New(group, "Confirm Landing!", rootmenu, self._PlayerConfirmLanding, self, groupname)
      MENU_GROUP_COMMAND:New(group, "Abort Holding",    rootmenu, self._PlayerAbortHolding,   self, groupname)
      if player.parking then
        MENU_GROUP_COMMAND:New(group, "Cancel Parking",    rootmenu, self._PlayerCancelParking, self, groupname)
      else
        MENU_GROUP_COMMAND:New(group, "Reserve Parking",  rootmenu, self._PlayerRequestParking, self, groupname)
      end

    elseif flight:IsLanding(player) then
      ---
      -- Landing
      ---

      MENU_GROUP_COMMAND:New(group, "Abort Landing",   rootmenu, self._PlayerAbortLanding,   self, groupname)
      if player.parking then
        MENU_GROUP_COMMAND:New(group, "Cancel Parking",    rootmenu, self._PlayerCancelParking, self, groupname)
      else
        MENU_GROUP_COMMAND:New(group, "Reserve Parking", rootmenu, self._PlayerRequestParking, self, groupname)
      end
      
    elseif flight:IsLanded(player) then
      ---
      -- Landed
      ---      
      
      MENU_GROUP_COMMAND:New(group, "Arrived at Parking", rootmenu, self._PlayerArrived,        self, groupname)
      if player.parking then
        MENU_GROUP_COMMAND:New(group, "Cancel Parking",    rootmenu, self._PlayerCancelParking, self, groupname)
      else
        MENU_GROUP_COMMAND:New(group, "Reserve Parking",    rootmenu, self._PlayerRequestParking, self, groupname)
      end
      
    elseif flight:IsArrived(player) then
      ---
      -- Arrived (at Parking)
      ---      
      
      if status==FLIGHTCONTROL.FlightStatus.READYTX then
        MENU_GROUP_COMMAND:New(group, "Abort Taxi",    rootmenu, self._PlayerAbortTaxi,   self, groupname)
      else
        MENU_GROUP_COMMAND:New(group, "Request Taxi",  rootmenu, self._PlayerRequestTaxi, self, groupname)
      end
      
    elseif flight:IsAirborne(player) then
      ---
      -- Airborne
      ---
      
      -- Nothing to do.      
      
    end
    
  else
  
    ---
    -- FC is NOT controlling this flight
    ---
  
    if flight:IsAirborne() then
      MENU_GROUP_COMMAND:New(group, "Inbound", rootmenu, self._PlayerRequestInbound, self, groupname)
    end
    
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Player Menu: Help
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Player menu not implemented.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerNotImplemented(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
  
    local text=string.format("Sorry, this feature is not implemented yet!")
    self:TextMessageToFlight(text, flight)
  
  end
  
end

--- Player radio check.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerRadioCheck(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
      
    -- Call sign.
    local callsign=self:_GetCallsignName(flight)
    
    -- Pilot radio check.
    local text = ""
    
    if type(self.frequency) == "table" then
      local multifreq = ""
      for _,_entry in pairs(self.frequency) do
        multifreq = string.format("%s%.2f, ",multifreq,_entry)
      end
      multifreq = string.gsub(multifreq,", $","")
      text=string.format("%s, %s, radio check %s", self.alias, callsign, multifreq)
    else
      text=string.format("%s, %s, radio check %.3f", self.alias, callsign, self.frequency)
    end
    -- Radio message.
    self:TransmissionPilot(text, flight)
        
    -- Message text.
    local text=string.format("%s, %s, reading you 5",  callsign, self.alias)
        
    -- Send message.
    self:TransmissionTower(text, flight, 10)
      
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

--- Player confirm status.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerConfirmStatus(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
      
    -- Call sign.
    local callsign=self:_GetCallsignName(flight)
    
    -- Pilot requests status.
    local text=string.format("%s, %s, confirm my status", self.alias, callsign)
    
    -- Radio message.
    self:TransmissionPilot(text, flight)
    
    -- Flight status.
    local s1=flight:GetState()
            
    -- Message text.
    local text=string.format("%s, %s, your current flight status is %s.",  callsign, self.alias, s1)
    
    if flight.flightcontrol then
      -- FC status.
      local s2=flight.flightcontrol:GetFlightStatus(flight)
      
      if flight.flightcontrol.airbasename==self.airbasename then
        text=text..string.format(" You are controlled by us with status %s", s2)
      else
        text=text..string.format(" You are controlled by %s with status %s", flight.flightcontrol.airbasename, s2)
      end
    else
      text=text..string.format(" You are not controlled by anyone.")
    end
        
    -- Send message.
    self:TransmissionTower(text, flight, 10)
      
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Player Menu: Info
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Player info about airbase.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerInfoAirbase(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
 
    local text=string.format("Airbase %s Info:", self.airbasename) 
    text=text..string.format("\nATC Status: %s", self:GetState())
    
    if type(self.frequency) == "table" then
      local multifreq = ""
      for i=1,#self.frequency do
        multifreq=string.format("%s%.2f %s, ",multifreq,self.frequency[i],UTILS.GetModulationName(self.modulation[i] or 0))
      end
      text=string.gsub(text,", $","")
      text=text..string.format("\nFrequencies: %s", multifreq)
    else
      text=text..string.format("\nFrequency: %.3f %s", self.frequency, UTILS.GetModulationName(self.modulation)) 
    end
    text=text..string.format("\nRunway Landing: %s", self:GetActiveRunwayText())
    text=text..string.format("\nRunway Takeoff: %s", self:GetActiveRunwayText(true))

    -- Message to flight
    self:TextMessageToFlight(text, flight, 10, true)
  
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

--- Player info about ATIS.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerInfoATIS(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
  
    local text=string.format("Airbase %s ATIS:", self.airbasename)
    local srstxt = string.format("Airbase %s ", self.airbasename)
    if self.atis then
      text=text..string.format("\nATIS %.3f MHz %s", self.atis.frequency, UTILS.GetModulationName(self.atis.modulation))
      srstxt=srstxt..string.format("ATIS %.3f Megahertz %s", self.atis.frequency, UTILS.GetModulationName(self.atis.modulation))
      if self.atis.towerfrequency then
        local tower=""
        for _,freq in pairs(self.atis.towerfrequency) do
          tower=tower..string.format("%.3f, ", freq)
        end
        text=text..string.format("\nTower %.3f MHz", self.atis.towerfrequency[1])
      end
      if self.atis.ils then
      end
      if self.atis.tacan then
        --TACAN
      end
      if self.atis.ndbinner then
      end
      if self.atis.ndbouter then
      
      end
      
    else
      text=text.." Not defined"      
    end

    -- Message to flight
    
    --self:TextMessageToFlight(text, flight, 10, true)
    -- Call sign.
    local callsign=self:_GetCallsignName(flight)
           
    -- Pilot calls inbound for landing.
    local rtext=string.format("%s, %s, request ATIS frequency.", self.alias, callsign)
    
    -- Radio message.
    self:TransmissionPilot(rtext, flight)
    if self.atis then
      self:TransmissionTower(srstxt,flight,10)
    else
      self:TransmissionTower(text,flight,10)
    end
    
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

--- Player info about traffic.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerInfoTraffic(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
  
    local Nflights= self:CountFlights()
    local NQparking=self:CountFlights(FLIGHTCONTROL.FlightStatus.PARKING)
    local NQreadytx=self:CountFlights(FLIGHTCONTROL.FlightStatus.READYTX)
    local NQtaxiout=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIOUT)
    local NQreadyto=self:CountFlights(FLIGHTCONTROL.FlightStatus.READYTO)
    local NQtakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF)
    local NQinbound=self:CountFlights(FLIGHTCONTROL.FlightStatus.INBOUND)
    local NQholding=self:CountFlights(FLIGHTCONTROL.FlightStatus.HOLDING)
    local NQlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING)
    local NQtaxiinb=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAXIINB)
    local NQarrived=self:CountFlights(FLIGHTCONTROL.FlightStatus.ARRIVED)
  
    --
    local text=string.format("Traffic %s airbase:", self.airbasename)
    text = text..string.format("\n- Total Flights %d", Nflights)
    if NQparking>0 then
      text=text..string.format("\n- Parking %d", NQparking)
    end
    if NQreadytx>0 then
      text=text..string.format("\n- Ready to taxi %d", NQreadytx)
    end
    if NQtaxiout>0 then
      text=text..string.format("\n- Taxi to runway %d", NQtaxiout)
    end
    if NQreadyto>0 then
      text=text..string.format("\n- Ready for takeoff %d", NQreadyto)
    end
    if NQtakeoff>0 then
      text=text..string.format("\n- Taking off %d", NQtakeoff)
    end
    if NQinbound>0 then
      text=text..string.format("\n- Inbound %d", NQinbound)
    end
    if NQholding>0 then
      text=text..string.format("\n- Holding pattern %d", NQholding)
    end
    if NQlanding>0 then
      text=text..string.format("\n- Landing %d", NQlanding)
    end
    if NQtaxiinb>0 then
      text=text..string.format("\n- Taxi to parking %d", NQtaxiinb)
    end
    if NQarrived>0 then
      text=text..string.format("\n- Arrived at parking %d", NQarrived)
    end
    
    -- Message to flight
    self:TextMessageToFlight(text, flight, 15, true)
  
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Player Menu: Inbound
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Player calls inbound.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerRequestInbound(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
      
    if flight:IsAirborne() then
      
      -- Call sign.
      local callsign=self:_GetCallsignName(flight)
            
      -- Get player element.
      local player=flight:GetPlayerElement()      
      
      -- Pilot calls inbound for landing.
      local text=string.format("%s, %s, inbound for landing", self.alias, callsign)
      
      -- Radio message.
      self:TransmissionPilot(text, flight)
      
      -- Current player coord.
      local flightcoord=flight:GetCoordinate(nil, player.name)
      
      -- Distance from player to airbase.
      local dist=flightcoord:Get2DDistance(self:GetCoordinate())
      
      if dist<UTILS.NMToMeters(50) then
      
        -- Call RTB event. This only sets the FC for AI.
        flight:RTB(self.airbase)
                
        -- Get holding point.
        local stack=self:_GetHoldingStack(flight)        
        
        if stack then
        
          -- Set flight.
          stack.flightgroup=flight
          
          -- Stack.
          flight.stack=stack
          
          -- Heading to holding point.
          local heading=flightcoord:HeadingTo(stack.pos0)
          
          -- Distance to holding point.
          local distance=flightcoord:Get2DDistance(stack.pos0)
          
          local dist=UTILS.MetersToNM(distance)
      
          -- Message text.
          local text=string.format("%s, %s, roger, fly heading %03d for %d nautical miles, hold at angels %d. Report entering the pattern.", 
          callsign, self.alias, heading, dist, stack.angels)
          
          -- Send message.
          self:TransmissionTower(text, flight, 10)

          -- Set flightcontrol for this flight. This also updates the menu.
          flight:SetFlightControl(self)

          -- Add flight to inbound queue.
          self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.INBOUND)
            
        else
        
          -- Message text.
          local text=string.format("Negative, could not get a holding stack for you! Try again later...")
          
          -- Send message.
          self:TextMessageToFlight(text, flight, 10)        
        
          -- Debug message.
          self:E(self.lid..string.format("WARNING: Could not get holding stack for flight %s", flight:GetName()))
        end
        
      else

          -- Message text.
          local text=string.format("Negative, you have to be withing 50 nautical miles of the airbase to request inbound!")
          
          -- Send message.
          self:TextMessageToFlight(text, flight, 10)
      
      end
      
    else  
      -- Error you are not airborne!
      local text=string.format("Negative, you must be AIRBORNE to call INBOUND!")
      
      -- Send message.
      self:TextMessageToFlight(text, flight, 10)
    end
      
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end


--- Player vector to inbound
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerVectorInbound(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
      
    -- Check if inbound, controlled and have a stack.
    if flight:IsInbound() and self:IsControlling(flight) and flight.stack then
    
      -- Call sign.
      local callsign=self:_GetCallsignName(flight)
            
      -- Get player element.
      local player=flight:GetPlayerElement()
      
      -- Current player coord.
      local flightcoord=flight:GetCoordinate(nil, player.name)
      
      -- Distance from player to airbase.
      local dist=flightcoord:Get2DDistance(self:GetCoordinate())
      
      -- Call sign.
      local callsign=self:_GetCallsignName(flight)

      -- Heading to holding point.
      local heading=flightcoord:HeadingTo(flight.stack.pos0)
      
      -- Distance to holding point in meters.
      local distance=flightcoord:Get2DDistance(flight.stack.pos0)
      
      -- Distance in NM.
      local dist=UTILS.MetersToNM(distance)
  
      -- Message text.
      local text=string.format("%s, fly heading %03d for %d nautical miles, hold at angels %d.", callsign, heading, dist, flight.stack.angels)
      
      -- Send message.
      self:TextMessageToFlight(text, flight)
      
    else
      -- Send message.
      local text="Negative, you must be INBOUND, CONTROLLED by us and have an assigned STACK!"
      self:TextMessageToFlight(text, flight)      
    end
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

--- Player aborts inbound.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerAbortInbound(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
      
    if flight:IsInbound() and self:IsControlling(flight) then
      
      -- Call sign.
      local callsign=self:_GetCallsignName(flight)
      
      -- Pilot calls inbound for landing.
      local text=string.format("%s, %s, abort inbound", self.alias, callsign)
      
      -- Radio message.
      self:TransmissionPilot(text, flight)
        
      -- Message text.
      local text=string.format("%s, %s, roger, have a nice day!",  callsign, self.alias)
          
      -- Send message.
      self:TransmissionTower(text, flight, 5)
              
      -- Set flight.
      if flight.stack then
        flight.stack.flightgroup=nil
        flight.stack=nil
      else
        self:E(self.lid.."ERROR: No stack!")
      end
            
      -- Remove flight. This also updates the menu.
      self:_RemoveFlight(flight)      
      
      -- Set flight to cruise.
      flight:Cruise()      

      -- Current base is nil.
      flight.currbase=nil
            
      -- Create player menu.
      --flight:_UpdateMenu()
            
    else
    
      -- Error you are not airborne!
      local text=string.format("Negative, you must be INBOUND and CONTROLLED by us!")
      
      -- Send message.
      self:TextMessageToFlight(text, flight, 10)
    end
      
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Player Menu: Holding
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Player calls holding.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerHolding(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
      
    if flight:IsInbound() then

      if self:IsControlling(flight) then
      
        -- Callsign.
        local callsign=self:_GetCallsignName(flight)
      
        -- Player element.
        local player=flight:GetPlayerElement()      
      
        -- Holding stack.
        local stack=flight.stack
        
        if stack then
        
          -- Pilot arrived at holding pattern.
          local text=string.format("%s, %s, arrived at holding pattern", self.alias, callsign)
          
          -- Radio message.
          self:TransmissionPilot(text, flight)        
        
          -- Current coordinate.
          local Coordinate=flight:GetCoordinate(nil, player.name)
        
          -- Distance.
          local dist=stack.pos0:Get2DDistance(Coordinate)
          
          local dmax=UTILS.NMToMeters(500)
          
          if dist<dmax then
        
            -- Message to flight
            local text=string.format("%s, roger, fly heading %d at angels %d and wait for landing clearance", callsign, stack.heading, stack.angels)
            
            -- Radio message from tower.
            self:TransmissionTower(text, flight, 10)
    
            -- Call holding event.        
            flight:Holding()
            
          else

            -- Message to flight
            local text=string.format("Negative, you have to be within %d NM of the arrival zone! You still %d NM away.", UTILS.MetersToNM(dmax), UTILS.MetersToNM(dist))
            self:TextMessageToFlight(text, flight, 10, true)
          
          end
          
        else
          -- Message to flight
          local text=string.format("Negative, we have no holding stack for you!")         
          self:TextMessageToFlight(text, flight, 10, true)
        end
                
      else
      
        -- Error: Not controlled by this FC.
        local text=string.format("Negative, you are not controlled by us!")
        
        -- Message to flight
        self:TextMessageToFlight(text, flight, 10, true)        
      
      end
    else
      -- Error you are not airborne!
      local text=string.format("Negative, you must be INBOUND to call HOLDING!")
      
          -- Message to flight
      self:TextMessageToFlight(text, flight, 10, true)
    end
    
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

--- Player aborts holding.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerAbortHolding(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
      
    if flight:IsHolding() and self:IsControlling(flight) then
      
      -- Call sign.
      local callsign=self:_GetCallsignName(flight)
      
      -- Pilot aborts holding
      local text=string.format("%s, %s, abort holding", self.alias, callsign)
      
      -- Radio message.
      self:TransmissionPilot(text, flight)
        
      -- Message text.
      local text=string.format("%s, %s, roger, have a nice day!",  callsign, self.alias)
          
      -- Send message.
      self:TransmissionTower(text, flight, 10)

      -- Not holding any more.
      flight.Tholding=nil
      
      -- Set flight to cruise. This also updates the menu.
      flight:Cruise()
      
      flight.currbase=nil

      -- Set flight.
      if flight.stack then
        flight.stack.flightgroup=nil
        flight.stack=nil
      else
        self:E(self.lid.."ERROR: No stack!")
      end

      -- Remove flight. This also updates the menu.
      self:_RemoveFlight(flight)
            
    else
    
      -- Error you are not airborne!
      local text=string.format("Negative, you must be HOLDING and CONTROLLED by us!")
      
      -- Send message.
      self:TextMessageToFlight(text, flight, 10)
    end
      
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Player Menu: Landing
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Player confirms landing.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerConfirmLanding(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
      
    if (flight:IsHolding() or flight:IsInbound()) and self:IsControlling(flight) then
      
      -- Call sign.
      local callsign=self:_GetCallsignName(flight)
      
      
      if self:GetFlightStatus(flight)==FLIGHTCONTROL.FlightStatus.LANDING then
      
        -- Runway.
        local runway=self:GetActiveRunwayText()
      
        -- Message.
        local text=string.format("Runway %s, cleared to land, %s", runway, callsign)
                  
        -- Transmit message.
        self:TransmissionPilot(text, flight)                
        
        -- Set flight to landing. This clears the stack and Tholding.
        flight:Landing()
        
        -- Message text.
        --local text=string.format("%s, continue approach.",  callsign)
            
        -- Send message.
        --self:TransmissionTower(text, flight, 10)
        
        -- Create player menu.
        flight:_UpdateMenu(0.5)
        
      else

        -- Pilot leaves pattern for landing
        local text=string.format("%s, %s, leaving pattern for landing.", self.alias, callsign)
        
        -- Radio message.
        self:TransmissionPilot(text, flight)

        -- Message text.
        local text=string.format("%s, negative! Hold position until you get clearance.",  callsign)
            
        -- Send message.
        self:TransmissionTower(text, flight, 10)            
      end
            
    else
    
      -- Error you are not airborne!
      local text=string.format("Negative, you must be HOLDING or INBOUND and CONTROLLED by us!")
      
      -- Send message.
      self:TextMessageToFlight(text, flight, 10)
    end
      
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

--- Player aborts landing.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerAbortLanding(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
  
    local flightstatus=self:GetFlightStatus(flight)
      
    if (flight:IsLanding() or flightstatus==FLIGHTCONTROL.FlightStatus.LANDING) and self:IsControlling(flight) then
      
      -- Call sign.
      local callsign=self:_GetCallsignName(flight)
      
      -- Pilot aborts landing.
      local text=string.format("%s, %s, abort landing", self.alias, callsign)
      
      -- Radio message.
      self:TransmissionPilot(text, flight)
                
      -- Message text.
      local text=string.format("%s, %s, roger, have a nice day!",  callsign, self.alias)
          
      -- Send message.
      self:TransmissionTower(text, flight, 10)
      
      -- Set flight.
      if flight.stack then
        flight.stack.flightgroup=nil
        flight.stack=nil
      end
      
      -- Not holding any more.
      flight.Tholding=nil
      
      -- Set flight to cruise.
      flight:Cruise()
      
      flight.currbase=nil
      
      -- Remove flight. This also updates the menu.
      self:_RemoveFlight(flight)
                  
    else
    
      -- Error you are not airborne!
      local text=string.format("Negative, you must be LANDING and CONTROLLED by us!")
      
      -- Send message.
      self:TextMessageToFlight(text, flight, 10)
    end
      
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

--- Player request direct approach.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerRequestDirectLanding(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
      
    if flight:IsInbound() and self:IsControlling(flight) then
      
      -- Call sign.
      local callsign=self:_GetCallsignName(flight)
      
      -- Number of flights taking off.
      local nTakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF)
      

      -- Message.
      local text=string.format("%s, request direct approach.", callsign)
                
      -- Transmit message.
      self:TransmissionPilot(text, flight)      
      
      if nTakeoff>self.NlandingTakeoff then

        -- Message text.
        local text=string.format("%s, negative! We have currently traffic taking off!",  callsign)
            
        -- Send message.
        self:TransmissionTower(text, flight, 10)
      
      else
      
        -- Runway.
        local runway=self:GetActiveRunwayText()      

        -- Message text.
        local text=string.format("%s, affirmative, runway %s. Confirm approach!",  callsign, runway)
            
        -- Send message.
        self:TransmissionTower(text, flight, 10)
        
        -- Set flight status to landing.
        self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.LANDING)
      
      end
            
    else
    
      -- Error you are not airborne!
      local text=string.format("Negative, you must be INBOUND and CONTROLLED by us!")
      
      -- Send message.
      self:TextMessageToFlight(text, flight, 10)
    end
      
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Player Menu: Taxi
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Player requests taxi.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerRequestTaxi(groupname)
  
  -- Get flight.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
  
    -- Get callsign.
    local callsign=self:_GetCallsignName(flight)
    
    -- Pilot request for taxi.
    local text=string.format("%s, %s, request taxi to runway.", self.alias, callsign)        
    self:TransmissionPilot(text, flight)
        
    if flight:IsParking() then
        
      -- Tell pilot to wait until cleared.
      local text=string.format("%s, %s, hold position until further notice.", callsign, self.alias)
      self:TransmissionTower(text, flight, 10)
      
      -- Set flight status to "Ready to Taxi".
      self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTX)
      
    elseif flight:IsTaxiing() then
    
      -- Runway for takeoff.
      local runway=self:GetActiveRunwayText(true)

      -- Tell pilot to wait until cleared.
      local text=string.format("%s, %s, taxi to runway %s, hold short.", callsign, self.alias, runway)
      self:TransmissionTower(text, flight, 10)

      -- Taxi out.    
      self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAXIOUT)
      
      -- Get player element.
      local playerElement=flight:GetPlayerElement()
      
      -- Set parking to free. Could be reserved.
      if playerElement and playerElement.parking then
        self:SetParkingFree(playerElement.parking)
      end
      
    else
      self:TextMessageToFlight(string.format("Negative, you must be PARKING to request TAXI!"), flight)
    end
    
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end  

end

--- Player aborts taxi.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerAbortTaxi(groupname)
  
  -- Get flight.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
  
    -- Get callsign.
    local callsign=self:_GetCallsignName(flight)
    
    -- Pilot request for taxi.
    local text=string.format("%s, %s, cancel my taxi request.", self.alias, callsign)
    self:TransmissionPilot(text, flight)
        
    if flight:IsParking() then
        
      -- Tell pilot remain parking.
      local text=string.format("%s, %s, roger, remain on your parking position.", callsign, self.alias)
      self:TransmissionTower(text, flight, 10)
      
      -- Set flight status to "Parking".
      self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING)
      
      -- Get player element.
      local playerElement=flight:GetPlayerElement()
      
      -- Set parking guard.
      if playerElement then
        self:SpawnParkingGuard(playerElement.unit)
      end
      
    elseif flight:IsTaxiing() then
    
      -- Tell pilot to return to parking.
      local text=string.format("%s, %s, roger, return to your parking position.", callsign, self.alias)
      self:TransmissionTower(text, flight, 10)
      
      -- Set flight status to "Taxi Inbound".
      self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAXIINB)
            
    else
      self:TextMessageToFlight(string.format("Negative, you must be PARKING or TAXIING to abort TAXI!"), flight)
    end
    
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end  

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Player Menu: Takeoff
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Player requests takeoff.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerRequestTakeoff(groupname)
      
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
  
    if flight:IsTaxiing() then
    
      -- Get callsign.
      local callsign=self:_GetCallsignName(flight)
      
      -- Pilot request for taxi.
      local text=string.format("%s, %s, ready for departure. Request takeoff.", self.alias, callsign)
      self:TransmissionPilot(text, flight)    
    
      -- Get number of flights landing.
      local Nlanding=self:CountFlights(FLIGHTCONTROL.FlightStatus.LANDING)
      
      -- Get number of flights taking off.
      local Ntakeoff=self:CountFlights(FLIGHTCONTROL.FlightStatus.TAKEOFF)
      
      --[[
      local text=""
      if Nlanding==0 and Ntakeoff==0 then
        text="No current traffic. You are cleared for takeoff."
        self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF)
      elseif Nlanding>0 and Ntakeoff>0 then
        text=string.format("Negative, we got %d flights inbound and %d outbound ahead of you. Hold position until futher notice.", Nlanding, Ntakeoff)
        self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTO)      
      elseif Nlanding>0 then
        if Nlanding==1 then
          text=string.format("Negative, we got %d flight inbound before it's your turn. Wait until futher notice.", Nlanding)
        else
          text=string.format("Negative, we got %d flights inbound. Wait until futher notice.", Nlanding)
        end
        self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTO)
      elseif Ntakeoff>0 then
        text=string.format("Negative, %d flights ahead of you are waiting for takeoff. Talk to you soon.", Ntakeoff)
        self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.READYTO)
      end
      ]]
      
      -- We only check for landing flights.
      local text=string.format("%s, %s, ", callsign, self.alias)
      if Nlanding==0 then
      
        -- No traffic.
        text=text.."no current traffic. You are cleared for takeoff."
        
        -- Set status to "Take off".
        self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAKEOFF)
      elseif Nlanding>0 then
        if Nlanding==1 then
          text=text..string.format("negative, we got %d flight inbound before it's your turn. Hold position until futher notice.", Nlanding)
        else
          text=text..string.format("negative, we got %d flights inbound. Hold positon until futher notice.", Nlanding)
        end
      end      
      
      -- Message from tower.
      self:TransmissionTower(text, flight, 10)
      
    else
      self:TextMessageToFlight(string.format("Negative, you must request TAXI before you can request TAKEOFF!"), flight)  
    end
    
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

--- Player wants to abort takeoff.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerAbortTakeoff(groupname)

  -- Get flight group.    
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then

    -- Flight status.  
    local status=self:GetFlightStatus(flight)

    -- Check that we are taking off or ready for takeoff.  
    if status==FLIGHTCONTROL.FlightStatus.TAKEOFF or status==FLIGHTCONTROL.FlightStatus.READYTO then
    
      -- Get callsign.
      local callsign=self:_GetCallsignName(flight)
      
      -- Pilot request for taxi.
      local text=string.format("%s, %s, abort takeoff.", self.alias, callsign)
      self:TransmissionPilot(text, flight)        
    
      -- Set new flight status.
      if flight:IsParking() then
      
        text=string.format("%s, %s, affirm, remain on your parking position.", callsign, self.alias)
        
        self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING)
        
        -- Get player element.
        local playerElement=flight:GetPlayerElement()
        
        -- Set parking guard.
        if playerElement then
          self:SpawnParkingGuard(playerElement.unit)
        end
        
      elseif flight:IsTaxiing() then
        text=string.format("%s, %s, roger, report whether you want to taxi back or takeoff later.", callsign, self.alias)
        self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.TAXIOUT)
      else
        env.info(self.lid.."ERROR")
      end
      
      -- Message from tower.
      self:TransmissionTower(text, flight, 10)
      
    else
      self:TextMessageToFlight("Negative, You are NOT in the takeoff queue", flight)
    end
    
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Player Menu: Parking
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Player reserves a parking spot.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerRequestParking(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
      
     -- Get callsign.
    local callsign=self:_GetCallsignName(flight)
    
    -- Get player element.
    local player=flight:GetPlayerElement()
     
    -- Set terminal type.
    local TerminalType=AIRBASE.TerminalType.FighterAircraft
    if flight.isHelo then
      TerminalType=AIRBASE.TerminalType.HelicopterUsable
    end    
        -- Current coordinate.
    local coord=flight:GetCoordinate(nil, player.name)

    -- Get spawn position if any.    
    local spot=self:_GetPlayerSpot(player.name)
    
    -- Get closest FREE parking spot if player was not spawned here or spot is already taken.
    if not spot then 
      spot=self:GetClosestParkingSpot(coord, TerminalType, AIRBASE.SpotStatus.FREE)
    end
    
    if spot then
      
      -- Message text.
      local text=string.format("%s, your assigned parking position is terminal ID %d.", callsign, spot.TerminalID)
      
      -- Transmit message.
      self:TransmissionTower(text, flight)
    
      -- If player already has a spot.
      if player.parking then
        self:SetParkingFree(player.parking)
      end
      
      -- Reserve parking for player.
      player.parking=spot
      self:SetParkingReserved(spot, player.name)
      
      -- Update menu ==> Cancel Parking.
      flight:_UpdateMenu(0.2)
      
    else
    
      -- Message text.
      local text=string.format("%s, no free parking spot available. Try again later.", callsign)
      
      -- Transmit message.
      self:TransmissionTower(text, flight)
      
    end
    
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

--- Player cancels parking spot reservation.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerCancelParking(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
      
     -- Get callsign.
    local callsign=self:_GetCallsignName(flight)
    
    -- Get player element.
    local player=flight:GetPlayerElement()
    
    -- If player already has a spot.
    if player.parking then
      self:SetParkingFree(player.parking)
      player.parking=nil
      self:TextMessageToFlight(string.format("%s, your parking spot reservation at terminal ID %d was cancelled.", callsign, player.parking.TerminalID), flight)
    else
      self:TextMessageToFlight("You did not have a valid parking spot reservation.", flight)
    end
    
    -- Update menu ==> Reserve Parking.
    flight:_UpdateMenu(0.2)
        
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

--- Player arrived at parking position.
-- @param #FLIGHTCONTROL self
-- @param #string groupname Name of the flight group.
function FLIGHTCONTROL:_PlayerArrived(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
      
    -- Player element.
    local player=flight:GetPlayerElement()

    -- Get current coordinate.
    local coord=flight:GetCoordinate(nil, player.name)
    
    -- Parking spot.
    local spot=self:_GetPlayerSpot(player.name) --#FLIGHTCONTROL.ParkingSpot
    if player.parking then
      spot=self:GetParkingSpotByID(player.parking.TerminalID)
    else
      if not spot then
        spot=self:GetClosestParkingSpot(coord)
      end
    end
        
    if spot then
    
      -- Get callsign.
      local callsign=self:_GetCallsignName(flight)
      
      -- Distance to parking spot.
      local dist=coord:Get2DDistance(spot.Coordinate)
      
      if dist<12 then
      
        -- Message text.
        local text=string.format("%s, %s, arrived at parking position. Terminal ID %d.", self.alias, callsign, spot.TerminalID)
        
        -- Transmit message.
        self:TransmissionPilot(text, flight)
        -- Message text.
        local text=""        
        if spot.ReservedBy and spot.ReservedBy~=player.name then
        
          -- Reserved by someone else.
          text=string.format("%s, this spot is already reserved for %s. Find yourself a different parking position.", callsign, self.alias, spot.ReservedBy)
        
        else
        
          -- Okay, have a drink...
          text=string.format("%s, %s, roger. Enjoy a cool bevarage in the officers' club.", callsign, self.alias)
        
          -- Set player element to parking.
          flight:ElementParking(player, spot)
          
          -- Set flight status to PARKING.
          self:SetFlightStatus(flight, FLIGHTCONTROL.FlightStatus.PARKING)
                
          -- Set parking guard.
          if player then
            self:SpawnParkingGuard(player.unit)
          end
          
        end        
                
        -- Transmit message.
        self:TransmissionTower(text, flight, 10)
        
      else
      
        -- Message text.
        local text=string.format("%s, %s, arrived at parking position.", self.alias, callsign)
        
        -- Transmit message.
        self:TransmissionPilot(text, flight)
        
        local text=""
        if spot.ReservedBy then
          if spot.ReservedBy==player.name then
            -- To far from reserved spot.
            text=string.format("%s, %s, you are still %d meters away from your reserved parking position at terminal ID %d. Continue taxiing!", callsign, self.alias, dist, spot.TerminalID)
          else
            -- Closest spot is reserved by someone else.
            --local spotFree=self:GetClosestParkingSpot(coord, nil, AIRBASE.SpotStatus.Free)          
            text=string.format("%s, %s, the closest parking spot is already reserved. Continue taxiing to a free spot!", callsign, self.alias)
          end        
        else
          -- Too far from closest spot.
          text=string.format("%s, %s, you are still %d meters away from the closest parking position. Continue taxiing to a proper spot!", callsign, self.alias, dist)
        end
        
        -- Transmit message.
        self:TransmissionTower(text, flight, 10)        
      
      end
      
    else
      -- TODO: No spot
    end
    
  else
    self:E(self.lid..string.format("Cannot find flight group %s.", tostring(groupname)))
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Flight and Element Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new flight group.
-- @param #FLIGHTCONTROL self
-- @param Wrapper.Group#GROUP group Aircraft group.
-- @return Ops.FlightGroup#FLIGHTGROUP Flight group.
function FLIGHTCONTROL:_CreateFlightGroup(group)
  
  -- Check if not already in flights
  if self:_InQueue(self.flights, group) then
    self:E(self.lid..string.format("WARNING: Flight group %s does already exist!", group:GetName()))
    return
  end
  
  -- Debug info.
  self:T(self.lid..string.format("Creating new flight for group %s of aircraft type %s.", group:GetName(), group:GetTypeName()))
  
  -- Get flightgroup from data base.
  local flight=_DATABASE:GetOpsGroup(group:GetName())
  
  -- If it does not exist yet, create one.
  if not flight then
    flight=FLIGHTGROUP:New(group:GetName())
  end
  
  -- Set flightcontrol.
  if flight.homebase and flight.homebase:GetName()==self.airbasename then
    flight:SetFlightControl(self)
  end

  return flight
end

--- Remove flight from all queues.
-- @param #FLIGHTCONTROL self
-- @param Ops.FlightGroup#FLIGHTGROUP Flight The flight to be removed.
function FLIGHTCONTROL:_RemoveFlight(Flight)
  
  -- Loop over all flights in group.
  for i,_flight in pairs(self.flights) do
    local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
    
    -- Check for name.
    if flight.groupname==Flight.groupname then
    
      -- Debug message.
      self:T(self.lid..string.format("Removing flight group %s", flight.groupname))
      
      -- Remove table entry.
      table.remove(self.flights, i)
      
      -- Remove myself.
      Flight.flightcontrol=nil
      
      -- Set flight status to unknown.
      self:SetFlightStatus(Flight, FLIGHTCONTROL.FlightStatus.UNKNOWN)
      
      return true  
    end
  end
  
  -- Debug message.
  self:E(self.lid..string.format("WARNING: Could NOT remove flight group %s", Flight.groupname))
end

--- Get flight from group. 
-- @param #FLIGHTCONTROL self
-- @param Wrapper.Group#GROUP group Group that will be removed from queue.
-- @param #table queue The queue from which the group will be removed.
-- @return Ops.FlightGroup#FLIGHTGROUP Flight group or nil.
-- @return #number Queue index or nil.
function FLIGHTCONTROL:_GetFlightFromGroup(group)

  if group then

    -- Group name
    local name=group:GetName()
    
    -- Loop over all flight groups in queue
    for i,_flight in pairs(self.flights) do
      local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
      
      if flight.groupname==name then
        return flight, i
      end
    end
  
    self:T2(self.lid..string.format("WARNING: Flight group %s could not be found in queue.", name))  
  end
  
  self:T2(self.lid..string.format("WARNING: Flight group could not be found in queue. Group is nil!"))
  return nil, nil
end

--- Get element of flight from its unit name. 
-- @param #FLIGHTCONTROL self
-- @param #string unitname Name of the unit.
-- @return Ops.OpsGroup#OPSGROUP.Element Element of the flight or nil.
-- @return #number Element index or nil.
-- @return Ops.FlightGroup#FLIGHTGROUP The Flight group or nil.
function FLIGHTCONTROL:_GetFlightElement(unitname)

  -- Get the unit.
  local unit=UNIT:FindByName(unitname)
  
  -- Check if unit exists.
  if unit then
  
    -- Get flight element from all flights.
    local flight=self:_GetFlightFromGroup(unit:GetGroup())
        
    -- Check if fight exists.
    if flight then

      -- Loop over all elements in flight group.
      for i,_element in pairs(flight.elements) do
        local element=_element --Ops.OpsGroup#OPSGROUP.Element
        
        if element.unit:GetName()==unitname then
          return element, i, flight
        end
      end
      
      self:T2(self.lid..string.format("WARNING: Flight element %s could not be found in flight group.", unitname, flight.groupname))
    end
  end
    
  return nil, nil, nil
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Check Sanity Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check status of all registered flights and do some sanity checks.
-- @param #FLIGHTCONTROL self
function FLIGHTCONTROL:_CheckFlights()

  -- First remove all dead flights.
  for i=#self.flights,1,-1 do
    local flight=self.flights[i] --Ops.FlightGroup#FLIGHTGROUP    
    if flight:IsDead() then
      self:T(self.lid..string.format("Removing DEAD flight %s", tostring(flight.groupname)))
      self:_RemoveFlight(flight)
    end  
  end

  -- Count number of players
  self.Nplayers=0
  for _,_flight in pairs(self.flights) do
    local flight=_flight --Ops.FlightGroup#FLIGHTGROUP    
    if not flight.isAI then
      self.Nplayers=self.Nplayers+1
    end    
  end

  -- Check speeding.  
  if self.speedLimitTaxi then

    for _,_flight in pairs(self.flights) do
      local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
      
      if not flight.isAI then
      
        -- Get player element.
        local playerElement=flight:GetPlayerElement()
        
        -- Current flight status.
        local flightstatus=self:GetFlightStatus(flight)
        
        if playerElement then
        
          -- Check if speeding while taxiing.
          if (flightstatus==FLIGHTCONTROL.FlightStatus.TAXIINB or flightstatus==FLIGHTCONTROL.FlightStatus.TAXIOUT) and self.speedLimitTaxi then
        
            -- Current speed in m/s.
            local speed=playerElement.unit:GetVelocityMPS()
            
            -- Current position.
            local coord=playerElement.unit:GetCoord()
            
            -- We do not want to check speed on runways.
            local onRunway=self:IsCoordinateRunway(coord)
                                   
            -- Debug output.
            self:T(self.lid..string.format("Player %s speed %.1f knots (max=%.1f) onRunway=%s", playerElement.playerName, UTILS.MpsToKnots(speed), UTILS.MpsToKnots(self.speedLimitTaxi), tostring(onRunway)))
            
            if speed and speed>self.speedLimitTaxi and not onRunway then
            
              -- Callsign.
              local callsign=self:_GetCallsignName(flight)            
            
              -- Radio text.
              local text=string.format("%s, slow down, you are taxiing too fast!", callsign)
              
              -- Radio message to player.
              self:TransmissionTower(text, flight)
              
              -- Get player data.
              local PlayerData=flight:_GetPlayerData()
              
              -- Trigger FSM speeding event.              
              self:PlayerSpeeding(PlayerData)
                        
            end
            
          end
          
        end
      end
    end
    
  end
  
end

--- Check status of all registered flights and do some sanity checks.
-- @param #FLIGHTCONTROL self
function FLIGHTCONTROL:_CheckParking()

  for TerminalID,_spot in pairs(self.parking) do
    local spot=_spot --Wrapper.Airbase#AIRBASE.ParkingSpot
  
    if spot.Reserved then
      if spot.MarkerID then
        spot.Coordinate:RemoveMark(spot.MarkerID)
      end
      spot.MarkerID=spot.Coordinate:MarkToCoalition(string.format("Parking reserved for %s", tostring(spot.Reserved)), self:GetCoalition())
    end

    -- First remove all dead flights.
    for i=1,#self.flights do
      local flight=self.flights[i] --Ops.FlightGroup#FLIGHTGROUP    
      for _,_element in pairs(flight.elements) do
        local element=_element --Ops.FlightGroup#FLIGHTGROUP.Element
        if element.parking and element.parking.TerminalID==TerminalID then
          if spot.MarkerID then
            spot.Coordinate:RemoveMark(spot.MarkerID)            
          end
          spot.MarkerID=spot.Coordinate:MarkToCoalition(string.format("Parking spot occupied by %s", tostring(element.name)), self:GetCoalition())
        end
      end
    end  

  end  
  

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Routing Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Tell AI to land at the airbase. Flight is added to the landing queue.
-- @param #FLIGHTCONTROL self
-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group.
-- @param #table parking Free parking spots table.
function FLIGHTCONTROL:_LandAI(flight, parking)

   -- Debug info.
  self:T(self.lid..string.format("Landing AI flight %s.", flight.groupname))
  

  -- Respawn?   
  local respawn=false
    
  if respawn then
  
    -- Get group template.
    local Template=flight.group:GetTemplate()
    
    -- TODO: get landing waypoints from flightgroup.
  
    -- Set route points.
    Template.route.points=wp
    
    for i,unit in pairs(Template.units) do
      local spot=parking[i] --Wrapper.Airbase#AIRBASE.ParkingSpot
      
      local element=flight:GetElementByName(unit.name)
      if element then
      
        -- Set the parking spot at the destination airbase.
        unit.parking_landing=spot.TerminalID
        
        local text=string.format("Reserving parking spot %d for unit %s", spot.TerminalID, tostring(unit.name))
        self:T(self.lid..text)
        
        -- Set parking to RESERVED.
        self:SetParkingReserved(spot, element.name)
        
      else
        env.info("FF error could not get element to assign parking!")      
      end
    end
         
    -- Debug message.
    self:TextMessageToFlight(string.format("Respawning group %s", flight.groupname), flight)
  
    --Respawn the group.
    flight:Respawn(Template)
    
  else
       
    -- Give signal to land.
    flight:ClearToLand()
    
  end
  
end

--- Get holding stack.
-- @param #FLIGHTCONTROL self
-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group.
-- @return #FLIGHTCONTROL.HoldingStack Holding point.
function FLIGHTCONTROL:_GetHoldingStack(flight)

  -- Debug message.
  self:T(self.lid..string.format("Getting holding point for flight %s", flight:GetName()))
  
  for i,_hp in pairs(self.holdingpatterns) do
    local holdingpattern=_hp --#FLIGHTCONTROL.HoldingPattern
    
    self:T(self.lid..string.format("Checking holding point %s", holdingpattern.name))
    
    for j,_stack in pairs(holdingpattern.stacks) do
      local stack=_stack --#FLIGHTCONTROL.HoldingStack
      local name=stack.flightgroup and stack.flightgroup:GetName() or "empty"
      self:T(self.lid..string.format("Stack %d: %s", j, name))
      if not stack.flightgroup then
        return stack
      end
    end
  
  end

  return nil
end


--- Count flights in holding pattern.
-- @param #FLIGHTCONTROL self
-- @param #FLIGHTCONTROL.HoldingPattern Pattern The pattern.
-- @return #FLIGHTCONTROL.HoldingStack Holding point.
function FLIGHTCONTROL:_CountFlightsInPattern(Pattern)

  local N=0
        
  for _,_stack in pairs(Pattern.stacks) do
    local stack=_stack --#FLIGHTCONTROL.HoldingStack
    if stack.flightgroup then
      N=N+1
    end
  end
  
  return N
end


--- AI flight on final.
-- @param #FLIGHTCONTROL self
-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:_FlightOnFinal(flight)

  -- Callsign.
  local callsign=self:_GetCallsignName(flight)

  -- Message text.
  local text=string.format("%s, final", callsign)
  
  -- Transmit message.
  self:TransmissionPilot(text, flight)

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Radio Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Radio transmission from tower.
-- @param #FLIGHTCONTROL self
-- @param #string Text The text to transmit.
-- @param Ops.FlightGroup#FLIGHTGROUP Flight The flight.
-- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec.
function FLIGHTCONTROL:TransmissionTower(Text, Flight, Delay)

  if self.radioOnlyIfPlayers==true and self.Nplayers==0 then
    self:T(self.lid.."No players ==> skipping TOWER radio transmission")
    return
  end 

  -- Spoken text.
  local text=self:_GetTextForSpeech(Text)
    
  -- "Subtitle".
  local subgroups=nil
  if Flight and not Flight.isAI then
    local playerData=Flight:_GetPlayerData()
    if playerData.subtitles and (not self.nosubs) then
      subgroups=subgroups or {}
      table.insert(subgroups, Flight.group)
    end
  end
  
  -- New transmission.  
  local transmission=self.msrsqueue:NewTransmission(text, nil, self.msrsTower, nil, 1, subgroups, Text)
  
  -- Set time stamp. Can be in the future.
  self.Tlastmessage=timer.getAbsTime() + (Delay or 0)
  
  -- Debug message.
  self:T(self.lid..string.format("Radio Tower: %s", Text))

end

--- Radio transmission.
-- @param #FLIGHTCONTROL self
-- @param #string Text The text to transmit.
-- @param Ops.FlightGroup#FLIGHTGROUP Flight The flight.
-- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec.
function FLIGHTCONTROL:TransmissionPilot(Text, Flight, Delay)

  if self.radioOnlyIfPlayers==true and self.Nplayers==0 then
    self:T(self.lid.."No players ==> skipping PILOT radio transmission")
    return
  end 


  -- Get player data.
  local playerData=Flight:_GetPlayerData()
    
  -- Check if player enabled his "voice".
  if playerData==nil or playerData.myvoice then

    -- Spoken text.
    local text=self:_GetTextForSpeech(Text)
    
    -- MSRS instance to use.
    local msrs=self.msrsPilot -- Sound.SRS#MSRS
 
    if Flight.useSRS and Flight.msrs then
      
      -- Pilot radio call using settings of the FLIGHTGROUP. We just overwrite the frequency.
      msrs=Flight.msrs
       
    end
        
    -- "Subtitle".
    local subgroups=nil
    if Flight and not Flight.isAI then
      local playerData=Flight:_GetPlayerData()
      if playerData.subtitles and (not self.nosubs) then
        subgroups=subgroups or {}
        table.insert(subgroups, Flight.group)
      end
    end      
    
    -- Add transmission to msrsqueue.
    local coordinate = Flight:GetCoordinate(true)
    msrs:SetCoordinate()
    self.msrsqueue:NewTransmission(text, nil, msrs, nil, 1, subgroups, Text, nil, self.frequency, self.modulation)
    
  end

  -- Set time stamp.
  self.Tlastmessage=timer.getAbsTime() + (Delay or 0)

  -- Debug message.
  self:T(self.lid..string.format("Radio Pilot: %s", Text))
  
end


--- Text message to group.
-- @param #FLIGHTCONTROL self
-- @param #string Text The text to transmit.
-- @param Ops.FlightGroup#FLIGHTGROUP Flight The flight.
-- @param #number Duration Duration in seconds. Default 5.
-- @param #boolean Clear Clear screen.
-- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec.
function FLIGHTCONTROL:TextMessageToFlight(Text, Flight, Duration, Clear, Delay)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, FLIGHTCONTROL.TextMessageToFlight, self, Text, Flight, Duration, Clear, 0)
  else

    if Flight and Flight.group and Flight.group:IsAlive() then
    
      -- Group ID.
      local gid=Flight.group:GetID()
    
      -- Out text.
      trigger.action.outTextForGroup(gid, self:_CleanText(Text), Duration or 5, Clear)
      
    end
    
  end

end

--- Clean text. Remove control sequences.
-- @param #FLIGHTCONTROL self
-- @param #string Text The text.
-- @param #string Cleaned text.
function FLIGHTCONTROL:_CleanText(Text)

  local text=Text:gsub("\n$",""):gsub("\n$","")

  return text
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- [INTERNAL] Add parking guard in front of a parking aircraft - delayed for MP.
-- @param #FLIGHTCONTROL self
-- @param Wrapper.Unit#UNIT unit The aircraft.
function FLIGHTCONTROL:_SpawnParkingGuard(unit)
      -- Position of the unit.
    local coordinate=unit:GetCoordinate()

    -- Parking spot.
    local spot=self:GetClosestParkingSpot(coordinate)
    
    if not spot.ParkingGuard then
    
      -- Current heading of the unit.
      local heading=unit:GetHeading()
      
      -- Length of the unit + 3 meters.
      local size, x, y, z=unit:GetObjectSize()
      
      local xdiff = 3
      --Fix for hangars, puts the guy out front and not on top.
      if AIRBASE._CheckTerminalType(spot.TerminalType, AIRBASE.TerminalType.Shelter) then 
          xdiff = 27-(x*0.5)              
      end
      
      if (AIRBASE._CheckTerminalType(spot.TerminalType, AIRBASE.TerminalType.OpenMed) or AIRBASE._CheckTerminalType(spot.TerminalType, AIRBASE.TerminalType.Shelter)) and self.airbasename == AIRBASE.Sinai.Ramon_Airbase then 
          xdiff = 12          
      end      
      
      -- Debug message.
      self:T2(self.lid..string.format("Parking guard for %s: heading=%d, length x=%.1f m, xdiff=%.1f m", unit:GetName(), heading, x, xdiff))
      
      -- Coordinate for the guard.
      local Coordinate=coordinate:Translate(x*0.5+xdiff, heading)
      
      -- Let him face the aircraft.
      local lookat=heading-180
      
      -- Set heading and AI off to save resources.
      self.parkingGuard:InitHeading(lookat)
      
      -- Turn AI Off.
      if self.parkingGuard:IsInstanceOf("SPAWN") then
        --self.parkingGuard:InitAIOff()
      end
      
      -- Group that is spawned.
      spot.ParkingGuard=self.parkingGuard:SpawnFromCoordinate(Coordinate)
      
    else
      self:E(self.lid.."ERROR: Parking Guard already exists!")
    end
end

--- Add parking guard in front of a parking aircraft.
-- @param #FLIGHTCONTROL self
-- @param Wrapper.Unit#UNIT unit The aircraft.
function FLIGHTCONTROL:SpawnParkingGuard(unit)
  
  if unit and self.parkingGuard then
    
    -- Schedule delay so in MP we get the heading of the client's plane
    self:ScheduleOnce(1,FLIGHTCONTROL._SpawnParkingGuard,self,unit)
    
  end
    
end

--- Remove parking guard.
-- @param #FLIGHTCONTROL self
-- @param #FLIGHTCONTROL.ParkingSpot spot
-- @param #number delay Delay in seconds.
function FLIGHTCONTROL:RemoveParkingGuard(spot, delay)

  if delay and delay>0 then
    self:ScheduleOnce(delay, FLIGHTCONTROL.RemoveParkingGuard, self, spot)
  else
  
    if spot.ParkingGuard then
      spot.ParkingGuard:Destroy()
      spot.ParkingGuard=nil
    end
    
  end

end

--- Check if a flight is on a runway
-- @param #FLIGHTCONTROL self
-- @param Ops.FlightGroup#FLIGHTGROUP flight
-- @param Wrapper.Airbase#AIRBASE.Runway Runway or nil.
function FLIGHTCONTROL:_IsFlightOnRunway(flight)

  for _,_runway in pairs(self.airbase.runways) do
    local runway=_runway --Wrapper.Airbase#AIRBASE.Runway
    
    local inzone=flight:IsInZone(runway.zone)
    
    if inzone then
      return runway
    end
    
  end

  return nil
end

--- [User] Set callsign options for TTS output. See @{Wrapper.Group#GROUP.GetCustomCallSign}() on how to set customized callsigns.
-- @param #FLIGHTCONTROL self
-- @param #boolean ShortCallsign If true, only call out the major flight number. Default = `true`.
-- @param #boolean Keepnumber If true, keep the **customized callsign** in the #GROUP name for players as-is, no amendments or numbers. Default = `true`.
-- @param #table CallsignTranslations (optional) Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized
-- callsigns from playername or group name.
-- @return #FLIGHTCONTROL self
function FLIGHTCONTROL:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations)
  if not ShortCallsign or ShortCallsign == false then
   self.ShortCallsign = false
  else
   self.ShortCallsign = true
  end
  self.Keepnumber = Keepnumber or false
  self.CallsignTranslations = CallsignTranslations
  return self  
end

--- Get callsign name of a given flight.
-- @param #FLIGHTCONTROL self
-- @param Ops.FlightGroup#FLIGHTGROUP flight Flight group.
-- @return #string Callsign or "Ghostrider 1-1".
function FLIGHTCONTROL:_GetCallsignName(flight)

  local callsign=flight:GetCallsignName(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations)
  
  --local name=string.match(callsign, "%a+")
  --local number=string.match(callsign, "%d+")
  
  return callsign
end


--- Get text for text-to-speech.
-- Numbers are spaced out, e.g. "Heading 180" becomes "Heading 1 8 0 ".
-- @param #FLIGHTCONTROL self
-- @param #string text Original text.
-- @return #string Spoken text.
function FLIGHTCONTROL:_GetTextForSpeech(text)

  --- Function to space out text.
  local function space(text)
    
    local res=""
    
    for i=1, #text do
      local char=text:sub(i,i)
      res=res..char.." "
    end
  
    return res
  end
  
  -- Space out numbers.
  local t=text:gsub("(%d+)", space)
  
  --TODO: 9 to niner.
  
  return t
end


--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned.
-- @param #FLIGHTCONTROL self
-- @param #string unitName Name of the player unit.
-- @return Wrapper.Unit#UNIT Unit of player or nil.
-- @return #string Name of the player or nil.
function FLIGHTCONTROL:_GetPlayerUnitAndName(unitName)

  if unitName then

    -- Get DCS unit from its name.
    local DCSunit=Unit.getByName(unitName)

    if DCSunit then

      -- Get player name if any.
      local playername=DCSunit:getPlayerName()

      -- Unit object.
      local unit=UNIT:Find(DCSunit)

      -- Check if enverything is there.
      if DCSunit and unit and playername then
        self:T(self.lid..string.format("Found DCS unit %s with player %s", tostring(unitName), tostring(playername)))
        return unit, playername
      end

    end

  end

  -- Return nil if we could not find a player.
  return nil,nil
end

--- Check holding pattern markers. Draw if they should exists and remove if they should not.
-- @param #FLIGHTCONTROL self
function FLIGHTCONTROL:_CheckMarkHoldingPatterns()

  for _,pattern in pairs(self.holdingpatterns) do
    local Pattern=pattern

    if self.markPatterns then
    
      self:_MarkHoldingPattern(Pattern)
    
    else
  
      self:_UnMarkHoldingPattern(Pattern)
  
    end

  end

end

--- Draw marks of holding pattern (if they do not exist.
-- @param #FLIGHTCONTROL self
-- @param #FLIGHTCONTROL.HoldingPattern Pattern Holding pattern table.
function FLIGHTCONTROL:_MarkHoldingPattern(Pattern)
  
  if not Pattern.markArrow then
    Pattern.markArrow=Pattern.pos0:ArrowToAll(Pattern.pos1, nil, {1,0,0}, 1, {1,1,0}, 0.5, 2, true)      
  end
  
  if not Pattern.markArrival then
    Pattern.markArrival=Pattern.arrivalzone:DrawZone()
  end
    
end

--- Removem markers of holding pattern (if they exist).
-- @param #FLIGHTCONTROL self
-- @param #FLIGHTCONTROL.HoldingPattern Pattern Holding pattern table.
function FLIGHTCONTROL:_UnMarkHoldingPattern(Pattern)
  
  if Pattern.markArrow then
    UTILS.RemoveMark(Pattern.markArrow)
    Pattern.markArrow=nil    
  end

  if Pattern.markArrival then
    UTILS.RemoveMark(Pattern.markArrival)
    Pattern.markArrival=nil    
  end
  
end

--- Add a holding pattern.
-- @param #FLIGHTCONTROL self
-- @return #FLIGHTCONTROL.HoldingPattern Holding pattern table.
function FLIGHTCONTROL:_AddHoldingPatternBackup()

  local runway=self:GetActiveRunway()
  
  local heading=runway.heading
  
  local vec2=self.airbase:GetVec2()
  
  local Vec2=UTILS.Vec2Translate(vec2, UTILS.NMToMeters(5), heading+90)
  
  local ArrivalZone=ZONE_RADIUS:New("Arrival Zone", Vec2, 5000)

  -- Add holding pattern with very low priority.
  self.holdingBackup=self:AddHoldingPattern(ArrivalZone, heading, 15, 5, 25, 999)

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Enhanced Airborne Group.
--
-- ## Main Features:
--
--    * Monitor flight status of elements and/or the entire group
--    * Monitor fuel and ammo status
--    * Conveniently set radio freqencies, TACAN, ROE etc
--    * Order helos to land at specifc coordinates
--    * Dynamically add and remove waypoints
--    * Sophisticated task queueing system (know when DCS tasks start and end)
--    * Convenient checks when the group enters or leaves a zone
--    * Detection events for new, known and lost units
--    * Simple LASER and IR-pointer setup
--    * Compatible with AUFTRAG class
--    * Many additional events that the mission designer can hook into
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/Flightgroup).
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ===
-- @module Ops.FlightGroup
-- @image OPS_FlightGroup.png


--- FLIGHTGROUP class.
-- @type FLIGHTGROUP
-- @field #string actype Type name of the aircraft.
-- @field #number rangemax Max range in meters.
-- @field #number ceiling Max altitude the aircraft can fly at in meters.
-- @field #number tankertype The refueling system type (0=boom, 1=probe), if the group is a tanker.
-- @field #number refueltype The refueling system type (0=boom, 1=probe), if the group can refuel from a tanker.
-- @field Ops.OpsGroup#OPSGROUP.Ammo ammo Ammunition data. Number of Guns, Rockets, Bombs, Missiles.
-- @field #boolean ai If true, flight is purely AI. If false, flight contains at least one human player.
-- @field #boolean fuellow Fuel low switch.
-- @field #number fuellowthresh Low fuel threshold in percent.
-- @field #boolean fuellowrtb RTB on low fuel switch.
-- @field #boolean fuelcritical Fuel critical switch.
-- @field #number fuelcriticalthresh Critical fuel threshold in percent.
-- @field #boolean fuelcriticalrtb RTB on critical fuel switch.
-- @field OPS.FlightControl#FLIGHTCONTROL flightcontrol The flightcontrol handling this group.
-- @field Ops.Airboss#AIRBOSS airboss The airboss handling this group.
-- @field Core.UserFlag#USERFLAG flaghold Flag for holding.
-- @field #number Tholding Abs. mission time stamp when the group reached the holding point.
-- @field #number Tparking Abs. mission time stamp when the group was spawned uncontrolled and is parking.
-- @field #table menu F10 radio menu.
-- @field #string controlstatus Flight control status.
-- @field #boolean despawnAfterLanding If `true`, group is despawned after landed at an airbase.
-- @field #boolean despawnAfterHolding If `true`, group is despawned after reaching the holding point.
-- @field #number RTBRecallCount Number that counts RTB calls.
-- @field OPS.FlightControl#FLIGHTCONTROL.HoldingStack stack Holding stack.
-- @field #boolean isReadyTO Flight is ready for takeoff. This is for FLIGHTCONTROL.
-- @field #boolean prohibitAB Disallow (true) or allow (false) AI to use the afterburner.
-- @field #boolean jettisonEmptyTanks Allow (true) or disallow (false) AI to jettison empty fuel tanks.
-- @field #boolean jettisonWeapons Allow (true) or disallow (false) AI to jettison weapons if in danger.
-- @field #number holdtime Time [s] flight is holding before going on final. Set to nil for indefinitely.
--
-- @extends Ops.OpsGroup#OPSGROUP

--- *To invent an airplane is nothing; to build one is something; to fly is everything.* -- Otto Lilienthal
--
-- ===
--
-- # The FLIGHTGROUP Concept
--
-- # Events
--
-- This class introduces a lot of additional events that will be handy in many situations.
-- Certain events like landing, takeoff etc. are triggered for each element and also have a corresponding event when the whole group reaches this state.
--
-- ## Spawning
--
-- ## Parking
--
-- ## Taxiing
--
-- ## Takeoff
--
-- ## Airborne
--
-- ## Landed
--
-- ## Arrived
--
-- ## Dead
--
-- ## Fuel
--
-- ## Ammo
--
-- ## Detected Units
--
-- ## Check In Zone
--
-- ## Passing Waypoint
--
--
-- # Tasking
--
-- The FLIGHTGROUP class significantly simplifies the monitoring of DCS tasks. Two types of tasks can be set
--
--     * **Scheduled Tasks**
--     * **Waypoint Tasks**
--
-- ## Scheduled Tasks
--
-- ## Waypoint Tasks
--
-- # Examples
--
-- Here are some examples to show how things are done.
--
-- ## 1. Spawn
--
--
--
-- @field #FLIGHTGROUP
FLIGHTGROUP = {
  ClassName          = "FLIGHTGROUP",
  homebase           =   nil,
  destbase           =   nil,
  homezone           =   nil,
  destzone           =   nil,
  actype             =   nil,
  speedMax           =   nil,
  rangemax           =   nil,
  ceiling            =   nil,
  fuellow            = false,
  fuellowthresh      =   nil,
  fuellowrtb         =   nil,
  fuelcritical       =   nil,
  fuelcriticalthresh =   nil,
  fuelcriticalrtb    = false,
  outofAAMrtb        = false,
  outofAGMrtb        = false,
  flightcontrol      =   nil,
  flaghold           =   nil,
  Tholding           =   nil,
  Tparking           =   nil,
  Twaiting           =   nil,
  menu               =   nil,
  isHelo             =   nil,
  RTBRecallCount     =     0,
  playerSettings     =    {},
  playerWarnings     =    {},
  prohibitAB         =   false,
  jettisonEmptyTanks =   true,
  jettisonWeapons    =   true, -- that's actually a negative option like prohibitAB  
}


--- Generalized attribute. See [DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes) on hoggit.
-- @type FLIGHTGROUP.Attribute
-- @field #string TRANSPORTPLANE Airplane with transport capability. This can be used to transport other assets.
-- @field #string AWACS Airborne Early Warning and Control System.
-- @field #string FIGHTER Fighter, interceptor, ... airplane.
-- @field #string BOMBER Aircraft which can be used for strategic bombing.
-- @field #string TANKER Airplane which can refuel other aircraft.
-- @field #string TRANSPORTHELO Helicopter with transport capability. This can be used to transport other assets.
-- @field #string ATTACKHELO Attack helicopter.
-- @field #string UAV Unpiloted Aerial Vehicle, e.g. drones.
-- @field #string OTHER Other aircraft type.
FLIGHTGROUP.Attribute = {
  TRANSPORTPLANE="TransportPlane",
  AWACS="AWACS",
  FIGHTER="Fighter",
  BOMBER="Bomber",
  TANKER="Tanker",
  TRANSPORTHELO="TransportHelo",
  ATTACKHELO="AttackHelo",
  UAV="UAV",
  OTHER="Other",
}

--- Radio Text.
-- @type FLIGHTGROUP.RadioText
-- @field #string normal
-- @field #string enhanced

--- Radio messages.
-- @type FLIGHTGROUP.RadioMessage
-- @field #FLIGHTGROUP.RadioText AIRBORNE
-- @field #FLIGHTGROUP.RadioText TAXIING
FLIGHTGROUP.RadioMessage = {
  AIRBORNE={normal="Airborn", enhanced="Airborn"},
  TAXIING={normal="Taxiing", enhanced="Taxiing"},
}

--- Skill level.
-- @type FLIGHTGROUP.PlayerSkill
-- @field #string STUDENT Flight Student. Shows tips and hints in important phases of the approach.
-- @field #string AVIATOR Naval aviator. Moderate number of hints but not really zip lip.
-- @field #string GRADUATE TOPGUN graduate. For people who know what they are doing. Nearly *ziplip*.
-- @field #string INSTRUCTOR TOPGUN instructor. For people who know what they are doing. Nearly *ziplip*.
FLIGHTGROUP.PlayerSkill = {
  STUDENT    = "Student",
  AVIATOR    = "Aviator",
  GRADUATE   = "Graduate",
  INSTRUCTOR = "Instructor",
}

--- Player data.
-- @type FLIGHTGROUP.PlayerData
-- @field #string name Player name.
-- @field #boolean subtitles Display subtitles.
-- @field #string skill Skill level.

--- FLIGHTGROUP players.
-- @field #table Players Player data.
FLIGHTGROUP.Players={}

--- FLIGHTGROUP class version.
-- @field #string version
FLIGHTGROUP.version="1.0.3"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: VTOL aircraft.
-- TODO: Mark assigned parking spot on F10 map.
-- TODO: Let user request a parking spot via F10 marker :)
-- DONE: Use new UnitLost event instead of crash/dead.
-- DONE: Monitor traveled distance in air ==> calculate fuel consumption ==> calculate range remaining. Will this give half way accurate results?
-- DONE: Out of AG/AA missiles. Safe state of out-of-ammo.
-- DONE: Add TACAN beacon.
-- DONE: Add tasks.
-- DONE: Waypoints, read, add, insert, detour.
-- DONE: Get ammo.
-- DONE: Get pylons.
-- DONE: Fuel threshhold ==> RTB.
-- DONE: ROE
-- NOGO: Respawn? With correct loadout, fuelstate. Solved in DCS 2.5.6!

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new FLIGHTGROUP object and start the FSM.
-- @param #FLIGHTGROUP self
-- @param Wrapper.Group#GROUP group The group object. Can also be given by its group name as `#string`.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:New(group)

  -- First check if we already have a flight group for this group.
  local og=_DATABASE:GetOpsGroup(group)
  if og then
    og:I(og.lid..string.format("WARNING: OPS group already exists in data base!"))
    return og
  end

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, OPSGROUP:New(group)) -- #FLIGHTGROUP

  -- Set some string id for output to DCS.log file.
  self.lid=string.format("FLIGHTGROUP %s | ", self.groupname or "N/A")

  -- Defaults
  self:SetDefaultROE()
  self:SetDefaultROT()
  self:SetDefaultEPLRS(self.isEPLRS)
  self:SetDetection()
  self:SetFuelLowThreshold()
  self:SetFuelLowRTB()
  self:SetFuelCriticalThreshold()
  self:SetFuelCriticalRTB()  

  -- Holding flag.
  self.flaghold=USERFLAG:New(string.format("%s_FlagHold", self.groupname))
  self.flaghold:Set(0)
  self.holdtime=2*60

  -- Add FSM transitions.
  --                 From State  -->   Event      -->      To State
  self:AddTransition("*",             "LandAtAirbase",     "Inbound")     -- Group is ordered to land at an airbase.
  self:AddTransition("*",             "RTB",               "Inbound")     -- Group is returning to (home/destination) airbase.
  self:AddTransition("*",             "RTZ",               "Inbound")     -- Group is returning to destination zone. Not implemented yet!
  self:AddTransition("Inbound",       "Holding",           "Holding")     -- Group is in holding pattern.

  self:AddTransition("*",             "Refuel",            "Going4Fuel")  -- Group is send to refuel at a tanker.
  self:AddTransition("Going4Fuel",    "Refueled",          "Cruising")    -- Group finished refueling.

  self:AddTransition("*",             "LandAt",            "LandingAt")   -- Helo group is ordered to land at a specific point.
  self:AddTransition("LandingAt",     "LandedAt",          "LandedAt")    -- Helo group landed landed at a specific point.

  self:AddTransition("*",             "FuelLow",           "*")          -- Fuel state of group is low. Default ~25%.
  self:AddTransition("*",             "FuelCritical",      "*")          -- Fuel state of group is critical. Default ~10%.

  self:AddTransition("Cruising",      "EngageTarget",     "Engaging")    -- Engage targets.
  self:AddTransition("Engaging",      "Disengage",        "Cruising")    -- Engagement over.

  self:AddTransition("*",             "ElementParking",   "*")           -- An element is parking.
  self:AddTransition("*",             "ElementEngineOn",  "*")           -- An element spooled up the engines.
  self:AddTransition("*",             "ElementTaxiing",   "*")           -- An element is taxiing to the runway.
  self:AddTransition("*",             "ElementTakeoff",   "*")           -- An element took off.
  self:AddTransition("*",             "ElementAirborne",  "*")           -- An element is airborne.
  self:AddTransition("*",             "ElementLanded",    "*")           -- An element landed.
  self:AddTransition("*",             "ElementArrived",   "*")           -- An element arrived.

  self:AddTransition("*",             "ElementOutOfAmmo", "*")           -- An element is completely out of ammo.

  self:AddTransition("*",             "Parking",          "Parking")     -- The whole flight group is parking.
  self:AddTransition("*",             "Taxiing",          "Taxiing")     -- The whole flight group is taxiing.
  self:AddTransition("*",             "Takeoff",          "Airborne")    -- The whole flight group is airborne.
  self:AddTransition("*",             "Airborne",         "Airborne")    -- The whole flight group is airborne.
  self:AddTransition("*",             "Cruise",           "Cruising")    -- The whole flight group is cruising.
  self:AddTransition("*",             "Landing",          "Landing")     -- The whole flight group is landing.
  self:AddTransition("*",             "Landed",           "Landed")      -- The whole flight group has landed.
  self:AddTransition("*",             "Arrived",          "Arrived")     -- The whole flight group has arrived.


  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Stop". Stops the FLIGHTGROUP and all its event handlers.
  -- @param #FLIGHTGROUP self

  --- Triggers the FSM event "Stop" after a delay. Stops the FLIGHTGROUP and all its event handlers.
  -- @function [parent=#FLIGHTGROUP] __Stop
  -- @param #FLIGHTGROUP self
  -- @param #number delay Delay in seconds.

  --- FSM Function OnAfterElementSpawned.
  -- @function [parent=#FLIGHTGROUP] OnAfterElementSpawned
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.

  --- FSM Function OnAfterElementParking.
  -- @function [parent=#FLIGHTGROUP] OnAfterElementParking
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
  -- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot.

  --- FSM Function OnAfterElementEngineOn.
  -- @function [parent=#FLIGHTGROUP] OnAfterElementEngineOn
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.

  --- FSM Function OnAfterElementTaxiing.
  -- @function [parent=#FLIGHTGROUP] OnAfterElementTaxiing
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.

  --- FSM Function OnAfterElementTakeoff.
  -- @function [parent=#FLIGHTGROUP] OnAfterElementTakeoff
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil.

  --- FSM Function OnAfterElementAirborne.
  -- @function [parent=#FLIGHTGROUP] OnAfterElementAirborne
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.

  --- FSM Function OnAfterElementLanded.
  -- @function [parent=#FLIGHTGROUP] OnAfterElementLanded
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil.

  --- FSM Function OnAfterElementArrived.
  -- @function [parent=#FLIGHTGROUP] OnAfterElementArrived
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase, where the element arrived.
  -- @param Wrapper.Airbase#AIRBASE.ParkingSpot Parking The Parking spot the element has.

  --- FSM Function OnAfterElementDestroyed.
  -- @function [parent=#FLIGHTGROUP] OnAfterElementDestroyed
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.

  --- FSM Function OnAfterElementDead.
  -- @function [parent=#FLIGHTGROUP] OnAfterElementDead
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.

  --- FSM Function OnAfterSpawned.
  -- @function [parent=#FLIGHTGROUP] OnAfterSpawned
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterParking.
  -- @function [parent=#FLIGHTGROUP] OnAfterParking
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterTaxiing.
  -- @function [parent=#FLIGHTGROUP] OnAfterTaxiing
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterTakeoff.
  -- @function [parent=#FLIGHTGROUP] OnAfterTakeoff
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterAirborne.
  -- @function [parent=#FLIGHTGROUP] OnAfterAirborne
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterCruise.
  -- @function [parent=#FLIGHTGROUP] OnAfterCruise
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterLanding.
  -- @function [parent=#FLIGHTGROUP] OnAfterLanding
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterLanded.
  -- @function [parent=#FLIGHTGROUP] OnAfterLanded
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase the flight landed.

  --- FSM Function OnAfterLandedAt.
  -- @function [parent=#FLIGHTGROUP] OnAfterLandedAt
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterArrived.
  -- @function [parent=#FLIGHTGROUP] OnAfterArrived
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterDead.
  -- @function [parent=#FLIGHTGROUP] OnAfterDead
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterUpdateRoute.
  -- @function [parent=#FLIGHTGROUP] OnAfterUpdateRoute
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last.
  -- @param #number N Waypoint  Max waypoint index to be included in the route. Default is the final waypoint.

  --- FSM Function OnAfterOutOfMissilesAA.
  -- @function [parent=#FLIGHTGROUP] OnAfterOutOfMissilesAA
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterOutOfMissilesAG.
  -- @function [parent=#FLIGHTGROUP] OnAfterOutOfMissilesAG
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterRTB.
  -- @function [parent=#FLIGHTGROUP] OnAfterRTB
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at.
  -- @param #number SpeedTo Speed used for traveling from current position to holding point in knots. Default 75% of max speed.
  -- @param #number SpeedHold Holding speed in knots. Default 250 kts.
  -- @param #number SpeedLand Landing speed in knots. Default 170 kts.

  --- FSM Function OnAfterLandAtAirbase.
  -- @function [parent=#FLIGHTGROUP] OnAfterLandAtAirbase
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at.

  --- FSM Function OnAfterWait.
  -- @function [parent=#FLIGHTGROUP] OnAfterWait
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever).
  -- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos.
  -- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos.

  --- FSM Function OnAfterRefuel.
  -- @function [parent=#FLIGHTGROUP] OnAfterRefuel
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Point#COORDINATE Coordinate The coordinate.

  --- FSM Function OnAfterRefueled.
  -- @function [parent=#FLIGHTGROUP] OnAfterRefueled
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterDisengage.
  -- @function [parent=#FLIGHTGROUP] OnAfterDisengage
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Set#SET_UNIT TargetUnitSet

  --- FSM Function OnAfterEngageTarget.
  -- @function [parent=#FLIGHTGROUP] OnAfterEngageTarget
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #table Target Target object. Can be a UNIT, STATIC, GROUP, SET_UNIT or SET_GROUP object.

  --- FSM Function OnAfterLandAt.
  -- @function [parent=#FLIGHTGROUP] OnAfterLandAt
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Set#SET_UNIT TargetUnitSet

  --- FSM Function OnAfterFuelLow.
  -- @function [parent=#FLIGHTGROUP] OnAfterFuelLow
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnAfterFuelCritical.
  -- @function [parent=#FLIGHTGROUP] OnAfterFuelCritical
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- FSM Function OnBeforeUpdateRoute.
  -- @function [parent=#FLIGHTGROUP] OnBeforeUpdateRoute
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last.
  -- @param #number N Waypoint  Max waypoint index to be included in the route. Default is the final waypoint.
  -- @return #boolean Transision allowed?

  --- FSM Function OnBeforeRTB.
  -- @function [parent=#FLIGHTGROUP] OnBeforeRTB
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at.
  -- @param #number SpeedTo Speed used for travelling from current position to holding point in knots.
  -- @param #number SpeedHold Holding speed in knots.

  --- FSM Function OnBeforeLandAtAirbase.
  -- @function [parent=#FLIGHTGROUP] OnBeforeLandAtAirbase
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at.

  --- FSM Function OnBeforeWait.
  -- @function [parent=#FLIGHTGROUP] OnBeforeWait
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever).
  -- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos.
  -- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos.

  --- FSM Function OnBeforeLandAt.
  -- @function [parent=#FLIGHTGROUP] OnBeforeLandAt
  -- @param #FLIGHTGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Point#COORDINATE Coordinate The coordinate where to land. Default is current position.
  -- @param #number Duration The duration in seconds to remain on ground. Default 600 sec (10 min).

  -- TODO: Add pseudo functions ? Normally done, but should be double check

  -- Handle events:
  self:HandleEvent(EVENTS.Birth,           self.OnEventBirth)
  self:HandleEvent(EVENTS.EngineStartup,   self.OnEventEngineStartup)
  self:HandleEvent(EVENTS.Takeoff,         self.OnEventTakeOff)
  self:HandleEvent(EVENTS.Land,            self.OnEventLanding)
  self:HandleEvent(EVENTS.EngineShutdown,  self.OnEventEngineShutdown)
  self:HandleEvent(EVENTS.PilotDead,       self.OnEventPilotDead)
  self:HandleEvent(EVENTS.Ejection,        self.OnEventEjection)
  self:HandleEvent(EVENTS.Crash,           self.OnEventCrash)
  self:HandleEvent(EVENTS.RemoveUnit,      self.OnEventRemoveUnit)
  self:HandleEvent(EVENTS.UnitLost,        self.OnEventUnitLost)
  self:HandleEvent(EVENTS.Kill,            self.OnEventKill)
  self:HandleEvent(EVENTS.PlayerLeaveUnit, self.OnEventPlayerLeaveUnit)

  -- Initialize group.
  self:_InitGroup()

  -- Init waypoints.
  self:_InitWaypoints()



  -- Start the status monitoring.
  self.timerStatus=TIMER:New(self.Status, self):Start(1, 30)

  -- Start queue update timer.
  self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5)

  -- Start check zone timer.
  self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(3, 10)

  -- Add OPSGROUP to _DATABASE.
  _DATABASE:AddOpsGroup(self)

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Add an *enroute* task to attack targets in a certain **circular** zone.
-- @param #FLIGHTGROUP self
-- @param Core.Zone#ZONE_RADIUS ZoneRadius The circular zone, where to engage targets.
-- @param #table TargetTypes (Optional) The target types, passed as a table, i.e. mind the curly brackets {}. Default {"Air"}.
-- @param #number Priority (Optional) Priority. Default 0.
function FLIGHTGROUP:AddTaskEnrouteEngageTargetsInZone(ZoneRadius, TargetTypes, Priority)
  local Task=self.group:EnRouteTaskEngageTargetsInZone(ZoneRadius:GetVec2(), ZoneRadius:GetRadius(), TargetTypes, Priority)
  self:AddTaskEnroute(Task)
end

--- Get airwing the flight group belongs to.
-- @param #FLIGHTGROUP self
-- @return Ops.Airwing#AIRWING The AIRWING object (if any).
function FLIGHTGROUP:GetAirwing()
  return self.legion
end

--- Get name of airwing the flight group belongs to.
-- @param #FLIGHTGROUP self
-- @return #string Name of the airwing or "None" if the flightgroup does not belong to any airwing.
function FLIGHTGROUP:GetAirwingName()
  local name=self.legion and self.legion.alias or "None"
  return name
end

--- Get squadron the flight group belongs to.
-- @param #FLIGHTGROUP self
-- @return Ops.Squadron#SQUADRON The SQUADRON of this flightgroup or #nil if the flightgroup does not belong to any squadron.
function FLIGHTGROUP:GetSquadron()
  return self.cohort
end

--- Get squadron name the flight group belongs to.
-- @param #FLIGHTGROUP self
-- @return #string The squadron name or "None" if the flightgroup does not belon to any squadron.
function FLIGHTGROUP:GetSquadronName()
  local name=self.cohort and self.cohort:GetName() or "None"
  return name
end

--- Set if aircraft is VTOL capable. Unfortunately, there is no DCS way to determine this via scripting.
-- @param #FLIGHTGROUP self
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetVTOL()
  self.isVTOL=true
  return self
end

--- Set if aircraft is **not** allowed to use afterburner.
-- @param #FLIGHTGROUP self
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetProhibitAfterburner()
  self.prohibitAB = true
  if self:GetGroup():IsAlive() then
    self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB, true)
  end
  return self 
end

--- Set if aircraft is allowed to use afterburner.
-- @param #FLIGHTGROUP self
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetAllowAfterburner()
  self.prohibitAB = false
  if self:GetGroup():IsAlive() then
    self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB, false)
  end
  return self 
end

--- Set if aircraft is allowed to drop empty fuel tanks - set to true to allow, and false to forbid it.
-- @param #FLIGHTGROUP self
-- @param #boolean Switch true or false
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetJettisonEmptyTanks(Switch)
  self.jettisonEmptyTanks = Switch
  if self:GetGroup():IsAlive() then
    self:GetGroup():SetOption(AI.Option.Air.id.JETT_TANKS_IF_EMPTY, Switch)
  end
  return self
end

--- Set if aircraft is allowed to drop weapons to escape danger - set to true to allow, and false to forbid it.
-- @param #FLIGHTGROUP self
-- @param #boolean Switch true or false
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetJettisonWeapons(Switch)
  self.jettisonWeapons = not Switch
  if self:GetGroup():IsAlive() then
    self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_JETT, not Switch)
  end
  return self
end

--- Set the aircraft to land straight in.
-- @param #FLIGHTGROUP self
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetOptionLandingStraightIn()
  self.OptionLandingStraightIn = true
  if self:GetGroup():IsAlive() then
    self:GetGroup():SetOptionLandingStraightIn()
  end
  return self
end

--- Set the aircraft to land in pairs.
-- @param #FLIGHTGROUP self
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetOptionLandingForcePair()
  self.OptionLandingForcePair = true
  if self:GetGroup():IsAlive() then
    self:GetGroup():SetOptionLandingForcePair()
  end
  return self
end

--- Set the aircraft to NOT land in pairs.
-- @param #FLIGHTGROUP self
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetOptionLandingRestrictPair()
  self.OptionLandingRestrictPair = true
  if self:GetGroup():IsAlive() then
    self:GetGroup():SetOptionLandingRestrictPair()
  end
  return self
end

--- Set the aircraft to land after overhead break.
-- @param #FLIGHTGROUP self
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetOptionLandingOverheadBreak()
  self.OptionLandingOverheadBreak = true
  if self:GetGroup():IsAlive() then
    self:GetGroup():SetOptionLandingOverheadBreak()
  end
  return self
end

--- [HELICOPTER] Set the aircraft to prefer takeoff and landing vertically.
-- @param #FLIGHTGROUP self
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetOptionPreferVertical()
  self.OptionPreferVertical = true
  if self:GetGroup():IsAlive() then
    self:GetGroup():OptionPreferVerticalLanding()
  end
  return self
end

--- Set if group is ready for taxi/takeoff if controlled by a `FLIGHTCONTROL`.
-- @param #FLIGHTGROUP self
-- @param #boolean ReadyTO If `true`, flight is ready for takeoff.
-- @param #number Delay Delay in seconds before value is set. Default 0 sec.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetReadyForTakeoff(ReadyTO, Delay)
  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, FLIGHTGROUP.SetReadyForTakeoff, self, ReadyTO, 0)
  else
    self:T(self.lid.."Set Ready for Takeoff switch for flightcontrol")
    self.isReadyTO=ReadyTO
  end
  return self
end

--- Set the FLIGHTCONTROL controlling this flight group.
-- @param #FLIGHTGROUP self
-- @param OPS.FlightControl#FLIGHTCONTROL flightcontrol The FLIGHTCONTROL object.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetFlightControl(flightcontrol)

  -- Check if there is already a FC.
  if self.flightcontrol then
    if self.flightcontrol:IsControlling(self) then
      -- Flight control is already controlling this flight!
      return
    else
      -- Remove flight from previous FC.
      self.flightcontrol:_RemoveFlight(self)
    end
  end

  -- Set FC.
  self:T(self.lid..string.format("Setting FLIGHTCONTROL to airbase %s", flightcontrol.airbasename))
  self.flightcontrol=flightcontrol

  -- Add flight to all flights.
  if not flightcontrol:IsFlight(self) then
    table.insert(flightcontrol.flights, self)
  end

  return self
end

--- Get the FLIGHTCONTROL controlling this flight group.
-- @param #FLIGHTGROUP self
-- @return OPS.FlightControl#FLIGHTCONTROL The FLIGHTCONTROL object.
function FLIGHTGROUP:GetFlightControl()
  return self.flightcontrol
end


--- Set the homebase.
-- @param #FLIGHTGROUP self
-- @param Wrapper.Airbase#AIRBASE HomeAirbase The home airbase.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetHomebase(HomeAirbase)
  if type(HomeAirbase)=="string" then
    HomeAirbase=AIRBASE:FindByName(HomeAirbase)
  end
  self.homebase=HomeAirbase
  return self
end

--- Set the destination airbase. This is where the flight will go, when the final waypoint is reached.
-- @param #FLIGHTGROUP self
-- @param Wrapper.Airbase#AIRBASE DestinationAirbase The destination airbase.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetDestinationbase(DestinationAirbase)
  if type(DestinationAirbase)=="string" then
    DestinationAirbase=AIRBASE:FindByName(DestinationAirbase)
  end
  self.destbase=DestinationAirbase
  return self
end


--- Set the AIRBOSS controlling this flight group.
-- @param #FLIGHTGROUP self
-- @param Ops.Airboss#AIRBOSS airboss The AIRBOSS object.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetAirboss(airboss)
  self.airboss=airboss
  return self
end

--- Set low fuel threshold. Triggers event "FuelLow" and calls event function "OnAfterFuelLow".
-- @param #FLIGHTGROUP self
-- @param #number threshold Fuel threshold in percent. Default 25 %.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetFuelLowThreshold(threshold)
  self.fuellowthresh=threshold or 25
  return self
end

--- Set if low fuel threshold is reached, flight goes RTB.
-- @param #FLIGHTGROUP self
-- @param #boolean switch If true or nil, flight goes RTB. If false, turn this off.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetFuelLowRTB(switch)
  if switch==false then
    self.fuellowrtb=false
  else
    self.fuellowrtb=true
  end
  return self
end

--- Set if flight is out of Air-Air-Missiles, flight goes RTB.
-- @param #FLIGHTGROUP self
-- @param #boolean switch If true or nil, flight goes RTB. If false, turn this off.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetOutOfAAMRTB(switch)
  if switch==false then
    self.outofAAMrtb=false
  else
    self.outofAAMrtb=true
  end
  return self
end

--- Set if flight is out of Air-Ground-Missiles, flight goes RTB.
-- @param #FLIGHTGROUP self
-- @param #boolean switch If true or nil, flight goes RTB. If false, turn this off.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetOutOfAGMRTB(switch)
  if switch==false then
    self.outofAGMrtb=false
  else
    self.outofAGMrtb=true
  end
  return self
end

--- Set if low fuel threshold is reached, flight tries to refuel at the neares tanker.
-- @param #FLIGHTGROUP self
-- @param #boolean switch If true or nil, flight goes for refuelling. If false, turn this off.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetFuelLowRefuel(switch)
  if switch==false then
    self.fuellowrefuel=false
  else
    self.fuellowrefuel=true
  end
  return self
end

--- Set fuel critical threshold. Triggers event "FuelCritical" and event function "OnAfterFuelCritical".
-- @param #FLIGHTGROUP self
-- @param #number threshold Fuel threshold in percent. Default 10 %.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetFuelCriticalThreshold(threshold)
  self.fuelcriticalthresh=threshold or 10
  return self
end

--- Set if critical fuel threshold is reached, flight goes RTB.
-- @param #FLIGHTGROUP self
-- @param #boolean switch If true or nil, flight goes RTB. If false, turn this off.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetFuelCriticalRTB(switch)
  if switch==false then
    self.fuelcriticalrtb=false
  else
    self.fuelcriticalrtb=true
  end
  return self
end


--- Enable that the group is despawned after landing. This can be useful to avoid DCS taxi issues with other AI or players or jamming taxiways.
-- @param #FLIGHTGROUP self
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetDespawnAfterLanding()
  self.despawnAfterLanding=true
  return self
end

--- Enable that the group is despawned after holding. This can be useful to avoid DCS taxi issues with other AI or players or jamming taxiways.
-- @param #FLIGHTGROUP self
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:SetDespawnAfterHolding()
  self.despawnAfterHolding=true
  return self
end


--- Check if flight is parking.
-- @param #FLIGHTGROUP self
-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element.
-- @return #boolean If true, flight is parking after spawned.
function FLIGHTGROUP:IsParking(Element)
  local is=self:Is("Parking")
  if Element then
    is=Element.status==OPSGROUP.ElementStatus.PARKING
  end
  return is 
end

--- Check if is taxiing to the runway.
-- @param #FLIGHTGROUP self
-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element.
-- @return #boolean If true, flight is taxiing after engine start up.
function FLIGHTGROUP:IsTaxiing(Element)
  local is=self:Is("Taxiing")
  if Element then
    is=Element.status==OPSGROUP.ElementStatus.TAXIING
  end
  return is
end

--- Check if flight is airborne or cruising.
-- @param #FLIGHTGROUP self
-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element.
-- @return #boolean If true, flight is airborne.
function FLIGHTGROUP:IsAirborne(Element)
  local is=self:Is("Airborne") or self:Is("Cruising")
  if Element then
    is=Element.status==OPSGROUP.ElementStatus.AIRBORNE
  end
  return is 
end

--- Check if flight is airborne or cruising.
-- @param #FLIGHTGROUP self
-- @return #boolean If true, flight is airborne.
function FLIGHTGROUP:IsCruising()
  local is=self:Is("Cruising")
  return is
end

--- Check if flight is landing.
-- @param #FLIGHTGROUP self
-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element.
-- @return #boolean If true, flight is landing, i.e. on final approach.
function FLIGHTGROUP:IsLanding(Element)
  local is=self:Is("Landing")
  if Element then
    is=Element.status==OPSGROUP.ElementStatus.LANDING
  end
  return is 
end

--- Check if flight has landed and is now taxiing to its parking spot.
-- @param #FLIGHTGROUP self
-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element.
-- @return #boolean If true, flight has landed
function FLIGHTGROUP:IsLanded(Element)
  local is=self:Is("Landed")
  if Element then
    is=Element.status==OPSGROUP.ElementStatus.LANDED
  end
  return is 
end

--- Check if flight has arrived at its destination parking spot.
-- @param #FLIGHTGROUP self
-- @param Ops.OpsGroup#OPSGROUP.Element Element (Optional) Only check status for given element.
-- @return #boolean If true, flight has arrived at its destination and is parking.
function FLIGHTGROUP:IsArrived(Element)
  local is=self:Is("Arrived")
  if Element then
    is=Element.status==OPSGROUP.ElementStatus.ARRIVED
  end
  return is 
end

--- Check if flight is inbound and traveling to holding pattern.
-- @param #FLIGHTGROUP self
-- @return #boolean If true, flight is holding.
function FLIGHTGROUP:IsInbound()
  local is=self:Is("Inbound")
  return is
end

--- Check if flight is holding and waiting for landing clearance.
-- @param #FLIGHTGROUP self
-- @return #boolean If true, flight is holding.
function FLIGHTGROUP:IsHolding()
  local is=self:Is("Holding")
  return is
end

--- Check if flight is going for fuel.
-- @param #FLIGHTGROUP self
-- @return #boolean If true, flight is refueling.
function FLIGHTGROUP:IsGoing4Fuel()
  local is=self:Is("Going4Fuel")
  return is
end

--- Check if helo(!) flight is ordered to land at a specific point.
-- @param #FLIGHTGROUP self
-- @return #boolean If true, group has task to land somewhere.
function FLIGHTGROUP:IsLandingAt()
  local is=self:Is("LandingAt")
  return is
end

--- Check if helo(!) flight has landed at a specific point.
-- @param #FLIGHTGROUP self
-- @return #boolean If true, has landed somewhere.
function FLIGHTGROUP:IsLandedAt()
  local is=self:Is("LandedAt")
  return is
end

--- Check if flight is low on fuel.
-- @param #FLIGHTGROUP self
-- @return #boolean If true, flight is low on fuel.
function FLIGHTGROUP:IsFuelLow()
  return self.fuellow
end

--- Check if flight is critical on fuel.
-- @param #FLIGHTGROUP self
-- @return #boolean If true, flight is critical on fuel.
function FLIGHTGROUP:IsFuelCritical()
  return self.fuelcritical
end

--- Check if flight is good on fuel (not below low or even critical state).
-- @param #FLIGHTGROUP self
-- @return #boolean If true, flight is good on fuel.
function FLIGHTGROUP:IsFuelGood()
  local isgood=not (self.fuellow or self.fuelcritical)
  return isgood
end


--- Check if flight can do air-to-ground tasks.
-- @param #FLIGHTGROUP self
-- @param #boolean ExcludeGuns If true, exclude gun
-- @return #boolean *true* if has air-to-ground weapons.
function FLIGHTGROUP:CanAirToGround(ExcludeGuns)
  local ammo=self:GetAmmoTot()
  if ExcludeGuns then
    return ammo.MissilesAG+ammo.Rockets+ammo.Bombs>0
  else
    return ammo.MissilesAG+ammo.Rockets+ammo.Bombs+ammo.Guns>0
  end
end

--- Check if flight can do air-to-air attacks.
-- @param #FLIGHTGROUP self
-- @param #boolean ExcludeGuns If true, exclude available gun shells.
-- @return #boolean *true* if has air-to-ground weapons.
function FLIGHTGROUP:CanAirToAir(ExcludeGuns)
  local ammo=self:GetAmmoTot()
  if ExcludeGuns then
    return ammo.MissilesAA>0
  else
    return ammo.MissilesAA+ammo.Guns>0
  end
end



--- Start an *uncontrolled* group.
-- @param #FLIGHTGROUP self
-- @param #number delay (Optional) Delay in seconds before the group is started. Default is immediately.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:StartUncontrolled(delay)

  if delay and delay>0 then
    self:T2(self.lid..string.format("Starting uncontrolled group in %d seconds", delay))
    self:ScheduleOnce(delay, FLIGHTGROUP.StartUncontrolled, self)
  else

    local alive=self:IsAlive()

    if alive~=nil then
      -- Check if group is already active.
      local _delay=0
      if alive==false then
        self:Activate()
        _delay=1
      end
      self:T(self.lid.."Starting uncontrolled group")
      self.group:StartUncontrolled(_delay)
      self.isUncontrolled=false
    else
      self:T(self.lid.."ERROR: Could not start uncontrolled group as it is NOT alive!")
    end

  end

  return self
end

--- Clear the group for landing when it is holding.
-- @param #FLIGHTGROUP self
-- @param #number Delay Delay in seconds before landing clearance is given.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:ClearToLand(Delay)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, FLIGHTGROUP.ClearToLand, self)
  else

    if self:IsHolding() then
    
      -- Set flag.
      self:T(self.lid..string.format("Clear to land ==> setting holding flag to 1 (true)"))
      self.flaghold:Set(1)
      
      -- Not holding any more.
      self.Tholding=nil
      
      -- Clear holding stack.
      if self.stack then
        self.stack.flightgroup=nil
        self.stack=nil
      end
      
    end

  end
  return self
end

--- Get min fuel of group. This returns the relative fuel amount of the element lowest fuel in the group.
-- @param #FLIGHTGROUP self
-- @return #number Relative fuel in percent.
function FLIGHTGROUP:GetFuelMin()

  local fuelmin=math.huge
  for i,_element in pairs(self.elements) do
    local element=_element --Ops.OpsGroup#OPSGROUP.Element

    local unit=element.unit

    local life=unit:GetLife()

    if unit and unit:IsAlive() and life>1 then
      local fuel=unit:GetFuel()
      if fuel<fuelmin then
        fuelmin=fuel
      end
    end

  end

  return fuelmin*100
end

--- Get number of kills of this group.
-- @param #FLIGHTGROUP self
-- @return #number Number of units killed.
function FLIGHTGROUP:GetKills()
  return self.Nkills
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Status update.
-- @param #FLIGHTGROUP self
function FLIGHTGROUP:Status()

  -- FSM state.
  local fsmstate=self:GetState()
  
  -- Is group alive?
  local alive=self:IsAlive()
  
  if alive then

    -- Update position.
    self:_UpdatePosition()
  
    -- Check if group has detected any units.
    self:_CheckDetectedUnits()
    
    -- Check ammo status.
    self:_CheckAmmoStatus()
    
    -- Check damage.
    self:_CheckDamage()
    
    -- Check if stuck while taxiing.
    self:_CheckStuck()
    
    -- Get current mission (if any).
    local mission=self:GetMissionCurrent()
    
     -- TODO: Check if group is waiting?
    if self:IsWaiting() then
      if self.Twaiting and self.dTwait then
        if timer.getAbsTime()>self.Twaiting+self.dTwait then
          --self.Twaiting=nil
          --self.dTwait=nil
          --self:_CheckGroupDone()
        end
      end
    end
    
    --- check if we need to end holding
    --self:T(self.lid.."Checking if we are holding at a holding point...")
    if mission and mission.missionHoldingCoord and self.isHoldingAtHoldingPoint == true then
      self:T(self.lid.."...yes")
      if mission:IsReadyToPush() then
        --self:T(self.lid.."Ready to push -> YES")
        -- move flag to 1
        self.flaghold:Set(1)
        -- Not waiting any more.
        self.Twaiting=nil
        self.dTwait=nil
        self.isHoldingAtHoldingPoint = false
      --else
       --self:T(self.lid.."Ready to push -> NO!")
      end
    --else
     --self:T(self.lid.."...no")
    end
    
    -- If mission, check if DCS task needs to be updated.
    if mission and mission.updateDCSTask then
    
      -- Orbit missions might need updates.
      if (mission:GetType()==AUFTRAG.Type.ORBIT or mission:GetType()==AUFTRAG.Type.RECOVERYTANKER or mission:GetType()==AUFTRAG.Type.CAP) and mission.orbitVec2 then
          
        -- Get 2D vector of orbit target.
        local vec2=mission:GetTargetVec2()
        
        -- Heading.
        local hdg=mission:GetTargetHeading()
        
        -- Heading change?
        local hdgchange=false
        if mission.orbitLeg then
          if UTILS.HdgDiff(hdg, mission.targetHeading)>0 then
            hdgchange=true
          end
        end
        
        -- Distance to previous position.
        local dist=UTILS.VecDist2D(vec2, mission.orbitVec2)
        
        -- Distance change?
        local distchange=dist>mission.orbitDeltaR
        
        -- Debug info.
        self:T3(self.lid..string.format("Checking orbit mission dist=%d meters", dist))
        
        -- Check if distance is larger than threshold.
        if distchange or hdgchange then
        
          -- Debug info.
          self:T3(self.lid..string.format("Updating orbit!"))
        
          -- Update DCS task. This also sets the new mission.orbitVec2.
          local DCSTask=mission:GetDCSMissionTask() --DCS#Task
          
          -- Get task.
          local Task=mission:GetGroupWaypointTask(self)
          
          -- Reset current orbit task.
          self.controller:resetTask()
          
          -- Push task after one second. We need to give resetTask some time or it will not work!
          self:_SandwitchDCSTask(DCSTask, Task, false, 1)
          
        end

      elseif mission.type==AUFTRAG.Type.CAPTUREZONE then
       
        -- Get task.
        local Task=mission:GetGroupWaypointTask(self)
        
        -- Update task: Engage or get new zone.
        if mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING or  mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.STARTED then
          self:_UpdateTask(Task, mission)
        end
        
      end    
    end
    
  
    -- TODO: _CheckParking() function
  
    -- Check if flight began to taxi (if it was parking).
    if self:IsParking() then
      for _,_element in pairs(self.elements) do
        local element=_element --Ops.OpsGroup#OPSGROUP.Element
        
        -- Check for parking spot.
        if element.parking then
  
          -- Get distance to assigned parking spot.
          local dist=self:_GetDistToParking(element.parking, element.unit:GetCoord())
          
          -- Debug info.
          self:T(self.lid..string.format("Distance to parking spot %d = %.1f meters", element.parking.TerminalID, dist))
                      
          -- If distance >10 meters, we consider the unit as taxiing. At least for fighters, the initial distance seems to be around 1.8 meters.
          if dist>12 and element.engineOn then
            self:ElementTaxiing(element)
          end
  
        else
          --self:T(self.lid..string.format("Element %s is in PARKING queue but has no parking spot assigned!", element.name))
        end
      end
    end

  else
    -- Check damage.
    self:_CheckDamage()   
  end
    
  ---
  -- Group
  ---

  -- Short info.
  if self.verbose>=1 then

    -- Number of elements.
    local nelem=self:CountElements()
    local Nelem=#self.elements

    -- Get number of tasks and missions.
    local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks()
    local nMissions=self:CountRemainingMissison()
    
    -- ROE and Alarm State.
    local roe=self:GetROE() or -1
    local rot=self:GetROT() or -1

    -- Waypoint stuff.
    local wpidxCurr=self.currentwp
    local wpuidCurr=self:GetWaypointUIDFromIndex(wpidxCurr) or 0
    local wpidxNext=self:GetWaypointIndexNext() or 0
    local wpuidNext=self:GetWaypointUIDFromIndex(wpidxNext) or 0
    local wpN=#self.waypoints or 0
    local wpF=tostring(self.passedfinalwp)
    
    -- Speed.
    local speed=UTILS.MpsToKnots(self.velocity or 0)
    local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed())
    
    -- Altitude.
    local alt=self.position and self.position.y or 0
    
    -- Heading in degrees.
    local hdg=self.heading or 0      
    
    -- TODO: GetFormation function.
    local formation=self.option.Formation or "unknown"
    
    -- Life points.
    local life=self.life or 0
    
    -- Total ammo.
    local ammo=self:GetAmmoTot().Total
    
    -- Detected units.
    local ndetected=self.detectionOn and tostring(self.detectedunits:Count()) or "Off"
    
    -- Get cargo weight.
    local cargo=0
    for _,_element in pairs(self.elements) do
      local element=_element --Ops.OpsGroup#OPSGROUP.Element
        cargo=cargo+element.weightCargo
      end
 
       -- Home and destination base.
    local home=self.homebase and self.homebase:GetName() or "unknown"
    local dest=self.destbase and self.destbase:GetName() or "unknown"
    local curr=self.currbase and self.currbase:GetName() or "N/A"
  
    -- Info text.
    local text=string.format("%s [%d/%d]: ROE/ROT=%d/%d | T/M=%d/%d | Wp=%d[%d]-->%d[%d]/%d [%s] | Life=%.1f | v=%.1f (%d) | Hdg=%03d | Ammo=%d | Detect=%s | Cargo=%.1f | Base=%s [%s-->%s]",
    fsmstate, nelem, Nelem, roe, rot, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, wpN, wpF, life, speed, speedEx, hdg, ammo, ndetected, cargo, curr, home, dest)
    self:I(self.lid..text)

  end

  ---
  -- Elements
  ---

  if self.verbose>=2 then
    local text="Elements:"
    for i,_element in pairs(self.elements) do
      local element=_element --Ops.OpsGroup#OPSGROUP.Element

      local name=element.name
      local status=element.status
      local unit=element.unit
      local fuel=unit:GetFuel() or 0
      local life=unit:GetLifeRelative() or 0
      local lp=unit:GetLife()
      local lp0=unit:GetLife0()
      local parking=element.parking and tostring(element.parking.TerminalID) or "X"

      -- Get ammo.
      local ammo=self:GetAmmoElement(element)

      -- Output text for element.
      text=text..string.format("\n[%d] %s: status=%s, fuel=%.1f, life=%.1f [%.1f/%.1f], guns=%d, rockets=%d, bombs=%d, missiles=%d (AA=%d, AG=%d, AS=%s), parking=%s",
      i, name, status, fuel*100, life*100, lp, lp0, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, ammo.MissilesAA, ammo.MissilesAG, ammo.MissilesAS, parking)
    end
    if #self.elements==0 then
      text=text.." none!"
    end
    self:I(self.lid..text)
  end

  ---
  -- Distance travelled
  ---

  if self.verbose>=4 and alive then
  
    -- TODO: _Check distance travelled.

    -- Travelled distance since last check.
    local ds=self.travelds

    -- Time interval.
    local dt=self.dTpositionUpdate

    -- Speed.
    local v=ds/dt


    -- Max fuel time remaining.
    local TmaxFuel=math.huge

    for _,_element in pairs(self.elements) do
      local element=_element --Ops.OpsGroup#OPSGROUP.Element

      -- Get relative fuel of element.
      local fuel=element.unit:GetFuel() or 0

      -- Relative fuel used since last check.
      local dFrel=element.fuelrel-fuel

      -- Relative fuel used per second.
      local dFreldt=dFrel/dt

      -- Fuel remaining in seconds.
      local Tfuel=fuel/dFreldt

      if Tfuel<TmaxFuel then
        TmaxFuel=Tfuel
      end

      -- Element consumption.
      self:T3(self.lid..string.format("Fuel consumption %s F=%.1f: dF=%.3f  dF/min=%.3f ==> Tfuel=%.1f min", element.name, fuel*100, dFrel*100, dFreldt*100*60, Tfuel/60))

      -- Store rel fuel.
      element.fuelrel=fuel
    end

    -- Log outut.
    self:T(self.lid..string.format("Travelled ds=%.1f km dt=%.1f s ==> v=%.1f knots. Fuel left for %.1f min", self.traveldist/1000, dt, UTILS.MpsToKnots(v), TmaxFuel/60))

  end

  --- 
  -- Track flight
  ---
  if false then
  
    for _,_element in pairs(self.elements) do
      local element=_element --Ops.OpsGroup#OPSGROUP.Element
      
      local unit=element.unit
      
      if unit and unit:IsAlive() then
      
        local vec3=unit:GetVec3()
        
        if vec3 and element.pos then
        
          local id=UTILS.GetMarkID()
          
          trigger.action.lineToAll(-1, id, vec3, element.pos, {1,1,1,0.5}, 1)
        
        end
        
        element.pos=vec3
        
      end
  
    end
    
  end

  ---
  -- Fuel State
  ---

  -- TODO: _CheckFuelState() function.

  -- Only if group is in air.
  if alive and self.group:IsAirborne(true) then

    local fuelmin=self:GetFuelMin()

    -- Debug info.
    self:T2(self.lid..string.format("Fuel state=%d", fuelmin))

    if fuelmin>=self.fuellowthresh then
      self.fuellow=false
    end

    if fuelmin>=self.fuelcriticalthresh then
      self.fuelcritical=false
    end


    -- Low fuel?
    if fuelmin<self.fuellowthresh and not self.fuellow then
      self:FuelLow()
    end

    -- Critical fuel?
    if fuelmin<self.fuelcriticalthresh and not self.fuelcritical then
      self:FuelCritical()
    end

  end

  ---
  -- Airboss Helo
  ---
  if self.isHelo and self.airboss and self:IsHolding() then
    if self.airboss:IsRecovering() or self:IsFuelCritical() then
      self:ClearToLand()
    end
  end

  ---
  -- Engage Detected Targets
  ---
  if self:IsAirborne() and self:IsFuelGood() and self.detectionOn and self.engagedetectedOn then

    local targetgroup, targetdist=self:_GetDetectedTarget()

    -- If we found a group, we engage it.
    if targetgroup then
      self:T(self.lid..string.format("Engaging target group %s at distance %d meters", targetgroup:GetName(), targetdist))
      self:EngageTarget(targetgroup)
    end

  end

  ---
  -- Cargo
  ---

  self:_CheckCargoTransport()

  ---
  -- Tasks & Missions
  ---

  self:_PrintTaskAndMissionStatus()

  -- All done?
  -- Get current mission (if any).
  local mission=self:GetMissionCurrent()
  if not mission then
    self.Twaiting=nil
    self.dTwait=nil
    
    -- Check if group is done.
    -- TODO: Not sure why I introduced this here.
    self:_CheckGroupDone()
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- DCS Events ==> See also OPSGROUP
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Flightgroup event function handling the crash of a unit.
-- @param #FLIGHTGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function FLIGHTGROUP:OnEventEngineStartup(EventData)

  -- Check that this is the right group.
  if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
    local unit=EventData.IniUnit
    local group=EventData.IniGroup
    local unitname=EventData.IniUnitName

    -- Get element.
    local element=self:GetElementByName(unitname)

    if element then

      if self:IsAirborne() or self:IsInbound() or self:IsHolding() then
        -- TODO: what?
      else
        self:T3(self.lid..string.format("EVENT: Element %s started engines ==> taxiing (if AI)", element.name))
        
        -- Element started engies.
        self:ElementEngineOn(element)
        
        -- Engines are on.
        element.engineOn=true
        
        --[[
        -- TODO: could be that this element is part of a human flight group.
        -- Problem: when player starts hot, the AI does too and starts to taxi immidiately :(
        --          when player starts cold, ?
        if self.isAI then
          self:ElementEngineOn(element)
        else
          if element.ai then
            -- AI wingmen will start taxiing even if the player/client is still starting up his engines :(
            self:ElementEngineOn(element)
          end
        end
        ]]
      end

    end

  end

end

--- Flightgroup event function handling the crash of a unit.
-- @param #FLIGHTGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function FLIGHTGROUP:OnEventTakeOff(EventData)
  self:T3(self.lid.."EVENT: TakeOff")

  -- Check that this is the right group.
  if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
    local unit=EventData.IniUnit
    local group=EventData.IniGroup
    local unitname=EventData.IniUnitName

    -- Get element.
    local element=self:GetElementByName(unitname)

    if element then
      self:T2(self.lid..string.format("EVENT: Element %s took off ==> airborne", element.name))
      self:ElementTakeoff(element, EventData.Place)
    end

  end

end

--- Flightgroup event function handling the crash of a unit.
-- @param #FLIGHTGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function FLIGHTGROUP:OnEventLanding(EventData)

  -- Check that this is the right group.
  if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
    local unit=EventData.IniUnit
    local group=EventData.IniGroup
    local unitname=EventData.IniUnitName

    -- Get element.
    local element=self:GetElementByName(unitname)

    local airbase=EventData.Place

    local airbasename="unknown"
    if airbase then
      airbasename=tostring(airbase:GetName())
    end

    if element then
      self:T3(self.lid..string.format("EVENT: Element %s landed at %s ==> landed", element.name, airbasename))
      self:ElementLanded(element, airbase)
    end

  end

end

--- Flightgroup event function handling the crash of a unit.
-- @param #FLIGHTGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function FLIGHTGROUP:OnEventEngineShutdown(EventData)

  -- Check that this is the right group.
  if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
    local unit=EventData.IniUnit
    local group=EventData.IniGroup
    local unitname=EventData.IniUnitName

    -- Get element.
    local element=self:GetElementByName(unitname)

    if element then
    
      -- Engines are off.
      element.engineOn=false

      if element.unit and element.unit:IsAlive() then

        local airbase=self:GetClosestAirbase()
        local parking=self:GetParkingSpot(element, 100, airbase)

        if airbase and parking then
          self:ElementArrived(element, airbase, parking)
          self:T3(self.lid..string.format("EVENT: Element %s shut down engines ==> arrived", element.name))
        else
          self:T3(self.lid..string.format("EVENT: Element %s shut down engines but is not parking. Is it dead?", element.name))
        end

      else
        --self:T(self.lid..string.format("EVENT: Element %s shut down engines but is NOT alive ==> waiting for crash event (==> dead)", element.name))
      end

    end -- element nil?

  end

end


--- Flightgroup event function handling the crash of a unit.
-- @param #FLIGHTGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function FLIGHTGROUP:OnEventCrash(EventData)

  -- Check that this is the right group.
  if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
    local unit=EventData.IniUnit
    local group=EventData.IniGroup
    local unitname=EventData.IniUnitName

    -- Get element.
    local element=self:GetElementByName(unitname)

    if element and element.status~=OPSGROUP.ElementStatus.DEAD then
      self:T(self.lid..string.format("EVENT: Element %s crashed ==> destroyed", element.name))
      self:ElementDestroyed(element)
    end

  end

end

--- Flightgroup event function handling the crash of a unit.
-- @param #FLIGHTGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function FLIGHTGROUP:OnEventUnitLost(EventData)

  -- Check that this is the right group.
  if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
    self:T2(self.lid..string.format("EVENT: Unit %s lost at t=%.3f", EventData.IniUnitName, timer.getTime()))

    local unit=EventData.IniUnit
    local group=EventData.IniGroup
    local unitname=EventData.IniUnitName

    -- Get element.
    local element=self:GetElementByName(unitname)

    if element and element.status~=OPSGROUP.ElementStatus.DEAD then
      self:T(self.lid..string.format("EVENT: Element %s unit lost ==> destroyed t=%.3f", element.name, timer.getTime()))
      self:ElementDestroyed(element)
    end

  end

end



-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "ElementSpawned" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
function FLIGHTGROUP:onafterElementSpawned(From, Event, To, Element)

  -- Debug info.
  self:T(self.lid..string.format("Element spawned %s", Element.name))
  
  if Element.playerName then
    self:_InitPlayerData(Element.playerName)
  end

  -- Set element status.
  self:_UpdateStatus(Element, OPSGROUP.ElementStatus.SPAWNED)

  if Element.unit:InAir(not self.isHelo) then  -- Setting check because of problems with helos dynamically spawned where inAir WRONGLY returned true if spawned at an airbase or farp!
    -- Trigger ElementAirborne event. Add a little delay because spawn is also delayed!
    self:__ElementAirborne(0.11, Element)
  else

    -- Get parking spot.
    local spot=self:GetParkingSpot(Element, 10)

    if spot then

      -- Trigger ElementParking event. Add a little delay because spawn is also delayed!
      self:__ElementParking(0.11, Element, spot)

    else
      -- TODO: This can happen if spawned on deck of a carrier!
      self:T(self.lid..string.format("Element spawned not in air but not on any parking spot."))
      self:__ElementParking(0.11, Element)
    end
  end
  
end

--- On after "ElementParking" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot.
function FLIGHTGROUP:onafterElementParking(From, Event, To, Element, Spot)

  -- Set parking spot.
  if Spot then
    self:_SetElementParkingAt(Element, Spot)
  end

  -- Debug info.
  self:T(self.lid..string.format("Element parking %s at spot %s", Element.name, Element.parking and tostring(Element.parking.TerminalID) or "N/A"))

  -- Set element status.
  self:_UpdateStatus(Element, OPSGROUP.ElementStatus.PARKING)

  if self:IsTakeoffCold() then
    -- Wait for engine startup event.
  elseif self:IsTakeoffHot() then
    self:__ElementEngineOn(0.5, Element)  -- delay a bit to allow all elements
    Element.engineOn=true
  elseif self:IsTakeoffRunway() then
    self:__ElementEngineOn(0.5, Element)
    Element.engineOn=true
  end
  
end

--- On after "ElementEngineOn" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
function FLIGHTGROUP:onafterElementEngineOn(From, Event, To, Element)

  -- Debug info.
  self:T(self.lid..string.format("Element %s started engines", Element.name))

  -- Set element status.
  self:_UpdateStatus(Element, OPSGROUP.ElementStatus.ENGINEON)
  
end

--- On after "ElementTaxiing" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
function FLIGHTGROUP:onafterElementTaxiing(From, Event, To, Element)

  -- Get terminal ID.
  local TerminalID=Element.parking and tostring(Element.parking.TerminalID) or "N/A"

  -- Debug info.
  self:T(self.lid..string.format("Element taxiing %s. Parking spot %s is now free", Element.name, TerminalID))

  -- Set parking spot to free. Also for FC.
  self:_SetElementParkingFree(Element)

  -- Set element status.
  self:_UpdateStatus(Element, OPSGROUP.ElementStatus.TAXIING)
  
end

--- On after "ElementTakeoff" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
-- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil.
function FLIGHTGROUP:onafterElementTakeoff(From, Event, To, Element, airbase)
  self:T(self.lid..string.format("Element takeoff %s at %s airbase.", Element.name, airbase and airbase:GetName() or "unknown"))

  -- Helos with skids just take off without taxiing!
  if Element.parking then
    self:_SetElementParkingFree(Element)
  end

  -- Set element status.
  self:_UpdateStatus(Element, OPSGROUP.ElementStatus.TAKEOFF, airbase)

  -- Trigger element airborne event.
  self:__ElementAirborne(0.01, Element)
  
end

--- On after "ElementAirborne" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
function FLIGHTGROUP:onafterElementAirborne(From, Event, To, Element)

  -- Debug info.
  self:T2(self.lid..string.format("Element airborne %s", Element.name))
  
  -- Set parking spot to free. Also for FC. This is usually done after taxiing but doing it here in case the group is teleported.
  self:_SetElementParkingFree(Element)  

  -- Set element status.
  self:_UpdateStatus(Element, OPSGROUP.ElementStatus.AIRBORNE)
  
end

--- On after "ElementLanded" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
-- @param Wrapper.Airbase#AIRBASE airbase The airbase if applicable or nil.
function FLIGHTGROUP:onafterElementLanded(From, Event, To, Element, airbase)

  -- Debug info.
  self:T2(self.lid..string.format("Element landed %s at %s airbase", Element.name, airbase and airbase:GetName() or "unknown"))
  
  -- Set element status.
  self:_UpdateStatus(Element, OPSGROUP.ElementStatus.LANDED, airbase)

  -- Helos with skids land directly on parking spots.
  if self.isHelo then

    local Spot=self:GetParkingSpot(Element, 10, airbase)

    if Spot then
      self:_SetElementParkingAt(Element, Spot)
      self:_UpdateStatus(Element, OPSGROUP.ElementStatus.ARRIVED)
    end

  end  

  -- Despawn after landing.
  if self.despawnAfterLanding then
    
    if self.legion then
     
      if airbase and self.legion.airbase and airbase.AirbaseName==self.legion.airbase.AirbaseName then
    
        if self:IsLanded() then
          -- Everybody landed ==> Return to legion. Will despawn the last one.
          self:ReturnToLegion()
        else
          -- Despawn the element.
          self:DespawnElement(Element)
        end
        
      end
            
    else

      -- Despawn the element.
      self:DespawnElement(Element)
      
    end
  end  
end

--- On after "ElementArrived" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
-- @param Wrapper.Airbase#AIRBASE airbase The airbase, where the element arrived.
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot Parking The Parking spot the element has.
function FLIGHTGROUP:onafterElementArrived(From, Event, To, Element, airbase, Parking)
  self:T(self.lid..string.format("Element arrived %s at %s airbase using parking spot %d", Element.name, airbase and airbase:GetName() or "unknown", Parking and Parking.TerminalID or -99))

  -- Set element parking.
  self:_SetElementParkingAt(Element, Parking)

  -- Set element status.
  self:_UpdateStatus(Element, OPSGROUP.ElementStatus.ARRIVED)
end

--- On after "ElementDestroyed" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
function FLIGHTGROUP:onafterElementDestroyed(From, Event, To, Element)

  -- Call OPSGROUP function.
  self:GetParent(self).onafterElementDestroyed(self, From, Event, To, Element)

end

--- On after "ElementDead" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The flight group element.
function FLIGHTGROUP:onafterElementDead(From, Event, To, Element)

  -- Check for flight control.
  if self.flightcontrol and Element.parking then
    self.flightcontrol:SetParkingFree(Element.parking)
  end
  
  -- Call OPSGROUP function. This will remove the flightcontrol. Therefore, has to be after setting parking free.
  self:GetParent(self).onafterElementDead(self, From, Event, To, Element)  

  -- Not parking any more.
  Element.parking=nil

end


--- On after "Spawned" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterSpawned(From, Event, To)
  self:T(self.lid..string.format("Flight spawned"))
  
  -- Debug info.
  if self.verbose>=1 then
    local text=string.format("Initialized Flight Group %s:\n", self.groupname)
    text=text..string.format("Unit type     = %s\n", tostring(self.actype))
    text=text..string.format("Speed max    = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax))
    text=text..string.format("Range max    = %.1f km\n", self.rangemax/1000)
    text=text..string.format("Ceiling      = %.1f feet\n", UTILS.MetersToFeet(self.ceiling))
    text=text..string.format("Weight       = %.1f kg\n", self:GetWeightTotal())
    text=text..string.format("Cargo bay    = %.1f kg\n", self:GetFreeCargobay())
    text=text..string.format("Tanker type  = %s\n", tostring(self.tankertype))
    text=text..string.format("Refuel type  = %s\n", tostring(self.refueltype))
    text=text..string.format("AI           = %s\n", tostring(self.isAI))
    text=text..string.format("Has EPLRS    = %s\n", tostring(self.isEPLRS))    
    text=text..string.format("Helicopter   = %s\n", tostring(self.isHelo))
    text=text..string.format("Elements     = %d\n", #self.elements)
    text=text..string.format("Waypoints    = %d\n", #self.waypoints)
    text=text..string.format("Radio        = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On))
    text=text..string.format("Ammo         = %d (G=%d/R=%d/B=%d/M=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Bombs, self.ammo.Missiles)
    text=text..string.format("FSM state    = %s\n", self:GetState())
    text=text..string.format("Is alive     = %s\n", tostring(self.group:IsAlive()))
    text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated()))
    text=text..string.format("Uncontrolled = %s\n", tostring(self:IsUncontrolled()))
    text=text..string.format("Start Air    = %s\n", tostring(self:IsTakeoffAir()))
    text=text..string.format("Start Cold   = %s\n", tostring(self:IsTakeoffCold()))
    text=text..string.format("Start Hot    = %s\n", tostring(self:IsTakeoffHot()))
    text=text..string.format("Start Rwy    = %s\n", tostring(self:IsTakeoffRunway()))
    text=text..string.format("Elements:")
    for i,_element in pairs(self.elements) do
      local element=_element --Ops.OpsGroup#OPSGROUP.Element
      text=text..string.format("\n[%d] %s: callsign=%s, modex=%s, player=%s", i, element.name, tostring(element.callsign), tostring(element.modex), tostring(element.playerName))
    end
    self:I(self.lid..text)
  end  

  -- Update position.
  self:_UpdatePosition()

  -- Not dead or destroyed yet.
  self.isDead=false
  self.isDestroyed=false

  if self.isAI then
  
    -- TODO: Could be that element is spawned UNCONTROLLED.
    --       In that case, the commands are not yet used.
    --       This should be shifted to something like after ACTIVATED

    -- Set ROE.
    self:SwitchROE(self.option.ROE)

    -- Set ROT.
    self:SwitchROT(self.option.ROT)

    -- Set default EPLRS.
    self:SwitchEPLRS(self.option.EPLRS)
    
    -- Set default Invisible.
    self:SwitchInvisible(self.option.Invisible)    

    -- Set default Immortal.
    self:SwitchImmortal(self.option.Immortal)    

    -- Set Formation
    self:SwitchFormation(self.option.Formation)

    -- Set TACAN beacon.
    self:_SwitchTACAN()

    -- Set radio freq and modu.
    if self.radioDefault then
      self:SwitchRadio()
    else
      self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, self.radio.On)
    end

    -- Set callsign.
    if self.callsignDefault then
      self:SwitchCallsign(self.callsignDefault.NumberSquad, self.callsignDefault.NumberGroup)
    else
      self:SetDefaultCallsign(self.callsign.NumberSquad, self.callsign.NumberGroup)
    end

    -- TODO: make this input.
    self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_JETT, self.jettisonWeapons)
    self:GetGroup():SetOption(AI.Option.Air.id.PROHIBIT_AB,   self.prohibitAB)   -- Does not seem to work. AI still used the after burner.
    self:GetGroup():SetOption(AI.Option.Air.id.RTB_ON_BINGO, false)
    self:GetGroup():SetOption(AI.Option.Air.id.JETT_TANKS_IF_EMPTY, self.jettisonEmptyTanks)
    --self.group:SetOption(AI.Option.Air.id.RADAR_USING, AI.Option.Air.val.RADAR_USING.FOR_CONTINUOUS_SEARCH)

    -- Update route.
    self:__UpdateRoute(-0.5)

  else
    
    -- Set flightcontrol.
    if self.currbase then
      local flightcontrol=_DATABASE:GetFlightControl(self.currbase:GetName())
      if flightcontrol then
        self:SetFlightControl(flightcontrol)
      else
        -- F10 other menu.
        self:_UpdateMenu(0.5)        
      end
    else
      self:_UpdateMenu(0.5)
    end
    
  end

end

--- On after "Parking" event. Add flight to flightcontrol of airbase.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterParking(From, Event, To)

  -- Get closest airbase
  local airbase=self:GetClosestAirbase()
  local airbasename=airbase:GetName() or "unknown"
  
  -- Debug info
  self:T(self.lid..string.format("Flight is parking at airbase %s", airbasename))  
  
  -- Set current airbase.
  self.currbase=airbase
  
  -- Set homebase to current airbase if not defined yet.
  -- This is necessary, e.g, when flights are spawned at an airbase because they do not have a takeoff waypoint.
  if not self.homebase then
    self.homebase=airbase
  end

  -- Parking time stamp.
  self.Tparking=timer.getAbsTime()

  -- Get FC of this airbase.
  local flightcontrol=_DATABASE:GetFlightControl(airbasename)

  if flightcontrol then

    -- Set FC for this flight. This also updates the menu.
    self:SetFlightControl(flightcontrol)

    if self.flightcontrol then

      -- Set flight status.
      self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.PARKING)

    end
    
  else
    self:T3(self.lid.."INFO: No flight control in onAfterParking!")
  end
end

--- On after "Taxiing" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterTaxiing(From, Event, To)
  self:T(self.lid..string.format("Flight is taxiing"))

  -- Parking over.
  self.Tparking=nil

  if self.flightcontrol and self.flightcontrol:IsControlling(self) then

    -- Add AI flight to takeoff queue.
    if self.isAI then
      -- AI flights go directly to TAKEOFF as we don't know when they finished taxiing.
      self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAKEOFF)
    else
      -- Human flights go to TAXI OUT queue. They will go to the ready for takeoff queue when they request it.
      self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIOUT)
    end

  end

end

--- On after "Takeoff" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Airbase#AIRBASE airbase The airbase the flight landed.
function FLIGHTGROUP:onafterTakeoff(From, Event, To, airbase)
  self:T(self.lid..string.format("Flight takeoff from %s", airbase and airbase:GetName() or "unknown airbase"))

  -- Remove flight from all FC queues.
  if self.flightcontrol and airbase and self.flightcontrol.airbasename==airbase:GetName() then
    self.flightcontrol:_RemoveFlight(self)
    self.flightcontrol=nil
  end

end

--- On after "Airborne" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterAirborne(From, Event, To)
  self:T(self.lid..string.format("Flight airborne"))

  -- No current airbase any more.
  self.currbase=nil
  
  -- Cruising.
  self:__Cruise(-0.01)

end

--- On after "Cruising" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterCruise(From, Event, To)
  self:T(self.lid..string.format("Flight cruising"))

  -- Not waiting anymore.
  self.Twaiting=nil
  self.dTwait=nil

  if self.isAI then
  
    ---
    -- AI
    ---
  
    -- Check group Done.
    self:_CheckGroupDone(nil, 120)
    
  else
  
    ---
    -- CLIENT
    ---
  
    -- Had this commented out (forgot why, probably because it was not necessary) but re-enabling it because of carrier launch.
    self:_UpdateMenu(0.1)
    
  end
    
end

--- On after "Landing" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterLanding(From, Event, To)
  self:T(self.lid..string.format("Flight is landing"))

  -- Everyone is landing now.
  self:_SetElementStatusAll(OPSGROUP.ElementStatus.LANDING)

  if self.flightcontrol and self.flightcontrol:IsControlling(self) then
    -- Add flight to landing queue.
    self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.LANDING)
  end
  
  -- Not holding any more.
  self.Tholding=nil
  
  -- Clear holding stack.
  if self.stack then
    self.stack.flightgroup=nil
    self.stack=nil
  end  

end


--- On after "Landed" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Airbase#AIRBASE airbase The airbase the flight landed.
function FLIGHTGROUP:onafterLanded(From, Event, To, airbase)
  self:T(self.lid..string.format("Flight landed at %s", airbase and airbase:GetName() or "unknown place"))

  if self.flightcontrol and self.flightcontrol:IsControlling(self) then
    -- Add flight to taxiinb queue.
    self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.TAXIINB)
  end

end

--- On after "LandedAt" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterLandedAt(From, Event, To)
  self:T(self.lid..string.format("Flight landed at"))

  -- Trigger (un-)loading process.
  if self:IsPickingup() then
    self:__Loading(-1)
  elseif self:IsTransporting() then
    self:__Unloading(-1)
  end

end

--- On after "Arrived" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterArrived(From, Event, To)
  self:T(self.lid..string.format("Flight arrived"))

  -- Flight Control
  if self.flightcontrol then
    -- Add flight to arrived queue.
    self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.ARRIVED)
  end
  
  if not self.isAI then
    -- Player landed. No despawn.
    return
  end
  
  --TODO: Check that current base is airwing base.
  local airwing=self:GetAirwing()  --airwing:GetAirbaseName()==self.currbase:GetName()

  -- Check what to do.
  if airwing and not (self:IsPickingup() or self:IsTransporting()) then
  
    -- Debug info.
    self:T(self.lid..string.format("Airwing asset group %s arrived ==> Adding asset back to stock of airwing %s", self.groupname, airwing.alias))
  
    -- Add the asset back to the airwing.
    --airwing:AddAsset(self.group, 1)
    self:ReturnToLegion(1)
        
  elseif self.isLandingAtAirbase then

    local Template=UTILS.DeepCopy(self.template)  --DCS#Template

    -- No late activation.
    self.isLateActivated=false
    Template.lateActivation=self.isLateActivated

    -- Spawn in uncontrolled state.
    self.isUncontrolled=true
    Template.uncontrolled=self.isUncontrolled

    -- First waypoint of the group.
    local SpawnPoint=Template.route.points[1]

    -- These are only for ships and FARPS.
    SpawnPoint.linkUnit = nil
    SpawnPoint.helipadId = nil
    SpawnPoint.airdromeId = nil

    -- Airbase.
    local airbase=self.isLandingAtAirbase --Wrapper.Airbase#AIRBASE

    -- Get airbase ID and category.
    local AirbaseID = airbase:GetID()

    -- Set airdromeId.
    if airbase:IsShip() then
      SpawnPoint.linkUnit = AirbaseID
      SpawnPoint.helipadId = AirbaseID
    elseif airbase:IsHelipad() then
      SpawnPoint.linkUnit = AirbaseID
      SpawnPoint.helipadId = AirbaseID
    elseif airbase:IsAirdrome() then
      SpawnPoint.airdromeId = AirbaseID
    end

    -- Set waypoint type/action.
    SpawnPoint.alt    = 0
    SpawnPoint.type   = COORDINATE.WaypointType.TakeOffParking
    SpawnPoint.action = COORDINATE.WaypointAction.FromParkingArea

    local units=Template.units

    for i=#units,1,-1 do
      local unit=units[i]
      local element=self:GetElementByName(unit.name)
      if element and element.status~=OPSGROUP.ElementStatus.DEAD then
        unit.parking=element.parking and element.parking.TerminalID or nil
        unit.parking_id=nil
        local vec3=element.unit:GetVec3()
        local heading=element.unit:GetHeading()
        unit.x=vec3.x
        unit.y=vec3.z
        unit.alt=vec3.y
        unit.heading=math.rad(heading)
        unit.psi=-unit.heading
      else
        table.remove(units, i)
      end
    end

    -- Respawn with this template.
    self:_Respawn(0, Template)

    -- Reset.
    self.isLandingAtAirbase=nil

    -- Init (un-)loading process.
    if self:IsPickingup() then
      self:__Loading(-1)
    elseif self:IsTransporting() then
      self:__Unloading(-1)
    end

  else
    -- Depawn after 5 min. Important to trigger dead events before DCS despawns on its own without any notification.
    self:T(self.lid..string.format("Despawning group in 5 minutes after arrival!"))
    self:Despawn(5*60)
  end
end

--- On after "Dead" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterDead(From, Event, To)

  -- Remove flight from all FC queues.
  if self.flightcontrol then
    self.flightcontrol:_RemoveFlight(self)
    self.flightcontrol=nil
  end

  -- Call OPSGROUP function.
  self:GetParent(self).onafterDead(self, From, Event, To)

end


--- On before "UpdateRoute" event. Update route of group, e.g after new waypoints and/or waypoint tasks have been added.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last.
-- @param #number N Waypoint  Max waypoint index to be included in the route. Default is the final waypoint.
-- @return #boolean Transision allowed?
function FLIGHTGROUP:onbeforeUpdateRoute(From, Event, To, n, N)

  -- Is transition allowed? We assume yes until proven otherwise.
  local allowed=true
  local trepeat=nil

  if self:IsAlive() then
    -- Alive & Airborne ==> Update route possible.
    self:T3(self.lid.."Update route possible. Group is ALIVE")
  elseif self:IsDead()  then
    -- Group is dead! No more updates.
    self:T(self.lid.."Update route denied. Group is DEAD!")
    allowed=false
  elseif self:IsInUtero() then
    self:T(self.lid.."Update route denied. Group is INUTERO!")
    allowed=false    
  else
    -- Not airborne yet. Try again in 5 sec.
    self:T(self.lid.."Update route denied ==> checking back in 5 sec")
    trepeat=-5
    allowed=false
  end
  
  -- Check if group is uncontrolled. If so, the mission task cannot be set yet!
  if allowed and self:IsUncontrolled() then
    self:T(self.lid.."Update route denied. Group is UNCONTROLLED!")
    local mission=self:GetMissionCurrent()
    if mission and mission.type==AUFTRAG.Type.ALERT5 then
      trepeat=nil --Alert 5 is just waiting for the real mission. No need to try to update the route.
    else
      trepeat=-5
    end
    allowed=false  
  end

  -- Requested waypoint index <1. Something is seriously wrong here!
  if n and n<1 then
    self:T(self.lid.."Update route denied because waypoint n<1!")
    allowed=false
  end

  -- No current waypoint. Something is serously wrong!
  if not self.currentwp then
    self:T(self.lid.."Update route denied because self.currentwp=nil!")
    allowed=false
  end

  local Nn=n or self.currentwp+1
  if not Nn or Nn<1 then
    self:T(self.lid.."Update route denied because N=nil or N<1")
    trepeat=-5
    allowed=false
  end

  -- Check for a current task.
  if self.taskcurrent>0 then

    -- Get the current task. Must not be executing already.
    local task=self:GetTaskByID(self.taskcurrent)

    if task then
      if task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then
        -- For patrol zone, we need to allow the update as we insert new waypoints.
        self:T2(self.lid.."Allowing update route for Task: PatrolZone")
      elseif task.dcstask.id==AUFTRAG.SpecialTask.CAPTUREZONE then
        -- For patrol zone, we need to allow the update as we insert new waypoints.
        self:T2(self.lid.."Allowing update route for Task: CaptureZone")
      elseif task.dcstask.id==AUFTRAG.SpecialTask.RECON then
        -- For recon missions, we need to allow the update as we insert new waypoints.
        self:T2(self.lid.."Allowing update route for Task: ReconMission")
      elseif task.dcstask.id==AUFTRAG.SpecialTask.PATROLRACETRACK then
        -- For recon missions, we need to allow the update as we insert new waypoints.
        self:T2(self.lid.."Allowing update route for Task: Patrol Race Track")
      elseif task.dcstask.id==AUFTRAG.SpecialTask.HOVER then
        -- For recon missions, we need to allow the update as we insert new waypoints.
        self:T2(self.lid.."Allowing update route for Task: Hover")
      elseif task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then
        -- For relocate
        self:T2(self.lid.."Allowing update route for Task: Relocate Cohort")          
      elseif task.description and task.description=="Task_Land_At" then
        -- We allow this
        self:T2(self.lid.."Allowing update route for Task: Task_Land_At")
      else
        local taskname=task and task.description or "No description"
        self:T(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s", self.taskcurrent, tostring(taskname)))
        allowed=false
      end
    else
      -- Now this can happen, if we directly use TaskExecute as the task is not in the task queue and cannot be removed. Therefore, also directly executed tasks should be added to the queue!
      self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d (>0!) but no task?!", self.taskcurrent))
      -- Anyhow, a task is running so we do not allow to update the route!
      allowed=false
    end
  end

  -- Not good, because mission will never start. Better only check if there is a current task!
  --if self.currentmission then
  --end

  -- Only AI flights.
  if not self.isAI then
    allowed=false
  end

  -- Debug info.
  self:T2(self.lid..string.format("Onbefore Updateroute in state %s: allowed=%s (repeat in %s)", self:GetState(), tostring(allowed), tostring(trepeat)))

  -- Try again?
  if trepeat then
    self:__UpdateRoute(trepeat, n)
  end

  return allowed
end

--- On after "UpdateRoute" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last.
-- @param #number N Waypoint  Max waypoint index to be included in the route. Default is the final waypoint.
function FLIGHTGROUP:onafterUpdateRoute(From, Event, To, n, N)

  -- Update route from this waypoint number onwards.
  n=n or self.currentwp+1

  -- Max index.
  N=N or #self.waypoints  
  N=math.min(N, #self.waypoints)

  -- Waypoints.
  local wp={}

  -- Current velocity.
  local speed=self.group and self.group:GetVelocityKMH() or 100

  -- Waypoint type.
  local waypointType=COORDINATE.WaypointType.TurningPoint
  local waypointAction=COORDINATE.WaypointAction.TurningPoint
  if self:IsLanded() or self:IsLandedAt() or self:IsAirborne()==false then
    -- Had some issues with passing waypoint function of the next WP called too ealy when the type is TurningPoint. Setting it to TakeOff solved it!
    waypointType=COORDINATE.WaypointType.TakeOff
    --waypointType=COORDINATE.WaypointType.TakeOffGroundHot
    --waypointAction=COORDINATE.WaypointAction.FromGroundAreaHot
  end

  -- Set current waypoint or we get problem that the _PassingWaypoint function is triggered too early, i.e. right now and not when passing the next WP.
  local current=self:GetCoordinate():WaypointAir(COORDINATE.WaypointAltType.BARO, waypointType, waypointAction, speed, true, nil, {}, "Current")
  table.insert(wp, current)

  -- Add remaining waypoints to route.
  for i=n, N do
    table.insert(wp, self.waypoints[i])
  end
  
  if wp[2] then
    self.speedWp=wp[2].speed
  end

  -- Debug info.
  local hb=self.homebase and self.homebase:GetName() or "unknown"
  local db=self.destbase and self.destbase:GetName() or "unknown"
  self:T(self.lid..string.format("Updating route for WP #%d-%d [%s], homebase=%s destination=%s", n, #wp, self:GetState(), hb, db))

  if #wp>1 then

    -- Route group to all defined waypoints remaining.
    self:Route(wp)

  else

    ---
    -- No waypoints left
    ---

    if self:IsAirborne() then
      self:T(self.lid.."No waypoints left ==> CheckGroupDone")
      self:_CheckGroupDone()
    end

  end

end

--- On after "OutOfMissilesAA" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterOutOfMissilesAA(From, Event, To)
  self:T(self.lid.."Group is out of AA Missiles!")
  if self.outofAAMrtb then
    -- Back to destination or home.
    local airbase=self.destbase or self.homebase
    self:T(self.lid.."Calling RTB in onafterOutOfMissilesAA")
    self:__RTB(-5, airbase)
  end
end

--- On after "OutOfMissilesAG" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterOutOfMissilesAG(From, Event, To)
  self:T(self.lid.."Group is out of AG Missiles!")
  if self.outofAGMrtb then
    -- Back to destination or home.
    local airbase=self.destbase or self.homebase
    self:T(self.lid.."Calling RTB in onafterOutOfMissilesAG")
    self:__RTB(-5, airbase)
  end
end

--- Check if flight is done, i.e.
--
--  * passed the final waypoint,
--  * no current task
--  * no current mission
--  * number of remaining tasks is zero
--  * number of remaining missions is zero
--
-- @param #FLIGHTGROUP self
-- @param #number delay Delay in seconds.
-- @param #number waittime Time to wait if group is done.
function FLIGHTGROUP:_CheckGroupDone(delay, waittime)

  -- FSM state.
  local fsmstate=self:GetState()

  if self:IsAlive() and self.isAI then

    if delay and delay>0 then
      -- Debug info.
      self:T(self.lid..string.format("Check FLIGHTGROUP [state=%s] done in %.3f seconds... (t=%.4f)", fsmstate, delay, timer.getTime()))
    
      -- Delayed call.
      self:ScheduleOnce(delay, FLIGHTGROUP._CheckGroupDone, self)
    else

      -- Debug info.
      self:T(self.lid..string.format("Check FLIGHTGROUP [state=%s] done? (t=%.4f)", fsmstate, timer.getTime()))

      -- Group is currently engaging.
      if self:IsEngaging() then
        self:T(self.lid.."Engaging! Group NOT done...")
        return
      end
      -- Check if group is going for fuel.
      if self:IsGoing4Fuel() then
        self:T(self.lid.."Going for FUEL! Group NOT done...")
        return      
      end
      
      -- Number of tasks remaining.
      local nTasks=self:CountRemainingTasks()

      -- Number of mission remaining.
      local nMissions=self:CountRemainingMissison()

      -- Number of cargo transports remaining.
      local nTransports=self:CountRemainingTransports()      

      -- Number of paused missions.
      local nPaused=self:_CountPausedMissions()

      -- First check if there is a paused mission and that all remaining missions are paused. If there are other missions in the queue, we will run those.
      if nPaused>0 and nPaused==nMissions then
        local missionpaused=self:_GetPausedMission()
        self:T(self.lid..string.format("Found paused mission %s [%s]. Unpausing mission...", missionpaused.name, missionpaused.type))
        self:UnpauseMission()
        return
      end

      -- Group is ordered to land at an airbase.
      if self.isLandingAtAirbase then
        self:T(self.lid..string.format("Landing at airbase %s! Group NOT done...", self.isLandingAtAirbase:GetName()))
        return
      end
      
      -- Group is waiting.
      if self:IsWaiting() then
        self:T(self.lid.."Waiting! Group NOT done...")
        return        
      end

      -- Debug info.
      self:T(self.lid..string.format("Remaining (final=%s): missions=%d, tasks=%d, transports=%d", tostring(self.passedfinalwp), nMissions, nTasks, nTransports))

      -- Final waypoint passed?
      -- Or next waypoint index is the first waypoint. Could be that the group was on a mission and the mission waypoints were deleted. then the final waypoint is FALSE but no real waypoint left.
      -- Since we do not do ad infinitum, this leads to a rapid oscillation between UpdateRoute and CheckGroupDone!
      if self:HasPassedFinalWaypoint() or self:GetWaypointIndexNext()==1 then
      
        ---
        -- Final Waypoint PASSED
        ---

        -- Got current mission or task?
        if self.currentmission==nil and self.taskcurrent==0 and (self.cargoTransport==nil or self.cargoTransport:GetCarrierTransportStatus(self)==OPSTRANSPORT.Status.DELIVERED) then

          -- Number of remaining tasks/missions?
          if nTasks==0 and nMissions==0 and nTransports==0 then

            local destbase=self.destbase or self.homebase --Wrapper.Airbase#AIRBASE
            local destzone=self.destzone or self.homezone --Wrapper.Airbase#AIRBASE

            -- Send flight to destination.
            if waittime then
              self:T(self.lid..string.format("Passed Final WP and No current and/or future missions/tasks/transports. Waittime given ==> Waiting for %d sec!", waittime))
              self:Wait(waittime)
            elseif destbase then
              if self.currbase and self.currbase.AirbaseName==destbase.AirbaseName and self:IsParking() then
                self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports AND parking at destination airbase ==> Arrived!")
                self:Arrived()
              else
                -- Only send RTB if current base is not yet the destination
                if self.currbase==nil or self.currbase.AirbaseName~=destbase.AirbaseName then
                  self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTB!")
                  self:__RTB(-0.1, destbase)
                end
              end
            elseif destzone then
              self:T(self.lid.."Passed Final WP and No current and/or future missions/tasks/transports ==> RTZ!")
              self:__RTZ(-0.1, destzone)
            else
              self:T(self.lid.."Passed Final WP and NO Tasks/Missions left. No DestBase or DestZone ==> Wait!")
              self:__Wait(-1)
            end

          else
            -- Check if not parking (could be on ALERT5 and just spawned (current mission=nil)
            if not self:IsParking() then            
              self:T(self.lid..string.format("Passed Final WP but Tasks=%d or Missions=%d left in the queue. Wait!", nTasks, nMissions))
              self:__Wait(-1)
            end
          end
        else
          self:T(self.lid..string.format("Passed Final WP but still have current Task (#%s) or Mission (#%s) left to do", tostring(self.taskcurrent), tostring(self.currentmission)))
        end
      else

        ---
        -- Final Waypoint NOT PASSED
        ---      
      
        -- Debug info.
        self:T(self.lid..string.format("Flight (status=%s) did NOT pass the final waypoint yet ==> update route in -0.01 sec", self:GetState()))
        
        -- Update route.
        self:__UpdateRoute(-0.01)
        
      end
    end

  end

end

--- On before "RTB" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at.
-- @param #number SpeedTo Speed used for travelling from current position to holding point in knots.
-- @param #number SpeedHold Holding speed in knots.
function FLIGHTGROUP:onbeforeRTB(From, Event, To, airbase, SpeedTo, SpeedHold)

  -- Debug info.
  self:T(self.lid..string.format("RTB: before event=%s: %s --> %s to %s", Event, From, To, airbase and airbase:GetName() or "None"))

  if self:IsAlive() then

    local allowed=true
    local Tsuspend=nil

    if airbase==nil then
      self:T(self.lid.."ERROR: Airbase is nil in RTB() call!")
      allowed=false
    end

    -- Check that coaliton is okay. We allow same (blue=blue, red=red) or landing on neutral bases.
    if airbase and airbase:GetCoalition()~=self.group:GetCoalition() and airbase:GetCoalition()>0 then
      self:T(self.lid..string.format("ERROR: Wrong airbase coalition %d in RTB() call! We allow only same as group %d or neutral airbases 0", airbase:GetCoalition(), self.group:GetCoalition()))
      return false
    end
    
    if self.currbase and self.currbase:GetName()==airbase:GetName() then
      self:T(self.lid.."WARNING: Currbase is already same as RTB airbase. RTB canceled!")
      return false
    end
    
    -- Check if the group has landed at an airbase. If so, we lost control and RTBing is not possible (only after a respawn).
    if self:IsLanded() then
      self:T(self.lid.."WARNING: Flight has already landed. RTB canceled!")
      return false    
    end

    if not self.group:IsAirborne(true) then
      -- this should really not happen, either the AUFTRAG is cancelled before the group was airborne or it is stuck at the ground for some reason
      self:T(self.lid..string.format("WARNING: Group [%s] is not AIRBORNE  ==> RTB event is suspended for 20 sec", self:GetState()))
      allowed=false
      Tsuspend=-20
      local groupspeed = self.group:GetVelocityMPS()
      if groupspeed<=1 and not self:IsParking() then
        self.RTBRecallCount = self.RTBRecallCount+1
      end
      if self.RTBRecallCount>6 then
        self:T(self.lid..string.format("WARNING: Group [%s] is not moving and was called RTB %d times. Assuming a problem and despawning!", self:GetState(), self.RTBRecallCount))
        self.RTBRecallCount=0
        self:Despawn(5)
        return
      end
    end

    -- Only if fuel is not low or critical.
    if self:IsFuelGood() then

      -- Check if there are remaining tasks.
      local Ntot,Nsched, Nwp=self:CountRemainingTasks()

      if self.taskcurrent>0 then
        self:T(self.lid..string.format("WARNING: Got current task ==> RTB event is suspended for 10 sec"))
        Tsuspend=-10
        allowed=false
      end

      if Nsched>0 then
        self:T(self.lid..string.format("WARNING: Still got %d SCHEDULED tasks in the queue ==> RTB event is suspended for 10 sec", Nsched))
        Tsuspend=-10
        allowed=false
      end

      if Nwp>0 then
        self:T(self.lid..string.format("WARNING: Still got %d WAYPOINT tasks in the queue ==> RTB event is suspended for 10 sec", Nwp))
        Tsuspend=-10
        allowed=false
      end
      
      if self.Twaiting and self.dTwait then
        self:T(self.lid..string.format("WARNING: Group is Waiting for a specific duration ==> RTB event is canceled", Nwp))
        allowed=false
      end

    end

    if Tsuspend and not allowed then
      self:T(self.lid.."Calling RTB in onbeforeRTB")
      self:__RTB(Tsuspend, airbase, SpeedTo, SpeedHold)
    end

    return allowed

  else
    self:T(self.lid.."WARNING: Group is not alive! RTB call not allowed.")
    return false
  end

end

--- On after "RTB" event. Order flight to hold at an airbase and wait for signal to land.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at.
-- @param #number SpeedTo Speed used for traveling from current position to holding point in knots. Default 75% of max speed.
-- @param #number SpeedHold Holding speed in knots. Default 250 kts.
-- @param #number SpeedLand Landing speed in knots. Default 170 kts.
function FLIGHTGROUP:onafterRTB(From, Event, To, airbase, SpeedTo, SpeedHold, SpeedLand)

  -- Debug info.
  self:T(self.lid..string.format("RTB: event=%s: %s --> %s to %s", Event, From, To, airbase:GetName()))

  -- Set the destination base.
  self.destbase=airbase
  
  -- Cancel all missions.
  self:CancelAllMissions()

  -- Land at airbase.
  self:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand)

end


--- On before "LandAtAirbase" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at.
function FLIGHTGROUP:onbeforeLandAtAirbase(From, Event, To, airbase)

  if self:IsAlive() then

    local allowed=true
    local Tsuspend=nil

    if airbase==nil then
      self:T(self.lid.."ERROR: Airbase is nil in LandAtAirbase() call!")
      allowed=false
    end

    -- Check that coaliton is okay. We allow same (blue=blue, red=red) or landing on neutral bases.
    if airbase and airbase:GetCoalition()~=self.group:GetCoalition() and airbase:GetCoalition()>0 then
      self:T(self.lid..string.format("ERROR: Wrong airbase coalition %d in LandAtAirbase() call! We allow only same as group %d or neutral airbases 0", airbase:GetCoalition(), self.group:GetCoalition()))
      return false
    end
    
    if self.currbase and self.currbase:GetName()==airbase:GetName() then
      self:T(self.lid.."WARNING: Currbase is already same as LandAtAirbase airbase. LandAtAirbase canceled!")
      return false
    end
    
    -- Check if the group has landed at an airbase. If so, we lost control and RTBing is not possible (only after a respawn).
    if self:IsLanded() then
      self:T(self.lid.."WARNING: Flight has already landed. LandAtAirbase canceled!")
      return false    
    end
    
    if self:IsParking() then      
      allowed=false
      Tsuspend=-30
      self:T(self.lid.."WARNING: Flight is parking. LandAtAirbase call delayed by 30 sec")
    elseif self:IsTaxiing() then
      allowed=false
      Tsuspend=-1
      self:T(self.lid.."WARNING: Flight is parking. LandAtAirbase call delayed by 1 sec")
    end
    
    if Tsuspend and not allowed then
      self:__LandAtAirbase(Tsuspend, airbase)
    end

    return allowed
  else
    self:T(self.lid.."WARNING: Group is not alive! LandAtAirbase call not allowed")
    return false
  end

end


--- On after "LandAtAirbase" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Airbase#AIRBASE airbase The airbase to hold at.
function FLIGHTGROUP:onafterLandAtAirbase(From, Event, To, airbase)

  self.isLandingAtAirbase=airbase

  self:_LandAtAirbase(airbase)

end

--- Land at an airbase.
-- @param #FLIGHTGROUP self
-- @param Wrapper.Airbase#AIRBASE airbase Airbase where the group shall land.
-- @param #number SpeedTo Speed used for travelling from current position to holding point in knots.
-- @param #number SpeedHold Holding speed in knots.
-- @param #number SpeedLand Landing speed in knots. Default 170 kts.
function FLIGHTGROUP:_LandAtAirbase(airbase, SpeedTo, SpeedHold, SpeedLand)

  -- Set current airbase.
  self.currbase=airbase
  
  -- Passed final waypoint!
  self:_PassedFinalWaypoint(true, "_LandAtAirbase")
  
  -- Not waiting any more.
  self.Twaiting=nil
  self.dTwait=nil

  -- Defaults:
  SpeedTo=SpeedTo or UTILS.KmphToKnots(self.speedCruise)
  SpeedHold=SpeedHold or (self.isHelo and 80 or 250)
  SpeedLand=SpeedLand or (self.isHelo and 40 or 170)

  -- Clear holding time in any case.
  self.Tholding=nil

  -- Debug message.
  local text=string.format("Flight group set to hold at airbase %s. SpeedTo=%d, SpeedHold=%d, SpeedLand=%d", airbase:GetName(), SpeedTo, SpeedHold, SpeedLand)
  self:T(self.lid..text)

  -- Holding altitude.
  local althold=self.isHelo and 1000+math.random(10)*100 or math.random(4,10)*1000

  -- Holding points.
  local c0=self:GetCoordinate()
  local p0=airbase:GetZone():GetRandomCoordinate():SetAltitude(UTILS.FeetToMeters(althold))
  local p1=nil
  local wpap=nil

  -- Do we have a flight control?
  local fc=_DATABASE:GetFlightControl(airbase:GetName())
  
  if fc and self.isAI then
  
    -- Get holding stack from flight control.
    local stack=fc:_GetHoldingStack(self)
    
    if stack then          
      
      stack.flightgroup=self
      self.stack=stack
      
      -- Race track points.
      p0=stack.pos0
      p1=stack.pos1
  
      -- Debug marks.
      if false then
        p0:MarkToAll(string.format("%s: Holding stack P0, alt=%d meters", self:GetName(), p0.y))
        p1:MarkToAll(string.format("%s: Holding stack P1, alt=%d meters", self:GetName(), p0.y))
      end
      
    else
      
    end

    -- Set flightcontrol for this flight.
    self:SetFlightControl(fc)

    -- Add flight to inbound queue.
    self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.INBOUND)
    
    -- Callsign.
    local callsign=self:GetCallsignName()
    
    -- Pilot calls inbound for landing.
    local text=string.format("%s, %s, inbound for landing", fc.alias, callsign)
      
    -- Radio message.
    fc:TransmissionPilot(text, self)
    
    -- Message text.
    local text=string.format("%s, %s, roger, hold at angels %d. Report entering the pattern.", callsign, fc.alias, stack.angels)
          
    -- Send message.
    fc:TransmissionTower(text, self, 10)
    
  end
  
  -- Some intermediate coordinate to climb to the default cruise alitude.
  local c1=c0:GetIntermediateCoordinate(p0, 0.25):SetAltitude(self.altitudeCruise, true)
  local c2=c0:GetIntermediateCoordinate(p0, 0.75):SetAltitude(self.altitudeCruise, true)

   -- Altitude above ground for a glide slope of 3 degrees.
  local x1=self.isHelo and UTILS.NMToMeters(2.0) or UTILS.NMToMeters(10)
  local x2=self.isHelo and UTILS.NMToMeters(1.0) or UTILS.NMToMeters(5)
  local alpha=math.rad(3)
  local h1=x1*math.tan(alpha)
  local h2=x2*math.tan(alpha)

  -- Get active runway.
  local runway=airbase:GetActiveRunwayLanding()

  -- Set holding flag to 0=false.
  self.flaghold:Set(0)

  -- Set holding time.
  local holdtime=self.holdtime
  if fc or self.airboss then
    holdtime=nil
  end

  -- Task fuction when reached holding point.
  local TaskArrived=self.group:TaskFunction("FLIGHTGROUP._ReachedHolding", self)

  -- Orbit until flaghold=1 (true) but max 5 min if no FC is giving the landing clearance.
  local TaskOrbit = self.group:TaskOrbit(p0, nil, UTILS.KnotsToMps(SpeedHold), p1)
  local TaskLand  = self.group:TaskCondition(nil, self.flaghold.UserFlagName, 1, nil, holdtime)
  local TaskHold  = self.group:TaskControlled(TaskOrbit, TaskLand)
  local TaskKlar  = self.group:TaskFunction("FLIGHTGROUP._ClearedToLand", self)  -- Once the holding flag becomes true, set trigger FLIGHTLANDING, i.e. set flight STATUS to LANDING.

  -- Waypoints from current position to holding point.
  local wp={}
  -- NOTE: Currently, this first waypoint confuses the AI. It makes them go in circles. Looks like they cannot find the waypoint and are flying around it.
  --wp[#wp+1]=c0:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Current Pos")
  wp[#wp+1]=c1:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Climb")
  wp[#wp+1]=c2:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {}, "Descent")
  wp[#wp+1]=p0:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(SpeedTo), true , nil, {TaskArrived, TaskHold, TaskKlar}, "Holding Point")

  -- Approach point: 10 NN in direction of runway.
  if airbase:IsAirdrome() then

    ---
    -- Airdrome
    ---

    -- Call a function to tell everyone we are on final.
    local TaskFinal = self.group:TaskFunction("FLIGHTGROUP._OnFinal", self)

    -- Final approach waypoint.
    local rheading
    if runway then
      rheading = runway.heading-180
    else
      -- AB HeloBase w/o runway eg Naqoura
      local wind = airbase:GetCoordinate():GetWind()
      rheading = -wind
    end
    
    local papp=airbase:GetCoordinate():Translate(x1, rheading):SetAltitude(h1)
    wp[#wp+1]=papp:WaypointAirTurningPoint("BARO", UTILS.KnotsToKmph(SpeedLand), {TaskFinal}, "Final Approach")

    -- Okay, it looks like it's best to specify the coordinates not at the airbase but a bit away. This causes a more direct landing approach.
    local pland=airbase:GetCoordinate():Translate(x2, rheading):SetAltitude(h2)
    wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand), airbase, {}, "Landing")
      
  elseif airbase:IsShip() or airbase:IsHelipad() then

    ---
    -- Ship or Helipad
    ---

    local pland=airbase:GetCoordinate()
    wp[#wp+1]=pland:WaypointAirLanding(UTILS.KnotsToKmph(SpeedLand), airbase, {}, "Landing")

  end

  if self.isAI then

    -- Clear all tasks.
    -- Warning, looks like this can make DCS CRASH! Had this after calling RTB once passed the final waypoint.
    --self:ClearTasks()

    -- Just route the group. Respawn might happen when going from holding to final.
    -- NOTE: I have delayed that here because of RTB calling _LandAtAirbase which resets current task immediately. 
    -- So the stop flag change to 1 will not trigger TaskDone() and a current mission is not done either!
    -- Looks like a delay of 0.1 sec was not enough for the stopflag to take effect. Increasing this to 1.0 sec.
    -- This delay is looking better. Hopefully not any unwanted side effects in other situations.
    self:Route(wp, 1.0)

  end

end

--- On before "Wait" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever).
-- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos.
-- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos.
function FLIGHTGROUP:onbeforeWait(From, Event, To, Duration, Altitude, Speed)

  local allowed=true
  local Tsuspend=nil

  -- Check for a current task.
  if self.taskcurrent>0 and not self:IsLandedAt() then
    self:T(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 30 sec!"))
    Tsuspend=-30
    allowed=false
  end
  
  -- Check for a current transport assignment.
  if self.cargoTransport and not self:IsLandedAt() then
    --self:T(self.lid..string.format("WARNING: Got current TRANSPORT assignment ==> WAIT event is suspended for 30 sec!"))
    --Tsuspend=-30
    --allowed=false  
  end

  -- Call wait again.
  if Tsuspend and not allowed then
    self:__Wait(Tsuspend, Duration, Altitude, Speed)
  end

  return allowed
end


--- On after "Wait" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever).
-- @param #number Altitude Altitude in feet. Default 10,000 ft for airplanes and 1,000 feet for helos.
-- @param #number Speed Speed in knots. Default 250 kts for airplanes and 20 kts for helos.
function FLIGHTGROUP:onafterWait(From, Event, To, Duration, Altitude, Speed)

  -- Group will orbit at its current position.
  local Coord=self:GetCoordinate()
  
  -- Set altitude: 1000 ft for helos and 10,000 ft for panes.
  if Altitude then
    Altitude=UTILS.FeetToMeters(Altitude)
  else
    Altitude=self.altitudeCruise
  end 
  
  -- Set speed.
  Speed=Speed or (self.isHelo and 20 or 250)

  -- Debug message.
  local text=string.format("Group set to wait/orbit at altitude %d m and speed %.1f km/h for %s seconds", Altitude, Speed, tostring(Duration))
  self:T(self.lid..text)

  --TODO: set ROE passive. introduce roe event/state/variable.

  -- Orbit until flaghold=1 (true) but max 5 min if no FC is giving the landing clearance.
  self.flaghold:Set(0)
  local TaskOrbit = self.group:TaskOrbit(Coord, Altitude, UTILS.KnotsToMps(Speed))
  local TaskStop  = self.group:TaskCondition(nil, self.flaghold.UserFlagName, 1, nil, Duration)
  local TaskCntr  = self.group:TaskControlled(TaskOrbit, TaskStop)
  local TaskOver  = self.group:TaskFunction("FLIGHTGROUP._FinishedWaiting", self)
  
  local DCSTasks
  if Duration or true then
    DCSTasks=self.group:TaskCombo({TaskCntr, TaskOver})
  else
    DCSTasks=self.group:TaskCombo({TaskOrbit, TaskOver})
  end

  
  -- Set task.
  self:PushTask(DCSTasks)
  
  -- Set time stamp.
  self.Twaiting=timer.getAbsTime()

  -- Max waiting time in seconds.
  self.dTwait=Duration

end


--- On after "Refuel" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Coordinate The coordinate.
function FLIGHTGROUP:onafterRefuel(From, Event, To, Coordinate)

  -- Debug message.
  local text=string.format("Flight group set to refuel at the nearest tanker")
  self:T(self.lid..text)

  --TODO: set ROE passive. introduce roe event/state/variable.
  --TODO: cancel current task

  -- Pause current mission if there is any.
  self:PauseMission()

  -- Refueling task.
  local TaskRefuel=self.group:TaskRefueling()
  local TaskFunction=self.group:TaskFunction("FLIGHTGROUP._FinishedRefuelling", self)
  local DCSTasks={TaskRefuel, TaskFunction}

  local Speed=self.speedCruise

  local coordinate=self:GetCoordinate()

  Coordinate=Coordinate or coordinate:Translate(UTILS.NMToMeters(5), self.group:GetHeading(), true)

  local wp0=coordinate:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true)
  local wp9=Coordinate:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true, nil, DCSTasks, "Refuel")

  self:Route({wp0, wp9}, 1)
  
  -- Set RTB on Bingo option. Currently DCS does not execute the refueling task if RTB_ON_BINGO is set to "NO RTB ON BINGO"
  self.group:SetOption(AI.Option.Air.id.RTB_ON_BINGO, true)

end

--- On after "Refueled" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterRefueled(From, Event, To)

  -- Debug message.
  local text=string.format("Flight group finished refuelling")
  self:T(self.lid..text)
  
  -- Set RTB on Bingo option to "NO RTB ON BINGO"
  self.group:SetOption(AI.Option.Air.id.RTB_ON_BINGO, false)  

  -- Check if flight is done.
  self:_CheckGroupDone(1)

end


--- On after "Holding" event. Flight arrived at the holding point.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterHolding(From, Event, To)

  -- Set holding flag to 0 (just in case).
  self.flaghold:Set(0)
  
  -- Despawn after holding.
  if self.despawnAfterHolding then
    if self.legion then
      self:ReturnToLegion(1)
    else
      self:Despawn(1)
    end
    return
  end

  -- Holding time stamp.
  self.Tholding=timer.getAbsTime()

  -- Debug message.
  local text=string.format("Flight group %s is HOLDING now", self.groupname)
  self:T(self.lid..text)

  -- Add flight to waiting/holding queue.
  if self.flightcontrol then

    -- Set flight status to holding.
    self.flightcontrol:SetFlightStatus(self, FLIGHTCONTROL.FlightStatus.HOLDING)
    
    if self.isAI then
    
      -- Callsign.
      local callsign=self:GetCallsignName()

      -- Pilot arrived at holding pattern.
      local text=string.format("%s, %s, arrived at holding pattern", self.flightcontrol.alias, callsign)
      
      if self.stack then
        text=text..string.format(", angels %d.", self.stack.angels)
      end
      
      -- Radio message.
      self.flightcontrol:TransmissionPilot(text, self)

      -- Message to flight
      local text=string.format("%s, roger, fly heading %d and wait for landing clearance", callsign, self.stack.heading)
      
      -- Radio message from tower.
      self.flightcontrol:TransmissionTower(text, self, 10)
          
    end

  elseif self.airboss then

    if self.isHelo then

      local carrierpos=self.airboss:GetCoordinate()
      local carrierheading=self.airboss:GetHeading()

      local Distance=UTILS.NMToMeters(5)
      local Angle=carrierheading+90
      local altitude=math.random(12, 25)*100
      local oc=carrierpos:Translate(Distance,Angle):SetAltitude(altitude, true)

      -- Orbit until flaghold=1 (true) but max 5 min if no FC is giving the landing clearance.
      local TaskOrbit=self.group:TaskOrbit(oc, nil, UTILS.KnotsToMps(50))
      local TaskLand=self.group:TaskCondition(nil, self.flaghold.UserFlagName, 1)
      local TaskHold=self.group:TaskControlled(TaskOrbit, TaskLand)
      local TaskKlar=self.group:TaskFunction("FLIGHTGROUP._ClearedToLand", self)  -- Once the holding flag becomes true, set trigger FLIGHTLANDING, i.e. set flight STATUS to LANDING.

      local DCSTask=self.group:TaskCombo({TaskOrbit, TaskHold, TaskKlar})

      self:SetTask(DCSTask)
    end

  end

end

--- On after "EngageTarget" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #table Target Target object. Can be a UNIT, STATIC, GROUP, SET_UNIT or SET_GROUP object.
function FLIGHTGROUP:onafterEngageTarget(From, Event, To, Target)

  -- DCS task.
  local DCStask=nil

  -- Check target object.
  if Target:IsInstanceOf("UNIT") or Target:IsInstanceOf("STATIC") then

    DCStask=self:GetGroup():TaskAttackUnit(Target, true)

  elseif Target:IsInstanceOf("GROUP") then

    DCStask=self:GetGroup():TaskAttackGroup(Target, nil, nil, nil, nil, nil, nil, true)

  elseif Target:IsInstanceOf("SET_UNIT") then

    local DCSTasks={}

    for _,_unit in pairs(Target:GetSet()) do --detected by =HRP= Zero
      local unit=_unit  --Wrapper.Unit#UNIT
      local task=self:GetGroup():TaskAttackUnit(unit, true)
      table.insert(DCSTasks)
    end

    -- Task combo.
    DCStask=self:GetGroup():TaskCombo(DCSTasks)

  elseif Target:IsInstanceOf("SET_GROUP") then

    local DCSTasks={}

    for _,_unit in pairs(Target:GetSet()) do --detected by =HRP= Zero
      local unit=_unit  --Wrapper.Unit#UNIT
      local task=self:GetGroup():TaskAttackGroup(Target, nil, nil, nil, nil, nil, nil, true)
      table.insert(DCSTasks)
    end

    -- Task combo.
    DCStask=self:GetGroup():TaskCombo(DCSTasks)

  else
    self:T("ERROR: unknown Target in EngageTarget! Needs to be a UNIT, STATIC, GROUP, SET_UNIT or SET_GROUP")
    return
  end

  -- Create new task.The description "Engage_Target" is checked so do not change that lightly.
  local Task=self:NewTaskScheduled(DCStask, 1, "Engage_Target", 0)

  -- Backup ROE setting.
  Task.backupROE=self:GetROE()

  -- Switch ROE to open fire
  self:SwitchROE(ENUMS.ROE.OpenFire)

  -- Pause current mission.
  local mission=self:GetMissionCurrent()
  if mission then
    self:PauseMission()
  end

  -- Execute task.
  self:TaskExecute(Task)

end

--- On after "Disengage" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Set#SET_UNIT TargetUnitSet
function FLIGHTGROUP:onafterDisengage(From, Event, To)
  self:T(self.lid.."Disengage target")
end

--- On before "LandAt" event. Check we have a helo group.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Coordinate The coordinate where to land. Default is current position.
-- @param #number Duration The duration in seconds to remain on ground. Default 600 sec (10 min).
function FLIGHTGROUP:onbeforeLandAt(From, Event, To, Coordinate, Duration)
  return self.isHelo
end

--- On after "LandAt" event. Order helicopter to land at a specific point.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Coordinate The coordinate where to land. Default is current position.
-- @param #number Duration The duration in seconds to remain on ground. Default `nil` = forever.
function FLIGHTGROUP:onafterLandAt(From, Event, To, Coordinate, Duration)

  -- Duration.
  --Duration=Duration or 600
  
  self:T(self.lid..string.format("Landing at Coordinate for %s seconds", tostring(Duration)))

  Coordinate=Coordinate or self:GetCoordinate()

  local DCStask=self.group:TaskLandAtVec2(Coordinate:GetVec2(), Duration)

  local Task=self:NewTaskScheduled(DCStask, 1, "Task_Land_At", 0)

  self:TaskExecute(Task)
  
end

--- On after "FuelLow" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterFuelLow(From, Event, To)

  -- Current min fuel.
  local fuel=self:GetFuelMin() or 0

  -- Debug message.
  local text=string.format("Low fuel %d for flight group %s", fuel, self.groupname)
  self:T(self.lid..text)

  -- Set switch to true.
  self.fuellow=true

  -- Back to destination or home.
  local airbase=self.destbase or self.homebase

  if self.fuellowrefuel and self.refueltype then

    -- Find nearest tanker within 50 NM.
    local tanker=self:FindNearestTanker(50)

    if tanker then

      -- Debug message.
      self:T(self.lid..string.format("Send to refuel at tanker %s", tanker:GetName()))

      -- Get a coordinate towards the tanker.
      local coordinate=self:GetCoordinate():GetIntermediateCoordinate(tanker:GetCoordinate(), 0.75)

      -- Trigger refuel even.
      self:Refuel(coordinate)

      return
    end
  end

  -- Send back to airbase.
  if airbase and self.fuellowrtb then
    self:T(self.lid.."Calling RTB in onafterFuelLow")
    self:RTB(airbase)
    --TODO: RTZ
  end

end

--- On after "FuelCritical" event.
-- @param #FLIGHTGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLIGHTGROUP:onafterFuelCritical(From, Event, To)

  -- Debug message.
  local text=string.format("Critical fuel for flight group %s", self.groupname)
  self:T(self.lid..text)

  -- Set switch to true.
  self.fuelcritical=true

  -- Airbase.
  local airbase=self.destbase or self.homebase

  if airbase and self.fuelcriticalrtb and not self:IsGoing4Fuel() then
    self:T(self.lid.."Calling RTB in onafterFuelCritical")
    self:RTB(airbase)
    --TODO: RTZ
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Task functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------



-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Mission functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------



-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Special Task Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function called when flight has reached the holding point.
-- @param Wrapper.Group#GROUP group Group object.
-- @param #FLIGHTGROUP flightgroup Flight group object.
function FLIGHTGROUP._ReachedHolding(group, flightgroup)
  flightgroup:T2(flightgroup.lid..string.format("Group reached holding point"))

  -- Trigger Holding event.
  flightgroup:__Holding(-1)
end

--- Function called when flight has reached the holding point.
-- @param Wrapper.Group#GROUP group Group object.
-- @param #FLIGHTGROUP flightgroup Flight group object.
function FLIGHTGROUP._ClearedToLand(group, flightgroup)
  flightgroup:T2(flightgroup.lid..string.format("Group was cleared to land"))

  -- Trigger Landing event.
  flightgroup:__Landing(-1)
end

--- Function called when flight is on final.
-- @param Wrapper.Group#GROUP group Group object.
-- @param #FLIGHTGROUP flightgroup Flight group object.
function FLIGHTGROUP._OnFinal(group, flightgroup)
  flightgroup:T2(flightgroup.lid..string.format("Group on final approach"))

  local fc=flightgroup.flightcontrol
  
  if fc and fc:IsControlling(flightgroup) then
    fc:_FlightOnFinal(flightgroup)
  end

end

--- Function called when flight finished refuelling.
-- @param Wrapper.Group#GROUP group Group object.
-- @param #FLIGHTGROUP flightgroup Flight group object.
function FLIGHTGROUP._FinishedRefuelling(group, flightgroup)
  flightgroup:T2(flightgroup.lid..string.format("Group finished refueling"))

  -- Trigger Holding event.
  flightgroup:__Refueled(-1)
end

--- Function called when flight finished waiting.
-- @param Wrapper.Group#GROUP group Group object.
-- @param #FLIGHTGROUP flightgroup Flight group object.
function FLIGHTGROUP._FinishedWaiting(group, flightgroup)
  flightgroup:T(flightgroup.lid..string.format("Group finished waiting"))
  
  -- Not waiting any more.
  flightgroup.Twaiting=nil
  flightgroup.dTwait=nil

  -- Check group done.
  flightgroup:_CheckGroupDone(0.1)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil.
-- @param #FLIGHTGROUP self
-- @param #table Template Template used to init the group. Default is `self.template`.
-- @param #number Delay Delay in seconds before group is initialized. Default `nil`, *i.e.* instantaneous.
-- @return #FLIGHTGROUP self
function FLIGHTGROUP:_InitGroup(Template, Delay)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, FLIGHTGROUP._InitGroup, self, Template, 0)
  else

    -- First check if group was already initialized.
    if self.groupinitialized then
      self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!")
      return
    end
  
    -- Group object.
    local group=self.group --Wrapper.Group#GROUP
  
    -- Helo group.
    self.isHelo=group:IsHelicopter()
  
    -- Max speed in km/h.
    self.speedMax=group:GetSpeedMax()
    
    -- Is group mobile?
    if self.speedMax and self.speedMax>3.6 then
      self.isMobile=true
    else
      self.isMobile=false
      self.speedMax = 0
    end  
  
    -- Cruise speed limit 380 kts for fixed and 110 knots for rotary wings.
    local speedCruiseLimit=self.isHelo and UTILS.KnotsToKmph(110) or UTILS.KnotsToKmph(380)
  
    -- Cruise speed: 70% of max speed but within limit.
    self.speedCruise=math.min(self.speedMax*0.7, speedCruiseLimit)
  
    -- Group ammo.
    self.ammo=self:GetAmmoTot()
    
    -- Get template of group.
    local template=Template or self:_GetTemplate()  
  
    -- Is (template) group uncontrolled.
    self.isUncontrolled=template~=nil and template.uncontrolled or false
  
    -- Is (template) group late activated.
    self.isLateActivated=template~=nil and template.lateActivation or false
  
    if template then
  
      -- Radio parameters from template. Default is set on spawn if not modified by user.
      self.radio.Freq=tonumber(template.frequency)
      self.radio.Modu=tonumber(template.modulation)
      self.radio.On=template.communication
    
      -- Set callsign. Default is set on spawn if not modified by user.
      local callsign=template.units[1].callsign
      --self:I({callsign=callsign})
      if type(callsign)=="number" then  -- Sometimes callsign is just "101".
        local cs=tostring(callsign)
        callsign={}
        callsign[1]=cs:sub(1,1)
        callsign[2]=cs:sub(2,2)
        callsign[3]=cs:sub(3,3)
      end
      self.callsign.NumberSquad=tonumber(callsign[1])
      self.callsign.NumberGroup=tonumber(callsign[2])
      self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad)
      
    end
  
    -- Set default formation.
    if self.isHelo then
      self.optionDefault.Formation=ENUMS.Formation.RotaryWing.EchelonLeft.D300
    else
      self.optionDefault.Formation=ENUMS.Formation.FixedWing.EchelonLeft.Group
    end
  
    -- Default TACAN off.
    if not self.tacanDefault then
      self:SetDefaultTACAN(nil, nil, nil, nil, true)
    end
    if not self.tacan then
      self.tacan=UTILS.DeepCopy(self.tacanDefault)
    end
  
    -- Is this purely AI?
    self.isAI=not self:_IsHuman(group)
  
    -- Create Menu.
    if not self.isAI then
      self.menu=self.menu or {}
      self.menu.atc=self.menu.atc or {} --#table
      self.menu.atc.root=self.menu.atc.root or MENU_GROUP:New(self.group, "ATC") --Core.Menu#MENU_GROUP
      self.menu.atc.help=self.menu.atc.help or MENU_GROUP:New(self.group, "Help", self.menu.atc.root) --Core.Menu#MENU_GROUP
    end
  
    -- Units of the group.
    local units=self.group:GetUnits()
    
    -- DCS group.
    local dcsgroup=Group.getByName(self.groupname)
    local size0=dcsgroup:getInitialSize()
    
    -- Quick check.
    if #units~=size0 then
      self:T(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units!", #units, size0))
    end  
  
    -- Add elemets.
    for _,unit in pairs(units) do
      self:_AddElementByName(unit:GetName())
    end
  
    -- Init done.
    self.groupinitialized=true
  end
    
  return self
end


--- Check if a unit is and element of the flightgroup.
-- @param #FLIGHTGROUP self
-- @return Wrapper.Airbase#AIRBASE Final destination airbase or #nil.
function FLIGHTGROUP:GetHomebaseFromWaypoints()

  local wp=self.waypoints0 and self.waypoints0[1] or nil --self:GetWaypoint(1)

  if wp then

    if wp and wp.action and wp.action==COORDINATE.WaypointAction.FromParkingArea
                         or wp.action==COORDINATE.WaypointAction.FromParkingAreaHot
                         or wp.action==COORDINATE.WaypointAction.FromRunway  then

      -- Get airbase ID depending on airbase category.
      local airbaseID=nil

      if wp.airdromeId then
        airbaseID=wp.airdromeId
      else
        airbaseID=-wp.helipadId
      end

      local airbase=AIRBASE:FindByID(airbaseID)

      return airbase
    end

    --TODO: Handle case where e.g. only one WP but that is not landing.
    --TODO: Probably other cases need to be taken care of.

  end

  return nil
end

--- Find the nearest friendly airbase (same or neutral coalition).
-- @param #FLIGHTGROUP self
-- @param #number Radius Search radius in NM. Default 50 NM.
-- @return Wrapper.Airbase#AIRBASE Closest tanker group #nil.
function FLIGHTGROUP:FindNearestAirbase(Radius)

  local coord=self:GetCoordinate()

  local dmin=math.huge
  local airbase=nil --Wrapper.Airbase#AIRBASE
  for _,_airbase in pairs(AIRBASE.GetAllAirbases()) do
    local ab=_airbase --Wrapper.Airbase#AIRBASE

    local coalitionAB=ab:GetCoalition()

    if coalitionAB==self:GetCoalition() or coalitionAB==coalition.side.NEUTRAL then

      if airbase then
        local d=ab:GetCoordinate():Get2DDistance(coord)

        if d<dmin then
          d=dmin
          airbase=ab
        end

      end

    end


  end

  return airbase
end

--- Find the nearest tanker.
-- @param #FLIGHTGROUP self
-- @param #number Radius Search radius in NM. Default 50 NM.
-- @return Wrapper.Group#GROUP Closest tanker group or `nil` if no tanker is in the given radius.
function FLIGHTGROUP:FindNearestTanker(Radius)

  Radius=UTILS.NMToMeters(Radius or 50)

  if self.refueltype then

    local coord=self:GetCoordinate()

    local units=coord:ScanUnits(Radius)

    local dmin=math.huge
    local tanker=nil --Wrapper.Unit#UNIT
    for _,_unit in pairs(units.Set) do
      local unit=_unit --Wrapper.Unit#UNIT

      local istanker, refuelsystem=unit:IsTanker()

      if istanker and self.refueltype==refuelsystem and unit:IsAlive() and unit:GetCoalition()==self:GetCoalition() then

        -- Distance.
        local d=unit:GetCoordinate():Get2DDistance(coord)

        if d<dmin then
          dmin=d
          tanker=unit
        end

      end

    end

    if tanker then
      return tanker:GetGroup()
    end

  end

  return nil
end

--- Check if a unit is and element of the flightgroup.
-- @param #FLIGHTGROUP self
-- @return Wrapper.Airbase#AIRBASE Final destination airbase or #nil.
function FLIGHTGROUP:GetDestinationFromWaypoints()

  local wp=self.waypoints0 and self.waypoints0[#self.waypoints0] or nil --self:GetWaypointFinal()

  if wp then

    if wp and wp.action and wp.action==COORDINATE.WaypointAction.Landing then

      -- Get airbase ID depending on airbase category.
      local airbaseID=wp.airdromeId or wp.helipadId

      local airbase=AIRBASE:FindByID(airbaseID)

      return airbase
    end

    --TODO: Handle case where e.g. only one WP but that is not landing.
    --TODO: Probably other cases need to be taken care of.

  end

  return nil
end

--- Check if this is a hot start.
-- @param #FLIGHTGROUP self
-- @return #boolean Hot start?
function FLIGHTGROUP:IsTakeoffHot()

  local wp=self.waypoints0 and self.waypoints0[1] or nil --self:GetWaypoint(1)

  if wp then

    if wp.action and wp.action==COORDINATE.WaypointAction.FromParkingAreaHot then
      return true
    else
      return false
    end

  end

  return nil
end

--- Check if this is a cold start.
-- @param #FLIGHTGROUP self
-- @return #boolean Cold start, i.e. engines off when spawned?
function FLIGHTGROUP:IsTakeoffCold()

  local wp=self.waypoints0 and self.waypoints0[1] or nil --self:GetWaypoint(1)

  if wp then

    if wp.action and wp.action==COORDINATE.WaypointAction.FromParkingArea then
      return true
    else
      return false
    end

  end

  return nil
end

--- Check if this is a runway start.
-- @param #FLIGHTGROUP self
-- @return #boolean Runway start?
function FLIGHTGROUP:IsTakeoffRunway()

  local wp=self.waypoints0 and self.waypoints0[1] or nil --self:GetWaypoint(1)

  if wp then

    if wp.action and wp.action==COORDINATE.WaypointAction.FromRunway then
      return true
    else
      return false
    end

  end

  return nil
end

--- Check if this is an air start.
-- @param #FLIGHTGROUP self
-- @return #boolean Air start?
function FLIGHTGROUP:IsTakeoffAir()

  local wp=self.waypoints0 and self.waypoints0[1] or nil --self:GetWaypoint(1)  

  if wp then

    if wp.action and wp.action==COORDINATE.WaypointAction.TurningPoint or wp.action==COORDINATE.WaypointAction.FlyoverPoint then
      return true
    else
      return false
    end

  end

  return nil
end

--- Check if the final waypoint is in the air.
-- @param #FLIGHTGROUP self
-- @param #table wp Waypoint. Default final waypoint.
-- @return #boolean If `true` final waypoint is a turning or flyover but not a landing type waypoint.
function FLIGHTGROUP:IsLandingAir(wp)

  wp=wp or self:GetWaypointFinal()

  if wp then

    if wp.action and wp.action==COORDINATE.WaypointAction.TurningPoint or wp.action==COORDINATE.WaypointAction.FlyoverPoint then
      return true
    else
      return false
    end

  end

  return nil
end

--- Check if the final waypoint is at an airbase.
-- @param #FLIGHTGROUP self
-- @param #table wp Waypoint. Default final waypoint.
-- @return #boolean If `true`, final waypoint is a landing waypoint at an airbase.
function FLIGHTGROUP:IsLandingAirbase(wp)

  wp=wp or self:GetWaypointFinal()

  if wp then

    if wp.action and wp.action==COORDINATE.WaypointAction.Landing then
      return true
    else
      return false
    end

  end

  return nil
end


--- Add an AIR waypoint to the flight plan.
-- @param #FLIGHTGROUP self
-- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. Use COORDINATE:SetAltitude(altitude) to define the altitude.
-- @param #number Speed Speed in knots. Default is cruise speed.
-- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint.
-- @param #number Altitude Altitude in feet. Default is y-component of Coordinate. Note that these altitudes are wrt to sea level (barometric altitude).
-- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call.
-- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table.
function FLIGHTGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Altitude, Updateroute)

  -- Create coordinate.
  local coordinate=self:_CoordinateFromObject(Coordinate)  

  -- Set waypoint index.
  local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID)
   
  -- Speed in knots.
  Speed=Speed or self:GetSpeedCruise()
  
  -- Debug info.
  self:T3(self.lid..string.format("Waypoint Speed=%.1f knots", Speed))
  
  -- Alt type default is barometric (ASL). For helos we use radar (AGL).
  local alttype=COORDINATE.WaypointAltType.BARO
  if self.isHelo then
    alttype=COORDINATE.WaypointAltType.RADIO
  end

  -- Create air waypoint.
  local wp=coordinate:WaypointAir(alttype, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, UTILS.KnotsToKmph(Speed), true, nil, {})

  -- Create waypoint data table.
  local waypoint=self:_CreateWaypoint(wp)

  -- Set altitude.
  if Altitude then
    waypoint.alt=UTILS.FeetToMeters(Altitude)
  end

  -- Add waypoint to table.
  self:_AddWaypoint(waypoint, wpnumber)

  -- Debug info.
  self:T(self.lid..string.format("Adding AIR waypoint #%d, speed=%.1f knots. Last waypoint passed was #%s. Total waypoints #%d", wpnumber, Speed, self.currentwp, #self.waypoints))

  -- Update route.
  if Updateroute==nil or Updateroute==true then
    self:__UpdateRoute(-0.01)
  end

  return waypoint
end

--- Add an LANDING waypoint to the flight plan.
-- @param #FLIGHTGROUP self
-- @param Wrapper.Airbase#AIRBASE Airbase The airbase where the group should land.
-- @param #number Speed Speed in knots. Default 350 kts.
-- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint.
-- @param #number Altitude Altitude in feet. Default is y-component of Coordinate. Note that these altitudes are wrt to sea level (barometric altitude).
-- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call.
-- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table.
function FLIGHTGROUP:AddWaypointLanding(Airbase, Speed, AfterWaypointWithID, Altitude, Updateroute)

  -- Set waypoint index.
  local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID)

  if wpnumber>self.currentwp then
    self:_PassedFinalWaypoint(false, "AddWaypointLanding")
  end

  -- Speed in knots.
  Speed=Speed or self.speedCruise

  -- Get coordinate of airbase.
  local Coordinate=Airbase:GetCoordinate()

  -- Create air waypoint.
  local wp=Coordinate:WaypointAir(COORDINATE.WaypointAltType.BARO, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed, nil, Airbase, {}, "Landing Temp", nil)

  -- Create waypoint data table.
  local waypoint=self:_CreateWaypoint(wp)

  -- Set altitude.
  if Altitude then
    waypoint.alt=UTILS.FeetToMeters(Altitude)
  end

  -- Add waypoint to table.
  self:_AddWaypoint(waypoint, wpnumber)

  -- Debug info.
  self:T(self.lid..string.format("Adding AIR waypoint #%d, speed=%.1f knots. Last waypoint passed was #%s. Total waypoints #%d", wpnumber, Speed, self.currentwp, #self.waypoints))

  -- Update route.
  if Updateroute==nil or Updateroute==true then
    self:__UpdateRoute(-1)
  end

  return waypoint
end

--- Get player element.
-- @param #FLIGHTGROUP self
-- @return Ops.OpsGroup#OPSGROUP.Element The element.
function FLIGHTGROUP:GetPlayerElement()

  for _,_element in pairs(self.elements) do
    local element=_element --Ops.OpsGroup#OPSGROUP.Element
    if not element.ai then
      return element
    end
  end

  return nil
end

--- Get player element.
-- @param #FLIGHTGROUP self
-- @return #string Player name or `nil`.
function FLIGHTGROUP:GetPlayerName()

  local playerElement=self:GetPlayerElement()
  
  if playerElement then
    return playerElement.playerName
  end

  return nil
end

--- Set parking spot of element.
-- @param #FLIGHTGROUP self
-- @param Ops.OpsGroup#OPSGROUP.Element Element The element.
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot.
function FLIGHTGROUP:_SetElementParkingAt(Element, Spot)

  -- Element is parking here.
  Element.parking=Spot

  if Spot then

    -- Debug info.
    self:T(self.lid..string.format("Element %s is parking on spot %d", Element.name, Spot.TerminalID))
    
    -- Get flightcontrol.
    local fc=_DATABASE:GetFlightControl(Spot.AirbaseName)
    
    if fc and not self.flightcontrol then
      self:SetFlightControl(fc)
    end

    if self.flightcontrol then

      -- Set parking spot to OCCUPIED.
      self.flightcontrol:SetParkingOccupied(Element.parking, Element.name)
    end

  end

end

--- Set parking spot of element to free
-- @param #FLIGHTGROUP self
-- @param Ops.OpsGroup#OPSGROUP.Element Element The element.
function FLIGHTGROUP:_SetElementParkingFree(Element)

  if Element.parking then

    -- Set parking to FREE.
    if self.flightcontrol then
      self.flightcontrol:SetParkingFree(Element.parking)
    end

    -- Not parking any more.
    Element.parking=nil

  end

end

--- Get onboard number.
-- @param #FLIGHTGROUP self
-- @param #string unitname Name of the unit.
-- @return #string Modex.
function FLIGHTGROUP:_GetOnboardNumber(unitname)

  local group=UNIT:FindByName(unitname):GetGroup()

  -- Units of template group.
  local units=group:GetTemplate().units

  -- Get numbers.
  local numbers={}
  for _,unit in pairs(units) do

    if unitname==unit.name then
      return tostring(unit.onboard_num)
    end

  end

  return nil
end

--- Checks if a human player sits in the unit.
-- @param #FLIGHTGROUP self
-- @param Wrapper.Unit#UNIT unit Aircraft unit.
-- @return #boolean If true, human player inside the unit.
function FLIGHTGROUP:_IsHumanUnit(unit)

  -- Get player unit or nil if no player unit.
  local playerunit=self:_GetPlayerUnitAndName(unit:GetName())

  if playerunit then
    return true
  else
    return false
  end
end

--- Checks if a group has a human player.
-- @param #FLIGHTGROUP self
-- @param Wrapper.Group#GROUP group Aircraft group.
-- @return #boolean If true, human player inside group.
function FLIGHTGROUP:_IsHuman(group)

  -- Get all units of the group.
  local units=group:GetUnits()

  -- Loop over all units.
  for _,_unit in pairs(units) do
    -- Check if unit is human.
    local human=self:_IsHumanUnit(_unit)
    if human then
      return true
    end
  end

  return false
end

--- Returns the unit of a player and the player name. If the unit does not belong to a player, nil is returned.
-- @param #FLIGHTGROUP self
-- @param #string _unitName Name of the player unit.
-- @return Wrapper.Unit#UNIT Unit of player or nil.
-- @return #string Name of the player or nil.
function FLIGHTGROUP:_GetPlayerUnitAndName(_unitName)
  self:F2(_unitName)

  if _unitName ~= nil then

    -- Get DCS unit from its name.
    local DCSunit=Unit.getByName(_unitName)

    if DCSunit then

      local playername=DCSunit:getPlayerName()
      local unit=UNIT:Find(DCSunit)

      if DCSunit and unit and playername then
        return unit, playername
      end

    end

  end

  -- Return nil if we could not find a player.
  return nil,nil
end

--- Returns the parking spot of the element.
-- @param #FLIGHTGROUP self
-- @param Ops.OpsGroup#OPSGROUP.Element element Element of the flight group.
-- @param #number maxdist Distance threshold in meters. Default 5 m.
-- @param Wrapper.Airbase#AIRBASE airbase (Optional) The airbase to check for parking. Default is closest airbase to the element.
-- @return Wrapper.Airbase#AIRBASE.ParkingSpot Parking spot or nil if no spot is within distance threshold.
function FLIGHTGROUP:GetParkingSpot(element, maxdist, airbase)

  -- Coordinate of unit landed
  local coord=element.unit:GetCoordinate()

  -- Airbase.
  airbase=airbase or self:GetClosestAirbase()

  if airbase == nil then
    self:T(self.lid.."No airbase found for element "..element.name)
    return nil
  end

  -- Parking table of airbase.
  local parking=airbase.parking --:GetParkingSpotsTable()

  -- If airbase is ship, translate parking coords. Alternatively, we just move the coordinate of the unit to the origin of the map, which is way more efficient.
  if airbase and airbase:IsShip() then
    -- No need to compute the relative position if there is only one parking spot.
    if #parking > 1 then
      coord = airbase:GetRelativeCoordinate( coord.x, coord.y, coord.z )
    else
      coord.x=0
      coord.z=0
      maxdist=500 -- 100 meters was not enough, e.g. on the Seawise Giant, where the spot is 139 meters from the "center".
    end
  end

  local spot=nil --Wrapper.Airbase#AIRBASE.ParkingSpot
  local dist=nil
  local distmin=math.huge
  for _,_parking in pairs(parking) do
    local parking=_parking --Wrapper.Airbase#AIRBASE.ParkingSpot
    
    -- Distance to spot.
    dist=coord:Get2DDistance(parking.Coordinate)
    
    if dist<distmin then
      distmin=dist
      spot=_parking
    end
  end

  if distmin<=maxdist and not element.unit:InAir() then
    return spot
  else
    return nil
  end
end

--- Get holding time.
-- @param #FLIGHTGROUP self
-- @return #number Holding time in seconds or -1 if flight is not holding.
function FLIGHTGROUP:GetHoldingTime()
  if self.Tholding then
    return timer.getAbsTime()-self.Tholding
  end

  return -1
end

--- Get parking time.
-- @param #FLIGHTGROUP self
-- @return #number Holding time in seconds or -1 if flight is not holding.
function FLIGHTGROUP:GetParkingTime()
  if self.Tparking then
    return timer.getAbsTime()-self.Tparking
  end

  return -1
end

--- Search unoccupied parking spots at the airbase for all flight elements.
-- @param #FLIGHTGROUP self
-- @return Wrapper.Airbase#AIRBASE Closest airbase
function FLIGHTGROUP:GetClosestAirbase()

  local group=self.group --Wrapper.Group#GROUP
  
  if group and group:IsAlive() then
    
    local coord=group:GetCoordinate()
    local coalition=self:GetCoalition()

    if coord then
        local airbase=coord:GetClosestAirbase() --(nil, coalition)
        return airbase
    else
        return nil
    end
  else
  
    return nil
  
  end
end

--- Search unoccupied parking spots at the airbase for all flight elements.
-- @param #FLIGHTGROUP self
-- @param Wrapper.Airbase#AIRBASE airbase The airbase where we search for parking spots.
-- @return #table Table of coordinates and terminal IDs of free parking spots.
function FLIGHTGROUP:GetParking(airbase)

  -- Init default
  local scanradius=50
  local scanunits=true
  local scanstatics=true
  local scanscenery=false
  local verysafe=true

  -- Function calculating the overlap of two (square) objects.
  local function _overlap(l1,l2,dist)
    local safedist=(l1/2+l2/2)*1.05  -- 5% safety margine added to safe distance!
    local safe = (dist > safedist)
    return safe
  end

  -- Get client coordinates.
  local function _clients()
    local clients=_DATABASE.CLIENTS
    local coords={}
    for clientname, client in pairs(clients) do
      local template=_DATABASE:GetGroupTemplateFromUnitName(clientname)
      if template then
        local units=template.units
        for i,unit in pairs(units) do
          local coord=COORDINATE:New(unit.x, unit.alt, unit.y)
          coords[unit.name]=coord
        end
      end
    end
    return coords
  end

  -- Get airbase category.
  local airbasecategory=airbase:GetAirbaseCategory()

  -- Get parking spot data table. This contains all free and "non-free" spots.
  local parkingdata=airbase:GetParkingSpotsTable()

  -- List of obstacles.
  local obstacles={}

  -- Loop over all parking spots and get the currently present obstacles.
  -- How long does this take on very large airbases, i.e. those with hundereds of parking spots? Seems to be okay!
  -- The alternative would be to perform the scan once but with a much larger radius and store all data.
  for _,_parkingspot in pairs(parkingdata) do
    local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot

    -- Scan a radius of 100 meters around the spot.
    local _,_,_,_units,_statics,_sceneries=parkingspot.Coordinate:ScanObjects(scanradius, scanunits, scanstatics, scanscenery)

    -- Check all units.
    for _,_unit in pairs(_units) do
      local unit=_unit --Wrapper.Unit#UNIT
      local _coord=unit:GetCoordinate()
      local _size=self:_GetObjectSize(unit:GetDCSObject())
      local _name=unit:GetName()
      table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="unit"})
    end

    -- Check all clients.
    local clientcoords=_clients()
    for clientname,_coord in pairs(clientcoords) do
      table.insert(obstacles, {coord=_coord, size=15, name=clientname, type="client"})
    end

    -- Check all statics.
    for _,static in pairs(_statics) do
      local _vec3=static:getPoint()
      local _coord=COORDINATE:NewFromVec3(_vec3)
      local _name=static:getName()
      local _size=self:_GetObjectSize(static)
      table.insert(obstacles, {coord=_coord, size=_size, name=_name, type="static"})
    end

    -- Check all scenery.
    for _,scenery in pairs(_sceneries) do
      local _vec3=scenery:getPoint()
      local _coord=COORDINATE:NewFromVec3(_vec3)
      local _name=scenery:getTypeName()
      local _size=self:_GetObjectSize(scenery)
      table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="scenery"})
    end

  end

  -- Parking data for all assets.
  local parking={}

  -- Get terminal type.
  local terminaltype=self:_GetTerminal(self.attribute, airbase:GetAirbaseCategory())

  -- Loop over all units - each one needs a spot.
  for i,_element in pairs(self.elements) do
    local element=_element --Ops.OpsGroup#OPSGROUP.Element

    -- Loop over all parking spots.
    local gotit=false
    for _,_parkingspot in pairs(parkingdata) do
      local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot

      -- Check correct terminal type for asset. We don't want helos in shelters etc.
      if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) then

        -- Assume free and no problematic obstacle.
        local free=true
        local problem=nil

        -- Safe parking using TO_AC from DCS result.
        if verysafe and parkingspot.TOAC then
          free=false
          self:T2(self.lid..string.format("Parking spot %d is occupied by other aircraft taking off (TOAC).", parkingspot.TerminalID))
        end

        -- Loop over all obstacles.
        for _,obstacle in pairs(obstacles) do

          -- Check if aircraft overlaps with any obstacle.
          local dist=parkingspot.Coordinate:Get2DDistance(obstacle.coord)
          local safe=_overlap(element.size, obstacle.size, dist)

          -- Spot is blocked.
          if not safe then
            free=false
            problem=obstacle
            problem.dist=dist
            break
          end

        end

        -- Check flightcontrol data.
        if self.flightcontrol and self.flightcontrol.airbasename==airbase:GetName() then
          local problem=self.flightcontrol:IsParkingReserved(parkingspot) or self.flightcontrol:IsParkingOccupied(parkingspot)
          if problem then
            free=false
          end
        end

        -- Check if spot is free
        if free then

          -- Add parkingspot for this element.
          table.insert(parking, parkingspot)

          self:T2(self.lid..string.format("Parking spot %d is free for element %s!", parkingspot.TerminalID, element.name))

          -- Add the unit as obstacle so that this spot will not be available for the next unit.
          table.insert(obstacles, {coord=parkingspot.Coordinate, size=element.size, name=element.name, type="element"})

          gotit=true
          break

        else

          -- Debug output for occupied spots.
          self:T2(self.lid..string.format("Parking spot %d is occupied or not big enough!", parkingspot.TerminalID))

        end

      end -- check terminal type
    end -- loop over parking spots

    -- No parking spot for at least one asset :(
    if not gotit then
      self:T(self.lid..string.format("WARNING: No free parking spot for element %s", element.name))
      return nil
    end

  end -- loop over asset units

  return parking
end

--- Size of the bounding box of a DCS object derived from the DCS descriptor table. If boundinb box is nil, a size of zero is returned.
-- @param #FLIGHTGROUP self
-- @param DCS#Object DCSobject The DCS object for which the size is needed.
-- @return #number Max size of object in meters (length (x) or width (z) components not including height (y)).
-- @return #number Length (x component) of size.
-- @return #number Height (y component) of size.
-- @return #number Width (z component) of size.
function FLIGHTGROUP:_GetObjectSize(DCSobject)
  local DCSdesc=DCSobject:getDesc()
  if DCSdesc.box then
    local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x)  --length
    local y=DCSdesc.box.max.y+math.abs(DCSdesc.box.min.y)  --height
    local z=DCSdesc.box.max.z+math.abs(DCSdesc.box.min.z)  --width
    return math.max(x,z), x , y, z
  end
  return 0,0,0,0
end

--- Get the generalized attribute of a group.
-- @param #FLIGHTGROUP self
-- @return #string Generalized attribute of the group.
function FLIGHTGROUP:_GetAttribute()

  -- Default
  local attribute=FLIGHTGROUP.Attribute.OTHER

  local group=self.group  --Wrapper.Group#GROUP

  if group then

    --- Planes
    local transportplane=group:HasAttribute("Transports") and group:HasAttribute("Planes")
    local awacs=group:HasAttribute("AWACS")
    local fighter=group:HasAttribute("Fighters") or group:HasAttribute("Interceptors") or group:HasAttribute("Multirole fighters") or (group:HasAttribute("Bombers") and not group:HasAttribute("Strategic bombers"))
    local bomber=group:HasAttribute("Strategic bombers")
    local tanker=group:HasAttribute("Tankers")
    local uav=group:HasAttribute("UAVs")
    --- Helicopters
    local transporthelo=group:HasAttribute("Transport helicopters")
    local attackhelicopter=group:HasAttribute("Attack helicopters")

    -- Define attribute. Order is important.
    if transportplane then
      attribute=FLIGHTGROUP.Attribute.AIR_TRANSPORTPLANE
    elseif awacs then
      attribute=FLIGHTGROUP.Attribute.AIR_AWACS
    elseif fighter then
      attribute=FLIGHTGROUP.Attribute.AIR_FIGHTER
    elseif bomber then
      attribute=FLIGHTGROUP.Attribute.AIR_BOMBER
    elseif tanker then
      attribute=FLIGHTGROUP.Attribute.AIR_TANKER
    elseif transporthelo then
      attribute=FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO
    elseif attackhelicopter then
      attribute=FLIGHTGROUP.Attribute.AIR_ATTACKHELO
    elseif uav then
      attribute=FLIGHTGROUP.Attribute.AIR_UAV
    end

  end

  return attribute
end

--- Get the proper terminal type based on generalized attribute of the group.
--@param #FLIGHTGROUP self
--@param #FLIGHTGROUP.Attribute _attribute Generlized attibute of unit.
--@param #number _category Airbase category.
--@return Wrapper.Airbase#AIRBASE.TerminalType Terminal type for this group.
function FLIGHTGROUP:_GetTerminal(_attribute, _category)

  -- Default terminal is "large".
  local _terminal=AIRBASE.TerminalType.OpenBig

  if _attribute==FLIGHTGROUP.Attribute.AIR_FIGHTER or _attribute==FLIGHTGROUP.Attribute.AIR_UAV then
    -- Fighter ==> small.
    _terminal=AIRBASE.TerminalType.FighterAircraft
  elseif _attribute==FLIGHTGROUP.Attribute.AIR_BOMBER or _attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTPLANE or _attribute==FLIGHTGROUP.Attribute.AIR_TANKER or _attribute==FLIGHTGROUP.Attribute.AIR_AWACS then
    -- Bigger aircraft.
    _terminal=AIRBASE.TerminalType.OpenBig
  elseif _attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO or _attribute==FLIGHTGROUP.Attribute.AIR_ATTACKHELO then
    -- Helicopter.
    _terminal=AIRBASE.TerminalType.HelicopterUsable
  else
    --_terminal=AIRBASE.TerminalType.OpenMedOrBig
  end

  -- For ships, we allow medium spots for all fixed wing aircraft. There are smaller tankers and AWACS aircraft that can use a carrier.
  if _category==Airbase.Category.SHIP then
    if not (_attribute==FLIGHTGROUP.Attribute.AIR_TRANSPORTHELO or _attribute==FLIGHTGROUP.Attribute.AIR_ATTACKHELO) then
      _terminal=AIRBASE.TerminalType.OpenMedOrBig
    end
  end

  return _terminal
end

--- Check if group got stuck. This overwrites the OPSGROUP function.
-- Here we only check if stuck whilst taxiing.
-- @param #FLIGHTGROUP self
-- @param #boolean Despawn If `true`, despawn group if stuck.
-- @return #number Time in seconds the group got stuck or nil if not stuck.
function FLIGHTGROUP:_CheckStuck(Despawn)

  -- Cases we are not stuck.
  if not self:IsTaxiing() then
    return nil
  end

  -- Current time.
  local Tnow=timer.getTime()

  -- Expected speed in m/s.
  local ExpectedSpeed=5

  -- Current speed in m/s.
  local speed=self:GetVelocity()

  -- Check speed.
  if speed<0.1 then

    if ExpectedSpeed>0 and not self.stuckTimestamp then
      self:T2(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected", speed, ExpectedSpeed))
      self.stuckTimestamp=Tnow
      self.stuckVec3=self:GetVec3()
    end

  else
    -- Moving (again).
    self.stuckTimestamp=nil
  end
  
  local holdtime=nil

  -- Somehow we are not moving...
  if self.stuckTimestamp then

    -- Time we are holding.
    holdtime=Tnow-self.stuckTimestamp
    
    -- Trigger stuck event.
    self:Stuck(holdtime)

    if holdtime>=5*60 and holdtime<15*60 then

      -- Debug warning.
      self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime))

    elseif holdtime>=15*60 then

      -- Debug warning.
      self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime))

      -- Look for a current mission and cancel it as we do not seem to be able to perform it.
      local mission=self:GetMissionCurrent()
      
      if mission then
        self:T(self.lid..string.format("WARNING: Cancelling mission %s [%s] due to being stuck", mission:GetName(), mission:GetType()))
        self:MissionCancel(mission)
      end

      if self.stuckDespawn then
        if self.legion then
          self:T(self.lid..string.format("Asset is returned to its legion after being stuck!"))
          self:ReturnToLegion()
        else
          self:T(self.lid..string.format("Despawning group after being stuck!"))
          self:Despawn()
        end
      end

    end

  end

  return holdtime
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- OPTION FUNCTIONS
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- MENU FUNCTIONS
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Update menu.
--@param #FLIGHTGROUP self
--@param #number delay Delay in seconds.
function FLIGHTGROUP:_UpdateMenu(delay)

  if delay and delay>0 then
    -- Delayed call.
    self:ScheduleOnce(delay, FLIGHTGROUP._UpdateMenu, self)
  else

    -- Player element.
    local player=self:GetPlayerElement()
    
    if player and player.status~=OPSGROUP.ElementStatus.DEAD then
  
      -- Debug text.
      if self.verbose>=2 then
        local text=string.format("Updating MENU: State=%s, ATC=%s [%s]", self:GetState(), 
        self.flightcontrol and self.flightcontrol.airbasename or "None", self.flightcontrol and self.flightcontrol:GetFlightStatus(self) or "Unknown")
      
        -- Message to group.
        MESSAGE:New(text, 5):ToGroup(self.group)
        self:T(self.lid..text)
      end
    
      -- Get current position of player.
      local position=self:GetCoordinate(nil, player.name)
  
      -- Get all FLIGHTCONTROLS
      local fc={}
      for airbasename,_flightcontrol in pairs(_DATABASE.FLIGHTCONTROLS) do
        local flightcontrol=_flightcontrol --OPS.FlightControl#FLIGHTCONTROL
  
        -- Get coord of airbase.
        local coord=flightcontrol:GetCoordinate()
  
        -- Distance to flight.
        local dist=coord:Get2DDistance(position)
  
        -- Add to table.
        table.insert(fc, {airbasename=airbasename, dist=dist})
      end
  
      -- Sort table wrt distance to airbases.
      local function _sort(a,b)
        return a.dist<b.dist
      end
      table.sort(fc, _sort)
      
      -- Remove all submenus.
      self.menu.atc.root:RemoveSubMenus()
      
      -- Create help menu.
      self:_CreateMenuAtcHelp(self.menu.atc.root)
      
      -- Max menu entries.
      local N=7
  
      -- If there is a designated FC, we put it first.
      local gotairbase=nil
      if self.flightcontrol then
        self.flightcontrol:_CreatePlayerMenu(self, self.menu.atc.root)
        gotairbase=self.flightcontrol.airbasename
        N=N-1
      end
  
      -- Max 8 entries in F10 menu.
      for i=1,math.min(#fc,N) do
        local airbasename=fc[i].airbasename
        if gotairbase==nil or airbasename~=gotairbase then
          local flightcontrol=_DATABASE:GetFlightControl(airbasename)
          flightcontrol:_CreatePlayerMenu(self, self.menu.atc.root)
        end
      end
      
    else
      self:E(self.lid.."ERROR: Player dead in update menu!")
    end
    
  end
end

--- Create player menu.
-- @param #FLIGHTGROUP self
-- @param #table rootmenu ATC root menu table.
function FLIGHTGROUP:_CreateMenuAtcHelp(rootmenu)

  -- Help menu.
  local helpmenu=MENU_GROUP:New(self.group, "Help",  rootmenu)
  
  -- Group name.
  local groupname=self.groupname
  
  ---
  -- Skill level menu
  ---
  local skillmenu=MENU_GROUP:New(self.group, "Skill Level", helpmenu)  
  MENU_GROUP_COMMAND:New(self.group, "Student",     skillmenu, self._PlayerSkill,  self, FLIGHTGROUP.PlayerSkill.STUDENT)
  MENU_GROUP_COMMAND:New(self.group, "Aviator",     skillmenu, self._PlayerSkill,  self, FLIGHTGROUP.PlayerSkill.AVIATOR)
  MENU_GROUP_COMMAND:New(self.group, "Graduate",    skillmenu, self._PlayerSkill,  self, FLIGHTGROUP.PlayerSkill.GRADUATE)
  MENU_GROUP_COMMAND:New(self.group, "Instructor",  skillmenu, self._PlayerSkill,  self, FLIGHTGROUP.PlayerSkill.INSTRUCTOR)
  
  ---
  -- Commands
  ---
  MENU_GROUP_COMMAND:New(self.group, "Subtitles On/Off", helpmenu, self._PlayerSubtitles,     self)
  MENU_GROUP_COMMAND:New(self.group, "My Voice On/Off",  helpmenu, self._MenuNotImplemented,  self, groupname)
  MENU_GROUP_COMMAND:New(self.group, "Mark Parking",     helpmenu, self._MarkParking,         self)
  MENU_GROUP_COMMAND:New(self.group, "Update Menu",      helpmenu, self._UpdateMenu,          self, 0)
  MENU_GROUP_COMMAND:New(self.group, "My Status",        helpmenu, self._PlayerMyStatus,      self, groupname)

end

--- Player menu not implemented.
-- @param #FLIGHTGROUP self
-- @param #string groupname Name of the flight group.
function FLIGHTGROUP:_MenuNotImplemented(groupname)

  -- Get flight group.
  local flight=_DATABASE:GetOpsGroup(groupname) --Ops.FlightGroup#FLIGHTGROUP
  
  if flight then
  
    local text=string.format("Sorry, this feature is not implemented yet!")
    
    MESSAGE:New(text, 10, nil, true):ToGroup(flight.group)
  
  end
  
end

--- Player status.
-- @param #FLIGHTGROUP self
function FLIGHTGROUP:_PlayerMyStatus()

  -- Flight control.
  local fc=self.flightcontrol
  
  -- Player data.
  local playerdata=self:_GetPlayerData()
  
  -- Player element.
  local playerElement=self:GetPlayerElement()

  -- Status text.
  local text=string.format("My Status:")
  text=text..string.format("\nPlayer Name: %s", tostring(playerdata.name))
  text=text..string.format("\nCallsign: %s", tostring(self:GetCallsignName()))
  text=text..string.format("\nFlight status: %s", tostring(self:GetState()))
  text=text..string.format("\nFlight control: %s [%s]", tostring(fc and fc.airbasename or "N/A"), tostring(fc and fc:GetFlightStatus(self) or "N/A"))
  text=text..string.format("\nSubtitles: %s", tostring(playerdata.subtitles))
  text=text..string.format("\nMy Voice: %s", tostring(playerdata.myvoice))
  
  if fc then
    if playerElement.parking then
      local spot=fc:GetParkingSpotByID(playerElement.parking.TerminalID)
      if spot then
        text=text..string.format("\nParking spot: %d [%s]", spot.TerminalID, spot.Status or "Unknown")
      end
    end
  end
  
  -- Send message.
  MESSAGE:New(text, 10, nil, true):ToGroup(self.group)
  
end

--- Player set subtitles.
-- @param #FLIGHTGROUP self
function FLIGHTGROUP:_PlayerSubtitles()

  -- Get Player data.
  local playerData=self:_GetPlayerData()
  
  if playerData then
  
    -- Switch setting.
    playerData.subtitles=not playerData.subtitles
    
    local onoff = playerData.subtitles == true and "ON" or "OFF"
    
    -- Display message.
    MESSAGE:New(string.format("%s, subtitles are now %s", playerData.name, onoff), 10, nil, true):ToGroup(self.group)
  
  else
    --TODO: Error
  end
    
end

--- Player mark parking.
-- @param #FLIGHTGROUP self
function FLIGHTGROUP:_MarkParking()

  local playerElement=self:GetPlayerElement()
  
  if playerElement then
  
    -- Player name.
    local playerName=tostring(playerElement.playerName)
    
    -- Message text.
    local message=string.format("No assigned parking spot for you could be found, %s", playerName)
    
    if playerElement.parking then
    
      local terminalID=playerElement.parking.TerminalID
      local spotStatus=tostring(playerElement.parking.Status)
    
      -- Marker text.
      local text=string.format("Your parking spot, %s\nTerminal ID=%d [%s]", playerName, terminalID, spotStatus)
      
      -- Text message.
      message=string.format("%s, your parking spot is Terminal ID=%d [%s]. Check the marker on the F10 map.", playerName, terminalID, spotStatus)
      
      -- New marker.
      playerElement.parking.Coordinate:MarkToGroup(text, self.group)
      
    end
    
    -- Text message to group.
    MESSAGE:New(string.format(message, playerName), 10):ToGroup(self.group)
    
  end
    
end

--- Player set skill.
-- @param #FLIGHTGROUP self
-- @param #string Skill Skill.
function FLIGHTGROUP:_PlayerSkill(Skill)

  -- Get Player data.
  local playerData=self:_GetPlayerData()
  
  if playerData then
  
    -- Switch setting.
    playerData.skill=Skill
    
    -- Display message.
    MESSAGE:New(string.format("%s, your skill is %s", playerData.name, tostring(playerData.skill)), 10, nil, true):ToGroup(self.group)
  
  else
    --TODO: Error
  end
    
end


--- Init player data.
-- @param #FLIGHTGROUP self
-- @param #string PlayerName Player name.
-- @return #FLIGHTGROUP.PlayerData Player data.
function FLIGHTGROUP:_InitPlayerData(PlayerName)

  if PlayerName then
  
    -- Check if data is already there.
    local playerData=FLIGHTGROUP.Players[PlayerName] --#FLIGHTGROUP.PlayerData
  
    if not playerData then
    
      local playerData={} --#FLIGHTGROUP.PlayerData
      playerData.name=PlayerName  
      playerData.skill=FLIGHTGROUP.PlayerSkill.STUDENT
      playerData.subtitles=true
      playerData.myvoice=true
      
      -- Debug message.
      self:T(self.lid..string.format("Init player data for %s", PlayerName))
      
      -- Set data globally.
      FLIGHTGROUP.Players[PlayerName]=playerData
    end
      
    return playerData
    
  else
    self:E(self.lid..string.format("ERROR: Player name is nil!"))
  end
  
  return nil
end

--- Get player data.
-- @param #FLIGHTGROUP self
-- @return #FLIGHTGROUP.PlayerData Player data.
function FLIGHTGROUP:_GetPlayerData()

  -- Get player element.
  local playerElement=self:GetPlayerElement()
  
  if playerElement and playerElement.playerName then
    return FLIGHTGROUP.Players[playerElement.playerName]
  end

  return nil
end  
    
--- Get distance to parking spot. Takes extra care of ships.
-- @param #FLIGHTGROUP self
-- @param Wrapper.Airbase#AIRBASE.ParkingSpot Spot Parking Spot.
-- @param Core.Point#COORDINATE Coordinate Reference coordinate.
-- @return #number Distance to parking spot in meters.
function FLIGHTGROUP:_GetDistToParking(Spot, Coordinate)

  local dist=99999
  
  if Spot then
  
    -- Get the airbase this spot belongs to.
    local airbase=AIRBASE:FindByName(Spot.AirbaseName)
    

    if airbase:IsShip() then --or airbase:IsHelipad() then
      
      -- Vec2 of airbase.
      local a=airbase:GetVec2()
      
      -- Vec2 of parking spot.
      local b=Spot.Coordinate:GetVec2()
      
      -- Vec2 of ref coordinate.
      local c=Coordinate:GetVec2()
      
      -- Vector from ref coord to airbase. This still needs to be rotated.
      local t=UTILS.Vec2Substract(c,a)
      
      -- Get the heading of the unit.
      local unit=UNIT:FindByName(Spot.AirbaseName)
      local hdg=unit:GetHeading()
      
      -- Rotate the vector so that it corresponds to facing "North".
      t=UTILS.Vec2Rotate2D(t, -hdg)
      
      -- Distance from spot to ref coordinate.
      dist=UTILS.VecDist2D(b,t)
    else
      -- Normal case.
      dist=Coordinate:Get2DDistance(Spot.Coordinate)  
    end
  
  end

  return dist
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Flotilla is a small naval group belonging to a fleet.
--
-- **Main Features:**
--
--    * Set parameters like livery, skill valid for all flotilla members.
--    * Define mission types, this flotilla can perform (see Ops.Auftrag#AUFTRAG).
--    * Pause/unpause flotilla operations.
--
-- ===
--
-- ### Author: **funkyfranky**
-- 
-- ===
-- @module Ops.Flotilla
-- @image OPS_Flotilla.png


--- FLOTILLA class.
-- @type FLOTILLA
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field Ops.OpsGroup#OPSGROUP.WeaponData weaponData Weapon data table with key=BitType.
-- @extends Ops.Cohort#COHORT

--- *No captain can do very wrong if he places his ship alongside that of an enemy.* -- Horation Nelson
--
-- ===
--
-- # The FLOTILLA Concept
-- 
-- A FLOTILLA is an essential part of a FLEET.
--
--
--
-- @field #FLOTILLA
FLOTILLA = {
  ClassName      = "FLOTILLA",
  verbose        =     0,
  weaponData     =    {},
}

--- FLOTILLA class version.
-- @field #string version
FLOTILLA.version="0.1.0"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: A lot.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new FLOTILLA object and start the FSM.
-- @param #FLOTILLA self
-- @param #string TemplateGroupName Name of the template group.
-- @param #number Ngroups Number of asset groups of this flotilla. Default 3.
-- @param #string FlotillaName Name of the flotilla. Must be **unique**!
-- @return #FLOTILLA self
function FLOTILLA:New(TemplateGroupName, Ngroups, FlotillaName)

  -- Inherit everything from COHORT class.
  local self=BASE:Inherit(self, COHORT:New(TemplateGroupName, Ngroups, FlotillaName)) -- #FLOTILLA
  
  -- All flotillas get mission type Nothing.
  self:AddMissionCapability(AUFTRAG.Type.NOTHING, 50)
  
  -- Is naval.
  self.isNaval=true

  -- Get initial ammo.
  self.ammo=self:_CheckAmmo()

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  -- TODO: Flotilla specific user functions.

--- Set fleet of this flotilla.
-- @param #FLOTILLA self
-- @param Ops.Fleet#FLEET Fleet The fleet.
-- @return #FLOTILLA self
function FLOTILLA:SetFleet(Fleet)
  self.legion=Fleet
  return self
end

--- Get fleet of this flotilla.
-- @param #FLOTILLA self
-- @return Ops.Fleet#FLEET The fleet.
function FLOTILLA:GetFleet()
  return self.legion
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start & Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers.
-- @param #FLOTILLA self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLOTILLA:onafterStart(From, Event, To)

  -- Short info.
  local text=string.format("Starting %s v%s %s", self.ClassName, self.version, self.name)
  self:I(self.lid..text)

  -- Start the status monitoring.
  self:__Status(-1)
end

--- On after "Status" event.
-- @param #FLOTILLA self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function FLOTILLA:onafterStatus(From, Event, To)

  if self.verbose>=1 then

    -- FSM state.
    local fsmstate=self:GetState()
  
    local callsign=self.callsignName and UTILS.GetCallsignName(self.callsignName) or "N/A"
    local skill=self.skill and tostring(self.skill) or "N/A"
    
    local NassetsTot=#self.assets
    local NassetsInS=self:CountAssets(true)
    local NassetsQP=0 ; local NassetsP=0 ; local NassetsQ=0  
    if self.legion then
      NassetsQP, NassetsP, NassetsQ=self.legion:CountAssetsOnMission(nil, self)
    end
    
    -- Short info.
    local text=string.format("%s [Type=%s, Call=%s, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", 
    fsmstate, self.aircrafttype, callsign, skill, NassetsTot, NassetsInS, NassetsQP, NassetsP, NassetsQ)
    self:T(self.lid..text)
    
    -- Weapon data info.
    if self.verbose>=3 and self.weaponData then
      local text="Weapon Data:"
      for bit,_weapondata in pairs(self.weaponData) do
        local weapondata=_weapondata --Ops.OpsGroup#OPSGROUP.WeaponData
        text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km", bit, weapondata.RangeMin/1000, weapondata.RangeMax/1000)
      end
      self:I(self.lid..text)
    end
    
    -- Check if group has detected any units.
    self:_CheckAssetStatus()
    
  end  
  
  if not self:IsStopped() then
    self:__Status(-60)
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------



-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Office of Military Intelligence.
--
-- **Main Features:**
--
--    * Detect and track contacts consistently
--    * Detect and track clusters of contacts consistently
--    * Use FSM events to link functionality into your scripts
--    * Easy setup
--
-- ===
--
-- ### Author: **funkyfranky**
-- @module Ops.Intel
-- @image OPS_Intel.png


--- INTEL class.
-- @type INTEL
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field #number coalition Coalition side number, e.g. `coalition.side.RED`.
-- @field #string alias Name of the agency.
-- @field Core.Set#SET_GROUP detectionset Set of detection groups, aka agents.
-- @field #table filterCategory Filter for unit categories.
-- @field #table filterCategoryGroup Filter for group categories.
-- @field Core.Set#SET_ZONE acceptzoneset Set of accept zones. If defined, only contacts in these zones are considered.
-- @field Core.Set#SET_ZONE rejectzoneset Set of reject zones. Contacts in these zones are not considered, even if they are in accept zones.
-- @field Core.Set#SET_ZONE conflictzoneset Set of conflict zones. Contacts in these zones are considered, even if they are not in accept zones or if they are in reject zones.
-- @field #table Contacts Table of detected items.
-- @field #table ContactsLost Table of lost detected items.
-- @field #table ContactsUnknown Table of new detected items.
-- @field #table Clusters Clusters of detected groups.
-- @field #boolean clusteranalysis If true, create clusters of detected targets.
-- @field #boolean clustermarkers If true, create cluster markers on F10 map.
-- @field #number clustercounter Running number of clusters.
-- @field #number dTforget Time interval in seconds before a known contact which is not detected any more is forgotten.
-- @field #number clusterradius Radius in meters in which groups/units are considered to belong to a cluster.
-- @field #number prediction Seconds default to be used with CalcClusterFuturePosition.
-- @field #boolean detectStatics If `true`, detect STATIC objects. Default `false`.
-- @field #number statusupdate Time interval in seconds after which the status is refreshed. Default 60 sec. Should be negative.
-- @extends Core.Fsm#FSM

--- Top Secret!
--
-- ===
--
-- # The INTEL Concept
--
--  * Lightweight replacement for @{Functional.Detection#DETECTION}
--  * Detect and track contacts consistently
--  * Detect and track clusters of contacts consistently
--  * Once detected and still alive, planes will be tracked 10 minutes, helicopters 20 minutes, ships and trains 1 hour, ground units 2 hours
--  * Use FSM events to link functionality into your scripts
--
-- # Basic Usage
--
-- ## Set up a detection SET_GROUP
--
--     Red_DetectionSetGroup = SET_GROUP:New()
--     Red_DetectionSetGroup:FilterPrefixes( { "Red EWR" } )
--     Red_DetectionSetGroup:FilterOnce()
--
-- ## New Intel type detection for the red side, logname "KGB"
--
--     RedIntel = INTEL:New(Red_DetectionSetGroup, "red", "KGB")
--     RedIntel:SetClusterAnalysis(true, true)
--     RedIntel:SetVerbosity(2)
--     RedIntel:__Start(2)
--
-- ## Hook into new contacts found
--
--     function RedIntel:OnAfterNewContact(From, Event, To, Contact)
--       local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown")
--       MESSAGE:New(text, 15, "KGB"):ToAll()
--     end
--
--  ## And/or new clusters found
--
--     function RedIntel:OnAfterNewCluster(From, Event, To, Cluster)
--       local text = string.format("NEW cluster #%d of size %d", Cluster.index, Cluster.size)
--       MESSAGE:New(text,15,"KGB"):ToAll()
--     end
--
--
-- @field #INTEL
INTEL = {
  ClassName       = "INTEL",
  verbose         =     0,
  lid             =   nil,
  alias           =   nil,
  filterCategory  =    {},
  detectionset    =   nil,
  Contacts        =    {},
  ContactsLost    =    {},
  ContactsUnknown =    {},
  Clusters        =    {},
  clustercounter  =     1,
  clusterradius   = 15000,
  clusteranalysis =  true,
  clustermarkers  = false,
  clusterarrows   = false,
  prediction      =   300,
  detectStatics   = false,
}

--- Detected item info.
-- @type INTEL.Contact
-- @field #string groupname Name of the group.
-- @field Wrapper.Group#GROUP group The contact group.
-- @field #string typename Type name of detected item.
-- @field #number category Category number.
-- @field #string categoryname Category name.
-- @field #string attribute Generalized attribute.
-- @field #number threatlevel Threat level of this item.
-- @field #number Tdetected Time stamp in abs. mission time seconds when this item was last detected.
-- @field Core.Point#COORDINATE position Last known position of the item.
-- @field DCS#Vec3 velocity 3D velocity vector. Components x,y and z in m/s.
-- @field #number speed Last known speed in m/s.
-- @field #boolean isship If `true`, contact is a naval group.
-- @field #boolean ishelo If `true`, contact is a helo group.
-- @field #boolean isground If `true`, contact is a ground group.
-- @field #boolean isStatic If `true`, contact is a STATIC object.
-- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this contact.
-- @field Ops.Target#TARGET target The Target attached to this contact.
-- @field #string recce The name of the recce unit that detected this contact.
-- @field #string ctype Contact type of #INTEL.Ctype.
-- @field #string platform [AIR] Contact platform name, e.g. Foxbat, Flanker_E, defaults to Bogey if unknown
-- @field #number heading [AIR] Heading of the contact, if available.
-- @field #boolean maneuvering [AIR] Contact has changed direction by >10 deg.
-- @field #number altitude [AIR] Flight altitude of the contact in meters.

--- Cluster info.
-- @type INTEL.Cluster
-- @field #number index Cluster index.
-- @field #number size Number of groups in the cluster.
-- @field #table Contacts Table of contacts in the cluster.
-- @field #number threatlevelMax Max threat level of cluster.
-- @field #number threatlevelSum Sum of threat levels.
-- @field #number threatlevelAve Average of threat levels.
-- @field Core.Point#COORDINATE coordinate Coordinate of the cluster.
-- @field Wrapper.Marker#MARKER marker F10 marker.
-- @field #number markerID Marker ID.
-- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this cluster.
-- @field #string ctype Cluster type of #INTEL.Ctype.
-- @field #number altitude [AIR] Average flight altitude of the cluster in meters.

--- Contact or cluster type.
-- @type INTEL.Ctype
-- @field #string GROUND Ground.
-- @field #string NAVAL Ship.
-- @field #string AIRCRAFT Airpane or helicopter.
-- @field #string STRUCTURE Static structure.
INTEL.Ctype={
  GROUND="Ground",
  NAVAL="Naval",
  AIRCRAFT="Aircraft",
  STRUCTURE="Structure"
}

--- INTEL class version.
-- @field #string version
INTEL.version="0.3.6"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Add min cluster size. Only create new clusters if they have a certain group size.
-- TODO: process detected set asynchroniously for better performance.
-- DONE: Add statics.
-- DONE: Filter detection methods.
-- DONE: Accept zones.
-- DONE: Reject zones.
-- NOGO: SetAttributeZone --> return groups of generalized attributes in a zone.
-- DONE: Loose units only if they remain undetected for a given time interval. We want to avoid fast oscillation between detected/lost states. Maybe 1-5 min would be a good time interval?!
-- DONE: Combine units to groups for all, new and lost.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new INTEL object and start the FSM.
-- @param #INTEL self
-- @param Core.Set#SET_GROUP DetectionSet Set of detection groups.
-- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral".
-- @param #string Alias An *optional* alias how this object is called in the logs etc.
-- @return #INTEL self
function INTEL:New(DetectionSet, Coalition, Alias)

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #INTEL

  -- Detection set.
  self.detectionset=DetectionSet or SET_GROUP:New()

  if Coalition and type(Coalition)=="string" then
    if Coalition=="blue" then
      Coalition=coalition.side.BLUE
    elseif Coalition=="red" then
      Coalition=coalition.side.RED
    elseif Coalition=="neutral" then
      Coalition=coalition.side.NEUTRAL
    else
      self:E("ERROR: Unknown coalition in INTEL!")
    end
  end

  -- Determine coalition from first group in set.
  self.coalition=Coalition or DetectionSet:CountAlive()>0 and DetectionSet:GetFirst():GetCoalition() or nil

  -- Filter coalition.
  if self.coalition then
    local coalitionname=UTILS.GetCoalitionName(self.coalition):lower()
    self.detectionset:FilterCoalitions(coalitionname)
  end

  -- Filter once.
  self.detectionset:FilterOnce()

  -- Set alias.
  if Alias then
    self.alias=tostring(Alias)
  else
    self.alias="INTEL SPECTRE"
    if self.coalition then
      if self.coalition==coalition.side.RED then
        self.alias="INTEL KGB"
      elseif self.coalition==coalition.side.BLUE then
        self.alias="INTEL CIA"
      end
    end
  end

  self.DetectVisual = true
  self.DetectOptical = true
  self.DetectRadar = true
  self.DetectIRST = true
  self.DetectRWR = true
  self.DetectDLINK = true

  self.statusupdate = -60

  -- Set some string id for output to DCS.log file.
  self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown")

  -- Start State.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  --                 From State  -->   Event        -->     To State
  self:AddTransition("Stopped",       "Start",              "Running")     -- Start FSM.
  self:AddTransition("*",             "Status",             "*")           -- INTEL status update.
  self:AddTransition("*",             "Stop",               "Stopped")     -- Stop FSM.

  self:AddTransition("*",             "Detect",             "*")           -- Start detection run. Not implemented yet!

  self:AddTransition("*",             "NewContact",         "*")           -- New contact has been detected.
  self:AddTransition("*",             "LostContact",        "*")           -- Contact could not be detected any more.

  self:AddTransition("*",             "NewCluster",         "*")           -- New cluster has been detected.
  self:AddTransition("*",             "LostCluster",        "*")           -- Cluster could not be detected any more.


  -- Defaults
  self:SetForgetTime()
  self:SetAcceptZones()
  self:SetRejectZones()
  self:SetConflictZones()

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the INTEL. Initializes parameters and starts event handlers.
  -- @function [parent=#INTEL] Start
  -- @param #INTEL self

  --- Triggers the FSM event "Start" after a delay. Starts the INTEL. Initializes parameters and starts event handlers.
  -- @function [parent=#INTEL] __Start
  -- @param #INTEL self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Stop". Stops the INTEL and all its event handlers.
  -- @param #INTEL self

  --- Triggers the FSM event "Stop" after a delay. Stops the INTEL and all its event handlers.
  -- @function [parent=#INTEL] __Stop
  -- @param #INTEL self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Status".
  -- @function [parent=#INTEL] Status
  -- @param #INTEL self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#INTEL] __Status
  -- @param #INTEL self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "NewContact".
  -- @function [parent=#INTEL] NewContact
  -- @param #INTEL self
  -- @param #INTEL.Contact Contact Detected contact.

  --- Triggers the FSM event "NewContact" after a delay.
  -- @function [parent=#INTEL] NewContact
  -- @param #INTEL self
  -- @param #number delay Delay in seconds.
  -- @param #INTEL.Contact Contact Detected contact.

  --- On After "NewContact" event.
  -- @function [parent=#INTEL] OnAfterNewContact
  -- @param #INTEL self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #INTEL.Contact Contact Detected contact.


  --- Triggers the FSM event "LostContact".
  -- @function [parent=#INTEL] LostContact
  -- @param #INTEL self
  -- @param #INTEL.Contact Contact Lost contact.

  --- Triggers the FSM event "LostContact" after a delay.
  -- @function [parent=#INTEL] LostContact
  -- @param #INTEL self
  -- @param #number delay Delay in seconds.
  -- @param #INTEL.Contact Contact Lost contact.

  --- On After "LostContact" event.
  -- @function [parent=#INTEL] OnAfterLostContact
  -- @param #INTEL self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #INTEL.Contact Contact Lost contact.


  --- Triggers the FSM event "NewCluster".
  -- @function [parent=#INTEL] NewCluster
  -- @param #INTEL self
  -- @param #INTEL.Cluster Cluster Detected cluster.

  --- Triggers the FSM event "NewCluster" after a delay.
  -- @function [parent=#INTEL] NewCluster
  -- @param #INTEL self
  -- @param #number delay Delay in seconds.
  -- @param #INTEL.Cluster Cluster Detected cluster.

  --- On After "NewCluster" event.
  -- @function [parent=#INTEL] OnAfterNewCluster
  -- @param #INTEL self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #INTEL.Cluster Cluster Detected cluster.


  --- Triggers the FSM event "LostCluster".
  -- @function [parent=#INTEL] LostCluster
  -- @param #INTEL self
  -- @param #INTEL.Cluster Cluster Lost cluster.
  -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`.

  --- Triggers the FSM event "LostCluster" after a delay.
  -- @function [parent=#INTEL] LostCluster
  -- @param #INTEL self
  -- @param #number delay Delay in seconds.
  -- @param #INTEL.Cluster Cluster Lost cluster.
  -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`.

  --- On After "LostCluster" event.
  -- @function [parent=#INTEL] OnAfterLostCluster
  -- @param #INTEL self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #INTEL.Cluster Cluster Lost cluster.
  -- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`.

  return self
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set accept zones. Only contacts detected in this/these zone(s) are considered.
-- @param #INTEL self
-- @param Core.Set#SET_ZONE AcceptZoneSet Set of accept zones.
-- @return #INTEL self
function INTEL:SetAcceptZones(AcceptZoneSet)
  self.acceptzoneset=AcceptZoneSet or SET_ZONE:New()
  return self
end

--- Add an accept zone. Only contacts detected in this zone are considered.
-- @param #INTEL self
-- @param Core.Zone#ZONE AcceptZone Add a zone to the accept zone set.
-- @return #INTEL self
function INTEL:AddAcceptZone(AcceptZone)
  self.acceptzoneset:AddZone(AcceptZone)
  return self
end

--- Remove an accept zone from the accept zone set.
-- @param #INTEL self
-- @param Core.Zone#ZONE AcceptZone Remove a zone from the accept zone set.
-- @return #INTEL self
function INTEL:RemoveAcceptZone(AcceptZone)
  self.acceptzoneset:Remove(AcceptZone:GetName(), true)
  return self
end

--- Set reject zones. Contacts detected in this/these zone(s) are rejected and not reported by the detection.
-- Note that reject zones overrule accept zones, i.e. if a unit is inside an accept zone and inside a reject zone, it is rejected.
-- @param #INTEL self
-- @param Core.Set#SET_ZONE RejectZoneSet Set of reject zone(s).
-- @return #INTEL self
function INTEL:SetRejectZones(RejectZoneSet)
  self.rejectzoneset=RejectZoneSet or SET_ZONE:New()
  return self
end

--- Add a reject zone. Contacts detected in this zone are rejected and not reported by the detection.
-- Note that reject zones overrule accept zones, i.e. if a unit is inside an accept zone and inside a reject zone, it is rejected.
-- @param #INTEL self
-- @param Core.Zone#ZONE RejectZone Add a zone to the reject zone set.
-- @return #INTEL self
function INTEL:AddRejectZone(RejectZone)
  self.rejectzoneset:AddZone(RejectZone)
  return self
end

--- Remove a reject zone from the reject zone set.
-- @param #INTEL self
-- @param Core.Zone#ZONE RejectZone Remove a zone from the reject zone set.
-- @return #INTEL self
function INTEL:RemoveRejectZone(RejectZone)
  self.rejectzoneset:Remove(RejectZone:GetName(), true)
  return self
end

--- Set conflict zones. Contacts detected in this/these zone(s) are reported by the detection.
-- Note that conflict zones overrule all other zones, i.e. if a unit is outside of an accept zone and inside a reject zone, it is still reported if inside a conflict zone.
-- @param #INTEL self
-- @param Core.Set#SET_ZONE ConflictZoneSet Set of conflict zone(s).
-- @return #INTEL self
function INTEL:SetConflictZones(ConflictZoneSet)
  self.conflictzoneset=ConflictZoneSet or SET_ZONE:New()
  return self
end

--- Add a conflict zone. Contacts detected in this zone are conflicted and not reported by the detection.
-- Note that conflict zones overrule all other zones, i.e. if a unit is outside of an accept zone and inside a reject zone, it is still reported if inside a conflict zone.
-- @param #INTEL self
-- @param Core.Zone#ZONE ConflictZone Add a zone to the conflict zone set.
-- @return #INTEL self
function INTEL:AddConflictZone(ConflictZone)
  self.conflictzoneset:AddZone(ConflictZone)
  return self
end

--- Remove a conflict zone from the conflict zone set.
-- Note that conflict zones overrule all other zones, i.e. if a unit is outside of an accept zone and inside a reject zone, it is still reported if inside a conflict zone.
-- @param #INTEL self
-- @param Core.Zone#ZONE ConflictZone Remove a zone from the conflict zone set.
-- @return #INTEL self
function INTEL:RemoveConflictZone(ConflictZone)
  self.conflictzoneset:Remove(ConflictZone:GetName(), true)
  return self
end

--- **OBSOLETE, will be removed in next version!**  Set forget contacts time interval.
-- Previously known contacts that are not detected any more, are "lost" after this time.
-- This avoids fast oscillations between a contact being detected and undetected.
-- @param #INTEL self
-- @param #number TimeInterval Time interval in seconds. Default is 120 sec.
-- @return #INTEL self
function INTEL:SetForgetTime(TimeInterval)
  return self
end

--- Filter unit categories. Valid categories are:
--
-- * Unit.Category.AIRPLANE
-- * Unit.Category.HELICOPTER
-- * Unit.Category.GROUND_UNIT
-- * Unit.Category.SHIP
-- * Unit.Category.STRUCTURE
--
-- @param #INTEL self
-- @param #table Categories Filter categories, e.g. {Unit.Category.AIRPLANE, Unit.Category.HELICOPTER}.
-- @return #INTEL self
function INTEL:SetFilterCategory(Categories)
  if type(Categories)~="table" then
    Categories={Categories}
  end

  self.filterCategory=Categories

  local text="Filter categories: "
  for _,category in pairs(self.filterCategory) do
    text=text..string.format("%d,", category)
  end
  self:T(self.lid..text)

  return self
end

--- Method to make the radar detection less accurate, e.g. for WWII scenarios.
-- @param #INTEL self
-- @param #number minheight Minimum flight height to be detected, in meters AGL (above ground)
-- @param #number thresheight Threshold to escape the radar if flying below minheight, defaults to 90 (90% escape chance)
-- @param #number thresblur Threshold to be detected by the radar overall, defaults to 85 (85% chance to be found)
-- @param #number closing Closing-in in km - the limit of km from which on it becomes increasingly difficult to escape radar detection if flying towards the radar position. Should be about 1/3 of the radar detection radius in kilometers, defaults to 20.
-- @return #INTEL self
function INTEL:SetRadarBlur(minheight,thresheight,thresblur,closing)
  self.RadarBlur = true
  self.RadarBlurMinHeight = minheight or 250 -- meters
  self.RadarBlurThresHeight = thresheight or 90 -- 10% chance to find a low flying group
  self.RadarBlurThresBlur = thresblur or 85 -- 25% chance to escape the radar overall
  self.RadarBlurClosing = closing or 20 -- 20km
  self.RadarBlurClosingSquare = self.RadarBlurClosing * self.RadarBlurClosing 
  return self
end

--- Set the accept range in kilometers from each of the recce. Only object closer than this range will be detected.
-- @param #INTEL self
-- @param #number Range Range in kilometers
-- @return #INTEL self
function INTEL:SetAcceptRange(Range)
  self.RadarAcceptRange = true
  self.RadarAcceptRangeKilometers = Range or 75
  return self
end

--- Filter group categories. Valid categories are:
--
-- * Group.Category.AIRPLANE
-- * Group.Category.HELICOPTER
-- * Group.Category.GROUND
-- * Group.Category.SHIP
-- * Group.Category.TRAIN
--
-- @param #INTEL self
-- @param #table GroupCategories Filter categories, e.g. `{Group.Category.AIRPLANE, Group.Category.HELICOPTER}`.
-- @return #INTEL self
function INTEL:FilterCategoryGroup(GroupCategories)
  if type(GroupCategories)~="table" then
    GroupCategories={GroupCategories}
  end

  self.filterCategoryGroup=GroupCategories

  local text="Filter group categories: "
  for _,category in pairs(self.filterCategoryGroup) do
    text=text..string.format("%d,", category)
  end
  self:T(self.lid..text)

  return self
end

--- Add a group to the detection set.
-- @param #INTEL self
-- @param Wrapper.Group#GROUP AgentGroup Group of agents. Can also be an @{Ops.OpsGroup#OPSGROUP} object.
-- @return #INTEL self
function INTEL:AddAgent(AgentGroup)

  -- Check if this was an OPS group.
  if AgentGroup:IsInstanceOf("OPSGROUP") then
    AgentGroup=AgentGroup:GetGroup()
  end

  -- Add to detection set.
  self.detectionset:AddGroup(AgentGroup,true)
  return self
end

--- Enable or disable cluster analysis of detected targets.
-- Targets will be grouped in coupled clusters.
-- @param #INTEL self
-- @param #boolean Switch If true, enable cluster analysis.
-- @param #boolean Markers If true, place markers on F10 map.
-- @param #boolean Arrows If true, draws arrows on F10 map.
-- @return #INTEL self
function INTEL:SetClusterAnalysis(Switch, Markers, Arrows)
  self.clusteranalysis=Switch
  self.clustermarkers=Markers
  self.clusterarrows=Arrows
  return self
end

--- Set whether STATIC objects are detected.
-- @param #INTEL self
-- @param #boolean Switch If `true`, statics are detected.
-- @return #INTEL self
function INTEL:SetDetectStatics(Switch)
  if Switch and Switch==true then
    self.detectStatics=true
  else
    self.detectStatics=false
  end
  return self
end

--- Set verbosity level for debugging.
-- @param #INTEL self
-- @param #number Verbosity The higher, the noisier, e.g. 0=off, 2=debug
-- @return #INTEL self
function INTEL:SetVerbosity(Verbosity)
  self.verbose=Verbosity or 2
  return self
end

--- Add a Mission (Auftrag) to a contact for tracking.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact
-- @param Ops.Auftrag#AUFTRAG Mission The mission connected with this contact
-- @return #INTEL self
function INTEL:AddMissionToContact(Contact, Mission)
  if Mission and Contact then
    Contact.mission = Mission
  end
  return self
end

--- Add a Mission (Auftrag) to a cluster for tracking.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster
-- @param Ops.Auftrag#AUFTRAG Mission The mission connected with this cluster
-- @return #INTEL self
function INTEL:AddMissionToCluster(Cluster, Mission)
  if Mission and Cluster then
    Cluster.mission = Mission
  end
  return self
end

--- Change radius of the Clusters.
-- @param #INTEL self
-- @param #number radius The radius of the clusters in kilometers. Default 15 km.
-- @return #INTEL self
function INTEL:SetClusterRadius(radius)
  self.clusterradius = (radius or 15)*1000
  return self
end

--- Set detection types for this #INTEL - all default to true.
-- @param #INTEL self
-- @param #boolean DetectVisual Visual detection
-- @param #boolean DetectOptical Optical detection
-- @param #boolean DetectRadar Radar detection
-- @param #boolean DetectIRST IRST detection
-- @param #boolean DetectRWR RWR detection
-- @param #boolean DetectDLINK Data link detection
-- @return self
function INTEL:SetDetectionTypes(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK)
  self.DetectVisual = DetectVisual and true
  self.DetectOptical = DetectOptical and true
  self.DetectRadar = DetectRadar and true
  self.DetectIRST = DetectIRST and true
  self.DetectRWR = DetectRWR and true
  self.DetectDLINK = DetectDLINK and true
  return self
end

--- Get table of #INTEL.Contact objects
-- @param #INTEL self
-- @return #table Contacts or nil if not running
function INTEL:GetContactTable()
  if self:Is("Running") then
    return self.Contacts
  else
    return nil
  end
end

--- Get table of #INTEL.Cluster objects
-- @param #INTEL self
-- @return #table Clusters or nil if not running
function INTEL:GetClusterTable()
  if self:Is("Running") and self.clusteranalysis then
    return self.Clusters
  else
    return nil
  end
end

--- Get name of a contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return #string Name of the contact.
function INTEL:GetContactName(Contact)
  return Contact.groupname
end

--- Get group of a contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return Wrapper.Group#GROUP Group object.
function INTEL:GetContactGroup(Contact)
  return Contact.group
end

--- Get threatlevel of a contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return #number Threat level.
function INTEL:GetContactThreatlevel(Contact)
  return Contact.threatlevel
end


--- Get type name of a contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return #string Type name.
function INTEL:GetContactTypeName(Contact)
  return Contact.typename
end

--- Get category name of a contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return #string Category name.
function INTEL:GetContactCategoryName(Contact)
  return Contact.categoryname
end

--- Get coordinate of a contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return Core.Point#COORDINATE Coordinates.
function INTEL:GetContactCoordinate(Contact)
  return Contact.position
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start & Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers.
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function INTEL:onafterStart(From, Event, To)

  -- Short info.
  local text=string.format("Starting INTEL v%s", self.version)
  self:I(self.lid..text)

  -- Start the status monitoring.
  self:__Status(-math.random(10))
  return self
end

--- On after "Status" event.
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function INTEL:onafterStatus(From, Event, To)

  -- FSM state.
  local fsmstate=self:GetState()

  -- Fresh arrays.
  self.ContactsLost={}
  self.ContactsUnknown={}

  -- Check if group has detected any units.
  self:UpdateIntel()

  -- Number of total contacts.
  local Ncontacts=#self.Contacts
  local Nclusters=#self.Clusters

  -- Short info.
  if self.verbose>=1 then
    local text=string.format("Status %s [Agents=%s]: Contacts=%d, Clusters=%d, New=%d, Lost=%d", fsmstate, self.detectionset:CountAlive(), Ncontacts, Nclusters, #self.ContactsUnknown, #self.ContactsLost)
    self:I(self.lid..text)
  end

  -- Detailed info.
  if self.verbose>=2 and Ncontacts>0 then
    local text="Detected Contacts:"
    for _,_contact in pairs(self.Contacts) do
      local contact=_contact --#INTEL.Contact
      local dT=timer.getAbsTime()-contact.Tdetected
      text=text..string.format("\n- %s (%s): %s, units=%d, T=%d sec", contact.categoryname, contact.attribute, contact.groupname, contact.isStatic and 1 or contact.group:CountAliveUnits(), dT)
      if contact.mission then
        local mission=contact.mission --Ops.Auftrag#AUFTRAG
        text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown")
      end
    end
    self:I(self.lid..text)
  end

  self:__Status(self.statusupdate)
  return self
end


--- Update detected items.
-- @param #INTEL self
function INTEL:UpdateIntel()

  -- Set of all detected units.
  local DetectedUnits={}

  -- Set of which units was detected by which recce
  local RecceDetecting = {}

  -- Loop over all units providing intel.
  for _,_group in pairs(self.detectionset.Set or {}) do
    local group=_group --Wrapper.Group#GROUP

    if group and group:IsAlive() then

      for _,_recce in pairs(group:GetUnits()) do
        local recce=_recce --Wrapper.Unit#UNIT

        -- Get detected units.
        self:GetDetectedUnits(recce, DetectedUnits, RecceDetecting, self.DetectVisual, self.DetectOptical, self.DetectRadar, self.DetectIRST, self.DetectRWR, self.DetectDLINK)

      end

    end
  end

  local remove={}
  for unitname,_unit in pairs(DetectedUnits) do
    local unit=_unit --Wrapper.Unit#UNIT
    
    local inconflictzone=false
    -- Check if unit is in any of the conflict zones.
    if self.conflictzoneset:Count()>0 then
      for _,_zone in pairs(self.conflictzoneset.Set) do
        local zone=_zone --Core.Zone#ZONE
        if unit:IsInZone(zone) then
          inconflictzone=true
          break
        end
      end
    end
    
    -- Check if unit is in any of the accept zones.
    if self.acceptzoneset:Count()>0 then
      local inzone=false
      for _,_zone in pairs(self.acceptzoneset.Set) do
        local zone=_zone --Core.Zone#ZONE
        if unit:IsInZone(zone) then
          inzone=true
          break
        end
      end

      -- Unit is not in accept zone ==> remove!
      if (not inzone) and (not inconflictzone) then
        table.insert(remove, unitname)
      end
    end

    -- Check if unit is in any of the reject zones.
    if self.rejectzoneset:Count()>0 then
      local inzone=false
      for _,_zone in pairs(self.rejectzoneset.Set) do
        local zone=_zone --Core.Zone#ZONE
        if unit:IsInZone(zone) then
          inzone=true
          break
        end
      end

      -- Unit is inside a reject zone ==> remove!
      if inzone and (not inconflictzone) then
        table.insert(remove, unitname)
      end
    end

    -- Filter unit categories. Added check that we have a UNIT and not a STATIC object because :GetUnitCategory() is only available for units.
    if #self.filterCategory>0 and unit:IsInstanceOf("UNIT") then
      local unitcategory=unit:GetUnitCategory()
      local keepit=false
      for _,filtercategory in pairs(self.filterCategory) do
        if unitcategory==filtercategory then
          keepit=true
          break
        end
      end
      if not keepit then
        self:T(self.lid..string.format("Removing unit %s category=%d", unitname, unit:GetCategory()))
        table.insert(remove, unitname)
      end
    end

  end

  -- Remove filtered units.
  for _,unitname in pairs(remove) do
    DetectedUnits[unitname]=nil
  end

  -- Create detected groups.
  local DetectedGroups={}
  local DetectedStatics={}
  local RecceGroups={}
  for unitname,_unit in pairs(DetectedUnits) do
    local unit=_unit --Wrapper.Unit#UNIT
    if unit:IsInstanceOf("UNIT") then
      local group=unit:GetGroup()
      if group then
        local groupname = group:GetName()
        DetectedGroups[groupname]=group
        RecceGroups[groupname]=RecceDetecting[unitname]
      end
    else
      if self.detectStatics then
        DetectedStatics[unitname]=unit
        RecceGroups[unitname]=RecceDetecting[unitname]
      end
    end
  end

  -- Create detected contacts.
  self:CreateDetectedItems(DetectedGroups, DetectedStatics, RecceGroups)

  -- Paint a picture of the battlefield.
  if self.clusteranalysis then
    self:PaintPicture()
  end

  return self
end

--- Update an #INTEL.Contact item.
-- @param #INTEL self
-- @param #INTEL.Contact Contact Contact.
-- @return #INTEL.Contact The contact.
function INTEL:_UpdateContact(Contact)

  if Contact.isStatic then

    -- Statics don't need to be updated.

  else

    if Contact.group and Contact.group:IsAlive() then

      Contact.Tdetected=timer.getAbsTime()
      Contact.position=Contact.group:GetCoordinate()
      Contact.velocity=Contact.group:GetVelocityVec3()
      Contact.speed=Contact.group:GetVelocityMPS()
      if Contact.group:IsAir() then
        Contact.altitude=Contact.group:GetAltitude()
        local oldheading = Contact.heading or 1
        local newheading = Contact.group:GetHeading()
        if newheading == 0 then newheading = 1 end
        local changeh = math.abs(((oldheading - newheading) + 360) % 360)
        Contact.heading = newheading
        if changeh > 10 then
          Contact.maneuvering = true
        else
          Contact.maneuvering = false
        end
      end
    end

  end
  return self
end

--- Create an #INTEL.Contact item from a given GROUP or STATIC object.
-- @param #INTEL self
-- @param Wrapper.Positionable#POSITIONABLE Positionable The GROUP or STATIC object.
-- @param #string RecceName The name of the recce group that has detected this contact.
-- @return #INTEL.Contact The contact.
function INTEL:_CreateContact(Positionable, RecceName)

  if Positionable and Positionable:IsAlive() then

    -- Create new contact.
    local item={} --#INTEL.Contact

    if Positionable:IsInstanceOf("GROUP") then

      local group=Positionable --Wrapper.Group#GROUP

      item.groupname=group:GetName()
      item.group=group
      item.Tdetected=timer.getAbsTime()
      item.typename=group:GetTypeName()
      item.attribute=group:GetAttribute()
      item.category=group:GetCategory()
      item.categoryname=group:GetCategoryName()
      item.threatlevel=group:GetThreatLevel()
      item.position=group:GetCoordinate()
      item.velocity=group:GetVelocityVec3()
      item.speed=group:GetVelocityMPS()
      item.recce=RecceName
      item.isground = group:IsGround() or false
      item.isship = group:IsShip() or false
      item.isStatic=false
      if group:IsAir() then
        item.platform=group:GetNatoReportingName()
        item.heading = group:GetHeading()
        item.maneuvering = false
        item.altitude = group:GetAltitude()
      else
        -- TODO optionally add ground types?
        item.platform="Unknown"
        item.altitude = group:GetAltitude(true)
      end
      if item.category==Group.Category.AIRPLANE or item.category==Group.Category.HELICOPTER then
        item.ctype=INTEL.Ctype.AIRCRAFT
      elseif item.category==Group.Category.GROUND or item.category==Group.Category.TRAIN then
        item.ctype=INTEL.Ctype.GROUND
      elseif item.category==Group.Category.SHIP then
        item.ctype=INTEL.Ctype.NAVAL
      end

      return item

    elseif Positionable:IsInstanceOf("STATIC") then

      local static=Positionable --Wrapper.Static#STATIC

      item.groupname=static:GetName()
      item.group=static
      item.Tdetected=timer.getAbsTime()
      item.typename=static:GetTypeName() or "Unknown"
      item.attribute="Static"
      item.category=3 --static:GetCategory()
      item.categoryname=static:GetCategoryName() or "Unknown"
      item.threatlevel=static:GetThreatLevel() or 0
      item.position=static:GetCoordinate()
      item.velocity=static:GetVelocityVec3()
      item.speed=0
      item.recce=RecceName
      item.isground = true
      item.isship = false
      item.isStatic=true
      item.ctype=INTEL.Ctype.STRUCTURE

      return item
    else
      self:E(self.lid..string.format("ERROR: object needs to be a GROUP or STATIC!"))
    end

  end

  return nil
end

--- Create detected items.
-- @param #INTEL self
-- @param #table DetectedGroups Table of detected Groups.
-- @param #table DetectedStatics Table of detected Statics.
-- @param #table RecceDetecting Table of detecting recce names.
function INTEL:CreateDetectedItems(DetectedGroups, DetectedStatics, RecceDetecting)
  self:F({RecceDetecting=RecceDetecting})

  -- Current time.
  local Tnow=timer.getAbsTime()

  -- Loop over groups.
  for groupname,_group in pairs(DetectedGroups) do
    local group=_group --Wrapper.Group#GROUP

      -- Create or update contact for this group.
      self:KnowObject(group, RecceDetecting[groupname])

  end

  -- Loop over statics.
  for staticname,_static in pairs(DetectedStatics) do
    local static=_static --Wrapper.Static#STATIC

    -- Create or update contact for this group.
    self:KnowObject(static, RecceDetecting[staticname])

  end

  -- Now check if there some groups could not be detected any more.
  for i=#self.Contacts,1,-1 do
    local item=self.Contacts[i] --#INTEL.Contact

    -- Check if deltaT>Tforget. We dont want quick oscillations between detected and undetected states.
    if self:_CheckContactLost(item) then

      -- Trigger LostContact event. This also adds the contact to the self.ContactsLost table.
      self:LostContact(item)

      -- Remove contact from table.
      self:RemoveContact(item)

    end
  end
  return self
end

--- (Internal) Return the detected target groups of the controllable as a @{Core.Set#SET_GROUP}.
-- The optional parameters specify the detection methods that can be applied.
-- If no detection method is given, the detection will use all the available methods by default.
-- @param #INTEL self
-- @param Wrapper.Unit#UNIT Unit The unit detecting.
-- @param #table DetectedUnits Table of detected units to be filled.
-- @param #table RecceDetecting Table of recce per unit to be filled.
-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets.
-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets.
-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar.
-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST.
-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR.
-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link.
function INTEL:GetDetectedUnits(Unit, DetectedUnits, RecceDetecting, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK)

  -- Get detected DCS units.
  local reccename = Unit:GetName()

  local detectedtargets=Unit:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK)

  for DetectionObjectID, Detection in pairs(detectedtargets or {}) do
    local DetectedObject=Detection.object -- DCS#Object

    -- NOTE: Got an object that exists but when trying UNIT:Find() the DCS getName() function failed. ID of the object was 5,000,031
    if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then

      -- Protected call to get the name of the object.
      local status,name =  pcall(
      function()
        local name=DetectedObject:getName()
        return name
      end)

      if status then

        local unit=UNIT:FindByName(name)
 
        if unit and unit:IsAlive() then
          local DetectionAccepted = true
          
          if self.RadarAcceptRange then
            local reccecoord = Unit:GetCoordinate()
            local coord = unit:GetCoordinate()
            local dist = math.floor(coord:Get2DDistance(reccecoord)/1000) -- km
            if dist > self.RadarAcceptRangeKilometers then DetectionAccepted = false end
          end
          
          if self.RadarBlur then
            local reccecoord = Unit:GetCoordinate()
            local coord = unit:GetCoordinate()
            local dist = math.floor(coord:Get2DDistance(reccecoord)/1000) -- km
            local AGL = unit:GetAltitude(true)
            local minheight = self.RadarBlurMinHeight or 250 -- meters
            local thresheight = self.RadarBlurThresHeight or 90 -- 10% chance to find a low flying group
            local thresblur = self.RadarBlurThresBlur or 85 -- 25% chance to escape the radar overall
            --local dist = math.floor(Distance)
            if dist <= self.RadarBlurClosing  then
              thresheight = (((dist*dist)/self.RadarBlurClosingSquare)*thresheight)
              thresblur = (((dist*dist)/self.RadarBlurClosingSquare)*thresblur)
            end
            local fheight = math.floor(math.random(1,10000)/100)
            local fblur = math.floor(math.random(1,10000)/100)
            if fblur > thresblur then DetectionAccepted = false end
            if AGL <= minheight and fheight < thresheight then DetectionAccepted = false end
            if self.debug or self.verbose > 1 then
              MESSAGE:New("Radar Blur",10):ToLogIf(self.debug):ToAllIf(self.verbose>1)      
              MESSAGE:New("Unit "..name.." is at "..math.floor(AGL).."m. Distance "..math.floor(dist).."km.",10):ToLogIf(self.debug):ToAllIf(self.verbose>1)
              MESSAGE:New(string.format("fheight = %d/%d | fblur = %d/%d",fheight,thresheight,fblur,thresblur),10):ToLogIf(self.debug):ToAllIf(self.verbose>1)
              MESSAGE:New("Detection Accepted = "..tostring(DetectionAccepted),10):ToLogIf(self.debug):ToAllIf(self.verbose>1)
            end
          end
           
          if DetectionAccepted then
            DetectedUnits[name]=unit
            RecceDetecting[name]=reccename
            self:T(string.format("Unit %s detect by %s", name, reccename))
          end
        else
          if self.detectStatics then
            local static=STATIC:FindByName(name, false)
            if static then
              --env.info("FF found static "..name)
              DetectedUnits[name]=static
              RecceDetecting[name]=reccename
            end
          end
        end

      else
        -- Warning!
        self:T(self.lid..string.format("WARNING: Could not get name of detected object ID=%s! Detected by %s", DetectedObject.id_, reccename))
      end
    end
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "NewContact" event.
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #INTEL.Contact Contact Detected contact.
function INTEL:onafterNewContact(From, Event, To, Contact)

  -- Debug text.
  self:F(self.lid..string.format("NEW contact %s", Contact.groupname))

  -- Add to table of unknown contacts.
  table.insert(self.ContactsUnknown, Contact)
  return self
end

--- On after "LostContact" event.
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #INTEL.Contact Contact Lost contact.
function INTEL:onafterLostContact(From, Event, To, Contact)

  -- Debug text.
  self:F(self.lid..string.format("LOST contact %s", Contact.groupname))

  -- Add to table of lost contacts.
  table.insert(self.ContactsLost, Contact)
  return self
end

--- On after "NewCluster" event.
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #INTEL.Cluster Cluster Detected cluster.
function INTEL:onafterNewCluster(From, Event, To, Cluster)

  -- Debug text.
  self:F(self.lid..string.format("NEW cluster #%d [%s] of size %d", Cluster.index, Cluster.ctype, Cluster.size))

  -- Add cluster to table.
  self:_AddCluster(Cluster)
  return self
end

--- On after "LostCluster" event.
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #INTEL.Cluster Cluster Lost cluster.
-- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`.
function INTEL:onafterLostCluster(From, Event, To, Cluster, Mission)

  -- Debug text.
  local text = self.lid..string.format("LOST cluster #%d [%s]", Cluster.index, Cluster.ctype)

  if Mission then
    local mission=Mission --Ops.Auftrag#AUFTRAG
    text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown")
  end

  self:T(text)
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Make the INTEL aware of a object that was not detected (yet). This will add the object to the contacts table and trigger a `NewContact` event.
-- @param #INTEL self
-- @param Wrapper.Positionable#POSITIONABLE Positionable Group or static object.
-- @param #string RecceName Name of the recce group that detected this object.
-- @param #number Tdetected Abs. mission time in seconds, when the object is detected. Default now.
-- @return #INTEL self
function INTEL:KnowObject(Positionable, RecceName, Tdetected)

  local Tnow=timer.getAbsTime()
  Tdetected=Tdetected or Tnow

  if Positionable and Positionable:IsAlive() then

    if Tdetected>Tnow then
      -- Delay call.
      self:ScheduleOnce(Tdetected-Tnow, self.KnowObject, self, Positionable, RecceName)
    else

      -- Name of the object.
      local name=Positionable:GetName()

      -- Try to get the contact by name.
      local contact=self:GetContactByName(name)

      if contact then

        -- Update contact info.
        self:_UpdateContact(contact)

      else

        -- Create new contact.
        contact=self:_CreateContact(Positionable, RecceName)

        if contact then

          -- Debug info.
          self:T(string.format("%s contact detected by %s", contact.groupname, RecceName or "unknown"))

          -- Add contact to table.
          self:AddContact(contact)

          -- Trigger new contact event.
          self:NewContact(contact)

        end

      end
    end
  end

  return self
end

--- Get a contact by name.
-- @param #INTEL self
-- @param #string groupname Name of the contact group.
-- @return #INTEL.Contact The contact.
function INTEL:GetContactByName(groupname)

  for i,_contact in pairs(self.Contacts) do
    local contact=_contact --#INTEL.Contact
    if contact.groupname==groupname then
      return contact
    end
  end

  return nil
end

--- Check if a Contact is already known. It is checked, whether the contact is in the contacts table.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact to be added.
-- @return #boolean If `true`, contact is already known.
function INTEL:_IsContactKnown(Contact)

  for i,_contact in pairs(self.Contacts) do
    local contact=_contact --#INTEL.Contact
    if contact.groupname==Contact.groupname then
      return true
    end
  end

  return false
end


--- Add a contact to our list.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact to be added.
-- @return #INTEL self
function INTEL:AddContact(Contact)

  -- First check if the contact is already in the table.
  if self:_IsContactKnown(Contact) then
    self:E(self.lid..string.format("WARNING: Contact %s is already in the contact table!", tostring(Contact.groupname)))
  else
    self:T(self.lid..string.format("Adding new Contact %s to table", tostring(Contact.groupname)))
    table.insert(self.Contacts, Contact)
  end

  return self
end

--- Remove a contact from our list.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact to be removed.
function INTEL:RemoveContact(Contact)

  for i,_contact in pairs(self.Contacts) do
    local contact=_contact --#INTEL.Contact

    if contact.groupname==Contact.groupname then
      table.remove(self.Contacts, i)
    end

  end
  return self
end

--- Check if a contact was lost.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact to be removed.
-- @return #boolean If true, contact was not detected for at least *dTforget* seconds.
function INTEL:_CheckContactLost(Contact)

  -- Group dead?
  if Contact.group==nil or not Contact.group:IsAlive() then
    return true
  end

  -- We never forget statics as they don't move.
  if Contact.isStatic then
    return false
  end

  -- Time since last detected.
  local dT=timer.getAbsTime()-Contact.Tdetected

  local dTforget=nil

  if Contact.category==Group.Category.GROUND then
    dTforget=60*60*2  -- 2 hours
  elseif Contact.category==Group.Category.AIRPLANE then
    dTforget=60*10    -- 10 min
  elseif Contact.category==Group.Category.HELICOPTER then
    dTforget=60*20    -- 20 min
  elseif Contact.category==Group.Category.SHIP then
    dTforget=60*60    -- 1 hour
  elseif Contact.category==Group.Category.TRAIN then
    dTforget=60*60    -- 1 hour
  end

  if dT>dTforget then
    return true
  else
    return false
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Cluster Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- [Internal] Paint picture of the battle field. Does Cluster analysis and updates clusters. Sets markers if markers are enabled.
-- @param #INTEL self
function INTEL:PaintPicture()
  self:F(self.lid.."Painting Picture!")

  -- First remove all lost contacts from clusters.
  for _,_contact in pairs(self.ContactsLost) do
    local contact=_contact --#INTEL.Contact

    -- Get cluster this contact belongs to (if any).
    local cluster=self:GetClusterOfContact(contact)

    if cluster then
      self:RemoveContactFromCluster(contact, cluster)
    end
  end

  -- Clean up cluster table.
  local ClusterSet = {}

  -- Now check if whole clusters were lost.
  for _i,_cluster in pairs(self.Clusters) do
    local cluster=_cluster --#INTEL.Cluster

    if cluster.size>0 and self:ClusterCountUnits(cluster)>0 then
      -- This one has size>0 and units>0
      table.insert(ClusterSet,_cluster)
    else

      -- This cluster is gone.

      -- Remove marker.
      if cluster.marker then
        cluster.marker:Remove()
      end

      -- Marker of the arrow.
      if cluster.markerID then
        COORDINATE:RemoveMark(cluster.markerID)
      end

      -- Lost cluster.
      self:LostCluster(cluster, cluster.mission)
    end
  end

  -- Set Clusters.
  self.Clusters = ClusterSet

  -- Update positions.
  self:_UpdateClusterPositions()


  for _,_contact in pairs(self.Contacts) do
    local contact=_contact --#INTEL.Contact

    -- Debug info.
    self:T(string.format("Paint Picture: checking for %s",contact.groupname))

    -- Get the current cluster (if any) this contact belongs to.
    local currentcluster=self:GetClusterOfContact(contact)

    if currentcluster then
      ---
      -- Contact is currently part of a cluster.
      ---

      -- Check if the contact is still connected to the cluster.
      local isconnected=self:IsContactConnectedToCluster(contact, currentcluster)

      if isconnected then

      else

        --- Not connected to current cluster any more.

        -- Remove from current cluster.
        self:RemoveContactFromCluster(contact, currentcluster)

        -- Find new cluster.
        local cluster=self:_GetClosestClusterOfContact(contact)

        if cluster then
           -- Add contact to cluster.
          self:AddContactToCluster(contact, cluster)
        else

          -- Create a new cluster.
          local newcluster=self:_CreateClusterFromContact(contact)

          -- Trigger new cluster event.
          self:NewCluster(newcluster)
        end

      end

    else

      ---
      -- Contact is not in any cluster yet.
      ---

      -- Debug info.
      self:T(self.lid..string.format("Paint Picture: contact %s has NO current cluster", contact.groupname))

      -- Get the closest existing cluster of this contact.
      local cluster=self:_GetClosestClusterOfContact(contact)

      if cluster then

        -- Debug info.
        self:T(self.lid..string.format("Paint Picture: contact %s has closest cluster #%d",contact.groupname, cluster.index))

        -- Add contact to this cluster.
        self:AddContactToCluster(contact, cluster)

      else
      
        -- Debug info.
        self:T(self.lid..string.format("Paint Picture: contact %s has no closest cluster ==> Create new cluster", contact.groupname))      

        -- Create a brand new cluster.
        local newcluster=self:_CreateClusterFromContact(contact)

        -- Trigger event for a new cluster.
        self:NewCluster(newcluster)
      end

    end

  end

  -- Update positions.
  self:_UpdateClusterPositions()

  -- Update F10 marker text if cluster has changed.
  if self.clustermarkers then
    for _,_cluster in pairs(self.Clusters) do
      local cluster=_cluster --#INTEL.Cluster
      --local coordinate=self:GetClusterCoordinate(cluster)

      -- Update F10 marker.
      if self.verbose >= 1 then
        BASE:I("Updating cluster marker and future position")
      end

      -- Update cluster markers.
      self:UpdateClusterMarker(cluster)

      -- Extrapolate future position of the cluster.
      self:CalcClusterFuturePosition(cluster, 300)

    end
  end

  return self
end

--- Create a new cluster.
-- @param #INTEL self
-- @return #INTEL.Cluster cluster The cluster.
function INTEL:_CreateCluster()

  -- Create new cluster.
  local cluster={} --#INTEL.Cluster

  cluster.index=self.clustercounter
  cluster.coordinate=COORDINATE:New(0, 0, 0)
  cluster.threatlevelSum=0
  cluster.threatlevelMax=0
  cluster.size=0
  cluster.Contacts={}
  cluster.altitude=0

  -- Increase counter.
  self.clustercounter=self.clustercounter+1

  return cluster
end

--- Create a new cluster from a first contact. The contact is automatically added to the cluster.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The first contact.
-- @return #INTEL.Cluster cluster The cluster.
function INTEL:_CreateClusterFromContact(Contact)

  local cluster=self:_CreateCluster()

  self:T(self.lid..string.format("Created NEW cluster #%d with first contact %s", cluster.index, Contact.groupname))

  cluster.coordinate:UpdateFromCoordinate(Contact.position)

  cluster.ctype=Contact.ctype

  self:AddContactToCluster(Contact, cluster)

  return cluster
end

--- Add cluster to table.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster to add.
function INTEL:_AddCluster(Cluster)

  --TODO: Check if cluster is already in the table.

  -- Add cluster.
  table.insert(self.Clusters, Cluster)

  return self
end

--- Add a contact to the cluster.
-- @param #INTEL self
-- @param #INTEL.Contact contact The contact.
-- @param #INTEL.Cluster cluster The cluster.
function INTEL:AddContactToCluster(contact, cluster)

  if contact and cluster then

    -- Add neighbour to cluster contacts.
    table.insert(cluster.Contacts, contact)

    -- Add to threat level sum.
    cluster.threatlevelSum=cluster.threatlevelSum+contact.threatlevel

    -- Increase size.
    cluster.size=cluster.size+1

    -- alt
    self:GetClusterAltitude(cluster,true)

    -- Debug info.
    self:T(self.lid..string.format("Adding contact %s to cluster #%d [%s] ==> New size=%d", contact.groupname, cluster.index, cluster.ctype, cluster.size))
  end

  return self
end

--- Remove a contact from a cluster.
-- @param #INTEL self
-- @param #INTEL.Contact contact The contact.
-- @param #INTEL.Cluster cluster The cluster.
function INTEL:RemoveContactFromCluster(contact, cluster)

  if contact and cluster then

    for i=#cluster.Contacts,1,-1 do
      local Contact=cluster.Contacts[i] --#INTEL.Contact

      if Contact.groupname==contact.groupname then

        -- Remove threat level sum.
        cluster.threatlevelSum=cluster.threatlevelSum-contact.threatlevel

        -- Decrease cluster size.
        cluster.size=cluster.size-1

        -- Remove from table.
        table.remove(cluster.Contacts, i)

        -- Debug info.
        self:T(self.lid..string.format("Removing contact %s from cluster #%d ==> New cluster size=%d", contact.groupname, cluster.index, cluster.size))

        return self
      end

    end

  end
  return self
end

--- Calculate cluster threat level sum.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @return #number Sum of all threat levels of all groups in the cluster.
function INTEL:CalcClusterThreatlevelSum(cluster)

  local threatlevel=0

  for _,_contact in pairs(cluster.Contacts) do
    local contact=_contact --#INTEL.Contact

    threatlevel=threatlevel+contact.threatlevel

  end
  cluster.threatlevelSum = threatlevel
  return threatlevel
end

--- Calculate cluster threat level average.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @return #number Average of all threat levels of all groups in the cluster.
function INTEL:CalcClusterThreatlevelAverage(cluster)

  local threatlevel=self:CalcClusterThreatlevelSum(cluster)
  threatlevel=threatlevel/cluster.size
  cluster.threatlevelAve = threatlevel
  return threatlevel
end

--- Calculate max cluster threat level.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @return #number Max threat levels of all groups in the cluster.
function INTEL:CalcClusterThreatlevelMax(cluster)

  local threatlevel=0

  for _,_contact in pairs(cluster.Contacts) do

    local contact=_contact --#INTEL.Contact

    if contact.threatlevel>threatlevel then
      threatlevel=contact.threatlevel
    end

  end
  cluster.threatlevelMax = threatlevel
  return threatlevel
end

--- Calculate cluster heading.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @return #number Heading average of all groups in the cluster.
function INTEL:CalcClusterDirection(cluster)

  local direction = 0
  local speedsum = 0
  local n=0
  for _,_contact in pairs(cluster.Contacts) do
    local contact=_contact --#INTEL.Contact

    if (not contact.isStatic) and contact.group:IsAlive() then
      local speed = contact.group:GetVelocityKNOTS()
      direction = direction + (contact.group:GetHeading()*speed)
      n=n+1
      speedsum = speedsum + speed
    end
  end

  --TODO: This calculation is WRONG!
  -- Simple example for two groups:
  -- First group is going West, i.e. heading 090
  -- Second group is going East, i.e. heading 270
  -- Total is 360/2=180, i.e. South!
  -- It should not go anywhere as the two movements cancel each other.
  -- Apple - Correct, edge case for N=2^x, but when 2 pairs of groups drive in exact opposite directions, the cluster will split at some point?
  -- maybe add the speed as weight to get a weighted factor:

  if n==0 then
    return 0
  else
    return math.floor(direction / (speedsum * n ))
  end
  
end

--- Calculate cluster speed.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @return #number Speed average of all groups in the cluster in MPS.
function INTEL:CalcClusterSpeed(cluster)

  local velocity = 0 ; local n=0
  for _,_contact in pairs(cluster.Contacts) do
    local contact=_contact --#INTEL.Contact

    if (not contact.isStatic) and contact.group:IsAlive() then
      velocity = velocity + contact.group:GetVelocityMPS()
      n=n+1
    end

  end

  if n==0 then
    return 0
  else
    return math.floor(velocity / n)
  end
end

--- Calculate cluster velocity vector.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @return DCS#Vec3 Velocity vector in m/s.
function INTEL:CalcClusterVelocityVec3(cluster)

  local v={x=0, y=0, z=0} --DCS#Vec3

  for _,_contact in pairs(cluster.Contacts) do
    local contact=_contact --#INTEL.Contact

    if (not contact.isStatic) and contact.group:IsAlive() then
      local vec=contact.group:GetVelocityVec3()
      v.x=v.x+vec.x
      v.y=v.y+vec.y
      v.z=v.y+vec.z
    end
  end

  return v
end

--- Calculate cluster future position after given seconds.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @param #number seconds Time interval in seconds. Default is `self.prediction`.
-- @return Core.Point#COORDINATE Calculated future position of the cluster.
function INTEL:CalcClusterFuturePosition(cluster, seconds)

  -- Get current position of the cluster.
  local p=self:GetClusterCoordinate(cluster)

  -- Velocity vector in m/s.
  local v=self:CalcClusterVelocityVec3(cluster)

  -- Time in seconds.
  local t=seconds or self.prediction

  -- Extrapolated vec3.
  local Vec3={x=p.x+v.x*t, y=p.y+v.y*t, z=p.z+v.z*t}

  -- Future position.
  local futureposition=COORDINATE:NewFromVec3(Vec3)

  -- Create an arrow pointing in the direction of the movement.
  if self.clustermarkers and self.clusterarrows then
    if cluster.markerID then
      COORDINATE:RemoveMark(cluster.markerID)
    end
    cluster.markerID = p:ArrowToAll(futureposition, self.coalition, {1,0,0}, 1, {1,1,0}, 0.5, 2, true, "Position Calc")
  end

  return futureposition
end


--- Check if contact is in any known cluster.
-- @param #INTEL self
-- @param #INTEL.Contact contact The contact.
-- @return #boolean If true, contact is in clusters
function INTEL:CheckContactInClusters(contact)

  for _,_cluster in pairs(self.Clusters) do
    local cluster=_cluster --#INTEL.Cluster

    for _,_contact in pairs(cluster.Contacts) do
      local Contact=_contact --#INTEL.Contact

      if Contact.groupname==contact.groupname then
        return true
      end
    end
  end

  return false
end

--- Check if contact is close to any other contact this cluster.
-- @param #INTEL self
-- @param #INTEL.Contact contact The contact.
-- @param #INTEL.Cluster cluster The cluster the check.
-- @return #boolean If `true`, contact is connected to this cluster.
-- @return #number Distance to cluster in meters.
function INTEL:IsContactConnectedToCluster(contact, cluster)

  -- Must be of the same type. We do not want to mix aircraft with ground units.
  if contact.ctype~=cluster.ctype then
    return false, math.huge
  end

  for _,_contact in pairs(cluster.Contacts) do
    local Contact=_contact --#INTEL.Contact

    -- Do not calcuate the distance to the contact itself unless it is the only contact in the cluster.
    if Contact.groupname~=contact.groupname or cluster.size==1 then

      --local dist=Contact.position:Get2DDistance(contact.position)
      local dist=Contact.position:DistanceFromPointVec2(contact.position)

      -- AIR - check for spatial proximity (corrected because airprox was always false for ctype~=INTEL.Ctype.AIRCRAFT)
      local airprox = true
      if contact.ctype == INTEL.Ctype.AIRCRAFT then
       self:T(string.format("Cluster Alt=%d | Contact Alt=%d",cluster.altitude,contact.altitude))
       local adist = math.abs(cluster.altitude - contact.altitude)
       if adist > UTILS.FeetToMeters(10000) then -- limit to 10kft
        airprox = false
       end
      end

      if dist<self.clusterradius and airprox then
        return true, dist
      end

    end

  end

  return false
end

--- Check if contact is close to any contact of known clusters.
-- @param #INTEL self
-- @param #INTEL.Contact contact The contact.
-- @return #INTEL.Cluster The cluster this contact is part of or nil otherwise.
function INTEL:IsContactPartOfAnyClusters(contact)

  for _,_cluster in pairs(self.Clusters) do
    local cluster=_cluster --#INTEL.Cluster

    if self:IsContactConnectedToCluster(contact, cluster) then
      return cluster
    end
  end

  return nil
end

--- Get distance to cluster.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @param #INTEL.Cluster Cluster The cluster to which the distance is calculated.
-- @return #number Distance in meters.
function INTEL:_GetDistContactToCluster(Contact, Cluster)

  local distmin=math.huge

  for _,_contact in pairs(Cluster.Contacts) do
    local contact=_contact --#INTEL.Contact

    if contact.group and contact.group:IsAlive() and Contact.groupname~=contact.groupname then

      local dist=Contact.position:Get2DDistance(contact.position)

      if dist<distmin then
        distmin=dist
      end
    end

  end

  return distmin
end

--- Get closest cluster of contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return #INTEL.Cluster The cluster this contact is part of or `#nil` otherwise.
-- @return #number Distance to cluster in meters.
function INTEL:_GetClosestClusterOfContact(Contact)

  local Cluster=nil  --#INTEL.Cluster

  local distmin=self.clusterradius

  if not Contact.altitude then
    Contact.altitude = Contact.group:GetAltitude()
  end

  for _,_cluster in pairs(self.Clusters) do
    local cluster=_cluster --#INTEL.Cluster

    if cluster.ctype==Contact.ctype then

      local dist=self:_GetDistContactToCluster(Contact, cluster)

      -- AIR - check for spatial proximity (ff: Changed because airprox was always false for ctype~=AIRCRAFT!)
      local airprox=true
      if Contact.ctype == INTEL.Ctype.AIRCRAFT then
        if not cluster.altitude then
          cluster.altitude = self:GetClusterAltitude(cluster,true)
        end
        local adist = math.abs(cluster.altitude - Contact.altitude)
         self:T(string.format("Cluster Alt=%d | Contact Alt=%d",cluster.altitude,Contact.altitude))
        if adist > UTILS.FeetToMeters(10000) then
          airprox = false
        end
      end

      if dist<distmin and airprox then
        Cluster=cluster
        distmin=dist
      end

    end
  end

  return Cluster, distmin
end

--- Get the cluster this contact belongs to (if any).
-- @param #INTEL self
-- @param #INTEL.Contact contact The contact.
-- @return #INTEL.Cluster The cluster this contact belongs to or nil.
function INTEL:GetClusterOfContact(contact)

  for _,_cluster in pairs(self.Clusters) do
    local cluster=_cluster --#INTEL.Cluster

    for _,_contact in pairs(cluster.Contacts) do
      local Contact=_contact --#INTEL.Contact

      if Contact.groupname==contact.groupname then
        return cluster
      end
    end
  end

  return nil
end

--- Get the altitude of a cluster.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster.
-- @param #boolean Update If `true`, update the altitude. Default is to just return the last stored altitude.
-- @return #number The average altitude (ASL) of this cluster in meters.
function INTEL:GetClusterAltitude(Cluster, Update)

  -- Init.
  local newalt = 0
  local n=0

  -- Loop over all contacts.
  for _,_contact in pairs(Cluster.Contacts) do
    local contact=_contact --#INTEL.Contact
    if contact.altitude then
      newalt = newalt + contact.altitude
      n=n+1
    end
  end

  -- Average.
  local avgalt = 0
  if n>0 then
    avgalt = newalt/n
  end

  -- Update cluster coordinate.
  Cluster.altitude = avgalt

  self:T(string.format("Updating Cluster Altitude: %d",Cluster.altitude))

  return Cluster.altitude
end

--- Get the coordinate of a cluster.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster.
-- @param #boolean Update If `true`, update the coordinate. Default is to just return the last stored position.
-- @return Core.Point#COORDINATE The coordinate of this cluster.
function INTEL:GetClusterCoordinate(Cluster, Update)

  -- Init.
  local x=0 ; local y=0 ; local z=0 ; local n=0

  -- Loop over all contacts.
  for _,_contact in pairs(Cluster.Contacts) do
    local contact=_contact --#INTEL.Contact

    local vec3=nil --DCS#Vec3

    if Update and contact.group and contact.group:IsAlive() then
      vec3 = contact.group:GetVec3()
    end

    if not vec3 then
      vec3=contact.position
    end

    if vec3 then

      -- Sum up posits.
      x=x+vec3.x
      y=y+vec3.y
      z=z+vec3.z

      -- Increase counter.
      n=n+1

    end

  end

  if n>0 then

    -- Average.
    local Vec3={x=x/n, y=y/n, z=z/n} --DCS#Vec3

    -- Update cluster coordinate.
    Cluster.coordinate:UpdateFromVec3(Vec3)

  end

  return Cluster.coordinate
end

--- Check if the coorindate of the cluster changed.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster.
-- @param #number Threshold in meters. Default 100 m.
-- @param Core.Point#COORDINATE Coordinate Reference coordinate. Default is the last known coordinate of the cluster.
-- @return #boolean If `true`, the coordinate changed by more than the given threshold.
function INTEL:_CheckClusterCoordinateChanged(Cluster, Coordinate, Threshold)

  Threshold=Threshold or 100

  Coordinate=Coordinate or Cluster.coordinate

  -- Positions of cluster.
  local a=Coordinate:GetVec3()
  local b=self:GetClusterCoordinate(Cluster, true):GetVec3()

  local dist=UTILS.VecDist3D(a,b)

  if dist>Threshold then
    return true
  else
    return false
  end

end

--- Update coordinates of the known clusters.
-- @param #INTEL self
function INTEL:_UpdateClusterPositions()
  for _,_cluster in pairs (self.Clusters) do
    local cluster=_cluster --#INTEL.Cluster

    -- Update cluster coordinate.
    local coord = self:GetClusterCoordinate(cluster, true)
    local alt = self:GetClusterAltitude(cluster,true)

    -- Debug info.
    self:T(self.lid..string.format("Updating Cluster position size: %s", cluster.size))
  end
  return self
end

--- Count number of alive units in contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return #number unitcount
function INTEL:ContactCountUnits(Contact)
  if Contact.isStatic then
    if Contact.group and Contact.group:IsAlive() then
      return 1
    else
      return 0
    end
  else
    if Contact.group then
      local n=Contact.group:CountAliveUnits()
      return n
    else
      return 0
    end
  end
end

--- Count number of alive units in cluster.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster
-- @return #number unitcount
function INTEL:ClusterCountUnits(Cluster)
  local unitcount = 0
  for _,_contact in pairs (Cluster.Contacts) do
    local contact=_contact --#INTEL.Contact
    unitcount = unitcount + self:ContactCountUnits(contact)
  end
  return unitcount
end

--- Update cluster F10 marker.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster.
-- @return #INTEL self
function INTEL:UpdateClusterMarker(cluster)

  -- Create a marker.
  local unitcount = self:ClusterCountUnits(cluster)
  local text=string.format("Cluster #%d: %s\nSize %d\nUnits %d\nTLsum=%d", cluster.index, cluster.ctype, cluster.size, unitcount, cluster.threatlevelSum)

  if not cluster.marker then

    -- First time ==> need to create a new marker object.
    cluster.marker=MARKER:New(cluster.coordinate, text):ToCoalition(self.coalition)

  else

    -- Need to refresh?
    local refresh=false

    -- Check if marker text changed.
    if cluster.marker.text~=text then
      cluster.marker.text=text
      refresh=true
    end

    -- Check if coordinate changed.
    local coordchange=self:_CheckClusterCoordinateChanged(cluster, cluster.marker.coordinate)
    if coordchange then
      cluster.marker.coordinate:UpdateFromCoordinate(cluster.coordinate)
      refresh=true
    end

    if refresh then
      cluster.marker:Refresh()
    end

  end

  return self
end

--- Get the contact with the highest threat level from the cluster.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster.
-- @return #INTEL.Contact the contact or nil if none
function INTEL:GetHighestThreatContact(Cluster)
  local threatlevel=-1
  local rcontact = nil
  
  for _,_contact in pairs(Cluster.Contacts) do

    local contact=_contact --Ops.Intel#INTEL.Contact

    if contact.threatlevel>threatlevel then
      threatlevel=contact.threatlevel
      rcontact = contact
    end

  end
  return rcontact
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------
-- Start INTEL_DLINK
----------------------------------------------------------------------------------------------

--- **Ops_DLink** - Support for Office of Military Intelligence.
--
-- **Main Features:**
--
--    * Overcome limitations of (non-available) datalinks between ground radars
--    * Detect and track contacts consistently across INTEL instances
--    * Use FSM events to link functionality into your scripts
--    * Easy setup
--
--- ===
--
-- ### Author: **applevangelist**

--- INTEL_DLINK class.
-- @type INTEL_DLINK
-- @field #string ClassName Name of the class.
-- @field #string lid Class id string for output to DCS log file.
-- @field #number verbose Make the logging verbose.
-- @field #string alias Alias name for logging.
-- @field #number cachetime Number of seconds to keep an object.
-- @field #number interval Number of seconds between collection runs.
-- @field #table contacts Table of Ops.Intel#INTEL.Contact contacts.
-- @field #table clusters Table of Ops.Intel#INTEL.Cluster clusters.
-- @field #table contactcoords Table of contacts' Core.Point#COORDINATE objects.
-- @extends Core.Fsm#FSM

--- INTEL_DLINK data aggregator
-- @field #INTEL_DLINK
INTEL_DLINK = {
  ClassName       = "INTEL_DLINK",
  verbose         =     0,
  lid             =   nil,
  alias           =   nil,
  cachetime       =   120,
  interval        =   20,
  contacts        =   {},
  clusters        =   {},
  contactcoords   =   {},
}

--- Version string
-- @field #string version
INTEL_DLINK.version = "0.0.2"

--- Function to instantiate a new object
-- @param #INTEL_DLINK self
-- @param #table Intels Table of Ops.Intel#INTEL objects.
-- @param #string Alias (optional) Name of this instance. Default "SPECTRE"
-- @param #number Interval (optional) When to query #INTEL objects for detected items (default 20 seconds).
-- @param #number Cachetime (optional) How long to cache detected items (default 300 seconds).
-- @usage Use #INTEL_DLINK if you want to merge data from a number of #INTEL objects into one. This might be useful to simulate a
-- Data Link, e.g. for Russian-tech based EWR, realising a Star Topology @{https://en.wikipedia.org/wiki/Network_topology#Star}
-- in a basic setup. It will collect the contacts and clusters from the #INTEL objects.
-- Contact duplicates are removed. Clusters might contain duplicates (Might fix that later, WIP).
--
-- Basic setup:
--
--     local datalink = INTEL_DLINK:New({myintel1,myintel2}), "FSB", 20, 300)
--     datalink:__Start(2)
--
-- Add an Intel while running:
--
--     datalink:AddIntel(myintel3)
--
-- Gather the data:
--
--     datalink:GetContactTable() -- #table of #INTEL.Contact contacts.
--     datalink:GetClusterTable() -- #table of #INTEL.Cluster clusters.
--     datalink:GetDetectedItemCoordinates() -- #table of contact coordinates, to be compatible with @{Functional.Detection#DETECTION}.
--
-- Gather data with the event function:
--
--     function datalink:OnAfterCollected(From, Event, To, Contacts, Clusters)
--       ... <your code here> ...
--     end
--
function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime)

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #INTEL_DLINK

  self.intels = Intels or {}
  self.contacts = {}
  self.clusters = {}
  self.contactcoords = {}

  -- Set alias.
  if Alias then
    self.alias=tostring(Alias)
  else
    self.alias="SPECTRE"
  end

  -- Interval
  self.interval = Interval or 20

  -- Set some string id for output to DCS.log file.
  self.lid=string.format("INTEL_DLINK %s | ", self.alias)

  -- Cache time
  self:SetDLinkCacheTime(Cachetime or 120)
  
    -- Start State.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  --                 From State  -->   Event        -->    To State
  self:AddTransition("Stopped",       "Start",             "Running")     -- Start FSM.
  self:AddTransition("*",             "Collect",           "*")           -- Collect data.
  self:AddTransition("*",             "Collected",         "*")           -- Collection of data done.
  self:AddTransition("*",             "Stop",              "Stopped")     -- Stop FSM.

  ----------------------------------------------------------------------------------------------
  -- Pseudo Functions
  ----------------------------------------------------------------------------------------------
  --- Triggers the FSM event "Start". Starts the INTEL_DLINK.
  -- @function [parent=#INTEL_DLINK] Start
  -- @param #INTEL_DLINK self

  --- Triggers the FSM event "Start" after a delay. Starts the INTEL_DLINK.
  -- @function [parent=#INTEL_DLINK] __Start
  -- @param #INTEL_DLINK self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the INTEL_DLINK.
  -- @param #INTEL_DLINK self

  --- Triggers the FSM event "Stop" after a delay. Stops the INTEL_DLINK.
  -- @function [parent=#INTEL_DLINK] __Stop
  -- @param #INTEL_DLINK self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Collect". Used internally to collect all data.
  -- @function [parent=#INTEL_DLINK] Collect
  -- @param #INTEL_DLINK self

  --- Triggers the FSM event "Collect" after a delay.
  -- @function [parent=#INTEL_DLINK] __Status
  -- @param #INTEL_DLINK self
  -- @param #number delay Delay in seconds.

  --- On After "Collected" event. Data tables have been refreshed.
  -- @function [parent=#INTEL_DLINK] OnAfterCollected
  -- @param #INTEL_DLINK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #table Contacts Table of #INTEL.Contact Contacts.
  -- @param #table Clusters Table of #INTEL.Cluster Clusters.

  return self
end
----------------------------------------------------------------------------------------------
-- Helper & User Functions
----------------------------------------------------------------------------------------------

--- Function to add an #INTEL object to the aggregator
-- @param #INTEL_DLINK self
-- @param Ops.Intel#INTEL Intel the #INTEL object to add
-- @return #INTEL_DLINK self
function INTEL_DLINK:AddIntel(Intel)
   self:T(self.lid .. "AddIntel")
   if Intel then
    table.insert(self.intels,Intel)
   end
   return self
end

----------------------------------------------------------------------------------------------
-- FSM Functions
----------------------------------------------------------------------------------------------

--- Function to start the work.
-- @param #INTEL_DLINK self
-- @param #string From The From state
-- @param #string Event The Event triggering this call
-- @param #string To The To state
-- @return #INTEL_DLINK self
function INTEL_DLINK:onafterStart(From, Event, To)
  self:T({From, Event, To})
  local text = string.format("Version %s started.", self.version)
  self:I(self.lid .. text)
  self:__Collect(-math.random(1,10))
  return self
end

  --- Function to set how long INTEL DLINK remembers contacts.
  -- @param #INTEL_DLINK self
  -- @param #number seconds Remember this many seconds. Defaults to 180.
  -- @return #INTEL_DLINK self
  function INTEL_DLINK:SetDLinkCacheTime(seconds)
    self.cachetime = math.abs(seconds or 120)
    self:I(self.lid.."Caching for "..self.cachetime.." seconds.")
    return self
  end

--- Function to collect data from the various #INTEL
-- @param #INTEL_DLINK self
-- @param #string From The From state
-- @param #string Event The Event triggering this call
-- @param #string To The To state
-- @return #INTEL_DLINK self
function INTEL_DLINK:onbeforeCollect(From, Event, To)
  self:T({From, Event, To})
  -- run through our #INTEL objects and gather the contacts tables
  self:T("Contacts Data Gathering")
  local newcontacts = {}
  local intels = self.intels -- #table
  for _,_intel in pairs (intels) do
    _intel = _intel -- #INTEL
    if _intel:Is("Running") then
      local ctable = _intel:GetContactTable() or {} -- #INTEL.Contact
        for _,_contact in pairs (ctable) do
          local _ID = string.format("%s-%d",_contact.groupname, _contact.Tdetected)
          self:T(string.format("Adding %s",_ID))
          newcontacts[_ID] = _contact
        end
    end
  end
  -- clean up for stale contacts and dupes
  self:T("Cleanup")
  local contacttable = {}
  local coordtable = {}
  local TNow = timer.getAbsTime()
  local Tcache = self.cachetime
  for _ind, _contact in pairs(newcontacts) do -- #string, #INTEL.Contact
    if TNow - _contact.Tdetected < Tcache then
      if (not contacttable[_contact.groupname]) or (contacttable[_contact.groupname] and contacttable[_contact.groupname].Tdetected < _contact.Tdetected) then
        self:T(string.format("Adding %s",_contact.groupname))
        contacttable[_contact.groupname] = _contact
        table.insert(coordtable,_contact.position)
      end
    end
  end
  -- run through our #INTEL objects and gather the clusters tables
  self:T("Clusters Data Gathering")
  local newclusters = {}
  local intels = self.intels -- #table
  for _,_intel in pairs (intels) do
    _intel = _intel -- #INTEL
    if _intel:Is("Running") then
      local ctable = _intel:GetClusterTable() or {} -- #INTEL.Cluster
        for _,_cluster in pairs (ctable) do
          local _ID = string.format("%s-%d", _intel.alias, _cluster.index)
          self:T(string.format("Adding %s",_ID))
          table.insert(newclusters,_cluster)
        end
    end
  end
  -- update self tables
  self.contacts = contacttable
  self.contactcoords = coordtable
  self.clusters = newclusters
  self:__Collected(1, contacttable, newclusters) -- make table available via FSM Event
  -- schedule next round
  local interv = self.interval * -1
  self:__Collect(interv)
  return self
end

--- Function called after collection is done
-- @param #INTEL_DLINK self
-- @param #string From The From state
-- @param #string Event The Event triggering this call
-- @param #string To The To state
-- @param #table Contacts The table of collected #INTEL.Contact contacts
-- @param #table Clusters The table of collected #INTEL.Cluster clusters
-- @return #INTEL_DLINK self
function INTEL_DLINK:onbeforeCollected(From, Event, To, Contacts, Clusters)
  self:T({From, Event, To})
  return self
end

--- Function to stop
-- @param #INTEL_DLINK self
-- @param #string From The From state
-- @param #string Event The Event triggering this call
-- @param #string To The To state
-- @return #INTEL_DLINK self
function INTEL_DLINK:onafterStop(From, Event, To)
  self:T({From, Event, To})
  local text = string.format("Version %s stopped.", self.version)
  self:I(self.lid .. text)
  return self
end

--- Function to query the detected contacts
-- @param #INTEL_DLINK self
-- @return #table Table of #INTEL.Contact contacts
function INTEL_DLINK:GetContactTable()
  self:T(self.lid .. "GetContactTable")
  return self.contacts
end

--- Function to query the detected clusters
-- @param #INTEL_DLINK self
-- @return #table Table of #INTEL.Cluster clusters
function INTEL_DLINK:GetClusterTable()
  self:T(self.lid .. "GetClusterTable")
  return self.clusters
end

--- Function to query the detected contact coordinates
-- @param #INTEL_DLINK self
-- @return #table Table of the contacts' Core.Point#COORDINATE objects.
function INTEL_DLINK:GetDetectedItemCoordinates()
  self:T(self.lid .. "GetDetectedItemCoordinates")
  return self.contactcoords
end

----------------------------------------------------------------------------------------------
-- End INTEL_DLINK
----------------------------------------------------------------------------------------------
--- **Ops** - Legion Warehouse.
--
-- Parent class of Airwings, Brigades and Fleets.
--
-- ===
--
-- ### Author: **funkyfranky**
-- 
-- ===
-- @module Ops.Legion
-- @image OPS_Legion.png


--- LEGION class.
-- @type LEGION
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity of output.
-- @field #string lid Class id string for output to DCS log file.
-- @field #table missionqueue Mission queue table.
-- @field #table transportqueue Transport queue.
-- @field #table cohorts Cohorts of this legion.
-- @field Ops.Commander#COMMANDER commander Commander of this legion.
-- @field Ops.Chief#CHIEF chief Chief of this legion.
-- @field #boolean tacview If `true`, show tactical overview on status update.
-- @extends Functional.Warehouse#WAREHOUSE

--- *Per aspera ad astra.*
--
-- ===
--
-- # The LEGION Concept
-- 
-- The LEGION class contains all functions that are common for the AIRWING, BRIGADE and FLEET classes, which inherit the LEGION class.
-- 
-- An LEGION consists of multiple COHORTs. These cohorts "live" in a WAREHOUSE, i.e. a physical structure that can be destroyed or captured.
-- 
-- ** The LEGION class is not meant to be used directly. Use AIRWING, BRIGADE or FLEET instead! **
--
-- @field #LEGION
LEGION = {
  ClassName      = "LEGION",
  verbose        =     0,
  lid            =   nil,
  missionqueue   =    {},
  transportqueue =    {},
  cohorts        =    {},
}

--- Random score that is added to the asset score in the selection process.
-- @field #number RandomAssetScore
LEGION.RandomAssetScore=1


--- LEGION class version.
-- @field #string version
LEGION.version="0.5.1"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- DONE: Create FLEED class.
-- DONE: Relocate cohorts.
-- DONE: Aircraft will not start hot on Alert5.
-- DONE: OPS transport.
-- DONE: Make general so it can be inherited by AIRWING and BRIGADE classes.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new LEGION class object.
-- @param #LEGION self
-- @param #string WarehouseName Name of the warehouse STATIC or UNIT object representing the warehouse.
-- @param #string LegionName Name of the legion. Must be **unique**!
-- @return #LEGION self
function LEGION:New(WarehouseName, LegionName)

  -- Inherit everything from WAREHOUSE class.
  local self=BASE:Inherit(self, WAREHOUSE:New(WarehouseName, LegionName)) -- #LEGION

  -- Nil check.
  if not self then
    BASE:E(string.format("ERROR: Could not find warehouse %s!", WarehouseName))
    return nil
  end

  -- Set some string id for output to DCS.log file.
  self.lid=string.format("LEGION %s | ", self.alias)
  
  -- Defaults:
  self:SetMarker(false)
  
  -- Dead and crash events are handled via opsgroups.
  self:UnHandleEvent(EVENTS.Crash)
  self:UnHandleEvent(EVENTS.Dead)

  -- Add FSM transitions.
  --                 From State  -->   Event        -->      To State
  self:AddTransition("*",             "MissionRequest",      "*")           -- Add a (mission) request to the warehouse.
  self:AddTransition("*",             "MissionCancel",       "*")           -- Cancel mission.
  self:AddTransition("*",             "MissionAssign",       "*")           -- Recruit assets, add to queue and request immediately.
  
  self:AddTransition("*",             "TransportRequest",    "*")           -- Add a (mission) request to the warehouse.
  self:AddTransition("*",             "TransportCancel",     "*")           -- Cancel transport.
  self:AddTransition("*",             "TransportAssign",     "*")           -- Recruit assets, add to queue and request immediately.  
  
  self:AddTransition("*",             "OpsOnMission",        "*")           -- An OPSGROUP was send on a Mission (AUFTRAG).
  
  self:AddTransition("*",             "LegionAssetReturned", "*")           -- An asset returned (from a mission) to the Legion warehouse.
  
  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the LEGION. Initializes parameters and starts event handlers.
  -- @function [parent=#LEGION] Start
  -- @param #LEGION self

  --- Triggers the FSM event "Start" after a delay. Starts the LEGION. Initializes parameters and starts event handlers.
  -- @function [parent=#LEGION] __Start
  -- @param #LEGION self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the LEGION and all its event handlers.
  -- @param #LEGION self

  --- Triggers the FSM event "Stop" after a delay. Stops the LEGION and all its event handlers.
  -- @function [parent=#LEGION] __Stop
  -- @param #LEGION self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "MissionCancel".
  -- @function [parent=#LEGION] MissionCancel
  -- @param #LEGION self
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.


  --- Triggers the FSM event "MissionAssign".
  -- @function [parent=#LEGION] MissionAssign
  -- @param #LEGION self
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.
  -- @param #table Legions The legion(s) from which the mission assets are requested.

  --- Triggers the FSM event "MissionAssign" after a delay.
  -- @function [parent=#LEGION] __MissionAssign
  -- @param #LEGION self
  -- @param #number delay Delay in seconds.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.
  -- @param #table Legions The legion(s) from which the mission assets are requested.

  --- On after "MissionAssign" event.
  -- @function [parent=#LEGION] OnAfterMissionAssign
  -- @param #LEGION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.
  -- @param #table Legions The legion(s) from which the mission assets are requested.


  --- Triggers the FSM event "MissionRequest".
  -- @function [parent=#LEGION] MissionRequest
  -- @param #LEGION self
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.
  -- @param #table Assets (Optional) Assets to add.

  --- Triggers the FSM event "MissionRequest" after a delay.
  -- @function [parent=#LEGION] __MissionRequest
  -- @param #LEGION self
  -- @param #number delay Delay in seconds.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.
  -- @param #table Assets (Optional) Assets to add.

  --- On after "MissionRequest" event.
  -- @function [parent=#LEGION] OnAfterMissionRequest
  -- @param #LEGION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.
  -- @param #table Assets (Optional) Assets to add.


  --- Triggers the FSM event "MissionCancel" after a delay.
  -- @function [parent=#LEGION] __MissionCancel
  -- @param #LEGION self
  -- @param #number delay Delay in seconds.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "MissionCancel" event.
  -- @function [parent=#LEGION] OnAfterMissionCancel
  -- @param #LEGION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.


  --- Triggers the FSM event "TransportAssign".
  -- @function [parent=#LEGION] TransportAssign
  -- @param #LEGION self
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.
  -- @param #table Legions The legion(s) to which this transport is assigned.

  --- Triggers the FSM event "TransportAssign" after a delay.
  -- @function [parent=#LEGION] __TransportAssign
  -- @param #LEGION self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.
  -- @param #table Legions The legion(s) to which this transport is assigned.

  --- On after "TransportAssign" event.
  -- @function [parent=#LEGION] OnAfterTransportAssign
  -- @param #LEGION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.
  -- @param #table Legions The legion(s) to which this transport is assigned.


  --- Triggers the FSM event "TransportRequest".
  -- @function [parent=#LEGION] TransportRequest
  -- @param #LEGION self
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.

  --- Triggers the FSM event "TransportRequest" after a delay.
  -- @function [parent=#LEGION] __TransportRequest
  -- @param #LEGION self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.

  --- On after "TransportRequest" event.
  -- @function [parent=#LEGION] OnAfterTransportRequest
  -- @param #LEGION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.


  --- Triggers the FSM event "TransportCancel".
  -- @function [parent=#LEGION] TransportCancel
  -- @param #LEGION self
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.

  --- Triggers the FSM event "TransportCancel" after a delay.
  -- @function [parent=#LEGION] __TransportCancel
  -- @param #LEGION self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.

  --- On after "TransportCancel" event.
  -- @function [parent=#LEGION] OnAfterTransportCancel
  -- @param #LEGION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.


  --- Triggers the FSM event "OpsOnMission".
  -- @function [parent=#LEGION] OpsOnMission
  -- @param #LEGION self
  -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- Triggers the FSM event "OpsOnMission" after a delay.
  -- @function [parent=#LEGION] __OpsOnMission
  -- @param #LEGION self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "OpsOnMission" event.
  -- @function [parent=#LEGION] OnAfterOpsOnMission
  -- @param #LEGION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroup The OPS group on mission.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.


  --- Triggers the FSM event "LegionAssetReturned".
  -- @function [parent=#LEGION] LegionAssetReturned
  -- @param #LEGION self
  -- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to.
  -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned.

  --- Triggers the FSM event "LegionAssetReturned" after a delay.
  -- @function [parent=#LEGION] __LegionAssetReturned
  -- @param #LEGION self
  -- @param #number delay Delay in seconds. 
  -- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to.
  -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned.

  --- On after "LegionAssetReturned" event. Triggered when an asset group returned to its Legion.
  -- @function [parent=#LEGION] OnAfterLegionAssetReturned
  -- @param #LEGION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to.
  -- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned.


  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set verbosity level.
-- @param #LEGION self
-- @param #number VerbosityLevel Level of output (higher=more). Default 0.
-- @return #LEGION self
function LEGION:SetVerbosity(VerbosityLevel)
  self.verbose=VerbosityLevel or 0
  return self
end

--- Set tactical overview on.
-- @param #LEGION self
-- @return #LEGION self
function LEGION:SetTacticalOverviewOn()  
  self.tacview=true
  return self
end

--- Add a mission for the legion. It will pick the best available assets for the mission and lauch it when ready. 
-- @param #LEGION self
-- @param Ops.Auftrag#AUFTRAG Mission Mission for this legion.
-- @return #LEGION self
function LEGION:AddMission(Mission)

  -- Set status to QUEUED. This event is only allowed for the first legion that calls it.
  Mission:Queued()
  
  -- Set legion status.
  Mission:SetLegionStatus(self, AUFTRAG.Status.QUEUED)
  
  -- Add legion to mission.
  Mission:AddLegion(self)
  
  -- Set target for ALERT 5.
  if Mission.type==AUFTRAG.Type.ALERT5 then
    Mission:_TargetFromObject(self:GetCoordinate())
  end

  -- Add mission to queue.
  table.insert(self.missionqueue, Mission)

  -- Info text.
  local text=string.format("Added mission %s (type=%s). Starting at %s. Stopping at %s",
  tostring(Mission.name), tostring(Mission.type), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF")
  self:T(self.lid..text)

  return self
end

--- Remove mission from queue.
-- @param #LEGION self
-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed.
-- @return #LEGION self
function LEGION:RemoveMission(Mission)

  for i,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    if mission.auftragsnummer==Mission.auftragsnummer then
      mission:RemoveLegion(self)
      table.remove(self.missionqueue, i)
      break
    end

  end

  return self
end

--- Add transport assignment to queue. 
-- @param #LEGION self
-- @param Ops.OpsTransport#OPSTRANSPORT OpsTransport Transport assignment.
-- @return #LEGION self
function LEGION:AddOpsTransport(OpsTransport)

  -- Is not queued at a legion.
  OpsTransport:Queued()
  
  -- Set legion status.
  OpsTransport:SetLegionStatus(self, AUFTRAG.Status.QUEUED)  

  -- Add mission to queue.
  table.insert(self.transportqueue, OpsTransport)
  
  -- Add this legion to the transport.
  OpsTransport:AddLegion(self)

  -- Info text.
  local text=string.format("Added Transport %s. Starting at %s-%s",
  tostring(OpsTransport.uid), UTILS.SecondsToClock(OpsTransport.Tstart, true), OpsTransport.Tstop and UTILS.SecondsToClock(OpsTransport.Tstop, true) or "INF")
  self:T(self.lid..text)

  return self
end

--- Add cohort to cohort table of this legion.
-- @param #LEGION self
-- @param Ops.Cohort#COHORT Cohort The cohort to be added.
-- @return #LEGION self
function LEGION:AddCohort(Cohort)
  
  if self:IsCohort(Cohort.name) then
  
    self:E(self.lid..string.format("ERROR: A cohort with name %s already exists in this legion. Cohorts must have UNIQUE names!"))
  
  else
  
    -- Add cohort to legion.
    table.insert(self.cohorts, Cohort)
    
  end

  return self
end

--- Remove cohort from cohor table of this legion.
-- @param #LEGION self
-- @param Ops.Cohort#COHORT Cohort The cohort to be added.
-- @return #LEGION self
function LEGION:DelCohort(Cohort)

  for i=#self.cohorts,1,-1 do
    local cohort=self.cohorts[i] --Ops.Cohort#COHORT
    if cohort.name==Cohort.name then
      self:T(self.lid..string.format("Removing Cohort %s", tostring(cohort.name)))
      table.remove(self.cohorts, i)
    end
  end

  return self
end

--- Remove specific asset from legion.
-- @param #LEGION self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset.
-- @return #LEGION self
function LEGION:DelAsset(Asset)

  if Asset.cohort then
    Asset.cohort:DelAsset(Asset)
  else
    self:E(self.lid..string.format("ERROR: Asset has not cohort attached. Cannot remove it from legion!"))
  end
  
  return self
end


--- Relocate a cohort to another legion.
-- Assets in stock are spawned and routed to the new legion.
-- If assets are spawned, running missions will be cancelled.
-- Cohort assets will not be available until relocation is finished.
-- @param #LEGION self
-- @param Ops.Cohort#COHORT Cohort The cohort to be relocated.
-- @param Ops.Legion#LEGION Legion The legion where the cohort is relocated to.
-- @param #number Delay Delay in seconds before relocation takes place. Default `nil`, *i.e.* ASAP.
-- @param #number NcarriersMin Min number of transport carriers in case the troops should be transported. Default `nil` for no transport.
-- @param #number NcarriersMax Max number of transport carriers.
-- @param #table TransportLegions Legion(s) assigned for transportation. Default is that transport assets can only be recruited from this legion.
-- @return #LEGION self
function LEGION:RelocateCohort(Cohort, Legion, Delay, NcarriersMin, NcarriersMax, TransportLegions)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, LEGION.RelocateCohort, self, Cohort, Legion, 0, NcarriersMin, NcarriersMax, TransportLegions)
  else
  
    -- Add cohort to legion.
    if Legion:IsCohort(Cohort.name) then
      self:E(self.lid..string.format("ERROR: Cohort %s is already part of new legion %s ==> CANNOT Relocate!", Cohort.name, Legion.alias))
      return self
    else
      table.insert(Legion.cohorts, Cohort)      
    end
    
    -- Check that cohort is part of this legion
    if not self:IsCohort(Cohort.name) then
      self:E(self.lid..string.format("ERROR: Cohort %s is NOT part of this legion %s ==> CANNOT Relocate!", Cohort.name, self.alias))
      return self    
    end
    
    -- Check that legions are different.
    if self.alias==Legion.alias then
      self:E(self.lid..string.format("ERROR: old legion %s is same as new legion %s ==> CANNOT Relocate!", self.alias, Legion.alias))
      return self    
    end
    
    -- Trigger Relocate event.
    Cohort:Relocate()
    
    -- Create a relocation mission.
    local mission=AUFTRAG:_NewRELOCATECOHORT(Legion, Cohort)
    
    if false then
      --- Disabled for now.
    
      -- Add assets to mission.
      mission:_AddAssets(Cohort.assets)
  
      -- Debug info.
      self:I(self.lid..string.format("Relocating Cohort %s [nassets=%d] to legion %s", Cohort.name, #Cohort.assets, Legion.alias))
      
      -- Assign mission to this legion.
      self:MissionAssign(mission, {self})
    
    else
    
      -- Assign cohort to mission.
      mission:AssignCohort(Cohort)
      
      -- All assets required.
      mission:SetRequiredAssets(#Cohort.assets)
      
      -- Set transportation.
      if NcarriersMin and NcarriersMin>0 then
        mission:SetRequiredTransport(Legion.spawnzone, NcarriersMin, NcarriersMax)
      end
      
      -- Assign transport legions.
      if TransportLegions then
        for _,legion in pairs(TransportLegions) do
          mission:AssignTransportLegion(legion)
        end
      end
      
      -- Set mission range very large. Mission designer should know...
      mission:SetMissionRange(10000)
      
      -- Add mission.
      self:AddMission(mission)
    end
      
  end

  return self
end

--- Get cohort by name.
-- @param #LEGION self
-- @param #string CohortName Name of the platoon.
-- @return Ops.Cohort#COHORT The Cohort object.
function LEGION:_GetCohort(CohortName)

  for _,_cohort in pairs(self.cohorts) do
    local cohort=_cohort --Ops.Cohort#COHORT

    if cohort.name==CohortName then
      return cohort
    end

  end

  return nil
end

--- Check if cohort is part of this legion.
-- @param #LEGION self
-- @param #string CohortName Name of the platoon.
-- @return #boolean If `true`, cohort is part of this legion.
function LEGION:IsCohort(CohortName)

  for _,_cohort in pairs(self.cohorts) do
    local cohort=_cohort --Ops.Cohort#COHORT

    if cohort.name==CohortName then
      return true
    end

  end

  return false
end

--- Get name of legion. This is the alias of the warehouse.
-- @param #LEGION self
-- @return #string Name of legion.
function LEGION:GetName()
  return self.alias
end

--- Get cohort of an asset.
-- @param #LEGION self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset.
-- @return Ops.Cohort#COHORT The Cohort object.
function LEGION:_GetCohortOfAsset(Asset)
  local cohort=self:_GetCohort(Asset.squadname)
  return cohort
end


--- Check if a BRIGADE class is calling.
-- @param #LEGION self
-- @return #boolean If true, this is a BRIGADE.
function LEGION:IsBrigade()
  local is=self.ClassName==BRIGADE.ClassName
  return is
end

--- Check if the AIRWING class is calling.
-- @param #LEGION self
-- @return #boolean If true, this is an AIRWING.
function LEGION:IsAirwing()
  local is=self.ClassName==AIRWING.ClassName
  return is
end

--- Check if the FLEET class is calling.
-- @param #LEGION self
-- @return #boolean If true, this is a FLEET.
function LEGION:IsFleet()
  local is=self.ClassName==FLEET.ClassName
  return is
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start & Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Start LEGION FSM.
-- @param #LEGION self
function LEGION:onafterStart(From, Event, To)

  -- Start parent Warehouse.
  self:GetParent(self, LEGION).onafterStart(self, From, Event, To)

  -- Info.
  self:T3(self.lid..string.format("Starting LEGION v%s", LEGION.version))

end

--- Check mission queue and assign ONE mission.
-- @param #LEGION self
-- @return #boolean If `true`, a mission was found and requested.
function LEGION:CheckMissionQueue()

  -- Number of missions.
  local Nmissions=#self.missionqueue

  -- Treat special cases.
  if Nmissions==0 then
    return nil
  end

  -- Loop over missions in queue.
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    if mission:IsNotOver() and mission:IsReadyToCancel() then
      mission:Cancel()
    end
    
    -- Housekeeping
    local TNow = timer.getTime()
    if mission:IsOver() and mission:IsNotRepeatable() and mission.DeletionTimstamp == nil then
      mission.DeletionTimstamp = TNow
    end
    if mission.DeletionTimstamp ~= nil and TNow - mission.DeletionTimstamp > 1800 then
      mission = nil
    end
  end
  
  -- Check that runway is operational and that carrier is not recovering.
  if self:IsAirwing() then
    if self:IsRunwayOperational()==false then
      return nil
    end
    local airboss=self.airboss --Ops.Airboss#AIRBOSS
    if airboss then
      if not airboss:IsIdle() then
        return nil
      end
    end
  end

  -- Sort results table wrt prio and start time.
  local function _sort(a, b)
    local taskA=a --Ops.Auftrag#AUFTRAG
    local taskB=b --Ops.Auftrag#AUFTRAG
    return (taskA.prio<taskB.prio) or (taskA.prio==taskB.prio and taskA.Tstart<taskB.Tstart)
  end
  table.sort(self.missionqueue, _sort)

  -- Search min importance.
  local vip=math.huge
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG
    if mission.importance and mission.importance<vip then
      vip=mission.importance
    end
  end

  -- Look for first task that is not accomplished.
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG
    
    -- Check if reinforcement is necessary.
    local reinforce=false
    if mission:IsExecuting() and mission.reinforce and mission.reinforce>0 then
    
      -- Number of current opsgroups.
      local N=mission.Nassigned-mission.Ndead
      
      if N<mission.NassetsMin then
        reinforce=true
      end
      
      -- Debug info.
      self:T(self.lid..string.format("Checking Reinforcement Nreinf=%d, Nops=%d, Nassigned=%d, Ndead=%d, Nmin=%d ==> Reinforce=%s", 
      mission.reinforce, N, mission.Nassigned, mission.Ndead, mission.NassetsMin, tostring(reinforce)))
    end

    -- Firstly, check if mission is due?
    if (mission:IsQueued(self) or reinforce) and mission:IsReadyToGo() and (mission.importance==nil or mission.importance<=vip) then

      -- Recruit best assets for the job.    
      local recruited, assets, legions=self:RecruitAssetsForMission(mission)

      -- Did we find enough assets?
      if recruited then
    
        -- Add to mission.
        --mission:_AddAssets(assets)
  
        -- Recruit asset for escorting recruited mission assets.
        local EscortAvail=self:RecruitAssetsForEscort(mission, assets)

        -- Transport available (or not required).
        local TransportAvail=true
        
        -- Is escort required and available?
        if EscortAvail then
        
          -- Recruit carrier assets for transport.
          local Transport=nil
          if mission.NcarriersMin then
          
            -- Transport legions.
            local Legions=mission.transportLegions or {self}
                        
            -- Assign carrier assets for transport.
            TransportAvail, Transport=self:AssignAssetsForTransport(Legions, assets, mission.NcarriersMin, mission.NcarriersMax, mission.transportDeployZone, mission.transportDisembarkZone, mission.carrierCategories, mission.carrierAttributes, mission.carrierProperties)
          end
          
          -- Add opstransport to mission.
          if TransportAvail and Transport then
            mission.opstransport=Transport
          end
          
        end
        
        if EscortAvail and TransportAvail then
        
          -- Got a mission.
          self:MissionRequest(mission, assets)
          
          -- Reduce number of reinforcements.
          if reinforce then
            mission.reinforce=mission.reinforce-#assets
            self:T(self.lid..string.format("Reinforced with N=%d Nreinforce=%d", #assets, mission.reinforce))
          end
          
          return true
        else
          -- Recruited assets but no requested escort available. Unrecruit assets!
          LEGION.UnRecruitAssets(assets, mission)
        end
        
      end -- recruited mission assets

    end -- mission due?
  end -- mission loop

  return nil
end

--- Check transport queue and assign ONE transport.
-- @param #LEGION self
-- @return #boolean If `true`, a transport was found and requested.
function LEGION:CheckTransportQueue()

  -- Number of missions.
  local Ntransports=#self.transportqueue

  -- Treat special cases.
  if Ntransports==0 then
    return nil
  end
  
  -- TODO: Remove transports that are over!
  
  -- Sort results table wrt prio and start time.
  local function _sort(a, b)
    local taskA=a --Ops.Auftrag#AUFTRAG
    local taskB=b --Ops.Auftrag#AUFTRAG
    return (taskA.prio<taskB.prio) or (taskA.prio==taskB.prio and taskA.Tstart<taskB.Tstart)
  end
  table.sort(self.transportqueue, _sort)

  -- Get the lowest importance value (lower means more important).
  -- If a mission with importance 1 exists, mission with importance 2 will not be assigned. Missions with no importance (nil) can still be selected. 
  local vip=math.huge
  for _,_transport in pairs(self.transportqueue) do
    local transport=_transport --Ops.OpsTransport#OPSTRANSPORT
    if transport.importance and transport.importance<vip then
      vip=transport.importance
    end
  end
  
  -- Look for first task that is not accomplished.
  for _,_transport in pairs(self.transportqueue) do
    local transport=_transport --Ops.OpsTransport#OPSTRANSPORT

    -- Check if transport is still queued and ready.
    if transport:IsQueued(self) and transport:IsReadyToGo() and (transport.importance==nil or transport.importance<=vip) then
    
      -- Recruit assets for transport.
      local recruited, assets, _=self:RecruitAssetsForTransport(transport)
      
      -- Did we find enough assets?
      if recruited then
      
        -- Add asset to transport.
        for _,_asset in pairs(assets) do
          local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
          transport:AddAsset(asset)
        end
        
        -- Got transport ==> Request and return.
        self:TransportRequest(transport)
        return true
      end
      
    end
  end
  
  -- No transport found.
  return nil
end



-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "MissionAssign" event. Mission is added to a LEGION mission queue and already requested. Needs assets to be added to the mission already.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
-- @param #table Legions The LEGIONs.
function LEGION:onafterMissionAssign(From, Event, To, Mission, Legions)
  
  for _,_Legion in pairs(Legions) do
    local Legion=_Legion --Ops.Legion#LEGION

    -- Debug info.
    self:T(self.lid..string.format("Assigning mission %s (%s) to legion %s", Mission.name, Mission.type, Legion.alias))
 
    -- Add mission to legion.
    Legion:AddMission(Mission)
    
    -- Directly request the mission as the assets have already been selected.
    Legion:MissionRequest(Mission)
    
  end

end

--- Create a request and add it to the warehouse queue.
-- @param #LEGION self
-- @param Functional.Warehouse#WAREHOUSE.Descriptor AssetDescriptor Descriptor describing the asset that is requested.
-- @param AssetDescriptorValue Value of the asset descriptor. Type depends on descriptor, i.e. could be a string, etc.
-- @param #number nAsset Number of groups requested that match the asset specification.
-- @param #number Prio Priority of the request. Number ranging from 1=high to 100=low.
-- @param #string Assignment A keyword or text that can later be used to identify this request and postprocess the assets.
-- @return Functional.Warehouse#WAREHOUSE.Queueitem The request.
function LEGION:_AddRequest(AssetDescriptor, AssetDescriptorValue, nAsset, Prio, Assignment)

  -- Defaults.
  nAsset=nAsset or 1
  Prio=Prio or 50

  -- Increase id.
  self.queueid=self.queueid+1

  -- Request queue table item.
  local request={
  uid=self.queueid,
  prio=Prio,
  warehouse=self,
  assetdesc=AssetDescriptor,
  assetdescval=AssetDescriptorValue,
  nasset=nAsset,
  transporttype=WAREHOUSE.TransportType.SELFPROPELLED,
  ntransport=0,
  assignment=tostring(Assignment),
  airbase=self:GetAirbase(),
  category=self:GetAirbaseCategory(),
  ndelivered=0,
  ntransporthome=0,
  assets={},
  toself=true,
  } --Functional.Warehouse#WAREHOUSE.Queueitem
  

  -- Add request to queue.
  table.insert(self.queue, request)
  
  local descval="assetlist"
  if request.assetdesc==WAREHOUSE.Descriptor.ASSETLIST then
  
  else
    descval=tostring(request.assetdescval)
  end

  local text=string.format("Warehouse %s: New request from warehouse %s.\nDescriptor %s=%s, #assets=%s; Transport=%s, #transports=%s.",
  self.alias, self.alias, request.assetdesc, descval, tostring(request.nasset), request.transporttype, tostring(request.ntransport))
  self:_DebugMessage(text, 5)

  return request
end


--- On after "MissionRequest" event. Performs a self request to the warehouse for the mission assets. Sets mission status to REQUESTED.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Auftrag#AUFTRAG Mission The requested mission.
-- @param #table Assets (Optional) Assets to add.
function LEGION:onafterMissionRequest(From, Event, To, Mission, Assets)

  -- Debug info.
  self:T(self.lid..string.format("MissionRequest for mission %s [%s]", Mission:GetName(), Mission:GetType()))
  
  -- Take provided assets or that of the mission.
  Assets=Assets or Mission.assets

  -- Set mission status from QUEUED to REQUESTED.
  Mission:Requested()
  
  -- Set legion status. Ensures that it is not considered in the next selection.
  Mission:SetLegionStatus(self, AUFTRAG.Status.REQUESTED)

  ---
  -- Some assets might already be spawned and even on a different mission (orbit).
  -- Need to dived to set into spawned and instock assets and handle the other
  ---

  -- Assets to be requested.
  local Assetlist={}

  for _,_asset in pairs(Assets) do
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem

    -- Check that this asset belongs to this Legion warehouse.
    if asset.wid==self.uid then

      if asset.spawned then
      
        ---
        -- Spawned Assets
        ---
  
        if asset.flightgroup and not asset.flightgroup:IsMissionInQueue(Mission) then
          
          -- Add new mission.
          asset.flightgroup:AddMission(Mission)
          
          ---
          -- Special Missions
          ---
          
          -- Get current mission.
          local currM=asset.flightgroup:GetMissionCurrent()
                              
          if currM then
          
            -- Cancel?
            local cancel=false
            -- Pause?
            local pause=false
          
            -- Check if mission is INTERCEPT and asset is currently on GCI mission. If so, GCI is paused.
            if (currM.type==AUFTRAG.Type.GCICAP or currM.type==AUFTRAG.Type.PATROLRACETRACK) and Mission.type==AUFTRAG.Type.INTERCEPT then
              pause=true
            elseif (currM.type==AUFTRAG.Type.ONGUARD or currM.type==AUFTRAG.Type.PATROLZONE) and (Mission.type==AUFTRAG.Type.ARTY or Mission.type==AUFTRAG.Type.GROUNDATTACK) then
              pause=true
            elseif currM.type==AUFTRAG.Type.NOTHING then
              pause=true
            end
            
            -- Cancel current ALERT5 mission.
            if currM.type==AUFTRAG.Type.ALERT5 then
              cancel=true
            end
                        
            -- Cancel current mission for relcation.
            if Mission.type==AUFTRAG.Type.RELOCATECOHORT then
              cancel=true
              
              -- Get request.
              local request=currM:_GetRequest(self)
              
              if request then
                self:T2(self.lid.."Removing group from cargoset")
                request.cargogroupset:Remove(asset.spawngroupname, true)
              else
                self:E(self.lid.."ERROR: no request for spawned asset!")
              end              
              
            end
            
            -- Cancel mission.
            if cancel then
              self:T(self.lid..string.format("Cancel current mission %s [%s] to send group on mission %s [%s]", currM.name, currM.type, Mission.name, Mission.type))
              asset.flightgroup:MissionCancel(currM)
            elseif pause then
              self:T(self.lid..string.format("Pausing current mission %s [%s] to send group on mission %s [%s]", currM.name, currM.type, Mission.name, Mission.type))
              asset.flightgroup:PauseMission()
            end
            
            -- Not reserved any more.
            asset.isReserved=false
          
          end
          
          -- Add asset to mission.
          Mission:AddAsset(asset)
          
          -- Trigger event.
          self:__OpsOnMission(2, asset.flightgroup, Mission)
  
        else
          self:T(self.lid.."ERROR: OPSGROUP for asset does NOT exist but it seems to be SPAWNED (asset.spawned=true)!")
        end
  
      else

        ---
        -- Stock Assets
        ---      
      
        -- These assets need to be requested and spawned.
        table.insert(Assetlist, asset)
        
      end
      
    end
  end

  -- Add request to legion warehouse.
  if #Assetlist>0 then

    --local text=string.format("Requesting assets for mission %s:", Mission.name)
    for i,_asset in pairs(Assetlist) do
      local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem

      -- Set asset to requested! Important so that new requests do not use this asset!
      asset.requested=true
      
      -- Spawned asset are not requested.
      if asset.spawned then
        asset.requested=false
      end
      
      -- Not reserved and more.
      asset.isReserved=false

      -- Set mission task so that the group is spawned with the right one.
      if Mission.missionTask then
        asset.missionTask=Mission.missionTask
      end
      
      -- Set takeoff type to parking for ALERT5 missions. We dont want them to take off without a proper mission if squadron start is hot.
      if Mission.type==AUFTRAG.Type.ALERT5 then
        asset.takeoffType=COORDINATE.WaypointType.TakeOffParking
      end
      
      -- Add asset to mission.
      Mission:AddAsset(asset)

    end
    
    -- Set assignment.
    -- TODO: Get/set functions for assignment string.
    local assignment=string.format("Mission-%d", Mission.auftragsnummer)
    
    --local request=Mission:_GetRequest(self)

    -- Add request to legion warehouse.
    --self:AddRequest(self, WAREHOUSE.Descriptor.ASSETLIST, Assetlist, #Assetlist, nil, nil, Mission.prio, assignment)
    local request=self:_AddRequest(WAREHOUSE.Descriptor.ASSETLIST, Assetlist, #Assetlist, Mission.prio, assignment)
    
    -- Debug Info.
    self:T(self.lid..string.format("Added request=%d for Nasssets=%d", request.uid, #Assetlist))

    -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here.
    --Mission.requestID[self.alias]=self.queueid
    Mission:_SetRequestID(self, self.queueid)
    
    -- Get request.
    --local request=self:GetRequestByID(self.queueid)
    
    -- Debug info.
    self:T(self.lid..string.format("Mission %s [%s] got Request ID=%d", Mission:GetName(), Mission:GetType(), self.queueid))
    
    -- Request ship.
    if request then
      if self:IsShip() then
        self:T(self.lid.."Warehouse physical structure is SHIP. Requestes assets will be late activated!")
        request.lateActivation=true
      end
    end    
    
  end

end

--- On after "TransportAssign" event. Transport is added to a LEGION transport queue and assets are requested from the LEGION warehouse.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.
-- @param #table Legions The legion(s) to which the transport is assigned.
function LEGION:onafterTransportAssign(From, Event, To, Transport, Legions)

  for _,_Legion in pairs(Legions) do
    local Legion=_Legion --Ops.Legion#LEGION

    -- Debug info.
    self:T(self.lid..string.format("Assigning transport %d to legion %s", Transport.uid, Legion.alias))
      
    -- Add mission to legion.
    Legion:AddOpsTransport(Transport)
    
    -- Directly request the mission as the assets have already been selected.
    Legion:TransportRequest(Transport)

  end

end

--- On after "TransportRequest" event. Performs a self request to the warehouse for the transport assets. Sets transport status to REQUESTED.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsTransport#OPSTRANSPORT Opstransport The requested mission.
function LEGION:onafterTransportRequest(From, Event, To, OpsTransport)
  
  -- List of assets that will be requested.
  local AssetList={}
  
  --TODO: Find spawned assets on ALERT 5 mission OPSTRANSPORT.

  --local text=string.format("Requesting assets for mission %s:", Mission.name)
  for i,_asset in pairs(OpsTransport.assets) do
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
    
    -- Check that this asset belongs to this Legion warehouse.
    if asset.wid==self.uid then      

      -- Set asset to requested! Important so that new requests do not use this asset!
      asset.requested=true
      asset.isReserved=false

      -- Set transport mission task.
      asset.missionTask=ENUMS.MissionTask.TRANSPORT
      
      -- Add asset to list.
      table.insert(AssetList, asset)      
    end
  end
  
  if #AssetList>0 then

    -- Set mission status from QUEUED to REQUESTED.
    OpsTransport:Requested()
    
    -- Set legion status. Ensures that it is not considered in the next selection.
    OpsTransport:SetLegionStatus(self, OPSTRANSPORT.Status.REQUESTED)
    
    -- TODO: Get/set functions for assignment string.
    local assignment=string.format("Transport-%d", OpsTransport.uid)

    -- Add request to legion warehouse.
    --self:AddRequest(self, WAREHOUSE.Descriptor.ASSETLIST, AssetList, #AssetList, nil, nil, OpsTransport.prio, assignment)
    self:_AddRequest(WAREHOUSE.Descriptor.ASSETLIST, AssetList, #AssetList, OpsTransport.prio, assignment)

    -- The queueid has been increased in the onafterAddRequest function. So we can simply use it here.
    OpsTransport.requestID[self.alias]=self.queueid
  end

end

--- On after "TransportCancel" event.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport to be cancelled.
function LEGION:onafterTransportCancel(From, Event, To, Transport)

  -- Info message.
  self:T(self.lid..string.format("Cancel transport UID=%d", Transport.uid))

  -- Set status to cancelled.
  Transport:SetLegionStatus(self, OPSTRANSPORT.Status.CANCELLED)

  for i=#Transport.assets, 1, -1 do
    local asset=Transport.assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem
    
    -- Asset should belong to this legion.
    if asset.wid==self.uid then

      local opsgroup=asset.flightgroup

      if opsgroup then
        opsgroup:TransportCancel(Transport)
      end
      
      -- Delete awaited transport.
      local cargos=Transport:GetCargoOpsGroups(false)
      for _,_cargo in pairs(cargos) do
        local cargo=_cargo --Ops.OpsGroup#OPSGROUP
        
        -- Remover my lift.
        cargo:_DelMyLift(Transport)
        
        -- Legion of cargo group
        local legion=cargo.legion
        
        -- Add asset back to legion.
        if legion then                  
          legion:T(self.lid..string.format("Adding cargo group %s back to legion", cargo:GetName()))
          legion:__AddAsset(0.1, cargo.group, 1)
        end
      end
      
      -- Remove asset from mission.
      Transport:DelAsset(asset)

      -- Not requested any more (if it was).
      asset.requested=nil
      asset.isReserved=nil
      
    end      
  end

  -- Remove queued request (if any).
  if Transport.requestID[self.alias] then
    self:_DeleteQueueItemByID(Transport.requestID[self.alias], self.queue)
  end

end


--- On after "MissionCancel" event. Cancels the missions of all flightgroups. Deletes request from warehouse queue.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Auftrag#AUFTRAG Mission The mission to be cancelled.
function LEGION:onafterMissionCancel(From, Event, To, Mission)

  -- Info message.
  self:T(self.lid..string.format("Cancel mission %s", Mission.name))

  -- Set status to cancelled.
  Mission:SetLegionStatus(self, AUFTRAG.Status.CANCELLED)

  for i=#Mission.assets, 1, -1 do
    local asset=Mission.assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem
    
    -- Asset should belong to this legion.
    if asset.wid==self.uid then

      local opsgroup=asset.flightgroup

      if opsgroup then
        opsgroup:MissionCancel(Mission)
      end
      
      -- Remove asset from mission.
      Mission:DelAsset(asset)

      -- Not requested any more (if it was).
      asset.requested=nil
      asset.isReserved=nil
      
    end      
  end

  -- Remove queued request (if any).
  local requestID=Mission:_GetRequestID(self)
  if requestID then
    self:_DeleteQueueItemByID(requestID, self.queue)
  end

end

--- On after "OpsOnMission".
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP OpsGroup Ops group on mission
-- @param Ops.Auftrag#AUFTRAG Mission The requested mission.
function LEGION:onafterOpsOnMission(From, Event, To, OpsGroup, Mission)
  -- Debug info.
  self:T2(self.lid..string.format("Group %s on mission %s [%s]", OpsGroup:GetName(), Mission:GetName(), Mission:GetType()))

  if self:IsAirwing() then
    -- Trigger event for Airwings.
    self:FlightOnMission(OpsGroup, Mission)
  elseif self:IsBrigade() then
    -- Trigger event for Brigades.
    self:ArmyOnMission(OpsGroup, Mission)
  else
    -- Trigger event for Fleets.
    self:NavyOnMission(OpsGroup, Mission)
  end
  
  -- Load group as cargo because it cannot swim! We pause the mission.
  if self:IsBrigade() and self:IsShip() then
    OpsGroup:PauseMission()
    self.warehouseOpsGroup:Load(OpsGroup, self.warehouseOpsElement)
  end
  
  -- Trigger event for chief.
  if self.chief then
    self.chief:OpsOnMission(OpsGroup, Mission)
  end
  
  -- Trigger event for commander.
  if self.commander then
    self.commander:OpsOnMission(OpsGroup, Mission)
  end

end

--- On after "NewAsset" event. Asset is added to the given cohort (asset assignment).
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset that has just been added.
-- @param #string assignment The (optional) assignment for the asset.
function LEGION:onafterNewAsset(From, Event, To, asset, assignment)

  -- Call parent WAREHOUSE function first.
  self:GetParent(self, LEGION).onafterNewAsset(self, From, Event, To, asset, assignment)

  -- Debug text.
  local text=string.format("New asset %s with assignment %s and request assignment %s", asset.spawngroupname, tostring(asset.assignment), tostring(assignment))
  self:T(self.lid..text)

  -- Get cohort.
  local cohort=self:_GetCohort(asset.assignment)

  -- Check if asset is already part of the squadron. If an asset returns, it will be added again! We check that asset.assignment is also assignment.
  if cohort then

    if asset.assignment==assignment then
    
      ---
      -- Asset is added to the COHORT for the first time
      ---

      local nunits=#asset.template.units

      -- Debug text.
      local text=string.format("Adding asset to cohort %s: assignment=%s, type=%s, attribute=%s, nunits=%d ngroup=%s", cohort.name, assignment, asset.unittype, asset.attribute, nunits, tostring(cohort.ngrouping))
      self:T(self.lid..text)

      -- Adjust number of elements in the group.
      if cohort.ngrouping then
        local template=asset.template

        local N=math.max(#template.units, cohort.ngrouping)
        
        -- We need to recalc the total weight and cargo bay.
        asset.weight=0
        asset.cargobaytot=0

        -- Handle units.
        for i=1,N do

          -- Unit template.
          local unit = template.units[i]

          -- If grouping is larger than units present, copy first unit.
          if i>nunits then
            table.insert(template.units, UTILS.DeepCopy(template.units[1]))
            asset.cargobaytot=asset.cargobaytot+asset.cargobay[1]
            asset.weight=asset.weight+asset.weights[1]
            template.units[i].x=template.units[1].x+5*(i-nunits)
            template.units[i].y=template.units[1].y+5*(i-nunits)
          else
            if i<=cohort.ngrouping then
              asset.weight=asset.weight+asset.weights[i]
              asset.cargobaytot=asset.cargobaytot+asset.cargobay[i]
            end
          end

          -- Remove units if original template contains more than in grouping.
          if i>cohort.ngrouping then
            template.units[i]=nil
          end
        end

        -- Set number of units.
        asset.nunits=cohort.ngrouping
        
        -- Debug info.
        self:T(self.lid..string.format("After regrouping: Nunits=%d, weight=%.1f cargobaytot=%.1f kg", #asset.template.units, asset.weight, asset.cargobaytot))
      end

      -- Set takeoff type.
      asset.takeoffType=cohort.takeoffType~=nil and cohort.takeoffType or self.takeoffType
      
      -- Set parking IDs.
      asset.parkingIDs=cohort.parkingIDs

      -- Create callsign and modex (needs to be after grouping).
      cohort:GetCallsign(asset)
      cohort:GetModex(asset)

      -- Set spawn group name. This has to include "AID-" for warehouse.
      asset.spawngroupname=string.format("%s_AID-%d", cohort.name, asset.uid)

      -- Add asset to cohort.
      cohort:AddAsset(asset)

    else

      ---
      -- Asset is returned to the COHORT
      ---
      
      self:T(self.lid..string.format("Asset returned to legion ==> calling LegionAssetReturned event"))
      
      -- Set takeoff type in case it was overwritten for an ALERT5 mission.
      asset.takeoffType=cohort.takeoffType
      
      -- Trigger event.
      self:LegionAssetReturned(cohort, asset)

    end

  end
end

--- On after "LegionAssetReturned" event. Triggered when an asset group returned to its legion.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Cohort#COHORT Cohort The cohort the asset belongs to.
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset that returned.
function LEGION:onafterLegionAssetReturned(From, Event, To, Cohort, Asset)
  -- Debug message.
  self:T(self.lid..string.format("Asset %s from Cohort %s returned! asset.assignment=\"%s\"", Asset.spawngroupname, Cohort.name, tostring(Asset.assignment)))

  -- Stop flightgroup.
  if Asset.flightgroup and not Asset.flightgroup:IsStopped() then
    Asset.flightgroup:Stop()
  end

  -- Return payload.
  if Asset.flightgroup:IsFlightgroup() then
    self:ReturnPayloadFromAsset(Asset)
  end

  -- Return tacan channel.
  if Asset.tacan then
    Cohort:ReturnTacan(Asset.tacan)
  end

  -- Set timestamp.
  Asset.Treturned=timer.getAbsTime()
  
end


--- On after "AssetSpawned" event triggered when an asset group is spawned into the cruel world.
-- Creates a new flightgroup element and adds the mission to the flightgroup queue.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Group#GROUP group The group spawned.
-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset that was spawned.
-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset.
function LEGION:onafterAssetSpawned(From, Event, To, group, asset, request)
  self:T({From, Event, To, group:GetName(), asset.assignment, request.assignment})
  
  -- Call parent warehouse function first.
  self:GetParent(self, LEGION).onafterAssetSpawned(self, From, Event, To, group, asset, request)

  -- Get the COHORT of the asset.
  local cohort=self:_GetCohortOfAsset(asset)

  -- Check if we have a cohort or if this was some other request.
  if cohort then
  
    -- Debug info.
    self:T(self.lid..string.format("Cohort asset spawned %s", asset.spawngroupname))

    -- Create a flight group.
    local flightgroup=self:_CreateFlightGroup(asset)

    ---
    -- Asset
    ---

    -- Set asset flightgroup.
    asset.flightgroup=flightgroup

    -- Not requested any more.
    asset.requested=nil

    -- Did not return yet.
    asset.Treturned=nil


    ---
    -- Cohort
    ---

    -- Get TACAN channel.
    local Tacan=cohort:FetchTacan()
    if Tacan then
      asset.tacan=Tacan
      flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band)
    end

    -- Set radio frequency and modulation
    local radioFreq, radioModu=cohort:GetRadio()
    if radioFreq then
      flightgroup:SwitchRadio(radioFreq, radioModu)
    end

    if cohort.fuellow then
      flightgroup:SetFuelLowThreshold(cohort.fuellow)
    end

    if cohort.fuellowRefuel then
      flightgroup:SetFuelLowRefuel(cohort.fuellowRefuel)
    end
    
    -- Assignment.
    local assignment=request.assignment
    
    -- Set pathfinding for naval groups.
    if self:IsFleet() then
      flightgroup:SetPathfinding(self.pathfinding)
    end
    
    if string.find(assignment, "Mission-") then

      ---
      -- Mission
      ---
      
      local uid=UTILS.Split(assignment, "-")[2]
  
      -- Get Mission (if any).
      local mission=self:GetMissionByID(uid)
      
      local despawnLanding=cohort.despawnAfterLanding~=nil and cohort.despawnAfterLanding or self.despawnAfterLanding
      if despawnLanding then
        flightgroup:SetDespawnAfterLanding()
      end
      
      local despawnHolding=cohort.despawnAfterHolding~=nil and cohort.despawnAfterHolding or self.despawnAfterHolding
      if despawnHolding then
        flightgroup:SetDespawnAfterHolding()
      end      
  
      -- Add mission to flightgroup queue.
      if mission then
  
        if Tacan then
          --mission:SetTACAN(Tacan, Morse, UnitName, Band)
        end
        
        -- Add mission to flightgroup queue. If mission has an OPSTRANSPORT attached, all added OPSGROUPS are added as CARGO for a transport.
        flightgroup:AddMission(mission)
        
        -- RTZ on out of ammo.
        if self:IsBrigade() or self:IsFleet() then
          flightgroup:SetReturnOnOutOfAmmo()
        end
                  
        -- Trigger event.
        self:__OpsOnMission(5, flightgroup, mission)
  
      else
  
        if Tacan then
          --flightgroup:SwitchTACAN(Tacan, Morse, UnitName, Band)
        end
  
      end
  
      -- Add group to the detection set of the CHIEF (INTEL).
      local chief=self.chief or (self.commander and self.commander.chief or nil) --Ops.Chief#CHIEF
      if chief then
        self:T(self.lid..string.format("Adding group %s to agents of CHIEF", group:GetName()))
        chief.detectionset:AddGroup(asset.flightgroup.group)
      end
      
    elseif string.find(assignment, "Transport-") then
    
      ---
      -- Transport
      ---
      
      local uid=UTILS.Split(assignment, "-")[2] 

            -- Get Mission (if any).
      local transport=self:GetTransportByID(uid)
  
      -- Add mission to flightgroup queue.
      if transport then
        flightgroup:AddOpsTransport(transport)
      end
  
    end
    
  end
  
end

--- On after "AssetDead" event triggered when an asset group died.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset that is dead.
-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request The request of the dead asset.
function LEGION:onafterAssetDead(From, Event, To, asset, request)

  -- Call parent warehouse function first.
  self:GetParent(self, LEGION).onafterAssetDead(self, From, Event, To, asset, request)
  
  -- Remove group from the detection set of the CHIEF (INTEL).
  if self.commander and self.commander.chief then
    self.commander.chief.detectionset:RemoveGroupsByName({asset.spawngroupname})
  end
  
  -- Remove asset from cohort and legion.
  self:DelAsset(asset)

  -- Remove asset from mission is done via Mission:AssetDead() call from flightgroup onafterFlightDead function
  -- Remove asset from squadron same
end

--- On after "Destroyed" event. Remove assets from cohorts. Stop cohorts.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function LEGION:onafterDestroyed(From, Event, To)

  -- Debug message.
  self:T(self.lid.."Legion warehouse destroyed!")

  -- Cancel all missions.
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG
    mission:Cancel()
  end

  -- Remove all cohort assets.
  for _,_cohort in pairs(self.cohorts) do
    local cohort=_cohort --Ops.Cohort#COHORT
    -- Stop Cohort. This also removes all assets.
    cohort:Stop()
  end

  -- Call parent warehouse function first.
  self:GetParent(self, LEGION).onafterDestroyed(self, From, Event, To)

end


--- On after "Request" event.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Functional.Warehouse#WAREHOUSE.Queueitem Request Information table of the request.
function LEGION:onafterRequest(From, Event, To, Request)

  if Request.toself then

    -- Assets
    local assets=Request.cargoassets
  
    -- Get Mission
    local Mission=self:GetMissionByID(Request.assignment)
  
    if Mission and assets then
  
      for _,_asset in pairs(assets) do
        local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
        -- This would be the place to modify the asset table before the asset is spawned.
      end
  
    end
    
  end

  -- Call parent warehouse function after assets have been adjusted.
  self:GetParent(self, LEGION).onafterRequest(self, From, Event, To, Request)

end

--- On after "SelfRequest" event.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Set#SET_GROUP groupset The set of asset groups that was delivered to the warehouse itself.
-- @param Functional.Warehouse#WAREHOUSE.Pendingitem request Pending self request.
function LEGION:onafterSelfRequest(From, Event, To, groupset, request)

  -- Call parent warehouse function first.
  self:GetParent(self, LEGION).onafterSelfRequest(self, From, Event, To, groupset, request)

  -- Get Mission
  local mission=self:GetMissionByID(request.assignment)

  for _,_asset in pairs(request.assets) do
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
  end

  for _,_group in pairs(groupset:GetSet()) do
    local group=_group --Wrapper.Group#GROUP
  end

end

--- On after "RequestSpawned" event.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Functional.Warehouse#WAREHOUSE.Pendingitem Request Information table of the request.
-- @param Core.Set#SET_GROUP CargoGroupSet Set of cargo groups.
-- @param Core.Set#SET_GROUP TransportGroupSet Set of transport groups if any.
function LEGION:onafterRequestSpawned(From, Event, To, Request, CargoGroupSet, TransportGroupSet)

  -- Call parent warehouse function.
  self:GetParent(self, LEGION).onafterRequestSpawned(self, From, Event, To, Request, CargoGroupSet, TransportGroupSet)

end

--- On after "Captured" event.
-- @param #LEGION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param DCS#coalition.side Coalition which captured the warehouse.
-- @param DCS#country.id Country which has captured the warehouse.
function LEGION:onafterCaptured(From, Event, To, Coalition, Country)

  -- Call parent warehouse function.
  self:GetParent(self, LEGION).onafterCaptured(self, From, Event, To, Coalition, Country)
  
  
  if self.chief then
    -- Trigger event for chief and commander.
    self.chief.commander:LegionLost(self, Coalition, Country)
    self.chief:LegionLost(self, Coalition, Country)
    -- Remove legion from chief and commander.
    self.chief:RemoveLegion(self)    
  elseif self.commander then
    -- Trigger event.
    self.commander:LegionLost(self, Coalition,Country)
    -- Remove legion from commander.
    self.commander:RemoveLegion(self)
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Mission Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new OPS group after an asset was spawned.
-- @param #LEGION self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset.
-- @return Ops.FlightGroup#FLIGHTGROUP The created flightgroup object.
function LEGION:_CreateFlightGroup(asset)

  -- Create flightgroup.
  local opsgroup=nil --Ops.OpsGroup#OPSGROUP  
  
  if self:IsAirwing() then
  
    ---
    -- FLIGHTGROUP
    ---
  
    opsgroup=FLIGHTGROUP:New(asset.spawngroupname)
    
  elseif self:IsBrigade() then
  
    ---
    -- ARMYGROUP
    ---  
  
    opsgroup=ARMYGROUP:New(asset.spawngroupname)
    opsgroup:SetValidateAndRepositionGroundUnits(self.ValidateAndRepositionGroundUnits)
    
  elseif self:IsFleet() then

    ---
    -- NAVYGROUP
    ---  
  
    opsgroup=NAVYGROUP:New(asset.spawngroupname)
    
  else
    self:E(self.lid.."ERROR: not airwing or brigade!")
  end

  -- Set legion.
  opsgroup:_SetLegion(self)

  -- Set cohort.
  opsgroup.cohort=self:_GetCohortOfAsset(asset)

  -- Set home base.
  opsgroup.homebase=self.airbase
  
  -- Set destination base
  opsgroup.destbase=self.airbase
  
  -- Set home zone.
  opsgroup.homezone=self.spawnzone  

  -- Set weapon data.
  if opsgroup.cohort.weaponData then
    local text="Weapon data for group:"
    opsgroup.weaponData=opsgroup.weaponData or {}
    for bittype,_weapondata in pairs(opsgroup.cohort.weaponData) do
      local weapondata=_weapondata --Ops.OpsGroup#OPSGROUP.WeaponData
      opsgroup.weaponData[bittype]=UTILS.DeepCopy(weapondata) -- Careful with the units.
      text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km", bittype, weapondata.RangeMin/1000, weapondata.RangeMax/1000)
    end
    self:T3(self.lid..text)
  end      

  return opsgroup
end

--- Display tactical overview.
-- @param #LEGION self 
function LEGION:_TacticalOverview()

  if self.tacview then

    local NassetsTotal=self:CountAssets(nil)
    local NassetsStock=self:CountAssets(true)
    local NassetsActiv=self:CountAssets(false)
  
    local NmissionsTotal=#self.missionqueue
    local NmissionsRunni=self:CountMissionsInQueue()
  
    -- Info message
    local text=string.format("Tactical Overview %s\n", self.alias)
    text=text..string.format("===================================\n")
  
    -- Asset info.
    text=text..string.format("Assets: %d [Active=%d, Stock=%d]\n", NassetsTotal, NassetsActiv, NassetsStock)
  
    -- Mission info.
    text=text..string.format("Missions: %d [Running=%d]\n", NmissionsTotal, NmissionsRunni)
    for _,mtype in pairs(AUFTRAG.Type) do
      local n=self:CountMissionsInQueue(mtype)
      if n>0 then
        local N=self:CountMissionsInQueue(mtype)
        text=text..string.format("  - %s: %d [Running=%d]\n", mtype, n, N)
      end
    end
  
    
    local Ntransports=#self.transportqueue
    if Ntransports>0 then
      text=text..string.format("Transports: %d\n", Ntransports)
      for _,_transport in pairs(self.transportqueue) do
        local transport=_transport --Ops.OpsTransport#OPSTRANSPORT
        text=text..string.format(" - %s", transport:GetState())
      end
    end
  
    -- Message to coalition.
    MESSAGE:New(text, 60, nil, true):ToCoalition(self:GetCoalition())  
  
    
  end
  
end

--- Check if an asset is currently on a mission (STARTED or EXECUTING).
-- @param #LEGION self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset.
-- @param #table MissionTypes Types on mission to be checked. Default all.
-- @return #boolean If true, asset has at least one mission of that type in the queue.
function LEGION:IsAssetOnMission(asset, MissionTypes)

  if MissionTypes then
    if type(MissionTypes)~="table" then
      MissionTypes={MissionTypes}
    end
  else
    -- Check all possible types.
    MissionTypes=AUFTRAG.Type
  end

  if asset.flightgroup and asset.flightgroup:IsAlive() then

    -- Loop over mission queue.
    for _,_mission in pairs(asset.flightgroup.missionqueue or {}) do
      local mission=_mission --Ops.Auftrag#AUFTRAG

      if mission:IsNotOver() then

        -- Get flight status.
        local status=mission:GetGroupStatus(asset.flightgroup)

        -- Only if mission is started or executing.
        if (status==AUFTRAG.GroupStatus.STARTED or status==AUFTRAG.GroupStatus.EXECUTING) and AUFTRAG.CheckMissionType(mission.type, MissionTypes) then
          return true
        end

      end

    end

  end
  
  return false
end

--- Get the current mission of the asset.
-- @param #LEGION self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The asset.
-- @return Ops.Auftrag#AUFTRAG Current mission or *nil*.
function LEGION:GetAssetCurrentMission(asset)

  if asset.flightgroup then
    return asset.flightgroup:GetMissionCurrent()
  end

  return nil
end

--- Count payloads in stock.
-- @param #LEGION self
-- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`.
-- @param #table UnitTypes Types of units.
-- @param #table Payloads Specific payloads to be counted only.
-- @return #number Count of available payloads in stock.
function LEGION:CountPayloadsInStock(MissionTypes, UnitTypes, Payloads)

  if MissionTypes then
    if type(MissionTypes)=="string" then
      MissionTypes={MissionTypes}
    end
  end

  if UnitTypes then
    if type(UnitTypes)=="string" then
      UnitTypes={UnitTypes}
    end
  end

  local function _checkUnitTypes(payload)
    if UnitTypes then
      for _,unittype in pairs(UnitTypes) do
        if unittype==payload.aircrafttype then
          return true
        end
      end
    else
      -- Unit type was not specified.
      return true
    end
    return false
  end

  local function _checkPayloads(payload)
    if Payloads then
      for _,Payload in pairs(Payloads) do
        if Payload.uid==payload.uid then
          return true
        end
      end
    else
      -- Payload was not specified.
      return nil
    end
    return false
  end

  local n=0
  for _,_payload in pairs(self.payloads or {}) do
    local payload=_payload --Ops.Airwing#AIRWING.Payload

    for _,MissionType in pairs(MissionTypes) do

      local specialpayload=_checkPayloads(payload)
      local compatible=AUFTRAG.CheckMissionCapability(MissionType, payload.capabilities)

      local goforit = specialpayload or (specialpayload==nil and compatible)

      if goforit and _checkUnitTypes(payload) then

        if payload.unlimited then
          -- Payload is unlimited. Return a BIG number.
          return 999
        else
          n=n+payload.navail
        end

      end

    end
  end

  return n
end

--- Count missions in mission queue.
-- @param #LEGION self
-- @param #table MissionTypes Types on mission to be checked. Default *all* possible types `AUFTRAG.Type`.
-- @return #number Number of missions that are not over yet.
function LEGION:CountMissionsInQueue(MissionTypes)

  MissionTypes=MissionTypes or AUFTRAG.Type

  local N=0
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    -- Check if this mission type is requested.
    if mission:IsNotOver() and AUFTRAG.CheckMissionType(mission.type, MissionTypes) then
      N=N+1
    end

  end

  return N
end

--- Count total number of assets of the legion.
-- @param #LEGION self
-- @param #boolean InStock If `true`, only assets that are in the warehouse stock/inventory are counted. If `false`, only assets that are NOT in stock (i.e. spawned) are counted. If `nil`, all assets are counted.
-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types.
-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `GROUP.Attribute.AIR_BOMBER`.
-- @return #number Amount of asset groups in stock.
function LEGION:CountAssets(InStock, MissionTypes, Attributes)

  local N=0

  for _,_cohort in pairs(self.cohorts) do
    local cohort=_cohort --Ops.Cohort#COHORT
    N=N+cohort:CountAssets(InStock, MissionTypes, Attributes)
  end

  return N
end

--- Get OPSGROUPs that are spawned and alive.
-- @param #LEGION self
-- @param #table MissionTypes (Optional) Get only assest that can perform certain mission type(s). Default is all types.
-- @param #table Attributes (Optional) Get only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`.
-- @return Core.Set#SET_OPSGROUP The set of OPSGROUPs. Can be empty if no groups are spawned or alive!
function LEGION:GetOpsGroups(MissionTypes, Attributes)

  local setLegion=SET_OPSGROUP:New()

  for _,_cohort in pairs(self.cohorts) do
    local cohort=_cohort --Ops.Cohort#COHORT
    
    -- Get cohort set.
    local setCohort=cohort:GetOpsGroups(MissionTypes, Attributes)
    
    -- Debug info.
    self:T2(self.lid..string.format("Found %d opsgroups of cohort %s", setCohort:Count(), cohort.name))
    
    -- Add to legion set.
    setLegion:AddSet(setCohort)
  end

  return setLegion
end

--- Count total number of assets in LEGION warehouse stock that also have a payload.
-- @param #LEGION self
-- @param #boolean Payloads (Optional) Specifc payloads to consider. Default all.
-- @param #table MissionTypes (Optional) Count only assest that can perform certain mission type(s). Default is all types.
-- @param #table Attributes (Optional) Count only assest that have a certain attribute(s), e.g. `WAREHOUSE.Attribute.AIR_BOMBER`.
-- @return #number Amount of asset groups in stock.
function LEGION:CountAssetsWithPayloadsInStock(Payloads, MissionTypes, Attributes)

  -- Total number counted.
  local N=0
  
  -- Number of payloads in stock per aircraft type.
  local Npayloads={}
  
  -- First get payloads for aircraft types of squadrons.
  for _,_cohort in pairs(self.cohorts) do
    local cohort=_cohort --Ops.Cohort#COHORT
    if Npayloads[cohort.aircrafttype]==nil then    
      Npayloads[cohort.aircrafttype]=self:CountPayloadsInStock(MissionTypes, cohort.aircrafttype, Payloads)
      self:T3(self.lid..string.format("Got Npayloads=%d for type=%s",Npayloads[cohort.aircrafttype], cohort.aircrafttype))
    end
  end

  for _,_cohort in pairs(self.cohorts) do
    local cohort=_cohort --Ops.Cohort#COHORT
    
    -- Number of assets in stock.
    local n=cohort:CountAssets(true, MissionTypes, Attributes)
    
    -- Number of payloads.
    local p=Npayloads[cohort.aircrafttype] or 0
    
    -- Only the smaller number of assets or paylods is really available.        
    local m=math.min(n, p)
    
    -- Add up what we have. Could also be zero.
    N=N+m
    
    -- Reduce number of available payloads.
    Npayloads[cohort.aircrafttype]=Npayloads[cohort.aircrafttype]-m
  end

  return N
end

--- Count assets on mission.
-- @param #LEGION self
-- @param #table MissionTypes Types on mission to be checked. Default all.
-- @param Ops.Cohort#COHORT Cohort Only count assets of this cohort. Default count assets of all cohorts.
-- @return #number Number of pending and queued assets.
-- @return #number Number of pending assets.
-- @return #number Number of queued assets.
function LEGION:CountAssetsOnMission(MissionTypes, Cohort)

  local Nq=0
  local Np=0

  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    -- Check if this mission type is requested.
    if AUFTRAG.CheckMissionType(mission.type, MissionTypes or AUFTRAG.Type) then

      for _,_asset in pairs(mission.assets or {}) do
        local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
        
        -- Ensure asset belongs to this letion.
        if asset.wid==self.uid then

          if Cohort==nil or Cohort.name==asset.squadname then
  
            local request, isqueued=self:GetRequestByID(mission.requestID[self.alias])
  
            if isqueued then
              Nq=Nq+1
            else
              Np=Np+1
            end
  
          end
          
        end
      end
    end
  end

  return Np+Nq, Np, Nq
end

--- Get assets on mission.
-- @param #LEGION self
-- @param #table MissionTypes Types on mission to be checked. Default all.
-- @return #table Assets on pending requests.
function LEGION:GetAssetsOnMission(MissionTypes)

  local assets={}
  local Np=0

  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    -- Check if this mission type is requested.
    if AUFTRAG.CheckMissionType(mission.type, MissionTypes) then

      for _,_asset in pairs(mission.assets or {}) do
        local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
        
        -- Ensure asset belongs to this legion.
        if asset.wid==self.uid then

          table.insert(assets, asset)
          
        end

      end
    end
  end

  return assets
end

--- Get the unit types of this legion. These are the unit types of all assigned cohorts.
-- @param #LEGION self
-- @param #boolean onlyactive Count only the active ones.
-- @param #table cohorts Table of cohorts. Default all.
-- @return #table Table of unit types.
function LEGION:GetAircraftTypes(onlyactive, cohorts)

  -- Get all unit types that can do the job.
  local unittypes={}

  -- Loop over all cohorts.
  for _,_cohort in pairs(cohorts or self.cohorts) do
    local cohort=_cohort --Ops.Cohort#COHORT

    if (not onlyactive) or cohort:IsOnDuty() then

      local gotit=false
      for _,unittype in pairs(unittypes) do
        if cohort.aircrafttype==unittype then
          gotit=true
          break
        end
      end
      if not gotit then
        table.insert(unittypes, cohort.aircrafttype)
      end

    end
  end

  return unittypes
end

--- Count payloads of all cohorts for all unit types.
-- @param #LEGION self
-- @param #string MissionType Mission type.
-- @param #table Cohorts Cohorts included.
-- @param #table Payloads (Optional) Special payloads.
-- @return #table Table of payloads for each unit type.
function LEGION:_CountPayloads(MissionType, Cohorts, Payloads)

  -- Number of payloads in stock per aircraft type.
  local Npayloads={}

  -- First get payloads for aircraft types of squadrons.
  for _,_cohort in pairs(Cohorts) do
    local cohort=_cohort --Ops.Cohort#COHORT
    
    -- We only need that element once.
    if Npayloads[cohort.aircrafttype]==nil then
      
      -- Count number of payloads in stock for the cohort aircraft type.
      Npayloads[cohort.aircrafttype]=cohort.legion:IsAirwing() and self:CountPayloadsInStock(MissionType, cohort.aircrafttype, Payloads) or 999
      
      -- Debug info.
      self:T2(self.lid..string.format("Got N=%d payloads for mission type=%s and unit type=%s", Npayloads[cohort.aircrafttype], MissionType, cohort.aircrafttype))
    end
  end

  return Npayloads
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Recruiting Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Recruit assets for a given mission.
-- @param #LEGION self
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
-- @return #boolean If `true` enough assets could be recruited.
-- @return #table Recruited assets.
-- @return #table Legions of recruited assets.
function LEGION:RecruitAssetsForMission(Mission)

  -- Get required assets.
  local NreqMin, NreqMax=Mission:GetRequiredAssets()
  
  -- Target position vector.
  local TargetVec2=Mission:GetTargetVec2()
  
  -- Payloads.
  local Payloads=Mission.payloads
  
  -- Largest cargo bay available of available carrier assets if mission assets need to be transported.
  local MaxWeight=nil
  
  if Mission.NcarriersMin then
  
    local legions={self}
    local cohorts=self.cohorts
    if Mission.transportLegions or Mission.transportCohorts then
      legions=Mission.transportLegions
      cohorts=Mission.transportCohorts
    end
      
    -- Get transport cohorts.
    local Cohorts=LEGION._GetCohorts(legions, cohorts)
    
    -- Filter cohorts that can actually perform transport missions.    
    local transportcohorts={}
    for _,_cohort in pairs(Cohorts) do
      local cohort=_cohort --Ops.Cohort#COHORT
      
      -- Check if cohort can perform transport to target.
      local can=LEGION._CohortCan(cohort, AUFTRAG.Type.OPSTRANSPORT, Mission.carrierCategories, Mission.carrierAttributes, Mission.carrierProperties, nil, TargetVec2)
      
      -- MaxWeight of cargo assets is limited by the largets available cargo bay. We don't want to select, e.g., tanks that cannot be transported by APCs or helos.
      if can and (MaxWeight==nil or cohort.cargobayLimit>MaxWeight) then
        MaxWeight=cohort.cargobayLimit
      end
    end
    
    self:T(self.lid..string.format("Largest cargo bay available=%.1f", MaxWeight or 0))
  end
  
  
  local legions={self}
  local cohorts=self.cohorts
  if Mission.specialLegions or Mission.specialCohorts then
    legions=Mission.specialLegions
    cohorts=Mission.specialCohorts
  end  

  -- Get cohorts.  
  local Cohorts=LEGION._GetCohorts(legions, cohorts, Operation, OpsQueue)

  -- Recuit assets.
  local recruited, assets, legions=LEGION.RecruitCohortAssets(Cohorts, Mission.type, Mission.alert5MissionType, NreqMin, NreqMax, TargetVec2, Payloads, 
  Mission.engageRange, Mission.refuelSystem, nil, nil, MaxWeight, nil, Mission.attributes, Mission.properties, {Mission.engageWeaponType})

  return recruited, assets, legions
end

--- Recruit assets for a given OPS transport.
-- @param #LEGION self
-- @param Ops.OpsTransport#OPSTRANSPORT Transport The OPS transport.
-- @return #boolean If `true`, enough assets could be recruited.
-- @return #table assets Recruited assets.
-- @return #table legions Legions of recruited assets.
function LEGION:RecruitAssetsForTransport(Transport)

  -- Get all undelivered cargo ops groups.
  local cargoOpsGroups=Transport:GetCargoOpsGroups(false)
  
  local weightGroup=0
  local TotalWeight=nil
  
  -- At least one group should be spawned.
  if #cargoOpsGroups>0 then
  
    -- Calculate the max weight so we know which cohorts can provide carriers.
    TotalWeight=0
    for _,_opsgroup in pairs(cargoOpsGroups) do
      local opsgroup=_opsgroup --Ops.OpsGroup#OPSGROUP
      local weight=opsgroup:GetWeightTotal()
      if weight>weightGroup then
        weightGroup=weight
      end
      TotalWeight=TotalWeight+weight
    end
  else
    -- No cargo groups!
    return false
  end


  -- TODO: Special transport cohorts/legions.

  -- Target is the deploy zone.
  local TargetVec2=Transport:GetDeployZone():GetVec2()
  
  -- Number of required carriers.
  local NreqMin,NreqMax=Transport:GetRequiredCarriers()
  

  -- Recruit assets and legions.
  local recruited, assets, legions=LEGION.RecruitCohortAssets(self.cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NreqMin, NreqMax, TargetVec2, nil, nil, nil, weightGroup, TotalWeight)

  return recruited, assets, legions  
end

--- Recruit assets performing an escort mission for a given asset.
-- @param #LEGION self
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
-- @param #table Assets Table of assets.
-- @return #boolean If `true`, enough assets could be recruited or no escort was required in the first place.
function LEGION:RecruitAssetsForEscort(Mission, Assets)

  -- Is an escort requested in the first place?
  if Mission.NescortMin and Mission.NescortMax and (Mission.NescortMin>0 or Mission.NescortMax>0) then
  
    -- Debug info.
    self:T(self.lid..string.format("Requested escort for mission %s [%s]. Required assets=%d-%d", Mission:GetName(), Mission:GetType(), Mission.NescortMin,Mission.NescortMax))
    
    -- Get special escort legions and/or cohorts.
    local Cohorts={}
    for _,_legion in pairs(Mission.escortLegions or {}) do
      local legion=_legion --Ops.Legion#LEGION
      for _,_cohort in pairs(legion.cohorts) do
        local cohort=_cohort --Ops.Cohort#COHORT
        table.insert(Cohorts, cohort)
      end
    end
    for _,_cohort in pairs(Mission.escortCohorts or {}) do
      local cohort=_cohort --Ops.Cohort#COHORT
      table.insert(Cohorts, cohort)
    end

    -- No escort cohorts/legions given ==> take own cohorts.    
    if #Cohorts==0 then
      Cohorts=self.cohorts
    end    
    
    -- Call LEGION function but provide COMMANDER as self.
    local assigned=LEGION.AssignAssetsForEscort(self, Cohorts, Assets, Mission.NescortMin, Mission.NescortMax, Mission.escortMissionType, Mission.escortTargetTypes)
    
    return assigned
  end

  return true
end

--- Get cohorts.
-- @param #table Legions Special legions.
-- @param #table Cohorts Special cohorts.
-- @param Ops.Operation#OPERATION Operation Operation.
-- @param #table OpsQueue Queue of operations.
-- @return #table Cohorts.
function LEGION._GetCohorts(Legions, Cohorts, Operation, OpsQueue)

  OpsQueue=OpsQueue or {}
  
  --- Function that check if a legion or cohort is part of an operation.
  local function CheckOperation(LegionOrCohort)
    -- No operations ==> no problem!
    if #OpsQueue==0 then
      return true
    end
    
    -- Cohort is not dedicated to a running(!) operation. We assume so.
    local isAvail=true
    
    -- Only available...
    if Operation then
      isAvail=false
    end
        
    for _,_operation in pairs(OpsQueue) do
      local operation=_operation --Ops.Operation#OPERATION
      
      -- Legion is assigned to this operation.
      local isOps=operation:IsAssignedCohortOrLegion(LegionOrCohort)
      
      if isOps and operation:IsRunning() then
        
        -- Is dedicated.
        isAvail=false
      
        if Operation==nil then
          -- No Operation given and this is dedicated to at least one operation.
          return false
        else
          if Operation.uid==operation.uid then
            -- Operation given and is part of it.
            return true
          end        
        end
      end
    end
    
    return isAvail
  end    

  -- Chosen cohorts.
  local cohorts={}
  
  -- Check if there are any special legions and/or cohorts.
  if (Legions and #Legions>0) or (Cohorts and #Cohorts>0) then
  
    -- Add cohorts of special legions.
    for _,_legion in pairs(Legions or {}) do
      local legion=_legion --Ops.Legion#LEGION
  
      -- Check that runway is operational.
      local Runway=true
      if legion:IsAirwing() then
        Runway=legion:IsRunwayOperational() and legion.airbase and legion.airbase:GetCoalition() == legion:GetCoalition()
      end

      -- Legion has to be running.
      if legion:IsRunning() and Runway then
      
        -- Add cohorts of legion.
        for _,_cohort in pairs(legion.cohorts) do
          local cohort=_cohort --Ops.Cohort#COHORT
          if (CheckOperation(cohort.legion) or CheckOperation(cohort)) and not UTILS.IsInTable(cohorts, cohort, "name") then
            table.insert(cohorts, cohort)
          end
        end
        
      end
    end
   
    -- Add special cohorts.
    for _,_cohort in pairs(Cohorts or {}) do
      local cohort=_cohort --Ops.Cohort#COHORT
      if CheckOperation(cohort) and not UTILS.IsInTable(cohorts, cohort, "name") then
        table.insert(cohorts, cohort)
      end
    end

  end

  return cohorts
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Recruiting and Optimization Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Recruit assets from Cohorts for the given parameters. **NOTE** that we set the `asset.isReserved=true` flag so it cant be recruited by anyone else.
-- @param Ops.Cohort#COHORT Cohort The Cohort.
-- @param #string MissionType Misson type(s).
-- @param #table Categories Group categories.
-- @param #table Attributes Group attributes. See `GROUP.Attribute.`
-- @param #table Properties DCS attributes.
-- @param #table WeaponTypes Bit of weapon types.
-- @param DCS#Vec2 TargetVec2 Target position.
-- @param RangeMax Max range in meters.
-- @param #number RefuelSystem Refueling system (boom or probe).
-- @param #number CargoWeight Cargo weight [kg]. This checks the cargo bay of the cohort assets and ensures that it is large enough to carry the given cargo weight.
-- @param #number MaxWeight Max weight [kg]. This checks whether the cohort asset group is not too heavy.
-- @return #boolean Returns `true` if given cohort can meet all requirements.
function LEGION._CohortCan(Cohort, MissionType, Categories, Attributes, Properties, WeaponTypes, TargetVec2, RangeMax, RefuelSystem, CargoWeight, MaxWeight)

  --- Function to check category.
  local function CheckCategory(_cohort)
    local cohort=_cohort --Ops.Cohort#COHORT
    if Categories and #Categories>0 then
      for _,category in pairs(Categories) do
        if category==cohort.category then
          return true
        end
      end
    else
      return true
    end
  end
  
  --- Function to check attribute.
  local function CheckAttribute(_cohort)
    local cohort=_cohort --Ops.Cohort#COHORT
    if Attributes and #Attributes>0 then
      for _,attribute in pairs(Attributes) do
        if attribute==cohort.attribute then
          return true
        end
      end
    else
      return true
    end
  end
  
  --- Function to check property.
  local function CheckProperty(_cohort)
    local cohort=_cohort --Ops.Cohort#COHORT
    if Properties and #Properties>0 then
      for _,Property in pairs(Properties) do
        for property,value in pairs(cohort.properties) do
          if Property==property then
            return true
          end
        end
      end
    else
      return true
    end
  end
  
  --- Function to check weapon type.
  local function CheckWeapon(_cohort)
    local cohort=_cohort --Ops.Cohort#COHORT
    if WeaponTypes and #WeaponTypes>0 then
      for _,WeaponType in pairs(WeaponTypes) do
        if WeaponType==ENUMS.WeaponFlag.Auto then
          return true
        else
          for _,_weaponData in pairs(cohort.weaponData or {}) do
            local weaponData=_weaponData --Ops.OpsGroup#OPSGROUP.WeaponData
            if weaponData.BitType==WeaponType then
              return true
            end
          end
        end
      end
      return false
    else
      return true
    end
  end

  --- Function to check range.
  local function CheckRange(_cohort)
    local cohort=_cohort --Ops.Cohort#COHORT
    
    -- Distance to target.
    local TargetDistance=TargetVec2 and UTILS.VecDist2D(TargetVec2, cohort.legion:GetVec2()) or 0
 
    -- Is in range?
    local Rmax=cohort:GetMissionRange(WeaponTypes)
    local RangeMax = RangeMax or 0    
    local InRange=(RangeMax and math.max(RangeMax, Rmax) or Rmax) >= TargetDistance
    
    --env.info(string.format("Range TargetDist=%.1f Rmax=%.1f RangeMax=%.1f InRange=%s", TargetDistance, Rmax, RangeMax, tostring(InRange)))
    
    return InRange    
  end


  --- Function to check weapon type.
  local function CheckRefueling(_cohort)
    local cohort=_cohort --Ops.Cohort#COHORT
    
    -- Has the requested refuelsystem?
    --local Refuel=RefuelSystem~=nil and (RefuelSystem==cohort.tankerSystem) or true
    
    -- STRANGE: Why did the above line did not give the same result?! Above Refuel is always true!
    if RefuelSystem then
      if cohort.tankerSystem then
        return RefuelSystem==cohort.tankerSystem
      else
        return false
      end
    else
      return true
    end    
  end

  --- Function to check cargo weight.
  local function CheckCargoWeight(_cohort)
    local cohort=_cohort --Ops.Cohort#COHORT
    if CargoWeight~=nil then
      return cohort.cargobayLimit>=CargoWeight
    else
      return true
    end    
  end

  --- Function to check cargo weight.
  local function CheckMaxWeight(_cohort)
    local cohort=_cohort --Ops.Cohort#COHORT
    if MaxWeight~=nil then
      cohort:T(string.format("Cohort weight=%.1f | max weight=%.1f", cohort.weightAsset, MaxWeight))
      return cohort.weightAsset<=MaxWeight
    else
      return true
    end    
  end

  
  -- Is capable of the mission type?
  local can=AUFTRAG.CheckMissionCapability(MissionType, Cohort.missiontypes)
  
  
  if can then
    can=CheckCategory(Cohort)
  else
    Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of mission types", Cohort.name))
    return false    
  end
  
  if can then
    if MissionType==AUFTRAG.Type.RELOCATECOHORT then
      can=Cohort:IsRelocating()
    else
      can=Cohort:IsOnDuty()  
    end
  else
    Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of category", Cohort.name))
    return false  
  end
  
  if can then
    can=CheckAttribute(Cohort)
  else
    Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of readyiness", Cohort.name))
    return false
  end
  
  if can then
    can=CheckProperty(Cohort)
  else
    Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of attribute", Cohort.name))
    return false
  end

  if can then
    can=CheckWeapon(Cohort)
  else
    Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of property", Cohort.name))
    return false
  end

  if can then
    can=CheckRange(Cohort)
  else
    Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of weapon type", Cohort.name))
    return false
  end
  
  if can then
    can=CheckRefueling(Cohort)
  else
    Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of range", Cohort.name))
    return false
  end

  if can then
    can=CheckCargoWeight(Cohort)
  else
    Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of refueling system", Cohort.name))
    return false
  end
  
  if can then
    can=CheckMaxWeight(Cohort)
  else
    Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of cargo weight", Cohort.name))
    return false
  end
  
  if can then
    return true
  else
    Cohort:T(Cohort.lid..string.format("Cohort %s cannot because of max weight", Cohort.name))
    return false
  end
  
  return nil
end

--- Recruit assets from Cohorts for the given parameters. **NOTE** that we set the `asset.isReserved=true` flag so it cannot be recruited by anyone else.
-- @param #table Cohorts Cohorts included.
-- @param #string MissionTypeRecruit Mission type for recruiting the cohort assets.
-- @param #string MissionTypeOpt Mission type for which the assets are optimized. Default is the same as `MissionTypeRecruit`.
-- @param #number NreqMin Minimum number of required assets.
-- @param #number NreqMax Maximum number of required assets.
-- @param DCS#Vec2 TargetVec2 Target position as 2D vector.
-- @param #table Payloads Special payloads.
-- @param #number RangeMax Max range in meters.
-- @param #number RefuelSystem Refuelsystem.
-- @param #number CargoWeight Cargo weight for recruiting transport carriers.
-- @param #number TotalWeight Total cargo weight in kg.
-- @param #number MaxWeight Max weight [kg] of the asset group.
-- @param #table Categories Group categories. 
-- @param #table Attributes Group attributes. See `GROUP.Attribute.`
-- @param #table Properties DCS attributes.
-- @param #table WeaponTypes Bit of weapon types.
-- @return #boolean If `true` enough assets could be recruited.
-- @return #table Recruited assets. **NOTE** that we set the `asset.isReserved=true` flag so it cant be recruited by anyone else.
-- @return #table Legions of recruited assets.
function LEGION.RecruitCohortAssets(Cohorts, MissionTypeRecruit, MissionTypeOpt, NreqMin, NreqMax, TargetVec2, Payloads, RangeMax, RefuelSystem, CargoWeight, TotalWeight, MaxWeight, Categories, Attributes, Properties, WeaponTypes)

  -- The recruited assets.
  local Assets={}

  -- Legions of recruited assets.
  local Legions={}
  
  -- Set MissionTypeOpt to Recruit if nil.
  if MissionTypeOpt==nil then
    MissionTypeOpt=MissionTypeRecruit
  end
    
  -- Loops over cohorts.
  for _,_cohort in pairs(Cohorts) do
    local cohort=_cohort --Ops.Cohort#COHORT
    
    -- Check if cohort can do the mission.
    local can=LEGION._CohortCan(cohort, MissionTypeRecruit, Categories, Attributes, Properties, WeaponTypes, TargetVec2, RangeMax, RefuelSystem, CargoWeight, MaxWeight)

    --env.info(string.format("RecruitCohortAssets %s Cohort=%s can=%s", MissionTypeRecruit, cohort:GetName(), tostring(can)))
    
    -- Check OnDuty, capable, in range and refueling type (if TANKER).
    if can then

      -- Recruit assets from cohort.
      local assets, npayloads=cohort:RecruitAssets(MissionTypeRecruit, 999)
      
      -- Add assets to the list.
      for _,asset in pairs(assets) do
        table.insert(Assets, asset)
      end
      
    end
    
  end
  
  -- Break if no assets could be found
  if #Assets==0 then
    --env.info(string.format("LEGION.RecruitCohortAssets: No assets could be recruited for mission type %s [Nmin=%s, Nmax=%s]", MissionTypeRecruit, tostring(NreqMin), tostring(NreqMax)))
    return false, {}, {}  
  end
  
  -- Now we have a long list with assets.
  LEGION._OptimizeAssetSelection(Assets, MissionTypeOpt, TargetVec2, false, TotalWeight)
  
  
  -- Get payloads for air assets.
  for _,_asset in pairs(Assets) do
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
    
    -- Only assets that have no payload. Should be only spawned assets!
    if asset.legion:IsAirwing() and not asset.payload then
    
      -- Fetch payload for asset. This can be nil!
      asset.payload=asset.legion:FetchPayloadFromStock(asset.unittype, MissionTypeOpt, Payloads)
              
    end    
  end
    
  -- Remove assets that dont have a payload.
  for i=#Assets,1,-1 do
    local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem
    if asset.legion:IsAirwing() and not asset.payload then
      table.remove(Assets, i)
    end
  end
    
  -- Now find the best asset for the given payloads.
  LEGION._OptimizeAssetSelection(Assets, MissionTypeOpt, TargetVec2, true, TotalWeight)

  -- Number of assets. At most NreqMax.
  local Nassets=math.min(#Assets, NreqMax)
  
  if #Assets>=NreqMin then
  
    ---
    -- Found enough assets
    ---

    -- Total cargo bay of all carrier assets.
    local cargobay=0

    -- Add assets to mission.
    for i=1,Nassets do
      local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem
      
      -- Asset is reserved and will not be picked for other missions.
      asset.isReserved=true
      
      -- Add legion.
      Legions[asset.legion.alias]=asset.legion
      
      -- Check if total cargo weight was given.
      if TotalWeight then
      
        -- Number of 
        local N=math.floor(asset.cargobaytot/asset.nunits / CargoWeight)*asset.nunits
        --env.info(string.format("cargobaytot=%d, cargoweight=%d ==> N=%d", asset.cargobaytot, CargoWeight, N))
        
        -- Sum up total cargo bay of all carrier assets.
        cargobay=cargobay + N*CargoWeight        
        
        -- Check if enough carrier assets were found to transport all cargo.
        if cargobay>=TotalWeight then
          --env.info(string.format("FF found enough assets to transport all cargo! N=%d [%d], cargobay=%.1f >= %.1f kg total weight", i, Nassets, cargobay, TotalWeight))
          Nassets=i
          break
        end
        
      end
      
    end
    
    -- Return payloads of not needed assets.
    for i=#Assets,Nassets+1,-1 do
      local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem
      if asset.legion:IsAirwing() and not asset.spawned then
        asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s", asset.spawngroupname))
        asset.legion:ReturnPayloadFromAsset(asset)
      end
      table.remove(Assets, i)
    end
    
    -- Found enough assets.
    return true, Assets, Legions
  else

    ---
    -- NOT enough assets
    ---
  
    -- Return payloads of assets.    
    for i=1,#Assets do
      local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem
      if asset.legion:IsAirwing() and not asset.spawned then
        asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s", asset.spawngroupname))
        asset.legion:ReturnPayloadFromAsset(asset)
      end
    end
      
    -- Not enough assets found.
    return false, {}, {}
  end

  return false, {}, {}
end

--- Unrecruit assets. Set `isReserved` to false, return payload to airwing and (optionally) remove from assigned mission.
-- @param #table Assets List of assets.
-- @param Ops.Auftrag#AUFTRAG Mission (Optional) The mission from which the assets will be deleted.
function LEGION.UnRecruitAssets(Assets, Mission)

  -- Return payloads of assets.    
  for i=1,#Assets do
    local asset=Assets[i] --Functional.Warehouse#WAREHOUSE.Assetitem
    -- Not reserved any more.
    asset.isReserved=false
    -- Return payload.
    if asset.legion:IsAirwing() and not asset.spawned then
      asset.legion:T2(asset.legion.lid..string.format("Returning payload from asset %s", asset.spawngroupname))
      asset.legion:ReturnPayloadFromAsset(asset)
    end
    -- Remove from mission.
    if Mission then
      Mission:DelAsset(asset)
    end    
  end  

end


--- Recruit and assign assets performing an escort mission for a given asset list. Note that each asset gets an escort.
-- @param #LEGION self
-- @param #table Cohorts Cohorts for escorting assets.
-- @param #table Assets Table of assets to be escorted.
-- @param #number NescortMin Min number of escort groups required per escorted asset.
-- @param #number NescortMax Max number of escort groups required per escorted asset.
-- @param #string MissionType Mission type.
-- @param #string TargetTypes Types of targets that are engaged.
-- @param #number EngageRange EngageRange in Nautical Miles.
-- @return #boolean If `true`, enough assets could be recruited or no escort was required in the first place.
function LEGION:AssignAssetsForEscort(Cohorts, Assets, NescortMin, NescortMax, MissionType, TargetTypes, EngageRange)

  -- Is an escort requested in the first place?
  if NescortMin and NescortMax and (NescortMin>0 or NescortMax>0) then
  
    -- Debug info.
    self:T(self.lid..string.format("Requested escort for %d assets from %d cohorts. Required escort assets=%d-%d", #Assets, #Cohorts, NescortMin, NescortMax))
    
    -- Escorts for each asset.        
    local Escorts={}
    
    local EscortAvail=true
    for _,_asset in pairs(Assets) do
      local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
      
      -- Target vector is the legion of the asset.
      local TargetVec2=asset.legion:GetVec2()
      
      -- We want airplanes for airplanes and helos for everything else.
      local Categories={Group.Category.HELICOPTER}
      local targetTypes={"Ground Units"}
      if asset.category==Group.Category.AIRPLANE then
        Categories={Group.Category.AIRPLANE}
        targetTypes={"Air"}
      end
      
      TargetTypes=TargetTypes or targetTypes
      
      -- Recruit escort asset for the mission asset.
      local Erecruited, eassets, elegions=LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.ESCORT, MissionType, NescortMin, NescortMax, TargetVec2, nil, nil, nil, nil, nil, nil, Categories)
      
      if Erecruited then
        Escorts[asset.spawngroupname]={EscortLegions=elegions, EscortAssets=eassets, ecategory=asset.category}
      else
        -- Could not find escort for this asset ==> Escort not possible ==> Break the loop.
        EscortAvail=false
        break
      end
    end
    
    -- ALL escorts could be recruited. 
    if EscortAvail then
    
      local N=0
      for groupname,value in pairs(Escorts) do
      
        local Elegions=value.EscortLegions
        local Eassets=value.EscortAssets
        local ecategory=value.ecategory
        
        for _,_legion in pairs(Elegions) do
          local legion=_legion --Ops.Legion#LEGION
    
          local OffsetVector=nil --DCS#Vec3
          if ecategory==Group.Category.GROUND then
            -- Overhead at 1000 ft.
            OffsetVector={}
            OffsetVector.x=0
            OffsetVector.y=UTILS.FeetToMeters(1000)
            OffsetVector.z=0
          elseif MissionType==AUFTRAG.Type.SEAD then
            -- Overhead slightly higher and right.
            OffsetVector={}
            OffsetVector.x=-100
            OffsetVector.y= 500
            OffsetVector.z= 500
          end
    
          -- Create and ESCORT mission for this asset.
          local escort=AUFTRAG:NewESCORT(groupname, OffsetVector, EngageRange, TargetTypes)
          
          -- For a SEAD mission, we also adjust the mission task.
          if MissionType==AUFTRAG.Type.SEAD then
            escort.missionTask=ENUMS.MissionTask.SEAD
            -- Add enroute task SEAD.
            local DCStask=CONTROLLABLE.EnRouteTaskSEAD(nil)
            table.insert(escort.enrouteTasks, DCStask)       
          end
          
          -- Reserve assts and add to mission.
          for _,_asset in pairs(Eassets) do
            local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
            escort:AddAsset(asset)
            N=N+1
          end
          
          -- Assign mission to legion.
          self:MissionAssign(escort, {legion})
        end
      end
      
      -- Debug info.
      self:T(self.lid..string.format("Recruited %d escort assets", N))
    
      -- Yup!
      return true    
    else

      -- Debug info.
      self:T(self.lid..string.format("Could not get at least one escort!"))      
    
      -- Could not get at least one escort. Unrecruit all recruited ones.
      for groupname,value in pairs(Escorts) do      
        local Eassets=value.EscortAssets
        LEGION.UnRecruitAssets(Eassets)
      end
      
      -- No,no!
      return false      
    end
    
  else
    -- No escort required.
    self:T(self.lid..string.format("No escort required! NescortMin=%s, NescortMax=%s", tostring(NescortMin), tostring(NescortMax)))
    return true
  end      

end

--- Recruit and assign assets performing an OPSTRANSPORT for a given asset list.
-- @param #LEGION self
-- @param #table Legions Transport legions.
-- @param #table CargoAssets Weight of the heaviest cargo group to be transported.
-- @param #number NcarriersMin Min number of carrier assets.
-- @param #number NcarriersMax Max number of carrier assets.
-- @param Core.Zone#ZONE DeployZone Deploy zone.
-- @param Core.Zone#ZONE DisembarkZone (Optional) Disembark zone. 
-- @param #table Categories Group categories.
-- @param #table Attributes Generalizes group attributes.
-- @param #table Properties DCS attributes.
-- @return #boolean If `true`, enough assets could be recruited and an OPSTRANSPORT object was created.
-- @return Ops.OpsTransport#OPSTRANSPORT Transport The transport.
function LEGION:AssignAssetsForTransport(Legions, CargoAssets, NcarriersMin, NcarriersMax, DeployZone, DisembarkZone, Categories, Attributes, Properties)

  -- Is an escort requested in the first place?
  if NcarriersMin and NcarriersMax and (NcarriersMin>0 or NcarriersMax>0) then
  
    -- Get cohorts.
    local Cohorts=LEGION._GetCohorts(Legions)
    
    -- Get all legions and heaviest cargo group weight
    local CargoLegions={} ; local CargoWeight=nil ; local TotalWeight=0
    for _,_asset in pairs(CargoAssets) do
      local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
      CargoLegions[asset.legion.alias]=asset.legion
      if CargoWeight==nil or asset.weight>CargoWeight then
        CargoWeight=asset.weight
      end
      TotalWeight=TotalWeight+asset.weight
    end
    
    -- Debug info.
    self:T(self.lid..string.format("Cargo weight=%.1f", CargoWeight))
    self:T(self.lid..string.format("Total weight=%.1f", TotalWeight))
  
    -- Target is the deploy zone.
    local TargetVec2=DeployZone:GetVec2()
    
    -- Recruit assets and legions.
    local TransportAvail, CarrierAssets, CarrierLegions=
    LEGION.RecruitCohortAssets(Cohorts, AUFTRAG.Type.OPSTRANSPORT, nil, NcarriersMin, NcarriersMax, TargetVec2, nil, nil, nil, CargoWeight, TotalWeight, nil, Categories, Attributes, Properties)
  
    if TransportAvail then
    
      -- Create and OPSTRANSPORT assignment.
      local Transport=OPSTRANSPORT:New(nil, nil, DeployZone)
      if DisembarkZone then
        Transport:SetDisembarkZone(DisembarkZone)
      end
      
      -- Debug info.    
      self:T(self.lid..string.format("Transport available with %d carrier assets", #CarrierAssets))
    
      -- Add cargo assets to transport.
      for _,_legion in pairs(CargoLegions) do
        local legion=_legion --Ops.Legion#LEGION
        
        -- Set pickup zone to spawn zone or airbase if the legion has one that is operational.
        local pickupzone=legion.spawnzone
        
        -- Add TZC from legion spawn zone to deploy zone.
        local tpz=Transport:AddTransportZoneCombo(nil, pickupzone, Transport:GetDeployZone())
        
        -- Set pickup airbase if the legion has an airbase. Could also be the ship itself.
        tpz.PickupAirbase=legion:IsRunwayOperational() and legion.airbase or nil
        
        -- Set embark zone to spawn zone.
        Transport:SetEmbarkZone(legion.spawnzone, tpz)
        
        -- Add cargo assets to transport.
        for _,_asset in pairs(CargoAssets) do
          local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
          if asset.legion.alias==legion.alias then
            Transport:AddAssetCargo(asset, tpz)
          end
        end
      end
      
      -- Add carrier assets.
      for _,_asset in pairs(CarrierAssets) do
        local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
        Transport:AddAsset(asset)
      end
          
      -- Assign TRANSPORT to legions. This also sends the request for the assets.
      self:TransportAssign(Transport, CarrierLegions)
      
      -- Got transport.
      return true, Transport
    else
      -- Debug info.    
      self:T(self.lid..string.format("Transport assets could not be allocated ==> Unrecruiting assets"))    
    
      -- Uncrecruit transport assets.
      LEGION.UnRecruitAssets(CarrierAssets)
      return false, nil
    end
  
    return nil, nil  
  end
  
  -- No transport requested in the first place.
  return true, nil
end


--- Calculate the mission score of an asset.
-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset Asset
-- @param #string MissionType Mission type for which the best assets are desired.
-- @param DCS#Vec2 TargetVec2 Target 2D vector.
-- @param #boolean IncludePayload If `true`, include the payload in the calulation if the asset has one attached.
-- @param #number TotalWeight The total weight of the cargo to be transported, if applicable.
-- @return #number Mission score.
function LEGION.CalculateAssetMissionScore(asset, MissionType, TargetVec2, IncludePayload, TotalWeight)
  
  -- Mission score.
  local score=0

  -- Prefer highly skilled assets.
  if asset.skill==AI.Skill.AVERAGE then
    score=score+0
  elseif asset.skill==AI.Skill.GOOD then
    score=score+10
  elseif asset.skill==AI.Skill.HIGH then
    score=score+20
  elseif asset.skill==AI.Skill.EXCELLENT then
    score=score+30
  end

  -- Add mission performance to score.
  score=score+asset.cohort:GetMissionPeformance(MissionType)

  -- Add payload performance to score.
  local function scorePayload(Payload, MissionType)
    for _,Capability in pairs(Payload.capabilities) do
      local capability=Capability --Ops.Auftrag#AUFTRAG.Capability
      if capability.MissionType==MissionType then
        return capability.Performance
      end
    end
    return 0
  end
  
  if IncludePayload and asset.payload then
    score=score+scorePayload(asset.payload, MissionType)
  end
    
  -- Origin: We take the OPSGROUP position or the one of the legion.
  local OrigVec2=asset.flightgroup and asset.flightgroup:GetVec2() or asset.legion:GetVec2()
  
  -- Distance factor.
  local distance=0
  if TargetVec2 and OrigVec2 then
  
    -- Distance in NM.
    distance=UTILS.MetersToNM(UTILS.VecDist2D(OrigVec2, TargetVec2))
    
    if asset.category==Group.Category.AIRPLANE or asset.category==Group.Category.HELICOPTER then
      -- Round: 55 NM ==> 5.5 ==> 6, 63 NM ==> 6.3 ==> 6
      distance=UTILS.Round(distance/10, 0)
    else
      -- For ground units the distance is a more important factor
      distance=UTILS.Round(distance, 0)
    end
    
  end
  
  -- Reduce score for legions that are futher away.
  score=score-distance
  
  -- Check for spawned assets.
  if asset.spawned and asset.flightgroup and asset.flightgroup:IsAlive() then
  
    -- Get current mission.
    local currmission=asset.flightgroup:GetMissionCurrent()
    
    if currmission then
    
      if currmission.type==AUFTRAG.Type.ALERT5 and currmission.alert5MissionType==MissionType then
        -- Prefer assets that are on ALERT5 for this mission type.
        score=score+25
      elseif (currmission.type==AUFTRAG.Type.GCICAP or currmission.type==AUFTRAG.Type.PATROLRACETRACK) and MissionType==AUFTRAG.Type.INTERCEPT then
        -- Prefer assets that are on GCICAP to perform INTERCEPTS. We set this even higher than alert5 because they are already in the air.
        score=score+35
      elseif (currmission.type==AUFTRAG.Type.ONGUARD or currmission.type==AUFTRAG.Type.PATROLZONE) and (MissionType==AUFTRAG.Type.ARTY  or MissionType==AUFTRAG.Type.GROUNDATTACK) then
        score=score+25
      elseif currmission.type==AUFTRAG.Type.NOTHING then
        score=score+30
      end
      
    end
  
    if MissionType==AUFTRAG.Type.OPSTRANSPORT or MissionType==AUFTRAG.Type.AMMOSUPPLY or MissionType==AUFTRAG.Type.AWACS or MissionType==AUFTRAG.Type.FUELSUPPLY or MissionType==AUFTRAG.Type.TANKER then
      -- TODO: need to check for missions that do not require ammo like transport, recon, awacs, tanker etc.
      -- We better take a fresh asset. Sometimes spawned assets do something else, which is difficult to check.
      score=score-10
    else
      -- Combat mission.
      if asset.flightgroup:IsOutOfAmmo() then
        -- Assets that are out of ammo are not considered.
        score=score-1000
      end
    end
  end
  
  -- TRANSPORT specific.
  if MissionType==AUFTRAG.Type.OPSTRANSPORT then
    if TotalWeight then
      -- Add 1 score point for each 10 kg of cargo bay capacity up to the total cargo weight,
      -- and then subtract 1 score point for each excess 10kg of cargo bay capacity.
      if asset.cargobaymax < TotalWeight then
        score=score+UTILS.Round(asset.cargobaymax/10, 0)
      else
        score=score+UTILS.Round(TotalWeight/10, 0)
      end
    else
      -- Add 1 score point for each 10 kg of cargo bay.
      score=score+UTILS.Round(asset.cargobaymax/10, 0)
    end
  end  

  -- TODO: This could be vastly improved. Need to gather ideas during testing.
  -- Calculate ETA? Assets on orbit missions should arrive faster even if they are further away.
  -- Max speed of assets.
  -- Fuel amount?
  -- Range of assets?
  
  if asset.legion and asset.legion.verbose>=2 then
    asset.legion:I(asset.legion.lid..string.format("Asset %s [spawned=%s] score=%d", asset.spawngroupname, tostring(asset.spawned), score))
  end

  return score
end

--- Optimize chosen assets for the mission at hand.
-- @param #table assets Table of (unoptimized) assets.
-- @param #string MissionType Mission type.
-- @param DCS#Vec2 TargetVec2 Target position as 2D vector.
-- @param #boolean IncludePayload If `true`, include the payload in the calulation if the asset has one attached.
-- @param #number TotalWeight The total weight of the cargo to be transported, if applicable.
function LEGION._OptimizeAssetSelection(assets, MissionType, TargetVec2, IncludePayload, TotalWeight)

  -- Calculate the mission score of all assets.
  for _,_asset in pairs(assets) do
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
    
    -- Calculate the asset score.
    asset.score=LEGION.CalculateAssetMissionScore(asset, MissionType, TargetVec2, IncludePayload, TotalWeight)
    
    if IncludePayload then
    
      -- Max random asset score.
      local RandomScoreMax=asset.legion and asset.legion.RandomAssetScore or LEGION.RandomAssetScore
      
      -- Random score.
      local RandomScore=math.random(0, RandomScoreMax)
      
      -- Debug info.
      --env.info(string.format("Asset %s: randomscore=%d, max=%d", asset.spawngroupname, RandomScore, RandomScoreMax))
      
      -- Add a bit of randomness.
      asset.score=asset.score+RandomScore
      
    end
  end

  --- Sort assets wrt to their mission score. Higher is better.
  local function optimize(a, b)
    local assetA=a --Functional.Warehouse#WAREHOUSE.Assetitem
    local assetB=b --Functional.Warehouse#WAREHOUSE.Assetitem
    -- Higher score wins. If equal score ==> closer wins.
    return (assetA.score>assetB.score)
  end
  table.sort(assets, optimize)

  -- Remove distance parameter.
  if LEGION.verbose>0 then
    local text=string.format("Optimized %d assets for %s mission/transport (payload=%s):", #assets, MissionType, tostring(IncludePayload))
    for i,Asset in pairs(assets) do
      local asset=Asset --Functional.Warehouse#WAREHOUSE.Assetitem
      text=text..string.format("\n%d. %s [%s]: score=%d", i, asset.spawngroupname, asset.squadname, asset.score or -1)
      asset.score=nil
    end
    env.info(text)
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Returns the mission for a given mission ID (Autragsnummer).
-- @param #LEGION self
-- @param #number mid Mission ID (Auftragsnummer).
-- @return Ops.Auftrag#AUFTRAG Mission table.
function LEGION:GetMissionByID(mid)

  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    if mission.auftragsnummer==tonumber(mid) then
      return mission
    end

  end

  return nil
end

--- Returns the mission for a given ID.
-- @param #LEGION self
-- @param #number uid Transport UID.
-- @return Ops.OpsTransport#OPSTRANSPORT Transport assignment.
function LEGION:GetTransportByID(uid)

  for _,_transport in pairs(self.transportqueue) do
    local transport=_transport --Ops.OpsTransport#OPSTRANSPORT

    if transport.uid==tonumber(uid) then
      return transport
    end

  end

  return nil
end

--- Returns the mission for a given request ID.
-- @param #LEGION self
-- @param #number RequestID Unique ID of the request.
-- @return Ops.Auftrag#AUFTRAG Mission table or *nil*.
function LEGION:GetMissionFromRequestID(RequestID)
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG
    local mid=mission.requestID[self.alias]
    if  mid and mid==RequestID then
      return mission
    end
  end
  return nil
end

--- Returns the mission for a given request.
-- @param #LEGION self
-- @param Functional.Warehouse#WAREHOUSE.Queueitem Request The warehouse request.
-- @return Ops.Auftrag#AUFTRAG Mission table or *nil*.
function LEGION:GetMissionFromRequest(Request)
  return self:GetMissionFromRequestID(Request.uid)
end

--- Fetch a payload from the airwing resources for a given unit and mission type.
-- The payload with the highest priority is preferred.
-- @param #LEGION self
-- @param #string UnitType The type of the unit.
-- @param #string MissionType The mission type.
-- @param #table Payloads Specific payloads only to be considered.
-- @return Ops.Airwing#AIRWING.Payload Payload table or *nil*.
function LEGION:FetchPayloadFromStock(UnitType, MissionType, Payloads)
  -- Polymorphic. Will return something when called by airwing.
  return nil
end

--- Return payload from asset back to stock.
-- @param #LEGION self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem asset The squadron asset.
function LEGION:ReturnPayloadFromAsset(asset)
  -- Polymorphic.
  return nil
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Enhanced Naval Group.
-- 
-- ## Main Features:
--
--    * Let the group steam into the wind
--    * Command a full stop
--    * Patrol waypoints *ad infinitum*
--    * Collision warning, if group is heading towards a land mass or another obstacle
--    * Automatic pathfinding, e.g. around islands
--    * Let a submarine dive and surface
--    * Manage TACAN and ICLS beacons
--    * Dynamically add and remove waypoints
--    * Sophisticated task queueing system (know when DCS tasks start and end)
--    * Convenient checks when the group enters or leaves a zone
--    * Detection events for new, known and lost units
--    * Simple LASER and IR-pointer setup
--    * Compatible with AUFTRAG class
--    * Many additional events that the mission designer can hook into
-- 
-- ===
-- 
-- ## Example Missions:
--
-- Demo missions can be found on [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/Navygroup)
--
-- ===
--
-- ### Author: **funkyfranky**
-- 
-- ===
-- @module Ops.NavyGroup
-- @image OPS_NavyGroup.png


--- NAVYGROUP class.
-- @type NAVYGROUP
-- @field #boolean turning If true, group is currently turning.
-- @field #NAVYGROUP.IntoWind intowind Into wind info.
-- @field #table Qintowind Queue of "into wind" turns.
-- @field #number intowindcounter Counter of into wind IDs.
-- @field #number depth Ordered depth in meters.
-- @field #boolean collisionwarning If true, collition warning.
-- @field #boolean pathfindingOn If true, enable pathfining.
-- @field #number pathCorridor Path corrdidor width in meters.
-- @field #boolean ispathfinding If true, group is currently path finding.
-- @field #NAVYGROUP.Target engage Engage target.
-- @field #boolean intowindold Use old calculation to determine heading into wind.
-- @extends Ops.OpsGroup#OPSGROUP

--- *Something must be left to chance; nothing is sure in a sea fight above all.* -- Horatio Nelson
--
-- ===
-- 
-- # The NAVYGROUP Concept
-- 
-- This class enhances naval groups.
-- 
-- @field #NAVYGROUP
NAVYGROUP = {
  ClassName       = "NAVYGROUP",
  turning         = false,
  intowind        = nil,
  intowindcounter = 0,
  Qintowind       = {},
  pathCorridor    = 400,
  engage          = {},  
}

--- Turn into wind parameters.
-- @type NAVYGROUP.IntoWind
-- @field #number Tstart Time to start.
-- @field #number Tstop Time to stop.
-- @field #boolean Uturn U-turn.
-- @field #number Speed Speed in knots.
-- @field #number Offset Offset angle in degrees.
-- @field #number Id Unique ID of the turn.
-- @field Ops.OpsGroup#OPSGROUP.Waypoint waypoint Turn into wind waypoint.
-- @field Core.Point#COORDINATE Coordinate Coordinate where we left the route.
-- @field #number Heading Heading the boat will take in degrees.
-- @field #boolean Open Currently active.
-- @field #boolean Over This turn is over.
-- @field #boolean Recovery If `true` this is a recovery window. If `false`, this is a launch window. If `nil` this is just a turn into the wind.

--- Engage Target.
-- @type NAVYGROUP.Target
-- @field Ops.Target#TARGET Target The target.
-- @field Core.Point#COORDINATE Coordinate Last known coordinate of the target.
-- @field Ops.OpsGroup#OPSGROUP.Waypoint Waypoint the waypoint created to go to the target.
-- @field #number roe ROE backup.
-- @field #number alarmstate Alarm state backup.

--- NavyGroup version.
-- @field #string version
NAVYGROUP.version="1.0.3"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Add Retreat.
-- TODO: Submaries.
-- TODO: Extend, shorten turn into wind windows.
-- TODO: Skipper menu.
-- DONE: Add EngageTarget.
-- DONE: Add RTZ.
-- DONE: Collision warning.
-- DONE: Detour, add temporary waypoint and resume route.
-- DONE: Stop and resume route.
-- DONE: Add waypoints.
-- DONE: Add tasks.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new NAVYGROUP class object.
-- @param #NAVYGROUP self
-- @param Wrapper.Group#GROUP group The group object. Can also be given by its group name as `#string`.
-- @return #NAVYGROUP self
function NAVYGROUP:New(group)

  -- First check if we already have an OPS group for this group.
  local og=_DATABASE:GetOpsGroup(group)
  if og then
    og:I(og.lid..string.format("WARNING: OPS group already exists in data base!"))
    return og
  end

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, OPSGROUP:New(group)) -- #NAVYGROUP
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("NAVYGROUP %s | ", self.groupname)
  
  -- Defaults
  self:SetDefaultROE()
  self:SetDefaultAlarmstate()
  self:SetDefaultEPLRS(self.isEPLRS)
  self:SetDefaultEmission()
  self:SetDetection()  
  self:SetPatrolAdInfinitum(true)
  self:SetPathfinding(false)

  -- Add FSM transitions.
  --                 From State  -->   Event      -->     To State
  self:AddTransition("*",             "FullStop",         "Holding")     -- Hold position.
  self:AddTransition("*",             "Cruise",           "Cruising")    -- Hold position.

  self:AddTransition("*",             "RTZ",              "Returning")   -- Group is returning to (home) zone.
  self:AddTransition("Returning",     "Returned",         "Returned")    -- Group is returned to (home) zone.

  self:AddTransition("*",             "Detour",           "Cruising")    -- Make a detour to a coordinate and resume route afterwards.
  self:AddTransition("*",             "DetourReached",    "*")           -- Group reached the detour coordinate.

  self:AddTransition("*",             "Retreat",          "Retreating")  -- Order a retreat.
  self:AddTransition("Retreating",    "Retreated",        "Retreated")   -- Group retreated.

  self:AddTransition("Cruising",      "EngageTarget",     "Engaging")    -- Engage a target from Cruising state
  self:AddTransition("Holding",       "EngageTarget",     "Engaging")    -- Engage a target from Holding state
  self:AddTransition("OnDetour",      "EngageTarget",     "Engaging")    -- Engage a target from OnDetour state
  self:AddTransition("Engaging",      "Disengage",        "Cruising")    -- Disengage and back to cruising.
  
  self:AddTransition("*",             "TurnIntoWind",     "Cruising")    -- Command the group to turn into the wind.
  self:AddTransition("*",             "TurnedIntoWind",   "*")           -- Group turned into wind.
  self:AddTransition("*",             "TurnIntoWindStop", "*")           -- Stop a turn into wind.  
  self:AddTransition("*",             "TurnIntoWindOver", "*")           -- Turn into wind is over.
  
  self:AddTransition("*",             "TurningStarted",   "*")           -- Group started turning.
  self:AddTransition("*",             "TurningStopped",   "*")           -- Group stopped turning.
  
  self:AddTransition("*",             "CollisionWarning", "*")           -- Collision warning.
  self:AddTransition("*",             "ClearAhead",       "*")           -- Clear ahead.
  
  self:AddTransition("Cruising",      "Dive",             "Cruising")    -- Command a submarine to dive.
  self:AddTransition("Engaging",      "Dive",             "Engaging")    -- Command a submarine to dive.
  self:AddTransition("Cruising",      "Surface",          "Cruising")    -- Command a submarine to go to the surface.
  self:AddTransition("Engaging",      "Surface",          "Engaging")    -- Command a submarine to go to the surface.
  
  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Cruise".
  -- @function [parent=#NAVYGROUP] Cruise
  -- @param #NAVYGROUP self
  -- @param #number Speed Speed in knots until next waypoint is reached.

  --- Triggers the FSM event "Cruise" after a delay.
  -- @function [parent=#NAVYGROUP] __Cruise
  -- @param #NAVYGROUP self
  -- @param #number delay Delay in seconds.
  -- @param #number Speed Speed in knots until next waypoint is reached.

  --- On after "Cruise" event.
  -- @function [parent=#NAVYGROUP] OnAfterCruise
  -- @param #NAVYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number Speed Speed in knots until next waypoint is reached.


  --- Triggers the FSM event "TurnIntoWind".
  -- @function [parent=#NAVYGROUP] TurnIntoWind
  -- @param #NAVYGROUP self
  -- @param #NAVYGROUP.IntoWind Into wind parameters.

  --- Triggers the FSM event "TurnIntoWind" after a delay.
  -- @function [parent=#NAVYGROUP] __TurnIntoWind
  -- @param #NAVYGROUP self
  -- @param #number delay Delay in seconds.
  -- @param #NAVYGROUP.IntoWind Into wind parameters.

  --- On after "TurnIntoWind" event.
  -- @function [parent=#NAVYGROUP] OnAfterTurnIntoWind
  -- @param #NAVYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #NAVYGROUP.IntoWind Into wind parameters.


  --- Triggers the FSM event "TurnedIntoWind".
  -- @function [parent=#NAVYGROUP] TurnedIntoWind
  -- @param #NAVYGROUP self

  --- Triggers the FSM event "TurnedIntoWind" after a delay.
  -- @function [parent=#NAVYGROUP] __TurnedIntoWind
  -- @param #NAVYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "TurnedIntoWind" event.
  -- @function [parent=#NAVYGROUP] OnAfterTurnedIntoWind
  -- @param #NAVYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "TurnIntoWindStop".
  -- @function [parent=#NAVYGROUP] TurnIntoWindStop
  -- @param #NAVYGROUP self

  --- Triggers the FSM event "TurnIntoWindStop" after a delay.
  -- @function [parent=#NAVYGROUP] __TurnIntoWindStop
  -- @param #NAVYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "TurnIntoWindStop" event.
  -- @function [parent=#NAVYGROUP] OnAfterTurnIntoWindStop
  -- @param #NAVYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "TurnIntoWindOver".
  -- @function [parent=#NAVYGROUP] TurnIntoWindOver
  -- @param #NAVYGROUP self
  -- @param #NAVYGROUP.IntoWind IntoWindData Data table.

  --- Triggers the FSM event "TurnIntoWindOver" after a delay.
  -- @function [parent=#NAVYGROUP] __TurnIntoWindOver
  -- @param #NAVYGROUP self
  -- @param #number delay Delay in seconds.
  -- @param #NAVYGROUP.IntoWind IntoWindData Data table.

  --- On after "TurnIntoWindOver" event.
  -- @function [parent=#NAVYGROUP] OnAfterTurnIntoWindOver
  -- @param #NAVYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #NAVYGROUP.IntoWind IntoWindData Data table.


  --- Triggers the FSM event "TurningStarted".
  -- @function [parent=#NAVYGROUP] TurningStarted
  -- @param #NAVYGROUP self

  --- Triggers the FSM event "TurningStarted" after a delay.
  -- @function [parent=#NAVYGROUP] __TurningStarted
  -- @param #NAVYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "TurningStarted" event.
  -- @function [parent=#NAVYGROUP] OnAfterTurningStarted
  -- @param #NAVYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "TurningStopped".
  -- @function [parent=#NAVYGROUP] TurningStopped
  -- @param #NAVYGROUP self

  --- Triggers the FSM event "TurningStopped" after a delay.
  -- @function [parent=#NAVYGROUP] __TurningStopped
  -- @param #NAVYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "TurningStopped" event.
  -- @function [parent=#NAVYGROUP] OnAfterTurningStopped
  -- @param #NAVYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "CollisionWarning".
  -- @function [parent=#NAVYGROUP] CollisionWarning
  -- @param #NAVYGROUP self

  --- Triggers the FSM event "CollisionWarning" after a delay.
  -- @function [parent=#NAVYGROUP] __CollisionWarning
  -- @param #NAVYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "CollisionWarning" event.
  -- @function [parent=#NAVYGROUP] OnAfterCollisionWarning
  -- @param #NAVYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "ClearAhead".
  -- @function [parent=#NAVYGROUP] ClearAhead
  -- @param #NAVYGROUP self

  --- Triggers the FSM event "ClearAhead" after a delay.
  -- @function [parent=#NAVYGROUP] __ClearAhead
  -- @param #NAVYGROUP self
  -- @param #number delay Delay in seconds.

  --- On after "ClearAhead" event.
  -- @function [parent=#NAVYGROUP] OnAfterClearAhead
  -- @param #NAVYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Dive".
  -- @function [parent=#NAVYGROUP] Dive
  -- @param #NAVYGROUP self
  -- @param #number Depth Dive depth in meters. Default 50 meters.
  -- @param #number Speed Speed in knots until next waypoint is reached.

  --- Triggers the FSM event "Dive" after a delay.
  -- @function [parent=#NAVYGROUP] __Dive
  -- @param #NAVYGROUP self
  -- @param #number delay Delay in seconds.
  -- @param #number Depth Dive depth in meters. Default 50 meters.
  -- @param #number Speed Speed in knots until next waypoint is reached.

  --- On after "Dive" event.
  -- @function [parent=#NAVYGROUP] OnAfterDive
  -- @param #NAVYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number Depth Dive depth in meters. Default 50 meters.
  -- @param #number Speed Speed in knots until next waypoint is reached.


  --- Triggers the FSM event "Surface".
  -- @function [parent=#NAVYGROUP] Surface
  -- @param #NAVYGROUP self
  -- @param #number Speed Speed in knots until next waypoint is reached.

  --- Triggers the FSM event "Surface" after a delay.
  -- @function [parent=#NAVYGROUP] __Surface
  -- @param #NAVYGROUP self
  -- @param #number delay Delay in seconds.
  -- @param #number Speed Speed in knots until next waypoint is reached.

  --- On after "Surface" event.
  -- @function [parent=#NAVYGROUP] OnAfterSurface
  -- @param #NAVYGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number Speed Speed in knots until next waypoint is reached.


  -- Init waypoints.
  self:_InitWaypoints()
  
  -- Initialize the group.
  self:_InitGroup()

  -- Handle events:
  self:HandleEvent(EVENTS.Birth,      self.OnEventBirth)
  self:HandleEvent(EVENTS.Dead,       self.OnEventDead)
  self:HandleEvent(EVENTS.RemoveUnit, self.OnEventRemoveUnit)
  self:HandleEvent(EVENTS.UnitLost,   self.OnEventRemoveUnit)  
  
  -- Start the status monitoring.
  self.timerStatus=TIMER:New(self.Status, self):Start(1, 30)

  -- Start queue update timer.
  self.timerQueueUpdate=TIMER:New(self._QueueUpdate, self):Start(2, 5)
  
  -- Start check zone timer.
  self.timerCheckZone=TIMER:New(self._CheckInZones, self):Start(2, 60)

  -- Add OPSGROUP to _DATABASE.
  _DATABASE:AddOpsGroup(self)
     
  return self  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Group patrols ad inifintum. If the last waypoint is reached, it will go to waypoint one and repeat its route.
-- @param #NAVYGROUP self
-- @param #boolean switch If true or nil, patrol until the end of time. If false, go along the waypoints once and stop.
-- @return #NAVYGROUP self
function NAVYGROUP:SetPatrolAdInfinitum(switch)
  if switch==false then
    self.adinfinitum=false
  else
    self.adinfinitum=true
  end
  return self
end

--- Enable/disable pathfinding.
-- @param #NAVYGROUP self
-- @param #boolean Switch If true, enable pathfinding.
-- @param #number CorridorWidth Corridor with in meters. Default 400 m.
-- @return #NAVYGROUP self
function NAVYGROUP:SetPathfinding(Switch, CorridorWidth)
  self.pathfindingOn=Switch
  self.pathCorridor=CorridorWidth or 400
  return self
end

--- Enable pathfinding.
-- @param #NAVYGROUP self
-- @param #number CorridorWidth Corridor with in meters. Default 400 m.
-- @return #NAVYGROUP self
function NAVYGROUP:SetPathfindingOn(CorridorWidth)
  self:SetPathfinding(true, CorridorWidth)
  return self
end

--- Disable pathfinding.
-- @param #NAVYGROUP self
-- @return #NAVYGROUP self
function NAVYGROUP:SetPathfindingOff()
  self:SetPathfinding(false, self.pathCorridor)
  return self
end

--- Set if old into wind calculation is used when carrier turns into the wind for a recovery.
-- @param #NAVYGROUP self
-- @param #boolean SwitchOn If `true` or `nil`, use old into wind calculation.
-- @return #NAVYGROUP self
function NAVYGROUP:SetIntoWindLegacy( SwitchOn )
  if SwitchOn==nil then
    SwitchOn=true
  end
  self.intowindold=SwitchOn
  return self
end


--- Add a *scheduled* task.
-- @param #NAVYGROUP self
-- @param Core.Point#COORDINATE Coordinate Coordinate of the target.
-- @param #string Clock Time when to start the attack.
-- @param #number Radius Radius in meters. Default 100 m.
-- @param #number Nshots Number of shots to fire. Default 3.
-- @param #number WeaponType Type of weapon. Default auto.
-- @param #number Prio Priority of the task.
-- @return Ops.OpsGroup#OPSGROUP.Task The task data.
function NAVYGROUP:AddTaskFireAtPoint(Coordinate, Clock, Radius, Nshots, WeaponType, Prio)

  local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType)

  local task=self:AddTask(DCStask, Clock, nil, Prio)

  return task
end

--- Add a *waypoint* task.
-- @param #NAVYGROUP self
-- @param Core.Point#COORDINATE Coordinate Coordinate of the target.
-- @param Ops.OpsGroup#OPSGROUP.Waypoint Waypoint Where the task is executed. Default is next waypoint.
-- @param #number Radius Radius in meters. Default 100 m.
-- @param #number Nshots Number of shots to fire. Default 3.
-- @param #number WeaponType Type of weapon. Default auto.
-- @param #number Prio Priority of the task.
-- @param #number Duration Duration in seconds after which the task is cancelled. Default *never*.
-- @return Ops.OpsGroup#OPSGROUP.Task The task table.
function NAVYGROUP:AddTaskWaypointFireAtPoint(Coordinate, Waypoint, Radius, Nshots, WeaponType, Prio, Duration)

  Waypoint=Waypoint or self:GetWaypointNext()

  local DCStask=CONTROLLABLE.TaskFireAtPoint(nil, Coordinate:GetVec2(), Radius, Nshots, WeaponType)

  local task=self:AddTaskWaypoint(DCStask, Waypoint, nil, Prio, Duration)

  return task
end


--- Add a *scheduled* task.
-- @param #NAVYGROUP self
-- @param Wrapper.Group#GROUP TargetGroup Target group.
-- @param #number WeaponExpend How much weapons does are used.
-- @param #number WeaponType Type of weapon. Default auto.
-- @param #string Clock Time when to start the attack.
-- @param #number Prio Priority of the task.
-- @return Ops.OpsGroup#OPSGROUP.Task The task data.
function NAVYGROUP:AddTaskAttackGroup(TargetGroup, WeaponExpend, WeaponType, Clock, Prio)

  local DCStask=CONTROLLABLE.TaskAttackGroup(nil, TargetGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit, GroupAttack)

  local task=self:AddTask(DCStask, Clock, nil, Prio)
  
  return task
end

--- Create a turn into wind window. Note that this is not executed as it not added to the queue.
-- @param #NAVYGROUP self
-- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now.
-- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time.
-- @param #number speed Speed in knots during turn into wind leg.
-- @param #boolean uturn If true (or nil), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint.
-- @param #number offset Offset angle in degrees, e.g. to account for an angled runway. Default 0 deg.
-- @return #NAVYGROUP.IntoWind Recovery window.
function NAVYGROUP:_CreateTurnIntoWind(starttime, stoptime, speed, uturn, offset)

  -- Absolute mission time in seconds.
  local Tnow=timer.getAbsTime()

  -- Convert number to Clock.
  if starttime and type(starttime)=="number" then
    starttime=UTILS.SecondsToClock(Tnow+starttime)
  end

  -- Input or now.
  starttime=starttime or UTILS.SecondsToClock(Tnow)

  -- Set start time.
  local Tstart=UTILS.ClockToSeconds(starttime)
  
  if uturn==nil then
    uturn=true
  end

  -- Set stop time.
  local Tstop=Tstart+90*60

  if stoptime==nil then
    Tstop=Tstart+90*60
  elseif type(stoptime)=="number" then
    Tstop=Tstart+stoptime
  else
    Tstop=UTILS.ClockToSeconds(stoptime)
  end


  -- Consistancy check for timing.
  if Tstart>Tstop then
    self:E(string.format("ERROR:Into wind stop time %s lies before start time %s. Input rejected!", UTILS.SecondsToClock(Tstart), UTILS.SecondsToClock(Tstop)))
    return self
  end
  if Tstop<=Tnow then
    self:E(string.format("WARNING: Into wind stop time %s already over. Tnow=%s! Input rejected.", UTILS.SecondsToClock(Tstop), UTILS.SecondsToClock(Tnow)))
    return self
  end

  -- Increase counter.
  self.intowindcounter=self.intowindcounter+1

  -- Recovery window.
  local recovery={} --#NAVYGROUP.IntoWind
  recovery.Tstart=Tstart
  recovery.Tstop=Tstop
  recovery.Open=false
  recovery.Over=false
  recovery.Speed=speed or 20
  recovery.Uturn=uturn and uturn or false
  recovery.Offset=offset or 0
  recovery.Id=self.intowindcounter

  return recovery
end

--- Add a time window, where the groups steams into the wind.
-- @param #NAVYGROUP self
-- @param #string starttime Start time, e.g. "8:00" for eight o'clock. Default now.
-- @param #string stoptime Stop time, e.g. "9:00" for nine o'clock. Default 90 minutes after start time.
-- @param #number speed Wind speed on deck in knots during turn into wind leg. Default 20 knots.
-- @param #boolean uturn If `true` (or `nil`), carrier wil perform a U-turn and go back to where it came from before resuming its route to the next waypoint. If false, it will go directly to the next waypoint.
-- @param #number offset Offset angle clock-wise in degrees, *e.g.* to account for an angled runway. Default 0 deg. Use around -9.1° for US carriers.
-- @return #NAVYGROUP.IntoWind Turn into window data table.
function NAVYGROUP:AddTurnIntoWind(starttime, stoptime, speed, uturn, offset)

  local recovery=self:_CreateTurnIntoWind(starttime, stoptime, speed, uturn, offset)
  
  --TODO: check if window is overlapping with an other and if extend the window.
  
  -- Add to table
  table.insert(self.Qintowind, recovery)

  return recovery
end

--- Get "Turn Into Wind" data. You can specify a certain ID.
-- @param #NAVYGROUP self
-- @param #number TID (Optional) Turn Into wind ID. If not given, the currently open "Turn into Wind" data is return (if there is any).
-- @return #NAVYGROUP.IntoWind Turn into window data table.
function NAVYGROUP:GetTurnIntoWind(TID)

  if TID then
  
    -- Look for a specific ID.
    for _,_turn in pairs(self.Qintowind) do
      local turn=_turn --#NAVYGROUP.IntoWind      
      if turn.Id==TID then
        return turn
      end    
    end
  
  else

    -- Return currently open window.
    return self.intowind
  
  end

  return nil
end

--- Extend duration of turn into wind.
-- @param #NAVYGROUP self
-- @param #number Duration Duration in seconds. Default 300 sec.
-- @param #NAVYGROUP.IntoWind TurnIntoWind (Optional) Turn into window data table. If not given, the currently open one is used (if there is any).
-- @return #NAVYGROUP self
function NAVYGROUP:ExtendTurnIntoWind(Duration, TurnIntoWind)

  Duration=Duration or 300

  -- ID of turn or nil
  local TID=TurnIntoWind and TurnIntoWind.Id or nil
  
  -- Get turn data.
  local turn=self:GetTurnIntoWind(TID)
  
  if turn then
    turn.Tstop=turn.Tstop+Duration
    self:T(self.lid..string.format("Extending turn into wind by %d seconds. New stop time is %s", Duration, UTILS.SecondsToClock(turn.Tstop)))
  else
    self:E(self.lid.."Could not get turn into wind to extend!")
  end

  return self
end


--- Remove steam into wind window from queue. If the window is currently active, it is stopped first.
-- @param #NAVYGROUP self
-- @param #NAVYGROUP.IntoWind IntoWindData Turn into window data table.
-- @return #NAVYGROUP self
function NAVYGROUP:RemoveTurnIntoWind(IntoWindData)

  -- Check if this is a window currently open.
  if self.intowind and self.intowind.Id==IntoWindData.Id then
    self:TurnIntoWindStop()
    return
  end  

  for i,_tiw in pairs(self.Qintowind) do
    local tiw=_tiw --#NAVYGROUP.IntoWind
    if tiw.Id==IntoWindData.Id then
      --env.info("FF removing window "..tiw.Id)
      table.remove(self.Qintowind, i)
      break
    end
  end
  
  return self
end


--- Check if the group is currently holding its positon.
-- @param #NAVYGROUP self
-- @return #boolean If true, group was ordered to hold.
function NAVYGROUP:IsHolding()
  return self:Is("Holding")
end

--- Check if the group is currently cruising.
-- @param #NAVYGROUP self
-- @return #boolean If true, group cruising.
function NAVYGROUP:IsCruising()
  return self:Is("Cruising")
end

--- Check if the group is currently on a detour.
-- @param #NAVYGROUP self
-- @return #boolean If true, group is on a detour
function NAVYGROUP:IsOnDetour()
  return self:Is("OnDetour")
end


--- Check if the group is currently diving.
-- @param #NAVYGROUP self
-- @return #boolean If true, group is currently diving.
function NAVYGROUP:IsDiving()
  return self:Is("Diving")
end

--- Check if the group is currently turning.
-- @param #NAVYGROUP self
-- @return #boolean If true, group is currently turning.
function NAVYGROUP:IsTurning()
  return self.turning
end

--- Check if the group is currently steaming into the wind.
-- @param #NAVYGROUP self
-- @return #boolean If true, group is currently steaming into the wind.
function NAVYGROUP:IsSteamingIntoWind()
  if self.intowind then
    return true
  else
    return false    
  end
end

--- Check if the group is currently recovering aircraft.
-- @param #NAVYGROUP self
-- @return #boolean If true, group is currently recovering.
function NAVYGROUP:IsRecovering()
  if self.intowind then
    if self.intowind.Recovery==true then
      return true
    else
      return false
    end
  else
    return false    
  end
end

--- Check if the group is currently launching aircraft.
-- @param #NAVYGROUP self
-- @return #boolean If true, group is currently launching.
function NAVYGROUP:IsLaunching()
  if self.intowind then
    if self.intowind.Recovery==false then
      return true
    else
      return false
    end
  else
    return false    
  end
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Update status.
-- @param #NAVYGROUP self
function NAVYGROUP:Status()

  -- FSM state.
  local fsmstate=self:GetState()

  -- Is group alive?
  local alive=self:IsAlive()
  
  -- Free path.
  local freepath=0
  
  -- Check if group is exists and is active.
  if alive then

    -- Update last known position, orientation, velocity.
    self:_UpdatePosition()
  
    -- Check if group has detected any units.
    self:_CheckDetectedUnits()
    
    -- Check if group started or stopped turning.
    self:_CheckTurning()
  
    -- Distance to next Waypoint.  
    local disttoWP=math.min(self:GetDistanceToWaypoint(), UTILS.NMToMeters(10))
    freepath=disttoWP
    
    -- Only check if not currently turning.
    if not self:IsTurning() then
    
      -- Check free path ahead.
      freepath=self:_CheckFreePath(freepath, 100)
      
      if disttoWP>1 and freepath<disttoWP then
      
        if not self.collisionwarning then
          -- Issue a collision warning event.
          self:CollisionWarning(freepath)
        end
    
        if self.pathfindingOn and not self.ispathfinding then
          self.ispathfinding=self:_FindPathToNextWaypoint()
        end
        
      end
      
    end
    
    -- Check into wind queue.
    self:_CheckTurnsIntoWind()

    -- Check ammo status.
    self:_CheckAmmoStatus()
          
    -- Check damage of elements and group.
    self:_CheckDamage()
    
    -- Check if group got stuck.
    self:_CheckStuck()
    
    -- Check if group is waiting.
    if self:IsWaiting() then
      if self.Twaiting and self.dTwait then
        if timer.getAbsTime()>self.Twaiting+self.dTwait then
          self.Twaiting=nil
          self.dTwait=nil
          if self:_CountPausedMissions()>0 then
            self:UnpauseMission()
          else          
            self:Cruise()
          end
        end
      end
    end


    -- Get current mission (if any).
    local mission=self:GetMissionCurrent()
    
    -- If mission, check if DCS task needs to be updated.
    if mission and mission.updateDCSTask  then
    
      if mission.type==AUFTRAG.Type.CAPTUREZONE then
       
        -- Get task.
        local Task=mission:GetGroupWaypointTask(self)
        
        -- Update task: Engage or get new zone.
        if mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING or  mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.STARTED then
          self:_UpdateTask(Task, mission)
        end
                  
      end
          
    end    

    
  else
    -- Check damage of elements and group.
    self:_CheckDamage()    
  end

  -- Group exists but can also be inactive.  
  if alive~=nil then

    if self.verbose>=1 then

      -- Number of elements.
      local nelem=self:CountElements()
      local Nelem=#self.elements
  
      -- Get number of tasks and missions.
      local nTaskTot, nTaskSched, nTaskWP=self:CountRemainingTasks()
      local nMissions=self:CountRemainingMissison()
      
      -- ROE and Alarm State.
      local roe=self:GetROE() or -1
      local als=self:GetAlarmstate() or -1

      -- Waypoint stuff.
      local wpidxCurr=self.currentwp
      local wpuidCurr=self:GetWaypointUIDFromIndex(wpidxCurr) or 0
      local wpidxNext=self:GetWaypointIndexNext() or 0
      local wpuidNext=self:GetWaypointUIDFromIndex(wpidxNext) or 0
      local wpN=#self.waypoints or 0
      local wpF=tostring(self.passedfinalwp)
      
      -- Speed.
      local speed=UTILS.MpsToKnots(self.velocity or 0)
      local speedEx=UTILS.MpsToKnots(self:GetExpectedSpeed())
      
      -- Altitude.
      local alt=self.position and self.position.y or 0
      
      -- Heading in degrees.
      local hdg=self.heading or 0      
      
      -- Life points.
      local life=self.life or 0
      
      -- Total ammo.            
      local ammo=self:GetAmmoTot().Total
      
      -- Detected units.
      local ndetected=self.detectionOn and tostring(self.detectedunits:Count()) or "Off"      
      
      -- Get cargo weight.
      local cargo=0
      for _,_element in pairs(self.elements) do
        local element=_element --Ops.OpsGroup#OPSGROUP.Element
        cargo=cargo+element.weightCargo
      end
      
      -- Into wind and turning status.
      local intowind=self:IsSteamingIntoWind() and UTILS.SecondsToClock(self.intowind.Tstop-timer.getAbsTime(), true) or "N/A"
      local turning=tostring(self:IsTurning())      

      -- Info text.
      local text=string.format("%s [%d/%d]: ROE/AS=%d/%d | T/M=%d/%d | Wp=%d[%d]-->%d[%d]/%d [%s] | Life=%.1f | v=%.1f (%d) | Hdg=%03d | Ammo=%d | Detect=%s | Cargo=%.1f | Turn=%s Collision=%d IntoWind=%s",
      fsmstate, nelem, Nelem, roe, als, nTaskTot, nMissions, wpidxCurr, wpuidCurr, wpidxNext, wpuidNext, wpN, wpF, life, speed, speedEx, hdg, ammo, ndetected, cargo, turning, freepath, intowind)
      self:I(self.lid..text)
            
    end
    
  else

    -- Info text.
    local text=string.format("State %s: Alive=%s", fsmstate, tostring(self:IsAlive()))
    self:T(self.lid..text)
  
  end

  ---
  -- Recovery Windows
  ---

  if alive and self.verbose>=2 and #self.Qintowind>0 then
  
    -- Debug output:
    local text=string.format(self.lid.."Turn into wind time windows:")
  
    -- Handle case with no recoveries.
    if #self.Qintowind==0 then
      text=text.." none!"
    end  
  
    -- Loop over all slots.
    for i,_recovery in pairs(self.Qintowind) do
      local recovery=_recovery --#NAVYGROUP.IntoWind
  
      -- Get start/stop clock strings.
      local Cstart=UTILS.SecondsToClock(recovery.Tstart)
      local Cstop=UTILS.SecondsToClock(recovery.Tstop)
  
      -- Debug text.
      text=text..string.format("\n[%d] ID=%d Start=%s Stop=%s Open=%s Over=%s", i, recovery.Id, Cstart, Cstop, tostring(recovery.Open), tostring(recovery.Over))
    end
  
    -- Debug output.
    self:I(self.lid..text)
  
  end

  ---
  -- Elements
  ---

  if self.verbose>=2 then
    local text="Elements:"
    for i,_element in pairs(self.elements) do
      local element=_element --Ops.OpsGroup#OPSGROUP.Element

      local name=element.name
      local status=element.status
      local unit=element.unit
      local life,life0=self:GetLifePoints(element)

      local life0=element.life0

      -- Get ammo.
      local ammo=self:GetAmmoElement(element)

      -- Output text for element.
      text=text..string.format("\n[%d] %s: status=%s, life=%.1f/%.1f, guns=%d, rockets=%d, bombs=%d, missiles=%d, cargo=%d/%d kg",
      i, name, status, life, life0, ammo.Guns, ammo.Rockets, ammo.Bombs, ammo.Missiles, element.weightCargo, element.weightMaxCargo)
    end
    if #self.elements==0 then
      text=text.." none!"
    end
    self:I(self.lid..text)
  end

  ---
  -- Engage Detected Targets
  ---
  if self:IsCruising() and self.detectionOn and self.engagedetectedOn then

    local targetgroup, targetdist=self:_GetDetectedTarget()

    -- If we found a group, we engage it.
    if targetgroup then
      self:I(self.lid..string.format("Engaging target group %s at distance %d meters", targetgroup:GetName(), targetdist))
      self:EngageTarget(targetgroup)
    end

  end

  ---
  -- Cargo
  ---
  
  self:_CheckCargoTransport()

  ---
  -- Tasks & Missions
  ---

  self:_PrintTaskAndMissionStatus()

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- DCS Events ==> See OPSGROUP
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- See OPSGROUP!

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "ElementSpawned" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Element Element The group element.
function NAVYGROUP:onafterElementSpawned(From, Event, To, Element)
  self:T(self.lid..string.format("Element spawned %s", Element.name))

  -- Set element status.
  self:_UpdateStatus(Element, OPSGROUP.ElementStatus.SPAWNED)

end

--- On after "Spawned" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function NAVYGROUP:onafterSpawned(From, Event, To)
  self:T(self.lid..string.format("Group spawned!"))

  -- Debug info.
  if self.verbose>=1 then
    local text=string.format("Initialized Navy Group %s [GID=%d]:\n", self.groupname, self.group:GetID())
    text=text..string.format("Unit type     = %s\n", self.actype)
    text=text..string.format("Speed max    = %.1f Knots\n", UTILS.KmphToKnots(self.speedMax))
    text=text..string.format("Speed cruise = %.1f Knots\n", UTILS.KmphToKnots(self.speedCruise))
    text=text..string.format("Weight       = %.1f kg\n", self:GetWeightTotal())
    text=text..string.format("Cargo bay    = %.1f kg\n", self:GetFreeCargobay())
    text=text..string.format("Has EPLRS    = %s\n", tostring(self.isEPLRS))    
    text=text..string.format("Is Submarine = %s\n", tostring(self.isSubmarine))    
    text=text..string.format("Elements     = %d\n", #self.elements)
    text=text..string.format("Waypoints    = %d\n", #self.waypoints)
    text=text..string.format("Radio        = %.1f MHz %s %s\n", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu), tostring(self.radio.On))
    text=text..string.format("Ammo         = %d (G=%d/R=%d/M=%d/T=%d)\n", self.ammo.Total, self.ammo.Guns, self.ammo.Rockets, self.ammo.Missiles, self.ammo.Torpedos)
    text=text..string.format("FSM state    = %s\n", self:GetState())
    text=text..string.format("Is alive     = %s\n", tostring(self:IsAlive()))
    text=text..string.format("LateActivate = %s\n", tostring(self:IsLateActivated()))
    self:I(self.lid..text)
  end

  -- Update position.
  self:_UpdatePosition()
  
  -- Not dead or destroyed yet.
  self.isDead=false
  self.isDestroyed=false  

  if self.isAI then
 
    -- Set default ROE.
    self:SwitchROE(self.option.ROE)
    
    -- Set default Alarm State.
    self:SwitchAlarmstate(self.option.Alarm)
    
    -- Set emission.
    self:SwitchEmission(self.option.Emission)    
    
    -- Set default EPLRS.
    self:SwitchEPLRS(self.option.EPLRS)
    
    -- Set default Invisible.
    self:SwitchInvisible(self.option.Invisible)    

    -- Set default Immortal.
    self:SwitchImmortal(self.option.Immortal)    
    
    -- Set TACAN beacon.
    self:_SwitchTACAN()
    
    -- Turn ICLS on.
    self:_SwitchICLS()    

    -- Set radio.
    if self.radioDefault then
      -- CAREFUL: This makes DCS crash for some ships like speed boats or Higgins boats! (On a respawn for example). Looks like the command SetFrequency is causing this.
      --self:SwitchRadio()
    else
      self:SetDefaultRadio(self.radio.Freq, self.radio.Modu, false)
    end

    -- Update route.
    if #self.waypoints>1 then  
      self:__Cruise(-0.1)
    else
      self:FullStop()
    end
    
  end
  
end

--- On before "UpdateRoute" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last.
-- @param #number N Waypoint  Max waypoint index to be included in the route. Default is the final waypoint.
-- @param #number Speed Speed in knots to the next waypoint.
-- @param #number Depth Depth in meters to the next waypoint.
function NAVYGROUP:onbeforeUpdateRoute(From, Event, To, n, Speed, Depth)

  -- Is transition allowed? We assume yes until proven otherwise.
  local allowed=true
  local trepeat=nil

  if self:IsWaiting() then
    self:T(self.lid.."Update route denied. Group is WAITING!")
    return false
  elseif self:IsInUtero() then
    self:T(self.lid.."Update route denied. Group is INUTERO!")
    return false
  elseif self:IsDead() then
    self:T(self.lid.."Update route denied. Group is DEAD!")
    return false
  elseif self:IsStopped() then
    self:T(self.lid.."Update route denied. Group is STOPPED!")
    return false
  elseif self:IsHolding() then
    self:T(self.lid.."Update route denied. Group is holding position!")
    return false
  elseif self:IsEngaging() then
    self:T(self.lid.."Update route allowed. Group is engaging!")
    return true      
  end
  
  -- Check for a current task.
  if self.taskcurrent>0 then

    -- Get the current task. Must not be executing already.
    local task=self:GetTaskByID(self.taskcurrent)

    if task then
      if task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then
        -- For patrol zone, we need to allow the update as we insert new waypoints.
        self:T2(self.lid.."Allowing update route for Task: PatrolZone")
      elseif task.dcstask.id==AUFTRAG.SpecialTask.RECON then
        -- For recon missions, we need to allow the update as we insert new waypoints.
        self:T2(self.lid.."Allowing update route for Task: ReconMission")
      elseif task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then
        -- For relocate
        self:T2(self.lid.."Allowing update route for Task: Relocate Cohort")
      elseif task.dcstask.id==AUFTRAG.SpecialTask.REARMING then
        -- For rearming
        self:T2(self.lid.."Allowing update route for Task: Rearming")                
      else
        local taskname=task and task.description or "No description"
        self:T(self.lid..string.format("WARNING: Update route denied because taskcurrent=%d>0! Task description = %s", self.taskcurrent, tostring(taskname)))
        allowed=false
      end
    else
      -- Now this can happen, if we directly use TaskExecute as the task is not in the task queue and cannot be removed. Therefore, also directly executed tasks should be added to the queue!
      self:T(self.lid..string.format("WARNING: before update route taskcurrent=%d (>0!) but no task?!", self.taskcurrent))
      -- Anyhow, a task is running so we do not allow to update the route!
      allowed=false
    end
  end

  -- Not good, because mission will never start. Better only check if there is a current task!
  --if self.currentmission then
  --end

  -- Only AI flights.
  if not self.isAI then
    allowed=false
  end

  -- Debug info.
  self:T2(self.lid..string.format("Onbefore Updateroute in state %s: allowed=%s (repeat in %s)", self:GetState(), tostring(allowed), tostring(trepeat)))

  -- Try again?
  if trepeat then
    self:__UpdateRoute(trepeat, n)
  end  
  
  return allowed
end

--- On after "UpdateRoute" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number n Next waypoint index. Default is the one coming after that one that has been passed last.
-- @param #number N Waypoint  Max waypoint index to be included in the route. Default is the final waypoint.
-- @param #number Speed Speed in knots to the next waypoint.
-- @param #number Depth Depth in meters to the next waypoint.
function NAVYGROUP:onafterUpdateRoute(From, Event, To, n, N, Speed, Depth)

  -- Update route from this waypoint number onwards.
  n=n or self:GetWaypointIndexNext()
  
  -- Max index.
  N=N or #self.waypoints  
  N=math.min(N, #self.waypoints)
  

  -- Waypoints.
  local waypoints={}
  
  for i=n, N do
  
    -- Waypoint.
    local wp=UTILS.DeepCopy(self.waypoints[i])  --Ops.OpsGroup#OPSGROUP.Waypoint
    
    --env.info(string.format("FF i=%d UID=%d   n=%d, N=%d", i, wp.uid, n, N))
      
    -- Speed.
    if Speed then
      -- Take speed specified.
      wp.speed=UTILS.KnotsToMps(Speed)
    else
      -- Take default waypoint speed. But make sure speed>0 if patrol ad infinitum.
      if wp.speed<0.1 then --self.adinfinitum and 
        wp.speed=UTILS.KmphToMps(self.speedCruise)
      end
    end
    
    -- Depth.
    if Depth then
      wp.alt=-Depth
    elseif self.depth then
      wp.alt=-self.depth
    else
      -- Take default waypoint alt.
      wp.alt=wp.alt or 0
    end
    
    -- Current set speed in m/s.
    if i==n then
      self.speedWp=wp.speed
      self.altWp=wp.alt
    end
  
    -- Add waypoint.
    table.insert(waypoints, wp)
  
  end
  
  -- Current waypoint.
  local current=self:GetCoordinate():WaypointNaval(UTILS.MpsToKmph(self.speedWp), self.altWp)
  table.insert(waypoints, 1, current)  

  
  if self:IsEngaging() or not self.passedfinalwp then
  
    if self.verbose>=10 then
      for i=1,#waypoints do
        local wp=waypoints[i] --Ops.OpsGroup#OPSGROUP.Waypoint
        local text=string.format("%s Waypoint [%d] UID=%d speed=%d m/s", self.groupname, i-1, wp.uid or -1, wp.speed)
        self:I(self.lid..text)
        COORDINATE:NewFromWaypoint(wp):MarkToAll(text)            
      end
    end

    -- Debug info.
    self:T(self.lid..string.format("Updateing route: WP %d-->%d (%d/%d), Speed=%.1f knots, Depth=%d m", self.currentwp, n, #waypoints, #self.waypoints, UTILS.MpsToKnots(self.speedWp), self.altWp))

    -- Route group to all defined waypoints remaining.
    self:Route(waypoints)
    
  else
  
    ---
    -- Passed final WP ==> Full Stop
    ---
  
    self:E(self.lid..string.format("WARNING: Passed final WP ==> Full Stop!"))
    self:FullStop()
    
  end

end

--- On after "Detour" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Coordinate Coordinate where to go.
-- @param #number Speed Speed in knots. Default cruise speed.
-- @param #number Depth Depth in meters. Default 0 meters.
-- @param #number ResumeRoute If true, resume route after detour point was reached. If false, the group will stop at the detour point and wait for futher commands.
function NAVYGROUP:onafterDetour(From, Event, To, Coordinate, Speed, Depth, ResumeRoute)
    
  -- Depth for submarines.
  Depth=Depth or 0

  -- Speed in knots.
  Speed=Speed or self:GetSpeedCruise()
  
  -- ID of current waypoint.
  local uid=self:GetWaypointCurrent().uid
  
  -- Add waypoint after current.
  local wp=self:AddWaypoint(Coordinate, Speed, uid, Depth, true)
  
  -- Set if we want to resume route after reaching the detour waypoint.
  if ResumeRoute then
    wp.detour=1
  else
    wp.detour=0
  end

end

--- On after "DetourReached" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function NAVYGROUP:onafterDetourReached(From, Event, To)
  self:T(self.lid.."Group reached detour coordinate.")
end

--- On after "TurnIntoWind" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #NAVYGROUP.IntoWind Into wind parameters.
function NAVYGROUP:onafterTurnIntoWind(From, Event, To, IntoWind)

  -- Calculate heading and speed of ship.
  local heading, speed=self:GetHeadingIntoWind(IntoWind.Offset, IntoWind.Speed)
  
  IntoWind.Heading=heading
  IntoWind.Open=true
  
  -- Get coordinate.
  IntoWind.Coordinate=self:GetCoordinate(true)
  
  -- Set current into wind parameters.
  self.intowind=IntoWind

  -- Debug info.
  self:T(self.lid..string.format("Steaming into wind: Heading=%03d Speed=%.1f, Tstart=%d Tstop=%d", IntoWind.Heading, speed, IntoWind.Tstart, IntoWind.Tstop))
  
  local distance=UTILS.NMToMeters(1000)
  
  local coord=self:GetCoordinate()
  local Coord=coord:Translate(distance, IntoWind.Heading)

  -- ID of current waypoint.
  local uid=self:GetWaypointCurrent().uid
  
  local wptiw=self:AddWaypoint(Coord, speed, uid)
  wptiw.intowind=true
  
  IntoWind.waypoint=wptiw
  
  if IntoWind.Uturn and false then
    IntoWind.Coordinate:MarkToAll("Return coord")
  end
  
end

--- On before "TurnIntoWindStop" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function NAVYGROUP:onbeforeTurnIntoWindStop(From, Event, To)

  if self.intowind then
    return true
  else
    return false
  end

end

--- On after "TurnIntoWindStop" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function NAVYGROUP:onafterTurnIntoWindStop(From, Event, To)
  self:TurnIntoWindOver(self.intowind)
end

--- On after "TurnIntoWindOver" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #NAVYGROUP.IntoWind IntoWindData Data table.
function NAVYGROUP:onafterTurnIntoWindOver(From, Event, To, IntoWindData)

  if IntoWindData and self.intowind and IntoWindData.Id==self.intowind.Id then

    -- Debug message.
    self:T2(self.lid.."Turn Into Wind Over!")
  
    -- Window over and not open anymore.
    self.intowind.Over=true
    self.intowind.Open=false
    
    -- Remove additional waypoint.
    self:RemoveWaypointByID(self.intowind.waypoint.uid)
  
    if self.intowind.Uturn then

      ---
      -- U-turn ==> Go to coordinate where we left the route.
      ---
    
      -- Detour to where we left the route.
      self:T(self.lid.."FF Turn Into Wind Over ==> Uturn!")

      -- ID of current waypoint.
      local uid=self:GetWaypointCurrent().uid
  
      -- Add temp waypoint.
      local wp=self:AddWaypoint(self.intowind.Coordinate, self:GetSpeedCruise(), uid) ; wp.temp=true

    else
    
      ---
      -- Go directly to next waypoint.
      ---
    
      -- Next waypoint index and speed.
      local indx=self:GetWaypointIndexNext()
      local speed=self:GetSpeedToWaypoint(indx)
      
      -- Update route.
      self:T(self.lid..string.format("FF Turn Into Wind Over ==> Next WP Index=%d at %.1f knots via update route!", indx, speed))
      self:__UpdateRoute(-1, indx, nil, speed)
      
    end
    
    -- Set current window to nil.
    self.intowind=nil
    
    -- Remove window from queue.
    self:RemoveTurnIntoWind(IntoWindData)

  end

end

--- On after "FullStop" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function NAVYGROUP:onafterFullStop(From, Event, To)
  self:T(self.lid.."Full stop ==> holding")

  -- Get current position.
  local pos=self:GetCoordinate()
  
  -- Create a new waypoint.
  local wp=pos:WaypointNaval(0)
  
  -- Create new route consisting of only this position ==> Stop!
  self:Route({wp})

end

--- On after "Cruise" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number Speed Speed in knots until next waypoint is reached. Default is speed set for waypoint.
function NAVYGROUP:onafterCruise(From, Event, To, Speed)

  -- Not waiting anymore.
  self.Twaiting=nil
  self.dTwait=nil

  -- No set depth.
  self.depth=nil

  self:__UpdateRoute(-0.1, nil, nil, Speed)

end

--- On after "Dive" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number Depth Dive depth in meters. Default 50 meters.
-- @param #number Speed Speed in knots until next waypoint is reached.
function NAVYGROUP:onafterDive(From, Event, To, Depth, Speed)

  Depth=Depth or 50

  self:I(self.lid..string.format("Diving to %d meters", Depth))
  
  self.depth=Depth
  
  self:__UpdateRoute(-1, nil, nil, Speed)

end

--- On after "Surface" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number Speed Speed in knots until next waypoint is reached.
function NAVYGROUP:onafterSurface(From, Event, To, Speed)

  self.depth=0

  self:__UpdateRoute(-1, nil, nil, Speed)

end

--- On after "TurningStarted" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function NAVYGROUP:onafterTurningStarted(From, Event, To)
  self.turning=true
end

--- On after "TurningStarted" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function NAVYGROUP:onafterTurningStopped(From, Event, To)
  self.turning=false
  self.collisionwarning=false
  
  if self:IsSteamingIntoWind() then
    self:TurnedIntoWind()
  end
  
end

--- On after "CollisionWarning" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number Distance Distance in meters where obstacle was detected.
function NAVYGROUP:onafterCollisionWarning(From, Event, To, Distance)
  self:T(self.lid..string.format("Iceberg ahead in %d meters!", Distance or -1))
  self.collisionwarning=true
end

--- On after "EngageTarget" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Group#GROUP Group the group to be engaged.
function NAVYGROUP:onafterEngageTarget(From, Event, To, Target)
  self:T(self.lid.."Engaging Target")

  if Target:IsInstanceOf("TARGET") then
    self.engage.Target=Target
  else
    self.engage.Target=TARGET:New(Target)
  end
 
  -- Target coordinate.
  self.engage.Coordinate=UTILS.DeepCopy(self.engage.Target:GetCoordinate()) 
 
  
  local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9)


  
  -- Backup ROE and alarm state.
  self.engage.roe=self:GetROE()
  self.engage.alarmstate=self:GetAlarmstate()
  
  -- Switch ROE and alarm state.
  self:SwitchAlarmstate(ENUMS.AlarmState.Auto)
  self:SwitchROE(ENUMS.ROE.OpenFire)

  -- ID of current waypoint.
  local uid=self:GetWaypointCurrent().uid
  
  -- Add waypoint after current.
  self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true)
  
  -- Set if we want to resume route after reaching the detour waypoint.
  self.engage.Waypoint.detour=1

end

--- Update engage target.
-- @param #NAVYGROUP self
function NAVYGROUP:_UpdateEngageTarget()

  if self.engage.Target and self.engage.Target:IsAlive() then

    -- Get current position vector.
    local vec3=self.engage.Target:GetVec3()
    
    if vec3 then
  
      -- Distance to last known position of target.
      local dist=UTILS.VecDist3D(vec3, self.engage.Coordinate:GetVec3())
      
      -- Check if target moved more than 100 meters.
      if dist>100 then
      
        --env.info("FF Update Engage Target Moved "..self.engage.Target:GetName())
      
        -- Update new position.
        self.engage.Coordinate:UpdateFromVec3(vec3)
  
        -- ID of current waypoint.
        local uid=self:GetWaypointCurrent().uid
      
        -- Remove current waypoint
        self:RemoveWaypointByID(self.engage.Waypoint.uid)
        
        local intercoord=self:GetCoordinate():GetIntermediateCoordinate(self.engage.Coordinate, 0.9)
    
          -- Add waypoint after current.
        self.engage.Waypoint=self:AddWaypoint(intercoord, nil, uid, Formation, true)
      
        -- Set if we want to resume route after reaching the detour waypoint.
        self.engage.Waypoint.detour=0      
      
      end
      
    else

      -- Could not get position of target (not alive any more?) ==> Disengage.
      self:Disengage()
    
    end
    
  else
  
    -- Target not alive any more ==> Disengage.
    self:Disengage()
    
  end

end

--- On after "Disengage" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function NAVYGROUP:onafterDisengage(From, Event, To)
  self:T(self.lid.."Disengage Target")

  -- Restore previous ROE and alarm state.
  self:SwitchROE(self.engage.roe)
  self:SwitchAlarmstate(self.engage.alarmstate)

  -- Get current task
  local task=self:GetTaskCurrent()

  -- Get if current task is ground attack.
  if task and task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK then
    self:T(self.lid.."Disengage with current task GROUNDATTACK ==> Task Done!")
    self:TaskDone(task)
  end    
  
  -- Remove current waypoint
  if self.engage.Waypoint then
    self:RemoveWaypointByID(self.engage.Waypoint.uid)
  end

  -- Check group is done
  self:_CheckGroupDone(1)
end

--- On after "OutOfAmmo" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function NAVYGROUP:onafterOutOfAmmo(From, Event, To)
  self:T(self.lid..string.format("Group is out of ammo at t=%.3f", timer.getTime()))
  
  -- Check if we want to retreat once out of ammo.
  if self.retreatOnOutOfAmmo then
    self:__Retreat(-1)
    return
  end
  
  -- Third, check if we want to RTZ once out of ammo.
  if self.rtzOnOutOfAmmo then
    self:__RTZ(-1)
  end

  -- Get current task.
  local task=self:GetTaskCurrent()
  
  if task then
    if task.dcstask.id=="FireAtPoint" or task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then
      self:T(self.lid..string.format("Cancelling current %s task because out of ammo!", task.dcstask.id))
      self:TaskCancel(task)
    end
  end
    
end

--- On after "RTZ" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE Zone The zone to return to.
-- @param #number Formation Formation of the group.
function NAVYGROUP:onafterRTZ(From, Event, To, Zone, Formation)
  
  -- Zone.
  local zone=Zone or self.homezone
  
  -- Cancel all missions in the queue.
  self:CancelAllMissions()
  
  if zone then
  
    if self:IsInZone(zone) then
      self:Returned()
    else
  
      -- Debug info.
      self:T(self.lid..string.format("RTZ to Zone %s", zone:GetName()))  
      
      local Coordinate=zone:GetRandomCoordinate()

      -- ID of current waypoint.
      local uid=self:GetWaypointCurrentUID()
      
      -- Add waypoint after current.
      local wp=self:AddWaypoint(Coordinate, nil, uid, Formation, true)
      
      -- Set if we want to resume route after reaching the detour waypoint.
      wp.detour=0
      
    end
        
  else
    self:T(self.lid.."ERROR: No RTZ zone given!")
  end

end


--- On after "Returned" event.
-- @param #NAVYGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function NAVYGROUP:onafterReturned(From, Event, To)

  -- Debug info.
  self:T(self.lid..string.format("Group returned"))
  
  if self.legion then
    -- Debug info.
    self:T(self.lid..string.format("Adding group back to warehouse stock"))
    
    -- Add asset back in 10 seconds.
    self.legion:__AddAsset(10, self.group, 1)
  end

end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Routing
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Add an a waypoint to the route.
-- @param #NAVYGROUP self
-- @param Core.Point#COORDINATE Coordinate The coordinate of the waypoint. Use `COORDINATE:SetAltitude()` to define the altitude.
-- @param #number Speed Speed in knots. Default is default cruise speed or 70% of max speed.
-- @param #number AfterWaypointWithID Insert waypoint after waypoint given ID. Default is to insert as last waypoint.
-- @param #number Depth Depth at waypoint in feet. Only for submarines.
-- @param #boolean Updateroute If true or nil, call UpdateRoute. If false, no call.
-- @return Ops.OpsGroup#OPSGROUP.Waypoint Waypoint table.
function NAVYGROUP:AddWaypoint(Coordinate, Speed, AfterWaypointWithID, Depth, Updateroute)

  -- Create coordinate.
  local coordinate=self:_CoordinateFromObject(Coordinate)  
  
  -- Set waypoint index.
  local wpnumber=self:GetWaypointIndexAfterID(AfterWaypointWithID)

  -- Speed in knots.
  Speed=Speed or self:GetSpeedCruise()

  -- Create a Naval waypoint.
  local wp=coordinate:WaypointNaval(UTILS.KnotsToKmph(Speed), Depth)

  -- Create waypoint data table.
  local waypoint=self:_CreateWaypoint(wp)

  -- Set altitude.
  if Depth then
    waypoint.alt=UTILS.FeetToMeters(Depth)
  end

  -- Add waypoint to table.
  self:_AddWaypoint(waypoint, wpnumber)
  
  -- Debug info.
  self:T(self.lid..string.format("Adding NAVAL waypoint index=%d uid=%d, speed=%.1f knots. Last waypoint passed was #%d. Total waypoints #%d", wpnumber, waypoint.uid, Speed, self.currentwp, #self.waypoints))

  -- Update route.
  if Updateroute==nil or Updateroute==true then
    self:__UpdateRoute(-0.01)
  end
  
  return waypoint
end

--- Initialize group parameters. Also initializes waypoints if self.waypoints is nil.
-- @param #NAVYGROUP self
-- @param #table Template Template used to init the group. Default is `self.template`.
-- @param #number Delay Delay in seconds before group is initialized. Default `nil`, *i.e.* instantaneous. 
-- @return #NAVYGROUP self
function NAVYGROUP:_InitGroup(Template, Delay)

  if Delay and Delay>0 then
    -- Delayed call
    self:ScheduleOnce(Delay, NAVYGROUP._InitGroup, self, Template, 0)
  else
  
    -- First check if group was already initialized.
    if self.groupinitialized then
      self:T(self.lid.."WARNING: Group was already initialized! Will NOT do it again!")
      return
    end
  
    -- Get template of group.
    local template=Template or self:_GetTemplate()
  
    -- Ships are always AI.
    self.isAI=true
    
    -- Is (template) group late activated.
    self.isLateActivated=template.lateActivation
    
    -- Naval groups cannot be uncontrolled.
    self.isUncontrolled=false
    
    -- Max speed in km/h.
    self.speedMax=self.group:GetSpeedMax()
    
    -- Is group mobile?
    if self.speedMax and self.speedMax>3.6 then
      self.isMobile=true
    else
      self.isMobile=false
      self.speedMax = 0
    end  
    
    -- Cruise speed: 70% of max speed.
    self.speedCruise=self.speedMax*0.7
    
    -- Group ammo.
    self.ammo=self:GetAmmoTot()
    
    -- Radio parameters from template. Default is set on spawn if not modified by the user.
    self.radio.On=true  -- Radio is always on for ships.
    self.radio.Freq=tonumber(template.units[1].frequency)/1000000
    self.radio.Modu=tonumber(template.units[1].modulation)
    
    -- Set default formation. No really applicable for ships.
    self.optionDefault.Formation="Off Road"
    self.option.Formation=self.optionDefault.Formation
  
    -- Default TACAN off (we check if something is set already to keep those values in case of respawn)
    if not self.tacanDefault then
      self:SetDefaultTACAN(nil, nil, nil, nil, true)
    end
    if not self.tacan then
      self.tacan=UTILS.DeepCopy(self.tacanDefault)
    end
    
    -- Default ICLS off.
    if not self.iclsDefault then
      self:SetDefaultICLS(nil, nil, nil, true)
    end
    if not self.icls then
      self.icls=UTILS.DeepCopy(self.iclsDefault)
    end
    
    -- Get all units of the group.
    local units=self.group:GetUnits()
  
    -- DCS group.
    local dcsgroup=Group.getByName(self.groupname)
    local size0=dcsgroup:getInitialSize()
    
    -- Quick check.
    if #units~=size0 then
      self:E(self.lid..string.format("ERROR: Got #units=%d but group consists of %d units!", #units, size0))
    end
    
    -- Add elemets.
    for _,unit in pairs(units) do
      self:_AddElementByName(unit:GetName())
    end
    
    -- Init done.
    self.groupinitialized=true
  end
  
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Option Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------



-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check for possible collisions between two coordinates.
-- @param #NAVYGROUP self
-- @param #number DistanceMax Max distance in meters ahead to check. Default 5000.
-- @param #number dx
-- @return #number Free distance in meters.
function NAVYGROUP:_CheckFreePath(DistanceMax, dx)

  local distance=DistanceMax or 5000
  local dx=dx or 100

  -- If the group is turning, we cannot really tell anything about a possible collision.
  if self:IsTurning() then
    return distance
  end
  
  -- Offset above sea level.
  local offsetY=0.1
  
  -- Current bug on Caucasus. LoS returns false.
  if UTILS.GetDCSMap()==DCSMAP.Caucasus then
    offsetY=5.01
  end
  
  local vec3=self:GetVec3()
  vec3.y=offsetY
  
  -- Current heading.
  local heading=self:GetHeading()
  
  local function LoS(dist)
    local checkvec3=UTILS.VecTranslate(vec3, dist, heading)
    local los=land.isVisible(vec3, checkvec3)
    return los
  end

  -- First check if everything is clear.
  if LoS(DistanceMax) then
    return DistanceMax
  end
  
  local function check()
  
    local xmin=0
    local xmax=DistanceMax
    
    local Nmax=100
    local eps=100

    local N=1
    while N<=Nmax do
    
      local d=xmax-xmin
      local x=xmin+d/2
      
      local los=LoS(x)
      
      -- Debug message.
      self:T(self.lid..string.format("N=%d: xmin=%.1f xmax=%.1f x=%.1f d=%.3f los=%s", N, xmin, xmax, x, d, tostring(los)))
      
      if los and d<=eps then
        return x
      end
      
      if los then
        xmin=x
      else
        xmax=x
      end
    
      N=N+1
    end
  
    return 0
  end

  local _check=check()

  return _check
end

--- Check if group is turning.
-- @param #NAVYGROUP self
function NAVYGROUP:_CheckTurning()

  local unit=self.group:GetUnit(1)
  
  if unit and unit:IsAlive() then

    -- Current orientation of carrier.
    local vNew=self.orientX --unit:GetOrientationX()
  
    -- Last orientation from 30 seconds ago.
    local vLast=self.orientXLast
  
    -- We only need the X-Z plane.
    vNew.y=0 ; vLast.y=0
  
    -- Angle between current heading and last time we checked ~30 seconds ago.
    local deltaLast=math.deg(math.acos(UTILS.VecDot(vNew,vLast)/UTILS.VecNorm(vNew)/UTILS.VecNorm(vLast)))
  
    -- Carrier is turning when its heading changed by at least two degrees since last check.
    local turning=math.abs(deltaLast)>=2
  
    -- Check if turning stopped.
    if self.turning and not turning then
  
      -- Carrier was turning but is not any more.
      self:TurningStopped()
      
    elseif turning and not self.turning then
  
      -- Carrier was not turning but is now.
      self:TurningStarted()    
  
    end
  
    -- Update turning.
    self.turning=turning
    
  end
  
end


--- Check queued turns into wind.
-- @param #NAVYGROUP self
function NAVYGROUP:_CheckTurnsIntoWind()

  -- Get current abs time.
  local time=timer.getAbsTime()

  if self.intowind then

    -- Check if time is over.
    if time>=self.intowind.Tstop then    
      self:TurnIntoWindOver(self.intowind)
    end
  
  else
  
    -- Get next window.
    local IntoWind=self:GetTurnIntoWindNext()

    -- Start turn into wind.
    if IntoWind then
      self:TurnIntoWind(IntoWind)
    end
    
  end
  
end

--- Get the next turn into wind window, which is not yet running.
-- @param #NAVYGROUP self
-- @return #NAVYGROUP.IntoWind Next into wind data. Could be `nil` if there is not next window.
function NAVYGROUP:GetTurnIntoWindNext()

  if #self.Qintowind>0 then

    -- Get current abs time.
    local time=timer.getAbsTime()
  
    -- Sort windows wrt to start time.
    table.sort(self.Qintowind, function(a, b) return a.Tstart<b.Tstart end)
  
    -- Loop over all slots.
    for _,_recovery in pairs(self.Qintowind) do
      local recovery=_recovery --#NAVYGROUP.IntoWind
  
      if time>=recovery.Tstart and time<recovery.Tstop and not (recovery.Open or recovery.Over) then
        return recovery
      end
      
    end    
  end

  return nil
end

--- Get the turn into wind window, which is currently open. 
-- @param #NAVYGROUP self
-- @return #NAVYGROUP.IntoWind Current into wind data. Could be `nil` if there is no window currenly open.
function NAVYGROUP:GetTurnIntoWindCurrent()
  return self.intowind
end

--- Get wind direction and speed at current position.
-- @param #NAVYGROUP self
-- @param #number Altitude Altitude in meters above main sea level at which the wind is calculated. Default 18 meters.
-- @return #number Direction the wind is blowing **from** in degrees.
-- @return #number Wind speed in m/s.
function NAVYGROUP:GetWind(Altitude)

  -- Current position of the carrier or input.
  local coord=self:GetCoordinate()

  -- Wind direction and speed. By default at 18 meters ASL.
  local Wdir, Wspeed=coord:GetWind(Altitude or 18)

  return Wdir, Wspeed
end

--- Get heading of group into the wind.
-- @param #NAVYGROUP self
-- @param #number Offset Offset angle in degrees, e.g. to account for an angled runway.
-- @param #number vdeck Desired wind speed on deck in Knots.
-- @return #number Carrier heading in degrees.
-- @return #number Carrier speed in knots.
function NAVYGROUP:GetHeadingIntoWind_old(Offset, vdeck)

  local function adjustDegreesForWindSpeed(windSpeed)
    local degreesAdjustment = 0
    -- the windspeeds are in m/s    
    -- +0 degrees at 15m/s = 37kts
    -- +0 degrees at 14m/s = 35kts
    -- +0 degrees at 13m/s = 33kts
    -- +4 degrees at 12m/s = 31kts
    -- +4 degrees at 11m/s = 29kts
    -- +4 degrees at 10m/s = 27kts
    -- +4 degrees at 9m/s = 27kts
    -- +4 degrees at 8m/s = 27kts
    -- +8 degrees at 7m/s = 27kts
    -- +8 degrees at 6m/s = 27kts
    -- +8 degrees at 5m/s = 26kts
    -- +20 degrees at 4m/s = 26kts
    -- +20 degrees at 3m/s = 26kts
    -- +30 degrees at 2m/s = 26kts 1s
  
    if windSpeed > 0 and windSpeed < 3 then
      degreesAdjustment = 30
    elseif windSpeed >= 3 and windSpeed < 5 then
      degreesAdjustment = 20
    elseif windSpeed >= 5 and windSpeed < 8 then
      degreesAdjustment = 8
    elseif windSpeed >= 8 and windSpeed < 13 then
      degreesAdjustment = 4
    elseif windSpeed >= 13 then
      degreesAdjustment = 0
    end
  
    return degreesAdjustment
  end

  Offset=Offset or 0

  -- Get direction the wind is blowing from. This is where we want to go.
  local windfrom, vwind=self:GetWind()

  -- Actually, we want the runway in the wind.
  local intowind = windfrom - Offset + adjustDegreesForWindSpeed(vwind)

  -- If no wind, take current heading.
  if vwind<0.1 then
    intowind=self:GetHeading()
  end
  
  -- Adjust negative values.
  if intowind<0 then
    intowind=intowind+360
  end

  -- Speed of carrier in m/s but at least 4 knots.
  local vtot = math.max(vdeck-UTILS.MpsToKnots(vwind), 4)
  
  return intowind, vtot
end


--- Get heading of group into the wind. This minimizes the cross wind for an angled runway.
-- Implementation based on [Mags & Bami](https://magwo.github.io/carrier-cruise/) work.
-- @param #NAVYGROUP self
-- @param #number Offset Offset angle in degrees, e.g. to account for an angled runway.
-- @param #number vdeck Desired wind speed on deck in Knots.
-- @return #number Carrier heading in degrees.
-- @return #number Carrier speed in knots.
function NAVYGROUP:GetHeadingIntoWind_new(Offset, vdeck)

  -- Default offset angle.
  Offset=Offset or 0

  -- Get direction the wind is blowing from.
  local windfrom, vwind=self:GetWind(18)
  
  -- Convert wind speed to knots.
  vwind=UTILS.MpsToKnots(vwind)
  
  -- Wind to in knots.
  local windto=(windfrom+180)%360
  
  -- Offset angle in rad. We also define the rotation to be clock-wise, which requires a minus sign.
  local alpha=math.rad(-Offset)
  
  -- Ships min/max speed.
  local Vmin=4
  local Vmax=UTILS.KmphToKnots(self.speedMax)
  
  -- Constant.
  local C = math.sqrt(math.cos(alpha)^2 / math.sin(alpha)^2 + 1)
  

  -- Upper limit of desired speed due to max boat speed.
  local vdeckMax=vwind + math.cos(alpha) * Vmax
  
  -- Lower limit of desired speed due to min boat speed.
  local vdeckMin=vwind + math.cos(alpha) * Vmin
  
  
  -- Speed of ship so it matches the desired speed.
  local v=0
  
  -- Angle wrt. to wind TO-direction 
  local theta=0

  if vdeck>vdeckMax then
    -- Boat cannot go fast enough
    
    -- Set max speed.
    v=Vmax
    
    -- Calculate theta.
    theta = math.asin(v/(vwind*C)) - math.asin(-1/C)
  
  elseif vdeck<vdeckMin then
    -- Boat cannot go slow enought
  
    -- Set min speed.
    v=Vmin
    
    -- Calculatge theta.
    theta = math.asin(v/(vwind*C)) - math.asin(-1/C)
  
  elseif vdeck*math.sin(alpha)>vwind then
    -- Too little wind
    
    -- Set theta to 90°
    theta=math.pi/2
    
    -- Set speed.
    v = math.sqrt(vdeck^2 - vwind^2)
  
  else
    -- Normal case
    theta = math.asin(vdeck * math.sin(alpha) / vwind)
    v = vdeck * math.cos(alpha) - vwind * math.cos(theta)
  end
  
  
  -- Ship heading so cross wind is min for the given wind.
  local intowind = (540 + (windto + math.deg(theta) )) % 360
  
  -- Debug info.
  self:T(self.lid..string.format("Heading into Wind: vship=%.1f, vwind=%.1f, WindTo=%03d°, Theta=%03d°, Heading=%03d", v, vwind, windto, theta, intowind))
  
  return intowind, v
end

--- Get heading of group into the wind. This minimizes the cross wind for an angled runway.
-- Implementation based on [Mags & Bami](https://magwo.github.io/carrier-cruise/) work.
-- @param #NAVYGROUP self
-- @param #number Offset Offset angle in degrees, e.g. to account for an angled runway.
-- @param #number vdeck Desired wind speed on deck in Knots.
-- @return #number Carrier heading in degrees.
-- @return #number Carrier speed in knots.
function NAVYGROUP:GetHeadingIntoWind(Offset, vdeck)

  if self.intowindold then
    --env.info("FF use OLD into wind")
    return self:GetHeadingIntoWind_old(Offset, vdeck)
  else
    --env.info("FF use NEW into wind")
    return self:GetHeadingIntoWind_new(Offset, vdeck)
  end

end


--- Find free path to next waypoint.
-- @param #NAVYGROUP self
-- @return #boolean If true, a path was found.
function NAVYGROUP:_FindPathToNextWaypoint()
  self:T3(self.lid.."Path finding")
  
  --TODO: Do not create a new ASTAR object each time this function is called but make it self.astar and reuse. Should be better for performance.

  -- Pathfinding A*
  local astar=ASTAR:New()
  
  -- Current positon of the group.
  local position=self:GetCoordinate()
  
  -- Next waypoint.
  local wpnext=self:GetWaypointNext()
  
  -- No next waypoint.
  if wpnext==nil then
    return
  end
  
  -- Next waypoint coordinate.
  local nextwp=wpnext.coordinate
  
  -- If we are currently turning into the wind...
  if wpnext.intowind then
    local hdg=self:GetHeading()
    nextwp=position:Translate(UTILS.NMToMeters(20), hdg, true)
  end
  
  local speed=UTILS.MpsToKnots(wpnext.speed)

  -- Set start coordinate.    
  astar:SetStartCoordinate(position)
  
  -- Set end coordinate.
  astar:SetEndCoordinate(nextwp)

  -- Distance to next waypoint.
  local dist=position:Get2DDistance(nextwp)
  
  -- Check distance >= 5 meters.
  if dist<5 then
    return
  end
  
  local boxwidth=dist*2
  local spacex=dist*0.1
  local delta=dist/10
  
  -- Create a grid of nodes. We only want nodes of surface type water.
  astar:CreateGrid({land.SurfaceType.WATER}, boxwidth, spacex, delta, delta, self.verbose>10)
  
  -- Valid neighbour nodes need to have line of sight.
  astar:SetValidNeighbourLoS(self.pathCorridor)
  
  --- Function to find a path and add waypoints to the group.
  local function findpath()
  
    -- Calculate path from start to end node.
    local path=astar:GetPath(true, true)
    
    if path then

      -- Loop over nodes in found path.
      local uid=self:GetWaypointCurrent().uid -- ID of current waypoint.
      
      for i,_node in ipairs(path) do
        local node=_node --Core.Astar#ASTAR.Node
          
        -- Add waypoints along detour path to next waypoint.
        local wp=self:AddWaypoint(node.coordinate, speed, uid)
        wp.astar=true
        
        -- Update id so the next wp is added after this one.
        uid=wp.uid

        -- Debug: smoke and mark path.
        if self.verbose>=10 then
          node.coordinate:MarkToAll(string.format("Path node #%d", i))
        end
        
      end
      
      return #path>0 
    else
      return false
    end
    
  end

  -- Return if path was found.
  return findpath()
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Operation with multiple phases.
--
-- ## Main Features:
--
--    * Define operation phases
--    * Define conditions when phases are over
--    * Option to have branches in the phase tree
--    * Dedicate resources to operations
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/Operation).
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ===
-- @module Ops.Operation
-- @image OPS_Operation.png


--- OPERATION class.
-- @type OPERATION
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #number uid Unique ID of the operation.
-- @field #string lid Class id string for output to DCS log file.
-- @field #string name Name of the operation.
-- @field #number Tstart Start time in seconds of abs mission time.
-- @field #number Tstop Stop time in seconds of abs mission time.
-- @field #number duration Duration of the operation in seconds.
-- @field Core.Condition#CONDITION conditionStart Start condition.
-- @field Core.Condition#CONDITION conditionOver Over condition.
-- @field #table branches Branches.
-- @field #OPERATION.Branch branchMaster Master branch.
-- @field #OPERATION.Branch branchActive Active branch.
-- @field #number counterPhase Running number counting the phases.
-- @field #number counterBranch Running number counting the branches.
-- @field #OPERATION.Phase phase Currently active phase (if any).
-- @field #OPERATION.Phase phaseLast The phase that was active before the current one.
-- @field #table cohorts Dedicated cohorts.
-- @field #table legions Dedicated legions.
-- @field #table targets Targets.
-- @field #table missions Missions.
-- @extends Core.Fsm#FSM

--- *Before this time tomorrow I shall have gained a peerage, or Westminster Abbey.* -- Horatio Nelson
--
-- ===
--
-- # The OPERATION Concept
--
-- This class allows you to create complex operations, which consist of multiple phases. Conditions can be specified, when a phase is over. If a phase is over, the next phase is started.
-- FSM events can be used to customize code that is executed at each phase. Phases can also switched manually, of course.
-- 
-- In the simplest case, adding phases leads to a linear chain. However, you can also create branches to contruct a more tree like structure of phases. You can switch between branches 
-- manually or add "edges" with conditions when to switch branches. We are diving a bit into graph theory here. So don't feel embarrassed at all, if you stick to linear chains.
-- 
-- # Constructor
-- 
-- A new operation can be created with the @{#OPERATION.New}(*Name*) function, where the parameter `Name` is a free to choose string.
-- 
-- ## Adding Phases
-- 
-- You can add phases with the  @{#OPERATION.AddPhase}(*Name*, *Branch*) function. The first parameter `Name` is the name of the phase. The second parameter `Branch` is the branch to which the phase is
-- added. If this is omitted (nil), the phase is added to the default, *i.e.* "master branch". More about adding branches later.
-- 
-- 
--
--
-- @field #OPERATION
OPERATION = {
  ClassName          = "OPERATION",
  verbose            =     0,
  branches           =    {},
  counterPhase       =     0,
  counterBranch      =     0,
  counterEdge        =     0,
  cohorts            =    {},
  legions            =    {},  
  targets            =    {},
  missions           =    {},
}

--- Global mission counter.
_OPERATIONID=0

--- Operation phase.
-- @type OPERATION.Phase
-- @field #number uid Unique ID of the phase.
-- @field #string name Name of the phase.
-- @field Core.Condition#CONDITION conditionOver Conditions when the phase is over.
-- @field #string status Phase status.
-- @field #number Tstart Abs. mission time when the phase was started.
-- @field #number nActive Number of times the phase was active.
-- @field #number duration Duration in seconds how long the phase should be active after it started.
-- @field #OPERATION.Branch branch The branch this phase belongs to.

--- Operation branch.
-- @type OPERATION.Branch
-- @field #number uid Unique ID of the branch.
-- @field #string name Name of the branch.
-- @field #table phases Phases of this branch.
-- @field #table edges Edges of this branch.

--- Operation edge.
-- @type OPERATION.Edge
-- @field #number uid Unique ID of the edge.
-- @field #OPERATION.Branch branchFrom The from branch.
-- @field #OPERATION.Phase phaseFrom The from phase after which to switch.
-- @field #OPERATION.Branch branchTo The branch to switch to.
-- @field #OPERATION.Phase phaseTo The phase to switch to.
-- @field Core.Condition#CONDITION conditionSwitch Conditions when to switch the branch.

--- Operation phase.
-- @type OPERATION.PhaseStatus
-- @field #string PLANNED Planned.
-- @field #string ACTIVE Active phase.
-- @field #string OVER Phase is over.
OPERATION.PhaseStatus={
  PLANNED="Planned",
  ACTIVE="Active",
  OVER="Over",
}

--- OPERATION class version.
-- @field #string version
OPERATION.version="0.2.0"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: "Over" conditions.
-- TODO: Repeat phases: after over ==> planned (not over)
-- DONE: Branches.
-- DONE: Phases.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new generic OPERATION object.
-- @param #OPERATION self
-- @param #string Name Name of the operation. Be creative! Default "Operation-01" where the last number is a running number.
-- @return #OPERATION self
function OPERATION:New(Name)

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #OPERATION

  -- Increase global counter.
  _OPERATIONID=_OPERATIONID+1
  
  -- Unique ID of the operation.
  self.uid=_OPERATIONID
  
  -- Set Name.
  self.name=Name or string.format("Operation-%02d", _OPERATIONID)
  
  -- Set log ID.
  self.lid=string.format("%s | ",self.name)
  
  -- FMS start state is PLANNED.
  self:SetStartState("Planned")
  
  -- Master branch.
  self.branchMaster=self:AddBranch("Master")
  
  self.conditionStart=CONDITION:New("Operation %s start", self.name)
  self.conditionStart:SetNoneResult(false) --If no condition function is specified, the ops will NOT be over.
  self.conditionStart:SetDefaultPersistence(false)
  
  self.conditionOver=CONDITION:New("Operation %s over", self.name)
  self.conditionOver:SetNoneResult(false)
  self.conditionOver:SetDefaultPersistence(false)
  
  
  -- Set master as active branch.
  self.branchActive=self.branchMaster

  -- Add FSM transitions.
  --                  From State     -->        Event            -->        To State
  self:AddTransition("*",                      "Start",                    "Running")
  
  self:AddTransition("*",                      "StatusUpdate",             "*")  
  
  self:AddTransition("Running",                "Pause",                    "Paused")
  self:AddTransition("Paused",                 "Unpause",                  "Running")
  
  self:AddTransition("*",                      "PhaseOver",                "*")
  self:AddTransition("*",                      "PhaseNext",                "*")
  self:AddTransition("*",                      "PhaseChange",              "*")
  
  self:AddTransition("*",                      "BranchSwitch",             "*")
  
  self:AddTransition("*",                      "Over",                     "Over")
  
  self:AddTransition("*",                      "Stop",                     "Stopped")
  

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start".
  -- @function [parent=#OPERATION] Start
  -- @param #OPERATION self

  --- Triggers the FSM event "Start" after a delay.
  -- @function [parent=#OPERATION] __Start
  -- @param #OPERATION self
  -- @param #number delay Delay in seconds.

  --- On after "Start" event.
  -- @function [parent=#OPERATION] OnAfterStart
  -- @param #OPERATION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- Triggers the FSM event "Stop".
  -- @function [parent=#OPERATION] Stop
  -- @param #OPERATION self

  --- Triggers the FSM event "Stop" after a delay.
  -- @function [parent=#OPERATION] __Stop
  -- @param #OPERATION self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "StatusUpdate".
  -- @function [parent=#OPERATION] StatusUpdate
  -- @param #OPERATION self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#OPERATION] __StatusUpdate
  -- @param #OPERATION self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "PhaseChange".
  -- @function [parent=#OPERATION] PhaseChange
  -- @param #OPERATION self
  -- @param #OPERATION.Phase Phase The new phase.

  --- Triggers the FSM event "PhaseChange" after a delay.
  -- @function [parent=#OPERATION] __PhaseChange
  -- @param #OPERATION self
  -- @param #number delay Delay in seconds.
  -- @param #OPERATION.Phase Phase The new phase.

  --- On after "PhaseChange" event.
  -- @function [parent=#OPERATION] OnAfterPhaseChange
  -- @param #OPERATION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #OPERATION.Phase Phase The new phase.


  --- Triggers the FSM event "PhaseNext".
  -- @function [parent=#OPERATION] PhaseNext
  -- @param #OPERATION self

  --- Triggers the FSM event "PhaseNext" after a delay.
  -- @function [parent=#OPERATION] __PhaseNext
  -- @param #OPERATION self
  -- @param #number delay Delay in seconds.

  --- On after "PhaseNext" event.
  -- @function [parent=#OPERATION] OnAfterPhaseNext
  -- @param #OPERATION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "PhaseOver".
  -- @function [parent=#OPERATION] PhaseOver
  -- @param #OPERATION self
  -- @param #OPERATION.Phase Phase The phase that is over.

  --- Triggers the FSM event "PhaseOver" after a delay.
  -- @function [parent=#OPERATION] __PhaseOver
  -- @param #OPERATION self
  -- @param #number delay Delay in seconds.
  -- @param #OPERATION.Phase Phase The phase that is over.

  --- On after "PhaseOver" event.
  -- @function [parent=#OPERATION] OnAfterPhaseOver
  -- @param #OPERATION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #OPERATION.Phase Phase The phase that is over.


  --- Triggers the FSM event "BranchSwitch".
  -- @function [parent=#OPERATION] BranchSwitch
  -- @param #OPERATION self
  -- @param #OPERATION.Branch Branch The branch that is now active.
  -- @param #OPERATION.Phase Phase The new phase.

  --- Triggers the FSM event "BranchSwitch" after a delay.
  -- @function [parent=#OPERATION] __BranchSwitch
  -- @param #OPERATION self
  -- @param #number delay Delay in seconds.
  -- @param #OPERATION.Branch Branch The branch that is now active.
  -- @param #OPERATION.Phase Phase The new phase.

  --- On after "BranchSwitch" event.
  -- @function [parent=#OPERATION] OnAfterBranchSwitch
  -- @param #OPERATION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #OPERATION.Branch Branch The branch that is now active.
  -- @param #OPERATION.Phase Phase The new phase.

  --- Triggers the FSM event "Over".
  -- @function [parent=#OPERATION] Over
  -- @param #OPERATION self

  --- Triggers the FSM event "Over" after a delay.
  -- @function [parent=#OPERATION] __Over
  -- @param #OPERATION self
  -- @param #number delay Delay in seconds.

  --- On after "Over" event.
  -- @function [parent=#OPERATION] OnAfterOver
  -- @param #OPERATION self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  -- Init status update.
  self:__StatusUpdate(-1)

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User API Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set verbosity level.
-- @param #OPERATION self
-- @param #number VerbosityLevel Level of output (higher=more). Default 0.
-- @return #OPERATION self
function OPERATION:SetVerbosity(VerbosityLevel)
  self.verbose=VerbosityLevel or 0
  return self
end

--- Set start and stop time of the operation.
-- @param #OPERATION self
-- @param #string ClockStart Time the mission is started, e.g. "05:00" for 5 am. If specified as a #number, it will be relative (in seconds) to the current mission time. Default is 5 seconds after mission was added.
-- @param #string ClockStop (Optional) Time the mission is stopped, e.g. "13:00" for 1 pm. If mission could not be started at that time, it will be removed from the queue. If specified as a #number it will be relative (in seconds) to the current mission time.
-- @return #OPERATION self
function OPERATION:SetTime(ClockStart, ClockStop)

  -- Current mission time.
  local Tnow=timer.getAbsTime()

  -- Set start time. Default in 5 sec.
  local Tstart=Tnow+5
  if ClockStart and type(ClockStart)=="number" then
    Tstart=Tnow+ClockStart
  elseif ClockStart and type(ClockStart)=="string" then
    Tstart=UTILS.ClockToSeconds(ClockStart)
  end

  -- Set stop time. Default nil.
  local Tstop=nil
  if ClockStop and type(ClockStop)=="number" then
    Tstop=Tnow+ClockStop
  elseif ClockStop and type(ClockStop)=="string" then
    Tstop=UTILS.ClockToSeconds(ClockStop)
  end

  self.Tstart=Tstart
  self.Tstop=Tstop

  if Tstop then
    self.duration=self.Tstop-self.Tstart
  end

  return self
end

--- Add (all) condition function when the whole operation is over. Must return a `#boolean`.
-- @param #OPERATION self
-- @param #function Function Function that needs to be `true` before the operation is over. 
-- @param ... Condition function arguments if any.
-- @return Core.Condition#CONDITION.Function Condition function table.
function OPERATION:AddConditonOverAll(Function, ...)
  local cf=self.conditionOver:AddFunctionAll(Function, ...)
  return cf  
end

--- Add (any) condition function when the whole operation is over. Must return a `#boolean`.
-- @param #OPERATION self
-- @param #function Function Function that needs to be `true` before the operation is over. 
-- @param ... Condition function arguments if any.
-- @return Core.Condition#CONDITION.Function Condition function table.
function OPERATION:AddConditonOverAny(Phase, Function, ...)
  local cf=self.conditionOver:AddFunctionAny(Function, ...)
  return cf  
end


--- Add a new phase to the operation. This is added add the end of all previously added phases (if any).
-- @param #OPERATION self
-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number.
-- @param #OPERATION.Branch Branch The branch to which this phase is added. Default is the master branch.
-- @param #number Duration Duration in seconds how long the phase will last. Default `nil`=forever.
-- @return #OPERATION.Phase Phase table object.
function OPERATION:AddPhase(Name, Branch, Duration)

  -- Branch.
  Branch=Branch or self.branchMaster

  -- Create a new phase.
  local phase=self:_CreatePhase(Name)
  
  -- Branch of phase
  phase.branch=Branch
  
  -- Set duraction of pahse (if any).
  phase.duration=Duration
  
  -- Debug output.
  self:T(self.lid..string.format("Adding phase %s to branch %s", phase.name, Branch.name))
  
  -- Add phase.
  table.insert(Branch.phases, phase)
  
  return phase
end

---Insert a new phase after an already defined phase of the operation.
-- @param #OPERATION self
-- @param #OPERATION.Phase PhaseAfter The phase after which the new phase is inserted.
-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number.
-- @return #OPERATION.Phase Phase table object.
function OPERATION:InsertPhaseAfter(PhaseAfter, Name)

  for i=1,#self.phases do
    local phase=self.phases[i] --#OPERATION.Phase
    if PhaseAfter.uid==phase.uid then
    
      -- Create a new phase.
      local phase=self:_CreatePhase(Name)
    
      
    end
  end
  
  return nil
end

--- Get a name of this operation.
-- @param #OPERATION self
-- @return #string Name of this operation or "Unknown".
function OPERATION:GetName()
  return self.name or "Unknown"
end

--- Get a phase by its name.
-- @param #OPERATION self
-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number.
-- @return #OPERATION.Phase Phase table object or nil if phase could not be found.
function OPERATION:GetPhaseByName(Name)

  for _,_branch in pairs(self.branches) do
    local branch=_branch --#OPERATION.Branch
    for _,_phase in pairs(branch.phases or {}) do
      local phase=_phase --#OPERATION.Phase
      if phase.name==Name then
        return phase
      end
    end
  end

  return nil
end

--- Set status of a phase.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @param #string Status New status, *e.g.* `OPERATION.PhaseStatus.OVER`.
-- @return #OPERATION self
function OPERATION:SetPhaseStatus(Phase, Status)

  if Phase then
  
    -- Debug message.
    self:T(self.lid..string.format("Phase %s status: %s-->%s", tostring(Phase.name), tostring(Phase.status), tostring(Status)))
    
    -- Set status.
    Phase.status=Status
    
    -- Set time stamp when phase becase active.
    if Phase.status==OPERATION.PhaseStatus.ACTIVE then
      Phase.Tstart=timer.getAbsTime()
      Phase.nActive=Phase.nActive+1
    elseif Phase.status==OPERATION.PhaseStatus.OVER then
      -- Trigger PhaseOver event.
      self:PhaseOver(Phase)
    end
    
  end
  
  return self
end

--- Get status of a phase.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @return #string Phase status, *e.g.* `OPERATION.PhaseStatus.OVER`.
function OPERATION:GetPhaseStatus(Phase)
  return Phase.status
end

--- Set condition when the given phase is over.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @param Core.Condition#CONDITION Condition Condition when the phase is over.
-- @return #OPERATION self
function OPERATION:SetPhaseConditonOver(Phase, Condition)
  if Phase then
    self:T(self.lid..string.format("Setting phase %s conditon over %s", self:GetPhaseName(Phase), Condition and Condition.name or "None"))
    Phase.conditionOver=Condition
  end
  return self
end

--- Add condition function when the given phase is over. Must return a `#boolean`.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @param #function Function Function that needs to be `true` before the phase is over. 
-- @param ... Condition function arguments if any.
-- @return Core.Condition#CONDITION.Function Condition function table.
function OPERATION:AddPhaseConditonOverAll(Phase, Function, ...)
  if Phase then
    local cf=Phase.conditionOver:AddFunctionAll(Function, ...)
    return cf  
  end
  return nil
end

--- Add condition function when the given phase is over. Must return a `#boolean`.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @param #function Function Function that needs to be `true` before the phase is over. 
-- @param ... Condition function arguments if any.
-- @return Core.Condition#CONDITION.Function Condition function table.
function OPERATION:AddPhaseConditonOverAny(Phase, Function, ...)
  if Phase then
    local cf=Phase.conditionOver:AddFunctionAny(Function, ...)
    return cf  
  end
  return nil
end

--- Set persistence of condition function. By default, condition functions are removed after a phase is over.
-- @param #OPERATION self
-- @param Core.Condition#CONDITION.Function ConditionFunction Condition function table.
-- @param #boolean IsPersistent If `true` or `nil`, condition function is persistent.
-- @return #OPERATION self
function OPERATION:SetConditionFunctionPersistence(ConditionFunction, IsPersistent)
  ConditionFunction.persistence=IsPersistent
  return self
end

--- Add condition function when the given phase is to be repeated. The provided function must return a `#boolean`.
-- If the condition evaluation returns `true`, the phase is set to state `Planned` instead of `Over` and can be repeated.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @param #function Function Function that needs to be `true` before the phase is over. 
-- @param ... Condition function arguments if any.
-- @return #OPERATION self
function OPERATION:AddPhaseConditonRepeatAll(Phase, Function, ...)
  if Phase then
    Phase.conditionRepeat:AddFunctionAll(Function, ...)  
  end
  return self
end


--- Get condition when the given phase is over.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @return Core.Condition#CONDITION Condition when the phase is over (if any).
function OPERATION:GetPhaseConditonOver(Phase, Condition)
  return Phase.conditionOver
end

--- Get how many times a phase has been active.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @return #number Number of times the phase has been active.
function OPERATION:GetPhaseNactive(Phase)
  return Phase.nActive
end

--- Get name of a phase.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase of which the name is returned. Default is the currently active phase.
-- @return #string The name of the phase or "None" if no phase is given or active.
function OPERATION:GetPhaseName(Phase)

  Phase=Phase or self.phase
  
  if Phase then
    return Phase.name
  end
  
  return "None"
end

--- Get currrently active phase.
-- @param #OPERATION self
-- @return #OPERATION.Phase Current phase or `nil` if no current phase is active.
function OPERATION:GetPhaseActive()
  return self.phase
end

--- Get index of phase.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @return #number The index.
-- @return #OPERATION.Branch The branch.
function OPERATION:GetPhaseIndex(Phase)

  local branch=Phase.branch
  
  for i,_phase in pairs(branch.phases) do
    local phase=_phase --#OPERATION.Phase
    if phase.uid==Phase.uid then
      return i, branch
    end
  end
  
  return nil
end

--- Get next phase.
-- @param #OPERATION self
-- @param #OPERATION.Branch Branch (Optional) The branch from which the next phase is retrieved. Default is the currently active branch.
-- @param #string PhaseStatus (Optional) Only return a phase, which is in this status. For example, `OPERATION.PhaseStatus.PLANNED` to make sure, the next phase is planned.
-- @return #OPERATION.Phase Next phase or `nil` if no next phase exists.
function OPERATION:GetPhaseNext(Branch, PhaseStatus)

  -- Branch.
  Branch=Branch or self:GetBranchActive()

  -- The phases of the branch.
  local phases=Branch.phases or {}
  
  local phase=nil
  if self.phase and self.phase.branch.uid==Branch.uid then
    phase=self.phase
  end
  
  -- Number of phases.
  local N=#phases
  
  -- Debug message.
  self:T(self.lid..string.format("Getting next phase! Branch=%s, Phases=%d, Status=%s", Branch.name, N, tostring(PhaseStatus)))
  
  if N>0 then
  
    -- Check if there there is an active phase already.
    if phase==nil and PhaseStatus==nil then
      return phases[1]
    end
    
    local n=1
    
    if phase then
      n=self:GetPhaseIndex(phase)+1
    end
  
    for i=n,N do
      local phase=phases[i] --#OPERATION.Phase
      
      if PhaseStatus==nil or PhaseStatus==phase.status then
        return phase
      end
      
    end
    
  end
  
  return nil
end

--- Count phases.
-- @param #OPERATION self
-- @param #string Status (Optional) Only count phases in a certain status, e.g. `OPERATION.PhaseStatus.PLANNED`.
-- @param #OPERATION.Branch Branch (Optional) Branch.
-- @return #number Number of phases
function OPERATION:CountPhases(Status, Branch)

  Branch=Branch or self.branchActive

  local N=0
  for _,_phase in pairs(Branch.phases) do
    local phase=_phase --#OPERATION.Phase
    if Status==nil or Status==phase.status then
      N=N+1
    end
  end

  return N
end


--- Add a new branch to the operation.
-- @param #OPERATION self
-- @param #string Name
-- @return #OPERATION.Branch Branch table object.
function OPERATION:AddBranch(Name)

  -- Create a new branch.
  local branch=self:_CreateBranch(Name)
  
  -- Add phase.
  table.insert(self.branches, branch)
  
  return branch
end

--- Get the master branch. This is the default branch and should always exist (if it was not explicitly deleted).
-- @param #OPERATION self
-- @return #OPERATION.Branch The master branch.
function OPERATION:GetBranchMaster()
  return self.branchMaster
end

--- Get the currently active branch.
-- @param #OPERATION self
-- @return #OPERATION.Branch The active branch. If no branch is active, the master branch is returned.
function OPERATION:GetBranchActive()
  return self.branchActive or self.branchMaster
end

--- Get name of the branch.
-- @param #OPERATION self
-- @param #OPERATION.Branch Branch The branch of which the name is requested. Default is the currently active or master branch.
-- @return #string Name Name or "None"
function OPERATION:GetBranchName(Branch)
  Branch=Branch or self:GetBranchActive()
  if Branch then
    return Branch.name
  end
  return "None"
end

--- Add an edge between two branches.
-- @param #OPERATION self
-- @param #OPERATION.Phase PhaseFrom The phase of the *from* branch *after* which to switch.
-- @param #OPERATION.Phase PhaseTo The phase of the *to* branch *to* which to switch.
-- @param Core.Condition#CONDITION ConditionSwitch (Optional) Condition(s) when to switch the branches.
-- @return #OPERATION.Edge Edge table object.
function OPERATION:AddEdge(PhaseFrom, PhaseTo, ConditionSwitch)

  local edge={} --#OPERATION.Edge
  
  edge.phaseFrom=PhaseFrom
  edge.phaseTo=PhaseTo

  edge.branchFrom=PhaseFrom.branch
  edge.branchTo=PhaseTo.branch  
  
  if ConditionSwitch then
    edge.conditionSwitch=ConditionSwitch
  else
    edge.conditionSwitch=CONDITION:New("Edge")
    edge.conditionSwitch:SetNoneResult(true)
  end
  
  table.insert(edge.branchFrom.edges, edge)

  return edge
end

--- Add condition function to an edge when branches are switched. The function must return a `#boolean`.
-- If multiple condition functions are added, all of these must return true for the branch switch to occur.
-- @param #OPERATION self
-- @param #OPERATION.Edge Edge The edge connecting the two branches.
-- @param #function Function Function that needs to be `true` for switching between the branches. 
-- @param ... Condition function arguments if any.
-- @return Core.Condition#CONDITION.Function Condition function table.
function OPERATION:AddEdgeConditonSwitchAll(Edge, Function, ...)
  if Edge then
    local cf=Edge.conditionSwitch:AddFunctionAll(Function, ...)
    return cf
  end
  return nil
end

--- Add mission to operation.
-- @param #OPERATION self
-- @param Ops.Auftrag#AUFTRAG Mission The mission to add.
-- @param #OPERATION.Phase Phase (Optional) The phase in which the mission should be executed. If no phase is given, it will be exectuted ASAP.
function OPERATION:AddMission(Mission, Phase)

  Mission.phase=Phase
  Mission.operation=self
  
  table.insert(self.missions, Mission)

  return self
end

--- Add Target to operation.
-- @param #OPERATION self
-- @param Ops.Target#TARGET Target The target to add.
-- @param #OPERATION.Phase Phase (Optional) The phase in which the target should be attacked. If no phase is given, it will be attacked ASAP.
function OPERATION:AddTarget(Target, Phase)

  Target.phase=Phase
  Target.operation=self
  
  table.insert(self.targets, Target)

  return self
end

--- Get targets of operation.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase (Optional) Only return targets set for this phase. Default is targets of all phases.
-- @return #table Targets Table of #TARGET objects 
function OPERATION:GetTargets(Phase)
  local N = {}
  for _,_target in pairs(self.targets) do
    local target=_target --Ops.Target#TARGET
    if target:IsAlive() and (Phase==nil or target.phase==Phase) then
      table.insert(N,target)
    end
  end
  return N
end

--- Count targets alive.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase (Optional) Only count targets set for this phase.
-- @return #number Number of phases
function OPERATION:CountTargets(Phase)

  local N=0
  for _,_target in pairs(self.targets) do
    local target=_target --Ops.Target#TARGET
    
    if target:IsAlive() and (Phase==nil or target.phase==Phase) then
      N=N+1
    end
  end

  return N
end

--- Assign cohort to operation.
-- @param #OPERATION self
-- @param Ops.Cohort#COHORT Cohort The cohort
-- @return #OPERATION self
function OPERATION:AssignCohort(Cohort)

  self:T(self.lid..string.format("Assiging Cohort %s to operation", Cohort.name))
  self.cohorts[Cohort.name]=Cohort

end

--- Assign legion to operation. All cohorts of this legion will be assigned and are only available.
-- @param #OPERATION self
-- @param Ops.Legion#LEGION Legion The legion to be assigned.
-- @return #OPERATION self
function OPERATION:AssignLegion(Legion)

  self.legions[Legion.alias]=Legion

end

--- Check if a given legion is assigned to this operation. All cohorts of this legion will be checked.
-- @param #OPERATION self
-- @param Ops.Legion#LEGION Legion The legion to be assigned.
-- @return #boolean If `true`, legion is assigned to this operation.
function OPERATION:IsAssignedLegion(Legion)

  local legion=self.legions[Legion.alias]

  if legion then
    self:T(self.lid..string.format("Legion %s is assigned to this operation", Legion.alias))
    return true
  else
    self:T(self.lid..string.format("Legion %s is NOT assigned to this operation", Legion.alias))
    return false
  end
  
end

--- Check if a given cohort is assigned to this operation.
-- @param #OPERATION self
-- @param Ops.Cohort#COHORT Cohort The Cohort.
-- @return #boolean If `true`, cohort is assigned to this operation.
function OPERATION:IsAssignedCohort(Cohort)

  local cohort=self.cohorts[Cohort.name]

  if cohort then
    self:T(self.lid..string.format("Cohort %s is assigned to this operation", Cohort.name))
    return true
  else
  
    -- Check if legion of this cohort was assigned.
    local Legion=Cohort.legion
    if Legion and self:IsAssignedLegion(Legion) then
      self:T(self.lid..string.format("Legion %s of Cohort %s is assigned to this operation", Legion.alias, Cohort.name))
      return true
    end
  
    self:T(self.lid..string.format("Cohort %s is NOT assigned to this operation", Cohort.name))
    return false
  end
  
  return nil
end

--- Check if a given cohort or legion is assigned to this operation.
-- @param #OPERATION self
-- @param Wrapper.Object#OBJECT Object The cohort or legion object.
-- @return #boolean If `true`, cohort is assigned to this operation.
function OPERATION:IsAssignedCohortOrLegion(Object)

  local isAssigned=nil
  if Object:IsInstanceOf("COHORT") then
    isAssigned=self:IsAssignedCohort(Object)    
  elseif Object:IsInstanceOf("LEGION") then
    isAssigned=self:IsAssignedLegion(Object)
  else
    self:E(self.lid.."ERROR: Unknown Object!")
  end

  return isAssigned
end

--- Check if operation is in FSM state "Planned".
-- @param #OPERATION self
-- @return #boolean If `true`, operation is "Planned".
function OPERATION:IsPlanned()
  local is=self:is("Planned")
  return is
end

--- Check if operation is in FSM state "Running".
-- @param #OPERATION self
-- @return #boolean If `true`, operation is "Running".
function OPERATION:IsRunning()
  local is=self:is("Running")
  return is
end

--- Check if operation is in FSM state "Paused".
-- @param #OPERATION self
-- @return #boolean If `true`, operation is "Paused".
function OPERATION:IsPaused()
  local is=self:is("Paused")
  return is
end

--- Check if operation is in FSM state "Over".
-- @param #OPERATION self
-- @return #boolean If `true`, operation is "Over".
function OPERATION:IsOver()
  local is=self:is("Over")
  return is
end

--- Check if operation is in FSM state "Stopped".
-- @param #OPERATION self
-- @return #boolean If `true`, operation is "Stopped".
function OPERATION:IsStopped()
  local is=self:is("Stopped")
  return is
end

--- Check if operation is **not** "Over" or "Stopped".
-- @param #OPERATION self
-- @return #boolean If `true`, operation is not "Over" or "Stopped".
function OPERATION:IsNotOver()
  local is=not (self:IsOver() or self:IsStopped())
  return is
end

--- Check if phase is in status "Active".
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @return #boolean If `true`, phase is active.
function OPERATION:IsPhaseActive(Phase)
  if Phase and Phase.status and Phase.status==OPERATION.PhaseStatus.ACTIVE then
    return true
  end
  return false
end

--- Check if a phase is the currently active one.
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase to check.
-- @return #boolean If `true`, this phase is currently active.
function OPERATION:IsPhaseActive(Phase)
  local phase=self:GetPhaseActive()
  if phase and phase.uid==Phase.uid then
    return true
  else
    return false
  end
  return nil
end

--- Check if phase is in status "Planned".
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @return #boolean If `true`, phase is Planned.
function OPERATION:IsPhasePlanned(Phase)
  if Phase and Phase.status and Phase.status==OPERATION.PhaseStatus.PLANNED then
    return true
  end
  return false
end

--- Check if phase is in status "Over".
-- @param #OPERATION self
-- @param #OPERATION.Phase Phase The phase.
-- @return #boolean If `true`, phase is over.
function OPERATION:IsPhaseOver(Phase)
  if Phase and Phase.status and Phase.status==OPERATION.PhaseStatus.OVER then
    return true
  end
  return false
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Status Update
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "Start" event.
-- @param #OPERATION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPERATION:onafterStart(From, Event, To)

  -- Debug message.
  self:T(self.lid..string.format("Starting Operation!"))
  return self 
end


--- On after "StatusUpdate" event.
-- @param #OPERATION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPERATION:onafterStatusUpdate(From, Event, To)

  -- Current abs. mission time.
  local Tnow=timer.getAbsTime()

  -- Current FSM state.
  local fsmstate=self:GetState()
  
  -- Start operation.
  if self:IsPlanned() then
  
    -- Start operation if start time has passed (if any) and start condition(s) are met (if any).
    if (self.Tstart and Tnow>self.Tstart or self.Tstart==nil) and (self.conditionStart==nil or self.conditionStart:Evaluate()) then
      self:Start()
    end
    
  elseif self:IsNotOver() then

    -- Operation is over if stop time has passed (if any) and over condition(s) are met (if any).
    if (self.Tstop and Tnow>self.Tstop or self.Tstop==nil) and (self.conditionOver==nil or self.conditionOver:Evaluate()) then
      self:Over()
    end
    
  end
  
  
  -- Check phases.
  if self:IsRunning() then
    self:_CheckPhases()
  end
  
  
  -- Debug output.
  if self.verbose>=1 then
  
    -- Current phase.
    local phaseName=self:GetPhaseName()
    local branchName=self:GetBranchName()
    local NphaseTot=self:CountPhases()
    local NphaseAct=self:CountPhases(OPERATION.PhaseStatus.ACTIVE)
    local NphasePla=self:CountPhases(OPERATION.PhaseStatus.PLANNED)
    local NphaseOvr=self:CountPhases(OPERATION.PhaseStatus.OVER)
    
    -- General info.
    local text=string.format("State=%s: Phase=%s [%s], Phases=%d [Active=%d, Planned=%d, Over=%d]", fsmstate, phaseName, branchName, NphaseTot, NphaseAct, NphasePla, NphaseOvr)
    self:I(self.lid..text)
    
  end
  
  -- Debug output.
  if self.verbose>=2 then
  
    -- Info on phases.
    local text="Phases:"
    for i,_phase in pairs(self.branchActive.phases) do
      local phase=_phase --#OPERATION.Phase
      text=text..string.format("\n[%d] %s [uid=%d]: status=%s Nact=%d", i, phase.name, phase.uid, tostring(phase.status), phase.nActive)
    end
    if text=="Phases:" then text=text.." None" end
    self:I(self.lid..text)
    
  end

  -- Next status update.
  self:__StatusUpdate(-30)
  
  return self 
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "PhaseNext" event.
-- @param #OPERATION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPERATION:onafterPhaseNext(From, Event, To)

  -- Get next phase.
  local Phase=self:GetPhaseNext()
  
  if Phase then
  
    -- Change phase to next one.
    self:PhaseChange(Phase)
          
  else
  
    -- No further phases defined ==> Operation is over.
    self:Over()
    
  end

  return self
end


--- On after "PhaseChange" event.
-- @param #OPERATION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPERATION.Phase Phase The new phase.
function OPERATION:onafterPhaseChange(From, Event, To, Phase)

  -- Previous phase (if any).
  local oldphase="None"
  if self.phase then
    if self.phase.status~=OPERATION.PhaseStatus.OVER then
      self:SetPhaseStatus(self.phase, OPERATION.PhaseStatus.OVER)
    end
    oldphase=self.phase.name
  end

  -- Debug message.
  self:I(self.lid..string.format("Phase change: %s --> %s", oldphase, Phase.name))
  
  -- Set currently active phase.
  self.phase=Phase
  
  -- Phase is active.
  self:SetPhaseStatus(Phase, OPERATION.PhaseStatus.ACTIVE)
  
  return self
end

--- On after "PhaseOver" event.
-- @param #OPERATION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPERATION.Phase Phase The phase that is over.
function OPERATION:onafterPhaseOver(From, Event, To, Phase)
  
  -- Remove all non-persistant condition functions.
  Phase.conditionOver:RemoveNonPersistant()

end

--- On after "BranchSwitch" event.
-- @param #OPERATION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPERATION.Branch Branch The new branch.
-- @param #OPERATION.Phase Phase The phase.
function OPERATION:onafterBranchSwitch(From, Event, To, Branch, Phase)

  -- Debug info.
  self:T(self.lid..string.format("Switching to branch %s", Branch.name))

  -- Set active branch.
  self.branchActive=Branch
  
  -- Change phase.
  self:PhaseChange(Phase)  
  
  return self
end

--- On after "Over" event.
-- @param #OPERATION self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPERATION:onafterOver(From, Event, To)

  -- Debug message.
  self:T(self.lid..string.format("Operation is over!"))
  
  -- No active phase.
  self.phase=nil
  
  -- Set all phases to OVER.
  for _,_branch in pairs(self.branches) do
    local branch=_branch --#OPERATION.Branch
    for _,_phase in pairs(branch.phases) do
      local phase=_phase --#OPERATION.Phase
      if not self:IsPhaseOver(phase) then
        self:SetPhaseStatus(phase, OPERATION.PhaseStatus.OVER)
      end
    end
  end 
  
  return self 
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc (private) Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check phases.
-- @param #OPERATION self
function OPERATION:_CheckPhases()

  -- Currently active phase.
  local phase=self:GetPhaseActive()
  
  -- Check if active phase is over if conditon over is defined.
  if phase and phase.conditionOver then
  
    -- Evaluate if phase is over.
    local isOver=phase.conditionOver:Evaluate()
    
    local Tnow=timer.getAbsTime()
    
    -- Check if duration of phase if over.
    if phase.duration and phase.Tstart and Tnow-phase.Tstart>phase.duration then
      isOver=true
    end
    
    -- Set phase status to over. This also triggers the PhaseOver() event.
    if isOver then
      self:SetPhaseStatus(phase, OPERATION.PhaseStatus.OVER)
    end
  end
  
  -- If no current phase or current phase is over, get next phase.
  if phase==nil or phase.status==OPERATION.PhaseStatus.OVER then
  
    for _,_edge in pairs(self.branchActive.edges) do
      local edge=_edge --#OPERATION.Edge
      
      if phase then
        --env.info(string.format("phase active uid=%d", phase.uid))
        --env.info(string.format("Phase from   uid=%d", edge.phaseFrom.uid))
      end
      
      if (edge.phaseFrom==nil) or (phase and edge.phaseFrom.uid==phase.uid) then

        -- Evaluate switch condition.      
        local switch=edge.conditionSwitch:Evaluate()
        
        if switch then
        
          -- Get next phase of the branch
          local phaseTo=edge.phaseTo or self:GetPhaseNext(edge.branchTo, nil)
          
          if phaseTo then
          
            -- Switch to new branch.
            self:BranchSwitch(edge.branchTo, phaseTo)
            
          else
          
            -- No next phase ==> Ops is over!
            self:Over()
            
          end
          
          -- Done here!
          return
        end
      end
      
    end
        
    -- Next phase.
    self:PhaseNext()
        
  end

end

--- Create a new phase object.
-- @param #OPERATION self
-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number.
-- @return #OPERATION.Phase Phase table object.
function OPERATION:_CreatePhase(Name)

  -- Increase phase counter.
  self.counterPhase=self.counterPhase+1

  local phase={} --#OPERATION.Phase
  phase.uid=self.counterPhase
  phase.name=Name or string.format("Phase-%02d", self.counterPhase)  
  phase.conditionOver=CONDITION:New(Name.." Over")
  phase.conditionOver:SetDefaultPersistence(false)
  phase.status=OPERATION.PhaseStatus.PLANNED
  phase.nActive=0

  return phase
end

--- Create a new branch object.
-- @param #OPERATION self
-- @param #string Name Name of the phase. Default "Phase-01" where the last number is a running number.
-- @return #OPERATION.Branch Branch table object.
function OPERATION:_CreateBranch(Name)

  -- Increase phase counter.
  self.counterBranch=self.counterBranch+1

  local branch={} --#OPERATION.Branch
  branch.uid=self.counterBranch
  branch.name=Name or string.format("Branch-%02d", self.counterBranch)
  branch.phases={}
  branch.edges={}

  return branch
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Generic group enhancement.
--
-- This class is **not** meant to be used itself by the end user. It contains common functionalities of derived classes for air, ground and sea.
--
-- ===
--
-- ### Author: **funkyfranky**
--
-- ===
-- @module Ops.OpsGroup
-- @image OPS_OpsGroup.png


--- OPSGROUP class.
-- @type OPSGROUP
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level. 0=silent.
-- @field #string lid Class id string for output to DCS log file.
-- @field #string groupname Name of the group.
-- @field Wrapper.Group#GROUP group Group object.
-- @field DCS#Group dcsgroup The DCS group object.
-- @field DCS#Controller controller The DCS controller of the group.
-- @field DCS#Template template Template table of the group.
-- @field #table elements Table of elements, i.e. units of the group.
-- @field #boolean isLateActivated Is the group late activated.
-- @field #boolean isUncontrolled Is the group uncontrolled.
-- @field #boolean isFlightgroup Is a FLIGHTGROUP.
-- @field #boolean isArmygroup Is an ARMYGROUP.
-- @field #boolean isNavygroup Is a NAVYGROUP.
-- @field #boolean isHelo If true, this is a helicopter group.
-- @field #boolean isVTOL If true, this is capable of Vertical TakeOff and Landing (VTOL).
-- @field #boolean isSubmarine If true, this is a submarine group.
-- @field #boolean isAI If true, group is purely AI.
-- @field #boolean isDestroyed If true, the whole group was destroyed.
-- @field #boolean isDead If true, the whole group is dead.
-- @field #table waypoints Table of waypoints.
-- @field #table waypoints0 Table of initial waypoints.
-- @field #boolean useMEtasks If `true`, use tasks set in the ME. Default `false`.
-- @field Wrapper.Airbase#AIRBASE homebase The home base of the flight group.
-- @field Wrapper.Airbase#AIRBASE destbase The destination base of the flight group.
-- @field Wrapper.Airbase#AIRBASE currbase The current airbase of the flight group, i.e. where it is currently located or landing at.
-- @field Core.Zone#ZONE homezone The home zone of the flight group. Set when spawn happens in air.
-- @field Core.Zone#ZONE destzone The destination zone of the flight group. Set when final waypoint is in air.
-- @field #number currentwp Current waypoint index. This is the index of the last passed waypoint.
-- @field #boolean adinfinitum Resume route at first waypoint when final waypoint is reached.
-- @field #number Twaiting Abs. mission time stamp when the group was ordered to wait.
-- @field #number dTwait Time to wait in seconds. Default `nil` (for ever).
-- @field #table taskqueue Queue of tasks.
-- @field #number taskcounter Running number of task ids.
-- @field #number taskcurrent ID of current task. If 0, there is no current task assigned.
-- @field #table taskenroute Enroute task of the group.
-- @field #table taskpaused Paused tasks.
-- @field #table missionqueue Queue of missions.
-- @field #number currentmission The ID (auftragsnummer) of the currently assigned AUFTRAG.
-- @field Core.Set#SET_UNIT detectedunits Set of detected units.
-- @field Core.Set#SET_GROUP detectedgroups Set of detected groups.
-- @field #string attribute Generalized attribute.
-- @field #number speedMax Max speed in km/h.
-- @field #number speedCruise Cruising speed in km/h.
-- @field #number speedWp Speed to the next waypoint in m/s.
-- @field #boolean isMobile If `true`, group is mobile (speed > 1 m/s)
-- @field #boolean passedfinalwp Group has passed the final waypoint.
-- @field #number wpcounter Running number counting waypoints.
-- @field Core.Set#SET_ZONE checkzones Set of zones.
-- @field Core.Set#SET_ZONE inzones Set of zones in which the group is currently in.
-- @field Core.Timer#TIMER timerStatus Timer for status update.
-- @field Core.Timer#TIMER timerCheckZone Timer for check zones.
-- @field Core.Timer#TIMER timerQueueUpdate Timer for queue updates.
-- @field #boolean groupinitialized If true, group parameters were initialized.
-- @field #boolean detectionOn If true, detected units of the group are analyzed.
-- @field #table pausedmissions Paused missions.
-- @field #number Ndestroyed Number of destroyed units.
-- @field #number Nkills Number kills of this groups.
-- @field #number Nhit Number of hits taken.
--
-- @field #boolean rearmOnOutOfAmmo If `true`, group will go to rearm once it runs out of ammo.
--
-- @field Ops.Legion#LEGION legion Legion the group belongs to.
-- @field Ops.Cohort#COHORT cohort Cohort the group belongs to.
--
-- @field Core.Point#COORDINATE coordinate Current coordinate.
--
-- @field DCS#Vec3 position Position of the group at last status check.
-- @field DCS#Vec3 positionLast Backup of last position vec to monitor changes.
-- @field #number heading Heading of the group at last status check.
-- @field #number headingLast Backup of last heading to monitor changes.
-- @field DCS#Vec3 orientX Orientation at last status check.
-- @field DCS#Vec3 orientXLast Backup of last orientation to monitor changes.
-- @field #number traveldist Distance traveled in meters. This is a lower bound.
-- @field #number traveltime Time.
--
-- @field Core.Astar#ASTAR Astar path finding.
-- @field #boolean ispathfinding If true, group is on pathfinding route.
--
-- @field #boolean engagedetectedOn If `true`, auto engage detected targets.
-- @field #number engagedetectedRmax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM.
-- @field #table engagedetectedTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default "All".
-- @field Core.Set#SET_ZONE engagedetectedEngageZones Set of zones in which targets are engaged. Default is anywhere.
-- @field Core.Set#SET_ZONE engagedetectedNoEngageZones Set of zones in which targets are *not* engaged. Default is nowhere.
--
-- @field #OPSGROUP.Radio radio Current radio settings.
-- @field #OPSGROUP.Radio radioDefault Default radio settings.
-- @field Sound.Radio#RADIOQUEUE radioQueue Radio queue.
--
-- @field #OPSGROUP.Beacon tacan Current TACAN settings.
-- @field #OPSGROUP.Beacon tacanDefault Default TACAN settings.
--
-- @field #OPSGROUP.Beacon icls Current ICLS settings.
-- @field #OPSGROUP.Beacon iclsDefault Default ICLS settings.
--
-- @field #OPSGROUP.Option option Current optional settings.
-- @field #OPSGROUP.Option optionDefault Default option settings.
--
-- @field #OPSGROUP.Callsign callsign Current callsign settings.
-- @field #OPSGROUP.Callsign callsignDefault Default callsign settings.
-- @field #string callsignName Callsign name.
-- @field #string callsignAlias Callsign alias.
--
-- @field #OPSGROUP.Spot spot Laser and IR spot.
-- 
-- @field DCS#Vec3 stuckVec3 Position where the group got stuck.
-- @field #number stuckTimestamp Time stamp [sec], when the group got stuck.
-- @field #boolean stuckDespawn If `true`, group gets despawned after beeing stuck for a certain time.
--
-- @field #OPSGROUP.Ammo ammo Initial ammount of ammo.
-- @field #OPSGROUP.WeaponData weaponData Weapon data table with key=BitType.
--
-- @field #OPSGROUP.Element carrier Carrier the group is loaded into as cargo.
-- @field #OPSGROUP carrierGroup Carrier group transporting this group as cargo.
-- @field #OPSGROUP.MyCarrier mycarrier Carrier group for this group.
-- @field #table cargoqueue Table containing cargo groups to be transported.
-- @field #table cargoBay Table containing OPSGROUP loaded into this group.
-- @field Ops.OpsTransport#OPSTRANSPORT cargoTransport Current cargo transport assignment.
-- @field Ops.OpsTransport#OPSTRANSPORT.TransportZoneCombo cargoTZC Transport zone combo (pickup, deploy etc.) currently used.
-- @field #string cargoStatus Cargo status of this group acting as cargo.
-- @field #number cargoTransportUID Unique ID of the transport assignment this cargo group is associated with.
-- @field #string carrierStatus Carrier status of this group acting as cargo carrier.
-- @field #OPSGROUP.CarrierLoader carrierLoader Carrier loader parameters.
-- @field #OPSGROUP.CarrierLoader carrierUnloader Carrier unloader parameters.
--
-- @field #boolean useSRS Use SRS for transmissions.
-- @field Sound.SRS#MSRS msrs MOOSE SRS wrapper.
--
-- @extends Core.Fsm#FSM

--- *A small group of determined and like-minded people can change the course of history.* -- Mahatma Gandhi
--
-- ===
--
-- # The OPSGROUP Concept
--
-- The OPSGROUP class contains common functions used by other classes such as FLIGHTGROUP, NAVYGROUP and ARMYGROUP.
-- Those classes inherit everything of this class and extend it with features specific to their unit category.
--
-- This class is **NOT** meant to be used by the end user itself.
--
--
-- @field #OPSGROUP
OPSGROUP = {
  ClassName          = "OPSGROUP",
  verbose            =     0,
  lid                =   nil,
  groupname          =   nil,
  group              =   nil,
  template           =   nil,
  isLateActivated    =   nil,
  waypoints          =   nil,
  waypoints0         =   nil,
  currentwp          =     1,
  elements           =    {},
  taskqueue          =    {},
  taskcounter        =   nil,
  taskcurrent        =   nil,
  taskenroute        =   nil,
  taskpaused         =    {},
  missionqueue       =    {},
  currentmission     =   nil,
  detectedunits      =    {},
  detectedgroups     =    {},
  attribute          =   nil,
  checkzones         =   nil,
  inzones            =   nil,
  groupinitialized   =   nil,
  wpcounter          =     1,
  radio              =    {},
  option             =    {},
  optionDefault      =    {},
  tacan              =    {},
  icls               =    {},
  callsign           =    {},
  Ndestroyed         =     0,
  Nkills             =     0,
  Nhit               =     0,
  weaponData         =    {},
  cargoqueue         =    {},
  cargoBay           =    {},
  mycarrier          =    {},
  carrierLoader      =    {},
  carrierUnloader    =    {},
  useMEtasks         = false,
  pausedmissions     =    {},
}


--- OPS group element.
-- @type OPSGROUP.Element
-- @field #string name Name of the element, i.e. the unit.
-- @field #string status The element status. See @{#OPSGROUP.ElementStatus}.
-- @field Wrapper.Unit#UNIT unit The UNIT object.
-- @field Wrapper.Group#GROUP group The GROUP object.
-- @field DCS#Unit DCSunit The DCS unit object.
-- @field DCS#Controller controller The DCS controller of the unit.
-- @field #boolean ai If true, element is AI.
-- @field #string skill Skill level.
-- @field #string playerName Name of player if this is a client.
-- @field #number Nhit Number of times the element was hit.
-- @field #boolean engineOn If `true`, engines were started.
--
-- @field Core.Zone#ZONE_POLYGON_BASE zoneBoundingbox Bounding box zone of the element unit.
-- @field Core.Zone#ZONE_POLYGON_BASE zoneLoad Loading zone.
-- @field Core.Zone#ZONE_POLYGON_BASE zoneUnload Unloading zone.
--
-- @field #string typename Type name.
-- @field #number category Aircraft category.
-- @field #string categoryname Aircraft category name.
--
-- @field #number size Size (max of length, width, height) in meters.
-- @field #number length Length of element in meters.
-- @field #number width Width of element in meters.
-- @field #number height Height of element in meters.
--
-- @field DCS#Vec3 vec3 Last known 3D position vector.
-- @field DCS#Vec3 orientX Last known ordientation vector in the direction of the nose X.
-- @field #number heading Last known heading in degrees.
--
-- @field #number life0 Initial life points.
-- @field #number life Life points when last updated.
-- @field #number damage Damage of element in percent.
--
-- @field DCS#Object.Desc descriptors Descriptors table.
-- @field #number weightEmpty Empty weight in kg.
-- @field #number weightMaxTotal Max. total weight in kg.
-- @field #number weightMaxCargo Max. cargo weight in kg.
-- @field #number weightCargo Current cargo weight in kg.
-- @field #number weight Current weight including cargo in kg.
-- @field #table cargoBay Cargo bay.
--
-- @field #string modex Tail number.
-- @field Wrapper.Client#CLIENT client The client if element is occupied by a human player.
-- @field #table pylons Table of pylons.
-- @field #number fuelmass Mass of fuel in kg.
-- @field #string callsign Call sign, e.g. "Uzi 1-1".
-- @field Wrapper.Airbase#AIRBASE.ParkingSpot parking The parking spot table the element is parking on.


--- Status of group element.
-- @type OPSGROUP.ElementStatus
-- @field #string INUTERO Element was not spawned yet or its status is unknown so far.
-- @field #string SPAWNED Element was spawned into the world.
-- @field #string PARKING Element is parking after spawned on ramp.
-- @field #string ENGINEON Element started its engines.
-- @field #string TAXIING Element is taxiing after engine startup.
-- @field #string TAKEOFF Element took of after takeoff event.
-- @field #string AIRBORNE Element is airborne. Either after takeoff or after air start.
-- @field #string LANDING Element is landing.
-- @field #string LANDED Element landed and is taxiing to its parking spot.
-- @field #string ARRIVED Element arrived at its parking spot and shut down its engines.
-- @field #string DEAD Element is dead after it crashed, pilot ejected or pilot dead events.
OPSGROUP.ElementStatus={
  INUTERO="InUtero",
  SPAWNED="Spawned",
  PARKING="Parking",
  ENGINEON="Engine On",
  TAXIING="Taxiing",
  TAKEOFF="Takeoff",
  AIRBORNE="Airborne",
  LANDING="Landing",
  LANDED="Landed",
  ARRIVED="Arrived",
  DEAD="Dead",
}

--- Status of group.
-- @type OPSGROUP.GroupStatus
-- @field #string INUTERO Not spawned yet or its status is unknown so far.
-- @field #string PARKING Parking after spawned on ramp.
-- @field #string TAXIING Taxiing after engine startup.
-- @field #string AIRBORNE Element is airborne. Either after takeoff or after air start.
-- @field #string LANDING Landing.
-- @field #string LANDED Landed and is taxiing to its parking spot.
-- @field #string ARRIVED Arrived at its parking spot and shut down its engines.
-- @field #string DEAD Element is dead after it crashed, pilot ejected or pilot dead events.
OPSGROUP.GroupStatus={
  INUTERO="InUtero",
  PARKING="Parking",
  TAXIING="Taxiing",
  AIRBORNE="Airborne",
  INBOUND="Inbound",
  LANDING="Landing",
  LANDED="Landed",
  ARRIVED="Arrived",
  DEAD="Dead",
}

--- Ops group task status.
-- @type OPSGROUP.TaskStatus
-- @field #string SCHEDULED Task is scheduled.
-- @field #string EXECUTING Task is being executed.
-- @field #string PAUSED Task is paused.
-- @field #string DONE Task is done.
OPSGROUP.TaskStatus={
  SCHEDULED="scheduled",
  EXECUTING="executing",
  PAUSED="paused",
  DONE="done",
}

--- Ops group task status.
-- @type OPSGROUP.TaskType
-- @field #string SCHEDULED Task is scheduled and will be executed at a given time.
-- @field #string WAYPOINT Task is executed at a specific waypoint.
OPSGROUP.TaskType={
  SCHEDULED="scheduled",
  WAYPOINT="waypoint",
}

--- Task structure.
-- @type OPSGROUP.Task
-- @field #string type Type of task: either SCHEDULED or WAYPOINT.
-- @field #boolean ismission This is an AUFTRAG task.
-- @field #number id Task ID. Running number to get the task.
-- @field #number prio Priority.
-- @field #number time Abs. mission time when to execute the task.
-- @field #table dcstask DCS task structure.
-- @field #string description Brief text which describes the task.
-- @field #string status Task status.
-- @field #number duration Duration before task is cancelled in seconds. Default never.
-- @field #number timestamp Abs. mission time, when task was started.
-- @field #number waypoint Waypoint index if task is a waypoint task.
-- @field Core.UserFlag#USERFLAG stopflag If flag is set to 1 (=true), the task is stopped.
-- @field #number backupROE Rules of engagement that are restored once the task is over.
-- @field Ops.Target#TARGET target Target object.

--- Option data.
-- @type OPSGROUP.Option
-- @field #number ROE Rule of engagement.
-- @field #number ROT Reaction on threat.
-- @field #number Alarm Alarm state.
-- @field #number Formation Formation.
-- @field #boolean EPLRS data link.
-- @field #boolean Disperse Disperse under fire.
-- @field #boolean Emission Emission on/off.
-- @field #boolean Invisible Invisible on/off.
-- @field #boolean Immortal Immortal on/off.

--- Beacon data.
-- @type OPSGROUP.Beacon
-- @field #number Channel Channel.
-- @field #number Morse Morse Code.
-- @field #string Band Band "X" or "Y" for TACAN beacon.
-- @field #string BeaconName Name of the unit acting as beacon.
-- @field Wrapper.Unit#UNIT BeaconUnit Unit object acting as beacon.
-- @field #boolean On If true, beacon is on, if false, beacon is turned off. If nil, has not been used yet.

--- Radio data.
-- @type OPSGROUP.Radio
-- @field #number Freq Frequency
-- @field #number Modu Modulation.
-- @field #boolean On If true, radio is on, if false, radio is turned off. If nil, has not been used yet.

--- Callsign data.
-- @type OPSGROUP.Callsign
-- @field #number NumberSquad Squadron number corresponding to a name like "Uzi".
-- @field #number NumberGroup Group number. First number after name, e.g. "Uzi-**1**-1".
-- @field #string NameSquad Name of the squad, e.g. "Uzi".

--- Weapon range data.
-- @type OPSGROUP.WeaponData
-- @field #number BitType Type of weapon.
-- @field #number RangeMin Min range in meters.
-- @field #number RangeMax Max range in meters.
-- @field #number ReloadTime Time to reload in seconds.

--- Laser and IR spot data.
-- @type OPSGROUP.Spot
-- @field #boolean CheckLOS If true, check LOS to target.
-- @field #boolean IRon If true, turn IR pointer on.
-- @field #number dt Update time interval in seconds.
-- @field DCS#Spot Laser Laser spot.
-- @field DCS#Spot IR Infra-red spot.
-- @field #number Code Laser code.
-- @field Wrapper.Group#GROUP TargetGroup The target group.
-- @field Wrapper.Positionable#POSITIONABLE TargetUnit The current target unit.
-- @field Core.Point#COORDINATE Coordinate where the spot is pointing.
-- @field #number TargetType Type of target: 0=coordinate, 1=static, 2=unit, 3=group.
-- @field #boolean On If true, the laser is on.
-- @field #boolean Paused If true, laser is paused.
-- @field #boolean lostLOS If true, laser lost LOS.
-- @field #OPSGROUP.Element element The element of the group that is lasing.
-- @field DCS#Vec3 vec3 The 3D positon vector of the laser (and IR) spot.
-- @field DCS#Vec3 offset Local offset of the laser source.
-- @field DCS#Vec3 offsetTarget Offset of the target.
-- @field Core.Timer#TIMER timer Spot timer.

--- Ammo data.
-- @type OPSGROUP.Ammo
-- @field #number Total Total amount of ammo.
-- @field #number Guns Amount of gun shells.
-- @field #number Bombs Amount of bombs.
-- @field #number Rockets Amount of rockets.
-- @field #number Torpedos Amount of torpedos.
-- @field #number Missiles Amount of missiles.
-- @field #number MissilesAA Amount of air-to-air missiles.
-- @field #number MissilesAG Amount of air-to-ground missiles.
-- @field #number MissilesAS Amount of anti-ship missiles.
-- @field #number MissilesCR Amount of cruise missiles.
-- @field #number MissilesBM Amount of ballistic missiles.
-- @field #number MissilesSA Amount of surfe-to-air missiles.

--- Spawn point data.
-- @type OPSGROUP.Spawnpoint
-- @field Core.Point#COORDINATE Coordinate Coordinate where to spawn
-- @field Wrapper.Airbase#AIRBASE Airport Airport where to spawn.
-- @field #table TerminalIDs Terminal IDs, where to spawn the group. It is a table of `#number`s because a group can consist of multiple units.

--- Waypoint data.
-- @type OPSGROUP.Waypoint
-- @field #number uid Waypoint's unit id, which is a running number.
-- @field #number speed Speed in m/s.
-- @field #number alt Altitude in meters. For submaries use negative sign for depth.
-- @field #string action Waypoint action (turning point, etc.). Ground groups have the formation here.
-- @field #table task Waypoint DCS task combo.
-- @field #string type Waypoint type.
-- @field #string name Waypoint description. Shown in the F10 map.
-- @field #number x Waypoint x-coordinate.
-- @field #number y Waypoint y-coordinate.
-- @field #number detour Signifies that this waypoint is not part of the normal route: 0=Hold, 1=Resume Route.
-- @field #boolean intowind If true, this waypoint is a turn into wind route point.
-- @field #boolean astar If true, this waypint was found by A* pathfinding algorithm.
-- @field #boolean temp If true, this is a temporary waypoint and will be deleted when passed. Also the passing waypoint FSM event is not triggered.
-- @field #number npassed Number of times a groups passed this waypoint.
-- @field Core.Point#COORDINATE coordinate Waypoint coordinate.
-- @field Core.Point#COORDINATE roadcoord Closest point to road.
-- @field #number roaddist Distance to closest point on road.
-- @field Wrapper.Marker#MARKER marker Marker on the F10 map.
-- @field #string formation Ground formation. Similar to action but on/off road.
-- @field #number missionUID Mission UID (Auftragsnr) this waypoint belongs to.

--- Cargo Carrier status.
-- @type OPSGROUP.CarrierStatus
-- @field #string NOTCARRIER This group is not a carrier yet.
-- @field #string PICKUP Carrier is on its way to pickup cargo.
-- @field #string LOADING Carrier is loading cargo.
-- @field #string LOADED Carrier has loaded cargo.
-- @field #string TRANSPORTING Carrier is transporting cargo.
-- @field #string UNLOADING Carrier is unloading cargo.
OPSGROUP.CarrierStatus={
  NOTCARRIER="not carrier",
  PICKUP="pickup",
  LOADING="loading",
  LOADED="loaded",
  TRANSPORTING="transporting",
  UNLOADING="unloading",
}

--- Cargo status.
-- @type OPSGROUP.CargoStatus
-- @field #string AWAITING Group is awaiting carrier.
-- @field #string NOTCARGO This group is no cargo yet.
-- @field #string ASSIGNED Cargo is assigned to a carrier. (Not used!)
-- @field #string BOARDING Cargo is boarding a carrier.
-- @field #string LOADED Cargo is loaded into a carrier.
OPSGROUP.CargoStatus={
  AWAITING="Awaiting carrier",
  NOTCARGO="not cargo",
  ASSIGNED="assigned to carrier",
  BOARDING="boarding",
  LOADED="loaded",
}

--- Cargo carrier loader parameters.
-- @type OPSGROUP.CarrierLoader
-- @field #string type Loader type "Front", "Back", "Left", "Right", "All".
-- @field #number length Length of (un-)loading zone in meters.
-- @field #number width Width of (un-)loading zone in meters.

--- Data of the carrier that has loaded this group.
-- @type OPSGROUP.MyCarrier
-- @field #OPSGROUP group The carrier group.
-- @field #OPSGROUP.Element element The carrier element.
-- @field #boolean reserved If `true`, the carrier has caro space reserved for me.

--- Element cargo bay data.
-- @type OPSGROUP.MyCargo
-- @field #OPSGROUP group The cargo group.
-- @field #number storageType Type of storage.
-- @field #number storageAmount Amount of storage.
-- @field #number storageWeight Weight of storage item.
-- @field #boolean reserved If `true`, the cargo bay space is reserved but cargo has not actually been loaded yet.

--- Cargo group data.
-- @type OPSGROUP.CargoGroup
-- @field #number uid Unique ID of this cargo data.
-- @field #string type Type of cargo: "OPSGROUP" or "STORAGE".
-- @field #OPSGROUP opsgroup The cargo opsgroup.
-- @field Ops.OpsTransport#OPSTRANSPORT.Storage storage Storage data.
-- @field #boolean delivered If `true`, group was delivered.
-- @field #boolean disembarkActivation If `true`, group is activated. If `false`, group is late activated.
-- @field Core.Zone#ZONE disembarkZone Zone where this group is disembarked to.
-- @field Core.Set#SET_OPSGROUP disembarkCarriers Carriers where this group is directly disembared to.
-- @field #string status Status of the cargo group. Not used yet.

--- OpsGroup version.
-- @field #string version
OPSGROUP.version="1.0.4"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: AI on/off.
-- TODO: F10 menu.
-- TODO: Add pseudo function.
-- TODO: Afterburner restrict.
-- TODO: What more options?
-- TODO: Shot events?
-- TODO: Marks to add waypoints/tasks on-the-fly.
-- DONE: Invisible/immortal.
-- DONE: Emission on/off
-- DONE: Damage?
-- DONE: Options EPLRS

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new OPSGROUP class object.
-- @param #OPSGROUP self
-- @param Wrapper.Group#GROUP group The GROUP object. Can also be given by its group name as `#string`.
-- @return #OPSGROUP self
function OPSGROUP:New(group)
    
  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #OPSGROUP

  -- Get group and group name.
  if type(group)=="string" then
    self.groupname=group
    self.group=GROUP:FindByName(self.groupname)
  else
    self.group=group
    self.groupname=group:GetName()
  end

  -- Set some string id for output to DCS.log file.
  self.lid=string.format("OPSGROUP %s | ", tostring(self.groupname))

  -- Check if group exists.
  if self.group then
    if not self:IsExist() then
      self:E(self.lid.."ERROR: GROUP does not exist! Returning nil")
      return nil
    end
  end
  
  if UTILS.IsInstanceOf(group,"OPSGROUP") then
    self:E(self.lid.."ERROR: GROUP is already an OPSGROUP: "..tostring(self.groupname).."!")
    return group
  end

  -- Set the template.
  self:_SetTemplate()

  -- Set DCS group and controller.
  self.dcsgroup=self:GetDCSGroup()
  self.controller=self.dcsgroup:getController()

  -- Category.
  self.category=self.dcsgroup:getCategory()
  if self.category==Group.Category.GROUND then
    self.isArmygroup=true
  elseif self.category==Group.Category.TRAIN then
    self.isArmygroup=true
    self.isTrain=true
  elseif self.category==Group.Category.SHIP then
    self.isNavygroup=true
  elseif self.category==Group.Category.AIRPLANE then
    self.isFlightgroup=true
  elseif self.category==Group.Category.HELICOPTER then
    self.isFlightgroup=true
    self.isHelo=true
  else

  end
  
  -- Set gen attribute.
  self.attribute=self.group:GetAttribute()

  local units=self.group:GetUnits()

  if units then
    local masterunit=units[1] --Wrapper.Unit#UNIT
    
    if masterunit then
      -- Get Descriptors.
      self.descriptors=masterunit:GetDesc()
  
      -- Set type name.
      self.actype=masterunit:GetTypeName()
  
      -- Is this a submarine.
      self.isSubmarine=masterunit:HasAttribute("Submarines")
  
      -- Has this a datalink?
      self.isEPLRS=masterunit:HasAttribute("Datalink")
  
      if self:IsFlightgroup() then
  
        self.rangemax=self.descriptors.range and self.descriptors.range*1000 or 500*1000
  
        self.ceiling=self.descriptors.Hmax
  
        self.tankertype=select(2, masterunit:IsTanker())
        self.refueltype=select(2, masterunit:IsRefuelable())
  
        --env.info("DCS Unit BOOM_AND_RECEPTACLE="..tostring(Unit.RefuelingSystem.BOOM_AND_RECEPTACLE))
        --env.info("DCS Unit PROBE_AND_DROGUE="..tostring(Unit.RefuelingSystem.PROBE_AND_DROGUE))
  
      end
    end
  end

  -- Init set of detected units.
  self.detectedunits=SET_UNIT:New()

  -- Init set of detected groups.
  self.detectedgroups=SET_GROUP:New()

  -- Init inzone set.
  self.inzones=SET_ZONE:New()

  -- Set Default altitude.
  self:SetDefaultAltitude()
  
  -- Group will return to its legion when done.
  self:SetReturnToLegion()

  -- Laser.
  self.spot={}
  self.spot.On=false
  self.spot.timer=TIMER:New(self._UpdateLaser, self)
  self.spot.Coordinate=COORDINATE:New(0, 0, 0)
  self:SetLaser(1688, true, false, 0.5)

  -- Cargo.
  self.cargoStatus=OPSGROUP.CargoStatus.NOTCARGO
  self.carrierStatus=OPSGROUP.CarrierStatus.NOTCARRIER
  self:SetCarrierLoaderAllAspect()
  self:SetCarrierUnloaderAllAspect()

  -- Init task counter.
  self.taskcurrent=0
  self.taskcounter=0

  -- Start state.
  self:SetStartState("InUtero")

  -- Add FSM transitions.
  --                 From State  -->   Event      -->     To State
  self:AddTransition("InUtero",       "Spawned",          "Spawned")     -- The whole group was spawned.
  self:AddTransition("*",             "Respawn",          "InUtero")     -- Respawn group.
  self:AddTransition("*",             "Dead",             "InUtero")     -- The whole group is dead and goes back to mummy.
  self:AddTransition("*",             "InUtero",          "InUtero")     -- Deactivated group goes back to mummy.
  self:AddTransition("*",             "Stop",             "Stopped")     -- Stop FSM.

  self:AddTransition("*",             "Hit",              "*")           -- Someone in the group was hit.
  self:AddTransition("*",             "Damaged",          "*")           -- Someone in the group took damage.
  self:AddTransition("*",             "Destroyed",        "*")           -- The whole group is dead.

  self:AddTransition("*",             "UpdateRoute",      "*")           -- Update route of group.

  self:AddTransition("*",             "PassingWaypoint",   "*")           -- Group passed a waypoint.
  self:AddTransition("*",             "PassedFinalWaypoint", "*")         -- Group passed the waypoint.
  self:AddTransition("*",             "GotoWaypoint",      "*")           -- Group switches to a specific waypoint.

  self:AddTransition("*",             "Wait",              "*")           -- Group will wait for further orders.
  self:AddTransition("*",             "Stuck",             "*")           -- Group got stuck.

  self:AddTransition("*",             "DetectedUnit",      "*")           -- Unit was detected (again) in this detection cycle.
  self:AddTransition("*",             "DetectedUnitNew",   "*")           -- Add a newly detected unit to the detected units set.
  self:AddTransition("*",             "DetectedUnitKnown", "*")           -- A known unit is still detected.
  self:AddTransition("*",             "DetectedUnitLost",  "*")           -- Group lost a detected target.

  self:AddTransition("*",             "DetectedGroup",      "*")          -- Group was detected (again) in this detection cycle.
  self:AddTransition("*",             "DetectedGroupNew",   "*")          -- Add a newly detected Group to the detected Groups set.
  self:AddTransition("*",             "DetectedGroupKnown", "*")          -- A known Group is still detected.
  self:AddTransition("*",             "DetectedGroupLost",  "*")          -- Group lost a detected target group.

  self:AddTransition("*",             "OutOfAmmo",         "*")          -- Group is completely out of ammo.
  self:AddTransition("*",             "OutOfGuns",         "*")          -- Group is out of gun shells.
  self:AddTransition("*",             "OutOfRockets",      "*")          -- Group is out of rockets.
  self:AddTransition("*",             "OutOfBombs",        "*")          -- Group is out of bombs.
  self:AddTransition("*",             "OutOfMissiles",     "*")          -- Group is out of missiles.
  self:AddTransition("*",             "OutOfTorpedos",     "*")          -- Group is out of torpedos.

  self:AddTransition("*",             "OutOfMissilesAA",   "*")          -- Group is out of A2A (air) missiles.
  self:AddTransition("*",             "OutOfMissilesAG",   "*")          -- Group is out of A2G (ground) missiles.
  self:AddTransition("*",             "OutOfMissilesAS",   "*")          -- Group is out of A2S (ship) missiles.

  self:AddTransition("*",             "EnterZone",        "*")           -- Group entered a certain zone.
  self:AddTransition("*",             "LeaveZone",        "*")           -- Group leaves a certain zone.

  self:AddTransition("*",             "LaserOn",          "*")            -- Turn laser on.
  self:AddTransition("*",             "LaserOff",         "*")            -- Turn laser off.
  self:AddTransition("*",             "LaserCode",        "*")            -- Switch laser code.
  self:AddTransition("*",             "LaserPause",       "*")            -- Turn laser off temporarily.
  self:AddTransition("*",             "LaserResume",       "*")           -- Turn laser back on again if it was paused.
  self:AddTransition("*",             "LaserLostLOS",     "*")            -- Lasing element lost line of sight.
  self:AddTransition("*",             "LaserGotLOS",      "*")            -- Lasing element got line of sight.

  self:AddTransition("*",             "TaskExecute",      "*")           -- Group will execute a task.
  self:AddTransition("*",             "TaskPause",        "*")           -- Pause current task. Not implemented yet!
  self:AddTransition("*",             "TaskCancel",       "*")           -- Cancel current task.
  self:AddTransition("*",             "TaskDone",         "*")           -- Task is over.

  self:AddTransition("*",             "MissionStart",     "*")           -- Mission is started.
  self:AddTransition("*",             "MissionExecute",   "*")           -- Mission execution began.
  self:AddTransition("*",             "MissionCancel",    "*")           -- Cancel current mission.
  self:AddTransition("*",             "PauseMission",     "*")           -- Pause the current mission.
  self:AddTransition("*",             "UnpauseMission",   "*")           -- Unpause the the paused mission.
  self:AddTransition("*",             "MissionDone",      "*")           -- Mission is over.

  self:AddTransition("*",             "ElementInUtero",   "*")           -- An element is in utero again.
  self:AddTransition("*",             "ElementSpawned",   "*")           -- An element was spawned.
  self:AddTransition("*",             "ElementDestroyed", "*")           -- An element was destroyed.
  self:AddTransition("*",             "ElementDead",      "*")           -- An element is dead.
  self:AddTransition("*",             "ElementDamaged",   "*")           -- An element was damaged.
  self:AddTransition("*",             "ElementHit",       "*")           -- An element was hit.

  self:AddTransition("*",             "Board",            "*")           -- Group is ordered to board the carrier.
  self:AddTransition("*",             "Embarked",         "*")           -- Group was loaded into a cargo carrier.
  self:AddTransition("*",             "Disembarked",      "*")           -- Group was unloaded from a cargo carrier.

  self:AddTransition("*",             "Pickup",           "*")           -- Carrier and is on route to pick up cargo.
  self:AddTransition("*",             "Loading",          "*")           -- Carrier is loading cargo.
  self:AddTransition("*",             "Load",             "*")           -- Carrier loads cargo into carrier.
  self:AddTransition("*",             "Loaded",           "*")           -- Carrier loaded cargo into carrier.
  self:AddTransition("*",             "LoadingDone",      "*")           -- Carrier loaded all assigned/possible cargo into carrier.
  self:AddTransition("*",             "Transport",        "*")           -- Carrier is transporting cargo.
  self:AddTransition("*",             "Unloading",        "*")           -- Carrier is unloading the cargo.
  self:AddTransition("*",             "Unload",           "*")           -- Carrier unloads a cargo group.
  self:AddTransition("*",             "Unloaded",         "*")           -- Carrier unloaded a cargo group.
  self:AddTransition("*",             "UnloadingDone",    "*")           -- Carrier unloaded all its current cargo.
  self:AddTransition("*",             "Delivered",        "*")           -- Carrier delivered ALL cargo of the transport assignment.

  self:AddTransition("*",             "TransportCancel",  "*")           -- Cancel (current) transport.

  self:AddTransition("*",             "HoverStart",        "*")           -- Helo group is hovering
  self:AddTransition("*",             "HoverEnd",        "*")           -- Helo group is flying on    
  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Stop". Stops the OPSGROUP and all its event handlers.
  -- @function [parent=#OPSGROUP] Stop
  -- @param #OPSGROUP self

  --- Triggers the FSM event "Stop" after a delay. Stops the OPSGROUP and all its event handlers.
  -- @function [parent=#OPSGROUP] __Stop
  -- @param #OPSGROUP self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Status".
  -- @function [parent=#OPSGROUP] Status
  -- @param #OPSGROUP self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#OPSGROUP] __Status
  -- @param #OPSGROUP self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "MissionStart".
  -- @function [parent=#OPSGROUP] MissionStart
  -- @param #OPSGROUP self
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- Triggers the FSM event "MissionStart" after a delay.
  -- @function [parent=#OPSGROUP] __MissionStart
  -- @param #OPSGROUP self
  -- @param #number delay Delay in seconds.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "MissionStart" event.
  -- @function [parent=#OPSGROUP] OnAfterMissionStart
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.


  --- Triggers the FSM event "MissionExecute".
  -- @function [parent=#OPSGROUP] MissionExecute
  -- @param #OPSGROUP self
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- Triggers the FSM event "MissionExecute" after a delay.
  -- @function [parent=#OPSGROUP] __MissionExecute
  -- @param #OPSGROUP self
  -- @param #number delay Delay in seconds.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "MissionExecute" event.
  -- @function [parent=#OPSGROUP] OnAfterMissionExecute
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.


  --- Triggers the FSM event "MissionCancel".
  -- @function [parent=#OPSGROUP] MissionCancel
  -- @param #OPSGROUP self
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- Triggers the FSM event "MissionCancel" after a delay.
  -- @function [parent=#OPSGROUP] __MissionCancel
  -- @param #OPSGROUP self
  -- @param #number delay Delay in seconds.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "MissionCancel" event.
  -- @function [parent=#OPSGROUP] OnAfterMissionCancel
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.


  --- Triggers the FSM event "MissionDone".
  -- @function [parent=#OPSGROUP] MissionDone
  -- @param #OPSGROUP self
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- Triggers the FSM event "MissionDone" after a delay.
  -- @function [parent=#OPSGROUP] __MissionDone
  -- @param #OPSGROUP self
  -- @param #number delay Delay in seconds.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "MissionDone" event.
  -- @function [parent=#OPSGROUP] OnAfterMissionDone
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.Auftrag#AUFTRAG Mission The mission.

  --- On after "HoverStart" event.
  -- @function [parent=#OPSGROUP] OnAfterHoverStart
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
    --- On after "HoverEnd" event.
  -- @function [parent=#OPSGROUP] OnAfterHoverEnd
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- Triggers the FSM event "TransportCancel".
  -- @function [parent=#OPSGROUP] TransportCancel
  -- @param #OPSGROUP self
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.

  --- Triggers the FSM event "TransportCancel" after a delay.
  -- @function [parent=#OPSGROUP] __TransportCancel
  -- @param #OPSGROUP self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.

  --- On after "TransportCancel" event.
  -- @function [parent=#OPSGROUP] OnAfterTransportCancel
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.
  
    --- On After "DetectedGroup" event.
  -- @function [parent=#OPSGROUP] OnAfterDetectedGroup
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Group#Group Group Detected Group.
  
  --- On After "DetectedGroupNew" event.
  -- @function [parent=#OPSGROUP] OnAfterDetectedGroupNew
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Group#Group Group Newly detected group.
  
  --- On After "DetectedGroupKnown" event.
  -- @function [parent=#OPSGROUP] OnAfterDetectedGroupKnown
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Group#Group Group Known detected group.
  
  --- On After "DetectedGroupLost" event.
  -- @function [parent=#OPSGROUP] OnAfterDetectedGroupLost
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Group#Group Group Lost detected group.
  
  --- On After "DetectedUnit" event.
  -- @function [parent=#OPSGROUP] OnAfterDetectedUnit
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Unit#Unit Unit Detected Unit.
  
  --- On After "DetectedUnitNew" event.
  -- @function [parent=#OPSGROUP] OnAfterDetectedUnitNew
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Unit#Unit Unit Newly detected unit.
  
  --- On After "DetectedUnitKnown" event.
  -- @function [parent=#OPSGROUP] OnAfterDetectedUnitKnown
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Unit#Unit Unit Known detected unit.
  
  --- On After "DetectedUnitLost" event.
  -- @function [parent=#OPSGROUP] OnAfterDetectedUnitLost
  -- @param #OPSGROUP self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Unit#Unit Unit Lost detected unit.
  
  -- TODO: Add pseudo functions.

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get coalition.
-- @param #OPSGROUP self
-- @return #number Coalition side of carrier.
function OPSGROUP:GetCoalition()
  return self.group:GetCoalition()
end

--- Returns the absolute total life points of the group.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Element Element (Optional) Only get life points of this element.
-- @return #number Life points, *i.e.* the sum of life points over all units in the group (unless a specific element was passed).  
-- @return #number Initial life points.
function OPSGROUP:GetLifePoints(Element)

  local life=0
  local life0=0

  if Element then

    local unit=Element.unit

    if unit then
      life=unit:GetLife()
      life0=unit:GetLife0()
      life=math.min(life, life0) -- Some units have more life than life0 returns!
    end

  else

    for _,element in pairs(self.elements) do
      local l,l0=self:GetLifePoints(element)
      life=life+l
      life0=life0+l0
    end

  end

  return life, life0
end

--- Get generalized attribute.
-- @param #OPSGROUP self
-- @return #string Generalized attribute.
function OPSGROUP:GetAttribute()
  return self.attribute
end

--- Set verbosity level.
-- @param #OPSGROUP self
-- @param #number VerbosityLevel Level of output (higher=more). Default 0.
-- @return #OPSGROUP self
function OPSGROUP:SetVerbosity(VerbosityLevel)
  self.verbose=VerbosityLevel or 0
  return self
end

--- Set legion this ops group belongs to.
-- @param #OPSGROUP self
-- @param Ops.Legion#LEGION Legion The Legion.
-- @return #OPSGROUP self
function OPSGROUP:_SetLegion(Legion)
  self:T2(self.lid..string.format("Adding opsgroup to legion %s", Legion.alias))
  self.legion=Legion
  return self
end

--- **[GROUND, NAVAL]** Set whether this group should return to its legion once all mission etc are finished. Only for ground and naval groups. Aircraft will 
-- @param #OPSGROUP self
-- @param #boolean Switch If `true` or `nil`, group will return. If `false`, group will not return and stay where it finishes its last mission.
-- @return #OPSGROUP self
function OPSGROUP:SetReturnToLegion(Switch)
  if Switch==false then
    self.legionReturn=false
  else
    self.legionReturn=true
  end
  self:T(self.lid..string.format("Setting ReturnToLegion=%s", tostring(self.legionReturn)))
  return self
end

--- Set default cruise speed.
-- @param #OPSGROUP self
-- @param #number Speed Speed in knots.
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultSpeed(Speed)
  if Speed then
    self.speedCruise=UTILS.KnotsToKmph(Speed)
  end
  return self
end

--- Get default cruise speed.
-- @param #OPSGROUP self
-- @return #number Cruise speed (>0) in knots.
function OPSGROUP:GetSpeedCruise()
  local speed=UTILS.KmphToKnots(self.speedCruise or self.speedMax*0.7)
  return speed
end

--- Set default cruise altitude.
-- @param #OPSGROUP self
-- @param #number Altitude Altitude in feet. Default is 10,000 ft for airplanes and 1,500 feet for helicopters.
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultAltitude(Altitude)
  if Altitude then
    self.altitudeCruise=UTILS.FeetToMeters(Altitude)
  else
    if self:IsFlightgroup() then
      if self.isHelo then
        self.altitudeCruise=UTILS.FeetToMeters(1500)
      else
        self.altitudeCruise=UTILS.FeetToMeters(10000)
      end
    else
      self.altitudeCruise=0
    end
  end
  return self
end

--- Get default cruise speed.
-- @param #OPSGROUP self
-- @return #number Cruise altitude in feet.
function OPSGROUP:GetCruiseAltitude()
  local alt=UTILS.MetersToFeet(self.altitudeCruise)
  return alt
end

--- Set current altitude.
-- @param #OPSGROUP self
-- @param #number Altitude Altitude in feet. Default is 10,000 ft for airplanes and 1,500 feet for helicopters.
-- @param #boolean Keep If `true` the group will maintain that speed on passing waypoints. If `nil` or `false` the group will return to the speed as defined by their route.
-- @return #OPSGROUP self
function OPSGROUP:SetAltitude(Altitude, Keep, RadarAlt)
  if Altitude then
    Altitude=UTILS.FeetToMeters(Altitude)
  else
    if self:IsFlightgroup() then
      if self.isHelo then
        Altitude=UTILS.FeetToMeters(1500)
      else
        Altitude=UTILS.FeetToMeters(10000)
      end
    else
      Altitude=0
    end
  end
  
  local AltType="BARO"
  if RadarAlt then
    AltType="RADIO"
  end
  
  if self.controller then
    self.controller:setAltitude(Altitude, Keep, AltType)
  end
  
  return self
end

--- Set current altitude.
-- @param #OPSGROUP self
-- @return #number Altitude in feet.
function OPSGROUP:GetAltitude()

  local alt=0
  
  if self.group then

    alt=self.group:GetAltitude()
    
    alt=UTILS.MetersToFeet(alt)
    
  end

  return alt
end

--- Set current speed.
-- @param #OPSGROUP self
-- @param #number Speed Speed in knots. Default is 70% of max speed.
-- @param #boolean Keep If `true` the group will maintain that speed on passing waypoints. If `nil` or `false` the group will return to the speed as defined by their route.
-- @param #boolean AltCorrected If `true`, use altitude corrected indicated air speed.
-- @return #OPSGROUP self
function OPSGROUP:SetSpeed(Speed, Keep, AltCorrected)
  if Speed then
  
  else
    Speed=UTILS.KmphToKnots(self.speedMax)
  end
  
  
  if AltCorrected then
    local altitude=self:GetAltitude()
    Speed=UTILS.KnotsToAltKIAS(Speed, altitude)
  end
  
  Speed=UTILS.KnotsToMps(Speed)
  
  if self.controller then
    self.controller:setSpeed(Speed, Keep)
  end
  
  return self
end

--- Set detection on or off.
-- If detection is on, detected targets of the group will be evaluated and FSM events triggered.
-- @param #OPSGROUP self
-- @param #boolean Switch If `true`, detection is on. If `false` or `nil`, detection is off. Default is off.
-- @return #OPSGROUP self
function OPSGROUP:SetDetection(Switch)
  self:T(self.lid..string.format("Detection is %s", tostring(Switch)))
  self.detectionOn=Switch
  return self
end

--- Get DCS group object.
-- @param #OPSGROUP self
-- @return DCS#Group DCS group object.
function OPSGROUP:GetDCSObject()
  return self.dcsgroup
end

--- Set detection on or off.
-- If detection is on, detected targets of the group will be evaluated and FSM events triggered.
-- @param #OPSGROUP self
-- @param Wrapper.Positionable#POSITIONABLE TargetObject The target object.
-- @param #boolean KnowType Make type known.
-- @param #boolean KnowDist Make distance known.
-- @param #number Delay Delay in seconds before the target is known.
-- @return #OPSGROUP self
function OPSGROUP:KnowTarget(TargetObject, KnowType, KnowDist, Delay)

  if Delay and Delay>0 then
    -- Delayed call.
    self:ScheduleOnce(Delay, OPSGROUP.KnowTarget, self, TargetObject, KnowType, KnowDist, 0)  
  else
  
    if TargetObject:IsInstanceOf("GROUP") then
      TargetObject=TargetObject:GetUnit(1)
    elseif TargetObject:IsInstanceOf("OPSGROUP") then
      TargetObject=TargetObject.group:GetUnit(1)
    end

    -- Get the DCS object.
    local object=TargetObject:GetDCSObject()
    
    for _,_element in pairs(self.elements) do
      local element=_element --#OPSGROUP.Element
      if element.controller then
        element.controller:knowTarget(object, true, true)
        --self:T(self.lid..string.format("Element %s should now know target %s", element.name, TargetObject:GetName()))
      end
    end
    
    -- Debug info.
    self:T(self.lid..string.format("We should now know target %s", TargetObject:GetName()))
    
  end

  return self
end

--- Check if target is detected.
-- @param #OPSGROUP self
-- @param Wrapper.Positionable#POSITIONABLE TargetObject The target object.
-- @return #boolean If `true`, target was detected.
function OPSGROUP:IsTargetDetected(TargetObject)
    
  local objects={}
  
  if TargetObject:IsInstanceOf("GROUP") then
    for _,unit in pairs(TargetObject:GetUnits()) do
      table.insert(objects, unit:GetDCSObject())
    end
  elseif TargetObject:IsInstanceOf("OPSGROUP") then
    for _,unit in pairs(TargetObject.group:GetUnits()) do
      table.insert(objects, unit:GetDCSObject())
    end    
  elseif TargetObject:IsInstanceOf("UNIT") or TargetObject:IsInstanceOf("STATIC") then
    table.insert(objects, TargetObject:GetDCSObject())
  end
  
  for _,object in pairs(objects or {}) do
  
    -- Check group controller.
    local detected, visible, lastTime, type, distance, lastPos, lastVel = self.controller:isTargetDetected(object, 1, 2, 4, 8, 16, 32)
    
    --env.info(self.lid..string.format("Detected target %s: %s", TargetObject:GetName(), tostring(detected)))
    
    if detected then
      return true
    end
    
    
    -- Check all elements.
    for _,_element in pairs(self.elements) do
      local element=_element --#OPSGROUP.Element
      if element.controller then
      
        -- Check.
        local detected, visible, lastTime, type, distance, lastPos, lastVel=
        element.controller:isTargetDetected(object, 1, 2, 4, 8, 16, 32)
        
        --env.info(self.lid..string.format("Element %s detected target %s: %s", element.name, TargetObject:GetName(), tostring(detected)))
        
        if detected then
          return true
        end
        
      end
    end
    
  end 

  return false
end

--- Check if a given coordinate is in weapon range.
-- @param #OPSGROUP self
-- @param Core.Point#COORDINATE TargetCoord Coordinate of the target.
-- @param #number WeaponBitType Weapon type.
-- @param Core.Point#COORDINATE RefCoord Reference coordinate.
-- @return #boolean If `true`, coordinate is in range.
function OPSGROUP:InWeaponRange(TargetCoord, WeaponBitType, RefCoord)

  RefCoord=RefCoord or self:GetCoordinate()
  
  local dist=TargetCoord:Get2DDistance(RefCoord)

  if WeaponBitType then
  
    local weapondata=self:GetWeaponData(WeaponBitType)
    
    if weapondata then
    
      if dist>=weapondata.RangeMin and dist<=weapondata.RangeMax then
        return true
      else
        return false
      end
    
    end
    
  else
  
    for _,_weapondata in pairs(self.weaponData or {}) do
      local weapondata=_weapondata --#OPSGROUP.WeaponData

      if dist>=weapondata.RangeMin and dist<=weapondata.RangeMax then
        return true
      end
            
    end

    return false  
  end
    

  return nil
end

--- Get a coordinate, which is in weapon range.
-- @param #OPSGROUP self
-- @param Core.Point#COORDINATE TargetCoord Coordinate of the target.
-- @param #number WeaponBitType Weapon type.
-- @param Core.Point#COORDINATE RefCoord Reference coordinate.
-- @param #table SurfaceTypes Valid surfaces types of the coordinate. Default any (nil).
-- @return Core.Point#COORDINATE Coordinate in weapon range
function OPSGROUP:GetCoordinateInRange(TargetCoord, WeaponBitType, RefCoord, SurfaceTypes)

  local coordInRange=nil --Core.Point#COORDINATE
  
  RefCoord=RefCoord or self:GetCoordinate()

  -- Get weapon range.
  local weapondata=self:GetWeaponData(WeaponBitType)
  
  -- Heading intervals to search for a possible new coordinate in range.
  local dh={0, -5, 5, -10, 10, -15, 15, -20, 20, -25, 25, -30, 30, -35, 35, -40, 40, -45, 45, -50, 50, -55, 55, -60, 60, -65, 65, -70, 70, -75, 75, -80, 80}
  
  -- Function that checks if the given surface type is valid
  local function _checkSurface(point)
    if SurfaceTypes then
      local stype=point:GetSurfaceType()
      for _,sf in pairs(SurfaceTypes) do
        if sf==stype then
          return true
        end
      end
      return false
    else
      return true
    end
  end  
  
  if weapondata then
  
    -- Heading to target.
    local heading=TargetCoord:HeadingTo(RefCoord)
  
    -- Distance to target.
    local dist=RefCoord:Get2DDistance(TargetCoord)
    
    local range=nil
    if dist>weapondata.RangeMax then
      range=weapondata.RangeMax
      self:T(self.lid..string.format("Out of max range = %.1f km by %.1f km for weapon %s", weapondata.RangeMax/1000, (weapondata.RangeMax-dist)/1000, tostring(WeaponBitType)))
    elseif dist<weapondata.RangeMin then
      range=weapondata.RangeMin
      self:T(self.lid..string.format("Out of min range = %.1f km by %.1f km for weapon %s", weapondata.RangeMin/1000, (weapondata.RangeMin-dist)/1000, tostring(WeaponBitType)))
    end
  
    -- Check if we are within range.
    if range then
      
      for _,delta in pairs(dh) do
      
        local h=heading+delta
  
        -- New waypoint coord.
        coordInRange=TargetCoord:Translate(range, h)
        
        if _checkSurface(coordInRange) then
          break
        end
        
      end
      
    else  
      -- Debug info.
      self:T(self.lid..string.format("Already in range for weapon %s", tostring(WeaponBitType)))                    
    end
  
  else
    self:T(self.lid..string.format("No weapon data for weapon type %s", tostring(WeaponBitType)))
  end
  
  return coordInRange
end

--- Set LASER parameters.
-- @param #OPSGROUP self
-- @param #number Code Laser code. Default 1688.
-- @param #boolean CheckLOS Check if lasing unit has line of sight to target coordinate. Default is `true`.
-- @param #boolean IROff If true, then dont switch on the additional IR pointer.
-- @param #number UpdateTime Time interval in seconds the beam gets up for moving targets. Default every 0.5 sec.
-- @return #OPSGROUP self
function OPSGROUP:SetLaser(Code, CheckLOS, IROff, UpdateTime)
  self.spot.Code=Code or 1688
  if CheckLOS~=nil then
    self.spot.CheckLOS=CheckLOS
  else
    self.spot.CheckLOS=true
  end
  self.spot.IRon=not IROff
  self.spot.dt=UpdateTime or 0.5
  return self
end

--- Get LASER code.
-- @param #OPSGROUP self
-- @return #number Current Laser code.
function OPSGROUP:GetLaserCode()
  return self.spot.Code
end

--- Get current LASER coordinate, i.e. where the beam is pointing at if the LASER is on.
-- @param #OPSGROUP self
-- @return Core.Point#COORDINATE Current position where the LASER is pointing at.
function OPSGROUP:GetLaserCoordinate()
  return self.spot.Coordinate
end

--- Get current target of the LASER. This can be a STATIC or UNIT object.
-- @param #OPSGROUP self
-- @return Wrapper.Positionable#POSITIONABLE Current target object.
function OPSGROUP:GetLaserTarget()
  return self.spot.TargetUnit
end

--- Define a SET of zones that trigger and event if the group enters or leaves any of the zones.
-- @param #OPSGROUP self
-- @param Core.Set#SET_ZONE CheckZonesSet Set of zones.
-- @return #OPSGROUP self
function OPSGROUP:SetCheckZones(CheckZonesSet)
  self.checkzones=CheckZonesSet
  return self
end

--- Add a zone that triggers and event if the group enters or leaves any of the zones.
-- @param #OPSGROUP self
-- @param Core.Zone#ZONE CheckZone Zone to check.
-- @return #OPSGROUP self
function OPSGROUP:AddCheckZone(CheckZone)
  if not self.checkzones then
    self.checkzones=SET_ZONE:New()
  end
  self.checkzones:AddZone(CheckZone)
  return self
end


--- Add a weapon range for ARTY auftrag.
-- @param #OPSGROUP self
-- @param #number RangeMin Minimum range in nautical miles. Default 0 NM.
-- @param #number RangeMax Maximum range in nautical miles. Default 10 NM.
-- @param #number BitType Bit mask of weapon type for which the given min/max ranges apply. Default is `ENUMS.WeaponFlag.Auto`, i.e. for all weapon types.
-- @param #function ConversionToMeters Function that converts input units of ranges to meters. Defaul `UTILS.NMToMeters`.
-- @return #OPSGROUP self
function OPSGROUP:AddWeaponRange(RangeMin, RangeMax, BitType, ConversionToMeters)

  ConversionToMeters=ConversionToMeters or UTILS.NMToMeters

  RangeMin=ConversionToMeters(RangeMin or 0)
  RangeMax=ConversionToMeters(RangeMax or 10)

  local weapon={} --#OPSGROUP.WeaponData

  weapon.BitType=BitType or ENUMS.WeaponFlag.Auto
  weapon.RangeMax=RangeMax
  weapon.RangeMin=RangeMin

  self.weaponData=self.weaponData or {}
  self.weaponData[tostring(weapon.BitType)]=weapon

  return self
end

--- Get weapon data.
-- @param #OPSGROUP self
-- @param #number BitType Type of weapon.
-- @return #OPSGROUP.WeaponData Weapon range data.
function OPSGROUP:GetWeaponData(BitType)

  BitType=tostring(BitType or ENUMS.WeaponFlag.Auto)

  if self.weaponData[BitType] then
    return self.weaponData[BitType]
  else
    return self.weaponData[tostring(ENUMS.WeaponFlag.Auto)]
  end

end

--- Get set of detected units.
-- @param #OPSGROUP self
-- @return Core.Set#SET_UNIT Set of detected units.
function OPSGROUP:GetDetectedUnits()
  return self.detectedunits or {}
end

--- Get set of detected groups.
-- @param #OPSGROUP self
-- @return Core.Set#SET_GROUP Set of detected groups.
function OPSGROUP:GetDetectedGroups()
  return self.detectedgroups or {}
end

--- Get inital amount of ammunition.
-- @param #OPSGROUP self
-- @return #OPSGROUP.Ammo Initial ammo table.
function OPSGROUP:GetAmmo0()
  return self.ammo
end

--- Get highest detected threat. Detection must be turned on. The threat level is a number between 0 and 10, where 0 is the lowest, e.g. unarmed units.
-- @param #OPSGROUP self
-- @param #number ThreatLevelMin Only consider threats with level greater or equal to this number. Default 1 (so unarmed units wont be considered).
-- @param #number ThreatLevelMax Only consider threats with level smaller or queal to this number. Default 10.
-- @return Wrapper.Unit#UNIT Highest threat unit detected by the group or `nil` if no threat is currently detected.
-- @return #number Threat level.
function OPSGROUP:GetThreat(ThreatLevelMin, ThreatLevelMax)

  ThreatLevelMin=ThreatLevelMin or 1
  ThreatLevelMax=ThreatLevelMax or 10

  local threat=nil --Wrapper.Unit#UNIT
  local level=0
  for _,_unit in pairs(self.detectedunits:GetSet()) do
    local unit=_unit --Wrapper.Unit#UNIT

    -- Get threatlevel of unit.
    local threatlevel=unit:GetThreatLevel()

    -- Check if withing threasholds.
    if threatlevel>=ThreatLevelMin and threatlevel<=ThreatLevelMax then

      if threatlevel<level then
        level=threatlevel
        threat=unit
      end

    end

  end

  return threat, level
end

--- Get highest threat.
-- @param #OPSGROUP self
-- @return Wrapper.Unit#UNIT The highest threat unit.
-- @return #number Threat level of the unit.
function OPSGROUP:GetHighestThreat()

  local threat=nil
  local levelmax=-1
  for _,_unit in pairs(self.detectedunits:GetSet()) do
    local unit=_unit --Wrapper.Unit#UNIT

    local threatlevel=unit:GetThreatLevel()

    if threatlevel>levelmax then
      threat=unit
      levelmax=threatlevel
    end

  end

  return threat, levelmax
end

--- Enable to automatically engage detected targets.
-- @param #OPSGROUP self
-- @param #number RangeMax Max range in NM. Only detected targets within this radius from the group will be engaged. Default is 25 NM.
-- @param #table TargetTypes Types of target attributes that will be engaged. See [DCS enum attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes). Default "All".
-- @param Core.Set#SET_ZONE EngageZoneSet Set of zones in which targets are engaged. Default is anywhere.
-- @param Core.Set#SET_ZONE NoEngageZoneSet Set of zones in which targets are *not* engaged. Default is nowhere.
-- @return #OPSGROUP self
function OPSGROUP:SetEngageDetectedOn(RangeMax, TargetTypes, EngageZoneSet, NoEngageZoneSet)

  -- Ensure table.
  if TargetTypes then
    if type(TargetTypes)~="table" then
      TargetTypes={TargetTypes}
    end
  else
    TargetTypes={"All"}
  end

  -- Ensure SET_ZONE if ZONE is provided.
  if EngageZoneSet and EngageZoneSet:IsInstanceOf("ZONE_BASE") then
    local zoneset=SET_ZONE:New():AddZone(EngageZoneSet)
    EngageZoneSet=zoneset
  end
  if NoEngageZoneSet and NoEngageZoneSet:IsInstanceOf("ZONE_BASE") then
    local zoneset=SET_ZONE:New():AddZone(NoEngageZoneSet)
    NoEngageZoneSet=zoneset
  end

  -- Set parameters.
  self.engagedetectedOn=true
  self.engagedetectedRmax=UTILS.NMToMeters(RangeMax or 25)
  self.engagedetectedTypes=TargetTypes
  self.engagedetectedEngageZones=EngageZoneSet
  self.engagedetectedNoEngageZones=NoEngageZoneSet

  -- Debug info.
  self:T(self.lid..string.format("Engage detected ON: Rmax=%d NM", UTILS.MetersToNM(self.engagedetectedRmax)))

  -- Ensure detection is ON or it does not make any sense.
  self:SetDetection(true)

  return self
end

--- Disable to automatically engage detected targets.
-- @param #OPSGROUP self
-- @return #OPSGROUP self
function OPSGROUP:SetEngageDetectedOff()
  self:T(self.lid..string.format("Engage detected OFF"))
  self.engagedetectedOn=false
  return self
end

--- Set that group is going to rearm once it runs out of ammo.
-- @param #OPSGROUP self
-- @return #OPSGROUP self
function OPSGROUP:SetRearmOnOutOfAmmo()
  self.rearmOnOutOfAmmo=true
  return self
end

--- Set that group is retreating once it runs out of ammo.
-- @param #OPSGROUP self
-- @return #OPSGROUP self
function OPSGROUP:SetRetreatOnOutOfAmmo()
  self.retreatOnOutOfAmmo=true
  return self
end

--- Set that group is return to legion once it runs out of ammo.
-- @param #OPSGROUP self
-- @return #OPSGROUP self
function OPSGROUP:SetReturnOnOutOfAmmo()
  self.rtzOnOutOfAmmo=true
  return self
end

--- Set max weight that each unit of the group can handle.
-- @param #OPSGROUP self
-- @param #number Weight Max weight of cargo in kg the unit can carry.
-- @param #string UnitName Name of the Unit. If not given, weight is set for all units of the group.
-- @return #OPSGROUP self
function OPSGROUP:SetCargoBayLimit(Weight, UnitName)

  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element
    
    if UnitName==nil or UnitName==element.name then
    
      element.weightMaxCargo=Weight
      
      if element.unit then
        element.unit:SetCargoBayWeightLimit(Weight)
      end
      
    end
  
  end

  return self
end

--- Check if an element of the group has line of sight to a coordinate.
-- @param #OPSGROUP self
-- @param Core.Point#COORDINATE Coordinate The position to which we check the LoS. Can also be a DCS#Vec3.
-- @param #OPSGROUP.Element Element The (optinal) element. If not given, all elements are checked.
-- @param DCS#Vec3 OffsetElement Offset vector of the element.
-- @param DCS#Vec3 OffsetCoordinate Offset vector of the coordinate.
-- @return #boolean If `true`, there is line of sight to the specified coordinate.
function OPSGROUP:HasLoS(Coordinate, Element, OffsetElement, OffsetCoordinate)

  if Coordinate then

    -- Target vector.
    local Vec3={x=Coordinate.x, y=Coordinate.y, z=Coordinate.z} --Coordinate:GetVec3()
  
    -- Optional offset.
    if OffsetCoordinate then
      Vec3=UTILS.VecAdd(Vec3, OffsetCoordinate)
    end
  
    --- Function to check LoS for an element of the group.
    local function checklos(vec3)
      if vec3 then
        if OffsetElement then
          vec3=UTILS.VecAdd(vec3, OffsetElement)
        end
        local _los=land.isVisible(vec3, Vec3)
        --self:I({los=_los, source=vec3, target=Vec3})
        return _los
      end
      return nil      
    end
  
    if Element then
      -- Check los for the given element.
      if Element.unit and Element.unit:IsAlive() then
        local vec3=Element.unit:GetVec3()
        local los=checklos(vec3)
        return los
      end
    else
  
      -- Check if any element has los.
      local gotit=false
      for _,_element in pairs(self.elements) do
        local element=_element --#OPSGROUP.Element
        if element and element.unit and element.unit:IsAlive() then
          gotit=true
          local vec3=element.unit:GetVec3()
          -- Get LoS of this element.
          local los=checklos(vec3)
          if los then
            return true
          end
        end
      end
  
      if gotit then
        return false
      end
    end
    
  end

  return nil
end

--- Get MOOSE GROUP object.
-- @param #OPSGROUP self
-- @return Wrapper.Group#GROUP Moose group object.
function OPSGROUP:GetGroup()
  return self.group
end

--- Get the group name.
-- @param #OPSGROUP self
-- @return #string Group name.
function OPSGROUP:GetName()
  return self.groupname
end

--- Get DCS GROUP object.
-- @param #OPSGROUP self
-- @return DCS#Group DCS group object.
function OPSGROUP:GetDCSGroup()
  local DCSGroup=Group.getByName(self.groupname)
  return DCSGroup
end

--- Get MOOSE UNIT object.
-- @param #OPSGROUP self
-- @param #number UnitNumber Number of the unit in the group. Default first unit.
-- @return Wrapper.Unit#UNIT The MOOSE UNIT object.
function OPSGROUP:GetUnit(UnitNumber)

  local DCSUnit=self:GetDCSUnit(UnitNumber)

  if DCSUnit then
    local unit=UNIT:Find(DCSUnit)
    return unit
  end

  return nil
end

--- Get DCS GROUP object.
-- @param #OPSGROUP self
-- @param #number UnitNumber Number of the unit in the group. Default first unit.
-- @return DCS#Unit DCS group object.
function OPSGROUP:GetDCSUnit(UnitNumber)

  local DCSGroup=self:GetDCSGroup()

  if DCSGroup then
    local unit=DCSGroup:getUnit(UnitNumber or 1)
    return unit
  else
    self:E(self.lid..string.format("ERROR: DCS group does not exist! Cannot get unit"))
  end

  return nil
end

--- Get DCS units.
-- @param #OPSGROUP self
-- @return #list<DCS#Unit> DCS units.
function OPSGROUP:GetDCSUnits()

  local DCSGroup=self:GetDCSGroup()

  if DCSGroup then
    local units=DCSGroup:getUnits()
    return units
  end

  return nil
end


--- Get current 2D position vector of the group.
-- @param #OPSGROUP self
-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group.
-- @return DCS#Vec2 Vector with x,y components.
function OPSGROUP:GetVec2(UnitName)

  local vec3=self:GetVec3(UnitName)

  if vec3 then
    local vec2={x=vec3.x, y=vec3.z}
    return vec2
  end

  return nil
end


--- Get current 3D position vector of the group.
-- @param #OPSGROUP self
-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group.
-- @return DCS#Vec3 Vector with x,y,z components.
function OPSGROUP:GetVec3(UnitName)

  local vec3=nil --DCS#Vec3

  -- First check if this group is loaded into a carrier
  local carrier=self:_GetMyCarrierElement()
  if carrier and carrier.status~=OPSGROUP.ElementStatus.DEAD and self:IsLoaded() then
    local unit=carrier.unit
    if unit and unit:IsExist() then
      vec3=unit:GetVec3()
      return vec3
    end
  end

  if self:IsExist() then

    local unit=nil --DCS#Unit
    if UnitName then
      unit=Unit.getByName(UnitName)
    else
      unit=self:GetDCSUnit()
    end


    if unit then
      local vec3=unit:getPoint()
      return vec3
    end

  end

  -- Return last known position.
  if self.position then
    return self.position
  end

  return nil
end

--- Get current coordinate of the group. If the current position cannot be determined, the last known position is returned.
-- @param #OPSGROUP self
-- @param #boolean NewObject Create a new coordiante object.
-- @param #string UnitName (Optional) Get position of a specifc unit of the group. Default is the first existing unit in the group.
-- @return Core.Point#COORDINATE The coordinate (of the first unit) of the group.
function OPSGROUP:GetCoordinate(NewObject, UnitName)

  local vec3=self:GetVec3(UnitName) or self.position --DCS#Vec3

  if vec3 then

    self.coordinate=self.coordinate or COORDINATE:New(0,0,0)

    self.coordinate.x=vec3.x
    self.coordinate.y=vec3.y
    self.coordinate.z=vec3.z

    if NewObject then
      local coord=COORDINATE:NewFromCoordinate(self.coordinate)
      return coord
    else
      return self.coordinate
    end
  else
    self:T(self.lid.."WARNING: Cannot get coordinate!")
  end

  return nil
end

--- Get current velocity of the group.
-- @param #OPSGROUP self
-- @param #string UnitName (Optional) Get velocity of a specific unit of the group. Default is from the first existing unit in the group.
-- @return #number Velocity in m/s.
function OPSGROUP:GetVelocity(UnitName)

  if self:IsExist() then

    local unit=nil --DCS#Unit

    if UnitName then
      unit=Unit.getByName(UnitName)
    else
      unit=self:GetDCSUnit()
    end

    if unit then

      local velvec3=unit:getVelocity()

      local vel=UTILS.VecNorm(velvec3)

      return vel

    else
      self:T(self.lid.."WARNING: Unit does not exist. Cannot get velocity!")
    end

  else
    self:T(self.lid.."WARNING: Group does not exist. Cannot get velocity!")
  end

  return nil
end

--- Get current heading of the group or (optionally) of a specific unit of the group.
-- @param #OPSGROUP self
-- @param #string UnitName (Optional) Get heading of a specific unit of the group. Default is from the first existing unit in the group.
-- @return #number Current heading of the group in degrees.
function OPSGROUP:GetHeading(UnitName)

  if self:IsExist() then

    local unit=nil --DCS#Unit
    if UnitName then
      unit=Unit.getByName(UnitName)
    else
      unit=self:GetDCSUnit()
    end

    if unit then

      local pos=unit:getPosition()

      local heading=math.atan2(pos.x.z, pos.x.x)

      if heading<0 then
        heading=heading+ 2*math.pi
      end

      heading=math.deg(heading)

      return heading
    end

  else
    self:T(self.lid.."WARNING: Group does not exist. Cannot get heading!")
  end

  return nil
end

--- Get current orientation of the group.
-- @param #OPSGROUP self
-- @param #string UnitName (Optional) Get orientation of a specific unit of the group. Default is the first existing unit of the group.
-- @return DCS#Vec3 Orientation X parallel to where the "nose" is pointing.
-- @return DCS#Vec3 Orientation Y pointing "upwards".
-- @return DCS#Vec3 Orientation Z perpendicular to the "nose".
function OPSGROUP:GetOrientation(UnitName)

  if self:IsExist() then

    local unit=nil --DCS#Unit

    if UnitName then
      unit=Unit.getByName(UnitName)
    else
      unit=self:GetDCSUnit()
    end

    if unit then

      local pos=unit:getPosition()

      return pos.x, pos.y, pos.z
    end

  else
    self:T(self.lid.."WARNING: Group does not exist. Cannot get orientation!")
  end

  return nil
end

--- Get current "X" orientation of the first unit in the group.
-- @param #OPSGROUP self
-- @param #string UnitName (Optional) Get orientation of a specific unit of the group. Default is the first existing unit of the group.
-- @return DCS#Vec3 Orientation X parallel to where the "nose" is pointing.
function OPSGROUP:GetOrientationX(UnitName)

  local X,Y,Z=self:GetOrientation(UnitName)

  return X
end



--- Check if task description is unique.
-- @param #OPSGROUP self
-- @param #string description Task destription
-- @return #boolean If true, no other task has the same description.
function OPSGROUP:CheckTaskDescriptionUnique(description)

  -- Loop over tasks in queue
  for _,_task in pairs(self.taskqueue) do
    local task=_task --#OPSGROUP.Task
    if task.description==description then
      return false
    end
  end

  return true
end


--- Despawn a unit of the group. A "Remove Unit" event is generated by default.
-- @param #OPSGROUP self
-- @param #string UnitName Name of the unit
-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately.
-- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated.
-- @return #OPSGROUP self
function OPSGROUP:DespawnUnit(UnitName, Delay, NoEventRemoveUnit)

  -- Debug info.
  self:T(self.lid.."Despawn element "..tostring(UnitName))

  -- Get element.
  local element=self:GetElementByName(UnitName)

  if element then

    -- Get DCS unit object.
    local DCSunit=Unit.getByName(UnitName)

    if DCSunit then

      -- Despawn unit.
      DCSunit:destroy()

      -- Element goes back in utero.
      self:ElementInUtero(element)

      if not NoEventRemoveUnit then
        self:CreateEventRemoveUnit(timer.getTime(), DCSunit)
      end

    end

  end

end

--- Despawn an element/unit of the group.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Element Element The element that will be despawned.
-- @param #number Delay Delay in seconds before the element will be despawned. Default immediately.
-- @param #boolean NoEventRemoveUnit If true, no event "Remove Unit" is generated.
-- @return #OPSGROUP self
function OPSGROUP:DespawnElement(Element, Delay, NoEventRemoveUnit)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, OPSGROUP.DespawnElement, self, Element, 0, NoEventRemoveUnit)
  else

    if Element then

      -- Get DCS unit object.
      local DCSunit=Unit.getByName(Element.name)

      if DCSunit then

        -- Destroy object.
        DCSunit:destroy()

        -- Create a remove unit event.
        if not NoEventRemoveUnit then
          self:CreateEventRemoveUnit(timer.getTime(), DCSunit)
        end

      end

    end

  end

  return self
end

--- Despawn the group. The whole group is despawned and a "`Remove Unit`" event is generated for all current units of the group.
-- If no `Remove Unit` event should be generated, the second optional parameter needs to be set to `true`.
-- If this group belongs to an AIRWING, BRIGADE or FLEET, it will be added to the warehouse stock if the `NoEventRemoveUnit` parameter is `false` or `nil`.
-- @param #OPSGROUP self
-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately.
-- @param #boolean NoEventRemoveUnit If `true`, **no** event "Remove Unit" is generated.
-- @return #OPSGROUP self
function OPSGROUP:Despawn(Delay, NoEventRemoveUnit)

  if Delay and Delay>0 then
    self.scheduleIDDespawn=self:ScheduleOnce(Delay, OPSGROUP.Despawn, self, 0, NoEventRemoveUnit)
  else

    -- Debug info.
    self:T(self.lid..string.format("Despawning Group!"))

    -- DCS group obejct.
    local DCSGroup=self:GetDCSGroup()

    if DCSGroup then

      -- Clear any task ==> makes DCS crash!
      --self.group:ClearTasks()

      -- Get all units.
      local units=self:GetDCSUnits()

      for i=1,#units do
        local unit=units[i]
        if unit then
          local name=unit:getName()
          if name then
            -- Despawn the unit.
            self:DespawnUnit(name, 0, NoEventRemoveUnit)
          end
        end
      end

    end
  end

  return self
end

--- Return group back to the legion it belongs to.
-- Group is despawned and added back to the stock.
-- @param #OPSGROUP self
-- @param #number Delay Delay in seconds before the group will be despawned. Default immediately
-- @return #OPSGROUP self
function OPSGROUP:ReturnToLegion(Delay)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, OPSGROUP.ReturnToLegion, self)
  else

    if self.legion then
      -- Add asset back.
      self:T(self.lid..string.format("Adding asset back to LEGION"))
      self.legion:AddAsset(self.group, 1)
    else
      self:E(self.lid..string.format("ERROR: Group does not belong to a LEGION!"))
    end
    
  end

  return self
end

--- Destroy a unit of the group. A *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated.
-- @param #OPSGROUP self
-- @param #string UnitName Name of the unit which should be destroyed.
-- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately.
-- @return #OPSGROUP self
function OPSGROUP:DestroyUnit(UnitName, Delay)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, OPSGROUP.DestroyUnit, self, UnitName, 0)
  else

    local unit=Unit.getByName(UnitName)

    if unit then

      -- Create a "Unit Lost" event.
      local EventTime=timer.getTime()

      if self:IsFlightgroup() then
        self:CreateEventUnitLost(EventTime, unit)
      else
        self:CreateEventDead(EventTime, unit)
      end
      
      -- Despawn unit.
      unit:destroy()      

    end

  end

end

--- Destroy group. The whole group is despawned and a *Unit Lost* for aircraft or *Dead* event for ground/naval units is generated for all current units.
-- @param #OPSGROUP self
-- @param #number Delay Delay in seconds before the group will be destroyed. Default immediately.
-- @return #OPSGROUP self
function OPSGROUP:Destroy(Delay)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, OPSGROUP.Destroy, self, 0)
  else
  
    self:T(self.lid.."Destroying group!")

    -- Get all units.
    local units=self:GetDCSUnits()

    if units then

      -- Create a "Unit Lost" event.
      for _,unit in pairs(units) do
        if unit then
          self:DestroyUnit(unit:getName())
        end
      end

    end

  end

  return self
end

--- Activate a *late activated* group.
-- @param #OPSGROUP self
-- @param #number delay (Optional) Delay in seconds before the group is activated. Default is immediately.
-- @return #OPSGROUP self
function OPSGROUP:Activate(delay)

  if delay and delay>0 then
    self:T2(self.lid..string.format("Activating late activated group in %d seconds", delay))
    self:ScheduleOnce(delay, OPSGROUP.Activate, self)
  else

    if self:IsAlive()==false then

      self:T(self.lid.."Activating late activated group")
      self.group:Activate()
      self.isLateActivated=false

    elseif self:IsAlive()==true then
      self:T(self.lid.."WARNING: Activating group that is already activated")
    else
      self:T(self.lid.."ERROR: Activating group that is does not exist!")
    end

  end

  return self
end

--- Deactivate the group. Group will be respawned in late activated state.
-- @param #OPSGROUP self
-- @param #number delay (Optional) Delay in seconds before the group is deactivated. Default is immediately.
-- @return #OPSGROUP self
function OPSGROUP:Deactivate(delay)

  if delay and delay>0 then
    self:ScheduleOnce(delay, OPSGROUP.Deactivate, self)
  else

    if self:IsAlive()==true then

      self.template.lateActivation=true

      local template=UTILS.DeepCopy(self.template)

      self:_Respawn(0, template)

    end

  end

  return self
end

--- Self destruction of group. An explosion is created at the position of each element.
-- @param #OPSGROUP self
-- @param #number Delay Delay in seconds. Default now.
-- @param #number ExplosionPower (Optional) Explosion power in kg TNT. Default 100 kg.
-- @param #string ElementName Name of the element that should be destroyed. Default is all elements.
-- @return #OPSGROUP self
function OPSGROUP:SelfDestruction(Delay, ExplosionPower, ElementName)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, OPSGROUP.SelfDestruction, self, 0, ExplosionPower, ElementName)
  else

    -- Loop over all elements.
    for i,_element in pairs(self.elements) do
      local element=_element --#OPSGROUP.Element
      
      if ElementName==nil or ElementName==element.name then

        local unit=element.unit
  
        if unit and unit:IsAlive() then
          unit:Explode(ExplosionPower or 100)
        end
        
      end
    end
  end

  return self
end

--- Use SRS Simple-Text-To-Speech for transmissions.
-- @param #OPSGROUP self
-- @param #string PathToSRS Path to SRS directory.
-- @param #string Gender Gender: "male" or "female" (default).
-- @param #string Culture Culture, e.g. "en-GB" (default).
-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`.
-- @param #number Port SRS port. Default 5002.
-- @param #string PathToGoogleKey Full path to the google credentials JSON file, e.g. `"C:\Users\myUsername\Downloads\key.json"`.
-- @param #string Label Label of the SRS comms for the SRS Radio overlay. Defaults to "ROBOT". No spaces allowed!
-- @param #number Volume Volume to be set, 0.0 = silent, 1.0 = loudest. Defaults to 1.0
-- @return #OPSGROUP self
function OPSGROUP:SetSRS(PathToSRS, Gender, Culture, Voice, Port, PathToGoogleKey, Label, Volume)
  self.useSRS=true
  local path = PathToSRS or MSRS.path
  local port = Port or MSRS.port
  self.msrs=MSRS:New(path, self.frequency, self.modulation)
  self.msrs:SetGender(Gender)
  self.msrs:SetCulture(Culture)
  self.msrs:SetVoice(Voice)
  self.msrs:SetPort(port)
  self.msrs:SetLabel(Label)
  if PathToGoogleKey then
    self.msrs:SetProviderOptionsGoogle(PathToGoogleKey,PathToGoogleKey)
    self.msrs:SetProvider(MSRS.Provider.GOOGLE)
  end
  self.msrs:SetCoalition(self:GetCoalition())
  self.msrs:SetVolume(Volume)
  return self
end

--- Send a radio transmission via SRS Text-To-Speech.
-- @param #OPSGROUP self
-- @param #string Text Text of transmission.
-- @param #number Delay Delay in seconds before the transmission is started.
-- @param #boolean SayCallsign If `true`, the callsign is prepended to the given text. Default `false`.
-- @param #number Frequency Override sender frequency, helpful when you need multiple radios from the same sender. Default is the frequency set for the OpsGroup.
-- @return #OPSGROUP self
function OPSGROUP:RadioTransmission(Text, Delay, SayCallsign, Frequency)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, OPSGROUP.RadioTransmission, self, Text, 0, SayCallsign)
  else

    if self.useSRS and self.msrs then

      local freq, modu, radioon=self:GetRadio()
      
      local coord = self:GetCoordinate()
      
      self.msrs:SetCoordinate(coord)
      
      if Frequency then
        self.msrs:SetFrequencies(Frequency)
      else
        self.msrs:SetFrequencies(freq)
      end
      self.msrs:SetModulations(modu)
      
      if SayCallsign then
        local callsign=self:GetCallsignName()
        Text=string.format("%s, %s", callsign, Text)
      end

      -- Debug info.
      self:T(self.lid..string.format("Radio transmission on %.3f MHz %s: %s", freq, UTILS.GetModulationName(modu), Text))

      self.msrs:PlayText(Text)
    end

  end

  return self
end

--- Set that this carrier is an all aspect loader.
-- @param #OPSGROUP self
-- @param #number Length Length of loading zone in meters. Default 50 m.
-- @param #number Width Width of loading zone in meters. Default 20 m.
-- @return #OPSGROUP self
function OPSGROUP:SetCarrierLoaderAllAspect(Length, Width)
  self.carrierLoader.type="front"
  self.carrierLoader.length=Length or 50
  self.carrierLoader.width=Width or 20
  return self
end

--- Set that this carrier is a front loader.
-- @param #OPSGROUP self
-- @param #number Length Length of loading zone in meters. Default 50 m.
-- @param #number Width Width of loading zone in meters. Default 20 m.
-- @return #OPSGROUP self
function OPSGROUP:SetCarrierLoaderFront(Length, Width)
  self.carrierLoader.type="front"
  self.carrierLoader.length=Length or 50
  self.carrierLoader.width=Width or 20
  return self
end

--- Set that this carrier is a back loader.
-- @param #OPSGROUP self
-- @param #number Length Length of loading zone in meters. Default 50 m.
-- @param #number Width Width of loading zone in meters. Default 20 m.
-- @return #OPSGROUP self
function OPSGROUP:SetCarrierLoaderBack(Length, Width)
  self.carrierLoader.type="back"
  self.carrierLoader.length=Length or 50
  self.carrierLoader.width=Width or 20
  return self
end

--- Set that this carrier is a starboard (right side) loader.
-- @param #OPSGROUP self
-- @param #number Length Length of loading zone in meters. Default 50 m.
-- @param #number Width Width of loading zone in meters. Default 20 m.
-- @return #OPSGROUP self
function OPSGROUP:SetCarrierLoaderStarboard(Length, Width)
  self.carrierLoader.type="right"
  self.carrierLoader.length=Length or 50
  self.carrierLoader.width=Width or 20
  return self
end

--- Set that this carrier is a port (left side) loader.
-- @param #OPSGROUP self
-- @param #number Length Length of loading zone in meters. Default 50 m.
-- @param #number Width Width of loading zone in meters. Default 20 m.
-- @return #OPSGROUP self
function OPSGROUP:SetCarrierLoaderPort(Length, Width)
  self.carrierLoader.type="left"
  self.carrierLoader.length=Length or 50
  self.carrierLoader.width=Width or 20
  return self
end


--- Set that this carrier is an all aspect unloader.
-- @param #OPSGROUP self
-- @param #number Length Length of loading zone in meters. Default 50 m.
-- @param #number Width Width of loading zone in meters. Default 20 m.
-- @return #OPSGROUP self
function OPSGROUP:SetCarrierUnloaderAllAspect(Length, Width)
  self.carrierUnloader.type="front"
  self.carrierUnloader.length=Length or 50
  self.carrierUnloader.width=Width or 20
  return self
end

--- Set that this carrier is a front unloader.
-- @param #OPSGROUP self
-- @param #number Length Length of loading zone in meters. Default 50 m.
-- @param #number Width Width of loading zone in meters. Default 20 m.
-- @return #OPSGROUP self
function OPSGROUP:SetCarrierUnloaderFront(Length, Width)
  self.carrierUnloader.type="front"
  self.carrierUnloader.length=Length or 50
  self.carrierUnloader.width=Width or 20
  return self
end

--- Set that this carrier is a back unloader.
-- @param #OPSGROUP self
-- @param #number Length Length of loading zone in meters. Default 50 m.
-- @param #number Width Width of loading zone in meters. Default 20 m.
-- @return #OPSGROUP self
function OPSGROUP:SetCarrierUnloaderBack(Length, Width)
  self.carrierUnloader.type="back"
  self.carrierUnloader.length=Length or 50
  self.carrierUnloader.width=Width or 20
  return self
end

--- Set that this carrier is a starboard (right side) unloader.
-- @param #OPSGROUP self
-- @param #number Length Length of loading zone in meters. Default 50 m.
-- @param #number Width Width of loading zone in meters. Default 20 m.
-- @return #OPSGROUP self
function OPSGROUP:SetCarrierUnloaderStarboard(Length, Width)
  self.carrierUnloader.type="right"
  self.carrierUnloader.length=Length or 50
  self.carrierUnloader.width=Width or 20
  return self
end

--- Set that this carrier is a port (left side) unloader.
-- @param #OPSGROUP self
-- @param #number Length Length of loading zone in meters. Default 50 m.
-- @param #number Width Width of loading zone in meters. Default 20 m.
-- @return #OPSGROUP self
function OPSGROUP:SetCarrierUnloaderPort(Length, Width)
  self.carrierUnloader.type="left"
  self.carrierUnloader.length=Length or 50
  self.carrierUnloader.width=Width or 20
  return self
end

--- Check if group is currently inside a zone.
-- @param #OPSGROUP self
-- @param Core.Zone#ZONE Zone The zone.
-- @return #boolean If true, group is in this zone
function OPSGROUP:IsInZone(Zone)
  local vec2=self:GetVec2()
  local is=false
  if vec2 then
    is=Zone:IsVec2InZone(vec2)
  else
    self:T3(self.lid.."WARNING: Cannot get vec2 at IsInZone()!")
  end
  return is
end

--- Get 2D distance to a coordinate.
-- @param #OPSGROUP self
-- @param Core.Point#COORDINATE Coordinate. Can also be a DCS#Vec2 or DCS#Vec3.
-- @return #number Distance in meters.
function OPSGROUP:Get2DDistance(Coordinate)

  local a=self:GetVec2()
  local b={}
  if Coordinate.z then
    b.x=Coordinate.x
    b.y=Coordinate.z
  else
    b.x=Coordinate.x
    b.y=Coordinate.y
  end

  local dist=UTILS.VecDist2D(a, b)

  return dist
end

--- Check if this is a FLIGHTGROUP.
-- @param #OPSGROUP self
-- @return #boolean If true, this is an airplane or helo group.
function OPSGROUP:IsFlightgroup()
  return self.isFlightgroup
end

--- Check if this is a ARMYGROUP.
-- @param #OPSGROUP self
-- @return #boolean If true, this is a ground group.
function OPSGROUP:IsArmygroup()
  return self.isArmygroup
end

--- Check if this is a NAVYGROUP.
-- @param #OPSGROUP self
-- @return #boolean If true, this is a ship group.
function OPSGROUP:IsNavygroup()
  return self.isNavygroup
end


--- Check if group is exists.
-- @param #OPSGROUP self
-- @return #boolean If true, the group exists or false if the group does not exist. If nil, the DCS group could not be found.
function OPSGROUP:IsExist()

  local DCSGroup=self:GetDCSGroup()

  if DCSGroup then
    local exists=DCSGroup:isExist()
    return exists
  end

  return nil
end

--- Check if group is activated.
-- @param #OPSGROUP self
-- @return #boolean If true, the group exists or false if the group does not exist. If nil, the DCS group could not be found.
function OPSGROUP:IsActive()

  if self.group then
    local active=self.group:IsActive()
    return active
  end

  return nil
end

--- Check if group is alive.
-- @param #OPSGROUP self
-- @return #boolean *true* if group is exists and is activated, *false* if group is exist but is NOT activated. *nil* otherwise, e.g. the GROUP object is *nil* or the group is not spawned yet.
function OPSGROUP:IsAlive()

  if self.group then
    local alive=self.group:IsAlive()
    return alive
  end

  return nil
end

--- Check if this group is currently "late activated" and needs to be "activated" to appear in the mission.
-- @param #OPSGROUP self
-- @return #boolean Is this the group late activated?
function OPSGROUP:IsLateActivated()
  return self.isLateActivated
end

--- Check if group is in state in utero. Note that dead groups are also in utero but will return `false` here.
-- @param #OPSGROUP self
-- @return #boolean If true, group is not spawned yet.
function OPSGROUP:IsInUtero()
  local is=self:Is("InUtero") and not self:IsDead()
  return is
end

--- Check if group is in state spawned.
-- @param #OPSGROUP self
-- @return #boolean If true, group is spawned.
function OPSGROUP:IsSpawned()
  local is=self:Is("Spawned")
  return is
end

--- Check if group is dead. Could be destroyed or despawned. FSM state of dead group is `InUtero` though.
-- @param #OPSGROUP self
-- @return #boolean If true, all units/elements of the group are dead.
function OPSGROUP:IsDead()
  return self.isDead
end

--- Check if group was destroyed.
-- @param #OPSGROUP self
-- @return #boolean If true, all units/elements of the group were destroyed.
function OPSGROUP:IsDestroyed()
  return self.isDestroyed
end

--- Check if FSM is stopped.
-- @param #OPSGROUP self
-- @return #boolean If true, FSM state is stopped.
function OPSGROUP:IsStopped()
  local is=self:Is("Stopped")
  return is
end

--- Check if this group is currently "uncontrolled" and needs to be "started" to begin its route.
-- @param #OPSGROUP self
-- @return #boolean If true, this group uncontrolled.
function OPSGROUP:IsUncontrolled()
  return self.isUncontrolled
end

--- Check if this group has passed its final waypoint.
-- @param #OPSGROUP self
-- @return #boolean If true, this group has passed the final waypoint.
function OPSGROUP:HasPassedFinalWaypoint()
  return self.passedfinalwp
end

--- Check if the group is currently rearming or on its way to the rearming place.
-- @param #OPSGROUP self
-- @return #boolean If true, group is rearming.
function OPSGROUP:IsRearming()
  local rearming=self:Is("Rearming") or self:Is("Rearm")
  return rearming
end

--- Check if the group is completely out of ammo.
-- @param #OPSGROUP self
-- @return #boolean If `true`, group is out-of-ammo.
function OPSGROUP:IsOutOfAmmo()
  return self.outofAmmo
end

--- Check if the group is out of bombs.
-- @param #OPSGROUP self
-- @return #boolean If `true`, group is out of bombs.
function OPSGROUP:IsOutOfBombs()
  return self.outofBombs
end

--- Check if the group is out of guns.
-- @param #OPSGROUP self
-- @return #boolean If `true`, group is out of guns.
function OPSGROUP:IsOutOfGuns()
  return self.outofGuns
end

--- Check if the group is out of missiles.
-- @param #OPSGROUP self
-- @return #boolean If `true`, group is out of missiles.
function OPSGROUP:IsOutOfMissiles()
  return self.outofMissiles
end

--- Check if the group is out of torpedos.
-- @param #OPSGROUP self
-- @return #boolean If `true`, group is out of torpedos.
function OPSGROUP:IsOutOfTorpedos()
  return self.outofTorpedos
end


--- Check if the group has currently switched a LASER on.
-- @param #OPSGROUP self
-- @return #boolean If true, LASER of the group is on.
function OPSGROUP:IsLasing()
  return self.spot.On
end

--- Check if the group is currently retreating or retreated.
-- @param #OPSGROUP self
-- @return #boolean If true, group is retreating or retreated.
function OPSGROUP:IsRetreating()
  local is=self:is("Retreating") or self:is("Retreated")
  return is
end

--- Check if the group is retreated (has reached its retreat zone).
-- @param #OPSGROUP self
-- @return #boolean If true, group is retreated.
function OPSGROUP:IsRetreated()
  local is=self:is("Retreated")
  return is
end


--- Check if the group is currently returning to a zone.
-- @param #OPSGROUP self
-- @return #boolean If true, group is returning.
function OPSGROUP:IsReturning()
  local is=self:is("Returning")
  return is
end

--- Check if the group is engaging another unit or group.
-- @param #OPSGROUP self
-- @return #boolean If true, group is engaging.
function OPSGROUP:IsEngaging()
  local is=self:is("Engaging")
  return is
end

--- Check if group is currently waiting.
-- @param #OPSGROUP self
-- @return #boolean If true, group is currently waiting.
function OPSGROUP:IsWaiting()
  if self.Twaiting then
    return true
  end
  return false
end

--- Check if the group is not a carrier yet.
-- @param #OPSGROUP self
-- @return #boolean If true, group is not a carrier.
function OPSGROUP:IsNotCarrier()
  return self.carrierStatus==OPSGROUP.CarrierStatus.NOTCARRIER
end

--- Check if the group is a carrier.
-- @param #OPSGROUP self
-- @return #boolean If true, group is a carrier.
function OPSGROUP:IsCarrier()
  return not self:IsNotCarrier()
end

--- Check if the group is picking up cargo.
-- @param #OPSGROUP self
-- @return #boolean If true, group is picking up.
function OPSGROUP:IsPickingup()
  return self.carrierStatus==OPSGROUP.CarrierStatus.PICKUP
end

--- Check if the group is loading cargo.
-- @param #OPSGROUP self
-- @return #boolean If true, group is loading.
function OPSGROUP:IsLoading()
  return self.carrierStatus==OPSGROUP.CarrierStatus.LOADING
end

--- Check if the group is transporting cargo.
-- @param #OPSGROUP self
-- @return #boolean If true, group is transporting.
function OPSGROUP:IsTransporting()
  return self.carrierStatus==OPSGROUP.CarrierStatus.TRANSPORTING
end

--- Check if the group is unloading cargo.
-- @param #OPSGROUP self
-- @return #boolean If true, group is unloading.
function OPSGROUP:IsUnloading()
  return self.carrierStatus==OPSGROUP.CarrierStatus.UNLOADING
end


--- Check if the group is assigned as cargo.
-- @param #OPSGROUP self
-- @param #boolean CheckTransport If `true` or `nil`, also check if cargo is associated with a transport assignment. If not, we consider it not cargo.
-- @return #boolean If true, group is cargo.
function OPSGROUP:IsCargo(CheckTransport)
  return not self:IsNotCargo(CheckTransport)
end

--- Check if the group is **not** cargo.
-- @param #OPSGROUP self
-- @param #boolean CheckTransport If `true` or `nil`, also check if cargo is associated with a transport assignment. If not, we consider it not cargo.
-- @return #boolean If true, group is *not* cargo.
function OPSGROUP:IsNotCargo(CheckTransport)
  local notcargo=self.cargoStatus==OPSGROUP.CargoStatus.NOTCARGO

  if notcargo then
    -- Not cargo.
    return true
  else
    -- Is cargo (e.g. loaded or boarding)

    if CheckTransport then
      -- Check if transport UID was set.
      if self.cargoTransportUID==nil then
        return true
      else
        -- Some transport UID was assigned.
        return false
      end
    else
      -- Is cargo.
      return false
    end

  end


  return notcargo
end

--- Check if awaiting a transport.
-- @param #OPSGROUP self
-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.
-- @return #OPSGROUP self
function OPSGROUP:_AddMyLift(Transport)
  self.mylifts=self.mylifts or {}
  self.mylifts[Transport.uid]=true
  return self
end

--- Remove my lift.
-- @param #OPSGROUP self
-- @param Ops.OpsTransport#OPSTRANSPORT Transport The transport.
-- @return #OPSGROUP self
function OPSGROUP:_DelMyLift(Transport)
  if self.mylifts then
    self.mylifts[Transport.uid]=nil
  end
  return self
end


--- Check if awaiting a transport lift.
-- @param #OPSGROUP self
-- @param Ops.OpsTransport#OPSTRANSPORT Transport (Optional) The transport.
-- @return #boolean If true, group is awaiting transport lift..
function OPSGROUP:IsAwaitingLift(Transport)

  if self.mylifts then

    for uid,iswaiting in pairs(self.mylifts) do
      if Transport==nil or Transport.uid==uid then
        if iswaiting==true then
          return true
        end
      end
    end

  end

  return false
end

--- Get paused mission.
-- @param #OPSGROUP self
-- @return Ops.Auftrag#AUFTRAG Paused mission or nil.
function OPSGROUP:_GetPausedMission()

  if self.pausedmissions and #self.pausedmissions>0 then
    for _,mid in pairs(self.pausedmissions) do
      if mid then
        local mission=self:GetMissionByID(mid)
        if mission and mission:IsNotOver() then
          return mission
        end
      end
    end
  end
  
  return nil
end

--- Count paused mission.
-- @param #OPSGROUP self
-- @return #number Number of paused missions.
function OPSGROUP:_CountPausedMissions()
  local N=0
  if self.pausedmissions and #self.pausedmissions>0 then
    for _,mid in pairs(self.pausedmissions) do
      local mission=self:GetMissionByID(mid)
      if mission and mission:IsNotOver() then
        N=N+1
      end
    end
  end
  
  return N
end

--- Remove paused mission from the table.
-- @param #OPSGROUP self
-- @param #number AuftragsNummer Mission ID of the paused mission to remove.
-- @return #OPSGROUP self
function OPSGROUP:_RemovePausedMission(AuftragsNummer)

  if self.pausedmissions and #self.pausedmissions>0 then
    for i=#self.pausedmissions,1,-1 do
      local mid=self.pausedmissions[i]
      if mid==AuftragsNummer then
        table.remove(self.pausedmissions, i)
        return self
      end
    end
  end
  
  return self
end

--- Check if the group is currently boarding a carrier.
-- @param #OPSGROUP self
-- @param #string CarrierGroupName (Optional) Additionally check if group is boarding this particular carrier group.
-- @return #boolean If true, group is boarding.
function OPSGROUP:IsBoarding(CarrierGroupName)
  if CarrierGroupName then
    local carrierGroup=self:_GetMyCarrierGroup()
    if carrierGroup and carrierGroup.groupname~=CarrierGroupName then
      return false
    end
  end
  return self.cargoStatus==OPSGROUP.CargoStatus.BOARDING
end

--- Check if the group is currently loaded into a carrier.
-- @param #OPSGROUP self
-- @param #string CarrierGroupName (Optional) Additionally check if group is loaded into a particular carrier group(s).
-- @return #boolean If true, group is loaded.
function OPSGROUP:IsLoaded(CarrierGroupName)

  local isloaded=self.cargoStatus==OPSGROUP.CargoStatus.LOADED
  
  -- If not loaded, we can return false
  if not isloaded then
    return false
  end

  if CarrierGroupName then
    if type(CarrierGroupName)~="table" then
      CarrierGroupName={CarrierGroupName}
    end
    for _,CarrierName in pairs(CarrierGroupName) do
      local carrierGroup=self:_GetMyCarrierGroup()
      if carrierGroup and carrierGroup.groupname==CarrierName then
        return isloaded
      end
    end
    -- Not in any specified carrier.
    return false
  end
  
  return isloaded
end

--- Check if the group is currently busy doing something.
--
-- * Boarding
-- * Rearming
-- * Returning
-- * Pickingup, Loading, Transporting, Unloading
-- * Engageing
--
-- @param #OPSGROUP self
-- @return #boolean If `true`, group is busy.
function OPSGROUP:IsBusy()

  if self:IsBoarding() then
    return true
  end

  if self:IsRearming() then
    return true
  end

  if self:IsReturning() then
    return true
  end

  -- Busy as carrier?
  if self:IsPickingup() or self:IsLoading() or self:IsTransporting() or self:IsUnloading() then
    return true
  end

  if self:IsEngaging() then
    return true
  end


  return false
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Waypoint Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get the waypoints.
-- @param #OPSGROUP self
-- @return #table Table of all waypoints.
function OPSGROUP:GetWaypoints()
  return self.waypoints
end

--- Mark waypoints on F10 map.
-- @param #OPSGROUP self
-- @param #number Duration Duration in seconds how long the waypoints are displayed before they are automatically removed. Default is that they are never removed.
-- @return #OPSGROUP self
function OPSGROUP:MarkWaypoints(Duration)

  for i,_waypoint in pairs(self.waypoints or {}) do
    local waypoint=_waypoint --#OPSGROUP.Waypoint

    local text=string.format("Waypoint ID=%d of %s", waypoint.uid, self.groupname)
    text=text..string.format("\nSpeed=%.1f kts, Alt=%d ft (%s)", UTILS.MpsToKnots(waypoint.speed), UTILS.MetersToFeet(waypoint.alt or 0), "BARO")

    if waypoint.marker then
      if waypoint.marker.text~=text then
        waypoint.marker.text=text
      end

    else
      waypoint.marker=MARKER:New(waypoint.coordinate, text):ToCoalition(self:GetCoalition())
    end
  end


  if Duration then
    self:RemoveWaypointMarkers(Duration)
  end

  return self
end

--- Remove waypoints markers on the F10 map.
-- @param #OPSGROUP self
-- @param #number Delay Delay in seconds before the markers are removed. Default is immediately.
-- @return #OPSGROUP self
function OPSGROUP:RemoveWaypointMarkers(Delay)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, OPSGROUP.RemoveWaypointMarkers, self)
  else

    for i,_waypoint in pairs(self.waypoints or {}) do
      local waypoint=_waypoint --#OPSGROUP.Waypoint

      if waypoint.marker then
        waypoint.marker:Remove()
      end
    end

  end

  return self
end


--- Get the waypoint from its unique ID.
-- @param #OPSGROUP self
-- @param #number uid Waypoint unique ID.
-- @return #OPSGROUP.Waypoint Waypoint data.
function OPSGROUP:GetWaypointByID(uid)

  for _,_waypoint in pairs(self.waypoints or {}) do
    local waypoint=_waypoint --#OPSGROUP.Waypoint
    if waypoint.uid==uid then
      return waypoint
    end
  end

  return nil
end

--- Get the waypoint from its index.
-- @param #OPSGROUP self
-- @param #number index Waypoint index.
-- @return #OPSGROUP.Waypoint Waypoint data.
function OPSGROUP:GetWaypointByIndex(index)

  for i,_waypoint in pairs(self.waypoints) do
    local waypoint=_waypoint --#OPSGROUP.Waypoint
    if i==index then
      return waypoint
    end
  end

  return nil
end

--- Get the waypoint UID from its index, i.e. its current position in the waypoints table.
-- @param #OPSGROUP self
-- @param #number index Waypoint index.
-- @return #number Unique waypoint ID.
function OPSGROUP:GetWaypointUIDFromIndex(index)

  for i,_waypoint in pairs(self.waypoints) do
    local waypoint=_waypoint --#OPSGROUP.Waypoint
    if i==index then
      return waypoint.uid
    end
  end

  return nil
end

--- Get the waypoint index (its position in the current waypoints table).
-- @param #OPSGROUP self
-- @param #number uid Waypoint unique ID.
-- @return #OPSGROUP.Waypoint Waypoint data.
function OPSGROUP:GetWaypointIndex(uid)

  if uid then
    for i,_waypoint in pairs(self.waypoints or {}) do
      local waypoint=_waypoint --#OPSGROUP.Waypoint
      if waypoint.uid==uid then
        return i
      end
    end
  end

  return nil
end

--- Get next waypoint index.
-- @param #OPSGROUP self
-- @param #boolean cyclic If `true`, return first waypoint if last waypoint was reached. Default is patrol ad infinitum value set.
-- @param #number i Waypoint index from which the next index is returned. Default is the last waypoint passed.
-- @return #number Next waypoint index.
function OPSGROUP:GetWaypointIndexNext(cyclic, i)

  -- If not specified, we take the adinititum value.
  if cyclic==nil then
    cyclic=self.adinfinitum
  end

  -- Total number of waypoints.
  local N=#self.waypoints

  -- Default is currentwp.
  i=i or self.currentwp

  -- If no next waypoint exists, because the final waypoint was reached, we return the last waypoint.
  local n=math.min(i+1, N)

  -- If last waypoint was reached, the first waypoint is the next in line.
  if cyclic and i==N then
    n=1
  end

  return n
end

--- Get current waypoint index. This is the index of the last passed waypoint.
-- @param #OPSGROUP self
-- @return #number Current waypoint index.
function OPSGROUP:GetWaypointIndexCurrent()
  return self.currentwp or 1
end

--- Get waypoint index after waypoint with given ID. So if the waypoint has index 3 it will return 4.
-- @param #OPSGROUP self
-- @param #number uid Unique ID of the waypoint. Default is new waypoint index after the last current one.
-- @return #number Index after waypoint with given ID.
function OPSGROUP:GetWaypointIndexAfterID(uid)

  local index=self:GetWaypointIndex(uid)
  if index then
    return index+1
  else
    return #self.waypoints+1
  end

end

--- Get waypoint.
-- @param #OPSGROUP self
-- @param #number indx Waypoint index.
-- @return #OPSGROUP.Waypoint Waypoint table.
function OPSGROUP:GetWaypoint(indx)
  return self.waypoints[indx]
end

--- Get final waypoint.
-- @param #OPSGROUP self
-- @return #OPSGROUP.Waypoint Final waypoint table.
function OPSGROUP:GetWaypointFinal()
  return self.waypoints[#self.waypoints]
end

--- Get next waypoint.
-- @param #OPSGROUP self
-- @param #boolean cyclic If true, return first waypoint if last waypoint was reached.
-- @return #OPSGROUP.Waypoint Next waypoint table.
function OPSGROUP:GetWaypointNext(cyclic)

  local n=self:GetWaypointIndexNext(cyclic)

  return self.waypoints[n]
end

--- Get current waypoint.
-- @param #OPSGROUP self
-- @return #OPSGROUP.Waypoint Current waypoint table.
function OPSGROUP:GetWaypointCurrent()
  return self.waypoints[self.currentwp]
end

--- Get current waypoint UID.
-- @param #OPSGROUP self
-- @return #number Current waypoint UID.
function OPSGROUP:GetWaypointCurrentUID()
  local wp=self:GetWaypointCurrent()
  if wp then
    return wp.uid
  end
  return nil
end

--- Get coordinate of next waypoint of the group.
-- @param #OPSGROUP self
-- @param #boolean cyclic If true, return first waypoint if last waypoint was reached.
-- @return Core.Point#COORDINATE Coordinate of the next waypoint.
function OPSGROUP:GetNextWaypointCoordinate(cyclic)

  -- Get next waypoint
  local waypoint=self:GetWaypointNext(cyclic)

  return waypoint.coordinate
end

--- Get waypoint coordinates.
-- @param #OPSGROUP self
-- @param #number index Waypoint index.
-- @return Core.Point#COORDINATE Coordinate of the next waypoint.
function OPSGROUP:GetWaypointCoordinate(index)
  local waypoint=self:GetWaypoint(index)
  if waypoint then
    return waypoint.coordinate
  end
  return nil
end

--- Get waypoint speed.
-- @param #OPSGROUP self
-- @param #number indx Waypoint index.
-- @return #number Speed set at waypoint in knots.
function OPSGROUP:GetWaypointSpeed(indx)

  local waypoint=self:GetWaypoint(indx)

  if waypoint then
    return UTILS.MpsToKnots(waypoint.speed)
  end

  return nil
end

--- Get unique ID of waypoint.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Waypoint waypoint The waypoint data table.
-- @return #number Unique ID.
function OPSGROUP:GetWaypointUID(waypoint)
  return waypoint.uid
end

--- Get unique ID of waypoint given its index.
-- @param #OPSGROUP self
-- @param #number indx Waypoint index.
-- @return #number Unique ID.
function OPSGROUP:GetWaypointID(indx)

  local waypoint=self:GetWaypoint(indx)

  if waypoint then
    return waypoint.uid
  end

  return nil

end

--- Returns a non-zero speed to the next waypoint (even if the waypoint speed is zero).
-- @param #OPSGROUP self
-- @param #number indx Waypoint index.
-- @return #number Speed to next waypoint (>0) in knots.
function OPSGROUP:GetSpeedToWaypoint(indx)

  local speed=self:GetWaypointSpeed(indx)

  if speed<=0.01 then
    speed=self:GetSpeedCruise()
  end

  return speed
end

--- Get distance to waypoint.
-- @param #OPSGROUP self
-- @param #number indx Waypoint index. Default is the next waypoint.
-- @return #number Distance in meters.
function OPSGROUP:GetDistanceToWaypoint(indx)
  local dist=0

  if #self.waypoints>0 then

    indx=indx or self:GetWaypointIndexNext()

    local wp=self:GetWaypoint(indx)

    if wp then

      local coord=self:GetCoordinate()

      dist=coord:Get2DDistance(wp.coordinate)
    end

  end

  return dist
end

--- Get time to waypoint based on current velocity.
-- @param #OPSGROUP self
-- @param #number indx Waypoint index. Default is the next waypoint.
-- @return #number Time in seconds. If velocity is 0
function OPSGROUP:GetTimeToWaypoint(indx)

  local s=self:GetDistanceToWaypoint(indx)

  local v=self:GetVelocity()

  local t=s/v

  if t==math.inf then
    return 365*24*60*60
  elseif t==math.nan then
    return 0
  else
    return t
  end

end

--- Returns the currently expected speed.
-- @param #OPSGROUP self
-- @return #number Expected speed in m/s.
function OPSGROUP:GetExpectedSpeed()

  if self:IsHolding() or self:Is("Rearming") or self:IsWaiting() or self:IsRetreated() then
    --env.info("GetExpectedSpeed - returning ZERO")
    return 0
  else
    --env.info("GetExpectedSpeed - returning self.speedWP = "..self.speedWp)
    return self.speedWp or 0
  end

end

--- Remove a waypoint with a ceratin UID.
-- @param #OPSGROUP self
-- @param #number uid Waypoint UID.
-- @return #OPSGROUP self
function OPSGROUP:RemoveWaypointByID(uid)

  local index=self:GetWaypointIndex(uid)

  if index then
    self:RemoveWaypoint(index)
  end

  return self
end

--- Remove a waypoint.
-- @param #OPSGROUP self
-- @param #number wpindex Waypoint number.
-- @return #OPSGROUP self
function OPSGROUP:RemoveWaypoint(wpindex)

  if self.waypoints then

    -- The waypoitn to be removed.
    local wp=self:GetWaypoint(wpindex)

    -- Is this a temporary waypoint.
    local istemp=wp.temp or wp.detour or wp.astar or wp.missionUID

    -- Number of waypoints before delete.
    local N=#self.waypoints

    -- Always keep at least one waypoint.
    if N==1 then
      self:T(self.lid..string.format("ERROR: Cannot remove waypoint with index=%d! It is the only waypoint and a group needs at least ONE waypoint", wpindex))
      return self
    end

    -- Check that wpindex is not larger than the number of waypoints in the table.
    if wpindex>N then
      self:T(self.lid..string.format("ERROR: Cannot remove waypoint with index=%d as there are only N=%d waypoints!", wpindex, N))
      return self
    end

    -- Remove waypoint marker.
    if wp and wp.marker then
      wp.marker:Remove()
    end

    -- Remove waypoint.
    table.remove(self.waypoints, wpindex)

    -- Number of waypoints after delete.
    local n=#self.waypoints

    -- Debug info.
    self:T(self.lid..string.format("Removing waypoint UID=%d [temp=%s]: index=%d [currentwp=%d]. N %d-->%d", wp.uid, tostring(istemp), wpindex, self.currentwp, N, n))

    -- Waypoint was not reached yet.
    if wpindex > self.currentwp then

      ---
      -- Removed a FUTURE waypoint
      ---

      -- TODO: patrol adinfinitum. Not sure this is handled correctly. If patrol adinfinitum and we have now only one WP left, we should at least go back.

      -- Could be that the waypoint we are currently moving to was the LAST waypoint. Then we now passed the final waypoint.
      if self.currentwp>=n and not (self.adinfinitum or istemp) then
        self:_PassedFinalWaypoint(true, "Removed FUTURE waypoint we are currently moving to and that was the LAST waypoint")
      end

      -- Check if group is done.
      self:_CheckGroupDone(1)

    else

      ---
      -- Removed a waypoint ALREADY PASSED
      ---

      -- If an already passed waypoint was deleted, we do not need to update the route.

      -- If current wp = 1 it stays 1. Otherwise decrease current wp.

      if self.currentwp==1 then

        if self.adinfinitum then
          self.currentwp=#self.waypoints
        else
          self.currentwp=1
        end

      else
        self.currentwp=self.currentwp-1
      end
      
      -- Could be that the waypoint we are currently moving to was the LAST waypoint. Then we now passed the final waypoint.
      if (self.adinfinitum or istemp) then
        self:_PassedFinalWaypoint(false, "Removed PASSED temporary waypoint")
      end
            
    end

  end

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- DCS Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Event function handling the birth of a unit.
-- @param #OPSGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function OPSGROUP:OnEventBirth(EventData)

  -- Check that this is the right group.
  if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
    local unit=EventData.IniUnit
    local group=EventData.IniGroup
    local unitname=EventData.IniUnitName

    -- Set homebase if not already set.
    if self.isFlightgroup then

      if EventData.Place then
        self.homebase=self.homebase or EventData.Place
        self.currbase=EventData.Place
      else
        self.currbase=nil
      end

      if self.homebase and not self.destbase then
        self.destbase=self.homebase
      end

      self:T(self.lid..string.format("EVENT: Element %s born at airbase %s ==> spawned", unitname, self.currbase and self.currbase:GetName() or "unknown"))
    else
      self:T3(self.lid..string.format("EVENT: Element %s born ==> spawned", unitname))
    end

    -- Get element.
    local element=self:GetElementByName(unitname)

    if element and element.status~=OPSGROUP.ElementStatus.SPAWNED then
    
      -- Debug info.
      self:T(self.lid..string.format("EVENT: Element %s born ==> spawned", unitname))
      
      self:T2(self.lid..string.format("DCS unit=%s isExist=%s", tostring(EventData.IniDCSUnit:getName()), tostring(EventData.IniDCSUnit:isExist()) ))

      -- Set element to spawned state.
      self:ElementSpawned(element)

    end

  end

end

--- Event function handling the hit of a unit.
-- @param #OPSGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function OPSGROUP:OnEventHit(EventData)

  -- Check that this is the right group. Here the hit group is stored as target.
  if EventData and EventData.TgtGroup and EventData.TgtUnit and EventData.TgtGroupName and EventData.TgtGroupName==self.groupname then
    self:T2(self.lid..string.format("EVENT: Unit %s hit!", EventData.TgtUnitName))

    local unit=EventData.TgtUnit
    local group=EventData.TgtGroup
    local unitname=EventData.TgtUnitName

    -- Get element.
    local element=self:GetElementByName(unitname)
    
    -- Increase group hit counter.
    self.Nhit=self.Nhit or 0
    self.Nhit=self.Nhit +  1

    if element and element.status~=OPSGROUP.ElementStatus.DEAD then
      -- Trigger Element Hit Event.
      self:ElementHit(element, EventData.IniUnit)
    end

  end

end

--- Event function handling the dead of a unit.
-- @param #OPSGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function OPSGROUP:OnEventDead(EventData)

  -- Check that this is the right group.
  if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
    self:T2(self.lid..string.format("EVENT: Unit %s dead!", EventData.IniUnitName))

    local unit=EventData.IniUnit
    local group=EventData.IniGroup
    local unitname=EventData.IniUnitName

    -- Get element.
    local element=self:GetElementByName(unitname)

    if element and element.status~=OPSGROUP.ElementStatus.DEAD then
      self:T(self.lid..string.format("EVENT: Element %s dead ==> destroyed", element.name))
      self:ElementDestroyed(element)
    end

  end

end

--- Event function handling when a unit is removed from the game.
-- @param #OPSGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function OPSGROUP:OnEventRemoveUnit(EventData)

  -- Check that this is the right group.
  if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
    self:T2(self.lid..string.format("EVENT: Unit %s removed!", EventData.IniUnitName))

    local unit=EventData.IniUnit
    local group=EventData.IniGroup
    local unitname=EventData.IniUnitName

    -- Get element.
    local element=self:GetElementByName(unitname)

    if element and element.status~=OPSGROUP.ElementStatus.DEAD then
      self:T(self.lid..string.format("EVENT: Element %s removed ==> dead", element.name))
      self:ElementDead(element)
    end

  end

end

--- Event function handling when a unit is removed from the game.
-- @param #OPSGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function OPSGROUP:OnEventPlayerLeaveUnit(EventData)

  -- Check that this is the right group.
  if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then
    self:T2(self.lid..string.format("EVENT: Player left Unit %s!", EventData.IniUnitName))

    local unit=EventData.IniUnit
    local group=EventData.IniGroup
    local unitname=EventData.IniUnitName

    -- Get element.
    local element=self:GetElementByName(unitname)

    if element and element.status~=OPSGROUP.ElementStatus.DEAD then
      self:T(self.lid..string.format("EVENT: Player left Element %s ==> dead", element.name))
      self:ElementDead(element)
    end

  end

end

--- Event function handling the event that a unit achieved a kill.
-- @param #OPSGROUP self
-- @param Core.Event#EVENTDATA EventData Event data.
function OPSGROUP:OnEventKill(EventData)
  --self:I("FF event kill")
  --self:I(EventData)

  -- Check that this is the right group.
  if EventData and EventData.IniGroup and EventData.IniUnit and EventData.IniGroupName and EventData.IniGroupName==self.groupname then

    -- Target name
    local targetname=tostring(EventData.TgtUnitName)

    -- Debug info.
    self:T2(self.lid..string.format("EVENT: Unit %s killed object %s!", tostring(EventData.IniUnitName), targetname))

    -- Check if this was a UNIT or STATIC object.
    local target=UNIT:FindByName(targetname)
    if not target then
      target=STATIC:FindByName(targetname, false)
    end

    -- Only count UNITS and STATICs (not SCENERY)
    if target then

      -- Debug info.
      self:T(self.lid..string.format("EVENT: Unit %s killed unit/static %s!", tostring(EventData.IniUnitName), targetname))

      -- Kill counter.
      self.Nkills=self.Nkills+1

      -- Check if on a mission.
      local mission=self:GetMissionCurrent()
      if mission then
        mission.Nkills=mission.Nkills+1 -- Increase mission kill counter.
      end

    end

  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Task Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set DCS task. Enroute tasks are injected automatically.
-- @param #OPSGROUP self
-- @param #table DCSTask DCS task structure.
-- @return #OPSGROUP self
function OPSGROUP:SetTask(DCSTask)

  if self:IsAlive() then

    -- Inject enroute tasks.
    if self.taskenroute and #self.taskenroute>0 then
      if tostring(DCSTask.id)=="ComboTask" then
        for _,task in pairs(self.taskenroute) do
          table.insert(DCSTask.params.tasks, 1, task)
        end
      else
        local tasks=UTILS.DeepCopy(self.taskenroute)
        table.insert(tasks, DCSTask)

        DCSTask=self.group.TaskCombo(self, tasks)
      end
    end

    -- Set task.
    self.controller:setTask(DCSTask)

    -- Debug info.
    local text=string.format("SETTING Task %s", tostring(DCSTask.id))
    if tostring(DCSTask.id)=="ComboTask" then
      for i,task in pairs(DCSTask.params.tasks) do
        text=text..string.format("\n[%d] %s", i, tostring(task.id))
      end
    end
    self:T(self.lid..text)
  end

  return self
end

--- Push DCS task.
-- @param #OPSGROUP self
-- @param #table DCSTask DCS task structure.
-- @return #OPSGROUP self
function OPSGROUP:PushTask(DCSTask)

  if self:IsAlive() then

    -- Inject enroute tasks.
    if self.taskenroute and #self.taskenroute>0 then
      if tostring(DCSTask.id)=="ComboTask" then
        for _,task in pairs(self.taskenroute) do
          table.insert(DCSTask.params.tasks, 1, task)
        end
      else
        local tasks=UTILS.DeepCopy(self.taskenroute)
        table.insert(tasks, DCSTask)

        DCSTask=self.group.TaskCombo(self, tasks)
      end
    end

    -- Push task.
    self.controller:pushTask(DCSTask)

    -- Debug info.
    local text=string.format("PUSHING Task %s", tostring(DCSTask.id))
    if tostring(DCSTask.id)=="ComboTask" then
      for i,task in pairs(DCSTask.params.tasks) do
        text=text..string.format("\n[%d] %s", i, tostring(task.id))
      end
    end
    self:T(self.lid..text)    
  end

  return self
end

--- Returns true if the DCS controller currently has a task.
-- @param #OPSGROUP self
-- @return #boolean True or false if the controller has a task. Nil if no controller.
function OPSGROUP:HasTaskController()
  local hastask=nil
  if self.controller then
    hastask=self.controller:hasTask()
  end
  self:T3(self.lid..string.format("Controller hasTask=%s", tostring(hastask)))
  return hastask
end

--- Clear DCS tasks.
-- @param #OPSGROUP self
-- @return #OPSGROUP self
function OPSGROUP:ClearTasks()
  local hastask=self:HasTaskController()
  if self:IsAlive() and self.controller and self:HasTaskController() then
    self:T(self.lid..string.format("CLEARING Tasks"))
    self.controller:resetTask()
  end
  return self
end



--- Add a *scheduled* task.
-- @param #OPSGROUP self
-- @param #table task DCS task table structure.
-- @param #string clock Mission time when task is executed. Default in 5 seconds. If argument passed as #number, it defines a relative delay in seconds.
-- @param #string description Brief text describing the task, e.g. "Attack SAM".
-- @param #number prio Priority of the task.
-- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never.
-- @return #OPSGROUP.Task The task structure.
function OPSGROUP:AddTask(task, clock, description, prio, duration)

  local newtask=self:NewTaskScheduled(task, clock, description, prio, duration)

  -- Add to table.
  table.insert(self.taskqueue, newtask)

  -- Info.
  self:T(self.lid..string.format("Adding SCHEDULED task %s starting at %s", newtask.description, UTILS.SecondsToClock(newtask.time, true)))
  self:T3({newtask=newtask})

  return newtask
end

--- Create a *scheduled* task.
-- @param #OPSGROUP self
-- @param #table task DCS task table structure.
-- @param #string clock Mission time when task is executed. Default in 5 seconds. If argument passed as #number, it defines a relative delay in seconds.
-- @param #string description Brief text describing the task, e.g. "Attack SAM".
-- @param #number prio Priority of the task.
-- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never.
-- @return #OPSGROUP.Task The task structure.
function OPSGROUP:NewTaskScheduled(task, clock, description, prio, duration)

  -- Increase counter.
  self.taskcounter=self.taskcounter+1

  -- Set time.
  local time=timer.getAbsTime()+5
  if clock then
    if type(clock)=="string" then
      time=UTILS.ClockToSeconds(clock)
    elseif type(clock)=="number" then
      time=timer.getAbsTime()+clock
    end
  end

  -- Task data structure.
  local newtask={} --#OPSGROUP.Task
  newtask.status=OPSGROUP.TaskStatus.SCHEDULED
  newtask.dcstask=task
  newtask.description=description or task.id
  newtask.prio=prio or 50
  newtask.time=time
  newtask.id=self.taskcounter
  newtask.duration=duration
  newtask.waypoint=-1
  newtask.type=OPSGROUP.TaskType.SCHEDULED
  newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id))
  newtask.stopflag:Set(0)

  return newtask
end

--- Add a *waypoint* task.
-- @param #OPSGROUP self
-- @param #table task DCS task table structure.
-- @param #OPSGROUP.Waypoint Waypoint where the task is executed. Default is the at *next* waypoint.
-- @param #string description Brief text describing the task, e.g. "Attack SAM".
-- @param #number prio Priority of the task. Number between 1 and 100. Default is 50.
-- @param #number duration Duration before task is cancelled in seconds counted after task started. Default never.
-- @return #OPSGROUP.Task The task structure.
function OPSGROUP:AddTaskWaypoint(task, Waypoint, description, prio, duration)

  -- Waypoint of task.
  Waypoint=Waypoint or self:GetWaypointNext()

  if Waypoint then

    -- Increase counter.
    self.taskcounter=self.taskcounter+1

    -- Task data structure.
    local newtask={} --#OPSGROUP.Task
    newtask.description=description or string.format("Task #%d", self.taskcounter)
    newtask.status=OPSGROUP.TaskStatus.SCHEDULED
    newtask.dcstask=task
    newtask.prio=prio or 50
    newtask.id=self.taskcounter
    newtask.duration=duration
    newtask.time=0
    newtask.waypoint=Waypoint.uid
    newtask.type=OPSGROUP.TaskType.WAYPOINT
    newtask.stopflag=USERFLAG:New(string.format("%s StopTaskFlag %d", self.groupname, newtask.id))
    newtask.stopflag:Set(0)

    -- Add to table.
    table.insert(self.taskqueue, newtask)

    -- Info.
    self:T(self.lid..string.format("Adding WAYPOINT task %s at WP ID=%d", newtask.description, newtask.waypoint))
    self:T3({newtask=newtask})

    -- Update route.
    --self:__UpdateRoute(-1)

    return newtask
  end

  return nil
end

--- Add an *enroute* task.
-- @param #OPSGROUP self
-- @param #table task DCS task table structure.
function OPSGROUP:AddTaskEnroute(task)

  if not self.taskenroute then
    self.taskenroute={}
  end

  -- Check not to add the same task twice!
  local gotit=false
  for _,Task in pairs(self.taskenroute) do
    if Task.id==task.id then
      gotit=true
      break
    end
  end

  if not gotit then
    self:T(self.lid..string.format("Adding enroute task"))
    table.insert(self.taskenroute, task)
  end

end

--- Get the unfinished waypoint tasks
-- @param #OPSGROUP self
-- @param #number id Unique waypoint ID.
-- @return #table Table of tasks. Table could also be empty {}.
function OPSGROUP:GetTasksWaypoint(id)

  -- Tasks table.
  local tasks={}

  -- Sort queue.
  self:_SortTaskQueue()

  -- Look for first task that SCHEDULED.
  for _,_task in pairs(self.taskqueue) do
    local task=_task --#OPSGROUP.Task
    if task.type==OPSGROUP.TaskType.WAYPOINT and task.status==OPSGROUP.TaskStatus.SCHEDULED and task.waypoint==id then
      table.insert(tasks, task)
    end
  end

  return tasks
end

--- Count remaining waypoint tasks.
-- @param #OPSGROUP self
-- @param #number uid Unique waypoint ID.
-- @return #number Number of waypoint tasks.
function OPSGROUP:CountTasksWaypoint(id)

  -- Tasks table.
  local n=0

  -- Look for first task that SCHEDULED.
  for _,_task in pairs(self.taskqueue) do
    local task=_task --#OPSGROUP.Task
    if task.type==OPSGROUP.TaskType.WAYPOINT and task.status==OPSGROUP.TaskStatus.SCHEDULED and task.waypoint==id then
      n=n+1
    end
  end

  return n
end

--- Sort task queue.
-- @param #OPSGROUP self
function OPSGROUP:_SortTaskQueue()

  -- Sort results table wrt prio and then start time.
  local function _sort(a, b)
    local taskA=a --#OPSGROUP.Task
    local taskB=b --#OPSGROUP.Task
    return (taskA.prio<taskB.prio) or (taskA.prio==taskB.prio and taskA.time<taskB.time)
  end

  --TODO: only needs to be sorted if a task was added, is done, or was removed.
  table.sort(self.taskqueue, _sort)

end


--- Count the number of tasks that still pending in the queue.
-- @param #OPSGROUP self
-- @return #number Total number of tasks remaining.
-- @return #number Number of SCHEDULED tasks remaining.
-- @return #number Number of WAYPOINT tasks remaining.
function OPSGROUP:CountRemainingTasks()

  local Ntot=0
  local Nwp=0
  local Nsched=0

  -- Loop over tasks queue.
  for _,_task in pairs(self.taskqueue) do
    local task=_task --#OPSGROUP.Task

    -- Task is still scheduled.
    if task.status==OPSGROUP.TaskStatus.SCHEDULED then

      -- Total number of tasks.
      Ntot=Ntot+1

      if task.type==OPSGROUP.TaskType.WAYPOINT then
        Nwp=Nwp+1
      elseif task.type==OPSGROUP.TaskType.SCHEDULED then
        Nsched=Nsched+1
      end

    end

  end

  return Ntot, Nsched, Nwp
end

--- Remove task from task queue.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Task Task The task to be removed from the queue.
-- @return #boolean True if task could be removed.
function OPSGROUP:RemoveTask(Task)

  for i=#self.taskqueue,1,-1 do
    local task=self.taskqueue[i] --#OPSGROUP.Task

    if task.id==Task.id then

      -- Remove task from queue.
      table.remove(self.taskqueue, i)

      -- Update route if this is a waypoint task.
      if task.type==OPSGROUP.TaskType.WAYPOINT and task.status==OPSGROUP.TaskStatus.SCHEDULED then
        self:_CheckGroupDone(1)
      end

      return true
    end
  end

  return false
end

--- Get next task in queue. Task needs to be in state SCHEDULED and time must have passed.
-- @param #OPSGROUP self
-- @return #OPSGROUP.Task The next task in line or `nil`.
function OPSGROUP:_GetNextTask()

  if self.taskpaused then
    --return self.taskpaused
  end

  if #self.taskqueue==0 then
    return nil
  end

  -- Sort queue wrt prio and start time.
  self:_SortTaskQueue()

  -- Current time.
  local time=timer.getAbsTime()

  -- Look for first task that is SCHEDULED.
  for _,_task in pairs(self.taskqueue) do
    local task=_task --#OPSGROUP.Task
    if task.type==OPSGROUP.TaskType.SCHEDULED and task.status==OPSGROUP.TaskStatus.SCHEDULED and time>=task.time then
      return task
    end
  end

  return nil
end

--- Get the currently executed task if there is any.
-- @param #OPSGROUP self
-- @return #OPSGROUP.Task Current task or nil.
function OPSGROUP:GetTaskCurrent()
  local task=self:GetTaskByID(self.taskcurrent, OPSGROUP.TaskStatus.EXECUTING)
  return task
end

--- Get task by its id.
-- @param #OPSGROUP self
-- @param #number id Task id.
-- @param #string status (Optional) Only return tasks with this status, e.g. OPSGROUP.TaskStatus.SCHEDULED.
-- @return #OPSGROUP.Task The task or nil.
function OPSGROUP:GetTaskByID(id, status)

  for _,_task in pairs(self.taskqueue) do
    local task=_task --#OPSGROUP.Task

    if task.id==id then
      if status==nil or status==task.status then
        return task
      end
    end

  end

  return nil
end

--- On before "TaskExecute" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Task Task The task.
function OPSGROUP:onbeforeTaskExecute(From, Event, To, Task)

  -- Get mission of this task (if any).
  local Mission=self:GetMissionByTaskID(Task.id)

  if Mission and (Mission.Tpush or #Mission.conditionPush>0) then

    if Mission:IsReadyToPush() then

      ---
      -- READY to push yet
      ---

      -- Group is currently waiting.
      if self:IsWaiting() then

        -- Not waiting any more.
        self.Twaiting=nil
        self.dTwait=nil

        -- For a flight group, we must cancel the wait/orbit task.
        if self:IsFlightgroup() then

          -- Set hold flag to 1. This is a condition in the wait/orbit task.
          self.flaghold:Set(1)

          -- Reexecute task in 1 sec to allow to flag to take effect.
          --self:__TaskExecute(-1, Task)

          -- Deny transition for now.
          --return false
        end
      end

    else

      ---
      -- NOT READY to push yet
      ---

      if self:IsWaiting() then
        -- Group is already waiting
      else
        -- Wait indefinitely.
        local alt=Mission.missionAltitude and UTILS.MetersToFeet(Mission.missionAltitude) or nil
        self:Wait(nil, alt)
      end

      -- Time to for the next try. Best guess is when push time is reached or 20 sec when push conditions are not true yet.
      local dt=Mission.Tpush and Mission.Tpush-timer.getAbsTime() or 20

      -- Debug info.
      self:T(self.lid..string.format("Mission %s task execute suspended for %d seconds", Mission.name, dt))

      -- Reexecute task.
      self:__TaskExecute(-dt, Task)

      -- Deny transition.
      return false
    end

  end
  
  if Mission and Mission.opstransport then
  
    local delivered=Mission.opstransport:IsCargoDelivered(self.groupname)
  
    if not delivered then
    
      local dt=30

      -- Debug info.
      self:T(self.lid..string.format("Mission %s task execute suspended for %d seconds because we were not delivered", Mission.name, dt))

      -- Reexecute task.
      self:__TaskExecute(-dt, Task)
      
      if (self:IsArmygroup() or self:IsNavygroup()) and self:IsCruising() then
        self:FullStop()
      end
      
      -- Deny transition.
      return false    
    end
  end

  return true
end

--- On after "TaskExecute" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP.Task Task The task.
function OPSGROUP:onafterTaskExecute(From, Event, To, Task)

  -- Debug message.
  local text=string.format("Task %s ID=%d execute", tostring(Task.description), Task.id)

  -- Debug info.  
  self:T(self.lid..text)

  -- Debug info.
  self:T2({Task})
  
  -- Cancel current task if there is any.
  if self.taskcurrent>0 then
    self:TaskCancel()
  end

  -- Set current task.
  self.taskcurrent=Task.id

  -- Set time stamp.
  Task.timestamp=timer.getAbsTime()

  -- Task status executing.
  Task.status=OPSGROUP.TaskStatus.EXECUTING

  -- Insert into task queue. Not sure any more, why I added this. But probably if a task is just executed without having been put into the queue.
  if self:GetTaskCurrent()==nil then
    table.insert(self.taskqueue, Task)
  end

  -- Get mission of this task (if any).
  local Mission=self:GetMissionByTaskID(self.taskcurrent)

  -- Update push DCS task.
  self:_UpdateTask(Task, Mission)

  -- Set AUFTRAG status.
  if Mission then
    self:MissionExecute(Mission)
  end

end

--- Update (DCS) task.
-- @param #OPSGROUP self
-- @param Ops.OpsGroup#OPSGROUP.Task Task The task.
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
function OPSGROUP:_UpdateTask(Task, Mission)

  Mission=Mission or self:GetMissionByTaskID(self.taskcurrent)

  if Task.dcstask.id==AUFTRAG.SpecialTask.FORMATION then

    -- Set of group(s) to follow Mother.
    local followSet=SET_GROUP:New():AddGroup(self.group)

    local param=Task.dcstask.params

    local followUnit=UNIT:FindByName(param.unitname)

    -- Define AI Formation object.
    Task.formation=AI_FORMATION:New(followUnit, followSet, AUFTRAG.SpecialTask.FORMATION, "Follow X at given parameters.")

    -- Formation parameters.
    Task.formation:FormationCenterWing(-param.offsetX, 50, math.abs(param.altitude), 50, param.offsetZ, 50)

    -- Set follow time interval.
    Task.formation:SetFollowTimeInterval(param.dtFollow)

    -- Formation mode.
    Task.formation:SetFlightModeFormation(self.group)

    -- Start formation FSM.
    Task.formation:Start()

  elseif Task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then

    ---
    -- Task patrol zone.
    ---

    -- Parameters.
    local zone=Task.dcstask.params.zone --Core.Zone#ZONE

    local surfacetypes=nil
    if self:IsArmygroup() then
      surfacetypes={land.SurfaceType.LAND, land.SurfaceType.ROAD}
    elseif self:IsNavygroup() then
      surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER}
    end

    -- Random coordinate in zone.
    local Coordinate=zone:GetRandomCoordinate(nil, nil, surfacetypes)

    --Coordinate:MarkToAll("Random Patrol Zone Coordinate")

    -- Speed and altitude.
    local Speed=Task.dcstask.params.speed and UTILS.MpsToKnots(Task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise)
    local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil

    local currUID=self:GetWaypointCurrent().uid

    -- New waypoint.
    local wp=nil --#OPSGROUP.Waypoint
    if self.isFlightgroup then
      wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude)
    elseif self.isArmygroup then
      wp=ARMYGROUP.AddWaypoint(self,   Coordinate, Speed, currUID, Task.dcstask.params.formation)
    elseif self.isNavygroup then
      wp=NAVYGROUP.AddWaypoint(self,   Coordinate, Speed, currUID, Altitude)
    end

    -- Set mission UID.
    wp.missionUID=Mission and Mission.auftragsnummer or nil

  elseif Task.dcstask.id==AUFTRAG.SpecialTask.RECON then

    ---
    -- Task recon.
    ---

    -- Target
    local target=Task.dcstask.params.target --Ops.Target#TARGET
    
    -- Init a table.
    self.reconindecies={}
    for i=1,#target.targets do
      table.insert(self.reconindecies, i)
    end
    
    local n=1
    if Task.dcstask.params.randomly then
      n=UTILS.GetRandomTableElement(self.reconindecies)
    else    
      table.remove(self.reconindecies, n)
    end        

    -- Target object and zone.
    local object=target.targets[n] --Ops.Target#TARGET.Object
    local zone=object.Object --Core.Zone#ZONE

    -- Random coordinate in zone.
    local Coordinate=zone:GetRandomCoordinate()

    -- Speed and altitude.
    local Speed=Task.dcstask.params.speed and UTILS.MpsToKnots(Task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise)
    local Altitude=Task.dcstask.params.altitude and UTILS.MetersToFeet(Task.dcstask.params.altitude) or nil

    --Coordinate:MarkToAll("Recon Waypoint Execute")

    local currUID=self:GetWaypointCurrent().uid

    -- New waypoint.
    local wp=nil --#OPSGROUP.Waypoint
    if self.isFlightgroup then
      wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude)
    elseif self.isArmygroup then
      wp=ARMYGROUP.AddWaypoint(self,   Coordinate, Speed, currUID, Task.dcstask.params.formation)
    elseif self.isNavygroup then
      wp=NAVYGROUP.AddWaypoint(self,   Coordinate, Speed, currUID, Altitude)
    end

    -- Set mission UID.
    wp.missionUID=Mission and Mission.auftragsnummer or nil

  elseif Task.dcstask.id==AUFTRAG.SpecialTask.AMMOSUPPLY or Task.dcstask.id==AUFTRAG.SpecialTask.FUELSUPPLY then

    ---
    -- Task "Ammo Supply" or "Fuel Supply" mission.
    ---

    -- Just stay put and wait until something happens.
    
  elseif Task.dcstask.id==AUFTRAG.SpecialTask.REARMING then

    ---
    -- Task "Rearming"
    ---

    -- Check if ammo is full.
    
    local rearmed=self:_CheckAmmoFull()
    
    if rearmed then
      self:T2(self.lid.."Ammo already full ==> reaming task done!")
      self:TaskDone(Task)
    else
      self:T2(self.lid.."Ammo not full ==> Rearm()")
      self:Rearm()
    end


  elseif Task.dcstask.id==AUFTRAG.SpecialTask.ALERT5 then

    ---
    -- Task "Alert 5" mission.
    ---

    -- Just stay put on the airfield and wait until something happens.

  elseif Task.dcstask.id==AUFTRAG.SpecialTask.ONGUARD or Task.dcstask.id==AUFTRAG.SpecialTask.ARMOREDGUARD then

    ---
    -- Task "On Guard" Mission.
    ---

    -- Just stay put.
    --TODO: Change ALARM STATE

    if self:IsArmygroup() or self:IsNavygroup() then
      -- Especially NAVYGROUP needs a full stop as patrol ad infinitum
      self:FullStop()
    else
      -- FLIGHTGROUP not implemented (intended!) for this AUFTRAG type.
    end

  elseif Task.dcstask.id==AUFTRAG.SpecialTask.NOTHING then

    ---
    -- Task "Nothing" Mission.
    ---

    -- Just stay put.
    --TODO: Change ALARM STATE

    if self:IsArmygroup() or self:IsNavygroup() then
      -- Especially NAVYGROUP needs a full stop as patrol ad infinitum
      self:__FullStop(0.1)
    else
      -- FLIGHTGROUP not implemented (intended!) for this AUFTRAG type.
    end

  elseif Task.dcstask.id==AUFTRAG.SpecialTask.AIRDEFENSE or Task.dcstask.id==AUFTRAG.SpecialTask.EWR then

    ---
    -- Task "AIRDEFENSE" or "EWR" Mission.
    ---

    -- Just stay put.
    --TODO: Change ALARM STATE

    if self:IsArmygroup() or self:IsNavygroup() then
      self:FullStop()
    else
      -- FLIGHTGROUP not implemented (intended!) for this AUFTRAG type.
    end

  elseif Task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK or Task.dcstask.id==AUFTRAG.SpecialTask.ARMORATTACK then  

    ---
    -- Task "Ground Attack" Mission.
    ---
    
    -- Engage target.
    local target=Task.dcstask.params.target --Ops.Target#TARGET
    
    -- Set speed. Default max.
    local speed=self.speedMax and UTILS.KmphToKnots(self.speedMax) or nil
    if Task.dcstask.params.speed then
      speed=UTILS.MpsToKnots(Task.dcstask.params.speed)
    end
    
    if target then
      self:EngageTarget(target, speed, Task.dcstask.params.formation)
    end
  
  elseif Task.dcstask.id==AUFTRAG.SpecialTask.PATROLRACETRACK then
  
    ---
    -- Task "Patrol Race Track" Mission.
    ---
    
    if self.isFlightgroup then
      self:T("We are Special Auftrag Patrol Race Track, starting now ...")
      --self:I({Task.dcstask.params})
      --[[
          Task.dcstask.params.TrackAltitude = self.TrackAltitude
          Task.dcstask.params.TrackSpeed = self.TrackSpeed
          Task.dcstask.params.TrackPoint1 = self.TrackPoint1
          Task.dcstask.params.TrackPoint2 = self.TrackPoint2
          Task.dcstask.params.TrackFormation = self.TrackFormation
      --]]
      local aircraft = self:GetGroup()
      aircraft:PatrolRaceTrack(Task.dcstask.params.TrackPoint1,Task.dcstask.params.TrackPoint2,Task.dcstask.params.TrackAltitude,Task.dcstask.params.TrackSpeed,Task.dcstask.params.TrackFormation,false,1)
    end
    
  elseif Task.dcstask.id==AUFTRAG.SpecialTask.HOVER then

    ---
    -- Task "Hover" Mission.
    ---
    
    if self.isFlightgroup then
      self:T("We are Special Auftrag HOVER, hovering now ...")
      --self:I({Task.dcstask.params})
      local alt = Task.dcstask.params.hoverAltitude
      local time =Task.dcstask.params.hoverTime
      local mSpeed = Task.dcstask.params.missionSpeed or self.speedCruise or 150
      local Speed = UTILS.KmphToKnots(mSpeed)
      local CruiseAlt = UTILS.FeetToMeters(Task.dcstask.params.missionAltitude or 1000)
      local helo = self:GetGroup()
      helo:SetSpeed(0.01,true)
      helo:SetAltitude(alt,true,"BARO")
      self:HoverStart()
      local function FlyOn(Helo,Speed,CruiseAlt,Task)
        if Helo then
          Helo:SetSpeed(Speed,true)
          Helo:SetAltitude(CruiseAlt,true,"BARO")
          self:T("We are Special Auftrag HOVER, end of hovering now ...")
          self:TaskDone(Task)
          self:HoverEnd()
        end
      end
      local timer = TIMER:New(FlyOn,helo,Speed,CruiseAlt,Task)
      timer:Start(time)
    end

  elseif Task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then

    ---
    -- Task "RelocateCohort" Mission.
    ---
    
    -- Debug mission.
    self:T(self.lid.."Executing task for relocation mission")
    
    -- The new legion.
    local legion=Task.dcstask.params.legion --Ops.Legion#LEGION
    
    -- Get random coordinate in spawn zone of new legion.
    local Coordinate=legion.spawnzone:GetRandomCoordinate()
    
    -- Get current waypoint ID.
    local currUID=self:GetWaypointCurrent().uid
    
    local wp=nil --#OPSGROUP.Waypoint
    if self.isArmygroup then
      self:T2(self.lid.."Routing group to spawn zone of new legion")
      wp=ARMYGROUP.AddWaypoint(self,   Coordinate, UTILS.KmphToKnots(self.speedCruise), currUID, Mission.optionFormation)     
    elseif self.isFlightgroup then
      self:T2(self.lid.."Routing group to intermediate point near new legion")
      Coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate, 0.8)
      wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, UTILS.KmphToKnots(self.speedCruise), currUID, UTILS.MetersToFeet(self.altitudeCruise))
    elseif self.isNavygroup then
      self:T2(self.lid.."Routing group to spawn zone of new legion")
      wp=NAVYGROUP.AddWaypoint(self,   Coordinate, UTILS.KmphToKnots(self.speedCruise), currUID)         
    else
    
    end
    
    wp.missionUID=Mission and Mission.auftragsnummer or nil
    
  elseif Task.dcstask.id==AUFTRAG.SpecialTask.CAPTUREZONE then

    ---
    -- Task "CaptureZone" Mission.
    -- Check if zone was captured or find new target to engage.
    ---  
    
    -- Not enganging already.
    if self:IsEngaging() then
    
      -- Group is currently engaging an enemy unit to capture the zone.
      self:T2(self.lid..string.format("CaptureZone: Engaging currently!"))
    else
    
      -- Get enemy coalitions. We do not include neutrals.
      local Coalitions=UTILS.GetCoalitionEnemy(self:GetCoalition(), false)
      
      -- Current target object.
      local zoneCurr=Task.target --Ops.OpsZone#OPSZONE
      
      if zoneCurr then
            
        self:T(self.lid..string.format("Current target zone=%s owner=%s", zoneCurr:GetName(), zoneCurr:GetOwnerName()))
        
        if zoneCurr:GetOwner()==self:GetCoalition() then
          -- Current zone captured ==> Find next zone or call it a day!
          
          -- Debug info.
          self:T(self.lid..string.format("Zone %s captured ==> Task DONE!", zoneCurr:GetName()))
          
          -- Task done.
          self:TaskDone(Task)
          
        else        
          -- Current zone NOT captured yet ==> Find Target
          
          -- Debug info.
          self:T(self.lid..string.format("Zone %s NOT captured!", zoneCurr:GetName()))          
          
          if Mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.EXECUTING then
          
            -- Debug info.
            self:T(self.lid..string.format("Zone %s NOT captured and EXECUTING ==> Find target", zoneCurr:GetName()))          
          
        
            -- Get closest target.
            local targetgroup=zoneCurr:GetScannedGroupSet():GetClosestGroup(self.coordinate, Coalitions)
            
            if targetgroup then
            
              -- Debug info.
              self:T(self.lid..string.format("Zone %s NOT captured: engaging target %s", zoneCurr:GetName(), targetgroup:GetName()))
            
              -- Engage target group.
              self:EngageTarget(targetgroup)
              
            else
              
              if self:IsFlightgroup() then
                -- Debug info.
                self:T(self.lid..string.format("Zone %s not captured but no target group could be found ==> TaskDone as FLIGHTGROUPS cannot capture zones", zoneCurr:GetName()))                
              
                -- Task done.
                self:TaskDone(Task)
              else
                -- Debug info.
                self:T(self.lid..string.format("Zone %s not captured but no target group could be found. Should be captured in the next zone evaluation.", zoneCurr:GetName()))                
              end
              
            end
            
          else
            self:T(self.lid..string.format("Zone %s NOT captured and NOT EXECUTING", zoneCurr:GetName()))            
          end
          
        end        
        
      else
        self:T(self.lid..string.format("NO Current target zone=%s"))
      end
      
    end
        
  else

    -- If task is scheduled (not waypoint) set task.
    if Task.type==OPSGROUP.TaskType.SCHEDULED or Task.ismission then

      -- DCS task.
      local DCSTask=nil

      -- BARRAGE is special!
      if Task.dcstask.id==AUFTRAG.SpecialTask.BARRAGE then
        ---
        -- BARRAGE
      
        -- Current vec2.
        local vec2=self:GetVec2()
        
        -- Task parameters.
        local param=Task.dcstask.params
        
        -- Set heading and altitude.
        local heading=param.heading or math.random(1, 360)
        local Altitude=param.altitude or 500
        local Alpha=param.angle or math.random(45, 85)
        local distance=Altitude/math.tan(math.rad(Alpha))
        local tvec2=UTILS.Vec2Translate(vec2, distance, heading)
        
        -- Debug info.
        self:T(self.lid..string.format("Barrage: Shots=%s, Altitude=%d m, Angle=%dÂ°, heading=%03dÂ°, distance=%d m", tostring(param.shots), Altitude, Alpha, heading, distance))
        
        -- Set fire at point task.
        DCSTask=CONTROLLABLE.TaskFireAtPoint(nil, tvec2, param.radius, param.shots, param.weaponType, Altitude)
        
      elseif Task.ismission and Task.dcstask.id=='FireAtPoint' then
      
        -- Copy DCS task.
        DCSTask=UTILS.DeepCopy(Task.dcstask)
        
        -- Get current ammo.
        local ammo=self:GetAmmoTot()
        
        -- Number of ammo avail.
        local nAmmo=ammo.Total
        
        local weaponType=DCSTask.params.weaponType or -1
        
        -- Adjust max number of ammo for specific weapon types requested.
        if weaponType==ENUMS.WeaponFlag.CruiseMissile then
          nAmmo=ammo.MissilesCR
        elseif weaponType==ENUMS.WeaponFlag.AnyRocket then
          nAmmo=ammo.Rockets
        elseif weaponType==ENUMS.WeaponFlag.Cannons then
          nAmmo=ammo.Guns
        end
        
        --TODO: Update target location while we're at it anyway.
        --TODO: Adjust mission result evaluation time? E.g. cruise missiles can fly a long time depending on target distance.
        
        -- Number of shots to be fired.
        local nShots=DCSTask.params.expendQty or 1
        
        -- Debug info.
        self:T(self.lid..string.format("Fire at point with nshots=%d of %d", nShots, nAmmo))
        
        if nShots==-1 then
          -- The -1 is for using all available ammo.
          nShots=nAmmo
          self:T(self.lid..string.format("Fire at point taking max amount of ammo = %d", nShots))
        elseif nShots<1 then
          local p=nShots
          nShots=UTILS.Round(p*nAmmo, 0)
          self:T(self.lid..string.format("Fire at point taking %.1f percent amount of ammo = %d", p, nShots))
        else
          -- Fire nShots but at most nAmmo.
          nShots=math.min(nShots, nAmmo)
        end
        
        -- Set quantity of task.
        DCSTask.params.expendQty=nShots
        
      else
        ---
        -- Take DCS task
        ---
        DCSTask=Task.dcstask
      end
      
      self:_SandwitchDCSTask(DCSTask, Task)

    elseif Task.type==OPSGROUP.TaskType.WAYPOINT then
      -- Waypoint tasks are executed elsewhere!
    else
      self:T(self.lid.."ERROR: Unknown task type: ")
    end

  end
  
end

--- Sandwitch DCS task in stop condition and push the task to the group.
-- @param #OPSGROUP self
-- @param DCS#Task DCSTask The DCS task.
-- @param Ops.OpsGroup#OPSGROUP.Task Task
-- @param #boolean SetTask Set task instead of pushing it.
-- @param #number Delay Delay in seconds. Default nil.
function OPSGROUP:_SandwitchDCSTask(DCSTask, Task, SetTask, Delay)

  if Delay and Delay>0 then
    -- Delayed call.
    self:ScheduleOnce(Delay, OPSGROUP._SandwitchDCSTask, self, DCSTask, Task, SetTask)    
  else

    local DCStasks={}
    if DCSTask.id=='ComboTask' then
      -- Loop over all combo tasks.
      for TaskID, Task in ipairs(DCSTask.params.tasks) do
        table.insert(DCStasks, Task)
      end
    else
      table.insert(DCStasks, DCSTask)
    end
    
    -- Combo task.
    local TaskCombo=self.group:TaskCombo(DCStasks)
    
    -- Stop condition!
    local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration)
    
    -- Controlled task.
    local TaskControlled=self.group:TaskControlled(TaskCombo, TaskCondition)
    
    -- Task done.
    local TaskDone=self.group:TaskFunction("OPSGROUP._TaskDone", self, Task)
    
    -- Final task.
    local TaskFinal=self.group:TaskCombo({TaskControlled, TaskDone})
    
    -- Set task for group.
    -- NOTE: I am pushing the task instead of setting it as it seems to keep the mission task alive.
    --       There were issues that flights did not proceed to a later waypoint because the task did not finish until the fired missiles
    --       impacted (took rather long). Then the flight flew to the nearest airbase and one lost completely the control over the group.
    if SetTask then
      self:SetTask(TaskFinal)
    else
      self:PushTask(TaskFinal)
    end
    
  end

end


--- On after "TaskCancel" event. Cancels the current task or simply sets the status to DONE if the task is not the current one.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP.Task Task The task to cancel. Default is the current task (if any).
function OPSGROUP:onafterTaskCancel(From, Event, To, Task)

  -- Get current task.
  local currenttask=self:GetTaskCurrent()

  -- If no task, we take the current task. But this could also be *nil*!
  Task=Task or currenttask

  if Task then

    -- Check if the task is the current task?
    if currenttask and Task.id==currenttask.id then

      -- Current stop flag value. I noticed cases, where setting the flag to 1 would not cancel the task, e.g. when firing HARMS on a dead ship.
      local stopflag=Task.stopflag:Get()

      -- Debug info.
      local text=string.format("Current task %s ID=%d cancelled (flag %s=%d)", Task.description, Task.id, Task.stopflag:GetName(), stopflag)
      self:T(self.lid..text)

      -- Set stop flag. When the flag is true, the _TaskDone function is executed and calls :TaskDone()
      Task.stopflag:Set(1)

      local done=false
      if Task.dcstask.id==AUFTRAG.SpecialTask.FORMATION then
        Task.formation:Stop()
        done=true
      elseif Task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then
        done=true
      elseif Task.dcstask.id==AUFTRAG.SpecialTask.RECON then
        done=true
      elseif Task.dcstask.id==AUFTRAG.SpecialTask.AMMOSUPPLY then
        done=true
      elseif Task.dcstask.id==AUFTRAG.SpecialTask.FUELSUPPLY then      
        done=true
      elseif Task.dcstask.id==AUFTRAG.SpecialTask.REARMING then
        done=true        
      elseif Task.dcstask.id==AUFTRAG.SpecialTask.ALERT5 then
        done=true
      elseif Task.dcstask.id==AUFTRAG.SpecialTask.ONGUARD or Task.dcstask.id==AUFTRAG.SpecialTask.ARMOREDGUARD then
        done=true
      elseif Task.dcstask.id==AUFTRAG.SpecialTask.GROUNDATTACK or Task.dcstask.id==AUFTRAG.SpecialTask.ARMORATTACK then
        done=true
      elseif Task.dcstask.id==AUFTRAG.SpecialTask.NOTHING then
        done=true        
      elseif stopflag==1 or (not self:IsAlive()) or self:IsDead() or self:IsStopped() then
        -- Manual call TaskDone if setting flag to one was not successful.
        done=true
      end

      if done then
        self:TaskDone(Task)
      end

    else

      -- Debug info.
      self:T(self.lid..string.format("TaskCancel: Setting task %s ID=%d to DONE", Task.description, Task.id))

      -- Call task done function.
      self:TaskDone(Task)

    end

  else

    local text=string.format("WARNING: No (current) task to cancel!")
    self:T(self.lid..text)

  end

end

--- On before "TaskDone" event. Deny transition if task status is PAUSED.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP.Task Task
function OPSGROUP:onbeforeTaskDone(From, Event, To, Task)

  local allowed=true

  if Task.status==OPSGROUP.TaskStatus.PAUSED then
    allowed=false
  end

  return allowed
end

--- On after "TaskDone" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP.Task Task
function OPSGROUP:onafterTaskDone(From, Event, To, Task)

  -- Debug message.
  local text=string.format("Task done: %s ID=%d", Task.description, Task.id)
  self:T(self.lid..text)

  -- No current task.
  if Task.id==self.taskcurrent then
    self.taskcurrent=0
  end

  -- Task status done.
  Task.status=OPSGROUP.TaskStatus.DONE

  -- Restore old ROE.
  if Task.backupROE then
    self:SwitchROE(Task.backupROE)
  end

  -- Check if this task was the task of the current mission ==> Mission Done!
  local Mission=self:GetMissionByTaskID(Task.id)

  if Mission and Mission:IsNotOver() then
  
    -- Get mission status of this group.
    local status=Mission:GetGroupStatus(self)

    -- Check if mission is paused.
    if status~=AUFTRAG.GroupStatus.PAUSED then
      --- 
      -- Mission is NOT over ==> trigger DONE
      ---

      if Mission.type==AUFTRAG.Type.CAPTUREZONE and Mission:CountMissionTargets()>0 then
      
        -- Remove mission waypoints.
        self:T(self.lid.."Remove mission waypoints")
        self:_RemoveMissionWaypoints(Mission, false)
        
        if self:IsFlightgroup() then
        
          -- A flight cannot capture so we assume done.
        
--          local opszone=Mission:GetTargetData() --Ops.OpsZone#OPSZONE
--          
--          if opszone then
--          
--            local mycoalition=self:GetCoalition()
--            
--            if mycoalition~=opszone:GetOwner() then
--              local nenemy=0
--              if mycoalition==coalition.side.BLUE then
--                nenemy=opszone.Nred
--              else
--                nenemy=opszone.Nblu
--              end
--                
--            end
--            
--          end
        
        else
      
          self:T(self.lid.."Task done ==> Route to mission for next opszone")
          self:MissionStart(Mission)
          
          return
        end      
      end

      -- Get egress waypoint uid.
      local EgressUID=Mission:GetGroupEgressWaypointUID(self)
            
      if EgressUID then
        -- Egress coordinate given ==> wait until we pass that waypoint.
        self:T(self.lid..string.format("Task Done but Egress waypoint defined ==> Will call Mission Done once group passed waypoint UID=%d!", EgressUID))
      else
        -- Mission done!
        self:T(self.lid.."Task Done ==> Mission Done!")
        self:MissionDone(Mission)
      end
    else
      ---
      -- Mission Paused: Do nothing! Just set the current mission to nil so we can launch a new one.
      ---
      if self:IsOnMission(Mission.auftragsnummer) then
        self.currentmission=nil
      end
      -- Remove mission waypoints.
      self:T(self.lid.."Remove mission waypoints")
      self:_RemoveMissionWaypoints(Mission, false)
    end

  else

    if Task.description=="Engage_Target" then
      self:T(self.lid.."Task DONE Engage_Target ==> Cruise")
      self:Disengage()
    end

    if Task.description==AUFTRAG.SpecialTask.ONGUARD or Task.description==AUFTRAG.SpecialTask.ARMOREDGUARD or Task.description==AUFTRAG.SpecialTask.NOTHING then
      self:T(self.lid.."Task DONE OnGuard ==> Cruise")
      self:Cruise()
    end
    
    if Task.description=="Task_Land_At" then
      self:T(self.lid.."Taske DONE Task_Land_At ==> Wait")
      self:Cruise()
      self:Wait(20, 100)
    else
      self:T(self.lid.."Task Done but NO mission found ==> _CheckGroupDone in 1 sec")
      self:_CheckGroupDone(1)
    end

  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Mission Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Add mission to queue.
-- @param #OPSGROUP self
-- @param Ops.Auftrag#AUFTRAG Mission Mission for this group.
-- @return #OPSGROUP self
function OPSGROUP:AddMission(Mission)

  -- Add group to mission.
  Mission:AddOpsGroup(self)

  -- Set group status to SCHEDULED..
  Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.SCHEDULED)

  -- Set mission status to SCHEDULED.
  Mission:Scheduled()

  -- Add elements.
  Mission.Nelements=Mission.Nelements+#self.elements
  
  -- Increase number of groups.
  Mission.Ngroups=Mission.Ngroups+1

  -- Add mission to queue.
  table.insert(self.missionqueue, Mission)

  -- ad infinitum?
  self.adinfinitum = Mission.DCStask.params.adinfinitum and Mission.DCStask.params.adinfinitum or false

  -- Info text.
  local text=string.format("Added %s mission %s starting at %s, stopping at %s",
  tostring(Mission.type), tostring(Mission.name), UTILS.SecondsToClock(Mission.Tstart, true), Mission.Tstop and UTILS.SecondsToClock(Mission.Tstop, true) or "INF")
  self:T(self.lid..text)

  return self
end

--- Remove mission from queue.
-- @param #OPSGROUP self
-- @param Ops.Auftrag#AUFTRAG Mission Mission to be removed.
-- @return #OPSGROUP self
function OPSGROUP:RemoveMission(Mission)

  --for i,_mission in pairs(self.missionqueue) do
  for i=#self.missionqueue,1,-1 do
  
    -- Mission.
    local mission=self.missionqueue[i] --Ops.Auftrag#AUFTRAG

    -- Check mission ID.
    if mission.auftragsnummer==Mission.auftragsnummer then

      -- Remove mission waypoint task.
      local Task=Mission:GetGroupWaypointTask(self)

      if Task then
        self:RemoveTask(Task)
      end

      -- Take care of a paused mission.
      for j=#self.pausedmissions,1,-1 do
        local mid=self.pausedmissions[j]
        if Mission.auftragsnummer==mid then
          table.remove(self.pausedmissions, j)
        end
      end

      -- Remove mission from queue.
      table.remove(self.missionqueue, i)

      return self
    end

  end

  return self
end

--- Cancel all missions in mission queue that are not already done or cancelled.
-- @param #OPSGROUP self
function OPSGROUP:CancelAllMissions()
  self:T(self.lid.."Cancelling ALL missions!")

  -- Cancel all missions.
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    -- Current group status.
    local mystatus=mission:GetGroupStatus(self)

    -- Check if mission is already over!
    if not (mystatus==AUFTRAG.GroupStatus.DONE or mystatus==AUFTRAG.GroupStatus.CANCELLED) then        
    --if mission:IsNotOver() then
      self:T(self.lid.."Cancelling mission "..tostring(mission:GetName()))
      self:MissionCancel(mission)
    end
  end

end

--- Count remaining missons.
-- @param #OPSGROUP self
-- @return #number Number of missions to be done.
function OPSGROUP:CountRemainingMissison()

  local N=0

  -- Loop over mission queue.
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    if mission and mission:IsNotOver() then

      -- Get group status.
      local status=mission:GetGroupStatus(self)

      if status~=AUFTRAG.GroupStatus.DONE and status~=AUFTRAG.GroupStatus.CANCELLED then
        N=N+1
      end

    end
  end

  return N
end

--- Count remaining cargo transport assignments.
-- @param #OPSGROUP self
-- @return #number Number of unfinished transports in the queue.
function OPSGROUP:CountRemainingTransports()

  local N=0

  -- Loop over mission queue.
  for _,_transport in pairs(self.cargoqueue) do
    local transport=_transport --Ops.OpsTransport#OPSTRANSPORT

    local mystatus=transport:GetCarrierTransportStatus(self)
    local status=transport:GetState()

    -- Debug info.
    self:T(self.lid..string.format("Transport my status=%s [%s]", mystatus, status))

    -- Count not delivered (executing or scheduled) assignments.
    if transport and mystatus==OPSTRANSPORT.Status.SCHEDULED and status~=OPSTRANSPORT.Status.DELIVERED and status~=OPSTRANSPORT.Status.CANCELLED then
      N=N+1
    end
  end

  -- In case we directly set the cargo transport (not in queue).
  if N==0 and self.cargoTransport and
    self.cargoTransport:GetState()~=OPSTRANSPORT.Status.DELIVERED and self.cargoTransport:GetCarrierTransportStatus(self)~=OPSTRANSPORT.Status.DELIVERED and
    self.cargoTransport:GetState()~=OPSTRANSPORT.Status.CANCELLED and self.cargoTransport:GetCarrierTransportStatus(self)~=OPSTRANSPORT.Status.CANCELLED then
    N=1
  end

  return N
end

--- Get next mission.
-- @param #OPSGROUP self
-- @return Ops.Auftrag#AUFTRAG Next mission or *nil*.
function OPSGROUP:_GetNextMission()

  -- Check if group is acting as carrier or cargo at the moment.
  if self:IsPickingup() or self:IsLoading() or self:IsTransporting() or self:IsUnloading() or self:IsLoaded() then
    return nil
  end

  -- Number of missions.
  local Nmissions=#self.missionqueue

  -- Treat special cases.
  if Nmissions==0 then
    return nil
  end

  -- Sort results table wrt times they have already been engaged.
  local function _sort(a, b)
    local taskA=a --Ops.Auftrag#AUFTRAG
    local taskB=b --Ops.Auftrag#AUFTRAG
    return (taskA.prio<taskB.prio) or (taskA.prio==taskB.prio and taskA.Tstart<taskB.Tstart)
  end
  table.sort(self.missionqueue, _sort)

  -- Look for first mission that is SCHEDULED.
  local vip=math.huge
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG
    if mission.importance and mission.importance<vip then
      vip=mission.importance
    end
  end

  -- Look for first mission that is SCHEDULED.
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    -- TODO: One could think of opsgroup specific start conditions. A legion also checks if "ready" but it can be other criteria for the group to actually start the mission.
    --       Good example is the above transport. The legion should start the mission but the group should only start after the transport is finished.

    -- Escort mission. Check that escorted group is alive.
    local isEscort=true
    if mission.type==AUFTRAG.Type.ESCORT then
      local target=mission:GetTargetData()
      if not target:IsAlive() then
        isEscort=false
      end
    end

    -- Local transport.
    local isTransport=true
    if mission.opstransport then
      local cargos=mission.opstransport:GetCargoOpsGroups(false) or {}
      for _,_opsgroup in pairs(cargos) do
        local opscargo=_opsgroup --Ops.OpsGroup#OPSGROUP
        if opscargo.groupname==self.groupname then
          --isTransport=false
          break
        end
      end
    end

    -- Conditons to start.
    local isScheduled=mission:GetGroupStatus(self)==AUFTRAG.GroupStatus.SCHEDULED
    local isReadyToGo=(mission:IsReadyToGo() or self.legion)
    local isImportant=(mission.importance==nil or mission.importance<=vip)
    
    -- Everything on go?
    local go=isScheduled and isReadyToGo and isImportant and isTransport and isEscort
    
    -- Debug info.
    self:T3(self.lid..string.format("Mission %s [%s]: Go=%s [Scheduled=%s, Ready=%s, Important=%s, Transport=%s, Escort=%s]", mission:GetName(), mission:GetType(), tostring(go), 
    tostring(isScheduled), tostring(isReadyToGo), tostring(isImportant), tostring(isTransport), tostring(isEscort)))

    -- Check necessary conditions.
    if go then
      return mission
    end

  end

  return nil
end

--- Get mission by its id (auftragsnummer).
-- @param #OPSGROUP self
-- @param #number id Mission id (auftragsnummer).
-- @return Ops.Auftrag#AUFTRAG The mission.
function OPSGROUP:GetMissionByID(id)

  if not id then
    return nil
  end

  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    if mission.auftragsnummer==id then
      return mission
    end

  end

  return nil
end

--- Check if a given mission is already in the queue.
-- @param #OPSGROUP self
-- @param Ops.Auftrag#AUFTRAG Mission the mission to check
-- @return #boolean If `true`, the mission is in the queue.
function OPSGROUP:IsMissionInQueue(Mission)

  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    if mission.auftragsnummer==Mission.auftragsnummer then
      return true
    end

  end

  return false
end

--- Check if a given mission type is already in the queue.
-- @param #OPSGROUP self
-- @param #string MissionType MissionType Type of mission.
-- @return #boolean If `true`, the mission type is in the queue.
function OPSGROUP:IsMissionTypeInQueue(MissionType)

  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    if mission:GetType()==MissionType then
      return true
    end

  end

  return false
end

--- Get mission by its task id.
-- @param #OPSGROUP self
-- @param #number taskid The id of the (waypoint) task of the mission.
-- @return Ops.Auftrag#AUFTRAG The mission.
function OPSGROUP:GetMissionByTaskID(taskid)

  if taskid then
    for _,_mission in pairs(self.missionqueue) do
      local mission=_mission --Ops.Auftrag#AUFTRAG

      local task=mission:GetGroupWaypointTask(self)

      if task and task.id and task.id==taskid then
        return mission
      end

    end
  end

  return nil
end

--- Get current mission.
-- @param #OPSGROUP self
-- @return Ops.Auftrag#AUFTRAG The current mission or *nil*.
function OPSGROUP:GetMissionCurrent()
  return self:GetMissionByID(self.currentmission)
end

--- Check if group is currently on a mission.
-- @param #OPSGROUP self
-- @param #number MissionUID (Optional) Check if group is currently on a mission with this UID. Default is to check for any current mission.
-- @return #boolean If `true`, group is currently on a mission.
function OPSGROUP:IsOnMission(MissionUID)
  if self.currentmission==nil then
    -- No current mission.
    return false
  else
    if MissionUID then
      -- Return if on specific mission.
      return MissionUID==self.currentmission
    else
      -- Is on any mission.
      return true    
    end
  end
  -- Is on any mission.
  return true
end

--- On before "MissionStart" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Auftrag#AUFTRAG Mission The mission table.
function OPSGROUP:onbeforeMissionStart(From, Event, To, Mission)

  -- Debug info.
  self:T(self.lid..string.format("Starting mission %s, FSM=%s, LateActivated=%s, UnControlled=%s", tostring(Mission.name), self:GetState(), tostring(self:IsLateActivated()), tostring(self:IsUncontrolled())))

  -- Delay for route to mission. Group needs to be activated and controlled.
  local delay=0

  -- Check if group is spawned.
  if self:IsInUtero() then

    -- Activate group if it is late activated.
    if self:IsLateActivated() then
      self:Activate(delay)
      delay=delay+1
    end

  end

  -- Startup group if it is uncontrolled. Alert 5 aircraft will not be started though!
  if self:IsFlightgroup() and self:IsUncontrolled() and Mission.type~=AUFTRAG.Type.ALERT5 then
    local fc=FLIGHTGROUP.GetFlightControl(self)
    if fc and fc:IsControlling(self) then
      FLIGHTGROUP.SetReadyForTakeoff(self, true)
    else
      self:StartUncontrolled(delay)
    end
  end

  return true
end

--- On after "MissionStart" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Auftrag#AUFTRAG Mission The mission table.
function OPSGROUP:onafterMissionStart(From, Event, To, Mission)

  -- Debug output.
  local text=string.format("Starting %s Mission %s, target %s", Mission.type, tostring(Mission.name), Mission:GetTargetName())
  self:T(self.lid..text)

  -- Set current mission.
  self.currentmission=Mission.auftragsnummer

  -- Set group mission status to STARTED.
  Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.STARTED)

  -- Set mission status to STARTED.
  Mission:__Started(3)
  
  -- Set ready for takeoff in case of FLIGHTCONTROL.
  --if self.isFlightgroup and Mission.type~=AUFTRAG.Type.ALERT5 then
  --  FLIGHTGROUP.SetReadyForTakeoff(self, true)
  --end
  
  -- Route group to mission zone.
  if self.speedMax>3.6 or true then

    self:RouteToMission(Mission, 3)

  else
    ---
    -- IMMOBILE Group
    ---

    -- Debug info.
    self:T(self.lid.."Immobile GROUP!")

    -- Add waypoint task. UpdateRoute is called inside.
    local Clock=Mission.Tpush and UTILS.SecondsToClock(Mission.Tpush) or 5
    
    -- Add mission task.
    local Task=self:AddTask(Mission.DCStask, Clock, Mission.name, Mission.prio, Mission.duration)
    Task.ismission=true

    -- Set waypoint task.
    Mission:SetGroupWaypointTask(self, Task)

    -- Execute task. This calls mission execute.
    self:__TaskExecute(3, Task)
  end

end

--- On after "MissionExecute" event. Mission execution began.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Auftrag#AUFTRAG Mission The mission table.
function OPSGROUP:onafterMissionExecute(From, Event, To, Mission)

  local text=string.format("Executing %s Mission %s, target %s", Mission.type, tostring(Mission.name), Mission:GetTargetName())
  self:T(self.lid..text)

  -- Set group mission status to EXECUTING.
  Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.EXECUTING)

  -- Set mission status to EXECUTING.
  Mission:Executing()
  
  -- Group is holding but has waypoints ==> Cruise.
  if self:IsHolding() and not self:HasPassedFinalWaypoint() then
    self:Cruise()
  end

  -- Set auto engage detected targets.
  if Mission.engagedetectedOn then
    self:SetEngageDetectedOn(UTILS.MetersToNM(Mission.engagedetectedRmax), Mission.engagedetectedTypes, Mission.engagedetectedEngageZones, Mission.engagedetectedNoEngageZones)
  end
  
  -- Set AB usage for mission execution based on Mission entry, if the option was set in the mission
  if self.isFlightgroup then
    if Mission.prohibitABExecute == true then
      self:SetProhibitAfterburner()
      self:T(self.lid.."Set prohibit AB")
    elseif Mission.prohibitABExecute == false then
      self:SetAllowAfterburner()
      self:T2(self.lid.."Set allow AB")
    end
  end
  
end

--- On after "PauseMission" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterPauseMission(From, Event, To)

  local Mission=self:GetMissionCurrent()

  if Mission then

    -- Set group mission status to PAUSED.
    Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.PAUSED)

    -- Get mission waypoint task.
    local Task=Mission:GetGroupWaypointTask(self)

    -- Debug message.
    self:T(self.lid..string.format("Pausing current mission %s. Task=%s", tostring(Mission.name), tostring(Task and Task.description or "WTF")))

    -- Cancelling the mission is actually cancelling the current task.
    self:TaskCancel(Task)
    
    self:_RemoveMissionWaypoints(Mission)

    -- Set mission to pause so we can unpause it later.
    table.insert(self.pausedmissions, 1, Mission.auftragsnummer)

  end

end

--- On after "UnpauseMission" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterUnpauseMission(From, Event, To)
  
  -- Get paused mission.
  local mission=self:_GetPausedMission()

  if mission then
  
    -- Debug info.
    self:T(self.lid..string.format("Unpausing mission %s [%s]", mission:GetName(), mission:GetType()))
    
    -- Set state of mission, e.g. for not teleporting again
    mission.unpaused=true
    
    -- Start mission.
    self:MissionStart(mission)
    
    -- Remove mission from pausedmissions queue
    for i,mid in pairs(self.pausedmissions) do
      --self:T(self.lid..string.format("Checking paused mission", mid))
      if mid==mission.auftragsnummer then
        self:T(self.lid..string.format("Removing paused mission id=%d", mid))
        table.remove(self.pausedmissions, i)
        break
      end
    end
    
  else
    self:T(self.lid.."ERROR: No mission to unpause!")
  end

end


--- On after "MissionCancel" event. Cancels the mission.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Auftrag#AUFTRAG Mission The mission to be cancelled.
function OPSGROUP:onafterMissionCancel(From, Event, To, Mission)

  if self:IsOnMission(Mission.auftragsnummer) then

    ---
    -- Current Mission
    ---

    -- Some missions dont have a task set, which could be cancelled.
    --[[
    if Mission.type==AUFTRAG.Type.ALERT5 or 
       Mission.type==AUFTRAG.Type.ONGUARD or 
       Mission.type==AUFTRAG.Type.ARMOREDGUARD or
       --Mission.type==AUFTRAG.Type.NOTHING or 
       Mission.type==AUFTRAG.Type.AIRDEFENSE or
       Mission.type==AUFTRAG.Type.EWR then
       
      -- Trigger mission don task.
      self:MissionDone(Mission)
      
      return
    end
    ]]

    -- Get mission waypoint task.
    local Task=Mission:GetGroupWaypointTask(self)
    
    if Task then

      -- Debug info.
      self:T(self.lid..string.format("Cancel current mission %s. Task=%s", tostring(Mission.name), tostring(Task and Task.description or "WTF")))
  
      -- Cancelling the mission is actually cancelling the current task.
      -- Note that two things can happen.
      -- 1.) Group is still on the way to the waypoint (status should be STARTED). In this case there would not be a current task!
      -- 2.) Group already passed the mission waypoint (status should be EXECUTING).
  
      self:TaskCancel(Task)
      
    else
    
      -- Some missions dont have a task set, which could be cancelled.

      -- Trigger mission don task.
      self:MissionDone(Mission)
    
    end

  else

    ---
    -- NOT the current mission
    ---

    -- Set mission group status.
    Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.CANCELLED)

    -- Remove mission from queue
    self:RemoveMission(Mission)

    -- Send group RTB or WAIT if nothing left to do.
    self:_CheckGroupDone(1)

  end

end

--- On after "MissionDone" event.
-- @param #OPSGROUP self
-- @param Ops.Auftrag#AUFTRAG Mission
-- @param #boolean Silently Remove waypoints by `table.remove()` and do not update the route.
function OPSGROUP:_RemoveMissionWaypoints(Mission, Silently)

  for i=#self.waypoints,1,-1 do
    local wp=self.waypoints[i] --#OPSGROUP.Waypoint
    if wp.missionUID==Mission.auftragsnummer then
      if Silently then
        table.remove(self.waypoints, i)
      else
        self:RemoveWaypoint(i)
      end
    end
  end

end

--- On after "MissionDone" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.Auftrag#AUFTRAG Mission The mission that is done.
function OPSGROUP:onafterMissionDone(From, Event, To, Mission)

  -- Debug info.
  local text=string.format("Mission %s DONE!", Mission.name)
  self:T(self.lid..text)

  -- Set group status.
  Mission:SetGroupStatus(self, AUFTRAG.GroupStatus.DONE)

  -- Set current mission to nil.
  if self:IsOnMission(Mission.auftragsnummer) then
    self.currentmission=nil
  end

  -- Remove mission waypoints.
  self:_RemoveMissionWaypoints(Mission)

  -- Decrease patrol data.
  if Mission.patroldata then
    Mission.patroldata.noccupied=Mission.patroldata.noccupied-1
    AIRWING.UpdatePatrolPointMarker(self,Mission.patroldata)
  end

  -- Switch auto engage detected off. This IGNORES that engage detected had been activated for the group!
  if Mission.engagedetectedOn then
    self:SetEngageDetectedOff()
  end

  -- ROE to default.
  if Mission.optionROE then
    self:SwitchROE()
  end
  -- ROT to default
  if self:IsFlightgroup() and Mission.optionROT then
    self:SwitchROT()
  end
  -- Alarm state to default.
  if Mission.optionAlarm then
    self:SwitchAlarmstate()
  end
  -- EPLRS to default.
  if Mission.optionEPLRS then
    self:SwitchEPLRS()
  end
  -- Emission to default.
  if Mission.optionEmission then
    self:SwitchEmission()
  end
  -- Invisible to default.
  if Mission.optionInvisible then
    self:SwitchInvisible()
  end
  -- Immortal to default.
  if Mission.optionImmortal then
    self:SwitchImmortal()
  end
  -- Formation to default.
  if Mission.optionFormation and self:IsFlightgroup() then
    self:SwitchFormation()
  end
  -- Radio freq and modu to default.
  if Mission.radio then
    self:SwitchRadio()
  end

  -- TACAN beacon.
  if Mission.tacan then

    -- Switch to default.
    self:_SwitchTACAN()

    -- Return Cohort's TACAN channel.
    local cohort=self.cohort --Ops.Cohort#COHORT
    if cohort then
      cohort:ReturnTacan(Mission.tacan.Channel)
    end

    -- Set asset TACAN to nil.
    local asset=Mission:GetAssetByName(self.groupname)
    if asset then
      asset.tacan=nil
    end
  end

  -- ICLS beacon to default.
  if Mission.icls then
    self:_SwitchICLS()
  end
  
  -- Return to legion?
  if self.legion and Mission.legionReturn~=nil then
    self:SetReturnToLegion(Mission.legionReturn)
  end

  -- Delay before check if group is done.
  local delay=1
  
  -- Special mission cases.
  if Mission.type==AUFTRAG.Type.ARTY then
    -- We add a 10 sec delay for ARTY. Found that they need some time to readjust the barrel of their gun. Not sure if necessary for all. Needs some more testing!
    delay=60   
  elseif Mission.type==AUFTRAG.Type.RELOCATECOHORT then
  
    -- New legion.
    local legion=Mission.DCStask.params.legion --Ops.Legion#LEGION
    
    -- Debug message.
    self:T(self.lid..string.format("Asset relocated to new legion=%s",tostring(legion.alias)))
    
    -- Get asset and change its warehouse id.
    local asset=Mission:GetAssetByName(self.groupname)
    if asset then
      asset.wid=legion.uid
    end
    
    -- Set new legion.
    self.legion=legion
    
    if self.isArmygroup then
      self:T2(self.lid.."Adding asset via ReturnToLegion()")
      self:ReturnToLegion()
    elseif self.isFlightgroup then
      self:T2(self.lid.."Adding asset via RTB to new legion airbase")
      self:RTB(self.legion.airbase)
    end
    
    return
  end
  
  -- Set AB usage based on Mission entry, if the option was set in the mission
  if self.isFlightgroup then
    if Mission.prohibitAB == true then
      self:T2("Setting prohibit AB")
      self:SetProhibitAfterburner()
    elseif Mission.prohibitAB == false then
      self:T2("Setting allow AB")
      self:SetAllowAfterburner()
    end
  end
  
  if self.legion and self.legionReturn==false and self.waypoints and #self.waypoints==1 then
    ---
    -- This is the case where a group was send on a mission (which is over now), has no addional
    -- waypoints or tasks and should NOT return to its legion.
    -- We create a new waypoint at the current position and let it hold here.
    ---
    
    local Coordinate=self:GetCoordinate()
    
    if self.isArmygroup then
      ARMYGROUP.AddWaypoint(self, Coordinate, 0, nil, nil, false)
    elseif self.isNavygroup then
      NAVYGROUP.AddWaypoint(self,Coordinate, 0, nil, nil, false)
    end
    
    -- Remove original waypoint.
    self:RemoveWaypoint(1)
    
    self:_PassedFinalWaypoint(true, "Passed final waypoint as group is done with mission but should NOT return to its legion")
  end
  
  -- Check if group is done.
  self:_CheckGroupDone(delay)

end

--- Route group to mission.
-- @param #OPSGROUP self
-- @param Ops.Auftrag#AUFTRAG mission The mission table.
-- @param #number delay Delay in seconds.
function OPSGROUP:RouteToMission(mission, delay)

  if delay and delay>0 then
    -- Delayed call.
    self:ScheduleOnce(delay, OPSGROUP.RouteToMission, self, mission)
  else

    -- Debug info.
    self:T(self.lid..string.format("Route To Mission"))

    -- Catch dead or stopped groups.
    if self:IsDead() or self:IsStopped() then
      self:T(self.lid..string.format("Route To Mission: I am DEAD or STOPPED! Ooops..."))
      return
    end
    
    -- Check if this group is cargo.
    if self:IsCargo() then
      self:T(self.lid..string.format("Route To Mission: I am CARGO! You cannot route me..."))
      return
    end

    -- OPSTRANSPORT: Just add the ops transport to the queue.
    if mission.type==AUFTRAG.Type.OPSTRANSPORT then
      self:T(self.lid..string.format("Route To Mission: I am OPSTRANSPORT! Add transport and return..."))
      self:AddOpsTransport(mission.opstransport)
      return
    end

    -- ALERT5: Just set the mission to executing.
    if mission.type==AUFTRAG.Type.ALERT5 then
      self:T(self.lid..string.format("Route To Mission: I am ALERT5! Go right to MissionExecute()..."))
      self:MissionExecute(mission)
      return
    end

    -- ID of current waypoint.
    local uid=self:GetWaypointCurrentUID()

    -- Ingress waypoint coordinate where the mission is executed.
    local waypointcoord=nil --Core.Point#COORDINATE
    
    -- Current coordinate of the group.
    local currentcoord=self:GetCoordinate()
    
    -- Road connection.
    local roadcoord=currentcoord:GetClosestPointToRoad()
    
    local roaddist=nil
    if roadcoord then
      roaddist=currentcoord:Get2DDistance(roadcoord)
    end
        
    -- Target zone.
    local targetzone=nil --Core.Zone#ZONE
  
    -- Random radius of 1000 meters.
    local randomradius=mission.missionWaypointRadius or 1000

    -- Surface types.
    local surfacetypes=nil
    if self:IsArmygroup() then
      surfacetypes={land.SurfaceType.LAND, land.SurfaceType.ROAD}
    elseif self:IsNavygroup() then
      surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER}
    end
    
    -- Get target object.
    local targetobject=mission:GetObjective(currentcoord, UTILS.GetCoalitionEnemy(self:GetCoalition(), true))
    
    if targetobject then
      self:T(self.lid..string.format("Route to mission target object %s", targetobject:GetName()))
    end
    
    -- Get ingress waypoint.    
    if mission.opstransport and not mission.opstransport:IsCargoDelivered(self.groupname) then
      
      -- Get transport zone combo.
      local tzc=mission.opstransport:GetTZCofCargo(self.groupname)
      
      local pickupzone=tzc.PickupZone
      
      if self:IsInZone(pickupzone) then
        -- We are already in the pickup zone.      
        self:PauseMission()
        self:FullStop()
        return
      else
        -- Get a random coordinate inside the pickup zone.
        waypointcoord=pickupzone:GetRandomCoordinate()
      end
      
    elseif mission.type==AUFTRAG.Type.PATROLZONE or 
           mission.type==AUFTRAG.Type.BARRAGE    or 
           mission.type==AUFTRAG.Type.AMMOSUPPLY or
           mission.type==AUFTRAG.Type.FUELSUPPLY or
           mission.type==AUFTRAG.Type.REARMING   or 
           mission.type==AUFTRAG.Type.AIRDEFENSE or
           mission.type==AUFTRAG.Type.EWR        then
      ---
      -- Missions with ZONE as target
      ---
      
      -- Get the zone.
      targetzone=targetobject --Core.Zone#ZONE
      
      -- Random coordinate.
      waypointcoord=targetzone:GetRandomCoordinate(nil , nil, surfacetypes)
      
    elseif mission.type==AUFTRAG.Type.ONGUARD or mission.type==AUFTRAG.Type.ARMOREDGUARD then
      ---
      -- Guard
      ---
      
      -- Mission waypoint
      waypointcoord=mission:GetMissionWaypointCoord(self.group, nil, surfacetypes)

    elseif mission.type==AUFTRAG.Type.NOTHING then
      ---
      -- Nothing
      ---

      -- Get the zone.
      targetzone=targetobject --Core.Zone#ZONE
      
      -- Random coordinate.
      waypointcoord=targetzone:GetRandomCoordinate(nil , nil, surfacetypes)
            
    elseif mission.type==AUFTRAG.Type.HOVER then
      ---
      -- Hover
      ---

      local zone=targetobject --Core.Zone#ZONE
      
      waypointcoord=zone:GetCoordinate()
      
    elseif mission.type==AUFTRAG.Type.RELOCATECOHORT then
      ---
      -- Relocation
      ---

      -- Roughly go to the new legion. 
      local ToCoordinate=mission.DCStask.params.legion:GetCoordinate()      
            
      if self.isFlightgroup then
        -- Get mission waypoint coord in direction of the 
        waypointcoord=currentcoord:GetIntermediateCoordinate(ToCoordinate, 0.2):SetAltitude(self.altitudeCruise)
      elseif self.isArmygroup then
        -- Army group: check for road connection.
        if roadcoord then
          waypointcoord=roadcoord
        else
          waypointcoord=currentcoord:GetIntermediateCoordinate(ToCoordinate, 100)
        end
      else
        -- Navy group: Route into direction of the target.
        waypointcoord=currentcoord:GetIntermediateCoordinate(ToCoordinate, 0.05)
      end
    
    elseif mission.type==AUFTRAG.Type.CAPTUREZONE then
    
      -- Get the zone.
      targetzone=targetobject:GetZone()
      
      -- Random coordinate.
      waypointcoord=targetzone:GetRandomCoordinate(nil , nil, surfacetypes)      
      
    else
      ---
      -- Default case
      ---
      
      waypointcoord=mission:GetMissionWaypointCoord(self.group, randomradius, surfacetypes)
    end

    -- Add enroute tasks.
    for _,task in pairs(mission.enrouteTasks) do
      self:AddTaskEnroute(task)
    end

    -- Speed to mission waypoint.
    local SpeedToMission=mission.missionSpeed and UTILS.KmphToKnots(mission.missionSpeed) or self:GetSpeedCruise()

    -- Special for Troop transport.
    if mission.type==AUFTRAG.Type.TROOPTRANSPORT then

      ---
      -- TROOP TRANSPORT
      ---

      -- Refresh DCS task with the known controllable.
      mission.DCStask=mission:GetDCSMissionTask(self.group)
      
      -- Create a pickup zone around the pickup coordinate. The troops will go to a random point inside the zone.
      -- This is necessary so the helos do not try to land at the exact same location where the troops wait.
      local pradius=mission.transportPickupRadius
      local pickupZone=ZONE_RADIUS:New("Pickup Zone", mission.transportPickup:GetVec2(), pradius)

      -- Add task to embark for the troops.
      for _,_group in pairs(mission.transportGroupSet.Set) do
        local group=_group --Wrapper.Group#GROUP

        if group and group:IsAlive() then
          -- Get random coordinate inside the zone.
          local pcoord=pickupZone:GetRandomCoordinate(20, pradius, {land.SurfaceType.LAND, land.SurfaceType.ROAD})
          
          -- Let the troops embark the transport.
          local DCSTask=group:TaskEmbarkToTransport(pcoord, pradius)
          group:SetTask(DCSTask, 5)
        end

      end

    elseif mission.type==AUFTRAG.Type.ARTY then

      ---
      -- ARTY
      ---

      -- Target Coord. 
      local targetcoord=mission:GetTargetCoordinate()
      
      -- In range already?
      local inRange=self:InWeaponRange(targetcoord, mission.engageWeaponType, waypointcoord)
      
      if inRange then
      
        --waypointcoord=self:GetCoordinate(true)
      
      else

        local coordInRange=self:GetCoordinateInRange(targetcoord, mission.engageWeaponType, waypointcoord, surfacetypes)
        
        if coordInRange then
  
          -- Add waypoint at 
          local waypoint=nil --#OPSGROUP.Waypoint
          if self:IsFlightgroup() then
            waypoint=FLIGHTGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise), false)
          elseif self:IsArmygroup() then
            waypoint=ARMYGROUP.AddWaypoint(self,   waypointcoord, SpeedToMission, uid, mission.optionFormation, false)
          elseif self:IsNavygroup() then
            waypoint=NAVYGROUP.AddWaypoint(self,   waypointcoord, SpeedToMission, uid, UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise), false)
          end
          waypoint.missionUID=mission.auftragsnummer
  
          -- Set waypoint coord to be the one in range. Take care of proper waypoint uid.
          waypointcoord=coordInRange
          uid=waypoint.uid
          
        end
        
      end
      
    end


    -- Distance to waypoint coordinate.
    local d=currentcoord:Get2DDistance(waypointcoord)
    
    -- Debug info.
    self:T(self.lid..string.format("Distance to ingress waypoint=%.1f m", d))
    
    -- Add mission execution (ingress) waypoint.
    local waypoint=nil --#OPSGROUP.Waypoint
    if self:IsFlightgroup() then
      

      local ingresscoord = mission:GetMissionIngressCoord()
      local holdingcoord = mission:GetMissionHoldingCoord()
      
      if holdingcoord then 
        waypoint=FLIGHTGROUP.AddWaypoint(self, holdingcoord, mission.missionHoldingCoordSpeed or SpeedToMission, uid, UTILS.MetersToFeet(mission.missionHoldingCoordAlt or self.altitudeCruise), false)
        uid=waypoint.uid
          -- Orbit until flaghold=1 (true) but max 5 min
        self.flaghold:Set(0)
        local TaskOrbit = self.group:TaskOrbit(holdingcoord, mission.missionHoldingCoordAlt, UTILS.KnotsToMps(mission.missionHoldingCoordSpeed or SpeedToMission))
        local TaskStop  = self.group:TaskCondition(nil, self.flaghold.UserFlagName, 1, nil, mission.missionHoldingDuration or 900)
        local TaskCntr  = self.group:TaskControlled(TaskOrbit, TaskStop)
        local TaskOver  = self.group:TaskFunction("FLIGHTGROUP._FinishedWaiting", self)       
        local DCSTasks=self.group:TaskCombo({TaskCntr, TaskOver})
        -- Add waypoint task. UpdateRoute is called inside.
        local waypointtask=self:AddTaskWaypoint(DCSTasks, waypoint, "Holding")
        waypointtask.ismission=false
        self.isHoldingAtHoldingPoint = true
      end
      
      if ingresscoord then 
        waypoint=FLIGHTGROUP.AddWaypoint(self, ingresscoord, mission.missionIngressCoordSpeed or SpeedToMission, uid, UTILS.MetersToFeet(mission.missionIngressCoordAlt or self.altitudeCruise), false)
        uid=waypoint.uid
      end
     
      waypoint=FLIGHTGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise), false)
      
    elseif self:IsArmygroup() then
    
      -- Set formation.
      local formation=mission.optionFormation
      
      -- If distance is < 1 km or RELOCATECOHORT mission, go off-road.
      if d<1000 or mission.type==AUFTRAG.Type.RELOCATECOHORT then
        formation=ENUMS.Formation.Vehicle.OffRoad
      end
      
      waypoint=ARMYGROUP.AddWaypoint(self, waypointcoord, SpeedToMission, uid, formation, false)
      
    elseif self:IsNavygroup() then
    
      waypoint=NAVYGROUP.AddWaypoint(self,   waypointcoord, SpeedToMission, uid, UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise), false)
      
    end
    waypoint.missionUID=mission.auftragsnummer

    -- Add waypoint task. UpdateRoute is called inside.
    local waypointtask=self:AddTaskWaypoint(mission.DCStask, waypoint, mission.name, mission.prio, mission.duration)
    waypointtask.ismission=true
    
    waypointtask.target=targetobject

    -- Set waypoint task.
    mission:SetGroupWaypointTask(self, waypointtask)

    -- Set waypoint index.
    mission:SetGroupWaypointIndex(self, waypoint.uid)

    -- Add egress waypoint.
    local egresscoord=mission:GetMissionEgressCoord()
    if egresscoord then
      local Ewaypoint=nil --#OPSGROUP.Waypoint
      if self:IsFlightgroup() then
        Ewaypoint=FLIGHTGROUP.AddWaypoint(self, egresscoord, mission.missionEgressCoordSpeed or SpeedToMission, waypoint.uid, UTILS.MetersToFeet(mission.missionEgressCoordAlt or self.altitudeCruise), false)
      elseif self:IsArmygroup() then
        Ewaypoint=ARMYGROUP.AddWaypoint(self,   egresscoord, SpeedToMission, waypoint.uid, mission.optionFormation, false)
      elseif self:IsNavygroup() then
        Ewaypoint=NAVYGROUP.AddWaypoint(self,   egresscoord, SpeedToMission, waypoint.uid, UTILS.MetersToFeet(mission.missionAltitude or self.altitudeCruise), false)
      end
      Ewaypoint.missionUID=mission.auftragsnummer
      mission:SetGroupEgressWaypointUID(self, Ewaypoint.uid)
    end
    
    -- Check if we are already where we want to be.
    if targetzone and self:IsInZone(targetzone) then
      self:T(self.lid.."Already in mission zone ==> TaskExecute()")
      self:TaskExecute(waypointtask)
      -- TODO: Calling PassingWaypoint here is probably better as it marks the mission waypoint as passed!
      --self:PassingWaypoint(waypoint)
      return
    elseif d<25 then
      self:T(self.lid.."Already within 25 meters of mission waypoint ==> TaskExecute()")
      self:TaskExecute(waypointtask)
      return
    end
    
    -- Check if group is mobile. Note that some immobile units report a speed of 1 m/s = 3.6 km/h.
    if (self.speedMax<=3.6 or mission.teleport) and not mission.unpaused then

      -- Teleport to waypoint coordinate. Mission will not be paused.
      self:Teleport(waypointcoord, nil, true)
      
      -- Execute task in one second.
      self:__TaskExecute(-1, waypointtask)
      
    else

      -- Give cruise command/update route.
      if self:IsArmygroup() then
        self:Cruise(SpeedToMission)
      elseif self:IsNavygroup() then
        self:Cruise(SpeedToMission)
      elseif self:IsFlightgroup() then
        self:UpdateRoute()
      end
          
    end
    
    ---
    -- Mission Specific Settings
    ---
    self:_SetMissionOptions(mission)

  end
end

--- Set mission specific options for ROE, Alarm state, etc.
-- @param #OPSGROUP self
-- @param Ops.Auftrag#AUFTRAG mission The mission table.
function OPSGROUP:_SetMissionOptions(mission)

  -- ROE
  if mission.optionROE then
    self:SwitchROE(mission.optionROE)
  end
  -- ROT
  if mission.optionROT then
    self:SwitchROT(mission.optionROT)
  end
  -- Alarm state
  if mission.optionAlarm then
    self:SwitchAlarmstate(mission.optionAlarm)
  end
  -- EPLRS
  if mission.optionEPLRS then
    self:SwitchEPLRS(mission.optionEPLRS)
  end
  -- Emission
  if mission.optionEmission then
    self:SwitchEmission(mission.optionEmission)
  end
  -- Invisible
  if mission.optionInvisible then
    self:SwitchInvisible(mission.optionInvisible)
  end
  -- Immortal
  if mission.optionImmortal then
    self:SwitchImmortal(mission.optionImmortal)
  end
  -- Formation
  if mission.optionFormation and self:IsFlightgroup() then
    self:SwitchFormation(mission.optionFormation)
  end
  -- Radio frequency and modulation.
  if mission.radio then
    self:SwitchRadio(mission.radio.Freq, mission.radio.Modu)
  end
  -- TACAN settings.
  if mission.tacan then
    self:SwitchTACAN(mission.tacan.Channel, mission.tacan.Morse, mission.tacan.BeaconName, mission.tacan.Band)
  end
  -- ICLS settings.
  if mission.icls then
    self:SwitchICLS(mission.icls.Channel, mission.icls.Morse, mission.icls.UnitName)
  end
  
  -- Set AB usage based on Mission entry, if the option was set in the mission
  if self.isFlightgroup then
    if mission.prohibitAB == true then
      self:SetProhibitAfterburner()
      self:T2("Set prohibit AB")
    elseif mission.prohibitAB == false then
      self:SetAllowAfterburner()
      self:T2("Set allow AB")
    end
  end

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Queue Update: Missions & Tasks
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "QueueUpdate" event.
-- @param #OPSGROUP self
function OPSGROUP:_QueueUpdate()

  ---
  -- Mission
  ---

  -- First check if group is alive? Late activated groups are activated and uncontrolled units are started automatically.
  if self:IsExist() then

    local mission=self:_GetNextMission()

    if mission then

      local currentmission=self:GetMissionCurrent()

      if currentmission then

        -- Current mission but new mission is urgent with higher prio.
        if mission.urgent and mission.prio<currentmission.prio then
          self:T(self.lid.."FF got urgent mission with higher prio!")
          self:MissionCancel(currentmission)
          self:__MissionStart(1, mission)
        end

      else
        -- No current mission.
        self:MissionStart(mission)
      end
    end
  end

  ---
  -- Tasks
  ---

  local ready=true

  -- For aircraft check airborne.
  if self:IsFlightgroup() then
    ready=self:IsAirborne()
  end

  -- Check no current task.
  if ready and self.taskcurrent<=0 then

    -- Get task from queue.
    local task=self:_GetNextTask()

    -- Execute task if any.
    if task then
      self:TaskExecute(task)
    end

  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On before "Wait" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number Duration Duration how long the group will be waiting in seconds. Default `nil` (=forever).
function OPSGROUP:onbeforeWait(From, Event, To, Duration)

  local allowed=true
  local Tsuspend=nil
  
  local mission=self:GetMissionCurrent()
  if mission then
    self:PauseMission()
    return true
  end

  -- Check for a current task.
  if self.taskcurrent>0 then
    self:T(self.lid..string.format("WARNING: Got current task ==> WAIT event is suspended for 30 sec!"))
    Tsuspend=-30
    allowed=false
  end

  -- Check for a current transport assignment.
  if self.cargoTransport then
    self:T(self.lid..string.format("WARNING: Got current TRANSPORT assignment ==> WAIT event is suspended for 30 sec!"))
    Tsuspend=-30
    allowed=false
  end

  -- Call wait again.
  if Tsuspend and not allowed then
    self:__Wait(Tsuspend, Duration)
  end

  return allowed
end

--- On after "Wait" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number Duration Duration in seconds how long the group will be waiting. Default `nil` (for ever).
function OPSGROUP:onafterWait(From, Event, To, Duration)

  -- Order Group to hold.
  self:FullStop()

  -- Set time stamp.
  self.Twaiting=timer.getAbsTime()

  -- Max waiting
  self.dTwait=Duration

end


--- On after "PassingWaypoint" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP.Waypoint Waypoint Waypoint data passed.
function OPSGROUP:onafterPassingWaypoint(From, Event, To, Waypoint)

  -- Get the current task.
  local task=self:GetTaskCurrent()

  -- Get the corresponding mission.
  local mission=nil  --Ops.Auftrag#AUFTRAG
  if task then
    mission=self:GetMissionByTaskID(task.id)
  end

  if task and task.dcstask.id==AUFTRAG.SpecialTask.PATROLZONE then

    ---
    -- SPECIAL TASK: Patrol Zone
    ---

    -- Remove old waypoint.
    self:RemoveWaypointByID(Waypoint.uid)

    -- Zone object.
    local zone=task.dcstask.params.zone --Core.Zone#ZONE

    -- Surface types.
    local surfacetypes=nil
    if self:IsArmygroup() then
      surfacetypes={land.SurfaceType.LAND, land.SurfaceType.ROAD}
    elseif self:IsNavygroup() then
      surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER}
    end

    -- Random coordinate in zone.
    local Coordinate=zone:GetRandomCoordinate(nil, nil, surfacetypes)

    -- Speed and altitude.
    local Speed=task.dcstask.params.speed and UTILS.MpsToKnots(task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise)
    local Altitude=UTILS.MetersToFeet(task.dcstask.params.altitude or self.altitudeCruise)

    local currUID=self:GetWaypointCurrent().uid

    local wp=nil --#OPSGROUP.Waypoint
    if self.isFlightgroup then
      wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude)
    elseif self.isArmygroup then
      wp=ARMYGROUP.AddWaypoint(self,   Coordinate, Speed, currUID, task.dcstask.params.formation)
    elseif self.isNavygroup then
      wp=NAVYGROUP.AddWaypoint(self,   Coordinate, Speed, currUID, Altitude)
    end
    wp.missionUID=mission and mission.auftragsnummer or nil

  elseif task and task.dcstask.id==AUFTRAG.SpecialTask.RECON then

    ---
    -- SPECIAL TASK: Recon Mission
    ---

    -- TARGET.
    local target=task.dcstask.params.target --Ops.Target#TARGET
    
    -- Init a table.
    if self.adinfinitum and #self.reconindecies==0 then -- all targets done once
      self.reconindecies={}
      for i=1,#target.targets do
        table.insert(self.reconindecies, i)
      end
    end
    
    if #self.reconindecies>0 then
    
      local n=1
      if task.dcstask.params.randomly then
        n=UTILS.GetRandomTableElement(self.reconindecies)
      else
        n=self.reconindecies[1]
        table.remove(self.reconindecies, 1)
      end
      
      -- Zone object.
      local object=target.targets[n] --Ops.Target#TARGET.Object
      local zone=object.Object --Core.Zone#ZONE

      -- Random coordinate in zone.
      local Coordinate=zone:GetRandomCoordinate()

      -- Speed and altitude.
      local Speed=task.dcstask.params.speed and UTILS.MpsToKnots(task.dcstask.params.speed) or UTILS.KmphToKnots(self.speedCruise)
      local Altitude=task.dcstask.params.altitude and UTILS.MetersToFeet(task.dcstask.params.altitude) or nil

      -- Debug.
      --Coordinate:MarkToAll("Recon Waypoint n="..tostring(n))

      local currUID=self:GetWaypointCurrent().uid

      local wp=nil --#OPSGROUP.Waypoint
      if self.isFlightgroup then
        wp=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, currUID, Altitude)
      elseif self.isArmygroup then
        wp=ARMYGROUP.AddWaypoint(self,   Coordinate, Speed, currUID, task.dcstask.params.formation)
      elseif self.isNavygroup then
        wp=NAVYGROUP.AddWaypoint(self,   Coordinate, Speed, currUID, Altitude)
      end
      wp.missionUID=mission and mission.auftragsnummer or nil
   
    else

      -- Get waypoint index.
      local wpindex=self:GetWaypointIndex(Waypoint.uid)

      -- Final waypoint reached?
      if wpindex==nil or wpindex==#self.waypoints then

        -- Set switch to true.
        if not self.adinfinitum or #self.waypoints<=1 then
          self:_PassedFinalWaypoint(true, "Passing waypoint and NOT adinfinitum and #self.waypoints<=1")
        end

      end

      -- Final zone reached ==> task done.
      self:TaskDone(task)

    end

  elseif task and task.dcstask.id==AUFTRAG.SpecialTask.RELOCATECOHORT then

    ---
    -- SPECIAL TASK: Relocate Mission
    ---

    -- TARGET.
    local legion=task.dcstask.params.legion --Ops.Legion#LEGION
    
   self:T(self.lid..string.format("Asset arrived at relocation task waypoint ==> Task Done!"))
    
    -- Final zone reached ==> task done.
    self:TaskDone(task)    

  elseif task and task.dcstask.id==AUFTRAG.SpecialTask.REARMING then

    ---
    -- SPECIAL TASK: Rearming Mission
    ---

    -- Debug info.
    self:T(self.lid..string.format("FF Rearming Mission ==> Rearm()"))
    
    -- Call rearm event.
    self:Rearm()

  else

    ---
    -- No special task active
    ---

    -- Apply tasks of this waypoint.
    local ntasks=self:_SetWaypointTasks(Waypoint)

    -- Get waypoint index.
    local wpindex=self:GetWaypointIndex(Waypoint.uid)

    -- Final waypoint reached?
    if wpindex==nil or wpindex==#self.waypoints then

      -- Ad infinitum and not mission waypoint?
      if self.adinfinitum then
        ---
        -- Ad Infinitum
        ---

        if Waypoint.missionUID then
          ---
          -- Last waypoint was a mission waypoint ==> Do nothing (when mission is over, it should take care of this)
          ---
        else

          ---
          -- Last waypoint reached.
          ---

          if #self.waypoints<=1 then
            -- Only one waypoint. Ad infinitum does not really make sense. However, another waypoint could be added later...
            self:_PassedFinalWaypoint(true, "PassingWaypoint: adinfinitum but only ONE WAYPOINT left")
          else

            --[[ Solved now!

            -- Looks like the passing waypoint function is triggered over and over again if the group is near the final waypoint.
            -- So the only good solution is to guide the group away from that waypoint and then update the route.

            -- Get first waypoint.
            local wp1=self:GetWaypointByIndex(1)

            -- Get a waypoint
            local Coordinate=Waypoint.coordinate:GetIntermediateCoordinate(wp1.coordinate, 0.1)
            
            local formation=nil
            if self.isArmygroup then
              formation=ENUMS.Formation.Vehicle.OffRoad
            end
                                    
            self:Detour(Coordinate, self.speedCruise, formation, true)
            
            ]]
            
                  
            -- Send 
            self:__UpdateRoute(-0.01, 1, 1)

          end
        end
      else
        ---
        -- NOT Ad Infinitum
        ---

        -- Final waypoint reached.
        self:_PassedFinalWaypoint(true, "PassingWaypoint: wpindex=#self.waypoints (or wpindex=nil)")
      end
      
    elseif wpindex==1 then

      -- Ad infinitum and not mission waypoint?
      if self.adinfinitum then
        ---
        -- Ad Infinitum
        ---

        if #self.waypoints<=1 then
          -- Only one waypoint. Ad infinitum does not really make sense. However, another waypoint could be added later...
          self:_PassedFinalWaypoint(true, "PassingWaypoint: adinfinitum but only ONE WAYPOINT left")
          
        else
        
          if not Waypoint.missionUID then
            -- Redo the route until the end.
            self:__UpdateRoute(-0.01, 2)
          end
        end
      end

    end

    -- Passing mission waypoint?
    local isEgress=false
    if Waypoint.missionUID then

      -- Debug info.
      self:T2(self.lid..string.format("Passing mission waypoint UID=%s", tostring(Waypoint.uid)))

      -- Get the mission.
      local mission=self:GetMissionByID(Waypoint.missionUID)

      -- Check if this was an Egress waypoint of the mission. If so, call Mission Done! This will call CheckGroupDone.
      local EgressUID=mission and mission:GetGroupEgressWaypointUID(self) or nil
      isEgress=EgressUID and Waypoint.uid==EgressUID
      if isEgress and mission:GetGroupStatus(self)~=AUFTRAG.GroupStatus.DONE then
        self:MissionDone(mission)
      end
    end

    -- Check if all tasks/mission are done?
    -- Note, we delay it for a second to let the OnAfterPassingwaypoint function to be executed in case someone wants to add another waypoint there.
    if ntasks==0 and self:HasPassedFinalWaypoint() and not isEgress then
      self:_CheckGroupDone(0.01)
    end

    -- Debug info.
    local text=string.format("Group passed waypoint %s/%d ID=%d: final=%s detour=%s astar=%s",
    tostring(wpindex), #self.waypoints, Waypoint.uid, tostring(self.passedfinalwp), tostring(Waypoint.detour), tostring(Waypoint.astar))
    self:T(self.lid..text)

  end
  
  -- Set expected speed.
  local wpnext=self:GetWaypointNext()
  if wpnext then
    self.speedWp=wpnext.speed
    self:T(self.lid..string.format("Expected/waypoint speed=%.1f m/s", self.speedWp))
  end

end

--- Set tasks at this waypoint
-- @param #OPSGROUP self
-- @param #OPSGROUP.Waypoint Waypoint The waypoint.
-- @return #number Number of tasks.
function OPSGROUP:_SetWaypointTasks(Waypoint)

  -- Get all waypoint tasks.
  local tasks=self:GetTasksWaypoint(Waypoint.uid)

  -- Debug info.
  local text=string.format("WP uid=%d tasks:", Waypoint.uid)
  local missiontask=nil --Ops.OpsGroup#OPSGROUP.Task
  if #tasks>0 then
    for i,_task in pairs(tasks) do
      local task=_task --#OPSGROUP.Task
      text=text..string.format("\n[%d] %s", i, task.description)
      if task.ismission then
        missiontask=task
      end
    end
  else
    text=text.." None"
  end
  self:T(self.lid..text)

  -- Check if there is mission task
  if missiontask then
    self:T(self.lid.."Executing mission task")
    local mission=self:GetMissionByTaskID(missiontask.id)
    if mission then
      if mission.opstransport and not mission.opstransport:IsCargoDelivered(self.groupname) then
        self:PauseMission()
        return
      end
    end
    self:TaskExecute(missiontask)
    return 1
  end

  -- TODO: maybe set waypoint enroute tasks?

  -- Tasks at this waypoints.
  local taskswp={}

  for _,task in pairs(tasks) do
    local Task=task --Ops.OpsGroup#OPSGROUP.Task

    -- Task execute.
    table.insert(taskswp, self.group:TaskFunction("OPSGROUP._TaskExecute", self, Task))

    -- Stop condition if userflag is set to 1 or task duration over.
    local TaskCondition=self.group:TaskCondition(nil, Task.stopflag:GetName(), 1, nil, Task.duration)

    -- Controlled task.
    table.insert(taskswp, self.group:TaskControlled(Task.dcstask, TaskCondition))

    -- Task done.
    table.insert(taskswp, self.group:TaskFunction("OPSGROUP._TaskDone", self, Task))

  end

  -- Execute waypoint tasks.
  if #taskswp>0 then
    self:SetTask(self.group:TaskCombo(taskswp))
  end

  return #taskswp
end

--- On after "PassedFinalWaypoint" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterPassedFinalWaypoint(From, Event, To)
  self:T(self.lid..string.format("Group passed final waypoint"))

  -- Check if group is done? No tasks mission running.
  --self:_CheckGroupDone()

end

--- On after "GotoWaypoint" event. Group will got to the given waypoint and execute its route from there.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number UID The goto waypoint unique ID.
-- @param #number Speed (Optional) Speed to waypoint in knots.
function OPSGROUP:onafterGotoWaypoint(From, Event, To, UID, Speed)

  local n=self:GetWaypointIndex(UID)

  if n then

    -- Speed to waypoint.
    Speed=Speed or self:GetSpeedToWaypoint(n)

    -- Debug message
    self:T(self.lid..string.format("Goto Waypoint UID=%d index=%d from %d at speed %.1f knots", UID, n, self.currentwp, Speed))

    -- Update the route.
    self:__UpdateRoute(0.1, n, nil, Speed)

  end

end

--- On after "DetectedUnit" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Unit#UNIT Unit The detected unit.
function OPSGROUP:onafterDetectedUnit(From, Event, To, Unit)

  -- Get unit name.
  local unitname=Unit and Unit:GetName() or "unknown"

  -- Debug.
  self:T2(self.lid..string.format("Detected unit %s", unitname))

  if self.detectedunits:FindUnit(unitname) then
    -- Unit is already in the detected unit set ==> Trigger "DetectedUnitKnown" event.
    self:DetectedUnitKnown(Unit)
  else
    -- Unit is was not detected ==> Trigger "DetectedUnitNew" event.
    self:DetectedUnitNew(Unit)
  end

end

--- On after "DetectedUnitNew" event. Add newly detected unit to detected unit set.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Unit#UNIT Unit The detected unit.
function OPSGROUP:onafterDetectedUnitNew(From, Event, To, Unit)

  -- Debug info.
  self:T(self.lid..string.format("Detected New unit %s", Unit:GetName()))

  -- Add unit to detected unit set.
  self.detectedunits:AddUnit(Unit)
end

--- On after "DetectedGroup" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Group#GROUP Group The detected Group.
function OPSGROUP:onafterDetectedGroup(From, Event, To, Group)

  -- Get group name.
  local groupname=Group and Group:GetName() or "unknown"

  -- Debug info.
  self:T(self.lid..string.format("Detected group %s", groupname))

  if self.detectedgroups:FindGroup(groupname) then
    -- Group is already in the detected set ==> Trigger "DetectedGroupKnown" event.
    self:DetectedGroupKnown(Group)
  else
    -- Group is was not detected ==> Trigger "DetectedGroupNew" event.
    self:DetectedGroupNew(Group)
  end

end

--- On after "DetectedGroupNew" event. Add newly detected group to detected group set.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Group#GROUP Group The detected group.
function OPSGROUP:onafterDetectedGroupNew(From, Event, To, Group)

  -- Debug info.
  self:T(self.lid..string.format("Detected New group %s", Group:GetName()))

  -- Add unit to detected unit set.
  self.detectedgroups:AddGroup(Group)
end

--- On after "EnterZone" event. Sets self.inzones[zonename]=true.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE Zone The zone that the group entered.
function OPSGROUP:onafterEnterZone(From, Event, To, Zone)
  local zonename=Zone and Zone:GetName() or "unknown"
  self:T2(self.lid..string.format("Entered Zone %s", zonename))
  self.inzones:Add(Zone:GetName(), Zone)
end

--- On after "LeaveZone" event. Sets self.inzones[zonename]=false.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE Zone The zone that the group entered.
function OPSGROUP:onafterLeaveZone(From, Event, To, Zone)
  local zonename=Zone and Zone:GetName() or "unknown"
  self:T2(self.lid..string.format("Left Zone %s", zonename))
  self.inzones:Remove(zonename, true)
end

--- On before "LaserOn" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Target Target Coordinate. Target can also be any POSITIONABLE from which we can obtain its coordinates.
function OPSGROUP:onbeforeLaserOn(From, Event, To, Target)

  -- Check if LASER is already on.
  if self.spot.On then
    return false
  end

  if Target then

    -- Target specified ==> set target.
    self:SetLaserTarget(Target)

  else
    -- No target specified.
    self:T(self.lid.."ERROR: No target provided for LASER!")
    return false
  end

  -- Get the first element alive.
  local element=self:GetElementAlive()

  if element then

    -- Set element.
    self.spot.element=element

    -- Height offset. No offset for aircraft. We take the height for ground or naval.
    local offsetY=2 --2m for ARMYGROUP, else there might be no LOS
    if self.isFlightgroup or self.isNavygroup then
      offsetY=element.height
    end

    -- Local offset of the LASER source.
    self.spot.offset={x=0, y=offsetY, z=0}

    -- Check LOS.
    if self.spot.CheckLOS then

      -- Check LOS.
      local los=self:HasLoS(self.spot.Coordinate, self.spot.element, self.spot.offset)

      --self:T({los=los, coord=self.spot.Coordinate, offset=self.spot.offset})

      if los then
        self:LaserGotLOS()
      else
        -- Try to switch laser on again in 10 sec.
        self:T(self.lid.."LASER got no LOS currently. Trying to switch the laser on again in 10 sec")
        self:__LaserOn(-10, Target)
        return false
      end

    end

  else
    self:T(self.lid.."ERROR: No element alive for lasing")
    return false
  end

  return true
end

--- On after "LaserOn" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Target Target Coordinate. Target can also be any POSITIONABLE from which we can obtain its coordinates.
function OPSGROUP:onafterLaserOn(From, Event, To, Target)

  -- Start timer that calls the update twice per sec by default.
  if not self.spot.timer:IsRunning() then
    self.spot.timer:Start(nil, self.spot.dt)
  end

  -- Get DCS unit.
  local DCSunit=self.spot.element.unit:GetDCSObject()

  -- Create laser and IR beams.
  self.spot.Laser=Spot.createLaser(DCSunit, self.spot.offset, self.spot.vec3, self.spot.Code or 1688)
  if self.spot.IRon then
    self.spot.IR=Spot.createInfraRed(DCSunit, self.spot.offset, self.spot.vec3)
  end

  -- Laser is on.
  self.spot.On=true

  -- No paused in case it was.
  self.spot.Paused=false

  -- Debug message.
  self:T(self.lid.."Switching LASER on")

end

--- On before "LaserOff" event. Check if LASER is on.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onbeforeLaserOff(From, Event, To)
  return self.spot.On or self.spot.Paused
end

--- On after "LaserOff" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterLaserOff(From, Event, To)

  -- Debug message.
  self:T(self.lid.."Switching LASER off")

  -- "Destroy" the laser beam.
  if self.spot.On then
    self.spot.Laser:destroy()
    self.spot.IR:destroy()

    -- Set to nil.
    self.spot.Laser=nil
    self.spot.IR=nil
  end

  -- Stop update timer.
  self.spot.timer:Stop()

  -- No target unit.
  self.spot.TargetUnit=nil

  -- Laser is off.
  self.spot.On=false

  -- Not paused if it was.
  self.spot.Paused=false
end

--- On after "LaserPause" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterLaserPause(From, Event, To)

  -- Debug message.
  self:T(self.lid.."Switching LASER off temporarily")

  -- "Destroy" the laser beam.
  self.spot.Laser:destroy()
  self.spot.IR:destroy()

  -- Set to nil.
  self.spot.Laser=nil
  self.spot.IR=nil

  -- Laser is off.
  self.spot.On=false

  -- Laser is paused.
  self.spot.Paused=true

end

--- On before "LaserResume" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onbeforeLaserResume(From, Event, To)
  return self.spot.Paused
end

--- On after "LaserResume" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterLaserResume(From, Event, To)

  -- Debug info.
  self:T(self.lid.."Resuming LASER")

  -- Unset paused.
  self.spot.Paused=false

  -- Set target.
  local target=nil
  if self.spot.TargetType==0 then
    target=self.spot.Coordinate
  elseif self.spot.TargetType==1 or self.spot.TargetType==2 then
    target=self.spot.TargetUnit
  elseif self.spot.TargetType==3 then
    target=self.spot.TargetGroup
  end

  -- Switch laser back on.
  if target then

    -- Debug message.
    self:T(self.lid.."Switching LASER on again")

    self:LaserOn(target)
  end

end

--- On after "LaserCode" event. Changes the LASER code.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number Code Laser code. Default is 1688.
function OPSGROUP:onafterLaserCode(From, Event, To, Code)

  -- Default is 1688.
  self.spot.Code=Code or 1688

  -- Debug message.
  self:T2(self.lid..string.format("Setting LASER Code to %d", self.spot.Code))

  if self.spot.On then

    -- Debug info.
    self:T(self.lid..string.format("New LASER Code is %d", self.spot.Code))

    -- Set LASER code.
    self.spot.Laser:setCode(self.spot.Code)
  end

end

--- On after "LaserLostLOS" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterLaserLostLOS(From, Event, To)

  -- No of sight.
  self.spot.LOS=false

  -- Lost line of sight.
  self.spot.lostLOS=true

  if self.spot.On then

    -- Switch laser off.
    self:LaserPause()

  end

end

--- On after "LaserGotLOS" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterLaserGotLOS(From, Event, To)

  -- Has line of sight.
  self.spot.LOS=true

  if self.spot.lostLOS then

    -- Did not loose LOS anymore.
    self.spot.lostLOS=false

    -- Resume laser if currently paused.
    if self.spot.Paused then
      self:LaserResume()
    end

  end

end

--- Set LASER target.
-- @param #OPSGROUP self
-- @param Wrapper.Positionable#POSITIONABLE Target The target to lase. Can also be a COORDINATE object.
function OPSGROUP:SetLaserTarget(Target)

  if Target then

    -- Check object type.
    if Target:IsInstanceOf("SCENERY") then

      -- Scenery as target. Treat it like a coordinate. Set offset to 1 meter above ground.
      self.spot.TargetType=0
      self.spot.offsetTarget={x=0, y=3, z=0}

    elseif Target:IsInstanceOf("POSITIONABLE") then

      local target=Target --Wrapper.Positionable#POSITIONABLE

      if target:IsAlive() then

        if target:IsInstanceOf("GROUP") then
          -- We got a GROUP as target.
          self.spot.TargetGroup=target
          self.spot.TargetUnit=target:GetHighestThreat()
          self.spot.TargetType=3
        else
          -- We got a UNIT or STATIC as target.
          self.spot.TargetUnit=target
          if target:IsInstanceOf("STATIC") then
            self.spot.TargetType=1
          elseif target:IsInstanceOf("UNIT") then
            self.spot.TargetType=2
          end
        end

        -- Get object size.
        local size,x,y,z=self.spot.TargetUnit:GetObjectSize()

        if y then
          self.spot.offsetTarget={x=0, y=y*0.75, z=0}
        else
          self.spot.offsetTarget={x=0, 2, z=0}
        end

      else
        self:T("WARNING: LASER target is not alive!")
        return
      end

    elseif Target:IsInstanceOf("COORDINATE") then

      -- Coordinate as target.
      self.spot.TargetType=0
      self.spot.offsetTarget={x=0, y=0, z=0}

    else
      self:T(self.lid.."ERROR: LASER target should be a POSITIONABLE (GROUP, UNIT or STATIC) or a COORDINATE object!")
      return
    end

    -- Set vec3 and account for target offset.
    self.spot.vec3=UTILS.VecAdd(Target:GetVec3(), self.spot.offsetTarget)

    -- Set coordinate.
    self.spot.Coordinate:UpdateFromVec3(self.spot.vec3)
    
    --self.spot.Coordinate:MarkToAll("Target Laser",ReadOnly,Text)
  end

end

--- Update laser point.
-- @param #OPSGROUP self
function OPSGROUP:_UpdateLaser()

  -- Check if we have a POSITIONABLE to lase.
  if self.spot.TargetUnit then

    ---
    -- Lasing a possibly moving target
    ---

    if self.spot.TargetUnit:IsAlive() then

      -- Get current target position.
      local vec3=self.spot.TargetUnit:GetVec3()

      -- Add target offset.
      vec3=UTILS.VecAdd(vec3, self.spot.offsetTarget)

      -- Calculate distance
      local dist=UTILS.VecDist3D(vec3, self.spot.vec3)

      -- Store current position.
      self.spot.vec3=vec3

      -- Update beam coordinate.
      self.spot.Coordinate:UpdateFromVec3(vec3)

      -- Update laser if target moved more than one meter.
      if dist>1 then

        -- If the laser is ON, set the new laser target point.
        if self.spot.On then
          self.spot.Laser:setPoint(vec3)
          if self.spot.IRon then
            self.spot.IR:setPoint(vec3)
          end
        end

      end

    else

      if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive() then

        -- Get first alive unit in the group.
        local unit=self.spot.TargetGroup:GetHighestThreat()

        if unit then
          self:T(self.lid..string.format("Switching to target unit %s in the group", unit:GetName()))
          self.spot.TargetUnit=unit
          -- We update the laser position in the next update cycle and then check the LOS.
          return
        else
          -- Switch laser off.
          self:T(self.lid.."Target is not alive any more ==> switching LASER off")
          self:LaserOff()
          return
        end

      else

        -- Switch laser off.
        self:T(self.lid.."Target is not alive any more ==> switching LASER off")
        self:LaserOff()
        return
      end

    end
  end

  -- Check LOS.
  if self.spot.CheckLOS then

    -- Check current LOS.
    local los=self:HasLoS(self.spot.Coordinate, self.spot.element, self.spot.offset)

    if los then
      -- Got LOS
      if self.spot.lostLOS then
        --self:T({los=self.spot.LOS, coord=self.spot.Coordinate, offset=self.spot.offset})
        self:LaserGotLOS()
      end

    else
      -- No LOS currently
      if not self.spot.lostLOS then
        self:LaserLostLOS()
      end

    end

  end

end

--- On before "ElementSpawned" event. Check that element is not in status spawned already.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP.Element Element The flight group element.
function OPSGROUP:onbeforeElementSpawned(From, Event, To, Element)

  if Element and Element.status==OPSGROUP.ElementStatus.SPAWNED then
    self:T2(self.lid..string.format("Element %s is already spawned ==> Transition denied!", Element.name))
    return false
  end

  return true
end

--- On after "ElementInUtero" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP.Element Element The flight group element.
function OPSGROUP:onafterElementInUtero(From, Event, To, Element)
  self:T(self.lid..string.format("Element in utero %s", Element.name))

  -- Set element status.
  self:_UpdateStatus(Element, OPSGROUP.ElementStatus.INUTERO)

end

--- On after "ElementDamaged" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP.Element Element The flight group element.
function OPSGROUP:onafterElementDamaged(From, Event, To, Element)
  self:T(self.lid..string.format("Element damaged %s", Element.name))
  
  if Element and (Element.status~=OPSGROUP.ElementStatus.DEAD and Element.status~=OPSGROUP.ElementStatus.INUTERO) then
  
    local lifepoints=0
    
    if Element.DCSunit and Element.DCSunit:isExist() then

      -- Get life of unit
      lifepoints=Element.DCSunit:getLife()
    
      -- Debug output.
      self:T(self.lid..string.format("Element life %s: %.2f/%.2f", Element.name, lifepoints, Element.life0))
        
    else
    
      self:T(self.lid..string.format("Element.DCSunit %s does not exist!", Element.name))
      
    end
    
    if lifepoints<=1.0 then
      self:T(self.lid..string.format("Element %s life %.2f <= 1.0 ==> Destroyed!", Element.name, lifepoints))
      self:ElementDestroyed(Element)
    end
    
  end
    
end

--- On after "ElementHit" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP.Element Element The flight group element.
-- @param Wrapper.Unit#UNIT Enemy Unit that hit the element or `nil`.
function OPSGROUP:onafterElementHit(From, Event, To, Element, Enemy)

  -- Increase element hit counter.
  Element.Nhit=Element.Nhit+1

  -- Debug message.
  self:T(self.lid..string.format("Element hit %s by %s [n=%d, N=%d]", Element.name, Enemy and Enemy:GetName() or "unknown", Element.Nhit, self.Nhit))

  -- Group was hit.
  self:__Hit(-3, Enemy)

end

--- On after "Hit" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Wrapper.Unit#UNIT Enemy Unit that hit the element or `nil`.
function OPSGROUP:onafterHit(From, Event, To, Enemy)
  self:T(self.lid..string.format("Group hit by %s", Enemy and Enemy:GetName() or "unknown"))  
end


--- On after "ElementDestroyed" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP.Element Element The flight group element.
function OPSGROUP:onafterElementDestroyed(From, Event, To, Element)
  self:T(self.lid..string.format("Element destroyed %s", Element.name))

  -- Cancel all missions.
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    mission:ElementDestroyed(self, Element)

  end

  -- Increase counter.
  self.Ndestroyed=self.Ndestroyed+1

  -- Element is dead.
  self:ElementDead(Element)

end

--- On after "ElementDead" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP.Element Element The flight group element.
function OPSGROUP:onafterElementDead(From, Event, To, Element)

  -- Debug info.
  self:T(self.lid..string.format("Element dead %s at t=%.3f", Element.name, timer.getTime()))

  -- Set element status.
  self:_UpdateStatus(Element, OPSGROUP.ElementStatus.DEAD)

  -- Check if element was lasing and if so, switch to another unit alive to lase.
  if self.spot.On and self.spot.element.name==Element.name then

    -- Switch laser off.
    self:LaserOff()

    -- If there is another element alive, switch laser on again.
    if self:GetNelements()>0 then

      -- New target if any.
      local target=nil

      if self.spot.TargetType==0 then
        -- Coordinate
        target=self.spot.Coordinate
      elseif self.spot.TargetType==1 or self.spot.TargetType==2 then
        -- Static or unit
        if self.spot.TargetUnit and self.spot.TargetUnit:IsAlive() then
          target=self.spot.TargetUnit
        end
      elseif self.spot.TargetType==3 then
        -- Group
        if self.spot.TargetGroup and self.spot.TargetGroup:IsAlive() then
          target=self.spot.TargetGroup
        end
      end

      -- Switch laser on again.
      if target then
        self:__LaserOn(-1, target)
      end
    end
  end


  -- Clear cargo bay of element.
  for i=#Element.cargoBay,1,-1 do
    local mycargo=Element.cargoBay[i] --#OPSGROUP.MyCargo
    
    if mycargo.group then

      -- Remove from cargo bay.
      self:_DelCargobay(mycargo.group)
  
      if mycargo.group and not (mycargo.group:IsDead() or mycargo.group:IsStopped()) then
  
        -- Remove my carrier
        mycargo.group:_RemoveMyCarrier()
  
        if mycargo.reserved then
  
          -- This group was not loaded yet ==> Not cargo any more.
          mycargo.group:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO)
  
        else
  
          -- Carrier dead ==> cargo dead.
          for _,cargoelement in pairs(mycargo.group.elements) do
  
            -- Debug info.
            self:T2(self.lid.."Cargo element dead "..cargoelement.name)
  
            -- Trigger dead event.
            mycargo.group:ElementDead(cargoelement)
  
          end
        end
  
      end
      
    else
          
      -- Add cargo to lost.
      if self.cargoTZC then
        for _,_cargo in pairs(self.cargoTZC.Cargos) do
          local cargo=_cargo --#OPSGROUP.CargoGroup          
          if cargo.uid==mycargo.cargoUID then          
            cargo.storage.cargoLost=cargo.storage.cargoLost+mycargo.storageAmount
          end
        end      
      end

      -- Remove cargo from cargo bay.
      self:_DelCargobayElement(Element, mycargo)
    
    end
  end

end

--- On after "Respawn" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #table Template The template used to respawn the group. Default is the inital template of the group.
function OPSGROUP:onafterRespawn(From, Event, To, Template)

  -- Debug info.
  self:T(self.lid.."Respawning group!")

  -- Copy template.
  local template=UTILS.DeepCopy(Template or self.template)

  -- Late activation off.
  template.lateActivation=false

  self:_Respawn(0, template)

end

--- Teleport the group to a different location.
-- @param #OPSGROUP self
-- @param Core.Point#COORDINATE Coordinate Coordinate where the group is teleported to.
-- @param #number Delay Delay in seconds before respawn happens. Default 0.
-- @param #boolean NoPauseMission If `true`, dont pause a running mission.
-- @return #OPSGROUP self
function OPSGROUP:Teleport(Coordinate, Delay, NoPauseMission)
    
  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, OPSGROUP.Teleport, self, Coordinate, 0, NoPauseMission)
  else    

    -- Debug message.
    self:T(self.lid.."FF Teleporting...")
    --Coordinate:MarkToAll("Teleport "..self.groupname)
    
    -- Check if we have a mission running.
    if self:IsOnMission() and not NoPauseMission then
      self:T(self.lid.."Pausing current mission for telport")
      self:PauseMission()
    end

    -- Get copy of template.
    local Template=UTILS.DeepCopy(self.template)  --DCS#Template
    
    -- Set late activation of template to current state.
    Template.lateActivation=self:IsLateActivated()
    
    -- Not uncontrolled.
    Template.uncontrolled=false
    
    -- Set waypoint in air for flighgroups.
    if self:IsFlightgroup() then
      Template.route.points[1]=Coordinate:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, 300, true, nil, nil, "Spawnpoint")
    elseif self:IsArmygroup() then
      Template.route.points[1]=Coordinate:WaypointGround(0)
    elseif self:IsNavygroup() then
      Template.route.points[1]=Coordinate:WaypointNaval(0)
    end

    -- Template units.
    local units=Template.units
    
    -- Table with teleported vectors.
    local d={}
    for i=1,#units do
      local unit=units[i]
      d[i]={x=Coordinate.x+(units[i].x-units[1].x), y=Coordinate.z+units[i].y-units[1].y}
    end    

    for i=#units,1,-1 do
      local unit=units[i]

      -- Get element.      
      local element=self:GetElementByName(unit.name)
      
      if element and element.status~=OPSGROUP.ElementStatus.DEAD then
      
        -- No parking.
        unit.parking=nil
        unit.parking_id=nil
        
        -- Current position.
        local vec3=element.unit:GetVec3()
        
        -- Current heading.
        local heading=element.unit:GetHeading()
        
        -- Set new x,y.
        unit.x=d[i].x
        unit.y=d[i].y
        
        -- Set altitude.
        unit.alt=Coordinate.y
        
        -- Set heading.
        unit.heading=math.rad(heading)
        unit.psi=-unit.heading
      else
        -- Remove unit from spawn template because it is already dead
        table.remove(units, i)
      end
    end

    -- Respawn from new template.  
    self:_Respawn(0, Template, true)
    
  end
end

--- Respawn the group.
-- @param #OPSGROUP self
-- @param #number Delay Delay in seconds before respawn happens. Default 0.
-- @param DCS#Template Template (optional) The template of the Group retrieved with GROUP:GetTemplate(). If the template is not provided, the template will be retrieved of the group itself.
-- @param #boolean Reset Reset waypoints and reinit group if `true`.
-- @return #OPSGROUP self
function OPSGROUP:_Respawn(Delay, Template, Reset)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, OPSGROUP._Respawn, self, 0, Template, Reset)
  else

    -- Debug message.
    self:T2(self.lid.."FF _Respawn")

    -- Given template or get copy of old.
    Template=Template or self:_GetTemplate(true)
    
    -- Number of destroyed units.
    self.Ndestroyed=0
    self.Nhit=0

    -- Check if group is currently alive.
    if self:IsAlive() then

      ---
      -- Group is ALIVE
      ---

      -- Template units.
      local units=Template.units

      for i=#units,1,-1 do
        local unit=units[i]
        
        -- Get the element.
        local element=self:GetElementByName(unit.name)
        
        if element and element.status~=OPSGROUP.ElementStatus.DEAD then
        
          if not Reset then
        
            -- Parking ID.
            unit.parking=element.parking and element.parking.TerminalID or unit.parking
            unit.parking_id=nil
            
            -- Get current position vector.
            local vec3=element.unit:GetVec3()
            
            -- Get heading.
            local heading=element.unit:GetHeading()
            
            -- Set unit position.
            unit.x=vec3.x
            unit.y=vec3.z
            unit.alt=vec3.y
            
            -- Set heading in rad.
            unit.heading=math.rad(heading)
            unit.psi=-unit.heading
            
          end
          
        else
        
          -- Element is dead. Remove from template.
          table.remove(units, i)
          
          self.Ndestroyed=self.Ndestroyed+1
          
        end
      end


      -- Despawn old group. Dont trigger any remove unit event since this is a respawn.
      self:Despawn(0, true)

    end

    -- Ensure elements in utero.
    for _,_element in pairs(self.elements) do
      local element=_element --#OPSGROUP.Element
      if element and element.status~=OPSGROUP.ElementStatus.DEAD then
        self:ElementInUtero(element)
      end
    end    

    -- Spawn with a little delay (especially Navy groups caused problems if they were instantly respawned)
    self:_Spawn(0.01, Template)

  end

  return self
end

--- Spawn group from a given template.
-- @param #OPSGROUP self
-- @param #number Delay Delay in seconds before respawn happens. Default 0.
-- @param DCS#Template Template (optional) The template of the Group retrieved with GROUP:GetTemplate(). If the template is not provided, the template will be retrieved of the group itself.
-- @return #OPSGROUP self
function OPSGROUP:_Spawn(Delay, Template)
  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, OPSGROUP._Spawn, self, 0, Template)
  else
    -- Debug output.
    self:T2({Template=Template})

    if self:IsArmygroup() and self.ValidateAndRepositionGroundUnits then
        UTILS.ValidateAndRepositionGroundUnits(Template.units)
    end

    -- Spawn new group.
    self.group=_DATABASE:Spawn(Template)
    self.group:SetValidateAndRepositionGroundUnits(self.ValidateAndRepositionGroundUnits)
    --local countryID=self.group:GetCountry()
    --local categoryID=self.group:GetCategory()
    --local dcsgroup=coalition.addGroup(countryID, categoryID, Template)

    -- Set DCS group and controller.
    self.dcsgroup=self:GetDCSGroup()
    self.controller=self.dcsgroup:getController()

    -- Set activation and controlled state.
    self.isLateActivated=Template.lateActivation
    self.isUncontrolled=Template.uncontrolled

    -- Not dead or destroyed any more.
    self.isDead=false
    self.isDestroyed=false

    self.groupinitialized=false    
    self.wpcounter=1
    self.currentwp=1

    -- Init waypoints.
    self:_InitWaypoints()

    -- Init Group. This call is delayed because NAVY groups did not like to be initialized just yet (group did not contain any units).
    self:_InitGroup(Template, 0.001)
    
    -- Reset events.
    --self:ResetEvents()  
  end
end

--- On after "InUtero" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterInUtero(From, Event, To)
  self:T(self.lid..string.format("Group inutero at t=%.3f", timer.getTime()))
  --TODO: set element status to inutero
end

--- On after "Damaged" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterDamaged(From, Event, To)
  self:T(self.lid..string.format("Group damaged at t=%.3f", timer.getTime()))
end

--- On after "Destroyed" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterDestroyed(From, Event, To)
  self:T(self.lid..string.format("Group destroyed at t=%.3f", timer.getTime()))
  self.isDestroyed=true
end

--- On before "Dead" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onbeforeDead(From, Event, To)
  if self.Ndestroyed==#self.elements then
    self:Destroyed()
  end
end

--- On after "Dead" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterDead(From, Event, To)

  -- Debug info.
  self:T(self.lid..string.format("Group dead at t=%.3f", timer.getTime()))

  -- Is dead now.
  self.isDead=true

  -- Cancel all missions.
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG

    self:T(self.lid.."Cancelling mission because group is dead! Mission name "..tostring(mission:GetName()))

    self:MissionCancel(mission)
    mission:GroupDead(self)

  end

  -- Delete waypoints so they are re-initialized at the next spawn.
  self:ClearWaypoints()
  self.groupinitialized=false

  -- Set cargo status to NOTCARGO.
  self.cargoStatus=OPSGROUP.CargoStatus.NOTCARGO
  self.carrierStatus=OPSGROUP.CarrierStatus.NOTCARRIER

  -- Remove from cargo bay of carrier.
  local mycarrier=self:_GetMyCarrierGroup()
  if mycarrier and not mycarrier:IsDead() then
    mycarrier:_DelCargobay(self)
    self:_RemoveMyCarrier()
  end

  -- Inform all transports in the queue that this carrier group is dead now.
  for i,_transport in pairs(self.cargoqueue) do
    local transport=_transport --Ops.OpsTransport#OPSTRANSPORT
    transport:__DeadCarrierGroup(1, self)
  end

  -- Cargo queue empty
  self.cargoqueue={}

  -- No current cargo transport.
  self.cargoTransport=nil
  self.cargoTZC=nil

  if self.Ndestroyed==#self.elements then
    if self.cohort then
      -- All elements were destroyed ==> Asset group is gone.
      self.cohort:DelGroup(self.groupname)
    end
  else
    -- Not all assets were destroyed (despawn) ==> Add asset back to legion?
  end
  
  
  if self.legion then
    if not self:IsInUtero() then
    
      -- Get asset.
      local asset=self.legion:GetAssetByName(self.groupname)
      
      -- Get request.
      local request=self.legion:GetRequestByID(asset.rid)
      
      -- Trigger asset dead event.
      self.legion:AssetDead(asset, request)
    end
  
    -- Stop in 5 sec to give possible respawn attempts a chance.  
    self:__Stop(-5)
    
  elseif not self.isAI then
    -- Stop player flights.
    self:__Stop(-1)
  end
    
end

--- On before "Stop" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onbeforeStop(From, Event, To)

  -- We check if
  if self:IsAlive() then
    self:T(self.lid..string.format("WARNING: Group is still alive! Will not stop the FSM. Use :Despawn() instead"))
    return false
  end

  return true
end

--- On after "Stop" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterStop(From, Event, To)

  -- Handle events:
  self:UnHandleEvent(EVENTS.Birth)
  self:UnHandleEvent(EVENTS.Dead)
  self:UnHandleEvent(EVENTS.RemoveUnit)

  -- Handle events:
  if self.isFlightgroup then
    self:UnHandleEvent(EVENTS.EngineStartup)
    self:UnHandleEvent(EVENTS.Takeoff)
    self:UnHandleEvent(EVENTS.Land)
    self:UnHandleEvent(EVENTS.EngineShutdown)
    self:UnHandleEvent(EVENTS.PilotDead)
    self:UnHandleEvent(EVENTS.Ejection)
    self:UnHandleEvent(EVENTS.Crash)
    self.currbase=nil
  elseif self.isArmygroup then
    self:UnHandleEvent(EVENTS.Hit)
  end
  
  for _,_mission in pairs(self.missionqueue) do
    local mission=_mission --Ops.Auftrag#AUFTRAG
    self:MissionCancel(mission)
  end

  -- Stop check timers.
  self.timerCheckZone:Stop()
  self.timerQueueUpdate:Stop()
  self.timerStatus:Stop()

  -- Stop FSM scheduler.
  self.CallScheduler:Clear()
  if self.Scheduler then
    self.Scheduler:Clear()
  end

  -- Flightcontrol.
  if self.flightcontrol then
    for _,_element in pairs(self.elements) do
      local element=_element --#OPSGROUP.Element
      if element.parking then
        self.flightcontrol:SetParkingFree(element.parking)
      end
    end
    self.flightcontrol:_RemoveFlight(self)
  end

  if self:IsAlive() and not (self:IsDead() or self:IsStopped()) then
    local life, life0=self:GetLifePoints()
    local state=self:GetState()
    local text=string.format("WARNING: Group is still alive! Current state=%s. Life points=%d/%d. Use OPSGROUP:Destroy() or OPSGROUP:Despawn() for a clean stop", state, life, life0)
    self:T(self.lid..text)
  end

  -- Remove flight from data base.
  _DATABASE.FLIGHTGROUPS[self.groupname]=nil

  -- Debug output.
  self:T(self.lid.."STOPPED! Unhandled events, cleared scheduler and removed from _DATABASE")
end

--- On after "OutOfAmmo" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterOutOfAmmo(From, Event, To)
  self:T(self.lid..string.format("Group is out of ammo at t=%.3f", timer.getTime()))
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Cargo Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check cargo transport assignments.
-- @param #OPSGROUP self
-- @return #OPSGROUP self
function OPSGROUP:_CheckCargoTransport()

  -- Abs. missin time in seconds.
  local Time=timer.getAbsTime()

  -- Cargo bay debug info.
  if self.verbose>=1 then
    local text=""
    for _,_element in pairs(self.elements) do
      local element=_element --#OPSGROUP.Element
      for _,_cargo in pairs(element.cargoBay) do
        local cargo=_cargo --#OPSGROUP.MyCargo
        if cargo.group then
          text=text..string.format("\n- %s in carrier %s, reserved=%s", tostring(cargo.group:GetName()), tostring(element.name), tostring(cargo.reserved))
        else
          text=text..string.format("\n- storage %s=%d kg in carrier %s [UID=%s]", 
          tostring(cargo.storageType), tostring(cargo.storageAmount*cargo.storageWeight), tostring(element.name), tostring(cargo.cargoUID))
        end
      end
    end
    if text=="" then
      text=" empty"
    end
    self:T(self.lid.."Cargo bay:"..text)
  end

  -- Cargo queue debug info.
  if self.verbose>=3 then
    local text=""
    for i,_transport in pairs(self.cargoqueue) do
      local transport=_transport --Ops.OpsTransport#OPSTRANSPORT
      local pickupzone=transport:GetPickupZone()
      local deployzone=transport:GetDeployZone()
      local pickupname=pickupzone and pickupzone:GetName() or "unknown"
      local deployname=deployzone and deployzone:GetName() or "unknown"
      text=text..string.format("\n[%d] UID=%d Status=%s: %s --> %s", i, transport.uid, transport:GetState(), pickupname, deployname)
      for j,_cargo in pairs(transport:GetCargos()) do
        local cargo=_cargo --#OPSGROUP.CargoGroup
        if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then
          local state=cargo.opsgroup:GetState()
          local status=cargo.opsgroup.cargoStatus
          local name=cargo.opsgroup.groupname
          local carriergroup, carrierelement, reserved=cargo.opsgroup:_GetMyCarrier()
          local carrierGroupname=carriergroup and carriergroup.groupname or "none"
          local carrierElementname=carrierelement and carrierelement.name or "none"
          text=text..string.format("\n  (%d) %s [%s]: %s, carrier=%s(%s), delivered=%s", j, name, state, status, carrierGroupname, carrierElementname, tostring(cargo.delivered))
        else
          --TODO: STORAGE
        end
      end
    end
    if text~="" then
      self:T(self.lid.."Cargo queue:"..text)
    end
  end

  if self.cargoTransport and self.cargoTransport:GetCarrierTransportStatus(self)==OPSTRANSPORT.Status.DELIVERED then
    -- Remove transport from queue.
    self:DelOpsTransport(self.cargoTransport)
    -- No current transport any more.
    self.cargoTransport=nil
    self.cargoTZC=nil
  end

  -- Get current mission (if any).  
  local mission=self:GetMissionCurrent()  

  -- Check if there is anything in the queue.
  if (not self.cargoTransport) and (mission==nil or mission.type==AUFTRAG.Type.NOTHING) then
    self.cargoTransport=self:_GetNextCargoTransport()
    if self.cargoTransport and mission then
      self:MissionCancel(mission)
    end
    if self.cargoTransport and not self:IsActive() then
      self:Activate()
    end
  end

  -- Now handle the transport.
  if self.cargoTransport then

    if self:IsNotCarrier() then

      -- Unset time stamps.
      self.Tpickingup=nil
      self.Tloading=nil
      self.Ttransporting=nil
      self.Tunloading=nil

      -- Get transport zone combo (TZC).
      self.cargoTZC=self.cargoTransport:_GetTransportZoneCombo(self)

      if self.cargoTZC then

        -- Found TZC
        self:T(self.lid..string.format("Not carrier ==> pickup at %s [TZC UID=%d]", self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName() or "unknown", self.cargoTZC.uid))

        -- Initiate the cargo transport process.
        self:__Pickup(-1)

      else
        self:T2(self.lid.."Not carrier ==> No TZC found")
      end

    elseif self:IsPickingup() then

      -- Set time stamp.
      self.Tpickingup=self.Tpickingup or Time

      -- Current pickup time.
      local tpickingup=Time-self.Tpickingup

      -- Debug Info.
      self:T(self.lid..string.format("Picking up at %s [TZC UID=%d] for %s sec...", self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName() or "unknown", self.cargoTZC.uid, tpickingup))

    elseif self:IsLoading() then

      -- Set loading time stamp.
      self.Tloading=self.Tloading or Time

      -- Current pickup time.
      local tloading=Time-self.Tloading

      --TODO: Check max loading time. If exceeded ==> abort transport. Time might depend on required cargos, because we need to give them time to arrive.

      -- Debug info.
      self:T(self.lid..string.format("Loading at %s [TZC UID=%d] for %.1f sec...", self.cargoTZC.PickupZone and self.cargoTZC.PickupZone:GetName() or "unknown", self.cargoTZC.uid, tloading))

      local boarding=false
      local gotcargo=false
      for _,_cargo in pairs(self.cargoTZC.Cargos) do
        local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup
              
        if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then

          -- Check if anyone is still boarding.
          if cargo.opsgroup and cargo.opsgroup:IsBoarding(self.groupname) then
            boarding=true
          end
  
          -- Check if we have any cargo to transport.
          if cargo.opsgroup and cargo.opsgroup:IsLoaded(self.groupname) then
            gotcargo=true
          end
          
        else
        
          -- Get cargo if it is in the cargo bay of any carrier element.
          local mycargo=self:_GetMyCargoBayFromUID(cargo.uid)
          
          if mycargo and mycargo.storageAmount>0 then
            gotcargo=true
          end
          
        end

      end

      -- Boarding finished ==> Transport cargo.
      if gotcargo and self.cargoTransport:_CheckRequiredCargos(self.cargoTZC, self) and not boarding then
        self:T(self.lid.."Boarding/loading finished ==> Loaded")
        self.Tloading=nil
        self:LoadingDone()
      else
        -- No cargo and no one is boarding ==> check again if we can make anyone board.
        self:Loading()
      end

    elseif self:IsTransporting() then

      -- Set time stamp.
      self.Ttransporting=self.Ttransporting or Time

      -- Current pickup time.
      local ttransporting=Time-self.Ttransporting

      -- Debug info.
      self:T(self.lid.."Transporting (nothing to do)")

    elseif self:IsUnloading() then

      -- Set time stamp.
      self.Tunloading=self.Tunloading or Time

      -- Current pickup time.
      local tunloading=Time-self.Tunloading

      -- Debug info.
      self:T(self.lid.."Unloading ==> Checking if all cargo was delivered")

      local delivered=true
      for _,_cargo in pairs(self.cargoTZC.Cargos) do
        local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup
        
        if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then

          local carrierGroup=cargo.opsgroup:_GetMyCarrierGroup()
  
          -- Check that this group is
          if (carrierGroup and carrierGroup:GetName()==self:GetName()) and not cargo.delivered then
            delivered=false
            break
          end
          
        else
          ---
          -- STORAGE
          ---
          
          -- Get cargo if it is in the cargo bay of any carrier element.
          local mycargo=self:_GetMyCargoBayFromUID(cargo.uid)
          
          if mycargo and not cargo.delivered then
            delivered=false
            break
          end
                    
        end

      end

      -- Unloading finished ==> pickup next batch or call it a day.
      if delivered then
        self:T(self.lid.."Unloading finished ==> UnloadingDone")
        self:UnloadingDone()
      else
        self:Unloading()
      end

    end

    -- Debug info. (At this point, we might not have a current cargo transport ==> hence the check)
    if self.verbose>=2 and self.cargoTransport then
      local pickupzone=self.cargoTransport:GetPickupZone(self.cargoTZC)
      local deployzone=self.cargoTransport:GetDeployZone(self.cargoTZC)
      local pickupname=pickupzone and pickupzone:GetName() or "unknown"
      local deployname=deployzone and deployzone:GetName() or "unknown"
      local text=string.format("Carrier [%s]: %s --> %s", self.carrierStatus, pickupname, deployname)
      for _,_cargo in pairs(self.cargoTransport:GetCargos(self.cargoTZC)) do
        local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup
        if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then
          local name=cargo.opsgroup:GetName()
          local gstatus=cargo.opsgroup:GetState()
          local cstatus=cargo.opsgroup.cargoStatus
          local weight=cargo.opsgroup:GetWeightTotal()
          local carriergroup, carrierelement, reserved=cargo.opsgroup:_GetMyCarrier()
          local carrierGroupname=carriergroup and carriergroup.groupname or "none"
          local carrierElementname=carrierelement and carrierelement.name or "none"
          text=text..string.format("\n- %s (%.1f kg) [%s]: %s, carrier=%s (%s), delivered=%s", name, weight, gstatus, cstatus, carrierElementname, carrierGroupname, tostring(cargo.delivered))
        else
          --TODO: Storage
        end
      end
      self:I(self.lid..text)
    end

  end

  return self
end


--- Check if a group is in the cargo bay.
-- @param #OPSGROUP self
-- @param #OPSGROUP OpsGroup Group to check.
-- @return #boolean If `true`, group is in the cargo bay.
function OPSGROUP:_IsInCargobay(OpsGroup)

  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element
    for _,_cargo in pairs(element.cargoBay) do
      local cargo=_cargo --#OPSGROUP.MyCargo
      if cargo.group.groupname==OpsGroup.groupname then
        return true
      end
    end
  end

  return false
end

--- Add OPSGROUP to cargo bay of a carrier.
-- @param #OPSGROUP self
-- @param #OPSGROUP CargoGroup Cargo group.
-- @param #OPSGROUP.Element CarrierElement The element of the carrier.
-- @param #boolean Reserved Only reserve the cargo bay space.
function OPSGROUP:_AddCargobay(CargoGroup, CarrierElement, Reserved)

  --TODO: Check group is not already in cargobay of this carrier or any other carrier.

  local cargo=self:_GetCargobay(CargoGroup)

  if cargo then
    cargo.reserved=Reserved
  else
  
    --cargo=self:_CreateMyCargo(CargoUID, CargoGroup)

    cargo={} --#OPSGROUP.MyCargo
    cargo.group=CargoGroup
    cargo.reserved=Reserved

    table.insert(CarrierElement.cargoBay, cargo)
  end


  -- Set my carrier.
  CargoGroup:_SetMyCarrier(self, CarrierElement, Reserved)

  -- Fill cargo bay (obsolete).
  self.cargoBay[CargoGroup.groupname]=CarrierElement.name

  if not Reserved then

    -- Cargo weight.
    local weight=CargoGroup:GetWeightTotal()

    -- Add weight to carrier.
    self:AddWeightCargo(CarrierElement.name, weight)

  end

  return self
end

--- Add warehouse storage to cargo bay of a carrier.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Element CarrierElement The element of the carrier.
-- @param #number CargoUID UID of the cargo data.
-- @param #string StorageType Storage type.
-- @param #number StorageAmount Storage amount.
-- @param #number StorageWeight Weight of a single storage item in kg.
function OPSGROUP:_AddCargobayStorage(CarrierElement, CargoUID, StorageType, StorageAmount, StorageWeight)

  local MyCargo=self:_CreateMyCargo(CargoUID, nil, StorageType, StorageAmount, StorageWeight)
  
  self:_AddMyCargoBay(MyCargo, CarrierElement)

  
end

--- Add OPSGROUP to cargo bay of a carrier.
-- @param #OPSGROUP self
-- @param #number CargoUID UID of the cargo data.
-- @param #OPSGROUP OpsGroup Cargo group.
-- @param #string StorageType Storage type.
-- @param #number StorageAmount Storage amount.
-- @param #number StorageWeight Weight of a single storage item in kg.
-- @return #OPSGROUP.MyCargo My cargo object.
function OPSGROUP:_CreateMyCargo(CargoUID, OpsGroup, StorageType, StorageAmount, StorageWeight)

  local cargo={} --#OPSGROUP.MyCargo
  
  cargo.cargoUID=CargoUID
  cargo.group=OpsGroup
  cargo.storageType=StorageType
  cargo.storageAmount=StorageAmount
  cargo.storageWeight=StorageWeight
  cargo.reserved=false

  return cargo
end


--- Add storage to cargo bay of a carrier.
-- @param #OPSGROUP self
-- @param #OPSGROUP.MyCargo MyCargo My cargo.
-- @param #OPSGROUP.Element CarrierElement The element of the carrier.
function OPSGROUP:_AddMyCargoBay(MyCargo, CarrierElement)

  table.insert(CarrierElement.cargoBay, MyCargo)

  if not MyCargo.reserved then

    -- Cargo weight.
    local weight=0
    
    if MyCargo.group then
      weight=MyCargo.group:GetWeightTotal()
    else
      weight=MyCargo.storageAmount*MyCargo.storageWeight
    end

    -- Add weight to carrier.
    self:AddWeightCargo(CarrierElement.name, weight)

  end


end

--- Get cargo bay data from a cargo data id.
-- @param #OPSGROUP self
-- @param #number uid Unique ID of cargo data.
-- @return #OPSGROUP.MyCargo Cargo My cargo.
-- @return #OPSGROUP.Element Element that has loaded the cargo.
function OPSGROUP:_GetMyCargoBayFromUID(uid)

  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element
    
    for i,_mycargo in pairs(element.cargoBay) do
      local mycargo=_mycargo --#OPSGROUP.MyCargo
      
      if mycargo.cargoUID and mycargo.cargoUID==uid then
        return mycargo, element, i
      end
    end
  end
  
  return nil, nil, nil
end


--- Get all groups currently loaded as cargo.
-- @param #OPSGROUP self
-- @param #string CarrierName (Optional) Only return cargo groups loaded into a particular carrier unit.
-- @return #table Cargo ops groups.
function OPSGROUP:GetCargoGroups(CarrierName)
  local cargos={}

  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element
    if CarrierName==nil or element.name==CarrierName then
      for _,_cargo in pairs(element.cargoBay) do
        local cargo=_cargo --#OPSGROUP.MyCargo
        if not cargo.reserved then
          table.insert(cargos, cargo.group)
        end
      end
    end
  end

  return cargos
end

--- Get cargo bay item.
-- @param #OPSGROUP self
-- @param #OPSGROUP CargoGroup Cargo group.
-- @return #OPSGROUP.MyCargo Cargo bay item or `nil` if the group is not in the carrier.
-- @return #number CargoBayIndex Index of item in the cargo bay table.
-- @return #OPSGROUP.Element Carrier element.
function OPSGROUP:_GetCargobay(CargoGroup)

  -- Loop over elements and their cargo bay items.
  local CarrierElement=nil  --#OPSGROUP.Element
  local cargobayIndex=nil
  local reserved=nil
  for i,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element
    for j,_cargo in pairs(element.cargoBay) do
      local cargo=_cargo --#OPSGROUP.MyCargo
      if cargo.group and cargo.group.groupname==CargoGroup.groupname then
        return cargo, j, element
      end
    end
  end

  return nil, nil, nil
end

--- Remove OPSGROUP from cargo bay of a carrier.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Element Element Cargo group.
-- @param #number CargoUID Cargo UID.
-- @return #OPSGROUP.MyCargo MyCargo My cargo data.
function OPSGROUP:_GetCargobayElement(Element, CargoUID)
  self:T3({Element=Element, CargoUID=CargoUID})

  for i,_mycargo in pairs(Element.cargoBay) do
    local mycargo=_mycargo --#OPSGROUP.MyCargo

    if mycargo.cargoUID and mycargo.cargoUID==CargoUID then
      return mycargo
    end
    
  end

  return nil
end

--- Remove OPSGROUP from cargo bay of a carrier.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Element Element Cargo group.
-- @param #OPSGROUP.MyCargo MyCargo My cargo data.
-- @return #boolean If `true`, cargo could be removed.
function OPSGROUP:_DelCargobayElement(Element, MyCargo)

  for i,_mycargo in pairs(Element.cargoBay) do
    local mycargo=_mycargo --#OPSGROUP.MyCargo
    
    if mycargo.cargoUID and MyCargo.cargoUID and mycargo.cargoUID==MyCargo.cargoUID then
      if MyCargo.group then
        self:RedWeightCargo(Element.name, MyCargo.group:GetWeightTotal())
      else
        self:RedWeightCargo(Element.name, MyCargo.storageAmount*MyCargo.storageWeight)
      end
      table.remove(Element.cargoBay, i)
      return true
    end
    
  end

  return false
end

--- Remove OPSGROUP from cargo bay of a carrier.
-- @param #OPSGROUP self
-- @param #OPSGROUP CargoGroup Cargo group.
-- @return #boolean If `true`, cargo could be removed.
function OPSGROUP:_DelCargobay(CargoGroup)

  if self.cargoBay[CargoGroup.groupname] then

    -- Not in cargo bay any more.
    self.cargoBay[CargoGroup.groupname]=nil

  end

  -- Get cargo bay info.
  local cargoBayItem, cargoBayIndex, CarrierElement=self:_GetCargobay(CargoGroup)

  if cargoBayItem and cargoBayIndex then

    -- Debug info.
    self:T(self.lid..string.format("Removing cargo group %s from cargo bay (index=%d) of carrier %s", CargoGroup:GetName(), cargoBayIndex, CarrierElement.name))

    -- Remove
    table.remove(CarrierElement.cargoBay, cargoBayIndex)

    -- Reduce weight (if cargo space was not just reserved).
    if not cargoBayItem.reserved then
      local weight=CargoGroup:GetWeightTotal()
      self:RedWeightCargo(CarrierElement.name, weight)
    end

    return true
  end

  self:T(self.lid.."ERROR: Group is not in cargo bay. Cannot remove it!")
  return false
end

--- Get cargo transport from cargo queue.
-- @param #OPSGROUP self
-- @return Ops.OpsTransport#OPSTRANSPORT The next due cargo transport or `nil`.
function OPSGROUP:_GetNextCargoTransport()

  -- Current position.
  local coord=self:GetCoordinate()

  -- Sort results table wrt prio and distance to pickup zone.
  local function _sort(a, b)
    local transportA=a --Ops.OpsTransport#OPSTRANSPORT
    local transportB=b --Ops.OpsTransport#OPSTRANSPORT
    --TODO: Include distance
    --local distA=transportA.pickupzone:GetCoordinate():Get2DDistance(coord)
    --local distB=transportB.pickupzone:GetCoordinate():Get2DDistance(coord)
    return (transportA.prio<transportB.prio) --or (transportA.prio==transportB.prio and distA<distB)
  end
  table.sort(self.cargoqueue, _sort)

  -- TODO: Find smarter next transport.

  -- Importance.
  local vip=math.huge
  for _,_cargotransport in pairs(self.cargoqueue) do
    local cargotransport=_cargotransport --Ops.OpsTransport#OPSTRANSPORT
    if cargotransport.importance and cargotransport.importance<vip then
      vip=cargotransport.importance
    end
  end

  -- Find next transport assignment.
  for _,_cargotransport in pairs(self.cargoqueue) do
    local cargotransport=_cargotransport --Ops.OpsTransport#OPSTRANSPORT

    local carrierstatusScheduled=cargotransport:GetCarrierTransportStatus(self)==OPSTRANSPORT.Status.SCHEDULED

    if cargotransport:IsReadyToGo() and carrierstatusScheduled and (cargotransport.importance==nil or cargotransport.importance<=vip) and not self:_CheckDelivered(cargotransport) then
      cargotransport:Executing()
      cargotransport:SetCarrierTransportStatus(self, OPSTRANSPORT.Status.EXECUTING)
      return cargotransport
    end

  end

  return nil
end

--- Check if all cargo of this transport assignment was delivered.
-- @param #OPSGROUP self
-- @param Ops.OpsTransport#OPSTRANSPORT CargoTransport The next due cargo transport or `nil`.
-- @return #boolean If true, all cargo was delivered.
function OPSGROUP:_CheckDelivered(CargoTransport)

  local done=true
  for _,_cargo in pairs(CargoTransport:GetCargos()) do
    local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup

    if self:CanCargo(cargo) then

      if cargo.delivered then
        -- This one is delivered.
      elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup==nil then
       
      elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and (cargo.opsgroup:IsDead() or cargo.opsgroup:IsStopped()) then
        -- This one is dead.
      else
        done=false --Someone is not done!
      end
      
    end

  end

  -- Debug info.
  self:T(self.lid..string.format("Cargotransport UID=%d Status=%s: delivered=%s", CargoTransport.uid, CargoTransport:GetState(), tostring(done)))

  return done
end


--- Check if all cargo of this transport assignment was delivered.
-- @param #OPSGROUP self
-- @param Ops.OpsTransport#OPSTRANSPORT CargoTransport The next due cargo transport or `nil`.
-- @return #boolean If true, all cargo was delivered.
function OPSGROUP:_CheckGoPickup(CargoTransport)

  local done=true

  if CargoTransport then

    for _,_cargo in pairs(CargoTransport:GetCargos()) do
      local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup

      if self:CanCargo(cargo) then

        if cargo.delivered then
          -- This one is delivered.
        elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and (cargo.opsgroup==nil or cargo.opsgroup:IsDead() or cargo.opsgroup:IsStopped()) then
          -- This one is dead.
        elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and (cargo.opsgroup:IsLoaded(CargoTransport:_GetCarrierNames())) then
          -- This one is loaded into a(nother) carrier.
        else
          done=false --Someone is not done!
        end

      end

    end

    -- Debug info.
    self:T(self.lid..string.format("Cargotransport UID=%d Status=%s: delivered=%s", CargoTransport.uid, CargoTransport:GetState(), tostring(done)))

  end

  return done
end

--- Create a cargo transport assignment.
-- @param #OPSGROUP self
-- @param Ops.OpsTransport#OPSTRANSPORT OpsTransport The troop transport assignment.
-- @return #OPSGROUP self
function OPSGROUP:AddOpsTransport(OpsTransport)

  -- Scheduled.
  OpsTransport:Scheduled()

  -- Add this group as carrier for the transport.
  OpsTransport:_AddCarrier(self)

  --Add to cargo queue
  table.insert(self.cargoqueue, OpsTransport)

  -- Debug message.
  self:T(self.lid.."Adding transport to carrier, #self.cargoqueue="..#self.cargoqueue)

  return self
end

--- Delete a cargo transport assignment from the cargo queue.
-- @param #OPSGROUP self
-- @param Ops.OpsTransport#OPSTRANSPORT CargoTransport Cargo transport do be deleted.
-- @return #OPSGROUP self
function OPSGROUP:DelOpsTransport(CargoTransport)

  for i=#self.cargoqueue,1,-1 do
    local transport=self.cargoqueue[i] --Ops.OpsTransport#OPSTRANSPORT
    if transport.uid==CargoTransport.uid then

      -- Debug info.
      self:T(self.lid..string.format("Removing transport UID=%d", transport.uid))

      -- Remove from queue.
      table.remove(self.cargoqueue, i)

      -- Remove carrier from ops transport.
      CargoTransport:_DelCarrier(self)

      return self
    end
  end

  return self
end

--- Get cargo transport assignment from the cargo queue by its unique ID.
-- @param #OPSGROUP self
-- @param #number uid Unique ID of the transport
-- @return Ops.OpsTransport#OPSTRANSPORT Transport.
function OPSGROUP:GetOpsTransportByUID(uid)

  for i=#self.cargoqueue,1,-1 do
    local transport=self.cargoqueue[i] --Ops.OpsTransport#OPSTRANSPORT
    if transport.uid==uid then
      return transport
    end
  end

  return nil
end


--- Get total weight of the group including cargo. Optionally, the total weight of a specific unit can be requested.
-- @param #OPSGROUP self
-- @param #string UnitName Name of the unit. Default is of the whole group.
-- @param #boolean IncludeReserved If `false`, cargo weight that is only *reserved* is **not** counted. By default (`true` or `nil`), the reserved cargo is included.
-- @return #number Total weight in kg.
function OPSGROUP:GetWeightTotal(UnitName, IncludeReserved)

  local weight=0
  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element

    if (UnitName==nil or UnitName==element.name) and element.status~=OPSGROUP.ElementStatus.DEAD then

      weight=weight+element.weightEmpty

      for _,_cargo in pairs(element.cargoBay) do
        local cargo=_cargo --#OPSGROUP.MyCargo

        local wcargo=0

        -- Count cargo that is not reserved or if reserved cargo should be included.
        if (not cargo.reserved) or (cargo.reserved==true and (IncludeReserved==true or IncludeReserved==nil)) then
          wcargo=cargo.group:GetWeightTotal(element.name)
        end

        weight=weight+wcargo

      end

    end

  end

  return weight
end

--- Get free cargo bay weight.
-- @param #OPSGROUP self
-- @param #string UnitName Name of the unit. Default is of the whole group.
-- @param #boolean IncludeReserved If `false`, cargo weight that is only *reserved* is **not** counted. By default (`true` or `nil`), the reserved cargo is included.
-- @return #number Free cargo bay in kg.
function OPSGROUP:GetFreeCargobay(UnitName, IncludeReserved)

  -- Max cargo weight.
  local weightCargoMax=self:GetWeightCargoMax(UnitName)

  -- Current cargo weight.
  local weightCargo=self:GetWeightCargo(UnitName, IncludeReserved)

  -- Free cargo.
  local Free=weightCargoMax-weightCargo

  -- Debug info.
  self:T(self.lid..string.format("Free cargo bay=%d kg (unit=%s)", Free, (UnitName or "whole group")))

  return Free
end

--- Get relative free cargo bay in percent.
-- @param #OPSGROUP self
-- @param #string UnitName Name of the unit. Default is of the whole group.
-- @param #boolean IncludeReserved If `false`, cargo weight that is only *reserved* is **not** counted. By default (`true` or `nil`), the reserved cargo is included.
-- @return #number Free cargo bay in percent.
function OPSGROUP:GetFreeCargobayRelative(UnitName, IncludeReserved)

  local free=self:GetFreeCargobay(UnitName, IncludeReserved)

  local total=self:GetWeightCargoMax(UnitName)

  local percent=free/total*100

  return percent
end

--- Get relative used (loaded) cargo bay in percent.
-- @param #OPSGROUP self
-- @param #string UnitName Name of the unit. Default is of the whole group.
-- @param #boolean IncludeReserved If `false`, cargo weight that is only *reserved* is **not** counted. By default (`true` or `nil`), the reserved cargo is included.
-- @return #number Used cargo bay in percent.
function OPSGROUP:GetUsedCargobayRelative(UnitName, IncludeReserved)
  local free=self:GetFreeCargobayRelative(UnitName, IncludeReserved)
  return 100-free
end

--- Get max weight of cargo (group) this group can load. This is the largest free cargo bay of any (not dead) element of the group.
-- Optionally, you can calculate the current max weight possible, which accounts for currently loaded cargo.
-- @param #OPSGROUP self
-- @param #boolean Currently If true, calculate the max weight currently possible in case there is already cargo loaded.
-- @return #number Max weight in kg.
function OPSGROUP:GetFreeCargobayMax(Currently)

  local maxweight=0
  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element

    if element.status~=OPSGROUP.ElementStatus.DEAD then

      local weight=element.weightMaxCargo

      if Currently then
        weight=weight-element.weightCargo
      end

      -- Check if this element can load more.
      if weight>maxweight then
        maxweight=weight
      end

    end
  end

  return maxweight
end


--- Get weight of the internal cargo the group is carriing right now.
-- @param #OPSGROUP self
-- @param #string UnitName Name of the unit. Default is of the whole group.
-- @param #boolean IncludeReserved If `false`, cargo weight that is only *reserved* is **not** counted. By default (`true` or `nil`), the reserved cargo is included.
-- @return #number Cargo weight in kg.
function OPSGROUP:GetWeightCargo(UnitName, IncludeReserved)

  -- Calculate weight based on actual cargo weight.
  local weight=0
  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element

    if (UnitName==nil or UnitName==element.name) and element.status~=OPSGROUP.ElementStatus.DEAD then

      weight=weight+element.weightCargo or 0

    end

  end

  -- Calculate weight from stuff in cargo bay. By default this includes the reserved weight if a cargo group was assigned and is currently boarding.
  local gewicht=0
  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element
    if (UnitName==nil or UnitName==element.name) and (element and element.status~=OPSGROUP.ElementStatus.DEAD) then
      for _,_cargo in pairs(element.cargoBay) do
        local cargo=_cargo --#OPSGROUP.MyCargo
        if (not cargo.reserved) or (cargo.reserved==true and (IncludeReserved==true or IncludeReserved==nil)) then
          if cargo.group then
            gewicht=gewicht+cargo.group:GetWeightTotal()
          else
            gewicht=gewicht+cargo.storageAmount*cargo.storageWeight
          end
          --self:I(self.lid..string.format("unit=%s (reserved=%s): cargo=%s weight=%d, total weight=%d", tostring(UnitName), tostring(IncludeReserved), cargo.group:GetName(), cargoweight, weight))
        end
      end
    end
  end

  -- Debug info.
  self:T3(self.lid..string.format("Unit=%s (reserved=%s): weight=%d, gewicht=%d", tostring(UnitName), tostring(IncludeReserved), weight, gewicht))

  -- Quick check.
  if IncludeReserved==false and gewicht~=weight then
    self:T(self.lid..string.format("ERROR: FF weight!=gewicht: weight=%.1f, gewicht=%.1f", weight, gewicht))
  end

  return gewicht
end

--- Get max weight of the internal cargo the group can carry. Optionally, the max cargo weight of a specific unit can be requested.
-- @param #OPSGROUP self
-- @param #string UnitName Name of the unit. Default is of the whole group.
-- @return #number Max cargo weight in kg. This does **not** include any cargo loaded or reserved currently.
function OPSGROUP:GetWeightCargoMax(UnitName)

  local weight=0
  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element

    if (UnitName==nil or UnitName==element.name) and element.status~=OPSGROUP.ElementStatus.DEAD then

      weight=weight+element.weightMaxCargo

    end

  end

  return weight
end

--- Get OPSGROUPs in the cargo bay.
-- @param #OPSGROUP self
-- @return #table Cargo OPSGROUPs.
function OPSGROUP:GetCargoOpsGroups()

  local opsgroups={}
  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element
    for _,_cargo in pairs(element.cargoBay) do
      local cargo=_cargo --#OPSGROUP.MyCargo
      table.insert(opsgroups, cargo.group)
    end
  end

  return opsgroups
end

--- Add weight to the internal cargo of an element of the group.
-- @param #OPSGROUP self
-- @param #string UnitName Name of the unit. Default is of the whole group.
-- @param #number Weight Cargo weight to be added in kg.
function OPSGROUP:AddWeightCargo(UnitName, Weight)

  local element=self:GetElementByName(UnitName)

  if element then --we do not check if the element is actually alive because we need to remove cargo from dead units

    -- Add weight.
    element.weightCargo=element.weightCargo+Weight

    -- Debug info.
    self:T(self.lid..string.format("%s: Adding %.1f kg cargo weight. New cargo weight=%.1f kg", UnitName, Weight, element.weightCargo))

    -- For airborne units, we set the weight in game.
    if self.isFlightgroup and element.unit and element.unit:IsAlive() then -- #2272 trying to deduct cargo weight from possibly dead units
      trigger.action.setUnitInternalCargo(element.name, element.weightCargo)  --https://wiki.hoggitworld.com/view/DCS_func_setUnitInternalCargo
    end

  end

  return self
end

--- Reduce weight to the internal cargo of an element of the group.
-- @param #OPSGROUP self
-- @param #string UnitName Name of the unit.
-- @param #number Weight Cargo weight to be reduced in kg.
function OPSGROUP:RedWeightCargo(UnitName, Weight)

  -- Reduce weight by adding negative weight.
  self:AddWeightCargo(UnitName, -Weight)

  return self
end

--- Get weight of warehouse storage to transport.
-- @param #OPSGROUP self
-- @param Ops.OpsTransport#OPSTRANSPORT.Storage Storage
-- @param #boolean Total Get total weight. Otherweise the amount left to deliver (total-loaded-lost-delivered).
-- @param #boolean Reserved Reduce weight that is reserved.
-- @param #boolean Amount Return amount not weight.
-- @return #number Weight of cargo in kg or amount in number of items, if `Amount=true`.
function OPSGROUP:_GetWeightStorage(Storage, Total, Reserved, Amount)

  local weight=Storage.cargoAmount
  
  if not Total then
    weight=weight-Storage.cargoLost-Storage.cargoLoaded-Storage.cargoDelivered
  end
  
  if Reserved then
    weight=weight-Storage.cargoReserved
  end
  
  if not Amount then
    weight=weight*Storage.cargoWeight
  end

  return weight
end

--- Check if the group can *in principle* be carrier of a cargo group. This checks the max cargo capacity of the group but *not* how much cargo is already loaded (if any).
-- **Note** that the cargo group *cannot* be split into units, i.e. the largest cargo bay of any element of the group must be able to load the whole cargo group in one piece.
-- @param #OPSGROUP self
-- @param Ops.OpsGroup#OPSGROUP.CargoGroup Cargo Cargo data, which needs a carrier.
-- @return #boolean If `true`, there is an element of the group that can load the whole cargo group.
function OPSGROUP:CanCargo(Cargo)

  if Cargo then
  
    local weight=math.huge
    
    if Cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then

      local weight=Cargo.opsgroup:GetWeightTotal()      

      for _,_element in pairs(self.elements) do
        local element=_element --#OPSGROUP.Element
  
        -- Check that element is not dead and has
        if element and element.status~=OPSGROUP.ElementStatus.DEAD and element.weightMaxCargo>=weight then
          return true
        end
      end


    else
    
      ---
      -- STORAGE
      ---
    
      -- Since storage cargo can be devided onto multiple carriers, we take the weight of a single cargo item (even 1 kg of fuel).
      weight=Cargo.storage.cargoWeight
      
    end
    
    -- Calculate cargo bay space.
    local bay=0
    for _,_element in pairs(self.elements) do
      local element=_element --#OPSGROUP.Element

      -- Check that element is not dead and has
      if element and element.status~=OPSGROUP.ElementStatus.DEAD then
        bay=bay+element.weightMaxCargo
      end
      
    end
    
    -- Check if cargo fits into cargo bay(s) of carrier group.
    if bay>=weight then
      return true
    end


  end

  return false
end

--- Find carrier for cargo by evaluating the free cargo bay storage.
-- @param #OPSGROUP self
-- @param #number Weight Weight of cargo in kg.
-- @return #OPSGROUP.Element Carrier able to transport the cargo.
function OPSGROUP:FindCarrierForCargo(Weight)

  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element

    local free=self:GetFreeCargobay(element.name)

    if free>=Weight then
      return element
    else
      self:T3(self.lid..string.format("%s: Weight %d>%d free cargo bay", element.name, Weight, free))
    end

  end

  return nil
end

--- Set my carrier.
-- @param #OPSGROUP self
-- @param #OPSGROUP CarrierGroup Carrier group.
-- @param #OPSGROUP.Element CarrierElement Carrier element.
-- @param #boolean Reserved If `true`, reserve space for me.
function OPSGROUP:_SetMyCarrier(CarrierGroup, CarrierElement, Reserved)

  -- Debug info.
  self:T(self.lid..string.format("Setting My Carrier: %s (%s), reserved=%s", CarrierGroup:GetName(), tostring(CarrierElement.name), tostring(Reserved)))

  self.mycarrier.group=CarrierGroup
  self.mycarrier.element=CarrierElement
  self.mycarrier.reserved=Reserved

  self.cargoTransportUID=CarrierGroup.cargoTransport and CarrierGroup.cargoTransport.uid or nil

end

--- Get my carrier group.
-- @param #OPSGROUP self
-- @return #OPSGROUP Carrier group.
function OPSGROUP:_GetMyCarrierGroup()
  if self.mycarrier and self.mycarrier.group then
    return self.mycarrier.group
  end
  return nil
end

--- Get my carrier element.
-- @param #OPSGROUP self
-- @return #OPSGROUP.Element Carrier element.
function OPSGROUP:_GetMyCarrierElement()
  if self.mycarrier and self.mycarrier.element then
    return self.mycarrier.element
  end
  return nil
end

--- Is my carrier reserved.
-- @param #OPSGROUP self
-- @return #boolean If `true`, space for me was reserved.
function OPSGROUP:_IsMyCarrierReserved()
  if self.mycarrier then
    return self.mycarrier.reserved
  end
  return nil
end



--- Get my carrier.
-- @param #OPSGROUP self
-- @return #OPSGROUP Carrier group.
-- @return #OPSGROUP.Element Carrier element.
-- @return #boolean If `true`, space is reserved for me
function OPSGROUP:_GetMyCarrier()
  return self.mycarrier.group, self.mycarrier.element, self.mycarrier.reserved
end


--- Remove my carrier.
-- @param #OPSGROUP self
-- @return #OPSGROUP self
function OPSGROUP:_RemoveMyCarrier()
  self:T(self.lid..string.format("Removing my carrier!"))
  self.mycarrier.group=nil
  self.mycarrier.element=nil
  self.mycarrier.reserved=nil
  self.mycarrier={}
  self.cargoTransportUID=nil
  return self
end

--- On after "Pickup" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterPickup(From, Event, To)

  -- Old status.
  local oldstatus=self.carrierStatus

  -- Set carrier status.
  self:_NewCarrierStatus(OPSGROUP.CarrierStatus.PICKUP)

  local TZC=self.cargoTZC

  -- Pickup zone.
  local Zone=TZC.PickupZone

  -- Check if already in the pickup zone.
  local inzone=self:IsInZone(Zone)

  -- Pickup at an airbase.
  local airbasePickup=TZC.PickupAirbase --Wrapper.Airbase#AIRBASE

  -- Check if group is already ready for loading.
  local ready4loading=false
  if self:IsArmygroup() or self:IsNavygroup() then

    -- Army and Navy groups just need to be inside the zone.
    ready4loading=inzone

  else

    -- Aircraft is already parking at the pickup airbase.
    ready4loading=self.currbase and airbasePickup and self.currbase:GetName()==airbasePickup:GetName() and self:IsParking()

    -- If a helo is landed in the zone, we also are ready for loading.
    if ready4loading==false and self.isHelo and self:IsLandedAt() and inzone then
      ready4loading=true
    end
  end

  -- Ready for loading?
  if ready4loading then

    -- We are already in the pickup zone ==> wait and initiate loading.
    if (self:IsArmygroup() or self:IsNavygroup()) and not self:IsHolding() then
      self:FullStop()
    end

    -- Start loading.
    self:__Loading(-5)

  else

    -- Set surface type of random coordinate.
    local surfacetypes=nil
    if self:IsArmygroup() or self:IsFlightgroup() then
      surfacetypes={land.SurfaceType.LAND}
    elseif self:IsNavygroup() then
      surfacetypes={land.SurfaceType.WATER}
    end

    -- Get a random coordinate in the pickup zone and let the carrier go there.
    local Coordinate=Zone:GetRandomCoordinate(nil, nil, surfacetypes)

    -- Current waypoint ID.
    local uid=self:GetWaypointCurrentUID()

    -- Add waypoint.
    if self:IsFlightgroup() then

      ---
      -- Flight Group
      ---

      -- Activate uncontrolled group.
      if self:IsParking() and self:IsUncontrolled() then
        self:StartUncontrolled()
      end

      if airbasePickup then

        ---
        -- Pickup at airbase
        ---

        -- Get a (random) pre-defined transport path.
        local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC)

        -- Get transport path.
        if path and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then

          for i=#path.waypoints,1,-1 do
            local wp=path.waypoints[i]
            local coordinate=COORDINATE:NewFromWaypoint(wp)
            local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true
            uid=waypoint.uid
            if i==1 then
              waypoint.temp=false
              waypoint.detour=1 --Needs to trigger the landatairbase function.
            end
          end

        else

          local coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate, 0.5)

          -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone.
          local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), true) ; waypoint.detour=1

        end

      elseif self.isHelo then

        ---
        -- Helo can also land in a zone (NOTE: currently VTOL cannot!)
        ---

        -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone.
        local waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), false) ; waypoint.detour=1

      else
        self:T(self.lid.."ERROR: Transportcarrier aircraft cannot land in Pickup zone! Specify a ZONE_AIRBASE as pickup zone")
      end

      -- Cancel landedAt task. This should trigger Cruise once airborne.
      if self.isHelo and self:IsLandedAt() then
        local Task=self:GetTaskCurrent()
        if Task then
          self:TaskCancel(Task)
        else
          self:T(self.lid.."ERROR: No current task but landed at?!")
        end
      end

      if self:IsWaiting() then
        self:__Cruise(-2)
      end

    elseif self:IsNavygroup() then

      ---
      -- Navy Group
      ---

      -- Get a (random) pre-defined transport path.
      local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC)

      -- Get transport path.
      if path then --and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then
        for i=#path.waypoints,1,-1 do
          local wp=path.waypoints[i]
          local coordinate=COORDINATE:NewFromWaypoint(wp)
          local waypoint=NAVYGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true
          uid=waypoint.uid
        end
      end

      -- NAVYGROUP
      local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, nil, uid, self.altitudeCruise, false) ; waypoint.detour=1

      -- Give cruise command.
      self:__Cruise(-2)


    elseif self:IsArmygroup() then

      ---
      -- Army Group
      ---

      -- Get a (random) pre-defined transport path.
      local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC)

      -- Formation used to go to the pickup zone..
      local Formation=self.cargoTransport:_GetFormationPickup(self.cargoTZC, self)
            
      -- Get transport path.
      if path and oldstatus~=OPSGROUP.CarrierStatus.NOTCARRIER then
        for i=#path.waypoints,1,-1 do
          local wp=path.waypoints[i]
          local coordinate=COORDINATE:NewFromWaypoint(wp)
          local waypoint=ARMYGROUP.AddWaypoint(self, coordinate, nil, uid, wp.action, false) ; waypoint.temp=true
          uid=waypoint.uid
        end
      end

      -- ARMYGROUP
      local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, uid, Formation, false) ; waypoint.detour=1

      -- Give cruise command.
      self:__Cruise(-2, nil, Formation)

    end

  end

end

--- On after "Loading" event.
-- @param #OPSGROUP self
-- @param #table Cargos Table of cargos.
-- @return #table Table of sorted cargos.
function OPSGROUP:_SortCargo(Cargos)

  -- Sort results table wrt descending weight.
  local function _sort(a, b)
    local cargoA=a --Ops.OpsGroup#OPSGROUP.CargoGroup
    local cargoB=b --Ops.OpsGroup#OPSGROUP.CargoGroup
    local weightA=0
    local weightB=0
    if cargoA.opsgroup then
      weightA=cargoA.opsgroup:GetWeightTotal()
    else
      weightA=self:_GetWeightStorage(cargoA.storage)
    end
    if cargoB.opsgroup then
      weightB=cargoB.opsgroup:GetWeightTotal()
    else
      weightB=self:_GetWeightStorage(cargoB.storage)
    end
    return weightA>weightB
  end
  
  table.sort(Cargos, _sort)

  return Cargos
end


--- On after "Loading" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterLoading(From, Event, To)

  -- Set carrier status.
  self:_NewCarrierStatus(OPSGROUP.CarrierStatus.LOADING)

  -- Get valid cargos of the TZC.
  local cargos={}
  for _,_cargo in pairs(self.cargoTZC.Cargos) do
    local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup
    
    -- Check if this group can carry the cargo.
    local canCargo=self:CanCargo(cargo)

    -- Check if this group is currently acting as carrier.
    local isCarrier=false
    
    
    -- Check if cargo is not already cargo.
    local isNotCargo=true 
    
    -- Check if cargo is holding or loaded
    local isHolding=cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and (cargo.opsgroup:IsHolding() or cargo.opsgroup:IsLoaded()) or true
    
    -- Check if cargo is in embark/pickup zone.
    -- Added InUtero here, if embark zone is moving (ship) and cargo has been spawned late activated and its position is not updated. Not sure if that breaks something else!
    local inZone=cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and (cargo.opsgroup:IsInZone(self.cargoTZC.EmbarkZone) or cargo.opsgroup:IsInUtero()) or true
    
    -- Check if cargo is currently on a mission.
    local isOnMission=cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsOnMission() or false
    
    -- Check if current mission is using this ops transport.
    if isOnMission then
      local mission=cargo.opsgroup:GetMissionCurrent()
      if mission and ((mission.opstransport and mission.opstransport.uid==self.cargoTransport.uid) or mission.type==AUFTRAG.Type.NOTHING) then  
        isOnMission=not isHolding
      end
    end
    
    local isAvail=true
    if cargo.type==OPSTRANSPORT.CargoType.STORAGE then
      local nAvail=cargo.storage.storageFrom:GetAmount(cargo.storage.cargoType)
      if nAvail>0 then
        isAvail=true
      else
        isAvail=false
      end
    else
      isCarrier=cargo.opsgroup:IsPickingup() or cargo.opsgroup:IsLoading() or cargo.opsgroup:IsTransporting() or cargo.opsgroup:IsUnloading()
      isNotCargo=cargo.opsgroup:IsNotCargo(true)
    end
    
    local isDead=cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsDead() or false
    
    -- Debug message.
    self:T(self.lid..string.format("Loading: canCargo=%s, isCarrier=%s, isNotCargo=%s, isHolding=%s, isOnMission=%s",
    tostring(canCargo), tostring(isCarrier), tostring(isNotCargo), tostring(isHolding), tostring(isOnMission)))    

    -- TODO: Need a better :IsBusy() function or :IsReadyForMission() :IsReadyForBoarding() :IsReadyForTransport()
    if canCargo and inZone and isNotCargo and isHolding and isAvail and (not (cargo.delivered or isDead or isCarrier or isOnMission)) then
      table.insert(cargos, cargo)
    end
  end

  -- Sort cargos.
  self:_SortCargo(cargos)

  -- Loop over all cargos.
  for _,_cargo in pairs(cargos) do
    local cargo=_cargo --#OPSGROUP.CargoGroup
    
    local weight=nil
    if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then
    
      -- Get total weight of group.
      weight=cargo.opsgroup:GetWeightTotal()
 
       -- Find a carrier for this cargo.
      local carrier=self:FindCarrierForCargo(weight)
  
      -- Order cargo group to board the carrier.
      if carrier then          
        cargo.opsgroup:Board(self, carrier)        
      end
      
    else
    
      ---
      -- STORAGE
      ---

      -- Get weight of cargo that needs to be transported.
      weight=self:_GetWeightStorage(cargo.storage, false)

      -- Get amount that the warehouse currently has.
      local Amount=cargo.storage.storageFrom:GetAmount(cargo.storage.cargoType)
      local Weight=Amount*cargo.storage.cargoWeight
      
      -- Make sure, we do not take more than the warehouse can provide.
      weight=math.min(weight, Weight)
            
      -- Debug info.
      self:T(self.lid..string.format("Loading storage weight=%d kg (warehouse has %d kg)!", weight, Weight))
      
      -- Loop over all elements of the carrier group.
      for _,_element in pairs(self.elements) do
        local element=_element --#OPSGROUP.Element
    
        -- Get the free cargo space of the carrier.
        local free=self:GetFreeCargobay(element.name)
        
        -- Min of weight or bay.
        local w=math.min(weight, free)
        
        -- Check that weight is >0 and also greater that at least one item. We cannot transport half a missile.
        if w>=cargo.storage.cargoWeight then
    
          -- Calculate item amount.
          local amount=math.floor(w/cargo.storage.cargoWeight)
          
          -- Remove items from warehouse.
          cargo.storage.storageFrom:RemoveAmount(cargo.storage.cargoType, amount)
          
          -- Add amount to loaded cargo.
          cargo.storage.cargoLoaded=cargo.storage.cargoLoaded+amount
  
          -- Add cargo to cargo by of element.        
          self:_AddCargobayStorage(element, cargo.uid, cargo.storage.cargoType, amount, cargo.storage.cargoWeight)
                 
          -- Reduce weight for the next element (if any).
          weight=weight-amount*cargo.storage.cargoWeight
          
          -- Debug info.
          local text=string.format("Element %s: loaded amount=%d (weight=%d) ==> left=%d kg", element.name, amount, amount*cargo.storage.cargoWeight, weight)
          self:T(self.lid..text)
          
          -- If no cargo left, break the loop.
          if weight<=0 then
            break
          end
          
        end
        
      end
      
    end    
  end
  
end

--- Set (new) cargo status.
-- @param #OPSGROUP self
-- @param #string Status New status.
function OPSGROUP:_NewCargoStatus(Status)

  -- Debug info.
  if self.verbose>=2 then
    self:I(self.lid..string.format("New cargo status: %s --> %s", tostring(self.cargoStatus), tostring(Status)))
  end

  -- Set cargo status.
  self.cargoStatus=Status

end

--- Set (new) carrier status.
-- @param #OPSGROUP self
-- @param #string Status New status.
function OPSGROUP:_NewCarrierStatus(Status)

  -- Debug info.
  if self.verbose>=2 then
    self:I(self.lid..string.format("New carrier status: %s --> %s", tostring(self.carrierStatus), tostring(Status)))
  end

  -- Set cargo status.
  self.carrierStatus=Status

end

--- Transfer cargo from to another carrier.
-- @param #OPSGROUP self
-- @param #OPSGROUP CargoGroup The cargo group to be transferred.
-- @param #OPSGROUP CarrierGroup The new carrier group.
-- @param #OPSGROUP.Element CarrierElement The new carrier element.
function OPSGROUP:_TransferCargo(CargoGroup, CarrierGroup, CarrierElement)

  -- Debug info.
  self:T(self.lid..string.format("Transferring cargo %s to new carrier group %s", CargoGroup:GetName(), CarrierGroup:GetName()))

  -- Unload from this and directly load into the other carrier.
  self:Unload(CargoGroup)
  CarrierGroup:Load(CargoGroup, CarrierElement)

end

--- On after "Load" event. Carrier loads a cargo group into ints cargo bay.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP CargoGroup The OPSGROUP loaded as cargo.
-- @param #OPSGROUP.Element Carrier The carrier element/unit.
function OPSGROUP:onafterLoad(From, Event, To, CargoGroup, Carrier)

  -- Debug info.
  self:T(self.lid..string.format("Loading group %s", tostring(CargoGroup.groupname)))

  -- Carrier element.
  local carrier=Carrier or CargoGroup:_GetMyCarrierElement() --#OPSGROUP.Element

  -- No carrier provided.
  if not carrier then
    -- Get total weight of group.
    local weight=CargoGroup:GetWeightTotal()
    -- Try to find a carrier manually.
    carrier=self:FindCarrierForCargo(weight)
  end

  if carrier then

    ---
    -- Embark Cargo
    ---

    -- New cargo status.
    CargoGroup:_NewCargoStatus(OPSGROUP.CargoStatus.LOADED)

    -- Clear all waypoints.
    CargoGroup:ClearWaypoints()

    -- Add into carrier bay.
    self:_AddCargobay(CargoGroup, carrier, false)

    -- Despawn this group.
    if CargoGroup:IsAlive() then
      CargoGroup:Despawn(0, true)
    end

    -- Trigger embarked event for cargo group.
    CargoGroup:Embarked(self, carrier)

    -- Trigger Loaded event.
    self:Loaded(CargoGroup)

    -- Trigger "Loaded" event for current cargo transport.
    if self.cargoTransport then
      CargoGroup:_DelMyLift(self.cargoTransport)
      self.cargoTransport:Loaded(CargoGroup, self, carrier)
    else
      self:T(self.lid..string.format("WARNING: Loaded cargo but no current OPSTRANSPORT assignment!"))
    end

  else
    self:T(self.lid.."ERROR: Cargo has no carrier on Load event!")
  end

end

--- On after "LoadingDone" event. Carrier has loaded all (possible) cargo at the pickup zone.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterLoadingDone(From, Event, To)

  -- Debug info.
  self:T(self.lid.."Carrier Loading Done ==> Transport")

  -- Order group to transport.
  self:__Transport(1)

end

--- On before "Transport" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onbeforeTransport(From, Event, To)

  if self.cargoTransport==nil then
    return false
  elseif self.cargoTransport:IsDelivered() then --could be if all cargo was dead on boarding
    return false
  end

  return true
end


--- On after "Transport" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterTransport(From, Event, To)

  -- Set carrier status.
  self:_NewCarrierStatus(OPSGROUP.CarrierStatus.TRANSPORTING)

  --TODO: This is all very similar to the onafterPickup() function. Could make it general.

  -- Deploy zone.
  local Zone=self.cargoTZC.DeployZone

  -- Check if already in deploy zone.
  local inzone=self:IsInZone(Zone)

  -- Deploy airbase (if any).
  local airbaseDeploy=self.cargoTZC.DeployAirbase --Wrapper.Airbase#AIRBASE

  -- Check if group is already ready for loading.
  local ready2deploy=false
  if self:IsArmygroup() or self:IsNavygroup() then
    ready2deploy=inzone
  else
    -- Aircraft is already parking at the pickup airbase.
    ready2deploy=self.currbase and airbaseDeploy and self.currbase:GetName()==airbaseDeploy:GetName() and self:IsParking()

    -- If a helo is landed in the zone, we also are ready for loading.
    if ready2deploy==false and (self.isHelo or self.isVTOL) and self:IsLandedAt() and inzone then
      ready2deploy=true
    end
  end
  
  --env.info(string.format("FF Transport: Zone=%s inzone=%s, ready2deploy=%s", Zone:GetName(), tostring(inzone), tostring(ready2deploy)))

  if inzone then

    -- We are already in the deploy zone ==> wait and initiate unloading.
    if (self:IsArmygroup() or self:IsNavygroup()) and not self:IsHolding() then
      self:FullStop()
    end

    -- Start unloading.
    self:__Unloading(-5)

  else

    local surfacetypes=nil
    if self:IsArmygroup() or self:IsFlightgroup() then
      surfacetypes={land.SurfaceType.LAND}
    elseif self:IsNavygroup() then
      surfacetypes={land.SurfaceType.WATER, land.SurfaceType.SHALLOW_WATER}
    end

    -- Coord where the carrier goes to unload.
    local Coordinate=Zone:GetRandomCoordinate(nil, nil, surfacetypes) --Core.Point#COORDINATE

    -- Current waypoint UID.
    local uid=self:GetWaypointCurrentUID()

    -- Add waypoint.
    if self:IsFlightgroup() then

      -- Activate uncontrolled group.
      if self:IsParking() and self:IsUncontrolled() then
        self:StartUncontrolled()
      end

      if airbaseDeploy then

        ---
        -- Deploy at airbase
        ---

        -- Get a (random) pre-defined transport path.
        local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC)

        -- Get transport path.
        if path then

          for i=1, #path.waypoints do
            local wp=path.waypoints[i]
            local coordinate=COORDINATE:NewFromWaypoint(wp)
            local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true
            uid=waypoint.uid
            if i==#path.waypoints then
              waypoint.temp=false
              waypoint.detour=1 --Needs to trigger the landatairbase function.
            end
          end

        else

          local coordinate=self:GetCoordinate():GetIntermediateCoordinate(Coordinate, 0.5)

          -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone.
          local waypoint=FLIGHTGROUP.AddWaypoint(self, coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), true) ; waypoint.detour=1

        end

      elseif self.isHelo then

        ---
        -- Helo can also land in a zone
        ---

        -- If this is a helo and no ZONE_AIRBASE was given, we make the helo land in the pickup zone.
        local waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate, nil, uid, UTILS.MetersToFeet(self.altitudeCruise), false) ; waypoint.detour=1

      else
        self:T(self.lid.."ERROR: Aircraft (cargo carrier) cannot land in Deploy zone! Specify a ZONE_AIRBASE as deploy zone")
      end

      -- Cancel landedAt task. This should trigger Cruise once airborne.
      if self.isHelo and self:IsLandedAt() then
        local Task=self:GetTaskCurrent()
        if Task then
          self:TaskCancel(Task)
        else
          self:T(self.lid.."ERROR: No current task but landed at?!")
        end
      end

    elseif self:IsArmygroup() then

      -- Get transport path.
      local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC)

      -- Formation used for transporting.
      local Formation=self.cargoTransport:_GetFormationTransport(self.cargoTZC, self)

      -- Get transport path.
      if path then
        for i=1,#path.waypoints do
          local wp=path.waypoints[i]
          local coordinate=COORDINATE:NewFromWaypoint(wp)
          local waypoint=ARMYGROUP.AddWaypoint(self, coordinate, nil, uid, wp.action, false) ; waypoint.temp=true
          uid=waypoint.uid
        end
      end

      -- ARMYGROUP
      local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, uid, Formation, false) ; waypoint.detour=1

      -- Give cruise command.
      self:Cruise(nil, Formation)

    elseif self:IsNavygroup() then

      -- Get a (random) pre-defined transport path.
      local path=self.cargoTransport:_GetPathTransport(self.category, self.cargoTZC)

      -- Get transport path.
      if path then
        for i=1,#path.waypoints do
          local wp=path.waypoints[i]
          local coordinate=COORDINATE:NewFromWaypoint(wp)
          local waypoint=NAVYGROUP.AddWaypoint(self, coordinate, nil, uid, nil, false) ; waypoint.temp=true
          uid=waypoint.uid
        end
      end

      -- NAVYGROUP
      local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate, nil, uid, self.altitudeCruise, false) ; waypoint.detour=1

      -- Give cruise command.
      self:Cruise()

    end

  end

end

--- On after "Unloading" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterUnloading(From, Event, To)

  -- Set carrier status to UNLOADING.
  self:_NewCarrierStatus(OPSGROUP.CarrierStatus.UNLOADING)
  
  self:T(self.lid.."Unloading..")

  -- Deploy zone.
  local zone=self.cargoTZC.DisembarkZone or self.cargoTZC.DeployZone  --Core.Zone#ZONE

  for _,_cargo in pairs(self.cargoTZC.Cargos) do
    local cargo=_cargo --#OPSGROUP.CargoGroup
    
    if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then

      -- Check that cargo is loaded into this group.
      -- NOTE: Could be that the element carriing this cargo group is DEAD, which would mean that the cargo group is also DEAD.
      if cargo.opsgroup:IsLoaded(self.groupname) and not cargo.opsgroup:IsDead() then
  
        -- Disembark to carrier.
        local carrier=nil        --Ops.OpsGroup#OPSGROUP.Element
        local carrierGroup=nil   --Ops.OpsGroup#OPSGROUP
        local disembarkToCarriers=cargo.disembarkCarriers~=nil or self.cargoTZC.disembarkToCarriers
        
        -- Set specifc zone for this cargo.
        if cargo.disembarkZone then
          zone=cargo.disembarkZone
        end
        
        self:T(self.lid..string.format("Unloading cargo %s to zone %s", cargo.opsgroup:GetName(), zone and zone:GetName() or "No Zone Found!"))
  
        -- Try to get the OPSGROUP if deploy zone is a ship.
        if zone and zone:IsInstanceOf("ZONE_AIRBASE") and zone:GetAirbase():IsShip() then
          local shipname=zone:GetAirbase():GetName()
          local ship=UNIT:FindByName(shipname)
          local group=ship:GetGroup()
          carrierGroup=_DATABASE:GetOpsGroup(group:GetName())
          carrier=carrierGroup:GetElementByName(shipname)
        end
  
        if disembarkToCarriers then
        
          -- Debug info.
          self:T(self.lid..string.format("Trying to find disembark carriers in zone %s", zone:GetName()))
          
          -- Disembarkcarriers.
          local disembarkCarriers=cargo.disembarkCarriers or self.cargoTZC.DisembarkCarriers
  
          -- Try to find a carrier that can take the cargo.
          carrier, carrierGroup=self.cargoTransport:FindTransferCarrierForCargo(cargo.opsgroup, zone, disembarkCarriers, self.cargoTZC.DeployAirbase)
  
          --TODO: max unloading time if transfer carrier does not arrive in the zone.
  
        end
  
        if (disembarkToCarriers and carrier and carrierGroup) or (not disembarkToCarriers)  then
  
          -- Cargo was delivered (somehow).
          cargo.delivered=true
  
          -- Increase number of delivered cargos.
          self.cargoTransport.Ndelivered=self.cargoTransport.Ndelivered+1
  
          if carrier and carrierGroup then
  
            ---
            -- Delivered to another carrier group.
            ---
  
            self:_TransferCargo(cargo.opsgroup, carrierGroup, carrier)
  
          elseif zone and zone:IsInstanceOf("ZONE_AIRBASE") and zone:GetAirbase():IsShip() then
  
            ---
            -- Delivered to a ship via helo that landed on its platform
            ---
  
            -- Issue warning.
            self:T(self.lid.."ERROR: Deploy/disembark zone is a ZONE_AIRBASE of a ship! Where to put the cargo? Dumping into the sea, sorry!")
  
            -- Unload but keep "in utero" (no coordinate provided).
            self:Unload(cargo.opsgroup)
  
          else
  
            ---
            -- Delivered to deploy zone
            ---
  
            if self.cargoTransport:GetDisembarkInUtero(self.cargoTZC) then
  
              -- Unload but keep "in utero" (no coordinate provided).
              self:Unload(cargo.opsgroup)
  
            else
  
              -- Get disembark zone of this TZC.
              local DisembarkZone=cargo.disembarkZone or self.cargoTransport:GetDisembarkZone(self.cargoTZC)
  
              local Coordinate=nil
  
  
              if DisembarkZone then
  
                -- Random coordinate in disembark zone.
                Coordinate=DisembarkZone:GetRandomCoordinate()
  
              else
  
                local element=cargo.opsgroup:_GetMyCarrierElement()
  
                if element then
  
                  -- Get random point in disembark zone.
                  local zoneCarrier=self:GetElementZoneUnload(element.name)
  
                  -- Random coordinate/heading in the zone.
                  Coordinate=zoneCarrier:GetRandomCoordinate()
  
                else
                  self:E(self.lid..string.format("ERROR carrier element nil!"))
                end
  
              end
  
              -- Random heading of the group.
              local Heading=math.random(0,359)
  
              -- Activation on/off.
              local activation=self.cargoTransport:GetDisembarkActivation(self.cargoTZC)
              if cargo.disembarkActivation~=nil then
                activation=cargo.disembarkActivation
              end
  
              -- Unload to Coordinate.
              self:Unload(cargo.opsgroup, Coordinate, activation, Heading)
  
            end
  
            -- Trigger "Unloaded" event for current cargo transport
            self.cargoTransport:Unloaded(cargo.opsgroup, self)
  
          end
  
        else
          self:T(self.lid.."Cargo needs carrier but no carrier is avaiable (yet)!")
        end
  
      else
        -- Not loaded or dead
      end
      
    else
      
      ---
      -- STORAGE
      ---
      
      -- TODO: should proabaly move this check to the top to include OPSGROUPS as well?!
      if not cargo.delivered then
      
        for _,_element in pairs(self.elements) do
          local element=_element --#OPSGROUP.Element
          
          -- Get my cargo from cargo bay of element.
          local mycargo=self:_GetCargobayElement(element, cargo.uid)
          
          if mycargo then        
            -- Add cargo to warehouse storage.
            cargo.storage.storageTo:AddAmount(mycargo.storageType, mycargo.storageAmount)
        
            -- Add amount to delivered.    
            cargo.storage.cargoDelivered=cargo.storage.cargoDelivered+mycargo.storageAmount                    
            
            -- Reduce loaded amount.
            cargo.storage.cargoLoaded=cargo.storage.cargoLoaded-mycargo.storageAmount
            
            -- Remove cargo from bay.
            self:_DelCargobayElement(element, mycargo)
            
            -- Debug info
            self:T2(self.lid..string.format("Cargo loaded=%d, delivered=%d, lost=%d", cargo.storage.cargoLoaded, cargo.storage.cargoDelivered, cargo.storage.cargoLost))
            
          end      
        end
        
        -- Get amount that was delivered.
        local amountToDeliver=self:_GetWeightStorage(cargo.storage, false, false, true)
        
        -- Get total amount to be delivered.
        local amountTotal=self:_GetWeightStorage(cargo.storage, true, false, true)
        
        -- Debug info.
        local text=string.format("Amount delivered=%d, total=%d", amountToDeliver, amountTotal)
        self:T(self.lid..text)
        
        if amountToDeliver<=0 then
        
          -- Cargo was delivered (somehow).
          cargo.delivered=true
    
          -- Increase number of delivered cargos.
          self.cargoTransport.Ndelivered=self.cargoTransport.Ndelivered+1
  
          -- Debug info.
          local text=string.format("Ndelivered=%d delivered=%s", self.cargoTransport.Ndelivered, tostring(cargo.delivered))
          self:T(self.lid..text)
          
        end
      end
      
    end

  end -- loop over cargos

end

--- On before "Unload" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP OpsGroup The OPSGROUP loaded as cargo.
-- @param Core.Point#COORDINATE Coordinate Coordinate were the group is unloaded to.
-- @param #number Heading Heading of group.
function OPSGROUP:onbeforeUnload(From, Event, To, OpsGroup, Coordinate, Heading)

  -- Remove group from carrier bay. If group is not in cargo bay, function will return false and transition is denied.
  local removed=self:_DelCargobay(OpsGroup)

  return removed
end

--- On after "Unload" event. Carrier unloads a cargo group from its cargo bay.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP OpsGroup The OPSGROUP loaded as cargo.
-- @param Core.Point#COORDINATE Coordinate Coordinate were the group is unloaded to.
-- @param #boolean Activated If `true`, group is active. If `false`, group is spawned in late activated state.
-- @param #number Heading (Optional) Heading of group in degrees. Default is random heading for each unit.
function OPSGROUP:onafterUnload(From, Event, To, OpsGroup, Coordinate, Activated, Heading)

  -- New cargo status.
  OpsGroup:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO)

  --TODO: Unload flightgroup. Find parking spot etc.

  if Coordinate then

    ---
    -- Respawn at a coordinate.
    ---

    -- Template for the respawned group.
    local Template=UTILS.DeepCopy(OpsGroup.template)  --DCS#Template

    -- No late activation.
    if Activated==false then
      Template.lateActivation=true
    else
      Template.lateActivation=false
    end

    -- Loop over template units.
    for _,Unit in pairs(Template.units) do

      local element=OpsGroup:GetElementByName(Unit.name)

      if element then

        local vec3=element.vec3

        -- Relative pos vector.
        local rvec2={x=Unit.x-Template.x, y=Unit.y-Template.y} --DCS#Vec2

        local cvec2={x=Coordinate.x, y=Coordinate.z} --DCS#Vec2

        -- Position.
        Unit.x=cvec2.x+rvec2.x
        Unit.y=cvec2.y+rvec2.y
        Unit.alt=land.getHeight({x=Unit.x, y=Unit.y})

        -- Heading.
        Unit.heading=Heading and math.rad(Heading) or Unit.heading
        Unit.psi=-Unit.heading

      end

    end

    -- Respawn group.
    OpsGroup:_Respawn(0, Template)

    -- Add current waypoint. These have been cleard on loading.
    if OpsGroup:IsNavygroup() then
      OpsGroup:ClearWaypoints()
      OpsGroup.currentwp=1
      OpsGroup.passedfinalwp=true
      NAVYGROUP.AddWaypoint(OpsGroup, Coordinate, nil, nil, nil, false)
    elseif OpsGroup:IsArmygroup() then
      OpsGroup:ClearWaypoints()
      OpsGroup.currentwp=1
      OpsGroup.passedfinalwp=true
      ARMYGROUP.AddWaypoint(OpsGroup, Coordinate, nil, nil, nil, false)
    end

  else

    ---
    -- Just remove from this carrier.
    ---

    -- Nothing to do.

    OpsGroup.position=self:GetVec3()

  end

  -- Trigger "Disembarked" event.
  OpsGroup:Disembarked(OpsGroup:_GetMyCarrierGroup(), OpsGroup:_GetMyCarrierElement())

  -- Trigger "Unloaded" event.
  self:Unloaded(OpsGroup)

  -- Remove my carrier.
  OpsGroup:_RemoveMyCarrier()

end

--- On after "Unloaded" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier.
function OPSGROUP:onafterUnloaded(From, Event, To, OpsGroupCargo)
  self:T(self.lid..string.format("Unloaded OPSGROUP %s", OpsGroupCargo:GetName()))
  
  if OpsGroupCargo.legion and OpsGroupCargo:IsInZone(OpsGroupCargo.legion.spawnzone) then
    self:T(self.lid..string.format("Unloaded group %s returned to legion", OpsGroupCargo:GetName()))
    OpsGroupCargo:Returned()
  end
  
  -- Check if there is a paused mission.
  local paused=OpsGroupCargo:_CountPausedMissions()>0
   
  if paused then
    OpsGroupCargo:UnpauseMission()
  end
  
end


--- On after "UnloadingDone" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSGROUP:onafterUnloadingDone(From, Event, To)

  -- Debug info
  self:T(self.lid.."Cargo unloading done..")

  -- Cancel landedAt task.
  if self:IsFlightgroup() and self:IsLandedAt() then
    local Task=self:GetTaskCurrent()
    self:__TaskCancel(5, Task)
  end

  -- Check everything was delivered (or is dead).
  local delivered=self:_CheckGoPickup(self.cargoTransport)

  if not delivered then

    -- Get new TZC.
    self.cargoTZC=self.cargoTransport:_GetTransportZoneCombo(self)

    if self.cargoTZC then

      -- Pickup the next batch.
      self:T(self.lid.."Unloaded: Still cargo left ==> Pickup")
      self:Pickup()

    else

      -- Debug info.
      self:T(self.lid..string.format("WARNING: Not all cargo was delivered but could not get a transport zone combo ==> setting carrier state to NOT CARRIER"))

      -- This is not a carrier anymore.
      self:_NewCarrierStatus(OPSGROUP.CarrierStatus.NOTCARRIER)

    end

  else

    -- Everything delivered.
    self:T(self.lid.."Unloaded: ALL cargo unloaded ==> Delivered (current)")
    self:Delivered(self.cargoTransport)

  end

end

--- On after "Delivered" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsTransport#OPSTRANSPORT CargoTransport The cargo transport assignment.
function OPSGROUP:onafterDelivered(From, Event, To, CargoTransport)

  -- Check if this was the current transport.
  if self.cargoTransport and self.cargoTransport.uid==CargoTransport.uid then

    -- Checks
    if self:IsPickingup() then
      -- Delete pickup waypoint?
      local wpindex=self:GetWaypointIndexNext(false)
      if wpindex then
        self:RemoveWaypoint(wpindex)
      end
      -- Remove landing airbase.
      self.isLandingAtAirbase=nil
    elseif self:IsLoading() then
      -- Nothing to do?
    elseif self:IsTransporting() then
      -- This should not happen. Carrier is transporting, how can the cargo be delivered?
    elseif self:IsUnloading() then
      -- Nothing to do?
    end

    -- This is not a carrier anymore.
    self:_NewCarrierStatus(OPSGROUP.CarrierStatus.NOTCARRIER)

    -- Startup uncontrolled aircraft to allow it to go back.
    if self:IsFlightgroup() then

      local function atbase(_airbase)
        local airbase=_airbase --Wrapper.Airbase#AIRBASE
        if airbase and self.currbase then
          if airbase.AirbaseName==self.currbase.AirbaseName then
            return true
          end
        end
        return false
      end

      -- Check if uncontrolled and NOT at destination. If so, start up uncontrolled and let flight return to whereever it wants to go.
      if self:IsUncontrolled() and not atbase(self.destbase) then
        self:StartUncontrolled()
      end
      if self:IsLandedAt() then
        local Task=self:GetTaskCurrent()
        self:TaskCancel(Task)
      end
    else
      -- Army & Navy: give Cruise command to "wake up" from waiting status.
      self:__Cruise(-0.1)
    end

    -- Set carrier transport status.
    self.cargoTransport:SetCarrierTransportStatus(self, OPSTRANSPORT.Status.DELIVERED)

    -- Check group done.
    self:T(self.lid..string.format("All cargo of transport UID=%d delivered ==> check group done in 0.2 sec", self.cargoTransport.uid))
    self:_CheckGroupDone(0.2)


  end

  -- Remove cargo transport from cargo queue.
  --self:DelOpsTransport(CargoTransport)

end

--- On after "TransportCancel" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsTransport#OPSTRANSPORT The transport to be cancelled.
function OPSGROUP:onafterTransportCancel(From, Event, To, Transport)

  if self.cargoTransport and self.cargoTransport.uid==Transport.uid then

    ---
    -- Current Transport
    ---

    -- Debug info.
    self:T(self.lid..string.format("Cancel current transport %d", Transport.uid))

    -- Call delivered=
    local calldelivered=false

    if self:IsPickingup() then

      -- On its way to the pickup zone. Remove waypoint. Will be done in delivered.
      calldelivered=true

    elseif self:IsLoading() then

      -- Handle cargo groups.
      local cargos=Transport:GetCargoOpsGroups(false)

      for _,_opsgroup in pairs(cargos) do
        local opsgroup=_opsgroup --#OPSGROUP

        if opsgroup:IsBoarding(self.groupname) then

          -- Remove boarding waypoint.
          opsgroup:RemoveWaypoint(self.currentwp+1)

          -- Remove from cargo bay (reserved), remove mycarrier, set cargo status.
          self:_DelCargobay(opsgroup)
          opsgroup:_RemoveMyCarrier()
          opsgroup:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO)

        elseif opsgroup:IsLoaded(self.groupname) then

            -- Get random point in disembark zone.
          local zoneCarrier=self:GetElementZoneUnload(opsgroup:_GetMyCarrierElement().name)

            -- Random coordinate/heading in the zone.
          local Coordinate=zoneCarrier and zoneCarrier:GetRandomCoordinate() or self.cargoTransport:GetEmbarkZone(self.cargoTZC):GetRandomCoordinate()

          -- Random heading of the group.
          local Heading=math.random(0,359)

          -- Unload to Coordinate.
          self:Unload(opsgroup, Coordinate, self.cargoTransport:GetDisembarkActivation(self.cargoTZC), Heading)

          -- Trigger "Unloaded" event for current cargo transport
          self.cargoTransport:Unloaded(opsgroup, self)

        end

      end

      -- Call delivered.
      calldelivered=true

    elseif self:IsTransporting() then

      -- Well, we cannot just unload the cargo anywhere.

      -- TODO: Best would be to bring the cargo back to the pickup zone!

    elseif self:IsUnloading() then
      -- Unloading anyway... delivered will be called when done.
    else

    end

    -- Transport delivered.
    if calldelivered then
      self:__Delivered(-2, Transport)
    end

  else

    ---
    -- NOT the current transport
    ---

    -- Set mission group status.
    Transport:SetCarrierTransportStatus(self, AUFTRAG.GroupStatus.CANCELLED)

    -- Remove transport from queue. This also removes the carrier from the transport.
    self:DelOpsTransport(Transport)

    -- Remove carrier.
    --Transport:_DelCarrier(self)

    -- Send group RTB or WAIT if nothing left to do.
    self:_CheckGroupDone(1)

  end

end


---
-- Cargo Group Functions
---

--- On before "Board" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP CarrierGroup The carrier group.
-- @param #OPSGROUP.Element Carrier The OPSGROUP element
function OPSGROUP:onbeforeBoard(From, Event, To, CarrierGroup, Carrier)

  if self:IsDead() then
    self:T(self.lid.."Group DEAD ==> Deny Board transition!")
    return false
  elseif CarrierGroup:IsDead() then
    self:T(self.lid.."Carrier Group DEAD ==> Deny Board transition!")
    self:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO)
    return false
  elseif Carrier.status==OPSGROUP.ElementStatus.DEAD then
    self:T(self.lid.."Carrier Element DEAD ==> Deny Board transition!")
    self:_NewCargoStatus(OPSGROUP.CargoStatus.NOTCARGO)
    return false
  end

  return true
end

--- On after "Board" event.
-- @param #OPSGROUP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #OPSGROUP CarrierGroup The carrier group.
-- @param #OPSGROUP.Element Carrier The OPSGROUP element
function OPSGROUP:onafterBoard(From, Event, To, CarrierGroup, Carrier)

  -- Army or Navy group.
  local CarrierIsArmyOrNavy=CarrierGroup:IsArmygroup() or CarrierGroup:IsNavygroup()
  local CargoIsArmyOrNavy=self:IsArmygroup() or self:IsNavygroup()

  -- Check that carrier is standing still.
  --if (CarrierIsArmyOrNavy and (CarrierGroup:IsHolding() and CarrierGroup:GetVelocity(Carrier.name)<=1)) or (CarrierGroup:IsFlightgroup() and (CarrierGroup:IsParking() or CarrierGroup:IsLandedAt())) then
  if (CarrierIsArmyOrNavy and (CarrierGroup:GetVelocity(Carrier.name)<=1)) or (CarrierGroup:IsFlightgroup() and (CarrierGroup:IsParking() or CarrierGroup:IsLandedAt())) then

    -- Board if group is mobile, not late activated and army or navy. Everything else is loaded directly.
    local board=self.speedMax>0 and CargoIsArmyOrNavy and self:IsAlive() and CarrierGroup:IsAlive()

    -- Armygroup cannot board ship ==> Load directly.
    if self:IsArmygroup() and CarrierGroup:IsNavygroup() then
      board=false
    end

    if self:IsLoaded() then

      -- Debug info.
      self:T(self.lid..string.format("Group is loaded currently ==> Moving directly to new carrier - No Unload(), Disembart() events triggered!"))

      -- Remove old/current carrier.
      self:_RemoveMyCarrier()

      -- Trigger Load event.
      CarrierGroup:Load(self)

    elseif board then

      -- Set cargo status.
      self:_NewCargoStatus(OPSGROUP.CargoStatus.BOARDING)

      -- Debug info.
      self:T(self.lid..string.format("Boarding group=%s [%s], carrier=%s", CarrierGroup:GetName(), CarrierGroup:GetState(), tostring(Carrier.name)))

      -- TODO: Implement embarkzone.
      local Coordinate=Carrier.unit:GetCoordinate()

      -- Clear all waypoints.
      self:ClearWaypoints(self.currentwp+1)

      if self.isArmygroup then
        local waypoint=ARMYGROUP.AddWaypoint(self, Coordinate, nil, nil, ENUMS.Formation.Vehicle.Diamond) ; waypoint.detour=1
        self:Cruise()
      else
        local waypoint=NAVYGROUP.AddWaypoint(self, Coordinate) ; waypoint.detour=1
        self:Cruise()
      end

      -- Set carrier. As long as the group is not loaded, we only reserve the cargo space.
      CarrierGroup:_AddCargobay(self, Carrier, true)

    else

      ---
      -- Direct load into carrier.
      ---

      -- Debug info.
      self:T(self.lid..string.format("Board [loaded=%s] with direct load to carrier group=%s, element=%s", tostring(self:IsLoaded()), CarrierGroup:GetName(), tostring(Carrier.name)))

      -- Get current carrier group.
      local mycarriergroup=self:_GetMyCarrierGroup()
      if mycarriergroup then
        self:T(self.lid..string.format("Current carrier group %s", mycarriergroup:GetName()))
      end
      
      -- Unload cargo first.
      if mycarriergroup and mycarriergroup:GetName()~=CarrierGroup:GetName() then
        -- TODO: Unload triggers other stuff like Disembarked. This can be a problem!
        self:T(self.lid.."Unloading from mycarrier")
        mycarriergroup:Unload(self)
      end

      -- Trigger Load event.
      CarrierGroup:Load(self)
    end

  else

    -- Redo boarding call.
    self:T(self.lid.."Carrier not ready for boarding yet ==> repeating boarding call in 10 sec")
    self:__Board(-10, CarrierGroup, Carrier)

    -- Set carrier. As long as the group is not loaded, we only reserve the cargo space.ï¿½
    CarrierGroup:_AddCargobay(self, Carrier, true)
  end


end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Internal Check Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check if group is in zones.
-- @param #OPSGROUP self
function OPSGROUP:_CheckInZones()

  if self.checkzones and self:IsAlive() then

    local Ncheck=self.checkzones:Count()
    local Ninside=self.inzones:Count()

    -- Debug info.
    self:T(self.lid..string.format("Check if group is in %d zones. Currently it is in %d zones.", self.checkzones:Count(), self.inzones:Count()))

    -- Firstly, check if group is still inside zone it was already in. If not, remove zones and trigger LeaveZone() event.
    local leftzones={}
    for inzonename, inzone in pairs(self.inzones:GetSet()) do

      -- Check if group is still inside the zone.
      local isstillinzone=self.group:IsInZone(inzone) --:IsPartlyOrCompletelyInZone(inzone)

      -- If not, trigger, LeaveZone event.
      if not isstillinzone then
        table.insert(leftzones, inzone)
      end
    end

    -- Trigger leave zone event.
    for _,leftzone in pairs(leftzones) do
      self:LeaveZone(leftzone)
    end


    -- Now, run of all check zones and see if the group entered a  zone.
    local enterzones={}
    for checkzonename,_checkzone in pairs(self.checkzones:GetSet()) do
      local checkzone=_checkzone --Core.Zone#ZONE

      -- Is group currtently in this check zone?
      local isincheckzone=self.group:IsInZone(checkzone) --:IsPartlyOrCompletelyInZone(checkzone)

      if isincheckzone and not self.inzones:_Find(checkzonename) then
        table.insert(enterzones, checkzone)
      end
    end

    -- Trigger enter zone event.
    for _,enterzone in pairs(enterzones) do
      self:EnterZone(enterzone)
    end

  end

end

--- Check detected units.
-- @param #OPSGROUP self
function OPSGROUP:_CheckDetectedUnits()

  if self.detectionOn and self.group and not self:IsDead() then

    -- Get detected DCS units.
    local detectedtargets=self.group:GetDetectedTargets()

    local detected={}
    local groups={}
    for DetectionObjectID, Detection in pairs(detectedtargets or {}) do
      local DetectedObject=Detection.object -- DCS#Object

      if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then

        -- Unit.
        local unit=UNIT:Find(DetectedObject)

        if unit and unit:IsAlive() then

          -- Name of detected unit
          local unitname=unit:GetName()

          -- Add unit to detected table of this run.
          table.insert(detected, unit)

          -- Trigger detected unit event ==> This also triggers the DetectedUnitNew and DetectedUnitKnown events.
          self:DetectedUnit(unit)

          -- Get group of unit.
          local group=unit:GetGroup()

          -- Add group to table.
          if group then
            groups[group:GetName()]=group
          end

        end
      end
    end

    -- Call detected group event.
    for groupname, group in pairs(groups) do
      self:DetectedGroup(group)
    end

    -- Loop over units in detected set.
    local lost={}
    for _,_unit in pairs(self.detectedunits:GetSet()) do
      local unit=_unit --Wrapper.Unit#UNIT

      -- Loop over detected units
      local gotit=false
      for _,_du in pairs(detected) do
        local du=_du --Wrapper.Unit#UNIT
        if unit:GetName()==du:GetName() then
          gotit=true
        end
      end

      if not gotit then
        table.insert(lost, unit:GetName())
        self:DetectedUnitLost(unit)
      end

    end

    -- Remove lost units from detected set.
    self.detectedunits:RemoveUnitsByName(lost)


    -- Loop over groups in detected set.
    local lost={}
    for _,_group in pairs(self.detectedgroups:GetSet()) do
      local group=_group --Wrapper.Group#GROUP

      -- Loop over detected units
      local gotit=false
      for _,_du in pairs(groups) do
        local du=_du --Wrapper.Group#GROUP
        if group:GetName()==du:GetName() then
          gotit=true
        end
      end

      if not gotit then
        table.insert(lost, group:GetName())
        self:DetectedGroupLost(group)
      end

    end

    -- Remove lost units from detected set.
    self.detectedgroups:RemoveGroupsByName(lost)

  end

end

--- Check if passed the final waypoint and, if necessary, update route.
-- @param #OPSGROUP self
-- @param #number delay Delay in seconds.
function OPSGROUP:_CheckGroupDone(delay)

  -- FSM state.
  local fsmstate=self:GetState()

  if self:IsAlive() and self.isAI then

    if delay and delay>0 then
      -- Debug info.
      self:T(self.lid..string.format("Check OPSGROUP done? [state=%s] in %.3f seconds...", fsmstate, delay))

      -- Delayed call.
      self:ScheduleOnce(delay, self._CheckGroupDone, self)
    else

      -- Debug info.
      self:T(self.lid..string.format("Check OSGROUP done? [state=%s]", fsmstate))

      -- Group is engaging something.
      if self:IsEngaging() then
        self:T(self.lid.."Engaging! Group NOT done ==> UpdateRoute()")
        self:UpdateRoute()
        return
      end

      -- Group is returning.
      if self:IsReturning() then
        self:T(self.lid.."Returning! Group NOT done...")
        return
      end

      -- Group is rearming.
      if self:IsRearming() then
        self:T(self.lid.."Rearming! Group NOT done...")
        return
      end

      -- Group is retreating.
      if self:IsRetreating() then
        self:T(self.lid.."Retreating! Group NOT done...")
        return
      end
      
      if self:IsBoarding() then
        self:T(self.lid.."Boarding! Group NOT done...")
        return      
      end
      
      -- Group is waiting. We deny all updates.
      if self:IsWaiting() then
        -- If group is waiting, we assume that is the way it is meant to be.
        self:T(self.lid.."Waiting! Group NOT done...")
        return
      end

      -- Number of tasks remaining.
      local nTasks=self:CountRemainingTasks()

      -- Number of mission remaining.
      local nMissions=self:CountRemainingMissison()

      -- Number of cargo transports remaining.
      local nTransports=self:CountRemainingTransports()
      
      -- Number of paused missions.
      local nPaused=self:_CountPausedMissions()

      -- First check if there is a paused mission and that all remaining missions are paused. If there are other missions in the queue, we will run those.
      if nPaused>0 and nPaused==nMissions then
        local missionpaused=self:_GetPausedMission()
        self:T(self.lid..string.format("Found paused mission %s [%s]. Unpausing mission...", missionpaused.name, missionpaused.type))
        self:UnpauseMission()
        return
      end
      
      -- Number of remaining tasks/missions?
      if nTasks>0 or nMissions>0 or nTransports>0 then
        self:T(self.lid..string.format("Group still has tasks, missions or transports ==> NOT DONE"))
        return
      end      

      -- Get current waypoint.
      local waypoint=self:GetWaypoint(self.currentwp)

      if waypoint then

        -- Number of tasks remaining for this waypoint.
        local ntasks=self:CountTasksWaypoint(waypoint.uid)

        -- We only want to update the route if there are no more tasks to be done.
        if ntasks>0 then
          self:T(self.lid..string.format("Still got %d tasks for the current waypoint UID=%d ==> RETURN (no action)", ntasks, waypoint.uid))
          return
        end
      end
      
      if self.adinfinitum then

        ---
        -- Parol Ad Infinitum
        ---

        if #self.waypoints>0 then

          -- Next waypoint index.
          local i=self:GetWaypointIndexNext(true)

          -- Get positive speed to first waypoint.
          local speed=self:GetSpeedToWaypoint(i)

          -- Cruise.
          self:Cruise(speed)

          -- Debug info.
          self:T(self.lid..string.format("Adinfinitum=TRUE ==> Goto WP index=%d at speed=%d knots", i, speed))

        else
          self:T(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop"))
          self:__FullStop(-1)
        end

      else

        ---
        -- Finite Patrol
        ---

        if self:HasPassedFinalWaypoint() then

          ---
          -- Passed FINAL waypoint
          ---

          if self.legion and self.legionReturn then

            self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE, LEGION set ==> RTZ"))
            if self.isArmygroup then
              self:T2(self.lid.."RTZ to legion spawn zone")
              self:RTZ(self.legion.spawnzone)
            elseif self.isNavygroup then
              self:T2(self.lid.."RTZ to legion port zone")
              self:RTZ(self.legion.portzone)
            end

          else

            -- No further waypoints. Command a full stop.
            self:__FullStop(-1)

            self:T(self.lid..string.format("Passed final WP, adinfinitum=FALSE ==> Full Stop"))

          end

        else

          ---
          -- Final waypoint NOT passed yet
          ---

          if #self.waypoints>0 then
            self:T(self.lid..string.format("NOT Passed final WP, #WP>0 ==> Update Route"))
            self:Cruise()
          else
            self:T(self.lid..string.format("WARNING: No waypoints left! Commanding a Full Stop"))
            self:__FullStop(-1)
          end

        end

      end

    end
  end

end

--- Check if group got stuck.
-- @param #OPSGROUP self
function OPSGROUP:_CheckStuck()

  -- Cases we are not stuck.
  if self:IsHolding() or self:Is("Rearming") or self:IsWaiting() or self:HasPassedFinalWaypoint() then
    return
  end

  -- Current time.
  local Tnow=timer.getTime()

  -- Expected speed in m/s.
  local ExpectedSpeed=self:GetExpectedSpeed()

  -- Current speed in m/s.
  local speed=self:GetVelocity()

  -- Check speed.
  if speed<0.1 then

    if ExpectedSpeed>0 and not self.stuckTimestamp then
      self:T2(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected", speed, ExpectedSpeed))
      self.stuckTimestamp=Tnow
      self.stuckVec3=self:GetVec3()
    end

  else
    -- Moving (again).
    self.stuckTimestamp=nil
  end

  -- Somehow we are not moving...
  if self.stuckTimestamp then

    -- Time we are holding.
    local holdtime=Tnow-self.stuckTimestamp

    if holdtime>=5*60 and holdtime<10*60 then

      -- Debug warning.
      self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime))

      -- Check what is happening.
      if self:IsEngaging() then
        self:__Disengage(1)        
      elseif self:IsReturning() then
        self:T2(self.lid.."RTZ because of stuck")
        self:__RTZ(1)
      else
        self:__Cruise(1)
      end

    elseif holdtime>=10*60 and holdtime<30*60 then

      -- Debug warning.
      self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime))

      --TODO: Stuck event!
      -- Look for a current mission and cancel it as we do not seem to be able to perform it.
      local mission=self:GetMissionCurrent()
      if mission then
        self:T(self.lid..string.format("WARNING: Cancelling mission %s [%s] due to being stuck", mission:GetName(), mission:GetType()))
        self:MissionCancel(mission)
      else
        -- Give cruise command again.
        if self:IsReturning() then
          self:T2(self.lid.."RTZ because of stuck")
          self:__RTZ(1)
        else
          self:__Cruise(1)
        end
      end

    elseif holdtime>=30*60 then

      -- Debug warning.
      self:T(self.lid..string.format("WARNING: Group came to an unexpected standstill. Speed=%.1f<%.1f m/s expected for %d sec", speed, ExpectedSpeed, holdtime))
      
      if self.legion then
        self:T(self.lid..string.format("Asset is returned to its legion after being stuck!"))
        self:ReturnToLegion()
      end

    end

  end

end


--- Check damage.
-- @param #OPSGROUP self
-- @return #OPSGROUP self
function OPSGROUP:_CheckDamage()

  self:T(self.lid..string.format("Checking damage..."))

  self.life=0
  local damaged=false
  
  for _,_element in pairs(self.elements) do
    local element=_element --Ops.OpsGroup#OPSGROUP.Element

    if element.status~=OPSGROUP.ElementStatus.DEAD and element.status~=OPSGROUP.ElementStatus.INUTERO then
  
      -- Current life points.
      local life=element.unit:GetLife()
  
      self.life=self.life+life
  
      if life<element.life then
        element.life=life
        self:ElementDamaged(element)
        damaged=true
      end
    
    end

  end

  -- If anyone in the group was damaged, trigger event.
  if damaged then
    self:Damaged()
  end

  return self
end

--- Check ammo is full.
-- @param #OPSGROUP self
-- @return #boolean If true, ammo is full.
function OPSGROUP:_CheckAmmoFull()

  -- Get current ammo.
  local ammo=self:GetAmmoTot()

  for key,value in pairs(self.ammo) do

    if ammo[key]<value then
      -- At least one type of ammunition is less than when spawned.
      return false
    end

  end

  return true
end

--- Check ammo status.
-- @param #OPSGROUP self
function OPSGROUP:_CheckAmmoStatus()

  -- First check if there was ammo initially.
  if self.ammo.Total>0 then

    -- Get current ammo.
    local ammo=self:GetAmmoTot()

    -- Check if rearming is completed.
    if self:IsRearming() then
      if ammo.Total>=self.ammo.Total then
        self:Rearmed()
      end
    end

    -- Total.
    if self.outofAmmo and ammo.Total>0 then
      self.outofAmmo=false
    end
    if ammo.Total==0 and not self.outofAmmo then
      self.outofAmmo=true
      self:OutOfAmmo()
    end

    -- Guns.
    if self.outofGuns and ammo.Guns>0 then
      self.outofGuns=false
    end
    if ammo.Guns==0 and self.ammo.Guns>0 and not self.outofGuns then
      self.outofGuns=true
      self:OutOfGuns()
    end

    -- Rockets.
    if self.outofRockets and ammo.Rockets>0 then
      self.outofRockets=false
    end
    if ammo.Rockets==0 and self.ammo.Rockets>0 and not self.outofRockets then
      self.outofRockets=true
      self:OutOfRockets()
    end

    -- Bombs.
    if self.outofBombs and ammo.Bombs>0 then
      self.outofBombs=false
    end
    if ammo.Bombs==0 and self.ammo.Bombs>0 and not self.outofBombs then
      self.outofBombs=true
      self:OutOfBombs()
    end

    -- Missiles (All).
    if self.outofMissiles and ammo.Missiles>0 then
      self.outofMissiles=false
    end
    if ammo.Missiles==0 and self.ammo.Missiles>0 and not self.outofMissiles then
      self.outofMissiles=true
      self:OutOfMissiles()
    end

    -- Missiles AA.
    if self.outofMissilesAA and ammo.MissilesAA>0 then
      self.outofMissilesAA=false
    end
    if ammo.MissilesAA==0 and self.ammo.MissilesAA>0 and not self.outofMissilesAA then
      self.outofMissilesAA=true
      self:OutOfMissilesAA()
    end

    -- Missiles AG.
    if self.outofMissilesAG and ammo.MissilesAG>0 then
      self.outofMissilesAG=false
    end
    if ammo.MissilesAG==0 and self.ammo.MissilesAG>0 and not self.outofMissilesAG then
      self.outofMissilesAG=true
      self:OutOfMissilesAG()
    end

    -- Missiles AS.
    if self.outofMissilesAS and ammo.MissilesAS>0 then
      self.outofMissilesAS=false
    end
    if ammo.MissilesAS==0 and self.ammo.MissilesAS>0 and not self.outofMissilesAS then
      self.outofMissilesAS=true
      self:OutOfMissilesAS()
    end

    -- Torpedos.
    if self.outofTorpedos and ammo.Torpedos>0 then
      self.outofTorpedos=false
    end
    if ammo.Torpedos==0 and self.ammo.Torpedos>0 and not self.outofTorpedos then
      self.outofTorpedos=true
      self:OutOfTorpedos()
    end


    -- Check if group is engaging.
    if self:IsEngaging() and ammo.Total==0 then
      self:Disengage()
    end

  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Status Info Common to Air, Land and Sea
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Print info on mission and task status to DCS log file.
-- @param #OPSGROUP self
function OPSGROUP:_PrintTaskAndMissionStatus()

  ---
  -- Tasks: verbose >= 3
  ---

  -- Task queue.
  if self.verbose>=3 and #self.taskqueue>0 then
    local text=string.format("Tasks #%d", #self.taskqueue)
    for i,_task in pairs(self.taskqueue) do
      local task=_task --Ops.OpsGroup#OPSGROUP.Task
      local name=task.description
      local taskid=task.dcstask.id or "unknown"
      local status=task.status
      local clock=UTILS.SecondsToClock(task.time, true)
      local eta=task.time-timer.getAbsTime()
      local started=task.timestamp and UTILS.SecondsToClock(task.timestamp, true) or "N/A"
      local duration=-1
      if task.duration then
        duration=task.duration
        if task.timestamp then
          -- Time the task is running.
          duration=task.duration-(timer.getAbsTime()-task.timestamp)
        else
          -- Time the task is supposed to run.
          duration=task.duration
        end
      end
      -- Output text for element.
      if task.type==OPSGROUP.TaskType.SCHEDULED then
        text=text..string.format("\n[%d] %s (%s): status=%s, scheduled=%s (%d sec), started=%s, duration=%d", i, taskid, name, status, clock, eta, started, duration)
      elseif task.type==OPSGROUP.TaskType.WAYPOINT then
        text=text..string.format("\n[%d] %s (%s): status=%s, waypoint=%d, started=%s, duration=%d, stopflag=%d", i, taskid, name, status, task.waypoint, started, duration, task.stopflag:Get())
      end
    end
    self:I(self.lid..text)
  end

  ---
  -- Missions: verbose>=2
  ---

  -- Current mission name.
  if self.verbose>=2 then
    local Mission=self:GetMissionByID(self.currentmission)

    -- Current status.
    local text=string.format("Missions %d, Current: %s", self:CountRemainingMissison(), Mission and Mission.name or "none")
    for i,_mission in pairs(self.missionqueue) do
      local mission=_mission --Ops.Auftrag#AUFTRAG
      local Cstart= UTILS.SecondsToClock(mission.Tstart, true)
      local Cstop = mission.Tstop and UTILS.SecondsToClock(mission.Tstop, true) or "INF"
      text=text..string.format("\n[%d] %s (%s) status=%s (%s), Time=%s-%s, prio=%d wp=%s targets=%d",
      i, tostring(mission.name), mission.type, mission:GetGroupStatus(self), tostring(mission.status), Cstart, Cstop, mission.prio, tostring(mission:GetGroupWaypointIndex(self)), mission:CountMissionTargets())
    end
    self:I(self.lid..text)
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Waypoints & Routing
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Simple task function. Can be used to call a function which has the warehouse and the executing group as parameters.
-- @param #OPSGROUP self
-- @param #string Function The name of the function to call passed as string.
-- @param #number uid Waypoint UID.
function OPSGROUP:_SimpleTaskFunction(Function, uid)

  -- Task script.
  local DCSScript = {}

  --_DATABASE:FindOpsGroup(groupname)

  DCSScript[#DCSScript+1]   = string.format('local mygroup = _DATABASE:FindOpsGroup(\"%s\") ', self.groupname)  -- The group that executes the task function. Very handy with the "...".
  DCSScript[#DCSScript+1]   = string.format('%s(mygroup, %d)', Function, uid)                                   -- Call the function, e.g. myfunction.(warehouse,mygroup)

  -- Create task.
  local DCSTask=CONTROLLABLE.TaskWrappedAction(self, CONTROLLABLE.CommandDoScript(self, table.concat(DCSScript)))

  return DCSTask
end

--- Enhance waypoint table.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Waypoint Waypoint data.
-- @return #OPSGROUP.Waypoint Modified waypoint data.
function OPSGROUP:_CreateWaypoint(waypoint)

  -- Set uid.
  waypoint.uid=self.wpcounter

  -- Waypoint has not been passed yet.
  waypoint.npassed=0

  -- Coordinate.
  waypoint.coordinate=COORDINATE:New(waypoint.x, waypoint.alt, waypoint.y)

  -- Set waypoint name.
  waypoint.name=string.format("Waypoint UID=%d", waypoint.uid)

  -- Set types.
  waypoint.patrol=false
  waypoint.detour=false
  waypoint.astar=false
  waypoint.temp=false

  -- Tasks of this waypoint
  local taskswp={}

  -- At each waypoint report passing.
  local TaskPassingWaypoint=self:_SimpleTaskFunction("OPSGROUP._PassingWaypoint", waypoint.uid)
  table.insert(taskswp, TaskPassingWaypoint)

  -- Waypoint task combo.
  waypoint.task=self.group:TaskCombo(taskswp)

  -- Increase UID counter.
  self.wpcounter=self.wpcounter+1

  return waypoint
end

--- Initialize Mission Editor waypoints.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Waypoint waypoint Waypoint data.
-- @param #number wpnumber Waypoint index/number. Default is as last waypoint.
function OPSGROUP:_AddWaypoint(waypoint, wpnumber)

  -- Index.
  wpnumber=wpnumber or #self.waypoints+1

  -- Add waypoint to table.
  table.insert(self.waypoints, wpnumber, waypoint)

  -- Debug info.
  self:T(self.lid..string.format("Adding waypoint at index=%d with UID=%d", wpnumber, waypoint.uid))

  -- Now we obviously did not pass the final waypoint.
  if self.currentwp and wpnumber>self.currentwp then
    self:_PassedFinalWaypoint(false, string.format("_AddWaypoint: wpnumber/index %d>%d self.currentwp", wpnumber, self.currentwp))
  end

end

--- Initialize Mission Editor waypoints.
-- @param #OPSGROUP self
-- @param #number WpIndexMin
-- @param #number WpIndexMax
-- @return #OPSGROUP self
function OPSGROUP:_InitWaypoints(WpIndexMin, WpIndexMax)

  -- Waypoints empty!
  self.waypoints={}
  self.waypoints0={}

  -- Get group template
  local template=_DATABASE:GetGroupTemplate(self.groupname)

  if template==nil then
    return self
  end

  -- Template waypoints.
  self.waypoints0=UTILS.DeepCopy(template.route.points) --self.group:GetTemplateRoutePoints()

  WpIndexMin=WpIndexMin or 1
  WpIndexMax=WpIndexMax or #self.waypoints0
  WpIndexMax=math.min(WpIndexMax, #self.waypoints0) --Ensure max is not out of bounce.

  --for index,wp in pairs(self.waypoints0) do

  for i=WpIndexMin,WpIndexMax do

    local wp=self.waypoints0[i] --DCS#Waypoint

    -- Coordinate of the waypoint.
    local Coordinate=COORDINATE:NewFromWaypoint(wp)

    -- Strange!
    wp.speed=wp.speed or 0

    -- Speed at the waypoint.
    local speedknots=UTILS.MpsToKnots(wp.speed)

    -- Expected speed to the first waypoint.
    if i<=2 then
      self.speedWp=wp.speed
      self:T(self.lid..string.format("Expected/waypoint speed=%.1f m/s", self.speedWp))
    end

    -- Speed in knots.
    local Speed=UTILS.MpsToKnots(wp.speed)

    -- Add waypoint.
    local Waypoint=nil
    if self:IsFlightgroup() then
      Waypoint=FLIGHTGROUP.AddWaypoint(self, Coordinate, Speed, nil, Altitude,  false)
    elseif self:IsArmygroup() then
      Waypoint=ARMYGROUP.AddWaypoint(self,   Coordinate, Speed, nil, wp.action, false)
    elseif self:IsNavygroup() then
      Waypoint=NAVYGROUP.AddWaypoint(self,   Coordinate, Speed, nil, Depth,     false)
    end

    -- Get DCS waypoint tasks set in the ME. EXPERIMENTAL!
    local DCStasks=wp.task and wp.task.params.tasks or nil
    if DCStasks and self.useMEtasks then
      for _,DCStask in pairs(DCStasks) do
        -- Wrapped Actions are commands. We do not take those.
        if DCStask.id and DCStask.id~="WrappedAction" then
          self:AddTaskWaypoint(DCStask,Waypoint, "ME Task")
        end
      end
    end

  end

  -- Debug info.
  self:T(self.lid..string.format("Initializing %d waypoints", #self.waypoints))

  -- Flight group specific.
  if self:IsFlightgroup() then

    -- Get home and destination airbases from waypoints.
    self.homebase=self.homebase or self:GetHomebaseFromWaypoints() -- GetHomebaseFromWaypoints() returns carriers or destroyers if no airbase is found.
    local destbase=self:GetDestinationFromWaypoints()
    self.destbase=self.destbase or destbase
    self.currbase=self:GetHomebaseFromWaypoints() -- Skipped To fix RTB issue

    --env.info("FF home base "..(self.homebase and self.homebase:GetName() or "unknown"))
    --env.info("FF dest base "..(self.destbase and self.destbase:GetName() or "unknown"))

    -- Remove the landing waypoint. We use RTB for that. It makes adding new waypoints easier as we do not have to check if the last waypoint is the landing waypoint.
    if destbase and #self.waypoints>1 then
      table.remove(self.waypoints, #self.waypoints)
    end

    -- Set destination to homebase.
    if self.destbase==nil then  -- Skipped To fix RTB issue
      self.destbase=self.homebase
    end

  end

  -- Update route.
  if #self.waypoints>0 then

    -- Check if only 1 wp?
    if #self.waypoints==1 then
      self:_PassedFinalWaypoint(true, "_InitWaypoints: #self.waypoints==1")
    end

  else
    self:T(self.lid.."WARNING: No waypoints initialized. Number of waypoints is 0!")
  end

  return self
end

--- Route group along waypoints.
-- @param #OPSGROUP self
-- @param #table waypoints Table of waypoints.
-- @param #number delay Delay in seconds.
-- @return #OPSGROUP self
function OPSGROUP:Route(waypoints, delay)

  if delay and delay>0 then
    self:ScheduleOnce(delay, OPSGROUP.Route, self, waypoints)
  else

    if self:IsAlive() then

      -- Clear all DCS tasks. NOTE: This can make DCS crash!
      --self:ClearTasks()

      -- DCS mission task.
      local DCSTask = {
        id = 'Mission',
        params = {
          airborne = self:IsFlightgroup(),
          route={points=waypoints},
        },
      }

      -- Set mission task.
      self:SetTask(DCSTask)

    else
      self:T(self.lid.."ERROR: Group is not alive! Cannot route group.")
    end
  end

  return self
end



--- Initialize Mission Editor waypoints.
-- @param #OPSGROUP self
-- @param #number n Waypoint
function OPSGROUP:_UpdateWaypointTasks(n)

  local waypoints=self.waypoints or {}
  local nwaypoints=#waypoints

  for i,_wp in pairs(waypoints) do
    local wp=_wp --Ops.OpsGroup#OPSGROUP.Waypoint

    if i>=n or nwaypoints==1 then

      -- Debug info.
      self:T2(self.lid..string.format("Updating waypoint task for waypoint %d/%d ID=%d. Last waypoint passed %d", i, nwaypoints, wp.uid, self.currentwp))

      -- Tasks of this waypoint
      local taskswp={}

      -- At each waypoint report passing.
      local TaskPassingWaypoint=self.group:TaskFunction("OPSGROUP._PassingWaypoint", self, wp.uid)
      table.insert(taskswp, TaskPassingWaypoint)

      -- Waypoint task combo.
      wp.task=self.group:TaskCombo(taskswp)

    end

  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Global Task Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Function called when a group is passing a waypoint.
--@param #OPSGROUP opsgroup Ops group object.
--@param #number uid Waypoint UID.
function OPSGROUP._PassingWaypoint(opsgroup, uid)

  -- Debug message.
  local text=string.format("Group passing waypoint uid=%d", uid)
  opsgroup:T(opsgroup.lid..text)

  -- Get waypoint data.
  local waypoint=opsgroup:GetWaypointByID(uid)

  if waypoint then

    -- Increase passing counter.
    waypoint.npassed=waypoint.npassed+1

    -- Current wp.
    local currentwp=opsgroup.currentwp

    -- Get the current waypoint index.
    opsgroup.currentwp=opsgroup:GetWaypointIndex(uid)

    local wpistemp=waypoint.temp or waypoint.detour or waypoint.astar

    -- Remove temp waypoints.
    if wpistemp then
      opsgroup:RemoveWaypointByID(uid)
    end

    -- Get next waypoint. Tricky part is that if
    local wpnext=opsgroup:GetWaypointNext()

    if wpnext then --and (opsgroup.currentwp<#opsgroup.waypoints or opsgroup.adinfinitum or wpistemp)

      -- Debug info.
      opsgroup:T(opsgroup.lid..string.format("Next waypoint UID=%d index=%d", wpnext.uid, opsgroup:GetWaypointIndex(wpnext.uid)))

      -- Set formation.
      if opsgroup.isArmygroup then
        opsgroup.option.Formation=wpnext.action
      end

      -- Set speed to next wp.
      opsgroup.speed=wpnext.speed

      if opsgroup.speed<0.01 then
        opsgroup.speed=UTILS.KmphToMps(opsgroup.speedCruise)
      end

    else

      -- Set passed final waypoint.
      opsgroup:_PassedFinalWaypoint(true, "_PassingWaypoint No next Waypoint found")

    end

    -- Check if final waypoint was reached.
    if opsgroup.currentwp==#opsgroup.waypoints and not (opsgroup.adinfinitum or wpistemp) then
      -- Set passed final waypoint.
      opsgroup:_PassedFinalWaypoint(true, "_PassingWaypoint currentwp==#waypoints and NOT adinfinitum and NOT a temporary waypoint")
    end

    -- Trigger PassingWaypoint event.
    if waypoint.temp then

      ---
      -- Temporary Waypoint
      ---

      if (opsgroup:IsNavygroup() or opsgroup:IsArmygroup()) and opsgroup.currentwp==#opsgroup.waypoints then
        --TODO: not sure if this works with FLIGHTGROUPS

        -- Removing this for now.
        opsgroup:Cruise()
      end

    elseif waypoint.astar then

      ---
      -- Pathfinding Waypoint
      ---

      -- Cruise.
      opsgroup:Cruise()

    elseif waypoint.detour then

      ---
      -- Detour Waypoint
      ---

      if opsgroup:IsRearming() then

        -- Trigger Rearming event.
        opsgroup:Rearming()

      elseif opsgroup:IsRetreating() then

        -- Trigger Retreated event.
        opsgroup:Retreated()

      elseif opsgroup:IsReturning() then

        -- Trigger Returned event.
        opsgroup:Returned()

      elseif opsgroup:IsPickingup() then

        if opsgroup:IsFlightgroup() then

          -- Land at current pos and wait for 60 min max.
          if opsgroup.cargoTZC then

            if opsgroup.cargoTZC.PickupAirbase then
              -- Pickup airbase specified. Land there.
              opsgroup:LandAtAirbase(opsgroup.cargoTZC.PickupAirbase)
            else
              -- Land somewhere in the pickup zone. Only helos can do that.
              local coordinate=opsgroup.cargoTZC.PickupZone:GetRandomCoordinate(nil, nil, {land.SurfaceType.LAND})
              opsgroup:LandAt(coordinate, 60*60)
            end

          else
            local coordinate=opsgroup:GetCoordinate()
            opsgroup:LandAt(coordinate, 60*60)
          end


        else

          -- Wait and load cargo.
          opsgroup:FullStop()
          opsgroup:__Loading(-5)

        end

      elseif opsgroup:IsTransporting() then

       if opsgroup:IsFlightgroup() then

          -- Land at current pos and wait for 60 min max.
          if opsgroup.cargoTZC then

            if opsgroup.cargoTZC.DeployAirbase then
              -- Deploy airbase specified. Land there.
              opsgroup:LandAtAirbase(opsgroup.cargoTZC.DeployAirbase)
            else
              -- Land somewhere in the pickup zone. Only helos can do that.
              local coordinate=opsgroup.cargoTZC.DeployZone:GetRandomCoordinate(nil, nil, {land.SurfaceType.LAND})
              opsgroup:LandAt(coordinate, 60*60)
            end

          else
            local coordinate=opsgroup:GetCoordinate()
            opsgroup:LandAt(coordinate, 60*60)
          end


        else
          -- Stop and unload.
          opsgroup:FullStop()
          opsgroup:Unloading()
        end

      elseif opsgroup:IsBoarding() then

        local carrierGroup=opsgroup:_GetMyCarrierGroup()
        local carrier=opsgroup:_GetMyCarrierElement()

        if carrierGroup and carrierGroup:IsAlive() then

          if carrier and carrier.unit and carrier.unit:IsAlive() then

            -- Load group into the carrier.
            carrierGroup:Load(opsgroup)

          else
            opsgroup:E(opsgroup.lid.."ERROR: Group cannot board assigned carrier UNIT as it is NOT alive!")
          end

        else
          opsgroup:E(opsgroup.lid.."ERROR: Group cannot board assigned carrier GROUP as it is NOT alive!")
        end

      elseif opsgroup:IsEngaging() then

        -- Nothing to do really.
        opsgroup:T(opsgroup.lid.."Passing engaging waypoint")

      else

        -- Trigger DetourReached event.
        opsgroup:DetourReached()

        if waypoint.detour==0 then
          opsgroup:FullStop()
        elseif waypoint.detour==1 then
          opsgroup:Cruise()
        else
          opsgroup:E("ERROR: waypoint.detour should be 0 or 1")
          opsgroup:FullStop()
        end

      end

    else

      ---
      -- Normal Route Waypoint
      ---

      -- Check if the group is still pathfinding.
      if opsgroup.ispathfinding then
        opsgroup.ispathfinding=false
      end

      -- Call event function.
      opsgroup:PassingWaypoint(waypoint)
    end

  end

end

--- Function called when a task is executed.
--@param Wrapper.Group#GROUP group Group which should execute the task.
--@param #OPSGROUP opsgroup Ops group.
--@param #OPSGROUP.Task task Task.
function OPSGROUP._TaskExecute(group, opsgroup, task)

  -- Debug message.
  local text=string.format("_TaskExecute %s", task.description)
  opsgroup:T3(opsgroup.lid..text)

  -- Set current task to nil so that the next in line can be executed.
  if opsgroup then
    opsgroup:TaskExecute(task)
  end
end

--- Function called when a task is done.
--@param Wrapper.Group#GROUP group Group for which the task is done.
--@param #OPSGROUP opsgroup Ops group.
--@param #OPSGROUP.Task task Task.
function OPSGROUP._TaskDone(group, opsgroup, task)

  -- Debug message.
  local text=string.format("_TaskDone %s", task.description)
  opsgroup:T(opsgroup.lid..text)

  -- Set current task to nil so that the next in line can be executed.
  if opsgroup then
    opsgroup:TaskDone(task)
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- OPTION FUNCTIONS
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set the default ROE for the group. This is the ROE state gets when the group is spawned or to which it defaults back after a mission.
-- @param #OPSGROUP self
-- @param #number roe ROE of group. Default is `ENUMS.ROE.ReturnFire`.
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultROE(roe)
  self.optionDefault.ROE=roe or ENUMS.ROE.ReturnFire
  return self
end

--- Set current ROE for the group.
-- @param #OPSGROUP self
-- @param #string roe ROE of group. Default is value set in `SetDefaultROE` (usually `ENUMS.ROE.ReturnFire`).
-- @return #OPSGROUP self
function OPSGROUP:SwitchROE(roe)

  if self:IsAlive() or self:IsInUtero() then

    self.option.ROE=roe or self.optionDefault.ROE

    if self:IsInUtero() then
      self:T2(self.lid..string.format("Setting current ROE=%d when GROUP is SPAWNED", self.option.ROE))
    else

      self.group:OptionROE(self.option.ROE)

      self:T(self.lid..string.format("Setting current ROE=%d (%s)", self.option.ROE, self:_GetROEName(self.option.ROE)))
    end


  else
    self:T(self.lid.."WARNING: Cannot switch ROE! Group is not alive")
  end

  return self
end

--- Get name of ROE corresponding to the numerical value.
-- @param #OPSGROUP self
-- @return #string Name of ROE.
function OPSGROUP:_GetROEName(roe)
  local name="unknown"
  if roe==0 then
    name="Weapon Free"
  elseif roe==1 then
    name="Open Fire/Weapon Free"
  elseif roe==2 then
    name="Open Fire"
  elseif roe==3 then
    name="Return Fire"
  elseif roe==4 then
    name="Weapon Hold"
  end
  return name
end

--- Get current ROE of the group.
-- @param #OPSGROUP self
-- @return #number Current ROE.
function OPSGROUP:GetROE()
  return self.option.ROE or self.optionDefault.ROE
end

--- Set the default ROT for the group. This is the ROT state gets when the group is spawned or to which it defaults back after a mission.
-- @param #OPSGROUP self
-- @param #number rot ROT of group. Default is `ENUMS.ROT.PassiveDefense`.
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultROT(rot)
  self.optionDefault.ROT=rot or ENUMS.ROT.PassiveDefense
  return self
end

--- Set ROT for the group.
-- @param #OPSGROUP self
-- @param #string rot ROT of group. Default is value set in `:SetDefaultROT` (usually `ENUMS.ROT.PassiveDefense`).
-- @return #OPSGROUP self
function OPSGROUP:SwitchROT(rot)

  if self:IsFlightgroup() then

    if self:IsAlive() or self:IsInUtero() then

      self.option.ROT=rot or self.optionDefault.ROT

      if self:IsInUtero() then
        self:T2(self.lid..string.format("Setting current ROT=%d when GROUP is SPAWNED", self.option.ROT))
      else

        self.group:OptionROT(self.option.ROT)

        -- Debug info.
        self:T(self.lid..string.format("Setting current ROT=%d (0=NoReaction, 1=Passive, 2=Evade, 3=ByPass, 4=AllowAbort)", self.option.ROT))
      end


    else
      self:T(self.lid.."WARNING: Cannot switch ROT! Group is not alive")
    end

  end

  return self
end

--- Get current ROT of the group.
-- @param #OPSGROUP self
-- @return #number Current ROT.
function OPSGROUP:GetROT()
  return self.option.ROT or self.optionDefault.ROT
end


--- Set the default Alarm State for the group. This is the state gets when the group is spawned or to which it defaults back after a mission.
-- @param #OPSGROUP self
-- @param #number alarmstate Alarm state of group. Default is `AI.Option.Ground.val.ALARM_STATE.AUTO` (0).
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultAlarmstate(alarmstate)
  self.optionDefault.Alarm=alarmstate or 0
  return self
end

--- Set current Alarm State of the group.
--
-- * 0 = "Auto"
-- * 1 = "Green"
-- * 2 = "Red"
--
-- @param #OPSGROUP self
-- @param #number alarmstate Alarm state of group. Default is 0="Auto".
-- @return #OPSGROUP self
function OPSGROUP:SwitchAlarmstate(alarmstate)

  if self:IsAlive() or self:IsInUtero() then

    if self.isArmygroup or self.isNavygroup  then

      self.option.Alarm=alarmstate or self.optionDefault.Alarm

      if self:IsInUtero() then
        self:T2(self.lid..string.format("Setting current Alarm State=%d when GROUP is SPAWNED", self.option.Alarm))
      else

        if self.option.Alarm==0 then
          self.group:OptionAlarmStateAuto()
        elseif self.option.Alarm==1 then
          self.group:OptionAlarmStateGreen()
        elseif self.option.Alarm==2 then
          self.group:OptionAlarmStateRed()
        else
          self:T("ERROR: Unknown Alarm State! Setting to AUTO")
          self.group:OptionAlarmStateAuto()
          self.option.Alarm=0
        end

        self:T(self.lid..string.format("Setting current Alarm State=%d (0=Auto, 1=Green, 2=Red)", self.option.Alarm))

      end

    end
  else
    self:T(self.lid.."WARNING: Cannot switch Alarm State! Group is not alive.")
  end

  return self
end

--- Get current Alarm State of the group.
-- @param #OPSGROUP self
-- @return #number Current Alarm State.
function OPSGROUP:GetAlarmstate()
  return self.option.Alarm or self.optionDefault.Alarm
end

--- Set the default EPLRS for the group.
-- @param #OPSGROUP self
-- @param #boolean OnOffSwitch If `true`, EPLRS is on by default. If `false` default EPLRS setting is off. If `nil`, default is on if group has EPLRS and off if it does not have a datalink.
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultEPLRS(OnOffSwitch)

  if OnOffSwitch==nil then
    self.optionDefault.EPLRS=self.isEPLRS
  else
    self.optionDefault.EPLRS=OnOffSwitch
  end

  return self
end

--- Switch EPLRS datalink on or off.
-- @param #OPSGROUP self
-- @param #boolean OnOffSwitch If `true` or `nil`, switch EPLRS on. If `false` EPLRS switched off.
-- @return #OPSGROUP self
function OPSGROUP:SwitchEPLRS(OnOffSwitch)

  if self:IsAlive() or self:IsInUtero() then

    if OnOffSwitch==nil then

      self.option.EPLRS=self.optionDefault.EPLRS

    else

      self.option.EPLRS=OnOffSwitch

    end

    if self:IsInUtero() then
      self:T2(self.lid..string.format("Setting current EPLRS=%s when GROUP is SPAWNED", tostring(self.option.EPLRS)))
    else

      self.group:CommandEPLRS(self.option.EPLRS)
      self:T(self.lid..string.format("Setting current EPLRS=%s", tostring(self.option.EPLRS)))

    end
  else
    self:E(self.lid.."WARNING: Cannot switch EPLRS! Group is not alive")
  end

  return self
end

--- Get current EPLRS state.
-- @param #OPSGROUP self
-- @return #boolean If `true`, EPLRS is on.
function OPSGROUP:GetEPLRS()
  return self.option.EPLRS or self.optionDefault.EPLRS
end

--- Set the default emission state for the group.
-- @param #OPSGROUP self
-- @param #boolean OnOffSwitch If `true`, EPLRS is on by default. If `false` default EPLRS setting is off. If `nil`, default is on if group has EPLRS and off if it does not have a datalink.
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultEmission(OnOffSwitch)

  if OnOffSwitch==nil then
    self.optionDefault.Emission=true
  else
    self.optionDefault.Emission=OnOffSwitch
  end

  return self
end

--- Switch emission on or off.
-- @param #OPSGROUP self
-- @param #boolean OnOffSwitch If `true` or `nil`, switch emission on. If `false` emission switched off.
-- @return #OPSGROUP self
function OPSGROUP:SwitchEmission(OnOffSwitch)

  if self:IsAlive() or self:IsInUtero() then

    if OnOffSwitch==nil then

      self.option.Emission=self.optionDefault.Emission

    else

      self.option.Emission=OnOffSwitch

    end

    if self:IsInUtero() then
      self:T2(self.lid..string.format("Setting current EMISSION=%s when GROUP is SPAWNED", tostring(self.option.Emission)))
    else

      self.group:EnableEmission(self.option.Emission)
      self:T(self.lid..string.format("Setting current EMISSION=%s", tostring(self.option.Emission)))

    end
  else
    self:E(self.lid.."WARNING: Cannot switch Emission! Group is not alive")
  end

  return self
end

--- Get current emission state.
-- @param #OPSGROUP self
-- @return #boolean If `true`, emission is on.
function OPSGROUP:GetEmission()
  return self.option.Emission or self.optionDefault.Emission
end

--- Set the default invisible for the group.
-- @param #OPSGROUP self
-- @param #boolean OnOffSwitch If `true`, group is ivisible by default.
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultInvisible(OnOffSwitch)

  if OnOffSwitch==nil then
    self.optionDefault.Invisible=true
  else
    self.optionDefault.Invisible=OnOffSwitch
  end

  return self
end

--- Switch invisibility on or off.
-- @param #OPSGROUP self
-- @param #boolean OnOffSwitch If `true` or `nil`, switch invisibliity on. If `false` invisibility switched off.
-- @return #OPSGROUP self
function OPSGROUP:SwitchInvisible(OnOffSwitch)

  if self:IsAlive() or self:IsInUtero() then

    if OnOffSwitch==nil then

      self.option.Invisible=self.optionDefault.Invisible

    else

      self.option.Invisible=OnOffSwitch

    end

    if self:IsInUtero() then
      self:T2(self.lid..string.format("Setting current INVISIBLE=%s when GROUP is SPAWNED", tostring(self.option.Invisible)))
    else

      self.group:SetCommandInvisible(self.option.Invisible)
      self:T(self.lid..string.format("Setting current INVISIBLE=%s", tostring(self.option.Invisible)))

    end
  else
    self:E(self.lid.."WARNING: Cannot switch Invisible! Group is not alive")
  end

  return self
end


--- Set the default immortal for the group.
-- @param #OPSGROUP self
-- @param #boolean OnOffSwitch If `true`, group is immortal by default.
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultImmortal(OnOffSwitch)

  if OnOffSwitch==nil then
    self.optionDefault.Immortal=true
  else
    self.optionDefault.Immortal=OnOffSwitch
  end

  return self
end

--- Switch immortality on or off.
-- @param #OPSGROUP self
-- @param #boolean OnOffSwitch If `true` or `nil`, switch immortality on. If `false` immortality switched off.
-- @return #OPSGROUP self
function OPSGROUP:SwitchImmortal(OnOffSwitch)

  if self:IsAlive() or self:IsInUtero() then

    if OnOffSwitch==nil then

      self.option.Immortal=self.optionDefault.Immortal

    else

      self.option.Immortal=OnOffSwitch

    end

    if self:IsInUtero() then
      self:T2(self.lid..string.format("Setting current IMMORTAL=%s when GROUP is SPAWNED", tostring(self.option.Immortal)))
    else

      self.group:SetCommandImmortal(self.option.Immortal)
      self:T(self.lid..string.format("Setting current IMMORTAL=%s", tostring(self.option.Immortal)))

    end
  else
    self:E(self.lid.."WARNING: Cannot switch Immortal! Group is not alive")
  end

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- SETTINGS FUNCTIONS
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set default TACAN parameters.
-- @param #OPSGROUP self
-- @param #number Channel TACAN channel. Default is 74.
-- @param #string Morse Morse code. Default "XXX".
-- @param #string UnitName Name of the unit acting as beacon.
-- @param #string Band TACAN mode. Default is "X" for ground and "Y" for airborne units.
-- @param #boolean OffSwitch If true, TACAN is off by default.
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultTACAN(Channel, Morse, UnitName, Band, OffSwitch)

  self.tacanDefault={}
  self.tacanDefault.Channel=Channel or 74
  self.tacanDefault.Morse=Morse or "XXX"
  self.tacanDefault.BeaconName=UnitName

  if self:IsFlightgroup() then
    Band=Band or "Y"
  else
    Band=Band or "X"
  end
  self.tacanDefault.Band=Band


  if OffSwitch then
    self.tacanDefault.On=false
  else
    self.tacanDefault.On=true
  end

  return self
end


--- Activate/switch TACAN beacon settings.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Beacon Tacan TACAN data table. Default is the default TACAN settings.
-- @return #OPSGROUP self
function OPSGROUP:_SwitchTACAN(Tacan)

  if Tacan then

    self:SwitchTACAN(Tacan.Channel, Tacan.Morse, Tacan.BeaconName, Tacan.Band)

  else

    if self.tacanDefault.On then
      self:SwitchTACAN()
    else
      self:TurnOffTACAN()
    end

  end

end

--- Activate/switch TACAN beacon settings.
-- @param #OPSGROUP self
-- @param #number Channel TACAN Channel.
-- @param #string Morse TACAN morse code. Default is the value set in @{#OPSGROUP.SetDefaultTACAN} or if not set "XXX".
-- @param #string UnitName Name of the unit in the group which should activate the TACAN beacon. Can also be given as #number to specify the unit number. Default is the first unit of the group.
-- @param #string Band TACAN channel mode "X" or "Y". Default is "Y" for aircraft and "X" for ground and naval groups.
-- @return #OPSGROUP self
function OPSGROUP:SwitchTACAN(Channel, Morse, UnitName, Band)

  if self:IsInUtero() then

    self:T(self.lid..string.format("Switching TACAN to DEFAULT when group is spawned"))
    self:SetDefaultTACAN(Channel, Morse, UnitName, Band)

  elseif self:IsAlive() then

    Channel=Channel or self.tacanDefault.Channel
    Morse=Morse or self.tacanDefault.Morse
    Band=Band or self.tacanDefault.Band
    UnitName=UnitName or self.tacanDefault.BeaconName
    local unit=self:GetUnit(1)  --Wrapper.Unit#UNIT

    if UnitName then
      if type(UnitName)=="number" then
        unit=self.group:GetUnit(UnitName)
      else
        unit=UNIT:FindByName(UnitName)
      end
    end

    if not unit then
      self:T(self.lid.."WARNING: Could not get TACAN unit. Trying first unit in the group")
      unit=self:GetUnit(1)
    end

    if unit and unit:IsAlive() then

      -- Unit ID.
      local UnitID=unit:GetID()

      -- Type
      local Type=BEACON.Type.TACAN

      -- System
      local System=BEACON.System.TACAN
      if self:IsFlightgroup() then
        System=BEACON.System.TACAN_TANKER_Y
      end

      -- Tacan frequency.
      local Frequency=UTILS.TACANToFrequency(Channel, Band)

      -- Activate beacon.
      unit:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Band, true, Morse, true)

      -- Update info.
      self.tacan.Channel=Channel
      self.tacan.Morse=Morse
      self.tacan.Band=Band
      self.tacan.BeaconName=unit:GetName()
      self.tacan.BeaconUnit=unit
      self.tacan.On=true

      -- Debug info.
      self:T(self.lid..string.format("Switching TACAN to Channel %d%s Morse %s on unit %s", self.tacan.Channel, self.tacan.Band, tostring(self.tacan.Morse), self.tacan.BeaconName))

    else
      self:T(self.lid.."ERROR: Cound not set TACAN! Unit is not alive")
    end

  else
    self:T(self.lid.."ERROR: Cound not set TACAN! Group is not alive and not in utero any more")
  end

  return self
end

--- Deactivate TACAN beacon.
-- @param #OPSGROUP self
-- @return #OPSGROUP self
function OPSGROUP:TurnOffTACAN()

  if self.tacan.BeaconUnit and self.tacan.BeaconUnit:IsAlive() then
    self.tacan.BeaconUnit:CommandDeactivateBeacon()
  end

  self:T(self.lid..string.format("Switching TACAN OFF"))
  self.tacan.On=false

end

--- Get current TACAN parameters.
-- @param #OPSGROUP self
-- @return #number TACAN channel.
-- @return #string TACAN Morse code.
-- @return #string TACAN band ("X" or "Y").
-- @return #boolean TACAN is On (true) or Off (false).
-- @return #string UnitName Name of the unit acting as beacon.
function OPSGROUP:GetTACAN()
  return self.tacan.Channel, self.tacan.Morse, self.tacan.Band, self.tacan.On, self.tacan.BeaconName
end

--- Get current TACAN parameters.
-- @param #OPSGROUP self
-- @return #OPSGROUP.Beacon TACAN beacon.
function OPSGROUP:GetBeaconTACAN()
  return self.tacan
end


--- Set default ICLS parameters.
-- @param #OPSGROUP self
-- @param #number Channel ICLS channel. Default is 1.
-- @param #string Morse Morse code. Default "XXX".
-- @param #string UnitName Name of the unit acting as beacon.
-- @param #boolean OffSwitch If true, TACAN is off by default.
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultICLS(Channel, Morse, UnitName, OffSwitch)

  self.iclsDefault={}
  self.iclsDefault.Channel=Channel or 1
  self.iclsDefault.Morse=Morse or "XXX"
  self.iclsDefault.BeaconName=UnitName

  if OffSwitch then
    self.iclsDefault.On=false
  else
    self.iclsDefault.On=true
  end

  return self
end


--- Activate/switch ICLS beacon settings.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Beacon Icls ICLS data table.
-- @return #OPSGROUP self
function OPSGROUP:_SwitchICLS(Icls)

  if Icls then

    self:SwitchICLS(Icls.Channel, Icls.Morse, Icls.BeaconName)

  else

    if self.iclsDefault.On then
      self:SwitchICLS()
    else
      self:TurnOffICLS()
    end

  end

end

--- Activate/switch ICLS beacon settings.
-- @param #OPSGROUP self
-- @param #number Channel ICLS Channel. Default is what is set in `SetDefaultICLS()` so usually channel 1.
-- @param #string Morse ICLS morse code. Default is what is set in `SetDefaultICLS()` so usually "XXX".
-- @param #string UnitName Name of the unit in the group which should activate the ICLS beacon. Can also be given as #number to specify the unit number. Default is the first unit of the group.
-- @return #OPSGROUP self
function OPSGROUP:SwitchICLS(Channel, Morse, UnitName)

  if self:IsInUtero() then

    self:SetDefaultICLS(Channel,Morse,UnitName)

    self:T2(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s when GROUP is SPAWNED", self.iclsDefault.Channel, tostring(self.iclsDefault.Morse), tostring(self.iclsDefault.BeaconName)))

  elseif self:IsAlive() then

    Channel=Channel or self.iclsDefault.Channel
    Morse=Morse or self.iclsDefault.Morse
    local unit=self:GetUnit(1)  --Wrapper.Unit#UNIT

    if UnitName then
      if type(UnitName)=="number" then
        unit=self:GetUnit(UnitName)
      else
        unit=UNIT:FindByName(UnitName)
      end
    end

    if not unit then
      self:T(self.lid.."WARNING: Could not get ICLS unit. Trying first unit in the group")
      unit=self:GetUnit(1)
    end

    if unit and unit:IsAlive() then

      -- Unit ID.
      local UnitID=unit:GetID()

      -- Activate beacon.
      unit:CommandActivateICLS(Channel, UnitID, Morse)

      -- Update info.
      self.icls.Channel=Channel
      self.icls.Morse=Morse
      self.icls.Band=nil
      self.icls.BeaconName=unit:GetName()
      self.icls.BeaconUnit=unit
      self.icls.On=true

      -- Debug info.
      self:T(self.lid..string.format("Switching ICLS to Channel %d Morse %s on unit %s", self.icls.Channel, tostring(self.icls.Morse), self.icls.BeaconName))

    else
      self:T(self.lid.."ERROR: Cound not set ICLS! Unit is not alive.")
    end

  end

  return self
end

--- Deactivate ICLS beacon.
-- @param #OPSGROUP self
-- @return #OPSGROUP self
function OPSGROUP:TurnOffICLS()

  if self.icls.BeaconUnit and self.icls.BeaconUnit:IsAlive() then
    self.icls.BeaconUnit:CommandDeactivateICLS()
  end

  self:T(self.lid..string.format("Switching ICLS OFF"))
  self.icls.On=false

end


--- Set default Radio frequency and modulation.
-- @param #OPSGROUP self
-- @param #number Frequency Radio frequency in MHz. Default 251 MHz.
-- @param #number Modulation Radio modulation. Default `radio.modulation.AM`.
-- @param #boolean OffSwitch If true, radio is OFF by default.
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultRadio(Frequency, Modulation, OffSwitch)

  self.radioDefault={}
  self.radioDefault.Freq=Frequency or 251
  self.radioDefault.Modu=Modulation or radio.modulation.AM
  if OffSwitch then
    self.radioDefault.On=false
  else
    self.radioDefault.On=true
  end

  return self
end

--- Get current Radio frequency and modulation.
-- @param #OPSGROUP self
-- @return #number Radio frequency in MHz or nil.
-- @return #number Radio modulation or nil.
-- @return #boolean If true, the radio is on. Otherwise, radio is turned off.
function OPSGROUP:GetRadio()
  return self.radio.Freq, self.radio.Modu, self.radio.On
end

--- Turn radio on or switch frequency/modulation.
-- @param #OPSGROUP self
-- @param #number Frequency Radio frequency in MHz. Default is value set in `SetDefaultRadio` (usually 251 MHz).
-- @param #number Modulation Radio modulation. Default is value set in `SetDefaultRadio` (usually `radio.modulation.AM`).
-- @return #OPSGROUP self
function OPSGROUP:SwitchRadio(Frequency, Modulation)

  if self:IsInUtero() then

    -- Set default radio.
    self:SetDefaultRadio(Frequency, Modulation)

    -- Debug info.
    self:T2(self.lid..string.format("Switching radio to frequency %.3f MHz %s when GROUP is SPAWNED", self.radioDefault.Freq, UTILS.GetModulationName(self.radioDefault.Modu)))

  elseif self:IsAlive() then

    Frequency=Frequency or self.radioDefault.Freq
    Modulation=Modulation or self.radioDefault.Modu

    if self:IsFlightgroup() and not self.radio.On then
      self.group:SetOption(AI.Option.Air.id.SILENCE, false)
    end

    -- Give command
    self.group:CommandSetFrequency(Frequency, Modulation)

    -- Update current settings.
    self.radio.Freq=Frequency
    self.radio.Modu=Modulation
    self.radio.On=true

    -- Debug info.
    self:T(self.lid..string.format("Switching radio to frequency %.3f MHz %s", self.radio.Freq, UTILS.GetModulationName(self.radio.Modu)))

  else
    self:T(self.lid.."ERROR: Cound not set Radio! Group is not alive or not in utero any more")
  end

  return self
end

--- Turn radio off.
-- @param #OPSGROUP self
-- @return #OPSGROUP self
function OPSGROUP:TurnOffRadio()

  if self:IsAlive() then

    if self:IsFlightgroup() then

      -- Set group to be silient.
      self.group:SetOption(AI.Option.Air.id.SILENCE, true)

      -- Radio is off.
      self.radio.On=false

      self:T(self.lid..string.format("Switching radio OFF"))
    else
      self:T(self.lid.."ERROR: Radio can only be turned off for aircraft!")
    end

  end

  return self
end



--- Set default formation.
-- @param #OPSGROUP self
-- @param #number Formation The formation the groups flies in.
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultFormation(Formation)

  self.optionDefault.Formation=Formation

  return self
end

--- Switch to a specific formation.
-- @param #OPSGROUP self
-- @param #number Formation New formation the group will fly in. Default is the setting of `SetDefaultFormation()`.
-- @return #OPSGROUP self
function OPSGROUP:SwitchFormation(Formation)

  if self:IsAlive() then

    Formation=Formation or self.optionDefault.Formation

    if self:IsFlightgroup() then

      self.group:SetOption(AI.Option.Air.id.FORMATION, Formation)

    elseif self.isArmygroup then

      -- Polymorphic and overwritten in ARMYGROUP.

    else
      self:T(self.lid.."ERROR: Formation can only be set for aircraft or ground units!")
      return self
    end

    -- Set current formation.
    self.option.Formation=Formation

    -- Debug info.
    self:T(self.lid..string.format("Switching formation to %s", tostring(self.option.Formation)))

  end

  return self
end



--- Set default callsign.
-- @param #OPSGROUP self
-- @param #number CallsignName Callsign name.
-- @param #number CallsignNumber Callsign number. Default 1.
-- @return #OPSGROUP self
function OPSGROUP:SetDefaultCallsign(CallsignName, CallsignNumber)

  self:T(self.lid..string.format("Setting Default callsing %s-%s", tostring(CallsignName), tostring(CallsignNumber)))

  self.callsignDefault={} --#OPSGROUP.Callsign
  self.callsignDefault.NumberSquad=CallsignName
  self.callsignDefault.NumberGroup=CallsignNumber or 1
  self.callsignDefault.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad)
  
  --self:I(self.lid..string.format("Default callsign=%s", self.callsignDefault.NameSquad))

  return self
end

--- Switch to a specific callsign.
-- @param #OPSGROUP self
-- @param #number CallsignName Callsign name.
-- @param #number CallsignNumber Callsign number.
-- @return #OPSGROUP self
function OPSGROUP:SwitchCallsign(CallsignName, CallsignNumber)

  if self:IsInUtero() then

    -- Set default callsign. We switch to this when group is spawned.
    self:SetDefaultCallsign(CallsignName, CallsignNumber)
    --self.callsign=UTILS.DeepCopy(self.callsignDefault)

  elseif self:IsAlive() then

    CallsignName=CallsignName or self.callsignDefault.NumberSquad
    CallsignNumber=CallsignNumber or self.callsignDefault.NumberGroup

    -- Set current callsign.
    self.callsign.NumberSquad=CallsignName
    self.callsign.NumberGroup=CallsignNumber

    -- Debug.
    self:T(self.lid..string.format("Switching callsign to %d-%d", self.callsign.NumberSquad, self.callsign.NumberGroup))

    -- Give command to change the callsign.
    self.group:CommandSetCallsign(self.callsign.NumberSquad, self.callsign.NumberGroup)

    -- Callsign of the group, e.g. Colt-1
    self.callsignName=UTILS.GetCallsignName(self.callsign.NumberSquad).."-"..self.callsign.NumberGroup
    self.callsign.NameSquad=UTILS.GetCallsignName(self.callsign.NumberSquad)

    -- Set callsign of elements.
    for _,_element in pairs(self.elements) do
      local element=_element --#OPSGROUP.Element
      if element.status~=OPSGROUP.ElementStatus.DEAD then
        element.callsign=element.unit:GetCallsign()
      end
    end

  else
    self:T(self.lid.."ERROR: Group is not alive and not in utero! Cannot switch callsign")
  end

  return self
end

--- Get callsign of the first element alive.
-- @param #OPSGROUP self
-- @param #boolean ShortCallsign If true, append major flight number only
-- @param #boolean Keepnumber (Player only) If true, and using a customized callsign in the #GROUP name after an #-sign, use all of that information.
-- @param #table CallsignTranslations (optional) Translation table between callsigns
-- @return #string Callsign name, e.g. Uzi11, or "Ghostrider11".
function OPSGROUP:GetCallsignName(ShortCallsign,Keepnumber,CallsignTranslations)

  local element=self:GetElementAlive()

  if element then
    self:T2(self.lid..string.format("Callsign %s", tostring(element.callsign)))
    local name=element.callsign or "Ghostrider11"
    name=name:gsub("-", "")
    if self.group:IsPlayer() or CallsignTranslations then
      name=self.group:GetCustomCallSign(ShortCallsign,Keepnumber,CallsignTranslations)
    end
    return name
  end

  return "Ghostrider11"
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Element and Group Status Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check if all elements of the group have the same status (or are dead).
-- @param #OPSGROUP self
-- @return #OPSGROUP self
function OPSGROUP:_UpdatePosition()

  if self:IsExist() then

    -- Backup last state to monitor differences.
    self.positionLast=self.position or self:GetVec3()
    self.headingLast=self.heading or self:GetHeading()
    self.orientXLast=self.orientX or self:GetOrientationX()
  self.velocityLast=self.velocity or self.group:GetVelocityMPS()

    -- Current state.
    self.position=self:GetVec3()
    self.heading=self:GetHeading()
    self.orientX=self:GetOrientationX()
    self.velocity=self:GetVelocity()

    for _,_element in pairs(self.elements) do
      local element=_element --#OPSGROUP.Element
      element.vec3=self:GetVec3(element.name)
    end

    -- Update time.
    local Tnow=timer.getTime()
    self.dTpositionUpdate=self.TpositionUpdate and Tnow-self.TpositionUpdate or 0
    self.TpositionUpdate=Tnow

    if not self.traveldist then
      self.traveldist=0
    end

    -- Travel distance since last check.
    self.travelds=UTILS.VecNorm(UTILS.VecSubstract(self.position, self.positionLast))

    -- Add up travelled distance.
    self.traveldist=self.traveldist+self.travelds

  end

  return self
end

--- Check if all elements of the group have the same status (or are dead).
-- @param #OPSGROUP self
-- @param #string unitname Name of unit.
function OPSGROUP:_AllSameStatus(status)

  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element

    if element.status==OPSGROUP.ElementStatus.DEAD then
      -- Do nothing. Element is already dead and does not count.
    elseif element.status~=status then
      -- At least this element has a different status.
      return false
    end

  end

  return true
end

--- Check if all elements of the group have the same status (or are dead).
-- @param #OPSGROUP self
-- @param #string status Status to check.
-- @return #boolean If true, all elements have a similar status.
function OPSGROUP:_AllSimilarStatus(status)

  -- Check if all are dead.
  if status==OPSGROUP.ElementStatus.DEAD then
    for _,_element in pairs(self.elements) do
      local element=_element --#OPSGROUP.Element
      if element.status~=OPSGROUP.ElementStatus.DEAD then
        -- At least one is still alive.
        return false
      end
    end
    return true
  end

  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element

    self:T2(self.lid..string.format("Status=%s, element %s status=%s", status, element.name, element.status))

    -- Dead units dont count ==> We wont return false for those.
    if element.status~=OPSGROUP.ElementStatus.DEAD then

      ----------
      -- ALIVE
      ----------

      if status==OPSGROUP.ElementStatus.INUTERO then

        -- Element INUTERO: Check that ALL others are also INUTERO
        if element.status~=status then
          return false
        end


      elseif status==OPSGROUP.ElementStatus.SPAWNED then

        -- Element SPAWNED: Check that others are not still IN UTERO
        if element.status~=status and
          element.status==OPSGROUP.ElementStatus.INUTERO  then
          return false
        end

      elseif status==OPSGROUP.ElementStatus.PARKING then

        -- Element PARKING: Check that the other are not still SPAWNED
        if element.status~=status or
         (element.status==OPSGROUP.ElementStatus.INUTERO or
          element.status==OPSGROUP.ElementStatus.SPAWNED) then
          return false
        end

      elseif status==OPSGROUP.ElementStatus.ENGINEON then

        -- Element TAXIING: Check that the other are not still SPAWNED or PARKING
        if element.status~=status and
         (element.status==OPSGROUP.ElementStatus.INUTERO or
          element.status==OPSGROUP.ElementStatus.SPAWNED or
          element.status==OPSGROUP.ElementStatus.PARKING) then
          return false
        end

      elseif status==OPSGROUP.ElementStatus.TAXIING then

        -- Element TAXIING: Check that the other are not still SPAWNED or PARKING
        if element.status~=status and
         (element.status==OPSGROUP.ElementStatus.INUTERO or
          element.status==OPSGROUP.ElementStatus.SPAWNED or
          element.status==OPSGROUP.ElementStatus.PARKING or
          element.status==OPSGROUP.ElementStatus.ENGINEON) then
          return false
        end

      elseif status==OPSGROUP.ElementStatus.TAKEOFF then

        -- Element TAKEOFF: Check that the other are not still SPAWNED, PARKING or TAXIING
        if element.status~=status and
         (element.status==OPSGROUP.ElementStatus.INUTERO or
          element.status==OPSGROUP.ElementStatus.SPAWNED or
          element.status==OPSGROUP.ElementStatus.PARKING or
          element.status==OPSGROUP.ElementStatus.ENGINEON or
          element.status==OPSGROUP.ElementStatus.TAXIING) then
          return false
        end

      elseif status==OPSGROUP.ElementStatus.AIRBORNE then

        -- Element AIRBORNE: Check that the other are not still SPAWNED, PARKING, TAXIING or TAKEOFF
        if element.status~=status and
         (element.status==OPSGROUP.ElementStatus.INUTERO or
          element.status==OPSGROUP.ElementStatus.SPAWNED or
          element.status==OPSGROUP.ElementStatus.PARKING or
          element.status==OPSGROUP.ElementStatus.ENGINEON or
          element.status==OPSGROUP.ElementStatus.TAXIING or
          element.status==OPSGROUP.ElementStatus.TAKEOFF) then
          return false
        end

      elseif status==OPSGROUP.ElementStatus.LANDED then

        -- Element LANDED: check that the others are not still AIRBORNE or LANDING
        if element.status~=status and
         (element.status==OPSGROUP.ElementStatus.AIRBORNE or
          element.status==OPSGROUP.ElementStatus.LANDING) then
          return false
        end

      elseif status==OPSGROUP.ElementStatus.ARRIVED then

        -- Element ARRIVED: check that the others are not still AIRBORNE, LANDING, or LANDED (taxiing).
        if element.status~=status and
         (element.status==OPSGROUP.ElementStatus.AIRBORNE or
          element.status==OPSGROUP.ElementStatus.LANDING  or
          element.status==OPSGROUP.ElementStatus.LANDED)  then
          return false
        end

      end

    else
      -- Element is dead. We don't care unless all are dead.
    end --DEAD

  end

  -- Debug info.
  self:T2(self.lid..string.format("All %d elements have similar status %s ==> returning TRUE", #self.elements, status))

  return true
end

--- Check if all elements of the group have the same status or are dead.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Element element Element.
-- @param #string newstatus New status of element
-- @param Wrapper.Airbase#AIRBASE airbase Airbase if applicable.
function OPSGROUP:_UpdateStatus(element, newstatus, airbase)

  -- Old status.
  local oldstatus=element.status

  -- Update status of element.
  element.status=newstatus

  -- Debug
  self:T3(self.lid..string.format("UpdateStatus element=%s: %s --> %s", element.name, oldstatus, newstatus))
  for _,_element in pairs(self.elements) do
    local Element=_element -- #OPSGROUP.Element
    self:T3(self.lid..string.format("Element %s: %s", Element.name, Element.status))
  end

  if newstatus==OPSGROUP.ElementStatus.INUTERO then
    ---
    -- INUTERO
    ---

    if self:_AllSimilarStatus(newstatus) then
      self:InUtero()
    end

  elseif newstatus==OPSGROUP.ElementStatus.SPAWNED then
    ---
    -- SPAWNED
    ---

    if self:_AllSimilarStatus(newstatus) then
      self:Spawned()
    end

  elseif newstatus==OPSGROUP.ElementStatus.PARKING then
    ---
    -- PARKING
    ---

    if self:_AllSimilarStatus(newstatus) then
      self:Parking()
    end

  elseif newstatus==OPSGROUP.ElementStatus.ENGINEON then
    ---
    -- ENGINEON
    ---

    -- No FLIGHT status. Waiting for taxiing.

  elseif newstatus==OPSGROUP.ElementStatus.TAXIING then
    ---
    -- TAXIING
    ---

    if self:_AllSimilarStatus(newstatus) then
      self:Taxiing()
    end

  elseif newstatus==OPSGROUP.ElementStatus.TAKEOFF then
    ---
    -- TAKEOFF
    ---

    if self:_AllSimilarStatus(newstatus) then
      -- Trigger takeoff event. Also triggers airborne event.
      self:Takeoff(airbase)
    end

  elseif newstatus==OPSGROUP.ElementStatus.AIRBORNE then
    ---
    -- AIRBORNE
    ---

    if self:_AllSimilarStatus(newstatus) then
      self:Airborne()
    end

  elseif newstatus==OPSGROUP.ElementStatus.LANDED then
    ---
    -- LANDED
    ---

    if self:_AllSimilarStatus(newstatus) then
      if self:IsLandingAt() then
        self:LandedAt()
      else
        self:Landed(airbase)
      end
    end

  elseif newstatus==OPSGROUP.ElementStatus.ARRIVED then
    ---
    -- ARRIVED
    ---

    if self:_AllSimilarStatus(newstatus) then

      if self:IsLanded() then
        self:Arrived()
      elseif self:IsAirborne() then
        self:Landed()
        self:Arrived()
      end

    end

  elseif newstatus==OPSGROUP.ElementStatus.DEAD then
    ---
    -- DEAD
    ---

    if self:_AllSimilarStatus(newstatus) then
      self:Dead()
    end

  end
end

--- Set status for all elements (except dead ones).
-- @param #OPSGROUP self
-- @param #string status Element status.
function OPSGROUP:_SetElementStatusAll(status)

  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element
    if element.status~=OPSGROUP.ElementStatus.DEAD then
      element.status=status
    end
  end

end

--- Get the element of a group.
-- @param #OPSGROUP self
-- @param #string unitname Name of unit.
-- @return #OPSGROUP.Element The element.
function OPSGROUP:GetElementByName(unitname)

  if unitname and type(unitname)=="string" then

    for _,_element in pairs(self.elements) do
      local element=_element --#OPSGROUP.Element

      if element.name==unitname then
        return element
      end

    end

  end

  return nil
end

--- Get the bounding box of the element.
-- @param #OPSGROUP self
-- @param #string UnitName Name of unit.
-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone.
function OPSGROUP:GetElementZoneBoundingBox(UnitName)

  local element=self:GetElementByName(UnitName)

  if element and element.status~=OPSGROUP.ElementStatus.DEAD then

    -- Create a new zone if necessary.
    element.zoneBoundingbox=element.zoneBoundingbox or ZONE_POLYGON_BASE:New(element.name.." Zone Bounding Box", {})

    -- Length in meters.
    local l=element.length
    -- Width in meters.
    local w=element.width

    -- Orientation vector.
    local X=self:GetOrientationX(element.name)

    -- Heading in degrees.
    local heading=math.deg(math.atan2(X.z, X.x))

    -- Debug info.
    self:T(self.lid..string.format("Element %s bouding box: l=%d w=%d heading=%d", element.name, l, w, heading))

    -- Set of edges facing "North" at the origin of the map.
    local b={}
    b[1]={x=l/2,  y=-w/2} --DCS#Vec2
    b[2]={x=l/2,  y=w/2}  --DCS#Vec2
    b[3]={x=-l/2, y=w/2}  --DCS#Vec2
    b[4]={x=-l/2, y=-w/2} --DCS#Vec2

    -- Rotate box to match current heading of the unit.
    for i,p in pairs(b) do
      b[i]=UTILS.Vec2Rotate2D(p, heading)
    end

    -- Translate the zone to the positon of the unit.
    local vec2=self:GetVec2(element.name)
    local d=UTILS.Vec2Norm(vec2)
    local h=UTILS.Vec2Hdg(vec2)
    for i,p in pairs(b) do
      b[i]=UTILS.Vec2Translate(p, d, h)
    end

    -- Update existing zone.
    element.zoneBoundingbox:UpdateFromVec2(b)

    return element.zoneBoundingbox
  end

  return nil
end

--- Get the loading zone of the element.
-- @param #OPSGROUP self
-- @param #string UnitName Name of unit.
-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone.
function OPSGROUP:GetElementZoneLoad(UnitName)

  local element=self:GetElementByName(UnitName)

  if element and element.status~=OPSGROUP.ElementStatus.DEAD then

    element.zoneLoad=element.zoneLoad or ZONE_POLYGON_BASE:New(element.name.." Zone Load", {})

    self:_GetElementZoneLoader(element, element.zoneLoad, self.carrierLoader)

    return element.zoneLoad
  end

  return nil
end

--- Get the unloading zone of the element.
-- @param #OPSGROUP self
-- @param #string UnitName Name of unit.
-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone.
function OPSGROUP:GetElementZoneUnload(UnitName)

  local element=self:GetElementByName(UnitName)

  if element and element.status~=OPSGROUP.ElementStatus.DEAD then

    element.zoneUnload=element.zoneUnload or ZONE_POLYGON_BASE:New(element.name.." Zone Unload", {})

    self:_GetElementZoneLoader(element, element.zoneUnload, self.carrierUnloader)

    return element.zoneUnload
  end

  return nil
end

--- Get/update the (un-)loading zone of the element.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Element Element Element.
-- @param Core.Zone#ZONE_POLYGON Zone The zone.
-- @param #OPSGROUP.CarrierLoader Loader Loader parameters.
-- @return Core.Zone#ZONE_POLYGON Bounding box polygon zone.
function OPSGROUP:_GetElementZoneLoader(Element, Zone, Loader)

  if Element.status~=OPSGROUP.ElementStatus.DEAD then

    local l=Element.length
    local w=Element.width

    -- Orientation 3D vector where the "nose" is pointing.
    local X=self:GetOrientationX(Element.name)

    -- Heading in deg.
    local heading=math.deg(math.atan2(X.z, X.x))

    -- Bounding box at the origin of the map facing "North".
    local b={}

    -- Create polygon rectangles.
    if Loader.type:lower()=="front" then
      table.insert(b, {x= l/2,               y=-Loader.width/2}) -- left, low
      table.insert(b, {x= l/2+Loader.length, y=-Loader.width/2}) -- left, up
      table.insert(b, {x= l/2+Loader.length, y= Loader.width/2}) -- right, up
      table.insert(b, {x= l/2,               y= Loader.width/2}) -- right, low
    elseif Loader.type:lower()=="back" then
      table.insert(b, {x=-l/2,               y=-Loader.width/2}) -- left, low
      table.insert(b, {x=-l/2-Loader.length, y=-Loader.width/2}) -- left, up
      table.insert(b, {x=-l/2-Loader.length, y= Loader.width/2}) -- right, up
      table.insert(b, {x=-l/2,               y= Loader.width/2}) -- right, low
    elseif Loader.type:lower()=="left" then
      table.insert(b, {x= Loader.length/2, y= -w/2})              -- right, up
      table.insert(b, {x= Loader.length/2, y= -w/2-Loader.width}) -- left,  up
      table.insert(b, {x=-Loader.length/2, y= -w/2-Loader.width}) -- left,  down
      table.insert(b, {x=-Loader.length/2, y= -w/2})              -- right, down
    elseif Loader.type:lower()=="right" then
      table.insert(b, {x= Loader.length/2, y=  w/2})              -- right, up
      table.insert(b, {x= Loader.length/2, y=  w/2+Loader.width}) -- left,  up
      table.insert(b, {x=-Loader.length/2, y=  w/2+Loader.width}) -- left,  down
      table.insert(b, {x=-Loader.length/2, y=  w/2})              -- right, down
    else
      -- All aspect. Rectangle around the unit but need to cut out the area of the unit itself.
      b[1]={x= l/2, y=-w/2} --DCS#Vec2
      b[2]={x= l/2, y= w/2} --DCS#Vec2
      b[3]={x=-l/2, y= w/2} --DCS#Vec2
      b[4]={x=-l/2, y=-w/2} --DCS#Vec2
      table.insert(b, {x=b[1].x+Loader.length, y=b[1].y-Loader.width})
      table.insert(b, {x=b[2].x+Loader.length, y=b[2].y+Loader.width})
      table.insert(b, {x=b[3].x-Loader.length, y=b[3].y+Loader.width})
      table.insert(b, {x=b[4].x-Loader.length, y=b[4].y-Loader.width})
    end

    -- Rotate edges to match the current heading of the unit.
    for i,p in pairs(b) do
      b[i]=UTILS.Vec2Rotate2D(p, heading)
    end

    -- Translate box to the current position of the unit.
    local vec2=self:GetVec2(Element.name)
    local d=UTILS.Vec2Norm(vec2)
    local h=UTILS.Vec2Hdg(vec2)

    for i,p in pairs(b) do
      b[i]=UTILS.Vec2Translate(p, d, h)
    end

    -- Update existing zone.
    Zone:UpdateFromVec2(b)

    return Zone
  end

  return nil
end


--- Get the first element of a group, which is alive.
-- @param #OPSGROUP self
-- @return #OPSGROUP.Element The element or `#nil` if no element is alive any more.
function OPSGROUP:GetElementAlive()

  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element
    if element.status~=OPSGROUP.ElementStatus.DEAD then
      if element.unit and element.unit:IsAlive() then
        return element
      end
    end
  end

  return nil
end


--- Get number of elements alive.
-- @param #OPSGROUP self
-- @param #string status (Optional) Only count number, which are in a special status.
-- @return #number Number of elements.
function OPSGROUP:GetNelements(status)

  local n=0
  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element
    if element.status~=OPSGROUP.ElementStatus.DEAD then
      if element.unit and element.unit:IsAlive() then
        if status==nil or element.status==status then
          n=n+1
        end
      end
    end
  end


  return n
end

--- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up.
-- @param #OPSGROUP self
-- @param #OPSGROUP.Element element The element.
-- @return #OPSGROUP.Ammo Ammo data.
function OPSGROUP:GetAmmoElement(element)
  return self:GetAmmoUnit(element.unit)
end

--- Get total amount of ammunition of the whole group.
-- @param #OPSGROUP self
-- @return #OPSGROUP.Ammo Ammo data.
function OPSGROUP:GetAmmoTot()

  local units=self.group:GetUnits()

  local Ammo={} --#OPSGROUP.Ammo
  Ammo.Total=0
  Ammo.Guns=0
  Ammo.Rockets=0
  Ammo.Bombs=0
  Ammo.Torpedos=0
  Ammo.Missiles=0
  Ammo.MissilesAA=0
  Ammo.MissilesAG=0
  Ammo.MissilesAS=0
  Ammo.MissilesCR=0
  Ammo.MissilesSA=0

  for _,_unit in pairs(units or {}) do
    local unit=_unit --Wrapper.Unit#UNIT

    if unit and unit:IsExist() then

      -- Get ammo of the unit.
      local ammo=self:GetAmmoUnit(unit)

      -- Add up total.
      Ammo.Total=Ammo.Total+ammo.Total
      Ammo.Guns=Ammo.Guns+ammo.Guns
      Ammo.Rockets=Ammo.Rockets+ammo.Rockets
      Ammo.Bombs=Ammo.Bombs+ammo.Bombs
      Ammo.Torpedos=Ammo.Torpedos+ammo.Torpedos
      Ammo.Missiles=Ammo.Missiles+ammo.Missiles
      Ammo.MissilesAA=Ammo.MissilesAA+ammo.MissilesAA
      Ammo.MissilesAG=Ammo.MissilesAG+ammo.MissilesAG
      Ammo.MissilesAS=Ammo.MissilesAS+ammo.MissilesAS
      Ammo.MissilesCR=Ammo.MissilesCR+ammo.MissilesCR
      Ammo.MissilesSA=Ammo.MissilesSA+ammo.MissilesSA

    end

  end

  return Ammo
end

--- Get the number of shells a unit or group currently has. For a group the ammo count of all units is summed up.
-- @param #OPSGROUP self
-- @param Wrapper.Unit#UNIT unit The unit object.
-- @param #boolean display Display ammo table as message to all. Default false.
-- @return #OPSGROUP.Ammo Ammo data.
function OPSGROUP:GetAmmoUnit(unit, display)

  -- Default is display false.
  if display==nil then
    display=false
  end

  -- Init counter.
  local nammo=0
  local nshells=0
  local nrockets=0
  local nmissiles=0
  local nmissilesAA=0
  local nmissilesAG=0
  local nmissilesAS=0
  local nmissilesSA=0
  local nmissilesBM=0
  local nmissilesCR=0
  local ntorps=0
  local nbombs=0

  unit=unit or self.group:GetUnit(1)

  if unit and unit:IsExist() then

    -- Output.
    local text=string.format("OPSGROUP group %s - unit %s:\n", self.groupname, unit:GetName())

    -- Get ammo table.
    local ammotable=unit:GetAmmo()

    if ammotable then

      local weapons=#ammotable
    
    --self:I(ammotable)

      -- Loop over all weapons.
      for w=1,weapons do

        -- Number of current weapon.
        local Nammo=ammotable[w]["count"]
      
      -- Range in meters. Seems only to exist for missiles (not shells).
      local rmin=ammotable[w]["desc"]["rangeMin"] or 0
      local rmax=ammotable[w]["desc"]["rangeMaxAltMin"] or 0

        -- Type name of current weapon.
        local Tammo=ammotable[w]["desc"]["typeName"]

        local _weaponString = UTILS.Split(Tammo,"%.")
        local _weaponName   = _weaponString[#_weaponString]

        -- Get the weapon category: shell=0, missile=1, rocket=2, bomb=3, torpedo=4
        local Category=ammotable[w].desc.category

        -- Get missile category: Weapon.MissileCategory AAM=1, SAM=2, BM=3, ANTI_SHIP=4, CRUISE=5, OTHER=6
        local MissileCategory=nil
        if Category==Weapon.Category.MISSILE then
          MissileCategory=ammotable[w].desc.missileCategory
        end

        -- We are specifically looking for shells or rockets here.
        if Category==Weapon.Category.SHELL then

          -- Add up all shells.
          nshells=nshells+Nammo

          -- Debug info.
          text=text..string.format("- %d shells of type %s, range=%d - %d meters\n", Nammo, _weaponName, rmin, rmax)

        elseif Category==Weapon.Category.ROCKET then

          -- Add up all rockets.
          nrockets=nrockets+Nammo

          -- Debug info.
          text=text..string.format("- %d rockets of type %s, \n", Nammo, _weaponName, rmin, rmax)

        elseif Category==Weapon.Category.BOMB then

          -- Add up all rockets.
          nbombs=nbombs+Nammo

          -- Debug info.
          text=text..string.format("- %d bombs of type %s\n", Nammo, _weaponName)

        elseif Category==Weapon.Category.MISSILE then

          -- Add up all cruise missiles (category 5)
          if MissileCategory==Weapon.MissileCategory.AAM then
            nmissiles=nmissiles+Nammo
            nmissilesAA=nmissilesAA+Nammo
          elseif MissileCategory==Weapon.MissileCategory.SAM then
            nmissiles=nmissiles+Nammo
            nmissilesSA=nmissilesSA+Nammo
          elseif MissileCategory==Weapon.MissileCategory.ANTI_SHIP then
            nmissiles=nmissiles+Nammo
            nmissilesAS=nmissilesAS+Nammo
          elseif MissileCategory==Weapon.MissileCategory.BM then
            nmissiles=nmissiles+Nammo
            nmissilesBM=nmissilesBM+Nammo
          elseif MissileCategory==Weapon.MissileCategory.CRUISE then
            nmissiles=nmissiles+Nammo
            nmissilesCR=nmissilesCR+Nammo
          elseif MissileCategory==Weapon.MissileCategory.OTHER then
            nmissiles=nmissiles+Nammo
            nmissilesAG=nmissilesAG+Nammo
          end

          -- Debug info.
          text=text..string.format("- %d %s missiles of type %s, range=%d - %d meters\n", Nammo, self:_MissileCategoryName(MissileCategory), _weaponName, rmin, rmax)

        elseif Category==Weapon.Category.TORPEDO then

          -- Add up all rockets.
          ntorps=ntorps+Nammo

          -- Debug info.
          text=text..string.format("- %d torpedos of type %s\n", Nammo, _weaponName)

        else

          -- Debug info.
          text=text..string.format("- %d unknown ammo of type %s (category=%d, missile category=%s)\n", Nammo, Tammo, Category, tostring(MissileCategory))

        end

      end
    end

    -- Debug text and send message.
    if display then
      self:I(self.lid..text)
    else
      self:T3(self.lid..text)
    end

  end

  -- Total amount of ammunition.
  nammo=nshells+nrockets+nmissiles+nbombs+ntorps

  local ammo={} --#OPSGROUP.Ammo
  ammo.Total=nammo
  ammo.Guns=nshells
  ammo.Rockets=nrockets
  ammo.Bombs=nbombs
  ammo.Torpedos=ntorps
  ammo.Missiles=nmissiles
  ammo.MissilesAA=nmissilesAA
  ammo.MissilesAG=nmissilesAG
  ammo.MissilesAS=nmissilesAS
  ammo.MissilesCR=nmissilesCR
  ammo.MissilesBM=nmissilesBM
  ammo.MissilesSA=nmissilesSA

  return ammo
end

--- Returns a name of a missile category.
-- @param #OPSGROUP self
-- @param #number categorynumber Number of missile category from weapon missile category enumerator. See https://wiki.hoggitworld.com/view/DCS_Class_Weapon
-- @return #string Missile category name.
function OPSGROUP:_MissileCategoryName(categorynumber)
  local cat="unknown"
  if categorynumber==Weapon.MissileCategory.AAM then
    cat="air-to-air"
  elseif categorynumber==Weapon.MissileCategory.SAM then
    cat="surface-to-air"
  elseif categorynumber==Weapon.MissileCategory.BM then
    cat="ballistic"
  elseif categorynumber==Weapon.MissileCategory.ANTI_SHIP then
    cat="anti-ship"
  elseif categorynumber==Weapon.MissileCategory.CRUISE then
    cat="cruise"
  elseif categorynumber==Weapon.MissileCategory.OTHER then
    cat="other"
  end
  return cat
end

--- Set passed final waypoint value.
-- @param #OPSGROUP self
-- @param #boolean final If `true`, final waypoint was passed.
-- @param #string comment Some comment as to why the final waypoint was passed.
function OPSGROUP:_PassedFinalWaypoint(final, comment)

  -- Debug info.
  self:T(self.lid..string.format("Passed final waypoint=%s [from %s]: comment \"%s\"", tostring(final), tostring(self.passedfinalwp), tostring(comment)))

  if final==true and not self.passedfinalwp then
    self:PassedFinalWaypoint()
  end

  -- Set value.
  self.passedfinalwp=final
end


--- Get coordinate from an object.
-- @param #OPSGROUP self
-- @param Wrapper.Object#OBJECT Object The object.
-- @return Core.Point#COORDINATE The coordinate of the object.
function OPSGROUP:_CoordinateFromObject(Object)

  if Object then
    if Object:IsInstanceOf("COORDINATE") then
      return Object
    else
      if Object:IsInstanceOf("POSITIONABLE") or Object:IsInstanceOf("ZONE_BASE") then
        self:T(self.lid.."WARNING: Coordinate is not a COORDINATE but a POSITIONABLE or ZONE. Trying to get coordinate")
        local coord=Object:GetCoordinate()
        return coord
      else
        self:T(self.lid.."ERROR: Coordinate is neither a COORDINATE nor any POSITIONABLE or ZONE!")
      end
    end
  else
    self:T(self.lid.."ERROR: Object passed is nil!")
  end

  return nil
end

--- Check if a unit is an element of the flightgroup.
-- @param #OPSGROUP self
-- @param #string unitname Name of unit.
-- @return #boolean If true, unit is element of the flight group or false if otherwise.
function OPSGROUP:_IsElement(unitname)

  for _,_element in pairs(self.elements) do
    local element=_element --Ops.OpsGroup#OPSGROUP.Element

    if element.name==unitname then
      return true
    end

  end

  return false
end

--- Count elements of the group.
-- @param #OPSGROUP self
-- @param #table States (Optional) Only count elements in specific states. Can also be a single state passed as #string.
-- @return #number Number of elements.
function OPSGROUP:CountElements(States)

  if States then
    if type(States)=="string" then
      States={States}
    end
  else
    States=OPSGROUP.ElementStatus
  end

  local IncludeDeads=true

  local N=0
  for _,_element in pairs(self.elements) do
    local element=_element --#OPSGROUP.Element
    if element and (IncludeDeads or element.status~=OPSGROUP.ElementStatus.DEAD) then
      for _,state in pairs(States) do
        if element.status==state then
          N=N+1
          break
        end
      end
    end
  end

  return N
end

--- Add a unit/element to the OPS group.
-- @param #OPSGROUP self
-- @param #string unitname Name of unit.
-- @return #OPSGROUP.Element The element or nil.
function OPSGROUP:_AddElementByName(unitname)

  local unit=UNIT:FindByName(unitname)

  if unit then
  
    -- Element table.
    local element=self:GetElementByName(unitname)

    -- Add element to table.
    if element then
      -- We already know this element.
    else
      -- Add a new element.
      element={}
      element.status=OPSGROUP.ElementStatus.INUTERO
      table.insert(self.elements, element)
    end

    -- Name and status.
    element.name=unitname

    -- Unit and group.
    element.unit=unit
    element.DCSunit=Unit.getByName(unitname)
    element.gid=element.DCSunit:getNumber()
    element.uid=element.DCSunit:getID()
    --element.group=unit:GetGroup()
    element.controller=element.DCSunit:getController()
    element.Nhit=0
    element.opsgroup=self

    -- Get unit template.
    local unittemplate=unit:GetTemplate()

    if unittemplate==nil then
      if element.DCSunit:getPlayerName() then
        element.skill="Client"
      end
    else
      element.skill=unittemplate~=nil and unittemplate.skill or "Unknown"
    end

    -- Skill etc.
    if element.skill=="Client" or element.skill=="Player" then
      element.ai=false
      element.client=CLIENT:FindByName(unitname)
      element.playerName=element.DCSunit:getPlayerName()
    else
      element.ai=true
    end

    -- Descriptors and type/category.
    element.descriptors=unit:GetDesc()
    element.category=unit:GetUnitCategory()
    element.categoryname=unit:GetCategoryName()
    element.typename=unit:GetTypeName()

    -- Describtors.
    --self:I({desc=element.descriptors})

    -- Ammo.
    element.ammo0=self:GetAmmoUnit(unit, false)

    -- Life points.
    element.life=unit:GetLife()
    element.life0=math.max(unit:GetLife0(), element.life) -- Some units report a life0 that is smaller than its initial life points.

    -- Size and dimensions.
    element.size, element.length, element.height, element.width=unit:GetObjectSize()

    -- Weight and cargo.
    element.weightEmpty=element.descriptors.massEmpty or 666

    if self.isArmygroup then

      element.weightMaxTotal=element.weightEmpty+10*95 --If max mass is not given, we assume 10 soldiers.

    elseif self.isNavygroup then

      element.weightMaxTotal=element.weightEmpty+10*1000

    else

      -- Looks like only aircraft have a massMax value in the descriptors.
      element.weightMaxTotal=element.descriptors.massMax or element.weightEmpty+8*95 --If max mass is not given, we assume 8 soldiers.

    end

    -- Max cargo weight:
    unit:SetCargoBayWeightLimit()
    element.weightMaxCargo=unit.__.CargoBayWeightLimit

    -- Cargo bay (empty).
    if element.cargoBay then
      -- After a respawn, the cargo bay might not be empty!
      element.weightCargo=self:GetWeightCargo(element.name, false)
    else
      element.cargoBay={}
      element.weightCargo=0
    end
    element.weight=element.weightEmpty+element.weightCargo
    
    -- FLIGHTGROUP specific.
    element.callsign=element.unit:GetCallsign()
    element.fuelmass=element.fuelmass0 or 99999
    element.fuelrel=element.unit:GetFuel() or 1
    
    if self.isFlightgroup and unittemplate then
      element.modex=unittemplate.onboard_num
      element.payload=unittemplate.payload
      element.pylons=unittemplate.payload and unittemplate.payload.pylons or nil
      element.fuelmass0=unittemplate.payload and unittemplate.payload.fuel or 0
    else
      element.callsign="Peter-1-1"
      element.modex="000"
      element.payload={}
      element.pylons={}
    end

    -- Debug text.
    local text=string.format("Adding element %s: status=%s, skill=%s, life=%.1f/%.1f category=%s (%d), type=%s, size=%.1f (L=%.1f H=%.1f W=%.1f), weight=%.1f/%.1f (cargo=%.1f/%.1f)",
    element.name, element.status, element.skill, element.life, element.life0, element.categoryname, element.category, element.typename,
    element.size, element.length, element.height, element.width, element.weight, element.weightMaxTotal, element.weightCargo, element.weightMaxCargo)
    self:T(self.lid..text)

    -- Trigger spawned event if alive.
    if unit:IsAlive() and element.status~=OPSGROUP.ElementStatus.SPAWNED then
      -- This needs to be slightly delayed (or moved elsewhere) or the first element will always trigger the group spawned event as it is not known that more elements are in the group.
      self:__ElementSpawned(0.05, element)
    end

    return element
  end

  return nil
end

--- Set the template of the group.
-- @param #OPSGROUP self
-- @param #table Template Template to set. Default is from the GROUP.
-- @return #OPSGROUP self
function OPSGROUP:_SetTemplate(Template)

  -- Set the template.
  self.template=Template or UTILS.DeepCopy(_DATABASE:GetGroupTemplate(self.groupname))  --self.group:GetTemplate()

  -- Debug info.
  self:T3(self.lid.."Setting group template")

  return self
end

--- Get the template of the group.
-- @param #OPSGROUP self
-- @param #boolean Copy Get a deep copy of the template.
-- @return #table Template table.
function OPSGROUP:_GetTemplate(Copy)

  if self.template then

    if Copy then
      local template=UTILS.DeepCopy(self.template)
      return template
    else
      return self.template
    end

  else
    self:T(self.lid..string.format("ERROR: No template was set yet!"))
  end

  return nil
end

--- Clear waypoints.
-- @param #OPSGROUP self
-- @param #number IndexMin Clear waypoints up to this min WP index. Default 1.
-- @param #number IndexMax Clear waypoints up to this max WP index. Default `#self.waypoints`.
function OPSGROUP:ClearWaypoints(IndexMin, IndexMax)

  IndexMin=IndexMin or 1
  IndexMax=IndexMax or #self.waypoints

  -- Clear all waypoints.
  for i=IndexMax,IndexMin,-1 do
    table.remove(self.waypoints, i)
  end
  --self.waypoints={}
end

--- Get target group.
-- @param #OPSGROUP self
-- @return Wrapper.Group#GROUP Detected target group.
-- @return #number Distance to target.
function OPSGROUP:_GetDetectedTarget()

  -- Target.
  local targetgroup=nil --Wrapper.Group#GROUP
  local targetdist=math.huge

  -- Loop over detected groups.
  for _,_group in pairs(self.detectedgroups:GetSet()) do
    local group=_group --Wrapper.Group#GROUP

    if group and group:IsAlive() then

      -- Get 3D vector of target.
      local targetVec3=group:GetVec3()

      -- Distance to target.
      local distance=UTILS.VecDist3D(self.position, targetVec3)

      if distance<=self.engagedetectedRmax and distance<targetdist then

        -- Check type attribute.
        local righttype=false
        for _,attribute in pairs(self.engagedetectedTypes) do
          local gotit=group:HasAttribute(attribute, false)
          self:T(self.lid..string.format("Group %s has attribute %s = %s", group:GetName(), attribute, tostring(gotit)))
          if gotit then
            righttype=true
            break
          end
        end

        -- We got the right type.
        if righttype then

          local insideEngage=true
          local insideNoEngage=false

          -- Check engage zones.
          if self.engagedetectedEngageZones then
            insideEngage=false
            for _,_zone in pairs(self.engagedetectedEngageZones.Set) do
              local zone=_zone --Core.Zone#ZONE
              local inzone=zone:IsVec3InZone(targetVec3)
              if inzone then
                insideEngage=true
                break
              end
            end
          end

          -- Check no engage zones.
          if self.engagedetectedNoEngageZones then
            for _,_zone in pairs(self.engagedetectedNoEngageZones.Set) do
              local zone=_zone --Core.Zone#ZONE
              local inzone=zone:IsVec3InZone(targetVec3)
              if inzone then
                insideNoEngage=true
                break
              end
            end
          end

          -- If inside engage but not inside no engage zones.
          if insideEngage and not insideNoEngage then
            targetdist=distance
            targetgroup=group
          end

        end

      end
    end
  end

  return targetgroup, targetdist
end

--- This function uses Disposition and other fallback logic to find better ground positions for ground units.
--- NOTE: This is not a spawn randomizer.
--- It will try to find clear ground locations avoiding trees, water, roads, runways, map scenery, statics and other units in the area and modifies the provided positions table.
--- Maintains the original layout and unit positions as close as possible by searching for the next closest valid position to each unit.
--- Uses UTILS.ValidateAndRepositionGroundUnits.
-- @param #boolean Enabled Enable/disable the feature.
function OPSGROUP:SetValidateAndRepositionGroundUnits(Enabled)
    self.ValidateAndRepositionGroundUnits = Enabled
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Transport assignment for OPS groups and storage.
-- 
-- ## Main Features:
--
--    * Transport troops from A to B
--    * Transport of warehouse storage (fuel, weapons and equipment)
--    * Supports ground, naval and airborne (airplanes and helicopters) units as carriers
--    * Use combined forces (ground, naval, air) to transport the troops
--    * Additional FSM events to hook into and customize your mission design
--
-- ===
--
-- ## Example Missions:
-- 
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/Transport).
--    
-- ===
--
-- ### Author: **funkyfranky**
-- 
-- ===
-- @module Ops.OpsTransport
-- @image OPS_OpsTransport.png


--- OPSTRANSPORT class.
-- @type OPSTRANSPORT
-- @field #string ClassName Name of the class.
-- @field #string lid Log ID.
-- @field #number uid Unique ID of the transport.
-- @field #number verbose Verbosity level.
-- 
-- @field #number prio Priority of this transport. Should be a number between 0 (high prio) and 100 (low prio).
-- @field #boolean urgent If true, transport is urgent.
-- @field #number importance Importance of this transport. Smaller=higher.
-- @field #number Tstart Start time in *abs.* seconds.
-- @field #number Tstop Stop time in *abs.* seconds. Default `#nil` (never stops).
-- @field #number duration Duration (`Tstop-Tstart`) of the transport in seconds.
-- @field #table conditionStart Start conditions.
-- 
-- @field #table carriers Carriers assigned for this transport.
-- @field #table carrierTransportStatus Status of each carrier.
--  
-- @field #table tzCombos Table of transport zone combos. Each element of the table is of type `#OPSTRANSPORT.TransportZoneCombo`.
-- @field #number tzcCounter Running number of added transport zone combos.
-- @field #OPSTRANSPORT.TransportZoneCombo tzcDefault Default transport zone combo.
-- 
-- @field #number Ncargo Total number of cargo groups.
-- @field #number Ncarrier Total number of assigned carriers.
-- @field #number Ndelivered Total number of cargo groups delivered.
-- @field #number NcarrierDead Total number of dead carrier groups
-- @field #number NcargoDead Totalnumber of dead cargo groups.
-- 
-- @field #string formationArmy Default formation for ground vehicles.
-- @field #string formationHelo Default formation for helicopters.
-- @field #string formationPlane Default formation for airplanes.
-- 
-- @field Ops.Auftrag#AUFTRAG mission The mission attached to this transport.
-- @field #table assets Warehouse assets assigned for this transport.
-- @field #table legions Assigned legions.
-- @field #table statusLegion Transport status of all assigned LEGIONs.
-- @field #string statusCommander Staus of the COMMANDER.
-- @field Ops.Commander#COMMANDER commander Commander of the transport.
-- @field Ops.Chief#CHIEF chief Chief of the transport.
-- @field Ops.OpsZone#OPSZONE opszone OPS zone.
-- @field #table requestID The ID of the queued warehouse request. Necessary to cancel the request if the transport was cancelled before the request is processed.
-- @field #number cargocounter Running number to generate cargo UIDs.
-- 
-- @extends Core.Fsm#FSM

--- *Victory is the beautiful, bright-colored flower; Transport is the stem without which it could never have blossomed* -- Winston Churchill
--
-- ===
--
-- # The OPSTRANSPORT Concept
-- 
-- This class simulates troop transport using carriers such as APCs, ships, helicopters or airplanes. The carriers and transported groups need to be OPSGROUPS (see ARMYGROUP, NAVYGROUP and FLIGHTGROUP classes).
-- 
-- **IMPORTANT NOTES**
-- 
-- * Cargo groups are **not** split and distributed into different carrier *units*. That means that the whole cargo group **must fit** into one of the carrier units.
-- * Cargo groups must be inside the pickup zones to be considered for loading. Groups not inside the pickup zone will not get the command to board. 
-- 
-- # Troop Transport
-- 
-- A new cargo transport assignment is created with the @{#OPSTRANSPORT.New}() function
-- 
--     local opstransport=OPSTRANSPORT:New(Cargo, PickupZone, DeployZone)
-- 
-- Here `Cargo` is an object of the troops to be transported. This can be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object.
-- 
-- `PickupZone` is the zone where the troops are picked up by the transport carriers. **Note** that troops *must* be inside this zone to be considered for loading!
-- 
-- `DeployZone` is the zone where the troops are transported to.
-- 
-- ## Assign to Carrier(s)
-- 
-- A transport can be assigned to one or multiple carrier OPSGROUPS with this @{Ops.OpsGroup#OPSGROUP.AddOpsTransport}() function
-- 
--     myopsgroup:AddOpsTransport(opstransport)
-- 
-- There is no restriction to the type of the carrier. It can be a ground group (e.g. an APC), a helicopter, an airplane or even a ship.
-- 
-- You can also mix carrier types. For instance, you can assign the same transport to APCs and helicopters. Or to helicopters and airplanes.
-- 
-- # Storage Transport
-- 
-- An instance of the OPSTRANSPORT class is created similarly to the troop transport case. However, the first parameter is `nil` as not troops 
-- are transported.
-- 
--     local storagetransport=OPSTRANSPORT:New(nil, PickupZone, DeployZone) 
-- 
-- ## Defining Storage
-- 
-- The storage warehouses from which the cargo is taken and to which the cargo is delivered have to be specified
-- 
--     storagetransport:AddCargoStorage(berlinStorage, batumiStorage, STORAGE.Liquid.JETFUEL, 1000)
--     
-- Here `berlinStorage` and `batumiStorage` are @{Wrapper.Storage#STORAGE} objects of DCS warehouses. 
-- 
-- Furthermore, that type of cargo (liquids or weapons/equipment) and the amount has to be specified. If weapons/equipment is the cargo,
-- we also need to specify the weight per storage item as this cannot be retrieved from the DCS API and is not stored in any MOOSE database.
-- 
--     storagetransport:AddCargoStorage(berlinStorage, batumiStorage, ENUMS.Storage.weapons.bombs.Mk_82, 9, 230)     
--
-- Finally, the transport is assigned to one or multiple groups, which carry out the transport
-- 
--     myopsgroup:AddOpsTransport(storagetransport)
-- 
-- # Examples
--
-- A carrier group is assigned to transport infantry troops from zone "Zone Kobuleti X" to zone "Zone Alpha".
--
--     -- Carrier group.
--     local carrier=ARMYGROUP:New("TPz Fuchs Group")
--       
--     -- Set of groups to transport.
--     local infantryset=SET_GROUP:New():FilterPrefixes("Infantry Platoon Alpha"):FilterOnce()
--     
--     -- Cargo transport assignment.
--     local opstransport=OPSTRANSPORT:New(infantryset, ZONE:New("Zone Kobuleti X"), ZONE:New("Zone Alpha"))
--     
--     -- Assign transport to carrier.
--     carrier:AddOpsTransport(opstransport)
-- 
-- 
-- @field #OPSTRANSPORT
OPSTRANSPORT = {
  ClassName       = "OPSTRANSPORT",
  verbose         =   0,
  carriers        =  {},
  carrierTransportStatus = {},
  tzCombos        =  {},
  tzcCounter      =   0,
  conditionStart  =  {},
  assets          =  {},
  legions         =  {},
  statusLegion    =  {},
  requestID       =  {},
  cargocounter    =   0,
}

--- Cargo transport status.
-- @type OPSTRANSPORT.Status
-- @field #string PLANNED Planning state.
-- @field #string QUEUED Queued state.
-- @field #string REQUESTED Requested state.
-- @field #string SCHEDULED Transport is scheduled in the cargo queue.
-- @field #string EXECUTING Transport is being executed.
-- @field #string DELIVERED Transport was delivered. 
-- @field #string CANCELLED Transport was cancelled.
-- @field #string SUCCESS Transport was a success.
-- @field #string FAILED Transport failed.
OPSTRANSPORT.Status={
  PLANNED="planned",
  QUEUED="queued",
  REQUESTED="requested",
  SCHEDULED="scheduled",
  EXECUTING="executing",
  DELIVERED="delivered",
  CANCELLED="cancelled",
  SUCCESS="success",
  FAILED="failed",  
}

--- Transport zone combination.
-- @type OPSTRANSPORT.TransportZoneCombo
-- @field #number uid Unique ID of the TZ combo.
-- @field #number Ncarriers Number of carrier groups using this transport zone.
-- @field #number Ncargo Number of cargos assigned. This is a running number and *not* decreased if cargo is delivered or dead.
-- @field #table Cargos Cargo groups of the TZ combo. Each element is of type `Ops.OpsGroup#OPSGROUP.CargoGroup`.
-- @field Core.Zone#ZONE PickupZone Pickup zone.
-- @field Core.Zone#ZONE DeployZone Deploy zone.
-- @field Core.Zone#ZONE EmbarkZone Embark zone if different from pickup zone.
-- @field Core.Zone#ZONE DisembarkZone Zone where the troops are disembared to.
-- @field Wrapper.Airbase#AIRBASE PickupAirbase Airbase for pickup.
-- @field Wrapper.Airbase#AIRBASE DeployAirbase Airbase for deploy.
-- @field #table PickupPaths Paths for pickup. 
-- @field #table TransportPaths Path for Transport. Each elment of the table is of type `#OPSTRANSPORT.Path`. 
-- @field #table RequiredCargos Required cargos.
-- @field #table DisembarkCarriers Carriers where the cargo is directly disembarked to.
-- @field #boolean disembarkToCarriers If `true`, cargo is supposed to embark to another carrier.
-- @field #boolean disembarkActivation If true, troops are spawned in late activated state when disembarked from carrier.
-- @field #boolean disembarkInUtero If true, troops are disembarked "in utero".
-- @field #boolean assets Cargo assets.
-- @field #number PickupFormation Formation used to pickup.
-- @field #number TransportFormation Formation used to transport.

--- Path used for pickup or transport.
-- @type OPSTRANSPORT.Path
-- @field #table waypoints Table of waypoints.
-- @field #number category Category for which carriers this path is used.
-- @field #number radius Radomization radius for waypoints in meters. Default 0 m.
-- @field #boolean reverse If `true`, path is used in reversed order.

--- Storage data.
-- @type OPSTRANSPORT.Storage
-- @field Wrapper.Storage#STORAGE storageFrom Storage from.
-- @field Wrapper.Storage#STORAGE storageTo Storage To.
-- @field #string cargoType Type of cargo.
-- @field #number cargoAmount Amount of cargo that should be transported.
-- @field #number cargoReserved Amount of cargo that is reserved for a carrier group.
-- @field #number cargoDelivered Amount of cargo that has been delivered.
-- @field #number cargoLost Amount of cargo that was lost.
-- @field #number cargoLoaded Amount of cargo that is loading.
-- @field #number cargoWeight Weight of one single cargo item in kg. Default 1 kg.

--- Storage data.
-- @type OPSTRANSPORT.CargoType
-- @field #string OPSGROUP Cargo is an OPSGROUP.
-- @field #string STORAGE Cargo is storage of DCS warehouse.
OPSTRANSPORT.CargoType={
  OPSGROUP="OPSGROUP",
  STORAGE="STORAGE",
}

--- Generic transport condition.
-- @type OPSTRANSPORT.Condition
-- @field #function func Callback function to check for a condition. Should return a #boolean.
-- @field #table arg Optional arguments passed to the condition callback function.

--- Transport ID.
_OPSTRANSPORTID=0

--- Army Group version.
-- @field #string version
OPSTRANSPORT.version="0.8.0"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Trains.
-- TODO: Stop transport.
-- TODO: Improve pickup and transport paths.
-- DONE: Storage.
-- DONE: Disembark parameters per cargo group.
-- DONE: Special transport cohorts/legions. Similar to mission.
-- DONE: Cancel transport.
-- DONE: Allow multiple pickup/depoly zones.
-- DONE: Add start conditions.
-- DONE: Check carrier(s) dead.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new OPSTRANSPORT class object. Essential input are the troops that should be transported and the zones where the troops are picked up and deployed.
-- @param #OPSTRANSPORT self
-- @param Core.Set#SET_GROUP CargoGroups Groups to be transported as cargo. Can also be a single @{Wrapper.Group#GROUP} or @{Ops.OpsGroup#OPSGROUP} object.
-- @param Core.Zone#ZONE PickupZone Pickup zone. This is the zone, where the carrier is going to pickup the cargo. **Important**: only cargo is considered, if it is in this zone when the carrier starts loading!
-- @param Core.Zone#ZONE DeployZone Deploy zone. This is the zone, where the carrier is going to drop off the cargo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:New(CargoGroups, PickupZone, DeployZone)

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #OPSTRANSPORT
  
  -- Increase ID counter.
  _OPSTRANSPORTID=_OPSTRANSPORTID+1
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("OPSTRANSPORT [UID=%d] | ", _OPSTRANSPORTID)
  
  -- UID of this transport.
  self.uid=_OPSTRANSPORTID
      
  -- Defaults.
  self:SetPriority()
  self:SetTime()
  self:SetRequiredCarriers()
  
  self.formationArmy=ENUMS.Formation.Vehicle.OnRoad
  self.formationHelo=ENUMS.Formation.RotaryWing.Wedge
  self.formationPlane=ENUMS.Formation.FixedWing.Wedge
  
  -- Init arrays and counters.
  self.carriers={}  
  self.Ncargo=0
  self.Ncarrier=0
  self.Ndelivered=0
  self.NcargoDead=0
  self.NcarrierDead=0
  
  -- Set default TZC.
  self.tzcDefault=self:AddTransportZoneCombo(CargoGroups, PickupZone, DeployZone)  
  
  -- FMS start state is PLANNED.
  self:SetStartState(OPSTRANSPORT.Status.PLANNED)
  
  -- PLANNED --> SCHEDULED --> EXECUTING --> DELIVERED  
  self:AddTransition("*",                           "Planned",          OPSTRANSPORT.Status.PLANNED)     -- Cargo transport was planned.
  self:AddTransition(OPSTRANSPORT.Status.PLANNED,   "Queued",           OPSTRANSPORT.Status.QUEUED)      -- Cargo is queued at at least one carrier.
  self:AddTransition(OPSTRANSPORT.Status.QUEUED,    "Requested",        OPSTRANSPORT.Status.REQUESTED)   -- Transport assets have been requested from a warehouse.
  self:AddTransition(OPSTRANSPORT.Status.REQUESTED, "Scheduled",        OPSTRANSPORT.Status.SCHEDULED)   -- Cargo is queued at at least one carrier.
  self:AddTransition(OPSTRANSPORT.Status.PLANNED,   "Scheduled",        OPSTRANSPORT.Status.SCHEDULED)   -- Cargo is queued at at least one carrier.  
  self:AddTransition(OPSTRANSPORT.Status.SCHEDULED, "Executing",        OPSTRANSPORT.Status.EXECUTING)   -- Cargo is being transported.  
  self:AddTransition("*",                           "Delivered",        OPSTRANSPORT.Status.DELIVERED)   -- Cargo was delivered.
  
  self:AddTransition("*",                           "StatusUpdate",     "*")
  self:AddTransition("*",                           "Stop",             "*")
  
  self:AddTransition("*",                           "Cancel",           OPSTRANSPORT.Status.CANCELLED)   -- Command to cancel the transport.  
  
  self:AddTransition("*",                           "Loaded",           "*")
  self:AddTransition("*",                           "Unloaded",         "*")
  
  self:AddTransition("*",                           "DeadCarrierUnit",  "*")
  self:AddTransition("*",                           "DeadCarrierGroup", "*")
  self:AddTransition("*",                           "DeadCarrierAll",   "*")

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "StatusUpdate".
  -- @function [parent=#OPSTRANSPORT] StatusUpdate
  -- @param #OPSTRANSPORT self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#OPSTRANSPORT] __StatusUpdate
  -- @param #OPSTRANSPORT self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Planned".
  -- @function [parent=#OPSTRANSPORT] Planned
  -- @param #OPSTRANSPORT self

  --- Triggers the FSM event "Planned" after a delay.
  -- @function [parent=#OPSTRANSPORT] __Planned
  -- @param #OPSTRANSPORT self
  -- @param #number delay Delay in seconds.

  --- On after "Planned" event.
  -- @function [parent=#OPSTRANSPORT] OnAfterPlanned
  -- @param #OPSTRANSPORT self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Queued".
  -- @function [parent=#OPSTRANSPORT] Queued
  -- @param #OPSTRANSPORT self

  --- Triggers the FSM event "Queued" after a delay.
  -- @function [parent=#OPSTRANSPORT] __Queued
  -- @param #OPSTRANSPORT self
  -- @param #number delay Delay in seconds.

  --- On after "Queued" event.
  -- @function [parent=#OPSTRANSPORT] OnAfterQueued
  -- @param #OPSTRANSPORT self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Requested".
  -- @function [parent=#OPSTRANSPORT] Requested
  -- @param #OPSTRANSPORT self

  --- Triggers the FSM event "Requested" after a delay.
  -- @function [parent=#OPSTRANSPORT] __Requested
  -- @param #OPSTRANSPORT self
  -- @param #number delay Delay in seconds.

  --- On after "Requested" event.
  -- @function [parent=#OPSTRANSPORT] OnAfterRequested
  -- @param #OPSTRANSPORT self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Scheduled".
  -- @function [parent=#OPSTRANSPORT] Scheduled
  -- @param #OPSTRANSPORT self

  --- Triggers the FSM event "Scheduled" after a delay.
  -- @function [parent=#OPSTRANSPORT] __Scheduled
  -- @param #OPSTRANSPORT self
  -- @param #number delay Delay in seconds.

  --- On after "Scheduled" event.
  -- @function [parent=#OPSTRANSPORT] OnAfterScheduled
  -- @param #OPSTRANSPORT self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Executing".
  -- @function [parent=#OPSTRANSPORT] Executing
  -- @param #OPSTRANSPORT self

  --- Triggers the FSM event "Executing" after a delay.
  -- @function [parent=#OPSTRANSPORT] __Executing
  -- @param #OPSTRANSPORT self
  -- @param #number delay Delay in seconds.

  --- On after "Executing" event.
  -- @function [parent=#OPSTRANSPORT] OnAfterExecuting
  -- @param #OPSTRANSPORT self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Delivered".
  -- @function [parent=#OPSTRANSPORT] Delivered
  -- @param #OPSTRANSPORT self

  --- Triggers the FSM event "Delivered" after a delay.
  -- @function [parent=#OPSTRANSPORT] __Delivered
  -- @param #OPSTRANSPORT self
  -- @param #number delay Delay in seconds.

  --- On after "Delivered" event.
  -- @function [parent=#OPSTRANSPORT] OnAfterDelivered
  -- @param #OPSTRANSPORT self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Cancel".
  -- @function [parent=#OPSTRANSPORT] Cancel
  -- @param #OPSTRANSPORT self

  --- Triggers the FSM event "Cancel" after a delay.
  -- @function [parent=#OPSTRANSPORT] __Cancel
  -- @param #OPSTRANSPORT self
  -- @param #number delay Delay in seconds.

  --- On after "Cancel" event.
  -- @function [parent=#OPSTRANSPORT] OnAfterCancel
  -- @param #OPSTRANSPORT self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Loaded".
  -- @function [parent=#OPSTRANSPORT] Loaded
  -- @param #OPSTRANSPORT self
  -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier.
  -- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element.

  --- Triggers the FSM event "Loaded" after a delay.
  -- @function [parent=#OPSTRANSPORT] __Loaded
  -- @param #OPSTRANSPORT self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier.
  -- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element.

  --- On after "Loaded" event.
  -- @function [parent=#OPSTRANSPORT] OnAfterLoaded
  -- @param #OPSTRANSPORT self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier.
  -- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element.


  --- Triggers the FSM event "Unloaded".
  -- @function [parent=#OPSTRANSPORT] Unloaded
  -- @param #OPSTRANSPORT self
  -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo.

  --- Triggers the FSM event "Unloaded" after a delay.
  -- @function [parent=#OPSTRANSPORT] __Unloaded
  -- @param #OPSTRANSPORT self
  -- @param #number delay Delay in seconds.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo.

  --- On after "Unloaded" event.
  -- @function [parent=#OPSTRANSPORT] OnAfterUnloaded
  -- @param #OPSTRANSPORT self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier.
  -- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo.

  
  --TODO: Psydofunctions

  -- Call status update.
  self:__StatusUpdate(-1)
  
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Add pickup and deploy zone combination.
-- @param #OPSTRANSPORT self
-- @param Core.Set#SET_GROUP CargoGroups Groups to be transported as cargo. Can also be a single @{Wrapper.Group#GROUP} or @{Ops.OpsGroup#OPSGROUP} object.
-- @param Core.Zone#ZONE PickupZone Zone where the troops are picked up.
-- @param Core.Zone#ZONE DeployZone Zone where the troops are picked up.
-- @return #OPSTRANSPORT.TransportZoneCombo Transport zone table.
function OPSTRANSPORT:AddTransportZoneCombo(CargoGroups, PickupZone, DeployZone)

  -- Increase counter.
  self.tzcCounter=self.tzcCounter+1  
  
  local tzcombo={} --#OPSTRANSPORT.TransportZoneCombo

  -- Init.
  tzcombo.uid=self.tzcCounter
  tzcombo.Ncarriers=0
  tzcombo.Ncargo=0
  tzcombo.Cargos={}
  tzcombo.RequiredCargos={}
  tzcombo.DisembarkCarriers={}
  tzcombo.PickupPaths={}
  tzcombo.TransportPaths={}
  
  -- Set zones.
  self:SetPickupZone(PickupZone, tzcombo)
  self:SetDeployZone(DeployZone, tzcombo)
  self:SetEmbarkZone(nil, tzcombo)
  
  -- Add cargo groups (could also be added later).
  if CargoGroups then
    self:AddCargoGroups(CargoGroups, tzcombo)
  end  
    
  -- Add to table.
  table.insert(self.tzCombos, tzcombo)
  
  return tzcombo
end

--- Add cargo groups to be transported.
-- @param #OPSTRANSPORT self
-- @param Core.Set#SET_GROUP GroupSet Set of groups to be transported. Can also be passed as a single GROUP or OPSGROUP object.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @param #boolean DisembarkActivation If `true`, cargo group is activated when disembarked. If `false`, cargo groups are late activated when disembarked. Default `nil` (usually activated).
-- @param Core.Zone#ZONE DisembarkZone Zone where the groups disembark to.
-- @param Core.Set#SET_OPSGROUP DisembarkCarriers Carrier groups where the cargo directly disembarks to.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:AddCargoGroups(GroupSet, TransportZoneCombo, DisembarkActivation, DisembarkZone, DisembarkCarriers)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  -- Check type of GroupSet provided.
  if GroupSet:IsInstanceOf("GROUP") or GroupSet:IsInstanceOf("OPSGROUP") then

    -- We got a single GROUP or OPSGROUP object.
    local cargo=self:_CreateCargoGroupData(GroupSet, TransportZoneCombo, DisembarkActivation, DisembarkZone, DisembarkCarriers)
    
    if cargo then
    
      -- Add to main table.
      --table.insert(self.cargos, cargo)
      self.Ncargo=self.Ncargo+1
      
      -- Add to TZC table.
      table.insert(TransportZoneCombo.Cargos, cargo)
      TransportZoneCombo.Ncargo=TransportZoneCombo.Ncargo+1
      
      cargo.opsgroup:_AddMyLift(self)
      
    end
    
  else
  
    -- We got a SET_GROUP object.
    
    for _,group in pairs(GroupSet.Set) do
    
      -- Call iteravely for each group.
      self:AddCargoGroups(group, TransportZoneCombo, DisembarkActivation)
      
    end
    
    -- Use FSM function to keep the SET up-to-date. Note that it overwrites the user FMS function, which cannot be used any more now.
    local groupset=GroupSet --Core.Set#SET_OPSGROUP
    function groupset.OnAfterAdded(groupset, From, Event, To, ObjectName, Object)
    
      self:T(self.lid..string.format("Adding Cargo Group %s", tostring(ObjectName)))
      self:AddCargoGroups(Object, TransportZoneCombo, DisembarkActivation, DisembarkZone, DisembarkCarriers)
    
    end    
    
  end
  
  -- Debug info.
  if self.verbose>=1 then
    local text=string.format("Added cargo groups:")
    local Weight=0
    for _,_cargo in pairs(self:GetCargos()) do
      local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup
      local weight=cargo.opsgroup:GetWeightTotal()
      Weight=Weight+weight
      text=text..string.format("\n- %s [%s] weight=%.1f kg", cargo.opsgroup:GetName(), cargo.opsgroup:GetState(), weight)
    end
    text=text..string.format("\nTOTAL: Ncargo=%d, Weight=%.1f kg", self.Ncargo, Weight)
    self:I(self.lid..text)
  end


  return self
end

--- Add cargo warehouse storage to be transported. This adds items such as fuel, weapons and other equipment, which is to be transported
-- from one DCS warehouse to another.
-- For weapons and equipment, the weight per item has to be specified explicitly as these cannot be retrieved by the DCS API. For liquids the
-- default value of 1 kg per item should be used as the amount of liquid is already given in kg.
-- @param #OPSTRANSPORT self
-- @param Wrapper.Storage#STORAGE StorageFrom Storage warehouse from which the cargo is taken.
-- @param Wrapper.Storage#STORAGE StorageTo Storage warehouse to which the cargo is delivered.
-- @param #string CargoType Type of cargo, *e.g.* `"weapons.bombs.Mk_84"` or liquid type as #number.
-- @param #number CargoAmount Amount of cargo. Liquids in kg.
-- @param #number CargoWeight Weight of a single cargo item in kg. Default 1 kg.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo if other than default.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:AddCargoStorage(StorageFrom, StorageTo, CargoType, CargoAmount, CargoWeight, TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  -- Cargo data.
  local cargo=self:_CreateCargoStorage(StorageFrom,StorageTo, CargoType, CargoAmount, CargoWeight, TransportZoneCombo)
  
  if cargo then
  
    -- Add total amount of ever assigned cargos.
    self.Ncargo=self.Ncargo+1

    -- Add to TZC table.
    table.insert(TransportZoneCombo.Cargos, cargo)

  end

end


--- Set pickup zone.
-- @param #OPSTRANSPORT self
-- @param Core.Zone#ZONE PickupZone Zone where the troops are picked up.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetPickupZone(PickupZone, TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  TransportZoneCombo.PickupZone=PickupZone

  if PickupZone and PickupZone:IsInstanceOf("ZONE_AIRBASE") then
    TransportZoneCombo.PickupAirbase=PickupZone._.ZoneAirbase
  end
  
  return self
end

--- Get pickup zone.
-- @param #OPSTRANSPORT self
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return Core.Zone#ZONE Zone where the troops are picked up.
function OPSTRANSPORT:GetPickupZone(TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  return TransportZoneCombo.PickupZone
end

--- Set deploy zone.
-- @param #OPSTRANSPORT self
-- @param Core.Zone#ZONE DeployZone Zone where the troops are deployed.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetDeployZone(DeployZone, TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  -- Set deploy zone.
  TransportZoneCombo.DeployZone=DeployZone

  -- Check if this is an airbase.
  if DeployZone and DeployZone:IsInstanceOf("ZONE_AIRBASE") then
    TransportZoneCombo.DeployAirbase=DeployZone._.ZoneAirbase
  end
  
  return self
end

--- Get deploy zone.
-- @param #OPSTRANSPORT self
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return Core.Zone#ZONE Zone where the troops are deployed.
function OPSTRANSPORT:GetDeployZone(TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  return TransportZoneCombo.DeployZone
end

--- Set embark zone.
-- @param #OPSTRANSPORT self
-- @param Core.Zone#ZONE EmbarkZone Zone where the troops are embarked.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetEmbarkZone(EmbarkZone, TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  TransportZoneCombo.EmbarkZone=EmbarkZone or TransportZoneCombo.PickupZone
  
  return self
end

--- Get embark zone.
-- @param #OPSTRANSPORT self
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return Core.Zone#ZONE Zone where the troops are embarked from.
function OPSTRANSPORT:GetEmbarkZone(TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  return TransportZoneCombo.EmbarkZone
end

--- Set disembark zone.
-- @param #OPSTRANSPORT self
-- @param Core.Zone#ZONE DisembarkZone Zone where the troops are disembarked.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetDisembarkZone(DisembarkZone, TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  TransportZoneCombo.DisembarkZone=DisembarkZone
  
  return self
end

--- Get disembark zone.
-- @param #OPSTRANSPORT self
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return Core.Zone#ZONE Zone where the troops are disembarked to.
function OPSTRANSPORT:GetDisembarkZone(TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  return TransportZoneCombo.DisembarkZone
end

--- Set activation status of group when disembarked from transport carrier.
-- @param #OPSTRANSPORT self
-- @param #boolean Active If `true` or `nil`, group is activated when disembarked. If `false`, group is late activated and needs to be activated manually.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetDisembarkActivation(Active, TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  if Active==true or Active==nil then
    TransportZoneCombo.disembarkActivation=true
  else
    TransportZoneCombo.disembarkActivation=false
  end
  
  return self
end

--- Get disembark activation.
-- @param #OPSTRANSPORT self
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #boolean If `true`, groups are spawned in late activated state.
function OPSTRANSPORT:GetDisembarkActivation(TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  return TransportZoneCombo.disembarkActivation
end

--- Set/add transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked.
-- @param #OPSTRANSPORT self
-- @param Core.Set#SET_GROUP Carriers Carrier set. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetDisembarkCarriers(Carriers, TransportZoneCombo)

  -- Debug info.
  self:T(self.lid.."Setting transfer carriers!")
  
  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault
  
  -- Set that we want to disembark to carriers.
  TransportZoneCombo.disembarkToCarriers=true
  
  self:_AddDisembarkCarriers(Carriers, TransportZoneCombo.DisembarkCarriers)

  return self
end

--- Set/add transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked.
-- @param #OPSTRANSPORT self
-- @param Core.Set#SET_GROUP Carriers Carrier set. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object.
-- @param #table Table the table to add.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:_AddDisembarkCarriers(Carriers, Table)

  if Carriers:IsInstanceOf("GROUP") or Carriers:IsInstanceOf("OPSGROUP") then
  
    local carrier=self:_GetOpsGroupFromObject(Carriers)
    if  carrier then
      table.insert(Table, carrier)
    end
      
  elseif Carriers:IsInstanceOf("SET_GROUP") or Carriers:IsInstanceOf("SET_OPSGROUP") then
  
    for _,object in pairs(Carriers:GetSet()) do
      local carrier=self:_GetOpsGroupFromObject(object)
      if carrier then
        table.insert(Table, carrier)
      end
    end
    
  else  
    self:E(self.lid.."ERROR: Carriers must be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object!")    
  end


end

--- Get transfer carrier(s). These are carrier groups, where the cargo is directly loaded into when disembarked.
-- @param #OPSTRANSPORT self
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #table Table of carrier OPS groups.
function OPSTRANSPORT:GetDisembarkCarriers(TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault
  
  return TransportZoneCombo.DisembarkCarriers
end


--- Set if group remains *in utero* after disembarkment from carrier. Can be used to directly load the group into another carrier. Similar to disembark in late activated state.
-- @param #OPSTRANSPORT self
-- @param #boolean InUtero If `true` or `nil`, group remains *in utero* after disembarkment.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetDisembarkInUtero(InUtero, TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault  

  if InUtero==true or InUtero==nil then
    TransportZoneCombo.disembarkInUtero=true
  else
    TransportZoneCombo.disembarkInUtero=false
  end
  
  return self
end

--- Get disembark in utero.
-- @param #OPSTRANSPORT self
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #boolean If `true`, groups stay in utero after disembarkment.
function OPSTRANSPORT:GetDisembarkInUtero(TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  return TransportZoneCombo.disembarkInUtero
end

--- Set pickup formation.
-- @param #OPSTRANSPORT self
-- @param #number Formation Pickup formation.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetFormationPickup(Formation, TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault  

  TransportZoneCombo.PickupFormation=Formation
  
  return self
end

--- Get pickup formation.
-- @param #OPSTRANSPORT self
-- @param Ops.OpsGroup#OPSGROUP OpsGroup
-- @return #string Formation.
function OPSTRANSPORT:_GetFormationDefault(OpsGroup)

  if OpsGroup.isArmygroup then
  
    return self.formationArmy
    
  elseif OpsGroup.isFlightgroup then
  
    if OpsGroup.isHelo then
      return self.formationHelo
    else
      return self.formationPlane
    end
  
  else
    return ENUMS.Formation.Vehicle.OffRoad
  end
  
  return nil
end

--- Get pickup formation.
-- @param #OPSTRANSPORT self
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @param Ops.OpsGroup#OPSGROUP OpsGroup
-- @return #number Formation.
function OPSTRANSPORT:_GetFormationPickup(TransportZoneCombo, OpsGroup)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault
  
  local formation=TransportZoneCombo.PickupFormation or self:_GetFormationDefault(OpsGroup)

  return formation
end

--- Set transport formation.
-- @param #OPSTRANSPORT self
-- @param #number Formation Pickup formation.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetFormationTransport(Formation, TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault  

  TransportZoneCombo.TransportFormation=Formation
  
  return self
end

--- Get transport formation.
-- @param #OPSTRANSPORT self
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @param Ops.OpsGroup#OPSGROUP OpsGroup
-- @return #number Formation.
function OPSTRANSPORT:_GetFormationTransport(TransportZoneCombo, OpsGroup)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault
  
  local formation=TransportZoneCombo.TransportFormation or self:_GetFormationDefault(OpsGroup)

  return formation
end



--- Set required cargo. This is a list of cargo groups that need to be loaded before the **first** transport will start.
-- @param #OPSTRANSPORT self
-- @param Core.Set#SET_GROUP Cargos Required cargo set. Can also be passed as a #GROUP, #OPSGROUP or #SET_OPSGROUP object.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetRequiredCargos(Cargos, TransportZoneCombo)

  -- Debug info.
  self:T(self.lid.."Setting required cargos!")
  
  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault  

  -- Create table.
  TransportZoneCombo.RequiredCargos=TransportZoneCombo.RequiredCargos or {}

  if Cargos:IsInstanceOf("GROUP") or Cargos:IsInstanceOf("OPSGROUP") then
  
    local cargo=self:_GetOpsGroupFromObject(Cargos)
    if cargo then
      table.insert(TransportZoneCombo.RequiredCargos, cargo)
    end
      
  elseif Cargos:IsInstanceOf("SET_GROUP") or Cargos:IsInstanceOf("SET_OPSGROUP") then
  
    for _,object in pairs(Cargos:GetSet()) do
      local cargo=self:_GetOpsGroupFromObject(object)
      if cargo then
        table.insert(TransportZoneCombo.RequiredCargos, cargo)
      end
    end
    
  else  
    self:E(self.lid.."ERROR: Required Cargos must be a GROUP, OPSGROUP, SET_GROUP or SET_OPSGROUP object!")    
  end

  return self
end

--- Get required cargos. This is a list of cargo groups that need to be loaded before the **first** transport will start.
-- @param #OPSTRANSPORT self
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #table Table of required cargo ops groups.
function OPSTRANSPORT:GetRequiredCargos(TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault  
  
  return TransportZoneCombo.RequiredCargos
end

--- Set number of required carrier groups for an OPSTRANSPORT assignment. Only used if transport is assigned at **LEGION** or higher level.
-- @param #OPSTRANSPORT self
-- @param #number NcarriersMin Number of carriers *at least* required. Default 1.
-- @param #number NcarriersMax Number of carriers *at most* used for transportation. Default is same as `NcarriersMin`.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetRequiredCarriers(NcarriersMin, NcarriersMax)

  self.nCarriersMin=NcarriersMin or 1
  
  self.nCarriersMax=NcarriersMax or self.nCarriersMin

  -- Ensure that max is at least equal to min.
  if self.nCarriersMax<self.nCarriersMin then
    self.nCarriersMax=self.nCarriersMin
  end

  return self
end

--- Get the number of required carrier groups for an OPSTRANSPORT assignment. Only used if transport is assigned at **LEGION** or higher level.
-- @param #OPSTRANSPORT self
-- @return #number Number of carriers *at least* required.
-- @return #number Number of carriers *at most* used for transportation.
function OPSTRANSPORT:GetRequiredCarriers()
  return self.nCarriersMin, self.nCarriersMax
end


--- Add a carrier assigned for this transport.
-- @param #OPSTRANSPORT self
-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:_AddCarrier(CarrierGroup)

  -- Check that this is not already an assigned carrier.
  if not self:IsCarrier(CarrierGroup) then
  
    -- Increase carrier count.
    self.Ncarrier=self.Ncarrier+1

    -- Set transport status to SCHEDULED.
    self:SetCarrierTransportStatus(CarrierGroup, OPSTRANSPORT.Status.SCHEDULED)
    
    -- Call scheduled event.
    self:Scheduled()
    
    -- Add carrier to table.
    table.insert(self.carriers, CarrierGroup)
    
  end 
  
  return self
end

--- Remove group from the current carrier list/table.
-- @param #OPSTRANSPORT self
-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP.
-- @param #number Delay Delay in seconds before the carrier is removed.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:_DelCarrier(CarrierGroup, Delay)

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, OPSTRANSPORT._DelCarrier, self, CarrierGroup)
  else
    if self:IsCarrier(CarrierGroup) then
   
      for i=#self.carriers,1,-1 do
        local carrier=self.carriers[i] --Ops.OpsGroup#OPSGROUP
        if carrier.groupname==CarrierGroup.groupname then
          self:T(self.lid..string.format("Removing carrier %s", CarrierGroup.groupname))
          table.remove(self.carriers, i)
        end
      end
          
    end
  end 
  
  return self
end

--- Get a list of alive carriers.
-- @param #OPSTRANSPORT self
-- @return #table Names of all carriers
function OPSTRANSPORT:_GetCarrierNames()

  local names={}
  for _,_carrier in pairs(self.carriers) do
    local carrier=_carrier --Ops.OpsGroup#OPSGROUP
    if carrier:IsAlive()~=nil then
      table.insert(names, carrier.groupname)
    end
  end
  
  return names
end

--- Get (all) cargo @{Ops.OpsGroup#OPSGROUP}s. Optionally, only delivered or undelivered groups can be returned.
-- @param #OPSTRANSPORT self
-- @param #boolean Delivered If `true`, only delivered groups are returned. If `false` only undelivered groups are returned. If `nil`, all groups are returned.
-- @param Ops.OpsGroup#OPSGROUP Carrier (Optional) Only count cargo groups that fit into the given carrier group. Current cargo is not a factor.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #table Cargo Ops groups. Can be and empty table `{}`.
function OPSTRANSPORT:GetCargoOpsGroups(Delivered, Carrier, TransportZoneCombo)

  local cargos=self:GetCargos(TransportZoneCombo, Carrier, Delivered)

  local opsgroups={}
  for _,_cargo in pairs(cargos) do
    local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup
    if cargo.type=="OPSGROUP" then
      if cargo.opsgroup and not (cargo.opsgroup:IsDead() or cargo.opsgroup:IsStopped()) then
        table.insert(opsgroups, cargo.opsgroup)
      end
    end
  end
    
  return opsgroups
end

--- Get (all) cargo @{Ops.OpsGroup#OPSGROUP}s. Optionally, only delivered or undelivered groups can be returned.
-- @param #OPSTRANSPORT self
-- @param #boolean Delivered If `true`, only delivered groups are returned. If `false` only undelivered groups are returned. If `nil`, all groups are returned.
-- @param Ops.OpsGroup#OPSGROUP Carrier (Optional) Only count cargo groups that fit into the given carrier group. Current cargo is not a factor.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #table Cargo Ops groups. Can be and empty table `{}`.
function OPSTRANSPORT:GetCargoStorages(Delivered, Carrier, TransportZoneCombo)

  local cargos=self:GetCargos(TransportZoneCombo, Carrier, Delivered)

  local opsgroups={}
  for _,_cargo in pairs(cargos) do
    local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup
    if cargo.type=="STORAGE" then
      table.insert(opsgroups, cargo.storage)
    end
  end
    
  return opsgroups
end

--- Get carriers.
-- @param #OPSTRANSPORT self
-- @return #table Carrier Ops groups.
function OPSTRANSPORT:GetCarriers()
  return self.carriers
end

--- Get cargos.
-- @param #OPSTRANSPORT self
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @param Ops.OpsGroup#OPSGROUP Carrier Specific carrier.
-- @param #boolean Delivered Delivered status.
-- @return #table Cargos.
function OPSTRANSPORT:GetCargos(TransportZoneCombo, Carrier, Delivered)
  
  local tczs=self.tzCombos
  if TransportZoneCombo then
    tczs={TransportZoneCombo}
  end

  local cargos={}
  for _,_tcz in pairs(tczs) do
    local tcz=_tcz --#OPSTRANSPORT.TransportZoneCombo
    for _,_cargo in pairs(tcz.Cargos) do
      local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup
      if Delivered==nil or cargo.delivered==Delivered  then
        if Carrier==nil or Carrier:CanCargo(cargo) then
          table.insert(cargos, cargo)
        end
      end
    end
  end

  return cargos
end

--- Get total weight.
-- @param #OPSTRANSPORT self
-- @param Ops.OpsGroup#OPSGROUP.CargoGroup Cargo Cargo data.
-- @param #boolean IncludeReserved Include reserved cargo.
-- @return #number Weight in kg.
function OPSTRANSPORT:GetCargoTotalWeight(Cargo, IncludeReserved)

  local weight=0

  if Cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then
    weight=Cargo.opsgroup:GetWeightTotal(nil, IncludeReserved)
  else
    if type(Cargo.storage.cargoType)=="number" then
      if IncludeReserved then
        return Cargo.storage.cargoAmount+Cargo.storage.cargoReserved
      else
        return Cargo.storage.cargoAmount
      end
    else
      if IncludeReserved then
        return Cargo.storage.cargoAmount*100 -- Assume 100 kg per item
      else
        return (Cargo.storage.cargoAmount+Cargo.storage.cargoReserved)*100 -- Assume 100 kg per item
      end
    end
  end

  return weight
end

--- Set transport start and stop time.
-- @param #OPSTRANSPORT self
-- @param #string ClockStart Time the transport is started, e.g. "05:00" for 5 am. If specified as a #number, it will be relative (in seconds) to the current mission time. Default is 5 seconds after mission was added.
-- @param #string ClockStop (Optional) Time the transport is stopped, e.g. "13:00" for 1 pm. If mission could not be started at that time, it will be removed from the queue. If specified as a #number it will be relative (in seconds) to the current mission time.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetTime(ClockStart, ClockStop)

  -- Current mission time.
  local Tnow=timer.getAbsTime()
  
  -- Set start time. Default in 5 sec.
  local Tstart=Tnow+5
  if ClockStart and type(ClockStart)=="number" then
    Tstart=Tnow+ClockStart
  elseif ClockStart and type(ClockStart)=="string" then
    Tstart=UTILS.ClockToSeconds(ClockStart)
  end

  -- Set stop time. Default nil.
  local Tstop=nil
  if ClockStop and type(ClockStop)=="number" then
    Tstop=Tnow+ClockStop
  elseif ClockStop and type(ClockStop)=="string" then
    Tstop=UTILS.ClockToSeconds(ClockStop)
  end
  
  self.Tstart=Tstart
  self.Tstop=Tstop

  if Tstop then
    self.duration=self.Tstop-self.Tstart
  end  

  return self
end

--- Set mission priority and (optional) urgency. Urgent missions can cancel other running missions. 
-- @param #OPSTRANSPORT self
-- @param #number Prio Priority 1=high, 100=low. Default 50.
-- @param #number Importance Number 1-10. If missions with lower value are in the queue, these have to be finished first. Default is `nil`.
-- @param #boolean Urgent If *true*, another running mission might be cancelled if it has a lower priority.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetPriority(Prio, Importance, Urgent)
  self.prio=Prio or 50
  self.urgent=Urgent
  self.importance=Importance
  return self
end

--- Set verbosity. 
-- @param #OPSTRANSPORT self
-- @param #number Verbosity Be more verbose. Default 0
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetVerbosity(Verbosity)
  self.verbose=Verbosity or 0
  return self
end

--- Add start condition.
-- @param #OPSTRANSPORT self
-- @param #function ConditionFunction Function that needs to be true before the transport can be started. Must return a #boolean.
-- @param ... Condition function arguments if any.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:AddConditionStart(ConditionFunction, ...)

  if ConditionFunction then
  
    local condition={} --#OPSTRANSPORT.Condition
    
    condition.func=ConditionFunction
    condition.arg={}
    if arg then
      condition.arg=arg
    end
    
    table.insert(self.conditionStart, condition)
    
  end
  
  return self
end

--- Add path used for transportation from the pickup to the deploy zone.
-- If multiple paths are defined, a random one is chosen. The path is retrieved from the waypoints of a given group.
-- **NOTE** that the category group defines for which carriers this path is valid.
-- For example, if you specify a GROUND group to provide the waypoints, only assigned GROUND carriers will use the
-- path. 
-- @param #OPSTRANSPORT self
-- @param Wrapper.Group#GROUP PathGroup A (late activated) GROUP defining a transport path by their waypoints.
-- @param #number Radius Randomization radius in meters. Default 0 m.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport Zone combo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:AddPathTransport(PathGroup, Reversed, Radius, TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault  

  if type(PathGroup)=="string" then
    PathGroup=GROUP:FindByName(PathGroup)
  end

  local path={} --#OPSTRANSPORT.Path
  path.category=PathGroup:GetCategory()
  path.radius=Radius or 0
  path.waypoints=PathGroup:GetTaskRoute()
  
  -- TODO: Check that only flyover waypoints are given for aircraft.

  -- Add path.
  table.insert(TransportZoneCombo.TransportPaths, path)

  return self
end

--- Get a path for transportation.
-- @param #OPSTRANSPORT self
-- @param #number Category Group category.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport Zone combo.
-- @return #OPSTRANSPORT.Path The path object.
function OPSTRANSPORT:_GetPathTransport(Category, TransportZoneCombo)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault
  
  local pathsTransport=TransportZoneCombo.TransportPaths

  if pathsTransport and #pathsTransport>0 then
  
    local paths={}
    
    for _,_path in pairs(pathsTransport) do
      local path=_path --#OPSTRANSPORT.Path
      if path.category==Category then
        table.insert(paths, path)
      end
    end
    
    if #paths>0 then
    
      local path=paths[math.random(#paths)] --#OPSTRANSPORT.Path
      
      return path
    end
  end

  return nil
end

--- Add a carrier assigned for this transport.
-- @param #OPSTRANSPORT self
-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP.
-- @param #string Status Carrier Status.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetCarrierTransportStatus(CarrierGroup, Status)

  -- Old status
  local oldstatus=self:GetCarrierTransportStatus(CarrierGroup)
  
  -- Debug info.
  self:T(self.lid..string.format("New carrier transport status for %s: %s --> %s", CarrierGroup:GetName(), oldstatus, Status))

  -- Set new status.
  self.carrierTransportStatus[CarrierGroup.groupname]=Status
    
  return self
end

--- Get carrier transport status.
-- @param #OPSTRANSPORT self
-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Carrier OPSGROUP.
-- @return #string Carrier status.
function OPSTRANSPORT:GetCarrierTransportStatus(CarrierGroup)
  local status=self.carrierTransportStatus[CarrierGroup.groupname] or "unknown"
  return status
end

--- Get unique ID of the transport assignment.
-- @param #OPSTRANSPORT self
-- @return #number UID.
function OPSTRANSPORT:GetUID()
  return self.uid
end

--- Get number of delivered cargo groups.
-- @param #OPSTRANSPORT self
-- @return #number Total number of delivered cargo groups.
function OPSTRANSPORT:GetNcargoDelivered()
  return self.Ndelivered
end

--- Get number of cargo groups.
-- @param #OPSTRANSPORT self
-- @return #number Total number of cargo groups.
function OPSTRANSPORT:GetNcargoTotal()
  return self.Ncargo
end

--- Get number of carrier groups assigned for this transport.
-- @param #OPSTRANSPORT self
-- @return #number Total number of carrier groups.
function OPSTRANSPORT:GetNcarrier()
  return self.Ncarrier
end

--- Add carrier asset to transport.
-- @param #OPSTRANSPORT self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be added.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:AddAsset(Asset, TransportZoneCombo)

  -- Debug info
  self:T(self.lid..string.format("Adding asset carrier \"%s\" to transport", tostring(Asset.spawngroupname)))

  -- Add asset to table.
  self.assets=self.assets or {}
    
  table.insert(self.assets, Asset)

  return self
end

--- Delete carrier asset from transport.
-- @param #OPSTRANSPORT self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be removed.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:DelAsset(Asset)

  for i,_asset in pairs(self.assets or {}) do
    local asset=_asset --Functional.Warehouse#WAREHOUSE.Assetitem
    
    if asset.uid==Asset.uid then
      self:T(self.lid..string.format("Removing asset \"%s\" from transport", tostring(Asset.spawngroupname)))
      table.remove(self.assets, i)
      return self
    end
    
  end

  return self
end

--- Add cargo asset.
-- @param #OPSTRANSPORT self
-- @param Functional.Warehouse#WAREHOUSE.Assetitem Asset The asset to be added.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:AddAssetCargo(Asset, TransportZoneCombo)

  -- Debug info
  self:T(self.lid..string.format("Adding asset cargo \"%s\" to transport and TZC=%s", tostring(Asset.spawngroupname), TransportZoneCombo and TransportZoneCombo.uid or "N/A"))

  -- Add asset to table.
  self.assetsCargo=self.assetsCargo or {}
    
  table.insert(self.assetsCargo, Asset)
  
  TransportZoneCombo.assetsCargo=TransportZoneCombo.assetsCargo or {}
  
  TransportZoneCombo.assetsCargo[Asset.spawngroupname]=Asset
  
  return self
end

--- Get transport zone combo of cargo group.
-- @param #OPSTRANSPORT self
-- @param #string GroupName Group name of cargo.
-- @return #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
function OPSTRANSPORT:GetTZCofCargo(GroupName)

  for _,_tzc in pairs(self.tzCombos) do
    local tzc=_tzc --#OPSTRANSPORT.TransportZoneCombo
    for _,_cargo in pairs(tzc.Cargos) do
      local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup
      if cargo.opsgroup:GetName()==GroupName then
        return tzc
      end
    end
  end

  return nil
end

--- Add LEGION to the transport.
-- @param #OPSTRANSPORT self
-- @param Ops.Legion#LEGION Legion The legion.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:AddLegion(Legion)

  -- Debug info.
  self:T(self.lid..string.format("Adding legion %s", Legion.alias))

  -- Add legion to table.
  table.insert(self.legions, Legion)
  
  return self
end

--- Remove LEGION from transport.
-- @param #OPSTRANSPORT self
-- @param Ops.Legion#LEGION Legion The legion.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:RemoveLegion(Legion)

  -- Loop over legions
  for i=#self.legions,1,-1 do
    local legion=self.legions[i] --Ops.Legion#LEGION
    if legion.alias==Legion.alias then
      -- Debug info.
      self:T(self.lid..string.format("Removing legion %s", Legion.alias))    
      table.remove(self.legions, i)
      return self
    end
  end
  
  self:E(self.lid..string.format("ERROR: Legion %s not found and could not be removed!", Legion.alias))
  return self
end

--- Check if an OPS group is assigned as carrier for this transport.
-- @param #OPSTRANSPORT self
-- @param Ops.OpsGroup#OPSGROUP CarrierGroup Potential carrier OPSGROUP.
-- @return #boolean If true, group is an assigned carrier. 
function OPSTRANSPORT:IsCarrier(CarrierGroup)

  if CarrierGroup then
    for _,_carrier in pairs(self.carriers) do
      local carrier=_carrier --Ops.OpsGroup#OPSGROUP
      if carrier.groupname==CarrierGroup.groupname then
        return true
      end
    end
  end

  return false
end

--- Check if transport is ready to be started.
-- * Start time passed.
-- * Stop time did not pass already.
-- * All start conditions are true.
-- @param #OPSTRANSPORT self
-- @return #boolean If true, mission can be started.
function OPSTRANSPORT:IsReadyToGo()

  -- Debug text.
  local text=self.lid.."Is ReadyToGo? "
  
  -- Current abs time.
  local Tnow=timer.getAbsTime()
  
  -- Pickup AND deploy zones must be set.
  local gotzones=false
  for _,_tz in pairs(self.tzCombos) do
    local tz=_tz --#OPSTRANSPORT.TransportZoneCombo
    if tz.PickupZone and tz.DeployZone then
      gotzones=true
      break
    end
  end
  if not gotzones then
    text=text.."No, pickup/deploy zone combo not yet defined!"
    return false
  end
  
  -- Start time did not pass yet.
  if self.Tstart and Tnow<self.Tstart then
    text=text.."No, start time not passed!"
    return false
  end
  
  -- Stop time already passed.
  if self.Tstop and Tnow>self.Tstop then
    text=text.."Nope, stop time already passed!"
    self:T(text)
    return false
  end
  
  -- All start conditions true?
  local startme=self:EvalConditionsAll(self.conditionStart)
  
  -- Nope, not yet.
  if not startme then
    text=text..("No way, at least one start condition is not true!")
    self:T(text)
    return false
  end
  
  -- We're good to go!
  text=text.."Yes!"
  self:T(text)
  return true
end

--- Set LEGION transport status.
-- @param #OPSTRANSPORT self
-- @param Ops.Legion#LEGION Legion The legion.
-- @param #string Status New status.
-- @return #OPSTRANSPORT self
function OPSTRANSPORT:SetLegionStatus(Legion, Status)

  -- Old status
  local status=self:GetLegionStatus(Legion)

  -- Debug info.
  self:T(self.lid..string.format("Setting LEGION %s to status %s-->%s", Legion.alias, tostring(status), tostring(Status)))

  -- New status.
  self.statusLegion[Legion.alias]=Status

  return self
end

--- Get LEGION transport status.
-- @param #OPSTRANSPORT self
-- @param Ops.Legion#LEGION Legion The legion.
-- @return #string status Current status.
function OPSTRANSPORT:GetLegionStatus(Legion)

  -- Current status.
  local status=self.statusLegion[Legion.alias] or "unknown"

  return status
end

--- Check if state is PLANNED.
-- @param #OPSTRANSPORT self
-- @return #boolean If true, status is PLANNED. 
function OPSTRANSPORT:IsPlanned()
  local is=self:is(OPSTRANSPORT.Status.PLANNED)
  return is
end

--- Check if state is QUEUED.
-- @param #OPSTRANSPORT self
-- @param Ops.Legion#LEGION Legion (Optional) Check if transport is queued at this legion.
-- @return #boolean If true, status is QUEUED. 
function OPSTRANSPORT:IsQueued(Legion)
  local is=self:is(OPSTRANSPORT.Status.QUEUED)
  if Legion then
    is=self:GetLegionStatus(Legion)==OPSTRANSPORT.Status.QUEUED
  end  
  return is
end

--- Check if state is REQUESTED.
-- @param #OPSTRANSPORT self
-- @param Ops.Legion#LEGION Legion (Optional) Check if transport is queued at this legion.
-- @return #boolean If true, status is REQUESTED. 
function OPSTRANSPORT:IsRequested(Legion)
  local is=self:is(OPSTRANSPORT.Status.REQUESTED)
  if Legion then
    is=self:GetLegionStatus(Legion)==OPSTRANSPORT.Status.REQUESTED
  end  
  return is
end

--- Check if state is SCHEDULED.
-- @param #OPSTRANSPORT self
-- @return #boolean If true, status is SCHEDULED. 
function OPSTRANSPORT:IsScheduled()
  local is=self:is(OPSTRANSPORT.Status.SCHEDULED)
  return is
end

--- Check if state is EXECUTING.
-- @param #OPSTRANSPORT self
-- @return #boolean If true, status is EXECUTING. 
function OPSTRANSPORT:IsExecuting()
  local is=self:is(OPSTRANSPORT.Status.EXECUTING)
  return is
end

--- Check if all cargo was delivered (or is dead).
-- @param #OPSTRANSPORT self
-- @param #number Nmin Number of groups that must be actually delivered (and are not dead). Default 0.
-- @return #boolean If true, all possible cargo was delivered. 
function OPSTRANSPORT:IsDelivered(Nmin)
  local is=self:is(OPSTRANSPORT.Status.DELIVERED)
  
--  Nmin=Nmin or 0
--  if Nmin>self.Ncargo then
--    Nmin=self.Ncargo
--  end
--  
--  if self.Ndelivered<Nmin then
--    is=false
--  end

  -- Check if Ndelivered is at least Nmin (if given)
  if is==false and Nmin and self.Ndelivered>=math.min(self.Ncargo, Nmin) then
    is=true
  end
  
  return is
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Status Update
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "StatusUpdate" event.
-- @param #OPSTRANSPORT self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSTRANSPORT:onafterStatusUpdate(From, Event, To)

  -- Current FSM state.
  local fsmstate=self:GetState()
  
  if self.verbose>=1 then
  
    -- Info text.    
    local text=string.format("%s: Ncargo=%d/%d, Ncarrier=%d/%d, Nlegions=%d", fsmstate:upper(), self.Ncargo, self.Ndelivered, #self.carriers, self.Ncarrier, #self.legions)

    -- Info about cargo and carrier.    
    if self.verbose>=2 then
    
      for i,_tz in pairs(self.tzCombos) do
        local tz=_tz --#OPSTRANSPORT.TransportZoneCombo
        local pickupzone=tz.PickupZone and tz.PickupZone:GetName() or "Unknown"
        local deployzone=tz.DeployZone and tz.DeployZone:GetName() or "Unknown"
        text=text..string.format("\n[%d] %s --> %s: Ncarriers=%d, Ncargo=%d (%d)", i, pickupzone, deployzone, tz.Ncarriers, #tz.Cargos, tz.Ncargo)
      end
    
    end

    -- Info about cargo and carrier.    
    if self.verbose>=3 then
    
      text=text..string.format("\nCargos:")
      for _,_cargo in pairs(self:GetCargos()) do
        local cargo=_cargo  --Ops.OpsGroup#OPSGROUP.CargoGroup
        if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then
          local carrier=cargo.opsgroup:_GetMyCarrierElement()
          local name=carrier and carrier.name or "none"
          local cstate=carrier and carrier.status or "N/A"
          text=text..string.format("\n- %s: %s [%s], weight=%d kg, carrier=%s [%s], delivered=%s [UID=%s]", 
          cargo.opsgroup:GetName(), cargo.opsgroup.cargoStatus:upper(), cargo.opsgroup:GetState(), cargo.opsgroup:GetWeightTotal(), name, cstate, tostring(cargo.delivered), tostring(cargo.opsgroup.cargoTransportUID))
        else
          --TODO: Storage
          local storage=cargo.storage
          text=text..string.format("\n- storage type=%s: amount: total=%d loaded=%d, lost=%d, delivered=%d, delivered=%s [UID=%s]",
          storage.cargoType, storage.cargoAmount, storage.cargoLoaded, storage.cargoLost, storage.cargoDelivered, tostring(cargo.delivered), tostring(cargo.uid))
        end
      end
      
      text=text..string.format("\nCarriers:")
      for _,_carrier in pairs(self.carriers) do
        local carrier=_carrier --Ops.OpsGroup#OPSGROUP
        text=text..string.format("\n- %s: %s [%s], Cargo Bay [current/reserved/total]=%d/%d/%d kg [free %d/%d/%d kg]", 
        carrier:GetName(), carrier.carrierStatus:upper(), carrier:GetState(), 
        carrier:GetWeightCargo(nil, false), carrier:GetWeightCargo(), carrier:GetWeightCargoMax(), 
        carrier:GetFreeCargobay(nil, false), carrier:GetFreeCargobay(), carrier:GetFreeCargobayMax())
      end
    end
    
    self:I(self.lid..text)
  end
  
  -- Check if all cargo was delivered (or is dead).
  self:_CheckDelivered()

  -- Update status again.
  if not self:IsDelivered() then
    self:__StatusUpdate(-30)
  end
end

--- Check if a cargo group was delivered.
-- @param #OPSTRANSPORT self
-- @param #string GroupName Name of the group.
-- @return #boolean If `true`, cargo was delivered.
function OPSTRANSPORT:IsCargoDelivered(GroupName)

  for _,_cargo in pairs(self:GetCargos()) do
    local cargo=_cargo  --Ops.OpsGroup#OPSGROUP.CargoGroup
    
    if cargo.opsgroup:GetName()==GroupName then
      return cargo.delivered
    end
    
  end
  
  return nil
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Event Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "Planned" event.
-- @param #OPSTRANSPORT self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSTRANSPORT:onafterPlanned(From, Event, To)
  self:T(self.lid..string.format("New status: %s-->%s", From, To))
end

--- On after "Scheduled" event.
-- @param #OPSTRANSPORT self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSTRANSPORT:onafterScheduled(From, Event, To)
  self:T(self.lid..string.format("New status: %s-->%s", From, To))
end

--- On after "Executing" event.
-- @param #OPSTRANSPORT self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSTRANSPORT:onafterExecuting(From, Event, To)
  self:T(self.lid..string.format("New status: %s-->%s", From, To))
end

--- On before "Delivered" event.
-- @param #OPSTRANSPORT self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSTRANSPORT:onbeforeDelivered(From, Event, To)

  -- Check that we do not call delivered again.
  if From==OPSTRANSPORT.Status.DELIVERED then
    return false
  end

  return true
end

--- On after "Delivered" event.
-- @param #OPSTRANSPORT self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSTRANSPORT:onafterDelivered(From, Event, To)
  self:T(self.lid..string.format("New status: %s-->%s", From, To))
  
  -- Inform all assigned carriers that cargo was delivered. They can have this in the queue or are currently processing this transport.
  for i=#self.carriers, 1, -1 do
    local carrier=self.carriers[i] --Ops.OpsGroup#OPSGROUP
    if self:GetCarrierTransportStatus(carrier)~=OPSTRANSPORT.Status.DELIVERED then
      carrier:Delivered(self)
    end 
  end
  
end

--- On after "Loaded" event.
-- @param #OPSTRANSPORT self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo OPSGROUP that was loaded into a carrier.
-- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier OPSGROUP that was loaded into a carrier.
-- @param Ops.OpsGroup#OPSGROUP.Element CarrierElement Carrier element.
function OPSTRANSPORT:onafterLoaded(From, Event, To, OpsGroupCargo, OpsGroupCarrier, CarrierElement)
  self:I(self.lid..string.format("Loaded OPSGROUP %s into carrier %s", OpsGroupCargo:GetName(), tostring(CarrierElement.name)))
end

--- On after "Unloaded" event.
-- @param #OPSTRANSPORT self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP OpsGroupCargo Cargo OPSGROUP that was unloaded from a carrier.
-- @param Ops.OpsGroup#OPSGROUP OpsGroupCarrier Carrier OPSGROUP that unloaded the cargo.
function OPSTRANSPORT:onafterUnloaded(From, Event, To, OpsGroupCargo, OpsGroupCarrier)
  self:I(self.lid..string.format("Unloaded OPSGROUP %s", OpsGroupCargo:GetName()))
end

--- On after "DeadCarrierGroup" event.
-- @param #OPSTRANSPORT self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Ops.OpsGroup#OPSGROUP OpsGroup Carrier OPSGROUP that is dead. 
function OPSTRANSPORT:onafterDeadCarrierGroup(From, Event, To, OpsGroup)
  self:I(self.lid..string.format("Carrier OPSGROUP %s dead!", OpsGroup:GetName()))

  -- Increase dead counter.
  self.NcarrierDead=self.NcarrierDead+1

  -- Remove group from carrier list/table.  
  self:_DelCarrier(OpsGroup)
  
  if #self.carriers==0 then
    self:DeadCarrierAll()
  end    
end

--- On after "DeadCarrierAll" event.
-- @param #OPSTRANSPORT self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state. 
function OPSTRANSPORT:onafterDeadCarrierAll(From, Event, To)
  self:I(self.lid..string.format("ALL Carrier OPSGROUPs are dead!"))

  if self.opszone then

    self:I(self.lid..string.format("Cancelling transport on CHIEF level"))
    self.chief:TransportCancel(self)
    
    --for _,_legion in pairs(self.legions) do
    --  local legion=_legion --Ops.Legion#LEGION
    --  legion:TransportCancel(self)
    --end

  else
  
    -- Check if cargo was delivered.
    self:_CheckDelivered()
    
    -- Set state back to PLANNED if not delivered.
    if not self:IsDelivered() then
      self:Planned()
    end
  
  end
  
end

--- On after "Cancel" event.
-- @param #OPSTRANSPORT self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSTRANSPORT:onafterCancel(From, Event, To)

  -- Number of OPSGROUPS assigned and alive.
  local Ngroups = #self.carriers

  -- Debug info.
  self:I(self.lid..string.format("CANCELLING transport in status %s. Will wait for %d carrier groups to report DONE before evaluation", self:GetState(), Ngroups))
  
  -- Time stamp.
  self.Tover=timer.getAbsTime()
  
  
  if self.chief then

    -- Debug info.
    self:T(self.lid..string.format("CHIEF will cancel the transport. Will wait for mission DONE before evaluation!"))
    
    -- CHIEF will cancel the transport.
    self.chief:TransportCancel(self)
  
  elseif self.commander then
  
    -- Debug info.
    self:T(self.lid..string.format("COMMANDER will cancel the transport. Will wait for transport DELIVERED before evaluation!"))
    
    -- COMMANDER will cancel the transport.
    self.commander:TransportCancel(self)

  elseif self.legions and #self.legions>0 then
  
    -- Loop over all LEGIONs.
    for _,_legion in pairs(self.legions or {}) do
      local legion=_legion --Ops.Legion#LEGION
    
      -- Debug info.
      self:T(self.lid..string.format("LEGION %s will cancel the transport. Will wait for transport DELIVERED before evaluation!", legion.alias))
    
      -- Legion will cancel all flight missions and remove queued request from warehouse queue.
      legion:TransportCancel(self)
      
    end
    
  else  
  
    -- Debug info.
    self:T(self.lid..string.format("No legion, commander or chief. Attached OPS groups will cancel the transport on their own. Will wait for transport DELIVERED before evaluation!"))
  
    -- Loop over all carrier groups.
    for _,_carrier in pairs(self:GetCarriers()) do
      local carrier=_carrier --Ops.OpsGroup#OPSGROUP
      carrier:TransportCancel(self)
    end
    
    -- Delete awaited transport.
    local cargos=self:GetCargoOpsGroups(false)
    for _,_cargo in pairs(cargos) do
      local cargo=_cargo --Ops.OpsGroup#OPSGROUP
        cargo:_DelMyLift(self)
    end
    
    
  end

  -- Special mission states.
  if self:IsPlanned() or self:IsQueued() or self:IsRequested() or Ngroups==0 then
    self:T(self.lid..string.format("Cancelled transport was in %s stage with %d carrier groups assigned and alive. Call it DELIVERED!", self:GetState(), Ngroups))
    self:Delivered()
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Check if all cargo of this transport assignment was delivered.
-- @param #OPSTRANSPORT self
function OPSTRANSPORT:_CheckDelivered()

  -- First check that at least one cargo was added (as we allow to do that later).
  if self.Ncargo>0 then

    local done=true
    local dead=true
    for _,_cargo in pairs(self:GetCargos()) do
      local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup
      
      if cargo.delivered then
        -- This one is delivered.
        dead=false
      elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup==nil then
        -- This one is nil?!
        dead=false
      elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsDestroyed() then
        -- This one was destroyed.
      elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsDead() then
        -- This one is dead.
      elseif cargo.type==OPSTRANSPORT.CargoType.OPSGROUP and cargo.opsgroup:IsStopped() then
        -- This one is stopped.
        dead=false
      else
        done=false --Someone is not done!
        dead=false
      end
     
    end
    
    if dead then
      self:I(self.lid.."All cargo DEAD ==> Delivered!")
      self:Delivered()
    elseif done then
      self:I(self.lid.."All cargo DONE ==> Delivered!")
      self:Delivered()  
    end
    
  end
  
end

--- Check if all required cargos are loaded.
-- @param #OPSTRANSPORT self
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @param Ops.OpsGroup#OPSGROUP CarrierGroup The carrier group asking.
-- @return #boolean If true, all required cargos are loaded or there is no required cargo or asking carrier is full.
function OPSTRANSPORT:_CheckRequiredCargos(TransportZoneCombo, CarrierGroup)

  -- Use default TZC if no transport zone combo is provided.
  TransportZoneCombo=TransportZoneCombo or self.tzcDefault
  
  -- Use input or take all cargos.
  local requiredCargos=TransportZoneCombo.Cargos
  
  -- Check if required cargos was set by user.
  if TransportZoneCombo.RequiredCargos and #TransportZoneCombo.RequiredCargos>0 then
    requiredCargos=TransportZoneCombo.RequiredCargos
  else
    requiredCargos={}
    for _,_cargo in pairs(TransportZoneCombo.Cargos) do
      local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup
      table.insert(requiredCargos, cargo.opsgroup)
    end
  end 
  
  if requiredCargos==nil or #requiredCargos==0 then
    return true
  end
  
  -- All carrier names.
  local carrierNames=self:_GetCarrierNames()
  
  -- Cargo groups not loaded yet.
  local weightmin=nil
  
  for _,_cargo in pairs(requiredCargos) do
    local cargo=_cargo --Ops.OpsGroup#OPSGROUP
    
    -- Is this cargo loaded into any carrier?
    local isLoaded=cargo:IsLoaded(carrierNames)
    
    if not isLoaded then
      local weight=cargo:GetWeightTotal()
      
      if weightmin==nil or weight<weightmin then
        weightmin=weight
      end
    end
    
  end
  
  if weightmin then
  
    -- Free space of carrier.
    local freeSpace=CarrierGroup:GetFreeCargobayMax(true)
    
    -- Debug info.
    self:T(self.lid..string.format("Check required cargos for carrier=%s free=%.1f, weight=%.1f", CarrierGroup:GetName(), freeSpace, weightmin))
    
    if weightmin<freeSpace then
      -- This group can still take cargo.
      return false    
    else
      -- This group is full! Even if there is cargo left, we cannot transport it.
      return true
    end
    
  end
  
  -- No cargo left.
  return true
end

--- Check if all given condition are true.
-- @param #OPSTRANSPORT self
-- @param #table Conditions Table of conditions.
-- @return #boolean If true, all conditions were true. Returns false if at least one condition returned false.
function OPSTRANSPORT:EvalConditionsAll(Conditions)

  -- Any stop condition must be true.
  for _,_condition in pairs(Conditions or {}) do
    local condition=_condition --#OPSTRANSPORT.Condition
  
    -- Call function.
    local istrue=condition.func(unpack(condition.arg))
    
    -- Any false will return false.
    if not istrue then
      return false
    end
    
  end

  -- All conditions were true.
  return true
end



--- Find transfer carrier element for cargo group.
-- @param #OPSTRANSPORT self
-- @param Ops.OpsGroup#OPSGROUP CargoGroup The cargo group that needs to be loaded into a carrier unit/element of the carrier group.
-- @param Core.Zone#ZONE Zone (Optional) Zone where the carrier must be in.
-- @param #table DisembarkCarriers Disembark carriers.
-- @param Wrapper.Airbase#AIRBASE DeployAirbase Airbase where to deploy.
-- @return Ops.OpsGroup#OPSGROUP.Element New carrier element for cargo or nil.
-- @return Ops.OpsGroup#OPSGROUP New carrier group for cargo or nil.
function OPSTRANSPORT:FindTransferCarrierForCargo(CargoGroup, Zone, DisembarkCarriers, DeployAirbase)

  -- Use default TZC if no transport zone combo is provided.
  --TransportZoneCombo=TransportZoneCombo or self.tzcDefault

  local carrier=nil --Ops.OpsGroup#OPSGROUP.Element
  local carrierGroup=nil --Ops.OpsGroup#OPSGROUP
  
  --TODO: maybe sort the carriers wrt to largest free cargo bay. Or better smallest free cargo bay that can take the cargo group weight.
  
  for _,_carrier in pairs(DisembarkCarriers or {}) do
    local carrierGroup=_carrier --Ops.OpsGroup#OPSGROUP
    
    -- First check if carrier is alive and loading cargo.
    if carrierGroup and carrierGroup:IsAlive() and (carrierGroup:IsLoading() or DeployAirbase) then
    
      -- Find an element of the group that has enough free space.
      carrier=carrierGroup:FindCarrierForCargo(CargoGroup)
      
      if carrier then
        if Zone==nil or Zone:IsVec2InZone(carrier.unit:GetVec2()) then
          return carrier, carrierGroup      
        else
          self:T2(self.lid.."Got transfer carrier but carrier not in zone (yet)!")
        end
      else
        self:T2(self.lid.."No transfer carrier available!")
      end
      
    end
  end

  self:T2(self.lid.."Could NOT find any carrier that is ALIVE and LOADING (or DELOYAIRBASE))!")
  return nil, nil
end

--- Create a cargo group data structure.
-- @param #OPSTRANSPORT self
-- @param Wrapper.Group#GROUP group The GROUP or OPSGROUP object.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @param #boolean DisembarkActivation If `true`, cargo group is activated when disembarked. 
-- @param Core.Zone#ZONE DisembarkZone Disembark zone, where the cargo is spawned when delivered.
-- @param Core.Set#SET_OPSGROUP DisembarkCarriers Disembark carriers cargo is directly loaded into when delivered.
-- @return Ops.OpsGroup#OPSGROUP.CargoGroup Cargo group data.
function OPSTRANSPORT:_CreateCargoGroupData(group, TransportZoneCombo, DisembarkActivation, DisembarkZone, DisembarkCarriers)

  -- Get ops group.
  local opsgroup=self:_GetOpsGroupFromObject(group)

  -- First check that this group is not already contained in this TZC.  
  for _,_cargo in pairs(TransportZoneCombo.Cargos or {}) do
    local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup
    if cargo.opsgroup.groupname==opsgroup.groupname then
      -- Group is already contained.
      return nil
    end
  end

  self.cargocounter=self.cargocounter+1

  -- Create a new data item.
  local cargo={} --Ops.OpsGroup#OPSGROUP.CargoGroup
  cargo.uid=self.cargocounter
  cargo.type="OPSGROUP"
  cargo.opsgroup=opsgroup
  cargo.delivered=false
  cargo.status="Unknown"
  cargo.tzcUID=TransportZoneCombo
  cargo.disembarkZone=DisembarkZone
  if DisembarkCarriers then
    cargo.disembarkCarriers={}
    self:_AddDisembarkCarriers(DisembarkCarriers, cargo.disembarkCarriers)
  end

  return cargo
end

--- Create a cargo group data structure.
-- @param #OPSTRANSPORT self
-- @param Wrapper.Storage#STORAGE StorageFrom Storage from.
-- @param Wrapper.Storage#STORAGE StorageTo Storage to.
-- @param #string CargoType Type of cargo.
-- @param #number CargoAmount Total amount of cargo that should be transported. Liquids in kg.
-- @param #number CargoWeight Weight of a single cargo item in kg. Default 1 kg.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return Ops.OpsGroup#OPSGROUP.CargoGroup Cargo group data.
function OPSTRANSPORT:_CreateCargoStorage(StorageFrom, StorageTo, CargoType, CargoAmount, CargoWeight, TransportZoneCombo)

  local storage={}  --#OPSTRANSPORT.Storage
  storage.storageFrom=StorageFrom
  storage.storageTo=StorageTo
  storage.cargoType=CargoType
  storage.cargoAmount=CargoAmount
  storage.cargoDelivered=0
  storage.cargoLost=0
  storage.cargoReserved=0
  storage.cargoLoaded=0
  storage.cargoWeight=CargoWeight or 1

  self.cargocounter=self.cargocounter+1  

  -- Create a new data item.
  local cargo={} --Ops.OpsGroup#OPSGROUP.CargoGroup
  cargo.uid=self.cargocounter
  cargo.type="STORAGE"
  cargo.opsgroup=nil
  cargo.storage=storage
  cargo.delivered=false
  cargo.status="Unknown"
  cargo.tzcUID=TransportZoneCombo
  cargo.disembarkZone=nil
  cargo.disembarkCarriers=nil

  return cargo
end

--- Count how many cargo groups are inside a zone.
-- @param #OPSTRANSPORT self
-- @param Core.Zone#ZONE Zone The zone object.
-- @param #boolean Delivered If `true`, only delivered groups are returned. If `false` only undelivered groups are returned. If `nil`, all groups are returned.
-- @param Ops.OpsGroup#OPSGROUP Carrier (Optional) Only count cargo groups that fit into the given carrier group. Current cargo is not a factor.
-- @param #OPSTRANSPORT.TransportZoneCombo TransportZoneCombo Transport zone combo.
-- @return #number Number of cargo groups.
function OPSTRANSPORT:_CountCargosInZone(Zone, Delivered, Carrier, TransportZoneCombo)

  -- Get cargo ops groups.
  --local cargos=self:GetCargoOpsGroups(Delivered, Carrier, TransportZoneCombo)
  
  local cargos=self:GetCargos(TransportZoneCombo, Carrier, Delivered)
  
  --- Function to check if carrier is supposed to be disembarked to.
  local function iscarrier(_cargo)
    local cargo=_cargo --Ops.OpsGroup#OPSGROUP
    
    local mycarrier=cargo:_GetMyCarrierGroup()
    
    if mycarrier and mycarrier:IsUnloading() then
    
      -- Get disembark carriers.
      local carriers=mycarrier.cargoTransport:GetDisembarkCarriers(mycarrier.cargoTZC)
    
      -- Check if carrier is in the list.
      for _,_carrier in pairs(carriers) do
        local carrier=_carrier --Ops.OpsGroup#OPSGROUP
        if Carrier:GetName()==carrier:GetName() then
          return true
        end
      end
      
      if mycarrier.cargoTZC and mycarrier.cargoTZC.Cargos then
        for _,_cargodata in pairs(mycarrier.cargoTZC.Cargos) do
          local cargodata=_cargodata --Ops.OpsGroup#OPSGROUP.CargoGroup
          if cargo:GetName()==cargodata.opsgroup:GetName() then          
            for _,_carrier in pairs(cargodata.disembarkCarriers) do
              local carrier=_carrier --Ops.OpsGroup#OPSGROUP
              if Carrier:GetName()==carrier:GetName() then
                return true
              end            
            end
          end
        end
      end
      
    end
    
    return false
  end
  
  local N=0
  for _,_cargo in pairs(cargos) do
    local cargo=_cargo --Ops.OpsGroup#OPSGROUP.CargoGroup
    
    local isNotCargo=true
    local isInZone=true
    local isInUtero=true
    
    if cargo.type==OPSTRANSPORT.CargoType.OPSGROUP then
      local opsgroup=cargo.opsgroup

      -- Is not cargo? 
      isNotCargo=opsgroup:IsNotCargo(true)
      if not isNotCargo then
        isNotCargo=iscarrier(opsgroup)
      end    
  
      -- Is in zone?
      isInZone=opsgroup:IsInZone(Zone)
      
      -- Is in utero?
      isInUtero=opsgroup:IsInUtero()

      -- Debug info.
      self:T(self.lid..string.format("Cargo=%s: notcargo=%s, iscarrier=%s inzone=%s, inutero=%s", opsgroup:GetName(), tostring(opsgroup:IsNotCargo(true)), tostring(iscarrier(opsgroup)), tostring(isInZone), tostring(isInUtero)))

      
    end


    -- We look for groups that are not cargo, in the zone or in utero.
    if isNotCargo and (isInZone or isInUtero) then
      N=N+1
    end
  end
  
  -- Debug info.
  self:T(self.lid..string.format("Found %d units in zone %s", N, Zone:GetName()))

  return N
end

--- Get a transport zone combination (TZC) for a carrier group. The pickup zone will be a zone, where the most cargo groups are located that fit into the carrier.
-- @param #OPSTRANSPORT self
-- @param Ops.OpsGroup#OPSGROUP Carrier The carrier OPS group.
-- @return Core.Zone#ZONE Pickup zone or `#nil`.
function OPSTRANSPORT:_GetTransportZoneCombo(Carrier)

  --- Selection criteria
  -- * Distance: pickup zone should be as close as possible.
  -- * Ncargo: Number of cargo groups. Pickup, where there is most cargo.
  -- * Ncarrier: Number of carriers already "working" on this TZC. Would be better if not all carriers work on the same combo while others are ignored.  
  
  -- Get carrier position.
  local vec2=Carrier:GetVec2()

  --- Penalty function.
  local function penalty(candidate)      
    local p=candidate.ncarriers*10-candidate.ncargo+candidate.distance/10
    return p
  end
  
  -- TZC candidates.
  local candidates={}
  
  for i,_transportzone in pairs(self.tzCombos) do
    local tz=_transportzone --#OPSTRANSPORT.TransportZoneCombo
    
    -- Check that pickup and deploy zones were defined.
    if tz.PickupZone and tz.DeployZone and tz.EmbarkZone then
    
      --TODO: Check if Carrier is an aircraft and if so, check that pickup AND deploy zones are airbases (not ships, not farps).
    
      -- Count undelivered cargos in embark(!) zone that fit into the carrier.
      local ncargo=self:_CountCargosInZone(tz.EmbarkZone, false, Carrier, tz)
      
      -- At least one group in the zone.
      if ncargo>=1 then
        
        -- Distance to the carrier in meters.
        local dist=tz.PickupZone:Get2DDistance(vec2)
        
        local ncarriers=0
        for _,_carrier in pairs(self.carriers) do
          local carrier=_carrier --Ops.OpsGroup#OPSGROUP
          if carrier and carrier:IsAlive() and carrier.cargoTZC and carrier.cargoTZC.uid==tz.uid then
            ncarriers=ncarriers+1
          end
        end
        
        -- New candidate.
        local candidate={tzc=tz, distance=dist/1000, ncargo=ncargo, ncarriers=ncarriers}
        
        -- Calculdate penalty of candidate.
        candidate.penalty=penalty(candidate)
        
        -- Add candidate.
        table.insert(candidates, candidate)
        
      end
    end
  end
  
  if #candidates>0 then
   
    -- Minimize penalty.
    local function optTZC(candA, candB)
      return candA.penalty<candB.penalty
    end
    table.sort(candidates, optTZC)
    
    -- Debug output.
    if self.verbose>=3 then
      local text="TZC optimized"
      for i,candidate in pairs(candidates) do
        text=text..string.format("\n[%d] TPZ=%d, Ncarriers=%d, Ncargo=%d, Distance=%.1f km, PENALTY=%d", i, candidate.tzc.uid, candidate.ncarriers, candidate.ncargo, candidate.distance, candidate.penalty)
      end
      self:I(self.lid..text)
    end
    
    -- Return best candidate.
    return candidates[1].tzc    
  else
    -- No candidates.
    self:T(self.lid..string.format("Could NOT find a pickup zone (with cargo) for carrier group %s", Carrier:GetName()))
  end  

  return nil
end

--- Get an OPSGROUP from a given OPSGROUP or GROUP object. If the object is a GROUP, an OPSGROUP is created automatically. 
-- @param #OPSTRANSPORT self
-- @param Core.Base#BASE Object The object, which can be a GROUP or OPSGROUP.
-- @return Ops.OpsGroup#OPSGROUP Ops Group.
function OPSTRANSPORT:_GetOpsGroupFromObject(Object)

  local opsgroup=nil

  if Object:IsInstanceOf("OPSGROUP") then
    -- We already have an OPSGROUP
    opsgroup=Object
  elseif Object:IsInstanceOf("GROUP") then
  
    -- Look into DB and try to find an existing OPSGROUP.
    opsgroup=_DATABASE:GetOpsGroup(Object)
    
    if not opsgroup then
      if Object:IsAir() then
        opsgroup=FLIGHTGROUP:New(Object)
      elseif Object:IsShip() then
        opsgroup=NAVYGROUP:New(Object)
      else
        opsgroup=ARMYGROUP:New(Object)
      end
    end
    
  else
    self:E(self.lid.."ERROR: Object must be a GROUP or OPSGROUP object!")
    return nil
  end

  return opsgroup
end
--- **Ops** - Strategic Zone.
--
-- **Main Features:**
--
--    * Monitor if a zone is captured
--    * Monitor if an airbase is captured
--    * Define conditions under which zones are captured/held
--    * Supports circular and polygon zone shapes
--
-- ===
--
-- ### Author: **funkyfranky**
-- 
-- @module Ops.OpsZone
-- @image OPS_OpsZone.png


--- OPSZONE class.
-- @type OPSZONE
-- @field #string ClassName Name of the class.
-- @field #string lid DCS log ID string.
-- @field #number verbose Verbosity of output.
-- @field Core.Zone#ZONE zone The zone.
-- @field Core.Zone#ZONE_RADIUS zoneCircular The circular zone.
-- @field Wrapper.Airbase#AIRBASE airbase The airbase that is monitored.
-- @field #string airbaseName Name of the airbase that is monitored.
-- @field #string zoneName Name of the zone.
-- @field #number zoneRadius Radius of the zone in meters.
-- @field #number ownerCurrent Coalition of the current owner of the zone.
-- @field #number ownerPrevious Coalition of the previous owner of the zone.
-- @field Core.Timer#TIMER timerStatus Timer for calling the status update.
-- @field #number Nred Number of red units in the zone.
-- @field #number Nblu Number of blue units in the zone.
-- @field #number Nnut Number of neutral units in the zone.
-- @field #table Ncoal Number of units in zone for each coalition.
-- @field #number Tred Threat level of red units in the zone.
-- @field #number Tblu Threat level of blue units in the zone.
-- @field #number Tnut Threat level of neutral units in the zone.
-- @field #number TminCaptured Time interval in seconds how long an attacker must have troops inside the zone to capture.
-- @field #number Tcaptured Time stamp (abs.) when the attacker destroyed all owning troops.
-- @field #table ObjectCategories Object categories for the scan.
-- @field #table UnitCategories Unit categories for the scan.
-- @field #number Tattacked Abs. mission time stamp when an attack was started.
-- @field #number dTCapture Time interval in seconds until a zone is captured.
-- @field #boolean neutralCanCapture Neutral units can capture. Default `false`.
-- @field #boolean drawZone If `true`, draw the zone on the F10 map.
-- @field #boolean markZone If `true`, mark the zone on the F10 map.
-- @field Wrapper.Marker#MARKER marker Marker on the F10 map.
-- @field #string markerText Text shown in the maker.
-- @field #table chiefs Chiefs that monitor this zone.
-- @field #table Missions Missions that are attached to this OpsZone.
-- @field #number nunitsCapture Number of units necessary to capture a zone.
-- @field #number threatlevelCapture Threat level necessary to capture a zone.
-- @field Core.Set#SET_UNIT ScanUnitSet Set of scanned units.
-- @field Core.Set#SET_GROUP ScanGroupSet Set of scanned groups.
-- @field #number UpdateSeconds Run status every this many seconds.
-- @extends Core.Fsm#FSM 

--- *Gentlemen, when the enemy is committed to a mistake we must not interrupt him too soon.* --- Horation Nelson
--
-- ===
--
-- # The OPSZONE Concept
--
-- An OPSZONE is a strategically important area.
--
--
-- @field #OPSZONE
OPSZONE = {
  ClassName      = "OPSZONE",
  verbose        =     0,
  Nred           =     0,
  Nblu           =     0,
  Nnut           =     0,
  Ncoal          =    {},
  Tred           =     0,
  Tblu           =     0,
  Tnut           =     0,
  chiefs         =    {},
  Missions       =    {},
  UpdateSeconds  =    120,
}

--- OPSZONE.MISSION
-- @type OPSZONE.MISSION
-- @field #number Coalition Coalition
-- @field #string Type Type of mission
-- @field Ops.Auftrag#AUFTRAG Mission The actual attached mission


--- Type of zone we are dealing with.
-- @type OPSZONE.ZoneType
-- @field #string Circular Zone is circular.
-- @field #string Polygon Zone is a polygon.
OPSZONE.ZoneType={
  Circular="Circular",
  Polygon="Polygon",
}

--- OPSZONE class version.
-- @field #string version
OPSZONE.version="0.6.2"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Pause/unpause evaluations.
-- TODO: Differentiate between ground attack and boming by air or arty.
-- DONE: Polygon zones.
-- DONE: Capture time, i.e. time how long a single coalition has to be inside the zone to capture it.
-- DONE: Capturing based on (total) threat level threshold. Unarmed units do not pose a threat and should not be able to hold a zone.
-- DONE: Can neutrals capture? No, since they are _neutral_!
-- DONE: Capture airbases.
-- DONE: Can statics capture or hold a zone? No, unless explicitly requested by mission designer.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new OPSZONE class object.
-- @param #OPSZONE self
-- @param Core.Zone#ZONE Zone The zone. Can be passed as ZONE\_RADIUS, ZONE_POLYGON, ZONE\_AIRBASE or simply as the name of the airbase.
-- @param #number CoalitionOwner Initial owner of the coaliton. Default `coalition.side.NEUTRAL`.
-- @return #OPSZONE self
-- @usage
-- myopszone = OPSZONE:New(ZONE:FindByName("OpsZoneOne"), coalition.side.RED) -- base zone from the mission editor
-- myopszone = OPSZONE:New(ZONE_RADIUS:New("OpsZoneTwo", mycoordinate:GetVec2(),5000),coalition.side.BLUE) -- radius zone of 5km at a coordinate
-- myopszone = OPSZONE:New(ZONE_RADIUS:New("Batumi")) -- airbase zone from Batumi Airbase, ca 2500m radius
-- myopszone = OPSZONE:New(ZONE_AIRBASE:New("Batumi",6000),coalition.side.BLUE) -- airbase zone from Batumi Airbase, but with a specific radius of 6km
-- 
function OPSZONE:New(Zone, CoalitionOwner)

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #OPSZONE
  
  -- Check if zone name instead of ZONE object was passed.
  if Zone then
    if type(Zone)=="string" then
      -- Convert string into a ZONE or ZONE_AIRBASE    
      local Name=Zone      
      Zone=ZONE:FindByName(Name)
      if not Zone then
        local airbase=AIRBASE:FindByName(Name)
        if airbase then
          Zone=ZONE_AIRBASE:New(Name, 2000)
        end
      end
      if not Zone then
        self:E(string.format("ERROR: No ZONE or ZONE_AIRBASE found for name: %s", Name))
        return nil
      end
    end
  else
    self:E("ERROR: First parameter Zone is nil in OPSZONE:New(Zone) call!")
    return nil    
  end
  
  -- Basic checks.
  if Zone:IsInstanceOf("ZONE_AIRBASE") then
    self.airbase=Zone._.ZoneAirbase
    self.airbaseName=self.airbase:GetName()
    self.zoneType=OPSZONE.ZoneType.Circular
    self.zoneCircular=Zone    
  elseif Zone:IsInstanceOf("ZONE_RADIUS") then
    -- Nothing to do.
    self.zoneType=OPSZONE.ZoneType.Circular
    self.zoneCircular=Zone
  elseif Zone:IsInstanceOf("ZONE_POLYGON_BASE") then
    -- Nothing to do.
    self.zoneType=OPSZONE.ZoneType.Polygon
    local zone=Zone --Core.Zone#ZONE_POLYGON
    self.zoneCircular=zone:GetZoneRadius(nil, true)
  else  
    self:E("ERROR: OPSZONE must be a SPHERICAL zone due to DCS restrictions!")
    return nil
  end

  -- Set some string id for output to DCS.log file.
  self.lid=string.format("OPSZONE %s | ", Zone:GetName())

  -- Set some values.
  self.zone=Zone
  self.zoneName=Zone:GetName()
  self.zoneRadius=self.zoneCircular:GetRadius()
  self.Missions = {}
  self.ScanUnitSet=SET_UNIT:New():FilterZones({Zone})
  self.ScanGroupSet=SET_GROUP:New():FilterZones({Zone})

  -- Add to database.
  _DATABASE:AddOpsZone(self)
  
  -- Current and previous owners.
  self.ownerCurrent=CoalitionOwner or coalition.side.NEUTRAL
  self.ownerPrevious=CoalitionOwner or coalition.side.NEUTRAL
  
  -- Contested.
  self.isContested=false
  
  self.Ncoal[coalition.side.BLUE]=0
  self.Ncoal[coalition.side.RED]=0
  self.Ncoal[coalition.side.NEUTRAL]=0
  
  -- We take the airbase coalition.
  if self.airbase then
    self.ownerCurrent=self.airbase:GetCoalition()
    self.ownerPrevious=self.airbase:GetCoalition()
  end
  
  -- Set object categories.
  self:SetObjectCategories()
  self:SetUnitCategories()
  
  -- Draw zone. Default is on.
  self:SetDrawZone()
  self:SetMarkZone(true)
  
  -- Default capture parameters.
  self:SetCaptureTime()
  self:SetCaptureNunits()
  self:SetCaptureThreatlevel()
  
  -- Status timer.
  self.timerStatus=TIMER:New(OPSZONE.Status, self)

  -- FMS start state is STOPPED.
  self:SetStartState("Stopped")
  
  -- Add FSM transitions.
  --                 From State    -->      Event       -->     To State
  self:AddTransition("Stopped",            "Start",             "Empty")       -- Start FSM.
  self:AddTransition("*",                  "Stop",              "Stopped")     -- Stop FSM.
  
  self:AddTransition("*",                  "Evaluated",         "*")           -- Evaluation done.

  self:AddTransition("*",                  "Captured",          "Guarded")     -- Zone was captured.
  
  self:AddTransition("Empty",              "Guarded",           "Guarded")     -- Owning coalition left the zone and returned.
  
  self:AddTransition("*",                  "Empty",             "Empty")       -- No red or blue units inside the zone.
   
  self:AddTransition("*",                  "Attacked",          "Attacked")    -- A guarded zone is under attack.
  self:AddTransition("*",                  "Defeated",          "Guarded")     -- The owning coalition defeated an attack.

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start".
  -- @function [parent=#OPSZONE] Start
  -- @param #OPSZONE self

  --- Triggers the FSM event "Start" after a delay.
  -- @function [parent=#OPSZONE] __Start
  -- @param #OPSZONE self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Stop".
  -- @function [parent=#OPSZONE] Stop
  -- @param #OPSZONE self

  --- Triggers the FSM event "Stop" after a delay.
  -- @function [parent=#OPSZONE] __Stop
  -- @param #OPSZONE self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "Evaluated".
  -- @function [parent=#OPSZONE] Evaluated
  -- @param #OPSZONE self

  --- Triggers the FSM event "Evaluated" after a delay.
  -- @function [parent=#OPSZONE] __Evaluated
  -- @param #OPSZONE self
  -- @param #number delay Delay in seconds.

  --- On after "Evaluated" event.
  -- @function [parent=#OPSZONE] OnAfterEvaluated
  -- @param #OPSZONE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.


  --- Triggers the FSM event "Captured".
  -- @function [parent=#OPSZONE] Captured
  -- @param #OPSZONE self
  -- @param #number Coalition Coalition side that captured the zone.

  --- Triggers the FSM event "Captured" after a delay.
  -- @function [parent=#OPSZONE] __Captured
  -- @param #OPSZONE self
  -- @param #number delay Delay in seconds.
  -- @param #number Coalition Coalition side that captured the zone.

  --- On after "Captured" event.
  -- @function [parent=#OPSZONE] OnAfterCaptured
  -- @param #OPSZONE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number Coalition Coalition side that captured the zone.


  --- Triggers the FSM event "Guarded".
  -- @function [parent=#OPSZONE] Guarded
  -- @param #OPSZONE self

  --- Triggers the FSM event "Guarded" after a delay.
  -- @function [parent=#OPSZONE] __Guarded
  -- @param #OPSZONE self
  -- @param #number delay Delay in seconds.

  --- On after "Guarded" event.
  -- @function [parent=#OPSZONE] OnAfterGuarded
  -- @param #OPSZONE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.  


  --- Triggers the FSM event "Empty".
  -- @function [parent=#OPSZONE] Empty
  -- @param #OPSZONE self

  --- Triggers the FSM event "Empty" after a delay.
  -- @function [parent=#OPSZONE] __Empty
  -- @param #OPSZONE self
  -- @param #number delay Delay in seconds.

  --- On after "Empty" event.
  -- @function [parent=#OPSZONE] OnAfterEmpty
  -- @param #OPSZONE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.  


  --- Triggers the FSM event "Attacked".
  -- @function [parent=#OPSZONE] Attacked
  -- @param #OPSZONE self
  -- @param #number AttackerCoalition Coalition side that is attacking the zone.

  --- Triggers the FSM event "Attacked" after a delay.
  -- @function [parent=#OPSZONE] __Attacked
  -- @param #OPSZONE self
  -- @param #number delay Delay in seconds.
  -- @param #number AttackerCoalition Coalition side that is attacking the zone.

  --- On after "Attacked" event.
  -- @function [parent=#OPSZONE] OnAfterAttacked
  -- @param #OPSZONE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number AttackerCoalition Coalition side that is attacking the zone.


  --- Triggers the FSM event "Defeated".
  -- @function [parent=#OPSZONE] Defeated
  -- @param #OPSZONE self
  -- @param #number DefeatedCoalition Coalition side that was defeated.

  --- Triggers the FSM event "Defeated" after a delay.
  -- @function [parent=#OPSZONE] __Defeated
  -- @param #OPSZONE self
  -- @param #number delay Delay in seconds.
  -- @param #number DefeatedCoalition Coalition side that was defeated.

  --- On after "Defeated" event.
  -- @function [parent=#OPSZONE] OnAfterDefeated
  -- @param #OPSZONE self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #number DefeatedCoalition Coalition side that was defeated.
    
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set verbosity level.
-- @param #OPSZONE self
-- @param #number VerbosityLevel Level of output (higher=more). Default 0.
-- @return #OPSZONE self
function OPSZONE:SetVerbosity(VerbosityLevel)
  self.verbose=VerbosityLevel or 0
  return self
end

--- Set categories of objects that can capture or hold the zone.
-- 
-- * Default is {Object.Category.UNIT, Object.Category.STATIC} so units and statics can capture and hold zones.
-- * Set to `{Object.Category.UNIT}` if only units should be able to capture and hold zones
-- 
-- Which units can capture zones can be further refined by `:SetUnitCategories()`.
-- 
-- @param #OPSZONE self
-- @param #table Categories Object categories. Default is `{Object.Category.UNIT, Object.Category.STATIC}`.
-- @return #OPSZONE self
function OPSZONE:SetObjectCategories(Categories)

  -- Ensure table if something was passed.
  if Categories and type(Categories)~="table" then
    Categories={Categories}
  end 

  -- Set categories.
  self.ObjectCategories=Categories or {Object.Category.UNIT, Object.Category.STATIC}
  
  return self
end

--- Set categories of units that can capture or hold the zone. See [DCS Class Unit](https://wiki.hoggitworld.com/view/DCS_Class_Unit).
-- @param #OPSZONE self
-- @param #table Categories Table of unit categories. Default `{Unit.Category.GROUND_UNIT}`.
-- @return #OPSZONE self
function OPSZONE:SetUnitCategories(Categories)

  -- Ensure table.
  if Categories and type(Categories)~="table" then
    Categories={Categories}
  end
  
  -- Set categories.
  self.UnitCategories=Categories or {Unit.Category.GROUND_UNIT}
  
  return self
end

--- Set threat level threshold that the offending units must have to capture a zone.
-- The reason why you might want to set this is that unarmed units (*e.g.* fuel trucks) should not be able to capture a zone as they do not pose a threat.
-- @param #OPSZONE self
-- @param #number Threatlevel Threat level threshold. Default 0.
-- @return #OPSZONE self
function OPSZONE:SetCaptureThreatlevel(Threatlevel)

  self.threatlevelCapture=Threatlevel or 0
  
  return self
end

--- Set how many units must be present in a zone to capture it. By default, one unit is enough.
-- @param #OPSZONE self
-- @param #number Nunits Number of units. Default 1.
-- @return #OPSZONE self
function OPSZONE:SetCaptureNunits(Nunits)

  Nunits=Nunits or 1

  self.nunitsCapture=Nunits
  
  return self
end


--- Set time how long an attacking coalition must have troops inside a zone before it captures the zone.
-- @param #OPSZONE self
-- @param #number Tcapture Time in seconds. Default 0.
-- @return #OPSZONE self
function OPSZONE:SetCaptureTime(Tcapture)

  self.TminCaptured=Tcapture or 0
  
  return self
end


--- Set whether *neutral* units can capture the zone.
-- @param #OPSZONE self
-- @param #boolean CanCapture If `true`, neutral units can.
-- @return #OPSZONE self
function OPSZONE:SetNeutralCanCapture(CanCapture)
  self.neutralCanCapture=CanCapture
  return self
end

--- Set if zone is drawn on the F10 map. Color will change depending on current owning coalition.
-- @param #OPSZONE self
-- @param #boolean Switch If `true` or `nil`, draw zone. If `false`, zone is not drawn.
-- @return #OPSZONE self
function OPSZONE:SetDrawZone(Switch)
  if Switch==false then
    self.drawZone=false
  else
    self.drawZone=true
  end
  return self
end

--- Set if zone is drawn on the F10 map for the owner coalition only.
-- @param #OPSZONE self
-- @param #boolean Switch If `false` or `nil`, draw zone for all coalitions. If `true`, zone is drawn for the owning coalition only if drawZone is true.
-- @return #OPSZONE self
function OPSZONE:SetDrawZoneForCoalition(Switch)
  if Switch==true then
    self.drawZoneForCoalition=true
  else
    self.drawZoneForCoalition=false
  end
  return self
end

--- Set if a marker on the F10 map shows the current zone status.
-- @param #OPSZONE self
-- @param #boolean Switch If `true`, zone is marked. If `false` or `nil`, zone is not marked.
-- @param #boolean ReadOnly If `true` or `nil` then mark is read only.
-- @return #OPSZONE self
function OPSZONE:SetMarkZone(Switch, ReadOnly)
  if Switch then
    self.markZone=true
    local Coordinate=self:GetCoordinate()
    self.markerText=self:_GetMarkerText()
    self.marker=self.marker or MARKER:New(Coordinate, self.markerText)
    if ReadOnly==false then
      self.marker.readonly=false
    else
      self.marker.readonly=true
    end    
    self.marker:ToAll()
  else
    if self.marker then
      self.marker:Remove()
    end
    self.marker=nil
    self.markZone=false
  end
  return self
end


--- Get current owner of the zone.
-- @param #OPSZONE self
-- @return #number Owner coalition.
function OPSZONE:GetOwner()
  return self.ownerCurrent
end

--- Get coalition name of current owner of the zone.
-- @param #OPSZONE self
-- @return #string  Owner coalition.
function OPSZONE:GetOwnerName()
  return UTILS.GetCoalitionName(self.ownerCurrent)
end

--- Get coordinate of zone.
-- @param #OPSZONE self
-- @return Core.Point#COORDINATE Coordinate of the zone.
function OPSZONE:GetCoordinate()
  local coordinate=self.zone:GetCoordinate()
  return coordinate
end

--- Get scanned units inside the zone.
-- @param #OPSZONE self
-- @return Core.Set#SET_UNIT Set of units inside the zone.
function OPSZONE:GetScannedUnitSet()
  return self.ScanUnitSet
end


--- Get scanned groups inside the zone.
-- @param #OPSZONE self
-- @return Core.Set#SET_GROUP Set of groups inside the zone.
function OPSZONE:GetScannedGroupSet()
  return self.ScanGroupSet
end

--- Returns a random coordinate in the zone.
-- @param #OPSZONE self
-- @param #number inner (Optional) Minimal distance from the center of the zone in meters. Default is 0 m.
-- @param #number outer (Optional) Maximal distance from the outer edge of the zone in meters. Default is the radius of the zone.
-- @param #table surfacetypes (Optional) Table of surface types. Can also be a single surface type. We will try max 1000 times to find the right type!
-- @return Core.Point#COORDINATE The random coordinate.
function OPSZONE:GetRandomCoordinate(inner, outer, surfacetypes)

  local zone=self:GetZone()
  
  local coord=zone:GetRandomCoordinate(inner, outer, surfacetypes)

  return coord
end

--- Get zone name.
-- @param #OPSZONE self
-- @return #string Name of the zone.
function OPSZONE:GetName()
  return self.zoneName
end

--- Get the zone object.
-- @param #OPSZONE self
-- @return Core.Zone#ZONE The zone.
function OPSZONE:GetZone()
  return self.zone
end

--- Get previous owner of the zone.
-- @param #OPSZONE self
-- @return #number Previous owner coalition.
function OPSZONE:GetPreviousOwner()
  return self.ownerPrevious
end

--- Get duration of the current attack.
-- @param #OPSZONE self
-- @return #number Duration in seconds since when the last attack began. Is `nil` if the zone is not under attack currently.
function OPSZONE:GetAttackDuration()
  if self:IsAttacked() and self.Tattacked then
    
    local dT=timer.getAbsTime()-self.Tattacked
    return dT
  end

  return nil
end

--- Find an OPSZONE using the Zone Name.
-- @param #OPSZONE self
-- @param #string ZoneName The zone name.
-- @return #OPSZONE The OPSZONE or nil if not found.
function OPSZONE:FindByName( ZoneName )
  local Found = _DATABASE:FindOpsZone( ZoneName )
  return Found
end

--- Check if the red coalition is currently owning the zone.
-- @param #OPSZONE self 
-- @return #boolean If `true`, zone is red.
function OPSZONE:IsRed()
  local is=self.ownerCurrent==coalition.side.RED
  return is
end

--- Check if the blue coalition is currently owning the zone.
-- @param #OPSZONE self 
-- @return #boolean If `true`, zone is blue.
function OPSZONE:IsBlue()
  local is=self.ownerCurrent==coalition.side.BLUE
  return is
end

--- Check if the neutral coalition is currently owning the zone.
-- @param #OPSZONE self 
-- @return #boolean If `true`, zone is neutral.
function OPSZONE:IsNeutral()
  local is=self.ownerCurrent==coalition.side.NEUTRAL
  return is
end

--- Check if a certain coalition is currently owning the zone.
-- @param #OPSZONE self 
-- @param #number Coalition The Coalition that is supposed to own the zone.
-- @return #boolean If `true`, zone is owned by the given coalition.
function OPSZONE:IsCoalition(Coalition)
  local is=self.ownerCurrent==Coalition
  return is
end

--- Check if zone is started (not stopped).
-- @param #OPSZONE self 
-- @return #boolean If `true`, zone is started.
function OPSZONE:IsStarted()
  local is=not self:IsStopped()
  return is
end

--- Check if zone is stopped.
-- @param #OPSZONE self 
-- @return #boolean If `true`, zone is stopped.
function OPSZONE:IsStopped()
  local is=self:is("Stopped")
  return is
end

--- Check if zone is guarded.
-- @param #OPSZONE self 
-- @return #boolean If `true`, zone is guarded.
function OPSZONE:IsGuarded()
  local is=self:is("Guarded")
  return is
end

--- Check if zone is empty.
-- @param #OPSZONE self 
-- @return #boolean If `true`, zone is empty.
function OPSZONE:IsEmpty()
  local is=self:is("Empty")
  return is
end

--- Check if zone is being attacked by the opposite coalition.
-- @param #OPSZONE self 
-- @return #boolean If `true`, zone is being attacked.
function OPSZONE:IsAttacked()
  local is=self:is("Attacked")
  return is
end

--- Check if zone is contested. Contested here means red *and* blue units are present in the zone.
-- @param #OPSZONE self 
-- @return #boolean If `true`, zone is contested.
function OPSZONE:IsContested()
  return self.isContested
end

--- Check if FMS is stopped.
-- @param #OPSZONE self 
-- @return #boolean If `true`, FSM is stopped
function OPSZONE:IsStopped()
  local is=self:is("Stopped")
  return is
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start/Stop Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Start OPSZONE FSM.
-- @param #OPSZONE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @return  #OPSZONE self
function OPSZONE:onafterStart(From, Event, To)

  -- Info.
  self:I(self.lid..string.format("Starting OPSZONE v%s", OPSZONE.version))
  
  -- Reinit the timer.
  self.timerStatus=self.timerStatus or TIMER:New(OPSZONE.Status, self)
  
  -- Status update.
  local EveryUpdateIn = self.UpdateSeconds or 120
  self.timerStatus:Start(1, EveryUpdateIn)
  
  -- Handle base captured event.
  if self.airbase then
    self:HandleEvent(EVENTS.BaseCaptured)
  end
  
  return self
end

--- Stop OPSZONE FSM.
-- @param #OPSZONE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSZONE:onafterStop(From, Event, To)

  -- Info.
  self:I(self.lid..string.format("Stopping OPSZONE"))
  
  -- Reinit the timer.
  self.timerStatus:Stop()
  
  -- Undraw zone.
  self.zone:UndrawZone()

  -- Remove marker.  
  if self.markZone then
    self.marker:Remove()
  end  
  
  -- Unhandle events.
  self:UnHandleEvent(EVENTS.BaseCaptured)

  -- Stop FSM scheduler.
  self.CallScheduler:Clear()
  if self.Scheduler then
    self.Scheduler:Clear()
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Status Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Update status.
-- @param #OPSZONE self
function OPSZONE:Status()

  -- Current FSM state.
  local fsmstate=self:GetState()
  
  -- Get contested.
  local contested=tostring(self:IsContested())

  -- Info message.
  if self.verbose>=1 then
    local text=string.format("State %s: Owner %d (previous %d), contested=%s, Nunits: red=%d, blue=%d, neutral=%d", fsmstate, self.ownerCurrent, self.ownerPrevious, contested, self.Nred, self.Nblu, self.Nnut)
    self:I(self.lid..text)
  end

  -- Scanning zone.
  self:Scan()
  
  -- Evaluate the scan result.
  self:EvaluateZone()
   
  -- Update F10 marker (only if enabled).
  self:_UpdateMarker()
  
  -- Undraw zone.
  if self.zone.DrawID and not self.drawZone then
    self.zone:UndrawZone()
  end
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On before "Captured" event.
-- @param #OPSZONE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number NewOwnerCoalition Coalition of the new owner.
function OPSZONE:onbeforeCaptured(From, Event, To, NewOwnerCoalition)

  -- Check if owner changed.
  
  if self.ownerCurrent==NewOwnerCoalition then
    self:T(self.lid.."")
  end

  return true
end

--- On after "Captured" event.
-- @param #OPSZONE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number NewOwnerCoalition Coalition of the new owner.
function OPSZONE:onafterCaptured(From, Event, To, NewOwnerCoalition)

  -- Debug info.
  self:T(self.lid..string.format("Zone captured by coalition=%d", NewOwnerCoalition))
  
  -- Set owners.
  self.ownerPrevious=self.ownerCurrent
  self.ownerCurrent=NewOwnerCoalition
  
  if self.drawZone then
  
    self.zone:UndrawZone()
    
    local color=self:_GetZoneColor()

    local coalition = nil
    if self.drawZoneForCoalition then
      coalition = self.ownerCurrent
    end
    self.zone:DrawZone(coalition, color, 1.0, color, 0.5)
  end  

  for _,_chief in pairs(self.chiefs) do
    local chief=_chief --Ops.Chief#CHIEF
    if chief.coalition==self.ownerCurrent then
      chief:ZoneCaptured(self)
    else
      chief:ZoneLost(self)
    end
  end
    
end

--- On after "Empty" event.
-- @param #OPSZONE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSZONE:onafterEmpty(From, Event, To)

  -- Debug info.
  self:T(self.lid..string.format("Zone is empty EVENT"))

end

--- On after "Attacked" event.
-- @param #OPSZONE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number AttackerCoalition Coalition of the attacking ground troops.
function OPSZONE:onafterAttacked(From, Event, To, AttackerCoalition)

  -- Debug info.
  self:T(self.lid..string.format("Zone is being attacked by coalition=%s!", tostring(AttackerCoalition)))
    
end

--- On after "Defeated" event.
-- @param #OPSZONE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number DefeatedCoalition Coalition side that was defeated.
function OPSZONE:onafterDefeated(From, Event, To, DefeatedCoalition)

  -- Debug info.
  self:T(self.lid..string.format("Defeated attack on zone by coalition=%d", DefeatedCoalition))
  
  -- Not attacked any more.
  self.Tattacked=nil
  
end

--- On enter "Guarded" state.
-- @param #OPSZONE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSZONE:onenterGuarded(From, Event, To)
  
  if From~=To then

    -- Debug info.
    self:T(self.lid..string.format("Zone is guarded"))

    -- Not attacked any more.
    self.Tattacked=nil
  
    if self.drawZone then
    
      self.zone:UndrawZone()
      
      local color=self:_GetZoneColor()

      local coalition = nil
      if self.drawZoneForCoalition then
          coalition = self.ownerCurrent
      end
      self.zone:DrawZone(coalition, color, 1.0, color, 0.5)
    end
    
  end

end

--- On enter "Attacked" state.
-- @param #OPSZONE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #number AttackerCoalition Coalition of the attacking ground troops.
function OPSZONE:onenterAttacked(From, Event, To, AttackerCoalition)

  -- Time stamp when the attack started.
  if From~="Attacked" then

    -- Debug info.
    self:T(self.lid..string.format("Zone is Attacked"))
  
    -- Set time stamp.
    self.Tattacked=timer.getAbsTime()
    
    -- Inform chief.
    if AttackerCoalition then
      for _,_chief in pairs(self.chiefs) do
        local chief=_chief --Ops.Chief#CHIEF
        if chief.coalition~=AttackerCoalition then
          chief:ZoneAttacked(self)
        end
      end
    end    
  
    -- Draw zone?
    if self.drawZone then
      self.zone:UndrawZone()
      
      -- Color.
      local color={1, 204/255, 204/255}

      local coalition = nil
      if self.drawZoneForCoalition then
          coalition = self.ownerCurrent
      end
      -- Draw zone.
      self.zone:DrawZone(coalition, color, 1.0, color, 0.5)
    end
    
    self:_CleanMissionTable()
    
  end

end

--- On enter "Empty" event.
-- @param #OPSZONE self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function OPSZONE:onenterEmpty(From, Event, To)

  if From~=To then

    -- Debug info.
    self:T(self.lid..string.format("Zone is empty now"))
  
    -- Inform chief.
    for _,_chief in pairs(self.chiefs) do
      local chief=_chief --Ops.Chief#CHIEF  
      chief:ZoneEmpty(self)
    end
  
    if self.drawZone then
      self.zone:UndrawZone()
      
      local color=self:_GetZoneColor()

      local coalition = nil
      if self.drawZoneForCoalition then
          coalition = self.ownerCurrent
      end
      self.zone:DrawZone(coalition, color, 1.0, color, 0.2)
    end
    
  end
    
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Scan Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Scan zone.
-- @param #OPSZONE self
-- @return #OPSZONE self
function OPSZONE:Scan()

  -- Debug info.
  if self.verbose>=3 then
    local text=string.format("Scanning zone %s R=%.1f m", self.zoneName, self.zoneRadius)
    self:I(self.lid..text)
  end

  -- Search.
  local SphereSearch={id=world.VolumeType.SPHERE, params={point=self.zone:GetVec3(), radius=self.zoneRadius}}

  -- Init number of red, blue and neutral units.  
  local Nred=0
  local Nblu=0
  local Nnut=0
  
  local Tred=0
  local Tblu=0
  local Tnut=0
  
  self.ScanGroupSet:Clear(false)
  self.ScanUnitSet:Clear(false)

  --- Function to evaluate the world search
  local function EvaluateZone(_ZoneObject)
  
    local ZoneObject=_ZoneObject --DCS#Object
    
    if ZoneObject then

      -- Object category.
      local ObjectCategory=Object.getCategory(ZoneObject)

      if ObjectCategory==Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() then
      
        ---
        -- UNIT
        ---
      
        -- This is a DCS unit object.
        local DCSUnit=ZoneObject --DCS#Unit
        
        --- Function to check if unit category is included.
        local function Included()
          
          if not self.UnitCategories then
            -- Any unit is included.
            return true
          else
            -- Check if found object is in specified categories.
            local CategoryDCSUnit = ZoneObject:getDesc().category
  
            for _,UnitCategory in pairs(self.UnitCategories) do
              if UnitCategory==CategoryDCSUnit then
                return true
              end
            end
  
          end
          
          return false
        end
        

        if Included() then
        
          -- Get Coalition.
          local Coalition=DCSUnit:getCoalition()
          
          local tl=0
          local unit=UNIT:Find(DCSUnit)
          if unit then
                      
            -- Inside zone.
            local inzone=true
            if self.zoneType==OPSZONE.ZoneType.Polygon then
            
              -- Check if unit is really inside the zone.
              inzone=unit:IsInZone(self.zone)
              
              -- Debug marker.
              -- Debug: Had cases where a (red) unit was clearly not inside the zone but the scan did find it!      
              --unit:GetCoordinate():MarkToAll(string.format("Unit %s inzone=%s", unit:GetName(), tostring(inzone)))
            end
            
            if inzone then          
                    
              -- Threat level of unit.
              tl=unit:GetThreatLevel()
              
              -- Add unit to set.
              self.ScanUnitSet:AddUnit(unit)
                          
              -- Get group of unit.
              local group=unit:GetGroup()
            
              -- Add group to scanned set.  
              if group then
                self.ScanGroupSet:AddGroup(group, true)
              end
                        
              -- Increase counter.
              if Coalition==coalition.side.RED then
                Nred=Nred+1
                Tred=Tred+tl
              elseif Coalition==coalition.side.BLUE then
                Nblu=Nblu+1
                Tblu=Tblu+tl
              elseif Coalition==coalition.side.NEUTRAL then
                Nnut=Nnut+1
                Tnut=Tnut+tl
              end
              
              -- Debug info.
              if self.verbose>=4 then
                self:I(self.lid..string.format("Found unit %s (coalition=%d)", DCSUnit:getName(), Coalition))
              end
            
            end
          end
          
        end
              
      elseif ObjectCategory==Object.Category.STATIC and ZoneObject:isExist() then

        ---
        -- STATIC
        ---
      
        -- This is a DCS static object.
        local DCSStatic=ZoneObject --DCS#StaticObject
        
        -- Get coalition.
        local Coalition=DCSStatic:getCoalition()
        
        -- CAREFUL! Downed pilots break routine here without any error thrown.
        --local unit=STATIC:Find(DCSStatic)
        
        -- Inside zone.
        local inzone=true
        if self.zoneType==OPSZONE.ZoneType.Polygon then
        
          local Vec3=DCSStatic:getPoint()
                  
          inzone=self.zone:IsVec3InZone(Vec3)
          
        end
        
        if inzone then                  

          -- Increase counter.
          if Coalition==coalition.side.RED then
            Nred=Nred+1
          elseif Coalition==coalition.side.BLUE then
            Nblu=Nblu+1
          elseif Coalition==coalition.side.NEUTRAL then
            Nnut=Nnut+1
          end
          
          -- Debug info
          if self.verbose>=4 then        
            self:I(self.lid..string.format("Found static %s (coalition=%d)", DCSStatic:getName(), Coalition))
          end
          
        end
      
      elseif ObjectCategory==Object.Category.SCENERY then
      
        ---
        -- SCENERY
        ---      
      
        local SceneryType = ZoneObject:getTypeName()
        local SceneryName = ZoneObject:getName()
        
        -- Debug info.
        self:T2(self.lid..string.format("Found scenery type=%s, name=%s", SceneryType, SceneryName))
      end

    end

    return true
  end

  -- Search objects.
  world.searchObjects(self.ObjectCategories, SphereSearch, EvaluateZone)
  
  -- Debug info.
  if self.verbose>=3 then
    local text=string.format("Scan result Nred=%d, Nblue=%d, Nneutral=%d", Nred, Nblu, Nnut)    
    if self.verbose>=4 then
      for _,_unit in pairs(self.ScanUnitSet:GetSet()) do
        local unit=_unit --Wrapper.Unit#UNIT
        text=text..string.format("\nUnit %s coalition=%s", unit:GetName(), unit:GetCoalitionName())
      end
  
      for _,_group in pairs(self.ScanGroupSet:GetSet()) do
        local group=_group --Wrapper.Group#GROUP
        text=text..string.format("\nGroup %s coalition=%s", group:GetName(), group:GetCoalitionName())
      end
    end    
    self:I(self.lid..text)
  end
  
  -- Set values.
  self.Nred=Nred
  self.Nblu=Nblu
  self.Nnut=Nnut
  
  
  self.Ncoal[coalition.side.BLUE]=Nblu
  self.Ncoal[coalition.side.RED]=Nred
  self.Ncoal[coalition.side.NEUTRAL]=Nnut
  
  self.Tblu=Tblu
  self.Tred=Tred
  self.Tnut=Tnut

  return self
end

--- Evaluate zone.
-- @param #OPSZONE self
-- @return #OPSZONE self
function OPSZONE:EvaluateZone()

  -- Set values.
  local Nred=self.Nred
  local Nblu=self.Nblu
  local Nnut=self.Nnut

  local Tnow=timer.getAbsTime()
  
  --- Capture
  -- @param #number coal Coaltion capturing.
  local function captured(coal)
  
    -- Blue captured red zone.
    if not self.airbase then
      
      -- Set time stamp if it does not exist.
      if not self.Tcaptured then
        self.Tcaptured=Tnow
      end
      
      -- Check if enough time elapsed.
      if Tnow-self.Tcaptured>=self.TminCaptured then
        self:Captured(coal)
        self.Tcaptured=nil
      end
    end  
  
  end
  

  if self:IsRed() then
  
    ---
    -- RED zone
    ---
  
    if Nred==0 then
    
      -- No red units in red zone any more.
    
      if Nblu>=self.nunitsCapture and self.Tblu>=self.threatlevelCapture then
      
        -- Blue captued red zone.
        captured(coalition.side.BLUE)
        
      elseif Nnut>=self.nunitsCapture and self.Tnut>=self.threatlevelCapture and self.neutralCanCapture then
      
        -- Neutral captured red zone.
        captured(coalition.side.NEUTRAL)
                    
      end
      
    else
    
      -- Red units in red zone.
      
      if Nblu>0 then
      
        if not self:IsAttacked() and self.Tblu>=self.threatlevelCapture then
          self:Attacked(coalition.side.BLUE)
        end
        
      elseif Nblu==0 then
      
        if self:IsAttacked() and self:IsContested() then
          self:Defeated(coalition.side.BLUE)
        elseif self:IsEmpty() then
          -- Red units left zone and returned (or from initial Empty state).
          self:Guarded()
        end
      
      end
      
    end
    
    -- Contested by blue?
    if Nblu==0 then
      self.isContested=false
    else  
      self.isContested=true
    end
    
  elseif self:IsBlue() then

    ---
    -- BLUE zone
    ---

    if Nblu==0 then
    
      -- No blue units in blue zone any more.
    
      if Nred>=self.nunitsCapture and self.Tred>=self.threatlevelCapture then
      
        -- Red captured blue zone.
        captured(coalition.side.RED)        
        
      elseif Nnut>=self.nunitsCapture and self.Tnut>=self.threatlevelCapture and self.neutralCanCapture then
      
        -- Neutral captured blue zone.
        captured(coalition.side.NEUTRAL)      
      
      end

    else
    
      -- Still blue units in blue zone.
      
      if Nred>0 then
      
        if not self:IsAttacked() and self.Tred>=self.threatlevelCapture then
          -- Red is attacking blue zone.
          self:Attacked(coalition.side.RED)
        end
        
      elseif Nred==0 then
      
        if self:IsAttacked() and self:IsContested() then
          -- Blue defeated read attack.
          self:Defeated(coalition.side.RED)
        elseif self:IsEmpty() then
          -- Blue units left zone and returned (or from initial Empty state).
          self:Guarded() 
        end

      end
      
    end
    
    -- Contested by red?
    if Nred==0 then
      self.isContested=false
    else  
      self.isContested=true
    end
  
  elseif self:IsNeutral() then

    ---
    -- NEUTRAL zone
    ---

    -- Not checked as neutrals cant capture (for now).
    --if Nnut==0 then 
    
      -- No neutral units in neutral zone any more.

      if Nred>0 and Nblu>0 then
        self:T(self.lid.."FF neutrals left neutral zone and red and blue are present! What to do?")
        if not self:IsAttacked() then
          self:Attacked()
        end
        self.isContested=true
      elseif Nred>=self.nunitsCapture and self.Tred>=self.threatlevelCapture then
        -- Red captured neutral zone.
        captured(coalition.side.RED)
      elseif Nblu>=self.nunitsCapture and self.Tblu>=self.threatlevelCapture then
        -- Blue captured neutral zone.
        captured(coalition.side.BLUE)
      end
      
    --end
  
  else
    self:E(self.lid.."ERROR: Unknown coaliton!")
  end

      
  -- No units of any coalition in zone any more ==> Empty!
  if Nblu==0 and Nred==0 and Nnut==0 and (not self:IsEmpty()) then
    self:Empty()
  end

  -- Finally, check airbase coalition
  if self.airbase then

    -- Current coalition.
    local airbasecoalition=self.airbase:GetCoalition()
    
    if airbasecoalition~=self.ownerCurrent then
      self:T(self.lid..string.format("Captured airbase %s: Coaltion %d-->%d", self.airbaseName, self.ownerCurrent, airbasecoalition))
      self:Captured(airbasecoalition)
    end
  
  end
  
  -- Trigger event.
  self:Evaluated()

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- DCS Event Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Monitor hit events.
-- @param #OPSZONE self
-- @param Core.Event#EVENTDATA EventData The event data.
function OPSZONE:OnEventHit(EventData)

  if self.HitsOn then

    local UnitHit = EventData.TgtUnit
    
    -- Check if unit is inside the capture zone and that it is of the defending coalition.
    if UnitHit and UnitHit:IsInZone(self) and UnitHit:GetCoalition()==self.ownerCurrent then
    
      -- Update last hit time.
      self.HitTimeLast=timer.getTime()
      
      -- Only trigger attacked event if not already in state "Attacked".
      if not self:IsAttacked() then
        self:T3(self.lid.."Hit ==> Attack")
        self:Attacked()
      end
      
    end

  end

end

--- Monitor base captured events.
-- @param #OPSZONE self
-- @param Core.Event#EVENTDATA EventData The event data.
function OPSZONE:OnEventBaseCaptured(EventData)

  if EventData and EventData.Place and self.airbase and self.airbaseName then

    -- Place is the airbase that was captured.
    local airbase=EventData.Place --Wrapper.Airbase#AIRBASE

    -- Check that this airbase belongs or did belong to this warehouse.
    if EventData.PlaceName==self.airbaseName then
    
      -- New coalition of the airbase
      local CoalitionNew=airbase:GetCoalition()

      -- Debug info.      
      self:I(self.lid..string.format("EVENT BASE CAPTURED: New coalition of airbase %s: %d [previous=%d]", self.airbaseName, CoalitionNew, self.ownerCurrent))
      
      -- Check that coalition actually changed.
      if CoalitionNew~=self.ownerCurrent then
        self:Captured(CoalitionNew)
      end
    
    end
    
  end  

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get RGB color of zone depending on current owner.
-- @param #OPSZONE self
-- @return #table RGB color.
function OPSZONE:_GetZoneColor()

  local color={0,0,0}
  
  if self.ownerCurrent==coalition.side.NEUTRAL then
    color=self.ZoneOwnerNeutral or {1, 1, 1}
  elseif self.ownerCurrent==coalition.side.BLUE then
    color=self.ZoneOwnerBlue or {0, 0, 1}
  elseif self.ownerCurrent==coalition.side.RED then
    color=self.ZoneOwnerRed or {1, 0, 0}
  else
  
  end

  return color
end

--- Set custom RGB color of zone depending on current owner.
-- @param #OPSZONE self
-- @param #table Neutral Color is a table of RGB values 0..1 for Red, Green, and Blue respectively, e.g. {1,0,0} for red.
-- @param #table Blue Color is a table of RGB values 0..1 for Red, Green, and Blue respectively, e.g. {0,1,0} for green.
-- @param #table Red Color is a table of RGB values 0..1 for Red, Green, and Blue respectively, e.g. {0,0,1} for blue.
-- @return  #OPSZONE self
function OPSZONE:SetZoneColor(Neutral, Blue, Red)
  
  self.ZoneOwnerNeutral = Neutral  or {1, 1, 1}
  self.ZoneOwnerBlue = Blue or {0, 0, 1}
  self.ZoneOwnerRed = Red or {1, 0, 0}

  return self
end

--- Update marker on the F10 map.
-- @param #OPSZONE self
function OPSZONE:_UpdateMarker()

  if self.markZone then
  
    -- Get marker text.
    local text=self:_GetMarkerText()
    
    -- Chck if marker text changed and if so, update the marker.
    if text~=self.markerText then
      self.markerText=text
      self.marker:UpdateText(self.markerText)
    end
    
    --TODO: Update position if changed.
  
  end

end

--- Get marker text.
-- @param #OPSZONE self
-- @return #string Marker text.
function OPSZONE:_GetMarkerText()

  local owner=UTILS.GetCoalitionName(self.ownerCurrent)
  local prevowner=UTILS.GetCoalitionName(self.ownerPrevious)

  -- Get marker text.
  local text=string.format("%s [N=%d, TL=%d T=%d]:\nOwner=%s [%s]\nState=%s [Contested=%s]\nBlue=%d [TL=%d]\nRed=%d [TL=%d]\nNeutral=%d [TL=%d]", 
  self.zoneName, self.nunitsCapture or 0, self.threatlevelCapture or 0, self.TminCaptured or 0, 
  owner, prevowner, self:GetState(), tostring(self:IsContested()), 
  self.Nblu, self.Tblu, self.Nred, self.Tred, self.Nnut, self.Tnut)
  
  return text
end

--- Add a chief that monitors this zone. Chief will be informed about capturing etc.
-- @param #OPSZONE self
-- @param Ops.Chief#CHIEF Chief The chief.
-- @return #table RGB color.
function OPSZONE:_AddChief(Chief)

  -- Add chief.
  table.insert(self.chiefs, Chief)

end

--- Add an entry to the OpsZone mission table
-- @param #OPSZONE self
-- @param #number Coalition Coalition of type e.g. coalition.side.NEUTRAL
-- @param #string Type Type of mission, e.g. AUFTRAG.Type.CAS
-- @param Ops.Auftrag#AUFTRAG Auftrag The Auftrag itself
-- @return #OPSZONE self
function OPSZONE:_AddMission(Coalition,Type,Auftrag)
  
  -- Add a mission
  local entry = {} -- #OPSZONE.MISSION
  entry.Coalition = Coalition or coalition.side.NEUTRAL
  entry.Type = Type or ""
  entry.Mission = Auftrag or nil
  
  table.insert(self.Missions,entry)
  
  return self
end

--- Get the OpsZone mission table. #table of #OPSZONE.MISSION entries
-- @param #OPSZONE self
-- @return #table Missions
function OPSZONE:_GetMissions()
  return self.Missions
end

--- Add an entry to the OpsZone mission table.
-- @param #OPSZONE self
-- @param #number Coalition Coalition of type e.g. `coalition.side.NEUTRAL`.
-- @param #string Type Type of mission, e.g. `AUFTRAG.Type.CAS`.
-- @return #boolean found True if we have that kind of mission, else false.
-- @return #table Missions Table of `Ops.Auftrag#AUFTRAG` entries.
function OPSZONE:_FindMissions(Coalition, Type)
  -- search the table
  local foundmissions = {}
  local found = false
  for _,_entry in pairs(self.Missions) do
    local entry = _entry -- #OPSZONE.MISSION
    if entry.Coalition == Coalition and entry.Type == Type and entry.Mission and entry.Mission:IsNotOver() then
      table.insert(foundmissions,entry.Mission)
      found = true
    end
  end
  return found, foundmissions
end

--- Clean mission table from missions that are over.
-- @param #OPSZONE self
-- @return #OPSZONE self
function OPSZONE:_CleanMissionTable()
  local missions = {}
  for _,_entry in pairs(self.Missions) do
    local entry = _entry -- #OPSZONE.MISSION
    if entry.Mission and entry.Mission:IsNotOver() then
      table.insert(missions,entry)
    end
  end
  self.Missions = missions
  return self
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - Brigade Platoon.
--
-- **Main Features:**
--
--    * Set parameters like livery, skill valid for all platoon members.
--    * Define modex and callsigns.
--    * Define mission types, this platoon can perform (see Ops.Auftrag#AUFTRAG).
--    * Pause/unpause platoon operations.
--
-- ===
--
-- ### Author: **funkyfranky**
-- @module Ops.Platoon
-- @image OPS_Platoon.png


--- PLATOON class.
-- @type PLATOON
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field Ops.OpsGroup#OPSGROUP.WeaponData weaponData Weapon data table with key=BitType.
-- @extends Ops.Cohort#COHORT

--- *Some cool cohort quote* -- Known Author
--
-- ===
--
-- # The PLATOON Concept
-- 
-- A PLATOON is essential part of an BRIGADE.
--
--
--
-- @field #PLATOON
PLATOON = {
  ClassName      = "PLATOON",
  verbose        =     0,
  weaponData     =    {},
}

--- PLATOON class version.
-- @field #string version
PLATOON.version="0.1.0"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: A lot.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new PLATOON object and start the FSM.
-- @param #PLATOON self
-- @param #string TemplateGroupName Name of the template group.
-- @param #number Ngroups Number of asset groups of this platoon. Default 3.
-- @param #string PlatoonName Name of the platoon. Must be **unique**!
-- @return #PLATOON self
function PLATOON:New(TemplateGroupName, Ngroups, PlatoonName)

  -- Inherit everything from COHORT class.
  local self=BASE:Inherit(self, COHORT:New(TemplateGroupName, Ngroups, PlatoonName)) -- #PLATOON
  
  -- All platoons get mission type Nothing.
  self:AddMissionCapability(AUFTRAG.Type.NOTHING, 50)
  
  -- Is ground.
  self.isGround=true

  -- Get ammo.
  self.ammo=self:_CheckAmmo()

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

  -- TODO: Platoon specific user functions.

--- Set brigade of this platoon.
-- @param #PLATOON self
-- @param Ops.Brigade#BRIGADE Brigade The brigade.
-- @return #PLATOON self
function PLATOON:SetBrigade(Brigade)
  self.legion=Brigade
  return self
end

--- Get brigade of this platoon.
-- @param #PLATOON self
-- @return Ops.Brigade#BRIGADE The brigade.
function PLATOON:GetBrigade()
  return self.legion
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start & Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--[[
--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers.
-- @param #PLATOON self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function PLATOON:onafterStart(From, Event, To)

  -- Short info.
  local text=string.format("Starting %s v%s %s", self.ClassName, self.version, self.name)
  self:I(self.lid..text)

  -- Start the status monitoring.
  self:__Status(-1)
end
]]

--- On after "Status" event.
-- @param #PLATOON self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function PLATOON:onafterStatus(From, Event, To)

  if self.verbose>=1 then

    -- FSM state.
    local fsmstate=self:GetState()
  
    local callsign=self.callsignName and UTILS.GetCallsignName(self.callsignName) or "N/A"
    local skill=self.skill and tostring(self.skill) or "N/A"
    
    local NassetsTot=#self.assets
    local NassetsInS=self:CountAssets(true)
    local NassetsQP=0 ; local NassetsP=0 ; local NassetsQ=0  
    if self.legion then
      NassetsQP, NassetsP, NassetsQ=self.legion:CountAssetsOnMission(nil, self)
    end
    
    -- Short info.
    local text=string.format("%s [Type=%s, Call=%s, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", 
    fsmstate, self.aircrafttype, callsign, skill, NassetsTot, NassetsInS, NassetsQP, NassetsP, NassetsQ)
    self:T(self.lid..text)
    
    -- Weapon data info.
    if self.verbose>=3 and self.weaponData then
      local text="Weapon Data:"
      for bit,_weapondata in pairs(self.weaponData) do
        local weapondata=_weapondata --Ops.OpsGroup#OPSGROUP.WeaponData
        text=text..string.format("\n- Bit=%s: Rmin=%.1f km, Rmax=%.1f km", bit, weapondata.RangeMin/1000, weapondata.RangeMax/1000)
      end
      self:I(self.lid..text)
    end
    
    -- Check if group has detected any units.
    self:_CheckAssetStatus()
    
  end  
  
  if not self:IsStopped() then
    self:__Status(-60)
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: Misc functions.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Ops** - PlayerTask (mission) for Players.
--
-- ## Main Features:
--
--    * Simplifies defining and executing Player tasks
--    * FSM events when a mission is added, done, successful or failed, replanned
--    * Ready to use SRS and localization
--    * Mission locations can be smoked, flared, illuminated and marked on the map
--
-- ===
--
-- ## Example Missions:
--
-- Demo missions can be found on [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/PlayerTask).
--
-- ===
--
-- ### Author: **Applevangelist**
-- ### Special thanks to: Streakeagle
--
-- ===
-- @module Ops.PlayerTask
-- @image OPS_PlayerTask.jpg
-- @date Last Update May 2025


do
-------------------------------------------------------------------------------------------------------------------
-- PLAYERTASK
-- TODO: PLAYERTASK
-------------------------------------------------------------------------------------------------------------------

--- PLAYERTASK class.
-- @type PLAYERTASK
-- @field #string ClassName Name of the class.
-- @field #boolean verbose Switch verbosity.
-- @field #string lid Class id string for output to DCS log file.
-- @field #number PlayerTaskNr (Globally unique) Number of the task.
-- @field Ops.Auftrag#AUFTRAG.Type Type The type of the task
-- @field Ops.Target#TARGET Target The target for this Task
-- @field Utilities.FiFo#FIFO Clients FiFo of Wrapper.Client#CLIENT planes executing this task
-- @field #boolean Repeat
-- @field #number repeats
-- @field #number RepeatNo
-- @field Wrapper.Marker#MARKER TargetMarker
-- @field #number SmokeColor
-- @field #number FlareColor
-- @field #table conditionSuccess
-- @field #table conditionFailure
-- @field Ops.PlayerTask#PLAYERTASKCONTROLLER TaskController
-- @field #number timestamp
-- @field #number lastsmoketime
-- @field #number coalition
-- @field #string Freetext
-- @field #string FreetextTTS
-- @field #string TaskSubType
-- @field #table NextTaskSuccess
-- @field #table NextTaskFailure
-- @field #string FinalState
-- @field #string TypeName
-- @field #number PreviousCount
-- @extends Core.Fsm#FSM


--- Global PlayerTaskNr counter
_PlayerTaskNr = 0

---
-- @field #PLAYERTASK
PLAYERTASK = {
  ClassName          = "PLAYERTASK",
  verbose            =   false,
  lid                =   nil,
  PlayerTaskNr       =   nil,
  Type               =   nil,
  TTSType            =   nil,
  Target             =   nil,
  Clients            =   nil,
  Repeat             =   false,
  repeats            =   0,
  RepeatNo           =   1,
  TargetMarker       =   nil,
  SmokeColor         =   nil,
  FlareColor         =   nil,
  conditionSuccess   =   {},
  conditionFailure   =   {},
  TaskController     =   nil,
  timestamp          =   0,
  lastsmoketime      =   0,
  Freetext           =   nil,
  FreetextTTS        =   nil,
  TaskSubType        =   nil,
  NextTaskSuccess    =   {},
  NextTaskFailure    =   {},
  FinalState         =   "none",
  PreviousCount      =   0,
  }

--- PLAYERTASK class version.
-- @field #string version
PLAYERTASK.version="0.1.28"

--- Generic task condition.
-- @type PLAYERTASK.Condition
-- @field #function func Callback function to check for a condition. Should return a #boolean.
-- @field #table arg Optional arguments passed to the condition callback function.

--- Constructor
-- @param #PLAYERTASK self
-- @param Ops.Auftrag#AUFTRAG.Type Type Type of this task
-- @param Ops.Target#TARGET Target Target for this task
-- @param #boolean Repeat Repeat this task if true (default = false)
-- @param #number Times Repeat on failure this many times if Repeat is true (default = 1)
-- @param #string TTSType TTS friendly task type name
-- @return #PLAYERTASK self
function PLAYERTASK:New(Type, Target, Repeat, Times, TTSType)

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #PLAYERTASK

  self.Type = Type

  self.Repeat = false
  self.repeats = 0
  self.RepeatNo = 1
  self.Clients = FIFO:New() -- Utilities.FiFo#FIFO
  self.TargetMarker = nil -- Wrapper.Marker#MARKER
  self.SmokeColor = SMOKECOLOR.Red
  self.conditionSuccess = {}
  self.conditionFailure = {}
  self.TaskController = nil -- Ops.PlayerTask#PLAYERTASKCONTROLLER
  self.timestamp = timer.getAbsTime()
  self.TTSType = TTSType or "close air support"
  self.lastsmoketime = 0

  if type(Repeat) == "boolean" and Repeat == true and type(Times) == "number" and Times > 1 then
    self.Repeat = true
    self.RepeatNo = Times or 1
  end

  _PlayerTaskNr = _PlayerTaskNr + 1

  self.PlayerTaskNr = _PlayerTaskNr

  self.lid=string.format("PlayerTask #%d %s | ", self.PlayerTaskNr, tostring(self.Type))

  if Target and Target.ClassName and Target.ClassName == "TARGET" then
    self.Target = Target
  elseif Target and Target.ClassName then
    self.Target = TARGET:New(Target)
  else
    self:E(self.lid.."*** NO VALID TARGET!")
    return self
  end

  self.PreviousCount = self.Target:CountTargets()

  self:T(self.lid.."Created.")

  -- FMS start state is PLANNED.
  self:SetStartState("Planned")

  -- PLANNED --> REQUESTED --> EXECUTING --> DONE
  self:AddTransition("*",            "Planned",          "Planned")   -- Task is in planning stage.
  self:AddTransition("*",            "Requested",        "Requested")   -- Task clients have been requested to join.
  self:AddTransition("*",            "ClientAdded",      "*")  -- Client has been added to the task
  self:AddTransition("*",            "ClientRemoved",    "*")  -- Client has been removed from the task
  self:AddTransition("*",            "Executing",        "Executing")   -- First client is executing the Task.
  self:AddTransition("*",            "Progress",         "*")   -- Task target count reduced - progress
  self:AddTransition("*",            "Done",             "Done")   -- All clients have reported that Task is done.
  self:AddTransition("*",            "Cancel",           "Done")   -- Command to cancel the Task.
  self:AddTransition("*",            "Success",          "Done")
  self:AddTransition("*",            "ClientAborted",    "*")
  self:AddTransition("*",            "Failed",           "Failed") -- Done or repeat --> PLANNED
  self:AddTransition("*",            "Status",           "*")
  self:AddTransition("*",            "Stop",             "Stopped")

  self:__Status(-5)
  return self

  ---
  -- Pseudo Functions
  ---

  --- On After "Planned" event. Task has been planned.
  -- @function [parent=#PLAYERTASK] OnAfterPlanned
  -- @param #PLAYERTASK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- On After "Requested" event. Task has been Requested.
  -- @function [parent=#PLAYERTASK] OnAfterRequested
  -- @param #PLAYERTASK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- On After "ClientAdded" event. Client has been added to the task.
  -- @function [parent=#PLAYERTASK] OnAfterClientAdded
  -- @param #PLAYERTASK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Client#CLIENT Client

  --- On After "ClientRemoved" event. Client has been removed from the task.
  -- @function [parent=#PLAYERTASK] OnAfterClientRemoved
  -- @param #PLAYERTASK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- On After "Executing" event. Task is executed by the 1st client.
  -- @function [parent=#PLAYERTASK] OnAfterExecuting
  -- @param #PLAYERTASK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- On After "Done" event. Task is done.
  -- @function [parent=#PLAYERTASK] OnAfterDone
  -- @param #PLAYERTASK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- On After "Cancel" event. Task has been cancelled.
  -- @function [parent=#PLAYERTASK] OnAfterCancel
  -- @param #PLAYERTASK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- On After "Planned" event. Task has been planned.
  -- @function [parent=#PLAYERTASK] OnAfterPilotPlanned
  -- @param #PLAYERTASK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- On After "Success" event. Task has been a success.
  -- @function [parent=#PLAYERTASK] OnAfterSuccess
  -- @param #PLAYERTASK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- On After "ClientAborted" event. A client has aborted the task.
  -- @function [parent=#PLAYERTASK] OnAfterClientAborted
  -- @param #PLAYERTASK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  --- On After "Failed" event. Task has been a failure.
  -- @function [parent=#PLAYERTASK] OnAfterFailed
  -- @param #PLAYERTASK self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

end

--- Constructor that automatically determines the task type based on the target.
-- @param #PLAYERTASK self
-- @param Ops.Target#TARGET Target Target for this task
-- @param #boolean Repeat Repeat this task if true (default = false)
-- @param #number Times Repeat on failure this many times if Repeat is true (default = 1)
-- @param #string TTSType TTS friendly task type name
-- @return #PLAYERTASK self
function PLAYERTASK:NewFromTarget(Target, Repeat, Times, TTSType)
    return PLAYERTASK:New(self:_GetTaskTypeForTarget(Target), Target, Repeat, Times, TTSType)
end

--- [Internal] Determines AUFTRAG type based on the target characteristics.
-- @param #PLAYERTASK self
-- @param Ops.Target#TARGET Target Target for this task
-- @return #string AUFTRAG.Type 
function PLAYERTASK:_GetTaskTypeForTarget(Target)

    local group = nil      --Wrapper.Group#GROUP
    local auftrag = nil

    if Target:IsInstanceOf("GROUP") then
        group = Target --Target is already a group.
    elseif Target:IsInstanceOf("SET_GROUP") then
        group = Target:GetFirst()
    elseif Target:IsInstanceOf("UNIT") then
        group = Target:GetGroup()
    elseif Target:IsInstanceOf("SET_UNIT") then
        group = Target:GetFirst():GetGroup()
    elseif Target:IsInstanceOf("AIRBASE") then

        auftrag = AUFTRAG.Type.BOMBRUNWAY

    elseif Target:IsInstanceOf("STATIC")
            or Target:IsInstanceOf("SET_STATIC")
            or Target:IsInstanceOf("SCENERY")
            or Target:IsInstanceOf("SET_SCENERY") then

        auftrag = AUFTRAG.Type.BOMBING

    elseif Target:IsInstanceOf("OPSZONE")
            or Target:IsInstanceOf("SET_OPSZONE") then
        auftrag = AUFTRAG.Type.CAPTUREZONE
    end

    if group then

        local category = group:GetCategory()
        local attribute = group:GetAttribute()

        if (category == Group.Category.AIRPLANE or category == Group.Category.HELICOPTER)
                and group:InAir() then

            auftrag = AUFTRAG.Type.INTERCEPT

        elseif category == Group.Category.GROUND or category == Group.Category.TRAIN then

            if attribute == GROUP.Attribute.GROUND_SAM
                    or attribute == GROUP.Attribute.GROUND_EWR then

                auftrag = AUFTRAG.Type.SEAD

            elseif attribute == GROUP.Attribute.GROUND_AAA
                    or attribute == GROUP.Attribute.GROUND_APC
                    or attribute == GROUP.Attribute.GROUND_IFV
                    or attribute == GROUP.Attribute.GROUND_TRUCK
                    or attribute == GROUP.Attribute.GROUND_TRAIN then

                auftrag = AUFTRAG.Type.BAI

            elseif attribute == GROUP.Attribute.GROUND_INFANTRY
                    or attribute == GROUP.Attribute.GROUND_ARTILLERY
                    or attribute == GROUP.Attribute.GROUND_TANK then

                auftrag = AUFTRAG.Type.CAS

            else

                auftrag = AUFTRAG.Type.BAI

            end

        elseif category == Group.Category.SHIP then

            auftrag = AUFTRAG.Type.ANTISHIP

        else
            self:T(self.lid .. "ERROR: Unknown Group category!")
        end
    end

    return auftrag

end


--- [Internal] Check OpsZone capture success condition.
-- @param #PLAYERTASK self
-- @param Ops.OpsZone#OPSZONE OpsZone The OpsZone target object.
-- @param #string CaptureSquadGroupNamePrefix The prefix of the group name that needs to capture the zone.
-- @param #number Coalition The coalition that needs to capture the zone.
-- @param #boolean CheckClientInZone Check if any of the clients are in zone.
-- @return #PLAYERTASK self
function PLAYERTASK:_CheckCaptureOpsZoneSuccess(OpsZone, CaptureSquadGroupNamePrefix, Coalition, CheckClientInZone)
    local isClientInZone = true
    if CheckClientInZone then
        isClientInZone = false
        for _, client in ipairs(self:GetClientObjects()) do
            local clientCoord = client:GetCoordinate()
            if OpsZone.zone:IsCoordinateInZone(clientCoord) then
                isClientInZone = true
                break
            end
        end
    end

    local isCaptureGroupInZone = false
    OpsZone:GetScannedGroupSet():ForEachGroup(function(group)
        if string.find(group:GetName(), CaptureSquadGroupNamePrefix) then
            isCaptureGroupInZone = true
        end
    end)

    return OpsZone:GetOwner() == Coalition and isClientInZone and isCaptureGroupInZone
end

--- [User] Override this function in order to implement custom logic if a player can join a task or not.
-- @param #PLAYERTASK self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Client#CLIENT Client
-- @return #boolean Outcome True if player can join the task, false if not
function PLAYERTASK:CanJoinTask(Group, Client)
    return true
end

--- [Internal] Add a PLAYERTASKCONTROLLER for this task
-- @param #PLAYERTASK self
-- @param Ops.PlayerTask#PLAYERTASKCONTROLLER Controller
-- @return #PLAYERTASK self
function PLAYERTASK:_SetController(Controller)
  self:T(self.lid.."_SetController")
  self.TaskController = Controller
  return self
end

--- [User] Set a coalition side for this task
-- @param #PLAYERTASK self
-- @param #number Coalition Coaltion side to add, e.g. coalition.side.BLUE
-- @return #PLAYERTASK self
function PLAYERTASK:SetCoalition(Coalition)
  self:T(self.lid.."SetCoalition")
  self.coalition = Coalition or coalition.side.BLUE
  return self
end

--- [User] Get the coalition side for this task
-- @param #PLAYERTASK self
-- @return #number Coalition Coaltion side, e.g. coalition.side.BLUE, or nil if not set
function PLAYERTASK:GetCoalition()
  self:T(self.lid.."GetCoalition")
  return self.coalition
end

--- [User] Get the Ops.Target#TARGET object for this task
-- @param #PLAYERTASK self
-- @return Ops.Target#TARGET Target
function PLAYERTASK:GetTarget()
  self:T(self.lid.."GetTarget")
  return self.Target
end

--- [USER] Add a free text description to this task.
-- @param #PLAYERTASK self
-- @param #string Text
-- @return #PLAYERTASK self
function PLAYERTASK:AddFreetext(Text)
  self:T(self.lid.."AddFreetext")
  self.Freetext = Text
  return self
end

--- [USER] Query if a task has free text description.
-- @param #PLAYERTASK self
-- @return #PLAYERTASK self
function PLAYERTASK:HasFreetext()
  self:T(self.lid.."HasFreetext")
  return self.Freetext ~= nil and true or false
end

--- [USER] Query if a task has free text TTS description.
-- @param #PLAYERTASK self
-- @return #PLAYERTASK self
function PLAYERTASK:HasFreetextTTS()
  self:T(self.lid.."HasFreetextTTS")
  return self.FreetextTTS ~= nil and true or false
end

--- [USER] Set a task sub type description to this task.
-- @param #PLAYERTASK self
-- @param #string Type
-- @return #PLAYERTASK self
function PLAYERTASK:SetSubType(Type)
  self:T(self.lid.."AddSubType")
  self.TaskSubType = Type
  return self
end

--- [USER] Get task sub type description from this task.
-- @param #PLAYERTASK self
-- @return #string Type or nil
function PLAYERTASK:GetSubType()
  self:T(self.lid.."GetSubType")
  return self.TaskSubType
end

--- [USER] Get the free text description from this task.
-- @param #PLAYERTASK self
-- @return #string Text
function PLAYERTASK:GetFreetext()
  self:T(self.lid.."GetFreetext")
  return self.Freetext  or self.FreetextTTS or "No Details"
end

--- [USER] Add a free text description for TTS to this task.
-- @param #PLAYERTASK self
-- @param #string TextTTS
-- @return #PLAYERTASK self
function PLAYERTASK:AddFreetextTTS(TextTTS)
  self:T(self.lid.."AddFreetextTTS")
  self.FreetextTTS = TextTTS
  return self
end

--- [USER] Get the free text TTS description from this task.
-- @param #PLAYERTASK self
-- @return #string Text
function PLAYERTASK:GetFreetextTTS()
  self:T(self.lid.."GetFreetextTTS")
  return self.FreetextTTS  or self.Freetext or "No Details"
end

--- [USER] Add a short free text description for the menu entry of this task.
-- @param #PLAYERTASK self
-- @param #string Text
-- @return #PLAYERTASK self
function PLAYERTASK:SetMenuName(Text)
  self:T(self.lid.."SetMenuName")
  self.Target.name = Text
  return self
end

--- [USER] Adds task success condition for dead STATIC, SET_STATIC, SCENERY or SET_SCENERY target object.
-- @return #PLAYERTASK self
-- @usage
-- -- We can use either STATIC, SET_STATIC, SCENERY or SET_SCENERY as target objects.
-- local mytask = PLAYERTASK:NewFromTarget(static, true, 50, "Destroy the target")
-- mytask:SetMenuName("Destroy Power Plant")
-- mytask:AddFreetext("Locate and destroy the power plant near Olenya.")
-- mytask:AddStaticObjectSuccessCondition()
--
-- playerTaskManager:AddPlayerTaskToQueue(mytask)
function PLAYERTASK:AddStaticObjectSuccessCondition()
    local task = self
    -- TODO Check if the killer is one of the task clients
    task:AddConditionSuccess(
            function(target)
                if target == nil then return false end

                local isDead = false
                if target:IsInstanceOf("STATIC")
                or target:IsInstanceOf("SCENERY")
                or target:IsInstanceOf("SET_SCENERY") then
                    isDead = (not target) or target:GetLife() < 1 or target:GetLife() < 0.2* target:GetLife0()
                elseif target:IsInstanceOf("SET_STATIC") then
                    local deadCount = 0
                    target:ForEachStatic(function(static)
                        if static:GetLife() < 1 or static:GetLife() < 0.2* static:GetLife0() then
                            deadCount = deadCount + 1
                        end
                    end)

                    if deadCount == target:Count() then
                        isDead = true
                    end
                end

                return isDead
            end, task:GetTarget()
    )

    -- TODO Check if the killer is one of the task clients
    --task:AddConditionFailure(
    --        function()
    --
    --        end)
    return self
end

--- [USER] Adds task success condition for AUFTRAG.Type.CAPTUREZONE for OpsZone or OpsZone set target object.
--- At least one of the task clients and one capture group need to be inside the zone in order for the capture to be successful.
-- @param #PLAYERTASK self
-- @param #SET_BASE CaptureSquadGroupNamePrefix The prefix of the group name that needs to capture the zone.
-- @param #number Coalition The coalition that needs to capture the zone.
-- @param #boolean CheckClientInZone If true, a CLIENT assigned to this task also needs to be in the zone for the task to be successful.
-- @return #PLAYERTASK self
-- @usage
-- -- We can use either STATIC, SET_STATIC, SCENERY or SET_SCENERY as target objects.
-- local opsZone = OPSZONE:New(zone, coalition.side.RED)
--
-- ...
--
-- -- We can use either OPSZONE or SET_OPSZONE.
-- local mytask = PLAYERTASK:NewFromTarget(opsZone, true, 50, "Capture the zone")
-- mytask:SetMenuName("Capture the ops zone")
-- mytask:AddFreetext("Transport capture squad to the ops zone.")
--
-- -- We set CaptureSquadGroupNamePrefix the group name prefix as set in the ME or the spawn of the group that need to be present at the OpsZone like a capture squad,
-- -- and set the capturing Coalition in order to trigger a successful task.
-- mytask:AddOpsZoneCaptureSuccessCondition("capture-squad", coalition.side.BLUE, false)
--
-- playerTaskManager:AddPlayerTaskToQueue(mytask)
function PLAYERTASK:AddOpsZoneCaptureSuccessCondition(CaptureSquadGroupNamePrefix, Coalition, CheckClientInZone)
    local task = self
    task:AddConditionSuccess(
            function(target)
                if target:IsInstanceOf("OPSZONE") then
                    return task:_CheckCaptureOpsZoneSuccess(target, CaptureSquadGroupNamePrefix, Coalition, CheckClientInZone or true)
                elseif target:IsInstanceOf("SET_OPSZONE") then
                    local successes = 0
                    local isClientInZone = false
                    target:ForEachZone(function(opszone)
                        if task:_CheckCaptureOpsZoneSuccess(opszone, CaptureSquadGroupNamePrefix, Coalition, CheckClientInZone or true) then
                            successes = successes + 1
                        end

                        for _, client in ipairs(task:GetClientObjects()) do
                            local clientCoord = client:GetCoordinate()
                            if opszone.zone:IsCoordinateInZone(clientCoord) then
                                isClientInZone = true
                                break
                            end
                        end
                    end)
                    return successes == target:Count() and isClientInZone
                end

                return false
            end, task:GetTarget()
    )
    return self
end

--- [USER] Adds task success condition for AUFTRAG.Type.RECON when a client is at a certain LOS distance from the target.
-- @param #PLAYERTASK self
-- @param #number MinDistance (Optional) Minimum distance in meters from client to target in LOS for success condition. (Default 5 NM)
-- @return #PLAYERTASK self
-- @usage
-- -- target can be any object that has a `GetCoordinate()` function like STATIC, GROUP, ZONE...
-- local mytask = PLAYERTASK:New(AUFTRAG.Type.RECON, ZONE:New("WF Zone"), true, 50, "Deep Earth")
-- mytask:SetMenuName("Recon weapon factory")
-- mytask:AddFreetext("Locate and investigate underground weapons factory near Kovdor.")
--
-- -- We set the MinDistance (optional) in meters for the client to be in LOS from the target in order to trigger a successful task.
-- mytask:AddReconSuccessCondition(10000) -- 10 km (default is 5 NM if not set)
--
-- playerTaskManager:AddPlayerTaskToQueue(mytask)
function PLAYERTASK:AddReconSuccessCondition(MinDistance)
    local task = self
    task:AddConditionSuccess(
            function(target)
                local targetLocation = target:GetCoordinate()
                local minD = MinDistance or UTILS.NMToMeters(5)
                for _, client in ipairs(task:GetClientObjects()) do
                    local clientCoord = client:GetCoordinate()
                    local distance = clientCoord:Get2DDistance(targetLocation)
                    local isLos = land.isVisible(clientCoord:GetVec3(), targetLocation:GetVec3())

                    if distance < minD and isLos then
                        return true
                    end
                end
                return false
            end, task:GetTarget())

    return self
end

--- [USER] Adds a time limit for the task to be completed.
-- @param #PLAYERTASK self
-- @param #number TimeLimit Time limit in seconds for the task to be completed. (Default 0 = no time limit)
-- @return #PLAYERTASK self
-- @usage
-- local mytask = PLAYERTASK:New(AUFTRAG.Type.RECON, ZONE:New("WF Zone"), true, 50, "Deep Earth")
-- mytask:SetMenuName("Recon weapon factory")
-- mytask:AddFreetext("Locate and investigate underground weapons factory near Kovdor.")
-- mytask:AddReconSuccessCondition(10000) -- 10 km
--
-- -- We set the TimeLimit to 10 minutes (600 seconds) from the moment the task is started, once the time has passed and the task is not yet successful it will trigger a failure.
-- mytask:AddTimeLimitFailureCondition(600)
--
-- playerTaskManager:AddPlayerTaskToQueue(mytask)
function PLAYERTASK:AddTimeLimitFailureCondition(TimeLimit)
    local task = self
    TimeLimit = TimeLimit or 0
    task.StartTime = -1
    task:AddConditionFailure(
            function()
                if task.StartTime == -1 then
                    task.StartTime = timer.getTime()
                end
                return TimeLimit > 0 and timer.getTime() - task.StartTime > TimeLimit
            end)
    return self
end

--- [USER] Add a task to be assigned to same clients when task was a success.
-- @param #PLAYERTASK self
-- @param Ops.PlayerTask#PLAYERTASK Task
-- @return #PLAYERTASK self
function PLAYERTASK:AddNextTaskAfterSuccess(Task)
  self:T(self.lid.."AddNextTaskAfterSuccess")
  table.insert(self.NextTaskSuccess,Task)
  return self
end

--- [USER] Add a task to be assigned to same clients when task was a failure.
-- @param #PLAYERTASK self
-- @param Ops.PlayerTask#PLAYERTASK Task
-- @return #PLAYERTASK self
function PLAYERTASK:AddNextTaskAfterFailure(Task)
  self:T(self.lid.."AddNextTaskAfterFailure")
  table.insert(self.NextTaskFailure,Task)
  return self
end

--- [User] Check if task is done
-- @param #PLAYERTASK self
-- @return #boolean done
function PLAYERTASK:IsDone()
  self:T(self.lid.."IsDone?")
  local IsDone = false
  local state = self:GetState()
  if state == "Done" or state == "Stopped" then
    IsDone = true
  end
  return IsDone
end

--- [User] Check if task is NOT done
-- @param #PLAYERTASK self
-- @return #boolean done
function PLAYERTASK:IsNotDone()
  self:T(self.lid.."IsNotDone?")
  local IsNotDone = not self:IsDone()
  return IsNotDone
end

--- [User] Check if PLAYERTASK has clients assigned to it.
-- @param #PLAYERTASK self
-- @return #boolean hasclients
function PLAYERTASK:HasClients()
  self:T(self.lid.."HasClients?")
  local hasclients = self:CountClients() > 0 and true or false
  return hasclients
end

--- [User] Get client names assigned as table of #strings
-- @param #PLAYERTASK self
-- @return #table clients
-- @return #number clientcount
function PLAYERTASK:GetClients()
  self:T(self.lid.."GetClients")
  local clientlist = self.Clients:GetIDStackSorted() or {}
  local count = self.Clients:Count()
  return clientlist, count
end

--- [User] Get #CLIENT objects assigned as table
-- @param #PLAYERTASK self
-- @return #table clients
-- @return #number clientcount
function PLAYERTASK:GetClientObjects()
  self:T(self.lid.."GetClientObjects")
  local clientlist = self.Clients:GetDataTable() or {}
  local count = self.Clients:Count()
  return clientlist, count
end

--- [User] Count clients
-- @param #PLAYERTASK self
-- @return #number clientcount
function PLAYERTASK:CountClients()
  self:T(self.lid.."CountClients")
  return self.Clients:Count()
end

--- [User] Check if a player name is assigned to this task
-- @param #PLAYERTASK self
-- @param #string Name
-- @return #boolean HasName
function PLAYERTASK:HasPlayerName(Name)
  self:T(self.lid.."HasPlayerName?")
  return self.Clients:HasUniqueID(Name)
end

--- [User] Add a client to this task
-- @param #PLAYERTASK self
-- @param Wrapper.Client#CLIENT Client
-- @return #PLAYERTASK self
function PLAYERTASK:AddClient(Client)
  self:T(self.lid.."AddClient")
  local name = Client:GetPlayerName()
  if not self.Clients:HasUniqueID(name) then
    self.Clients:Push(Client,name)
    self:__ClientAdded(-2,Client)
  end
  if self.TaskController and self.TaskController.Scoring then
    self.TaskController.Scoring:_AddPlayerFromUnit(Client)
  end
  return self
end

--- [User] Remove a client from this task
-- @param #PLAYERTASK self
-- @param Wrapper.Client#CLIENT Client
-- @param #string Name Name of the client
-- @return #PLAYERTASK self
function PLAYERTASK:RemoveClient(Client,Name)
  self:T(self.lid.."RemoveClient")
  local name = Name or Client:GetPlayerName()
  if self.Clients:HasUniqueID(name) then
    self.Clients:PullByID(name)
    if self.verbose then
      self.Clients:Flush()
    end
    self:__ClientRemoved(-2,Client)
    if self.Clients:Count() == 0 then
      self:__Failed(-1)
    end
  end
  return self
end

--- [User] Client has aborted task this task
-- @param #PLAYERTASK self
-- @param Wrapper.Client#CLIENT Client (optional)
-- @return #PLAYERTASK self
function PLAYERTASK:ClientAbort(Client)
  self:T(self.lid.."ClientAbort")
  if Client and Client:IsAlive() then
    self:RemoveClient(Client)
    self:__ClientAborted(-1,Client)
    return self
  else
    -- no client given, abort whole task if no one else is assigned
    if self.Clients:Count() == 0 then
      -- return to planned state if repeat    
      self:__Failed(-1)
    end
  end
  return self
end

--- [User] Create target mark on F10 map
-- @param #PLAYERTASK self
-- @param #string Text (optional) Text to show on the marker
-- @param #number Coalition (optional) Coalition this marker is for. Default = All.
-- @param #boolean ReadOnly (optional) Make target marker read-only. Default = false.
-- @return #PLAYERTASK self
function PLAYERTASK:MarkTargetOnF10Map(Text,Coalition,ReadOnly)
  self:T(self.lid.."MarkTargetOnF10Map")
  if self.Target then
    local coordinate = self.Target:GetCoordinate()
    if coordinate then
      if self.TargetMarker then
        -- Marker exists, delete one first
        self.TargetMarker:Remove()
      end
      local text = Text or ("Target of "..self.lid)
      self.TargetMarker = MARKER:New(coordinate,text)
      if ReadOnly then
        self.TargetMarker:ReadOnly()
      end
      if Coalition then
        self.TargetMarker:ToCoalition(Coalition)
      else
        self.TargetMarker:ToAll()
      end
    end
  end
  return self
end

--- [User] Smoke Target
-- @param #PLAYERTASK self
-- @param #number Color, defaults to SMOKECOLOR.Red
-- @return #PLAYERTASK self
function PLAYERTASK:SmokeTarget(Color)
  self:T(self.lid.."SmokeTarget")
  local color = Color or SMOKECOLOR.Red
  if not self.lastsmoketime then self.lastsmoketime = 0 end
  local TDiff = timer.getAbsTime() - self.lastsmoketime
  if self.Target and TDiff > 299 then
    local coordinate = self.Target:GetAverageCoordinate()
    if coordinate then
      coordinate:Smoke(color)
      self.lastsmoketime = timer.getAbsTime()
    end
  end
  return self
end

--- [User] Flare Target
-- @param #PLAYERTASK self
-- @param #number Color, defaults to FLARECOLOR.Red
-- @return #PLAYERTASK self
function PLAYERTASK:FlareTarget(Color)
  self:T(self.lid.."SmokeTarget")
  local color = Color or FLARECOLOR.Red
  if self.Target then
    local coordinate = self.Target:GetAverageCoordinate()
    if coordinate then
      coordinate:Flare(color,0)
    end
  end
  return self
end

--- [User] Illuminate Target Area
-- @param #PLAYERTASK self
-- @param #number Power Power of illumination bomb in Candela. Default 1000 cd.
-- @param #number Height Height above target used to release the bomb, default 150m.
-- @return #PLAYERTASK self
function PLAYERTASK:IlluminateTarget(Power,Height)
  self:T(self.lid.."IlluminateTarget")
  local Power = Power or 1000
  local Height = Height or 150
  if self.Target then
    local coordinate = self.Target:GetAverageCoordinate()
    if coordinate then
    local bcoord = COORDINATE:NewFromVec2( coordinate:GetVec2(), Height )
      bcoord:IlluminationBomb(Power)
    end
  end
  return self
end

-- success / failure function addion courtesy @FunkyFranky.

--- [User] Add success condition.
-- @param #PLAYERTASK self
-- @param #function ConditionFunction If this function returns `true`, the mission is cancelled.
-- @param ... Condition function arguments if any.
-- @return #PLAYERTASK self
function PLAYERTASK:AddConditionSuccess(ConditionFunction, ...)

  local condition={} --#PLAYERTASK.Condition

  condition.func=ConditionFunction
  condition.arg={}
  if arg then
    condition.arg=arg
  end

  table.insert(self.conditionSuccess, condition)

  return self
end

--- [User] Add failure condition.
-- @param #PLAYERTASK self
-- @param #function ConditionFunction If this function returns `true`, the task is cancelled.
-- @param ... Condition function arguments if any.
-- @return #PLAYERTASK self
function PLAYERTASK:AddConditionFailure(ConditionFunction, ...)

  local condition={} --#PLAYERTASK.Condition

  condition.func=ConditionFunction
  condition.arg={}
  if arg then
    condition.arg=arg
  end

  table.insert(self.conditionFailure, condition)

  return self
end

--- [Internal] Check if any of the given conditions is true.
-- @param #PLAYERTASK self
-- @param #table Conditions Table of conditions.
-- @return #boolean If true, at least one condition is true.
function PLAYERTASK:_EvalConditionsAny(Conditions)

  -- Any stop condition must be true.
  for _,_condition in pairs(Conditions or {}) do
    local condition=_condition --#AUFTRAG.Condition

    -- Call function.
    local istrue=condition.func(unpack(condition.arg))

    -- Any true will return true.
    if istrue then
      return true
    end

  end

  -- No condition was true.
  return false
end

--- [Internal] On after status call
-- @param #PLAYERTASK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #PLAYERTASK self
function PLAYERTASK:onafterStatus(From, Event, To)
  self:T({From, Event, To})
  self:T(self.lid.."onafterStatus")
  
  local status = self:GetState()
  
  if status == "Stopped" then return self end
  
  -- update marker in case target is moving
  if self.TargetMarker then
    local coordinate = self.Target:GetCoordinate() 
    self.TargetMarker:UpdateCoordinate(coordinate,0.5) 
  end
  
  -- Check Target status
  local targetdead = false
  
  if self.Type ~= AUFTRAG.Type.CTLD and self.Type ~= AUFTRAG.Type.CSAR then
    if self.Target:IsDead() or self.Target:IsDestroyed() or self.Target:CountTargets() == 0 then
      targetdead = true
      self:__Success(-2)
      status = "Success"
      return self
    end
  end
  
  local clientsalive = false  
    
  if status == "Executing" then    
    -- Check Clients alive
    local ClientTable = self.Clients:GetDataTable()
    for _,_client in pairs(ClientTable) do
      local client = _client -- Wrapper.Client#CLIENT
      if client:IsAlive() then
        clientsalive=true -- one or more clients alive
      end
    end
    
    -- Failed?
    if status == "Executing" and (not clientsalive) and (not targetdead) then
      self:__Failed(-2)
      status = "Failed"
    end
  end 
  
  -- Continue if we are not done
  if status ~= "Done" and status ~= "Stopped" then 
    -- Any success condition true?
    local successCondition=self:_EvalConditionsAny(self.conditionSuccess)
  
    -- Any failure condition true?
    local failureCondition=self:_EvalConditionsAny(self.conditionFailure)
  
    if failureCondition and status ~= "Failed" then
      self:__Failed(-2)
      status = "Failed"
    elseif successCondition then
      self:__Success(-2)
      status = "Success"
    end
    
    if status ~= "Failed" and status ~= "Success" then
      -- Partial Success?
      local targetcount = self.Target:CountTargets()
      if targetcount < self.PreviousCount then
        -- Progress
        self:__Progress(-2,targetcount)
        self.PreviousCount = targetcount
      end
    end
    
    if self.verbose then
      self:I(self.lid.."Target dead: "..tostring(targetdead).." | Clients alive: " .. tostring(clientsalive))
    end
  
    self:__Status(-20)
  elseif status ~= "Stopped" then
    self:__Stop(-1)
  end
  
  return self
end


--- [Internal] On after progress call
-- @param #PLAYERTASK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #number TargetCount
-- @return #PLAYERTASK self
function PLAYERTASK:onafterProgress(From, Event, To, TargetCount)
  self:T({From, Event, To})
  if self.TaskController then
    if self.TaskController.Scoring then
      local clients,count = self:GetClientObjects()
      if count > 0 then
        for _,_client in pairs(clients) do
          self.TaskController.Scoring:AddGoalScore(_client,self.Type,nil,10) 
        end
      end
    end
    self.TaskController:__TaskProgress(-1,self,TargetCount) 
  end
  return self
end

--- [Internal] On after planned call
-- @param #PLAYERTASK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #PLAYERTASK self
function PLAYERTASK:onafterPlanned(From, Event, To)
  self:T({From, Event, To})
  self.timestamp = timer.getAbsTime()
  return self
end

--- [Internal] On after requested call
-- @param #PLAYERTASK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #PLAYERTASK self
function PLAYERTASK:onafterRequested(From, Event, To)
  self:T({From, Event, To})
  self.timestamp = timer.getAbsTime()
  return self
end

--- [Internal] On after executing call
-- @param #PLAYERTASK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #PLAYERTASK self
function PLAYERTASK:onafterExecuting(From, Event, To)
  self:T({From, Event, To})
  self.timestamp = timer.getAbsTime()
  return self
end

--- [Internal] On after status call
-- @param #PLAYERTASK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #PLAYERTASK self
function PLAYERTASK:onafterStop(From, Event, To)
  self:T({From, Event, To})
  self.timestamp = timer.getAbsTime()
  return self
end

--- [Internal] On after client added call
-- @param #PLAYERTASK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Wrapper.Client#CLIENT Client
-- @return #PLAYERTASK self
function PLAYERTASK:onafterClientAdded(From, Event, To, Client)
  self:T({From, Event, To})
  if Client and self.verbose then
    local text = string.format("Player %s joined task %03d!",Client:GetPlayerName() or "Generic",self.PlayerTaskNr)
    self:T(self.lid..text)
  end
  self.timestamp = timer.getAbsTime()
  return self
end

--- [Internal] On after done call
-- @param #PLAYERTASK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #PLAYERTASK self
function PLAYERTASK:onafterDone(From, Event, To)
  self:T({From, Event, To})
  if self.TaskController then
    self.TaskController:__TaskDone(-1,self)
  end
  self.timestamp = timer.getAbsTime()
  self:__Stop(-1)
  return self
end

--- [Internal] On after cancel call
-- @param #PLAYERTASK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #PLAYERTASK self
function PLAYERTASK:onafterCancel(From, Event, To)
  self:T({From, Event, To})
  if self.TaskController then
    self.TaskController:__TaskCancelled(-1,self)
  end
  self.timestamp = timer.getAbsTime()
  self.FinalState = "Cancelled"
  self:__Done(-1)
  return self
end

--- [Internal] On after success call
-- @param #PLAYERTASK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #PLAYERTASK self
function PLAYERTASK:onafterSuccess(From, Event, To)
  self:T({From, Event, To})
  if self.TaskController then
    self.TaskController:__TaskSuccess(-1,self)
  end
  if self.TargetMarker then
    self.TargetMarker:Remove()
  end
  if self.TaskController and self.TaskController.Scoring then
    local clients,count = self:GetClientObjects()
    if count > 0 then
      for _,_client in pairs(clients) do
        local auftrag = self:GetSubType()
        self.TaskController.Scoring:AddGoalScore(_client,self.Type,nil,self.TaskController.Scores[self.Type]) 
      end
    end
  end 
  self.timestamp = timer.getAbsTime()
  self.FinalState = "Success"
  self:__Done(-1)
  return self
end

--- [Internal] On after failed call
-- @param #PLAYERTASK self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #PLAYERTASK self
function PLAYERTASK:onafterFailed(From, Event, To)
  self:T({From, Event, To})
  self.repeats = self.repeats + 1
  -- repeat on failed?
  if self.Repeat and (self.repeats <= self.RepeatNo) then
    if self.TaskController then
      self.TaskController:__TaskRepeatOnFailed(-1,self)
    end
    self:__Planned(-1)
    return self
  else
    if self.TargetMarker then
      self.TargetMarker:Remove()
    end
    self.FinalState = "Failed"
    if self.TaskController then
      self.TaskController:__TaskFailed(-1,self)
    end
    self:__Done(-1.5)
  end
  if self.TaskController.Scoring then
    local clients,count = self:GetClientObjects()
    if count > 0 then
      for _,_client in pairs(clients) do
        local auftrag = self:GetSubType()
        self.TaskController.Scoring:AddGoalScore(_client,self.Type,nil,-self.TaskController.Scores[self.Type]) 
      end
    end
  end 
  self.timestamp = timer.getAbsTime()
  return self
end
-------------------------------------------------------------------------------------------------------------------
-- END PLAYERTASK
-------------------------------------------------------------------------------------------------------------------
end

do
-------------------------------------------------------------------------------------------------------------------
-- PLAYERTASKCONTROLLER
-- TODO: PLAYERTASKCONTROLLER
-- DONE Playername customized
-- DONE Coalition-level screen info to SET based
-- DONE Flash directions
-- DONE less rebuilds menu, Task info menu available after join
-- DONE Limit menu entries
-- DONE Integrated basic CTLD tasks
-- DONE Integrate basic CSAR tasks
-------------------------------------------------------------------------------------------------------------------

--- PLAYERTASKCONTROLLER class.
-- @type PLAYERTASKCONTROLLER
-- @field #string ClassName Name of the class.
-- @field #boolean verbose Switch verbosity.
-- @field #string lid Class id string for output to DCS log file.
-- @field Utilities.FiFo#FIFO TargetQueue
-- @field Utilities.FiFo#FIFO TaskQueue
-- @field Utilities.FiFo#FIFO TasksPerPlayer
-- @field Utilities.FiFo#FIFO PrecisionTasks
-- @field Core.Set#SET_CLIENT ClientSet
-- @field Core.Set#SET_CLIENT ActiveClientSet
-- @field #string ClientFilter
-- @field #string Name
-- @field #string Type
-- @field #boolean UseGroupNames
-- @field #table PlayerMenu
-- @field #boolean usecluster
-- @field #number ClusterRadius
-- @field #string MenuName
-- @field #boolean NoScreenOutput
-- @field #number TargetRadius
-- @field #boolean UseWhiteList
-- @field #table WhiteList
-- @field #boolean UseBlackList
-- @field #table BlackList
-- @field Core.TextAndSound#TEXTANDSOUND gettext
-- @field #string locale
-- @field #boolean precisionbombing
-- @field Ops.FlightGroup#FLIGHTGROUP LasingDrone
-- @field Core.MarkerOps_Base#MARKEROPS_BASE MarkerOps
-- @field #boolean taskinfomenu
-- @field #boolean MarkerReadOnly
-- @field #table FlashPlayer List of player who switched Flashing Direction Info on
-- @field #boolean AllowFlash Flashing directions for players allowed
-- @field #number menuitemlimit
-- @field #boolean activehasinfomenu
-- @field #number holdmenutime
-- @field #table customcallsigns
-- @field #boolean ShortCallsign
-- @field #boolean Keepnumber
-- @field #table CallsignTranslations
-- @field #table PlayerFlashMenu
-- @field #table PlayerJoinMenu
-- @field #table PlayerInfoMenu
-- @field #boolean noflaresmokemenu
-- @field #boolean illumenu
-- @field #boolean TransmitOnlyWithPlayers
-- @field #boolean buddylasing
-- @field Ops.PlayerRecce#PLAYERRECCE PlayerRecce
-- @field #number Coalition
-- @field Core.Menu#MENU_MISSION MenuParent
-- @field #boolean ShowMagnetic Also show magnetic angles
-- @field #boolean InfoHasCoordinate
-- @field #boolean InfoHasLLDDM
-- @field #table PlayerMenuTag
-- @field #boolean UseTypeNames
-- @field Functional.Scoring#SCORING Scoring
-- @field Core.ClientMenu#CLIENTMENUMANAGER JoinTaskMenuTemplate
-- @field Core.ClientMenu#CLIENTMENU JoinMenu
-- @field Core.ClientMenu#CLIENTMENU JoinTopMenu
-- @field Core.ClientMenu#CLIENTMENU JoinInfoMenu
-- @field Core.ClientMenu#CLIENTMENUMANAGER ActiveTaskMenuTemplate
-- @field Core.ClientMenu#CLIENTMENU ActiveTopMenu
-- @field Core.ClientMenu#CLIENTMENU ActiveInfoMenu
-- @field Core.ClientMenu#CLIENTMENU MenuNoTask
-- @field #boolean InformationMenu Show Radio Info Menu
-- @field #number TaskInfoDuration How long to show the briefing info on the screen
-- @extends Core.Fsm#FSM

---
--
-- *It is our attitude at the beginning of a difficult task which, more than anything else, which will affect its successful outcome.* (William James)
--
-- ===
-- 
-- # PLAYERTASKCONTROLLER 
-- 
--    * Simplifies defining, executing and controlling of Player tasks
--    * FSM events when a mission is added, done, successful or failed, replanned
--    * Ready to use SRS and localization
--    * Mission locations can be smoked, flared and marked on the map
-- 
-- ## 1 Overview
-- 
-- PLAYERTASKCONTROLLER is used to auto-create (optional) and control tasks for players. It can be set up as Air-to-Ground (A2G, main focus), Air-to-Ship (A2S) or Air-to-Air (A2A) controller.
-- For the latter task type, also have a look at the @{Ops.AWACS#AWACS} class which allows for more complex scenarios.
-- One task at a time can be joined by the player from the F10 menu. A task can be joined by multiple players. Once joined, task information is available via the F10 menu, the task location
-- can be marked on the map and for A2G/S targets, the target can be marked with smoke and flares.
-- 
-- For the mission designer, tasks can be auto-created by means of detection with the integrated @{Ops.Intel#INTEL} class setup, or be manually added to the task queue.
-- 
-- ## 2 Task Types
-- 
-- Targets can be of types GROUP, SET\_GROUP, UNIT, SET\_UNIT, STATIC, SET\_STATIC, SET\_SCENERY, AIRBASE, ZONE or COORDINATE. The system will auto-create tasks for players from these targets.
-- Tasks are created as @{Ops.PlayerTask#PLAYERTASK} objects, which leverage @{Ops.Target#TARGET} for the management of the actual target. The system creates these task types
-- from the target objects:  
-- 
--  * A2A - AUFTRAG.Type.INTERCEPT
--  * A2S - AUFTRAG.Type.ANTISHIP
--  * A2G - AUFTRAG.Type.CAS, AUFTRAG.Type.BAI, AUFTRAG.Type.SEAD, AUFTRAG.Type.BOMBING, AUFTRAG.Type.PRECISIONBOMBING, AUFTRAG.Type.BOMBRUNWAY
--  * A2GS - A2S and A2G combined
-- 
-- Task types are derived from @{Ops.Auftrag#AUFTRAG}:   
--  
--  * CAS - Close air support, created to attack ground units, where friendly ground units are around the location in a bespoke radius (default: 500m/1km diameter)
--  * BAI - Battlefield air interdiction, same as above, but no friendlies around
--  * SEAD - Same as CAS, but the enemy ground units field AAA, SAM or EWR units
--  * Bombing - Against static targets
--  * Precision Bombing - (if enabled) Laser-guided bombing, against **static targets** and **high-value (non-SAM) ground targets (MBTs etc)**
--  * Bomb Runway - Against Airbase runways (in effect, drop bombs over the runway)
--  * ZONE and COORDINATE - Targets will be scanned for GROUND or STATIC enemy units and tasks created from these
--  * Intercept - Any airborne targets, if the controller is of type "A2A"
--  * Anti-Ship - Any ship targets, if the controller is of type "A2S"
--  * CTLD - Combat transport and logistics deployment
--  * CSAR - Combat search and rescue
--  * RECON - Identify targets
--  * CAPTUREZONE - Capture an Ops.OpsZone#OPSZONE
--  * Any #string name can be passed as Auftrag type, but then you need to make sure to define a success condition, and possibly also add the task type to the standard scoring list: `PLAYERTASKCONTROLLER.Scores["yournamehere"]=100`
--  
-- ## 3 Task repetition
--  
-- On failure, tasks will be replanned by default for a maximum of 5 times.
-- 
-- ## 3.1 Pre-configured success conditions
-- 
-- Pre-configured success conditions for #PLAYERTASK tasks are available as follows:
-- 
-- `mytask:AddStaticObjectSuccessCondition()` -- success if static object is at least 80% dead
-- 
-- `mytask:AddOpsZoneCaptureSuccessCondition(CaptureSquadGroupNamePrefix,Coalition)`  -- success if a squad of the given (partial) name and coalition captures the OpsZone
-- 
-- `mytask:AddReconSuccessCondition(MinDistance)`  -- success if object is in line-of-sight with the given min distance in NM
-- 
-- `mytask:AddTimeLimitSuccessCondition(TimeLimit)` -- failure if the task is not completed within the time limit in seconds given
-- 
-- ## 3.2 Task chaining
-- 
-- You can create chains of tasks, which will depend on success or failure of the previous task with the following commands:
-- 
-- `mytask:AddNextTaskAfterSuccess(FollowUpTask)` and  
-- 
-- `mytask:AddNextTaskAfterFailure(FollowUpTask)`
-- 
-- ## 4 SETTINGS, SRS and language options (localization)
-- 
-- The system can optionally communicate to players via SRS. Also localization is available, both "en" and "de" has been build in already.
-- Player and global @{Core.Settings#SETTINGS} for coordinates will be observed.
--  
-- ## 5 Setup  
-- 
-- A basic setup is very simple:
-- 
--            -- Settings - we want players to have a settings menu, be on imperial measures, and get directions as BR
--            _SETTINGS:SetPlayerMenuOn()
--            _SETTINGS:SetImperial()
--            _SETTINGS:SetA2G_BR()
-- 
--            -- Set up the A2G task controller for the blue side named "82nd Airborne"
--            local taskmanager = PLAYERTASKCONTROLLER:New("82nd Airborne",coalition.side.BLUE,PLAYERTASKCONTROLLER.Type.A2G)
--            
--            -- set locale to English
--            taskmanager:SetLocale("en")
--            
--            -- Set up detection with grup names *containing* "Blue Recce", these will add targets to our controller via detection. Can be e.g. a drone.
--            taskmanager:SetupIntel("Blue Recce")
--            
--            -- Add a single Recce group name "Blue Humvee"
--            taskmanager:AddAgent(GROUP:FindByName("Blue Humvee"))
--            
--            -- Set the callsign for SRS and Menu name to be "Groundhog"
--            taskmanager:SetMenuName("Groundhog")
--            
--            -- Add accept- and reject-zones for detection
--            -- Accept zones are handy to limit e.g. the engagement to a certain zone. The example is a round, mission editor created zone named "AcceptZone"
--            taskmanager:AddAcceptZone(ZONE:New("AcceptZone"))
--            
--            -- Reject zones are handy to create borders. The example is a ZONE_POLYGON, created in the mission editor, late activated with waypoints, 
--            -- named "AcceptZone#ZONE_POLYGON"
--            taskmanager:AddRejectZone(ZONE:FindByName("RejectZone"))
--            
--            -- Set up using SRS for messaging
--            local hereSRSPath = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio"
--            local hereSRSPort = 5002
--            -- local hereSRSGoogle = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio\\yourkey.json"
--            taskmanager:SetSRS({130,255},{radio.modulation.AM,radio.modulation.AM},hereSRSPath,"female","en-GB",hereSRSPort,"Microsoft Hazel Desktop",0.7,hereSRSGoogle)
--            
--            -- Controller will announce itself under these broadcast frequencies, handy to use cold-start frequencies here of your aircraft
--            taskmanager:SetSRSBroadcast({127.5,305},{radio.modulation.AM,radio.modulation.AM})
--            
--            -- Example: Manually add an AIRBASE as a target
--            taskmanager:AddTarget(AIRBASE:FindByName(AIRBASE.Caucasus.Senaki_Kolkhi))
--            
--            -- Example: Manually add a COORDINATE as a target
--            taskmanager:AddTarget(GROUP:FindByName("Scout Coordinate"):GetCoordinate())
--            
--            -- Set a whitelist for tasks, e.g. skip SEAD tasks
--            taskmanager:SetTaskWhiteList({AUFTRAG.Type.CAS, AUFTRAG.Type.BAI, AUFTRAG.Type.BOMBING, AUFTRAG.Type.BOMBRUNWAY})
--            
--            -- Set target radius
--            taskmanager:SetTargetRadius(1000)
-- 
-- ## 6 Localization
-- 
-- Localization for English and German texts are build-in. Default setting is English. Change with @{#PLAYERTASKCONTROLLER.SetLocale}()
-- 
-- ### 6.1 Adding Localization
-- 
-- A list of fields to be defined follows below. **Note** that in some cases `string.format()` is used to format texts for screen and SRS. 
-- Hence, the `%d`, `%s` and `%f` special characters need to appear in the exact same amount and order of appearance in the localized text or it will create errors.
-- To add a localization, the following texts need to be translated and set in your mission script **before** @{#PLAYERTASKCONTROLLER.New}():   
-- 
--            PLAYERTASKCONTROLLER.Messages = {
--              EN = {
--                TASKABORT = "Task aborted!",
--                NOACTIVETASK = "No active task!",
--                FREQUENCIES = "frequencies ",
--                FREQUENCY = "frequency %.3f",
--                BROADCAST = "%s, %s, switch to %s for task assignment!",
--                CASTTS = "close air support",
--                SEADTTS = "suppress air defense",
--                BOMBTTS = "bombing",
--                PRECBOMBTTS = "precision bombing",
--                BAITTS = "battle field air interdiction",
--                ANTISHIPTTS = "anti-ship",
--                INTERCEPTTS = "intercept",
--                BOMBRUNWAYTTS = "bomb runway",
--                HAVEACTIVETASK = "You already have one active task! Complete it first!",
--                PILOTJOINEDTASK = "%s, %s. You have been assigned %s task %03d",
--                TASKNAME = "%s Task ID %03d",
--                TASKNAMETTS = "%s Task ID %03d",
--                THREATHIGH = "high",
--                THREATMEDIUM = "medium",
--                THREATLOW = "low",
--                THREATTEXT = "%s\nThreat: %s\nTargets left: %d\nCoord: %s",
--                THREATTEXTTTS = "%s, %s. Target information for %s. Threat level %s. Targets left %d. Target location %s.",
--                MARKTASK = "%s, %s, copy, task %03d location marked on map!",
--                SMOKETASK = "%s, %s, copy, task %03d location smoked!",
--                FLARETASK = "%s, %s, copy, task %03d location illuminated!",
--                ABORTTASK = "All stations, %s, %s has aborted %s task %03d!",
--                UNKNOWN = "Unknown",
--                MENUTASKING = " Tasking ",
--                MENUACTIVE = "Active Task",
--                MENUINFO = "Info",
--                MENUMARK = "Mark on map",
--                MENUSMOKE = "Smoke",
--                MENUFLARE = "Flare",
--                MENUILLU = "Illuminate",
--                MENUABORT = "Abort",
--                MENUJOIN = "Join Task",
--                MENUTASKINFO = Task Info",
--                MENUTASKNO = "TaskNo",
--                MENUNOTASKS = "Currently no tasks available.",
--                TASKCANCELLED = "Task #%03d %s is cancelled!",
--                TASKCANCELLEDTTS = "%s, task %03d %s is cancelled!",
--                TASKSUCCESS = "Task #%03d %s completed successfully!",
--                TASKSUCCESSTTS = "%s, task %03d %s completed successfully!",
--                TASKFAILED = "Task #%03d %s was a failure!",
--                TASKFAILEDTTS = "%s, task %03d %s was a failure!",
--                TASKFAILEDREPLAN = "Task #%03d %s was a failure! Replanning!",
--                TASKFAILEDREPLANTTS = "%s, task %03d %s was a failure! Replanning!",
--                TASKADDED = "%s has a new task %s available!",
--                PILOTS = "\nPilot(s): ",
--                PILOTSTTS = ". Pilot(s): ",
--                YES = "Yes",
--                NO = "No",
--                NONE = "None",
--                POINTEROVERTARGET = "%s, %s, pointer in reach for task %03d, lasing!",
--                POINTERTARGETREPORT = "\nPointer in reach: %s\nLasing: %s",
--                RECCETARGETREPORT = "\nRecce %s in reach: %s\nLasing: %s",
--                POINTERTARGETLASINGTTS = ". Pointer in reach and lasing.",
--                TARGET = "Target",
--                FLASHON = "%s - Flashing directions is now ON!",
--                FLASHOFF = "%s - Flashing directions is now OFF!",
--                FLASHMENU = "Flash Directions Switch",
--                BRIEFING = "Briefing",
--                TARGETLOCATION ="Target location",
--                COORDINATE = "Coordinate",
--                INFANTRY = "Infantry",
--                TECHNICAL = "Technical",
--                ARTILLERY = "Artillery",
--                TANKS = "Tanks",
--                AIRDEFENSE = "Airdefense",
--                SAM = "SAM",
--                GROUP = "Group",
--                ELEVATION = "\nTarget Elevation: %s %s",
--                METER = "meter",
--                FEET = "feet",
--              },
-- 
-- e.g.
-- 
--            taskmanager.Messages = {
--              FR = {
--                TASKABORT = "Tâche abandonnée!",
--                NOACTIVETASK = "Aucune tâche active!",
--                FREQUENCIES = "fréquences ",
--                FREQUENCY = "fréquence %.3f",
--                BROADCAST = "%s, %s, passer au %s pour l'attribution des tâches!",
--                ...
--                TASKADDED = "%s a créé une nouvelle tâche %s",
--                PILOTS = "\nPilote(s): ",
--                PILOTSTTS = ". Pilote(s): ",
--              },
--  
-- and then `taskmanager:SetLocale("fr")` **after** @{#PLAYERTASKCONTROLLER.New}() in your script.
-- 
-- If you just want to replace a **single text block** in the table, you can do this like so:
-- 
--            mycontroller.Messages.EN.NOACTIVETASK = "Choose a task first!"   
--            mycontroller.Messages.FR.YES = "Oui" 
-- 
-- ## 7 Events
--
--  The class comes with a number of FSM-based events that missions designers can use to shape their mission.
--  These are:
--  
-- ### 7.1 TaskAdded. 
--      
-- The event is triggered when a new task is added to the controller. Use @{#PLAYERTASKCONTROLLER.OnAfterTaskAdded}() to link into this event:
--      
--              function taskmanager:OnAfterTaskAdded(From, Event, To, Task)
--                ... your code here ...
--              end
-- 
-- ### 7.2 TaskDone. 
--      
-- The event is triggered when a task has ended. Use @{#PLAYERTASKCONTROLLER.OnAfterTaskDone}() to link into this event:
--      
--              function taskmanager:OnAfterTaskDone(From, Event, To, Task)
--                ... your code here ...
--              end
--          
-- ### 7.3 TaskCancelled. 
--      
-- The event is triggered when a task was cancelled manually. Use @{#PLAYERTASKCONTROLLER.OnAfterTaskCancelled}()` to link into this event:
--      
--              function taskmanager:OnAfterTaskCancelled(From, Event, To, Task)
--                ... your code here ...
--              end
--          
-- ### 7.4 TaskSuccess. 
--      
-- The event is triggered when a task completed successfully. Use @{#PLAYERTASKCONTROLLER.OnAfterTaskSuccess}() to link into this event:
--      
--              function taskmanager:OnAfterTaskSuccess(From, Event, To, Task)
--                ... your code here ...
--              end
--          
-- ### 7.5 TaskFailed. 
--      
-- The event is triggered when a task failed, no repeats. Use @{#PLAYERTASKCONTROLLER.OnAfterTaskFailed}() to link into this event:
--      
--              function taskmanager:OnAfterTaskFailed(From, Event, To, Task)
--                ... your code here ...
--              end
--          
-- ### 7.6 TaskRepeatOnFailed. 
--      
-- The event is triggered when a task failed and is re-planned for execution. Use @{#PLAYERTASKCONTROLLER.OnAfterRepeatOnFailed}() to link into this event:
--      
--              function taskmanager:OnAfterRepeatOnFailed(From, Event, To, Task)
--                ... your code here ...
--              end
-- 
-- ## 8 Using F10 map markers to create new targets
-- 
-- You can use F10 map markers to create new target points for player tasks.  
-- Enable this option with e.g., setting the tag to be used to "TARGET":
-- 
--            taskmanager:EnableMarkerOps("TARGET")
--            
-- Set a marker on the map and add the following text to create targets from it: "TARGET". This is effectively the same as adding a COORDINATE object as target.
-- The marker can be deleted any time.
--         
-- ## 9 Discussion
--
-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-playertask channel.  
-- 
--                          
-- @field #PLAYERTASKCONTROLLER
PLAYERTASKCONTROLLER = {
  ClassName          = "PLAYERTASKCONTROLLER",
  verbose            =   false,
  lid                =   nil,
  TargetQueue        =   nil,
  ClientSet          =   nil,
  UseGroupNames      =   true,
  PlayerMenu         =   {},
  usecluster         =   false,
  MenuName           =   nil,
  ClusterRadius      =   0.5,
  NoScreenOutput     =   false,
  TargetRadius       =   500,
  UseWhiteList       =   false,
  WhiteList          =   {},
  gettext            =   nil,
  locale             =   "en",
  precisionbombing   =   false,
  taskinfomenu       =   false,
  activehasinfomenu  =   false,
  MarkerReadOnly     =   false,
  customcallsigns    =   {},
  ShortCallsign      =   true,
  Keepnumber         =   false,
  CallsignTranslations = nil,
  PlayerFlashMenu    =   {},
  PlayerJoinMenu     =   {},
  PlayerInfoMenu     =   {},
  PlayerMenuTag      =   {},
  noflaresmokemenu   =   false,
  illumenu           =   false,
  TransmitOnlyWithPlayers = true,
  buddylasing        = false,
  PlayerRecce        = nil,
  Coalition          = nil,
  MenuParent         = nil,
  ShowMagnetic       = true,
  InfoHasLLDDM       = false,
  InfoHasCoordinate  = false,
  UseTypeNames       = false,
  Scoring            = nil,
  MenuNoTask         = nil,
  InformationMenu    = false,
  TaskInfoDuration   = 30,
  }

---
-- @type Type
-- @field #string A2A Air-to-Air Controller
-- @field #string A2G Air-to-Ground Controller
-- @field #string A2S Air-to-Ship Controller
-- @field #string A2GS Air-to-Ground-and-Ship Controller
PLAYERTASKCONTROLLER.Type = {
  A2A = "Air-To-Air",
  A2G = "Air-To-Ground",
  A2S = "Air-To-Sea",
  A2GS = "Air-To-Ground-Sea",
}

--- Define new AUFTRAG Types
AUFTRAG.Type.PRECISIONBOMBING = "Precision Bombing"
AUFTRAG.Type.CTLD = "Combat Transport"
AUFTRAG.Type.CSAR = "Combat Rescue"
AUFTRAG.Type.CONQUER = "Conquer"
---
-- @type Scores
PLAYERTASKCONTROLLER.Scores = {
  [AUFTRAG.Type.PRECISIONBOMBING] = 100,
  [AUFTRAG.Type.CTLD] = 100,
  [AUFTRAG.Type.CSAR] = 100,
  [AUFTRAG.Type.INTERCEPT] = 100,
  [AUFTRAG.Type.ANTISHIP] = 100,
  [AUFTRAG.Type.CAS] = 100,
  [AUFTRAG.Type.BAI] = 100,
  [AUFTRAG.Type.SEAD] = 100,
  [AUFTRAG.Type.BOMBING] = 100,
  [AUFTRAG.Type.BOMBRUNWAY] = 100,
  [AUFTRAG.Type.CONQUER] = 100,
  [AUFTRAG.Type.RECON] = 100,
  [AUFTRAG.Type.ESCORT] = 100,
  [AUFTRAG.Type.CAP] = 100,
  [AUFTRAG.Type.CAPTUREZONE] = 100,
}
 
--- 
-- @type SeadAttributes
-- @field #number SAM GROUP.Attribute.GROUND_SAM 
-- @field #number AAA GROUP.Attribute.GROUND_AAA
-- @field #number EWR GROUP.Attribute.GROUND_EWR 
PLAYERTASKCONTROLLER.SeadAttributes = {
  SAM = GROUP.Attribute.GROUND_SAM,
  AAA = GROUP.Attribute.GROUND_AAA,
  EWR = GROUP.Attribute.GROUND_EWR,
}
 
---
-- @field Messages 
PLAYERTASKCONTROLLER.Messages = {
  EN = {
    TASKABORT = "Task aborted!",
    NOACTIVETASK = "No active task!",
    FREQUENCIES = "frequencies ",
    FREQUENCY = "frequency %.3f",
    BROADCAST = "%s, %s, switch to %s for task assignment!",
    CASTTS = "close air support",
    SEADTTS = "suppress air defense",
    BOMBTTS = "bombing",
    PRECBOMBTTS = "precision bombing",
    BAITTS = "battle field air interdiction",
    ANTISHIPTTS = "anti-ship",
    INTERCEPTTS = "intercept",
    BOMBRUNWAYTTS = "bomb runway",
    HAVEACTIVETASK = "You already have one active task! Complete it first!",
    PILOTJOINEDTASK = "%s, %s. You have been assigned %s task %03d",
    TASKNAME = "%s Task ID %03d",
    TASKNAMETTS = "%s Task ID %03d",
    THREATHIGH = "high",
    THREATMEDIUM = "medium",
    THREATLOW = "low",
    THREATTEXT = "%s\nThreat: %s\nTargets left: %d\nCoord: %s",
    ELEVATION = "\nTarget Elevation: %s %s",
    METER = "meter",
    FEET = "feet",
    THREATTEXTTTS = "%s, %s. Target information for %s. Threat level %s. Targets left %d. Target location %s.",
    MARKTASK = "%s, %s, copy, task %03d location marked on map!",
    SMOKETASK = "%s, %s, copy, task %03d location smoked!",
    FLARETASK = "%s, %s, copy, task %03d location illuminated!",
    ABORTTASK = "All stations, %s, %s has aborted %s task %03d!",
    UNKNOWN = "Unknown",
    MENUTASKING = " Tasking ",
    MENUACTIVE = "Active Task",
    MENUINFO = "Info",
    MENUMARK = "Mark on map",
    MENUSMOKE = "Smoke",
    MENUFLARE = "Flare",
    MENUILLU = "Illuminate",
    MENUABORT = "Abort",
    MENUJOIN = "Join Task",
    MENUTASKINFO = "Task Info",
    MENUTASKNO = "TaskNo",
    MENUNOTASKS = "Currently no tasks available.",
    TASKCANCELLED = "Task #%03d %s is cancelled!",
    TASKCANCELLEDTTS = "%s, task %03d %s is cancelled!",
    TASKSUCCESS = "Task #%03d %s completed successfully!",
    TASKSUCCESSTTS = "%s, task %03d %s completed successfully!",
    TASKFAILED = "Task #%03d %s was a failure!",
    TASKFAILEDTTS = "%s, task %03d %s was a failure!",
    TASKFAILEDREPLAN = "Task #%03d %s available for reassignment!",
    TASKFAILEDREPLANTTS = "%s, task %03d %s vailable for reassignment!",
    TASKADDED = "%s has a new %s task available!",
    PILOTS = "\nPilot(s): ",
    PILOTSTTS = ". Pilot(s): ",
    YES = "Yes",
    NO = "No",
    NONE = "None",
    POINTEROVERTARGET = "%s, %s, pointer in reach for task %03d, lasing!",
    POINTERTARGETREPORT = "\nPointer in reach: %s\nLasing: %s",
    RECCETARGETREPORT = "\nRecce %s in reach: %s\nLasing: %s",
    POINTERTARGETLASINGTTS = ". Pointer in reach and lasing.",
    TARGET = "Target",
    FLASHON = "%s - Flashing directions is now ON!",
    FLASHOFF = "%s - Flashing directions is now OFF!",
    FLASHMENU = "Flash Directions Switch",
    BRIEFING = "Briefing",
    TARGETLOCATION ="Target location",
    COORDINATE = "Coordinate",
    INFANTRY = "Infantry",
    TECHNICAL = "Technical",
    ARTILLERY = "Artillery",
    TANKS = "Tanks",
    AIRDEFENSE = "Airdefense",
    SAM = "SAM",
    GROUP = "Group",
    UNARMEDSHIP = "Merchant",
    LIGHTARMEDSHIP = "Light Boat",
    CORVETTE = "Corvette",
    FRIGATE = "Frigate",
    CRUISER = "Cruiser",
    DESTROYER = "Destroyer",
    CARRIER = "Aircraft Carrier",
    RADIOS = "Radios",
  },
  DE = {
    TASKABORT = "Auftrag abgebrochen!",
    NOACTIVETASK = "Kein aktiver Auftrag!",
    FREQUENCIES = "Frequenzen ",
    FREQUENCY = "Frequenz %.3f",
    BROADCAST = "%s, %s, Radio %s für Aufgabenzuteilung!",
    CASTTS = "Nahbereichsunterstützung",
    SEADTTS = "Luftabwehr ausschalten",
    BOMBTTS = "Bombardieren",
    PRECBOMBTTS = "Präzisionsbombardieren",
    BAITTS = "Luftunterstützung",
    ANTISHIPTTS = "Anti-Schiff",
    INTERCEPTTS = "Abfangen",
    BOMBRUNWAYTTS = "Startbahn Bombardieren",
    HAVEACTIVETASK = "Du hast einen aktiven Auftrag! Beende ihn zuerst!",
    PILOTJOINEDTASK = "%s, %s hat Auftrag %s %03d angenommen",
    TASKNAME = "%s Auftrag ID %03d",
    TASKNAMETTS = "%s Auftrag ID %03d",
    THREATHIGH = "hoch",
    THREATMEDIUM = "mittel",
    THREATLOW = "niedrig",
    THREATTEXT = "%s\nGefahrstufe: %s\nZiele: %d\nKoord: %s",
    ELEVATION = "\nZiel Höhe: %s %s",
    METER = "Meter",
    FEET = "Fuss",
    THREATTEXTTTS = "%s, %s. Zielinformation zu %s. Gefahrstufe %s. Ziele %d. Zielposition %s.",
    MARKTASK = "%s, %s, verstanden, Zielposition %03d auf der Karte markiert!",
    SMOKETASK = "%s, %s, verstanden, Zielposition %03d mit Rauch markiert!",
    FLARETASK = "%s, %s, verstanden, Zielposition %03d beleuchtet!",
    ABORTTASK = "%s, an alle, %s hat Auftrag %s %03d abgebrochen!",
    UNKNOWN = "Unbekannt",
    MENUTASKING = " Aufträge ",
    MENUACTIVE = "Aktiver Auftrag",
    MENUINFO = "Information",
    MENUMARK = "Kartenmarkierung",
    MENUSMOKE = "Rauchgranate",
    MENUFLARE = "Leuchtgranate",
    MENUILLU = "Feldbeleuchtung",
    MENUABORT = "Abbrechen",
    MENUJOIN = "Auftrag annehmen",
    MENUTASKINFO = "Auftrag Briefing",
    MENUTASKNO = "AuftragsNr",
    MENUNOTASKS = "Momentan keine Aufträge verfügbar.",
    TASKCANCELLED = "Auftrag #%03d %s wurde beendet!",
    TASKCANCELLEDTTS = "%s, Auftrag %03d %s wurde beendet!",
    TASKSUCCESS = "Auftrag #%03d %s erfolgreich!",
    TASKSUCCESSTTS = "%s, Auftrag %03d %s erfolgreich!",
    TASKFAILED = "Auftrag #%03d %s gescheitert!",
    TASKFAILEDTTS = "%s, Auftrag %03d %s gescheitert!",
    TASKFAILEDREPLAN = "Auftrag #%03d %s gescheitert! Neuplanung!",
    TASKFAILEDREPLANTTS = "%s, Auftrag %03d %s gescheitert! Neuplanung!",
    TASKADDED = "%s hat einen neuen Auftrag %s erstellt!",
    PILOTS = "\nPilot(en): ",
    PILOTSTTS = ". Pilot(en): ",
    YES = "Ja",
    NO = "Nein",
    NONE = "Keine",
    POINTEROVERTARGET = "%s, %s, Marker im Zielbereich für %03d, Laser an!",
    POINTERTARGETREPORT = "\nMarker im Zielbereich: %s\nLaser an: %s",
    RECCETARGETREPORT = "\nSpäher % im Zielbereich: %s\nLasing: %s",
    POINTERTARGETLASINGTTS = ". Marker im Zielbereich, Laser is an.",
    TARGET = "Ziel",
    FLASHON = "%s - Richtungsangaben einblenden ist EIN!",
    FLASHOFF = "%s - Richtungsangaben einblenden ist AUS!",
    FLASHMENU = "Richtungsangaben Schalter",
    BRIEFING = "Briefing",
    TARGETLOCATION ="Zielposition",
    COORDINATE = "Koordinate",
    INFANTRY = "Infantrie",
    TECHNICAL = "Technische",
    ARTILLERY = "Artillerie",
    TANKS = "Panzer",
    AIRDEFENSE = "Flak",
    SAM = "Luftabwehr",
    GROUP = "Einheit",
    UNARMEDSHIP = "Handelsschiff",
    LIGHTARMEDSHIP = "Tender",
    CORVETTE = "Korvette",
    FRIGATE = "Fregatte",
    CRUISER = "Kreuzer",
    DESTROYER = "Zerstörer",
    CARRIER = "Flugzeugträger",
    RADIOS = "Frequenzen",
  },
}
  
--- PLAYERTASK class version.
-- @field #string version
PLAYERTASKCONTROLLER.version="0.1.70"

--- Create and run a new TASKCONTROLLER instance.
-- @param #PLAYERTASKCONTROLLER self
-- @param #string Name Name of this controller
-- @param #number Coalition of this controller, e.g. coalition.side.BLUE
-- @param #string Type Type of the tasks controlled, defaults to PLAYERTASKCONTROLLER.Type.A2G
-- @param #string ClientFilter (optional) Additional prefix filter for the SET_CLIENT. Can be handed as @{Core.Set#SET_CLIENT} also.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter)
  
  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #PLAYERTASKCONTROLLER
  
  self.Name = Name or "CentCom"
  self.Coalition = Coalition or coalition.side.BLUE
  self.CoalitionName = UTILS.GetCoalitionName(Coalition)
  self.Type = Type or PLAYERTASKCONTROLLER.Type.A2G
  
  self.usecluster = false
  if self.Type == PLAYERTASKCONTROLLER.Type.A2A then
    self.usecluster = true
  end
  
  self.ClusterRadius = 0.5
  self.TargetRadius = 500
  
  self.ClientFilter = ClientFilter --or ""
  
  self.TargetQueue = FIFO:New() -- Utilities.FiFo#FIFO
  self.TaskQueue = FIFO:New() -- Utilities.FiFo#FIFO
  self.TasksPerPlayer = FIFO:New() -- Utilities.FiFo#FIFO
  self.PrecisionTasks = FIFO:New() -- Utilities.FiFo#FIFO
  self.LasingDroneSet = SET_OPSGROUP:New() -- Core.Set#SET_OPSGROUP
  --self.PlayerMenu = {} -- #table
  self.FlashPlayer = {} -- #table
  self.AllowFlash = false
  self.lasttaskcount = 0
  
  self.taskinfomenu = false
  self.activehasinfomenu = false
  self.MenuName = nil
  self.menuitemlimit = 6
  self.holdmenutime = 30
  
  self.MarkerReadOnly = false
  
  self.repeatonfailed = true
  self.repeattimes = 5
  self.UseGroupNames = true
  
  self.customcallsigns = {}
  self.ShortCallsign = true
  self.Keepnumber = false 
  self.CallsignTranslations = nil
  
  self.noflaresmokemenu = false
  self.illumenu = false
  
  self.ShowMagnetic = true
  
  self.UseTypeNames = false
  
  self.InformationMenu = false
  
  self.TaskInfoDuration = 30
  
  self.IsClientSet = false
  
  if ClientFilter and type(ClientFilter) == "table" and ClientFilter.ClassName and ClientFilter.ClassName == "SET_CLIENT" then
    -- we have a predefined SET_CLIENT
    self.ClientSet = ClientFilter
    self.IsClientSet = true
  end
   
  if ClientFilter and not self.IsClientSet then
    self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterPrefixes(ClientFilter):FilterStart()
  elseif not self.IsClientSet then
    self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterStart()
  end
  
  self.ActiveClientSet = SET_CLIENT:New()
  
  self.lid=string.format("PlayerTaskController %s %s | ", self.Name, tostring(self.Type))
  
  self:_InitLocalization() 
  
  -- FSM start state is STOPPED.
  self:SetStartState("Stopped")
  
  self:AddTransition("Stopped",      "Start",                 "Running")
  self:AddTransition("*",            "Status",                "*")
  self:AddTransition("*",            "TaskAdded",             "*")
  self:AddTransition("*",            "TaskDone",              "*")
  self:AddTransition("*",            "TaskCancelled",         "*")
  self:AddTransition("*",            "TaskSuccess",           "*")
  self:AddTransition("*",            "TaskFailed",            "*")
  self:AddTransition("*",            "TaskProgress",          "*")
  self:AddTransition("*",            "TaskTargetSmoked",      "*")
  self:AddTransition("*",            "TaskTargetFlared",      "*")
  self:AddTransition("*",            "TaskTargetIlluminated", "*")
  self:AddTransition("*",            "TaskRepeatOnFailed",    "*")
  self:AddTransition("*",            "PlayerJoinedTask",      "*")
  self:AddTransition("*",            "PlayerAbortedTask",      "*")
  self:AddTransition("*",            "Stop",                  "Stopped")
  
  self:__Start(2)
  local starttime = math.random(5,10)
  self:__Status(starttime)
  
  self:I(self.lid..self.version.." Started.")
  
  return self
  
  ---
  -- Pseudo Functions
  ---
  
  --- On After "TaskAdded" event. Task has been added.
  -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskAdded
  -- @param #PLAYERTASKCONTROLLER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.PlayerTask#PLAYERTASK Task
  
   --- On After "TaskDone" event. Task is done.
  -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskDone
  -- @param #PLAYERTASKCONTROLLER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.PlayerTask#PLAYERTASK Task
   
  --- On After "TaskCancelled" event. Task has been cancelled.
  -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskCancelled
  -- @param #PLAYERTASKCONTROLLER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.PlayerTask#PLAYERTASK Task
   
  --- On After "TaskFailed" event. Task has failed.
  -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskFailed
  -- @param #PLAYERTASKCONTROLLER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.PlayerTask#PLAYERTASK Task
   
  --- On After "TaskSuccess" event. Task has been a success.
  -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskSuccess
  -- @param #PLAYERTASKCONTROLLER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.PlayerTask#PLAYERTASK Task
  
  --- On After "TaskProgress" event. Task target count has been reduced.
  -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskProgress
  -- @param #PLAYERTASKCONTROLLER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.PlayerTask#PLAYERTASK Task The current Task.
  -- @param #number TargetCount Targets left over
   
  --- On After "TaskRepeatOnFailed" event. Task has failed and will be repeated.
  -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskRepeatOnFailed
  -- @param #PLAYERTASKCONTROLLER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.PlayerTask#PLAYERTASK Task
  
  --- On After "TaskTargetSmoked" event. Task smoked.
  -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskTargetSmoked
  -- @param #PLAYERTASKCONTROLLER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.PlayerTask#PLAYERTASK Task
  
  --- On After "TaskTargetFlared" event. Task flared.
  -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskTargetFlared
  -- @param #PLAYERTASKCONTROLLER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.PlayerTask#PLAYERTASK Task
  
  --- On After "TaskTargetIlluminated" event. Task illuminated.
  -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterTaskTargetIlluminated
  -- @param #PLAYERTASKCONTROLLER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Ops.PlayerTask#PLAYERTASK Task
  
    --- On After "PlayerJoinedTask" event. Player joined a task.
  -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterPlayerJoinedTask
  -- @param #PLAYERTASKCONTROLLER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Group#GROUP Group The player group object
  -- @param Wrapper.Client#CLIENT Client The player client object
  -- @param Ops.PlayerTask#PLAYERTASK Task
  
    --- On After "PlayerAbortedTask" event. Player aborted a task.
  -- @function [parent=#PLAYERTASKCONTROLLER] OnAfterPlayerAbortedTask
  -- @param #PLAYERTASKCONTROLLER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Group#GROUP Group The player group object
  -- @param Wrapper.Client#CLIENT Client The player client object
  -- @param Ops.PlayerTask#PLAYERTASK Task
  
end

--- [User] Set or create a SCORING object for this taskcontroller
-- @param #PLAYERTASKCONTROLLER self
-- @param Functional.Scoring#SCORING Scoring (optional) the Scoring object
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:EnableScoring(Scoring)
  self.Scoring = Scoring or SCORING:New(self.Name)
  return self
end

--- [User] Remove the SCORING object from this taskcontroller
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:DisableScoring()
  self.Scoring = nil
  return self
end

--- [Internal] Init localization
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_InitLocalization()
  self:T(self.lid.."_InitLocalization")
  self.gettext = TEXTANDSOUND:New("PLAYERTASKCONTROLLER","en") -- Core.TextAndSound#TEXTANDSOUND
  self.locale = "en"
  for locale,table in pairs(self.Messages) do
    local Locale = string.lower(tostring(locale))
    self:T("**** Adding locale: "..Locale)
    for ID,Text in pairs(table) do
      self:T(string.format('Adding ID %s',tostring(ID)))
      self.gettext:AddEntry(Locale,tostring(ID),Text)
    end
  end
  return self
end

--- [User] Show target menu entries of type names for GROUND targets (off by default!), e.g. "Tank Group..."
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetEnableUseTypeNames()
  self:T(self.lid.."SetEnableUseTypeNames")
  self.UseTypeNames = true
  return self
end

--- [User] Do not show target menu entries of type names for GROUND targets
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetDisableUseTypeNames()
  self:T(self.lid.."SetDisableUseTypeNames")
  self.UseTypeNames = false
  return self
end

--- [User] Set flash directions option for player (player based info)
-- @param #PLAYERTASKCONTROLLER self
-- @param #boolean OnOff Set to `true` to switch on and `false` to switch off. Default is OFF.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetAllowFlashDirection(OnOff)
  self:T(self.lid.."SetAllowFlashDirection")
  self.AllowFlash = OnOff
  return self
end

--- [User] Set to show a menu entry to retrieve the radio frequencies used.
-- @param #PLAYERTASKCONTROLLER self
-- @param #boolean OnOff Set to `true` to switch on and `false` to switch off. Default is OFF.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetShowRadioInfoMenu(OnOff)
  self:T(self.lid.."SetAllowRadioInfoMenu")
  self.InformationMenu = OnOff
  return self
end

--- [User] Do not show menu entries to smoke or flare targets
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetDisableSmokeFlareTask()
  self:T(self.lid.."SetDisableSmokeFlareTask")
  self.noflaresmokemenu = true
  return self
end

--- [User] For SRS - Switch to only transmit if there are players on the server.
-- @param #PLAYERTASKCONTROLLER self
-- @param #boolean Switch If true, only send SRS if there are alive Players.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetTransmitOnlyWithPlayers(Switch)
  self.TransmitOnlyWithPlayers = Switch
  if self.SRSQueue then
    self.SRSQueue:SetTransmitOnlyWithPlayers(Switch)
  end
  return self
end

--- [User] Show menu entries to smoke or flare targets (on by default!)
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetEnableSmokeFlareTask()
  self:T(self.lid.."SetEnableSmokeFlareTask")
  self.noflaresmokemenu = false
  return self
end

--- [User] Show menu entries to illuminate targets. Needs smoke/flare enabled.
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetEnableIlluminateTask()
  self:T(self.lid.."SetEnableSmokeFlareTask")
  self.illumenu = true
  return self
end

--- [User] Do not show menu entries to illuminate targets.
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetDisableIlluminateTask()
  self:T(self.lid.."SetDisableIlluminateTask")
  self.illumenu = false
  return self
end

--- [User] Show info text on screen with a coordinate info in any case (OFF by default)
-- @param #PLAYERTASKCONTROLLER self
-- @param #boolean OnOff Switch on = true or off = false
-- @param #boolean LLDDM Show LLDDM = true or LLDMS = false
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetInfoShowsCoordinate(OnOff,LLDDM)
  self:T(self.lid.."SetInfoShowsCoordinate")
  self.InfoHasCoordinate = OnOff
  self.InfoHasLLDDM = LLDDM
  return self
end

--- [User] Set callsign options for TTS output. See @{Wrapper.Group#GROUP.GetCustomCallSign}() on how to set customized callsigns.
-- @param #PLAYERTASKCONTROLLER self
-- @param #boolean ShortCallsign If true, only call out the major flight number
-- @param #boolean Keepnumber If true, keep the **customized callsign** in the #GROUP name for players as-is, no amendments or numbers.
-- @param #table CallsignTranslations (optional) Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized
-- callsigns from playername or group name.
-- @param #func CallsignCustomFunc (Optional) For player names only(!). If given, this function will return the callsign. Needs to take the groupname and the playername as first two arguments.
-- @param #arg ... (Optional) Comma separated arguments to add to the custom function call after groupname and playername.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations,CallsignCustomFunc,...)
  if not ShortCallsign or ShortCallsign == false then
   self.ShortCallsign = false
  else
   self.ShortCallsign = true
  end
  self.Keepnumber = Keepnumber or false
  self.CallsignTranslations = CallsignTranslations
  self.CallsignCustomFunc = CallsignCustomFunc
  self.CallsignCustomArgs = arg or {}
  return self  
end

--- [Internal] Get text for text-to-speech.
-- Numbers are spaced out, e.g. "Heading 180" becomes "Heading 1 8 0 ".
-- @param #PLAYERTASKCONTROLLER self
-- @param #string text Original text.
-- @return #string Spoken text.
function PLAYERTASKCONTROLLER:_GetTextForSpeech(text)
 self:T(self.lid.."_GetTextForSpeech")
  -- Space out numbers.
  text=string.gsub(text,"%d","%1 ")
  -- get rid of leading or trailing spaces
  text=string.gsub(text,"^%s*","")
  text=string.gsub(text,"%s*$","")
  text=string.gsub(text,"  "," ")
  
  return text
end

--- [User] Set repetition options for tasks.
-- @param #PLAYERTASKCONTROLLER self
-- @param #boolean OnOff Set to `true` to switch on and `false` to switch off (defaults to true)
-- @param #number Repeats Number of repeats (defaults to 5)
-- @return #PLAYERTASKCONTROLLER self
-- @usage `taskmanager:SetTaskRepetition(true, 5)`
function PLAYERTASKCONTROLLER:SetTaskRepetition(OnOff, Repeats)
  self:T(self.lid.."SetTaskRepetition")
  if OnOff then
    self.repeatonfailed = true
    self.repeattimes = Repeats or 5
  else
    self.repeatonfailed = false
    self.repeattimes = Repeats or 5
  end
  return self
end

--- [User] Set how long the briefing is shown on screen.
-- @param #PLAYERTASKCONTROLLER self
-- @param #number Seconds Duration in seconds. Defaults to 30 seconds.
-- @return #PLAYERTASKCONTROLLER self 
function PLAYERTASKCONTROLLER:SetBriefingDuration(Seconds)
  self:T(self.lid.."SetBriefingDuration")
  self.TaskInfoDuration = Seconds or 30
  return self
end

--- [Internal] Send message to SET_CLIENT of players
-- @param #PLAYERTASKCONTROLLER self
-- @param #string Text the text to be send
-- @param #number Seconds (optional) Seconds to show, default 10
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_SendMessageToClients(Text,Seconds)
  self:T(self.lid.."_SendMessageToClients")
  local seconds = Seconds or 10
  self.ClientSet:ForEachClient(
    function (Client)
      if Client ~= nil and Client:IsActive() then
        local m = MESSAGE:New(Text,seconds,"Tasking"):ToClient(Client)
      end
    end
  )
  return self
end

--- [User] Allow precision laser-guided bombing on statics and "high-value" ground units (MBT etc)
-- @param #PLAYERTASKCONTROLLER self
-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FlightGroup (e.g. drone) to be used for lasing (one unit in one group only).
-- Can optionally be handed as Ops.ArmyGroup#ARMYGROUP - **Note** might not find an LOS spot or get lost on the way. Cannot island-hop.
-- @param #number LaserCode The lasercode to be used. Defaults to 1688.
-- @param Core.Point#COORDINATE HoldingPoint (Optional) Point where the drone should initially circle. If not set, defaults to BullsEye of the coalition.
-- @param #number Alt (Optional) Altitude in feet. Only applies if using a FLIGHTGROUP object! Defaults to 10000.
-- @param #number Speed (Optional) Speed in knots. Only applies if using a FLIGHTGROUP object! Defaults to 120.
-- @param #number MaxTravelDist (Optional) Max distance to travel to traget. Only applies if using a FLIGHTGROUP object! Defaults to 100 NM.
-- @return #PLAYERTASKCONTROLLER self
-- @usage
-- -- Set up precision bombing, FlightGroup as lasing unit
--        local FlightGroup = FLIGHTGROUP:New("LasingUnit")
--        FlightGroup:Activate()
--        taskmanager:EnablePrecisionBombing(FlightGroup,1688)
--
-- -- Alternatively, set up precision bombing, ArmyGroup as lasing unit
--        local ArmyGroup = ARMYGROUP:New("LasingUnit")
--        ArmyGroup:SetDefaultROE(ENUMS.ROE.WeaponHold)
--        ArmyGroup:SetDefaultInvisible(true)
--        ArmyGroup:Activate()
--        taskmanager:EnablePrecisionBombing(ArmyGroup,1688)
--
function PLAYERTASKCONTROLLER:EnablePrecisionBombing(FlightGroup,LaserCode,HoldingPoint,Alt,Speed,MaxTravelDist)
  self:T(self.lid.."EnablePrecisionBombing")
  
  if not self.LasingDroneSet then 
    self.LasingDroneSet = SET_OPSGROUP:New()
  end
  
  local LasingDrone -- Ops.FlightGroup#FLIGHTGROUP FlightGroup
  
  if FlightGroup then
    if FlightGroup.ClassName and (FlightGroup.ClassName == "FLIGHTGROUP" or FlightGroup.ClassName == "ARMYGROUP")then
      -- ok we have a FG
      LasingDrone = FlightGroup -- Ops.FlightGroup#FLIGHTGROUP FlightGroup
      
      self.precisionbombing = true

      LasingDrone.playertask = {}
      LasingDrone.playertask.id = 0
      LasingDrone.playertask.busy = false
      LasingDrone.playertask.lasercode = LaserCode or 1688     
      LasingDrone:SetLaser(LasingDrone.playertask.lasercode)
      LasingDrone.playertask.template = LasingDrone:_GetTemplate(true)
      LasingDrone.playertask.alt = Alt or 10000
      LasingDrone.playertask.speed = Speed or 120
      LasingDrone.playertask.maxtravel = UTILS.NMToMeters(MaxTravelDist or 50)
      
      -- let it orbit the BullsEye if FG
      if LasingDrone:IsFlightgroup() then
        --settings.IsFlightgroup = true
        local BullsCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( self.Coalition ))
        if HoldingPoint then BullsCoordinate = HoldingPoint end
        local Orbit = AUFTRAG:NewORBIT_CIRCLE(BullsCoordinate,Alt,Speed)
        Orbit:SetMissionAltitude(Alt)
        LasingDrone:AddMission(Orbit)
      elseif LasingDrone:IsArmygroup() then
        --settings.IsArmygroup = true
        local BullsCoordinate = COORDINATE:NewFromVec3( coalition.getMainRefPoint( self.Coalition ))
        if HoldingPoint then BullsCoordinate = HoldingPoint end
        local Orbit = AUFTRAG:NewONGUARD(BullsCoordinate)
        LasingDrone:AddMission(Orbit)
      end
      
      self.LasingDroneSet:AddObject(FlightGroup)
      
    elseif FlightGroup.ClassName and (FlightGroup.ClassName == "SET_OPSGROUP") then --SET_OPSGROUP
      FlightGroup:ForEachGroup(
        function(group)
          self:EnablePrecisionBombing(group,LaserCode,HoldingPoint,Alt,Speed,MaxTravelDist)
        end  
      )
    else
      self:E(self.lid.."No OPSGROUP/SET_OPSGROUP object passed or object is not alive!")
    end
  else
    self.autolase = nil
    self.precisionbombing = false
  end
  return self
end

--- [User] Convenience function - add done or ground allowing precision laser-guided bombing on statics and "high-value" ground units (MBT etc)
-- @param #PLAYERTASKCONTROLLER self
-- @param Ops.FlightGroup#FLIGHTGROUP FlightGroup The FlightGroup (e.g. drone) to be used for lasing (one unit in one group only).
-- Can optionally be handed as Ops.ArmyGroup#ARMYGROUP - **Note** might not find an LOS spot or get lost on the way. Cannot island-hop.
-- @param #number LaserCode The lasercode to be used. Defaults to 1688.
-- @param Core.Point#COORDINATE HoldingPoint (Optional) Point where the drone should initially circle. If not set, defaults to BullsEye of the coalition.
-- @param #number Alt (Optional) Altitude in feet. Only applies if using a FLIGHTGROUP object! Defaults to 10000.
-- @param #number Speed (Optional) Speed in knots. Only applies if using a FLIGHTGROUP object! Defaults to 120.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddPrecisionBombingOpsGroup(FlightGroup,LaserCode,HoldingPoint, Alt, Speed)
  self:EnablePrecisionBombing(FlightGroup,LaserCode,HoldingPoint,Alt,Speed)
  return self
end


--- [User] Allow precision laser-guided bombing on statics and "high-value" ground units (MBT etc) with player units lasing.
-- @param #PLAYERTASKCONTROLLER self
-- @param Ops.PlayerRecce#PLAYERRECCE Recce (Optional) The PLAYERRECCE object governing the lasing players.
-- @return #PLAYERTASKCONTROLLER self 
function PLAYERTASKCONTROLLER:EnableBuddyLasing(Recce)
  self:T(self.lid.."EnableBuddyLasing")
  self.buddylasing = true
  self.PlayerRecce = Recce
  return self
end

--- [User] Allow precision laser-guided bombing on statics and "high-value" ground units (MBT etc) with player units lasing.
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:DisableBuddyLasing()
  self:T(self.lid.."DisableBuddyLasing")
  self.buddylasing = false
  return self
end

--- [User] Allow addition of targets with user F10 map markers.
-- @param #PLAYERTASKCONTROLLER self
-- @param #string Tag (Optional) The tagname to use to identify commands, defaults to "TASK"
-- @return #PLAYERTASKCONTROLLER self
-- @usage
-- Enable the function like so:
--          mycontroller:EnableMarkerOps("TASK")
-- Then as a player in a client slot, you can add a map marker on the F10 map. Next edit the text
-- in the marker to make it identifiable, e.g
-- 
-- TASK Name=Tanks Sochi, Text=Destroy tank group located near Sochi!
-- 
-- Where **TASK** is the tag that tells the controller this mark is a target location (must).
-- **Name=** ended by a comma **,** tells the controller the supposed menu entry name (optional). No extra spaces! End with a comma!
-- **Text=** tells the controller the supposed free text task description (optional, only taken if **Name=** is present first). No extra spaces!
function PLAYERTASKCONTROLLER:EnableMarkerOps(Tag)
  self:T(self.lid.."EnableMarkerOps")
   
  local tag = Tag or "TASK"
  local MarkerOps = MARKEROPS_BASE:New(tag,{"Name","Text"},true)
  
  local function Handler(Keywords,Coord,Text)
    if self.verbose then
      local m = MESSAGE:New(string.format("Target added from marker at: %s", Coord:ToStringA2G(nil, nil, self.ShowMagnetic)),15,"INFO"):ToAll()
      local m = MESSAGE:New(string.format("Text: %s", Text),15,"INFO"):ToAll()
    end
    local menuname = string.match(Text,"Name=(.+),")
    local freetext = string.match(Text,"Text=(.+)")
    if menuname then
      Coord.menuname = menuname
      if freetext then
       Coord.freetext = freetext
      end
    end 
    self:AddTarget(Coord)
  end
  
  -- Event functions
  function MarkerOps:OnAfterMarkAdded(From,Event,To,Text,Keywords,Coord)
    Handler(Keywords,Coord,Text)
  end
  
  function MarkerOps:OnAfterMarkChanged(From,Event,To,Text,Keywords,Coord)
    Handler(Keywords,Coord,Text)
  end
  
  self.MarkerOps = MarkerOps 
   
  return self
end

--- [Internal] Get player name
-- @param #PLAYERTASKCONTROLLER self
-- @param Wrapper.Client#CLIENT Client
-- @return #string playername
-- @return #string ttsplayername
function PLAYERTASKCONTROLLER:_GetPlayerName(Client)
  self:T(self.lid.."_GetPlayerName")
  local playername = Client:GetPlayerName()
  local ttsplayername = nil
  if not self.customcallsigns[playername] then
    local playergroup = Client:GetGroup()
    if playergroup ~= nil then
      ttsplayername = playergroup:GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs)
      local newplayername = self:_GetTextForSpeech(ttsplayername)
      self.customcallsigns[playername] = newplayername
      ttsplayername = newplayername
    end
  else
    ttsplayername = self.customcallsigns[playername]
  end
  return playername, ttsplayername
end

--- [User] Disable precision laser-guided bombing on statics and "high-value" ground units (MBT etc)
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:DisablePrecisionBombing(FlightGroup,LaserCode)
  self:T(self.lid.."DisablePrecisionBombing")
  self.autolase = nil
  self.precisionbombing = false
  return self
end

--- [User] Enable extra menu to show task detail information before joining
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:EnableTaskInfoMenu()
  self:T(self.lid.."EnableTaskInfoMenu")
  self.taskinfomenu = true
  return self
end

--- [User] Disable extra menu to show task detail information before joining
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:DisableTaskInfoMenu()
  self:T(self.lid.."DisableTaskInfoMenu")
  self.taskinfomenu = false
  return self
end

--- [User] Set menu build fine-tuning options
-- @param #PLAYERTASKCONTROLLER self
-- @param #boolean InfoMenu If `true` this option will allow to show the Task Info-Menu also when a player has an active task. 
-- Since the menu isn't refreshed if a player holds an active task, the info in there might be stale.
-- @param #number ItemLimit Number of items per task type to show, default 5. 
-- @param #number HoldTime Minimum number of seconds between menu refreshes (called every 30 secs) if a player has **no active task**.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetMenuOptions(InfoMenu,ItemLimit,HoldTime)
  self:T(self.lid.."SetMenuOptions")
  self.activehasinfomenu = InfoMenu or false
  if self.activehasinfomenu then
    self:EnableTaskInfoMenu()
  end
  self.menuitemlimit = ItemLimit+1 or 6
  self.holdmenutime = HoldTime or 30
  return self
end

--- [User] Forbid F10 markers to be deleted by pilots. Note: Marker will auto-delete when the undelying task is done.
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetMarkerReadOnly()
  self:T(self.lid.."SetMarkerReadOnly")
  self.MarkerReadOnly = true
  return self
end

--- [User] Allow F10 markers to be deleted by pilots. Note: Marker will auto-delete when the undelying task is done.
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetMarkerDeleteable()
  self:T(self.lid.."SetMarkerDeleteable")
  self.MarkerReadOnly = false
  return self
end

--- [Internal] Event handling
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Event#EVENTDATA EventData
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_EventHandler(EventData)
  self:T(self.lid.."_EventHandler: "..EventData.id)
  --self:T(self.lid.."_EventHandler: "..EventData.IniPlayerName)
  if EventData.id == EVENTS.UnitLost or EventData.id == EVENTS.PlayerLeaveUnit or EventData.id == EVENTS.Ejection or EventData.id == EVENTS.Crash or EventData.id == EVENTS.PilotDead then
    if EventData.IniPlayerName then
      self:T(self.lid.."Event for player: "..EventData.IniPlayerName)
      --if self.PlayerMenu[EventData.IniPlayerName] then
        --self.PlayerMenu[EventData.IniPlayerName]:Remove()
        --self.PlayerMenu[EventData.IniPlayerName] = nil
      --end
        local text = ""
        if self.TasksPerPlayer:HasUniqueID(EventData.IniPlayerName) then
          local task = self.TasksPerPlayer:PullByID(EventData.IniPlayerName) -- Ops.PlayerTask#PLAYERTASK
          local Client = _DATABASE:FindClient( EventData.IniPlayerName )
          if Client then
            task:RemoveClient(Client)
            --text = "Task aborted!"
            text = self.gettext:GetEntry("TASKABORT",self.locale)
            self.ActiveTaskMenuTemplate:ResetMenu(Client)
            self.JoinTaskMenuTemplate:ResetMenu(Client)
          else
            task:RemoveClient(nil,EventData.IniPlayerName)
            --text = "Task aborted!"
            text = self.gettext:GetEntry("TASKABORT",self.locale)
          end
        else
          --text = "No active task!"
          text = self.gettext:GetEntry("NOACTIVETASK",self.locale)
        end
        self:T(self.lid..text) 
    end
  elseif EventData.id == EVENTS.PlayerEnterAircraft and EventData.IniCoalition == self.Coalition then
    if EventData.IniPlayerName and EventData.IniGroup then
      --if self.IsClientSet and self.ClientSet:IsNotInSet(CLIENT:FindByName(EventData.IniUnitName)) then
      if self.IsClientSet and (not self.ClientSet:IsIncludeObject(CLIENT:FindByName(EventData.IniUnitName))) then
        self:T(self.lid.."Client not in SET: "..EventData.IniPlayerName)
        return self
      end
      self:T(self.lid.."Event for player: "..EventData.IniPlayerName)
      
      if self.UseSRS then
        local frequency = self.Frequency
        local freqtext = ""
        if type(frequency) == "table" then
          freqtext = self.gettext:GetEntry("FREQUENCIES",self.locale)
          freqtext = freqtext..table.concat(frequency,", ")      
        else
          local freqt = self.gettext:GetEntry("FREQUENCY",self.locale)
          freqtext = string.format(freqt,frequency)
        end
        local modulation = self.Modulation
        if type(modulation) == "table" then modulation = modulation[1] end
        modulation = UTILS.GetModulationName(modulation)
        local switchtext = self.gettext:GetEntry("BROADCAST",self.locale)
        
        local playername = EventData.IniPlayerName 
        if EventData.IniGroup then
          -- personalized flight name in player naming
          if self.customcallsigns[playername] then
            self.customcallsigns[playername] = nil
          end
          playername = EventData.IniGroup:GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs)
        end
        playername = self:_GetTextForSpeech(playername)
        --local text = string.format("%s, %s, switch to %s for task assignment!",EventData.IniPlayerName,self.MenuName or self.Name,freqtext)
        local text = string.format(switchtext,playername,self.MenuName or self.Name,freqtext)
        self.SRSQueue:NewTransmission(text,nil,self.SRS,timer.getAbsTime()+60,2,{EventData.IniGroup},text,30,self.BCFrequency,self.BCModulation)
      end
      if EventData.IniPlayerName then
        --self.PlayerMenu[EventData.IniPlayerName] = nil
        local player = _DATABASE:FindClient( EventData.IniUnitName )
        self:_SwitchMenuForClient(player,"Info")
      end
    end
  end
  return self
end

--- [User] Set locale for localization. Defaults to "en"
-- @param #PLAYERTASKCONTROLLER self
-- @param #string Locale The locale to use
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetLocale(Locale)
  self:T(self.lid.."SetLocale")
  self.locale = Locale or "en"
  return self
end

--- [User] Switch screen output.
-- @param #PLAYERTASKCONTROLLER self
-- @param #boolean OnOff. Switch screen output off (true) or on (false)
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SuppressScreenOutput(OnOff)
  self:T(self.lid.."SuppressScreenOutput")
  self.NoScreenOutput = OnOff or false
  return self
end

--- [User] Set target radius. Determines the zone radius to distinguish CAS from BAI tasks and to find enemies if the TARGET object is a COORDINATE.
-- @param #PLAYERTASKCONTROLLER self
-- @param #number Radius Radius to use in meters. Defaults to 500 meters.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetTargetRadius(Radius)
  self:T(self.lid.."SetTargetRadius")
  self.TargetRadius = Radius or 500
  return self
end

--- [User] Set the cluster radius if you want to use target clusters rather than single group detection. 
-- Note that for a controller type A2A target clustering is on by default. Also remember that the diameter of the resulting zone is double the radius.
-- @param #PLAYERTASKCONTROLLER self
-- @param #number Radius Target cluster radius in kilometers. Default is 0.5km.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetClusterRadius(Radius)
  self:T(self.lid.."SetClusterRadius")
  self.ClusterRadius = Radius or 0.5
  self.usecluster = true
  return self
end

--- [User] Manually cancel a specific task
-- @param #PLAYERTASKCONTROLLER self
-- @param Ops.PlayerTask#PLAYERTASK Task The task to be cancelled
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:CancelTask(Task)
  self:T(self.lid.."CancelTask")
  Task:__Cancel(-1)
  return self
end

--- [User] Switch usage of target names for menu entries on or off
-- @param #PLAYERTASKCONTROLLER self
-- @param #boolean OnOff If true, set to on (default), if nil or false, set to off
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SwitchUseGroupNames(OnOff)
  self:T(self.lid.."SwitchUseGroupNames")
  if OnOff then
    self.UseGroupNames = true
  else
   self.UseGroupNames = false
  end
  return self
end

--- [User] Switch showing additional magnetic angles
-- @param #PLAYERTASKCONTROLLER self
-- @param #boolean OnOff If true, set to on (default), if nil or false, set to off
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SwitchMagenticAngles(OnOff)
  self:T(self.lid.."SwitchMagenticAngles")
  if OnOff then
    self.ShowMagnetic = true
  else
   self.ShowMagnetic = false
  end
  return self
end

--- [Internal] Get task types for the menu
-- @param #PLAYERTASKCONTROLLER self
-- @return #table TaskTypes
function PLAYERTASKCONTROLLER:_GetAvailableTaskTypes()
  self:T(self.lid.."_GetAvailableTaskTypes")
  local tasktypes = {}
  self.TaskQueue:ForEach(
    function (Task)
      local task = Task -- Ops.PlayerTask#PLAYERTASK
      local type = Task.Type
      tasktypes[type] = {}
    end
  )
  return tasktypes
end

--- [Internal] Get task per type for the menu
-- @param #PLAYERTASKCONTROLLER self
-- @return #table TasksPerTypes
function PLAYERTASKCONTROLLER:_GetTasksPerType()
  self:T(self.lid.."_GetTasksPerType")
  local tasktypes = self:_GetAvailableTaskTypes()
  
  --self:I({tasktypes})
  
  -- Sort tasks per threat level first
  local datatable = self.TaskQueue:GetDataTable()
  local threattable = {}
  for _,_task in pairs(datatable) do
    local task = _task -- Ops.PlayerTask#PLAYERTASK
    local threat = task.Target:GetThreatLevelMax()
    if not task:IsDone() then
      threattable[#threattable+1]={task=task,threat=threat}
    end
  end
  
  table.sort(threattable, function (k1, k2) return k1.threat > k2.threat end )
  
  for _id,_data in pairs(threattable) do
    local threat=_data.threat
    local task = _data.task -- Ops.PlayerTask#PLAYERTASK
    local type = task.Type
    local name = task.Target:GetName()
    --self:I(name)
    if not task:IsDone() then
      --self:I(name)
      table.insert(tasktypes[type],task)
    end
  end
  
  --[[
  for _type,_data in pairs(tasktypes) do
    self:I("Task Type: ".._type)
    for _id,_task in pairs(_data) do
      self:I("Task Name: ".._task.Target:GetName())
    end
  end
  --]]
  
  return tasktypes
end

--- [Internal] Check target queue
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_CheckTargetQueue()
 self:T(self.lid.."_CheckTargetQueue")
 if self.TargetQueue:Count() > 0 then
  local object = self.TargetQueue:Pull() -- Wrapper.Positionable#POSITIONABLE
  local target = TARGET:New(object)
  if object.menuname then
    target.menuname = object.menuname
    if object.freetext then
      target.freetext = object.freetext
    end
  end
  
  if object:IsInstanceOf("UNIT") or object:IsInstanceOf("GROUP") then
  
    if self.UseTypeNames and object:IsGround() then
      --   * Threat level  0: Unit is unarmed.
      --   * Threat level  1: Unit is infantry.
      --   * Threat level  2: Unit is an infantry vehicle.
      --   * Threat level  3: Unit is ground artillery.
      --   * Threat level  4: Unit is a tank.
      --   * Threat level  5: Unit is a modern tank or ifv with ATGM.
      --   * Threat level  6: Unit is a AAA.
      --   * Threat level  7: Unit is a SAM or manpad, IR guided.
      --   * Threat level  8: Unit is a Short Range SAM, radar guided.
      --   * Threat level  9: Unit is a Medium Range SAM, radar guided.
      --   * Threat level 10: Unit is a Long Range SAM, radar guided.
      local threat = object:GetThreatLevel()
      local typekey = "INFANTRY"
      if threat == 0 or threat == 2 then
        typekey = "TECHNICAL"
      elseif threat == 3 then
        typekey = "ARTILLERY" 
      elseif threat == 4 or  threat == 5 then
        typekey = "TANKS"
      elseif threat == 6 or threat == 7 then
        typekey = "AIRDEFENSE"
      elseif threat >= 8 then
        typekey = "SAM"
      end
      local typename = self.gettext:GetEntry(typekey,self.locale)
      local gname = self.gettext:GetEntry("GROUP",self.locale)
      target.TypeName = string.format("%s %s",typename,gname)
      --self:T(self.lid.."Target TypeName = "..target.TypeName)
    end
    
    if self.UseTypeNames and object:IsShip() then
      local threat = object:GetThreatLevel()
      local typekey = "UNARMEDSHIP"
      if threat == 1 then
        typekey = "LIGHTARMEDSHIP"
      elseif threat == 2 then
        typekey = "CORVETTE" 
      elseif threat == 3 or  threat == 4 then
        typekey = "FRIGATE"
      elseif threat == 5 or threat == 6 then
        typekey = "CRUISER"
      elseif threat == 7 or threat == 8 then
        typekey = "DESTROYER"
      elseif threat >= 9 then
        typekey = "CARRIER"
      end
      local typename = self.gettext:GetEntry(typekey,self.locale)
      target.TypeName = typename
      --self:T(self.lid.."Target TypeName = "..target.TypeName)
    end
  
  end
  
  self:_AddTask(target)
 end  
 return self
end

--- [Internal] Check task queue
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_CheckTaskQueue()
 self:T(self.lid.."_CheckTaskQueue")
 if self.TaskQueue:Count() > 0 then
   -- remove done tasks
   local tasks = self.TaskQueue:GetIDStack()
   for _id,_entry in pairs(tasks) do
    local data = _entry.data -- Ops.PlayerTask#PLAYERTASK
    self:T("Looking at Task: "..data.PlayerTaskNr.." Type: "..data.Type.." State: "..data:GetState())
    if data:GetState() == "Done" or data:GetState() == "Stopped" then
      local task = self.TaskQueue:ReadByID(_id) -- Ops.PlayerTask#PLAYERTASK
      -- DONE: Remove clients from the task
      local clientsattask = task.Clients:GetIDStackSorted()
      for _,_id in pairs(clientsattask) do
        self:T("*****Removing player " .. _id)
        self.TasksPerPlayer:PullByID(_id)
      end
      local clients=task:GetClientObjects()
      for _,client in pairs(clients) do
        self:_RemoveMenuEntriesForTask(task,client)
        --self:_SwitchMenuForClient(client,"Info")
      end
      for _,client in pairs(clients) do
       -- self:_RemoveMenuEntriesForTask(Task,client)
        self:_SwitchMenuForClient(client,"Info",5)
      end
      -- Follow-up tasks?
      local nexttasks = {}
      if task.FinalState == "Success" then
        nexttasks = task.NextTaskSuccess
      elseif task.FinalState == "Failed" then
       nexttasks = task.NextTaskFailure
      end
      local clientlist, count = task:GetClientObjects()
      if count > 0 then
        for _,_client in pairs(clientlist) do
          local client = _client --Wrapper.Client#CLIENT
          local group = client:GetGroup()
          for _,task in pairs(nexttasks) do  
            self:_JoinTask(task,true,group,client)
          end
        end
      end
      local TNow = timer.getAbsTime()
      if TNow - task.timestamp > 5 then
        self:_RemoveMenuEntriesForTask(task)
        local task = self.TaskQueue:PullByID(_id) -- Ops.PlayerTask#PLAYERTASK
        task = nil
      end
    end
   end
 end  
 return self
end

--- [Internal] Check precision task queue
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_CheckPrecisionTasks()
 self:T(self.lid.."_CheckPrecisionTasks")
 self:T({count=self.PrecisionTasks:Count(),enabled=self.precisionbombing})
 if self.PrecisionTasks:Count() > 0 and self.precisionbombing then
   
   -- alive checks
   self.LasingDroneSet:ForEachGroup(  
   function(LasingDrone)
     if not LasingDrone or LasingDrone:IsDead() then
      -- we need a new drone
      self:E(self.lid.."Lasing drone is dead ... creating a new one!")
      if LasingDrone then
        LasingDrone:_Respawn(1,nil,true)
      else
        --[[
        -- DONE: Handle ArmyGroup
        if LasingDrone:IsFlightgroup() then
          local FG = FLIGHTGROUP:New(LasingDroneTemplate)
          FG:Activate()
          self:EnablePrecisionBombing(FG,self.LaserCode or 1688)
        else
          local FG = ARMYGROUP:New(LasingDroneTemplate)
          FG:Activate()
          self:EnablePrecisionBombing(FG,self.LaserCode or 1688)
        end -- if LasingDroneIsFlightgroup
        --]]
      end -- if LasingDrone
     end -- if not LasingDrone
    end -- function
    )
  
  local function SelectDrone(coord)
    local selected = nil
    local mindist = math.huge
    local dist = math.huge
    self.LasingDroneSet:ForEachGroup(
      function(grp)
        if grp.playertask and (not grp.playertask.busy) then
          local gc = grp:GetCoordinate()
          if coord and gc then
            dist = coord:Get2DDistance(gc)
          end
          if dist < mindist then
            selected = grp
            mindist = dist
          end
        end
      end
    )
    return selected
  end
  
  local task = self.PrecisionTasks:Pull() -- Ops.PlayerTask#PLAYERTASK
  local taskpt = task.Target:GetCoordinate() 
    
  local SelectedDrone = SelectDrone(taskpt) -- Ops.OpsGroup#OPSGROUP
    
  -- do we have a lasing unit assignable?
  if SelectedDrone and SelectedDrone:IsAlive() then
    if SelectedDrone.playertask and (not SelectedDrone.playertask.busy) then
      -- not busy, get a task
      self:T(self.lid.."Sending lasing unit to target")
      local isassigned = self:_FindLasingDroneForTaskID(task.PlayerTaskNr)
      -- distance check
      local startpoint = SelectedDrone:GetCoordinate()
      local endpoint = task.Target:GetCoordinate()      
      local dist = math.huge
      if startpoint and endpoint then
        dist = startpoint:Get2DDistance(endpoint)
      end
      if dist <= SelectedDrone.playertask.maxtravel and (not isassigned) then
        SelectedDrone.playertask.id = task.PlayerTaskNr
        SelectedDrone.playertask.busy = true
        SelectedDrone.playertask.inreach = false
        SelectedDrone.playertask.reachmessage = false
        -- move the drone to target
        if SelectedDrone:IsFlightgroup() then
          SelectedDrone:CancelAllMissions()
          local auftrag = AUFTRAG:NewORBIT_CIRCLE(task.Target:GetCoordinate(),SelectedDrone.playertask.alt,SelectedDrone.playertask.speed)
          SelectedDrone:AddMission(auftrag)   
        elseif SelectedDrone:IsArmygroup() then
          local tgtcoord = task.Target:GetCoordinate()
          local tgtzone = ZONE_RADIUS:New("ArmyGroup-"..math.random(1,10000),tgtcoord:GetVec2(),3000)
          local finalpos=nil -- Core.Point#COORDINATE
          for i=1,50 do
            finalpos = tgtzone:GetRandomCoordinate(2500,0,{land.SurfaceType.LAND,land.SurfaceType.ROAD,land.SurfaceType.SHALLOW_WATER}) 
            if finalpos then
              if finalpos:IsLOS(tgtcoord,0) then
                break
              end
            end
          end
          if finalpos then
            SelectedDrone:CancelAllMissions()
            -- yeah we got one
            local auftrag = AUFTRAG:NewARMOREDGUARD(finalpos,"Off road")
            SelectedDrone:AddMission(auftrag)
          else
            -- could not find LOS position!
            self:E("***Could not find LOS position to post ArmyGroup for lasing!")
            SelectedDrone.playertask.id = 0
            SelectedDrone.playertask.busy = false
            SelectedDrone.playertask.inreach = false
            SelectedDrone.playertask.reachmessage = false
          end
        end
      else
        self:T(self.lid.."Lasing unit too far from target")
      end
      
    end
  end
  
  self.PrecisionTasks:Push(task,task.PlayerTaskNr)
  
  
    local function DronesWithTask(SelectedDrone)
    -- handle drones with a task
    if SelectedDrone.playertask and SelectedDrone.playertask.busy then
      -- drone is busy, set up laser when over target
      local task = self.PrecisionTasks:ReadByID(SelectedDrone.playertask.id) -- Ops.PlayerTask#PLAYERTASK
      self:T("Looking at Task: "..task.PlayerTaskNr.." Type: "..task.Type.." State: "..task:GetState())
      if (not task) or task:GetState() == "Done" or task:GetState() == "Stopped" then
        -- we're done here
        local task = self.PrecisionTasks:PullByID(SelectedDrone.playertask.id) -- Ops.PlayerTask#PLAYERTASK
        self:_CheckTaskQueue()
        task = nil
        if SelectedDrone:IsLasing() then
          SelectedDrone:__LaserOff(-1)
        end
        SelectedDrone.playertask.busy = false
        SelectedDrone.playertask.inreach = false
        SelectedDrone.playertask.id = 0
        SelectedDrone.playertask.reachmessage = false
        self:T(self.lid.."Laser Off")
      else
        -- not done yet
        self:T(self.lid.."Not done yet")
        local dcoord = SelectedDrone:GetCoordinate()
        local tcoord = task.Target:GetCoordinate()
        tcoord.y = tcoord.y + 2 
        local dist = dcoord:Get2DDistance(tcoord)
        self:T(self.lid.."Dist "..dist)
        -- close enough?
        if dist < 3000 and not SelectedDrone:IsLasing() then
          self:T(self.lid.."Laser On")
          SelectedDrone:__LaserOn(-1,tcoord)
          SelectedDrone.playertask.inreach = true
          if not SelectedDrone.playertask.reachmessage then
            --local textmark = self.gettext:GetEntry("FLARETASK",self.locale)
            SelectedDrone.playertask.reachmessage = true
            local clients = task:GetClients()
            local text = ""
            for _,playername in pairs(clients) do
              local pointertext = self.gettext:GetEntry("POINTEROVERTARGET",self.locale)
              local ttsplayername = playername
              if self.customcallsigns[playername] then
                ttsplayername = self.customcallsigns[playername]
              end --
              --text = string.format("%s, %s, pointer over target for task %03d, lasing!", playername, self.MenuName or self.Name, task.PlayerTaskNr)
              text = string.format(pointertext, ttsplayername, self.MenuName or self.Name, task.PlayerTaskNr)
              if not self.NoScreenOutput then
                local client = nil
                self.ClientSet:ForEachClient(
                  function(Client)
                    if Client:GetPlayerName() == playername then client = Client end
                  end
                ) 
                if client then
                    local m = MESSAGE:New(text,15,"Tasking"):ToClient(client)
                end --
              end --
            end --
            if self.UseSRS then
              self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2)
            end --
          end --
        end --
      end -- end else
    end -- end handle drones with a task
   end -- end function
   
   self.LasingDroneSet:ForEachGroup(DronesWithTask)
  
 end --
 return self
end

--- [Internal] Check task queue for a specific player name
-- @param #PLAYERTASKCONTROLLER self
-- @return #boolean outcome
function PLAYERTASKCONTROLLER:_CheckPlayerHasTask(PlayerName)
  self:T(self.lid.."_CheckPlayerHasTask")
  return self.TasksPerPlayer:HasUniqueID(PlayerName)
end

--- [User] Add a target object to the target queue
-- @param #PLAYERTASKCONTROLLER self
-- @param Wrapper.Positionable#POSITIONABLE Target The target GROUP, SET\_GROUP, UNIT, SET\_UNIT, STATIC, SET\_STATIC, AIRBASE, ZONE or COORDINATE.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddTarget(Target)
  self:T(self.lid.."AddTarget")
  self.TargetQueue:Push(Target)
  return self
end

--- [Internal] Check for allowed task type, if there is a (positive) whitelist
-- @param #PLAYERTASKCONTROLLER self
-- @param #string Type
-- @return #boolean Outcome
function PLAYERTASKCONTROLLER:_CheckTaskTypeAllowed(Type)
  self:T(self.lid.."_CheckTaskTypeAllowed")
  local Outcome = false
  if self.UseWhiteList then
    for _,_type in pairs(self.WhiteList) do
      if Type == _type then
        Outcome = true
        break
      end
    end
  else
    return true
  end
  return Outcome
end

--- [Internal] Check for allowed task type, if there is a (negative) blacklist
-- @param #PLAYERTASKCONTROLLER self
-- @param #string Type
-- @return #boolean Outcome
function PLAYERTASKCONTROLLER:_CheckTaskTypeDisallowed(Type)
  self:T(self.lid.."_CheckTaskTypeDisallowed")
  local Outcome = false
  if self.UseBlackList then
    for _,_type in pairs(self.BlackList) do
      if Type == _type then
        Outcome = true
        break
      end
    end
  else
    return true
  end
  return Outcome
end

--- [User] Set up a (positive) whitelist of allowed task types. Only these types will be generated.
-- @param #PLAYERTASKCONTROLLER self
-- @param #table WhiteList Table of task types that can be generated. Use to restrict available types.
-- @return #PLAYERTASKCONTROLLER self
-- @usage Currently, the following task types will be generated, if detection has been set up:
-- A2A - AUFTRAG.Type.INTERCEPT
-- A2S - AUFTRAG.Type.ANTISHIP
-- A2G - AUFTRAG.Type.CAS, AUFTRAG.Type.BAI, AUFTRAG.Type.SEAD, AUFTRAG.Type.BOMBING, AUFTRAG.Type.PRECISIONBOMBING, AUFTRAG.Type.BOMBRUNWAY
-- A2GS - A2G + A2S
-- If you don't want SEAD tasks generated, use as follows where "mycontroller" is your PLAYERTASKCONTROLLER object:
-- 
--            `mycontroller:SetTaskWhiteList({AUFTRAG.Type.CAS, AUFTRAG.Type.BAI, AUFTRAG.Type.BOMBING, AUFTRAG.Type.BOMBRUNWAY})`
--            
function PLAYERTASKCONTROLLER:SetTaskWhiteList(WhiteList)
  self:T(self.lid.."SetTaskWhiteList")
  self.WhiteList = WhiteList
  self.UseWhiteList = true
  return self
end

--- [User] Set up a (negative) blacklist of forbidden task types. These types will **not** be generated.
-- @param #PLAYERTASKCONTROLLER self
-- @param #table BlackList Table of task types that cannot be generated. Use to restrict available types.
-- @return #PLAYERTASKCONTROLLER self
-- @usage Currently, the following task types will be generated, if detection has been set up:
-- A2A - AUFTRAG.Type.INTERCEPT
-- A2S - AUFTRAG.Type.ANTISHIP
-- A2G - AUFTRAG.Type.CAS, AUFTRAG.Type.BAI, AUFTRAG.Type.SEAD, AUFTRAG.Type.BOMBING, AUFTRAG.Type.PRECISIONBOMBING, AUFTRAG.Type.BOMBRUNWAY
-- A2GS - A2G + A2S
-- If you don't want SEAD tasks generated, use as follows where "mycontroller" is your PLAYERTASKCONTROLLER object:
-- 
--            `mycontroller:SetTaskBlackList({AUFTRAG.Type.SEAD})`
--            
function PLAYERTASKCONTROLLER:SetTaskBlackList(BlackList)
  self:T(self.lid.."SetTaskBlackList")
  self.BlackList = BlackList
  self.UseBlackList = true
  return self
end

--- [User] Change the list of attributes, which are considered on GROUP or SET\_GROUP level of a target to create SEAD player tasks.
-- @param #PLAYERTASKCONTROLLER self
-- @param #table Attributes Table of attribute types considered to lead to a SEAD type player task.
-- @return #PLAYERTASKCONTROLLER self
-- @usage
-- Default attribute types are: GROUP.Attribute.GROUND_SAM, GROUP.Attribute.GROUND_AAA, and GROUP.Attribute.GROUND_EWR.
-- If you want to e.g. exclude AAA, so target groups with this attribute are assigned CAS or BAI tasks, and not SEAD, use this function as follows:
--
--            `mycontroller:SetSEADAttributes({GROUP.Attribute.GROUND_SAM, GROUP.Attribute.GROUND_EWR})`
--
function PLAYERTASKCONTROLLER:SetSEADAttributes(Attributes)
  self:T(self.lid.."SetSEADAttributes")
  if type(Attributes) ~= "table" then
    Attributes = {Attributes}
  end
  self.SeadAttributes = Attributes
  return self
end

--- [Internal] Function the check against SeadAttributes
-- @param #PLAYERTASKCONTROLLER self
-- @param #string Attribute
-- @return #boolean IsSead
function PLAYERTASKCONTROLLER:_IsAttributeSead(Attribute)
  self:T(self.lid.."_IsAttributeSead?")
  local IsSead = false
  for _,_attribute in pairs(self.SeadAttributes) do
    if Attribute == _attribute then
      IsSead = true
      break
    end
  end
  return IsSead
end

--- [Internal] Add a task to the task queue
-- @param #PLAYERTASKCONTROLLER self
-- @param Ops.Target#TARGET Target
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_AddTask(Target)
  self:T(self.lid.."_AddTask")
  local cat = Target:GetCategory()
  local threat = Target:GetThreatLevelMax()
  local type = AUFTRAG.Type.CAS
  
  --local ttstype = "close air support"
  local ttstype = self.gettext:GetEntry("CASTTS",self.locale)
  
  if cat == TARGET.Category.GROUND then
    type = AUFTRAG.Type.CAS
    -- TODO: debug BAI, CAS, SEAD
    local targetobject = Target:GetObject() -- Wrapper.Positionable#POSITIONABLE
    if targetobject:IsInstanceOf("UNIT") then
      self:T("SEAD Check UNIT")
      if targetobject:HasSEAD() then
        type = AUFTRAG.Type.SEAD
        --ttstype = "suppress air defense"
        ttstype = self.gettext:GetEntry("SEADTTS",self.locale)
      end
    elseif targetobject:IsInstanceOf("GROUP") then
      self:T("SEAD Check GROUP")
      local attribute = targetobject:GetAttribute()
       if self:_IsAttributeSead(attribute) then
        type = AUFTRAG.Type.SEAD
        --ttstype = "suppress air defense"
        ttstype = self.gettext:GetEntry("SEADTTS",self.locale)
       end
    elseif targetobject:IsInstanceOf("SET_GROUP") then
      self:T("SEAD Check SET_GROUP")
      targetobject:ForEachGroup(
        function (group)
          local attribute = group:GetAttribute()
           if self:_IsAttributeSead(attribute) then
            type = AUFTRAG.Type.SEAD
            --ttstype = "suppress air defense"
            ttstype = self.gettext:GetEntry("SEADTTS",self.locale)
           end
        end
      )     
    elseif targetobject:IsInstanceOf("SET_UNIT") then
      self:T("SEAD Check SET_UNIT")
      targetobject:ForEachUnit(
        function (unit)
          if unit:HasSEAD() then
            type = AUFTRAG.Type.SEAD
            --ttstype = "suppress air defenses"
            ttstype = self.gettext:GetEntry("SEADTTS",self.locale)
          end
        end
      )
    elseif targetobject:IsInstanceOf("SET_STATIC") or targetobject:IsInstanceOf("STATIC") then
      self:T("(PRECISION-)BOMBING SET_STATIC or STATIC")
      if self.precisionbombing then
        type = AUFTRAG.Type.PRECISIONBOMBING
        ttstype = self.gettext:GetEntry("PRECBOMBTTS",self.locale)
      else
        type = AUFTRAG.Type.BOMBING
        ttstype = self.gettext:GetEntry("BOMBTTS",self.locale)
      end

    end
    -- if there are no friendlies nearby ~0.5km and task isn't SEAD, then it's BAI
    local targetcoord = Target:GetCoordinate()
    local targetvec2 = targetcoord:GetVec2()
    local targetzone = ZONE_RADIUS:New(self.Name,targetvec2,self.TargetRadius)
    local coalition = targetobject:GetCoalitionName() or "Blue"
    coalition = string.lower(coalition)
    self:T("Target coalition is "..tostring(coalition))
    local filtercoalition = "blue"
    if coalition == "blue" then filtercoalition = "red" end
    local friendlyset = SET_GROUP:New():FilterCategoryGround():FilterCoalitions(filtercoalition):FilterZones({targetzone}):FilterOnce()
    if friendlyset:Count() == 0 and type == AUFTRAG.Type.CAS then
      type = AUFTRAG.Type.BAI
      --ttstype = "battle field air interdiction"
      ttstype = self.gettext:GetEntry("BAITTS",self.locale)
    end
    -- see if we can do precision bombing
    if (type == AUFTRAG.Type.BAI or type == AUFTRAG.Type.CAS) and (self.precisionbombing or self.buddylasing) then
      -- threatlevel between 3 and 6 means, it's artillery, tank, modern tank or AAA
      if threat > 2 and threat < 7 then
        type = AUFTRAG.Type.PRECISIONBOMBING
        ttstype = self.gettext:GetEntry("PRECBOMBTTS",self.locale)
      end
    end
  elseif cat == TARGET.Category.NAVAL then
    type = AUFTRAG.Type.ANTISHIP
    --ttstype = "anti-ship"
    ttstype = self.gettext:GetEntry("ANTISHIPTTS",self.locale)
  elseif cat == TARGET.Category.AIRCRAFT then
    type = AUFTRAG.Type.INTERCEPT
    --ttstype = "intercept"
    ttstype = self.gettext:GetEntry("INTERCEPTTS",self.locale)
  elseif cat == TARGET.Category.AIRBASE then
    --TODO: Define Success Criteria, AB hit? Runway blocked, how to determine? change of coalition? Void of enemies?
    -- Current implementation - bombing in AFB zone (EVENTS.Shot)
    type = AUFTRAG.Type.BOMBRUNWAY
    -- ttstype = "bomb runway"
    ttstype = self.gettext:GetEntry("BOMBRUNWAYTTS",self.locale)
  elseif cat == TARGET.Category.COORDINATE or cat == TARGET.Category.ZONE then
    --TODO: Define Success Criteria, void of enemies?
    -- Current implementation - find SET of enemies in ZONE or 500m radius around coordinate, and assign as targets
    local zone = Target:GetObject()
    if cat == TARGET.Category.COORDINATE then
      zone = ZONE_RADIUS:New("TargetZone-"..math.random(1,10000),Target:GetVec2(),self.TargetRadius)
    end
    -- find some enemies around there...
    local enemies = self.CoalitionName == "Blue" and "red" or "blue"
    local enemysetg = SET_GROUP:New():FilterCoalitions(enemies):FilterCategoryGround():FilterActive(true):FilterZones({zone}):FilterOnce()
    local enemysets = SET_STATIC:New():FilterCoalitions(enemies):FilterZones({zone}):FilterOnce()
    local countg = enemysetg:Count()
    local counts = enemysets:Count()
    if countg > 0 then
      -- observe Tags coming from MarkerOps
      if Target.menuname then
        enemysetg.menuname = Target.menuname
        if Target.freetext then
          enemysetg.freetext = Target.freetext
        end
      end
      self:AddTarget(enemysetg)
    end
    if counts > 0 then
      -- observe Tags coming from MarkerOps
      if Target.menuname then
        enemysets.menuname = Target.menuname
        if Target.freetext then
          enemysets.freetext = Target.freetext
        end
      end
      self:AddTarget(enemysets)
    end
    return self
  end
  
  if self.UseWhiteList then
    if not self:_CheckTaskTypeAllowed(type) then
      return self
    end
  end
  
  if self.UseBlackList then
    if self:_CheckTaskTypeDisallowed(type) then
      return self
    end
  end
  
  local task = PLAYERTASK:New(type,Target,self.repeatonfailed,self.repeattimes,ttstype)
  
  -- observe Tags coming from MarkerOps
  if Target.menuname then
    task:SetMenuName(Target.menuname)
    if Target.freetext then
      task:AddFreetext(Target.freetext)
    end
  end
  
  task.coalition = self.Coalition
  task.TypeName = Target.TypeName
  
  if type == AUFTRAG.Type.BOMBRUNWAY then
    -- task to handle event shot
    task:HandleEvent(EVENTS.Shot)
    function task:OnEventShot(EventData)
      local data = EventData -- Core.Event#EVENTDATA EventData
      local wcat = Object.getCategory(data.Weapon) -- cat 2 or 3
      local coord = data.IniUnit:GetCoordinate() or data.IniGroup:GetCoordinate()
      local vec2 = coord:GetVec2()  or {x=0, y=0}
      local coal = data.IniCoalition
      local afbzone = AIRBASE:FindByName(Target:GetName()):GetZone()
      local runways = AIRBASE:FindByName(Target:GetName()):GetRunways() or {}
      local inrunwayzone = false
      for _,_runway in pairs(runways) do
        local runway = _runway -- Wrapper.Airbase#AIRBASE.Runway
        if runway.zone:IsVec2InZone(vec2) then
          inrunwayzone = true
        end
      end
      local inzone = afbzone:IsVec2InZone(vec2)
      if coal == task.coalition and (wcat == 2 or wcat == 3) and (inrunwayzone or inzone) then
        -- bombing/rockets inside target AFB zone - well done!
        task:__Success(-20)
      end
    end
  end
  
  task:_SetController(self)
  self.TaskQueue:Push(task)
  self:__TaskAdded(10,task)
  
  return self
end

--- [User] Add a PLAYERTASK object to the list of (open) tasks
-- @param #PLAYERTASKCONTROLLER self
-- @param Ops.PlayerTask#PLAYERTASK PlayerTask
-- @param #boolean Silent If true, make no "has new task" announcement
-- @param #boolean TaskFilter If true, apply the white/black-list task filters here, also
-- @return #PLAYERTASKCONTROLLER self
-- @usage
-- Example to create a PLAYERTASK of type CTLD and give Players 10 minutes to complete:
-- 
--        local newtask = PLAYERTASK:New(AUFTRAG.Type.CTLD,ZONE:Find("Unloading"),false,0,"Combat Transport")
--        newtask.Time0 = timer.getAbsTime()    -- inject a timestamp for T0
--        newtask:AddFreetext("Transport crates to the drop zone and build a vehicle in the next 10 minutes!")
--        
--        -- add a condition for failure - fail after 10 minutes
--        newtask:AddConditionFailure(
--          function()
--            local Time = timer.getAbsTime()
--            if Time - newtask.Time0 > 600 then
--              return true
--            end 
--            return false
--          end
--          )  
--          
--        taskmanager:AddPlayerTaskToQueue(PlayerTask)     
function PLAYERTASKCONTROLLER:AddPlayerTaskToQueue(PlayerTask,Silent,TaskFilter)
  self:T(self.lid.."AddPlayerTaskToQueue")
  if PlayerTask and PlayerTask.ClassName and PlayerTask.ClassName == "PLAYERTASK" then
    if TaskFilter then  
      if self.UseWhiteList and (not self:_CheckTaskTypeAllowed(PlayerTask.Type)) then
          return self
      end      
      if self.UseBlackList and self:_CheckTaskTypeDisallowed(PlayerTask.Type) then
          return self
      end
    end
    PlayerTask:_SetController(self)
    PlayerTask:SetCoalition(self.Coalition)
    self.TaskQueue:Push(PlayerTask)
    if not Silent then
      self:__TaskAdded(10,PlayerTask)
    end
  else
    self:E(self.lid.."***** NO valid PAYERTASK object sent!")
  end
  return self
end

--- [User] Override this function in order to implement custom logic if a player can join a task or not.
-- @param #PLAYERTASKCONTROLLER self
-- @param Ops.PlayerTask#PLAYERTASK Task
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Client#CLIENT Client
-- @return #boolean Outcome True if player can join the task, false if not
function PLAYERTASKCONTROLLER:CanJoinTask(Task, Group, Client)
    return true
end

--- [Internal] Join a player to a task
-- @param #PLAYERTASKCONTROLLER self
-- @param Ops.PlayerTask#PLAYERTASK Task
-- @param #boolean Force Assign task even if client already has one
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Client#CLIENT Client
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_JoinTask(Task, Force, Group, Client)
  self:T({Force, Group, Client})
  self:T(self.lid.."_JoinTask")

  if not self:CanJoinTask(Task, Group, Client) then
    return self
  end

  if not Task:CanJoinTask(Group, Client) then
    return self
  end

  local force = false
  if type(Force) == "boolean" then
    force = Force
  end
  local playername, ttsplayername = self:_GetPlayerName(Client)
  if self.TasksPerPlayer:HasUniqueID(playername) and not force then
    -- Player already has a task
    if not self.NoScreenOutput then
      local text = self.gettext:GetEntry("HAVEACTIVETASK",self.locale)
      local m=MESSAGE:New(text,"10","Tasking"):ToClient(Client)
    end
    return self
  end
  local taskstate = Task:GetState()
  if not Task:IsDone() then
    if taskstate ~= "Executing" then
      Task:__Requested(-1)
      Task:__Executing(-2)
    end
    Task:AddClient(Client)
    local joined = self.gettext:GetEntry("PILOTJOINEDTASK",self.locale)
    -- PILOTJOINEDTASK = "%s, %s. You have been assigned %s task %03d",
    --self:I(string.format("Task %s | TaskType %s | Number %s | Type %s",self.MenuName or self.Name, Task.TTSType, tonumber(Task.PlayerTaskNr),type(Task.PlayerTaskNr)))
    local text = string.format(joined,ttsplayername, self.MenuName or self.Name, Task.TTSType, Task.PlayerTaskNr)
    self:T(self.lid..text)
    if not self.NoScreenOutput then
      self:_SendMessageToClients(text)
      --local m=MESSAGE:New(text,"10","Tasking"):ToAll()
    end
    if self.UseSRS then
      self:T(self.lid..text)
      self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2)
    end
    self.TasksPerPlayer:Push(Task,playername)
    self:__PlayerJoinedTask(1, Group, Client, Task)
    -- clear menu
    self:_SwitchMenuForClient(Client,"Active",1)
  end
  if Task.Type == AUFTRAG.Type.PRECISIONBOMBING then
    if not self.PrecisionTasks:HasUniqueID(Task.PlayerTaskNr) then
      self.PrecisionTasks:Push(Task,Task.PlayerTaskNr)
    end
  end
  return self
end

--- [Internal] Switch flashing info for a client
-- @param #PLAYERTASKCONTROLLER self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Client#CLIENT Client
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_SwitchFlashing(Group, Client)
  self:T(self.lid.."_SwitchFlashing")
  local playername, ttsplayername = self:_GetPlayerName(Client)
  if (not self.FlashPlayer[playername]) or (self.FlashPlayer[playername] == false) then
    -- Switch on
    self.FlashPlayer[playername] = Client
    local flashtext = self.gettext:GetEntry("FLASHON",self.locale)
    local text = string.format(flashtext,ttsplayername)
    local m = MESSAGE:New(text,10,"Tasking"):ToClient(Client)
  else
    -- Switch off
    self.FlashPlayer[playername] = false
    local flashtext = self.gettext:GetEntry("FLASHOFF",self.locale)
    local text = string.format(flashtext,ttsplayername)
    local m = MESSAGE:New(text,10,"Tasking"):ToClient(Client)
  end
  return self
end

function PLAYERTASKCONTROLLER:_ShowRadioInfo(Group, Client)
  self:T(self.lid.."_ShowRadioInfo")
  local playername, ttsplayername = self:_GetPlayerName(Client)
  
  if self.UseSRS then
    local frequency = self.Frequency
    local freqtext = ""
    if type(frequency) == "table" then
      freqtext = self.gettext:GetEntry("FREQUENCIES",self.locale)
      freqtext = freqtext..table.concat(frequency,", ")      
    else
      local freqt = self.gettext:GetEntry("FREQUENCY",self.locale)
      freqtext = string.format(freqt,frequency)
    end
    
    local switchtext = self.gettext:GetEntry("BROADCAST",self.locale)

    playername = ttsplayername or self:_GetTextForSpeech(playername)
    --local text = string.format("%s, %s, switch to %s for task assignment!",EventData.IniPlayerName,self.MenuName or self.Name,freqtext)
    local text = string.format(switchtext,playername,self.MenuName or self.Name,freqtext)
    self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2,{Group},text,30,self.BCFrequency,self.BCModulation)
  end
  
  return self
end

--- [Internal] Flashing directional info for a client
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_FlashInfo()
  self:T(self.lid.."_FlashInfo")
  for _playername,_client in pairs(self.FlashPlayer) do
    if _client and _client:IsAlive() then
      if self.TasksPerPlayer:HasUniqueID(_playername) then
        local task = self.TasksPerPlayer:ReadByID(_playername) -- Ops.PlayerTask#PLAYERTASK
        local Coordinate = task.Target:GetCoordinate()
        local CoordText = ""
        if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then
          CoordText = Coordinate:ToStringA2G(_client, nil, self.ShowMagnetic)
        else
          CoordText = Coordinate:ToStringA2A(_client, nil, self.ShowMagnetic)
        end
        local targettxt = self.gettext:GetEntry("TARGET",self.locale)
        local text = "Target: "..CoordText
        local m = MESSAGE:New(text,10,"Tasking"):ToClient(_client)
      end
    end
  end
  return self
end

--- [Internal] Find matching drone for precision bombing task, if any is assigned.
-- @param #PLAYERTASKCONTROLLER self
-- @param #number ID Task ID to look for
-- @return Ops.OpsGroup#OPSGROUP Drone
function PLAYERTASKCONTROLLER:_FindLasingDroneForTaskID(ID)
  local drone = nil
  self.LasingDroneSet:ForEachGroup(
    function(grp)
      if grp and grp:IsAlive() and grp.playertask and grp.playertask.id and grp.playertask.id == ID then
        drone = grp
      end
    end
  )
  return drone
end

--- [Internal] Show active task info
-- @param #PLAYERTASKCONTROLLER self
-- @param Ops.PlayerTask#PLAYERTASK Task
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Client#CLIENT Client
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Task, Group, Client)
  self:T(self.lid.."_ActiveTaskInfo")
  local playername, ttsplayername = self:_GetPlayerName(Client)
  local text = ""
  local textTTS = ""
  local task = nil
  if type(Task) ~= "string" then
    task = Task
  end
  if self.TasksPerPlayer:HasUniqueID(playername) or task then
    -- NODO: Show multiple?
    -- Details
    local task = task or self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK
    local tname = self.gettext:GetEntry("TASKNAME",self.locale)
    local ttsname = self.gettext:GetEntry("TASKNAMETTS",self.locale)
    local taskname = string.format(tname,task.Type,task.PlayerTaskNr)
    local ttstaskname = string.format(ttsname,task.TTSType,task.PlayerTaskNr)
    local Coordinate = task.Target:GetCoordinate() or COORDINATE:New(0,0,0) -- Core.Point#COORDINATE
    local Elevation = Coordinate:GetLandHeight() or 0 -- meters
    local CoordText = ""
    local CoordTextLLDM = nil
    local LasingDrone = self:_FindLasingDroneForTaskID(task.PlayerTaskNr)
    if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then
      CoordText = Coordinate:ToStringA2G(Client,nil,self.ShowMagnetic)
    else
      CoordText = Coordinate:ToStringA2A(Client,nil,self.ShowMagnetic)
    end
    --self:I("CoordText = "..CoordText)
    -- Threat Level
    local ThreatLevel = task.Target:GetThreatLevelMax()
    --local ThreatLevelText = "high"
    local ThreatLevelText = self.gettext:GetEntry("THREATHIGH",self.locale)
    if ThreatLevel > 3 and ThreatLevel < 8 then
     --ThreatLevelText = "medium"
     ThreatLevelText = self.gettext:GetEntry("THREATMEDIUM",self.locale)
    elseif  ThreatLevel <= 3 then
     --ThreatLevelText = "low"
     ThreatLevelText = self.gettext:GetEntry("THREATLOW",self.locale)
    end
    -- Targetno and Threat
    local targets = task.Target:CountTargets() or 0
    local clientlist, clientcount = task:GetClients()
    local ThreatGraph = "[" .. string.rep(  "■", ThreatLevel ) .. string.rep(  "□", 10 - ThreatLevel ) .. "]: "..ThreatLevel
    local ThreatLocaleText = self.gettext:GetEntry("THREATTEXT",self.locale)
    text = string.format(ThreatLocaleText, taskname, ThreatGraph, targets, CoordText)
    local settings = _DATABASE:GetPlayerSettings(playername) or _SETTINGS -- Core.Settings#SETTINGS
    local elevationmeasure = self.gettext:GetEntry("FEET",self.locale)
    if settings:IsMetric() then
      elevationmeasure = self.gettext:GetEntry("METER",self.locale)
      --Elevation = math.floor(UTILS.MetersToFeet(Elevation))
    else
      Elevation = math.floor(UTILS.MetersToFeet(Elevation))
    end
    -- ELEVATION = "\nTarget Elevation: %s %s",
    local elev = self.gettext:GetEntry("ELEVATION",self.locale)
    text = text .. string.format(elev,tostring(math.floor(Elevation)),elevationmeasure)
    -- Prec bombing
    if task.Type == AUFTRAG.Type.PRECISIONBOMBING and self.precisionbombing then
      if LasingDrone and LasingDrone.playertask then
        local yes = self.gettext:GetEntry("YES",self.locale)
        local no = self.gettext:GetEntry("NO",self.locale)
        local inreach = LasingDrone.playertask.inreach == true and yes or no
        local islasing = LasingDrone:IsLasing() == true and yes or no
        local prectext = self.gettext:GetEntry("POINTERTARGETREPORT",self.locale)
        prectext = string.format(prectext,inreach,islasing)
        text = text .. prectext.." ("..LasingDrone.playertask.lasercode..")"
      end
    end
    -- Buddylasing
   if task.Type == AUFTRAG.Type.PRECISIONBOMBING and self.buddylasing then
    if self.PlayerRecce then
      local yes = self.gettext:GetEntry("YES",self.locale)
      local no = self.gettext:GetEntry("NO",self.locale)
      -- TODO make dist dependent on PlayerRecce Object
      local reachdist = 8000
      local inreach = false
      -- someone close enough?
      local pset = self.PlayerRecce.PlayerSet:GetAliveSet()
      for _,_player in pairs(pset) do
        local player = _player -- Wrapper.Client#CLIENT
        local pcoord = player:GetCoordinate()
        if pcoord:Get2DDistance(Coordinate) <= reachdist then
          inreach = true
          local callsign = player:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs)
          local playername = player:GetPlayerName()
          local islasing = no
          if self.PlayerRecce.CanLase[player:GetTypeName()] and self.PlayerRecce.AutoLase[playername] then
            -- TODO - maybe compare Spot target
            islasing = yes
          end
          local inrtext = inreach == true and yes or no
          local prectext = self.gettext:GetEntry("RECCETARGETREPORT",self.locale)
          -- RECCETARGETREPORT = "\nSpäher % im Zielbereich: %s\nLasing: %s",
          prectext = string.format(prectext,callsign,inrtext,islasing)
          text = text .. prectext
        end
      end
    end
    -- Transport
   elseif task.Type == AUFTRAG.Type.CTLD or task.Type == AUFTRAG.Type.CSAR then
   --                THREATTEXT = "%s\nThreat: %s\nTargets left: %d\nCoord: %s",
   --                THREATTEXTTTS = "%s, %s. Target information for %s. Threat level %s. Targets left %d. Target location %s.",
    text = taskname
    textTTS = taskname
    local detail = task:GetFreetext()
    local detailTTS = task:GetFreetextTTS()
    local brieftxt = self.gettext:GetEntry("BRIEFING",self.locale)
    local locatxt = self.gettext:GetEntry("TARGETLOCATION",self.locale)
    text = text .. string.format("\n%s: %s\n%s %s",brieftxt,detail,locatxt,CoordText)
    --text = text .. "\nBriefing: "..detail.."\nTarget location "..CoordText
    --textTTS = textTTS .. "; Briefing: "..detailTTS.."\nTarget location "..CoordText
    textTTS = textTTS .. string.format("; %s: %s; %s %s",brieftxt,detailTTS,locatxt,CoordText)
   end
   
   -- Pilots
    local clienttxt = self.gettext:GetEntry("PILOTS",self.locale)
    if clientcount > 0 then
      for _,_name in pairs(clientlist) do
        if self.customcallsigns[_name] then
          _name = self.customcallsigns[_name]
          _name = string.gsub(_name, "(%d) ","%1") 
        end
        clienttxt = clienttxt .. _name .. ", "
      end
      clienttxt=string.gsub(clienttxt,", $",".")
    else
      local keine = self.gettext:GetEntry("NONE",self.locale)
      clienttxt = clienttxt .. keine
    end  
    text = text .. clienttxt
    textTTS = textTTS .. clienttxt
    -- Task Report
    if self.InfoHasCoordinate then
      if self.InfoHasLLDDM then
        CoordTextLLDM = Coordinate:ToStringLLDDM()
      else
        CoordTextLLDM = Coordinate:ToStringLLDMS()
      end
      -- TARGETLOCATION
      local locatxt = self.gettext:GetEntry("COORDINATE",self.locale)
      text = string.format("%s\n%s: %s",text,locatxt,CoordTextLLDM)
    end
    if task:HasFreetext() and not ( task.Type == AUFTRAG.Type.CTLD or task.Type == AUFTRAG.Type.CSAR) then
      local brieftxt = self.gettext:GetEntry("BRIEFING",self.locale)
      text = text .. string.format("\n%s: ",brieftxt)..task:GetFreetext()
    end

    if self.UseSRS then
      if string.find(CoordText," BR, ") then
        CoordText = string.gsub(CoordText," BR, "," Bee, Arr; ")
      end
      if self.ShowMagnetic then
        text=string.gsub(text,"°M|","° magnetic; ")
      end
      if string.find(CoordText,"MGRS") then
        local Text = string.gsub(CoordText,"MGRS ","")
        Text = string.gsub(Text,"%s+","")
        Text = string.gsub(Text,"([%a%d])","%1;") -- "0 5 1 "
        Text = string.gsub(Text,"0","zero")
        Text = string.gsub(Text,"9","niner")
        CoordText = "MGRS;"..Text
        if self.PathToGoogleKey then
          --CoordText = string.format("<say-as interpret-as=\'characters\'>%s</say-as>",CoordText)
          --doesn't seem to work any longer
        end
        --self:I(self.lid.." | ".. CoordText)
      end
      local ThreatLocaleTextTTS = self.gettext:GetEntry("THREATTEXTTTS",self.locale)
      local ttstext = string.format(ThreatLocaleTextTTS,ttsplayername,self.MenuName or self.Name,ttstaskname,ThreatLevelText, targets, CoordText)
      -- POINTERTARGETLASINGTTS = ". Pointer over target and lasing."
      if task.Type == AUFTRAG.Type.PRECISIONBOMBING and self.precisionbombing then
        if LasingDrone and  LasingDrone.playertask.inreach and LasingDrone:IsLasing() then
          local lasingtext = self.gettext:GetEntry("POINTERTARGETLASINGTTS",self.locale)
          ttstext = ttstext .. lasingtext
        end
      elseif task.Type == AUFTRAG.Type.CTLD or task.Type == AUFTRAG.Type.CSAR then
       ttstext = textTTS
       if string.find(ttstext," BR, ") then
        CoordText = string.gsub(ttstext," BR, "," Bee, Arr, ")
       end
      elseif task:HasFreetext() then
      
        -- add tts freetext
        local brieftxt = self.gettext:GetEntry("BRIEFING",self.locale)
        ttstext = ttstext .. string.format("; %s: ",brieftxt)..task:GetFreetextTTS()
      end
      --self:I("**** TTS Text ****\n"..ttstext.."\n*****")
      self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,2)
    end  
  else
    text = self.gettext:GetEntry("NOACTIVETASK",self.locale)
  end
  if not self.NoScreenOutput then
    local m=MESSAGE:New(text,self.TaskInfoDuration or 30,"Tasking"):ToClient(Client)
  end
  return self
end

--- [Internal] Mark task on F10 map
-- @param #PLAYERTASKCONTROLLER self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Client#CLIENT Client
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_MarkTask(Group, Client)
  self:T(self.lid.."_MarkTask")
  local playername, ttsplayername = self:_GetPlayerName(Client)
  local text = ""
  if self.TasksPerPlayer:HasUniqueID(playername) then
    local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK
    text = string.format("Task ID #%03d | Type: %s | Threat: %d",task.PlayerTaskNr,task.Type,task.Target:GetThreatLevelMax())
    task:MarkTargetOnF10Map(text,self.Coalition,self.MarkerReadOnly)
    local textmark = self.gettext:GetEntry("MARKTASK",self.locale)
    --text = string.format("%s, copy pilot %s, task %03d location marked on map!", self.MenuName or self.Name, playername, task.PlayerTaskNr)
    text = string.format(textmark, ttsplayername, self.MenuName or self.Name, task.PlayerTaskNr)
    self:T(self.lid..text)
    if self.UseSRS then
      self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2)
    end
  else
    text = self.gettext:GetEntry("NOACTIVETASK",self.locale)
  end
  if not self.NoScreenOutput then
    local m=MESSAGE:New(text,"10","Tasking"):ToClient(Client)
  end
  return self
end

--- [Internal] Smoke task location
-- @param #PLAYERTASKCONTROLLER self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Client#CLIENT Client
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_SmokeTask(Group, Client)
  self:T(self.lid.."_SmokeTask")
  local playername, ttsplayername = self:_GetPlayerName(Client) 
  local text = ""
  if self.TasksPerPlayer:HasUniqueID(playername) then
    local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK
    task:SmokeTarget()
    local textmark = self.gettext:GetEntry("SMOKETASK",self.locale)
    text = string.format(textmark, ttsplayername, self.MenuName or self.Name, task.PlayerTaskNr)
    self:T(self.lid..text)
    --local m=MESSAGE:New(text,"10","Tasking"):ToAll()
    if self.UseSRS then
      self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2)
    end
    self:__TaskTargetSmoked(5,task)
  else
    text = self.gettext:GetEntry("NOACTIVETASK",self.locale)
  end
  if not self.NoScreenOutput then
    local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client)
  end
  return self
end

--- [Internal] Flare task location
-- @param #PLAYERTASKCONTROLLER self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Client#CLIENT Client
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_FlareTask(Group, Client)
  self:T(self.lid.."_FlareTask")
  local playername, ttsplayername = self:_GetPlayerName(Client)
  local text = ""
  if self.TasksPerPlayer:HasUniqueID(playername) then
    local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK
    task:FlareTarget()
    local textmark = self.gettext:GetEntry("FLARETASK",self.locale)
    text = string.format(textmark, ttsplayername, self.MenuName or self.Name, task.PlayerTaskNr)
    self:T(self.lid..text)
    --local m=MESSAGE:New(text,"10","Tasking"):ToAll()
    if self.UseSRS then
      self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2)
    end
    self:__TaskTargetFlared(5,task)
  else
    text = self.gettext:GetEntry("NOACTIVETASK",self.locale)
  end
  if not self.NoScreenOutput then
    local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client)
  end
  return self
end

--- [Internal] Illuminate task location
-- @param #PLAYERTASKCONTROLLER self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Client#CLIENT Client
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_IlluminateTask(Group, Client)
  self:T(self.lid.."_IlluminateTask")
  local playername, ttsplayername = self:_GetPlayerName(Client)
  local text = ""
  if self.TasksPerPlayer:HasUniqueID(playername) then
    local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK
    task:FlareTarget()
    local textmark = self.gettext:GetEntry("FLARETASK",self.locale)
    text = string.format(textmark, ttsplayername, self.MenuName or self.Name, task.PlayerTaskNr)
    self:T(self.lid..text)
    --local m=MESSAGE:New(text,"10","Tasking"):ToAll()
    if self.UseSRS then
      self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2)
    end
    self:__TaskTargetIlluminated(5,task)
  else
    text = self.gettext:GetEntry("NOACTIVETASK",self.locale)
  end
  if not self.NoScreenOutput then
    local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client)
  end
  return self
end

--- [Internal] Abort Task
-- @param #PLAYERTASKCONTROLLER self
-- @param Wrapper.Group#GROUP Group
-- @param Wrapper.Client#CLIENT Client
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_AbortTask(Group, Client)
  self:T(self.lid.."_AbortTask")
  local playername, ttsplayername = self:_GetPlayerName(Client)
  local text = ""
  if self.TasksPerPlayer:HasUniqueID(playername) then
    local task = self.TasksPerPlayer:PullByID(playername) -- Ops.PlayerTask#PLAYERTASK
    task:ClientAbort(Client)
    local textmark = self.gettext:GetEntry("ABORTTASK",self.locale)
    -- ABORTTASK = "%s, to all stations, %s has aborted %s task %03d!",
    text = string.format(textmark, self.MenuName or self.Name, ttsplayername, task.TTSType, task.PlayerTaskNr)
    self:T(self.lid..text)
    --local m=MESSAGE:New(text,"10","Tasking"):ToAll()
    if self.UseSRS then
      self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,2)
    end
    self:__PlayerAbortedTask(1,Group, Client,task)
  else
    text = self.gettext:GetEntry("NOACTIVETASK",self.locale)
  end
  if not self.NoScreenOutput then
    local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client)
  end
  self:_SwitchMenuForClient(Client,"Info",1)
  return self
end


-- TODO - New Menu Manager
--- [Internal] _UpdateJoinMenuTemplate
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_UpdateJoinMenuTemplate()
  self:T("_UpdateJoinMenuTemplate")
  if self.TaskQueue:Count() > 0 then
    local taskpertype = self:_GetTasksPerType()
    local JoinMenu = self.JoinMenu -- Core.ClientMenu#CLIENTMENU
    --self:I(JoinMenu.UUID)
    local controller = self.JoinTaskMenuTemplate -- Core.ClientMenu#CLIENTMENUMANAGER
    local actcontroller = self.ActiveTaskMenuTemplate -- Core.ClientMenu#CLIENTMENUMANAGER
    local actinfomenu = self.ActiveInfoMenu
    --local entrynumbers = {}
    --local existingentries = {}
    
    if self.TaskQueue:Count() == 0 and self.MenuNoTask == nil then
      local menunotasks = self.gettext:GetEntry("MENUNOTASKS",self.locale)  
      self.MenuNoTask = controller:NewEntry(menunotasks,self.JoinMenu)
      controller:AddEntry(self.MenuNoTask)
    end
    
    if self.TaskQueue:Count() > 0 and self.MenuNoTask ~= nil then
      controller:DeleteGenericEntry(self.MenuNoTask)
      controller:DeleteF10Entry(self.MenuNoTask)
      self.MenuNoTask = nil
    end
    
    local maxn = self.menuitemlimit
    -- Generate task type menu items
    for _type,_ in pairs(taskpertype) do
      local found = controller:FindEntriesByText(_type)
      --self:I({found})
      if #found == 0 then
        local newentry = controller:NewEntry(_type,JoinMenu)
        controller:AddEntry(newentry)
        if self.JoinInfoMenu then
          local newentry = controller:NewEntry(_type,self.JoinInfoMenu)
          controller:AddEntry(newentry)
        end
        if actinfomenu then
          local newentry = actcontroller:NewEntry(_type,self.ActiveInfoMenu)
          actcontroller:AddEntry(newentry)
        end
      end
    end
    
    local typelist = self:_GetAvailableTaskTypes()
    -- Slot in Tasks
    for _tasktype,_data in pairs(typelist) do
      self:T("**** Building for TaskType: ".._tasktype)
      --local tasks = taskpertype[_tasktype] or {}
      for _,_task in pairs(taskpertype[_tasktype]) do
        _task = _task -- Ops.PlayerTask#PLAYERTASK
        self:T("**** Building for Task: ".._task.Target:GetName())
        if _task.InMenu then
          self:T("**** Task already in Menu ".._task.Target:GetName())
        else
          local menutaskno = self.gettext:GetEntry("MENUTASKNO",self.locale)
          --local text = string.format("%s %03d [%d%s",menutaskno,_task.PlayerTaskNr,pilotcount,newtext)
          local text = string.format("%s %03d",menutaskno,_task.PlayerTaskNr)
          if self.UseGroupNames then
            local name = _task.Target:GetName()
            if name ~= "Unknown" then
              --text = string.format("%s (%03d) [%d%s",name,_task.PlayerTaskNr,pilotcount,newtext)
              text = string.format("%s (%03d)",name,_task.PlayerTaskNr)
            end
          end
          local parenttable, number = controller:FindEntriesByText(_tasktype,JoinMenu)
          if number > 0 then
            local Parent = parenttable[1]
            local matches, count = controller:FindEntriesByParent(Parent)
            self:T("***** Join Menu ".._tasktype.. " # of entries: "..count)
            if count < self.menuitemlimit then
              local taskentry = controller:NewEntry(text,Parent,self._JoinTask,self,_task,"false")
              controller:AddEntry(taskentry)
              _task.InMenu = true
              if not _task.UUIDS then _task.UUIDS = {} end
              table.insert(_task.UUIDS,taskentry.UUID)
            end
          end
          if self.JoinInfoMenu then
            local parenttable, number = controller:FindEntriesByText(_tasktype,self.JoinInfoMenu)
            if number > 0 then
              local Parent = parenttable[1]
              local matches, count = controller:FindEntriesByParent(Parent)
              self:T("***** Join Info Menu ".._tasktype.. " # of entries: "..count)
              if count < self.menuitemlimit then
                local taskentry = controller:NewEntry(text,Parent,self._ActiveTaskInfo,self,_task)
                controller:AddEntry(taskentry)
                _task.InMenu = true
                if not _task.UUIDS then _task.UUIDS = {} end
                table.insert(_task.UUIDS,taskentry.UUID)
              end
            end      
          end
          if actinfomenu then
            local parenttable, number = actcontroller:FindEntriesByText(_tasktype,self.ActiveInfoMenu)
            if number > 0 then
              local Parent = parenttable[1]
              local matches, count = actcontroller:FindEntriesByParent(Parent)
              self:T("***** Active Info Menu ".._tasktype.. " # of entries: "..count)
                if count < self.menuitemlimit then
                local taskentry = actcontroller:NewEntry(text,Parent,self._ActiveTaskInfo,self,_task)
                actcontroller:AddEntry(taskentry)
                _task.InMenu = true
                if not _task.AUUIDS then _task.AUUIDS = {} end
                table.insert(_task.AUUIDS,taskentry.UUID)
              end
            end      
          end
        end
      end
    end  
  end
  return self
end

--- [Internal] _RemoveMenuEntriesForTask
-- @param #PLAYERTASKCONTROLLER self
-- @param #PLAYERTASK Task
-- @param Wrapper.Client#CLIENT Client
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_RemoveMenuEntriesForTask(Task,Client)
  self:T("_RemoveMenuEntriesForTask")
  --self:I("Task name: "..Task.Target:GetName())
  --self:I("Client: "..Client:GetPlayerName())
  if Task then
    if Task.UUIDS and self.JoinTaskMenuTemplate then
      --self:I("***** JoinTaskMenuTemplate")
      --UTILS.PrintTableToLog(Task.UUIDS)
      local controller = self.JoinTaskMenuTemplate
      for _,_uuid in pairs(Task.UUIDS) do
        local Entry = controller:FindEntryByUUID(_uuid)
        if Entry then
          controller:DeleteF10Entry(Entry,Client)
          controller:DeleteGenericEntry(Entry)
          --UTILS.PrintTableToLog(controller.menutree)
        end
      end
    end

    if Task.AUUIDS and self.ActiveTaskMenuTemplate then
      --self:I("***** ActiveTaskMenuTemplate")
      --UTILS.PrintTableToLog(Task.AUUIDS)
      for _,_uuid in pairs(Task.AUUIDS) do
        local controller = self.ActiveTaskMenuTemplate
        local Entry = controller:FindEntryByUUID(_uuid)
        if Entry then
          controller:DeleteF10Entry(Entry,Client)
          controller:DeleteGenericEntry(Entry)
          --UTILS.PrintTableToLog(controller.menutree)
        end
      end
    end
    
    Task.UUIDS = nil
    Task.AUUIDS = nil
  end
  return self
end

--- [Internal] _CreateJoinMenuTemplate
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_CreateJoinMenuTemplate()
  self:T("_CreateActiveTaskMenuTemplate")
  
  local menujoin = self.gettext:GetEntry("MENUJOIN",self.locale)
  local menunotasks = self.gettext:GetEntry("MENUNOTASKS",self.locale)  
  local flashtext = self.gettext:GetEntry("FLASHMENU",self.locale)
 
  local JoinTaskMenuTemplate = CLIENTMENUMANAGER:New(self.ClientSet,"JoinTask")
  
  if not self.JoinTopMenu then
   local taskings = self.gettext:GetEntry("MENUTASKING",self.locale)
   local longname = self.Name..taskings..self.Type
   local menuname = self.MenuName or longname
   self.JoinTopMenu = JoinTaskMenuTemplate:NewEntry(menuname,self.MenuParent)
  end
  
  if self.AllowFlash then
    JoinTaskMenuTemplate:NewEntry(flashtext,self.JoinTopMenu,self._SwitchFlashing,self)
  end
  
  self.JoinMenu = JoinTaskMenuTemplate:NewEntry(menujoin,self.JoinTopMenu)
  
  if self.taskinfomenu then
    local menutaskinfo = self.gettext:GetEntry("MENUTASKINFO",self.locale)
    self.JoinInfoMenu = JoinTaskMenuTemplate:NewEntry(menutaskinfo,self.JoinTopMenu)
  end
  
  if self.TaskQueue:Count() == 0 and self.MenuNoTask == nil then
    self.MenuNoTask = JoinTaskMenuTemplate:NewEntry(menunotasks,self.JoinMenu)
  end
  
  if self.TaskQueue:Count() > 0 and self.MenuNoTask ~= nil then
    JoinTaskMenuTemplate:DeleteGenericEntry(self.MenuNoTask)
    self.MenuNoTask = nil
  end
  
  if self.InformationMenu then
    local radioinfo = self.gettext:GetEntry("RADIOS",self.locale)
    JoinTaskMenuTemplate:NewEntry(radioinfo,self.JoinTopMenu,self._ShowRadioInfo,self)
  end
  
  self.JoinTaskMenuTemplate = JoinTaskMenuTemplate
  
  return self
end

--- [Internal] _CreateActiveTaskMenuTemplate
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_CreateActiveTaskMenuTemplate()
  self:T("_CreateActiveTaskMenuTemplate")
  
  local menuactive = self.gettext:GetEntry("MENUACTIVE",self.locale)
  local menuinfo = self.gettext:GetEntry("MENUINFO",self.locale)
  local menumark = self.gettext:GetEntry("MENUMARK",self.locale)
  local menusmoke = self.gettext:GetEntry("MENUSMOKE",self.locale)
  local menuflare = self.gettext:GetEntry("MENUFLARE",self.locale)
  local menuillu = self.gettext:GetEntry("MENUILLU",self.locale)
  local menuabort = self.gettext:GetEntry("MENUABORT",self.locale)
  
  local ActiveTaskMenuTemplate = CLIENTMENUMANAGER:New(self.ActiveClientSet,"ActiveTask")
  
  if not self.ActiveTopMenu then
   local taskings = self.gettext:GetEntry("MENUTASKING",self.locale)
   local longname = self.Name..taskings..self.Type
   local menuname = self.MenuName or longname
   self.ActiveTopMenu = ActiveTaskMenuTemplate:NewEntry(menuname,self.MenuParent)
  end
  
  if self.AllowFlash then
    local flashtext = self.gettext:GetEntry("FLASHMENU",self.locale)
    ActiveTaskMenuTemplate:NewEntry(flashtext,self.ActiveTopMenu,self._SwitchFlashing,self)
  end
  
  local active = ActiveTaskMenuTemplate:NewEntry(menuactive,self.ActiveTopMenu)
  ActiveTaskMenuTemplate:NewEntry(menuinfo,active,self._ActiveTaskInfo,self,"NONE")
  ActiveTaskMenuTemplate:NewEntry(menumark,active,self._MarkTask,self)
  
  if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A and self.noflaresmokemenu ~= true then
    ActiveTaskMenuTemplate:NewEntry(menusmoke,active,self._SmokeTask,self)
    ActiveTaskMenuTemplate:NewEntry(menuflare,active,self._FlareTask,self)
    
    if self.illumenu then
      ActiveTaskMenuTemplate:NewEntry(menuillu,active,self._IlluminateTask,self)
    end
    
  end
  
  ActiveTaskMenuTemplate:NewEntry(menuabort,active,self._AbortTask,self)
  self.ActiveTaskMenuTemplate = ActiveTaskMenuTemplate
  
  if self.taskinfomenu and self.activehasinfomenu then
    local menutaskinfo = self.gettext:GetEntry("MENUTASKINFO",self.locale)
    self.ActiveInfoMenu = ActiveTaskMenuTemplate:NewEntry(menutaskinfo,self.ActiveTopMenu)
  end
  
  return self
end

--- [Internal] _SwitchMenuForClient
-- @param #PLAYERTASKCONTROLLER self
-- @param Wrapper.Client#CLIENT Client The client
-- @param #string MenuType
-- @param #number Delay
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_SwitchMenuForClient(Client,MenuType,Delay)
  self:T(self.lid.."_SwitchMenuForClient")
  if Delay then
    self:ScheduleOnce(Delay,self._SwitchMenuForClient,self,Client,MenuType)
    return self
  end
  if MenuType == "Info" then
    self.ClientSet:AddClientsByName(Client:GetName())
    self.ActiveClientSet:Remove(Client:GetName(),true)
    self.ActiveTaskMenuTemplate:ResetMenu(Client)
    self.JoinTaskMenuTemplate:ResetMenu(Client)
    self.JoinTaskMenuTemplate:Propagate(Client)
  elseif MenuType == "Active" then
    self.ActiveClientSet:AddClientsByName(Client:GetName())
    self.ClientSet:Remove(Client:GetName(),true)
    self.ActiveTaskMenuTemplate:ResetMenu(Client)
    self.JoinTaskMenuTemplate:ResetMenu(Client)
    self.ActiveTaskMenuTemplate:Propagate(Client)
  else
    self:E(self.lid .."Unknown menu type in _SwitchMenuForClient:"..tostring(MenuType))
  end
  return self
end

--- [User] Add agent group to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Wrapper.Group#GROUP Recce Group of agents. Can also be an @{Ops.OpsGroup#OPSGROUP} object.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddAgent(Recce)
  self:T(self.lid.."AddAgent")
  if self.Intel then
    self.Intel:AddAgent(Recce)
  else
    self:E(self.lid.."*****NO detection has been set up (yet)!")
  end
  return self
end

--- [User] Add agent SET_GROUP to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Set#SET_GROUP RecceSet SET_GROUP of agents.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddAgentSet(RecceSet)
  self:T(self.lid.."AddAgentSet")
  if self.Intel then
    local Set = RecceSet:GetAliveSet()
    for _,_Recce in pairs(Set) do
      self.Intel:AddAgent(_Recce)
    end
  else
    self:E(self.lid.."*****NO detection has been set up (yet)!")
  end
  return self
end

--- [User] Set up detection of STATIC objects. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param #boolean OnOff Set to `true`for on and `false`for off.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SwitchDetectStatics(OnOff)
  self:T(self.lid.."SwitchDetectStatics")
  if self.Intel then
    self.Intel:SetDetectStatics(OnOff)
  else
    self:E(self.lid.."***** NO detection has been set up (yet)!")
  end
  return self
end

--- [User] Add an accept zone to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Zone#ZONE AcceptZone Add a zone to the accept zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddAcceptZone(AcceptZone)
  self:T(self.lid.."AddAcceptZone")
  if self.Intel then
    self.Intel:AddAcceptZone(AcceptZone)
  else
    self:E(self.lid.."*****NO detection has been set up (yet)!")
  end
  return self
end

--- [User] Add an accept SET_ZONE to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Set#SET_ZONE AcceptZoneSet Add a SET_ZONE to the accept zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddAcceptZoneSet(AcceptZoneSet)
  self:T(self.lid.."AddAcceptZoneSet")
  if self.Intel then
    self.Intel.acceptzoneset:AddSet(AcceptZoneSet)
  else
    self:E(self.lid.."*****NO detection has been set up (yet)!")
  end
  return self
end

--- [User] Add a reject zone to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Zone#ZONE RejectZone Add a zone to the reject zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddRejectZone(RejectZone)
  self:T(self.lid.."AddRejectZone")
  if self.Intel then
    self.Intel:AddRejectZone(RejectZone)
  else
    self:E(self.lid.."*****NO detection has been set up (yet)!")
  end
  return self
end

--- [User] Add a reject SET_ZONE to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Set#SET_ZONE  RejectZoneSet Add a zone to the reject zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddRejectZoneSet(RejectZoneSet)
  self:T(self.lid.."AddRejectZoneSet")
  if self.Intel then
    self.Intel.rejectzoneset:AddSet(RejectZoneSet)
  else
    self:E(self.lid.."*****NO detection has been set up (yet)!")
  end
  return self
end

--- [User] Add a conflict zone to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Zone#ZONE ConflictZone Add a zone to the conflict zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddConflictZone(ConflictZone)
  self:T(self.lid.."AddConflictZone")
  if self.Intel then
    self.Intel:AddConflictZone(ConflictZone)
  else
    self:E(self.lid.."*****NO detection has been set up (yet)!")
  end
  return self
end

--- [User] Add a conflict SET_ZONE to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Set#SET_ZONE ConflictZoneSet Add a zone to the conflict zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddConflictZoneSet(ConflictZoneSet)
  self:T(self.lid.."AddConflictZoneSet")
  if self.Intel then
    self.Intel.conflictzoneset:AddSet(ConflictZoneSet)
  else
    self:E(self.lid.."*****NO detection has been set up (yet)!")
  end
  return self
end

--- [User] Remove an accept zone from INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Zone#ZONE AcceptZone Remove this zone from the accept zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:RemoveAcceptZone(AcceptZone)
  self:T(self.lid.."RemoveAcceptZone")
  if self.Intel then
    self.Intel:RemoveAcceptZone(AcceptZone)
  else
    self:E(self.lid.."*****NO detection has been set up (yet)!")
  end
  return self
end

--- [User] Remove a reject zone from INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Zone#ZONE RejectZone Remove this zone from the reject zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:RemoveRejectZone(RejectZone)
  self:T(self.lid.."RemoveRejectZone")
  if self.Intel then
    self.Intel:RemoveRejectZone(RejectZone)
  else
    self:E(self.lid.."*****NO detection has been set up (yet)!")
  end
  return self
end

--- [User] Remove a conflict zone from INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Zone#ZONE ConflictZone Remove this zone from the conflict zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:RemoveConflictZone(ConflictZone)
  self:T(self.lid.."RemoveConflictZone")
  if self.Intel then
    self.Intel:RemoveConflictZone(ConflictZone)
  else
    self:E(self.lid.."*****NO detection has been set up (yet)!")
  end
  return self
end

--- [User] Set the top menu name to a custom string.
-- @param #PLAYERTASKCONTROLLER self
-- @param #string Name The name to use as the top menu designation.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetMenuName(Name)
 self:T(self.lid.."SetMenuName: "..Name)
 self.MenuName = Name
 return self
end

--- [User] Set the top menu to be a sub-menu of another MENU entry.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Menu#MENU_MISSION Menu
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetParentMenu(Menu)
 self:T(self.lid.."SetParentMenu")
 --self.MenuParent = Menu
 return self
end

--- [User] Set up INTEL detection
-- @param #PLAYERTASKCONTROLLER self
-- @param #string RecceName This name will be used to build a detection group set. All groups with this string somewhere in their group name will be added as Recce.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetupIntel(RecceName)
  self:T(self.lid.."SetupIntel")
  self.RecceSet = SET_GROUP:New():FilterCoalitions(self.CoalitionName):FilterPrefixes(RecceName):FilterStart()
  self.Intel = INTEL:New(self.RecceSet,self.Coalition,self.Name.."-Intel")
  self.Intel:SetClusterAnalysis(true,false,false)
  self.Intel:SetClusterRadius(self.ClusterRadius or 0.5)
  self.Intel.statusupdate = 25
  self.Intel:SetAcceptZones()
  self.Intel:SetRejectZones()
  --if self.verbose then
    --self.Intel:SetDetectionTypes(true,true,false,true,true,true)
  --end
  if self.Type == PLAYERTASKCONTROLLER.Type.A2G or self.Type == PLAYERTASKCONTROLLER.Type.A2GS then
    self.Intel:SetDetectStatics(true)
  end
  self.Intel:__Start(2)
  
  local function NewCluster(Cluster)
    if not self.usecluster then return self end
    local cluster = Cluster -- Ops.Intel#INTEL.Cluster
    local type = cluster.ctype
    self:T({type,self.Type})
    if (type == INTEL.Ctype.AIRCRAFT and self.Type == PLAYERTASKCONTROLLER.Type.A2A) or (type == INTEL.Ctype.NAVAL and (self.Type == PLAYERTASKCONTROLLER.Type.A2S or self.Type == PLAYERTASKCONTROLLER.Type.A2GS)) then
      self:T("A2A or A2S")
      local contacts = cluster.Contacts -- #table of GROUP
      local targetset = SET_GROUP:New()
      for _,_object in pairs(contacts) do
        local contact = _object -- Ops.Intel#INTEL.Contact
        self:T("Adding group: "..contact.groupname)
        targetset:AddGroup(contact.group,true)
      end
      self:AddTarget(targetset)
    elseif (type == INTEL.Ctype.GROUND or type == INTEL.Ctype.STRUCTURE) and (self.Type == PLAYERTASKCONTROLLER.Type.A2G or self.Type == PLAYERTASKCONTROLLER.Type.A2GS) then
      self:T("A2G")
      local contacts = cluster.Contacts -- #table of GROUP or STATIC
      local targetset = nil -- Core.Set#SET_BASE
      if type == INTEL.Ctype.GROUND then
        targetset = SET_GROUP:New()
        for _,_object in pairs(contacts) do
          local contact = _object -- Ops.Intel#INTEL.Contact
          self:T("Adding group: "..contact.groupname)
          targetset:AddGroup(contact.group,true)
        end
      elseif type == INTEL.Ctype.STRUCTURE then
        targetset = SET_STATIC:New()
        for _,_object in pairs(contacts) do
          local contact = _object -- Ops.Intel#INTEL.Contact
          self:T("Adding static: "..contact.groupname)
          targetset:AddStatic(contact.group)
        end
      end
      if targetset then
        self:AddTarget(targetset)
      end
    end
  end
  
  local function NewContact(Contact)
    if self.usecluster then return self end
    local contact = Contact -- Ops.Intel#INTEL.Contact
    local type = contact.ctype
    self:T({type,self.Type})
    if (type == INTEL.Ctype.AIRCRAFT and self.Type == PLAYERTASKCONTROLLER.Type.A2A) or (type == INTEL.Ctype.NAVAL and (self.Type == PLAYERTASKCONTROLLER.Type.A2S or self.Type == PLAYERTASKCONTROLLER.Type.A2GS)) then
      self:T("A2A or A2S")
      self:T("Adding group: "..contact.groupname)
      self:AddTarget(contact.group)
    elseif (type == INTEL.Ctype.GROUND or type == INTEL.Ctype.STRUCTURE) and (self.Type == PLAYERTASKCONTROLLER.Type.A2G or self.Type == PLAYERTASKCONTROLLER.Type.A2GS) then
      self:T("A2G")
      self:T("Adding group: "..contact.groupname)
      self:AddTarget(contact.group)
    end
  end
  
  function self.Intel:OnAfterNewCluster(From,Event,To,Cluster)
    NewCluster(Cluster)
  end
  
  function self.Intel:OnAfterNewContact(From,Event,To,Contact)
    NewContact(Contact)
  end
  
  return self
end

--- [User] Set SRS TTS details - see @{Sound.SRS} for details.`SetSRS()` will try to use as many attributes configured with @{Sound.SRS#MSRS.LoadConfigFile}() as possible.
-- @param #PLAYERTASKCONTROLLER self
-- @param #number Frequency Frequency to be used. Can also be given as a table of multiple frequencies, e.g. 271 or {127,251}. There needs to be exactly the same number of modulations!
-- @param #number Modulation Modulation to be used. Can also be given as a table of multiple modulations, e.g. radio.modulation.AM or {radio.modulation.FM,radio.modulation.AM}. There needs to be exactly the same number of frequencies!
-- @param #string PathToSRS Defaults to "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio"
-- @param #string Gender (Optional) Defaults to "male"
-- @param #string Culture (Optional) Defaults to "en-US"
-- @param #number Port (Optional) Defaults to 5002
-- @param #string Voice (Optional) Use a specifc voice with the @{Sound.SRS#SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`.
-- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS.
-- @param #number Volume (Optional) Volume - between 0.0 (silent) and 1.0 (loudest)
-- @param #string PathToGoogleKey (Optional) Path to your google key if you want to use google TTS; if you use a config file for MSRS, hand in nil here.
-- @param #string AccessKey (Optional) Your Google API access key. This is necessary if DCS-gRPC is used as backend; if you use a config file for MSRS, hand in nil here.
-- @param Core.Point#COORDINATE Coordinate Coordinate from which the controller radio is sending
-- @param #string Backend (Optional) MSRS Backend to be used, can be MSRS.Backend.SRSEXE or MSRS.Backend.GRPC; if you use a config file for MSRS, hand in nil here.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetSRS(Frequency,Modulation,PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey,AccessKey,Coordinate,Backend)
  self:T(self.lid.."SetSRS")
  self.PathToSRS = PathToSRS or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" --
  self.Gender = Gender or MSRS.gender or "male" --
  self.Culture = Culture or MSRS.culture or "en-US" --
  self.Port = Port or MSRS.port or 5002 --
  self.Voice = Voice or MSRS.voice
  self.PathToGoogleKey = PathToGoogleKey --
  self.AccessKey = AccessKey
  self.Volume = Volume or 1.0 --
  self.UseSRS = true
  self.Frequency = Frequency or {127,251} --
  self.BCFrequency = self.Frequency
  self.Modulation = Modulation or {radio.modulation.FM,radio.modulation.AM} --
  self.BCModulation = self.Modulation
  -- set up SRS 
  self.SRS=MSRS:New(self.PathToSRS,self.Frequency,self.Modulation,Backend)
  self.SRS:SetCoalition(self.Coalition)
  self.SRS:SetLabel(self.MenuName or self.Name)
  self.SRS:SetGender(self.Gender)
  self.SRS:SetCulture(self.Culture)
  self.SRS:SetPort(self.Port)
  self.SRS:SetVolume(self.Volume)
  if self.PathToGoogleKey then
    --self.SRS:SetGoogle(self.PathToGoogleKey)
    self.SRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.AccessKey)
    self.SRS:SetProvider(MSRS.Provider.GOOGLE)
  end
   -- Pre-configured Google?
  if (not PathToGoogleKey) and self.SRS:GetProvider() == MSRS.Provider.GOOGLE then
    self.PathToGoogleKey = MSRS.poptions.gcloud.credentials
    self.Voice = Voice or MSRS.poptions.gcloud.voice
    self.AccessKey = AccessKey or MSRS.poptions.gcloud.key
  end
  if Coordinate then
    self.SRS:SetCoordinate(Coordinate)
  end
  self.SRS:SetVoice(self.Voice)
  self.SRSQueue = MSRSQUEUE:New(self.MenuName or self.Name)
  self.SRSQueue:SetTransmitOnlyWithPlayers(self.TransmitOnlyWithPlayers)
  return self
end

--- [User] Set SRS Broadcast - for the announcement to joining players which SRS frequency, modulation to use. Use in case you want to set this differently to the standard SRS.
-- @param #PLAYERTASKCONTROLLER self
-- @param #number Frequency Frequency to be used. Can also be given as a table of multiple frequencies, e.g. 271 or {127,251}.  There needs to be exactly the same number of modulations!
-- @param #number Modulation Modulation to be used. Can also be given as a table of multiple modulations, e.g. radio.modulation.AM or {radio.modulation.FM,radio.modulation.AM}.  There needs to be exactly the same number of frequencies!
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetSRSBroadcast(Frequency,Modulation)
  self:T(self.lid.."SetSRSBroadcast")
  if self.SRS then
    self.BCFrequency = Frequency
    self.BCModulation = Modulation
  end
  return self
end

-------------------------------------------------------------------------------------------------------------------
-- FSM Functions PLAYERTASKCONTROLLER
-- TODO: FSM Functions PLAYERTASKCONTROLLER
-------------------------------------------------------------------------------------------------------------------

--- [Internal] On after start call
-- @param #PLAYERTASKCONTROLLER self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:onafterStart(From, Event, To)
  self:T({From, Event, To})
  self:T(self.lid.."onafterStart")
  self:_CreateJoinMenuTemplate()
  self:_CreateActiveTaskMenuTemplate()
  -- Player Events
  self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler)
  self:HandleEvent(EVENTS.Ejection, self._EventHandler)
  self:HandleEvent(EVENTS.Crash, self._EventHandler)
  self:HandleEvent(EVENTS.PilotDead, self._EventHandler)
  self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler)
  self:HandleEvent(EVENTS.UnitLost, self._EventHandler)
  self:SetEventPriority(5)          
  return self
end

--- [Internal] On after Status call
-- @param #PLAYERTASKCONTROLLER self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:onafterStatus(From, Event, To)
  self:T({From, Event, To})
  
  self:_CheckTargetQueue()
  self:_CheckTaskQueue()
  self:_CheckPrecisionTasks()
  if self.AllowFlash then
    self:_FlashInfo()
  end
  
  local targetcount = self.TargetQueue:Count()
  local taskcount = self.TaskQueue:Count()
  local playercount = self.ClientSet:CountAlive()
  local assignedtasks = self.TasksPerPlayer:Count()
  local enforcedmenu = false
  
  if taskcount ~= self.lasttaskcount then
    self.lasttaskcount = taskcount
    if taskcount < self.menuitemlimit then
      enforcedmenu = true
    end
  end
  
  self:_UpdateJoinMenuTemplate()
  
  if self.verbose then
    local text = string.format("%s | New Targets: %02d | Active Tasks: %02d | Active Players: %02d | Assigned Tasks: %02d",self.MenuName, targetcount,taskcount,playercount,assignedtasks)
    self:I(text)
  end
  
  if self:GetState() ~= "Stopped" then
    self:__Status(-30)
  end
  return self
end

--- [Internal] On after task done
-- @param #PLAYERTASKCONTROLLER self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Ops.PlayerTask#PLAYERTASK Task
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:onafterTaskDone(From, Event, To, Task)
  self:T({From, Event, To})
  self:T(self.lid.."TaskDone")
  return self
end

--- [Internal] On after task cancelled
-- @param #PLAYERTASKCONTROLLER self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Ops.PlayerTask#PLAYERTASK Task
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:onafterTaskCancelled(From, Event, To, Task)
  self:T({From, Event, To})
  self:T(self.lid.."TaskCancelled")
  local canceltxt = self.gettext:GetEntry("TASKCANCELLED",self.locale)
  local canceltxttts = self.gettext:GetEntry("TASKCANCELLEDTTS",self.locale)
  local taskname = string.format(canceltxt, Task.PlayerTaskNr, tostring(Task.Type))
  if not self.NoScreenOutput then
    self:_SendMessageToClients(taskname,15)
    --local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition)
  end
  if self.UseSRS then
    taskname = string.format(canceltxttts, self.MenuName or self.Name, Task.PlayerTaskNr, tostring(Task.TTSType))
    self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2)
  end
  
  local clients=Task:GetClientObjects()
  for _,client in pairs(clients) do
    self:_RemoveMenuEntriesForTask(Task,client)
    --self:_SwitchMenuForClient(client,"Info")
  end
  for _,client in pairs(clients) do
    --self:_RemoveMenuEntriesForTask(Task,client)
    self:_SwitchMenuForClient(client,"Info",5)
  end
  return self
end

--- [Internal] On after task success
-- @param #PLAYERTASKCONTROLLER self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Ops.PlayerTask#PLAYERTASK Task
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:onafterTaskSuccess(From, Event, To, Task)
  self:T({From, Event, To})
  self:T(self.lid.."TaskSuccess")
  local succtxt = self.gettext:GetEntry("TASKSUCCESS",self.locale)
  local succtxttts = self.gettext:GetEntry("TASKSUCCESSTTS",self.locale)
  local taskname = string.format(succtxt, Task.PlayerTaskNr, tostring(Task.Type))
  if not self.NoScreenOutput then
    self:_SendMessageToClients(taskname,15)
    --local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition)
  end
  if self.UseSRS then
    taskname = string.format(succtxttts, self.MenuName or self.Name, Task.PlayerTaskNr, tostring(Task.TTSType))
    self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2)
  end
  
  local clients=Task:GetClientObjects()
  for _,client in pairs(clients) do
    self:_RemoveMenuEntriesForTask(Task,client)
    --self:_SwitchMenuForClient(client,"Info")
  end
  for _,client in pairs(clients) do
   -- self:_RemoveMenuEntriesForTask(Task,client)
    self:_SwitchMenuForClient(client,"Info",5)
  end
  return self
end

--- [Internal] On after task failed
-- @param #PLAYERTASKCONTROLLER self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Ops.PlayerTask#PLAYERTASK Task
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:onafterTaskFailed(From, Event, To, Task)
  self:T({From, Event, To})
  self:T(self.lid.."TaskFailed")
  local failtxt = self.gettext:GetEntry("TASKFAILED",self.locale)
  local failtxttts = self.gettext:GetEntry("TASKFAILEDTTS",self.locale)
  local taskname = string.format(failtxt, Task.PlayerTaskNr, tostring(Task.Type))
  if not self.NoScreenOutput then
    self:_SendMessageToClients(taskname,15)
    --local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition)
  end
  if self.UseSRS then
    taskname = string.format(failtxttts, self.MenuName or self.Name, Task.PlayerTaskNr, tostring(Task.TTSType))
    self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2)
  end
  local clients=Task:GetClientObjects()
  for _,client in pairs(clients) do
    self:_RemoveMenuEntriesForTask(Task,client)
    --self:_SwitchMenuForClient(client,"Info")
  end
  for _,client in pairs(clients) do
   -- self:_RemoveMenuEntriesForTask(Task,client)
    self:_SwitchMenuForClient(client,"Info",5)
  end
  return self
end

--- [Internal] On after task failed, repeat planned
-- @param #PLAYERTASKCONTROLLER self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Ops.PlayerTask#PLAYERTASK Task
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:onafterTaskRepeatOnFailed(From, Event, To, Task)
  self:T({From, Event, To})
  self:T(self.lid.."RepeatOnFailed")
  local repfailtxt = self.gettext:GetEntry("TASKFAILEDREPLAN",self.locale)
  local repfailtxttts = self.gettext:GetEntry("TASKFAILEDREPLANTTS",self.locale)
  local taskname = string.format(repfailtxt, Task.PlayerTaskNr, tostring(Task.Type))
  if not self.NoScreenOutput then
    self:_SendMessageToClients(taskname,15)
    --local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition)
  end
  if self.UseSRS then
    taskname = string.format(repfailtxttts, self.MenuName or self.Name, Task.PlayerTaskNr, tostring(Task.TTSType))
    self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2)
  end
  return self
end

--- [Internal] On after task added
-- @param #PLAYERTASKCONTROLLER self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Ops.PlayerTask#PLAYERTASK Task
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:onafterTaskAdded(From, Event, To, Task)
  self:T({From, Event, To})
  self:T(self.lid.."TaskAdded")
  local addtxt = self.gettext:GetEntry("TASKADDED",self.locale)
  local taskname = string.format(addtxt, self.MenuName or self.Name, tostring(Task.Type))
  if not self.NoScreenOutput then
    self:_SendMessageToClients(taskname,15)
    --local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition)
  end
  if self.UseSRS then
    taskname = string.format(addtxt, self.MenuName or self.Name, tostring(Task.TTSType))
    self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2)
  end
  return self
end

--- [Internal] On after Stop call
-- @param #PLAYERTASKCONTROLLER self
-- @param #string From
-- @param #string Event 
-- @param #string To
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:onafterStop(From, Event, To)
  self:T({From, Event, To})
  self:T(self.lid.."Stopped.")
    -- Player leaves
  self:UnHandleEvent(EVENTS.PlayerLeaveUnit)
  self:UnHandleEvent(EVENTS.Ejection)
  self:UnHandleEvent(EVENTS.Crash)
  self:UnHandleEvent(EVENTS.PilotDead)
  self:UnHandleEvent(EVENTS.PlayerEnterAircraft)
  return self
end
-------
-- END PLAYERTASKCONTROLLER
----- 
end
--- **Ops** - Allow a player in a helo like the Gazelle, KA-50 to recon and lase ground targets.
--
-- ## Features:
--
--   * Allow a player in a helicopter to detect, smoke, flare, lase and report ground units to others.
--   * Implements visual detection from the helo
--   * Implements optical detection via the Gazelle Vivianne system and lasing
--   * KA-50 BlackShark basic support
--   * Everyone else gets visual detection only
--   * Upload target info to a PLAYERTASKCONTROLLER Instance
--
-- ===
--
-- # Demo Missions
--
-- ### Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/).
--
-- ===
--
--
-- ### Authors:
--
--   * Applevangelist (Design & Programming)
--   
-- ===
--
-- @module Ops.PlayerRecce
-- @image Ops_PlayerRecce.png

-------------------------------------------------------------------------------------------------------------------
-- PLAYERRECCE
-- TODO: PLAYERRECCE
-- DONE: No messages when no targets to flare or smoke
-- DONE: Smoke not all targets
-- DONE: Messages to Attack Group, use client settings
-- DONE: Lasing dist 8km
-- DONE: Reference Point RP
-- DONE: Sort for multiple targets in one direction
-- DONE: Targets with forget timeout, also report 
-------------------------------------------------------------------------------------------------------------------

--- PLAYERRECCE class.
-- @type PLAYERRECCE
-- @field #string ClassName Name of the class.
-- @field #boolean verbose Switch verbosity.
-- @field #string lid Class id string for output to DCS log file.
-- @field #string version
-- @field #table ViewZone
-- @field #table ViewZoneVisual
-- @field #table ViewZoneLaser
-- @field #table LaserFOV
-- @field #table LaserTarget
-- @field Core.Set#SET_CLIENT PlayerSet
-- @field #string Name
-- @field #number Coalition
-- @field #string CoalitionName
-- @field #boolean debug
-- @field #table LaserSpots
-- @field #table UnitLaserCodes
-- @field #table LaserCodes
-- @field #table ClientMenus
-- @field #table OnStation
-- @field #number minthreatlevel
-- @field #number lasingtime
-- @field #table AutoLase
-- @field Core.Set#SET_CLIENT AttackSet
-- @field #boolean TransmitOnlyWithPlayers
-- @field Sound.SRS#MSRS SRS
-- @field Sound.SRS#MSRSQUEUE SRSQueue
-- @field #boolean UseController
-- @field Ops.PlayerTask#PLAYERTASKCONTROLLER Controller
-- @field #boolean ShortCallsign
-- @field #boolean Keepnumber
-- @field #table CallsignTranslations
-- @field Core.Point#COORDINATE ReferencePoint
-- @field #string RPName
-- @field Wrapper.Marker#MARKER RPMarker
-- @field #number TForget
-- @field Utilities.FiFo#FIFO TargetCache
-- @field #boolean smokeownposition
-- @field #table SmokeOwn
-- @field #boolean smokeaveragetargetpos
-- @field #boolean reporttostringbullsonly
-- @extends Core.Fsm#FSM

---
--
-- *It is our attitude at the beginning of a difficult task which, more than anything else, which will affect its successful outcome.* (William James)
--
-- ===
-- 
-- # PLAYERRECCE 
-- 
--   * Allow a player in a helicopter to detect, smoke, flare, lase and report ground units to others.
--   * Implements visual detection from the helo
--   * Implements optical detection via the Gazelle Vivianne system and lasing
--   * KA-50 BlackShark basic support
--   * Everyone else gets visual detection only
--   * Upload target info to a PLAYERTASKCONTROLLER Instance
--    
-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) channel.  
-- 
--                          
-- @field #PLAYERRECCE
PLAYERRECCE = {
  ClassName          =   "PLAYERRECCE",
  verbose            =   true,
  lid                =   nil,
  version            =   "0.1.26",
  ViewZone           =   {},
  ViewZoneVisual     =   {},
  ViewZoneLaser      =   {},
  LaserFOV           =   {},
  LaserTarget        =   {},
  PlayerSet          =   nil,
  debug              =   false,
  LaserSpots         =   {},
  UnitLaserCodes     =   {},
  LaserCodes         =   {},
  ClientMenus        =   {},
  OnStation          =   {},
  minthreatlevel     =   0,
  lasingtime         =   60,
  AutoLase           =   {},
  AttackSet          =   nil,
  TransmitOnlyWithPlayers = true,
  UseController      =   false,
  Controller         =   nil,
  ShortCallsign      =   true,
  Keepnumber         =   true,
  CallsignTranslations = nil,
  ReferencePoint     =   nil,
  TForget            =   600,
  TargetCache        =   nil,
  smokeownposition   =   false,
  SmokeOwn           =   {},
  smokeaveragetargetpos = true,
  reporttostringbullsonly = true,
}

--- 
-- @type PlayerRecceDetected
-- @field #boolean detected
-- @field Wrapper.Client#CLIENT recce
-- @field #string playername
-- @field #number timestamp

---
-- @type LaserRelativePos
-- @field #string typename Unit type name
PLAYERRECCE.LaserRelativePos = {
  ["SA342M"] = { x = 1.7, y = 1.2, z = 0 },
  ["SA342Mistral"] = { x = 1.7, y = 1.2, z = 0 },
  ["SA342Minigun"] = { x = 1.7, y = 1.2, z = 0 },
  ["SA342L"] = { x = 1.7, y = 1.2, z = 0 },
  ["Ka-50"] = { x = 6.1, y = -0.85 , z = 0 },
  ["Ka-50_3"] = { x = 6.1, y = -0.85 , z = 0 },
  ["OH58D"] = {x = 0, y = 2.8, z = 0},
}

---
-- @type MaxViewDistance
-- @field #string typename Unit type name
PLAYERRECCE.MaxViewDistance = {
  ["SA342M"] = 8000,
  ["SA342Mistral"] = 8000,
  ["SA342Minigun"] = 8000,
  ["SA342L"] = 8000,
  ["Ka-50"] = 8000, 
  ["Ka-50_3"] = 8000,
  ["OH58D"] = 8000, 
}

---
-- @type Cameraheight
-- @field #string typename Unit type name
PLAYERRECCE.Cameraheight = {
  ["SA342M"] = 2.85,
  ["SA342Mistral"] = 2.85,
  ["SA342Minigun"] = 2.85,
  ["SA342L"] = 2.85,
  ["Ka-50"] = 0.5, 
  ["Ka-50_3"] = 0.5,
  ["OH58D"] = 4.25, 
}

---
-- @type CanLase
-- @field #string typename Unit type name
PLAYERRECCE.CanLase = {
  ["SA342M"] = true,
  ["SA342Mistral"] = true,
  ["SA342Minigun"] = false, -- no optics
  ["SA342L"] = true,
  ["Ka-50"] = true,
  ["Ka-50_3"] = true,
  ["OH58D"] = false, -- has onboard and useable laser   
}

---
-- @type SmokeColor
-- @field #string color
PLAYERRECCE.SmokeColor = {
  ["highsmoke"] = SMOKECOLOR.Orange,
  ["medsmoke"] = SMOKECOLOR.White,
  ["lowsmoke"] = SMOKECOLOR.Green,
  ["lasersmoke"] = SMOKECOLOR.Red,
  ["ownsmoke"] = SMOKECOLOR.Blue,
}

---
-- @type FlareColor
-- @field #string color
PLAYERRECCE.FlareColor = {
  ["highflare"] =FLARECOLOR.Yellow,
  ["medflare"] = FLARECOLOR.White,
  ["lowflare"] = FLARECOLOR.Green,
  ["laserflare"] = FLARECOLOR.Red,
  ["ownflare"] = FLARECOLOR.Green,
}

--- Create and run a new PlayerRecce instance.
-- @param #PLAYERRECCE self
-- @param #string Name The name of this instance
-- @param #number Coalition, e.g. coalition.side.BLUE
-- @param Core.Set#SET_CLIENT PlayerSet The set of pilots working as recce
-- @return #PLAYERRECCE self
function PLAYERRECCE:New(Name, Coalition, PlayerSet)
  
  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #PLAYERRECCE
  
  self.Name = Name or "Blue FACA"
  self.Coalition = Coalition or coalition.side.BLUE
  self.CoalitionName = UTILS.GetCoalitionName(Coalition)
  self.PlayerSet = PlayerSet
  
  self.lid=string.format("PlayerForwardController %s %s | ", self.Name, self.version)
  
  self:SetLaserCodes( { 1688, 1130, 4785, 6547, 1465, 4578 } ) -- set self.LaserCodes
  self.lasingtime = 60
  
  self.minthreatlevel = 0
  
  self.reporttostringbullsonly = true
  
  self.TForget = 600
  self.TargetCache = FIFO:New()
  
  -- FSM start state is STOPPED.
  self:SetStartState("Stopped")
  
  self:AddTransition("Stopped",      "Start",               "Running")
  self:AddTransition("*",            "Status",              "*")
  self:AddTransition("*",            "RecceOnStation",      "*")
  self:AddTransition("*",            "RecceOffStation",     "*")
  self:AddTransition("*",            "TargetDetected",      "*")
  self:AddTransition("*",            "TargetsSmoked",       "*")
  self:AddTransition("*",            "TargetsFlared",       "*")
  self:AddTransition("*",            "Illumination",        "*")
  self:AddTransition("*",            "TargetLasing",        "*")
  self:AddTransition("*",            "TargetLOSLost",       "*")
  self:AddTransition("*",            "TargetReport",        "*")
  self:AddTransition("*",            "TargetReportSent",    "*")
  self:AddTransition("*",            "Shack",                "*")
  self:AddTransition("Running",      "Stop",                "Stopped")
  
  -- Player Events
  self:HandleEvent(EVENTS.PlayerLeaveUnit, self._EventHandler)
  self:HandleEvent(EVENTS.Ejection, self._EventHandler)
  self:HandleEvent(EVENTS.Crash, self._EventHandler)
  self:HandleEvent(EVENTS.PilotDead, self._EventHandler)
  self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler)
  
  self:__Start(-1)
  local starttime = math.random(5,10)
  self:__Status(-starttime)
  
  self:I(self.lid.." Started.")
  
  ------------------------
  --- Pseudo Functions ---
  ------------------------
  
    --- Triggers the FSM event "Start". Starts the PLAYERRECCE. Note: Start() is called automatically after New().
  -- @function [parent=#PLAYERRECCE] Start
  -- @param #PLAYERRECCE self

  --- Triggers the FSM event "Start" after a delay. Starts the PLAYERRECCE. Note: Start() is called automatically after New().
  -- @function [parent=#PLAYERRECCE] __Start
  -- @param #PLAYERRECCE self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the PLAYERRECCE and all its event handlers.
  -- @param #PLAYERRECCE self
  
  --- Triggers the FSM event "Stop" after a delay. Stops the PLAYERRECCE and all its event handlers.
  -- @function [parent=#PLAYERRECCE] __Stop
  -- @param #PLAYERRECCE self
  -- @param #number delay Delay in seconds.
 
  --- FSM Function OnAfterRecceOnStation. Recce came on station.
  -- @function [parent=#PLAYERRECCE] OnAfterRecceOnStation
  -- @param #PLAYERRECCE self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Client#CLIENT Client
  -- @param #string Playername
  -- @return #PLAYERRECCE self
   
  --- FSM Function OnAfterRecceOffStation. Recce went off duty.
  -- @function [parent=#PLAYERRECCE] OnAfterRecceOffStation
  -- @param #PLAYERRECCE self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Client#CLIENT Client
  -- @param #string Playername
  -- @return #PLAYERRECCE self
   
  --- FSM Function OnAfterTargetDetected. Targets detected.
  -- @function [parent=#PLAYERRECCE] OnAfterTargetDetected
  -- @param #PLAYERRECCE self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param #table Targetsbyclock #table with index 1..12 containing a #table of Wrapper.Unit#UNIT objects each.
  -- @param Wrapper.Client#CLIENT Client
  -- @param #string Playername
  -- @return #PLAYERRECCE self
  
  --- FSM Function OnAfterTargetsSmoked. Smoke grenade shot.
  -- @function [parent=#PLAYERRECCE] OnAfterTargetsSmoked
  -- @param #PLAYERRECCE self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Client#CLIENT Client
  -- @param #string Playername
  -- @param Core.Set#SET_UNIT TargetSet
  -- @return #PLAYERRECCE self
   
  --- FSM Function OnAfterTargetsFlared. Flares shot.
  -- @function [parent=#PLAYERRECCE] OnAfterTargetsFlared
  -- @param #PLAYERRECCE self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Client#CLIENT Client
  -- @param #string Playername
  -- @param Core.Set#SET_UNIT TargetSet
  -- @return #PLAYERRECCE self
   
  --- FSM Function OnAfterIllumination. Illumination rocket shot.
  -- @function [parent=#PLAYERRECCE] OnAfterIllumination
  -- @param #PLAYERRECCE self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Client#CLIENT Client
  -- @param #string Playername
  -- @param Core.Set#SET_UNIT TargetSet
  -- @return #PLAYERRECCE self
   
  --- FSM Function OnAfterTargetLasing. Lasing a new target.
  -- @function [parent=#PLAYERRECCE] OnAfterTargetLasing
  -- @param #PLAYERRECCE self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Client#CLIENT Client
  -- @param Wrapper.Unit#UNIT Target
  -- @param #number Lasercode
  -- @param #number Lasingtime
  -- @return #PLAYERRECCE self
   
  --- FSM Function OnAfterTargetLOSLost. Lost LOS on lased target.
  -- @function [parent=#PLAYERRECCE] OnAfterTargetLOSLost
  -- @param #PLAYERRECCE self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Client#CLIENT Client
  -- @param Wrapper.Unit#UNIT Target
  -- @return #PLAYERRECCE self
   
  --- FSM Function OnAfterTargetReport. Laser target report sent.
  -- @function [parent=#PLAYERRECCE] OnAfterTargetReport
  -- @param #PLAYERRECCE self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Client#CLIENT Client
  -- @param Core.Set#SET_UNIT TargetSet
  -- @param Wrapper.Unit#UNIT Target Target currently lased
  -- @param #string Text
  -- @return #PLAYERRECCE self
   
  --- FSM Function OnAfterTargetReportSent. All targets report sent.
  -- @function [parent=#PLAYERRECCE] OnAfterTargetReportSent
  -- @param #PLAYERRECCE self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Client#CLIENT Client Client sending the report
  -- @param #string Playername Player name
  -- @param Core.Set#SET_UNIT TargetSet Set of targets
  -- @return #PLAYERRECCE self
   
  --- FSM Function OnAfterShack. Lased target has been destroyed.
  -- @function [parent=#PLAYERRECCE] OnAfterShack
  -- @param #PLAYERRECCE self
  -- @param #string From State.
  -- @param #string Event Trigger.
  -- @param #string To State.
  -- @param Wrapper.Client#CLIENT Client
  -- @param Wrapper.Unit#UNIT Target The destroyed target (if obtainable)
  -- @return #PLAYERRECCE self
  
  return self
end

------------------------------------------------------------------------------------------
-- TODO: Functions
------------------------------------------------------------------------------------------

--- [Internal] Event handling
-- @param #PLAYERRECCE self
-- @param Core.Event#EVENTDATA EventData
-- @return #PLAYERRECCE self
function PLAYERRECCE:_EventHandler(EventData)
  self:T(self.lid.."_EventHandler: "..EventData.id)
  if EventData.id == EVENTS.PlayerLeaveUnit or EventData.id == EVENTS.Ejection or EventData.id == EVENTS.Crash or EventData.id == EVENTS.PilotDead then
    if EventData.IniPlayerName then
      self:T(self.lid.."Event for player: "..EventData.IniPlayerName)
      if self.ClientMenus[EventData.IniPlayerName] then
        self.ClientMenus[EventData.IniPlayerName]:Remove()
      end  
      self.ClientMenus[EventData.IniPlayerName] = nil
      self.LaserSpots[EventData.IniPlayerName] = nil
      self.OnStation[EventData.IniPlayerName] = false
      self.LaserFOV[EventData.IniPlayerName] = nil
      self.UnitLaserCodes[EventData.IniPlayerName] = nil
      self.LaserTarget[EventData.IniPlayerName] = nil
      self.AutoLase[EventData.IniPlayerName] = false
      if self.ViewZone[EventData.IniPlayerName] then self.ViewZone[EventData.IniPlayerName]:UndrawZone() end
      if self.ViewZoneLaser[EventData.IniPlayerName] then self.ViewZoneLaser[EventData.IniPlayerName]:UndrawZone() end
      if self.ViewZoneVisual[EventData.IniPlayerName] then self.ViewZoneVisual[EventData.IniPlayerName]:UndrawZone() end
    end
  elseif EventData.id == EVENTS.PlayerEnterAircraft and EventData.IniCoalition == self.Coalition then
    if EventData.IniPlayerName then
      self:T(self.lid.."Event for player: "..EventData.IniPlayerName)
      self.UnitLaserCodes[EventData.IniPlayerName] = 1688
      self.ClientMenus[EventData.IniPlayerName] = nil
      self.LaserSpots[EventData.IniPlayerName] = nil
      self.OnStation[EventData.IniPlayerName] = false
      self.LaserFOV[EventData.IniPlayerName] = nil
      self.UnitLaserCodes[EventData.IniPlayerName] = nil
      self.LaserTarget[EventData.IniPlayerName] = nil
      self.AutoLase[EventData.IniPlayerName] = false
      if self.ViewZone[EventData.IniPlayerName] then self.ViewZone[EventData.IniPlayerName]:UndrawZone() end
      if self.ViewZoneLaser[EventData.IniPlayerName] then self.ViewZoneLaser[EventData.IniPlayerName]:UndrawZone() end
      if self.ViewZoneVisual[EventData.IniPlayerName] then self.ViewZoneVisual[EventData.IniPlayerName]:UndrawZone() end
      self:_BuildMenus()
    end
  end
  return self
end

--- (Internal) Function to determine clockwise direction to target.
-- @param #PLAYERRECCE self
-- @param Wrapper.Unit#UNIT unit The Helicopter
-- @param Wrapper.Unit#UNIT target The downed Group
-- @return #number direction
function PLAYERRECCE:_GetClockDirection(unit, target)
  self:T(self.lid .. " _GetClockDirection")
 
  local _playerPosition = unit:GetCoordinate() -- get position of helicopter
  local _targetpostions = target:GetCoordinate() -- get position of downed pilot
  local _heading = unit:GetHeading() -- heading
  --self:I("Heading = ".._heading)
  local DirectionVec3 = _playerPosition:GetDirectionVec3( _targetpostions )
  local Angle = _playerPosition:GetAngleDegrees( DirectionVec3 )
  --self:I("Angle = "..Angle)
  local clock = 12
  local hours = 0   
  if _heading and Angle then
    clock = 12
    --if angle == 0 then angle = 360 end
    clock = _heading-Angle  
    hours = (clock/30)*-1
    --self:I("hours = "..hours)
    clock = 12+hours
    clock = UTILS.Round(clock,0)
    if clock > 12 then clock = clock-12 end
    if clock == 0 then clock = 12 end
  end
  --self:I("Clock ="..clock)    
  return clock
end

--- [User] Set a table of possible laser codes.
-- Each new RECCE can select a code from this table, default is { 1688, 1130, 4785, 6547, 1465, 4578 }.
-- @param #PLAYERRECCE self
-- @param #list<#number> LaserCodes
-- @return #PLAYERRECCE
function PLAYERRECCE:SetLaserCodes( LaserCodes )
  self.LaserCodes = ( type( LaserCodes ) == "table" ) and LaserCodes or { LaserCodes }
  return self
end

--- [User] Set a reference point coordinate for A2G Operations. Will be used in coordinate references.
-- @param #PLAYERRECCE self
-- @param Core.Point#COORDINATE Coordinate Coordinate of the RP
-- @param #string Name Name of the RP
-- @return #PLAYERRECCE
function PLAYERRECCE:SetReferencePoint(Coordinate,Name)
  self.ReferencePoint = Coordinate
  self.RPName = Name
  if self.RPMarker then
    self.RPMarker:Remove()
  end
  local llddm = Coordinate:ToStringLLDDM()
  local lldms = Coordinate:ToStringLLDMS()
  local mgrs = Coordinate:ToStringMGRS()
  local text = string.format("%s RP %s\n%s\n%s\n%s",self.Name,Name,llddm,lldms,mgrs)
  self.RPMarker = MARKER:New(Coordinate,text)
  self.RPMarker:ReadOnly()
  self.RPMarker:ToCoalition(self.Coalition)
  return self
end

--- [User] Set PlayerTaskController. Allows to upload target reports to the controller, in turn creating tasks for other players.
-- @param #PLAYERRECCE self
-- @param Ops.PlayerTask#PLAYERTASKCONTROLLER Controller
-- @return #PLAYERRECCE
function PLAYERRECCE:SetPlayerTaskController(Controller)
  self.UseController = true
  self.Controller = Controller
  return self
end

--- [User] Set a set of clients which will receive target reports
-- @param #PLAYERRECCE self
-- @param Core.Set#SET_CLIENT AttackSet
-- @return #PLAYERRECCE
function PLAYERRECCE:SetAttackSet(AttackSet)
  self.AttackSet = AttackSet
  return self
end

---[Internal] Check Helicopter camera in on
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT client
-- @param #string playername
-- @return #boolean OnOff
function PLAYERRECCE:_CameraOn(client,playername)
  local camera = true
  local unit = client -- Wrapper.Unit#UNIT
  if unit and unit:IsAlive() then
    local typename = unit:GetTypeName()
    if string.find(typename,"SA342") then
      local dcsunit = Unit.getByName(client:GetName())
      local vivihorizontal = dcsunit:getDrawArgumentValue(215) or 0 -- (not in MiniGun) 1 to -1 -- zero is straight ahead, 1/-1 = 180 deg
      if vivihorizontal < -0.7 or vivihorizontal > 0.7 then 
        camera = false
      end
    elseif string.find(typename,"OH58") then
      local dcsunit = Unit.getByName(client:GetName())
      local vivihorizontal = dcsunit:getDrawArgumentValue(528) or 0 -- Kiow
      if vivihorizontal < -0.527 or vivihorizontal > 0.527 then 
        camera = false
      end  
    elseif string.find(typename,"Ka-50")  then
      camera = true
    end
  end
  return camera
end

--- [Internal] Get the view parameters from a Kiowa MMS camera
-- @param #PLAYERRECCE self
-- @param Wrapper.Unit#UNIT Kiowa
-- @return #number cameraheading in degrees.
-- @return #number cameranodding in degrees.
-- @return #number maxview in meters.
-- @return #boolean cameraison If true, camera is on, else off.
function PLAYERRECCE:_GetKiowaMMSSight(Kiowa)
  self:T(self.lid.."_GetKiowaMMSSight")
  local unit = Kiowa -- Wrapper.Unit#UNIT
  if unit and unit:IsAlive() then
    local dcsunit = Unit.getByName(Kiowa:GetName())
    --[[
    shagrat — 01/01/2025 23:13
    Found the necessary ARGS for the Kiowa MMS angle and rotation:
    Arg 527 vertical movement
     0 = neutral
    -1.0 = max depression (30° max depression angle)
    +1.0 = max elevation angle (30° max elevation angle)
    
    Arg 528 horizontal movement 
    0 = forward (0 degr)
    -0.25 = 90° left
    -0.5 = rear (180°) left (max 190° = -0.527
    +0.25 = 90° right
    +0.5 = 180° right (max 190° = 0.527)
    --]]
    local mmshorizontal = dcsunit:getDrawArgumentValue(528) or 0
    local mmsvertical = dcsunit:getDrawArgumentValue(527) or 0
    self:T(string.format("Kiowa MMS Arguments Read: H %.3f V %.3f",mmshorizontal,mmsvertical))
    local mmson = true
    if mmshorizontal < -0.527 or mmshorizontal > 0.527 then mmson = false end
    local horizontalview = mmshorizontal / 0.527 * 190
    local heading = unit:GetHeading()
    local mmsheading = (heading+horizontalview)%360
    --local mmsyaw = mmsvertical * 30
    local mmsyaw = math.atan(mmsvertical)*40
    local maxview = self:_GetActualMaxLOSight(unit,mmsheading, mmsyaw,not mmson)
    if maxview > 8000 then maxview = 8000 end
    self:T(string.format("Kiowa MMS Heading %d, Yaw %d, MaxView %dm MMS On %s",mmsheading,mmsyaw,maxview,tostring(mmson)))
    return mmsheading,mmsyaw,maxview,mmson
  end
  return 0,0,0,false
end


--- [Internal] Get the view parameters from a Gazelle camera
-- @param #PLAYERRECCE self
-- @param Wrapper.Unit#UNIT Gazelle
-- @return #number cameraheading in degrees.
-- @return #number cameranodding in degrees.
-- @return #number maxview in meters.
-- @return #boolean cameraison If true, camera is on, else off.
function PLAYERRECCE:_GetGazelleVivianneSight(Gazelle)
  self:T(self.lid.."GetGazelleVivianneSight")
  local unit = Gazelle -- Wrapper.Unit#UNIT
  if unit and unit:IsAlive() then
    local dcsunit = Unit.getByName(Gazelle:GetName())
    local vivihorizontal = dcsunit:getDrawArgumentValue(215) or 0 -- (not in MiniGun) 1 to -1 -- zero is straight ahead, 1/-1 = 180 deg,
    local vivivertical = dcsunit:getDrawArgumentValue(216) or 0 -- L/Mistral/Minigun model has no 216, ca 10deg up (=1) and down (=-1)
    -- vertical model limits 1.53846, -1.10731
    local vivioff = false
    -- -1 = -180, 1 = 180
    -- Actual model view -0,66 to 0,66
    -- Nick view 1.53846, -1.10731 for - 30° to +45° 
    if vivihorizontal < -0.67 then  -- model end
      vivihorizontal = -0.67
      vivioff = false
      --return 0,0,0,false 
    elseif vivihorizontal > 0.67 then -- vivi off
      vivihorizontal = 0.67
      vivioff = true
      return 0,0,0,false
    end

    local horizontalview = vivihorizontal * -180 
    --local verticalview = vivivertical * 30 -- ca +/- 30°
    local verticalview = math.atan(vivivertical)

    local heading = unit:GetHeading()
    local viviheading = (heading+horizontalview)%360
    local maxview = self:_GetActualMaxLOSight(unit,viviheading, verticalview,vivioff)
    if maxview > 8000 then maxview = 8000 end
    return viviheading, verticalview,maxview, not vivioff
  end
  return 0,0,0,false
end

--- [Internal] Get the max line of sight based on unit head and camera nod via trigonometry. Returns 0 if camera is off.
-- @param #PLAYERRECCE self
-- @param Wrapper.Unit#UNIT unit The unit which LOS we want
-- @param #number vheading Heading where the unit or camera is looking
-- @param #number vnod Nod down in degrees
-- @param #boolean vivoff Camera on or off
-- @return #number maxview Max view distance in meters
function PLAYERRECCE:_GetActualMaxLOSight(unit,vheading, vnod, vivoff)
  self:T(self.lid.."_GetActualMaxLOSight")
  if vivoff then return 0 end
  --if vnod < -0.03 then vnod = -0.03 end
  local maxview = 0
  if unit and unit:IsAlive() then
    local typename = unit:GetTypeName()
    maxview = self.MaxViewDistance[typename] or 8000  
    local CamHeight = self.Cameraheight[typename] or 1
    if vnod < -2 then
        -- Looking down
        -- determine max distance we're looking at
        local beta = 90
        local gamma = 90-math.abs(vnod)
        local alpha = 90-gamma
        local a = unit:GetHeight()-unit:GetCoordinate():GetLandHeight()+CamHeight
        local b = a / math.sin(math.rad(alpha))
        local c = b * math.sin(math.rad(gamma))
        maxview = c*1.2 -- +20%
    end
  end 
  return math.ceil(math.abs(maxview))
end

--- [User] Set callsign options for TTS output. See @{Wrapper.Group#GROUP.GetCustomCallSign}() on how to set customized callsigns.
-- @param #PLAYERRECCE self
-- @param #boolean ShortCallsign If true, only call out the major flight number
-- @param #boolean Keepnumber If true, keep the **customized callsign** in the #GROUP name for players as-is, no amendments or numbers.
-- @param #table CallsignTranslations (optional) Table to translate between DCS standard callsigns and bespoke ones. Does not apply if using customized
-- callsigns from playername or group name.
-- @param #func CallsignCustomFunc (Optional) For player names only(!). If given, this function will return the callsign. Needs to take the groupname and the playername as first two arguments.
-- @param #arg ... (Optional) Comma separated arguments to add to the custom function call after groupname and playername.
-- @return #PLAYERRECCE self
function PLAYERRECCE:SetCallSignOptions(ShortCallsign,Keepnumber,CallsignTranslations,CallsignCustomFunc,...)
  if not ShortCallsign or ShortCallsign == false then
   self.ShortCallsign = false
  else
   self.ShortCallsign = true
  end
  self.Keepnumber = Keepnumber or false
  self.CallsignTranslations = CallsignTranslations
  self.CallsignCustomFunc = CallsignCustomFunc
  self.CallsignCustomArgs = arg or {}
  return self  
end

--- [Internal] Build a ZONE_POLYGON from a given viewport of a unit
-- @param #PLAYERRECCE self
-- @param Wrapper.Unit#UNIT unit The unit which is looking
-- @param #number vheading Heading where the unit or camera is looking
-- @param #number minview Min line of sight - for lasing
-- @param #number maxview Max line of sight
-- @param #number angle  Angle left/right to be added to heading to form a triangle
-- @param #boolean camon Camera is switched on
-- @param #boolean laser Zone is for lasing
-- @return Core.Zone#ZONE_POLYGON ViewZone or nil if camera is off
function PLAYERRECCE:_GetViewZone(unit, vheading, minview, maxview, angle, camon, laser)
  self:T(self.lid.."_GetViewZone")
  local viewzone = nil
  if not camon then return nil end
  if unit and unit:IsAlive() then
    local unitname = unit:GetName()
    if not laser then
      -- Triangle
      local startpos = unit:GetCoordinate()
      local heading1 = (vheading+angle)%360
      local heading2 = (vheading-angle)%360
      local pos1 = startpos:Translate(maxview,heading1)
      local pos2 = startpos:Translate(maxview,heading2)
      local array = {}
      table.insert(array,startpos:GetVec2())
      table.insert(array,pos1:GetVec2())
      table.insert(array,pos2:GetVec2())
      viewzone = ZONE_POLYGON:NewFromPointsArray(unitname,array)
    else
      -- Square
      local startp = unit:GetCoordinate()
      local heading1 = (vheading+90)%360
      local heading2 = (vheading-90)%360
      self:T({heading1,heading2})
      local startpos = startp:Translate(minview,vheading)
      local pos1 = startpos:Translate(12.5,heading1)
      local pos2 = startpos:Translate(12.5,heading2)
      local pos3 = pos1:Translate(maxview,vheading)
      local pos4 = pos2:Translate(maxview,vheading)
      local array = {}
      table.insert(array,pos1:GetVec2())
      table.insert(array,pos2:GetVec2())
      table.insert(array,pos4:GetVec2())
      table.insert(array,pos3:GetVec2())
      viewzone = ZONE_POLYGON:NewFromPointsArray(unitname,array)
    end 
  end
  return viewzone
end

--- [Internal] 
--@param #PLAYERRECCE self
--@param Wrapper.Client#CLIENT client
--@return Core.Set#SET_UNIT Set of targets, can be empty!
--@return #number count Count of targets
function PLAYERRECCE:_GetKnownTargets(client)
  self:T(self.lid.."_GetKnownTargets")
  local finaltargets = SET_UNIT:New()
  local targets = self.TargetCache:GetDataTable()
  local playername = client:GetPlayerName()
  for _,_target in pairs(targets) do
    local targetdata = _target.PlayerRecceDetected -- Ops.PlayerRecce#PLAYERRECCE.PlayerRecceDetected
    if targetdata.playername == playername then
      finaltargets:Add(_target:GetName(),_target)
    end
  end
  return finaltargets,finaltargets:CountAlive()
end

--- [Internal] 
--@param #PLAYERRECCE self
--@return #PLAYERRECCE self
function PLAYERRECCE:_CleanupTargetCache()
  self:T(self.lid.."_CleanupTargetCache")
  local cleancache = FIFO:New()
  self.TargetCache:ForEach(
    function(unit)
      local pull = false
      if unit and unit:IsAlive() and unit:GetLife() > 1 then
        if unit.PlayerRecceDetected and unit.PlayerRecceDetected.timestamp then
          local TNow = timer.getTime()
          if TNow-unit.PlayerRecceDetected.timestamp > self.TForget then
            -- Forget this unit
            pull = true
            unit.PlayerRecceDetected=nil
          end
        else
          -- no timestamp
          pull = true
        end
      else
        -- dead
        pull = true
      end
      if not pull then
        cleancache:Push(unit,unit:GetName())
      end
    end
  )
  self.TargetCache = nil
  self.TargetCache = cleancache
  return self
end

--- [Internal] 
--@param #PLAYERRECCE self
--@param Wrapper.Unit#UNIT unit The FACA unit
--@param #boolean camera If true, use the unit's camera for targets in sight
--@param #laser laser Use laser zone
--@return Core.Set#SET_UNIT Set of targets, can be empty!
--@return #number count Count of targets
function PLAYERRECCE:_GetTargetSet(unit,camera,laser)
  self:T(self.lid.."_GetTargetSet")
  local finaltargets = SET_UNIT:New()
  local finalcount = 0
  local minview = 0
  local typename = unit:GetTypeName()
  local playername = unit:GetPlayerName()
  local maxview = self.MaxViewDistance[typename] or 8000
  local heading,nod,maxview,angle = 0,30,8000,10
  local camon = false
  local name = unit:GetName()
  if string.find(typename,"SA342") and camera then
    heading,nod,maxview,camon = self:_GetGazelleVivianneSight(unit)
    angle=10
    -- Model nod and actual TV view don't compute
    maxview = self.MaxViewDistance[typename] or 8000
  elseif string.find(typename,"Ka-50") and camera then
    heading = unit:GetHeading()
    nod,maxview,camon = 10,1000,true
    angle = 10
    maxview = self.MaxViewDistance[typename] or 8000
  elseif string.find(typename,"OH58") and camera then
    --heading = unit:GetHeading()
    nod,maxview,camon = 0,8000,true
    heading,nod,maxview,camon = self:_GetKiowaMMSSight(unit)
    angle = 8
    if maxview == 0 then
      maxview = self.MaxViewDistance[typename] or 8000
    end
  else
    -- visual
    heading = unit:GetHeading()
    nod,maxview,camon = 10,3000,true
    maxview = self.MaxViewDistance[typename] or 3000
    angle = 45
  end
  if laser then
    -- get min/max values
    if not self.LaserFOV[playername] then
      minview = 100
      maxview = 2000
      self.LaserFOV[playername] = {
        min=100,
        max=2000,
      }
    else
      minview = self.LaserFOV[playername].min
      maxview = self.LaserFOV[playername].max
    end
  end
  local zone = self:_GetViewZone(unit,heading,minview,maxview,angle,camon,laser)
  if zone then
    local redcoalition = "red"
    if self.Coalition == coalition.side.RED then
      redcoalition = "blue"
    end
    -- determine what we can see
    local startpos = unit:GetCoordinate()
    local targetset = SET_UNIT:New():FilterCategories("ground"):FilterActive(true):FilterZones({zone}):FilterCoalitions(redcoalition):FilterOnce()
    self:T("Prefilter Target Count = "..targetset:CountAlive())
    -- TODO - Threat level filter?
    -- TODO - Min distance from unit?
    targetset:ForEach(
      function(_unit)
        local _unit = _unit -- Wrapper.Unit#UNIT
        local _unitpos = _unit:GetCoordinate()
        if startpos:IsLOS(_unitpos) and _unit:IsAlive() and _unit:GetLife()>1 then
            self:T("Adding to final targets: ".._unit:GetName())
          finaltargets:Add(_unit:GetName(),_unit)
        end
      end
      )
    finalcount = finaltargets:CountAlive()
    self:T(string.format("%s Unit: %s | Targets in view %s",self.lid,name,finalcount))
  end
  return finaltargets, finalcount, zone
end

---[Internal] 
--@param #PLAYERRECCE self
--@param Core.Set#SET_UNIT targetset Set of targets, can be empty!
--@return Wrapper.Unit#UNIT Target or nil
function PLAYERRECCE:_GetHVTTarget(targetset)
   self:T(self.lid.."_GetHVTTarget")
   -- sort units
   local unitsbythreat = {}
   local minthreat = self.minthreatlevel or 0
   for _,_unit in pairs(targetset.Set) do
    local unit = _unit -- Wrapper.Unit#UNIT
    if unit and unit:IsAlive() and unit:GetLife() >1 then
      local threat = unit:GetThreatLevel()
      if threat >= minthreat then
        -- prefer radar units
        if unit:HasAttribute("RADAR_BAND1_FOR_ARM") or unit:HasAttribute("RADAR_BAND2_FOR_ARM") or unit:HasAttribute("Optical Tracker") then
          threat = 11
        end
        table.insert(unitsbythreat,{unit,threat})
      end
    end
  end
  
  table.sort(unitsbythreat, function(a,b)
    local aNum = a[2] -- Coin value of a
    local bNum = b[2] -- Coin value of b
    return aNum > bNum -- Return their comparisons, < for ascending, > for descending
  end)
 
 if unitsbythreat[1] and unitsbythreat[1][1] then   
  return unitsbythreat[1][1]  
 else
  return nil
 end
end

--- [Internal] 
--@param #PLAYERRECCE self
--@param Wrapper.Client#CLIENT client The FACA unit
--@param Core.Set#SET_UNIT targetset Set of targets, can be empty!
--@return #PLAYERRECCE self
function PLAYERRECCE:_LaseTarget(client,targetset)
  self:T(self.lid.."_LaseTarget")
  -- get one target
  local target = self:_GetHVTTarget(targetset) -- Wrapper.Unit#UNIT
  local playername = client:GetPlayerName()
  local laser = nil -- Core.Spot#SPOT
  -- set laser
  if not self.LaserSpots[playername] then
    laser = SPOT:New(client)
    if not self.UnitLaserCodes[playername] then
      self.UnitLaserCodes[playername] = 1688
    end
    laser.LaserCode = self.UnitLaserCodes[playername] or 1688
    self.LaserSpots[playername] = laser
  else
    laser = self.LaserSpots[playername]
  end
  -- old target
  if self.LaserTarget[playername] then
    -- still looking at target?
    local target=self.LaserTarget[playername] -- Ops.Target#TARGET
    local oldtarget = target:GetObject() --or laser.Target
    self:T("Targetstate: "..target:GetState())
    self:T("Laser State: "..tostring(laser:IsLasing()))
    if (not oldtarget) or targetset:IsNotInSet(oldtarget) or target:IsDead() or target:IsDestroyed() then
      -- lost LOS or dead
      laser:LaseOff()
      self:T(self.lid.."Target Life Points: "..target:GetLife() or "none")
      if target:IsDead() or target:IsDestroyed() or target:GetDamage() > 79 or target:GetLife() <= 1 then
        self:__Shack(-1,client,oldtarget)
        --self.LaserTarget[playername] = nil
      else
        self:__TargetLOSLost(-1,client,oldtarget)
        --self.LaserTarget[playername] = nil
      end
      self.LaserTarget[playername] = nil
      oldtarget = nil
      self.LaserSpots[playername] = nil
    elseif oldtarget and laser and (not laser:IsLasing()) then
      --laser:LaseOff()
      self:T("Switching laser back on ..")
      local lasercode = self.UnitLaserCodes[playername] or laser.LaserCode or 1688
      local lasingtime = self.lasingtime or 60
      --local targettype = target:GetTypeName()
      laser:LaseOn(oldtarget,lasercode,lasingtime)
      --self:__TargetLasing(-1,client,oldtarget,lasercode,lasingtime) 
    else
      -- we should not be here...
      self:T("Target alive and laser is on!")
      --self.LaserSpots[playername] = nil
    end
  -- new target
  elseif (not laser:IsLasing()) and target then
    local relativecam = self.LaserRelativePos[client:GetTypeName()]
    laser:SetRelativeStartPosition(relativecam)
    local lasercode = self.UnitLaserCodes[playername] or laser.LaserCode or 1688
    local lasingtime = self.lasingtime or 60
    --local targettype = target:GetTypeName()
    laser:LaseOn(target,lasercode,lasingtime) 
    self.LaserTarget[playername] = TARGET:New(target)
    --self.LaserTarget[playername].TStatus = 9
    self:__TargetLasing(-1,client,target,lasercode,lasingtime)
  end
  return self
end

--- [Internal] 
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT client
-- @param Wrapper.Group#GROUP group
-- @param #string playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:_SetClientLaserCode(client,group,playername,code)
  self:T(self.lid.."_SetClientLaserCode")
  self.UnitLaserCodes[playername] = code or 1688
  if self.ClientMenus[playername] then
    self.ClientMenus[playername]:Remove()
    self.ClientMenus[playername]=nil
  end
  self:_BuildMenus()
  return self
end

--- [Internal] 
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT client
-- @param Wrapper.Group#GROUP group
-- @param #string playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:_SwitchOnStation(client,group,playername)
  self:T(self.lid.."_SwitchOnStation")
  if not self.OnStation[playername] then
    self.OnStation[playername] = true
    self:__RecceOnStation(-1,client,playername)
  else
    self.OnStation[playername] = false
    self:__RecceOffStation(-1,client,playername)
  end
  if self.ClientMenus[playername] then
    self.ClientMenus[playername]:Remove()
    self.ClientMenus[playername]=nil
  end
  self:_BuildMenus(client)
  return self
end

--- [Internal] 
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT client
-- @param Wrapper.Group#GROUP group
-- @param #string playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:_SwitchSmoke(client,group,playername)
  self:T(self.lid.."_SwitchLasing")
  if not self.SmokeOwn[playername] then
    self.SmokeOwn[playername] = true
    MESSAGE:New("Smoke self is now ON",10,self.Name or "FACA"):ToClient(client)
  else
    self.SmokeOwn[playername] = false
    MESSAGE:New("Smoke self is now OFF",10,self.Name or "FACA"):ToClient(client)
  end
  if self.ClientMenus[playername] then
    self.ClientMenus[playername]:Remove()
    self.ClientMenus[playername]=nil
  end
  self:_BuildMenus(client)
  return self
end

--- [Internal] 
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT client
-- @param Wrapper.Group#GROUP group
-- @param #string playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:_SwitchLasing(client,group,playername)
  self:T(self.lid.."_SwitchLasing")
  if not self.AutoLase[playername] then
    self.AutoLase[playername] = true
    MESSAGE:New("Lasing is now ON",10,self.Name or "FACA"):ToClient(client)
  else
    self.AutoLase[playername] = false
    if self.LaserSpots[playername] then 
      local laser = self.LaserSpots[playername] -- Core.Spot#SPOT
      if laser:IsLasing() then
        laser:LaseOff()
      end
      self.LaserSpots[playername] = nil
    end
    MESSAGE:New("Lasing is now OFF",10,self.Name or "FACA"):ToClient(client)
  end
  if self.ClientMenus[playername] then
    self.ClientMenus[playername]:Remove()
    self.ClientMenus[playername]=nil
  end
  self:_BuildMenus(client)
  return self
end

--- [Internal] 
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT client
-- @param Wrapper.Group#GROUP group
-- @param #string playername
-- @param #number mindist
-- @param #number maxdist
-- @return #PLAYERRECCE self
function PLAYERRECCE:_SwitchLasingDist(client,group,playername,mindist,maxdist)
  self:T(self.lid.."_SwitchLasingDist")
  local mind  = mindist or 100
  local maxd  = maxdist or 2000
  if not self.LaserFOV[playername] then
    self.LaserFOV[playername] = {
      min=mind,
      max=maxd,
    }
  else
    self.LaserFOV[playername].min=mind
    self.LaserFOV[playername].max=maxd
  end
  MESSAGE:New(string.format("Laser distance set to %d-%dm!",mindist,maxdist),10,"FACA"):ToClient(client)
  if self.ClientMenus[playername] then
    self.ClientMenus[playername]:Remove()
    self.ClientMenus[playername]=nil
  end
  self:_BuildMenus(client)
  return self
end

--- [Internal] 
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT client
-- @param Wrapper.Group#GROUP group
-- @param #string playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:_WIP(client,group,playername)
  self:T(self.lid.."_WIP")
  return self
end

--- [Internal] 
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT client
-- @param Wrapper.Group#GROUP group
-- @param #string playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:_SmokeTargets(client,group,playername)
  self:T(self.lid.."_SmokeTargets")
  local cameraset = self:_GetTargetSet(client,true) -- Core.Set#SET_UNIT
  local visualset = self:_GetTargetSet(client,false) -- Core.Set#SET_UNIT
  
  if cameraset:CountAlive() > 0 or visualset:CountAlive() > 0 then
    self:__TargetsSmoked(-1,client,playername,cameraset)
  else
    return self
  end
  
  local highsmoke = self.SmokeColor.highsmoke
  local medsmoke = self.SmokeColor.medsmoke
  local lowsmoke = self.SmokeColor.lowsmoke
  local lasersmoke = self.SmokeColor.lasersmoke
  local laser = self.LaserSpots[playername] -- Core.Spot#SPOT
  
  -- laser targer gets extra smoke
  if laser and laser.Target and laser.Target:IsAlive() then
    laser.Target:GetCoordinate():Smoke(lasersmoke)
  end
  
  local coord = visualset:GetCoordinate()
  if coord and self.smokeaveragetargetpos then
    coord:SetAtLandheight()
    coord:Smoke(medsmoke)
  else
    -- smoke everything 
    for _,_unit in pairs(visualset.Set) do
      local unit = _unit --Wrapper.Unit#UNIT
      if unit and unit:IsAlive() then
        local coord = unit:GetCoordinate()
        local threat = unit:GetThreatLevel()
        if coord then
          local color = lowsmoke
          if threat > 7 then
            color = highsmoke
          elseif threat > 2 then
            color = medsmoke
          end
          coord:Smoke(color)
        end
      end
    end
  end
  if self.SmokeOwn[playername] then
    local cc = client:GetVec2()
    -- don't smoke mid-air
    local lc = COORDINATE:NewFromVec2(cc,1)
    local color = self.SmokeColor.ownsmoke
    lc:Smoke(color)
  end
  
  return self
end

--- [Internal] 
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT client
-- @param Wrapper.Group#GROUP group
-- @param #string playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:_FlareTargets(client,group,playername)
  self:T(self.lid.."_FlareTargets")
  local cameraset = self:_GetTargetSet(client,true) -- Core.Set#SET_UNIT
  local visualset = self:_GetTargetSet(client,false) -- Core.Set#SET_UNIT
  cameraset:AddSet(visualset)
  if cameraset:CountAlive() > 0 then
    self:__TargetsFlared(-1,client,playername,cameraset)
  end
  local highsmoke = self.FlareColor.highflare
  local medsmoke = self.FlareColor.medflare
  local lowsmoke = self.FlareColor.lowflare
  local lasersmoke = self.FlareColor.laserflare
  local laser = self.LaserSpots[playername] -- Core.Spot#SPOT
  -- laser targer gets extra smoke
  if laser and laser.Target and laser.Target:IsAlive() then
    laser.Target:GetCoordinate():Flare(lasersmoke)
    if cameraset:IsInSet(laser.Target) then
      cameraset:Remove(laser.Target:GetName(),true)
    end
  end
  -- smoke everything else
  for _,_unit in pairs(cameraset.Set) do
    local unit = _unit --Wrapper.Unit#UNIT
    if unit and unit:IsAlive() then
      local coord = unit:GetCoordinate()
      local threat = unit:GetThreatLevel()
      if coord then
        local color = lowsmoke
        if threat > 7 then
          color = highsmoke
        elseif threat > 2 then
          color = medsmoke
        end
        coord:Flare(color)
      end
    end
  end
  return self
end

--- [Internal] 
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT client
-- @param Wrapper.Group#GROUP group
-- @param #string playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:_IlluTargets(client,group,playername)
  self:T(self.lid.."_IlluTargets")
  local totalset, count = self:_GetKnownTargets(client) -- Core.Set#SET_UNIT
  if count > 0 then
    local coord = totalset:GetCoordinate() -- Core.Point#COORDINATE
    coord.y = coord.y + 200
    coord:IlluminationBomb(nil,1)
    self:__Illumination(1,client,playername,totalset)
  end
  return self
end

--- [Internal] 
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT client
-- @param Wrapper.Group#GROUP group
-- @param #string playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:_UploadTargets(client,group,playername)
  self:T(self.lid.."_UploadTargets")
  --local targetset, number = self:_GetTargetSet(client,true)
  --local vtargetset, vnumber = self:_GetTargetSet(client,false)
  local totalset, count = self:_GetKnownTargets(client)
  --local totalset = SET_UNIT:New()
 -- totalset:AddSet(targetset)
  --totalset:AddSet(vtargetset)
  if count > 0 then
    self.Controller:AddTarget(totalset)
    self:__TargetReportSent(1,client,playername,totalset)
  end
  return self
end

--- [Internal] 
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT client
-- @param Wrapper.Group#GROUP group
-- @param #string playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:_ReportLaserTargets(client,group,playername)
self:T(self.lid.."_ReportLaserTargets")
  local targetset, number = self:_GetTargetSet(client,true,true)
  if number > 0 and self.AutoLase[playername] then
    local Settings = ( client and _DATABASE:GetPlayerSettings( playername  ) ) or _SETTINGS
    local target = self:_GetHVTTarget(targetset) -- the one we're lasing
    local ThreatLevel = target:GetThreatLevel() or 1
    local ThreatLevelText = "high"
    if ThreatLevel > 3 and ThreatLevel < 8 then
     ThreatLevelText = "medium"
    elseif  ThreatLevel <= 3 then
     ThreatLevelText = "low"
    end
    local ThreatGraph = "[" .. string.rep(  "■", ThreatLevel ) .. string.rep(  "□", 10 - ThreatLevel ) .. "]: "..ThreatLevel
    local report = REPORT:New("Lasing Report")
    report:Add(string.rep("-",15))
    report:Add("Target type: "..target:GetTypeName() or "unknown")
    report:Add("Threat Level: "..ThreatGraph.." ("..ThreatLevelText..")")
    if not self.ReferencePoint then
      report:Add("Location: "..client:GetCoordinate():ToStringBULLS(self.Coalition,Settings))
      if self.reporttostringbullsonly ~= true then
        report:Add("Location: "..client:GetCoordinate():ToStringA2G(nil,Settings))
      end
    else
      report:Add("Location: "..client:GetCoordinate():ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings))
    end
    report:Add("Laser Code: "..self.UnitLaserCodes[playername] or 1688)
    report:Add(string.rep("-",15))
    local text = report:Text()
    self:__TargetReport(1,client,targetset,target,text)
  else
    local report = REPORT:New("Lasing Report")
    report:Add(string.rep("-",15))
    report:Add("N O  T A R G E T S")
    report:Add(string.rep("-",15))
    local text = report:Text()
    self:__TargetReport(1,client,nil,nil,text)
  end
  return self
end

--- [Internal] 
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT client
-- @param Wrapper.Group#GROUP group
-- @param #string playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:_ReportVisualTargets(client,group,playername)
  self:T(self.lid.."_ReportVisualTargets")
  local targetset, number = self:_GetKnownTargets(client)
    if number > 0 then
    local Settings = ( client and _DATABASE:GetPlayerSettings( playername ) ) or _SETTINGS
    local ThreatLevel = targetset:CalculateThreatLevelA2G()
    local ThreatLevelText = "high"
    if ThreatLevel > 3 and ThreatLevel < 8 then
     ThreatLevelText = "medium"
    elseif  ThreatLevel <= 3 then
     ThreatLevelText = "low"
    end
    local ThreatGraph = "[" .. string.rep(  "■", ThreatLevel ) .. string.rep(  "□", 10 - ThreatLevel ) .. "]: "..ThreatLevel
    local report = REPORT:New("Target Report")
    report:Add(string.rep("-",15))
    report:Add("Target count: "..number)
    report:Add("Threat Level: "..ThreatGraph.." ("..ThreatLevelText..")")
    if not self.ReferencePoint then
      report:Add("Location: "..client:GetCoordinate():ToStringBULLS(self.Coalition,Settings))
      if self.reporttostringbullsonly ~= true then
        report:Add("Location: "..client:GetCoordinate():ToStringA2G(nil,Settings))
      end
    else
      report:Add("Location: "..client:GetCoordinate():ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings))
      if self.reporttostringbullsonly ~= true then
        report:Add("Location: "..client:GetCoordinate():ToStringA2G(nil,Settings))
      end
    end
    report:Add(string.rep("-",15))
    local text = report:Text()
    self:__TargetReport(1,client,targetset,nil,text)
  else
    local report = REPORT:New("Target Report")
    report:Add(string.rep("-",15))
    report:Add("N O  T A R G E T S")
    report:Add(string.rep("-",15))
    local text = report:Text()
    self:__TargetReport(1,client,nil,nil,text)
  end
  return self
end

--- [Internal] Build Menus
-- @param #PLAYERRECCE self
-- @param Wrapper.Client#CLIENT Client (optional) Client object
-- @return #PLAYERRECCE self
function PLAYERRECCE:_BuildMenus(Client)
  self:T(self.lid.."_BuildMenus")
  local clients = self.PlayerSet -- Core.Set#SET_CLIENT
  local clientset = clients:GetSetObjects()
  if Client then clientset = {Client} end
  for _,_client in pairs(clientset) do
    local client = _client -- Wrapper.Client#CLIENT
    if client and client:IsAlive() then
      local playername = client:GetPlayerName()
      self:T("Menu for "..playername)
      if not self.UnitLaserCodes[playername] then
        self:_SetClientLaserCode(nil,nil,playername,1688)
      end
      if self.SmokeOwn[playername] == nil then
        self.SmokeOwn[playername] = self.smokeownposition
      end
      local group = client:GetGroup()
      if not self.ClientMenus[playername] then
        self:T("Start Menubuild for "..playername)
        local canlase = self.CanLase[client:GetTypeName()]
        self.ClientMenus[playername] = MENU_GROUP:New(group,self.MenuName or self.Name or "RECCE")
        local txtonstation = self.OnStation[playername] and "ON" or "OFF"
        local text = string.format("Switch On-Station (%s)",txtonstation)
        local onstationmenu = MENU_GROUP_COMMAND:New(group,text,self.ClientMenus[playername],self._SwitchOnStation,self,client,group,playername)
        if self.OnStation[playername] then
          local smoketopmenu = MENU_GROUP:New(group,"Visual Markers",self.ClientMenus[playername])
          local smokemenu = MENU_GROUP_COMMAND:New(group,"Smoke Targets",smoketopmenu,self._SmokeTargets,self,client,group,playername)
          local flaremenu = MENU_GROUP_COMMAND:New(group,"Flare Targets",smoketopmenu,self._FlareTargets,self,client,group,playername)
          local illumenu = MENU_GROUP_COMMAND:New(group,"Illuminate Area",smoketopmenu,self._IlluTargets,self,client,group,playername)
          local ownsm = self.SmokeOwn[playername] and "ON" or "OFF"
          local owntxt = string.format("Switch smoke self (%s)",ownsm)
          local ownsmoke = MENU_GROUP_COMMAND:New(group,owntxt,smoketopmenu,self._SwitchSmoke,self,client,group,playername)
          if canlase then
            local txtonstation = self.AutoLase[playername] and "ON" or "OFF"
            local text = string.format("Switch Lasing (%s)",txtonstation)
            local lasemenu = MENU_GROUP_COMMAND:New(group,text,self.ClientMenus[playername],self._SwitchLasing,self,client,group,playername)
            local lasedist = MENU_GROUP:New(group,"Set Laser Distance",self.ClientMenus[playername])
            local mindist = 100
            local maxdist = 2000
            if self.LaserFOV[playername] and self.LaserFOV[playername].max then
              maxdist = self.LaserFOV[playername].max
            end
            local laselist={}
            for i=2,8 do
             local dist1 = (i*1000)-1000
             local dist2 = i*1000
             dist1 = dist1 == 1000 and 100 or dist1
             local text = string.format("%d-%dm",dist1,dist2)
             if dist2 == maxdist then
              text = text .. " (*)"
             end
             laselist[i] = MENU_GROUP_COMMAND:New(group,text,lasedist,self._SwitchLasingDist,self,client,group,playername,dist1,dist2)
            end
          end
          local targetmenu = MENU_GROUP:New(group,"Target Report",self.ClientMenus[playername])
          if canlase then
            local reportL = MENU_GROUP_COMMAND:New(group,"Laser Target",targetmenu,self._ReportLaserTargets,self,client,group,playername)
          end
          local reportV = MENU_GROUP_COMMAND:New(group,"Visual Targets",targetmenu,self._ReportVisualTargets,self,client,group,playername)
          if self.UseController then
            local text = string.format("Target Upload to %s",self.Controller.MenuName or self.Controller.Name)
            local upload = MENU_GROUP_COMMAND:New(group,text,targetmenu,self._UploadTargets,self,client,group,playername)
          end
          if canlase then
            local lasecodemenu = MENU_GROUP:New(group,"Set Laser Code",self.ClientMenus[playername])
            local codemenu = {}
            for _,_code in pairs(self.LaserCodes) do
              --self._SetClientLaserCode,self,client,group,playername)
              if _code == self.UnitLaserCodes[playername] then
                _code = tostring(_code).."(*)"
              end
              codemenu[playername.._code] = MENU_GROUP_COMMAND:New(group,tostring(_code),lasecodemenu,self._SetClientLaserCode,self,client,group,playername,_code)
            end 
          end   
        end
      end
    end
  end
  return self
end

--- [Internal] 
-- @param #PLAYERRECCE self
-- @param Core.Set#SET_UNIT targetset
-- @param Wrapper.Client#CLIENT client
-- @param #string playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:_CheckNewTargets(targetset,client,playername)
  self:T(self.lid.."_CheckNewTargets")
  local tempset = SET_UNIT:New()
  targetset:ForEach(
    function(unit)
      if unit and unit:IsAlive() then
        self:T("Report unit: "..unit:GetName())
        if not unit.PlayerRecceDetected then
          self:T("New unit: "..unit:GetName())
          unit.PlayerRecceDetected = {
            detected = true,
            recce = client,
            playername = playername,
            timestamp = timer.getTime()
          }
          --self:TargetDetected(unit,client,playername)
          tempset:Add(unit:GetName(),unit)
          if not self.TargetCache:HasUniqueID(unit:GetName()) then
            self.TargetCache:Push(unit,unit:GetName())
          end
        end
        if unit.PlayerRecceDetected and unit.PlayerRecceDetected.timestamp then
          local TNow = timer.getTime()
          if TNow-unit.PlayerRecceDetected.timestamp > self.TForget then
           unit.PlayerRecceDetected = {
              detected = true,
              recce = client,
              playername = playername,
              timestamp = timer.getTime()
              }
           if not self.TargetCache:HasUniqueID(unit:GetName()) then
            self.TargetCache:Push(unit,unit:GetName())
           end
           tempset:Add(unit:GetName(),unit)  
          end
        end
      end
    end
  )
  local targetsbyclock = {}
  for i=1,12 do
    targetsbyclock[i] = {}
  end
  tempset:ForEach(
    function (object)
      local obj=object -- Wrapper.Unit#UNIT
      local clock = self:_GetClockDirection(client,obj)
      table.insert(targetsbyclock[clock],obj)
    end
  )
  self:T("Known target Count: "..self.TargetCache:Count())
  if tempset:CountAlive() > 0 then
    self:TargetDetected(targetsbyclock,client,playername)
  end
  return self
end

--- [User] Set SRS TTS details - see @{Sound.SRS} for details
-- @param #PLAYERRECCE self
-- @param #number Frequency Frequency to be used. Can also be given as a table of multiple frequencies, e.g. 271 or {127,251}. There needs to be exactly the same number of modulations!
-- @param #number Modulation Modulation to be used. Can also be given as a table of multiple modulations, e.g. radio.modulation.AM or {radio.modulation.FM,radio.modulation.AM}. There needs to be exactly the same number of frequencies!
-- @param #string PathToSRS Defaults to "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio"
-- @param #string Gender (Optional) Defaults to "male"
-- @param #string Culture (Optional) Defaults to "en-US"
-- @param #number Port (Optional) Defaults to 5002
-- @param #string Voice (Optional) Use a specifc voice with the @{Sound.SRS#SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`.
-- Note that this must be installed on your windows system. Can also be Google voice types, if you are using Google TTS.
-- @param #number Volume (Optional) Volume - between 0.0 (silent) and 1.0 (loudest)
-- @param #string PathToGoogleKey (Optional) Path to your google key if you want to use google TTS
-- @param #string Backend (optional) Backend to be used, can be MSRS.Backend.SRSEXE or MSRS.Backend.GRPC
-- @return #PLAYERRECCE self
function PLAYERRECCE:SetSRS(Frequency,Modulation,PathToSRS,Gender,Culture,Port,Voice,Volume,PathToGoogleKey,Backend)
  self:T(self.lid.."SetSRS")
  self.PathToSRS = PathToSRS or MSRS.path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio" --
  self.Gender = Gender or MSRS.gender or "male" --
  self.Culture = Culture or MSRS.culture or "en-US" --
  self.Port = Port or MSRS.port or 5002 --
  self.Voice = Voice or MSRS.voice --
  self.PathToGoogleKey = PathToGoogleKey --
  self.Volume = Volume or 1.0 -- 
  self.UseSRS = true
  self.Frequency = Frequency or {127,251} --
  self.BCFrequency = self.Frequency
  self.Modulation = Modulation or {radio.modulation.FM,radio.modulation.AM} --
  self.BCModulation = self.Modulation
  -- set up SRS 
  self.SRS=MSRS:New(self.PathToSRS,self.Frequency,self.Modulation)
  self.SRS:SetCoalition(self.Coalition)
  self.SRS:SetLabel(self.MenuName or self.Name)
  self.SRS:SetGender(self.Gender)
  self.SRS:SetCulture(self.Culture)
  self.SRS:SetPort(self.Port)
  self.SRS:SetVolume(self.Volume)
  if Backend then
    self.SRS:SetBackend(Backend)
  end
  if self.PathToGoogleKey then
    self.SRS:SetProviderOptionsGoogle(self.PathToGoogleKey,self.PathToGoogleKey)
    self.SRS:SetProvider(MSRS.Provider.GOOGLE)
  end
     -- Pre-configured Google?
  if (not PathToGoogleKey) and self.SRS:GetProvider() == MSRS.Provider.GOOGLE then
    self.PathToGoogleKey = MSRS.poptions.gcloud.credentials
    self.Voice = Voice or MSRS.poptions.gcloud.voice
  end
  self.SRS:SetVoice(self.Voice)
  self.SRSQueue = MSRSQUEUE:New(self.MenuName or self.Name)
  self.SRSQueue:SetTransmitOnlyWithPlayers(self.TransmitOnlyWithPlayers)
  return self
end

--- [User] For SRS - Switch to only transmit if there are players on the server.
-- @param #PLAYERRECCE self
-- @param #boolean Switch If true, only send SRS if there are alive Players.
-- @return #PLAYERRECCE self
function PLAYERRECCE:SetTransmitOnlyWithPlayers(Switch)
  self.TransmitOnlyWithPlayers = Switch
  if self.SRSQueue then
    self.SRSQueue:SetTransmitOnlyWithPlayers(Switch)
  end
  return self
end

--- [User] Set the top menu name to a custom string.
-- @param #PLAYERRECCE self
-- @param #string Name The name to use as the top menu designation.
-- @return #PLAYERRECCE self
function PLAYERRECCE:SetMenuName(Name)
 self:T(self.lid.."SetMenuName: "..Name)
 self.MenuName = Name
 return self
end

--- [User] Set reporting to be BULLS only or BULLS plus playersettings based coordinate.
-- @param #PLAYERRECCE self
-- @param #boolean OnOff
-- @return #PLAYERRECCE self
function PLAYERRECCE:SetReportBullsOnly(OnOff)
 self:T(self.lid.."SetReportBullsOnly: "..tostring(OnOff))
 self.reporttostringbullsonly = OnOff
 return self
end

--- [User] Enable smoking of own position
-- @param #PLAYERRECCE self
-- @return #PLAYERRECCE self
function PLAYERRECCE:EnableSmokeOwnPosition()
  self:T(self.lid.."EnableSmokeOwnPosition")
  self.smokeownposition = true
  return self
end

--- [User] Enable auto lasing for the Kiowa OH-58D.
-- @param #PLAYERRECCE self
-- @return #PLAYERRECCE self
function PLAYERRECCE:EnableKiowaAutolase()
  self:T(self.lid.."EnableKiowaAutolase")
  self.CanLase.OH58D = true
  return self
end

--- [User] Disable smoking of own position
-- @param #PLAYERRECCE self
-- @return #PLAYERRECCE 
function PLAYERRECCE:DisableSmokeOwnPosition()
  self:T(self.lid.."DisableSmokeOwnPosition")
  self.smokeownposition = false
  return self
end

--- [User] Enable smoking of average target positions, instead of all targets visible. Loses smoke per threatlevel -- each is med threat. Default is - smoke all positions.
-- @param #PLAYERRECCE self
-- @return #PLAYERRECCE self
function PLAYERRECCE:EnableSmokeAverageTargetPosition()
  self:T(self.lid.."ENableSmokeOwnPosition")
  self.smokeaveragetargetpos = true
  return self
end

--- [User] Disable smoking of average target positions, instead of all targets visible. Default is - smoke all positions.
-- @param #PLAYERRECCE self
-- @return #PLAYERRECCE 
function PLAYERRECCE:DisableSmokeAverageTargetPosition()
  self:T(self.lid.."DisableSmokeAverageTargetPosition")
  self.smokeaveragetargetpos = false
  return self
end

--- [Internal] Get text for text-to-speech.
-- Numbers are spaced out, e.g. "Heading 180" becomes "Heading 1 8 0 ".
-- @param #PLAYERRECCE self
-- @param #string text Original text.
-- @return #string Spoken text.
function PLAYERRECCE:_GetTextForSpeech(text)
  
  -- Space out numbers.
  text=string.gsub(text,"%d","%1 ")
  -- get rid of leading or trailing spaces
  text=string.gsub(text,"^%s*","")
  text=string.gsub(text,"%s*$","")
  text=string.gsub(text,"0","zero")
  text=string.gsub(text,"9","niner")
  text=string.gsub(text,"  "," ")
  
  return text
end

------------------------------------------------------------------------------------------
-- TODO: FSM Functions
------------------------------------------------------------------------------------------

--- [Internal] Status Loop
-- @param #PLAYERRECCE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #PLAYERRECCE self
function PLAYERRECCE:onafterStatus(From, Event, To)
  self:T({From, Event, To})
  
  if not self.timestamp then
    self.timestamp = timer.getTime()
  else
    local tNow = timer.getTime()
    if tNow - self.timestamp >= 60 then
      self:_CleanupTargetCache()
      self.timestamp = timer.getTime()
    end
  end
  
  self:_BuildMenus()
  
  self.PlayerSet:ForEachClient(
    function(Client)
        local client = Client -- Wrapper.Client#CLIENT
        local playername = client:GetPlayerName()
        local cameraison = self:_CameraOn(client,playername)
        if client and client:IsAlive() and self.OnStation[playername] then
          ---
         local targetset, targetcount, tzone = nil,0,nil
         local laserset, lzone = nil,nil
         local vistargetset, vistargetcount, viszone = nil,0,nil
          -- targets on camera
          if cameraison then
            targetset, targetcount, tzone = self:_GetTargetSet(client,true)
            if targetset then
              if self.ViewZone[playername] then
                self.ViewZone[playername]:UndrawZone()
              end
              if self.debug and tzone then
                self.ViewZone[playername]=tzone:DrawZone(self.Coalition,{0,0,1},nil,nil,nil,1)
              end
            end
            self:T({targetcount=targetcount})
          end
          -- lase targets on camera
          if self.AutoLase[playername] and cameraison then
            laserset, targetcount, lzone = self:_GetTargetSet(client,true,true)
            if targetcount > 0 or self.LaserTarget[playername] then
              if self.CanLase[client:GetTypeName()] then
                -- DONE move to lase at will
                self:_LaseTarget(client,laserset)
              end
            end
            if lzone then
              if self.ViewZoneLaser[playername] then
                self.ViewZoneLaser[playername]:UndrawZone()
              end
              if self.debug and tzone then
                self.ViewZoneLaser[playername]=lzone:DrawZone(self.Coalition,{0,1,0},nil,nil,nil,1)
              end
            end
            self:T({lasercount=targetcount})
          end
          -- visual targets
          vistargetset, vistargetcount, viszone = self:_GetTargetSet(client,false)
          if vistargetset then
            if self.ViewZoneVisual[playername] then
              self.ViewZoneVisual[playername]:UndrawZone()
            end
            if self.debug and viszone then
              self.ViewZoneVisual[playername]=viszone:DrawZone(self.Coalition,{1,0,0},nil,nil,nil,3)
            end
          end
          self:T({visualtargetcount=vistargetcount})
          if targetset then
            vistargetset:AddSet(targetset)
          end
          if laserset then
            vistargetset:AddSet(laserset)
          end
          if not cameraison and self.debug then
            if self.ViewZoneLaser[playername] then
                self.ViewZoneLaser[playername]:UndrawZone()
              end
            if self.ViewZone[playername] then
              self.ViewZone[playername]:UndrawZone()
            end
          end
          self:_CheckNewTargets(vistargetset,client,playername)
        end
    end
  )
   
  self:__Status(-10)
  return self
end

--- [Internal] Recce on station
-- @param #PLAYERRECCE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Wrapper.Client#CLIENT Client
-- @param #string Playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:onafterRecceOnStation(From, Event, To, Client, Playername)
  self:T({From, Event, To})
  local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs)
  local coord = Client:GetCoordinate()
  local coordtext = coord:ToStringBULLS(self.Coalition)
  if self.ReferencePoint then
    local Settings = Client and _DATABASE:GetPlayerSettings(Playername) or _SETTINGS -- Core.Settings#SETTINGS
    coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings)
  end
  local text1 = "Party time!"
  local text2 = string.format("All stations, FACA %s on station\nat %s!",callsign, coordtext)
  local text2tts = string.format(" All stations, FACA %s on station at %s!",callsign, coordtext)
  text2tts = self:_GetTextForSpeech(text2tts)
  if self.debug then
  self:T(text2.."\n"..text2tts)
  end
  if self.UseSRS then
    local grp = Client:GetGroup()
    local coord = grp:GetCoordinate()
    if coord then
      self.SRS:SetCoordinate(coord)
    end
    self.SRSQueue:NewTransmission(text1,nil,self.SRS,nil,2)
    self.SRSQueue:NewTransmission(text2tts,nil,self.SRS,nil,3)
    MESSAGE:New(text2,10,self.Name or "FACA"):ToCoalition(self.Coalition)
  else
    MESSAGE:New(text1,10,self.Name or "FACA"):ToClient(Client)
    MESSAGE:New(text2,10,self.Name or "FACA"):ToCoalition(self.Coalition)
  end
  return self
end

--- [Internal] Recce off station
-- @param #PLAYERRECCE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Wrapper.Client#CLIENT Client
-- @param #string Playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:onafterRecceOffStation(From, Event, To, Client, Playername)
  self:T({From, Event, To})
  local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs)
  local coord = Client:GetCoordinate()
  local coordtext = coord:ToStringBULLS(self.Coalition)
  if self.ReferencePoint then
    local Settings = Client and _DATABASE:GetPlayerSettings(Playername) or _SETTINGS -- Core.Settings#SETTINGS
    coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings)
  end
  local text = string.format("All stations, FACA %s leaving station\nat %s, good bye!",callsign, coordtext)
  local texttts = string.format("All stations, FACA %s leaving station at %s, good bye!",callsign, coordtext)
  texttts = self:_GetTextForSpeech(texttts)
  if self.debug then
    self:T(text.."\n"..texttts)
  end
  local text1 = "Going home!"
  if self.UseSRS then
    local grp = Client:GetGroup()
    local coord = grp:GetCoordinate()
    if coord then
      self.SRS:SetCoordinate(coord)
    end
    self.SRSQueue:NewTransmission(text1,nil,self.SRS,nil,2)
    self.SRSQueue:NewTransmission(texttts,nil,self.SRS,nil,3)
    MESSAGE:New(text,10,self.Name or "FACA"):ToCoalition(self.Coalition)
  else
    MESSAGE:New(text,10,self.Name or "FACA"):ToCoalition(self.Coalition)
  end
  return self
end

--- [Internal] Target Detected
-- @param #PLAYERRECCE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #table Targetsbyclock. #table with index 1..12 containing a #table of Wrapper.Unit#UNIT objects each.
-- @param Wrapper.Client#CLIENT Client
-- @param #string Playername
-- @return #PLAYERRECCE self
function PLAYERRECCE:onafterTargetDetected(From, Event, To, Targetsbyclock, Client, Playername)
  self:T({From, Event, To})

  local dunits = "meters"
  local Settings = Client and _DATABASE:GetPlayerSettings(Playername) or _SETTINGS -- Core.Settings#SETTINGS
  local clientcoord = Client:GetCoordinate()
 
  for i=1,12 do
    local targets = Targetsbyclock[i] --#table
    local targetno = #targets
    if targetno == 1 then
      -- only one
      local targetdistance = clientcoord:Get2DDistance(targets[1]:GetCoordinate()) or 100
      local Threatlvl = targets[1]:GetThreatLevel()
      local ThreatTxt = "Low"
      if Threatlvl >=7  then
        ThreatTxt = "Medium"
      elseif Threatlvl >=3  then
        ThreatTxt = "High"
      end
      if Settings:IsMetric() then
       targetdistance = UTILS.Round(targetdistance,-2)
       if targetdistance >= 1000 then
        targetdistance = UTILS.Round(targetdistance/1000,0)
        dunits = "kilometer"
       end
      else
       if UTILS.MetersToNM(targetdistance) >=1 then
        targetdistance = UTILS.Round(UTILS.MetersToNM(targetdistance),0)
        dunits = "miles"
       else
         targetdistance = UTILS.Round(UTILS.MetersToFeet(targetdistance),-2)
         dunits = "feet"
       end
      end
      local text = string.format("Target! %s! %s o\'clock, %d %s!", ThreatTxt,i, targetdistance, dunits)
      local ttstext = string.format("Target! %s! %s oh clock, %d %s!", ThreatTxt, i, targetdistance, dunits)
      if self.UseSRS then
        local grp = Client:GetGroup()
        if clientcoord then
          self.SRS:SetCoordinate(clientcoord)
        end
        self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10)
      else
        MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client)
      end
    elseif targetno > 1 then
      -- multiple
      local function GetNearest(TTable)
        local distance = 10000000
        for _,_unit in pairs(TTable) do
          local dist = clientcoord:Get2DDistance(_unit:GetCoordinate()) or 100
          if dist < distance then
            distance = dist
          end
        end
        return distance
      end
      local targetdistance = GetNearest(targets)
      if Settings:IsMetric() then
       targetdistance = UTILS.Round(targetdistance,-2)
        if targetdistance >= 1000 then
        targetdistance = UTILS.Round(targetdistance/1000,0)
        dunits = "kilometer"
       end
      else
       if UTILS.MetersToNM(targetdistance) >=1 then
        targetdistance = UTILS.Round(UTILS.MetersToNM(targetdistance),0)
        dunits = "miles"
       else
         targetdistance = UTILS.Round(UTILS.MetersToFeet(targetdistance),-2)
         dunits = "feet"
       end
      end
      local text = string.format(" %d targets! %s o\'clock, %d %s!", targetno, i, targetdistance, dunits)
      local ttstext = string.format("%d targets! %s oh clock, %d %s!", targetno, i, targetdistance, dunits)
      if self.UseSRS then
        local grp = Client:GetGroup()
        local coord = grp:GetCoordinate()
        if coord then
          self.SRS:SetCoordinate(coord)
        end
        self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10)
      else
        MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client)
      end
    end
  end
  return self
end

--- [Internal] Targets Illuminated
-- @param #PLAYERRECCE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Wrapper.Client#CLIENT Client
-- @param #string Playername
-- @param Core.Set#SET_UNIT TargetSet
-- @return #PLAYERRECCE self
function PLAYERRECCE:onafterIllumination(From, Event, To, Client, Playername, TargetSet)
  self:T({From, Event, To})
  local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs)
  local coord = Client:GetCoordinate()
  local coordtext = coord:ToStringBULLS(self.Coalition)
  if self.AttackSet then
    for _,_client in pairs(self.AttackSet.Set) do
      local client = _client --Wrapper.Client#CLIENT
      if client and client:IsAlive() then
        local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName())  or _SETTINGS
        local coordtext = coord:ToStringA2G(client,Settings)
        if self.ReferencePoint then
          coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings)
        end
        local text = string.format("All stations, %s fired illumination\nat %s!",callsign, coordtext)
        MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client)
      end
    end
  end
  local text = "Sunshine!"
  local ttstext = "Sunshine!"
  if self.UseSRS then
    local grp = Client:GetGroup()
    local coord = grp:GetCoordinate()
    if coord then
      self.SRS:SetCoordinate(coord)
    end
    self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10)
  else
    MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client)
  end
  return self
end

--- [Internal] Targets Smoked
-- @param #PLAYERRECCE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Wrapper.Client#CLIENT Client
-- @param #string Playername
-- @param Core.Set#SET_UNIT TargetSet
-- @return #PLAYERRECCE self
function PLAYERRECCE:onafterTargetsSmoked(From, Event, To, Client, Playername, TargetSet)
  self:T({From, Event, To})
  local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs)
  local coord = Client:GetCoordinate()
  local coordtext = coord:ToStringBULLS(self.Coalition)
  if self.AttackSet then
    for _,_client in pairs(self.AttackSet.Set) do
      local client = _client --Wrapper.Client#CLIENT
      if client and client:IsAlive() then
        local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName())  or _SETTINGS
        local coordtext = coord:ToStringA2G(client,Settings)
        if self.ReferencePoint then
          coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings)
        end
        local text = string.format("All stations, %s smoked targets\nat %s!",callsign, coordtext)
        MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client)
      end
    end
  end
  local text = "Smoke on!"
  local ttstext = "Smoke and Mirrors!"
  if self.UseSRS then
    local grp = Client:GetGroup()
    local coord = grp:GetCoordinate()
    if coord then
      self.SRS:SetCoordinate(coord)
    end
    self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10)
  else
    MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client)
  end
  return self
end

--- [Internal] Targets Flared
-- @param #PLAYERRECCE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Wrapper.Client#CLIENT Client
-- @param #string Playername
-- @param Core.Set#SET_UNIT TargetSet
-- @return #PLAYERRECCE self
function PLAYERRECCE:onafterTargetsFlared(From, Event, To, Client, Playername, TargetSet)
  self:T({From, Event, To})
  local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs)
  local coord = Client:GetCoordinate()
  local coordtext = coord:ToStringBULLS(self.Coalition)
  if self.AttackSet then
    for _,_client in pairs(self.AttackSet.Set) do
      local client = _client --Wrapper.Client#CLIENT
      if client and client:IsAlive() then
        local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName())  or _SETTINGS
        if self.ReferencePoint then
          coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings)
        end
        local coordtext = coord:ToStringA2G(client,Settings)
        local text = string.format("All stations, %s flared targets\nat %s!",callsign, coordtext)
        MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client)
      end
    end
  end
  local text = "Fireworks!"
  local ttstext = "Fire works!"
  if self.UseSRS then
    local grp = Client:GetGroup()
    local coord = grp:GetCoordinate()
    if coord then
      self.SRS:SetCoordinate(coord)
    end
    self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10)
  else
    MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client)
  end
  return self
end
 
--- [Internal] Target lasing
-- @param #PLAYERRECCE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Wrapper.Client#CLIENT Client
-- @param Wrapper.Unit#UNIT Target
-- @param #number Lasercode
-- @param #number Lasingtime
-- @return #PLAYERRECCE self
function PLAYERRECCE:onafterTargetLasing(From, Event, To, Client, Target, Lasercode, Lasingtime)
  self:T({From, Event, To})
  local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs)
  local Settings = ( Client and _DATABASE:GetPlayerSettings( Client:GetPlayerName() ) ) or _SETTINGS
  local coord = Client:GetCoordinate()
  local coordtext = coord:ToStringBULLS(self.Coalition,Settings)
  if self.ReferencePoint then
    coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings)
  end
  local targettype = Target:GetTypeName()
  if self.AttackSet then
    for _,_client in pairs(self.AttackSet.Set) do
      local client = _client --Wrapper.Client#CLIENT
      if client and client:IsAlive() then
        local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName())  or _SETTINGS
        if self.ReferencePoint then
          coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings)
        end
        local coordtext = coord:ToStringA2G(client,Settings)
        local text = string.format("All stations, %s lasing %s\nat %s!\nCode %d, Duration %d plus seconds!",callsign, targettype, coordtext, Lasercode, Lasingtime)
        MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client)
      end
    end
  end
  local text = "Lasing!"
  local ttstext = "Laser on!"
  if self.UseSRS then
    local grp = Client:GetGroup()
    local coord = grp:GetCoordinate()
    if coord then
      self.SRS:SetCoordinate(coord)
    end
    self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10)
  else
    MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client)
  end
  return self
end

--- [Internal] Lased target destroyed
-- @param #PLAYERRECCE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Wrapper.Client#CLIENT Client
-- @param Wrapper.Unit#UNIT Target
-- @return #PLAYERRECCE self
function PLAYERRECCE:onafterShack(From, Event, To, Client, Target)
  self:T({From, Event, To})
  local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs)
  local Settings = ( Client and _DATABASE:GetPlayerSettings( Client:GetPlayerName() ) ) or _SETTINGS
  local coord = Client:GetCoordinate()
  local coordtext = coord:ToStringBULLS(self.Coalition,Settings)
  if self.ReferencePoint then
    coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings)
  end
  local targettype = "target"
  if self.AttackSet then
    for _,_client in pairs(self.AttackSet.Set) do
      local client = _client --Wrapper.Client#CLIENT
      if client and client:IsAlive() then
        local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName())  or _SETTINGS
        if self.ReferencePoint then
          coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings)
        end
        local coordtext = coord:ToStringA2G(client,Settings)
        local text = string.format("All stations, %s good hit on %s\nat %s!",callsign, targettype, coordtext)
        MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client)
      end
    end
  end
  local text = "Shack!"
  local ttstext = "Shack!"
  if self.UseSRS then
    local grp = Client:GetGroup()
    local coord = grp:GetCoordinate()
    if coord then
      self.SRS:SetCoordinate(coord)
    end
    self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1)
  else
    MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client)
  end
  return self
end

--- [Internal] Laser lost LOS
-- @param #PLAYERRECCE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Wrapper.Client#CLIENT Client
-- @param Wrapper.Unit#UNIT Target
-- @return #PLAYERRECCE self
function PLAYERRECCE:onafterTargetLOSLost(From, Event, To, Client, Target)
  self:T({From, Event, To})
  local callsign = Client:GetGroup():GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations,self.CallsignCustomFunc,self.CallsignCustomArgs)
  local Settings = ( Client and _DATABASE:GetPlayerSettings( Client:GetPlayerName() ) ) or _SETTINGS
  local coord = Client:GetCoordinate()
  local coordtext = coord:ToStringBULLS(self.Coalition,Settings)
  if self.ReferencePoint then
    coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,Client,Settings)
  end
  local targettype = "target" --Target:GetTypeName()
  if self.AttackSet then
    for _,_client in pairs(self.AttackSet.Set) do
      local client = _client --Wrapper.Client#CLIENT
      if client and client:IsAlive() then
        local Settings = client and _DATABASE:GetPlayerSettings(client:GetPlayerName())  or _SETTINGS
        if self.ReferencePoint then
          coordtext = coord:ToStringFromRPShort(self.ReferencePoint,self.RPName,client,Settings)
        end
        local coordtext = coord:ToStringA2G(client,Settings)
        local text = string.format("All stations, %s lost sight of %s\nat %s!",callsign, targettype, coordtext)
        MESSAGE:New(text,15,self.Name or "FACA"):ToClient(client)
      end
    end
  end
  local text = "Lost LOS!"
  local ttstext = "Lost L O S!"
  if self.UseSRS then
    local grp = Client:GetGroup()
    local coord = grp:GetCoordinate()
    if coord then
      self.SRS:SetCoordinate(coord)
    end
    self.SRSQueue:NewTransmission(ttstext,nil,self.SRS,nil,1,{grp},text,10)
  else
    MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client)
  end
  return self
end

--- [Internal] Target report
-- @param #PLAYERRECCE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Wrapper.Client#CLIENT Client
-- @param Core.Set#SET_UNIT TargetSet
-- @param Wrapper.Unit#UNIT Target
-- @param #string Text
-- @return #PLAYERRECCE self
function PLAYERRECCE:onafterTargetReport(From, Event, To, Client, TargetSet, Target, Text)
  self:T({From, Event, To})
  MESSAGE:New(Text,45,self.Name or "FACA"):ToClient(Client)
  if self.AttackSet then
    -- send message to AttackSet
    for _,_client in pairs(self.AttackSet.Set) do
      local client = _client -- Wrapper.Client#CLIENT
      if client and client:IsAlive() and client ~= Client then
        MESSAGE:New(Text,45,self.Name or "FACA"):ToClient(client)
      end
    end
  end
  return self
end

--- [Internal] Target data upload
-- @param #PLAYERRECCE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Wrapper.Client#CLIENT Client Client sending the report
-- @param #string Playername Player name
-- @param Core.Set#SET_UNIT TargetSet Set of targets
-- @return #PLAYERRECCE self
function PLAYERRECCE:onafterTargetReportSent(From, Event, To, Client, Playername, TargetSet)
  self:T({From, Event, To})
  local text = "Upload completed!"
  if self.UseSRS then
    local grp = Client:GetGroup()
    local coord = grp:GetCoordinate()
    if coord then
      self.SRS:SetCoordinate(coord)
    end
    self.SRSQueue:NewTransmission(text,nil,self.SRS,nil,1,{grp},text,10)
  else
    MESSAGE:New(text,10,self.Name or "FACA"):ToClient(Client)
  end
  return self
end

--- [Internal] Stop
-- @param #PLAYERRECCE self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #PLAYERRECCE self
function PLAYERRECCE:onafterStop(From, Event, To)
  self:I({From, Event, To})
  -- Player Events
  self:UnHandleEvent(EVENTS.PlayerLeaveUnit)
  self:UnHandleEvent(EVENTS.Ejection)
  self:UnHandleEvent(EVENTS.Crash)
  self:UnHandleEvent(EVENTS.PilotDead)
  self:UnHandleEvent(EVENTS.PlayerEnterAircraft)
  return self
end

------------------------------------------------------------------------------------------
-- TODO: END PLAYERRECCE
------------------------------------------------------------------------------------------
--- **Ops** - Airwing Squadron.
--
-- **Main Features:**
--
--    * Set parameters like livery, skill valid for all squadron members.
--    * Define modex and callsigns.
--    * Define mission types, this squadron can perform (see Ops.Auftrag#AUFTRAG).
--    * Pause/unpause squadron operations.
--
-- ===
--
-- ### Author: **funkyfranky**
-- @module Ops.Squadron
-- @image OPS_Squadron.png


--- SQUADRON class.
-- @type SQUADRON
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field #string name Name of the squadron.
-- @field #string templatename Name of the template group.
-- @field #string aircrafttype Type of the airframe the squadron is using.
-- @field Wrapper.Group#GROUP templategroup Template group.
-- @field #number ngrouping User defined number of units in the asset group.
-- @field #table assets Squadron assets.
-- @field #table missiontypes Capabilities (mission types and performances) of the squadron.
-- @field #number fuellow Low fuel threshold.
-- @field #boolean fuellowRefuel If `true`, flight tries to refuel at the nearest tanker.
-- @field #number maintenancetime Time in seconds needed for maintenance of a returned flight.
-- @field #number repairtime Time in seconds for each
-- @field #string livery Livery of the squadron.
-- @field #number skill Skill of squadron members.
-- @field #number modex Modex.
-- @field #number modexcounter Counter to incease modex number for assets.
-- @field #string callsignName Callsign name.
-- @field #number callsigncounter Counter to increase callsign names for new assets.
-- @field #number Ngroups Number of asset flight groups this squadron has. 
-- @field #number engageRange Mission range in meters.
-- @field #string attribute Generalized attribute of the squadron template group.
-- @field #number tankerSystem For tanker squads, the refuel system used (boom=0 or probpe=1). Default nil.
-- @field #number refuelSystem For refuelable squads, the refuel system used (boom=0 or probe=1). Default nil.
-- @field #table tacanChannel List of TACAN channels available to the squadron.
-- @field #number radioFreq Radio frequency in MHz the squad uses.
-- @field #number radioModu Radio modulation the squad uses.
-- @field #string takeoffType Take of type.
-- @field #table parkingIDs Parking IDs for this squadron.
-- @field #boolean despawnAfterLanding Aircraft are despawned after landing.
-- @field #boolean despawnAfterHolding Aircraft are despawned after holding.
-- @extends Ops.Cohort#COHORT

--- *It is unbelievable what a squadron of twelve aircraft did to tip the balance* -- Adolf Galland
--
-- ===
--
-- # The SQUADRON Concept
-- 
-- A SQUADRON is essential part of an @{Ops.Airwing#AIRWING} and consists of **one** type of aircraft.
-- 
-- 
--
-- @field #SQUADRON
SQUADRON = {
  ClassName      = "SQUADRON",
  verbose        =     0,
  modex          =   nil,
  modexcounter   =     0,
  callsignName   =   nil,
  callsigncounter=    11,
  tankerSystem   =   nil,
  refuelSystem   =   nil,
}

--- SQUADRON class version.
-- @field #string version
SQUADRON.version="0.8.1"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- DONE: Parking spots for squadrons?
-- DONE: Engage radius.
-- DONE: Modex.
-- DONE: Call signs.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new SQUADRON object and start the FSM.
-- @param #SQUADRON self
-- @param #string TemplateGroupName Name of the template group.
-- @param #number Ngroups Number of asset groups of this squadron. Default 3.
-- @param #string SquadronName Name of the squadron, e.g. "VFA-37". Must be **unique**!
-- @return #SQUADRON self
function SQUADRON:New(TemplateGroupName, Ngroups, SquadronName)

  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, COHORT:New(TemplateGroupName, Ngroups, SquadronName)) -- #SQUADRON

  -- Everyone can ORBIT.
  self:AddMissionCapability(AUFTRAG.Type.ORBIT)
  
  -- Is air.
  self.isAir=true

  -- Refueling system.
  self.refuelSystem=select(2, self.templategroup:GetUnit(1):IsRefuelable())
  self.tankerSystem=select(2, self.templategroup:GetUnit(1):IsTanker())

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  -- See COHORT class

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set number of units in groups.
-- @param #SQUADRON self
-- @param #number nunits Number of units. Must be >=1 and <=4. Default 2.
-- @return #SQUADRON self
function SQUADRON:SetGrouping(nunits)
  self.ngrouping=nunits or 2
  if self.ngrouping<1 then self.ngrouping=1 end
  if self.ngrouping>4 then self.ngrouping=4 end
  return self
end

--- Set valid parking spot IDs. Assets of this squad are only allowed to be spawned at these parking spots. **Note** that the IDs are different from the ones displayed in the mission editor!
-- @param #SQUADRON self
-- @param #table ParkingIDs Table of parking ID numbers or a single `#number`.
-- @return #SQUADRON self
function SQUADRON:SetParkingIDs(ParkingIDs)
  if type(ParkingIDs)~="table" then
    ParkingIDs={ParkingIDs}
  end
  self.parkingIDs=ParkingIDs
  return self
end


--- Set takeoff type. All assets of this squadron will be spawned with cold (default) or hot engines.
-- Spawning on runways is not supported.
-- @param #SQUADRON self
-- @param #string TakeoffType Take off type: "Cold" (default) or "Hot" with engines on or "Air" for spawning in air.
-- @return #SQUADRON self
function SQUADRON:SetTakeoffType(TakeoffType)
  TakeoffType=TakeoffType or "Cold"
  if TakeoffType:lower()=="hot" then
    self.takeoffType=COORDINATE.WaypointType.TakeOffParkingHot
  elseif TakeoffType:lower()=="cold" then
    self.takeoffType=COORDINATE.WaypointType.TakeOffParking
  elseif TakeoffType:lower()=="air" then
    self.takeoffType=COORDINATE.WaypointType.TurningPoint    
  else
    self.takeoffType=COORDINATE.WaypointType.TakeOffParking
  end
  return self
end

--- Set takeoff type cold (default). All assets of this squadron will be spawned with engines off (cold).
-- @param #SQUADRON self
-- @return #SQUADRON self
function SQUADRON:SetTakeoffCold()
  self:SetTakeoffType("Cold")
  return self
end

--- Set takeoff type hot. All assets of this squadron will be spawned with engines on (hot).
-- @param #SQUADRON self
-- @return #SQUADRON self
function SQUADRON:SetTakeoffHot()
  self:SetTakeoffType("Hot")
  return self
end

--- Set takeoff type air. All assets of this squadron will be spawned in air above the airbase.
-- @param #SQUADRON self
-- @return #SQUADRON self
function SQUADRON:SetTakeoffAir()
  self:SetTakeoffType("Air")
  return self
end

--- Set despawn after landing. Aircraft will be despawned after the landing event.
-- Can help to avoid DCS AI taxiing issues.
-- @param #SQUADRON self
-- @param #boolean Switch If `true` (default), activate despawn after landing.
-- @return #SQUADRON self
function SQUADRON:SetDespawnAfterLanding(Switch)
  if Switch then
    self.despawnAfterLanding=Switch
  else
    self.despawnAfterLanding=true
  end
  return self
end

--- Set despawn after holding. Aircraft will be despawned when they arrive at their holding position at the airbase.
-- Can help to avoid DCS AI taxiing issues.
-- @param #SQUADRON self
-- @param #boolean Switch If `true` (default), activate despawn after holding.
-- @return #SQUADRON self
function SQUADRON:SetDespawnAfterHolding(Switch)
  if Switch then
    self.despawnAfterHolding=Switch
  else
    self.despawnAfterHolding=true
  end
  return self
end


--- Set low fuel threshold.
-- @param #SQUADRON self
-- @param #number LowFuel Low fuel threshold in percent. Default 25.
-- @return #SQUADRON self
function SQUADRON:SetFuelLowThreshold(LowFuel)
  self.fuellow=LowFuel or 25
  return self
end

--- Set if low fuel threshold is reached, flight tries to refuel at the neares tanker.
-- @param #SQUADRON self
-- @param #boolean switch If true or nil, flight goes for refuelling. If false, turn this off.
-- @return #SQUADRON self
function SQUADRON:SetFuelLowRefuel(switch)
  if switch==false then
    self.fuellowRefuel=false
  else
    self.fuellowRefuel=true
  end
  return self
end

--- Set airwing.
-- @param #SQUADRON self
-- @param Ops.Airwing#AIRWING Airwing The airwing.
-- @return #SQUADRON self
function SQUADRON:SetAirwing(Airwing)
  self.legion=Airwing
  return self
end

--- Get airwing.
-- @param #SQUADRON self
-- @return Ops.Airwing#AIRWING The airwing.
function SQUADRON:GetAirwing(Airwing)
  return self.legion
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start & Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers.
-- @param #SQUADRON self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function SQUADRON:onafterStart(From, Event, To)

  -- Short info.
  local text=string.format("Starting SQUADRON", self.name)
  self:T(self.lid..text)

  -- Start the status monitoring.
  self:__Status(-1)
end

--- On after "Status" event.
-- @param #SQUADRON self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function SQUADRON:onafterStatus(From, Event, To)

  if self.verbose>=1 then

    -- FSM state.
    local fsmstate=self:GetState()
  
    local callsign=self.callsignName and UTILS.GetCallsignName(self.callsignName) or "N/A"
    local modex=self.modex and self.modex or -1
    local skill=self.skill and tostring(self.skill) or "N/A"
    
    local NassetsTot=#self.assets
    local NassetsInS=self:CountAssets(true)
    local NassetsQP=0 ; local NassetsP=0 ; local NassetsQ=0  
    if self.legion then
      NassetsQP, NassetsP, NassetsQ=self.legion:CountAssetsOnMission(nil, self)
    end
    
    -- Short info.
    local text=string.format("%s [Type=%s, Call=%s, Modex=%d, Skill=%s]: Assets Total=%d, Stock=%d, Mission=%d [Active=%d, Queue=%d]", 
    fsmstate, self.aircrafttype, callsign, modex, skill, NassetsTot, NassetsInS, NassetsQP, NassetsP, NassetsQ)
    self:I(self.lid..text)
    
    -- Check if group has detected any units.
    self:_CheckAssetStatus()
    
  end  
  
  if not self:IsStopped() then
    self:__Status(-60)
  end
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- **Ops** - Target.
--
-- **Main Features:**
--
--    * Manages target, number alive, life points, damage etc.
--    * Events when targets are damaged or destroyed
--    * Various target objects: UNIT, GROUP, STATIC, SCENERY, AIRBASE, COORDINATE, ZONE, SET_GROUP, SET_UNIT, SET_STATIC, SET_SCENERY, SET_ZONE
--
-- ===
--
-- ### Author: **funkyfranky**
-- ### Additions: **applevangelist**
-- 
-- @module Ops.Target
-- @image OPS_Target.png


--- TARGET class.
-- @type TARGET
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #number uid Unique ID of the target.
-- @field #string lid Class id string for output to DCS log file.
-- @field #table targets Table of target objects.
-- @field #number targetcounter Running number to generate target object IDs.
-- @field #number life Total life points on last status update.
-- @field #number life0 Total life points of completely healthy targets.
-- @field #number threatlevel0 Initial threat level.
-- @field #number category Target category (Ground, Air, Sea).
-- @field #number N0 Number of initial target elements/units.
-- @field #number Ntargets0 Number of initial target objects.
-- @field #number Ndestroyed Number of target elements/units that were destroyed.
-- @field #number Ndead Number of target elements/units that are dead (destroyed or despawned).
-- @field #table elements Table of target elements/units.
-- @field #table casualties Table of dead element names.
-- @field #number prio Priority.
-- @field #number importance Importance.
-- @field Ops.Auftrag#AUFTRAG mission Mission attached to this target.
-- @field Ops.Intel#INTEL.Contact contact Contact attached to this target.
-- @field #boolean isDestroyed If true, target objects were destroyed.
-- @field #table resources Resource list.
-- @field #table conditionStart Start condition functions.
-- @field Ops.Operation#OPERATION operation Operation this target is part of.
-- @extends Core.Fsm#FSM

--- **It is far more important to be able to hit the target than it is to haggle over who makes a weapon or who pulls a trigger** -- Dwight D Eisenhower
--
-- ===
--
-- # The TARGET Concept
-- 
-- Define a target of your mission and monitor its status. Events are triggered when the target is damaged or destroyed.
-- 
-- A target can consist of one or multiple "objects".
--
--
-- @field #TARGET
TARGET = {
  ClassName      = "TARGET",
  verbose        =     0,
  lid            =   nil,
  targets        =    {},
  targetcounter  =     0,
  life           =     0,
  life0          =     0,
  N0             =     0,
  Ntargets0      =     0,
  Ndestroyed     =     0,
  Ndead          =     0,
  elements       =    {},
  casualties     =    {},
  threatlevel0   =     0,
  conditionStart =    {},
  TStatus        =    30,
}


--- Type.
-- @type TARGET.ObjectType
-- @field #string GROUP Target is a GROUP object.
-- @field #string UNIT Target is a UNIT object.
-- @field #string STATIC Target is a STATIC object.
-- @field #string SCENERY Target is a SCENERY object.
-- @field #string COORDINATE Target is a COORDINATE.
-- @field #string AIRBASE Target is an AIRBASE.
-- @field #string ZONE Target is a ZONE object.
-- @field #string OPSZONE Target is an OPSZONE object.
TARGET.ObjectType={
  GROUP="Group",
  UNIT="Unit",
  STATIC="Static",
  SCENERY="Scenery",
  COORDINATE="Coordinate",
  AIRBASE="Airbase",
  ZONE="Zone",
  OPSZONE="OpsZone"
}


--- Category.
-- @type TARGET.Category
-- @field #string AIRCRAFT 
-- @field #string GROUND
-- @field #string NAVAL
-- @field #string AIRBASE
-- @field #string COORDINATE
-- @field #string ZONE
TARGET.Category={
  AIRCRAFT="Aircraft",
  GROUND="Ground",
  NAVAL="Naval",
  AIRBASE="Airbase",
  COORDINATE="Coordinate",
  ZONE="Zone",
}

--- Object status.
-- @type TARGET.ObjectStatus
-- @field #string ALIVE Object is alive.
-- @field #string DEAD Object is dead.
-- @field #string DAMAGED Object is damaged.
TARGET.ObjectStatus={
  ALIVE="Alive",
  DEAD="Dead",
  DAMAGED="Damaged",
}

--- Resource.
-- @type TARGET.Resource
-- @field #string MissionType Mission type, e.g. `AUFTRAG.Type.BAI`.
-- @field #number Nmin Min number of assets.
-- @field #number Nmax Max number of assets.
-- @field #table Attributes Generalized attribute, e.g. `{GROUP.Attribute.GROUND_INFANTRY}`.
-- @field #table Properties Properties ([DCS attributes](https://wiki.hoggitworld.com/view/DCS_enum_attributes)), e.g. `"Attack helicopters"` or `"Mobile AAA"`.
-- @field Ops.Auftrag#AUFTRAG mission Attached mission.

--- Target object.
-- @type TARGET.Object
-- @field #number ID Target unique ID.
-- @field #string Name Target name.
-- @field #string Type Target type.
-- @field Wrapper.Positionable#POSITIONABLE Object The object, which can be many things, e.g. a UNIT, GROUP, STATIC, SCENERY, AIRBASE or COORDINATE object.
-- @field #number Life Life points on last status update.
-- @field #number Life0 Life points of completely healthy target.
-- @field #number N0 Number of initial elements.
-- @field #number Ndead Number of dead elements.
-- @field #number Ndestroyed Number of destroyed elements.
-- @field #string Status Status "Alive" or "Dead".
-- @field Core.Point#COORDINATE Coordinate of the target object.

--- Global target ID counter.
_TARGETID=0

--- TARGET class version.
-- @field #string version
TARGET.version="0.7.1"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- DONE: Had cases where target life was 0 but target was not dead. Need to figure out why! <== This is due to delayed dead event.
-- DONE: Add pseudo functions.
-- DONE: Initial object can be nil.

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new TARGET object and start the FSM.
-- @param #TARGET self
-- @param #table TargetObject Target object. Can be a: UNIT, GROUP, STATIC, SCENERY, AIRBASE, COORDINATE, ZONE, SET_GROUP, SET_UNIT, SET_STATIC, SET_SCENERY, SET_ZONE
-- @return #TARGET self
function TARGET:New(TargetObject)

  -- Inherit everything from INTEL class.
  local self=BASE:Inherit(self, FSM:New()) --#TARGET

  -- Increase counter.
  _TARGETID=_TARGETID+1
  
  -- Set UID.
  self.uid=_TARGETID
  
  if TargetObject then
 
    -- Add object.
    self:AddObject(TargetObject)
    
  end
  
  -- Defaults.
  self:SetPriority()
  self:SetImportance()
  self.TStatus = 30
  
  -- Log ID.
  self.lid=string.format("TARGET #%03d | ", _TARGETID)

  -- Start state.
  self:SetStartState("Stopped")

  -- Add FSM transitions.
  --                 From State     -->      Event        -->     To State
  self:AddTransition("Stopped",            "Start",               "Alive")       -- Start FSM.
  self:AddTransition("*",                  "Status",              "*")           -- Status update.
  self:AddTransition("*",                  "Stop",                "Stopped")     -- Stop FSM.
  
  self:AddTransition("*",                  "ObjectDamaged",       "*")           -- A target object was damaged.  
  self:AddTransition("*",                  "ObjectDestroyed",     "*")           -- A target object was destroyed.
  self:AddTransition("*",                  "ObjectDead",          "*")           -- A target object is dead (destroyed or despawned).
  
  self:AddTransition("*",                  "Damaged",             "Damaged")     -- Target was damaged.
  self:AddTransition("*",                  "Destroyed",           "Dead")        -- Target was completely destroyed.
  self:AddTransition("*",                  "Dead",                "Dead")        -- Target is dead. Could be destroyed or despawned.

  ------------------------
  --- Pseudo Functions ---
  ------------------------

  --- Triggers the FSM event "Start". Starts the TARGET. Initializes parameters and starts event handlers.
  -- @function [parent=#TARGET] Start
  -- @param #TARGET self

  --- Triggers the FSM event "Start" after a delay. Starts the TARGET. Initializes parameters and starts event handlers.
  -- @function [parent=#TARGET] __Start
  -- @param #TARGET self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Stop". Stops the TARGET and all its event handlers.
  -- @param #TARGET self

  --- Triggers the FSM event "Stop" after a delay. Stops the TARGET and all its event handlers.
  -- @function [parent=#TARGET] __Stop
  -- @param #TARGET self
  -- @param #number delay Delay in seconds.

  --- Triggers the FSM event "Status".
  -- @function [parent=#TARGET] Status
  -- @param #TARGET self

  --- Triggers the FSM event "Status" after a delay.
  -- @function [parent=#TARGET] __Status
  -- @param #TARGET self
  -- @param #number delay Delay in seconds.


  --- Triggers the FSM event "ObjectDamaged".
  -- @function [parent=#TARGET] ObjectDamaged
  -- @param #TARGET self
  -- @param #TARGET.Object Target Target object.

  --- Triggers the FSM event "ObjectDestroyed".
  -- @function [parent=#TARGET] ObjectDestroyed
  -- @param #TARGET self
  -- @param #TARGET.Object Target Target object.

  --- Triggers the FSM event "ObjectDead".
  -- @function [parent=#TARGET] ObjectDead
  -- @param #TARGET self
  -- @param #TARGET.Object Target Target object.


  --- Triggers the FSM event "Damaged".
  -- @function [parent=#TARGET] Damaged
  -- @param #TARGET self

  --- Triggers the FSM event "Destroyed".
  -- @function [parent=#TARGET] Destroyed
  -- @param #TARGET self

  --- Triggers the FSM event "Dead".
  -- @function [parent=#TARGET] Dead
  -- @param #TARGET self

  
  --- On After "ObjectDamaged" event. A (sub-) target object has been damaged, e.g. a UNIT of a GROUP, or an object of a SET
  -- @function [parent=#TARGET] OnAfterObjectDamaged
  -- @param #TARGET self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #TARGET.Object Target Target object.
  
  --- On After "ObjectDestroyed" event. A (sub-) target object has been destroyed, e.g. a UNIT of a GROUP, or an object of a SET
  -- @function [parent=#TARGET] OnAfterObjectDestroyed
  -- @param #TARGET self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #TARGET.Object Target Target object.
  
  --- On After "ObjectDead" event. A (sub-) target object is dead, e.g. a UNIT of a GROUP, or an object of a SET
  -- @function [parent=#TARGET] OnAfterObjectDead
  -- @param #TARGET self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #TARGET.Object Target Target object.

  
  --- On After "Damaged" event. Any of the target objects has been damaged.
  -- @function [parent=#TARGET] OnAfterDamaged
  -- @param #TARGET self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
  --- On After "Destroyed" event. All target objects have been destroyed.
  -- @function [parent=#TARGET] OnAfterDestroyed
  -- @param #TARGET self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  
  --- On After "Dead" event. All target objects are dead.
  -- @function [parent=#TARGET] OnAfterDead
  -- @param #TARGET self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.

  -- Start.
  self:__Start(-0.1)

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create target data from a given object. Valid objects are:
-- 
-- * GROUP
-- * UNIT
-- * STATIC
-- * SCENERY
-- * AIRBASE
-- * COORDINATE
-- * ZONE
-- * SET_GROUP
-- * SET_UNIT
-- * SET_STATIC
-- * SET_SCENERY
-- * SET_OPSGROUP
-- * SET_ZONE
-- * SET_OPSZONE
-- 
-- @param #TARGET self
-- @param Wrapper.Positionable#POSITIONABLE Object The target UNIT, GROUP, STATIC, SCENERY, AIRBASE, COORDINATE, ZONE, SET_GROUP, SET_UNIT, SET_STATIC, SET_SCENERY, SET_ZONE
function TARGET:AddObject(Object)
    
  if Object:IsInstanceOf("SET_GROUP")    or 
     Object:IsInstanceOf("SET_UNIT")     or 
     Object:IsInstanceOf("SET_STATIC")   or 
     Object:IsInstanceOf("SET_SCENERY")  or 
     Object:IsInstanceOf("SET_OPSGROUP") or  
     Object:IsInstanceOf("SET_OPSZONE")  then

    ---
    -- Sets
    ---

    local set=Object --Core.Set#SET_GROUP
    
    for _,object in pairs(set.Set) do
      self:AddObject(object)
    end
    
  elseif  Object:IsInstanceOf("SET_ZONE") then
  
    local set=Object --Core.Set#SET_ZONE
    
    set:SortByName()
  
    for index,ZoneName in pairs(set.Index) do
      local zone=set.Set[ZoneName] --Core.Zone#ZONE
      self:_AddObject(zone)
    end
    
  else
  
    ---
    -- Groups, Units, Statics, Airbases, Coordinates
    ---
  
    if Object:IsInstanceOf("OPSGROUP") then
      self:_AddObject(Object:GetGroup()) -- We add the MOOSE GROUP object not the OPSGROUP object.
    --elseif Object:IsInstanceOf("OPSZONE") then
      --self:_AddObject(Object:GetZone())
    else
      self:_AddObject(Object)
    end
    
  end

  return self
end

--- Set priority of the target.
-- @param #TARGET self
-- @param #number Priority Priority of the target. Default 50.
-- @return #TARGET self
function TARGET:SetPriority(Priority)
  self.prio=Priority or 50
  return self
end

--- Set importance of the target.
-- @param #TARGET self
-- @param #number Importance Importance of the target. Default `nil`.
-- @return #TARGET self
function TARGET:SetImportance(Importance)
  self.importance=Importance
  return self
end

--- Add start condition.
-- @param #TARGET self
-- @param #function ConditionFunction Function that needs to be true before the mission can be started. Must return a #boolean.
-- @param ... Condition function arguments if any.
-- @return #TARGET self
function TARGET:AddConditionStart(ConditionFunction, ...)

  local condition={} --Ops.Auftrag#AUFTRAG.Condition

  condition.func=ConditionFunction
  condition.arg={}
  if arg then
    condition.arg=arg
  end

  table.insert(self.conditionStart, condition)

  return self
end

--- Add stop condition.
-- @param #TARGET self
-- @param #function ConditionFunction Function that needs to be true before the mission can be started. Must return a #boolean.
-- @param ... Condition function arguments if any.
-- @return #TARGET self
function TARGET:AddConditionStop(ConditionFunction, ...)

  local condition={} --Ops.Auftrag#AUFTRAG.Condition

  condition.func=ConditionFunction
  condition.arg={}
  if arg then
    condition.arg=arg
  end

  table.insert(self.conditionStop, condition)

  return self
end

--- Check if all given condition are true.
-- @param #TARGET self
-- @param #table Conditions Table of conditions.
-- @return #boolean If true, all conditions were true. Returns false if at least one condition returned false.
function TARGET:EvalConditionsAll(Conditions)

  -- Any stop condition must be true.
  for _,_condition in pairs(Conditions or {}) do
    local condition=_condition --Ops.Auftrag#AUFTRAG.Condition

    -- Call function.
    local istrue=condition.func(unpack(condition.arg))

    -- Any false will return false.
    if not istrue then
      return false
    end

  end

  -- All conditions were true.
  return true
end


--- Check if any of the given conditions is true.
-- @param #TARGET self
-- @param #table Conditions Table of conditions.
-- @return #boolean If true, at least one condition is true.
function TARGET:EvalConditionsAny(Conditions)

  -- Any stop condition must be true.
  for _,_condition in pairs(Conditions or {}) do
    local condition=_condition --Ops.Auftrag#AUFTRAG.Condition

    -- Call function.
    local istrue=condition.func(unpack(condition.arg))

    -- Any true will return true.
    if istrue then
      return true
    end

  end

  -- No condition was true.
  return false
end

--- Add mission type and number of required assets to resource.
-- @param #TARGET self
-- @param #string MissionType Mission Type.
-- @param #number Nmin Min number of required assets.
-- @param #number Nmax Max number of requried assets.
-- @param #table Attributes Generalized attribute(s).
-- @param #table Properties DCS attribute(s). Default `nil`.
-- @return #TARGET.Resource The resource table.
function TARGET:AddResource(MissionType, Nmin, Nmax, Attributes, Properties)
  
  -- Ensure table.
  if Attributes and type(Attributes)~="table" then
    Attributes={Attributes}
  end
  
  -- Ensure table.
  if Properties and type(Properties)~="table" then
    Properties={Properties}
  end
  
  -- Create new resource table.
  local resource={} --#TARGET.Resource
  resource.MissionType=MissionType
  resource.Nmin=Nmin or 1
  resource.Nmax=Nmax or 1
  resource.Attributes=Attributes or {}
  resource.Properties=Properties or {}
  
  -- Init resource table.
  self.resources=self.resources or {}
  
  -- Add to table.
  table.insert(self.resources, resource)
  
  -- Debug output.
  if self.verbose>10 then
    local text="Resource:"
    for _,_r in pairs(self.resources) do
      local r=_r --#TARGET.Resource
      text=text..string.format("\nmission=%s, Nmin=%d, Nmax=%d, attribute=%s, properties=%s", r.MissionType, r.Nmin, r.Nmax, tostring(r.Attributes[1]), tostring(r.Properties[1]))
    end
    self:I(self.lid..text)
  end
    
  return resource
end

--- Check if TARGET is alive.
-- @param #TARGET self
-- @return #boolean If true, target is alive.
function TARGET:IsAlive()

  for _,_target in pairs(self.targets) do
    local target=_target --Ops.Target#TARGET.Object
    if target.Status~=TARGET.ObjectStatus.DEAD then
      if self.isDestroyed then
        self:E(self.lid..string.format("ERROR: target is DESTROYED but target object status is not DEAD but %s for object %s", target.Status, target.Name))
      elseif self:IsDead() then
        self:E(self.lid..string.format("ERROR: target is DEAD but target object status is not DEAD but %s for object %s", target.Status, target.Name))
      end
      return true
    end
  end

  return false
end

--- Check if TARGET is destroyed.
-- @param #TARGET self
-- @return #boolean If true, target is destroyed.
function TARGET:IsDestroyed()
  return self.isDestroyed
end


--- Check if TARGET is dead.
-- @param #TARGET self
-- @return #boolean If true, target is dead.
function TARGET:IsDead()
  local is=self:Is("Dead")
  return is
end

--- Check if target object is dead.
-- @param #TARGET self
-- @param #TARGET.Object TargetObject The target object.
-- @return #boolean If true, target is dead.
function TARGET:IsTargetDead(TargetObject)
  local isDead=TargetObject.Status==TARGET.ObjectStatus.DEAD
  return isDead
end

--- Check if target object is alive.
-- @param #TARGET self
-- @param #TARGET.Object TargetObject The target object.
-- @return #boolean If true, target is dead.
function TARGET:IsTargetAlive(TargetObject)
  local isAlive=TargetObject.Status==TARGET.ObjectStatus.ALIVE
  return isAlive
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start & Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers.
-- @param #TARGET self
-- @param Wrapper.Group#GROUP Group Flight group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function TARGET:onafterStart(From, Event, To)
  self:T({From, Event, To})
  -- Short info.
  local text=string.format("Starting Target")
  self:T(self.lid..text)

  self:HandleEvent(EVENTS.Dead,       self.OnEventUnitDeadOrLost)
  self:HandleEvent(EVENTS.UnitLost,   self.OnEventUnitDeadOrLost)
  self:HandleEvent(EVENTS.RemoveUnit, self.OnEventUnitDeadOrLost)

  self:__Status(-1)
  return self
end

--- On after "Status" event.
-- @param #TARGET self
-- @param Wrapper.Group#GROUP Group Flight group.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function TARGET:onafterStatus(From, Event, To)
  --self:T({From, Event, To})
  
  -- FSM state.
  local fsmstate=self:GetState()
    
  -- Update damage.
  local damaged=false
  for i,_target in pairs(self.targets) do
    local target=_target --#TARGET.Object
    
    -- old life
    local life=target.Life
    
    -- curr life    
    target.Life=self:GetTargetLife(target)
    
    -- TODO: special case ED bug > life **increases** after hits on SCENERY
    if target.Life > target.Life0 then
      local delta = 2*(target.Life-target.Life0)
      target.Life0 = target.Life0 + delta
      life = target.Life0 
      self.life0 = self.life0+delta
    end
    
    -- Check if life decreased ==> damaged
    if target.Life<life then
      --target.Status = TARGET.ObjectStatus.DAMAGED
      self:ObjectDamaged(target)
      damaged=true
    end
    
    if target.Life<1 and target.Status~=TARGET.ObjectStatus.DEAD then
      self:E(self.lid..string.format("FF life is zero but no object dead event fired ==> object dead now for target object %s!", tostring(target.Name)))
      self:ObjectDead(target)
      damaged = true
    end
    
  end
  
  -- Target was damaged.
  if damaged then
    self:Damaged()
  end
  
  -- Log output verbose=1.
  if self.verbose>=1 then
    local text=string.format("%s: Targets=%d/%d [%d, %d], Life=%.1f/%.1f, Damage=%.1f", 
    fsmstate, self:CountTargets(), self.N0, self.Ndestroyed, self.Ndead, self:GetLife(), self:GetLife0(), self:GetDamage())
    if self:CountTargets() == 0 or self:GetDamage() >= 100 then
      text=text.." - Dead!"
    elseif damaged then
      text=text.." - Damaged!"
    end
    self:I(self.lid..text)
  end  
  
  -- Log output verbose=2.
  if self.verbose>=2 then
    local text="Target:"
    for i,_target in pairs(self.targets) do
      local target=_target --#TARGET.Object
      local damage=(1-target.Life/target.Life0)*100
      text=text..string.format("\n[%d] %s %s %s: Life=%.1f/%.1f, Damage=%.1f, N0=%d, Ndestroyed=%d, Ndead=%d", 
      i, target.Type, target.Name, target.Status, target.Life, target.Life0, damage, target.N0, target.Ndestroyed, target.Ndead)
    end
    self:I(self.lid..text)
  end
  
  -- Consitency check if target is still alive but all target objects are dead
  if self:IsAlive() and (self:CountTargets()==0 or self:GetDamage()>=100) then
    self:Dead()
  end
  
  -- Quick sanity check
  for i,_target in pairs(self.targets) do
    local target=_target --#TARGET.Object
    if target.Ndestroyed>target.N0 then
      self:E(self.lid..string.format("ERROR: Number of destroyed target objects greater than number of initial target objects: %d>%d!", target.Ndestroyed, target.N0))
    end
    if target.Ndestroyed>target.N0 then
      self:E(self.lid..string.format("ERROR: Number of dead target objects greater than number of initial target objects: %d>%d!", target.Ndead, target.N0))
    end
  end
  
  -- Update status again in 30 sec.
  if self:IsAlive() then
    self:__Status(-self.TStatus)
  else
    self:I(self.lid..string.format("Target is not alive any more ==> no further status updates are carried out"))
  end
  
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after "ObjectDamaged" event.
-- @param #TARGET self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #TARGET.Object Target Target object.
function TARGET:onafterObjectDamaged(From, Event, To, Target)
  self:T({From, Event, To})
  -- Debug info.
  self:T(self.lid..string.format("Object %s damaged", Target.Name))
  
  return self
end

--- On after "ObjectDestroyed" event.
-- @param #TARGET self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #TARGET.Object Target Target object.
function TARGET:onafterObjectDestroyed(From, Event, To, Target)
  self:T({From, Event, To})
  -- Debug message.
  self:T(self.lid..string.format("Object %s destroyed", Target.Name))
  
  -- Increase destroyed counter.
  self.Ndestroyed=self.Ndestroyed+1
  
  Target.Ndestroyed=Target.Ndestroyed+1
  
  Target.Life=0
  
  -- Call object dead event.
  self:ObjectDead(Target)
  
  return self
end

--- On after "ObjectDead" event.
-- @param #TARGET self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #TARGET.Object Target Target object.
function TARGET:onafterObjectDead(From, Event, To, Target)
  self:T({From, Event, To})
  -- Debug message.
  self:T(self.lid..string.format("Object %s dead", Target.Name))

  -- Set target status.
  Target.Status=TARGET.ObjectStatus.DEAD
  
  -- Increase dead object counter
  Target.Ndead=Target.Ndead+1
  
  -- Set target object life to 0.
  Target.Life=0
  
  -- Increase dead counter.
  self.Ndead=self.Ndead+1
  
  -- Check if anyone is alive?
  local dead=true
  for _,_target in pairs(self.targets) do
    local target=_target --#TARGET.Object
    if target.Status==TARGET.ObjectStatus.ALIVE then
      dead=false
      break -- break the loop because we now we are not dead
    end
  end
  
  -- All dead ==> Trigger destroyed event.
  if dead then
  
    if self.Ndestroyed==self.Ntargets0 then
  
      self.isDestroyed=true
      
      self:Destroyed()
      
    else
    
      self:Dead()
    
    end
  else
    self:Damaged()
  end
  
  return self
end

--- On after "Damaged" event.
-- @param #TARGET self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function TARGET:onafterDamaged(From, Event, To)
  self:T({From, Event, To})
  
  self:T(self.lid..string.format("TARGET damaged"))
  
  return self
end

--- On after "Destroyed" event.
-- @param #TARGET self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function TARGET:onafterDestroyed(From, Event, To)
  self:T({From, Event, To})
  
  self:T(self.lid..string.format("TARGET destroyed"))
  
  self:Dead()
  
  return self
end

--- On after "Dead" event.
-- @param #TARGET self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function TARGET:onafterDead(From, Event, To)
  self:T({From, Event, To})
  
  self:T(self.lid..string.format("TARGET dead"))
  
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Event function handling the loss of a unit.
-- @param #TARGET self
-- @param Core.Event#EVENTDATA EventData Event data.
function TARGET:OnEventUnitDeadOrLost(EventData)

  local Name=EventData and EventData.IniUnitName or nil

  -- Check that this is the right group.
  if self:IsElement(Name) and not self:IsCasualty(Name) then
  
    -- Debug info.
    self:T(self.lid..string.format("EVENT ID=%d: Unit %s dead or lost!", EventData.id, tostring(Name)))
    
    -- Add to the list of casualties.
    table.insert(self.casualties, Name)
        
    -- Try to get target Group.
    local target=self:GetTargetByName(EventData.IniGroupName)
    
    -- Try unit target.
    if not target then    
      target=self:GetTargetByName(EventData.IniUnitName)      
    end
    
    -- Check if we could find a target object.
    if target then
    
      local Ndead=target.Ndead
      local Ndestroyed=target.Ndestroyed
      if EventData.id==EVENTS.RemoveUnit then
        Ndead=Ndead+1
      else
        Ndestroyed=Ndestroyed+1
        Ndead=Ndead+1
      end
      
      
      -- Check if ALL objects are dead
      if Ndead==target.N0 then
      
        if Ndestroyed>=target.N0 then

          -- Debug message.
          self:T2(self.lid..string.format("EVENT ID=%d: target %s dead/lost ==> destroyed", EventData.id, tostring(target.Name)))
          
          target.Life = 0
          
          -- Trigger object destroyed event. This sets the Life to zero and increases Ndestroyed
          self:ObjectDestroyed(target)
          
        else
        
          -- Debug message.
          self:T2(self.lid..string.format("EVENT ID=%d: target %s removed ==> dead", EventData.id, tostring(target.Name)))
          
          target.Life = 0
          
          -- Trigger object dead event.  This sets the Life to zero and increases Ndead counter
          self:ObjectDead(target)
        
        end
      
      end

    end -- Event belongs to this TARGET 
    
  end
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Adding and Removing Targets
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create target data from a given object.
-- @param #TARGET self
-- @param Wrapper.Positionable#POSITIONABLE Object The target UNIT, GROUP, STATIC, SCENERY, AIRBASE, COORDINATE, ZONE, SET_GROUP, SET_UNIT, SET_STATIC, SET_SCENERY, SET_ZONE
function TARGET:_AddObject(Object)

  local target={}  --#TARGET.Object
  
  target.N0=0
  target.Ndead=0
  target.Ndestroyed=0
  
  if Object:IsInstanceOf("GROUP") then
  
    local group=Object --Wrapper.Group#GROUP
  
    target.Type=TARGET.ObjectType.GROUP
    target.Name=group:GetName()   
    
    target.Coordinate=group:GetCoordinate() 
        
    local units=group:GetUnits()
      
    target.Life=0 ; target.Life0=0
    for _,_unit in pairs(units or {}) do
      local unit=_unit --Wrapper.Unit#UNIT
      
      local life=unit:GetLife()
      
      target.Life=target.Life+life
      target.Life0=target.Life0+math.max(unit:GetLife0(), life)  -- There was an issue with ships that life is greater life0, which cannot be!
      
      self.threatlevel0=self.threatlevel0+unit:GetThreatLevel()
      
      table.insert(self.elements, unit:GetName())
      
      target.N0=target.N0+1
    end
  
  elseif Object:IsInstanceOf("UNIT") then
  
    local unit=Object --Wrapper.Unit#UNIT
    
    target.Type=TARGET.ObjectType.UNIT
    target.Name=unit:GetName()
    
    target.Coordinate=unit:GetCoordinate()
    
    if unit then
      target.Life=unit:GetLife()
      target.Life0=math.max(unit:GetLife0(), target.Life)  -- There was an issue with ships that life is greater life0!
      
      self.threatlevel0=self.threatlevel0+unit:GetThreatLevel()
      
      table.insert(self.elements, unit:GetName())
      
      target.N0=target.N0+1
    end

  elseif Object:IsInstanceOf("STATIC") then
  
    local static=Object --Wrapper.Static#STATIC
    
    target.Type=TARGET.ObjectType.STATIC
    target.Name=static:GetName()
    
    target.Coordinate=static:GetCoordinate()
    
    if static and static:IsAlive() then
    
      target.Life0=static:GetLife0()
      target.Life=static:GetLife()      
      target.N0=target.N0+1
      
      table.insert(self.elements, target.Name)
      
    end

  elseif Object:IsInstanceOf("SCENERY") then
  
    local scenery=Object --Wrapper.Scenery#SCENERY
    
    target.Type=TARGET.ObjectType.SCENERY
    target.Name=scenery:GetName()
    
    target.Coordinate=scenery:GetCoordinate()

    target.Life0=scenery:GetLife0()
    
    if target.Life0==0 then target.Life0 = 1 end
    
    target.Life=scenery:GetLife()
    
    target.N0=target.N0+1
    
    table.insert(self.elements, target.Name)

  elseif Object:IsInstanceOf("AIRBASE") then
  
    local airbase=Object --Wrapper.Airbase#AIRBASE
    
    target.Type=TARGET.ObjectType.AIRBASE
    target.Name=airbase:GetName()
    
    target.Coordinate=airbase:GetCoordinate()

    target.Life0=1
    target.Life=1
    
    target.N0=target.N0+1
    
    table.insert(self.elements, target.Name)

  elseif Object:IsInstanceOf("COORDINATE") then

    local coord=UTILS.DeepCopy(Object) --Core.Point#COORDINATE

    target.Type=TARGET.ObjectType.COORDINATE
    target.Name=coord:ToStringMGRS()
    
    target.Coordinate=coord

    target.Life0=1
    target.Life=1
    
    target.N0=target.N0+1
      
  elseif Object:IsInstanceOf("ZONE_BASE") then
  
    local zone=Object --Core.Zone#ZONE_BASE
    Object=zone --:GetCoordinate()
    
    target.Type=TARGET.ObjectType.ZONE
    target.Name=zone:GetName()
    
    target.Coordinate=zone:GetCoordinate()

    target.Life0=1
    target.Life=1
    
    target.N0=target.N0+1

  elseif Object:IsInstanceOf("OPSZONE") then
  
  
    local zone=Object --Ops.OpsZone#OPSZONE
    Object=zone
    
    target.Type=TARGET.ObjectType.OPSZONE
    target.Name=zone:GetName()
    
    target.Coordinate=zone:GetCoordinate()
    
    target.N0=target.N0+1

    target.Life0=1
    target.Life=1
  
  else
    self:E(self.lid.."ERROR: Unknown object type!")
    return nil
  end
  
  self.life=self.life+target.Life
  self.life0=self.life0+target.Life0
  
  self.N0=self.N0+target.N0
  self.Ntargets0=self.Ntargets0+1
 
  -- Increase counter.
  self.targetcounter=self.targetcounter+1
  
  target.ID=self.targetcounter
  target.Status=TARGET.ObjectStatus.ALIVE
  target.Object=Object
  
  table.insert(self.targets, target)
  
  if self.name==nil then
    self.name=self:GetTargetName(target)
  end
  if self.category==nil then
    self.category=self:GetTargetCategory(target)
  end
  
  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Life and Damage Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get target life points.
-- @param #TARGET self
-- @return #number Number of initial life points when mission was planned.
function TARGET:GetLife0()
  return self.life0
end

--- Get current damage.
-- @param #TARGET self
-- @return #number Damage in percent.
function TARGET:GetDamage()
  local life=self:GetLife()/self:GetLife0()
  local damage=1-life
  return damage*100
end

--- Get target life points.
-- @param #TARGET self
-- @param #TARGET.Object Target Target object.
-- @return #number Life points of target.
function TARGET:GetTargetLife(Target)

  if Target.Type==TARGET.ObjectType.GROUP then

    if Target.Object and Target.Object:IsAlive() then
    
      local units=Target.Object:GetUnits()
      
      local life=0
      for _,_unit in pairs(units or {}) do
        local unit=_unit --Wrapper.Unit#UNIT
        life=life+unit:GetLife()
      end
      
      return life
    else
      return 0
    end

  elseif Target.Type==TARGET.ObjectType.UNIT then

    local unit=Target.Object --Wrapper.Unit#UNIT

    if unit and unit:IsAlive() then
    
      -- Note! According to the profiler, there is a big difference if we "return unit:GetLife()" or "local life=unit:GetLife(); return life"!
      local life=unit:GetLife()
      return life
    else
      return 0
    end
  
  elseif Target.Type==TARGET.ObjectType.STATIC then
  
    if Target.Object and Target.Object:IsAlive() then
      local life=Target.Object:GetLife()
      return life
      --return 1
    else
      return 0
    end

  elseif Target.Type==TARGET.ObjectType.SCENERY then
  
    if Target.Object and Target.Object:IsAlive(25)  then
      local life = Target.Object:GetLife()
      return life
    else
      return 0
    end
    
  elseif Target.Type==TARGET.ObjectType.AIRBASE then
  
    if Target.Status==TARGET.ObjectStatus.ALIVE then
      return 1
    else
      return 0
    end
    
  elseif Target.Type==TARGET.ObjectType.COORDINATE then
  
    return 1

  elseif Target.Type==TARGET.ObjectType.ZONE or Target.Type==TARGET.ObjectType.OPSZONE then
  
    return 1
    
  else
    self:E("ERROR: unknown target object type in GetTargetLife!")
  end
  
  return self
end

--- Get current total life points. This is the sum of all target objects.
-- @param #TARGET self
-- @return #number Life points of target.
function TARGET:GetLife()
  
  local N=0

  for _,_target in pairs(self.targets) do
    local Target=_target --#TARGET.Object
    
    N=N+self:GetTargetLife(Target)

  end
  
  return N
end

--- Get target threat level
-- @param #TARGET self
-- @param #TARGET.Object Target Target object.
-- @return #number Threat level of target.
function TARGET:GetTargetThreatLevelMax(Target)

  if Target.Type==TARGET.ObjectType.GROUP then
  
    local group=Target.Object --Wrapper.Group#GROUP

    if group and group:IsAlive() then
    
      local tl=group:GetThreatLevel()

      return tl
    else
      return 0
    end

  elseif Target.Type==TARGET.ObjectType.UNIT then

    local unit=Target.Object --Wrapper.Unit#UNIT

    if unit and unit:IsAlive() then
    
      -- Note! According to the profiler, there is a big difference if we "return unit:GetLife()" or "local life=unit:GetLife(); return life"!
      local life=unit:GetThreatLevel()
      return life
    else
      return 0
    end
  
  elseif Target.Type==TARGET.ObjectType.STATIC then
  
    return 0

  elseif Target.Type==TARGET.ObjectType.SCENERY then
  
    return 0
        
  elseif Target.Type==TARGET.ObjectType.AIRBASE then

    return 0  
    
  elseif Target.Type==TARGET.ObjectType.COORDINATE then
  
    return 0

  elseif Target.Type==TARGET.ObjectType.ZONE then
    
    local zone = Target.Object -- Core.Zone#ZONE_RADIUS
    local foundunits = {}
    if zone:IsInstanceOf("ZONE_RADIUS") or zone:IsInstanceOf("ZONE_POLYGON") then
      zone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT,Unit.Category.SHIP})
      foundunits = zone:GetScannedSetUnit()
    else
      foundunits = SET_UNIT:New():FilterZones({zone}):FilterOnce()
    end
    local ThreatMax = foundunits:GetThreatLevelMax() or 0
    return ThreatMax
  
  elseif Target.Type==TARGET.ObjectType.OPSZONE then
    
    local unitset = Target.Object:GetScannedUnitSet() -- Core.Set#SET_UNIT
    local ThreatMax = unitset:GetThreatLevelMax()
    return ThreatMax
    
  else
    self:E("ERROR: unknown target object type in GetTargetThreatLevel!")
    return 0
  end
  
  return self
end


--- Get threat level.
-- @param #TARGET self
-- @return #number Threat level.
function TARGET:GetThreatLevelMax()
  
  local N=0

  for _,_target in pairs(self.targets) do
    local Target=_target --#TARGET.Object
    
    local n=self:GetTargetThreatLevelMax(Target)
    
    if n>N then
      N=n
    end

  end
  
  return N
end

--- Get target 2D position vector.
-- @param #TARGET self
-- @param #TARGET.Object Target Target object.
-- @return DCS#Vec2 Vector with x,y components.
function TARGET:GetTargetVec2(Target)

  local vec3=self:GetTargetVec3(Target)
  
  if vec3 then
    return {x=vec3.x, y=vec3.z}
  end
  
  return nil
end

--- Get target 3D position vector.
-- @param #TARGET self
-- @param #TARGET.Object Target Target object.
-- @param #boolean Average
-- @return DCS#Vec3 Vector with x,y,z components.
function TARGET:GetTargetVec3(Target, Average)

  if Target.Type==TARGET.ObjectType.GROUP then
  
    local object=Target.Object --Wrapper.Group#GROUP

    if object and object:IsAlive() then
      local vec3=object:GetVec3()
      if Average then
        vec3=object:GetAverageVec3()
      end
      
      if vec3 then
        return vec3
      else
        return nil
      end
    else
    
      return nil

    end

  elseif Target.Type==TARGET.ObjectType.UNIT then
  
    local object=Target.Object --Wrapper.Unit#UNIT

    if object and object:IsAlive() then
      local vec3=object:GetVec3()
      return vec3
    else
      return nil
    end
  
  elseif Target.Type==TARGET.ObjectType.STATIC then
  
    local object=Target.Object --Wrapper.Static#STATIC
  
    if object and object:IsAlive() then
      local vec3=object:GetVec3()
      return vec3
    else
      return nil
    end

  elseif Target.Type==TARGET.ObjectType.SCENERY then
  
    local object=Target.Object --Wrapper.Scenery#SCENERY
  
    if object then
      local vec3=object:GetVec3()
      return vec3
    else
      return nil
    end
    
  elseif Target.Type==TARGET.ObjectType.AIRBASE then
  
    local object=Target.Object --Wrapper.Airbase#AIRBASE
    
    local vec3=object:GetVec3()
    return vec3
  
    --if Target.Status==TARGET.ObjectStatus.ALIVE then      
    --end
    
  elseif Target.Type==TARGET.ObjectType.COORDINATE then
  
    local object=Target.Object --Core.Point#COORDINATE
  
    local vec3={x=object.x, y=object.y, z=object.z}
    return vec3
    
  elseif Target.Type==TARGET.ObjectType.ZONE then
  
    local object=Target.Object --Core.Zone#ZONE
  
    local vec3=object:GetVec3()
    return vec3

  elseif Target.Type==TARGET.ObjectType.OPSZONE then
  
    local object=Target.Object --Ops.OpsZone#OPSZONE
  
    local vec3=object:GetZone():GetVec3()
    return vec3
        
  end

  self:E(self.lid.."ERROR: Unknown TARGET type! Cannot get Vec3")
end

--- Get heading of the target.
-- @param #TARGET self
-- @param #TARGET.Object Target Target object.
-- @return #number Heading in degrees.
function TARGET:GetTargetHeading(Target)

  if Target.Type==TARGET.ObjectType.GROUP then
  
    local object=Target.Object --Wrapper.Group#GROUP

    if object and object:IsAlive() then
      local heading=object:GetHeading()

      if heading then
        return heading
      else
        return nil
      end
    else
    
      return nil

    end

  elseif Target.Type==TARGET.ObjectType.UNIT then
  
    local object=Target.Object --Wrapper.Unit#UNIT

    if object and object:IsAlive() then
      local heading=object:GetHeading()
      return heading
    else
      return nil
    end
  
  elseif Target.Type==TARGET.ObjectType.STATIC then
  
    local object=Target.Object --Wrapper.Static#STATIC
  
    if object and object:IsAlive() then
      local heading=object:GetHeading()
      return heading
    else
      return nil
    end

  elseif Target.Type==TARGET.ObjectType.SCENERY then
  
    local object=Target.Object --Wrapper.Scenery#SCENERY
  
    if object then
      local heading=object:GetHeading()
      return heading
    else
      return nil
    end
    
  elseif Target.Type==TARGET.ObjectType.AIRBASE then
  
    local object=Target.Object --Wrapper.Airbase#AIRBASE
    
    -- Airbase has no real heading. Return 0. Maybe take the runway heading?
    return 0
    
  elseif Target.Type==TARGET.ObjectType.COORDINATE then
  
    local object=Target.Object --Core.Point#COORDINATE
  
    -- A coordinate has no heading. Return 0.
    return 0
    
  elseif Target.Type==TARGET.ObjectType.ZONE or Target.Type==TARGET.ObjectType.OPSZONE then
  
    local object=Target.Object --Core.Zone#ZONE
  
    -- A zone has no heading. Return 0.
    return 0
        
  end

  self:E(self.lid.."ERROR: Unknown TARGET type! Cannot get heading")
end


--- Get target coordinate.
-- @param #TARGET self
-- @param #TARGET.Object Target Target object.
-- @param #boolean Average
-- @return Core.Point#COORDINATE Coordinate of the target.
function TARGET:GetTargetCoordinate(Target, Average)

  if Target.Type==TARGET.ObjectType.COORDINATE then
  
    -- Coordinate is the object itself.
    return Target.Object
    
  else
    
    -- Get updated position vector.
    local vec3=self:GetTargetVec3(Target, Average)
    
    -- Update position. This saves us to create a new COORDINATE object each time.
    if vec3 then
      Target.Coordinate.x=vec3.x
      Target.Coordinate.y=vec3.y
      Target.Coordinate.z=vec3.z
    end
    
    return Target.Coordinate
  
  end

  return nil
end

--- Get target name.
-- @param #TARGET self
-- @param #TARGET.Object Target Target object.
-- @return #string Name of the target object.
function TARGET:GetTargetName(Target)

  if Target.Type==TARGET.ObjectType.GROUP then

    if Target.Object and Target.Object:IsAlive() then

      return Target.Object:GetName()

    end

  elseif Target.Type==TARGET.ObjectType.UNIT then

    if Target.Object and Target.Object:IsAlive() then
      return Target.Object:GetName()
    end
  
  elseif Target.Type==TARGET.ObjectType.STATIC then
  
    if Target.Object and Target.Object:IsAlive() then
      return Target.Object:GetName()
    end
    
  elseif Target.Type==TARGET.ObjectType.AIRBASE then
  
    if Target.Status==TARGET.ObjectStatus.ALIVE then
      return Target.Object:GetName()
    end
    
  elseif Target.Type==TARGET.ObjectType.COORDINATE then
  
    local coord=Target.Object  --Core.Point#COORDINATE
    
    return coord:ToStringMGRS()

  elseif Target.Type==TARGET.ObjectType.ZONE then
  
    local Zone=Target.Object  --Core.Zone#ZONE
    
    return Zone:GetName()
    
  elseif Target.Type==TARGET.ObjectType.SCENERY then
  
    local Zone=Target.Object  --Core.Zone#ZONE
    
    return Zone:GetName()  
  end

  return "Unknown"
end

--- Get name.
-- @param #TARGET self
-- @return #string Name of the target usually the first object.
function TARGET:GetName()
  local name=self.name or "Unknown"
  return name
end

--- Get 2D vector.
-- @param #TARGET self
-- @return DCS#Vec2 2D vector of the target.
function TARGET:GetVec2()

  for _,_target in pairs(self.targets) do
    local Target=_target --#TARGET.Object
    
    local coordinate=self:GetTargetVec2(Target)
    
    if coordinate then
      return coordinate
    end

  end

  self:E(self.lid..string.format("ERROR: Cannot get Vec2 of target %s", self.name))
  return nil
end

--- Get 3D vector.
-- @param #TARGET self
-- @return DCS#Vec3 3D vector of the target.
function TARGET:GetVec3()

  for _,_target in pairs(self.targets) do
    local Target=_target --#TARGET.Object
    
    local coordinate=self:GetTargetVec3(Target)
    
    if coordinate then
      return coordinate
    end

  end

  self:E(self.lid..string.format("ERROR: Cannot get Vec3 of target %s", self.name))
  return nil
end

--- Get coordinate.
-- @param #TARGET self
-- @return Core.Point#COORDINATE Coordinate of the target.
function TARGET:GetCoordinate()

  for _,_target in pairs(self.targets) do
    local Target=_target --#TARGET.Object
    
    local coordinate=self:GetTargetCoordinate(Target)
    
    if coordinate then
      return coordinate
    end

  end

  self:E(self.lid..string.format("ERROR: Cannot get coordinate of target %s", tostring(self.name)))
  return nil
end

--- Get average coordinate.
-- @param #TARGET self
-- @return Core.Point#COORDINATE Coordinate of the target.
function TARGET:GetAverageCoordinate()

  for _,_target in pairs(self.targets) do
    local Target=_target --#TARGET.Object
    
    local coordinate=self:GetTargetCoordinate(Target, true)
    
    if coordinate then
      return coordinate
    end

  end

  self:E(self.lid..string.format("ERROR: Cannot get average coordinate of target %s", tostring(self.name)))
  return nil
end


--- Get coordinates of all targets. (e.g. for a SET_STATIC)
-- @param #TARGET self
-- @return #table Table with coordinates of all targets.
function TARGET:GetCoordinates()
    local coordinates={}

    for _,_target in pairs(self.targets) do
        local target=_target --#TARGET.Object

        local coordinate=self:GetTargetCoordinate(target)
        if coordinate then
            table.insert(coordinates, coordinate)
        end

    end

    return coordinates
end

--- Get heading of target.
-- @param #TARGET self
-- @return #number Heading of the target in degrees.
function TARGET:GetHeading()

  for _,_target in pairs(self.targets) do
    local Target=_target --#TARGET.Object
    
    local heading=self:GetTargetHeading(Target)
    
    if heading then
      return heading
    end

  end

  self:E(self.lid..string.format("ERROR: Cannot get heading of target %s", tostring(self.name)))
  return nil
end

--- Get category.
-- @param #TARGET self
-- @return #string Target category. See `TARGET.Category.X`, where `X=AIRCRAFT, GROUND`.
function TARGET:GetCategory()
  return self.category
end


--- Get target category.
-- @param #TARGET self
-- @param #TARGET.Object Target Target object.
-- @return #TARGET.Category Target category.
function TARGET:GetTargetCategory(Target)

  local category=nil

  if Target.Type==TARGET.ObjectType.GROUP then

    if Target.Object and Target.Object:IsAlive()~=nil then
    
      local group=Target.Object --Wrapper.Group#GROUP
      
      local cat=group:GetCategory()
      
      if cat==Group.Category.AIRPLANE or cat==Group.Category.HELICOPTER then
        category=TARGET.Category.AIRCRAFT
      elseif cat==Group.Category.GROUND or cat==Group.Category.TRAIN then
        category=TARGET.Category.GROUND
      elseif cat==Group.Category.SHIP then
        category=TARGET.Category.NAVAL
      end

    end

  elseif Target.Type==TARGET.ObjectType.UNIT then

    if Target.Object and Target.Object:IsAlive()~=nil then
      local unit=Target.Object --Wrapper.Unit#UNIT
      
      local group=unit:GetGroup()

      local cat=group:GetCategory()
      
      if cat==Group.Category.AIRPLANE or cat==Group.Category.HELICOPTER then
        category=TARGET.Category.AIRCRAFT
      elseif cat==Group.Category.GROUND or cat==Group.Category.TRAIN then
        category=TARGET.Category.GROUND
      elseif cat==Group.Category.SHIP then
        category=TARGET.Category.NAVAL
      end
      
    end
  
  elseif Target.Type==TARGET.ObjectType.STATIC then
  
    return TARGET.Category.GROUND
    
  elseif Target.Type==TARGET.ObjectType.SCENERY then
  
    return TARGET.Category.GROUND
      
  elseif Target.Type==TARGET.ObjectType.AIRBASE then
  
    return TARGET.Category.AIRBASE
    
  elseif Target.Type==TARGET.ObjectType.COORDINATE then
  
    return TARGET.Category.COORDINATE

  elseif Target.Type==TARGET.ObjectType.ZONE then
  
    return TARGET.Category.ZONE
    
  elseif Target.Type==TARGET.ObjectType.OPSZONE then
  
    return TARGET.Category.OPSZONE

  else
    self:E("ERROR: unknown target category!")
  end

  return category
end


--- Get coalition of target object. If an object has no coalition (*e.g.* a coordinate) it is returned as neutral.
-- @param #TARGET self
-- @param #TARGET.Object Target Target object.
-- @return #number Coalition number.
function TARGET:GetTargetCoalition(Target)


  -- We take neutral for objects that do not have a coalition.
  local coal=coalition.side.NEUTRAL


  if Target.Type==TARGET.ObjectType.GROUP then

    if Target.Object and Target.Object:IsAlive()~=nil then    
      local object=Target.Object --Wrapper.Group#GROUP
      
      coal=object:GetCoalition()
      
    end

  elseif Target.Type==TARGET.ObjectType.UNIT then

    if Target.Object and Target.Object:IsAlive()~=nil then
      local object=Target.Object --Wrapper.Unit#UNIT
      
      coal=object:GetCoalition()
      
    end
  
  elseif Target.Type==TARGET.ObjectType.STATIC then
    local object=Target.Object --Wrapper.Static#STATIC
    
    coal=object:GetCoalition()
    
  elseif Target.Type==TARGET.ObjectType.SCENERY then
  
    -- Scenery has no coalition.
      
  elseif Target.Type==TARGET.ObjectType.AIRBASE then
    local object=Target.Object --Wrapper.Airbase#AIRBASE
  
    coal=object:GetCoalition()
    
  elseif Target.Type==TARGET.ObjectType.COORDINATE then
  
    -- Coordinate has no coalition.

  elseif Target.Type==TARGET.ObjectType.ZONE then
  
    -- Zone has no coalition.
    
  elseif Target.Type==TARGET.ObjectType.OPSZONE then
    local object=Target.Object --Ops.OpsZone#OPSZONE
  
    coal=object:GetOwner()

  else
    self:E("ERROR: unknown target category!")
  end


  return coal
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get a target object by its name.
-- @param #TARGET self
-- @param #string ObjectName Object name.
-- @return #TARGET.Object The target object table or nil.
function TARGET:GetTargetByName(ObjectName)

  for _,_target in pairs(self.targets) do
    local target=_target --#TARGET.Object
    if ObjectName==target.Name then
      return target
    end
  end

  return nil
end


--- Get the first target objective alive.
-- @param #TARGET self
-- @param Core.Point#COORDINATE RefCoordinate (Optional) Reference coordinate to determine the closest target objective.
-- @param #table Coalitions (Optional) Only consider targets of the given coalition(s). 
-- @return #TARGET.Object The target objective.
function TARGET:GetObjective(RefCoordinate, Coalitions)

  if RefCoordinate then
  
    local dmin=math.huge
    local tmin=nil --#TARGET.Object
    
    for _,_target in pairs(self.targets) do
      local target=_target --#TARGET.Object
      
      if target.Status~=TARGET.ObjectStatus.DEAD and (Coalitions==nil or UTILS.IsInTable(UTILS.EnsureTable(Coalitions), self:GetTargetCoalition(target))) then
      
        local vec3=self:GetTargetVec3(target)
        
        local d=UTILS.VecDist3D(vec3, RefCoordinate)
        
        if d<dmin then
          dmin=d
          tmin=target
        end
      
      end
      
      
    end  
  
    return tmin  
  else

    for _,_target in pairs(self.targets) do
      local target=_target --#TARGET.Object
      if target.Status~=TARGET.ObjectStatus.DEAD and (Coalitions==nil or UTILS.IsInTable(UTILS.EnsureTable(Coalitions), self:GetTargetCoalition(target))) then
        return target
      end
    end
    
  end

  return nil
end

--- Get the first target object alive.
-- @param #TARGET self
-- @param Core.Point#COORDINATE RefCoordinate Reference coordinate to determine the closest target objective.
-- @param #table Coalitions (Optional) Only consider targets of the given coalition(s). 
-- @return Wrapper.Positionable#POSITIONABLE The target object or nil.
function TARGET:GetObject(RefCoordinate, Coalitions)

  local target=self:GetObjective(RefCoordinate, Coalitions)
  
  if target then
    return target.Object
  end

  return nil
end

--- Get all target objects.
-- @param #TARGET self
-- @return #table List of target objects.
function TARGET:GetObjects()
    local objects={}

    for _,_target in pairs(self.targets) do
        local target=_target --#TARGET.Object

        table.insert(objects, target.Object)
    end

    return objects
end

--- Count alive objects.
-- @param #TARGET self
-- @param #TARGET.Object Target Target objective.
-- @param #table Coalitions (Optional) Only count targets of the given coalition(s). 
-- @return #number Number of alive target objects.
function TARGET:CountObjectives(Target, Coalitions)

  local N=0

  if Target.Type==TARGET.ObjectType.GROUP then

    local target=Target.Object --Wrapper.Group#GROUP
    
    local units=target:GetUnits()
    
    for _,_unit in pairs(units or {}) do
      local unit=_unit --Wrapper.Unit#UNIT
      if unit and unit:IsAlive()~=nil and unit:GetLife()>1 then
        if Coalitions==nil or UTILS.IsInTable(UTILS.EnsureTable(Coalitions), unit:GetCoalition()) then
          N=N+1
        end
      end
    end

  elseif Target.Type==TARGET.ObjectType.UNIT then
  
    local target=Target.Object --Wrapper.Unit#UNIT        
    
    if target and target:IsAlive()~=nil and target:GetLife()>1 then
      if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetCoalition()) then    
        N=N+1
      end
    end
    
  elseif Target.Type==TARGET.ObjectType.STATIC then
  
    local target=Target.Object --Wrapper.Static#STATIC
    
    if target and target:IsAlive() then
      if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetCoalition()) then
        N=N+1
      end
    end

  elseif Target.Type==TARGET.ObjectType.SCENERY then
  
    if Target.Status~=TARGET.ObjectStatus.DEAD then
      N=N+1
    end
    
  elseif Target.Type==TARGET.ObjectType.AIRBASE then
  
    local target=Target.Object --Wrapper.Airbase#AIRBASE
  
    if Target.Status==TARGET.ObjectStatus.ALIVE then
      if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetCoalition()) then
        N=N+1
      end
    end
    
  elseif Target.Type==TARGET.ObjectType.COORDINATE then
  
    -- No target, where we can check the alive status, so we assume it is alive. Changed this because otherwise target count is 0 if we pass a coordinate.
    -- This is also more consitent with the life and is alive status.
    N=N+1

  elseif Target.Type==TARGET.ObjectType.ZONE then
  
    -- No target, where we can check the alive status, so we assume it is alive. Changed this because otherwise target count is 0 if we pass a coordinate.
    -- This is also more consitent with the life and is alive status.
    N=N+1
        
  elseif Target.Type==TARGET.ObjectType.OPSZONE then
    
    local target=Target.Object --Ops.OpsZone#OPSZONE
    
    if Coalitions==nil or UTILS.IsInTable(Coalitions, target:GetOwner()) then
      N=N+1
    end
    
  else
    self:E(self.lid.."ERROR: Unknown target type! Cannot count targets")
  end

  return N
end

--- Count alive targets.
-- @param #TARGET self
-- @param #table Coalitions (Optional) Only count targets of the given coalition(s). 
-- @return #number Number of alive target objects.
function TARGET:CountTargets(Coalitions)
  
  local N=0
  
  for _,_target in pairs(self.targets) do
    local Target=_target --#TARGET.Object
  
    N=N+self:CountObjectives(Target, Coalitions)
    
  end
  
  return N
end

--- Check if something is an element of the TARGET.
-- @param #TARGET self
-- @param #string Name The name of the potential element.
-- @return #boolean If `true`, this name is part of this TARGET.
function TARGET:IsElement(Name)

  if Name==nil then
    return false
  end

  for _,name in pairs(self.elements) do
    if name==Name then
      return true
    end
  end
  
  return false
end

--- Check if something is a a casualty of this TARGET.
-- @param #TARGET self
-- @param #string Name The name of the potential element.
-- @return #boolean If `true`, this name is a casualty of this TARGET.
function TARGET:IsCasualty(Name)

  if Name==nil then
    return false
  end

  for _,name in pairs(self.casualties) do
    if tostring(name)==tostring(Name) then
      return true
    end
  end
  
  return false
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------
-- Easy CAP/GCI Class, based on OPS classes
-------------------------------------------------------------------------
-- 
-- ## Documentation:
-- 
-- https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Ops.EasyGCICAP.html
-- 
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/EasyGCICAP).
-- 
-------------------------------------------------------------------------
-- Date: September 2023
-- Last Update: Aug 2025
-------------------------------------------------------------------------
--
--- **Ops** - Easy GCI & CAP Manager
--
-- ===
--
-- **Main Features:**
--
--    * Automatically create and manage A2A CAP/GCI defenses using an AirWing and Squadrons for one coalition
--    * Easy set-up
--    * Add additional AirWings on other airbases
--    * Each wing can have more than one Squadron - tasking to Squadrons is done on a random basis per AirWing
--    * Create borders and zones of engagement
--    * Detection can be ground based and/or via AWACS
--
-- ===
-- 
-- ### AUTHOR: **applevangelist**
--
-- @module Ops.EasyGCICAP
-- @image AI_Combat_Air_Patrol.JPG


--- EASYGCICAP Class
-- @type EASYGCICAP
-- @field #string ClassName
-- @field #number overhead
-- @field #number engagerange
-- @field #number capgrouping
-- @field #string airbasename
-- @field Wrapper.Airbase#AIRBASE airbase
-- @field #number coalition
-- @field #string alias
-- @field #table wings
-- @field Ops.Intel#INTEL Intel
-- @field #number resurrection
-- @field #number capspeed
-- @field #number capalt
-- @field #number capdir
-- @field #number capleg
-- @field #number maxinterceptsize
-- @field #number missionrange
-- @field #number noalert5
-- @field #table ManagedAW
-- @field #table ManagedSQ
-- @field #table ManagedCP
-- @field #table ManagedTK
-- @field #table ManagedEWR
-- @field #table ManagedREC
-- @field #number MaxAliveMissions
-- @field #boolean debug
-- @field #number repeatsonfailure
-- @field Core.Set#SET_ZONE GoZoneSet
-- @field Core.Set#SET_ZONE NoGoZoneSet
-- @field Core.Set#SET_ZONE ConflictZoneSet
-- @field #boolean Monitor
-- @field #boolean TankerInvisible
-- @field #number CapFormation
-- @field #table ReadyFlightGroups
-- @field #boolean DespawnAfterLanding
-- @field #boolean DespawnAfterHolding
-- @field #list<Ops.Auftrag#AUFTRAG> ListOfAuftrag
-- @field #string defaulttakeofftype Take off type
-- @field #number FuelLowThreshold
-- @field #number FuelCriticalThreshold
-- @field #boolean showpatrolpointmarks
-- @field #table EngageTargetTypes
-- @extends Core.Fsm#FSM

--- *“Airspeed, altitude, and brains. Two are always needed to successfully complete the flight.”* -- Unknown.
--
-- ===
--
-- # The EasyGCICAP Concept
-- 
-- The idea of this class is partially to make the OPS classes easier operational for an A2A CAP/GCI defense network, and to replace the legacy AI_A2A_Dispatcher system - not to it's
-- full extent, but make a basic system work very quickly.
--
-- # Setup
-- 
-- ## Basic understanding
-- 
-- The basics are, there is **one** and only **one** AirWing per airbase. Each AirWing has **at least** one Squadron, who will do both CAP and GCI tasks. Squadrons will be randomly chosen for the task at hand.
-- Each AirWing has **at least** one CAP Point that it manages. CAP Points will be covered by the AirWing automatically as long as airframes are available. Detected intruders will be assigned to **one**
-- AirWing based on proximity (that is, if you have more than one). 
-- 
-- ## Assignment of tasks for intruders
-- 
-- Either a CAP Plane or a newly spawned GCI plane will take care of the intruders. Standard overhead is 0.75, i.e. a group of 3 intrudes will
-- be managed by 2 planes from the assigned AirWing. There is an maximum missions limitation per AirWing, so we do not spam the skies.
-- 
-- ## Basic set-up code
-- 
-- ### Prerequisites
-- 
-- You have to put a **STATIC WAREHOUSE** object on the airbase with the UNIT name according to the name of the airbase. **Do not put any other static type or it creates a conflict with the airbase name!** 
-- E.g. for Kuitaisi this has to have the unit name Kutaisi. This object symbolizes the AirWing HQ.
-- Next put a late activated template group for your CAP/GCI Squadron on the map. Last, put a zone on the map for the CAP operations, let's name it "Blue Zone 1". Size of the zone plays no role.
-- Put an EW radar system on the map and name it aptly, like "Blue EWR".
-- 
-- ### Zones
-- 
-- For our example, you create a RED and a BLUE border, as a closed polygonal zone representing the borderlines. You can also have conflict zone, where - for our example - BLUE will attack
-- RED planes, despite being on RED territory. Think of a no-fly zone or an limited area of engagement. Conflict zones take precedence over borders, i.e. they can overlap all borders.
-- 
-- ### Code it
-- 
--          -- Set up a basic system for the blue side, we'll reside on Kutaisi, and use GROUP objects with "Blue EWR" in the name as EW Radar Systems.
--          local mywing = EASYGCICAP:New("Blue CAP Operations",AIRBASE.Caucasus.Kutaisi,"blue","Blue EWR")
--          
--          -- Add a CAP patrol point belonging to our airbase, we'll be at 30k ft doing 400 kn, initial direction 90 degrees (East), leg 20NM
--          mywing:AddPatrolPointCAP(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone 1"):GetCoordinate(),30000,400,90,20)
--          
--          -- Add a Squadron with template "Blue Sq1 M2000c", 20 airframes, skill good, Modex starting with 102 and skin "Vendee Jeanne"
--          mywing:AddSquadron("Blue Sq1 M2000c","CAP Kutaisi",AIRBASE.Caucasus.Kutaisi,20,AI.Skill.GOOD,102,"ec1.5_Vendee_Jeanne_clean")
--          
--          -- Add a couple of zones
--          -- We'll defend our own border
--          mywing:AddAcceptZone(ZONE_POLYGON:New( "Blue Border", GROUP:FindByName( "Blue Border" ) ))
--          -- We'll attack intruders also here - conflictzones can overlap borders(!) - limited zone of engagement
--          mywing:AddConflictZone(ZONE_POLYGON:New("Red Defense Zone", GROUP:FindByName( "Red Defense Zone" )))
--          -- We'll leave the reds alone on their turf
--          mywing:AddRejectZone(ZONE_POLYGON:New( "Red Border", GROUP:FindByName( "Red Border" ) ))
--          
--          -- Optional - Draw the borders on the map so we see what's going on
--          -- Set up borders on map
--          local BlueBorder = ZONE_POLYGON:New( "Blue Border", GROUP:FindByName( "Blue Border" ) )
--          BlueBorder:DrawZone(-1,{0,0,1},1,FillColor,FillAlpha,1,true)
--          local ConflictZone = ZONE_POLYGON:New("Red Defense Zone", GROUP:FindByName( "Red Defense Zone" ))
--          ConflictZone:DrawZone(-1,{1,1,0},1,FillColor,FillAlpha,2,true)
--          local BlueNoGoZone = ZONE_POLYGON:New( "Red Border", GROUP:FindByName( "Red Border" ) )
--          BlueNoGoZone:DrawZone(-1,{1,0,0},1,FillColor,FillAlpha,4,true)
--          
-- ### Add a second airwing with squads and own CAP point (optional)
--          
--          -- Set this up at Sukhumi
--          mywing:AddAirwing(AIRBASE.Caucasus.Sukhumi_Babushara,"Blue CAP Sukhumi")
--          -- CAP Point "Blue Zone 2"
--          mywing:AddPatrolPointCAP(AIRBASE.Caucasus.Sukhumi_Babushara,ZONE:FindByName("Blue Zone 2"):GetCoordinate(),30000,400,90,20)
--          
--          -- This one has two squadrons to choose from
--          mywing:AddSquadron("Blue Sq3 F16","CAP Sukhumi II",AIRBASE.Caucasus.Sukhumi_Babushara,20,AI.Skill.GOOD,402,"JASDF 6th TFS 43-8526 Skull Riders")
--          mywing:AddSquadron("Blue Sq2 F15","CAP Sukhumi I",AIRBASE.Caucasus.Sukhumi_Babushara,20,AI.Skill.GOOD,202,"390th Fighter SQN")
--          
-- ### Add a tanker (optional)
--        
--        -- **Note** If you need different tanker types, i.e. Boom and Drogue, set them up at different AirWings!
--        -- Add a tanker point
--        mywing:AddPatrolPointTanker(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone Tanker"):GetCoordinate(),20000,280,270,50)
--        -- Add a tanker squad - Radio 251 AM, TACAN 51Y
--        mywing:AddTankerSquadron("Blue Tanker","Tanker Ops Kutaisi",AIRBASE.Caucasus.Kutaisi,20,AI.Skill.EXCELLENT,602,nil,251,radio.modulation.AM,51)
--        
-- ### Add an AWACS (optional)
--        
--        -- Add an AWACS point
--        mywing:AddPatrolPointAwacs(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone AWACS"):GetCoordinate(),25000,300,270,50)
--        -- Add an AWACS squad - Radio 251 AM, TACAN 51Y
--        mywing:AddAWACSSquadron("Blue AWACS","AWACS Ops Kutaisi",AIRBASE.Caucasus.Kutaisi,20,AI.Skill.AVERAGE,702,nil,271,radio.modulation.AM)        
--
-- # Fine-Tuning
--
-- ## Change Defaults
-- 
-- * @{#EASYGCICAP.SetDefaultResurrection}: Set how many seconds the AirWing stays inoperable after the AirWing STATIC HQ ist destroyed, default 900 secs. 
-- * @{#EASYGCICAP.SetDefaultCAPSpeed}: Set how many knots the CAP flights should do (will be altitude corrected), default 300 kn.
-- * @{#EASYGCICAP.SetDefaultCAPAlt}: Set at which altitude (ASL) the CAP planes will fly, default 25,000 ft.
-- * @{#EASYGCICAP.SetDefaultCAPDirection}: Set the initial direction from the CAP point the planes will fly in degrees, default is 90°.
-- * @{#EASYGCICAP.SetDefaultCAPLeg}: Set the length of the CAP leg, default is 15 NM.
-- * @{#EASYGCICAP.SetDefaultCAPGrouping}: Set how many planes will be spawned per mission (CVAP/GCI), defaults to 2.
-- * @{#EASYGCICAP.SetDefaultMissionRange}: Set how many NM the planes can go from the home base, defaults to 100.
-- * @{#EASYGCICAP.SetDefaultNumberAlert5Standby}: Set how many planes will be spawned on cold standby (Alert5), default 2.
-- * @{#EASYGCICAP.SetDefaultEngageRange}: Set max engage range for CAP flights if they detect intruders, defaults to 50.
-- * @{#EASYGCICAP.SetMaxAliveMissions}: Set max parallel missions can be done (CAP+GCI+Alert5+Tanker+AWACS), defaults to 8.
-- * @{#EASYGCICAP.SetDefaultRepeatOnFailure}: Set max repeats on failure for intercepting/killing intruders, defaults to 3.
-- * @{#EASYGCICAP.SetTankerAndAWACSInvisible}: Set Tanker and AWACS to be invisible to enemy AI eyes. Is set to `true` by default.
-- 
-- ## Debug and Monitor
-- 
--          mywing.debug = true -- log information
--          mywing.Monitor = true -- show some statistics on screen
--
--
-- @field #EASYGCICAP
EASYGCICAP = {
  ClassName = "EASYGCICAP",
  overhead = 0.75,
  capgrouping = 2,
  airbasename = nil,
  airbase = nil,
  coalition = "blue",
  alias = nil,
  wings = {},
  Intel = nil,
  resurrection = 900,
  capspeed = 300,
  capalt = 25000,
  capdir = 45,
  capleg = 15,
  maxinterceptsize = 2,
  missionrange = 100,
  noalert5 = 4,
  ManagedAW = {},
  ManagedSQ = {},
  ManagedCP = {},
  ManagedTK = {},
  ManagedEWR = {},
  ManagedREC = {},
  MaxAliveMissions = 8,
  debug = false,
  engagerange = 50,
  repeatsonfailure = 3,
  GoZoneSet = nil,
  NoGoZoneSet = nil,
  ConflictZoneSet = nil,
  Monitor = false,
  TankerInvisible = true,
  CapFormation = nil,
  ReadyFlightGroups = {},
  DespawnAfterLanding = false,
  DespawnAfterHolding = true,
  ListOfAuftrag = {},
  defaulttakeofftype = "hot",
  FuelLowThreshold = 25,
  FuelCriticalThreshold = 10,
  showpatrolpointmarks = false,
  EngageTargetTypes = {"Air"},
}

--- Internal Squadron data type
-- @type EASYGCICAP.Squad
-- @field #string TemplateName
-- @field #string SquadName
-- @field #string AirbaseName
-- @field #number AirFrames
-- @field #string Skill
-- @field #string Modex
-- @field #string Livery
-- @field #boolean Tanker
-- @field #boolean AWACS
-- @field #boolean RECON
-- @field #number Frequency
-- @field #number Modulation
-- @field #number TACAN

--- Internal Wing data type
-- @type EASYGCICAP.Wing
-- @field #string AirbaseName
-- @field #string Alias
-- @field #string CapZoneName

--- Internal CapPoint data type
-- @type EASYGCICAP.CapPoint
-- @field #string AirbaseName
-- @field Core.Point#COORDINATE Coordinate
-- @field #number Altitude
-- @field #number Speed
-- @field #number Heading
-- @field #number LegLength
-- @field Core.Zone#ZONE_BASE Zone

--- EASYGCICAP class version.
-- @field #string version
EASYGCICAP.version="0.1.30"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO: TBD

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new GCICAP Manager
-- @param #EASYGCICAP self
-- @param #string Alias A Name for this GCICAP
-- @param #string AirbaseName Name of the Home Airbase
-- @param #string Coalition Coalition, e.g. "blue" or "red"
-- @param #string EWRName (Partial) group name of the EWR system of the coalition, e.g. "Red EWR", can be handed in as table of names, e.g.{"EWR","Radar","SAM"}
-- @return #EASYGCICAP self
function EASYGCICAP:New(Alias, AirbaseName, Coalition, EWRName)
  -- Inherit everything from FSM class.
  local self=BASE:Inherit(self, FSM:New()) -- #EASYGCICAP
  
  -- defaults
  self.alias = Alias or AirbaseName.." CAP Wing"
  self.coalitionname = string.lower(Coalition) or "blue"
  self.coalition = self.coalitionname == "blue" and coalition.side.BLUE or coalition.side.RED
  self.wings = {}
  if type(EWRName) == "string" then EWRName = {EWRName} end
  self.EWRName = EWRName --or self.coalitionname.." EWR"
  --self.CapZoneName = CapZoneName
  self.airbasename = AirbaseName
  self.airbase = AIRBASE:FindByName(self.airbasename)
  self.GoZoneSet = SET_ZONE:New()
  self.NoGoZoneSet = SET_ZONE:New()
  self.ConflictZoneSet = SET_ZONE:New()
  self.resurrection = 900
  self.capspeed = 300
  self.capalt = 25000
  self.capdir = 90
  self.capleg = 15
  self.capgrouping = 2
  self.missionrange = 100
  self.noalert5 = 2
  self.MaxAliveMissions = 8
  self.engagerange = 50
  self.repeatsonfailure = 3
  self.Monitor = false
  self.TankerInvisible = true
  self.CapFormation = ENUMS.Formation.FixedWing.FingerFour.Group
  self.DespawnAfterLanding = false
  self.DespawnAfterHolding = true
  self.ListOfAuftrag = {}
  self.defaulttakeofftype = "hot"
  self.FuelLowThreshold = 25
  self.FuelCriticalThreshold = 10
  self.showpatrolpointmarks = false
  self.EngageTargetTypes = {"Air"}
  
  -- Set some string id for output to DCS.log file.
  self.lid=string.format("EASYGCICAP %s | ", self.alias)

  -- Add FSM transitions.
  --               From State  -->   Event      -->      To State
  self:SetStartState("Stopped")
  self:AddTransition("Stopped", "Start",  "Running")
  self:AddTransition("Running", "Stop",   "Stopped")
  self:AddTransition("*",       "Status", "*")  
  
  self:AddAirwing(self.airbasename,self.alias,self.CapZoneName)
  
  self:I(self.lid.."Created new instance (v"..self.version..")")
  
  self:__Start(math.random(6,12))
  
  return self
end

-------------------------------------------------------------------------
-- Functions
-------------------------------------------------------------------------

--- Get a specific managed AirWing by name
-- @param #EASYGCICAP self
-- @param #string AirbaseName Airbase name of the home of this wing.
-- @return Ops.AirWing#AIRWING Airwing or nil if not found
function EASYGCICAP:GetAirwing(AirbaseName)
  self:T(self.lid.."GetAirwing")
  if self.wings[AirbaseName] then
    return self.wings[AirbaseName][1]
  end
  return nil
end

--- Get a table of all managed AirWings
-- @param #EASYGCICAP self
-- @return #table Table of Ops.AirWing#AIRWING Airwings
function EASYGCICAP:GetAirwingTable()
  self:T(self.lid.."GetAirwingTable")
  local Wingtable = {}
  for _,_object in pairs(self.wings or {}) do
    table.insert(Wingtable,_object[1])
  end
  return Wingtable
end

--- Set "fuel low" threshold for CAP and INTERCEPT flights.
-- @param #EASYGCICAP self
-- @param #number Percent RTB if fuel at this percent. Values: 1..100, defaults to 25.
-- @return #EASYGCICAP self
function EASYGCICAP:SetFuelLow(Percent)
  self:T(self.lid.."SetFuelLow")
  self.FuelLowThreshold = Percent or 25
  return self
end

--- Set markers on the map for Patrol Points.
-- @param #EASYGCICAP self
-- @param #boolean onoff Set to true to switch markers on.
-- @return #EASYGCICAP self
function EASYGCICAP:ShowPatrolPointMarkers(onoff)
  if onoff then
    self.showpatrolpointmarks = true
  else
    self.showpatrolpointmarks = false
  end
  return self
end

--- Set "fuel critical" threshold for CAP and INTERCEPT flights.
-- @param #EASYGCICAP self
-- @param #number Percent RTB if fuel at this percent. Values: 1..100, defaults to 10.
-- @return #EASYGCICAP self
function EASYGCICAP:SetFuelCritical(Percent)
  self:T(self.lid.."SetFuelCritical")
  self.FuelCriticalThreshold = Percent or 10
  return self
end

--- Set CAP formation.
-- @param #EASYGCICAP self
-- @param #number Formation Formation to fly, defaults to ENUMS.Formation.FixedWing.FingerFour.Group
-- @return #EASYGCICAP self
function EASYGCICAP:SetCAPFormation(Formation)
  self:T(self.lid.."SetCAPFormation")
  self.CapFormation = Formation
  return self
end

--- Set Tanker and AWACS to be invisible to enemy AI eyes
-- @param #EASYGCICAP self
-- @param #boolean Switch Set to true or false, by default this is set to true already
-- @return #EASYGCICAP self 
function EASYGCICAP:SetTankerAndAWACSInvisible(Switch)
  self:T(self.lid.."SetTankerAndAWACSInvisible")
  self.TankerInvisible = Switch
  return self
end

--- (internal) Count alive missions in our internal stack.
-- @param #EASYGCICAP self
-- @return #number count
function EASYGCICAP:_CountAliveAuftrags()
  local alive = 0
  for _,_auftrag in pairs(self.ListOfAuftrag) do
    local auftrag = _auftrag -- Ops.Auftrag#AUFTRAG
    if auftrag and (not (auftrag:IsCancelled() or auftrag:IsDone() or auftrag:IsOver())) then
       alive = alive + 1
    end
  end
  return alive
end

--- Set Maximum of alive missions created by this instance to stop airplanes spamming the map
-- @param #EASYGCICAP self
-- @param #number Maxiumum Maxmimum number of parallel missions allowed. Count is Intercept-Missions + Alert5-Missions, default is 8
-- @return #EASYGCICAP self 
function EASYGCICAP:SetMaxAliveMissions(Maxiumum)
  self:T(self.lid.."SetMaxAliveMissions")
  self.MaxAliveMissions = Maxiumum or 8
  return self
end

--- Add default time to resurrect Airwing building if destroyed
-- @param #EASYGCICAP self
-- @param #number Seconds Seconds, defaults to 900
-- @return #EASYGCICAP self 
function EASYGCICAP:SetDefaultResurrection(Seconds)
  self:T(self.lid.."SetDefaultResurrection")
  self.resurrection = Seconds or 900
  return self
end

--- Add default repeat attempts if an Intruder intercepts fails.
-- @param #EASYGCICAP self
-- @param #number Retries Retries, defaults to 3
-- @return #EASYGCICAP self 
function EASYGCICAP:SetDefaultRepeatOnFailure(Retries)
  self:T(self.lid.."SetDefaultRepeatOnFailure")
  self.repeatsonfailure = Retries or 3
  return self
end

--- Add default take off type for the airwings.
-- @param #EASYGCICAP self
-- @param #string Takeoff Can be "hot", "cold", or "air" - default is "hot".
-- @return #EASYGCICAP self 
function EASYGCICAP:SetDefaultTakeOffType(Takeoff)
  self:T(self.lid.."SetDefaultTakeOffType")
  self.defaulttakeofftype = Takeoff or "hot"
  return self
end

--- Set default CAP Speed in knots
-- @param #EASYGCICAP self
-- @param #number Speed Speed defaults to 300
-- @return #EASYGCICAP self
function EASYGCICAP:SetDefaultCAPSpeed(Speed)
  self:T(self.lid.."SetDefaultSpeed")
  self.capspeed = Speed or 300
  return self
end

--- Set default CAP Altitude in feet
-- @param #EASYGCICAP self
-- @param #number Altitude Altitude defaults to 25000
-- @return #EASYGCICAP self
function EASYGCICAP:SetDefaultCAPAlt(Altitude)
  self:T(self.lid.."SetDefaultAltitude")
  self.capalt = Altitude or 25000
  return self
end

--- Set default CAP lieg initial direction in degrees
-- @param #EASYGCICAP self
-- @param #number Direction Direction defaults to 90 (East)
-- @return #EASYGCICAP self
function EASYGCICAP:SetDefaultCAPDirection(Direction)
  self:T(self.lid.."SetDefaultDirection")
  self.capdir = Direction or 90
  return self
end

--- Set default leg length in NM
-- @param #EASYGCICAP self
-- @param #number Leg Leg defaults to 15
-- @return #EASYGCICAP self
function EASYGCICAP:SetDefaultCAPLeg(Leg)
  self:T(self.lid.."SetDefaultLeg")
 self.capleg = Leg or 15
 return self
end

--- Set default grouping, i.e. how many airplanes per CAP point
-- @param #EASYGCICAP self
-- @param #number Grouping Grouping defaults to 2
-- @return #EASYGCICAP self
function EASYGCICAP:SetDefaultCAPGrouping(Grouping)
 self:T(self.lid.."SetDefaultCAPGrouping")
 self.capgrouping = Grouping or 2
 return self
end

--- Set default range planes can fly from their homebase in NM
-- @param #EASYGCICAP self
-- @param #number Range Range defaults to 100 NM
-- @return #EASYGCICAP self
function EASYGCICAP:SetDefaultMissionRange(Range)
  self:T(self.lid.."SetDefaultMissionRange")
  self.missionrange = Range or 100
  return self
end

--- Set default number of airframes standing by for intercept tasks (visible on the airfield)
-- @param #EASYGCICAP self
-- @param #number Airframes defaults to 2
-- @return #EASYGCICAP self
function EASYGCICAP:SetDefaultNumberAlert5Standby(Airframes)
  self:T(self.lid.."SetDefaultNumberAlert5Standby")
  self.noalert5 = math.abs(Airframes) or 2
  return self
end

--- Set default engage range for intruders detected by CAP flights in NM.
-- @param #EASYGCICAP self
-- @param #number Range defaults to 50NM
-- @return #EASYGCICAP self
function EASYGCICAP:SetDefaultEngageRange(Range)
  self:T(self.lid.."SetDefaultEngageRange")
  self.engagerange = Range or 50
  return self
end

--- Set default overhead for intercept calculations
-- @param #EASYGCICAP self
-- @param #number Overhead The overhead to use.
-- @return #EASYGCICAP self
-- @usage Either a CAP Plane or a newly spawned GCI plane will take care of intruders. Standard overhead is 0.75, i.e. a group of 3 intrudes will
-- be managed by 2 planes from the assigned AirWing. There is an maximum missions limitation per AirWing, so we do not spam the skies.
function EASYGCICAP:SetDefaultOverhead(Overhead)
  self:T(self.lid.."SetDefaultOverhead")
  self.overhead = Overhead or 0.75
  return self
end

--- Set default despawning after landing.
-- @param #EASYGCICAP self
-- @return #EASYGCICAP self
function EASYGCICAP:SetDefaultDespawnAfterLanding()
  self:T(self.lid.."SetDefaultDespawnAfterLanding")
  self.DespawnAfterLanding = true
  self.DespawnAfterHolding = false
  return self
end

--- Set default despawning after holding (despawn in air close to AFB).
-- @param #EASYGCICAP self
-- @return #EASYGCICAP self
function EASYGCICAP:SetDefaultDespawnAfterHolding()
  self:T(self.lid.."SetDefaultDespawnAfterLanding")
  self.DespawnAfterLanding = false
  self.DespawnAfterHolding = true
  return self
end

--- Set CAP mission start to vary randomly between Start end End seconds.
-- @param #EASYGCICAP self
-- @param #number Start
-- @param #number End 
-- @return #EASYGCICAP self
function EASYGCICAP:SetCapStartTimeVariation(Start, End)
  self.capOptionVaryStartTime = Start or 5
  self.capOptionVaryEndTime = End or 60
  return self
end


--- Set which target types CAP flights will prefer to engage, defaults to {"Air"}
-- @param #EASYGCICAP self
-- @param #table types Table of comma separated #string entries, defaults to {"Air"} (everything that flies and is not a weapon). Useful other options are e.g. {"Bombers"}, {"Fighters"}, 
-- or {"Helicopters"} or combinations like {"Bombers", "Fighters", "UAVs"}. See [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_attributes).
-- @return #EASYGCICAP self
function EASYGCICAP:SetCAPEngageTargetTypes(types)
  self.EngageTargetTypes = types or {"Air"}
  return self
end

--- Add an AirWing to the manager
-- @param #EASYGCICAP self
-- @param #string Airbasename
-- @param #string Alias
-- @return #EASYGCICAP self 
function EASYGCICAP:AddAirwing(Airbasename, Alias)
  self:T(self.lid.."AddAirwing "..Airbasename)
  
  -- Create Airwing data entry
  local AWEntry = {} -- #EASYGCICAP.Wing
  AWEntry.AirbaseName = Airbasename
  AWEntry.Alias = Alias
  --AWEntry.CapZoneName = CapZoneName
  
  self.ManagedAW[Airbasename] = AWEntry
  
  return self  
end

--- (Internal) Create actual AirWings from the list
-- @param #EASYGCICAP self
-- @return #EASYGCICAP self 
function EASYGCICAP:_CreateAirwings()
  self:T(self.lid.."_CreateAirwings")
  for airbase,data in pairs(self.ManagedAW) do
    local wing = data -- #EASYGCICAP.Wing
    local afb = wing.AirbaseName
    local alias = wing.Alias
    --local cz = wing.CapZoneName
    self:_AddAirwing(airbase,alias)
  end
  return self
end

--- (internal) Create and add another AirWing to the manager
-- @param #EASYGCICAP self
-- @param #string Airbasename
-- @param #string Alias
-- @return #EASYGCICAP self 
function EASYGCICAP:_AddAirwing(Airbasename, Alias)
  self:T(self.lid.."_AddAirwing "..Airbasename)
  
  local CapFormation = self.CapFormation
  local DespawnAfterLanding = self.DespawnAfterLanding
  local DespawnAfterHolding = self.DespawnAfterHolding
  
  -- Check STATIC name
  local check = STATIC:FindByName(Airbasename,false) or UNIT:FindByName(Airbasename)
  if check == nil then
    MESSAGE:New(self.lid.."There's no warehouse static on the map (wrong naming?) for airbase "..tostring(Airbasename).."!",30,"CHECK"):ToAllIf(self.debug):ToLog()
    return
  end
  
  -- Create Airwing
  local CAP_Wing = AIRWING:New(Airbasename,Alias)
  CAP_Wing:SetVerbosityLevel(0)
  CAP_Wing:SetReportOff()
  CAP_Wing:SetMarker(false)
  CAP_Wing:SetAirbase(AIRBASE:FindByName(Airbasename))
  CAP_Wing:SetRespawnAfterDestroyed()
  CAP_Wing:SetNumberCAP(self.capgrouping)
  CAP_Wing:SetCapCloseRaceTrack(true)
    
  if self.showpatrolpointmarks then
    CAP_Wing:ShowPatrolPointMarkers(true)
  end
  
  if self.capOptionVaryStartTime then
    CAP_Wing:SetCapStartTimeVariation(self.capOptionVaryStartTime,self.capOptionVaryEndTime)
  end
  
  if CapFormation then
    CAP_Wing:SetCAPFormation(CapFormation)
  end
  if #self.ManagedTK > 0 then
    CAP_Wing:SetNumberTankerBoom(1)
    CAP_Wing:SetNumberTankerProbe(1)
  end
  if #self.ManagedEWR > 0 then
    CAP_Wing:SetNumberAWACS(1)
  end
  if #self.ManagedREC > 0 then
    CAP_Wing:SetNumberRecon(1)
  end

  CAP_Wing:SetTakeoffType(self.defaulttakeofftype)
  CAP_Wing:SetLowFuelThreshold(0.3)
  CAP_Wing.RandomAssetScore = math.random(50,100)
  CAP_Wing:Start()

  local Intel = self.Intel
  
  local TankerInvisible = self.TankerInvisible
  local engagerange = self.engagerange
  local GoZoneSet = self.GoZoneSet
  local NoGoZoneSet = self.NoGoZoneSet
  local FuelLow = self.FuelLowThreshold or 25
  local FuelCritical = self.FuelCriticalThreshold or 10
  local EngageTypes = self.EngageTargetTypes or {"Air"}
  
  function CAP_Wing:onbeforeFlightOnMission(From, Event, To, Flightgroup, Mission)
    local flightgroup = Flightgroup -- Ops.FlightGroup#FLIGHTGROUP
    if DespawnAfterLanding then
      flightgroup:SetDespawnAfterLanding()
    elseif DespawnAfterHolding then
      flightgroup:SetDespawnAfterHolding()
    end
    flightgroup:SetDestinationbase(AIRBASE:FindByName(Airbasename))
    flightgroup:GetGroup():CommandEPLRS(true,5)
    flightgroup:GetGroup():SetOptionRadarUsingForContinousSearch()
    flightgroup:GetGroup():SetOptionLandingOverheadBreak()
    if Mission.type ~= AUFTRAG.Type.TANKER and Mission.type ~= AUFTRAG.Type.AWACS and Mission.type ~= AUFTRAG.Type.RECON then
      flightgroup:SetDetection(true)
      flightgroup:SetEngageDetectedOn(engagerange,EngageTypes,GoZoneSet,NoGoZoneSet)
      flightgroup:SetOutOfAAMRTB()
      flightgroup:SetFuelLowRTB(true)
      flightgroup:SetFuelLowThreshold(FuelLow)
      flightgroup:SetFuelCriticalRTB(true)
      flightgroup:SetFuelCriticalThreshold(FuelCritical)
      if CapFormation then
        flightgroup:GetGroup():SetOption(AI.Option.Air.id.FORMATION,CapFormation)
      end
    end
    if Mission.type == AUFTRAG.Type.TANKER or Mission.type == AUFTRAG.Type.AWACS or Mission.type == AUFTRAG.Type.RECON then
      if TankerInvisible then
        flightgroup:GetGroup():SetCommandInvisible(true)
      end
      if Mission.type == AUFTRAG.Type.RECON then
        flightgroup:SetDetection(true)
      end
    end
    flightgroup:GetGroup():OptionROTEvadeFire()   
    flightgroup:SetFuelLowRTB(true)
    Intel:AddAgent(flightgroup)
    if DespawnAfterHolding then
      function flightgroup:onbeforeHolding(From,Event,To)
        self:Despawn(1,true)
      end 
    end
  end
  
  if self.noalert5 > 0 then  
    local alert = AUFTRAG:NewALERT5(AUFTRAG.Type.INTERCEPT) 
    alert:SetRequiredAssets(self.noalert5)
    alert:SetRepeat(99) 
    CAP_Wing:AddMission(alert)
    table.insert(self.ListOfAuftrag,alert)
  end
    
  self.wings[Airbasename] = { CAP_Wing, AIRBASE:FindByName(Airbasename):GetZone(), Airbasename }
  
  return self 
end

--- Add a CAP patrol point to a Wing
-- @param #EASYGCICAP self
-- @param #string AirbaseName Name of the Wing's airbase
-- @param Core.Point#COORDINATE Coordinate. Can be handed as a Core.Zone#ZONE object (e.g. in case you want  the point to align with a moving zone).
-- @param #number Altitude Defaults to 25000 feet ASL.
-- @param #number Speed  Defaults to 300 knots TAS.
-- @param #number Heading Defaults to 90 degrees (East).
-- @param #number LegLength Defaults to 15 NM.
-- @return #EASYGCICAP self
function EASYGCICAP:AddPatrolPointCAP(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength)
  self:T(self.lid.."AddPatrolPointCAP")--..Coordinate:ToStringLLDDM())
  local coordinate = Coordinate
  local EntryCAP = {} -- #EASYGCICAP.CapPoint  
  if Coordinate:IsInstanceOf("ZONE_BASE") then
    -- adjust coordinate and get the coordinate from the zone
    coordinate = Coordinate:GetCoordinate()
    EntryCAP.Zone = Coordinate
  end
  EntryCAP.AirbaseName = AirbaseName
  EntryCAP.Coordinate = coordinate
  EntryCAP.Altitude = Altitude or 25000
  EntryCAP.Speed = Speed or 300
  EntryCAP.Heading = Heading or 90
  EntryCAP.LegLength = LegLength or 15
  self.ManagedCP[#self.ManagedCP+1] = EntryCAP
  if self.debug then
    local mark = MARKER:New(coordinate,self.lid.."Patrol Point"):ToAll()
  end
  return self
end

--- Add a RECON patrol point to a Wing
-- @param #EASYGCICAP self
-- @param #string AirbaseName Name of the Wing's airbase
-- @param Core.Point#COORDINATE Coordinate. Can be handed as a Core.Zone#ZONE object (e.g. in case you want  the point to align with a moving zone).
-- @param #number Altitude Defaults to 25000 feet.
-- @param #number Speed  Defaults to 300 knots.
-- @param #number Heading Defaults to 90 degrees (East).
-- @param #number LegLength Defaults to 15 NM.
-- @return #EASYGCICAP self
function EASYGCICAP:AddPatrolPointRecon(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength)
  self:T(self.lid.."AddPatrolPointRecon "..Coordinate:ToStringLLDDM())
  local EntryCAP = {} -- #EASYGCICAP.CapPoint
  EntryCAP.AirbaseName = AirbaseName
  EntryCAP.Coordinate = Coordinate
  EntryCAP.Altitude = Altitude or 25000
  EntryCAP.Speed = Speed or 300
  EntryCAP.Heading = Heading or 90
  EntryCAP.LegLength = LegLength or 15
  self.ManagedREC[#self.ManagedREC+1] = EntryCAP
  if self.debug then
    local mark = MARKER:New(Coordinate,self.lid.."Patrol Point Recon"):ToAll()
  end
  return self
end

--- Add a TANKER patrol point to a Wing
-- @param #EASYGCICAP self
-- @param #string AirbaseName Name of the Wing's airbase
-- @param Core.Point#COORDINATE Coordinate. Can be handed as a Core.Zone#ZONE object (e.g. in case you want  the point to align with a moving zone).
-- @param #number Altitude Defaults to 25000 feet.
-- @param #number Speed  Defaults to 300 knots.
-- @param #number Heading Defaults to 90 degrees (East).
-- @param #number LegLength Defaults to 15 NM.
-- @return #EASYGCICAP self
function EASYGCICAP:AddPatrolPointTanker(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength)
  self:T(self.lid.."AddPatrolPointTanker "..Coordinate:ToStringLLDDM())
  local EntryCAP = {} -- #EASYGCICAP.CapPoint
  EntryCAP.AirbaseName = AirbaseName
  EntryCAP.Coordinate = Coordinate
  EntryCAP.Altitude = Altitude or 25000
  EntryCAP.Speed = Speed or 300
  EntryCAP.Heading = Heading or 90
  EntryCAP.LegLength = LegLength or 15
  self.ManagedTK[#self.ManagedTK+1] = EntryCAP
  if self.debug then
    local mark = MARKER:New(Coordinate,self.lid.."Patrol Point Tanker"):ToAll()
  end
  return self
end

--- Add an AWACS patrol point to a Wing
-- @param #EASYGCICAP self
-- @param #string AirbaseName Name of the Wing's airbase
-- @param Core.Point#COORDINATE Coordinate. Can be handed as a Core.Zone#ZONE object (e.g. in case you want  the point to align with a moving zone).
-- @param #number Altitude Defaults to 25000 feet.
-- @param #number Speed  Defaults to 300 knots.
-- @param #number Heading Defaults to 90 degrees (East).
-- @param #number LegLength Defaults to 15 NM.
-- @return #EASYGCICAP self
function EASYGCICAP:AddPatrolPointAwacs(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength)
  self:T(self.lid.."AddPatrolPointAwacs "..Coordinate:ToStringLLDDM())
  local EntryCAP = {} -- #EASYGCICAP.CapPoint
  EntryCAP.AirbaseName = AirbaseName
  EntryCAP.Coordinate = Coordinate
  EntryCAP.Altitude = Altitude or 25000
  EntryCAP.Speed = Speed or 300
  EntryCAP.Heading = Heading or 90
  EntryCAP.LegLength = LegLength or 15
  self.ManagedEWR[#self.ManagedEWR+1] = EntryCAP
  if self.debug then
    local mark = MARKER:New(Coordinate,self.lid.."Patrol Point AWACS"):ToAll()
  end
  return self
end

--- (Internal) Set actual Tanker Points from the list
-- @param #EASYGCICAP self
-- @return #EASYGCICAP self 
function EASYGCICAP:_SetTankerPatrolPoints()
  self:T(self.lid.."_SetTankerPatrolPoints")
  for _,_data in pairs(self.ManagedTK) do
    local data = _data --#EASYGCICAP.CapPoint
    self:T("Airbasename = "..data.AirbaseName)
    if not self.wings[data.AirbaseName] then
      MESSAGE:New(self.lid.."You are trying to create a TANKER point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog()
      return
    end
    local Wing = self.wings[data.AirbaseName][1] -- Ops.Airwing#AIRWING
    local Coordinate = data.Coordinate
    local Altitude = data.Altitude
    local Speed = data.Speed 
    local Heading = data.Heading
    local LegLength = data.LegLength
    Wing:AddPatrolPointTANKER(Coordinate,Altitude,Speed,Heading,LegLength)
  end
  
  return self
end

--- (Internal) Set actual Awacs Points from the list
-- @param #EASYGCICAP self
-- @return #EASYGCICAP self 
function EASYGCICAP:_SetAwacsPatrolPoints()
  self:T(self.lid.."_SetAwacsPatrolPoints")
  for _,_data in pairs(self.ManagedEWR) do
    local data = _data --#EASYGCICAP.CapPoint
    self:T("Airbasename = "..data.AirbaseName)
    if not self.wings[data.AirbaseName] then
      MESSAGE:New(self.lid.."You are trying to create an AWACS point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog()
      return
    end
    local Wing = self.wings[data.AirbaseName][1] -- Ops.Airwing#AIRWING
    local Coordinate = data.Coordinate
    local Altitude = data.Altitude
    local Speed = data.Speed 
    local Heading = data.Heading
    local LegLength = data.LegLength
    Wing:AddPatrolPointAWACS(Coordinate,Altitude,Speed,Heading,LegLength)
  end
  
  return self
end

--- (Internal) Set actual PatrolPoints from the list
-- @param #EASYGCICAP self
-- @return #EASYGCICAP self 
function EASYGCICAP:_SetCAPPatrolPoints()
  self:T(self.lid.."_SetCAPPatrolPoints")
  for _,_data in pairs(self.ManagedCP) do
    local data = _data --#EASYGCICAP.CapPoint
    self:T("Airbasename = "..data.AirbaseName)
    if not self.wings[data.AirbaseName] then
      MESSAGE:New(self.lid.."You are trying to create a CAP point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog()
      return
    end
    local Wing = self.wings[data.AirbaseName][1] -- Ops.Airwing#AIRWING
    local Coordinate = data.Coordinate
    local Altitude = data.Altitude
    local Speed = data.Speed 
    local Heading = data.Heading
    local LegLength = data.LegLength
    local Zone = _data.Zone
    if Zone then
        Wing:AddPatrolPointCAP(Zone,Altitude,Speed,Heading,LegLength)
    else
        Wing:AddPatrolPointCAP(Coordinate,Altitude,Speed,Heading,LegLength)
    end
  end
  
  return self
end

--- (Internal) Set actual PatrolPoints from the list
-- @param #EASYGCICAP self
-- @return #EASYGCICAP self 
function EASYGCICAP:_SetReconPatrolPoints()
  self:T(self.lid.."_SetReconPatrolPoints")
  for _,_data in pairs(self.ManagedREC) do
    local data = _data --#EASYGCICAP.CapPoint
    self:T("Airbasename = "..data.AirbaseName)
    if not self.wings[data.AirbaseName] then
      MESSAGE:New(self.lid.."You are trying to create a RECON point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog()
      return
    end
    local Wing = self.wings[data.AirbaseName][1] -- Ops.Airwing#AIRWING
    local Coordinate = data.Coordinate
    local Altitude = data.Altitude
    local Speed = data.Speed 
    local Heading = data.Heading
    local LegLength = data.LegLength
    Wing:AddPatrolPointRecon(Coordinate,Altitude,Speed,Heading,LegLength)
  end
  
  return self
end

--- (Internal) Create actual Squadrons from the list
-- @param #EASYGCICAP self
-- @return #EASYGCICAP self 
function EASYGCICAP:_CreateSquads()
  self:T(self.lid.."_CreateSquads")
  for name,data in pairs(self.ManagedSQ) do
    local squad = data -- #EASYGCICAP.Squad
    local SquadName = name
    local TemplateName = squad.TemplateName
    local AirbaseName = squad.AirbaseName
    local AirFrames = squad.AirFrames
    local Skill = squad.Skill
    local Modex = squad.Modex
    local Livery = squad.Livery
    local Frequeny = squad.Frequency
    local Modulation = squad.Modulation
    local TACAN = squad.TACAN
    if squad.Tanker then
      self:_AddTankerSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery,Frequeny,Modulation,TACAN)
    elseif squad.AWACS then
      self:_AddAWACSSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery,Frequeny,Modulation)
    elseif squad.RECON then
      self:_AddReconSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery)
    else
      self:_AddSquadron(TemplateName,SquadName,AirbaseName,AirFrames,Skill,Modex,Livery)
    end
  end
  return self
end

--- Add a Squadron to an Airwing of the manager
-- @param #EASYGCICAP self
-- @param #string TemplateName Name of the group template.
-- @param #string SquadName Squadron name - must be unique!
-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi
-- @param #number AirFrames Number of available airframes, e.g. 20.
-- @param #string Skill (optional) Skill level, e.g. AI.Skill.AVERAGE
-- @param #string Modex (optional) Modex to be used,e.g. 402.
-- @param #string Livery (optional) Livery name to be used.
-- @return #EASYGCICAP self 
function EASYGCICAP:AddSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery)
  self:T(self.lid.."AddSquadron "..SquadName)
  -- Add Squadron Data
  local EntrySQ = {} -- #EASYGCICAP.Squad
  EntrySQ.TemplateName = TemplateName
  EntrySQ.SquadName = SquadName
  EntrySQ.AirbaseName = AirbaseName
  EntrySQ.AirFrames = AirFrames or 20
  EntrySQ.Skill = Skill or AI.Skill.AVERAGE
  EntrySQ.Modex = Modex or 402
  EntrySQ.Livery = Livery
  
  self.ManagedSQ[SquadName] = EntrySQ
  
  return self
end

--- Add a Recon Squadron to an Airwing of the manager
-- @param #EASYGCICAP self
-- @param #string TemplateName Name of the group template.
-- @param #string SquadName Squadron name - must be unique!
-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi
-- @param #number AirFrames Number of available airframes, e.g. 20.
-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE
-- @param #string Modex (optional) Modex to be used,e.g. 402.
-- @param #string Livery (optional) Livery name to be used.
-- @return #EASYGCICAP self 
function EASYGCICAP:AddReconSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery)
  self:T(self.lid.."AddReconSquadron "..SquadName)
  -- Add Squadron Data
  local EntrySQ = {} -- #EASYGCICAP.Squad
  EntrySQ.TemplateName = TemplateName
  EntrySQ.SquadName = SquadName
  EntrySQ.AirbaseName = AirbaseName
  EntrySQ.AirFrames = AirFrames or 20
  EntrySQ.Skill = Skill or AI.Skill.AVERAGE
  EntrySQ.Modex = Modex or 402
  EntrySQ.Livery = Livery
  EntrySQ.RECON = true
    
  self.ManagedSQ[SquadName] = EntrySQ
  
  return self
end

--- Add a Tanker Squadron to an Airwing of the manager
-- @param #EASYGCICAP self
-- @param #string TemplateName Name of the group template.
-- @param #string SquadName Squadron name - must be unique!
-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi
-- @param #number AirFrames Number of available airframes, e.g. 20.
-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE
-- @param #string Modex (optional) Modex to be used,e.g. 402.
-- @param #string Livery (optional) Livery name to be used.
-- @param #number Frequency (optional) Radio Frequency to be used. 
-- @param #number Modulation (optional) Radio Modulation to be used, e.g. radio.modulation.AM or radio.modulation.FM
-- @param #number TACAN (optional)  TACAN channel, e.g. 71, resulting in Channel 71Y
-- @return #EASYGCICAP self 
function EASYGCICAP:AddTankerSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery, Frequency, Modulation, TACAN)
  self:T(self.lid.."AddTankerSquadron "..SquadName)
  -- Add Squadron Data
  local EntrySQ = {} -- #EASYGCICAP.Squad
  EntrySQ.TemplateName = TemplateName
  EntrySQ.SquadName = SquadName
  EntrySQ.AirbaseName = AirbaseName
  EntrySQ.AirFrames = AirFrames or 20
  EntrySQ.Skill = Skill or AI.Skill.AVERAGE
  EntrySQ.Modex = Modex or 602
  EntrySQ.Livery = Livery
  EntrySQ.Frequency = Frequency
  EntrySQ.Modulation = Livery
  EntrySQ.TACAN = TACAN
  EntrySQ.Tanker = true
  
  self.ManagedSQ[SquadName] = EntrySQ
  
  return self
end

--- Add an AWACS Squadron to an Airwing of the manager
-- @param #EASYGCICAP self
-- @param #string TemplateName Name of the group template.
-- @param #string SquadName Squadron name - must be unique!
-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi
-- @param #number AirFrames Number of available airframes, e.g. 20.
-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE
-- @param #string Modex (optional) Modex to be used,e.g. 402.
-- @param #string Livery (optional) Livery name to be used.
-- @param #number Frequency (optional) Radio Frequency to be used. 
-- @param #number Modulation (optional) Radio Modulation to be used, e.g. radio.modulation.AM or radio.modulation.FM
-- @return #EASYGCICAP self 
function EASYGCICAP:AddAWACSSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery, Frequency, Modulation)
  self:T(self.lid.."AddAWACSSquadron "..SquadName)
  -- Add Squadron Data
  local EntrySQ = {} -- #EASYGCICAP.Squad
  EntrySQ.TemplateName = TemplateName
  EntrySQ.SquadName = SquadName
  EntrySQ.AirbaseName = AirbaseName
  EntrySQ.AirFrames = AirFrames or 20
  EntrySQ.Skill = Skill or AI.Skill.AVERAGE
  EntrySQ.Modex = Modex or 702
  EntrySQ.Livery = Livery
  EntrySQ.Frequency = Frequency
  EntrySQ.Modulation = Livery
  EntrySQ.AWACS = true
  
  self.ManagedSQ[SquadName] = EntrySQ
  
  return self
end

--- (Internal) Add a Squadron to an Airwing of the manager
-- @param #EASYGCICAP self
-- @param #string TemplateName Name of the group template.
-- @param #string SquadName Squadron name - must be unique!
-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi
-- @param #number AirFrames Number of available airframes, e.g. 20.
-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE
-- @param #string Modex (optional) Modex to be used,e.g. 402.
-- @param #string Livery (optional) Livery name to be used.
-- @param #number Frequency (optional) Radio Frequency to be used. 
-- @param #number Modulation (optional) Radio Modulation to be used, e.g. radio.modulation.AM or radio.modulation.FM
-- @return #EASYGCICAP self 
function EASYGCICAP:_AddSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery, Frequency, Modulation)
  self:T(self.lid.."_AddSquadron "..SquadName)
  -- Add Squadrons
  local Squadron_One = SQUADRON:New(TemplateName,AirFrames,SquadName)
  Squadron_One:AddMissionCapability({AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.PATROLRACETRACK, AUFTRAG.Type.ALERT5})
  --Squadron_One:SetFuelLowRefuel(true)
  Squadron_One:SetFuelLowThreshold(0.3)
  Squadron_One:SetTurnoverTime(10,20)
  Squadron_One:SetModex(Modex)
  Squadron_One:SetLivery(Livery)
  Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE)
  Squadron_One:SetMissionRange(self.missionrange)
  
  local wing = self.wings[AirbaseName][1] -- Ops.Airwing#AIRWING
  
  wing:AddSquadron(Squadron_One)
  wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.PATROLRACETRACK, AUFTRAG.Type.ALERT5},75)
  
  return self
end

--- (Internal) Add a Recon Squadron to an Airwing of the manager
-- @param #EASYGCICAP self
-- @param #string TemplateName Name of the group template.
-- @param #string SquadName Squadron name - must be unique!
-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi
-- @param #number AirFrames Number of available airframes, e.g. 20.
-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE
-- @param #string Modex (optional) Modex to be used,e.g. 402.
-- @param #string Livery (optional) Livery name to be used.
-- @return #EASYGCICAP self 
function EASYGCICAP:_AddReconSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery)
  self:T(self.lid.."_AddReconSquadron "..SquadName)
  -- Add Squadrons
  local Squadron_One = SQUADRON:New(TemplateName,AirFrames,SquadName)
  Squadron_One:AddMissionCapability({AUFTRAG.Type.RECON})
  --Squadron_One:SetFuelLowRefuel(true)
  Squadron_One:SetFuelLowThreshold(0.3)
  Squadron_One:SetTurnoverTime(10,20)
  Squadron_One:SetModex(Modex)
  Squadron_One:SetLivery(Livery)
  Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE)
  Squadron_One:SetMissionRange(self.missionrange)
  
  local wing = self.wings[AirbaseName][1] -- Ops.Airwing#AIRWING
  
  wing:AddSquadron(Squadron_One)
  wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.RECON},75)
  
  return self
end

--- (Internal) Add a Tanker Squadron to an Airwing of the manager
-- @param #EASYGCICAP self
-- @param #string TemplateName Name of the group template.
-- @param #string SquadName Squadron name - must be unique!
-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi
-- @param #number AirFrames Number of available airframes, e.g. 20.
-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE
-- @param #string Modex (optional) Modex to be used,e.g. 402.
-- @param #string Livery (optional) Livery name to be used.
-- @param #number Frequency (optional) Radio frequency of the Tanker
-- @param #number Modulation (Optional) Radio modulation of the Tanker
-- @param #number TACAN (Optional) TACAN Channel to be used, will always be an "Y" channel
-- @return #EASYGCICAP self 
function EASYGCICAP:_AddTankerSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery, Frequency, Modulation, TACAN)
  self:T(self.lid.."_AddTankerSquadron "..SquadName)
  -- Add Squadrons
  local Squadron_One = SQUADRON:New(TemplateName,AirFrames,SquadName)
  Squadron_One:AddMissionCapability({AUFTRAG.Type.TANKER})
  --Squadron_One:SetFuelLowRefuel(true)
  Squadron_One:SetFuelLowThreshold(0.3)
  Squadron_One:SetTurnoverTime(10,20)
  Squadron_One:SetModex(Modex)
  Squadron_One:SetLivery(Livery)
  Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE)
  Squadron_One:SetMissionRange(self.missionrange)
  Squadron_One:SetRadio(Frequency,Modulation)
  if TACAN then
    Squadron_One:AddTacanChannel(TACAN,TACAN)
  end
  
  local wing = self.wings[AirbaseName][1] -- Ops.Airwing#AIRWING
  
  wing:AddSquadron(Squadron_One)
  wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.TANKER},75)
  
  return self
end

--- (Internal) Add a AWACS Squadron to an Airwing of the manager
-- @param #EASYGCICAP self
-- @param #string TemplateName Name of the group template.
-- @param #string SquadName Squadron name - must be unique!
-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi
-- @param #number AirFrames Number of available airframes, e.g. 20.
-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE
-- @param #string Modex (optional) Modex to be used,e.g. 402.
-- @param #string Livery (optional) Livery name to be used.
-- @param #number Frequency (optional) Radio frequency of the AWACS
-- @param #number Modulation (Optional) Radio modulation of the AWACS
-- @return #EASYGCICAP self 
function EASYGCICAP:_AddAWACSSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery, Frequency, Modulation)
  self:T(self.lid.."_AddAWACSSquadron "..SquadName)
  -- Add Squadrons
  local Squadron_One = SQUADRON:New(TemplateName,AirFrames,SquadName)
  Squadron_One:AddMissionCapability({AUFTRAG.Type.AWACS})
  --Squadron_One:SetFuelLowRefuel(true)
  Squadron_One:SetFuelLowThreshold(0.3)
  Squadron_One:SetTurnoverTime(10,20)
  Squadron_One:SetModex(Modex)
  Squadron_One:SetLivery(Livery)
  Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE)
  Squadron_One:SetMissionRange(self.missionrange)
  Squadron_One:SetRadio(Frequency,Modulation)
  local wing = self.wings[AirbaseName][1] -- Ops.Airwing#AIRWING
  
  wing:AddSquadron(Squadron_One)
  wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.AWACS},75)
  
  return self
end

--- Add a zone to the accepted zones set.
-- @param #EASYGCICAP self
-- @param Core.Zone#ZONE_BASE Zone
-- @return #EASYGCICAP self 
function EASYGCICAP:AddAcceptZone(Zone)
  self:T(self.lid.."AddAcceptZone")
  self.GoZoneSet:AddZone(Zone)
  return self
end

--- Add a zone to the rejected zones set.
-- @param #EASYGCICAP self
-- @param Core.Zone#ZONE_BASE Zone
-- @return #EASYGCICAP self 
function EASYGCICAP:AddRejectZone(Zone)
  self:T(self.lid.."AddRejectZone")
  self.NoGoZoneSet:AddZone(Zone)
  return self
end

--- Add a zone to the conflict zones set.
-- @param #EASYGCICAP self
-- @param Core.Zone#ZONE_BASE Zone
-- @return #EASYGCICAP self 
function EASYGCICAP:AddConflictZone(Zone)
  self:T(self.lid.."AddConflictZone")
  self.ConflictZoneSet:AddZone(Zone)
  self.GoZoneSet:AddZone(Zone)
  return self
end


--- (Internal) Try to assign the intercept to a FlightGroup already in air and ready.
-- @param #EASYGCICAP self
-- @param #table ReadyFlightGroups ReadyFlightGroups
-- @param Ops.Auftrag#AUFTRAG InterceptAuftrag The Auftrag
-- @param Wrapper.Group#GROUP Group The Target
-- @param #number WingSize Calculated number of Flights
-- @return #boolean assigned
-- @return #number leftover
function EASYGCICAP:_TryAssignIntercept(ReadyFlightGroups,InterceptAuftrag,Group,WingSize)
  self:T("_TryAssignIntercept for size "..WingSize or 1)
  local assigned = false
  local wingsize = WingSize or 1
  local mindist = 0
  local disttable = {}
  if Group and Group:IsAlive() then
    local gcoord = Group:GetCoordinate() or COORDINATE:New(0,0,0)
    self:T(self.lid..string.format("Assignment for %s",Group:GetName()))
    for _name,_FG in pairs(ReadyFlightGroups or {}) do
      local FG = _FG -- Ops.FlightGroup#FLIGHTGROUP
      local fcoord = FG:GetCoordinate()
      local dist = math.floor(UTILS.Round(fcoord:Get2DDistance(gcoord)/1000,1))
      self:T(self.lid..string.format("FG %s Distance %dkm",_name,dist))
      disttable[#disttable+1] = { FG=FG, dist=dist}
      if dist>mindist then mindist=dist end
    end
    
    local function sortDistance(a, b)
      return a.dist < b.dist
    end
    
    table.sort(disttable, sortDistance)
    
    for _,_entry in ipairs(disttable) do
      local FG = _entry.FG -- Ops.FlightGroup#FLIGHTGROUP
      FG:AddMission(InterceptAuftrag)
      local cm = FG:GetMissionCurrent()
      if cm then cm:Cancel() end
      wingsize = wingsize - 1
      self:T(self.lid..string.format("Assigned to FG %s Distance %dkm",FG:GetName(),_entry.dist))
      if wingsize == 0 then 
        assigned = true
        break 
      end
    end
  end
  
  return assigned, wingsize
end

--- Here, we'll decide if we need to launch an intercepting flight, and from where
-- @param #EASYGCICAP self
-- @param Ops.Intel#INTEL.Cluster Cluster
-- @return #EASYGCICAP self 
function EASYGCICAP:_AssignIntercept(Cluster)
   -- Here, we'll decide if we need to launch an intercepting flight, and from where
  local overhead = self.overhead
  local capspeed = self.capspeed + 100
  local capalt = self.capalt
  local maxsize = self.maxinterceptsize
  local repeatsonfailure = self.repeatsonfailure
  
  local wings = self.wings
  local ctlpts = self.ManagedCP
  local MaxAliveMissions = self.MaxAliveMissions --* self.capgrouping
  local nogozoneset = self.NoGoZoneSet
  local conflictzoneset = self.ConflictZoneSet
  local ReadyFlightGroups = self.ReadyFlightGroups
  
  -- Aircraft?
  if Cluster.ctype ~= INTEL.Ctype.AIRCRAFT then return end
  -- Threatlevel 0..10
  local contact = self.Intel:GetHighestThreatContact(Cluster)
  local name = contact.groupname --#string
  local threat = contact.threatlevel --#number
  local position = self.Intel:CalcClusterFuturePosition(Cluster,300)
  -- calculate closest zone
  local bestdistance = 2000*1000 -- 2000km
  local targetairwing = nil -- Ops.Airwing#AIRWING
  local targetawname = "" -- #string
  local clustersize = self.Intel:ClusterCountUnits(Cluster) or 1
  local wingsize = math.abs(overhead * (clustersize+1))
  if wingsize > maxsize then wingsize = maxsize end
  -- existing mission, and if so - done?
  local retrymission = true
  if Cluster.mission and (not Cluster.mission:IsOver()) then 
    retrymission = false
  end
  if (retrymission) and (wingsize >= 1) then
   MESSAGE:New(string.format("**** %s Interceptors need wingsize %d", UTILS.GetCoalitionName(self.coalition), wingsize),15,"CAPGCI"):ToAllIf(self.debug):ToLog()
    for _,_data in pairs (wings) do
      local airwing = _data[1] -- Ops.Airwing#AIRWING
      local zone = _data[2] -- Core.Zone#ZONE
      local zonecoord = zone:GetCoordinate()
      local name = _data[3] -- #string
      local coa = AIRBASE:FindByName(name):GetCoalition()
      local distance = position:DistanceFromPointVec2(zonecoord)
      local airframes = airwing:CountAssets(true)
      local samecoalitionab = coa == self.coalition and true or false
      if distance < bestdistance and airframes >= wingsize and samecoalitionab == true then
        bestdistance = distance
        targetairwing = airwing
        targetawname = name
      end
    end
    for _,_data in pairs (ctlpts) do
      --local airwing = _data[1] -- Ops.Airwing#AIRWING
      --local zone = _data[2] -- Core.Zone#ZONE
      --local zonecoord = zone:GetCoordinate()
      --local name = _data[3] -- #string
      
      local data = _data -- #EASYGCICAP.CapPoint
      local name = data.AirbaseName
      local zonecoord = data.Coordinate
      if data.Zone then
            -- refresh coordinate in case we have a (moving) zone
            zonecoord = data.Zone:GetCoordinate()
      end
      local airwing = wings[name][1]
      local coa = AIRBASE:FindByName(name):GetCoalition()
      local samecoalitionab = coa == self.coalition and true or false
      local distance = position:DistanceFromPointVec2(zonecoord)
      local airframes = airwing:CountAssets(true)
      if distance < bestdistance and airframes >= wingsize and samecoalitionab == true then
        bestdistance = distance
        targetairwing = airwing -- Ops.Airwing#AIRWING
        targetawname = name
      end
    end
    local text = string.format("Closest Airwing is %s", targetawname)
    local m = MESSAGE:New(text,10,"CAPGCI"):ToAllIf(self.debug):ToLog()
    -- Do we have a matching airwing?
    if targetairwing then
      local AssetCount = targetairwing:CountAssetsOnMission(MissionTypes,Cohort)
      local missioncount = self:_CountAliveAuftrags()
      -- Enough airframes on mission already?
      self:T(self.lid.." Assets on Mission "..AssetCount)
      if missioncount < MaxAliveMissions then
        local repeats = repeatsonfailure
        local InterceptAuftrag = AUFTRAG:NewINTERCEPT(contact.group)
          :SetMissionRange(150)
          :SetPriority(1,true,1)
          --:SetRequiredAssets(wingsize)
          :SetRepeatOnFailure(repeats)
          :SetMissionSpeed(UTILS.KnotsToAltKIAS(capspeed,capalt))
          :SetMissionAltitude(capalt)
          
          if nogozoneset:Count() > 0 then
            InterceptAuftrag:AddConditionSuccess(
              function(group,zoneset,conflictset)
                local success = false
                if group and group:IsAlive() then
                  local coord = group:GetCoordinate()
                  if coord and zoneset:Count() > 0 and zoneset:IsCoordinateInZone(coord) then
                    success = true
                  end
                  if coord and conflictset:Count() > 0 and conflictset:IsCoordinateInZone(coord) then
                    success = false
                  end
                end
                return success
              end,
              contact.group,
              nogozoneset,
              conflictzoneset
            )
          end
          
        table.insert(self.ListOfAuftrag,InterceptAuftrag)
        local assigned, rest = self:_TryAssignIntercept(ReadyFlightGroups,InterceptAuftrag,contact.group,wingsize)
        if not assigned  then
          InterceptAuftrag:SetRequiredAssets(rest)
          targetairwing:AddMission(InterceptAuftrag)
        end
        Cluster.mission = InterceptAuftrag
      end
    else
      MESSAGE:New("**** Not enough airframes available or max mission limit reached!",15,"CAPGCI"):ToAllIf(self.debug):ToLog()
    end
   end
end

--- (Internal) Start detection.
-- @param #EASYGCICAP self
-- @return #EASYGCICAP self
function EASYGCICAP:_StartIntel()
  self:T(self.lid.."_StartIntel")
  -- Border GCI Detection
  local BlueAir_DetectionSetGroup = SET_GROUP:New()
  BlueAir_DetectionSetGroup:FilterPrefixes( self.EWRName )
  BlueAir_DetectionSetGroup:FilterStart()
  
  -- Intel type detection
  local BlueIntel = INTEL:New(BlueAir_DetectionSetGroup,self.coalitionname, self.alias)
  BlueIntel:SetClusterAnalysis(true,false,false)
  BlueIntel:SetForgetTime(300)
  BlueIntel:SetAcceptZones(self.GoZoneSet)
  BlueIntel:SetRejectZones(self.NoGoZoneSet)
  BlueIntel:SetConflictZones(self.ConflictZoneSet)
  BlueIntel:SetVerbosity(0)
  BlueIntel:Start()
  
  if self.debug then 
    BlueIntel.debug = true
  end
  
  local function AssignCluster(Cluster)
    self:_AssignIntercept(Cluster)
  end
  
  function BlueIntel:onbeforeNewCluster(From,Event,To,Cluster)
    AssignCluster(Cluster)
  end
  
  self.Intel = BlueIntel  
  return self
end

-------------------------------------------------------------------------
-- TODO FSM Functions
-------------------------------------------------------------------------

--- (Internal) FSM Function onafterStart
-- @param #EASYGCICAP self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #EASYGCICAP self
function EASYGCICAP:onafterStart(From,Event,To)
  self:T({From,Event,To})
  self:_StartIntel()
  self:_CreateAirwings()
  self:_CreateSquads()
  self:_SetCAPPatrolPoints()
  self:_SetTankerPatrolPoints()
  self:_SetAwacsPatrolPoints()
  self:_SetReconPatrolPoints()
  self:__Status(-10)
  return self
end

--- (Internal) FSM Function onbeforeStatus
-- @param #EASYGCICAP self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #EASYGCICAP self
function EASYGCICAP:onbeforeStatus(From,Event,To)
  self:T({From,Event,To})
  if self:GetState() == "Stopped" then return false end
  return self
end

--- (Internal) FSM Function onafterStatus
-- @param #EASYGCICAP self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #EASYGCICAP self
function EASYGCICAP:onafterStatus(From,Event,To)
  self:T({From,Event,To})
  -- cleanup
  local cleaned = false
  local cleanlist = {}
  for _,_auftrag in pairs(self.ListOfAuftrag) do
    local auftrag = _auftrag -- Ops.Auftrag#AUFTRAG
    if auftrag and (not (auftrag:IsCancelled() or auftrag:IsDone() or auftrag:IsOver())) then
      table.insert(cleanlist,auftrag)
      cleaned = true
    end
  end
  if cleaned == true then
    self.ListOfAuftrag = nil
    self.ListOfAuftrag = cleanlist
  end
  -- Gather Some Stats
  local function counttable(tbl)
    local count = 0
    for _,_data in pairs(tbl) do
      count = count + 1
    end
    return count
  end
  local wings = counttable(self.ManagedAW)
  local squads = counttable(self.ManagedSQ)
  local caps = counttable(self.ManagedCP)
  local assets = 0
  local instock = 0
  local capmission = 0
  local interceptmission = 0
  local reconmission = 0
  local awacsmission = 0
  local tankermission = 0
  for _,_wing in pairs(self.wings) do
    local count = _wing[1]:CountAssetsOnMission(MissionTypes,Cohort)
    local count2 = _wing[1]:CountAssets(true,MissionTypes,Attributes)
    capmission = capmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.GCICAP,AUFTRAG.Type.PATROLRACETRACK})
    interceptmission = interceptmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.INTERCEPT})
    reconmission = reconmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.RECON})
    awacsmission = awacsmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.AWACS})
    tankermission = tankermission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.TANKER})
    assets = assets + count
    instock = instock + count2
    local assetsonmission = _wing[1]:GetAssetsOnMission({AUFTRAG.Type.GCICAP,AUFTRAG.Type.PATROLRACETRACK})
    -- update ready groups
    self.ReadyFlightGroups = nil
    self.ReadyFlightGroups = {}
    for _,_asset in pairs(assetsonmission or {}) do
      local asset = _asset -- Functional.Warehouse#WAREHOUSE.Assetitem
      local FG = asset.flightgroup -- Ops.FlightGroup#FLIGHTGROUP
      if FG then
        local name = FG:GetName()
        local engage = FG:IsEngaging()
        local hasmissiles = FG:IsOutOfMissiles() == nil and true or false
        local ready = hasmissiles and FG:IsFuelGood() and FG:IsAirborne()
        --self:T(string.format("Flightgroup %s Engaging = %s Ready = %s",tostring(name),tostring(engage),tostring(ready)))
        if ready then
          self.ReadyFlightGroups[name] = FG
        end
      end
    end
  end
  if self.Monitor then
    local threatcount = #self.Intel.Clusters or 0
    local text =  "GCICAP "..self.alias
    text = text.."\nWings: "..wings.."\nSquads: "..squads.."\nCapPoints: "..caps.."\nAssets on Mission: "..assets.."\nAssets in Stock: "..instock
    text = text.."\nThreats: "..threatcount
    text = text.."\nAirWing managed Missions: "..capmission+awacsmission+tankermission+reconmission
    text = text.."\n - CAP: "..capmission
    text = text.."\n - AWACS: "..awacsmission
    text = text.."\n - TANKER: "..tankermission
    text = text.."\n - Recon: "..reconmission
    text = text.."\nSelf managed Missions:"
    text = text.."\n - Mission Limit: "..self.MaxAliveMissions
    text = text.."\n - Alert5+Intercept "..self:_CountAliveAuftrags()
    MESSAGE:New(text,15,"GCICAP"):ToAll():ToLogIf(self.debug)
  end
  self:__Status(30)
  return self
end

--- (Internal) FSM Function onafterStop
-- @param #EASYGCICAP self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #EASYGCICAP self
function EASYGCICAP:onafterStop(From,Event,To)
  self:T({From,Event,To})
  self.Intel:Stop()
  for _,_wing in pairs(self.wings or {}) do
    _wing:Stop()
  end
  return self
end
--- **AI** - Balance player slots with AI to create an engaging simulation environment, independent of the amount of players. 
-- 
-- **Features:**
-- 
--   * Automatically spawn AI as a replacement of free player slots for a coalition.
--   * Make the AI to perform tasks.
--   * Define a maximum amount of AI to be active at the same time.
--   * Configure the behaviour of AI when a human joins a slot for which an AI is active.
-- 
-- ===
-- 
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_Balancer)
-- 
-- ===
-- 
-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl2CJVIrL1TdAumuVS8n64B7)
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
--   * **Dutch_Baron**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-)
-- 
-- ===
-- 
-- @module AI.AI_Balancer
-- @image AI_Balancing.JPG

-- @type AI_BALANCER
-- @field Core.Set#SET_CLIENT SetClient
-- @field Core.Spawn#SPAWN SpawnAI
-- @field Wrapper.Group#GROUP Test
-- @extends Core.Fsm#FSM_SET

--- ![Banner Image](..\Images\deprecated.png)
-- 
-- Monitors and manages as many replacement AI groups as there are
-- CLIENTS in a SET\_CLIENT collection, which are not occupied by human players. 
-- In other words, use AI_BALANCER to simulate human behaviour by spawning in replacement AI in multi player missions.
-- 
-- The parent class @{Core.Fsm#FSM_SET} manages the functionality to control the Finite State Machine (FSM). 
-- The mission designer can tailor the behaviour of the AI_BALANCER, by defining event and state transition methods.
-- An explanation about state and event transition methods can be found in the @{Core.Fsm} module documentation.
-- 
-- The mission designer can tailor the AI_BALANCER behaviour, by implementing a state or event handling method for the following:
-- 
--   * @{#AI_BALANCER.OnAfterSpawned}( AISet, From, Event, To, AIGroup ): Define to add extra logic when an AI is spawned.
-- 
-- ## 1. AI_BALANCER construction
-- 
-- Create a new AI_BALANCER object with the @{#AI_BALANCER.New}() method:
-- 
-- ## 2. AI_BALANCER is a FSM
-- 
-- ![Process](..\Presentations\AI_BALANCER\Dia13.JPG)
-- 
-- ### 2.1. AI_BALANCER States
-- 
--   * **Monitoring** ( Set ): Monitoring the Set if all AI is spawned for the Clients.
--   * **Spawning** ( Set, ClientName ): There is a new AI group spawned with ClientName as the name of reference.
--   * **Spawned** ( Set, AIGroup ): A new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes.
--   * **Destroying** ( Set, AIGroup ): The AI is being destroyed.
--   * **Returning** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods. Handle this state to customize the return behaviour of the AI, if any.
-- 
-- ### 2.2. AI_BALANCER Events
-- 
--   * **Monitor** ( Set ): Every 10 seconds, the Monitor event is triggered to monitor the Set.
--   * **Spawn** ( Set, ClientName ): Triggers when there is a new AI group to be spawned with ClientName as the name of reference.
--   * **Spawned** ( Set, AIGroup ): Triggers when a new AI has been spawned. You can handle this event to customize the AI behaviour with other AI FSMs or own processes.
--   * **Destroy** ( Set, AIGroup ): The AI is being destroyed.
--   * **Return** ( Set, AIGroup ): The AI is returning to the airbase specified by the ReturnToAirbase methods.
--    
-- ## 3. AI_BALANCER spawn interval for replacement AI
-- 
-- Use the method @{#AI_BALANCER.InitSpawnInterval}() to set the earliest and latest interval in seconds that is waited until a new replacement AI is spawned.
-- 
-- ## 4. AI_BALANCER returns AI to Airbases
-- 
-- By default, When a human player joins a slot that is AI_BALANCED, the AI group will be destroyed by default. 
-- However, there are 2 additional options that you can use to customize the destroy behaviour.
-- When a human player joins a slot, you can configure to let the AI return to:
-- 
--    * @{#AI_BALANCER.ReturnToHomeAirbase}: Returns the AI to the **home** @{Wrapper.Airbase#AIRBASE}.
--    * @{#AI_BALANCER.ReturnToNearestAirbases}: Returns the AI to the **nearest friendly** @{Wrapper.Airbase#AIRBASE}.
-- 
-- Note that when AI returns to an airbase, the AI_BALANCER will trigger the **Return** event and the AI will return, 
-- otherwise the AI_BALANCER will trigger a **Destroy** event, and the AI will be destroyed.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- @field #AI_BALANCER
AI_BALANCER = {
  ClassName = "AI_BALANCER",
  PatrolZones = {},
  AIGroups = {},
  Earliest = 5, -- Earliest a new AI can be spawned is in 5 seconds.
  Latest = 60, -- Latest a new AI can be spawned is in 60 seconds.
}



--- Creates a new AI_BALANCER object
-- @param #AI_BALANCER self
-- @param Core.Set#SET_CLIENT SetClient A SET\_CLIENT object that will contain the CLIENT objects to be monitored if they are alive or not (joined by a player).
-- @param Core.Spawn#SPAWN SpawnAI The default Spawn object to spawn new AI Groups when needed.
-- @return #AI_BALANCER
function AI_BALANCER:New( SetClient, SpawnAI )
  
  -- Inherits from BASE
  local self = BASE:Inherit( self, FSM_SET:New( SET_GROUP:New() ) ) -- AI.AI_Balancer#AI_BALANCER
  
  -- TODO: Define the OnAfterSpawned event
  self:SetStartState( "None" )
  self:AddTransition( "*", "Monitor", "Monitoring" )
  self:AddTransition( "*", "Spawn", "Spawning" )
  self:AddTransition( "Spawning", "Spawned", "Spawned" )
  self:AddTransition( "*", "Destroy", "Destroying" )
  self:AddTransition( "*", "Return", "Returning" )
  
  self.SetClient = SetClient
  self.SetClient:FilterOnce()
  self.SpawnAI = SpawnAI
  
  self.SpawnQueue = {}

  self.ToNearestAirbase = false
  self.ToHomeAirbase = false
  
  self:__Monitor( 1 )

  return self
end

--- Sets the earliest to the latest interval in seconds how long AI_BALANCER will wait to spawn a new AI.
-- Provide 2 identical seconds if the interval should be a fixed amount of seconds.
-- @param #AI_BALANCER self
-- @param #number Earliest The earliest a new AI can be spawned in seconds.
-- @param #number Latest The latest a new AI can be spawned in seconds.
-- @return self
function AI_BALANCER:InitSpawnInterval( Earliest, Latest )

  self.Earliest = Earliest
  self.Latest = Latest
  
  return self
end

--- Returns the AI to the nearest friendly @{Wrapper.Airbase#AIRBASE}.
-- @param #AI_BALANCER self
-- @param DCS#Distance ReturnThresholdRange If there is an enemy @{Wrapper.Client#CLIENT} within the ReturnThresholdRange given in meters, the AI will not return to the nearest @{Wrapper.Airbase#AIRBASE}.
-- @param Core.Set#SET_AIRBASE ReturnAirbaseSet The SET of @{Core.Set#SET_AIRBASE}s to evaluate where to return to.
function AI_BALANCER:ReturnToNearestAirbases( ReturnThresholdRange, ReturnAirbaseSet )

  self.ToNearestAirbase = true
  self.ReturnThresholdRange = ReturnThresholdRange
  self.ReturnAirbaseSet = ReturnAirbaseSet
end

--- Returns the AI to the home @{Wrapper.Airbase#AIRBASE}.
-- @param #AI_BALANCER self
-- @param DCS#Distance ReturnThresholdRange If there is an enemy @{Wrapper.Client#CLIENT} within the ReturnThresholdRange given in meters, the AI will not return to the nearest @{Wrapper.Airbase#AIRBASE}.
function AI_BALANCER:ReturnToHomeAirbase( ReturnThresholdRange )

  self.ToHomeAirbase = true
  self.ReturnThresholdRange = ReturnThresholdRange
end

--- AI_BALANCER:onenterSpawning
-- @param #AI_BALANCER self
-- @param Core.Set#SET_GROUP SetGroup
-- @param #string ClientName
-- @param Wrapper.Group#GROUP AIGroup
function AI_BALANCER:onenterSpawning( SetGroup, From, Event, To, ClientName )

  -- OK, Spawn a new group from the default SpawnAI object provided.
  local AIGroup = self.SpawnAI:Spawn() -- Wrapper.Group#GROUP
  if AIGroup then
    AIGroup:T( { "Spawning new AIGroup", ClientName = ClientName } )
    --TODO: need to rework UnitName thing ...
    
    SetGroup:Remove( ClientName ) -- Ensure that the previously allocated AIGroup to ClientName is removed in the Set.
    SetGroup:Add( ClientName, AIGroup )
    self.SpawnQueue[ClientName] = nil
    
    -- Fire the Spawned event. The first parameter is the AIGroup just Spawned.
    -- Mission designers can catch this event to bind further actions to the AIGroup.
    self:Spawned( AIGroup )
  end
end

--- AI_BALANCER:onenterDestroying
-- @param #AI_BALANCER self
-- @param Core.Set#SET_GROUP SetGroup
-- @param Wrapper.Group#GROUP AIGroup
function AI_BALANCER:onenterDestroying( SetGroup, From, Event, To, ClientName, AIGroup )

  AIGroup:Destroy()
  SetGroup:Flush( self )
  SetGroup:Remove( ClientName )
  SetGroup:Flush( self )
end

--- RTB
-- @param #AI_BALANCER self
-- @param Core.Set#SET_GROUP SetGroup
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param Wrapper.Group#GROUP AIGroup
function AI_BALANCER:onenterReturning( SetGroup, From, Event, To, AIGroup )

    local AIGroupTemplate = AIGroup:GetTemplate()
    if self.ToHomeAirbase == true then
      local WayPointCount = #AIGroupTemplate.route.points
      local SwitchWayPointCommand = AIGroup:CommandSwitchWayPoint( 1, WayPointCount, 1 )
      AIGroup:SetCommand( SwitchWayPointCommand )
      AIGroup:MessageToRed( "Returning to home base ...", 30 )
    else
      -- Okay, we need to send this Group back to the nearest base of the Coalition of the AI.
      local PointVec2 = COORDINATE:New(AIGroup:GetVec2().x, 0, AIGroup:GetVec2().y)
      local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 )
      self:T( ClosestAirbase.AirbaseName )
      AIGroup:RouteRTB(ClosestAirbase)
    end

end

--- AI_BALANCER:onenterMonitoring
-- @param #AI_BALANCER self
function AI_BALANCER:onenterMonitoring( SetGroup )

  self:T2( { self.SetClient:Count() } )
  --self.SetClient:Flush()

  self.SetClient:ForEachClient(
    --- SetClient:ForEachClient
    -- @param Wrapper.Client#CLIENT Client
    function( Client )
      self:T3(Client.ClientName)

      local AIGroup = self.Set:Get( Client.UnitName ) -- Wrapper.Group#GROUP
      if AIGroup then self:T( { AIGroup = AIGroup:GetName(), IsAlive = AIGroup:IsAlive() } ) end
      if Client:IsAlive() == true then

        if AIGroup and AIGroup:IsAlive() == true then

          if self.ToNearestAirbase == false and self.ToHomeAirbase == false then
            self:Destroy( Client.UnitName, AIGroup )
          else
            -- We test if there is no other CLIENT within the self.ReturnThresholdRange of the first unit of the AI group.
            -- If there is a CLIENT, the AI stays engaged and will not return.
            -- If there is no CLIENT within the self.ReturnThresholdRange, then the unit will return to the Airbase return method selected.

            local PlayerInRange = { Value = false }          
            local RangeZone = ZONE_RADIUS:New( 'RangeZone', AIGroup:GetVec2(), self.ReturnThresholdRange )
            
            self:T2( RangeZone )
            
            _DATABASE:ForEachPlayerUnit(
              --- Nameless function
              -- @param Wrapper.Unit#UNIT RangeTestUnit
              function( RangeTestUnit, RangeZone, AIGroup, PlayerInRange )
                self:T2( { PlayerInRange, RangeTestUnit.UnitName, RangeZone.ZoneName } )
                if RangeTestUnit:IsInZone( RangeZone ) == true then
                  self:T2( "in zone" )
                  if RangeTestUnit:GetCoalition() ~= AIGroup:GetCoalition() then
                    self:T2( "in range" )
                    PlayerInRange.Value = true
                  end
                end
              end,
              
              --- Nameless function
              -- @param Core.Zone#ZONE_RADIUS RangeZone
              -- @param Wrapper.Group#GROUP AIGroup
              function( RangeZone, AIGroup, PlayerInRange )
                if PlayerInRange.Value == false then
                  self:Return( AIGroup )
                end
              end
              , RangeZone, AIGroup, PlayerInRange
            )
            
          end
          self.Set:Remove( Client.UnitName )
        end
      else
        if not AIGroup or not AIGroup:IsAlive() == true then
          self:T( "Client " .. Client.UnitName .. " not alive." )
          self:T( { Queue = self.SpawnQueue[Client.UnitName] } )
          if not self.SpawnQueue[Client.UnitName] then
            -- Spawn a new AI taking into account the spawn interval Earliest, Latest
            self:__Spawn( math.random( self.Earliest, self.Latest ), Client.UnitName )
            self.SpawnQueue[Client.UnitName] = true
            self:T( "New AI Spawned for Client " .. Client.UnitName )
          end
        end
      end
      return true
    end
  )
  
  self:__Monitor( 10 )
end
--- **AI** - Models the process of AI air operations.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===
-- 
-- @module AI.AI_Air
-- @image MOOSE.JPG

---
-- @type AI_AIR
-- @extends Core.Fsm#FSM_CONTROLLABLE

--- The AI_AIR class implements the core functions to operate an AI @{Wrapper.Group}.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- # 1) AI_AIR constructor
--   
--   * @{#AI_AIR.New}(): Creates a new AI_AIR object.
-- 
-- # 2) AI_AIR is a Finite State Machine.
-- 
-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed.
-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state.
-- 
-- So, each of the rows have the following structure.
-- 
--   * **From** => **Event** => **To**
-- 
-- Important to know is that an event can only be executed if the **current state** is the **From** state.
-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed,
-- and the resulting state will be the **To** state.
-- 
-- These are the different possible state transitions of this state machine implementation: 
-- 
--   * Idle => Start => Monitoring
--
-- ## 2.1) AI_AIR States.
-- 
--   * **Idle**: The process is idle.
-- 
-- ## 2.2) AI_AIR Events.
-- 
--   * **Start**: Start the transport process.
--   * **Stop**: Stop the transport process.
--   * **Monitor**: Monitor and take action.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- @field #AI_AIR
AI_AIR = {
  ClassName = "AI_AIR",
}

AI_AIR.TaskDelay = 0.5 -- The delay of each task given to the AI.

--- Creates a new AI_AIR process.
-- @param #AI_AIR self
-- @param Wrapper.Group#GROUP AIGroup The group object to receive the A2G Process.
-- @return #AI_AIR
function AI_AIR:New( AIGroup )

  -- Inherits from BASE
  local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_AIR
  
  self:SetControllable( AIGroup )
  
  self:SetStartState( "Stopped" ) 

  self:AddTransition( "*", "Queue", "Queued" )

  self:AddTransition( "*", "Start", "Started" )
  
  --- Start Handler OnBefore for AI_AIR
  -- @function [parent=#AI_AIR] OnBeforeStart
  -- @param #AI_AIR self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @return #boolean
  
  --- Start Handler OnAfter for AI_AIR
  -- @function [parent=#AI_AIR] OnAfterStart
  -- @param #AI_AIR self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  
  --- Start Trigger for AI_AIR
  -- @function [parent=#AI_AIR] Start
  -- @param #AI_AIR self
  
  --- Start Asynchronous Trigger for AI_AIR
  -- @function [parent=#AI_AIR] __Start
  -- @param #AI_AIR self
  -- @param #number Delay

  self:AddTransition( "*", "Stop", "Stopped" )

--- OnLeave Transition Handler for State Stopped.
-- @function [parent=#AI_AIR] OnLeaveStopped
-- @param #AI_AIR self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnEnter Transition Handler for State Stopped.
-- @function [parent=#AI_AIR] OnEnterStopped
-- @param #AI_AIR self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

--- OnBefore Transition Handler for Event Stop.
-- @function [parent=#AI_AIR] OnBeforeStop
-- @param #AI_AIR self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnAfter Transition Handler for Event Stop.
-- @function [parent=#AI_AIR] OnAfterStop
-- @param #AI_AIR self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
	
--- Synchronous Event Trigger for Event Stop.
-- @function [parent=#AI_AIR] Stop
-- @param #AI_AIR self

--- Asynchronous Event Trigger for Event Stop.
-- @function [parent=#AI_AIR] __Stop
-- @param #AI_AIR self
-- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR.

--- OnBefore Transition Handler for Event Status.
-- @function [parent=#AI_AIR] OnBeforeStatus
-- @param #AI_AIR self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnAfter Transition Handler for Event Status.
-- @function [parent=#AI_AIR] OnAfterStatus
-- @param #AI_AIR self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
	
--- Synchronous Event Trigger for Event Status.
-- @function [parent=#AI_AIR] Status
-- @param #AI_AIR self

--- Asynchronous Event Trigger for Event Status.
-- @function [parent=#AI_AIR] __Status
-- @param #AI_AIR self
-- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "RTB", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR.

--- OnBefore Transition Handler for Event RTB.
-- @function [parent=#AI_AIR] OnBeforeRTB
-- @param #AI_AIR self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnAfter Transition Handler for Event RTB.
-- @function [parent=#AI_AIR] OnAfterRTB
-- @param #AI_AIR self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
	
--- Synchronous Event Trigger for Event RTB.
-- @function [parent=#AI_AIR] RTB
-- @param #AI_AIR self

--- Asynchronous Event Trigger for Event RTB.
-- @function [parent=#AI_AIR] __RTB
-- @param #AI_AIR self
-- @param #number Delay The delay in seconds.

--- OnLeave Transition Handler for State Returning.
-- @function [parent=#AI_AIR] OnLeaveReturning
-- @param #AI_AIR self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnEnter Transition Handler for State Returning.
-- @function [parent=#AI_AIR] OnEnterReturning
-- @param #AI_AIR self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

  self:AddTransition( "Patrolling", "Refuel", "Refuelling" ) 

  --- Refuel Handler OnBefore for AI_AIR
  -- @function [parent=#AI_AIR] OnBeforeRefuel
  -- @param #AI_AIR self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @return #boolean
  
  --- Refuel Handler OnAfter for AI_AIR
  -- @function [parent=#AI_AIR] OnAfterRefuel
  -- @param #AI_AIR self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  
  --- Refuel Trigger for AI_AIR
  -- @function [parent=#AI_AIR] Refuel
  -- @param #AI_AIR self
  
  --- Refuel Asynchronous Trigger for AI_AIR
  -- @function [parent=#AI_AIR] __Refuel
  -- @param #AI_AIR self
  -- @param #number Delay

  self:AddTransition( "*", "Takeoff", "Airborne" )
  self:AddTransition( "*", "Return", "Returning" )
  self:AddTransition( "*", "Hold", "Holding" )
  self:AddTransition( "*", "Home", "Home" )
  self:AddTransition( "*", "LostControl", "LostControl" )
  self:AddTransition( "*", "Fuel", "Fuel" )
  self:AddTransition( "*", "Damaged", "Damaged" )
  self:AddTransition( "*", "Eject", "*" )
  self:AddTransition( "*", "Crash", "Crashed" )
  self:AddTransition( "*", "PilotDead", "*" )
  
  self.IdleCount = 0
  
  self.RTBSpeedMaxFactor = 0.6
  self.RTBSpeedMinFactor = 0.5
  
  return self
end

-- @param Wrapper.Group#GROUP self
-- @param Core.Event#EVENTDATA EventData
function GROUP:OnEventTakeoff( EventData, Fsm )
  Fsm:Takeoff()
  self:UnHandleEvent( EVENTS.Takeoff )
end



function AI_AIR:SetDispatcher( Dispatcher )
  self.Dispatcher = Dispatcher
end

function AI_AIR:GetDispatcher()
  return self.Dispatcher
end

function AI_AIR:SetTargetDistance( Coordinate )

  local CurrentCoord = self.Controllable:GetCoordinate()
  self.TargetDistance = CurrentCoord:Get2DDistance( Coordinate )

  self.ClosestTargetDistance = ( not self.ClosestTargetDistance or self.ClosestTargetDistance > self.TargetDistance ) and self.TargetDistance or self.ClosestTargetDistance
end


function AI_AIR:ClearTargetDistance()

  self.TargetDistance = nil
  self.ClosestTargetDistance = nil
end


--- Sets (modifies) the minimum and maximum speed of the patrol.
-- @param #AI_AIR self
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h.
-- @return #AI_AIR self
function AI_AIR:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed )
  self:F2( { PatrolMinSpeed, PatrolMaxSpeed } )
  
  self.PatrolMinSpeed = PatrolMinSpeed
  self.PatrolMaxSpeed = PatrolMaxSpeed
end


--- Sets (modifies) the minimum and maximum RTB speed of the patrol.
-- @param #AI_AIR self
-- @param DCS#Speed  RTBMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h.
-- @param DCS#Speed  RTBMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h.
-- @return #AI_AIR self
function AI_AIR:SetRTBSpeed( RTBMinSpeed, RTBMaxSpeed )
  self:F( { RTBMinSpeed, RTBMaxSpeed } )
  
  self.RTBMinSpeed = RTBMinSpeed
  self.RTBMaxSpeed = RTBMaxSpeed
end


--- Sets the floor and ceiling altitude of the patrol.
-- @param #AI_AIR self
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @return #AI_AIR self
function AI_AIR:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude )
  self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } )
  
  self.PatrolFloorAltitude = PatrolFloorAltitude
  self.PatrolCeilingAltitude = PatrolCeilingAltitude
end


--- Sets the home airbase.
-- @param #AI_AIR self
-- @param Wrapper.Airbase#AIRBASE HomeAirbase
-- @return #AI_AIR self
function AI_AIR:SetHomeAirbase( HomeAirbase )
  self:F2( { HomeAirbase } )
  
  self.HomeAirbase = HomeAirbase
end

--- Sets to refuel at the given tanker.
-- @param #AI_AIR self
-- @param Wrapper.Group#GROUP TankerName The group name of the tanker as defined within the Mission Editor or spawned.
-- @return #AI_AIR self
function AI_AIR:SetTanker( TankerName )
  self:F2( { TankerName } )
  
  self.TankerName = TankerName
end


--- Sets the disengage range, that when engaging a target beyond the specified range, the engagement will be cancelled and the plane will RTB.
-- @param #AI_AIR self
-- @param #number DisengageRadius The disengage range.
-- @return #AI_AIR self
function AI_AIR:SetDisengageRadius( DisengageRadius )
  self:F2( { DisengageRadius } )
  
  self.DisengageRadius = DisengageRadius
end

--- Set the status checking off.
-- @param #AI_AIR self
-- @return #AI_AIR self
function AI_AIR:SetStatusOff()
  self:F2()
  
  self.CheckStatus = false
end


--- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base.
-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel threshold is calculated.
-- When the fuel threshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targeted to the AI_AIR.
-- Once the time is finished, the old AI will return to the base.
-- @param #AI_AIR self
-- @param #number FuelThresholdPercentage The threshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel.
-- @param #number OutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base.
-- @return #AI_AIR self
function AI_AIR:SetFuelThreshold( FuelThresholdPercentage, OutOfFuelOrbitTime )

  self.FuelThresholdPercentage = FuelThresholdPercentage
  self.OutOfFuelOrbitTime = OutOfFuelOrbitTime
  
  self.Controllable:OptionRTBBingoFuel( false )
  
  return self
end

--- When the AI is damaged beyond a certain threshold, it is required that the AI returns to the home base.
-- However, damage cannot be foreseen early on. 
-- Therefore, when the damage threshold is reached, 
-- the AI will return immediately to the home base (RTB).
-- Note that for groups, the average damage of the complete group will be calculated.
-- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage threshold will be 0.25.
-- @param #AI_AIR self
-- @param #number PatrolDamageThreshold The threshold in percentage (between 0 and 1) when the AI is considered to be damaged.
-- @return #AI_AIR self
function AI_AIR:SetDamageThreshold( PatrolDamageThreshold )

  self.PatrolManageDamage = true
  self.PatrolDamageThreshold = PatrolDamageThreshold
  
  return self
end



--- Defines a new patrol route using the @{AI.AI_Patrol#AI_PATROL_ZONE} parameters and settings.
-- @param #AI_AIR self
-- @return #AI_AIR self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_AIR:onafterStart( Controllable, From, Event, To )

  self:__Status( 10 ) -- Check status status every 30 seconds.
  
  self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead )
  self:HandleEvent( EVENTS.Crash, self.OnCrash )
  self:HandleEvent( EVENTS.Ejection, self.OnEjection )
  
  Controllable:OptionROEHoldFire()
  Controllable:OptionROTVertical()
end

--- Coordinates the approriate returning action.
-- @param #AI_AIR self
-- @return #AI_AIR self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_AIR:onafterReturn( Controllable, From, Event, To )

  self:__RTB( self.TaskDelay )
  
end

-- @param #AI_AIR self
function AI_AIR:onbeforeStatus()

  return self.CheckStatus
end

-- @param #AI_AIR self
function AI_AIR:onafterStatus()

  if self.Controllable and self.Controllable:IsAlive() then
  
    local RTB = false
    
    local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() )
    
    if not self:Is( "Holding" ) and not self:Is( "Returning" ) then
      local DistanceFromHomeBase = self.HomeAirbase:GetCoordinate():Get2DDistance( self.Controllable:GetCoordinate() )
      
      if DistanceFromHomeBase > self.DisengageRadius then
        self:T( self.Controllable:GetName() .. " is too far from home base, RTB!" )
        self:Hold( 300 )
        RTB = false
      end
    end

-- I think this code is not requirement anymore after release 2.5.    
--    if self:Is( "Fuel" ) or self:Is( "Damaged" ) or self:Is( "LostControl" ) then
--      if DistanceFromHomeBase < 5000 then
--        self:E( self.Controllable:GetName() .. " is near the home base, RTB!" )
--        self:Home( "Destroy" )
--      end
--    end


    if not self:Is( "Fuel" ) and not self:Is( "Home" ) and not self:is( "Refuelling" )then

      local Fuel = self.Controllable:GetFuelMin()

      -- If the fuel in the controllable is below the threshold percentage,
      -- then send for refuel in case of a tanker, otherwise RTB.
      if Fuel < self.FuelThresholdPercentage then

        if self.TankerName then
          self:T( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" )
          self:Refuel()
        else
          self:T( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... RTB!" )
          local OldAIControllable = self.Controllable

          local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed )
          local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.OutOfFuelOrbitTime,nil ) )
          OldAIControllable:SetTask( TimedOrbitTask, 10 )

          self:Fuel()
          RTB = true
        end
      else
      end
    end

    if self:Is( "Fuel" ) and not self:Is( "Home" ) and not self:is( "Refuelling" ) then
      RTB = true
    end

    -- TODO: Check GROUP damage function.
    local Damage = self.Controllable:GetLife()
    local InitialLife = self.Controllable:GetLife0()

    -- If the group is damaged, then RTB.
    -- Note that a group can consist of more units, so if one unit is damaged of a group, the mission may continue.
    -- The damaged unit will RTB due to DCS logic, and the others will continue to engage.
    if ( Damage / InitialLife ) < self.PatrolDamageThreshold then
      self:T( self.Controllable:GetName() .. " is damaged: " .. Damage .. " ... RTB!" )
      self:Damaged()
      RTB = true
      self:SetStatusOff()
    end

    -- Check if planes went RTB and are out of control.
    -- We only check if planes are out of control, when they are in duty.
    if self.Controllable:HasTask() == false then
      if not self:Is( "Started" ) and 
         not self:Is( "Stopped" ) and
         not self:Is( "Fuel" ) and 
         not self:Is( "Damaged" ) and 
         not self:Is( "Home" ) then
        if self.IdleCount >= 10 then
          if Damage ~= InitialLife then
            self:Damaged()
          else  
            self:T( self.Controllable:GetName() .. " control lost! " )

            self:LostControl()
          end
        else
          self.IdleCount = self.IdleCount + 1
        end
      end
    else
      self.IdleCount = 0
    end

    if RTB == true then
      self:__RTB( self.TaskDelay )
    end

    if not self:Is("Home") then
      self:__Status( 10 )
    end

  end
end


-- @param Wrapper.Group#GROUP AIGroup
function AI_AIR.RTBRoute( AIGroup, Fsm )

  AIGroup:F( { "AI_AIR.RTBRoute:", AIGroup:GetName() } )

  if AIGroup:IsAlive() then
    Fsm:RTB()
  end

end

-- @param Wrapper.Group#GROUP AIGroup
function AI_AIR.RTBHold( AIGroup, Fsm )

  AIGroup:F( { "AI_AIR.RTBHold:", AIGroup:GetName() } )
  if AIGroup:IsAlive() then
    Fsm:__RTB( Fsm.TaskDelay )
    Fsm:Return()
    local Task = AIGroup:TaskOrbitCircle( 4000, 400 )
    AIGroup:SetTask( Task )
  end

end

--- Set the min and max factors on RTB speed. Use this, if your planes are heading back to base too fast. Default values are 0.5 and 0.6. 
-- The RTB speed is calculated as the max speed of the unit multiplied by MinFactor (lower bracket) and multiplied by MaxFactor (upper bracket). 
-- A random value in this bracket is then applied in the waypoint routing generation.
-- @param #AI_AIR self
-- @param #number MinFactor Lower bracket factor. Defaults to 0.5.
-- @param #number MaxFactor Upper bracket factor. Defaults to 0.6.
-- @return #AI_AIR self
function AI_AIR:SetRTBSpeedFactors(MinFactor,MaxFactor)
  self.RTBSpeedMaxFactor = MaxFactor or 0.6
  self.RTBSpeedMinFactor = MinFactor or 0.5
  return self
end


-- @param #AI_AIR self
-- @param Wrapper.Group#GROUP AIGroup
function AI_AIR:onafterRTB( AIGroup, From, Event, To )
  self:F( { AIGroup, From, Event, To } )

    if AIGroup and AIGroup:IsAlive() then

    self:T( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" )

    self:ClearTargetDistance()
    --AIGroup:ClearTasks()

    AIGroup:OptionProhibitAfterburner(true)

    local EngageRoute = {}

    --- Calculate the target route point.

    local FromCoord = AIGroup:GetCoordinate()
    if not FromCoord then return end
        
    local ToTargetCoord = self.HomeAirbase:GetCoordinate() -- coordinate is on land height(!)

    local ToTargetVec3 = ToTargetCoord:GetVec3()
    ToTargetVec3.y = ToTargetCoord:GetLandHeight()+3000 -- let's set this 1000m/3000 feet above ground
    local ToTargetCoord2 = COORDINATE:NewFromVec3( ToTargetVec3 )

    if not self.RTBMinSpeed or not self.RTBMaxSpeed then    
      local RTBSpeedMax = AIGroup:GetSpeedMax()
      local RTBSpeedMaxFactor = self.RTBSpeedMaxFactor or 0.6
      local RTBSpeedMinFactor = self.RTBSpeedMinFactor or 0.5
      self:SetRTBSpeed( RTBSpeedMax * RTBSpeedMinFactor, RTBSpeedMax * RTBSpeedMaxFactor)  
    end

    local RTBSpeed = math.random( self.RTBMinSpeed, self.RTBMaxSpeed )
    --local ToAirbaseAngle = FromCoord:GetAngleDegrees( FromCoord:GetDirectionVec3( ToTargetCoord2 ) )

    local Distance = FromCoord:Get2DDistance( ToTargetCoord2 )

    --local ToAirbaseCoord = FromCoord:Translate( 5000, ToAirbaseAngle )
    local ToAirbaseCoord = ToTargetCoord2

    if Distance < 5000 then
      self:T( "RTB and near the airbase!" )
      self:Home()
      return
    end

    if not AIGroup:InAir() == true then
      self:T( "Not anymore in the air, considered Home." )
      self:Home()
      return
    end


    --- Create a route point of type air.
    local FromRTBRoutePoint = FromCoord:WaypointAir( 
      self.PatrolAltType, 
      COORDINATE.WaypointType.TurningPoint, 
      COORDINATE.WaypointAction.TurningPoint, 
      RTBSpeed, 
      true 
    )

    --- Create a route point of type air.
    local ToRTBRoutePoint = ToAirbaseCoord:WaypointAir( 
      self.PatrolAltType, 
      COORDINATE.WaypointType.TurningPoint, 
      COORDINATE.WaypointAction.TurningPoint, 
      RTBSpeed, 
      true 
    )

    EngageRoute[#EngageRoute+1] = FromRTBRoutePoint
    EngageRoute[#EngageRoute+1] = ToRTBRoutePoint

    local Tasks = {}
    Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_AIR.RTBRoute", self )

    EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks )

    AIGroup:OptionROEHoldFire()
    AIGroup:OptionROTEvadeFire()

    --- NOW ROUTE THE GROUP!
    AIGroup:Route( EngageRoute, self.TaskDelay )

  end

end

-- @param #AI_AIR self
-- @param Wrapper.Group#GROUP AIGroup
function AI_AIR:onafterHome( AIGroup, From, Event, To )
  self:F( { AIGroup, From, Event, To } )

  self:T( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" )
  
  if AIGroup and AIGroup:IsAlive() then
  end

end



-- @param #AI_AIR self
-- @param Wrapper.Group#GROUP AIGroup
function AI_AIR:onafterHold( AIGroup, From, Event, To, HoldTime )
  self:F( { AIGroup, From, Event, To } )

  self:T( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" )
  
  if AIGroup and AIGroup:IsAlive() then
    local Coordinate = AIGroup:GetCoordinate()
    if Coordinate == nil then return end
    local OrbitTask = AIGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed, Coordinate )
    local TimedOrbitTask = AIGroup:TaskControlled( OrbitTask, AIGroup:TaskCondition( nil, nil, nil, nil, HoldTime , nil ) )
    
    local RTBTask = AIGroup:TaskFunction( "AI_AIR.RTBHold", self )
    
    local OrbitHoldTask = AIGroup:TaskOrbitCircle( 4000, self.PatrolMinSpeed )
    
    --AIGroup:SetState( AIGroup, "AI_AIR", self )
    
    AIGroup:SetTask( AIGroup:TaskCombo( { TimedOrbitTask, RTBTask, OrbitHoldTask } ), 1 )
  end

end

-- @param Wrapper.Group#GROUP AIGroup
function AI_AIR.Resume( AIGroup, Fsm )

  AIGroup:T( { "AI_AIR.Resume:", AIGroup:GetName() } )
  if AIGroup:IsAlive() then
    Fsm:__RTB( Fsm.TaskDelay )
  end
  
end

-- @param #AI_AIR self
-- @param Wrapper.Group#GROUP AIGroup
function AI_AIR:onafterRefuel( AIGroup, From, Event, To )
  self:F( { AIGroup, From, Event, To } )

  if AIGroup and AIGroup:IsAlive() then
  
    -- Get tanker group.
    local Tanker = GROUP:FindByName( self.TankerName )

    if Tanker and Tanker:IsAlive() and Tanker:IsAirPlane() then

      self:T( "Group " .. self.Controllable:GetName() .. " ... Refuelling! State=" .. self:GetState() .. ", Refuelling tanker " .. self.TankerName )

      local RefuelRoute = {}
  
      --- Calculate the target route point.
      
      local FromRefuelCoord = AIGroup:GetCoordinate()
      local ToRefuelCoord = Tanker:GetCoordinate()
      local ToRefuelSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed )
      
      --- Create a route point of type air.
      local FromRefuelRoutePoint = FromRefuelCoord:WaypointAir(self.PatrolAltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, ToRefuelSpeed, true)

      --- Create a route point of type air. NOT used!
      local ToRefuelRoutePoint = Tanker:GetCoordinate():WaypointAir(self.PatrolAltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, ToRefuelSpeed, true)
  
      self:F( { ToRefuelSpeed = ToRefuelSpeed } )
      
      RefuelRoute[#RefuelRoute+1] = FromRefuelRoutePoint
      RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint
      
      AIGroup:OptionROEHoldFire()
      AIGroup:OptionROTEvadeFire()
      
      -- Get Class name for .Resume function
      local classname=self:GetClassName()
      
      -- AI_A2A_CAP can call this function but does not have a .Resume function. Try to fix.
      if classname=="AI_A2A_CAP" then
        classname="AI_AIR_PATROL"
      end
      
      env.info("FF refueling classname="..classname)
  
      local Tasks = {}
      Tasks[#Tasks+1] = AIGroup:TaskRefueling()
      Tasks[#Tasks+1] = AIGroup:TaskFunction(  classname .. ".Resume", self )
      RefuelRoute[#RefuelRoute].task = AIGroup:TaskCombo( Tasks )
  
      AIGroup:Route( RefuelRoute, self.TaskDelay )
      
    else
    
      -- No tanker defined ==> RTB!
      self:RTB()
      
    end
    
  end

end
    


-- @param #AI_AIR self
function AI_AIR:onafterDead()
  self:SetStatusOff()
end


-- @param #AI_AIR self
-- @param Core.Event#EVENTDATA EventData
function AI_AIR:OnCrash( EventData )

  if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then
    if #self.Controllable:GetUnits() == 1 then
      self:__Crash( self.TaskDelay, EventData )
    end
  end
end

-- @param #AI_AIR self
-- @param Core.Event#EVENTDATA EventData
function AI_AIR:OnEjection( EventData )

  if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then
    self:__Eject( self.TaskDelay, EventData )
  end
end

-- @param #AI_AIR self
-- @param Core.Event#EVENTDATA EventData
function AI_AIR:OnPilotDead( EventData )

  if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then
    self:__PilotDead( self.TaskDelay, EventData )
  end
end
--- **AI** - Models the process of A2G patrolling and engaging ground targets for airplanes and helicopters.
--
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_Air_Patrol
-- @image AI_Air_To_Ground_Patrol.JPG

-- @type AI_AIR_PATROL
-- @extends AI.AI_Air#AI_AIR

--- The AI_AIR_PATROL class implements the core functions to patrol a @{Core.Zone} by an AI @{Wrapper.Group}
-- and automatically engage any airborne enemies that are within a certain range or within a certain zone.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- ![Process](..\Presentations\AI_CAP\Dia3.JPG)
-- 
-- The AI_AIR_PATROL is assigned a @{Wrapper.Group} and this must be done before the AI_AIR_PATROL process can be started using the **Start** event.
-- 
-- ![Process](..\Presentations\AI_CAP\Dia4.JPG)
-- 
-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits.
-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits.
-- 
-- ![Process](..\Presentations\AI_CAP\Dia5.JPG)
-- 
-- This cycle will continue.
-- 
-- ![Process](..\Presentations\AI_CAP\Dia6.JPG)
-- 
-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event.
--
-- ![Process](..\Presentations\AI_CAP\Dia9.JPG)
-- 
-- When enemies are detected, the AI will automatically engage the enemy.
-- 
-- ![Process](..\Presentations\AI_CAP\Dia10.JPG)
-- 
-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB.
-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land.
-- 
-- ![Process](..\Presentations\AI_CAP\Dia13.JPG)
-- 
-- ## 1. AI_AIR_PATROL constructor
--   
--   * @{#AI_AIR_PATROL.New}(): Creates a new AI_AIR_PATROL object.
-- 
-- ## 2. AI_AIR_PATROL is a FSM
-- 
-- ![Process](..\Presentations\AI_CAP\Dia2.JPG)
-- 
-- ### 2.1 AI_AIR_PATROL States
-- 
--   * **None** ( Group ): The process is not started yet.
--   * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone.
--   * **Engaging** ( Group ): The AI is engaging the bogeys.
--   * **Returning** ( Group ): The AI is returning to Base..
-- 
-- ### 2.2 AI_AIR_PATROL Events
-- 
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.PatrolRoute}**: Route the AI to a new random 3D point within the Patrol Zone.
--   * **@{#AI_AIR_PATROL.Engage}**: Let the AI engage the bogeys.
--   * **@{#AI_AIR_PATROL.Abort}**: Aborts the engagement and return patrolling in the patrol zone.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets.
--   * **@{#AI_AIR_PATROL.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}.
--   * **@{#AI_AIR_PATROL.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task.
--   * **Status** ( Group ): The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB.
--
-- ## 3. Set the Range of Engagement
-- 
-- ![Range](..\Presentations\AI_CAP\Dia11.JPG)
-- 
-- An optional range can be set in meters, 
-- that will define when the AI will engage with the detected airborne enemy targets.
-- The range can be beyond or smaller than the range of the Patrol Zone.
-- The range is applied at the position of the AI.
-- Use the method @{#AI_AIR_PATROL.SetEngageRange}() to define that range.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_AIR_PATROL
AI_AIR_PATROL = {
  ClassName = "AI_AIR_PATROL",
}

--- Creates a new AI_AIR_PATROL object
-- @param #AI_AIR_PATROL self
-- @param AI.AI_Air#AI_AIR AI_Air The AI_AIR FSM.
-- @param Wrapper.Group#GROUP AIGroup The AI group.
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Altitude PatrolFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the patrol.
-- @param DCS#Speed  PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO.
-- @return #AI_AIR_PATROL
function AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )

  -- Inherits from BASE
  local self = BASE:Inherit( self, AI_Air ) -- #AI_AIR_PATROL

  local SpeedMax = AIGroup:GetSpeedMax()

  self.PatrolZone = PatrolZone

  self.PatrolFloorAltitude = PatrolFloorAltitude or 1000
  self.PatrolCeilingAltitude = PatrolCeilingAltitude or 1500
  self.PatrolMinSpeed = PatrolMinSpeed or SpeedMax * 0.5
  self.PatrolMaxSpeed = PatrolMaxSpeed or SpeedMax * 0.75

  -- defafult PatrolAltType to "RADIO" if not specified
  self.PatrolAltType = PatrolAltType or "RADIO"

  self:AddTransition( { "Started", "Airborne", "Refuelling" }, "Patrol", "Patrolling" )

  --- OnBefore Transition Handler for Event Patrol.
  -- @function [parent=#AI_AIR_PATROL] OnBeforePatrol
  -- @param #AI_AIR_PATROL self
  -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.

  --- OnAfter Transition Handler for Event Patrol.
  -- @function [parent=#AI_AIR_PATROL] OnAfterPatrol
  -- @param #AI_AIR_PATROL self
  -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.

  --- Synchronous Event Trigger for Event Patrol.
  -- @function [parent=#AI_AIR_PATROL] Patrol
  -- @param #AI_AIR_PATROL self

  --- Asynchronous Event Trigger for Event Patrol.
  -- @function [parent=#AI_AIR_PATROL] __Patrol
  -- @param #AI_AIR_PATROL self
  -- @param #number Delay The delay in seconds.

  --- OnLeave Transition Handler for State Patrolling.
  -- @function [parent=#AI_AIR_PATROL] OnLeavePatrolling
  -- @param #AI_AIR_PATROL self
  -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.

  --- OnEnter Transition Handler for State Patrolling.
  -- @function [parent=#AI_AIR_PATROL] OnEnterPatrolling
  -- @param #AI_AIR_PATROL self
  -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.

    self:AddTransition( "Patrolling", "PatrolRoute", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_PATROL.

  --- OnBefore Transition Handler for Event PatrolRoute.
  -- @function [parent=#AI_AIR_PATROL] OnBeforePatrolRoute
  -- @param #AI_AIR_PATROL self
  -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.

  --- OnAfter Transition Handler for Event PatrolRoute.
  -- @function [parent=#AI_AIR_PATROL] OnAfterPatrolRoute
  -- @param #AI_AIR_PATROL self
  -- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.

  --- Synchronous Event Trigger for Event PatrolRoute.
  -- @function [parent=#AI_AIR_PATROL] PatrolRoute
  -- @param #AI_AIR_PATROL self

  --- Asynchronous Event Trigger for Event PatrolRoute.
  -- @function [parent=#AI_AIR_PATROL] __PatrolRoute
  -- @param #AI_AIR_PATROL self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_PATROL.

  return self
end

--- Set the Engage Range when the AI will engage with airborne enemies. 
-- @param #AI_AIR_PATROL self
-- @param #number EngageRange The Engage Range.
-- @return #AI_AIR_PATROL self
function AI_AIR_PATROL:SetEngageRange( EngageRange )
  self:F2()

  if EngageRange then  
    self.EngageRange = EngageRange
  else
    self.EngageRange = nil
  end
end

--- Set race track parameters. CAP flights will perform race track patterns rather than randomly patrolling the zone.
-- @param #AI_AIR_PATROL self
-- @param #number LegMin Min Length of the race track leg in meters. Default 10,000 m.
-- @param #number LegMax Max length of the race track leg in meters. Default 15,000 m.
-- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North.
-- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from South to North.
-- @param #number DurationMin (Optional) Min duration before switching the orbit position. Default is keep same orbit until RTB or engage.
-- @param #number DurationMax (Optional) Max duration before switching the orbit position. Default is keep same orbit until RTB or engage.
-- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading. 
-- @return #AI_AIR_PATROL self
function AI_AIR_PATROL:SetRaceTrackPattern(LegMin, LegMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates)

  self.racetrack=true
  self.racetracklegmin=LegMin or 10000
  self.racetracklegmax=LegMax or 15000
  self.racetrackheadingmin=HeadingMin or 0
  self.racetrackheadingmax=HeadingMax or 180
  self.racetrackdurationmin=DurationMin
  self.racetrackdurationmax=DurationMax

  if self.racetrackdurationmax and not self.racetrackdurationmin then
    self.racetrackdurationmin=self.racetrackdurationmax
  end

  self.racetrackcapcoordinates=CapCoordinates

end

--- Defines a new patrol route using the @{AI.AI_Patrol#AI_PATROL_ZONE} parameters and settings.
-- @param #AI_AIR_PATROL self
-- @return #AI_AIR_PATROL self
-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_AIR_PATROL:onafterPatrol( AIPatrol, From, Event, To )
  self:F2()

  self:ClearTargetDistance()

  self:__PatrolRoute( self.TaskDelay )

  AIPatrol:OnReSpawn(
    function( PatrolGroup )
      self:__Reset( self.TaskDelay )
      self:__PatrolRoute( self.TaskDelay )
    end
  )
end

--- This static method is called from the route path within the last task at the last waypoint of the AIPatrol.
-- Note that this method is required, as triggers the next route when patrolling for the AIPatrol.
-- @param Wrapper.Group#GROUP AIPatrol The AI group.
-- @param #AI_AIR_PATROL Fsm The FSM.
function AI_AIR_PATROL.___PatrolRoute( AIPatrol, Fsm )

  AIPatrol:F( { "AI_AIR_PATROL.___PatrolRoute:", AIPatrol:GetName() } )

  if AIPatrol and AIPatrol:IsAlive() then
    Fsm:PatrolRoute()
  end

end

--- Defines a new patrol route using the @{AI.AI_Patrol#AI_PATROL_ZONE} parameters and settings.
-- @param #AI_AIR_PATROL self
-- @param Wrapper.Group#GROUP AIPatrol The Group managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_AIR_PATROL:onafterPatrolRoute( AIPatrol, From, Event, To )

  self:F2()

  -- When RTB, don't allow anymore the routing.
  if From == "RTB" then
    return
  end

  if AIPatrol and  AIPatrol:IsAlive() then

    local PatrolRoute = {}

    --- Calculate the target route point.

    local CurrentCoord = AIPatrol:GetCoordinate()

    local altitude= math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude )

    local ToTargetCoord = self.PatrolZone:GetRandomPointVec2()
    ToTargetCoord:SetAlt( altitude )
    self:SetTargetDistance( ToTargetCoord ) -- For RTB status check

    local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed )
    local speedkmh=ToTargetSpeed

    local FromWP = CurrentCoord:WaypointAir(self.PatrolAltType or "RADIO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, ToTargetSpeed, true)
    PatrolRoute[#PatrolRoute+1] = FromWP

    if self.racetrack then

      -- Random heading.
      local heading = math.random(self.racetrackheadingmin, self.racetrackheadingmax)

      -- Random leg length.
      local leg=math.random(self.racetracklegmin, self.racetracklegmax)

      -- Random duration if any.
      local duration = self.racetrackdurationmin
      if self.racetrackdurationmax then
        duration=math.random(self.racetrackdurationmin, self.racetrackdurationmax)
      end

      -- CAP coordinate.
      local c0=self.PatrolZone:GetRandomCoordinate()
      if self.racetrackcapcoordinates and #self.racetrackcapcoordinates>0 then
        c0=self.racetrackcapcoordinates[math.random(#self.racetrackcapcoordinates)]
      end

      -- Race track points.
      local c1=c0:SetAltitude(altitude) --Core.Point#COORDINATE
      local c2=c1:Translate(leg, heading):SetAltitude(altitude)

      self:SetTargetDistance(c0) -- For RTB status check

      -- Debug:
      self:T(string.format("Patrol zone race track: v=%.1f knots, h=%.1f ft, heading=%03d, leg=%d m, t=%s sec", UTILS.KmphToKnots(speedkmh), UTILS.MetersToFeet(altitude), heading, leg, tostring(duration)))
      --c1:MarkToAll("Race track c1")
      --c2:MarkToAll("Race track c2")

      -- Task to orbit.              
      local taskOrbit=AIPatrol:TaskOrbit(c1, altitude, UTILS.KmphToMps(speedkmh), c2)

      -- Task function to redo the patrol at other random position.
      local taskPatrol=AIPatrol:TaskFunction("AI_AIR_PATROL.___PatrolRoute", self)

      -- Controlled task with task condition.
      local taskCond=AIPatrol:TaskCondition(nil, nil, nil, nil, duration, nil)
      local taskCont=AIPatrol:TaskControlled(taskOrbit, taskCond)

      -- Second waypoint
      PatrolRoute[2]=c1:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskCont, taskPatrol}, "CAP Orbit")

    else

      --- Create a route point of type air.
      local ToWP = ToTargetCoord:WaypointAir(self.PatrolAltType, COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, ToTargetSpeed, true)  
      PatrolRoute[#PatrolRoute+1] = ToWP
      
      local Tasks = {}
      Tasks[#Tasks+1] = AIPatrol:TaskFunction("AI_AIR_PATROL.___PatrolRoute", self)
      PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks )

    end

    AIPatrol:OptionROEReturnFire()
    AIPatrol:OptionROTEvadeFire()

    AIPatrol:Route( PatrolRoute, self.TaskDelay )

  end

end

--- Resumes the AIPatrol
-- @param Wrapper.Group#GROUP AIPatrol
-- @param Core.Fsm#FSM Fsm
function AI_AIR_PATROL.Resume( AIPatrol, Fsm )

  AIPatrol:F( { "AI_AIR_PATROL.Resume:", AIPatrol:GetName() } )
  if AIPatrol and AIPatrol:IsAlive() then
    Fsm:__Reset( Fsm.TaskDelay )
    Fsm:__PatrolRoute( Fsm.TaskDelay )
  end

end
--- **AI** - Models the process of air to ground engagement for airplanes and helicopters.
--
-- This is a class used in the @{AI.AI_A2G_Dispatcher}.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_Air_Engage
-- @image AI_Air_To_Ground_Engage.JPG



--- @type AI_AIR_ENGAGE
-- @extends AI.AI_AIR#AI_AIR


--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- The AI_AIR_ENGAGE is assigned a @{Wrapper.Group} and this must be done before the AI_AIR_ENGAGE process can be started using the **Start** event.
-- 
-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits.
-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits.
-- 
-- This cycle will continue.
-- 
-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event.
-- 
-- When enemies are detected, the AI will automatically engage the enemy.
-- 
-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB.
-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land.
-- 
-- ## 1. AI_AIR_ENGAGE constructor
--   
--   * @{#AI_AIR_ENGAGE.New}(): Creates a new AI_AIR_ENGAGE object.
--
-- ## 2. Set the Zone of Engagement
--
-- An optional @{Core.Zone} can be set,
-- that will define when the AI will engage with the detected airborne enemy targets.
-- Use the method @{AI.AI_CAP#AI_AIR_ENGAGE.SetEngageZone}() to define that Zone.
  --
  -- # Developer Note
  -- 
  -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
  -- Therefore, this class is considered to be deprecated
  --
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_AIR_ENGAGE
AI_AIR_ENGAGE = {
  ClassName = "AI_AIR_ENGAGE",
}



--- Creates a new AI_AIR_ENGAGE object
-- @param #AI_AIR_ENGAGE self
-- @param AI.AI_Air#AI_AIR AI_Air The AI_AIR FSM.
-- @param Wrapper.Group#GROUP AIGroup The AI group.
-- @param DCS#Speed EngageMinSpeed (optional, default = 50% of max speed) The minimum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Speed  EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement.
-- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement.
-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO".
-- @return #AI_AIR_ENGAGE
function AI_AIR_ENGAGE:New( AI_Air, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )

  -- Inherits from BASE
  local self = BASE:Inherit( self, AI_Air ) -- #AI_AIR_ENGAGE

  self.Accomplished = false
  self.Engaging = false
  
  local SpeedMax = AIGroup:GetSpeedMax()
  
  self.EngageMinSpeed = EngageMinSpeed or SpeedMax * 0.5
  self.EngageMaxSpeed = EngageMaxSpeed or SpeedMax * 0.75
  self.EngageFloorAltitude = EngageFloorAltitude or 1000
  self.EngageCeilingAltitude = EngageCeilingAltitude or 1500
  self.EngageAltType = EngageAltType or "RADIO"
  
  self:AddTransition( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "EngageRoute", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE.

  --- OnBefore Transition Handler for Event EngageRoute.
  -- @function [parent=#AI_AIR_ENGAGE] OnBeforeEngageRoute
  -- @param #AI_AIR_ENGAGE self
  -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event EngageRoute.
  -- @function [parent=#AI_AIR_ENGAGE] OnAfterEngageRoute
  -- @param #AI_AIR_ENGAGE self
  -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event EngageRoute.
  -- @function [parent=#AI_AIR_ENGAGE] EngageRoute
  -- @param #AI_AIR_ENGAGE self
  
  --- Asynchronous Event Trigger for Event EngageRoute.
  -- @function [parent=#AI_AIR_ENGAGE] __EngageRoute
  -- @param #AI_AIR_ENGAGE self
  -- @param #number Delay The delay in seconds.

--- OnLeave Transition Handler for State Engaging.
-- @function [parent=#AI_AIR_ENGAGE] OnLeaveEngaging
-- @param #AI_AIR_ENGAGE self
-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnEnter Transition Handler for State Engaging.
-- @function [parent=#AI_AIR_ENGAGE] OnEnterEngaging
-- @param #AI_AIR_ENGAGE self
-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

  self:AddTransition( { "Started", "Engaging", "Returning", "Airborne", "Patrolling" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE.

  --- OnBefore Transition Handler for Event Engage.
  -- @function [parent=#AI_AIR_ENGAGE] OnBeforeEngage
  -- @param #AI_AIR_ENGAGE self
  -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Engage.
  -- @function [parent=#AI_AIR_ENGAGE] OnAfterEngage
  -- @param #AI_AIR_ENGAGE self
  -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
    
  --- Synchronous Event Trigger for Event Engage.
  -- @function [parent=#AI_AIR_ENGAGE] Engage
  -- @param #AI_AIR_ENGAGE self
  
  --- Asynchronous Event Trigger for Event Engage.
  -- @function [parent=#AI_AIR_ENGAGE] __Engage
  -- @param #AI_AIR_ENGAGE self
  -- @param #number Delay The delay in seconds.

--- OnLeave Transition Handler for State Engaging.
-- @function [parent=#AI_AIR_ENGAGE] OnLeaveEngaging
-- @param #AI_AIR_ENGAGE self
-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnEnter Transition Handler for State Engaging.
-- @function [parent=#AI_AIR_ENGAGE] OnEnterEngaging
-- @param #AI_AIR_ENGAGE self
-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

  self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE.
  
  --- OnBefore Transition Handler for Event Fired.
  -- @function [parent=#AI_AIR_ENGAGE] OnBeforeFired
  -- @param #AI_AIR_ENGAGE self
  -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Fired.
  -- @function [parent=#AI_AIR_ENGAGE] OnAfterFired
  -- @param #AI_AIR_ENGAGE self
  -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Fired.
  -- @function [parent=#AI_AIR_ENGAGE] Fired
  -- @param #AI_AIR_ENGAGE self
  
  --- Asynchronous Event Trigger for Event Fired.
  -- @function [parent=#AI_AIR_ENGAGE] __Fired
  -- @param #AI_AIR_ENGAGE self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE.

  --- OnBefore Transition Handler for Event Destroy.
  -- @function [parent=#AI_AIR_ENGAGE] OnBeforeDestroy
  -- @param #AI_AIR_ENGAGE self
  -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Destroy.
  -- @function [parent=#AI_AIR_ENGAGE] OnAfterDestroy
  -- @param #AI_AIR_ENGAGE self
  -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Destroy.
  -- @function [parent=#AI_AIR_ENGAGE] Destroy
  -- @param #AI_AIR_ENGAGE self
  
  --- Asynchronous Event Trigger for Event Destroy.
  -- @function [parent=#AI_AIR_ENGAGE] __Destroy
  -- @param #AI_AIR_ENGAGE self
  -- @param #number Delay The delay in seconds.


  self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE.

  --- OnBefore Transition Handler for Event Abort.
  -- @function [parent=#AI_AIR_ENGAGE] OnBeforeAbort
  -- @param #AI_AIR_ENGAGE self
  -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Abort.
  -- @function [parent=#AI_AIR_ENGAGE] OnAfterAbort
  -- @param #AI_AIR_ENGAGE self
  -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Abort.
  -- @function [parent=#AI_AIR_ENGAGE] Abort
  -- @param #AI_AIR_ENGAGE self
  
  --- Asynchronous Event Trigger for Event Abort.
  -- @function [parent=#AI_AIR_ENGAGE] __Abort
  -- @param #AI_AIR_ENGAGE self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_AIR_ENGAGE.

  --- OnBefore Transition Handler for Event Accomplish.
  -- @function [parent=#AI_AIR_ENGAGE] OnBeforeAccomplish
  -- @param #AI_AIR_ENGAGE self
  -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Accomplish.
  -- @function [parent=#AI_AIR_ENGAGE] OnAfterAccomplish
  -- @param #AI_AIR_ENGAGE self
  -- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Accomplish.
  -- @function [parent=#AI_AIR_ENGAGE] Accomplish
  -- @param #AI_AIR_ENGAGE self
  
  --- Asynchronous Event Trigger for Event Accomplish.
  -- @function [parent=#AI_AIR_ENGAGE] __Accomplish
  -- @param #AI_AIR_ENGAGE self
  -- @param #number Delay The delay in seconds.  

  self:AddTransition( { "Patrolling", "Engaging" }, "Refuel", "Refuelling" ) 

  return self
end

--- onafter event handler for Start event.
-- @param #AI_AIR_ENGAGE self
-- @param Wrapper.Group#GROUP AIGroup The AI group managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_AIR_ENGAGE:onafterStart( AIGroup, From, Event, To )

  self:GetParent( self, AI_AIR_ENGAGE ).onafterStart( self, AIGroup, From, Event, To )

  AIGroup:HandleEvent( EVENTS.Takeoff, nil, self )

end



--- onafter event handler for Engage event.
-- @param #AI_AIR_ENGAGE self
-- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_AIR_ENGAGE:onafterEngage( AIGroup, From, Event, To )
  -- TODO: This function is overwritten below!
  self:HandleEvent( EVENTS.Dead )
end

-- todo: need to fix this global function


--- onbefore event handler for Engage event.
-- @param #AI_AIR_ENGAGE self
-- @param Wrapper.Group#GROUP AIGroup The group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_AIR_ENGAGE:onbeforeEngage( AIGroup, From, Event, To )
  if self.Accomplished == true then
    return false
  end  
  return true
end

--- onafter event handler for Abort event.
-- @param #AI_AIR_ENGAGE self
-- @param Wrapper.Group#GROUP AIGroup The AI Group managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_AIR_ENGAGE:onafterAbort( AIGroup, From, Event, To )
  AIGroup:ClearTasks()
  self:Return()
end


-- @param #AI_AIR_ENGAGE self
-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_AIR_ENGAGE:onafterAccomplish( AIGroup, From, Event, To )
  self.Accomplished = true
  --self:SetDetectionOff()
end

-- @param #AI_AIR_ENGAGE self
-- @param Wrapper.Group#GROUP AIGroup The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @param Core.Event#EVENTDATA EventData
function AI_AIR_ENGAGE:onafterDestroy( AIGroup, From, Event, To, EventData )

  if EventData.IniUnit then
    self.AttackUnits[EventData.IniUnit] = nil
  end
end

-- @param #AI_AIR_ENGAGE self
-- @param Core.Event#EVENTDATA EventData
function AI_AIR_ENGAGE:OnEventDead( EventData )
  self:F( { "EventDead", EventData } )

  if EventData.IniDCSUnit then
    if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then
      self:__Destroy( self.TaskDelay, EventData )
    end
  end  
end


-- @param Wrapper.Group#GROUP AIControllable
function AI_AIR_ENGAGE.___EngageRoute( AIGroup, Fsm, AttackSetUnit )
  Fsm:T(string.format("AI_AIR_ENGAGE.___EngageRoute: %s", tostring(AIGroup:GetName())))
  
  if AIGroup and AIGroup:IsAlive() then
    Fsm:__EngageRoute( Fsm.TaskDelay or 0.1, AttackSetUnit )
  end
end


-- @param #AI_AIR_ENGAGE self
-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @param Core.Set#SET_UNIT AttackSetUnit Unit set to be attacked.
function AI_AIR_ENGAGE:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit )
  self:T( { DefenderGroup, From, Event, To, AttackSetUnit } )
  
  local DefenderGroupName = DefenderGroup:GetName()

  self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air!

  local AttackCount = AttackSetUnit:CountAlive()
  
  if AttackCount > 0 then

    if DefenderGroup:IsAlive() then

      local EngageAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude )
      local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed )

      -- Determine the distance to the target.
      -- If it is less than 10km, then attack without a route.
      -- Otherwise perform a route attack.

      local DefenderCoord = DefenderGroup:GetPointVec3()
      DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude.

      local TargetCoord = AttackSetUnit:GetRandomSurely():GetPointVec3()
      
      if TargetCoord == nil then
        self:Return()
        return
      end
      
      TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude.
      
      local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord )
      local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 )
      
      -- TODO: A factor of * 3 is way too close. This causes the AI not to engange until merged sometimes!
      if TargetDistance <= EngageDistance * 9 then

        --self:T(string.format("AI_AIR_ENGAGE onafterEngageRoute ==> __Engage - target distance = %.1f km", TargetDistance/1000))
        self:__Engage( 0.1, AttackSetUnit )

      else
      
        --self:T(string.format("FF AI_AIR_ENGAGE onafterEngageRoute ==> Routing - target distance = %.1f km", TargetDistance/1000))

        local EngageRoute = {}
        local AttackTasks = {}
        
        --- Calculate the target route point.
        
        local FromWP = DefenderCoord:WaypointAir(self.PatrolAltType or "RADIO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, EngageSpeed, true)
        
        EngageRoute[#EngageRoute+1] = FromWP
  
        self:SetTargetDistance( TargetCoord ) -- For RTB status check
        
        local FromEngageAngle = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) )
        local ToCoord=DefenderCoord:Translate( EngageDistance, FromEngageAngle, true )
        
        local ToWP = ToCoord:WaypointAir(self.PatrolAltType or "RADIO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, EngageSpeed, true)
  
        EngageRoute[#EngageRoute+1] = ToWP

        AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_AIR_ENGAGE.___EngageRoute", self, AttackSetUnit )
        EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks )
        
        DefenderGroup:OptionROEReturnFire()
        DefenderGroup:OptionROTEvadeFire()
        
        DefenderGroup:Route( EngageRoute, self.TaskDelay or 0.1 )
      end

    end
  else
    -- TODO: This will make an A2A Dispatcher CAP flight to return rather than going back to patrolling!
    self:T( DefenderGroupName .. ": No targets found -> Going RTB")
    self:Return()
  end
end


-- @param Wrapper.Group#GROUP AIControllable
function AI_AIR_ENGAGE.___Engage( AIGroup, Fsm, AttackSetUnit )

  Fsm:T(string.format("AI_AIR_ENGAGE.___Engage: %s", tostring(AIGroup:GetName())))
  
  if AIGroup and AIGroup:IsAlive() then
    local delay=Fsm.TaskDelay or 0.1
    Fsm:__Engage(delay, AttackSetUnit)
  end
end


-- @param #AI_AIR_ENGAGE self
-- @param Wrapper.Group#GROUP DefenderGroup The GroupGroup managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @param Core.Set#SET_UNIT AttackSetUnit Set of units to be attacked.
function AI_AIR_ENGAGE:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit )
  self:F( { DefenderGroup, From, Event, To, AttackSetUnit} )
  
  local DefenderGroupName = DefenderGroup:GetName()

  self.AttackSetUnit = AttackSetUnit -- Kept in memory in case of resume from refuel in air!

  local AttackCount = AttackSetUnit:CountAlive()
  self:T({AttackCount = AttackCount})
  
  if AttackCount > 0 then

    if DefenderGroup and DefenderGroup:IsAlive() then

      local EngageAltitude = math.random( self.EngageFloorAltitude or 500, self.EngageCeilingAltitude or 1000 )
      local EngageSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed )

      local DefenderCoord = DefenderGroup:GetPointVec3()
      DefenderCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude.

      local TargetCoord = AttackSetUnit:GetRandomSurely():GetPointVec3()
      if not TargetCoord then
          self:Return()
          return
      end
      TargetCoord:SetY( EngageAltitude ) -- Ground targets don't have an altitude.
      
      local TargetDistance = DefenderCoord:Get2DDistance( TargetCoord )
      
      local EngageDistance = ( DefenderGroup:IsHelicopter() and 5000 ) or ( DefenderGroup:IsAirPlane() and 10000 )
      
      local EngageRoute = {}
      local AttackTasks = {}
      
      local FromWP = DefenderCoord:WaypointAir(self.EngageAltType or "RADIO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, EngageSpeed, true)
      EngageRoute[#EngageRoute+1] = FromWP

      self:SetTargetDistance( TargetCoord ) -- For RTB status check

      local FromEngageAngle = DefenderCoord:GetAngleDegrees( DefenderCoord:GetDirectionVec3( TargetCoord ) )
      local ToCoord=DefenderCoord:Translate( EngageDistance, FromEngageAngle, true )
      
      local ToWP = ToCoord:WaypointAir(self.EngageAltType or "RADIO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, EngageSpeed, true)
      EngageRoute[#EngageRoute+1] = ToWP
      
      -- TODO: A factor of * 3 this way too low. This causes the AI NOT to engage until very close or even merged sometimes. Some A2A missiles have a much longer range! Needs more frequent updates of the task!
      if TargetDistance <= EngageDistance * 9 then
      
        local AttackUnitTasks = self:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude ) -- Polymorphic
        
        if #AttackUnitTasks == 0 then
          self:T( DefenderGroupName .. ": No valid targets found -> Going RTB")
          self:Return()
          return
        else
          local text=string.format("%s: Engaging targets at distance %.2f NM", DefenderGroupName, UTILS.MetersToNM(TargetDistance))
          self:T(text)
          DefenderGroup:OptionROEOpenFire()
          DefenderGroup:OptionROTEvadeFire()
          DefenderGroup:OptionKeepWeaponsOnThreat()
  
          AttackTasks[#AttackTasks+1] = DefenderGroup:TaskCombo( AttackUnitTasks )
        end
      end

      AttackTasks[#AttackTasks+1] = DefenderGroup:TaskFunction( "AI_AIR_ENGAGE.___Engage", self, AttackSetUnit )
      EngageRoute[#EngageRoute].task = DefenderGroup:TaskCombo( AttackTasks )
      
      DefenderGroup:Route( EngageRoute, self.TaskDelay or 0.1 )
      
    end
  else
    -- TODO: This will make an A2A Dispatcher CAP flight to return rather than going back to patrolling!
    self:T( DefenderGroupName .. ": No targets found -> returning.")
    self:Return()
    return
  end
end

-- @param Wrapper.Group#GROUP AIEngage
function AI_AIR_ENGAGE.Resume( AIEngage, Fsm )

  AIEngage:F( { "Resume:", AIEngage:GetName() } )
  if AIEngage and AIEngage:IsAlive() then
    Fsm:__Reset( Fsm.TaskDelay or 0.1 )
    Fsm:__EngageRoute( Fsm.TaskDelay or 0.2, Fsm.AttackSetUnit )
  end
  
end
--- **AI** - Models the process of air patrol of airplanes.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===
-- 
-- @module AI.AI_A2A_Patrol
-- @image AI_Air_Patrolling.JPG


-- @type AI_A2A_PATROL
-- @extends AI.AI_A2A#AI_A2A

--- Implements the core functions to patrol a @{Core.Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group}.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia3.JPG)
-- 
-- The AI_A2A_PATROL is assigned a @{Wrapper.Group} and this must be done before the AI_A2A_PATROL process can be started using the **Start** event.
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia4.JPG)
-- 
-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits.
-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits.
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia5.JPG)
-- 
-- This cycle will continue.
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia6.JPG)
-- 
-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event.
--
-- ![Process](..\Presentations\AI_PATROL\Dia9.JPG)
-- 
---- Note that the enemy is not engaged! To model enemy engagement, either tailor the **Detected** event, or
-- use derived AI_ classes to model AI offensive or defensive behaviour.
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia10.JPG)
-- 
-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB.
-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land.
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia11.JPG)
-- 
-- ## 1. AI_A2A_PATROL constructor
--   
--   * @{#AI_A2A_PATROL.New}(): Creates a new AI_A2A_PATROL object.
-- 
-- ## 2. AI_A2A_PATROL is a FSM
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia2.JPG)
-- 
-- ### 2.1. AI_A2A_PATROL States
-- 
--   * **None** ( Group ): The process is not started yet.
--   * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone.
--   * **Returning** ( Group ): The AI is returning to Base.
--   * **Stopped** ( Group ): The process is stopped.
--   * **Crashed** ( Group ): The AI has crashed or is dead.
-- 
-- ### 2.2. AI_A2A_PATROL Events
-- 
--   * **Start** ( Group ): Start the process.
--   * **Stop** ( Group ): Stop the process.
--   * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone.
--   * **RTB** ( Group ): Route the AI to the home base.
--   * **Detect** ( Group ): The AI is detecting targets.
--   * **Detected** ( Group ): The AI has detected new targets.
--   * **Status** ( Group ): The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB.
--    
-- ## 3. Set or Get the AI controllable
-- 
--   * @{#AI_A2A_PATROL.SetControllable}(): Set the AIControllable.
--   * @{#AI_A2A_PATROL.GetControllable}(): Get the AIControllable.
--
-- ## 4. Set the Speed and Altitude boundaries of the AI controllable
--
--   * @{#AI_A2A_PATROL.SetSpeed}(): Set the patrol speed boundaries of the AI, for the next patrol.
--   * @{#AI_A2A_PATROL.SetAltitude}(): Set altitude boundaries of the AI, for the next patrol.
-- 
-- ## 5. Manage the detection process of the AI controllable
-- 
-- The detection process of the AI controllable can be manipulated.
-- Detection requires an amount of CPU power, which has an impact on your mission performance.
-- Only put detection on when absolutely necessary, and the frequency of the detection can also be set.
-- 
--   * @{#AI_A2A_PATROL.SetDetectionOn}(): Set the detection on. The AI will detect for targets.
--   * @{#AI_A2A_PATROL.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased.
-- 
-- The detection frequency can be set with @{#AI_A2A_PATROL.SetRefreshTimeInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection.
-- Use the method @{#AI_A2A_PATROL.GetDetectedUnits}() to obtain a list of the @{Wrapper.Unit}s detected by the AI.
-- 
-- The detection can be filtered to potential targets in a specific zone.
-- Use the method @{#AI_A2A_PATROL.SetDetectionZone}() to set the zone where targets need to be detected.
-- Note that when the zone is too far away, or the AI is not heading towards the zone, or the AI is too high, no targets may be detected
-- according the weather conditions.
-- 
-- ## 6. Manage the "out of fuel" in the AI_A2A_PATROL
-- 
-- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base.
-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel threshold is calculated.
-- When the fuel threshold is reached, the AI will continue for a given time its patrol task in orbit, 
-- while a new AI is targeted to the AI_A2A_PATROL.
-- Once the time is finished, the old AI will return to the base.
-- Use the method @{#AI_A2A_PATROL.ManageFuel}() to have this proces in place.
-- 
-- ## 7. Manage "damage" behaviour of the AI in the AI_A2A_PATROL
-- 
-- When the AI is damaged, it is required that a new Patrol is started. However, damage cannon be foreseen early on. 
-- Therefore, when the damage threshold is reached, the AI will return immediately to the home base (RTB).
-- Use the method @{#AI_A2A_PATROL.ManageDamage}() to have this proces in place.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_A2A_PATROL
AI_A2A_PATROL = {
  ClassName = "AI_A2A_PATROL",
}

--- Creates a new AI_A2A_PATROL object
-- @param #AI_A2A_PATROL self
-- @param Wrapper.Group#GROUP AIPatrol The patrol group object.
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to BARO
-- @return #AI_A2A_PATROL self
-- @usage
-- -- Define a new AI_A2A_PATROL Object. This PatrolArea will patrol a Group within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h.
-- PatrolZone = ZONE:New( 'PatrolZone' )
-- PatrolSpawn = SPAWN:New( 'Patrol Group' )
-- PatrolArea = AI_A2A_PATROL:New( PatrolZone, 3000, 6000, 600, 900 )
function AI_A2A_PATROL:New( AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )

  local AI_Air = AI_AIR:New( AIPatrol )
  local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIPatrol, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType  )
  local self = BASE:Inherit( self, AI_Air_Patrol ) -- #AI_A2A_PATROL
  
  self:SetFuelThreshold( .2, 60 )
  self:SetDamageThreshold( 0.4 )
  self:SetDisengageRadius( 70000 )

  
  self.PatrolZone = PatrolZone
  self.PatrolFloorAltitude = PatrolFloorAltitude
  self.PatrolCeilingAltitude = PatrolCeilingAltitude
  self.PatrolMinSpeed = PatrolMinSpeed
  self.PatrolMaxSpeed = PatrolMaxSpeed
  
  -- defafult PatrolAltType to "BARO" if not specified
  self.PatrolAltType = PatrolAltType or "BARO"
  
  self:AddTransition( { "Started", "Airborne", "Refuelling" }, "Patrol", "Patrolling" )

--- OnBefore Transition Handler for Event Patrol.
-- @function [parent=#AI_A2A_PATROL] OnBeforePatrol
-- @param #AI_A2A_PATROL self
-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnAfter Transition Handler for Event Patrol.
-- @function [parent=#AI_A2A_PATROL] OnAfterPatrol
-- @param #AI_A2A_PATROL self
-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
	
--- Synchronous Event Trigger for Event Patrol.
-- @function [parent=#AI_A2A_PATROL] Patrol
-- @param #AI_A2A_PATROL self

--- Asynchronous Event Trigger for Event Patrol.
-- @function [parent=#AI_A2A_PATROL] __Patrol
-- @param #AI_A2A_PATROL self
-- @param #number Delay The delay in seconds.

--- OnLeave Transition Handler for State Patrolling.
-- @function [parent=#AI_A2A_PATROL] OnLeavePatrolling
-- @param #AI_A2A_PATROL self
-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnEnter Transition Handler for State Patrolling.
-- @function [parent=#AI_A2A_PATROL] OnEnterPatrolling
-- @param #AI_A2A_PATROL self
-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

  self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_PATROL.

--- OnBefore Transition Handler for Event Route.
-- @function [parent=#AI_A2A_PATROL] OnBeforeRoute
-- @param #AI_A2A_PATROL self
-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnAfter Transition Handler for Event Route.
-- @function [parent=#AI_A2A_PATROL] OnAfterRoute
-- @param #AI_A2A_PATROL self
-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
	
--- Synchronous Event Trigger for Event Route.
-- @function [parent=#AI_A2A_PATROL] Route
-- @param #AI_A2A_PATROL self

--- Asynchronous Event Trigger for Event Route.
-- @function [parent=#AI_A2A_PATROL] __Route
-- @param #AI_A2A_PATROL self
-- @param #number Delay The delay in seconds.



  self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_PATROL.
  
  return self
end




--- Sets (modifies) the minimum and maximum speed of the patrol.
-- @param #AI_A2A_PATROL self
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h.
-- @return #AI_A2A_PATROL self
function AI_A2A_PATROL:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed )
  self:F2( { PatrolMinSpeed, PatrolMaxSpeed } )
  
  self.PatrolMinSpeed = PatrolMinSpeed
  self.PatrolMaxSpeed = PatrolMaxSpeed
end



--- Sets the floor and ceiling altitude of the patrol.
-- @param #AI_A2A_PATROL self
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @return #AI_A2A_PATROL self
function AI_A2A_PATROL:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude )
  self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } )
  
  self.PatrolFloorAltitude = PatrolFloorAltitude
  self.PatrolCeilingAltitude = PatrolCeilingAltitude
end


--- Defines a new patrol route using the @{AI.AI_Patrol#AI_PATROL_ZONE} parameters and settings.
-- @param #AI_A2A_PATROL self
-- @return #AI_A2A_PATROL self
-- @param Wrapper.Group#GROUP AIPatrol The Group Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_A2A_PATROL:onafterPatrol( AIPatrol, From, Event, To )
  self:F2()

  self:ClearTargetDistance()

  self:__Route( 1 )
  
  AIPatrol:OnReSpawn(
    function( PatrolGroup )
      self:__Reset( 1 )
      self:__Route( 5 )
    end
  )
end


--- This static method is called from the route path within the last task at the last waypoint of the AIPatrol.
-- Note that this method is required, as triggers the next route when patrolling for the AIPatrol.
-- @param Wrapper.Group#GROUP AIPatrol The AI group.
-- @param #AI_A2A_PATROL Fsm The FSM.
function AI_A2A_PATROL.PatrolRoute( AIPatrol, Fsm )

  AIPatrol:F( { "AI_A2A_PATROL.PatrolRoute:", AIPatrol:GetName() } )

  if AIPatrol and AIPatrol:IsAlive() then
    Fsm:Route()
  end
  
end


--- Defines a new patrol route using the @{AI.AI_Patrol#AI_PATROL_ZONE} parameters and settings.
-- @param #AI_A2A_PATROL self
-- @param Wrapper.Group#GROUP AIPatrol The Group managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_A2A_PATROL:onafterRoute( AIPatrol, From, Event, To )
  self:F2()

  -- When RTB, don't allow anymore the routing.
  if From == "RTB" then
    return
  end

  
  if AIPatrol and AIPatrol:IsAlive() then
    
    local PatrolRoute = {}

    --- Calculate the target route point.
    
    local CurrentCoord = AIPatrol:GetCoordinate()
    
    -- Random altitude.
    local altitude=math.random(self.PatrolFloorAltitude, self.PatrolCeilingAltitude)
      
    -- Random speed in km/h.
    local speedkmh = math.random(self.PatrolMinSpeed, self.PatrolMaxSpeed)
    
    -- First waypoint is current position.
    PatrolRoute[1]=CurrentCoord:WaypointAirTurningPoint(nil, speedkmh, {}, "Current")    
    
    if self.racetrack then
      
      -- Random heading.
      local heading = math.random(self.racetrackheadingmin, self.racetrackheadingmax)
      
      -- Random leg length.
      local leg=math.random(self.racetracklegmin, self.racetracklegmax)
      
      -- Random duration if any.
      local duration = self.racetrackdurationmin
      if self.racetrackdurationmax then
        duration=math.random(self.racetrackdurationmin, self.racetrackdurationmax)
      end
      
      -- CAP coordinate.
      local c0=self.PatrolZone:GetRandomCoordinate()
      if self.racetrackcapcoordinates and #self.racetrackcapcoordinates>0 then
        c0=self.racetrackcapcoordinates[math.random(#self.racetrackcapcoordinates)]
      end
      
      -- Race track points.
      local c1=c0:SetAltitude(altitude) --Core.Point#COORDINATE
      local c2=c1:Translate(leg, heading):SetAltitude(altitude)
      
      self:SetTargetDistance(c0) -- For RTB status check
      
      -- Debug:
      self:T(string.format("Patrol zone race track: v=%.1f knots, h=%.1f ft, heading=%03d, leg=%d m, t=%s sec", UTILS.KmphToKnots(speedkmh), UTILS.MetersToFeet(altitude), heading, leg, tostring(duration)))
      --c1:MarkToAll("Race track c1")
      --c2:MarkToAll("Race track c2")

      -- Task to orbit.              
      local taskOrbit=AIPatrol:TaskOrbit(c1, altitude, UTILS.KmphToMps(speedkmh), c2)
      
      -- Task function to redo the patrol at other random position.
      local taskPatrol=AIPatrol:TaskFunction("AI_A2A_PATROL.PatrolRoute", self)
      
      -- Controlled task with task condition.
      local taskCond=AIPatrol:TaskCondition(nil, nil, nil, nil, duration, nil)
      local taskCont=AIPatrol:TaskControlled(taskOrbit, taskCond)
      
      -- Second waypoint
      PatrolRoute[2]=c1:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskCont, taskPatrol}, "CAP Orbit")

    else

      -- Target coordinate.        
      local ToTargetCoord=self.PatrolZone:GetRandomCoordinate() --Core.Point#COORDINATE
      ToTargetCoord:SetAltitude(altitude)
      
      self:SetTargetDistance( ToTargetCoord ) -- For RTB status check
      
      local taskReRoute=AIPatrol:TaskFunction( "AI_A2A_PATROL.PatrolRoute", self )
      
      PatrolRoute[2]=ToTargetCoord:WaypointAirTurningPoint(self.PatrolAltType, speedkmh, {taskReRoute}, "Patrol Point")
            
    end

    -- ROE    
    AIPatrol:OptionROEReturnFire()
    AIPatrol:OptionROTEvadeFire()
  
    -- Patrol.
    AIPatrol:Route( PatrolRoute, 0.5)
  end

end

--- **AI** - Models the process of Combat Air Patrol (CAP) for airplanes.
--
-- This is a class used in the @{AI.AI_A2A_Dispatcher}.
--
-- ===
--
-- ### Author: **FlightControl**
--
-- ===
--
-- @module AI.AI_A2A_Cap
-- @image AI_Combat_Air_Patrol.JPG

-- @type AI_A2A_CAP
-- @extends AI.AI_Air_Patrol#AI_AIR_PATROL
-- @extends AI.AI_Air_Engage#AI_AIR_ENGAGE

--- The AI_A2A_CAP class implements the core functions to patrol a @{Core.Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group}
-- and automatically engage any airborne enemies that are within a certain range or within a certain zone.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- ![Process](..\Presentations\AI_CAP\Dia3.JPG)
--
-- The AI_A2A_CAP is assigned a @{Wrapper.Group} and this must be done before the AI_A2A_CAP process can be started using the **Start** event.
--
-- ![Process](..\Presentations\AI_CAP\Dia4.JPG)
--
-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits.
-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits.
--
-- ![Process](..\Presentations\AI_CAP\Dia5.JPG)
--
-- This cycle will continue.
--
-- ![Process](..\Presentations\AI_CAP\Dia6.JPG)
--
-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event.
--
-- ![Process](..\Presentations\AI_CAP\Dia9.JPG)
--
-- When enemies are detected, the AI will automatically engage the enemy.
--
-- ![Process](..\Presentations\AI_CAP\Dia10.JPG)
--
-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB.
-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land.
--
-- ![Process](..\Presentations\AI_CAP\Dia13.JPG)
--
-- ## 1. AI_A2A_CAP constructor
--
--   * @{#AI_A2A_CAP.New}(): Creates a new AI_A2A_CAP object.
--
-- ## 2. AI_A2A_CAP is a FSM
--
-- ![Process](..\Presentations\AI_CAP\Dia2.JPG)
--
-- ### 2.1 AI_A2A_CAP States
--
--   * **None** ( Group ): The process is not started yet.
--   * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone.
--   * **Engaging** ( Group ): The AI is engaging the bogeys.
--   * **Returning** ( Group ): The AI is returning to Base..
--
-- ### 2.2 AI_A2A_CAP Events
--
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone.
--   * **@{#AI_A2A_CAP.Engage}**: Let the AI engage the bogeys.
--   * **@{#AI_A2A_CAP.Abort}**: Aborts the engagement and return patrolling in the patrol zone.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets.
--   * **@{#AI_A2A_CAP.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}.
--   * **@{#AI_A2A_CAP.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task.
--   * **Status** ( Group ): The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB.
--
-- ## 3. Set the Range of Engagement
--
-- ![Range](..\Presentations\AI_CAP\Dia11.JPG)
--
-- An optional range can be set in meters,
-- that will define when the AI will engage with the detected airborne enemy targets.
-- The range can be beyond or smaller than the range of the Patrol Zone.
-- The range is applied at the position of the AI.
-- Use the method @{#AI_A2A_CAP.SetEngageRange}() to define that range.
--
-- ## 4. Set the Zone of Engagement
--
-- ![Zone](..\Presentations\AI_CAP\Dia12.JPG)
--
-- An optional @{Core.Zone} can be set,
-- that will define when the AI will engage with the detected airborne enemy targets.
-- Use the method @{#AI_A2A_CAP.SetEngageZone}() to define that Zone.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
--
-- @field #AI_A2A_CAP
AI_A2A_CAP = {
  ClassName = "AI_A2A_CAP",
}

--- Creates a new AI_A2A_CAP object
-- @param #AI_A2A_CAP self
-- @param Wrapper.Group#GROUP AICap
-- @param DCS#Speed  EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Speed  EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement.
-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement.
-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO".
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO".
-- @return #AI_A2A_CAP
function AI_A2A_CAP:New2( AICap, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType )

  -- Multiple inheritance ... :-)
  local AI_Air = AI_AIR:New( AICap )
  local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )
  local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AICap, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )
  local self = BASE:Inherit( self, AI_Air_Engage ) --#AI_A2A_CAP

  self:SetFuelThreshold( .2, 60 )
  self:SetDamageThreshold( 0.4 )
  self:SetDisengageRadius( 70000 )


  return self
end

--- Creates a new AI_A2A_CAP object
-- @param #AI_A2A_CAP self
-- @param Wrapper.Group#GROUP AICap
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#Speed  EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Speed  EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO
-- @return #AI_A2A_CAP
function AI_A2A_CAP:New( AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, PatrolAltType )

  return self:New2( AICap, EngageMinSpeed, EngageMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, PatrolZone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType )

end

--- onafter State Transition for Event Patrol.
-- @param #AI_A2A_CAP self
-- @param Wrapper.Group#GROUP AICap The AI Group managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_A2A_CAP:onafterStart( AICap, From, Event, To )

  self:GetParent( self, AI_A2A_CAP ).onafterStart( self, AICap, From, Event, To )
  AICap:HandleEvent( EVENTS.Takeoff, nil, self )

end

--- Set the Engage Zone which defines where the AI will engage bogies.
-- @param #AI_A2A_CAP self
-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP.
-- @return #AI_A2A_CAP self
function AI_A2A_CAP:SetEngageZone( EngageZone )
  self:F2()

  if EngageZone then
    self.EngageZone = EngageZone
  else
    self.EngageZone = nil
  end
end

--- Set the Engage Range when the AI will engage with airborne enemies.
-- @param #AI_A2A_CAP self
-- @param #number EngageRange The Engage Range.
-- @return #AI_A2A_CAP self
function AI_A2A_CAP:SetEngageRange( EngageRange )
  self:F2()

  if EngageRange then
    self.EngageRange = EngageRange
  else
    self.EngageRange = nil
  end
end

--- Evaluate the attack and create an AttackUnitTask list.
-- @param #AI_A2A_CAP self
-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack.
-- @param Wrapper.Group#GROUP DefenderGroup The group of defenders.
-- @param #number EngageAltitude The altitude to engage the targets.
-- @return #AI_A2A_CAP self
function AI_A2A_CAP:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude )

  local AttackUnitTasks = {}

  for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do
    local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT
    if AttackUnit and AttackUnit:IsAlive() and AttackUnit:IsAir() then
      -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition()
      -- Maybe the detected set also contains
      self:T( { "Attacking Task:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } )
      AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit )
    end
  end

  return AttackUnitTasks
end
--- **AI** - Models the process of Ground Controlled Interception (GCI) for airplanes.
--
-- This is a class used in the @{AI.AI_A2A_Dispatcher}.
--
-- ===
--
-- ### Author: **FlightControl**
--
-- ===
--
-- @module AI.AI_A2A_Gci
-- @image AI_Ground_Control_Intercept.JPG



-- @type AI_A2A_GCI
-- @extends AI.AI_A2A#AI_A2A


--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders.
--
-- ![Banner Image](..\Images\deprecated.png)
--
-- The AI_A2A_GCI is assigned a @{Wrapper.Group} and this must be done before the AI_A2A_GCI process can be started using the **Start** event.
--
-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits.
-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits.
--
-- This cycle will continue.
--
-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event.
--
-- When enemies are detected, the AI will automatically engage the enemy.
--
-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB.
-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land.
--
-- ## 1. AI_A2A_GCI constructor
--
--   * @{#AI_A2A_GCI.New}(): Creates a new AI_A2A_GCI object.
--
-- ## 2. AI_A2A_GCI is a FSM
--
-- ![Process](..\Presentations\AI_GCI\Dia2.JPG)
--
-- ### 2.1 AI_A2A_GCI States
--
--   * **None** ( Group ): The process is not started yet.
--   * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone.
--   * **Engaging** ( Group ): The AI is engaging the bogeys.
--   * **Returning** ( Group ): The AI is returning to Base..
--
-- ### 2.2 AI_A2A_GCI Events
--
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone.
--   * **@{#AI_A2A_GCI.Engage}**: Let the AI engage the bogeys.
--   * **@{#AI_A2A_GCI.Abort}**: Aborts the engagement and return patrolling in the patrol zone.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets.
--   * **@{#AI_A2A_GCI.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}.
--   * **@{#AI_A2A_GCI.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task.
--   * **Status** ( Group ): The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB.
--
-- # Developer Note
--
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
--
-- @field #AI_A2A_GCI
AI_A2A_GCI = {
  ClassName = "AI_A2A_GCI",
}



--- Creates a new AI_A2A_GCI object
-- @param #AI_A2A_GCI self
-- @param Wrapper.Group#GROUP AIIntercept
-- @param DCS#Speed  EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Speed  EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement.
-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement.
-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO".
-- @return #AI_A2A_GCI
function AI_A2A_GCI:New2( AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )

  local AI_Air = AI_AIR:New( AIIntercept )
  local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air, AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )
  local self = BASE:Inherit( self, AI_Air_Engage ) -- #AI_A2A_GCI

  self:SetFuelThreshold( .2, 60 )
  self:SetDamageThreshold( 0.4 )
  self:SetDisengageRadius( 70000 )

  return self
end

--- Creates a new AI_A2A_GCI object
-- @param #AI_A2A_GCI self
-- @param Wrapper.Group#GROUP AIIntercept
-- @param DCS#Speed  EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Speed  EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement.
-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement.
-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO".
-- @return #AI_A2A_GCI
function AI_A2A_GCI:New( AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )

  return self:New2( AIIntercept, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )
end

--- onafter State Transition for Event Patrol.
-- @param #AI_A2A_GCI self
-- @param Wrapper.Group#GROUP AIIntercept The AI Group managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_A2A_GCI:onafterStart( AIIntercept, From, Event, To )

  self:GetParent( self, AI_A2A_GCI ).onafterStart( self, AIIntercept, From, Event, To )
end


--- Evaluate the attack and create an AttackUnitTask list.
-- @param #AI_A2A_GCI self
-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack.
-- @param Wrapper.Group#GROUP DefenderGroup The group of defenders.
-- @param #number EngageAltitude The altitude to engage the targets.
-- @return #AI_A2A_GCI self
function AI_A2A_GCI:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude )

  local AttackUnitTasks = {}

  for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do
    local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT
    self:T( { "Attacking Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } )
    if AttackUnit:IsAlive() and AttackUnit:IsAir() then
      -- TODO: Add coalition check? Only attack units of if AttackUnit:GetCoalition()~=AICap:GetCoalition()
      -- Maybe the detected set also contains
      AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit )
    end
  end

  return AttackUnitTasks
end
--- **AI** - Manages the process of an automatic A2A defense system based on an EWR network targets and coordinating CAP and GCI.
--
-- ===
--
-- Features:
--
--    * Setup quickly an A2A defense system for a coalition.
--    * Setup (CAP) Control Air Patrols at defined zones to enhance your A2A defenses.
--    * Setup (GCI) Ground Control Intercept at defined airbases to enhance your A2A defenses.
--    * Define and use an EWR (Early Warning Radar) network.
--    * Define squadrons at airbases.
--    * Enable airbases for A2A defenses.
--    * Add different plane types to different squadrons.
--    * Add multiple squadrons to different airbases.
--    * Define different ranges to engage upon intruders.
--    * Establish an automatic in air refuel process for CAP using refuel tankers.
--    * Setup default settings for all squadrons and A2A defenses.
--    * Setup specific settings for specific squadrons.
--    * Quickly setup an A2A defense system using @{#AI_A2A_GCICAP}.
--    * Setup a more advanced defense system using @{#AI_A2A_DISPATCHER}.
--
-- ===
--
-- ## Missions:
--
-- [AID-A2A - AI A2A Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher)
--
-- ===
--
-- ## YouTube Channel:
--
-- [DCS WORLD - MOOSE - A2A GCICAP - Build an automatic A2A Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx)
--
-- ===
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- # QUICK START GUIDE
--
-- There are basically two classes available to model an A2A defense system.
--
-- AI\_A2A\_DISPATCHER is the main A2A defense class that models the A2A defense system.
-- AI\_A2A\_GCICAP derives or inherits from AI\_A2A\_DISPATCHER and is a more **noob** user friendly class, but is less flexible.
--
-- Before you start using the AI\_A2A\_DISPATCHER or AI\_A2A\_GCICAP ask yourself the following questions.
--
-- ## 0. Do I need AI\_A2A\_DISPATCHER or do I need AI\_A2A\_GCICAP?
--
-- AI\_A2A\_GCICAP, automates a lot of the below questions using the mission editor and requires minimal lua scripting.
-- But the AI\_A2A\_GCICAP provides less flexibility and a lot of options are defaulted.
-- With AI\_A2A\_DISPATCHER you can setup a much more **fine grained** A2A defense mechanism, but some more (easy) lua scripting is required.
--
-- ## 1. Which Coalition am I modeling an A2A defense system for? blue or red?
--
-- One AI\_A2A\_DISPATCHER object can create a defense system for **one coalition**, which is blue or red.
-- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI\_A2A\_DISPATCHER **objects**,
-- each governing their defense system.
--
--
-- ## 2. Which type of EWR will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow).
--
-- The MOOSE framework leverages the @{Functional.Detection} classes to perform the EWR detection.
-- Several types of @{Functional.Detection} classes exist, and the most common characteristics of these classes is that they:
--
--    * Perform detections from multiple FACs as one co-operating entity.
--    * Communicate with a Head Quarters, which consolidates each detection.
--    * Groups detections based on a method (per area, per type or per unit).
--    * Communicates detections.
--
-- ## 3. Which EWR units will be used as part of the detection system? Only Ground or also Airborne?
--
-- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units.
-- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US).
-- Additionally, ANY other radar capable unit can be part of the EWR network! Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar.
-- The position of these units is very important as they need to provide enough coverage
-- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them.
--
-- ## 4. Is a border required?
--
-- Is this a cold war or a hot war situation? In case of a cold war situation, a border can be set that will only trigger defenses
-- if the border is crossed by enemy units.
--
-- ## 5. What maximum range needs to be checked to allow defenses to engage any attacker?
--
-- A good functioning defense will have a "maximum range" evaluated to the enemy when CAP will be engaged or GCI will be spawned.
--
-- ## 6. Which Airbases, Carrier Ships, FARPs will take part in the defense system for the Coalition?
--
-- Carefully plan which airbases will take part in the coalition. Color each airbase in the color of the coalition.
--
-- ## 7. Which Squadrons will I create and which name will I give each Squadron?
--
-- The defense system works with Squadrons. Each Squadron must be given a unique name, that forms the **key** to the defense system.
-- Several options and activities can be set per Squadron.
--
-- ## 8. Where will the Squadrons be located? On Airbases? On Carrier Ships? On FARPs?
--
-- Squadrons are placed as the "home base" on an airfield, carrier or farp.
-- Carefully plan where each Squadron will be located as part of the defense system.
--
-- ## 9. Which plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron?
--
-- Per Squadron, one or multiple plane models can be allocated as **Templates**.
-- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**.
-- The A2A defense system will select from the given templates a random template to spawn a new plane (group).
--
-- ## 10. Which payloads, skills and skins will these plane models have?
--
-- Per Squadron, even if you have one plane model, you can still allocate multiple templates of one plane model,
-- each having different payloads, skills and skins.
-- The A2A defense system will select from the given templates a random template to spawn a new plane (group).
--
-- ## 11. For each Squadron, which will perform CAP?
--
-- Per Squadron, evaluate which Squadrons will perform CAP.
-- Not all Squadrons need to perform CAP.
--
-- ## 12. For each Squadron doing CAP, in which ZONE(s) will the CAP be performed?
--
-- Per CAP, evaluate **where** the CAP will be performed, in other words, define the **zone**.
-- Near the border or a bit further away?
--
-- ## 13. For each Squadron doing CAP, which zone types will I create?
--
-- Per CAP zone, evaluate whether you want:
--
--    * simple trigger zones
--    * polygon zones
--    * moving zones
--
-- Depending on the type of zone selected, a different @{Core.Zone} object needs to be created from a ZONE_ class.
--
-- ## 14. For each Squadron doing CAP, what are the time intervals and CAP amounts to be performed?
--
-- For each CAP:
--
--    * **How many** CAP you want to have airborne at the same time?
--    * **How frequent** you want the defense mechanism to check whether to start a new CAP?
--
-- ## 15. For each Squadron, which will perform GCI?
--
-- For each Squadron, evaluate which Squadrons will perform GCI?
-- Not all Squadrons need to perform GCI.
--
-- ## 16. For each Squadron, which takeoff method will I use?
--
-- For each Squadron, evaluate which takeoff method will be used:
--
--    * Straight from the air
--    * From the runway
--    * From a parking spot with running engines
--    * From a parking spot with cold engines
--
-- **The default takeoff method is straight in the air.**
--
-- ## 17. For each Squadron, which landing method will I use?
--
-- For each Squadron, evaluate which landing method will be used:
--
--    * Despawn near the airbase when returning
--    * Despawn after landing on the runway
--    * Despawn after engine shutdown after landing
--
-- **The default landing method is despawn when near the airbase when returning.**
--
-- ## 18. For each Squadron, which overhead will I use?
--
-- For each Squadron, depending on the airplane type (modern, old) and payload, which overhead is required to provide any defense?
-- In other words, if **X** attacker airplanes are detected, how many **Y** defense airplanes need to be spawned per squadron?
-- The **Y** is dependent on the type of airplane (era), payload, fuel levels, skills etc.
-- The overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected.
--
-- **The default overhead is 1. A value greater than 1, like 1.5 will increase the overhead with 50%, a value smaller than 1, like 0.5 will decrease the overhead with 50%.**
--
-- ## 19. For each Squadron, which grouping will I use?
--
-- When multiple targets are detected, how will defense airplanes be grouped when multiple defense airplanes are spawned for multiple attackers?
-- Per one, two, three, four?
--
-- **The default grouping is 1. That means, that each spawned defender will act individually.**
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
--
-- ### Authors: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons).
-- ### Authors: **Stonehouse**, **SNAFU** in terms of the advice, documentation, and the original GCICAP script.
--
-- @module AI.AI_A2A_Dispatcher
-- @image AI_Air_To_Air_Dispatching.JPG

do -- AI_A2A_DISPATCHER

  --- AI_A2A_DISPATCHER class.
  -- @type AI_A2A_DISPATCHER
  -- @extends Tasking.DetectionManager#DETECTION_MANAGER

  --- Create an automatic air defence system for a coalition.
  --
  -- ===
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia3.JPG)
  --
  -- It includes automatic spawning of Combat Air Patrol aircraft (CAP) and Ground Controlled Intercept aircraft (GCI) in response to enemy air movements that are detected by a ground based radar network.
  -- CAP flights will take off and proceed to designated CAP zones where they will remain on station until the ground radars direct them to intercept detected enemy aircraft or they run short of fuel and must return to base (RTB). When a CAP flight leaves their zone to perform an interception or return to base a new CAP flight will spawn to take their place.
  -- If all CAP flights are engaged or RTB then additional GCI interceptors will scramble to intercept unengaged enemy aircraft under ground radar control.
  -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system.
  -- In short it is a plug in very flexible and configurable air defence module for DCS World.
  --
  -- Note that in order to create a two way A2A defense system, two AI\_A2A\_DISPATCHER defense system may need to be created, for each coalition one.
  -- This is a good implementation, because maybe in the future, more coalitions may become available in DCS world.
  --
  -- ===
  --
  -- # USAGE GUIDE
  --
  -- ## 1. AI\_A2A\_DISPATCHER constructor:
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_1.JPG)
  --
  --
  -- The @{#AI_A2A_DISPATCHER.New}() method creates a new AI\_A2A\_DISPATCHER instance.
  --
  -- ### 1.1. Define the **EWR network**:
  --
  -- As part of the AI\_A2A\_DISPATCHER :New() constructor, an EWR network must be given as the first parameter.
  -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy.
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia5.JPG)
  --
  -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units.
  -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US).
  -- Additionally, ANY other radar capable unit can be part of the EWR network! Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar.
  -- The position of these units is very important as they need to provide enough coverage
  -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them.
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia7.JPG)
  --
  -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates.
  -- For example if they are a long way forward and can detect enemy planes on the ground and taking off
  -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition.
  -- Having the radars further back will mean a slower escalation because fewer targets will be detected and
  -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map.
  -- It all depends on what the desired effect is.
  --
  -- EWR networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection#DETECTION_BASE} object that is given as the input parameter of the AI\_A2A\_DISPATCHER class.
  -- By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network,
  -- increasing or decreasing the radar coverage of the Early Warning System.
  --
  -- See the following example to setup an EWR network containing EWR stations and AWACS.
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_2.JPG)
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_3.JPG)
  --
  --     -- Define a SET_GROUP object that builds a collection of groups that define the EWR network.
  --     -- Here we build the network with all the groups that have a name starting with DF CCCP AWACS and DF CCCP EWR.
  --     DetectionSetGroup = SET_GROUP:New()
  --     DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } )
  --     DetectionSetGroup:FilterStart()
  --
  --     -- Setup the detection and group targets to a 30km range!
  --     Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 )
  --
  --     -- Setup the A2A dispatcher, and initialize it.
  --     A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **DetectionSetGroup**.
  -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with **DF CCCP AWACS** or **DF CCCP EWR** to be included in the Set.
  -- **DetectionSetGroup** is then being ordered to start the dynamic filtering. Note that any destroy or new spawn of a group with the above names will be removed or added to the Set.
  --
  -- Then a new Detection object is created from the class DETECTION_AREAS. A grouping radius of 30000 is chosen, which is 30km.
  -- The **Detection** object is then passed to the @{#AI_A2A_DISPATCHER.New}() method to indicate the EWR network configuration and setup the A2A defense detection mechanism.
  --
  -- You could build a **mutual defense system** like this:
  --
  --     A2ADispatcher_Red = AI_A2A_DISPATCHER:New( EWR_Red )
  --     A2ADispatcher_Blue = AI_A2A_DISPATCHER:New( EWR_Blue )
  --
  -- ### 1.2. Define the detected **target grouping radius**:
  --
  -- The target grouping radius is a property of the Detection object, that was passed to the AI\_A2A\_DISPATCHER object, but can be changed.
  -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation.
  -- Fast planes like in the 80s, need a larger radius than WWII planes.
  -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft.
  --
  -- Note that detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate
  -- group being detected. This may result in additional GCI being started by the dispatcher! So don't make this value too small!
  --
  -- ## 3. Set the **Engage Radius**:
  --
  -- Define the **Engage Radius** to **engage any target by airborne friendlies**,
  -- which are executing **cap** or **returning** from an intercept mission.
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia10.JPG)
  --
  -- If there is a target area detected and reported,
  -- then any friendlies that are airborne near this target area,
  -- will be commanded to (re-)engage that target when available (if no other tasks were commanded).
  --
  -- For example, if **50000** or **50km** is given as a value, then any friendly that is airborne within **50km** from the detected target,
  -- will be considered to receive the command to engage that target area.
  --
  -- You need to evaluate the value of this parameter carefully:
  --
  --   * If too small, more intercept missions may be triggered upon detected target areas.
  --   * If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far.
  --
  -- The **default** Engage Radius is defined as **100000** or **100km**.
  -- Use the method @{#AI_A2A_DISPATCHER.SetEngageRadius}() to set a specific Engage Radius.
  -- **The Engage Radius is defined for ALL squadrons which are operational.**
  --
  -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher/AID-A2A-019%20-%20Engage%20Range%20Test)
  --
  -- In this example an Engage Radius is set to various values.
  --
  --     -- Set 50km as the radius to engage any target by airborne friendlies.
  --     A2ADispatcher:SetEngageRadius( 50000 )
  --
  --     -- Set 100km as the radius to engage any target by airborne friendlies.
  --     A2ADispatcher:SetEngageRadius() -- 100000 is the default value.
  --
  --
  -- ## 4. Set the **Ground Controlled Intercept Radius** or **Gci radius**:
  --
  -- When targets are detected that are still really far off, you don't want the AI_A2A_DISPATCHER to launch intercepts just yet.
  -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target**
  -- being **smaller** than the **Ground Controlled Intercept radius** or **Gci radius**.
  --
  -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or,
  -- when you don't want to let the AI_A2A_DISPATCHER react immediately when a certain border or area is not being crossed.
  --
  -- Use the method @{#AI_A2A_DISPATCHER.SetGciRadius}() to set a specific controlled ground intercept radius.
  -- **The Ground Controlled Intercept radius is defined for ALL squadrons which are operational.**
  --
  -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher/AID-A2A-013%20-%20Intercept%20Test)
  --
  -- In these examples, the Gci Radius is set to various values:
  --
  --     -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --     A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  --     -- Set 100km as the radius to ground control intercept detected targets from the nearest airbase.
  --     A2ADispatcher:SetGciRadius( 100000 )
  --
  --     -- Set 200km as the radius to ground control intercept.
  --     A2ADispatcher:SetGciRadius() -- 200000 is the default value.
  --
  -- ## 5. Set the **borders**:
  --
  -- According to the tactical and strategic design of the mission broadly decide the shape and extent of red and blue territories.
  -- They should be laid out such that a border area is created between the two coalitions.
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia4.JPG)
  --
  -- **Define a border area to simulate a cold war scenario.**
  -- Use the method @{#AI_A2A_DISPATCHER.SetBorderZone}() to create a border zone for the dispatcher.
  --
  -- A **cold war** is one where CAP aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border.
  -- A **hot war** is one where CAP aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send CAP and GCI aircraft to attack it.
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia9.JPG)
  --
  -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE}.
  -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than
  -- it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are.
  -- In a hot war the borders are effectively defined by the ground based radar coverage of a coalition.
  --
  -- Demonstration Mission: [AID-009 - AI_A2A - Border Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher/AID-A2A-009%20-%20Border%20Test)
  --
  -- In this example a border is set for the CCCP A2A dispatcher:
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_4.JPG)
  --
  --     -- Setup the A2A dispatcher, and initialize it.
  --     A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  --     -- Setup the border.
  --     -- Initialize the dispatcher, setting up a border zone. This is a polygon,
  --     -- which takes the waypoints of a late activated group with the name CCCP Border as the boundaries of the border area.
  --     -- Any enemy crossing this border will be engaged.
  --
  --     CCCPBorderZone = ZONE_POLYGON:New( "CCCP Border", GROUP:FindByName( "CCCP Border" ) )
  --     A2ADispatcher:SetBorderZone( CCCPBorderZone )
  --
  -- ## 6. Squadrons:
  --
  -- The AI\_A2A\_DISPATCHER works with **Squadrons**, that need to be defined using the different methods available.
  --
  -- Use the method @{#AI_A2A_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield,
  -- while defining which plane types are being used by the squadron and how many resources are available.
  --
  -- Squadrons:
  --
  --   * Have name (string) that is the identifier or key of the squadron.
  --   * Have specific plane types.
  --   * Are located at one airbase.
  --   * Optionally have a limited set of resources. The default is that squadrons have **unlimited resources**.
  --
  -- The name of the squadron given acts as the **squadron key** in the AI\_A2A\_DISPATCHER:Squadron...() methods.
  --
  -- Additionally, squadrons have specific configuration options to:
  --
  --   * Control how new aircraft are taking off from the airfield (in the air, cold, hot, at the runway).
  --   * Control how returning aircraft are landing at the airfield (in the air near the airbase, after landing, after engine shutdown).
  --   * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped.
  --   * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned.
  --
  -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft.
  --
  -- This example defines a couple of squadrons. Note the templates defined within the Mission Editor.
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_5.JPG)
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_6.JPG)
  --
  --      -- Setup the squadrons.
  --      A2ADispatcher:SetSquadron( "Mineralnye", AIRBASE.Caucasus.Mineralnye_Vody, { "SQ CCCP SU-27" }, 20 )
  --      A2ADispatcher:SetSquadron( "Maykop", AIRBASE.Caucasus.Maykop_Khanskaya, { "SQ CCCP MIG-31" }, 20 )
  --      A2ADispatcher:SetSquadron( "Mozdok", AIRBASE.Caucasus.Mozdok, { "SQ CCCP MIG-31" }, 20 )
  --      A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-27" }, 20 )
  --      A2ADispatcher:SetSquadron( "Novo", AIRBASE.Caucasus.Novorossiysk, { "SQ CCCP SU-27" }, 20 )
  --
  -- ### 6.1. Set squadron take-off methods
  --
  -- Use the various SetSquadronTakeoff... methods to control how squadrons are taking-off from the airfield:
  --
  --   * @{#AI_A2A_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield.
  --
  -- **The default take-off method is to spawn new aircraft directly in the air.**
  --
  -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency.
  -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that:
  --
  --   * aircraft will stop waiting for each other or for a landing aircraft before takeoff.
  --   * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other.
  --   * aircraft may collide at the airbase.
  --   * aircraft may be awaiting the landing of a plane currently in the air, but never lands ...
  --
  -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs.
  -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues!
  --
  -- This example sets the default takeoff method to be from the runway.
  -- And for a couple of squadrons overrides this default method.
  --
  --      -- Setup the Takeoff methods
  --
  --      -- The default takeoff
  --      A2ADispatcher:SetDefaultTakeOffFromRunway()
  --
  --      -- The individual takeoff per squadron
  --      A2ADispatcher:SetSquadronTakeoff( "Mineralnye", AI_A2A_DISPATCHER.Takeoff.Air )
  --      A2ADispatcher:SetSquadronTakeoffInAir( "Sochi" )
  --      A2ADispatcher:SetSquadronTakeoffFromRunway( "Mozdok" )
  --      A2ADispatcher:SetSquadronTakeoffFromParkingCold( "Maykop" )
  --      A2ADispatcher:SetSquadronTakeoffFromParkingHot( "Novo" )
  --
  --
  -- ### 6.1. Set Squadron takeoff altitude when spawning new aircraft in the air.
  --
  -- In the case of the @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() there is also an other parameter that can be applied.
  -- That is modifying or setting the **altitude** from where planes spawn in the air.
  -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAirAltitude}() to set the altitude for a specific squadron.
  -- The default takeoff altitude can be modified or set using the method @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAirAltitude}().
  -- As part of the method @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() a parameter can be specified to set the takeoff altitude.
  -- If this parameter is not specified, then the default altitude will be used for the squadron.
  --
  -- ### 6.2. Set squadron landing methods
  --
  -- In analogy with takeoff, the landing methods are to control how squadrons land at the airfield:
  --
  --   * @{#AI_A2A_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines.
  --
  -- You can use these methods to minimize the airbase coordination overhead and to increase the airbase efficiency.
  -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the
  -- A2A defense system, as no new CAP or GCI planes can takeoff.
  -- Note that the method @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft.
  -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control.
  --
  -- This example defines the default landing method to be at the runway.
  -- And for a couple of squadrons overrides this default method.
  --
  --      -- Setup the Landing methods
  --
  --      -- The default landing method
  --      A2ADispatcher:SetDefaultLandingAtRunway()
  --
  --      -- The individual landing per squadron
  --      A2ADispatcher:SetSquadronLandingAtRunway( "Mineralnye" )
  --      A2ADispatcher:SetSquadronLandingNearAirbase( "Sochi" )
  --      A2ADispatcher:SetSquadronLandingAtEngineShutdown( "Mozdok" )
  --      A2ADispatcher:SetSquadronLandingNearAirbase( "Maykop" )
  --      A2ADispatcher:SetSquadronLanding( "Novo", AI_A2A_DISPATCHER.Landing.AtRunway )
  --
  --
  -- ### 6.3. Set squadron grouping
  --
  -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronGrouping}() to set the grouping of CAP or GCI flights that will take-off when spawned.
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia12.JPG)
  --
  -- In the case of GCI, the @{#AI_A2A_DISPATCHER.SetSquadronGrouping}() method has additional behavior. When there aren't enough CAP flights airborne, a GCI will be initiated for the remaining
  -- targets to be engaged. Depending on the grouping parameter, the spawned flights for GCI are grouped into this setting.
  -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by CAP or any airborne flight,
  -- a GCI needs to be started, the GCI flights will be grouped as follows: Group 1 of 2 flights and Group 2 of one flight!
  --
  -- Even more ... If one target has been detected, and the overhead is 1.5, grouping is 1, then two groups of planes will be spawned, with one unit each!
  --
  -- The **grouping value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense flights grouping when the tactical situation changes.
  --
  -- ### 6.4. Overhead and Balance the effectiveness of the air defenses in case of GCI.
  --
  -- The effectiveness can be set with the **overhead parameter**. This is a number that is used to calculate the amount of Units that dispatching command will allocate to GCI in surplus of detected amount of units.
  -- The **default value** of the overhead parameter is 1.0, which means **equal balance**.
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia11.JPG)
  --
  -- However, depending on the (type of) aircraft (strength and payload) in the squadron and the amount of resources available, this parameter can be changed.
  --
  -- The @{#AI_A2A_DISPATCHER.SetSquadronOverhead}() method can be used to tweak the defense strength,
  -- taking into account the plane types of the squadron.
  --
  -- For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles...
  -- So in this case, one may want to use the @{#AI_A2A_DISPATCHER.SetOverhead}() method to allocate more defending planes as the amount of detected attacking planes.
  -- The overhead must be given as a decimal value with 1 as the neutral value, which means that overhead values:
  --
  --   * Higher than 1.0, for example 1.5, will increase the defense unit amounts. For 4 planes detected, 6 planes will be spawned.
  --   * Lower than 1, for example 0.75, will decrease the defense unit amounts. For 4 planes detected, only 3 planes will be spawned.
  --
  -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group
  -- multiplied by the Overhead and rounded up to the smallest integer.
  --
  -- For example ... If one target has been detected, and the overhead is 1.5, grouping is 1, then two groups of planes will be spawned, with one unit each!
  --
  -- The **overhead value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense overhead when the tactical situation changes.
  --
  -- ## 6.5. Squadron fuel threshold.
  --
  -- When an airplane gets **out of fuel** to a certain %, which is by default **15% (0.15)**, there are two possible actions that can be taken:
  --  - The defender will go RTB, and will be replaced with a new defender if possible.
  --  - The defender will refuel at a tanker, if a tanker has been specified for the squadron.
  --
  -- Use the method @{#AI_A2A_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel threshold** of spawned airplanes for all squadrons.
  --
  -- ## 7. Setup a squadron for CAP
  --
  -- ### 7.1. Set the CAP zones
  --
  -- CAP zones are patrol areas where Combat Air Patrol (CAP) flights loiter until they either return to base due to low fuel or are assigned an interception task by ground control.
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia6.JPG)
  --
  --   * As the CAP flights wander around within the zone waiting to be tasked, these zones need to be large enough that the aircraft are not constantly turning
  --   but do not have to be big and numerous enough to completely cover a border.
  --
  --   * CAP zones can be of any type, and are derived from the @{Core.Zone#ZONE_BASE} class. Zones can be @{Core.Zone#ZONE}, @{Core.Zone#ZONE_POLYGON}, @{Core.Zone#ZONE_UNIT}, @{Core.Zone#ZONE_GROUP}, etc.
  --   This allows to setup **static, moving and/or complex zones** wherein aircraft will perform the CAP.
  --
  --   * Typically 20000-50000 metres width is used and they are spaced so that aircraft in the zone waiting for tasks don't have to far to travel to protect their coalitions important targets.
  --   These targets are chosen as part of the mission design and might be an important airfield or town etc.
  --   Zone size is also determined somewhat by territory size, plane types
  --   (eg WW2 aircraft might mean smaller zones or more zones because they are slower and take longer to intercept enemy aircraft).
  --
  --   * In a **cold war** it is important to make sure a CAP zone doesn't intrude into enemy territory as otherwise CAP flights will likely cross borders
  --   and spark a full scale conflict which will escalate rapidly.
  --
  --   * CAP flights do not need to be in the CAP zone before they are "on station" and ready for tasking.
  --
  --   * Typically if a CAP flight is tasked and therefore leaves their zone empty while they go off and intercept their target another CAP flight will spawn to take their place.
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia7.JPG)
  --
  -- The following example illustrates how CAP zones are coded:
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_8.JPG)
  --
  --      -- CAP Squadron execution.
  --      CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) )
  --      A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 )
  --      A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 )
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_7.JPG)
  --
  --      CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) )
  --      A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" )
  --      A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 )
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_9.JPG)
  --
  --      CAPZoneMiddle = ZONE:New( "CAP Zone Middle")
  --      A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" )
  --      A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 )
  --
  -- Note the different @{Core.Zone} MOOSE classes being used to create zones of different types. Please click the @{Core.Zone} link for more information about the different zone types.
  -- Zones can be circles, can be setup in the mission editor using trigger zones, but can also be setup in the mission editor as polygons and in this case GROUP objects are being used!
  --
  -- ## 7.2. Set the squadron to execute CAP:
  --
  -- The method @{#AI_A2A_DISPATCHER.SetSquadronCap}() defines a CAP execution for a squadron.
  --
  -- Setting-up a CAP zone also requires specific parameters:
  --
  --   * The minimum and maximum altitude
  --   * The minimum speed and maximum patrol speed
  --   * The minimum and maximum engage speed
  --   * The type of altitude measurement
  --
  -- These define how the squadron will perform the CAP while patrolling. Different terrain types requires different types of CAP.
  --
  -- The @{#AI_A2A_DISPATCHER.SetSquadronCapInterval}() method specifies **how much** and **when** CAP flights will takeoff.
  --
  -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system.
  --
  -- For example, the following setup will create a CAP for squadron "Sochi":
  --
  --      A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" )
  --      A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 )
  --
  -- ## 7.3. Squadron tanker to refuel when executing CAP and defender is out of fuel.
  --
  -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker.
  -- This greatly increases the efficiency of your CAP operations.
  --
  -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected.
  -- Then, use the method @{#AI_A2A_DISPATCHER.SetDefaultTanker}() to set the default tanker for the refuelling.
  -- You can also specify a specific tanker for refuelling for a squadron  by using the method @{#AI_A2A_DISPATCHER.SetSquadronTanker}().
  --
  -- When the tanker specified is alive and in the air, the tanker will be used for refuelling.
  --
  -- For example, the following setup will create a CAP for squadron "Gelend" with a refuel task for the squadron:
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_10.JPG)
  --
  --      -- Define the CAP
  --      A2ADispatcher:SetSquadron( "Gelend", AIRBASE.Caucasus.Gelendzhik, { "SQ CCCP SU-30" }, 20 )
  --      A2ADispatcher:SetSquadronCap( "Gelend", ZONE:New( "PatrolZoneGelend" ), 4000, 8000, 600, 800, 1000, 1300 )
  --      A2ADispatcher:SetSquadronCapInterval( "Gelend", 2, 30, 600, 1 )
  --      A2ADispatcher:SetSquadronGci( "Gelend", 900, 1200 )
  --
  --      -- Setup the Refuelling for squadron "Gelend", at tanker (group) "TankerGelend" when the fuel in the tank of the CAP defenders is less than 80%.
  --      A2ADispatcher:SetSquadronFuelThreshold( "Gelend", 0.8 )
  --      A2ADispatcher:SetSquadronTanker( "Gelend", "TankerGelend" )
  --
  -- ## 7.4 Set up race track pattern
  --
  -- By default, flights patrol randomly within the CAP zone. It is also possible to let them fly a race track pattern using the
  -- @{#AI_A2A_DISPATCHER.SetDefaultCapRacetrack}(*LeglengthMin*, *LeglengthMax*, *HeadingMin*, *HeadingMax*, *DurationMin*, *DurationMax*) or
  -- @{#AI_A2A_DISPATCHER.SetSquadronCapRacetrack}(*SquadronName*, *LeglengthMin*, *LeglengthMax*, *HeadingMin*, *HeadingMax*, *DurationMin*, *DurationMax*) functions.
  -- The first function enables this for all squadrons, the latter only for specific squadrons. For example,
  --
  --      -- Enable race track pattern for CAP squadron "Mineralnye".
  --      A2ADispatcher:SetSquadronCapRacetrack("Mineralnye", 10000, 20000, 90, 180, 10*60, 20*60)
  --
  -- In this case the squadron "Mineralnye" will a race track pattern at a random point in the CAP zone. The leg length will be randomly selected between 10,000 and 20,000 meters. The heading
  -- of the race track will randomly selected between 90 (West to East) and 180 (North to South) degrees.
  -- After a random duration between 10 and 20 minutes, the flight will get a new random orbit location.
  --
  -- Note that all parameters except the squadron name are optional. If not specified, default values are taken. Speed and altitude are taken from the CAP command used earlier on, e.g.
  --
  --      A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 )
  --
  -- Also note that the center of the race track pattern is chosen randomly within the patrol zone and can be close the the boarder of the zone. Hence, it cannot be guaranteed that the
  -- whole pattern lies within the patrol zone.
  --
  -- ## 8. Setup a squadron for GCI:
  --
  -- The method @{#AI_A2A_DISPATCHER.SetSquadronGci}() defines a GCI execution for a squadron.
  --
  -- Setting-up a GCI readiness also requires specific parameters:
  --
  --   * The minimum speed and maximum patrol speed
  --
  -- Essentially this controls how many flights of GCI aircraft can be active at any time.
  -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers.
  -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders,
  -- too short will mean that the intruders may have already passed the ideal interception point!
  --
  -- For example, the following setup will create a GCI for squadron "Sochi":
  --
  --      A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 )
  --
  -- ## 9. Other configuration options
  --
  -- ### 9.1. Set a tactical display panel:
  --
  -- Every 30 seconds, a tactical display panel can be shown that illustrates what the status is of the different groups controlled by AI\_A2A\_DISPATCHER.
  -- Use the method @{#AI_A2A_DISPATCHER.SetTacticalDisplay}() to switch on the tactical display panel. The default will not show this panel.
  -- Note that there may be some performance impact if this panel is shown.
  --
  -- ## 10. Defaults settings.
  --
  -- This provides a good overview of the different parameters that are setup or hardcoded by default.
  -- For some default settings, a method is available that allows you to tweak the defaults.
  --
  -- ## 10.1. Default takeoff method.
  --
  -- The default **takeoff method** is set to **in the air**, which means that new spawned airplanes will be spawned directly in the air above the airbase by default.
  --
  -- **The default takeoff method can be set for ALL squadrons that don't have an individual takeoff method configured.**
  --
  --   * @{#AI_A2A_DISPATCHER.SetDefaultTakeoff}() is the generic configuration method to control takeoff by default from the air, hot, cold or from the runway. See the method for further details.
  --   * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffInAir}() will spawn by default new aircraft from the squadron directly in the air.
  --   * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromParkingCold}() will spawn by default new aircraft in without running engines at a parking spot at the airfield.
  --   * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromParkingHot}() will spawn by default new aircraft in with running engines at a parking spot at the airfield.
  --   * @{#AI_A2A_DISPATCHER.SetDefaultTakeoffFromRunway}() will spawn by default new aircraft at the runway at the airfield.
  --
  -- ## 10.2. Default landing method.
  --
  -- The default **landing method** is set to **near the airbase**, which means that returning airplanes will be despawned directly in the air by default.
  --
  -- The default landing method can be set for ALL squadrons that don't have an individual landing method configured.
  --
  --   * @{#AI_A2A_DISPATCHER.SetDefaultLanding}() is the generic configuration method to control by default landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown.
  --   * @{#AI_A2A_DISPATCHER.SetDefaultLandingNearAirbase}() will despawn by default the returning aircraft in the air when near the airfield.
  --   * @{#AI_A2A_DISPATCHER.SetDefaultLandingAtRunway}() will despawn by default the returning aircraft directly after landing at the runway.
  --   * @{#AI_A2A_DISPATCHER.SetDefaultLandingAtEngineShutdown}() will despawn by default the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines.
  --
  -- ## 10.3. Default overhead.
  --
  -- The default **overhead** is set to **1**. That essentially means that there isn't any overhead set by default.
  --
  -- The default overhead value can be set for ALL squadrons that don't have an individual overhead value configured.
  --
  -- Use the @{#AI_A2A_DISPATCHER.SetDefaultOverhead}() method can be used to set the default overhead or defense strength for ALL squadrons.
  --
  -- ## 10.4. Default grouping.
  --
  -- The default **grouping** is set to **one airplane**. That essentially means that there won't be any grouping applied by default.
  --
  -- The default grouping value can be set for ALL squadrons that don't have an individual grouping value configured.
  --
  -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultGrouping}() to set the **default grouping** of spawned airplanes for all squadrons.
  --
  -- ## 10.5. Default RTB fuel threshold.
  --
  -- When an airplane gets **out of fuel** to a certain %, which is **15% (0.15)**, it will go RTB, and will be replaced with a new airplane when applicable.
  --
  -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel threshold** of spawned airplanes for all squadrons.
  --
  -- ## 10.6. Default RTB damage threshold.
  --
  -- When an airplane is **damaged** to a certain %, which is **40% (0.40)**, it will go RTB, and will be replaced with a new airplane when applicable.
  --
  -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage threshold** of spawned airplanes for all squadrons.
  --
  -- ## 10.7. Default settings for CAP.
  --
  -- ### 10.7.1. Default CAP Time Interval.
  --
  -- CAP is time driven, and will evaluate in random time intervals if a new CAP needs to be spawned.
  -- The **default CAP time interval** is between **180** and **600** seconds.
  --
  -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons.
  -- Note that you can still change the CAP limit and CAP time intervals for each CAP individually using the @{#AI_A2A_DISPATCHER.SetSquadronCapTimeInterval}() method.
  --
  -- ### 10.7.2. Default CAP limit.
  --
  -- Multiple CAP can be airborne at the same time for one squadron, which is controlled by the **CAP limit**.
  -- The **default CAP limit** is 1 CAP per squadron to be airborne at the same time.
  -- Note that the default CAP limit is used when a Squadron CAP is defined, and cannot be changed afterwards.
  -- So, ensure that you set the default CAP limit **before** you spawn the Squadron CAP.
  --
  -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultCapTimeInterval}() to set the **default CAP time interval** of spawned airplanes for all squadrons.
  -- Note that you can still change the CAP limit and CAP time intervals for each CAP individually using the @{#AI_A2A_DISPATCHER.SetSquadronCapTimeInterval}() method.
  --
  -- ## 10.7.3. Default tanker for refuelling when executing CAP.
  --
  -- Instead of sending CAP to RTB when out of fuel, you can let CAP refuel in mid air using a tanker.
  -- This greatly increases the efficiency of your CAP operations.
  --
  -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected.
  -- Then, use the method @{#AI_A2A_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher.
  -- Use the method @{#AI_A2A_DISPATCHER.SetDefaultFuelThreshold}() to set the % left in the defender airplane tanks when a refuel action is needed.
  --
  -- When the tanker specified is alive and in the air, the tanker will be used for refuelling.
  --
  -- For example, the following setup will set the default refuel tanker to "Tanker":
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_DISPATCHER-ME_11.JPG)
  --
  --      -- Define the CAP
  --      A2ADispatcher:SetSquadron( "Sochi", AIRBASE.Caucasus.Sochi_Adler, { "SQ CCCP SU-34" }, 20 )
  --      A2ADispatcher:SetSquadronCap( "Sochi", ZONE:New( "PatrolZone" ), 4000, 8000, 600, 800, 1000, 1300 )
  --      A2ADispatcher:SetSquadronCapInterval("Sochi", 2, 30, 600, 1 )
  --      A2ADispatcher:SetSquadronGci( "Sochi", 900, 1200 )
  --
  --      -- Set the default tanker for refuelling to "Tanker", when the default fuel threshold has reached 90% fuel left.
  --      A2ADispatcher:SetDefaultFuelThreshold( 0.9 )
  --      A2ADispatcher:SetDefaultTanker( "Tanker" )
  --
  -- ## 10.8. Default settings for GCI.
  --
  -- ## 10.8.1. Optimal intercept point calculation.
  --
  -- When intruders are detected, the intrusion path of the attackers can be monitored by the EWR.
  -- Although defender planes might be on standby at the airbase, it can still take some time to get the defenses up in the air if there aren't any defenses airborne.
  -- This time can easily take 2 to 3 minutes, and even then the defenders still need to fly towards the target, which takes also time.
  --
  -- Therefore, an optimal **intercept point** is calculated which takes a couple of parameters:
  --
  --   * The average bearing of the intruders for an amount of seconds.
  --   * The average speed of the intruders for an amount of seconds.
  --   * An assumed time it takes to get planes operational at the airbase.
  --
  -- The **intercept point** will determine:
  --
  --   * If there are any friendlies close to engage the target. These can be defenders performing CAP or defenders in RTB.
  --   * The optimal airbase from where defenders will takeoff for GCI.
  --
  -- Use the method @{#AI_A2A_DISPATCHER.SetIntercept}() to modify the assumed intercept delay time to calculate a valid interception.
  --
  -- ## 10.8.2. Default Disengage Radius.
  --
  -- The radius to **disengage any target** when the **distance** of the defender to the **home base** is larger than the specified meters.
  -- The default Disengage Radius is **300km** (300000 meters). Note that the Disengage Radius is applicable to ALL squadrons!
  --
  -- Use the method @{#AI_A2A_DISPATCHER.SetDisengageRadius}() to modify the default Disengage Radius to another distance setting.
  --
  -- ## 11. Airbase capture:
  --
  -- Different squadrons can be located at one airbase.
  -- If the airbase gets captured, that is, when there is an enemy unit near the airbase, and there aren't anymore friendlies at the airbase, the airbase will change coalition ownership.
  -- As a result, the GCI and CAP will stop!
  -- However, the squadron will still stay alive. Any airplane that is airborne will continue its operations until all airborne airplanes
  -- of the squadron will be destroyed. This to keep consistency of air operations not to confuse the players.
  --
  -- ## 12. Q & A:
  --
  -- ### 12.1. Which countries will be selected for each coalition?
  --
  -- Which countries are assigned to a coalition influences which units are available to the coalition.
  -- For example because the mission calls for a EWR radar on the blue side the Ukraine might be chosen as a blue country
  -- so that the 55G6 EWR radar unit is available to blue.
  -- Some countries assign different tasking to aircraft, for example Germany assigns the CAP task to F-4E Phantoms but the USA does not.
  -- Therefore if F4s are wanted as a coalition's CAP or GCI aircraft Germany will need to be assigned to that coalition.
  --
  -- ### 12.2. Country, type, load out, skill and skins for CAP and GCI aircraft?
  --
  --   * Note these can be from any countries within the coalition but must be an aircraft with one of the main tasks being "CAP".
  --   * Obviously skins which are selected must be available to all players that join the mission otherwise they will see a default skin.
  --   * Load outs should be appropriate to a CAP mission eg perhaps drop tanks for CAP flights and extra missiles for GCI flights.
  --   * These decisions will eventually lead to template aircraft units being placed as late activation units that the script will use as templates for spawning CAP and GCI flights. Up to 4 different aircraft configurations can be chosen for each coalition. The spawned aircraft will inherit the characteristics of the template aircraft.
  --   * The selected aircraft type must be able to perform the CAP tasking for the chosen country.
  --
  --
  -- @field #AI_A2A_DISPATCHER
  AI_A2A_DISPATCHER = {
    ClassName = "AI_A2A_DISPATCHER",
    Detection = nil,
  }

  --- Squadron data structure.
  -- @type AI_A2A_DISPATCHER.Squadron
  -- @field #string Name Name of the squadron.
  -- @field #number ResourceCount Number of resources.
  -- @field #string AirbaseName Name of the home airbase.
  -- @field Wrapper.Airbase#AIRBASE Airbase The home airbase of the squadron.
  -- @field #boolean Captured If true, airbase of the squadron was captured.
  -- @field #table Resources Flight group resources Resources[TemplateID][GroupName] = SpawnGroup.
  -- @field #boolean Uncontrolled If true, flight groups are spawned uncontrolled and later activated.
  -- @field #table Gci GCI.
  -- @field #number Overhead Squadron overhead.
  -- @field #number Grouping Squadron flight group size.
  -- @field #number Takeoff Takeoff type.
  -- @field #number TakeoffAltitude Altitude in meters for spawn in air.
  -- @field #number Landing Landing type.
  -- @field #number FuelThreshold Fuel threshold [0,1] for RTB.
  -- @field #string TankerName Name of the refuelling tanker.
  -- @field #table Table of template group names of the squadron.
  -- @field #table Spawn Table of spawns Core.Spawn#SPAWN.
  -- @field #table TemplatePrefixes
  -- @field #boolean Racetrack If true, CAP flights will perform a racetrack pattern rather than randomly patrolling the zone.
  -- @field #number RacetrackLengthMin Min Length of race track in meters. Default 10,000 m.
  -- @field #number RacetrackLengthMax Max Length of race track in meters. Default 15,000 m.
  -- @field #number RacetrackHeadingMin Min heading of race track in degrees. Default 0 deg, i.e. from South to North.
  -- @field #number RacetrackHeadingMax Max heading of race track in degrees. Default 180 deg, i.e. from North to South.
  -- @field #number RacetrackDurationMin Min duration in seconds before the CAP flight changes its orbit position. Default never.
  -- @field #number RacetrackDurationMax Max duration in seconds before the CAP flight changes its orbit position. Default never.

  --- Enumerator for spawns at airbases
  -- @type AI_A2A_DISPATCHER.Takeoff
  -- @extends Wrapper.Group#GROUP.Takeoff
  
  ---
  -- @field #AI_A2A_DISPATCHER.Takeoff Takeoff
  AI_A2A_DISPATCHER.Takeoff = GROUP.Takeoff

  --- Defines Landing type/location.
  -- @field Landing
  AI_A2A_DISPATCHER.Landing = {
    NearAirbase = 1,
    AtRunway = 2,
    AtEngineShutdown = 3,
  }

  --- AI_A2A_DISPATCHER constructor.
  -- This is defining the A2A DISPATCHER for one coalition.
  -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units.
  -- The Detection object is polymorphic, depending on the type of detection object chosen, the detection will work differently.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage
  --
  --     -- Setup the Detection, using DETECTION_AREAS.
  --     -- First define the SET of GROUPs that are defining the EWR network.
  --     -- Here with prefixes DF CCCP AWACS, DF CCCP EWR.
  --     DetectionSetGroup = SET_GROUP:New()
  --     DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } )
  --     DetectionSetGroup:FilterStart()
  --
  --     -- Define the DETECTION_AREAS, using the DetectionSetGroup, with a 30km grouping radius.
  --     Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 )
  --
  --     -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --     A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  function AI_A2A_DISPATCHER:New( Detection )

    -- Inherits from DETECTION_MANAGER
    local self = BASE:Inherit( self, DETECTION_MANAGER:New( nil, Detection ) ) -- #AI_A2A_DISPATCHER

    self.Detection = Detection -- Functional.Detection#DETECTION_AREAS

    -- This table models the DefenderSquadron templates.
    self.DefenderSquadrons = {} -- The Defender Squadrons.
    self.DefenderSpawns = {}
    self.DefenderTasks = {} -- The Defenders Tasks.
    self.DefenderDefault = {} -- The Defender Default Settings over all Squadrons.

    self.SetSendPlayerMessages = false --#boolean Flash messages to player
    
    -- TODO: Check detection through radar.
    self.Detection:FilterCategories( { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } )
    -- self.Detection:InitDetectRadar( true )
    self.Detection:SetRefreshTimeInterval( 30 )

    self:SetEngageRadius()
    self:SetGciRadius()
    self:SetIntercept( 300 ) -- A default intercept delay time of 300 seconds.
    self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km.

    self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Air )
    self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above the ground.
    self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.NearAirbase )
    self:SetDefaultOverhead( 1 )
    self:SetDefaultGrouping( 1 )
    self:SetDefaultFuelThreshold( 0.15, 0 ) -- 15% of fuel remaining in the tank will trigger the airplane to return to base or refuel.
    self:SetDefaultDamageThreshold( 0.4 ) -- When 40% of damage, go RTB.
    self:SetDefaultCapTimeInterval( 180, 600 ) -- Between 180 and 600 seconds.
    self:SetDefaultCapLimit( 1 ) -- Maximum one CAP per squadron.

    self:AddTransition( "Started", "Assign", "Started" )

    --- OnAfter Transition Handler for Event Assign.
    -- @function [parent=#AI_A2A_DISPATCHER] OnAfterAssign
    -- @param #AI_A2A_DISPATCHER self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @param Tasking.Task_A2A#AI_A2A Task
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param #string PlayerName

    self:AddTransition( "*", "CAP", "*" )

    --- CAP Handler OnBefore for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] OnBeforeCAP
    -- @param #AI_A2A_DISPATCHER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean

    --- CAP Handler OnAfter for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] OnAfterCAP
    -- @param #AI_A2A_DISPATCHER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To

    --- CAP Trigger for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] CAP
    -- @param #AI_A2A_DISPATCHER self

    --- CAP Asynchronous Trigger for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] __CAP
    -- @param #AI_A2A_DISPATCHER self
    -- @param #number Delay

    self:AddTransition( "*", "GCI", "*" )

    --- GCI Handler OnBefore for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] OnBeforeGCI
    -- @param #AI_A2A_DISPATCHER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean

    --- GCI Handler OnAfter for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] OnAfterGCI
    -- @param #AI_A2A_DISPATCHER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item.
    -- @param #number DefendersMissing Number of missing defenders.
    -- @param #table DefenderFriendlies Friendly defenders.

    --- GCI Trigger for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] GCI
    -- @param #AI_A2A_DISPATCHER self
    -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item.
    -- @param #number DefendersMissing Number of missing defenders.
    -- @param #table DefenderFriendlies Friendly defenders.

    --- GCI Asynchronous Trigger for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] __GCI
    -- @param #AI_A2A_DISPATCHER self
    -- @param #number Delay
    -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item.
    -- @param #number DefendersMissing Number of missing defenders.
    -- @param #table DefenderFriendlies Friendly defenders.

    self:AddTransition( "*", "ENGAGE", "*" )

    --- ENGAGE Handler OnBefore for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] OnBeforeENGAGE
    -- @param #AI_A2A_DISPATCHER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item.
    -- @param #table Defenders Defenders table.
    -- @return #boolean

    --- ENGAGE Handler OnAfter for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] OnAfterENGAGE
    -- @param #AI_A2A_DISPATCHER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item.
    -- @param #table Defenders Defenders table.

    --- ENGAGE Trigger for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] ENGAGE
    -- @param #AI_A2A_DISPATCHER self
    -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item.
    -- @param #table Defenders Defenders table.

    --- ENGAGE Asynchronous Trigger for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] __ENGAGE
    -- @param #AI_A2A_DISPATCHER self
    -- @param #number Delay
    -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item.
    -- @param #table Defenders Defenders table.

    -- Subscribe to the CRASH event so that when planes are shot
    -- by a Unit from the dispatcher, they will be removed from the detection...
    -- This will avoid the detection to still "know" the shot unit until the next detection.
    -- Otherwise, a new intercept or engage may happen for an already shot plane!

    self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead )
    self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead )
    -- self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead )

    self:HandleEvent( EVENTS.Land )
    self:HandleEvent( EVENTS.EngineShutdown )

    -- Handle the situation where the airbases are captured.
    self:HandleEvent( EVENTS.BaseCaptured )

    self:SetTacticalDisplay( false )

    self.DefenderCAPIndex = 0

    self:__Start( 5 )

    return self
  end

  --- On after "Start" event.
  -- @param #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:onafterStart( From, Event, To )

    self:GetParent( self, AI_A2A_DISPATCHER ).onafterStart( self, From, Event, To )

    -- Spawn the resources.
    for SquadronName, _DefenderSquadron in pairs( self.DefenderSquadrons ) do
      local DefenderSquadron = _DefenderSquadron -- #AI_A2A_DISPATCHER.Squadron
      DefenderSquadron.Resources = {}
      if DefenderSquadron.ResourceCount then
        for Resource = 1, DefenderSquadron.ResourceCount do
          self:ParkDefender( DefenderSquadron )
        end
      end
    end
  end

  --- Park defender.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #AI_A2A_DISPATCHER.Squadron DefenderSquadron The squadron.
  function AI_A2A_DISPATCHER:ParkDefender( DefenderSquadron )

    local TemplateID = math.random( 1, #DefenderSquadron.Spawn )

    local Spawn = DefenderSquadron.Spawn[TemplateID] -- Core.Spawn#SPAWN

    Spawn:InitGrouping( 1 )

    local SpawnGroup

    if self:IsSquadronVisible( DefenderSquadron.Name ) then

      local Grouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping

      Grouping = 1

      Spawn:InitGrouping( Grouping )

      SpawnGroup = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, SPAWN.Takeoff.Cold )

      local GroupName = SpawnGroup:GetName()

      DefenderSquadron.Resources = DefenderSquadron.Resources or {}

      DefenderSquadron.Resources[TemplateID] = DefenderSquadron.Resources[TemplateID] or {}
      DefenderSquadron.Resources[TemplateID][GroupName] = {}
      DefenderSquadron.Resources[TemplateID][GroupName] = SpawnGroup

      self.uncontrolled = self.uncontrolled or {}
      self.uncontrolled[DefenderSquadron.Name] = self.uncontrolled[DefenderSquadron.Name] or {}

      table.insert( self.uncontrolled[DefenderSquadron.Name], { group = SpawnGroup, name = GroupName, grouping = Grouping } )
    end

  end

  --- Event base captured.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Core.Event#EVENTDATA EventData
  function AI_A2A_DISPATCHER:OnEventBaseCaptured( EventData )

    local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured.

    self:T( "Captured " .. AirbaseName )

    -- Now search for all squadrons located at the airbase, and sanitize them.
    for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do
      if Squadron.AirbaseName == AirbaseName then
        Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning.
        Squadron.Captured = true
        self:T( "Squadron " .. SquadronName .. " captured." )
      end
    end
  end

  --- Event dead or crash.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Core.Event#EVENTDATA EventData
  function AI_A2A_DISPATCHER:OnEventCrashOrDead( EventData )
    self.Detection:ForgetDetectedUnit( EventData.IniUnitName )
  end

  --- Event land.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Core.Event#EVENTDATA EventData
  function AI_A2A_DISPATCHER:OnEventLand( EventData )
    self:F( "Landed" )
    local DefenderUnit = EventData.IniUnit
    local Defender = EventData.IniGroup
    local Squadron = self:GetSquadronFromDefender( Defender )
    if Squadron then
      self:F( { SquadronName = Squadron.Name } )
      local LandingMethod = self:GetSquadronLanding( Squadron.Name )
      if LandingMethod == AI_A2A_DISPATCHER.Landing.AtRunway then
        local DefenderSize = Defender:GetSize()
        if DefenderSize == 1 then
          self:RemoveDefenderFromSquadron( Squadron, Defender )
        end
        DefenderUnit:Destroy()
        self:ParkDefender( Squadron )
        return
      end
      if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then
        -- Damaged units cannot be repaired anymore.
        DefenderUnit:Destroy()
        return
      end
    end
  end

  --- Event engine shutdown.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Core.Event#EVENTDATA EventData
  function AI_A2A_DISPATCHER:OnEventEngineShutdown( EventData )
    local DefenderUnit = EventData.IniUnit
    local Defender = EventData.IniGroup
    local Squadron = self:GetSquadronFromDefender( Defender )
    if Squadron then
      self:F( { SquadronName = Squadron.Name } )
      local LandingMethod = self:GetSquadronLanding( Squadron.Name )
      if LandingMethod == AI_A2A_DISPATCHER.Landing.AtEngineShutdown and not DefenderUnit:InAir() then
        local DefenderSize = Defender:GetSize()
        if DefenderSize == 1 then
          self:RemoveDefenderFromSquadron( Squadron, Defender )
        end
        DefenderUnit:Destroy()
        self:ParkDefender( Squadron )
      end
    end
  end

  --- Define the radius to engage any target by airborne friendlies, which are executing cap or returning from an intercept mission.
  -- If there is a target area detected and reported, then any friendlies that are airborne near this target area,
  -- will be commanded to (re-)engage that target when available (if no other tasks were commanded).
  --
  -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target,
  -- will be considered to receive the command to engage that target area.
  --
  -- You need to evaluate the value of this parameter carefully:
  --
  --   * If too small, more intercept missions may be triggered upon detected target areas.
  --   * If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far.
  --
  -- **Use the method @{#AI_A2A_DISPATCHER.SetEngageRadius}() to modify the default Engage Radius for ALL squadrons.**
  --
  -- Demonstration Mission: [AID-019 - AI_A2A - Engage Range Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher/AID-A2A-019%20-%20Engage%20Range%20Test)
  --
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target.
  -- @return #AI_A2A_DISPATCHER
  -- @usage
  --
  --   -- Set 50km as the radius to engage any target by airborne friendlies.
  --   A2ADispatcher:SetEngageRadius( 50000 )
  --
  --   -- Set 100km as the radius to engage any target by airborne friendlies.
  --   A2ADispatcher:SetEngageRadius() -- 100000 is the default value.
  --
  function AI_A2A_DISPATCHER:SetEngageRadius( EngageRadius )

    self.Detection:SetFriendliesRange( EngageRadius or 100000 )

    return self
  end

  --- Define the radius to disengage any target when the distance to the home base is larger than the specified meters.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number DisengageRadius (Optional, Default = 300000) The radius in meters to disengage a target when too far from the home base.
  -- @return #AI_A2A_DISPATCHER
  -- @usage
  --
  --   -- Set 50km as the Disengage Radius.
  --   A2ADispatcher:SetDisengageRadius( 50000 )
  --
  --   -- Set 100km as the Disengage Radius.
  --   A2ADispatcher:SetDisengageRadius() -- 300000 is the default value.
  --
  function AI_A2A_DISPATCHER:SetDisengageRadius( DisengageRadius )

    self.DisengageRadius = DisengageRadius or 300000

    return self
  end

  --- Define the radius to check if a target can be engaged by an ground controlled intercept.
  -- When targets are detected that are still really far off, you don't want the AI_A2A_DISPATCHER to launch intercepts just yet.
  -- You want it to wait until a certain Gci range is reached, which is the **distance of the closest airbase to target**
  -- being **smaller** than the **Ground Controlled Intercept radius** or **Gci radius**.
  --
  -- The **default** Gci radius is defined as **200000** or **200km**. Override the default Gci radius when the era of the warfare is early, or,
  -- when you don't want to let the AI_A2A_DISPATCHER react immediately when a certain border or area is not being crossed.
  --
  -- Use the method @{#AI_A2A_DISPATCHER.SetGciRadius}() to set a specific controlled ground intercept radius.
  -- **The Ground Controlled Intercept radius is defined for ALL squadrons which are operational.**
  --
  -- Demonstration Mission: [AID-013 - AI_A2A - Intercept Test](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher/AID-A2A-013%20-%20Intercept%20Test)
  --
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number GciRadius (Optional, Default = 200000) The radius to ground control intercept detected targets from the nearest airbase.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage
  --
  --   -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --   A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  --   -- Set 100km as the radius to ground control intercept detected targets from the nearest airbase.
  --   A2ADispatcher:SetGciRadius( 100000 )
  --
  --   -- Set 200km as the radius to ground control intercept.
  --   A2ADispatcher:SetGciRadius() -- 200000 is the default value.
  --
  function AI_A2A_DISPATCHER:SetGciRadius( GciRadius )

    self.GciRadius = GciRadius or 200000

    return self
  end

  --- Define a border area to simulate a **cold war** scenario.
  -- A **cold war** is one where CAP aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border.
  -- A **hot war** is one where CAP aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send CAP and GCI aircraft to attack it.
  -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this.
  -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1
  -- @param #AI_A2A_DISPATCHER self
  -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage
  --
  --   -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --   A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  --   -- Set one ZONE_POLYGON object as the border for the A2A dispatcher.
  --   local BorderZone = ZONE_POLYGON( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- The GROUP object is a late activate helicopter unit.
  --   A2ADispatcher:SetBorderZone( BorderZone )
  --
  -- or
  --
  --   -- Set two ZONE_POLYGON objects as the border for the A2A dispatcher.
  --   local BorderZone1 = ZONE_POLYGON( "CCCP Border1", GROUP:FindByName( "CCCP Border1" ) ) -- The GROUP object is a late activate helicopter unit.
  --   local BorderZone2 = ZONE_POLYGON( "CCCP Border2", GROUP:FindByName( "CCCP Border2" ) ) -- The GROUP object is a late activate helicopter unit.
  --   A2ADispatcher:SetBorderZone( { BorderZone1, BorderZone2 } )
  --
  --
  function AI_A2A_DISPATCHER:SetBorderZone( BorderZone )

    self.Detection:SetAcceptZones( BorderZone )

    return self
  end

  --- Display a tactical report every 30 seconds about which aircraft are:
  --   * Patrolling
  --   * Engaging
  --   * Returning
  --   * Damaged
  --   * Out of Fuel
  --   * ...
  -- @param #AI_A2A_DISPATCHER self
  -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage
  --
  --   -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --   A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  --   -- Now Setup the Tactical Display for debug mode.
  --   A2ADispatcher:SetTacticalDisplay( true )
  --
  function AI_A2A_DISPATCHER:SetTacticalDisplay( TacticalDisplay )

    self.TacticalDisplay = TacticalDisplay

    return self
  end

  --- Set the default damage threshold when defenders will RTB.
  -- The default damage threshold is by default set to 40%, which means that when the airplane is 40% damaged, it will go RTB.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the % of the damage threshold before going RTB.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage
  --
  --   -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --   A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  --   -- Now Setup the default damage threshold.
  --   A2ADispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the airplane 90% damaged.
  --
  function AI_A2A_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold )

    self.DefenderDefault.DamageThreshold = DamageThreshold

    return self
  end

  --- Set the default CAP time interval for squadrons, which will be used to determine a random CAP timing.
  -- The default CAP time interval is between 180 and 600 seconds.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number CapMinSeconds The minimum amount of seconds for the random time interval.
  -- @param #number CapMaxSeconds The maximum amount of seconds for the random time interval.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage
  --
  --   -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --   A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  --   -- Now Setup the default CAP time interval.
  --   A2ADispatcher:SetDefaultCapTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds.
  --
  function AI_A2A_DISPATCHER:SetDefaultCapTimeInterval( CapMinSeconds, CapMaxSeconds )

    self.DefenderDefault.CapMinSeconds = CapMinSeconds
    self.DefenderDefault.CapMaxSeconds = CapMaxSeconds

    return self
  end

  --- Set the default CAP limit for squadrons, which will be used to determine how many CAP can be airborne at the same time for the squadron.
  -- The default CAP limit is 1 CAP, which means one CAP group being spawned.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number CapLimit The maximum amount of CAP that can be airborne at the same time for the squadron.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage
  --
  --   -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --   A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  --   -- Now Setup the default CAP limit.
  --   A2ADispatcher:SetDefaultCapLimit( 2 ) -- Maximum 2 CAP per squadron.
  --
  function AI_A2A_DISPATCHER:SetDefaultCapLimit( CapLimit )

    self.DefenderDefault.CapLimit = CapLimit

    return self
  end

  --- Set intercept.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number InterceptDelay Delay in seconds before intercept.
  -- @return #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:SetIntercept( InterceptDelay )

    self.DefenderDefault.InterceptDelay = InterceptDelay

    local Detection = self.Detection -- Functional.Detection#DETECTION_AREAS
    Detection:SetIntercept( true, InterceptDelay )

    return self
  end

  --- Calculates which AI friendlies are nearby the area
  -- @param #AI_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem
  -- @return #table A list of the friendlies nearby.
  function AI_A2A_DISPATCHER:GetAIFriendliesNearBy( DetectedItem )

    local FriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem )

    return FriendliesNearBy
  end

  --- Return the defender tasks table.
  -- @param #AI_A2A_DISPATCHER self
  -- @return #table Defender tasks as table.
  function AI_A2A_DISPATCHER:GetDefenderTasks()
    return self.DefenderTasks or {}
  end

  --- Get defender task.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Wrapper.Group#GROUP Defender The defender group.
  -- @return #table Defender task.
  function AI_A2A_DISPATCHER:GetDefenderTask( Defender )
    return self.DefenderTasks[Defender]
  end

  --- Get defender task FSM.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Wrapper.Group#GROUP Defender The defender group.
  -- @return Core.Fsm#FSM The FSM.
  function AI_A2A_DISPATCHER:GetDefenderTaskFsm( Defender )
    return self:GetDefenderTask( Defender ).Fsm
  end

  --- Get target of defender.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Wrapper.Group#GROUP Defender The defender group.
  -- @return Target
  function AI_A2A_DISPATCHER:GetDefenderTaskTarget( Defender )
    return self:GetDefenderTask( Defender ).Target
  end

  ---
  -- @param #AI_A2A_DISPATCHER self
  -- @param Wrapper.Group#GROUP Defender The defender group.
  -- @return #string Squadron name of the defender task.
  function AI_A2A_DISPATCHER:GetDefenderTaskSquadronName( Defender )
    return self:GetDefenderTask( Defender ).SquadronName
  end

  ---
  -- @param #AI_A2A_DISPATCHER self
  -- @param Wrapper.Group#GROUP Defender The defender group.
  function AI_A2A_DISPATCHER:ClearDefenderTask( Defender )
    if Defender and Defender:IsAlive() and self.DefenderTasks[Defender] then
      local Target = self.DefenderTasks[Defender].Target
      local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") "
      Message = Message .. Defender:GetName()
      if Target then
        Message = Message .. (Target and (" from " .. Target.Index .. " [" .. Target.Set:Count() .. "]")) or ""
      end
      self:F( { Target = Message } )
    end
    self.DefenderTasks[Defender] = nil
    return self
  end

  ---
  -- @param #AI_A2A_DISPATCHER self
  -- @param Wrapper.Group#GROUP Defender The defender group.
  function AI_A2A_DISPATCHER:ClearDefenderTaskTarget( Defender )

    local DefenderTask = self:GetDefenderTask( Defender )

    if Defender and Defender:IsAlive() and DefenderTask then
      local Target = DefenderTask.Target
      local Message = "Clearing (" .. DefenderTask.Type .. ") "
      Message = Message .. Defender:GetName()
      if Target then
        Message = Message .. ((Target and (" from " .. Target.Index .. " [" .. Target.Set:Count() .. "]")) or "")
      end
      self:F( { Target = Message } )
    end
    if Defender and DefenderTask and DefenderTask.Target then
      DefenderTask.Target = nil
    end
    --    if Defender and DefenderTask then
    --      if DefenderTask.Fsm:Is( "Fuel" )
    --      or DefenderTask.Fsm:Is( "LostControl")
    --      or DefenderTask.Fsm:Is( "Damaged" ) then
    --        self:ClearDefenderTask( Defender )
    --      end
    --    end
    return self
  end

  --- Set defender task.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName Name of the squadron.
  -- @param Wrapper.Group#GROUP Defender The defender group.
  -- @param #table Type Type of the defender task
  -- @param Core.Fsm#FSM Fsm The defender task FSM.
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem Target The defender detected item.
  -- @return #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target )

    self:F( { SquadronName = SquadronName, Defender = Defender:GetName(), Type = Type, Target = Target } )

    self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {}
    self.DefenderTasks[Defender].Type = Type
    self.DefenderTasks[Defender].Fsm = Fsm
    self.DefenderTasks[Defender].SquadronName = SquadronName

    if Target then
      self:SetDefenderTaskTarget( Defender, Target )
    end
    return self
  end

  --- Set defender task target.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Wrapper.Group#GROUP Defender The defender group.
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection The detection object.
  -- @return #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:SetDefenderTaskTarget( Defender, AttackerDetection )

    local Message = "(" .. self.DefenderTasks[Defender].Type .. ") "
    Message = Message .. Defender:GetName()
    Message = Message .. ((AttackerDetection and (" target " .. AttackerDetection.Index .. " [" .. AttackerDetection.Set:Count() .. "]")) or "")
    self:F( { AttackerDetection = Message } )
    if AttackerDetection then
      self.DefenderTasks[Defender].Target = AttackerDetection
    end
    return self
  end

  --- This is the main method to define Squadrons programmatically.
  -- Squadrons:
  --
  --   * Have a **name or key** that is the identifier or key of the squadron.
  --   * Have **specific plane types** defined by **templates**.
  --   * Are **located at one specific airbase**. Multiple squadrons can be located at one airbase through.
  --   * Optionally have a limited set of **resources**. The default is that squadrons have unlimited resources.
  --
  -- The name of the squadron given acts as the **squadron key** in the AI\_A2A\_DISPATCHER:Squadron...() methods.
  --
  -- Additionally, squadrons have specific configuration options to:
  --
  --   * Control how new aircraft are **taking off** from the airfield (in the air, cold, hot, at the runway).
  --   * Control how returning aircraft are **landing** at the airfield (in the air near the airbase, after landing, after engine shutdown).
  --   * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped.
  --   * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned.
  --
  -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft.
  --
  -- @param #AI_A2A_DISPATCHER self
  --
  -- @param #string SquadronName A string (text) that defines the squadron identifier or the key of the Squadron.
  -- It can be any name, for example `"104th Squadron"` or `"SQ SQUADRON1"`, whatever.
  -- As long as you remember that this name becomes the identifier of your squadron you have defined.
  -- You need to use this name in other methods too!
  --
  -- @param #string AirbaseName The airbase name where you want to have the squadron located.
  -- You need to specify here EXACTLY the name of the airbase as you see it in the mission editor.
  -- Examples are `"Batumi"` or `"Tbilisi-Lochini"`.
  -- EXACTLY the airbase name, between quotes `""`.
  -- To ease the airbase naming when using the LDT editor and IntelliSense, the @{Wrapper.Airbase#AIRBASE} class contains enumerations of the airbases of each map.
  --
  --    * Caucasus: @{Wrapper.Airbase#AIRBASE.Caucaus}
  --    * Nevada or NTTR: @{Wrapper.Airbase#AIRBASE.Nevada}
  --    * Normandy: @{Wrapper.Airbase#AIRBASE.Normandy}
  --
  -- @param #string TemplatePrefixes A string or an array of strings specifying the **prefix names of the templates** (not going to explain what is templates here again).
  -- Examples are `{ "104th", "105th" }` or `"104th"` or `"Template 1"` or `"BLUE PLANES"`.
  -- Just remember that your template (groups late activated) need to start with the prefix you have specified in your code.
  -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets.
  --
  -- @param #number ResourceCount (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available.
  -- @return #AI_A2A_DISPATCHER self
  --
  -- @usage
  --   -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --   A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  -- @usage
  --   -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock...
  --   A2ADispatcher:SetSquadron( "Squadron1", "Batumi", "SQ1", 40 )
  --
  -- @usage
  --   -- This will create squadron "Sq 1" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" and has 20 planes in stock...
  --   -- Note that in this implementation, the A2A dispatcher will select a random plane type when a new plane (group) needs to be spawned for defenses.
  --   -- Note the usage of the {} for the airplane templates list.
  --   A2ADispatcher:SetSquadron( "Sq 1", "Batumi", { "Mig-29", "Su-27" }, 40 )
  --
  -- @usage
  --   -- This will create 2 squadrons "104th" and "23th" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" respectively and each squadron has 10 planes in stock...
  --   A2ADispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 )
  --   A2ADispatcher:SetSquadron( "23th", "Batumi", "Su-27", 10 )
  --
  -- @usage
  --   -- This is an example like the previous, but now with infinite resources.
  --   -- The ResourceCount parameter is not given in the SetSquadron method.
  --   A2ADispatcher:SetSquadron( "104th", "Batumi", "Mig-29" )
  --   A2ADispatcher:SetSquadron( "23th", "Batumi", "Su-27" )
  --
  function AI_A2A_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount )

    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {}

    local DefenderSquadron = self.DefenderSquadrons[SquadronName] -- #AI_A2A_DISPATCHER.Squadron

    DefenderSquadron.Name = SquadronName
    DefenderSquadron.Airbase = AIRBASE:FindByName( AirbaseName )
    DefenderSquadron.AirbaseName = DefenderSquadron.Airbase:GetName()
    if not DefenderSquadron.Airbase then
      error( "Cannot find airbase with name:" .. AirbaseName )
    end

    DefenderSquadron.Spawn = {}
    if type( TemplatePrefixes ) == "string" then
      local SpawnTemplate = TemplatePrefixes
      self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 )
      DefenderSquadron.Spawn[1] = self.DefenderSpawns[SpawnTemplate]
    else
      for TemplateID, SpawnTemplate in pairs( TemplatePrefixes ) do
        self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 )
        DefenderSquadron.Spawn[#DefenderSquadron.Spawn + 1] = self.DefenderSpawns[SpawnTemplate]
      end
    end
    DefenderSquadron.ResourceCount = ResourceCount
    DefenderSquadron.TemplatePrefixes = TemplatePrefixes
    DefenderSquadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured.

    self:SetSquadronLanguage( SquadronName, "EN" ) -- Squadrons speak English by default.

    self:F( { Squadron = { SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } )

    return self
  end

  --- Get an item from the Squadron table.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName Name of the squadron.
  -- @return #AI_A2A_DISPATCHER.Squadron Defender squadron table.
  function AI_A2A_DISPATCHER:GetSquadron( SquadronName )
    local DefenderSquadron = self.DefenderSquadrons[SquadronName]

    if not DefenderSquadron then
      error( "Unknown Squadron:" .. SquadronName )
    end

    return DefenderSquadron
  end
  
  --- Get a resource count from a specific squadron
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string Squadron Name of the squadron.
  -- @return #number Number of airframes available or nil if the squadron does not exist
  function AI_A2A_DISPATCHER:QuerySquadron(Squadron)
    local Squadron = self:GetSquadron(Squadron)
    if Squadron.ResourceCount then
      self:T2(string.format("%s = %s",Squadron.Name,Squadron.ResourceCount))
      return Squadron.ResourceCount
    end
    self:F({Squadron = Squadron.Name,SquadronResourceCount = Squadron.ResourceCount})
    return nil
  end

  --- [DEPRECATED - Might create problems launching planes] Set the Squadron visible before startup of the dispatcher.
  -- All planes will be spawned as uncontrolled on the parking spot.
  -- They will lock the parking spot.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage
  --
  --   -- Set the Squadron visible before startup of dispatcher.
  --   A2ADispatcher:SetSquadronVisible( "Mineralnye" )
  --
  function AI_A2A_DISPATCHER:SetSquadronVisible( SquadronName )

    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {}

    local DefenderSquadron = self:GetSquadron( SquadronName ) -- #AI_A2A_DISPATCHER.Squadron

    DefenderSquadron.Uncontrolled = true

    -- For now, grouping is forced to 1 due to other parts of the class which would not work well with grouping>1.
    DefenderSquadron.Grouping = 1

    -- Get free parking for fighter aircraft.
    local nfreeparking = DefenderSquadron.Airbase:GetFreeParkingSpotsNumber( AIRBASE.TerminalType.FighterAircraft, true )

    -- Take number of free parking spots if no resource count was specified.
    DefenderSquadron.ResourceCount = DefenderSquadron.ResourceCount or nfreeparking

    -- Check that resource count is not larger than free parking spots.
    DefenderSquadron.ResourceCount = math.min( DefenderSquadron.ResourceCount, nfreeparking )

    -- Set uncontrolled spawning option.
    for SpawnTemplate, _DefenderSpawn in pairs( self.DefenderSpawns ) do
      local DefenderSpawn = _DefenderSpawn -- Core.Spawn#SPAWN
      DefenderSpawn:InitUnControlled( true )
    end

  end

  --- Check if the Squadron is visible before startup of the dispatcher.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @return #boolean true if visible.
  -- @usage
  --
  --   -- Set the Squadron visible before startup of dispatcher.
  --   local IsVisible = A2ADispatcher:IsSquadronVisible( "Mineralnye" )
  --
  function AI_A2A_DISPATCHER:IsSquadronVisible( SquadronName )

    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {}

    local DefenderSquadron = self:GetSquadron( SquadronName ) -- #AI_A2A_DISPATCHER.Squadron

    if DefenderSquadron then
      return DefenderSquadron.Uncontrolled == true
    end

    return nil

  end

  --- Set a CAP for a Squadron.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed.
  -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed.
  -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement.
  -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement.
  -- @param #number EngageAltType The altitude type to engage, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed.
  -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed.
  -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed.
  -- @param #number PatrolFloorAltitude The minimum altitude at which the cap can be executed.
  -- @param #number PatrolCeilingAltitude the maximum altitude at which the cap can be executed.
  -- @param #number PatrolAltType The altitude type to patrol, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @return #AI_A2A_DISPATCHER
  -- @usage
  --
  --   -- CAP Squadron execution.
  --   CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) )
  --   -- Setup a CAP, engaging between 800 and 900 km/h, altitude 30 (above the sea), radio altitude measurement,
  --   -- patrolling speed between 500 and 600 km/h, altitude between 4000 and 10000 meters, barometric altitude measurement.
  --   A2ADispatcher:SetSquadronCapV2( "Mineralnye", 800, 900, 30, 30, "RADIO", CAPZoneEast, 500, 600, 4000, 10000, "BARO" )
  --   A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 )
  --
  --   CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) )
  --   -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 4000 and 10000 meters, radio altitude measurement,
  --   -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, barometric altitude measurement.
  --   A2ADispatcher:SetSquadronCapV2( "Sochi", 800, 1200, 2000, 3000, "RADIO", CAPZoneWest, 600, 800, 4000, 8000, "BARO" )
  --   A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 )
  --
  --   CAPZoneMiddle = ZONE:New( "CAP Zone Middle")
  --   -- Setup a CAP, engaging between 800 and 1200 km/h, altitude between 5000 and 8000 meters, barometric altitude measurement,
  --   -- patrolling speed between 600 and 800 km/h, altitude between 4000 and 8000, radio altitude.
  --   A2ADispatcher:SetSquadronCapV2( "Maykop", 800, 1200, 5000, 8000, "BARO", CAPZoneMiddle, 600, 800, 4000, 8000, "RADIO" )
  --   A2ADispatcher:SetSquadronCapInterval( "Maykop", 2, 30, 120, 1 )
  --
  function AI_A2A_DISPATCHER:SetSquadronCap2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType )

    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {}
    self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {}

    local DefenderSquadron = self:GetSquadron( SquadronName )

    local Cap = self.DefenderSquadrons[SquadronName].Cap
    Cap.Name = SquadronName
    Cap.EngageMinSpeed = EngageMinSpeed
    Cap.EngageMaxSpeed = EngageMaxSpeed
    Cap.EngageFloorAltitude = EngageFloorAltitude
    Cap.EngageCeilingAltitude = EngageCeilingAltitude
    Cap.Zone = Zone
    Cap.PatrolMinSpeed = PatrolMinSpeed
    Cap.PatrolMaxSpeed = PatrolMaxSpeed
    Cap.PatrolFloorAltitude = PatrolFloorAltitude
    Cap.PatrolCeilingAltitude = PatrolCeilingAltitude
    Cap.PatrolAltType = PatrolAltType
    Cap.EngageAltType = EngageAltType

    self:SetSquadronCapInterval( SquadronName, self.DefenderDefault.CapLimit, self.DefenderDefault.CapMinSeconds, self.DefenderDefault.CapMaxSeconds, 1 )

    self:T( { CAP = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageAltType } } )

    -- Add the CAP to the EWR network.

    local RecceSet = self.Detection:GetDetectionSet()
    RecceSet:FilterPrefixes( DefenderSquadron.TemplatePrefixes )
    RecceSet:FilterStart()

    self.Detection:SetFriendlyPrefixes( DefenderSquadron.TemplatePrefixes )

    return self
  end

  --- Set a CAP for a Squadron.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed.
  -- @param #number PatrolFloorAltitude The minimum altitude at which the cap can be executed.
  -- @param #number PatrolCeilingAltitude the maximum altitude at which the cap can be executed.
  -- @param #number PatrolMinSpeed The minimum speed at which the cap can be executed.
  -- @param #number PatrolMaxSpeed The maximum speed at which the cap can be executed.
  -- @param #number EngageMinSpeed The minimum speed at which the engage can be executed.
  -- @param #number EngageMaxSpeed The maximum speed at which the engage can be executed.
  -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @return #AI_A2A_DISPATCHER
  -- @usage
  --
  --   -- CAP Squadron execution.
  --   CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) )
  --   A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 )
  --   A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 )
  --
  --   CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) )
  --   A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" )
  --   A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 )
  --
  --   CAPZoneMiddle = ZONE:New( "CAP Zone Middle")
  --   A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" )
  --   A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 )
  --
  function AI_A2A_DISPATCHER:SetSquadronCap( SquadronName, Zone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType )

    return self:SetSquadronCap2( SquadronName, EngageMinSpeed, EngageMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, AltType )
  end

  --- Set the squadron CAP parameters.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number CapLimit (optional) The maximum amount of CAP groups to be spawned. Note that a CAP is a group, so can consist out of 1 to 4 airplanes. The default is 1 CAP group.
  -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new CAP will be spawned. The default is 180 seconds.
  -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new CAP will be spawned. The default is 600 seconds.
  -- @param #number Probability Is not in use, you can skip this parameter.
  -- @return #AI_A2A_DISPATCHER
  -- @usage
  --
  --   -- CAP Squadron execution.
  --   CAPZoneEast = ZONE_POLYGON:New( "CAP Zone East", GROUP:FindByName( "CAP Zone East" ) )
  --   A2ADispatcher:SetSquadronCap( "Mineralnye", CAPZoneEast, 4000, 10000, 500, 600, 800, 900 )
  --   A2ADispatcher:SetSquadronCapInterval( "Mineralnye", 2, 30, 60, 1 )
  --
  --   CAPZoneWest = ZONE_POLYGON:New( "CAP Zone West", GROUP:FindByName( "CAP Zone West" ) )
  --   A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" )
  --   A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 )
  --
  --   CAPZoneMiddle = ZONE:New( "CAP Zone Middle")
  --   A2ADispatcher:SetSquadronCap( "Maykop", CAPZoneMiddle, 4000, 8000, 600, 800, 800, 1200, "RADIO" )
  --   A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 )
  --
  function AI_A2A_DISPATCHER:SetSquadronCapInterval( SquadronName, CapLimit, LowInterval, HighInterval, Probability )

    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {}
    self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {}

    local DefenderSquadron = self:GetSquadron( SquadronName )

    local Cap = self.DefenderSquadrons[SquadronName].Cap
    if Cap then
      Cap.LowInterval = LowInterval or 180
      Cap.HighInterval = HighInterval or 600
      Cap.Probability = Probability or 1
      Cap.CapLimit = CapLimit or 1
      Cap.Scheduler = Cap.Scheduler or SCHEDULER:New( self )
      local Scheduler = Cap.Scheduler -- Core.Scheduler#SCHEDULER
      local ScheduleID = Cap.ScheduleID
      local Variance = (Cap.HighInterval - Cap.LowInterval) / 2
      local Repeat = Cap.LowInterval + Variance
      local Randomization = Variance / Repeat
      local Start = math.random( 1, Cap.HighInterval )

      if ScheduleID then
        Scheduler:Stop( ScheduleID )
      end

      Cap.ScheduleID = Scheduler:Schedule( self, self.SchedulerCAP, { SquadronName }, Start, Repeat, Randomization )
    else
      error( "This squadron does not exist:" .. SquadronName )
    end

  end

  ---
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @return #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:GetCAPDelay( SquadronName )

    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {}
    self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {}

    local DefenderSquadron = self:GetSquadron( SquadronName )

    local Cap = self.DefenderSquadrons[SquadronName].Cap
    if Cap then
      return math.random( Cap.LowInterval, Cap.HighInterval )
    else
      error( "This squadron does not exist:" .. SquadronName )
    end
  end

  --- Check if squadron can do CAP.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @return #AI_A2A_DISPATCHER.Squadron DefenderSquadron
  function AI_A2A_DISPATCHER:CanCAP( SquadronName )
    self:F( { SquadronName = SquadronName } )

    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {}
    self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {}

    local DefenderSquadron = self:GetSquadron( SquadronName )

    if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured.

      if (not DefenderSquadron.ResourceCount) or (DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0) then -- And, if there are sufficient resources.

        local Cap = DefenderSquadron.Cap
        if Cap then
          local CapCount = self:CountCapAirborne( SquadronName )
          self:F( { CapCount = CapCount } )
          if CapCount < Cap.CapLimit then
            local Probability = math.random()
            if Probability <= Cap.Probability then
              return DefenderSquadron
            end
          end
        end
      end
    end
    return nil
  end

  --- Set race track pattern as default when any squadron is performing CAP.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number LeglengthMin Min length of the race track leg in meters. Default 10,000 m.
  -- @param #number LeglengthMax Max length of the race track leg in meters. Default 15,000 m.
  -- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. counter clockwise from South to North.
  -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. counter clockwise from North to South.
  -- @param #number DurationMin (Optional) Min duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage.
  -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage.
  -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading.
  -- @return #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:SetDefaultCapRacetrack( LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates )

    self.DefenderDefault.Racetrack = true
    self.DefenderDefault.RacetrackLengthMin = LeglengthMin
    self.DefenderDefault.RacetrackLengthMax = LeglengthMax
    self.DefenderDefault.RacetrackHeadingMin = HeadingMin
    self.DefenderDefault.RacetrackHeadingMax = HeadingMax
    self.DefenderDefault.RacetrackDurationMin = DurationMin
    self.DefenderDefault.RacetrackDurationMax = DurationMax
    self.DefenderDefault.RacetrackCoordinates = CapCoordinates

    return self
  end

  --- Set race track pattern when squadron is performing CAP.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName Name of the squadron.
  -- @param #number LeglengthMin Min length of the race track leg in meters. Default 10,000 m.
  -- @param #number LeglengthMax Max length of the race track leg in meters. Default 15,000 m.
  -- @param #number HeadingMin Min heading of the race track in degrees. Default 0 deg, i.e. from South to North.
  -- @param #number HeadingMax Max heading of the race track in degrees. Default 180 deg, i.e. from North to South.
  -- @param #number DurationMin (Optional) Min duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage.
  -- @param #number DurationMax (Optional) Max duration in seconds before switching the orbit position. Default is keep same orbit until RTB or engage.
  -- @param #table CapCoordinates Table of coordinates of first race track point. Second point is determined by leg length and heading.
  -- @return #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:SetSquadronCapRacetrack( SquadronName, LeglengthMin, LeglengthMax, HeadingMin, HeadingMax, DurationMin, DurationMax, CapCoordinates )

    local DefenderSquadron = self:GetSquadron( SquadronName )

    if DefenderSquadron then
      DefenderSquadron.Racetrack = true
      DefenderSquadron.RacetrackLengthMin = LeglengthMin
      DefenderSquadron.RacetrackLengthMax = LeglengthMax
      DefenderSquadron.RacetrackHeadingMin = HeadingMin
      DefenderSquadron.RacetrackHeadingMax = HeadingMax
      DefenderSquadron.RacetrackDurationMin = DurationMin
      DefenderSquadron.RacetrackDurationMax = DurationMax
      DefenderSquadron.RacetrackCoordinates = CapCoordinates
    end

    return self
  end

  --- Check if squadron can do GCI.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @return #table DefenderSquadron
  function AI_A2A_DISPATCHER:CanGCI( SquadronName )
    self:F( { SquadronName = SquadronName } )

    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {}
    self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {}

    local DefenderSquadron = self:GetSquadron( SquadronName )

    if DefenderSquadron.Captured == false then -- We can only spawn new CAP if the base has not been captured.

      if (not DefenderSquadron.ResourceCount) or (DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0) then -- And, if there are sufficient resources.
        local Gci = DefenderSquadron.Gci
        if Gci then
          return DefenderSquadron
        end
      end
    end
    return nil
  end

  --- Set squadron GCI.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageMinSpeed The minimum speed [km/h] at which the GCI can be executed.
  -- @param #number EngageMaxSpeed The maximum speed [km/h] at which the GCI can be executed.
  -- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement.
  -- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement.
  -- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO".
  -- @return #AI_A2A_DISPATCHER
  -- @usage
  --
  --   -- GCI Squadron execution.
  --   A2ADispatcher:SetSquadronGci2( "Mozdok", 900, 1200, 5000, 5000, "BARO" )
  --   A2ADispatcher:SetSquadronGci2( "Novo", 900, 2100, 30, 30, "RADIO" )
  --   A2ADispatcher:SetSquadronGci2( "Maykop", 900, 1200, 100, 300, "RADIO" )
  --
  function AI_A2A_DISPATCHER:SetSquadronGci2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )

    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {}
    self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {}

    local Intercept = self.DefenderSquadrons[SquadronName].Gci
    Intercept.Name = SquadronName
    Intercept.EngageMinSpeed = EngageMinSpeed
    Intercept.EngageMaxSpeed = EngageMaxSpeed
    Intercept.EngageFloorAltitude = EngageFloorAltitude
    Intercept.EngageCeilingAltitude = EngageCeilingAltitude
    Intercept.EngageAltType = EngageAltType

    self:T( { GCI = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } )
  end

  --- Set squadron GCI.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageMinSpeed The minimum speed [km/h] at which the GCI can be executed.
  -- @param #number EngageMaxSpeed The maximum speed [km/h] at which the GCI can be executed.
  -- @return #AI_A2A_DISPATCHER
  -- @usage
  --
  --   -- GCI Squadron execution.
  --   A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 )
  --   A2ADispatcher:SetSquadronGci( "Novo", 900, 2100 )
  --   A2ADispatcher:SetSquadronGci( "Maykop", 900, 1200 )
  --
  function AI_A2A_DISPATCHER:SetSquadronGci( SquadronName, EngageMinSpeed, EngageMaxSpeed )

    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {}
    self.DefenderSquadrons[SquadronName].Gci = self.DefenderSquadrons[SquadronName].Gci or {}

    local Intercept = self.DefenderSquadrons[SquadronName].Gci
    Intercept.Name = SquadronName
    Intercept.EngageMinSpeed = EngageMinSpeed
    Intercept.EngageMaxSpeed = EngageMaxSpeed

    self:F( { GCI = { SquadronName, EngageMinSpeed, EngageMaxSpeed } } )
  end

  --- Defines the default amount of extra planes that will take-off as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number Overhead The % of Units that dispatching command will allocate to intercept in surplus of detected amount of units.
  -- @return #AI_A2A_DISPATCHER
  -- The default overhead is 1, so equal balance. The @{#AI_A2A_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength,
  -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles...
  -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes.
  -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values:
  --
  --   * Higher than 1, will increase the defense unit amounts.
  --   * Lower than 1, will decrease the defense unit amounts.
  --
  -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group
  -- multiplied by the Overhead and rounded up to the smallest integer.
  --
  -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution.
  --
  -- See example below.
  --
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2.
  --   -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 =>  rounded up gives 3.
  --   -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes.
  --   -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6  => rounded up gives 6 planes.
  --
  --   A2ADispatcher:SetDefaultOverhead( 1.5 )
  --
  function AI_A2A_DISPATCHER:SetDefaultOverhead( Overhead )

    self.DefenderDefault.Overhead = Overhead

    return self
  end

  --- Defines the amount of extra planes that will take-off as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number Overhead The % of Units that dispatching command will allocate to intercept in surplus of detected amount of units.
  -- @return #AI_A2A_DISPATCHER self
  -- The default overhead is 1, so equal balance. The @{#AI_A2A_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength,
  -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2A missiles payload, may still be less effective than a F-15C with short missiles...
  -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes.
  -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values:
  --
  --   * Higher than 1, will increase the defense unit amounts.
  --   * Lower than 1, will decrease the defense unit amounts.
  --
  -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group
  -- multiplied by the Overhead and rounded up to the smallest integer.
  --
  -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution.
  --
  -- See example below.
  --
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2.
  --   -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 =>  rounded up gives 3.
  --   -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes.
  --   -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6  => rounded up gives 6 planes.
  --
  --   A2ADispatcher:SetSquadronOverhead( "SquadronName", 1.5 )
  --
  function AI_A2A_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.Overhead = Overhead

    return self
  end

  --- Sets the default grouping of new airplanes spawned.
  -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Set a grouping by default per 2 airplanes.
  --   A2ADispatcher:SetDefaultGrouping( 2 )
  --
  function AI_A2A_DISPATCHER:SetDefaultGrouping( Grouping )

    self.DefenderDefault.Grouping = Grouping

    return self
  end

  --- Sets the grouping of new airplanes spawned.
  -- Grouping will trigger how new airplanes will be grouped if more than one airplane is spawned for defense.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number Grouping The level of grouping that will be applied of the CAP or GCI defenders.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Set a grouping per 2 airplanes.
  --   A2ADispatcher:SetSquadronGrouping( "SquadronName", 2 )
  --
  function AI_A2A_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.Grouping = Grouping

    return self
  end

  --- Defines the default method at which new flights will spawn and take-off as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights by default take-off in the air.
  --   A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Air )
  --
  --   -- Let new flights by default take-off from the runway.
  --   A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Runway )
  --
  --   -- Let new flights by default take-off from the airbase hot.
  --   A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Hot )
  --
  --   -- Let new flights by default take-off from the airbase cold.
  --   A2ADispatcher:SetDefaultTakeoff( AI_A2A_Dispatcher.Takeoff.Cold )
  --
  function AI_A2A_DISPATCHER:SetDefaultTakeoff( Takeoff )

    self.DefenderDefault.Takeoff = Takeoff

    return self
  end

  --- Defines the method at which new flights will spawn and take-off as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights take-off in the air.
  --   A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Air )
  --
  --   -- Let new flights take-off from the runway.
  --   A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Runway )
  --
  --   -- Let new flights take-off from the airbase hot.
  --   A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Hot )
  --
  --   -- Let new flights take-off from the airbase cold.
  --   A2ADispatcher:SetSquadronTakeoff( "SquadronName", AI_A2A_Dispatcher.Takeoff.Cold )
  --
  function AI_A2A_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.Takeoff = Takeoff

    return self
  end

  --- Gets the default method at which new flights will spawn and take-off as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway.
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights by default take-off in the air.
  --   local TakeoffMethod = A2ADispatcher:GetDefaultTakeoff()
  --   if TakeOffMethod == , AI_A2A_Dispatcher.Takeoff.InAir then
  --     ...
  --   end
  --
  function AI_A2A_DISPATCHER:GetDefaultTakeoff()

    return self.DefenderDefault.Takeoff
  end

  --- Gets the method at which new flights will spawn and take-off as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway.
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights take-off in the air.
  --   local TakeoffMethod = A2ADispatcher:GetSquadronTakeoff( "SquadronName" )
  --   if TakeOffMethod == , AI_A2A_Dispatcher.Takeoff.InAir then
  --     ...
  --   end
  --
  function AI_A2A_DISPATCHER:GetSquadronTakeoff( SquadronName )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff
  end

  --- Sets flights to default take-off in the air, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights by default take-off in the air.
  --   A2ADispatcher:SetDefaultTakeoffInAir()
  --
  function AI_A2A_DISPATCHER:SetDefaultTakeoffInAir()

    self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Air )

    return self
  end

  --- Set flashing player messages on or off
  -- @param #AI_A2A_DISPATCHER self
  -- @param #boolean onoff Set messages on (true) or off (false)
  function AI_A2A_DISPATCHER:SetSendMessages( onoff )
      self.SetSendPlayerMessages = onoff
  end

  --- Sets flights to take-off in the air, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number TakeoffAltitude (optional) The altitude in meters above the ground. If not given, the default takeoff altitude will be used.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights take-off in the air.
  --   A2ADispatcher:SetSquadronTakeoffInAir( "SquadronName" )
  --
  function AI_A2A_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude )

    self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Air )

    if TakeoffAltitude then
      self:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude )
    end

    return self
  end

  --- Sets flights by default to take-off from the runway, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights by default take-off from the runway.
  --   A2ADispatcher:SetDefaultTakeoffFromRunway()
  --
  function AI_A2A_DISPATCHER:SetDefaultTakeoffFromRunway()

    self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Runway )

    return self
  end

  --- Sets flights to take-off from the runway, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights take-off from the runway.
  --   A2ADispatcher:SetSquadronTakeoffFromRunway( "SquadronName" )
  --
  function AI_A2A_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName )

    self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Runway )

    return self
  end

  --- Sets flights by default to take-off from the airbase at a hot location, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights by default take-off at a hot parking spot.
  --   A2ADispatcher:SetDefaultTakeoffFromParkingHot()
  --
  function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingHot()

    self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Hot )

    return self
  end

  --- Sets flights to take-off from the airbase at a hot location, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights take-off in the air.
  --   A2ADispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" )
  --
  function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName )

    self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Hot )

    return self
  end

  --- Sets flights to by default take-off from the airbase at a cold location, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights take-off from a cold parking spot.
  --   A2ADispatcher:SetDefaultTakeoffFromParkingCold()
  --
  function AI_A2A_DISPATCHER:SetDefaultTakeoffFromParkingCold()

    self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Cold )

    return self
  end

  --- Sets flights to take-off from the airbase at a cold location, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights take-off from a cold parking spot.
  --   A2ADispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" )
  --
  function AI_A2A_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName )

    self:SetSquadronTakeoff( SquadronName, AI_A2A_DISPATCHER.Takeoff.Cold )

    return self
  end

  --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number TakeoffAltitude The altitude in meters above the ground.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Set the default takeoff altitude when taking off in the air.
  --   A2ADispatcher:SetDefaultTakeoffInAirAltitude( 2000 )  -- This makes planes start at 2000 meters above the ground.
  --
  function AI_A2A_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude )

    self.DefenderDefault.TakeoffAltitude = TakeoffAltitude

    return self
  end

  --- Defines the default altitude where airplanes will spawn in the air and take-off as part of the defense system, when the take-off in the air method has been selected.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number TakeoffAltitude The altitude in meters above the ground.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Set the default takeoff altitude when taking off in the air.
  --   A2ADispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes planes start at 2000 meters above the ground.
  --
  function AI_A2A_DISPATCHER:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.TakeoffAltitude = TakeoffAltitude

    return self
  end

  --- Defines the default method at which flights will land and despawn as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights by default despawn near the airbase when returning.
  --   A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.NearAirbase )
  --
  --   -- Let new flights by default despawn after landing land at the runway.
  --   A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.AtRunway )
  --
  --   -- Let new flights by default despawn after landing and parking, and after engine shutdown.
  --   A2ADispatcher:SetDefaultLanding( AI_A2A_Dispatcher.Landing.AtEngineShutdown )
  --
  function AI_A2A_DISPATCHER:SetDefaultLanding( Landing )

    self.DefenderDefault.Landing = Landing

    return self
  end

  --- Defines the method at which flights will land and despawn as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights despawn near the airbase when returning.
  --   A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.NearAirbase )
  --
  --   -- Let new flights despawn after landing land at the runway.
  --   A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.AtRunway )
  --
  --   -- Let new flights despawn after landing and parking, and after engine shutdown.
  --   A2ADispatcher:SetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.AtEngineShutdown )
  --
  function AI_A2A_DISPATCHER:SetSquadronLanding( SquadronName, Landing )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.Landing = Landing

    return self
  end

  --- Gets the default method at which flights will land and despawn as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights by default despawn near the airbase when returning.
  --   local LandingMethod = A2ADispatcher:GetDefaultLanding( AI_A2A_Dispatcher.Landing.NearAirbase )
  --   if LandingMethod == AI_A2A_Dispatcher.Landing.NearAirbase then
  --    ...
  --   end
  --
  function AI_A2A_DISPATCHER:GetDefaultLanding()

    return self.DefenderDefault.Landing
  end

  --- Gets the method at which flights will land and despawn as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let new flights despawn near the airbase when returning.
  --   local LandingMethod = A2ADispatcher:GetSquadronLanding( "SquadronName", AI_A2A_Dispatcher.Landing.NearAirbase )
  --   if LandingMethod == AI_A2A_Dispatcher.Landing.NearAirbase then
  --    ...
  --   end
  --
  function AI_A2A_DISPATCHER:GetSquadronLanding( SquadronName )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    return DefenderSquadron.Landing or self.DefenderDefault.Landing
  end

  --- Sets flights by default to land and despawn near the airbase in the air, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let flights by default to land near the airbase and despawn.
  --   A2ADispatcher:SetDefaultLandingNearAirbase()
  --
  function AI_A2A_DISPATCHER:SetDefaultLandingNearAirbase()

    self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.NearAirbase )

    return self
  end

  --- Sets flights to land and despawn near the airbase in the air, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let flights to land near the airbase and despawn.
  --   A2ADispatcher:SetSquadronLandingNearAirbase( "SquadronName" )
  --
  function AI_A2A_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName )

    self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.NearAirbase )

    return self
  end

  --- Sets flights by default to land and despawn at the runway, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let flights by default land at the runway and despawn.
  --   A2ADispatcher:SetDefaultLandingAtRunway()
  --
  function AI_A2A_DISPATCHER:SetDefaultLandingAtRunway()

    self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtRunway )

    return self
  end

  --- Sets flights to land and despawn at the runway, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let flights land at the runway and despawn.
  --   A2ADispatcher:SetSquadronLandingAtRunway( "SquadronName" )
  --
  function AI_A2A_DISPATCHER:SetSquadronLandingAtRunway( SquadronName )

    self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.AtRunway )

    return self
  end

  --- Sets flights by default to land and despawn at engine shutdown, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let flights by default land and despawn at engine shutdown.
  --   A2ADispatcher:SetDefaultLandingAtEngineShutdown()
  --
  function AI_A2A_DISPATCHER:SetDefaultLandingAtEngineShutdown()

    self:SetDefaultLanding( AI_A2A_DISPATCHER.Landing.AtEngineShutdown )

    return self
  end

  --- Sets flights to land and despawn at engine shutdown, as part of the defense system.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage:
  --
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --
  --   -- Let flights land and despawn at engine shutdown.
  --   A2ADispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" )
  --
  function AI_A2A_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName )

    self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.AtEngineShutdown )

    return self
  end

  --- Set the default fuel threshold when defenders will RTB or Refuel in the air.
  -- The fuel threshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the % of the threshold of fuel remaining in the tank when the plane will go RTB or Refuel.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage
  --
  --   -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --   A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  --   -- Now Setup the default fuel threshold.
  --   A2ADispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank.
  --
  function AI_A2A_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold )

    self.DefenderDefault.FuelThreshold = FuelThreshold

    return self
  end

  --- Set the fuel threshold for the squadron when defenders will RTB or Refuel in the air.
  -- The fuel threshold is by default set to 15%, which means that an airplane will stay in the air until 15% of its fuel has been consumed.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the % of the threshold of fuel remaining in the tank when the plane will go RTB or Refuel.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage
  --
  --   -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --   A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  --   -- Now Setup the default fuel threshold.
  --   A2ADispatcher:SetSquadronFuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank.
  --
  function AI_A2A_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.FuelThreshold = FuelThreshold

    return self
  end

  --- Set the default tanker where defenders will Refuel in the air.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor.
  -- @return #AI_A2A_DISPATCHER self
  -- @usage
  --
  --   -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --   A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  --   -- Now Setup the default fuel threshold.
  --   A2ADispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank.
  --
  --   -- Now Setup the default tanker.
  --   A2ADispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor.
  --
  function AI_A2A_DISPATCHER:SetDefaultTanker( TankerName )

    self.DefenderDefault.TankerName = TankerName

    return self
  end

  --- Set the squadron tanker where defenders will Refuel in the air.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor.
  -- @return #AI_A2A_DISPATCHER
  -- @usage
  --
  --   -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --   A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  --   -- Now Setup the squadron fuel threshold.
  --   A2ADispatcher:SetSquadronFuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank.
  --
  --   -- Now Setup the squadron tanker.
  --   A2ADispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor.
  --
  function AI_A2A_DISPATCHER:SetSquadronTanker( SquadronName, TankerName )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.TankerName = TankerName

    return self
  end

  --- Set the squadron language.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #string Language A string defining the language to be embedded within the miz file.
  -- @return #AI_A2A_DISPATCHER
  -- @usage
  --
  --   -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --   A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )
  --
  --   -- Set for English.
  --   A2ADispatcher:SetSquadronLanguage( "SquadronName", "EN" ) -- This squadron speaks English.
  --
  --   -- Set for Russian.
  --   A2ADispatcher:SetSquadronLanguage( "SquadronName", "RU" ) -- This squadron speaks Russian.
  function AI_A2A_DISPATCHER:SetSquadronLanguage( SquadronName, Language )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.Language = Language

    if DefenderSquadron.RadioQueue then
      DefenderSquadron.RadioQueue:SetLanguage( Language )
    end

    return self
  end

  --- Set the frequency of communication and the mode of communication for voice overs.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number RadioFrequency The frequency of communication.
  -- @param #number RadioModulation The modulation of communication.
  -- @param #number RadioPower The power in Watts of communication.
  function AI_A2A_DISPATCHER:SetSquadronRadioFrequency( SquadronName, RadioFrequency, RadioModulation, RadioPower )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.RadioFrequency = RadioFrequency
    DefenderSquadron.RadioModulation = RadioModulation or radio.modulation.AM
    DefenderSquadron.RadioPower = RadioPower or 100

    if DefenderSquadron.RadioQueue then
      DefenderSquadron.RadioQueue:Stop()
    end

    DefenderSquadron.RadioQueue = nil

    DefenderSquadron.RadioQueue = RADIOSPEECH:New( DefenderSquadron.RadioFrequency, DefenderSquadron.RadioModulation )
    DefenderSquadron.RadioQueue.power = DefenderSquadron.RadioPower
    DefenderSquadron.RadioQueue:Start( 0.5 )

    DefenderSquadron.RadioQueue:SetLanguage( DefenderSquadron.Language )
  end

  --- Add defender to squadron. Resource count will get smaller.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #AI_A2A_DISPATCHER.Squadron Squadron The squadron.
  -- @param Wrapper.Group#GROUP Defender The defender group.
  -- @param #number Size Size of the group.
  function AI_A2A_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size )
    self.Defenders = self.Defenders or {}
    local DefenderName = Defender:GetName()
    self.Defenders[DefenderName] = Squadron
    if Squadron.ResourceCount then
      Squadron.ResourceCount = Squadron.ResourceCount - Size
    end
    self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } )
  end

  --- Remove defender from squadron. Resource count will increase.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #AI_A2A_DISPATCHER.Squadron Squadron The squadron.
  -- @param Wrapper.Group#GROUP Defender The defender group.
  function AI_A2A_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender )
    self.Defenders = self.Defenders or {}
    local DefenderName = Defender:GetName()
    if Squadron.ResourceCount then
      Squadron.ResourceCount = Squadron.ResourceCount + Defender:GetSize()
    end
    self.Defenders[DefenderName] = nil
    self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } )
  end

  --- Get squadron from defender.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Wrapper.Group#GROUP Defender The defender group.
  -- @return #AI_A2A_DISPATCHER.Squadron Squadron The squadron.
  function AI_A2A_DISPATCHER:GetSquadronFromDefender( Defender )
    self.Defenders = self.Defenders or {}
    if Defender ~= nil then
      local DefenderName = Defender:GetName()
      self:F( { DefenderName = DefenderName } )
      return self.Defenders[DefenderName]
    else
      return nil
    end
  end

  --- Creates an SWEEP task when there are targets for it.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem
  -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units.
  function AI_A2A_DISPATCHER:EvaluateSWEEP( DetectedItem )
    self:F( { DetectedItem.ItemID } )

    local DetectedSet = DetectedItem.Set
    local DetectedZone = DetectedItem.Zone

    if DetectedItem.IsDetected == false then

      -- Here we're doing something advanced... We're copying the DetectedSet.
      local TargetSetUnit = SET_UNIT:New()
      TargetSetUnit:SetDatabase( DetectedSet )
      TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection.

      return TargetSetUnit
    end

    return nil
  end

  --- Count number of airborne CAP flights.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName Name of the squadron.
  -- @return #number Number of defender CAP groups.
  function AI_A2A_DISPATCHER:CountCapAirborne( SquadronName )

    local CapCount = 0

    local DefenderSquadron = self.DefenderSquadrons[SquadronName]
    if DefenderSquadron then
      for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do
        if DefenderTask.SquadronName == SquadronName then
          if DefenderTask.Type == "CAP" then
            if AIGroup and AIGroup:IsAlive() then
              -- Check if the CAP is patrolling or engaging. If not, this is not a valid CAP, even if it is alive!
              -- The CAP could be damaged, lost control, or out of fuel!
              -- env.info("FF fsm state "..tostring(DefenderTask.Fsm:GetState()))
              if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" ) or DefenderTask.Fsm:Is( "Started" ) then
                -- env.info("FF capcount "..CapCount)
                CapCount = CapCount + 1
              end
            end
          end
        end
      end
    end

    return CapCount
  end

  --- Count number of engaging defender groups.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detection object.
  -- @return #number Number of defender groups engaging.
  function AI_A2A_DISPATCHER:CountDefendersEngaged( AttackerDetection )

    -- First, count the active AIGroups Units, targeting the DetectedSet
    local DefenderCount = 0

    local DetectedSet = AttackerDetection.Set
    -- DetectedSet:Flush()

    local DefenderTasks = self:GetDefenderTasks()

    for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do
      local Defender = DefenderGroup -- Wrapper.Group#GROUP
      local DefenderTaskTarget = DefenderTask.Target -- Functional.Detection#DETECTION_BASE.DetectedItem
      local DefenderSquadronName = DefenderTask.SquadronName

      if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then
        local Squadron = self:GetSquadron( DefenderSquadronName )
        local SquadronOverhead = Squadron.Overhead or self.DefenderDefault.Overhead

        local DefenderSize = Defender:GetInitialSize()
        if DefenderSize then
          DefenderCount = DefenderCount + DefenderSize / SquadronOverhead
          self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize )
        else
          DefenderCount = 0
        end
      end
    end

    self:F( { DefenderCount = DefenderCount } )

    return DefenderCount
  end

  --- Count defenders to be engaged if number of attackers larger than number of defenders.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item.
  -- @param #number DefenderCount Number of defenders.
  -- @return #table Table of friendly groups.
  function AI_A2A_DISPATCHER:CountDefendersToBeEngaged( AttackerDetection, DefenderCount )

    local Friendlies = nil

    local AttackerSet = AttackerDetection.Set
    local AttackerCount = AttackerSet:Count()

    local DefenderFriendlies = self:GetAIFriendliesNearBy( AttackerDetection )

    for FriendlyDistance, AIFriendly in UTILS.spairs( DefenderFriendlies or {} ) do
      -- We only allow to ENGAGE targets as long as the Units on both sides are balanced.
      if AttackerCount > DefenderCount then
    --self:T("***** AI_A2A_DISPATCHER:CountDefendersToBeEngaged() *****\nThis is supposed to be a UNIT:")
    if AIFriendly then
      local classname = AIFriendly.ClassName or "No Class Name"
      local unitname = AIFriendly.IdentifiableName or "No Unit Name"
      --self:T("Class Name: " .. classname)
      --self:T("Unit Name: " .. unitname)
      --self:T({AIFriendly})
    end
    local Friendly = nil
    if AIFriendly and AIFriendly:IsAlive() then
      --self:T("AIFriendly alive, getting GROUP")
      Friendly = AIFriendly:GetGroup() -- Wrapper.Group#GROUP
    end
    
        if Friendly and Friendly:IsAlive() then
          -- Ok, so we have a friendly near the potential target.
          -- Now we need to check if the AIGroup has a Task.
          local DefenderTask = self:GetDefenderTask( Friendly )
          if DefenderTask then
            -- The Task should be CAP or GCI
            if DefenderTask.Type == "CAP" or DefenderTask.Type == "GCI" then
              -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet
              if DefenderTask.Target == nil then
                if DefenderTask.Fsm:Is( "Returning" ) or DefenderTask.Fsm:Is( "Patrolling" ) then
                  Friendlies = Friendlies or {}
                  Friendlies[Friendly] = Friendly
                  DefenderCount = DefenderCount + Friendly:GetSize()
                  self:F( { Friendly = Friendly:GetName(), FriendlyDistance = FriendlyDistance } )
                end
              end
            end
          end
        end
      else
        break
      end
    end

    return Friendlies
  end

  --- Activate resource.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #AI_A2A_DISPATCHER.Squadron DefenderSquadron The defender squadron.
  -- @param #number DefendersNeeded Number of defenders needed. Default 4.
  -- @return Wrapper.Group#GROUP The defender group.
  -- @return #boolean Grouping.
  function AI_A2A_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded )

    local SquadronName = DefenderSquadron.Name

    DefendersNeeded = DefendersNeeded or 4

    local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping

    DefenderGrouping = (DefenderGrouping < DefendersNeeded) and DefenderGrouping or DefendersNeeded

    -- env.info(string.format("FF resource activate: Squadron=%s grouping=%d needed=%d visible=%s", SquadronName, DefenderGrouping, DefendersNeeded, tostring(self:IsSquadronVisible( SquadronName ))))

    if self:IsSquadronVisible( SquadronName ) then

      local n = #self.uncontrolled[SquadronName]

      if n > 0 then
        -- Random number 1,...n
        local id = math.random( n )

        -- Pick a random defender group.
        local Defender = self.uncontrolled[SquadronName][id].group -- Wrapper.Group#GROUP

        -- Start uncontrolled group.
        Defender:StartUncontrolled()

        -- Get grouping.
        DefenderGrouping = self.uncontrolled[SquadronName][id].grouping

        -- Add defender to squadron.
        self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping )

        -- Remove defender from uncontrolled table.
        table.remove( self.uncontrolled[SquadronName], id )

        return Defender, DefenderGrouping
      else
        return nil, 0
      end

      -- Here we CAP the new planes.
      -- The Resources table is filled in advance.
      local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template.

      --[[
      -- We determine the grouping based on the parameters set.
      self:F( { DefenderGrouping = DefenderGrouping } )

      -- New we will form the group to spawn in.
      -- We search for the first free resource matching the template.
      local DefenderUnitIndex = 1
      local DefenderCAPTemplate = nil
      local DefenderName = nil
      for GroupName, DefenderGroup in pairs( DefenderSquadron.Resources[TemplateID] or {} ) do
        self:F( { GroupName = GroupName } )
        local DefenderTemplate = _DATABASE:GetGroupTemplate( GroupName )
        if DefenderUnitIndex == 1 then
          DefenderCAPTemplate = UTILS.DeepCopy( DefenderTemplate )
          self.DefenderCAPIndex = self.DefenderCAPIndex + 1
          DefenderCAPTemplate.name = SquadronName .. "#" .. self.DefenderCAPIndex .. "#" .. GroupName
          DefenderName = DefenderCAPTemplate.name
        else
          -- Add the unit in the template to the DefenderCAPTemplate.
          local DefenderUnitTemplate = DefenderTemplate.units[1]
          DefenderCAPTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate
        end
        DefenderUnitIndex = DefenderUnitIndex + 1
        DefenderSquadron.Resources[TemplateID][GroupName] = nil
        if DefenderUnitIndex > DefenderGrouping then
          break
        end

      end

      if DefenderCAPTemplate then
        local TakeoffMethod = self:GetSquadronTakeoff( SquadronName )
        local SpawnGroup = GROUP:Register( DefenderName )
        DefenderCAPTemplate.lateActivation = nil
        DefenderCAPTemplate.uncontrolled = nil
        local Takeoff = self:GetSquadronTakeoff( SquadronName )
        DefenderCAPTemplate.route.points[1].type   = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type
        DefenderCAPTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action
        local Defender = _DATABASE:Spawn( DefenderCAPTemplate )

        self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping )
        return Defender, DefenderGrouping
      end
      ]]

    else

      ----------------------------
      --- Squadron not visible ---
      ----------------------------

      local Spawn = DefenderSquadron.Spawn[math.random( 1, #DefenderSquadron.Spawn )] -- Core.Spawn#SPAWN

      if DefenderGrouping then
        Spawn:InitGrouping( DefenderGrouping )
      else
        Spawn:InitGrouping()
      end

      local TakeoffMethod = self:GetSquadronTakeoff( SquadronName )

      local Defender = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP

      self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping )

      return Defender, DefenderGrouping
    end

    return nil, nil
  end

  --- On after "CAP" event.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param #string SquadronName Name of the squadron.
  function AI_A2A_DISPATCHER:onafterCAP( From, Event, To, SquadronName )

    self:F( { SquadronName = SquadronName } )
    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {}
    self.DefenderSquadrons[SquadronName].Cap = self.DefenderSquadrons[SquadronName].Cap or {}

    local DefenderSquadron = self:CanCAP( SquadronName )

    if DefenderSquadron then

      local Cap = DefenderSquadron.Cap

      if Cap then

        local DefenderCAP, DefenderGrouping = self:ResourceActivate( DefenderSquadron )

        if DefenderCAP then

          local AI_A2A_Fsm = AI_A2A_CAP:New2( DefenderCAP, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.EngageFloorAltitude, Cap.EngageCeilingAltitude, Cap.EngageAltType, Cap.Zone, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.PatrolFloorAltitude, Cap.PatrolCeilingAltitude, Cap.PatrolAltType )
          AI_A2A_Fsm:SetDispatcher( self )
          AI_A2A_Fsm:SetHomeAirbase( DefenderSquadron.Airbase )
          AI_A2A_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 )
          AI_A2A_Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold )
          AI_A2A_Fsm:SetDisengageRadius( self.DisengageRadius )
          AI_A2A_Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName )
          if DefenderSquadron.Racetrack or self.DefenderDefault.Racetrack then
            AI_A2A_Fsm:SetRaceTrackPattern( DefenderSquadron.RacetrackLengthMin or self.DefenderDefault.RacetrackLengthMin,
                                            DefenderSquadron.RacetrackLengthMax or self.DefenderDefault.RacetrackLengthMax,
                                            DefenderSquadron.RacetrackHeadingMin or self.DefenderDefault.RacetrackHeadingMin,
                                            DefenderSquadron.RacetrackHeadingMax or self.DefenderDefault.RacetrackHeadingMax,
                                            DefenderSquadron.RacetrackDurationMin or self.DefenderDefault.RacetrackDurationMin,
                                            DefenderSquadron.RacetrackDurationMax or self.DefenderDefault.RacetrackDurationMax,
                                            DefenderSquadron.RacetrackCoordinates or self.DefenderDefault.RacetrackCoordinates )
          end
          AI_A2A_Fsm:Start()

          self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", AI_A2A_Fsm )

          function AI_A2A_Fsm:onafterTakeoff( DefenderGroup, From, Event, To )
            -- Issue GetCallsign() returns nil, see https://github.com/FlightControl-Master/MOOSE/issues/1228
            if DefenderGroup and DefenderGroup:IsAlive() then
              self:F( { "CAP Takeoff", DefenderGroup:GetName() } )
              -- self:GetParent(self).onafterBirth( self, Defender, From, Event, To )

              local DefenderName = DefenderGroup:GetCallsign()
              local Dispatcher = AI_A2A_Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER
              local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )

              if Squadron then
                if self.SetSendPlayerMessages then
                  Dispatcher:MessageToPlayers( Squadron,  DefenderName .. " Wheels up.", DefenderGroup )
                end
                AI_A2A_Fsm:__Patrol( 2 ) -- Start Patrolling
              end
            end
          end

          function AI_A2A_Fsm:onafterPatrolRoute( DefenderGroup, From, Event, To )
            if DefenderGroup and DefenderGroup:IsAlive() then
              self:F( { "CAP PatrolRoute", DefenderGroup:GetName() } )
              self:GetParent( self ).onafterPatrolRoute( self, DefenderGroup, From, Event, To )

              local DefenderName = DefenderGroup:GetCallsign()
              local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER
              local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
              if Squadron and self.SetSendPlayerMessages then       
                  Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", patrolling.", DefenderGroup )
              end

              Dispatcher:ClearDefenderTaskTarget( DefenderGroup )
            end
          end

          function AI_A2A_Fsm:onafterRTB( DefenderGroup, From, Event, To )
            if DefenderGroup and DefenderGroup:IsAlive() then
              self:F( { "CAP RTB", DefenderGroup:GetName() } )

              self:GetParent( self ).onafterRTB( self, DefenderGroup, From, Event, To )

              local DefenderName = DefenderGroup:GetCallsign()
              local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER
              local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
              if Squadron and self.SetSendPlayerMessages then
                Dispatcher:MessageToPlayers( Squadron,  DefenderName .. " returning to base.", DefenderGroup )
              end
              Dispatcher:ClearDefenderTaskTarget( DefenderGroup )
            end
          end

          --- AI_A2A_Fsm:onafterHome
          -- @param #AI_A2A_DISPATCHER self
          function AI_A2A_Fsm:onafterHome( Defender, From, Event, To, Action )
            if Defender and Defender:IsAlive() then
              self:F( { "CAP Home", Defender:GetName() } )
              self:GetParent( self ).onafterHome( self, Defender, From, Event, To )

              local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER
              local Squadron = Dispatcher:GetSquadronFromDefender( Defender )

              if Action and Action == "Destroy" then
                Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender )
                Defender:Destroy()
              end

              if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then
                Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender )
                Defender:Destroy()
                Dispatcher:ParkDefender( Squadron )
              end
            end
          end
        end
      end
    end

  end

  --- On after "ENGAGE" event.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item.
  -- @param #table Defenders Defenders table.
  function AI_A2A_DISPATCHER:onafterENGAGE( From, Event, To, AttackerDetection, Defenders )
    self:F( "ENGAGING Detection ID=" .. tostring( AttackerDetection.ID ) )

    if Defenders then

      for DefenderID, Defender in pairs( Defenders ) do

        local Fsm = self:GetDefenderTaskFsm( Defender )

        Fsm:EngageRoute( AttackerDetection.Set ) -- Engage on the TargetSetUnit

        self:SetDefenderTaskTarget( Defender, AttackerDetection )

      end

    end
  end

  --- On after "GCI" event.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem AttackerDetection Detected item.
  -- @param #number DefendersMissing Number of missing defenders.
  -- @param #table DefenderFriendlies Friendly defenders.
  function AI_A2A_DISPATCHER:onafterGCI( From, Event, To, AttackerDetection, DefendersMissing, DefenderFriendlies )

    self:F( "GCI Detection ID=" .. tostring( AttackerDetection.ID ) )

    self:F( { From, Event, To, AttackerDetection.Index, DefendersMissing, DefenderFriendlies } )

    local AttackerSet = AttackerDetection.Set
    local AttackerUnit = AttackerSet:GetFirst()

    if AttackerUnit and AttackerUnit:IsAlive() then
      local AttackerCount = AttackerSet:Count()
      local DefenderCount = 0

      for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do

        local Fsm = self:GetDefenderTaskFsm( DefenderGroup )
        Fsm:__EngageRoute( 0.1, AttackerSet ) -- Engage on the TargetSetUnit

        self:SetDefenderTaskTarget( DefenderGroup, AttackerDetection )

        DefenderCount = DefenderCount + DefenderGroup:GetSize()
      end

      self:F( { DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } )
      DefenderCount = DefendersMissing

      local ClosestDistance = 0
      local ClosestDefenderSquadronName = nil

      local BreakLoop = false

      while (DefenderCount > 0 and not BreakLoop) do

        self:F( { DefenderSquadrons = self.DefenderSquadrons } )

        for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons or {} ) do

          self:F( { GCI = DefenderSquadron.Gci } )

          for InterceptID, Intercept in pairs( DefenderSquadron.Gci or {} ) do

            self:F( { DefenderSquadron } )
            local SpawnCoord = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE
            local AttackerCoord = AttackerUnit:GetCoordinate()
            local InterceptCoord = AttackerDetection.InterceptCoord
            self:F( { InterceptCoord = InterceptCoord } )
            if InterceptCoord then
              local InterceptDistance = SpawnCoord:Get2DDistance( InterceptCoord )
              local AirbaseDistance = SpawnCoord:Get2DDistance( AttackerCoord )
              self:F( { InterceptDistance = InterceptDistance, AirbaseDistance = AirbaseDistance, InterceptCoord = InterceptCoord } )

              if ClosestDistance == 0 or InterceptDistance < ClosestDistance then

                -- Only intercept if the distance to target is smaller or equal to the GciRadius limit.
                if AirbaseDistance <= self.GciRadius then
                  ClosestDistance = InterceptDistance
                  ClosestDefenderSquadronName = SquadronName
                end
              end
            end
          end
        end

        if ClosestDefenderSquadronName then

          local DefenderSquadron = self:CanGCI( ClosestDefenderSquadronName )

          if DefenderSquadron then

            local Gci = self.DefenderSquadrons[ClosestDefenderSquadronName].Gci

            if Gci then

              local DefenderOverhead = DefenderSquadron.Overhead or self.DefenderDefault.Overhead
              local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping
              local DefendersNeeded = math.ceil( DefenderCount * DefenderOverhead )

              self:F( { Overhead = DefenderOverhead, SquadronOverhead = DefenderSquadron.Overhead, DefaultOverhead = self.DefenderDefault.Overhead } )
              self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } )
              self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } )

              -- DefenderSquadron.ResourceCount can have the value nil, which expresses unlimited resources.
              -- DefendersNeeded cannot exceed DefenderSquadron.ResourceCount!
              if DefenderSquadron.ResourceCount and DefendersNeeded > DefenderSquadron.ResourceCount then
                DefendersNeeded = DefenderSquadron.ResourceCount
                BreakLoop = true
              end

              while (DefendersNeeded > 0) do

                local DefenderGCI, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded )

                DefendersNeeded = DefendersNeeded - DefenderGrouping

                if DefenderGCI then

                  DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead

                  local Fsm = AI_A2A_GCI:New2( DefenderGCI, Gci.EngageMinSpeed, Gci.EngageMaxSpeed, Gci.EngageFloorAltitude, Gci.EngageCeilingAltitude, Gci.EngageAltType )
                  Fsm:SetDispatcher( self )
                  Fsm:SetHomeAirbase( DefenderSquadron.Airbase )
                  Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 )
                  Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold )
                  Fsm:SetDisengageRadius( self.DisengageRadius )
                  Fsm:Start()

                  self:SetDefenderTask( ClosestDefenderSquadronName, DefenderGCI, "GCI", Fsm, AttackerDetection )

                  function Fsm:onafterTakeoff( DefenderGroup, From, Event, To )
                    self:F( { "GCI Birth", DefenderGroup:GetName() } )
                    -- self:GetParent(self).onafterBirth( self, Defender, From, Event, To )

                    local DefenderName = DefenderGroup:GetCallsign()
                    local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER
                    local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
                    local DefenderTarget = Dispatcher:GetDefenderTaskTarget( DefenderGroup )

                    if DefenderTarget then
                      if Squadron.Language == "EN" and self.SetSendPlayerMessages then
                        Dispatcher:MessageToPlayers( Squadron,  DefenderName .. " wheels up.", DefenderGroup )
                      elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then
                        Dispatcher:MessageToPlayers( Squadron,  DefenderName .. " колёса вверх.", DefenderGroup )
                      end
                      -- Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit
                      Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit
                    end
                  end

                  function Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit )
                    self:F( { "GCI Route", DefenderGroup:GetName() } )

                    local DefenderName = DefenderGroup:GetCallsign()
                    local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER
                    local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )

                    if Squadron and AttackSetUnit:Count() > 0 then
                      local FirstUnit = AttackSetUnit:GetFirst()
                      local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE

                      if Squadron.Language == "EN" and self.SetSendPlayerMessages then
                        Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", intercepting bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup )
                      elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then
                        Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", перехватывая боги в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup )
                      elseif Squadron.Language == "DE" and self.SetSendPlayerMessages then
                        Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", Eindringlinge abfangen bei" .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup )
                      end
                    end
                    self:GetParent( Fsm ).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit )
                  end

                  function Fsm:onafterEngage( DefenderGroup, From, Event, To, AttackSetUnit )
                    self:F( { "GCI Engage", DefenderGroup:GetName() } )

                    local DefenderName = DefenderGroup:GetCallsign()
                    local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER
                    local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )

                    if Squadron and AttackSetUnit:Count() > 0 then
                      local FirstUnit = AttackSetUnit:GetFirst()
                      local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE

                      if Squadron.Language == "EN" and self.SetSendPlayerMessages then
                        Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", engaging bogeys at " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup )
                      elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then
                        Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", задействуя боги в " .. Coordinate:ToStringA2A( DefenderGroup, nil, Squadron.Language ), DefenderGroup )
                      end
                    end
                    self:GetParent( Fsm ).onafterEngage( self, DefenderGroup, From, Event, To, AttackSetUnit )
                  end

                  function Fsm:onafterRTB( DefenderGroup, From, Event, To )
                    self:F( { "GCI RTB", DefenderGroup:GetName() } )
                    self:GetParent( self ).onafterRTB( self, DefenderGroup, From, Event, To )

                    local DefenderName = DefenderGroup:GetCallsign()
                    local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER
                    local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )

                    if Squadron then
                      if Squadron.Language == "EN" and self.SetSendPlayerMessages then
                        Dispatcher:MessageToPlayers( Squadron,  DefenderName .. " returning to base.", DefenderGroup )
                      elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then
                        Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", возвращение на базу.", DefenderGroup )
                      end
                    end
                    Dispatcher:ClearDefenderTaskTarget( DefenderGroup )
                  end

                  --- function Fsm:onafterLostControl
                  -- @param #AI_A2A_DISPATCHER self
                  function Fsm:onafterLostControl( Defender, From, Event, To )
                    self:F( { "GCI LostControl", Defender:GetName() } )
                    self:GetParent( self ).onafterHome( self, Defender, From, Event, To )

                    local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER
                    local Squadron = Dispatcher:GetSquadronFromDefender( Defender )
                    if Defender:IsAboveRunway() then
                      Dispatcher:RemoveDefenderFromSquadron( Squadron, Defender )
                      Defender:Destroy()
                    end
                  end

                  --- function Fsm:onafterHome
                  -- @param #AI_A2A_DISPATCHER self
                  function Fsm:onafterHome( DefenderGroup, From, Event, To, Action )
                    self:F( { "GCI Home", DefenderGroup:GetName() } )
                    self:GetParent( self ).onafterHome( self, DefenderGroup, From, Event, To )

                    local DefenderName = DefenderGroup:GetCallsign()
                    local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER
                    local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )

                      if Squadron.Language == "EN" and self.SetSendPlayerMessages then
                        Dispatcher:MessageToPlayers( Squadron,  DefenderName .. " landing at base.", DefenderGroup )
                      elseif Squadron.Language == "RU" and self.SetSendPlayerMessages then
                        Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", посадка на базу.", DefenderGroup )
                      end

                    if Action and Action == "Destroy" then
                      Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup )
                      DefenderGroup:Destroy()
                    end

                    if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2A_DISPATCHER.Landing.NearAirbase then
                      Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup )
                      DefenderGroup:Destroy()
                      Dispatcher:ParkDefender( Squadron )
                    end
                  end

                end -- if DefenderGCI then
              end -- while ( DefendersNeeded > 0 ) do
            end
          else
            -- No more resources, try something else.
            -- Subject for a later enhancement to try to depart from another squadron and disable this one.
            BreakLoop = true
            break
          end
        else
          -- There isn't any closest airbase anymore, break the loop.
          break
        end
      end -- if DefenderSquadron then
    end -- if AttackerUnit
  end

  --- Creates an ENGAGE task when there are human friendlies airborne near the targets.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item.
  -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units or nil.
  function AI_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem )
    self:F( { DetectedItem.ItemID } )

    -- First, count the active AIGroups Units, targeting the DetectedSet
    local DefenderCount = self:CountDefendersEngaged( DetectedItem )
    local DefenderGroups = self:CountDefendersToBeEngaged( DetectedItem, DefenderCount )

    self:F( { DefenderCount = DefenderCount } )

    -- Only allow ENGAGE when:
    -- 1. There are friendly units near the detected attackers.
    -- 2. There is sufficient fuel
    -- 3. There is sufficient ammo
    -- 4. The plane is not damaged
    if DefenderGroups and DetectedItem.IsDetected == true then
      return DefenderGroups
    end

    return nil
  end

  --- Creates an GCI task when there are targets for it.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item.
  -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units or nil if there are no targets to be set.
  -- @return #table Table of friendly groups.
  function AI_A2A_DISPATCHER:EvaluateGCI( DetectedItem )
    self:F( { DetectedItem.ItemID } )

    local AttackerSet = DetectedItem.Set
    local AttackerCount = AttackerSet:Count()

    -- First, count the active AIGroups Units, targeting the DetectedSet
    local DefenderCount = self:CountDefendersEngaged( DetectedItem )
    local DefendersMissing = AttackerCount - DefenderCount
    self:F( { AttackerCount = AttackerCount, DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } )

    local Friendlies = self:CountDefendersToBeEngaged( DetectedItem, DefenderCount )

    if DetectedItem.IsDetected == true then

      return DefendersMissing, Friendlies
    end

    return nil, nil
  end

  --- Assigns A2G AI Tasks in relation to the detected items.
  -- @param #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:Order( DetectedItem )

    local detection = self.Detection -- Functional.Detection#DETECTION_AREAS

    local ShortestDistance = 999999999

    -- Get coordinate (or nil).
    local AttackCoordinate = detection:GetDetectedItemCoordinate( DetectedItem )

    -- Issue https://github.com/FlightControl-Master/MOOSE/issues/1232
    if AttackCoordinate then

      for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do

        self:T( { DefenderSquadron = DefenderSquadron.Name } )

        local Airbase = DefenderSquadron.Airbase
        local AirbaseCoordinate = Airbase:GetCoordinate()

        local EvaluateDistance = AttackCoordinate:Get2DDistance( AirbaseCoordinate )

        if EvaluateDistance <= ShortestDistance then
          ShortestDistance = EvaluateDistance
        end
      end

    end

    return ShortestDistance
  end

  --- Shows the tactical display.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object.
  function AI_A2A_DISPATCHER:ShowTacticalDisplay( Detection )

    local AreaMsg = {}
    local TaskMsg = {}
    local ChangeMsg = {}

    local TaskReport = REPORT:New()

    local Report = REPORT:New( "Tactical Overview:" )

    local DefenderGroupCount = 0

    -- Now that all obsolete tasks are removed, loop through the detected targets.
    -- for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do
    for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b )
      return self:Order( t[a] ) < self:Order( t[b] )
    end ) do

      local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem
      local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT
      local DetectedCount = DetectedSet:Count()
      local DetectedZone = DetectedItem.Zone

      self:F( { "Target ID", DetectedItem.ItemID } )
      DetectedSet:Flush( self )

      local DetectedID = DetectedItem.ID
      local DetectionIndex = DetectedItem.Index
      local DetectedItemChanged = DetectedItem.Changed

      -- Show tactical situation
      Report:Add( string.format( "\n- Target %s (%s): (#%d) %s", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Set:GetObjectNames() ) )
      for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do
        local Defender = Defender -- Wrapper.Group#GROUP
        if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then
          if Defender and Defender:IsAlive() then
            DefenderGroupCount = DefenderGroupCount + 1
            local Fuel = Defender:GetFuelMin() * 100
            local Damage = Defender:GetLife() / Defender:GetLife0() * 100
            Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s",
                                       Defender:GetName(),
                                       Defender:GetSize(),
                                       Defender:GetInitialSize(),
                                       DefenderTask.Type,
                                       DefenderTask.Fsm:GetState(),
                                       Defender:GetSize(),
                                       Fuel,
                                       Damage,
                                       Defender:HasTask() == true and "Executing" or "Idle" ) )
          end
        end
      end
    end

    Report:Add( "\n- No Targets:" )
    local TaskCount = 0
    for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do
      TaskCount = TaskCount + 1
      local Defender = Defender -- Wrapper.Group#GROUP
      if not DefenderTask.Target then
        if Defender:IsAlive() then
          local DefenderHasTask = Defender:HasTask()
          local Fuel = Defender:GetFuelMin() * 100
          local Damage = Defender:GetLife() / Defender:GetLife0() * 100
          DefenderGroupCount = DefenderGroupCount + 1
          Report:Add( string.format( " - %s*%d/%d (%s - %s): (#%d) F: %3d, D:%3d - %s",
                                     Defender:GetName(),
                                     Defender:GetSize(),
                                     Defender:GetInitialSize(),
                                     DefenderTask.Type,
                                     DefenderTask.Fsm:GetState(),
                                     Defender:GetSize(),
                                     Fuel,
                                     Damage,
                                     Defender:HasTask() == true and "Executing" or "Idle" ) )
        end
      end
    end
    Report:Add( string.format( "\n- %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) )

    self:F( Report:Text( "\n" ) )
    trigger.action.outText( Report:Text( "\n" ), 25 )

    return true

  end

  --- Assigns A2A AI Tasks in relation to the detected items.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object.
  -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop.
  function AI_A2A_DISPATCHER:ProcessDetected( Detection )

    local AreaMsg = {}
    local TaskMsg = {}
    local ChangeMsg = {}

    local TaskReport = REPORT:New()

    for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do
      local AIGroup = AIGroup -- Wrapper.Group#GROUP
      if not AIGroup:IsAlive() then
        local DefenderTaskFsm = self:GetDefenderTaskFsm( AIGroup )
        self:F( { Defender = AIGroup:GetName(), DefenderState = DefenderTaskFsm:GetState() } )
        if not DefenderTaskFsm:Is( "Started" ) then
          self:ClearDefenderTask( AIGroup )
        end
      else
        if DefenderTask.Target then
          local AttackerItem = Detection:GetDetectedItemByIndex( DefenderTask.Target.Index )
          if not AttackerItem then
            self:F( { "Removing obsolete Target:", DefenderTask.Target.Index } )
            self:ClearDefenderTaskTarget( AIGroup )
          else
            if DefenderTask.Target.Set then
              local AttackerCount = DefenderTask.Target.Set:Count()
              if AttackerCount == 0 then
                self:F( { "All Targets destroyed in Target, removing:", DefenderTask.Target.Index } )
                self:ClearDefenderTaskTarget( AIGroup )
              end
            end
          end
        end
      end
    end

    local Report = REPORT:New( "Tactical Overviews" )

    local DefenderGroupCount = 0

    -- Now that all obsolete tasks are removed, loop through the detected targets.
    -- Closest detected targets to be considered first!
    -- for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do
    for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b )
      return self:Order( t[a] ) < self:Order( t[b] )
    end ) do

      local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem
      local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT
      local DetectedCount = DetectedSet:Count()
      local DetectedZone = DetectedItem.Zone

      self:F( { "Target ID", DetectedItem.ItemID } )
      DetectedSet:Flush( self )

      local DetectedID = DetectedItem.ID
      local DetectionIndex = DetectedItem.Index
      local DetectedItemChanged = DetectedItem.Changed

      do
        local Friendlies = self:EvaluateENGAGE( DetectedItem ) -- Returns a SetUnit if there are targets to be GCIed...
        if Friendlies then
          self:F( { AIGroups = Friendlies } )
          self:ENGAGE( DetectedItem, Friendlies )
        end
      end

      do
        local DefendersMissing, Friendlies = self:EvaluateGCI( DetectedItem )
        if DefendersMissing and DefendersMissing > 0 then
          self:F( { DefendersMissing = DefendersMissing } )
          self:GCI( DetectedItem, DefendersMissing, Friendlies )
        end
      end
    end

    if self.TacticalDisplay then
      self:ShowTacticalDisplay( Detection )
    end

    return true
  end

end

do

  --- Calculates which HUMAN friendlies are nearby the area.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item.
  -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type.
  function AI_A2A_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem )

    local DetectedSet = DetectedItem.Set
    local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem )

    local PlayerTypes = {}
    local PlayersCount = 0

    if PlayersNearBy then
      local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G()
      for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do
        local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT
        local PlayerName = PlayerUnit:GetPlayerName()
        -- self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } )
        if PlayerUnit:IsAirPlane() and PlayerName ~= nil then
          local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel()
          PlayersCount = PlayersCount + 1
          local PlayerType = PlayerUnit:GetTypeName()
          PlayerTypes[PlayerName] = PlayerType
          if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then
          end
        end
      end

    end

    -- self:F( { PlayersCount = PlayersCount } )

    local PlayerTypesReport = REPORT:New()

    if PlayersCount > 0 then
      for PlayerName, PlayerType in pairs( PlayerTypes ) do
        PlayerTypesReport:Add( string.format( '"%s" in %s', PlayerName, PlayerType ) )
      end
    else
      PlayerTypesReport:Add( "-" )
    end

    return PlayersCount, PlayerTypesReport
  end

  --- Calculates which friendlies are nearby the area.
  -- @param #AI_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item.
  -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type.
  function AI_A2A_DISPATCHER:GetFriendliesNearBy( DetectedItem )

    local DetectedSet = DetectedItem.Set
    local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem )

    local FriendlyTypes = {}
    local FriendliesCount = 0

    if FriendlyUnitsNearBy then
      local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G()
      for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do
        local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT
        if FriendlyUnit:IsAirPlane() then
          local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel()
          FriendliesCount = FriendliesCount + 1
          local FriendlyType = FriendlyUnit:GetTypeName()
          FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and (FriendlyTypes[FriendlyType] + 1) or 1
          if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then
          end
        end
      end

    end

    -- self:F( { FriendliesCount = FriendliesCount } )

    local FriendlyTypesReport = REPORT:New()

    if FriendliesCount > 0 then
      for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do
        FriendlyTypesReport:Add( string.format( "%d of %s", FriendlyTypeCount, FriendlyType ) )
      end
    else
      FriendlyTypesReport:Add( "-" )
    end

    return FriendliesCount, FriendlyTypesReport
  end

  --- Schedules a new CAP for the given SquadronName.
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  function AI_A2A_DISPATCHER:SchedulerCAP( SquadronName )
    self:CAP( SquadronName )
  end
  
  --- Add resources to a Squadron
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string Squadron The squadron name.
  -- @param #number Amount Number of resources to add.
  function AI_A2A_DISPATCHER:AddToSquadron(Squadron,Amount)
    local Squadron = self:GetSquadron(Squadron)
    if Squadron.ResourceCount then
      Squadron.ResourceCount = Squadron.ResourceCount + Amount
    end
    self:T({Squadron = Squadron.Name,SquadronResourceCount = Squadron.ResourceCount})
  end
  
  --- Remove resources from a Squadron
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string Squadron The squadron name.
  -- @param #number Amount Number of resources to remove.
  function AI_A2A_DISPATCHER:RemoveFromSquadron(Squadron,Amount)
    local Squadron = self:GetSquadron(Squadron)
    if Squadron.ResourceCount then
      Squadron.ResourceCount = Squadron.ResourceCount - Amount
    end
    self:T({Squadron = Squadron.Name,SquadronResourceCount = Squadron.ResourceCount})
  end

end

do

  -- @type AI_A2A_GCICAP
  -- @extends #AI_A2A_DISPATCHER

  --- Create an automatic air defence system for a coalition setting up GCI and CAP air defenses.
  -- The class derives from @{#AI_A2A_DISPATCHER} and thus, all the methods that are defined in the @{#AI_A2A_DISPATCHER} class, can be used also in AI\_A2A\_GCICAP.
  --
  -- ===
  --
  -- # Demo Missions
  --
  -- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2A_Dispatcher)
  --
  -- ===
  --
  -- # YouTube Channel
  --
  -- ### [DCS WORLD - MOOSE - A2A GCICAP - Build an automatic A2A Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx)
  --
  -- ===
  --
  -- ![Banner Image](..\Presentations\AI_A2A_DISPATCHER\Dia3.JPG)
  --
  -- AI\_A2A\_GCICAP includes automatic spawning of Combat Air Patrol aircraft (CAP) and Ground Controlled Intercept aircraft (GCI) in response to enemy
  -- air movements that are detected by an airborne or ground based radar network.
  --
  -- With a little time and with a little work it provides the mission designer with a convincing and completely automatic air defence system.
  --
  -- The AI_A2A_GCICAP provides a lightweight configuration method using the mission editor. Within a very short time, and with very little coding,
  -- the mission designer is able to configure a complete A2A defense system for a coalition using the DCS Mission Editor available functions.
  -- Using the DCS Mission Editor, you define borders of the coalition which are guarded by GCICAP,
  -- configure airbases to belong to the coalition, define squadrons flying certain types of planes or payloads per airbase, and define CAP zones.
  -- **Very little lua needs to be applied, a one liner**, which is fully explained below, which can be embedded
  -- right in a DO SCRIPT trigger action or in a larger DO SCRIPT FILE trigger action.
  --
  -- CAP flights will take off and proceed to designated CAP zones where they will remain on station until the ground radars direct them to intercept
  -- detected enemy aircraft or they run short of fuel and must return to base (RTB).
  --
  -- When a CAP flight leaves their zone to perform a GCI or return to base a new CAP flight will spawn to take its place.
  -- If all CAP flights are engaged or RTB then additional GCI interceptors will scramble to intercept unengaged enemy aircraft under ground radar control.
  --
  -- In short it is a plug in very flexible and configurable air defence module for DCS World.
  --
  -- ===
  --
  -- # The following actions need to be followed when using AI\_A2A\_GCICAP in your mission:
  --
  -- ## 1) Configure a working AI\_A2A\_GCICAP defense system for ONE coalition.
  --
  -- ### 1.1) Define which airbases are for which coalition.
  --
  -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_1.JPG)
  --
  -- Color the airbases red or blue. You can do this by selecting the airbase on the map, and select the coalition blue or red.
  --
  -- ### 1.2) Place groups of units given a name starting with a **EWR prefix** of your choice to build your EWR network.
  --
  -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_2.JPG)
  --
  -- **All EWR groups starting with the EWR prefix (text) will be included in the detection system.**
  --
  -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy.
  -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units.
  -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US).
  -- Additionally, ANY other radar capable unit can be part of the EWR network!
  -- Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar.
  -- The position of these units is very important as they need to provide enough coverage
  -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them.
  --
  -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates.
  -- For example if they are a long way forward and can detect enemy planes on the ground and taking off
  -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition.
  -- Having the radars further back will mean a slower escalation because fewer targets will be detected and
  -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map.
  -- It all depends on what the desired effect is.
  --
  -- EWR networks are **dynamically maintained**. By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network,
  -- increasing or decreasing the radar coverage of the Early Warning System.
  --
  -- ### 1.3) Place Airplane or Helicopter Groups with late activation switched on above the airbases to define Squadrons.
  --
  -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_3.JPG)
  --
  -- These are **templates**, with a given name starting with a **Template prefix** above each airbase that you wanna have a squadron.
  -- These **templates** need to be within 1.5km from the airbase center. They don't need to have a slot at the airplane, they can just be positioned above the airbase,
  -- without a route, and should only have ONE unit.
  --
  -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_4.JPG)
  --
  -- **All airplane or helicopter groups that are starting with any of the chosen Template Prefixes will result in a squadron created at the airbase.**
  --
  -- ### 1.4) Place floating helicopters to create the CAP zones defined by its route points.
  --
  -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_5.JPG)
  --
  -- **All airplane or helicopter groups that are starting with any of the chosen Template Prefixes will result in a squadron created at the airbase.**
  --
  -- The helicopter indicates the start of the CAP zone.
  -- The route points define the form of the CAP zone polygon.
  --
  -- ![Mission Editor Action](..\Presentations\AI_A2A_DISPATCHER\AI_A2A_GCICAP-ME_6.JPG)
  --
  -- **The place of the helicopter is important, as the airbase closest to the helicopter will be the airbase from where the CAP planes will take off for CAP.**
  --
  -- ## 2) There are a lot of defaults set, which can be further modified using the methods in @{#AI_A2A_DISPATCHER}:
  --
  -- ### 2.1) Planes are taking off in the air from the airbases.
  --
  -- This prevents airbases to get cluttered with airplanes taking off, it also reduces the risk of human players colliding with taxiing airplanes,
  -- resulting in the airbase to halt operations.
  --
  -- You can change the way how planes take off by using the inherited methods from AI\_A2A\_DISPATCHER:
  --
  --   * @{#AI_A2A_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield.
  --
  -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency.
  -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that:
  --
  --   * aircraft will stop waiting for each other or for a landing aircraft before takeoff.
  --   * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other.
  --   * aircraft may collide at the airbase.
  --   * aircraft may be awaiting the landing of a plane currently in the air, but never lands ...
  --
  -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs.
  -- If you experience while testing problems with aircraft take-off or landing, please use one of the above methods as a solution to workaround these issues!
  --
  -- ### 2.2) Planes return near the airbase or will land if damaged.
  --
  -- When damaged airplanes return to the airbase, they will be routed and will disappear in the air when they are near the airbase.
  -- There are exceptions to this rule, airplanes that aren't "listening" anymore due to damage or out of fuel, will return to the airbase and land.
  --
  -- You can change the way how planes land by using the inherited methods from AI\_A2A\_DISPATCHER:
  --
  --   * @{#AI_A2A_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway.
  --   * @{#AI_A2A_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines.
  --
  -- You can use these methods to minimize the airbase coordination overhead and to increase the airbase efficiency.
  -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the
  -- A2A defense system, as no new CAP or GCI planes can takeoff.
  -- Note that the method @{#AI_A2A_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft.
  -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control.
  --
  -- ### 2.3) CAP operations setup for specific airbases, will be executed with the following parameters:
  --
  --   * The altitude will range between 6000 and 10000 meters.
  --   * The CAP speed will vary between 500 and 800 km/h.
  --   * The engage speed between 800 and 1200 km/h.
  --
  -- You can change or add a CAP zone by using the inherited methods from AI\_A2A\_DISPATCHER:
  --
  -- The method @{#AI_A2A_DISPATCHER.SetSquadronCap}() defines a CAP execution for a squadron.
  --
  -- Setting-up a CAP zone also requires specific parameters:
  --
  --   * The minimum and maximum altitude
  --   * The minimum speed and maximum patrol speed
  --   * The minimum and maximum engage speed
  --   * The type of altitude measurement
  --
  -- These define how the squadron will perform the CAP while patrolling. Different terrain types requires different types of CAP.
  --
  -- The @{#AI_A2A_DISPATCHER.SetSquadronCapInterval}() method specifies **how much** and **when** CAP flights will takeoff.
  --
  -- It is recommended not to overload the air defense with CAP flights, as these will decrease the performance of the overall system.
  --
  -- For example, the following setup will create a CAP for squadron "Sochi":
  --
  --    A2ADispatcher:SetSquadronCap( "Sochi", CAPZoneWest, 4000, 8000, 600, 800, 800, 1200, "BARO" )
  --    A2ADispatcher:SetSquadronCapInterval( "Sochi", 2, 30, 120, 1 )
  --
  -- ### 2.4) Each airbase will perform GCI when required, with the following parameters:
  --
  --   * The engage speed is between 800 and 1200 km/h.
  --
  -- You can change or add a GCI parameters by using the inherited methods from AI\_A2A\_DISPATCHER:
  --
  -- The method @{#AI_A2A_DISPATCHER.SetSquadronGci}() defines a GCI execution for a squadron.
  --
  -- Setting-up a GCI readiness also requires specific parameters:
  --
  --   * The minimum speed and maximum patrol speed
  --
  -- Essentially this controls how many flights of GCI aircraft can be active at any time.
  -- Note allowing large numbers of active GCI flights can adversely impact mission performance on low or medium specification hosts/servers.
  -- GCI needs to be setup at strategic airbases. Too far will mean that the aircraft need to fly a long way to reach the intruders,
  -- too short will mean that the intruders may have already passed the ideal interception point!
  --
  -- For example, the following setup will create a GCI for squadron "Sochi":
  --
  --    A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 )
  --
  -- ### 2.5) Grouping or detected targets.
  --
  -- Detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate
  -- group being detected.
  --
  -- Targets will be grouped within a radius of 30km by default.
  --
  -- The radius indicates that detected targets need to be grouped within a radius of 30km.
  -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation.
  -- Fast planes like in the 80s, need a larger radius than WWII planes.
  -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft.
  --
  -- ## 3) Additional notes:
  --
  -- In order to create a two way A2A defense system, **two AI\_A2A\_GCICAP defense systems must need to be created**, for each coalition one.
  -- Each defense system needs its own EWR network setup, airplane templates and CAP configurations.
  --
  -- This is a good implementation, because maybe in the future, more coalitions may become available in DCS world.
  --
  -- ## 4) Coding examples how to use the AI\_A2A\_GCICAP class:
  --
  -- ### 4.1) An easy setup:
  --
  --      -- Setup the AI_A2A_GCICAP dispatcher for one coalition, and initialize it.
  --      GCI_Red = AI_A2A_GCICAP:New( "EWR CCCP", "SQUADRON CCCP", "CAP CCCP", 2 )
  --   --
  -- The following parameters were given to the :New method of AI_A2A_GCICAP, and mean the following:
  --
  --    * `"EWR CCCP"`: Groups of the blue coalition are placed that define the EWR network. These groups start with the name `EWR CCCP`.
  --    * `"SQUADRON CCCP"`: Late activated Groups objects of the red coalition are placed above the relevant airbases that will contain these templates in the squadron.
  --      These late activated Groups start with the name `SQUADRON CCCP`. Each Group object contains only one Unit, and defines the weapon payload, skin and skill level.
  --    * `"CAP CCCP"`: CAP Zones are defined using floating, late activated Helicopter Group objects, where the route points define the route of the polygon of the CAP Zone.
  --      These Helicopter Group objects start with the name `CAP CCCP`, and will be the locations wherein CAP will be performed.
  --    * `2` Defines how many CAP airplanes are patrolling in each CAP zone defined simultaneously.
  --
  -- ### 4.2) A more advanced setup:
  --
  --      -- Setup the AI_A2A_GCICAP dispatcher for the blue coalition.
  --
  --      A2A_GCICAP_Blue = AI_A2A_GCICAP:New( { "BLUE EWR" }, { "104th", "105th", "106th" }, { "104th CAP" }, 4 )
  --
  -- The following parameters for the :New method have the following meaning:
  --
  --    * `{ "BLUE EWR" }`: An array of the group name prefixes of the groups of the blue coalition are placed that define the EWR network. These groups start with the name `BLUE EWR`.
  --    * `{ "104th", "105th", "106th" } `: An array of the group name prefixes of the Late activated Groups objects of the blue coalition are
  --      placed above the relevant airbases that will contain these templates in the squadron.
  --      These late activated Groups start with the name `104th` or `105th` or `106th`.
  --    * `{ "104th CAP" }`: An array of the names of the CAP zones are defined using floating, late activated helicopter group objects,
  --      where the route points define the route of the polygon of the CAP Zone.
  --      These Helicopter Group objects start with the name `104th CAP`, and will be the locations wherein CAP will be performed.
  --    * `4` Defines how many CAP airplanes are patrolling in each CAP zone defined simultaneously.
  --
  -- @field #AI_A2A_GCICAP
  AI_A2A_GCICAP = {
    ClassName = "AI_A2A_GCICAP",
    Detection = nil,
  }

  --- AI_A2A_GCICAP constructor.
  -- @param #AI_A2A_GCICAP self
  -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network.
  -- @param #string TemplatePrefixes A list of template prefixes.
  -- @param #string CapPrefixes A list of CAP zone prefixes (polygon zones).
  -- @param #number CapLimit A number of how many CAP maximum will be spawned.
  -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area.
  -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter.
  -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task.
  -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI.
  -- @param #number ResourceCount The amount of resources that will be allocated to each squadron.
  -- @return #AI_A2A_GCICAP
  -- @usage
  --
  --   -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources.
  --   -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network.
  --   -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates.
  --   -- The CAP Zone prefix is "CAP Zone".
  --   -- The CAP Limit is 2.
  --   A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2 )
  --
  -- @usage
  --
  --   -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources.
  --   -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network.
  --   -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates.
  --   -- The CAP Zone prefix is "CAP Zone".
  --   -- The CAP Limit is 2.
  --   -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets.
  --   A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000 )
  --
  -- @usage
  --
  --   -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources.
  --   -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network.
  --   -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates.
  --   -- The CAP Zone prefix is "CAP Zone".
  --   -- The CAP Limit is 2.
  --   -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets.
  --   -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition,
  --   -- will be considered a defense task if the target is within 60km from the defender.
  --   A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000 )
  --
  -- @usage
  --
  --   -- Setup a new GCICAP dispatcher object. Each squadron has unlimited resources.
  --   -- The EWR network group prefix is DF CCCP. All groups starting with DF CCCP will be part of the EWR network.
  --   -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates.
  --   -- The CAP Zone prefix is "CAP Zone".
  --   -- The CAP Limit is 2.
  --   -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets.
  --   -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition,
  --   -- will be considered a defense task if the target is within 60km from the defender.
  --   -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement.
  --   A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000 )
  --
  -- @usage
  --
  --   -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources.
  --   -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network.
  --   -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates.
  --   -- The CAP Zone prefix is "CAP Zone".
  --   -- The CAP Limit is 2.
  --   -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets.
  --   -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition,
  --   -- will be considered a defense task if the target is within 60km from the defender.
  --   -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement.
  --   -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created.
  --
  --   A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, { "CAP Zone" }, 2, 20000, 60000, 150000, 30 )
  --
  -- @usage
  --
  --   -- Setup a new GCICAP dispatcher object. Each squadron has 30 resources.
  --   -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network.
  --   -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates.
  --   -- The CAP Zone prefix is nil. No CAP is created.
  --   -- The CAP Limit is nil.
  --   -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets.
  --   -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defender being assigned to a task.
  --   -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement.
  --   -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created.
  --
  --   A2ADispatcher = AI_A2A_GCICAP:New( { "DF CCCP" }, { "SQ CCCP" }, nil, nil, nil, nil, nil, 30 )
  --
  function AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount )

    local EWRSetGroup = SET_GROUP:New()
    EWRSetGroup:FilterPrefixes( EWRPrefixes )
    EWRSetGroup:FilterStart()

    local Detection = DETECTION_AREAS:New( EWRSetGroup, GroupingRadius or 30000 )

    local self = BASE:Inherit( self, AI_A2A_DISPATCHER:New( Detection ) ) -- #AI_A2A_GCICAP

    self:SetEngageRadius( EngageRadius )
    self:SetGciRadius( GciRadius )

    -- Determine the coalition of the EWRNetwork, this will be the coalition of the GCICAP.
    local EWRFirst = EWRSetGroup:GetFirst() -- Wrapper.Group#GROUP
    local EWRCoalition = EWRFirst:GetCoalition()

    -- Determine the airbases belonging to the coalition.
    local AirbaseNames = {} -- #list<#string>
    for AirbaseID, AirbaseData in pairs( _DATABASE.AIRBASES ) do
      local Airbase = AirbaseData -- Wrapper.Airbase#AIRBASE
      local AirbaseName = Airbase:GetName()
      if Airbase:GetCoalition() == EWRCoalition then
        table.insert( AirbaseNames, AirbaseName )
      end
    end

    self.Templates = SET_GROUP:New():FilterPrefixes( TemplatePrefixes ):FilterOnce()

    -- Setup squadrons

    self:T( { Airbases = AirbaseNames } )

    self:T( "Defining Templates for Airbases ..." )
    for AirbaseID, AirbaseName in pairs( AirbaseNames ) do
      local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE
      local AirbaseName = Airbase:GetName()
      local AirbaseCoord = Airbase:GetCoordinate()
      local AirbaseZone = ZONE_RADIUS:New( "Airbase", AirbaseCoord:GetVec2(), 3000 )
      local Templates = nil
      self:T( { Airbase = AirbaseName } )
      for TemplateID, Template in pairs( self.Templates:GetSet() ) do
        local Template = Template -- Wrapper.Group#GROUP
        local TemplateCoord = Template:GetCoordinate()
        if AirbaseZone:IsVec2InZone( TemplateCoord:GetVec2() ) then
          Templates = Templates or {}
          table.insert( Templates, Template:GetName() )
          self:T( { Template = Template:GetName() } )
        end
      end
      if Templates then
        self:SetSquadron( AirbaseName, AirbaseName, Templates, ResourceCount )
      end
    end

    -- Setup CAP.
    -- Find for each CAP the nearest airbase to the (start or center) of the zone.
    -- CAP will be launched from there.

    self.CAPTemplates = SET_GROUP:New()
    self.CAPTemplates:FilterPrefixes( CapPrefixes )
    self.CAPTemplates:FilterOnce()

    self:T( "Setting up CAP ..." )
    for CAPID, CAPTemplate in pairs( self.CAPTemplates:GetSet() ) do
      local CAPZone = ZONE_POLYGON:New( CAPTemplate:GetName(), CAPTemplate )
      -- Now find the closest airbase from the ZONE (start or center)
      local AirbaseDistance = 99999999
      local AirbaseClosest = nil -- Wrapper.Airbase#AIRBASE
      self:T( { CAPZoneGroup = CAPID } )
      for AirbaseID, AirbaseName in pairs( AirbaseNames ) do
        local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE
        local AirbaseName = Airbase:GetName()
        local AirbaseCoord = Airbase:GetCoordinate()
        local Squadron = self.DefenderSquadrons[AirbaseName]
        if Squadron then
          local Distance = AirbaseCoord:Get2DDistance( CAPZone:GetCoordinate() )
          self:T( { AirbaseDistance = Distance } )
          if Distance < AirbaseDistance then
            AirbaseDistance = Distance
            AirbaseClosest = Airbase
          end
        end
      end
      if AirbaseClosest then
        self:T( { CAPAirbase = AirbaseClosest:GetName() } )
        self:SetSquadronCap( AirbaseClosest:GetName(), CAPZone, 6000, 10000, 500, 800, 800, 1200, "RADIO" )
        self:SetSquadronCapInterval( AirbaseClosest:GetName(), CapLimit, 300, 600, 1 )
      end
    end

    -- Setup GCI.
    -- GCI is setup for all Squadrons.
    self:T( "Setting up GCI ..." )
    for AirbaseID, AirbaseName in pairs( AirbaseNames ) do
      local Airbase = _DATABASE:FindAirbase( AirbaseName ) -- Wrapper.Airbase#AIRBASE
      local AirbaseName = Airbase:GetName()
      local Squadron = self.DefenderSquadrons[AirbaseName]
      self:F( { Airbase = AirbaseName } )
      if Squadron then
        self:T( { GCIAirbase = AirbaseName } )
        self:SetSquadronGci( AirbaseName, 800, 1200 )
      end
    end

    self:__Start( 5 )

    self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead )
    self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead )
    -- self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead )

    self:HandleEvent( EVENTS.Land )
    self:HandleEvent( EVENTS.EngineShutdown )

    return self
  end

  --- AI_A2A_GCICAP constructor with border.
  -- @param #AI_A2A_GCICAP self
  -- @param #string EWRPrefixes A list of prefixes that of groups that setup the Early Warning Radar network.
  -- @param #string TemplatePrefixes A list of template prefixes.
  -- @param #string BorderPrefix A Border Zone Prefix.
  -- @param #string CapPrefixes A list of CAP zone prefixes (polygon zones).
  -- @param #number CapLimit A number of how many CAP maximum will be spawned.
  -- @param #number GroupingRadius The radius in meters wherein detected planes are being grouped as one target area.
  -- For airplanes, 6000 (6km) is recommended, and is also the default value of this parameter.
  -- @param #number EngageRadius The radius in meters wherein detected airplanes will be engaged by airborne defenders without a task.
  -- @param #number GciRadius The radius in meters wherein detected airplanes will GCI.
  -- @param #number ResourceCount The amount of resources that will be allocated to each squadron.
  -- @return #AI_A2A_GCICAP
  -- @usage
  --
  --   -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources.
  --   -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network.
  --   -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates.
  --   -- The CAP Zone prefix is "CAP Zone".
  --   -- The CAP Limit is 2.
  --
  --   A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2 )
  --
  -- @usage
  --
  --   -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources.
  --   -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network.
  --   -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates.
  --   -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border.
  --   -- The CAP Zone prefix is "CAP Zone".
  --   -- The CAP Limit is 2.
  --   -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets.
  --
  --   A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000 )
  --
  -- @usage
  --
  --   -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources.
  --   -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network.
  --   -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates.
  --   -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border.
  --   -- The CAP Zone prefix is "CAP Zone".
  --   -- The CAP Limit is 2.
  --   -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets.
  --   -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition,
  --   -- will be considered a defense task if the target is within 60km from the defender.
  --
  --   A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000 )
  --
  -- @usage
  --
  --   -- Setup a new GCICAP dispatcher object with a border. Each squadron has unlimited resources.
  --   -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network.
  --   -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates.
  --   -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border.
  --   -- The CAP Zone prefix is "CAP Zone".
  --   -- The CAP Limit is 2.
  --   -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets.
  --   -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition,
  --   -- will be considered a defense task if the target is within 60km from the defender.
  --   -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement.
  --
  --   A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000 )
  --
  -- @usage
  --
  --   -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources.
  --   -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network.
  --   -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates.
  --   -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border.
  --   -- The CAP Zone prefix is "CAP Zone".
  --   -- The CAP Limit is 2.
  --   -- The Grouping Radius is set to 20000. Thus all planes within a 20km radius will be grouped as a group of targets.
  --   -- The Engage Radius is set to 60000. Any defender without a task, and in healthy condition,
  --   -- will be considered a defense task if the target is within 60km from the defender.
  --   -- The GCI Radius is set to 150000. Any target detected within 150km will be considered for GCI engagement.
  --   -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created.
  --
  --   A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", { "CAP Zone" }, 2, 20000, 60000, 150000, 30 )
  --
  -- @usage
  --
  --   -- Setup a new GCICAP dispatcher object with a border. Each squadron has 30 resources.
  --   -- The EWR network group prefix is "DF CCCP". All groups starting with "DF CCCP" will be part of the EWR network.
  --   -- The Squadron Templates prefix is "SQ CCCP". All groups starting with "SQ CCCP" will be considered as airplane templates.
  --   -- The Border prefix is "Border". This will setup a border using the group defined within the mission editor with the name Border.
  --   -- The CAP Zone prefix is nil. No CAP is created.
  --   -- The CAP Limit is nil.
  --   -- The Grouping Radius is nil. The default range of 6km radius will be grouped as a group of targets.
  --   -- The Engage Radius is set nil. The default Engage Radius will be used to consider a defender being assigned to a task.
  --   -- The GCI Radius is nil. Any target detected within the default GCI Radius will be considered for GCI engagement.
  --   -- The amount of resources for each squadron is set to 30. Thus about 30 resources are allocated to each squadron created.
  --
  --   A2ADispatcher = AI_A2A_GCICAP:NewWithBorder( { "DF CCCP" }, { "SQ CCCP" }, "Border", nil, nil, nil, nil, nil, 30 )
  --
  function AI_A2A_GCICAP:NewWithBorder( EWRPrefixes, TemplatePrefixes, BorderPrefix, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount )

    local self = AI_A2A_GCICAP:New( EWRPrefixes, TemplatePrefixes, CapPrefixes, CapLimit, GroupingRadius, EngageRadius, GciRadius, ResourceCount )

    if BorderPrefix then
      self:SetBorderZone( ZONE_POLYGON:New( BorderPrefix, GROUP:FindByName( BorderPrefix ) ) )
    end

    return self

  end
  
end
--- **AI** - Models the process of air to ground BAI engagement for airplanes and helicopters.
--
-- This is a class used in the @{AI.AI_A2G_Dispatcher}.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_A2G_BAI
-- @image AI_Air_To_Ground_Engage.JPG

-- @type AI_A2G_BAI
-- @extends AI.AI_A2A_Engage#AI_A2A_Engage -- TODO: Documentation. This class does not exist, unable to determine what it extends.

--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_A2G_BAI
AI_A2G_BAI = {
  ClassName = "AI_A2G_BAI",
}

--- Creates a new AI_A2G_BAI object
-- @param #AI_A2G_BAI self
-- @param Wrapper.Group#GROUP AIGroup
-- @param DCS#Speed  EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Speed  EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement.
-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement.
-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO".
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO
-- @return #AI_A2G_BAI
function AI_A2G_BAI:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )

  local AI_Air = AI_AIR:New( AIGroup )
  local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL
  local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )
  local self = BASE:Inherit( self, AI_Air_Engage )

  return self
end

--- Creates a new AI_A2G_BAI object
-- @param #AI_A2G_BAI self
-- @param Wrapper.Group#GROUP AIGroup
-- @param DCS#Speed  EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Speed  EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement.
-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement.
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO
-- @return #AI_A2G_BAI
function AI_A2G_BAI:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )

  return self:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType)
end

--- Evaluate the attack and create an AttackUnitTask list. 
-- @param #AI_A2G_BAI self
-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack.
-- @param Wrapper.Group#GROUP DefenderGroup The group of defenders.
-- @param #number EngageAltitude The altitude to engage the targets.
-- @return #AI_A2G_BAI self
function AI_A2G_BAI:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude )

  local AttackUnitTasks = {}

  local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 )
  for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do
    if AttackUnit then
      if AttackUnit:IsAlive() and AttackUnit:IsGround() then
        self:T( { "BAI Unit:", AttackUnit:GetName() } )
        AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude )
      end
    end
  end

  return AttackUnitTasks
end
--- **AI** - Models the process of air to ground engagement for airplanes and helicopters.
--
-- This is a class used in the @{AI.AI_A2G_Dispatcher}.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_A2G_CAS
-- @image AI_Air_To_Ground_Engage.JPG

-- @type AI_A2G_CAS
-- @extends AI.AI_A2G_Patrol#AI_AIR_PATROL TODO: Documentation. This class does not exist, unable to determine what it extends.

--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders.
--
-- # Developer Note
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_A2G_CAS
AI_A2G_CAS = {
  ClassName = "AI_A2G_CAS",
}

--- Creates a new AI_A2G_CAS object
-- @param #AI_A2G_CAS self
-- @param Wrapper.Group#GROUP AIGroup
-- @param DCS#Speed  EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Speed  EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement.
-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement.
-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO".
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO
-- @return #AI_A2G_CAS
function AI_A2G_CAS:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )

  local AI_Air = AI_AIR:New( AIGroup )
  local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) -- #AI_AIR_PATROL
  local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )
  local self = BASE:Inherit( self, AI_Air_Engage )

  return self
end

--- Creates a new AI_A2G_CAS object
-- @param #AI_A2G_CAS self
-- @param Wrapper.Group#GROUP AIGroup
-- @param DCS#Speed  EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Speed  EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement.
-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement.
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO
-- @return #AI_A2G_CAS
function AI_A2G_CAS:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )

  return self:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType)
end

--- Evaluate the attack and create an AttackUnitTask list. 
-- @param #AI_A2G_CAS self
-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack.
-- @param Wrapper.Group#GROUP DefenderGroup The group of defenders.
-- @param #number EngageAltitude The altitude to engage the targets.
-- @return #AI_A2G_CAS self
function AI_A2G_CAS:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude )

  local AttackUnitTasks = {}
  
  local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 )
  for AttackUnitIndex, AttackUnit in ipairs( AttackSetUnitPerThreatLevel or {} ) do
    if AttackUnit then
      if AttackUnit:IsAlive() and AttackUnit:IsGround() then
        self:T( { "CAS Unit:", AttackUnit:GetName() } )
        AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude )
      end
    end
  end

  return AttackUnitTasks
end
--- **AI** - Models the process of air to ground SEAD engagement for airplanes and helicopters.
--
-- This is a class used in the @{AI.AI_A2G_Dispatcher}.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_A2G_SEAD
-- @image AI_Air_To_Ground_Engage.JPG



-- @type AI_A2G_SEAD
-- @extends AI.AI_A2G_Patrol#AI_AIR_PATROL


--- Implements the core functions to SEAD intruders. Use the Engage trigger to intercept intruders.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- The AI_A2G_SEAD is assigned a @{Wrapper.Group} and this must be done before the AI_A2G_SEAD process can be started using the **Start** event.
-- 
-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits.
-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits.
-- 
-- This cycle will continue.
-- 
-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event.
-- 
-- When enemies are detected, the AI will automatically engage the enemy.
-- 
-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB.
-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land.
-- 
-- ## 1. AI_A2G_SEAD constructor
--   
--   * @{#AI_A2G_SEAD.New}(): Creates a new AI_A2G_SEAD object.
--
-- ## 3. Set the Range of Engagement
-- 
-- An optional range can be set in meters, 
-- that will define when the AI will engage with the detected airborne enemy targets.
-- The range can be beyond or smaller than the range of the Patrol Zone.
-- The range is applied at the position of the AI.
-- Use the method @{#AI_AIR_PATROL.SetEngageRange}() to define that range.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_A2G_SEAD
AI_A2G_SEAD = {
  ClassName = "AI_A2G_SEAD",
}

--- Creates a new AI_A2G_SEAD object
-- @param #AI_A2G_SEAD self
-- @param Wrapper.Group#GROUP AIGroup
-- @param DCS#Speed  EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Speed  EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement.
-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement.
-- @param DCS#AltitudeType EngageAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to "RADIO".
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO
-- @return #AI_A2G_SEAD
function AI_A2G_SEAD:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )

  local AI_Air = AI_AIR:New( AIGroup )
  local AI_Air_Patrol = AI_AIR_PATROL:New( AI_Air, AIGroup, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )
  local AI_Air_Engage = AI_AIR_ENGAGE:New( AI_Air_Patrol, AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )
  local self = BASE:Inherit( self, AI_Air_Engage )

  return self
end


--- Creates a new AI_A2G_SEAD object
-- @param #AI_A2G_SEAD self
-- @param Wrapper.Group#GROUP AIGroup
-- @param DCS#Speed  EngageMinSpeed The minimum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Speed  EngageMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h when engaging a target.
-- @param DCS#Altitude EngageFloorAltitude The lowest altitude in meters where to execute the engagement.
-- @param DCS#Altitude EngageCeilingAltitude The highest altitude in meters where to execute the engagement.
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Group} in km/h.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO
-- @return #AI_A2G_SEAD
function AI_A2G_SEAD:New( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )

  return self:New2( AIGroup, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, PatrolAltType, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )
end


--- Evaluate the attack and create an AttackUnitTask list. 
-- @param #AI_A2G_SEAD self
-- @param Core.Set#SET_UNIT AttackSetUnit The set of units to attack.
-- @param Wrapper.Group#GROUP DefenderGroup The group of defenders.
-- @param #number EngageAltitude The altitude to engage the targets.
-- @return #AI_A2G_SEAD self
function AI_A2G_SEAD:CreateAttackUnitTasks( AttackSetUnit, DefenderGroup, EngageAltitude )

  local AttackUnitTasks = {}
  
  local AttackSetUnitPerThreatLevel = AttackSetUnit:GetSetPerThreatLevel( 10, 0 )
  for AttackUnitID, AttackUnit in ipairs( AttackSetUnitPerThreatLevel ) do
    if AttackUnit then
      if AttackUnit:IsAlive() and AttackUnit:IsGround() then
        local HasRadar = AttackUnit:HasSEAD() 
        if HasRadar then
          self:F( { "SEAD Unit:", AttackUnit:GetName() } )
          AttackUnitTasks[#AttackUnitTasks+1] = DefenderGroup:TaskAttackUnit( AttackUnit, true, false, nil, nil, EngageAltitude )
        end
      end
    end
  end
  
  return AttackUnitTasks
end

--- **AI** - Create an automated A2G defense system with reconnaissance units, coordinating SEAD, BAI and CAS operations.
-- 
-- ===
-- 
-- Features:
-- 
--    * Setup quickly an A2G defense system for a coalition.
--    * Setup multiple defense zones to defend specific coordinates in your battlefield.
--    * Setup (SEAD) Suppression of Air Defense squadrons, to gain control in the air of enemy grounds.
--    * Setup (BAI) Battleground Air Interdiction squadrons to attack remote enemy ground units and targets.
--    * Setup (CAS) Controlled Air Support squadrons, to attack close by enemy ground units near friendly installations.
--    * Define and use a detection network controlled by recce.
--    * Define A2G defense squadrons at airbases, FARPs and carriers.
--    * Enable airbases for A2G defenses.
--    * Add different planes and helicopter templates to squadrons.
--    * Assign squadrons to execute a specific engagement type depending on threat level of the detected ground enemy unit composition.
--    * Add multiple squadrons to different airbases, FARPs or carriers.
--    * Define different ranges to engage upon.
--    * Establish an automatic in air refuel process for planes using refuel tankers.
--    * Setup default settings for all squadrons and A2G defenses.
--    * Setup specific settings for specific squadrons.
-- 
-- ===
-- 
-- ## Missions:
-- 
-- [AID-A2G - AI A2G Dispatching](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_A2G_Dispatcher)
-- 
-- ===
-- 
-- ## YouTube Channel:
-- 
-- [DCS WORLD - MOOSE - A2G DISPATCHER - Build an automatic A2G Defense System - Introduction](https://www.youtube.com/watch?v=zwSxWRAGVH8)
-- 
-- ===
-- 
-- # QUICK START GUIDE
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- The following class is available to model an A2G defense system.
-- 
-- AI_A2G_DISPATCHER is the main A2G defense class that models the A2G defense system.
-- 
-- Before you start using the AI_A2G_DISPATCHER, ask yourself the following questions:
-- 
-- 
-- ## 1. Which coalition am I modeling an A2G defense system for? Blue or red?
-- 
-- One AI_A2G_DISPATCHER object can create a defense system for **one coalition**, which is blue or red.
-- If you want to create a **mutual defense system**, for both blue and red, then you need to create **two** AI_A2G_DISPATCHER **objects**,
-- each governing their defense system for one coalition.
-- 
--      
-- ## 2. Which type of detection will I setup? Grouping based per AREA, per TYPE or per UNIT? (Later others will follow).
-- 
-- The MOOSE framework leverages the @{Functional.Detection} classes to perform the reconnaissance, detecting enemy units
-- and reporting them to the head quarters.
-- Several types of @{Functional.Detection} classes exist, and the most common characteristics of these classes is that they:
-- 
--    * Perform detections from multiple recce as one co-operating entity.
--    * Communicate with a @{Tasking.CommandCenter}, which consolidates each detection.
--    * Groups detections based on a method (per area, per type or per unit).
--    * Communicates detections.
-- 
-- 
-- ## 3. Which recce units can be used as part of the detection system? Only ground based, or also airborne?
-- 
-- Depending on the type of mission you want to achieve, different types of units can be engaged to perform ground enemy targets reconnaissance.
-- Ground recce (FAC) are very useful units to determine the position of enemy ground targets when they spread out over the battlefield at strategic positions.
-- Using their varying detection technology, and especially those ground units which have spotting technology, can be extremely effective at
-- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective.
-- Unfortunately, they lack sometimes the visibility to detect targets at greater range, or when scenery is preventing line of sight.
-- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then
-- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully!
-- 
-- Airborne recce (AFAC) are also very effective. The are capable of patrolling at a functional detection altitude,
-- having an overview of the whole battlefield. However, airborne recce can be vulnerable to air to ground attacks,
-- so you need air superiority to make them effective. 
-- Airborne recce will also have varying ground detection technology, which plays a big role in the effectiveness of the reconnaissance.
-- Certain helicopter or plane types have ground searching radars or advanced ground scanning technology, and are very effective
-- compared to air units having only visual detection capabilities.
-- For example, for the red coalition, the Mi-28N and the Su-34; and for the blue side, the reaper, are such effective airborne recce units.
-- 
-- Typically, don't want these recce units to engage with the enemy, you want to keep them at position. Therefore, it is a good practice
-- to set the ROE for these recce to hold weapons, and make them invisible from the enemy.
-- 
-- It is not possible to perform a recce function as a player (unit).
-- 
-- 
-- ## 4. How do the defenses decide **when and where to engage** on approaching enemy units?
-- 
-- The A2G dispatcher needs you to setup (various) defense coordinates, which are strategic positions in the battle field to be defended.
-- Any ground based enemy approaching within the proximity of such a defense point, may trigger for a defensive action by friendly air units.
-- 
-- There are 2 important parameters that play a role in the defensive decision making: defensiveness and reactivity.
-- 
-- The A2G dispatcher provides various parameters to setup the **defensiveness**, 
-- which models the decision **when** a defender will engage with the approaching enemy.
-- Defensiveness is calculated by a probability distribution model when to trigger a defense action, 
-- depending on the distance of the enemy unit from the defense coordinates, and a **defensiveness factor**. 
-- 
-- The other parameter considered for defensive action is **where the enemy is located**, thus the distance from a defense coordinate, 
-- which we call the **reactive distance**. By default, the reactive distance is set to 60km, but can be changed by the mission designer
-- using the available method explained further below.
-- The combination of the defensiveness and reactivity results in a model that, the closer the attacker is to the defense point, 
-- the higher the probability will be that a defense action will be launched!
-- 
-- 
-- ## 5. Are defense coordinates and defense reactivity the only parameters?
-- 
-- No, depending on the target type, and the threat level of the target, the probability of defense will be higher.
-- In other words, when a SAM-10 radar emitter is detected, its probability for defense will be much higher than when a BMP-1 vehicle is
-- detected, even when both enemies are at the same distance from a defense coordinate.
-- This will ensure optimal defenses, SEAD tasks will be launched much more quicker against engaging radar emitters, to ensure air superiority.
-- Approaching main battle tanks will be engaged much faster, than a group of approaching trucks.
-- 
-- 
-- ## 6. Which Squadrons will I create and which name will I give each Squadron?
-- 
-- The A2G defense system works with **Squadrons**. Each Squadron must be given a unique name, that forms the **key** to the squadron.
-- Several options and activities can be set per Squadron. A free format name can be given, but always ensure that the name is meaningful
-- for your mission, and remember that squadron names are used for communication to the players of your mission.
-- 
-- There are mainly 3 types of defenses: **SEAD**, **BAI**, and **CAS**.
-- 
-- Suppression of Air Defenses (SEAD) are effective against radar emitters.
-- Battleground Air Interdiction (BAI) tasks are launched when there are no friendlies around.
-- Close Air Support (CAS) is launched when the enemy is close near friendly units.
-- 
-- Depending on the defense type, different payloads will be needed. See further points on squadron definition.
-- 
-- 
-- ## 7. Where will the Squadrons be located? On Airbases? On Carriers? On FARPs?
-- 
-- Squadrons are placed at the **home base** on an **airfield**, **carrier** or **FARP**.
-- Carefully plan where each Squadron will be located as part of the defense system required for mission effective defenses.
-- If the home base of the squadron is too far from assumed enemy positions, then the defenses will be too late.
-- The home bases must be **behind** enemy lines, you want to prevent your home bases to be engaged by enemies!
-- Depending on the units applied for defenses, the home base can be further or closer to the enemies.
-- Any airbase, FARP, or carrier can act as the launching platform for A2G defenses.
-- Carefully plan which airbases will take part in the coalition. Color each airbase **in the color of the coalition**, using the mission editor,
-- or your air units will not return for landing at the airbase!
-- 
-- 
-- ## 8. Which helicopter or plane models will I assign for each Squadron? Do I need one plane model or more plane models per squadron?
-- 
-- Per Squadron, one or multiple helicopter or plane models can be allocated as **Templates**.
-- These are late activated groups with one airplane or helicopter that start with a specific name, called the **template prefix**.
-- The A2G defense system will select from the given templates a random template to spawn a new plane (group).
-- 
-- A squadron will perform specific task types (SEAD, BAI or CAS). So, squadrons will require specific templates for the
-- task types it will perform. A squadron executing SEAD defenses, will require a payload with long range anti-radar seeking missiles.
--  
--  
-- ## 9. Which payloads, skills and skins will these plane models have?
-- 
-- Per Squadron, even if you have one plane model, you can still allocate multiple templates of one plane model, 
-- each having different payloads, skills and skins. 
-- The A2G defense system will select from the given templates a random template to spawn a new plane (group).
-- 
-- 
-- ## 10. How do squadrons engage in a defensive action?
-- 
-- There are two ways how squadrons engage and execute your A2G defenses. 
-- Squadrons can start the defense directly from the airbase, FARP or carrier. When a squadron launches a defensive group, that group
-- will start directly from the airbase. The other way is to launch early on in the mission a patrolling mechanism.
-- Squadrons will launch air units to patrol in specific zone(s), so that when ground enemy targets are detected, that the airborne
-- A2G defenses can come immediately into action.
-- 
-- 
-- ## 11. For each Squadron doing a patrol, which zone types will I create?
-- 
-- Per zone, evaluate whether you want:
-- 
--    * simple trigger zones
--    * polygon zones
--    * moving zones
-- 
-- Depending on the type of zone selected, a different @{Core.Zone} object needs to be created from a ZONE_ class.
-- 
-- 
-- ## 12. Are moving defense coordinates possible?
-- 
-- Yes, different COORDINATE types are possible to be used.
-- The COORDINATE_UNIT will help you to specify a defense coordinate that is attached to a moving unit.
-- 
-- 
-- ## 13. How many defense coordinates do I need to create?
-- 
-- It depends, but the idea is to define only the necessary defense points that drive your mission.
-- If you define too many defense coordinates, the performance of your mission may decrease. For each defined defense coordinate,
-- all the possible enemies are evaluated. Note that each defense coordinate has a reach depending on the size of the associated defense radius.
-- The default defense radius is about 60km. Depending on the defense reactivity, defenses will be launched when the enemy is at a
-- closer distance from the defense coordinate than the defense radius.
-- 
-- 
-- ## 14. For each Squadron doing patrols, what are the time intervals and patrol amounts to be performed?
-- 
-- For each patrol:
-- 
--    * **How many** patrols you want to have airborne at the same time?
--    * **How frequent** you want the defense mechanism to check whether to start a new patrol?
-- 
-- Other considerations:
-- 
--    * **How far** is the patrol area from the engagement "hot zone". You want to ensure that the enemy is reached on time!
--    * **How safe** is the patrol area taking into account air superiority. Is it well defended, are there nearby A2A bases?
-- 
-- 
-- ## 15. For each Squadron, which takeoff method will I use?
-- 
-- For each Squadron, evaluate which takeoff method will be used:
-- 
--    * Straight from the air
--    * From the runway
--    * From a parking spot with running engines
--    * From a parking spot with cold engines
-- 
-- **The default takeoff method is straight in the air.**
-- This takeoff method is the most useful if you want to avoid airplane clutter at airbases, but it is the least realistic one.
-- 
-- 
-- ## 16. For each Squadron, which landing method will I use?
-- 
-- For each Squadron, evaluate which landing method will be used:
-- 
--    * Despawn near the airbase when returning
--    * Despawn after landing on the runway
--    * Despawn after engine shutdown after landing
--    
-- **The default landing method is to despawn when near the airbase when returning.**
-- This landing method is the most useful if you want to avoid aircraft clutter at airbases, but it is the least realistic one.
-- 
-- 
-- ## 19. For each Squadron, which **defense overhead** will I use?
-- 
-- For each Squadron, depending on the helicopter or airplane type (modern, old) and payload, which overhead is required to provide any defense?
-- 
-- In other words, if **X** enemy ground units are detected, how many **Y** defense helicopters or airplanes need to engage (per squadron)?
-- The **Y** is dependent on the type of aircraft (era), payload, fuel levels, skills etc.
-- But the most important factor is the payload, which is the amount of A2G weapons the defense can carry to attack the enemy ground units.
-- For example, a Ka-50 can carry 16 Vikhrs, this means that it potentially can destroy at least 8 ground units without a reload of ammunition.
-- That means, that one defender can destroy more enemy ground units.
-- Thus, the overhead is a **factor** that will calculate dynamically how many **Y** defenses will be required based on **X** attackers detected.
-- 
-- **The default overhead is 1. A smaller value than 1, like 0.25 will decrease the overhead to a 1 / 4 ratio, meaning, 
-- one defender for each 4 detected ground enemy units. **
-- 
-- 
-- ## 19. For each Squadron, which grouping will I use?
-- 
-- When multiple targets are detected, how will defenses be grouped when multiple defense air units are spawned for multiple enemy ground units?
-- Per one, two, three, four?
-- 
-- **The default grouping is 1. That means, that each spawned defender will act individually.**
-- But you can specify a number between 1 and 4, so that the defenders will act as a group.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- ### Author: **FlightControl** rework of GCICAP + introduction of new concepts (squadrons).
-- 
-- @module AI.AI_A2G_Dispatcher
-- @image AI_Air_To_Ground_Dispatching.JPG


do -- AI_A2G_DISPATCHER

  --- AI_A2G_DISPATCHER class.
  -- @type AI_A2G_DISPATCHER
  -- @extends Tasking.DetectionManager#DETECTION_MANAGER

  --- Create an automated A2G defense system based on a detection network of reconnaissance vehicles and air units, coordinating SEAD, BAI and CAS operations.
  -- 
  -- ===
  -- 
  -- When your mission is in the need to take control of the AI to automate and setup a process of air to ground defenses, this is the module you need.
  -- The defense system work through the definition of defense coordinates, which are points in your friendly area within the battle field, that your mission need to have defended.
  -- Multiple defense coordinates can be setup. Defense coordinates can be strategic or tactical positions or references to strategic units or scenery.
  -- The A2G dispatcher will evaluate every x seconds the tactical situation around each defense coordinate. When a defense coordinate
  -- is under threat, it will communicate through the command center that defensive actions need to be taken and will launch groups of air units for defense.
  -- The level of threat to the defense coordinate varies upon the strength and types of the enemy units, the distance to the defense point, and the defensiveness parameters.
  -- Defensive actions are taken through probability, but the closer and the more threat the enemy poses to the defense coordinate, the faster it will be attacked by friendly A2G units.
  -- 
  -- Please study carefully the underlying explanations how to setup and use this module, as it has many features.
  -- It also requires a little study to ensure that you get a good understanding of the defense mechanisms, to ensure a strong
  -- defense for your missions.
  -- 
  -- ===
  -- 
  -- # USAGE GUIDE
  -- 
  -- 
  -- ## 1. AI\_A2G\_DISPATCHER constructor:
  -- 
  -- 
  -- The @{#AI_A2G_DISPATCHER.New}() method creates a new AI_A2G_DISPATCHER instance.
  -- 
  -- 
  -- ### 1.1. Define the **reconnaissance network**:
  -- 
  -- As part of the AI_A2G_DISPATCHER :New() constructor, a reconnaissance network must be given as the first parameter.
  -- A reconnaissance network is provided by passing a @{Functional.Detection} object.
  -- The most effective reconnaissance for the A2G dispatcher would be to use the @{Functional.Detection#DETECTION_AREAS} object.
  -- 
  -- A reconnaissance network, is used to detect enemy ground targets, 
  -- potentially group them into areas, and to understand the position, level of threat of the enemy.
  -- 
  -- As explained in the introduction, depending on the type of mission you want to achieve, different types of units can be applied to detect ground enemy targets.
  -- Ground based units are very useful to act as a reconnaissance, but they lack sometimes the visibility to detect targets at greater range.
  -- Recce are very useful to acquire the position of enemy ground targets when spread out over the battlefield at strategic positions.
  -- Ground units also have varying detectors, and especially the ground units which have laser guiding missiles can be extremely effective at
  -- detecting targets at great range. The terrain elevation characteristics are a big tool in making ground recce to be more effective.
  -- If you succeed to position recce at higher level terrain providing a broad and far overview of the lower terrain in the distance, then
  -- the recce will be very effective at detecting approaching enemy targets. Therefore, always use the terrain very carefully!
  -- 
  -- Beside ground level units to use for reconnaissance, air units are also very effective. The are capable of patrolling at great speed
  -- covering a large terrain. However, airborne recce can be vulnerable to air to ground attacks, and you need air superiority to make then
  -- effective. Also the instruments available at the air units play a big role in the effectiveness of the reconnaissance.
  -- Air units which have ground detection capabilities will be much more effective than air units with only visual detection capabilities.
  -- For the red coalition, the Mi-28N and for the blue side, the reaper are such effective reconnaissance airborne units.
  -- 
  -- Reconnaissance networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection} instance that is given as the first parameter to the A2G dispatcher.
  -- By defining in a **smart way the names or name prefixes of the reconnaissance groups**, these groups will be **automatically added or removed** to or from the reconnaissance network, 
  -- when these groups are spawned in or destroyed during the ongoing battle. 
  -- By spawning in dynamically additional recce, you can ensure that there is sufficient reconnaissance coverage so the defense mechanism is continuously
  -- alerted of new enemy ground targets.
  -- 
  -- The following is an example defense of a new reconnaissance network using a @{Functional.Detection#DETECTION_AREAS} object.
  -- 
  --        -- Define a SET_GROUP object that builds a collection of groups that define the recce network.
  --        -- Here we build the network with all the groups that have a name starting with CCCP Recce.
  --        DetectionSetGroup = SET_GROUP:New() -- Define a set of group objects, called DetectionSetGroup.
  -- 
  --        DetectionSetGroup:FilterPrefixes( { "CCCP Recce" } ) -- The DetectionSetGroup will search for groups that start with the name "CCCP Recce".
  -- 
  --        -- This command will start the dynamic filtering, so when groups spawn in or are destroyed,
  --        -- which have a group name starting with "CCCP Recce", then these will be automatically added or removed from the set.
  --        DetectionSetGroup:FilterStart()
  -- 
  --        -- This command defines the reconnaissance network.
  --        -- It will group any detected ground enemy targets within a radius of 1km.
  --        -- It uses the DetectionSetGroup, which defines the set of reconnaissance groups to detect for enemy ground targets.
  --        Detection = DETECTION_AREAS:New( DetectionSetGroup, 1000 )
  -- 
  --        -- Setup the A2G dispatcher, and initialize it.
  --        A2GDispatcher = AI_A2G_DISPATCHER:New( Detection )
  -- 
  -- 
  -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **DetectionSetGroup**.
  -- **DetectionSetGroup** is then being configured to filter all active groups with a group name starting with `"CCCP Recce"` to be included in the set.
  -- **DetectionSetGroup** is then calling `FilterStart()`, which is starting the dynamic filtering or inclusion of these groups.
  -- Note that any destroy or new spawn of a group having a name, starting with the above prefix, will be removed or added to the set.
  -- 
  -- Then a new detection object is created from the class `DETECTION_AREAS`. A grouping radius of 1000 meters (1km) is chosen.
  -- 
  -- The `Detection` object is then passed to the @{#AI_A2G_DISPATCHER.New}() method to indicate the reconnaissance network
  -- configuration and setup the A2G defense detection mechanism.
  -- 
  -- 
  -- ### 1.2. Setup the A2G dispatcher for both a red and blue coalition.
  -- 
  -- Following the above described procedure, you'll need to create for each coalition an separate detection network, and a separate A2G dispatcher.
  -- Ensure that while doing so, that you name the objects differently both for red and blue coalition.
  -- 
  -- For example like this for the red coalition:
  -- 
  --        DetectionRed = DETECTION_AREAS:New( DetectionSetGroupRed, 1000 )
  --        A2GDispatcherRed = AI_A2G_DISPATCHER:New( DetectionRed )
  --        
  -- And for the blue coalition:
  -- 
  --        DetectionBlue = DETECTION_AREAS:New( DetectionSetGroupBlue, 1000 )
  --        A2GDispatcherBlue = AI_A2G_DISPATCHER:New( DetectionBlue )
  -- 
  -- Note: Also the SET_GROUP objects should be created for each coalition separately, containing each red and blue recce respectively!
  -- 
  -- 
  -- ### 1.3. Define the enemy ground target **grouping radius**, in case you use DETECTION_AREAS:
  -- 
  -- The target grouping radius is a property of the DETECTION_AREAS class, that was passed to the AI_A2G_DISPATCHER:New() method
  -- but can be changed. The grouping radius should not be too small, but also depends on the types of ground forces and the way you want your mission to evolve.
  -- A large radius will mean large groups of enemy ground targets, while making smaller groups will result in a more fragmented defense system.
  -- Typically I suggest a grouping radius of 1km. This is the right balance to create efficient defenses.
  -- 
  -- Note that detected targets are constantly re-grouped, that is, when certain detected enemy ground units are moving further than the group radius
  -- then these units will become a separate area being detected. This may result in additional defenses being started by the dispatcher,
  -- so don't make this value too small! Again, about 1km, or 1000 meters, is recommended.
  -- 
  -- 
  -- ## 2. Setup (a) **Defense Coordinate(s)**.
  -- 
  -- As explained above, defense coordinates are the center of your defense operations.
  -- The more threat to the defense coordinate, the higher it is likely a defensive action will be launched.
  -- 
  -- Find below an example how to add defense coordinates:
  -- 
  --        -- Add defense coordinates.
  --        A2GDispatcher:AddDefenseCoordinate( "HQ", GROUP:FindByName( "HQ" ):GetCoordinate() )
  -- 
  -- In this example, the coordinate of a group called `"HQ"` is retrieved, using `:GetCoordinate()`
  -- This returns a COORDINATE object, pointing to the first unit within the GROUP object.
  -- 
  -- The method @{#AI_A2G_DISPATCHER.AddDefenseCoordinate}() adds a new defense coordinate to the `A2GDispatcher` object.
  -- The first parameter is the key of the defense coordinate, the second the coordinate itself.
  -- 
  -- Later, a COORDINATE_UNIT will be added to the framework, which can be used to assign "moving" coordinates to an A2G dispatcher.
  -- 
  -- **REMEMBER!**
  -- 
  --   - **Defense coordinates are the center of the A2G dispatcher defense system!**
  --   - **You can define more defense coordinates to defend a larger area.**
  --   - **Detected enemy ground targets are not immediately engaged, but are engaged with a reactivity or probability calculation!**
  -- 
  -- But, there is more to it ...
  -- 
  -- 
  -- ### 2.1. The **Defense Radius**.
  -- 
  -- The defense radius defines the maximum radius that a defense will be initiated around each defense coordinate.
  -- So even when there are targets further away than the defense radius, then these targets won't be engaged upon.
  -- By default, the defense radius is set to 100km (100.000 meters), but can be changed using the @{#AI_A2G_DISPATCHER.SetDefenseRadius}() method.
  -- Note that the defense radius influences the defense reactivity also! The larger the defense radius, the more reactive the defenses will be.
  -- 
  -- For example:
  -- 
  --        A2GDispatcher:SetDefenseRadius( 30000 )
  -- 
  -- This defines an A2G dispatcher which will engage on enemy ground targets within 30km radius around the defense coordinate.
  -- Note that the defense radius **applies to all defense coordinates** defined within the A2G dispatcher.
  -- 
  -- 
  -- ### 2.2. The **Defense Reactivity**.
  -- 
  -- There are three levels that can be configured to tweak the defense reactivity. As explained above, the threat to a defense coordinate is
  -- also determined by the distance of the enemy ground target to the defense coordinate.
  -- If you want to have a **low** defense reactivity, that is, the probability that an A2G defense will engage to the enemy ground target, then
  -- use the @{#AI_A2G_DISPATCHER.SetDefenseReactivityLow}() method. For medium and high reactivity, use the methods
  -- @{#AI_A2G_DISPATCHER.SetDefenseReactivityMedium}() and @{#AI_A2G_DISPATCHER.SetDefenseReactivityHigh}() respectively.
  -- 
  -- Note that the reactivity of defenses is always in relation to the Defense Radius! the shorter the distance,
  -- the less reactive the defenses will be in terms of distance to enemy ground targets!
  -- 
  -- For example:
  -- 
  --        A2GDispatcher:SetDefenseReactivityHigh()
  --        
  -- This defines an A2G dispatcher with high defense reactivity.
  -- 
  -- 
  -- ## 3. **Squadrons**.
  -- 
  -- The A2G dispatcher works with **Squadrons**, that need to be defined using the different methods available.
  -- 
  -- Use the method @{#AI_A2G_DISPATCHER.SetSquadron}() to **setup a new squadron** active at an airfield, FARP or carrier,
  -- while defining which helicopter or plane **templates** are being used by the squadron and how many **resources** are available.
  -- 
  -- **Multiple squadrons** can be defined within one A2G dispatcher, each having specific defense tasks and defense parameter settings!
  -- 
  -- Squadrons:
  -- 
  --   * Have name (string) that is the identifier or **key** of the squadron.
  --   * Have specific helicopter or plane **templates**.
  --   * Are located at **one** airbase, farp or carrier.
  --   * Optionally have a **limited set of resources**. The default is that squadrons have **unlimited resources**.
  -- 
  -- The name of the squadron given acts as the **squadron key** in all `A2GDispatcher:SetSquadron...()` or `A2GDispatcher:GetSquadron...()` methods.
  -- 
  -- Additionally, squadrons have specific configuration options to:
  -- 
  --   * Control how new helicopters or aircraft are taking off from the airfield, farp or carrier (in the air, cold, hot, at the runway).
  --   * Control how returning helicopters or aircraft are landing at the airfield, farp or carrier (in the air near the airbase, after landing, after engine shutdown).
  --   * Control the **grouping** of new helicopters or aircraft spawned at the airfield, farp or carrier. If there is more than one helicopter or aircraft to be spawned, these may be grouped.
  --   * Control the **overhead** or defensive strength of the squadron. Depending on the types of helicopters, planes, amount of resources and payload (weapon configuration) chosen,
  --     the mission designer can choose to increase or reduce the amount of planes spawned.
  -- 
  -- The method @{#AI_A2G_DISPATCHER.SetSquadron}() defines for you a new squadron. 
  -- The provided parameters are the squadron name, airbase name and a list of template prefixes, and a number that indicates the amount of resources.
  -- 
  -- For example, this defines 3 new squadrons:
  -- 
  --        A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50" }, 10 )
  --        A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50" }, 10 )
  --        A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50" }, 10 )
  -- 
  -- The latter 2 will depart from FARPs, which bare the name `"CAS"` and `"BAI"`.
  -- 
  -- 
  -- ### 3.1. Squadrons **Tasking**.
  -- 
  -- Squadrons can be commanded to execute 3 types of tasks, as explained above:
  -- 
  --   - SEAD: Suppression of Air Defenses, which are ground targets that have medium or long range radar emitters.
  --   - BAI : Battlefield Air Interdiction, which are targets further away from the front-line.
  --   - CAS : Close Air Support, when there are enemy ground targets close to friendly units.
  -- 
  -- You need to configure each squadron which task types you want it to perform. Read on ...
  -- 
  -- 
  -- ### 3.2. Squadrons enemy ground target **engagement types**.
  --   
  -- There are two ways how targets can be engaged: directly **on call** from the airfield, FARP or carrier, or through a **patrol**.
  -- 
  -- Patrols are extremely handy, as these will get your helicopters or airplanes airborne in advance. They will patrol in defined zones outlined,
  -- and will engage with the targets once commanded. If the patrol zone is close enough to the enemy ground targets, then the time required
  -- to engage is heavily minimized!
  -- 
  -- However; patrols come with a side effect: since your resources are airborne, they will be vulnerable to incoming air attacks from the enemy.
  -- 
  -- The mission designer needs to carefully balance the need for patrols or the need for engagement on call from the airfields.
  -- 
  -- 
  -- ### 3.3. Squadron **on call** engagement.
  -- 
  -- So to make squadrons engage targets from the airfields, use the following methods:
  -- 
  --   - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSead}() method.
  --   - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBai}() method.
  --   - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCas}() method.
  -- 
  -- Note that for the tasks, specific helicopter or airplane templates are required to be used, which you can configure using your mission editor.
  -- Especially the payload (weapons configuration) is important to get right.
  -- 
  -- For example, the following will define for the squadrons different tasks:
  -- 
  --        A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 )
  --        A2GDispatcher:SetSquadronSead( "Maykop SEAD", 120, 250 )
  --        
  --        A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 )
  --        A2GDispatcher:SetSquadronBai( "Maykop BAI", 120, 250 )
  -- 
  --        A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 )
  --        A2GDispatcher:SetSquadronCas( "Maykop CAS", 120, 250 )
  --        
  -- 
  -- ### 3.4. Squadron **on patrol engagement**.
  -- 
  -- Squadrons can be setup to patrol in the air near the engagement hot zone.
  -- When needed, the A2G defense units will be close to the battle area, and can engage quickly.
  -- 
  -- So to make squadrons engage targets from a patrol zone, use the following methods:
  -- 
  --   - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrol}() method.
  --   - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrol}() method.
  --   - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrol}() method.
  -- 
  -- Because a patrol requires more parameters, the following methods must be used to fine-tune the patrols for each squadron.
  -- 
  --   - For SEAD, use the @{#AI_A2G_DISPATCHER.SetSquadronSeadPatrolInterval}() method.
  --   - For BAI, use the @{#AI_A2G_DISPATCHER.SetSquadronBaiPatrolInterval}() method.
  --   - For CAS, use the @{#AI_A2G_DISPATCHER.SetSquadronCasPatrolInterval}() method.
  -- 
  -- Here an example to setup patrols of various task types:
  -- 
  --        A2GDispatcher:SetSquadron( "Maykop SEAD", AIRBASE.Caucasus.Maykop_Khanskaya, { "CCCP KA-50 SEAD" }, 10 )
  --        A2GDispatcher:SetSquadronSeadPatrol( "Maykop SEAD", PatrolZone, 300, 500, 50, 80, 250, 300 )
  --        A2GDispatcher:SetSquadronPatrolInterval( "Maykop SEAD", 2, 30, 60, 1, "SEAD" )
  --        
  --        A2GDispatcher:SetSquadron( "Maykop BAI", "BAI", { "CCCP KA-50 BAI" }, 10 )
  --        A2GDispatcher:SetSquadronBaiPatrol( "Maykop BAI", PatrolZone, 800, 900, 50, 80, 250, 300 )
  --        A2GDispatcher:SetSquadronPatrolInterval( "Maykop BAI", 2, 30, 60, 1, "BAI" )
  -- 
  --        A2GDispatcher:SetSquadron( "Maykop CAS", "CAS", { "CCCP KA-50 CAS" }, 10 )
  --        A2GDispatcher:SetSquadronCasPatrol( "Maykop CAS", PatrolZone, 600, 700, 50, 80, 250, 300 )
  --        A2GDispatcher:SetSquadronPatrolInterval( "Maykop CAS", 2, 30, 60, 1, "CAS" )
  --        
  -- 
  -- ### 3.5. Set squadron takeoff methods
  -- 
  -- Use the various SetSquadronTakeoff... methods to control how squadrons are taking-off from the home airfield, FARP or ship.
  -- 
  --   * @{#AI_A2G_DISPATCHER.SetSquadronTakeoff}() is the generic configuration method to control takeoff from the air, hot, cold or from the runway. See the method for further details.
  --   * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() will spawn new aircraft from the squadron directly in the air.
  --   * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingCold}() will spawn new aircraft in without running engines at a parking spot at the airfield.
  --   * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromParkingHot}() will spawn new aircraft in with running engines at a parking spot at the airfield.
  --   * @{#AI_A2G_DISPATCHER.SetSquadronTakeoffFromRunway}() will spawn new aircraft at the runway at the airfield.
  -- 
  -- **The default landing method is to spawn new aircraft directly in the air.**
  -- 
  -- Use these methods to fine-tune for specific airfields that are known to create bottlenecks, or have reduced airbase efficiency.
  -- The more and the longer aircraft need to taxi at an airfield, the more risk there is that:
  -- 
  --   * aircraft will stop waiting for each other or for a landing aircraft before takeoff.
  --   * aircraft may get into a "dead-lock" situation, where two aircraft are blocking each other.
  --   * aircraft may collide at the airbase.
  --   * aircraft may be awaiting the landing of a plane currently in the air, but never lands ...
  -- 
  -- Currently within the DCS engine, the airfield traffic coordination is erroneous and contains a lot of bugs.
  -- If you experience while testing problems with aircraft takeoff or landing, please use one of the above methods as a solution to workaround these issues!
  -- 
  -- This example sets the default takeoff method to be from the runway.
  -- And for a couple of squadrons overrides this default method.
  -- 
  --      -- Setup the takeoff methods
  -- 
  --      -- Set the default takeoff method
  --      A2GDispatcher:SetDefaultTakeoffFromRunway()
  -- 
  --      -- Set the individual squadrons takeoff method
  --      A2GDispatcher:SetSquadronTakeoff( "Mineralnye", AI_A2G_DISPATCHER.Takeoff.Air )
  --      A2GDispatcher:SetSquadronTakeoffInAir( "Sochi" )
  --      A2GDispatcher:SetSquadronTakeoffFromRunway( "Mozdok" )
  --      A2GDispatcher:SetSquadronTakeoffFromParkingCold( "Maykop" )
  --      A2GDispatcher:SetSquadronTakeoffFromParkingHot( "Novo" )
  -- 
  -- 
  -- ### 3.5.1. Set Squadron takeoff altitude when spawning new aircraft in the air.
  -- 
  -- In the case of the @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() there is also an other parameter that can be applied.
  -- That is modifying or setting the **altitude** from where planes spawn in the air.
  -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAirAltitude}() to set the altitude for a specific squadron.
  -- The default takeoff altitude can be modified or set using the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAirAltitude}().
  -- As part of the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInAir}() a parameter can be specified to set the takeoff altitude.
  -- If this parameter is not specified, then the default altitude will be used for the squadron.
  -- 
  -- 
  -- ### 3.5.2. Set Squadron takeoff interval.
  -- 
  -- The different types of available airfields have different amounts of available launching platforms:
  -- 
  --   - Airbases typically have a lot of platforms.
  --   - FARPs have 4 platforms.
  --   - Ships have 2 to 4 platforms.
  -- 
  -- Depending on the demand of requested takeoffs by the A2G dispatcher, an airfield can become overloaded. Too many aircraft need to be taken
  -- off at the same time, which will result in clutter as described above. In order to better control this behaviour, a takeoff scheduler is implemented,
  -- which can be used to control how many aircraft are ordered for takeoff between specific time intervals.
  -- The takeoff intervals can be specified per squadron, which make sense, as each squadron have a "home" airfield.
  -- 
  -- For this purpose, the method @{#AI_A2G_DISPATCHER.SetSquadronTakeoffInterval}() can be used to specify the takeoff intervals of
  -- aircraft groups per squadron to avoid cluttering of aircraft at airbases.
  -- This is especially useful for FARPs and ships. Each takeoff dispatch is queued by the dispatcher and when the interval time
  -- has been reached, a new group will be spawned or activated for takeoff.
  -- 
  -- The interval needs to be estimated, and depends on the time needed for the aircraft group to actually depart from the launch platform, and
  -- the way how the aircraft are starting up. Cold starts take the longest duration, hot starts a few seconds, and runway takeoff also a few seconds for FARPs and ships.
  -- 
  -- See the underlying example:
  -- 
  --      -- Imagine a squadron launched from a FARP, with a grouping of 4.
  --      -- Aircraft will cold start from the FARP, and thus, a maximum of 4 aircraft can be launched at the same time.
  --      -- Additionally, depending on the group composition of the aircraft, defending units will be ordered for takeoff together.
  --      -- It takes about 3 to 4 minutes for helicopters to takeoff from FARPs in cold start.
  --      A2GDispatcher:SetSquadronTakeoffInterval( "Mineralnye", 60 * 4 )
  -- 
  -- 
  -- ### 3.6. Set squadron landing methods
  -- 
  -- In analogy with takeoff, the landing methods are to control how squadrons land at the airfield:
  -- 
  --   * @{#AI_A2G_DISPATCHER.SetSquadronLanding}() is the generic configuration method to control landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown.
  --   * @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will despawn the returning aircraft in the air when near the airfield.
  --   * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtRunway}() will despawn the returning aircraft directly after landing at the runway.
  --   * @{#AI_A2G_DISPATCHER.SetSquadronLandingAtEngineShutdown}() will despawn the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines.
  -- 
  -- You can use these methods to minimize the airbase coordination overhead and to increase the airbase efficiency.
  -- When there are lots of aircraft returning for landing, at the same airbase, the takeoff process will be halted, which can cause a complete failure of the
  -- A2G defense system, as no new SEAD, BAI or CAS planes can takeoff.
  -- Note that the method @{#AI_A2G_DISPATCHER.SetSquadronLandingNearAirbase}() will only work for returning aircraft, not for damaged or out of fuel aircraft.
  -- Damaged or out-of-fuel aircraft are returning to the nearest friendly airbase and will land, and are out of control from ground control.
  -- 
  -- This example defines the default landing method to be at the runway.
  -- And for a couple of squadrons overrides this default method.
  -- 
  --      -- Setup the Landing methods
  -- 
  --      -- The default landing method
  --      A2GDispatcher:SetDefaultLandingAtRunway()
  -- 
  --      -- The individual landing per squadron
  --      A2GDispatcher:SetSquadronLandingAtRunway( "Mineralnye" )
  --      A2GDispatcher:SetSquadronLandingNearAirbase( "Sochi" )
  --      A2GDispatcher:SetSquadronLandingAtEngineShutdown( "Mozdok" )
  --      A2GDispatcher:SetSquadronLandingNearAirbase( "Maykop" )
  --      A2GDispatcher:SetSquadronLanding( "Novo", AI_A2G_DISPATCHER.Landing.AtRunway )
  -- 
  -- 
  -- ### 3.7. Set squadron **grouping**.
  -- 
  -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronGrouping}() to set the grouping of aircraft when spawned in.
  -- 
  -- In the case of **on call** engagement, the @{#AI_A2G_DISPATCHER.SetSquadronGrouping}() method has additional behaviour.
  -- When there aren't enough patrol flights airborne, a on call will be initiated for the remaining
  -- targets to be engaged. Depending on the grouping parameter, the spawned flights for on call aircraft are grouped into this setting.   
  -- For example with a group setting of 2, if 3 targets are detected and cannot be engaged by the available patrols or any airborne flight, 
  -- an additional on call flight needs to be started.
  -- 
  -- The **grouping value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense flights grouping when the tactical situation changes.
  -- 
  -- ### 3.8. Set the squadron **overhead** to balance the effectiveness of the A2G defenses.
  -- 
  -- The effectiveness can be set with the **overhead parameter**. This is a number that is used to calculate the amount of Units that dispatching command will allocate to GCI in surplus of detected amount of units.
  -- The **default value** of the overhead parameter is 1.0, which means **equal balance**.
  -- 
  -- However, depending on the (type of) aircraft (strength and payload) in the squadron and the amount of resources available, this parameter can be changed.
  -- 
  -- The @{#AI_A2G_DISPATCHER.SetSquadronOverhead}() method can be used to tweak the defense strength,
  -- taking into account the plane types of the squadron. 
  -- 
  -- For example, a A-10C with full long-distance A2G missiles payload, may still be less effective than a Su-23 with short range A2G missiles...
  -- So in this case, one may want to use the @{#AI_A2G_DISPATCHER.SetOverhead}() method to allocate more defending planes as the amount of detected attacking ground units.
  -- The overhead must be given as a decimal value with 1 as the neutral value, which means that overhead values:
  -- 
  --   * Higher than 1.0, for example 1.5, will increase the defense unit amounts. For 4 attacking ground units detected, 6 aircraft will be spawned.
  --   * Lower than 1, for example 0.75, will decrease the defense unit amounts. For 4 attacking ground units detected, only 3 aircraft will be spawned.
  -- 
  -- The amount of defending units is calculated by multiplying the amount of detected attacking ground units as part of the detected group
  -- multiplied by the overhead parameter, and rounded up to the smallest integer.
  -- 
  -- Typically, for A2G defenses, values small than 1 will be used. Here are some good values for a couple of aircraft to support CAS operations:
  -- 
  --   - A-10C: 0.15
  --   - Su-34: 0.15
  --   - A-10A: 0.25
  --   - SU-25T: 0.10
  -- 
  -- So generically, the amount of missiles that an aircraft can take will determine its attacking effectiveness. The longer the range of the missiles,
  -- the less risk that the defender may be destroyed by the enemy, thus, the less aircraft needs to be activated in a defense.
  -- 
  -- The **overhead value is set for a Squadron**, and can be **dynamically adjusted** during mission execution, so to adjust the defense overhead when the tactical situation changes.
  --
  -- ### 3.8. Set the squadron **engage limit**.
  -- 
  -- To limit the amount of aircraft to defend against a large group of intruders, an **engage limit** can be defined per squadron.
  -- This limit will avoid an extensive amount of aircraft to engage with the enemy if the attacking ground forces are enormous.
  -- 
  -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronEngageLimit}() to limit the amount of aircraft that will engage with the enemy, per squadron.
  --
  -- ## 4. Set the **fuel threshold**.
  -- 
  -- When an aircraft gets **out of fuel** with only a certain % of fuel left, which is **15% (0.15)** by default, there are two possible actions that can be taken:
  --  - The aircraft will go RTB, and will be replaced with a new aircraft if possible.
  --  - The aircraft will refuel at a tanker, if a tanker has been specified for the squadron.
  -- 
  -- Use the method @{#AI_A2G_DISPATCHER.SetSquadronFuelThreshold}() to set the **squadron fuel threshold** of the aircraft for all squadrons.
  -- 
  -- ## 6. Other configuration options
  -- 
  -- ### 6.1. Set a tactical display panel.
  -- 
  -- Every 30 seconds, a tactical display panel can be shown that illustrates what the status is of the different groups controlled by AI_A2G_DISPATCHER.
  -- Use the method @{#AI_A2G_DISPATCHER.SetTacticalDisplay}() to switch on the tactical display panel. The default will not show this panel.
  -- Note that there may be some performance impact if this panel is shown.
  -- 
  -- ## 10. Default settings.
  -- 
  -- Default settings configure the standard behaviour of the squadrons.
  -- This section a good overview of the different parameters that setup the behaviour of **ALL** the squadrons by default.
  -- Note that default behaviour can be tweaked, and thus, this will change the behaviour of all the squadrons.
  -- Unless there is a specific behaviour set for a specific squadron, the default configured behaviour will be followed.
  -- 
  -- ## 10.1. Default **takeoff** behaviour.
  -- 
  -- The default takeoff behaviour is set to **in the air**, which means that new spawned aircraft will be spawned directly in the air above the airbase by default.
  -- 
  -- **The default takeoff method can be set for ALL squadrons that don't have an individual takeoff method configured.**
  -- 
  --   * @{#AI_A2G_DISPATCHER.SetDefaultTakeoff}() is the generic configuration method to control takeoff by default from the air, hot, cold or from the runway. See the method for further details.
  --   * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffInAir}() will spawn by default new aircraft from the squadron directly in the air.
  --   * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromParkingCold}() will spawn by default new aircraft in without running engines at a parking spot at the airfield.
  --   * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromParkingHot}() will spawn by default new aircraft in with running engines at a parking spot at the airfield.
  --   * @{#AI_A2G_DISPATCHER.SetDefaultTakeoffFromRunway}() will spawn by default new aircraft at the runway at the airfield.
  -- 
  -- ## 10.2. Default landing behaviour.
  -- 
  -- The default landing behaviour is set to **near the airbase**, which means that returning aircraft will be despawned directly in the air by default.
  -- 
  -- The default landing method can be set for ALL squadrons that don't have an individual landing method configured.
  -- 
  --   * @{#AI_A2G_DISPATCHER.SetDefaultLanding}() is the generic configuration method to control by default landing, namely despawn the aircraft near the airfield in the air, right after landing, or at engine shutdown.
  --   * @{#AI_A2G_DISPATCHER.SetDefaultLandingNearAirbase}() will despawn by default the returning aircraft in the air when near the airfield.
  --   * @{#AI_A2G_DISPATCHER.SetDefaultLandingAtRunway}() will despawn by default the returning aircraft directly after landing at the runway.
  --   * @{#AI_A2G_DISPATCHER.SetDefaultLandingAtEngineShutdown}() will despawn by default the returning aircraft when the aircraft has returned to its parking spot and has turned off its engines.
  -- 
  -- ## 10.3. Default **overhead**.
  -- 
  -- The default overhead is set to **0.25**. That essentially means that for each 4 ground enemies there will be 1 aircraft dispatched.
  -- 
  -- The default overhead value can be set for ALL squadrons that don't have an individual overhead value configured.
  --
  -- Use the @{#AI_A2G_DISPATCHER.SetDefaultOverhead}() method can be used to set the default overhead or defense strength for ALL squadrons.
  --
  -- ## 10.4. Default **grouping**.
  -- 
  -- The default grouping is set to **one aircraft**. That essentially means that there won't be any grouping applied by default.
  -- 
  -- The default grouping value can be set for ALL squadrons that don't have an individual grouping value configured.
  -- 
  -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultGrouping}() to set the **default grouping** of spawned aircraft for all squadrons.
  -- 
  -- ## 10.5. Default RTB fuel threshold.
  -- 
  -- When an aircraft gets **out of fuel** with only a certain % of fuel left, which is **15% (0.15)** by default, it will go RTB, and will be replaced with a new aircraft when applicable.
  -- 
  -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the **default fuel threshold** of spawned aircraft for all squadrons.
  -- 
  -- ## 10.6. Default RTB damage threshold.
  -- 
  -- When an aircraft is **damaged** to a certain %, which is **40% (0.40)** by default, it will go RTB, and will be replaced with a new aircraft when applicable.
  -- 
  -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultDamageThreshold}() to set the **default damage threshold** of spawned aircraft for all squadrons.
  -- 
  -- ## 10.7. Default settings for **patrol**.
  -- 
  -- ### 10.7.1. Default **patrol time Interval**.
  -- 
  -- Patrol dispatching is time event driven, and will evaluate in random time intervals if a new patrol needs to be dispatched.
  -- 
  -- The default patrol time interval is between **180** and **600** seconds.
  -- 
  -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultPatrolTimeInterval}() to set the **default patrol time interval** of dispatched aircraft for ALL squadrons.
  --   
  -- Note that you can still change the patrol limit and patrol time intervals for each patrol individually using 
  -- the @{#AI_A2G_DISPATCHER.SetSquadronPatrolTimeInterval}() method.
  -- 
  -- ### 10.7.2. Default **patrol limit**.
  -- 
  -- Multiple patrol can be airborne at the same time for one squadron, which is controlled by the **patrol limit**.
  -- The **default patrol limit** is 1 patrol per squadron to be airborne at the same time.
  -- Note that the default patrol limit is used when a squadron patrol is defined, and cannot be changed afterwards.
  -- So, ensure that you set the default patrol limit **before** you define or setup the squadron patrol.
  -- 
  -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultPatrolTimeInterval}() to set the **default patrol time interval** of dispatched aircraft patrols for all squadrons.  
  -- Note that you can still change the patrol limit and patrol time intervals for each patrol individually using 
  -- the @{#AI_A2G_DISPATCHER.SetSquadronPatrolTimeInterval}() method.
  -- 
  -- ## 10.7.3. Default tanker for refuelling when executing SEAD, BAI and CAS operations.
  -- 
  -- Instead of sending SEAD, BAI and CAS aircraft to RTB when out of fuel, you can let SEAD, BAI and CAS aircraft refuel in mid air using a tanker.
  -- This greatly increases the efficiency of your SEAD, BAI and CAS operations.
  -- 
  -- In the mission editor, setup a group with task Refuelling. A tanker unit of the correct coalition will be automatically selected.
  -- Then, use the method @{#AI_A2G_DISPATCHER.SetDefaultTanker}() to set the tanker for the dispatcher.
  -- Use the method @{#AI_A2G_DISPATCHER.SetDefaultFuelThreshold}() to set the % left in the defender aircraft tanks when a refuel action is needed.
  -- 
  -- When the tanker specified is alive and in the air, the tanker will be used for refuelling.
  -- 
  -- For example, the following setup will set the default refuel tanker to "Tanker":
  -- 
  --      -- Set the default tanker for refuelling to "Tanker", when the default fuel threshold has reached 90% fuel left.
  --      A2GDispatcher:SetDefaultFuelThreshold( 0.9 )
  --      A2GDispatcher:SetDefaultTanker( "Tanker" )
  --  
  -- ## 10.8. Default settings for GCI.
  -- 
  -- ## 10.8.1. Optimal intercept point calculation.
  -- 
  -- When intruders are detected, the intrusion path of the attackers can be monitored by the EWR.  
  -- Although defender planes might be on standby at the airbase, it can still take some time to get the defenses up in the air if there aren't any defenses airborne.
  -- This time can easily take 2 to 3 minutes, and even then the defenders still need to fly towards the target, which takes also time.
  -- 
  -- Therefore, an optimal **intercept point** is calculated which takes a couple of parameters:
  -- 
  --   * The average bearing of the intruders for an amount of seconds.
  --   * The average speed of the intruders for an amount of seconds.
  --   * An assumed time it takes to get planes operational at the airbase.
  -- 
  -- The **intercept point** will determine:
  -- 
  --   * If there are any friendlies close to engage the target. These can be defenders performing CAP or defenders in RTB.
  --   * The optimal airbase from where defenders will takeoff for GCI.
  -- 
  -- Use the method @{#AI_A2G_DISPATCHER.SetIntercept}() to modify the assumed intercept delay time to calculate a valid interception.
  -- 
  -- ## 10.8.2. Default Disengage Radius.
  -- 
  -- The radius to **disengage any target** when the **distance** of the defender to the **home base** is larger than the specified meters.
  -- The default Disengage Radius is **300km** (300000 meters). Note that the Disengage Radius is applicable to ALL squadrons!
  --   
  -- Use the method @{#AI_A2G_DISPATCHER.SetDisengageRadius}() to modify the default Disengage Radius to another distance setting.
  -- 
  -- ## 11. Airbase capture:
  -- 
  -- Different squadrons can be located at one airbase.
  -- If the airbase gets captured, that is when there is an enemy unit near the airbase and there are no friendlies at the airbase, the airbase will change coalition ownership.
  -- As a result, further SEAD, BAI, and CAS operations from that airbase will stop.
  -- However, the squadron will still stay alive. Any aircraft that is airborne will continue its operations until all airborne aircraft
  -- of the squadron are destroyed. This is to keep consistency of air operations and avoid confusing players.
  -- 
  -- 
  -- 
  -- 
  -- @field #AI_A2G_DISPATCHER
  AI_A2G_DISPATCHER = {
    ClassName = "AI_A2G_DISPATCHER",
    Detection = nil,
  }

  --- Definition of a Squadron.
  -- @type AI_A2G_DISPATCHER.Squadron
  -- @field #string Name The Squadron name.
  -- @field Wrapper.Airbase#AIRBASE Airbase The home airbase.
  -- @field #string AirbaseName The name of the home airbase.
  -- @field Core.Spawn#SPAWN Spawn The spawning object.
  -- @field #number ResourceCount The number of resources available.
  -- @field #list<#string> TemplatePrefixes The list of template prefixes.
  -- @field #boolean Captured true if the squadron is captured.
  -- @field #number Overhead The overhead for the squadron.

  --- List of defense coordinates.
  -- @type AI_A2G_DISPATCHER.DefenseCoordinates
  -- @map <#string,Core.Point#COORDINATE> A list of all defense coordinates mapped per defense coordinate name.

  -- @field #AI_A2G_DISPATCHER.DefenseCoordinates DefenseCoordinates
  AI_A2G_DISPATCHER.DefenseCoordinates = {}

  --- Enumerator for spawns at airbases.
  -- @type AI_A2G_DISPATCHER.Takeoff
  -- @extends Wrapper.Group#GROUP.Takeoff
  
  -- @field #AI_A2G_DISPATCHER.Takeoff Takeoff
  AI_A2G_DISPATCHER.Takeoff = GROUP.Takeoff
  
  --- Defines Landing location.
  -- @field #AI_A2G_DISPATCHER.Landing
  AI_A2G_DISPATCHER.Landing = {
    NearAirbase = 1,
    AtRunway = 2,
    AtEngineShutdown = 3,
  }
  
  --- A defense queue item description.
  -- @type AI_A2G_DISPATCHER.DefenseQueueItem
  -- @field Squadron
  -- @field #AI_A2G_DISPATCHER.Squadron DefenderSquadron The squadron in the queue.
  -- @field DefendersNeeded
  -- @field Defense
  -- @field DefenseTaskType
  -- @field Functional.Detection#DETECTION_BASE AttackerDetection
  -- @field DefenderGrouping
  -- @field #string SquadronName The name of the squadron.
  
  --- Queue of planned defenses to be launched.
  -- This queue exists because defenses must be launched from FARPs, in the air, from airbases, or from carriers.
  -- And some of these platforms have very limited amount of "launching" platforms.
  -- Therefore, this queue concept is introduced that queues each defender request.
  -- Depending on the location of the launching site, the queued defenders will be launched at varying time intervals.
  -- This guarantees that launched defenders are also directly existing ...
  -- @type AI_A2G_DISPATCHER.DefenseQueue
  -- @list<#AI_A2G_DISPATCHER.DefenseQueueItem> DefenseQueueItem A list of all defenses being queued ...
  
  -- @field #AI_A2G_DISPATCHER.DefenseQueue DefenseQueue
  AI_A2G_DISPATCHER.DefenseQueue = {}
  
  --- Defense approach types.
  -- @type AI_A2G_DISPATCHER.DefenseApproach
  AI_A2G_DISPATCHER.DefenseApproach = {
    Random = 1,
    Distance = 2,
  }
  
  --- AI_A2G_DISPATCHER constructor.
  -- This is defining the A2G DISPATCHER for one coalition.
  -- The Dispatcher works with a @{Functional.Detection#DETECTION_BASE} object that is taking of the detection of targets using the EWR units.
  -- The Detection object is polymorphic, depending on the type of detection object chosen, the detection will work differently.
  -- @param #AI_A2G_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE Detection The DETECTION object that will detects targets using the the Early Warning Radar network.
  -- @return #AI_A2G_DISPATCHER self
  -- @usage
  --   
  --   -- Setup the Detection, using DETECTION_AREAS.
  --   -- First define the SET of GROUPs that are defining the EWR network.
  --   -- Here with prefixes DF CCCP AWACS, DF CCCP EWR.
  --   DetectionSetGroup = SET_GROUP:New()
  --   DetectionSetGroup:FilterPrefixes( { "DF CCCP AWACS", "DF CCCP EWR" } )
  --   DetectionSetGroup:FilterStart()
  --   
  --   -- Define the DETECTION_AREAS, using the DetectionSetGroup, with a 30km grouping radius.
  --   Detection = DETECTION_AREAS:New( DetectionSetGroup, 30000 )
  -- 
  --   -- Now Setup the A2G dispatcher, and initialize it using the Detection object.
  --   A2GDispatcher = AI_A2G_DISPATCHER:New( Detection )  --   
  -- 
  function AI_A2G_DISPATCHER:New( Detection )

    -- Inherits from DETECTION_MANAGER
    local self = BASE:Inherit( self, DETECTION_MANAGER:New( nil, Detection ) ) -- #AI_A2G_DISPATCHER
    
    self.Detection = Detection -- Functional.Detection#DETECTION_AREAS
    
    self.Detection:FilterCategories( Unit.Category.GROUND_UNIT )
    
    -- This table models the DefenderSquadron templates.
    self.DefenderSquadrons = {} -- The Defender Squadrons.
    self.DefenderSpawns = {}
    self.DefenderTasks = {} -- The Defenders Tasks.
    self.DefenderDefault = {} -- The Defender Default Settings over all Squadrons.
    
    -- TODO: Check detection through radar.
--    self.Detection:FilterCategories( { Unit.Category.GROUND } )
--    self.Detection:InitDetectRadar( false )
--    self.Detection:InitDetectVisual( true )
--    self.Detection:SetRefreshTimeInterval( 30 )
    
    self.SetSendPlayerMessages = false --flash messages to players
    
    self:SetDefenseRadius()
    self:SetDefenseLimit( nil )
    self:SetDefenseApproach( AI_A2G_DISPATCHER.DefenseApproach.Random )
    self:SetIntercept( 300 )  -- A default intercept delay time of 300 seconds.
    self:SetDisengageRadius( 300000 ) -- The default Disengage Radius is 300 km.
    
    self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Air )
    self:SetDefaultTakeoffInAirAltitude( 500 ) -- Default takeoff is 500 meters above ground level (AGL).
    self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.NearAirbase )
    self:SetDefaultOverhead( 1 )
    self:SetDefaultGrouping( 1 )
    self:SetDefaultFuelThreshold( 0.15, 0 ) -- 15% of fuel remaining in the tank will trigger the aircraft to return to base or refuel.
    self:SetDefaultDamageThreshold( 0.4 ) -- When 40% of damage, go RTB.
    self:SetDefaultPatrolTimeInterval( 180, 600 ) -- Between 180 and 600 seconds.
    self:SetDefaultPatrolLimit( 1 ) -- Maximum one Patrol per squadron.
    
    
    self:AddTransition( "Started", "Assign", "Started" )
    
    --- OnAfter Transition Handler for Event Assign.
    -- @function [parent=#AI_A2G_DISPATCHER] OnAfterAssign
    -- @param #AI_A2G_DISPATCHER self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @param Tasking.Task_A2G#AI_A2G Task
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param #string PlayerName
    
    self:AddTransition( "*", "Patrol", "*" )

    --- Patrol Handler OnBefore for AI_A2G_DISPATCHER
    -- @function [parent=#AI_A2G_DISPATCHER] OnBeforePatrol
    -- @param #AI_A2G_DISPATCHER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Patrol Handler OnAfter for AI_A2G_DISPATCHER
    -- @function [parent=#AI_A2G_DISPATCHER] OnAfterPatrol
    -- @param #AI_A2G_DISPATCHER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Patrol Trigger for AI_A2G_DISPATCHER
    -- @function [parent=#AI_A2G_DISPATCHER] Patrol
    -- @param #AI_A2G_DISPATCHER self
    
    --- Patrol Asynchronous Trigger for AI_A2G_DISPATCHER
    -- @function [parent=#AI_A2G_DISPATCHER] __Patrol
    -- @param #AI_A2G_DISPATCHER self
    -- @param #number Delay
    
    self:AddTransition( "*", "Defend", "*" )

    --- Defend Handler OnBefore for AI_A2G_DISPATCHER
    -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeDefend
    -- @param #AI_A2G_DISPATCHER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Defend Handler OnAfter for AI_A2G_DISPATCHER
    -- @function [parent=#AI_A2G_DISPATCHER] OnAfterDefend
    -- @param #AI_A2G_DISPATCHER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Defend Trigger for AI_A2G_DISPATCHER
    -- @function [parent=#AI_A2G_DISPATCHER] Defend
    -- @param #AI_A2G_DISPATCHER self
    
    --- Defend Asynchronous Trigger for AI_A2G_DISPATCHER
    -- @function [parent=#AI_A2G_DISPATCHER] __Defend
    -- @param #AI_A2G_DISPATCHER self
    -- @param #number Delay
    
    self:AddTransition( "*", "Engage", "*" )
        
    --- Engage Handler OnBefore for AI_A2G_DISPATCHER
    -- @function [parent=#AI_A2G_DISPATCHER] OnBeforeEngage
    -- @param #AI_A2G_DISPATCHER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Engage Handler OnAfter for AI_A2G_DISPATCHER
    -- @function [parent=#AI_A2G_DISPATCHER] OnAfterEngage
    -- @param #AI_A2G_DISPATCHER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Engage Trigger for AI_A2G_DISPATCHER
    -- @function [parent=#AI_A2G_DISPATCHER] Engage
    -- @param #AI_A2G_DISPATCHER self
    
    --- Engage Asynchronous Trigger for AI_A2G_DISPATCHER
    -- @function [parent=#AI_A2G_DISPATCHER] __Engage
    -- @param #AI_A2G_DISPATCHER self
    -- @param #number Delay
    
    
    -- Subscribe to the CRASH event so that when planes are shot
    -- by a Unit from the dispatcher, they will be removed from the detection...
    -- This will avoid the detection to still "know" the shot unit until the next detection.
    -- Otherwise, a new defense or engage may happen for an already shot plane!
    
    
    self:HandleEvent( EVENTS.Crash, self.OnEventCrashOrDead )
    self:HandleEvent( EVENTS.Dead, self.OnEventCrashOrDead )
    --self:HandleEvent( EVENTS.RemoveUnit, self.OnEventCrashOrDead )
    
    
    self:HandleEvent( EVENTS.Land )
    self:HandleEvent( EVENTS.EngineShutdown )
    
    -- Handle the situation where the airbases are captured.
    self:HandleEvent( EVENTS.BaseCaptured )
    
    self:SetTacticalDisplay( false )
    
    self.DefenderPatrolIndex = 0
    
    self:SetDefenseReactivityMedium()
    
    self.TakeoffScheduleID = self:ScheduleRepeat( 10, 10, 0, nil, self.ResourceTakeoff, self )
    
    self:__Start( 1 )    
    
    return self
  end


  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:onafterStart( From, Event, To )

    self:GetParent( self ).onafterStart( self, From, Event, To )

    -- Spawn the resources.
    for SquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do
      DefenderSquadron.Resource = {}
      for Resource = 1, DefenderSquadron.ResourceCount or 0 do
        self:ResourcePark( DefenderSquadron )
      end
      self:T( "Parked resources for squadron " .. DefenderSquadron.Name )
    end
    
  end
  

  --- Locks the DefenseItem from being defended.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string DetectedItemIndex The index of the detected item.
  function AI_A2G_DISPATCHER:Lock( DetectedItemIndex )
    self:F( { DetectedItemIndex = DetectedItemIndex } )
    local DetectedItem = self.Detection:GetDetectedItemByIndex( DetectedItemIndex )
    if DetectedItem then
      self:F( { Locked = DetectedItem } )
      self.Detection:LockDetectedItem( DetectedItem )
    end
  end
  
  
  --- Unlocks the DefenseItem from being defended.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string DetectedItemIndex The index of the detected item.
  function AI_A2G_DISPATCHER:Unlock( DetectedItemIndex )
    self:F( { DetectedItemIndex = DetectedItemIndex } )
    self:F( { Index = self.Detection.DetectedItemsByIndex } )
    local DetectedItem = self.Detection:GetDetectedItemByIndex( DetectedItemIndex )
    if DetectedItem then
      self:F( { Unlocked = DetectedItem } )
      self.Detection:UnlockDetectedItem( DetectedItem )
    end
  end
  
  
  --- Sets maximum zones to be engaged at one time by defenders.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number DefenseLimit The maximum amount of detected items to be engaged at the same time.
  function AI_A2G_DISPATCHER:SetDefenseLimit( DefenseLimit )
    self:F( { DefenseLimit = DefenseLimit } )
    
    self.DefenseLimit = DefenseLimit
  end
  

  --- Sets the method of the tactical approach of the defenses.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number DefenseApproach Use the structure AI_A2G_DISPATCHER.DefenseApproach to set the defense approach.
  -- The default defense approach is AI_A2G_DISPATCHER.DefenseApproach.Random.
  function AI_A2G_DISPATCHER:SetDefenseApproach( DefenseApproach )
    self:F( { DefenseApproach = DefenseApproach } )
    
    self._DefenseApproach = DefenseApproach
  end
  

  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:ResourcePark( DefenderSquadron )
    local TemplateID = math.random( 1, #DefenderSquadron.Spawn )
    local Spawn = DefenderSquadron.Spawn[ TemplateID ] -- Core.Spawn#SPAWN
    Spawn:InitGrouping( 1 )
    local SpawnGroup
    if self:IsSquadronVisible( DefenderSquadron.Name ) then
      SpawnGroup = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, SPAWN.Takeoff.Cold )
      local GroupName = SpawnGroup:GetName()
      DefenderSquadron.Resources = DefenderSquadron.Resources or {}
      DefenderSquadron.Resources[TemplateID] = DefenderSquadron.Resources[TemplateID] or {}
      DefenderSquadron.Resources[TemplateID][GroupName] = {}
      DefenderSquadron.Resources[TemplateID][GroupName] = SpawnGroup
    end
  end


  -- @param #AI_A2G_DISPATCHER self
  -- @param Core.Event#EVENTDATA EventData
  function AI_A2G_DISPATCHER:OnEventBaseCaptured( EventData )

    local AirbaseName = EventData.PlaceName -- The name of the airbase that was captured.
    
    self:T( "Captured " .. AirbaseName )
    
    -- Now search for all squadrons located at the airbase, and sanitize them.
    for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do
      if Squadron.AirbaseName == AirbaseName then
        Squadron.ResourceCount = -999 -- The base has been captured, and the resources are eliminated. No more spawning.
        Squadron.Captured = true
        self:T( "Squadron " .. SquadronName .. " captured." )
      end
    end
  end


  -- @param #AI_A2G_DISPATCHER self
  -- @param Core.Event#EVENTDATA EventData
  function AI_A2G_DISPATCHER:OnEventCrashOrDead( EventData )
    self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) 
  end


  -- @param #AI_A2G_DISPATCHER self
  -- @param Core.Event#EVENTDATA EventData
  function AI_A2G_DISPATCHER:OnEventLand( EventData )
    self:F( "Landed" )
    local DefenderUnit = EventData.IniUnit
    local Defender = EventData.IniGroup
    local Squadron = self:GetSquadronFromDefender( Defender )
    if Squadron then
      self:F( { SquadronName = Squadron.Name } )
      local LandingMethod = self:GetSquadronLanding( Squadron.Name )
      
      if LandingMethod == AI_A2G_DISPATCHER.Landing.AtRunway then
        local DefenderSize = Defender:GetSize()
        if DefenderSize == 1 then
          self:RemoveDefenderFromSquadron( Squadron, Defender )
        end
        DefenderUnit:Destroy()
        self:ResourcePark( Squadron )
        return
      end
      if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then
        -- Damaged units cannot be repaired anymore.
        DefenderUnit:Destroy()
        return
      end
    end 
  end
  

  -- @param #AI_A2G_DISPATCHER self
  -- @param Core.Event#EVENTDATA EventData
  function AI_A2G_DISPATCHER:OnEventEngineShutdown( EventData )
    local DefenderUnit = EventData.IniUnit
    local Defender = EventData.IniGroup
    local Squadron = self:GetSquadronFromDefender( Defender )
    if Squadron then
      self:F( { SquadronName = Squadron.Name } )
      local LandingMethod = self:GetSquadronLanding( Squadron.Name )
      if LandingMethod == AI_A2G_DISPATCHER.Landing.AtEngineShutdown and
        not DefenderUnit:InAir() then
        local DefenderSize = Defender:GetSize()
        if DefenderSize == 1 then
          self:RemoveDefenderFromSquadron( Squadron, Defender )
        end
        DefenderUnit:Destroy()
        self:ResourcePark( Squadron )
      end
    end 
  end


  do -- Manage the defensive behaviour
  
    -- @param #AI_A2G_DISPATCHER self
    -- @param #string DefenseCoordinateName The name of the coordinate to be defended by A2G defenses.
    -- @param Core.Point#COORDINATE DefenseCoordinate The coordinate to be defended by A2G defenses.
    function AI_A2G_DISPATCHER:AddDefenseCoordinate( DefenseCoordinateName, DefenseCoordinate )
      self.DefenseCoordinates[DefenseCoordinateName] = DefenseCoordinate
    end
    

    -- @param #AI_A2G_DISPATCHER self
    function AI_A2G_DISPATCHER:SetDefenseReactivityLow()
      self.DefenseReactivity = 0.05
    end
    

    -- @param #AI_A2G_DISPATCHER self
    function AI_A2G_DISPATCHER:SetDefenseReactivityMedium()
      self.DefenseReactivity = 0.15
    end
    

    -- @param #AI_A2G_DISPATCHER self
    function AI_A2G_DISPATCHER:SetDefenseReactivityHigh()
      self.DefenseReactivity = 0.5
    end
  
  end
  

  --- Define the radius to disengage any target when the distance to the home base is larger than the specified meters.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number DisengageRadius (Optional, Default = 300000) The radius to disengage a target when too far from the home base.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --   -- Set 50km as the Disengage Radius.
  --   A2GDispatcher:SetDisengageRadius( 50000 )
  --   
  --   -- Set 100km as the Disengage Radius.
  --   A2GDispatcher:SetDisengageRadius() -- 300000 is the default value.
  --   
  function AI_A2G_DISPATCHER:SetDisengageRadius( DisengageRadius )

    self.DisengageRadius = DisengageRadius or 300000
  
    return self
  end
  
  
  --- Define the defense radius to check if a target can be engaged by a squadron group for SEAD, BAI, or CAS for defense.
  -- When targets are detected that are still really far off, you don't want the AI_A2G_DISPATCHER to launch defenders, as they might need to travel too far.
  -- You want it to wait until a certain defend radius is reached, which is calculated as:
  --   1. the **distance of the closest airbase to target**, being smaller than the **Defend Radius**.
  --   2. the **distance to any defense reference point**.
  -- 
  -- The **default** defense radius is defined as **40000** or **40km**. Override the default defense radius when the era of the warfare is early, or, 
  -- when you don't want to let the AI_A2G_DISPATCHER react immediately when a certain border or area is not being crossed.
  -- 
  -- Use the method @{#AI_A2G_DISPATCHER.SetDefendRadius}() to set a specific defend radius for all squadrons,
  -- **the Defense Radius is defined for ALL squadrons which are operational.**
  -- 
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number DefenseRadius (Optional, Default = 20000) The defense radius to engage detected targets from the nearest capable and available squadron airbase.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --   -- Now Setup the A2G dispatcher, and initialize it using the Detection object.
  --   A2GDispatcher = AI_A2G_DISPATCHER:New( Detection ) 
  --   
  --   -- Set 100km as the radius to defend from detected targets from the nearest airbase.
  --   A2GDispatcher:SetDefendRadius( 100000 )
  --   
  --   -- Set 200km as the radius to defend.
  --   A2GDispatcher:SetDefendRadius() -- 200000 is the default value.
  --   
  function AI_A2G_DISPATCHER:SetDefenseRadius( DefenseRadius )

    self.DefenseRadius = DefenseRadius or 40000
    
    self.Detection:SetAcceptRange( self.DefenseRadius ) 
  
    return self
  end
  
  
  --- Define a border area to simulate a **cold war** scenario.
  -- A **cold war** is one where Patrol aircraft patrol their territory but will not attack enemy aircraft or launch GCI aircraft unless enemy aircraft enter their territory. In other words the EWR may detect an enemy aircraft but will only send aircraft to attack it if it crosses the border.
  -- A **hot war** is one where Patrol aircraft will intercept any detected enemy aircraft and GCI aircraft will launch against detected enemy aircraft without regard for territory. In other words if the ground radar can detect the enemy aircraft then it will send Patrol and GCI aircraft to attack it.
  -- If it's a cold war then the **borders of red and blue territory** need to be defined using a @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE}. This method needs to be used for this.
  -- If a hot war is chosen then **no borders** actually need to be defined using the helicopter units other than it makes it easier sometimes for the mission maker to envisage where the red and blue territories roughly are. In a hot war the borders are effectively defined by the ground based radar coverage of a coalition. Set the noborders parameter to 1
  -- @param #AI_A2G_DISPATCHER self
  -- @param Core.Zone#ZONE_BASE BorderZone An object derived from ZONE_BASE, or a list of objects derived from ZONE_BASE.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --   -- Now Setup the A2G dispatcher, and initialize it using the Detection object.
  --   A2GDispatcher = AI_A2G_DISPATCHER:New( Detection )  
  --   
  --   -- Set one ZONE_POLYGON object as the border for the A2G dispatcher.
  --   local BorderZone = ZONE_POLYGON( "CCCP Border", GROUP:FindByName( "CCCP Border" ) ) -- The GROUP object is a late activate helicopter unit.
  --   A2GDispatcher:SetBorderZone( BorderZone )
  --   
  -- or
  --   
  --   -- Set two ZONE_POLYGON objects as the border for the A2G dispatcher.
  --   local BorderZone1 = ZONE_POLYGON( "CCCP Border1", GROUP:FindByName( "CCCP Border1" ) ) -- The GROUP object is a late activate helicopter unit.
  --   local BorderZone2 = ZONE_POLYGON( "CCCP Border2", GROUP:FindByName( "CCCP Border2" ) ) -- The GROUP object is a late activate helicopter unit.
  --   A2GDispatcher:SetBorderZone( { BorderZone1, BorderZone2 } )
  --   
  function AI_A2G_DISPATCHER:SetBorderZone( BorderZone )

    self.Detection:SetAcceptZones( BorderZone )

    return self
  end
  

  --- Display a tactical report every 30 seconds about which aircraft are:
  --   * Patrolling
  --   * Engaging
  --   * Returning
  --   * Damaged
  --   * Out of Fuel
  --   * ...
  -- @param #AI_A2G_DISPATCHER self
  -- @param #boolean TacticalDisplay Provide a value of **true** to display every 30 seconds a tactical overview.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --   -- Now Setup the A2G dispatcher, and initialize it using the Detection object.
  --   A2GDispatcher = AI_A2G_DISPATCHER:New( Detection )  
  --   
  --   -- Now Setup the Tactical Display for debug mode.
  --   A2GDispatcher:SetTacticalDisplay( true )
  --   
  function AI_A2G_DISPATCHER:SetTacticalDisplay( TacticalDisplay )
    
    self.TacticalDisplay = TacticalDisplay
    
    return self
  end  


  --- Set the default damage threshold when defenders will RTB.
  -- The default damage threshold is by default set to 40%, which means that when the aircraft is 40% damaged, it will go RTB.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number DamageThreshold A decimal number between 0 and 1, that expresses the % of damage when the aircraft will go RTB.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --   -- Now Setup the A2G dispatcher, and initialize it using the Detection object.
  --   A2GDispatcher = AI_A2G_DISPATCHER:New( Detection )  
  --   
  --   -- Now Setup the default damage threshold.
  --   A2GDispatcher:SetDefaultDamageThreshold( 0.90 ) -- Go RTB when the aircraft is 90% damaged.
  --   
  function AI_A2G_DISPATCHER:SetDefaultDamageThreshold( DamageThreshold )
    
    self.DefenderDefault.DamageThreshold = DamageThreshold
    
    return self
  end  


  --- Set the default Patrol time interval for squadrons, which will be used to determine a random Patrol timing.
  -- The default Patrol time interval is between 180 and 600 seconds.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number PatrolMinSeconds The minimum amount of seconds for the random time interval.
  -- @param #number PatrolMaxSeconds The maximum amount of seconds for the random time interval.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --   -- Now Setup the A2G dispatcher, and initialize it using the Detection object.
  --   A2GDispatcher = AI_A2G_DISPATCHER:New( Detection )  
  --   
  --   -- Now Setup the default Patrol time interval.
  --   A2GDispatcher:SetDefaultPatrolTimeInterval( 300, 1200 ) -- Between 300 and 1200 seconds.
  --   
  function AI_A2G_DISPATCHER:SetDefaultPatrolTimeInterval( PatrolMinSeconds, PatrolMaxSeconds )
    
    self.DefenderDefault.PatrolMinSeconds = PatrolMinSeconds
    self.DefenderDefault.PatrolMaxSeconds = PatrolMaxSeconds
    
    return self
  end


  --- Set the default Patrol limit for squadrons, which will be used to determine how many Patrol can be airborne at the same time for the squadron.
  -- The default Patrol limit is 1 Patrol, which means one Patrol group being spawned.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number PatrolLimit The maximum amount of Patrol that can be airborne at the same time for the squadron.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --   -- Now Setup the A2G dispatcher, and initialize it using the Detection object.
  --   A2GDispatcher = AI_A2G_DISPATCHER:New( Detection )  
  --   
  --   -- Now Setup the default Patrol limit.
  --   A2GDispatcher:SetDefaultPatrolLimit( 2 ) -- Maximum 2 Patrol per squadron.
  --   
  function AI_A2G_DISPATCHER:SetDefaultPatrolLimit( PatrolLimit )
    
    self.DefenderDefault.PatrolLimit = PatrolLimit
    
    return self
  end  


  --- Set the default engage limit for squadrons, which will be used to determine how many air units will engage at the same time with the enemy.
  -- The default eatrol limit is 1, which means one eatrol group maximum per squadron.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number EngageLimit The maximum engages that can be done at the same time per squadron.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --   -- Now Setup the A2G dispatcher, and initialize it using the Detection object.
  --   A2GDispatcher = AI_A2G_DISPATCHER:New( Detection )  
  --   
  --   -- Now Setup the default Patrol limit.
  --   A2GDispatcher:SetDefaultEngageLimit( 2 ) -- Maximum 2 engagements with the enemy per squadron.
  --   
  function AI_A2G_DISPATCHER:SetDefaultEngageLimit( EngageLimit )
    
    self.DefenderDefault.EngageLimit = EngageLimit
    
    return self
  end  


  function AI_A2G_DISPATCHER:SetIntercept( InterceptDelay )
    
    self.DefenderDefault.InterceptDelay = InterceptDelay
    
    local Detection = self.Detection -- Functional.Detection#DETECTION_AREAS
    Detection:SetIntercept( true, InterceptDelay )
    
    return self
  end  


  --- Calculates which defender friendlies are nearby the area, to help protect the area.
  -- @param #AI_A2G_DISPATCHER self
  -- @param DetectedItem
  -- @return #table A list of the defender friendlies nearby, sorted by distance.
  function AI_A2G_DISPATCHER:GetDefenderFriendliesNearBy( DetectedItem )
  
--    local DefenderFriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem )

    local DefenderFriendliesNearBy = {}
    
    local DetectionCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem )
    
    local ScanZone = ZONE_RADIUS:New( "ScanZone", DetectionCoordinate:GetVec2(), self.DefenseRadius )
    
    ScanZone:Scan( Object.Category.UNIT, { Unit.Category.AIRPLANE, Unit.Category.HELICOPTER } )
    
    local DefenderUnits = ScanZone:GetScannedUnits()
    
    for DefenderUnitID, DefenderUnit in pairs( DefenderUnits ) do
      local DefenderUnit = UNIT:FindByName( DefenderUnit:getName() )
      
      DefenderFriendliesNearBy[#DefenderFriendliesNearBy+1] = DefenderUnit
    end
    
    
    return DefenderFriendliesNearBy
  end


  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:GetDefenderTasks()
    return self.DefenderTasks or {}
  end
  

  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:GetDefenderTask( Defender )
    return self.DefenderTasks[Defender]
  end


  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:GetDefenderTaskFsm( Defender )
    return self:GetDefenderTask( Defender ).Fsm
  end
  
  
  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:GetDefenderTaskTarget( Defender )
    return self:GetDefenderTask( Defender ).Target
  end
  
  
  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:GetDefenderTaskSquadronName( Defender )
    return self:GetDefenderTask( Defender ).SquadronName
  end

  
  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:ClearDefenderTask( Defender )
    if Defender:IsAlive() and self.DefenderTasks[Defender] then
      local Target = self.DefenderTasks[Defender].Target
      local Message = "Clearing (" .. self.DefenderTasks[Defender].Type .. ") " 
      Message = Message .. Defender:GetName() 
      if Target then
        Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or ""
      end
      self:F( { Target = Message } )
    end
    self.DefenderTasks[Defender] = nil
    return self
  end

  
  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:ClearDefenderTaskTarget( Defender )
    
    local DefenderTask = self:GetDefenderTask( Defender )
    
    if Defender:IsAlive() and DefenderTask then
      local Target = DefenderTask.Target
      local Message = "Clearing (" .. DefenderTask.Type .. ") " 
      Message = Message .. Defender:GetName() 
      if Target then
        Message = Message .. ( Target and ( " from " .. Target.Index .. " [" .. Target.Set:Count() .. "]" ) ) or ""
      end
      self:F( { Target = Message } )
    end
    if Defender and DefenderTask and DefenderTask.Target then
      DefenderTask.Target = nil
    end
--    if Defender and DefenderTask then
--      if DefenderTask.Fsm:Is( "Fuel" ) 
--      or DefenderTask.Fsm:Is( "LostControl") 
--      or DefenderTask.Fsm:Is( "Damaged" ) then
--        self:ClearDefenderTask( Defender )
--      end
--    end
    return self
  end

  
  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target, Size )
  
    self:F( { SquadronName = SquadronName, Defender = Defender:GetName() } )
  
    self.DefenderTasks[Defender] = self.DefenderTasks[Defender] or {}
    self.DefenderTasks[Defender].Type = Type
    self.DefenderTasks[Defender].Fsm = Fsm
    self.DefenderTasks[Defender].SquadronName = SquadronName
    self.DefenderTasks[Defender].Size = Size

    if Target then
      self:SetDefenderTaskTarget( Defender, Target )
    end
    return self
  end
  
  
  ---
  -- @param #AI_A2G_DISPATCHER self
  -- @param Wrapper.Group#GROUP AIGroup
  function AI_A2G_DISPATCHER:SetDefenderTaskTarget( Defender, AttackerDetection )
    
    local Message = "(" .. self.DefenderTasks[Defender].Type .. ") " 
    Message = Message .. Defender:GetName() 
    Message = Message .. ( AttackerDetection and ( " target " .. AttackerDetection.Index .. " [" .. AttackerDetection.Set:Count() .. "]" ) ) or ""
    self:F( { AttackerDetection = Message } )
    if AttackerDetection then
      self.DefenderTasks[Defender].Target = AttackerDetection
    end
    return self
  end


  --- This is the main method to define Squadrons programmatically.  
  -- Squadrons:
  -- 
  --   * Have a **name or key** that is the identifier or key of the squadron.
  --   * Have **specific plane types** defined by **templates**.
  --   * Are **located at one specific airbase**. Multiple squadrons can be located at one airbase through.
  --   * Optionally have a limited set of **resources**. The default is that squadrons have unlimited resources.
  -- 
  -- The name of the squadron given acts as the **squadron key** in the AI\_A2G\_DISPATCHER:Squadron...() methods.
  -- 
  -- Additionally, squadrons have specific configuration options to:
  -- 
  --   * Control how new aircraft are **taking off** from the airfield (in the air, cold, hot, at the runway).
  --   * Control how returning aircraft are **landing** at the airfield (in the air near the airbase, after landing, after engine shutdown).
  --   * Control the **grouping** of new aircraft spawned at the airfield. If there is more than one aircraft to be spawned, these may be grouped.
  --   * Control the **overhead** or defensive strength of the squadron. Depending on the types of planes and amount of resources, the mission designer can choose to increase or reduce the amount of planes spawned.
  --   
  -- For performance and bug workaround reasons within DCS, squadrons have different methods to spawn new aircraft or land returning or damaged aircraft.
  -- 
  -- @param #AI_A2G_DISPATCHER self
  -- 
  -- @param #string SquadronName A string (text) that defines the squadron identifier or the key of the Squadron. 
  -- It can be any name, for example `"104th Squadron"` or `"SQ SQUADRON1"`, whatever. 
  -- As long as you remember that this name becomes the identifier of your squadron you have defined. 
  -- You need to use this name in other methods too!
  -- 
  -- @param #string AirbaseName The airbase name where you want to have the squadron located. 
  -- You need to specify here EXACTLY the name of the airbase as you see it in the mission editor. 
  -- Examples are `"Batumi"` or `"Tbilisi-Lochini"`. 
  -- EXACTLY the airbase name, between quotes `""`.
  -- To ease the airbase naming when using the LDT editor and IntelliSense, the @{Wrapper.Airbase#AIRBASE} class contains enumerations of the airbases of each map.
  --    
  --    * Caucasus: @{Wrapper.Airbase#AIRBASE.Caucaus}
  --    * Nevada or NTTR: @{Wrapper.Airbase#AIRBASE.Nevada}
  --    * Normandy: @{Wrapper.Airbase#AIRBASE.Normandy}
  -- 
  -- @param #string TemplatePrefixes A string or an array of strings specifying the **prefix names of the templates** (not going to explain what is templates here again). 
  -- Examples are `{ "104th", "105th" }` or `"104th"` or `"Template 1"` or `"BLUE PLANES"`. 
  -- Just remember that your template (groups late activated) need to start with the prefix you have specified in your code.
  -- If you have only one prefix name for a squadron, you don't need to use the `{ }`, otherwise you need to use the brackets.
  -- 
  -- @param #number ResourceCount (optional) A number that specifies how many resources are in stock of the squadron. If not specified, the squadron will have infinite resources available.
  -- 
  -- @usage
  --   -- Now Setup the A2G dispatcher, and initialize it using the Detection object.
  --   A2GDispatcher = AI_A2G_DISPATCHER:New( Detection )  
  --   
  -- @usage
  --   -- This will create squadron "Squadron1" at "Batumi" airbase, and will use plane types "SQ1" and has 40 planes in stock...  
  --   A2GDispatcher:SetSquadron( "Squadron1", "Batumi", "SQ1", 40 )
  --   
  -- @usage
  --   -- This will create squadron "Sq 1" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" and has 20 planes in stock...
  --   -- Note that in this implementation, the A2G dispatcher will select a random plane type when a new plane (group) needs to be spawned for defenses.
  --   -- Note the usage of the {} for the airplane templates list.
  --   A2GDispatcher:SetSquadron( "Sq 1", "Batumi", { "Mig-29", "Su-27" }, 40 )
  --   
  -- @usage
  --   -- This will create 2 squadrons "104th" and "23th" at "Batumi" airbase, and will use plane types "Mig-29" and "Su-27" respectively and each squadron has 10 planes in stock...
  --   A2GDispatcher:SetSquadron( "104th", "Batumi", "Mig-29", 10 )
  --   A2GDispatcher:SetSquadron( "23th", "Batumi", "Su-27", 10 )
  --   
  -- @usage
  --   -- This is an example like the previous, but now with infinite resources.
  --   -- The ResourceCount parameter is not given in the SetSquadron method.
  --   A2GDispatcher:SetSquadron( "104th", "Batumi", "Mig-29" )
  --   A2GDispatcher:SetSquadron( "23th", "Batumi", "Su-27" )
  --   
  --   
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadron( SquadronName, AirbaseName, TemplatePrefixes, ResourceCount )
  
    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} 

    local DefenderSquadron = self.DefenderSquadrons[SquadronName]
    
    DefenderSquadron.Name = SquadronName
    DefenderSquadron.Airbase = AIRBASE:FindByName( AirbaseName )
    DefenderSquadron.AirbaseName = DefenderSquadron.Airbase:GetName()
    if not DefenderSquadron.Airbase then
      error( "Cannot find airbase with name:" .. AirbaseName )
    end
    
    DefenderSquadron.Spawn = {}
    if type( TemplatePrefixes ) == "string" then
      local SpawnTemplate = TemplatePrefixes
      self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 )
      DefenderSquadron.Spawn[1] = self.DefenderSpawns[SpawnTemplate]
    else
      for TemplateID, SpawnTemplate in pairs( TemplatePrefixes ) do
        self.DefenderSpawns[SpawnTemplate] = self.DefenderSpawns[SpawnTemplate] or SPAWN:New( SpawnTemplate ) -- :InitCleanUp( 180 )
        DefenderSquadron.Spawn[#DefenderSquadron.Spawn+1] = self.DefenderSpawns[SpawnTemplate]
      end
    end
    DefenderSquadron.ResourceCount = ResourceCount
    DefenderSquadron.TemplatePrefixes = TemplatePrefixes
    DefenderSquadron.Captured = false -- Not captured. This flag will be set to true, when the airbase where the squadron is located, is captured.

    self:SetSquadronTakeoffInterval( SquadronName, 0 )
   
    self:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } )
    
    return self
  end
  

  --- Get an item from the Squadron table.
  -- @param #AI_A2G_DISPATCHER self
  -- @return #table
  function AI_A2G_DISPATCHER:GetSquadron( SquadronName )

    local DefenderSquadron = self.DefenderSquadrons[SquadronName]
    
    if not DefenderSquadron then
      error( "Unknown Squadron:" .. SquadronName )
    end
    
    return DefenderSquadron
  end

  --- Get a resource count from a specific squadron
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string Squadron Name of the squadron.
  -- @return #number Number of airframes available or nil if the squadron does not exist
  function AI_A2G_DISPATCHER:QuerySquadron(Squadron)
    local Squadron = self:GetSquadron(Squadron)
    if Squadron.ResourceCount then
      self:T2(string.format("%s = %s",Squadron.Name,Squadron.ResourceCount))
      return Squadron.ResourceCount
    end
    self:F({Squadron = Squadron.Name,SquadronResourceCount = Squadron.ResourceCount})
    return nil
  end
  
  --- Set the Squadron visible before startup of the dispatcher.
  -- All planes will be spawned as uncontrolled on the parking spot.
  -- They will lock the parking spot.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Set the Squadron visible before startup of dispatcher.
  --        A2GDispatcher:SetSquadronVisible( "Mineralnye" )
  --        
  -- TODO: disabling because of bug in queueing.       
--  function AI_A2G_DISPATCHER:SetSquadronVisible( SquadronName )
--  
--    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} 
--    
--    local DefenderSquadron = self:GetSquadron( SquadronName )
--    
--    DefenderSquadron.Uncontrolled = true
--    self:SetSquadronTakeoffFromParkingCold( SquadronName )
--    self:SetSquadronLandingAtEngineShutdown( SquadronName )
--
--    for SpawnTemplate, DefenderSpawn in pairs( self.DefenderSpawns ) do
--      DefenderSpawn:InitUnControlled()
--    end
--
--  end


  --- Check if the Squadron is visible before startup of the dispatcher.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @return #boolean true if visible.
  -- @usage
  -- 
  --        -- Set the Squadron visible before startup of dispatcher.
  --        local IsVisible = A2GDispatcher:IsSquadronVisible( "Mineralnye" )
  --        
  function AI_A2G_DISPATCHER:IsSquadronVisible( SquadronName )
  
    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} 
    
    local DefenderSquadron = self:GetSquadron( SquadronName )

    if DefenderSquadron then
      return DefenderSquadron.Uncontrolled == true
    end
    
    return nil
    
  end

  
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number TakeoffInterval  Only Takeoff new units each specified interval in seconds in 10 seconds steps.
  -- @usage
  -- 
  --        -- Set the Squadron Takeoff interval every 60 seconds for squadron "SQ50", which is good for a FARP cold start.
  --        A2GDispatcher:SetSquadronTakeoffInterval( "SQ50", 60 )
  --        
  function AI_A2G_DISPATCHER:SetSquadronTakeoffInterval( SquadronName, TakeoffInterval )

    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} 
    
    local DefenderSquadron = self:GetSquadron( SquadronName )

    if DefenderSquadron then
      DefenderSquadron.TakeoffInterval = TakeoffInterval or 0
      DefenderSquadron.TakeoffTime = 0
    end
    
  end
  
  
  --- Set the squadron patrol parameters for a specific task type.  
  -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods.
  -- 
  --   - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for SEAD tasks.
  --   - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for CAS tasks.
  --   - @{#AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval} for BAI tasks.
  --   
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Note that each Patrol is a group, and can consist of 1 to 4 aircraft. The default is 1 Patrol group.
  -- @param #number LowInterval (optional) The minimum time boundary in seconds when a new Patrol will be spawned. The default is 180 seconds.
  -- @param #number HighInterval (optional) The maximum time boundary in seconds when a new Patrol will be spawned. The default is 600 seconds.
  -- @param #number Probability Is not in use, you can skip this parameter.
  -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI".
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 )
  --        A2GDispatcher:SetSquadronPatrolInterval( "Mineralnye", 2, 30, 60, 1, "SEAD" )
  -- 
  function AI_A2G_DISPATCHER:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, DefenseTaskType )
  
    local DefenderSquadron = self:GetSquadron( SquadronName )

    local Patrol = DefenderSquadron[DefenseTaskType]
    if Patrol then
      Patrol.LowInterval = LowInterval or 180
      Patrol.HighInterval = HighInterval or 600
      Patrol.Probability = Probability or 1
      Patrol.PatrolLimit = PatrolLimit or 1
      Patrol.Scheduler = Patrol.Scheduler or SCHEDULER:New( self ) 
      local Scheduler = Patrol.Scheduler -- Core.Scheduler#SCHEDULER
      local ScheduleID = Patrol.ScheduleID
      local Variance = ( Patrol.HighInterval - Patrol.LowInterval ) / 2
      local Repeat = Patrol.LowInterval + Variance
      local Randomization = Variance / Repeat
      local Start = math.random( 1, Patrol.HighInterval )
      
      if ScheduleID then
        Scheduler:Stop( ScheduleID )
      end
      
      Patrol.ScheduleID = Scheduler:Schedule( self, self.SchedulerPatrol, { SquadronName }, Start, Repeat, Randomization )
    else
      error( "This squadron does not exist:" .. SquadronName )
    end

  end


  --- Set the squadron Patrol parameters for SEAD tasks.  
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Each Patrol group can consist of 1 to 4 aircraft. The default is 1 Patrol group.
  -- @param #number LowInterval (optional) The minimum time in seconds between new Patrols being spawned. The default is 180 seconds.
  -- @param #number HighInterval (optional) The maximum ttime in seconds between new Patrols being spawned. The default is 600 seconds.
  -- @param #number Probability Is not in use, you can skip this parameter.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 )
  --        A2GDispatcher:SetSquadronSeadPatrolInterval( "Mineralnye", 2, 30, 60, 1 )
  -- 
  function AI_A2G_DISPATCHER:SetSquadronSeadPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability )

    self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "SEAD" )  

  end
  
  
  --- Set the squadron Patrol parameters for CAS tasks.  
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Each Patrol group can consist of 1 to 4 aircraft. The default is 1 Patrol group.
  -- @param #number LowInterval (optional) The minimum time in seconds between new Patrols being spawned. The default is 180 seconds.
  -- @param #number HighInterval (optional) The maximum time in seconds between new Patrols being spawned. The default is 600 seconds.
  -- @param #number Probability Is not in use, you can skip this parameter.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 )
  --        A2GDispatcher:SetSquadronCasPatrolInterval( "Mineralnye", 2, 30, 60, 1 )
  -- 
  function AI_A2G_DISPATCHER:SetSquadronCasPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability )

    self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "CAS" )  

  end
  
  
  --- Set the squadron Patrol parameters for BAI tasks.  
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number PatrolLimit (optional) The maximum amount of Patrol groups to be spawned. Each Patrol group can consist of 1 to 4 aircraft. The default is 1 Patrol group.
  -- @param #number LowInterval (optional) The minimum time in seconds between new Patrols being spawned. The default is 180 seconds.
  -- @param #number HighInterval (optional) The maximum time in seconds between new Patrols being spawned. The default is 600 seconds.
  -- @param #number Probability Is not in use, you can skip this parameter.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 )
  --        A2GDispatcher:SetSquadronBaiPatrolInterval( "Mineralnye", 2, 30, 60, 1 )
  -- 
  function AI_A2G_DISPATCHER:SetSquadronBaiPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability )

    self:SetSquadronPatrolInterval( SquadronName, PatrolLimit, LowInterval, HighInterval, Probability, "BAI" )  

  end
  
  
  ---
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:GetPatrolDelay( SquadronName )
  
    self.DefenderSquadrons[SquadronName] = self.DefenderSquadrons[SquadronName] or {} 
    self.DefenderSquadrons[SquadronName].Patrol = self.DefenderSquadrons[SquadronName].Patrol or {}

    local DefenderSquadron = self:GetSquadron( SquadronName )

    local Patrol = self.DefenderSquadrons[SquadronName].Patrol
    if Patrol then
      return math.random( Patrol.LowInterval, Patrol.HighInterval )
    else
      error( "This squadron does not exist:" .. SquadronName )
    end
  end


  ---
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @return #table DefenderSquadron
  function AI_A2G_DISPATCHER:CanPatrol( SquadronName, DefenseTaskType )
    self:F({SquadronName = SquadronName})
  
    local DefenderSquadron = self:GetSquadron( SquadronName )

    if DefenderSquadron.Captured == false then -- We can only spawn new Patrol if the base has not been captured.
    
      if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0  ) then -- And, if there are sufficient resources.

        local Patrol = DefenderSquadron[DefenseTaskType]
        if Patrol and Patrol.Patrol == true then
          local PatrolCount = self:CountPatrolAirborne( SquadronName, DefenseTaskType )
          self:F( { PatrolCount = PatrolCount, PatrolLimit = Patrol.PatrolLimit, PatrolProbability = Patrol.Probability } )
          if PatrolCount < Patrol.PatrolLimit then
            local Probability = math.random()
            if Probability <= Patrol.Probability then
              return DefenderSquadron, Patrol
            end
          end
        else
          self:F( "No patrol for " .. SquadronName )
        end
      end
    end
    return nil
  end


  ---
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @return #table DefenderSquadron
  function AI_A2G_DISPATCHER:CanDefend( SquadronName, DefenseTaskType )
    self:F({SquadronName = SquadronName, DefenseTaskType})
  
    local DefenderSquadron = self:GetSquadron( SquadronName )

    if DefenderSquadron.Captured == false then -- We can only spawn new defense if the home airbase has not been captured.
    
      if ( not DefenderSquadron.ResourceCount ) or ( DefenderSquadron.ResourceCount and DefenderSquadron.ResourceCount > 0  ) then -- And, if there are sufficient resources.
        if DefenderSquadron[DefenseTaskType] and ( DefenderSquadron[DefenseTaskType].Defend == true ) then
          return DefenderSquadron, DefenderSquadron[DefenseTaskType]
        end
      end
    end
    return nil
  end


  --- Set the squadron engage limit for a specific task type.  
  -- Mission designers should not use this method, instead use the below methods. This method is used by the below methods.
  -- 
  --   - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for SEAD tasks.
  --   - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for CAS tasks.
  --   - @{#AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit} for BAI tasks.
  --   
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron.
  -- @param #string DefenseTaskType Should contain "SEAD", "CAS" or "BAI".
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronEngageLimit( "Mineralnye", 2, "SEAD" ) -- Engage maximum 2 groups with the enemy for SEAD defense.
  -- 
  function AI_A2G_DISPATCHER:SetSquadronEngageLimit( SquadronName, EngageLimit, DefenseTaskType )
  
    local DefenderSquadron = self:GetSquadron( SquadronName )

    local Defense = DefenderSquadron[DefenseTaskType]
    if Defense then
      Defense.EngageLimit = EngageLimit or 1
    else
      error( "This squadron does not exist:" .. SquadronName )
    end

  end


  --- Set a squadron to engage for suppression of air defenses, when a defense point is under attack.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the SEAD task can be executed.
  -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the SEAD task can be executed.
  -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement.
  -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement.
  -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @usage 
  -- 
  --        -- SEAD Squadron execution.
  --        A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200, 4000, 5000, "BARO" )
  --        A2GDispatcher:SetSquadronSead( "Novo", 900, 2100, 6000, 9000, "BARO" )
  --        A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200, 30, 100, "RADIO" )
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadronSead2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )
  
    local DefenderSquadron = self:GetSquadron( SquadronName )

    DefenderSquadron.SEAD = DefenderSquadron.SEAD or {}
    
    local Sead = DefenderSquadron.SEAD
    Sead.Name = SquadronName
    Sead.EngageMinSpeed = EngageMinSpeed
    Sead.EngageMaxSpeed = EngageMaxSpeed
    Sead.EngageFloorAltitude = EngageFloorAltitude or 500
    Sead.EngageCeilingAltitude = EngageCeilingAltitude or 1000
    Sead.EngageAltType = EngageAltType
    Sead.Defend = true
    
    self:T( { SEAD = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } )
    
    return self
  end


  --- Set a squadron to engage for suppression of air defenses, when a defense point is under attack.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the SEAD task can be executed.
  -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the SEAD task can be executed.
  -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement.
  -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement.
  -- @usage 
  -- 
  --        -- SEAD Squadron execution.
  --        A2GDispatcher:SetSquadronSead( "Mozdok", 900, 1200, 4000, 5000 )
  --        A2GDispatcher:SetSquadronSead( "Novo", 900, 2100, 6000, 8000 )
  --        A2GDispatcher:SetSquadronSead( "Maykop", 900, 1200, 6000, 10000 )
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadronSead( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude )

    return self:SetSquadronSead2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, "RADIO" )  
  end


  --- Set the squadron SEAD engage limit.  
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronSeadEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for SEAD defense.
  -- 
  function AI_A2G_DISPATCHER:SetSquadronSeadEngageLimit( SquadronName, EngageLimit )

    self:SetSquadronEngageLimit( SquadronName, EngageLimit, "SEAD" )  

  end
  

  --- Set a Sead patrol for a Squadron.
  -- The Sead patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed.
  -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed.
  -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed.
  -- @param #number PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed.
  -- @param #number PatrolCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed.
  -- @param #number PatrolAltType The altitude type when patrolling, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed.
  -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed.
  -- @param #number EngageFloorAltitude (optional, default = 1000m ) The minimum altitude at which the engage can be executed.
  -- @param #number EngageCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the engage can be executed.
  -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Sead Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronSeadPatrol2( "Mineralnye", PatrolZoneEast, 500, 600, 4000, 10000, "BARO", 800, 900, 2000, 3000, "RADIO", )
  --        
  function AI_A2G_DISPATCHER:SetSquadronSeadPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )
  
    local DefenderSquadron = self:GetSquadron( SquadronName )

    DefenderSquadron.SEAD = DefenderSquadron.SEAD or {}
    
    local SeadPatrol = DefenderSquadron.SEAD
    SeadPatrol.Name = SquadronName
    SeadPatrol.Zone = Zone
    SeadPatrol.PatrolFloorAltitude = PatrolFloorAltitude
    SeadPatrol.PatrolCeilingAltitude = PatrolCeilingAltitude
    SeadPatrol.EngageFloorAltitude = EngageFloorAltitude
    SeadPatrol.EngageCeilingAltitude = EngageCeilingAltitude
    SeadPatrol.PatrolMinSpeed = PatrolMinSpeed
    SeadPatrol.PatrolMaxSpeed = PatrolMaxSpeed
    SeadPatrol.EngageMinSpeed = EngageMinSpeed
    SeadPatrol.EngageMaxSpeed = EngageMaxSpeed
    SeadPatrol.PatrolAltType = PatrolAltType
    SeadPatrol.EngageAltType = EngageAltType
    SeadPatrol.Patrol = true

    self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "SEAD" )
    
    self:T( { SEAD = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } )
  end
  
  
  --- Set a Sead patrol for a Squadron.
  -- The Sead patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed.
  -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed.
  -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed.
  -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed.
  -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed.
  -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed.
  -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed.
  -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Sead Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronSeadPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 )
  --        
  function AI_A2G_DISPATCHER:SetSquadronSeadPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType )

    self:SetSquadronSeadPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, FloorAltitude, CeilingAltitude, AltType, EngageMinSpeed, EngageMaxSpeed, FloorAltitude, CeilingAltitude, AltType )
  
  end
 

  --- Set a squadron to engage for close air support, when a defense point is under attack.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the CAS task can be executed.
  -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the CAS task can be executed.
  -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement.
  -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement.
  -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @usage 
  -- 
  --        -- CAS Squadron execution.
  --        A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200, 4000, 5000, "BARO" )
  --        A2GDispatcher:SetSquadronCas( "Novo", 900, 2100, 6000, 9000, "BARO" )
  --        A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200, 30, 100, "RADIO" )
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadronCas2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )
  
    local DefenderSquadron = self:GetSquadron( SquadronName )

    DefenderSquadron.CAS = DefenderSquadron.CAS or {}
    
    local Cas = DefenderSquadron.CAS
    Cas.Name = SquadronName
    Cas.EngageMinSpeed = EngageMinSpeed
    Cas.EngageMaxSpeed = EngageMaxSpeed
    Cas.EngageFloorAltitude = EngageFloorAltitude or 500
    Cas.EngageCeilingAltitude = EngageCeilingAltitude or 1000
    Cas.EngageAltType = EngageAltType
    Cas.Defend = true
    
    self:T( { CAS = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } )
    
    return self
  end


  --- Set a squadron to engage for close air support, when a defense point is under attack.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the CAS task can be executed.
  -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the CAS task can be executed.
  -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement.
  -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement.
  -- @usage 
  -- 
  --        -- CAS Squadron execution.
  --        A2GDispatcher:SetSquadronCas( "Mozdok", 900, 1200, 4000, 5000 )
  --        A2GDispatcher:SetSquadronCas( "Novo", 900, 2100, 6000, 8000 )
  --        A2GDispatcher:SetSquadronCas( "Maykop", 900, 1200, 6000, 10000 )
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadronCas( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude )

    return self:SetSquadronCas2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, "RADIO" )  
  end


  --- Set the squadron CAS engage limit.  
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronCasEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for CAS defense.
  -- 
  function AI_A2G_DISPATCHER:SetSquadronCasEngageLimit( SquadronName, EngageLimit )

    self:SetSquadronEngageLimit( SquadronName, EngageLimit, "CAS" )  

  end


  --- Set a Cas patrol for a Squadron.
  -- The Cas patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed.
  -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed.
  -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed.
  -- @param #number PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed.
  -- @param #number PatrolCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed.
  -- @param #number PatrolAltType The altitude type when patrolling, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed.
  -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed.
  -- @param #number EngageFloorAltitude (optional, default = 1000m ) The minimum altitude at which the engage can be executed.
  -- @param #number EngageCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the engage can be executed.
  -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Cas Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronCasPatrol2( "Mineralnye", PatrolZoneEast, 500, 600, 4000, 10000, "BARO", 800, 900, 2000, 3000, "RADIO", )
  --        
  function AI_A2G_DISPATCHER:SetSquadronCasPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )
  
    local DefenderSquadron = self:GetSquadron( SquadronName )

    DefenderSquadron.CAS = DefenderSquadron.CAS or {}
    
    local CasPatrol = DefenderSquadron.CAS
    CasPatrol.Name = SquadronName
    CasPatrol.Zone = Zone
    CasPatrol.PatrolFloorAltitude = PatrolFloorAltitude
    CasPatrol.PatrolCeilingAltitude = PatrolCeilingAltitude
    CasPatrol.EngageFloorAltitude = EngageFloorAltitude
    CasPatrol.EngageCeilingAltitude = EngageCeilingAltitude
    CasPatrol.PatrolMinSpeed = PatrolMinSpeed
    CasPatrol.PatrolMaxSpeed = PatrolMaxSpeed
    CasPatrol.EngageMinSpeed = EngageMinSpeed
    CasPatrol.EngageMaxSpeed = EngageMaxSpeed
    CasPatrol.PatrolAltType = PatrolAltType
    CasPatrol.EngageAltType = EngageAltType
    CasPatrol.Patrol = true

    self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "CAS" )
    
    self:T( { CAS = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } )
  end
  
  
  --- Set a Cas patrol for a Squadron.
  -- The Cas patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed.
  -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed.
  -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed.
  -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed.
  -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed.
  -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed.
  -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed.
  -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Cas Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronCasPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 )
  --        
  function AI_A2G_DISPATCHER:SetSquadronCasPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType )

    self:SetSquadronCasPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, FloorAltitude, CeilingAltitude, AltType, EngageMinSpeed, EngageMaxSpeed, FloorAltitude, CeilingAltitude, AltType )
  
  end
  

  --- Set a squadron to engage for a battlefield area interdiction, when a defense point is under attack.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the BAI task can be executed.
  -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the BAI task can be executed.
  -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement.
  -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement.
  -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @usage 
  -- 
  --        -- BAI Squadron execution.
  --        A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200, 4000, 5000, "BARO" )
  --        A2GDispatcher:SetSquadronBai( "Novo", 900, 2100, 6000, 9000, "BARO" )
  --        A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200, 30, 100, "RADIO" )
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadronBai2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )
  
    local DefenderSquadron = self:GetSquadron( SquadronName )

    DefenderSquadron.BAI = DefenderSquadron.BAI or {}
    
    local Bai = DefenderSquadron.BAI
    Bai.Name = SquadronName
    Bai.EngageMinSpeed = EngageMinSpeed
    Bai.EngageMaxSpeed = EngageMaxSpeed
    Bai.EngageFloorAltitude = EngageFloorAltitude or 500
    Bai.EngageCeilingAltitude = EngageCeilingAltitude or 1000
    Bai.EngageAltType = EngageAltType
    Bai.Defend = true
    
    self:T( { BAI = { SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } )
    
    return self
  end


  --- Set a squadron to engage for a battlefield area interdiction, when a defense point is under attack.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the BAI task can be executed.
  -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the BAI task can be executed.
  -- @param DCS#Altitude EngageFloorAltitude (optional, default = 1000m ) The lowest altitude in meters where to execute the engagement.
  -- @param DCS#Altitude EngageCeilingAltitude (optional, default = 1500m ) The highest altitude in meters where to execute the engagement.
  -- @usage 
  -- 
  --        -- BAI Squadron execution.
  --        A2GDispatcher:SetSquadronBai( "Mozdok", 900, 1200, 4000, 5000 )
  --        A2GDispatcher:SetSquadronBai( "Novo", 900, 2100, 6000, 8000 )
  --        A2GDispatcher:SetSquadronBai( "Maykop", 900, 1200, 6000, 10000 )
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadronBai( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude )

    return self:SetSquadronBai2( SquadronName, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, "RADIO" )  
  end


  --- Set the squadron BAI engage limit.  
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageLimit The maximum amount of groups to engage with the enemy for this squadron.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronBaiEngageLimit( "Mineralnye", 2 ) -- Engage maximum 2 groups with the enemy for BAI defense.
  -- 
  function AI_A2G_DISPATCHER:SetSquadronBaiEngageLimit( SquadronName, EngageLimit )

    self:SetSquadronEngageLimit( SquadronName, EngageLimit, "BAI" )  

  end
  

  --- Set a Bai patrol for a Squadron.
  -- The Bai patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed.
  -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed.
  -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed.
  -- @param #number PatrolFloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed.
  -- @param #number PatrolCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed.
  -- @param #number PatrolAltType The altitude type when patrolling, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed.
  -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed.
  -- @param #number EngageFloorAltitude (optional, default = 1000m ) The minimum altitude at which the engage can be executed.
  -- @param #number EngageCeilingAltitude (optional, default = 1500m ) The maximum altitude at which the engage can be executed.
  -- @param #number EngageAltType The altitude type when engaging, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Bai Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronBaiPatrol2( "Mineralnye", PatrolZoneEast, 500, 600, 4000, 10000, "BARO", 800, 900, 2000, 3000, "RADIO", )
  --        
  function AI_A2G_DISPATCHER:SetSquadronBaiPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType )
  
    local DefenderSquadron = self:GetSquadron( SquadronName )

    DefenderSquadron.BAI = DefenderSquadron.BAI or {}
    
    local BaiPatrol = DefenderSquadron.BAI
    BaiPatrol.Name = SquadronName
    BaiPatrol.Zone = Zone
    BaiPatrol.PatrolFloorAltitude = PatrolFloorAltitude
    BaiPatrol.PatrolCeilingAltitude = PatrolCeilingAltitude
    BaiPatrol.EngageFloorAltitude = EngageFloorAltitude
    BaiPatrol.EngageCeilingAltitude = EngageCeilingAltitude
    BaiPatrol.PatrolMinSpeed = PatrolMinSpeed
    BaiPatrol.PatrolMaxSpeed = PatrolMaxSpeed
    BaiPatrol.EngageMinSpeed = EngageMinSpeed
    BaiPatrol.EngageMaxSpeed = EngageMaxSpeed
    BaiPatrol.PatrolAltType = PatrolAltType
    BaiPatrol.EngageAltType = EngageAltType
    BaiPatrol.Patrol = true

    self:SetSquadronPatrolInterval( SquadronName, self.DefenderDefault.PatrolLimit, self.DefenderDefault.PatrolMinSeconds, self.DefenderDefault.PatrolMaxSeconds, 1, "BAI" )
    
    self:T( { BAI = { Zone:GetName(), PatrolMinSpeed, PatrolMaxSpeed, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolAltType, EngageMinSpeed, EngageMaxSpeed, EngageFloorAltitude, EngageCeilingAltitude, EngageAltType } } )
  end
  

  --- Set a Bai patrol for a Squadron.
  -- The Bai patrol will start a patrol of the aircraft at a specified zone, and will engage when commanded.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param Core.Zone#ZONE_BASE Zone The @{Core.Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the Patrol will be executed.
  -- @param #number FloorAltitude (optional, default = 1000m ) The minimum altitude at which the cap can be executed.
  -- @param #number CeilingAltitude (optional, default = 1500m ) The maximum altitude at which the cap can be executed.
  -- @param #number PatrolMinSpeed (optional, default = 50% of max speed) The minimum speed at which the cap can be executed.
  -- @param #number PatrolMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the cap can be executed.
  -- @param #number EngageMinSpeed (optional, default = 50% of max speed) The minimum speed at which the engage can be executed.
  -- @param #number EngageMaxSpeed (optional, default = 75% of max speed) The maximum speed at which the engage can be executed.
  -- @param #number AltType The altitude type, which is a string "BARO" defining Barometric or "RADIO" defining radio controlled altitude.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --        -- Bai Patrol Squadron execution.
  --        PatrolZoneEast = ZONE_POLYGON:New( "Patrol Zone East", GROUP:FindByName( "Patrol Zone East" ) )
  --        A2GDispatcher:SetSquadronBaiPatrol( "Mineralnye", PatrolZoneEast, 4000, 10000, 500, 600, 800, 900 )
  --        
  function AI_A2G_DISPATCHER:SetSquadronBaiPatrol( SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType )

    self:SetSquadronBaiPatrol2( SquadronName, Zone, PatrolMinSpeed, PatrolMaxSpeed, FloorAltitude, CeilingAltitude, AltType, EngageMinSpeed, EngageMaxSpeed, FloorAltitude, CeilingAltitude, AltType )
  
  end
  

  --- Defines the default amount of extra planes that will takeoff as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number Overhead The % of Units that dispatching command will allocate to intercept in surplus of detected amount of units.
  -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength,
  -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles...
  -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes.
  -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: 
  -- 
  --   * Higher than 1, will increase the defense unit amounts.
  --   * Lower than 1, will decrease the defense unit amounts.
  -- 
  -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group 
  -- multiplied by the Overhead and rounded up to the smallest integer. 
  -- 
  -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution.
  -- 
  -- See example below.
  --  
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2.
  --   -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 =>  rounded up gives 3.
  --   -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes.
  --   -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6  => rounded up gives 6 planes.
  --   
  --   A2GDispatcher:SetDefaultOverhead( 1.5 )
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetDefaultOverhead( Overhead )

    self.DefenderDefault.Overhead = Overhead
    
    return self
  end


  --- Defines the amount of extra planes that will takeoff as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number Overhead The % of Units that dispatching command will allocate to intercept in surplus of detected amount of units.
  -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength,
  -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles...
  -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes.
  -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: 
  -- 
  --   * Higher than 1, will increase the defense unit amounts.
  --   * Lower than 1, will decrease the defense unit amounts.
  -- 
  -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group 
  -- multiplied by the Overhead and rounded up to the smallest integer. 
  -- 
  -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution.
  -- 
  -- See example below.
  --  
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2.
  --   -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 =>  rounded up gives 3.
  --   -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes.
  --   -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6  => rounded up gives 6 planes.
  --   
  --   A2GDispatcher:SetSquadronOverhead( "SquadronName", 1.5 )
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadronOverhead( SquadronName, Overhead )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.Overhead = Overhead
    
    return self
  end


  --- Gets the overhead of planes as part of the defense system, in comparison with the attackers.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @return #number The % of Units that dispatching command will allocate to intercept in surplus of detected amount of units.
  -- The default overhead is 1, so equal balance. The @{#AI_A2G_DISPATCHER.SetOverhead}() method can be used to tweak the defense strength,
  -- taking into account the plane types of the squadron. For example, a MIG-31 with full long-distance A2G missiles payload, may still be less effective than a F-15C with short missiles...
  -- So in this case, one may want to use the Overhead method to allocate more defending planes as the amount of detected attacking planes.
  -- The overhead must be given as a decimal value with 1 as the neutral value, which means that Overhead values: 
  -- 
  --   * Higher than 1, will increase the defense unit amounts.
  --   * Lower than 1, will decrease the defense unit amounts.
  -- 
  -- The amount of defending units is calculated by multiplying the amount of detected attacking planes as part of the detected group 
  -- multiplied by the Overhead and rounded up to the smallest integer. 
  -- 
  -- The Overhead value set for a Squadron, can be programmatically adjusted (by using this SetOverhead method), to adjust the defense overhead during mission execution.
  -- 
  -- See example below.
  --  
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- An overhead of 1,5 with 1 planes detected, will allocate 2 planes ( 1 * 1,5 ) = 1,5 => rounded up gives 2.
  --   -- An overhead of 1,5 with 2 planes detected, will allocate 3 planes ( 2 * 1,5 ) = 3 =>  rounded up gives 3.
  --   -- An overhead of 1,5 with 3 planes detected, will allocate 5 planes ( 3 * 1,5 ) = 4,5 => rounded up gives 5 planes.
  --   -- An overhead of 1,5 with 4 planes detected, will allocate 6 planes ( 4 * 1,5 ) = 6  => rounded up gives 6 planes.
  --   
  --   local SquadronOverhead = A2GDispatcher:GetSquadronOverhead( "SquadronName" )
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:GetSquadronOverhead( SquadronName )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    return DefenderSquadron.Overhead or self.DefenderDefault.Overhead
  end


  --- Sets the default grouping of new aircraft spawned.
  -- Grouping will trigger how new aircraft will be grouped if more than one aircraft is spawned for defense.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number Grouping The level of grouping that will be applied for the Patrol.
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Set a grouping by default per 2 aircraft.
  --   A2GDispatcher:SetDefaultGrouping( 2 )
  -- 
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetDefaultGrouping( Grouping )
  
    self.DefenderDefault.Grouping = Grouping
    
    return self
  end


  --- Sets the Squadron grouping of new aircraft spawned.
  -- Grouping will trigger how new aircraft will be grouped if more than one aircraft is spawned for defense.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number Grouping The level of grouping that will be applied for a Patrol from the Squadron.
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Set a Squadron specific grouping per 2 aircraft.
  --   A2GDispatcher:SetSquadronGrouping( "SquadronName", 2 )
  -- 
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadronGrouping( SquadronName, Grouping )
  
    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.Grouping = Grouping
    
    return self
  end


  --- Sets the engage probability if the squadron will engage on a detected target.
  -- This can be configured per squadron, to ensure that each squadron as a specific defensive probability setting.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number EngageProbability The probability when the squadron will consider to engage the detected target. 
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Set an defense probability for squadron SquadronName of 50%.
  --   -- This will result that this squadron has 50% chance to engage on a detected target.
  --   A2GDispatcher:SetSquadronEngageProbability( "SquadronName", 0.5 )
  -- 
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadronEngageProbability( SquadronName, EngageProbability )
  
    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.EngageProbability = EngageProbability
    
    return self
  end


  --- Defines the default method at which new flights will spawn and takeoff as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway.
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights by default takeoff in the air.
  --   A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Air )
  --   
  --   -- Let new flights by default takeoff from the runway.
  --   A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Runway )
  --   
  --   -- Let new flights by default takeoff from the airbase hot.
  --   A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Hot )
  -- 
  --   -- Let new flights by default takeoff from the airbase cold.
  --   A2GDispatcher:SetDefaultTakeoff( AI_A2G_Dispatcher.Takeoff.Cold )
  -- 
  -- 
  -- @return #AI_A2G_DISPATCHER
  -- 
  function AI_A2G_DISPATCHER:SetDefaultTakeoff( Takeoff )

    self.DefenderDefault.Takeoff = Takeoff
    
    return self
  end

  --- Defines the method at which new flights will spawn and takeoff as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway.
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights takeoff in the air.
  --   A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Air )
  --   
  --   -- Let new flights takeoff from the runway.
  --   A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Runway )
  --   
  --   -- Let new flights takeoff from the airbase hot.
  --   A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Hot )
  -- 
  --   -- Let new flights takeoff from the airbase cold.
  --   A2GDispatcher:SetSquadronTakeoff( "SquadronName", AI_A2G_Dispatcher.Takeoff.Cold )
  -- 
  -- 
  -- @return #AI_A2G_DISPATCHER
  -- 
  function AI_A2G_DISPATCHER:SetSquadronTakeoff( SquadronName, Takeoff )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.Takeoff = Takeoff
    
    return self
  end
  

  --- Gets the default method at which new flights will spawn and takeoff as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway.
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights by default takeoff in the air.
  --   local TakeoffMethod = A2GDispatcher:GetDefaultTakeoff()
  --   if TakeoffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then
  --     ...
  --   end
  --   
  function AI_A2G_DISPATCHER:GetDefaultTakeoff( )

    return self.DefenderDefault.Takeoff
  end
  
  --- Gets the method at which new flights will spawn and takeoff as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @return #number Takeoff From the airbase hot, from the airbase cold, in the air, from the runway.
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights takeoff in the air.
  --   local TakeoffMethod = A2GDispatcher:GetSquadronTakeoff( "SquadronName" )
  --   if TakeoffMethod == , AI_A2G_Dispatcher.Takeoff.InAir then
  --     ...
  --   end
  --   
  function AI_A2G_DISPATCHER:GetSquadronTakeoff( SquadronName )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    return DefenderSquadron.Takeoff or self.DefenderDefault.Takeoff
  end
  

  --- Sets flights to default takeoff in the air, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights by default takeoff in the air.
  --   A2GDispatcher:SetDefaultTakeoffInAir()
  --   
  -- @return #AI_A2G_DISPATCHER
  -- 
  function AI_A2G_DISPATCHER:SetDefaultTakeoffInAir()

    self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Air )
    
    return self
  end

  
  --- Sets flights to takeoff in the air, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number TakeoffAltitude (optional) The altitude in meters above the ground. If not given, the default takeoff altitude will be used.
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights takeoff in the air.
  --   A2GDispatcher:SetSquadronTakeoffInAir( "SquadronName" )
  --   
  -- @return #AI_A2G_DISPATCHER
  -- 
  function AI_A2G_DISPATCHER:SetSquadronTakeoffInAir( SquadronName, TakeoffAltitude )

    self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Air )
    
    if TakeoffAltitude then
      self:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude )
    end
    
    return self
  end


  --- Sets flights by default to takeoff from the runway, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights by default takeoff from the runway.
  --   A2GDispatcher:SetDefaultTakeoffFromRunway()
  --   
  -- @return #AI_A2G_DISPATCHER
  -- 
  function AI_A2G_DISPATCHER:SetDefaultTakeoffFromRunway()

    self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Runway )
    
    return self
  end

  
  --- Sets flights to takeoff from the runway, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights takeoff from the runway.
  --   A2GDispatcher:SetSquadronTakeoffFromRunway( "SquadronName" )
  --   
  -- @return #AI_A2G_DISPATCHER
  -- 
  function AI_A2G_DISPATCHER:SetSquadronTakeoffFromRunway( SquadronName )

    self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Runway )
    
    return self
  end
  

  --- Sets flights by default to takeoff from the airbase at a hot location, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights by default takeoff at a hot parking spot.
  --   A2GDispatcher:SetDefaultTakeoffFromParkingHot()
  --   
  -- @return #AI_A2G_DISPATCHER
  -- 
  function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingHot()

    self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Hot )
    
    return self
  end

  --- Sets flights to takeoff from the airbase at a hot location, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights takeoff in the air.
  --   A2GDispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" )
  --   
  -- @return #AI_A2G_DISPATCHER
  -- 
  function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingHot( SquadronName )

    self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Hot )
    
    return self
  end
  
  
  --- Sets flights to by default takeoff from the airbase at a cold location, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights takeoff from a cold parking spot.
  --   A2GDispatcher:SetDefaultTakeoffFromParkingCold()
  --   
  -- @return #AI_A2G_DISPATCHER
  -- 
  function AI_A2G_DISPATCHER:SetDefaultTakeoffFromParkingCold()

    self:SetDefaultTakeoff( AI_A2G_DISPATCHER.Takeoff.Cold )
    
    return self
  end
  

  --- Sets flights to takeoff from the airbase at a cold location, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights takeoff from a cold parking spot.
  --   A2GDispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" )
  --   
  -- @return #AI_A2G_DISPATCHER
  -- 
  function AI_A2G_DISPATCHER:SetSquadronTakeoffFromParkingCold( SquadronName )

    self:SetSquadronTakeoff( SquadronName, AI_A2G_DISPATCHER.Takeoff.Cold )
    
    return self
  end
  

  --- Defines the default altitude where aircraft will spawn in the air and takeoff as part of the defense system, when the takeoff in the air method has been selected.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number TakeoffAltitude The altitude in meters above ground level (AGL).
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Set the default takeoff altitude when taking off in the air.
  --   A2GDispatcher:SetDefaultTakeoffInAirAltitude( 2000 )  -- This makes planes start at 2000 meters above the ground.
  -- 
  -- @return #AI_A2G_DISPATCHER
  -- 
  function AI_A2G_DISPATCHER:SetDefaultTakeoffInAirAltitude( TakeoffAltitude )

    self.DefenderDefault.TakeoffAltitude = TakeoffAltitude
    
    return self
  end

  --- Defines the default altitude where aircraft will spawn in the air and takeoff as part of the defense system, when the takeoff in the air method has been selected.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number TakeoffAltitude The altitude in meters above ground level (AGL).
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Set the default takeoff altitude when taking off in the air.
  --   A2GDispatcher:SetSquadronTakeoffInAirAltitude( "SquadronName", 2000 ) -- This makes aircraft start at 2000 meters above ground level (AGL).
  --   
  -- @return #AI_A2G_DISPATCHER
  -- 
  function AI_A2G_DISPATCHER:SetSquadronTakeoffInAirAltitude( SquadronName, TakeoffAltitude )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.TakeoffAltitude = TakeoffAltitude
    
    return self
  end
  

  --- Defines the default method at which flights will land and despawn as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights by default despawn near the airbase when returning.
  --   A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.NearAirbase )
  --   
  --   -- Let new flights by default despawn after landing land at the runway.
  --   A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtRunway )
  --   
  --   -- Let new flights by default despawn after landing and parking, and after engine shutdown.
  --   A2GDispatcher:SetDefaultLanding( AI_A2G_Dispatcher.Landing.AtEngineShutdown )
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetDefaultLanding( Landing )

    self.DefenderDefault.Landing = Landing
    
    return self
  end
  

  --- Defines the method at which flights will land and despawn as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights despawn near the airbase when returning.
  --   A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.NearAirbase )
  --   
  --   -- Let new flights despawn after landing land at the runway.
  --   A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtRunway )
  --   
  --   -- Let new flights despawn after landing and parking, and after engine shutdown.
  --   A2GDispatcher:SetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.AtEngineShutdown )
  -- 
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadronLanding( SquadronName, Landing )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.Landing = Landing
    
    return self
  end
  

  --- Gets the default method at which flights will land and despawn as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights by default despawn near the airbase when returning.
  --   local LandingMethod = A2GDispatcher:GetDefaultLanding()
  --   if LandingMethod == AI_A2G_Dispatcher.Landing.NearAirbase then
  --    ...
  --   end
  -- 
  function AI_A2G_DISPATCHER:GetDefaultLanding()

    return self.DefenderDefault.Landing
  end
  

  --- Gets the method at which flights will land and despawn as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @return #number Landing The landing method which can be NearAirbase, AtRunway, AtEngineShutdown
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let new flights despawn near the airbase when returning.
  --   local LandingMethod = A2GDispatcher:GetSquadronLanding( "SquadronName", AI_A2G_Dispatcher.Landing.NearAirbase )
  --   if LandingMethod == AI_A2G_Dispatcher.Landing.NearAirbase then
  --    ...
  --   end
  -- 
  function AI_A2G_DISPATCHER:GetSquadronLanding( SquadronName )

    local DefenderSquadron = self:GetSquadron( SquadronName )
    return DefenderSquadron.Landing or self.DefenderDefault.Landing
  end
  

  --- Sets flights by default to land and despawn near the airbase in the air, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let flights by default to land near the airbase and despawn.
  --   A2GDispatcher:SetDefaultLandingNearAirbase()
  --   
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetDefaultLandingNearAirbase()

    self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.NearAirbase )
    
    return self
  end
  

  --- Sets flights to land and despawn near the airbase in the air, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let flights to land near the airbase and despawn.
  --   A2GDispatcher:SetSquadronLandingNearAirbase( "SquadronName" )
  --   
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadronLandingNearAirbase( SquadronName )

    self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.NearAirbase )
    
    return self
  end
  

  --- Sets flights by default to land and despawn at the runway, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let flights by default land at the runway and despawn.
  --   A2GDispatcher:SetDefaultLandingAtRunway()
  --   
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetDefaultLandingAtRunway()

    self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.AtRunway )
    
    return self
  end
  

  --- Sets flights to land and despawn at the runway, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let flights land at the runway and despawn.
  --   A2GDispatcher:SetSquadronLandingAtRunway( "SquadronName" )
  --   
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadronLandingAtRunway( SquadronName )

    self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.AtRunway )
    
    return self
  end
  

  --- Sets flights by default to land and despawn at engine shutdown, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let flights by default land and despawn at engine shutdown.
  --   A2GDispatcher:SetDefaultLandingAtEngineShutdown()
  --   
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetDefaultLandingAtEngineShutdown()

    self:SetDefaultLanding( AI_A2G_DISPATCHER.Landing.AtEngineShutdown )
    
    return self
  end
  

  --- Sets flights to land and despawn at engine shutdown, as part of the defense system.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @usage:
  -- 
  --   local A2GDispatcher = AI_A2G_DISPATCHER:New( ... )
  --   
  --   -- Let flights land and despawn at engine shutdown.
  --   A2GDispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" )
  --   
  -- @return #AI_A2G_DISPATCHER
  function AI_A2G_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName )

    self:SetSquadronLanding( SquadronName, AI_A2G_DISPATCHER.Landing.AtEngineShutdown )
    
    return self
  end
  
  --- Set the default fuel threshold when defenders will RTB or Refuel in the air.
  -- The fuel threshold is by default set to 15%, which means that an aircraft will stay in the air until 15% of its fuel is remaining.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the % of the threshold of fuel remaining in the tank when the plane will go RTB or Refuel.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --   -- Now Setup the A2G dispatcher, and initialize it using the Detection object.
  --   A2GDispatcher = AI_A2G_DISPATCHER:New( Detection )  
  --   
  --   -- Now Setup the default fuel threshold.
  --   A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank.
  --   
  function AI_A2G_DISPATCHER:SetDefaultFuelThreshold( FuelThreshold )
    
    self.DefenderDefault.FuelThreshold = FuelThreshold
    
    return self
  end  


  --- Set the fuel threshold for the squadron when defenders will RTB or Refuel in the air.
  -- The fuel threshold is by default set to 15%, which means that an aircraft will stay in the air until 15% of its fuel is remaining.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number FuelThreshold A decimal number between 0 and 1, that expresses the % of the threshold of fuel remaining in the tank when the plane will go RTB or Refuel.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --   -- Now Setup the A2G dispatcher, and initialize it using the Detection object.
  --   A2GDispatcher = AI_A2G_DISPATCHER:New( Detection )  
  --   
  --   -- Now Setup the default fuel threshold.
  --   A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank.
  --   
  function AI_A2G_DISPATCHER:SetSquadronFuelThreshold( SquadronName, FuelThreshold )
    
    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.FuelThreshold = FuelThreshold
    
    return self
  end  

  --- Set the default tanker where defenders will Refuel in the air.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --   -- Now Setup the A2G dispatcher, and initialize it using the Detection object.
  --   A2GDispatcher = AI_A2G_DISPATCHER:New( Detection )  
  --   
  --   -- Now Setup the default fuel threshold.
  --   A2GDispatcher:SetDefaultFuelThreshold( 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank.
  --   
  --   -- Now Setup the default tanker.
  --   A2GDispatcher:SetDefaultTanker( "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor.
  function AI_A2G_DISPATCHER:SetDefaultTanker( TankerName )
    
    self.DefenderDefault.TankerName = TankerName
    
    return self
  end  


  --- Set the squadron tanker where defenders will Refuel in the air.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #string TankerName A string defining the group name of the Tanker as defined within the Mission Editor.
  -- @return #AI_A2G_DISPATCHER
  -- @usage
  -- 
  --   -- Now Setup the A2G dispatcher, and initialize it using the Detection object.
  --   A2GDispatcher = AI_A2G_DISPATCHER:New( Detection )  
  --   
  --   -- Now Setup the squadron fuel threshold.
  --   A2GDispatcher:SetSquadronRefuelThreshold( "SquadronName", 0.30 ) -- Go RTB when only 30% of fuel remaining in the tank.
  --   
  --   -- Now Setup the squadron tanker.
  --   A2GDispatcher:SetSquadronTanker( "SquadronName", "Tanker" ) -- The group name of the tanker is "Tanker" in the Mission Editor.
  function AI_A2G_DISPATCHER:SetSquadronTanker( SquadronName, TankerName )
    
    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.TankerName = TankerName
    
    return self
  end  


  --- Set the frequency of communication and the mode of communication for voice overs.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The name of the squadron.
  -- @param #number RadioFrequency The frequency of communication.
  -- @param #number RadioModulation The modulation of communication.
  -- @param #number RadioPower The power in Watts of communication.
  function AI_A2G_DISPATCHER:SetSquadronRadioFrequency( SquadronName, RadioFrequency, RadioModulation, RadioPower )
  
    local DefenderSquadron = self:GetSquadron( SquadronName )
    DefenderSquadron.RadioFrequency = RadioFrequency
    DefenderSquadron.RadioModulation = RadioModulation or radio.modulation.AM 
    DefenderSquadron.RadioPower = RadioPower or 100

    if DefenderSquadron.RadioQueue then
      DefenderSquadron.RadioQueue:Stop()
    end

    DefenderSquadron.RadioQueue = nil

    DefenderSquadron.RadioQueue = RADIOSPEECH:New( DefenderSquadron.RadioFrequency, DefenderSquadron.RadioModulation )
    DefenderSquadron.RadioQueue.power = DefenderSquadron.RadioPower
    DefenderSquadron.RadioQueue:Start( 0.5 )
    
    DefenderSquadron.RadioQueue:SetLanguage( DefenderSquadron.Language )
  end 


  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:AddDefenderToSquadron( Squadron, Defender, Size )
    self.Defenders = self.Defenders or {}
    local DefenderName = Defender:GetName()
    self.Defenders[ DefenderName ] = Squadron
    if Squadron.ResourceCount then
      Squadron.ResourceCount = Squadron.ResourceCount - Size
    end
    self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } )
  end

  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:RemoveDefenderFromSquadron( Squadron, Defender )
    self.Defenders = self.Defenders or {}
    local DefenderName = Defender:GetName()
    if Squadron.ResourceCount then
      Squadron.ResourceCount = Squadron.ResourceCount + Defender:GetSize()
    end
    self.Defenders[ DefenderName ] = nil
    self:F( { DefenderName = DefenderName, SquadronResourceCount = Squadron.ResourceCount } )
  end
  
  function AI_A2G_DISPATCHER:GetSquadronFromDefender( Defender )
    self.Defenders = self.Defenders or {}
    local DefenderName = Defender:GetName()
    self:F( { DefenderName = DefenderName } )
    return self.Defenders[ DefenderName ] 
  end

  
  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:CountPatrolAirborne( SquadronName, DefenseTaskType )

    local PatrolCount = 0
    
    local DefenderSquadron = self.DefenderSquadrons[SquadronName]
    if DefenderSquadron then
      for AIGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do
        if DefenderTask.SquadronName == SquadronName then
          if DefenderTask.Type == DefenseTaskType then
            if AIGroup:IsAlive() then
              -- Check if the Patrol is patrolling or engaging. If not, this is not a valid Patrol, even if it is alive!
              -- The Patrol could be damaged, lost control, or out of fuel!
              if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" )
                    or DefenderTask.Fsm:Is( "Started" ) then
                PatrolCount = PatrolCount + 1
              end
            end
          end
        end
      end
    end

    return PatrolCount
  end
  
  
  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:CountDefendersEngaged( AttackerDetection, AttackerCount )

    -- First, count the active AIGroups Units, targeting the DetectedSet
    local DefendersEngaged = 0
    local DefendersTotal = 0
    
    local AttackerSet = AttackerDetection.Set
    local DefendersMissing = AttackerCount
    --DetectedSet:Flush()
    
    local DefenderTasks = self:GetDefenderTasks()
    for DefenderGroup, DefenderTask in pairs( DefenderTasks ) do
      local Defender = DefenderGroup -- Wrapper.Group#GROUP
      local DefenderTaskTarget = DefenderTask.Target
      local DefenderSquadronName = DefenderTask.SquadronName
      local DefenderSize = DefenderTask.Size

      -- Count the total of defenders on the battlefield.
      --local DefenderSize = Defender:GetInitialSize()
      if DefenderTask.Target then
        --if DefenderTask.Fsm:Is( "Engaging" ) then
          self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize )
          DefendersTotal = DefendersTotal + DefenderSize
          if DefenderTaskTarget and DefenderTaskTarget.Index == AttackerDetection.Index then
          
            local SquadronOverhead = self:GetSquadronOverhead( DefenderSquadronName )
            self:F( { SquadronOverhead = SquadronOverhead } )
            if DefenderSize then
              DefendersEngaged = DefendersEngaged + DefenderSize
              DefendersMissing = DefendersMissing - DefenderSize / SquadronOverhead
              self:F( "Defender Group Name: " .. Defender:GetName() .. ", Size: " .. DefenderSize )
            else
              DefendersEngaged = 0
            end
          end
        --end
      end

      
    end

    for QueueID, QueueItem in pairs( self.DefenseQueue ) do
      local QueueItem = QueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem
      if QueueItem.AttackerDetection and QueueItem.AttackerDetection.ItemID == AttackerDetection.ItemID then
        DefendersMissing = DefendersMissing - QueueItem.DefendersNeeded / QueueItem.DefenderSquadron.Overhead
        --DefendersEngaged = DefendersEngaged + QueueItem.DefenderGrouping
        self:F( { QueueItemName = QueueItem.Defense, QueueItem_ItemID = QueueItem.AttackerDetection.ItemID, DetectedItem = AttackerDetection.ItemID, DefendersMissing = DefendersMissing } )
      end
    end

    self:F( { DefenderCount = DefendersEngaged } )

    return DefendersTotal, DefendersEngaged, DefendersMissing
  end
  
  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:CountDefenders( AttackerDetection, DefenderCount, DefenderTaskType )
  
    local Friendlies = nil

    local AttackerSet = AttackerDetection.Set
    local AttackerCount = AttackerSet:Count()

    local DefenderFriendlies = self:GetDefenderFriendliesNearBy( AttackerDetection )
    
    for FriendlyDistance, DefenderFriendlyUnit in UTILS.spairs( DefenderFriendlies or {} ) do
      -- We only allow to engage targets as long as the units on both sides are balanced.
      if AttackerCount > DefenderCount then 
        local FriendlyGroup = DefenderFriendlyUnit:GetGroup() -- Wrapper.Group#GROUP
        if FriendlyGroup and FriendlyGroup:IsAlive() then
          -- Ok, so we have a friendly near the potential target.
          -- Now we need to check if the AIGroup has a Task.
          local DefenderTask = self:GetDefenderTask( FriendlyGroup )
          if DefenderTask then
            -- The Task should be of the same type.
            if DefenderTaskType == DefenderTask.Type then 
              -- If there is no target, then add the AIGroup to the ResultAIGroups for Engagement to the AttackerSet
              if DefenderTask.Target == nil then
                if DefenderTask.Fsm:Is( "Returning" )
                or DefenderTask.Fsm:Is( "Patrolling" ) then
                  Friendlies = Friendlies or {}
                  Friendlies[FriendlyGroup] = FriendlyGroup
                  DefenderCount = DefenderCount + FriendlyGroup:GetSize()
                  self:F( { Friendly = FriendlyGroup:GetName(), FriendlyDistance = FriendlyDistance } )
                end
              end
            end
          end 
        end
      else
        break
      end
    end
    
    return Friendlies
  end


  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:ResourceActivate( DefenderSquadron, DefendersNeeded )
  
    local SquadronName = DefenderSquadron.Name
    DefendersNeeded = DefendersNeeded or 4
    local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping
    DefenderGrouping = ( DefenderGrouping < DefendersNeeded ) and DefenderGrouping or DefendersNeeded
    
    if self:IsSquadronVisible( SquadronName ) then
    
      -- Here we Patrol the new planes.
      -- The Resources table is filled in advance.
      local TemplateID = math.random( 1, #DefenderSquadron.Spawn ) -- Choose the template.
  
      -- We determine the grouping based on the parameters set.
      self:F( { DefenderGrouping = DefenderGrouping } )
      
      -- New we will form the group to spawn in.
      -- We search for the first free resource matching the template.
      local DefenderUnitIndex = 1
      local DefenderPatrolTemplate = nil
      local DefenderName = nil
      for GroupName, DefenderGroup in pairs( DefenderSquadron.Resources[TemplateID] or {} ) do
        self:F( { GroupName = GroupName } )
        local DefenderTemplate = _DATABASE:GetGroupTemplate( GroupName )
        if DefenderUnitIndex == 1 then
          DefenderPatrolTemplate = UTILS.DeepCopy( DefenderTemplate )
          self.DefenderPatrolIndex = self.DefenderPatrolIndex + 1
          --DefenderPatrolTemplate.name = SquadronName .. "#" .. self.DefenderPatrolIndex .. "#" .. GroupName
          DefenderPatrolTemplate.name = GroupName
          DefenderName = DefenderPatrolTemplate.name
        else
          -- Add the unit in the template to the DefenderPatrolTemplate.
          local DefenderUnitTemplate = DefenderTemplate.units[1]
          DefenderPatrolTemplate.units[DefenderUnitIndex] = DefenderUnitTemplate
        end
        DefenderPatrolTemplate.units[DefenderUnitIndex].name = string.format( DefenderPatrolTemplate.name .. '-%02d', DefenderUnitIndex )
        DefenderPatrolTemplate.units[DefenderUnitIndex].unitId = nil
        DefenderUnitIndex = DefenderUnitIndex + 1
        DefenderSquadron.Resources[TemplateID][GroupName] = nil
        if DefenderUnitIndex > DefenderGrouping then
          break
        end
        
      end 
      
      if DefenderPatrolTemplate then
        local TakeoffMethod = self:GetSquadronTakeoff( SquadronName )
        local SpawnGroup = GROUP:Register( DefenderName )
        DefenderPatrolTemplate.lateActivation = nil
        DefenderPatrolTemplate.uncontrolled = nil
        local Takeoff = self:GetSquadronTakeoff( SquadronName )
        DefenderPatrolTemplate.route.points[1].type   = GROUPTEMPLATE.Takeoff[Takeoff][1] -- type
        DefenderPatrolTemplate.route.points[1].action = GROUPTEMPLATE.Takeoff[Takeoff][2] -- action
        local Defender = _DATABASE:Spawn( DefenderPatrolTemplate )
        self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping )
        Defender:Activate()
        return Defender, DefenderGrouping
      end
    else
      local Spawn = DefenderSquadron.Spawn[ math.random( 1, #DefenderSquadron.Spawn ) ] -- Core.Spawn#SPAWN
      if DefenderGrouping then
        Spawn:InitGrouping( DefenderGrouping )
      else
        Spawn:InitGrouping()
      end
      
      local TakeoffMethod = self:GetSquadronTakeoff( SquadronName )
      local Defender = Spawn:SpawnAtAirbase( DefenderSquadron.Airbase, TakeoffMethod, DefenderSquadron.TakeoffAltitude or self.DefenderDefault.TakeoffAltitude ) -- Wrapper.Group#GROUP
      self:AddDefenderToSquadron( DefenderSquadron, Defender, DefenderGrouping )
      return Defender, DefenderGrouping
    end

    return nil, nil
  end
  
  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:onafterPatrol( From, Event, To, SquadronName, DefenseTaskType )
  
    local DefenderSquadron, Patrol = self:CanPatrol( SquadronName, DefenseTaskType )
    
    -- Determine if there are sufficient resources to form a complete group for patrol.
    if DefenderSquadron then    
      local DefendersNeeded
      local DefendersGrouping = ( DefenderSquadron.Grouping or self.DefenderDefault.Grouping )
      if DefenderSquadron.ResourceCount == nil then
        DefendersNeeded = DefendersGrouping
      else
        if DefenderSquadron.ResourceCount >= DefendersGrouping then
          DefendersNeeded = DefendersGrouping 
        else
          DefendersNeeded = DefenderSquadron.ResourceCount
        end
      end
      
      if Patrol then
        self:ResourceQueue( true, DefenderSquadron, DefendersNeeded, Patrol, DefenseTaskType, nil, SquadronName )
      end
    end
    
  end

  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:ResourceQueue( Patrol, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName )

  self:F( { DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName } )

    local DefenseQueueItem = {} -- #AI_A2G_DISPATCHER.DefenderQueueItem


    DefenseQueueItem.Patrol = Patrol
    DefenseQueueItem.DefenderSquadron = DefenderSquadron
    DefenseQueueItem.DefendersNeeded = DefendersNeeded
    DefenseQueueItem.Defense = Defense
    DefenseQueueItem.DefenseTaskType = DefenseTaskType
    DefenseQueueItem.AttackerDetection = AttackerDetection
    DefenseQueueItem.SquadronName  = SquadronName
    
    table.insert( self.DefenseQueue, DefenseQueueItem )
    self:F( { QueueItems = #self.DefenseQueue } )

  end
  
  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:ResourceTakeoff()

    for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do
      self:F( { DefenseQueueID } )
    end
  
    for SquadronName, Squadron in pairs( self.DefenderSquadrons ) do
      
      if #self.DefenseQueue > 0 then

        self:F( { SquadronName, Squadron.Name, Squadron.TakeoffTime, Squadron.TakeoffInterval, timer.getTime() } )
      
        local DefenseQueueItem = self.DefenseQueue[1]
        self:F( {DefenderSquadron=DefenseQueueItem.DefenderSquadron} )
        
        if DefenseQueueItem.SquadronName == SquadronName then

          if Squadron.TakeoffTime + Squadron.TakeoffInterval < timer.getTime() then
            Squadron.TakeoffTime = timer.getTime()
  
            if DefenseQueueItem.Patrol == true then
              self:ResourcePatrol( DefenseQueueItem.DefenderSquadron, DefenseQueueItem.DefendersNeeded, DefenseQueueItem.Defense, DefenseQueueItem.DefenseTaskType, DefenseQueueItem.AttackerDetection, DefenseQueueItem.SquadronName )
            else
              self:ResourceEngage( DefenseQueueItem.DefenderSquadron, DefenseQueueItem.DefendersNeeded, DefenseQueueItem.Defense, DefenseQueueItem.DefenseTaskType, DefenseQueueItem.AttackerDetection, DefenseQueueItem.SquadronName )
            end
            table.remove( self.DefenseQueue, 1 )
          end
        end
      end
      
    end
  
  end

  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:ResourcePatrol( DefenderSquadron, DefendersNeeded, Patrol, DefenseTaskType, AttackerDetection, SquadronName )


    self:F({DefenderSquadron=DefenderSquadron})
    self:F({DefendersNeeded=DefendersNeeded})
    self:F({Patrol=Patrol})
    self:F({DefenseTaskType=DefenseTaskType})
    self:F({AttackerDetection=AttackerDetection})
    self:F({SquadronName=SquadronName})
    
    local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded )    

    if DefenderGroup then

      local AI_A2G_PATROL = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS }
      
      local AI_A2G_Fsm = AI_A2G_PATROL[DefenseTaskType]:New2( DefenderGroup, Patrol.EngageMinSpeed, Patrol.EngageMaxSpeed, Patrol.EngageFloorAltitude, Patrol.EngageCeilingAltitude, Patrol.EngageAltType, Patrol.Zone, Patrol.PatrolFloorAltitude, Patrol.PatrolCeilingAltitude, Patrol.PatrolMinSpeed, Patrol.PatrolMaxSpeed, Patrol.PatrolAltType )
      AI_A2G_Fsm:SetDispatcher( self )
      AI_A2G_Fsm:SetHomeAirbase( DefenderSquadron.Airbase )
      AI_A2G_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 )
      AI_A2G_Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold )
      AI_A2G_Fsm:SetDisengageRadius( self.DisengageRadius )
      AI_A2G_Fsm:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName )
      AI_A2G_Fsm:Start()

      self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, AI_A2G_Fsm, nil, DefenderGrouping )
      
      function AI_A2G_Fsm:onafterTakeoff( DefenderGroup, From, Event, To )
        self:F({"Takeoff", DefenderGroup:GetName()})
        --self:GetParent(self).onafterBirth( self, Defender, From, Event, To )
        
        local DefenderName = DefenderGroup:GetCallsign() -- #string
        local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER
        local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
        
        if Squadron then
          if self.SetSendPlayerMessages then
            Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", wheels up.", DefenderGroup )
          end
          AI_A2G_Fsm:Patrol() -- Engage on the TargetSetUnit
        end
      end

      function AI_A2G_Fsm:onafterPatrolRoute( DefenderGroup, From, Event, To )
        self:F({"PatrolRoute", DefenderGroup:GetName()})
        self:GetParent(self).onafterPatrolRoute( self, DefenderGroup, From, Event, To )
        
        local DefenderName = DefenderGroup:GetCallsign()
        local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER
        local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
        if Squadron and self.SetSendPlayerMessages then
          Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", patrolling.", DefenderGroup )
        end

        Dispatcher:ClearDefenderTaskTarget( DefenderGroup )
      end

      function AI_A2G_Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit )
        self:F({"Engage Route", DefenderGroup:GetName()})
        
        self:GetParent(self).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit )
        
        local DefenderName = DefenderGroup:GetCallsign()
        local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER
        local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
        
        if Squadron and AttackSetUnit:Count() > 0 then
          local FirstUnit = AttackSetUnit:GetFirst()
          local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE
          if self.SetSendPlayerMessages then
            Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", moving on to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup )
          end
        end
      end

      function AI_A2G_Fsm:OnAfterEngage( DefenderGroup, From, Event, To, AttackSetUnit )
        self:F({"Engage Route", DefenderGroup:GetName()})
        --self:GetParent(self).onafterBirth( self, Defender, From, Event, To )
        
        local DefenderName = DefenderGroup:GetCallsign()
        local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER
        local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
        local FirstUnit = AttackSetUnit:GetFirst()
        if FirstUnit then
          local Coordinate = FirstUnit:GetCoordinate()
          if self.SetSendPlayerMessages then
            Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup )
          end
        end
      end

      function AI_A2G_Fsm:onafterRTB( DefenderGroup, From, Event, To )
        self:F({"RTB", DefenderGroup:GetName()})
        self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To )
        
        local DefenderName = DefenderGroup:GetCallsign()
        local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER
        local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
        if self.SetSendPlayerMessages then
          Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", returning to base.", DefenderGroup )
        end
        Dispatcher:ClearDefenderTaskTarget( DefenderGroup )
      end

      -- @param #AI_A2G_DISPATCHER self
      function AI_A2G_Fsm:onafterLostControl( DefenderGroup, From, Event, To )
        self:F({"LostControl", DefenderGroup:GetName()})
        self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To )
        
        local DefenderName = DefenderGroup:GetCallsign()
        local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER
        local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
        if self.SetSendPlayerMessages then
          Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", lost control." )
        end
        if DefenderGroup:IsAboveRunway() then
          Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup )
          DefenderGroup:Destroy()
        end
      end
      
      -- @param #AI_A2G_DISPATCHER self
      function AI_A2G_Fsm:onafterHome( DefenderGroup, From, Event, To, Action )
        self:F({"Home", DefenderGroup:GetName()})
        self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To )
        
        local DefenderName = DefenderGroup:GetCallsign()
        local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER
        local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
        if self.SetSendPlayerMessages then
          Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", landing at base.", DefenderGroup )
        end
        if Action and Action == "Destroy" then
          Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup )
          DefenderGroup:Destroy()
        end

        if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then
          Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup )
          DefenderGroup:Destroy()
          Dispatcher:ResourcePark( Squadron, DefenderGroup )
        end
      end
    end

  end
  
  
  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:ResourceEngage( DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, AttackerDetection, SquadronName )
    
    self:F({DefenderSquadron=DefenderSquadron})
    self:F({DefendersNeeded=DefendersNeeded})
    self:F({Defense=Defense})
    self:F({DefenseTaskType=DefenseTaskType})
    self:F({AttackerDetection=AttackerDetection})
    self:F({SquadronName=SquadronName})
    
    local DefenderGroup, DefenderGrouping = self:ResourceActivate( DefenderSquadron, DefendersNeeded )    

    if DefenderGroup then

      local AI_A2G_ENGAGE = { SEAD = AI_A2G_SEAD, BAI = AI_A2G_BAI, CAS = AI_A2G_CAS }

      local AI_A2G_Fsm = AI_A2G_ENGAGE[DefenseTaskType]:New( DefenderGroup, Defense.EngageMinSpeed, Defense.EngageMaxSpeed, Defense.EngageFloorAltitude, Defense.EngageCeilingAltitude, Defense.EngageAltType ) -- AI.AI_AIR_ENGAGE
      AI_A2G_Fsm:SetDispatcher( self )
      AI_A2G_Fsm:SetHomeAirbase( DefenderSquadron.Airbase )
      AI_A2G_Fsm:SetFuelThreshold( DefenderSquadron.FuelThreshold or self.DefenderDefault.FuelThreshold, 60 )
      AI_A2G_Fsm:SetDamageThreshold( self.DefenderDefault.DamageThreshold )
      AI_A2G_Fsm:SetDisengageRadius( self.DisengageRadius )
      AI_A2G_Fsm:Start()

      self:SetDefenderTask( SquadronName, DefenderGroup, DefenseTaskType, AI_A2G_Fsm, AttackerDetection, DefenderGrouping )
      
      function AI_A2G_Fsm:onafterTakeoff( DefenderGroup, From, Event, To )
        self:F({"Defender Birth", DefenderGroup:GetName()})
        --self:GetParent(self).onafterBirth( self, Defender, From, Event, To )
        
        local DefenderName = DefenderGroup:GetCallsign()
        local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER
        local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
        local DefenderTarget = Dispatcher:GetDefenderTaskTarget( DefenderGroup )
        
        self:F( { DefenderTarget = DefenderTarget } )
        
        if DefenderTarget then
          if self.SetSendPlayerMessages then
            Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", wheels up.", DefenderGroup )
          end
          AI_A2G_Fsm:EngageRoute( DefenderTarget.Set ) -- Engage on the TargetSetUnit
        end
      end

      function AI_A2G_Fsm:onafterEngageRoute( DefenderGroup, From, Event, To, AttackSetUnit )
        self:F({"Engage Route", DefenderGroup:GetName()})
        
        local DefenderName = DefenderGroup:GetCallsign()
        local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER
        local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
        
        if Squadron then
          local FirstUnit = AttackSetUnit:GetRandomSurely()
          if FirstUnit then
          local Coordinate = FirstUnit:GetCoordinate() -- Core.Point#COORDINATE
           if self.SetSendPlayerMessages then
            Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", on route to ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup )
           end
          else
            return
          end
        end
        self:GetParent(self).onafterEngageRoute( self, DefenderGroup, From, Event, To, AttackSetUnit )
      end

      function AI_A2G_Fsm:OnAfterEngage( DefenderGroup, From, Event, To, AttackSetUnit )
        self:F({"Engage Route", DefenderGroup:GetName()})
        --self:GetParent(self).onafterBirth( self, Defender, From, Event, To )
        
        local DefenderName = DefenderGroup:GetCallsign()
        local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER
        local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
        local FirstUnit = AttackSetUnit:GetFirst()
        if FirstUnit then
          local Coordinate = FirstUnit:GetCoordinate()
          if self.SetSendPlayerMessages then
            Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", engaging ground target at " .. Coordinate:ToStringA2G( DefenderGroup ), DefenderGroup )
          end
        end
      end

      function AI_A2G_Fsm:onafterRTB( DefenderGroup, From, Event, To )
        self:F({"Defender RTB", DefenderGroup:GetName()})
        
        local DefenderName = DefenderGroup:GetCallsign()
        local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER
        local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
        if self.SetSendPlayerMessages then
          Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", returning to base.", DefenderGroup )
        end
        self:GetParent(self).onafterRTB( self, DefenderGroup, From, Event, To )

        Dispatcher:ClearDefenderTaskTarget( DefenderGroup )
      end

      -- @param #AI_A2G_DISPATCHER self
      function AI_A2G_Fsm:onafterLostControl( DefenderGroup, From, Event, To )
        self:F({"Defender LostControl", DefenderGroup:GetName()})
        self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To )
        
        local DefenderName = DefenderGroup:GetCallsign()
        local Dispatcher = AI_A2G_Fsm:GetDispatcher() -- #AI_A2G_DISPATCHER
        local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
        if self.SetSendPlayerMessages then
          Dispatcher:MessageToPlayers( Squadron,  "Squadron " .. Squadron.Name .. ", " .. DefenderName .. " lost control." )
        end
        if DefenderGroup:IsAboveRunway() then
          Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup )
          DefenderGroup:Destroy()
        end
      end
      
      -- @param #AI_A2G_DISPATCHER self
      function AI_A2G_Fsm:onafterHome( DefenderGroup, From, Event, To, Action )
        self:F({"Defender Home", DefenderGroup:GetName()})
        self:GetParent(self).onafterHome( self, DefenderGroup, From, Event, To )
        
        local DefenderName = DefenderGroup:GetCallsign()
        local Dispatcher = self:GetDispatcher() -- #AI_A2G_DISPATCHER
        local Squadron = Dispatcher:GetSquadronFromDefender( DefenderGroup )
        if self.SetSendPlayerMessages then
          Dispatcher:MessageToPlayers( Squadron,  DefenderName .. ", landing at base.", DefenderGroup )
        end
        if Action and Action == "Destroy" then
          Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup )
          DefenderGroup:Destroy()
        end

        if Dispatcher:GetSquadronLanding( Squadron.Name ) == AI_A2G_DISPATCHER.Landing.NearAirbase then
          Dispatcher:RemoveDefenderFromSquadron( Squadron, DefenderGroup )
          DefenderGroup:Destroy()
          Dispatcher:ResourcePark( Squadron, DefenderGroup )
        end
      end
    end
  end

  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:onafterEngage( From, Event, To, AttackerDetection, Defenders )
  
    if Defenders then

      for DefenderID, Defender in pairs( Defenders or {} ) do

        local Fsm = self:GetDefenderTaskFsm( Defender )
        Fsm:Engage( AttackerDetection.Set ) -- Engage on the TargetSetUnit
        
        self:SetDefenderTaskTarget( Defender, AttackerDetection )

      end
    end
  end

  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:HasDefenseLine( DefenseCoordinate, DetectedItem )

    local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem )
    local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate )

    -- Now check if this coordinate is not in a danger zone, meaning, that the attack line is not crossing other coordinates.
    -- (y1 - y2)x + (x2 - x1)y + (x1y2 - x2y1) = 0
    
    local c1 = DefenseCoordinate
    local c2 = AttackCoordinate
    
    local a = c1.z - c2.z -- Calculate a
    local b = c2.x - c1.x -- Calculate b
    local c = c1.x * c2.z - c2.x * c1.z -- calculate c
    
    local ok = true
    
    -- Now we check if each coordinate radius of about 30km of each attack is crossing a defense line. If yes, then this is not a good attack!
    for AttackItemID, CheckAttackItem in pairs( self.Detection:GetDetectedItems() ) do
    
      -- Only compare other detected coordinates.
      if AttackItemID ~= DetectedItem.ID then
    
        local CheckAttackCoordinate = self.Detection:GetDetectedItemCoordinate( CheckAttackItem )
        
        local x = CheckAttackCoordinate.x
        local y = CheckAttackCoordinate.z
        local r = 5000
        
        -- now we check if the coordinate is intersecting with the defense line.
        
        local IntersectDistance = ( math.abs( a * x + b * y + c ) ) / math.sqrt( a * a + b * b )
        self:F( { IntersectDistance = IntersectDistance, x = x, y = y } )
        
        local IntersectAttackDistance = CheckAttackCoordinate:Get2DDistance( DefenseCoordinate )
        
        self:F( { IntersectAttackDistance=IntersectAttackDistance, EvaluateDistance=EvaluateDistance } )
        
        -- If the distance of the attack coordinate is larger than the test radius; then the line intersects, and this is not a good coordinate.
        if IntersectDistance < r and IntersectAttackDistance < EvaluateDistance then
          ok = false
          break
        end
      end
    end
    
    return ok
  end

  ---
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:onafterDefend( From, Event, To, DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, DefenderFriendlies, DefenseTaskType )

    self:F( { From, Event, To, DetectedItem.Index, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing, DefenderFriendlies = DefenderFriendlies } )

    DetectedItem.Type = DefenseTaskType -- This is set to report the task type in the status panel.

    local AttackerSet = DetectedItem.Set
    local AttackerUnit = AttackerSet:GetFirst()
    
    if AttackerUnit and AttackerUnit:IsAlive() then
      local AttackerCount = AttackerSet:Count()
      local DefenderCount = 0
  
      for DefenderID, DefenderGroup in pairs( DefenderFriendlies or {} ) do

        -- Here we check if the defenders have a defense line to the attackers.
        -- If the attackers are behind enemy lines or too close to an other defense line; then don't engage.
        local DefenseCoordinate = DefenderGroup:GetCoordinate()
        local HasDefenseLine = self:HasDefenseLine( DefenseCoordinate, DetectedItem )
  
        if HasDefenseLine == true then
          local SquadronName = self:GetDefenderTask( DefenderGroup ).SquadronName
          local SquadronOverhead = self:GetSquadronOverhead( SquadronName )
  
          local Fsm = self:GetDefenderTaskFsm( DefenderGroup )
          Fsm:EngageRoute( AttackerSet ) -- Engage on the TargetSetUnit
          
          self:SetDefenderTaskTarget( DefenderGroup, DetectedItem )
    
          local DefenderGroupSize = DefenderGroup:GetSize()
          DefendersMissing = DefendersMissing - DefenderGroupSize / SquadronOverhead
          DefendersTotal = DefendersTotal + DefenderGroupSize / SquadronOverhead
        end
        
        if DefendersMissing <= 0 then
          break
        end
      end
  
      self:F( { DefenderCount = DefenderCount, DefendersMissing = DefendersMissing } )
      DefenderCount = DefendersMissing
  
      local ClosestDistance = 0
      local EngageSquadronName = nil
      
      local BreakLoop = false
      
      while( DefenderCount > 0 and not BreakLoop ) do
      
        self:F( { DefenderSquadrons = self.DefenderSquadrons } )

        for SquadronName, DefenderSquadron in UTILS.rpairs( self.DefenderSquadrons or {} ) do
        
          if DefenderSquadron[DefenseTaskType] then

            local AirbaseCoordinate = DefenderSquadron.Airbase:GetCoordinate() -- Core.Point#COORDINATE
            local AttackerCoord = AttackerUnit:GetCoordinate()
            local InterceptCoord = DetectedItem.InterceptCoord
            self:F( { InterceptCoord = InterceptCoord } )
            if InterceptCoord then
              local InterceptDistance = AirbaseCoordinate:Get2DDistance( InterceptCoord )
              local AirbaseDistance = AirbaseCoordinate:Get2DDistance( AttackerCoord )
              self:F( { InterceptDistance = InterceptDistance, AirbaseDistance = AirbaseDistance, InterceptCoord = InterceptCoord } )
              
              -- Only intercept if the distance to target is smaller or equal to the GciRadius limit.
              if AirbaseDistance <= self.DefenseRadius then
              
                -- Check if there is a defense line...
                local HasDefenseLine = self:HasDefenseLine( AirbaseCoordinate, DetectedItem )
                if HasDefenseLine == true then
                  local EngageProbability = ( DefenderSquadron.EngageProbability or 1 )
                  local Probability = math.random()
                  if Probability < EngageProbability then
                    EngageSquadronName = SquadronName
                    break
                  end
                end
              end
            end
          end
        end
        
        if EngageSquadronName then
        
          local DefenderSquadron, Defense = self:CanDefend( EngageSquadronName, DefenseTaskType )
          
          if Defense then
  
              local DefenderOverhead = DefenderSquadron.Overhead or self.DefenderDefault.Overhead
              local DefenderGrouping = DefenderSquadron.Grouping or self.DefenderDefault.Grouping
              local DefendersNeeded = math.ceil( DefenderCount * DefenderOverhead )
              
              self:F( { Overhead = DefenderOverhead, SquadronOverhead = DefenderSquadron.Overhead , DefaultOverhead = self.DefenderDefault.Overhead } )
              self:F( { Grouping = DefenderGrouping, SquadronGrouping = DefenderSquadron.Grouping, DefaultGrouping = self.DefenderDefault.Grouping } )
              self:F( { DefendersCount = DefenderCount, DefendersNeeded = DefendersNeeded } )

              -- Validate that the maximum limit of Defenders has been reached.
              -- If yes, then cancel the engaging of more defenders.
              local DefendersLimit = DefenderSquadron.EngageLimit or self.DefenderDefault.EngageLimit
              if DefendersLimit then
                if DefendersTotal >= DefendersLimit then
                  DefendersNeeded = 0
                  BreakLoop = true
                else
                  -- If the total of amount of defenders + the defenders needed, is larger than the limit of defenders,
                  -- then the defenders needed is the difference between defenders total - defenders limit.
                  if DefendersTotal + DefendersNeeded > DefendersLimit then
                    DefendersNeeded =  DefendersLimit - DefendersTotal
                  end
                end
              end
              
              -- DefenderSquadron.ResourceCount can have the value nil, which expresses unlimited resources.
              -- DefendersNeeded cannot exceed DefenderSquadron.ResourceCount!
              if DefenderSquadron.ResourceCount and DefendersNeeded > DefenderSquadron.ResourceCount then
                DefendersNeeded = DefenderSquadron.ResourceCount
                BreakLoop = true
              end
              
              while ( DefendersNeeded > 0 ) do
                self:ResourceQueue( false, DefenderSquadron, DefendersNeeded, Defense, DefenseTaskType, DetectedItem, EngageSquadronName )
                DefendersNeeded = DefendersNeeded - DefenderGrouping
                DefenderCount = DefenderCount - DefenderGrouping / DefenderOverhead
              end  -- while ( DefendersNeeded > 0 ) do
          else
            -- No more resources, try something else.
            -- Subject for a later enhancement to try to depart from another squadron and disable this one.
            BreakLoop = true
            break
          end
        else
          -- There isn't any closest airbase anymore, break the loop.
          break
        end
      end -- if DefenderSquadron then
    end -- if AttackerUnit
  end



  --- Creates an SEAD task when the targets have radars.
  -- @param #AI_A2G_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item.
  -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged.
  -- @return #nil If there are no targets to be set.
  function AI_A2G_DISPATCHER:Evaluate_SEAD( DetectedItem )
    self:F( { DetectedItem.ItemID } )
  
    local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT
    local AttackerCount = AttackerSet:HasSEAD() -- Is the AttackerSet a SEAD group, then the amount of radar emitters will be returned; that need to be attacked.
    
    if ( AttackerCount > 0 ) then
    
      -- First, count the active defenders, engaging the DetectedItem.
      local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount )
      
      self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } )
  
      local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "SEAD" )
      
      
  
      if DetectedItem.IsDetected == true then
        
        return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups
      end
    end
    
    return 0, 0, 0
  end


  --- Creates an CAS task.
  -- @param #AI_A2G_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item.
  -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged.
  -- @return #nil If there are no targets to be set.
  function AI_A2G_DISPATCHER:Evaluate_CAS( DetectedItem )
    self:F( { DetectedItem.ItemID } )
  
    local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT
    local AttackerCount = AttackerSet:Count()
    local AttackerRadarCount = AttackerSet:HasSEAD()
    local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT )
    local IsCas = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == true ) -- Is the AttackerSet a CAS group?
    
    if IsCas == true then
    
      -- First, count the active defenders, engaging the DetectedItem.
      local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount )
      
      self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } )
  
      local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "CAS" )
  
      if DetectedItem.IsDetected == true then
        
        return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups
      end
    end
    
    return 0, 0, 0
  end


  --- Evaluates an BAI task.
  -- @param #AI_A2G_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem The detected item.
  -- @return Core.Set#SET_UNIT The set of units of the targets to be engaged.
  -- @return #nil If there are no targets to be set.
  function AI_A2G_DISPATCHER:Evaluate_BAI( DetectedItem )
    self:F( { DetectedItem.ItemID } )
  
    local AttackerSet = DetectedItem.Set -- Core.Set#SET_UNIT
    local AttackerCount = AttackerSet:Count()
    local AttackerRadarCount = AttackerSet:HasSEAD()
    local IsFriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT )
    local IsBai = ( AttackerRadarCount == 0 ) and ( IsFriendliesNearBy == false ) -- Is the AttackerSet a BAI group?
    
    if IsBai == true then
    
      -- First, count the active defenders, engaging the DetectedItem.
      local DefendersTotal, DefendersEngaged, DefendersMissing = self:CountDefendersEngaged( DetectedItem, AttackerCount )
  
      self:F( { AttackerCount = AttackerCount, DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } )
  
      local DefenderGroups = self:CountDefenders( DetectedItem, DefendersEngaged, "BAI" )
  
      if DetectedItem.IsDetected == true then
        
        return DefendersTotal, DefendersEngaged, DefendersMissing, DefenderGroups
      end
    end
    
    return 0, 0, 0
  end


  --- Determine the distance as the keys of reference of the detected items.
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:Keys( DetectedItem )
  
    self:F( { DetectedItem = DetectedItem } )
  
    local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem )
    
    local ShortestDistance = 999999999
  
    for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do
      local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE

      local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate )
      
      if EvaluateDistance <= ShortestDistance then
        ShortestDistance = EvaluateDistance
      end
    end
    
    return ShortestDistance
  end


  --- Assigns A2G AI Tasks in relation to the detected items.
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:Order( DetectedItem )
    local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem )
    
    local ShortestDistance = 999999999
  
    for DefenseCoordinateName, DefenseCoordinate in pairs( self.DefenseCoordinates ) do
      local DefenseCoordinate = DefenseCoordinate -- Core.Point#COORDINATE

      local EvaluateDistance = AttackCoordinate:Get2DDistance( DefenseCoordinate )
      
      if EvaluateDistance <= ShortestDistance then
        ShortestDistance = EvaluateDistance
      end
    end
    
    return ShortestDistance
  end
  

  --- Shows the tactical display.
  -- @param #AI_A2G_DISPATCHER self
  function AI_A2G_DISPATCHER:ShowTacticalDisplay( Detection )

    local AreaMsg = {}
    local TaskMsg = {}
    local ChangeMsg = {}
    
    local TaskReport = REPORT:New()

    local DefenseTotal = 0

    local Report = REPORT:New( "\nTactical Overview" )

    local DefenderGroupCount = 0
    local DefendersTotal = 0

    -- Now that all obsolete tasks are removed, loop through the detected targets.
    --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do
    for DetectedItemID, DetectedItem in UTILS.spairs( Detection:GetDetectedItems(), function( t, a, b ) return self:Order(t[a]) <  self:Order(t[b]) end  ) do
    
      if not self.Detection:IsDetectedItemLocked( DetectedItem ) == true then
        local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem
        local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT
        local DetectedCount = DetectedSet:Count()
        local DetectedZone = DetectedItem.Zone
  
        self:F( { "Target ID", DetectedItem.ItemID } )
        
        self:F( { DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } )
        DetectedSet:Flush( self )
  
        local DetectedID = DetectedItem.ID
        local DetectionIndex = DetectedItem.Index
        local DetectedItemChanged = DetectedItem.Changed
  
        -- Show tactical situation
        local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G()
        Report:Add( string.format( " - %1s%s ( %04s ): ( #%02d - %-4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep(   "■",  ThreatLevel ) ) )
        for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do
          local Defender = Defender -- Wrapper.Group#GROUP
           if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then
             if Defender:IsAlive() then
               DefenderGroupCount = DefenderGroupCount + 1
               local Fuel = Defender:GetFuelMin() * 100
               local Damage = Defender:GetLife() / Defender:GetLife0() * 100
               Report:Add( string.format( "   - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", 
                                          Defender:GetName(), 
                                          DefenderTask.Type, 
                                          DefenderTask.Fsm:GetState(), 
                                          Defender:GetSize(), 
                                          Fuel,
                                          Damage, 
                                          Defender:HasTask() == true and "Executing" or "Idle" ) )
             end
           end
        end
      end
    end

    Report:Add( "\n - No Targets:")
    local TaskCount = 0
    for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do
      TaskCount = TaskCount + 1
      local Defender = Defender -- Wrapper.Group#GROUP
      if not DefenderTask.Target then
        if Defender:IsAlive() then
          local DefenderHasTask = Defender:HasTask()
          local Fuel = Defender:GetFuelMin() * 100
          local Damage = Defender:GetLife() / Defender:GetLife0() * 100
          DefenderGroupCount = DefenderGroupCount + 1
          Report:Add( string.format( "   - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", 
                                     Defender:GetName(), 
                                     DefenderTask.Type, 
                                     DefenderTask.Fsm:GetState(), 
                                     Defender:GetSize(),
                                     Fuel,
                                     Damage, 
                                     Defender:HasTask() == true and "Executing" or "Idle" ) )
        end
      end
    end
    Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) )

    Report:Add( string.format( "\n - %d Queued Aircraft Launches", #self.DefenseQueue ) )
    for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do
      local DefenseQueueItem = DefenseQueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem
      Report:Add( string.format( "   - %s - %s", DefenseQueueItem.SquadronName, DefenseQueueItem.DefenderSquadron.TakeoffTime, DefenseQueueItem.DefenderSquadron.TakeoffInterval) )
      
    end
    
    Report:Add( string.format( "\n - Squadron Resources: ", #self.DefenseQueue ) )
    for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do
      Report:Add( string.format( "   - %s - %s", DefenderSquadronName, DefenderSquadron.ResourceCount and tostring(DefenderSquadron.ResourceCount) or "n/a" ) )
    end

    self:F( Report:Text( "\n" ) )
    trigger.action.outText( Report:Text( "\n" ), 25 )
    
  end


  --- Assigns A2G AI Tasks in relation to the detected items.
  -- @param #AI_A2G_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object.
  -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop.
  function AI_A2G_DISPATCHER:ProcessDetected( Detection )
  
    local AreaMsg = {}
    local TaskMsg = {}
    local ChangeMsg = {}
    
    local TaskReport = REPORT:New()

    local DefenseTotal = 0
          
    for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do
      local DefenderGroup = DefenderGroup -- Wrapper.Group#GROUP
      local DefenderTaskFsm = self:GetDefenderTaskFsm( DefenderGroup )
      --if DefenderTaskFsm:Is( "LostControl" ) then
      --  self:ClearDefenderTask( DefenderGroup )
      --end
      if not DefenderGroup:IsAlive() then
        self:F( { Defender = DefenderGroup:GetName(), DefenderState = DefenderTaskFsm:GetState() } )
        if not DefenderTaskFsm:Is( "Started" ) then
          self:ClearDefenderTask( DefenderGroup )
        end
      else
      -- TODO: prio 1, what is this index stuff again, simplify it.
        if DefenderTask.Target then
        self:F( { TargetIndex = DefenderTask.Target.Index } )
          local AttackerItem = Detection:GetDetectedItemByIndex( DefenderTask.Target.Index )
          if not AttackerItem then
            self:F( { "Removing obsolete Target:", DefenderTask.Target.Index } )
            self:ClearDefenderTaskTarget( DefenderGroup )
          else
            if DefenderTask.Target.Set then
              local TargetCount = DefenderTask.Target.Set:Count()
              if TargetCount == 0 then
                self:F( { "All Targets destroyed in Target, removing:", DefenderTask.Target.Index } )
                self:ClearDefenderTask( DefenderGroup )
              end
            end
          end
        end
      end
    end

--    for DefenderGroup, DefenderTask in pairs( self:GetDefenderTasks() ) do
--      DefenseTotal = DefenseTotal + 1
--    end
    
    local Report = REPORT:New( "\nTactical Overview" )

    local DefenderGroupCount = 0

    local DefendersTotal = 0

    -- Now that all obsolete tasks are removed, loop through the detected targets.
    --for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do
    for DetectedDistance, DetectedItem in UTILS.kpairs( Detection:GetDetectedItems(), function( t ) return self:Keys( t ) end, function( t, a, b ) return self:Order(t[a]) <  self:Order(t[b]) end  ) do
    
      if not self.Detection:IsDetectedItemLocked( DetectedItem ) == true then
        local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem
        local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT
        local DetectedCount = DetectedSet:Count()
        local DetectedZone = DetectedItem.Zone
  
        self:F( { "Target ID", DetectedItem.ItemID } )
        
        self:F( { DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } )
        DetectedSet:Flush( self )
  
        local DetectedID = DetectedItem.ID
        local DetectionIndex = DetectedItem.Index
        local DetectedItemChanged = DetectedItem.Changed
        
        local AttackCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem )
        
        -- Calculate if for this DetectedItem if a defense needs to be initiated.
        -- This calculation is based on the distance between the defense point and the attackers, and the defensiveness parameter.
        -- The attackers closest to the defense coordinates will be handled first, or course!

        local EngageDefenses = nil        
          
        self:F( { DetectedDistance = DetectedDistance, DefenseRadius = self.DefenseRadius } )
        if DetectedDistance <= self.DefenseRadius then
        
          self:F( { DetectedApproach = self._DefenseApproach } )
          if self._DefenseApproach == AI_A2G_DISPATCHER.DefenseApproach.Distance then
            EngageDefenses = true
            self:F( { EngageDefenses = EngageDefenses } )
          end

          if self._DefenseApproach == AI_A2G_DISPATCHER.DefenseApproach.Random then
            local DistanceProbability = ( self.DefenseRadius / DetectedDistance * self.DefenseReactivity )
            local DefenseProbability = math.random()
  
            self:F( { DistanceProbability = DistanceProbability, DefenseProbability = DefenseProbability } )
  
            if DefenseProbability <= DistanceProbability / ( 300 / 30 ) then
              EngageDefenses = true
            end
          end
          
          
        end
        
        self:F( { EngageDefenses = EngageDefenses, DefenseLimit = self.DefenseLimit, DefenseTotal = DefenseTotal } )
        
        -- There needs to be an EngageCoordinate.
        -- If self.DefenseLimit is set (thus limit the amount of defenses to one zone), then only start a new defense if the maximum has not been reached.
        -- If self.DefenseLimit has not been set, there is an unlimited amount of zones to be defended.
        if ( EngageDefenses and ( self.DefenseLimit and DefenseTotal < self.DefenseLimit ) or not self.DefenseLimit ) then
          do 
            local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_SEAD( DetectedItem ) -- Returns a SET_UNIT with the SEAD targets to be engaged...
            if DefendersMissing > 0 then
              self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } )
              self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "SEAD" )
            end
          end
    
          do 
            local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_CAS( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged...
            if DefendersMissing > 0 then
              self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } )
              self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "CAS" )
            end
          end
    
          do 
            local DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies = self:Evaluate_BAI( DetectedItem ) -- Returns a SET_UNIT with the CAS targets to be engaged...
            if DefendersMissing > 0 then
              self:F( { DefendersTotal = DefendersTotal, DefendersEngaged = DefendersEngaged, DefendersMissing = DefendersMissing } )
              self:Defend( DetectedItem, DefendersTotal, DefendersEngaged, DefendersMissing, Friendlies, "BAI" )
            end
          end
        end
        
        for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do
          local Defender = Defender -- Wrapper.Group#GROUP
          if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then
            DefenseTotal = DefenseTotal + 1
          end
        end
        
        for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do
          local DefenseQueueItem = DefenseQueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem
          if DefenseQueueItem.AttackerDetection and DefenseQueueItem.AttackerDetection.Index and DefenseQueueItem.AttackerDetection.Index == DetectedItem.Index then
            DefenseTotal = DefenseTotal + 1
          end
        end
  
        if self.TacticalDisplay then      
          -- Show tactical situation
          local ThreatLevel = DetectedItem.Set:CalculateThreatLevelA2G()
          Report:Add( string.format( " - %1s%s ( %4s ): ( #%d - %4s ) %s" , ( DetectedItem.IsDetected == true ) and "!" or " ", DetectedItem.ItemID, DetectedItem.Index, DetectedItem.Set:Count(), DetectedItem.Type or " --- ", string.rep(   "■", ThreatLevel ) ) )
          for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do
            local Defender = Defender -- Wrapper.Group#GROUP
             if DefenderTask.Target and DefenderTask.Target.Index == DetectedItem.Index then
               if Defender:IsAlive() then
                 DefenderGroupCount = DefenderGroupCount + 1
                 local Fuel = Defender:GetFuelMin() * 100
                 local Damage = Defender:GetLife() / Defender:GetLife0() * 100
                 Report:Add( string.format( "   - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", 
                                            Defender:GetName(), 
                                            DefenderTask.Type, 
                                            DefenderTask.Fsm:GetState(), 
                                            Defender:GetSize(), 
                                            Fuel,
                                            Damage, 
                                            Defender:HasTask() == true and "Executing" or "Idle" ) )
               end
             end
          end
        end
      end
    end

    if self.TacticalDisplay then
      Report:Add( "\n - No Targets:")
      local TaskCount = 0
      for Defender, DefenderTask in pairs( self:GetDefenderTasks() ) do
        TaskCount = TaskCount + 1
        local Defender = Defender -- Wrapper.Group#GROUP
        if not DefenderTask.Target then
          if Defender:IsAlive() then
            local DefenderHasTask = Defender:HasTask()
            local Fuel = Defender:GetFuelMin() * 100
            local Damage = Defender:GetLife() / Defender:GetLife0() * 100
            DefenderGroupCount = DefenderGroupCount + 1
            Report:Add( string.format( "   - %s ( %s - %s ): ( #%d ) F: %3d, D:%3d - %s", 
                                       Defender:GetName(), 
                                       DefenderTask.Type, 
                                       DefenderTask.Fsm:GetState(), 
                                       Defender:GetSize(),
                                       Fuel,
                                       Damage, 
                                       Defender:HasTask() == true and "Executing" or "Idle" ) )
          end
        end
      end
      Report:Add( string.format( "\n - %d Tasks - %d Defender Groups", TaskCount, DefenderGroupCount ) )

      Report:Add( string.format( "\n - %d Queued Aircraft Launches", #self.DefenseQueue ) )
      for DefenseQueueID, DefenseQueueItem in pairs( self.DefenseQueue ) do
        local DefenseQueueItem = DefenseQueueItem -- #AI_A2G_DISPATCHER.DefenseQueueItem
        Report:Add( string.format( "   - %s - %s", DefenseQueueItem.SquadronName, DefenseQueueItem.DefenderSquadron.TakeoffTime, DefenseQueueItem.DefenderSquadron.TakeoffInterval) )
        
      end

      Report:Add( string.format( "\n - Squadron Resources: ", #self.DefenseQueue ) )
      for DefenderSquadronName, DefenderSquadron in pairs( self.DefenderSquadrons ) do
        Report:Add( string.format( "   - %s - %s", DefenderSquadronName, DefenderSquadron.ResourceCount and tostring(DefenderSquadron.ResourceCount) or "n/a" ) )
      end
  
      self:F( Report:Text( "\n" ) )
      trigger.action.outText( Report:Text( "\n" ), 25 )
    end
    
    return true
  end

end

do

  --- Calculates which HUMAN friendlies are nearby the area.
  -- @param #AI_A2G_DISPATCHER self
  -- @param DetectedItem The detected item.
  -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type.
  function AI_A2G_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem )
  
    local DetectedSet = DetectedItem.Set
    local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem )
    
    local PlayerTypes = {}
    local PlayersCount = 0

    if PlayersNearBy then
      local DetectedThreatLevel = DetectedSet:CalculateThreatLevelA2G()
      for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do
        local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT
        local PlayerName = PlayerUnit:GetPlayerName()
        --self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } )
        if PlayerUnit:IsAirPlane() and PlayerName ~= nil then
          local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel()
          PlayersCount = PlayersCount + 1
          local PlayerType = PlayerUnit:GetTypeName()
          PlayerTypes[PlayerName] = PlayerType
          if DetectedThreatLevel < FriendlyUnitThreatLevel + 2 then
          end
        end
      end
      
    end

    --self:F( { PlayersCount = PlayersCount } )
    
    local PlayerTypesReport = REPORT:New()
    
    if PlayersCount > 0 then
      for PlayerName, PlayerType in pairs( PlayerTypes ) do
        PlayerTypesReport:Add( string.format('"%s" in %s', PlayerName, PlayerType ) )
      end
    else
      PlayerTypesReport:Add( "-" )
    end
    
    
    return PlayersCount, PlayerTypesReport
  end

  --- Calculates which friendlies are nearby the area.
  -- @param #AI_A2G_DISPATCHER self
  -- @param DetectedItem The detected item.
  -- @return #number, Core.Report#REPORT The amount of friendlies and a text string explaining which friendlies of which type.
  function AI_A2G_DISPATCHER:GetFriendliesNearBy( DetectedItem )
  
    local DetectedSet = DetectedItem.Set
    local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem )
    
    local FriendlyTypes = {}
    local FriendliesCount = 0

    if FriendlyUnitsNearBy then
      local DetectedThreatLevel = DetectedSet:CalculateThreatLevelA2G()
      for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do
        local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT
        if FriendlyUnit:IsAirPlane() then
          local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel()
          FriendliesCount = FriendliesCount + 1
          local FriendlyType = FriendlyUnit:GetTypeName()
          FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and ( FriendlyTypes[FriendlyType] + 1 ) or 1
          if DetectedThreatLevel < FriendlyUnitThreatLevel + 2 then
          end
        end
      end
      
    end

    --self:F( { FriendliesCount = FriendliesCount } )
    
    local FriendlyTypesReport = REPORT:New()
    
    if FriendliesCount > 0 then
      for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do
        FriendlyTypesReport:Add( string.format("%d of %s", FriendlyTypeCount, FriendlyType ) )
      end
    else
      FriendlyTypesReport:Add( "-" )
    end
    
    
    return FriendliesCount, FriendlyTypesReport
  end

  --- Schedules a new Patrol for the given SquadronName.
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  function AI_A2G_DISPATCHER:SchedulerPatrol( SquadronName )
    local PatrolTaskTypes = { "SEAD", "CAS", "BAI" }
    local PatrolTaskType = PatrolTaskTypes[math.random(1,3)]
    self:Patrol( SquadronName, PatrolTaskType )    
  end
  
    --- Set flashing player messages on or off
  -- @param #AI_A2G_DISPATCHER self
  -- @param #boolean onoff Set messages on (true) or off (false)
  function AI_A2G_DISPATCHER:SetSendMessages( onoff )
      self.SetSendPlayerMessages = onoff
  end
end

  --- Add resources to a Squadron
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string Squadron The squadron name.
  -- @param #number Amount Number of resources to add.
  function AI_A2G_DISPATCHER:AddToSquadron(Squadron,Amount)
    local Squadron = self:GetSquadron(Squadron)
    if Squadron.ResourceCount then
      Squadron.ResourceCount = Squadron.ResourceCount + Amount
    end
    self:T({Squadron = Squadron.Name,SquadronResourceCount = Squadron.ResourceCount})
  end
  
  --- Remove resources from a Squadron
  -- @param #AI_A2G_DISPATCHER self
  -- @param #string Squadron The squadron name.
  -- @param #number Amount Number of resources to remove.
  function AI_A2G_DISPATCHER:RemoveFromSquadron(Squadron,Amount)
    local Squadron = self:GetSquadron(Squadron)
    if Squadron.ResourceCount then
      Squadron.ResourceCount = Squadron.ResourceCount - Amount
    end
    self:T({Squadron = Squadron.Name,SquadronResourceCount = Squadron.ResourceCount})
  end
  --- **AI** - Perform Air Patrolling for airplanes.
-- 
-- **Features:**
-- 
--   * Patrol AI airplanes within a given zone.
--   * Trigger detected events when enemy airplanes are detected.
--   * Manage a fuel threshold to RTB on time.
-- 
-- ===
-- 
-- AI PATROL classes makes AI Controllables execute an Patrol.
-- 
-- There are the following types of PATROL classes defined:
-- 
--   * @{#AI_PATROL_ZONE}: Perform a PATROL in a zone.
--   
-- ===
-- 
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_Patrol)
-- 
-- ===
-- 
-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl35HvYZKA6G22WMt7iI3zky)
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
--   * **Dutch_Baron**: Working together with James has resulted in the creation of the AI_BALANCER class. James has shared his ideas on balancing AI with air units, and together we made a first design which you can use now :-)
--   * **Pikey**: Testing and API concept review.
-- 
-- ===
-- 
-- @module AI.AI_Patrol
-- @image AI_Air_Patrolling.JPG

--- AI_PATROL_ZONE class
-- @type AI_PATROL_ZONE
-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling.
-- @field Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @field DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @field DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @field DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h.
-- @field DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h.
-- @field Core.Spawn#SPAWN CoordTest
-- @extends Core.Fsm#FSM_CONTROLLABLE

--- Implements the core functions to patrol a @{Core.Zone} by an AI @{Wrapper.Controllable} or @{Wrapper.Group}.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia3.JPG)
-- 
-- The AI_PATROL_ZONE is assigned a @{Wrapper.Group} and this must be done before the AI_PATROL_ZONE process can be started using the **Start** event.
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia4.JPG)
-- 
-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits.
-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits.
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia5.JPG)
-- 
-- This cycle will continue.
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia6.JPG)
-- 
-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event.
--
-- ![Process](..\Presentations\AI_PATROL\Dia9.JPG)
-- 
---- Note that the enemy is not engaged! To model enemy engagement, either tailor the **Detected** event, or
-- use derived AI_ classes to model AI offensive or defensive behaviour.
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia10.JPG)
-- 
-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB.
-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land.
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia11.JPG)
-- 
-- ## 1. AI_PATROL_ZONE constructor
--   
--   * @{#AI_PATROL_ZONE.New}(): Creates a new AI_PATROL_ZONE object.
-- 
-- ## 2. AI_PATROL_ZONE is a FSM
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia2.JPG)
-- 
-- ### 2.1. AI_PATROL_ZONE States
-- 
--   * **None** ( Group ): The process is not started yet.
--   * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone.
--   * **Returning** ( Group ): The AI is returning to Base.
--   * **Stopped** ( Group ): The process is stopped.
--   * **Crashed** ( Group ): The AI has crashed or is dead.
-- 
-- ### 2.2. AI_PATROL_ZONE Events
-- 
--   * **Start** ( Group ): Start the process.
--   * **Stop** ( Group ): Stop the process.
--   * **Route** ( Group ): Route the AI to a new random 3D point within the Patrol Zone.
--   * **RTB** ( Group ): Route the AI to the home base.
--   * **Detect** ( Group ): The AI is detecting targets.
--   * **Detected** ( Group ): The AI has detected new targets.
--   * **Status** ( Group ): The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB.
--    
-- ## 3. Set or Get the AI controllable
-- 
--   * @{#AI_PATROL_ZONE.SetControllable}(): Set the AIControllable.
--   * @{#AI_PATROL_ZONE.GetControllable}(): Get the AIControllable.
--
-- ## 4. Set the Speed and Altitude boundaries of the AI controllable
--
--   * @{#AI_PATROL_ZONE.SetSpeed}(): Set the patrol speed boundaries of the AI, for the next patrol.
--   * @{#AI_PATROL_ZONE.SetAltitude}(): Set altitude boundaries of the AI, for the next patrol.
-- 
-- ## 5. Manage the detection process of the AI controllable
-- 
-- The detection process of the AI controllable can be manipulated.
-- Detection requires an amount of CPU power, which has an impact on your mission performance.
-- Only put detection on when absolutely necessary, and the frequency of the detection can also be set.
-- 
--   * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets.
--   * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased.
-- 
-- The detection frequency can be set with @{#AI_PATROL_ZONE.SetRefreshTimeInterval}( seconds ), where the amount of seconds specify how much seconds will be waited before the next detection.
-- Use the method @{#AI_PATROL_ZONE.GetDetectedUnits}() to obtain a list of the @{Wrapper.Unit}s detected by the AI.
-- 
-- The detection can be filtered to potential targets in a specific zone.
-- Use the method @{#AI_PATROL_ZONE.SetDetectionZone}() to set the zone where targets need to be detected.
-- Note that when the zone is too far away, or the AI is not heading towards the zone, or the AI is too high, no targets may be detected
-- according the weather conditions.
-- 
-- ## 6. Manage the "out of fuel" in the AI_PATROL_ZONE
-- 
-- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base.
-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel threshold is calculated.
-- When the fuel threshold is reached, the AI will continue for a given time its patrol task in orbit, 
-- while a new AI is targeted to the AI_PATROL_ZONE.
-- Once the time is finished, the old AI will return to the base.
-- Use the method @{#AI_PATROL_ZONE.ManageFuel}() to have this process in place.
-- 
-- ## 7. Manage "damage" behaviour of the AI in the AI_PATROL_ZONE
-- 
-- When the AI is damaged, it is required that a new AIControllable is started. However, damage cannon be foreseen early on. 
-- Therefore, when the damage threshold is reached, the AI will return immediately to the home base (RTB).
-- Use the method @{#AI_PATROL_ZONE.ManageDamage}() to have this process in place.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_PATROL_ZONE
AI_PATROL_ZONE = {
  ClassName = "AI_PATROL_ZONE",
}

--- Creates a new AI_PATROL_ZONE object
-- @param #AI_PATROL_ZONE self
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO
-- @return #AI_PATROL_ZONE self
-- @usage
-- -- Define a new AI_PATROL_ZONE Object. This PatrolArea will patrol an AIControllable within PatrolZone between 3000 and 6000 meters, with a variying speed between 600 and 900 km/h.
-- PatrolZone = ZONE:New( 'PatrolZone' )
-- PatrolSpawn = SPAWN:New( 'Patrol Group' )
-- PatrolArea = AI_PATROL_ZONE:New( PatrolZone, 3000, 6000, 600, 900 )
function AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )

  -- Inherits from BASE
  local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_PATROL_ZONE


  self.PatrolZone = PatrolZone
  self.PatrolFloorAltitude = PatrolFloorAltitude
  self.PatrolCeilingAltitude = PatrolCeilingAltitude
  self.PatrolMinSpeed = PatrolMinSpeed
  self.PatrolMaxSpeed = PatrolMaxSpeed

  -- defafult PatrolAltType to "BARO" if not specified
  self.PatrolAltType = PatrolAltType or "BARO"

  self:SetRefreshTimeInterval( 30 )

  self.CheckStatus = true

  self:ManageFuel( .2, 60 )
  self:ManageDamage( 1 )


  self.DetectedUnits = {} -- This table contains the targets detected during patrol.

  self:SetStartState( "None" ) 

  self:AddTransition( "*", "Stop", "Stopped" )

--- OnLeave Transition Handler for State Stopped.
-- @function [parent=#AI_PATROL_ZONE] OnLeaveStopped
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnEnter Transition Handler for State Stopped.
-- @function [parent=#AI_PATROL_ZONE] OnEnterStopped
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

--- OnBefore Transition Handler for Event Stop.
-- @function [parent=#AI_PATROL_ZONE] OnBeforeStop
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnAfter Transition Handler for Event Stop.
-- @function [parent=#AI_PATROL_ZONE] OnAfterStop
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

--- Synchronous Event Trigger for Event Stop.
-- @function [parent=#AI_PATROL_ZONE] Stop
-- @param #AI_PATROL_ZONE self

--- Asynchronous Event Trigger for Event Stop.
-- @function [parent=#AI_PATROL_ZONE] __Stop
-- @param #AI_PATROL_ZONE self
-- @param #number Delay The delay in seconds.

  self:AddTransition( "None", "Start", "Patrolling" )

--- OnBefore Transition Handler for Event Start.
-- @function [parent=#AI_PATROL_ZONE] OnBeforeStart
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnAfter Transition Handler for Event Start.
-- @function [parent=#AI_PATROL_ZONE] OnAfterStart
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

--- Synchronous Event Trigger for Event Start.
-- @function [parent=#AI_PATROL_ZONE] Start
-- @param #AI_PATROL_ZONE self

--- Asynchronous Event Trigger for Event Start.
-- @function [parent=#AI_PATROL_ZONE] __Start
-- @param #AI_PATROL_ZONE self
-- @param #number Delay The delay in seconds.

--- OnLeave Transition Handler for State Patrolling.
-- @function [parent=#AI_PATROL_ZONE] OnLeavePatrolling
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnEnter Transition Handler for State Patrolling.
-- @function [parent=#AI_PATROL_ZONE] OnEnterPatrolling
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

  self:AddTransition( "Patrolling", "Route", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE.

--- OnBefore Transition Handler for Event Route.
-- @function [parent=#AI_PATROL_ZONE] OnBeforeRoute
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnAfter Transition Handler for Event Route.
-- @function [parent=#AI_PATROL_ZONE] OnAfterRoute
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
	
--- Synchronous Event Trigger for Event Route.
-- @function [parent=#AI_PATROL_ZONE] Route
-- @param #AI_PATROL_ZONE self

--- Asynchronous Event Trigger for Event Route.
-- @function [parent=#AI_PATROL_ZONE] __Route
-- @param #AI_PATROL_ZONE self
-- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE.

--- OnBefore Transition Handler for Event Status.
-- @function [parent=#AI_PATROL_ZONE] OnBeforeStatus
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnAfter Transition Handler for Event Status.
-- @function [parent=#AI_PATROL_ZONE] OnAfterStatus
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

--- Synchronous Event Trigger for Event Status.
-- @function [parent=#AI_PATROL_ZONE] Status
-- @param #AI_PATROL_ZONE self

--- Asynchronous Event Trigger for Event Status.
-- @function [parent=#AI_PATROL_ZONE] __Status
-- @param #AI_PATROL_ZONE self
-- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "Detect", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE.

--- OnBefore Transition Handler for Event Detect.
-- @function [parent=#AI_PATROL_ZONE] OnBeforeDetect
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnAfter Transition Handler for Event Detect.
-- @function [parent=#AI_PATROL_ZONE] OnAfterDetect
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
	
--- Synchronous Event Trigger for Event Detect.
-- @function [parent=#AI_PATROL_ZONE] Detect
-- @param #AI_PATROL_ZONE self

--- Asynchronous Event Trigger for Event Detect.
-- @function [parent=#AI_PATROL_ZONE] __Detect
-- @param #AI_PATROL_ZONE self
-- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "Detected", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE.

--- OnBefore Transition Handler for Event Detected.
-- @function [parent=#AI_PATROL_ZONE] OnBeforeDetected
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnAfter Transition Handler for Event Detected.
-- @function [parent=#AI_PATROL_ZONE] OnAfterDetected
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
	
--- Synchronous Event Trigger for Event Detected.
-- @function [parent=#AI_PATROL_ZONE] Detected
-- @param #AI_PATROL_ZONE self

--- Asynchronous Event Trigger for Event Detected.
-- @function [parent=#AI_PATROL_ZONE] __Detected
-- @param #AI_PATROL_ZONE self
-- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "RTB", "Returning" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE.

--- OnBefore Transition Handler for Event RTB.
-- @function [parent=#AI_PATROL_ZONE] OnBeforeRTB
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnAfter Transition Handler for Event RTB.
-- @function [parent=#AI_PATROL_ZONE] OnAfterRTB
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

--- Synchronous Event Trigger for Event RTB.
-- @function [parent=#AI_PATROL_ZONE] RTB
-- @param #AI_PATROL_ZONE self

--- Asynchronous Event Trigger for Event RTB.
-- @function [parent=#AI_PATROL_ZONE] __RTB
-- @param #AI_PATROL_ZONE self
-- @param #number Delay The delay in seconds.

--- OnLeave Transition Handler for State Returning.
-- @function [parent=#AI_PATROL_ZONE] OnLeaveReturning
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnEnter Transition Handler for State Returning.
-- @function [parent=#AI_PATROL_ZONE] OnEnterReturning
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

  self:AddTransition( "*", "Reset", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_PATROL_ZONE.

  self:AddTransition( "*", "Eject", "*" )
  self:AddTransition( "*", "Crash", "Crashed" )
  self:AddTransition( "*", "PilotDead", "*" )

  return self
end




--- Sets (modifies) the minimum and maximum speed of the patrol.
-- @param #AI_PATROL_ZONE self
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h.
-- @return #AI_PATROL_ZONE self
function AI_PATROL_ZONE:SetSpeed( PatrolMinSpeed, PatrolMaxSpeed )
  self:F2( { PatrolMinSpeed, PatrolMaxSpeed } )

  self.PatrolMinSpeed = PatrolMinSpeed
  self.PatrolMaxSpeed = PatrolMaxSpeed
end



--- Sets the floor and ceiling altitude of the patrol.
-- @param #AI_PATROL_ZONE self
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @return #AI_PATROL_ZONE self
function AI_PATROL_ZONE:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude )
  self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } )

  self.PatrolFloorAltitude = PatrolFloorAltitude
  self.PatrolCeilingAltitude = PatrolCeilingAltitude
end

--   * @{#AI_PATROL_ZONE.SetDetectionOn}(): Set the detection on. The AI will detect for targets.
--   * @{#AI_PATROL_ZONE.SetDetectionOff}(): Set the detection off, the AI will not detect for targets. The existing target list will NOT be erased.

--- Set the detection on. The AI will detect for targets.
-- @param #AI_PATROL_ZONE self
-- @return #AI_PATROL_ZONE self
function AI_PATROL_ZONE:SetDetectionOn()
  self:F2()

  self.DetectOn = true
end

--- Set the detection off. The AI will NOT detect for targets.
-- However, the list of already detected targets will be kept and can be enquired!
-- @param #AI_PATROL_ZONE self
-- @return #AI_PATROL_ZONE self
function AI_PATROL_ZONE:SetDetectionOff()
  self:F2()

  self.DetectOn = false
end

--- Set the status checking off.
-- @param #AI_PATROL_ZONE self
-- @return #AI_PATROL_ZONE self
function AI_PATROL_ZONE:SetStatusOff()
  self:F2()
  
  self.CheckStatus = false
end

--- Activate the detection. The AI will detect for targets if the Detection is switched On.
-- @param #AI_PATROL_ZONE self
-- @return #AI_PATROL_ZONE self
function AI_PATROL_ZONE:SetDetectionActivated()
  self:F2()
  
  self:ClearDetectedUnits()
  self.DetectActivated = true
  self:__Detect( -self.DetectInterval )
end

--- Deactivate the detection. The AI will NOT detect for targets.
-- @param #AI_PATROL_ZONE self
-- @return #AI_PATROL_ZONE self
function AI_PATROL_ZONE:SetDetectionDeactivated()
  self:F2()
  
  self:ClearDetectedUnits()
  self.DetectActivated = false
end

--- Set the interval in seconds between each detection executed by the AI.
-- The list of already detected targets will be kept and updated.
-- Newly detected targets will be added, but already detected targets that were 
-- not detected in this cycle, will NOT be removed!
-- The default interval is 30 seconds.
-- @param #AI_PATROL_ZONE self
-- @param #number Seconds The interval in seconds.
-- @return #AI_PATROL_ZONE self
function AI_PATROL_ZONE:SetRefreshTimeInterval( Seconds )
  self:F2()

  if Seconds then  
    self.DetectInterval = Seconds
  else
    self.DetectInterval = 30
  end
end

--- Set the detection zone where the AI is detecting targets.
-- @param #AI_PATROL_ZONE self
-- @param Core.Zone#ZONE DetectionZone The zone where to detect targets.
-- @return #AI_PATROL_ZONE self
function AI_PATROL_ZONE:SetDetectionZone( DetectionZone )
  self:F2()

  if DetectionZone then  
    self.DetectZone = DetectionZone
  else
    self.DetectZone = nil
  end
end

--- Gets a list of @{Wrapper.Unit#UNIT}s that were detected by the AI.
-- No filtering is applied, so, ANY detected UNIT can be in this list.
-- It is up to the mission designer to use the @{Wrapper.Unit} class and methods to filter the targets.
-- @param #AI_PATROL_ZONE self
-- @return #table The list of @{Wrapper.Unit#UNIT}s
function AI_PATROL_ZONE:GetDetectedUnits()
  self:F2()

  return self.DetectedUnits 
end

--- Clears the list of @{Wrapper.Unit#UNIT}s that were detected by the AI.
-- @param #AI_PATROL_ZONE self
function AI_PATROL_ZONE:ClearDetectedUnits()
  self:F2()
  self.DetectedUnits = {}
end

--- When the AI is out of fuel, it is required that a new AI is started, before the old AI can return to the home base.
-- Therefore, with a parameter and a calculation of the distance to the home base, the fuel threshold is calculated.
-- When the fuel threshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targeted to the AI_PATROL_ZONE.
-- Once the time is finished, the old AI will return to the base.
-- @param #AI_PATROL_ZONE self
-- @param #number PatrolFuelThresholdPercentage The threshold in percentage (between 0 and 1) when the AIControllable is considered to get out of fuel.
-- @param #number PatrolOutOfFuelOrbitTime The amount of seconds the out of fuel AIControllable will orbit before returning to the base.
-- @return #AI_PATROL_ZONE self
function AI_PATROL_ZONE:ManageFuel( PatrolFuelThresholdPercentage, PatrolOutOfFuelOrbitTime )

  self.PatrolFuelThresholdPercentage = PatrolFuelThresholdPercentage
  self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime

  return self
end

--- When the AI is damaged beyond a certain threshold, it is required that the AI returns to the home base.
-- However, damage cannot be foreseen early on. 
-- Therefore, when the damage threshold is reached, 
-- the AI will return immediately to the home base (RTB).
-- Note that for groups, the average damage of the complete group will be calculated.
-- So, in a group of 4 airplanes, 2 lost and 2 with damage 0.2, the damage threshold will be 0.25.
-- @param #AI_PATROL_ZONE self
-- @param #number PatrolDamageThreshold The threshold in percentage (between 0 and 1) when the AI is considered to be damaged.
-- @return #AI_PATROL_ZONE self
function AI_PATROL_ZONE:ManageDamage( PatrolDamageThreshold )

  self.PatrolManageDamage = true
  self.PatrolDamageThreshold = PatrolDamageThreshold

  return self
end

--- Defines a new patrol route using the @{#AI_PATROL_ZONE} parameters and settings.
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #AI_PATROL_ZONE self
function AI_PATROL_ZONE:onafterStart( Controllable, From, Event, To )
  self:F2()

  self:__Route( 1 ) -- Route to the patrol point. The asynchronous trigger is important, because a spawned group and units takes at least one second to come live.
  self:__Status( 60 ) -- Check status status every 30 seconds.
  self:SetDetectionActivated()

  self:HandleEvent( EVENTS.PilotDead, self.OnPilotDead )
  self:HandleEvent( EVENTS.Crash, self.OnCrash )
  self:HandleEvent( EVENTS.Ejection, self.OnEjection )

  Controllable:OptionROEHoldFire()
  Controllable:OptionROTVertical()

  self.Controllable:OnReSpawn(
    function( PatrolGroup )
      self:T( "ReSpawn" )
      self:__Reset( 1 )
      self:__Route( 5 )
    end
  )

  self:SetDetectionOn()
  
end


-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable+
function AI_PATROL_ZONE:onbeforeDetect( Controllable, From, Event, To )

  return self.DetectOn and self.DetectActivated
end

-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable
function AI_PATROL_ZONE:onafterDetect( Controllable, From, Event, To )

  local Detected = false

  local DetectedTargets = Controllable:GetDetectedTargets()
  for TargetID, Target in pairs( DetectedTargets or {} ) do
    local TargetObject = Target.object

    if TargetObject and TargetObject:isExist() and TargetObject.id_ < 50000000 then

      local TargetUnit = UNIT:Find( TargetObject )

      -- Check that target is alive due to issue https://github.com/FlightControl-Master/MOOSE/issues/1234
      if TargetUnit and TargetUnit:IsAlive() then

        local TargetUnitName = TargetUnit:GetName()

        if self.DetectionZone then
          if TargetUnit:IsInZone( self.DetectionZone ) then
            self:T( {"Detected ", TargetUnit } )
            if self.DetectedUnits[TargetUnit] == nil then
              self.DetectedUnits[TargetUnit] = true
            end
            Detected = true 
          end
        else       
          if self.DetectedUnits[TargetUnit] == nil then
            self.DetectedUnits[TargetUnit] = true
          end
          Detected = true
        end
  
      end
    end
  end

  self:__Detect( -self.DetectInterval )

  if Detected == true then
    self:__Detected( 1.5 )
  end
  
end

-- @param Wrapper.Controllable#CONTROLLABLE AIControllable
-- This static method is called from the route path within the last task at the last waypoint of the Controllable.
-- Note that this method is required, as triggers the next route when patrolling for the Controllable.
function AI_PATROL_ZONE:_NewPatrolRoute( AIControllable )

  local PatrolZone = AIControllable:GetState( AIControllable, "PatrolZone" ) -- PatrolCore.Zone#AI_PATROL_ZONE
  PatrolZone:__Route( 1 )
end


--- Defines a new patrol route using the @{#AI_PATROL_ZONE} parameters and settings.
-- @param #AI_PATROL_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_PATROL_ZONE:onafterRoute( Controllable, From, Event, To )

  self:F2()

  -- When RTB, don't allow anymore the routing.
  if From == "RTB" then
    return
  end

  local life = self.Controllable:GetLife() or 0
  if self.Controllable:IsAlive() and life > 1 then
    -- Determine if the AIControllable is within the PatrolZone. 
    -- If not, make a waypoint within the to that the AIControllable will fly at maximum speed to that point.

    local PatrolRoute = {}

    -- Calculate the current route point of the controllable as the start point of the route.
    -- However, when the controllable is not in the air,
    -- the controllable current waypoint is probably the airbase...
    -- Thus, if we would take the current waypoint as the startpoint, upon take-off, the controllable flies
    -- immediately back to the airbase, and this is not correct.
    -- Therefore, when on a runway, get as the current route point a random point within the PatrolZone.
    -- This will make the plane fly immediately to the patrol zone.
    
    if self.Controllable:InAir() == false then
      self:T( "Not in the air, finding route path within PatrolZone" )
      local CurrentVec2 = self.Controllable:GetVec2()
      if not CurrentVec2 then return end
      --Done: Create GetAltitude function for GROUP, and delete GetUnit(1).
      local CurrentAltitude = self.Controllable:GetAltitude()
      local CurrentPointVec3 = COORDINATE:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y )
      local ToPatrolZoneSpeed = self.PatrolMaxSpeed
      local CurrentRoutePoint = CurrentPointVec3:WaypointAir( 
          self.PatrolAltType, 
          COORDINATE.WaypointType.TakeOffParking, 
          COORDINATE.WaypointAction.FromParkingArea, 
          ToPatrolZoneSpeed, 
          true 
        )
      PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint
    else
      self:T( "In the air, finding route path within PatrolZone" )
      local CurrentVec2 = self.Controllable:GetVec2()
      if not CurrentVec2 then return end
      --DONE: Create GetAltitude function for GROUP, and delete GetUnit(1).
      local CurrentAltitude = self.Controllable:GetAltitude()
      local CurrentPointVec3 = COORDINATE:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y )
      local ToPatrolZoneSpeed = self.PatrolMaxSpeed
      local CurrentRoutePoint = CurrentPointVec3:WaypointAir( 
          self.PatrolAltType, 
          COORDINATE.WaypointType.TurningPoint, 
          COORDINATE.WaypointAction.TurningPoint, 
          ToPatrolZoneSpeed, 
          true 
        )
      PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint
    end    
    
    
    --- Define a random point in the @{Core.Zone}. The AI will fly to that point within the zone.
    
      --- Find a random 2D point in PatrolZone.
    local ToTargetVec2 = self.PatrolZone:GetRandomVec2()
    self:T2( ToTargetVec2 )

    --- Define Speed and Altitude.
    local ToTargetAltitude = math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude )
    local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed )
    self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } )
    
    --- Obtain a 3D @{Point} from the 2D point + altitude.
    local ToTargetPointVec3 = COORDINATE:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y )
    
    --- Create a route point of type air.
    local ToTargetRoutePoint = ToTargetPointVec3:WaypointAir( 
      self.PatrolAltType, 
      COORDINATE.WaypointType.TurningPoint, 
      COORDINATE.WaypointAction.TurningPoint, 
      ToTargetSpeed, 
      true 
    )
    
    --self.CoordTest:SpawnFromVec3( ToTargetPointVec3:GetVec3() )
    
    --ToTargetPointVec3:SmokeRed()

    PatrolRoute[#PatrolRoute+1] = ToTargetRoutePoint
    
    --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable...
    self.Controllable:WayPointInitialize( PatrolRoute )
    
    --- Do a trick, link the NewPatrolRoute function of the PATROLGROUP object to the AIControllable in a temporary variable ...
    self.Controllable:SetState( self.Controllable, "PatrolZone", self )
    self.Controllable:WayPointFunction( #PatrolRoute, 1, "AI_PATROL_ZONE:_NewPatrolRoute" )

    --- NOW ROUTE THE GROUP!
    self.Controllable:WayPointExecute( 1, 2 )
  end

end

-- @param #AI_PATROL_ZONE self
function AI_PATROL_ZONE:onbeforeStatus()

  return self.CheckStatus
end

-- @param #AI_PATROL_ZONE self
function AI_PATROL_ZONE:onafterStatus()
  self:F2()

  if self.Controllable and self.Controllable:IsAlive() then
  
    local RTB = false
    
    local Fuel = self.Controllable:GetFuelMin()
    if Fuel < self.PatrolFuelThresholdPercentage then
      self:T( self.Controllable:GetName() .. " is out of fuel:" .. Fuel .. ", RTB!" )
      local OldAIControllable = self.Controllable
      
      local OrbitTask = OldAIControllable:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed )
      local TimedOrbitTask = OldAIControllable:TaskControlled( OrbitTask, OldAIControllable:TaskCondition(nil,nil,nil,nil,self.PatrolOutOfFuelOrbitTime,nil ) )
      OldAIControllable:SetTask( TimedOrbitTask, 10 )

      RTB = true
    end
    
    -- TODO: Check GROUP damage function.
    local Damage = self.Controllable:GetLife()
    if Damage <= self.PatrolDamageThreshold then
      self:T( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" )
      RTB = true
    end
    
    if self:IsInstanceOf("AI_CAS") or self:IsInstanceOf("AI_BAI") then
      local atotal,shells,rockets,bombs,missiles = self.Controllable:GetAmmunition()
      local arelevant = rockets+bombs   
      if arelevant == 0 or missiles == 0 then 
        RTB = true
        self:T({total=atotal,shells=shells,rockets=rockets,bombs=bombs,missiles=missiles})
        self:T( self.Controllable:GetName() .. " is out of ammo, RTB!" ) 
      end
    end
    
    if RTB == true then
      self:RTB()
    else
      self:__Status( 60 ) -- Execute the Patrol event after 30 seconds.
    end
  end
end

-- @param #AI_PATROL_ZONE self
function AI_PATROL_ZONE:onafterRTB()
  self:F2()

  if self.Controllable and self.Controllable:IsAlive() then

    self:SetDetectionOff()
    self.CheckStatus = false
    
    local PatrolRoute = {}
  
    --- Calculate the current route point.
    local CurrentVec2 = self.Controllable:GetVec2()
    if not CurrentVec2 then return end
    --DONE: Create GetAltitude function for GROUP, and delete GetUnit(1).
    --local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude()
    local CurrentAltitude = self.Controllable:GetAltitude()
    local CurrentPointVec3 = COORDINATE:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y )
    local ToPatrolZoneSpeed = self.PatrolMaxSpeed
    local CurrentRoutePoint = CurrentPointVec3:WaypointAir( 
        self.PatrolAltType, 
        COORDINATE.WaypointType.TurningPoint, 
        COORDINATE.WaypointAction.TurningPoint, 
        ToPatrolZoneSpeed, 
        true 
      )
    
    PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint
    
    --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable...
    self.Controllable:WayPointInitialize( PatrolRoute )
  
    --- NOW ROUTE THE GROUP!
    self.Controllable:WayPointExecute( 1, 1 )
    
  end
    
end

-- @param #AI_PATROL_ZONE self
function AI_PATROL_ZONE:onafterDead()
  self:SetDetectionOff()
  self:SetStatusOff()
end

-- @param #AI_PATROL_ZONE self
-- @param Core.Event#EVENTDATA EventData
function AI_PATROL_ZONE:OnCrash( EventData )

  if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then
    if #self.Controllable:GetUnits() == 1 then
      self:__Crash( 1, EventData )
    end
  end
end

-- @param #AI_PATROL_ZONE self
-- @param Core.Event#EVENTDATA EventData
function AI_PATROL_ZONE:OnEjection( EventData )

  if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then
    self:__Eject( 1, EventData )
  end
end

-- @param #AI_PATROL_ZONE self
-- @param Core.Event#EVENTDATA EventData
function AI_PATROL_ZONE:OnPilotDead( EventData )

  if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then
    self:__PilotDead( 1, EventData )
  end
end
--- **AI** - Perform Combat Air Patrolling (CAP) for airplanes.
--
-- **Features:**
-- 
--   * Patrol AI airplanes within a given zone.
--   * Trigger detected events when enemy airplanes are detected.
--   * Manage a fuel threshold to RTB on time.
--   * Engage the enemy when detected.
--
-- ===
-- 
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_CAP)
-- 
-- ===
-- 
-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1YCyPxJgoZn-CfhwyeW65L)
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions:
--
--   * **Quax**: Concept, Advice & Testing.
--   * **Pikey**: Concept, Advice & Testing.
--   * **Gunterlund**: Test case revision.
--   * **Whisper**: Testing.
--   * **Delta99**: Testing.
-- 
-- ===       
--
-- @module AI.AI_CAP
-- @image AI_Combat_Air_Patrol.JPG

-- @type AI_CAP_ZONE
-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling.
-- @field Core.Zone#ZONE_BASE TargetZone The @{Core.Zone} where the patrol needs to be executed.
-- @extends AI.AI_Patrol#AI_PATROL_ZONE

--- Implements the core functions to patrol a @{Core.Zone} by an AI @{Wrapper.Controllable} or @{Wrapper.Group} 
-- and automatically engage any airborne enemies that are within a certain range or within a certain zone.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- ![Process](..\Presentations\AI_CAP\Dia3.JPG)
-- 
-- The AI_CAP_ZONE is assigned a @{Wrapper.Group} and this must be done before the AI_CAP_ZONE process can be started using the **Start** event.
-- 
-- ![Process](..\Presentations\AI_CAP\Dia4.JPG)
-- 
-- The AI will fly towards the random 3D point within the patrol zone, using a random speed within the given altitude and speed limits.
-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits.
-- 
-- ![Process](..\Presentations\AI_CAP\Dia5.JPG)
-- 
-- This cycle will continue.
-- 
-- ![Process](..\Presentations\AI_CAP\Dia6.JPG)
-- 
-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event.
--
-- ![Process](..\Presentations\AI_CAP\Dia9.JPG)
-- 
-- When enemies are detected, the AI will automatically engage the enemy.
-- 
-- ![Process](..\Presentations\AI_CAP\Dia10.JPG)
-- 
-- Until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB.
-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land.
-- 
-- ![Process](..\Presentations\AI_CAP\Dia13.JPG)
-- 
-- ## 1. AI_CAP_ZONE constructor
--   
--   * @{#AI_CAP_ZONE.New}(): Creates a new AI_CAP_ZONE object.
-- 
-- ## 2. AI_CAP_ZONE is a FSM
-- 
-- ![Process](..\Presentations\AI_CAP\Dia2.JPG)
-- 
-- ### 2.1 AI_CAP_ZONE States
-- 
--   * **None** ( Group ): The process is not started yet.
--   * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone.
--   * **Engaging** ( Group ): The AI is engaging the bogeys.
--   * **Returning** ( Group ): The AI is returning to Base..
-- 
-- ### 2.2 AI_CAP_ZONE Events
-- 
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone.
--   * **@{#AI_CAP_ZONE.Engage}**: Let the AI engage the bogeys.
--   * **@{#AI_CAP_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets.
--   * **@{#AI_CAP_ZONE.Destroy}**: The AI has destroyed a bogey @{Wrapper.Unit}.
--   * **@{#AI_CAP_ZONE.Destroyed}**: The AI has destroyed all bogeys @{Wrapper.Unit}s assigned in the CAS task.
--   * **Status** ( Group ): The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB.
--
-- ## 3. Set the Range of Engagement
-- 
-- ![Range](..\Presentations\AI_CAP\Dia11.JPG)
-- 
-- An optional range can be set in meters, 
-- that will define when the AI will engage with the detected airborne enemy targets.
-- The range can be beyond or smaller than the range of the Patrol Zone.
-- The range is applied at the position of the AI.
-- Use the method @{#AI_CAP_ZONE.SetEngageRange}() to define that range.
--
-- ## 4. Set the Zone of Engagement
-- 
-- ![Zone](..\Presentations\AI_CAP\Dia12.JPG)
-- 
-- An optional @{Core.Zone} can be set, 
-- that will define when the AI will engage with the detected airborne enemy targets.
-- Use the method @{#AI_CAP_ZONE.SetEngageZone}() to define that Zone.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_CAP_ZONE
AI_CAP_ZONE = {
  ClassName = "AI_CAP_ZONE",
}

--- Creates a new AI_CAP_ZONE object
-- @param #AI_CAP_ZONE self
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO
-- @return #AI_CAP_ZONE self
function AI_CAP_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType )

  -- Inherits from BASE
  local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAP_ZONE

  self.Accomplished = false
  self.Engaging = false

  self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE.

  --- OnBefore Transition Handler for Event Engage.
  -- @function [parent=#AI_CAP_ZONE] OnBeforeEngage
  -- @param #AI_CAP_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.

  --- OnAfter Transition Handler for Event Engage.
  -- @function [parent=#AI_CAP_ZONE] OnAfterEngage
  -- @param #AI_CAP_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.

  --- Synchronous Event Trigger for Event Engage.
  -- @function [parent=#AI_CAP_ZONE] Engage
  -- @param #AI_CAP_ZONE self

  --- Asynchronous Event Trigger for Event Engage.
  -- @function [parent=#AI_CAP_ZONE] __Engage
  -- @param #AI_CAP_ZONE self
  -- @param #number Delay The delay in seconds.

--- OnLeave Transition Handler for State Engaging.
-- @function [parent=#AI_CAP_ZONE] OnLeaveEngaging
-- @param #AI_CAP_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnEnter Transition Handler for State Engaging.
-- @function [parent=#AI_CAP_ZONE] OnEnterEngaging
-- @param #AI_CAP_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

  self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE.

  --- OnBefore Transition Handler for Event Fired.
  -- @function [parent=#AI_CAP_ZONE] OnBeforeFired
  -- @param #AI_CAP_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.

  --- OnAfter Transition Handler for Event Fired.
  -- @function [parent=#AI_CAP_ZONE] OnAfterFired
  -- @param #AI_CAP_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.

  --- Synchronous Event Trigger for Event Fired.
  -- @function [parent=#AI_CAP_ZONE] Fired
  -- @param #AI_CAP_ZONE self

  --- Asynchronous Event Trigger for Event Fired.
  -- @function [parent=#AI_CAP_ZONE] __Fired
  -- @param #AI_CAP_ZONE self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE.

  --- OnBefore Transition Handler for Event Destroy.
  -- @function [parent=#AI_CAP_ZONE] OnBeforeDestroy
  -- @param #AI_CAP_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.

  --- OnAfter Transition Handler for Event Destroy.
  -- @function [parent=#AI_CAP_ZONE] OnAfterDestroy
  -- @param #AI_CAP_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.

  --- Synchronous Event Trigger for Event Destroy.
  -- @function [parent=#AI_CAP_ZONE] Destroy
  -- @param #AI_CAP_ZONE self

  --- Asynchronous Event Trigger for Event Destroy.
  -- @function [parent=#AI_CAP_ZONE] __Destroy
  -- @param #AI_CAP_ZONE self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE.

  --- OnBefore Transition Handler for Event Abort.
  -- @function [parent=#AI_CAP_ZONE] OnBeforeAbort
  -- @param #AI_CAP_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.

  --- OnAfter Transition Handler for Event Abort.
  -- @function [parent=#AI_CAP_ZONE] OnAfterAbort
  -- @param #AI_CAP_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.

  --- Synchronous Event Trigger for Event Abort.
  -- @function [parent=#AI_CAP_ZONE] Abort
  -- @param #AI_CAP_ZONE self

  --- Asynchronous Event Trigger for Event Abort.
  -- @function [parent=#AI_CAP_ZONE] __Abort
  -- @param #AI_CAP_ZONE self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAP_ZONE.

  --- OnBefore Transition Handler for Event Accomplish.
  -- @function [parent=#AI_CAP_ZONE] OnBeforeAccomplish
  -- @param #AI_CAP_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.

  --- OnAfter Transition Handler for Event Accomplish.
  -- @function [parent=#AI_CAP_ZONE] OnAfterAccomplish
  -- @param #AI_CAP_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.

  --- Synchronous Event Trigger for Event Accomplish.
  -- @function [parent=#AI_CAP_ZONE] Accomplish
  -- @param #AI_CAP_ZONE self

  --- Asynchronous Event Trigger for Event Accomplish.
  -- @function [parent=#AI_CAP_ZONE] __Accomplish
  -- @param #AI_CAP_ZONE self
  -- @param #number Delay The delay in seconds.  

  return self
end

--- Set the Engage Zone which defines where the AI will engage bogies. 
-- @param #AI_CAP_ZONE self
-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAP.
-- @return #AI_CAP_ZONE self
function AI_CAP_ZONE:SetEngageZone( EngageZone )
  self:F2()

  if EngageZone then
    self.EngageZone = EngageZone
  else
    self.EngageZone = nil
  end
end

--- Set the Engage Range when the AI will engage with airborne enemies. 
-- @param #AI_CAP_ZONE self
-- @param #number EngageRange The Engage Range.
-- @return #AI_CAP_ZONE self
function AI_CAP_ZONE:SetEngageRange( EngageRange )
  self:F2()

  if EngageRange then  
    self.EngageRange = EngageRange
  else
    self.EngageRange = nil
  end
end

--- onafter State Transition for Event Start.
-- @param #AI_CAP_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_CAP_ZONE:onafterStart( Controllable, From, Event, To )

  -- Call the parent Start event handler
  self:GetParent(self).onafterStart( self, Controllable, From, Event, To )
  self:HandleEvent( EVENTS.Dead )

end

-- @param AI.AI_CAP#AI_CAP_ZONE 
-- @param Wrapper.Group#GROUP EngageGroup
function AI_CAP_ZONE.EngageRoute( EngageGroup, Fsm )

  EngageGroup:F( { "AI_CAP_ZONE.EngageRoute:", EngageGroup:GetName() } )

  if EngageGroup:IsAlive() then
    Fsm:__Engage( 1 )
  end
end

-- @param #AI_CAP_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_CAP_ZONE:onbeforeEngage( Controllable, From, Event, To )
  
  if self.Accomplished == true then
    return false
  end
end

-- @param #AI_CAP_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_CAP_ZONE:onafterDetected( Controllable, From, Event, To )

  if From ~= "Engaging" then

    local Engage = false

    for DetectedUnit, Detected in pairs( self.DetectedUnits ) do

      local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT
      self:T( DetectedUnit )
      if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then
        Engage = true
        break
      end
    end

    if Engage == true then
      self:F( 'Detected -> Engaging' )
      self:__Engage( 1 )
    end
  end
end

-- @param #AI_CAP_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_CAP_ZONE:onafterAbort( Controllable, From, Event, To )
  Controllable:ClearTasks()
  self:__Route( 1 )
end

-- @param #AI_CAP_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_CAP_ZONE:onafterEngage( Controllable, From, Event, To )

  if Controllable and Controllable:IsAlive() then

    local EngageRoute = {}

    --- Calculate the current route point.
    local CurrentVec2 = self.Controllable:GetVec2()

    if not CurrentVec2 then return self end

    --DONE: Create GetAltitude function for GROUP, and delete GetUnit(1).
    local CurrentAltitude = self.Controllable:GetAltitude()
    local CurrentPointVec3 = COORDINATE:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y )
    local ToEngageZoneSpeed = self.PatrolMaxSpeed
    local CurrentRoutePoint = CurrentPointVec3:WaypointAir(
        self.PatrolAltType,
        COORDINATE.WaypointType.TurningPoint,
        COORDINATE.WaypointAction.TurningPoint,
        ToEngageZoneSpeed,
        true
      )

    EngageRoute[#EngageRoute+1] = CurrentRoutePoint

     --- Find a random 2D point in PatrolZone.
    local ToTargetVec2 = self.PatrolZone:GetRandomVec2()
    self:T2( ToTargetVec2 )

    --- Define Speed and Altitude.
    local ToTargetAltitude = math.random( self.EngageFloorAltitude, self.EngageCeilingAltitude )
    local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed )
    self:T2( { self.PatrolMinSpeed, self.PatrolMaxSpeed, ToTargetSpeed } )

    --- Obtain a 3D @{Point} from the 2D point + altitude.
    local ToTargetPointVec3 = COORDINATE:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y )

    --- Create a route point of type air.
    local ToPatrolRoutePoint = ToTargetPointVec3:WaypointAir(
      self.PatrolAltType,
      COORDINATE.WaypointType.TurningPoint,
      COORDINATE.WaypointAction.TurningPoint,
      ToTargetSpeed,
      true
    )

    EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint

    Controllable:OptionROEOpenFire()
    Controllable:OptionROTEvadeFire()

    local AttackTasks = {}

    for DetectedUnit, Detected in pairs( self.DetectedUnits ) do
      local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT
      self:T( { DetectedUnit, DetectedUnit:IsAlive(), DetectedUnit:IsAir() } )
      if DetectedUnit:IsAlive() and DetectedUnit:IsAir() then
        if self.EngageZone then
          if DetectedUnit:IsInZone( self.EngageZone ) then
            self:F( {"Within Zone and Engaging ", DetectedUnit } )
            AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit )
          end
        else 
          if self.EngageRange then
            if DetectedUnit:GetPointVec3():Get2DDistance(Controllable:GetPointVec3() ) <= self.EngageRange then
              self:F( {"Within Range and Engaging", DetectedUnit } )
              AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit )
            end
          else
            AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit )
          end
        end
      else
        self.DetectedUnits[DetectedUnit] = nil
      end
    end

    if #AttackTasks == 0 then
      self:F("No targets found -> Going back to Patrolling")
      self:__Abort( 1 )
      self:__Route( 1 )
      self:SetDetectionActivated()
    else

      AttackTasks[#AttackTasks+1] = Controllable:TaskFunction( "AI_CAP_ZONE.EngageRoute", self )
      EngageRoute[1].task = Controllable:TaskCombo( AttackTasks )

      self:SetDetectionDeactivated()
    end

    Controllable:Route( EngageRoute, 0.5 )

  end
end

-- @param #AI_CAP_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_CAP_ZONE:onafterAccomplish( Controllable, From, Event, To )
  self.Accomplished = true
  self:SetDetectionOff()
end

-- @param #AI_CAP_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @param Core.Event#EVENTDATA EventData
function AI_CAP_ZONE:onafterDestroy( Controllable, From, Event, To, EventData )

  if EventData.IniUnit then
    self.DetectedUnits[EventData.IniUnit] = nil
  end
end

-- @param #AI_CAP_ZONE self
-- @param Core.Event#EVENTDATA EventData
function AI_CAP_ZONE:OnEventDead( EventData )
  self:F( { "EventDead", EventData } )

  if EventData.IniDCSUnit then
    if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit] then
      self:__Destroy( 1, EventData )
    end
  end
end
--- **AI** - Perform Close Air Support (CAS) near friendlies.
--
-- **Features:**
-- 
--   * Hold and standby within a patrol zone.
--   * Engage upon command the enemies within an engagement zone.
--   * Loop the zone until all enemies are eliminated.
--   * Trigger different events upon the results achieved.
--   * After combat, return to the patrol zone and hold.
--   * RTB when commanded or after fuel.
--
-- ===
-- 
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_CAS)
-- 
-- ===
-- 
-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl3JBO1WDqqpyYRRmIkR2ir2)
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
--
--   * **Quax**: Concept, Advice & Testing.
--   * **Pikey**: Concept, Advice & Testing.
--   * **Gunterlund**: Test case revision.
--
-- ===
--
-- @module AI.AI_CAS
-- @image AI_Close_Air_Support.JPG

--- AI_CAS_ZONE class
-- @type AI_CAS_ZONE
-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling.
-- @field Core.Zone#ZONE_BASE TargetZone The @{Core.Zone} where the patrol needs to be executed.
-- @extends AI.AI_Patrol#AI_PATROL_ZONE

--- Implements the core functions to provide Close Air Support in an Engage @{Core.Zone} by an AIR @{Wrapper.Controllable} or @{Wrapper.Group}.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- The AI_CAS_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone.
-- 
-- ![HoldAndEngage](..\Presentations\AI_CAS\Dia3.JPG)
-- 
-- The AI_CAS_ZONE is assigned a @{Wrapper.Group} and this must be done before the AI_CAS_ZONE process can be started through the **Start** event.
--  
-- ![Start Event](..\Presentations\AI_CAS\Dia4.JPG)
-- 
-- Upon started, The AI will **Route** itself towards the random 3D point within a patrol zone, 
-- using a random speed within the given altitude and speed limits.
-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits.
-- This cycle will continue until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB.
-- 
-- ![Route Event](..\Presentations\AI_CAS\Dia5.JPG)
-- 
-- When the AI is commanded to provide Close Air Support (through the event **Engage**), the AI will fly towards the Engage Zone.
-- Any target that is detected in the Engage Zone will be reported and will be destroyed by the AI.
-- 
-- ![Engage Event](..\Presentations\AI_CAS\Dia6.JPG)
-- 
-- The AI will detect the targets and will only destroy the targets within the Engage Zone.
-- 
-- ![Engage Event](..\Presentations\AI_CAS\Dia7.JPG)
-- 
-- Every target that is destroyed, is reported< by the AI.
-- 
-- ![Engage Event](..\Presentations\AI_CAS\Dia8.JPG)
-- 
-- Note that the AI does not know when the Engage Zone is cleared, and therefore will keep circling in the zone. 
--
-- ![Engage Event](..\Presentations\AI_CAS\Dia9.JPG)
-- 
-- Until it is notified through the event **Accomplish**, which is to be triggered by an observing party:
-- 
--   * a FAC
--   * a timed event
--   * a menu option selected by a human
--   * a condition
--   * others ...
-- 
-- ![Engage Event](..\Presentations\AI_CAS\Dia10.JPG)
-- 
-- When the AI has accomplished the CAS, it will fly back to the Patrol Zone.
-- 
-- ![Engage Event](..\Presentations\AI_CAS\Dia11.JPG)
-- 
-- It will keep patrolling there, until it is notified to RTB or move to another CAS Zone.
-- It can be notified to go RTB through the **RTB** event.
-- 
-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land.
-- 
-- ![Engage Event](..\Presentations\AI_CAS\Dia12.JPG)
--
-- ## AI_CAS_ZONE constructor
--
--   * @{#AI_CAS_ZONE.New}(): Creates a new AI_CAS_ZONE object.
--
-- ## AI_CAS_ZONE is a FSM
-- 
-- ![Process](..\Presentations\AI_CAS\Dia2.JPG)
-- 
-- ### 2.1. AI_CAS_ZONE States
-- 
--   * **None** ( Group ): The process is not started yet.
--   * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone.
--   * **Engaging** ( Group ): The AI is engaging the targets in the Engage Zone, executing CAS.
--   * **Returning** ( Group ): The AI is returning to Base..
-- 
-- ### 2.2. AI_CAS_ZONE Events
-- 
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone.
--   * **@{#AI_CAS_ZONE.Engage}**: Engage the AI to provide CAS in the Engage Zone, destroying any target it finds.
--   * **@{#AI_CAS_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets.
--   * **@{#AI_CAS_ZONE.Destroy}**: The AI has destroyed a target @{Wrapper.Unit}.
--   * **@{#AI_CAS_ZONE.Destroyed}**: The AI has destroyed all target @{Wrapper.Unit}s assigned in the CAS task.
--   * **Status**: The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_CAS_ZONE
AI_CAS_ZONE = {
  ClassName = "AI_CAS_ZONE",
}



--- Creates a new AI_CAS_ZONE object
-- @param #AI_CAS_ZONE self
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h.
-- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO
-- @return #AI_CAS_ZONE self
function AI_CAS_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType )

  -- Inherits from BASE
  local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_CAS_ZONE

  self.EngageZone = EngageZone
  self.Accomplished = false
  
  self:SetDetectionZone( self.EngageZone )

  self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE.

  --- OnBefore Transition Handler for Event Engage.
  -- @function [parent=#AI_CAS_ZONE] OnBeforeEngage
  -- @param #AI_CAS_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Engage.
  -- @function [parent=#AI_CAS_ZONE] OnAfterEngage
  -- @param #AI_CAS_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Engage.
  -- @function [parent=#AI_CAS_ZONE] Engage
  -- @param #AI_CAS_ZONE self
  -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone.
  -- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement.
  -- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. 
  -- If parameter is not defined the unit / controllable will choose expend on its own discretion.
  -- Use the structure @{DCS#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack.
  -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
  -- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.
  
  --- Asynchronous Event Trigger for Event Engage.
  -- @function [parent=#AI_CAS_ZONE] __Engage
  -- @param #AI_CAS_ZONE self
  -- @param #number Delay The delay in seconds.
  -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone.
  -- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement.
  -- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. 
  -- If parameter is not defined the unit / controllable will choose expend on its own discretion.
  -- Use the structure @{DCS#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack.
  -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
  -- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.

--- OnLeave Transition Handler for State Engaging.
-- @function [parent=#AI_CAS_ZONE] OnLeaveEngaging
-- @param #AI_CAS_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnEnter Transition Handler for State Engaging.
-- @function [parent=#AI_CAS_ZONE] OnEnterEngaging
-- @param #AI_CAS_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

  self:AddTransition( "Engaging", "Target", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE.

  self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE.
  
  --- OnBefore Transition Handler for Event Fired.
  -- @function [parent=#AI_CAS_ZONE] OnBeforeFired
  -- @param #AI_CAS_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Fired.
  -- @function [parent=#AI_CAS_ZONE] OnAfterFired
  -- @param #AI_CAS_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Fired.
  -- @function [parent=#AI_CAS_ZONE] Fired
  -- @param #AI_CAS_ZONE self
  
  --- Asynchronous Event Trigger for Event Fired.
  -- @function [parent=#AI_CAS_ZONE] __Fired
  -- @param #AI_CAS_ZONE self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE.

  --- OnBefore Transition Handler for Event Destroy.
  -- @function [parent=#AI_CAS_ZONE] OnBeforeDestroy
  -- @param #AI_CAS_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Destroy.
  -- @function [parent=#AI_CAS_ZONE] OnAfterDestroy
  -- @param #AI_CAS_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Destroy.
  -- @function [parent=#AI_CAS_ZONE] Destroy
  -- @param #AI_CAS_ZONE self
  
  --- Asynchronous Event Trigger for Event Destroy.
  -- @function [parent=#AI_CAS_ZONE] __Destroy
  -- @param #AI_CAS_ZONE self
  -- @param #number Delay The delay in seconds.


  self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE.

  --- OnBefore Transition Handler for Event Abort.
  -- @function [parent=#AI_CAS_ZONE] OnBeforeAbort
  -- @param #AI_CAS_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Abort.
  -- @function [parent=#AI_CAS_ZONE] OnAfterAbort
  -- @param #AI_CAS_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Abort.
  -- @function [parent=#AI_CAS_ZONE] Abort
  -- @param #AI_CAS_ZONE self
  
  --- Asynchronous Event Trigger for Event Abort.
  -- @function [parent=#AI_CAS_ZONE] __Abort
  -- @param #AI_CAS_ZONE self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_CAS_ZONE.

  --- OnBefore Transition Handler for Event Accomplish.
  -- @function [parent=#AI_CAS_ZONE] OnBeforeAccomplish
  -- @param #AI_CAS_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Accomplish.
  -- @function [parent=#AI_CAS_ZONE] OnAfterAccomplish
  -- @param #AI_CAS_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Accomplish.
  -- @function [parent=#AI_CAS_ZONE] Accomplish
  -- @param #AI_CAS_ZONE self
  
  --- Asynchronous Event Trigger for Event Accomplish.
  -- @function [parent=#AI_CAS_ZONE] __Accomplish
  -- @param #AI_CAS_ZONE self
  -- @param #number Delay The delay in seconds.  

  return self
end


--- Set the Engage Zone where the AI is performing CAS. Note that if the EngageZone is changed, the AI needs to re-detect targets.
-- @param #AI_CAS_ZONE self
-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing CAS.
-- @return #AI_CAS_ZONE self
function AI_CAS_ZONE:SetEngageZone( EngageZone )
  self:F2()

  if EngageZone then  
    self.EngageZone = EngageZone
  else
    self.EngageZone = nil
  end
end



--- onafter State Transition for Event Start.
-- @param #AI_CAS_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_CAS_ZONE:onafterStart( Controllable, From, Event, To )

  -- Call the parent Start event handler
  self:GetParent(self).onafterStart( self, Controllable, From, Event, To )
  self:HandleEvent( EVENTS.Dead )
  
  self:SetDetectionDeactivated() -- When not engaging, set the detection off.
end

-- @param AI.AI_CAS#AI_CAS_ZONE 
-- @param Wrapper.Group#GROUP EngageGroup
function AI_CAS_ZONE.EngageRoute( EngageGroup, Fsm )

  EngageGroup:F( { "AI_CAS_ZONE.EngageRoute:", EngageGroup:GetName() } )

  if EngageGroup:IsAlive() then
    Fsm:__Engage( 1, Fsm.EngageSpeed, Fsm.EngageAltitude, Fsm.EngageWeaponExpend, Fsm.EngageAttackQty, Fsm.EngageDirection )
  end
end


-- @param #AI_CAS_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_CAS_ZONE:onbeforeEngage( Controllable, From, Event, To )
  
  if self.Accomplished == true then
    return false
  end
end

-- @param #AI_CAS_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_CAS_ZONE:onafterTarget( Controllable, From, Event, To )

  if Controllable:IsAlive() then

    local AttackTasks = {}

    for DetectedUnit, Detected in pairs( self.DetectedUnits ) do
      local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT
      if DetectedUnit:IsAlive() then
        if DetectedUnit:IsInZone( self.EngageZone ) then
          if Detected == true then
            self:F( {"Target: ", DetectedUnit } )
            self.DetectedUnits[DetectedUnit] = false
            local AttackTask = Controllable:TaskAttackUnit( DetectedUnit, false, self.EngageWeaponExpend, self.EngageAttackQty, self.EngageDirection, self.EngageAltitude, nil )
            self.Controllable:PushTask( AttackTask, 1 )
          end
        end
      else
        self.DetectedUnits[DetectedUnit] = nil
      end
    end

    self:__Target( -10 )

  end
end


-- @param #AI_CAS_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_CAS_ZONE:onafterAbort( Controllable, From, Event, To )
  Controllable:ClearTasks()
  self:__Route( 1 )
end

-- @param #AI_CAS_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone.
-- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement.
-- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion.
-- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
-- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.
function AI_CAS_ZONE:onafterEngage( Controllable, From, Event, To, 
                                    EngageSpeed, 
                                    EngageAltitude, 
                                    EngageWeaponExpend, 
                                    EngageAttackQty, 
                                    EngageDirection )
  self:F("onafterEngage")

  self.EngageSpeed = EngageSpeed or 400
  self.EngageAltitude = EngageAltitude or 2000
  self.EngageWeaponExpend = EngageWeaponExpend
  self.EngageAttackQty = EngageAttackQty
  self.EngageDirection = EngageDirection

  if Controllable:IsAlive() then

    Controllable:OptionROEOpenFire()
    Controllable:OptionROTVertical()

    local EngageRoute = {}

    --- Calculate the current route point.
    local CurrentVec2 = self.Controllable:GetVec2()
    
    --DONE: Create GetAltitude function for GROUP, and delete GetUnit(1).
    local CurrentAltitude = self.Controllable:GetAltitude()
    local CurrentPointVec3 = COORDINATE:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y )
    local ToEngageZoneSpeed = self.PatrolMaxSpeed
    local CurrentRoutePoint = CurrentPointVec3:WaypointAir( 
        self.PatrolAltType, 
        COORDINATE.WaypointType.TurningPoint, 
        COORDINATE.WaypointAction.TurningPoint, 
        self.EngageSpeed, 
        true 
      )
    
    EngageRoute[#EngageRoute+1] = CurrentRoutePoint

    local AttackTasks = {}

    for DetectedUnit, Detected in pairs( self.DetectedUnits ) do
      local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT
      self:T( DetectedUnit )
      if DetectedUnit:IsAlive() then
        if DetectedUnit:IsInZone( self.EngageZone ) then
          self:F( {"Engaging ", DetectedUnit } )
          AttackTasks[#AttackTasks+1] = Controllable:TaskAttackUnit( DetectedUnit, 
                                                                     true, 
                                                                     EngageWeaponExpend, 
                                                                     EngageAttackQty, 
                                                                     EngageDirection 
                                                                   )
        end
      else
        self.DetectedUnits[DetectedUnit] = nil
      end
    end

    AttackTasks[#AttackTasks+1] = Controllable:TaskFunction( "AI_CAS_ZONE.EngageRoute", self )
    EngageRoute[#EngageRoute].task = Controllable:TaskCombo( AttackTasks )

    --- Define a random point in the @{Core.Zone}. The AI will fly to that point within the zone.
    
      --- Find a random 2D point in EngageZone.
    local ToTargetVec2 = self.EngageZone:GetRandomVec2()
    self:T2( ToTargetVec2 )

    --- Obtain a 3D @{Point} from the 2D point + altitude.
    local ToTargetPointVec3 = COORDINATE:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y )
    
    --- Create a route point of type air.
    local ToTargetRoutePoint = ToTargetPointVec3:WaypointAir( 
      self.PatrolAltType, 
      COORDINATE.WaypointType.TurningPoint, 
      COORDINATE.WaypointAction.TurningPoint, 
      self.EngageSpeed, 
      true 
    )
    
    EngageRoute[#EngageRoute+1] = ToTargetRoutePoint
  
    Controllable:Route( EngageRoute, 0.5 )
    
    self:SetRefreshTimeInterval( 2 )
    self:SetDetectionActivated()
    self:__Target( -2 ) -- Start targeting
  end
end


-- @param #AI_CAS_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_CAS_ZONE:onafterAccomplish( Controllable, From, Event, To )
  self.Accomplished = true
  self:SetDetectionDeactivated()
end


-- @param #AI_CAS_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @param Core.Event#EVENTDATA EventData
function AI_CAS_ZONE:onafterDestroy( Controllable, From, Event, To, EventData )

  if EventData.IniUnit then
    self.DetectedUnits[EventData.IniUnit] = nil
  end
end


-- @param #AI_CAS_ZONE self
-- @param Core.Event#EVENTDATA EventData
function AI_CAS_ZONE:OnEventDead( EventData )
  self:F( { "EventDead", EventData } )

  if EventData.IniDCSUnit then
    if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit] then
      self:__Destroy( 1, EventData )
    end
  end
end


--- **AI** - Peform Battlefield Area Interdiction (BAI) within an engagement zone.
--
-- **Features:**
-- 
--   * Hold and standby within a patrol zone.
--   * Engage upon command the assigned targets within an engagement zone.
--   * Loop the zone until all targets are eliminated.
--   * Trigger different events upon the results achieved.
--   * After combat, return to the patrol zone and hold.
--   * RTB when commanded or after out of fuel.
--
-- ===
-- 
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_BAI)
-- 
-- ===
-- 
-- ### [YouTube Playlist]()
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
--   * **Gunterlund**: Test case revision.
-- 
-- ===
--
-- @module AI.AI_BAI
-- @image AI_Battlefield_Air_Interdiction.JPG


--- AI_BAI_ZONE class
-- @type AI_BAI_ZONE
-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling.
-- @field Core.Zone#ZONE_BASE TargetZone The @{Core.Zone} where the patrol needs to be executed.
-- @extends AI.AI_Patrol#AI_PATROL_ZONE

--- Implements the core functions to provide BattleGround Air Interdiction in an Engage @{Core.Zone} by an AIR @{Wrapper.Controllable} or @{Wrapper.Group}.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- The AI_BAI_ZONE runs a process. It holds an AI in a Patrol Zone and when the AI is commanded to engage, it will fly to an Engage Zone.
-- 
-- ![HoldAndEngage](..\Presentations\AI_BAI\Dia3.JPG)
-- 
-- The AI_BAI_ZONE is assigned a @{Wrapper.Group} and this must be done before the AI_BAI_ZONE process can be started through the **Start** event.
--  
-- ![Start Event](..\Presentations\AI_BAI\Dia4.JPG)
-- 
-- Upon started, The AI will **Route** itself towards the random 3D point within a patrol zone, 
-- using a random speed within the given altitude and speed limits.
-- Upon arrival at the 3D point, a new random 3D point will be selected within the patrol zone using the given limits.
-- This cycle will continue until a fuel or damage threshold has been reached by the AI, or when the AI is commanded to RTB.
-- 
-- ![Route Event](..\Presentations\AI_BAI\Dia5.JPG)
-- 
-- When the AI is commanded to provide BattleGround Air Interdiction (through the event **Engage**), the AI will fly towards the Engage Zone.
-- Any target that is detected in the Engage Zone will be reported and will be destroyed by the AI.
-- 
-- ![Engage Event](..\Presentations\AI_BAI\Dia6.JPG)
-- 
-- The AI will detect the targets and will only destroy the targets within the Engage Zone.
-- 
-- ![Engage Event](..\Presentations\AI_BAI\Dia7.JPG)
-- 
-- Every target that is destroyed, is reported< by the AI.
-- 
-- ![Engage Event](..\Presentations\AI_BAI\Dia8.JPG)
-- 
-- Note that the AI does not know when the Engage Zone is cleared, and therefore will keep circling in the zone. 
--
-- ![Engage Event](..\Presentations\AI_BAI\Dia9.JPG)
-- 
-- Until it is notified through the event **Accomplish**, which is to be triggered by an observing party:
-- 
--   * a FAC
--   * a timed event
--   * a menu option selected by a human
--   * a condition
--   * others ...
-- 
-- ![Engage Event](..\Presentations\AI_BAI\Dia10.JPG)
-- 
-- When the AI has accomplished the Bombing, it will fly back to the Patrol Zone.
-- 
-- ![Engage Event](..\Presentations\AI_BAI\Dia11.JPG)
-- 
-- It will keep patrolling there, until it is notified to RTB or move to another BOMB Zone.
-- It can be notified to go RTB through the **RTB** event.
-- 
-- When the fuel threshold has been reached, the airplane will fly towards the nearest friendly airbase and will land.
-- 
-- ![Engage Event](..\Presentations\AI_BAI\Dia12.JPG)
--
-- # 1. AI_BAI_ZONE constructor
--
--   * @{#AI_BAI_ZONE.New}(): Creates a new AI_BAI_ZONE object.
--
-- ## 2. AI_BAI_ZONE is a FSM
-- 
-- ![Process](..\Presentations\AI_BAI\Dia2.JPG)
-- 
-- ### 2.1. AI_BAI_ZONE States
-- 
--   * **None** ( Group ): The process is not started yet.
--   * **Patrolling** ( Group ): The AI is patrolling the Patrol Zone.
--   * **Engaging** ( Group ): The AI is engaging the targets in the Engage Zone, executing BOMB.
--   * **Returning** ( Group ): The AI is returning to Base..
-- 
-- ### 2.2. AI_BAI_ZONE Events
-- 
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Start}**: Start the process.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Route}**: Route the AI to a new random 3D point within the Patrol Zone.
--   * **@{#AI_BAI_ZONE.Engage}**: Engage the AI to provide BOMB in the Engage Zone, destroying any target it finds.
--   * **@{#AI_BAI_ZONE.Abort}**: Aborts the engagement and return patrolling in the patrol zone.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.RTB}**: Route the AI to the home base.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detect}**: The AI is detecting targets.
--   * **@{AI.AI_Patrol#AI_PATROL_ZONE.Detected}**: The AI has detected new targets.
--   * **@{#AI_BAI_ZONE.Destroy}**: The AI has destroyed a target @{Wrapper.Unit}.
--   * **@{#AI_BAI_ZONE.Destroyed}**: The AI has destroyed all target @{Wrapper.Unit}s assigned in the BOMB task.
--   * **Status**: The AI is checking status (fuel and damage). When the thresholds have been reached, the AI will RTB.
-- 
-- ## 3. Modify the Engage Zone behaviour to pinpoint a **map object** or **scenery object**
-- 
-- Use the method @{#AI_BAI_ZONE.SearchOff}() to specify that the EngageZone is not to be searched for potential targets (UNITs), but that the center of the zone
-- is the point where a map object is to be destroyed (like a bridge).
-- 
-- Example:
-- 
--      -- Tell the BAI not to search for potential targets in the BAIEngagementZone, but rather use the center of the BAIEngagementZone as the bombing location.
--      AIBAIZone:SearchOff()
-- 
-- Searching can be switched back on with the method @{#AI_BAI_ZONE.SearchOn}(). Use the method @{#AI_BAI_ZONE.SearchOnOff}() to flexibily switch searching on or off.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_BAI_ZONE
AI_BAI_ZONE = {
  ClassName = "AI_BAI_ZONE",
}



--- Creates a new AI_BAI_ZONE object
-- @param #AI_BAI_ZONE self
-- @param Core.Zone#ZONE_BASE PatrolZone The @{Core.Zone} where the patrol needs to be executed.
-- @param DCS#Altitude PatrolFloorAltitude The lowest altitude in meters where to execute the patrol.
-- @param DCS#Altitude PatrolCeilingAltitude The highest altitude in meters where to execute the patrol.
-- @param DCS#Speed  PatrolMinSpeed The minimum speed of the @{Wrapper.Controllable} in km/h.
-- @param DCS#Speed  PatrolMaxSpeed The maximum speed of the @{Wrapper.Controllable} in km/h.
-- @param Core.Zone#ZONE_BASE EngageZone The zone where the engage will happen.
-- @param DCS#AltitudeType PatrolAltType The altitude type ("RADIO"=="AGL", "BARO"=="ASL"). Defaults to RADIO
-- @return #AI_BAI_ZONE self
function AI_BAI_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageZone, PatrolAltType )

  -- Inherits from BASE
  local self = BASE:Inherit( self, AI_PATROL_ZONE:New( PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_BAI_ZONE

  self.EngageZone = EngageZone
  self.Accomplished = false
  
  self:SetDetectionZone( self.EngageZone )
  self:SearchOn()

  self:AddTransition( { "Patrolling", "Engaging" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE.

  --- OnBefore Transition Handler for Event Engage.
  -- @function [parent=#AI_BAI_ZONE] OnBeforeEngage
  -- @param #AI_BAI_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string. 
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Engage.
  -- @function [parent=#AI_BAI_ZONE] OnAfterEngage
  -- @param #AI_BAI_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Engage.
  -- @function [parent=#AI_BAI_ZONE] Engage
  -- @param #AI_BAI_ZONE self
  -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone.
  -- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement.
  -- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. 
  -- If parameter is not defined the unit / controllable will choose expend on its own discretion.
  -- Use the structure @{DCS#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack.
  -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
  -- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.
  
  --- Asynchronous Event Trigger for Event Engage.
  -- @function [parent=#AI_BAI_ZONE] __Engage
  -- @param #AI_BAI_ZONE self
  -- @param #number Delay The delay in seconds.
  -- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone.
  -- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement.
  -- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. 
  -- If parameter is not defined the unit / controllable will choose expend on its own discretion.
  -- Use the structure @{DCS#AI.Task.WeaponExpend} to define the amount of weapons to be release at each attack.
  -- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
  -- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.

--- OnLeave Transition Handler for State Engaging.
-- @function [parent=#AI_BAI_ZONE] OnLeaveEngaging
-- @param #AI_BAI_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @return #boolean Return false to cancel Transition.

--- OnEnter Transition Handler for State Engaging.
-- @function [parent=#AI_BAI_ZONE] OnEnterEngaging
-- @param #AI_BAI_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.

  self:AddTransition( "Engaging", "Target", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE.

  self:AddTransition( "Engaging", "Fired", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE.
  
  --- OnBefore Transition Handler for Event Fired.
  -- @function [parent=#AI_BAI_ZONE] OnBeforeFired
  -- @param #AI_BAI_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Fired.
  -- @function [parent=#AI_BAI_ZONE] OnAfterFired
  -- @param #AI_BAI_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Fired.
  -- @function [parent=#AI_BAI_ZONE] Fired
  -- @param #AI_BAI_ZONE self
  
  --- Asynchronous Event Trigger for Event Fired.
  -- @function [parent=#AI_BAI_ZONE] __Fired
  -- @param #AI_BAI_ZONE self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE.

  --- OnBefore Transition Handler for Event Destroy.
  -- @function [parent=#AI_BAI_ZONE] OnBeforeDestroy
  -- @param #AI_BAI_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Destroy.
  -- @function [parent=#AI_BAI_ZONE] OnAfterDestroy
  -- @param #AI_BAI_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Destroy.
  -- @function [parent=#AI_BAI_ZONE] Destroy
  -- @param #AI_BAI_ZONE self
  
  --- Asynchronous Event Trigger for Event Destroy.
  -- @function [parent=#AI_BAI_ZONE] __Destroy
  -- @param #AI_BAI_ZONE self
  -- @param #number Delay The delay in seconds.


  self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE.

  --- OnBefore Transition Handler for Event Abort.
  -- @function [parent=#AI_BAI_ZONE] OnBeforeAbort
  -- @param #AI_BAI_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Abort.
  -- @function [parent=#AI_BAI_ZONE] OnAfterAbort
  -- @param #AI_BAI_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Abort.
  -- @function [parent=#AI_BAI_ZONE] Abort
  -- @param #AI_BAI_ZONE self
  
  --- Asynchronous Event Trigger for Event Abort.
  -- @function [parent=#AI_BAI_ZONE] __Abort
  -- @param #AI_BAI_ZONE self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_BAI_ZONE.

  --- OnBefore Transition Handler for Event Accomplish.
  -- @function [parent=#AI_BAI_ZONE] OnBeforeAccomplish
  -- @param #AI_BAI_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Accomplish.
  -- @function [parent=#AI_BAI_ZONE] OnAfterAccomplish
  -- @param #AI_BAI_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Accomplish.
  -- @function [parent=#AI_BAI_ZONE] Accomplish
  -- @param #AI_BAI_ZONE self
  
  --- Asynchronous Event Trigger for Event Accomplish.
  -- @function [parent=#AI_BAI_ZONE] __Accomplish
  -- @param #AI_BAI_ZONE self
  -- @param #number Delay The delay in seconds.  

  return self
end


--- Set the Engage Zone where the AI is performing BOMB. Note that if the EngageZone is changed, the AI needs to re-detect targets.
-- @param #AI_BAI_ZONE self
-- @param Core.Zone#ZONE EngageZone The zone where the AI is performing BOMB.
-- @return #AI_BAI_ZONE self
function AI_BAI_ZONE:SetEngageZone( EngageZone )
  self:F2()

  if EngageZone then  
    self.EngageZone = EngageZone
  else
    self.EngageZone = nil
  end
end


--- Specifies whether to search for potential targets in the zone, or let the center of the zone be the bombing coordinate.
-- AI_BAI_ZONE will search for potential targets by default.
-- @param #AI_BAI_ZONE self
-- @return #AI_BAI_ZONE
function AI_BAI_ZONE:SearchOnOff( Search )

  self.Search = Search
  
  return self
end

--- If Search is Off, the current zone coordinate will be the center of the bombing.
-- @param #AI_BAI_ZONE self
-- @return #AI_BAI_ZONE
function AI_BAI_ZONE:SearchOff()

  self:SearchOnOff( false )

  return self
end


--- If Search is On, BAI will search for potential targets in the zone.
-- @param #AI_BAI_ZONE self
-- @return #AI_BAI_ZONE
function AI_BAI_ZONE:SearchOn()

  self:SearchOnOff( true )

  return self
end


--- onafter State Transition for Event Start.
-- @param #AI_BAI_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_BAI_ZONE:onafterStart( Controllable, From, Event, To )

  -- Call the parent Start event handler
  self:GetParent(self).onafterStart( self, Controllable, From, Event, To )
  self:HandleEvent( EVENTS.Dead )
  
  self:SetDetectionDeactivated() -- When not engaging, set the detection off.
end

-- @param Wrapper.Controllable#CONTROLLABLE AIControllable
function _NewEngageRoute( AIControllable )

  AIControllable:T( "NewEngageRoute" )
  local EngageZone = AIControllable:GetState( AIControllable, "EngageZone" ) -- AI.AI_BAI#AI_BAI_ZONE
  EngageZone:__Engage( 1, EngageZone.EngageSpeed, EngageZone.EngageAltitude, EngageZone.EngageWeaponExpend, EngageZone.EngageAttackQty, EngageZone.EngageDirection )
end


-- @param #AI_BAI_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_BAI_ZONE:onbeforeEngage( Controllable, From, Event, To )
  
  if self.Accomplished == true then
    return false
  end
end

-- @param #AI_BAI_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_BAI_ZONE:onafterTarget( Controllable, From, Event, To )
  self:F({"onafterTarget",self.Search,Controllable:IsAlive()})
  
  

  if Controllable:IsAlive() then

    local AttackTasks = {}

    if self.Search == true then
      for DetectedUnit, Detected in pairs( self.DetectedUnits ) do
        local DetectedUnit = DetectedUnit -- Wrapper.Unit#UNIT
        if DetectedUnit:IsAlive() then
          if DetectedUnit:IsInZone( self.EngageZone ) then
            if Detected == true then
              self:F( {"Target: ", DetectedUnit } )
              self.DetectedUnits[DetectedUnit] = false
              local AttackTask = Controllable:TaskAttackUnit( DetectedUnit, false, self.EngageWeaponExpend, self.EngageAttackQty, self.EngageDirection, self.EngageAltitude, nil )
              self.Controllable:PushTask( AttackTask, 1 )
            end
          end
        else
          self.DetectedUnits[DetectedUnit] = nil
        end
      end
    else
      self:F("Attack zone")
      local AttackTask = Controllable:TaskAttackMapObject( 
        self.EngageZone:GetPointVec2():GetVec2(),
        true, 
        self.EngageWeaponExpend, 
        self.EngageAttackQty, 
        self.EngageDirection,
        self.EngageAltitude
      )
      self.Controllable:PushTask( AttackTask, 1 )
    end

    self:__Target( -10 )

  end
end


-- @param #AI_BAI_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_BAI_ZONE:onafterAbort( Controllable, From, Event, To )
  Controllable:ClearTasks()
  self:__Route( 1 )
end

-- @param #AI_BAI_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @param #number EngageSpeed (optional) The speed the Group will hold when engaging to the target zone.
-- @param DCS#Distance EngageAltitude (optional) Desired altitude to perform the unit engagement.
-- @param DCS#AI.Task.WeaponExpend EngageWeaponExpend (optional) Determines how much weapon will be released at each attack. If parameter is not defined the unit / controllable will choose expend on its own discretion.
-- @param #number EngageAttackQty (optional) This parameter limits maximal quantity of attack. The aicraft/controllable will not make more attack than allowed even if the target controllable not destroyed and the aicraft/controllable still have ammo. If not defined the aircraft/controllable will attack target until it will be destroyed or until the aircraft/controllable will run out of ammo.
-- @param DCS#Azimuth EngageDirection (optional) Desired ingress direction from the target to the attacking aircraft. Controllable/aircraft will make its attacks from the direction. Of course if there is no way to attack from the direction due the terrain controllable/aircraft will choose another direction.
function AI_BAI_ZONE:onafterEngage( Controllable, From, Event, To, 
                                    EngageSpeed, 
                                    EngageAltitude, 
                                    EngageWeaponExpend, 
                                    EngageAttackQty, 
                                    EngageDirection )

  self:F("onafterEngage")

  self.EngageSpeed = EngageSpeed or 400
  self.EngageAltitude = EngageAltitude or 2000
  self.EngageWeaponExpend = EngageWeaponExpend
  self.EngageAttackQty = EngageAttackQty
  self.EngageDirection = EngageDirection

  if Controllable:IsAlive() then

    local EngageRoute = {}

    --- Calculate the current route point.
    local CurrentVec2 = self.Controllable:GetVec2()
    
    --DONE: Create GetAltitude function for GROUP, and delete GetUnit(1).
    local CurrentAltitude = self.Controllable:GetAltitude()
    local CurrentPointVec3 = COORDINATE:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y )
    local ToEngageZoneSpeed = self.PatrolMaxSpeed
    local CurrentRoutePoint = CurrentPointVec3:WaypointAir( 
        self.PatrolAltType, 
        COORDINATE.WaypointType.TurningPoint, 
        COORDINATE.WaypointAction.TurningPoint, 
        self.EngageSpeed, 
        true 
      )
    
    EngageRoute[#EngageRoute+1] = CurrentRoutePoint

    local AttackTasks = {}
    
    if self.Search == true then
  
      for DetectedUnitID, DetectedUnitData in pairs( self.DetectedUnits ) do
        local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT
        self:T( DetectedUnit )
        if DetectedUnit:IsAlive() then
          if DetectedUnit:IsInZone( self.EngageZone ) then
            self:F( {"Engaging ", DetectedUnit } )
            AttackTasks[#AttackTasks+1] = Controllable:TaskBombing( 
              DetectedUnit:GetPointVec2():GetVec2(),
              true, 
              EngageWeaponExpend, 
              EngageAttackQty, 
              EngageDirection,
              EngageAltitude 
            )
          end
        else
          self.DetectedUnits[DetectedUnit] = nil
        end
      end
    else
      self:F("Attack zone")
      AttackTasks[#AttackTasks+1] = Controllable:TaskAttackMapObject( 
        self.EngageZone:GetPointVec2():GetVec2(),
        true, 
        EngageWeaponExpend, 
        EngageAttackQty, 
        EngageDirection,
        EngageAltitude 
      )
    end

    EngageRoute[#EngageRoute].task = Controllable:TaskCombo( AttackTasks )

    --- Define a random point in the @{Core.Zone}. The AI will fly to that point within the zone.
    
      --- Find a random 2D point in EngageZone.
    local ToTargetVec2 = self.EngageZone:GetRandomVec2()
    self:T2( ToTargetVec2 )

    --- Obtain a 3D @{Point} from the 2D point + altitude.
    local ToTargetPointVec3 = COORDINATE:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y )
    
    --- Create a route point of type air.
    local ToTargetRoutePoint = ToTargetPointVec3:WaypointAir( 
      self.PatrolAltType, 
      COORDINATE.WaypointType.TurningPoint, 
      COORDINATE.WaypointAction.TurningPoint, 
      self.EngageSpeed, 
      true 
    )
    
    EngageRoute[#EngageRoute+1] = ToTargetRoutePoint

    Controllable:OptionROEOpenFire()
    Controllable:OptionROTVertical()

    --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable...
    Controllable:WayPointInitialize( EngageRoute )
    
    --- Do a trick, link the NewEngageRoute function of the object to the AIControllable in a temporary variable ...
    Controllable:SetState( Controllable, "EngageZone", self )

    Controllable:WayPointFunction( #EngageRoute, 1, "_NewEngageRoute" )

    --- NOW ROUTE THE GROUP!
    Controllable:WayPointExecute( 1 )

    self:SetRefreshTimeInterval( 2 )
    self:SetDetectionActivated()
    self:__Target( -2 ) -- Start targeting
  end
end


-- @param #AI_BAI_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
function AI_BAI_ZONE:onafterAccomplish( Controllable, From, Event, To )
  self.Accomplished = true
  self:SetDetectionDeactivated()
end


-- @param #AI_BAI_ZONE self
-- @param Wrapper.Controllable#CONTROLLABLE Controllable The Controllable Object managed by the FSM.
-- @param #string From The From State string.
-- @param #string Event The Event string.
-- @param #string To The To State string.
-- @param Core.Event#EVENTDATA EventData
function AI_BAI_ZONE:onafterDestroy( Controllable, From, Event, To, EventData )

  if EventData.IniUnit then
    self.DetectedUnits[EventData.IniUnit] = nil
  end
end


-- @param #AI_BAI_ZONE self
-- @param Core.Event#EVENTDATA EventData
function AI_BAI_ZONE:OnEventDead( EventData )
  self:F( { "EventDead", EventData } )

  if EventData.IniDCSUnit then
    if self.DetectedUnits and self.DetectedUnits[EventData.IniUnit] then
      self:__Destroy( 1, EventData )
    end
  end
end


--- **AI** - Build large airborne formations of aircraft.
-- 
-- **Features:**
--
--   * Build in-air formations consisting of more than 40 aircraft as one group.
--   * Build different formation types.
--   * Assign a group leader that will guide the large formation path.
-- 
-- ===
--
-- ## Additional Material:
--
-- * **Demo Missions:** [GitHub](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_Formation)
-- * **YouTube videos:** [Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0bFIJ9jIdYM22uaWmIN4oz)
-- * **Guides:** None
--
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
-- ===
--   
-- @module AI.AI_Formation
-- @image AI_Large_Formations.JPG

--- AI_FORMATION class
-- @type AI_FORMATION
-- @extends Core.Fsm#FSM_SET
-- @field Wrapper.Unit#UNIT FollowUnit
-- @field Core.Set#SET_GROUP FollowGroupSet
-- @field #string FollowName
-- @field #AI_FORMATION.MODE FollowMode The mode the escort is in.
-- @field Core.Scheduler#SCHEDULER FollowScheduler The instance of the SCHEDULER class.
-- @field #number FollowDistance The current follow distance.
-- @field #boolean ReportTargets If true, nearby targets are reported.
-- @Field DCSTypes#AI.Option.Air.val.ROE OptionROE Which ROE is set to the FollowGroup.
-- @field DCSTypes#AI.Option.Air.val.REACTION_ON_THREAT OptionReactionOnThreat Which REACTION_ON_THREAT is set to the FollowGroup.
-- @field #number dtFollow Time step between position updates.


--- Build large formations, make AI follow a @{Wrapper.Client#CLIENT} (player) leader or a @{Wrapper.Unit#UNIT} (AI) leader.
-- 
-- ![Banner Image](..\Images\deprecated.png)
--
-- AI_FORMATION makes AI @{Wrapper.Group#GROUP}s fly in formation of various compositions.
-- The AI_FORMATION class models formations in a different manner than the internal DCS formation logic!!!
-- The purpose of the class is to:
-- 
--   * Make formation building a process that can be managed while in flight, rather than a task.
--   * Human players can guide formations, consisting of larget planes.
--   * Build large formations (like a large bomber field).
--   * Form formations that DCS does not support off the shelve.
-- 
-- A few remarks:
-- 
--   * Depending on the type of plane, the change in direction by the leader may result in the formation getting disentangled while in flight and needs to be rebuild.
--   * Formations are vulnerable to collissions, but is depending on the type of plane, the distance between the planes and the speed and angle executed by the leader.
--   * Formations may take a while to build up.
-- 
-- As a result, the AI_FORMATION is not perfect, but is very useful to:
-- 
--   * Model large formations when flying straight line. You can build close formations when doing this.
--   * Make humans guide a large formation, when the planes are wide from each other.
--   
-- ## AI_FORMATION construction
-- 
-- Create a new SPAWN object with the @{#AI_FORMATION.New} method:
--
--   * @{#AI_FORMATION.New}(): Creates a new AI_FORMATION object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT} or a @{Wrapper.Unit#UNIT}, with an optional briefing text.
--
-- ## Formation methods
-- 
-- The following methods can be used to set or change the formation:
-- 
--  * @{#AI_FORMATION.FormationLine}(): Form a line formation (core formation function).
--  * @{#AI_FORMATION.FormationTrail}(): Form a trail formation.
--  * @{#AI_FORMATION.FormationLeftLine}(): Form a left line formation.
--  * @{#AI_FORMATION.FormationRightLine}(): Form a right line formation.
--  * @{#AI_FORMATION.FormationRightWing}(): Form a right wing formation.
--  * @{#AI_FORMATION.FormationLeftWing}(): Form a left wing formation.
--  * @{#AI_FORMATION.FormationCenterWing}(): Form a center wing formation.
--  * @{#AI_FORMATION.FormationCenterVic}(): Form a Vic formation (same as CenterWing.
--  * @{#AI_FORMATION.FormationCenterBoxed}(): Form a center boxed formation.
--  
-- ## Randomization
-- 
-- Use the method @{AI.AI_Formation#AI_FORMATION.SetFlightRandomization}() to simulate the formation flying errors that pilots make while in formation. Is a range set in meters.
--
-- @usage
-- local FollowGroupSet = SET_GROUP:New():FilterCategories("plane"):FilterCoalitions("blue"):FilterPrefixes("Follow"):FilterStart()
-- FollowGroupSet:Flush()
-- local LeaderUnit = UNIT:FindByName( "Leader" )
-- local LargeFormation = AI_FORMATION:New( LeaderUnit, FollowGroupSet, "Center Wing Formation", "Briefing" )
-- LargeFormation:FormationCenterWing( 500, 50, 0, 250, 250 )
-- LargeFormation:__Start( 1 )
  --
  -- # Developer Note
  -- 
  -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
  -- Therefore, this class is considered to be deprecated
  --
-- @field #AI_FORMATION 
AI_FORMATION = {
  ClassName = "AI_FORMATION",
  FollowName = nil, -- The Follow Name
  FollowUnit = nil,
  FollowGroupSet = nil,
  FollowMode = 1,
  MODE = {
    FOLLOW = 1,
    MISSION = 2,
  },
  FollowScheduler = nil,
  OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE,
  OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION,
  dtFollow = 0.5,
}

AI_FORMATION.__Enum = {}

-- @type AI_FORMATION.__Enum.Formation
-- @field #number None
-- @field #number Line
-- @field #number Trail
-- @field #number Stack
-- @field #number LeftLine
-- @field #number RightLine
-- @field #number LeftWing
-- @field #number RightWing
-- @field #number Vic
-- @field #number Box
AI_FORMATION.__Enum.Formation = {
  None = 0,
  Mission = 1,
  Line = 2,
  Trail = 3,
  Stack = 4,
  LeftLine = 5,
  RightLine = 6,
  LeftWing = 7,
  RightWing = 8,
  Vic = 9,
  Box = 10,
}

-- @type AI_FORMATION.__Enum.Mode
-- @field #number Mission
-- @field #number Formation
AI_FORMATION.__Enum.Mode = {
  Mission = "M",
  Formation = "F",
  Attack = "A",
  Reconnaissance = "R",
}

-- @type AI_FORMATION.__Enum.ReportType
-- @field #number All
-- @field #number Airborne
-- @field #number GroundRadar
-- @field #number Ground
AI_FORMATION.__Enum.ReportType = {
  All = "*",
  Airborne = "A",
  GroundRadar = "R",
  Ground = "G",
}

--- AI_FORMATION class constructor for an AI group
-- @param #AI_FORMATION self
-- @param Wrapper.Unit#UNIT FollowUnit The UNIT leading the FolllowGroupSet.
-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
-- @param #string FollowName Name of the escort.
-- @param #string FollowBriefing Briefing.
-- @return #AI_FORMATION self
function AI_FORMATION:New( FollowUnit, FollowGroupSet, FollowName, FollowBriefing ) --R2.1
  local self = BASE:Inherit( self, FSM_SET:New( FollowGroupSet ) )
  self:F( { FollowUnit, FollowGroupSet, FollowName } )

  self.FollowUnit = FollowUnit -- Wrapper.Unit#UNIT
  self.FollowGroupSet = FollowGroupSet -- Core.Set#SET_GROUP
  
  self.FollowGroupSet:ForEachGroup(
    function( FollowGroup )
      --self:E("Following")
      FollowGroup:SetState( self, "Mode", self.__Enum.Mode.Formation )
    end
  )
  
  self:SetFlightModeFormation()
  
  self:SetFlightRandomization( 2 )
  
  self:SetStartState( "None" ) 

  self:AddTransition( "*", "Stop", "Stopped" )

  self:AddTransition( {"None", "Stopped"}, "Start", "Following" )

  self:AddTransition( "*", "FormationLine", "*" )
  --- FormationLine Handler OnBefore for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnBeforeFormationLine
  -- @param #AI_FORMATION self
  -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  -- @return #boolean
  
  --- FormationLine Handler OnAfter for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnAfterFormationLine
  -- @param #AI_FORMATION self
  -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationLine Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] FormationLine
  -- @param #AI_FORMATION self
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationLine Asynchronous Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] __FormationLine
  -- @param #AI_FORMATION self
  -- @param #number Delay
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  self:AddTransition( "*", "FormationTrail", "*" )
  --- FormationTrail Handler OnBefore for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnBeforeFormationTrail
  -- @param #AI_FORMATION self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @return #boolean
  
  --- FormationTrail Handler OnAfter for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnAfterFormationTrail
  -- @param #AI_FORMATION self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  
  --- FormationTrail Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] FormationTrail
  -- @param #AI_FORMATION self
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  
  --- FormationTrail Asynchronous Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] __FormationTrail
  -- @param #AI_FORMATION self
  -- @param #number Delay
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.

  self:AddTransition( "*", "FormationStack", "*" )
  --- FormationStack Handler OnBefore for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnBeforeFormationStack
  -- @param #AI_FORMATION self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @return #boolean
  
  --- FormationStack Handler OnAfter for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnAfterFormationStack
  -- @param #AI_FORMATION self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  
  --- FormationStack Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] FormationStack
  -- @param #AI_FORMATION self
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  
  --- FormationStack Asynchronous Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] __FormationStack
  -- @param #AI_FORMATION self
  -- @param #number Delay
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.

  self:AddTransition( "*", "FormationLeftLine", "*" )  
  --- FormationLeftLine Handler OnBefore for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnBeforeFormationLeftLine
  -- @param #AI_FORMATION self
  -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  -- @return #boolean
  
  --- FormationLeftLine Handler OnAfter for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnAfterFormationLeftLine
  -- @param #AI_FORMATION self
  -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationLeftLine Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] FormationLeftLine
  -- @param #AI_FORMATION self
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationLeftLine Asynchronous Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] __FormationLeftLine
  -- @param #AI_FORMATION self
  -- @param #number Delay
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.

  self:AddTransition( "*", "FormationRightLine", "*" )  
  --- FormationRightLine Handler OnBefore for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnBeforeFormationRightLine
  -- @param #AI_FORMATION self
  -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  -- @return #boolean
  
  --- FormationRightLine Handler OnAfter for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnAfterFormationRightLine
  -- @param #AI_FORMATION self
  -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationRightLine Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] FormationRightLine
  -- @param #AI_FORMATION self
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationRightLine Asynchronous Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] __FormationRightLine
  -- @param #AI_FORMATION self
  -- @param #number Delay
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.

  self:AddTransition( "*", "FormationLeftWing", "*" )
  --- FormationLeftWing Handler OnBefore for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnBeforeFormationLeftWing
  -- @param #AI_FORMATION self
  -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  -- @return #boolean
  
  --- FormationLeftWing Handler OnAfter for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnAfterFormationLeftWing
  -- @param #AI_FORMATION self
  -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationLeftWing Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] FormationLeftWing
  -- @param #AI_FORMATION self
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationLeftWing Asynchronous Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] __FormationLeftWing
  -- @param #AI_FORMATION self
  -- @param #number Delay
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  self:AddTransition( "*", "FormationRightWing", "*" )
  --- FormationRightWing Handler OnBefore for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnBeforeFormationRightWing
  -- @param #AI_FORMATION self
  -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  -- @return #boolean
  
  --- FormationRightWing Handler OnAfter for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnAfterFormationRightWing
  -- @param #AI_FORMATION self
  -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationRightWing Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] FormationRightWing
  -- @param #AI_FORMATION self
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationRightWing Asynchronous Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] __FormationRightWing
  -- @param #AI_FORMATION self
  -- @param #number Delay
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  self:AddTransition( "*", "FormationCenterWing", "*" )
  --- FormationCenterWing Handler OnBefore for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnBeforeFormationCenterWing
  -- @param #AI_FORMATION self
  -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  -- @return #boolean
  
  --- FormationCenterWing Handler OnAfter for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnAfterFormationCenterWing
  -- @param #AI_FORMATION self
  -- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationCenterWing Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] FormationCenterWing
  -- @param #AI_FORMATION self
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationCenterWing Asynchronous Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] __FormationCenterWing
  -- @param #AI_FORMATION self
  -- @param #number Delay
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.

  self:AddTransition( "*", "FormationVic", "*" )
  --- FormationVic Handler OnBefore for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnBeforeFormationVic
  -- @param #AI_FORMATION self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  -- @return #boolean
  
  --- FormationVic Handler OnAfter for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnAfterFormationVic
  -- @param #AI_FORMATION self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationVic Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] FormationVic
  -- @param #AI_FORMATION self
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  
  --- FormationVic Asynchronous Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] __FormationVic
  -- @param #AI_FORMATION self
  -- @param #number Delay
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.

  self:AddTransition( "*", "FormationBox", "*" )
  --- FormationBox Handler OnBefore for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnBeforeFormationBox
  -- @param #AI_FORMATION self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  -- @param #number ZLevels The amount of levels on the Z-axis.
  -- @return #boolean
  
  --- FormationBox Handler OnAfter for AI_FORMATION
  -- @function [parent=#AI_FORMATION] OnAfterFormationBox
  -- @param #AI_FORMATION self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  -- @param #number ZLevels The amount of levels on the Z-axis.
  
  --- FormationBox Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] FormationBox
  -- @param #AI_FORMATION self
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  -- @param #number ZLevels The amount of levels on the Z-axis.
  
  --- FormationBox Asynchronous Trigger for AI_FORMATION
  -- @function [parent=#AI_FORMATION] __FormationBox
  -- @param #AI_FORMATION self
  -- @param #number Delay
  -- @param #number XStart The start position on the X-axis in meters for the first group.
  -- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
  -- @param #number YStart The start position on the Y-axis in meters for the first group.
  -- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #number ZStart The start position on the Z-axis in meters for the first group.
  -- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
  -- @param #number ZLevels The amount of levels on the Z-axis.
  
 
  self:AddTransition( "*", "Follow", "Following" )

  self:FormationLeftLine( 500, 0, 250, 250 )
  
  self.FollowName = FollowName
  self.FollowBriefing = FollowBriefing


  self.CT1 = 0
  self.GT1 = 0

  self.FollowMode = AI_FORMATION.MODE.MISSION

  return self
end


--- Set time interval between updates of the formation.
-- @param #AI_FORMATION self
-- @param #number dt Time step in seconds between formation updates. Default is every 0.5 seconds.
-- @return #AI_FORMATION
function AI_FORMATION:SetFollowTimeInterval(dt) --R2.1
  self.dtFollow=dt or 0.5
  return self
end

--- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to.
-- This allows to visualize where the escort is flying to.
-- @param #AI_FORMATION self
-- @param #boolean SmokeDirection If true, then the direction vector will be smoked.
-- @return #AI_FORMATION
function AI_FORMATION:TestSmokeDirectionVector( SmokeDirection ) --R2.1
  self.SmokeDirectionVector = ( SmokeDirection == true ) and true or false
  return self
end

--- FormationLine Handler OnAfter for AI_FORMATION
-- @param #AI_FORMATION self
-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @return #AI_FORMATION
function AI_FORMATION:onafterFormationLine( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, Formation ) --R2.1
  self:F( { FollowGroupSet, From , Event ,To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, Formation } )

  XStart = XStart or self.XStart
  XSpace = XSpace or self.XSpace
  YStart = YStart or self.YStart
  YSpace = YSpace or self.YSpace
  ZStart = ZStart or self.ZStart
  ZSpace = ZSpace or self.ZSpace

  FollowGroupSet:Flush( self )
  
  local FollowSet = FollowGroupSet:GetSet()
  
  local i = 1  --FF i=0 caused first unit to have no XSpace! Probably needs further adjustments. This is just a quick work around.
  
  for FollowID, FollowGroup in pairs( FollowSet ) do
  
    local PointVec3 = COORDINATE:New()
    PointVec3:SetX( XStart + i * XSpace )
    PointVec3:SetY( YStart + i * YSpace )
    PointVec3:SetZ( ZStart + i * ZSpace )
  
    local Vec3 = PointVec3:GetVec3()
    FollowGroup:SetState( self, "FormationVec3", Vec3 )
    i = i + 1

    FollowGroup:SetState( FollowGroup, "Formation", Formation )
  end
  
  return self

end

--- FormationTrail Handler OnAfter for AI_FORMATION
-- @param #AI_FORMATION self
-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @return #AI_FORMATION
function AI_FORMATION:onafterFormationTrail( FollowGroupSet, From , Event , To, XStart, XSpace, YStart ) --R2.1

  self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,0,0, self.__Enum.Formation.Trail )

  return self
end


--- FormationStack Handler OnAfter for AI_FORMATION
-- @param #AI_FORMATION self
-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
-- @return #AI_FORMATION
function AI_FORMATION:onafterFormationStack( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace ) --R2.1

  self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,0,0, self.__Enum.Formation.Stack )

  return self
end




--- FormationLeftLine Handler OnAfter for AI_FORMATION
-- @param #AI_FORMATION self
-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @return #AI_FORMATION
function AI_FORMATION:onafterFormationLeftLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) --R2.1

  self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,-ZStart,-ZSpace, self.__Enum.Formation.LeftLine )

  return self
end


--- FormationRightLine Handler OnAfter for AI_FORMATION
-- @param #AI_FORMATION self
-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @return #AI_FORMATION
function AI_FORMATION:onafterFormationRightLine( FollowGroupSet, From , Event , To, XStart, YStart, ZStart, ZSpace ) --R2.1

  self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,0,YStart,0,ZStart,ZSpace,self.__Enum.Formation.RightLine)

  return self
end


--- FormationLeftWing Handler OnAfter for AI_FORMATION
-- @param #AI_FORMATION self
-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
function AI_FORMATION:onafterFormationLeftWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) --R2.1

  self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,-ZStart,-ZSpace,self.__Enum.Formation.LeftWing)

  return self
end


--- FormationRightWing Handler OnAfter for AI_FORMATION
-- @function [parent=#AI_FORMATION] OnAfterFormationRightWing
-- @param #AI_FORMATION self
-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
function AI_FORMATION:onafterFormationRightWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, ZStart, ZSpace ) --R2.1

  self:onafterFormationLine(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,0,ZStart,ZSpace,self.__Enum.Formation.RightWing)

  return self
end


--- FormationCenterWing Handler OnAfter for AI_FORMATION
-- @param #AI_FORMATION self
-- @param Core.Set#SET_GROUP FollowGroupSet The group AI escorting the FollowUnit.
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
function AI_FORMATION:onafterFormationCenterWing( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) --R2.1

  local FollowSet = FollowGroupSet:GetSet()
  
  local i = 0
  
  for FollowID, FollowGroup in pairs( FollowSet ) do
  
    local PointVec3 = COORDINATE:New()
    
    local Side = ( i % 2 == 0 ) and 1 or -1
    local Row = i / 2 + 1
    
    PointVec3:SetX( XStart + Row * XSpace )
    PointVec3:SetY( YStart )
    PointVec3:SetZ( Side * ( ZStart + i * ZSpace ) )
  
    local Vec3 = PointVec3:GetVec3()
    FollowGroup:SetState( self, "FormationVec3", Vec3 )
    i = i + 1
    FollowGroup:SetState( FollowGroup, "Formation", self.__Enum.Formation.Vic )
  end
  
  return self
end


--- FormationVic Handle for AI_FORMATION
-- @param #AI_FORMATION self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @return #AI_FORMATION
function AI_FORMATION:onafterFormationVic( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace ) --R2.1

  self:onafterFormationCenterWing(FollowGroupSet,From,Event,To,XStart,XSpace,YStart,YSpace,ZStart,ZSpace)
  
  return self
end

--- FormationBox Handler OnAfter for AI_FORMATION
-- @param #AI_FORMATION self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @param #number ZLevels The amount of levels on the Z-axis.
-- @return #AI_FORMATION
function AI_FORMATION:onafterFormationBox( FollowGroupSet, From , Event , To, XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels ) --R2.1

  local FollowSet = FollowGroupSet:GetSet()
  
  local i = 0
  
  for FollowID, FollowGroup in pairs( FollowSet ) do
  
    local PointVec3 = COORDINATE:New()
    
    local ZIndex = i % ZLevels
    local XIndex = math.floor( i / ZLevels )
    local YIndex = math.floor( i / ZLevels )
    
    PointVec3:SetX( XStart + XIndex * XSpace )
    PointVec3:SetY( YStart + YIndex * YSpace )
    PointVec3:SetZ( -ZStart - (ZSpace * ZLevels / 2 ) + ZSpace * ZIndex )
  
    local Vec3 = PointVec3:GetVec3()
    FollowGroup:SetState( self, "FormationVec3", Vec3 )
    i = i + 1
    FollowGroup:SetState( FollowGroup, "Formation", self.__Enum.Formation.Box )
  end

  return self
end


--- Use the method @{AI.AI_Formation#AI_FORMATION.SetFlightRandomization}() to make the air units in your formation randomize their flight a bit while in formation.
-- @param #AI_FORMATION self
-- @param #number FlightRandomization The formation flying errors that pilots can make while in formation. Is a range set in meters.
-- @return #AI_FORMATION
function AI_FORMATION:SetFlightRandomization( FlightRandomization ) --R2.1

  self.FlightRandomization = FlightRandomization
  
  return self
end


--- Gets your escorts to flight mode.
-- @param #AI_FORMATION self
-- @param Wrapper.Group#GROUP FollowGroup FollowGroup.
-- @return #AI_FORMATION
function AI_FORMATION:GetFlightMode( FollowGroup )

  if FollowGroup then
    FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) )
    FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission )
  end
  
  
  return FollowGroup:GetState( FollowGroup, "Mode" )
end



--- This sets your escorts to fly a mission.
-- @param #AI_FORMATION self
-- @param Wrapper.Group#GROUP FollowGroup FollowGroup.
-- @return #AI_FORMATION
function AI_FORMATION:SetFlightModeMission( FollowGroup )

  if FollowGroup then
    FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) )
    FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission )
  else
    self.FollowGroupSet:ForSomeGroupAlive(
      -- @param Core.Group#GROUP EscortGroup
      function( FollowGroup )
        FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) )
        FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Mission )
      end
    )
  end
  
  
  return self
end


--- This sets your escorts to execute an attack.
-- @param #AI_FORMATION self
-- @param Wrapper.Group#GROUP FollowGroup FollowGroup.
-- @return #AI_FORMATION
function AI_FORMATION:SetFlightModeAttack( FollowGroup )

  if FollowGroup then
    FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) )
    FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Attack )
  else
    self.FollowGroupSet:ForSomeGroupAlive(
      -- @param Core.Group#GROUP EscortGroup
      function( FollowGroup )
        FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) )
        FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Attack )
      end
    )
  end
  
  
  return self
end


--- This sets your escorts to fly in a formation.
-- @param #AI_FORMATION self
-- @param Wrapper.Group#GROUP FollowGroup FollowGroup.
-- @return #AI_FORMATION
function AI_FORMATION:SetFlightModeFormation( FollowGroup )

  if FollowGroup then
    FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) )
    FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation )
  else
    self.FollowGroupSet:ForSomeGroupAlive(
      -- @param Core.Group#GROUP EscortGroup
      function( FollowGroup )
        FollowGroup:SetState( FollowGroup, "PreviousMode", FollowGroup:GetState( FollowGroup, "Mode" ) )
        FollowGroup:SetState( FollowGroup, "Mode", self.__Enum.Mode.Formation )
      end
    )
  end
  
  return self
end




--- Stop function. Formation will not be updated any more.
-- @param #AI_FORMATION self
-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To The to state.
function AI_FORMATION:onafterStop(FollowGroupSet, From, Event, To) --R2.1
  self:E("Stopping formation.")
end

--- Follow event fuction. Check if coming from state "stopped". If so the transition is rejected.
-- @param #AI_FORMATION self
-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To The to state.
function AI_FORMATION:onbeforeFollow( FollowGroupSet, From, Event, To ) --R2.1
  if From=="Stopped" then
    return false  -- Deny transition.
  end
  return true
end

--- Enter following state.
-- @param #AI_FORMATION self
-- @param Core.Set#SET_GROUP FollowGroupSet The following set of groups.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To The to state.
function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1

  if self.FollowUnit:IsAlive() then

    local ClientUnit = self.FollowUnit

    local CT1, CT2, CV1, CV2
    CT1 = ClientUnit:GetState( self, "CT1" )

    local CuVec3=ClientUnit:GetVec3()

    if CT1 == nil or CT1 == 0 then
      ClientUnit:SetState( self, "CV1",  CuVec3)
      ClientUnit:SetState( self, "CT1", timer.getTime() )
    else
      CT1 = ClientUnit:GetState( self, "CT1" )
      CT2 = timer.getTime()
      CV1 = ClientUnit:GetState( self, "CV1" )
      CV2 = CuVec3
      
      ClientUnit:SetState( self, "CT1", CT2 )
      ClientUnit:SetState( self, "CV1", CV2 )
    end
        
    --FollowGroupSet:ForEachGroupAlive( bla, self, ClientUnit, CT1, CV1, CT2, CV2)
    
    for _,_group in pairs(FollowGroupSet:GetSet()) do
      local group=_group --Wrapper.Group#GROUP
      if group and group:IsAlive() then
        self:FollowMe(group, ClientUnit, CT1, CV1, CT2, CV2)
      end
    end
    
    self:__Follow( -self.dtFollow )
  end
  
end


--- Follow me.
-- @param #AI_FORMATION self
-- @param Wrapper.Group#GROUP FollowGroup Follow group.
-- @param Wrapper.Unit#UNIT ClientUnit Client Unit.
-- @param DCS#Time CT1 Time
-- @param DCS#Vec3 CV1 Vec3
-- @param DCS#Time CT2 Time
-- @param DCS#Vec3 CV2 Vec3
function AI_FORMATION:FollowMe(FollowGroup, ClientUnit, CT1, CV1, CT2, CV2)

  if FollowGroup:GetState( FollowGroup, "Mode" ) == self.__Enum.Mode.Formation and not self:Is("Stopped") then
 
    self:T({Mode=FollowGroup:GetState( FollowGroup, "Mode" )})

    FollowGroup:OptionROTEvadeFire()
    FollowGroup:OptionROEReturnFire()

    local GroupUnit = FollowGroup:GetUnit( 1 )
    
    local GuVec3=GroupUnit:GetVec3()
    
    local FollowFormation = FollowGroup:GetState( self, "FormationVec3" )
    
    if FollowFormation then
      local FollowDistance = FollowFormation.x
      
      local GT1 = GroupUnit:GetState( self, "GT1" )
  
      if CT1 == nil or CT1 == 0 or GT1 == nil or GT1 == 0 then
        GroupUnit:SetState( self, "GV1", GuVec3)
        GroupUnit:SetState( self, "GT1", timer.getTime() ) 
      else
        local CD = ( ( CV2.x - CV1.x )^2 + ( CV2.y - CV1.y )^2 + ( CV2.z - CV1.z )^2 ) ^ 0.5
        local CT = CT2 - CT1
  
        local CS = ( 3600 / CT ) * ( CD / 1000 ) / 3.6

        local CDv = { x = CV2.x - CV1.x, y = CV2.y - CV1.y, z = CV2.z - CV1.z }
        local Ca = math.atan2( CDv.x, CDv.z )
  
        local GT1 = GroupUnit:GetState( self, "GT1" )
        local GT2 = timer.getTime()
        
        local GV1 = GroupUnit:GetState( self, "GV1" )        
        local GV2 = GuVec3
        
        --[[
        GV2:AddX( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) )
        GV2:AddY( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) )
        GV2:AddZ( math.random( -Formation.FlightRandomization / 2, Formation.FlightRandomization / 2 ) )
        ]]
        
        GV2.x=GV2.x+math.random( -self.FlightRandomization / 2, self.FlightRandomization / 2 )
        GV2.y=GV2.y+math.random( -self.FlightRandomization / 2, self.FlightRandomization / 2 )
        GV2.z=GV2.z+math.random( -self.FlightRandomization / 2, self.FlightRandomization / 2 )

        
        GroupUnit:SetState( self, "GT1", GT2 )
        GroupUnit:SetState( self, "GV1", GV2 )
        
  
        local GD = ( ( GV2.x - GV1.x )^2 + ( GV2.y - GV1.y )^2 + ( GV2.z - GV1.z )^2 ) ^ 0.5
        local GT = GT2 - GT1
  

        -- Calculate the distance
        local GDv =  { x = GV2.x - CV1.x, y =  GV2.y - CV1.y, z = GV2.z - CV1.z }
        local Alpha_T = math.atan2( GDv.x, GDv.z ) - math.atan2( CDv.x, CDv.z ) 
        local Alpha_R = ( Alpha_T < 0 ) and Alpha_T + 2 * math.pi or Alpha_T
        local Position = math.cos( Alpha_R )
        local GD = ( ( GDv.x )^2 + ( GDv.z )^2 ) ^ 0.5
        local Distance = GD * Position + - CS * 0.5
  
        -- Calculate the group direction vector
        local GV = { x = GV2.x - CV2.x, y = GV2.y - CV2.y, z = GV2.z - CV2.z  }
  
        -- Calculate GH2, GH2 with the same height as CV2.
        local GH2 = { x = GV2.x, y = CV2.y + FollowFormation.y, z = GV2.z }
  
        -- Calculate the angle of GV to the orthonormal plane
        local alpha = math.atan2( GV.x, GV.z )
  
        local GVx = FollowFormation.z * math.cos( Ca ) + FollowFormation.x * math.sin( Ca )
        local GVz = FollowFormation.x * math.cos( Ca ) - FollowFormation.z * math.sin( Ca )


        -- Now we calculate the intersecting vector between the circle around CV2 with radius FollowDistance and GH2.
        -- From the GeoGebra model: CVI = (x(CV2) + FollowDistance cos(alpha), y(GH2) + FollowDistance sin(alpha), z(CV2))
        local Inclination = ( Distance + FollowFormation.x ) / 10
        if Inclination < -30 then
          Inclination = - 30
        end
        
        local CVI = { 
          x = CV2.x + CS * 10 * math.sin(Ca),
          y = GH2.y + Inclination, -- + FollowFormation.y,
          --y = GH2.y,
          z = CV2.z + CS * 10 * math.cos(Ca),
        }
  
        -- Calculate the direction vector DV of the escort group. We use CVI as the base and CV2 as the direction.
        local DV = { x = CV2.x - CVI.x, y = CV2.y - CVI.y, z = CV2.z - CVI.z }
  
        -- We now calculate the unary direction vector DVu, so that we can multiply DVu with the speed, which is expressed in meters / s.
        -- We need to calculate this vector to predict the point the escort group needs to fly to according its speed.
        -- The distance of the destination point should be far enough not to have the aircraft starting to swipe left to right...
        local DVu = { x = DV.x / FollowDistance, y = DV.y, z = DV.z / FollowDistance }
  
        -- Now we can calculate the group destination vector GDV.
        local GDV = { x = CVI.x, y = CVI.y, z = CVI.z }
              
        local ADDx = FollowFormation.x * math.cos(alpha) - FollowFormation.z * math.sin(alpha)
        local ADDz = FollowFormation.z * math.cos(alpha) + FollowFormation.x * math.sin(alpha)
              
        local GDV_Formation = { 
          x = GDV.x - GVx, 
          y = GDV.y, 
          z = GDV.z - GVz
        }
             
        -- Debug smoke. 
        if self.SmokeDirectionVector == true then
          trigger.action.smoke( GDV, trigger.smokeColor.Green )
          trigger.action.smoke( GDV_Formation, trigger.smokeColor.White )
        end
              
              
              
        local Time = 120
              
        local Speed = - ( Distance + FollowFormation.x ) / Time

        if Distance > -10000 then
          Speed = - ( Distance + FollowFormation.x ) / 60
        end
 
        if Distance > -2500 then
          Speed = - ( Distance + FollowFormation.x ) / 20
        end
              
        local GS = Speed + CS

        --self:F( { Distance = Distance, Speed = Speed, CS = CS, GS = GS } )              
  
        -- Now route the escort to the desired point with the desired speed.
        FollowGroup:RouteToVec3( GDV_Formation, GS ) -- DCS models speed in Mps (Miles per second)
        
      end
    end
  end
end
--- **AI** - Taking the lead of AI escorting your flight or of other AI.
-- 
-- ===
-- 
-- ## Features:
-- 
--   * Escort navigation commands.
--   * Escort hold at position commands.
--   * Escorts reporting detected targets.
--   * Escorts scanning targets in advance.
--   * Escorts attacking specific targets.
--   * Request assistance from other groups for attack.
--   * Manage rule of engagement of escorts.
--   * Manage the allowed evasion techniques of escorts.
--   * Make escort to execute a defined mission or path.
--   * Escort tactical situation reporting.
-- 
-- ===
-- 
-- ## Missions:
-- 
-- [ESC - Escorting](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_Escort)
-- 
-- ===
-- 
-- Allows you to interact with escorting AI on your flight and take the lead.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- Each escorting group can be commanded with a complete set of radio commands (radio menu in your flight, and then F10).
--
-- The radio commands will vary according the category of the group. The richest set of commands are with helicopters and airPlanes.
-- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts.
-- 
-- Escorts detect targets using a built-in detection mechanism. The detected targets are reported at a specified time interval.
-- Once targets are reported, each escort has these targets as menu options to command the attack of these targets.
-- Targets are by default grouped per area of 5000 meters, but the kind of detection and the grouping range can be altered.
-- 
-- Different formations can be selected in the Flight menu: Trail, Stack, Left Line, Right Line, Left Wing, Right Wing, Central Wing and Boxed formations are available.
-- The Flight menu also allows for a mass attack, where all of the escorts are commanded to attack a target.
-- 
-- Escorts can emit flares to reports their location. They can be commanded to hold at a location, which can be their current or the leader location.
-- In this way, you can spread out the escorts over the battle field before a coordinated attack.
-- 
-- But basically, the escort class provides 4 modes of operation, and depending on the mode, you are either leading the flight, or following the flight.
-- 
-- ## Leading the flight
-- 
-- When leading the flight, you are expected to guide the escorts towards the target areas, 
-- and carefully coordinate the attack based on the threat levels reported, and the available weapons
-- carried by the escorts. Ground ships or ground troops can execute A-assisted attacks, when they have long-range ground precision weapons for attack.
-- 
-- ## Following the flight
-- 
-- Escorts can be commanded to execute a specific mission path. In this mode, the escorts are in the lead.
-- You as a player, are following the escorts, and are commanding them to progress the mission while
-- ensuring that the escorts survive. You are joining the escorts in the battlefield. They will detect and report targets
-- and you will ensure that the attacks are well coordinated, assigning the correct escort type for the detected target
-- type. Once the attack is finished, the escort will resume the mission it was assigned.
-- In other words, you can use the escorts for reconnaissance, and for guiding the attack.
-- Imagine you as a mi-8 pilot, assigned to pickup cargo. Two ka-50s are guiding the way, and you are
-- following. You are in control. The ka-50s detect targets, report them, and you command how the attack
-- will commence and from where. You can control where the escorts are holding position and which targets
-- are attacked first. You are in control how the ka-50s will follow their mission path.
-- 
-- Escorts can act as part of a AI A2G dispatcher offensive. In this way, You was a player are in control.
-- The mission is defined by the A2G dispatcher, and you are responsible to join the flight and ensure that the
-- attack is well coordinated.
-- 
-- It is with great proud that I present you this class, and I hope you will enjoy the functionality and the dynamism
-- it brings in your DCS world simulations.
-- 
-- # RADIO MENUs that can be created:
-- 
-- Find a summary below of the current available commands:
--
-- ## Navigation ...:
-- 
-- Escort group navigation functions:
--
--   * **"Join-Up":** The escort group fill follow you in the assigned formation.
--   * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color.
--   * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops.
--
-- ## Hold position ...:
-- 
-- Escort group navigation functions:
--
--   * **"At current location":** The escort group will hover above the ground at the position they were. The altitude can be specified as a parameter.
--   * **"At my location":** The escort group will hover or orbit at the position where you are. The escort will fly to your location and hold position. The altitude can be specified as a parameter.
--
-- ## Report targets ...:
-- 
-- Report targets will make the escort group to report any target that it identifies within detection range. Any detected target can be attacked using the "Attack Targets" menu function. (see below).
--
--   * **"Report now":** Will report the current detected targets.
--   * **"Report targets on":** Will make the escorts to report the detected targets and will fill the "Attack Targets" menu list.
--   * **"Report targets off":** Will stop detecting targets.
--
-- ## Attack targets ...:
-- 
-- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed.
-- This menu will be available in Flight menu or in each Escort menu.
--
-- ## Scan targets ...:
-- 
-- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or rejoin formation.
--
--   * **"Scan targets 30 seconds":** Scan 30 seconds for targets.
--   * **"Scan targets 60 seconds":** Scan 60 seconds for targets.
--
-- ## Request assistance from ...:
-- 
-- This menu item will list all detected targets within a 15km range, similar as with the menu item **Attack Targets**.
-- This menu item allows to request attack support from other ground based escorts supporting the current escort.
-- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles.
-- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area.
--
-- ## ROE ...:
-- 
-- Sets the Rules of Engagement (ROE) of the escort group when in flight.
--
--   * **"Hold Fire":** The escort group will hold fire.
--   * **"Return Fire":** The escort group will return fire.
--   * **"Open Fire":** The escort group will open fire on designated targets.
--   * **"Weapon Free":** The escort group will engage with any target.
--
-- ## Evasion ...:
-- 
-- Will define the evasion techniques that the escort group will perform during flight or combat.
--
--   * **"Fight until death":** The escort group will have no reaction to threats.
--   * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed.
--   * **"Evade enemy fire":** The rescort group will evade enemy fire before firing.
--   * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres.
--
-- ## Resume Mission ...:
-- 
-- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint.
-- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission.
--
-- ===
-- 
-- ### Authors: **FlightControl** 
-- 
-- ===
--
-- @module AI.AI_Escort
-- @image Escorting.JPG


---
-- @type AI_ESCORT
-- @extends AI.AI_Formation#AI_FORMATION


-- TODO: Add the menus when the class Start method is activated.
-- TODO: Remove the menus when the class Stop method is called.

--- AI_ESCORT class
-- 
-- # AI_ESCORT construction methods.
-- 
-- Create a new AI_ESCORT object with the @{#AI_ESCORT.New} method:
--
--  * @{#AI_ESCORT.New}: Creates a new AI_ESCORT object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text.
--
-- @usage
-- -- Declare a new EscortPlanes object as follows:
-- 
-- -- First find the GROUP object and the CLIENT object.
-- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor.
-- local EscortGroup = SET_GROUP:New():FilterPrefixes("Escort"):FilterOnce() -- The the group name of the escorts contains "Escort".
-- 
-- -- Now use these 2 objects to construct the new EscortPlanes object.
-- EscortPlanes = AI_ESCORT:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." )
-- EscortPlanes:MenusAirplanes() -- create menus for airplanes
-- EscortPlanes:__Start(2)
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- @field #AI_ESCORT
AI_ESCORT = {
  ClassName = "AI_ESCORT",
  EscortName = nil, -- The Escort Name
  EscortUnit = nil,
  EscortGroup = nil,
  EscortMode = 1,
  Targets = {}, -- The identified targets
  FollowScheduler = nil,
  ReportTargets = true,
  OptionROE = AI.Option.Air.val.ROE.OPEN_FIRE,
  OptionReactionOnThreat = AI.Option.Air.val.REACTION_ON_THREAT.ALLOW_ABORT_MISSION,
  SmokeDirectionVector = false,
  TaskPoints = {}
}

-- @field Functional.Detection#DETECTION_AREAS
AI_ESCORT.Detection = nil

--- AI_ESCORT class constructor for an AI group
-- @param #AI_ESCORT self
-- @param Wrapper.Client#CLIENT EscortUnit The client escorted by the EscortGroup.
-- @param Core.Set#SET_GROUP EscortGroupSet The set of group AI escorting the EscortUnit.
-- @param #string EscortName Name of the escort.
-- @param #string EscortBriefing A text showing the AI_ESCORT briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown.
-- @return #AI_ESCORT self
-- @usage
-- -- Declare a new EscortPlanes object as follows:
-- 
-- -- First find the GROUP object and the CLIENT object.
-- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor.
-- local EscortGroup = SET_GROUP:New():FilterPrefixes("Escort"):FilterOnce() -- The the group name of the escorts contains "Escort".
-- 
-- -- Now use these 2 objects to construct the new EscortPlanes object.
-- EscortPlanes = AI_ESCORT:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." )
-- EscortPlanes:MenusAirplanes() -- create menus for airplanes
-- EscortPlanes:__Start(2)
-- 
-- 
function AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing )
  
  local self = BASE:Inherit( self, AI_FORMATION:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) ) -- #AI_ESCORT
  self:F( { EscortUnit, EscortGroupSet } )

  self.PlayerUnit = self.FollowUnit -- Wrapper.Unit#UNIT
  self.PlayerGroup = self.FollowUnit:GetGroup() -- Wrapper.Group#GROUP
  
  self.EscortName = EscortName
  self.EscortGroupSet = EscortGroupSet
  
  self.EscortGroupSet:SetSomeIteratorLimit( 8 )

  self.EscortBriefing = EscortBriefing
 
  self.Menu = {}
  self.Menu.HoldAtEscortPosition = self.Menu.HoldAtEscortPosition or {}
  self.Menu.HoldAtLeaderPosition = self.Menu.HoldAtLeaderPosition or {}
  self.Menu.Flare = self.Menu.Flare or {}
  self.Menu.Smoke = self.Menu.Smoke or {}
  self.Menu.Targets = self.Menu.Targets or {}
  self.Menu.ROE = self.Menu.ROE or {}
  self.Menu.ROT = self.Menu.ROT or {}
  
--  if not EscortBriefing then
--    EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") reporting! " ..
--      "We're escorting your flight. " ..
--      "Use the Radio Menu and F10 and use the options under + " .. EscortName .. "\n",
--      60, EscortUnit
--    )
--  else
--    EscortGroup:MessageToClient( EscortGroup:GetCategoryName() .. " '" .. EscortName .. "' (" .. EscortGroup:GetCallsign() .. ") " .. EscortBriefing,
--      60, EscortUnit
--    )
--  end

  self.FollowDistance = 100
  self.CT1 = 0
  self.GT1 = 0


  EscortGroupSet:ForEachGroup(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      -- Set EscortGroup known at EscortUnit.
      if not self.PlayerUnit._EscortGroups then
        self.PlayerUnit._EscortGroups = {}
      end
    
      if not self.PlayerUnit._EscortGroups[EscortGroup:GetName()] then
        self.PlayerUnit._EscortGroups[EscortGroup:GetName()] = {}
        self.PlayerUnit._EscortGroups[EscortGroup:GetName()].EscortGroup = EscortGroup
        self.PlayerUnit._EscortGroups[EscortGroup:GetName()].EscortName = self.EscortName
        self.PlayerUnit._EscortGroups[EscortGroup:GetName()].Detection = self.Detection
      end  
    end
  )
  
  self:SetFlightReportType( self.__Enum.ReportType.All )
  
  return self
end


function AI_ESCORT:_InitFlightMenus()

  self:SetFlightMenuJoinUp()
  self:SetFlightMenuFormation( "Trail" )
  self:SetFlightMenuFormation( "Stack" )
  self:SetFlightMenuFormation( "LeftLine" )
  self:SetFlightMenuFormation( "RightLine" )
  self:SetFlightMenuFormation( "LeftWing" )
  self:SetFlightMenuFormation( "RightWing" )
  self:SetFlightMenuFormation( "Vic" )
  self:SetFlightMenuFormation( "Box" )

  self:SetFlightMenuHoldAtEscortPosition()
  self:SetFlightMenuHoldAtLeaderPosition()
  
  self:SetFlightMenuFlare()
  self:SetFlightMenuSmoke()

  self:SetFlightMenuROE()
  self:SetFlightMenuROT()

  self:SetFlightMenuTargets()
  self:SetFlightMenuReportType()

end

function AI_ESCORT:_InitEscortMenus( EscortGroup )

  EscortGroup.EscortMenu = MENU_GROUP:New( self.PlayerGroup, EscortGroup:GetCallsign(), self.MainMenu )
  
  self:SetEscortMenuJoinUp( EscortGroup )
  self:SetEscortMenuResumeMission( EscortGroup )
  
  self:SetEscortMenuHoldAtEscortPosition( EscortGroup )
  self:SetEscortMenuHoldAtLeaderPosition( EscortGroup )
  
  self:SetEscortMenuFlare( EscortGroup )
  self:SetEscortMenuSmoke( EscortGroup )

  self:SetEscortMenuROE( EscortGroup )
  self:SetEscortMenuROT( EscortGroup )
  
  self:SetEscortMenuTargets( EscortGroup )

end

function AI_ESCORT:_InitEscortRoute( EscortGroup )

  EscortGroup.MissionRoute = EscortGroup:GetTaskRoute()

end


-- @param #AI_ESCORT self
-- @param Core.Set#SET_GROUP EscortGroupSet
function AI_ESCORT:onafterStart( EscortGroupSet )

  self:F()

  EscortGroupSet:ForEachGroup(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      EscortGroup:WayPointInitialize()
    
      EscortGroup:OptionROTVertical()
      EscortGroup:OptionROEOpenFire()
    end
  )

  -- TODO:Revise this...  
  local LeaderEscort = EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP
  if LeaderEscort then
    local Report = REPORT:New( "Escort reporting:" )
    Report:Add( "Joining Up " .. EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.PlayerUnit ) )
    LeaderEscort:MessageTypeToGroup( Report:Text(),  MESSAGE.Type.Information, self.PlayerUnit )
  end

  self.Detection = DETECTION_AREAS:New( EscortGroupSet, 5000 )

  -- This only makes the escort report detections made by the escort, not through DLINK.
  -- These must be enquired using other facilities.
  -- In this way, the escort will report the target areas that are relevant for the mission.
  self.Detection:InitDetectVisual( true )
  self.Detection:InitDetectIRST( true )
  self.Detection:InitDetectOptical( true )
  self.Detection:InitDetectRadar( true )
  self.Detection:InitDetectRWR( true )
  
  self.Detection:SetAcceptRange( 100000 )

  self.Detection:__Start( 30 )
    
  self.MainMenu = MENU_GROUP:New( self.PlayerGroup, self.EscortName )
  self.FlightMenu = MENU_GROUP:New( self.PlayerGroup, "Flight", self.MainMenu )

  self:_InitFlightMenus()
  
  self.EscortGroupSet:ForSomeGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )

      self:_InitEscortMenus( EscortGroup )
      self:_InitEscortRoute( EscortGroup )

      self:SetFlightModeFormation( EscortGroup )
      
      -- @param #AI_ESCORT self
      -- @param Core.Event#EVENTDATA EventData
      function EscortGroup:OnEventDeadOrCrash( EventData )
        self:F( { "EventDead", EventData } )
        self.EscortMenu:Remove()
      end

      EscortGroup:HandleEvent( EVENTS.Dead, EscortGroup.OnEventDeadOrCrash )
      EscortGroup:HandleEvent( EVENTS.Crash, EscortGroup.OnEventDeadOrCrash )
      
    end
  )
  

end

-- @param #AI_ESCORT self
-- @param Core.Set#SET_GROUP EscortGroupSet
function AI_ESCORT:onafterStop( EscortGroupSet )

  self:F()

  EscortGroupSet:ForEachGroup(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      EscortGroup:WayPointInitialize()
    
      EscortGroup:OptionROTVertical()
      EscortGroup:OptionROEOpenFire()
    end
  )

  self.Detection:Stop()
    
  self.MainMenu:Remove()

end

--- Set a Detection method for the EscortUnit to be reported upon.
-- Detection methods are based on the derived classes from DETECTION_BASE.
-- @param #AI_ESCORT self
-- @param Functional.Detection#DETECTION_AREAS Detection
function AI_ESCORT:SetDetection( Detection )

  self.Detection = Detection
  self.EscortGroup.Detection = self.Detection
  self.PlayerUnit._EscortGroups[self.EscortGroup:GetName()].Detection = self.EscortGroup.Detection
  
  Detection:__Start( 1 )
  
end

--- This function is for test, it will put on the frequency of the FollowScheduler a red smoke at the direction vector calculated for the escort to fly to.
-- This allows to visualize where the escort is flying to.
-- @param #AI_ESCORT self
-- @param #boolean SmokeDirection If true, then the direction vector will be smoked.
function AI_ESCORT:TestSmokeDirectionVector( SmokeDirection )
  self.SmokeDirectionVector = ( SmokeDirection == true ) and true or false
end


--- Defines the default menus for helicopters.
-- @param #AI_ESCORT self
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @param #number ZLevels The amount of levels on the Z-axis.
-- @return #AI_ESCORT
function AI_ESCORT:MenusHelicopters( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels )
  self:F()

--  self:MenuScanForTargets( 100, 60 )

  self.XStart = XStart or 50
  self.XSpace = XSpace or 50
  self.YStart = YStart or 50
  self.YSpace = YSpace or 50
  self.ZStart = ZStart or 50
  self.ZSpace = ZSpace or 50
  self.ZLevels = ZLevels or 10

  self:MenuJoinUp()
  self:MenuFormationTrail(self.XStart,self.XSpace,self.YStart)
  self:MenuFormationStack(self.XStart,self.XSpace,self.YStart,self.YSpace)
  self:MenuFormationLeftLine(self.XStart,self.YStart,self.ZStart,self.ZSpace)
  self:MenuFormationRightLine(self.XStart,self.YStart,self.ZStart,self.ZSpace)
  self:MenuFormationLeftWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace)
  self:MenuFormationRightWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace)
  self:MenuFormationVic(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace)
  self:MenuFormationBox(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace,self.ZLevels)
  
  self:MenuHoldAtEscortPosition( 30 )
  self:MenuHoldAtEscortPosition( 100 )
  self:MenuHoldAtEscortPosition( 500 )
  self:MenuHoldAtLeaderPosition( 30, 500 )
  
  self:MenuFlare()
  self:MenuSmoke()

  self:MenuTargets( 60 )
  self:MenuAssistedAttack()
  self:MenuROE()
  self:MenuROT()

  return self
end


--- Defines the default menus for airplanes.
-- @param #AI_ESCORT self
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @param #number ZLevels The amount of levels on the Z-axis.
-- @return #AI_ESCORT
function AI_ESCORT:MenusAirplanes( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels )
  self:F()

--  self:MenuScanForTargets( 100, 60 )

  self.XStart = XStart or 50
  self.XSpace = XSpace or 50
  self.YStart = YStart or 50
  self.YSpace = YSpace or 50
  self.ZStart = ZStart or 50
  self.ZSpace = ZSpace or 50
  self.ZLevels = ZLevels or 10

  self:MenuJoinUp()
  self:MenuFormationTrail(self.XStart,self.XSpace,self.YStart)
  self:MenuFormationStack(self.XStart,self.XSpace,self.YStart,self.YSpace)
  self:MenuFormationLeftLine(self.XStart,self.YStart,self.ZStart,self.ZSpace)
  self:MenuFormationRightLine(self.XStart,self.YStart,self.ZStart,self.ZSpace)
  self:MenuFormationLeftWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace)
  self:MenuFormationRightWing(self.XStart,self.XSpace,self.YStart,self.ZStart,self.ZSpace)
  self:MenuFormationVic(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace)
  self:MenuFormationBox(self.XStart,self.XSpace,self.YStart,self.YSpace,self.ZStart,self.ZSpace,self.ZLevels)
  
  self:MenuHoldAtEscortPosition( 1000, 500 )
  self:MenuHoldAtLeaderPosition( 1000, 500 )
  
  self:MenuFlare()
  self:MenuSmoke()

  self:MenuTargets( 60 )
  self:MenuAssistedAttack()
  self:MenuROE()
  self:MenuROT()

  return self
end


function AI_ESCORT:SetFlightMenuFormation( Formation )

  local FormationID = "Formation" .. Formation
  
  local MenuFormation = self.Menu[FormationID]

  if MenuFormation then
    local Arguments = MenuFormation.Arguments
    --self:T({Arguments=unpack(Arguments)})
    local FlightMenuFormation = MENU_GROUP:New( self.PlayerGroup, "Formation", self.MainMenu )
    local MenuFlightFormationID = MENU_GROUP_COMMAND:New( self.PlayerGroup, Formation, FlightMenuFormation, 
      function ( self, Formation, ... )
        self.EscortGroupSet:ForSomeGroupAlive(
          -- @param Wrapper.Group#GROUP EscortGroup
          function( EscortGroup, self, Formation, Arguments )
            if EscortGroup:IsAir() then
              self:E({FormationID=FormationID})
              self[FormationID]( self, unpack(Arguments) )
            end
          end, self, Formation, Arguments
        )
      end, self, Formation, Arguments
    )
  end
    
  return self
end


function AI_ESCORT:MenuFormation( Formation, ... )

  local FormationID = "Formation"..Formation
  self.Menu[FormationID] = self.Menu[FormationID] or {}
  self.Menu[FormationID].Arguments = arg

end


--- Defines a menu slot to let the escort to join in a trail formation.
-- This menu will appear under **Formation**.
-- @param #AI_ESCORT self
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @return #AI_ESCORT
function AI_ESCORT:MenuFormationTrail( XStart, XSpace, YStart )

  self:MenuFormation( "Trail", XStart, XSpace, YStart )

  return self
end

--- Defines a menu slot to let the escort to join in a stacked formation.
-- This menu will appear under **Formation**.
-- @param #AI_ESCORT self
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
-- @return #AI_ESCORT
function AI_ESCORT:MenuFormationStack( XStart, XSpace, YStart, YSpace )

  self:MenuFormation( "Stack", XStart, XSpace, YStart, YSpace )

  return self
end


--- Defines a menu slot to let the escort to join in a leFt wing formation.
-- This menu will appear under **Formation**.
-- @param #AI_ESCORT self
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @return #AI_ESCORT
function AI_ESCORT:MenuFormationLeftLine( XStart, YStart, ZStart, ZSpace )

  self:MenuFormation( "LeftLine", XStart, YStart, ZStart, ZSpace )

  return self
end


--- Defines a menu slot to let the escort to join in a right line formation.
-- This menu will appear under **Formation**.
-- @param #AI_ESCORT self
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @return #AI_ESCORT
function AI_ESCORT:MenuFormationRightLine( XStart, YStart, ZStart, ZSpace )

  self:MenuFormation( "RightLine", XStart, YStart, ZStart, ZSpace )

  return self
end


--- Defines a menu slot to let the escort to join in a left wing formation.
-- This menu will appear under **Formation**.
-- @param #AI_ESCORT self
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @return #AI_ESCORT
function AI_ESCORT:MenuFormationLeftWing( XStart, XSpace, YStart, ZStart, ZSpace )

  self:MenuFormation( "LeftWing", XStart, XSpace, YStart, ZStart, ZSpace )

  return self
end


--- Defines a menu slot to let the escort to join in a right wing formation.
-- This menu will appear under **Formation**.
-- @param #AI_ESCORT self
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @return #AI_ESCORT
function AI_ESCORT:MenuFormationRightWing( XStart, XSpace, YStart, ZStart, ZSpace )

  self:MenuFormation( "RightWing", XStart, XSpace, YStart, ZStart, ZSpace )

  return self
end


--- Defines a menu slot to let the escort to join in a center wing formation.
-- This menu will appear under **Formation**.
-- @param #AI_ESCORT self
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @return #AI_ESCORT
function AI_ESCORT:MenuFormationCenterWing( XStart, XSpace, YStart, YSpace, ZStart, ZSpace )

  self:MenuFormation( "CenterWing", XStart, XSpace, YStart, YSpace, ZStart, ZSpace )

  return self
end


--- Defines a menu slot to let the escort to join in a vic formation.
-- This menu will appear under **Formation**.
-- @param #AI_ESCORT self
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @return #AI_ESCORT
function AI_ESCORT:MenuFormationVic( XStart, XSpace, YStart, YSpace, ZStart, ZSpace )

  self:MenuFormation( "Vic", XStart, XSpace, YStart, YSpace, ZStart, ZSpace )

  return self
end


--- Defines a menu slot to let the escort to join in a box formation.
-- This menu will appear under **Formation**.
-- @param #AI_ESCORT self
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
-- @param #number ZStart The start position on the Z-axis in meters for the first group.
-- @param #number ZSpace The space between groups on the Z-axis in meters for each sequent group.
-- @param #number ZLevels The amount of levels on the Z-axis.
-- @return #AI_ESCORT
function AI_ESCORT:MenuFormationBox( XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels )

  self:MenuFormation( "Box", XStart, XSpace, YStart, YSpace, ZStart, ZSpace, ZLevels )

  return self
end

function AI_ESCORT:SetFlightMenuJoinUp()

  if self.Menu.JoinUp == true then
    local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu )
    local FlightMenuJoinUp  = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up",  FlightMenuReportNavigation, AI_ESCORT._FlightJoinUp, self )
  end
  
end


--- Sets a menu slot to join formation for an escort.
-- @param #AI_ESCORT self
-- @return #AI_ESCORT
function AI_ESCORT:SetEscortMenuJoinUp( EscortGroup )

  if self.Menu.JoinUp == true then
    if EscortGroup:IsAir() then
      local EscortGroupName = EscortGroup:GetName()
      local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu )
      local EscortMenuJoinUp = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Join Up", EscortMenuReportNavigation, AI_ESCORT._JoinUp, self, EscortGroup )
    end
  end
end



--- Defines --- Defines a menu slot to let the escort to join formation.
-- @param #AI_ESCORT self
-- @return #AI_ESCORT
function AI_ESCORT:MenuJoinUp()

  self.Menu.JoinUp = true

  return self
end


function AI_ESCORT:SetFlightMenuHoldAtEscortPosition()

  for _, MenuHoldAtEscortPosition in pairs( self.Menu.HoldAtEscortPosition or {} ) do
    local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu )
  
    local FlightMenuHoldPosition = MENU_GROUP_COMMAND
      :New(
        self.PlayerGroup,
        MenuHoldAtEscortPosition.MenuText,
        FlightMenuReportNavigation,
        AI_ESCORT._FlightHoldPosition,
        self,
        nil,
        MenuHoldAtEscortPosition.Height,
        MenuHoldAtEscortPosition.Speed
      )
  
    end
  return self
end

function AI_ESCORT:SetEscortMenuHoldAtEscortPosition( EscortGroup )

  for _, HoldAtEscortPosition in pairs( self.Menu.HoldAtEscortPosition or {}) do
    if EscortGroup:IsAir() then
      local EscortGroupName = EscortGroup:GetName()
      local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu )
      local EscortMenuHoldPosition = MENU_GROUP_COMMAND
        :New(
          self.PlayerGroup,
          HoldAtEscortPosition.MenuText,
          EscortMenuReportNavigation,
          AI_ESCORT._HoldPosition,
          self,
          EscortGroup,
          EscortGroup,
          HoldAtEscortPosition.Height,
          HoldAtEscortPosition.Speed
        )
    end
  end
  
  return self
end


--- Defines a menu slot to let the escort hold at their current position and stay low with a specified height during a specified time in seconds.
-- This menu will appear under **Hold position**.
-- @param #AI_ESCORT self
-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters.
-- @param DCS#Time Speed Optional parameter that lets the escort orbit with a specified speed. The default value is a speed that is average for the type of airplane or helicopter.
-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed.
-- @return #AI_ESCORT
function AI_ESCORT:MenuHoldAtEscortPosition( Height, Speed, MenuTextFormat )
  self:F( { Height, Speed, MenuTextFormat } )

  if not Height then
    Height = 30
  end

  if not Speed then
    Speed = 0
  end

  local MenuText = ""
  if not MenuTextFormat then
    if Speed == 0 then
      MenuText = string.format( "Hold at %d meter", Height )
    else
      MenuText = string.format( "Hold at %d meter at %d", Height, Speed )
    end
  else
    if Speed == 0 then
      MenuText = string.format( MenuTextFormat, Height )
    else
      MenuText = string.format( MenuTextFormat, Height, Speed )
    end
  end

  self.Menu.HoldAtEscortPosition = self.Menu.HoldAtEscortPosition or {}
  self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition+1] = {}
  self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].Height = Height
  self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].Speed = Speed
  self.Menu.HoldAtEscortPosition[#self.Menu.HoldAtEscortPosition].MenuText = MenuText

  return self
end


function AI_ESCORT:SetFlightMenuHoldAtLeaderPosition()

  for _, MenuHoldAtLeaderPosition in pairs( self.Menu.HoldAtLeaderPosition or {}) do
    local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu )
  
    local FlightMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND
      :New(
        self.PlayerGroup,
        MenuHoldAtLeaderPosition.MenuText,
        FlightMenuReportNavigation,
        AI_ESCORT._FlightHoldPosition,
        self,
        self.PlayerGroup,
        MenuHoldAtLeaderPosition.Height,
        MenuHoldAtLeaderPosition.Speed
      )
  end

  return self
end

function AI_ESCORT:SetEscortMenuHoldAtLeaderPosition( EscortGroup )

  for _, HoldAtLeaderPosition in pairs( self.Menu.HoldAtLeaderPosition or {}) do
    if EscortGroup:IsAir() then
      
      local EscortGroupName = EscortGroup:GetName()
      local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu )
  
      local EscortMenuHoldAtLeaderPosition = MENU_GROUP_COMMAND
        :New(
          self.PlayerGroup,
          HoldAtLeaderPosition.MenuText,
          EscortMenuReportNavigation,
          AI_ESCORT._HoldPosition,
          self,
          self.PlayerGroup,
          EscortGroup,
          HoldAtLeaderPosition.Height,
          HoldAtLeaderPosition.Speed
        )
    end
  end

  return self
end

--- Defines a menu slot to let the escort hold at the client position and stay low with a specified height during a specified time in seconds.
-- This menu will appear under **Navigation**.
-- @param #AI_ESCORT self
-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters.
-- @param DCS#Time Speed Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given.
-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed.
-- @return #AI_ESCORT
function AI_ESCORT:MenuHoldAtLeaderPosition( Height, Speed, MenuTextFormat )
  self:F( { Height, Speed, MenuTextFormat } )

  if not Height then
    Height = 30
  end

  if not Speed then
    Speed = 0
  end

  local MenuText = ""
  if not MenuTextFormat then
    if Speed == 0 then
      MenuText = string.format( "Rejoin and hold at %d meter", Height )
    else
      MenuText = string.format( "Rejoin and hold at %d meter at %d", Height, Speed )
    end
  else
    if Speed == 0 then
      MenuText = string.format( MenuTextFormat, Height )
    else
      MenuText = string.format( MenuTextFormat, Height, Speed )
    end
  end

  self.Menu.HoldAtLeaderPosition = self.Menu.HoldAtLeaderPosition or {}
  self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition+1] = {}
  self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].Height = Height
  self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].Speed = Speed
  self.Menu.HoldAtLeaderPosition[#self.Menu.HoldAtLeaderPosition].MenuText = MenuText

  return self
end

--- Defines a menu slot to let the escort scan for targets at a certain height for a certain time in seconds.
-- This menu will appear under **Scan targets**.
-- @param #AI_ESCORT self
-- @param DCS#Distance Height Optional parameter that sets the height in meters to let the escort orbit at the current location. The default value is 30 meters.
-- @param DCS#Time Seconds Optional parameter that lets the escort orbit at the current position for a specified time. (not implemented yet). The default value is 0 seconds, meaning, that the escort will orbit forever until a sequent command is given.
-- @param #string MenuTextFormat Optional parameter that shows the menu option text. The text string is formatted, and should contain one or two %d tokens in the string. The first for the Height, the second for the Time (if given). If no text is given, the default text will be displayed.
-- @return #AI_ESCORT
function AI_ESCORT:MenuScanForTargets( Height, Seconds, MenuTextFormat )
  self:F( { Height, Seconds, MenuTextFormat } )

  if self.EscortGroup:IsAir() then
    if not self.EscortMenuScan then
      self.EscortMenuScan = MENU_GROUP:New( self.PlayerGroup, "Scan for targets", self.EscortMenu )
    end

    if not Height then
      Height = 100
    end

    if not Seconds then
      Seconds = 30
    end

    local MenuText = ""
    if not MenuTextFormat then
      if Seconds == 0 then
        MenuText = string.format( "At %d meter", Height )
      else
        MenuText = string.format( "At %d meter for %d seconds", Height, Seconds )
      end
    else
      if Seconds == 0 then
        MenuText = string.format( MenuTextFormat, Height )
      else
        MenuText = string.format( MenuTextFormat, Height, Seconds )
      end
    end

    if not self.EscortMenuScanForTargets then
      self.EscortMenuScanForTargets = {}
    end

    self.EscortMenuScanForTargets[#self.EscortMenuScanForTargets+1] = MENU_GROUP_COMMAND
      :New(
        self.PlayerGroup,
        MenuText,
        self.EscortMenuScan,
        AI_ESCORT._ScanTargets,
        self,
        30
      )
  end

  return self
end


function AI_ESCORT:SetFlightMenuFlare()

  for _, MenuFlare in pairs( self.Menu.Flare or {}) do
    local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu )
    local FlightMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuFlare.MenuText, FlightMenuReportNavigation )
  
    local FlightMenuFlareGreenFlight  = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare",  FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Green,  "Released a green flare!"   )
    local FlightMenuFlareRedFlight    = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red flare",    FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Red,    "Released a red flare!"     )
    local FlightMenuFlareWhiteFlight  = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white flare",  FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.White,  "Released a white flare!"   )
    local FlightMenuFlareYellowFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release yellow flare", FlightMenuFlare, AI_ESCORT._FlightFlare, self, FLARECOLOR.Yellow, "Released a yellow flare!"  )
  end

  return self
end

function AI_ESCORT:SetEscortMenuFlare( EscortGroup )

  for _, MenuFlare in pairs( self.Menu.Flare or {}) do
    if EscortGroup:IsAir() then
      
      local EscortGroupName = EscortGroup:GetName()
      local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu )
      local EscortMenuFlare = MENU_GROUP:New( self.PlayerGroup, MenuFlare.MenuText, EscortMenuReportNavigation )

      local EscortMenuFlareGreen  = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green flare",  EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Green,  "Released a green flare!"   )
      local EscortMenuFlareRed    = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red flare",    EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Red,    "Released a red flare!"     )
      local EscortMenuFlareWhite  = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white flare",  EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.White,  "Released a white flare!"   )
      local EscortMenuFlareYellow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release yellow flare", EscortMenuFlare, AI_ESCORT._Flare, self, EscortGroup, FLARECOLOR.Yellow, "Released a yellow flare!"  )
    end
  end

  return self
end



--- Defines a menu slot to let the escort disperse a flare in a certain color.
-- This menu will appear under **Navigation**.
-- The flare will be fired from the first unit in the group.
-- @param #AI_ESCORT self
-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed.
-- @return #AI_ESCORT
function AI_ESCORT:MenuFlare( MenuTextFormat )
  self:F()

  local MenuText = ""
  if not MenuTextFormat then
    MenuText = "Flare"
  else
    MenuText = MenuTextFormat
  end

  self.Menu.Flare = self.Menu.Flare or {}
  self.Menu.Flare[#self.Menu.Flare+1] = {}
  self.Menu.Flare[#self.Menu.Flare].MenuText = MenuText

  return self
end


function AI_ESCORT:SetFlightMenuSmoke()

  for _, MenuSmoke in pairs( self.Menu.Smoke or {}) do
    local FlightMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", self.FlightMenu )
    local FlightMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuSmoke.MenuText, FlightMenuReportNavigation )
  
    local FlightMenuSmokeGreenFlight  = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke",  FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Green,  "Releasing green smoke!"   )
    local FlightMenuSmokeRedFlight    = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red smoke",    FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Red,    "Releasing red smoke!"     )
    local FlightMenuSmokeWhiteFlight  = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white smoke",  FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.White,  "Releasing white smoke!"   )
    local FlightMenuSmokeOrangeFlight = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release orange smoke", FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Orange, "Releasing orange smoke!"  )
    local FlightMenuSmokeBlueFlight   = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release blue smoke",   FlightMenuSmoke, AI_ESCORT._FlightSmoke, self, SMOKECOLOR.Blue,   "Releasing blue smoke!"    )
  end

  return self
end


function AI_ESCORT:SetEscortMenuSmoke( EscortGroup )

  for _, MenuSmoke in pairs( self.Menu.Smoke or {}) do
    if EscortGroup:IsAir() then
      
      local EscortGroupName = EscortGroup:GetName()
      local EscortMenuReportNavigation = MENU_GROUP:New( self.PlayerGroup, "Navigation", EscortGroup.EscortMenu )
      local EscortMenuSmoke = MENU_GROUP:New( self.PlayerGroup, MenuSmoke.MenuText, EscortMenuReportNavigation )

      local EscortMenuSmokeGreen  = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release green smoke",  EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Green,  "Releasing green smoke!"   )
      local EscortMenuSmokeRed    = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release red smoke",    EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Red,    "Releasing red smoke!"     )
      local EscortMenuSmokeWhite  = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release white smoke",  EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.White,  "Releasing white smoke!"   )
      local EscortMenuSmokeOrange = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release orange smoke", EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Orange, "Releasing orange smoke!"  )
      local EscortMenuSmokeBlue   = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Release blue smoke",   EscortMenuSmoke, AI_ESCORT._Smoke, self, EscortGroup, SMOKECOLOR.Blue,   "Releasing blue smoke!"    )
    end
  end

  return self
end


--- Defines a menu slot to let the escort disperse a smoke in a certain color.
-- This menu will appear under **Navigation**.
-- Note that smoke menu options will only be displayed for ships and ground units. Not for air units.
-- The smoke will be fired from the first unit in the group.
-- @param #AI_ESCORT self
-- @param #string MenuTextFormat Optional parameter that shows the menu option text. If no text is given, the default text will be displayed.
-- @return #AI_ESCORT
function AI_ESCORT:MenuSmoke( MenuTextFormat )
  self:F()

  local MenuText = ""
  if not MenuTextFormat then
    MenuText = "Smoke"
  else
    MenuText = MenuTextFormat
  end

  self.Menu.Smoke = self.Menu.Smoke or {}
  self.Menu.Smoke[#self.Menu.Smoke+1] = {}
  self.Menu.Smoke[#self.Menu.Smoke].MenuText = MenuText

  return self
end

function AI_ESCORT:SetFlightMenuReportType()

  local FlightMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", self.FlightMenu )
  local MenuStamp = FlightMenuReportTargets:GetStamp()

  local FlightReportType = self:GetFlightReportType()
  
  if FlightReportType ~= self.__Enum.ReportType.All then
    local FlightMenuReportTargetsAll = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report all targets",  FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeAll, self )
                                                         :SetTag( "ReportType" )
                                                         :SetStamp( MenuStamp )
  end
  
  if FlightReportType == self.__Enum.ReportType.All or FlightReportType ~= self.__Enum.ReportType.Airborne then
    local FlightMenuReportTargetsAirborne = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report airborne targets",  FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeAirborne, self )
                                                              :SetTag( "ReportType" )
                                                              :SetStamp( MenuStamp )
  end
  
  if FlightReportType == self.__Enum.ReportType.All or FlightReportType ~= self.__Enum.ReportType.GroundRadar then
    local FlightMenuReportTargetsGroundRadar = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report gound radar targets",  FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeGroundRadar, self )
                                                                 :SetTag( "ReportType" )
                                                                 :SetStamp( MenuStamp )
  end
  if FlightReportType == self.__Enum.ReportType.All or FlightReportType ~= self.__Enum.ReportType.Ground then
    local FlightMenuReportTargetsGround = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report ground targets",  FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportTypeGround, self )
                                                            :SetTag( "ReportType" )
                                                            :SetStamp( MenuStamp )
  end
  
  FlightMenuReportTargets:RemoveSubMenus( MenuStamp, "ReportType" )

end


function AI_ESCORT:SetFlightMenuTargets()

  local FlightMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", self.FlightMenu )

  -- Report Targets
  local FlightMenuReportTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets now!", FlightMenuReportTargets, AI_ESCORT._FlightReportNearbyTargetsNow, self )
  local FlightMenuReportTargetsOn =  MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on",   FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, true )
  local FlightMenuReportTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off",  FlightMenuReportTargets, AI_ESCORT._FlightSwitchReportNearbyTargets, self, false )

  -- Attack Targets
  self.FlightMenuAttack = MENU_GROUP:New( self.PlayerGroup, "Attack targets", self.FlightMenu )
  local FlightMenuAttackNearby = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Attack nearest targets",  self.FlightMenuAttack, AI_ESCORT._FlightAttackNearestTarget, self ):SetTag( "Attack" )
  local FlightMenuAttackNearbyAir = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Attack nearest airborne targets",  self.FlightMenuAttack, AI_ESCORT._FlightAttackNearestTarget, self, self.__Enum.ReportType.Air ):SetTag( "Attack" )
  local FlightMenuAttackNearbyGround = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Attack nearest ground targets",  self.FlightMenuAttack, AI_ESCORT._FlightAttackNearestTarget, self, self.__Enum.ReportType.Ground ):SetTag( "Attack" )
  
  for _, MenuTargets in pairs( self.Menu.Targets or {}) do
    MenuTargets.FlightReportTargetsScheduler = SCHEDULER:New( self, self._FlightReportTargetsScheduler, {}, MenuTargets.Interval, MenuTargets.Interval )
  end

  return self
end


function AI_ESCORT:SetEscortMenuTargets( EscortGroup )

  for _, MenuTargets in pairs( self.Menu.Targets or {} or {}) do
    if EscortGroup:IsAir() then
      local EscortGroupName = EscortGroup:GetName()
      --local EscortMenuReportTargets = MENU_GROUP:New( self.PlayerGroup, "Report targets", EscortGroup.EscortMenu )

      -- Report Targets
      EscortGroup.EscortMenuReportNearbyTargetsNow = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets", EscortGroup.EscortMenu, AI_ESCORT._ReportNearbyTargetsNow, self, EscortGroup, true )
      --EscortGroup.EscortMenuReportNearbyTargetsOn = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets on", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, true )
      --EscortGroup.EscortMenuReportNearbyTargetsOff = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Report targets off", EscortGroup.EscortMenuReportNearbyTargets, AI_ESCORT._SwitchReportNearbyTargets, self, EscortGroup, false )
    
      -- Attack Targets
      --local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu )
    
      EscortGroup.ReportTargetsScheduler = SCHEDULER:New( self, self._ReportTargetsScheduler, { EscortGroup }, 1, MenuTargets.Interval )
      EscortGroup.ResumeScheduler = SCHEDULER:New( self, self._ResumeScheduler, { EscortGroup }, 1, 60 )
    end
  end

  return self
end



--- Defines a menu slot to let the escort report their current detected targets with a specified time interval in seconds.
-- This menu will appear under **Report targets**.
-- Note that if a report targets menu is not specified, no targets will be detected by the escort, and the attack and assisted attack menus will not be displayed.
-- @param #AI_ESCORT self
-- @param DCS#Time Seconds Optional parameter that lets the escort report their current detected targets after specified time interval in seconds. The default time is 30 seconds.
-- @return #AI_ESCORT
function AI_ESCORT:MenuTargets( Seconds )
  self:F( { Seconds } )

  if not Seconds then
    Seconds = 30
  end

  self.Menu.Targets = self.Menu.Targets or {}
  self.Menu.Targets[#self.Menu.Targets+1] = {}
  self.Menu.Targets[#self.Menu.Targets].Interval = Seconds
  
  return self
end

--- Defines a menu slot to let the escort attack its detected targets using assisted attack from another escort joined also with the client.
-- This menu will appear under **Request assistance from**.
-- Note that this method needs to be preceded with the method MenuTargets.
-- @param #AI_ESCORT self
-- @return #AI_ESCORT
function AI_ESCORT:MenuAssistedAttack()
  self:F()

  self.EscortGroupSet:ForSomeGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      if not EscortGroup:IsAir() then
        -- Request assistance from other escorts.
        -- This is very useful to let f.e. an escorting ship attack a target detected by an escorting plane...
        self.EscortMenuTargetAssistance = MENU_GROUP:New( self.PlayerGroup, "Request assistance from", EscortGroup.EscortMenu )
      end
    end
  )

  return self
end

function AI_ESCORT:SetFlightMenuROE()

  for _, MenuROE in pairs( self.Menu.ROE or {}) do
    local FlightMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", self.FlightMenu )
  
    local FlightMenuROEHoldFire   = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire",          FlightMenuROE, AI_ESCORT._FlightROEHoldFire,   self,  "Holding weapons!" )
    local FlightMenuROEReturnFire = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire",        FlightMenuROE, AI_ESCORT._FlightROEReturnFire, self, "Returning fire!" )
    local FlightMenuROEOpenFire   = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire",          FlightMenuROE, AI_ESCORT._FlightROEOpenFire,   self,  "Open fire at designated targets!" )
    local FlightMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", FlightMenuROE, AI_ESCORT._FlightROEWeaponFree, self,  "Engaging all targets!" )
  end

  return self
end


function AI_ESCORT:SetEscortMenuROE( EscortGroup )

  for _, MenuROE in pairs( self.Menu.ROE or {}) do
    if EscortGroup:IsAir() then

      local EscortGroupName = EscortGroup:GetName()
      local EscortMenuROE = MENU_GROUP:New( self.PlayerGroup, "Rule Of Engagement", EscortGroup.EscortMenu )
      
      if EscortGroup:OptionROEHoldFirePossible() then
        local EscortMenuROEHoldFire         = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Hold fire",          EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEHoldFire, "Holding weapons!" )
      end
      if EscortGroup:OptionROEReturnFirePossible() then
        local EscortMenuROEReturnFire       = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Return fire",        EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEReturnFire, "Returning fire!" )
      end
      if EscortGroup:OptionROEOpenFirePossible() then
        EscortGroup.EscortMenuROEOpenFire   = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open Fire",          EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEOpenFire, "Opening fire on designated targets!!" )
      end
      if EscortGroup:OptionROEWeaponFreePossible() then
        EscortGroup.EscortMenuROEWeaponFree = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Engage all targets", EscortMenuROE, AI_ESCORT._ROE, self, EscortGroup, EscortGroup.OptionROEWeaponFree, "Opening fire on targets of opportunity!" )
      end
    end
  end

  return self
end


--- Defines a menu to let the escort set its rules of engagement.
-- All rules of engagement will appear under the menu **ROE**.
-- @param #AI_ESCORT self
-- @return #AI_ESCORT
function AI_ESCORT:MenuROE()
  self:F()

  self.Menu.ROE = self.Menu.ROE or {}
  self.Menu.ROE[#self.Menu.ROE+1] = {}

  return self
end


function AI_ESCORT:SetFlightMenuROT()

  for _, MenuROT in pairs( self.Menu.ROT or {}) do
    local FlightMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", self.FlightMenu )
  
    local FlightMenuROTNoReaction     = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death",             FlightMenuROT, AI_ESCORT._FlightROTNoReaction,     self, "Fighting until death!" )
    local FlightMenuROTPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", FlightMenuROT, AI_ESCORT._FlightROTPassiveDefense, self, "Defending using jammers, chaff and flares!" )
    local FlightMenuROTEvadeFire      = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire",                     FlightMenuROT, AI_ESCORT._FlightROTEvadeFire,      self, "Evading on enemy fire!" )
    local FlightMenuROTVertical       = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire",    FlightMenuROT, AI_ESCORT._FlightROTVertical,       self, "Evading on enemy fire with vertical manoeuvres!" )
  end

  return self
end


function AI_ESCORT:SetEscortMenuROT( EscortGroup )

  for _, MenuROT in pairs( self.Menu.ROT or {}) do
    if EscortGroup:IsAir() then

      local EscortGroupName = EscortGroup:GetName()
      local EscortMenuROT = MENU_GROUP:New( self.PlayerGroup, "Reaction On Threat", EscortGroup.EscortMenu )

      if not EscortGroup.EscortMenuEvasion then
        -- Reaction to Threats
        if EscortGroup:OptionROTNoReactionPossible() then
          local EscortMenuEvasionNoReaction     = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Fight until death",             EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTNoReaction, "Fighting until death!" )
        end
        if EscortGroup:OptionROTPassiveDefensePossible() then
          local EscortMenuEvasionPassiveDefense = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Use flares, chaff and jammers", EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTPassiveDefense, "Defending using jammers, chaff and flares!" )
        end
        if EscortGroup:OptionROTEvadeFirePossible() then
          local EscortMenuEvasionEvadeFire      = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Open fire",                     EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTEvadeFire, "Evading on enemy fire!" )
        end
        if EscortGroup:OptionROTVerticalPossible() then
          local EscortMenuOptionEvasionVertical = MENU_GROUP_COMMAND:New( self.PlayerGroup, "Avoid radar and evade fire",    EscortMenuROT, AI_ESCORT._ROT, self, EscortGroup, EscortGroup.OptionROTVertical, "Evading on enemy fire with vertical manoeuvres!" )
        end
      end
    end
  end

  return self
end



--- Defines a menu to let the escort set its evasion when under threat.
-- All rules of engagement will appear under the menu **Evasion**.
-- @param #AI_ESCORT self
-- @return #AI_ESCORT
function AI_ESCORT:MenuROT( MenuTextFormat )
  self:F( MenuTextFormat )

  self.Menu.ROT = self.Menu.ROT or {}
  self.Menu.ROT[#self.Menu.ROT+1] = {}

  return self
end

--- Defines a menu to let the escort resume its mission from a waypoint on its route.
-- All rules of engagement will appear under the menu **Resume mission from**.
-- @param #AI_ESCORT self
-- @return #AI_ESCORT
function AI_ESCORT:SetEscortMenuResumeMission( EscortGroup )
  self:F()

  if EscortGroup:IsAir() then
    local EscortGroupName = EscortGroup:GetName()
    EscortGroup.EscortMenuResumeMission = MENU_GROUP:New( self.PlayerGroup, "Resume from", EscortGroup.EscortMenu )
  end

  return self
end


-- @param #AI_ESCORT self
-- @param Wrapper.Group#GROUP OrbitGroup
-- @param Wrapper.Group#GROUP EscortGroup
-- @param #number OrbitHeight
-- @param #number OrbitSeconds
function AI_ESCORT:_HoldPosition( OrbitGroup, EscortGroup, OrbitHeight, OrbitSeconds )

  local EscortUnit = self.PlayerUnit

  local OrbitUnit = OrbitGroup:GetUnit(1) -- Wrapper.Unit#UNIT
  
  self:SetFlightModeMission( EscortGroup )

  local PointFrom = {}
  local GroupVec3 = EscortGroup:GetUnit(1):GetVec3()
  PointFrom = {}
  PointFrom.x = GroupVec3.x
  PointFrom.y = GroupVec3.z
  PointFrom.speed = 250
  PointFrom.type = AI.Task.WaypointType.TURNING_POINT
  PointFrom.alt = GroupVec3.y
  PointFrom.alt_type = AI.Task.AltitudeType.BARO

  local OrbitPoint = OrbitUnit:GetVec2()
  local PointTo = {}
  PointTo.x = OrbitPoint.x
  PointTo.y = OrbitPoint.y
  PointTo.speed = 250
  PointTo.type = AI.Task.WaypointType.TURNING_POINT
  PointTo.alt = OrbitHeight
  PointTo.alt_type = AI.Task.AltitudeType.BARO
  PointTo.task = EscortGroup:TaskOrbitCircleAtVec2( OrbitPoint, OrbitHeight, 0 )

  local Points = { PointFrom, PointTo }

  EscortGroup:OptionROEHoldFire()
  EscortGroup:OptionROTPassiveDefense()

  EscortGroup:SetTask( EscortGroup:TaskRoute( Points ), 1 )
  EscortGroup:MessageTypeToGroup( "Orbiting at current location.", MESSAGE.Type.Information, EscortUnit:GetGroup() )

end


-- @param #AI_ESCORT self
-- @param Wrapper.Group#GROUP OrbitGroup
-- @param #number OrbitHeight
-- @param #number OrbitSeconds
function AI_ESCORT:_FlightHoldPosition( OrbitGroup, OrbitHeight, OrbitSeconds )

  local EscortUnit = self.PlayerUnit

  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup, OrbitGroup )
      if EscortGroup:IsAir() then
        if OrbitGroup == nil then
          OrbitGroup = EscortGroup
        end
        self:_HoldPosition( OrbitGroup, EscortGroup, OrbitHeight, OrbitSeconds )
      end
    end, OrbitGroup
  )

end  



function AI_ESCORT:_JoinUp( EscortGroup )

  local EscortUnit = self.PlayerUnit

  self:SetFlightModeFormation( EscortGroup )

  EscortGroup:MessageTypeToGroup( "Joining up!", MESSAGE.Type.Information, EscortUnit:GetGroup() )
end


function AI_ESCORT:_FlightJoinUp()

  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      if EscortGroup:IsAir() then
        self:_JoinUp( EscortGroup )
      end
    end
  )

end


--- Lets the escort to join in a trail formation.
-- @param #AI_ESCORT self
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @return #AI_ESCORT
function AI_ESCORT:_EscortFormationTrail( EscortGroup, XStart, XSpace, YStart )

  self:FormationTrail( XStart, XSpace, YStart )

end


function AI_ESCORT:_FlightFormationTrail( XStart, XSpace, YStart )

  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      if EscortGroup:IsAir() then
        self:_EscortFormationTrail( EscortGroup, XStart, XSpace, YStart )
      end
    end
  )

end

--- Lets the escort to join in a stacked formation.
-- @param #AI_ESCORT self
-- @param #number XStart The start position on the X-axis in meters for the first group.
-- @param #number XSpace The space between groups on the X-axis in meters for each sequent group.
-- @param #number YStart The start position on the Y-axis in meters for the first group.
-- @param #number YSpace The space between groups on the Y-axis in meters for each sequent group.
-- @return #AI_ESCORT
function AI_ESCORT:_EscortFormationStack( EscortGroup, XStart, XSpace, YStart, YSpace )

  self:FormationStack( XStart, XSpace, YStart, YSpace )

end


function AI_ESCORT:_FlightFormationStack( XStart, XSpace, YStart, YSpace )

  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      if EscortGroup:IsAir() then
        self:_EscortFormationStack( EscortGroup, XStart, XSpace, YStart, YSpace )
      end
    end
  )

end


function AI_ESCORT:_Flare( EscortGroup, Color, Message )

  local EscortUnit = self.PlayerUnit

  EscortGroup:GetUnit(1):Flare( Color )
  EscortGroup:MessageTypeToGroup( Message, MESSAGE.Type.Information, EscortUnit:GetGroup() )
end


function AI_ESCORT:_FlightFlare( Color, Message )

  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      if EscortGroup:IsAir() then
        self:_Flare( EscortGroup, Color, Message )
      end
    end
  )
  
end



function AI_ESCORT:_Smoke( EscortGroup, Color, Message )

  local EscortUnit = self.PlayerUnit

  EscortGroup:GetUnit(1):Smoke( Color )
  EscortGroup:MessageTypeToGroup( Message, MESSAGE.Type.Information, EscortUnit:GetGroup() )
end

function AI_ESCORT:_FlightSmoke( Color, Message )

  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      if EscortGroup:IsAir() then
        self:_Smoke( EscortGroup, Color, Message )
      end
    end
  )

end


function AI_ESCORT:_ReportNearbyTargetsNow( EscortGroup )

  local EscortUnit = self.PlayerUnit

  self:_ReportTargetsScheduler( EscortGroup )

end


function AI_ESCORT:_FlightReportNearbyTargetsNow()

  self:_FlightReportTargetsScheduler()
  
end



function AI_ESCORT:_FlightSwitchReportNearbyTargets( ReportTargets )

  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      if EscortGroup:IsAir() then
        self:_EscortSwitchReportNearbyTargets( EscortGroup, ReportTargets )
      end
    end
  )

end

function AI_ESCORT:SetFlightReportType( ReportType )

  self.FlightReportType = ReportType

end

function AI_ESCORT:GetFlightReportType()

  return self.FlightReportType

end

function AI_ESCORT:_FlightSwitchReportTypeAll()

  self:SetFlightReportType( self.__Enum.ReportType.All )
  self:SetFlightMenuReportType()

  local EscortGroup = self.EscortGroupSet:GetFirst()
  EscortGroup:MessageTypeToGroup( "Reporting all targets.", MESSAGE.Type.Information, self.PlayerGroup )

end

function AI_ESCORT:_FlightSwitchReportTypeAirborne()

  self:SetFlightReportType( self.__Enum.ReportType.Airborne )
  self:SetFlightMenuReportType()

  local EscortGroup = self.EscortGroupSet:GetFirst()
  EscortGroup:MessageTypeToGroup( "Reporting airborne targets.", MESSAGE.Type.Information, self.PlayerGroup )

end

function AI_ESCORT:_FlightSwitchReportTypeGroundRadar()

  self:SetFlightReportType( self.__Enum.ReportType.Ground )
  self:SetFlightMenuReportType()

  local EscortGroup = self.EscortGroupSet:GetFirst()
  EscortGroup:MessageTypeToGroup( "Reporting ground radar targets.", MESSAGE.Type.Information, self.PlayerGroup )

end

function AI_ESCORT:_FlightSwitchReportTypeGround()

  self:SetFlightReportType( self.__Enum.ReportType.Ground )
  self:SetFlightMenuReportType()

  local EscortGroup = self.EscortGroupSet:GetFirst()
  EscortGroup:MessageTypeToGroup( "Reporting ground targets.", MESSAGE.Type.Information, self.PlayerGroup )

end


function AI_ESCORT:_ScanTargets( ScanDuration )

  local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP
  local EscortUnit = self.PlayerUnit

  self.FollowScheduler:Stop( self.FollowSchedule )

  if EscortGroup:IsHelicopter() then
    EscortGroup:PushTask(
      EscortGroup:TaskControlled(
        EscortGroup:TaskOrbitCircle( 200, 20 ),
        EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil )
      ), 1 )
  elseif EscortGroup:IsAirPlane() then
    EscortGroup:PushTask(
      EscortGroup:TaskControlled(
        EscortGroup:TaskOrbitCircle( 1000, 500 ),
        EscortGroup:TaskCondition( nil, nil, nil, nil, ScanDuration, nil )
      ), 1 )
  end

  EscortGroup:MessageToClient( "Scanning targets for " .. ScanDuration .. " seconds.", ScanDuration, EscortUnit )

  if self.EscortMode == AI_ESCORT.MODE.FOLLOW then
    self.FollowScheduler:Start( self.FollowSchedule )
  end

end

-- @param Wrapper.Group#GROUP EscortGroup
-- @param #AI_ESCORT self
function AI_ESCORT.___Resume( EscortGroup, self )

  self:F( { self=self } )

  local PlayerGroup = self.PlayerGroup
  
  EscortGroup:OptionROEHoldFire()
  EscortGroup:OptionROTVertical()
  
  EscortGroup:SetState( EscortGroup, "Mode", EscortGroup:GetState( EscortGroup, "PreviousMode" ) )
  
  if EscortGroup:GetState( EscortGroup, "Mode" ) == self.__Enum.Mode.Mission then
    EscortGroup:MessageTypeToGroup( "Resuming route.", MESSAGE.Type.Information, PlayerGroup )
  else
    EscortGroup:MessageTypeToGroup( "Rejoining formation.", MESSAGE.Type.Information, PlayerGroup )
  end

end


-- @param #AI_ESCORT self
-- @param Wrapper.Group#GROUP EscortGroup
-- @param #number WayPoint
function AI_ESCORT:_ResumeMission( EscortGroup, WayPoint )

  --self.FollowScheduler:Stop( self.FollowSchedule )
  
  self:SetFlightModeMission( EscortGroup )

  local WayPoints = EscortGroup.MissionRoute
  self:T( WayPoint, WayPoints )

  for WayPointIgnore = 1, WayPoint do
    table.remove( WayPoints, 1 )
  end

  EscortGroup:SetTask( EscortGroup:TaskRoute( WayPoints ), 1 )
  
  EscortGroup:MessageTypeToGroup( "Resuming mission from waypoint ", MESSAGE.Type.Information, self.PlayerGroup )
end


-- @param #AI_ESCORT self
-- @param Wrapper.Group#GROUP EscortGroup The escort group that will attack the detected item.
-- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem
function AI_ESCORT:_AttackTarget( EscortGroup, DetectedItem )

  self:F( EscortGroup )
  
  self:SetFlightModeAttack( EscortGroup )

  if EscortGroup:IsAir() then
    EscortGroup:OptionROEOpenFire()
    EscortGroup:OptionROTVertical()
    EscortGroup:SetState( EscortGroup, "Escort", self )

    local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem )
    
    local Tasks = {}
    local AttackUnitTasks = {}

    DetectedSet:ForEachUnit(
      -- @param Wrapper.Unit#UNIT DetectedUnit
      function( DetectedUnit, Tasks )
        if DetectedUnit:IsAlive() then
          AttackUnitTasks[#AttackUnitTasks+1] = EscortGroup:TaskAttackUnit( DetectedUnit )
        end
      end, Tasks
    )    

    Tasks[#Tasks+1] = EscortGroup:TaskCombo( AttackUnitTasks )
    Tasks[#Tasks+1] = EscortGroup:TaskFunction( "AI_ESCORT.___Resume", self )
    
    EscortGroup:PushTask( 
      EscortGroup:TaskCombo(
        Tasks
      ), 1
    )
    
  else
  
    local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem )
    
    local Tasks = {}

    DetectedSet:ForEachUnit(
      -- @param Wrapper.Unit#UNIT DetectedUnit
      function( DetectedUnit, Tasks )
        if DetectedUnit:IsAlive() then
          Tasks[#Tasks+1] = EscortGroup:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 )
        end
      end, Tasks
    )    

    EscortGroup:PushTask( 
      EscortGroup:TaskCombo(
        Tasks
      ), 1
    )

  end
  
  local DetectedTargetsReport = REPORT:New( "Engaging target:\n" )
  local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, self.PlayerGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) )
  local ReportSummary = DetectedItemReportSummary:Text(", ")
  DetectedTargetsReport:AddIndent( ReportSummary, "-" )

  EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text(), MESSAGE.Type.Information, self.PlayerGroup )
end


function AI_ESCORT:_FlightAttackTarget( DetectedItem )

  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup, DetectedItem )
      if EscortGroup:IsAir() then
        self:_AttackTarget( EscortGroup, DetectedItem )
      end
    end, DetectedItem
  )

end


function AI_ESCORT:_FlightAttackNearestTarget( TargetType )

  self.Detection:Detect()
  self:_FlightReportTargetsScheduler()
  
  local EscortGroup = self.EscortGroupSet:GetFirst()
  local AttackDetectedItem = nil
  local DetectedItems = self.Detection:GetDetectedItems()

  for DetectedItemIndex, DetectedItem in UTILS.spairs( DetectedItems, function( t, a, b ) return self:Distance( self.PlayerUnit, t[a] ) < self:Distance( self.PlayerUnit, t[b] ) end  ) do
  
    local DetectedItemSet = self.Detection:GetDetectedItemSet( DetectedItem )
    
    local HasGround = DetectedItemSet:HasGroundUnits() > 0
    local HasAir = DetectedItemSet:HasAirUnits() > 0
    
    local FlightReportType = self:GetFlightReportType()
        
    if ( TargetType and TargetType == self.__Enum.ReportType.Ground and HasGround ) or
       ( TargetType and TargetType == self.__Enum.ReportType.Air and HasAir ) or
       ( TargetType ==  nil ) then
      AttackDetectedItem = DetectedItem
      break
    end
  end
  
  if AttackDetectedItem then
    self:_FlightAttackTarget( AttackDetectedItem )
  else
    EscortGroup:MessageTypeToGroup( "Nothing to attack!", MESSAGE.Type.Information, self.PlayerGroup )
  end

end


--- 
-- @param #AI_ESCORT self
-- @param Wrapper.Group#GROUP EscortGroup The escort group that will attack the detected item.
-- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem
function AI_ESCORT:_AssistTarget( EscortGroup, DetectedItem )

  local EscortUnit = self.PlayerUnit

  local DetectedSet = self.Detection:GetDetectedItemSet( DetectedItem )
  
  local Tasks = {}

  DetectedSet:ForEachUnit(
    -- @param Wrapper.Unit#UNIT DetectedUnit
    function( DetectedUnit, Tasks )
      if DetectedUnit:IsAlive() then
        Tasks[#Tasks+1] = EscortGroup:TaskFireAtPoint( DetectedUnit:GetVec2(), 50 )
      end
    end, Tasks
  )    

  EscortGroup:SetTask( 
    EscortGroup:TaskCombo(
      Tasks
    ), 1
  )


  EscortGroup:MessageTypeToGroup( "Assisting attack!", MESSAGE.Type.Information, EscortUnit:GetGroup() )

end

function AI_ESCORT:_ROE( EscortGroup, EscortROEFunction, EscortROEMessage )
    pcall( function() EscortROEFunction( EscortGroup ) end )
    EscortGroup:MessageTypeToGroup( EscortROEMessage, MESSAGE.Type.Information, self.PlayerGroup )
end


function AI_ESCORT:_FlightROEHoldFire( EscortROEMessage )
  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      self:_ROE( EscortGroup, EscortGroup.OptionROEHoldFire, EscortROEMessage )
    end
  )
end

function AI_ESCORT:_FlightROEOpenFire( EscortROEMessage )
  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      self:_ROE( EscortGroup, EscortGroup.OptionROEOpenFire, EscortROEMessage )
    end
  )
end

function AI_ESCORT:_FlightROEReturnFire( EscortROEMessage )
  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      self:_ROE( EscortGroup, EscortGroup.OptionROEReturnFire, EscortROEMessage )
    end
  )
end

function AI_ESCORT:_FlightROEWeaponFree( EscortROEMessage )
  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      self:_ROE( EscortGroup, EscortGroup.OptionROEWeaponFree, EscortROEMessage )
    end
  )
end


function AI_ESCORT:_ROT( EscortGroup, EscortROTFunction, EscortROTMessage )
  pcall( function() EscortROTFunction( EscortGroup ) end )
  EscortGroup:MessageTypeToGroup( EscortROTMessage, MESSAGE.Type.Information, self.PlayerGroup )
end


function AI_ESCORT:_FlightROTNoReaction( EscortROTMessage )
  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      self:_ROT( EscortGroup, EscortGroup.OptionROTNoReaction, EscortROTMessage )
    end
  )
end

function AI_ESCORT:_FlightROTPassiveDefense( EscortROTMessage )
  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      self:_ROT( EscortGroup, EscortGroup.OptionROTPassiveDefense, EscortROTMessage )
    end
  )
end

function AI_ESCORT:_FlightROTEvadeFire( EscortROTMessage )
  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      self:_ROT( EscortGroup, EscortGroup.OptionROTEvadeFire, EscortROTMessage )
    end
  )
end

function AI_ESCORT:_FlightROTVertical( EscortROTMessage )
  self.EscortGroupSet:ForEachGroupAlive(
    -- @param Wrapper.Group#GROUP EscortGroup
    function( EscortGroup )
      self:_ROT( EscortGroup, EscortGroup.OptionROTVertical, EscortROTMessage )
    end
  )
end

--- Registers the waypoints
-- @param #AI_ESCORT self
-- @return #table
function AI_ESCORT:RegisterRoute()
  self:F()

  local EscortGroup = self.EscortGroup -- Wrapper.Group#GROUP

  local TaskPoints = EscortGroup:GetTaskRoute()

  self:T( TaskPoints )

  return TaskPoints
end

--- Resume Scheduler.
-- @param #AI_ESCORT self
-- @param Wrapper.Group#GROUP EscortGroup
function AI_ESCORT:_ResumeScheduler( EscortGroup )
  self:F( EscortGroup:GetName() )

  if EscortGroup:IsAlive() and self.PlayerUnit:IsAlive() then


    local EscortGroupName = EscortGroup:GetCallsign() 

    if EscortGroup.EscortMenuResumeMission then
      EscortGroup.EscortMenuResumeMission:RemoveSubMenus()

      local TaskPoints = EscortGroup.MissionRoute
      
      for WayPointID, WayPoint in pairs( TaskPoints ) do
        local EscortVec3 = EscortGroup:GetVec3()
        local Distance = ( ( WayPoint.x - EscortVec3.x )^2 +
          ( WayPoint.y - EscortVec3.z )^2
          ) ^ 0.5 / 1000
        MENU_GROUP_COMMAND:New( self.PlayerGroup, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", EscortGroup.EscortMenuResumeMission, AI_ESCORT._ResumeMission, self, EscortGroup, WayPointID )
      end
    end
  end
end


--- Measure distance between coordinate player and coordinate detected item.
-- @param #AI_ESCORT self
function AI_ESCORT:Distance( PlayerUnit, DetectedItem )

  local DetectedCoordinate = self.Detection:GetDetectedItemCoordinate( DetectedItem )
  local PlayerCoordinate = PlayerUnit:GetCoordinate()
  
  return DetectedCoordinate:Get3DDistance( PlayerCoordinate )

end

--- Report Targets Scheduler.
-- @param #AI_ESCORT self
-- @param Wrapper.Group#GROUP EscortGroup
function AI_ESCORT:_ReportTargetsScheduler( EscortGroup, Report )
  self:F( EscortGroup:GetName() )

  if EscortGroup:IsAlive() and self.PlayerUnit:IsAlive() then

      local EscortGroupName = EscortGroup:GetCallsign() 
 
      local DetectedTargetsReport = REPORT:New( "Reporting targets:\n" ) -- A new report to display the detected targets as a message to the player.
    

      if EscortGroup.EscortMenuTargetAssistance then
        EscortGroup.EscortMenuTargetAssistance:RemoveSubMenus()
      end

      local DetectedItems = self.Detection:GetDetectedItems()

      local ClientEscortTargets = self.Detection

      local TimeUpdate = timer.getTime()
      
      local EscortMenuAttackTargets = MENU_GROUP:New( self.PlayerGroup, "Attack targets", EscortGroup.EscortMenu )
      
      local DetectedTargets = false
      
      for DetectedItemIndex, DetectedItem in UTILS.spairs( DetectedItems, function( t, a, b ) return self:Distance( self.PlayerUnit, t[a] ) < self:Distance( self.PlayerUnit, t[b] ) end  ) do
      --for DetectedItemIndex, DetectedItem in pairs( DetectedItems ) do

        local DetectedItemSet = self.Detection:GetDetectedItemSet( DetectedItem )
        
        local HasGround = DetectedItemSet:HasGroundUnits() > 0
        local HasGroundRadar = HasGround and DetectedItemSet:HasRadar() > 0
        local HasAir = DetectedItemSet:HasAirUnits() > 0
        
        local FlightReportType = self:GetFlightReportType()
          

        if ( FlightReportType == self.__Enum.ReportType.All ) or
           ( FlightReportType == self.__Enum.ReportType.Airborne and HasAir ) or
           ( FlightReportType == self.__Enum.ReportType.Ground and HasGround ) or
           ( FlightReportType == self.__Enum.ReportType.GroundRadar and HasGroundRadar ) then
      
          DetectedTargets = true
  
          local DetectedMenu = self.Detection:DetectedItemReportMenu( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) ):Text("\n")
  
          local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, EscortGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) )
          local ReportSummary = DetectedItemReportSummary:Text(", ")
          DetectedTargetsReport:AddIndent( ReportSummary, "-" )
          
          if EscortGroup:IsAir() then
  
            MENU_GROUP_COMMAND:New( self.PlayerGroup,
              DetectedMenu,
              EscortMenuAttackTargets,
              AI_ESCORT._AttackTarget,
              self,
              EscortGroup,
              DetectedItem
            ):SetTag( "Escort" ):SetTime( TimeUpdate )
          else
            if self.EscortMenuTargetAssistance then
              local MenuTargetAssistance = MENU_GROUP:New( self.PlayerGroup, EscortGroupName, EscortGroup.EscortMenuTargetAssistance )
              MENU_GROUP_COMMAND:New( self.PlayerGroup,
                DetectedMenu,
                MenuTargetAssistance,
                AI_ESCORT._AssistTarget,
                self,
                EscortGroup,
                DetectedItem
              )
            end
          end
        end
      end

      EscortMenuAttackTargets:RemoveSubMenus( TimeUpdate, "Escort" )

      if Report then
        if DetectedTargets then
          EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.PlayerGroup )
        else
          EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.PlayerGroup )
        end
      end

      return true
  end
  
  return false
end

--- Report Targets Scheduler for the flight. The report is generated from the perspective of the player plane, and is reported by the first plane in the formation set.
-- @param #AI_ESCORT self
-- @param Wrapper.Group#GROUP EscortGroup
function AI_ESCORT:_FlightReportTargetsScheduler()

  self:F("FlightReportTargetScheduler")
  
  local EscortGroup = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP
  
  local DetectedTargetsReport = REPORT:New( "Reporting your targets:\n" ) -- A new report to display the detected targets as a message to the player.

  if EscortGroup and ( self.PlayerUnit:IsAlive() and EscortGroup:IsAlive() ) then

    local TimeUpdate = timer.getTime()

    local DetectedItems = self.Detection:GetDetectedItems()

    local DetectedTargets = false

    local ClientEscortTargets = self.Detection

    for DetectedItemIndex, DetectedItem in UTILS.spairs( DetectedItems, function( t, a, b ) return self:Distance( self.PlayerUnit, t[a] ) < self:Distance( self.PlayerUnit, t[b] ) end  ) do
    
      self:F("FlightReportTargetScheduler Targets")

      local DetectedItemSet = self.Detection:GetDetectedItemSet( DetectedItem )
      
      local HasGround = DetectedItemSet:HasGroundUnits() > 0
      local HasGroundRadar = HasGround and DetectedItemSet:HasRadar() > 0
      local HasAir = DetectedItemSet:HasAirUnits() > 0
      
      local FlightReportType = self:GetFlightReportType()
          

      if ( FlightReportType == self.__Enum.ReportType.All ) or
         ( FlightReportType == self.__Enum.ReportType.Airborne and HasAir ) or
         ( FlightReportType == self.__Enum.ReportType.Ground and HasGround ) or
         ( FlightReportType == self.__Enum.ReportType.GroundRadar and HasGroundRadar ) then
         

        DetectedTargets = true -- There are detected targets, when the content of the for loop is executed. We use it to display a message.
        
        local DetectedItemReportMenu = self.Detection:DetectedItemReportMenu( DetectedItem, self.PlayerGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) )
        local ReportMenuText = DetectedItemReportMenu:Text(", ")
        
        MENU_GROUP_COMMAND:New( self.PlayerGroup,
          ReportMenuText,
          self.FlightMenuAttack,
          AI_ESCORT._FlightAttackTarget,
          self,
          DetectedItem
        ):SetTag( "Flight" ):SetTime( TimeUpdate )
  
        local DetectedItemReportSummary = self.Detection:DetectedItemReportSummary( DetectedItem, self.PlayerGroup, _DATABASE:GetPlayerSettings( self.PlayerUnit:GetPlayerName() ) )
        local ReportSummary = DetectedItemReportSummary:Text(", ")
        DetectedTargetsReport:AddIndent( ReportSummary, "-" )
      end
    end

    self.FlightMenuAttack:RemoveSubMenus( TimeUpdate, "Flight" )

    if DetectedTargets then
      EscortGroup:MessageTypeToGroup( DetectedTargetsReport:Text( "\n" ), MESSAGE.Type.Information, self.PlayerGroup )
--    else
--      EscortGroup:MessageTypeToGroup( "No targets detected.", MESSAGE.Type.Information, self.PlayerGroup )
    end

    return true
  end
  
  return false
end
--- **AI** - Taking the lead of AI escorting your flight or of other AI, upon request using the menu.
-- 
-- ===
-- 
-- ## Features:
-- 
--   * Escort navigation commands.
--   * Escort hold at position commands.
--   * Escorts reporting detected targets.
--   * Escorts scanning targets in advance.
--   * Escorts attacking specific targets.
--   * Request assistance from other groups for attack.
--   * Manage rule of engagement of escorts.
--   * Manage the allowed evasion techniques of escorts.
--   * Make escort to execute a defined mission or path.
--   * Escort tactical situation reporting.
-- 
-- ===
-- 
-- ## Missions:
-- 
-- [ESC - Escorting](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_Escort)
-- 
-- ===
-- 
-- Allows you to interact with escorting AI on your flight and take the lead.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- Each escorting group can be commanded with a complete set of radio commands (radio menu in your flight, and then F10).
--
-- The radio commands will vary according the category of the group. The richest set of commands are with helicopters and airPlanes.
-- Ships and Ground troops will have a more limited set, but they can provide support through the bombing of targets designated by the other escorts.
-- 
-- Escorts detect targets using a built-in detection mechanism. The detected targets are reported at a specified time interval.
-- Once targets are reported, each escort has these targets as menu options to command the attack of these targets.
-- Targets are by default grouped per area of 5000 meters, but the kind of detection and the grouping range can be altered.
-- 
-- Different formations can be selected in the Flight menu: Trail, Stack, Left Line, Right Line, Left Wing, Right Wing, Central Wing and Boxed formations are available.
-- The Flight menu also allows for a mass attack, where all of the escorts are commanded to attack a target.
-- 
-- Escorts can emit flares to reports their location. They can be commanded to hold at a location, which can be their current or the leader location.
-- In this way, you can spread out the escorts over the battle field before a coordinated attack.
-- 
-- But basically, the escort class provides 4 modes of operation, and depending on the mode, you are either leading the flight, or following the flight.
-- 
-- ## Leading the flight
-- 
-- When leading the flight, you are expected to guide the escorts towards the target areas, 
-- and carefully coordinate the attack based on the threat levels reported, and the available weapons
-- carried by the escorts. Ground ships or ground troops can execute A-assisted attacks, when they have long-range ground precision weapons for attack.
-- 
-- ## Following the flight
-- 
-- Escorts can be commanded to execute a specific mission path. In this mode, the escorts are in the lead.
-- You as a player, are following the escorts, and are commanding them to progress the mission while
-- ensuring that the escorts survive. You are joining the escorts in the battlefield. They will detect and report targets
-- and you will ensure that the attacks are well coordinated, assigning the correct escort type for the detected target
-- type. Once the attack is finished, the escort will resume the mission it was assigned.
-- In other words, you can use the escorts for reconnaissance, and for guiding the attack.
-- Imagine you as a mi-8 pilot, assigned to pickup cargo. Two ka-50s are guiding the way, and you are
-- following. You are in control. The ka-50s detect targets, report them, and you command how the attack
-- will commence and from where. You can control where the escorts are holding position and which targets
-- are attacked first. You are in control how the ka-50s will follow their mission path.
-- 
-- Escorts can act as part of a AI A2G dispatcher offensive. In this way, You was a player are in control.
-- The mission is defined by the A2G dispatcher, and you are responsible to join the flight and ensure that the
-- attack is well coordinated.
-- 
-- It is with great proud that I present you this class, and I hope you will enjoy the functionality and the dynamism
-- it brings in your DCS world simulations.
-- 
-- # RADIO MENUs that can be created:
-- 
-- Find a summary below of the current available commands:
--
-- ## Navigation ...:
-- 
-- Escort group navigation functions:
--
--   * **"Join-Up":** The escort group fill follow you in the assigned formation.
--   * **"Flare":** Provides menu commands to let the escort group shoot a flare in the air in a color.
--   * **"Smoke":** Provides menu commands to let the escort group smoke the air in a color. Note that smoking is only available for ground and naval troops.
--
-- ## Hold position ...:
-- 
-- Escort group navigation functions:
--
--   * **"At current location":** The escort group will hover above the ground at the position they were. The altitude can be specified as a parameter.
--   * **"At my location":** The escort group will hover or orbit at the position where you are. The escort will fly to your location and hold position. The altitude can be specified as a parameter.
--
-- ## Report targets ...:
-- 
-- Report targets will make the escort group to report any target that it identifies within detection range. Any detected target can be attacked using the "Attack Targets" menu function. (see below).
--
--   * **"Report now":** Will report the current detected targets.
--   * **"Report targets on":** Will make the escorts to report the detected targets and will fill the "Attack Targets" menu list.
--   * **"Report targets off":** Will stop detecting targets.
--
-- ## Attack targets ...:
-- 
-- This menu item will list all detected targets within a 15km range. Depending on the level of detection (known/unknown) and visuality, the targets type will also be listed.
-- This menu will be available in Flight menu or in each Escort menu.
--
-- ## Scan targets ...:
-- 
-- Menu items to pop-up the escort group for target scanning. After scanning, the escort group will resume with the mission or rejoin formation.
--
--   * **"Scan targets 30 seconds":** Scan 30 seconds for targets.
--   * **"Scan targets 60 seconds":** Scan 60 seconds for targets.
--
-- ## Request assistance from ...:
-- 
-- This menu item will list all detected targets within a 15km range, similar as with the menu item **Attack Targets**.
-- This menu item allows to request attack support from other ground based escorts supporting the current escort.
-- eg. the function allows a player to request support from the Ship escort to attack a target identified by the Plane escort with its Tomahawk missiles.
-- eg. the function allows a player to request support from other Planes escorting to bomb the unit with illumination missiles or bombs, so that the main plane escort can attack the area.
--
-- ## ROE ...:
-- 
-- Sets the Rules of Engagement (ROE) of the escort group when in flight.
--
--   * **"Hold Fire":** The escort group will hold fire.
--   * **"Return Fire":** The escort group will return fire.
--   * **"Open Fire":** The escort group will open fire on designated targets.
--   * **"Weapon Free":** The escort group will engage with any target.
--
-- ## Evasion ...:
-- 
-- Will define the evasion techniques that the escort group will perform during flight or combat.
--
--   * **"Fight until death":** The escort group will have no reaction to threats.
--   * **"Use flares, chaff and jammers":** The escort group will use passive defense using flares and jammers. No evasive manoeuvres are executed.
--   * **"Evade enemy fire":** The rescort group will evade enemy fire before firing.
--   * **"Go below radar and evade fire":** The escort group will perform evasive vertical manoeuvres.
--
-- ## Resume Mission ...:
-- 
-- Escort groups can have their own mission. This menu item will allow the escort group to resume their Mission from a given waypoint.
-- Note that this is really fantastic, as you now have the dynamic of taking control of the escort groups, and allowing them to resume their path or mission.
  --
  -- # Developer Note
  -- 
  -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
  -- Therefore, this class is considered to be deprecated
  --
-- ===
-- 
-- ### Authors: **FlightControl** 
-- 
-- ===
--
-- @module AI.AI_Escort_Request
-- @image Escorting.JPG



-- @type AI_ESCORT_REQUEST
-- @extends AI.AI_Escort#AI_ESCORT

--- AI_ESCORT_REQUEST class
-- 
-- # AI_ESCORT_REQUEST construction methods.
-- 
-- Create a new AI_ESCORT_REQUEST object with the @{#AI_ESCORT_REQUEST.New} method:
--
--  * @{#AI_ESCORT_REQUEST.New}: Creates a new AI_ESCORT_REQUEST object from a @{Wrapper.Group#GROUP} for a @{Wrapper.Client#CLIENT}, with an optional briefing text.
--
-- @usage
-- -- Declare a new EscortPlanes object as follows:
-- 
-- -- First find the GROUP object and the CLIENT object.
-- local EscortUnit = CLIENT:FindByName( "Unit Name" ) -- The Unit Name is the name of the unit flagged with the skill Client in the mission editor.
-- local EscortGroup = GROUP:FindByName( "Group Name" ) -- The Group Name is the name of the group that will escort the Escort Client.
-- 
-- -- Now use these 2 objects to construct the new EscortPlanes object.
-- EscortPlanes = AI_ESCORT_REQUEST:New( EscortUnit, EscortGroup, "Desert", "Welcome to the mission. You are escorted by a plane with code name 'Desert', which can be instructed through the F10 radio menu." )
--
-- @field #AI_ESCORT_REQUEST
AI_ESCORT_REQUEST = {
  ClassName = "AI_ESCORT_REQUEST",
}

--- AI_ESCORT_REQUEST.Mode class
-- @type AI_ESCORT_REQUEST.MODE
-- @field #number FOLLOW
-- @field #number MISSION

--- MENUPARAM type
-- @type MENUPARAM
-- @field #AI_ESCORT_REQUEST ParamSelf
-- @field #Distance ParamDistance
-- @field #function ParamFunction
-- @field #string ParamMessage

--- AI_ESCORT_REQUEST class constructor for an AI group
-- @param #AI_ESCORT_REQUEST self
-- @param Wrapper.Client#CLIENT EscortUnit The client escorted by the EscortGroup.
-- @param Core.Spawn#SPAWN EscortSpawn The spawn object of AI, escorting the EscortUnit.
-- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where escorts will be spawned once requested.
-- @param #string EscortName Name of the escort.
-- @param #string EscortBriefing A text showing the AI_ESCORT_REQUEST briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown.
-- @return #AI_ESCORT_REQUEST
-- @usage
-- EscortSpawn = SPAWN:NewWithAlias( "Red A2G Escort Template", "Red A2G Escort AI" ):InitLimit( 10, 10 )
-- EscortSpawn:ParkAtAirbase( AIRBASE:FindByName( AIRBASE.Caucasus.Sochi_Adler ), AIRBASE.TerminalType.OpenBig )
--
-- local EscortUnit = UNIT:FindByName( "Red A2G Pilot" )
--
-- Escort = AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, AIRBASE:FindByName(AIRBASE.Caucasus.Sochi_Adler), "A2G", "Briefing" )
-- Escort:FormationTrail( 50, 100, 100 )
-- Escort:Menus()
-- Escort:__Start( 5 )
function AI_ESCORT_REQUEST:New( EscortUnit, EscortSpawn, EscortAirbase, EscortName, EscortBriefing )
  
  local EscortGroupSet = SET_GROUP:New():FilterDeads():FilterCrashes()
  local self = BASE:Inherit( self, AI_ESCORT:New( EscortUnit, EscortGroupSet, EscortName, EscortBriefing ) ) -- #AI_ESCORT_REQUEST

  self.EscortGroupSet = EscortGroupSet
  self.EscortSpawn = EscortSpawn
  self.EscortAirbase = EscortAirbase

  self.LeaderGroup = self.PlayerUnit:GetGroup()

  self.Detection = DETECTION_AREAS:New( self.EscortGroupSet, 5000 )
  self.Detection:__Start( 30 )
  
  self.SpawnMode = self.__Enum.Mode.Mission
  
  return self
end

-- @param #AI_ESCORT_REQUEST self
function AI_ESCORT_REQUEST:SpawnEscort()

  local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Hot )

  self:ScheduleOnce( 0.1,
    function( EscortGroup )

      EscortGroup:OptionROTVertical()
      EscortGroup:OptionROEHoldFire()
  
      self.EscortGroupSet:AddGroup( EscortGroup )

      local LeaderEscort = self.EscortGroupSet:GetFirst() -- Wrapper.Group#GROUP
      local Report = REPORT:New()
      Report:Add( "Joining Up " .. self.EscortGroupSet:GetUnitTypeNames():Text( ", " ) .. " from " .. LeaderEscort:GetCoordinate():ToString( self.EscortUnit ) )
      LeaderEscort:MessageTypeToGroup( Report:Text(),  MESSAGE.Type.Information, self.PlayerUnit )

      self:SetFlightModeFormation( EscortGroup )
      self:FormationTrail()

      self:_InitFlightMenus()
      self:_InitEscortMenus( EscortGroup )
      self:_InitEscortRoute( EscortGroup )

      -- @param #AI_ESCORT self
      -- @param Core.Event#EVENTDATA EventData
      function EscortGroup:OnEventDeadOrCrash( EventData )
        self:F( { "EventDead", EventData } )
        self.EscortMenu:Remove()
      end

      EscortGroup:HandleEvent( EVENTS.Dead, EscortGroup.OnEventDeadOrCrash )
      EscortGroup:HandleEvent( EVENTS.Crash, EscortGroup.OnEventDeadOrCrash )

    end, EscortGroup
  )

end

-- @param #AI_ESCORT_REQUEST self
-- @param Core.Set#SET_GROUP EscortGroupSet
function AI_ESCORT_REQUEST:onafterStart( EscortGroupSet )

  self:F()

  if not self.MenuRequestEscort then
    self.MainMenu = MENU_GROUP:New( self.PlayerGroup, self.EscortName )
    self.MenuRequestEscort = MENU_GROUP_COMMAND:New( self.LeaderGroup, "Request new escort ", self.MainMenu, 
      function()
        self:SpawnEscort()
      end
      )
  end

  self:GetParent( self ).onafterStart( self, EscortGroupSet )

  self:HandleEvent( EVENTS.Dead, self.OnEventDeadOrCrash )
  self:HandleEvent( EVENTS.Crash, self.OnEventDeadOrCrash )
    
end

-- @param #AI_ESCORT_REQUEST self
-- @param Core.Set#SET_GROUP EscortGroupSet
function AI_ESCORT_REQUEST:onafterStop( EscortGroupSet )

  self:F()
  
  EscortGroupSet:ForEachGroup(
    -- @param Core.Group#GROUP EscortGroup
    function( EscortGroup )
      EscortGroup:WayPointInitialize()
    
      EscortGroup:OptionROTVertical()
      EscortGroup:OptionROEOpenFire()
    end
  )

  self.Detection:Stop()
    
  self.MainMenu:Remove()
  
end

--- Set the spawn mode to be mission execution.
-- @param #AI_ESCORT_REQUEST self
function AI_ESCORT_REQUEST:SetEscortSpawnMission()

  self.SpawnMode = self.__Enum.Mode.Mission
    
end
--- **AI** - Models the automatic assignment of AI escorts to player flights.
--
-- ## Features:
-- --     
--   * Provides the facilities to trigger escorts when players join flight slots.
--   * 
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_Escort_Dispatcher
-- @image MOOSE.JPG


-- @type AI_ESCORT_DISPATCHER
-- @extends Core.Fsm#FSM


--- Models the automatic assignment of AI escorts to player flights.
--
-- # Developer Note
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
--   
-- @field #AI_ESCORT_DISPATCHER
AI_ESCORT_DISPATCHER = {
  ClassName = "AI_ESCORT_DISPATCHER",
}

-- @field #list 
AI_ESCORT_DISPATCHER.AI_Escorts = {}


--- Creates a new AI_ESCORT_DISPATCHER object.
-- @param #AI_ESCORT_DISPATCHER self
-- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers for which escorts are spawned in.
-- @param Core.Spawn#SPAWN EscortSpawn The spawn object that will spawn in the Escorts.
-- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where the escorts are spawned.
-- @param #string EscortName Name of the escort, which will also be the name of the escort menu.
-- @param #string EscortBriefing A text showing the briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown.
-- @return #AI_ESCORT_DISPATCHER
-- @usage
-- 
-- -- Create a new escort when a player joins an SU-25T plane.
-- Create a carrier set, which contains the player slots that can be joined by the players, for which escorts will be defined.
-- local Red_SU25T_CarrierSet = SET_GROUP:New():FilterPrefixes( "Red A2G Player Su-25T" ):FilterStart()
-- 
-- -- Create a spawn object that will spawn in the escorts, once the player has joined the player slot.
-- local Red_SU25T_EscortSpawn = SPAWN:NewWithAlias( "Red A2G Su-25 Escort", "Red AI A2G SU-25 Escort" ):InitLimit( 10, 10 )
-- 
-- -- Create an airbase object, where the escorts will be spawned.
-- local Red_SU25T_Airbase = AIRBASE:FindByName( AIRBASE.Caucasus.Maykop_Khanskaya )
-- 
-- -- Park the airplanes at the airbase, visible before start.
-- Red_SU25T_EscortSpawn:ParkAtAirbase( Red_SU25T_Airbase, AIRBASE.TerminalType.OpenMedOrBig )
-- 
-- -- New create the escort dispatcher, using the carrier set, the escort spawn object at the escort airbase.
-- -- Provide a name of the escort, which will be also the name appearing on the radio menu for the group.
-- -- And a briefing to appear when the player joins the player slot.
-- Red_SU25T_EscortDispatcher = AI_ESCORT_DISPATCHER:New( Red_SU25T_CarrierSet, Red_SU25T_EscortSpawn, Red_SU25T_Airbase, "Escort Su-25", "You Su-25T is escorted by one Su-25. Use the radio menu to control the escorts." )
-- 
-- -- The dispatcher needs to be started using the :Start() method.
-- Red_SU25T_EscortDispatcher:Start()
function AI_ESCORT_DISPATCHER:New( CarrierSet, EscortSpawn, EscortAirbase, EscortName, EscortBriefing )

  local self = BASE:Inherit( self, FSM:New() ) -- #AI_ESCORT_DISPATCHER

  self.CarrierSet = CarrierSet
  self.EscortSpawn = EscortSpawn
  self.EscortAirbase = EscortAirbase
  self.EscortName = EscortName
  self.EscortBriefing = EscortBriefing

  self:SetStartState( "Idle" ) 
  
  self:AddTransition( "Monitoring", "Monitor", "Monitoring" )

  self:AddTransition( "Idle", "Start", "Monitoring" )
  self:AddTransition( "Monitoring", "Stop", "Idle" )
  
  -- Put a Dead event handler on CarrierSet, to ensure that when a carrier is destroyed, that all internal parameters are reset.
  function self.CarrierSet.OnAfterRemoved( CarrierSet, From, Event, To, CarrierName, Carrier )
    self:F( { Carrier = Carrier:GetName() } )
  end
  
  return self
end

function AI_ESCORT_DISPATCHER:onafterStart( From, Event, To )

  self:HandleEvent( EVENTS.Birth )
  
  self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventExit )
  self:HandleEvent( EVENTS.Crash, self.OnEventExit )
  self:HandleEvent( EVENTS.Dead, self.OnEventExit )

end

-- @param #AI_ESCORT_DISPATCHER self
-- @param Core.Event#EVENTDATA EventData
function AI_ESCORT_DISPATCHER:OnEventExit( EventData )

  local PlayerGroupName = EventData.IniGroupName
  local PlayerGroup = EventData.IniGroup
  local PlayerUnit = EventData.IniUnit
  
  self:T({EscortAirbase= self.EscortAirbase } )
  self:T({PlayerGroupName = PlayerGroupName } )
  self:T({PlayerGroup = PlayerGroup})
  self:T({FirstGroup = self.CarrierSet:GetFirst()})
  self:T({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )})
  
  if self.CarrierSet:FindGroup( PlayerGroupName ) then
    if self.AI_Escorts[PlayerGroupName] then
      self.AI_Escorts[PlayerGroupName]:Stop()
      self.AI_Escorts[PlayerGroupName] = nil
    end
  end
      
end

-- @param #AI_ESCORT_DISPATCHER self
-- @param Core.Event#EVENTDATA EventData
function AI_ESCORT_DISPATCHER:OnEventBirth( EventData )

  local PlayerGroupName = EventData.IniGroupName
  local PlayerGroup = EventData.IniGroup
  local PlayerUnit = EventData.IniUnit
  
  self:T({EscortAirbase= self.EscortAirbase } )
  self:T({PlayerGroupName = PlayerGroupName } )
  self:T({PlayerGroup = PlayerGroup})
  self:T({FirstGroup = self.CarrierSet:GetFirst()})
  self:T({FindGroup = self.CarrierSet:FindGroup( PlayerGroupName )})
  
  if self.CarrierSet:FindGroup( PlayerGroupName ) then
    if not self.AI_Escorts[PlayerGroupName] then
      local LeaderUnit = PlayerUnit
      local EscortGroup = self.EscortSpawn:SpawnAtAirbase( self.EscortAirbase, SPAWN.Takeoff.Hot )
      self:T({EscortGroup = EscortGroup})
      
      self:ScheduleOnce( 1,
        function( EscortGroup )
          local EscortSet = SET_GROUP:New()
          EscortSet:AddGroup( EscortGroup )
          self.AI_Escorts[PlayerGroupName] = AI_ESCORT:New( LeaderUnit, EscortSet, self.EscortName, self.EscortBriefing )
          self.AI_Escorts[PlayerGroupName]:FormationTrail( 0, 100, 0 )
          if EscortGroup:IsHelicopter() then
            self.AI_Escorts[PlayerGroupName]:MenusHelicopters()
          else
            self.AI_Escorts[PlayerGroupName]:MenusAirplanes()
          end
          self.AI_Escorts[PlayerGroupName]:__Start( 0.1 )
        end, EscortGroup
      )
    end
  end

end


--- Start Trigger for AI_ESCORT_DISPATCHER
-- @function [parent=#AI_ESCORT_DISPATCHER] Start
-- @param #AI_ESCORT_DISPATCHER self

--- Start Asynchronous Trigger for AI_ESCORT_DISPATCHER
-- @function [parent=#AI_ESCORT_DISPATCHER] __Start
-- @param #AI_ESCORT_DISPATCHER self
-- @param #number Delay

--- Stop Trigger for AI_ESCORT_DISPATCHER
-- @function [parent=#AI_ESCORT_DISPATCHER] Stop
-- @param #AI_ESCORT_DISPATCHER self

--- Stop Asynchronous Trigger for AI_ESCORT_DISPATCHER
-- @function [parent=#AI_ESCORT_DISPATCHER] __Stop
-- @param #AI_ESCORT_DISPATCHER self
-- @param #number Delay






--- **AI** - Models the assignment of AI escorts to player flights upon request using the radio menu.
--
-- ## Features:
--     
--   * Provides the facilities to trigger escorts when players join flight units.
--   * Provide a menu for which escorts can be requested.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_Escort_Dispatcher_Request
-- @image MOOSE.JPG


-- @type AI_ESCORT_DISPATCHER_REQUEST
-- @extends Core.Fsm#FSM


--- Models the assignment of AI escorts to player flights upon request using the radio menu.
--
-- ![Banner Image](..\Images\deprecated.png)
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
--   
-- @field #AI_ESCORT_DISPATCHER_REQUEST
AI_ESCORT_DISPATCHER_REQUEST = {
  ClassName = "AI_ESCORT_DISPATCHER_REQUEST",
}

-- @field #list 
AI_ESCORT_DISPATCHER_REQUEST.AI_Escorts = {}


--- Creates a new AI_ESCORT_DISPATCHER_REQUEST object.
-- @param #AI_ESCORT_DISPATCHER_REQUEST self
-- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers for which escorts are requested. 
-- @param Core.Spawn#SPAWN EscortSpawn The spawn object that will spawn in the Escorts.
-- @param Wrapper.Airbase#AIRBASE EscortAirbase The airbase where the escorts are spawned.
-- @param #string EscortName Name of the escort, which will also be the name of the escort menu.
-- @param #string EscortBriefing A text showing the briefing to the player. Note that if no EscortBriefing is provided, the default briefing will be shown.
-- @return #AI_ESCORT_DISPATCHER_REQUEST
function AI_ESCORT_DISPATCHER_REQUEST:New( CarrierSet, EscortSpawn, EscortAirbase, EscortName, EscortBriefing )

  local self = BASE:Inherit( self, FSM:New() ) -- #AI_ESCORT_DISPATCHER_REQUEST

  self.CarrierSet = CarrierSet
  self.EscortSpawn = EscortSpawn
  self.EscortAirbase = EscortAirbase
  self.EscortName = EscortName
  self.EscortBriefing = EscortBriefing

  self:SetStartState( "Idle" ) 
  
  self:AddTransition( "Monitoring", "Monitor", "Monitoring" )

  self:AddTransition( "Idle", "Start", "Monitoring" )
  self:AddTransition( "Monitoring", "Stop", "Idle" )
  
  -- Put a Dead event handler on CarrierSet, to ensure that when a carrier is destroyed, that all internal parameters are reset.
  function self.CarrierSet.OnAfterRemoved( CarrierSet, From, Event, To, CarrierName, Carrier )
    self:F( { Carrier = Carrier:GetName() } )
  end
  
  return self
end

function AI_ESCORT_DISPATCHER_REQUEST:onafterStart( From, Event, To )

  self:HandleEvent( EVENTS.Birth )
  
  self:HandleEvent( EVENTS.PlayerLeaveUnit, self.OnEventExit )
  self:HandleEvent( EVENTS.Crash, self.OnEventExit )
  self:HandleEvent( EVENTS.Dead, self.OnEventExit )

end

-- @param #AI_ESCORT_DISPATCHER_REQUEST self
-- @param Core.Event#EVENTDATA EventData
function AI_ESCORT_DISPATCHER_REQUEST:OnEventExit( EventData )

  local PlayerGroupName = EventData.IniGroupName
  local PlayerGroup = EventData.IniGroup
  local PlayerUnit = EventData.IniUnit
  
  if self.CarrierSet:FindGroup( PlayerGroupName ) then
    if self.AI_Escorts[PlayerGroupName] then
      self.AI_Escorts[PlayerGroupName]:Stop()
      self.AI_Escorts[PlayerGroupName] = nil
    end
  end
      
end

-- @param #AI_ESCORT_DISPATCHER_REQUEST self
-- @param Core.Event#EVENTDATA EventData
function AI_ESCORT_DISPATCHER_REQUEST:OnEventBirth( EventData )

  local PlayerGroupName = EventData.IniGroupName
  local PlayerGroup = EventData.IniGroup
  local PlayerUnit = EventData.IniUnit
  
  if self.CarrierSet:FindGroup( PlayerGroupName ) then
    if not self.AI_Escorts[PlayerGroupName] then
      local LeaderUnit = PlayerUnit
      self:ScheduleOnce( 0.1,
        function()
          self.AI_Escorts[PlayerGroupName] = AI_ESCORT_REQUEST:New( LeaderUnit, self.EscortSpawn, self.EscortAirbase, self.EscortName, self.EscortBriefing )
          self.AI_Escorts[PlayerGroupName]:FormationTrail( 0, 100, 0 )
          if PlayerGroup:IsHelicopter() then
            self.AI_Escorts[PlayerGroupName]:MenusHelicopters()
          else
            self.AI_Escorts[PlayerGroupName]:MenusAirplanes()
          end
          self.AI_Escorts[PlayerGroupName]:__Start( 0.1 )
        end
      )
    end
  end

end


--- Start Trigger for AI_ESCORT_DISPATCHER_REQUEST
-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] Start
-- @param #AI_ESCORT_DISPATCHER_REQUEST self

--- Start Asynchronous Trigger for AI_ESCORT_DISPATCHER_REQUEST
-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] __Start
-- @param #AI_ESCORT_DISPATCHER_REQUEST self
-- @param #number Delay

--- Stop Trigger for AI_ESCORT_DISPATCHER_REQUEST
-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] Stop
-- @param #AI_ESCORT_DISPATCHER_REQUEST self

--- Stop Asynchronous Trigger for AI_ESCORT_DISPATCHER_REQUEST
-- @function [parent=#AI_ESCORT_DISPATCHER_REQUEST] __Stop
-- @param #AI_ESCORT_DISPATCHER_REQUEST self
-- @param #number Delay






--- **AI** - Models the intelligent transportation of infantry and other cargo.
--
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_Cargo
-- @image Cargo.JPG

--- @type AI_CARGO
-- @extends Core.Fsm#FSM_CONTROLLABLE


--- Base class for the dynamic cargo handling capability for AI groups.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- Carriers can be mobilized to intelligently transport infantry and other cargo within the simulation.
-- The AI_CARGO module uses the @{Cargo.Cargo} capabilities within the MOOSE framework.
-- CARGO derived objects must be declared within the mission to make the AI_CARGO object recognize the cargo.
-- Please consult the @{Cargo.Cargo} module for more information. 
-- 
-- The derived classes from this module are:
-- 
--    * @{AI.AI_Cargo_APC} - Cargo transportation using APCs and other vehicles between zones.
--    * @{AI.AI_Cargo_Helicopter} - Cargo transportation using helicopters between zones.
--    * @{AI.AI_Cargo_Airplane} - Cargo transportation using airplanes to and from airbases.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
-- 
-- @field #AI_CARGO
AI_CARGO = {
  ClassName = "AI_CARGO",
  Coordinate = nil, -- Core.Point#COORDINATE,
  Carrier_Cargo = {},
}

--- Creates a new AI_CARGO object.
-- @param #AI_CARGO self
-- @param Wrapper.Group#GROUP Carrier Cargo carrier group.
-- @param Core.Set#SET_CARGO CargoSet Set of cargo(s) to transport.
-- @return #AI_CARGO self
function AI_CARGO:New( Carrier, CargoSet )

  local self = BASE:Inherit( self, FSM_CONTROLLABLE:New( Carrier ) ) -- #AI_CARGO

  self.CargoSet = CargoSet -- Core.Set#SET_CARGO
  self.CargoCarrier = Carrier -- Wrapper.Group#GROUP

  self:SetStartState( "Unloaded" )
  
  -- Board
  self:AddTransition( "Unloaded",     "Pickup",     "Unloaded" )
  self:AddTransition( "*",            "Load",       "*" )
  self:AddTransition( "*",            "Reload",       "*" )
  self:AddTransition( "*",            "Board",      "*" )
  self:AddTransition( "*",            "Loaded",     "Loaded" )
  self:AddTransition( "Loaded",       "PickedUp",   "Loaded" )
  
  -- Unload
  self:AddTransition( "Loaded",       "Deploy",     "*" )
  self:AddTransition( "*",            "Unload",     "*" )
  self:AddTransition( "*",            "Unboard",    "*" )
  self:AddTransition( "*",            "Unloaded",   "Unloaded" )
  self:AddTransition( "Unloaded",     "Deployed",   "Unloaded" )

  
  --- Pickup Handler OnBefore for AI_CARGO
  -- @function [parent=#AI_CARGO] OnBeforePickup
  -- @param #AI_CARGO self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Core.Point#COORDINATE Coordinate
  -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do. 
  -- @return #boolean
  
  --- Pickup Handler OnAfter for AI_CARGO
  -- @function [parent=#AI_CARGO] OnAfterPickup
  -- @param #AI_CARGO self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Core.Point#COORDINATE Coordinate
  -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do.
  
  --- Pickup Trigger for AI_CARGO
  -- @function [parent=#AI_CARGO] Pickup
  -- @param #AI_CARGO self
  -- @param Core.Point#COORDINATE Coordinate
  -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do.
  
  --- Pickup Asynchronous Trigger for AI_CARGO
  -- @function [parent=#AI_CARGO] __Pickup
  -- @param #AI_CARGO self
  -- @param #number Delay
  -- @param Core.Point#COORDINATE Coordinate Pickup place. If not given, loading starts at the current location.
  -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do.
  
  --- Deploy Handler OnBefore for AI_CARGO
  -- @function [parent=#AI_CARGO] OnBeforeDeploy
  -- @param #AI_CARGO self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Core.Point#COORDINATE Coordinate
  -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do.
  -- @return #boolean
  
  --- Deploy Handler OnAfter for AI_CARGO
  -- @function [parent=#AI_CARGO] OnAfterDeploy
  -- @param #AI_CARGO self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Core.Point#COORDINATE Coordinate
  -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do.
  
  --- Deploy Trigger for AI_CARGO
  -- @function [parent=#AI_CARGO] Deploy
  -- @param #AI_CARGO self
  -- @param Core.Point#COORDINATE Coordinate
  -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do.
  
  --- Deploy Asynchronous Trigger for AI_CARGO
  -- @function [parent=#AI_CARGO] __Deploy
  -- @param #AI_CARGO self
  -- @param #number Delay
  -- @param Core.Point#COORDINATE Coordinate
  -- @param #number Speed Speed in km/h. Default is 50% of max possible speed the group can do.

  
  --- Loaded Handler OnAfter for AI_CARGO
  -- @function [parent=#AI_CARGO] OnAfterLoaded
  -- @param #AI_CARGO self
  -- @param Wrapper.Group#GROUP Carrier
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  
  --- Unloaded Handler OnAfter for AI_CARGO
  -- @function [parent=#AI_CARGO] OnAfterUnloaded
  -- @param #AI_CARGO self
  -- @param Wrapper.Group#GROUP Carrier
  -- @param #string From
  -- @param #string Event
  -- @param #string To

  --- On after Deployed event.
  -- @function [parent=#AI_CARGO] OnAfterDeployed
  -- @param #AI_CARGO self
  -- @param Wrapper.Group#GROUP Carrier
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
  -- @param #boolean Defend Defend for APCs.
  
  
  for _, CarrierUnit in pairs( Carrier:GetUnits() ) do
    local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT
    CarrierUnit:SetCargoBayWeightLimit()
  end
  
  self.Transporting = false
  self.Relocating = false
  
  return self
end



function AI_CARGO:IsTransporting()

  return self.Transporting == true
end

function AI_CARGO:IsRelocating()

  return self.Relocating == true
end


--- On after Pickup event.
-- @param #AI_CARGO self
-- @param Wrapper.Group#GROUP APC
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate of the pickup point.
-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Height Height in meters to move to the home coordinate.
-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided.
function AI_CARGO:onafterPickup( APC, From, Event, To, Coordinate, Speed, Height, PickupZone )

  self.Transporting = false
  self.Relocating = true
  
end


--- On after Deploy event.
-- @param #AI_CARGO self
-- @param Wrapper.Group#GROUP APC
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate Deploy place.
-- @param #number Speed Speed in km/h to drive to the depoly coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Height Height in meters to move to the deploy coordinate.
-- @param Core.Zone#ZONE DeployZone The zone where the cargo will be deployed.
function AI_CARGO:onafterDeploy( APC, From, Event, To, Coordinate, Speed, Height, DeployZone )

  self.Relocating = false
  self.Transporting = true
  
end

--- On before Load event.
-- @param #AI_CARGO self
-- @param Wrapper.Group#GROUP Carrier
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided.
function AI_CARGO:onbeforeLoad( Carrier, From, Event, To, PickupZone )
  self:F( { Carrier, From, Event, To } )

  local Boarding = false

  local LoadInterval = 2
  local LoadDelay = 1
  local Carrier_List = {}
  local Carrier_Weight = {}

  if Carrier and Carrier:IsAlive() then
    self.Carrier_Cargo = {}
    for _, CarrierUnit in pairs( Carrier:GetUnits() ) do
      local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT
      
      local CargoBayFreeWeight = CarrierUnit:GetCargoBayFreeWeight()
      self:F({CargoBayFreeWeight=CargoBayFreeWeight})
      
      Carrier_List[#Carrier_List+1] = CarrierUnit
      Carrier_Weight[CarrierUnit] = CargoBayFreeWeight
    end

    local Carrier_Count = #Carrier_List
    local Carrier_Index = 1
      
    local Loaded = false

    for _, Cargo in UTILS.spairs( self.CargoSet:GetSet(), function( t, a, b ) return t[a]:GetWeight() > t[b]:GetWeight() end ) do
      local Cargo = Cargo -- Cargo.Cargo#CARGO

      self:F( { IsUnLoaded = Cargo:IsUnLoaded(), IsDeployed = Cargo:IsDeployed(), Cargo:GetName(), Carrier:GetName() } )

      -- Try all Carriers, but start from the one according the Carrier_Index
      for Carrier_Loop = 1, #Carrier_List do

        local CarrierUnit = Carrier_List[Carrier_Index] -- Wrapper.Unit#UNIT

        -- This counters loop through the available Carriers.
        Carrier_Index = Carrier_Index + 1
        if Carrier_Index > Carrier_Count then
          Carrier_Index = 1
        end
        
        if Cargo:IsUnLoaded() and not Cargo:IsDeployed() then
          if Cargo:IsInLoadRadius( CarrierUnit:GetCoordinate() ) then
            self:F( { "In radius", CarrierUnit:GetName() } )
            
            local CargoWeight = Cargo:GetWeight()
            local CarrierSpace=Carrier_Weight[CarrierUnit] 
  
            -- Only when there is space within the bay to load the next cargo item!
            if CarrierSpace > CargoWeight then
              Carrier:RouteStop()
              --Cargo:Ungroup()
              Cargo:__Board( -LoadDelay, CarrierUnit )
              self:__Board( LoadDelay, Cargo, CarrierUnit, PickupZone )

              LoadDelay = LoadDelay + Cargo:GetCount() * LoadInterval

              -- So now this CarrierUnit has Cargo that is being loaded.
              -- This will be used further in the logic to follow and to check cargo status.
              self.Carrier_Cargo[Cargo] = CarrierUnit
              Boarding = true
              Carrier_Weight[CarrierUnit] = Carrier_Weight[CarrierUnit] - CargoWeight
              Loaded = true
              
              -- Ok, we loaded a cargo, now we can stop the loop.
              break
            else
              self:T(string.format("WARNING: Cargo too heavy for carrier %s. Cargo=%.1f > %.1f free space", tostring(CarrierUnit:GetName()), CargoWeight, CarrierSpace))
            end
          end
        end
        
      end

    end
    
    if not Loaded == true then
      -- No loading happened, so we need to pickup something else.
      self.Relocating = false
    end
  end

  return Boarding
  
end


--- On before Reload event.
-- @param #AI_CARGO self
-- @param Wrapper.Group#GROUP Carrier
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided.
function AI_CARGO:onbeforeReload( Carrier, From, Event, To )
  self:F( { Carrier, From, Event, To } )

  local Boarding = false

  local LoadInterval = 2
  local LoadDelay = 1
  local Carrier_List = {}
  local Carrier_Weight = {}

  if Carrier and Carrier:IsAlive() then
    for _, CarrierUnit in pairs( Carrier:GetUnits() ) do
      local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT
      
      Carrier_List[#Carrier_List+1] = CarrierUnit
    end

    local Carrier_Count = #Carrier_List
    local Carrier_Index = 1
      
    local Loaded = false

    for Cargo, CarrierUnit in pairs( self.Carrier_Cargo ) do
      local Cargo = Cargo -- Cargo.Cargo#CARGO

      self:F( { IsUnLoaded = Cargo:IsUnLoaded(), IsDeployed = Cargo:IsDeployed(), Cargo:GetName(), Carrier:GetName() } )

      -- Try all Carriers, but start from the one according the Carrier_Index
      for Carrier_Loop = 1, #Carrier_List do

        local CarrierUnit = Carrier_List[Carrier_Index] -- Wrapper.Unit#UNIT

        -- This counters loop through the available Carriers.
        Carrier_Index = Carrier_Index + 1
        if Carrier_Index > Carrier_Count then
          Carrier_Index = 1
        end
        
        if Cargo:IsUnLoaded() and not Cargo:IsDeployed() then
          Carrier:RouteStop()
          Cargo:__Board( -LoadDelay, CarrierUnit )
          self:__Board( LoadDelay, Cargo, CarrierUnit )

          LoadDelay = LoadDelay + Cargo:GetCount() * LoadInterval

          -- So now this CarrierUnit has Cargo that is being loaded.
          -- This will be used further in the logic to follow and to check cargo status.
          self.Carrier_Cargo[Cargo] = CarrierUnit
          Boarding = true
          Loaded = true
        end
        
      end

    end
    
    if not Loaded == true then
      -- No loading happened, so we need to pickup something else.
      self.Relocating = false
    end
  end

  return Boarding
  
end

--- On after Board event.
-- @param #AI_CARGO self
-- @param Wrapper.Group#GROUP Carrier
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Cargo.Cargo#CARGO Cargo Cargo object.
-- @param Wrapper.Unit#UNIT CarrierUnit
-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided.
function AI_CARGO:onafterBoard( Carrier, From, Event, To, Cargo, CarrierUnit, PickupZone )
  self:F( { Carrier, From, Event, To, Cargo, CarrierUnit:GetName() } )

  if Carrier and Carrier:IsAlive() then
    self:F({ IsLoaded = Cargo:IsLoaded(), Cargo:GetName(), Carrier:GetName() } )
    if not Cargo:IsLoaded() and not Cargo:IsDestroyed() then
      self:__Board( -10, Cargo, CarrierUnit, PickupZone )
      return
    end
  end

  self:__Loaded( 0.1, Cargo, CarrierUnit, PickupZone )
  
end

--- On after Loaded event.
-- @param #AI_CARGO self
-- @param Wrapper.Group#GROUP Carrier
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @return #boolean Cargo loaded.
-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided.
function AI_CARGO:onafterLoaded( Carrier, From, Event, To, Cargo, PickupZone )
  self:F( { Carrier, From, Event, To } )

  local Loaded = true

  if Carrier and Carrier:IsAlive() then
    for Cargo, CarrierUnit in pairs( self.Carrier_Cargo ) do
      local Cargo = Cargo -- Cargo.Cargo#CARGO
      self:F( { IsLoaded = Cargo:IsLoaded(), IsDestroyed = Cargo:IsDestroyed(), Cargo:GetName(), Carrier:GetName() } )
      if not Cargo:IsLoaded() and not Cargo:IsDestroyed() then
        Loaded = false
      end
    end
  end
  
  if Loaded then
    self:__PickedUp( 0.1, PickupZone )
  end
  
end

--- On after PickedUp event.
-- @param #AI_CARGO self
-- @param Wrapper.Group#GROUP Carrier
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided.
function AI_CARGO:onafterPickedUp( Carrier, From, Event, To, PickupZone )
  self:F( { Carrier, From, Event, To } )

  Carrier:RouteResume()

  local HasCargo = false
  if Carrier and Carrier:IsAlive() then
    for Cargo, CarrierUnit in pairs( self.Carrier_Cargo ) do
      HasCargo = true
      break
    end
  end

  self.Relocating = false
  if HasCargo then
    self:F( "Transporting" )
    self.Transporting = true
  end
  
end




--- On after Unload event.
-- @param #AI_CARGO self
-- @param Wrapper.Group#GROUP Carrier
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
function AI_CARGO:onafterUnload( Carrier, From, Event, To, DeployZone, Defend )
  self:F( { Carrier, From, Event, To, DeployZone, Defend = Defend } )

  local UnboardInterval = 5
  local UnboardDelay = 5

  if Carrier and Carrier:IsAlive() then
    for _, CarrierUnit in pairs( Carrier:GetUnits() ) do
      local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT
      Carrier:RouteStop()
      for _, Cargo in pairs( CarrierUnit:GetCargo() ) do
        self:F( { Cargo = Cargo:GetName(), Isloaded = Cargo:IsLoaded() } )
        if Cargo:IsLoaded() then
          Cargo:__UnBoard( UnboardDelay )
          UnboardDelay = UnboardDelay + Cargo:GetCount() * UnboardInterval
          self:__Unboard( UnboardDelay, Cargo, CarrierUnit, DeployZone, Defend )
          if not Defend == true then
            Cargo:SetDeployed( true )
          end
        end 
      end
    end
  end
  
end

--- On after Unboard event.
-- @param #AI_CARGO self
-- @param Wrapper.Group#GROUP Carrier
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string Cargo.Cargo#CARGO Cargo Cargo object.
-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
function AI_CARGO:onafterUnboard( Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend )
  self:F( { Carrier, From, Event, To, Cargo:GetName(), DeployZone = DeployZone, Defend = Defend } )

  if Carrier and Carrier:IsAlive() then
    if not Cargo:IsUnLoaded() then
      self:__Unboard( 10, Cargo, CarrierUnit, DeployZone, Defend ) 
      return
    end
  end

  self:Unloaded( Cargo, CarrierUnit, DeployZone, Defend )
  
end

--- On after Unloaded event.
-- @param #AI_CARGO self
-- @param Wrapper.Group#GROUP Carrier
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string Cargo.Cargo#CARGO Cargo Cargo object.
-- @param #boolean Deployed Cargo is deployed.
-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
function AI_CARGO:onafterUnloaded( Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend )
  self:F( { Carrier, From, Event, To, Cargo:GetName(), DeployZone = DeployZone, Defend = Defend } )

  local AllUnloaded = true

  --Cargo:Regroup()

  if Carrier and Carrier:IsAlive() then
    for _, CarrierUnit in pairs( Carrier:GetUnits() ) do
      local CarrierUnit = CarrierUnit -- Wrapper.Unit#UNIT
      local IsEmpty = CarrierUnit:IsCargoEmpty()
      self:T({ IsEmpty = IsEmpty })
      if not IsEmpty then
        AllUnloaded = false
        break
      end
    end
    
    if AllUnloaded == true then
      if DeployZone == true then
        self.Carrier_Cargo = {}
      end
      self.CargoCarrier = Carrier
    end
  end

  if AllUnloaded == true then
    self:__Deployed( 5, DeployZone, Defend )
  end
  
end

--- On after Deployed event.
-- @param #AI_CARGO self
-- @param Wrapper.Group#GROUP Carrier
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
-- @param #boolean Defend Defend for APCs.
function AI_CARGO:onafterDeployed( Carrier, From, Event, To, DeployZone, Defend )
  self:F( { Carrier, From, Event, To, DeployZone = DeployZone, Defend = Defend } )

  if not Defend == true then
    self.Transporting = false
  else
    self:F( "Defending" )
    
  end

end
--- **AI** - Models the intelligent transportation of cargo using ground vehicles.
--
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_Cargo_APC
-- @image AI_Cargo_Dispatching_For_APC.JPG

-- @type AI_CARGO_APC
-- @extends AI.AI_Cargo#AI_CARGO


--- Brings a dynamic cargo handling capability for an AI vehicle group.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- Armoured Personnel Carriers (APC), Trucks, Jeeps and other ground based carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation.
-- 
-- The AI_CARGO_APC class uses the @{Cargo.Cargo} capabilities within the MOOSE framework.
-- @{Cargo.Cargo} must be declared within the mission to make the AI_CARGO_APC object recognize the cargo.
-- Please consult the @{Cargo.Cargo} module for more information. 
-- 
-- ## Cargo loading.
-- 
-- The module will load automatically cargo when the APCs are within boarding or loading radius.
-- The boarding or loading radius is specified when the cargo is created in the simulation, and therefore, this radius depends on the type of cargo
-- and the specified boarding radius.
-- 
-- ## **Defending** the APCs when enemies nearby.
-- 
-- Cargo will defend the carrier with its available arms, and to avoid cargo being lost within the battlefield.
--  
-- When the APCs are approaching enemy units, something special is happening. 
-- The APCs will stop moving, and the loaded infantry will unboard and follow the APCs and will help to defend the group.
-- The carrier will hold the route once the unboarded infantry is further than 50 meters from the APCs, 
-- to ensure that the APCs are not too far away from the following running infantry.
-- Once all enemies are cleared, the infantry will board again automatically into the APCs. Once boarded, the APCs will follow its pre-defined route.
-- 
-- A combat radius needs to be specified in meters at the @{#AI_CARGO_APC.New}() method. 
-- This combat radius will trigger the unboarding of troops when enemies are within the combat radius around the APCs.
-- During my tests, I've noticed that there is a balance between ensuring that the infantry is within sufficient hit radius (effectiveness) versus
-- vulnerability of the infantry. It all depends on the kind of enemies that are expected to be encountered. 
-- A combat radius of 350 meters to 500 meters has been proven to be the most effective and efficient.
-- 
-- However, when the defense of the carrier, is not required, it must be switched off.
-- This is done by disabling the defense of the carrier using the method @{#AI_CARGO_APC.SetCombatRadius}(), and providing a combat radius of 0 meters.
-- It can be switched on later when required by reenabling the defense using the method and providing a combat radius larger than 0.
-- 
-- ## Infantry or cargo **health**.
-- 
-- When infantry is unboarded from the APCs, the infantry is actually respawned into the battlefield. 
-- As a result, the unboarding infantry is very _healthy_ every time it unboards.
-- This is due to the limitation of the DCS simulator, which is not able to specify the health of new spawned units as a parameter.
-- However, infantry that was destroyed when unboarded and following the APCs, won't be respawned again. Destroyed is destroyed.
-- As a result, there is some additional strength that is gained when an unboarding action happens, but in terms of simulation balance this has
-- marginal impact on the overall battlefield simulation. Fortunately, the firing strength of infantry is limited, and thus, respacing healthy infantry every
-- time is not so much of an issue ... 
-- 
-- ## Control the APCs on the map.
-- 
-- It is possible also as a human ground commander to influence the path of the APCs, by pointing a new path using the DCS user interface on the map.
-- In this case, the APCs will change the direction towards its new indicated route. However, there is a catch!
-- Once the APCs are near the enemy, and infantry is unboarded, the APCs won't be able to hold the route until the infantry could catch up.
-- The APCs will simply drive on and won't stop! This is a limitation in ED that prevents user actions being controlled by the scripting engine.
-- No workaround is possible on this.
-- 
-- ## Cargo deployment.
--  
-- Using the @{#AI_CARGO_APC.Deploy}() method, you are able to direct the APCs towards a point on the battlefield to unboard/unload the cargo at the specific coordinate. 
-- The APCs will follow nearby roads as much as possible, to ensure fast and clean cargo transportation between the objects and villages in the simulation environment.
-- 
-- ## Cargo pickup.
--  
-- Using the @{#AI_CARGO_APC.Pickup}() method, you are able to direct the APCs towards a point on the battlefield to board/load the cargo at the specific coordinate. 
-- The APCs will follow nearby roads as much as possible, to ensure fast and clean cargo transportation between the objects and villages in the simulation environment.
-- 
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- 
-- @field #AI_CARGO_APC
AI_CARGO_APC = {
  ClassName = "AI_CARGO_APC",
  Coordinate = nil, -- Core.Point#COORDINATE,
}

--- Creates a new AI_CARGO_APC object.
-- @param #AI_CARGO_APC self
-- @param Wrapper.Group#GROUP APC The carrier APC group.
-- @param Core.Set#SET_CARGO CargoSet The set of cargo to be transported.
-- @param #number CombatRadius Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby. When the combat radius is 0, no defense will happen of the carrier.
-- @return #AI_CARGO_APC
function AI_CARGO_APC:New( APC, CargoSet, CombatRadius )

  local self = BASE:Inherit( self, AI_CARGO:New( APC, CargoSet ) ) -- #AI_CARGO_APC

  self:AddTransition( "*", "Monitor", "*" )
  self:AddTransition( "*", "Follow", "Following" )
  self:AddTransition( "*", "Guard", "Unloaded" )
  self:AddTransition( "*", "Home", "*" )
  self:AddTransition( "*", "Reload", "Boarding" )
  self:AddTransition( "*", "Deployed", "*" )
  self:AddTransition( "*", "PickedUp", "*" )
  self:AddTransition( "*", "Destroyed", "Destroyed" )

  self:SetCombatRadius( CombatRadius )

  self:SetCarrier( APC )
  
  return self
end


--- Set the Carrier.
-- @param #AI_CARGO_APC self
-- @param Wrapper.Group#GROUP CargoCarrier
-- @return #AI_CARGO_APC
function AI_CARGO_APC:SetCarrier( CargoCarrier )

  self.CargoCarrier = CargoCarrier -- Wrapper.Group#GROUP
  self.CargoCarrier:SetState( self.CargoCarrier, "AI_CARGO_APC", self )

  CargoCarrier:HandleEvent( EVENTS.Dead )
  
  function CargoCarrier:OnEventDead( EventData )
    self:F({"dead"})
    local AICargoTroops = self:GetState( self, "AI_CARGO_APC" )
    self:F({AICargoTroops=AICargoTroops})
    if AICargoTroops then
      self:F({})
      if not AICargoTroops:Is( "Loaded" ) then
        -- There are enemies within combat radius. Unload the CargoCarrier.
        AICargoTroops:Destroyed()
      end
    end
  end

--  CargoCarrier:HandleEvent( EVENTS.Hit )
--  
--  function CargoCarrier:OnEventHit( EventData )
--    self:F({"hit"})
--    local AICargoTroops = self:GetState( self, "AI_CARGO_APC" )
--    if AICargoTroops then
--      self:F( { OnHitLoaded = AICargoTroops:Is( "Loaded" ) } )
--      if AICargoTroops:Is( "Loaded" ) or AICargoTroops:Is( "Boarding" ) then
--        -- There are enemies within combat radius. Unload the CargoCarrier.
--        AICargoTroops:Unload( false )
--      end
--    end
--  end
  
  self.Zone = ZONE_UNIT:New( self.CargoCarrier:GetName() .. "-Zone", self.CargoCarrier, self.CombatRadius )
  self.Coalition = self.CargoCarrier:GetCoalition()
  
  self:SetControllable( CargoCarrier )

  self:Guard()

  return self
end

--- Set whether or not the carrier will use roads to *pickup* and *deploy* the cargo.
-- @param #AI_CARGO_APC self
-- @param #boolean Offroad If true, carrier will not use roads. If `nil` or `false` the carrier will use roads when available.
-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`.
-- @return #AI_CARGO_APC self
function AI_CARGO_APC:SetOffRoad(Offroad, Formation)

  self:SetPickupOffRoad(Offroad, Formation)
  self:SetDeployOffRoad(Offroad, Formation)
  
  return self
end

--- Set whether the carrier will *not* use roads to *pickup* the cargo.
-- @param #AI_CARGO_APC self
-- @param #boolean Offroad If true, carrier will not use roads.
-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`.
-- @return #AI_CARGO_APC self
function AI_CARGO_APC:SetPickupOffRoad(Offroad, Formation)

  self.pickupOffroad=Offroad
  self.pickupFormation=Formation or ENUMS.Formation.Vehicle.OffRoad
  
  return self
end

--- Set whether the carrier will *not* use roads to *deploy* the cargo.
-- @param #AI_CARGO_APC self
-- @param #boolean Offroad If true, carrier will not use roads.
-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`.
-- @return #AI_CARGO_APC self
function AI_CARGO_APC:SetDeployOffRoad(Offroad, Formation)

  self.deployOffroad=Offroad
  self.deployFormation=Formation or ENUMS.Formation.Vehicle.OffRoad
  
  return self
end


--- Find a free Carrier within a radius.
-- @param #AI_CARGO_APC self
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Radius
-- @return Wrapper.Group#GROUP NewCarrier
function AI_CARGO_APC:FindCarrier( Coordinate, Radius )

  local CoordinateZone = ZONE_RADIUS:New( "Zone" , Coordinate:GetVec2(), Radius )
  CoordinateZone:Scan( { Object.Category.UNIT } )
  for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do
    local NearUnit = UNIT:Find( DCSUnit )
    self:F({NearUnit=NearUnit})
    if not NearUnit:GetState( NearUnit, "AI_CARGO_APC" ) then
      local Attributes = NearUnit:GetDesc()
      self:F({Desc=Attributes})
      if NearUnit:HasAttribute( "Trucks" ) then
        return NearUnit:GetGroup()
      end
    end
  end
  
  return nil

end

--- Enable/Disable unboarding of cargo (infantry) when enemies are nearby (to help defend the carrier).
-- This is only valid for APCs and trucks etc, thus ground vehicles.
-- @param #AI_CARGO_APC self
-- @param #number CombatRadius Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby. 
-- When the combat radius is 0, no defense will happen of the carrier. 
-- When the combat radius is not provided, no defense will happen!
-- @return #AI_CARGO_APC
-- @usage
-- 
-- -- Disembark the infantry when the carrier is under attack.
-- AICargoAPC:SetCombatRadius( true )
-- 
-- -- Keep the cargo in the carrier when the carrier is under attack.
-- AICargoAPC:SetCombatRadius( false )
function AI_CARGO_APC:SetCombatRadius( CombatRadius )

  self.CombatRadius = CombatRadius or 0

  if self.CombatRadius > 0 then
    self:__Monitor( -5 )
  end

  return self
end


--- Follow Infantry to the Carrier.
-- @param #AI_CARGO_APC self
-- @param #AI_CARGO_APC Me
-- @param Wrapper.Unit#UNIT APCUnit
-- @param Cargo.CargoGroup#CARGO_GROUP Cargo
-- @return #AI_CARGO_APC
function AI_CARGO_APC:FollowToCarrier( Me, APCUnit, CargoGroup )

  local InfantryGroup = CargoGroup:GetGroup()

  self:F( { self = self:GetClassNameAndID(), InfantryGroup = InfantryGroup:GetName() } )
  
  --if self:Is( "Following" ) then

  if APCUnit:IsAlive() then
    -- We check if the Cargo is near to the CargoCarrier.
    if InfantryGroup:IsPartlyInZone( ZONE_UNIT:New( "Radius", APCUnit, 25 ) ) then
  
      -- The Cargo does not need to follow the Carrier.
      Me:Guard()
    
    else
      
      self:F( { InfantryGroup = InfantryGroup:GetName() } )
    
      if InfantryGroup:IsAlive() then
            
        self:F( { InfantryGroup = InfantryGroup:GetName() } )
  
        local Waypoints = {}
        
        -- Calculate the new Route.
        local FromCoord = InfantryGroup:GetCoordinate()
        local FromGround = FromCoord:WaypointGround( 10, "Diamond" )
        self:F({FromGround=FromGround})
        table.insert( Waypoints, FromGround )
  
        local ToCoord = APCUnit:GetCoordinate():GetRandomCoordinateInRadius( 10, 5 )
        local ToGround = ToCoord:WaypointGround( 10, "Diamond" )
        self:F({ToGround=ToGround})
        table.insert( Waypoints, ToGround )
        
        local TaskRoute = InfantryGroup:TaskFunction( "AI_CARGO_APC.FollowToCarrier", Me, APCUnit, CargoGroup )
        
        self:F({Waypoints = Waypoints})
        local Waypoint = Waypoints[#Waypoints]
        InfantryGroup:SetTaskWaypoint( Waypoint, TaskRoute ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone.
      
        InfantryGroup:Route( Waypoints, 1 ) -- Move after a random seconds to the Route. See the Route method for details.
      end
    end
  end
end


--- On after Monitor event.
-- @param #AI_CARGO_APC self
-- @param Wrapper.Group#GROUP APC
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AI_CARGO_APC:onafterMonitor( APC, From, Event, To )
  self:F( { APC, From, Event, To, IsTransporting = self:IsTransporting() } )

  if self.CombatRadius > 0 then
    if APC and APC:IsAlive() then
      if self.CarrierCoordinate then
        if self:IsTransporting() == true then
          local Coordinate = APC:GetCoordinate()
          if self:Is( "Unloaded" ) or self:Is( "Loaded" ) then
            self.Zone:Scan( { Object.Category.UNIT } )
            if self.Zone:IsAllInZoneOfCoalition( self.Coalition ) then
              if self:Is( "Unloaded" ) then
                -- There are no enemies within combat radius. Reload the CargoCarrier.
                self:Reload()
              end
            else
              if self:Is( "Loaded" ) then
                -- There are enemies within combat radius. Unload the CargoCarrier.
                self:__Unload( 1, nil, true ) -- The 2nd parameter is true, which means that the unload is for defending the carrier, not to deploy!
              else
                if self:Is( "Unloaded" ) then
                  --self:Follow()
                end
                self:F( "I am here" .. self:GetCurrentState() )
                if self:Is( "Following" ) then
                  for Cargo, APCUnit in pairs( self.Carrier_Cargo ) do
                    local Cargo = Cargo -- Cargo.Cargo#CARGO
                    local APCUnit = APCUnit -- Wrapper.Unit#UNIT
                    if Cargo:IsAlive() then
                      if not Cargo:IsNear( APCUnit, 40 ) then
                        APCUnit:RouteStop()
                        self.CarrierStopped = true
                      else
                        if self.CarrierStopped then
                          if Cargo:IsNear( APCUnit, 25 ) then
                            APCUnit:RouteResume()
                            self.CarrierStopped = nil
                          end
                        end
                      end
                    end
                  end
                end
              end
            end
          end
        end
        
      end
      self.CarrierCoordinate = APC:GetCoordinate()
    end
  
    self:__Monitor( -5 )
  end
  
end


--- On after Follow event.
-- @param #AI_CARGO_APC self
-- @param Wrapper.Group#GROUP APC
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function AI_CARGO_APC:onafterFollow( APC, From, Event, To )
  self:F( { APC, From, Event, To } )

  self:F( "Follow" )
  if APC and APC:IsAlive() then
    for Cargo, APCUnit in pairs( self.Carrier_Cargo ) do
      local Cargo = Cargo -- Cargo.Cargo#CARGO
      if Cargo:IsUnLoaded() then
        self:FollowToCarrier( self, APCUnit, Cargo )
        APCUnit:RouteResume()
      end
    end
  end
  
end

--- Pickup task function. Triggers Load event.
-- @param Wrapper.Group#GROUP APC The cargo carrier group.
-- @param #AI_CARGO_APC sel `AI_CARGO_APC` class.
-- @param Core.Point#COORDINATE Coordinate. The coordinate (not used).
-- @param #number Speed Speed (not used).
-- @param Core.Zone#ZONE PickupZone Pickup zone.
function AI_CARGO_APC._Pickup(APC, self, Coordinate, Speed, PickupZone)

  APC:F( { "AI_CARGO_APC._Pickup:", APC:GetName() } )

  if APC:IsAlive() then
    self:Load( PickupZone )
  end
end

--- Deploy task function. Triggers Unload event.
-- @param Wrapper.Group#GROUP APC The cargo carrier group.
-- @param #AI_CARGO_APC self `AI_CARGO_APC` class.
-- @param Core.Point#COORDINATE Coordinate. The coordinate (not used).
-- @param Core.Zone#ZONE DeployZone Deploy zone.
function AI_CARGO_APC._Deploy(APC, self, Coordinate, DeployZone)

  APC:F( { "AI_CARGO_APC._Deploy:", APC } )

  if APC:IsAlive() then
    self:Unload( DeployZone )
  end
end



--- On after Pickup event.
-- @param #AI_CARGO_APC self
-- @param Wrapper.Group#GROUP APC
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate of the pickup point.
-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Height Height in meters to move to the pickup coordinate. This parameter is ignored for APCs.
-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided.
function AI_CARGO_APC:onafterPickup( APC, From, Event, To, Coordinate, Speed, Height, PickupZone )

  if APC and APC:IsAlive() then

    if Coordinate then
      self.RoutePickup = true
      
      local _speed=Speed or APC:GetSpeedMax()*0.5

      -- Route on road.
      local Waypoints = {}
      
      if self.pickupOffroad then
        Waypoints[1]=APC:GetCoordinate():WaypointGround(Speed, self.pickupFormation)
        Waypoints[2]=Coordinate:WaypointGround(_speed, self.pickupFormation, DCSTasks)
      else
        Waypoints=APC:TaskGroundOnRoad(Coordinate, _speed, ENUMS.Formation.Vehicle.OffRoad, true)
      end
      
  
      local TaskFunction = APC:TaskFunction( "AI_CARGO_APC._Pickup", self, Coordinate, Speed, PickupZone )

      local Waypoint = Waypoints[#Waypoints]
      APC:SetTaskWaypoint( Waypoint, TaskFunction ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone.
    
      APC:Route( Waypoints, 1 ) -- Move after a random seconds to the Route. See the Route method for details.
    else
      AI_CARGO_APC._Pickup( APC, self, Coordinate, Speed, PickupZone )
    end

    self:GetParent( self, AI_CARGO_APC ).onafterPickup( self, APC, From, Event, To, Coordinate, Speed, Height, PickupZone )
  end
  
end


--- On after Deploy event.
-- @param #AI_CARGO_APC self
-- @param Wrapper.Group#GROUP APC
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate Deploy place.
-- @param #number Speed Speed in km/h to drive to the depoly coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Height Height in meters to move to the deploy coordinate. This parameter is ignored for APCs.
-- @param Core.Zone#ZONE DeployZone The zone where the cargo will be deployed.
function AI_CARGO_APC:onafterDeploy( APC, From, Event, To, Coordinate, Speed, Height, DeployZone )

  if APC and APC:IsAlive() then

    self.RouteDeploy = true
    
    -- Set speed in km/h.
    local speedmax=APC:GetSpeedMax()    
    local _speed=Speed or speedmax*0.5    
    _speed=math.min(_speed, speedmax)

    -- Route on road.
    local Waypoints = {}
    
    if self.deployOffroad then
      Waypoints[1]=APC:GetCoordinate():WaypointGround(Speed, self.deployFormation)
      Waypoints[2]=Coordinate:WaypointGround(_speed, self.deployFormation, DCSTasks)
    else
      Waypoints=APC:TaskGroundOnRoad(Coordinate, _speed, ENUMS.Formation.Vehicle.OffRoad, true)
    end

    -- Task function
    local TaskFunction = APC:TaskFunction( "AI_CARGO_APC._Deploy", self, Coordinate, DeployZone )
    
    -- Last waypoint
    local Waypoint = Waypoints[#Waypoints]
    
    -- Set task function
    APC:SetTaskWaypoint(Waypoint, TaskFunction) -- Set for the given Route at Waypoint 2 the TaskRouteToZone.
  
    -- Route group
    APC:Route( Waypoints, 1 ) -- Move after a random seconds to the Route. See the Route method for details.

    -- Call parent function.
    self:GetParent( self, AI_CARGO_APC ).onafterDeploy( self, APC, From, Event, To, Coordinate, Speed, Height, DeployZone )

  end
  
end

--- On after Unloaded event.
-- @param #AI_CARGO_APC self
-- @param Wrapper.Group#GROUP Carrier
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #string Cargo.Cargo#CARGO Cargo Cargo object.
-- @param #boolean Deployed Cargo is deployed.
-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
function AI_CARGO_APC:onafterUnloaded( Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend )
  self:F( { Carrier, From, Event, To, DeployZone = DeployZone, Defend = Defend } )


  self:GetParent( self, AI_CARGO_APC ).onafterUnloaded( self, Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone, Defend  )

  -- If Defend == true then we need to scan for possible enemies within combat zone and engage only ground forces.
  if Defend == true then
    self.Zone:Scan( { Object.Category.UNIT } )
    if not self.Zone:IsAllInZoneOfCoalition( self.Coalition ) then
      -- OK, enemies nearby, now find the enemies and attack them.
      local AttackUnits = self.Zone:GetScannedUnits() -- #list<DCS#Unit>
      local Move = {}
      local CargoGroup = Cargo.CargoObject -- Wrapper.Group#GROUP
      Move[#Move+1] = CargoGroup:GetCoordinate():WaypointGround( 70, "Custom" )
      for UnitId, AttackUnit in pairs( AttackUnits ) do
        local MooseUnit = UNIT:Find( AttackUnit )
        if MooseUnit:GetCoalition() ~= CargoGroup:GetCoalition() then
          Move[#Move+1] = MooseUnit:GetCoordinate():WaypointGround( 70, "Line abreast" )
          --MoveTo.Task = CargoGroup:TaskCombo( CargoGroup:TaskAttackUnit( MooseUnit, true ) )
          self:F( { MooseUnit = MooseUnit:GetName(), CargoGroup = CargoGroup:GetName() } )
        end
      end
      CargoGroup:RoutePush( Move, 0.1 )
    end
  
  end

end

--- On after Deployed event.
-- @param #AI_CARGO_APC self
-- @param Wrapper.Group#GROUP Carrier
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
function AI_CARGO_APC:onafterDeployed( APC, From, Event, To, DeployZone, Defend )
  self:F( { APC, From, Event, To, DeployZone = DeployZone, Defend = Defend } )

  self:__Guard( 0.1 )

  self:GetParent( self, AI_CARGO_APC ).onafterDeployed( self, APC, From, Event, To, DeployZone, Defend )

end


--- On after Home event.
-- @param #AI_CARGO_APC self
-- @param Wrapper.Group#GROUP APC
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate Home place.
-- @param #number Speed Speed in km/h to drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Height Height in meters to move to the home coordinate. This parameter is ignored for APCs.
function AI_CARGO_APC:onafterHome( APC, From, Event, To, Coordinate, Speed, Height, HomeZone )

  if APC and APC:IsAlive() ~= nil then

    self.RouteHome = true
    
    Speed = Speed or APC:GetSpeedMax()*0.5
    
    local Waypoints = APC:TaskGroundOnRoad( Coordinate, Speed, "Line abreast", true )

    self:F({Waypoints = Waypoints})
    local Waypoint = Waypoints[#Waypoints]
  
    APC:Route( Waypoints, 1 ) -- Move after a random seconds to the Route. See the Route method for details.
    
  end
  
end
--- **AI** - Models the intelligent transportation of cargo using helicopters.
--
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_Cargo_Helicopter
-- @image AI_Cargo_Dispatching_For_Helicopters.JPG

-- @type AI_CARGO_HELICOPTER
-- @extends Core.Fsm#FSM_CONTROLLABLE


--- Brings a dynamic cargo handling capability for an AI helicopter group.
--  
--  ![Banner Image](..\Images\deprecated.png)
--  
-- Helicopter carriers can be mobilized to intelligently transport infantry and other cargo within the simulation.
-- 
-- The AI_CARGO_HELICOPTER class uses the @{Cargo.Cargo} capabilities within the MOOSE framework.
-- @{Cargo.Cargo} must be declared within the mission to make the AI_CARGO_HELICOPTER object recognize the cargo.
-- Please consult the @{Cargo.Cargo} module for more information. 
-- 
-- ## Cargo pickup.
--  
-- Using the @{#AI_CARGO_HELICOPTER.Pickup}() method, you are able to direct the helicopters towards a point on the battlefield to board/load the cargo at the specific coordinate. 
-- Ensure that the landing zone is horizontally flat, and that trees cannot be found in the landing vicinity, or the helicopters won't land or will even crash!
-- 
-- ## Cargo deployment.
--  
-- Using the @{#AI_CARGO_HELICOPTER.Deploy}() method, you are able to direct the helicopters towards a point on the battlefield to unboard/unload the cargo at the specific coordinate. 
-- Ensure that the landing zone is horizontally flat, and that trees cannot be found in the landing vicinity, or the helicopters won't land or will even crash!
-- 
-- ## Infantry health.
-- 
-- When infantry is unboarded from the helicopters, the infantry is actually respawned into the battlefield. 
-- As a result, the unboarding infantry is very _healthy_ every time it unboards.
-- This is due to the limitation of the DCS simulator, which is not able to specify the health of new spawned units as a parameter.
-- However, infantry that was destroyed when unboarded, won't be respawned again. Destroyed is destroyed.
-- As a result, there is some additional strength that is gained when an unboarding action happens, but in terms of simulation balance this has
-- marginal impact on the overall battlefield simulation. Fortunately, the firing strength of infantry is limited, and thus, respacing healthy infantry every
-- time is not so much of an issue ... 
-- 
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_CARGO_HELICOPTER
AI_CARGO_HELICOPTER = {
  ClassName = "AI_CARGO_HELICOPTER",
  Coordinate = nil, -- Core.Point#COORDINATE,
}

AI_CARGO_QUEUE = {}

--- Creates a new AI_CARGO_HELICOPTER object.
-- @param #AI_CARGO_HELICOPTER self
-- @param Wrapper.Group#GROUP Helicopter
-- @param Core.Set#SET_CARGO CargoSet
-- @return #AI_CARGO_HELICOPTER
function AI_CARGO_HELICOPTER:New( Helicopter, CargoSet )

  local self = BASE:Inherit( self, AI_CARGO:New( Helicopter, CargoSet ) ) -- #AI_CARGO_HELICOPTER

  self.Zone = ZONE_GROUP:New( Helicopter:GetName(), Helicopter, 300 )

  self:SetStartState( "Unloaded" ) 
  -- Boarding
  self:AddTransition( "Unloaded",     "Pickup",     "Unloaded" )
  self:AddTransition( "*",            "Landed",     "*" )
  self:AddTransition( "*",            "Load",       "*" )
  self:AddTransition( "*",            "Loaded",     "Loaded" )
  self:AddTransition( "Loaded",       "PickedUp",   "Loaded" )
  
  -- Unboarding
  self:AddTransition( "Loaded",       "Deploy",     "*" )
  self:AddTransition( "*",            "Queue",      "*" )
  self:AddTransition( "*",            "Orbit" ,     "*" )
  self:AddTransition( "*",            "Destroyed",  "*" )
  self:AddTransition( "*",            "Unload",     "*" )    
  self:AddTransition( "*",            "Unloaded",   "Unloaded" )
  self:AddTransition( "Unloaded",     "Deployed",   "Unloaded" )
  
  -- RTB
  self:AddTransition( "*", "Home" , "*" ) 

  --- Pickup Handler OnBefore for AI_CARGO_HELICOPTER
  -- @function [parent=#AI_CARGO_HELICOPTER] OnBeforePickup
  -- @param #AI_CARGO_HELICOPTER self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Core.Point#COORDINATE Coordinate
  -- @return #boolean
  
  --- Pickup Handler OnAfter for AI_CARGO_HELICOPTER
  -- @function [parent=#AI_CARGO_HELICOPTER] OnAfterPickup
  -- @param #AI_CARGO_HELICOPTER self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Core.Point#COORDINATE Coordinate
  -- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
  
    --- PickedUp Handler OnAfter for AI_CARGO_HELICOPTER - Cargo set has been picked up, ready to deploy
  -- @function [parent=#AI_CARGO_HELICOPTER] OnAfterPickedUp
  -- @param #AI_CARGO_HELICOPTER self
  -- @param Wrapper.Group#GROUP Helicopter The helicopter #GROUP object
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Wrapper.Unit#UNIT Unit The helicopter #UNIT object

    --- Unloaded Handler OnAfter for AI_CARGO_HELICOPTER - Cargo unloaded, carrier is empty
  -- @function [parent=#AI_CARGO_HELICOPTER] OnAfterUnloaded
  -- @param #AI_CARGO_HELICOPTER self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Cargo.CargoGroup#CARGO_GROUP Cargo The #CARGO_GROUP object.
  -- @param Wrapper.Unit#UNIT Unit The helicopter #UNIT object
  
  --- Pickup Trigger for AI_CARGO_HELICOPTER
  -- @function [parent=#AI_CARGO_HELICOPTER] Pickup
  -- @param #AI_CARGO_HELICOPTER self
  -- @param Core.Point#COORDINATE Coordinate
  -- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
  
  --- Pickup Asynchronous Trigger for AI_CARGO_HELICOPTER
  -- @function [parent=#AI_CARGO_HELICOPTER] __Pickup
  -- @param #AI_CARGO_HELICOPTER self
  -- @param #number Delay Delay in seconds.
  -- @param Core.Point#COORDINATE Coordinate
  -- @param #number Speed Speed in km/h to go to the pickup coordinate. Default is 50% of max possible speed the unit can go.
  
  --- Deploy Handler OnBefore for AI_CARGO_HELICOPTER
  -- @function [parent=#AI_CARGO_HELICOPTER] OnBeforeDeploy
  -- @param #AI_CARGO_HELICOPTER self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Core.Point#COORDINATE Coordinate Place at which cargo is deployed.
  -- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
  -- @return #boolean
  
  --- Deploy Handler OnAfter for AI_CARGO_HELICOPTER
  -- @function [parent=#AI_CARGO_HELICOPTER] OnAfterDeploy
  -- @param #AI_CARGO_HELICOPTER self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Core.Point#COORDINATE Coordinate
  -- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
  
    --- Deployed Handler OnAfter for AI_CARGO_HELICOPTER
  -- @function [parent=#AI_CARGO_HELICOPTER] OnAfterDeployed
  -- @param #AI_CARGO_HELICOPTER self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  
  --- Deploy Trigger for AI_CARGO_HELICOPTER
  -- @function [parent=#AI_CARGO_HELICOPTER] Deploy
  -- @param #AI_CARGO_HELICOPTER self
  -- @param Core.Point#COORDINATE Coordinate Place at which the cargo is deployed.
  -- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
  
  --- Deploy Asynchronous Trigger for AI_CARGO_HELICOPTER
  -- @function [parent=#AI_CARGO_HELICOPTER] __Deploy
  -- @param #number Delay Delay in seconds.
  -- @param #AI_CARGO_HELICOPTER self
  -- @param Core.Point#COORDINATE Coordinate Place at which the cargo is deployed.
  -- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
  
  --- Home Trigger for AI_CARGO_HELICOPTER
  -- @function [parent=#AI_CARGO_HELICOPTER] Home
  -- @param #AI_CARGO_HELICOPTER self
  -- @param Core.Point#COORDINATE Coordinate Place to which the helicopter will go.
  -- @param #number Speed (optional) Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
  -- @param #number Height (optional) Height the Helicopter should be flying at.
  
  --- Home Asynchronous Trigger for AI_CARGO_HELICOPTER
  -- @function [parent=#AI_CARGO_HELICOPTER] __Home
  -- @param #number Delay Delay in seconds.
  -- @param #AI_CARGO_HELICOPTER self
  -- @param Core.Point#COORDINATE Coordinate Place to which the helicopter will go.
  -- @param #number Speed (optional) Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
  -- @param #number Height (optional) Height the Helicopter should be flying at. 

  -- We need to capture the Crash events for the helicopters.
  -- The helicopter reference is used in the semaphore AI_CARGO_QUEUE.
  -- So, we need to unlock this when the helo is not anymore ...
  Helicopter:HandleEvent( EVENTS.Crash,
    function( Helicopter, EventData )
      AI_CARGO_QUEUE[Helicopter] = nil
    end
  )

  -- We need to capture the Land events for the helicopters.
  -- The helicopter reference is used in the semaphore AI_CARGO_QUEUE.
  -- So, we need to unlock this when the helo has landed, which can be anywhere ...
  -- But only free the landing coordinate after 1 minute, to ensure that all helos have left.
  Helicopter:HandleEvent( EVENTS.Land,
    function( Helicopter, EventData )
      self:ScheduleOnce( 60, 
        function( Helicopter )
          AI_CARGO_QUEUE[Helicopter] = nil
        end, Helicopter
      )
    end
  )

  self:SetCarrier( Helicopter )
  
  self.landingspeed = 15 -- kph
  self.landingheight = 5.5 -- meter
  
  return self
end





--- Set the Carrier.
-- @param #AI_CARGO_HELICOPTER self
-- @param Wrapper.Group#GROUP Helicopter
-- @return #AI_CARGO_HELICOPTER
function AI_CARGO_HELICOPTER:SetCarrier( Helicopter )

  local AICargo = self

  self.Helicopter = Helicopter -- Wrapper.Group#GROUP
  self.Helicopter:SetState( self.Helicopter, "AI_CARGO_HELICOPTER", self )

  self.RoutePickup = false
  self.RouteDeploy = false

  Helicopter:HandleEvent( EVENTS.Dead )
  Helicopter:HandleEvent( EVENTS.Hit )
  Helicopter:HandleEvent( EVENTS.Land )
  
  function Helicopter:OnEventDead( EventData )
    local AICargoTroops = self:GetState( self, "AI_CARGO_HELICOPTER" )
    self:F({AICargoTroops=AICargoTroops})
    if AICargoTroops then
      self:F({})
      if not AICargoTroops:Is( "Loaded" ) then
        -- There are enemies within combat range. Unload the Helicopter.
        AICargoTroops:Destroyed()
      end
    end
  end
  
  function Helicopter:OnEventLand( EventData )
    AICargo:Landed()
  end
  
  self.Coalition = self.Helicopter:GetCoalition()
  
  self:SetControllable( Helicopter )

  return self
end

--- Set landingspeed and -height for helicopter landings. Adjust after tracing if your helis get stuck after landing.
-- @param #AI_CARGO_HELICOPTER self
-- @param #number speed Landing speed in kph(!), e.g. 15
-- @param #number height Landing height in meters(!), e.g. 5.5
-- @return #AI_CARGO_HELICOPTER self
-- @usage If your choppers get stuck, add tracing to your script to determine if they hit the right parameters like so:
--    
--        BASE:TraceOn()
--        BASE:TraceClass("AI_CARGO_HELICOPTER")
--        
-- Watch the DCS.log for entries stating `Helicopter:<name>, Height = Helicopter:<number>, Velocity = Helicopter:<number>`
-- Adjust if necessary.
function AI_CARGO_HELICOPTER:SetLandingSpeedAndHeight(speed, height)
  local _speed = speed or 15
  local _height = height or 5.5
  self.landingheight = _height
  self.landingspeed = _speed
  return self
end

-- @param #AI_CARGO_HELICOPTER self
-- @param Wrapper.Group#GROUP Helicopter
-- @param From
-- @param Event
-- @param To
function AI_CARGO_HELICOPTER:onafterLanded( Helicopter, From, Event, To )
  self:F({From, Event, To})
  Helicopter:F( { Name = Helicopter:GetName() } )

  if Helicopter and Helicopter:IsAlive() then

    -- S_EVENT_LAND is directly called in two situations:
    -- 1 - When the helo lands normally on the ground.
    -- 2 - when the helo is hit and goes RTB or even when it is destroyed.
    -- For point 2, this is an issue, the infantry may not unload in this case!
    -- So we check if the helo is on the ground, and velocity< 15.
    -- Only then the infantry can unload (and load too, for consistency)!

    self:T( { Helicopter:GetName(), Height = Helicopter:GetHeight( true ), Velocity = Helicopter:GetVelocityKMH() } )

    if self.RoutePickup == true then
      if Helicopter:GetHeight( true ) <= self.landingheight then --and Helicopter:GetVelocityKMH() < self.landingspeed then
        --self:Load( Helicopter:GetPointVec2() )
        self:Load( self.PickupZone )
        self.RoutePickup = false
      end
    end
    
    if self.RouteDeploy == true then
      if Helicopter:GetHeight( true ) <= self.landingheight then --and Helicopter:GetVelocityKMH() < self.landingspeed then
        self:Unload( self.DeployZone )
        self.RouteDeploy = false
      end
    end
     
  end
  
end

-- @param #AI_CARGO_HELICOPTER self
-- @param Wrapper.Group#GROUP Helicopter
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Speed
function AI_CARGO_HELICOPTER:onafterQueue( Helicopter, From, Event, To, Coordinate, Speed, DeployZone )
  self:F({From, Event, To, Coordinate, Speed, DeployZone})
  local HelicopterInZone = false

  if Helicopter and Helicopter:IsAlive() == true then
    
    local Distance = Coordinate:DistanceFromPointVec2( Helicopter:GetCoordinate() )
    
    if Distance > 2000 then
      self:__Queue( -10, Coordinate, Speed, DeployZone )
    else
    
      local ZoneFree = true

      for Helicopter, ZoneQueue in pairs( AI_CARGO_QUEUE ) do
        local ZoneQueue = ZoneQueue -- Core.Zone#ZONE_RADIUS
        if ZoneQueue:IsCoordinateInZone( Coordinate ) then
          ZoneFree = false
        end
      end
      
      self:F({ZoneFree=ZoneFree})
      
      if ZoneFree == true then
     
        local ZoneQueue = ZONE_RADIUS:New( Helicopter:GetName(), Coordinate:GetVec2(), 100 )
     
        AI_CARGO_QUEUE[Helicopter] = ZoneQueue 
      
        local Route = {}
        
--          local CoordinateFrom = Helicopter:GetCoordinate()
--          local WaypointFrom = CoordinateFrom:WaypointAir( 
--            "RADIO", 
--            COORDINATE.WaypointType.TurningPoint, 
--            COORDINATE.WaypointAction.TurningPoint, 
--            Speed, 
--            true 
--          )
--          Route[#Route+1] = WaypointFrom
        local CoordinateTo = Coordinate
        
        local landheight = CoordinateTo:GetLandHeight() -- get target height
        CoordinateTo.y = landheight + 50 -- flight height should be 50m above ground
    
        local WaypointTo = CoordinateTo:WaypointAir( 
          "RADIO", 
          COORDINATE.WaypointType.TurningPoint, 
          COORDINATE.WaypointAction.TurningPoint, 
          50, 
          true 
        )
        Route[#Route+1] = WaypointTo
        
        local Tasks = {}
        Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() )
        Route[#Route].task = Helicopter:TaskCombo( Tasks )
    
        Route[#Route+1] = WaypointTo
    
        -- Now route the helicopter
        Helicopter:Route( Route, 0 )
        
        -- Keep the DeployZone, because when the helo has landed, we want to provide the DeployZone to the mission designer as part of the Unloaded event.
        self.DeployZone = DeployZone
        
      else
        self:__Queue( -10, Coordinate, Speed, DeployZone )
      end
    end
  else
    AI_CARGO_QUEUE[Helicopter] = nil
  end
end


-- @param #AI_CARGO_HELICOPTER self
-- @param Wrapper.Group#GROUP Helicopter
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Speed
function AI_CARGO_HELICOPTER:onafterOrbit( Helicopter, From, Event, To, Coordinate )
  self:F({From, Event, To, Coordinate})
  
  if Helicopter and Helicopter:IsAlive() then
    
    local Route = {}
    
    local CoordinateTo = Coordinate
    local landheight = CoordinateTo:GetLandHeight() -- get target height
    CoordinateTo.y = landheight + 50 -- flight height should be 50m above ground
    
    local WaypointTo = CoordinateTo:WaypointAir("RADIO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, 50, true)
    Route[#Route+1] = WaypointTo
    
    local Tasks = {}
    Tasks[#Tasks+1] = Helicopter:TaskOrbitCircle( math.random( 30, 80 ), 150, CoordinateTo:GetRandomCoordinateInRadius( 800, 500 ) )
    Route[#Route].task = Helicopter:TaskCombo( Tasks )

    Route[#Route+1] = WaypointTo

    -- Now route the helicopter
    Helicopter:Route(Route, 0)
  end
end



--- On after Deployed event.
-- @param #AI_CARGO_HELICOPTER self
-- @param Wrapper.Group#GROUP Helicopter
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Cargo.Cargo#CARGO Cargo Cargo object.
-- @param #boolean Deployed Cargo is deployed.
-- @return #boolean True if all cargo has been unloaded.
function AI_CARGO_HELICOPTER:onafterDeployed( Helicopter, From, Event, To, DeployZone )
  self:F( { From, Event, To, DeployZone = DeployZone } )

  self:Orbit( Helicopter:GetCoordinate(), 50 )

 -- Free the coordinate zone after 30 seconds, so that the original helicopter can fly away first.
  self:ScheduleOnce( 30, 
    function( Helicopter )
      AI_CARGO_QUEUE[Helicopter] = nil
    end, Helicopter
  )
  
  self:GetParent( self, AI_CARGO_HELICOPTER ).onafterDeployed( self, Helicopter, From, Event, To, DeployZone )
  
end

--- On after Pickup event.
-- @param #AI_CARGO_HELICOPTER self
-- @param Wrapper.Group#GROUP Helicopter
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate Pickup place.
-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Height Height in meters to move to the pickup coordinate. This parameter is ignored for APCs.
-- @param Core.Zone#ZONE PickupZone (optional) The zone where the cargo will be picked up. The PickupZone can be nil, if there wasn't any PickupZoneSet provided.
function AI_CARGO_HELICOPTER:onafterPickup( Helicopter, From, Event, To, Coordinate, Speed, Height, PickupZone )
  self:F({Coordinate, Speed, Height, PickupZone })
  
  if Helicopter and Helicopter:IsAlive() ~= nil then

    Helicopter:Activate()

    self.RoutePickup = true
    Coordinate.y = Height
    
    local _speed=Speed or Helicopter:GetSpeedMax()*0.5        
     
    local Route = {}
    
    --- Calculate the target route point.
    local CoordinateFrom = Helicopter:GetCoordinate()

    --- Create a route point of type air.
    local WaypointFrom = CoordinateFrom:WaypointAir("RADIO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, _speed, true)

    --- Create a route point of type air.
    local CoordinateTo = Coordinate
    local landheight = CoordinateTo:GetLandHeight() -- get target height
    CoordinateTo.y = landheight + 50 -- flight height should be 50m above ground
    
    local WaypointTo = CoordinateTo:WaypointAir("RADIO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint,_speed, true)

    Route[#Route+1] = WaypointFrom
    Route[#Route+1] = WaypointTo
    
    --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable...
    Helicopter:WayPointInitialize( Route )
  
    local Tasks = {}
    
    Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() )
    Route[#Route].task = Helicopter:TaskCombo( Tasks )

    Route[#Route+1] = WaypointTo

    -- Now route the helicopter
    Helicopter:Route( Route, 1 )
    
    self.PickupZone = PickupZone

    self:GetParent( self, AI_CARGO_HELICOPTER ).onafterPickup( self, Helicopter, From, Event, To, Coordinate, Speed, Height, PickupZone )

  end
  
end

--- Depoloy function and queue.
-- @param #AI_CARGO_HELICOPTER self
-- @param Wrapper.Group#GROUP AICargoHelicopter
-- @param Core.Point#COORDINATE Coordinate Coordinate
function AI_CARGO_HELICOPTER:_Deploy( AICargoHelicopter, Coordinate, DeployZone )
  AICargoHelicopter:__Queue( -10, Coordinate, 100, DeployZone )
end

--- On after Deploy event.
-- @param #AI_CARGO_HELICOPTER self
-- @param Wrapper.Group#GROUP Helicopter Transport helicopter.
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate Place at which the cargo is deployed.
-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Height Height in meters to move to the deploy coordinate.
function AI_CARGO_HELICOPTER:onafterDeploy( Helicopter, From, Event, To, Coordinate, Speed, Height, DeployZone )
  self:F({From, Event, To, Coordinate, Speed, Height, DeployZone})
  if Helicopter and Helicopter:IsAlive() ~= nil then

    self.RouteDeploy = true

     
    local Route = {}
    
    --- Calculate the target route point.

    Coordinate.y = Height
    
    local _speed=Speed or Helicopter:GetSpeedMax()*0.5      

    --- Create a route point of type air.
    local CoordinateFrom = Helicopter:GetCoordinate()
    local WaypointFrom = CoordinateFrom:WaypointAir("RADIO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, _speed, true)
    Route[#Route+1] = WaypointFrom
    Route[#Route+1] = WaypointFrom

    --- Create a route point of type air.
    
    local CoordinateTo = Coordinate
    local landheight = CoordinateTo:GetLandHeight() -- get target height
    CoordinateTo.y = landheight + 50 -- flight height should be 50m above ground
    
    local WaypointTo = CoordinateTo:WaypointAir("RADIO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, _speed, true)

    Route[#Route+1] = WaypointTo
    Route[#Route+1] = WaypointTo
    
    --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable...
    Helicopter:WayPointInitialize( Route )
  
    local Tasks = {}
    
    -- The _Deploy function does not exist.
    Tasks[#Tasks+1] = Helicopter:TaskFunction( "AI_CARGO_HELICOPTER._Deploy", self, Coordinate, DeployZone )
    
    Tasks[#Tasks+1] = Helicopter:TaskOrbitCircle( math.random( 30, 100 ), _speed, CoordinateTo:GetRandomCoordinateInRadius( 800, 500 ) )
    
    --Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() )
    Route[#Route].task = Helicopter:TaskCombo( Tasks )

    Route[#Route+1] = WaypointTo

    -- Now route the helicopter
    Helicopter:Route( Route, 0 )

    self:GetParent( self, AI_CARGO_HELICOPTER ).onafterDeploy( self, Helicopter, From, Event, To, Coordinate, Speed, Height, DeployZone )
  end
  
end


--- On after Home event.
-- @param #AI_CARGO_HELICOPTER self
-- @param Wrapper.Group#GROUP Helicopter
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate Home place.
-- @param #number Speed Speed in km/h to fly to the pickup coordinate. Default is 50% of max possible speed the unit can go.
-- @param #number Height Height in meters to move to the home coordinate.
-- @param Core.Zone#ZONE HomeZone The zone wherein the carrier will return when all cargo has been transported. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
function AI_CARGO_HELICOPTER:onafterHome( Helicopter, From, Event, To, Coordinate, Speed, Height, HomeZone )
  self:F({From, Event, To, Coordinate, Speed, Height})
  
  if Helicopter and Helicopter:IsAlive() ~= nil then

    self.RouteHome = true
     
    local Route = {}
    
    --- Calculate the target route point.

    --Coordinate.y = Height
    Height = Height or 50
    
    Speed = Speed or Helicopter:GetSpeedMax()*0.5          

    --- Create a route point of type air.
    local CoordinateFrom = Helicopter:GetCoordinate()
    
    local WaypointFrom = CoordinateFrom:WaypointAir("RADIO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true)
    Route[#Route+1] = WaypointFrom

    --- Create a route point of type air.
    local CoordinateTo = Coordinate
    local landheight = CoordinateTo:GetLandHeight() -- get target height
    CoordinateTo.y = landheight + Height -- flight height should be 50m above ground
    
    local WaypointTo = CoordinateTo:WaypointAir("RADIO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, Speed, true)

    Route[#Route+1] = WaypointTo
    
    --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable...
    Helicopter:WayPointInitialize( Route )
  
    local Tasks = {}
    
    Tasks[#Tasks+1] = Helicopter:TaskLandAtVec2( CoordinateTo:GetVec2() )
    Route[#Route].task = Helicopter:TaskCombo( Tasks )
 
    Route[#Route+1] = WaypointTo

    -- Now route the helicopter
    Helicopter:Route(Route, 0)
  end
  
end

--- **AI** - Models the intelligent transportation of cargo using airplanes.
--
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_Cargo_Airplane
-- @image AI_Cargo_Dispatching_For_Airplanes.JPG

-- @type AI_CARGO_AIRPLANE
-- @extends Core.Fsm#FSM_CONTROLLABLE


--- Brings a dynamic cargo handling capability for an AI airplane group.
-- 
-- ![Banner Image](..\Images\deprecated.png)
--  
-- Airplane carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation between airbases.
-- 
-- The AI_CARGO_AIRPLANE module uses the @{Cargo.Cargo} capabilities within the MOOSE framework.
-- @{Cargo.Cargo} must be declared within the mission to make AI_CARGO_AIRPLANE recognize the cargo.
-- Please consult the @{Cargo.Cargo} module for more information. 
-- 
-- ## Cargo pickup.
--  
-- Using the @{#AI_CARGO_AIRPLANE.Pickup}() method, you are able to direct the helicopters towards a point on the battlefield to board/load the cargo at the specific coordinate. 
-- Ensure that the landing zone is horizontally flat, and that trees cannot be found in the landing vicinity, or the helicopters won't land or will even crash!
-- 
-- ## Cargo deployment.
--  
-- Using the @{#AI_CARGO_AIRPLANE.Deploy}() method, you are able to direct the helicopters towards a point on the battlefield to unboard/unload the cargo at the specific coordinate. 
-- Ensure that the landing zone is horizontally flat, and that trees cannot be found in the landing vicinity, or the helicopters won't land or will even crash!
-- 
-- ## Infantry health.
-- 
-- When infantry is unboarded from the APCs, the infantry is actually respawned into the battlefield. 
-- As a result, the unboarding infantry is very _healthy_ every time it unboards.
-- This is due to the limitation of the DCS simulator, which is not able to specify the health of new spawned units as a parameter.
-- However, infantry that was destroyed when unboarded, won't be respawned again. Destroyed is destroyed.
-- As a result, there is some additional strength that is gained when an unboarding action happens, but in terms of simulation balance this has
-- marginal impact on the overall battlefield simulation. Fortunately, the firing strength of infantry is limited, and thus, respacing healthy infantry every
-- time is not so much of an issue ... 
-- 
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- @field #AI_CARGO_AIRPLANE
AI_CARGO_AIRPLANE = {
  ClassName = "AI_CARGO_AIRPLANE",
  Coordinate = nil, -- Core.Point#COORDINATE
}

--- Creates a new AI_CARGO_AIRPLANE object.
-- @param #AI_CARGO_AIRPLANE self
-- @param Wrapper.Group#GROUP Airplane Plane used for transportation of cargo.
-- @param Core.Set#SET_CARGO CargoSet Cargo set to be transported.
-- @return #AI_CARGO_AIRPLANE
function AI_CARGO_AIRPLANE:New( Airplane, CargoSet )

  local self = BASE:Inherit( self, AI_CARGO:New( Airplane, CargoSet ) ) -- #AI_CARGO_AIRPLANE

  self:AddTransition( "*", "Landed", "*" )
  self:AddTransition( "*", "Home" ,  "*" ) 
  
  self:AddTransition( "*", "Destroyed", "Destroyed" )

  --- Pickup Handler OnBefore for AI_CARGO_AIRPLANE
  -- @function [parent=#AI_CARGO_AIRPLANE] OnBeforePickup
  -- @param #AI_CARGO_AIRPLANE self
  -- @param Wrapper.Group#GROUP Airplane Cargo transport plane.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where troops are picked up.
  -- @param #number Speed in km/h for travelling to pickup base.
  -- @return #boolean
  
  --- Pickup Handler OnAfter for AI_CARGO_AIRPLANE
  -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterPickup
  -- @param #AI_CARGO_AIRPLANE self
  -- @param Wrapper.Group#GROUP Airplane Cargo transport plane.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff.
  -- @param #number Speed Speed in km/h for travelling to pickup base.
  -- @param #number Height Height in meters to move to the pickup coordinate.
  -- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up.
  
  --- Pickup Trigger for AI_CARGO_AIRPLANE
  -- @function [parent=#AI_CARGO_AIRPLANE] Pickup
  -- @param #AI_CARGO_AIRPLANE self
  -- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff.
  -- @param #number Speed Speed in km/h for travelling to pickup base.
  -- @param #number Height Height in meters to move to the pickup coordinate.
  -- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up.
  
  --- Pickup Asynchronous Trigger for AI_CARGO_AIRPLANE
  -- @function [parent=#AI_CARGO_AIRPLANE] __Pickup
  -- @param #AI_CARGO_AIRPLANE self
  -- @param #number Delay Delay in seconds.
  -- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff.
  -- @param #number Speed Speed in km/h for travelling to pickup base.
  -- @param #number Height Height in meters to move to the pickup coordinate.
  -- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up.
  
  --- Deploy Handler OnBefore for AI_CARGO_AIRPLANE
  -- @function [parent=#AI_CARGO_AIRPLANE] OnBeforeDeploy
  -- @param #AI_CARGO_AIRPLANE self
  -- @param Wrapper.Group#GROUP Airplane Cargo plane.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Wrapper.Airbase#AIRBASE Airbase Destination airbase where troops are deployed.
  -- @param #number Speed Speed in km/h for travelling to deploy base.
  -- @return #boolean
  
  --- Deploy Handler OnAfter for AI_CARGO_AIRPLANE
  -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterDeploy
  -- @param #AI_CARGO_AIRPLANE self
  -- @param Wrapper.Group#GROUP Airplane Cargo plane.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff.
  -- @param #number Speed Speed in km/h for travelling to the deploy base.
  -- @param #number Height Height in meters to move to the home coordinate.
  -- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed.
  
  --- Deploy Trigger for AI_CARGO_AIRPLANE
  -- @function [parent=#AI_CARGO_AIRPLANE] Deploy
  -- @param #AI_CARGO_AIRPLANE self
  -- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff.
  -- @param #number Speed Speed in km/h for travelling to the deploy base.
  -- @param #number Height Height in meters to move to the home coordinate.
  -- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed. 
  
  --- Deploy Asynchronous Trigger for AI_CARGO_AIRPLANE
  -- @function [parent=#AI_CARGO_AIRPLANE] __Deploy
  -- @param #AI_CARGO_AIRPLANE self
  -- @param #number Delay Delay in seconds.
  -- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff.
  -- @param #number Speed Speed in km/h for travelling to the deploy base.
  -- @param #number Height Height in meters to move to the home coordinate.
  -- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed.

  --- On after Loaded event, i.e. triggered when the cargo is inside the carrier.
  -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterLoaded
  -- @param #AI_CARGO_AIRPLANE self
  -- @param Wrapper.Group#GROUP Airplane Cargo plane.
  -- @param From
  -- @param Event
  -- @param To


  --- On after Deployed event.
  -- @function [parent=#AI_CARGO_AIRPLANE] OnAfterDeployed
  -- @param #AI_CARGO_AIRPLANE self
  -- @param Wrapper.Group#GROUP Airplane Cargo plane.
  -- @param #string From From state.
  -- @param #string Event Event.
  -- @param #string To To state.
  -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed.
  
  -- Set carrier. 
  self:SetCarrier( Airplane )
  
  return self
end


--- Set the Carrier (controllable). Also initializes events for carrier and defines the coalition.
-- @param #AI_CARGO_AIRPLANE self
-- @param Wrapper.Group#GROUP Airplane Transport plane.
-- @return #AI_CARGO_AIRPLANE self
function AI_CARGO_AIRPLANE:SetCarrier( Airplane )

  local AICargo = self

  self.Airplane = Airplane -- Wrapper.Group#GROUP
  self.Airplane:SetState( self.Airplane, "AI_CARGO_AIRPLANE", self )

  self.RoutePickup = false
  self.RouteDeploy = false

  Airplane:HandleEvent( EVENTS.Dead )
  Airplane:HandleEvent( EVENTS.Hit )
  Airplane:HandleEvent( EVENTS.EngineShutdown )
  
  function Airplane:OnEventDead( EventData )
    local AICargoTroops = self:GetState( self, "AI_CARGO_AIRPLANE" )
    self:F({AICargoTroops=AICargoTroops})
    if AICargoTroops then
      self:F({})
      if not AICargoTroops:Is( "Loaded" ) then
        -- There are enemies within combat range. Unload the Airplane.
        AICargoTroops:Destroyed()
      end
    end
  end
  
  
  function Airplane:OnEventHit( EventData )
    local AICargoTroops = self:GetState( self, "AI_CARGO_AIRPLANE" )
    if AICargoTroops then
      self:F( { OnHitLoaded = AICargoTroops:Is( "Loaded" ) } )
      if AICargoTroops:Is( "Loaded" ) or AICargoTroops:Is( "Boarding" ) then
        -- There are enemies within combat range. Unload the Airplane.
        AICargoTroops:Unload()
      end
    end
  end
  
  
  function Airplane:OnEventEngineShutdown( EventData )
    AICargo.Relocating = false
    AICargo:Landed( self.Airplane )
  end
  
  self.Coalition = self.Airplane:GetCoalition()
  
  self:SetControllable( Airplane )

  return self
end


--- Find a free Carrier within a range.
-- @param #AI_CARGO_AIRPLANE self
-- @param Wrapper.Airbase#AIRBASE Airbase
-- @param #number Radius
-- @return Wrapper.Group#GROUP NewCarrier
function AI_CARGO_AIRPLANE:FindCarrier( Coordinate, Radius )

  local CoordinateZone = ZONE_RADIUS:New( "Zone" , Coordinate:GetVec2(), Radius )
  CoordinateZone:Scan( { Object.Category.UNIT } )
  for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do
    local NearUnit = UNIT:Find( DCSUnit )
    self:F({NearUnit=NearUnit})
    if not NearUnit:GetState( NearUnit, "AI_CARGO_AIRPLANE" ) then
      local Attributes = NearUnit:GetDesc()
      self:F({Desc=Attributes})
      if NearUnit:HasAttribute( "Trucks" ) then
        self:SetCarrier( NearUnit )
        break
      end
    end
  end

end

--- On after "Landed" event. Called on engine shutdown and initiates the pickup mission or unloading event.
-- @param #AI_CARGO_AIRPLANE self
-- @param Wrapper.Group#GROUP Airplane Cargo transport plane.
-- @param From
-- @param Event
-- @param To
function AI_CARGO_AIRPLANE:onafterLanded( Airplane, From, Event, To )

  self:F({Airplane, From, Event, To})

  if Airplane and Airplane:IsAlive()~=nil then

    -- Aircraft was sent to this airbase to pickup troops. Initiate loadling.
    if self.RoutePickup == true then
      self:Load( self.PickupZone )
    end
    
    -- Aircraft was send to this airbase to deploy troops. Initiate unloading.
    if self.RouteDeploy == true then
      self:Unload()
      self.RouteDeploy = false
    end
     
  end
  
end


--- On after "Pickup" event. Routes transport to pickup airbase.
-- @param #AI_CARGO_AIRPLANE self
-- @param Wrapper.Group#GROUP Airplane Cargo transport plane.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Coordinate The coordinate where to pickup stuff.
-- @param #number Speed Speed in km/h for travelling to pickup base.
-- @param #number Height Height in meters to move to the pickup coordinate.
-- @param Core.Zone#ZONE_AIRBASE PickupZone The airbase zone where the cargo will be picked up.
function AI_CARGO_AIRPLANE:onafterPickup( Airplane, From, Event, To, Coordinate, Speed, Height, PickupZone )

  if Airplane and Airplane:IsAlive() then
    
    local airbasepickup=Coordinate:GetClosestAirbase()
    
    self.PickupZone = PickupZone or ZONE_AIRBASE:New(airbasepickup:GetName())
  
    -- Get closest airbase of current position.
    local ClosestAirbase, DistToAirbase=Airplane:GetCoordinate():GetClosestAirbase()
  
    -- Two cases. Aircraft spawned in air or at an airbase.
    if Airplane:InAir() then
      self.Airbase=nil  --> route will start in air
    else      
      self.Airbase=ClosestAirbase
    end
    
    -- Set pickup airbase.
    local Airbase = self.PickupZone:GetAirbase()
    
    -- Distance from closest to pickup airbase ==> we need to know if we are already at the pickup airbase. 
    local Dist = Airbase:GetCoordinate():Get2DDistance(ClosestAirbase:GetCoordinate())
    
    if Airplane:InAir() or Dist>500 then
    
      -- Route aircraft to pickup airbase.
      self:Route( Airplane, Airbase, Speed, Height ) 
          
      -- Set airbase as starting point in the next Route() call.
      self.Airbase = Airbase
      
      -- Aircraft is on a pickup mission.
      self.RoutePickup = true
      
    else
    
      -- We are already at the right airbase ==> Landed ==> triggers loading of troops. Is usually called at engine shutdown event.
      self.RoutePickup=true
      self:Landed()
      
    end

    self:GetParent( self, AI_CARGO_AIRPLANE ).onafterPickup( self, Airplane, From, Event, To, Coordinate, Speed, Height, self.PickupZone )
    
  end

  
end

--- On after Depoly event. Routes plane to the airbase where the troops are deployed.
-- @param #AI_CARGO_AIRPLANE self
-- @param Wrapper.Group#GROUP Airplane Cargo transport plane.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Point#COORDINATE Coordinate Coordinate where to deploy stuff.
-- @param #number Speed Speed in km/h for travelling to the deploy base.
-- @param #number Height Height in meters to move to the home coordinate.
-- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed.
function AI_CARGO_AIRPLANE:onafterDeploy( Airplane, From, Event, To, Coordinate, Speed, Height, DeployZone )

  if Airplane and Airplane:IsAlive()~=nil then
    
    local Airbase = Coordinate:GetClosestAirbase()
    
    if DeployZone then
      Airbase=DeployZone:GetAirbase()
    end
    
    -- Activate uncontrolled airplane.
    if Airplane:IsAlive()==false then
      Airplane:SetCommand({id = 'Start', params = {}})
    end
    
    -- Route to destination airbase.
    self:Route( Airplane, Airbase, Speed, Height )
    
    -- Aircraft is on a depoly mission.
    self.RouteDeploy = true
    
    -- Set destination airbase for next :Route() command.
    self.Airbase = Airbase
    
    self:GetParent( self, AI_CARGO_AIRPLANE ).onafterDeploy( self, Airplane, From, Event, To, Coordinate, Speed, Height, DeployZone )
  end
  
end


--- On after Unload event. Cargo is beeing unloaded, i.e. the unboarding process is started.
-- @param #AI_CARGO_AIRPLANE self
-- @param Wrapper.Group#GROUP Airplane Cargo transport plane.
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE_AIRBASE DeployZone The airbase zone where the cargo will be deployed.
function AI_CARGO_AIRPLANE:onafterUnload( Airplane, From, Event, To, DeployZone )

  local UnboardInterval = 10
  local UnboardDelay = 10

  if Airplane and Airplane:IsAlive() then
    for _, AirplaneUnit in pairs( Airplane:GetUnits() ) do
      local Cargos = AirplaneUnit:GetCargo()
      for CargoID, Cargo in pairs( Cargos ) do
      
        local Angle = 180
        local CargoCarrierHeading = Airplane:GetHeading() -- Get Heading of object in degrees.
        local CargoDeployHeading = ( ( CargoCarrierHeading + Angle ) >= 360 ) and ( CargoCarrierHeading + Angle - 360 ) or ( CargoCarrierHeading + Angle )
        self:T( { CargoCarrierHeading, CargoDeployHeading } )
        local CargoDeployCoordinate = Airplane:GetPointVec2():Translate( 150, CargoDeployHeading )
      
         Cargo:__UnBoard( UnboardDelay, CargoDeployCoordinate )
         UnboardDelay = UnboardDelay + UnboardInterval
         Cargo:SetDeployed( true )
         self:__Unboard( UnboardDelay, Cargo, AirplaneUnit, DeployZone ) 
      end
    end
  end
  
end

--- Route the airplane from one airport or it's current position to another airbase.
-- @param #AI_CARGO_AIRPLANE self
-- @param Wrapper.Group#GROUP Airplane Airplane group to be routed.
-- @param Wrapper.Airbase#AIRBASE Airbase Destination airbase.
-- @param #number Speed Speed in km/h. Default is 80% of max possible speed the group can do.
-- @param #number Height Height in meters to move to the Airbase.
-- @param #boolean Uncontrolled If true, spawn group in uncontrolled state.
function AI_CARGO_AIRPLANE:Route( Airplane, Airbase, Speed, Height, Uncontrolled )

  if Airplane and Airplane:IsAlive() then

    -- Set takeoff type.
    local Takeoff = SPAWN.Takeoff.Cold
    
    -- Get template of group.
    local Template = Airplane:GetTemplate()
    
    -- Nil check
    if Template==nil then
      return
    end

    -- Waypoints of the route.
    local Points={}
    
    -- To point.
    local AirbasePointVec2 = Airbase:GetPointVec2()
    local ToWaypoint = AirbasePointVec2:WaypointAir(COORDINATE.WaypointAltType.BARO, "Land", "Landing", Speed or Airplane:GetSpeedMax()*0.8, true, Airbase)
        
    --ToWaypoint["airdromeId"]   = Airbase:GetID()
    --ToWaypoint["speed_locked"] = true
    

    -- If self.Airbase~=nil then group is currently at an airbase, where it should be respawned.        
    if self.Airbase then
    
      -- Second point of the route. First point is done in RespawnAtCurrentAirbase() routine.
      Template.route.points[2] = ToWaypoint
    
      -- Respawn group at the current airbase.
      Airplane:RespawnAtCurrentAirbase(Template, Takeoff, Uncontrolled)
      
    else
  
      -- From point.
      local GroupPoint = Airplane:GetVec2()
      local FromWaypoint = {}
      FromWaypoint.x      = GroupPoint.x
      FromWaypoint.y      = GroupPoint.y
      FromWaypoint.type   = "Turning Point"
      FromWaypoint.action = "Turning Point"
      FromWaypoint.speed  = Airplane:GetSpeedMax()*0.8
 
      -- The two route points. 
      Points[1] = FromWaypoint
      Points[2] = ToWaypoint

      local PointVec3 = Airplane:GetPointVec3()
      Template.x = PointVec3.x
      Template.y = PointVec3.z
 
      Template.route.points = Points
            
      local GroupSpawned = Airplane:Respawn(Template)
    
    end
  end
end

--- On after Home event. Aircraft will be routed to their home base.
-- @param #AI_CARGO_AIRPLANE self
-- @param Wrapper.Group#GROUP Airplane The cargo plane.
-- @param From From state.
-- @param Event Event.
-- @param To To State.
-- @param Core.Point#COORDINATE Coordinate Home place (not used).
-- @param #number Speed Speed in km/h to fly to the home airbase (zone). Default is 80% of max possible speed the unit can go.
-- @param #number Height Height in meters to move to the home coordinate.
-- @param Core.Zone#ZONE_AIRBASE HomeZone The home airbase (zone) where the plane should return to.
function AI_CARGO_AIRPLANE:onafterHome(Airplane, From, Event, To, Coordinate, Speed, Height, HomeZone )
  if Airplane and Airplane:IsAlive() then

    -- We are going home!
    self.RouteHome = true
       
    -- Home Base.
    local HomeBase=HomeZone:GetAirbase()
    self.Airbase=HomeBase
    
    -- Now route the airplane home
   self:Route( Airplane, HomeBase, Speed, Height )
    
  end
  
end
--- **AI** - Models the intelligent transportation of infantry and other cargo.
--
-- ===
--
-- ### Author: **acrojason** (derived from AI_Cargo_APC by FlightControl)
--
-- ===
--
-- @module AI.AI_Cargo_Ship
-- @image AI_Cargo_Dispatcher.JPG

-- @type AI_CARGO_SHIP
-- @extends AI.AI_Cargo#AI_CARGO

--- Brings a dynamic cargo handling capability for an AI naval group.
--
-- ![Banner Image](..\Images\deprecated.png)
--
-- Naval ships can be utilized to transport cargo around the map following naval shipping lanes.
-- The AI_CARGO_SHIP class uses the @{Cargo.Cargo} capabilities within the MOOSE framework. 
-- @{Cargo.Cargo} must be declared within the mission or warehouse to make the AI_CARGO_SHIP recognize the cargo.
-- Please consult the @{Cargo.Cargo} module for more information.
--
-- ## Cargo loading.
-- 
-- The module will automatically load cargo when the Ship is within boarding or loading radius. 
-- The boarding or loading radius is specified when the cargo is created in the simulation and depends on the type of 
-- cargo and the specified boarding radius.
--
-- ## Defending the Ship when enemies are nearby
-- This is not supported for naval cargo because most tanks don't float. Protect your transports...
--
-- ## Infantry or cargo **health**.
-- When cargo is unboarded from the Ship, the cargo is actually respawned into the battlefield.
-- As a result, the unboarding cargo is very _healthy_ every time it unboards.
-- This is due to the limitation of the DCS simulator, which is not able to specify the health of newly spawned units as a parameter.
-- However, cargo that was destroyed when unboarded and following the Ship won't be respawned again (this is likely not a thing for
-- naval cargo due to the lack of support for defending the Ship mentioned above). Destroyed is destroyed.
-- As a result, there is some additional strength that is gained when an unboarding action happens, but in terms of simulation balance
-- this has marginal impact on the overall battlefield simulation. Given the relatively short duration of DCS missions and the somewhat
-- lengthy naval transport times, most units entering the Ship as cargo will be freshly en route to an amphibious landing or transporting 
-- between warehouses.
--
-- ## Control the Ships on the map.
-- 
-- Currently, naval transports can only be controlled via scripts due to their reliance upon predefined Shipping Lanes created in the Mission
-- Editor. An interesting future enhancement could leverage new pathfinding functionality for ships in the Ops module.
--
-- ## Cargo deployment.
--
-- Using the @{#AI_CARGO_SHIP.Deploy}() method, you are able to direct the Ship towards a Deploy zone to unboard/unload the cargo at the
-- specified coordinate. The Ship will follow the Shipping Lane to ensure consistent cargo transportation within the simulation environment.
--
-- ## Cargo pickup.
--
-- Using the @{#AI_CARGO_SHIP.Pickup}() method, you are able to direct the Ship towards a Pickup zone to board/load the cargo at the specified
-- coordinate. The Ship will follow the Shipping Lane to ensure consistent cargo transportation within the simulation environment.
--
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- @field #AI_CARGO_SHIP
AI_CARGO_SHIP = {
    ClassName = "AI_CARGO_SHIP",
    Coordinate = nil -- Core.Point#COORDINATE
}

--- Creates a new AI_CARGO_SHIP object.
-- @param #AI_CARGO_SHIP self
-- @param Wrapper.Group#GROUP Ship  The carrier Ship group
-- @param Core.Set#SET_CARGO CargoSet  The set of cargo to be transported
-- @param #number CombatRadius  Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby. When CombatRadius is 0, no defense will occur.
-- @param #table ShippingLane  Table containing list of Shipping Lanes to be used
-- @return #AI_CARGO_SHIP
function AI_CARGO_SHIP:New( Ship, CargoSet, CombatRadius, ShippingLane )

    local self = BASE:Inherit( self, AI_CARGO:New( Ship, CargoSet ) ) -- #AI_CARGO_SHIP

    self:AddTransition( "*", "Monitor", "*" )
    self:AddTransition( "*", "Destroyed", "Destroyed" )
    self:AddTransition( "*", "Home", "*" )

    self:SetCombatRadius( 0 )  -- Don't want to deploy cargo in middle of water to defend Ship, so set CombatRadius to 0
    self:SetShippingLane ( ShippingLane )

    self:SetCarrier( Ship )

    return self
end

--- Set the Carrier
-- @param #AI_CARGO_SHIP self
-- @param Wrapper.Group#GROUP CargoCarrier
-- @return #AI_CARGO_SHIP
function AI_CARGO_SHIP:SetCarrier( CargoCarrier )
    self.CargoCarrier = CargoCarrier -- Wrapper.Group#GROUIP
    self.CargoCarrier:SetState( self.CargoCarrier, "AI_CARGO_SHIP", self )

    CargoCarrier:HandleEvent( EVENTS.Dead )

    function CargoCarrier:OnEventDead( EventData )
        self:F({"dead"})
        local AICargoTroops = self:GetState( self, "AI_CARGO_SHIP" )
        self:F({AICargoTroops=AICargoTroops})
        if AICargoTroops then
            self:F({})
            if not AICargoTroops:Is( "Loaded" ) then
                -- Better hope they can swim!
                AICargoTroops:Destroyed()
            end
        end
    end

    self.Zone = ZONE_UNIT:New( self.CargoCarrier:GetName() .. "-Zone", self.CargoCarrier, self.CombatRadius )
    self.Coalition = self.CargoCarrier:GetCoalition()

    self:SetControllable( CargoCarrier )

    return self
end


--- FInd a free Carrier within a radius
-- @param #AI_CARGO_SHIP self
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Radius
-- @return Wrapper.Group#GROUP NewCarrier
function AI_CARGO_SHIP:FindCarrier( Coordinate, Radius )

    local CoordinateZone = ZONE_RADIUS:New( "Zone", Coordinate:GetVec2(), Radius )
    CoordinateZone:Scan( { Object.Category.UNIT } )
    for _, DCSUnit in pairs( CoordinateZone:GetScannedUnits() ) do
        local NearUnit = UNIT:Find( DCSUnit )
        self:F({NearUnit=NearUnit})
        if not NearUnit:GetState( NearUnit, "AI_CARGO_SHIP" ) then
            local Attributes = NearUnit:GetDesc()
            self:F({Desc=Attributes})
            if NearUnit:HasAttributes( "Trucks" ) then
                return NearUnit:GetGroup()
            end
        end
    end

    return nil
end

function AI_CARGO_SHIP:SetShippingLane( ShippingLane )
    self.ShippingLane = ShippingLane

    return self
end

function AI_CARGO_SHIP:SetCombatRadius( CombatRadius )
    self.CombatRadius = CombatRadius or 0

    return self
end


--- Follow Infantry to the Carrier
-- @param #AI_CARGO_SHIP self
-- @param #AI_CARGO_SHIP Me
-- @param Wrapper.Unit#UNIT ShipUnit
-- @param Cargo.CargoGroup#CARGO_GROUP Cargo
-- @return #AI_CARGO_SHIP
function AI_CARGO_SHIP:FollowToCarrier( Me, ShipUnit, CargoGroup )

    local InfantryGroup = CargoGroup:GetGroup()

    self:F( { self=self:GetClassNameAndID(), InfantryGroup = InfantryGroup:GetName() } )

    if ShipUnit:IsAlive() then
        -- Check if the Cargo is near the CargoCarrier
        if InfantryGroup:IsPartlyInZone( ZONE_UNIT:New( "Radius", ShipUnit, 1000 ) ) then

            -- Cargo does not need to navigate to Carrier
            Me:Guard()
        else

            self:F( { InfantryGroup = InfantryGroup:GetName() } )
            if InfantryGroup:IsAlive() then

                self:F( { InfantryGroup = InfantryGroup:GetName() } )
                local Waypoints = {}

                -- Calculate new route
                local FromCoord = InfantryGroup:GetCoordinate()
                local FromGround = FromCoord:WaypointGround( 10, "Diamond" )
                self:F({FromGround=FromGround})
                table.insert( Waypoints, FromGround )

                local ToCoord = ShipUnit:GetCoordinate():GetRandomCoordinateInRadius( 10, 5 )
                local ToGround = ToCoord:WaypointGround( 10, "Diamond" )
                self:F({ToGround=ToGround})
                table.insert( Waypoints, ToGround )

                local TaskRoute = InfantryGroup:TaskFunction( "AI_CARGO_SHIP.FollowToCarrier", Me, ShipUnit, CargoGroup )

                self:F({Waypoints=Waypoints})
                local Waypoint = Waypoints[#Waypoints]
                InfantryGroup:SetTaskWaypoint( Waypoint, TaskRoute ) -- Set for the given Route at Waypoint 2 the TaskRouteToZone

                InfantryGroup:Route( Waypoints, 1 ) -- Move after a random number of seconds to the Route.  See Route method for details
            end
        end
    end
end


function AI_CARGO_SHIP:onafterMonitor( Ship, From, Event, To )
    self:F( { Ship, From, Event, To, IsTransporting = self:IsTransporting() } )

    if self.CombatRadius > 0 then
        -- We really shouldn't find ourselves in here for Ships since the CombatRadius should always be 0.
        -- This is to avoid Unloading the Ship in the middle of the sea.
        if Ship and Ship:IsAlive() then
            if self.CarrierCoordinate then
                if self:IsTransporting() == true then
                    local Coordinate = Ship:GetCoordinate()
                        if self:Is( "Unloaded" ) or self:Is( "Loaded" ) then
                            self.Zone:Scan( { Object.Category.UNIT } )
                            if self.Zone:IsAllInZoneOfCoalition( self.Coalition ) then
                                if self:Is( "Unloaded" ) then
                                    -- There are no enemies within combat radius. Reload the CargoCarrier.
                                    self:Reload()
                                end
                            else
                                if self:Is( "Loaded" ) then
                                    -- There are enemies within combat radius. Unload the CargoCarrier.
                                    self:__Unload( 1, nil, true ) -- The 2nd parameter is true, which means that the unload is for defending the carrier, not to deploy!
                                else
                                    if self:Is( "Unloaded" ) then
                                    --self:Follow()
                                    end
                                    self:F( "I am here" .. self:GetCurrentState() )
                                    if self:Is( "Following" ) then
                                        for Cargo, ShipUnit in pairs( self.Carrier_Cargo ) do
                                            local Cargo = Cargo -- Cargo.Cargo#CARGO
                                            local ShipUnit = ShipUnit -- Wrapper.Unit#UNIT
                                            if Cargo:IsAlive() then
                                                if not Cargo:IsNear( ShipUnit, 40 ) then
                                                    ShipUnit:RouteStop()
                                                    self.CarrierStopped = true
                                                else
                                                    if self.CarrierStopped then
                                                        if Cargo:IsNear( ShipUnit, 25 ) then
                                                            ShipUnit:RouteResume()
                                                            self.CarrierStopped = nil
                                                        end
                                                    end
                                                end
                                            end
                                        end
                                    end
                                end
                            end
                        end
                    end
                end
            self.CarrierCoordinate = Ship:GetCoordinate()
        end
        self:__Monitor( -5 )
    end
end

--- Check if cargo ship is alive and trigger Load event
-- @param Wrapper.Group#Group Ship
-- @param #AI_CARGO_SHIP self
function AI_CARGO_SHIP._Pickup( Ship, self, Coordinate, Speed, PickupZone )
    
    Ship:F( { "AI_CARGO_Ship._Pickup:", Ship:GetName() } )

    if Ship:IsAlive() then
        self:Load( PickupZone )
    end
end

--- Check if cargo ship is alive and trigger Unload event. Good time to remind people that Lua is case sensitive and Unload != UnLoad
-- @param Wrapper.Group#GROUP Ship
-- @param #AI_CARGO_SHIP self
function AI_CARGO_SHIP._Deploy( Ship, self, Coordinate, DeployZone )
    Ship:F( { "AI_CARGO_Ship._Deploy:", Ship } )

    if Ship:IsAlive() then
        self:Unload( DeployZone )
    end
end

--- on after Pickup event.
-- @param AI_CARGO_SHIP Ship
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE  Coordinate of the pickup point
-- @param #number Speed  Speed in km/h to sail to the pickup coordinate. Default is 50% of max speed for the unit
-- @param #number Height  Altitude in meters to move to the pickup coordinate. This parameter is ignored for Ships
-- @param Core.Zone#ZONE PickupZone (optional)  The zone where the cargo will be picked up. The PickupZone can be nil if there was no PickupZoneSet provided
function AI_CARGO_SHIP:onafterPickup( Ship, From, Event, To, Coordinate, Speed, Height, PickupZone )
    
    if Ship and Ship:IsAlive() then
        AI_CARGO_SHIP._Pickup( Ship, self, Coordinate, Speed, PickupZone )
        self:GetParent( self, AI_CARGO_SHIP ).onafterPickup( self, Ship, From, Event, To, Coordinate, Speed, Height, PickupZone )
    end
end

--- On after Deploy event.
-- @param #AI_CARGO_SHIP self
-- @param Wrapper.Group#GROUP SHIP
-- @param From
-- @param Event
-- @param To
-- @param Core.Point#COORDINATE Coordinate  Coordinate of the deploy point
-- @param #number Speed  Speed in km/h to sail to the deploy coordinate. Default is 50% of max speed for the unit
-- @param #number Height  Altitude in meters to move to the deploy coordinate. This parameter is ignored for Ships
-- @param Core.Zone#ZONE  DeployZone The zone where the cargo will be deployed.
function AI_CARGO_SHIP:onafterDeploy( Ship, From, Event, To, Coordinate, Speed, Height, DeployZone )

  if Ship and Ship:IsAlive() then
    
    Speed = Speed or Ship:GetSpeedMax()*0.8
    local lane = self.ShippingLane

    if lane then
      local Waypoints = {}

      for i=1, #lane do
        local coord = lane[i]
        local Waypoint = coord:WaypointGround(_speed)
        table.insert(Waypoints, Waypoint)
      end

      local TaskFunction = Ship:TaskFunction( "AI_CARGO_SHIP._Deploy", self, Coordinate, DeployZone )
      local Waypoint = Waypoints[#Waypoints]
      Ship:SetTaskWaypoint( Waypoint, TaskFunction )
      Ship:Route(Waypoints, 1)  
      self:GetParent( self, AI_CARGO_SHIP ).onafterDeploy( self, Ship, From, Event, To, Coordinate, Speed, Height, DeployZone )
    else
      self:E(self.lid.."ERROR: No shipping lane defined for Naval Transport!")
    end
  end
end

--- On after Unload event.
-- @param #AI_CARGO_SHIP self
-- @param Wrapper.Group#GROUP Ship
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
function AI_CARGO_SHIP:onafterUnload( Ship, From, Event, To, DeployZone, Defend )
    self:F( { Ship, From, Event, To, DeployZone, Defend = Defend } )
  
    local UnboardInterval = 5
    local UnboardDelay = 5
  
    if Ship and Ship:IsAlive() then
      for _, ShipUnit in pairs( Ship:GetUnits() ) do
        local ShipUnit = ShipUnit -- Wrapper.Unit#UNIT
        Ship:RouteStop()
        for _, Cargo in pairs( ShipUnit:GetCargo() ) do
          self:F( { Cargo = Cargo:GetName(), Isloaded = Cargo:IsLoaded() } )
          if Cargo:IsLoaded() then
            local unboardCoord = DeployZone:GetRandomPointVec2()
            Cargo:__UnBoard( UnboardDelay, unboardCoord, 1000)
            UnboardDelay = UnboardDelay + Cargo:GetCount() * UnboardInterval
            self:__Unboard( UnboardDelay, Cargo, ShipUnit, DeployZone, Defend )
            if not Defend == true then
              Cargo:SetDeployed( true )
            end
          end 
        end
      end
    end
end

function AI_CARGO_SHIP:onafterHome( Ship, From, Event, To, Coordinate, Speed, Height, HomeZone )
    if Ship and Ship:IsAlive() then
        
        self.RouteHome = true
        Speed = Speed or Ship:GetSpeedMax()*0.8
        local lane = self.ShippingLane
 
        if lane then
            local Waypoints = {}

            -- Need to find a more generalized way to do this instead of reversing the shipping lane.
            -- This only works if the Source/Dest route waypoints are numbered 1..n and not n..1
            for i=#lane, 1, -1 do
                local coord = lane[i]
                local Waypoint = coord:WaypointGround(_speed)
                table.insert(Waypoints, Waypoint)
            end

            local Waypoint = Waypoints[#Waypoints]
            Ship:Route(Waypoints, 1)
        
        else
            self:E(self.lid.."ERROR: No shipping lane defined for Naval Transport!")
        end
    end
end
--- **AI** - Models the intelligent transportation of infantry and other cargo.
--
-- ## Features:
-- 
--   * AI_CARGO_DISPATCHER is the **base class** for:
--   
--     * @{AI.AI_Cargo_Dispatcher_APC#AI_CARGO_DISPATCHER_APC}
--     * @{AI.AI_Cargo_Dispatcher_Helicopter#AI_CARGO_DISPATCHER_HELICOPTER}
--     * @{AI.AI_Cargo_Dispatcher_Airplane#AI_CARGO_DISPATCHER_AIRPLANE}
--     
--   * Provides the facilities to transport cargo over the battle field for the above classes.
--   * Dispatches transport tasks to a common set of cargo transporting groups.
--   * Different options can be setup to tweak the cargo transporation behaviour.
-- 
-- ===
-- 
-- ## Test Missions:
-- 
-- Test missions can be located on the main GITHUB site.
-- 
-- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AI/AI_Cargo_Dispatcher)
-- 
-- ===
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- # The dispatcher concept.
-- 
-- Carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation.
-- The AI_CARGO_DISPATCHER module uses the @{Cargo.Cargo} capabilities within the MOOSE framework, to enable Carrier GROUP objects 
-- to transport @{Cargo.Cargo} towards several deploy zones.
-- @{Cargo.Cargo} must be declared within the mission to make the AI_CARGO_DISPATCHER object recognize the cargo.
-- Please consult the @{Cargo.Cargo} module for more information. 
-- 
-- 
-- ## Why cargo dispatching?
-- 
-- It provides a realistic way of distributing your army forces around the battlefield, and to provide a quick means of cargo transportation.
-- Instead of having troops or cargo to "appear" suddenly at certain locations, the dispatchers will pickup the cargo and transport it.
-- It also allows to enforce or retreat your army from certain zones when needed, using helicopters or APCs.
-- Airplanes can transport cargo over larger distances between the airfields.
-- 
-- 
-- ## What is a cargo object then?
-- 
-- In order to make use of the MOOSE cargo system, you need to **declare** the DCS objects as MOOSE cargo objects!
-- This sounds complicated, but it is actually quite simple.
-- 
-- See here an example:
-- 
--     local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 )
--     
-- The above code declares a MOOSE cargo object called `EngineerCargoGroup`.
-- It actually just refers to an infantry group created within the sim called `"Engineers"`.
-- The infantry group now becomes controlled by the MOOSE cargo object `EngineerCargoGroup`.
-- A MOOSE cargo object also has properties, like the type of cargo, the logical name, and the reporting range.
-- 
-- For more information, please consult the @{Cargo.Cargo} module documentation. Please read through it, because it will explain how to setup the cargo objects for use
-- within your dispatchers.
-- 
-- 
-- ## Do I need to do a lot of coding to setup a dispatcher?
-- 
-- No! It requires a bit of studying to set it up, but once you understand the different components that use the cargo dispatcher, it becomes very easy.
-- Also, the dispatchers work in a true dynamic environment. The carriers and cargo, pickup and deploy zones can be created dynamically in your mission,
-- and will automatically be recognized by the dispatcher.
-- 
-- 
-- ## Is the dispatcher causing a lot of CPU overhead?
-- 
-- A little yes, but once the cargo is properly loaded into the carrier, the CPU consumption is very little.
-- When infantry or vehicles board into a carrier, or unboard from a carrier, you may perceive certain performance lags.
-- We are working to minimize the impact of those.
-- That being said, the DCS simulator is limited. It is just impossible to deploy hundreds of cargo over the battlefield, hundreds of helicopters transporting, 
-- without any performance impact. The amount of helicopters that are active and flying in your simulation influences more the performance than the dispatchers.
-- It really comes down to trying it out and getting experienced with what is possible and what is not (or too much).
-- 
-- 
-- ## Are the dispatchers a "black box" in terms of the logic?
-- 
-- No. You can tailor the dispatcher mechanisms using event handlers, and create additional logic to enhance the behaviour and dynamism in your own mission.
-- The events are listed below, and so are the options, but here are a couple of examples of what is possible:
-- 
--    * You could handle the **Deployed** event, when all the cargo is unloaded from a carrier in the dispatcher.
--      Adding your own code to the event handler, you could move the deployed cargo (infantry) to specific points to engage in the battlefield.
--    
--    * When a carrier is picking up cargo, the *Pickup** event is triggered, and you can inform the coalition of this event, 
--      because it is an indication that troops are planned to join.
-- 
--    
-- ## Are there options that you can set to modify the behaviour of the carries?
-- 
-- Yes, there are options to configure:
-- 
--    * the location where carriers will park or land near the cargo for pickup. 
--    * the location where carriers will park or land in the deploy zone for cargo deployment.
--    * the height for airborne carriers when they fly to and from pickup and deploy zones.
--    * the speed of the carriers. This is an important parameter, because depending on the tactication situation, speed will influence the detection by radars.
-- 
-- 
-- ## Can the zones be of any zone type?
-- 
-- Yes, please ensure that the zones are declared using the @{Core.Zone} classes.
-- Possible zones that function at the moment are ZONE, ZONE_GROUP, ZONE_UNIT, ZONE_POLYGON.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_Cargo_Dispatcher
-- @image AI_Cargo_Dispatcher.JPG


-- @type AI_CARGO_DISPATCHER
-- @field Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. 
-- @field Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects.
-- @field Core.Zone#SET_ZONE PickupZoneSet The set of pickup zones, which are used to where the cargo can be picked up by the carriers. If nil, then cargo can be picked up everywhere. 
-- @field Core.Zone#SET_ZONE DeployZoneSet The set of deploy zones, which are used to where the cargo will be deployed by the carriers. 
-- @field #number PickupMaxSpeed The maximum speed to move to the cargo pickup location.
-- @field #number PickupMinSpeed The minimum speed to move to the cargo pickup location.
-- @field #number DeployMaxSpeed The maximum speed to move to the cargo deploy location.
-- @field #number DeployMinSpeed The minimum speed to move to the cargo deploy location.
-- @field #number PickupMaxHeight The maximum height to fly to the cargo pickup location.
-- @field #number PickupMinHeight The minimum height to fly to the cargo pickup location.
-- @field #number DeployMaxHeight The maximum height to fly to the cargo deploy location.
-- @field #number DeployMinHeight The minimum height to fly to the cargo deploy location.
-- @field #number PickupOuterRadius The outer radius in meters around the cargo coordinate to pickup the cargo.
-- @field #number PickupInnerRadius The inner radius in meters around the cargo coordinate to pickup the cargo.
-- @field #number DeployOuterRadius The outer radius in meters around the cargo coordinate to deploy the cargo.
-- @field #number DeployInnerRadius The inner radius in meters around the cargo coordinate to deploy the cargo.
-- @field Core.Zone#ZONE_BASE HomeZone The home zone where the carriers will return when there is no more cargo to pickup.
-- @field #number MonitorTimeInterval The interval in seconds when the cargo dispatcher will search for new cargo to be picked up.
-- @extends Core.Fsm#FSM


--- A dynamic cargo handling capability for AI groups.
-- 
-- ---   
-- 
-- Carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation.
-- The AI_CARGO_DISPATCHER module uses the @{Cargo.Cargo} capabilities within the MOOSE framework, to enable Carrier GROUP objects 
-- to transport @{Cargo.Cargo} towards several deploy zones.
-- @{Cargo.Cargo} must be declared within the mission to make the AI_CARGO_DISPATCHER object recognize the cargo.
-- Please consult the @{Cargo.Cargo} module for more information. 
-- 
-- # 1) AI_CARGO_DISPATCHER constructor.
--   
--   * @{#AI_CARGO_DISPATCHER.New}(): Creates a new AI_CARGO_DISPATCHER object.
-- 
-- Find below some examples of AI cargo dispatcher objects created.
-- 
-- ### An AI dispatcher object for a helicopter squadron, moving infantry from pickup zones to deploy zones.
-- 
--        local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart()
--        local SetHelicopter = SET_GROUP:New():FilterPrefixes( "Helicopter" ):FilterStart()
--        local SetPickupZones = SET_ZONE:New():FilterPrefixes( "Pickup" ):FilterStart()
--        local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart()
--      
--        AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) 
--        AICargoDispatcherHelicopter:SetHomeZone( ZONE:FindByName( "Home" ) )
-- 
-- ### An AI dispatcher object for a vehicle squadron, moving infantry from pickup zones to deploy zones.
-- 
--        local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart()
--        local SetAPC = SET_GROUP:New():FilterPrefixes( "APC" ):FilterStart()
--        local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart()
--      
--        AICargoDispatcherAPC = AI_CARGO_DISPATCHER_APC:New( SetAPC, SetCargoInfantry, nil, SetDeployZones ) 
--        AICargoDispatcherAPC:Start()
-- 
-- ### An AI dispatcher object for an airplane squadron, moving infantry and vehicles from pickup airbases to deploy airbases.
--   
--        local CargoInfantrySet = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart()
--        local AirplanesSet = SET_GROUP:New():FilterPrefixes( "Airplane" ):FilterStart()
--        local PickupZoneSet = SET_ZONE:New()
--        local DeployZoneSet = SET_ZONE:New()
--      
--        PickupZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Gudauta ) )
--        DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Sochi_Adler ) )
--        DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Maykop_Khanskaya ) )
--        DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Mineralnye_Vody ) )
--        DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Vaziani ) )
--      
--        AICargoDispatcherAirplanes = AI_CARGO_DISPATCHER_AIRPLANE:New( AirplanesSet, CargoInfantrySet, PickupZoneSet, DeployZoneSet ) 
--        AICargoDispatcherAirplanes:SetHomeZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Kobuleti ) )
-- 
-- ---
-- 
-- # 2) AI_CARGO_DISPATCHER is a Finite State Machine.
-- 
-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed.
-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state.
-- 
-- So, each of the rows have the following structure.
-- 
--   * **From** => **Event** => **To**
-- 
-- Important to know is that an event can only be executed if the **current state** is the **From** state.
-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed,
-- and the resulting state will be the **To** state.
-- 
-- These are the different possible state transitions of this state machine implementation: 
-- 
--   * Idle => Start => Monitoring
--   * Monitoring => Monitor => Monitoring
--   * Monitoring => Stop => Idle
--      
--   * Monitoring => Pickup => Monitoring
--   * Monitoring => Load => Monitoring
--   * Monitoring => Loading => Monitoring
--   * Monitoring => Loaded => Monitoring
--   * Monitoring => PickedUp => Monitoring
--   * Monitoring => Deploy => Monitoring
--   * Monitoring => Unload => Monitoring
--   * Monitoring => Unloaded => Monitoring
--   * Monitoring => Deployed => Monitoring
--   * Monitoring => Home => Monitoring
--
-- ## 2.1) AI_CARGO_DISPATCHER States.
-- 
--   * **Monitoring**: The process is dispatching.
--   * **Idle**: The process is idle.
-- 
-- ## 2.2) AI_CARGO_DISPATCHER Events.
-- 
--   * **Start**: Start the transport process.
--   * **Stop**: Stop the transport process.
--   * **Monitor**: Monitor and take action.
--   
--   * **Pickup**: Pickup cargo.
--   * **Load**: Load the cargo.
--   * **Loading**: The dispatcher is coordinating the loading of a cargo.
--   * **Loaded**: Flag that the cargo is loaded.
--   * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup.
--   * **Deploy**: Deploy cargo to a location.
--   * **Unload**: Unload the cargo.
--   * **Unloaded**: Flag that the cargo is unloaded.
--   * **Deployed**: All cargo is unloaded from the carriers in the group.
--   * **Home**: A Carrier is going home.
-- 
-- ---
-- 
-- # 3) Enhance your mission scripts with **Tailored** Event Handling!
-- 
-- Use these methods to capture the events and tailor the events with your own code!
-- All classes derived from AI_CARGO_DISPATCHER can capture these events, and you can write your own code.
-- 
-- In order to properly capture the events, it is mandatory that you execute the following actions using your script:
-- 
--   * Copy / Paste the code section into your script.
--   * Change the CLASS literal to the object name you have in your script.
--   * Within the function, you can now write your own code!
--   * IntelliSense will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, 
--     but you need to declare them as they are automatically provided by the event handling system of MOOSE.
-- 
-- You can send messages or fire off any other events within the code section. The sky is the limit!
-- 
-- Mission AID-CGO-140, AID-CGO-240 and AID-CGO-340 contain examples how these events can be tailored.
-- 
-- For those who don't have the time to check the test missions, find the underlying example of a Deployed event that is tailored.
-- 
--      --- Deployed Handler OnAfter for AI_CARGO_DISPATCHER.
--      -- Use this event handler to tailor the event when a carrier has deployed all cargo objects from the CarrierGroup.
--      -- You can use this event handler to post messages to players, or provide status updates etc.
--      -- @function OnAfterDeployed
--      -- @param #AICargoDispatcherHelicopter self
--      -- @param #string From A string that contains the "*from state name*" when the event was fired.
--      -- @param #string Event A string that contains the "*event name*" when the event was fired.
--      -- @param #string To A string that contains the "*to state name*" when the event was fired.
--      -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
--      -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
--      function AICargoDispatcherHelicopter:OnAfterDeployed( From, Event, To, CarrierGroup, DeployZone )
--      
--        MESSAGE:NewType( "Group " .. CarrierGroup:GetName() .. " deployed all cargo in zone " .. DeployZone:GetName(), MESSAGE.Type.Information ):ToAll()
--      
--      end 
-- 
-- 
-- ## 3.1) Tailor the **Pickup** event
-- 
-- Use this event handler to tailor the event when a CarrierGroup is routed towards a new pickup Coordinate and a specified Speed.
-- You can use this event handler to post messages to players, or provide status updates etc.
-- 
-- 
--      --- Pickup event handler OnAfter for CLASS.
--      -- Use this event handler to tailor the event when a CarrierGroup is routed towards a new pickup Coordinate and a specified Speed.
--      -- You can use this event handler to post messages to players, or provide status updates etc.
--      -- @param #CLASS self
--      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
--      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
--      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
--      -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
--      -- @param Core.Point#COORDINATE Coordinate The coordinate of the pickup location.
--      -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the pickup Coordinate.
--      -- @param #number Height Height in meters to move to the pickup coordinate.
--      -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone.
--      function CLASS:OnAfterPickup( From, Event, To, CarrierGroup, Coordinate, Speed, Height, PickupZone )
--      
--        -- Write here your own code.
--      
--      end
--      
-- 
-- ## 3.2) Tailor the **Load** event
-- 
-- Use this event handler to tailor the event when a CarrierGroup has initiated the loading or boarding of cargo within reporting or near range.
-- You can use this event handler to post messages to players, or provide status updates etc.
-- 
-- 
--      --- Load event handler OnAfter for CLASS.
--      -- Use this event handler to tailor the event when a CarrierGroup has initiated the loading or boarding of cargo within reporting or near range.
--      -- You can use this event handler to post messages to players, or provide status updates etc.
--      -- @param #CLASS self
--      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
--      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
--      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
--      -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
--      -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone.
--      function CLASS:OnAfterLoad( From, Event, To, CarrierGroup, PickupZone )
--      
--        -- Write here your own code.
--      
--      end
--      
-- 
-- ## 3.3) Tailor the **Loading** event
-- 
-- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of loading or boarding of a cargo object.
-- You can use this event handler to post messages to players, or provide status updates etc.
-- 
-- 
--      --- Loading event handler OnAfter for CLASS.
--      -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of loading or boarding of a cargo object.
--      -- You can use this event handler to post messages to players, or provide status updates etc.
--      -- Note that this event is triggered repeatedly until all cargo (units) have been boarded into the carrier.
--      -- @param #CLASS self
--      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
--      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
--      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
--      -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
--      -- @param Cargo.Cargo#CARGO Cargo The cargo object.
--      -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo loading operation.
--      -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone.
--      function CLASS:OnAfterLoading( From, Event, To, CarrierGroup, Cargo, CarrierUnit, PickupZone )
--      
--        -- Write here your own code.
--      
--      end
-- 
-- 
-- ## 3.4) Tailor the **Loaded** event
-- 
-- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has loaded a cargo object.
-- You can use this event handler to post messages to players, or provide status updates etc.
-- Note that if more cargo objects were loading or boarding into the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit.
-- 
-- The function provides the CarrierGroup, which is the main group that was loading the Cargo into the CarrierUnit.
-- A CarrierUnit is part of the larger CarrierGroup.
-- 
-- 
--      --- Loaded event handler OnAfter for CLASS.
--      -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has loaded a cargo object.
--      -- You can use this event handler to post messages to players, or provide status updates etc.
--      -- Note that if more cargo objects were loading or boarding into the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit.
--      -- A CarrierUnit can be part of the larger CarrierGroup.
--      -- @param #CLASS self
--      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
--      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
--      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
--      -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
--      -- @param Cargo.Cargo#CARGO Cargo The cargo object.
--      -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo loading operation.
--      -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone.
--      function CLASS:OnAfterLoaded( From, Event, To, CarrierGroup, Cargo, CarrierUnit, PickupZone )
--      
--        -- Write here your own code.
--      
--      end
-- 
-- 
-- ## 3.5) Tailor the **PickedUp** event
--
-- Use this event handler to tailor the event when a carrier has picked up all cargo objects into the CarrierGroup.
-- You can use this event handler to post messages to players, or provide status updates etc.
--
--
--      --- PickedUp event handler OnAfter for CLASS.
--      -- Use this event handler to tailor the event when a carrier has picked up all cargo objects into the CarrierGroup.
--      -- You can use this event handler to post messages to players, or provide status updates etc.
--      -- @param #CLASS self
--      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
--      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
--      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
--      -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
--      -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone.
--      function CLASS:OnAfterPickedUp( From, Event, To, CarrierGroup, PickupZone )
--      
--        -- Write here your own code.
--      
--      end
--      
-- 
-- ## 3.6) Tailor the **Deploy** event
-- 
-- Use this event handler to tailor the event when a CarrierGroup is routed to a deploy coordinate, to Unload all cargo objects in each CarrierUnit.
-- You can use this event handler to post messages to players, or provide status updates etc.
-- 
-- 
--      --- Deploy event handler OnAfter for CLASS.
--      -- Use this event handler to tailor the event when a CarrierGroup is routed to a deploy coordinate, to Unload all cargo objects in each CarrierUnit.
--      -- You can use this event handler to post messages to players, or provide status updates etc.
--      -- @param #CLASS self
--      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
--      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
--      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
--      -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
--      -- @param Core.Point#COORDINATE Coordinate The deploy coordinate.
--      -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the deploy Coordinate.
--      -- @param #number Height Height in meters to move to the deploy coordinate.
--      -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
--      function CLASS:OnAfterDeploy( From, Event, To, CarrierGroup, Coordinate, Speed, Height, DeployZone )
--      
--        -- Write here your own code.
--      
--      end
-- 
-- 
-- ## 3.7) Tailor the **Unload** event
--
-- Use this event handler to tailor the event when a CarrierGroup has initiated the unloading or unboarding of cargo.
-- You can use this event handler to post messages to players, or provide status updates etc.
--
--
--      --- Unload event handler OnAfter for CLASS.
--      -- Use this event handler to tailor the event when a CarrierGroup has initiated the unloading or unboarding of cargo.
--      -- You can use this event handler to post messages to players, or provide status updates etc.
--      -- @param #CLASS self
--      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
--      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
--      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
--      -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
--      -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
--      function CLASS:OnAfterUnload( From, Event, To, CarrierGroup, DeployZone )
--      
--        -- Write here your own code.
--      
--      end
-- 
-- 
-- ## 3.8) Tailor the **Unloading** event
-- 
-- 
--      --- UnLoading event handler OnAfter for CLASS.
--      -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of unloading or unboarding of a cargo object.
--      -- You can use this event handler to post messages to players, or provide status updates etc.
--      -- Note that this event is triggered repeatedly until all cargo (units) have been unboarded from the CarrierUnit.
--      -- @param #CLASS self
--      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
--      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
--      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
--      -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
--      -- @param Cargo.Cargo#CARGO Cargo The cargo object.
--      -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo unloading operation.
--      -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
--      function CLASS:OnAfterUnload( From, Event, To, CarrierGroup, Cargo, CarrierUnit, DeployZone )
--      
--        -- Write here your own code.
--      
--      end
--      
--      
-- ## 3.9) Tailor the **Unloaded** event
-- 
-- 
-- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has unloaded a cargo object.
-- You can use this event handler to post messages to players, or provide status updates etc.
-- 
--      --- Unloaded event handler OnAfter for CLASS.
--      -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has unloaded a cargo object.
--      -- You can use this event handler to post messages to players, or provide status updates etc.
--      -- Note that if more cargo objects were unloading or unboarding from the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit.
--      -- A CarrierUnit can be part of the larger CarrierGroup.
--      -- @param #CLASS self
--      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
--      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
--      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
--      -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
--      -- @param Cargo.Cargo#CARGO Cargo The cargo object.
--      -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo unloading operation.
--      -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
--      function CLASS:OnAfterUnloaded( From, Event, To, CarrierGroup, Cargo, CarrierUnit, DeployZone )
--      
--        -- Write here your own code.
--      
--      end
--      
--      
-- ## 3.10) Tailor the **Deployed** event
-- 
-- Use this event handler to tailor the event when a carrier has deployed all cargo objects from the CarrierGroup.
-- You can use this event handler to post messages to players, or provide status updates etc.
-- 
-- 
--      --- Deployed event handler OnAfter for CLASS.
--      -- Use this event handler to tailor the event when a carrier has deployed all cargo objects from the CarrierGroup.
--      -- You can use this event handler to post messages to players, or provide status updates etc.
--      -- @param #CLASS self
--      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
--      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
--      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
--      -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
--      -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
--      function CLASS:OnAfterDeployed( From, Event, To, CarrierGroup, DeployZone )
--      
--        -- Write here your own code.
--      
--      end
-- 
-- ## 3.11) Tailor the **Home** event
-- 
-- Use this event handler to tailor the event when a CarrierGroup is returning to the HomeZone, after it has deployed all cargo objects from the CarrierGroup.
-- You can use this event handler to post messages to players, or provide status updates etc.
-- 
--      --- Home event handler OnAfter for CLASS.
--      -- Use this event handler to tailor the event when a CarrierGroup is returning to the HomeZone, after it has deployed all cargo objects from the CarrierGroup.
--      -- You can use this event handler to post messages to players, or provide status updates etc.
--      -- If there is no HomeZone is specified, the CarrierGroup will stay at the current location after having deployed all cargo and this event won't be triggered.
--      -- @param #CLASS self
--      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
--      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
--      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
--      -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
--      -- @param Core.Point#COORDINATE Coordinate The home coordinate the Carrier will arrive and stop it's activities.
--      -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the home Coordinate.
--      -- @param #number Height Height in meters to move to the home coordinate.
--      -- @param Core.Zone#ZONE HomeZone The zone wherein the carrier will return when all cargo has been transported. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
--      function CLASS:OnAfterHome( From, Event, To, CarrierGroup, Coordinate, Speed, Height, HomeZone )
--
--        -- Write here your own code.
--      
--      end      
-- 
-- ---
-- 
-- # 4) Set the pickup parameters.
-- 
-- Several parameters can be set to pickup cargo:
-- 
--    * @{#AI_CARGO_DISPATCHER.SetPickupRadius}(): Sets or randomizes the pickup location for the carrier around the cargo coordinate in a radius defined an outer and optional inner radius. 
--    * @{#AI_CARGO_DISPATCHER.SetPickupSpeed}(): Set the speed or randomizes the speed in km/h to pickup the cargo.
--    * @{#AI_CARGO_DISPATCHER.SetPickupHeight}(): Set the height or randomizes the height in meters to pickup the cargo.
--    
-- ---   
--    
-- # 5) Set the deploy parameters.
-- 
-- Several parameters can be set to deploy cargo:
-- 
--    * @{#AI_CARGO_DISPATCHER.SetDeployRadius}(): Sets or randomizes the deploy location for the carrier around the cargo coordinate in a radius defined an outer and an optional inner radius. 
--    * @{#AI_CARGO_DISPATCHER.SetDeploySpeed}(): Set the speed or randomizes the speed in km/h to deploy the cargo.
--    * @{#AI_CARGO_DISPATCHER.SetDeployHeight}(): Set the height or randomizes the height in meters to deploy the cargo.
-- 
-- ---
-- 
-- # 6) Set the home zone when there isn't any more cargo to pickup.
-- 
-- A home zone can be specified to where the Carriers will move when there isn't any cargo left for pickup.
-- Use @{#AI_CARGO_DISPATCHER.SetHomeZone}() to specify the home zone.
-- 
-- If no home zone is specified, the carriers will wait near the deploy zone for a new pickup command.
-- 
-- ===
--   
-- @field #AI_CARGO_DISPATCHER
AI_CARGO_DISPATCHER = {
  ClassName = "AI_CARGO_DISPATCHER",
  AI_Cargo = {},
  PickupCargo = {}
}

--- List of AI_Cargo
-- @field #list
AI_CARGO_DISPATCHER.AI_Cargo = {}

--- List of PickupCargo
-- @field #list
AI_CARGO_DISPATCHER.PickupCargo = {}


--- Creates a new AI_CARGO_DISPATCHER object.
-- @param #AI_CARGO_DISPATCHER self
-- @param Core.Set#SET_GROUP CarrierSet The set of @{Wrapper.Group#GROUP} objects of carriers that will transport the cargo. 
-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects.
-- @param Core.Set#SET_ZONE PickupZoneSet (optional) The set of pickup zones, which are used to where the cargo can be picked up by the carriers. If nil, then cargo can be picked up everywhere. 
-- @param Core.Set#SET_ZONE DeployZoneSet The set of deploy zones, which are used to where the cargo will be deployed by the carriers. 
-- @return #AI_CARGO_DISPATCHER
-- @usage
-- 
--      -- An AI dispatcher object for a helicopter squadron, moving infantry from pickup zones to deploy zones.
-- 
--      local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart()
--      local SetHelicopter = SET_GROUP:New():FilterPrefixes( "Helicopter" ):FilterStart()
--      local SetPickupZones = SET_ZONE:New():FilterPrefixes( "Pickup" ):FilterStart()
--      local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart()
--      
--      AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) 
--      AICargoDispatcherHelicopter:Start()
-- 
-- @usage
-- 
--      -- An AI dispatcher object for a vehicle squadron, moving infantry from pickup zones to deploy zones.
-- 
--      local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart()
--      local SetAPC = SET_GROUP:New():FilterPrefixes( "APC" ):FilterStart()
--      local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart()
--      
--      AICargoDispatcherAPC = AI_CARGO_DISPATCHER_APC:New( SetAPC, SetCargoInfantry, nil, SetDeployZones ) 
--      AICargoDispatcherAPC:Start()
-- 
-- @usage
-- 
--      -- An AI dispatcher object for an airplane squadron, moving infantry and vehicles from pickup airbases to deploy airbases.
--   
--      local CargoInfantrySet = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart()
--      local AirplanesSet = SET_GROUP:New():FilterPrefixes( "Airplane" ):FilterStart()
--      local PickupZoneSet = SET_ZONE:New()
--      local DeployZoneSet = SET_ZONE:New()
--      
--      PickupZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Gudauta ) )
--      DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Sochi_Adler ) )
--      DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Maykop_Khanskaya ) )
--      DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Mineralnye_Vody ) )
--      DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Vaziani ) )
--      
--      AICargoDispatcherAirplanes = AI_CARGO_DISPATCHER_AIRPLANE:New( AirplanesSet, CargoInfantrySet, PickupZoneSet, DeployZoneSet ) 
--      AICargoDispatcherAirplanes:Start()
-- 
function AI_CARGO_DISPATCHER:New( CarrierSet, CargoSet, PickupZoneSet, DeployZoneSet )

  local self = BASE:Inherit( self, FSM:New() ) -- #AI_CARGO_DISPATCHER

  self.SetCarrier = CarrierSet -- Core.Set#SET_GROUP
  self.SetCargo = CargoSet -- Core.Set#SET_CARGO
  

  self.PickupZoneSet=PickupZoneSet
  self.DeployZoneSet=DeployZoneSet

  self:SetStartState( "Idle" ) 
  
  self:AddTransition( "Monitoring", "Monitor", "Monitoring" )

  self:AddTransition( "Idle", "Start", "Monitoring" )
  self:AddTransition( "Monitoring", "Stop", "Idle" )
  

  self:AddTransition( "Monitoring", "Pickup", "Monitoring" )
  self:AddTransition( "Monitoring", "Load", "Monitoring" )
  self:AddTransition( "Monitoring", "Loading", "Monitoring" )
  self:AddTransition( "Monitoring", "Loaded", "Monitoring" )
  self:AddTransition( "Monitoring", "PickedUp", "Monitoring" )

  self:AddTransition( "Monitoring", "Transport", "Monitoring" )

  self:AddTransition( "Monitoring", "Deploy", "Monitoring" )
  self:AddTransition( "Monitoring", "Unload", "Monitoring" )
  self:AddTransition( "Monitoring", "Unloading", "Monitoring" )
  self:AddTransition( "Monitoring", "Unloaded", "Monitoring" )
  self:AddTransition( "Monitoring", "Deployed", "Monitoring" )
  
  self:AddTransition( "Monitoring", "Home", "Monitoring" )
  
  self:SetMonitorTimeInterval( 30 )
  
  self:SetDeployRadius( 500, 200 )
  
  self.PickupCargo = {}
  self.CarrierHome = {}
  
  -- Put a Dead event handler on SetCarrier, to ensure that when a carrier is destroyed, that all internal parameters are reset.
  function self.SetCarrier.OnAfterRemoved( SetCarrier, From, Event, To, CarrierName, Carrier )
    self:F( { Carrier = Carrier:GetName() } )
    self.PickupCargo[Carrier] = nil
    self.CarrierHome[Carrier] = nil
  end
  
  return self
end


--- Set the monitor time interval.
-- @param #AI_CARGO_DISPATCHER self
-- @param #number MonitorTimeInterval The interval in seconds when the cargo dispatcher will search for new cargo to be picked up.
-- @return #AI_CARGO_DISPATCHER
function AI_CARGO_DISPATCHER:SetMonitorTimeInterval( MonitorTimeInterval )

  self.MonitorTimeInterval = MonitorTimeInterval
  
  return self
end


--- Set the home zone.
-- When there is nothing anymore to pickup, the carriers will go to a random coordinate in this zone.
-- They will await here new orders.
-- @param #AI_CARGO_DISPATCHER self
-- @param Core.Zone#ZONE_BASE HomeZone The home zone where the carriers will return when there is no more cargo to pickup.
-- @return #AI_CARGO_DISPATCHER
-- @usage
-- 
-- -- Create a new cargo dispatcher
-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) 
-- 
-- -- Set the home coordinate
-- local HomeZone = ZONE:New( "Home" )
-- AICargoDispatcherHelicopter:SetHomeZone( HomeZone )
-- 
function AI_CARGO_DISPATCHER:SetHomeZone( HomeZone )

  self.HomeZone = HomeZone
  
  return self
end


--- Sets or randomizes the pickup location for the carrier around the cargo coordinate in a radius defined an outer and optional inner radius.
-- This radius is influencing the location where the carrier will land to pickup the cargo.
-- There are two aspects that are very important to remember and take into account:
-- 
--   - Ensure that the outer and inner radius are within reporting radius set by the cargo.
--     For example, if the cargo has a reporting radius of 400 meters, and the outer and inner radius is set to 500 and 450 respectively, 
--     then no cargo will be loaded!!!
--   - Also take care of the potential cargo position and possible reasons to crash the carrier. This is especially important
--     for locations which are crowded with other objects, like in the middle of villages or cities.
--     So, for the best operation of cargo operations, always ensure that the cargo is located at open spaces.
-- 
-- The default radius is 0, so the center. In case of a polygon zone, a random location will be selected as the center in the zone.
-- @param #AI_CARGO_DISPATCHER self
-- @param #number OuterRadius The outer radius in meters around the cargo coordinate.
-- @param #number InnerRadius (optional) The inner radius in meters around the cargo coordinate.
-- @return #AI_CARGO_DISPATCHER
-- @usage
-- 
-- -- Create a new cargo dispatcher
-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) 
-- 
-- -- Set the carrier to land within a band around the cargo coordinate between 500 and 300 meters!
-- AICargoDispatcherHelicopter:SetPickupRadius( 500, 300 )
-- 
function AI_CARGO_DISPATCHER:SetPickupRadius( OuterRadius, InnerRadius )

  OuterRadius = OuterRadius or 0
  InnerRadius = InnerRadius or OuterRadius

  self.PickupOuterRadius = OuterRadius
  self.PickupInnerRadius = InnerRadius
  
  return self
end


--- Set the speed or randomizes the speed in km/h to pickup the cargo.
-- @param #AI_CARGO_DISPATCHER self
-- @param #number MaxSpeed (optional) The maximum speed to move to the cargo pickup location.
-- @param #number MinSpeed The minimum speed to move to the cargo pickup location.
-- @return #AI_CARGO_DISPATCHER
-- @usage
-- 
-- -- Create a new cargo dispatcher
-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) 
-- 
-- -- Set the minimum pickup speed to be 100 km/h and the maximum speed to be 200 km/h.
-- AICargoDispatcherHelicopter:SetPickupSpeed( 200, 100 )
-- 
function AI_CARGO_DISPATCHER:SetPickupSpeed( MaxSpeed, MinSpeed )

  MaxSpeed = MaxSpeed or 999
  MinSpeed = MinSpeed or MaxSpeed

  self.PickupMinSpeed = MinSpeed
  self.PickupMaxSpeed = MaxSpeed
  
  return self
end


--- Sets or randomizes the deploy location for the carrier around the cargo coordinate in a radius defined an outer and an optional inner radius.
-- This radius is influencing the location where the carrier will land to deploy the cargo.
-- There is an aspect that is very important to remember and take into account:
-- 
--   - Take care of the potential cargo position and possible reasons to crash the carrier. This is especially important
--     for locations which are crowded with other objects, like in the middle of villages or cities.
--     So, for the best operation of cargo operations, always ensure that the cargo is located at open spaces.
-- 
-- The default radius is 0, so the center. In case of a polygon zone, a random location will be selected as the center in the zone.
-- @param #AI_CARGO_DISPATCHER self
-- @param #number OuterRadius The outer radius in meters around the cargo coordinate.
-- @param #number InnerRadius (optional) The inner radius in meters around the cargo coordinate.
-- @return #AI_CARGO_DISPATCHER
-- @usage
-- 
-- -- Create a new cargo dispatcher
-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) 
-- 
-- -- Set the carrier to land within a band around the cargo coordinate between 500 and 300 meters!
-- AICargoDispatcherHelicopter:SetDeployRadius( 500, 300 )
-- 
function AI_CARGO_DISPATCHER:SetDeployRadius( OuterRadius, InnerRadius )

  OuterRadius = OuterRadius or 0
  InnerRadius = InnerRadius or OuterRadius

  self.DeployOuterRadius = OuterRadius
  self.DeployInnerRadius = InnerRadius
  
  return self
end


--- Sets or randomizes the speed in km/h to deploy the cargo.
-- @param #AI_CARGO_DISPATCHER self
-- @param #number MaxSpeed The maximum speed to move to the cargo deploy location.
-- @param #number MinSpeed (optional) The minimum speed to move to the cargo deploy location.
-- @return #AI_CARGO_DISPATCHER
-- @usage
-- 
-- -- Create a new cargo dispatcher
-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) 
-- 
-- -- Set the minimum deploy speed to be 100 km/h and the maximum speed to be 200 km/h.
-- AICargoDispatcherHelicopter:SetDeploySpeed( 200, 100 )
-- 
function AI_CARGO_DISPATCHER:SetDeploySpeed( MaxSpeed, MinSpeed )

  MaxSpeed = MaxSpeed or 999
  MinSpeed = MinSpeed or MaxSpeed

  self.DeployMinSpeed = MinSpeed
  self.DeployMaxSpeed = MaxSpeed
  
  return self
end


--- Set the height or randomizes the height in meters to fly and pickup the cargo. The default height is 200 meters.
-- @param #AI_CARGO_DISPATCHER self
-- @param #number MaxHeight (optional) The maximum height to fly to the cargo pickup location.
-- @param #number MinHeight (optional) The minimum height to fly to the cargo pickup location.
-- @return #AI_CARGO_DISPATCHER
-- @usage
-- 
-- -- Create a new cargo dispatcher
-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) 
-- 
-- -- Set the minimum pickup fly height to be 50 meters and the maximum height to be 200 meters.
-- AICargoDispatcherHelicopter:SetPickupHeight( 200, 50 )
-- 
function AI_CARGO_DISPATCHER:SetPickupHeight( MaxHeight, MinHeight )

  MaxHeight = MaxHeight or 200
  MinHeight = MinHeight or MaxHeight

  self.PickupMinHeight = MinHeight
  self.PickupMaxHeight = MaxHeight
  
  return self
end


--- Set the height or randomizes the height in meters to fly and deploy the cargo.  The default height is 200 meters.
-- @param #AI_CARGO_DISPATCHER self
-- @param #number MaxHeight (optional) The maximum height to fly to the cargo deploy location.
-- @param #number MinHeight (optional) The minimum height to fly to the cargo deploy location.
-- @return #AI_CARGO_DISPATCHER
-- @usage
-- 
-- -- Create a new cargo dispatcher
-- AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) 
-- 
-- -- Set the minimum deploy fly height to be 50 meters and the maximum height to be 200 meters.
-- AICargoDispatcherHelicopter:SetDeployHeight( 200, 50 )
-- 
function AI_CARGO_DISPATCHER:SetDeployHeight( MaxHeight, MinHeight )

  MaxHeight = MaxHeight or 200
  MinHeight = MinHeight or MaxHeight

  self.DeployMinHeight = MinHeight
  self.DeployMaxHeight = MaxHeight
  
  return self
end


--- The Start trigger event, which actually takes action at the specified time interval.
-- @param #AI_CARGO_DISPATCHER self
function AI_CARGO_DISPATCHER:onafterMonitor()

  self:F("Carriers")
  self.SetCarrier:Flush()
  
  for CarrierGroupName, Carrier in pairs( self.SetCarrier:GetSet() ) do
    local Carrier = Carrier -- Wrapper.Group#GROUP
    if Carrier:IsAlive() ~= nil then
      local AI_Cargo = self.AI_Cargo[Carrier]
      if not AI_Cargo then
      
        -- ok, so this Carrier does not have yet an AI_CARGO handling object...
        -- let's create one and also declare the Loaded and UnLoaded handlers.
        self.AI_Cargo[Carrier] = self:AICargo( Carrier, self.SetCargo, self.CombatRadius )
        AI_Cargo = self.AI_Cargo[Carrier]
        
        --- Pickup event handler OnAfter for AI_CARGO_DISPATCHER.
        -- Use this event handler to tailor the event when a CarrierGroup is routed towards a new pickup Coordinate and a specified Speed.
        -- You can use this event handler to post messages to players, or provide status updates etc.
        -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterPickup
        -- @param #AI_CARGO_DISPATCHER self
        -- @param #string From A string that contains the "*from state name*" when the event was triggered.
        -- @param #string Event A string that contains the "*event name*" when the event was triggered.
        -- @param #string To A string that contains the "*to state name*" when the event was triggered.
        -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
        -- @param Core.Point#COORDINATE Coordinate The coordinate of the pickup location.
        -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the pickup Coordinate.
        -- @param #number Height Height in meters to move to the pickup coordinate.
        -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone.
        function AI_Cargo.OnAfterPickup( AI_Cargo, CarrierGroup, From, Event, To, Coordinate, Speed, Height, PickupZone )
          self:Pickup( CarrierGroup, Coordinate, Speed, Height, PickupZone )
        end
        
        --- Load event handler OnAfter for AI_CARGO_DISPATCHER.
        -- Use this event handler to tailor the event when a CarrierGroup has initiated the loading or boarding of cargo within reporting or near range.
        -- You can use this event handler to post messages to players, or provide status updates etc.
        -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterLoad
        -- @param #AI_CARGO_DISPATCHER self
        -- @param #string From A string that contains the "*from state name*" when the event was triggered.
        -- @param #string Event A string that contains the "*event name*" when the event was triggered.
        -- @param #string To A string that contains the "*to state name*" when the event was triggered.
        -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
        -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone.
        
        function AI_Cargo.OnAfterLoad( AI_Cargo, CarrierGroup, From, Event, To, PickupZone )
          self:Load( CarrierGroup, PickupZone )
        end
  
        --- Loading event handler OnAfter for AI_CARGO_DISPATCHER.
        -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of loading or boarding of a cargo object.
        -- You can use this event handler to post messages to players, or provide status updates etc.
        -- Note that this event is triggered repeatedly until all cargo (units) have been boarded into the carrier.
        -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterLoading
        -- @param #AI_CARGO_DISPATCHER self
        -- @param #string From A string that contains the "*from state name*" when the event was triggered.
        -- @param #string Event A string that contains the "*event name*" when the event was triggered.
        -- @param #string To A string that contains the "*to state name*" when the event was triggered.
        -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
        -- @param Cargo.Cargo#CARGO Cargo The cargo object.
        -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo loading operation.
        -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone.
        
        function AI_Cargo.OnAfterBoard( AI_Cargo, CarrierGroup, From, Event, To, Cargo, CarrierUnit, PickupZone )
          self:Loading( CarrierGroup, Cargo, CarrierUnit, PickupZone )
        end
  
        --- Loaded event handler OnAfter for AI_CARGO_DISPATCHER.
        -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has loaded a cargo object.
        -- You can use this event handler to post messages to players, or provide status updates etc.
        -- Note that if more cargo objects were loading or boarding into the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit.
        -- A CarrierUnit can be part of the larger CarrierGroup.
        -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterLoaded
        -- @param #AI_CARGO_DISPATCHER self
        -- @param #string From A string that contains the "*from state name*" when the event was triggered.
        -- @param #string Event A string that contains the "*event name*" when the event was triggered.
        -- @param #string To A string that contains the "*to state name*" when the event was triggered.
        -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
        -- @param Cargo.Cargo#CARGO Cargo The cargo object.
        -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo loading operation.
        -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone.
        
        function AI_Cargo.OnAfterLoaded( AI_Cargo, CarrierGroup, From, Event, To, Cargo, CarrierUnit, PickupZone )
          self:Loaded( CarrierGroup, Cargo, CarrierUnit, PickupZone )
        end
  
        --- PickedUp event handler OnAfter for AI_CARGO_DISPATCHER.
        -- Use this event handler to tailor the event when a carrier has picked up all cargo objects into the CarrierGroup.
        -- You can use this event handler to post messages to players, or provide status updates etc.
        -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterPickedUp
        -- @param #AI_CARGO_DISPATCHER self
        -- @param #string From A string that contains the "*from state name*" when the event was triggered.
        -- @param #string Event A string that contains the "*event name*" when the event was triggered.
        -- @param #string To A string that contains the "*to state name*" when the event was triggered.
        -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
        -- @param Core.Zone#ZONE_AIRBASE PickupZone (optional) The zone from where the cargo is picked up. Note that the zone is optional and may not be provided, but for AI_CARGO_DISPATCHER_AIRBASE there will always be a PickupZone, as the pickup location is an airbase zone.
        
        function AI_Cargo.OnAfterPickedUp( AI_Cargo, CarrierGroup, From, Event, To, PickupZone )
          self:PickedUp( CarrierGroup, PickupZone )
          self:Transport( CarrierGroup )
        end
  
  
        --- Deploy event handler OnAfter for AI_CARGO_DISPATCHER.
        -- Use this event handler to tailor the event when a CarrierGroup is routed to a deploy coordinate, to Unload all cargo objects in each CarrierUnit.
        -- You can use this event handler to post messages to players, or provide status updates etc.
        -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterDeploy
        -- @param #AI_CARGO_DISPATCHER self
        -- @param #string From A string that contains the "*from state name*" when the event was triggered.
        -- @param #string Event A string that contains the "*event name*" when the event was triggered.
        -- @param #string To A string that contains the "*to state name*" when the event was triggered.
        -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
        -- @param Core.Point#COORDINATE Coordinate The deploy coordinate.
        -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the deploy Coordinate.
        -- @param #number Height Height in meters to move to the deploy coordinate.
        -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
        
        function AI_Cargo.OnAfterDeploy( AI_Cargo, CarrierGroup, From, Event, To, Coordinate, Speed, Height, DeployZone )
          self:Deploy( CarrierGroup, Coordinate, Speed, Height, DeployZone )
        end      
  
  
        --- Unload event handler OnAfter for AI_CARGO_DISPATCHER.
        -- Use this event handler to tailor the event when a CarrierGroup has initiated the unloading or unboarding of cargo.
        -- You can use this event handler to post messages to players, or provide status updates etc.
        -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterUnload
        -- @param #AI_CARGO_DISPATCHER self
        -- @param #string From A string that contains the "*from state name*" when the event was triggered.
        -- @param #string Event A string that contains the "*event name*" when the event was triggered.
        -- @param #string To A string that contains the "*to state name*" when the event was triggered.
        -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
        -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
        
        function AI_Cargo.OnAfterUnload( AI_Cargo, Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone )
          self:Unloading( Carrier, Cargo, CarrierUnit, DeployZone )
        end      
  
        --- UnLoading event handler OnAfter for AI_CARGO_DISPATCHER.
        -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup is in the process of unloading or unboarding of a cargo object.
        -- You can use this event handler to post messages to players, or provide status updates etc.
        -- Note that this event is triggered repeatedly until all cargo (units) have been unboarded from the CarrierUnit.
        -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterUnloading
        -- @param #AI_CARGO_DISPATCHER self
        -- @param #string From A string that contains the "*from state name*" when the event was triggered.
        -- @param #string Event A string that contains the "*event name*" when the event was triggered.
        -- @param #string To A string that contains the "*to state name*" when the event was triggered.
        -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
        -- @param Cargo.Cargo#CARGO Cargo The cargo object.
        -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo unloading operation.
        -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
        
        function AI_Cargo.OnAfterUnboard( AI_Cargo, CarrierGroup, From, Event, To, Cargo, CarrierUnit, DeployZone )
          self:Unloading( CarrierGroup, Cargo, CarrierUnit, DeployZone )
        end
  
  
        --- Unloaded event handler OnAfter for AI_CARGO_DISPATCHER.
        -- Use this event handler to tailor the event when a CarrierUnit of a CarrierGroup has unloaded a cargo object.
        -- You can use this event handler to post messages to players, or provide status updates etc.
        -- Note that if more cargo objects were unloading or unboarding from the CarrierUnit, then this event can be triggered multiple times for each different Cargo/CarrierUnit.
        -- A CarrierUnit can be part of the larger CarrierGroup.
        -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterUnloaded
        -- @param #AI_CARGO_DISPATCHER self
        -- @param #string From A string that contains the "*from state name*" when the event was triggered.
        -- @param #string Event A string that contains the "*event name*" when the event was triggered.
        -- @param #string To A string that contains the "*to state name*" when the event was triggered.
        -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
        -- @param Cargo.Cargo#CARGO Cargo The cargo object.
        -- @param Wrapper.Unit#UNIT CarrierUnit The carrier unit that is executing the cargo unloading operation.
        -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
        
        function AI_Cargo.OnAfterUnloaded( AI_Cargo, Carrier, From, Event, To, Cargo, CarrierUnit, DeployZone )
          self:Unloaded( Carrier, Cargo, CarrierUnit, DeployZone )
        end      
  
        --- Deployed event handler OnAfter for AI_CARGO_DISPATCHER.
        -- Use this event handler to tailor the event when a carrier has deployed all cargo objects from the CarrierGroup.
        -- You can use this event handler to post messages to players, or provide status updates etc.
        -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterDeployed
        -- @param #AI_CARGO_DISPATCHER self
        -- @param #string From A string that contains the "*from state name*" when the event was triggered.
        -- @param #string Event A string that contains the "*event name*" when the event was triggered.
        -- @param #string To A string that contains the "*to state name*" when the event was triggered.
        -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
        -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
        
        function AI_Cargo.OnAfterDeployed( AI_Cargo, Carrier, From, Event, To, DeployZone )
          self:Deployed( Carrier, DeployZone )
        end      
  
        --- Home event handler OnAfter for AI_CARGO_DISPATCHER.
        -- Use this event handler to tailor the event when a CarrierGroup is returning to the HomeZone, after it has deployed all cargo objects from the CarrierGroup.
        -- You can use this event handler to post messages to players, or provide status updates etc.
        -- If there is no HomeZone is specified, the CarrierGroup will stay at the current location after having deployed all cargo.
        -- @function [parent=#AI_CARGO_DISPATCHER] OnAfterHome
        -- @param #AI_CARGO_DISPATCHER self
        -- @param #string From A string that contains the "*from state name*" when the event was triggered.
        -- @param #string Event A string that contains the "*event name*" when the event was triggered.
        -- @param #string To A string that contains the "*to state name*" when the event was triggered.
        -- @param Wrapper.Group#GROUP CarrierGroup The group object that contains the CarrierUnits.
        -- @param Core.Point#COORDINATE Coordinate The home coordinate the Carrier will arrive and stop it's activities.
        -- @param #number Speed The velocity in meters per second on which the CarrierGroup is routed towards the home Coordinate.
        -- @param #number Height Height in meters to move to the home coordinate.
        -- @param Core.Zone#ZONE HomeZone The zone wherein the carrier will return when all cargo has been transported. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
        
        function AI_Cargo.OnAfterHome( AI_Cargo, Carrier, From, Event, To, Coordinate, Speed, Height, HomeZone )
          self:Home( Carrier, Coordinate, Speed, Height, HomeZone )
        end      
      end
  
      -- The Pickup sequence ...
      -- Check if this Carrier need to go and Pickup something...
      -- So, if the cargo bay is not full yet with cargo to be loaded ...
      self:T( { Carrier = CarrierGroupName, IsRelocating = AI_Cargo:IsRelocating(), IsTransporting = AI_Cargo:IsTransporting() } )
      if AI_Cargo:IsRelocating() == false and AI_Cargo:IsTransporting() == false then
        -- ok, so there is a free Carrier
        -- now find the first cargo that is Unloaded
        
        local PickupCargo = nil
        local PickupZone = nil
        
        self.SetCargo:Flush()
        for CargoName, Cargo in UTILS.spairs( self.SetCargo:GetSet(), function( t, a, b ) return t[a]:GetWeight() < t[b]:GetWeight() end ) do
          local Cargo = Cargo -- Cargo.Cargo#CARGO
          self:F( { Cargo = Cargo:GetName(), UnLoaded = Cargo:IsUnLoaded(), Deployed = Cargo:IsDeployed(), PickupCargo = self.PickupCargo[Carrier] ~= nil } )
          if Cargo:IsUnLoaded() == true and Cargo:IsDeployed() == false then
            local CargoCoordinate = Cargo:GetCoordinate()
            local CoordinateFree = true
            --self.PickupZoneSet:Flush()
            --PickupZone = self.PickupZoneSet:GetRandomZone()
            PickupZone = self.PickupZoneSet and self.PickupZoneSet:IsCoordinateInZone( CargoCoordinate )
            if not self.PickupZoneSet or PickupZone then
              for CarrierPickup, Coordinate in pairs( self.PickupCargo ) do
                if CarrierPickup:IsAlive() == true then
                  if CargoCoordinate:Get2DDistance( Coordinate ) <= 25 then
                    self:F( { "Coordinate not free for ", Cargo = Cargo:GetName(), Carrier:GetName(), PickupCargo = self.PickupCargo[Carrier] ~= nil } )
                    CoordinateFree = false
                    break
                  end
                else
                  self.PickupCargo[CarrierPickup] = nil
                end
              end
              if CoordinateFree == true then
                -- Check if this cargo can be picked-up by at least one carrier unit of AI_Cargo.
                local LargestLoadCapacity = 0
                for _, Carrier in pairs( Carrier:GetUnits() ) do
                  local LoadCapacity = Carrier:GetCargoBayFreeWeight()
                  if LargestLoadCapacity < LoadCapacity then
                    LargestLoadCapacity = LoadCapacity
                  end
                end
                -- So if there is a carrier that has the required load capacity to load the total weight of the cargo, dispatch the carrier.
                -- Otherwise break and go to the next carrier.
                -- This will skip cargo which is too large to be able to be loaded by carriers
                -- and will secure an efficient dispatching scheme.
                if LargestLoadCapacity >= Cargo:GetWeight() then
                  self.PickupCargo[Carrier] = CargoCoordinate
                  PickupCargo = Cargo
                  break
                else
                  local text=string.format("WARNING: Cargo %s is too heavy to be loaded into transport. Cargo weight %.1f > %.1f load capacity of carrier %s.", 
                  tostring(Cargo:GetName()), Cargo:GetWeight(), LargestLoadCapacity, tostring(Carrier:GetName()))
                  self:T(text)
                end
              end
            end
          end
        end
        
        if PickupCargo then
          self.CarrierHome[Carrier] = nil
          local PickupCoordinate = PickupCargo:GetCoordinate():GetRandomCoordinateInRadius( self.PickupOuterRadius, self.PickupInnerRadius )
          AI_Cargo:Pickup( PickupCoordinate, math.random( self.PickupMinSpeed, self.PickupMaxSpeed ), math.random( self.PickupMinHeight, self.PickupMaxHeight ), PickupZone )
          break
        else
          if self.HomeZone then
            if not self.CarrierHome[Carrier] then
              self.CarrierHome[Carrier] = true
              AI_Cargo:Home( self.HomeZone:GetRandomPointVec2(), math.random( self.PickupMinSpeed, self.PickupMaxSpeed ), math.random( self.PickupMinHeight, self.PickupMaxHeight ), self.HomeZone )
            end
          end
        end
      end
    end
  end

  self:__Monitor( self.MonitorTimeInterval )
end


--- Start Trigger for AI_CARGO_DISPATCHER
-- @function [parent=#AI_CARGO_DISPATCHER] Start
-- @param #AI_CARGO_DISPATCHER self

--- Start Asynchronous Trigger for AI_CARGO_DISPATCHER
-- @function [parent=#AI_CARGO_DISPATCHER] __Start
-- @param #AI_CARGO_DISPATCHER self
-- @param #number Delay

function AI_CARGO_DISPATCHER:onafterStart( From, Event, To )
  self:__Monitor( -1 )
end


--- Stop Trigger for AI_CARGO_DISPATCHER
-- @function [parent=#AI_CARGO_DISPATCHER] Stop
-- @param #AI_CARGO_DISPATCHER self

--- Stop Asynchronous Trigger for AI_CARGO_DISPATCHER
-- @function [parent=#AI_CARGO_DISPATCHER] __Stop
-- @param #AI_CARGO_DISPATCHER self
-- @param #number Delay


--- Make a Carrier run for a cargo deploy action after the cargo has been loaded, by default.
-- @param #AI_CARGO_DISPATCHER self
-- @param From
-- @param Event
-- @param To
-- @param Wrapper.Group#GROUP Carrier
-- @param Cargo.Cargo#CARGO Cargo
-- @return #AI_CARGO_DISPATCHER
function AI_CARGO_DISPATCHER:onafterTransport( From, Event, To, Carrier, Cargo )

  if self.DeployZoneSet then
    if self.AI_Cargo[Carrier]:IsTransporting() == true then
      local DeployZone = self.DeployZoneSet:GetRandomZone()
      
      local DeployCoordinate = DeployZone:GetCoordinate():GetRandomCoordinateInRadius( self.DeployOuterRadius, self.DeployInnerRadius )
      self.AI_Cargo[Carrier]:__Deploy( 0.1, DeployCoordinate, math.random( self.DeployMinSpeed, self.DeployMaxSpeed ), math.random( self.DeployMinHeight, self.DeployMaxHeight ), DeployZone )
    end
  end
  
   self:F( { Carrier = Carrier:GetName(), PickupCargo = self.PickupCargo } )
   self.PickupCargo[Carrier] = nil
end

--- **AI** - Models the intelligent transportation of infantry and other cargo using APCs.
--
-- ## Features:
-- 
--   * Quickly transport cargo to various deploy zones using ground vehicles (APCs, trucks ...).
--   * Various @{Cargo.Cargo#CARGO} types can be transported. These are infantry groups and crates.
--   * Define a list of deploy zones of various types to transport the cargo to.
--   * The vehicles follow the roads to ensure the fastest possible cargo transportation over the ground.
--   * Multiple vehicles can transport multiple cargo as one vehicle group.
--   * Multiple vehicle groups can be enabled as one collaborating transportation process.
--   * Infantry loaded as cargo, will unboard in case enemies are nearby and will help defending the vehicles.
--   * Different ranges can be setup for enemy defenses.
--   * Different options can be setup to tweak the cargo transporation behaviour.
--  
-- ===
-- 
-- ## Test Missions:
-- 
-- Test missions can be located on the main GITHUB site.
-- 
-- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/]  
-- (https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/AID%20-%20AI%20Dispatching/AID-CGO%20-%20AI%20Cargo%20Dispatching)
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_Cargo_Dispatcher_APC
-- @image AI_Cargo_Dispatching_For_APC.JPG

-- @type AI_CARGO_DISPATCHER_APC
-- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER


--- A dynamic cargo transportation capability for AI groups.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- Armoured Personnel APCs (APC), Trucks, Jeeps and other carrier equipment can be mobilized to intelligently transport infantry and other cargo within the simulation.
-- 
-- The AI_CARGO_DISPATCHER_APC module is derived from the AI_CARGO_DISPATCHER module.
-- 
-- ## Note! In order to fully understand the mechanisms of the AI_CARGO_DISPATCHER_APC class, it is recommended that you first consult and READ the documentation of the @{AI.AI_Cargo_Dispatcher} module!!!
-- 
-- Especially to learn how to **Tailor the different cargo handling events**, this will be very useful!
-- 
-- On top, the AI_CARGO_DISPATCHER_APC class uses the @{Cargo.Cargo} capabilities within the MOOSE framework.
-- Also ensure that you fully understand how to declare and setup Cargo objects within the MOOSE framework before using this class.
-- CARGO derived objects must be declared within the mission to make the AI_CARGO_DISPATCHER_HELICOPTER object recognize the cargo.
-- 
-- 
-- # 1) AI_CARGO_DISPATCHER_APC constructor.
--   
--   * @{#AI_CARGO_DISPATCHER_APC.New}(): Creates a new AI_CARGO_DISPATCHER_APC object.
-- 
-- ---
-- 
-- # 2) AI_CARGO_DISPATCHER_APC is a Finite State Machine.
-- 
-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed.
-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state.
-- 
-- So, each of the rows have the following structure.
-- 
--   * **From** => **Event** => **To**
-- 
-- Important to know is that an event can only be executed if the **current state** is the **From** state.
-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed,
-- and the resulting state will be the **To** state.
-- 
-- These are the different possible state transitions of this state machine implementation: 
-- 
--   * Idle => Start => Monitoring
--   * Monitoring => Monitor => Monitoring
--   * Monitoring => Stop => Idle
--      
--   * Monitoring => Pickup => Monitoring
--   * Monitoring => Load => Monitoring
--   * Monitoring => Loading => Monitoring
--   * Monitoring => Loaded => Monitoring
--   * Monitoring => PickedUp => Monitoring
--   * Monitoring => Deploy => Monitoring
--   * Monitoring => Unload => Monitoring
--   * Monitoring => Unloaded => Monitoring
--   * Monitoring => Deployed => Monitoring
--   * Monitoring => Home => Monitoring
-- 
--      
-- ## 2.1) AI_CARGO_DISPATCHER States.
-- 
--   * **Monitoring**: The process is dispatching.
--   * **Idle**: The process is idle.
-- 
-- ## 2.2) AI_CARGO_DISPATCHER Events.
-- 
--   * **Start**: Start the transport process.
--   * **Stop**: Stop the transport process.
--   * **Monitor**: Monitor and take action.
--   
--   * **Pickup**: Pickup cargo.
--   * **Load**: Load the cargo.
--   * **Loading**: The dispatcher is coordinating the loading of a cargo.
--   * **Loaded**: Flag that the cargo is loaded.
--   * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup.
--   * **Deploy**: Deploy cargo to a location.
--   * **Unload**: Unload the cargo.
--   * **Unloaded**: Flag that the cargo is unloaded.
--   * **Deployed**: All cargo is unloaded from the carriers in the group.
--   * **Home**: A Carrier is going home.
-- 
-- ## 2.3) Enhance your mission scripts with **Tailored** Event Handling!
-- 
-- Within your mission, you can capture these events when triggered, and tailor the events with your own code!
-- Check out the @{AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER} class at chapter 3 for details on the different event handlers that are available and how to use them.
-- 
-- **There are a lot of templates available that allows you to quickly setup an event handler for a specific event type!**
-- 
-- ---
-- 
-- # 3) Set the pickup parameters.
-- 
-- Several parameters can be set to pickup cargo:
-- 
--    * @{#AI_CARGO_DISPATCHER_APC.SetPickupRadius}(): Sets or randomizes the pickup location for the APC around the cargo coordinate in a radius defined an outer and optional inner radius. 
--    * @{#AI_CARGO_DISPATCHER_APC.SetPickupSpeed}(): Set the speed or randomizes the speed in km/h to pickup the cargo.
--    
-- # 4) Set the deploy parameters.
-- 
-- Several parameters can be set to deploy cargo:
-- 
--    * @{#AI_CARGO_DISPATCHER_APC.SetDeployRadius}(): Sets or randomizes the deploy location for the APC around the cargo coordinate in a radius defined an outer and an optional inner radius. 
--    * @{#AI_CARGO_DISPATCHER_APC.SetDeploySpeed}(): Set the speed or randomizes the speed in km/h to deploy the cargo.
-- 
-- # 5) Set the home zone when there isn't any more cargo to pickup.
-- 
-- A home zone can be specified to where the APCs will move when there isn't any cargo left for pickup.
-- Use @{#AI_CARGO_DISPATCHER_APC.SetHomeZone}() to specify the home zone.
-- 
-- If no home zone is specified, the APCs will wait near the deploy zone for a new pickup command.   
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_CARGO_DISPATCHER_APC
AI_CARGO_DISPATCHER_APC = {
  ClassName = "AI_CARGO_DISPATCHER_APC",
}

--- Creates a new AI_CARGO_DISPATCHER_APC object.
-- @param #AI_CARGO_DISPATCHER_APC self
-- @param Core.Set#SET_GROUP APCSet The set of @{Wrapper.Group#GROUP} objects of vehicles, trucks, APCs that will transport the cargo.
-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects.
-- @param Core.Set#SET_ZONE PickupZoneSet (optional) The set of pickup zones, which are used to where the cargo can be picked up by the APCs. If nil, then cargo can be picked up everywhere. 
-- @param Core.Set#SET_ZONE DeployZoneSet The set of deploy zones, which are used to where the cargo will be deployed by the APCs. 
-- @param DCS#Distance CombatRadius The cargo will be unloaded from the APC and engage the enemy if the enemy is within CombatRadius range. The radius is in meters, the default value is 500 meters.
-- @return #AI_CARGO_DISPATCHER_APC
-- @usage
-- 
--      -- An AI dispatcher object for a vehicle squadron, moving infantry from pickup zones to deploy zones.
-- 
--      local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart()
--      local SetAPC = SET_GROUP:New():FilterPrefixes( "APC" ):FilterStart()
--      local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart()
--      
--      AICargoDispatcherAPC = AI_CARGO_DISPATCHER_APC:New( SetAPC, SetCargoInfantry, nil, SetDeployZones ) 
--      AICargoDispatcherAPC:Start()
-- 
function AI_CARGO_DISPATCHER_APC:New( APCSet, CargoSet, PickupZoneSet, DeployZoneSet, CombatRadius )

  local self = BASE:Inherit( self, AI_CARGO_DISPATCHER:New( APCSet, CargoSet, PickupZoneSet, DeployZoneSet ) ) -- #AI_CARGO_DISPATCHER_APC

  self:SetDeploySpeed( 120, 70 )
  self:SetPickupSpeed( 120, 70 )
  self:SetPickupRadius( 0, 0 )
  self:SetDeployRadius( 0, 0 )
  
  self:SetPickupHeight()
  self:SetDeployHeight()
  
  self:SetCombatRadius( CombatRadius )

  return self
end


--- AI cargo
-- @param #AI_CARGO_DISPATCHER_APC self
-- @param Wrapper.Group#GROUP APC The APC carrier.
-- @param Core.Set#SET_CARGO CargoSet Cargo set.
-- @return AI.AI_Cargo_APC#AI_CARGO_DISPATCHER_APC AI cargo APC object.
function AI_CARGO_DISPATCHER_APC:AICargo( APC, CargoSet )

  local aicargoapc=AI_CARGO_APC:New(APC, CargoSet, self.CombatRadius)
  
  aicargoapc:SetDeployOffRoad(self.deployOffroad, self.deployFormation)
  aicargoapc:SetPickupOffRoad(self.pickupOffroad, self.pickupFormation)
  
  return aicargoapc
end

--- Enable/Disable unboarding of cargo (infantry) when enemies are nearby (to help defend the carrier).
-- This is only valid for APCs and trucks etc, thus ground vehicles.
-- @param #AI_CARGO_DISPATCHER_APC self
-- @param #number CombatRadius Provide the combat radius to defend the carrier by unboarding the cargo when enemies are nearby. 
-- When the combat radius is 0 (default), no defense will happen of the carrier. 
-- When the combat radius is not provided, no defense will happen!
-- @return #AI_CARGO_DISPATCHER_APC
-- @usage
-- 
-- -- Disembark the infantry when the carrier is under attack.
-- AICargoDispatcher:SetCombatRadius( 500 )
-- 
-- -- Keep the cargo in the carrier when the carrier is under attack.
-- AICargoDispatcher:SetCombatRadius( 0 )
function AI_CARGO_DISPATCHER_APC:SetCombatRadius( CombatRadius )

  self.CombatRadius = CombatRadius or 0

  return self
end

--- Set whether the carrier will *not* use roads to *pickup* and *deploy* the cargo.
-- @param #AI_CARGO_DISPATCHER_APC self
-- @param #boolean Offroad If true, carrier will not use roads.
-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`.
-- @return #AI_CARGO_DISPATCHER_APC self
function AI_CARGO_DISPATCHER_APC:SetOffRoad(Offroad, Formation)

  self:SetPickupOffRoad(Offroad, Formation)
  self:SetDeployOffRoad(Offroad, Formation)
  
  return self
end

--- Set whether the carrier will *not* use roads to *pickup* the cargo.
-- @param #AI_CARGO_DISPATCHER_APC self
-- @param #boolean Offroad If true, carrier will not use roads.
-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`.
-- @return #AI_CARGO_DISPATCHER_APC self
function AI_CARGO_DISPATCHER_APC:SetPickupOffRoad(Offroad, Formation)

  self.pickupOffroad=Offroad
  self.pickupFormation=Formation or ENUMS.Formation.Vehicle.OffRoad
  
  return self
end

--- Set whether the carrier will *not* use roads to *deploy* the cargo.
-- @param #AI_CARGO_DISPATCHER_APC self
-- @param #boolean Offroad If true, carrier will not use roads.
-- @param #number Formation Offroad formation used. Default is `ENUMS.Formation.Vehicle.Offroad`.
-- @return #AI_CARGO_DISPATCHER_APC self
function AI_CARGO_DISPATCHER_APC:SetDeployOffRoad(Offroad, Formation)

  self.deployOffroad=Offroad
  self.deployFormation=Formation or ENUMS.Formation.Vehicle.OffRoad
  
  return self
end--- **AI** - Models the intelligent transportation of infantry and other cargo using Helicopters.
--
-- ## Features:
-- 
--   * The helicopters will fly towards the pickup locations to pickup the cargo.
--   * The helicopters will fly towards the deploy zones to deploy the cargo.
--   * Precision deployment as well as randomized deployment within the deploy zones are possible.
--   * Helicopters will orbit the deploy zones when there is no space for landing until the deploy zone is free.
--   
-- ===
-- 
-- ## Test Missions:
-- 
-- Test missions can be located on the main GITHUB site.
-- 
-- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/]  
-- (https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/AID%20-%20AI%20Dispatching/AID-CGO%20-%20AI%20Cargo%20Dispatching)
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_Cargo_Dispatcher_Helicopter
-- @image AI_Cargo_Dispatching_For_Helicopters.JPG

-- @type AI_CARGO_DISPATCHER_HELICOPTER
-- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER


--- A dynamic cargo handling capability for AI helicopter groups.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- Helicopters can be mobilized to intelligently transport infantry and other cargo within the simulation.
-- 
-- 
-- The AI_CARGO_DISPATCHER_HELICOPTER module is derived from the AI_CARGO_DISPATCHER module.
-- 
-- ## Note! In order to fully understand the mechanisms of the AI_CARGO_DISPATCHER_HELICOPTER class, it is recommended that you first consult and READ the documentation of the @{AI.AI_Cargo_Dispatcher} module!!!**
-- 
-- Especially to learn how to **Tailor the different cargo handling events**, this will be very useful!
-- 
-- On top, the AI_CARGO_DISPATCHER_HELICOPTER class uses the @{Cargo.Cargo} capabilities within the MOOSE framework.
-- Also ensure that you fully understand how to declare and setup Cargo objects within the MOOSE framework before using this class.
-- CARGO derived objects must be declared within the mission to make the AI_CARGO_DISPATCHER_HELICOPTER object recognize the cargo.
-- 
-- ---
-- 
-- # 1. AI\_CARGO\_DISPATCHER\_HELICOPTER constructor.
--   
--   * @{#AI_CARGO_DISPATCHER\_HELICOPTER.New}(): Creates a new AI\_CARGO\_DISPATCHER\_HELICOPTER object.
-- 
-- ---
-- 
-- # 2. AI\_CARGO\_DISPATCHER\_HELICOPTER is a Finite State Machine.
-- 
-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed.
-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state.
-- 
-- So, each of the rows have the following structure.
-- 
--   * **From** => **Event** => **To**
-- 
-- Important to know is that an event can only be executed if the **current state** is the **From** state.
-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed,
-- and the resulting state will be the **To** state.
-- 
-- These are the different possible state transitions of this state machine implementation: 
-- 
--   * Idle => Start => Monitoring
--   * Monitoring => Monitor => Monitoring
--   * Monitoring => Stop => Idle
--      
--   * Monitoring => Pickup => Monitoring
--   * Monitoring => Load => Monitoring
--   * Monitoring => Loading => Monitoring
--   * Monitoring => Loaded => Monitoring
--   * Monitoring => PickedUp => Monitoring
--   * Monitoring => Deploy => Monitoring
--   * Monitoring => Unload => Monitoring
--   * Monitoring => Unloaded => Monitoring
--   * Monitoring => Deployed => Monitoring
--   * Monitoring => Home => Monitoring
-- 
--      
-- ## 2.1) AI_CARGO_DISPATCHER States.
-- 
--   * **Monitoring**: The process is dispatching.
--   * **Idle**: The process is idle.
-- 
-- ## 2.2) AI_CARGO_DISPATCHER Events.
-- 
--   * **Start**: Start the transport process.
--   * **Stop**: Stop the transport process.
--   * **Monitor**: Monitor and take action.
--   
--   * **Pickup**: Pickup cargo.
--   * **Load**: Load the cargo.
--   * **Loading**: The dispatcher is coordinating the loading of a cargo.
--   * **Loaded**: Flag that the cargo is loaded.
--   * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup.
--   * **Deploy**: Deploy cargo to a location.
--   * **Unload**: Unload the cargo.
--   * **Unloaded**: Flag that the cargo is unloaded.
--   * **Deployed**: All cargo is unloaded from the carriers in the group.
--   * **Home**: A Carrier is going home.
-- 
-- ## 2.3) Enhance your mission scripts with **Tailored** Event Handling!
-- 
-- Within your mission, you can capture these events when triggered, and tailor the events with your own code!
-- Check out the @{AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER} class at chapter 3 for details on the different event handlers that are available and how to use them.
-- 
-- **There are a lot of templates available that allows you to quickly setup an event handler for a specific event type!**
-- 
-- ---
-- 
-- ## 3. Set the pickup parameters.
-- 
-- Several parameters can be set to pickup cargo:
-- 
--    * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetPickupRadius}(): Sets or randomizes the pickup location for the helicopter around the cargo coordinate in a radius defined an outer and optional inner radius. 
--    * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetPickupSpeed}(): Set the speed or randomizes the speed in km/h to pickup the cargo.
--    * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetPickupHeight}(): Set the height or randomizes the height in meters to pickup the cargo.
-- 
-- ---   
--    
-- ## 4. Set the deploy parameters.
-- 
-- Several parameters can be set to deploy cargo:
-- 
--    * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetDeployRadius}(): Sets or randomizes the deploy location for the helicopter around the cargo coordinate in a radius defined an outer and an optional inner radius. 
--    * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetDeploySpeed}(): Set the speed or randomizes the speed in km/h to deploy the cargo.
--    * @{#AI_CARGO_DISPATCHER_HELICOPTER.SetDeployHeight}(): Set the height or randomizes the height in meters to deploy the cargo.
-- 
-- ---
-- 
-- ## 5. Set the home zone when there isn't any more cargo to pickup.
-- 
-- A home zone can be specified to where the Helicopters will move when there isn't any cargo left for pickup.
-- Use @{#AI_CARGO_DISPATCHER_HELICOPTER.SetHomeZone}() to specify the home zone.
-- 
-- If no home zone is specified, the helicopters will wait near the deploy zone for a new pickup command.   
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_CARGO_DISPATCHER_HELICOPTER
AI_CARGO_DISPATCHER_HELICOPTER = {
  ClassName = "AI_CARGO_DISPATCHER_HELICOPTER",
}

--- Creates a new AI_CARGO_DISPATCHER_HELICOPTER object.
-- @param #AI_CARGO_DISPATCHER_HELICOPTER self
-- @param Core.Set#SET_GROUP HelicopterSet The set of @{Wrapper.Group#GROUP} objects of helicopters that will transport the cargo.
-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects.
-- @param Core.Set#SET_ZONE PickupZoneSet (optional) The set of pickup zones, which are used to where the cargo can be picked up by the APCs. If nil, then cargo can be picked up everywhere. 
-- @param Core.Set#SET_ZONE DeployZoneSet The set of deploy zones, which are used to where the cargo will be deployed by the Helicopters. 
-- @return #AI_CARGO_DISPATCHER_HELICOPTER
-- @usage
-- 
--      -- An AI dispatcher object for a helicopter squadron, moving infantry from pickup zones to deploy zones.
-- 
--      local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart()
--      local SetHelicopter = SET_GROUP:New():FilterPrefixes( "Helicopter" ):FilterStart()
--      local SetPickupZones = SET_ZONE:New():FilterPrefixes( "Pickup" ):FilterStart()
--      local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart()
--      
--      AICargoDispatcherHelicopter = AI_CARGO_DISPATCHER_HELICOPTER:New( SetHelicopter, SetCargoInfantry, SetPickupZones, SetDeployZones ) 
--      AICargoDispatcherHelicopter:Start()
-- 
function AI_CARGO_DISPATCHER_HELICOPTER:New( HelicopterSet, CargoSet, PickupZoneSet, DeployZoneSet )

  local self = BASE:Inherit( self, AI_CARGO_DISPATCHER:New( HelicopterSet, CargoSet, PickupZoneSet, DeployZoneSet ) ) -- #AI_CARGO_DISPATCHER_HELICOPTER

  self:SetPickupSpeed( 350, 150 )
  self:SetDeploySpeed( 350, 150 )

  self:SetPickupRadius( 40, 12 )
  self:SetDeployRadius( 40, 12 )
  
  self:SetPickupHeight( 500, 200 )
  self:SetDeployHeight( 500, 200 )
  
  return self
end


function AI_CARGO_DISPATCHER_HELICOPTER:AICargo( Helicopter, CargoSet )

  local dispatcher = AI_CARGO_HELICOPTER:New( Helicopter, CargoSet )
  dispatcher:SetLandingSpeedAndHeight(27, 6)
  return dispatcher
  
end

--- **AI** - Models the intelligent transportation of infantry and other cargo using Planes.
--
-- ## Features:
--
--   * The airplanes will fly towards the pickup airbases to pickup the cargo.
--   * The airplanes will fly towards the deploy airbases to deploy the cargo.
--   
-- ===
-- 
-- ## Test Missions:
-- 
-- Test missions can be located on the main GITHUB site.
-- 
-- [FlightControl-Master/MOOSE_MISSIONS/AID - AI Dispatching/AID-CGO - AI Cargo Dispatching/]  
-- (https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/AID%20-%20AI%20Dispatching/AID-CGO%20-%20AI%20Cargo%20Dispatching)
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_Cargo_Dispatcher_Airplane
-- @image AI_Cargo_Dispatching_For_Airplanes.JPG


-- @type AI_CARGO_DISPATCHER_AIRPLANE
-- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER


--- Brings a dynamic cargo handling capability for AI groups.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- Airplanes can be mobilized to intelligently transport infantry and other cargo within the simulation.
-- 
-- The AI_CARGO_DISPATCHER_AIRPLANE module is derived from the AI_CARGO_DISPATCHER module.
-- 
-- ## Note! In order to fully understand the mechanisms of the AI_CARGO_DISPATCHER_AIRPLANE class, it is recommended that you first consult and READ the documentation of the @{AI.AI_Cargo_Dispatcher} module!!!**
-- 
-- Especially to learn how to **Tailor the different cargo handling events**, this will be very useful!
-- 
-- On top, the AI_CARGO_DISPATCHER_AIRPLANE class uses the @{Cargo.Cargo} capabilities within the MOOSE framework.
-- Also ensure that you fully understand how to declare and setup Cargo objects within the MOOSE framework before using this class.
-- CARGO derived objects must be declared within the mission to make the AI_CARGO_DISPATCHER_HELICOPTER object recognize the cargo.
-- 
-- # 1) AI_CARGO_DISPATCHER_AIRPLANE constructor.
--   
--   * @{#AI_CARGO_DISPATCHER_AIRPLANE.New}(): Creates a new AI_CARGO_DISPATCHER_AIRPLANE object.
-- 
-- ---
-- 
-- # 2) AI_CARGO_DISPATCHER_AIRPLANE is a Finite State Machine.
-- 
-- This section must be read as follows. Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed.
-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state.
-- 
-- So, each of the rows have the following structure.
-- 
--   * **From** => **Event** => **To**
-- 
-- Important to know is that an event can only be executed if the **current state** is the **From** state.
-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed,
-- and the resulting state will be the **To** state.
-- 
-- These are the different possible state transitions of this state machine implementation: 
-- 
--   * Idle => Start => Monitoring
--   * Monitoring => Monitor => Monitoring
--   * Monitoring => Stop => Idle
--      
--   * Monitoring => Pickup => Monitoring
--   * Monitoring => Load => Monitoring
--   * Monitoring => Loading => Monitoring
--   * Monitoring => Loaded => Monitoring
--   * Monitoring => PickedUp => Monitoring
--   * Monitoring => Deploy => Monitoring
--   * Monitoring => Unload => Monitoring
--   * Monitoring => Unloaded => Monitoring
--   * Monitoring => Deployed => Monitoring
--   * Monitoring => Home => Monitoring
-- 
--      
-- ## 2.1) AI_CARGO_DISPATCHER States.
-- 
--   * **Monitoring**: The process is dispatching.
--   * **Idle**: The process is idle.
-- 
-- ## 2.2) AI_CARGO_DISPATCHER Events.
-- 
--   * **Start**: Start the transport process.
--   * **Stop**: Stop the transport process.
--   * **Monitor**: Monitor and take action.
--   
--   * **Pickup**: Pickup cargo.
--   * **Load**: Load the cargo.
--   * **Loading**: The dispatcher is coordinating the loading of a cargo.
--   * **Loaded**: Flag that the cargo is loaded.
--   * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup.
--   * **Deploy**: Deploy cargo to a location.
--   * **Unload**: Unload the cargo.
--   * **Unloaded**: Flag that the cargo is unloaded.
--   * **Deployed**: All cargo is unloaded from the carriers in the group.
--   * **Home**: A Carrier is going home.
-- 
-- ## 2.3) Enhance your mission scripts with **Tailored** Event Handling!
-- 
-- Within your mission, you can capture these events when triggered, and tailor the events with your own code!
-- Check out the @{AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER} class at chapter 3 for details on the different event handlers that are available and how to use them.
-- 
-- **There are a lot of templates available that allows you to quickly setup an event handler for a specific event type!**
-- 
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- 
-- @field #AI_CARGO_DISPATCHER_AIRPLANE
AI_CARGO_DISPATCHER_AIRPLANE = {
  ClassName = "AI_CARGO_DISPATCHER_AIRPLANE",
}

--- Creates a new AI_CARGO_DISPATCHER_AIRPLANE object.
-- @param #AI_CARGO_DISPATCHER_AIRPLANE self
-- @param Core.Set#SET_GROUP AirplaneSet The set of @{Wrapper.Group#GROUP} objects of airplanes that will transport the cargo.
-- @param Core.Set#SET_CARGO CargoSet The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, CARGO_SLINGLOAD objects.
-- @param Core.Zone#SET_ZONE PickupZoneSet The set of zone airbases where the cargo has to be picked up.
-- @param Core.Zone#SET_ZONE DeployZoneSet The set of zone airbases where the cargo is deployed. Choice for each cargo is random.
-- @return #AI_CARGO_DISPATCHER_AIRPLANE self
-- @usage
-- 
--      -- An AI dispatcher object for an airplane squadron, moving infantry and vehicles from pickup airbases to deploy airbases.
--   
--      local CargoInfantrySet = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart()
--      local AirplanesSet = SET_GROUP:New():FilterPrefixes( "Airplane" ):FilterStart()
--      local PickupZoneSet = SET_ZONE:New()
--      local DeployZoneSet = SET_ZONE:New()
--      
--      PickupZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Gudauta ) )
--      DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Sochi_Adler ) )
--      DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Maykop_Khanskaya ) )
--      DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Mineralnye_Vody ) )
--      DeployZoneSet:AddZone( ZONE_AIRBASE:New( AIRBASE.Caucasus.Vaziani ) )
--      
--      AICargoDispatcherAirplanes = AI_CARGO_DISPATCHER_AIRPLANE:New( AirplanesSet, CargoInfantrySet, PickupZoneSet, DeployZoneSet ) 
--      AICargoDispatcherAirplanes:Start()
-- 
function AI_CARGO_DISPATCHER_AIRPLANE:New( AirplaneSet, CargoSet, PickupZoneSet, DeployZoneSet )

  local self = BASE:Inherit( self, AI_CARGO_DISPATCHER:New( AirplaneSet, CargoSet, PickupZoneSet, DeployZoneSet ) ) -- #AI_CARGO_DISPATCHER_AIRPLANE

  self:SetPickupSpeed( 1200, 600 )
  self:SetDeploySpeed( 1200, 600 )
  
  self:SetPickupRadius( 0, 0 )
  self:SetDeployRadius( 0, 0 )

  self:SetPickupHeight( 8000, 6000 )  
  self:SetDeployHeight( 8000, 6000 )
    
  self:SetMonitorTimeInterval( 600 )

  return self
end

function AI_CARGO_DISPATCHER_AIRPLANE:AICargo( Airplane, CargoSet )

  return AI_CARGO_AIRPLANE:New( Airplane, CargoSet )
end
--- **AI** - Models the intelligent transportation of infantry and other cargo using Ships.
--
-- ## Features:
-- 
--   * Transport cargo to various deploy zones using naval vehicles.
--   * Various @{Cargo.Cargo#CARGO} types can be transported, including infantry, vehicles, and crates.
--   * Define a deploy zone of various types to determine the destination of the cargo.
--   * Ships will follow shipping lanes as defined in the Mission Editor.
--   * Multiple ships can transport multiple cargo as a single group.
--
-- ===
--
-- ## Test Missions: 
--
-- NEED TO DO
--
-- ===
--
-- ### Author: **acrojason** (derived from AI_Cargo_Dispatcher_APC by FlightControl)
--
-- ===
--
-- @module AI.AI_Cargo_Dispatcher_Ship
-- @image AI_Cargo_Dispatcher.JPG

-- @type AI_CARGO_DISPATCHER_SHIP
-- @extends AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER


--- A dynamic cargo transportation capability for AI groups.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- Naval vessels can be mobilized to semi-intelligently transport cargo within the simulation.
--
-- The AI_CARGO_DISPATCHER_SHIP module is derived from the AI_CARGO_DISPATCHER module.
--
-- ## Note! In order to fully understand the mechanisms of the AI_CARGO_DISPATCHER_SHIP class, it is recommended that you first consult and READ the documentation of the @{AI.AI_Cargo_Dispatcher} module!!!
--
-- This will be particularly helpful in order to determine how to **Tailor the different cargo handling events**.
--
-- The AI_CARGO_DISPATCHER_SHIP class uses the @{Cargo.Cargo} capabilities within the MOOSE framework.
-- Also ensure that you fully understand how to declare and setup Cargo objects within the MOOSE framework before using this class.
-- CARGO derived objects must generally be declared within the mission to make the AI_CARGO_DISPATCHER_SHIP object recognize the cargo.
--
--
-- # 1) AI_CARGO_DISPATCHER_SHIP constructor.
-- 
--   * @{#AI_CARGO_DISPATCHER_SHIP.New}(): Creates a new AI_CARGO_DISPATCHER_SHIP object.
--
-- ---
--
-- # 2) AI_CARGO_DISPATCHER_SHIP is a Finite State Machine.
--
-- This section must be read as follows... Each of the rows indicate a state transition, triggered through an event, and with an ending state of the event was executed.
-- The first column is the **From** state, the second column the **Event**, and the third column the **To** state.
--
-- So, each of the rows have the following structure.
--
--   * **From** => **Event** => **To**
--
-- Important to know is that an event can only be executed if the **current state** is the **From** state.
-- This, when an **Event** that is being triggered has a **From** state that is equal to the **Current** state of the state machine, the event will be executed,
-- and the resulting state will be the **To** state.
--
-- These are the different possible state transitions of this state machine implementation:
-- 
--   * Idle => Start => Monitoring
--   * Monitoring => Monitor => Monitoring
--   * Monitoring => Stop => Idle
--      
--   * Monitoring => Pickup => Monitoring
--   * Monitoring => Load => Monitoring
--   * Monitoring => Loading => Monitoring
--   * Monitoring => Loaded => Monitoring
--   * Monitoring => PickedUp => Monitoring
--   * Monitoring => Deploy => Monitoring
--   * Monitoring => Unload => Monitoring
--   * Monitoring => Unloaded => Monitoring
--   * Monitoring => Deployed => Monitoring
--   * Monitoring => Home => Monitoring
-- 
--      
-- ## 2.1) AI_CARGO_DISPATCHER States.
-- 
--   * **Monitoring**: The process is dispatching.
--   * **Idle**: The process is idle.
-- 
-- ## 2.2) AI_CARGO_DISPATCHER Events.
-- 
--   * **Start**: Start the transport process.
--   * **Stop**: Stop the transport process.
--   * **Monitor**: Monitor and take action.
--   
--   * **Pickup**: Pickup cargo.
--   * **Load**: Load the cargo.
--   * **Loading**: The dispatcher is coordinating the loading of a cargo.
--   * **Loaded**: Flag that the cargo is loaded.
--   * **PickedUp**: The dispatcher has loaded all requested cargo into the CarrierGroup.
--   * **Deploy**: Deploy cargo to a location.
--   * **Unload**: Unload the cargo.
--   * **Unloaded**: Flag that the cargo is unloaded.
--   * **Deployed**: All cargo is unloaded from the carriers in the group.
--   * **Home**: A Carrier is going home.
-- 
-- ## 2.3) Enhance your mission scripts with **Tailored** Event Handling!
-- 
-- Within your mission, you can capture these events when triggered, and tailor the events with your own code!
-- Check out the @{AI.AI_Cargo_Dispatcher#AI_CARGO_DISPATCHER} class at chapter 3 for details on the different event handlers that are available and how to use them.
-- 
-- **There are a lot of templates available that allows you to quickly setup an event handler for a specific event type!**
-- 
-- ---
-- 
-- # 3) Set the pickup parameters.
-- 
-- Several parameters can be set to pickup cargo:
-- 
--    * @{#AI_CARGO_DISPATCHER_SHIP.SetPickupRadius}(): Sets or randomizes the pickup location for the Ship around the cargo coordinate in a radius defined an outer and optional inner radius. 
--    * @{#AI_CARGO_DISPATCHER_SHIP.SetPickupSpeed}(): Set the speed or randomizes the speed in km/h to pickup the cargo.
--    
-- # 4) Set the deploy parameters.
-- 
-- Several parameters can be set to deploy cargo:
-- 
--    * @{#AI_CARGO_DISPATCHER_SHIP.SetDeployRadius}(): Sets or randomizes the deploy location for the Ship around the cargo coordinate in a radius defined an outer and an optional inner radius. 
--    * @{#AI_CARGO_DISPATCHER_SHIP.SetDeploySpeed}(): Set the speed or randomizes the speed in km/h to deploy the cargo.
-- 
-- # 5) Set the home zone when there isn't any more cargo to pickup.
-- 
-- A home zone can be specified to where the Ship will move when there isn't any cargo left for pickup.
-- Use @{#AI_CARGO_DISPATCHER_SHIP.SetHomeZone}() to specify the home zone.
-- 
-- If no home zone is specified, the Ship will wait near the deploy zone for a new pickup command.   
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- @field #AI_CARGO_DISPATCHER_SHIP
AI_CARGO_DISPATCHER_SHIP = {
    ClassName = "AI_CARGO_DISPATCHER_SHIP"
  }
  
--- Creates a new AI_CARGO_DISPATCHER_SHIP object.
-- @param #AI_CARGO_DISPATCHER_SHIP self
-- @param Core.Set#SET_GROUP ShipSet  The set of @{Wrapper.Group#GROUP} objects of Ships that will transport the cargo
-- @param Core.Set#SET_CARGO CargoSet  The set of @{Cargo.Cargo#CARGO} objects, which can be CARGO_GROUP, CARGO_CRATE, or CARGO_SLINGLOAD objects.
-- @param Core.Set#SET_ZONE PickupZoneSet  The set of pickup zones which are used to determine from where the cargo can be picked up by the Ship. 
-- @param Core.Set#SET_ZONE DeployZoneSet  The set of deploy zones which determine where the cargo will be deployed by the Ship.
-- @param #table ShippingLane  Table containing list of Shipping Lanes to be used
-- @return #AI_CARGO_DISPATCHER_SHIP
-- @usage
--
--      -- An AI dispatcher object for a naval group, moving cargo from pickup zones to deploy zones via a predetermined Shipping Lane
--
--      local SetCargoInfantry = SET_CARGO:New():FilterTypes( "Infantry" ):FilterStart()
--      local SetShip = SET_GROUP:New():FilterPrefixes( "Ship" ):FilterStart()
--      local SetPickupZones = SET_ZONE:New():FilterPrefixes( "Pickup" ):FilterStart()
--      local SetDeployZones = SET_ZONE:New():FilterPrefixes( "Deploy" ):FilterStart()
--      NEED MORE THOUGHT - ShippingLane is part of Warehouse.......
--      local ShippingLane = SET_GROUP:New():FilterPrefixes( "ShippingLane" ):FilterOnce():GetSetObjects()
-- 
--      AICargoDispatcherShip = AI_CARGO_DISPATCHER_SHIP:New( SetShip, SetCargoInfantry, SetPickupZones, SetDeployZones, ShippingLane )
--      AICargoDispatcherShip:Start()
--
function AI_CARGO_DISPATCHER_SHIP:New( ShipSet, CargoSet, PickupZoneSet, DeployZoneSet, ShippingLane )

  local self = BASE:Inherit( self, AI_CARGO_DISPATCHER:New( ShipSet, CargoSet, PickupZoneSet, DeployZoneSet ) )

  self:SetPickupSpeed( 60, 10 )
  self:SetDeploySpeed( 60, 10 )

  self:SetPickupRadius( 500, 6000 )
  self:SetDeployRadius( 500, 6000 )

  self:SetPickupHeight( 0, 0 )
  self:SetDeployHeight( 0, 0 )

  self:SetShippingLane( ShippingLane )

  self:SetMonitorTimeInterval( 600 )

  return self
end

function AI_CARGO_DISPATCHER_SHIP:SetShippingLane( ShippingLane )
  self.ShippingLane = ShippingLane

  return self

end

function AI_CARGO_DISPATCHER_SHIP:AICargo( Ship, CargoSet )

  return AI_CARGO_SHIP:New( Ship, CargoSet, 0, self.ShippingLane )
end--- (SP) (MP) (FSM) Accept or reject process for player (task) assignments.
--
-- ===
-- 
-- ![Banner Image](..\Images\deprecated.png)
--
-- # @{#ACT_ASSIGN} FSM template class, extends @{Core.Fsm#FSM_PROCESS}
--
-- ## ACT_ASSIGN state machine:
--
-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur.
-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below.
-- Each derived class follows exactly the same process, using the same events and following the same state transitions,
-- but will have **different implementation behaviour** upon each event or state transition.
--
-- ### ACT_ASSIGN **Events**:
--
-- These are the events defined in this class:
--
--   * **Start**:  Start the tasking acceptance process.
--   * **Assign**:  Assign the task.
--   * **Reject**:  Reject the task..
--
-- ### ACT_ASSIGN **Event methods**:
--
-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process.
-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine:
--
--   * **Immediate**: The event method has exactly the name of the event.
--   * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed.
--
-- ### ACT_ASSIGN **States**:
--
--   * **UnAssigned**: The player has not accepted the task.
--   * **Assigned (*)**: The player has accepted the task.
--   * **Rejected (*)**: The player has not accepted the task.
--   * **Waiting**: The process is awaiting player feedback.
--   * **Failed (*)**: The process has failed.
--
-- (*) End states of the process.
--
-- ### ACT_ASSIGN state transition methods:
--
-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state.
-- There are 2 moments when state transition methods will be called by the state machine:
--
--   * **Before** the state transition.
--     The state transition method needs to start with the name **OnBefore + the name of the state**.
--     If the state transition method returns false, then the processing of the state transition will not be done!
--     If you want to change the behaviour of the AIControllable at this event, return false,
--     but then you'll need to specify your own logic using the AIControllable!
--
--   * **After** the state transition.
--     The state transition method needs to start with the name **OnAfter + the name of the state**.
--     These state transition methods need to provide a return value, which is specified at the function description.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
--
-- # 1) @{#ACT_ASSIGN_ACCEPT} class, extends @{Core.Fsm#ACT_ASSIGN}
--
-- The ACT_ASSIGN_ACCEPT class accepts by default a task for a player. No player intervention is allowed to reject the task.
--
-- ## 1.1) ACT_ASSIGN_ACCEPT constructor:
--
--   * @{#ACT_ASSIGN_ACCEPT.New}(): Creates a new ACT_ASSIGN_ACCEPT object.
--
-- ===
--
-- # 2) @{#ACT_ASSIGN_MENU_ACCEPT} class, extends @{Core.Fsm#ACT_ASSIGN}
--
-- The ACT_ASSIGN_MENU_ACCEPT class accepts a task when the player accepts the task through an added menu option.
-- This assignment type is useful to conditionally allow the player to choose whether or not he would accept the task.
-- The assignment type also allows to reject the task.
--
-- ## 2.1) ACT_ASSIGN_MENU_ACCEPT constructor:
-- -----------------------------------------
--
--   * @{#ACT_ASSIGN_MENU_ACCEPT.New}(): Creates a new ACT_ASSIGN_MENU_ACCEPT object.
--
-- ===
--
-- @module Actions.Act_Assign
-- @image MOOSE.JPG


do -- ACT_ASSIGN

  --- ACT_ASSIGN class
  -- @type ACT_ASSIGN
  -- @field Tasking.Task#TASK Task
  -- @field Wrapper.Unit#UNIT ProcessUnit
  -- @field Core.Zone#ZONE_BASE TargetZone
  -- @extends Core.Fsm#FSM_PROCESS
  ACT_ASSIGN = {
    ClassName = "ACT_ASSIGN",
  }


  --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted.
  -- @param #ACT_ASSIGN self
  -- @return #ACT_ASSIGN The task acceptance process.
  function ACT_ASSIGN:New()

    -- Inherits from BASE
    local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIGN" ) ) -- Core.Fsm#FSM_PROCESS

    self:AddTransition( "UnAssigned", "Start", "Waiting" )
    self:AddTransition( "Waiting",  "Assign", "Assigned" )
    self:AddTransition( "Waiting", "Reject", "Rejected" )
    self:AddTransition( "*", "Fail", "Failed" )

    self:AddEndState( "Assigned" )
    self:AddEndState( "Rejected" )
    self:AddEndState( "Failed" )

    self:SetStartState( "UnAssigned" )

    return self
  end

end -- ACT_ASSIGN



do -- ACT_ASSIGN_ACCEPT

  --- ACT_ASSIGN_ACCEPT class
  -- @type ACT_ASSIGN_ACCEPT
  -- @field Tasking.Task#TASK Task
  -- @field Wrapper.Unit#UNIT ProcessUnit
  -- @field Core.Zone#ZONE_BASE TargetZone
  -- @extends #ACT_ASSIGN
  ACT_ASSIGN_ACCEPT = {
    ClassName = "ACT_ASSIGN_ACCEPT",
  }


  --- Creates a new task assignment state machine. The process will accept the task by default, no player intervention accepted.
  -- @param #ACT_ASSIGN_ACCEPT self
  -- @param #string TaskBriefing
  function ACT_ASSIGN_ACCEPT:New( TaskBriefing )

    local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_ACCEPT

    self.TaskBriefing = TaskBriefing

    return self
  end

  function ACT_ASSIGN_ACCEPT:Init( FsmAssign )

    self.TaskBriefing = FsmAssign.TaskBriefing
  end

  --- StateMachine callback function
  -- @param #ACT_ASSIGN_ACCEPT self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ASSIGN_ACCEPT:onafterStart( ProcessUnit, Task, From, Event, To )

    self:__Assign( 1 )
  end

  --- StateMachine callback function
  -- @param #ACT_ASSIGN_ACCEPT self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ASSIGN_ACCEPT:onenterAssigned( ProcessUnit, Task, From, Event, To, TaskGroup )

    self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() )
  end

end -- ACT_ASSIGN_ACCEPT


do -- ACT_ASSIGN_MENU_ACCEPT

  --- ACT_ASSIGN_MENU_ACCEPT class
  -- @type ACT_ASSIGN_MENU_ACCEPT
  -- @field Tasking.Task#TASK Task
  -- @field Wrapper.Unit#UNIT ProcessUnit
  -- @field Core.Zone#ZONE_BASE TargetZone
  -- @extends #ACT_ASSIGN
  ACT_ASSIGN_MENU_ACCEPT = {
    ClassName = "ACT_ASSIGN_MENU_ACCEPT",
  }

  --- Init.
  -- @param #ACT_ASSIGN_MENU_ACCEPT self
  -- @param #string TaskBriefing
  -- @return #ACT_ASSIGN_MENU_ACCEPT self
  function ACT_ASSIGN_MENU_ACCEPT:New( TaskBriefing )

    -- Inherits from BASE
    local self = BASE:Inherit( self, ACT_ASSIGN:New() ) -- #ACT_ASSIGN_MENU_ACCEPT

    self.TaskBriefing = TaskBriefing

    return self
  end


  --- Creates a new task assignment state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator.
  -- @param #ACT_ASSIGN_MENU_ACCEPT self
  -- @param #string TaskBriefing
  -- @return #ACT_ASSIGN_MENU_ACCEPT self
  function ACT_ASSIGN_MENU_ACCEPT:Init( TaskBriefing )

    self.TaskBriefing = TaskBriefing

    return self
  end

  --- StateMachine callback function
  -- @param #ACT_ASSIGN_MENU_ACCEPT self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ASSIGN_MENU_ACCEPT:onafterStart( ProcessUnit, Task, From, Event, To )

    self:GetCommandCenter():MessageToGroup( "Task " .. self.Task:GetName() .. " has been assigned to you and your group!\nRead the briefing and use the Radio Menu (F10) / Task ... CONFIRMATION menu to accept or reject the task.\nYou have 2 minutes to accept, or the task assignment will be cancelled!", ProcessUnit:GetGroup(), 120 )

    local TaskGroup = ProcessUnit:GetGroup()

    self.Menu = MENU_GROUP:New( TaskGroup, "Task " .. self.Task:GetName() .. " CONFIRMATION" )
    self.MenuAcceptTask = MENU_GROUP_COMMAND:New( TaskGroup, "Accept task " .. self.Task:GetName(), self.Menu, self.MenuAssign, self, TaskGroup )
    self.MenuRejectTask = MENU_GROUP_COMMAND:New( TaskGroup, "Reject task " .. self.Task:GetName(), self.Menu, self.MenuReject, self, TaskGroup )

    self:__Reject( 120, TaskGroup )
  end

  --- Menu function.
  -- @param #ACT_ASSIGN_MENU_ACCEPT self
  function ACT_ASSIGN_MENU_ACCEPT:MenuAssign( TaskGroup )

    self:__Assign( -1, TaskGroup )
  end

  --- Menu function.
  -- @param #ACT_ASSIGN_MENU_ACCEPT self
  function ACT_ASSIGN_MENU_ACCEPT:MenuReject( TaskGroup )

    self:__Reject( -1, TaskGroup )
  end

  --- StateMachine callback function
  -- @param #ACT_ASSIGN_MENU_ACCEPT self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ASSIGN_MENU_ACCEPT:onafterAssign( ProcessUnit, Task, From, Event, To, TaskGroup  )

    self.Menu:Remove()
  end

  --- StateMachine callback function
  -- @param #ACT_ASSIGN_MENU_ACCEPT self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ASSIGN_MENU_ACCEPT:onafterReject( ProcessUnit, Task, From, Event, To, TaskGroup )
    self:F( { TaskGroup = TaskGroup } )

    self.Menu:Remove()
    --TODO: need to resolve this problem ... it has to do with the events ...
    --self.Task:UnAssignFromUnit( ProcessUnit )needs to become a callback funtion call upon the event
    self.Task:RejectGroup( TaskGroup )
  end

  --- StateMachine callback function
  -- @param #ACT_ASSIGN_ACCEPT self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ASSIGN_MENU_ACCEPT:onenterAssigned( ProcessUnit, Task, From, Event, To, TaskGroup )

    --self.Task:AssignToGroup( TaskGroup )
    self.Task:Assign( ProcessUnit, ProcessUnit:GetPlayerName() )
  end

end -- ACT_ASSIGN_MENU_ACCEPT
--- (SP) (MP) (FSM) Route AI or players through waypoints or to zones.
--
-- ===
--
-- ![Banner Image](..\Images\deprecated.png)
--
-- # @{#ACT_ROUTE} FSM class, extends @{Core.Fsm#FSM_PROCESS}
--
-- ## ACT_ROUTE state machine:
--
-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur.
-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below.
-- Each derived class follows exactly the same process, using the same events and following the same state transitions,
-- but will have **different implementation behaviour** upon each event or state transition.
--
-- ### ACT_ROUTE **Events**:
--
-- These are the events defined in this class:
--
--   * **Start**:  The process is started. The process will go into the Report state.
--   * **Report**: The process is reporting to the player the route to be followed.
--   * **Route**: The process is routing the controllable.
--   * **Pause**: The process is pausing the route of the controllable.
--   * **Arrive**: The controllable has arrived at a route point.
--   * **More**:  There are more route points that need to be followed. The process will go back into the Report state.
--   * **NoMore**:  There are no more route points that need to be followed. The process will go into the Success state.
--
-- ### ACT_ROUTE **Event methods**:
--
-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process.
-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine:
--
--   * **Immediate**: The event method has exactly the name of the event.
--   * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed.
--
-- ### ACT_ROUTE **States**:
--
--   * **None**: The controllable did not receive route commands.
--   * **Arrived (*)**: The controllable has arrived at a route point.
--   * **Aborted (*)**: The controllable has aborted the route path.
--   * **Routing**: The controllable is understay to the route point.
--   * **Pausing**: The process is pausing the routing. AI air will go into hover, AI ground will stop moving. Players can fly around.
--   * **Success (*)**: All route points were reached.
--   * **Failed (*)**: The process has failed.
--
-- (*) End states of the process.
--
-- ### ACT_ROUTE state transition methods:
--
-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state.
-- There are 2 moments when state transition methods will be called by the state machine:
--
--   * **Before** the state transition.
--     The state transition method needs to start with the name **OnBefore + the name of the state**.
--     If the state transition method returns false, then the processing of the state transition will not be done!
--     If you want to change the behaviour of the AIControllable at this event, return false,
--     but then you'll need to specify your own logic using the AIControllable!
--
--   * **After** the state transition.
--     The state transition method needs to start with the name **OnAfter + the name of the state**.
--     These state transition methods need to provide a return value, which is specified at the function description.
--
-- ===
--
-- # 1) @{#ACT_ROUTE_ZONE} class, extends @{#ACT_ROUTE}
--
-- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Wrapper.Controllable} player @{Wrapper.Unit} to a @{Core.Zone}.
-- The player receives on perioding times messages with the coordinates of the route to follow.
-- Upon arrival at the zone, a confirmation of arrival is sent, and the process will be ended.
--
-- # 1.1) ACT_ROUTE_ZONE constructor:
--
--   * @{#ACT_ROUTE_ZONE.New}(): Creates a new ACT_ROUTE_ZONE object.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
--
-- @module Actions.Act_Route
-- @image MOOSE.JPG


do -- ACT_ROUTE

  --- ACT_ROUTE class
  -- @type ACT_ROUTE
  -- @field Tasking.Task#TASK TASK
  -- @field Wrapper.Unit#UNIT ProcessUnit
  -- @field Core.Zone#ZONE_BASE Zone
  -- @field Core.Point#COORDINATE Coordinate
  -- @extends Core.Fsm#FSM_PROCESS
  ACT_ROUTE = {
    ClassName = "ACT_ROUTE",
  }


  --- Creates a new routing state machine. The process will route a CLIENT to a ZONE until the CLIENT is within that ZONE.
  -- @param #ACT_ROUTE self
  -- @return #ACT_ROUTE self
  function ACT_ROUTE:New()

    -- Inherits from BASE
    local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ROUTE" ) ) -- Core.Fsm#FSM_PROCESS

    self:AddTransition( "*", "Reset", "None" )
    self:AddTransition( "None", "Start", "Routing" )
    self:AddTransition( "*", "Report", "*" )
    self:AddTransition( "Routing", "Route", "Routing" )
    self:AddTransition( "Routing", "Pause", "Pausing" )
    self:AddTransition( "Routing", "Arrive", "Arrived" )
    self:AddTransition( "*", "Cancel", "Cancelled" )
    self:AddTransition( "Arrived", "Success", "Success" )
    self:AddTransition( "*", "Fail", "Failed" )
    self:AddTransition( "", "", "" )
    self:AddTransition( "", "", "" )

    self:AddEndState( "Arrived" )
    self:AddEndState( "Failed" )
    self:AddEndState( "Cancelled" )

    self:SetStartState( "None" )

    self:SetRouteMode( "C" )

    return self
  end

  --- Set a Cancel Menu item.
  -- @param #ACT_ROUTE self
  -- @return #ACT_ROUTE
  function ACT_ROUTE:SetMenuCancel( MenuGroup, MenuText, ParentMenu, MenuTime, MenuTag )

    self.CancelMenuGroupCommand = MENU_GROUP_COMMAND:New(
      MenuGroup,
      MenuText,
      ParentMenu,
      self.MenuCancel,
      self
    ):SetTime( MenuTime ):SetTag( MenuTag )

    ParentMenu:SetTime( MenuTime )

    ParentMenu:Remove( MenuTime, MenuTag )

    return self
  end

  --- Set the route mode.
  -- There are 2 route modes supported:
  --
  --   * SetRouteMode( "B" ): Route mode is Bearing and Range.
  --   * SetRouteMode( "C" ): Route mode is LL or MGRS according coordinate system setup.
  --
  -- @param #ACT_ROUTE self
  -- @return #ACT_ROUTE
  function ACT_ROUTE:SetRouteMode( RouteMode )

    self.RouteMode = RouteMode

    return self
  end

  --- Get the routing text to be displayed.
  -- The route mode determines the text displayed.
  -- @param #ACT_ROUTE self
  -- @param Wrapper.Unit#UNIT Controllable
  -- @return #string
  function ACT_ROUTE:GetRouteText( Controllable )

    local RouteText = ""

    local Coordinate = nil -- Core.Point#COORDINATE

    if self.Coordinate then
      Coordinate = self.Coordinate
    end

    if self.Zone then
      Coordinate = self.Zone:GetPointVec3( self.Altitude )
      Coordinate:SetHeading( self.Heading )
    end


    local Task = self:GetTask() -- This is to dermine that the coordinates are for a specific task mode (A2A or A2G).
    local CC = self:GetTask():GetMission():GetCommandCenter()
    if CC then
      if CC:IsModeWWII() then
        -- Find closest reference point to the target.
        local ShortestDistance = 0
        local ShortestReferencePoint = nil
        local ShortestReferenceName = ""
        self:F( { CC.ReferencePoints } )
        for ZoneName, Zone in pairs( CC.ReferencePoints ) do
          self:F( { ZoneName = ZoneName } )
          local Zone = Zone -- Core.Zone#ZONE
          local ZoneCoord = Zone:GetCoordinate()
          local ZoneDistance = ZoneCoord:Get2DDistance( Coordinate )
          self:F( { ShortestDistance, ShortestReferenceName } )
          if ShortestDistance == 0 or ZoneDistance < ShortestDistance then
            ShortestDistance = ZoneDistance
            ShortestReferencePoint = ZoneCoord
            ShortestReferenceName = CC.ReferenceNames[ZoneName]
          end
        end
        if ShortestReferencePoint then
          RouteText = Coordinate:ToStringFromRP( ShortestReferencePoint, ShortestReferenceName, Controllable )
        end
      else
        RouteText = Coordinate:ToString( Controllable, nil, Task )
      end
    end

    return RouteText
  end


  function ACT_ROUTE:MenuCancel()
    self:F("Cancelled")
    self.CancelMenuGroupCommand:Remove()
    self:__Cancel( 1 )
  end

  --- Task Events

  --- StateMachine callback function
  -- @param #ACT_ROUTE self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ROUTE:onafterStart( ProcessUnit, From, Event, To )


    self:__Route( 1 )
  end

  --- Check if the controllable has arrived.
  -- @param #ACT_ROUTE self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @return #boolean
  function ACT_ROUTE:onfuncHasArrived( ProcessUnit )
    return false
  end

  --- StateMachine callback function
  -- @param #ACT_ROUTE self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ROUTE:onbeforeRoute( ProcessUnit, From, Event, To )

    if ProcessUnit:IsAlive() then
      local HasArrived = self:onfuncHasArrived( ProcessUnit ) -- Polymorphic
      if self.DisplayCount >= self.DisplayInterval then
        self:T( { HasArrived = HasArrived } )
        if not HasArrived then
          self:Report()
        end
        self.DisplayCount = 1
      else
        self.DisplayCount = self.DisplayCount + 1
      end

      if HasArrived then
        self:__Arrive( 1 )
      else
        self:__Route( 1 )
      end

      return HasArrived -- if false, then the event will not be executed...
    end

    return false

  end

end -- ACT_ROUTE


do -- ACT_ROUTE_POINT

  --- ACT_ROUTE_POINT class
  -- @type ACT_ROUTE_POINT
  -- @field Tasking.Task#TASK TASK
  -- @extends #ACT_ROUTE
  ACT_ROUTE_POINT = {
    ClassName = "ACT_ROUTE_POINT",
  }


  --- Creates a new routing state machine.
  -- The task will route a controllable to a Coordinate until the controllable is within the Range.
  -- @param #ACT_ROUTE_POINT self
  -- @param Core.Point#COORDINATE The Coordinate to Target.
  -- @param #number Range The Distance to Target.
  -- @param Core.Zone#ZONE_BASE Zone
  function ACT_ROUTE_POINT:New( Coordinate, Range )
    local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_POINT

    self.Coordinate = Coordinate
    self.Range = Range or 0

    self.DisplayInterval = 30
    self.DisplayCount = 30
    self.DisplayMessage = true
    self.DisplayTime = 10 -- 10 seconds is the default

    return self
  end

  --- Creates a new routing state machine.
  -- The task will route a controllable to a Coordinate until the controllable is within the Range.
  -- @param #ACT_ROUTE_POINT self
  function ACT_ROUTE_POINT:Init( FsmRoute )

    self.Coordinate = FsmRoute.Coordinate
    self.Range = FsmRoute.Range or 0

    self.DisplayInterval = 30
    self.DisplayCount = 30
    self.DisplayMessage = true
    self.DisplayTime = 10 -- 10 seconds is the default
    self:SetStartState("None")
  end

  --- Set Coordinate
  -- @param #ACT_ROUTE_POINT self
  -- @param Core.Point#COORDINATE Coordinate The Coordinate to route to.
  function ACT_ROUTE_POINT:SetCoordinate( Coordinate )
    self:F2( { Coordinate } )
    self.Coordinate = Coordinate
  end

  --- Get Coordinate
  -- @param #ACT_ROUTE_POINT self
  -- @return Core.Point#COORDINATE Coordinate The Coordinate to route to.
  function ACT_ROUTE_POINT:GetCoordinate()
    self:F2( { self.Coordinate } )
    return self.Coordinate
  end

  --- Set Range around Coordinate
  -- @param #ACT_ROUTE_POINT self
  -- @param #number Range The Range to consider the arrival. Default is 10000 meters.
  function ACT_ROUTE_POINT:SetRange( Range )
    self:F2( { Range } )
    self.Range = Range or 10000
  end

  --- Get Range around Coordinate
  -- @param #ACT_ROUTE_POINT self
  -- @return #number The Range to consider the arrival. Default is 10000 meters.
  function ACT_ROUTE_POINT:GetRange()
    self:F2( { self.Range } )
    return self.Range
  end

  --- Method override to check if the controllable has arrived.
  -- @param #ACT_ROUTE_POINT self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @return #boolean
  function ACT_ROUTE_POINT:onfuncHasArrived( ProcessUnit )

    if ProcessUnit:IsAlive() then
      local Distance = self.Coordinate:Get2DDistance( ProcessUnit:GetCoordinate() )

      if Distance <= self.Range then
        local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", you have arrived."
        self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Information )
        return true
      end
    end

    return false
  end

  --- Task Events

  --- StateMachine callback function
  -- @param #ACT_ROUTE_POINT self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ROUTE_POINT:onafterReport( ProcessUnit, From, Event, To )

    local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", " .. self:GetRouteText( ProcessUnit )

    self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Update )
  end

end -- ACT_ROUTE_POINT


do -- ACT_ROUTE_ZONE

  --- ACT_ROUTE_ZONE class
  -- @type ACT_ROUTE_ZONE
  -- @field Tasking.Task#TASK TASK
  -- @field Wrapper.Unit#UNIT ProcessUnit
  -- @field Core.Zone#ZONE_BASE Zone
  -- @extends #ACT_ROUTE
  ACT_ROUTE_ZONE = {
    ClassName = "ACT_ROUTE_ZONE",
  }


  --- Creates a new routing state machine. The task will route a controllable to a ZONE until the controllable is within that ZONE.
  -- @param #ACT_ROUTE_ZONE self
  -- @param Core.Zone#ZONE_BASE Zone
  function ACT_ROUTE_ZONE:New( Zone )
    local self = BASE:Inherit( self, ACT_ROUTE:New() ) -- #ACT_ROUTE_ZONE

    self.Zone = Zone

    self.DisplayInterval = 30
    self.DisplayCount = 30
    self.DisplayMessage = true
    self.DisplayTime = 10 -- 10 seconds is the default

    return self
  end

  function ACT_ROUTE_ZONE:Init( FsmRoute )

    self.Zone = FsmRoute.Zone

    self.DisplayInterval = 30
    self.DisplayCount = 30
    self.DisplayMessage = true
    self.DisplayTime = 10 -- 10 seconds is the default
  end

  --- Set Zone
  -- @param #ACT_ROUTE_ZONE self
  -- @param Core.Zone#ZONE_BASE Zone The Zone object where to route to.
  -- @param #number Altitude
  -- @param #number Heading
  function ACT_ROUTE_ZONE:SetZone( Zone, Altitude, Heading ) -- R2.2 Added altitude and heading
    self.Zone = Zone
    self.Altitude = Altitude
    self.Heading = Heading
  end

  --- Get Zone
  -- @param #ACT_ROUTE_ZONE self
  -- @return Core.Zone#ZONE_BASE Zone The Zone object where to route to.
  function ACT_ROUTE_ZONE:GetZone()
    return self.Zone
  end

  --- Method override to check if the controllable has arrived.
  -- @param #ACT_ROUTE self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @return #boolean
  function ACT_ROUTE_ZONE:onfuncHasArrived( ProcessUnit )

    if ProcessUnit:IsInZone( self.Zone ) then
      local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", you have arrived within the zone."
      self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Information )
    end

    return ProcessUnit:IsInZone( self.Zone )
  end

  --- Task Events

  --- StateMachine callback function
  -- @param #ACT_ROUTE_ZONE self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ROUTE_ZONE:onafterReport( ProcessUnit, From, Event, To )
    self:F( { ProcessUnit = ProcessUnit } )

    local RouteText = "Task \"" .. self:GetTask():GetName() .. "\", " .. self:GetRouteText( ProcessUnit )
    self:GetCommandCenter():MessageTypeToGroup( RouteText, ProcessUnit:GetGroup(), MESSAGE.Type.Update )
  end

end -- ACT_ROUTE_ZONE
--- **Actions** - ACT_ACCOUNT_ classes **account for** (detect, count & report) various DCS events occurring on UNITs.
--
-- ![Banner Image](..\Images\deprecated.png)
--
-- ===
--
-- @module Actions.Act_Account
-- @image MOOSE.JPG

do -- ACT_ACCOUNT
  
  --- # @{#ACT_ACCOUNT} FSM class, extends @{Core.Fsm#FSM_PROCESS}
  -- 
  -- ![Banner Image](..\Images\deprecated.png)
  -- 
  -- ## ACT_ACCOUNT state machine:
  --
  -- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur.
  -- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below.
  -- Each derived class follows exactly the same process, using the same events and following the same state transitions,
  -- but will have **different implementation behaviour** upon each event or state transition.
  --
  -- ### ACT_ACCOUNT States
  --
  --   * **Assigned**: The player is assigned.
  --   * **Waiting**: Waiting for an event.
  --   * **Report**: Reporting.
  --   * **Account**: Account for an event.
  --   * **Accounted**: All events have been accounted for, end of the process.
  --   * **Failed**: Failed the process.
  --
  -- ### ACT_ACCOUNT Events
  --
  --   * **Start**: Start the process.
  --   * **Wait**: Wait for an event.
  --   * **Report**: Report the status of the accounting.
  --   * **Event**: An event happened, process the event.
  --   * **More**: More targets.
  --   * **NoMore (*)**: No more targets.
  --   * **Fail (*)**: The action process has failed.
  --
  -- (*) End states of the process.
  --
  -- ### ACT_ACCOUNT state transition methods:
  --
  -- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state.
  -- There are 2 moments when state transition methods will be called by the state machine:
  --
  --   * **Before** the state transition.
  --     The state transition method needs to start with the name **OnBefore + the name of the state**.
  --     If the state transition method returns false, then the processing of the state transition will not be done!
  --     If you want to change the behaviour of the AIControllable at this event, return false,
  --     but then you'll need to specify your own logic using the AIControllable!
  --
  --   * **After** the state transition.
  --     The state transition method needs to start with the name **OnAfter + the name of the state**.
  --     These state transition methods need to provide a return value, which is specified at the function description.
  --
  -- # Developer Note
  -- 
  -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
  -- Therefore, this class is considered to be deprecated
  --
  -- @type ACT_ACCOUNT
  -- @field Core.Set#SET_UNIT TargetSetUnit
  -- @extends Core.Fsm#FSM_PROCESS
  ACT_ACCOUNT = {
    ClassName = "ACT_ACCOUNT",
    TargetSetUnit = nil,
  }

  --- Creates a new DESTROY process.
  -- @param #ACT_ACCOUNT self
  -- @return #ACT_ACCOUNT
  function ACT_ACCOUNT:New()

    -- Inherits from BASE
    local self = BASE:Inherit( self, FSM_PROCESS:New() ) -- Core.Fsm#FSM_PROCESS

    self:AddTransition( "Assigned", "Start", "Waiting" )
    self:AddTransition( "*", "Wait", "Waiting" )
    self:AddTransition( "*", "Report", "Report" )
    self:AddTransition( "*", "Event", "Account" )
    self:AddTransition( "Account", "Player", "AccountForPlayer" )
    self:AddTransition( "Account", "Other", "AccountForOther" )
    self:AddTransition( { "Account", "AccountForPlayer", "AccountForOther" }, "More", "Wait" )
    self:AddTransition( { "Account", "AccountForPlayer", "AccountForOther" }, "NoMore", "Accounted" )
    self:AddTransition( "*", "Fail", "Failed" )

    self:AddEndState( "Failed" )

    self:SetStartState( "Assigned" )

    return self
  end

  --- Process Events

  --- StateMachine callback function
  -- @param #ACT_ACCOUNT self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ACCOUNT:onafterStart( ProcessUnit, From, Event, To )

    self:HandleEvent( EVENTS.Dead, self.onfuncEventDead )
    self:HandleEvent( EVENTS.Crash, self.onfuncEventCrash )
    self:HandleEvent( EVENTS.Hit )

    self:__Wait( 1 )
  end

    --- StateMachine callback function
    -- @param #ACT_ACCOUNT self
    -- @param Wrapper.Unit#UNIT ProcessUnit
    -- @param #string Event
    -- @param #string From
    -- @param #string To
  function ACT_ACCOUNT:onenterWaiting( ProcessUnit, From, Event, To )

    if self.DisplayCount >= self.DisplayInterval then
      self:Report()
      self.DisplayCount = 1
    else
      self.DisplayCount = self.DisplayCount + 1
    end

    return true -- Process always the event.
  end

  --- StateMachine callback function
  -- @param #ACT_ACCOUNT self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ACCOUNT:onafterEvent( ProcessUnit, From, Event, To )

    self:__NoMore( 1 )
  end

end -- ACT_ACCOUNT

do -- ACT_ACCOUNT_DEADS

  --- # @{#ACT_ACCOUNT_DEADS} FSM class, extends @{#ACT_ACCOUNT}
  --
  -- The ACT_ACCOUNT_DEADS class accounts (detects, counts and reports) successful kills of DCS units.
  -- The process is given a @{Core.Set} of units that will be tracked upon successful destruction.
  -- The process will end after each target has been successfully destroyed.
  -- Each successful dead will trigger an Account state transition that can be scored, modified or administered.
  --
  --
  -- ## ACT_ACCOUNT_DEADS constructor:
  --
  --   * @{#ACT_ACCOUNT_DEADS.New}(): Creates a new ACT_ACCOUNT_DEADS object.
  --
  -- @type ACT_ACCOUNT_DEADS
  -- @field Core.Set#SET_UNIT TargetSetUnit
  -- @extends #ACT_ACCOUNT
  ACT_ACCOUNT_DEADS = {
    ClassName = "ACT_ACCOUNT_DEADS",
  }

  --- Creates a new DESTROY process.
  -- @param #ACT_ACCOUNT_DEADS self
  -- @param Core.Set#SET_UNIT TargetSetUnit
  -- @param #string TaskName
  function ACT_ACCOUNT_DEADS:New()
    -- Inherits from BASE
    local self = BASE:Inherit( self, ACT_ACCOUNT:New() ) -- #ACT_ACCOUNT_DEADS

    self.DisplayInterval = 30
    self.DisplayCount = 30
    self.DisplayMessage = true
    self.DisplayTime = 10 -- 10 seconds is the default
    self.DisplayCategory = "HQ" -- Targets is the default display category

    return self
  end

  function ACT_ACCOUNT_DEADS:Init( FsmAccount )

    self.Task = self:GetTask()
    self.TaskName = self.Task:GetName()
  end

  --- Process Events

  --- StateMachine callback function
  -- @param #ACT_ACCOUNT_DEADS self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ACCOUNT_DEADS:onenterReport( ProcessUnit, Task, From, Event, To )

    local MessageText = "Your group with assigned " .. self.TaskName .. " task has " .. Task.TargetSetUnit:GetUnitTypesText() .. " targets left to be destroyed."
    self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information )
  end

  --- StateMachine callback function
  -- @param #ACT_ACCOUNT_DEADS self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param Tasking.Task#TASK Task
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Core.Event#EVENTDATA EventData
  function ACT_ACCOUNT_DEADS:onafterEvent( ProcessUnit, Task, From, Event, To, EventData  )
    self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } )

    if Task.TargetSetUnit:FindUnit( EventData.IniUnitName ) then
      local PlayerName = ProcessUnit:GetPlayerName()
      local PlayerHit = self.PlayerHits and self.PlayerHits[EventData.IniUnitName]
      if PlayerHit == PlayerName then
        self:Player( EventData )
      else
        self:Other( EventData )
      end
    end
  end

  --- StateMachine callback function
  -- @param #ACT_ACCOUNT_DEADS self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param Tasking.Task#TASK Task
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Core.Event#EVENTDATA EventData
  function ACT_ACCOUNT_DEADS:onenterAccountForPlayer( ProcessUnit, Task, From, Event, To, EventData  )
    self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } )

    local TaskGroup = ProcessUnit:GetGroup()

    Task.TargetSetUnit:Remove( EventData.IniUnitName )

    local MessageText = "You have destroyed a target.\nYour group assigned with task " .. self.TaskName .. " has\n" .. Task.TargetSetUnit:Count() .. " targets ( " .. Task.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed."
    self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information )

    local PlayerName = ProcessUnit:GetPlayerName()
    Task:AddProgress( PlayerName, "Destroyed " .. EventData.IniTypeName, timer.getTime(), 1 )

    if Task.TargetSetUnit:Count() > 0 then
      self:__More( 1 )
    else
      self:__NoMore( 1 )
    end
  end

  --- StateMachine callback function
  -- @param #ACT_ACCOUNT_DEADS self
  -- @param Wrapper.Unit#UNIT ProcessUnit
  -- @param Tasking.Task#TASK Task
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Core.Event#EVENTDATA EventData
  function ACT_ACCOUNT_DEADS:onenterAccountForOther( ProcessUnit, Task, From, Event, To, EventData  )
    self:T( { ProcessUnit:GetName(), Task:GetName(), From, Event, To, EventData } )

    local TaskGroup = ProcessUnit:GetGroup()
    Task.TargetSetUnit:Remove( EventData.IniUnitName )

    local MessageText = "One of the task targets has been destroyed.\nYour group assigned with task " .. self.TaskName .. " has\n" .. Task.TargetSetUnit:Count() .. " targets ( " .. Task.TargetSetUnit:GetUnitTypesText() .. " ) left to be destroyed."
    self:GetCommandCenter():MessageTypeToGroup( MessageText, ProcessUnit:GetGroup(), MESSAGE.Type.Information )

    if Task.TargetSetUnit:Count() > 0 then
      self:__More( 1 )
    else
      self:__NoMore( 1 )
    end
  end

  --- DCS Events

  -- @param #ACT_ACCOUNT_DEADS self
  -- @param Core.Event#EVENTDATA EventData
  function ACT_ACCOUNT_DEADS:OnEventHit( EventData )
    self:T( { "EventDead", EventData } )

    if EventData.IniPlayerName and EventData.TgtDCSUnitName then
      self.PlayerHits = self.PlayerHits or {}
      self.PlayerHits[EventData.TgtDCSUnitName] = EventData.IniPlayerName
    end
  end

  -- @param #ACT_ACCOUNT_DEADS self
  -- @param Core.Event#EVENTDATA EventData
  function ACT_ACCOUNT_DEADS:onfuncEventDead( EventData )
    self:T( { "EventDead", EventData } )

    if EventData.IniDCSUnit then
      self:Event( EventData )
    end
  end

  --- DCS Events

  -- @param #ACT_ACCOUNT_DEADS self
  -- @param Core.Event#EVENTDATA EventData
  function ACT_ACCOUNT_DEADS:onfuncEventCrash( EventData )
    self:T( { "EventDead", EventData } )

    if EventData.IniDCSUnit then
      self:Event( EventData )
    end
  end

end -- ACT_ACCOUNT DEADS
--- (SP) (MP) (FSM) Route AI or players through waypoints or to zones.
--
-- ![Banner Image](..\Images\deprecated.png)
-- ## ACT_ASSIST state machine:
--
-- This class is a state machine: it manages a process that is triggered by events causing state transitions to occur.
-- All derived classes from this class will start with the class name, followed by a \_. See the relevant derived class descriptions below.
-- Each derived class follows exactly the same process, using the same events and following the same state transitions,
-- but will have **different implementation behaviour** upon each event or state transition.
--
-- ### ACT_ASSIST **Events**:
--
-- These are the events defined in this class:
--
--   * **Start**:  The process is started.
--   * **Next**: The process is smoking the targets in the given zone.
--
-- ### ACT_ASSIST **Event methods**:
--
-- Event methods are available (dynamically allocated by the state machine), that accomodate for state transitions occurring in the process.
-- There are two types of event methods, which you can use to influence the normal mechanisms in the state machine:
--
--   * **Immediate**: The event method has exactly the name of the event.
--   * **Delayed**: The event method starts with a __ + the name of the event. The first parameter of the event method is a number value, expressing the delay in seconds when the event will be executed.
--
-- ### ACT_ASSIST **States**:
--
--   * **None**: The controllable did not receive route commands.
--   * **AwaitSmoke (*)**: The process is awaiting to smoke the targets in the zone.
--   * **Smoking (*)**: The process is smoking the targets in the zone.
--   * **Failed (*)**: The process has failed.
--
-- (*) End states of the process.
--
-- ### ACT_ASSIST state transition methods:
--
-- State transition functions can be set **by the mission designer** customizing or improving the behaviour of the state.
-- There are 2 moments when state transition methods will be called by the state machine:
--
--   * **Before** the state transition.
--     The state transition method needs to start with the name **OnBefore + the name of the state**.
--     If the state transition method returns false, then the processing of the state transition will not be done!
--     If you want to change the behaviour of the AIControllable at this event, return false,
--     but then you'll need to specify your own logic using the AIControllable!
--
--   * **After** the state transition.
--     The state transition method needs to start with the name **OnAfter + the name of the state**.
--     These state transition methods need to provide a return value, which is specified at the function description.
--
-- ===
--
-- # 1) @{#ACT_ASSIST_SMOKE_TARGETS_ZONE} class, extends @{#ACT_ASSIST}
--
-- The ACT_ASSIST_SMOKE_TARGETS_ZONE class implements the core functions to smoke targets in a @{Core.Zone}.
-- The targets are smoked within a certain range around each target, simulating a realistic smoking behaviour.
-- At random intervals, a new target is smoked.
--
-- # 1.1) ACT_ASSIST_SMOKE_TARGETS_ZONE constructor:
--
--   * @{#ACT_ASSIST_SMOKE_TARGETS_ZONE.New}(): Creates a new ACT_ASSIST_SMOKE_TARGETS_ZONE object.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
--
-- @module Actions.Act_Assist
-- @image MOOSE.JPG


do -- ACT_ASSIST

  --- ACT_ASSIST class
  -- @type ACT_ASSIST
  -- @extends Core.Fsm#FSM_PROCESS
  ACT_ASSIST = {
    ClassName = "ACT_ASSIST",
  }

  --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator.
  -- @param #ACT_ASSIST self
  -- @return #ACT_ASSIST
  function ACT_ASSIST:New()

    -- Inherits from BASE
    local self = BASE:Inherit( self, FSM_PROCESS:New( "ACT_ASSIST" ) ) -- Core.Fsm#FSM_PROCESS

    self:AddTransition( "None", "Start", "AwaitSmoke" )
    self:AddTransition( "AwaitSmoke", "Next", "Smoking" )
    self:AddTransition( "Smoking", "Next", "AwaitSmoke" )
    self:AddTransition( "*", "Stop", "Success" )
    self:AddTransition( "*", "Fail", "Failed" )

    self:AddEndState( "Failed" )
    self:AddEndState( "Success" )

    self:SetStartState( "None" )

    return self
  end

  --- Task Events

  --- StateMachine callback function
  -- @param #ACT_ASSIST self
  -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ASSIST:onafterStart( ProcessUnit, From, Event, To )

    local ProcessGroup = ProcessUnit:GetGroup()
    local MissionMenu = self:GetMission():GetMenu( ProcessGroup )

    local function MenuSmoke( MenuParam )
      local self = MenuParam.self
      local SmokeColor = MenuParam.SmokeColor
      self.SmokeColor = SmokeColor
      self:__Next( 1 )
    end

    self.Menu = MENU_GROUP:New( ProcessGroup, "Target acquisition", MissionMenu )
    self.MenuSmokeBlue   = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop blue smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Blue } )
    self.MenuSmokeGreen  = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop green smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Green } )
    self.MenuSmokeOrange = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Orange smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Orange } )
    self.MenuSmokeRed    = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop Red smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.Red } )
    self.MenuSmokeWhite  = MENU_GROUP_COMMAND:New( ProcessGroup, "Drop White smoke on targets", self.Menu, MenuSmoke, { self = self, SmokeColor = SMOKECOLOR.White } )
  end

  --- StateMachine callback function
  -- @param #ACT_ASSIST self
  -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ASSIST:onafterStop( ProcessUnit, From, Event, To )

    self.Menu:Remove() -- When stopped, remove the menus
  end

end

do -- ACT_ASSIST_SMOKE_TARGETS_ZONE

  --- ACT_ASSIST_SMOKE_TARGETS_ZONE class
  -- @type ACT_ASSIST_SMOKE_TARGETS_ZONE
  -- @field Core.Set#SET_UNIT TargetSetUnit
  -- @field Core.Zone#ZONE_BASE TargetZone
  -- @extends #ACT_ASSIST
  ACT_ASSIST_SMOKE_TARGETS_ZONE = {
    ClassName = "ACT_ASSIST_SMOKE_TARGETS_ZONE",
  }

--  function ACT_ASSIST_SMOKE_TARGETS_ZONE:_Destructor()
--    self:E("_Destructor")
--
--    self.Menu:Remove()
--    self:EventRemoveAll()
--  end

  --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator.
  -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self
  -- @param Core.Set#SET_UNIT TargetSetUnit
  -- @param Core.Zone#ZONE_BASE TargetZone
  function ACT_ASSIST_SMOKE_TARGETS_ZONE:New( TargetSetUnit, TargetZone )
    local self = BASE:Inherit( self, ACT_ASSIST:New() ) -- #ACT_ASSIST

    self.TargetSetUnit = TargetSetUnit
    self.TargetZone = TargetZone

    return self
  end

  function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( FsmSmoke )

    self.TargetSetUnit = FsmSmoke.TargetSetUnit
    self.TargetZone = FsmSmoke.TargetZone
  end

  --- Creates a new target smoking state machine. The process will request from the menu if it accepts the task, if not, the unit is removed from the simulator.
  -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self
  -- @param Core.Set#SET_UNIT TargetSetUnit
  -- @param Core.Zone#ZONE_BASE TargetZone
  -- @return #ACT_ASSIST_SMOKE_TARGETS_ZONE self
  function ACT_ASSIST_SMOKE_TARGETS_ZONE:Init( TargetSetUnit, TargetZone )

    self.TargetSetUnit = TargetSetUnit
    self.TargetZone = TargetZone

    return self
  end

  --- StateMachine callback function
  -- @param #ACT_ASSIST_SMOKE_TARGETS_ZONE self
  -- @param Wrapper.Controllable#CONTROLLABLE ProcessUnit
  -- @param #string Event
  -- @param #string From
  -- @param #string To
  function ACT_ASSIST_SMOKE_TARGETS_ZONE:onenterSmoking( ProcessUnit, From, Event, To )

    self.TargetSetUnit:ForEachUnit(
      -- @param Wrapper.Unit#UNIT SmokeUnit
      function( SmokeUnit )
        if math.random( 1, ( 100 * self.TargetSetUnit:Count() ) / 4 ) <= 100 then
          SCHEDULER:New( self,
            function()
              if SmokeUnit:IsAlive() then
                SmokeUnit:Smoke( self.SmokeColor, 150 )
              end
            end, {}, math.random( 10, 60 )
          )
        end
      end
    )

  end

end
--- **Shapes** - Class that serves as the base shapes drawn in the Mission Editor
--
--
-- ### Author: **nielsvaes/coconutcockpit**
--
-- ===
-- @module Shapes.SHAPE_BASE
-- @image CORE_Pathline.png


--- SHAPE_BASE class.
-- @type SHAPE_BASE
-- @field #string ClassName Name of the class.
-- @field #string Name Name of the shape
-- @field #table CenterVec2 Vec2 of the center of the shape, this will be assigned automatically
-- @field #table Points List of 3D points defining the shape, this will be assigned automatically
-- @field #table Coords List of COORDINATE defining the path, this will be assigned automatically
-- @field #table MarkIDs List any MARKIDs this class use, this will be assigned automatically
-- @extends Core.Base#BASE

--- *I'm in love with the shape of you -- Ed Sheeran
--
-- ===
--
-- # SHAPE_BASE
-- The class serves as the base class to deal with these shapes using MOOSE. You should never use this class on its own,
-- rather use:
--      CIRCLE
--      LINE
--      OVAL
--      POLYGON
--      TRIANGLE (although this one's a bit special as well)
--
-- ===
-- The idea is that anything you draw on the map in the Mission Editor can be turned in a shape to work with in MOOSE.
-- This is the base class that all other shape classes are built on. There are some shared functions, most of which are overridden in the derived classes
--
-- @field #SHAPE_BASE
SHAPE_BASE = {
    ClassName = "SHAPE_BASE",
    Name = "",
    CenterVec2 = nil,
    Points = {},
    Coords = {},
    MarkIDs = {},
    ColorString = "",
    ColorRGBA = {}
}

--- Creates a new instance of SHAPE_BASE.
-- @return #SHAPE_BASE The new instance
function SHAPE_BASE:New()
    local self = BASE:Inherit(self, BASE:New())
    return self
end

--- Finds a shape on the map by its name.
-- @param #string shape_name Name of the shape to find
-- @return #SHAPE_BASE The found shape
function SHAPE_BASE:FindOnMap(shape_name)
    local self = BASE:Inherit(self, BASE:New())

    local found = false

    for _, layer in pairs(env.mission.drawings.layers) do
        for _, object in pairs(layer["objects"]) do
            if object["name"] == shape_name then
                self.Name = object["name"]
                self.CenterVec2 = { x = object["mapX"], y = object["mapY"] }
                self.ColorString = object["colorString"]
                self.ColorRGBA = UTILS.HexToRGBA(self.ColorString)
                found = true
            end
        end
    end
    if not found then
        self:E("Can't find a shape with name " .. shape_name)
    end
    return self
end

function SHAPE_BASE:GetAllShapes(filter)
    filter = filter or ""
    local return_shapes = {}
    for _, layer in pairs(env.mission.drawings.layers) do
        for _, object in pairs(layer["objects"]) do
            if string.contains(object["name"], filter) then
                table.add(return_shapes, object)
            end
        end
    end

    return return_shapes
end

--- Offsets the shape to a new position.
-- @param #table new_vec2 The new position
function SHAPE_BASE:Offset(new_vec2)
    local offset_vec2 = UTILS.Vec2Subtract(new_vec2, self.CenterVec2)
    self.CenterVec2 = new_vec2
    if self.ClassName == "POLYGON" then
        for _, point in pairs(self.Points) do
            point.x = point.x + offset_vec2.x
            point.y = point.y + offset_vec2.y
        end
    end
end

--- Gets the name of the shape.
-- @return #string The name of the shape
function SHAPE_BASE:GetName()
    return self.Name
end

function SHAPE_BASE:GetColorString()
    return self.ColorString
end

function SHAPE_BASE:GetColorRGBA()
    return self.ColorRGBA
end

function SHAPE_BASE:GetColorRed()
    return self.ColorRGBA.R
end

function SHAPE_BASE:GetColorGreen()
    return self.ColorRGBA.G
end

function SHAPE_BASE:GetColorBlue()
    return self.ColorRGBA.B
end

function SHAPE_BASE:GetColorAlpha()
    return self.ColorRGBA.A
end

--- Gets the center position of the shape.
-- @return #table The center position
function SHAPE_BASE:GetCenterVec2()
    return self.CenterVec2
end

--- Gets the center coordinate of the shape.
-- @return #COORDINATE The center coordinate
function SHAPE_BASE:GetCenterCoordinate()
    return COORDINATE:NewFromVec2(self.CenterVec2)
end

--- Gets the coordinate of the shape.
-- @return #COORDINATE The coordinate
function SHAPE_BASE:GetCoordinate()
    return self:GetCenterCoordinate()
end

--- Checks if a point is contained within the shape.
-- @param #table _ The point to check
-- @return #bool True if the point is contained, false otherwise
function SHAPE_BASE:ContainsPoint(_)
    self:E("This needs to be set in the derived class")
end

--- Checks if a unit is contained within the shape.
-- @param #string unit_name The name of the unit to check
-- @return #bool True if the unit is contained, false otherwise
function SHAPE_BASE:ContainsUnit(unit_name)
    local unit = UNIT:FindByName(unit_name)

    if unit == nil or not unit:IsAlive() then
        return false
    end

    if self:ContainsPoint(unit:GetVec2()) then
        return true
    end
    return false
end

--- Checks if any unit of a group is contained within the shape.
-- @param #string group_name The name of the group to check
-- @return #bool True if any unit of the group is contained, false otherwise
function SHAPE_BASE:ContainsAnyOfGroup(group_name)
    local group = GROUP:FindByName(group_name)

    if group == nil or not group:IsAlive() then
        return false
    end

    for _, unit in pairs(group:GetUnits()) do
        if self:ContainsPoint(unit:GetVec2()) then
            return true
        end
    end
    return false
end

--- Checks if all units of a group are contained within the shape.
-- @param #string group_name The name of the group to check
-- @return #bool True if all units of the group are contained, false otherwise
function SHAPE_BASE:ContainsAllOfGroup(group_name)
    local group = GROUP:FindByName(group_name)

    if group == nil or not group:IsAlive() then
        return false
    end

    for _, unit in pairs(group:GetUnits()) do
        if not self:ContainsPoint(unit:GetVec2()) then
            return false
        end
    end
    return true
end
--
--
-- ### Author: **nielsvaes/coconutcockpit**
--
-- ===
-- @module Shapes.CIRCLE
-- @image MOOSE.JPG

--- CIRCLE class.
-- @type CIRCLE
-- @field #string ClassName Name of the class.
-- @field #number Radius Radius of the circle

--- *It's NOT hip to be square* -- Someone, somewhere, probably
--
-- ===
--
-- # CIRCLE
-- CIRCLEs can be fetched from the drawings in the Mission Editor

---
-- This class has some of the standard CIRCLE functions you'd expect. One function of interest is CIRCLE:PointInSector() that you can use if a point is
-- within a certain sector (pizza slice) of a circle. This can be useful for many things, including rudimentary, "radar-like" searches from a unit.
-- 
-- CIRCLE class with properties and methods for handling circles.
-- @field #CIRCLE
CIRCLE = {
    ClassName = "CIRCLE",
    Radius = nil,
}
--- Finds a circle on the map by its name. The circle must have been added in the Mission Editor
-- @param #string shape_name Name of the circle to find
-- @return #CIRCLE The found circle, or nil if not found
function CIRCLE:FindOnMap(shape_name)
    local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name))
    for _, layer in pairs(env.mission.drawings.layers) do
        for _, object in pairs(layer["objects"]) do
            if string.find(object["name"], shape_name, 1, true) then
                if object["polygonMode"] == "circle" then
                    self.Radius = object["radius"]
                end
            end
        end
    end

    return self
end

--- Finds a circle by its name in the database.
-- @param #string shape_name Name of the circle to find
-- @return #CIRCLE The found circle, or nil if not found
function CIRCLE:Find(shape_name)
    return _DATABASE:FindShape(shape_name)
end

--- Creates a new circle from a center point and a radius.
-- @param #table vec2 The center point of the circle
-- @param #number radius The radius of the circle
-- @return #CIRCLE The new circle
function CIRCLE:New(vec2, radius)
    local self = BASE:Inherit(self, SHAPE_BASE:New())
    self.CenterVec2 = vec2
    self.Radius = radius
    return self
end

--- Gets the radius of the circle.
-- @return #number The radius of the circle
function CIRCLE:GetRadius()
    return self.Radius
end

--- Checks if a point is contained within the circle.
-- @param #table point The point to check
-- @return #boolean True if the point is contained, false otherwise
function CIRCLE:ContainsPoint(point)
    if ((point.x - self.CenterVec2.x) ^ 2 + (point.y - self.CenterVec2.y) ^ 2) ^ 0.5 <= self.Radius then
        return true
    end
    return false
end

--- Checks if a point is contained within a sector of the circle. The start and end sector need to be clockwise
-- @param #table point The point to check
-- @param #table sector_start The start point of the sector
-- @param #table sector_end The end point of the sector
-- @param #table center The center point of the sector
-- @param #number radius The radius of the sector
-- @return #bool True if the point is contained, false otherwise
function CIRCLE:PointInSector(point, sector_start, sector_end, center, radius)
    center = center or self.CenterVec2
    radius = radius or self.Radius

    local function are_clockwise(v1, v2)
        return -v1.x * v2.y + v1.y * v2.x > 0
    end

    local function is_in_radius(rp)
        return rp.x * rp.x + rp.y * rp.y <= radius ^ 2
    end

    local rel_pt = {
        x = point.x - center.x,
        y = point.y - center.y
    }

    local rel_sector_start = {
        x = sector_start.x - center.x,
        y = sector_start.y - center.y,
    }

    local rel_sector_end = {
        x = sector_end.x - center.x,
        y = sector_end.y - center.y,
    }

    return not are_clockwise(rel_sector_start, rel_pt) and
           are_clockwise(rel_sector_end, rel_pt) and
           is_in_radius(rel_pt, radius)
end

--- Checks if a unit is contained within a sector of the circle. The start and end sector need to be clockwise
-- @param #string unit_name The name of the unit to check
-- @param #table sector_start The start point of the sector
-- @param #table sector_end The end point of the sector
-- @param #table center The center point of the sector
-- @param #number radius The radius of the sector
-- @return #bool True if the unit is contained, false otherwise
function CIRCLE:UnitInSector(unit_name, sector_start, sector_end, center, radius)
    center = center or self.CenterVec2
    radius = radius or self.Radius

    if self:PointInSector(UNIT:FindByName(unit_name):GetVec2(), sector_start, sector_end, center, radius) then
        return true
    end
    return false
end

--- Checks if any unit of a group is contained within a sector of the circle. The start and end sector need to be clockwise
-- @param #string group_name The name of the group to check
-- @param #table sector_start The start point of the sector
-- @param #table sector_end The end point of the sector
-- @param #table center The center point of the sector
-- @param #number radius The radius of the sector
-- @return #bool True if any unit of the group is contained, false otherwise
function CIRCLE:AnyOfGroupInSector(group_name, sector_start, sector_end, center, radius)
    center = center or self.CenterVec2
    radius = radius or self.Radius

    for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do
        if self:PointInSector(unit:GetVec2(), sector_start, sector_end, center, radius) then
            return true
        end
    end
    return false
end

--- Checks if all units of a group are contained within a sector of the circle. The start and end sector need to be clockwise
-- @param #string group_name The name of the group to check
-- @param #table sector_start The start point of the sector
-- @param #table sector_end The end point of the sector
-- @param #table center The center point of the sector
-- @param #number radius The radius of the sector
-- @return #bool True if all units of the group are contained, false otherwise
function CIRCLE:AllOfGroupInSector(group_name, sector_start, sector_end, center, radius)
    center = center or self.CenterVec2
    radius = radius or self.Radius

    for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do
        if not self:PointInSector(unit:GetVec2(), sector_start, sector_end, center, radius) then
            return false
        end
    end
    return true
end

--- Checks if a unit is contained within a radius of the circle.
-- @param #string unit_name The name of the unit to check
-- @param #table center The center point of the radius
-- @param #number radius The radius to check
-- @return #bool True if the unit is contained, false otherwise
function CIRCLE:UnitInRadius(unit_name, center, radius)
    center = center or self.CenterVec2
    radius = radius or self.Radius

    if UTILS.IsInRadius(center, UNIT:FindByName(unit_name):GetVec2(), radius) then
        return true
    end
    return false
end

--- Checks if any unit of a group is contained within a radius of the circle.
-- @param #string group_name The name of the group to check
-- @param #table center The center point of the radius
-- @param #number radius The radius to check
-- @return #bool True if any unit of the group is contained, false otherwise
function CIRCLE:AnyOfGroupInRadius(group_name, center, radius)
    center = center or self.CenterVec2
    radius = radius or self.Radius

    for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do
        if UTILS.IsInRadius(center, unit:GetVec2(), radius) then
            return true
        end
    end
    return false
end

--- Checks if all units of a group are contained within a radius of the circle.
-- @param #string group_name The name of the group to check
-- @param #table center The center point of the radius
-- @param #number radius The radius to check
-- @return #bool True if all units of the group are contained, false otherwise
function CIRCLE:AllOfGroupInRadius(group_name, center, radius)
    center = center or self.CenterVec2
    radius = radius or self.Radius

    for _, unit in pairs(GROUP:FindByName(group_name):GetUnits()) do
        if not UTILS.IsInRadius(center, unit:GetVec2(), radius) then
            return false
        end
    end
    return true
end

--- Returns a random Vec2 within the circle.
-- @return #table The random Vec2
function CIRCLE:GetRandomVec2()

    math.random()
    math.random()
    math.random()
    
    local angle = math.random() * 2 * math.pi

    local rx = math.random(0, self.Radius) * math.cos(angle) + self.CenterVec2.x
    local ry = math.random(0, self.Radius) * math.sin(angle) + self.CenterVec2.y

    return {x=rx, y=ry}
end

--- Returns a random Vec2 on the border of the circle.
-- @return #table The random Vec2
function CIRCLE:GetRandomVec2OnBorder()
    
    math.random()
    math.random()
    math.random()

    local angle = math.random() * 2 * math.pi

    local rx = self.Radius * math.cos(angle) + self.CenterVec2.x
    local ry = self.Radius * math.sin(angle) + self.CenterVec2.y

    return {x=rx, y=ry}
end

--- Calculates the bounding box of the circle. The bounding box is the smallest rectangle that contains the circle.
-- @return #table The bounding box of the circle
function CIRCLE:GetBoundingBox()
    local min_x = self.CenterVec2.x - self.Radius
    local min_y = self.CenterVec2.y - self.Radius
    local max_x = self.CenterVec2.x + self.Radius
    local max_y = self.CenterVec2.y + self.Radius

    return {
        {x=min_x, y=min_x}, {x=max_x, y=min_y}, {x=max_x, y=max_y}, {x=min_x, y=max_y}
    }
end

---
--
-- ### Author: **nielsvaes/coconutcockpit**
--
-- ===
-- @module Shapes.CUBE
-- @image MOOSE.JPG

--- LINE class.
-- @type CUBE
-- @field #string ClassName Name of the class.
-- @field #number Points points of the line
-- @field #number Coords coordinates of the line

--
-- ===

---
-- @field #CUBE
CUBE = {
    ClassName = "CUBE",
    Points = {},
    Coords = {}
}

--- Points need to be added in the following order:
--- p1 -> p4 make up the front face of the cube
--- p5 -> p8 make up the back face of the cube
---        p1 connects to p5
---        p2 connects to p6
---        p3 connects to p7
---        p4 connects to p8
---
---         8-----------7
---        /|          /|
---       / |         / |
---      4--+--------3  |
---      |  |        |  |
---      |  |        |  |
---      |  |        |  |
---      |  5--------+--6
---      | /         | /
---      |/          |/
---      1-----------2
---
function CUBE:New(p1, p2, p3, p4, p5, p6, p7, p8)
    local self = BASE:Inherit(self, SHAPE_BASE)
    self.Points = {p1, p2, p3, p4, p5, p6, p7, p8}
    for _, point in spairs(self.Points) do
        table.insert(self.Coords, COORDINATE:NewFromVec3(point))
    end
    return self
end

function CUBE:GetCenter()
    local center = { x=0, y=0, z=0 }
    for _, point in pairs(self.Points) do
      center.x = center.x + point.x
      center.y = center.y + point.y
      center.z = center.z + point.z
    end

    center.x = center.x / 8
    center.y = center.y / 8
    center.z = center.z / 8
    return center
end

function CUBE:ContainsPoint(point, cube_points)
    cube_points = cube_points or self.Points
    local min_x, min_y, min_z = math.huge, math.huge, math.huge
    local max_x, max_y, max_z = -math.huge, -math.huge, -math.huge

    -- Find the minimum and maximum x, y, and z values of the cube points
    for _, p in ipairs(cube_points) do
        if p.x < min_x then min_x = p.x end
        if p.y < min_y then min_y = p.y end
        if p.z < min_z then min_z = p.z end
        if p.x > max_x then max_x = p.x end
        if p.y > max_y then max_y = p.y end
        if p.z > max_z then max_z = p.z end
    end

    return point.x >= min_x and point.x <= max_x and point.y >= min_y and point.y <= max_y and point.z >= min_z and point.z <= max_z
end
---
--
-- ### Author: **nielsvaes/coconutcockpit**
--
-- ===
-- @module Shapes.LINE
-- @image MOOSE.JPG

--- LINE class.
-- @type LINE
-- @field #string ClassName Name of the class.
-- @field #number Points points of the line
-- @field #number Coords coordinates of the line

--
-- ===

---
-- @field #LINE
LINE = {
    ClassName = "LINE",
    Points = {},
    Coords = {},
}

--- Finds a line on the map by its name. The line must be drawn in the Mission Editor
-- @param #string line_name Name of the line to find
-- @return #LINE The found line, or nil if not found
function LINE:FindOnMap(line_name)
    local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(line_name))

    for _, layer in pairs(env.mission.drawings.layers) do
        for _, object in pairs(layer["objects"]) do
            if object["name"] == line_name then
                if object["primitiveType"] == "Line" then
                    for _, point in UTILS.spairs(object["points"]) do
                        local p = {x = object["mapX"] + point["x"],
                                   y = object["mapY"] + point["y"] }
                        local coord = COORDINATE:NewFromVec2(p)
                        table.insert(self.Points, p)
                        table.insert(self.Coords, coord)
                    end
                end
            end
        end
    end

    self:I(#self.Points)
    if #self.Points == 0 then
        return nil
    end

    self.MarkIDs = {}

    return self
end

--- Finds a line by its name in the database.
-- @param #string shape_name Name of the line to find
-- @return #LINE The found line, or nil if not found
function LINE:Find(shape_name)
    return _DATABASE:FindShape(shape_name)
end

--- Creates a new line from two points.
-- @param #table vec2 The first point of the line
-- @param #number radius The second point of the line
-- @return #LINE The new line
function LINE:New(...)
    local self = BASE:Inherit(self, SHAPE_BASE:New())
    self.Points = {...}
    self:I(self.Points)
    for _, point in UTILS.spairs(self.Points) do
        table.insert(self.Coords, COORDINATE:NewFromVec2(point))
    end
    return self
end

--- Creates a new line from a circle.
-- @param #table center_point center point of the circle
-- @param #number radius radius of the circle, half length of the line
-- @param #number angle_degrees degrees the line will form from center point
-- @return #LINE The new line
function LINE:NewFromCircle(center_point, radius, angle_degrees)
    local self = BASE:Inherit(self, SHAPE_BASE:New())
    self.CenterVec2 = center_point
    local angleRadians = math.rad(angle_degrees)

    local point1 = {
        x = center_point.x + radius * math.cos(angleRadians),
        y = center_point.y + radius * math.sin(angleRadians)
    }

    local point2 = {
        x = center_point.x + radius * math.cos(angleRadians + math.pi),
        y = center_point.y + radius * math.sin(angleRadians + math.pi)
    }

    for _, point in pairs{point1, point2} do
        table.insert(self.Points, point)
        table.insert(self.Coords, COORDINATE:NewFromVec2(point))
    end

    return self
end

--- Gets the coordinates of the line.
-- @return #table The coordinates of the line
function LINE:Coordinates()
    return self.Coords
end

--- Gets the start coordinate of the line. The start coordinate is the first point of the line.
-- @return #COORDINATE The start coordinate of the line
function LINE:GetStartCoordinate()
    return self.Coords[1]
end

--- Gets the end coordinate of the line. The end coordinate is the last point of the line.
-- @return #COORDINATE The end coordinate of the line
function LINE:GetEndCoordinate()
    return self.Coords[#self.Coords]
end

--- Gets the start point of the line. The start point is the first point of the line.
-- @return #table The start point of the line
function LINE:GetStartPoint()
    return self.Points[1]
end

--- Gets the end point of the line. The end point is the last point of the line.
-- @return #table The end point of the line
function LINE:GetEndPoint()
    return self.Points[#self.Points]
end

--- Gets the length of the line.
-- @return #number The length of the line
function LINE:GetLength()
    local total_length = 0
        for i=1, #self.Points - 1 do
        local x1, y1 = self.Points[i]["x"], self.Points[i]["y"]
        local x2, y2 = self.Points[i+1]["x"], self.Points[i+1]["y"]
        local segment_length = math.sqrt((x2 - x1)^2 + (y2 - y1)^2)
        total_length = total_length + segment_length
    end
    return total_length
end

--- Returns a random point on the line.
-- @param #table points (optional) The points of the line or 2 other points if you're just using the LINE class without an object of it
-- @return #table The random point
function LINE:GetRandomPoint(points)
    points = points or self.Points
    local rand = math.random() -- 0->1

    local random_x = points[1].x + rand * (points[2].x - points[1].x)
    local random_y = points[1].y + rand * (points[2].y - points[1].y)

    return { x= random_x, y= random_y }
end

--- Gets the heading of the line.
-- @param #table points (optional) The points of the line or 2 other points if you're just using the LINE class without an object of it
-- @return #number The heading of the line
function LINE:GetHeading(points)
    points = points or self.Points

    local angle = math.atan2(points[2].y - points[1].y, points[2].x - points[1].x)

    angle = math.deg(angle)
    if angle < 0 then
        angle = angle + 360
    end

    return angle
end


--- Return each part of the line as a new line
-- @return #table The points
function LINE:GetIndividualParts()
    local parts = {}
    if #self.Points == 2 then
        parts = {self}
    end

    for i=1, #self.Points -1 do
        local p1 = self.Points[i]
        local p2 = self.Points[i % #self.Points + 1]
        table.add(parts, LINE:New(p1, p2))
    end

    return parts
end

--- Gets a number of points in between the start and end points of the line.
-- @param #number amount The number of points to get
-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point
-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point
-- @return #table The points
function LINE:GetPointsInbetween(amount, start_point, end_point)
    start_point = start_point or self:GetStartPoint()
    end_point = end_point or self:GetEndPoint()
    if amount == 0 then return {start_point, end_point} end

    amount = amount + 1
    local points = {}

    local difference = { x = end_point.x - start_point.x, y = end_point.y - start_point.y }
    local divided = { x = difference.x / amount, y = difference.y / amount }

    for j=0, amount do
        local part_pos = {x = divided.x * j, y = divided.y * j}
        -- add part_pos vector to the start point so the new point is placed along in the line
        local point = {x = start_point.x + part_pos.x, y = start_point.y + part_pos.y}
        table.insert(points, point)
    end
    return points
end

--- Gets a number of points in between the start and end points of the line.
-- @param #number amount The number of points to get
-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point
-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point
-- @return #table The points
function LINE:GetCoordinatesInBetween(amount, start_point, end_point)
    local coords = {}
    for _, pt in pairs(self:GetPointsInbetween(amount, start_point, end_point)) do
        table.add(coords, COORDINATE:NewFromVec2(pt))
    end
    return coords
end


function LINE:GetRandomPoint(start_point, end_point)
    start_point = start_point or self:GetStartPoint()
    end_point = end_point or self:GetEndPoint()

    local fraction = math.random()

    local difference = { x = end_point.x - start_point.x, y = end_point.y - start_point.y }
    local part_pos = {x = difference.x * fraction, y = difference.y * fraction}
    local random_point = { x = start_point.x + part_pos.x, y = start_point.y + part_pos.y}

    return random_point
end


function LINE:GetRandomCoordinate(start_point, end_point)
    start_point = start_point or self:GetStartPoint()
    end_point = end_point or self:GetEndPoint()

    return COORDINATE:NewFromVec2(self:GetRandomPoint(start_point, end_point))
end


--- Gets a number of points on a sine wave between the start and end points of the line.
-- @param #number amount The number of points to get
-- @param #table start_point (Optional) The start point of the line, defaults to the object's start point
-- @param #table end_point (Optional) The end point of the line, defaults to the object's end point
-- @param #number frequency (Optional) The frequency of the sine wave, default 1
-- @param #number phase (Optional) The phase of the sine wave, default 0
-- @param #number amplitude (Optional) The amplitude of the sine wave, default 100
-- @return #table The points
function LINE:GetPointsBetweenAsSineWave(amount, start_point, end_point, frequency, phase, amplitude)
    amount = amount or 20
    start_point = start_point or self:GetStartPoint()
    end_point = end_point or self:GetEndPoint()
    frequency = frequency or 1   -- number of cycles per unit of x
    phase = phase or 0           -- offset in radians
    amplitude = amplitude or 100 -- maximum height of the wave

    local points = {}

    -- Returns the y-coordinate of the sine wave at x
    local function sine_wave(x)
        return amplitude * math.sin(2 * math.pi * frequency * (x - start_point.x) + phase)
    end

    -- Plot x-amount of points on the sine wave between point_01 and point_02
    local x = start_point.x
    local step = (end_point.x - start_point.x) / 20
    for _=1, amount do
        local y = sine_wave(x)
        x = x + step
        table.add(points, {x=x, y=y})
    end
    return points
end

--- Calculates the bounding box of the line. The bounding box is the smallest rectangle that contains the line.
-- @return #table The bounding box of the line
function LINE:GetBoundingBox()
    local min_x, min_y, max_x, max_y = self.Points[1].x, self.Points[1].y, self.Points[2].x, self.Points[2].y

    for i = 2, #self.Points do
        local x, y = self.Points[i].x, self.Points[i].y

        if x < min_x then
            min_x = x
        end
        if y < min_y then
            min_y = y
        end
        if x > max_x then
            max_x = x
        end
        if y > max_y then
            max_y = y
        end
    end
    return {
        {x=min_x, y=min_x}, {x=max_x, y=min_y}, {x=max_x, y=max_y}, {x=min_x, y=max_y}
    }
end

--- Draws the line on the map.
-- @param #table points The points of the line
function LINE:Draw()
    for i=1, #self.Coords -1 do
        local c1 = self.Coords[i]
        local c2 = self.Coords[i % #self.Coords + 1]
        table.add(self.MarkIDs, c1:LineToAll(c2))
    end
end

--- Removes the drawing of the line from the map.
function LINE:RemoveDraw()
    for _, mark_id in pairs(self.MarkIDs) do
        UTILS.RemoveMark(mark_id)
    end
end
---
--
-- ### Author: **nielsvaes/coconutcockpit**
--
-- ===
-- @module Shapes.OVAL
-- @image MOOSE.JPG

--- OVAL class.
-- @type OVAL
-- @field #string ClassName Name of the class.
-- @field #number MajorAxis The major axis (radius) of the oval
-- @field #number MinorAxis The minor axis (radius) of the oval
-- @field #number Angle The angle the oval is rotated on

--- *The little man removed his hat, what an egg shaped head he had* -- Agatha Christie
--
-- ===
--
-- # OVAL
-- OVALs can be fetched from the drawings in the Mission Editor
--
-- The major and minor axes define how elongated the shape of an oval is. This class has some basic functions that the other SHAPE classes have as well.
-- Since it's not possible to draw the shape of an oval while the mission is running, right now the draw function draws 2 cicles. One with the major axis and one with
-- the minor axis. It then draws a diamond shape on an angle where the corners touch the major and minor axes to give an indication of what the oval actually
-- looks like.
--
-- Using ovals can be handy to find an area on the ground that is actually an intersection of a cone and a plane. So imagine you're faking the view cone of
-- a targeting pod and

--- OVAL class with properties and methods for handling ovals.
-- @field #OVAL
OVAL = {
    ClassName = "OVAL",
    MajorAxis = nil,
    MinorAxis = nil,
    Angle = 0,
    DrawPoly=nil
}

--- Finds an oval on the map by its name. The oval must be drawn on the map.
-- @param #string shape_name Name of the oval to find
-- @return #OVAL The found oval, or nil if not found
function OVAL:FindOnMap(shape_name)
    local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name))
    for _, layer in pairs(env.mission.drawings.layers) do
        for _, object in pairs(layer["objects"]) do
            if string.find(object["name"], shape_name, 1, true) then
                if object["polygonMode"] == "oval" then
                    self.CenterVec2 = { x = object["mapX"], y = object["mapY"] }
                    self.MajorAxis = object["r1"]
                    self.MinorAxis = object["r2"]
                    self.Angle = object["angle"]
                end
            end
        end
    end

    return self
end

--- Finds an oval by its name in the database.
-- @param #string shape_name Name of the oval to find
-- @return #OVAL The found oval, or nil if not found
function OVAL:Find(shape_name)
    return _DATABASE:FindShape(shape_name)
end

--- Creates a new oval from a center point, major axis, minor axis, and angle.
-- @param #table vec2 The center point of the oval
-- @param #number major_axis The major axis of the oval
-- @param #number minor_axis The minor axis of the oval
-- @param #number angle The angle of the oval
-- @return #OVAL The new oval
function OVAL:New(vec2, major_axis, minor_axis, angle)
    local self = BASE:Inherit(self, SHAPE_BASE:New())
    self.CenterVec2 = vec2
    self.MajorAxis = major_axis
    self.MinorAxis = minor_axis
    self.Angle = angle or 0

    return self
end

--- Gets the major axis of the oval.
-- @return #number The major axis of the oval
function OVAL:GetMajorAxis()
    return self.MajorAxis
end

--- Gets the minor axis of the oval.
-- @return #number The minor axis of the oval
function OVAL:GetMinorAxis()
    return self.MinorAxis
end

--- Gets the angle of the oval.
-- @return #number The angle of the oval
function OVAL:GetAngle()
    return self.Angle
end

--- Sets the major axis of the oval.
-- @param #number value The new major axis
function OVAL:SetMajorAxis(value)
    self.MajorAxis = value
end

--- Sets the minor axis of the oval.
-- @param #number value The new minor axis
function OVAL:SetMinorAxis(value)
    self.MinorAxis = value
end

--- Sets the angle of the oval.
-- @param #number value The new angle
function OVAL:SetAngle(value)
    self.Angle = value
end

--- Checks if a point is contained within the oval.
-- @param #table point The point to check
-- @return #bool True if the point is contained, false otherwise
function OVAL:ContainsPoint(point)
    local cos, sin = math.cos, math.sin
    local dx = point.x - self.CenterVec2.x
    local dy = point.y - self.CenterVec2.y
    local rx = dx * cos(self.Angle) + dy * sin(self.Angle)
    local ry = -dx * sin(self.Angle) + dy * cos(self.Angle)
    return rx * rx / (self.MajorAxis * self.MajorAxis) + ry * ry / (self.MinorAxis * self.MinorAxis) <= 1
end

--- Returns a random Vec2 within the oval.
-- @return #table The random Vec2
function OVAL:GetRandomVec2()
    local theta = math.rad(self.Angle)

    local random_point = math.sqrt(math.random())  --> uniformly
    --local random_point = math.random()  --> more clumped around center
    local phi = math.random() * 2 * math.pi
    local x_c = random_point * math.cos(phi)
    local y_c = random_point * math.sin(phi)
    local x_e = x_c * self.MajorAxis
    local y_e = y_c * self.MinorAxis
    local rx = (x_e * math.cos(theta) - y_e * math.sin(theta)) + self.CenterVec2.x
    local ry = (x_e * math.sin(theta) + y_e * math.cos(theta)) + self.CenterVec2.y

    return {x=rx, y=ry}
end

--- Calculates the bounding box of the oval. The bounding box is the smallest rectangle that contains the oval.
-- @return #table The bounding box of the oval
function OVAL:GetBoundingBox()
    local min_x = self.CenterVec2.x - self.MajorAxis
    local min_y = self.CenterVec2.y - self.MinorAxis
    local max_x = self.CenterVec2.x + self.MajorAxis
    local max_y = self.CenterVec2.y + self.MinorAxis

    return {
        {x=min_x, y=min_x}, {x=max_x, y=min_y}, {x=max_x, y=max_y}, {x=min_x, y=max_y}
    }
end

--- Draws the oval on the map, for debugging
-- @param #number angle (Optional) The angle of the oval. If nil will use self.Angle
function OVAL:Draw()
    --for pt in pairs(self:PointsOnEdge(20)) do
    --    COORDINATE:NewFromVec2(pt)
   --end

    self.DrawPoly = POLYGON:NewFromPoints(self:PointsOnEdge(20))
    self.DrawPoly:Draw(true)




    ---- TODO: draw a better shape using line segments
    --angle = angle or self.Angle
    --local coor = self:GetCenterCoordinate()
    --
    --table.add(self.MarkIDs, coor:CircleToAll(self.MajorAxis))
    --table.add(self.MarkIDs, coor:CircleToAll(self.MinorAxis))
    --table.add(self.MarkIDs, coor:LineToAll(coor:Translate(self.MajorAxis, self.Angle)))
    --
    --local pt_1 = coor:Translate(self.MajorAxis, self.Angle)
    --local pt_2 = coor:Translate(self.MinorAxis, self.Angle - 90)
    --local pt_3 = coor:Translate(self.MajorAxis, self.Angle - 180)
    --local pt_4 = coor:Translate(self.MinorAxis, self.Angle - 270)
    --table.add(self.MarkIDs, pt_1:QuadToAll(pt_2, pt_3, pt_4), -1, {0, 1, 0}, 1, {0, 1, 0})
end

--- Removes the drawing of the oval from the map
function OVAL:RemoveDraw()
    self.DrawPoly:RemoveDraw()
end


function OVAL:PointsOnEdge(num_points)
    num_points = num_points or 20
    local points = {}
    local dtheta = 2 * math.pi / num_points

    for i = 0, num_points - 1 do
        local theta = i * dtheta
        local x = self.CenterVec2.x + self.MajorAxis * math.cos(theta) * math.cos(self.Angle) - self.MinorAxis * math.sin(theta) * math.sin(self.Angle)
        local y = self.CenterVec2.y + self.MajorAxis * math.cos(theta) * math.sin(self.Angle) + self.MinorAxis * math.sin(theta) * math.cos(self.Angle)
        table.insert(points, {x = x, y = y})
    end

    return points
end


---
--
-- ### Author: **nielsvaes/coconutcockpit**
--
-- ===
-- @module Shapes.POLYGON
-- @image MOOSE.JPG

--- POLYGON class.
-- @type POLYGON
-- @field #string ClassName Name of the class.
-- @field #table Points List of 3D points defining the shape, this will be assigned automatically if you're passing in a drawing from the Mission Editor
-- @field #table Coords List of COORDINATE defining the path, this will be assigned automatically if you're passing in a drawing from the Mission Editor
-- @field #table MarkIDs List any MARKIDs this class use, this will be assigned automatically if you're passing in a drawing from the Mission Editor
-- @field #table Triangles List of TRIANGLEs that make up the shape of the POLYGON after being triangulated
-- @extends Core.Base#BASE

--- *Polygons are fashionable at the moment* -- Trip Hawkins
--
-- ===
--
-- # POLYGON
-- POLYGONs can be fetched from the drawings in the Mission Editor if the drawing is:
-- * A closed shape made with line segments
-- * A closed shape made with a freehand line
-- * A freehand drawn polygon
-- * A rect
-- Use the POLYGON:FindOnMap() of POLYGON:Find() functions for this. You can also create a non existing polygon in memory using the POLYGON:New() function. Pass in a
-- any number of Vec2s into this function to define the shape of the polygon you want.
--
-- You can draw very intricate and complex polygons in the Mission Editor to avoid (or include) map objects. You can then generate random points within this complex
-- shape for spawning groups or checking positions.
--
-- When a POLYGON is made, it's automatically triangulated. The resulting triangles are stored in POLYGON.Triangles. This also immeadiately saves the surface area
-- of the POLYGON. Because the POLYGON is triangulated, it's possible to generate random points within this POLYGON without having to use a trial and error method to see if
-- the point is contained within the shape.
-- Using POLYGON:GetRandomVec2() will result in a truly, non-biased, random Vec2 within the shape. You'll want to use this function most. There's also POLYGON:GetRandomNonWeightedVec2
-- which ignores the size of the triangles in the polygon to pick a random points. This will result in more points clumping together in parts of the polygon where the triangles are
-- the smallest.

---
-- @field #POLYGON
POLYGON = {
    ClassName = "POLYGON",
    Points = {},
    Coords = {},
    Triangles = {},
    SurfaceArea = 0,
    TriangleMarkIDs = {},
    OutlineMarkIDs = {},
    Angle = nil,   -- for arrows
    Heading = nil  -- for arrows
}

--- Finds a polygon on the map by its name. The polygon must be added in the mission editor.
-- @param #string shape_name Name of the polygon to find
-- @return #POLYGON The found polygon, or nil if not found
function POLYGON:FindOnMap(shape_name)
    local self = BASE:Inherit(self, SHAPE_BASE:FindOnMap(shape_name))

    for _, layer in pairs(env.mission.drawings.layers) do
        for _, object in pairs(layer["objects"]) do
            if object["name"] == shape_name then
                if (object["primitiveType"] == "Line" and object["closed"] == true) or (object["polygonMode"] == "free") then
                    for _, point in UTILS.spairs(object["points"]) do
                        local p = {x = object["mapX"] + point["x"],
                                   y = object["mapY"] + point["y"] }
                        local coord = COORDINATE:NewFromVec2(p)
                        self.Points[#self.Points + 1] = p
                        self.Coords[#self.Coords + 1] = coord
                    end
                elseif object["polygonMode"] == "rect" then
                    local angle = object["angle"]
                    local half_width  = object["width"] / 2
                    local half_height = object["height"] / 2

                    local p1 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x - half_height, y = self.CenterVec2.y + half_width }, self.CenterVec2, angle)
                    local p2 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x + half_height, y = self.CenterVec2.y + half_width }, self.CenterVec2, angle)
                    local p3 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x + half_height, y = self.CenterVec2.y - half_width }, self.CenterVec2, angle)
                    local p4 = UTILS.RotatePointAroundPivot({ x = self.CenterVec2.x - half_height, y = self.CenterVec2.y - half_width }, self.CenterVec2, angle)

                    self.Points = {p1, p2, p3, p4}
                    for _, point in pairs(self.Points) do
                        self.Coords[#self.Coords + 1] = COORDINATE:NewFromVec2(point)
                    end
                elseif object["polygonMode"] == "arrow" then
                    for _, point in UTILS.spairs(object["points"]) do
                        local p = {x = object["mapX"] + point["x"],
                                   y = object["mapY"] + point["y"] }
                        local coord = COORDINATE:NewFromVec2(p)
                        self.Points[#self.Points + 1] = p
                        self.Coords[#self.Coords + 1] = coord
                    end
                    self.Angle = object["angle"]
                    self.Heading = UTILS.ClampAngle(self.Angle + 90)
                end
            end
        end
    end

    if #self.Points == 0 then
        return nil
    end

    self.CenterVec2 = self:GetCentroid()
    self.Triangles = self:Triangulate()
    self.SurfaceArea = self:__CalculateSurfaceArea()

    self.TriangleMarkIDs = {}
    self.OutlineMarkIDs = {}
    return self
end

--- Creates a polygon from a zone. The zone must be defined in the mission.
-- @param #string zone_name Name of the zone
-- @return #POLYGON The polygon created from the zone, or nil if the zone is not found
function POLYGON:FromZone(zone_name)
    for _, zone in pairs(env.mission.triggers.zones) do
        if zone["name"] == zone_name then
            return POLYGON:New(unpack(zone["verticies"] or {}))
        end
    end
end

--- Finds a polygon by its name in the database.
-- @param #string shape_name Name of the polygon to find
-- @return #POLYGON The found polygon, or nil if not found
function POLYGON:Find(shape_name)
    return _DATABASE:FindShape(shape_name)
end

--- Creates a new polygon from a list of points. Each point is a table with 'x' and 'y' fields.
-- @param #table ... Points of the polygon
-- @return #POLYGON The new polygon
function POLYGON:New(...)
    local self = BASE:Inherit(self, SHAPE_BASE:New())

    self.Points = {...}
    self.Coords = {}
    for _, point in UTILS.spairs(self.Points) do
        table.insert(self.Coords, COORDINATE:NewFromVec2(point))
    end
    self.Triangles = self:Triangulate()
    self.SurfaceArea = self:__CalculateSurfaceArea()

    return self
end

--- Calculates the centroid of the polygon. The centroid is the average of the 'x' and 'y' coordinates of the points.
-- @return #table The centroid of the polygon
function POLYGON:GetCentroid()
    local function sum(t)
        local total = 0
        for _, value in pairs(t) do
            total = total + value
        end
        return total
    end

    local x_values = {}
    local y_values = {}
    local length = table.length(self.Points)

    for _, point in pairs(self.Points) do
        table.insert(x_values, point.x)
        table.insert(y_values, point.y)
    end

    local x = sum(x_values) / length
    local y = sum(y_values) / length

    return {
            ["x"] = x,
            ["y"] = y
           }
end

--- Returns the coordinates of the polygon. Each coordinate is a COORDINATE object.
-- @return #table The coordinates of the polygon
function POLYGON:GetCoordinates()
    return self.Coords
end

--- Returns the start coordinate of the polygon. The start coordinate is the first point of the polygon.
-- @return #COORDINATE The start coordinate of the polygon
function POLYGON:GetStartCoordinate()
    return self.Coords[1]
end

--- Returns the end coordinate of the polygon. The end coordinate is the last point of the polygon.
-- @return #COORDINATE The end coordinate of the polygon
function POLYGON:GetEndCoordinate()
    return self.Coords[#self.Coords]
end

--- Returns the start point of the polygon. The start point is the first point of the polygon.
-- @return #table The start point of the polygon
function POLYGON:GetStartPoint()
    return self.Points[1]
end

--- Returns the end point of the polygon. The end point is the last point of the polygon.
-- @return #table The end point of the polygon
function POLYGON:GetEndPoint()
    return self.Points[#self.Points]
end

--- Returns the points of the polygon. Each point is a table with 'x' and 'y' fields.
-- @return #table The points of the polygon
function POLYGON:GetPoints()
    return self.Points
end

--- Calculates the surface area of the polygon. The surface area is the sum of the areas of the triangles that make up the polygon.
-- @return #number The surface area of the polygon
function POLYGON:GetSurfaceArea()
    return self.SurfaceArea
end

--- Calculates the bounding box of the polygon. The bounding box is the smallest rectangle that contains the polygon.
-- @return #table The bounding box of the polygon
function POLYGON:GetBoundingBox()
    local min_x, min_y, max_x, max_y = self.Points[1].x, self.Points[1].y, self.Points[1].x, self.Points[1].y

    for i = 2, #self.Points do
        local x, y = self.Points[i].x, self.Points[i].y

        if x < min_x then
            min_x = x
        end
        if y < min_y then
            min_y = y
        end
        if x > max_x then
            max_x = x
        end
        if y > max_y then
            max_y = y
        end
    end
    return {
        {x=min_x, y=min_x}, {x=max_x, y=min_y}, {x=max_x, y=max_y}, {x=min_x, y=max_y}
    }
end

--- Triangulates the polygon. The polygon is divided into triangles.
-- @param #table points (optional) Points of the polygon or other points if you're just using the POLYGON class without an object of it
-- @return #table The triangles of the polygon
function POLYGON:Triangulate(points)
    points = points or self.Points
    local triangles = {}

    local function get_orientation(shape_points)
        local sum = 0
        for i = 1, #shape_points do
            local j = i % #shape_points + 1
            sum = sum + (shape_points[j].x - shape_points[i].x) * (shape_points[j].y + shape_points[i].y)
        end
        return sum >= 0 and "clockwise" or "counter-clockwise" -- sum >= 0, return "clockwise", else return "counter-clockwise"
    end

    local function ensure_clockwise(shape_points)
        local orientation = get_orientation(shape_points)
        if orientation == "counter-clockwise" then
            -- Reverse the order of shape_points so they're clockwise
            local reversed = {}
            for i = #shape_points, 1, -1 do
                table.insert(reversed, shape_points[i])
            end
            return reversed
        end
        return shape_points
    end

    local function is_clockwise(p1, p2, p3)
        local cross_product = (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x)
        return cross_product < 0
    end

    local function divide_recursively(shape_points)
        if #shape_points == 3 then
            table.insert(triangles, TRIANGLE:New(shape_points[1], shape_points[2], shape_points[3]))
        elseif #shape_points > 3 then                                                                -- find an ear -> a triangle with no other points inside it
            for i, p1 in ipairs(shape_points) do
                local p2 = shape_points[(i % #shape_points) + 1]
                local p3 = shape_points[(i + 1) % #shape_points + 1]
                local triangle = TRIANGLE:New(p1, p2, p3)
                local is_ear = true

                if not is_clockwise(p1, p2, p3) then
                    is_ear = false
                else
                    for _, point in ipairs(shape_points) do
                        if point ~= p1 and point ~= p2 and point ~= p3 and triangle:ContainsPoint(point) then
                            is_ear = false
                            break
                        end
                    end
                end

                if is_ear then
                    -- Check if any point in the original polygon is inside the ear triangle
                    local is_valid_triangle = true
                    for _, point in ipairs(points) do
                        if point ~= p1 and point ~= p2 and point ~= p3 and triangle:ContainsPoint(point) then
                            is_valid_triangle = false
                            break
                        end
                    end
                    if is_valid_triangle then
                        table.insert(triangles, triangle)
                        local remaining_points = {}
                        for j, point in ipairs(shape_points) do
                            if point ~= p2 then
                                table.insert(remaining_points, point)
                            end
                        end
                        divide_recursively(remaining_points)
                        break
                    end
                end
            end
        end
    end

    points = ensure_clockwise(points)
    divide_recursively(points)
    return triangles
end

function POLYGON:CovarianceMatrix()
    local cx, cy = self:GetCentroid()
    local covXX, covYY, covXY = 0, 0, 0
    for _, p in ipairs(self.points) do
        covXX = covXX + (p.x - cx)^2
        covYY = covYY + (p.y - cy)^2
        covXY = covXY + (p.x - cx) * (p.y - cy)
    end
    covXX = covXX / (#self.points - 1)
    covYY = covYY / (#self.points - 1)
    covXY = covXY / (#self.points - 1)
    return covXX, covYY, covXY
end

function POLYGON:Direction()
    local covXX, covYY, covXY = self:CovarianceMatrix()
    -- Simplified calculation for the largest eigenvector's direction
    local theta = 0.5 * math.atan2(2 * covXY, covXX - covYY)
    return math.cos(theta), math.sin(theta)
end

--- Returns a random Vec2 within the polygon. The Vec2 is weighted by the areas of the triangles that make up the polygon.
-- @return #table The random Vec2
function POLYGON:GetRandomVec2()

    local weights = {}
    for _, triangle in pairs(self.Triangles) do
        weights[triangle] = triangle.SurfaceArea / self.SurfaceArea
    end

    local random_weight = math.random()
    local accumulated_weight = 0
    for triangle, weight in pairs(weights) do
        accumulated_weight = accumulated_weight + weight
        if accumulated_weight >= random_weight then
            return triangle:GetRandomVec2()
        end
    end
end

--- Returns a random non-weighted Vec2 within the polygon. The Vec2 is chosen from one of the triangles that make up the polygon.
-- @return #table The random non-weighted Vec2
function POLYGON:GetRandomNonWeightedVec2()
    return self.Triangles[math.random(1, #self.Triangles)]:GetRandomVec2()
end

--- Checks if a point is contained within the polygon. The point is a table with 'x' and 'y' fields.
-- @param #table point The point to check
-- @param #table points (optional) Points of the polygon or other points if you're just using the POLYGON class without an object of it
-- @return #bool True if the point is contained, false otherwise
function POLYGON:ContainsPoint(point, polygon_points)
    local x = point.x
    local y = point.y

    polygon_points = polygon_points or self.Points

    local counter = 0
    local num_points = #polygon_points
    for current_index = 1, num_points do
        local next_index = (current_index % num_points) + 1
        local current_x, current_y = polygon_points[current_index].x, polygon_points[current_index].y
        local next_x, next_y = polygon_points[next_index].x, polygon_points[next_index].y
        if ((current_y > y) ~= (next_y > y)) and (x < (next_x - current_x) * (y - current_y) / (next_y - current_y) + current_x) then
            counter = counter + 1
        end
    end
    return counter % 2 == 1
end

--- Draws the polygon on the map. The polygon can be drawn with or without inner triangles. This is just for debugging
-- @param #bool include_inner_triangles Whether to include inner triangles in the drawing
function POLYGON:Draw(include_inner_triangles)
    include_inner_triangles = include_inner_triangles or false
    for i=1, #self.Coords do
        local c1 = self.Coords[i]
        local c2 = self.Coords[i % #self.Coords + 1]
        table.add(self.OutlineMarkIDs, c1:LineToAll(c2))
    end


    if include_inner_triangles then
        for _, triangle in ipairs(self.Triangles) do
            triangle:Draw()
        end
    end
end

--- Removes the drawing of the polygon from the map.
function POLYGON:RemoveDraw()
    for _, triangle in pairs(self.Triangles) do
        triangle:RemoveDraw()
    end
    for _, mark_id in pairs(self.OutlineMarkIDs) do
        UTILS.RemoveMark(mark_id)
    end
end

--- Calculates the surface area of the polygon. The surface area is the sum of the areas of the triangles that make up the polygon.
-- @return #number The surface area of the polygon
function POLYGON:__CalculateSurfaceArea()
    local area = 0
    for _, triangle in pairs(self.Triangles) do
        area = area + triangle.SurfaceArea
    end
    return area
end























--- TRIANGLE class with properties and methods for handling triangles. This class is mostly used by the POLYGON class, but you can use it on its own as well
--
-- ### Author: **nielsvaes/coconutcockpit**
--
--
-- ===
-- @module Shapes.TRIANGLE
-- @image MOOSE.JPG

--- LINE class.
-- @type CUBE
-- @field #string ClassName Name of the class.
-- @field #number Points points of the line
-- @field #number Coords coordinates of the line

--
-- ===

---
-- @field #TRIANGLE
TRIANGLE = {
    ClassName = "TRIANGLE",
    Points = {},
    Coords = {},
    SurfaceArea = 0
}

--- Creates a new triangle from three points. The points need to be given as Vec2s
-- @param #table p1 The first point of the triangle
-- @param #table p2 The second point of the triangle
-- @param #table p3 The third point of the triangle
-- @return #TRIANGLE The new triangle
function TRIANGLE:New(p1, p2, p3)
    local self = BASE:Inherit(self, SHAPE_BASE:New())
    self.Points = {p1, p2, p3}

    local center_x = (p1.x + p2.x + p3.x) / 3
    local center_y = (p1.y + p2.y + p3.y) / 3
    self.CenterVec2 = {x=center_x, y=center_y}

    for _, pt in pairs({p1, p2, p3}) do
        table.add(self.Coords, COORDINATE:NewFromVec2(pt))
    end

    self.SurfaceArea = math.abs((p2.x - p1.x) * (p3.y - p1.y) - (p3.x - p1.x) * (p2.y - p1.y)) * 0.5

    self.MarkIDs = {}
    return self
end

--- Checks if a point is contained within the triangle.
-- @param #table pt The point to check
-- @param #table points (optional) The points of the triangle, or 3 other points if you're just using the TRIANGLE class without an object of it
-- @return #bool True if the point is contained, false otherwise
function TRIANGLE:ContainsPoint(pt, points)
    points = points or self.Points

    local function sign(p1, p2, p3)
        return (p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y)
    end

    local d1 = sign(pt, self.Points[1], self.Points[2])
    local d2 = sign(pt, self.Points[2], self.Points[3])
    local d3 = sign(pt, self.Points[3], self.Points[1])

    local has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0)
    local has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0)

    return not (has_neg and has_pos)
end

--- Returns a random Vec2 within the triangle.
-- @param #table points The points of the triangle, or 3 other points if you're just using the TRIANGLE class without an object of it
-- @return #table The random Vec2
function TRIANGLE:GetRandomVec2(points)

    math.random()
    math.random()
    math.random()
    
    points = points or self.Points
    local pt = {math.random(), math.random()}
    table.sort(pt)
    local s = pt[1]
    local t = pt[2] - pt[1]
    local u = 1 - pt[2]

    return {x = s * points[1].x + t * points[2].x + u * points[3].x,
            y = s * points[1].y + t * points[2].y + u * points[3].y}
end

--- Draws the triangle on the map, just for debugging
function TRIANGLE:Draw()
    for i=1, #self.Coords do
        local c1 = self.Coords[i]
        local c2 = self.Coords[i % #self.Coords + 1]
        table.add(self.MarkIDs, c1:LineToAll(c2))
    end
end

--- Removes the drawing of the triangle from the map.
function TRIANGLE:RemoveDraw()
    for _, mark_id in pairs(self.MarkIDs) do
        UTILS.RemoveMark(mark_id)
    end
end
--- **Sound** - Manage user sound.
--
-- ===
-- 
-- ## Features:
-- 
--   * Play sounds wihtin running missions.
-- 
-- ===
-- 
-- Management of DCS User Sound.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===
-- 
-- @module Sound.UserSound
-- @image Core_Usersound.JPG

do -- UserSound

  -- @type USERSOUND
  -- @extends Core.Base#BASE


  --- Management of DCS User Sound.
  -- 
  -- ## USERSOUND constructor
  --   
  --   * @{#USERSOUND.New}(): Creates a new USERSOUND object.
  -- 
  -- @field #USERSOUND
  USERSOUND = {
    ClassName = "USERSOUND",
  }
  
  --- USERSOUND Constructor.
  -- @param #USERSOUND self
  -- @param #string UserSoundFileName The filename of the usersound.
  -- @return #USERSOUND
  function USERSOUND:New( UserSoundFileName ) 
  
    local self = BASE:Inherit( self, BASE:New() ) -- #USERSOUND

    self.UserSoundFileName = UserSoundFileName

    return self
  end


  --- Set usersound filename.
  -- @param #USERSOUND self
  -- @param #string UserSoundFileName The filename of the usersound.
  -- @return #USERSOUND The usersound instance.
  -- @usage
  --   local BlueVictory = USERSOUND:New( "BlueVictory.ogg" )
  --   BlueVictory:SetFileName( "BlueVictoryLoud.ogg" ) -- Set the BlueVictory to change the file name to play a louder sound.
  --   
  function USERSOUND:SetFileName( UserSoundFileName ) 
    
    self.UserSoundFileName = UserSoundFileName

    return self
  end  

  


  --- Play the usersound to all players.
  -- @param #USERSOUND self
  -- @return #USERSOUND The usersound instance.
  -- @usage
  --   local BlueVictory = USERSOUND:New( "BlueVictory.ogg" )
  --   BlueVictory:ToAll() -- Play the sound that Blue has won.
  --   
  function USERSOUND:ToAll() 
    
    trigger.action.outSound( self.UserSoundFileName )
    
    return self
  end  

  
  --- Play the usersound to the given coalition.
  -- @param #USERSOUND self
  -- @param DCS#coalition Coalition The coalition to play the usersound to.
  -- @return #USERSOUND The usersound instance.
  -- @usage
  --   local BlueVictory = USERSOUND:New( "BlueVictory.ogg" )
  --   BlueVictory:ToCoalition( coalition.side.BLUE ) -- Play the sound that Blue has won to the blue coalition.
  --   
  function USERSOUND:ToCoalition( Coalition ) 
    
    trigger.action.outSoundForCoalition(Coalition, self.UserSoundFileName )
    
    return self
  end  


  --- Play the usersound to the given country.
  -- @param #USERSOUND self
  -- @param DCS#country Country The country to play the usersound to.
  -- @return #USERSOUND The usersound instance.
  -- @usage
  --   local BlueVictory = USERSOUND:New( "BlueVictory.ogg" )
  --   BlueVictory:ToCountry( country.id.USA ) -- Play the sound that Blue has won to the USA country.
  --   
  function USERSOUND:ToCountry( Country ) 
    
    trigger.action.outSoundForCountry( Country, self.UserSoundFileName )
    
    return self
  end  


  --- Play the usersound to the given @{Wrapper.Group}.
  -- @param #USERSOUND self
  -- @param Wrapper.Group#GROUP Group The @{Wrapper.Group} to play the usersound to.
  -- @param #number Delay (Optional) Delay in seconds, before the sound is played. Default 0.
  -- @return #USERSOUND The usersound instance.
  -- @usage
  --   local BlueVictory = USERSOUND:New( "BlueVictory.ogg" )
  --   local PlayerGroup = GROUP:FindByName( "PlayerGroup" ) -- Search for the active group named "PlayerGroup", that contains a human player.
  --   BlueVictory:ToGroup( PlayerGroup ) -- Play the victory sound to the player group.
  --   
  function USERSOUND:ToGroup( Group, Delay ) 
  
    Delay=Delay or 0
    if Delay>0 then
      SCHEDULER:New(nil, USERSOUND.ToGroup,{self, Group}, Delay)      
    else
      trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName )
    end
    
    return self
  end  
  
  --- Play the usersound to the given @{Wrapper.Unit}.
  -- @param #USERSOUND self
  -- @param Wrapper.Unit#UNIT Unit The @{Wrapper.Unit} to play the usersound to.
  -- @param #number Delay (Optional) Delay in seconds, before the sound is played. Default 0.
  -- @return #USERSOUND The usersound instance.
  -- @usage
  --   local BlueVictory = USERSOUND:New( "BlueVictory.ogg" )
  --   local PlayerUnit = UNIT:FindByName( "PlayerUnit" ) -- Search for the active unit named "PlayerUnit", a human player.
  --   BlueVictory:ToUnit( PlayerUnit ) -- Play the victory sound to the player unit.
  --   
  function USERSOUND:ToUnit( Unit, Delay ) 
  
    Delay=Delay or 0
    if Delay>0 then
      SCHEDULER:New(nil, USERSOUND.ToUnit,{self, Unit}, Delay)      
    else
      trigger.action.outSoundForUnit( Unit:GetID(), self.UserSoundFileName )
    end
    
    return self
  end 


  --- Play the usersound to the given @{Wrapper.Client}.
  -- @param #USERSOUND self
  -- @param Wrapper.Client#CLIENT The @{Wrapper.Client} to play the usersound to.
  -- @param #number Delay (Optional) Delay in seconds, before the sound is played. Default 0.
  -- @return #USERSOUND The usersound instance.
  -- @usage
  --   local BlueVictory = USERSOUND:New( "BlueVictory.ogg" )
  --   local PlayerUnit = CLIENT:FindByPlayerName("Karl Heinz")-- Search for the active client with playername "Karl Heinz", a human player.
  --   BlueVictory:ToClient( PlayerUnit ) -- Play the victory sound to the player unit.
  --   
  function USERSOUND:ToClient( Client, Delay ) 
    Delay=Delay or 0
    if Delay>0 then
      SCHEDULER:New(nil, USERSOUND.ToClient,{self, Client}, Delay)      
    else
      trigger.action.outSoundForUnit( Client:GetID(), self.UserSoundFileName )
    end
    return self
  end 
end--- **Sound** - Sound output classes.
--
-- ===
-- 
-- ## Features:
-- 
--   * Create a SOUNDFILE object (mp3 or ogg) to be played via DCS or SRS transmissions
--   * Create a SOUNDTEXT object for text-to-speech output vis SRS Simple-Text-To-Speech (MSRS)
-- 
-- ===
-- 
-- ### Author: **funkyfranky**
-- 
-- ===
-- 
-- There are two classes, SOUNDFILE and SOUNDTEXT, defined in this section that deal with playing
-- sound files or arbitrary text (via SRS Simple-Text-To-Speech), respectively.
-- 
-- The SOUNDFILE and SOUNDTEXT objects can be defined and used in other MOOSE classes.
-- 
-- 
-- @module Sound.SoundOutput
-- @image Sound_SoundOutput.png

do -- Sound Base

  -- @type SOUNDBASE
  -- @field #string ClassName Name of the class.
  -- @extends Core.Base#BASE


  --- Basic sound output inherited by other classes suche as SOUNDFILE and SOUNDTEXT.
  -- 
  -- This class is **not** meant to be used by "ordinary" users.
  -- 
  -- @field #SOUNDBASE
  SOUNDBASE={
    ClassName   = "SOUNDBASE",
  }
  
  --- Constructor to create a new SOUNDBASE object.
  -- @param #SOUNDBASE self
  -- @return #SOUNDBASE self
  function SOUNDBASE:New()
  
    -- Inherit BASE.
    local self=BASE:Inherit(self, BASE:New()) -- #SOUNDBASE

    

    return self
  end
  
  --- Function returns estimated speech time in seconds.
  -- Assumptions for time calc: 100 Words per min, avarage of 5 letters for english word so
  -- 
  --   * 5 chars * 100wpm = 500 characters per min = 8.3 chars per second
  --   
  -- So lengh of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec map function:
  -- 
  -- * (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
  -- 
  -- @param #string Text The text string to analyze.
  -- @param #number Speed Speed factor. Default 1.
  -- @param #boolean isGoogle If true, google text-to-speech is used.
  function SOUNDBASE:GetSpeechTime(length,speed,isGoogle)
  
    local maxRateRatio = 3 
  
    speed = speed or 1.0
    isGoogle = isGoogle or false
  
    local speedFactor = 1.0
    if isGoogle then
        speedFactor = speed
    else
        if speed ~= 0 then
            speedFactor = math.abs(speed) * (maxRateRatio - 1) / 10 + 1
        end
        if speed < 0 then
            speedFactor = 1/speedFactor
        end
    end
  
    -- Words per minute.
    local wpm = math.ceil(100 * speedFactor)
    
    -- Characters per second.
    local cps = math.floor((wpm * 5)/60)
  
    if type(length) == "string" then
        length = string.len(length)
    end
  
    return math.ceil(length/cps)
  end  
  
end


do -- Sound File

  -- @type SOUNDFILE
  -- @field #string ClassName Name of the class
  -- @field #string filename Name of the flag.
  -- @field #string path Directory path, where the sound file is located. This includes the final slash "/".
  -- @field #string duration Duration of the sound file in seconds.
  -- @field #string subtitle Subtitle of the transmission.
  -- @field #number subduration Duration in seconds how long the subtitle is displayed.
  -- @field #boolean useSRS If true, sound file is played via SRS. Sound file needs to be on local disk not inside the miz file!
  -- @extends Core.Base#BASE


  --- Sound files used by other classes.
  -- 
  -- # The SOUNDFILE Concept
  --   
  -- A SOUNDFILE object hold the important properties that are necessary to play the sound file, e.g. its file name, path, duration.
  -- 
  -- It can be created with the @{#SOUNDFILE.New}(*FileName*, *Path*, *Duration*) function:
  -- 
  --     local soundfile=SOUNDFILE:New("My Soundfile.ogg", "Sound File/", 3.5)
  -- 
  -- ## SRS
  -- 
  -- If sound files are supposed to be played via SRS, you need to use the @{#SOUNDFILE.SetPlayWithSRS}() function.
  -- 
  -- # Location/Path
  -- 
  -- ## DCS
  -- 
  -- DCS can only play sound files that are located inside the mission (.miz) file. In particular, DCS cannot make use of files that are stored on
  -- your hard drive.
  -- 
  -- The default location where sound files are stored in DCS is the directory "l10n/DEFAULT/". This is where sound files are placed, if they are
  -- added via the mission editor (TRIGGERS-->ACTIONS-->SOUND TO ALL). Note however, that sound files which are not added with a trigger command,
  -- will be deleted each time the mission is saved! Therefore, this directory is not ideal to be used especially if many sound files are to
  -- be included since for each file a trigger action needs to be created. Which is cumbersome, to say the least.
  -- 
  -- The recommended way is to create a new folder inside the mission (.miz) file (a miz file is essentially zip file and can be opened, e.g., with 7-Zip)
  -- and to place the sound files in there. Sound files in these folders are not wiped out by DCS on the next save.
  -- 
  -- ## SRS
  -- 
  -- SRS sound files need to be located on your local drive (not inside the miz). Therefore, you need to specify the full path.
  -- 
  -- @field #SOUNDFILE
  SOUNDFILE={
    ClassName   = "SOUNDFILE",
    filename    =  nil,
    path        = "l10n/DEFAULT/",
    duration    =    3,
    subtitle    =   nil,
    subduration =   0,
    useSRS      = false,
  }
  
  --- Constructor to create a new SOUNDFILE object.
  -- @param #SOUNDFILE self
  -- @param #string FileName The name of the sound file, e.g. "Hello World.ogg".
  -- @param #string Path The path of the directory, where the sound file is located. Default is "l10n/DEFAULT/" within the miz file.
  -- @param #number Duration Duration in seconds, how long it takes to play the sound file. Default is 3 seconds.
  -- @param #boolean UseSrs Set if SRS should be used to play this file. Default is false.
  -- @return #SOUNDFILE self
  function SOUNDFILE:New(FileName, Path, Duration, UseSrs)

    -- Inherit BASE.
    local self=BASE:Inherit(self, BASE:New()) -- #SOUNDFILE

    -- Debug info:
    self:F( {FileName, Path, Duration, UseSrs} )

    -- Set file name.
    self:SetFileName(FileName)

    -- Set if SRS should be used to play this file
    self:SetPlayWithSRS(UseSrs or false)

    -- Set path.
    self:SetPath(Path)

    -- Set duration.
    self:SetDuration(Duration)

    return self
  end

  --- Set path, where the sound file is located.
  -- @param #SOUNDFILE self
  -- @param #string Path Path to the directory, where the sound file is located. In case this is nil, it defaults to the DCS mission temp directory.
  -- @return #SOUNDFILE self
  function SOUNDFILE:SetPath(Path)
    self:F( {Path} )

    -- Init path.
    if not Path then
      if self.useSRS then -- use path to mission temp dir
        self.path = lfs.tempdir() .. "Mission\\l10n\\DEFAULT"
      else -- use internal path in miz file
        self.path="l10n/DEFAULT/"
      end
    else
      self.path = Path
    end

    -- Remove (back)slashes.
    local nmax=1000 ; local n=1
    while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do
      self.path=self.path:sub(1,#self.path-1)
      n=n+1
    end

    -- Append slash.
    self.path=self.path.."/"

    self:T("self.path=".. self.path)

    return self
  end

  --- Get path of the directory, where the sound file is located.
  -- @param #SOUNDFILE self
  -- @return #string Path.
  function SOUNDFILE:GetPath()
    local path=self.path or "l10n/DEFAULT/"
    return path
  end

  --- Set sound file name. This must be a .ogg or .mp3 file!
  -- @param #SOUNDFILE self
  -- @param #string FileName Name of the file. Default is "Hello World.mp3".
  -- @return #SOUNDFILE self
  function SOUNDFILE:SetFileName(FileName)
    --TODO: check that sound file is really .ogg or .mp3
    self.filename=FileName or "Hello World.mp3"
    return self
  end

  --- Get the sound file name.
  -- @param #SOUNDFILE self
  -- @return #string Name of the soud file. This does *not* include its path.
  function SOUNDFILE:GetFileName()
    return self.filename
  end


  --- Set duration how long it takes to play the sound file.
  -- @param #SOUNDFILE self
  -- @param #string Duration Duration in seconds. Default 3 seconds.
  -- @return #SOUNDFILE self
  function SOUNDFILE:SetDuration(Duration)
    if Duration and type(Duration)=="string" then
      Duration=tonumber(Duration)
    end
    self.duration=Duration or 3
    return self
  end  

  --- Get duration how long the sound file takes to play.
  -- @param #SOUNDFILE self
  -- @return #number Duration in seconds.
  function SOUNDFILE:GetDuration()
    return self.duration or 3
  end

  --- Get the complete sound file name inlcuding its path.
  -- @param #SOUNDFILE self
  -- @return #string Name of the sound file.
  function SOUNDFILE:GetName()
    local path=self:GetPath()
    local filename=self:GetFileName()
    local name=string.format("%s%s", path, filename)
    return name
  end
  
  --- Set whether sound files should be played via SRS.
  -- @param #SOUNDFILE self
  -- @param #boolean Switch If true or nil, use SRS. If false, use DCS transmission.
  -- @return #SOUNDFILE self
  function SOUNDFILE:SetPlayWithSRS(Switch)
    self:F( {Switch} )
    if Switch==true or Switch==nil then
      self.useSRS=true
    else
      self.useSRS=false
    end
    self:T("self.useSRS=".. tostring(self.useSRS))
    return self
  end

end

do -- Text-To-Speech

  -- @type SOUNDTEXT
  -- @field #string ClassName Name of the class
  -- @field #string text Text to speak.
  -- @field #number duration Duration in seconds.
  -- @field #string gender Gender: "male", "female".
  -- @field #string culture Culture, e.g. "en-GB".
  -- @field #string voice Specific voice to use. Overrules `gender` and `culture` settings.
  -- @extends Core.Base#BASE


  --- Text-to-speech objects for other classes.
  -- 
  -- # The SOUNDTEXT Concept
  -- 
  -- A SOUNDTEXT object holds all necessary information to play a general text via SRS Simple-Text-To-Speech.
  -- 
  -- It can be created with the @{#SOUNDTEXT.New}(*Text*, *Duration*) function.
  --   
  --   * @{#SOUNDTEXT.New}(*Text, Duration*): Creates a new SOUNDTEXT object.
  --
  -- # Options
  -- 
  -- ## Gender
  -- 
  -- You can choose a gender ("male" or "femal") with the @{#SOUNDTEXT.SetGender}(*Gender*) function.
  -- Note that the gender voice needs to be installed on your windows machine for the used culture (see below).
  -- 
  -- ## Culture
  -- 
  -- You can choose a "culture" (accent) with the @{#SOUNDTEXT.SetCulture}(*Culture*) function, where the default (SRS) culture is "en-GB".
  -- 
  -- Other examples for culture are: "en-US" (US accent), "de-DE" (German), "it-IT" (Italian), "ru-RU" (Russian), "zh-CN" (Chinese).
  -- 
  -- Note that the chosen culture needs to be installed on your windows machine. 
  --
  -- ## Specific Voice
  -- 
  -- You can use a specific voice for the transmission with the @{#SOUNDTEXT.SetVoice}(*VoiceName*) function. Here are some examples
  --
  -- * Name: Microsoft Hazel Desktop, Culture: en-GB,  Gender: Female, Age: Adult, Desc: Microsoft Hazel Desktop - English (Great Britain)
  -- * Name: Microsoft David Desktop, Culture: en-US,  Gender: Male, Age: Adult, Desc: Microsoft David Desktop - English (United States)
  -- * Name: Microsoft Zira Desktop, Culture: en-US,  Gender: Female, Age: Adult, Desc: Microsoft Zira Desktop - English (United States)
  -- * Name: Microsoft Hedda Desktop, Culture: de-DE,  Gender: Female, Age: Adult, Desc: Microsoft Hedda Desktop - German
  -- * Name: Microsoft Helena Desktop, Culture: es-ES,  Gender: Female, Age: Adult, Desc: Microsoft Helena Desktop - Spanish (Spain)
  -- * Name: Microsoft Hortense Desktop, Culture: fr-FR,  Gender: Female, Age: Adult, Desc: Microsoft Hortense Desktop - French
  -- * Name: Microsoft Elsa Desktop, Culture: it-IT,  Gender: Female, Age: Adult, Desc: Microsoft Elsa Desktop - Italian (Italy)
  -- * Name: Microsoft Irina Desktop, Culture: ru-RU,  Gender: Female, Age: Adult, Desc: Microsoft Irina Desktop - Russian
  -- * Name: Microsoft Huihui Desktop, Culture: zh-CN,  Gender: Female, Age: Adult, Desc: Microsoft Huihui Desktop - Chinese (Simplified)
  -- 
  -- Note that this must be installed on your windos machine. Also note that this overrides any culture and gender settings.
  -- 
  -- @field #SOUNDTEXT
  SOUNDTEXT={
    ClassName   = "SOUNDTEXT",
  }
  
  --- Constructor to create a new SOUNDTEXT object.
  -- @param #SOUNDTEXT self
  -- @param #string Text The text to speak.
  -- @param #number Duration Duration in seconds, how long it takes to play the text. Default is 3 seconds.
  -- @return #SOUNDTEXT self
  function SOUNDTEXT:New(Text, Duration)
  
    -- Inherit BASE.
    local self=BASE:Inherit(self, BASE:New()) -- #SOUNDTEXT

    self:SetText(Text)
    self:SetDuration(Duration or MSRS.getSpeechTime(Text))
    --self:SetGender()
    --self:SetCulture()
    
    -- Debug info:
    self:T(string.format("New SOUNDTEXT: text=%s, duration=%.1f sec", self.text, self.duration))

    return self
  end
  
  --- Set text.
  -- @param #SOUNDTEXT self
  -- @param #string Text Text to speak. Default "Hello World!".
  -- @return #SOUNDTEXT self
  function SOUNDTEXT:SetText(Text)
    
    self.text=Text or "Hello World!"
                  
    return self
  end
  
  --- Set duration, how long it takes to speak the text.
  -- @param #SOUNDTEXT self
  -- @param #number Duration Duration in seconds. Default 3 seconds.
  -- @return #SOUNDTEXT self
  function SOUNDTEXT:SetDuration(Duration)
    
    self.duration=Duration or 3
                  
    return self
  end    
  
  --- Set gender.
  -- @param #SOUNDTEXT self
  -- @param #string Gender Gender: "male" or "female" (default).
  -- @return #SOUNDTEXT self
  function SOUNDTEXT:SetGender(Gender)
    
    self.gender=Gender or "female"
                  
    return self
  end
  
  --- Set TTS culture - local for the voice.
  -- @param #SOUNDTEXT self
  -- @param #string Culture TTS culture. Default "en-GB".
  -- @return #SOUNDTEXT self
  function SOUNDTEXT:SetCulture(Culture)
    
    self.culture=Culture or "en-GB"
                  
    return self
  end    
  
  --- Set to use a specific voice name.
  -- See the list from `DCS-SR-ExternalAudio.exe --help` or if using google see [google voices](https://cloud.google.com/text-to-speech/docs/voices).
  -- @param #SOUNDTEXT self
  -- @param #string VoiceName Voice name. Note that this will overrule `Gender` and `Culture`.
  -- @return #SOUNDTEXT self
  function SOUNDTEXT:SetVoice(VoiceName)
    
    self.voice=VoiceName
                  
    return self
  end
  
end--- **Sound** - Radio transmissions.
--
-- ===
--
-- ## Features:
--
--   * Provide radio functionality to broadcast radio transmissions.
--
-- What are radio communications in DCS?
--
--   * Radio transmissions consist of **sound files** that are broadcasted on a specific **frequency** (e.g. 115MHz) and **modulation** (e.g. AM),
--   * They can be **subtitled** for a specific **duration**, the **power** in Watts of the transmitter's antenna can be set, and the transmission can be **looped**.
--
-- How to supply DCS my own Sound Files?
--
--   * Your sound files need to be encoded in **.ogg** or .wav,
--   * Your sound files should be **as tiny as possible**. It is suggested you encode in .ogg with low bitrate and sampling settings,
--   * They need to be added in .\l10n\DEFAULT\ in you .miz file (which can be decompressed like a .zip file),
--   * For simplicity sake, you can **let DCS' Mission Editor add the file** itself, by creating a new Trigger with the action "Sound to Country", and choosing your sound file and a country you don't use in your mission.
--
-- Due to weird DCS quirks, **radio communications behave differently** if sent by a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or by any other @{Wrapper.Positionable#POSITIONABLE}
--
--   * If the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, DCS will set the power of the transmission automatically,
--   * If the transmitter is any other @{Wrapper.Positionable#POSITIONABLE}, the transmisison can't be subtitled or looped.
--
-- Note that obviously, the **frequency** and the **modulation** of the transmission are important only if the players are piloting an **Advanced System Modelling** enabled aircraft,
-- like the A10C or the Mirage 2000C. They will **hear the transmission** if they are tuned on the **right frequency and modulation** (and if they are close enough - more on that below).
-- If an FC3 aircraft is used, it will **hear every communication, whatever the frequency and the modulation** is set to. The same is true for TACAN beacons. If your aircraft isn't compatible,
-- you won't hear/be able to use the TACAN beacon information.
--
-- ===
--
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/Sound/Radio)
--
-- ===
--
-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky
--
-- @module Sound.Radio
-- @image Core_Radio.JPG


--- *It's not true I had nothing on, I had the radio on.* -- Marilyn Monroe
--
-- # RADIO usage
--
-- There are 3 steps to a successful radio transmission.
--
--   * First, you need to **"add a @{#RADIO} object** to your @{Wrapper.Positionable#POSITIONABLE}. This is done using the @{Wrapper.Positionable#POSITIONABLE.GetRadio}() function,
--   * Then, you will **set the relevant parameters** to the transmission (see below),
--   * When done, you can actually **broadcast the transmission** (i.e. play the sound) with the @{#RADIO.Broadcast}() function.
--
-- Methods to set relevant parameters for both a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP} or any other @{Wrapper.Positionable#POSITIONABLE}
--
--   * @{#RADIO.SetFileName}() : Sets the file name of your sound file (e.g. "Noise.ogg"),
--   * @{#RADIO.SetFrequency}() : Sets the frequency of your transmission.
--   * @{#RADIO.SetModulation}() : Sets the modulation of your transmission.
--   * @{#RADIO.SetLoop}() : Choose if you want the transmission to be looped. If you need your transmission to be looped, you might need a @{#BEACON} instead...
--
-- Additional Methods to set relevant parameters if the transmitter is a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}
--
--   * @{#RADIO.SetSubtitle}() : Set both the subtitle and its duration,
--   * @{#RADIO.NewUnitTransmission}() : Shortcut to set all the relevant parameters in one method call
--
-- Additional Methods to set relevant parameters if the transmitter is any other @{Wrapper.Positionable#POSITIONABLE}
--
--   * @{#RADIO.SetPower}() : Sets the power of the antenna in Watts
--   * @{#RADIO.NewGenericTransmission}() : Shortcut to set all the relevant parameters in one method call
--
-- What is this power thing?
--
--   * If your transmission is sent by a @{Wrapper.Positionable#POSITIONABLE} other than a @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}, you can set the power of the antenna,
--   * Otherwise, DCS sets it automatically, depending on what's available on your Unit,
--   * If the player gets **too far** from the transmitter, or if the antenna is **too weak**, the transmission will **fade** and **become noisyer**,
--   * This an automated DCS calculation you have no say on,
--   * For reference, a standard VOR station has a 100 W antenna, a standard AA TACAN has a 120 W antenna, and civilian ATC's antenna usually range between 300 and 500 W,
--   * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission.
--
-- @type RADIO
-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will transmit the radio calls.
-- @field #string FileName Name of the sound file played.
-- @field #number Frequency Frequency of the transmission in Hz.
-- @field #number Modulation Modulation of the transmission (either radio.modulation.AM or radio.modulation.FM).
-- @field #string Subtitle Subtitle of the transmission.
-- @field #number SubtitleDuration Duration of the Subtitle in seconds.
-- @field #number Power Power of the antenna is Watts.
-- @field #boolean Loop Transmission is repeated (default true).
-- @field #string alias Name of the radio transmitter.
-- @extends Core.Base#BASE
RADIO = {
  ClassName        = "RADIO",
  FileName         = "",
  Frequency        = 0,
  Modulation       = radio.modulation.AM,
  Subtitle         = "",
  SubtitleDuration = 0,
  Power            = 100,
  Loop             = false,
  alias            = nil,
  moduhasbeenset   = false,
}

--- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast.
-- If you want to create a RADIO, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetRadio}() instead.
-- @param #RADIO self
-- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Wrapper.Positionable#POSITIONABLE} that will receive radio capabilities.
-- @return #RADIO The RADIO object or #nil if Positionable is invalid.
function RADIO:New(Positionable)

  -- Inherit base
  local self = BASE:Inherit( self, BASE:New() ) -- Sound.Radio#RADIO
  self:F(Positionable)

  if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid
    self.Positionable = Positionable
    return self
  end

  self:E({error="The passed positionable is invalid, no RADIO created!", positionable=Positionable})
  return nil
end

--- Set alias of the transmitter.
-- @param #RADIO self
-- @param #string alias Name of the radio transmitter.
-- @return #RADIO self
function RADIO:SetAlias(alias)
  self.alias=tostring(alias)
  return self
end

--- Get alias of the transmitter.
-- @param #RADIO self
-- @return #string Name of the transmitter.
function RADIO:GetAlias()
  return tostring(self.alias)
end

--- Set the file name for the radio transmission.
-- @param #RADIO self
-- @param #string FileName File name of the sound file (i.e. "Noise.ogg")
-- @return #RADIO self
function RADIO:SetFileName(FileName)
  self:F2(FileName)

  if type(FileName) == "string" then

    if FileName:find(".ogg") or FileName:find(".wav") then
      if not FileName:find("l10n/DEFAULT/") then
        FileName = "l10n/DEFAULT/" .. FileName
      end

      self.FileName = FileName
      return self
    end
  end

  self:E({"File name invalid. Maybe something wrong with the extension?", FileName})
  return self
end

--- Set the frequency for the radio transmission.
-- If the transmitting positionable is a unit or group, this also set the command "SetFrequency" with the defined frequency and modulation.
-- @param #RADIO self
-- @param #number Frequency Frequency in MHz.
-- @return #RADIO self
function RADIO:SetFrequency(Frequency)
  self:F2(Frequency)

  if type(Frequency) == "number" then
    
    -- If frequency is in range
    --if (Frequency >= 30 and Frequency <= 87.995) or (Frequency >= 108 and Frequency <= 173.995) or (Frequency >= 225 and Frequency <= 399.975) then

      -- Convert frequency from MHz to Hz
      self.Frequency = Frequency
      self.HertzFrequency = Frequency * 1000000

      -- If the RADIO is attached to a UNIT or a GROUP, we need to send the DCS Command "SetFrequency" to change the UNIT or GROUP frequency
      if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then

        local commandSetFrequency={
          id = "SetFrequency",
          params = {
            frequency  = self.HertzFrequency,
            modulation = self.Modulation,
          }
        }

        self:T2(commandSetFrequency)
        self.Positionable:SetCommand(commandSetFrequency)
      end

      return self
    --end
  end

  self:E({"Frequency is not a number. Frequency unchanged.", Frequency})
  return self
end

--- Set AM or FM modulation of the radio transmitter. Set this before you set a frequency!
-- @param #RADIO self
-- @param #number Modulation Modulation is either radio.modulation.AM or radio.modulation.FM.
-- @return #RADIO self
function RADIO:SetModulation(Modulation)
  self:F2(Modulation)
  if type(Modulation) == "number" then
    if Modulation == radio.modulation.AM or Modulation == radio.modulation.FM then --TODO Maybe make this future proof if ED decides to add an other modulation ?
      self.Modulation = Modulation
      if self.moduhasbeenset == false and Modulation == radio.modulation.FM then -- override default
        self:SetFrequency(self.Frequency)
      end
      self.moduhasbeenset = true
      return self
    end
  end
  self:E({"Modulation is invalid. Use DCS's enum radio.modulation. Modulation unchanged.", self.Modulation})
  return self
end

--- Check validity of the power passed and sets RADIO.Power
-- @param #RADIO self
-- @param #number Power Power in W.
-- @return #RADIO self
function RADIO:SetPower(Power)
  self:F2(Power)

  if type(Power) == "number" then
    self.Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that
  else
    self:E({"Power is invalid. Power unchanged.", self.Power})
  end

  return self
end

--- Set message looping on or off.
-- @param #RADIO self
-- @param #boolean Loop If true, message is repeated indefinitely.
-- @return #RADIO self
function RADIO:SetLoop(Loop)
  self:F2(Loop)
  if type(Loop) == "boolean" then
    self.Loop = Loop
    return self
  end
  self:E({"Loop is invalid. Loop unchanged.", self.Loop})
  return self
end

--- Check validity of the subtitle and the subtitleDuration  passed and sets RADIO.subtitle and RADIO.subtitleDuration
-- Both parameters are mandatory, since it wouldn't make much sense to change the Subtitle and not its duration
-- @param #RADIO self
-- @param #string Subtitle
-- @param #number SubtitleDuration in s
-- @return #RADIO self
-- @usage
-- -- create the broadcaster and attaches it a RADIO
-- local MyUnit = UNIT:FindByName("MyUnit")
-- local MyUnitRadio = MyUnit:GetRadio()
--
-- -- add a subtitle for the next transmission, which will be up for 10s
-- MyUnitRadio:SetSubtitle("My Subtitle, 10)
function RADIO:SetSubtitle(Subtitle, SubtitleDuration)
  self:F2({Subtitle, SubtitleDuration})
  if type(Subtitle) == "string" then
    self.Subtitle = Subtitle
  else
    self.Subtitle = ""
    self:E({"Subtitle is invalid. Subtitle reset.", self.Subtitle})
  end
  if type(SubtitleDuration) == "number" then
    self.SubtitleDuration = SubtitleDuration
  else
    self.SubtitleDuration = 0
    self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration})
  end
  return self
end

--- Create a new transmission, that is to say, populate the RADIO with relevant data
-- In this function the data is especially relevant if the broadcaster is anything but a UNIT or a GROUP,
-- but it will work with a UNIT or a GROUP anyway.
-- Only the #RADIO and the Filename are mandatory
-- @param #RADIO self
-- @param #string FileName Name of the sound file that will be transmitted.
-- @param #number Frequency Frequency in MHz.
-- @param #number Modulation Modulation of frequency, which is either radio.modulation.AM or radio.modulation.FM.
-- @param #number Power Power in W.
-- @return #RADIO self
function RADIO:NewGenericTransmission(FileName, Frequency, Modulation, Power, Loop)
  self:F({FileName, Frequency, Modulation, Power})

  self:SetFileName(FileName)
  if Frequency then self:SetFrequency(Frequency) end
  if Modulation then self:SetModulation(Modulation) end
  if Power then self:SetPower(Power) end
  if Loop then self:SetLoop(Loop) end

  return self
end


--- Create a new transmission, that is to say, populate the RADIO with relevant data
-- In this function the data is especially relevant if the broadcaster is a UNIT or a GROUP,
-- but it will work for any @{Wrapper.Positionable#POSITIONABLE}.
-- Only the RADIO and the Filename are mandatory.
-- @param #RADIO self
-- @param #string FileName Name of sound file.
-- @param #string Subtitle Subtitle to be displayed with sound file.
-- @param #number SubtitleDuration Duration of subtitle display in seconds.
-- @param #number Frequency Frequency in MHz.
-- @param #number Modulation Modulation which can be either radio.modulation.AM or radio.modulation.FM
-- @param #boolean Loop If true, loop message.
-- @return #RADIO self
function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop)
  self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop})

  -- Set file name.
  self:SetFileName(FileName)

  -- Set modulation AM/FM.
  if Modulation then
    self:SetModulation(Modulation)
  end

  -- Set frequency.
  if Frequency then
    self:SetFrequency(Frequency)
  end

  -- Set subtitle.
  if Subtitle then
    self:SetSubtitle(Subtitle, SubtitleDuration or 0)
  end

  -- Set Looping.
  if Loop then
    self:SetLoop(Loop)
  end

  return self
end

--- Broadcast the transmission.
-- * The Radio has to be populated with the new transmission before broadcasting.
-- * Please use RADIO setters or either @{#RADIO.NewGenericTransmission} or @{#RADIO.NewUnitTransmission}
-- * This class is in fact pretty smart, it determines the right DCS function to use depending on the type of POSITIONABLE
-- * If the POSITIONABLE is not a UNIT or a GROUP, we use the generic (but limited) trigger.action.radioTransmission()
-- * If the POSITIONABLE is a UNIT or a GROUP, we use the "TransmitMessage" Command
-- * If your POSITIONABLE is a UNIT or a GROUP, the Power is ignored.
-- * If your POSITIONABLE is not a UNIT or a GROUP, the Subtitle, SubtitleDuration are ignored
-- @param #RADIO self
-- @param #boolean viatrigger Use trigger.action.radioTransmission() in any case, i.e. also for UNITS and GROUPS.
-- @return #RADIO self
function RADIO:Broadcast(viatrigger)
  self:F({viatrigger=viatrigger})

  -- If the POSITIONABLE is actually a UNIT or a GROUP, use the more complicated DCS command system.
  if (self.Positionable.ClassName=="UNIT" or self.Positionable.ClassName=="GROUP") and (not viatrigger) then
    self:T("Broadcasting from a UNIT or a GROUP")

    local commandTransmitMessage={
      id = "TransmitMessage",
      params = {
        file = self.FileName,
        duration = self.SubtitleDuration,
        subtitle = self.Subtitle,
        loop = self.Loop,
      }}

    self:T3(commandTransmitMessage)
    self.Positionable:SetCommand(commandTransmitMessage)
  else
    -- If the POSITIONABLE is anything else, we revert to the general singleton function
    -- I need to give it a unique name, so that the transmission can be stopped later. I use the class ID
    self:T("Broadcasting from a POSITIONABLE")
    trigger.action.radioTransmission(self.FileName, self.Positionable:GetPositionVec3(), self.Modulation, self.Loop, self.Frequency, self.Power, tostring(self.ID))
  end

  return self
end



--- Stops a transmission
-- This function is especially useful to stop the broadcast of looped transmissions
-- @param #RADIO self
-- @return #RADIO self
function RADIO:StopBroadcast()
  self:F()
  -- If the POSITIONABLE is a UNIT or a GROUP, stop the transmission with the DCS "StopTransmission" command
  if self.Positionable.ClassName == "UNIT" or self.Positionable.ClassName == "GROUP" then

    local commandStopTransmission={id="StopTransmission", params={}}

    self.Positionable:SetCommand(commandStopTransmission)
  else
    -- Else, we use the appropriate singleton funciton
    trigger.action.stopRadioTransmission(tostring(self.ID))
  end
  return self
end
--- **Sound** - Queues Radio Transmissions.
-- 
-- ===
-- 
-- ## Features:
-- 
--   * Manage Radio Transmissions
--
-- ===
--
-- ### Authors: funkyfranky
--
-- @module Sound.RadioQueue
-- @image Core_Radio.JPG

--- Manages radio transmissions.
-- 
-- The main goal of the RADIOQUEUE class is to string together multiple sound files to play a complete sentence.
-- The underlying problem is that radio transmissions in DCS are not queued but played "on top" of each other.
-- Therefore, to achive the goal, it is vital to know the precise duration how long it takes to play the sound file.
-- 
-- @type RADIOQUEUE
-- @field #string ClassName Name of the class "RADIOQUEUE".
-- @field #boolean Debugmode Debug mode. More info.
-- @field #string lid ID for dcs.log.
-- @field #number frequency The radio frequency in Hz.
-- @field #number modulation The radio modulation. Either radio.modulation.AM or radio.modulation.FM.
-- @field Core.Scheduler#SCHEDULER scheduler The scheduler.
-- @field #string RQid The radio queue scheduler ID.
-- @field #table queue The queue of transmissions.
-- @field #string alias Name of the radio.
-- @field #number dt Time interval in seconds for checking the radio queue.
-- @field #number delay Time delay before starting the radio queue. 
-- @field #number Tlast Time (abs) when the last transmission finished.
-- @field Core.Point#COORDINATE sendercoord Coordinate from where transmissions are broadcasted.
-- @field #number sendername Name of the sending unit or static.
-- @field #boolean senderinit Set frequency was initialized.
-- @field #number power Power of radio station in Watts. Default 100 W.
-- @field #table numbers Table of number transmission parameters.
-- @field #boolean checking Scheduler is checking the radio queue. 
-- @field #boolean schedonce Call ScheduleOnce instead of normal scheduler.
-- @field Sound.SRS#MSRS msrs Moose SRS class.
-- @extends Core.Base#BASE
RADIOQUEUE = {
  ClassName   = "RADIOQUEUE",
  Debugmode   = nil,
  lid         = nil,
  frequency   = nil,
  modulation  = nil,
  scheduler   = nil,
  RQid        = nil,
  queue       =  {},
  alias       = nil,
  dt          = nil,
  delay       = nil,
  Tlast       = nil,
  sendercoord = nil,
  sendername  = nil,
  senderinit  = nil,
  power       = nil,
  numbers     =  {},
  checking    = nil,
  schedonce   = false,
}

--- Radio queue transmission data.
-- @type RADIOQUEUE.Transmission
-- @field #string filename Name of the file to be transmitted.
-- @field #string path Path in miz file where the file is located.
-- @field #number duration Duration in seconds.
-- @field #string subtitle Subtitle of the transmission.
-- @field #number subduration Duration of the subtitle being displayed.
-- @field #number Tstarted Mission time (abs) in seconds when the transmission started.
-- @field #boolean isplaying If true, transmission is currently playing.
-- @field #number Tplay Mission time (abs) in seconds when the transmission should be played.
-- @field #number interval Interval in seconds before next transmission.
-- @field Sound.SoundOutput#SOUNDFILE soundfile Sound file object to play via SRS.
-- @field Sound.SoundOutput#SOUNDTEXT soundtext Sound TTS object to play via SRS.


--- Create a new RADIOQUEUE object for a given radio frequency/modulation.
-- @param #RADIOQUEUE self
-- @param #number frequency The radio frequency in MHz.
-- @param #number modulation (Optional) The radio modulation. Default `radio.modulation.AM` (=0).
-- @param #string alias (Optional) Name of the radio queue.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:New(frequency, modulation, alias)

  -- Inherit base
  local self=BASE:Inherit(self, BASE:New()) -- #RADIOQUEUE
  
  self.alias=alias or "My Radio"
  
  self.lid=string.format("RADIOQUEUE %s | ", self.alias)
  
  if frequency==nil then
    self:E(self.lid.."ERROR: No frequency specified as first parameter!")
    return nil
  end
  
  -- Frequency in Hz.
  self.frequency=frequency*1000000
  
  -- Modulation.
  self.modulation=modulation or radio.modulation.AM
  
  -- Set radio power.
  self:SetRadioPower()
  
  -- Scheduler.
  self.scheduler=SCHEDULER:New()
  self.scheduler:NoTrace()
  
  return self
end

--- Start the radio queue.
-- @param #RADIOQUEUE self
-- @param #number delay (Optional) Delay in seconds, before the radio queue is started. Default 1 sec.
-- @param #number dt (Optional) Time step in seconds for checking the queue. Default 0.01 sec.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:Start(delay, dt)
  
  -- Delay before start.
  self.delay=delay or 1
  
  -- Time interval for queue check.
  self.dt=dt or 0.01
  
  -- Debug message.
  self:I(self.lid..string.format("Starting RADIOQUEUE %s on Frequency %.2f MHz [modulation=%d] in %.1f seconds (dt=%.3f sec)", self.alias, self.frequency/1000000, self.modulation, self.delay, self.dt))

  -- Start Scheduler.
  if self.schedonce then
    self:_CheckRadioQueueDelayed(self.delay)
  else
    self.RQid=self.scheduler:Schedule(nil, RADIOQUEUE._CheckRadioQueue, {self}, self.delay, self.dt)
  end
  
  return self
end

--- Stop the radio queue. Stop scheduler and delete queue.
-- @param #RADIOQUEUE self
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:Stop()
  self:I(self.lid.."Stopping RADIOQUEUE.")
  self.scheduler:Stop(self.RQid)
  self.queue={}
  return self
end

--- Set coordinate from where the transmission is broadcasted.
-- @param #RADIOQUEUE self
-- @param Core.Point#COORDINATE coordinate Coordinate of the sender.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:SetSenderCoordinate(coordinate)
  self.sendercoord=coordinate
  return self
end

--- Set name of unit or static from which transmissions are made.
-- @param #RADIOQUEUE self
-- @param #string name Name of the unit or static used for transmissions.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:SetSenderUnitName(name)
  self.sendername=name
  return self
end

--- Set radio power. Note that this only applies if no relay unit is used.
-- @param #RADIOQUEUE self
-- @param #number power Radio power in Watts. Default 100 W.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:SetRadioPower(power)
  self.power=power or 100
  return self
end

--- Set SRS.
-- @param #RADIOQUEUE self
-- @param #string PathToSRS Path to SRS.
-- @param #number Port SRS port. Default 5002.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:SetSRS(PathToSRS, Port)
  local path = PathToSRS or MSRS.path
  local port = Port or MSRS.port
  self.msrs=MSRS:New(path, self.frequency/1000000, self.modulation)
  self.msrs:SetPort(port)
  return self
end

--- Set parameters of a digit.
-- @param #RADIOQUEUE self
-- @param #number digit The digit 0-9.
-- @param #string filename The name of the sound file.
-- @param #number duration The duration of the sound file in seconds.
-- @param #string path The directory within the miz file where the sound is located. Default "l10n/DEFAULT/".
-- @param #string subtitle Subtitle of the transmission.
-- @param #number subduration Duration [sec] of the subtitle being displayed. Default 5 sec.
-- @return #RADIOQUEUE self The RADIOQUEUE object.
function RADIOQUEUE:SetDigit(digit, filename, duration, path, subtitle, subduration)

  local transmission={} --#RADIOQUEUE.Transmission
  transmission.filename=filename
  transmission.duration=duration
  transmission.path=path or "l10n/DEFAULT/"
  transmission.subtitle=nil
  transmission.subduration=nil
  
  -- Convert digit to string in case it is given as a number.
  if type(digit)=="number" then
    digit=tostring(digit)
  end

  -- Set transmission.
  self.numbers[digit]=transmission
  
  return self
end

--- Add a transmission to the radio queue.
-- @param #RADIOQUEUE self
-- @param #RADIOQUEUE.Transmission transmission The transmission data table. 
-- @return #RADIOQUEUE self
function RADIOQUEUE:AddTransmission(transmission)
  self:F({transmission=transmission})
  
  -- Init.
  transmission.isplaying=false
  transmission.Tstarted=nil

  -- Add to queue.
  table.insert(self.queue, transmission)
  
  -- Start checking.
  if self.schedonce and not self.checking then
    self:_CheckRadioQueueDelayed()
  end

  return self
end

--- Create a new transmission and add it to the radio queue.
-- @param #RADIOQUEUE self
-- @param #string filename Name of the sound file. Usually an ogg or wav file type.
-- @param #number duration Duration in seconds the file lasts.
-- @param #number path Directory path inside the miz file where the sound file is located. Default "l10n/DEFAULT/".
-- @param #number tstart Start time (abs) seconds. Default now.
-- @param #number interval Interval in seconds after the last transmission finished.
-- @param #string subtitle Subtitle of the transmission.
-- @param #number subduration Duration [sec] of the subtitle being displayed. Default 5 sec.
-- @return #RADIOQUEUE.Transmission Radio transmission table.
function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval, subtitle, subduration)

  -- Sanity checks.
  if not filename then
    self:E(self.lid.."ERROR: No filename specified.")
    return nil
  end
  if type(filename)~="string" then
    self:E(self.lid.."ERROR: Filename specified is NOT a string.")
    return nil    
  end

  if not duration then
    self:E(self.lid.."ERROR: No duration of transmission specified.")
    return nil
  end
  if type(duration)~="number" then
    self:E(self.lid..string.format("ERROR: Duration specified is NOT a number but type=%s. Filename=%s, duration=%s", type(duration), tostring(filename), tostring(duration)))
    return nil    
  end
  

  local transmission={} --#RADIOQUEUE.Transmission
  transmission.filename=filename
  transmission.duration=duration
  transmission.path=path or "l10n/DEFAULT/"
  transmission.Tplay=tstart or timer.getAbsTime()
  transmission.subtitle=subtitle
  transmission.interval=interval or 0
  if transmission.subtitle then
    transmission.subduration=subduration or 5
  else
    transmission.subduration=nil
  end
  
  -- Add transmission to queue.  
  self:AddTransmission(transmission)
  
  return transmission
end

--- Add a SOUNDFILE to the radio queue.
-- @param #RADIOQUEUE self
-- @param Sound.SoundOutput#SOUNDFILE soundfile Sound file object to be added.
-- @param #number tstart Start time (abs) seconds. Default now.
-- @param #number interval Interval in seconds after the last transmission finished.
-- @return #RADIOQUEUE self
function RADIOQUEUE:AddSoundFile(soundfile, tstart, interval)
  --env.info(string.format("FF add soundfile: name=%s%s", soundfile:GetPath(), soundfile:GetFileName()))
  local transmission=self:NewTransmission(soundfile:GetFileName(), soundfile.duration, soundfile:GetPath(), tstart, interval, soundfile.subtitle, soundfile.subduration)
  transmission.soundfile=soundfile
  return self
end

--- Add a SOUNDTEXT to the radio queue.
-- @param #RADIOQUEUE self
-- @param Sound.SoundOutput#SOUNDTEXT soundtext Text-to-speech text.
-- @param #number tstart Start time (abs) seconds. Default now.
-- @param #number interval Interval in seconds after the last transmission finished.
-- @return #RADIOQUEUE self
function RADIOQUEUE:AddSoundText(soundtext, tstart, interval)

  local transmission=self:NewTransmission("SoundText.ogg", soundtext.duration, nil, tstart, interval, soundtext.subtitle, soundtext.subduration)
  transmission.soundtext=soundtext
  return self
end


--- Convert a number (as string) into a radio transmission.
-- E.g. for board number or headings.
-- @param #RADIOQUEUE self
-- @param #string number Number string, e.g. "032" or "183".
-- @param #number delay Delay before transmission in seconds.
-- @param #number interval Interval between the next call.
-- @return #number Duration of the call in seconds.
function RADIOQUEUE:Number2Transmission(number, delay, interval)
  
  -- Split string into characters.
  local numbers=UTILS.GetCharacters(number)

  local wait=0    
  for i=1,#numbers do
  
    -- Current number
    local n=numbers[i]
        
    -- Radio call.
    local transmission=UTILS.DeepCopy(self.numbers[n]) --#RADIOQUEUE.Transmission
    
    transmission.Tplay=timer.getAbsTime()+(delay or 0)
    
    if interval and i==1 then
      transmission.interval=interval
    end
    
    self:AddTransmission(transmission)
    
    -- Add up duration of the number.
    wait=wait+transmission.duration
  end
  
  -- Return the total duration of the call.
  return wait
end


--- Broadcast radio message.
-- @param #RADIOQUEUE self
-- @param #RADIOQUEUE.Transmission transmission The transmission.
function RADIOQUEUE:Broadcast(transmission)
  self:T("Broadcast")

  if ((transmission.soundfile and transmission.soundfile.useSRS) or transmission.soundtext) and self.msrs then
    self:_BroadcastSRS(transmission)
    return
  end

  -- Get unit sending the transmission.
  local sender=self:_GetRadioSender()
  
  -- Construct file name.
  local filename=string.format("%s%s", transmission.path, transmission.filename)
  
  if sender then
    
    -- Broadcasting from aircraft. Only players tuned in to the right frequency will see the message.
    self:T(self.lid..string.format("Broadcasting from aircraft %s", sender:GetName()))
    
    
    --if not self.senderinit then
    -- TODO Seems to be a DCS bug - if I explode ANY unit in a group the BC assignment gets lost
    
      -- Command to set the Frequency for the transmission.
      local commandFrequency={
        id="SetFrequency",
        params={
          frequency=self.frequency,  -- Frequency in Hz.
          modulation=self.modulation,
        }}
          
      -- Set commend for frequency
      sender:SetCommand(commandFrequency)
      
      self.senderinit=true
    --end
    
    -- Set subtitle only if duration>0 sec.
    local subtitle=nil
    local duration=nil
    if transmission.subtitle and transmission.subduration and transmission.subduration>0 then
      subtitle=transmission.subtitle
      duration=transmission.subduration
    end
    
    -- Command to tranmit the call.
    local commandTransmit={
      id = "TransmitMessage",
      params = {
        file=filename,
        duration=duration,
        subtitle=subtitle,
        loop=false,
      }}    
    
    -- Set command for radio transmission. 
    sender:SetCommand(commandTransmit)
    
    -- Debug message.
    if self.Debugmode then
      local text=string.format("file=%s, freq=%.2f MHz, duration=%.2f sec, subtitle=%s", filename, self.frequency/1000000, transmission.duration, transmission.subtitle or "")
      MESSAGE:New(text, 2, "RADIOQUEUE "..self.alias):ToAll()
    end
      
  else
    
    -- Broadcasting from carrier. No subtitle possible. Need to send messages to players.
    self:T(self.lid..string.format("Broadcasting via trigger.action.radioTransmission()"))
  
    -- Position from where to transmit.
    local vec3=nil
    
    -- Try to get positon from sender unit/static.
    if self.sendername then
      vec3=self:_GetRadioSenderCoord()
    end
    
    -- Try to get fixed positon.
    if self.sendercoord and not vec3 then
      vec3=self.sendercoord:GetVec3()
    end
    
    -- Transmit via trigger.
    if vec3 then
      self:T("Sending")
      self:T( { filename = filename, vec3 = vec3, modulation = self.modulation, frequency = self.frequency, power = self.power } )
      
      -- Trigger transmission.
      trigger.action.radioTransmission(filename, vec3, self.modulation, false, self.frequency, self.power)
      
      -- Debug message.
      if self.Debugmode then
        local text=string.format("file=%s, freq=%.2f MHz, duration=%.2f sec, subtitle=%s", filename, self.frequency/1000000, transmission.duration, transmission.subtitle or "")
        MESSAGE:New(string.format(text, filename, transmission.duration, transmission.subtitle or ""), 5, "RADIOQUEUE "..self.alias):ToAll()
      end
    else
      self:E("ERROR: Could not get vec3 to determine transmission origin! Did you specify a sender and is it still alive?")
    end

  end
end

--- Broadcast radio message.
-- @param #RADIOQUEUE self
-- @param #RADIOQUEUE.Transmission transmission The transmission.
function RADIOQUEUE:_BroadcastSRS(transmission)

  if transmission.soundfile and transmission.soundfile.useSRS then
    self.msrs:PlaySoundFile(transmission.soundfile)
  elseif transmission.soundtext then
    self.msrs:PlaySoundText(transmission.soundtext)
  end

end

--- Start checking the radio queue.
-- @param #RADIOQUEUE self
-- @param #number delay Delay in seconds before checking.
function RADIOQUEUE:_CheckRadioQueueDelayed(delay)
  self.checking=true
  self:ScheduleOnce(delay or self.dt, RADIOQUEUE._CheckRadioQueue, self)
end

--- Check radio queue for transmissions to be broadcasted.
-- @param #RADIOQUEUE self
function RADIOQUEUE:_CheckRadioQueue()

  -- Check if queue is empty.
  if #self.queue==0 then
    -- Queue is now empty. Nothing to else to do.
    self.checking=false
    return
  end

  -- Get current abs time.
  local time=timer.getAbsTime()
  
  local playing=false
  local next=nil  --#RADIOQUEUE.Transmission
  local remove=nil
  for i,_transmission in ipairs(self.queue) do
    local transmission=_transmission  --#RADIOQUEUE.Transmission
    
    -- Check if transmission time has passed.
    if time>=transmission.Tplay then 
      
      -- Check if transmission is currently playing.
      if transmission.isplaying then
      
        -- Check if transmission is finished.
        if time>=transmission.Tstarted+transmission.duration then
          
          -- Transmission over.
          transmission.isplaying=false
          
          -- Remove ith element in queue.
          remove=i
          
          -- Store time last transmission finished.
          self.Tlast=time
                    
        else -- still playing
        
          -- Transmission is still playing.
          playing=true
          
        end
      
      else -- not playing yet
      
        local Tlast=self.Tlast
      
        if transmission.interval==nil  then
      
          -- Not playing ==> this will be next.
          if next==nil then
            next=transmission
          end
          
        else
        
          if Tlast==nil or time-Tlast>=transmission.interval then
            next=transmission            
          else
            
          end
        end
        
        -- We got a transmission or one with an interval that is not due yet. No need for anything else.
        if next or Tlast then
          break
        end
             
      end
      
    else
      
        -- Transmission not due yet.
      
    end  
  end
  
  -- Found a new transmission.
  if next~=nil and not playing then
    self:Broadcast(next)
    next.isplaying=true
    next.Tstarted=time
  end
  
  -- Remove completed calls from queue.
  if remove then
    table.remove(self.queue, remove)
  end
  
  -- Check queue.
  if self.schedonce then
    self:_CheckRadioQueueDelayed()
  end
  
end

--- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work.
-- @param #RADIOQUEUE self
-- @return Wrapper.Unit#UNIT Sending unit or nil if was not setup, is not an aircraft or ground unit or is not alive.
function RADIOQUEUE:_GetRadioSender()

  -- Check if we have a sending aircraft.
  local sender=nil  --Wrapper.Unit#UNIT

  -- Try the general default.
  if self.sendername then
  
    -- First try to find a unit 
    sender=UNIT:FindByName(self.sendername)

    -- Check that sender is alive and an aircraft.
    if sender and sender:IsAlive() and (sender:IsAir() or sender:IsGround()) then
      return sender
    end
    
  end
    
  return nil
end

--- Get unit from which we want to transmit a radio message. This has to be an aircraft or ground unit for subtitles to work.
-- @param #RADIOQUEUE self
-- @return DCS#Vec3 Vector 3D.
function RADIOQUEUE:_GetRadioSenderCoord()

  local vec3=nil

  -- Try the general default.
  if self.sendername then
  
    -- First try to find a unit 
    local sender=UNIT:FindByName(self.sendername)

    -- Check that sender is alive and an aircraft.
    if sender and sender:IsAlive() then
      return sender:GetVec3()
    end
    
    -- Now try a static. 
    local sender=STATIC:FindByName( self.sendername, false )

    -- Check that sender is alive and an aircraft.
    if sender then
      return sender:GetVec3()
    end
    
  end
    
  return nil
end
--- **Core** - Makes the radio talk.
--
-- ===
--
-- ## Features:
--
--   * Send text strings using a vocabulary that is converted in spoken language.
--   * Possiblity to implement multiple language.
--
-- ===
--
-- ### Authors: FlightControl
--
-- @module Sound.RadioSpeech
-- @image Core_Radio.JPG

--- Makes the radio speak.
--
-- # RADIOSPEECH usage
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
--
-- @type RADIOSPEECH
-- @extends Sound.RadioQueue#RADIOQUEUE
RADIOSPEECH = {
  ClassName = "RADIOSPEECH",
  Vocabulary = {
    EN = {},
    DE = {},
    RU = {},
  }
}


RADIOSPEECH.Vocabulary.EN = {
  ["1"] = { "1", 0.25 },
  ["2"] = { "2", 0.25 },
  ["3"] = { "3", 0.30 },
  ["4"] = { "4", 0.35 },
  ["5"] = { "5", 0.35 },
  ["6"] = { "6", 0.42 },
  ["7"] = { "7", 0.38 },
  ["8"] = { "8", 0.20 },
  ["9"] = { "9", 0.32 },
  ["10"] = { "10", 0.35 },
  ["11"] = { "11", 0.40 },
  ["12"] = { "12", 0.42 },
  ["13"] = { "13", 0.38 },
  ["14"] = { "14", 0.42 },
  ["15"] = { "15", 0.42 },
  ["16"] = { "16", 0.52 },
  ["17"] = { "17", 0.59 },
  ["18"] = { "18", 0.40 },
  ["19"] = { "19", 0.47 },
  ["20"] = { "20", 0.38 },
  ["30"] = { "30", 0.29 },
  ["40"] = { "40", 0.35 },
  ["50"] = { "50", 0.32 },
  ["60"] = { "60", 0.44 },
  ["70"] = { "70", 0.48 },
  ["80"] = { "80", 0.26 },
  ["90"] = { "90", 0.36 },
  ["100"] = { "100", 0.55 },
  ["200"] = { "200", 0.55 },
  ["300"] = { "300", 0.61 },
  ["400"] = { "400", 0.60 },
  ["500"] = { "500", 0.61 },
  ["600"] = { "600", 0.65 },
  ["700"] = { "700", 0.70 },
  ["800"] = { "800", 0.54 },
  ["900"] = { "900", 0.60 },
  ["1000"] = { "1000", 0.60 },
  ["2000"] = { "2000", 0.61 },
  ["3000"] = { "3000", 0.64 },
  ["4000"] = { "4000", 0.62 },
  ["5000"] = { "5000", 0.69 },
  ["6000"] = { "6000", 0.69 },
  ["7000"] = { "7000", 0.75 },
  ["8000"] = { "8000", 0.59 },
  ["9000"] = { "9000", 0.65 },

  ["chevy"] = { "chevy", 0.35 },
  ["colt"] = { "colt", 0.35 },
  ["springfield"] = { "springfield", 0.65 },
  ["dodge"] = { "dodge", 0.35 },
  ["enfield"] = { "enfield", 0.5 },
  ["ford"] = { "ford", 0.32 },
  ["pontiac"] = { "pontiac", 0.55 },
  ["uzi"] = { "uzi", 0.28 },

  ["degrees"] = { "degrees", 0.5 },
  ["kilometers"] = { "kilometers", 0.65 },
  ["km"] = { "kilometers", 0.65 },
  ["miles"] = { "miles", 0.45 },
  ["meters"] = { "meters", 0.41 },
  ["mi"] = { "miles", 0.45 },
  ["feet"] = { "feet", 0.29 },

  ["br"] = { "br", 1.1 },
  ["bra"] = { "bra", 0.3 },


  ["returning to base"] = { "returning_to_base", 0.85 },
  ["on route to ground target"] = { "on_route_to_ground_target", 1.05 },
  ["intercepting bogeys"] = { "intercepting_bogeys", 1.00 },
  ["engaging ground target"] = { "engaging_ground_target", 1.20 },
  ["engaging bogeys"] = { "engaging_bogeys", 0.81 },
  ["wheels up"] = { "wheels_up", 0.42 },
  ["landing at base"] = { "landing at base", 0.8 },
  ["patrolling"] = { "patrolling", 0.55 },

  ["for"] = { "for", 0.31 },
  ["and"] = { "and", 0.31 },
  ["at"] = { "at", 0.3 },
  ["dot"] = { "dot", 0.26 },
  ["defender"] = { "defender", 0.45 },
}

RADIOSPEECH.Vocabulary.RU = {
  ["1"] = { "1", 0.34 },
  ["2"] = { "2", 0.30 },
  ["3"] = { "3", 0.23 },
  ["4"] = { "4", 0.51 },
  ["5"] = { "5", 0.31 },
  ["6"] = { "6", 0.44 },
  ["7"] = { "7", 0.25 },
  ["8"] = { "8", 0.43 },
  ["9"] = { "9", 0.45 },
  ["10"] = { "10", 0.53 },
  ["11"] = { "11", 0.66 },
  ["12"] = { "12", 0.70 },
  ["13"] = { "13", 0.66 },
  ["14"] = { "14", 0.80 },
  ["15"] = { "15", 0.65 },
  ["16"] = { "16", 0.75 },
  ["17"] = { "17", 0.74 },
  ["18"] = { "18", 0.85 },
  ["19"] = { "19", 0.80 },
  ["20"] = { "20", 0.58 },
  ["30"] = { "30", 0.51 },
  ["40"] = { "40", 0.51 },
  ["50"] = { "50", 0.67 },
  ["60"] = { "60", 0.76 },
  ["70"] = { "70", 0.68 },
  ["80"] = { "80", 0.84 },
  ["90"] = { "90", 0.71 },
  ["100"] = { "100", 0.35 },
  ["200"] = { "200", 0.59 },
  ["300"] = { "300", 0.53 },
  ["400"] = { "400", 0.70 },
  ["500"] = { "500", 0.50 },
  ["600"] = { "600", 0.58 },
  ["700"] = { "700", 0.64 },
  ["800"] = { "800", 0.77 },
  ["900"] = { "900", 0.75 },
  ["1000"] = { "1000", 0.87 },
  ["2000"] = { "2000", 0.83 },
  ["3000"] = { "3000", 0.84 },
  ["4000"] = { "4000", 1.00 },
  ["5000"] = { "5000", 0.77 },
  ["6000"] = { "6000", 0.90 },
  ["7000"] = { "7000", 0.77 },
  ["8000"] = { "8000", 0.92 },
  ["9000"] = { "9000", 0.87 },

  ["градусы"] = { "degrees", 0.5 },
  ["километры"] = { "kilometers", 0.65 },
  ["km"] = { "kilometers", 0.65 },
  ["мили"] = { "miles", 0.45 },
  ["mi"] = { "miles", 0.45 },
  ["метров"] = { "meters", 0.41 },
  ["m"] = { "meters", 0.41 },
  ["ноги"] = { "feet", 0.37 },

  ["br"] = { "br", 1.1 },
  ["bra"] = { "bra", 0.3 },


  ["возвращение на базу"] = { "returning_to_base", 1.40 },
  ["на пути к наземной цели"] = { "on_route_to_ground_target", 1.45 },
  ["перехват боги"] = { "intercepting_bogeys", 1.22 },
  ["поражение наземной цели"] = { "engaging_ground_target", 1.53 },
  ["привлечение болотных птиц"] = { "engaging_bogeys", 1.68 },
  ["колёса вверх..."] = { "wheels_up", 0.92 },
  ["посадка на базу"] = { "landing at base", 1.04 },
  ["патрулирование"] = { "patrolling", 0.96 },

  ["для"] = { "for", 0.27 },
  ["и"] = { "and", 0.17 },
  ["на сайте"] = { "at", 0.19 },
  ["точка"] = { "dot", 0.51 },
  ["защитник"] = { "defender", 0.45 },
}

--- Create a new RADIOSPEECH object for a given radio frequency/modulation.
-- @param #RADIOSPEECH self
-- @param #number frequency The radio frequency in MHz.
-- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM.
-- @return #RADIOSPEECH self The RADIOSPEECH object.
function RADIOSPEECH:New(frequency, modulation)

  -- Inherit base
  local self = BASE:Inherit( self, RADIOQUEUE:New( frequency, modulation ) ) -- #RADIOSPEECH

  self.Language = "EN"

  self:BuildTree()

  return self
end

function RADIOSPEECH:SetLanguage( Langauge )

  self.Language = Langauge
end


--- Add Sentence to the Speech collection.
-- @param #RADIOSPEECH self
-- @param #string RemainingSentence The remaining sentence during recursion.
-- @param #table Speech The speech node.
-- @param #string Sentence The full sentence.
-- @param #string Data The speech data.
-- @return #RADIOSPEECH self The RADIOSPEECH object.
function RADIOSPEECH:AddSentenceToSpeech( RemainingSentence, Speech, Sentence, Data )

  self:I( { RemainingSentence, Speech, Sentence, Data } )

  local Token, RemainingSentence = RemainingSentence:match( "^ *([^ ]+)(.*)" )
  self:I( { Token = Token, RemainingSentence = RemainingSentence } )

  -- Is there a Token?
  if Token then

    -- We check if the Token is already in the Speech collection.
    if not Speech[Token] then

      -- There is not yet a vocabulary registered for this.
      Speech[Token] = {}

      if RemainingSentence and RemainingSentence ~= "" then
        -- We use recursion to iterate through the complete Sentence, and make a chain of Tokens.
        -- The last Speech node in the collection contains the Sentence and the Data to be spoken.
        -- This to ensure that during the actual speech:
        -- - Complete sentences are being understood.
        -- - Words without speech are ignored.
        -- - Incorrect sequence of words are ignored.
        Speech[Token].Next = {}
        self:AddSentenceToSpeech( RemainingSentence, Speech[Token].Next, Sentence, Data )
      else
        -- There is no remaining sentence, so we add speech to the Sentence.
        -- The recursion stops here.
        Speech[Token].Sentence = Sentence
        Speech[Token].Data = Data
      end
    end
  end
end

--- Build the tree structure based on the language words, in order to find the correct sentences and to ignore incomprehensible words.
-- @param #RADIOSPEECH self
-- @return #RADIOSPEECH self The RADIOSPEECH object.
function RADIOSPEECH:BuildTree()

  self.Speech = {}

  for Language, Sentences in pairs( self.Vocabulary ) do
    self:I( { Language = Language, Sentences = Sentences })
    self.Speech[Language] = {}
    for Sentence, Data in pairs( Sentences ) do
      self:I( { Sentence = Sentence, Data = Data } )
      self:AddSentenceToSpeech( Sentence, self.Speech[Language], Sentence, Data )
    end
  end

  self:I( { Speech = self.Speech } )

  return self
end

--- Speak a sentence.
-- @param #RADIOSPEECH self
-- @param #string Sentence The sentence to be spoken.
function RADIOSPEECH:SpeakWords( Sentence, Speech, Language )

  local OriginalSentence = Sentence

  -- lua does not parse UTF-8, so the match statement will fail on cyrillic using %a.
  -- therefore, the only way to parse the statement is to use blank, comma or dot as a delimiter.
  -- and then check if the character can be converted to a number or not.
  local Word, RemainderSentence = Sentence:match( "^[., ]*([^ .,]+)(.*)" )

  self:I( { Word = Word, Speech = Speech[Word], RemainderSentence = RemainderSentence } )


  if Word then
    if Word ~= "" and tonumber(Word) == nil then

      -- Construct of words
      Word = Word:lower()
      if Speech[Word] then
        -- The end of the sentence has been reached. Now Speech.Next should be nil, otherwise there is an error.
        if Speech[Word].Next == nil then
          self:I( { Sentence = Speech[Word].Sentence, Data = Speech[Word].Data } )
          self:NewTransmission( Speech[Word].Data[1] .. ".wav", Speech[Word].Data[2], Language .. "/" )
        else
          if RemainderSentence and RemainderSentence ~= "" then
            return self:SpeakWords( RemainderSentence, Speech[Word].Next, Language )
          end
        end
      end
      return RemainderSentence
    end
    return OriginalSentence
  else
    return ""
  end

end

--- Speak a sentence.
-- @param #RADIOSPEECH self
-- @param #string Sentence The sentence to be spoken.
function RADIOSPEECH:SpeakDigits( Sentence, Speech, Langauge )

  local OriginalSentence = Sentence

  -- lua does not parse UTF-8, so the match statement will fail on cyrillic using %a.
  -- therefore, the only way to parse the statement is to use blank, comma or dot as a delimiter.
  -- and then check if the character can be converted to a number or not.
  local Digits, RemainderSentence = Sentence:match( "^[., ]*([^ .,]+)(.*)" )

  self:I( { Digits = Digits, Speech = Speech[Digits], RemainderSentence = RemainderSentence } )

  if Digits then
    if Digits ~= "" and tonumber( Digits ) ~= nil then

      -- Construct numbers
      local Number = tonumber( Digits )
      local Multiple = nil
      while Number >= 0 do
        if Number > 1000 then
          Multiple = math.floor( Number / 1000 ) * 1000
        elseif Number > 100 then
          Multiple = math.floor( Number / 100 ) * 100
        elseif Number > 20 then
          Multiple = math.floor( Number / 10 ) * 10
        elseif Number >= 0 then
          Multiple = Number
        end
        Sentence = tostring( Multiple )
        if Speech[Sentence] then
          self:I( { Speech = Speech[Sentence].Sentence, Data = Speech[Sentence].Data } )
          self:NewTransmission( Speech[Sentence].Data[1] .. ".wav", Speech[Sentence].Data[2], Langauge .. "/" )
        end
        Number = Number - Multiple
        Number = ( Number == 0 ) and -1 or Number
      end
      return RemainderSentence
    end
    return OriginalSentence
  else
    return ""
  end

end



--- Speak a sentence.
-- @param #RADIOSPEECH self
-- @param #string Sentence The sentence to be spoken.
function RADIOSPEECH:Speak( Sentence, Language )

  self:I( { Sentence, Language } )

  local Language = Language or "EN"

  self:I( { Language = Language } )

  -- If there is no node for Speech, then we start at the first nodes of the language.
  local Speech = self.Speech[Language]

  self:I( { Speech = Speech, Language = Language } )

  self:NewTransmission( "_In.wav", 0.52, Language .. "/" )

  repeat

    Sentence = self:SpeakWords( Sentence, Speech, Language )

    self:I( { Sentence = Sentence } )

    Sentence = self:SpeakDigits( Sentence, Speech, Language )

    self:I( { Sentence = Sentence } )

--    Sentence = self:SpeakSymbols( Sentence, Speech )
--
--    self:I( { Sentence = Sentence } )

  until not Sentence or Sentence == ""

  self:NewTransmission( "_Out.wav", 0.28, Language .. "/" )

end
--- **Sound** - Simple Radio Standalone (SRS) Integration and Text-to-Speech.
--
-- ===
--
-- **Main Features:**
--
--    * Incease immersion of your missions with more sound output
--    * Play sound files via SRS
--    * Play text-to-speech via SRS
--
-- ===
--
-- ## Youtube Videos: None yet
--
-- ===
--
-- ## Example Missions: [GitHub](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/Sound/MSRS).
--
-- ===
--
-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases)
--
-- ===
--
-- The goal of the [SRS](https://github.com/ciribob/DCS-SimpleRadioStandalone) project is to bring VoIP communication into DCS and to make communication as frictionless as possible.
--
-- ===
--
-- ### Author: **funkyfranky**
-- @module Sound.SRS
-- @image Sound_MSRS.png

--- MSRS class.
-- @type MSRS
-- @field #string ClassName Name of the class.
-- @field #string lid Class id string for output to DCS log file.
-- @field #table frequencies Frequencies used in the transmissions.
-- @field #table modulations Modulations used in the transmissions.
-- @field #number coalition Coalition of the transmission.
-- @field #number port Port. Default 5002.
-- @field #string name Name. Default "MSRS".
-- @field #number volume Volume between 0 (min) and 1 (max). Default 1.
-- @field #string culture Culture. Default "en-GB".
-- @field #string gender Gender. Default "female".
-- @field #string voice Specific voice. Only used if no explicit provider voice specified.
-- @field Core.Point#COORDINATE coordinate Coordinate from where the transmission is send.
-- @field #string path Path to the SRS exe.
-- @field #string Label Label showing up on the SRS radio overlay. Default is "ROBOT". No spaces allowed.
-- @field #string ConfigFileName Name of the standard config file.
-- @field #string ConfigFilePath Path to the standard config file.
-- @field #boolean ConfigLoaded If `true` if config file was loaded.
-- @field #table poptions Provider options. Each element is a data structure of type `MSRS.ProvierOptions`.
-- @field #string provider Provider of TTS (win, gcloud, azure, amazon).
-- @field #string backend Backend used as interface to SRS (MSRS.Backend.SRSEXE or MSRS.Backend.GRPC).
-- @field #boolean UsePowerShell Use PowerShell to execute the command and not cmd.exe
-- @extends Core.Base#BASE

--- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde
--
-- ===
--
-- # The MSRS Concept
--
-- This class allows to broadcast sound files or text via Simple Radio Standalone (SRS).
--
-- ## Prerequisites
--
-- * This script needs SRS version >= 1.9.6
-- * You need to de-sanitize os, io and lfs in the missionscripting.lua
-- * Optional: DCS-gRPC as backend to communicate with SRS (vide infra)
--
-- ## Knwon Issues
--
-- ### Pop-up Window
--
-- The text-to-speech conversion of SRS is done via an external exe file. When this file is called, a windows `cmd` window is briefly opended. That puts DCS out of focus, which is annoying,
-- expecially in VR but unavoidable (if you have a solution, please feel free to share!).
--
-- NOTE that this is not an issue if the mission is running on a server.
-- Also NOTE that using DCS-gRPC as backend will avoid the pop-up window.
--
-- # Play Sound Files
--
--     local soundfile=SOUNDFILE:New("My Soundfile.ogg", "D:\\Sounds For DCS")
--     local msrs=MSRS:New("C:\\Path To SRS", 251, radio.modulation.AM)
--     msrs:PlaySoundFile(soundfile)
--
-- # Play Text-To-Speech
--
-- Basic example:
--
--     -- Create a SOUNDTEXT object.
--     local text=SOUNDTEXT:New("All Enemies destroyed")
--
--     -- MOOSE SRS
--     local msrs=MSRS:New("D:\\DCS\\_SRS\\", 305, radio.modulation.AM)
--
--     -- Text-to speech with default voice after 2 seconds.
--     msrs:PlaySoundText(text, 2)
--
-- ## Set Gender
--
-- Use a specific gender with the @{#MSRS.SetGender} function, e.g. `SetGender("male")` or `:SetGender("female")`.
--
-- ## Set Culture
--
-- Use a specific "culture" with the @{#MSRS.SetCulture} function, e.g. `:SetCulture("en-US")` or `:SetCulture("de-DE")`.
--
-- ## Set Voice
--
-- Use a specific voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`.
-- Note that this must be installed on your windows system.
--
-- Note that you can set voices for each provider via the @{#MSRS.SetVoiceProvider} function. Also shortcuts are available, *i.e.*
-- @{#MSRS.SetVoiceWindows}, @{#MSRS.SetVoiceGoogle}, @{#MSRS.SetVoiceAzure} and @{#MSRS.SetVoiceAmazon}.
--
-- For voices there are enumerators in this class to help you out on voice names:
--
--     MSRS.Voices.Microsoft -- e.g. MSRS.Voices.Microsoft.Hedda - the Microsoft enumerator contains all voices known to work with SRS
--     MSRS.Voices.Google -- e.g. MSRS.Voices.Google.Standard.en_AU_Standard_A or MSRS.Voices.Google.Wavenet.de_DE_Wavenet_C - The Google enumerator contains voices for EN, DE, IT, FR and ES.
--
-- ## Set Coordinate
--
-- Use @{#MSRS.SetCoordinate} to define the origin from where the transmission is broadcasted.
-- Note that this is only a factor if SRS server has line-of-sight and/or distance limit enabled.
--
-- ## Set SRS Port
--
-- Use @{#MSRS.SetPort} to define the SRS port. Defaults to 5002.
--
-- ## Set SRS Volume
--
-- Use @{#MSRS.SetVolume} to define the SRS volume. Defaults to 1.0. Allowed values are between 0.0 and 1.0, from silent to loudest.
--
-- ## Config file for many variables, auto-loaded by Moose
--
-- See @{#MSRS.LoadConfigFile} for details on how to set this up.
--
-- ## TTS Providers
--
-- The default provider for generating speech from text is the native Windows TTS service. Note that you need to install the voices you want to use.
-- 
-- **Pro-Tip** - use the command line with power shell to call `DCS-SR-ExternalAudio.exe` - it will tell you what is missing
-- and also the Google Console error, in case you have missed a step in setting up your Google TTS.
-- For example, `.\DCS-SR-ExternalAudio.exe -t "Text Message" -f 255 -m AM -c 2 -s 2 -z -G "Path_To_You_Google.Json"`
-- plays a message on 255 MHz AM for the blue coalition in-game.
--
-- ### Google
-- 
-- In order to use Google Cloud for TTS you need to use @{#MSRS.SetProvider} and @{#MSRS.SetProviderOptionsGoogle} functions:
-- 
--     msrs:SetProvider(MSRS.Provider.GOOGLE)
--     msrs:SetProviderOptionsGoogle(CredentialsFile, AccessKey)
--
-- The parameter `CredentialsFile` is used with the default 'DCS-SR-ExternalAudio.exe' backend and must be the full path to the credentials JSON file.
-- The `AccessKey` parameter is used with the DCS-gRPC backend (see below).
--
-- You can set the voice to use with Google via @{#MSRS.SetVoiceGoogle}.
--
-- When using Google it also allows you to utilize SSML in your text for more flexibility.
-- For more information on setting up a cloud account, visit: https://cloud.google.com/text-to-speech
-- Google's supported SSML reference: https://cloud.google.com/text-to-speech/docs/ssml
--
-- ### Amazon Web Service [Only DCS-gRPC backend]
-- 
-- In order to use Amazon Web Service (AWS) for TTS you need to use @{#MSRS.SetProvider} and @{#MSRS.SetProviderOptionsAmazon} functions:
-- 
--     msrs:SetProvider(MSRS.Provider.AMAZON)
--     msrs:SetProviderOptionsAmazon(AccessKey, SecretKey, Region)
--     
-- The parameters `AccessKey` and `SecretKey` are your AWS access and secret keys, respectively. The parameter `Region` is your [AWS region](https://docs.aws.amazon.com/general/latest/gr/pol.html).
-- 
-- You can set the voice to use with AWS via @{#MSRS.SetVoiceAmazon}.
-- 
-- ### Microsoft Azure [Only DCS-gRPC backend]
-- 
-- In order to use Microsoft Azure for TTS you need to use @{#MSRS.SetProvider} and @{#MSRS.SetProviderOptionsAzure} functions:
-- 
--     msrs:SetProvider(MSRS.Provider.AZURE)
--     msrs:SetProviderOptionsAmazon(AccessKey, Region)
--
-- The parameter `AccessKey` is your Azure access key. The parameter `Region` is your [Azure region](https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/regions).
--
-- You can set the voice to use with Azure via @{#MSRS.SetVoiceAzure}.
--
-- ## Backend
--
-- The default interface to SRS is via calling the 'DCS-SR-ExternalAudio.exe'. As noted above, this has the unavoidable drawback that a pop-up briefly appears
-- and DCS might be put out of focus.
--
-- ## DCS-gRPC as an alternative to 'DCS-SR-ExternalAudio.exe' for TTS
--
-- Another interface to SRS is [DCS-gRPC](https://github.com/DCS-gRPC/rust-server). This does not call an exe file and therefore avoids the annoying pop-up window.
-- In addition to Windows and Google cloud, it also offers Microsoft Azure and Amazon Web Service as providers for TTS.
--
-- Use @{#MSRS.SetDefaultBackendGRPC} to enable [DCS-gRPC](https://github.com/DCS-gRPC/rust-server) as an alternate backend for transmitting text-to-speech over SRS.
-- This can be useful if 'DCS-SR-ExternalAudio.exe' cannot be used in the environment or to use Azure or AWS clouds for TTS.  Note that DCS-gRPC does not (yet?) support
-- all of the features and options available with 'DCS-SR-ExternalAudio.exe'. Of note, only text-to-speech is supported and it it cannot be used to transmit audio files.
--
-- DCS-gRPC must be installed and configured per the [DCS-gRPC documentation](https://github.com/DCS-gRPC/rust-server) and already running via either the 'autostart' mechanism
-- or a Lua call to 'GRPC.load()' prior to use of the alternate DCS-gRPC backend. If a cloud TTS provider is being used, the API key must be set via the 'Config\dcs-grpc.lua'
-- configuration file prior DCS-gRPC being started. DCS-gRPC can be used both with DCS dedicated server and regular DCS installations.
--
-- To use the default local Windows TTS with DCS-gRPC, Windows 2019 Server (or newer) or Windows 10/11 are required.  Voices for non-local languages and dialects may need to
-- be explicitly installed.
--
-- To set the MSRS class to use the DCS-gRPC backend for all future instances, call the function `MSRS.SetDefaultBackendGRPC()`.
--
-- **Note** - When using other classes that use MSRS with the alternate DCS-gRPC backend, pass them strings instead of nil values for non-applicable fields with filesystem paths,
-- such as the SRS path or Google credential path. This will help maximize compatibility with other classes that were written for the default backend.
--
-- Basic Play Text-To-Speech example using alternate DCS-gRPC backend (DCS-gRPC not previously started):
--
--     -- Start DCS-gRPC
--     GRPC.load()
--     -- Select the alternate DCS-gRPC backend for new MSRS instances
--     MSRS.SetDefaultBackendGRPC()
--     -- Create a SOUNDTEXT object.
--     local text=SOUNDTEXT:New("All Enemies destroyed")
--     -- MOOSE SRS
--     local msrs=MSRS:New('', 305.0)
--     -- Text-to speech with default voice after 30 seconds.
--     msrs:PlaySoundText(text, 30)
--
-- Basic example of using another class (ATIS) with SRS and the DCS-gRPC backend (DCS-gRPC not previously started):
--
--     -- Start DCS-gRPC
--     GRPC.load()
--     -- Select the alternate DCS-gRPC backend for new MSRS instances
--     MSRS.SetDefaultBackendGRPC()
--     -- Create new ATIS as usual
--     atis=ATIS:New("Nellis", 251, radio.modulation.AM)
--     -- ATIS:SetSRS() expects a string for the SRS path even though it is not needed with DCS-gRPC
--     atis:SetSRS('')
--     -- Start ATIS
--     atis:Start()
--
-- @field #MSRS
MSRS = {
  ClassName      =     "MSRS",
  lid            =        nil,
  port           =       5002,
  name           =     "MSRS",
  backend        =   "srsexe",
  frequencies    =         {},
  modulations    =         {},
  coalition      =          0,
  gender         =   "female",
  culture        =        nil,
  voice          =        nil,
  volume         =          1,
  speed          =          1,
  coordinate     =        nil,
  provider       =      "win",
  Label          =    "ROBOT",
  ConfigFileName =    "Moose_MSRS.lua",
  ConfigFilePath =    "Config\\",
  ConfigLoaded   =     false,
  poptions       =        {},
  UsePowerShell  =      false,
}

--- MSRS class version.
-- @field #string version
MSRS.version="0.3.3"

--- Voices
-- @type MSRS.Voices
MSRS.Voices = {
  Amazon = {
    Generative = {
      en_AU = {
        Olivia = "Olivia",
      },
      en_GB = {
        Amy = "Amy",
      },
      en_US = {
        Danielle = "Danielle",
        Joanna = "Joanna",
        Ruth = "Ruth",
        Stephen = "Stephen",
        },
      fr_FR = {
        ["Léa"] = "Léa",
        ["Rémi"] = "Rémi",
      },
      de_DE = {
        Vicki = "Vicki",
        Daniel = "Daniel",
      },
      it_IT = {
        Bianca = "Bianca",
        Adriano = "Adriano",
      },
      es_ES = {
        Lucia = "Lucia",
        Sergio = "Sergio",    
      },
    },
    LongForm = {
      en_US = {
        Danielle = "Danielle",
        Gregory = "Gregory",
        Ivy = "Ivy",
        Ruth = "Ruth",
        Patrick = "Patrick",
      },
      es_ES = {
        Alba = "Alba",
        ["Raúl"] = "Raúl",    
      },
    },
    Neural = {
      en_AU = {
        Olivia = "Olivia",
      },
      en_GB = {
        Amy = "Amy",
        Emma = "Emma",
        Brian = "Brian",
        Arthur = "Arthur",
      },
      en_US = {
        Danielle = "Danielle",
        Gregory = "Gregory",
        Ivy = "Ivy",
        Joanna = "Joanna",
        Kendra = "Kendra",
        Kimberly = "Kimberly",
        Salli = "Salli",
        Joey = "Joey",
        Kevin = "Kevin",  
        Ruth = "Ruth",
        Stephen = "Stephen",       
      },
      fr_FR = {
        ["Léa"] = "Léa",
        ["Rémi"] = "Rémi",
      },
      de_DE = {
        Vicki = "Vicki",
        Daniel = "Daniel",
      },
      it_IT = {
        Bianca = "Bianca",
        Adriano = "Adriano",
      },
      es_ES = {
        Lucia = "Lucia",
        Sergio = "Sergio",    
      },
    },
    Standard = {
      en_AU = {
        Nicole = "Nicole",
        Russel = "Russel",
      },
      en_GB = {
        Amy = "Amy",
        Emma = "Emma",
        Brian = "Brian",
      },
      en_IN = {
        Aditi = "Aditi",
        Raveena = "Raveena",
      },
      en_US = {
        Ivy = "Ivy",
        Joanna = "Joanna",
        Kendra = "Kendra",
        Kimberly = "Kimberly",
        Salli = "Salli",
        Joey = "Joey",
        Kevin = "Kevin",       
      },
      fr_FR = {
        Celine = "Celine",
        ["Léa"] = "Léa",
        Mathieu = "Mathieu",
      },
      de_DE = {
        Marlene = "Marlene",
        Vicki = "Vicki",
        Hans = "Hans",
      },
      it_IT = {
        Carla = "Carla",
        Bianca = "Bianca",
        Giorgio = "Giorgio",
      },
      es_ES = {
        Conchita = "Conchita",
        Lucia = "Lucia",
        Enrique = "Enrique",     
      },
    },
  },
  Microsoft = { -- working ones if not using gRPC and MS
    ["Hedda"] = "Microsoft Hedda Desktop", -- de-DE
    ["Hazel"] = "Microsoft Hazel Desktop", -- en-GB
    ["David"] = "Microsoft David Desktop", -- en-US
    ["Zira"] = "Microsoft Zira Desktop", -- en-US
    ["Hortense"] = "Microsoft Hortense Desktop", --fr-FR
    ["de_DE_Hedda"] = "Microsoft Hedda Desktop", -- de-DE
    ["en_GB_Hazel"] = "Microsoft Hazel Desktop", -- en-GB
    ["en_US_David"] = "Microsoft David Desktop", -- en-US
    ["en_US_Zira"] = "Microsoft Zira Desktop", -- en-US
    ["fr_FR_Hortense"] = "Microsoft Hortense Desktop", --fr-FR
    },
  MicrosoftGRPC = { -- en-US/GB voices only as of Jan 2024, working ones if using gRPC and MS, if voice packs are installed
    --["Hedda"] = "Hedda", -- de-DE
    ["Hazel"] = "Hazel", -- en-GB
    ["George"] = "George", -- en-GB
    ["Susan"] = "Susan", -- en-GB
    ["David"] = "David", -- en-US
    ["Zira"] = "Zira", -- en-US
    ["Mark"] = "Mark", -- en-US
    ["James"] = "James", --en-AU
    ["Catherine"] = "Catherine", --en-AU
    ["Richard"] = "Richard", --en-CA
    ["Linda"] = "Linda", --en-CA
    ["Ravi"] = "Ravi", --en-IN
    ["Heera"] = "Heera", --en-IN
    ["Sean"] = "Sean", --en-IR
    ["en_GB_Hazel"] = "Hazel", -- en-GB
    ["en_GB_George"] = "George", -- en-GB
    ["en_GB_Susan"] = "Susan", -- en-GB
    ["en_US_David"] = "David", -- en-US
    ["en_US_Zira"] = "Zira", -- en-US
    ["en_US_Mark"] = "Mark", -- en-US
    ["en_AU_James"] = "James", --en-AU
    ["en_AU_Catherine"] = "Catherine", --en-AU
    ["en_CA_Richard"] = "Richard", --en-CA
    ["en_CA_Linda"] = "Linda", --en-CA
    ["en_IN_Ravi"] = "Ravi", --en-IN
    ["en_IN_Heera"] = "Heera", --en-IN
    ["en_IR_Sean"] = "Sean", --en-IR   
    },  
  Google = {
    Standard = {
       ["en_AU_Standard_A"] = 'en-AU-Standard-A', -- [1] FEMALE
       ["en_AU_Standard_B"] = 'en-AU-Standard-B', -- [2] MALE
       ["en_AU_Standard_C"] = 'en-AU-Standard-C', -- [3] FEMALE
       ["en_AU_Standard_D"] = 'en-AU-Standard-D', -- [4] MALE
       -- IN
        ["en_IN_Standard_A"] = 'en-IN-Standard-A', -- Female
        ["en_IN_Standard_B"] = 'en-IN-Standard-B', -- Male
        ["en_IN_Standard_C"] = 'en-IN-Standard-C', -- Male
        ["en_IN_Standard_D"] = 'en-IN-Standard-D', -- Female
        ["en_IN_Standard_E"] = 'en-IN-Standard-E', -- Female
        ["en_IN_Standard_F"] = 'en-IN-Standard-F', -- Male
       -- 2025 changes
       ["en_GB_Standard_A"] = 'en-GB-Standard-A', -- Female
       ["en_GB_Standard_B"] = 'en-GB-Standard-B', -- Male
       ["en_GB_Standard_C"] = 'en-GB-Standard-C', -- Female
       ["en_GB_Standard_D"] = 'en-GB-Standard-D', -- Male
       ["en_GB_Standard_F"] = 'en-GB-Standard-F', -- Female
       ["en_GB_Standard_N"] = 'en-GB-Standard-N', -- Female
       ["en_GB_Standard_O"] = 'en-GB-Standard-O', -- Male
       -- US
       ["en_US_Standard_A"] = 'en-US-Standard-A', -- Male
       ["en_US_Standard_B"] = 'en-US-Standard-B', -- Male
       ["en_US_Standard_C"] = 'en-US-Standard-C', -- Female
       ["en_US_Standard_D"] = 'en-US-Standard-D', -- Male
       ["en_US_Standard_E"] = 'en-US-Standard-E', -- Female
       ["en_US_Standard_F"] = 'en-US-Standard-F', -- Female
       ["en_US_Standard_G"] = 'en-US-Standard-G', -- Female
       ["en_US_Standard_H"] = 'en-US-Standard-H', -- Female
       ["en_US_Standard_I"] = 'en-US-Standard-I', -- Male
       ["en_US_Standard_J"] = 'en-US-Standard-J', -- Male
       -- 2025 catalog changes
       ["fr_FR_Standard_A"] = "fr-FR-Standard-F", -- Female
       ["fr_FR_Standard_B"] = "fr-FR-Standard-G", -- Male
       ["fr_FR_Standard_C"] = "fr-FR-Standard-F", -- Female
       ["fr_FR_Standard_D"] = "fr-FR-Standard-G", -- Male
       ["fr_FR_Standard_E"] = "fr-FR-Standard-F", -- Female
       ["fr_FR_Standard_G"] = "fr-FR-Standard-G", -- Male
       ["fr_FR_Standard_F"] = "fr-FR-Standard-F", -- Female
       -- 2025 catalog changes
      ["de_DE_Standard_A"] = 'de-DE-Standard-A', -- Female
      ["de_DE_Standard_B"] = 'de-DE-Standard-B', -- Male
      ["de_DE_Standard_C"] = 'de-DE-Standard-C', -- Female
      ["de_DE_Standard_D"] = 'de-DE-Standard-D', -- Male
      ["de_DE_Standard_E"] = 'de-DE-Standard-E', -- Male
      ["de_DE_Standard_F"] = 'de-DE-Standard-F', -- Female
      ["de_DE_Standard_G"] = 'de-DE-Standard-G', -- Female
      ["de_DE_Standard_H"] = 'de-DE-Standard-H', -- Male
      -- ES
       ["es_ES_Standard_A"] = "es-ES-Standard-E", -- Female
       ["es_ES_Standard_B"] = "es-ES-Standard-F", -- Male
       ["es_ES_Standard_C"] = "es-ES-Standard-E", -- Female
       ["es_ES_Standard_D"] = "es-ES-Standard-F", -- Male
       ["es_ES_Standard_E"] = "es-ES-Standard-E", -- Female
       ["es_ES_Standard_F"] = "es-ES-Standard-F", -- Male
       -- 2025 catalog changes
       ["it_IT_Standard_A"] = "it-IT-Standard-E", -- Female
       ["it_IT_Standard_B"] = "it-IT-Standard-E", -- Female
       ["it_IT_Standard_C"] = "it-IT-Standard-F", -- Male
       ["it_IT_Standard_D"] = "it-IT-Standard-F", -- Male
       ["it_IT_Standard_E"] = "it-IT-Standard-E", -- Female
       ["it_IT_Standard_F"] = "it-IT-Standard-F", -- Male
      },
      Wavenet = {
       ["en_AU_Wavenet_A"] = 'en-AU-Wavenet-A', -- Female
       ["en_AU_Wavenet_B"] = 'en-AU-Wavenet-B', -- Male
       ["en_AU_Wavenet_C"] = 'en-AU-Wavenet-C', -- Female
       ["en_AU_Wavenet_D"] = 'en-AU-Wavenet-D', -- Male
       -- IN
        ["en_IN_Wavenet_A"] = 'en-IN-Wavenet-A', -- Female
        ["en_IN_Wavenet_B"] = 'en-IN-Wavenet-B', -- Male
        ["en_IN_Wavenet_C"] = 'en-IN-Wavenet-C', -- Male
        ["en_IN_Wavenet_D"] = 'en-IN-Wavenet-D', -- Female
        ["en_IN_Wavenet_E"] = 'en-IN-Wavenet-E', -- Female
        ["en_IN_Wavenet_F"] = 'en-IN-Wavenet-F', -- Male
       -- 2025 changes
       ["en_GB_Wavenet_A"] = 'en-GB-Wavenet-A', -- [9] FEMALE
       ["en_GB_Wavenet_B"] = 'en-GB-Wavenet-B', -- [10] MALE
       ["en_GB_Wavenet_C"] = 'en-GB-Wavenet-C', -- [11] FEMALE
       ["en_GB_Wavenet_D"] = 'en-GB-Wavenet-D', -- [12] MALE
       ["en_GB_Wavenet_F"] = 'en-GB-Wavenet-F', -- [13] FEMALE
       ["en_GB_Wavenet_O"] = 'en-GB-Wavenet-O', -- [12] MALE
       ["en_GB_Wavenet_N"] = 'en-GB-Wavenet-N', -- [13] FEMALE  
       -- US   
       ["en_US_Wavenet_A"] = 'en-US-Wavenet-A', -- Male
       ["en_US_Wavenet_B"] = 'en-US-Wavenet-B', -- Male
       ["en_US_Wavenet_C"] = 'en-US-Wavenet-C', -- Female
       ["en_US_Wavenet_D"] = 'en-US-Wavenet-D', -- Male
       ["en_US_Wavenet_E"] = 'en-US-Wavenet-E', -- Female
       ["en_US_Wavenet_F"] = 'en-US-Wavenet-F', -- Female
       ["en_US_Wavenet_G"] = 'en-US-Wavenet-G', -- Female
       ["en_US_Wavenet_H"] = 'en-US-Wavenet-H', -- Female
       ["en_US_Wavenet_I"] = 'en-US-Wavenet-I', -- Male
       ["en_US_Wavenet_J"] = 'en-US-Wavenet-J', -- Male
       -- 2025 catalog changes
       ["fr_FR_Wavenet_A"] = "fr-FR-Wavenet-F", -- Female
       ["fr_FR_Wavenet_B"] = "fr-FR-Wavenet-G", -- Male
       ["fr_FR_Wavenet_C"] = "fr-FR-Wavenet-F", -- Female
       ["fr_FR_Wavenet_D"] = "fr-FR-Wavenet-G", -- Male
       ["fr_FR_Wavenet_E"] = "fr-FR-Wavenet-F", -- Female
       ["fr_FR_Wavenet_G"] = "fr-FR-Wavenet-G", -- Male
       ["fr_FR_Wavenet_F"] = "fr-FR-Wavenet-F", -- Female
       -- 2025 catalog changes
        ["de_DE_Wavenet_A"] = 'de-DE-Wavenet-A', -- Female
        ["de_DE_Wavenet_B"] = 'de-DE-Wavenet-B', -- Male
        ["de_DE_Wavenet_C"] = 'de-DE-Wavenet-C', -- Female
        ["de_DE_Wavenet_D"] = 'de-DE-Wavenet-D', -- Male
        ["de_DE_Wavenet_E"] = 'de-DE-Wavenet-E', -- Male
        ["de_DE_Wavenet_F"] = 'de-DE-Wavenet-F', -- Female
        ["de_DE_Wavenet_G"] = 'de-DE-Wavenet-G', -- Female
        ["de_DE_Wavenet_H"] = 'de-DE-Wavenet-H', -- Male
       -- ES
       ["es_ES_Wavenet_B"] = "es-ES-Wavenet-E", -- Male
       ["es_ES_Wavenet_C"] = "es-ES-Wavenet-F", -- Female
       ["es_ES_Wavenet_D"] = "es-ES-Wavenet-E", -- Female
       ["es_ES_Wavenet_E"] = "es-ES-Wavenet-E", -- Male
       ["es_ES_Wavenet_F"] = "es-ES-Wavenet-F", -- Female
       -- 2025 catalog changes
       ["it_IT_Wavenet_A"] = "it-IT-Wavenet-E", -- Female
       ["it_IT_Wavenet_B"] = "it-IT-Wavenet-E", -- Female
       ["it_IT_Wavenet_C"] = "it-IT-Wavenet-F", -- Male
       ["it_IT_Wavenet_D"] = "it-IT-Wavenet-F", -- Male
       ["it_IT_Wavenet_E"] = "it-IT-Wavenet-E", -- Female
       ["it_IT_Wavenet_F"] = "it-IT-Wavenet-F", -- Male
      } ,
      Chirp3HD = {
       ["en_GB_Chirp3_HD_Aoede"] = 'en-GB-Chirp3-HD-Aoede', -- Female
       ["en_GB_Chirp3_HD_Charon"] = 'en-GB-Chirp3-HD-Charon', -- Male
       ["en_GB_Chirp3_HD_Fenrir"] = 'en-GB-Chirp3-HD-Fenrir', -- Male
       ["en_GB_Chirp3_HD_Kore"] = 'en-GB-Chirp3-HD-Kore', -- Female
       ["en_GB_Chirp3_HD_Leda"] = 'en-GB-Chirp3-HD-Leda', -- Female
       ["en_GB_Chirp3_HD_Orus"] = 'en-GB-Chirp3-HD-Orus', -- Male
       ["en_GB_Chirp3_HD_Puck"] = 'en-GB-Chirp3-HD-Puck', -- Male
       ["en_GB_Chirp3_HD_Zephyr"] = 'en-GB-Chirp3-HD-Zephyr', -- Female
       --["de_DE_Chirp3_HD_Aoede"] = 'de-DE-Chirp3-HD-Aoede', -- Female (Datenfehler im Original)
       ["en_US_Chirp3_HD_Charon"] = 'en-US-Chirp3-HD-Charon', -- Male
       ["en_US_Chirp3_HD_Fenrir"] = 'en-US-Chirp3-HD-Fenrir', -- Male
       ["en_US_Chirp3_HD_Kore"] = 'en-US-Chirp3-HD-Kore', -- Female
       ["en_US_Chirp3_HD_Leda"] = 'en-US-Chirp3-HD-Leda', -- Female
       ["en_US_Chirp3_HD_Orus"] = 'en-US-Chirp3-HD-Orus', -- Male
       ["en_US_Chirp3_HD_Puck"] = 'en-US-Chirp3-HD-Puck', -- Male
       --["de_DE_Chirp3_HD_Zephyr"] = 'de-DE-Chirp3-HD-Zephyr', -- Female (Datenfehler im Original)
       -- DE
       ["de_DE_Chirp3_HD_Aoede"] = 'de-DE-Chirp3-HD-Aoede', -- Female
       ["de_DE_Chirp3_HD_Charon"] = 'de-DE-Chirp3-HD-Charon', -- Male
       ["de_DE_Chirp3_HD_Fenrir"] = 'de-DE-Chirp3-HD-Fenrir', -- Male
       ["de_DE_Chirp3_HD_Kore"] = 'de-DE-Chirp3-HD-Kore', -- Female
       ["de_DE_Chirp3_HD_Leda"] = 'de-DE-Chirp3-HD-Leda', -- Female
       ["de_DE_Chirp3_HD_Orus"] = 'de-DE-Chirp3-HD-Orus', -- Male
       ["de_DE_Chirp3_HD_Puck"] = 'de-DE-Chirp3-HD-Puck', -- Male
       ["de_DE_Chirp3_HD_Zephyr"] = 'de-DE-Chirp3-HD-Zephyr', -- Female
       -- AU
       ["en_AU_Chirp3_HD_Aoede"] = 'en-AU-Chirp3-HD-Aoede', -- Female
       ["en_AU_Chirp3_HD_Charon"] = 'en-AU-Chirp3-HD-Charon', -- Male
       ["en_AU_Chirp3_HD_Fenrir"] = 'en-AU-Chirp3-HD-Fenrir', -- Male
       ["en_AU_Chirp3_HD_Kore"] = 'en-AU-Chirp3-HD-Kore', -- Female
       ["en_AU_Chirp3_HD_Leda"] = 'en-AU-Chirp3-HD-Leda', -- Female
       ["en_AU_Chirp3_HD_Orus"] = 'en-AU-Chirp3-HD-Orus', -- Male
       ["en_AU_Chirp3_HD_Puck"] = 'en-AU-Chirp3-HD-Puck', -- Male
       ["en_AU_Chirp3_HD_Zephyr"] = 'en-AU-Chirp3-HD-Zephyr', -- Female
       -- IN
       ["en_IN_Chirp3_HD_Aoede"] = 'en-IN-Chirp3-HD-Aoede', -- Female
       ["en_IN_Chirp3_HD_Charon"] = 'en-IN-Chirp3-HD-Charon', -- Male
       ["en_IN_Chirp3_HD_Fenrir"] = 'en-IN-Chirp3-HD-Fenrir', -- Male
       ["en_IN_Chirp3_HD_Kore"] = 'en-IN-Chirp3-HD-Kore', -- Female
       ["en_IN_Chirp3_HD_Leda"] = 'en-IN-Chirp3-HD-Leda', -- Female
       ["en_IN_Chirp3_HD_Orus"] = 'en-IN-Chirp3-HD-Orus', -- Male
      },
      ChirpHD = {
       ["en_US_Chirp_HD_D"] = 'en-US-Chirp-HD-D', -- Male
       ["en_US_Chirp_HD_F"] = 'en-US-Chirp-HD-F', -- Female
       ["en_US_Chirp_HD_O"] = 'en-US-Chirp-HD-O', -- Female
       -- DE
       ["de_DE_Chirp_HD_D"] = 'de-DE-Chirp-HD-D', -- Male
       ["de_DE_Chirp_HD_F"] = 'de-DE-Chirp-HD-F', -- Female
       ["de_DE_Chirp_HD_O"] = 'de-DE-Chirp-HD-O', -- Female
       -- AU
       ["en_AU_Chirp_HD_D"] = 'en-AU-Chirp-HD-D', -- Male
       ["en_AU_Chirp_HD_F"] = 'en-AU-Chirp-HD-F', -- Female
       ["en_AU_Chirp_HD_O"] = 'en-AU-Chirp-HD-O', -- Female
       -- IN
       ["en_IN_Chirp_HD_D"] = 'en-IN-Chirp-HD-D', -- Male
       ["en_IN_Chirp_HD_F"] = 'en-IN-Chirp-HD-F', -- Female
       ["en_IN_Chirp_HD_O"] = 'en-IN-Chirp-HD-O', -- Female
      },
    },
    Neural2 = {
       ["en_GB_Neural2_A"] = 'en-GB-Neural2-A', -- Female
       ["en_GB_Neural2_B"] = 'en-GB-Neural2-B', -- Male
       ["en_GB_Neural2_C"] = 'en-GB-Neural2-C', -- Female
       ["en_GB_Neural2_D"] = 'en-GB-Neural2-D', -- Male
       ["en_GB_Neural2_F"] = 'en-GB-Neural2-F', -- Female
       ["en_GB_Neural2_N"] = 'en-GB-Neural2-N', -- Female
       ["en_GB_Neural2_O"] = 'en-GB-Neural2-O', -- Male
       -- US
       ["en_US_Neural2_A"] = 'en-US-Neural2-A', -- Male
       ["en_US_Neural2_C"] = 'en-US-Neural2-C', -- Female
       ["en_US_Neural2_D"] = 'en-US-Neural2-D', -- Male
       ["en_US_Neural2_E"] = 'en-US-Neural2-E', -- Female
       ["en_US_Neural2_F"] = 'en-US-Neural2-F', -- Female
       ["en_US_Neural2_G"] = 'en-US-Neural2-G', -- Female
       ["en_US_Neural2_H"] = 'en-US-Neural2-H', -- Female
       ["en_US_Neural2_I"] = 'en-US-Neural2-I', -- Male
       ["en_US_Neural2_J"] = 'en-US-Neural2-J', -- Male
       -- DE
       ["de_DE_Neural2_G"] = 'de-DE-Neural2-G', -- Female
       ["de_DE_Neural2_H"] = 'de-DE-Neural2-H', -- Male
       -- AU
       ["en_AU_Neural2_A"] = 'en-AU-Neural2-A', -- Female
       ["en_AU_Neural2_B"] = 'en-AU-Neural2-B', -- Male
       ["en_AU_Neural2_C"] = 'en-AU-Neural2-C', -- Female
       ["en_AU_Neural2_D"] = 'en-AU-Neural2-D', -- Male
       -- IN
       ["en_IN_Neural2_A"] = 'en-IN-Neural2-A', -- Female
       ["en_IN_Neural2_B"] = 'en-IN-Neural2-B', -- Male
       ["en_IN_Neural2_C"] = 'en-IN-Neural2-C', -- Male
       ["en_IN_Neural2_D"] = 'en-IN-Neural2-D', -- Female
    },
    News = {
       ["en_GB_News_G"] = 'en-GB-News-G', -- Female
       ["en_GB_News_H"] = 'en-GB-News-H', -- Female
       ["en_GB_News_I"] = 'en-GB-News-I', -- Female
       ["en_GB_News_J"] = 'en-GB-News-J', -- Male
       ["en_GB_News_K"] = 'en-GB-News-K', -- Male
       ["en_GB_News_L"] = 'en-GB-News-L', -- Male
       ["en_GB_News_M"] = 'en-GB-News-M', -- Male
       -- US
       ["en_US_News_K"] = 'en-US-News-K', -- Female
       ["en_US_News_L"] = 'en-US-News-L', -- Female
       ["en_US_News_N"] = 'en-US-News-N', -- Male
       -- AU
       ["en_AU_News_E"] = 'en-AU-News-E', -- Female
       ["en_AU_News_F"] = 'en-AU-News-F', -- Female
       ["en_AU_News_G"] = 'en-AU-News-G', -- Male
    },
    Casual = {
       ["en_US_Casual_K"] = 'en-US-Casual-K', -- Male
    },
    Polyglot = {
       ["en_US_Polyglot_1"] = 'en-US-Polyglot-1', -- Male
       ["de_DE_Polyglot_1"] = 'de-DE-Polyglot-1', -- Male
       ["en_AU_Polyglot_1"] = 'en-AU-Polyglot-1', -- Male
    },
    Studio = {
       -- Englisch (UK) - Studio
       ["en_GB_Studio_B"] = 'en-GB-Studio-B', -- Male
       ["en_GB_Studio_C"] = 'en-GB-Studio-C', -- Female
       -- Englisch (USA) - Studio
       ["en_US_Studio_O"] = 'en-US-Studio-O', -- Female
       ["en_US_Studio_Q"] = 'en-US-Studio-Q', -- Male
       -- DE
       ["de_DE_Studio_B"] = 'de-DE-Studio-B', -- Male
       ["de_DE_Studio_C"] = 'de-DE-Studio-C', -- Female
    },
  }


--- Backend options to communicate with SRS.
-- @type MSRS.Backend
-- @field #string SRSEXE Use `DCS-SR-ExternalAudio.exe`.
-- @field #string GRPC Use DCS-gRPC.
MSRS.Backend = {
  SRSEXE = "srsexe",
  GRPC   = "grpc",
}

--- Text-to-speech providers. These are compatible with the DCS-gRPC conventions.
-- @type MSRS.Provider
-- @field #string WINDOWS Microsoft windows (`win`).
-- @field #string GOOGLE Google (`gcloud`).
-- @field #string AZURE Microsoft Azure (`azure`). Only possible with DCS-gRPC backend.
-- @field #string AMAZON Amazon Web Service (`aws`). Only possible with DCS-gRPC backend.
MSRS.Provider = {
  WINDOWS = "win",
  GOOGLE  = "gcloud",
  AZURE   = "azure",
  AMAZON  = "aws",
}

--- Function for UUID.
function MSRS.uuid()
  local random = math.random
  local template = 'yxxx-xxxxxxxxxxxx'
  return string.gsub( template, '[xy]', function( c )
    local v = (c == 'x') and random( 0, 0xf ) or random( 8, 0xb )
    return string.format( '%x', v )
  end )
end

--- Provider options.
-- @type MSRS.ProviderOptions
-- @field #string provider Provider.
-- @field #string credentials Google credentials JSON file (full path).
-- @field #string key Access key (DCS-gRPC with Google, AWS, AZURE as provider).
-- @field #string secret Secret key (DCS-gRPC with AWS as provider)
-- @field #string region Region.
-- @field #string defaultVoice Default voice (not used).
-- @field #string voice Voice used.

--- GRPC options.
-- @type MSRS.GRPCOptions
-- @field #string plaintext
-- @field #string srsClientName
-- @field #table position
-- @field #string coalition
-- @field #MSRS.ProviderOptions gcloud
-- @field #MSRS.ProviderOptions win
-- @field #MSRS.ProviderOptions azure
-- @field #MSRS.ProviderOptions aws
-- @field #string DefaultProvider

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- DONE: Refactoring of input/config file.
-- DONE: Refactoring gRPC backend.
-- TODO: Add functions to remove freqs and modulations.
-- DONE: Add coordinate.
-- DONE: Add google.
-- DONE: Add gRPC google options
-- DONE: Add loading default config file

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Create a new MSRS object. Required argument is the frequency and modulation.
-- Other parameters are read from the `Moose_MSRS.lua` config file. If you do not have that file set up you must set up and use the `DCS-SR-ExternalAudio.exe` (not DCS-gRPC) as backend, you need to still
-- set the path to the exe file via @{#MSRS.SetPath}.
--
-- @param #MSRS self
-- @param #string Path Path to SRS directory. Default `C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio`.
-- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. Can also be given as a #table of multiple frequencies.
-- @param #number Modulation Radio modulation: 0=AM (default), 1=FM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. Can also be given as a #table of multiple modulations.
-- @param #string Backend Backend used: `MSRS.Backend.SRSEXE` (default) or `MSRS.Backend.GRPC`.
-- @return #MSRS self
function MSRS:New(Path, Frequency, Modulation, Backend)

  -- Inherit everything from BASE class.
  local self=BASE:Inherit(self, BASE:New()) -- #MSRS

  self:F( {Path, Frequency, Modulation, Backend} )

  -- Defaults.
  Frequency = Frequency or 143
  Modulation = Modulation or radio.modulation.AM

  self.lid = string.format("%s-%s | ", "unknown", self.version)

  if not self.ConfigLoaded then

    -- Defaults.
    self:SetPath(Path)
    self:SetPort()
    self:SetFrequencies(Frequency)
    self:SetModulations(Modulation)
    self:SetGender()
    self:SetCoalition()
    self:SetLabel()
    self:SetVolume()
    self:SetBackend(Backend)

  else

    -- Default overwrites from :New()
    
    if Path then
      self:SetPath(Path)
    end

    if Frequency then
      self:SetFrequencies(Frequency)
    end

    if Modulation then
      self:SetModulations(Modulation)
    end

    if Backend then
      self:SetBackend(Backend)
    end

  end

  self.lid = string.format("%s-%s | ", self.name, self.version)

  if not io or not os then
    self:E(self.lid.."***** ERROR - io or os NOT desanitized! MSRS will not work!")
  end

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Set backend to communicate with SRS.
-- There are two options:
--
-- - `MSRS.Backend.SRSEXE`: This is the default and uses the `DCS-SR-ExternalAudio.exe`.
-- - `MSRS.Backend.GRPC`: Via DCS-gRPC.
--
-- @param #MSRS self
-- @param #string Backend Backend used. Default is `MSRS.Backend.SRSEXE`.
-- @return #MSRS self
function MSRS:SetBackend(Backend)
  self:F( {Backend=Backend} )
  Backend = Backend or MSRS.Backend.SRSEXE -- avoid nil
  local function Checker(back)
    local ok = false
    for _,_backend in pairs(MSRS.Backend) do
      if tostring(back) == _backend then ok = true end
    end
    return ok
  end
  
  if Checker(Backend) then  
    self.backend=Backend 
  else    
    MESSAGE:New("ERROR: Backend "..tostring(Backend).." is not supported!",30,"MSRS",true):ToLog():ToAll()    
  end 
  return self
end

--- Set DCS-gRPC as backend to communicate with SRS.
-- @param #MSRS self
-- @return #MSRS self
function MSRS:SetBackendGRPC()
  self:F()
  self:SetBackend(MSRS.Backend.GRPC)

  return self
end

--- Set `DCS-SR-ExternalAudio.exe` as backend to communicate with SRS.
-- @param #MSRS self
-- @return #MSRS self
function MSRS:SetBackendSRSEXE()
  self:F()
  self:SetBackend(MSRS.Backend.SRSEXE)

  return self
end

--- Set the default backend.
-- @param #string Backend
function MSRS.SetDefaultBackend(Backend)
  MSRS.backend=Backend or MSRS.Backend.SRSEXE
end

--- Set DCS-gRPC to be the default backend.
-- @param #MSRS self
function MSRS.SetDefaultBackendGRPC()
  MSRS.backend=MSRS.Backend.GRPC
end

--- Get currently set backend.
-- @param #MSRS self
-- @return #string Backend.
function MSRS:GetBackend()
  return self.backend
end

--- Set path to SRS install directory. More precisely, path to where the `DCS-SR-ExternalAudio.exe` is located.
-- @param #MSRS self
-- @param #string Path Path to the directory, where the sound file is located. Default is `C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio`.
-- @return #MSRS self
function MSRS:SetPath(Path)
  self:F( {Path=Path} )

  -- Set path.
  self.path=Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio"

  -- Remove (back)slashes.
  local n=1 ; local nmax=1000
  while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do
    self.path=self.path:sub(1,#self.path-1)
    n=n+1
  end

  -- Debug output.
  self:F(string.format("SRS path=%s", self:GetPath()))
  
  return self
end

--- Get path to SRS directory.
-- @param #MSRS self
-- @return #string Path to the directory. This includes the final slash "/".
function MSRS:GetPath()
  return self.path
end

--- Set SRS volume.
-- @param #MSRS self
-- @param #number Volume Volume - 1.0 is max, 0.0 is silence
-- @return #MSRS self
function MSRS:SetVolume(Volume)
  self:F( {Volume=Volume} )
  local volume = Volume or 1
  if volume > 1 then volume = 1 elseif volume < 0 then volume = 0 end
  self.volume = volume
  return self
end

--- Get SRS volume.
-- @param #MSRS self
-- @return #number Volume Volume - 1.0 is max, 0.0 is silence
function MSRS:GetVolume()
  return self.volume
end

--- Set label.
-- @param #MSRS self
-- @param #number Label. Default "ROBOT"
-- @return #MSRS self
function MSRS:SetLabel(Label)
  self:F( {Label=Label} )
  self.Label=Label or "ROBOT"
  return self
end

--- Get label.
-- @param #MSRS self
-- @return #number Label.
function MSRS:GetLabel()
  return self.Label
end

--- Set port.
-- @param #MSRS self
-- @param #number Port Port. Default 5002.
-- @return #MSRS self
function MSRS:SetPort(Port)
  self:F( {Port=Port} )
  self.port=Port or 5002
  self:T(string.format("SRS port=%s", self:GetPort()))
  return self
end

--- Get port.
-- @param #MSRS self
-- @return #number Port.
function MSRS:GetPort()
  return self.port
end

--- Set coalition.
-- @param #MSRS self
-- @param #number Coalition Coalition. Default 0.
-- @return #MSRS self
function MSRS:SetCoalition(Coalition)
  self:F( {Coalition=Coalition} )
  self.coalition=Coalition or 0
  return self
end

--- Get coalition.
-- @param #MSRS self
-- @return #number Coalition.
function MSRS:GetCoalition()
  return self.coalition
end


--- Set frequencies.
-- @param #MSRS self
-- @param #table Frequencies Frequencies in MHz. Can also be given as a #number if only one frequency should be used.
-- @return #MSRS self
function MSRS:SetFrequencies(Frequencies)
  self:F( Frequencies )
  self.frequencies=UTILS.EnsureTable(Frequencies, false)

  return self
end

--- Add frequencies.
-- @param #MSRS self
-- @param #table Frequencies Frequencies in MHz. Can also be given as a #number if only one frequency should be used.
-- @return #MSRS self
function MSRS:AddFrequencies(Frequencies)
  self:F( Frequencies )
  for _,_freq in pairs(UTILS.EnsureTable(Frequencies, false)) do
    self:T(self.lid..string.format("Adding frequency %s", tostring(_freq)))
    table.insert(self.frequencies,_freq)
  end

  return self
end

--- Get frequencies.
-- @param #MSRS self
-- @return #table Frequencies in MHz.
function MSRS:GetFrequencies()
  return self.frequencies
end


--- Set modulations.
-- @param #MSRS self
-- @param #table Modulations Modulations. Can also be given as a #number if only one modulation should be used.
-- @return #MSRS self
function MSRS:SetModulations(Modulations)
  self:F( Modulations )
  self.modulations=UTILS.EnsureTable(Modulations, false)

  -- Debug info.
  self:T(self.lid.."Modulations:")
  self:T(self.modulations)

  return self
end

--- Add modulations.
-- @param #MSRS self
-- @param #table Modulations Modulations. Can also be given as a #number if only one modulation should be used.
-- @return #MSRS self
function MSRS:AddModulations(Modulations)
  self:F( Modulations )
   for _,_mod in pairs(UTILS.EnsureTable(Modulations, false)) do
    table.insert(self.modulations,_mod)
   end

  return self
end

--- Get modulations.
-- @param #MSRS self
-- @return #table Modulations.
function MSRS:GetModulations()
  return self.modulations
end

--- Set gender.
-- @param #MSRS self
-- @param #string Gender Gender: "male" or "female" (default).
-- @return #MSRS self
function MSRS:SetGender(Gender)
  self:F( {Gender=Gender} )
  Gender=Gender or "female"

  self.gender=Gender:lower()

  -- Debug output.
  self:T("Setting gender to "..tostring(self.gender))

  return self
end

--- Set culture.
-- @param #MSRS self
-- @param #string Culture Culture, *e.g.* "en-GB".
-- @return #MSRS self
function MSRS:SetCulture(Culture)
  self:F( {Culture=Culture} )
  self.culture=Culture

  return self
end

--- Set to use a specific voice. Note that this will override any gender and culture settings as a voice already has a certain gender/culture.
-- @param #MSRS self
-- @param #string Voice Voice.
-- @return #MSRS self
function MSRS:SetVoice(Voice)
  self:F( {Voice=Voice} )
  self.voice=Voice

  return self
end

--- Set to use a specific voice for a given provider. Note that this will override any gender and culture settings.
-- @param #MSRS self
-- @param #string Voice Voice.
-- @param #string Provider Provider. Default is as set by @{#MSRS.SetProvider}, which itself defaults to `MSRS.Provider.WINDOWS` if not set.
-- @return #MSRS self
function MSRS:SetVoiceProvider(Voice, Provider)
  self:F( {Voice=Voice, Provider=Provider} )
  self.poptions=self.poptions or {}

  self.poptions[Provider or self:GetProvider()].voice=Voice

  return self
end

--- Set to use a specific voice if Microsoft Windows' native TTS is use as provider. Note that this will override any gender and culture settings.
-- @param #MSRS self
-- @param #string Voice Voice. Default `"Microsoft Hazel Desktop"`.
-- @return #MSRS self
function MSRS:SetVoiceWindows(Voice)
  self:F( {Voice=Voice} )
  self:SetVoiceProvider(Voice or "Microsoft Hazel Desktop", MSRS.Provider.WINDOWS)

  return self
end

--- Set to use a specific voice if Google is use as provider. Note that this will override any gender and culture settings.
-- @param #MSRS self
-- @param #string Voice Voice. Default `MSRS.Voices.Google.Standard.en_GB_Standard_A`.
-- @return #MSRS self
function MSRS:SetVoiceGoogle(Voice)
  self:F( {Voice=Voice} )
  self:SetVoiceProvider(Voice or MSRS.Voices.Google.Standard.en_GB_Standard_A, MSRS.Provider.GOOGLE)

  return self
end


--- Set to use a specific voice if Microsoft Azure is use as provider (only DCS-gRPC backend). Note that this will override any gender and culture settings.
-- @param #MSRS self
-- @param #string Voice [Azure Voice](https://learn.microsoft.com/azure/cognitive-services/speech-service/language-support). Default `"en-US-AriaNeural"`.
-- @return #MSRS self
function MSRS:SetVoiceAzure(Voice)
  self:F( {Voice=Voice} )
  self:SetVoiceProvider(Voice or "en-US-AriaNeural", MSRS.Provider.AZURE)

  return self
end

--- Set to use a specific voice if Amazon Web Service is use as provider (only DCS-gRPC backend). Note that this will override any gender and culture settings.
-- @param #MSRS self
-- @param #string Voice [AWS Voice](https://docs.aws.amazon.com/polly/latest/dg/voicelist.html). Default `"Brian"`.
-- @return #MSRS self
function MSRS:SetVoiceAmazon(Voice)
  self:F( {Voice=Voice} )
  self:SetVoiceProvider(Voice or "Brian", MSRS.Provider.AMAZON)

  return self
end

--- Get voice.
-- @param #MSRS self
-- @param #string Provider Provider. Default is the currently set provider (`self.provider`).
-- @return #string Voice.
function MSRS:GetVoice(Provider)

  Provider=Provider or self.provider

  if Provider and self.poptions[Provider] and self.poptions[Provider].voice then
    return self.poptions[Provider].voice
  else
    return self.voice
  end

end

--- Set the coordinate from which the transmissions will be broadcasted. Note that this is only a factor if SRS has line-of-sight or distance enabled.
-- @param #MSRS self
-- @param Core.Point#COORDINATE Coordinate Origin of the transmission.
-- @return #MSRS self
function MSRS:SetCoordinate(Coordinate)
  self:F( Coordinate )
  self.coordinate=Coordinate

  return self
end

--- **[Deprecated]** Use google text-to-speech credentials. Also sets Google as default TTS provider.
-- @param #MSRS self
-- @param #string PathToCredentials Full path to the google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json". Can also be the Google API key.
-- @return #MSRS self
function MSRS:SetGoogle(PathToCredentials)
  self:F( {PathToCredentials=PathToCredentials} )
  if PathToCredentials then

    self.provider = MSRS.Provider.GOOGLE

    self:SetProviderOptionsGoogle(PathToCredentials, PathToCredentials)

  end

  return self
end

--- **[Deprecated]** Use google text-to-speech set the API key (only for DCS-gRPC).
-- @param #MSRS self
-- @param #string APIKey API Key, usually a string of length 40 with characters and numbers.
-- @return #MSRS self
function MSRS:SetGoogleAPIKey(APIKey)
  self:F( {APIKey=APIKey} )
  if APIKey then

    self.provider = MSRS.Provider.GOOGLE

    if self.poptions[MSRS.Provider.GOOGLE] then
      self.poptions[MSRS.Provider.GOOGLE].key=APIKey
    else
      self:SetProviderOptionsGoogle(nil ,APIKey)
    end

  end
  return self
end


--- Set provider used to generate text-to-speech.
-- These options are available:
--
-- - `MSRS.Provider.WINDOWS`: Microsoft Windows (default)
-- - `MSRS.Provider.GOOGLE`: Google Cloud
-- - `MSRS.Provider.AZURE`: Microsoft Azure (only with DCS-gRPC backend)
-- - `MSRS.Provier.AMAZON`: Amazon Web Service (only with DCS-gRPC backend)
--
-- Note that all providers except Microsoft Windows need as additonal information the credentials of your account.
--
-- @param #MSRS self
-- @param #string Provider
-- @return #MSRS self
function MSRS:SetProvider(Provider)
  BASE:F( {Provider=Provider} )
  if self then
    self.provider = Provider or MSRS.Provider.WINDOWS
    return self
  else
    MSRS.provider = Provider or MSRS.Provider.WINDOWS
  end
  return
end

--- Get provider.
-- @param #MSRS self
-- @return #MSRS self
function MSRS:GetProvider()
  return self.provider or MSRS.Provider.WINDOWS
end

--- Set provider options and credentials.
-- @param #MSRS self
-- @param #string Provider Provider.
-- @param #string CredentialsFile Full path to your credentials file. For Google this is the path to a JSON file.
-- @param #string AccessKey Your API access key.
-- @param #string SecretKey Your secret key.
-- @param #string Region Region to use.
-- @return #MSRS.ProviderOptions Provider optionas table.
function MSRS:SetProviderOptions(Provider, CredentialsFile, AccessKey, SecretKey, Region)
  BASE:F( {Provider, CredentialsFile, AccessKey, SecretKey, Region} )
  local option=MSRS._CreateProviderOptions(Provider, CredentialsFile, AccessKey, SecretKey, Region)

  if self then
    self.poptions=self.poptions or {}
    self.poptions[Provider]=option
  else
    MSRS.poptions=MSRS.poptions or {}
    MSRS.poptions[Provider]=option
  end

  return option
end

--- Create MSRS.ProviderOptions.
-- @param #string Provider Provider.
-- @param #string CredentialsFile Full path to your credentials file. For Google this is the path to a JSON file.
-- @param #string AccessKey Your API access key.
-- @param #string SecretKey Your secret key.
-- @param #string Region Region to use.
-- @return #MSRS.ProviderOptions Provider optionas table.
function MSRS._CreateProviderOptions(Provider, CredentialsFile, AccessKey, SecretKey, Region)
  BASE:F( {Provider, CredentialsFile, AccessKey, SecretKey, Region} )
  local option={} --#MSRS.ProviderOptions

  option.provider=Provider
  option.credentials=CredentialsFile
  option.key=AccessKey
  option.secret=SecretKey
  option.region=Region

  return option
end

--- Set provider options and credentials for Google Cloud.
-- @param #MSRS self
-- @param #string CredentialsFile Full path to your credentials file. For Google this is the path to a JSON file. This is used if `DCS-SR-ExternalAudio.exe` is used as backend.
-- @param #string AccessKey Your API access key. This is necessary if DCS-gRPC is used as backend.
-- @return #MSRS self
function MSRS:SetProviderOptionsGoogle(CredentialsFile, AccessKey)
  self:F( {CredentialsFile, AccessKey} )
  self:SetProviderOptions(MSRS.Provider.GOOGLE, CredentialsFile, AccessKey)
  return self
end

--- Set provider options and credentials for Amazon Web Service (AWS). Only supported in combination with DCS-gRPC as backend.
-- @param #MSRS self
-- @param #string AccessKey Your API access key.
-- @param #string SecretKey Your secret key.
-- @param #string Region Your AWS [region](https://docs.aws.amazon.com/general/latest/gr/pol.html).
-- @return #MSRS self
function MSRS:SetProviderOptionsAmazon(AccessKey, SecretKey, Region)
  self:F( {AccessKey, SecretKey, Region} )
  self:SetProviderOptions(MSRS.Provider.AMAZON, nil, AccessKey, SecretKey, Region)
  return self
end

--- Set provider options and credentials for Microsoft Azure. Only supported in combination with DCS-gRPC as backend.
-- @param #MSRS self
-- @param #string AccessKey Your API access key.
-- @param #string Region Your Azure [region](https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/regions).
-- @return #MSRS self
function MSRS:SetProviderOptionsAzure(AccessKey, Region)
  self:F( {AccessKey, Region} )
  self:SetProviderOptions(MSRS.Provider.AZURE, nil, AccessKey, nil, Region)
  return self
end


--- Get provider options.
-- @param #MSRS self
-- @param #string Provider Provider. Default is as set via @{#MSRS.SetProvider}.
-- @return #MSRS.ProviderOptions Provider options.
function MSRS:GetProviderOptions(Provider)
  return self.poptions[Provider or self.provider] or {}
end


--- Use Google to provide text-to-speech.
-- @param #MSRS self
-- @return #MSRS self
function MSRS:SetTTSProviderGoogle()
  self:F()
  self:SetProvider(MSRS.Provider.GOOGLE)
  return self
end

--- Use Microsoft to provide text-to-speech.
-- @param #MSRS self
-- @return #MSRS self
function MSRS:SetTTSProviderMicrosoft()
  self:F()
  self:SetProvider(MSRS.Provider.WINDOWS)
  return self
end


--- Use Microsoft Azure to provide text-to-speech. Only supported if used in combination with DCS-gRPC as backend.
-- @param #MSRS self
-- @return #MSRS self
function MSRS:SetTTSProviderAzure()
  self:F()
  self:SetProvider(MSRS.Provider.AZURE)
  return self
end

--- Use Amazon Web Service (AWS) to provide text-to-speech. Only supported if used in combination with DCS-gRPC as backend.
-- @param #MSRS self
-- @return #MSRS self
function MSRS:SetTTSProviderAmazon()
  self:F()
  self:SetProvider(MSRS.Provider.AMAZON)
  return self
end


--- Print SRS help to DCS log file.
-- @param #MSRS self
-- @return #MSRS self
function MSRS:Help()
  self:F()
  -- Path and exe.
  local path=self:GetPath()
  local exe="DCS-SR-ExternalAudio.exe"

  -- Text file for output.
  local filename = os.getenv('TMP') .. "\\MSRS-help-"..MSRS.uuid()..".txt"

  -- Print help.
  local command=string.format("%s/%s --help > %s", path, exe, filename)
  os.execute(command)

  local f=assert(io.open(filename, "rb"))
  local data=f:read("*all")
  f:close()

  -- Print to log file.
  env.info("SRS help output:")
  env.info("======================================================================")
  env.info(data)
  env.info("======================================================================")

  return self
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Transmission Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Play sound file (ogg or mp3) via SRS.
-- @param #MSRS self
-- @param Sound.SoundOutput#SOUNDFILE Soundfile Sound file to play.
-- @param #number Delay Delay in seconds, before the sound file is played.
-- @return #MSRS self
function MSRS:PlaySoundFile(Soundfile, Delay)
  self:F( {Soundfile, Delay} )

  -- Sound file name.
  local soundfile=Soundfile:GetName()

  -- First check if text file exists!
  local exists=UTILS.FileExists(soundfile)
  if not exists then
    self:E("ERROR: MSRS sound file does not exist! File="..soundfile)
    return self
  end

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, MSRS.PlaySoundFile, self, Soundfile, 0)
  else

    -- Get command.
    local command=self:_GetCommand()

    -- Append file.
    command=command..' --file="'..tostring(soundfile)..'"'
    command=string.gsub(command,"--ssml","-h")    
    
    -- Execute command.
    self:_ExecCommand(command)

  end

  return self
end

--- Play a SOUNDTEXT text-to-speech object.
-- @param #MSRS self
-- @param Sound.SoundOutput#SOUNDTEXT SoundText Sound text.
-- @param #number Delay Delay in seconds, before the sound file is played.
-- @return #MSRS self
function MSRS:PlaySoundText(SoundText, Delay)
  self:F( {SoundText, Delay} )

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, MSRS.PlaySoundText, self, SoundText, 0)
  else

    if self.backend==MSRS.Backend.GRPC then
      self:_DCSgRPCtts(SoundText.text, nil, SoundText.gender, SoundText.culture, SoundText.voice, SoundText.volume, SoundText.label, SoundText.coordinate)
    else

      -- Get command.
      local command=self:_GetCommand(nil, nil, nil, SoundText.gender, SoundText.voice, SoundText.culture, SoundText.volume, SoundText.speed)

      -- Append text.
      command=command..string.format(" --text=\"%s\"", tostring(SoundText.text))

      -- Execute command.
      self:_ExecCommand(command)

    end

  end

  return self
end

--- Play text message via MSRS.
-- @param #MSRS self
-- @param #string Text Text message.
-- @param #number Delay Delay in seconds, before the message is played.
-- @param Core.Point#COORDINATE Coordinate Coordinate.
-- @return #MSRS self
function MSRS:PlayText(Text, Delay, Coordinate)
  self:F( {Text, Delay, Coordinate} )

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, MSRS.PlayText, self, Text, nil, Coordinate)
  else

    if self.backend==MSRS.Backend.GRPC then
      self:T(self.lid.."Transmitting")
      self:_DCSgRPCtts(Text, nil, nil , nil, nil, nil, nil, Coordinate)
    else
      self:PlayTextExt(Text, Delay, nil, nil, nil, nil, nil, nil, nil, Coordinate)
    end

  end

  return self
end

--- Play text message via MSRS with explicitly specified options.
-- @param #MSRS self
-- @param #string Text Text message.
-- @param #number Delay Delay in seconds, before the message is played.
-- @param #table Frequencies Radio frequencies.
-- @param #table Modulations Radio modulations.
-- @param #string Gender Gender.
-- @param #string Culture Culture.
-- @param #string Voice Voice.
-- @param #number Volume Volume.
-- @param #string Label Label.
-- @param Core.Point#COORDINATE Coordinate Coordinate.
-- @return #MSRS self
function MSRS:PlayTextExt(Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate)
  self:T({Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate} )

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, self.PlayTextExt, self, Text, 0, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate)
  else

    Frequencies = Frequencies or self:GetFrequencies()
    Modulations = Modulations or self:GetModulations()

    if self.backend==MSRS.Backend.SRSEXE then

      -- Get command line.
      local command=self:_GetCommand(UTILS.EnsureTable(Frequencies, false), UTILS.EnsureTable(Modulations, false), nil, Gender, Voice, Culture, Volume, nil, nil, Label, Coordinate)

      -- Append text.
      command=command..string.format(" --text=\"%s\"", tostring(Text))

      -- Execute command.
      self:_ExecCommand(command)

    elseif self.backend==MSRS.Backend.GRPC then
      --BASE:I("MSRS.Backend.GRPC")

      self:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate)

    end

  end

  return self
end


--- Play text file via MSRS.
-- @param #MSRS self
-- @param #string TextFile Full path to the file.
-- @param #number Delay Delay in seconds, before the message is played.
-- @return #MSRS self
function MSRS:PlayTextFile(TextFile, Delay)
  self:F( {TextFile, Delay} )

  if Delay and Delay>0 then
    self:ScheduleOnce(Delay, MSRS.PlayTextFile, self, TextFile, 0)
  else

    -- First check if text file exists!
    local exists=UTILS.FileExists(TextFile)
    if not exists then
      self:E("ERROR: MSRS Text file does not exist! File="..tostring(TextFile))
      return self
    end

    -- Get command line.
    local command=self:_GetCommand()

    -- Append text file.
    command=command..string.format(" --textFile=\"%s\"", tostring(TextFile))

    -- Debug output.
    self:T(string.format("MSRS TextFile command=%s", command))

    -- Count length of command.
    local l=string.len(command)
    self:T(string.format("Command length=%d", l))

    -- Execute command.
    self:_ExecCommand(command)

  end

  return self
end


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get lat, long and alt from coordinate.
-- @param #MSRS self
-- @param Core.Point#Coordinate Coordinate Coordinate. Can also be a DCS#Vec3.
-- @return #number Latitude (or 0 if no input coordinate was given).
-- @return #number Longitude (or 0 if no input coordinate was given).
-- @return #number Altitude (or 0 if no input coordinate was given).
function MSRS:_GetLatLongAlt(Coordinate)
  self:F( {Coordinate=Coordinate} )

  local lat=0.0
  local lon=0.0
  local alt=0.0

  if Coordinate then
    lat, lon, alt=coord.LOtoLL(Coordinate)
  end

  return lat, lon, math.floor(alt)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Backend ExternalAudio.exe
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get SRS command to play sound using the `DCS-SR-ExternalAudio.exe`.
-- @param #MSRS self
-- @param #table freqs Frequencies in MHz.
-- @param #table modus Modulations.
-- @param #number coal Coalition.
-- @param #string gender Gender.
-- @param #string voice Voice.
-- @param #string culture Culture.
-- @param #number volume Volume.
-- @param #number speed Speed.
-- @param #number port Port.
-- @param #string label Label, defaults to "ROBOT" (displayed sender name in the radio overlay of SRS) - No spaces allowed!
-- @param Core.Point#COORDINATE coordinate Coordinate.
-- @return #string Command.
function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, speed, port, label, coordinate)
  self:F( {freqs, modus, coal, gender, voice, culture, volume, speed, port, label, coordinate} )

  local path=self:GetPath()
  local exe="DCS-SR-ExternalAudio.exe"
  local fullPath = string.format("%s\\%s", path, exe)

  freqs=table.concat(freqs or self.frequencies, ",")
  modus=table.concat(modus or self.modulations, ",")

  coal=coal or self.coalition
  gender=gender or self.gender
  voice=voice or self:GetVoice(self.provider) or self.voice
  culture=culture or self.culture
  volume=volume or self.volume
  speed=speed or self.speed
  port=port or self.port
  label=label or self.Label
  coordinate=coordinate or self.coordinate

  -- Replace modulation
  modus=modus:gsub("0", "AM")
  modus=modus:gsub("1", "FM")

  -- Command.
  local pwsh = string.format('Start-Process -WindowStyle Hidden -WorkingDirectory \"%s\" -FilePath \"%s\" -ArgumentList \'-f "%s" -m "%s" -c %s -p %s -n "%s" -v "%.1f"', path, exe, freqs, modus, coal, port, label,volume )
  
  local command=string.format('"%s\\%s" -f "%s" -m "%s" -c %s -p %s -n "%s" -v "%.1f"', path, exe, freqs, modus, coal, port, label,volume)

  -- Set voice or gender/culture.
  if voice and self.UsePowerShell ~= true then
    -- Use a specific voice (no need for gender and/or culture.
    command=command..string.format(" --voice=\"%s\"", tostring(voice))
    pwsh=pwsh..string.format(" --voice=\"%s\"", tostring(voice))
  else
    -- Add gender.
    if gender and gender~="female" then
      command=command..string.format(" -g %s", tostring(gender))
      pwsh=pwsh..string.format(" -g %s", tostring(gender))
    end
    -- Add culture.
    if culture and culture~="en-GB" then
      command=command..string.format(" -l %s", tostring(culture))
      pwsh=pwsh..string.format(" -l %s", tostring(culture))
    end
  end

  -- Set coordinate.
  if coordinate then
    local lat,lon,alt=self:_GetLatLongAlt(coordinate)
    command=command..string.format(" -L %.4f -O %.4f -A %d", lat, lon, alt)
    pwsh=pwsh..string.format(" -L %.4f -O %.4f -A %d", lat, lon, alt)
  end

  -- Set provider options
  if self.provider==MSRS.Provider.GOOGLE then
    local pops=self:GetProviderOptions()
    command=command..string.format(' --ssml -G "%s"', pops.credentials)
    pwsh=pwsh..string.format(' --ssml -G "%s"', pops.credentials)
  elseif self.provider==MSRS.Provider.WINDOWS then
    -- Nothing to do.
  else
    self:E("ERROR: SRS only supports WINDOWS and GOOGLE as TTS providers! Use DCS-gRPC backend for other providers such as AWS and Azure.")
  end

  if not UTILS.FileExists(fullPath) then
    self:E("ERROR: MSRS SRS executable does not exist! FullPath="..fullPath)
    command="CommandNotFound"
  end

  -- Debug output.
  self:T("MSRS command from _GetCommand="..command)
  
  if self.UsePowerShell == true then
    return pwsh
  else
    return command
  end
end

--- Execute SRS command to play sound using the `DCS-SR-ExternalAudio.exe`.
-- @param #MSRS self
-- @param #string command Command to executer
-- @return #number Return value of os.execute() command.
function MSRS:_ExecCommand(command)
  self:T2( {command=command} )

  -- Skip this function if _GetCommand was not able to find the executable
  if string.find(command, "CommandNotFound") then return 0 end

  local batContent = command.." && exit"
  -- Create a tmp file.
  local filename=os.getenv('TMP').."\\MSRS-"..MSRS.uuid()..".bat"
  
  if self.UsePowerShell == true then
   filename=os.getenv('TMP').."\\MSRS-"..MSRS.uuid()..".ps1"
   batContent = command .. "\'"
   self:T({batContent=batContent})
  end
  
  local script=io.open(filename, "w+")
  script:write(batContent)
  script:close()

  self:T("MSRS batch file created: "..filename)
  self:T("MSRS batch content: "..batContent)

  local res=nil
  if self.UsePowerShell ~= true then

    -- Create a tmp file.
    local filenvbs = os.getenv('TMP') .. "\\MSRS-"..MSRS.uuid()..".vbs"

    -- VBS script
    local script = io.open(filenvbs, "w+")
    script:write(string.format('Dim WinScriptHost\n'))
    script:write(string.format('Set WinScriptHost = CreateObject("WScript.Shell")\n'))
    script:write(string.format('WinScriptHost.Run Chr(34) & "%s" & Chr(34), 0\n', filename))
    script:write(string.format('Set WinScriptHost = Nothing'))
    script:close()
    self:T("MSRS vbs file created to start batch="..filenvbs)

    -- Run visual basic script. This still pops up a window but very briefly and does not put the DCS window out of focus.
    local runvbs=string.format('cscript.exe //Nologo //B "%s"', filenvbs)

    -- Debug output.
    self:T("MSRS execute VBS command="..runvbs)

    -- Play file in 0.01 seconds
    res=os.execute(runvbs)

    -- Remove file in 1 second.
    timer.scheduleFunction(os.remove, filename, timer.getTime()+1)
    timer.scheduleFunction(os.remove, filenvbs, timer.getTime()+1)
    self:T("MSRS vbs and batch file removed")

  elseif self.UsePowerShell == true then

    local pwsh = string.format('start /min "" powershell.exe  -ExecutionPolicy Unrestricted -WindowStyle Hidden -Command "%s"',filename)
    --env.info("[MSRS] TextToSpeech Command :\n" .. pwsh.."\n")
    
    if string.len(pwsh) > 255 then
      self:E("[MSRS] - pwsh string too long")      
    end
    
    -- Play file in 0.01 seconds
    res=os.execute(pwsh)
    
    -- Remove file in 1 second.
    timer.scheduleFunction(os.remove, filename, timer.getTime()+1)

  else
    -- Play command.
    command=string.format('start /b "" "%s"', filename)

    -- Debug output.
    self:T("MSRS execute command="..command)

    -- Execute command
    res=os.execute(command)

    -- Remove file in 1 second.
    timer.scheduleFunction(os.remove, filename, timer.getTime()+1)

  end


  return res
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- DCS-gRPC Backend Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- DCS-gRPC v0.70 TTS API call:
-- GRPC.tts(ssml, frequency[, options]) - Synthesize text (ssml; SSML tags supported) to speech and transmit it over SRS on the frequency with the following optional options (and their defaults):

-- {
--     -- The plain text without any transformations made to it for the purpose of getting it spoken out
--     -- as desired (no SSML tags, no FOUR NINER instead of 49, ...). Even though this field is
--     -- optional, please consider providing it as it can be used to display the spoken text to players
--     -- with hearing impairments.
--     plaintext = null, -- e.g. `= "Hello Pilot"`

--     -- Name of the SRS client.
--     srsClientName = "DCS-gRPC",

--     -- The origin of the transmission. Relevant if the SRS server has "Line of
--     -- Sight" and/or "Distance Limit" enabled.
--     position = {
--         lat = 0.0,
--         lon = 0.0,
--         alt = 0.0, -- in meters
--     },

--     -- The coalition of the transmission. Relevant if the SRS server has "Secure
--     -- Coalition Radios" enabled. Supported values are: `blue` and `red`. Defaults
--     -- to being spectator if not specified.
--     coalition = null,

--     -- TTS provider to be use. Defaults to the one configured in your config or to Windows'
--     -- built-in TTS. Examples:
--     -- `= { aws = {} }` / `= { aws = { voice = "..." } }` enable AWS TTS
--     -- `= { azure = {} }` / `= { azure = { voice = "..." } }` enable Azure TTS
--     -- `= { gcloud = {} }` / `= { gcloud = { voice = "..." } }` enable Google Cloud TTS
--     -- `= { win = {} }` / `= { win = { voice = "..." } }` enable Windows TTS
--     provider = null,
-- }

--- Make DCS-gRPC API call to transmit text-to-speech over SRS.
-- @param #MSRS self
-- @param #string Text Text of message to transmit (can also be SSML).
-- @param #table Frequencies Radio frequencies to transmit on. Can also accept a number in MHz.
-- @param #string Gender Gender.
-- @param #string Culture Culture.
-- @param #string Voice Voice.
-- @param #number Volume Volume.
-- @param #string Label Label.
-- @param Core.Point#COORDINATE Coordinate Coordinate.
-- @return #MSRS self
function MSRS:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate)

  -- Debug info.
  self:T("MSRS_BACKEND_DCSGRPC:_DCSgRPCtts()")
  self:T({Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate})

  local options = {} -- #MSRS.GRPCOptions

  local ssml = Text or ''

  -- Get frequenceies.
  Frequencies = UTILS.EnsureTable(Frequencies, true) or self:GetFrequencies()

  -- Plain text (not really used.
  options.plaintext=Text

  -- Name shows as sender.
  options.srsClientName = Label or self.Label

  -- Set position.
  if self.coordinate then
    options.position = {}
    options.position.lat, options.position.lon, options.position.alt = self:_GetLatLongAlt(self.coordinate)
  end

  -- Coalition (gRPC expects lower case)
  options.coalition = UTILS.GetCoalitionName(self.coalition):lower()

  -- Provider (win, gcloud, ...)
  local provider = self.provider or MSRS.Provider.WINDOWS

  -- Provider options: voice, credentials
  options.provider = {}
  options.provider[provider] = self:GetProviderOptions(provider)

  -- Voice
  Voice=Voice or self:GetVoice(self.provider) or self.voice
  
  if Voice then
    -- We use a specific voice
    options.provider[provider].voice = Voice
  else
  
    -- DCS-gRPC doesn't directly support language/gender, but can use SSML

    local preTag, genderProp, langProp, postTag = '', '', '', ''

    local gender=""
    if self.gender then
      gender=string.format(' gender=\"%s\"', self.gender)
    end
    local language=""
    if self.culture then
      language=string.format(' language=\"%s\"', self.culture)
    end

    if self.gender or self.culture then
      ssml=string.format("<voice%s%s>%s</voice>", gender, language, Text)
    end
  end
  
  for _,freq in pairs(Frequencies) do
    self:T("Calling GRPC.tts with the following parameter:")
    self:T({ssml=ssml, freq=freq, options=options})
    self:T(options.provider[provider])
    GRPC.tts(ssml, freq*1e6, options)
  end

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Config File
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Get central SRS configuration to be able to play tts over SRS radio using the `DCS-SR-ExternalAudio.exe`.
-- @param #MSRS self
-- @param #string Path Path to config file, defaults to "C:\Users\<yourname>\Saved Games\DCS\Config"
-- @param #string Filename File to load, defaults to "Moose_MSRS.lua"
-- @return #boolean success
-- @usage
--  0) Benefits: Centralize configuration of SRS, keep paths and keys out of the mission source code, making it safer and easier to move missions to/between servers,
--  and also make config easier to use in the code.
--  1) Create a config file named "Moose_MSRS.lua" at this location "C:\Users\<yourname>\Saved Games\DCS\Config" (or wherever your Saved Games folder resides).
--  2) The file needs the following structure:
--
--     -- Moose MSRS default Config
--     MSRS_Config = {
--       Path = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio", -- Path to SRS install directory.
--       Port = 5002,            -- Port of SRS server. Default 5002.
--       Backend = "srsexe",     -- Interface to SRS: "srsexe" or "grpc".
--       Frequency = {127, 243}, -- Default frequences. Must be a table 1..n entries!
--       Modulation = {0,0},     -- Default modulations. Must be a table, 1..n entries, one for each frequency!
--       Volume = 1.0,           -- Default volume [0,1].
--       Coalition = 0,          -- 0 = Neutral, 1 = Red, 2 = Blue (only a factor if SRS server has encryption enabled).
--       Coordinate = {0,0,0},   -- x, y, alt (only a factor if SRS server has line-of-sight and/or distance limit enabled).
--       Culture = "en-GB",
--       Gender = "male",
--       Voice = "Microsoft Hazel Desktop", -- Voice that is used if no explicit provider voice is specified.
--       Label = "MSRS",   
--       Provider = "win", --Provider for generating TTS (win, gcloud, azure, aws).      
--       -- Windows
--       win = {
--         voice = "Microsoft Hazel Desktop",
--       },
--       -- Google Cloud
--       gcloud = {
--         voice = "en-GB-Standard-A", -- The Google Cloud voice to use (see https://cloud.google.com/text-to-speech/docs/voices).
--         credentials="C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio\\yourfilename.json", -- Full path to credentials JSON file (only for SRS-TTS.exe backend)
--         key="Your access Key", -- Google API access key (only for DCS-gRPC backend)
--       },
--       -- Amazon Web Service
--       aws = {
--         voice = "Brian", -- The default AWS voice to use (see https://docs.aws.amazon.com/polly/latest/dg/voicelist.html).
--         key="Your access Key",  -- Your AWS key.
--         secret="Your secret key", -- Your AWS secret key.
--         region="eu-central-1", -- Your AWS region (see https://docs.aws.amazon.com/general/latest/gr/pol.html).
--       },
--       -- Microsoft Azure
--       azure = {
--         voice="en-US-AriaNeural",  --The default Azure voice to use (see https://learn.microsoft.com/azure/cognitive-services/speech-service/language-support).
--         key="Your access key", -- Your Azure access key.
--         region="westeurope", -- The Azure region to use (see https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/regions).
--       },
--     }
--
--  3) The config file is automatically loaded when Moose starts. You can also load the config into the MSRS raw class manually before you do anything else:
--
--         MSRS.LoadConfigFile() -- Note the "." here
--
--  Optionally, your might want to provide a specific path and filename:
--
--         MSRS.LoadConfigFile(nil,MyPath,MyFilename) -- Note the "." here
--
--  This will populate variables for the MSRS raw class and all instances you create with e.g. `mysrs = MSRS:New()`
--  Optionally you can also load this per **single instance** if so needed, i.e.
--
--     mysrs:LoadConfigFile(Path,Filename)
--
--  4) Use the config in your code like so, variable names are basically the same as in the config file, but all lower case, examples:
--
--         -- Needed once only
--         MESSAGE.SetMSRS(MSRS.path,MSRS.port,nil,127,rado.modulation.FM,nil,nil,nil,nil,nil,"TALK")
--
--         -- later on in your code
--
--         MESSAGE:New("Test message!",15,"SPAWN"):ToSRS(243,radio.modulation.AM,nil,nil,MSRS.Voices.Google.Standard.fr_FR_Standard_C)
--
--         -- Create new ATIS as usual
--         atis=ATIS:New(AIRBASE.Caucasus.Batumi, 123, radio.modulation.AM)
--         atis:SetSRS(nil,nil,nil,MSRS.Voices.Google.Standard.en_US_Standard_H)
--         --Start ATIS
--         atis:Start()
function MSRS:LoadConfigFile(Path,Filename)

  if lfs == nil then
    env.info("*****Note - lfs and os need to be desanitized for MSRS to work!")
    return false
  end

  local path = Path or lfs.writedir()..MSRS.ConfigFilePath
  local file = Filename or MSRS.ConfigFileName or "Moose_MSRS.lua"
  local pathandfile = path..file
  local filexsists =  UTILS.FileExists(pathandfile)

  if filexsists and not MSRS.ConfigLoaded then

    env.info("FF reading config file")

    -- Load global MSRS_Config
    assert(loadfile(path..file))()

    if MSRS_Config then

      local Self = self or MSRS  --#MSRS

      Self.path = MSRS_Config.Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone\\ExternalAudio"
      Self.port = MSRS_Config.Port or 5002
      Self.backend = MSRS_Config.Backend or MSRS.Backend.SRSEXE
      Self.frequencies = MSRS_Config.Frequency or {127,243}
      Self.modulations = MSRS_Config.Modulation or {0,0}
      Self.coalition = MSRS_Config.Coalition or 0
      if MSRS_Config.Coordinate then
        Self.coordinate = COORDINATE:New( MSRS_Config.Coordinate[1], MSRS_Config.Coordinate[2], MSRS_Config.Coordinate[3] )
      end
      Self.culture = MSRS_Config.Culture or "en-GB"
      Self.gender = MSRS_Config.Gender or "male"
      Self.Label = MSRS_Config.Label or "MSRS"
      Self.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel

      Self.provider = MSRS_Config.Provider or MSRS.Provider.WINDOWS
      for _,provider in pairs(MSRS.Provider) do
        if MSRS_Config[provider] then
          Self.poptions[provider]=MSRS_Config[provider]
        end
      end

      Self.ConfigLoaded = true

    end
    env.info("MSRS - Successfully loaded default configuration from disk!",false)
  end

  if not filexsists then
    env.info("MSRS - Cannot find default configuration file!",false)
    return false
  end

  return true
end

--- Function returns estimated speech time in seconds.
-- Assumptions for time calc: 100 Words per min, average of 5 letters for english word so
--
--   * 5 chars * 100wpm = 500 characters per min = 8.3 chars per second
--
-- So length of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec map function:
--
-- * (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
--
-- @param #number length can also be passed as #string
-- @param #number speed Defaults to 1.0
-- @param #boolean isGoogle We're using Google TTS
function MSRS.getSpeechTime(length,speed,isGoogle)

  local maxRateRatio = 3

  speed = speed or 1.0
  isGoogle = isGoogle or false

  local speedFactor = 1.0
  if isGoogle then
    speedFactor = speed
  else
    if speed ~= 0 then
      speedFactor = math.abs( speed ) * (maxRateRatio - 1) / 10 + 1
    end
    if speed < 0 then
      speedFactor = 1 / speedFactor
    end
  end

  local wpm = math.ceil( 100 * speedFactor )
  local cps = math.floor( (wpm * 5) / 60 )

  if type( length ) == "string" then
    length = string.len( length )
  end

  return length/cps --math.ceil(length/cps)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Manages radio transmissions.
--
-- The purpose of the MSRSQUEUE class is to manage SRS text-to-speech (TTS) messages using the MSRS class.
-- This can be used to submit multiple TTS messages and the class takes care that they are transmitted one after the other (and not overlapping).
--
-- @type MSRSQUEUE
-- @field #string ClassName Name of the class "MSRSQUEUE".
-- @field #string lid ID for dcs.log.
-- @field #table queue The queue of transmissions.
-- @field #string alias Name of the radio queue.
-- @field #number dt Time interval in seconds for checking the radio queue.
-- @field #number Tlast Time (abs) when the last transmission finished.
-- @field #boolean checking If `true`, the queue update function is scheduled to be called again.
-- @extends Core.Base#BASE
MSRSQUEUE = {
  ClassName   = "MSRSQUEUE",
  Debugmode   = nil,
  lid         = nil,
  queue       =  {},
  alias       = nil,
  dt          = nil,
  Tlast       = nil,
  checking    = nil,
}

--- Radio queue transmission data.
-- @type MSRSQUEUE.Transmission
-- @field #string text Text to be transmitted.
-- @field Sound.SRS#MSRS msrs MOOSE SRS object.
-- @field #number duration Duration in seconds.
-- @field #table subgroups Groups to send subtitle to.
-- @field #string subtitle Subtitle of the transmission.
-- @field #number subduration Duration of the subtitle being displayed.
-- @field #number frequency Frequency.
-- @field #number modulation Modulation.
-- @field #number Tstarted Mission time (abs) in seconds when the transmission started.
-- @field #boolean isplaying If true, transmission is currently playing.
-- @field #number Tplay Mission time (abs) in seconds when the transmission should be played.
-- @field #number interval Interval in seconds before next transmission.
-- @field #boolean TransmitOnlyWithPlayers If true, only transmit if there are alive Players.
-- @field Core.Set#SET_CLIENT PlayerSet PlayerSet created when TransmitOnlyWithPlayers == true
-- @field #string gender Voice gender
-- @field #string culture Voice culture
-- @field #string voice Voice if any
-- @field #number volume Volume
-- @field #string label Label to be used
-- @field Core.Point#COORDINATE coordinate Coordinate for this transmission

--- Create a new MSRSQUEUE object for a given radio frequency/modulation.
-- @param #MSRSQUEUE self
-- @param #string alias (Optional) Name of the radio queue.
-- @return #MSRSQUEUE self The MSRSQUEUE object.
function MSRSQUEUE:New(alias)

  -- Inherit base
  local self=BASE:Inherit(self, BASE:New()) --#MSRSQUEUE

  self.alias=alias or "My Radio"

  self.dt=1.0

  self.lid=string.format("MSRSQUEUE %s | ", self.alias)

  return self
end

--- Clear the radio queue.
-- @param #MSRSQUEUE self
-- @return #MSRSQUEUE self The MSRSQUEUE object.
function MSRSQUEUE:Clear()
  self:T(self.lid.."Clearing MSRSQUEUE")
  self.queue={}
  return self
end


--- Add a transmission to the radio queue.
-- @param #MSRSQUEUE self
-- @param #MSRSQUEUE.Transmission transmission The transmission data table.
-- @return #MSRSQUEUE self
function MSRSQUEUE:AddTransmission(transmission)
  -- Init.
  transmission.isplaying=false
  transmission.Tstarted=nil

  -- Add to queue.
  table.insert(self.queue, transmission)

  -- Start checking.
  if not self.checking then
    self:_CheckRadioQueue()
  end

  return self
end

--- Switch to only transmit if there are players on the server.
-- @param #MSRSQUEUE self
-- @param #boolean Switch If true, only send SRS if there are alive Players.
-- @return #MSRSQUEUE self
function MSRSQUEUE:SetTransmitOnlyWithPlayers(Switch)
  self.TransmitOnlyWithPlayers = Switch
  if Switch == false or Switch==nil then
    if self.PlayerSet then
      self.PlayerSet:FilterStop()
    end
    self.PlayerSet = nil
  else
    self.PlayerSet = SET_CLIENT:New():FilterStart()
  end
  return self
end

--- Create a new transmission and add it to the radio queue.
-- @param #MSRSQUEUE self
-- @param #string text Text to play.
-- @param #number duration Duration in seconds the file lasts. Default is determined by number of characters of the text message.
-- @param Sound.SRS#MSRS msrs MOOSE SRS object.
-- @param #number tstart Start time (abs) seconds. Default now.
-- @param #number interval Interval in seconds after the last transmission finished.
-- @param #table subgroups Groups that should receive the subtiltle.
-- @param #string subtitle Subtitle displayed when the message is played.
-- @param #number subduration Duration [sec] of the subtitle being displayed. Default 5 sec.
-- @param #number frequency Radio frequency if other than MSRS default.
-- @param #number modulation Radio modulation if other then MSRS default.
-- @param #string gender Gender of the voice
-- @param #string culture Culture of the voice
-- @param #string voice Specific voice
-- @param #number volume Volume setting
-- @param #string label Label to be used
-- @param Core.Point#COORDINATE coordinate Coordinate to be used
-- @return #MSRSQUEUE.Transmission Radio transmission table.
function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgroups, subtitle, subduration, frequency, modulation, gender, culture, voice, volume, label,coordinate)
  self:T({Text=text, Dur=duration, start=tstart, int=interval, sub=subgroups, subt=subtitle, sudb=subduration, F=frequency, M=modulation, G=gender, C=culture, V=voice, Vol=volume, L=label})
  if self.TransmitOnlyWithPlayers then
    if self.PlayerSet and self.PlayerSet:CountAlive() == 0 then
      return self
    end
  end

  -- Sanity checks.
  if not text then
    self:E(self.lid.."ERROR: No text specified.")
    return nil
  end
  if type(text)~="string" then
    self:E(self.lid.."ERROR: Text specified is NOT a string.")
    return nil
  end


  -- Create a new transmission object.
  local transmission={} --#MSRSQUEUE.Transmission
  transmission.text=text
  transmission.duration=duration or MSRS.getSpeechTime(text)
  transmission.msrs=msrs
  transmission.Tplay=tstart or timer.getAbsTime()
  transmission.subtitle=subtitle
  transmission.interval=interval or 0
  transmission.frequency=frequency or msrs.frequencies
  transmission.modulation=modulation or msrs.modulations
  transmission.subgroups=subgroups
  if transmission.subtitle then
    transmission.subduration=subduration or transmission.duration
  else
    transmission.subduration=0 --nil
  end
  transmission.gender = gender or msrs.gender
  transmission.culture = culture or msrs.culture
  transmission.voice = voice or msrs.voice
  transmission.volume = volume or msrs.volume
  transmission.label = label or msrs.Label
  transmission.coordinate = coordinate or msrs.coordinate
 
  -- Add transmission to queue.
  self:AddTransmission(transmission)

  return transmission
end

--- Broadcast radio message.
-- @param #MSRSQUEUE self
-- @param #MSRSQUEUE.Transmission transmission The transmission.
function MSRSQUEUE:Broadcast(transmission)
  self:T(self.lid.."Broadcast")
  
  if transmission.frequency then
    transmission.msrs:PlayTextExt(transmission.text, nil, transmission.frequency, transmission.modulation, transmission.gender, transmission.culture, transmission.voice, transmission.volume, transmission.label, transmission.coordinate)
  else
    transmission.msrs:PlayText(transmission.text,nil,transmission.coordinate)
  end

  local function texttogroup(gid)
    -- Text to group.
    trigger.action.outTextForGroup(gid, transmission.subtitle, transmission.subduration, true)
  end

  if transmission.subgroups and #transmission.subgroups>0 then

    for _,_group in pairs(transmission.subgroups) do
      local group=_group --Wrapper.Group#GROUP

      if group and group:IsAlive() then
        local gid=group:GetID()

        self:ScheduleOnce(4, texttogroup, gid)
      end

    end

  end

end

--- Calculate total transmission duration of all transmission in the queue.
-- @param #MSRSQUEUE self
-- @return #number Total transmission duration.
function MSRSQUEUE:CalcTransmisstionDuration()

  local Tnow=timer.getAbsTime()

  local T=0
  for _,_transmission in pairs(self.queue) do
    local transmission=_transmission --#MSRSQUEUE.Transmission

    if transmission.isplaying then

      -- Playing for dt seconds.
      local dt=Tnow-transmission.Tstarted

      T=T+transmission.duration-dt

    else
      T=T+transmission.duration
    end

  end

  return T
end

--- Check radio queue for transmissions to be broadcasted.
-- @param #MSRSQUEUE self
-- @param #number delay Delay in seconds before checking.
function MSRSQUEUE:_CheckRadioQueue(delay)

  -- Transmissions in queue.
  local N=#self.queue

  -- Debug info.
  self:T2(self.lid..string.format("Check radio queue %s: delay=%.3f sec, N=%d, checking=%s", self.alias, delay or 0, N, tostring(self.checking)))

  if delay and delay>0 then

    -- Delayed call.
    self:ScheduleOnce(delay, MSRSQUEUE._CheckRadioQueue, self)

    -- Checking on.
    self.checking=true

  else

    -- Check if queue is empty.
    if N==0 then

      -- Debug info.
      self:T(self.lid..string.format("Check radio queue %s empty ==> disable checking", self.alias))

      -- Queue is now empty. Nothing to else to do. We start checking again, if a transmission is added.
      self.checking=false

      return
    end

    -- Get current abs time.
    local time=timer.getAbsTime()

    -- Checking on.
    self.checking=true

    -- Set dt.
    local dt=self.dt


    local playing=false
    local next=nil  --#MSRSQUEUE.Transmission
    local remove=nil
    for i,_transmission in ipairs(self.queue) do
      local transmission=_transmission  --#MSRSQUEUE.Transmission

      -- Check if transmission time has passed.
      if time>=transmission.Tplay then

        -- Check if transmission is currently playing.
        if transmission.isplaying then

          -- Check if transmission is finished.
          if time>=transmission.Tstarted+transmission.duration then

            -- Transmission over.
            transmission.isplaying=false

            -- Remove ith element in queue.
            remove=i

            -- Store time last transmission finished.
            self.Tlast=time

          else -- still playing

            -- Transmission is still playing.
            playing=true

            dt=transmission.duration-(time-transmission.Tstarted)

          end

        else -- not playing yet

          local Tlast=self.Tlast

          if transmission.interval==nil  then

            -- Not playing ==> this will be next.
            if next==nil then
              next=transmission
            end

          else

            if Tlast==nil or time-Tlast>=transmission.interval then
              next=transmission
            else

            end
          end

          -- We got a transmission or one with an interval that is not due yet. No need for anything else.
          if next or Tlast then
            break
          end

        end

      else

          -- Transmission not due yet.

      end
    end

    -- Found a new transmission.
    if next~=nil and not playing then
      -- Debug info.
      self:T(self.lid..string.format("Broadcasting text=\"%s\" at T=%.3f", next.text, time))

      -- Call SRS.
      self:Broadcast(next)

      next.isplaying=true
      next.Tstarted=time
      dt=next.duration
    end

    -- Remove completed call from queue.
    if remove then
      -- Remove from queue.
      table.remove(self.queue, remove)
      N=N-1

      -- Check if queue is empty.
      if #self.queue==0 then
        -- Debug info.
        self:T(self.lid..string.format("Check radio queue %s empty ==> disable checking", self.alias))

        self.checking=false

        return
      end
    end

    -- Check queue.
    self:_CheckRadioQueue(dt)

  end

end

MSRS.LoadConfigFile()
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Tasking** - A command center governs multiple missions, and takes care of the reporting and communications.
-- 
-- **Features:**
-- 
--   * Govern multiple missions.
--   * Communicate to coalitions, groups.
--   * Assign tasks.
--   * Manage the menus.
--   * Manage reference zones.
-- 
-- ===
--  
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Tasking.CommandCenter
-- @image Task_Command_Center.JPG


--- The COMMANDCENTER class
-- @type COMMANDCENTER
-- @field Wrapper.Group#GROUP HQ
-- @field DCS#coalition CommandCenterCoalition
-- @list<Tasking.Mission#MISSION> Missions
-- @extends Core.Base#BASE


--- Governs multiple missions, the tasking and the reporting.
-- 
-- ![Banner Image](..\Images\deprecated.png)
--  
-- Command centers govern missions, communicates the task assignments between human players of the coalition, and manages the menu flow.
-- It can assign a random task to a player when requested.
-- The commandcenter provides the facilitites to communicate between human players online, executing a task.
--  
-- ## 1. Create a command center object.
--
--   * @{#COMMANDCENTER.New}(): Creates a new COMMANDCENTER object.
-- 
-- ## 2. Command center mission management.
-- 
-- Command centers manage missions. These can be added, removed and provides means to retrieve missions.
-- These methods are heavily used by the task dispatcher classes.
-- 
--   * @{#COMMANDCENTER.AddMission}(): Adds a mission to the commandcenter control.
--   * @{#COMMANDCENTER.RemoveMission}(): Removes a mission to the commandcenter control.
--   * @{#COMMANDCENTER.GetMissions}(): Retrieves the missions table controlled by the commandcenter.
-- 
-- ## 3. Communication management between players. 
-- 
-- Command center provide means of communication between players. 
-- Because a command center is a central object governing multiple missions,  
-- there are several levels at which communication needs to be done.
-- Within MOOSE, communication is facilitated using the message system within the DCS simulator.
-- 
-- Messages can be sent between players at various levels:
-- 
--   - On a global level, to all players.
--   - On a coalition level, only to the players belonging to the same coalition.
--   - On a group level, to the players belonging to the same group.
--   
-- Messages can be sent to **all players** by the command center using the method @{Tasking.CommandCenter#COMMANDCENTER.MessageToAll}().
-- 
-- To send messages to **the coalition of the command center**, there are two methods available:
--  
--   - Use the method @{Tasking.CommandCenter#COMMANDCENTER.MessageToCoalition}() to send a specific message to the coalition, with a given message display duration.
--   - You can send a specific type of message using the method @{Tasking.CommandCenter#COMMANDCENTER.MessageTypeToCoalition}().
--     This will send a message of a specific type to the coalition, and as a result its display duration will be flexible according the message display time selection by the human player.
--     
-- To send messages **to the group** of human players, there are also two methods available:
-- 
--   - Use the method @{Tasking.CommandCenter#COMMANDCENTER.MessageToGroup}() to send a specific message to a group, with a given message display duration.
--   - You can send a specific type of message using the method @{Tasking.CommandCenter#COMMANDCENTER.MessageTypeToGroup}().
--     This will send a message of a specific type to the group, and as a result its display duration will be flexible according the message display time selection by the human player .
--     
-- Messages are considered to be sometimes disturbing for human players, therefore, the settings menu provides the means to activate or deactivate messages.
-- For more information on the message types and display timings that can be selected and configured using the menu, refer to the @{Core.Settings} menu description.
--     
-- ## 4. Command center detailed methods.
-- 
-- Various methods are added to manage command centers.
-- 
-- ### 4.1. Naming and description.
-- 
-- There are 3 methods that can be used to retrieve the description of a command center:
-- 
--   - Use the method @{Tasking.CommandCenter#COMMANDCENTER.GetName}() to retrieve the name of the command center. 
--     This is the name given as part of the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor.
--     The returned name using this method, is not to be used for message communication.
-- 
-- A textual description can be retrieved that provides the command center name to be used within message communication:
-- 
--   - @{Tasking.CommandCenter#COMMANDCENTER.GetShortText}() returns the command center name as `CC [CommandCenterName]`.
--   - @{Tasking.CommandCenter#COMMANDCENTER.GetText}() returns the command center name as `Command Center [CommandCenterName]`.
-- 
-- ### 4.2. The coalition of the command center.
-- 
-- The method @{Tasking.CommandCenter#COMMANDCENTER.GetCoalition}() returns the coalition of the command center.
-- The return value is an enumeration of the type @{DCS#coalition.side}, which contains the RED, BLUE and NEUTRAL coalition. 
-- 
-- ### 4.3. The command center is a real object.
-- 
-- The command center must be represented by a live object within the DCS simulator. As a result, the command center   
-- can be a @{Wrapper.Unit}, a @{Wrapper.Group}, an @{Wrapper.Airbase} or a @{Wrapper.Static} object.
-- 
-- Using the method @{Tasking.CommandCenter#COMMANDCENTER.GetPositionable}() you retrieve the polymorphic positionable object representing
-- the command center, but just be aware that you should be able to use the representable object derivation methods.
-- 
-- ### 5. Command center reports.
-- 
-- Because a command center giverns multiple missions, there are several reports available that are generated by command centers.
-- These reports are generated using the following methods:
-- 
--   - @{Tasking.CommandCenter#COMMANDCENTER.ReportSummary}(): Creates a summary report of all missions governed by the command center.
--   - @{Tasking.CommandCenter#COMMANDCENTER.ReportDetails}(): Creates a detailed report of all missions governed by the command center.
--   - @{Tasking.CommandCenter#COMMANDCENTER.ReportMissionPlayers}(): Creates a report listing the players active at the missions governed by the command center.
--   
-- ## 6. Reference Zones.
-- 
-- Command Centers may be aware of certain Reference Zones within the battleground. These Reference Zones can refer to
-- known areas, recognizable buildings or sites, or any other point of interest.
-- Command Centers will use these Reference Zones to help pilots with defining coordinates in terms of navigation
-- during the WWII era.
-- The Reference Zones are related to the WWII mode that the Command Center will operate in.
-- Use the method @{#COMMANDCENTER.SetModeWWII}() to set the mode of communication to the WWII mode.
-- 
-- In WWII mode, the Command Center will receive detected targets, and will select for each target the closest
-- nearby Reference Zone. This allows pilots to navigate easier through the battle field readying for combat.
-- 
-- The Reference Zones need to be set by the Mission Designer in the Mission Editor.
-- Reference Zones are set by normal trigger zones. One can color the zones in a specific color, 
-- and the radius of the zones doesn't matter, only the point is important. Place the center of these Reference Zones at
-- specific scenery objects or points of interest (like cities, rivers, hills, crossing etc).
-- The trigger zones indicating a Reference Zone need to follow a specific syntax.
-- The name of each trigger zone expressing a Reference Zone need to start with a classification name of the object,
-- followed by a #, followed by a symbolic name of the Reference Zone.
-- A few examples:
-- 
--   * A church at Tskinvali would be indicated as: *Church#Tskinvali*
--   * A train station near Kobuleti would be indicated as: *Station#Kobuleti*
--   
-- The COMMANDCENTER class contains a method to indicate which trigger zones need to be used as Reference Zones.
-- This is done by using the method @{#COMMANDCENTER.SetReferenceZones}().
-- For the moment, only one Reference Zone class can be specified, but in the future, more classes will become possible.
-- 
-- ## 7. Tasks.
-- 
-- ### 7.1. Automatically assign tasks.
-- 
-- One of the most important roles of the command center is the management of tasks.
-- The command center can assign automatically tasks to the players using the @{Tasking.CommandCenter#COMMANDCENTER.SetAutoAssignTasks}() method.
-- When this method is used with a parameter true; the command center will scan at regular intervals which players in a slot are not having a task assigned.
-- For those players; the tasking is enabled to assign automatically a task.
-- An Assign Menu will be accessible for the player under the command center menu, to configure the automatic tasking to switched on or off.
-- 
-- ### 7.2. Automatically accept assigned tasks.
-- 
-- When a task is assigned; the mission designer can decide if players are immediately assigned to the task; or they can accept/reject the assigned task.
-- Use the method @{Tasking.CommandCenter#COMMANDCENTER.SetAutoAcceptTasks}() to configure this behaviour.
-- If the tasks are not automatically accepted; the player will receive a message that he needs to access the command center menu and
-- choose from 2 added menu options either to accept or reject the assigned task within 30 seconds.
-- If the task is not accepted within 30 seconds; the task will be cancelled and a new task will be assigned.
-- 
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- @field #COMMANDCENTER
COMMANDCENTER = {
  ClassName = "COMMANDCENTER",
  CommandCenterName = "",
  CommandCenterCoalition = nil,
  CommandCenterPositionable = nil,
  Name = "",
  ReferencePoints = {},
  ReferenceNames = {},
  CommunicationMode = "80",
}


---
-- @type COMMANDCENTER.AutoAssignMethods
COMMANDCENTER.AutoAssignMethods = {
  ["Random"] = 1,
  ["Distance"] = 2,
  ["Priority"] = 3,
  }

--- The constructor takes an IDENTIFIABLE as the HQ command center.
-- @param #COMMANDCENTER self
-- @param Wrapper.Positionable#POSITIONABLE CommandCenterPositionable
-- @param #string CommandCenterName
-- @return #COMMANDCENTER
function COMMANDCENTER:New( CommandCenterPositionable, CommandCenterName )

  local self = BASE:Inherit( self, BASE:New() ) -- #COMMANDCENTER

  self.CommandCenterPositionable = CommandCenterPositionable  
  self.CommandCenterName = CommandCenterName or CommandCenterPositionable:GetName()
  self.CommandCenterCoalition = CommandCenterPositionable:GetCoalition()
  
	self.Missions = {}

  self:SetAutoAssignTasks( false )
  self:SetAutoAcceptTasks( true )
  self:SetAutoAssignMethod( COMMANDCENTER.AutoAssignMethods.Distance )
  self:SetFlashStatus( false )
  self:SetMessageDuration(10)
  
  self:HandleEvent( EVENTS.Birth,
    -- @param #COMMANDCENTER self
    -- @param Core.Event#EVENTDATA EventData
    function( self, EventData )
      if EventData.IniObjectCategory == 1 then
        local EventGroup = GROUP:Find( EventData.IniDCSGroup )
        --self:E( { CommandCenter = self:GetName(), EventGroup = EventGroup:GetName(), HasGroup = self:HasGroup( EventGroup ), EventData = EventData } )
        if EventGroup and EventGroup:IsAlive() and self:HasGroup( EventGroup ) then
          local CommandCenterMenu = MENU_GROUP:New( EventGroup, self:GetText() )
          local MenuReporting = MENU_GROUP:New( EventGroup, "Missions Reports", CommandCenterMenu )
          local MenuMissionsSummary = MENU_GROUP_COMMAND:New( EventGroup, "Missions Status Report", MenuReporting, self.ReportSummary, self, EventGroup )
          local MenuMissionsDetails = MENU_GROUP_COMMAND:New( EventGroup, "Missions Players Report", MenuReporting, self.ReportMissionsPlayers, self, EventGroup )
          --self:ReportSummary( EventGroup )
          local PlayerUnit = EventData.IniUnit
          for MissionID, Mission in pairs( self:GetMissions() ) do
            local Mission = Mission -- Tasking.Mission#MISSION
            local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled!
            Mission:JoinUnit( PlayerUnit, PlayerGroup )
          end
          self:SetMenu()
        end
      end
      
    end
    )
  
--  -- When a player enters a client or a unit, the CommandCenter will check for each Mission and each Task in the Mission if the player has things to do.
--  -- For these elements, it will=
--  -- - Set the correct menu.
--  -- - Assign the PlayerUnit to the Task if required.
--  -- - Send a message to the other players in the group that this player has joined.
--  self:HandleEvent( EVENTS.PlayerEnterUnit,
--    -- @param #COMMANDCENTER self
--    -- @param Core.Event#EVENTDATA EventData
--    function( self, EventData )
--      local PlayerUnit = EventData.IniUnit
--      for MissionID, Mission in pairs( self:GetMissions() ) do
--        local Mission = Mission -- Tasking.Mission#MISSION
--        local PlayerGroup = EventData.IniGroup -- The GROUP object should be filled!
--        Mission:JoinUnit( PlayerUnit, PlayerGroup )
--      end
--      self:SetMenu()
--    end
--  )

  -- Handle when a player leaves a slot and goes back to spectators ... 
  -- The PlayerUnit will be UnAssigned from the Task.
  -- When there is no Unit left running the Task, the Task goes into Abort...
  self:HandleEvent( EVENTS.MissionEnd,
    -- @param #TASK self
    -- @param Core.Event#EVENTDATA EventData
    function( self, EventData )
      local PlayerUnit = EventData.IniUnit
      for MissionID, Mission in pairs( self:GetMissions() ) do
        local Mission = Mission -- Tasking.Mission#MISSION
        Mission:Stop()
      end
    end
  )

  -- Handle when a player leaves a slot and goes back to spectators ... 
  -- The PlayerUnit will be UnAssigned from the Task.
  -- When there is no Unit left running the Task, the Task goes into Abort...
  self:HandleEvent( EVENTS.PlayerLeaveUnit,
    -- @param #TASK self
    -- @param Core.Event#EVENTDATA EventData
    function( self, EventData )
      local PlayerUnit = EventData.IniUnit
      for MissionID, Mission in pairs( self:GetMissions() ) do
        local Mission = Mission -- Tasking.Mission#MISSION
        if Mission:IsENGAGED() then
          Mission:AbortUnit( PlayerUnit )
        end
      end
    end
  )

  -- Handle when a player crashes ... 
  -- The PlayerUnit will be UnAssigned from the Task.
  -- When there is no Unit left running the Task, the Task goes into Abort...
  self:HandleEvent( EVENTS.Crash,
    -- @param #TASK self
    -- @param Core.Event#EVENTDATA EventData
    function( self, EventData )
      local PlayerUnit = EventData.IniUnit
      for MissionID, Mission in pairs( self:GetMissions() ) do
        local Mission = Mission -- Tasking.Mission#MISSION
        if Mission:IsENGAGED() then
          Mission:CrashUnit( PlayerUnit )
        end
      end
    end
  )
  
  self:SetMenu()
  
  _SETTINGS:SetSystemMenu( CommandCenterPositionable )
  
  self:SetCommandMenu()
	
	return self
end

--- Gets the name of the HQ command center.
-- @param #COMMANDCENTER self
-- @return #string
function COMMANDCENTER:GetName()

  return self.CommandCenterName
end

--- Gets the text string of the HQ command center.
-- @param #COMMANDCENTER self
-- @return #string
function COMMANDCENTER:GetText()

  return "Command Center [" .. self.CommandCenterName .. "]"
end

--- Gets the short text string of the HQ command center.
-- @param #COMMANDCENTER self
-- @return #string
function COMMANDCENTER:GetShortText()

  return "CC [" .. self.CommandCenterName .. "]"
end


--- Gets the coalition of the command center.
-- @param #COMMANDCENTER self
-- @return #number Coalition of the command center.
function COMMANDCENTER:GetCoalition()

  return self.CommandCenterCoalition
end


--- Gets the POSITIONABLE of the HQ command center.
-- @param #COMMANDCENTER self
-- @return Wrapper.Positionable#POSITIONABLE
function COMMANDCENTER:GetPositionable()
  return self.CommandCenterPositionable
end

--- Get the Missions governed by the HQ command center.
-- @param #COMMANDCENTER self
-- @return #list<Tasking.Mission#MISSION>
function COMMANDCENTER:GetMissions()

  return self.Missions or {}
end

--- Add a MISSION to be governed by the HQ command center.
-- @param #COMMANDCENTER self
-- @param Tasking.Mission#MISSION Mission
-- @return Tasking.Mission#MISSION
function COMMANDCENTER:AddMission( Mission )

  self.Missions[Mission] = Mission

  return Mission
end

--- Removes a MISSION to be governed by the HQ command center.
-- The given Mission is not nilified.
-- @param #COMMANDCENTER self
-- @param Tasking.Mission#MISSION Mission
-- @return Tasking.Mission#MISSION
function COMMANDCENTER:RemoveMission( Mission )

  self.Missions[Mission] = nil

  return Mission
end

--- Set special Reference Zones known by the Command Center to guide airborne pilots during WWII.
-- 
-- These Reference Zones are normal trigger zones, with a special naming.
-- The Reference Zones need to be set by the Mission Designer in the Mission Editor.
-- Reference Zones are set by normal trigger zones. One can color the zones in a specific color, 
-- and the radius of the zones doesn't matter, only the center of the zone is important. Place the center of these Reference Zones at
-- specific scenery objects or points of interest (like cities, rivers, hills, crossing etc).
-- The trigger zones indicating a Reference Zone need to follow a specific syntax.
-- The name of each trigger zone expressing a Reference Zone need to start with a classification name of the object,
-- followed by a #, followed by a symbolic name of the Reference Zone.
-- A few examples:
-- 
--   * A church at Tskinvali would be indicated as: *Church#Tskinvali*
--   * A train station near Kobuleti would be indicated as: *Station#Kobuleti*
-- 
-- Taking the above example, this is how this method would be used:
-- 
--     CC:SetReferenceZones( "Church" )
--     CC:SetReferenceZones( "Station" )
-- 
-- 
-- @param #COMMANDCENTER self
-- @param #string ReferenceZonePrefix The name before the #-mark indicating the class of the Reference Zones.
-- @return #COMMANDCENTER
function COMMANDCENTER:SetReferenceZones( ReferenceZonePrefix )
  local MatchPattern = "(.*)#(.*)"
  self:F( { MatchPattern = MatchPattern } )
  for ReferenceZoneName in pairs( _DATABASE.ZONENAMES ) do
    local ZoneName, ReferenceName = string.match( ReferenceZoneName, MatchPattern )
    self:F( { ZoneName = ZoneName, ReferenceName = ReferenceName } )
    if ZoneName and ReferenceName and ZoneName == ReferenceZonePrefix then
      self.ReferencePoints[ReferenceZoneName] = ZONE:New( ReferenceZoneName )
      self.ReferenceNames[ReferenceZoneName] = ReferenceName
    end
  end
  return self
end

--- Set the commandcenter operations in WWII mode
-- This will disable LL, MGRS, BRA, BULLS navigatin messages sent by the Command Center, 
-- and will be replaced by a navigation using Reference Zones.
-- It will also disable the settings at the settings menu for these.
-- @param #COMMANDCENTER self
-- @return #COMMANDCENTER
function COMMANDCENTER:SetModeWWII()
  self.CommunicationMode = "WWII"
  return self
end


--- Returns if the commandcenter operations is in WWII mode
-- @param #COMMANDCENTER self
-- @return #boolean true if in WWII mode.
function COMMANDCENTER:IsModeWWII()
  return self.CommunicationMode == "WWII"
end




--- Sets the menu structure of the Missions governed by the HQ command center.
-- @param #COMMANDCENTER self
function COMMANDCENTER:SetMenu()
  self:F2()

  local MenuTime = timer.getTime()
  for MissionID, Mission in pairs( self:GetMissions() or {} ) do
    local Mission = Mission -- Tasking.Mission#MISSION
    Mission:SetMenu( MenuTime )
  end

  for MissionID, Mission in pairs( self:GetMissions() or {} ) do
    Mission = Mission -- Tasking.Mission#MISSION
    Mission:RemoveMenu( MenuTime )
  end
  
end

--- Gets the commandcenter menu structure governed by the HQ command center.
-- @param #COMMANDCENTER self
-- @param Wrapper.Group#Group TaskGroup Task Group.
-- @return Core.Menu#MENU_COALITION
function COMMANDCENTER:GetMenu( TaskGroup )

  local MenuTime = timer.getTime()

  self.CommandCenterMenus = self.CommandCenterMenus or {}
  local CommandCenterMenu
  
  local CommandCenterText = self:GetText()
  CommandCenterMenu = MENU_GROUP:New( TaskGroup, CommandCenterText ):SetTime(MenuTime)
  self.CommandCenterMenus[TaskGroup] = CommandCenterMenu

  if self.AutoAssignTasks == false then
    local AssignTaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Assign Task", CommandCenterMenu, self.AssignTask, self, TaskGroup ):SetTime(MenuTime):SetTag("AutoTask")
  end
  CommandCenterMenu:Remove( MenuTime, "AutoTask" )
    
  return self.CommandCenterMenus[TaskGroup]
end


--- Assigns a random task to a TaskGroup.
-- @param #COMMANDCENTER self
-- @return #COMMANDCENTER
function COMMANDCENTER:AssignTask( TaskGroup )

  local Tasks = {}
  local AssignPriority = 99999999
  local AutoAssignMethod = self.AutoAssignMethod

  for MissionID, Mission in pairs( self:GetMissions() ) do
    local Mission = Mission -- Tasking.Mission#MISSION
    local MissionTasks = Mission:GetGroupTasks( TaskGroup )
    for MissionTaskName, MissionTask in pairs( MissionTasks or {} ) do
      local MissionTask = MissionTask -- Tasking.Task#TASK
      if MissionTask:IsStatePlanned() or MissionTask:IsStateReplanned() or MissionTask:IsStateAssigned() then
        local TaskPriority = MissionTask:GetAutoAssignPriority( self.AutoAssignMethod, self, TaskGroup )
        if TaskPriority < AssignPriority then
          AssignPriority = TaskPriority
          Tasks = {}
        end
        if TaskPriority == AssignPriority then
          Tasks[#Tasks+1] = MissionTask
        end
      end
    end
  end
  
  local Task = Tasks[ math.random( 1, #Tasks ) ] -- Tasking.Task#TASK
  
  if Task then

    self:T( "Assigning task " .. Task:GetName() .. " using auto assign method " .. self.AutoAssignMethod .. " to " .. TaskGroup:GetName() .. " with task priority " .. AssignPriority )
    
    if not self.AutoAcceptTasks == true then
      Task:SetAutoAssignMethod( ACT_ASSIGN_MENU_ACCEPT:New( Task.TaskBriefing ) )
    end
    
    Task:AssignToGroup( TaskGroup )
    
  end

end


--- Sets the menu of the command center.
-- This command is called within the :New() method.
-- @param #COMMANDCENTER self
function COMMANDCENTER:SetCommandMenu()

  local MenuTime = timer.getTime()
  
  if self.CommandCenterPositionable and self.CommandCenterPositionable:IsInstanceOf(GROUP) then
    local CommandCenterText = self:GetText()
    local CommandCenterMenu = MENU_GROUP:New( self.CommandCenterPositionable, CommandCenterText ):SetTime(MenuTime)
  
    if self.AutoAssignTasks == false then
      local AutoAssignTaskMenu = MENU_GROUP_COMMAND:New( self.CommandCenterPositionable, "Assign Task On", CommandCenterMenu, self.SetAutoAssignTasks, self, true ):SetTime(MenuTime):SetTag("AutoTask")
    else
      local AutoAssignTaskMenu = MENU_GROUP_COMMAND:New( self.CommandCenterPositionable, "Assign Task Off", CommandCenterMenu, self.SetAutoAssignTasks, self, false ):SetTime(MenuTime):SetTag("AutoTask")
    end
    CommandCenterMenu:Remove( MenuTime, "AutoTask" )
  end

end



--- Automatically assigns tasks to all TaskGroups.
-- One of the most important roles of the command center is the management of tasks.
-- When this method is used with a parameter true; the command center will scan at regular intervals which players in a slot are not having a task assigned.
-- For those players; the tasking is enabled to assign automatically a task.
-- An Assign Menu will be accessible for the player under the command center menu, to configure the automatic tasking to switched on or off.
-- @param #COMMANDCENTER self
-- @param #boolean AutoAssign true for ON and false or nil for OFF.
function COMMANDCENTER:SetAutoAssignTasks( AutoAssign )

  self.AutoAssignTasks = AutoAssign or false
  
  if self.AutoAssignTasks == true then
    self.autoAssignTasksScheduleID=self:ScheduleRepeat( 10, 30, 0, nil, self.AssignTasks, self )
  else
    self:ScheduleStop()
    -- FF this is not the schedule ID
    --self:ScheduleStop( self.AssignTasks )
  end
  
end

--- Automatically accept tasks for all TaskGroups.
-- When a task is assigned; the mission designer can decide if players are immediately assigned to the task; or they can accept/reject the assigned task.
-- If the tasks are not automatically accepted; the player will receive a message that he needs to access the command center menu and
-- choose from 2 added menu options either to accept or reject the assigned task within 30 seconds.
-- If the task is not accepted within 30 seconds; the task will be cancelled and a new task will be assigned.
-- @param #COMMANDCENTER self
-- @param #boolean AutoAccept true for ON and false or nil for OFF.
function COMMANDCENTER:SetAutoAcceptTasks( AutoAccept )

  self.AutoAcceptTasks = AutoAccept or false
  
end


--- Define the method to be used to assign automatically a task from the available tasks in the mission.
-- There are 3 types of methods that can be applied for the moment:
-- 
--   1. Random - assigns a random task in the mission to the player.
--   2. Distance - assigns a task based on a distance evaluation from the player. The closest are to be assigned first.
--   3. Priority - assigns a task based on the priority as defined by the mission designer, using the SetTaskPriority parameter.
--   
-- The different task classes implement the logic to determine the priority of automatic task assignment to a player, depending on one of the above methods.
-- The method @{Tasking.Task#TASK.GetAutoAssignPriority} calculate the priority of the tasks to be assigned. 
-- @param #COMMANDCENTER self
-- @param #COMMANDCENTER.AutoAssignMethods AutoAssignMethod A selection of an assign method from the COMMANDCENTER.AutoAssignMethods enumeration.
function COMMANDCENTER:SetAutoAssignMethod( AutoAssignMethod )

  self.AutoAssignMethod = AutoAssignMethod or COMMANDCENTER.AutoAssignMethods.Random
  
end

--- Automatically assigns tasks to all TaskGroups.
-- @param #COMMANDCENTER self
function COMMANDCENTER:AssignTasks()

  local GroupSet = self:AddGroups()

  for GroupID, TaskGroup in pairs( GroupSet:GetSet() ) do
    local TaskGroup = TaskGroup -- Wrapper.Group#GROUP
    
    if TaskGroup:IsAlive() then
      self:GetMenu( TaskGroup )
      
      if self:IsGroupAssigned( TaskGroup ) then
      else
        -- Only groups with planes or helicopters will receive automatic tasks.
        -- TODO Workaround DCS-BUG-3 - https://github.com/FlightControl-Master/MOOSE/issues/696
        if TaskGroup:IsAir() then
          self:AssignTask( TaskGroup )
        end
      end
    end
  end

end


--- Get all the Groups active within the command center.
-- @param #COMMANDCENTER self
-- @return Core.Set#SET_GROUP The set of groups active within the command center.
function COMMANDCENTER:AddGroups()

  local GroupSet = SET_GROUP:New()
  
  for MissionID, Mission in pairs( self.Missions ) do
    local Mission = Mission -- Tasking.Mission#MISSION
    GroupSet = Mission:AddGroups( GroupSet )
  end
  
  return GroupSet
end


--- Checks of the TaskGroup has a Task.
-- @param #COMMANDCENTER self
-- @return #boolean When true, the TaskGroup has a Task, otherwise the returned value will be false.
function COMMANDCENTER:IsGroupAssigned( TaskGroup )

  local Assigned = false
  
  for MissionID, Mission in pairs( self.Missions ) do
    local Mission = Mission -- Tasking.Mission#MISSION
    if Mission:IsGroupAssigned( TaskGroup ) then
      Assigned = true
      break
    end
  end
  
  return Assigned
end


--- Checks of the command center has the given MissionGroup.
-- @param #COMMANDCENTER self
-- @param Wrapper.Group#GROUP MissionGroup The group active within one of the missions governed by the command center.
-- @return #boolean
function COMMANDCENTER:HasGroup( MissionGroup )

  local Has = false
  
  for MissionID, Mission in pairs( self.Missions ) do
    local Mission = Mission -- Tasking.Mission#MISSION
    if Mission:HasGroup( MissionGroup ) then
      Has = true
      break
    end      
  end
  
  return Has
end

--- Let the command center send a Message to all players.
-- @param #COMMANDCENTER self
-- @param #string Message The message text.
function COMMANDCENTER:MessageToAll( Message )

    self:GetPositionable():MessageToAll( Message, self.MessageDuration, self:GetName() )

end

--- Let the command center send a message to the MessageGroup.
-- @param #COMMANDCENTER self
-- @param #string Message The message text.
-- @param Wrapper.Group#GROUP MessageGroup The group to receive the message.
function COMMANDCENTER:MessageToGroup( Message, MessageGroup )

  self:GetPositionable():MessageToGroup( Message, self.MessageDuration, MessageGroup, self:GetShortText() )

end

--- Let the command center send a message to the MessageGroup.
-- @param #COMMANDCENTER self
-- @param #string Message The message text.
-- @param Wrapper.Group#GROUP MessageGroup The group to receive the message.
-- @param Core.Message#MESSAGE.MessageType MessageType The type of the message, resulting in automatic time duration and prefix of the message.
function COMMANDCENTER:MessageTypeToGroup( Message, MessageGroup, MessageType )

  self:GetPositionable():MessageTypeToGroup( Message, MessageType, MessageGroup, self:GetShortText() )

end

--- Let the command center send a message to the coalition of the command center.
-- @param #COMMANDCENTER self
-- @param #string Message The message text.
function COMMANDCENTER:MessageToCoalition( Message )

  local CCCoalition = self:GetPositionable():GetCoalition()
    --TODO: Fix coalition bug!
    
    self:GetPositionable():MessageToCoalition( Message, self.MessageDuration, CCCoalition, self:GetShortText() )

end


--- Let the command center send a message of a specified type to the coalition of the command center.
-- @param #COMMANDCENTER self
-- @param #string Message The message text.
-- @param Core.Message#MESSAGE.MessageType MessageType The type of the message, resulting in automatic time duration and prefix of the message.
function COMMANDCENTER:MessageTypeToCoalition( Message, MessageType )

  local CCCoalition = self:GetPositionable():GetCoalition()
    --TODO: Fix coalition bug!
    
    self:GetPositionable():MessageTypeToCoalition( Message, MessageType, CCCoalition, self:GetShortText() )

end


--- Let the command center send a report of the status of all missions to a group.
-- Each Mission is listed, with an indication how many Tasks are still to be completed.
-- @param #COMMANDCENTER self
-- @param Wrapper.Group#GROUP ReportGroup The group to receive the report.
function COMMANDCENTER:ReportSummary( ReportGroup )
  self:F( ReportGroup )

  local Report = REPORT:New()

  -- List the name of the mission.
  local Name = self:GetName()
  Report:Add( string.format( '%s - Report Summary Missions', Name ) )
  
  for MissionID, Mission in pairs( self.Missions ) do
    local Mission = Mission -- Tasking.Mission#MISSION
    Report:Add( " - " .. Mission:ReportSummary( ReportGroup ) )
  end
  
  self:MessageToGroup( Report:Text(), ReportGroup )
end

--- Let the command center send a report of the players of all missions to a group.
-- Each Mission is listed, with an indication how many Tasks are still to be completed.
-- @param #COMMANDCENTER self
-- @param Wrapper.Group#GROUP ReportGroup The group to receive the report.
function COMMANDCENTER:ReportMissionsPlayers( ReportGroup )
  self:F( ReportGroup )

  local Report = REPORT:New()
  
  Report:Add( "Players active in all missions." )

  for MissionID, MissionData in pairs( self.Missions ) do
    local Mission = MissionData -- Tasking.Mission#MISSION
    Report:Add( " - " .. Mission:ReportPlayersPerTask(ReportGroup) )
  end
  
  self:MessageToGroup( Report:Text(), ReportGroup )
end

--- Let the command center send a report of the status of a task to a group.
-- Report the details of a Mission, listing the Mission, and all the Task details.
-- @param #COMMANDCENTER self
-- @param Wrapper.Group#GROUP ReportGroup The group to receive the report.
-- @param Tasking.Task#TASK Task The task to be reported.
function COMMANDCENTER:ReportDetails( ReportGroup, Task )
  self:F( ReportGroup )

  local Report = REPORT:New()
  
  for MissionID, Mission in pairs( self.Missions ) do
    local Mission = Mission -- Tasking.Mission#MISSION
    Report:Add( " - " .. Mission:ReportDetails() )
  end
  
  self:MessageToGroup( Report:Text(), ReportGroup )
end


--- Let the command center flash a report of the status of the subscribed task to a group.
-- @param #COMMANDCENTER self
-- @param Flash #boolean
function COMMANDCENTER:SetFlashStatus( Flash )
  self:F()

  self.FlashStatus = Flash and true
end

--- Duration a command center message is shown.
-- @param #COMMANDCENTER self
-- @param seconds #number
function COMMANDCENTER:SetMessageDuration(seconds)
  self:F()

  self.MessageDuration = 10 or seconds
end
--- **Tasking** - A mission models a goal to be achieved through the execution and completion of tasks by human players.
-- 
-- **Features:**
-- 
--   * A mission has a goal to be achieved, through the execution and completion of tasks of different categories by human players.
--   * A mission manages these tasks.
--   * A mission has a state, that indicates the fase of the mission.
--   * A mission has a menu structure, that facilitates mission reports and tasking menus.
--   * A mission can assign a task to a player.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Tasking.Mission
-- @image Task_Mission.JPG

---
-- @type MISSION
-- @field #MISSION.Clients _Clients
-- @field Core.Menu#MENU_COALITION MissionMenu
-- @field #string MissionBriefing
-- @extends Core.Fsm#FSM

--- Models goals to be achieved and can contain multiple tasks to be executed to achieve the goals.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- A mission contains multiple tasks and can be of different task types.
-- These tasks need to be assigned to human players to be executed.
-- 
-- A mission can have multiple states, which will evolve as the mission progresses during the DCS simulation.
-- 
--   - **IDLE**: The mission is defined, but not started yet. No task has yet been joined by a human player as part of the mission.
--   - **ENGAGED**: The mission is ongoing, players have joined tasks to be executed.
--   - **COMPLETED**: The goals of the mission has been successfully reached, and the mission is flagged as completed.
--   - **FAILED**: For a certain reason, the goals of the mission has not been reached, and the mission is flagged as failed.
--   - **HOLD**: The mission was enaged, but for some reason it has been put on hold.
--   
-- Note that a mission goals need to be checked by a goal check trigger: @{#MISSION.OnBeforeMissionGoals}(), which may return false if the goal has not been reached.
-- This goal is checked automatically by the mission object every x seconds.
-- 
--   - @{#MISSION.Start}() or @{#MISSION.__Start}() will start the mission, and will bring it from **IDLE** state to **ENGAGED** state.
--   - @{#MISSION.Stop}() or @{#MISSION.__Stop}() will stop the mission, and will bring it from **ENGAGED** state to **IDLE** state.
--   - @{#MISSION.Complete}() or @{#MISSION.__Complete}() will complete the mission, and will bring the mission state to **COMPLETED**.
--     Note that the mission must be in state **ENGAGED** to be able to complete the mission.
--   - @{#MISSION.Fail}() or @{#MISSION.__Fail}() will fail the mission, and will bring the mission state to **FAILED**.
--     Note that the mission must be in state **ENGAGED** to be able to fail the mission.
--   - @{#MISSION.Hold}() or @{#MISSION.__Hold}() will hold the mission, and will bring the mission state to **HOLD**.
--     Note that the mission must be in state **ENGAGED** to be able to hold the mission.
--     Re-engage the mission using the engage trigger.
-- 
-- The following sections provide an overview of the most important methods that can be used as part of a mission object.
-- Note that the @{Tasking.CommandCenter} system is using most of these methods to manage the missions in its system.
-- 
-- ## 1. Create a mission object.
--
--   - @{#MISSION.New}(): Creates a new MISSION object.
-- 
-- ## 2. Mission task management.
-- 
-- Missions maintain tasks, which can be added or removed, or enquired.
-- 
--   - @{#MISSION.AddTask}(): Adds a task to the mission.
--   - @{#MISSION.RemoveTask}(): Removes a task from the mission.
-- 
-- ## 3. Mission detailed methods.
-- 
-- Various methods are added to manage missions.
-- 
-- ### 3.1. Naming and description.
-- 
-- There are several methods that can be used to retrieve the properties of a mission:
-- 
--   - Use the method @{#MISSION.GetName}() to retrieve the name of the mission. 
--     This is the name given as part of the @{#MISSION.New}() constructor.
-- 
-- A textual description can be retrieved that provides the mission name to be used within message communication:
-- 
--   - @{#MISSION.GetShortText}() returns the mission name as `Mission "MissionName"`.
--   - @{#MISSION.GetText}() returns the mission  name as `Mission "MissionName (MissionPriority)"`. A longer version including the priority text of the mission.
-- 
-- ### 3.2. Get task information.
-- 
--   - @{#MISSION.GetTasks}(): Retrieves a list of the tasks controlled by the mission.
--   - @{#MISSION.GetTask}(): Retrieves a specific task controlled by the mission.
--   - @{#MISSION.GetTasksRemaining}(): Retrieve a list of the tasks that aren't finished or failed, and are governed by the mission.
--   - @{#MISSION.GetGroupTasks}(): Retrieve a list of the tasks that can be assigned to a @{Wrapper.Group}.
--   - @{#MISSION.GetTaskTypes}(): Retrieve a list of the different task types governed by the mission.
-- 
-- ### 3.3. Get the command center.
-- 
--   - @{#MISSION.GetCommandCenter}(): Retrieves the @{Tasking.CommandCenter} governing the mission.
-- 
-- ### 3.4. Get the groups active in the mission as a @{Core.Set}.
-- 
--   - @{#MISSION.GetGroups}(): Retrieves a @{Core.Set#SET_GROUP} of all the groups active in the mission (as part of the tasks).
--   
-- ### 3.5. Get the names of the players.
-- 
--   - @{#MISSION.GetPlayerNames}(): Retrieves the list of the players that were active within th mission..
-- 
-- ## 4. Menu management.
-- 
-- A mission object is able to manage its own menu structure. Use the @{#MISSION.GetMenu}() and @{#MISSION.SetMenu}() to manage the underlying submenu
-- structure managing the tasks of the mission.
-- 
-- ## 5. Reporting management.
--  
-- Several reports can be generated for a mission, and will return a text string that can be used to display using the @{Core.Message} system.
-- 
--   - @{#MISSION.ReportBriefing}(): Generates the briefing for the mission.
--   - @{#MISSION.ReportOverview}(): Generates an overview of the tasks and status of the mission.
--   - @{#MISSION.ReportDetails}(): Generates a detailed report of the tasks of the mission.
--   - @{#MISSION.ReportSummary}(): Generates a summary report of the tasks of the mission.
--   - @{#MISSION.ReportPlayersPerTask}(): Generates a report showing the active players per task.
--   - @{#MISSION.ReportPlayersProgress}(): Generates a report showing the task progress per player.
-- 
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- @field #MISSION 
MISSION = {
	ClassName = "MISSION",
	Name = "",
	MissionStatus = "PENDING",
	AssignedGroups = {},
}

--- This is the main MISSION declaration method. Each Mission is like the master or a Mission orchestration between, Clients, Tasks, Stages etc.
-- @param #MISSION self
-- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter
-- @param #string MissionName Name of the mission. This name will be used to reference the status of each mission by the players.
-- @param #string MissionPriority String indicating the "priority" of the Mission. e.g. "Primary", "Secondary". It is free format and up to the Mission designer to choose. There are no rules behind this field.
-- @param #string MissionBriefing String indicating the mission briefing to be shown when a player joins a @{Wrapper.Client#CLIENT}.
-- @param DCS#coalition.side MissionCoalition Side of the coalition, i.e. and enumerator @{#DCS.coalition.side} corresponding to RED, BLUE or NEUTRAL.
-- @return #MISSION self
function MISSION:New( CommandCenter, MissionName, MissionPriority, MissionBriefing, MissionCoalition )

  local self = BASE:Inherit( self, FSM:New() ) -- Core.Fsm#FSM

  self:T( { MissionName, MissionPriority, MissionBriefing, MissionCoalition } )
  
  self.CommandCenter = CommandCenter
  CommandCenter:AddMission( self )
  
  self.Name = MissionName
  self.MissionPriority = MissionPriority
  self.MissionBriefing = MissionBriefing
  self.MissionCoalition = MissionCoalition
  
  self.Tasks = {}
  self.TaskNumber = 0
  self.PlayerNames = {} -- These are the players that achieved progress in the mission.

  self:SetStartState( "IDLE" )
  
  self:AddTransition( "IDLE", "Start", "ENGAGED" )
  
  --- OnLeave Transition Handler for State IDLE.
  -- @function [parent=#MISSION] OnLeaveIDLE
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnEnter Transition Handler for State IDLE.
  -- @function [parent=#MISSION] OnEnterIDLE
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  
  --- OnLeave Transition Handler for State ENGAGED.
  -- @function [parent=#MISSION] OnLeaveENGAGED
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnEnter Transition Handler for State ENGAGED.
  -- @function [parent=#MISSION] OnEnterENGAGED
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  
  --- OnBefore Transition Handler for Event Start.
  -- @function [parent=#MISSION] OnBeforeStart
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Start.
  -- @function [parent=#MISSION] OnAfterStart
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Start.
  -- @function [parent=#MISSION] Start
  -- @param #MISSION self
  
  --- Asynchronous Event Trigger for Event Start.
  -- @function [parent=#MISSION] __Start
  -- @param #MISSION self
  -- @param #number Delay The delay in seconds.
  
  self:AddTransition( "ENGAGED", "Stop", "IDLE" )
  
  --- OnLeave Transition Handler for State IDLE.
  -- @function [parent=#MISSION] OnLeaveIDLE
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnEnter Transition Handler for State IDLE.
  -- @function [parent=#MISSION] OnEnterIDLE
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  
  --- OnBefore Transition Handler for Event Stop.
  -- @function [parent=#MISSION] OnBeforeStop
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Stop.
  -- @function [parent=#MISSION] OnAfterStop
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Stop.
  -- @function [parent=#MISSION] Stop
  -- @param #MISSION self
  
  --- Asynchronous Event Trigger for Event Stop.
  -- @function [parent=#MISSION] __Stop
  -- @param #MISSION self
  -- @param #number Delay The delay in seconds.
  
  self:AddTransition( "ENGAGED", "Complete", "COMPLETED" )
  
  --- OnLeave Transition Handler for State COMPLETED.
  -- @function [parent=#MISSION] OnLeaveCOMPLETED
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnEnter Transition Handler for State COMPLETED.
  -- @function [parent=#MISSION] OnEnterCOMPLETED
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  
  --- OnBefore Transition Handler for Event Complete.
  -- @function [parent=#MISSION] OnBeforeComplete
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Complete.
  -- @function [parent=#MISSION] OnAfterComplete
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Complete.
  -- @function [parent=#MISSION] Complete
  -- @param #MISSION self
  
  --- Asynchronous Event Trigger for Event Complete.
  -- @function [parent=#MISSION] __Complete
  -- @param #MISSION self
  -- @param #number Delay The delay in seconds.
  
  self:AddTransition( "*", "Fail", "FAILED" )
  
  --- OnLeave Transition Handler for State FAILED.
  -- @function [parent=#MISSION] OnLeaveFAILED
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnEnter Transition Handler for State FAILED.
  -- @function [parent=#MISSION] OnEnterFAILED
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  
  --- OnBefore Transition Handler for Event Fail.
  -- @function [parent=#MISSION] OnBeforeFail
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  -- @return #boolean Return false to cancel Transition.
  
  --- OnAfter Transition Handler for Event Fail.
  -- @function [parent=#MISSION] OnAfterFail
  -- @param #MISSION self
  -- @param #string From The From State string.
  -- @param #string Event The Event string.
  -- @param #string To The To State string.
  	
  --- Synchronous Event Trigger for Event Fail.
  -- @function [parent=#MISSION] Fail
  -- @param #MISSION self
  
  --- Asynchronous Event Trigger for Event Fail.
  -- @function [parent=#MISSION] __Fail
  -- @param #MISSION self
  -- @param #number Delay The delay in seconds.
  
  
  self:AddTransition( "*", "MissionGoals", "*" )
  
  --- MissionGoals Handler OnBefore for MISSION
  -- @function [parent=#MISSION] OnBeforeMissionGoals
  -- @param #MISSION self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @return #boolean
  
  --- MissionGoals Handler OnAfter for MISSION
  -- @function [parent=#MISSION] OnAfterMissionGoals
  -- @param #MISSION self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  
  --- MissionGoals Trigger for MISSION
  -- @function [parent=#MISSION] MissionGoals
  -- @param #MISSION self
  
  --- MissionGoals Asynchronous Trigger for MISSION
  -- @function [parent=#MISSION] __MissionGoals
  -- @param #MISSION self
  -- @param #number Delay
  
	-- Private  implementations
	
	CommandCenter:SetMenu()

	return self
end




--- FSM function for a MISSION
-- @param #MISSION self
-- @param #string From
-- @param #string Event
-- @param #string To
function MISSION:onenterCOMPLETED( From, Event, To )

  self:GetCommandCenter():MessageTypeToCoalition( self:GetText() .. " has been completed! Good job guys!", MESSAGE.Type.Information )
end

--- Gets the mission name.
-- @param #MISSION self
-- @return #MISSION self
function MISSION:GetName()
  return self.Name
end


--- Gets the mission text.
-- @param #MISSION self
-- @return #MISSION self
function MISSION:GetText()
  return string.format( 'Mission "%s (%s)"', self.Name, self.MissionPriority )
end


--- Gets the short mission text.
-- @param #MISSION self
-- @return #MISSION self
function MISSION:GetShortText()
  return string.format( 'Mission "%s"', self.Name )
end


--- Add a Unit to join the Mission.
-- For each Task within the Mission, the Unit is joined with the Task.
-- If the Unit was not part of a Task in the Mission, false is returned.
-- If the Unit is part of a Task in the Mission, true is returned.
-- @param #MISSION self
-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission.
-- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission.
-- @return #boolean true if Unit is part of a Task in the Mission.
function MISSION:JoinUnit( PlayerUnit, PlayerGroup )
  self:T( { Mission = self:GetName(), PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } )
  
  local PlayerUnitAdded = false
  
  for TaskID, Task in pairs( self:GetTasks() ) do
    local Task = Task -- Tasking.Task#TASK
    if Task:JoinUnit( PlayerUnit, PlayerGroup ) then
      PlayerUnitAdded = true
    end
  end
  
  return PlayerUnitAdded
end

--- Aborts a PlayerUnit from the Mission.
-- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned.
-- If the Unit was not part of a Task in the Mission, false is returned.
-- If the Unit is part of a Task in the Mission, true is returned.
-- @param #MISSION self
-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission.
-- @return #MISSION
function MISSION:AbortUnit( PlayerUnit )
  self:F( { PlayerUnit = PlayerUnit } )
  
  for TaskID, Task in pairs( self:GetTasks() ) do
    local Task = Task -- Tasking.Task#TASK
    local PlayerGroup = PlayerUnit:GetGroup()
    Task:AbortGroup( PlayerGroup )
  end
  
  return self
end

--- Handles a crash of a PlayerUnit from the Mission.
-- For each Task within the Mission, the PlayerUnit is removed from Task where it is assigned.
-- If the Unit was not part of a Task in the Mission, false is returned.
-- If the Unit is part of a Task in the Mission, true is returned.
-- @param #MISSION self
-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player crashing.
-- @return #MISSION
function MISSION:CrashUnit( PlayerUnit )
  self:F( { PlayerUnit = PlayerUnit } )
  
  for TaskID, Task in pairs( self:GetTasks() ) do
    local Task = Task -- Tasking.Task#TASK
    local PlayerGroup = PlayerUnit:GetGroup()
    Task:CrashGroup( PlayerGroup )
  end
  
  return self
end

--- Add a scoring to the mission.
-- @param #MISSION self
-- @return #MISSION self
function MISSION:AddScoring( Scoring )
  self.Scoring = Scoring
  return self
end

--- Get the scoring object of a mission.
-- @param #MISSION self
-- @return #SCORING Scoring
function MISSION:GetScoring()
  return self.Scoring
end

--- Gets the groups for which TASKS are given in the mission
-- @param #MISSION self
-- @param Core.Set#SET_GROUP GroupSet
-- @return Core.Set#SET_GROUP
function MISSION:GetGroups()
  
  return self:AddGroups()
  
end

--- Adds the groups for which TASKS are given in the mission
-- @param #MISSION self
-- @param Core.Set#SET_GROUP GroupSet
-- @return Core.Set#SET_GROUP
function MISSION:AddGroups( GroupSet )
  
  GroupSet = GroupSet or SET_GROUP:New()
  
  for TaskID, Task in pairs( self:GetTasks() ) do
    local Task = Task -- Tasking.Task#TASK
    GroupSet = Task:AddGroups( GroupSet )
  end
  
  return GroupSet
  
end


--- Sets the Planned Task menu.
-- @param #MISSION self
-- @param #number MenuTime
function MISSION:SetMenu( MenuTime )
  self:F( { self:GetName(), MenuTime } )
  
  local MenuCount = {}
  --for TaskID, Task in UTILS.spairs( self:GetTasks(), function( t, a, b ) return t[a]:ReportOrder( ReportGroup ) <  t[b]:ReportOrder( ReportGroup ) end  ) do
  for TaskID, Task in pairs( self:GetTasks() ) do
    local Task = Task -- Tasking.Task#TASK
    local TaskType = Task:GetType()
    MenuCount[TaskType] = MenuCount[TaskType] or 1
    if MenuCount[TaskType] <= 10 then
      Task:SetMenu( MenuTime )
      MenuCount[TaskType] = MenuCount[TaskType] + 1
    end
  end
end

--- Removes the Planned Task menu.
-- @param #MISSION self
-- @param #number MenuTime
function MISSION:RemoveMenu( MenuTime )
  self:F( { self:GetName(), MenuTime } )
  
  for _, Task in pairs( self:GetTasks() ) do
    local Task = Task -- Tasking.Task#TASK
    Task:RemoveMenu( MenuTime )
  end
end



do -- Group Assignment

  --- Returns if the @{Tasking.Mission} is assigned to the Group.
  -- @param #MISSION self
  -- @param Wrapper.Group#GROUP MissionGroup
  -- @return #boolean
  function MISSION:IsGroupAssigned( MissionGroup )
  
    local MissionGroupName = MissionGroup:GetName()
    
    if self.AssignedGroups[MissionGroupName] == MissionGroup then
      self:T2( { "Mission is assigned to:", MissionGroup:GetName() } )
      return true
    end
    
    self:T2( { "Mission is not assigned to:", MissionGroup:GetName() } )
    return false
  end
  
  
  --- Set @{Wrapper.Group} assigned to the @{Tasking.Mission}.
  -- @param #MISSION self
  -- @param Wrapper.Group#GROUP MissionGroup
  -- @return #MISSION
  function MISSION:SetGroupAssigned( MissionGroup )
  
    local MissionName = self:GetName()
    local MissionGroupName = MissionGroup:GetName()
  
    self.AssignedGroups[MissionGroupName] = MissionGroup
    self:T( string.format( "Mission %s is assigned to %s", MissionName, MissionGroupName ) )
    
    return self
  end
  
  --- Clear the @{Wrapper.Group} assignment from the @{Tasking.Mission}.
  -- @param #MISSION self
  -- @param Wrapper.Group#GROUP MissionGroup
  -- @return #MISSION
  function MISSION:ClearGroupAssignment( MissionGroup )
  
    local MissionName = self:GetName()
    local MissionGroupName = MissionGroup:GetName()
  
    self.AssignedGroups[MissionGroupName] = nil
    --self:E( string.format( "Mission %s is unassigned to %s", MissionName, MissionGroupName ) )
    
    return self
  end
  
end

--- Gets the COMMANDCENTER.
-- @param #MISSION self
-- @return Tasking.CommandCenter#COMMANDCENTER
function MISSION:GetCommandCenter()
  return self.CommandCenter
end


--- Removes a Task menu.
-- @param #MISSION self
-- @param Tasking.Task#TASK Task
-- @return #MISSION self
function MISSION:RemoveTaskMenu( Task )
    
  Task:RemoveMenu()  
end


--- Gets the root mission menu for the TaskGroup. Obsolete?! Originally no reference to TaskGroup parameter!
-- @param #MISSION self
-- @param Wrapper.Group#GROUP TaskGroup Task group.
-- @return Core.Menu#MENU_COALITION self
function MISSION:GetRootMenu( TaskGroup ) -- R2.2

  local CommandCenter = self:GetCommandCenter()
  local CommandCenterMenu = CommandCenter:GetMenu( TaskGroup )

  local MissionName = self:GetText()
  --local MissionMenu = CommandCenterMenu:GetMenu( MissionName )
  
  self.MissionMenu = MENU_COALITION:New( self.MissionCoalition, MissionName, CommandCenterMenu )

  return self.MissionMenu
end

--- Gets the mission menu for the TaskGroup.
-- @param #MISSION self
-- @param Wrapper.Group#GROUP TaskGroup Task group.
-- @return Core.Menu#MENU_COALITION self
function MISSION:GetMenu( TaskGroup ) -- R2.1 -- Changed Menu Structure

  local CommandCenter = self:GetCommandCenter()
  local CommandCenterMenu = CommandCenter:GetMenu( TaskGroup )

  self.MissionGroupMenu = self.MissionGroupMenu or {}
  self.MissionGroupMenu[TaskGroup] = self.MissionGroupMenu[TaskGroup] or {}
  
  local GroupMenu = self.MissionGroupMenu[TaskGroup]
  
  local MissionText = self:GetText()
  self.MissionMenu = MENU_GROUP:New( TaskGroup, MissionText, CommandCenterMenu )
  
  GroupMenu.BriefingMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Mission Briefing", self.MissionMenu, self.MenuReportBriefing, self, TaskGroup )

  GroupMenu.MarkTasks = MENU_GROUP_COMMAND:New( TaskGroup, "Mark Task Locations on Map", self.MissionMenu, self.MarkTargetLocations, self, TaskGroup )
  GroupMenu.TaskReportsMenu = MENU_GROUP:New( TaskGroup, "Task Reports", self.MissionMenu )
  GroupMenu.ReportTasksMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Tasks Summary", GroupMenu.TaskReportsMenu, self.MenuReportTasksSummary, self, TaskGroup )
  GroupMenu.ReportPlannedTasksMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Planned Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Planned" )
  GroupMenu.ReportAssignedTasksMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Assigned Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Assigned" )
  GroupMenu.ReportSuccessTasksMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Successful Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Success" )
  GroupMenu.ReportFailedTasksMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Failed Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Failed" )
  GroupMenu.ReportHeldTasksMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Held Tasks", GroupMenu.TaskReportsMenu, self.MenuReportTasksPerStatus, self, TaskGroup, "Hold" )
  
  GroupMenu.PlayerReportsMenu = MENU_GROUP:New( TaskGroup, "Statistics Reports", self.MissionMenu )
  GroupMenu.ReportMissionHistory = MENU_GROUP_COMMAND:New( TaskGroup, "Report Mission Progress", GroupMenu.PlayerReportsMenu, self.MenuReportPlayersProgress, self, TaskGroup )
  GroupMenu.ReportPlayersPerTaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, "Report Players per Task", GroupMenu.PlayerReportsMenu, self.MenuReportPlayersPerTask, self, TaskGroup )
  
  return self.MissionMenu
end




--- Get the TASK identified by the TaskNumber from the Mission. This function is useful in GoalFunctions.
-- @param #string TaskName The Name of the @{Tasking.Task} within the @{Tasking.Mission}.
-- @return Tasking.Task#TASK The Task
-- @return #nil Returns nil if no task was found.
function MISSION:GetTask( TaskName )
  self:F( { TaskName } )

  return self.Tasks[TaskName]
end


--- Return the next @{Tasking.Task} ID to be completed within the @{Tasking.Mission}. 
-- @param #MISSION self
-- @param Tasking.Task#TASK Task is the @{Tasking.Task} object.
-- @return Tasking.Task#TASK The task added.
function MISSION:GetNextTaskID( Task )

  self.TaskNumber = self.TaskNumber + 1

  return self.TaskNumber
end


--- Register a @{Tasking.Task} to be completed within the @{Tasking.Mission}. 
-- Note that there can be multiple @{Tasking.Task}s registered to be completed. 
-- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached.
-- @param #MISSION self
-- @param Tasking.Task#TASK Task is the @{Tasking.Task} object.
-- @return Tasking.Task#TASK The task added.
function MISSION:AddTask( Task )

  local TaskName = Task:GetTaskName()
  self:T( { "==> Adding TASK ", MissionName = self:GetName(), TaskName = TaskName } )

  self.Tasks[TaskName] = Task
  
  self:GetCommandCenter():SetMenu()

  return Task
end


--- Removes a @{Tasking.Task} to be completed within the @{Tasking.Mission}. 
-- Note that there can be multiple @{Tasking.Task}s registered to be completed. 
-- Each Task can be set a certain Goals. The Mission will not be completed until all Goals are reached.
-- @param #MISSION self
-- @param Tasking.Task#TASK Task is the @{Tasking.Task} object.
-- @return #nil The cleaned Task reference.
function MISSION:RemoveTask( Task )

  local TaskName = Task:GetTaskName()
  self:T( { "<== Removing TASK ", MissionName = self:GetName(), TaskName = TaskName } )

  self:F( TaskName )
  self.Tasks[TaskName] = self.Tasks[TaskName] or { n = 0 }

  -- Ensure everything gets garbarge collected.
  self.Tasks[TaskName] = nil 
  Task = nil
  
  collectgarbage()

  self:GetCommandCenter():SetMenu()
  
  return nil
end

--- Is the @{Tasking.Mission} **COMPLETED**.
-- @param #MISSION self
-- @return #boolean
function MISSION:IsCOMPLETED()
  return self:Is( "COMPLETED" )
end

--- Is the @{Tasking.Mission} **IDLE**.
-- @param #MISSION self
-- @return #boolean
function MISSION:IsIDLE()
  return self:Is( "IDLE" )
end

--- Is the @{Tasking.Mission} **ENGAGED**.
-- @param #MISSION self
-- @return #boolean
function MISSION:IsENGAGED()
  return self:Is( "ENGAGED" )
end

--- Is the @{Tasking.Mission} **FAILED**.
-- @param #MISSION self
-- @return #boolean
function MISSION:IsFAILED()
  return self:Is( "FAILED" )
end

--- Is the @{Tasking.Mission} **HOLD**.
-- @param #MISSION self
-- @return #boolean
function MISSION:IsHOLD()
  return self:Is( "HOLD" )
end

--- Validates if the Mission has a Group
-- @param #MISSION
-- @return #boolean true if the Mission has a Group.
function MISSION:HasGroup( TaskGroup )
  local Has = false
  
  for TaskID, Task in pairs( self:GetTasks() ) do
    local Task = Task -- Tasking.Task#TASK
    if Task:HasGroup( TaskGroup ) then
      Has = true
      break
    end
  end
  
  return Has
end

-- @param #MISSION self
-- @return #number
function MISSION:GetTasksRemaining()
  -- Determine how many tasks are remaining.
  local TasksRemaining = 0
  for TaskID, Task in pairs( self:GetTasks() ) do
    local Task = Task -- Tasking.Task#TASK
    if Task:IsStateSuccess() or Task:IsStateFailed() then
    else
      TasksRemaining = TasksRemaining + 1
    end
  end
  return TasksRemaining
end

-- @param #MISSION self
-- @return #number
function MISSION:GetTaskTypes()
  -- Determine how many tasks are remaining.
  local TaskTypeList = {}
  local TasksRemaining = 0
  for TaskID, Task in pairs( self:GetTasks() ) do
    local Task = Task -- Tasking.Task#TASK
    local TaskType = Task:GetType()
    TaskTypeList[TaskType] = TaskType
  end
  return TaskTypeList
end


function MISSION:AddPlayerName( PlayerName )
  self.PlayerNames = self.PlayerNames or {}
  self.PlayerNames[PlayerName] = PlayerName
  return self
end

function MISSION:GetPlayerNames()
  return self.PlayerNames
end


--- Create a briefing report of the Mission.
-- @param #MISSION self
-- @return #string
function MISSION:ReportBriefing()

  local Report = REPORT:New()

  -- List the name of the mission.
  local Name = self:GetText()
  
  -- Determine the status of the mission.
  local Status = "<" .. self:GetState() .. ">"
  
  Report:Add( string.format( '%s - %s - Mission Briefing Report', Name, Status ) )

  Report:Add( self.MissionBriefing )
  
  return Report:Text()
end


----- Create a status report of the Mission.
---- This reports provides a one liner of the mission status. It indicates how many players and how many Tasks.
---- 
----     Mission "<MissionName>" - Status "<MissionStatus>"
----      - Task Types: <TaskType>, <TaskType>
----      - <xx> Planned Tasks (xp)
----      - <xx> Assigned Tasks(xp)
----      - <xx> Success Tasks (xp)
----      - <xx> Hold Tasks (xp)
----      - <xx> Cancelled Tasks (xp)
----      - <xx> Aborted Tasks (xp)
----      - <xx> Failed Tasks (xp)
---- 
-- @param #MISSION self
---- @return #string
--function MISSION:ReportSummary()
--
--  local Report = REPORT:New()
--
--  -- List the name of the mission.
--  local Name = self:GetText()
--  
--  -- Determine the status of the mission.
--  local Status = "<" .. self:GetState() .. ">"
--
--  Report:Add( string.format( '%s - Status "%s"', Name, Status ) )
--  
--  local TaskTypes = self:GetTaskTypes()
--  
--  Report:Add( string.format( " - Task Types: %s", table.concat(TaskTypes, ", " ) ) )
--  
--  local TaskStatusList = { "Planned", "Assigned", "Success", "Hold", "Cancelled", "Aborted", "Failed" }
--  
--  for TaskStatusID, TaskStatus in pairs( TaskStatusList ) do
--    local TaskCount = 0
--    local TaskPlayerCount = 0 
--    -- Determine how many tasks are remaining.
--    for TaskID, Task in pairs( self:GetTasks() ) do
--      local Task = Task -- Tasking.Task#TASK
--      if Task:Is( TaskStatus ) then
--        TaskCount = TaskCount + 1
--        TaskPlayerCount = TaskPlayerCount + Task:GetPlayerCount()
--      end
--    end
--    if TaskCount > 0 then
--      Report:Add( string.format( " - %02d %s Tasks (%dp)", TaskCount, TaskStatus, TaskPlayerCount ) )
--    end
--  end
--
--  return Report:Text()
--end


--- Create an active player report of the Mission.
-- This reports provides a one liner of the mission status. It indicates how many players and how many Tasks.
-- 
--     Mission "<MissionName>" - <MissionStatus> - Active Players Report
--      - Player "<PlayerName>: Task <TaskName> <TaskStatus>, Task <TaskName> <TaskStatus>
--      - Player <PlayerName>: Task <TaskName> <TaskStatus>, Task <TaskName> <TaskStatus>
--      - ..
-- 
-- @param #MISSION self
-- @return #string
function MISSION:ReportPlayersPerTask( ReportGroup )

  local Report = REPORT:New()

  -- List the name of the mission.
  local Name = self:GetText()
  
  -- Determine the status of the mission.
  local Status = "<" .. self:GetState() .. ">"

  Report:Add( string.format( '%s - %s - Players per Task Report', Name, Status ) )
  
  local PlayerList = {}
  
  -- Determine how many tasks are remaining.
  for TaskID, Task in pairs( self:GetTasks() ) do
    local Task = Task -- Tasking.Task#TASK
    local PlayerNames = Task:GetPlayerNames()
    for PlayerName, PlayerGroup in pairs( PlayerNames ) do
      PlayerList[PlayerName] = Task:GetName()
    end
    
  end

  for PlayerName, TaskName in pairs( PlayerList ) do
    Report:Add( string.format( ' - Player (%s): Task "%s"', PlayerName, TaskName ) )
  end
  
  return Report:Text()
end

--- Create an Mission Progress report of the Mission.
-- This reports provides a one liner per player of the mission achievements per task.
-- 
--     Mission "<MissionName>" - <MissionStatus> - Active Players Report
--      - Player <PlayerName>: Task <TaskName> <TaskStatus>: <Progress>
--      - Player <PlayerName>: Task <TaskName> <TaskStatus>: <Progress>
--      - ..
-- 
-- @param #MISSION self
-- @return #string
function MISSION:ReportPlayersProgress( ReportGroup )

  local Report = REPORT:New()

  -- List the name of the mission.
  local Name = self:GetText()
  
  -- Determine the status of the mission.
  local Status = "<" .. self:GetState() .. ">"

  Report:Add( string.format( '%s - %s - Players per Task Progress Report', Name, Status ) )
  
  local PlayerList = {}
  
  -- Determine how many tasks are remaining.
  for TaskID, Task in pairs( self:GetTasks() ) do
    local Task = Task -- Tasking.Task#TASK
    local TaskName = Task:GetName()
    local Goal = Task:GetGoal()
    PlayerList[TaskName] = PlayerList[TaskName] or {}
    if Goal then
      local TotalContributions = Goal:GetTotalContributions()
      local PlayerContributions = Goal:GetPlayerContributions()
      self:F( { TotalContributions = TotalContributions, PlayerContributions = PlayerContributions } )
      for PlayerName, PlayerContribution in pairs( PlayerContributions ) do
         PlayerList[TaskName][PlayerName] = string.format( 'Player (%s): Task "%s": %d%%', PlayerName, TaskName, PlayerContributions[PlayerName] * 100 / TotalContributions )       
      end
    else
      PlayerList[TaskName]["_"] = string.format( 'Player (---): Task "%s": %d%%', TaskName, 0 )
    end    
  end

  for TaskName, TaskData in pairs( PlayerList ) do
    for PlayerName, TaskText in pairs( TaskData ) do
      Report:Add( string.format( ' - %s', TaskText ) )
    end
  end
  
  return Report:Text()
end


--- Mark all the target locations on the Map.
-- @param #MISSION self
-- @param Wrapper.Group#GROUP ReportGroup
-- @return #string
function MISSION:MarkTargetLocations( ReportGroup )

  local Report = REPORT:New()

  -- List the name of the mission.
  local Name = self:GetText()
  
  -- Determine the status of the mission.
  local Status = "<" .. self:GetState() .. ">"
  
  Report:Add( string.format( '%s - %s - All Tasks are marked on the map. Select a Task from the Mission Menu and Join the Task!!!', Name, Status ) )

  -- Determine how many tasks are remaining.
  for TaskID, Task in UTILS.spairs( self:GetTasks(), function( t, a, b ) return t[a]:ReportOrder( ReportGroup ) <  t[b]:ReportOrder( ReportGroup ) end  ) do
    local Task = Task -- Tasking.Task#TASK
    Task:MenuMarkToGroup( ReportGroup )
  end
  
  return Report:Text()
end


--- Create a summary report of the Mission (one line).
-- @param #MISSION self
-- @param Wrapper.Group#GROUP ReportGroup
-- @return #string
function MISSION:ReportSummary( ReportGroup )

  local Report = REPORT:New()

  -- List the name of the mission.
  local Name = self:GetText()
  
  -- Determine the status of the mission.
  local Status = "<" .. self:GetState() .. ">"
  
  Report:Add( string.format( '%s - %s - Task Overview Report', Name, Status ) )

  -- Determine how many tasks are remaining.
  for TaskID, Task in UTILS.spairs( self:GetTasks(), function( t, a, b ) return t[a]:ReportOrder( ReportGroup ) <  t[b]:ReportOrder( ReportGroup ) end  ) do
    local Task = Task -- Tasking.Task#TASK
    Report:Add( "- " .. Task:ReportSummary( ReportGroup ) )
  end
  
  return Report:Text()
end

--- Create a overview report of the Mission (multiple lines).
-- @param #MISSION self
-- @return #string
function MISSION:ReportOverview( ReportGroup, TaskStatus )

  self:F( { TaskStatus = TaskStatus } )

  local Report = REPORT:New()

  -- List the name of the mission.
  local Name = self:GetText()
  
  -- Determine the status of the mission.
  local Status = "<" .. self:GetState() .. ">"

  Report:Add( string.format( '%s - %s - %s Tasks Report', Name, Status, TaskStatus ) )
  
  -- Determine how many tasks are remaining.
  local Tasks = 0
  for TaskID, Task in UTILS.spairs( self:GetTasks(), function( t, a, b ) return t[a]:ReportOrder( ReportGroup ) <  t[b]:ReportOrder( ReportGroup ) end  ) do
    local Task = Task -- Tasking.Task#TASK
    if Task:Is( TaskStatus ) then
      Report:Add( string.rep( "-", 140 ) )
      Report:Add( Task:ReportOverview( ReportGroup ) )
    end
    Tasks = Tasks + 1
    if Tasks >= 8 then
      break
    end
  end

  return Report:Text()
end

--- Create a detailed report of the Mission, listing all the details of the Task.
-- @param #MISSION self
-- @return #string
function MISSION:ReportDetails( ReportGroup )

  local Report = REPORT:New()
  
  -- List the name of the mission.
  local Name = self:GetText()
  
  -- Determine the status of the mission.
  local Status = "<" .. self:GetState() .. ">"
  
  Report:Add( string.format( '%s - %s - Task Detailed Report', Name, Status ) )
  
  -- Determine how many tasks are remaining.
  local TasksRemaining = 0
  for TaskID, Task in pairs( self:GetTasks() ) do
    local Task = Task -- Tasking.Task#TASK
    Report:Add( string.rep( "-", 140 ) )
    Report:Add( Task:ReportDetails( ReportGroup ) )
  end

  return Report:Text()
end

--- Get all the TASKs from the Mission. This function is useful in GoalFunctions.
-- @return {TASK,...} Structure of TASKS with the @{Tasking.Task#TASK} number as the key.
-- @usage
-- -- Get Tasks from the Mission.
-- Tasks = Mission:GetTasks()
-- env.info( "Task 2 Completion = " .. Tasks[2]:GetGoalPercentage() .. "%" )
function MISSION:GetTasks()

	return self.Tasks or {}
end

--- Get the relevant tasks of a TaskGroup.
-- @param #MISSION
-- @param Wrapper.Group#GROUP TaskGroup
-- @return #list<Tasking.Task#TASK>
function MISSION:GetGroupTasks( TaskGroup )

  local Tasks = {}
  
  for TaskID, Task in pairs( self:GetTasks() ) do
    local Task = Task -- Tasking.Task#TASK
    if Task:HasGroup( TaskGroup ) then
      Tasks[#Tasks+1] = Task
    end
  end
  
  return Tasks
end


--- Reports the briefing.
-- @param #MISSION self
-- @param Wrapper.Group#GROUP ReportGroup The group to which the report needs to be sent.
function MISSION:MenuReportBriefing( ReportGroup )

  local Report = self:ReportBriefing()
  
  self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Briefing )
end


--- Mark all the targets of the Mission on the Map.
-- @param #MISSION self
-- @param Wrapper.Group#GROUP ReportGroup
function MISSION:MenuMarkTargetLocations( ReportGroup )

  local Report = self:MarkTargetLocations( ReportGroup )
  
  self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview )
end



--- Report the task summary.
-- @param #MISSION self
-- @param Wrapper.Group#GROUP ReportGroup
function MISSION:MenuReportTasksSummary( ReportGroup )

  local Report = self:ReportSummary( ReportGroup )
  
  self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview )
end




-- @param #MISSION self
-- @param #string TaskStatus The status
-- @param Wrapper.Group#GROUP ReportGroup
function MISSION:MenuReportTasksPerStatus( ReportGroup, TaskStatus )

  local Report = self:ReportOverview( ReportGroup, TaskStatus )
  
  self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview )
end


-- @param #MISSION self
-- @param Wrapper.Group#GROUP ReportGroup
function MISSION:MenuReportPlayersPerTask( ReportGroup )

  local Report = self:ReportPlayersPerTask()
  
  self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview )
end

-- @param #MISSION self
-- @param Wrapper.Group#GROUP ReportGroup
function MISSION:MenuReportPlayersProgress( ReportGroup )

  local Report = self:ReportPlayersProgress()
  
  self:GetCommandCenter():MessageTypeToGroup( Report, ReportGroup, MESSAGE.Type.Overview )
end





--- **Tasking** - A task object governs the main engine to administer human taskings.
-- 
-- **Features:**
-- 
--   * A base class for other task classes filling in the details and making a concrete task process.
--   * Manage the overall task execution, following-up the progression made by the pilots and actors.
--   * Provide a mechanism to set a task status, depending on the progress made within the task.
--   * Manage a task briefing.
--   * Manage the players executing the task.
--   * Manage the task menu system.
--   * Manage the task goal and scoring.
-- 
-- ===
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- # 1) Tasking from a player perspective.
-- 
-- Tasking can be controlled by using the "other" menu in the radio menu of the player group.
-- 
-- ![Other Menu](../Tasking/Menu_Main.JPG)
-- 
-- ## 1.1) Command Centers govern multiple Missions.
-- 
-- Depending on the tactical situation, your coalition may have one (or multiple) command center(s).
-- These command centers govern one (or multiple) mission(s).
-- 
-- For each command center, there will be a separate **Command Center Menu** that focuses on the missions governed by that command center.
-- 
-- ![Command Center](../Tasking/Menu_CommandCenter.JPG)
-- 
-- In the above example menu structure, there is one command center with the name **`[Lima]`**.
-- The command center has one @{Tasking.Mission}, named **`"Overlord"`** with **`High`** priority.
-- 
-- ## 1.2) Missions govern multiple Tasks.
-- 
-- A mission has a mission goal to be achieved by the players within the coalition.
-- The mission goal is actually dependent on the tactical situation of the overall battlefield and the conditions set to achieve the goal.
-- So a mission can be much more than just shoot stuff ... It can be a combination of different conditions or events to complete a mission goal.
-- 
-- A mission can be in a specific state during the simulation run. For more information about these states, please check the @{Tasking.Mission} section.
-- 
-- To achieve the mission goal, a mission administers @{#TASK}s that are set to achieve the mission goal by the human players.
-- Each of these tasks can be **dynamically created** using a task dispatcher, or **coded** by the mission designer.
-- Each mission has a separate **Mission Menu**, that focuses on the administration of these tasks.
-- 
-- On top, a mission has a mission briefing, can help to allocate specific points of interest on the map, and provides various reports.
-- 
-- ![Mission](../Tasking/Menu_Mission.JPG)
-- 
-- The above shows a mission menu in detail of **`"Overlord"`**.
-- 
-- The two other menus are related to task assignment. Which will be detailed later.
-- 
-- ### 1.2.1) Mission briefing.
-- 
-- The task briefing will show a message containing a description of the mission goal, and other tactical information.
-- 
-- ![Mission](../Tasking/Report_Briefing.JPG)
-- 
-- ### 1.2.2) Mission Map Locations.
-- 
-- Various points of interest as part of the mission can be indicated on the map using the *Mark Task Locations on Map* menu.
-- As a result, the map will contain various points of interest for the player (group).
-- 
-- ![Mission](../Tasking/Report_Mark_Task_Location.JPG)
-- 
-- ### 1.2.3) Mission Task Reports.
-- 
-- Various reports can be generated on the status of each task governed within the mission.
-- 
-- ![Mission](../Tasking/Report_Task_Summary.JPG)
-- 
-- The Task Overview Report will show each task, with its task status and a short coordinate information.
-- 
-- ![Mission](../Tasking/Report_Tasks_Planned.JPG)
-- 
-- The other Task Menus will show for each task more details, for example here the planned tasks report. 
-- Note that the order of the tasks are shortest distance first to the unit position seated by the player.
-- 
-- ### 1.2.4) Mission Statistics.
-- 
-- Various statistics can be displayed regarding the mission.
-- 
-- ![Mission](../Tasking/Report_Statistics_Progress.JPG)
-- 
-- A statistic report on the progress of the mission. Each task achievement will increase the % to 100% as a goal to complete the task.
-- 
-- ## 1.3) Join a Task.
-- 
-- The mission menu contains a very important option, that is to join a task governed within the mission.
-- In order to join a task, select the **Join Planned Task** menu, and a new menu will be given.
-- 
-- ![Mission](../Tasking/Menu_Join_Planned_Tasks.JPG)
-- 
-- A mission governs multiple tasks, as explained earlier. Each task is of a certain task type.
-- This task type was introduced to have some sort of task classification system in place for the player.
-- A short acronym is shown that indicates the task type. The meaning of each acronym can be found in the task types explanation.
-- 
-- ![Mission](../Tasking/Menu_Join_Tasks.JPG)
-- 
-- When the player selects a task type, a list of the available tasks of that type are listed...
-- In this case the **`SEAD`** task type was selected and a list of available **`SEAD`** tasks can be selected.
-- 
-- ![Mission](../Tasking/Menu_Join_Planned_Task.JPG)
-- 
-- A new list of menu options are now displayed that allow to join the task selected, but also to obtain first some more information on the task.
-- 
-- ### 1.3.1) Report Task Details.
-- 
-- ![Mission](../Tasking/Report_Task_Detailed.JPG)
-- 
-- When selected, a message is displayed that shows detailed information on the task, like the coordinate, enemy target information, threat level etc.
-- 
-- ### 1.3.2) Mark Task Location on Map.
-- 
-- ![Mission](../Tasking/Report_Task_Detailed.JPG)
-- 
-- When selected, the target location on the map is indicated with specific information on the task.
-- 
-- ### 1.3.3) Join Task.
-- 
-- ![Mission](../Tasking/Report_Task_Detailed.JPG)
-- 
-- By joining a task, the player will indicate that the task is assigned to him, and the task is started.
-- The Command Center will communicate several task details to the player and the coalition of the player.
-- 
-- ## 1.4) Task Control and Actions.
-- 
-- ![Mission](../Tasking/Menu_Main_Task.JPG)
-- 
-- When a player has joined a task, a **Task Action Menu** is available to be used by the player. 
--
-- ![Mission](../Tasking/Menu_Task.JPG)
-- 
-- The task action menu contains now menu items specific to the task, but also one generic menu item, which is to control the task.
-- This **Task Control Menu** allows to display again the task details and the task map location information.
-- But it also allows to abort a task!
-- 
-- Depending on the task type, the task action menu can contain more menu items which are specific to the task.
-- For example, cargo transportation tasks will contain various additional menu items to select relevant cargo coordinates,
-- or to load/unload cargo.
-- 
-- ## 1.5) Automatic task assignment.
-- 
-- ![Command Center](../Tasking/Menu_CommandCenter.JPG)
-- 
-- When we take back the command center menu, you see two additional **Assign Task** menu items.
-- The menu **Assign Task On** will automatically allocate a task to the player.
-- After the selection of this menu, the menu will change into **Assign Task Off**,
-- and will need to be selected again by the player to switch of the automatic task assignment.
-- 
-- The other option is to select **Assign Task**, which will assign a new random task to the player.
-- 
-- When a task is automatically assigned to a player, the task needs to be confirmed as accepted within 30 seconds.
-- If this is not the case, the task will be cancelled automatically, and a new random task will be assigned to the player.
-- This will continue to happen until the player accepts the task or switches off the automatic task assignment process.
-- 
-- The player can accept the task using the menu **Confirm Task Acceptance** ...
-- 
-- ## 1.6) Task states.
-- 
-- A task has a state, reflecting the progress or completion status of the task:
-- 
--   - **Planned**: Expresses that the task is created, but not yet in execution and is not assigned yet to a pilot.
--   - **Assigned**: Expresses that the task is assigned to a group of pilots, and that the task is in execution mode.
--   - **Success**: Expresses the successful execution and finalization of the task.
--   - **Failed**: Expresses the failure of a task.
--   - **Abort**: Expresses that the task is aborted by by the player using the abort menu.
--   - **Cancelled**: Expresses that the task is cancelled by HQ or through a logical situation where a cancellation of the task is required.
-- 
-- ### 1.6.1) Task progress.
-- 
-- The task governor takes care of the **progress** and **completion** of the task **goal(s)**.
-- Tasks are executed by **human pilots** and actors within a DCS simulation.
-- Pilots can use a **menu system** to engage or abort a task, and provides means to
-- understand the **task briefing** and goals, and the relevant **task locations** on the map and 
-- obtain **various reports** related to the task.
-- 
-- ### 1.6.2) Task completion.
-- 
-- As the task progresses, the **task status** will change over time, from Planned state to Completed state.
-- **Multiple pilots** can execute the same task, as such, the tasking system provides a **co-operative model** for joint task execution.
-- Depending on the task progress, a **scoring** can be allocated to award pilots of the achievements made.
-- The scoring is fully flexible, and different levels of awarding can be provided depending on the task type and complexity.
-- 
-- A normal flow of task status would evolve from the **Planned** state, to the **Assigned** state ending either in a **Success** or a **Failed** state.
-- 
--      Planned -> Assigned -> Success
--                          -> Failed
--                          -> Cancelled
--                          
-- The state completion is by default set to **Success**, if the goals of the task have been reached, but can be overruled by a goal method.
-- 
-- Depending on the tactical situation, a task can be **Cancelled** by the mission governor.
-- It is actually the mission designer who has the flexibility to decide at which conditions a task would be set to **Success**, **Failed** or **Cancelled**.
-- This decision all depends on the task goals, and the phase/evolution of the task conditions that would accomplish the goals.
-- 
-- For example, if the task goal is to merely destroy a target, and the target is mid-mission destroyed by another event than the pilot destroying the target,
-- the task goal could be set to **Failed**, or .. **Cancelled** ...
-- However, it could very well be also acceptable that the task would be flagged as **Success**.
-- 
-- The tasking mechanism governs beside the progress also a scoring mechanism, and in case of goal completion without any active pilot involved
-- in the execution of the task, could result in a **Success** task completion status, but no score would be awarded, as there were no players involved. 
-- 
-- These different completion states are important for the mission designer to reflect scoring to a player.
-- A success could mean a positive score to be given, while a failure could mean a negative score or penalties to be awarded.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- ### Author(s): **FlightControl**
-- 
-- ### Contribution(s):
-- 
-- ===
-- 
-- @module Tasking.Task
-- @image MOOSE.JPG

---
-- @type TASK
-- @field Core.Scheduler#SCHEDULER TaskScheduler
-- @field Tasking.Mission#MISSION Mission
-- @field Core.Set#SET_GROUP SetGroup The Set of Groups assigned to the Task
-- @field Core.Fsm#FSM_PROCESS FsmTemplate
-- @field Tasking.Mission#MISSION Mission
-- @field Tasking.CommandCenter#COMMANDCENTER CommandCenter
-- @field Tasking.TaskInfo#TASKINFO TaskInfo
-- @extends Core.Fsm#FSM_TASK

--- Governs the main engine to administer human taskings.
-- 
-- A task is governed by a @{Tasking.Mission} object. Tasks are of different types.
-- The @{#TASK} object is used or derived by more detailed tasking classes that will implement the task execution mechanisms
-- and goals. 
-- 
-- # 1) Derived task classes.
-- 
-- The following TASK_ classes are derived from @{#TASK}.
-- 
--      TASK
--        TASK_A2A
--          TASK_A2A_ENGAGE
--          TASK_A2A_INTERCEPT
--          TASK_A2A_SWEEP
--        TASK_A2G
--          TASK_A2G_SEAD
--          TASK_A2G_CAS
--          TASK_A2G_BAI
--        TASK_CARGO
--          TASK_CARGO_TRANSPORT
--          TASK_CARGO_CSAR
-- 
-- ## 1.1) A2A Tasks
-- 
--   - @{Tasking.Task_A2A#TASK_A2A_ENGAGE} - Models an A2A engage task of a target group of airborne intruders mid-air.
--   - @{Tasking.Task_A2A#TASK_A2A_INTERCEPT} - Models an A2A ground intercept task of a target group of airborne intruders mid-air.
--   - @{Tasking.Task_A2A#TASK_A2A_SWEEP} - Models an A2A sweep task to clean an area of previously detected intruders mid-air.
-- 
-- ## 1.2) A2G Tasks
-- 
--   - @{Tasking.Task_A2G#TASK_A2G_SEAD} - Models an A2G Suppression or Extermination of Air Defenses task to clean an area of air to ground defense threats.
--   - @{Tasking.Task_A2G#TASK_A2G_CAS} - Models an A2G Close Air Support task to provide air support to nearby friendlies near the front-line.
--   - @{Tasking.Task_A2G#TASK_A2G_BAI} - Models an A2G Battlefield Air Interdiction task to provide air support to nearby friendlies near the front-line.
-- 
-- ## 1.3) Cargo Tasks  
-- 
--   - @{Tasking.Task_CARGO#TASK_CARGO_TRANSPORT} - Models the transportation of cargo to deployment zones. 
--   - @{Tasking.Task_CARGO#TASK_CARGO_CSAR} - Models the rescue of downed friendly pilots from behind enemy lines.    
-- 
-- 
-- # 2) Task status events.
-- 
-- The task statuses can be set by using the following methods:
-- 
--   - @{#TASK.Success}() - Set the task to **Success** state.
--   - @{#TASK.Fail}() - Set the task to **Failed** state.
--   - @{#TASK.Hold}() - Set the task to **Hold** state.
--   - @{#TASK.Abort}() - Set the task to **Aborted** state, aborting the task. The task may be replanned.
--   - @{#TASK.Cancel}() - Set the task to **Cancelled** state, cancelling the task.
-- 
-- The mentioned derived TASK_ classes are implementing the task status transitions out of the box.
-- So no extra logic needs to be written.
--   
-- # 3) Goal conditions for a task.
-- 
-- Every 30 seconds, a @{#Task.Goal} trigger method is fired. 
-- You as a mission designer, can capture the **Goal** event trigger to check your own task goal conditions and take action!
-- 
-- ## 3.1) Goal event handler `OnAfterGoal()`.
-- 
-- And this is a really great feature! Imagine a task which has **several conditions to check** before the task can move into **Success** state.
-- You can do this with the OnAfterGoal method.
-- 
-- The following code provides an example of such a goal condition check implementation.
-- 
--      function Task:OnAfterGoal()
--        if condition == true then
--          self:Success() -- This will flag the task to Success when the condition is true.
--        else
--          if condition2 == true and condition3 == true then
--            self:Fail() -- This will flag the task to Failed, when condition2 and condition3 would be true.
--          end
--        end
--      end
-- 
-- So the @{#TASK.OnAfterGoal}() event handler would be called every 30 seconds automatically, 
-- and within this method, you can now check the conditions and take respective action.
-- 
-- ## 3.2) Goal event trigger `Goal()`.
-- 
-- If you would need to check a goal at your own defined event timing, then just call the @{#TASK.Goal}() method within your logic.
-- The @{#TASK.OnAfterGoal}() event handler would then directly be called and would execute the logic. 
-- Note that you can also delay the goal check by using the delayed event trigger syntax `:__Goal( Delay )`. 
-- 
-- 
-- # 4) Score task completion.
-- 
-- Upon reaching a certain task status in a task, additional scoring can be given. If the Mission has a scoring system attached, the scores will be added to the mission scoring.
-- Use the method @{#TASK.AddScore}() to add scores when a status is reached.
-- 
-- # 5) Task briefing.
-- 
-- A task briefing is a text that is shown to the player when he is assigned to the task.
-- The briefing is broadcasted by the command center owning the mission.
-- 
-- The briefing is part of the parameters in the @{#TASK.New}() constructor, 
-- but can separately be modified later in your mission using the
-- @{#TASK.SetBriefing}() method.
-- 
-- 
-- @field #TASK TASK
-- 
TASK = {
  ClassName = "TASK",
  TaskScheduler = nil,
  ProcessClasses = {}, -- The container of the Process classes that will be used to create and assign new processes for the task to ProcessUnits.
  Processes = {}, -- The container of actual process objects instantiated and assigned to ProcessUnits.
  Players = nil,
  Scores = {},
  Menu = {},
  SetGroup = nil,
  FsmTemplate = nil,
  Mission = nil,
  CommandCenter = nil,
  TimeOut = 0,
  AssignedGroups = {},
}

--- FSM PlayerAborted event handler prototype for TASK.
-- @function [parent=#TASK] OnAfterPlayerAborted
-- @param #TASK self
-- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he went back to spectators or left the mission.
-- @param #string PlayerName The name of the Player.

--- FSM PlayerCrashed event handler prototype for TASK.
-- @function [parent=#TASK] OnAfterPlayerCrashed
-- @param #TASK self
-- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he crashed in the mission.
-- @param #string PlayerName The name of the Player.

--- FSM PlayerDead event handler prototype for TASK.
-- @function [parent=#TASK] OnAfterPlayerDead
-- @param #TASK self
-- @param Wrapper.Unit#UNIT PlayerUnit The Unit of the Player when he died in the mission.
-- @param #string PlayerName The name of the Player.

--- FSM Fail synchronous event function for TASK.
-- Use this event to Fail the Task.
-- @function [parent=#TASK] Fail
-- @param #TASK self

--- FSM Fail asynchronous event function for TASK.
-- Use this event to Fail the Task.
-- @function [parent=#TASK] __Fail
-- @param #TASK self

--- FSM Abort synchronous event function for TASK.
-- Use this event to Abort the Task.
-- @function [parent=#TASK] Abort
-- @param #TASK self

--- FSM Abort asynchronous event function for TASK.
-- Use this event to Abort the Task.
-- @function [parent=#TASK] __Abort
-- @param #TASK self

--- FSM Success synchronous event function for TASK.
-- Use this event to make the Task a Success.
-- @function [parent=#TASK] Success
-- @param #TASK self

--- FSM Success asynchronous event function for TASK.
-- Use this event to make the Task a Success.
-- @function [parent=#TASK] __Success
-- @param #TASK self

--- FSM Cancel synchronous event function for TASK.
-- Use this event to Cancel the Task.
-- @function [parent=#TASK] Cancel
-- @param #TASK self

--- FSM Cancel asynchronous event function for TASK.
-- Use this event to Cancel the Task.
-- @function [parent=#TASK] __Cancel
-- @param #TASK self

--- FSM Replan synchronous event function for TASK.
-- Use this event to Replan the Task.
-- @function [parent=#TASK] Replan
-- @param #TASK self

--- FSM Replan asynchronous event function for TASK.
-- Use this event to Replan the Task.
-- @function [parent=#TASK] __Replan
-- @param #TASK self


--- Instantiates a new TASK. Should never be used. Interface Class.
-- @param #TASK self
-- @param Tasking.Mission#MISSION Mission The mission wherein the Task is registered.
-- @param Core.Set#SET_GROUP SetGroupAssign The set of groups for which the Task can be assigned.
-- @param #string TaskName The name of the Task
-- @param #string TaskType The type of the Task
-- @return #TASK self
function TASK:New( Mission, SetGroupAssign, TaskName, TaskType, TaskBriefing )

  local self = BASE:Inherit( self, FSM_TASK:New( TaskName ) ) -- Tasking.Task#TASK

  self:SetStartState( "Planned" )
  self:AddTransition( "Planned", "Assign", "Assigned" )
  self:AddTransition( "Assigned", "AssignUnit", "Assigned" )
  self:AddTransition( "Assigned", "Success", "Success" )
  self:AddTransition( "Assigned", "Hold", "Hold" )
  self:AddTransition( "Assigned", "Fail", "Failed" )
  self:AddTransition( { "Planned", "Assigned" }, "Abort", "Aborted" )
  self:AddTransition( "Assigned", "Cancel", "Cancelled" )
  self:AddTransition( "Assigned", "Goal", "*" )
  
  self.Fsm = {}
  
  local Fsm = self:GetUnitProcess()
  Fsm:SetStartState( "Planned" )
  Fsm:AddProcess   ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "Assigned", Rejected = "Reject" }  )
  Fsm:AddTransition( "Assigned", "Assigned", "*" )
  
  --- Goal Handler OnBefore for TASK
  -- @function [parent=#TASK] OnBeforeGoal
  -- @param #TASK self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the player.
  -- @param #string PlayerName The name of the player.
  -- @return #boolean
  
  --- Goal Handler OnAfter for TASK
  -- @function [parent=#TASK] OnAfterGoal
  -- @param #TASK self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the player.
  -- @param #string PlayerName The name of the player.
  
  --- Goal Trigger for TASK
  -- @function [parent=#TASK] Goal
  -- @param #TASK self
  -- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the player.
  -- @param #string PlayerName The name of the player.
  
  --- Goal Asynchronous Trigger for TASK
  -- @function [parent=#TASK] __Goal
  -- @param #TASK self
  -- @param #number Delay
  -- @param Wrapper.Unit#UNIT PlayerUnit The @{Wrapper.Unit} of the player.
  -- @param #string PlayerName The name of the player.
  
  
  
  self:AddTransition( "*", "PlayerCrashed", "*" )
  self:AddTransition( "*", "PlayerAborted", "*" )
  self:AddTransition( "*", "PlayerRejected", "*" )
  self:AddTransition( "*", "PlayerDead", "*" )
  self:AddTransition( { "Failed", "Aborted", "Cancelled" }, "Replan", "Planned" )
  self:AddTransition( "*", "TimeOut", "Cancelled" )

  self:F( "New TASK " .. TaskName )

  self.Processes = {}

  self.Mission = Mission
  self.CommandCenter = Mission:GetCommandCenter()
  
  self.SetGroup = SetGroupAssign

  self:SetType( TaskType )
  self:SetName( TaskName )
  self:SetID( Mission:GetNextTaskID( self ) ) -- The Mission orchestrates the task sequences ..

  self:SetBriefing( TaskBriefing )
  
  
  self.TaskInfo = TASKINFO:New( self )
  
  self.TaskProgress = {}
  
  return self
end

--- Get the Task FSM Process Template
-- @param #TASK self
-- @return Core.Fsm#FSM_PROCESS
function TASK:GetUnitProcess( TaskUnit )

  if TaskUnit then
    return self:GetStateMachine( TaskUnit )
  else
    self.FsmTemplate = self.FsmTemplate or FSM_PROCESS:New()
    return self.FsmTemplate 
  end
end

--- Sets the Task FSM Process Template
-- @param #TASK self
-- @param Core.Fsm#FSM_PROCESS
function TASK:SetUnitProcess( FsmTemplate )

  self.FsmTemplate = FsmTemplate
end

--- Add a PlayerUnit to join the Task.
-- For each Group within the Task, the Unit is checked if it can join the Task.
-- If the Unit was not part of the Task, false is returned.
-- If the Unit is part of the Task, true is returned.
-- @param #TASK self
-- @param Wrapper.Unit#UNIT PlayerUnit The CLIENT or UNIT of the Player joining the Mission.
-- @param Wrapper.Group#GROUP PlayerGroup The GROUP of the player joining the Mission.
-- @return #boolean true if Unit is part of the Task.
function TASK:JoinUnit( PlayerUnit, PlayerGroup )
  self:F( { PlayerUnit = PlayerUnit, PlayerGroup = PlayerGroup } )
  
  local PlayerUnitAdded = false
  
  local PlayerGroups = self:GetGroups()

  -- Is the PlayerGroup part of the PlayerGroups?  
  if PlayerGroups:IsIncludeObject( PlayerGroup ) then
  
    -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is added to the Task.
    -- If the PlayerGroup is not assigned to the Task, the menu needs to be set. In that case, the PlayerUnit will become the GroupPlayer leader.
    if self:IsStatePlanned() or self:IsStateReplanned() then
      --self:SetMenuForGroup( PlayerGroup )
      --self:MessageToGroups( PlayerUnit:GetPlayerName() .. " is planning to join Task " .. self:GetName() )
    end
    if self:IsStateAssigned() then
      local IsGroupAssigned = self:IsGroupAssigned( PlayerGroup )
      self:F( { IsGroupAssigned = IsGroupAssigned } )
      if IsGroupAssigned then
        self:AssignToUnit( PlayerUnit )
        self:MessageToGroups( PlayerUnit:GetPlayerName() .. " joined Task " .. self:GetName() )
      end
    end
  end
  
  return PlayerUnitAdded
end

--- A group rejecting a planned task.
-- @param #TASK self
-- @param Wrapper.Group#GROUP PlayerGroup The group rejecting the task.
-- @return #TASK
function TASK:RejectGroup( PlayerGroup )
  
  local PlayerGroups = self:GetGroups()

  -- Is the PlayerGroup part of the PlayerGroups?  
  if PlayerGroups:IsIncludeObject( PlayerGroup ) then
  
    -- Check if the PlayerGroup is already assigned or is planned to be assigned to the Task. 
    -- If yes, the PlayerGroup is aborted from the Task.
    -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group.
    if self:IsStatePlanned() then

      local IsGroupAssigned = self:IsGroupAssigned( PlayerGroup )
      if IsGroupAssigned then
        local PlayerName = PlayerGroup:GetUnit(1):GetPlayerName()
        self:GetMission():GetCommandCenter():MessageToGroup( "Task " .. self:GetName() .. " has been rejected! We will select another task.", PlayerGroup )
        self:UnAssignFromGroup( PlayerGroup )

        self:PlayerRejected( PlayerGroup:GetUnit(1) )
      end
      
    end
  end
  
  return self
end


--- A group aborting the task.
-- @param #TASK self
-- @param Wrapper.Group#GROUP PlayerGroup The group aborting the task.
-- @return #TASK
function TASK:AbortGroup( PlayerGroup )
  
  local PlayerGroups = self:GetGroups()

  -- Is the PlayerGroup part of the PlayerGroups?  
  if PlayerGroups:IsIncludeObject( PlayerGroup ) then
  
    -- Check if the PlayerGroup is already assigned or is planned to be assigned to the Task. 
    -- If yes, the PlayerGroup is aborted from the Task.
    -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group.
    if self:IsStateAssigned() then

      local IsGroupAssigned = self:IsGroupAssigned( PlayerGroup )
      if IsGroupAssigned then
        local PlayerName = PlayerGroup:GetUnit(1):GetPlayerName()
        self:UnAssignFromGroup( PlayerGroup )

        -- Now check if the task needs to go to hold...
        -- It will go to hold, if there are no players in the mission...
        PlayerGroups:Flush( self )
        local IsRemaining = false
        for GroupName, AssignedGroup in pairs( PlayerGroups:GetSet() or {} ) do
          if self:IsGroupAssigned( AssignedGroup ) == true then
            IsRemaining = true
            self:F( { Task = self:GetName(), IsRemaining = IsRemaining } )
           break
          end
        end

        self:F( { Task = self:GetName(), IsRemaining = IsRemaining } )
        if IsRemaining == false then
          self:Abort()
        end
        
        self:PlayerAborted( PlayerGroup:GetUnit(1) )
      end
      
    end
  end
  
  return self
end


--- A group crashing and thus aborting from the task.
-- @param #TASK self
-- @param Wrapper.Group#GROUP PlayerGroup The group aborting the task.
-- @return #TASK
function TASK:CrashGroup( PlayerGroup )
  self:F( { PlayerGroup = PlayerGroup } )
  
  local PlayerGroups = self:GetGroups()

  -- Is the PlayerGroup part of the PlayerGroups?  
  if PlayerGroups:IsIncludeObject( PlayerGroup ) then
  
    -- Check if the PlayerGroup is already assigned to the Task. If yes, the PlayerGroup is aborted from the Task.
    -- If the PlayerUnit was the last unit of the PlayerGroup, the menu needs to be removed from the Group.
    if self:IsStateAssigned() then
      local IsGroupAssigned = self:IsGroupAssigned( PlayerGroup )
      self:F( { IsGroupAssigned = IsGroupAssigned } )
      if IsGroupAssigned then
        local PlayerName = PlayerGroup:GetUnit(1):GetPlayerName()
        self:MessageToGroups( PlayerName .. " crashed! " )
        self:UnAssignFromGroup( PlayerGroup )

        -- Now check if the task needs to go to hold...
        -- It will go to hold, if there are no players in the mission...
        
        PlayerGroups:Flush( self )
        local IsRemaining = false
        for GroupName, AssignedGroup in pairs( PlayerGroups:GetSet() or {} ) do
          if self:IsGroupAssigned( AssignedGroup ) == true then
            IsRemaining = true
            self:F( { Task = self:GetName(), IsRemaining = IsRemaining } )
           break
          end
        end

        self:F( { Task = self:GetName(), IsRemaining = IsRemaining } )
        if IsRemaining == false then
          self:Abort()
        end
        
        self:PlayerCrashed( PlayerGroup:GetUnit(1) )
      end
      
    end
  end
  
  return self
end



--- Gets the Mission to where the TASK belongs.
-- @param #TASK self
-- @return Tasking.Mission#MISSION
function TASK:GetMission()

  return self.Mission
end


--- Gets the SET_GROUP assigned to the TASK.
-- @param #TASK self
-- @return Core.Set#SET_GROUP
function TASK:GetGroups()

  return self.SetGroup
end


--- Gets the SET_GROUP assigned to the TASK.
-- @param #TASK self
-- @param Core.Set#SET_GROUP GroupSet
-- @return Core.Set#SET_GROUP
function TASK:AddGroups( GroupSet )

  GroupSet = GroupSet or SET_GROUP:New()
 
  self.SetGroup:ForEachGroup(
    -- @param Wrapper.Group#GROUP GroupSet
    function( GroupItem )
      GroupSet:Add( GroupItem:GetName(), GroupItem)
    end
  )
  
  return GroupSet
end

do -- Group Assignment

  --- Returns if the @{#TASK} is assigned to the Group.
  -- @param #TASK self
  -- @param Wrapper.Group#GROUP TaskGroup
  -- @return #boolean
  function TASK:IsGroupAssigned( TaskGroup )
  
    local TaskGroupName = TaskGroup:GetName()
    
    if self.AssignedGroups[TaskGroupName] then
      --self:T( { "Task is assigned to:", TaskGroup:GetName() } )
      return true
    end
    
    --self:T( { "Task is not assigned to:", TaskGroup:GetName() } )
    return false
  end
  
  
  --- Set @{Wrapper.Group} assigned to the @{#TASK}.
  -- @param #TASK self
  -- @param Wrapper.Group#GROUP TaskGroup
  -- @return #TASK
  function TASK:SetGroupAssigned( TaskGroup )
  
    local TaskName = self:GetName()
    local TaskGroupName = TaskGroup:GetName()
  
    self.AssignedGroups[TaskGroupName] = TaskGroup
    self:F( string.format( "Task %s is assigned to %s", TaskName, TaskGroupName ) )
    
    -- Set the group to be assigned at mission level. This allows to decide the menu options on mission level for this group.
    self:GetMission():SetGroupAssigned( TaskGroup )
    
    local SetAssignedGroups = self:GetGroups()
    
--    SetAssignedGroups:ForEachGroup(
--      function( AssignedGroup )
--        if self:IsGroupAssigned(AssignedGroup) then
--          self:GetMission():GetCommandCenter():MessageToGroup( string.format( "Task %s is assigned to group %s.", TaskName, TaskGroupName ), AssignedGroup )
--        else
--          self:GetMission():GetCommandCenter():MessageToGroup( string.format( "Task %s is assigned to your group.", TaskName ), AssignedGroup )
--        end
--      end
--    )
    
    return self
  end
  
  --- Clear the @{Wrapper.Group} assignment from the @{#TASK}.
  -- @param #TASK self
  -- @param Wrapper.Group#GROUP TaskGroup
  -- @return #TASK
  function TASK:ClearGroupAssignment( TaskGroup )
  
    local TaskName = self:GetName()
    local TaskGroupName = TaskGroup:GetName()
  
    self.AssignedGroups[TaskGroupName] = nil
    --self:F( string.format( "Task %s is unassigned to %s", TaskName, TaskGroupName ) )

    -- Set the group to be assigned at mission level. This allows to decide the menu options on mission level for this group.
    self:GetMission():ClearGroupAssignment( TaskGroup )
    
    local SetAssignedGroups = self:GetGroups()

    SetAssignedGroups:ForEachGroup(
      function( AssignedGroup )
        if self:IsGroupAssigned(AssignedGroup) then
          --self:GetMission():GetCommandCenter():MessageToGroup( string.format( "Task %s is unassigned from group %s.", TaskName, TaskGroupName ), AssignedGroup )
        else
          --self:GetMission():GetCommandCenter():MessageToGroup( string.format( "Task %s is unassigned from your group.", TaskName ), AssignedGroup )
        end
      end
    )
    
    return self
  end
  
end

do -- Group Assignment

  -- @param #TASK self
  -- @param Actions.Act_Assign#ACT_ASSIGN AcceptClass
  function TASK:SetAssignMethod( AcceptClass )
  
    local ProcessTemplate = self:GetUnitProcess()

    ProcessTemplate:SetProcess( "Planned", "Accept", AcceptClass ) -- Actions.Act_Assign#ACT_ASSIGN
  end


  --- Assign the @{#TASK} to a @{Wrapper.Group}.
  -- @param #TASK self
  -- @param Wrapper.Group#GROUP TaskGroup
  -- @return #TASK
  function TASK:AssignToGroup( TaskGroup )
    self:F( TaskGroup:GetName() )
    
    local TaskGroupName = TaskGroup:GetName()
    local Mission = self:GetMission()
    local CommandCenter = Mission:GetCommandCenter()
    
    self:SetGroupAssigned( TaskGroup )
    
    local TaskUnits = TaskGroup:GetUnits()
    for UnitID, UnitData in pairs( TaskUnits ) do
      local TaskUnit = UnitData -- Wrapper.Unit#UNIT
      local PlayerName = TaskUnit:GetPlayerName()
      self:F(PlayerName)
      if PlayerName ~= nil and PlayerName ~= "" then
        self:AssignToUnit( TaskUnit )
        CommandCenter:MessageToGroup( 
          string.format( 'Task "%s": Briefing for player (%s):\n%s', 
            self:GetName(), 
            PlayerName, 
            self:GetBriefing()
          ), TaskGroup 
        )
      end
    end

    CommandCenter:SetMenu()
    
    self:MenuFlashTaskStatus( TaskGroup, self:GetMission():GetCommandCenter().FlashStatus )
    
    return self
  end
  
  --- UnAssign the @{#TASK} from a @{Wrapper.Group}.
  -- @param #TASK self
  -- @param Wrapper.Group#GROUP TaskGroup
  function TASK:UnAssignFromGroup( TaskGroup )
    self:F2( { TaskGroup = TaskGroup:GetName() } )
    
    self:ClearGroupAssignment( TaskGroup )
  
    local TaskUnits = TaskGroup:GetUnits()
    for UnitID, UnitData in pairs( TaskUnits ) do
      local TaskUnit = UnitData -- Wrapper.Unit#UNIT
      local PlayerName = TaskUnit:GetPlayerName()
      if PlayerName ~= nil and PlayerName ~= "" then -- Only remove units that have players!
        self:UnAssignFromUnit( TaskUnit )
      end
    end

    local Mission = self:GetMission()
    local CommandCenter = Mission:GetCommandCenter()
    CommandCenter:SetMenu()
    
    self:MenuFlashTaskStatus( TaskGroup, false ) -- stop message flashing, if any #1383 & #1312
    
  end
end


---
-- @param #TASK self
-- @param Wrapper.Group#GROUP FindGroup
-- @return #boolean
function TASK:HasGroup( FindGroup )

  local SetAttackGroup = self:GetGroups()
  return SetAttackGroup:FindGroup( FindGroup:GetName() )

end

--- Assign the @{#TASK} to an alive @{Wrapper.Unit}.
-- @param #TASK self
-- @param Wrapper.Unit#UNIT TaskUnit
-- @return #TASK self
function TASK:AssignToUnit( TaskUnit )
  self:F( TaskUnit:GetName() )
  
  local FsmTemplate = self:GetUnitProcess()
  
  -- Assign a new FsmUnit to TaskUnit.
  local FsmUnit = self:SetStateMachine( TaskUnit, FsmTemplate:Copy( TaskUnit, self ) ) -- Core.Fsm#FSM_PROCESS
  
  FsmUnit:SetStartState( "Planned" )
  
  FsmUnit:Accept() -- Each Task needs to start with an Accept event to start the flow.

  return self
end

--- UnAssign the @{#TASK} from an alive @{Wrapper.Unit}.
-- @param #TASK self
-- @param Wrapper.Unit#UNIT TaskUnit
-- @return #TASK self
function TASK:UnAssignFromUnit( TaskUnit )
  self:F( TaskUnit:GetName() )
  
  self:RemoveStateMachine( TaskUnit )
  
  -- If a Task Control Menu had been set, then this will be removed.
  self:RemoveTaskControlMenu( TaskUnit )
  return self
end

--- Sets the TimeOut for the @{#TASK}. If @{#TASK} stayed planned for longer than TimeOut, it gets into Cancelled status.
-- @param #TASK self
-- @param #integer Timer in seconds
-- @return #TASK self
function TASK:SetTimeOut ( Timer )
  self:F( Timer )
  self.TimeOut = Timer
  self:__TimeOut( self.TimeOut )
  return self
end

--- Send a message of the @{#TASK} to the assigned @{Wrapper.Group}s.
-- @param #TASK self
function TASK:MessageToGroups( Message )
  self:F( { Message = Message } )

  local Mission = self:GetMission()
  local CC = Mission:GetCommandCenter()
  
  for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do
    TaskGroup = TaskGroup -- Wrapper.Group#GROUP
    if TaskGroup:IsAlive() == true then
      CC:MessageToGroup( Message, TaskGroup, TaskGroup:GetName() )
    end
  end
end


--- Send the briefing message of the @{#TASK} to the assigned @{Wrapper.Group}s.
-- @param #TASK self
function TASK:SendBriefingToAssignedGroups()
  self:F2()
  
  for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do
    if TaskGroup:IsAlive() then
      if self:IsGroupAssigned( TaskGroup ) then    
        TaskGroup:Message( self.TaskBriefing, 60 )
      end
    end
  end
end


--- UnAssign the @{#TASK} from the @{Wrapper.Group}s.
-- @param #TASK self
function TASK:UnAssignFromGroups()
  self:F2()
  
  for TaskGroupName, TaskGroup in pairs( self.SetGroup:GetSet() ) do
    if TaskGroup:IsAlive() == true then
      if self:IsGroupAssigned(TaskGroup) then
        self:UnAssignFromGroup( TaskGroup )
      end
    end
  end
end



--- Returns if the @{#TASK} has still alive and assigned Units.
-- @param #TASK self
-- @return #boolean
function TASK:HasAliveUnits()
  self:F()
  
  for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
    if TaskGroup:IsAlive() == true then
      if self:IsStateAssigned() then
        if self:IsGroupAssigned( TaskGroup ) then
          for TaskUnitID, TaskUnit in pairs( TaskGroup:GetUnits() ) do
            if TaskUnit:IsAlive() then
              self:T( { HasAliveUnits = true } )
              return true
            end
          end
        end
      end
    end
  end
  
  self:T( { HasAliveUnits = false } )
  return false
end

--- Set the menu options of the @{#TASK} to all the groups in the SetGroup.
-- @param #TASK self
-- @param #number MenuTime
-- @return #TASK
function TASK:SetMenu( MenuTime ) --R2.1 Mission Reports and Task Reports added. Fixes issue #424.
  self:F( { self:GetName(), MenuTime } )

  --self.SetGroup:Flush()
  --for TaskGroupID, TaskGroupData in pairs( self.SetGroup:GetAliveSet() ) do
  for TaskGroupID, TaskGroupData in pairs( self.SetGroup:GetSet() ) do
    local TaskGroup = TaskGroupData -- Wrapper.Group#GROUP
    if TaskGroup:IsAlive() == true and TaskGroup:GetPlayerNames() then
    
      -- Set Mission Menus
      
      local Mission = self:GetMission()
      local MissionMenu = Mission:GetMenu( TaskGroup )
      if MissionMenu then
        self:SetMenuForGroup( TaskGroup, MenuTime )
      end
    end
  end  
end



--- Set the Menu for a Group
-- @param #TASK self
-- @param #number MenuTime
-- @return #TASK
function TASK:SetMenuForGroup( TaskGroup, MenuTime )

  if self:IsStatePlanned() or self:IsStateAssigned() then
    self:SetPlannedMenuForGroup( TaskGroup, MenuTime )
    if self:IsGroupAssigned( TaskGroup ) then
      self:SetAssignedMenuForGroup( TaskGroup, MenuTime )
    end
  end
end


--- Set the planned menu option of the @{#TASK}.
-- @param #TASK self
-- @param Wrapper.Group#GROUP TaskGroup
-- @param #string MenuText The menu text.
-- @param #number MenuTime
-- @return #TASK self
function TASK:SetPlannedMenuForGroup( TaskGroup, MenuTime )
  self:F( TaskGroup:GetName() )

  local Mission = self:GetMission()
  local MissionName = Mission:GetName()
  local MissionMenu = Mission:GetMenu( TaskGroup )

  local TaskType = self:GetType()
  local TaskPlayerCount = self:GetPlayerCount()
  local TaskPlayerString = string.format( " (%dp)", TaskPlayerCount )
  local TaskText = string.format( "%s", self:GetName() )
  local TaskName = string.format( "%s", self:GetName() )

  self.MenuPlanned = self.MenuPlanned or {}
  self.MenuPlanned[TaskGroup] = MENU_GROUP_DELAYED:New( TaskGroup, "Join Planned Task", MissionMenu, Mission.MenuReportTasksPerStatus, Mission, TaskGroup, "Planned" ):SetTime( MenuTime ):SetTag( "Tasking" )
  local TaskTypeMenu = MENU_GROUP_DELAYED:New( TaskGroup, TaskType, self.MenuPlanned[TaskGroup] ):SetTime( MenuTime ):SetTag( "Tasking" )
  local TaskTypeMenu = MENU_GROUP_DELAYED:New( TaskGroup, TaskText, TaskTypeMenu ):SetTime( MenuTime ):SetTag( "Tasking" )
  
  if not Mission:IsGroupAssigned( TaskGroup ) then
    --self:F( { "Replacing Join Task menu" } )
    local JoinTaskMenu = MENU_GROUP_COMMAND_DELAYED:New( TaskGroup, string.format( "Join Task" ), TaskTypeMenu, self.MenuAssignToGroup, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" )
    local MarkTaskMenu = MENU_GROUP_COMMAND_DELAYED:New( TaskGroup, string.format( "Mark Task Location on Map" ), TaskTypeMenu, self.MenuMarkToGroup, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" )
  end
  
  local ReportTaskMenu = MENU_GROUP_COMMAND_DELAYED:New( TaskGroup, string.format( "Report Task Details" ), TaskTypeMenu, self.MenuTaskStatus, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" )
      
  return self
end

--- Set the assigned menu options of the @{#TASK}.
-- @param #TASK self
-- @param Wrapper.Group#GROUP TaskGroup
-- @param #number MenuTime
-- @return #TASK self
function TASK:SetAssignedMenuForGroup( TaskGroup, MenuTime )
  self:F( { TaskGroup:GetName(), MenuTime } )

  local TaskType = self:GetType()
  local TaskPlayerCount = self:GetPlayerCount()
  local TaskPlayerString = string.format( " (%dp)", TaskPlayerCount )
  local TaskText = string.format( "%s%s", self:GetName(), TaskPlayerString ) --, TaskThreatLevelString )
  local TaskName = string.format( "%s", self:GetName() )

  for UnitName, TaskUnit in pairs( TaskGroup:GetPlayerUnits() ) do
    local TaskUnit = TaskUnit -- Wrapper.Unit#UNIT
    if TaskUnit then
      local MenuControl = self:GetTaskControlMenu( TaskUnit )
      local TaskControl = MENU_GROUP:New( TaskGroup, "Control Task", MenuControl ):SetTime( MenuTime ):SetTag( "Tasking" )
      if self:IsStateAssigned() then
        local TaskMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Abort Task" ), TaskControl, self.MenuTaskAbort, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" )
      end
      local MarkMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Mark Task Location on Map" ), TaskControl, self.MenuMarkToGroup, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" )
      local TaskTypeMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Report Task Details" ), TaskControl, self.MenuTaskStatus, self, TaskGroup ):SetTime( MenuTime ):SetTag( "Tasking" )
      if not self.FlashTaskStatus then
        local TaskFlashStatusMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Flash Task Details" ), TaskControl, self.MenuFlashTaskStatus, self, TaskGroup, true ):SetTime( MenuTime ):SetTag( "Tasking" )
      else
        local TaskFlashStatusMenu = MENU_GROUP_COMMAND:New( TaskGroup, string.format( "Stop Flash Task Details" ), TaskControl, self.MenuFlashTaskStatus, self, TaskGroup, nil ):SetTime( MenuTime ):SetTag( "Tasking" )
      end      
    end
  end

  return self
end

--- Remove the menu options of the @{#TASK} to all the groups in the SetGroup.
-- @param #TASK self
-- @param #number MenuTime
-- @return #TASK
function TASK:RemoveMenu( MenuTime )
  self:F( { self:GetName(), MenuTime } )

  for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
    if TaskGroup:IsAlive() == true then
      local TaskGroup = TaskGroup -- Wrapper.Group#GROUP 
      if TaskGroup:IsAlive() == true and TaskGroup:GetPlayerNames() then
        self:RefreshMenus( TaskGroup, MenuTime )
      end
    end
  end
end


--- Remove the menu option of the @{#TASK} for a @{Wrapper.Group}.
-- @param #TASK self
-- @param Wrapper.Group#GROUP TaskGroup
-- @param #number MenuTime
-- @return #TASK self
function TASK:RefreshMenus( TaskGroup, MenuTime )
  self:F( { TaskGroup:GetName(), MenuTime } )

  local Mission = self:GetMission()
  local MissionName = Mission:GetName()
  local MissionMenu = Mission:GetMenu( TaskGroup )

  local TaskName = self:GetName()
  self.MenuPlanned = self.MenuPlanned or {}
  local PlannedMenu = self.MenuPlanned[TaskGroup]
  
  self.MenuAssigned = self.MenuAssigned or {}
  local AssignedMenu = self.MenuAssigned[TaskGroup]
  
  if PlannedMenu then
    self.MenuPlanned[TaskGroup] = PlannedMenu:Remove( MenuTime , "Tasking" )
    PlannedMenu:Set()
  end
  
  if AssignedMenu then
    self.MenuAssigned[TaskGroup] = AssignedMenu:Remove( MenuTime, "Tasking" )
    AssignedMenu:Set()
  end
  
end

--- Remove the assigned menu option of the @{#TASK} for a @{Wrapper.Group}.
-- @param #TASK self
-- @param Wrapper.Group#GROUP TaskGroup
-- @param #number MenuTime
-- @return #TASK self
function TASK:RemoveAssignedMenuForGroup( TaskGroup )
  self:F()

  local Mission = self:GetMission()
  local MissionName = Mission:GetName()
  local MissionMenu = Mission:GetMenu( TaskGroup )
  
  if MissionMenu then
    MissionMenu:RemoveSubMenus()
  end
  
end

-- @param #TASK self
-- @param Wrapper.Group#GROUP TaskGroup
function TASK:MenuAssignToGroup( TaskGroup )

  self:F( "Join Task menu selected")
  
  self:AssignToGroup( TaskGroup )
end

-- @param #TASK self
-- @param Wrapper.Group#GROUP TaskGroup
function TASK:MenuMarkToGroup( TaskGroup )
  self:F()

  self:UpdateTaskInfo( self.DetectedItem )
  
  local TargetCoordinates = self.TaskInfo:GetData( "Coordinates" ) -- Core.Point#COORDINATE
  if TargetCoordinates then
    for TargetCoordinateID, TargetCoordinate in pairs( TargetCoordinates ) do
      local Report = REPORT:New():SetIndent( 0 )
      self.TaskInfo:Report( Report, "M", TaskGroup, self )
      local MarkText = Report:Text( ", " ) 
      self:F( { Coordinate = TargetCoordinate, MarkText = MarkText } )
      TargetCoordinate:MarkToGroup( MarkText, TaskGroup )
      --Coordinate:MarkToAll( Briefing )
    end
  else
    local TargetCoordinate = self.TaskInfo:GetData( "Coordinate" ) -- Core.Point#COORDINATE
    if TargetCoordinate then
      local Report = REPORT:New():SetIndent( 0 )
      self.TaskInfo:Report( Report, "M", TaskGroup, self )
      local MarkText = Report:Text( ", " ) 
      self:F( { Coordinate = TargetCoordinate, MarkText = MarkText } )
      TargetCoordinate:MarkToGroup( MarkText, TaskGroup )
    end
  end
  
end

--- Report the task status.
-- @param #TASK self
-- @param Wrapper.Group#GROUP TaskGroup
function TASK:MenuTaskStatus( TaskGroup )

  if TaskGroup:IsAlive() then

    local ReportText = self:ReportDetails( TaskGroup )
    
    self:T( ReportText )
    self:GetMission():GetCommandCenter():MessageTypeToGroup( ReportText, TaskGroup, MESSAGE.Type.Detailed )
  end

end

--- Report the task status.
-- @param #TASK self
function TASK:MenuFlashTaskStatus( TaskGroup, Flash )

  self.FlashTaskStatus = Flash

  if self.FlashTaskStatus then
    self.FlashTaskScheduler, self.FlashTaskScheduleID = SCHEDULER:New( self, self.MenuTaskStatus, { TaskGroup }, 0, 60) --Issue #1383 never ending flash messages
  else
    if self.FlashTaskScheduler then
      self.FlashTaskScheduler:Stop( self.FlashTaskScheduleID )
      self.FlashTaskScheduler = nil
      self.FlashTaskScheduleID = nil
    end
  end

end

--- Report the task status.
-- @param #TASK self
function TASK:MenuTaskAbort( TaskGroup )

  self:AbortGroup( TaskGroup )
end



--- Returns the @{#TASK} name.
-- @param #TASK self
-- @return #string TaskName
function TASK:GetTaskName()
  return self.TaskName
end

--- Returns the @{#TASK} briefing.
-- @param #TASK self
-- @return #string Task briefing.
function TASK:GetTaskBriefing()
  return self.TaskBriefing
end




--- Get the default or currently assigned @{Core.Fsm#FSM_PROCESS} template with key ProcessName.
-- @param #TASK self
-- @param #string ProcessName
-- @return Core.Fsm#FSM_PROCESS
function TASK:GetProcessTemplate( ProcessName )

  local ProcessTemplate = self.ProcessClasses[ProcessName]
  
  return ProcessTemplate
end



-- TODO: Obsolete?
--- Fail processes from @{#TASK} with key @{Wrapper.Unit}.
-- @param #TASK self
-- @param #string TaskUnitName
-- @return #TASK self
function TASK:FailProcesses( TaskUnitName )

  for ProcessID, ProcessData in pairs( self.Processes[TaskUnitName] ) do
    local Process = ProcessData 
    Process.Fsm:Fail()
  end
end

--- Add a FiniteStateMachine to @{#TASK} with key @{Wrapper.Unit}.
-- @param #TASK self
-- @param Wrapper.Unit#UNIT TaskUnit
-- @param Core.Fsm#FSM_PROCESS Fsm
-- @return #TASK self
function TASK:SetStateMachine( TaskUnit, Fsm )
  self:F2( { TaskUnit, self.Fsm[TaskUnit] ~= nil, Fsm:GetClassNameAndID() } )

  self.Fsm[TaskUnit] = Fsm
    
  return Fsm
end

--- Gets the FiniteStateMachine of @{#TASK} with key @{Wrapper.Unit}.
-- @param #TASK self
-- @param Wrapper.Unit#UNIT TaskUnit
-- @return Core.Fsm#FSM_PROCESS
function TASK:GetStateMachine( TaskUnit )
  self:F2( { TaskUnit, self.Fsm[TaskUnit] ~= nil } )

  return self.Fsm[TaskUnit]
end

--- Remove FiniteStateMachines from @{#TASK} with key @{Wrapper.Unit}.
-- @param #TASK self
-- @param Wrapper.Unit#UNIT TaskUnit
-- @return #TASK self
function TASK:RemoveStateMachine( TaskUnit )
  self:F( { TaskUnit = TaskUnit:GetName(), HasFsm = ( self.Fsm[TaskUnit] ~= nil ) } )

  --self:F( self.Fsm )
  --for TaskUnitT, Fsm in pairs( self.Fsm ) do
    --local Fsm = Fsm -- Core.Fsm#FSM_PROCESS
    --self:F( TaskUnitT )
    --self.Fsm[TaskUnit] = nil
  --end

  if self.Fsm[TaskUnit] then
    self.Fsm[TaskUnit]:Remove()
    self.Fsm[TaskUnit] = nil
  end
  
  collectgarbage()
  self:F( "Garbage Collected, Processes should be finalized now ...")
end


--- Checks if there is a FiniteStateMachine assigned to @{Wrapper.Unit} for @{#TASK}.
-- @param #TASK self
-- @param Wrapper.Unit#UNIT TaskUnit
-- @return #TASK self
function TASK:HasStateMachine( TaskUnit )
  self:F( { TaskUnit, self.Fsm[TaskUnit] ~= nil } )

  return ( self.Fsm[TaskUnit] ~= nil )
end


--- Gets the Scoring of the task
-- @param #TASK self
-- @return Functional.Scoring#SCORING Scoring
function TASK:GetScoring()
  return self.Mission:GetScoring()
end


--- Gets the Task Index, which is a combination of the Task type, the Task name.
-- @param #TASK self
-- @return #string The Task ID
function TASK:GetTaskIndex()

  local TaskType = self:GetType()
  local TaskName = self:GetName()

  return TaskType .. "." .. TaskName
end

--- Sets the Name of the Task
-- @param #TASK self
-- @param #string TaskName
function TASK:SetName( TaskName )
  self.TaskName = TaskName
end

--- Gets the Name of the Task
-- @param #TASK self
-- @return #string The Task Name
function TASK:GetName()
  return self.TaskName
end

--- Sets the Type of the Task
-- @param #TASK self
-- @param #string TaskType
function TASK:SetType( TaskType )
  self.TaskType = TaskType
end

--- Gets the Type of the Task
-- @param #TASK self
-- @return #string TaskType
function TASK:GetType()
  return self.TaskType
end

--- Sets the ID of the Task
-- @param #TASK self
-- @param #string TaskID
function TASK:SetID( TaskID )
  self.TaskID = TaskID
end

--- Gets the ID of the Task
-- @param #TASK self
-- @return #string TaskID
function TASK:GetID()
  return self.TaskID
end


--- Sets a @{#TASK} to status **Success**.
-- @param #TASK self
function TASK:StateSuccess()
  self:SetState( self, "State", "Success" )
  return self
end

--- Is the @{#TASK} status **Success**.
-- @param #TASK self
function TASK:IsStateSuccess()
  return self:Is( "Success" )
end

--- Sets a @{#TASK} to status **Failed**.
-- @param #TASK self
function TASK:StateFailed()
  self:SetState( self, "State", "Failed" )
  return self
end

--- Is the @{#TASK} status **Failed**.
-- @param #TASK self
function TASK:IsStateFailed()
  return self:Is( "Failed" )
end

--- Sets a @{#TASK} to status **Planned**.
-- @param #TASK self
function TASK:StatePlanned()
  self:SetState( self, "State", "Planned" )
  return self
end

--- Is the @{#TASK} status **Planned**.
-- @param #TASK self
function TASK:IsStatePlanned()
  return self:Is( "Planned" )
end

--- Sets a @{#TASK} to status **Aborted**.
-- @param #TASK self
function TASK:StateAborted()
  self:SetState( self, "State", "Aborted" )
  return self
end

--- Is the @{#TASK} status **Aborted**.
-- @param #TASK self
function TASK:IsStateAborted()
  return self:Is( "Aborted" )
end

--- Sets a @{#TASK} to status **Cancelled**.
-- @param #TASK self
function TASK:StateCancelled()
  self:SetState( self, "State", "Cancelled" )
  return self
end

--- Is the @{#TASK} status **Cancelled**.
-- @param #TASK self
function TASK:IsStateCancelled()
  return self:Is( "Cancelled" )
end

--- Sets a @{#TASK} to status **Assigned**.
-- @param #TASK self
function TASK:StateAssigned()
  self:SetState( self, "State", "Assigned" )
  return self
end

--- Is the @{#TASK} status **Assigned**.
-- @param #TASK self
function TASK:IsStateAssigned()
  return self:Is( "Assigned" )
end

--- Sets a @{#TASK} to status **Hold**.
-- @param #TASK self
function TASK:StateHold()
  self:SetState( self, "State", "Hold" )
  return self
end

--- Is the @{#TASK} status **Hold**.
-- @param #TASK self
function TASK:IsStateHold()
  return self:Is( "Hold" )
end

--- Sets a @{#TASK} to status **Replanned**.
-- @param #TASK self
function TASK:StateReplanned()
  self:SetState( self, "State", "Replanned" )
  return self
end

--- Is the @{#TASK} status **Replanned**.
-- @param #TASK self
function TASK:IsStateReplanned()
  return self:Is( "Replanned" )
end

--- Gets the @{#TASK} status.
-- @param #TASK self
function TASK:GetStateString()
  return self:GetState( self, "State" )
end

--- Sets a @{#TASK} briefing.
-- @param #TASK self
-- @param #string TaskBriefing
-- @return #TASK self
function TASK:SetBriefing( TaskBriefing )
  self:F(TaskBriefing)
  self.TaskBriefing = TaskBriefing
  return self
end

--- Gets the @{#TASK} briefing.
-- @param #TASK self
-- @return #string The briefing text.
function TASK:GetBriefing()
  return self.TaskBriefing
end




--- FSM function for a TASK
-- @param #TASK self
-- @param #string Event
-- @param #string From
-- @param #string To
function TASK:onenterAssigned( From, Event, To, PlayerUnit, PlayerName )

  --- This test is required, because the state transition will be fired also when the state does not change in case of an event.  
  if From ~= "Assigned" then

    local PlayerNames = self:GetPlayerNames()
    local PlayerText = REPORT:New()
    for PlayerName, TaskName in pairs( PlayerNames ) do
      PlayerText:Add( PlayerName )
    end

    self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " is assigned to players " .. PlayerText:Text(",") .. ". Good Luck!" )

    -- Set the total Progress to be achieved.
    self:SetGoalTotal() -- Polymorphic to set the initial goal total!
    
    if self.Dispatcher then
      self:F( "Firing Assign event " )
      self.Dispatcher:Assign( self, PlayerUnit, PlayerName )
    end
    
    self:GetMission():__Start( 1 )
    
    -- When the task is assigned, the task goal needs to be checked of the derived classes.
    self:__Goal( -10, PlayerUnit, PlayerName )  -- Polymorphic
     
    self:SetMenu()

    self:F( { "--> Task Assigned", TaskName = self:GetName(), Mission = self:GetMission():GetName() } )
    self:F( { "--> Task Player Names", PlayerNames = PlayerNames } )

  end
end


--- FSM function for a TASK
-- @param #TASK self
-- @param #string Event
-- @param #string From
-- @param #string To
function TASK:onenterSuccess( From, Event, To )

  self:F( { "<-> Task Replanned", TaskName = self:GetName(), Mission = self:GetMission():GetName() } )
  self:F( { "<-> Task Player Names", PlayerNames = self:GetPlayerNames() } )
  
  self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " is successful! Good job!" )
  self:UnAssignFromGroups()
  
  self:GetMission():__MissionGoals( 1 )
  
end


--- FSM function for a TASK
-- @param #TASK self
-- @param #string From
-- @param #string Event
-- @param #string To
function TASK:onenterAborted( From, Event, To )

  self:F( { "<-- Task Aborted", TaskName = self:GetName(), Mission = self:GetMission():GetName() } )
  self:F( { "<-- Task Player Names", PlayerNames = self:GetPlayerNames() } )
  
  if From ~= "Aborted" then
    self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has been aborted! Task may be replanned." )
    self:__Replan( 5 )
    self:SetMenu()
  end
  
end


--- FSM function for a TASK
-- @param #TASK self
-- @param #string From
-- @param #string Event
-- @param #string To
function TASK:onenterCancelled( From, Event, To )

  self:F( { "<-- Task Cancelled", TaskName = self:GetName(), Mission = self:GetMission():GetName() } )
  self:F( { "<-- Player Names", PlayerNames = self:GetPlayerNames() } )
  
  if From ~= "Cancelled" then
    self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has been cancelled! The tactical situation has changed." )
    self:UnAssignFromGroups()
    self:SetMenu()
  end
  
end

--- FSM function for a TASK
-- @param #TASK self
-- @param #string From
-- @param #string Event
-- @param #string To
function TASK:onafterReplan( From, Event, To )

  self:F( { "Task Replanned", TaskName = self:GetName(), Mission = self:GetMission():GetName() } )
  self:F( { "Task Player Names", PlayerNames = self:GetPlayerNames() } )
  
  self:GetMission():GetCommandCenter():MessageToCoalition( "Replanning Task " .. self:GetName() .. "." )
  
  self:SetMenu()
  
end

--- FSM function for a TASK
-- @param #TASK self
-- @param #string From
-- @param #string Event
-- @param #string To
function TASK:onenterFailed( From, Event, To )

  self:F( { "Task Failed", TaskName = self:GetName(), Mission = self:GetMission():GetName() } )
  self:F( { "Task Player Names", PlayerNames = self:GetPlayerNames() } )

  self:GetMission():GetCommandCenter():MessageToCoalition( "Task " .. self:GetName() .. " has failed!" )
  
  self:UnAssignFromGroups()
end

--- FSM function for a TASK
-- @param #TASK self
-- @param #string Event
-- @param #string From
-- @param #string To
function TASK:onstatechange( From, Event, To )

  if self:IsTrace() then
    --MESSAGE:New( "@ Task " .. self.TaskName .. " : " .. From .. " changed to " .. To .. " by " .. Event, 2 ):ToAll()
  end

  if self.Scores[To] then
    local Scoring = self:GetScoring()
    if Scoring then
      self:F( { self.Scores[To].ScoreText, self.Scores[To].Score } )
      Scoring:_AddMissionScore( self.Mission, self.Scores[To].ScoreText, self.Scores[To].Score )
    end
  end

end

--- FSM function for a TASK
-- @param #TASK self
-- @param #string Event
-- @param #string From
-- @param #string To
function TASK:onenterPlanned( From, Event, To)
  if not self.TimeOut == 0 then 
    self.__TimeOut( self.TimeOut )
  end
end

--- FSM function for a TASK
-- @param #TASK self
-- @param #string Event
-- @param #string From
-- @param #string To
function TASK:onbeforeTimeOut( From, Event, To )
  if From == "Planned" then
    self:RemoveMenu()
    return true
  end
  return false
end

do -- Links

  --- Set goal of a task
  -- @param #TASK self
  -- @param Core.Goal#GOAL Goal
  -- @return #TASK
  function TASK:SetGoal( Goal )
    self.Goal = Goal
  end


  --- Get goal of a task
  -- @param #TASK self
  -- @return Core.Goal#GOAL The Goal
  function TASK:GetGoal()
    return self.Goal
  end


  --- Set dispatcher of a task
  -- @param #TASK self
  -- @param Tasking.DetectionManager#DETECTION_MANAGER Dispatcher
  -- @return #TASK
  function TASK:SetDispatcher( Dispatcher )
    self.Dispatcher = Dispatcher
  end

  --- Set detection of a task
  -- @param #TASK self
  -- @param Functional.Detection#DETECTION_BASE Detection
  -- @param DetectedItem
  -- @return #TASK
  function TASK:SetDetection( Detection, DetectedItem )
    
    self:F( { DetectedItem, Detection } )
    
    self.Detection = Detection
    self.DetectedItem = DetectedItem
  end

end

do -- Reporting

--- Create a summary report of the Task.
-- List the Task Name and Status
-- @param #TASK self
-- @param Wrapper.Group#GROUP ReportGroup
-- @return #string
function TASK:ReportSummary( ReportGroup ) 

  self:UpdateTaskInfo( self.DetectedItem )
  
  local Report = REPORT:New()
  
  -- List the name of the Task.
  Report:Add( "Task " .. self:GetName() )
  
  -- Determine the status of the Task.
  Report:Add( "State: <" .. self:GetState() .. ">" )
  
  self.TaskInfo:Report( Report, "S", ReportGroup, self )
  
  return Report:Text( ', ' )
end

--- Create an overiew report of the Task.
-- List the Task Name and Status
-- @param #TASK self
-- @return #string
function TASK:ReportOverview( ReportGroup )

  self:UpdateTaskInfo( self.DetectedItem )
  
  -- List the name of the Task.
  local TaskName = self:GetName()
  local Report = REPORT:New()
  
  self.TaskInfo:Report( Report, "O", ReportGroup, self )
  
  return Report:Text()
end

--- Create a count of the players in the Task.
-- @param #TASK self
-- @return #number The total number of players in the task.
function TASK:GetPlayerCount() --R2.1 Get a count of the players.

  local PlayerCount = 0

  -- Loop each Unit active in the Task, and find Player Names.
  for TaskGroupID, PlayerGroup in pairs( self:GetGroups():GetSet() ) do
    local PlayerGroup = PlayerGroup -- Wrapper.Group#GROUP
    if PlayerGroup:IsAlive() == true then
      if self:IsGroupAssigned( PlayerGroup ) then
        local PlayerNames = PlayerGroup:GetPlayerNames()
        PlayerCount = PlayerCount + ((PlayerNames) and #PlayerNames or 0) -- PlayerNames can be nil when there are no players.
      end
    end
  end

  return PlayerCount
end


--- Create a list of the players in the Task.
-- @param #TASK self
-- @return #map<#string,Wrapper.Group#GROUP> A map of the players
function TASK:GetPlayerNames() --R2.1 Get a map of the players.

  local PlayerNameMap = {}

  -- Loop each Unit active in the Task, and find Player Names.
  for TaskGroupID, PlayerGroup in pairs( self:GetGroups():GetSet() ) do
    local PlayerGroup = PlayerGroup -- Wrapper.Group#GROUP
    if PlayerGroup:IsAlive() == true then
      if self:IsGroupAssigned( PlayerGroup ) then
        local PlayerNames = PlayerGroup:GetPlayerNames()
        for PlayerNameID, PlayerName in pairs( PlayerNames or {} ) do
          PlayerNameMap[PlayerName] = PlayerGroup
        end
      end
    end
  end

  return PlayerNameMap
end


--- Create a detailed report of the Task.
-- List the Task Status, and the Players assigned to the Task.
-- @param #TASK self
-- @param Wrapper.Group#GROUP TaskGroup
-- @return #string
function TASK:ReportDetails( ReportGroup )

  self:UpdateTaskInfo( self.DetectedItem )

  local Report = REPORT:New():SetIndent( 3 )
  
  -- List the name of the Task.
  local Name = self:GetName()
  
  -- Determine the status of the Task.
  local Status = "<" .. self:GetState() .. ">"

  Report:Add( "Task " .. Name .. " - " .. Status .. " - Detailed Report" )

  -- Loop each Unit active in the Task, and find Player Names.
  local PlayerNames = self:GetPlayerNames()
  
  local PlayerReport = REPORT:New()
  for PlayerName, PlayerGroup in pairs( PlayerNames ) do
    PlayerReport:Add( "Players group " .. PlayerGroup:GetCallsign() .. ": " .. PlayerName )
  end
  local Players = PlayerReport:Text()
  
  if Players ~= "" then
    Report:AddIndent( "Players assigned:", "-" )
    Report:AddIndent( Players )
  end
  
  self.TaskInfo:Report( Report, "D", ReportGroup, self )
  
  return Report:Text()
end


end -- Reporting


do -- Additional Task Scoring and Task Progress

  --- Add Task Progress for a Player Name
  -- @param #TASK self
  -- @param #string PlayerName The name of the player.
  -- @param #string ProgressText The text that explains the Progress achieved.
  -- @param #number ProgressTime The time the progress was achieved.
  -- @oaram #number ProgressPoints The amount of points of magnitude granted. This will determine the shared Mission Success scoring.
  -- @return #TASK
  function TASK:AddProgress( PlayerName, ProgressText, ProgressTime, ProgressPoints )
    self.TaskProgress = self.TaskProgress or {}
    self.TaskProgress[ProgressTime] = self.TaskProgress[ProgressTime] or {}
    self.TaskProgress[ProgressTime].PlayerName = PlayerName
    self.TaskProgress[ProgressTime].ProgressText = ProgressText
    self.TaskProgress[ProgressTime].ProgressPoints = ProgressPoints
    self:GetMission():AddPlayerName( PlayerName )
    return self
  end
  
  function TASK:GetPlayerProgress( PlayerName )
    local ProgressPlayer = 0
    for ProgressTime, ProgressData in pairs( self.TaskProgress ) do
      if PlayerName == ProgressData.PlayerName then
        ProgressPlayer = ProgressPlayer + ProgressData.ProgressPoints
      end
    end
    return ProgressPlayer
  end

  --- Set a score when progress has been made by the player.
  -- @param #TASK self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points to be granted when task process has been achieved.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK
  function TASK:SetScoreOnProgress( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountPlayer", "Player " .. PlayerName .. " has achieved progress.", Score )
    
    return self
  end

  --- Set a score when all the targets in scope of the A2A attack, have been destroyed.
  -- @param #TASK self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK
  function TASK:SetScoreOnSuccess( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Success", "The task is a success!", Score )
    
    return self
  end

  --- Set a penalty when the A2A attack has failed.
  -- @param #TASK self
  -- @param #string PlayerName The name of the player.
  -- @param #number Penalty The penalty in points, must be a negative value!
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK
  function TASK:SetScoreOnFail( PlayerName, Penalty, TaskUnit )
    self:F( { PlayerName, Penalty, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Failed", "The task is a failure!", Penalty )
    
    return self
  end

end

do -- Task Control Menu
  
  -- The Task Control Menu is a menu attached to the task at the main menu to quickly be able to do actions in the task.
  -- The Task Control Menu can only be shown when the task is assigned to the player.
  -- The Task Control Menu is linked to the process executing the task, so no task menu can be set to the main static task definition.
  
  --- Init Task Control Menu
  -- @param #TASK self
  -- @param Wrapper.Unit#UNIT TaskUnit The @{Wrapper.Unit} that contains a player.
  -- @return Task Control Menu Refresh ID
  function TASK:InitTaskControlMenu( TaskUnit )

    self.TaskControlMenuTime = timer.getTime()
    
    return self.TaskControlMenuTime
  end
  
  --- Get Task Control Menu
  -- @param #TASK self
  -- @param Wrapper.Unit#UNIT TaskUnit The @{Wrapper.Unit} that contains a player.
  -- @return Core.Menu#MENU_GROUP TaskControlMenu The Task Control Menu
  function TASK:GetTaskControlMenu( TaskUnit, TaskName )
  
    TaskName = TaskName or ""
    
    local TaskGroup = TaskUnit:GetGroup()
    local TaskPlayerCount = TaskGroup:GetPlayerCount()
    
    if TaskPlayerCount <= 1 then
      self.TaskControlMenu = MENU_GROUP:New( TaskUnit:GetGroup(), "Task " .. self:GetName() .. " control" ):SetTime( self.TaskControlMenuTime )
    else
      self.TaskControlMenu = MENU_GROUP:New( TaskUnit:GetGroup(), "Task " .. self:GetName() .. " control for " .. TaskUnit:GetPlayerName() ):SetTime( self.TaskControlMenuTime )
    end
    
    return self.TaskControlMenu
  end

  --- Remove Task Control Menu
  -- @param #TASK self
  -- @param Wrapper.Unit#UNIT TaskUnit The @{Wrapper.Unit} that contains a player.
  function TASK:RemoveTaskControlMenu( TaskUnit )
  
    if self.TaskControlMenu then
      self.TaskControlMenu:Remove()
      self.TaskControlMenu = nil
    end
  end
  
  --- Refresh Task Control Menu
  -- @param #TASK self
  -- @param Wrapper.Unit#UNIT TaskUnit The @{Wrapper.Unit} that contains a player.
  -- @param MenuTime The refresh time that was used to refresh the Task Control Menu items.
  -- @param MenuTag The tag.
  function TASK:RefreshTaskControlMenu( TaskUnit, MenuTime, MenuTag )
  
    if self.TaskControlMenu then
      self.TaskControlMenu:Remove( MenuTime, MenuTag )
    end
  end
  
end
--- **Tasking** - Controls the information of a Task.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Tasking.TaskInfo
-- @image MOOSE.JPG

---
-- @type TASKINFO
-- @extends Core.Base#BASE

--- 
-- # TASKINFO class, extends @{Core.Base#BASE}
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- ## The TASKINFO class implements the methods to contain information and display information of a task. 
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- @field #TASKINFO
TASKINFO = {
  ClassName = "TASKINFO",
}

---
-- @type TASKINFO.Detail #string A string that flags to document which level of detail needs to be shown in the report.
-- 
--   - "M" for Markings on the Map (F10).
--   - "S" for Summary Reports.
--   - "O" for Overview Reports.
--   - "D" for Detailed Reports.
TASKINFO.Detail = ""

--- Instantiates a new TASKINFO. 
-- @param #TASKINFO self
-- @param Tasking.Task#TASK Task The task owning the information.
-- @return #TASKINFO self
function TASKINFO:New( Task )

  local self = BASE:Inherit( self, BASE:New() ) -- Core.Base#BASE
  
  self.Task = Task
  self.VolatileInfo = SET_BASE:New()
  self.PersistentInfo = SET_BASE:New()
  
  self.Info = self.VolatileInfo
  
  return self
end


--- Add taskinfo. 
-- @param #TASKINFO self
-- @param #string Key The info key.
-- @param Data The data of the info.
-- @param #number Order The display order, which is a number from 0 to 100.
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports.
-- @return #TASKINFO self
function TASKINFO:AddInfo( Key, Data, Order, Detail, Keep, ShowKey, Type )
  self.VolatileInfo:Add( Key, { Data = Data, Order = Order, Detail = Detail, ShowKey = ShowKey, Type = Type } )
  if Keep == true then
    self.PersistentInfo:Add( Key, { Data = Data, Order = Order, Detail = Detail, ShowKey = ShowKey, Type = Type } )
  end
  return self
end


--- Get taskinfo. 
-- @param #TASKINFO self
-- @param #string The info key.
-- @return Data The data of the info.
-- @return #number Order The display order, which is a number from 0 to 100.
-- @return #TASKINFO.Detail Detail The detail Level.
function TASKINFO:GetInfo( Key )
  local Object = self:Get( Key )
  return Object.Data, Object.Order, Object.Detail
end


--- Get data. 
-- @param #TASKINFO self
-- @param #string The info key.
-- @return Data The data of the info.
function TASKINFO:GetData( Key )
  local Object = self.Info:Get( Key )
  return Object and Object.Data
end


--- Add Text. 
-- @param #TASKINFO self
-- @param #string Key The key.
-- @param #string Text The text.
-- @param #number Order The display order, which is a number from 0 to 100.
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports.
-- @return #TASKINFO self
function TASKINFO:AddText( Key, Text, Order, Detail, Keep )
  self:AddInfo( Key, Text, Order, Detail, Keep )
  return self
end


--- Add the task name. 
-- @param #TASKINFO self
-- @param #number Order The display order, which is a number from 0 to 100.
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports.
-- @return #TASKINFO self
function TASKINFO:AddTaskName( Order, Detail, Keep )
  self:AddInfo( "TaskName", self.Task:GetName(), Order, Detail, Keep )
  return self
end




--- Add a Coordinate. 
-- @param #TASKINFO self
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Order The display order, which is a number from 0 to 100.
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports.
-- @return #TASKINFO self
function TASKINFO:AddCoordinate( Coordinate, Order, Detail, Keep, ShowKey, Name )
  self:AddInfo( Name or "Coordinate", Coordinate, Order, Detail, Keep, ShowKey, "Coordinate" )
  return self
end


--- Get the Coordinate. 
-- @param #TASKINFO self
-- @return Core.Point#COORDINATE Coordinate
function TASKINFO:GetCoordinate( Name )
  return self:GetData( Name or "Coordinate" )
end



--- Add Coordinates. 
-- @param #TASKINFO self
-- @param #list<Core.Point#COORDINATE> Coordinates
-- @param #number Order The display order, which is a number from 0 to 100.
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports.
-- @return #TASKINFO self
function TASKINFO:AddCoordinates( Coordinates, Order, Detail, Keep )
  self:AddInfo( "Coordinates", Coordinates, Order, Detail, Keep )
  return self
end



--- Add Threat. 
-- @param #TASKINFO self
-- @param #string ThreatText The text of the Threat.
-- @param #string ThreatLevel The level of the Threat.
-- @param #number Order The display order, which is a number from 0 to 100.
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports.
-- @return #TASKINFO self
function TASKINFO:AddThreat( ThreatText, ThreatLevel, Order, Detail, Keep )
  self:AddInfo( "Threat", " [" .. string.rep(  "■", ThreatLevel ) .. string.rep(  "□", 10 - ThreatLevel ) .. "]:" .. ThreatText, Order, Detail, Keep )
  return self
end


--- Get Threat. 
-- @param #TASKINFO self
-- @return #string The threat
function TASKINFO:GetThreat()
  self:GetInfo( "Threat" )
  return self
end



--- Add the Target count. 
-- @param #TASKINFO self
-- @param #number TargetCount The amount of targets.
-- @param #number Order The display order, which is a number from 0 to 100.
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports.
-- @return #TASKINFO self
function TASKINFO:AddTargetCount( TargetCount, Order, Detail, Keep )
  self:AddInfo( "Counting", string.format( "%d", TargetCount ), Order, Detail, Keep )
  return self
end

--- Add the Targets. 
-- @param #TASKINFO self
-- @param #number TargetCount The amount of targets.
-- @param #string TargetTypes The text containing the target types.
-- @param #number Order The display order, which is a number from 0 to 100.
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports.
-- @return #TASKINFO self
function TASKINFO:AddTargets( TargetCount, TargetTypes, Order, Detail, Keep )
  self:AddInfo( "Targets", string.format( "%d of %s", TargetCount, TargetTypes ), Order, Detail, Keep )
  return self
end

--- Get Targets. 
-- @param #TASKINFO self
-- @return #string The targets
function TASKINFO:GetTargets()
  self:GetInfo( "Targets" )
  return self
end




--- Add the QFE at a Coordinate. 
-- @param #TASKINFO self
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Order The display order, which is a number from 0 to 100.
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports.
-- @return #TASKINFO self
function TASKINFO:AddQFEAtCoordinate( Coordinate, Order, Detail, Keep )
  self:AddInfo( "QFE", Coordinate, Order, Detail, Keep )
  return self
end

--- Add the Temperature at a Coordinate. 
-- @param #TASKINFO self
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Order The display order, which is a number from 0 to 100.
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports.
-- @return #TASKINFO self
function TASKINFO:AddTemperatureAtCoordinate( Coordinate, Order, Detail, Keep )
  self:AddInfo( "Temperature", Coordinate, Order, Detail, Keep )
  return self
end

--- Add the Wind at a Coordinate. 
-- @param #TASKINFO self
-- @param Core.Point#COORDINATE Coordinate
-- @param #number Order The display order, which is a number from 0 to 100.
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports.
-- @return #TASKINFO self
function TASKINFO:AddWindAtCoordinate( Coordinate, Order, Detail, Keep )
  self:AddInfo( "Wind", Coordinate, Order, Detail, Keep )
  return self
end

--- Add Cargo. 
-- @param #TASKINFO self
-- @param Cargo.Cargo#CARGO Cargo
-- @param #number Order The display order, which is a number from 0 to 100.
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports.
-- @return #TASKINFO self
function TASKINFO:AddCargo( Cargo, Order, Detail, Keep )
  self:AddInfo( "Cargo", Cargo, Order, Detail, Keep )
  return self
end


--- Add Cargo set. 
-- @param #TASKINFO self
-- @param Core.Set#SET_CARGO SetCargo
-- @param #number Order The display order, which is a number from 0 to 100.
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param #boolean Keep (optional) If true, this would indicate that the planned taskinfo would be persistent when the task is completed, so that the original planned task info is used at the completed reports.
-- @return #TASKINFO self
function TASKINFO:AddCargoSet( SetCargo, Order, Detail, Keep )

  local CargoReport = REPORT:New()
  CargoReport:Add( "" )
  SetCargo:ForEachCargo(
    -- @param Cargo.Cargo#CARGO Cargo
    function( Cargo )
      CargoReport:Add( string.format( ' - %s (%s) %s - status %s ', Cargo:GetName(), Cargo:GetType(), Cargo:GetTransportationMethod(), Cargo:GetCurrentState() ) )
    end
  )

  self:AddInfo( "Cargo", CargoReport:Text(), Order, Detail, Keep )
  

  return self
end



--- Create the taskinfo Report
-- @param #TASKINFO self
-- @param Core.Report#REPORT Report
-- @param #TASKINFO.Detail Detail The detail Level.
-- @param Wrapper.Group#GROUP ReportGroup
-- @param Tasking.Task#TASK Task
-- @return #TASKINFO self
function TASKINFO:Report( Report, Detail, ReportGroup, Task )

  local Line = 0
  local LineReport = REPORT:New()

  if not self.Task:IsStatePlanned() and not self.Task:IsStateAssigned() then
    self.Info = self.PersistentInfo
  end

  for Key, Data in UTILS.spairs( self.Info.Set, function( t, a, b ) return t[a].Order < t[b].Order end ) do

    if Data.Detail:find( Detail ) then
      local Text = ""
      local ShowKey = ( Data.ShowKey == nil or Data.ShowKey == true )
      if     Key == "TaskName" then
        Key = nil
        Text = Data.Data
      elseif Data.Type and Data.Type == "Coordinate" then
        local Coordinate = Data.Data -- Core.Point#COORDINATE
        Text = Coordinate:ToString( ReportGroup:GetUnit(1), nil, Task )
      elseif Key == "Threat" then
        local DataText = Data.Data -- #string
        Text = DataText
      elseif Key == "Counting" then
        local DataText = Data.Data -- #string
        Text = DataText
      elseif Key == "Targets" then
        local DataText = Data.Data -- #string
        Text = DataText
      elseif Key == "QFE" then
        local Coordinate = Data.Data -- Core.Point#COORDINATE
        Text = Coordinate:ToStringPressure( ReportGroup:GetUnit(1), nil, Task )
      elseif Key == "Temperature" then
        local Coordinate = Data.Data -- Core.Point#COORDINATE
        Text = Coordinate:ToStringTemperature( ReportGroup:GetUnit(1), nil, Task )
      elseif Key == "Wind" then
        local Coordinate = Data.Data -- Core.Point#COORDINATE
        Text = Coordinate:ToStringWind( ReportGroup:GetUnit(1), nil, Task )
      elseif Key == "Cargo" then
        local DataText = Data.Data -- #string
        Text = DataText
      elseif Key == "Friendlies" then
        local DataText = Data.Data -- #string
        Text = DataText
      elseif Key == "Players" then
        local DataText = Data.Data -- #string
        Text = DataText
      else
        local DataText = Data.Data -- #string
        if type(DataText) == "string" then --Issue #1388 - don't just assume this is a string
          Text = DataText
        end
      end

      if Line < math.floor( Data.Order / 10 ) then
        if Line == 0 then
          Report:AddIndent( LineReport:Text( ", " ), "-" )
        else
          Report:AddIndent( LineReport:Text( ", " ) )
        end
        LineReport = REPORT:New()
        Line = math.floor( Data.Order / 10 )
      end

      if Text ~= "" then
        LineReport:Add( ( ( Key and ShowKey == true ) and ( Key .. ": " ) or "" ) .. Text )
      end

    end
  end
  
  Report:AddIndent( LineReport:Text( ", " ) )
end
--- **Tasking** - This module contains the TASK_MANAGER class and derived classes.
-- 
-- ===
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- 1) @{Tasking.Task_Manager#TASK_MANAGER} class, extends @{Core.Fsm#FSM}
-- ===
-- The @{Tasking.Task_Manager#TASK_MANAGER} class defines the core functions to report tasks to groups.
-- Reportings can be done in several manners, and it is up to the derived classes if TASK_MANAGER to model the reporting behaviour.
-- 
-- 1.1) TASK_MANAGER constructor:
-- -----------------------------------
--   * @{Tasking.Task_Manager#TASK_MANAGER.New}(): Create a new TASK_MANAGER instance.
-- 
-- 1.2) TASK_MANAGER reporting:
-- ---------------------------------
-- Derived TASK_MANAGER classes will manage tasks using the method @{Tasking.Task_Manager#TASK_MANAGER.ManageTasks}(). This method implements polymorphic behaviour.
-- 
-- The time interval in seconds of the task management can be changed using the methods @{Tasking.Task_Manager#TASK_MANAGER.SetRefreshTimeInterval}(). 
-- To control how long a reporting message is displayed, use @{Tasking.Task_Manager#TASK_MANAGER.SetReportDisplayTime}().
-- Derived classes need to implement the method @{Tasking.Task_Manager#TASK_MANAGER.GetReportDisplayTime}() to use the correct display time for displayed messages during a report.
-- 
-- Task management can be started and stopped using the methods @{Tasking.Task_Manager#TASK_MANAGER.StartTasks}() and @{Tasking.Task_Manager#TASK_MANAGER.StopTasks}() respectively.
-- If an ad-hoc report is requested, use the method @{Tasking.Task_Manager#TASK_MANAGER#ManageTasks}().
-- 
-- The default task management interval is every 60 seconds.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- ### Contributions: Mechanist, Prof_Hilactic, FlightControl - Concept & Testing
-- ### Author: FlightControl - Framework Design &  Programming
-- 
-- @module Tasking.Task_Manager
-- @image MOOSE.JPG

do -- TASK_MANAGER

  --- TASK_MANAGER class.
  -- @type TASK_MANAGER
  -- @field Core.Set#SET_GROUP SetGroup The set of group objects containing players for which tasks are managed.
  -- @extends Core.Fsm#FSM
  TASK_MANAGER = {
    ClassName = "TASK_MANAGER",
    SetGroup = nil,
  }

  --- TASK\_MANAGER constructor.
  -- @param #TASK_MANAGER self
  -- @param Core.Set#SET_GROUP SetGroup The set of group objects containing players for which tasks are managed.
  -- @return #TASK_MANAGER self
  function TASK_MANAGER:New( SetGroup )

    -- Inherits from BASE
    local self = BASE:Inherit( self, FSM:New() ) -- #TASK_MANAGER

    self.SetGroup = SetGroup

    self:SetStartState( "Stopped" )
    self:AddTransition( "Stopped", "StartTasks", "Started" )

    --- StartTasks Handler OnBefore for TASK_MANAGER
    -- @function [parent=#TASK_MANAGER] OnBeforeStartTasks
    -- @param #TASK_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean

    --- StartTasks Handler OnAfter for TASK_MANAGER
    -- @function [parent=#TASK_MANAGER] OnAfterStartTasks
    -- @param #TASK_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To

    --- StartTasks Trigger for TASK_MANAGER
    -- @function [parent=#TASK_MANAGER] StartTasks
    -- @param #TASK_MANAGER self

    --- StartTasks Asynchronous Trigger for TASK_MANAGER
    -- @function [parent=#TASK_MANAGER] __StartTasks
    -- @param #TASK_MANAGER self
    -- @param #number Delay

    self:AddTransition( "Started", "StopTasks", "Stopped" )

    --- StopTasks Handler OnBefore for TASK_MANAGER
    -- @function [parent=#TASK_MANAGER] OnBeforeStopTasks
    -- @param #TASK_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean

    --- StopTasks Handler OnAfter for TASK_MANAGER
    -- @function [parent=#TASK_MANAGER] OnAfterStopTasks
    -- @param #TASK_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To

    --- StopTasks Trigger for TASK_MANAGER
    -- @function [parent=#TASK_MANAGER] StopTasks
    -- @param #TASK_MANAGER self

    --- StopTasks Asynchronous Trigger for TASK_MANAGER
    -- @function [parent=#TASK_MANAGER] __StopTasks
    -- @param #TASK_MANAGER self
    -- @param #number Delay

    self:AddTransition( "Started", "Manage", "Started" )

    self:AddTransition( "Started", "Success", "Started" )

    --- Success Handler OnAfter for TASK_MANAGER
    -- @function [parent=#TASK_MANAGER] OnAfterSuccess
    -- @param #TASK_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Tasking.Task#TASK Task

    self:AddTransition( "Started", "Failed", "Started" )

    --- Failed Handler OnAfter for TASK_MANAGER
    -- @function [parent=#TASK_MANAGER] OnAfterFailed
    -- @param #TASK_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Tasking.Task#TASK Task

    self:AddTransition( "Started", "Aborted", "Started" )

    --- Aborted Handler OnAfter for TASK_MANAGER
    -- @function [parent=#TASK_MANAGER] OnAfterAborted
    -- @param #TASK_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Tasking.Task#TASK Task

    self:AddTransition( "Started", "Cancelled", "Started" )

    --- Cancelled Handler OnAfter for TASK_MANAGER
    -- @function [parent=#TASK_MANAGER] OnAfterCancelled
    -- @param #TASK_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Tasking.Task#TASK Task

    self:SetRefreshTimeInterval( 30 )

    return self
  end

  function TASK_MANAGER:onafterStartTasks( From, Event, To )
    self:Manage()
  end

  function TASK_MANAGER:onafterManage( From, Event, To )

    self:__Manage( -self._RefreshTimeInterval )

    self:ManageTasks()
  end

  --- Set the refresh time interval in seconds when a new task management action needs to be done.
  -- @param #TASK_MANAGER self
  -- @param #number RefreshTimeInterval The refresh time interval in seconds when a new task management action needs to be done.
  -- @return #TASK_MANAGER self
  function TASK_MANAGER:SetRefreshTimeInterval( RefreshTimeInterval )
    self:F2()

    self._RefreshTimeInterval = RefreshTimeInterval
  end


  --- Manages the tasks for the @{Core.Set#SET_GROUP}.
  -- @param #TASK_MANAGER self
  -- @return #TASK_MANAGER self
  function TASK_MANAGER:ManageTasks()

  end

end

--- **Tasking** - This module contains the DETECTION_MANAGER class and derived classes.
-- 
-- ===
-- 
-- The @{#DETECTION_MANAGER} class defines the core functions to report detected objects to groups.
-- Reportings can be done in several manners, and it is up to the derived classes if DETECTION_MANAGER to model the reporting behaviour.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- 1.1) DETECTION_MANAGER constructor:
-- -----------------------------------
--   * @{#DETECTION_MANAGER.New}(): Create a new DETECTION_MANAGER instance.
-- 
-- 1.2) DETECTION_MANAGER reporting:
-- ---------------------------------
-- Derived DETECTION_MANAGER classes will reports detected units using the method @{#DETECTION_MANAGER.ReportDetected}(). This method implements polymorphic behaviour.
-- 
-- The time interval in seconds of the reporting can be changed using the methods @{#DETECTION_MANAGER.SetRefreshTimeInterval}(). 
-- To control how long a reporting message is displayed, use @{#DETECTION_MANAGER.SetReportDisplayTime}().
-- Derived classes need to implement the method @{#DETECTION_MANAGER.GetReportDisplayTime}() to use the correct display time for displayed messages during a report.
-- 
-- Reporting can be started and stopped using the methods @{#DETECTION_MANAGER.StartReporting}() and @{#DETECTION_MANAGER.StopReporting}() respectively.
-- If an ad-hoc report is requested, use the method @{#DETECTION_MANAGER.ReportNow}().
-- 
-- The default reporting interval is every 60 seconds. The reporting messages are displayed 15 seconds.
-- 
-- ===
-- 
-- 2) @{#DETECTION_REPORTING} class, extends @{#DETECTION_MANAGER}
-- ===
-- The @{#DETECTION_REPORTING} class implements detected units reporting. Reporting can be controlled using the reporting methods available in the @{Tasking.DetectionManager#DETECTION_MANAGER} class.
-- 
-- 2.1) DETECTION_REPORTING constructor:
-- -------------------------------
-- The @{#DETECTION_REPORTING.New}() method creates a new DETECTION_REPORTING instance.
--    
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--   
-- ===
-- 
-- ### Contributions: Mechanist, Prof_Hilactic, FlightControl - Concept & Testing
-- ### Author: FlightControl - Framework Design &  Programming
-- 
-- @module Tasking.DetectionManager
-- @image Task_Detection_Manager.JPG

do -- DETECTION MANAGER
  
  -- @type DETECTION_MANAGER
  -- @field Core.Set#SET_GROUP SetGroup The groups to which the FAC will report to.
  -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects.
  -- @field Tasking.CommandCenter#COMMANDCENTER CC The command center that is used to communicate with the players.
  -- @extends Core.Fsm#FSM

  --- DETECTION_MANAGER class.
  -- @field #DETECTION_MANAGER
  DETECTION_MANAGER = {
    ClassName = "DETECTION_MANAGER",
    SetGroup = nil,
    Detection = nil,
  }
  
  -- @field Tasking.CommandCenter#COMMANDCENTER
  DETECTION_MANAGER.CC = nil
  
  --- FAC constructor.
  -- @param #DETECTION_MANAGER self
  -- @param Core.Set#SET_GROUP SetGroup
  -- @param Functional.Detection#DETECTION_BASE Detection
  -- @return #DETECTION_MANAGER self
  function DETECTION_MANAGER:New( SetGroup, Detection )
  
    -- Inherits from BASE
    local self = BASE:Inherit( self, FSM:New() ) -- #DETECTION_MANAGER
    
    self.SetGroup = SetGroup
    self.Detection = Detection
    
    self:SetStartState( "Stopped" )
    self:AddTransition( "Stopped", "Start", "Started" )
    
    --- Start Handler OnBefore for DETECTION_MANAGER
    -- @function [parent=#DETECTION_MANAGER] OnBeforeStart
    -- @param #DETECTION_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Start Handler OnAfter for DETECTION_MANAGER
    -- @function [parent=#DETECTION_MANAGER] OnAfterStart
    -- @param #DETECTION_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Start Trigger for DETECTION_MANAGER
    -- @function [parent=#DETECTION_MANAGER] Start
    -- @param #DETECTION_MANAGER self
    
    --- Start Asynchronous Trigger for DETECTION_MANAGER
    -- @function [parent=#DETECTION_MANAGER] __Start
    -- @param #DETECTION_MANAGER self
    -- @param #number Delay
    
    
    
    self:AddTransition( "Started", "Stop", "Stopped" )
    
    --- Stop Handler OnBefore for DETECTION_MANAGER
    -- @function [parent=#DETECTION_MANAGER] OnBeforeStop
    -- @param #DETECTION_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @return #boolean
    
    --- Stop Handler OnAfter for DETECTION_MANAGER
    -- @function [parent=#DETECTION_MANAGER] OnAfterStop
    -- @param #DETECTION_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    
    --- Stop Trigger for DETECTION_MANAGER
    -- @function [parent=#DETECTION_MANAGER] Stop
    -- @param #DETECTION_MANAGER self
    
    --- Stop Asynchronous Trigger for DETECTION_MANAGER
    -- @function [parent=#DETECTION_MANAGER] __Stop
    -- @param #DETECTION_MANAGER self
    -- @param #number Delay

    self:AddTransition( "Started", "Success", "Started" )
    
    --- Success Handler OnAfter for DETECTION_MANAGER
    -- @function [parent=#DETECTION_MANAGER] OnAfterSuccess
    -- @param #DETECTION_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Tasking.Task#TASK Task
    
    
    self:AddTransition( "Started", "Failed", "Started" )
    
    --- Failed Handler OnAfter for DETECTION_MANAGER
    -- @function [parent=#DETECTION_MANAGER] OnAfterFailed
    -- @param #DETECTION_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Tasking.Task#TASK Task
    
    
    self:AddTransition( "Started", "Aborted", "Started" )
    
    --- Aborted Handler OnAfter for DETECTION_MANAGER
    -- @function [parent=#DETECTION_MANAGER] OnAfterAborted
    -- @param #DETECTION_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Tasking.Task#TASK Task
    
    self:AddTransition( "Started", "Cancelled", "Started" )
    
    --- Cancelled Handler OnAfter for DETECTION_MANAGER
    -- @function [parent=#DETECTION_MANAGER] OnAfterCancelled
    -- @param #DETECTION_MANAGER self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Tasking.Task#TASK Task
    

    self:AddTransition( "Started", "Report", "Started" )
    
    self:SetRefreshTimeInterval( 30 )
    self:SetReportDisplayTime( 25 )
  
    Detection:__Start( 3 )

    return self
  end
  
  function DETECTION_MANAGER:onafterStart( From, Event, To )
    self:Report()
  end
  
  function DETECTION_MANAGER:onafterReport( From, Event, To )

    self:__Report( -self._RefreshTimeInterval )
    
    self:ProcessDetected( self.Detection )
  end
  
  --- Set the reporting time interval.
  -- @param #DETECTION_MANAGER self
  -- @param #number RefreshTimeInterval The interval in seconds when a report needs to be done.
  -- @return #DETECTION_MANAGER self
  function DETECTION_MANAGER:SetRefreshTimeInterval( RefreshTimeInterval )
    self:F2()
  
    self._RefreshTimeInterval = RefreshTimeInterval
  end
  
  
  --- Set the reporting message display time.
  -- @param #DETECTION_MANAGER self
  -- @param #number ReportDisplayTime The display time in seconds when a report needs to be done.
  -- @return #DETECTION_MANAGER self
  function DETECTION_MANAGER:SetReportDisplayTime( ReportDisplayTime )
    self:F2()
  
    self._ReportDisplayTime = ReportDisplayTime
  end
  
  --- Get the reporting message display time.
  -- @param #DETECTION_MANAGER self
  -- @return #number ReportDisplayTime The display time in seconds when a report needs to be done.
  function DETECTION_MANAGER:GetReportDisplayTime()
    self:F2()
  
    return self._ReportDisplayTime
  end


  --- Set a command center to communicate actions to the players reporting to the command center.
  -- @param #DETECTION_MANAGER self
  -- @return #DETECTION_MANGER self
  function DETECTION_MANAGER:SetTacticalMenu( DispatcherMainMenuText, DispatcherMenuText )

    local DispatcherMainMenu = MENU_MISSION:New( DispatcherMainMenuText, nil )
    local DispatcherMenu = MENU_MISSION_COMMAND:New( DispatcherMenuText, DispatcherMainMenu,
      function()
        self:ShowTacticalDisplay( self.Detection )
      end
      )
    
    return self
  end
  
  

  
  --- Set a command center to communicate actions to the players reporting to the command center.
  -- @param #DETECTION_MANAGER self
  -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center.
  -- @return #DETECTION_MANGER self
  function DETECTION_MANAGER:SetCommandCenter( CommandCenter )
    
    self.CC = CommandCenter
    
    return self
  end
  
  
  --- Get the command center to communicate actions to the players.
  -- @param #DETECTION_MANAGER self
  -- @return Tasking.CommandCenter#COMMANDCENTER The command center.
  function DETECTION_MANAGER:GetCommandCenter()
    
    return self.CC
  end
 
  
  --- Send an information message to the players reporting to the command center.
  -- @param #DETECTION_MANAGER self
  -- @param #table Squadron The squadron table.
  -- @param #string Message The message to be sent.
  -- @param #string SoundFile The name of the sound file .wav or .ogg.
  -- @param #number SoundDuration The duration of the sound.
  -- @param #string SoundPath The path pointing to the folder in the mission file.
  -- @param Wrapper.Group#GROUP DefenderGroup The defender group sending the message.
  -- @return #DETECTION_MANGER self
  function DETECTION_MANAGER:MessageToPlayers( Squadron,  Message, DefenderGroup )
  
    self:F( { Message = Message } )
    
--    if not self.PreviousMessage or self.PreviousMessage ~= Message then
--      self.PreviousMessage = Message
--      if self.CC then
--        self.CC:MessageToCoalition( Message )
--      end
--    end

    if self.CC then
      self.CC:MessageToCoalition( Message )
    end
    
    Message = Message:gsub( "°", " degrees " )
    Message = Message:gsub( "(%d)%.(%d)", "%1 dot %2" )
    
  -- Here we handle the transmission of the voice over.
  -- If for a certain reason the Defender does not exist, we use the coordinate of the airbase to send the message from.
    local RadioQueue = Squadron.RadioQueue -- Core.RadioSpeech#RADIOSPEECH
    if RadioQueue then
      local DefenderUnit = DefenderGroup:GetUnit(1)
      if DefenderUnit and DefenderUnit:IsAlive() then
        RadioQueue:SetSenderUnitName( DefenderUnit:GetName() )
      end
      RadioQueue:Speak( Message, Squadron.Language )
    end
    
    return self
  end
  
  
  
  --- Reports the detected items to the @{Core.Set#SET_GROUP}.
  -- @param #DETECTION_MANAGER self
  -- @param Functional.Detection#DETECTION_BASE Detection
  -- @return #DETECTION_MANAGER self
  function DETECTION_MANAGER:ProcessDetected( Detection )
  
  end

end


do -- DETECTION_REPORTING

  --- DETECTION_REPORTING class.
  -- @type DETECTION_REPORTING
  -- @field Core.Set#SET_GROUP SetGroup The groups to which the FAC will report to.
  -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects.
  -- @extends #DETECTION_MANAGER
  DETECTION_REPORTING = {
    ClassName = "DETECTION_REPORTING",
  }
  
  
  --- DETECTION_REPORTING constructor.
  -- @param #DETECTION_REPORTING self
  -- @param Core.Set#SET_GROUP SetGroup
  -- @param Functional.Detection#DETECTION_AREAS Detection
  -- @return #DETECTION_REPORTING self
  function DETECTION_REPORTING:New( SetGroup, Detection )
  
    -- Inherits from DETECTION_MANAGER
    local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #DETECTION_REPORTING
    
    self:Schedule( 1, 30 )
    return self
  end
  
  --- Creates a string of the detected items in a @{Functional.Detection} object.
  -- @param #DETECTION_MANAGER self
  -- @param Core.Set#SET_UNIT DetectedSet The detected Set created by the @{Functional.Detection#DETECTION_BASE} object.
  -- @return #DETECTION_MANAGER self
  function DETECTION_REPORTING:GetDetectedItemsText( DetectedSet )
    self:F2()
  
    local MT = {} -- Message Text
    local UnitTypes = {}
  
    for DetectedUnitID, DetectedUnitData in pairs( DetectedSet:GetSet() ) do
      local DetectedUnit = DetectedUnitData -- Wrapper.Unit#UNIT
      if DetectedUnit:IsAlive() then
        local UnitType = DetectedUnit:GetTypeName()
    
        if not UnitTypes[UnitType] then
          UnitTypes[UnitType] = 1
        else
          UnitTypes[UnitType] = UnitTypes[UnitType] + 1
        end
      end
    end
  
    for UnitTypeID, UnitType in pairs( UnitTypes ) do
      MT[#MT+1] = UnitType .. " of " .. UnitTypeID
    end
  
    return table.concat( MT, ", " )
  end
  
  
  
  --- Reports the detected items to the @{Core.Set#SET_GROUP}.
  -- @param #DETECTION_REPORTING self
  -- @param Wrapper.Group#GROUP Group The @{Wrapper.Group} object to where the report needs to go.
  -- @param Functional.Detection#DETECTION_AREAS Detection The detection created by the @{Functional.Detection#DETECTION_BASE} object.
  -- @return #boolean Return true if you want the reporting to continue... false will cancel the reporting loop.
  function DETECTION_REPORTING:ProcessDetected( Group, Detection )
    self:F2( Group )
  
    local DetectedMsg = {}
    for DetectedAreaID, DetectedAreaData in pairs( Detection:GetDetectedAreas() ) do
      local DetectedArea = DetectedAreaData -- Functional.Detection#DETECTION_AREAS.DetectedArea
      DetectedMsg[#DetectedMsg+1] = " - Group #" .. DetectedAreaID .. ": " .. self:GetDetectedItemsText( DetectedArea.Set )
    end  
    local FACGroup = Detection:GetDetectionGroups()
    FACGroup:MessageToGroup( "Reporting detected target groups:\n" .. table.concat( DetectedMsg, "\n" ), self:GetReportDisplayTime(), Group  )
  
    return true
  end

end

--- **Tasking** - Dynamically allocates A2G tasks to human players, based on detected ground targets through reconnaissance.
--
-- **Features:**
--
--   * Dynamically assign tasks to human players based on detected targets.
--   * Dynamically change the tasks as the tactical situation evolves during the mission.
--   * Dynamically assign (CAS) Close Air Support tasks for human players.
--   * Dynamically assign (BAI) Battlefield Air Interdiction tasks for human players.
--   * Dynamically assign (SEAD) Suppression of Enemy Air Defense tasks for human players to eliminate G2A missile threats.
--   * Define and use an EWR (Early Warning Radar) network.
--   * Define different ranges to engage upon intruders.
--   * Keep task achievements.
--   * Score task achievements.
--
-- ===
--
-- ### Author: **FlightControl**
--
-- ### Contributions:
--
-- ===
--
-- @module Tasking.Task_A2G_Dispatcher
-- @image Task_A2G_Dispatcher.JPG

do -- TASK_A2G_DISPATCHER

  --- TASK\_A2G\_DISPATCHER class.
  -- @type TASK_A2G_DISPATCHER
  -- @field Core.Set#SET_GROUP SetGroup The groups to which the FAC will report to.
  -- @field Functional.Detection#DETECTION_BASE Detection The DETECTION_BASE object that is used to report the detected objects.
  -- @field Tasking.Mission#MISSION Mission
  -- @extends Tasking.DetectionManager#DETECTION_MANAGER

  --- Orchestrates dynamic **A2G Task Dispatching** based on the detection results of a linked @{Functional.Detection} object.
  -- 
  -- ![Banner Image](..\Images\deprecated.png)
  --
  -- It uses the Tasking System within the MOOSE framework, which is a multi-player Tasking Orchestration system.
  -- It provides a truly dynamic battle environment for pilots and ground commanders to engage upon,
  -- in a true co-operation environment wherein **Multiple Teams** will collaborate in Missions to **achieve a common Mission Goal**.
  --
  -- The A2G dispatcher will dispatch the A2G Tasks to a defined  @{Core.Set} of @{Wrapper.Group}s that will be manned by **Players**.   
  -- We call this the **AttackSet** of the A2G dispatcher. So, the Players are seated in the @{Wrapper.Client}s of the @{Wrapper.Group} @{Core.Set}.
  --
  -- Depending on the actions of the enemy, preventive tasks are dispatched to the players to orchestrate the engagement in a true co-operation.
  -- The detection object will group the detected targets by its grouping method, and integrates a @{Core.Set} of @{Wrapper.Group}s that are Recce vehicles or air units.
  -- We call this the **RecceSet** of the A2G dispatcher.
  --
  -- Depending on the current detected tactical situation, different task types will be dispatched to the Players seated in the AttackSet..
  -- There are currently 3 **Task Types** implemented in the TASK\_A2G\_DISPATCHER:
  --
  --   - **SEAD Task**: Dispatched when there are ground based Radar Emitters detected within an area.
  --   - **CAS Task**: Dispatched when there are no ground based Radar Emitters within the area, but there are friendly ground Units within 6 km from the enemy.
  --   - **BAI Task**: Dispatched when there are no ground based Radar Emitters within the area, and there aren't friendly ground Units within 6 km from the enemy.
  --
  -- # 0. Tactical Situations
  --
  -- This chapters provides some insights in the tactical situations when certain Task Types are created.
  -- The Task Types are depending on the enemy positions that were detected, and the current location of friendly units.
  --
  -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia3.JPG)
  --
  -- In the demonstration mission [TAD-A2G-000 - AREAS - Detection test], 
  -- the tactical situation is a demonstration how the A2G detection works.
  -- This example will be taken further in the explanation in the following chapters.
  --
  -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia4.JPG)
  --
  -- The red coalition are the players, the blue coalition is the enemy.
  --
  -- Red reconnaissance vehicles and airborne units are detecting the targets.
  -- We call this the RecceSet as explained above, which is a Set of Groups that
  -- have a group name starting with `Recce` (configured in the mission script).
  --
  -- Red attack units are responsible for executing the mission for the command center.
  -- We call this the AttackSet, which is a Set of Groups with a group name starting with `Attack` (configured in the mission script).
  -- These units are setup in this demonstration mission to be ground vehicles and airplanes.
  -- For demonstration purposes, the attack airplane is stationed on the ground to explain
  -- the messages and the menus properly.
  -- Further test missions demonstrate the A2G task dispatcher from within air.
  --
  -- Depending upon the detection results, the A2G dispatcher will create different tasks.
  --
  -- # 0.1. SEAD Task
  --
  -- A SEAD Task is dispatched when there are ground based Radar Emitters detected within an area.
  --
  -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia9.JPG)
  --
  --   - Once all Radar Emitting Units have been destroyed, the Task will convert into a BAI or CAS task!
  --   - A CAS and BAI task may be converted into a SEAD task, once a radar has been detected within the area!
  --
  -- # 0.2. CAS Task
  --
  -- A CAS Task is dispatched when there are no ground based Radar Emitters within the area, but there are friendly ground Units within 6 km from the enemy.
  --
  -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia10.JPG)
  --
  --   - After the detection of the CAS task, if the friendly Units are destroyed, the CAS task will convert into a BAI task!
  --   - Only ground Units are taken into account. Airborne units are ships are not considered friendlies that require Close Air Support.
  --
  -- # 0.3. BAI Task
  --
  -- A BAI Task is dispatched when there are no ground based Radar Emitters within the area, and there aren't friendly ground Units within 6 km from the enemy.
  --
  -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia11.JPG)
  --
  --   - A BAI task may be converted into a CAS task if friendly Ground Units approach within 6 km range!
  --
  -- # 1. Player Experience
  --
  -- The A2G dispatcher is residing under a @{Tasking.CommandCenter}, which is orchestrating a @{Tasking.Mission}.
  -- As a result, you'll find for DCS World missions that implement the A2G dispatcher a **Command Center Menu** and under this one or more **Mission Menus**.
  --
  -- For example, if there are 2 Command Centers (CC).
  -- Each CC is controlling a couple of Missions, the Radio Menu Structure could look like this:
  --
  --      Radio MENU Structure (F10. Other)
  --
  --      F1. Command Center [Gori]
  --        F1. Mission "Alpha (Primary)"
  --        F2. Mission "Beta (Secondary)"
  --        F3. Mission "Gamma (Tactical)"
  --      F1. Command Center [Lima]
  --        F1. Mission "Overlord (High)"
  --
  -- Command Center [Gori] is controlling Mission "Alpha", "Beta", "Gamma".  Alpha is the Primary mission, Beta the Secondary and there is a Tactical mission Gamma.
  -- Command Center [Lima] is controlling Missions "Overlord", which needs to be executed with High priority.
  --
  -- ## 1.1. Mission Menu (Under the Command Center Menu)
  --
  -- The Mission Menu controls the information of the mission, including the:
  --
  --   - **Mission Briefing**: A briefing of the Mission in text, which will be shown as a message.
  --   - **Mark Task Locations**: A summary of each Task will be shown on the map as a marker.
  --   - **Create Task Reports**: A menu to create various reports of the current tasks dispatched by the A2G dispatcher.
  --   - **Create Mission Reports**: A menu to create various reports on the current mission.
  --
  -- For CC [Lima], Mission "Overlord", the menu structure could look like this:
  --
  --      Radio MENU Structure (F10. Other)
  --
  --      F1. Command Center [Lima]
  --        F1. Mission "Overlord"
  --          F1. Mission Briefing
  --          F2. Mark Task Locations on Map
  --          F3. Task Reports
  --          F4. Mission Reports
  --
  -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia5.JPG)
  --
  -- ### 1.1.1. Mission Briefing Menu
  --
  -- The Mission Briefing Menu will show in text a summary description of the overall mission objectives and expectations.
  -- Note that the Mission Briefing is not the briefing of a specific task, but rather provides an overall strategy and tactical situation, 
  -- and explains the mission goals.
  --
  --
  -- ### 1.1.2. Mark Task Locations Menu
  --
  -- The Mark Task Locations Menu will mark the location indications of the Tasks on the map, if this intelligence is known by the Command Center.
  -- For A2G tasks this information will always be know, but it can be that for other tasks a location intelligence will be less relevant.
  -- Note that each Planned task and each Engaged task will be marked. Completed, Failed and Cancelled tasks are not marked.
  -- Depending on the task type, a summary information is shown to bring to the player the relevant information for situational awareness.
  --
  -- ### 1.1.3. Task Reports Menu
  --
  -- The Task Reports Menu is a sub menu, that allows to create various reports:
  --
  --   - **Tasks Summary**: This report will list all the Tasks that are or were active within the mission, indicating its status.
  --   - **Planned Tasks**: This report will list all the Tasks that are in status Planned, which are Tasks not assigned to any player, and are ready to be executed.
  --   - **Assigned Tasks**: This report will list all the Tasks that are in status Assigned, which are Tasks assigned to (a) player(s) and are currently executed.
  --   - **Successful Tasks**: This report will list all the Tasks that are in status Success, which are Tasks executed by (a) player(s) and are completed successfully.
  --   - **Failed Tasks**: This report will list all the Tasks that are in status Success, which are Tasks executed by (a) player(s) and that have failed.
  --
  -- The information shown of the tasks will vary according the underlying task type, but are self explanatory.
  --
  -- For CC [Gori], Mission "Alpha", the Task Reports menu structure could look like this:
  --
  --      Radio MENU Structure (F10. Other)
  --
  --      F1. Command Center [Gori]
  --        F1. Mission "Alpha"
  --          F1. Mission Briefing
  --          F2. Mark Task Locations on Map
  --          F3. Task Reports
  --            F1. Tasks Summary
  --            F2. Planned Tasks
  --            F3. Assigned Tasks
  --            F4. Successful Tasks
  --            F5. Failed Tasks
  --          F4. Mission Reports
  --
  -- Note that these reports provide an "overview" of the tasks. Detailed information of the task can be retrieved using the Detailed Report on the Task Menu.
  -- (See later).
  --
  -- ### 1.1.4. Mission Reports Menu
  --
  -- The Mission Reports Menu is a sub menu, that provides options to retrieve further information on the current Mission:
  --
  --   - **Report Mission Progress**: Shows the progress of the current Mission. Each Task has a % of completion.
  --   - **Report Players per Task**: Show which players are engaged on which Task within the Mission.
  --
  -- For CC |Gori|, Mission "Alpha", the Mission Reports menu structure could look like this:
  --
  --      Radio MENU Structure (F10. Other)
  --
  --      F1. Command Center [Gori]
  --        F1. Mission "Alpha"
  --          F1. Mission Briefing
  --          F2. Mark Task Locations on Map
  --          F3. Task Reports
  --          F4. Mission Reports
  --            F1. Report Mission Progress
  --            F2. Report Players per Task
  --
  --
  -- ## 1.2. Task Management Menus
  --
  -- Very important to remember is: **Multiple Players can be assigned to the same Task, but from the player perspective, the Player can only be assigned to one Task per Mission at the same time!**
  -- Consider this like the two major modes in which a player can be in. He can be free of tasks or he can be assigned to a Task.
  -- Depending on whether a Task has been Planned or Assigned to a Player (Group), 
  -- **the Mission Menu will contain extra Menus to control specific Tasks.**
  --
  -- #### 1.2.1. Join a Planned Task
  --
  -- If the Player has not yet been assigned to a Task within the Mission, the Mission Menu will contain additionally a:
  --
  --   - Join Planned Task Menu: This menu structure allows the player to join a planned task (a Task with status Planned).
  --
  -- For CC |Gori|, Mission "Alpha", the menu structure could look like this:
  --
  --      Radio MENU Structure (F10. Other)
  --
  --      F1. Command Center [Gori]
  --        F1. Mission "Alpha"
  --          F1. Mission Briefing
  --          F2. Mark Task Locations on Map
  --          F3. Task Reports
  --          F4. Mission Reports
  --          F5. Join Planned Task
  --
  -- **The F5. Join Planned Task allows the player to join a Planned Task and take an engagement in the running Mission.**
  --
  -- #### 1.2.2. Manage an Assigned Task
  --
  -- If the Player has been assigned to one Task within the Mission, the Mission Menu will contain an extra:
  --
  --   - Assigned Task __TaskName__ Menu: This menu structure allows the player to take actions on the currently engaged task.
  --
  -- In this example, the Group currently seated by the player is not assigned yet to a Task.
  -- The Player has the option to assign itself to a Planned Task using menu option F5 under the Mission Menu "Alpha".
  --
  -- This would be an example menu structure, 
  -- for CC |Gori|, Mission "Alpha", when a player would have joined Task CAS.001:
  --
  --      Radio MENU Structure (F10. Other)
  --
  --      F1. Command Center [Gori]
  --        F1. Mission "Alpha"
  --          F1. Mission Briefing
  --          F2. Mark Task Locations on Map
  --          F3. Task Reports
  --          F4. Mission Reports
  --          F5. Assigned Task CAS.001
  --
  -- **The F5. Assigned Task __TaskName__ allows the player to control the current Assigned Task and take further actions.**
  --
  -- ## 1.3. Join Planned Task Menu
  --
  -- The Join Planned Task Menu contains the different Planned A2G Tasks **in a structured Menu Hierarchy**.
  -- The Menu Hierarchy is structuring the Tasks per **Task Type**, and then by **Task Name (ID)**.
  --
  -- For example, for CC [Gori], Mission "Alpha",
  -- if a Mission "ALpha" contains 5 Planned Tasks, which would be:
  --
  --   - 2 CAS Tasks
  --   - 1 BAI Task
  --   - 2 SEAD Tasks
  --
  -- the Join Planned Task Menu Hierarchy could look like this:
  --
  --      Radio MENU Structure (F10. Other)
  --
  --      F1. Command Center [Gori]
  --        F1. Mission "Alpha"
  --          F1. Mission Briefing
  --          F2. Mark Task Locations on Map
  --          F3. Task Reports
  --          F4. Mission Reports
  --          F5. Join Planned Task
  --            F2. BAI
  --              F1. BAI.001
  --            F1. CAS
  --              F1. CAS.002
  --            F3. SEAD
  --              F1. SEAD.003
  --              F2. SEAD.004
  --              F3. SEAD.005
  --
  -- An example from within a running simulation:
  --
  -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia6.JPG)
  --
  -- Each Task Type Menu would have a list of the Task Menus underneath. 
  -- Each Task Menu (eg. `CAS.001`) has a **detailed Task Menu structure to control the specific task**!
  --
  -- ### 1.3.1. Planned Task Menu
  --
  -- Each Planned Task Menu will allow for the following actions:
  --
  --   - Report Task Details: Provides a detailed report on the Planned Task.
  --   - Mark Task Location on Map: Mark the approximate location of the Task on the Map, if relevant.
  --   - Join Task: Join the Task. This is THE menu option to let a Player join the Task, and to engage within the Mission.
  --
  -- The Join Planned Task Menu could look like this for for CC |Gori|, Mission "Alpha":
  --
  --      Radio MENU Structure (F10. Other)
  --
  --      F1. Command Center |Gori|
  --        F1. Mission "Alpha"
  --          F1. Mission Briefing
  --          F2. Mark Task Locations on Map
  --          F3. Task Reports
  --          F4. Mission Reports
  --          F5. Join Planned Task
  --            F1. CAS
  --              F1. CAS.001
  --                F1. Report Task Details
  --                F2. Mark Task Location on Map
  --                F3. Join Task
  --
  -- **The Join Task is THE menu option to let a Player join the Task, and to engage within the Mission.**
  --
  --
  -- ## 1.4. Assigned Task Menu
  --
  -- The Assigned Task Menu allows to control the **current assigned task** within the Mission.
  --
  -- Depending on the Type of Task, the following menu options will be available:
  --
  --   - **Report Task Details**: Provides a detailed report on the Planned Task.
  --   - **Mark Task Location on Map**: Mark the approximate location of the Task on the Map, if relevant.
  --   - **Abort Task: Abort the current assigned Task:** This menu option lets the player abort the Task.
  --
  -- For example, for CC |Gori|, Mission "Alpha", the Assigned Menu could be:
  --
  --      F1. Command Center |Gori|
  --        F1. Mission "Alpha"
  --          F1. Mission Briefing
  --          F2. Mark Task Locations on Map
  --          F3. Task Reports
  --          F4. Mission Reports
  --          F5. Assigned Task
  --            F1. Report Task Details
  --            F2. Mark Task Location on Map
  --            F3. Abort Task
  --
  -- Task abortion will result in the Task to be Cancelled, and the Task **may** be **Replanned**.
  -- However, this will depend on the setup of each Mission.
  --
  -- ## 1.5. Messages
  --
  -- During game play, different messages are displayed.
  -- These messages provide an update of the achievements made, and the state wherein the task is.
  --
  -- The various reports can be used also to retrieve the current status of the mission and its tasks.
  --
  -- ![](..\Presentations\TASK_A2G_DISPATCHER\Dia7.JPG)
  --
  -- The @{Core.Settings} menu provides additional options to control the timing of the messages.
  -- There are:
  --
  --   - Status messages, which are quick status updates. The settings menu allows to switch off these messages.
  --   - Information messages, which are shown a bit longer, as they contain important information.
  --   - Summary reports, which are quick reports showing a high level summary.
  --   - Overview reports, which are providing the essential information. It provides an overview of a greater thing, and may take a bit of time to read.
  --   - Detailed reports, which provide with very detailed information. It takes a bit longer to read those reports, so the display of those could be a bit longer.
  --
  -- # 2. TASK\_A2G\_DISPATCHER constructor
  --
  -- The @{#TASK_A2G_DISPATCHER.New}() method creates a new TASK\_A2G\_DISPATCHER instance.
  --
  -- # 3. Usage
  --
  -- To use the TASK\_A2G\_DISPATCHER class, you need:
  --
  --   - A @{Tasking.CommandCenter} object. The master communication channel.
  --   - A @{Tasking.Mission} object. Each task belongs to a Mission.
  --   - A @{Functional.Detection} object. There are several detection grouping methods to choose from.
  --   - A @{Tasking.Task_A2G_Dispatcher} object. The master A2G task dispatcher.
  --   - A @{Core.Set} of @{Wrapper.Group} objects that will detect the enemy, the RecceSet. This is attached to the @{Functional.Detection} object.
  --   - A @{Core.Set} of @{Wrapper.Group} objects that will attack the enemy, the AttackSet. This is attached to the @{Tasking.Task_A2G_Dispatcher} object.
  --
  -- Below an example mission declaration that is defines a Task A2G Dispatcher object.
  --
  --     -- Declare the Command Center
  --     local HQ = GROUP
  --       :FindByName( "HQ", "Bravo HQ" )
  --
  --     local CommandCenter = COMMANDCENTER
  --       :New( HQ, "Lima" )
  --
  --     -- Declare the Mission for the Command Center.
  --     local Mission = MISSION
  --       :New( CommandCenter, "Overlord", "High", "Attack Detect Mission Briefing", coalition.side.RED )
  --
  --     -- Define the RecceSet that will detect the enemy.
  --     local RecceSet = SET_GROUP
  --       :New()
  --       :FilterPrefixes( "FAC" )
  --       :FilterCoalitions("red")
  --       :FilterStart()
  --
  --     -- Setup the detection. We use DETECTION_AREAS to detect and group the enemies within areas of 3 km radius.
  --     local DetectionAreas = DETECTION_AREAS
  --       :New( RecceSet, 3000 )  -- The RecceSet will detect the enemies.
  --
  --     -- Setup the AttackSet, which is a SET_GROUP.
  --     -- The SET_GROUP is a dynamic collection of GROUP objects.
  --     local AttackSet = SET_GROUP
  --       :New()  -- Create the SET_GROUP object.
  --       :FilterCoalitions( "red" ) -- Only incorporate the RED coalitions.
  --       :FilterPrefixes( "Attack" ) -- Only incorporate groups that start with the name Attack.
  --       :FilterStart() -- Enable the dynamic filtering. From this moment the AttackSet will contain all groups that are red and start with the name Attack.
  --
  --     -- Now we have everything to setup the main A2G TaskDispatcher.
  --     TaskDispatcher = TASK_A2G_DISPATCHER
  --       :New( Mission, AttackSet, DetectionAreas ) -- We assign the TaskDispatcher under Mission. The AttackSet will engage the enemy and will receive the dispatched Tasks. The DetectionAreas will report any detected enemies to the TaskDispatcher.
  --
  --
  -- # Developer Note
  -- 
  -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
  -- Therefore, this class is considered to be deprecated
  --
  --
  -- @field #TASK_A2G_DISPATCHER
  TASK_A2G_DISPATCHER = {
    ClassName = "TASK_A2G_DISPATCHER",
    Mission = nil,
    Detection = nil,
    Tasks = {}
  }

  --- TASK_A2G_DISPATCHER constructor.
  -- @param #TASK_A2G_DISPATCHER self
  -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done.
  -- @param Core.Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission.
  -- @param Functional.Detection#DETECTION_BASE Detection The detection results that are used to dynamically assign new tasks to human players.
  -- @return #TASK_A2G_DISPATCHER self
  function TASK_A2G_DISPATCHER:New( Mission, SetGroup, Detection )

    -- Inherits from DETECTION_MANAGER
    local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #TASK_A2G_DISPATCHER

    self.Detection = Detection
    self.Mission = Mission
    self.FlashNewTask = true -- set to false to suppress flash messages

    self.Detection:FilterCategories( { Unit.Category.GROUND_UNIT } )

    self:AddTransition( "Started", "Assign", "Started" )

    --- OnAfter Transition Handler for Event Assign.
    -- @function [parent=#TASK_A2G_DISPATCHER] OnAfterAssign
    -- @param #TASK_A2G_DISPATCHER self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @param Tasking.Task_A2G#TASK_A2G Task
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param #string PlayerName

    self:__Start( 5 )

    return self
  end

  --- Set flashing player messages on or off
  -- @param #TASK_A2G_DISPATCHER self
  -- @param #boolean onoff Set messages on (true) or off (false)
  function TASK_A2G_DISPATCHER:SetSendMessages( onoff )
    self.FlashNewTask = onoff
  end

  --- Creates a SEAD task when there are targets for it.
  -- @param #TASK_A2G_DISPATCHER self
  -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem
  -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units.
  -- @return #nil If there are no targets to be set.
  function TASK_A2G_DISPATCHER:EvaluateSEAD( DetectedItem )
    self:F( { DetectedItem.ItemID } )

    local DetectedSet = DetectedItem.Set
    local DetectedZone = DetectedItem.Zone

    -- Determine if the set has radar targets. If it does, construct a SEAD task.
    local RadarCount = DetectedSet:HasSEAD()

    if RadarCount > 0 then

      -- Here we're doing something advanced... We're copying the DetectedSet, but making a new Set only with SEADable Radar units in it.
      local TargetSetUnit = SET_UNIT:New()
      TargetSetUnit:SetDatabase( DetectedSet )
      TargetSetUnit:FilterHasSEAD()
      TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection.

      return TargetSetUnit
    end

    return nil
  end

  --- Creates a CAS task when there are targets for it.
  -- @param #TASK_A2G_DISPATCHER self
  -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem
  -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units.
  -- @return #nil If there are no targets to be set.
  function TASK_A2G_DISPATCHER:EvaluateCAS( DetectedItem )
    self:F( { DetectedItem.ItemID } )

    local DetectedSet = DetectedItem.Set
    local DetectedZone = DetectedItem.Zone

    -- Determine if the set has ground units.
    -- There should be ground unit friendlies nearby. Airborne units are valid friendlies types.
    -- And there shouldn't be any radar.
    local GroundUnitCount = DetectedSet:HasGroundUnits()
    local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) -- Are there friendlies nearby of type GROUND_UNIT?
    local RadarCount = DetectedSet:HasSEAD()

    if RadarCount == 0 and GroundUnitCount > 0 and FriendliesNearBy == true then

      -- Copy the Set
      local TargetSetUnit = SET_UNIT:New()
      TargetSetUnit:SetDatabase( DetectedSet )
      TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection.

      return TargetSetUnit
    end

    return nil
  end

  --- Creates a BAI task when there are targets for it.
  -- @param #TASK_A2G_DISPATCHER self
  -- @param Functional.Detection#DETECTION_AREAS.DetectedItem DetectedItem
  -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units.
  -- @return #nil If there are no targets to be set.
  function TASK_A2G_DISPATCHER:EvaluateBAI( DetectedItem, FriendlyCoalition )
    self:F( { DetectedItem.ItemID } )

    local DetectedSet = DetectedItem.Set
    local DetectedZone = DetectedItem.Zone

    -- Determine if the set has ground units.
    -- There shouldn't be any ground unit friendlies nearby.
    -- And there shouldn't be any radar.
    local GroundUnitCount = DetectedSet:HasGroundUnits()
    local FriendliesNearBy = self.Detection:IsFriendliesNearBy( DetectedItem, Unit.Category.GROUND_UNIT ) -- Are there friendlies nearby of type GROUND_UNIT?
    local RadarCount = DetectedSet:HasSEAD()

    if RadarCount == 0 and GroundUnitCount > 0 and FriendliesNearBy == false then

      -- Copy the Set
      local TargetSetUnit = SET_UNIT:New()
      TargetSetUnit:SetDatabase( DetectedSet )
      TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection.

      return TargetSetUnit
    end

    return nil
  end

  function TASK_A2G_DISPATCHER:RemoveTask( TaskIndex )
    self.Mission:RemoveTask( self.Tasks[TaskIndex] )
    self.Tasks[TaskIndex] = nil
  end

  --- Evaluates the removal of the Task from the Mission.
  -- Can only occur when the DetectedItem is Changed AND the state of the Task is "Planned".
  -- @param #TASK_A2G_DISPATCHER self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Tasking.Task#TASK Task
  -- @param #boolean DetectedItemID
  -- @param #boolean DetectedItemChange
  -- @return Tasking.Task#TASK
  function TASK_A2G_DISPATCHER:EvaluateRemoveTask( Mission, Task, TaskIndex, DetectedItemChanged )

    if Task then
      if (Task:IsStatePlanned() and DetectedItemChanged == true) or Task:IsStateCancelled() then
        -- self:F( "Removing Tasking: " .. Task:GetTaskName() )
        self:RemoveTask( TaskIndex )
      end
    end

    return Task
  end

  --- Assigns tasks in relation to the detected items to the @{Core.Set#SET_GROUP}.
  -- @param #TASK_A2G_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object.
  -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop.
  function TASK_A2G_DISPATCHER:ProcessDetected( Detection )
    self:F()

    local AreaMsg = {}
    local TaskMsg = {}
    local ChangeMsg = {}

    local Mission = self.Mission

    if Mission:IsIDLE() or Mission:IsENGAGED() then

      local TaskReport = REPORT:New()

      -- Checking the task queue for the dispatcher, and removing any obsolete task!
      for TaskIndex, TaskData in pairs( self.Tasks ) do
        local Task = TaskData -- Tasking.Task#TASK
        if Task:IsStatePlanned() then
          local DetectedItem = Detection:GetDetectedItemByIndex( TaskIndex )
          if not DetectedItem then
            local TaskText = Task:GetName()
            for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
              if self.FlashNewTask then
                Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2G task %s for %s removed.", TaskText, Mission:GetShortText() ), TaskGroup )
              end
            end
            Task = self:RemoveTask( TaskIndex )
          end
        end
      end

      --- First we need to  the detected targets.
      for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do

        local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem
        local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT
        local DetectedZone = DetectedItem.Zone
        -- self:F( { "Targets in DetectedItem", DetectedItem.ItemID, DetectedSet:Count(), tostring( DetectedItem ) } )
        -- DetectedSet:Flush( self )

        local DetectedItemID = DetectedItem.ID
        local TaskIndex = DetectedItem.Index
        local DetectedItemChanged = DetectedItem.Changed

        self:F( { DetectedItemChanged = DetectedItemChanged, DetectedItemID = DetectedItemID, TaskIndex = TaskIndex } )

        local Task = self.Tasks[TaskIndex] -- Tasking.Task_A2G#TASK_A2G

        if Task then
          -- If there is a Task and the task was assigned, then we check if the task was changed ... If it was, we need to reevaluate the targets.
          if Task:IsStateAssigned() then
            if DetectedItemChanged == true then -- The detection has changed, thus a new TargetSet is to be evaluated and set
              local TargetsReport = REPORT:New()
              local TargetSetUnit = self:EvaluateSEAD( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed...
              if TargetSetUnit then
                if Task:IsInstanceOf( TASK_A2G_SEAD ) then
                  Task:SetTargetSetUnit( TargetSetUnit )
                  Task:SetDetection( Detection, DetectedItem )
                  Task:UpdateTaskInfo( DetectedItem )
                  TargetsReport:Add( Detection:GetChangeText( DetectedItem ) )
                else
                  Task:Cancel()
                end
              else
                local TargetSetUnit = self:EvaluateCAS( DetectedItem ) -- Returns a SetUnit if there are targets to be CASed...
                if TargetSetUnit then
                  if Task:IsInstanceOf( TASK_A2G_CAS ) then
                    Task:SetTargetSetUnit( TargetSetUnit )
                    Task:SetDetection( Detection, DetectedItem )
                    Task:UpdateTaskInfo( DetectedItem )
                    TargetsReport:Add( Detection:GetChangeText( DetectedItem ) )
                  else
                    Task:Cancel()
                    Task = self:RemoveTask( TaskIndex )
                  end
                else
                  local TargetSetUnit = self:EvaluateBAI( DetectedItem ) -- Returns a SetUnit if there are targets to be BAIed...
                  if TargetSetUnit then
                    if Task:IsInstanceOf( TASK_A2G_BAI ) then
                      Task:SetTargetSetUnit( TargetSetUnit )
                      Task:SetDetection( Detection, DetectedItem )
                      Task:UpdateTaskInfo( DetectedItem )
                      TargetsReport:Add( Detection:GetChangeText( DetectedItem ) )
                    else
                      Task:Cancel()
                      Task = self:RemoveTask( TaskIndex )
                    end
                  end
                end
              end

              -- Now we send to each group the changes, if any.
              for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
                local TargetsText = TargetsReport:Text( ", " )
                if (Mission:IsGroupAssigned( TaskGroup )) and TargetsText ~= "" and self.FlashNewTask then
                  Mission:GetCommandCenter():MessageToGroup( string.format( "Task %s has change of targets:\n %s", Task:GetName(), TargetsText ), TaskGroup )
                end
              end
            end
          end
        end

        if Task then
          if Task:IsStatePlanned() then
            if DetectedItemChanged == true then -- The detection has changed, thus a new TargetSet is to be evaluated and set
              if Task:IsInstanceOf( TASK_A2G_SEAD ) then
                local TargetSetUnit = self:EvaluateSEAD( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed...
                if TargetSetUnit then
                  Task:SetTargetSetUnit( TargetSetUnit )
                  Task:SetDetection( Detection, DetectedItem )
                  Task:UpdateTaskInfo( DetectedItem )
                else
                  Task:Cancel()
                  Task = self:RemoveTask( TaskIndex )
                end
              else
                if Task:IsInstanceOf( TASK_A2G_CAS ) then
                  local TargetSetUnit = self:EvaluateCAS( DetectedItem ) -- Returns a SetUnit if there are targets to be CASed...
                  if TargetSetUnit then
                    Task:SetTargetSetUnit( TargetSetUnit )
                    Task:SetDetection( Detection, DetectedItem )
                    Task:UpdateTaskInfo( DetectedItem )
                  else
                    Task:Cancel()
                    Task = self:RemoveTask( TaskIndex )
                  end
                else
                  if Task:IsInstanceOf( TASK_A2G_BAI ) then
                    local TargetSetUnit = self:EvaluateBAI( DetectedItem ) -- Returns a SetUnit if there are targets to be BAIed...
                    if TargetSetUnit then
                      Task:SetTargetSetUnit( TargetSetUnit )
                      Task:SetDetection( Detection, DetectedItem )
                      Task:UpdateTaskInfo( DetectedItem )
                    else
                      Task:Cancel()
                      Task = self:RemoveTask( TaskIndex )
                    end
                  else
                    Task:Cancel()
                    Task = self:RemoveTask( TaskIndex )
                  end
                end
              end
            end
          end
        end

        -- Evaluate SEAD
        if not Task then
          local TargetSetUnit = self:EvaluateSEAD( DetectedItem ) -- Returns a SetUnit if there are targets to be SEADed...
          if TargetSetUnit then
            Task = TASK_A2G_SEAD:New( Mission, self.SetGroup, string.format( "SEAD.%03d", DetectedItemID ), TargetSetUnit )
            DetectedItem.DesignateMenuName = string.format( "SEAD.%03d", DetectedItemID ) -- inject a name for DESIGNATE, if using same DETECTION object
            Task:SetDetection( Detection, DetectedItem )
          end

          -- Evaluate CAS
          if not Task then
            local TargetSetUnit = self:EvaluateCAS( DetectedItem ) -- Returns a SetUnit if there are targets to be CASed...
            if TargetSetUnit then
              Task = TASK_A2G_CAS:New( Mission, self.SetGroup, string.format( "CAS.%03d", DetectedItemID ), TargetSetUnit )
              DetectedItem.DesignateMenuName = string.format( "CAS.%03d", DetectedItemID ) -- inject a name for DESIGNATE, if using same DETECTION object
              Task:SetDetection( Detection, DetectedItem )
            end

            -- Evaluate BAI
            if not Task then
              local TargetSetUnit = self:EvaluateBAI( DetectedItem, self.Mission:GetCommandCenter():GetPositionable():GetCoalition() ) -- Returns a SetUnit if there are targets to be BAIed...
              if TargetSetUnit then
                Task = TASK_A2G_BAI:New( Mission, self.SetGroup, string.format( "BAI.%03d", DetectedItemID ), TargetSetUnit )
                DetectedItem.DesignateMenuName = string.format( "BAI.%03d", DetectedItemID ) -- inject a name for DESIGNATE, if using same DETECTION object
                Task:SetDetection( Detection, DetectedItem )
              end
            end
          end

          if Task then
            self.Tasks[TaskIndex] = Task
            Task:SetTargetZone( DetectedZone )
            Task:SetDispatcher( self )
            Task:UpdateTaskInfo( DetectedItem )
            Mission:AddTask( Task )

            function Task.OnEnterSuccess( Task, From, Event, To )
              self:Success( Task )
            end

            function Task.OnEnterCancelled( Task, From, Event, To )
              self:Cancelled( Task )
            end

            function Task.OnEnterFailed( Task, From, Event, To )
              self:Failed( Task )
            end

            function Task.OnEnterAborted( Task, From, Event, To )
              self:Aborted( Task )
            end

            TaskReport:Add( Task:GetName() )
          else
            self:F( "This should not happen" )
          end
        end

        -- OK, so the tasking has been done, now delete the changes reported for the area.
        Detection:AcceptChanges( DetectedItem )
      end

      -- TODO set menus using the HQ coordinator
      Mission:GetCommandCenter():SetMenu()

      local TaskText = TaskReport:Text( ", " )
      for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
        if (not Mission:IsGroupAssigned( TaskGroup )) and TaskText ~= "" and self.FlashNewTask then
          Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup )
        end
      end

    end

    return true
  end

end
--- **Tasking** - The TASK_A2G models tasks for players in Air to Ground engagements.
--
-- ===
--
-- ### Author: **FlightControl**
--
-- ### Contributions:
--
-- ===
--
-- @module Tasking.Task_A2G
-- @image MOOSE.JPG

do -- TASK_A2G

  --- The TASK_A2G class
  -- @type TASK_A2G
  -- @field Core.Set#SET_UNIT TargetSetUnit
  -- @extends Tasking.Task#TASK

  --- The TASK_A2G class defines Air To Ground tasks for a @{Core.Set} of Target Units,
  -- based on the tasking capabilities defined in @{Tasking.Task#TASK}.
  -- 
  -- ![Banner Image](..\Images\deprecated.png)
  -- 
  -- The TASK_A2G is implemented using a @{Core.Fsm#FSM_TASK}, and has the following statuses:
  --
  --   * **None**: Start of the process
  --   * **Planned**: The A2G task is planned.
  --   * **Assigned**: The A2G task is assigned to a @{Wrapper.Group#GROUP}.
  --   * **Success**: The A2G task is successfully completed.
  --   * **Failed**: The A2G task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ.
  --
  -- ## 1) Set the scoring of achievements in an A2G attack.
  --
  -- Scoring or penalties can be given in the following circumstances:
  --
  --   * @{#TASK_A2G.SetScoreOnDestroy}(): Set a score when a target in scope of the A2G attack, has been destroyed.
  --   * @{#TASK_A2G.SetScoreOnSuccess}(): Set a score when all the targets in scope of the A2G attack, have been destroyed.
  --   * @{#TASK_A2G.SetPenaltyOnFailed}(): Set a penalty when the A2G attack has failed.
  --
  -- # Developer Note
  -- 
  -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
  -- Therefore, this class is considered to be deprecated
  --
  -- @field #TASK_A2G
  TASK_A2G = {
    ClassName = "TASK_A2G"
  }

  --- Instantiates a new TASK_A2G.
  -- @param #TASK_A2G self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned.
  -- @param #string TaskName The name of the Task.
  -- @param Core.Set#SET_UNIT UnitSetTargets
  -- @param #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range.
  -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known.
  -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be.
  -- @return #TASK_A2G self
  function TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskType, TaskBriefing )
    local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType, TaskBriefing ) ) -- Tasking.Task#TASK_A2G
    self:F()

    self.TargetSetUnit = TargetSetUnit
    self.TaskType = TaskType

    local Fsm = self:GetUnitProcess()

    Fsm:AddTransition( "Assigned", "RouteToRendezVous", "RoutingToRendezVous" )
    Fsm:AddProcess( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } )
    Fsm:AddProcess( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } )

    Fsm:AddTransition( { "Arrived", "RoutingToRendezVous" }, "ArriveAtRendezVous", "ArrivedAtRendezVous" )

    Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "Engage", "Engaging" )
    Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "HoldAtRendezVous", "HoldingAtRendezVous" )

    Fsm:AddProcess( "Engaging", "Account", ACT_ACCOUNT_DEADS:New(), {} )
    Fsm:AddTransition( "Engaging", "RouteToTarget", "Engaging" )
    Fsm:AddProcess( "Engaging", "RouteToTargetZone", ACT_ROUTE_ZONE:New(), {} )
    Fsm:AddProcess( "Engaging", "RouteToTargetPoint", ACT_ROUTE_POINT:New(), {} )
    Fsm:AddTransition( "Engaging", "RouteToTargets", "Engaging" )

    -- Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" )
    -- Fsm:AddTransition( "Accounted", "Success", "Success" )
    Fsm:AddTransition( "Rejected", "Reject", "Aborted" )
    Fsm:AddTransition( "Failed", "Fail", "Failed" )

    --- Test
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_A2G#TASK_A2G Task
    function Fsm:onafterAssigned( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      -- Determine the first Unit from the self.RendezVousSetUnit

      self:RouteToRendezVous()
    end

    --- Test
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_A2G#TASK_A2G Task
    function Fsm:onafterRouteToRendezVous( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      -- Determine the first Unit from the self.RendezVousSetUnit

      if Task:GetRendezVousZone( TaskUnit ) then
        self:__RouteToRendezVousZone( 0.1 )
      else
        if Task:GetRendezVousCoordinate( TaskUnit ) then
          self:__RouteToRendezVousPoint( 0.1 )
        else
          self:__ArriveAtRendezVous( 0.1 )
        end
      end
    end

    --- Test
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task#TASK_A2G Task
    function Fsm:OnAfterArriveAtRendezVous( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      -- Determine the first Unit from the self.TargetSetUnit

      self:__Engage( 0.1 )
    end

    --- Test
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task#TASK_A2G Task
    function Fsm:onafterEngage( TaskUnit, Task )
      self:F( { self } )
      self:__Account( 0.1 )
      self:__RouteToTarget( 0.1 )
      self:__RouteToTargets( -10 )
    end

    --- Test
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_A2G#TASK_A2G Task
    function Fsm:onafterRouteToTarget( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      -- Determine the first Unit from the self.TargetSetUnit

      if Task:GetTargetZone( TaskUnit ) then
        self:__RouteToTargetZone( 0.1 )
      else
        local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT
        if TargetUnit then
          local Coordinate = TargetUnit:GetPointVec3()
          self:T( { TargetCoordinate = Coordinate, Coordinate:GetX(), Coordinate:GetY(), Coordinate:GetZ() } )
          Task:SetTargetCoordinate( Coordinate, TaskUnit )
        end
        self:__RouteToTargetPoint( 0.1 )
      end
    end

    --- Test
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_A2G#TASK_A2G Task
    function Fsm:onafterRouteToTargets( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT
      if TargetUnit then
        Task:SetTargetCoordinate( TargetUnit:GetCoordinate(), TaskUnit )
      end
      self:__RouteToTargets( -10 )
    end

    return self

  end

  -- @param #TASK_A2G self
  -- @param Core.Set#SET_UNIT TargetSetUnit The set of targets.
  function TASK_A2G:SetTargetSetUnit( TargetSetUnit )

    self.TargetSetUnit = TargetSetUnit
  end

  -- @param #TASK_A2G self
  function TASK_A2G:GetPlannedMenuText()
    return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )"
  end

  -- @param #TASK_A2G self
  -- @param Core.Point#COORDINATE RendezVousCoordinate The Coordinate object referencing to the 2D point where the RendezVous point is located on the map.
  -- @param #number RendezVousRange The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point.
  -- @param Wrapper.Unit#UNIT TaskUnit
  function TASK_A2G:SetRendezVousCoordinate( RendezVousCoordinate, RendezVousRange, TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT
    ActRouteRendezVous:SetCoordinate( RendezVousCoordinate )
    ActRouteRendezVous:SetRange( RendezVousRange )
  end

  -- @param #TASK_A2G self
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return Core.Point#COORDINATE The Coordinate object referencing to the 2D point where the RendezVous point is located on the map.
  -- @return #number The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point.
  function TASK_A2G:GetRendezVousCoordinate( TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT
    return ActRouteRendezVous:GetCoordinate(), ActRouteRendezVous:GetRange()
  end

  -- @param #TASK_A2G self
  -- @param Core.Zone#ZONE_BASE RendezVousZone The Zone object where the RendezVous is located on the map.
  -- @param Wrapper.Unit#UNIT TaskUnit
  function TASK_A2G:SetRendezVousZone( RendezVousZone, TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE
    ActRouteRendezVous:SetZone( RendezVousZone )
  end

  -- @param #TASK_A2G self
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return Core.Zone#ZONE_BASE The Zone object where the RendezVous is located on the map.
  function TASK_A2G:GetRendezVousZone( TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE
    return ActRouteRendezVous:GetZone()
  end

  -- @param #TASK_A2G self
  -- @param Core.Point#COORDINATE TargetCoordinate The Coordinate object where the Target is located on the map.
  -- @param Wrapper.Unit#UNIT TaskUnit
  function TASK_A2G:SetTargetCoordinate( TargetCoordinate, TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT
    ActRouteTarget:SetCoordinate( TargetCoordinate )
  end

  -- @param #TASK_A2G self
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return Core.Point#COORDINATE The Coordinate object where the Target is located on the map.
  function TASK_A2G:GetTargetCoordinate( TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT
    return ActRouteTarget:GetCoordinate()
  end

  -- @param #TASK_A2G self
  -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map.
  -- @param Wrapper.Unit#UNIT TaskUnit
  function TASK_A2G:SetTargetZone( TargetZone, TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE
    ActRouteTarget:SetZone( TargetZone )
  end

  -- @param #TASK_A2G self
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map.
  function TASK_A2G:GetTargetZone( TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE
    return ActRouteTarget:GetZone()
  end

  function TASK_A2G:SetGoalTotal()

    self.GoalTotal = self.TargetSetUnit:CountAlive()
  end

  function TASK_A2G:GetGoalTotal()

    return self.GoalTotal
  end

  --- Return the relative distance to the target vicinity from the player, in order to sort the targets in the reports per distance from the threats.
  -- @param #TASK_A2G self
  function TASK_A2G:ReportOrder( ReportGroup )
    self:UpdateTaskInfo( self.DetectedItem )

    local Coordinate = self.TaskInfo:GetData( "Coordinate" )
    local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate )

    return Distance
  end

  --- This method checks every 10 seconds if the goal has been reached of the task.
  -- @param #TASK_A2G self
  function TASK_A2G:onafterGoal( TaskUnit, From, Event, To )
    local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT

    if TargetSetUnit:CountAlive() == 0 then
      self:Success()
    end

    self:__Goal( -10 )
  end

  -- @param #TASK_A2G self
  function TASK_A2G:UpdateTaskInfo( DetectedItem )

    if self:IsStatePlanned() or self:IsStateAssigned() then
      local TargetCoordinate = DetectedItem and self.Detection:GetDetectedItemCoordinate( DetectedItem ) or self.TargetSetUnit:GetFirst():GetCoordinate()
      self.TaskInfo:AddTaskName( 0, "MSOD" )
      self.TaskInfo:AddCoordinate( TargetCoordinate, 1, "SOD" )

      local ThreatLevel, ThreatText
      if DetectedItem then
        ThreatLevel, ThreatText = self.Detection:GetDetectedItemThreatLevel( DetectedItem )
      else
        ThreatLevel, ThreatText = self.TargetSetUnit:CalculateThreatLevelA2G()
      end
      self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 10, "MOD", true )

      if self.Detection then
        local DetectedItemsCount = self.TargetSetUnit:CountAlive()
        local ReportTypes = REPORT:New()
        local TargetTypes = {}
        for TargetUnitName, TargetUnit in pairs( self.TargetSetUnit:GetSet() ) do
          local TargetType = self.Detection:GetDetectedUnitTypeName( TargetUnit )
          if not TargetTypes[TargetType] then
            TargetTypes[TargetType] = TargetType
            ReportTypes:Add( TargetType )
          end
        end
        self.TaskInfo:AddTargetCount( DetectedItemsCount, 11, "O", true )
        self.TaskInfo:AddTargets( DetectedItemsCount, ReportTypes:Text( ", " ), 20, "D", true )
      else
        local DetectedItemsCount = self.TargetSetUnit:CountAlive()
        local DetectedItemsTypes = self.TargetSetUnit:GetTypeNames()
        self.TaskInfo:AddTargetCount( DetectedItemsCount, 11, "O", true )
        self.TaskInfo:AddTargets( DetectedItemsCount, DetectedItemsTypes, 20, "D", true )
      end
      self.TaskInfo:AddQFEAtCoordinate( TargetCoordinate, 30, "MOD" )
      self.TaskInfo:AddTemperatureAtCoordinate( TargetCoordinate, 31, "MD" )
      self.TaskInfo:AddWindAtCoordinate( TargetCoordinate, 32, "MD" )
    end

  end

  --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection.
  -- @param #TASK_A2G self
  -- @param #number AutoAssignMethod The method to be applied to the task.
  -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center.
  -- @param Wrapper.Group#GROUP TaskGroup The player group.
  function TASK_A2G:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup )

    if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then
      return math.random( 1, 9 )
    elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then
      local Coordinate = self.TaskInfo:GetData( "Coordinate" )
      local Distance = Coordinate:Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() )
      self:F( { Distance = Distance } )
      return math.floor( Distance )
    elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then
      return 1
    end

    return 0
  end

end

do -- TASK_A2G_SEAD

  --- The TASK_A2G_SEAD class
  -- @type TASK_A2G_SEAD
  -- @field Core.Set#SET_UNIT TargetSetUnit
  -- @extends Tasking.Task#TASK

  --- Defines an Suppression or Extermination of Air Defenses task for a human player to be executed.
  -- These tasks are important to be executed as they will help to achieve air superiority at the vicinity.
  -- 
  -- The TASK_A2G_SEAD is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create SEAD tasks 
  -- based on detected enemy ground targets.
  -- 
  -- @field #TASK_A2G_SEAD
  TASK_A2G_SEAD = {
    ClassName = "TASK_A2G_SEAD"
  }

  --- Instantiates a new TASK_A2G_SEAD.
  -- @param #TASK_A2G_SEAD self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned.
  -- @param #string TaskName The name of the Task.
  -- @param Core.Set#SET_UNIT TargetSetUnit 
  -- @param #string TaskBriefing The briefing of the task.
  -- @return #TASK_A2G_SEAD self
  function TASK_A2G_SEAD:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing )
    local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "SEAD", TaskBriefing ) ) -- #TASK_A2G_SEAD
    self:F()

    Mission:AddTask( self )

    self:SetBriefing( TaskBriefing or "Execute a Suppression of Enemy Air Defenses." )

    return self
  end

  --- Set a score when a target in scope of the A2G attack, has been destroyed .
  -- @param #TASK_A2G_SEAD self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points to be granted when task process has been achieved.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2G_SEAD
  function TASK_A2G_SEAD:SetScoreOnProgress( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has SEADed a target.", Score )

    return self
  end

  --- Set a score when all the targets in scope of the A2G attack, have been destroyed.
  -- @param #TASK_A2G_SEAD self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2G_SEAD
  function TASK_A2G_SEAD:SetScoreOnSuccess( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Success", "All radar emitting targets have been successfully SEADed!", Score )

    return self
  end

  --- Set a penalty when the A2G attack has failed.
  -- @param #TASK_A2G_SEAD self
  -- @param #string PlayerName The name of the player.
  -- @param #number Penalty The penalty in points, must be a negative value!
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2G_SEAD
  function TASK_A2G_SEAD:SetScoreOnFail( PlayerName, Penalty, TaskUnit )
    self:F( { PlayerName, Penalty, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Failed", "The SEADing has failed!", Penalty )

    return self
  end

end

do -- TASK_A2G_BAI

  --- The TASK_A2G_BAI class
  -- @type TASK_A2G_BAI
  -- @field Core.Set#SET_UNIT TargetSetUnit
  -- @extends Tasking.Task#TASK

  --- Defines a Battlefield Air Interdiction task for a human player to be executed.
  -- These tasks are more strategic in nature and are most of the time further away from friendly forces.
  -- BAI tasks can also be used to express the abscence of friendly forces near the vicinity.
  -- 
  -- The TASK_A2G_BAI is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create BAI tasks 
  -- based on detected enemy ground targets.
  -- 
  -- @field #TASK_A2G_BAI
  TASK_A2G_BAI = { ClassName = "TASK_A2G_BAI" }

  --- Instantiates a new TASK_A2G_BAI.
  -- @param #TASK_A2G_BAI self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned.
  -- @param #string TaskName The name of the Task.
  -- @param Core.Set#SET_UNIT TargetSetUnit 
  -- @param #string TaskBriefing The briefing of the task.
  -- @return #TASK_A2G_BAI self
  function TASK_A2G_BAI:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing )
    local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "BAI", TaskBriefing ) ) -- #TASK_A2G_BAI
    self:F()

    Mission:AddTask( self )

    self:SetBriefing( TaskBriefing or "Execute a Battlefield Air Interdiction of a group of enemy targets." )

    return self
  end

  --- Set a score when a target in scope of the A2G attack, has been destroyed .
  -- @param #TASK_A2G_BAI self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points to be granted when task process has been achieved.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2G_BAI
  function TASK_A2G_BAI:SetScoreOnProgress( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has destroyed a target in Battlefield Air Interdiction (BAI).", Score )

    return self
  end

  --- Set a score when all the targets in scope of the A2G attack, have been destroyed.
  -- @param #TASK_A2G_BAI self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2G_BAI
  function TASK_A2G_BAI:SetScoreOnSuccess( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Success", "All targets have been successfully destroyed! The Battlefield Air Interdiction (BAI) is a success!", Score )

    return self
  end

  --- Set a penalty when the A2G attack has failed.
  -- @param #TASK_A2G_BAI self
  -- @param #string PlayerName The name of the player.
  -- @param #number Penalty The penalty in points, must be a negative value!
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2G_BAI
  function TASK_A2G_BAI:SetScoreOnFail( PlayerName, Penalty, TaskUnit )
    self:F( { PlayerName, Penalty, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Failed", "The Battlefield Air Interdiction (BAI) has failed!", Penalty )

    return self
  end

end

do -- TASK_A2G_CAS

  --- The TASK_A2G_CAS class
  -- @type TASK_A2G_CAS
  -- @field Core.Set#SET_UNIT TargetSetUnit
  -- @extends Tasking.Task#TASK

  --- Defines an Close Air Support task for a human player to be executed.
  -- Friendly forces will be in the vicinity within 6km from the enemy.
  -- 
  -- The TASK_A2G_CAS is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create CAS tasks 
  -- based on detected enemy ground targets.
  -- 
  -- @field #TASK_A2G_CAS
  TASK_A2G_CAS = { ClassName = "TASK_A2G_CAS" }

  --- Instantiates a new TASK_A2G_CAS.
  -- @param #TASK_A2G_CAS self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned.
  -- @param #string TaskName The name of the Task.
  -- @param Core.Set#SET_UNIT TargetSetUnit 
  -- @param #string TaskBriefing The briefing of the task.
  -- @return #TASK_A2G_CAS self
  function TASK_A2G_CAS:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing )
    local self = BASE:Inherit( self, TASK_A2G:New( Mission, SetGroup, TaskName, TargetSetUnit, "CAS", TaskBriefing ) ) -- #TASK_A2G_CAS
    self:F()

    Mission:AddTask( self )

    self:SetBriefing( TaskBriefing or ( "Execute a Close Air Support for a group of enemy targets. " .. "Beware of friendlies at the vicinity! " ) )

    return self
  end

  --- Set a score when a target in scope of the A2G attack, has been destroyed .
  -- @param #TASK_A2G_CAS self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points to be granted when task process has been achieved.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2G_CAS
  function TASK_A2G_CAS:SetScoreOnProgress( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has destroyed a target in Close Air Support (CAS).", Score )

    return self
  end

  --- Set a score when all the targets in scope of the A2G attack, have been destroyed.
  -- @param #TASK_A2G_CAS self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2G_CAS
  function TASK_A2G_CAS:SetScoreOnSuccess( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Success", "All targets have been successfully destroyed! The Close Air Support (CAS) was a success!", Score )

    return self
  end

  --- Set a penalty when the A2G attack has failed.
  -- @param #TASK_A2G_CAS self
  -- @param #string PlayerName The name of the player.
  -- @param #number Penalty The penalty in points, must be a negative value!
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2G_CAS
  function TASK_A2G_CAS:SetScoreOnFail( PlayerName, Penalty, TaskUnit )
    self:F( { PlayerName, Penalty, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Failed", "The Close Air Support (CAS) has failed!", Penalty )

    return self
  end

end
--- **Tasking** - Dynamically allocates A2A tasks to human players, based on detected airborne targets through an EWR network.
--
-- **Features:**
--
--   * Dynamically assign tasks to human players based on detected targets.
--   * Dynamically change the tasks as the tactical situation evolves during the mission.
--   * Dynamically assign (CAP) Control Air Patrols tasks for human players to perform CAP.
--   * Dynamically assign (GCI) Ground Control Intercept tasks for human players to perform GCI.
--   * Dynamically assign Engage tasks for human players to engage on close-by airborne bogeys.
--   * Define and use an EWR (Early Warning Radar) network.
--   * Define different ranges to engage upon intruders.
--   * Keep task achievements.
--   * Score task achievements.
--
-- ===
--
-- ### Author: **FlightControl**
--
-- ### Contributions:
--
-- ===
--
-- @module Tasking.Task_A2A_Dispatcher
-- @image Task_A2A_Dispatcher.JPG

do -- TASK_A2A_DISPATCHER

  --- TASK_A2A_DISPATCHER class.
  -- @type TASK_A2A_DISPATCHER
  -- @extends Tasking.DetectionManager#DETECTION_MANAGER

  --- Orchestrates the dynamic dispatching of tasks upon groups of detected units determined a @{Core.Set} of EWR installation groups.
  -- 
  -- ![Banner Image](..\Images\deprecated.png)
  --
  -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia3.JPG)
  --
  -- The EWR will detect units, will group them, and will dispatch @{Tasking.Task}s to groups. Depending on the type of target detected, different tasks will be dispatched.
  -- Find a summary below describing for which situation a task type is created:
  --
  -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia9.JPG)
  --
  --   * **INTERCEPT Task**: Is created when the target is known, is detected and within a danger zone, and there is no friendly airborne in range.
  --   * **SWEEP Task**: Is created when the target is unknown, was detected and the last position is only known, and within a danger zone, and there is no friendly airborne in range.
  --   * **ENGAGE Task**: Is created when the target is known, is detected and within a danger zone, and there is a friendly airborne in range, that will receive this task.
  --
  -- ## 1. TASK\_A2A\_DISPATCHER constructor:
  --
  -- The @{#TASK_A2A_DISPATCHER.New}() method creates a new TASK\_A2A\_DISPATCHER instance.
  --
  -- ### 1.1. Define or set the **Mission**:
  --
  -- Tasking is executed to accomplish missions. Therefore, a MISSION object needs to be given as the first parameter.
  --
  --     local HQ = GROUP:FindByName( "HQ", "Bravo" )
  --     local CommandCenter = COMMANDCENTER:New( HQ, "Lima" )
  --     local Mission = MISSION:New( CommandCenter, "A2A Mission", "High", "Watch the air enemy units being detected.", coalition.side.RED )
  --
  -- Missions are governed by COMMANDCENTERS, so, ensure you have a COMMANDCENTER object installed and setup within your mission.
  -- Create the MISSION object, and hook it under the command center.
  --
  -- ### 1.2. Build a set of the groups seated by human players:
  --
  -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia6.JPG)
  --
  -- A set or collection of the groups wherein human players can be seated, these can be clients or units that can be joined as a slot or jumping into.
  --
  --     local AttackGroups = SET_GROUP:New():FilterCoalitions( "red" ):FilterPrefixes( "Defender" ):FilterStart()
  --
  -- The set is built using the SET_GROUP class. Apply any filter criteria to identify the correct groups for your mission.
  -- Only these slots or units will be able to execute the mission and will receive tasks for this mission, once available.
  --
  -- ### 1.3. Define the **EWR network**:
  --
  -- As part of the TASK\_A2A\_DISPATCHER constructor, an EWR network must be given as the third parameter.
  -- An EWR network, or, Early Warning Radar network, is used to early detect potential airborne targets and to understand the position of patrolling targets of the enemy.
  --
  -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia5.JPG)
  --
  -- Typically EWR networks are setup using 55G6 EWR, 1L13 EWR, Hawk sr and Patriot str ground based radar units. 
  -- These radars have different ranges and 55G6 EWR and 1L13 EWR radars are Eastern Bloc units (eg Russia, Ukraine, Georgia) while the Hawk and Patriot radars are Western (eg US).
  -- Additionally, ANY other radar capable unit can be part of the EWR network! Also AWACS airborne units, planes, helicopters can help to detect targets, as long as they have radar.
  -- The position of these units is very important as they need to provide enough coverage 
  -- to pick up enemy aircraft as they approach so that CAP and GCI flights can be tasked to intercept them.
  --
  -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia7.JPG)
  --
  -- Additionally in a hot war situation where the border is no longer respected the placement of radars has a big effect on how fast the war escalates.
  -- For example if they are a long way forward and can detect enemy planes on the ground and taking off
  -- they will start to vector CAP and GCI flights to attack them straight away which will immediately draw a response from the other coalition.
  -- Having the radars further back will mean a slower escalation because fewer targets will be detected and
  -- therefore less CAP and GCI flights will spawn and this will tend to make just the border area active rather than a melee over the whole map.
  -- It all depends on what the desired effect is.
  --
  -- EWR networks are **dynamically constructed**, that is, they form part of the @{Functional.Detection#DETECTION_BASE} object that is given as the input parameter of the TASK\_A2A\_DISPATCHER class.
  -- By defining in a **smart way the names or name prefixes of the groups** with EWR capable units, these groups will be **automatically added or deleted** from the EWR network,
  -- increasing or decreasing the radar coverage of the Early Warning System.
  --
  -- See the following example to setup an EWR network containing EWR stations and AWACS.
  --
  --     local EWRSet = SET_GROUP:New():FilterPrefixes( "EWR" ):FilterCoalitions("red"):FilterStart()
  --
  --     local EWRDetection = DETECTION_AREAS:New( EWRSet, 6000 )
  --     EWRDetection:SetFriendliesRange( 10000 )
  --     EWRDetection:SetRefreshTimeInterval(30)
  --
  --     -- Setup the A2A dispatcher, and initialize it.
  --     A2ADispatcher = TASK_A2A_DISPATCHER:New( Mission, AttackGroups, EWRDetection )
  --
  -- The above example creates a SET_GROUP instance, and stores this in the variable (object) **EWRSet**.
  -- **EWRSet** is then being configured to filter all active groups with a group name starting with **EWR** to be included in the Set.
  -- **EWRSet** is then being ordered to start the dynamic filtering. Note that any destroy or new spawn of a group with the above names will be removed or added to the Set.
  -- Then a new **EWRDetection** object is created from the class DETECTION_AREAS. A grouping radius of 6000 is chosen, which is 6 km.
  -- The **EWRDetection** object is then passed to the @{#TASK_A2A_DISPATCHER.New}() method to indicate the EWR network configuration and setup the A2A tasking and detection mechanism.
  --
  -- ### 2. Define the detected **target grouping radius**:
  --
  -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia8.JPG)
  --
  -- The target grouping radius is a property of the Detection object, that was passed to the AI\_A2A\_DISPATCHER object, but can be changed.
  -- The grouping radius should not be too small, but also depends on the types of planes and the era of the simulation.
  -- Fast planes like in the 80s, need a larger radius than WWII planes.
  -- Typically I suggest to use 30000 for new generation planes and 10000 for older era aircraft.
  --
  -- Note that detected targets are constantly re-grouped, that is, when certain detected aircraft are moving further than the group radius, then these aircraft will become a separate
  -- group being detected. This may result in additional GCI being started by the dispatcher! So don't make this value too small!
  --
  -- ## 3. Set the **Engage radius**:
  --
  -- Define the radius to engage any target by airborne friendlies, which are executing cap or returning from an intercept mission.
  --
  -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia11.JPG)
  --
  -- So, if there is a target area detected and reported,
  -- then any friendlies that are airborne near this target area,
  -- will be commanded to (re-)engage that target when available (if no other tasks were commanded).
  -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target,
  -- will be considered to receive the command to engage that target area.
  -- You need to evaluate the value of this parameter carefully.
  -- If too small, more intercept missions may be triggered upon detected target areas.
  -- If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far.
  --
  -- ## 4. Set **Scoring** and **Messages**:
  --
  -- The TASK\_A2A\_DISPATCHER is a state machine. It triggers the event Assign when a new player joins a @{Tasking.Task} dispatched by the TASK\_A2A\_DISPATCHER.
  -- An _event handler_ can be defined to catch the **Assign** event, and add **additional processing** to set _scoring_ and to _define messages_,
  -- when the player reaches certain achievements in the task.
  --
  -- The prototype to handle the **Assign** event needs to be developed as follows:
  --
  --      TaskDispatcher = TASK_A2A_DISPATCHER:New( ... )
  --
  --      -- @param #TaskDispatcher self
  --      -- @param #string From Contains the name of the state from where the Event was triggered.
  --      -- @param #string Event Contains the name of the event that was triggered. In this case Assign.
  --      -- @param #string To Contains the name of the state that will be transitioned to.
  --      -- @param Tasking.Task_A2A#TASK_A2A Task The Task object, which is any derived object from TASK_A2A.
  --      -- @param Wrapper.Unit#UNIT TaskUnit The Unit or Client that contains the Player.
  --      -- @param #string PlayerName The name of the Player that joined the TaskUnit.
  --      function TaskDispatcher:OnAfterAssign( From, Event, To, Task, TaskUnit, PlayerName )
  --        Task:SetScoreOnProgress( PlayerName, 20, TaskUnit )
  --        Task:SetScoreOnSuccess( PlayerName, 200, TaskUnit )
  --        Task:SetScoreOnFail( PlayerName, -100, TaskUnit )
  --      end
  --
  -- The **OnAfterAssign** method (function) is added to the TaskDispatcher object.
  -- This method will be called when a new player joins a unit in the set of groups in scope of the dispatcher.
  -- So, this method will be called only **ONCE** when a player joins a unit in scope of the task.
  --
  -- The TASK class implements various methods to additional **set scoring** for player achievements:
  --
  --   * @{Tasking.Task#TASK.SetScoreOnProgress}() will add additional scores when a player achieves **Progress** while executing the task.
  --     Examples of **task progress** can be destroying units, arriving at zones etc.
  --
  --   * @{Tasking.Task#TASK.SetScoreOnSuccess}() will add additional scores when the task goes into **Success** state.
  --     This means the **task has been successfully completed**.
  --
  --   * @{Tasking.Task#TASK.SetScoreOnSuccess}() will add additional (negative) scores when the task goes into **Failed** state.
  --     This means the **task has not been successfully completed**, and the scores must be given with a negative value!
  --
  -- # Developer Note
  -- 
  -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
  -- Therefore, this class is considered to be deprecated
  --
  -- @field #TASK_A2A_DISPATCHER
  TASK_A2A_DISPATCHER = {
    ClassName = "TASK_A2A_DISPATCHER",
    Mission = nil,
    Detection = nil,
    Tasks = {},
    SweepZones = {},
  }

  --- TASK_A2A_DISPATCHER constructor.
  -- @param #TASK_A2A_DISPATCHER self
  -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done.
  -- @param Core.Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission.
  -- @param Functional.Detection#DETECTION_BASE Detection The detection results that are used to dynamically assign new tasks to human players.
  -- @return #TASK_A2A_DISPATCHER self
  function TASK_A2A_DISPATCHER:New( Mission, SetGroup, Detection )

    -- Inherits from DETECTION_MANAGER
    local self = BASE:Inherit( self, DETECTION_MANAGER:New( SetGroup, Detection ) ) -- #TASK_A2A_DISPATCHER

    self.Detection = Detection
    self.Mission = Mission
    self.FlashNewTask = false

    -- TODO: Check detection through radar.
    self.Detection:FilterCategories( Unit.Category.AIRPLANE, Unit.Category.HELICOPTER )
    self.Detection:InitDetectRadar( true )
    self.Detection:SetRefreshTimeInterval( 30 )

    self:AddTransition( "Started", "Assign", "Started" )

    --- OnAfter Transition Handler for Event Assign.
    -- @function [parent=#TASK_A2A_DISPATCHER] OnAfterAssign
    -- @param #TASK_A2A_DISPATCHER self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @param Tasking.Task_A2A#TASK_A2A Task
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param #string PlayerName

    self:__Start( 5 )

    return self
  end

  --- Define the radius to when an ENGAGE task will be generated for any nearby by airborne friendlies, which are executing cap or returning from an intercept mission.
  -- So, if there is a target area detected and reported,
  -- then any friendlies that are airborne near this target area,
  -- will be commanded to (re-)engage that target when available (if no other tasks were commanded).
  -- An ENGAGE task will be created for those pilots.
  -- For example, if 100000 is given as a value, then any friendly that is airborne within 100km from the detected target, 
  -- will be considered to receive the command to engage that target area.
  -- You need to evaluate the value of this parameter carefully.
  -- If too small, more intercept missions may be triggered upon detected target areas.
  -- If too large, any airborne cap may not be able to reach the detected target area in time, because it is too far.
  -- @param #TASK_A2A_DISPATCHER self
  -- @param #number EngageRadius (Optional, Default = 100000) The radius to report friendlies near the target.
  -- @return #TASK_A2A_DISPATCHER
  -- @usage
  --
  --   -- Set 50km as the radius to engage any target by airborne friendlies.
  --   TaskA2ADispatcher:SetEngageRadius( 50000 )
  --
  --   -- Set 100km as the radius to engage any target by airborne friendlies.
  --   TaskA2ADispatcher:SetEngageRadius() -- 100000 is the default value.
  --
  function TASK_A2A_DISPATCHER:SetEngageRadius( EngageRadius )

    self.Detection:SetFriendliesRange( EngageRadius or 100000 )

    return self
  end

  --- Set flashing player messages on or off
  -- @param #TASK_A2A_DISPATCHER self
  -- @param #boolean onoff Set messages on (true) or off (false)
  function TASK_A2A_DISPATCHER:SetSendMessages( onoff )
    self.FlashNewTask = onoff
  end

  --- Creates an INTERCEPT task when there are targets for it.
  -- @param #TASK_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem
  -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units.
  -- @return #nil If there are no targets to be set.
  function TASK_A2A_DISPATCHER:EvaluateINTERCEPT( DetectedItem )
    self:F( { DetectedItem.ItemID } )

    local DetectedSet = DetectedItem.Set
    local DetectedZone = DetectedItem.Zone

    -- Check if there is at least one UNIT in the DetectedSet is visible.

    if DetectedItem.IsDetected == true then

      -- Here we're doing something advanced... We're copying the DetectedSet.
      local TargetSetUnit = SET_UNIT:New()
      TargetSetUnit:SetDatabase( DetectedSet )
      TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection.

      return TargetSetUnit
    end

    return nil
  end

  --- Creates an SWEEP task when there are targets for it.
  -- @param #TASK_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem
  -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units.
  -- @return #nil If there are no targets to be set.
  function TASK_A2A_DISPATCHER:EvaluateSWEEP( DetectedItem )
    self:F( { DetectedItem.ItemID } )

    local DetectedSet = DetectedItem.Set
    local DetectedZone = DetectedItem.Zone -- TODO: This seems unused, remove?

    if DetectedItem.IsDetected == false then

      -- Here we're doing something advanced... We're copying the DetectedSet.
      local TargetSetUnit = SET_UNIT:New()
      TargetSetUnit:SetDatabase( DetectedSet )
      TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection.

      return TargetSetUnit
    end

    return nil
  end

  --- Creates an ENGAGE task when there are human friendlies airborne near the targets.
  -- @param #TASK_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE.DetectedItem DetectedItem
  -- @return Core.Set#SET_UNIT TargetSetUnit: The target set of units.
  -- @return #nil If there are no targets to be set.
  function TASK_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem )
    self:F( { DetectedItem.ItemID } )

    local DetectedSet = DetectedItem.Set
    local DetectedZone = DetectedItem.Zone -- TODO: This seems unused, remove?

    local PlayersCount, PlayersReport = self:GetPlayerFriendliesNearBy( DetectedItem )

    -- Only allow ENGAGE when there are Players near the zone, and when the Area has detected items since the last run in a 60 seconds time zone.
    if PlayersCount > 0 and DetectedItem.IsDetected == true then

      -- Here we're doing something advanced... We're copying the DetectedSet.
      local TargetSetUnit = SET_UNIT:New()
      TargetSetUnit:SetDatabase( DetectedSet )
      TargetSetUnit:FilterOnce() -- Filter but don't do any events!!! Elements are added manually upon each detection.

      return TargetSetUnit
    end

    return nil
  end

  --- Evaluates the removal of the Task from the Mission.
  -- Can only occur when the DetectedItem is Changed AND the state of the Task is "Planned".
  -- @param #TASK_A2A_DISPATCHER self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Tasking.Task#TASK Task
  -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object.
  -- @param #boolean DetectedItemID
  -- @param #boolean DetectedItemChange
  -- @return Tasking.Task#TASK
  function TASK_A2A_DISPATCHER:EvaluateRemoveTask( Mission, Task, Detection, DetectedItem, DetectedItemIndex, DetectedItemChanged )

    if Task then

      if Task:IsStatePlanned() then
        local TaskName = Task:GetName()
        local TaskType = TaskName:match( "(%u+)%.%d+" )

        self:T2( { TaskType = TaskType } )

        local Remove = false

        local IsPlayers = Detection:IsPlayersNearBy( DetectedItem )
        if TaskType == "ENGAGE" then
          if IsPlayers == false then
            Remove = true
          end
        end

        if TaskType == "INTERCEPT" then
          if IsPlayers == true then
            Remove = true
          end
          if DetectedItem.IsDetected == false then
            Remove = true
          end
        end

        if TaskType == "SWEEP" then
          if DetectedItem.IsDetected == true then
            Remove = true
          end
        end

        local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT
        -- DetectedSet:Flush( self )
        -- self:F( { DetectedSetCount = DetectedSet:Count() } )
        if DetectedSet:Count() == 0 then
          Remove = true
        end

        if DetectedItemChanged == true or Remove then
          Task = self:RemoveTask( DetectedItemIndex )
        end
      end
    end

    return Task
  end

  --- Calculates which friendlies are nearby the area
  -- @param #TASK_A2A_DISPATCHER self
  -- @param DetectedItem
  -- @return #number, Tasking.CommandCenter#REPORT
  function TASK_A2A_DISPATCHER:GetFriendliesNearBy( DetectedItem )

    local DetectedSet = DetectedItem.Set
    local FriendlyUnitsNearBy = self.Detection:GetFriendliesNearBy( DetectedItem, Unit.Category.AIRPLANE )

    local FriendlyTypes = {}
    local FriendliesCount = 0

    if FriendlyUnitsNearBy then
      local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G()
      for FriendlyUnitName, FriendlyUnitData in pairs( FriendlyUnitsNearBy ) do
        local FriendlyUnit = FriendlyUnitData -- Wrapper.Unit#UNIT
        if FriendlyUnit:IsAirPlane() then
          local FriendlyUnitThreatLevel = FriendlyUnit:GetThreatLevel()
          FriendliesCount = FriendliesCount + 1
          local FriendlyType = FriendlyUnit:GetTypeName()
          FriendlyTypes[FriendlyType] = FriendlyTypes[FriendlyType] and (FriendlyTypes[FriendlyType] + 1) or 1
          if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then
          end
        end
      end

    end

    -- self:F( { FriendliesCount = FriendliesCount } )

    local FriendlyTypesReport = REPORT:New()

    if FriendliesCount > 0 then
      for FriendlyType, FriendlyTypeCount in pairs( FriendlyTypes ) do
        FriendlyTypesReport:Add( string.format( "%d of %s", FriendlyTypeCount, FriendlyType ) )
      end
    else
      FriendlyTypesReport:Add( "-" )
    end

    return FriendliesCount, FriendlyTypesReport
  end

  --- Calculates which HUMAN friendlies are nearby the area
  -- @param #TASK_A2A_DISPATCHER self
  -- @param DetectedItem
  -- @return #number, Tasking.CommandCenter#REPORT
  function TASK_A2A_DISPATCHER:GetPlayerFriendliesNearBy( DetectedItem )

    local DetectedSet = DetectedItem.Set
    local PlayersNearBy = self.Detection:GetPlayersNearBy( DetectedItem )

    local PlayerTypes = {}
    local PlayersCount = 0

    if PlayersNearBy then
      local DetectedTreatLevel = DetectedSet:CalculateThreatLevelA2G()
      for PlayerUnitName, PlayerUnitData in pairs( PlayersNearBy ) do
        local PlayerUnit = PlayerUnitData -- Wrapper.Unit#UNIT
        local PlayerName = PlayerUnit:GetPlayerName()
        -- self:F( { PlayerName = PlayerName, PlayerUnit = PlayerUnit } )
        if PlayerUnit:IsAirPlane() and PlayerName ~= nil then
          local FriendlyUnitThreatLevel = PlayerUnit:GetThreatLevel()
          PlayersCount = PlayersCount + 1
          local PlayerType = PlayerUnit:GetTypeName()
          PlayerTypes[PlayerName] = PlayerType
          if DetectedTreatLevel < FriendlyUnitThreatLevel + 2 then
          end
        end
      end

    end

    local PlayerTypesReport = REPORT:New()

    if PlayersCount > 0 then
      for PlayerName, PlayerType in pairs( PlayerTypes ) do
        PlayerTypesReport:Add( string.format( '"%s" in %s', PlayerName, PlayerType ) )
      end
    else
      PlayerTypesReport:Add( "-" )
    end

    return PlayersCount, PlayerTypesReport
  end

  function TASK_A2A_DISPATCHER:RemoveTask( TaskIndex )
    self.Mission:RemoveTask( self.Tasks[TaskIndex] )
    self.Tasks[TaskIndex] = nil
  end

  --- Assigns tasks in relation to the detected items to the @{Core.Set#SET_GROUP}.
  -- @param #TASK_A2A_DISPATCHER self
  -- @param Functional.Detection#DETECTION_BASE Detection The detection created by the @{Functional.Detection#DETECTION_BASE} derived object.
  -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop.
  function TASK_A2A_DISPATCHER:ProcessDetected( Detection )
    self:F()

    local AreaMsg = {}
    local TaskMsg = {}
    local ChangeMsg = {}

    local Mission = self.Mission

    if Mission:IsIDLE() or Mission:IsENGAGED() then

      local TaskReport = REPORT:New()

      -- Checking the task queue for the dispatcher, and removing any obsolete task!
      for TaskIndex, TaskData in pairs( self.Tasks ) do
        local Task = TaskData -- Tasking.Task#TASK
        if Task:IsStatePlanned() then
          local DetectedItem = Detection:GetDetectedItemByIndex( TaskIndex )
          if not DetectedItem then
            local TaskText = Task:GetName()
            for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
              Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2A task %s for %s removed.", TaskText, Mission:GetShortText() ), TaskGroup )
            end
            Task = self:RemoveTask( TaskIndex )
          end
        end
      end

      -- Now that all obsolete tasks are removed, loop through the detected targets.
      for DetectedItemID, DetectedItem in pairs( Detection:GetDetectedItems() ) do

        local DetectedItem = DetectedItem -- Functional.Detection#DETECTION_BASE.DetectedItem
        local DetectedSet = DetectedItem.Set -- Core.Set#SET_UNIT
        local DetectedCount = DetectedSet:Count()
        local DetectedZone = DetectedItem.Zone
        -- self:F( { "Targets in DetectedItem", DetectedItem.ItemID, DetectedSet:Count(), tostring( DetectedItem ) } )
        -- DetectedSet:Flush( self )

        local DetectedID = DetectedItem.ID
        local TaskIndex = DetectedItem.Index
        local DetectedItemChanged = DetectedItem.Changed

        local Task = self.Tasks[TaskIndex]
        Task = self:EvaluateRemoveTask( Mission, Task, Detection, DetectedItem, TaskIndex, DetectedItemChanged ) -- Task will be removed if it is planned and changed.

        -- Evaluate INTERCEPT
        if not Task and DetectedCount > 0 then
          local TargetSetUnit = self:EvaluateENGAGE( DetectedItem ) -- Returns a SetUnit if there are targets to be INTERCEPTed...
          if TargetSetUnit then
            Task = TASK_A2A_ENGAGE:New( Mission, self.SetGroup, string.format( "ENGAGE.%03d", DetectedID ), TargetSetUnit )
            Task:SetDetection( Detection, DetectedItem )
            Task:UpdateTaskInfo( DetectedItem )
          else
            local TargetSetUnit = self:EvaluateINTERCEPT( DetectedItem ) -- Returns a SetUnit if there are targets to be INTERCEPTed...
            if TargetSetUnit then
              Task = TASK_A2A_INTERCEPT:New( Mission, self.SetGroup, string.format( "INTERCEPT.%03d", DetectedID ), TargetSetUnit )
              Task:SetDetection( Detection, DetectedItem )
              Task:UpdateTaskInfo( DetectedItem )
            else
              local TargetSetUnit = self:EvaluateSWEEP( DetectedItem ) -- Returns a SetUnit 
              if TargetSetUnit then
                Task = TASK_A2A_SWEEP:New( Mission, self.SetGroup, string.format( "SWEEP.%03d", DetectedID ), TargetSetUnit )
                Task:SetDetection( Detection, DetectedItem )
                Task:UpdateTaskInfo( DetectedItem )
              end
            end
          end

          if Task then
            self.Tasks[TaskIndex] = Task
            Task:SetTargetZone( DetectedZone, DetectedItem.Coordinate.y, DetectedItem.Coordinate.Heading )
            Task:SetDispatcher( self )
            Mission:AddTask( Task )

            function Task.OnEnterSuccess( Task, From, Event, To )
              self:Success( Task )
            end

            function Task.OnEnterCancelled( Task, From, Event, To )
              self:Cancelled( Task )
            end

            function Task.OnEnterFailed( Task, From, Event, To )
              self:Failed( Task )
            end

            function Task.OnEnterAborted( Task, From, Event, To )
              self:Aborted( Task )
            end

            TaskReport:Add( Task:GetName() )
          else
            self:F( "This should not happen" )
          end

        end

        if Task then
          local FriendliesCount, FriendliesReport = self:GetFriendliesNearBy( DetectedItem, Unit.Category.AIRPLANE )
          Task.TaskInfo:AddText( "Friendlies", string.format( "%d ( %s )", FriendliesCount, FriendliesReport:Text( "," ) ), 40, "MOD" )
          local PlayersCount, PlayersReport = self:GetPlayerFriendliesNearBy( DetectedItem )
          Task.TaskInfo:AddText( "Players", string.format( "%d ( %s )", PlayersCount, PlayersReport:Text( "," ) ), 40, "MOD" )
        end

        -- OK, so the tasking has been done, now delete the changes reported for the area.
        Detection:AcceptChanges( DetectedItem )
      end

      -- TODO set menus using the HQ coordinator
      Mission:GetCommandCenter():SetMenu()

      local TaskText = TaskReport:Text( ", " )

      for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
        if (not Mission:IsGroupAssigned( TaskGroup )) and TaskText ~= "" and (self.FlashNewTask) then
          Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup )
        end
      end

    end

    return true
  end

end
--- **Tasking** - The TASK_A2A models tasks for players in Air to Air engagements.
--
-- ===
--
-- ### Author: **FlightControl**
--
-- ### Contributions:
--
-- ===
--
-- @module Tasking.Task_A2A
-- @image MOOSE.JPG

do -- TASK_A2A

  --- The TASK_A2A class
  -- @type TASK_A2A
  -- @field Core.Set#SET_UNIT TargetSetUnit
  -- @extends Tasking.Task#TASK

  --- Defines Air To Air tasks for a @{Core.Set} of Target Units, 
  -- based on the tasking capabilities defined in @{Tasking.Task#TASK}.
  -- 
  -- ![Banner Image](..\Images\deprecated.png)
  -- 
  -- The TASK_A2A is implemented using a @{Core.Fsm#FSM_TASK}, and has the following statuses:
  -- 
  --   * **None**: Start of the process
  --   * **Planned**: The A2A task is planned.
  --   * **Assigned**: The A2A task is assigned to a @{Wrapper.Group#GROUP}.
  --   * **Success**: The A2A task is successfully completed.
  --   * **Failed**: The A2A task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ.
  -- 
  -- # 1) Set the scoring of achievements in an A2A attack.
  -- 
  -- Scoring or penalties can be given in the following circumstances:
  -- 
  --   * @{#TASK_A2A.SetScoreOnDestroy}(): Set a score when a target in scope of the A2A attack, has been destroyed.
  --   * @{#TASK_A2A.SetScoreOnSuccess}(): Set a score when all the targets in scope of the A2A attack, have been destroyed.
  --   * @{#TASK_A2A.SetPenaltyOnFailed}(): Set a penalty when the A2A attack has failed.
  --
  -- # Developer Note
  -- 
  -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
  -- Therefore, this class is considered to be deprecated
  --
  -- @field #TASK_A2A
  TASK_A2A = {
    ClassName = "TASK_A2A"
  }

  --- Instantiates a new TASK_A2A.
  -- @param #TASK_A2A self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Core.Set#SET_GROUP SetAttack The set of groups for which the Task can be assigned.
  -- @param #string TaskName The name of the Task.
  -- @param Core.Set#SET_UNIT UnitSetTargets
  -- @param #number TargetDistance The distance to Target when the Player is considered to have "arrived" at the engagement range.
  -- @param Core.Zone#ZONE_BASE TargetZone The target zone, if known.
  -- If the TargetZone parameter is specified, the player will be routed to the center of the zone where all the targets are assumed to be.
  -- @return #TASK_A2A self
  function TASK_A2A:New( Mission, SetAttack, TaskName, TargetSetUnit, TaskType, TaskBriefing )
    local self = BASE:Inherit( self, TASK:New( Mission, SetAttack, TaskName, TaskType, TaskBriefing ) ) -- Tasking.Task#TASK_A2A
    self:F()

    self.TargetSetUnit = TargetSetUnit
    self.TaskType = TaskType

    local Fsm = self:GetUnitProcess()

    Fsm:AddTransition( "Assigned", "RouteToRendezVous", "RoutingToRendezVous" )
    Fsm:AddProcess( "RoutingToRendezVous", "RouteToRendezVousPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtRendezVous" } )
    Fsm:AddProcess( "RoutingToRendezVous", "RouteToRendezVousZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtRendezVous" } )

    Fsm:AddTransition( { "Arrived", "RoutingToRendezVous" }, "ArriveAtRendezVous", "ArrivedAtRendezVous" )

    Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "Engage", "Engaging" )
    Fsm:AddTransition( { "ArrivedAtRendezVous", "HoldingAtRendezVous" }, "HoldAtRendezVous", "HoldingAtRendezVous" )

    Fsm:AddProcess( "Engaging", "Account", ACT_ACCOUNT_DEADS:New(), {} )
    Fsm:AddTransition( "Engaging", "RouteToTarget", "Engaging" )
    Fsm:AddProcess( "Engaging", "RouteToTargetZone", ACT_ROUTE_ZONE:New(), {} )
    Fsm:AddProcess( "Engaging", "RouteToTargetPoint", ACT_ROUTE_POINT:New(), {} )
    Fsm:AddTransition( "Engaging", "RouteToTargets", "Engaging" )

    -- Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" )
    -- Fsm:AddTransition( "Accounted", "Success", "Success" )
    Fsm:AddTransition( "Rejected", "Reject", "Aborted" )
    Fsm:AddTransition( "Failed", "Fail", "Failed" )

    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param #TASK_CARGO Task
    function Fsm:OnLeaveAssigned( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )

      self:SelectAction()
    end

    --- Test
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_A2A#TASK_A2A Task
    function Fsm:onafterRouteToRendezVous( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      -- Determine the first Unit from the self.RendezVousSetUnit

      if Task:GetRendezVousZone( TaskUnit ) then
        self:__RouteToRendezVousZone( 0.1 )
      else
        if Task:GetRendezVousCoordinate( TaskUnit ) then
          self:__RouteToRendezVousPoint( 0.1 )
        else
          self:__ArriveAtRendezVous( 0.1 )
        end
      end
    end

    --- Test
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task#TASK_A2A Task
    function Fsm:OnAfterArriveAtRendezVous( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      -- Determine the first Unit from the self.TargetSetUnit

      self:__Engage( 0.1 )
    end

    --- Test
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task#TASK_A2A Task
    function Fsm:onafterEngage( TaskUnit, Task )
      self:F( { self } )
      self:__Account( 0.1 )
      self:__RouteToTarget( 0.1 )
      self:__RouteToTargets( -10 )
    end

    --- Test
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_A2A#TASK_A2A Task
    function Fsm:onafterRouteToTarget( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      -- Determine the first Unit from the self.TargetSetUnit

      if Task:GetTargetZone( TaskUnit ) then
        self:__RouteToTargetZone( 0.1 )
      else
        local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT
        if TargetUnit then
          local Coordinate = TargetUnit:GetPointVec3()
          self:T( { TargetCoordinate = Coordinate, Coordinate:GetX(), Coordinate:GetAlt(), Coordinate:GetZ() } )
          Task:SetTargetCoordinate( Coordinate, TaskUnit )
        end
        self:__RouteToTargetPoint( 0.1 )
      end
    end

    --- Test
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_A2A#TASK_A2A Task
    function Fsm:onafterRouteToTargets( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      local TargetUnit = Task.TargetSetUnit:GetFirst() -- Wrapper.Unit#UNIT
      if TargetUnit then
        Task:SetTargetCoordinate( TargetUnit:GetCoordinate(), TaskUnit )
      end
      self:__RouteToTargets( -10 )
    end

    return self

  end

  -- @param #TASK_A2A self
  -- @param Core.Set#SET_UNIT TargetSetUnit The set of targets.
  function TASK_A2A:SetTargetSetUnit( TargetSetUnit )

    self.TargetSetUnit = TargetSetUnit
  end

  -- @param #TASK_A2A self
  function TASK_A2A:GetPlannedMenuText()
    return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )"
  end

  -- @param #TASK_A2A self
  -- @param Core.Point#COORDINATE RendezVousCoordinate The Coordinate object referencing to the 2D point where the RendezVous point is located on the map.
  -- @param #number RendezVousRange The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point.
  -- @param Wrapper.Unit#UNIT TaskUnit
  function TASK_A2A:SetRendezVousCoordinate( RendezVousCoordinate, RendezVousRange, TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT
    ActRouteRendezVous:SetCoordinate( RendezVousCoordinate )
    ActRouteRendezVous:SetRange( RendezVousRange )
  end

  -- @param #TASK_A2A self
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return Core.Point#COORDINATE The Coordinate object referencing to the 2D point where the RendezVous point is located on the map.
  -- @return #number The RendezVousRange that defines when the player is considered to have arrived at the RendezVous point.
  function TASK_A2A:GetRendezVousCoordinate( TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT
    return ActRouteRendezVous:GetCoordinate(), ActRouteRendezVous:GetRange()
  end

  -- @param #TASK_A2A self
  -- @param Core.Zone#ZONE_BASE RendezVousZone The Zone object where the RendezVous is located on the map.
  -- @param Wrapper.Unit#UNIT TaskUnit
  function TASK_A2A:SetRendezVousZone( RendezVousZone, TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE
    ActRouteRendezVous:SetZone( RendezVousZone )
  end

  -- @param #TASK_A2A self
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return Core.Zone#ZONE_BASE The Zone object where the RendezVous is located on the map.
  function TASK_A2A:GetRendezVousZone( TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteRendezVous = ProcessUnit:GetProcess( "RoutingToRendezVous", "RouteToRendezVousZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE
    return ActRouteRendezVous:GetZone()
  end

  -- @param #TASK_A2A self
  -- @param Core.Point#COORDINATE TargetCoordinate The Coordinate object where the Target is located on the map.
  -- @param Wrapper.Unit#UNIT TaskUnit
  function TASK_A2A:SetTargetCoordinate( TargetCoordinate, TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT
    ActRouteTarget:SetCoordinate( TargetCoordinate )
  end

  -- @param #TASK_A2A self
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return Core.Point#COORDINATE The Coordinate object where the Target is located on the map.
  function TASK_A2A:GetTargetCoordinate( TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT
    return ActRouteTarget:GetCoordinate()
  end

  -- @param #TASK_A2A self
  -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map.
  -- @param Wrapper.Unit#UNIT TaskUnit
  function TASK_A2A:SetTargetZone( TargetZone, Altitude, Heading, TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE
    ActRouteTarget:SetZone( TargetZone, Altitude, Heading )
  end

  -- @param #TASK_A2A self
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map.
  function TASK_A2A:GetTargetZone( TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE
    return ActRouteTarget:GetZone()
  end

  function TASK_A2A:SetGoalTotal()

    self.GoalTotal = self.TargetSetUnit:Count()
  end

  function TASK_A2A:GetGoalTotal()

    return self.GoalTotal
  end

  --- Return the relative distance to the target vicinity from the player, in order to sort the targets in the reports per distance from the threats.
  -- @param #TASK_A2A self
  function TASK_A2A:ReportOrder( ReportGroup )
    self:UpdateTaskInfo( self.DetectedItem )

    local Coordinate = self.TaskInfo:GetData( "Coordinate" )
    local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate )

    return Distance
  end

  --- This method checks every 10 seconds if the goal has been reached of the task.
  -- @param #TASK_A2A self
  function TASK_A2A:onafterGoal( TaskUnit, From, Event, To )
    local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT

    if TargetSetUnit:Count() == 0 then
      self:Success()
    end

    self:__Goal( -10 )
  end

  -- @param #TASK_A2A self
  function TASK_A2A:UpdateTaskInfo( DetectedItem )

    if self:IsStatePlanned() or self:IsStateAssigned() then
      local TargetCoordinate = DetectedItem and self.Detection:GetDetectedItemCoordinate( DetectedItem ) or self.TargetSetUnit:GetFirst():GetCoordinate()
      self.TaskInfo:AddTaskName( 0, "MSOD" )
      self.TaskInfo:AddCoordinate( TargetCoordinate, 1, "SOD" )

      local ThreatLevel, ThreatText
      if DetectedItem then
        ThreatLevel, ThreatText = self.Detection:GetDetectedItemThreatLevel( DetectedItem )
      else
        ThreatLevel, ThreatText = self.TargetSetUnit:CalculateThreatLevelA2G()
      end
      self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 10, "MOD", true )

      if self.Detection then
        local DetectedItemsCount = self.TargetSetUnit:Count()
        local ReportTypes = REPORT:New()
        local TargetTypes = {}
        for TargetUnitName, TargetUnit in pairs( self.TargetSetUnit:GetSet() ) do
          local TargetType = self.Detection:GetDetectedUnitTypeName( TargetUnit )
          if not TargetTypes[TargetType] then
            TargetTypes[TargetType] = TargetType
            ReportTypes:Add( TargetType )
          end
        end
        self.TaskInfo:AddTargetCount( DetectedItemsCount, 11, "O", true )
        self.TaskInfo:AddTargets( DetectedItemsCount, ReportTypes:Text( ", " ), 20, "D", true )
      else
        local DetectedItemsCount = self.TargetSetUnit:Count()
        local DetectedItemsTypes = self.TargetSetUnit:GetTypeNames()
        self.TaskInfo:AddTargetCount( DetectedItemsCount, 11, "O", true )
        self.TaskInfo:AddTargets( DetectedItemsCount, DetectedItemsTypes, 20, "D", true )
      end
    end
  end

  --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection.
  -- @param #TASK_A2A self
  -- @param #number AutoAssignMethod The method to be applied to the task.
  -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center.
  -- @param Wrapper.Group#GROUP TaskGroup The player group.
  function TASK_A2A:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup )

    if AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then
      return math.random( 1, 9 )
    elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then
      local Coordinate = self.TaskInfo:GetData( "Coordinate" )
      local Distance = Coordinate:Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() )
      return math.floor( Distance )
    elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then
      return 1
    end

    return 0
  end

end

do -- TASK_A2A_INTERCEPT

  --- The TASK_A2A_INTERCEPT class
  -- @type TASK_A2A_INTERCEPT
  -- @field Core.Set#SET_UNIT TargetSetUnit
  -- @extends Tasking.Task#TASK

  --- Defines an intercept task for a human player to be executed.
  -- When enemy planes need to be intercepted by human players, use this task type to urge the players to get out there!
  --
  -- The TASK_A2A_INTERCEPT is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create intercept tasks
  -- based on detected airborne enemy targets intruding friendly airspace.
  --
  -- The task is defined for a @{Tasking.Mission#MISSION}, where a friendly @{Core.Set#SET_GROUP} consisting of GROUPs with one human players each, is intercepting the targets.
  -- The task is given a name and a briefing, that is used in the menu structure and in the reporting.
  --
  -- @field #TASK_A2A_INTERCEPT
  TASK_A2A_INTERCEPT = {
    ClassName = "TASK_A2A_INTERCEPT"
  }

  --- Instantiates a new TASK_A2A_INTERCEPT.
  -- @param #TASK_A2A_INTERCEPT self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned.
  -- @param #string TaskName The name of the Task.
  -- @param Core.Set#SET_UNIT TargetSetUnit
  -- @param #string TaskBriefing The briefing of the task.
  -- @return #TASK_A2A_INTERCEPT
  function TASK_A2A_INTERCEPT:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing )
    local self = BASE:Inherit( self, TASK_A2A:New( Mission, SetGroup, TaskName, TargetSetUnit, "INTERCEPT", TaskBriefing ) ) -- #TASK_A2A_INTERCEPT
    self:F()

    Mission:AddTask( self )

    self:SetBriefing( TaskBriefing or "Intercept incoming intruders.\n" )

    return self
  end

  --- Set a score when a target in scope of the A2A attack, has been destroyed.
  -- @param #TASK_A2A_INTERCEPT self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points to be granted when task process has been achieved.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2A_INTERCEPT
  function TASK_A2A_INTERCEPT:SetScoreOnProgress( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has intercepted a target.", Score )

    return self
  end

  --- Set a score when all the targets in scope of the A2A attack, have been destroyed.
  -- @param #TASK_A2A_INTERCEPT self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2A_INTERCEPT
  function TASK_A2A_INTERCEPT:SetScoreOnSuccess( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Success", "All targets have been successfully intercepted!", Score )

    return self
  end

  --- Set a penalty when the A2A attack has failed.
  -- @param #TASK_A2A_INTERCEPT self
  -- @param #string PlayerName The name of the player.
  -- @param #number Penalty The penalty in points, must be a negative value!
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2A_INTERCEPT
  function TASK_A2A_INTERCEPT:SetScoreOnFail( PlayerName, Penalty, TaskUnit )
    self:F( { PlayerName, Penalty, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Failed", "The intercept has failed!", Penalty )

    return self
  end

end

do -- TASK_A2A_SWEEP

  --- The TASK_A2A_SWEEP class
  -- @type TASK_A2A_SWEEP
  -- @field Core.Set#SET_UNIT TargetSetUnit
  -- @extends Tasking.Task#TASK

  --- Defines a sweep task for a human player to be executed.
  -- A sweep task needs to be given when targets were detected but somehow the detection was lost.
  -- Most likely, these enemy planes are hidden in the mountains or are flying under radar.
  -- These enemy planes need to be sweeped by human players, and use this task type to urge the players to get out there and find those enemy fighters.
  --
  -- The TASK_A2A_SWEEP is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create sweep tasks
  -- based on detected airborne enemy targets intruding friendly airspace, for which the detection has been lost for more than 60 seconds.
  --
  -- The task is defined for a @{Tasking.Mission#MISSION}, where a friendly @{Core.Set#SET_GROUP} consisting of GROUPs with one human players each, is sweeping the targets.
  -- The task is given a name and a briefing, that is used in the menu structure and in the reporting.
  --
  -- @field #TASK_A2A_SWEEP
  TASK_A2A_SWEEP = {
    ClassName = "TASK_A2A_SWEEP"
  }

  --- Instantiates a new TASK_A2A_SWEEP.
  -- @param #TASK_A2A_SWEEP self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned.
  -- @param #string TaskName The name of the Task.
  -- @param Core.Set#SET_UNIT TargetSetUnit 
  -- @param #string TaskBriefing The briefing of the task.
  -- @return #TASK_A2A_SWEEP self
  function TASK_A2A_SWEEP:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing )
    local self = BASE:Inherit( self, TASK_A2A:New( Mission, SetGroup, TaskName, TargetSetUnit, "SWEEP", TaskBriefing ) ) -- #TASK_A2A_SWEEP
    self:F()

    Mission:AddTask( self )

    self:SetBriefing( TaskBriefing or "Perform a fighter sweep. Incoming intruders were detected and could be hiding at the location.\n" )

    return self
  end

  -- @param #TASK_A2A_SWEEP self
  function TASK_A2A_SWEEP:onafterGoal( TaskUnit, From, Event, To )
    local TargetSetUnit = self.TargetSetUnit -- Core.Set#SET_UNIT

    if TargetSetUnit:Count() == 0 then
      self:Success()
    end

    self:__Goal( -10 )
  end

  --- Set a score when a target in scope of the A2A attack, has been destroyed.
  -- @param #TASK_A2A_SWEEP self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points to be granted when task process has been achieved.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2A_SWEEP
  function TASK_A2A_SWEEP:SetScoreOnProgress( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has sweeped a target.", Score )

    return self
  end

  --- Set a score when all the targets in scope of the A2A attack, have been destroyed.
  -- @param #TASK_A2A_SWEEP self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2A_SWEEP
  function TASK_A2A_SWEEP:SetScoreOnSuccess( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Success", "All targets have been successfully sweeped!", Score )

    return self
  end

  --- Set a penalty when the A2A attack has failed.
  -- @param #TASK_A2A_SWEEP self
  -- @param #string PlayerName The name of the player.
  -- @param #number Penalty The penalty in points, must be a negative value!
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2A_SWEEP
  function TASK_A2A_SWEEP:SetScoreOnFail( PlayerName, Penalty, TaskUnit )
    self:F( { PlayerName, Penalty, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Failed", "The sweep has failed!", Penalty )

    return self
  end

end

do -- TASK_A2A_ENGAGE

  --- The TASK_A2A_ENGAGE class
  -- @type TASK_A2A_ENGAGE
  -- @field Core.Set#SET_UNIT TargetSetUnit
  -- @extends Tasking.Task#TASK

  --- Defines an engage task for a human player to be executed.
  -- When enemy planes are close to human players, use this task type is used urge the players to get out there!
  --
  -- The TASK_A2A_ENGAGE is used by the @{Tasking.Task_A2A_Dispatcher#TASK_A2A_DISPATCHER} to automatically create engage tasks
  -- based on detected airborne enemy targets intruding friendly airspace.
  --
  -- The task is defined for a @{Tasking.Mission#MISSION}, where a friendly @{Core.Set#SET_GROUP} consisting of GROUPs with one human players each, is engaging the targets.
  -- The task is given a name and a briefing, that is used in the menu structure and in the reporting.
  --
  -- @field #TASK_A2A_ENGAGE
  TASK_A2A_ENGAGE = {
    ClassName = "TASK_A2A_ENGAGE"
  }

  --- Instantiates a new TASK_A2A_ENGAGE.
  -- @param #TASK_A2A_ENGAGE self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned.
  -- @param #string TaskName The name of the Task.
  -- @param Core.Set#SET_UNIT TargetSetUnit
  -- @param #string TaskBriefing The briefing of the task.
  -- @return #TASK_A2A_ENGAGE self
  function TASK_A2A_ENGAGE:New( Mission, SetGroup, TaskName, TargetSetUnit, TaskBriefing )
    local self = BASE:Inherit( self, TASK_A2A:New( Mission, SetGroup, TaskName, TargetSetUnit, "ENGAGE", TaskBriefing ) ) -- #TASK_A2A_ENGAGE
    self:F()

    Mission:AddTask( self )

    self:SetBriefing( TaskBriefing or "Bogeys are nearby! Players close by are ordered to ENGAGE the intruders!\n" )

    return self
  end

  --- Set a score when a target in scope of the A2A attack, has been destroyed .
  -- @param #TASK_A2A_ENGAGE self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points to be granted when task process has been achieved.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2A_ENGAGE
  function TASK_A2A_ENGAGE:SetScoreOnProgress( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScoreProcess( "Engaging", "Account", "AccountForPlayer", "Player " .. PlayerName .. " has engaged and destroyed a target.", Score )

    return self
  end

  --- Set a score when all the targets in scope of the A2A attack, have been destroyed.
  -- @param #TASK_A2A_ENGAGE self
  -- @param #string PlayerName The name of the player.
  -- @param #number Score The score in points.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2A_ENGAGE
  function TASK_A2A_ENGAGE:SetScoreOnSuccess( PlayerName, Score, TaskUnit )
    self:F( { PlayerName, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Success", "All targets have been successfully engaged!", Score )

    return self
  end

  --- Set a penalty when the A2A attack has failed.
  -- @param #TASK_A2A_ENGAGE self
  -- @param #string PlayerName The name of the player.
  -- @param #number Penalty The penalty in points, must be a negative value!
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_A2A_ENGAGE
  function TASK_A2A_ENGAGE:SetScoreOnFail( PlayerName, Penalty, TaskUnit )
    self:F( { PlayerName, Penalty, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Failed", "The target engagement has failed!", Penalty )

    return self
  end

end

--- **Tasking** - Base class to model tasks for players to transport cargo.
-- 
-- ## Features:
-- 
--   * TASK_CARGO is the **base class** for:
--   
--     * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT}
--     * @{Tasking.Task_Cargo_CSAR#TASK_CARGO_CSAR}
-- 
-- 
-- ===
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- ## Test Missions:
-- 
-- Test missions can be located on the main GITHUB site.
--
-- [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/Tasking/Task_Cargo_Dispatcher)
--
-- ===
--
-- ## Tasking system.
-- 
-- #### If you are not yet aware what the MOOSE tasking system is about, read FIRST the explanation on the @{Tasking.Task} module.
-- 
-- ===
-- 
-- ## Context of cargo tasking.
-- 
-- The Moose framework provides various CARGO classes that allow DCS physical or logical objects to be transported or sling loaded by Carriers.
-- The CARGO_ classes, as part of the MOOSE core, are able to Board, Load, UnBoard and UnLoad cargo between Carrier units.
-- 
-- The TASK_CARGO class is not meant to use within your missions as a mission designer. It is a base class, and other classes are derived from it.
-- 
-- The following TASK_CARGO_ classes are important, as they implement the CONCRETE tasks:
-- 
--   * @{Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT}: Defines a task for a human player to transport a set of cargo between various zones.
--   * @{Tasking.Task_Cargo_CSAR#TASK_CARGO_CSAR}: Defines a task for a human player to Search and Rescue wounded pilots.
-- 
-- However! The menu system and basic usage of the TASK_CARGO classes is explained in the @{#TASK_CARGO} class description.
-- So please browse further below to understand how to use it from a player perspective!
-- 
-- ===
-- 
-- ## Cargo tasking from a player perspective.
-- 
-- A human player can join the battle field in a client airborne slot or a ground vehicle within the CA module (ALT-J).
-- The player needs to accept the task from the task overview list within the mission, using the menus.
-- 
-- Once the task is assigned to the player and accepted by the player, the player will obtain 
-- an extra **Cargo (Radio) Menu** that contains the CARGO objects that need to be transported.
-- 
-- Each @{Tasking.Task_CARGO#TASK_CARGO} object has a certain state:
-- 
--   * **UnLoaded**: The cargo is located within the battlefield. It may still need to be transported.
--   * **Loaded**: The cargo is loaded within a Carrier. This can be your air unit, or another air unit, or even a vehicle.
--   * **Boarding**: The cargo is running or moving towards your Carrier for loading.
--   * **UnBoarding**: The cargo is driving or jumping out of your Carrier and moves to a location in the Deployment Zone.
-- 
-- Cargo must be transported towards different Deployment @{Core.Zone}s.
-- 
-- The Cargo Menu system allows to execute **various actions** to transport the cargo.
-- In the menu, you'll find for each CARGO, that is part of the scope of the task, various actions that can be completed.
-- Depending on the location of your Carrier unit, the menu options will vary.
-- 
-- ### Joining a Cargo Transport Task
-- 
-- Once you've joined a task, using the **Join Planned Task Menu**, 
-- you can Pickup cargo from a pickup location and Deploy cargo in deployment zones, using the **Task Action Menu**.
--  
-- ### Task Action Menu.
-- 
-- When a player has joined a **`CARGO`** task (type), for that player only, 
-- it's **Task Action Menu** will show an additional menu options.
-- 
-- From within this menu, you will be able to route to a cargo location, deploy zone, and load/unload cargo.
-- 
-- ### Pickup cargo by Boarding, Loading and Sling Loading.
-- 
-- There are three different ways how cargo can be picked up:
-- 
--   - **Boarding**: Moveable cargo (like infantry or vehicles), can be boarded, that means, the cargo will move towards your carrier to board.
--     However, it can only execute the boarding actions if it is within the foreseen **Reporting Range**. 
--     Therefore, it is important that you steer your Carrier within the Reporting Range around the cargo, 
--     so that boarding actions can be executed on the cargo. The reporting range is set by the mission designer.
--     Fortunately, the cargo is reporting to you when it is within reporting range.
-- 
--   - **Loading**: Stationary cargo (like crates), which are heavy, can only be loaded or sling loaded, meaning, 
--     your carrier must be close enough to the cargo to be able to load the cargo within the carrier bays.
--     Moose provides you with an additional menu system to load stationary cargo into your carrier bays using the menu.
--     These menu options will become available, when the carrier is within loading range.
--     The Moose cargo will report to the carrier when the range is close enough. The load range is set by the mission designer.
--   
--   - **Sling Loading**: Stationary cargo (like crates), which are heavy, can only be loaded or sling loaded, meaning, 
--     your carrier must be close enough to the cargo to be able to load the cargo within the carrier bays.
--     Sling loading cargo is done using the default DCS menu system. However, Moose cargo will report to the carrier that
--     it is within sling loading range. 
--     
-- In order to be able to pickup cargo, you'll need to know where the cargo is located, right?
-- 
-- Fortunately, if your Carrier is not within the reporting range of the cargo, 
-- **the HQ can help to route you to the locations of cargo**.
-- 
-- ![Task_Types](../Tasking/Task_Cargo_Main_Menu.JPG)
-- 
-- Use the task action menu to receive HQ help for this.
-- 
-- ![Task_Types](../Tasking/Task_Cargo_Action_Menu.JPG)
--  
-- Depending on the location within the battlefield, the task action menu will contain **Route options** that can be selected
-- to start the HQ sending you routing messages.
-- The **route options will vary**, depending on the position of your carrier, and the location of the cargo and the deploy zones.
-- Note that the route options will **only be created** for cargo that is **in scope of your cargo transportation task**,
-- so there may be other cargo objects within the DCS simulation, but if those belong to other cargo transportations tasks,
-- then no routing options will be shown for these cargo.
-- This is done to ensure that **different teams** have a **defined scope** for defined cargo, and that **multiple teams** can join
-- **multiple tasks**, transporting cargo **simultaneously** in a **cooperation**.
-- 
-- In this example, there is a menu option to **Route to pickup cargo...**.
-- Use this menu to route towards cargo locations for pickup into your carrier.
-- 
-- ![Task_Types](../Tasking/Task_Cargo_Types_Menu.JPG)
-- 
-- When you select this menu, you'll see a new menu listing the different cargo types that are out there in the dcs simulator.
-- These cargo types are symbolic names that are assigned by the mission designer, like oil, liquid, engineers, food, workers etc.
-- MOOSE has introduced this concept to allow mission designers to make different cargo types for different purposes.
-- Only the creativity of the mission designer limits now the things that can be done with cargo ...
-- Okay, let's continue ..., and let's select Oil ...
-- 
-- When selected, the HQ will send you routing messages.
-- 
-- ![Task_Types](../Tasking/Task_Cargo_Routing_BR.JPG)  
-- 
-- An example of routing in BR mode.
-- 
-- Note that the coordinate display format in the message can be switched between LL DMS, LL DDM, MGRS and BR.
-- 
-- ![Task_Types](../Tasking/Main_Settings.JPG)
--   
-- Use the @{Core.Settings} menu to change your display format preferences. 
-- 
-- ![Task_Types](../Tasking/Settings_A2G_Coordinate.JPG)
-- 
-- There you can change the display format to another format that suits your need.
-- Because cargo transportation is Air 2 Ground oriented, you need to select the A2G coordinate format display options.
-- Note that the main settings menu contains much more
-- options to control your display formats, like switch to metric and imperial, or change the duration of the display messages.
-- 
-- ![Task_Types](../Tasking/Task_Cargo_Routing_LL.JPG)  
-- 
-- Here I changed the routing display format to LL DMS.
-- 
-- One important thing to know, is that the routing messages will flash at regular time intervals.
-- When using BR coordinate display format, the **distance and angle will change accordingly** from your carrier position and the location of the cargo.
-- 
-- Another important note is the routing towards deploy zones.
-- These routing options will only be shown, when your carrier bays have cargo loaded.
-- So, only when there is something to be deployed from your carrier, the deploy options will be shown.
--     
-- #### Pickup Cargo.
-- 
-- In order to pickup cargo, use the **task action menu** to **route to a specific cargo**.
-- When a cargo route is selected, the HQ will send you routing messages indicating the location of the cargo.
--  
-- Upon arrival at the cargo, and when the cargo is within **reporting range**, the cargo will contact you and **further instructions will be given**.
-- 
--   - When your Carrier is airborne, you will receive instructions to land your Carrier.
--     The action will not be completed until you've landed your Carrier.
--     
--   - For ground carriers, you can just drive to the optimal cargo board or load position.
-- 
-- It takes a bit of skill to land a helicopter near a cargo to be loaded, but that is part of the game, isn't it?
-- Expecially when you are landing in a "hot" zone, so when cargo is under immediate threat of fire.
-- 
-- #### Board Cargo (infantry).
-- 
-- ![](../Tasking/Boarding_Ready.png)  
-- 
-- If your Carrier is within the **Reporting Range of the cargo**, and the cargo is **moveable**, the **cargo can be boarded**!
-- This type of cargo will be most of the time be infantry.
-- 
-- ![](../Tasking/Boarding_Menu.png)  
-- 
-- A **Board cargo...** sub menu has appeared, because your carrier is in boarding range of the cargo (infantry).
-- Select the **Board cargo...** menu.
-- 
-- ![](../Tasking/Boarding_Menu_Engineers.png)  
-- 
-- Any cargo that can be boarded (thus movable cargo), within boarding range of the carrier, will be listed here!
-- In this example, the cargo **Engineers** can be boarded, by selecting the menu option.
-- 
-- ![](../Tasking/Boarding_Started.png)  
-- 
-- After the menu option to board the cargo has been selected, the boarding process is started.
-- A message from the cargo is communicated to the pilot, that boarding is started.
-- 
-- ![](../Tasking/Boarding_Ongoing.png)  
-- 
-- **The pilot must wait at the exact position until all cargo has been boarded!**  
-- 
-- The moveable cargo will run in formation to your carrier, and will board one by one, depending on the near range set by the mission designer.
-- The near range as added because carriers can be large or small, depending on the object size of the carrier.
-- 
-- ![](../Tasking/Boarding_In_Progress.png)  
-- 
-- ![](../Tasking/Boarding_Almost_Done.png)  
-- 
-- Note that multiple units may need to board your Carrier, so it is required to await the full boarding process.
-- 
-- ![](../Tasking/Boarding_Done.png)  
-- 
-- Once the cargo is fully boarded within your Carrier, you will be notified of this.
-- 
-- **Remarks:**
-- 
--   * For airborne Carriers, it is required to land first before the Boarding process can be initiated.
--     If during boarding the Carrier gets airborne, the boarding process will be cancelled.
--   * The carrier must remain stationary when the boarding sequence has started until further notified.
-- 
-- #### Load Cargo.
-- 
-- Cargo can be loaded into vehicles or helicopters or airplanes, as long as the carrier is sufficiently near to the cargo object.
-- 
-- ![](../Tasking/Loading_Ready.png)  
-- 
-- If your Carrier is within the **Loading Range of the cargo**, thus, sufficiently near to the cargo, and the cargo is **stationary**, the **cargo can be loaded**, but not boarded!
-- 
-- ![](../Tasking/Loading_Menu.png)  
-- 
-- Select the task action menu and now a **Load cargo...** sub menu will be listed.
-- Select the **Load cargo...** sub menu, and a further detailed menu will be shown.
-- 
-- ![](../Tasking/Loading_Menu_Crate.png)  
-- 
-- For each non-moveable cargo object (crates etc), **within loading range of the carrier**, the cargo will be listed and can be loaded into the carrier!
-- 
-- ![](../Tasking/Loading_Cargo_Loaded.png)  
-- 
-- Once the cargo is loaded within your Carrier, you will be notified of this.
-- 
-- **Remarks:**
--  
--   * For airborne Carriers, it is required to **land first right near the cargo**, before the loading process can be initiated.
-- As stated, this requires some pilot skills :-)
-- 
-- #### Sling Load Cargo (helicopters only).
-- 
-- If your Carrier is within the **Loading Range of the cargo**, and the cargo is **stationary**, the **cargo can also be sling loaded**!
-- Note that this is only possible for helicopters.
-- 
-- To sling load cargo, there is no task action menu required. Just follow the normal sling loading procedure and the cargo will report.
-- Use the normal DCS sling loading menu system to hook the cargo you the cable attached on your helicopter.
-- 
-- Again note that you may land firstly right next to the cargo, before the loading process can be initiated.
-- As stated, this requires some pilot skills :-)
-- 
-- 
-- ### Deploy cargo by Unboarding, Unloading and Sling Deploying.
-- 
-- #### **Deploying the relevant cargo within deploy zones, will make you achieve cargo transportation tasks!!!**
-- 
-- There are two different ways how cargo can be deployed:
-- 
--   - **Unboarding**: Moveable cargo (like infantry or vehicles), can be unboarded, that means, 
--     the cargo will step out of the carrier and will run to a group location.
--     Moose provides you with an additional menu system to unload stationary cargo from the carrier bays,
--     using the menu. These menu options will become available, when the carrier is within the deploy zone.
-- 
--   - **Unloading**: Stationary cargo (like crates), which are heavy, can only be unloaded or sling loaded. 
--     Moose provides you with an additional menu system to unload stationary cargo from the carrier bays,
--     using the menu. These menu options will become available, when the carrier is within the deploy zone.
--   
--   - **Sling Deploying**: Stationary cargo (like crates), which are heavy, can also be sling deployed. 
--     Once the cargo is within the deploy zone, the cargo can be deployed from the sling onto the ground.
--     
-- In order to be able to deploy cargo, you'll need to know where the deploy zone is located, right?
-- Fortunately, the HQ can help to route you to the locations of deploy zone.
-- Use the task action menu to receive HQ help for this.
-- 
-- ![](../Tasking/Routing_Deploy_Zone_Menu.png)
--  
-- Depending on the location within the battlefield, the task action menu will contain **Route options** that can be selected
-- to start the HQ sending you routing messages. Also, if the carrier cargo bays contain cargo, 
-- then beside **Route options** there will also be **Deploy options** listed.
-- These **Deploy options** are meant to route you to the deploy zone locations.
-- 
-- ![](../Tasking/Routing_Deploy_Zone_Menu_Workplace.png)
-- 
-- Depending on the task that you have selected, the deploy zones will be listed.
-- **There may be multiple deploy zones within the mission, but only the deploy zones relevant for your task will be available in the menu!**
--  
-- ![](../Tasking/Routing_Deploy_Zone_Message.png)
-- 
-- When a routing option is selected, you are sent routing messages in a selected coordinate format.
-- Possible routing coordinate formats are: Bearing Range (BR), Lattitude Longitude (LL) or Military Grid System (MGRS).
-- Note that for LL, there are two sub formats. (See pickup).
-- 
-- ![](../Tasking/Routing_Deploy_Zone_Arrived.png)
-- 
-- When you are within the range of the deploy zone (can be also a polygon!), a message is communicated by HQ that you have arrived within the zone!
-- 
-- The routing messages are formulated in the coordinate format that is currently active as configured in your settings profile.
-- Use the **Settings Menu** to select the coordinate format that you would like to use for location determination.
-- 
-- #### Unboard Cargo.
-- 
-- If your carrier contains cargo, and the cargo is **moveable**, the **cargo can be unboarded**!
-- You can only unload cargo if there is cargo within your cargo bays within the carrier.
-- 
-- ![](../Tasking/Unboarding_Menu.png)
-- 
-- Select the task action menu and now an **Unboard cargo...** sub menu will be listed!
-- Again, this option will only be listed if there is a non moveable cargo within your cargo bays.
-- 
-- ![](../Tasking/Unboarding_Menu_Engineers.png)
-- 
-- Now you will see a menu option to unload the non-moveable cargo.
-- In this example, you can unload the **Engineers** that was loaded within your carrier cargo bays.
-- Depending on the cargo loaded within your cargo bays, you will see other options here!
-- Select the relevant menu option from the cargo unload menu, and the cargo will unloaded from your carrier.
-- 
-- ![](../Tasking/Unboarding_Started.png)
-- 
-- **The cargo will step out of your carrier and will move towards a grouping point.**
-- When the unboarding process has started, you will be notified by a message to your carrier.
-- 
-- ![](../Tasking/Unboarding_In_Progress.png)
-- 
-- The moveable cargo will unboard one by one, so note that multiple units may need to unboard your Carrier, 
-- so it is required to await the full completion of the unboarding process.
-- 
-- ![](../Tasking/Unboarding_Done.png)
-- 
-- Once the cargo is fully unboarded from your carrier, you will be notified of this.
--
-- **Remarks:**
-- 
--   * For airborne carriers, it is required to land first before the unboarding process can be initiated.
--     If during unboarding the Carrier gets airborne, the unboarding process will be cancelled.
--   * Once the moveable cargo is unboarded, they will start moving towards a specified gathering point.
--   * The moveable cargo will send a message to your carrier with unboarding status updates.
-- 
-- **Deploying a cargo within a deployment zone, may complete a deployment task! So ensure that you deploy the right cargo at the right deployment zone!**
-- 
-- #### Unload Cargo.
-- 
-- If your carrier contains cargo, and the cargo is **stationary**, the **cargo can be unloaded**, but not unboarded!
-- You can only unload cargo if there is cargo within your cargo bays within the carrier.
-- 
-- ![](../Tasking/Unloading_Menu.png)
-- 
-- Select the task action menu and now an **Unload cargo...** sub menu will be listed! 
-- Again, this option will only be listed if there is a non moveable cargo within your cargo bays.
-- 
-- ![](../Tasking/Unloading_Menu_Crate.png)
-- 
-- Now you will see a menu option to unload the non-moveable cargo.
-- In this example, you can unload the **Crate** that was loaded within your carrier cargo bays.
-- Depending on the cargo loaded within your cargo bays, you will see other options here!
-- Select the relevant menu option from the cargo unload menu, and the cargo will unloaded from your carrier.
-- 
-- ![](../Tasking/Unloading_Done.png)
-- 
-- Once the cargo is unloaded fom your Carrier, you may be notified of this, when there is a truck near to the cargo.
-- If there is no truck near to the unload area, no message will be sent to your carrier!
-- 
-- **Remarks:**
-- 
--   * For airborne Carriers, it is required to land first, before the unloading process can be initiated.
--   * A truck must be near the unload area to get messages to your carrier of the unload event!
--   * Unloading is only for non-moveable cargo.
--   * The non-moveable cargo must be within your cargo bays, or no unload option will be available.
--   
-- **Deploying a cargo within a deployment zone, may complete a deployment task! So ensure that you deploy the right cargo at the right deployment zone!**
-- 
-- 
-- #### Sling Deploy Cargo (helicopters only).
-- 
-- If your Carrier is within the **deploy zone**, and the cargo is **stationary**, the **cargo can also be sling deploying**!
-- Note that this is only possible for helicopters.
-- 
-- To sling deploy cargo, there is no task action menu required. Just follow the normal sling deploying procedure.
-- 
-- **Deploying a cargo within a deployment zone, may complete a deployment task! So ensure that you deploy the right cargo at the right deployment zone!**
-- 
-- ## Cargo tasking from a mission designer perspective.
-- 
-- Please consult the documentation how to implement the derived classes of SET_CARGO in:
-- 
--   - @{Tasking.Task_CARGO#TASK_CARGO}: Documents the main methods how to handle the cargo tasking from a mission designer perspective.
--   - @{Tasking.Task_CARGO#TASK_CARGO_TRANSPORT}: Documents the specific methods how to handle the cargo transportation tasking from a mission designer perspective.
--   - @{Tasking.Task_CARGO#TASK_CARGO_CSAR}: Documents the specific methods how to handle the cargo CSAR tasking from a mission designer perspective.
--   
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
--   
-- @module Tasking.Task_CARGO
-- @image MOOSE.JPG

do -- TASK_CARGO

  -- @type TASK_CARGO
  -- @extends Tasking.Task#TASK

  --- Model tasks for players to transport Cargo.
  -- 
  -- This models the process of a flexible transporation tasking system of cargo.
  -- 
  -- # 1) A flexible tasking system.
  -- 
  -- The TASK_CARGO classes provide you with a flexible tasking sytem, 
  -- that allows you to transport cargo of various types between various locations
  -- and various dedicated deployment zones.
  -- 
  -- The cargo in scope of the TASK\_CARGO classes must be explicitly given, and is of type SET\_CARGO.
  -- The SET_CARGO contains a collection of CARGO objects that must be handled by the players in the mission.
  -- 
  -- # 2) Cargo Tasking from a mission designer perspective.
  -- 
  -- A cargo task is governed by a @{Tasking.Mission} object. Tasks are of different types.
  -- The @{#TASK} object is used or derived by more detailed tasking classes that will implement the task execution mechanisms
  -- and goals. 
  -- 
  -- ## 2.1) Derived cargo task classes.
  -- 
  -- The following TASK_CARGO classes are derived from @{#TASK}.
  -- 
  --      TASK
  --        TASK_CARGO
  --          TASK_CARGO_TRANSPORT
  --          TASK_CARGO_CSAR
  -- 
  -- ### 2.1.1) Cargo Tasks  
  -- 
  --   - @{Tasking.Task_CARGO#TASK_CARGO_TRANSPORT} - Models the transportation of cargo to deployment zones. 
  --   - @{Tasking.Task_CARGO#TASK_CARGO_CSAR} - Models the rescue of downed friendly pilots from behind enemy lines.    
  --   
  -- ## 2.2) Handle TASK_CARGO Events ...
  -- 
  -- The TASK_CARGO classes define Cargo transport tasks,
  -- based on the tasking capabilities defined in @{Tasking.Task#TASK}.
  -- 
  -- ### 2.2.1) Boarding events.
  -- 
  -- Specific Cargo event can be captured, that allow to trigger specific actions!
  -- 
  --   * **Boarded**: Triggered when the Cargo has been Boarded into your Carrier.
  --   * **UnBoarded**: Triggered when the cargo has been Unboarded from your Carrier and has arrived at the Deployment Zone.
  -- 
  -- ### 2.2.2) Loading events.
  -- 
  -- Specific Cargo event can be captured, that allow to trigger specific actions!
  -- 
  --   * **Loaded**: Triggered when the Cargo has been Loaded into your Carrier.
  --   * **UnLoaded**: Triggered when the cargo has been Unloaded from your Carrier and has arrived at the Deployment Zone.
  -- 
  -- ### 2.2.2) Standard TASK_CARGO Events
  -- 
  -- The TASK_CARGO is implemented using a @{Core.Fsm#FSM_TASK}, and has the following standard statuses:
  -- 
  --   * **None**: Start of the process.
  --   * **Planned**: The cargo task is planned.
  --   * **Assigned**: The cargo task is assigned to a @{Wrapper.Group#GROUP}.
  --   * **Success**: The cargo task is successfully completed.
  --   * **Failed**: The cargo task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ.
  -- 
  -- 
  -- 
  -- ===
  -- 
  -- @field #TASK_CARGO
  TASK_CARGO = {
    ClassName = "TASK_CARGO",
  }
  
  --- Instantiates a new TASK_CARGO.
  -- @param #TASK_CARGO self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned.
  -- @param #string TaskName The name of the Task.
  -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported.
  -- @param #string TaskType The type of Cargo task.
  -- @param #string TaskBriefing The Cargo Task briefing.
  -- @return #TASK_CARGO self
  function TASK_CARGO:New( Mission, SetGroup, TaskName, SetCargo, TaskType, TaskBriefing )
    local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType, TaskBriefing ) ) -- #TASK_CARGO
    self:F( {Mission, SetGroup, TaskName, SetCargo, TaskType})
  
    self.SetCargo = SetCargo
    self.TaskType = TaskType
    self.SmokeColor = SMOKECOLOR.Red
    
    self.CargoItemCount = {} -- Map of Carriers having a cargo item count to check the cargo loading limits.
    self.CargoLimit = 10
    
    self.DeployZones = {} -- setmetatable( {}, { __mode = "v" } ) -- weak table on value

    self:AddTransition( "*", "CargoDeployed", "*" )
    
    --- CargoDeployed Handler OnBefore for TASK_CARGO
    -- @function [parent=#TASK_CARGO] OnBeforeCargoDeployed
    -- @param #TASK_CARGO self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc.
    -- @param Cargo.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status.
    -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded.
    -- @return #boolean
    
    --- CargoDeployed Handler OnAfter for TASK_CARGO
    -- @function [parent=#TASK_CARGO] OnAfterCargoDeployed
    -- @param #TASK_CARGO self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc.
    -- @param Cargo.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status.
    -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded.
    -- @usage
    -- 
    --   -- Add a Transport task to transport cargo of different types to a Transport Deployment Zone.
    --  TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups )
    --  
    --  local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart()
    --  local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 )
    --  local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 )
    --  local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 )
    --  local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 )
    --  local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 )
    --  
    --  -- Here we add the task. We name the task "Build a Workplace".
    --  -- We provide the CargoSetWorkmaterials, and a briefing as the 2nd and 3rd parameter.
    --  -- The :AddTransportTask() returns a Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT object, which we keep as a reference for further actions.
    --  -- The WorkplaceTask holds the created and returned Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT object.
    --  local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." )
    --  
    --  -- Here we set a TransportDeployZone. We use the WorkplaceTask as the reference, and provide a ZONE object.
    --  TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) )
    --  
    --  Helos = { SPAWN:New( "Helicopters 1" ), SPAWN:New( "Helicopters 2" ), SPAWN:New( "Helicopters 3" ), SPAWN:New( "Helicopters 4" ), SPAWN:New( "Helicopters 5" ) }
    --  EnemyHelos = { SPAWN:New( "Enemy Helicopters 1" ), SPAWN:New( "Enemy Helicopters 2" ), SPAWN:New( "Enemy Helicopters 3" ) }
    --  
    --  -- This is our worker method! So when a cargo is deployed within a deployment zone, this method will be called.
    --  -- By example we are spawning here a random friendly helicopter and a random enemy helicopter.
    --  function WorkplaceTask:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone )
    --    Helos[ math.random(1,#Helos) ]:Spawn()
    --    EnemyHelos[ math.random(1,#EnemyHelos) ]:Spawn()
    --  end
    
    self:AddTransition( "*", "CargoPickedUp", "*" )

    --- CargoPickedUp Handler OnBefore for TASK_CARGO
    -- @function [parent=#TASK_CARGO] OnBeforeCargoPickedUp
    -- @param #TASK_CARGO self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc.
    -- @param Cargo.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status.
    -- @return #boolean
    
    --- CargoPickedUp Handler OnAfter for TASK_CARGO
    -- @function [parent=#TASK_CARGO] OnAfterCargoPickedUp
    -- @param #TASK_CARGO self
    -- @param #string From
    -- @param #string Event
    -- @param #string To
    -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc.
    -- @param Cargo.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status.

    
    local Fsm = self:GetUnitProcess()
    
--    Fsm:SetStartState( "Planned" )
--
--    Fsm:AddProcess   ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "SelectAction", Rejected = "Reject" }  )
    
    Fsm:AddTransition( { "Planned", "Assigned", "Cancelled", "WaitingForCommand", "ArrivedAtPickup", "ArrivedAtDeploy", "Boarded", "UnBoarded", "Loaded", "UnLoaded", "Landed", "Boarding" }, "SelectAction", "*" )

    Fsm:AddTransition( "*", "RouteToPickup", "RoutingToPickup" )
    Fsm:AddProcess   ( "RoutingToPickup", "RouteToPickupPoint", ACT_ROUTE_POINT:New(), { Arrived = "ArriveAtPickup", Cancelled = "CancelRouteToPickup" } )
    Fsm:AddTransition( "Arrived", "ArriveAtPickup", "ArrivedAtPickup" )
    Fsm:AddTransition( "Cancelled", "CancelRouteToPickup", "Cancelled" )

    Fsm:AddTransition( "*", "RouteToDeploy", "RoutingToDeploy" )
    Fsm:AddProcess   ( "RoutingToDeploy", "RouteToDeployZone", ACT_ROUTE_ZONE:New(), { Arrived = "ArriveAtDeploy", Cancelled = "CancelRouteToDeploy" } )
    Fsm:AddTransition( "Arrived", "ArriveAtDeploy", "ArrivedAtDeploy" )
    Fsm:AddTransition( "Cancelled", "CancelRouteToDeploy", "Cancelled" )
    
    Fsm:AddTransition( { "ArrivedAtPickup", "ArrivedAtDeploy", "Landing" }, "Land", "Landing" )
    Fsm:AddTransition( "Landing", "Landed", "Landed" )
    
    Fsm:AddTransition( "*", "PrepareBoarding", "AwaitBoarding" )
    Fsm:AddTransition( "AwaitBoarding", "Board", "Boarding" )
    Fsm:AddTransition( "Boarding", "Boarded", "Boarded" )
    
    Fsm:AddTransition( "*", "Load", "Loaded" )

    Fsm:AddTransition( "*", "PrepareUnBoarding", "AwaitUnBoarding" )
    Fsm:AddTransition( "AwaitUnBoarding", "UnBoard", "UnBoarding" )
    Fsm:AddTransition( "UnBoarding", "UnBoarded", "UnBoarded" )

    Fsm:AddTransition( "*", "Unload", "Unloaded" )
    
    Fsm:AddTransition( "*", "Planned", "Planned" )
    
    
    Fsm:AddTransition( "Deployed", "Success", "Success" )
    Fsm:AddTransition( "Rejected", "Reject", "Aborted" )
    Fsm:AddTransition( "Failed", "Fail", "Failed" )

    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param #TASK_CARGO Task
    function Fsm:OnAfterAssigned( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      
      self:SelectAction()
    end
    
    

    --- 
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param #TASK_CARGO Task
    function Fsm:onafterSelectAction( TaskUnit, Task )
      
      local TaskUnitName = TaskUnit:GetName()
      local MenuTime = Task:InitTaskControlMenu( TaskUnit )
      local MenuControl = Task:GetTaskControlMenu( TaskUnit )
      
      Task.SetCargo:ForEachCargo(
        
        -- @param Cargo.Cargo#CARGO Cargo
        function( Cargo ) 
        
          if Cargo:IsAlive() then
        
--            if Task:is( "RoutingToPickup" ) then
--              MENU_GROUP_COMMAND:New(
--                TaskUnit:GetGroup(),
--                "Cancel Route " .. Cargo.Name,
--                MenuControl,
--                self.MenuRouteToPickupCancel,
--                self,
--                Cargo
--              ):SetTime(MenuTime)
--            end

            --self:F( { CargoUnloaded = Cargo:IsUnLoaded(), CargoLoaded = Cargo:IsLoaded(), CargoItemCount = CargoItemCount } )
        
            local TaskGroup = TaskUnit:GetGroup()
            
            if Cargo:IsUnLoaded() then
              local CargoBayFreeWeight = TaskUnit:GetCargoBayFreeWeight()
              local CargoWeight = Cargo:GetWeight()
              
              self:F({CargoBayFreeWeight=CargoBayFreeWeight})
  
              -- Only when there is space within the bay to load the next cargo item!
              if CargoBayFreeWeight > CargoWeight then 
                if Cargo:IsInReportRadius( TaskUnit:GetPointVec2() ) then
                  local NotInDeployZones = true
                  for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do
                    if Cargo:IsInZone( DeployZone ) then
                      NotInDeployZones = false
                    end
                  end
                  if NotInDeployZones then
                    if not TaskUnit:InAir() then
                      if Cargo:CanBoard() == true then
                        if Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then
                          Cargo:Report( "Ready for boarding.", "board", TaskUnit:GetGroup() )
                          local BoardMenu = MENU_GROUP:New( TaskGroup, "Board cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" )
                          MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, BoardMenu, self.MenuBoardCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent()
                        else
                          Cargo:Report( "Board at " .. Cargo:GetCoordinate():ToString( TaskUnit:GetGroup() .. "." ), "reporting", TaskUnit:GetGroup() )
                        end
                      else
                        if Cargo:CanLoad() == true then
                          if Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then
                            Cargo:Report( "Ready for loading.", "load", TaskUnit:GetGroup() )
                            local LoadMenu = MENU_GROUP:New( TaskGroup, "Load cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" )
                            MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, LoadMenu, self.MenuLoadCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent()
                          else
                            Cargo:Report( "Load at " .. Cargo:GetCoordinate():ToString( TaskUnit:GetGroup() ) .. " within " .. Cargo.NearRadius .. ".", "reporting", TaskUnit:GetGroup() )
                          end
                        else
                          --local Cargo = Cargo -- Cargo.CargoSlingload#CARGO_SLINGLOAD
                          if Cargo:CanSlingload() == true then
                            if Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then
                              Cargo:Report( "Ready for sling loading.", "slingload", TaskUnit:GetGroup() )
                              local SlingloadMenu = MENU_GROUP:New( TaskGroup, "Slingload cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" )
                              MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, SlingloadMenu, self.MenuLoadCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent()
                            else
                              Cargo:Report( "Slingload at " .. Cargo:GetCoordinate():ToString( TaskUnit:GetGroup() ) .. ".", "reporting", TaskUnit:GetGroup() )
                            end
                          end
                        end
                      end
                    else
                      Cargo:ReportResetAll( TaskUnit:GetGroup() )
                    end
                  end
                else
                  if not Cargo:IsDeployed() == true then
                    local RouteToPickupMenu = MENU_GROUP:New( TaskGroup, "Route to pickup cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" )
                    --MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, RouteToPickupMenu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent()
                    Cargo:ReportResetAll( TaskUnit:GetGroup() )
                    if Cargo:CanBoard() == true then
                      if not Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then
                        local BoardMenu = MENU_GROUP:New( TaskGroup, "Board cargo", RouteToPickupMenu ):SetTime( MenuTime ):SetTag( "Cargo" )
                        MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, BoardMenu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent()
                      end
                    else
                      if Cargo:CanLoad() == true then
                        if not Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then
                          local LoadMenu = MENU_GROUP:New( TaskGroup, "Load cargo", RouteToPickupMenu ):SetTime( MenuTime ):SetTag( "Cargo" )
                          MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, LoadMenu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent()
                        end
                      else
                        --local Cargo = Cargo -- Cargo.CargoSlingload#CARGO_SLINGLOAD
                        if Cargo:CanSlingload() == true then
                          if not Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then
                            local SlingloadMenu = MENU_GROUP:New( TaskGroup, "Slingload cargo", RouteToPickupMenu ):SetTime( MenuTime ):SetTag( "Cargo" )
                            MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, SlingloadMenu, self.MenuRouteToPickup, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent()
                          end
                        end
                      end
                    end
                  end
                end
              end
              
              -- Cargo in deployzones are flagged as deployed.
              for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do
                if Cargo:IsInZone( DeployZone ) then
                  Task:I( { CargoIsDeployed = Task.CargoDeployed and "true" or "false" } )
                  if Cargo:IsDeployed() == false then
                    Cargo:SetDeployed( true )
                    -- Now we call a callback method to handle the CargoDeployed event.
                    Task:I( { CargoIsAlive = Cargo:IsAlive() and "true" or "false" } )
                    if Cargo:IsAlive() then
                      Task:CargoDeployed( TaskUnit, Cargo, DeployZone )
                    end
                  end
                end
              end
              
            end
            
            if Cargo:IsLoaded() == true and Cargo:IsLoadedInCarrier( TaskUnit ) == true then
              if not TaskUnit:InAir() then
                if Cargo:CanUnboard() == true then
                  local UnboardMenu = MENU_GROUP:New( TaskGroup, "Unboard cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" )
                  MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, UnboardMenu, self.MenuUnboardCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent()
                else
                  if Cargo:CanUnload() == true then
                    local UnloadMenu = MENU_GROUP:New( TaskGroup, "Unload cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" )
                    MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), Cargo.Name, UnloadMenu, self.MenuUnloadCargo, self, Cargo ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent()
                  end
                end
              end
            end

            -- Deployzones are optional zones that can be selected to request routing information.
            for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do
              if not Cargo:IsInZone( DeployZone ) then
                local RouteToDeployMenu = MENU_GROUP:New( TaskGroup, "Route to deploy cargo", MenuControl ):SetTime( MenuTime ):SetTag( "Cargo" )
                MENU_GROUP_COMMAND:New( TaskUnit:GetGroup(), "Zone " .. DeployZoneName, RouteToDeployMenu, self.MenuRouteToDeploy, self, DeployZone ):SetTime(MenuTime):SetTag("Cargo"):SetRemoveParent()
              end
            end
          end
        
        end
      )

      Task:RefreshTaskControlMenu( TaskUnit, MenuTime, "Cargo" )
      
      self:__SelectAction( -1 )
      
    end
    
    
    --- 
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param #TASK_CARGO Task
    function Fsm:OnLeaveWaitingForCommand( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      
      --local MenuControl = Task:GetTaskControlMenu( TaskUnit )
      
      --MenuControl:Remove()
    end
    
    function Fsm:MenuBoardCargo( Cargo )
      self:__PrepareBoarding( 1.0, Cargo )
    end
    
    function Fsm:MenuLoadCargo( Cargo )
      self:__Load( 1.0, Cargo )
    end
    
    function Fsm:MenuUnboardCargo( Cargo, DeployZone )
      self:__PrepareUnBoarding( 1.0, Cargo, DeployZone )
    end
    
    function Fsm:MenuUnloadCargo( Cargo, DeployZone )
      self:__Unload( 1.0, Cargo, DeployZone )
    end

    function Fsm:MenuRouteToPickup( Cargo )
      self:__RouteToPickup( 1.0, Cargo )
    end

    function Fsm:MenuRouteToDeploy( DeployZone )
      self:__RouteToDeploy( 1.0, DeployZone )
    end
    
    
    
    ---
    --#TASK_CAROG_TRANSPORT self
    --#Wrapper.Unit#UNIT

    
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    -- @param From
    -- @param Event
    -- @param To
    -- @param Cargo.Cargo#CARGO Cargo
    function Fsm:onafterRouteToPickup( TaskUnit, Task, From, Event, To, Cargo )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )

      if Cargo:IsAlive() then
        self.Cargo = Cargo -- Cargo.Cargo#CARGO
        Task:SetCargoPickup( self.Cargo, TaskUnit )
        self:__RouteToPickupPoint( -0.1 )
      end
      
    end



    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    function Fsm:onafterArriveAtPickup( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      if self.Cargo:IsAlive() then
        if TaskUnit:IsAir() then
          Task:GetMission():GetCommandCenter():MessageToGroup( "Land", TaskUnit:GetGroup() )
          self:__Land( -0.1, "Pickup" )
        else
          self:__SelectAction( -0.1 )
        end
      end
    end


    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    function Fsm:onafterCancelRouteToPickup( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      
      Task:GetMission():GetCommandCenter():MessageToGroup( "Cancelled routing to Cargo " .. self.Cargo:GetName(), TaskUnit:GetGroup() )
      self:__SelectAction( -0.1 )
    end


    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    function Fsm:onafterRouteToDeploy( TaskUnit, Task, From, Event, To, DeployZone )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )

      self:F( DeployZone )
      self.DeployZone = DeployZone
      Task:SetDeployZone( self.DeployZone, TaskUnit )
      self:__RouteToDeployZone( -0.1 )
    end


    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    function Fsm:onafterArriveAtDeploy( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      
      if TaskUnit:IsAir() then
        Task:GetMission():GetCommandCenter():MessageToGroup( "Land", TaskUnit:GetGroup() )
        self:__Land( -0.1, "Deploy" )
      else
        self:__SelectAction( -0.1 )
      end
    end


    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    function Fsm:onafterCancelRouteToDeploy( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      
      Task:GetMission():GetCommandCenter():MessageToGroup( "Cancelled routing to deploy zone " .. self.DeployZone:GetName(), TaskUnit:GetGroup() )
      self:__SelectAction( -0.1 )
    end



    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    function Fsm:onafterLand( TaskUnit, Task, From, Event, To, Action )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      
      if Action == "Pickup" then
        if self.Cargo:IsAlive() then
          if self.Cargo:IsInReportRadius( TaskUnit:GetPointVec2() ) then
            if TaskUnit:InAir() then
              self:__Land( -10, Action )
            else
              Task:GetMission():GetCommandCenter():MessageToGroup( "Landed at pickup location...", TaskUnit:GetGroup() )
              self:__Landed( -0.1, Action )
            end
          else
            self:__RouteToPickup( -0.1, self.Cargo )
          end
        end
      else
        if TaskUnit:IsAlive() then
          if TaskUnit:IsInZone( self.DeployZone ) then
            if TaskUnit:InAir() then
              self:__Land( -10, Action )
            else
              Task:GetMission():GetCommandCenter():MessageToGroup( "Landed at deploy zone " .. self.DeployZone:GetName(), TaskUnit:GetGroup() )
              self:__Landed( -0.1, Action )
            end
          else
            self:__RouteToDeploy( -0.1, self.Cargo )
          end
        end
      end
    end

    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    function Fsm:onafterLanded( TaskUnit, Task, From, Event, To, Action )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      
      if Action == "Pickup" then
        if self.Cargo:IsAlive() then
          if self.Cargo:IsInReportRadius( TaskUnit:GetPointVec2() ) then
            if TaskUnit:InAir() then
              self:__Land( -0.1, Action )
            else
              self:__SelectAction( -0.1 )
            end
          else
            self:__RouteToPickup( -0.1, self.Cargo )
          end
        end
      else
        if TaskUnit:IsAlive() then
          if TaskUnit:IsInZone( self.DeployZone ) then
            if TaskUnit:InAir() then
              self:__Land( -10, Action )
            else
              self:__SelectAction( -0.1 )
            end
          else
            self:__RouteToDeploy( -0.1, self.Cargo )
          end
        end
      end
    end
    
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    function Fsm:onafterPrepareBoarding( TaskUnit, Task, From, Event, To, Cargo )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      
      if Cargo and Cargo:IsAlive() then
        self:__Board( -0.1, Cargo )
      end
    end

    
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    function Fsm:onafterBoard( TaskUnit, Task, From, Event, To, Cargo  )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )

      function Cargo:OnEnterLoaded( From, Event, To, TaskUnit, TaskProcess )
        self:F({From, Event, To, TaskUnit, TaskProcess })
        TaskProcess:__Boarded( 0.1, self )
      end

      if Cargo:IsAlive() then
        if Cargo:IsInLoadRadius( TaskUnit:GetPointVec2() ) then
          if TaskUnit:InAir() then
            --- ABORT the boarding. Split group if any and go back to select action.
          else
            Cargo:MessageToGroup( "Boarding ...", TaskUnit:GetGroup() )
            if not Cargo:IsBoarding() then
              Cargo:Board( TaskUnit, nil, self )
            end
          end
        else
          --self:__ArriveAtCargo( -0.1 )
        end
      end
    end


    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    function Fsm:onafterBoarded( TaskUnit, Task, From, Event, To, Cargo  )
      
      local TaskUnitName = TaskUnit:GetName()
      self:F( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } )

      Cargo:MessageToGroup( "Boarded cargo " .. Cargo:GetName(), TaskUnit:GetGroup() )
      
      self:__Load( -0.1, Cargo )
      
    end
    

    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    function Fsm:onafterLoad( TaskUnit, Task, From, Event, To, Cargo )
      
      local TaskUnitName = TaskUnit:GetName()
      self:F( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } )
      
      if not Cargo:IsLoaded() then
        Cargo:Load( TaskUnit )
      end

      Cargo:MessageToGroup( "Loaded cargo " .. Cargo:GetName(), TaskUnit:GetGroup() )
      TaskUnit:AddCargo( Cargo )

      Task:CargoPickedUp( TaskUnit, Cargo )

      self:SelectAction( -1 )
      
    end
    

    --- 
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    -- @param From
    -- @param Event
    -- @param To
    -- @param Cargo
    -- @param Core.Zone#ZONE_BASE DeployZone
    function Fsm:onafterPrepareUnBoarding( TaskUnit, Task, From, Event, To, Cargo )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID(), From, Event, To, Cargo  } )

      self.Cargo = Cargo
      self.DeployZone = nil

      -- Check if the Cargo is at a deployzone... If it is, provide it as a parameter!      
      if Cargo:IsAlive() then
        for DeployZoneName, DeployZone in pairs( Task.DeployZones ) do
          if Cargo:IsInZone( DeployZone ) then
            self.DeployZone = DeployZone  -- Core.Zone#ZONE_BASE
            break      
          end
        end
        self:__UnBoard( -0.1, Cargo, self.DeployZone )
      end
    end
    
    --- 
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    -- @param From
    -- @param Event
    -- @param To
    -- @param Cargo
    -- @param Core.Zone#ZONE_BASE DeployZone
    function Fsm:onafterUnBoard( TaskUnit, Task, From, Event, To, Cargo, DeployZone )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID(), From, Event, To, Cargo, DeployZone } )

      function self.Cargo:OnEnterUnLoaded( From, Event, To, DeployZone, TaskProcess )
        self:F({From, Event, To, DeployZone, TaskProcess })
        TaskProcess:__UnBoarded( -0.1 )
      end

      if self.Cargo:IsAlive() then
        self.Cargo:MessageToGroup( "UnBoarding ...", TaskUnit:GetGroup() )
        if DeployZone then
          self.Cargo:UnBoard( DeployZone:GetCoordinate():GetRandomCoordinateInRadius( 25, 10 ), 400, self )
        else
          self.Cargo:UnBoard( TaskUnit:GetCoordinate():GetRandomCoordinateInRadius( 25, 10 ), 400, self )
        end          
      end
    end


    --- 
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    function Fsm:onafterUnBoarded( TaskUnit, Task )

      local TaskUnitName = TaskUnit:GetName()
      self:F( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } )
      
      self.Cargo:MessageToGroup( "UnBoarded cargo " .. self.Cargo:GetName(), TaskUnit:GetGroup() )
      
      self:Unload( self.Cargo )
    end

    --- 
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_Cargo#TASK_CARGO Task
    function Fsm:onafterUnload( TaskUnit, Task, From, Event, To, Cargo, DeployZone )

      local TaskUnitName = TaskUnit:GetName()
      self:F( { TaskUnit = TaskUnitName, Task = Task and Task:GetClassNameAndID() } )
      
      if not Cargo:IsUnLoaded() then
        if DeployZone then
          Cargo:UnLoad( DeployZone:GetCoordinate():GetRandomCoordinateInRadius( 25, 10 ), 400, self )
        else
          Cargo:UnLoad( TaskUnit:GetCoordinate():GetRandomCoordinateInRadius( 25, 10 ), 400, self )
        end          
      end
      TaskUnit:RemoveCargo( Cargo )
      
      Cargo:MessageToGroup( "Unloaded cargo " .. Cargo:GetName(), TaskUnit:GetGroup() )

      self:Planned()
      self:__SelectAction( 1 )
    end
    
    return self
 
  end


    --- Set a limit on the amount of cargo items that can be loaded into the Carriers.
    -- @param #TASK_CARGO self
    -- @param CargoLimit Specifies a number of cargo items that can be loaded in the helicopter.
    -- @return #TASK_CARGO
    function TASK_CARGO:SetCargoLimit( CargoLimit )
      self.CargoLimit = CargoLimit
      return self
    end
    

    --@param Color Might be SMOKECOLOR.Blue, SMOKECOLOR.Red SMOKECOLOR.Orange, SMOKECOLOR.White or SMOKECOLOR.Green
    function TASK_CARGO:SetSmokeColor(SmokeColor)
       -- Makes sure Coloe is set
       if SmokeColor == nil then
          self.SmokeColor = SMOKECOLOR.Red -- Make sure a default color is exist
          
       elseif type(SmokeColor) == "number" then
       self:F2(SmokeColor)
        if SmokeColor > 0 and SmokeColor <=5 then -- Make sure number is within ragne, assuming first enum is one
          self.SmokeColor = SMOKECOLOR.SmokeColor
        end
       end
    end
     
    --@return SmokeColor
    function TASK_CARGO:GetSmokeColor()
      return self.SmokeColor
    end
  
  -- @param #TASK_CARGO self
  function TASK_CARGO:GetPlannedMenuText()
    return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.TargetSetUnit:GetUnitTypesText() .. " )"
  end

  -- @param #TASK_CARGO self
  -- @return Core.Set#SET_CARGO The Cargo Set.
  function TASK_CARGO:GetCargoSet()
  
    return self.SetCargo
  end
  
  -- @param #TASK_CARGO self
  -- @return #list<Core.Zone#ZONE_BASE> The Deployment Zones.
  function TASK_CARGO:GetDeployZones()
  
    return self.DeployZones
  end

  -- @param #TASK_CARGO self
  -- @param AI.AI_Cargo#AI_CARGO Cargo The cargo.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_CARGO
  function TASK_CARGO:SetCargoPickup( Cargo, TaskUnit )
  
    self:F({Cargo, TaskUnit})
    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local MenuTime = self:InitTaskControlMenu( TaskUnit )
    local MenuControl = self:GetTaskControlMenu( TaskUnit )
  
    local ActRouteCargo = ProcessUnit:GetProcess( "RoutingToPickup", "RouteToPickupPoint" ) -- Actions.Act_Route#ACT_ROUTE_POINT
    ActRouteCargo:Reset()
    ActRouteCargo:SetCoordinate( Cargo:GetCoordinate() )
    ActRouteCargo:SetRange( Cargo:GetLoadRadius() )
    ActRouteCargo:SetMenuCancel( TaskUnit:GetGroup(), "Cancel Routing to Cargo " .. Cargo:GetName(), MenuControl, MenuTime, "Cargo" )
    ActRouteCargo:Start()

    return self
  end
  

  -- @param #TASK_CARGO self
  -- @param Core.Zone#ZONE DeployZone
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_CARGO
  function TASK_CARGO:SetDeployZone( DeployZone, TaskUnit )
  
    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local MenuTime = self:InitTaskControlMenu( TaskUnit )
    local MenuControl = self:GetTaskControlMenu( TaskUnit )
  
    local ActRouteDeployZone = ProcessUnit:GetProcess( "RoutingToDeploy", "RouteToDeployZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE
    ActRouteDeployZone:Reset()
    ActRouteDeployZone:SetZone( DeployZone )
    ActRouteDeployZone:SetMenuCancel( TaskUnit:GetGroup(), "Cancel Routing to Deploy Zone" .. DeployZone:GetName(), MenuControl, MenuTime, "Cargo" )
    ActRouteDeployZone:Start()
    
    return self
  end
   
  
  -- @param #TASK_CARGO self
  -- @param Core.Zone#ZONE DeployZone
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_CARGO
  function TASK_CARGO:AddDeployZone( DeployZone, TaskUnit )
  
    self.DeployZones[DeployZone:GetName()] = DeployZone

    return self
  end
  
  -- @param #TASK_CARGO self
  -- @param Core.Zone#ZONE DeployZone
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_CARGO
  function TASK_CARGO:RemoveDeployZone( DeployZone, TaskUnit )
  
    self.DeployZones[DeployZone:GetName()] = nil

    return self
  end
  
  -- @param #TASK_CARGO self
  -- @param #list<Core.Zone#ZONE> DeployZones
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_CARGO
  function TASK_CARGO:SetDeployZones( DeployZones, TaskUnit )
  
    for DeployZoneID, DeployZone in pairs( DeployZones or {} ) do
      self.DeployZones[DeployZone:GetName()] = DeployZone
    end

    return self
  end
  
  

  -- @param #TASK_CARGO self
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map.
  function TASK_CARGO:GetTargetZone( TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteTarget = ProcessUnit:GetProcess( "Engaging", "RouteToTargetZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE
    return ActRouteTarget:GetZone()
  end

  --- Set a score when progress is made.
  -- @param #TASK_CARGO self
  -- @param #string Text The text to display to the player, when there is progress on the task goals.
  -- @param #number Score The score in points.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_CARGO
  function TASK_CARGO:SetScoreOnProgress( Text, Score, TaskUnit )
    self:F( { Text, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScoreProcess( "Engaging", "Account", "Account", Text, Score )
    
    return self
  end

  --- Set a score when success is achieved.
  -- @param #TASK_CARGO self
  -- @param #string Text The text to display to the player, when the task goals have been achieved.
  -- @param #number Score The score in points.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_CARGO
  function TASK_CARGO:SetScoreOnSuccess( Text, Score, TaskUnit )
    self:F( { Text, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Success", Text, Score )
    
    return self
  end

  --- Set a penalty when the task goals have failed..
  -- @param #TASK_CARGO self
  -- @param #string Text The text to display to the player, when the task goals has failed.
  -- @param #number Penalty The penalty in points.
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return #TASK_CARGO
  function TASK_CARGO:SetScoreOnFail( Text, Penalty, TaskUnit )
    self:F( { Text, Score, TaskUnit } )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    ProcessUnit:AddScore( "Failed", Text, Penalty )
    
    return self
  end
  
  function TASK_CARGO:SetGoalTotal()
  
    self.GoalTotal = self.SetCargo:Count()
  end

  function TASK_CARGO:GetGoalTotal()
  
    return self.GoalTotal
  end
  
  -- @param #TASK_CARGO self
  function TASK_CARGO:UpdateTaskInfo()
  
    if self:IsStatePlanned() or self:IsStateAssigned() then
      self.TaskInfo:AddTaskName( 0, "MSOD" )
      self.TaskInfo:AddCargoSet( self.SetCargo, 10, "SOD", true )
      local Coordinates = {}
      for CargoName, Cargo in pairs( self.SetCargo:GetSet() ) do
        local Cargo = Cargo -- Cargo.Cargo#CARGO
        if not Cargo:IsLoaded() then
          Coordinates[#Coordinates+1] = Cargo:GetCoordinate()
        end
      end
      self.TaskInfo:AddCoordinates( Coordinates, 1, "M" )
    end
  end

  function TASK_CARGO:ReportOrder( ReportGroup ) 
    
    return 0
  end

  --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection.
  -- @param #TASK_CARGO self
  -- @param #number AutoAssignMethod The method to be applied to the task.
  -- @param Wrapper.Group#GROUP TaskGroup The player group.
  function TASK_CARGO:GetAutoAssignPriority( AutoAssignMethod, TaskGroup )
  
    if     AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then
      return math.random( 1, 9 )
    elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then
      return 0
    elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then
      return 1
    end

    return 0
  end
  
  
  
end 


--- **Tasking** - Models tasks for players to transport cargo.
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- **Specific features:**
-- 
--   * Creates a task to transport #Cargo.Cargo to and between deployment zones.
--   * Derived from the TASK_CARGO class, which is derived from the TASK class.
--   * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled.
--   * Co-operation tasking, so a player joins a group of players executing the same task.
-- 
-- 
-- **A complete task menu system to allow players to:**
--   
--   * Join the task, abort the task.
--   * Mark the task location on the map.
--   * Provide details of the target.
--   * Route to the cargo.
--   * Route to the deploy zones.
--   * Load/Unload cargo.
--   * Board/Unboard cargo.
--   * Slingload cargo.
--   * Display the task briefing.
--   
--   
-- **A complete mission menu system to allow players to:**
--   
--   * Join a task, abort the task.
--   * Display task reports.
--   * Display mission statistics.
--   * Mark the task locations on the map.
--   * Provide details of the targets.
--   * Display the mission briefing.
--   * Provide status updates as retrieved from the command center.
--   * Automatically assign a random task as part of a mission.
--   * Manually assign a specific task as part of a mission.
--   
--   
--  **A settings system, using the settings menu:**
--  
--   * Tweak the duration of the display of messages.
--   * Switch between metric and imperial measurement system.
--   * Switch between coordinate formats used in messages: BR, BRA, LL DMS, LL DDM, MGRS.
--   * Different settings modes for A2G and A2A operations.
--   * Various other options.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- Please read through the #Tasking.Task_Cargo process to understand the mechanisms of tasking and cargo tasking and handling.
-- 
-- Enjoy!
-- FC
-- 
-- ===
-- 
-- @module Tasking.Task_Cargo_Transport
-- @image Task_Cargo_Transport.JPG


do -- TASK_CARGO_TRANSPORT

  -- @type TASK_CARGO_TRANSPORT
  -- @extends Tasking.Task_CARGO#TASK_CARGO

  --- Orchestrates the task for players to transport cargo to or between deployment zones.
  -- 
  -- Transport tasks are suited to govern the process of transporting cargo to specific deployment zones.
  -- Typically, this task is executed by helicopter pilots, but it can also be executed by ground forces!
  -- 
  -- ===
  -- 
  -- A transport task can be created manually.
  --  
  -- # 1) Create a transport task manually (code it).
  -- 
  -- Although it is recommended to use the dispatcher, you can create a transport task yourself as a mission designer.
  -- It is easy, as it works just like any other task setup.
  -- 
  -- ## 1.1) Create a command center.
  -- 
  -- First you need to create a command center using the Tasking.CommandCenter#COMMANDCENTER.New constructor.
  -- 
  --     local CommandCenter = COMMANDCENTER
  --        :New( HQ, "Lima" ) -- Create the CommandCenter.
  --     
  -- ## 1.2) Create a mission.
  -- 
  -- Tasks work in a mission, which groups these tasks to achieve a joint mission goal.
  -- A command center can govern multiple missions.
  -- Create a new mission, using the Tasking.Mission#MISSION.New constructor.
  -- 
  --     -- Declare the Mission for the Command Center.
  --     local Mission = MISSION
  --       :New( CommandCenter, 
  --             "Overlord", 
  --             "High", 
  --             "Transport the cargo to the deploy zones.", 
  --             coalition.side.RED 
  --           ) 
  -- 
  -- ## 1.3) Create the transport cargo task.
  -- 
  -- So, now that we have a command center and a mission, we now create the transport task.
  -- We create the transport task using the #TASK_CARGO_TRANSPORT.New constructor.
  -- 
  -- Because a transport task will not generate the cargo itself, you'll need to create it first.
  -- The cargo in this case will be the downed pilot!
  -- 
  --     -- Here we define the "cargo set", which is a collection of cargo objects.
  --     -- The cargo set will be the input for the cargo transportation task.
  --     -- So a transportation object is handling a cargo set, which is automatically refreshed when new cargo is added/deleted.
  --     local CargoSet = SET_CARGO:New():FilterTypes( "Cargo" ):FilterStart()
  --    
  --     -- Now we add cargo into the battle scene.
  --     local PilotGroup = GROUP:FindByName( "Engineers" )
  --      
  --     -- CARGO_GROUP can be used to setup cargo with a GROUP object underneath.
  --     -- We name this group Engineers.
  --     -- Note that the name of the cargo is "Engineers".
  --     -- The cargoset "CargoSet" will embed all defined cargo of type "Pilots" (prefix) into its set.
  --     local CargoGroup = CARGO_GROUP:New( PilotGroup, "Cargo", "Engineer Team 1", 500 )
  -- 
  -- What is also needed, is to have a set of @{Wrapper.Group}s defined that contains the clients of the players.
  -- 
  --     -- Allocate the Transport, which are the helicopter to retrieve the pilot, that can be manned by players.
  --     local GroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart()
  -- 
  -- Now that we have a CargoSet and a GroupSet, we can now create the TransportTask manually.
  -- 
  --     -- Declare the transport task.
  --     local TransportTask = TASK_CARGO_TRANSPORT
  --       :New( Mission, 
  --             GroupSet, 
  --             "Transport Engineers", 
  --             CargoSet, 
  --             "Fly behind enemy lines, and retrieve the downed pilot." 
  --           )
  -- 
  -- So you can see, setting up a transport task manually is a lot of work.
  -- It is better you use the cargo dispatcher to create transport tasks and it will work as it is intended.
  -- By doing this, cargo transport tasking will become a dynamic experience.
  -- 
  -- 
  -- # 2) Create a task using the Tasking.Task_Cargo_Dispatcher module.
  -- 
  -- Actually, it is better to **GENERATE** these tasks using the Tasking.Task_Cargo_Dispatcher module.
  -- Using the dispatcher module, transport tasks can be created easier.
  -- 
  -- Find below an example how to use the TASK_CARGO_DISPATCHER class:
  -- 
  -- 
  --        -- Find the HQ group.
  --        HQ = GROUP:FindByName( "HQ", "Bravo" )
  --    
  --        -- Create the command center with the name "Lima".
  --        CommandCenter = COMMANDCENTER
  --          :New( HQ, "Lima" )
  --    
  --        -- Create the mission, for the command center, with the name "Operation Cargo Fun", a "Tactical" mission, with the mission briefing "Transport Cargo", for the BLUE coalition.
  --        Mission = MISSION
  --          :New( CommandCenter, "Operation Cargo Fun", "Tactical", "Transport Cargo", coalition.side.BLUE )
  --    
  --        -- Create the SET of GROUPs containing clients (players) that will transport the cargo.
  --        -- These are have a name that start with "Transport" and are of the "blue" coalition.
  --        TransportGroups = SET_GROUP:New():FilterCoalitions( "blue" ):FilterPrefixes( "Transport" ):FilterStart()
  --    
  --    
  --        -- Here we create the TASK_CARGO_DISPATCHER object! This is where we assign the dispatcher to generate tasks in the Mission for the TransportGroups.
  --        TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups )
  --    
  --    
  --        -- Here we declare the SET of CARGOs called "Workmaterials".
  --        local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart()
  --    
  --        -- Here we declare (add) CARGO_GROUP objects of various types, that are filtered and added in the CargoSetworkmaterials cargo set.
  --        -- These cargo objects have the type "Workmaterials" which is exactly the type of cargo the CargoSetworkmaterials is filtering on.
  --        local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 )
  --        local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 )
  --        local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 )
  --        local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 )
  --        local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 )
  --    
  --          -- And here we create a new WorkplaceTask, using the :AddTransportTask method of the TaskDispatcher.
  --          local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." )
  --          TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) )
  -- 
  -- # 3) Handle cargo task events.
  -- 
  -- When a player is picking up and deploying cargo using his carrier, events are generated by the tasks. These events can be captured and tailored with your own code.
  -- 
  -- In order to properly capture the events and avoid mistakes using the documentation, it is advised that you execute the following actions:
  -- 
  --   * **Copy / Paste** the code section into your script.
  --   * **Change** the "myclass" literal to the task object name you have in your script.
  --   * Within the function, you can now **write your own code**!
  --   * **IntelliSense** will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, 
  --     but you need to declare them as they are automatically provided by the event handling system of MOOSE.
  -- 
  -- You can send messages or fire off any other events within the code section. The sky is the limit!
  -- 
  -- 
  -- ## 3.1) Handle the CargoPickedUp event.
  -- 
  -- Find below an example how to tailor the **CargoPickedUp** event, generated by the WorkplaceTask:
  -- 
  --      function WorkplaceTask:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo )
  --        
  --        MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has picked up cargo.", MESSAGE.Type.Information ):ToAll()
  --        
  --      end
  -- 
  -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has picked up a cargo object in the CarrierGroup.
  -- You can use this event handler to post messages to players, or provide status updates etc.
  -- 
  --      --- CargoPickedUp event handler OnAfter for "myclass".
  --      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
  --      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
  --      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
  --      -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has picked up the cargo.
  --      -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been picked up. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object!
  --      function myclass:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo )
  --      
  --        -- Write here your own code.
  --      
  --      end
  -- 
  -- 
  -- ## 3.2) Handle the CargoDeployed event.
  -- 
  -- Find below an example how to tailor the **CargoDeployed** event, generated by the WorkplaceTask:
  -- 
  --      function WorkplaceTask:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone )
  --        
  --        MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has deployed cargo at zone " .. DeployZone:GetName(), MESSAGE.Type.Information ):ToAll()
  --        
  --        Helos[ math.random(1,#Helos) ]:Spawn()
  --        EnemyHelos[ math.random(1,#EnemyHelos) ]:Spawn()
  --      end
  -- 
  -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has deployed a cargo object from the CarrierGroup.
  -- You can use this event handler to post messages to players, or provide status updates etc.
  -- 
  -- 
  --      --- CargoDeployed event handler OnAfter foR "myclass".
  --      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
  --      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
  --      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
  --      -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has deployed the cargo.
  --      -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been deployed. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object!
  --      -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
  --      function myclass:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone )
  --      
  --        -- Write here your own code.
  --      
  --      end
  -- 
  -- 
  -- 
  -- ===
  -- 
  -- @field #TASK_CARGO_TRANSPORT
  TASK_CARGO_TRANSPORT = {
    ClassName = "TASK_CARGO_TRANSPORT",
  }
  
  --- Instantiates a new TASK_CARGO_TRANSPORT.
  -- @param #TASK_CARGO_TRANSPORT self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned.
  -- @param #string TaskName The name of the Task.
  -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported.
  -- @param #string TaskBriefing The Cargo Task briefing.
  -- @return #TASK_CARGO_TRANSPORT self
  function TASK_CARGO_TRANSPORT:New( Mission, SetGroup, TaskName, SetCargo, TaskBriefing )
    local self = BASE:Inherit( self, TASK_CARGO:New( Mission, SetGroup, TaskName, SetCargo, "Transport", TaskBriefing ) ) -- #TASK_CARGO_TRANSPORT
    self:F()
    
    Mission:AddTask( self )
    
    local Fsm = self:GetUnitProcess()

    local CargoReport = REPORT:New( "Transport Cargo. The following cargo needs to be transported including initial positions:")
    
    SetCargo:ForEachCargo(
      -- @param Core.Cargo#CARGO Cargo
      function( Cargo )
        local CargoType = Cargo:GetType()
        local CargoName = Cargo:GetName()
        local CargoCoordinate = Cargo:GetCoordinate()
        CargoReport:Add( string.format( '- "%s" (%s) at %s', CargoName, CargoType, CargoCoordinate:ToStringMGRS() ) )
      end
    )
    
    self:SetBriefing( 
      TaskBriefing or 
      CargoReport:Text()
    )

    
    return self
  end 

  function TASK_CARGO_TRANSPORT:ReportOrder( ReportGroup ) 
    
    return 0
  end

  
  --- 
  -- @param #TASK_CARGO_TRANSPORT self
  -- @return #boolean
  function TASK_CARGO_TRANSPORT:IsAllCargoTransported()
  
    local CargoSet = self:GetCargoSet()
    local Set = CargoSet:GetSet()
    
    local DeployZones = self:GetDeployZones()
    
    local CargoDeployed = true
    
    -- Loop the CargoSet (so evaluate each Cargo in the SET_CARGO ).
    for CargoID, CargoData in pairs( Set ) do
      local Cargo = CargoData -- Core.Cargo#CARGO
      
      self:F( { Cargo = Cargo:GetName(), CargoDeployed = Cargo:IsDeployed() } )

      if Cargo:IsDeployed() then
      
--        -- Loop the DeployZones set for the TASK_CARGO_TRANSPORT.
--        for DeployZoneID, DeployZone in pairs( DeployZones ) do
--        
--          -- If all cargo is in one of the deploy zones, then all is good.
--          self:T( { Cargo.CargoObject } )
--          if Cargo:IsInZone( DeployZone ) == false then
--            CargoDeployed = false
--          end
--        end
      else
        CargoDeployed = false
      end
    end

    self:F( { CargoDeployed = CargoDeployed } )
    
    return CargoDeployed
  end
  
  -- @param #TASK_CARGO_TRANSPORT self
  function TASK_CARGO_TRANSPORT:onafterGoal( TaskUnit, From, Event, To )
    local CargoSet = self.CargoSet
    
    if self:IsAllCargoTransported() then
      self:Success()
    end
    
    self:__Goal( -10 )
  end

end

--- **Tasking** - Orchestrates the task for players to execute CSAR for downed pilots.
-- 
-- **Specific features:**
-- 
--   * Creates a task to retrieve a pilot @{Cargo.Cargo} from behind enemy lines.
--   * Derived from the TASK_CARGO class, which is derived from the TASK class.
--   * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled.
--   * Co-operation tasking, so a player joins a group of players executing the same task.
-- 
-- 
-- **A complete task menu system to allow players to:**
--   
--   * Join the task, abort the task.
--   * Mark the task location on the map.
--   * Provide details of the target.
--   * Route to the cargo.
--   * Route to the deploy zones.
--   * Load/Unload cargo.
--   * Board/Unboard cargo.
--   * Slingload cargo.
--   * Display the task briefing.
--   
--   
-- **A complete mission menu system to allow players to:**
--   
--   * Join a task, abort the task.
--   * Display task reports.
--   * Display mission statistics.
--   * Mark the task locations on the map.
--   * Provide details of the targets.
--   * Display the mission briefing.
--   * Provide status updates as retrieved from the command center.
--   * Automatically assign a random task as part of a mission.
--   * Manually assign a specific task as part of a mission.
--   
--   
--  **A settings system, using the settings menu:**
--  
--   * Tweak the duration of the display of messages.
--   * Switch between metric and imperial measurement system.
--   * Switch between coordinate formats used in messages: BR, BRA, LL DMS, LL DDM, MGRS.
--   * Different settings modes for A2G and A2A operations.
--   * Various other options.
--
-- ===
-- 
-- ![Banner Image](..\Images\deprecated.png)
-- 
-- Please read through the @{Tasking.Task_CARGO} process to understand the mechanisms of tasking and cargo tasking and handling.
-- 
-- The cargo will be a downed pilot, which is located somwhere on the battlefield. Use the menus system and facilities to 
-- join the CSAR task, and retrieve the pilot from behind enemy lines. The menu system is generic, there is nothing
-- specific on a CSAR task that requires further explanation, than reading the generic TASK_CARGO explanations.
-- 
-- Enjoy!
-- FC
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Tasking.Task_Cargo_CSAR
-- @image Task_Cargo_CSAR.JPG


do -- TASK_CARGO_CSAR

  -- @type TASK_CARGO_CSAR
  -- @extends Tasking.Task_Cargo#TASK_CARGO
  
  --- Orchestrates the task for players to execute CSAR for downed pilots.
  -- 
  -- CSAR tasks are suited to govern the process of return downed pilots behind enemy lines back to safetly.
  -- Typically, this task is executed by helicopter pilots, but it can also be executed by ground forces!
  -- 
  -- ===
  -- 
  -- A CSAR task can be created manually, but actually, it is better to **GENERATE** these tasks using the 
  -- @{Tasking.Task_Cargo_Dispatcher} module.
  -- 
  -- Using the dispatcher, CSAR tasks will be created **automatically** when a pilot ejects from a damaged AI aircraft.
  -- When this happens, the pilot actually will survive, but needs to be retrieved from behind enemy lines.
  -- 
  -- # 1) Create a CSAR task manually (code it).
  -- 
  -- Although it is recommended to use the dispatcher, you can create a CSAR task yourself as a mission designer.
  -- It is easy, as it works just like any other task setup.
  -- 
  -- ## 1.1) Create a command center.
  -- 
  -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor.
  -- 
  --     local CommandCenter = COMMANDCENTER
  --        :New( HQ, "Lima" ) -- Create the CommandCenter.
  --     
  -- ## 1.2) Create a mission.
  -- 
  -- Tasks work in a mission, which groups these tasks to achieve a joint mission goal.
  -- A command center can govern multiple missions.
  -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor.
  -- 
  --     -- Declare the Mission for the Command Center.
  --     local Mission = MISSION
  --       :New( CommandCenter, 
  --             "Overlord", 
  --             "High", 
  --             "Retrieve the downed pilots.", 
  --             coalition.side.RED 
  --           ) 
  -- 
  -- ## 1.3) Create the CSAR cargo task.
  -- 
  -- So, now that we have a command center and a mission, we now create the CSAR task.
  -- We create the CSAR task using the @{#TASK_CARGO_CSAR.New}() constructor.
  -- 
  -- Because a CSAR task will not generate the cargo itself, you'll need to create it first.
  -- The cargo in this case will be the downed pilot!
  -- 
  --     -- Here we define the "cargo set", which is a collection of cargo objects.
  --     -- The cargo set will be the input for the cargo transportation task.
  --     -- So a transportation object is handling a cargo set, which is automatically refreshed when new cargo is added/deleted.
  --     local CargoSet = SET_CARGO:New():FilterTypes( "Pilots" ):FilterStart()
  --    
  --     -- Now we add cargo into the battle scene.
  --     local PilotGroup = GROUP:FindByName( "Pilot" )
  --      
  --     -- CARGO_GROUP can be used to setup cargo with a GROUP object underneath.
  --     -- We name this group Engineers.
  --     -- Note that the name of the cargo is "Engineers".
  --     -- The cargoset "CargoSet" will embed all defined cargo of type "Pilots" (prefix) into its set.
  --     local CargoGroup = CARGO_GROUP:New( PilotGroup, "Pilots", "Downed Pilot", 500 )
  -- 
  -- What is also needed, is to have a set of @{Wrapper.Group}s defined that contains the clients of the players.
  -- 
  --     -- Allocate the Transport, which are the helicopter to retrieve the pilot, that can be manned by players.
  --     local GroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart()
  -- 
  -- Now that we have a CargoSet and a GroupSet, we can now create the CSARTask manually.
  -- 
  --     -- Declare the CSAR task.
  --     local CSARTask = TASK_CARGO_CSAR
  --       :New( Mission, 
  --             GroupSet, 
  --             "CSAR Pilot", 
  --             CargoSet, 
  --             "Fly behind enemy lines, and retrieve the downed pilot." 
  --           )
  -- 
  -- So you can see, setting up a CSAR task manually is a lot of work.
  -- It is better you use the cargo dispatcher to generate CSAR tasks and it will work as it is intended.
  -- By doing this, CSAR tasking will become a dynamic experience.
  -- 
  -- # 2) Create a task using the @{Tasking.Task_Cargo_Dispatcher} module.
  -- 
  -- Actually, it is better to **GENERATE** these tasks using the @{Tasking.Task_Cargo_Dispatcher} module.
  -- Using the dispatcher module, transport tasks can be created much more easy.
  -- 
  -- Find below an example how to use the TASK_CARGO_DISPATCHER class:
  -- 
  -- 
  --      -- Find the HQ group.
  --      HQ = GROUP:FindByName( "HQ", "Bravo" )
  --    
  --      -- Create the command center with the name "Lima".
  --      CommandCenter = COMMANDCENTER
  --        :New( HQ, "Lima" )
  --    
  --      -- Create the mission, for the command center, with the name "CSAR Mission", a "Tactical" mission, with the mission briefing "Rescue downed pilots.", for the RED coalition.
  --      Mission = MISSION
  --        :New( CommandCenter, "CSAR Mission", "Tactical", "Rescue downed pilots.", coalition.side.RED )
  --    
  --      -- Create the SET of GROUPs containing clients (players) that will transport the cargo.
  --      -- These are have a name that start with "Rescue" and are of the "red" coalition.
  --      AttackGroups = SET_GROUP:New():FilterCoalitions( "red" ):FilterPrefixes( "Rescue" ):FilterStart()
  --    
  --    
  --      -- Here we create the TASK_CARGO_DISPATCHER object! This is where we assign the dispatcher to generate tasks in the Mission for the AttackGroups.
  --      TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, AttackGroups )
  --    
  --      
  --      -- Here the task dispatcher will generate automatically CSAR tasks once a pilot ejects.
  --      TaskDispatcher:StartCSARTasks( 
  --        "CSAR", 
  --        { ZONE_UNIT:New( "Hospital", STATIC:FindByName( "Hospital" ), 100 ) }, 
  --        "One of our pilots has ejected. Go out to Search and Rescue our pilot!\n" .. 
  --        "Use the radio menu to let the command center assist you with the CSAR tasking."
  --      )
  -- 
  -- # 3) Handle cargo task events.
  -- 
  -- When a player is picking up and deploying cargo using his carrier, events are generated by the tasks. These events can be captured and tailored with your own code.
  -- 
  -- In order to properly capture the events and avoid mistakes using the documentation, it is advised that you execute the following actions:
  -- 
  --   * **Copy / Paste** the code section into your script.
  --   * **Change** the CLASS literal to the task object name you have in your script.
  --   * Within the function, you can now **write your own code**!
  --   * **IntelliSense** will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, 
  --     but you need to declare them as they are automatically provided by the event handling system of MOOSE.
  -- 
  -- You can send messages or fire off any other events within the code section. The sky is the limit!
  -- 
  -- NOTE: CSAR tasks are actually automatically created by the TASK_CARGO_DISPATCHER. So the underlying is not really applicable for mission designers as they will use the dispatcher instead
  -- of capturing these events from manually created CSAR tasks!
  -- 
  -- ## 3.1) Handle the **CargoPickedUp** event.
  -- 
  -- Find below an example how to tailor the **CargoPickedUp** event, generated by the CSARTask:
  -- 
  --      function CSARTask:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo )
  --        
  --        MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has picked up cargo.", MESSAGE.Type.Information ):ToAll()
  --        
  --      end
  -- 
  -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has picked up a cargo object in the CarrierGroup.
  -- You can use this event handler to post messages to players, or provide status updates etc.
  -- 
  --      --- CargoPickedUp event handler OnAfter for CLASS.
  --      -- @param #CLASS self
  --      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
  --      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
  --      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
  --      -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has picked up the cargo.
  --      -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been picked up. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object!
  --      function CLASS:OnAfterCargoPickedUp( From, Event, To, TaskUnit, Cargo )
  --      
  --        -- Write here your own code.
  --      
  --      end
  -- 
  -- 
  -- ## 3.2) Handle the **CargoDeployed** event.
  -- 
  -- Find below an example how to tailor the **CargoDeployed** event, generated by the CSARTask:
  -- 
  --      function CSARTask:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone )
  --        
  --        MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has deployed cargo at zone " .. DeployZone:GetName(), MESSAGE.Type.Information ):ToAll()
  --        
  --      end
  -- 
  -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has deployed a cargo object from the CarrierGroup.
  -- You can use this event handler to post messages to players, or provide status updates etc.
  -- 
  -- 
  --      --- CargoDeployed event handler OnAfter for CLASS.
  --      -- @param #CLASS self
  --      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
  --      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
  --      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
  --      -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has deployed the cargo.
  --      -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been deployed. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object!
  --      -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
  --      function CLASS:OnAfterCargoDeployed( From, Event, To, TaskUnit, Cargo, DeployZone )
  --      
  --        -- Write here your own code.
  --      
  --      end
  --      
  -- ===
  -- 
  -- @field #TASK_CARGO_CSAR
  TASK_CARGO_CSAR = {
    ClassName = "TASK_CARGO_CSAR",
  }
  
  --- Instantiates a new TASK_CARGO_CSAR.
  -- @param #TASK_CARGO_CSAR self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned.
  -- @param #string TaskName The name of the Task.
  -- @param Core.Set#SET_CARGO SetCargo The scope of the cargo to be transported.
  -- @param #string TaskBriefing The Cargo Task briefing.
  -- @return #TASK_CARGO_CSAR self
  function TASK_CARGO_CSAR:New( Mission, SetGroup, TaskName, SetCargo, TaskBriefing )
    local self = BASE:Inherit( self, TASK_CARGO:New( Mission, SetGroup, TaskName, SetCargo, "CSAR", TaskBriefing ) ) -- #TASK_CARGO_CSAR
    self:F()
    
    Mission:AddTask( self )
    
    
    -- Events
    
    self:AddTransition( "*", "CargoPickedUp", "*" )
    self:AddTransition( "*", "CargoDeployed", "*" )
    
    self:F( { CargoDeployed = self.CargoDeployed ~= nil and "true" or "false" } )
    
      --- OnAfter Transition Handler for Event CargoPickedUp.
      -- @function [parent=#TASK_CARGO_CSAR] OnAfterCargoPickedUp
      -- @param #TASK_CARGO_CSAR self
      -- @param #string From The From State string.
      -- @param #string Event The Event string.
      -- @param #string To The To State string.
      -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that PickedUp the cargo. You can use this to retrieve the PlayerName etc.
      -- @param Cargo.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status.
        
      --- OnAfter Transition Handler for Event CargoDeployed.
      -- @function [parent=#TASK_CARGO_CSAR] OnAfterCargoDeployed
      -- @param #TASK_CARGO_CSAR self
      -- @param #string From The From State string.
      -- @param #string Event The Event string.
      -- @param #string To The To State string.
      -- @param Wrapper.Unit#UNIT TaskUnit The Unit (Client) that Deployed the cargo. You can use this to retrieve the PlayerName etc.
      -- @param Cargo.Cargo#CARGO Cargo The Cargo that got PickedUp by the TaskUnit. You can use this to check Cargo Status.
      -- @param Core.Zone#ZONE DeployZone The zone where the Cargo got Deployed or UnBoarded.
        
    local Fsm = self:GetUnitProcess()

    local CargoReport = REPORT:New( "Rescue a downed pilot from the following position:")
    
    SetCargo:ForEachCargo(
      --- @param Cargo.Cargo#CARGO Cargo
      function( Cargo )
        local CargoType = Cargo:GetType()
        local CargoName = Cargo:GetName()
        local CargoCoordinate = Cargo:GetCoordinate()
        CargoReport:Add( string.format( '- "%s" (%s) at %s', CargoName, CargoType, CargoCoordinate:ToStringMGRS() ) )
      end
    )
    
    self:SetBriefing( 
      TaskBriefing or 
      CargoReport:Text()
    )

    
    return self
  end 
  


  function TASK_CARGO_CSAR:ReportOrder( ReportGroup ) 
    
    return 0
  end

  
  --- 
  -- @param #TASK_CARGO_CSAR self
  -- @return #boolean
  function TASK_CARGO_CSAR:IsAllCargoTransported()
  
    local CargoSet = self:GetCargoSet()
    local Set = CargoSet:GetSet()
    
    local DeployZones = self:GetDeployZones()
    
    local CargoDeployed = true
    
    -- Loop the CargoSet (so evaluate each Cargo in the SET_CARGO ).
    for CargoID, CargoData in pairs( Set ) do
      local Cargo = CargoData -- Cargo.Cargo#CARGO
      
      self:F( { Cargo = Cargo:GetName(), CargoDeployed = Cargo:IsDeployed() } )

      if Cargo:IsDeployed() then
      
--        -- Loop the DeployZones set for the TASK_CARGO_CSAR.
--        for DeployZoneID, DeployZone in pairs( DeployZones ) do
--        
--          -- If all cargo is in one of the deploy zones, then all is good.
--          self:T( { Cargo.CargoObject } )
--          if Cargo:IsInZone( DeployZone ) == false then
--            CargoDeployed = false
--          end
--        end
      else
        CargoDeployed = false
      end
    end

    self:F( { CargoDeployed = CargoDeployed } )
    
    return CargoDeployed
  end
  
  --- @param #TASK_CARGO_CSAR self
  function TASK_CARGO_CSAR:onafterGoal( TaskUnit, From, Event, To )
    local CargoSet = self.CargoSet
    
    if self:IsAllCargoTransported() then
      self:Success()
    end
    
    self:__Goal( -10 )
  end

end

--- **Tasking** - Creates and manages player TASK_CARGO tasks.
-- 
-- The **TASK_CARGO_DISPATCHER** allows you to setup various tasks for let human
-- players transport cargo as part of a task. 
-- 
-- ![Banner Image](..\Images\deprecated.png)
--  
-- The cargo dispatcher will implement for you mechanisms to create cargo transportation tasks:
--  
--   * As setup by the mission designer.
--   * Dynamically create CSAR missions (when a pilot is downed as part of a downed plane).
--   * Dynamically spawn new cargo and create cargo taskings!
--   
-- 
--   
-- **Specific features:**
-- 
--   * Creates a task to transport @{Cargo.Cargo} to and between deployment zones.
--   * Derived from the TASK_CARGO class, which is derived from the TASK class.
--   * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled.
--   * Co-operation tasking, so a player joins a group of players executing the same task.
-- 
-- 
-- **A complete task menu system to allow players to:**
--   
--   * Join the task, abort the task.
--   * Mark the task location on the map.
--   * Provide details of the target.
--   * Route to the cargo.
--   * Route to the deploy zones.
--   * Load/Unload cargo.
--   * Board/Unboard cargo.
--   * Slingload cargo.
--   * Display the task briefing.
--   
--   
-- **A complete mission menu system to allow players to:**
--   
--   * Join a task, abort the task.
--   * Display task reports.
--   * Display mission statistics.
--   * Mark the task locations on the map.
--   * Provide details of the targets.
--   * Display the mission briefing.
--   * Provide status updates as retrieved from the command center.
--   * Automatically assign a random task as part of a mission.
--   * Manually assign a specific task as part of a mission.
--   
--   
--  **A settings system, using the settings menu:**
--  
--   * Tweak the duration of the display of messages.
--   * Switch between metric and imperial measurement system.
--   * Switch between coordinate formats used in messages: BR, BRA, LL DMS, LL DDM, MGRS.
--   * Different settings modes for A2G and A2A operations.
--   * Various other options.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
--
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Tasking.Task_Cargo_Dispatcher
-- @image Task_Cargo_Dispatcher.JPG

do -- TASK_CARGO_DISPATCHER

  --- TASK_CARGO_DISPATCHER class.
  -- @type TASK_CARGO_DISPATCHER
  -- @extends Tasking.Task_Manager#TASK_MANAGER
  -- @field TASK_CARGO_DISPATCHER.CSAR CSAR
  -- @field Core.Set#SET_ZONE SetZonesCSAR

  -- @type TASK_CARGO_DISPATCHER.CSAR
  -- @field Wrapper.Unit#UNIT PilotUnit
  -- @field Tasking.Task#TASK Task
  

  --- Implements the dynamic dispatching of cargo tasks.
  -- 
  -- The **TASK_CARGO_DISPATCHER** allows you to setup various tasks for let human
  -- players transport cargo as part of a task. 
  -- 
  -- There are currently **two types of tasks** that can be constructed:
  -- 
  --   * A **normal cargo transport** task, which tasks humans to transport cargo from a location towards a deploy zone.
  --   * A **CSAR** cargo transport task. CSAR tasks are **automatically generated** when a friendly (AI) plane is downed and the friendly pilot ejects... 
  --   You as a player (the helo pilot) can go out in the battlefield, fly behind enemy lines, and rescue the pilot (back to a deploy zone).
  -- 
  -- Let's explore **step by step** how to setup the task cargo dispatcher.
  -- 
  -- # 1. Setup a mission environment.
  -- 
  -- It is easy, as it works just like any other task setup, so setup a command center and a mission.
  -- 
  -- ## 1.1. Create a command center.
  -- 
  -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor.
  -- 
  --     local CommandCenter = COMMANDCENTER
  --        :New( HQ, "Lima" ) -- Create the CommandCenter.
  --     
  -- ## 1.2. Create a mission.
  -- 
  -- Tasks work in a mission, which groups these tasks to achieve a joint mission goal.
  -- A command center can govern multiple missions.
  -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor.
  -- 
  --     -- Declare the Mission for the Command Center.
  --     local Mission = MISSION
  --       :New( CommandCenter, 
  --             "Overlord", 
  --             "High", 
  --             "Transport the cargo.", 
  --             coalition.side.RED 
  --           ) 
  -- 
  -- 
  -- # 2. Dispatch a **transport cargo** task.
  -- 
  -- So, now that we have a command center and a mission, we now create the transport task.
  -- We create the transport task using the @{#TASK_CARGO_DISPATCHER.AddTransportTask}() constructor.
  -- 
  -- ## 2.1. Create the cargo in the mission.
  -- 
  -- Because a transport task will not generate the cargo itself, you'll need to create it first.
  -- 
  --     -- Here we define the "cargo set", which is a collection of cargo objects.
  --     -- The cargo set will be the input for the cargo transportation task.
  --     -- So a transportation object is handling a cargo set, which is automatically updated when new cargo is added/deleted.
  --     local WorkmaterialsCargoSet = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart()
  --    
  --     -- Now we add cargo into the battle scene.
  --     local PilotGroup = GROUP:FindByName( "Engineers" )
  --      
  --     -- CARGO_GROUP can be used to setup cargo with a GROUP object underneath.
  --     -- We name the type of this group "Workmaterials", so that this cargo group will be included within the WorkmaterialsCargoSet.
  --     -- Note that the name of the cargo is "Engineer Team 1".
  --     local CargoGroup = CARGO_GROUP:New( PilotGroup, "Workmaterials", "Engineer Team 1", 500 )
  -- 
  -- What is also needed, is to have a set of @{Wrapper.Group}s defined that contains the clients of the players.
  -- 
  --     -- Allocate the Transport, which are the helicopters to retrieve the pilot, that can be manned by players.
  --     -- The name of these helicopter groups containing one client begins with "Transport", as modelled within the mission editor.
  --     local PilotGroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart()
  -- 
  -- ## 2.2. Setup the cargo transport task.
  -- 
  -- First, we need to create a TASK_CARGO_DISPATCHER object.
  -- 
  --     TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, PilotGroupSet )
  -- 
  -- So, the variable `TaskDispatcher` will contain the object of class TASK_CARGO_DISPATCHER, which will allow you to dispatch cargo transport tasks:
  --  
  --   * for mission `Mission`.
  --   * for the group set `PilotGroupSet`.
  -- 
  -- Now that we have `TaskDispatcher` object, we can now **create the TransportTask**, using the @{#TASK_CARGO_DISPATCHER.AddTransportTask}() method!
  -- 
  --     local TransportTask = TaskDispatcher:AddTransportTask( 
  --       "Transport workmaterials", 
  --       WorkmaterialsCargoSet, 
  --       "Transport the workers, engineers and the equipment near the Workplace." )
  -- 
  -- As a result of this code, the `TransportTask` (returned) variable will contain an object of @{#TASK_CARGO_TRANSPORT}!
  -- We pass to the method the title of the task, and the `WorkmaterialsCargoSet`, which is the set of cargo groups to be transported!
  -- This object can also be used to setup additional things, or to control this specific task with special actions.
  -- 
  -- And you're done! As you can see, it is a bit of work, but the reward is great.
  -- And, because all this is done using program interfaces, you can build a mission with a **dynamic cargo transport task mechanism** yourself!
  -- Based on events happening within your mission, you can use the above methods to create new cargo, and setup a new task for cargo transportation to a group of players!
  -- 
  --     
  -- # 3. Dispatch CSAR tasks.
  -- 
  -- CSAR tasks can be dynamically created when a friendly pilot ejects, or can be created manually.
  -- We'll explore both options.
  -- 
  -- ## 3.1. CSAR task dynamic creation.
  -- 
  -- Because there is an "event" in a running simulation that creates CSAR tasks, the method @{#TASK_CARGO_DISPATCHER.StartCSARTasks}() will create automatically:
  -- 
  --   1. a new downed pilot at the location where the plane was shot
  --   2. declare that pilot as cargo
  --   3. creates a CSAR task automatically to retrieve that pilot
  --   4. requires deploy zones to be specified where to transport the downed pilot to, in order to complete that task.
  -- 
  -- You create a CSAR task dynamically in a very easy way:
  -- 
  --     TaskDispatcher:StartCSARTasks( 
  --       "CSAR", 
  --       { ZONE_UNIT:New( "Hospital", STATIC:FindByName( "Hospital" ), 100 ) }, 
  --       "One of our pilots has ejected. Go out to Search and Rescue our pilot!\n" .. 
  --       "Use the radio menu to let the command center assist you with the CSAR tasking."
  --     )
  -- 
  -- The method @{#TASK_CARGO_DISPATCHER.StopCSARTasks}() will automatically stop with the creation of CSAR tasks when friendly pilots eject.
  -- 
  -- **Remarks:** 
  --   
  --   * the ZONE_UNIT can also be a ZONE, or a ZONE_POLYGON object, or any other ZONE_ object!
  --   * you can declare the array of zones in another variable, or course!
  -- 
  -- 
  -- ## 3.2. CSAR task manual creation.
  -- 
  -- We create the CSAR task using the @{#TASK_CARGO_DISPATCHER.AddCSARTask}() constructor.
  -- 
  -- The method will create a new CSAR task, and will generate the pilots cargo itself, at the specified coordinate.
  -- 
  -- What is first needed, is to have a set of @{Wrapper.Group}s defined that contains the clients of the players.
  -- 
  --     -- Allocate the Transport, which are the helicopter to retrieve the pilot, that can be manned by players.
  --     local GroupSet = SET_GROUP:New():FilterPrefixes( "Transport" ):FilterStart()
  -- 
  -- We need to create a TASK_CARGO_DISPATCHER object.
  -- 
  --     TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, GroupSet )
  -- 
  -- So, the variable `TaskDispatcher` will contain the object of class TASK_CARGO_DISPATCHER, which will allow you to dispatch cargo CSAR tasks:
  --  
  --   * for mission `Mission`.
  --   * for the group of players (pilots) captured within the `GroupSet` (those groups with a name starting with `"Transport"`).
  -- 
  -- Now that we have a PilotsCargoSet and a GroupSet, we can now create the CSAR task manually.
  -- 
  --     -- Declare the CSAR task.
  --     local CSARTask = TaskDispatcher:AddCSARTask( 
  --       "CSAR Task",
  --       Coordinate,
  --       270,
  --       "Bring the pilot back!"
  --     )
  -- 
  -- As a result of this code, the `CSARTask` (returned) variable will contain an object of @{#TASK_CARGO_CSAR}!
  -- We pass to the method the title of the task, and the `WorkmaterialsCargoSet`, which is the set of cargo groups to be transported!
  -- This object can also be used to setup additional things, or to control this specific task with special actions.
  -- Note that when you declare a CSAR task manually, you'll still need to specify a deployment zone!
  -- 
  -- # 4. Setup the deploy zone(s).
  -- 
  -- The task cargo dispatcher also foresees methods to setup the deployment zones to where the cargo needs to be transported!
  -- 
  -- There are two levels on which deployment zones can be configured:
  -- 
  --   * Default deploy zones: The TASK_CARGO_DISPATCHER object can have default deployment zones, which will apply over all tasks active in the task dispatcher.
  --   * Task specific deploy zones: The TASK_CARGO_DISPATCHER object can have specific deployment zones which apply to a specific task only!
  -- 
  -- Note that for Task specific deployment zones, there are separate deployment zone creation methods per task type!
  -- 
  -- ## 4.1. Setup default deploy zones.
  -- 
  -- Use the @{#TASK_CARGO_DISPATCHER.SetDefaultDeployZone}() to setup one deployment zone, and @{#TASK_CARGO_DISPATCHER.SetDefaultDeployZones}() to setup multiple default deployment zones in one call.
  -- 
  -- ## 4.2. Setup task specific deploy zones for a **transport task**.
  -- 
  -- Use the @{#TASK_CARGO_DISPATCHER.SetTransportDeployZone}() to setup one deployment zone, and @{#TASK_CARGO_DISPATCHER.SetTransportDeployZones}() to setup multiple default deployment zones in one call.
  -- 
  -- ## 4.3. Setup task specific deploy zones for a **CSAR task**. 
  -- 
  -- Use the @{#TASK_CARGO_DISPATCHER.SetCSARDeployZone}() to setup one deployment zone, and @{#TASK_CARGO_DISPATCHER.SetCSARDeployZones}() to setup multiple default deployment zones in one call.
  -- 
  -- ## 4.4. **CSAR ejection zones**. 
  -- 
  -- Setup a set of zones where the pilots will only eject and a task is created for CSAR. When such a set of zones is given, any ejection outside those zones will not result in a pilot created for CSAR!
  -- 
  -- Use the @{#TASK_CARGO_DISPATCHER.SetCSARZones}() to setup the set of zones.
  -- 
  -- ## 4.5. **CSAR ejection maximum**.
  -- 
  -- Setup how many pilots will eject the maximum. This to avoid an overload of CSAR tasks being created :-) The default is endless CSAR tasks.
  -- 
  -- Use the @{#TASK_CARGO_DISPATCHER.SetMaxCSAR}() to setup the maximum of pilots that will eject for CSAR.
  -- 
  -- 
  -- # 5) Handle cargo task events.
  -- 
  -- When a player is picking up and deploying cargo using his carrier, events are generated by the dispatcher. These events can be captured and tailored with your own code.
  -- 
  -- In order to properly capture the events and avoid mistakes using the documentation, it is advised that you execute the following actions:
  -- 
  --   * **Copy / Paste** the code section into your script.
  --   * **Change** the CLASS literal to the task object name you have in your script.
  --   * Within the function, you can now **write your own code**!
  --   * **IntelliSense** will recognize the type of the variables provided by the function. Note: the From, Event and To variables can be safely ignored, 
  --     but you need to declare them as they are automatically provided by the event handling system of MOOSE.
  -- 
  -- You can send messages or fire off any other events within the code section. The sky is the limit!
  -- 
  -- First, we need to create a TASK_CARGO_DISPATCHER object.
  -- 
  --      TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, PilotGroupSet )
  -- 
  -- Second, we create a new cargo transport task for the transportation of workmaterials.
  -- 
  --      TaskDispatcher:AddTransportTask( 
  --        "Transport workmaterials", 
  --        WorkmaterialsCargoSet, 
  --        "Transport the workers, engineers and the equipment near the Workplace." )
  -- 
  -- Note that we don't really need to keep the resulting task, it is kept internally also in the dispatcher.
  -- 
  -- Using the `TaskDispatcher` object, we can now cpature the CargoPickedUp and CargoDeployed events.
  -- 
  -- ## 5.1) Handle the **CargoPickedUp** event.
  -- 
  -- Find below an example how to tailor the **CargoPickedUp** event, generated by the `TaskDispatcher`:
  -- 
  --      function TaskDispatcher:OnAfterCargoPickedUp( From, Event, To, Task, TaskPrefix, TaskUnit, Cargo )
  --        
  --        MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has picked up cargo for task " .. Task:GetName() .. ".", MESSAGE.Type.Information ):ToAll()
  --        
  --      end
  -- 
  -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has picked up a cargo object in the CarrierGroup.
  -- You can use this event handler to post messages to players, or provide status updates etc.
  -- 
  --      --- CargoPickedUp event handler OnAfter for CLASS.
  --      -- @param #CLASS self
  --      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
  --      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
  --      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
  --      -- @param Tasking.Task_Cargo#TASK_CARGO Task The cargo task for which the cargo has been picked up. Note that this will be a derived TAKS_CARGO object!
  --      -- @param #string TaskPrefix The prefix of the task that was provided when the task was created.
  --      -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has picked up the cargo.
  --      -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been picked up. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object!
  --      function CLASS:OnAfterCargoPickedUp( From, Event, To, Task, TaskPrefix, TaskUnit, Cargo )
  --      
  --        -- Write here your own code.
  --      
  --      end
  -- 
  -- 
  -- ## 5.2) Handle the **CargoDeployed** event.
  -- 
  -- Find below an example how to tailor the **CargoDeployed** event, generated by the `TaskDispatcher`:
  -- 
  --       function WorkplaceTask:OnAfterCargoDeployed( From, Event, To, Task, TaskPrefix, TaskUnit, Cargo, DeployZone )
  --        
  --         MESSAGE:NewType( "Unit " .. TaskUnit:GetName().. " has deployed cargo at zone " .. DeployZone:GetName() .. " for task " .. Task:GetName() .. ".", MESSAGE.Type.Information ):ToAll()
  --        
  --         Helos[ math.random(1,#Helos) ]:Spawn()
  --         EnemyHelos[ math.random(1,#EnemyHelos) ]:Spawn()
  --       end
  -- 
  -- If you want to code your own event handler, use this code fragment to tailor the event when a player carrier has deployed a cargo object from the CarrierGroup.
  -- You can use this event handler to post messages to players, or provide status updates etc.
  -- 
  -- 
  --      --- CargoDeployed event handler OnAfter for CLASS.
  --      -- @param #CLASS self
  --      -- @param #string From A string that contains the "*from state name*" when the event was triggered.
  --      -- @param #string Event A string that contains the "*event name*" when the event was triggered.
  --      -- @param #string To A string that contains the "*to state name*" when the event was triggered.
  --      -- @param Tasking.Task_Cargo#TASK_CARGO Task The cargo task for which the cargo has been deployed. Note that this will be a derived TAKS_CARGO object!
  --      -- @param #string TaskPrefix The prefix of the task that was provided when the task was created.
  --      -- @param Wrapper.Unit#UNIT TaskUnit The unit (client) of the player that has deployed the cargo.
  --      -- @param Cargo.Cargo#CARGO Cargo The cargo object that has been deployed. Note that this can be a CARGO_GROUP, CARGO_CRATE or CARGO_SLINGLOAD object!
  --      -- @param Core.Zone#ZONE DeployZone The zone wherein the cargo is deployed. This can be any zone type, like a ZONE, ZONE_GROUP, ZONE_AIRBASE.
  --      function CLASS:OnAfterCargoDeployed( From, Event, To, Task, TaskPrefix, TaskUnit, Cargo, DeployZone )
  --      
  --        -- Write here your own code.
  --      
  --      end
  -- 
  -- 
  -- 
  -- @field #TASK_CARGO_DISPATCHER
  TASK_CARGO_DISPATCHER = {
    ClassName = "TASK_CARGO_DISPATCHER",
    Mission = nil,
    Tasks = {},
    CSAR = {},
    CSARSpawned = 0,
    
    Transport = {},
    TransportCount = 0,
  }
  
  
  --- TASK_CARGO_DISPATCHER constructor.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done.
  -- @param Core.Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission.
  -- @return #TASK_CARGO_DISPATCHER self
  function TASK_CARGO_DISPATCHER:New( Mission, SetGroup )
  
    -- Inherits from DETECTION_MANAGER
    local self = BASE:Inherit( self, TASK_MANAGER:New( SetGroup ) ) -- #TASK_CARGO_DISPATCHER
    
    self.Mission = Mission
    
    self:AddTransition( "Started", "Assign", "Started" )
    self:AddTransition( "Started", "CargoPickedUp", "Started" )
    self:AddTransition( "Started", "CargoDeployed", "Started" )
    
    --- OnAfter Transition Handler for Event Assign.
    -- @function [parent=#TASK_CARGO_DISPATCHER] OnAfterAssign
    -- @param #TASK_CARGO_DISPATCHER self
    -- @param #string From The From State string.
    -- @param #string Event The Event string.
    -- @param #string To The To State string.
    -- @param Tasking.Task_A2A#TASK_A2A Task
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param #string PlayerName
    
    self:SetCSARRadius()
    self:__StartTasks( 5 )
    
    self.MaxCSAR = nil
    self.CountCSAR = 0
    
    -- For CSAR missions, we process the event when a pilot ejects.
    
    self:HandleEvent( EVENTS.Ejection )
    
    return self
  end


  --- Sets the set of zones were pilots will only be spawned (eject) when the planes crash.  
  -- Note that because this is a set of zones, the MD can create the zones dynamically within his mission!
  -- Just provide a set of zones, see usage, but find the tactical situation here:
  -- 
  -- ![CSAR Zones](../Tasking/CSAR_Zones.JPG)
  -- 
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param Core.Set#SET_ZONE SetZonesCSAR The set of zones where pilots will only be spawned for CSAR when they eject.
  -- @usage
  -- 
  --      TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, AttackGroups )
  -- 
  --      -- Use this call to pass the set of zones.
  --      -- Note that you can create the set of zones inline, because the FilterOnce method (and other SET_ZONE methods return self).
  --      -- So here the zones can be created as normal trigger zones (MOOSE creates a collection of ZONE objects when teh mission starts of all trigger zones).
  --      -- Just name them as CSAR zones here.
  --      TaskDispatcher:SetCSARZones( SET_ZONE:New():FilterPrefixes("CSAR"):FilterOnce() )
  -- 
  function TASK_CARGO_DISPATCHER:SetCSARZones( SetZonesCSAR )

    self.SetZonesCSAR = SetZonesCSAR
  
  end


  --- Sets the maximum of pilots that will be spawned (eject) when the planes crash.  
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param #number MaxCSAR The maximum of pilots that will eject for CSAR.
  -- @usage
  -- 
  --      TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, AttackGroups )
  -- 
  --      -- Use this call to the maximum of CSAR to 10.
  --      TaskDispatcher:SetMaxCSAR( 10 )
  -- 
  function TASK_CARGO_DISPATCHER:SetMaxCSAR( MaxCSAR )

    self.MaxCSAR = MaxCSAR
  
  end



  --- Handle the event when a pilot ejects.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param Core.Event#EVENTDATA EventData
  function TASK_CARGO_DISPATCHER:OnEventEjection( EventData )
    self:F( { EventData = EventData } )
    
    if self.CSARTasks == true then

      local CSARCoordinate = EventData.IniUnit:GetCoordinate()
      local CSARCoalition  = EventData.IniUnit:GetCoalition()
      local CSARCountry    = EventData.IniUnit:GetCountry()
      local CSARHeading    = EventData.IniUnit:GetHeading()
      
      -- Only add a CSAR task if the coalition of the mission is equal to the coalition of the ejected unit.
      if CSARCoalition == self.Mission:GetCommandCenter():GetCoalition() then
        -- And only add if the eject is in one of the zones, if defined.
        if not self.SetZonesCSAR or ( self.SetZonesCSAR and self.SetZonesCSAR:IsCoordinateInZone( CSARCoordinate ) ) then
          -- And only if the maximum of pilots is not reached that ejected!
          if not self.MaxCSAR or ( self.MaxCSAR and self.CountCSAR < self.MaxCSAR ) then
            local CSARTaskName = self:AddCSARTask( self.CSARTaskName, CSARCoordinate, CSARHeading, CSARCountry, self.CSARBriefing )     
            self:SetCSARDeployZones( CSARTaskName, self.CSARDeployZones )
            self.CountCSAR = self.CountCSAR + 1
          end
        end
      end
    end
    
    return self
  end
  

  --- Define one default deploy zone for all the cargo tasks.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param DefaultDeployZone A default deploy zone.
  -- @return #TASK_CARGO_DISPATCHER
  function TASK_CARGO_DISPATCHER:SetDefaultDeployZone( DefaultDeployZone )

    self.DefaultDeployZones = { DefaultDeployZone }
  
    return self
  end
  
  
  --- Define the deploy zones for all the cargo tasks.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param DefaultDeployZones A list of the deploy zones.
  -- @return #TASK_CARGO_DISPATCHER
  -- 
  function TASK_CARGO_DISPATCHER:SetDefaultDeployZones( DefaultDeployZones )

    self.DefaultDeployZones = DefaultDeployZones
  
    return self
  end


  --- Start the generation of CSAR tasks to retrieve a downed pilots.
  -- You need to specify a task briefing, a task name, default deployment zone(s).
  -- This method can only be used once!
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param #string CSARTaskName The CSAR task name.
  -- @param #string CSARDeployZones The zones to where the CSAR deployment should be directed.
  -- @param #string CSARBriefing The briefing of the CSAR tasks.
  -- @return #TASK_CARGO_DISPATCHER
  function TASK_CARGO_DISPATCHER:StartCSARTasks( CSARTaskName, CSARDeployZones, CSARBriefing)
  
    if not self.CSARTasks then
      self.CSARTasks = true
      self.CSARTaskName = CSARTaskName
      self.CSARDeployZones = CSARDeployZones
      self.CSARBriefing = CSARBriefing
    else
      error( "TASK_CARGO_DISPATCHER: The generation of CSAR tasks has already started." )
    end
  
    return self
  end
  
  
  --- Stop the generation of CSAR tasks to retrieve a downed pilots.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @return #TASK_CARGO_DISPATCHER
  function TASK_CARGO_DISPATCHER:StopCSARTasks()
  
    if self.CSARTasks then
      self.CSARTasks = nil
      self.CSARTaskName = nil
      self.CSARDeployZones = nil
      self.CSARBriefing = nil
    else
      error( "TASK_CARGO_DISPATCHER: The generation of CSAR tasks was not yet started." )
    end
  
    return self
  end
  
  
  --- Add a CSAR task to retrieve a downed pilot.
  -- You need to specify a coordinate from where the pilot will be spawned to be rescued.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param #string CSARTaskPrefix (optional) The prefix of the CSAR task. 
  -- @param Core.Point#COORDINATE CSARCoordinate The coordinate where a downed pilot will be spawned.
  -- @param #number CSARHeading The heading of the pilot in degrees.
  -- @param #DCSCountry CSARCountry The country ID of the pilot that will be spawned.
  -- @param #string CSARBriefing The briefing of the CSAR task.
  -- @return #string The CSAR Task Name as a string. The Task Name is the main key and is shown in the task list of the Mission Tasking menu.
  -- @usage
  -- 
  --   -- Add a CSAR task to rescue a downed pilot from within a coordinate.
  --   local Coordinate = PlaneUnit:GetPointVec2()
  --   TaskA2ADispatcher:AddCSARTask( "CSAR Task", Coordinate )
  --   
  --   -- Add a CSAR task to rescue a downed pilot from within a coordinate of country RUSSIA, which is pointing to the west (270°).
  --   local Coordinate = PlaneUnit:GetPointVec2()
  --   TaskA2ADispatcher:AddCSARTask( "CSAR Task", Coordinate, 270, Country.RUSSIA )
  --   
  function TASK_CARGO_DISPATCHER:AddCSARTask( CSARTaskPrefix, CSARCoordinate, CSARHeading, CSARCountry, CSARBriefing )

    local CSARCoalition = self.Mission:GetCommandCenter():GetCoalition()

    CSARHeading = CSARHeading or 0
    CSARCountry = CSARCountry or self.Mission:GetCommandCenter():GetCountry()

    self.CSARSpawned = self.CSARSpawned + 1
    
    local CSARTaskName = string.format( ( CSARTaskPrefix or "CSAR" ) .. ".%03d", self.CSARSpawned )
    
    -- Create the CSAR Pilot SPAWN object.
    -- Let us create the Template for the replacement Pilot :-)
    local Template = {
      ["visible"] = false,
      ["hidden"] = false,
      ["task"] = "Ground Nothing",
      ["name"] = string.format( "CSAR Pilot#%03d", self.CSARSpawned ),
      ["x"] = CSARCoordinate.x,
      ["y"] = CSARCoordinate.z,
      ["units"] = 
      {
        [1] = 
        {
          ["type"] = ( CSARCoalition == coalition.side.BLUE ) and "Soldier M4" or "Infantry AK",
          ["name"] = string.format( "CSAR Pilot#%03d-01", self.CSARSpawned ),
          ["skill"] = "Excellent",
          ["playerCanDrive"] = false,
          ["x"] = CSARCoordinate.x,
          ["y"] = CSARCoordinate.z,
          ["heading"] = CSARHeading,
        }, -- end of [1]
      }, -- end of ["units"]
    }

    local CSARGroup = GROUP:NewTemplate( Template, CSARCoalition, Group.Category.GROUND, CSARCountry )

    self.CSAR[CSARTaskName] = {} 
    self.CSAR[CSARTaskName].PilotGroup = CSARGroup
    self.CSAR[CSARTaskName].Briefing = CSARBriefing
    self.CSAR[CSARTaskName].Task = nil
    self.CSAR[CSARTaskName].TaskPrefix = CSARTaskPrefix
    
    return CSARTaskName
  end


  --- Define the radius to when a CSAR task will be generated for any downed pilot within range of the nearest CSAR airbase.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param #number CSARRadius (Optional, Default = 50000) The radius in meters to decide whether a CSAR needs to be created.
  -- @return #TASK_CARGO_DISPATCHER
  -- @usage
  -- 
  --   -- Set 20km as the radius to CSAR any downed pilot within range of the nearest CSAR airbase.
  --   TaskA2ADispatcher:SetEngageRadius( 20000 )
  --   
  --   -- Set 50km as the radius to to CSAR any downed pilot within range of the nearest CSAR airbase.
  --   TaskA2ADispatcher:SetEngageRadius() -- 50000 is the default value.
  --   
  function TASK_CARGO_DISPATCHER:SetCSARRadius( CSARRadius )

    self.CSARRadius = CSARRadius or 50000
  
    return self
  end
  
  
  --- Define one deploy zone for the CSAR tasks.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param #string CSARTaskName (optional) The name of the CSAR task. 
  -- @param CSARDeployZone A CSAR deploy zone.
  -- @return #TASK_CARGO_DISPATCHER
  function TASK_CARGO_DISPATCHER:SetCSARDeployZone( CSARTaskName, CSARDeployZone )

    if CSARTaskName then
      self.CSAR[CSARTaskName].DeployZones = { CSARDeployZone }
    end
  
    return self
  end
  
  
  --- Define the deploy zones for the CSAR tasks.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param #string CSARTaskName (optional) The name of the CSAR task.
  -- @param CSARDeployZones A list of the CSAR deploy zones.
  -- @return #TASK_CARGO_DISPATCHER
  -- 
  function TASK_CARGO_DISPATCHER:SetCSARDeployZones( CSARTaskName, CSARDeployZones )

    if CSARTaskName and self.CSAR[CSARTaskName] then
      self.CSAR[CSARTaskName].DeployZones = CSARDeployZones
    end
  
    return self
  end


  --- Add a Transport task to transport cargo from fixed locations to a deployment zone.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param #string TaskPrefix (optional) The prefix of the transport task. 
  -- This prefix will be appended with a . + a number of 3 digits.
  -- If no TaskPrefix is given, then "Transport" will be used as the prefix. 
  -- @param Core.Set#SET_CARGO SetCargo The SetCargo to be transported.
  -- @param #string Briefing The briefing of the task transport to be shown to the player.
  -- @param #boolean Silent If true don't send a message that a new task is available.
  -- @return Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT
  -- @usage
  -- 
  --   -- Add a Transport task to transport cargo of different types to a Transport Deployment Zone.
  --  TaskDispatcher = TASK_CARGO_DISPATCHER:New( Mission, TransportGroups )
  --  
  --  local CargoSetWorkmaterials = SET_CARGO:New():FilterTypes( "Workmaterials" ):FilterStart()
  --  local EngineerCargoGroup = CARGO_GROUP:New( GROUP:FindByName( "Engineers" ), "Workmaterials", "Engineers", 250 )
  --  local ConcreteCargo = CARGO_SLINGLOAD:New( STATIC:FindByName( "Concrete" ), "Workmaterials", "Concrete", 150, 50 )
  --  local CrateCargo = CARGO_CRATE:New( STATIC:FindByName( "Crate" ), "Workmaterials", "Crate", 150, 50 )
  --  local EnginesCargo = CARGO_CRATE:New( STATIC:FindByName( "Engines" ), "Workmaterials", "Engines", 150, 50 )
  --  local MetalCargo = CARGO_CRATE:New( STATIC:FindByName( "Metal" ), "Workmaterials", "Metal", 150, 50 )
  --  
  --  -- Here we add the task. We name the task "Build a Workplace".
  --  -- We provide the CargoSetWorkmaterials, and a briefing as the 2nd and 3rd parameter.
  --  -- The :AddTransportTask() returns a Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT object, which we keep as a reference for further actions.
  --  -- The WorkplaceTask holds the created and returned Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT object.
  --  local WorkplaceTask = TaskDispatcher:AddTransportTask( "Build a Workplace", CargoSetWorkmaterials, "Transport the workers, engineers and the equipment near the Workplace." )
  --  
  --  -- Here we set a TransportDeployZone. We use the WorkplaceTask as the reference, and provide a ZONE object.
  --  TaskDispatcher:SetTransportDeployZone( WorkplaceTask, ZONE:New( "Workplace" ) )
  --  
  function TASK_CARGO_DISPATCHER:AddTransportTask( TaskPrefix, SetCargo, Briefing, Silent )

    self.TransportCount = self.TransportCount + 1
    
    local verbose = Silent or false
    
    local TaskName = string.format( ( TaskPrefix or "Transport" ) .. ".%03d", self.TransportCount )
    
    self.Transport[TaskName] = {} 
    self.Transport[TaskName].SetCargo = SetCargo
    self.Transport[TaskName].Briefing = Briefing
    self.Transport[TaskName].Task = nil
    self.Transport[TaskName].TaskPrefix = TaskPrefix
    
    self:ManageTasks(verbose)
    
    return self.Transport[TaskName] and self.Transport[TaskName].Task
  end


  --- Define one deploy zone for the Transport tasks.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT Task The name of the Transport task. 
  -- @param TransportDeployZone A Transport deploy zone.
  -- @return #TASK_CARGO_DISPATCHER
  -- @usage
  -- 
  -- 
  function TASK_CARGO_DISPATCHER:SetTransportDeployZone( Task, TransportDeployZone )

    if self.Transport[Task.TaskName] then
      self.Transport[Task.TaskName].DeployZones = { TransportDeployZone }
    else
      error( "Task does not exist" )
    end

    self:ManageTasks()
  
    return self
  end
  
  
  --- Define the deploy zones for the Transport tasks.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param Tasking.Task_Cargo_Transport#TASK_CARGO_TRANSPORT Task The name of the Transport task. 
  -- @param TransportDeployZones A list of the Transport deploy zones.
  -- @return #TASK_CARGO_DISPATCHER
  -- 
  function TASK_CARGO_DISPATCHER:SetTransportDeployZones( Task, TransportDeployZones )

    if self.Transport[Task.TaskName] then
      self.Transport[Task.TaskName].DeployZones = TransportDeployZones
    else
      error( "Task does not exist" )
    end

    self:ManageTasks()
  
    return self
  end
  
  --- Evaluates of a CSAR task needs to be started.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @return Core.Set#SET_CARGO The SetCargo to be rescued.
  -- @return #nil If there is no CSAR task required.
  function TASK_CARGO_DISPATCHER:EvaluateCSAR( CSARUnit )
  
    local CSARCargo = CARGO_GROUP:New( CSARUnit, "Pilot", CSARUnit:GetName(), 80, 1500, 10 )
    
    local SetCargo = SET_CARGO:New()
    SetCargo:AddCargosByName( CSARUnit:GetName() )
    
    SetCargo:Flush(self)
    
    return SetCargo
    
  end

  

  --- Assigns tasks to the @{Core.Set#SET_GROUP}.
  -- @param #TASK_CARGO_DISPATCHER self
  -- @param #boolean Silent Announce new task (nil/false) or not (true).
  -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop.
  function TASK_CARGO_DISPATCHER:ManageTasks(Silent)
    self:F()
    local verbose = Silent and true
    local AreaMsg = {}
    local TaskMsg = {}
    local ChangeMsg = {}
    
    local Mission = self.Mission
    
    if Mission:IsIDLE() or Mission:IsENGAGED() then
    
      local TaskReport = REPORT:New()
      
      -- Checking the task queue for the dispatcher, and removing any obsolete task!
      for TaskIndex, TaskData in pairs( self.Tasks ) do
        local Task = TaskData -- Tasking.Task#TASK
        if Task:IsStatePlanned() then
          -- Here we need to check if the pilot is still existing.
--          local DetectedItem = Detection:GetDetectedItemByIndex( TaskIndex )
--          if not DetectedItem then
--            local TaskText = Task:GetName()
--            for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
--              Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2A task %s for %s removed.", TaskText, Mission:GetShortText() ), TaskGroup )
--            end
--            Task = self:RemoveTask( TaskIndex )
--          end
        end
      end

      -- Now that all obsolete tasks are removed, loop through the CSAR pilots.
      for CSARName, CSAR in pairs( self.CSAR ) do
      
        if not CSAR.Task then
          -- New CSAR Task
          local SetCargo = self:EvaluateCSAR( CSAR.PilotGroup )
          CSAR.Task = TASK_CARGO_CSAR:New( Mission, self.SetGroup, CSARName, SetCargo, CSAR.Briefing )
          CSAR.Task.TaskPrefix = CSAR.TaskPrefix -- We keep the TaskPrefix for further reference!
          Mission:AddTask( CSAR.Task )
          TaskReport:Add( CSARName )
          if CSAR.DeployZones then
            CSAR.Task:SetDeployZones( CSAR.DeployZones or {} )
          else
            CSAR.Task:SetDeployZones( self.DefaultDeployZones or {} )
          end
          
          -- Now broadcast the onafterCargoPickedUp event to the Task Cargo Dispatcher.
          function CSAR.Task.OnAfterCargoPickedUp( Task, From, Event, To, TaskUnit, Cargo )
            self:CargoPickedUp( Task, Task.TaskPrefix, TaskUnit, Cargo )
          end

          -- Now broadcast the onafterCargoDeployed event to the Task Cargo Dispatcher.
          function CSAR.Task.OnAfterCargoDeployed( Task, From, Event, To, TaskUnit, Cargo, DeployZone )
            self:CargoDeployed( Task, Task.TaskPrefix, TaskUnit, Cargo, DeployZone )
          end
          
        end
      end
      
      
      -- Now that all obsolete tasks are removed, loop through the Transport tasks.
      for TransportName, Transport in pairs( self.Transport ) do
        
        if not Transport.Task then
          -- New Transport Task
          Transport.Task = TASK_CARGO_TRANSPORT:New( Mission, self.SetGroup, TransportName, Transport.SetCargo, Transport.Briefing )
          Transport.Task.TaskPrefix = Transport.TaskPrefix -- We keep the TaskPrefix for further reference!
          Mission:AddTask( Transport.Task )
          TaskReport:Add( TransportName )
          function Transport.Task.OnEnterSuccess( Task, From, Event, To )
            self:Success( Task )
          end

          function Transport.Task.OnEnterCancelled( Task, From, Event, To )
            self:Cancelled( Task )
          end
          
          function Transport.Task.OnEnterFailed( Task, From, Event, To )
            self:Failed( Task )
          end

          function Transport.Task.OnEnterAborted( Task, From, Event, To )
            self:Aborted( Task )
          end

          -- Now broadcast the onafterCargoPickedUp event to the Task Cargo Dispatcher.
          function Transport.Task.OnAfterCargoPickedUp( Task, From, Event, To, TaskUnit, Cargo )
            self:CargoPickedUp( Task, Task.TaskPrefix, TaskUnit, Cargo )
          end

          -- Now broadcast the onafterCargoDeployed event to the Task Cargo Dispatcher.
          function Transport.Task.OnAfterCargoDeployed( Task, From, Event, To, TaskUnit, Cargo, DeployZone )
            self:CargoDeployed( Task, Task.TaskPrefix, TaskUnit, Cargo, DeployZone )
          end
          
        end
        
        if Transport.DeployZones then
          Transport.Task:SetDeployZones( Transport.DeployZones or {} )
        else
          Transport.Task:SetDeployZones( self.DefaultDeployZones or {} )
        end
        
      end
      
      
      -- TODO set menus using the HQ coordinator
      Mission:GetCommandCenter():SetMenu()

      local TaskText = TaskReport:Text(", ")
      
      for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
        if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" and not verbose then
          Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup )
        end
      end
      
    end
    
    return true
  end

end
--- **Tasking** - The TASK_Protect models tasks for players to protect or capture specific zones.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: MillerTime
-- 
-- ===
--   
-- @module Tasking.Task_Capture_Zone
-- @image MOOSE.JPG

do -- TASK_ZONE_GOAL

  --- The TASK_ZONE_GOAL class
  -- @type TASK_ZONE_GOAL
  -- @field Functional.ZoneGoal#ZONE_GOAL ZoneGoal
  -- @extends Tasking.Task#TASK

  --- # TASK_ZONE_GOAL class, extends @{Tasking.Task#TASK}
  -- 
  -- ![Banner Image](..\Images\deprecated.png)
  -- 
  -- The TASK_ZONE_GOAL class defines the task to protect or capture a protection zone. 
  -- The TASK_ZONE_GOAL is implemented using a @{Core.Fsm#FSM_TASK}, and has the following statuses:
  -- 
  --   * **None**: Start of the process
  --   * **Planned**: The A2G task is planned.
  --   * **Assigned**: The A2G task is assigned to a @{Wrapper.Group#GROUP}.
  --   * **Success**: The A2G task is successfully completed.
  --   * **Failed**: The A2G task has failed. This will happen if the player exists the task early, without communicating a possible cancellation to HQ.
  -- 
  -- ## Set the scoring of achievements in an A2G attack.
  -- 
  -- Scoring or penalties can be given in the following circumstances:
  -- 
  --   * @{#TASK_ZONE_GOAL.SetScoreOnDestroy}(): Set a score when a target in scope of the A2G attack, has been destroyed.
  --   * @{#TASK_ZONE_GOAL.SetScoreOnSuccess}(): Set a score when all the targets in scope of the A2G attack, have been destroyed.
  --   * @{#TASK_ZONE_GOAL.SetPenaltyOnFailed}(): Set a penalty when the A2G attack has failed.
  --
  -- # Developer Note
  -- 
  -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
  -- Therefore, this class is considered to be deprecated
  --
  -- @field #TASK_ZONE_GOAL
  TASK_ZONE_GOAL = {
    ClassName = "TASK_ZONE_GOAL",
  }
  
  --- Instantiates a new TASK_ZONE_GOAL.
  -- @param #TASK_ZONE_GOAL self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned.
  -- @param #string TaskName The name of the Task.
  -- @param Functional.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal
  -- @return #TASK_ZONE_GOAL self
  function TASK_ZONE_GOAL:New( Mission, SetGroup, TaskName, ZoneGoal, TaskType, TaskBriefing )
    local self = BASE:Inherit( self, TASK:New( Mission, SetGroup, TaskName, TaskType, TaskBriefing ) ) -- #TASK_ZONE_GOAL
    self:F()
  
    self.ZoneGoal = ZoneGoal
    self.TaskType = TaskType
    
    local Fsm = self:GetUnitProcess()
    

    Fsm:AddTransition( "Assigned", "StartMonitoring", "Monitoring" )
    Fsm:AddTransition( "Monitoring", "Monitor", "Monitoring", {} )
    Fsm:AddProcess( "Monitoring", "RouteToZone", ACT_ROUTE_ZONE:New(), {} )
    
    Fsm:AddTransition( "Rejected", "Reject", "Aborted" )
    Fsm:AddTransition( "Failed", "Fail", "Failed" )
    
    self:SetTargetZone( self.ZoneGoal:GetZone() )

    --- Test 
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task#TASK Task
    function Fsm:OnAfterAssigned( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      
      self:__StartMonitoring( 0.1 )
      self:__RouteToZone( 0.1 )
    end
    
    --- Test 
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task#TASK_ZONE_GOAL Task
    function Fsm:onafterStartMonitoring( TaskUnit, Task )
      self:F( { self } )
      self:__Monitor( 0.1 )
    end
    
    --- Monitor Loop
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task#TASK_ZONE_GOAL Task
    function Fsm:onafterMonitor( TaskUnit, Task )
      self:F( { self } )
      self:__Monitor( 15 )
    end
    
    --- Test 
    -- @param #FSM_PROCESS self
    -- @param Wrapper.Unit#UNIT TaskUnit
    -- @param Tasking.Task_A2G#TASK_ZONE_GOAL Task
    function Fsm:onafterRouteTo( TaskUnit, Task )
      self:F( { TaskUnit = TaskUnit, Task = Task and Task:GetClassNameAndID() } )
      -- Determine the first Unit from the self.TargetSetUnit
      
      if Task:GetTargetZone( TaskUnit ) then
        self:__RouteToZone( 0.1 )
      end
    end

    return self
 
  end

  -- @param #TASK_ZONE_GOAL self
  -- @param Functional.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal Engine.
  function TASK_ZONE_GOAL:SetProtect( ZoneGoal )
  
    self.ZoneGoal = ZoneGoal -- Functional.ZoneGoal#ZONE_GOAL
  end
   

  
  -- @param #TASK_ZONE_GOAL self
  function TASK_ZONE_GOAL:GetPlannedMenuText()
    return self:GetStateString() .. " - " .. self:GetTaskName() .. " ( " .. self.ZoneGoal:GetZoneName() .. " )"
  end

  
  -- @param #TASK_ZONE_GOAL self
  -- @param Core.Zone#ZONE_BASE TargetZone The Zone object where the Target is located on the map.
  -- @param Wrapper.Unit#UNIT TaskUnit
  function TASK_ZONE_GOAL:SetTargetZone( TargetZone, TaskUnit )
  
    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteZone = ProcessUnit:GetProcess( "Monitoring", "RouteToZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE
    ActRouteZone:SetZone( TargetZone )
  end
   

  -- @param #TASK_ZONE_GOAL self
  -- @param Wrapper.Unit#UNIT TaskUnit
  -- @return Core.Zone#ZONE_BASE The Zone object where the Target is located on the map.
  function TASK_ZONE_GOAL:GetTargetZone( TaskUnit )

    local ProcessUnit = self:GetUnitProcess( TaskUnit )

    local ActRouteZone = ProcessUnit:GetProcess( "Monitoring", "RouteToZone" ) -- Actions.Act_Route#ACT_ROUTE_ZONE
    return ActRouteZone:GetZone()
  end

  function TASK_ZONE_GOAL:SetGoalTotal( GoalTotal )
  
    self.GoalTotal = GoalTotal
  end

  function TASK_ZONE_GOAL:GetGoalTotal()
  
    return self.GoalTotal
  end

end 


do -- TASK_CAPTURE_ZONE

  --- The TASK_CAPTURE_ZONE class
  -- @type TASK_CAPTURE_ZONE
  -- @field Functional.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal
  -- @extends #TASK_ZONE_GOAL

  --- # TASK_CAPTURE_ZONE class, extends @{Tasking.Task_Capture_Zone#TASK_ZONE_GOAL}
  -- 
  -- The TASK_CAPTURE_ZONE class defines an Suppression or Extermination of Air Defenses task for a human player to be executed.
  -- These tasks are important to be executed as they will help to achieve air superiority at the vicinity.
  -- 
  -- The TASK_CAPTURE_ZONE is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create SEAD tasks 
  -- based on detected enemy ground targets.
  -- 
  -- @field #TASK_CAPTURE_ZONE
  TASK_CAPTURE_ZONE = {
    ClassName = "TASK_CAPTURE_ZONE",
  }
  

  --- Instantiates a new TASK_CAPTURE_ZONE.
  -- @param #TASK_CAPTURE_ZONE self
  -- @param Tasking.Mission#MISSION Mission
  -- @param Core.Set#SET_GROUP SetGroup The set of groups for which the Task can be assigned.
  -- @param #string TaskName The name of the Task.
  -- @param Functional.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoalCoalition
  -- @param #string TaskBriefing The briefing of the task.
  -- @return #TASK_CAPTURE_ZONE self
  function TASK_CAPTURE_ZONE:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, TaskBriefing)
    local self = BASE:Inherit( self, TASK_ZONE_GOAL:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, "CAPTURE", TaskBriefing ) ) -- #TASK_CAPTURE_ZONE
    self:F()
    
    Mission:AddTask( self )
    
    self.TaskCoalition = ZoneGoalCoalition:GetCoalition()
    self.TaskCoalitionName = ZoneGoalCoalition:GetCoalitionName()
    self.TaskZoneName = ZoneGoalCoalition:GetZoneName()
    
    ZoneGoalCoalition:MonitorDestroyedUnits()
    
    self:SetBriefing( 
      TaskBriefing or 
      "Capture Zone " .. self.TaskZoneName
    )
    
    self:UpdateTaskInfo( true )
    
    self:SetGoal( self.ZoneGoal.Goal )

    return self
  end 


  --- Instantiates a new TASK_CAPTURE_ZONE.
  -- @param #TASK_CAPTURE_ZONE self
  function TASK_CAPTURE_ZONE:UpdateTaskInfo( Persist ) 
  
    Persist = Persist or false
  
    local ZoneCoordinate = self.ZoneGoal:GetZone():GetCoordinate() 
    self.TaskInfo:AddTaskName( 0, "MSOD", Persist )
    self.TaskInfo:AddCoordinate( ZoneCoordinate, 1, "SOD", Persist )
--    self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD", Persist )
--    self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD", Persist )
    local SetUnit = self.ZoneGoal:GetScannedSetUnit()
    local ThreatLevel, ThreatText = SetUnit:CalculateThreatLevelA2G()
    local ThreatCount = SetUnit:Count()
    self.TaskInfo:AddThreat( ThreatText, ThreatLevel, 20, "MOD", Persist )
    self.TaskInfo:AddInfo( "Remaining Units", ThreatCount, 21, "MOD", Persist, true)
    
    if self.Dispatcher then
      local DefenseTaskCaptureDispatcher = self.Dispatcher:GetDefenseTaskCaptureDispatcher() -- Tasking.Task_Capture_Dispatcher#TASK_CAPTURE_DISPATCHER
      
      if DefenseTaskCaptureDispatcher then
        -- Loop through all zones of the player Defenses, and check which zone has an assigned task!
        -- The Zones collection contains a Task. This Task is checked if it is assigned.
        -- If Assigned, then this task will be the task that is the closest to the defense zone.
        for TaskName, CaptureZone in pairs( DefenseTaskCaptureDispatcher.Zones or {} ) do
          local Task = CaptureZone.Task -- Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE
          if Task  and Task:IsStateAssigned() then -- We also check assigned.
            -- Now we register the defense player zone information to the task report.
            self.TaskInfo:AddInfo( "Defense Player Zone", Task.ZoneGoal:GetName(), 30, "MOD", Persist ) 
            self.TaskInfo:AddCoordinate( Task.ZoneGoal:GetZone():GetCoordinate(), 31, "MOD", Persist, false, "Defense Player Coordinate" )
          end
        end
      end
      local DefenseAIA2GDispatcher = self.Dispatcher:GetDefenseAIA2GDispatcher() -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER
      
      if DefenseAIA2GDispatcher then
        -- Loop through all the tasks of the AI Defenses, and check which zone is involved in the defenses and is active!
        for Defender, Task in pairs( DefenseAIA2GDispatcher:GetDefenderTasks() or {} ) do
          local DetectedItem = DefenseAIA2GDispatcher:GetDefenderTaskTarget( Defender )
          if DetectedItem then
            local DetectedZone = DefenseAIA2GDispatcher.Detection:GetDetectedItemZone( DetectedItem )
            if DetectedZone then
              self.TaskInfo:AddInfo( "Defense AI Zone", DetectedZone:GetName(), 40, "MOD", Persist  )
              self.TaskInfo:AddCoordinate( DetectedZone:GetCoordinate(), 41, "MOD", Persist, false, "Defense AI Coordinate" )
            end
          end
        end
      end
    end
    
  end
    

  function TASK_CAPTURE_ZONE:ReportOrder( ReportGroup ) 

    local Coordinate = self.TaskInfo:GetCoordinate()
    local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate )
    
    return Distance
  end
  
  
  -- @param #TASK_CAPTURE_ZONE self
  -- @param Wrapper.Unit#UNIT TaskUnit
  function TASK_CAPTURE_ZONE:OnAfterGoal( From, Event, To, PlayerUnit, PlayerName )
  
    self:F( { PlayerUnit = PlayerUnit, Achieved = self.ZoneGoal.Goal:IsAchieved() } )
    
    if self.ZoneGoal then
      if self.ZoneGoal.Goal:IsAchieved() then
        local TotalContributions = self.ZoneGoal.Goal:GetTotalContributions()
        local PlayerContributions = self.ZoneGoal.Goal:GetPlayerContributions()
        self:F( { TotalContributions = TotalContributions, PlayerContributions = PlayerContributions } )
        for PlayerName, PlayerContribution in pairs( PlayerContributions ) do
           local Scoring = self:GetScoring()
           if Scoring then
             Scoring:_AddMissionGoalScore( self.Mission, PlayerName, "Zone " .. self.ZoneGoal:GetZoneName() .." captured", PlayerContribution * 200 / TotalContributions )
           end
        end
        self:Success()
      end
    end
    
    self:__Goal( -10, PlayerUnit, PlayerName )
  end

  --- This function is called from the @{Tasking.CommandCenter#COMMANDCENTER} to determine the method of automatic task selection.
  -- @param #TASK_CAPTURE_ZONE self
  -- @param #number AutoAssignMethod The method to be applied to the task.
  -- @param Tasking.CommandCenter#COMMANDCENTER CommandCenter The command center.
  -- @param Wrapper.Group#GROUP TaskGroup The player group.
  function TASK_CAPTURE_ZONE:GetAutoAssignPriority( AutoAssignMethod, CommandCenter, TaskGroup, AutoAssignReference )
  
    if     AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Random then
      return math.random( 1, 9 )
    elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Distance then
      local Coordinate = self.TaskInfo:GetCoordinate()
      local Distance = Coordinate:Get2DDistance( CommandCenter:GetPositionable():GetCoordinate() )
      return math.floor( Distance )
    elseif AutoAssignMethod == COMMANDCENTER.AutoAssignMethods.Priority then
      return 1
    end

    return 0
  end

end

--- **Tasking** - Creates and manages player TASK_ZONE_CAPTURE tasks.
-- 
-- The **TASK_CAPTURE_DISPATCHER** allows you to setup various tasks for let human
-- players capture zones in a co-operation effort. 
--  
-- The dispatcher will implement for you mechanisms to create capture zone tasks:
--  
--   * As setup by the mission designer.
--   * Dynamically capture zone tasks.
--   
-- 
--   
-- **Specific features:**
-- 
--   * Creates a task to capture zones and achieve mission goals.
--   * Orchestrate the task flow, so go from Planned to Assigned to Success, Failed or Cancelled.
--   * Co-operation tasking, so a player joins a group of players executing the same task.
-- 
-- 
-- **A complete task menu system to allow players to:**
--   
--   * Join the task, abort the task.
--   * Mark the location of the zones to capture on the map.
--   * Provide details of the zones.
--   * Route to the zones.
--   * Display the task briefing.
--   
--   
-- **A complete mission menu system to allow players to:**
--   
--   * Join a task, abort the task.
--   * Display task reports.
--   * Display mission statistics.
--   * Mark the task locations on the map.
--   * Provide details of the zones.
--   * Display the mission briefing.
--   * Provide status updates as retrieved from the command center.
--   * Automatically assign a random task as part of a mission.
--   * Manually assign a specific task as part of a mission.
--   
--   
--  **A settings system, using the settings menu:**
--  
--   * Tweak the duration of the display of messages.
--   * Switch between metric and imperial measurement system.
--   * Switch between coordinate formats used in messages: BR, BRA, LL DMS, LL DDM, MGRS.
--   * Various other options.
--
-- # Developer Note
-- 
-- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE
-- Therefore, this class is considered to be deprecated
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Tasking.Task_Capture_Dispatcher
-- @image MOOSE.JPG

do -- TASK_CAPTURE_DISPATCHER

  --- TASK_CAPTURE_DISPATCHER class.
  -- @type TASK_CAPTURE_DISPATCHER
  -- @extends Tasking.Task_Manager#TASK_MANAGER
  -- @field TASK_CAPTURE_DISPATCHER.ZONE ZONE

  -- @type TASK_CAPTURE_DISPATCHER.CSAR
  -- @field Wrapper.Unit#UNIT PilotUnit
  -- @field Tasking.Task#TASK Task
  

  --- Implements the dynamic dispatching of capture zone tasks.
  -- 
  -- ![Banner Image](..\Images\deprecated.png)
  -- 
  -- The **TASK_CAPTURE_DISPATCHER** allows you to setup various tasks for let human
  -- players capture zones in a co-operation effort. 
  --  
  -- Let's explore **step by step** how to setup the task capture zone dispatcher.
  -- 
  -- # 1. Setup a mission environment.
  -- 
  -- It is easy, as it works just like any other task setup, so setup a command center and a mission.
  -- 
  -- ## 1.1. Create a command center.
  -- 
  -- First you need to create a command center using the @{Tasking.CommandCenter#COMMANDCENTER.New}() constructor.
  -- The command assumes that you´ve setup a group in the mission editor with the name HQ.
  -- This group will act as the command center object.
  -- It is a good practice to mark this group as invisible and invulnerable.
  -- 
  --     local CommandCenter = COMMANDCENTER
  --        :New( GROUP:FindByName( "HQ" ), "HQ" ) -- Create the CommandCenter.
  --     
  -- ## 1.2. Create a mission.
  -- 
  -- Tasks work in a **mission**, which groups these tasks to achieve a joint **mission goal**. A command center can **govern multiple missions**.
  -- 
  -- Create a new mission, using the @{Tasking.Mission#MISSION.New}() constructor.
  -- 
  --     -- Declare the Mission for the Command Center.
  --     local Mission = MISSION
  --       :New( CommandCenter, 
  --             "Overlord", 
  --             "High", 
  --             "Capture the blue zones.", 
  --             coalition.side.RED 
  --           ) 
  -- 
  -- 
  -- # 2. Dispatch a **capture zone** task.
  -- 
  -- So, now that we have a command center and a mission, we now create the capture zone task.
  -- We create the capture zone task using the @{#TASK_CAPTURE_DISPATCHER.AddCaptureZoneTask}() constructor.
  -- 
  -- ## 2.1. Create the capture zones.
  -- 
  -- Because a capture zone task will not generate the capture zones, you'll need to create them first.
  -- 
  --     
  --     -- We define here a capture zone; of the type ZONE_CAPTURE_COALITION.
  --     -- The zone to be captured has the name Alpha, and was defined in the mission editor as a trigger zone.
  --     CaptureZone = ZONE:New( "Alpha" )
  --     CaptureZoneCoalitionApha = ZONE_CAPTURE_COALITION:New( CaptureZone, coalition.side.RED )
  -- 
  -- ## 2.2. Create a set of player groups.
  --    
  -- What is also needed, is to have a set of @{Wrapper.Group}s defined that contains the clients of the players.
  -- 
  --     -- Allocate the player slots, which must be aircraft (airplanes or helicopters), that can be manned by players.
  --     -- We use the method FilterPrefixes to filter those player groups that have client slots, as defined in the mission editor.
  --     -- In this example, we filter the groups where the name starts with "Blue Player", which captures the blue player slots.
  --     local PlayerGroupSet = SET_GROUP:New():FilterPrefixes( "Blue Player" ):FilterStart()
  -- 
  -- ## 2.3. Setup the capture zone task.
  -- 
  -- First, we need to create a TASK_CAPTURE_DISPATCHER object.
  -- 
  --     TaskCaptureZoneDispatcher = TASK_CAPTURE_DISPATCHER:New( Mission, PilotGroupSet )
  -- 
  -- So, the variable `TaskCaptureZoneDispatcher` will contain the object of class TASK_CAPTURE_DISPATCHER, 
  -- which will allow you to dispatch capture zone tasks:
  --  
  --   * for mission `Mission`, as was defined in section 1.2.
  --   * for the group set `PilotGroupSet`, as was defined in section 2.2.
  -- 
  -- Now that we have `TaskDispatcher` object, we can now **create the TaskCaptureZone**, using the @{#TASK_CAPTURE_DISPATCHER.AddCaptureZoneTask}() method!
  -- 
  --     local TaskCaptureZone = TaskCaptureZoneDispatcher:AddCaptureZoneTask( 
  --       "Capture zone Alpha", 
  --       CaptureZoneCoalitionAlpha, 
  --       "Fly to zone Alpha and eliminate all enemy forces to capture it." )
  -- 
  -- As a result of this code, the `TaskCaptureZone` (returned) variable will contain an object of @{#TASK_CAPTURE_ZONE}!
  -- We pass to the method the title of the task, and the `CaptureZoneCoalitionAlpha`, which is the zone to be captured, as defined in section 2.1!
  -- This returned `TaskCaptureZone` object can now be used to setup additional task configurations, or to control this specific task with special events.
  -- 
  -- And you're done! As you can see, it is a small bit of work, but the reward is great.
  -- And, because all this is done using program interfaces, you can easily build a mission to capture zones yourself!
  -- Based on various events happening within your mission, you can use the above methods to create new capture zones, 
  -- and setup a new capture zone task and assign it to a group of players, while your mission is running!
  -- 
  --     
  -- 
  -- @field #TASK_CAPTURE_DISPATCHER
  TASK_CAPTURE_DISPATCHER = {
    ClassName = "TASK_CAPTURE_DISPATCHER",
    Mission = nil,
    Tasks = {},
    Zones = {},
    ZoneCount = 0,
  }


  
  TASK_CAPTURE_DISPATCHER.AI_A2G_Dispatcher = nil -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER
  
  --- TASK_CAPTURE_DISPATCHER constructor.
  -- @param #TASK_CAPTURE_DISPATCHER self
  -- @param Tasking.Mission#MISSION Mission The mission for which the task dispatching is done.
  -- @param Core.Set#SET_GROUP SetGroup The set of groups that can join the tasks within the mission.
  -- @return #TASK_CAPTURE_DISPATCHER self
  function TASK_CAPTURE_DISPATCHER:New( Mission, SetGroup )
  
    -- Inherits from DETECTION_MANAGER
    local self = BASE:Inherit( self, TASK_MANAGER:New( SetGroup ) ) -- #TASK_CAPTURE_DISPATCHER
    
    self.Mission = Mission
    self.FlashNewTask = false
    
    self:AddTransition( "Started", "Assign", "Started" )
    self:AddTransition( "Started", "ZoneCaptured", "Started" )
    
    self:__StartTasks( 5 )
    
    return self
  end


  --- Link a task capture dispatcher from the other coalition to understand its plan for defenses.
  -- This is used for the tactical overview, so the players also know the zones attacked by the other coalition!
  -- @param #TASK_CAPTURE_DISPATCHER self
  -- @param #TASK_CAPTURE_DISPATCHER DefenseTaskCaptureDispatcher
  function TASK_CAPTURE_DISPATCHER:SetDefenseTaskCaptureDispatcher( DefenseTaskCaptureDispatcher )
  
    self.DefenseTaskCaptureDispatcher = DefenseTaskCaptureDispatcher
  end


  --- Get the linked task capture dispatcher from the other coalition to understand its plan for defenses.
  -- This is used for the tactical overview, so the players also know the zones attacked by the other coalition!
  -- @param #TASK_CAPTURE_DISPATCHER self
  -- @return #TASK_CAPTURE_DISPATCHER
  function TASK_CAPTURE_DISPATCHER:GetDefenseTaskCaptureDispatcher()
  
    return self.DefenseTaskCaptureDispatcher
  end


  --- Link an AI A2G dispatcher from the other coalition to understand its plan for defenses.
  -- This is used for the tactical overview, so the players also know the zones attacked by the other AI A2G dispatcher!
  -- @param #TASK_CAPTURE_DISPATCHER self
  -- @param AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER DefenseAIA2GDispatcher
  function TASK_CAPTURE_DISPATCHER:SetDefenseAIA2GDispatcher( DefenseAIA2GDispatcher )
  
    self.DefenseAIA2GDispatcher = DefenseAIA2GDispatcher
  end


  --- Get the linked AI A2G dispatcher from the other coalition to understand its plan for defenses.
  -- This is used for the tactical overview, so the players also know the zones attacked by the AI A2G dispatcher!
  -- @param #TASK_CAPTURE_DISPATCHER self
  -- @return AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER
  function TASK_CAPTURE_DISPATCHER:GetDefenseAIA2GDispatcher()
  
    return self.DefenseAIA2GDispatcher
  end


  --- Add a capture zone task.
  -- @param #TASK_CAPTURE_DISPATCHER self
  -- @param #string TaskPrefix (optional) The prefix of the capture zone task. 
  -- If no TaskPrefix is given, then "Capture" will be used as the TaskPrefix. 
  -- The TaskPrefix will be appended with a . + a number of 3 digits, if the TaskPrefix already exists in the task collection.
  -- @param Functional.ZoneCaptureCoalition#ZONE_CAPTURE_COALITION CaptureZone The zone of the coalition to be captured as the task goal.
  -- @param #string Briefing The briefing of the task to be shown to the player.
  -- @return Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE
  -- @usage
  -- 
  --  
  function TASK_CAPTURE_DISPATCHER:AddCaptureZoneTask( TaskPrefix, CaptureZone, Briefing )

    local TaskName = TaskPrefix or "Capture"
    if self.Zones[TaskName] then
      self.ZoneCount = self.ZoneCount + 1
      TaskName = string.format( "%s.%03d", TaskName, self.ZoneCount )
    end
    
    self.Zones[TaskName] = {} 
    self.Zones[TaskName].CaptureZone = CaptureZone
    self.Zones[TaskName].Briefing = Briefing
    self.Zones[TaskName].Task = nil
    self.Zones[TaskName].TaskPrefix = TaskPrefix
    
    self:ManageTasks()
    
    return self.Zones[TaskName] and self.Zones[TaskName].Task
  end


  --- Link an AI_A2G_DISPATCHER to the TASK_CAPTURE_DISPATCHER.
  -- @param #TASK_CAPTURE_DISPATCHER self
  -- @param AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER AI_A2G_Dispatcher The AI Dispatcher to be linked to the tasking. 
  -- @return Tasking.Task_Capture_Zone#TASK_CAPTURE_ZONE
  function TASK_CAPTURE_DISPATCHER:Link_AI_A2G_Dispatcher( AI_A2G_Dispatcher )

    self.AI_A2G_Dispatcher = AI_A2G_Dispatcher -- AI.AI_A2G_Dispatcher#AI_A2G_DISPATCHER
    AI_A2G_Dispatcher.Detection:LockDetectedItems()

    return self
  end


  --- Assigns tasks to the @{Core.Set#SET_GROUP}.
  -- @param #TASK_CAPTURE_DISPATCHER self
  -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop.
  function TASK_CAPTURE_DISPATCHER:ManageTasks()
    self:F()
  
    local AreaMsg = {}
    local TaskMsg = {}
    local ChangeMsg = {}
    
    local Mission = self.Mission
    
    if Mission:IsIDLE() or Mission:IsENGAGED() then
    
      local TaskReport = REPORT:New()
      
      -- Checking the task queue for the dispatcher, and removing any obsolete task!
      for TaskIndex, TaskData in pairs( self.Tasks ) do
        local Task = TaskData -- Tasking.Task#TASK
        if Task:IsStatePlanned() then
        -- Here we need to check if the pilot is still existing.
--            Task = self:RemoveTask( TaskIndex )
        end
        
      end

      -- Now that all obsolete tasks are removed, loop through the Zone tasks.
      for TaskName, CaptureZone in pairs( self.Zones ) do
        
        if not CaptureZone.Task then
          -- New Transport Task
          CaptureZone.Task = TASK_CAPTURE_ZONE:New( Mission, self.SetGroup, TaskName, CaptureZone.CaptureZone, CaptureZone.Briefing )
          CaptureZone.Task.TaskPrefix = CaptureZone.TaskPrefix -- We keep the TaskPrefix for further reference!
          Mission:AddTask( CaptureZone.Task )
          TaskReport:Add( TaskName )
          
          -- Link the Task Dispatcher to the capture zone task, because it is used on the UpdateTaskInfo.
          CaptureZone.Task:SetDispatcher( self )
          CaptureZone.Task:UpdateTaskInfo()

          function CaptureZone.Task.OnEnterAssigned( Task, From, Event, To )
            if self.AI_A2G_Dispatcher then
              self.AI_A2G_Dispatcher:Unlock( Task.TaskZoneName ) -- This will unlock the zone to be defended by AI.
            end        
            CaptureZone.Task:UpdateTaskInfo()
            CaptureZone.Task.ZoneGoal.Attacked = true
          end
          
          function CaptureZone.Task.OnEnterSuccess( Task, From, Event, To )
            --self:Success( Task )
            if self.AI_A2G_Dispatcher then
              self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI.
            end
            CaptureZone.Task:UpdateTaskInfo()
            CaptureZone.Task.ZoneGoal.Attacked = false
          end

          function CaptureZone.Task.OnEnterCancelled( Task, From, Event, To )
            self:Cancelled( Task )
            if self.AI_A2G_Dispatcher then
              self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI.
            end
            CaptureZone.Task:UpdateTaskInfo()
            CaptureZone.Task.ZoneGoal.Attacked = false
          end
            
          function CaptureZone.Task.OnEnterFailed( Task, From, Event, To )
            self:Failed( Task )
            if self.AI_A2G_Dispatcher then
              self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI.
            end
            CaptureZone.Task:UpdateTaskInfo()
            CaptureZone.Task.ZoneGoal.Attacked = false
          end

          function CaptureZone.Task.OnEnterAborted( Task, From, Event, To )
            self:Aborted( Task )
            if self.AI_A2G_Dispatcher then
              self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI.
            end
            CaptureZone.Task:UpdateTaskInfo()
            CaptureZone.Task.ZoneGoal.Attacked = false
          end

          -- Now broadcast the onafterCargoPickedUp event to the Task Cargo Dispatcher.
          function CaptureZone.Task.OnAfterCaptured( Task, From, Event, To, TaskUnit )
            self:Captured( Task, Task.TaskPrefix, TaskUnit )
            if self.AI_A2G_Dispatcher then
              self.AI_A2G_Dispatcher:Lock( Task.TaskZoneName ) -- This will lock the zone from being defended by AI.
            end
            CaptureZone.Task:UpdateTaskInfo()
            CaptureZone.Task.ZoneGoal.Attacked = false
          end

        end
        
      end
      
      
      -- TODO set menus using the HQ coordinator
      Mission:GetCommandCenter():SetMenu()

      local TaskText = TaskReport:Text(", ")
      
      for TaskGroupID, TaskGroup in pairs( self.SetGroup:GetSet() ) do
        if ( not Mission:IsGroupAssigned(TaskGroup) ) and TaskText ~= "" and ( not self.FlashNewTask ) then
          Mission:GetCommandCenter():MessageToGroup( string.format( "%s has tasks %s. Subscribe to a task using the radio menu.", Mission:GetShortText(), TaskText ), TaskGroup )
        end
      end
      
    end
    
    return true
  end

end
--- GLOBALS: The order of the declarations is important here. Don't touch it.

--- Declare the event dispatcher based on the EVENT class
_EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT

--- Declare the timer dispatcher based on the SCHEDULEDISPATCHER class
_SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.ScheduleDispatcher#SCHEDULEDISPATCHER

--- Declare the main database object, which is used internally by the MOOSE classes.
_DATABASE = DATABASE:New() -- Core.Database#DATABASE

--- Settings
_SETTINGS = SETTINGS:Set() -- Core.Settings#SETTINGS
_SETTINGS:SetPlayerMenuOn()

--- Register cargos.
_DATABASE:_RegisterCargos()

--- Register zones.
_DATABASE:_RegisterZones()
_DATABASE:_RegisterAirbases()

--- Check if os etc is available.
BASE:I("Checking de-sanitization of os, io and lfs:")
local __na = false
if os then
  BASE:I("- os available")
else
  BASE:I("- os NOT available! Some functions may not work.")
  __na = true
end
if io then
  BASE:I("- io available")
else
  BASE:I("- io NOT available! Some functions may not work.")
  __na = true
end
if lfs then
  BASE:I("- lfs available")
else
  BASE:I("- lfs NOT available! Some functions may not work.")
  __na = true
end
if __na then
  BASE:I("Check <DCS install folder>/Scripts/MissionScripting.lua and comment out the lines with sanitizeModule(''). Use at your own risk!)")
end
BASE.ServerName = "Unknown"
if lfs and loadfile then
  local serverfile = lfs.writedir() .. 'Config/serverSettings.lua'
  if UTILS.FileExists(serverfile) then
    loadfile(serverfile)()
    if cfg and cfg.name then
      BASE.ServerName = cfg.name
    end
  end
  BASE.ServerName = BASE.ServerName or "Unknown"
  BASE:I("Server Name: " .. tostring(BASE.ServerName))
end
BASE:TraceOnOff( false )
env.info( '*** MOOSE INCLUDE END *** ' )
